这篇在介绍实例选项的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 {// 初始数据可以直接声明为实例的 propertypublic 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:
// jsexport default {name: "TodoList",}
示例 - ts:
// tsimport { Component, Vue } from "vue-property-decorator";@Componentexport 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// tsimport { 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:
// jsexport default {data: () => ({user: {name: 'Li Lei',age: 18,},loading: false,list: []})}
示例 - ts:
// tsimport { Component, Vue } from "vue-property-decorator";@Componentexport default class App extends Vue {public user = {name: 'Li Lei',age: 18,}public loading = falsepublic list: unknown[] = []}
风格指南:
在ts的类中,不写public默认为public。建议还是都写上,便于一眼区分类别。至于public、private、readonly等在何时使用,在后面会讲到。
computed 计算属性
示例 - js:
// jsexport 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:
// tsimport { Component, Vue } from "vue-property-decorator";@Componentexport default class App extends Vue {public num1 = 1public 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:
// jsexport default {methods: {consoleNumber() {console.log(123)},},}
示例 - ts:
// ts@Componentexport default class App extends Vue {public consoleNumber() {console.log(123)}}
filters 过滤器
示例 - js:
// jsexport 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:
// tsimport { Component, Vue, Watch } from 'vue-property-decorator'@Componentexport default class App extends Vue {public num = 10public 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:
// jsexport default {props: {propA: { type: Number, default: 1 },propB: { type: [String, Number], default: '' },propC: { type: [Object, Array], default: () => ({}) },},}
示例 - ts:
// tsimport { Component, Vue, Prop } from 'vue-property-decorator'@Componentexport 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-countthis.$emit('add-to-count', n)},resetCount() {this.count = 0this.$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>### 更好地写法:```typescriptimport { Component, Vue, Emit } from 'vue-property-decorator'@Componentexport 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 += nthis.emitAddToCount(n)}public resetCount() {this.count = 0this.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.tsimport { Component, Mixins } from 'vue-property-decorator'@Componentexport default class FormMixins extends Mixins() {public form = {type: 1,search: '搜索信息',}public logFormSearch() {console.log(this.form.search)}}// mixins/UserMixins.tsimport { Component, Mixins } from 'vue-property-decorator'@Componentexport default class UserMixins extends Mixins() {public user = {name: '韩梅梅',age: 18,}public logUserName() {console.log(this.user.name)}}
使用 - .vue
// App.vueimport { Component, Mixins } from 'vue-property-decorator'import FormMixins from '@/mixins/FormMixins'import UserMixins from '@/mixins/UserMixins'@Componentexport 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.backgroundColorel.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.vueimport TodoListItem from '@/components/TodoListItem.vue'export default {components: { TodoListItem },provide: () => ({userId: 1,userName: '韩梅梅',}),}// 孙组件 - TodoListItem.vueexport 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.vueimport { 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.vueimport { 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 Componentprivate mounted() {console.log(this.refTitle) // 等同于 this.$refs.refTitleconsole.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的事件,但是像单选框、复选框等类型的输入控件可能会将valueattribute 用于不同的目的。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'@Componentexport 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格式命名。
