这篇在介绍实例选项的ts写法时,会给出js的写法以作对比,同时也会给出选项的风格指南,保持良好的代码规范。
基本用法
Vue.extend
<script>
import Vue from 'vue'
const Component = Vue.extend({
data: () => ({...}),
methods: {...},
})
</script>
Vue.extend学习成本最低,现有写法的基础上,学习成本几乎为0。但是对ts的支持不佳。这就意味着会出现丢失代码提示、类型检查、编译报错等问题。
vue-class-component(推荐)
<script lang="ts">
// vue-property-decorator 库强依赖于 vue-class-component 库
import { Component, Vue } from 'vue-property-decorator'
// @Component 修饰符注明了此类为一个 Vue 组件
@Component({
// 所有的组件选项都可以放在这里,与原写法类似
components: {...},
})
export default class ComponentName extends Vue {
// 初始数据可以直接声明为实例的 property
public message: string = 'Hello!'
// 组件方法也可以直接声明为实例的方法
public onClick (): void {
window.alert(this.message)
}
// ...
}
</script>
Vue与typescript结合的第二种方法,推荐使用该写法。
基于 vue-property-decorator 和 vuex-class 提供的装饰器,可以很好的与typescript结合。
注: vue-property-decorator
强依赖于 vue-class-component
vue-class-component 写法区别与风格指南
下面介绍 .vue 文件中 vue-class-component 风格的ts写法与普通js写法的区别。同时介绍应该注意的事项和风格指南。
script标签
示例 - js:
<script>
</script>
示例 - ts:
<script lang="ts">
</script>
name 组件名
示例 - js:
// js
export default {
name: "TodoList",
}
示例 - ts:
// ts
import { Component, Vue } from "vue-property-decorator";
@Component
export default class TodoList extends Vue {}
风格指南:
- name属性应该采用大驼峰 PascalCase 的形式命名。
- ts写法在导出class时一定要带上命名,不要写匿名组件,在编写指令或者其他组件时,可能会遇到不可预计的影响
components 引入组件
示例 - js:
```javascript // js import HelloWorld from “@/components/HelloWorld.vue”;
export default { components: { HelloWorld } }
<a name="jbrfo"></a>
### 示例 - ts:
```typescript
// ts
import { Component, Vue } from "vue-property-decorator";
// 注意不能省略.vue的后缀。因为在shims-vue.d.ts中声明扩展的模块是 "*.vue"
import HelloWorld from "@/components/HelloWorld.vue";
@Component({
components: {
HelloWorld
}
})
export default class App extends Vue {}
导入的组件放在 @Component 的装饰器中。
data 组件数据
示例 - js:
// js
export default {
data: () => ({
user: {
name: 'Li Lei',
age: 18,
},
loading: false,
list: []
})
}
示例 - ts:
// ts
import { Component, Vue } from "vue-property-decorator";
@Component
export default class App extends Vue {
public user = {
name: 'Li Lei',
age: 18,
}
public loading = false
public list: unknown[] = []
}
风格指南:
在ts的类中,不写public默认为public。建议还是都写上,便于一眼区分类别。至于public、private、readonly等在何时使用,在后面会讲到。
computed 计算属性
示例 - js:
// js
export default {
data: () => ({
num1: 1,
num2: 1,
}),
computed: {
// 普通计算属性
totalCount() {
return this.num1 + this.num2
},
// get、set计算属性
numCalc: {
get() {
return this.num2 + 10
},
set(val) {
this.num2 = val - 10
},
},
},
}
示例 - ts:
// ts
import { Component, Vue } from "vue-property-decorator";
@Component
export default class App extends Vue {
public num1 = 1
public num2 = 1
// 普通计算属性
private get totalCount() {
return this.num1 + this.num2
}
// get、set版计算属性
private get numCalc() {
return this.num2 + 10
}
private set numCalc(val) {
this.num2 = val - 10
}
}
风格指南:
计算属性主要是一个名词,因此不要用动词作为函数的命名。
推荐如:totalCount
, fullName
等
methods 方法
示例 - js:
// js
export default {
methods: {
consoleNumber() {
console.log(123)
},
},
}
示例 - ts:
// ts
@Component
export default class App extends Vue {
public consoleNumber() {
console.log(123)
}
}
filters 过滤器
示例 - js:
// js
export default {
filters: {
formatValue(val) {
return val + 10
},
},
}
示例 - ts:
// ts
@Component({
filters: {
formatValue(val) {
return val + 10
},
},
})
export default class App extends Vue {}
风格指南:
filters的函数命名推荐为 format + 要过滤的val。
watch 监听
示例 - js:
export default {
data: () => ({
num: 10,
user: {
age: 10,
},
}),
watch: {
num(newValue, oldValue) {
console.log('newValue', newValue)
console.log('oldValue', oldValue)
},
'user.age': function(newValue, oldValue) {
console.log('newValue', newValue)
console.log('oldValue', oldValue)
},
user: {
immediate: true,
deep: true,
handler(newValue, oldValue) {
console.log('newValue', newValue)
console.log('oldValue', oldValue)
},
},
},
}
示例 - ts:
// ts
import { Component, Vue, Watch } from 'vue-property-decorator'
@Component
export default class App extends Vue {
public num = 10
public user = {
age: 10,
}
@Watch('num')
onNumChange(newVal: number, oldVal: number) {
console.log('newVal', newVal)
console.log('oldVal', oldVal)
}
@Watch('user.age')
onUserAgeChange(newVal: number, oldVal: number) {
console.log('newVal', newVal)
console.log('oldVal', oldVal)
}
@Watch('user', { immediate: true, deep: true })
onUserChange(newVal: { age: number }, oldVal: { age: number }) {
console.log('oldVal', oldVal)
console.log('newVal', newVal)
}
}
风格指南:
watch需要依赖于装饰器。@Watch()第一个参数为要监听的属性。第二个参数是一个object,用于设置deep等参数。
下面紧跟一个函数,函数的命名推荐使用小驼峰,以on开头,Change结尾:onXXXChange
Prop
示例 - js:
// js
export default {
props: {
propA: { type: Number, default: 1 },
propB: { type: [String, Number], default: '' },
propC: { type: [Object, Array], default: () => ({}) },
},
}
示例 - ts:
// ts
import { Component, Vue, Prop } from 'vue-property-decorator'
@Component
export default class App extends Vue {
@Prop({ type: Number, default: 1 })
readonly propA!: number
@Prop({ type: [String, Number], default: '' })
readonly propB!: string | number
@Prop({ type: [Object, Array], default: () => ({}) })
readonly propC!: object | unknown[]
}
风格指南:
- @Props装饰器里的参数与原Vue普通的写法一致
- 声明prop字段前要加上
readonly
,以保持Vue中prop是单向数据流,不允许直接赋值与修改 prop装饰器内必须添加default默认值,声明时无需再赋值,后面要加上感叹号
!
以告知ts编译器当前属性是非空的。Emit 自定义事件
示例1 - js:
export default {
data() {
return {
count: 0,
}
},
methods: {
addToCount(n) {
this.count += n
// 将addToCount转成add-to-count
this.$emit('add-to-count', n)
},
resetCount() {
this.count = 0
this.$emit('reset')
},
returnValue() {
this.$emit('return-value', 10)
},
onInputChange(e) {
this.$emit('on-input-change', e.target.value)
},
handlePromise() {
const promise = new Promise((resolve) => {
setTimeout(() => {
resolve(20)
}, 0)
})
promise.then((value) => {
this.$emit('promise', value)
})
},
},
}
示例1 - ts:
```typescript import { Vue, Component, Emit } from ‘vue-property-decorator’
@Component export default class YourComponent extends Vue { count = 0
@Emit() addToCount(n: number) { this.count += n }
@Emit(‘reset’) resetCount() { this.count = 0 }
@Emit() returnValue() { return 10 }
@Emit() onInputChange(e) { return e.target.value }
@Emit() handlePromise() { return new Promise((resolve) => { setTimeout(() => { resolve(20) }, 0) }) } }
- @Emit装饰器接收一个可选参数,作为事件名;如果没有提供这个参数,$emit会将回调函数的camelCase(驼峰式)转为kebab-case(短横线命名),并将其作为事件名
- @Emit会将回调函数的返回值作为第二个参数,如果返回值是一个Promise对象,$emit会将Promise对象状态为resolved之后触发
- @Emit的回调函数的参数,会放在其返回值之后,一起被$emit当作参数使用
- @Emit装饰器可以选择不传事件名,不返回处理的值,在里面添加业务处理。
但是为了能够让emit事件更加的清晰,以及与组件内业务代码的解耦,推荐如下写法:
<a name="sDv3c"></a>
### 更好地写法:
```typescript
import { Component, Vue, Emit } from 'vue-property-decorator'
@Component
export default class App extends Vue {
public count = 1
@Emit('add-to-count')
public emitAddToCount(val: number) {
return val
}
@Emit('reset')
public emitReset() {
return
}
@Emit('return-value')
public emitReturnValue(val: number) {
return val
}
@Emit('on-input-change')
public emitOnInputChange(e: Event) {
return (e.target as HTMLInputElement).value
}
@Emit('promise')
public emitPromise(val: unknown) {
return val
}
public addToCount(n: number) {
this.count += n
this.emitAddToCount(n)
}
public resetCount() {
this.count = 0
this.emitReset()
}
public handlePromise() {
const promise = new Promise(resolve => {
setTimeout(() => {
resolve(20)
}, 0)
})
promise.then(val => {
this.emitPromise(val)
})
}
}
风格指南:
- 明确每个emit接收的事件名
- emit处理函数都以 emit 开头
- emit处理函数声明仅接收参数和直接返回参数,避免与业务逻辑耦合
- 若想传递多个参数,还是建议编写业务代码时,将多个参数作为一整个对象传递
生命周期 Hook
示例 - ts:
```typescript import { Component, Vue } from ‘vue-property-decorator’
@Component export default class App extends Vue { private beforeCreated() { console.log(‘beforeCreated’) } private created() { console.log(‘created’) } private beforeMount() { console.log(‘beforeMount’) } private mounted() { console.log(‘mounted’) } private beforeUpdate() { console.log(‘beforeUpdate’) } private updated() { console.log(‘updated’) } private beforeDestroy() { console.log(‘beforeDestroy’) } private destroyed() { console.log(‘destroyed’) } }
<a name="vaDfU"></a>
## mixins 混入
<a name="L5EBD"></a>
### 示例 - ts:
<a name="XQXP9"></a>
#### 创建 - FormMixins.ts
```typescript
// mixins/FormMixins.ts
import { Component, Mixins } from 'vue-property-decorator'
@Component
export default class FormMixins extends Mixins() {
public form = {
type: 1,
search: '搜索信息',
}
public logFormSearch() {
console.log(this.form.search)
}
}
// mixins/UserMixins.ts
import { Component, Mixins } from 'vue-property-decorator'
@Component
export default class UserMixins extends Mixins() {
public user = {
name: '韩梅梅',
age: 18,
}
public logUserName() {
console.log(this.user.name)
}
}
使用 - .vue
// App.vue
import { Component, Mixins } from 'vue-property-decorator'
import FormMixins from '@/mixins/FormMixins'
import UserMixins from '@/mixins/UserMixins'
@Component
export default class App extends Mixins(FormMixins, UserMixins) {
private mounted() {
this.logFormSearch() // 搜索信息
this.logUserName() // 韩梅梅
}
}
风格指南:
- mixins命名时要使用大驼峰命名
- mixins命名以
Mixins
结尾directive 自定义指令
示例 - ts:
创建 - colorDirective.ts
```typescript // colorDirective.ts // 获取类型提示 import { DirectiveOptions } from ‘vue’
const directive: DirectiveOptions = { inserted(el, node) { /**
* Using value:
* v-color={color: 'red', backgroundColor: 'blue'}
*/
if (node.value) {
el.style.backgroundColor = node.value.backgroundColor
el.style.color = node.value.color
}
/**Using modifiers:
* v-color.color
* v-color.backgroundColor
*/
if (node.modifiers.color) {
el.style.color = node.value
}
if (node.modifiers.backgroundColor) {
el.style.backgroundColor = node.value
}
}, }
export default directive
<a name="VgTON"></a>
#### 使用 - .vue
```vue
// App.vue
<template>
<div id="app">
<h1 v-color="{color: 'red', backgroundColor: 'blue'}">
about
</h1>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
import colorDirective from '@/directive/colorDirective'
@Component({
directives: {
color: colorDirective,
},
})
export default class App extends Vue {}
</script>
风格指南:
自定义指令用法与普通Vue的指令用法相同,写在@Component装饰器中。
类型提示仅在封装directives时。
provide 和 inject
示例 - js:
// js
// 祖先组件 - Todo.vue
import TodoListItem from '@/components/TodoListItem.vue'
export default {
components: { TodoListItem },
provide: () => ({
userId: 1,
userName: '韩梅梅',
}),
}
// 孙组件 - TodoListItem.vue
export default {
name: 'TodoListItem',
inject: {
todoUserName: {
from: 'userName',
default: '韩梅梅',
},
todoUserId: {
from: 'userId',
default: 1
},
},
mounted() {
console.log(this.todoUserName) // 韩梅梅
console.log(this.todoUserId) // 1
},
}
示例 - ts:
// ts
// 祖先组件 - Todo.vue
import { Component, Vue, Provide } from 'vue-property-decorator'
import TodoListItem from '@/components/TodoListItem.vue'
@Component({
components: { TodoListItem },
})
export default class Todo extends Vue {
@Provide('userName') public userName = '韩梅梅'
@Provide('userId') public userId = 1
}
// 孙组件 - TodoListItem.vue
import { Component, Vue, Inject } from 'vue-property-decorator'
@Component({})
export default class TodoListItem extends Vue {
@Inject('userId') public todoUserId = 1
@Inject('userName') public todoUserName = '韩梅梅'
private mounted() {
console.log(this.todoUserName) // 韩梅梅
console.log(this.todoUserId) // 1
}
}
Ref
示例 - ts:
在Vue中,ref可以绑定DOM元素,也可以直接绑定Vue组件。
<template>
<div class="todo">
<div ref="refTitle">
this is title
</div>
<TodoItem ref="refTodoItem" />
</div>
</template>
<script lang="ts">
import { Component, Vue, Ref } from 'vue-property-decorator'
import TodoItem from '@/components/TodoItem.vue'
@Component({
components: { TodoItem },
})
export default class Todo extends Vue {
@Ref('refTitle') readonly refTitle!: HTMLDivElement // DOM
@Ref('refTodoItem') readonly refTodoItem!: TodoItem // Vue Component
private mounted() {
console.log(this.refTitle) // 等同于 this.$refs.refTitle
console.log(this.refTodoItem) // 等同于 this.$refs.refTodoItem
}
}
</script>
风格指南:
@Ref
装饰器函数第一个参数是模板中ref的值,代表你要绑定的是哪个ref。后面的变量代表可以用this.xxx访问到ref的变量名。
**
- ref的命名,推荐以ref开头。DOM的ref进行语义化的命名,如
refTitle
。Vue组件的则以ref开头,后面跟上驼峰式的组件名,如refTodoItem
。 - ref的命名和要在class内通过this访问的变量名,命名保持一致。
- 变量前加上
readonly
关键字,以保持ref对象不能随意赋值修改的属性。 - 变量后无需赋值,同时加上非空断言。即
refTitle!
。来告诉ts编译器这个ref的类型就是冒号后面声明的类型,且无需手动赋值。 - DOM类型的ref。官方已经定义好了几乎所有DOM类型的声明(https://developer.mozilla.org/zh-CN/docs/Web/API#%E6%8E%A5%E5%8F%A3)。实在无法确定的,可以先使用
HTMLElement
类型。 - 组件类型的ref。import 获取组件后,直接将值作为ref的类型。(ts的import一般都会将值和类型同时引入)
Model
Vue官方文档: 一个组件上的
v-model
默认会利用名为value
的 prop 和名为input
的事件,但是像单选框、复选框等类型的输入控件可能会将value
attribute 用于不同的目的。model
选项可以用来避免这样的冲突:
示例 - js:
父组件 Parent.vue
<template>
<div class="parent">
<h1>parent</h1>
<ChildItem v-model="parentAge" />
</div>
</template>
<script>
import ChildItem from '@/components/ChildItem.vue'
export default {
components: {
ChildItem,
},
data: () => ({
parentAge: 18,
}),
}
</script>
子组件 ChildItem.vue
<template>
<div class="child">
<button @click="getRandomName()">
获取随机年龄 - ({{ userAge }})
</button>
</div>
</template>
<script>
export default {
name: 'ChildItem',
model: {
prop: 'user-age',
event: 'age-change',
},
props: {
userAge: {
type: Number,
default: 0,
},
},
methods: {
getRandomAge() {
// 这里触发model绑定的事件名
this.$emit('age-change', parseInt(Math.random() * 15, 10))
},
},
}
</script>
示例 - ts:
父组件 TodoApp.vue
<template>
<div class="todo-app">
<TodoItem v-model="parentAge" />
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
import TodoItem from '@/components/TodoItem.vue'
@Component({
components: { TodoItem },
})
export default class TodoApp extends Vue {
public parentAge = 18
}
</script>
子组件 TodoAppItem.vue
<template>
<div class="todo">
<button @click="getRandomAge()">
获取随机年龄 - ({{ userAge }})
</button>
</div>
</template>
<script lang="ts">
import { Component, Vue, Model, Emit } from 'vue-property-decorator'
@Component
export default class TodoAppItem extends Vue {
@Model('user-age')
readonly userAge!: number
@Emit('user-age')
emitAgeChange(age: number) {
return age
}
public getRandomAge() {
this.emitAgeChange(parseInt(`${Math.random() * 15}`, 10))
}
}
</script>
风格指南:
父组件通过 v-model
传值,子组件不再需要 prop
。可以直接通过 @Model
装饰器同时配置 v-model
以及获取 prop
的属性。@Emit
装饰器中传入跟 @Model
中一样的值即可。
- 通过 @Model 装饰器获取的属性也可以直接通过this访问,但是不能直接修改,因此加上
readonly
修饰符 @Emit
装饰器的参数最好使用横线连接kabeb-case格式,由于要跟@Model
参数保持一致,因此model的参数最好也是用kabeb-case格式命名。