概述
VUE 是前端三剑客之一, 之前一直处在写过 demo 的地步, 没有很深入的体会, 这次公司因为招不到 react 的人, 所以希望尝试将技术栈换为 vue, 以便更方便招人.
于是就有了 VUE 的踏坑之旅, 而且一开始就是配合 ts 来食用, 因为在 react 中 ts 的配合相当的好, jsx 里的代码类型提示应有尽有, 可以很大的提升开发效率以及减少 bug. 以为在 VUE 也能得到这样好的体验.
但是….
VUE 虽然也可以支持 JSX, 官方还是推荐使用模板渲染, 而模板里压根没有代码提示, 所以 ts 的功力就废了一半(剩下的一半还在 script 里), 具体的讨论请参考
准备
前戏差不多了, 开始步入正题.
- 安装
vue cli 3.x
这个脚手架可以快速启动项目
yarn global add @vue/cli
推荐全局安装.
-
在任意文件夹下
vue create .
. 会在当前文件夹中注入初始项目脚手架, 包含 webpack, babel, ts, lint, jest, jsdoc 等一整套模板 -
使用 ts 会默认使用
vue-property-decorator
这个库 github, 这个库用装饰器的写法, 将很多 js 中繁杂的配置抽象出来, 一开始不习惯. 习惯了可以大大提高效率的. -
注意对eslint的配置: 需要在
vscode
的setting.json
文件里添加如下内容已实现对.vue
文件格式的支持以及自动保存时format:
"eslint.validate": [
"javascript",
"javascriptreact",
{
"language": "vue",
"autoFix": true
}
],
"eslint.autoFixOnSave": true,
- 建议配置一个 vs code 的
user snippet
"tsvue": {
"scope": "javascript,typescript, vue",
"prefix": "tsvue",
"body": [
"<template>",
" <div>",
" </div>",
"</template>",
"<script lang=\"ts\">",
"import Vue from 'vue';",
"import { Component } from 'vue-property-decorator';",
"@Component",
"export default class $1 extends Vue {",
"$2",
"}",
"</script>"
],
"description": "Vue typescript template"
}
vue-property-decorator
的食用方法
@Component(options)
这一个是与另一个 vue 的库 vue-class-component一样的用法. 这个装饰器库源自 class 库, 只是再封装了一层, 使代码更为简洁明了. options 里面需要配置 decorator 库不支持的属性, 哪些是不支持的呢? 那就请看完全文, 凡是没写的都是不支持的. 比如components
, filters
, directives
等
components
表示该组件引入了哪些子组件
<template>
<div id="app">
<HelloWorld />
</div>
</template>
<script lang="ts">
@Component({
components: {
HelloWorld, // 声明子组件的引用
}
})
export default class App extends Vue {}
</script>
filters
filter 表示对数据的筛选, 跟 linux 中的管道符十分相似, 数据通过 filter 进行处理变成新的数据.
注意, 在这里配置时一定要使用 filters, 不要忘了 s, 否则不会报错但是也没作用.
<template>
<div></div>
</template>
<script lang="ts">
@Component({
filters: {
addWorld: (value: string) => `${value} world`,
},
})
export default class App extends Vue {
private msg = 'Hello' // filter 之后显示 hello world
}
</script>
directives
具体的介绍可以看 Vue 的官方介绍. 简单来说就是 DOM 节点在一些特定钩子触发时添加一些额外的功能
钩子函数有:
- bind 指令绑定到元素时调用, 可以进行一次性设置
- inserted 绑定元素被插入到父节点时调用
- update VNode 更新时调用
- componentUpdated VNode 及子 VNode 全部更新后调用
- unbind 元素解绑时调用
如果bind 和 update 时调用一样可以进行简写, 不用指定某个钩子
钩子函数参数:
- el 对应绑定的 DOM
- binding
- name 指令名
v-demo="1+1"
为 “demo” - value 绑定的值
v-demo="1+1"
为 2 - oldValue 在 update componentUpdated 可用
- expression 字符串形式的表达式
v-demo="1+1"
为 “1+1” - arg 指令的参数
v-demo:foo
为 ‘foo’, 注意要在 modifier 前使用 arg, 不然会将 arg 作为 modifier 的一部分, 如v-demo.a:foo
arg 为 undefined, modifier 为{'a:foo': true}
- modifiers 包含修饰符的对象 比如
v-demo.a
这个值为{a:true}
- name 指令名
- vnode vue 的虚拟节点, 可参考源码查看可用属性
- oldVnode 上一个虚拟节点(update 和 componentUpdated 可用)
看个简单的实例:
<template>
<span v-demo:foo.a="1+1">test</span>
</template>
<script lang="ts">
@Component({
directives: {
demo: {
bind(el, binding, vnode) {
console.log(`bindingName: ${binding.name}, value: ${binding.value}, args: ${binding.arg}, expression: ${binding.expression}`); // bindingName: demo, value: 2, args: foo, expression: 1+1
console.log('modifier:', binding.modifiers); // {a:true}, 无法转为 primitive, 所以单独打印
},
},
demoSimplify(el, binding, vnode) {
// do stuff
},
},
})
export default class App extends Vue {}
</script>
@Prop()
父子组件传递数据 props的修饰符, 参数可以传
- Constructor 例如
String, Number, Boolean
- Constructor[], 构造函数的队列, 类型在这队列中即可
- PropOptions
- type 类型不对会报错
Invalid prop: type check failed for prop "xxx". Expected Function, got String with value "xxx".
- default 如果父组件没有传的话为该值, 注意只能用这一种形式来表示默认值, 不能
@Prop() name = 1
来表示默认值 1, 虽然看起来一样, 但是会在 console 里报错, 不允许修改 props 中的值 - required 没有会报错
[Vue warn]: Missing required prop: "xxx"
- validator 为一个函数, 参数为传入的值, 比如
(value) => value > 100
- type 类型不对会报错
父组件:
<template>
<div id="app">
<PropComponent :count='count' />
</div>
</template>
<script lang="ts">
@Component({
components: {
PropComponent
}
})
class Parent extends Vue {
private count = 101
}
</script>
子组件:
<template>
<div>8</div>
</template>
<script lang="ts">
@Component
export default class PropsComponent extends Vue {
@Prop({
type: Number,
validator: (value) => {
return value > 100;
},
required: true
}) private count!: string // !表示有值, 否则 ts 会告警未初始化
}
</script>
@PropSync()
与 Prop 的区别是子组件可以对 props 进行更改, 并同步给父组件,
子组件:
<template>
<div>
<p>8</p>
<button @click="innerCount += 1">increment</button>
</div>
</template>
<script lang="ts">
@Component
export default class PropSyncComponent extends Vue {
@PropSync('count') private innerCount!: number // 注意@PropSync 里的参数不能与定义的实例属性同名, 因为还是那个原理, props 是只读的.
}
</script>
父组件: 注意父组件里绑定 props 时需要加修饰符 .sync
<template>
<PropSyncComponent :count.sync="count"/>
</template>
<script lang="ts">
@Component({
components: {
PropSyncComponent
}
})
export default class PropSyncComponent extends Vue {
@PropSync('count') private innerCount!: number // 注意@PropSync 里的参数不能与定义的实例属性同名, 因为还是那个原理, props 是只读的.
}
</script>
也可结合 input 元素的 v-model
绑定数据, 实时更新. 由读者自行实现.
@Watch
监听属性发生更改时被触发. 可接受配置参数 options
immediate?: boolean
是否在侦听开始之后立即调用该函数deep?: boolean
是否深度监听.
<template>
<div>
<button @click="innerName.name.firstName = 'lorry'">change deeper</button>
<button @click="innerName.name = 'lorry'">change deep</button>
</div>
</template>
<script lang="ts">
@Component
export default class PropSyncComponent extends Vue {
private person = { name: { firstName: 'jiang' } }
@Watch('person', {
deep: true,
})
private firstNameChange(person: number, oldPerson:number) {
console.log(`count change from${oldName.name.first}to: ${oldName.name.}`);
}
}
</script>
@Emit
- 接受一个参数
event?: string
, 如果没有的话会自动将 camelCase 转为 dash-case 作为事件名. - 会将函数的返回值作为回调函数的第二个参数, 如果是 Promise 对象,则回调函数会等 Promise resolve 掉之后触发.
- 如果$emit 还有别的参数, 比如点击事件的
event
, 会在返回值之后, 也就是第三个参数.
子组件:
<template>
<div>
<button @click="emitChange">Emit!!</button>
</div>
</template>
<script lang="ts">
@Component
export default class EmitComponent extends Vue {
private count = 0;
@Emit('button-click')
private emitChange() {
this.count += 1;
return this.count;
}
}
</script>
父组件, 父组件的对应元素上绑定事件即可:
<template>
<EmitComponent v-on:button-click='listenChange'/>
</template>
<script lang="ts">
@Component({
components: {
EmitComponent,
},
})
export default class App extends Vue {
private listenChange(value: number, event: any) {
console.log(value, e);
}
}
</script>
@Ref
跟 react 中的一样, ref 是用于引用实际的 DOM 元素或者子组件.应尽可能避免直接使用, 但如果不得不用 ref 比 document 拿要方便很多, 参数传一个字符串refKey?:string
, 注意这里如果省略传输参数, 那么会自动将属性名作为参数, 注意与@Emit
的区别, @Emit
在不传参数的情况下会转为 dash-case, 而 @Ref
不会转, 为原属性名
<template>
<div>
<span>Name:</span>
<input type="text" v-model="value" ref='name' />
</div>
</template>
<script lang="ts">
@Component
export default class RefComponent extends Vue {
@Ref('name') readonly name!: string;
private value = 'lorry'
private mounted() {
console.log(this.inputName); // <input type="text">
// do stuff to ref
}
}
</script>
@Provide/@inject && @ProvideReactive/@InjectReactive
其本质是转换为 inject
和 provide
, 这是 vue 中元素向更深层的子组件传递数据的方式.两者需要一起使用.与 react 的 context
十分的像.
任意代的子组件:
<template>
<span>Inject deeper: </span>
</template>
<script lang="ts">
@Component
export default class InjectComponent extends Vue {
@Inject() private bar!: string
private mounted() {
console.log(this.bar);
}
}
</script>
任意祖先元素:
<script>
export default class App extends Vue {
@Provide() private bar = 'deeper lorry'
}
</script>
方便很多, 如果为了避免命名冲突, 可以使用 ES6 的 Symbol
特性作为 key, 以祖先元素举例:
需要注意的是避免相互引用的问题, symbol 的引用最好放到组件外单独有个文件存起来.
export const s = Symbol()
父组件:
<script>
export default class App extends Vue {
@Provide(s) private bar = 'deeper lorry'
}
</script>
子组件:
<script>
@Component
export default class App extends Vue {
@Inject(s) private baz = 'deeper lorry'
}
</script>
@ProvideReactive/@InjectReactive
顾名思义就是响应式的注入, 会同步更新到子组件中.比如下例可以实现在 input
中的输入实时注入到子组件中
父组件
<template>
<div id="app">
<input type="text" v-model="bar">
<InjectComponent />
</div>
</template>
<script>
@Component({
InjectComponent
})
export default class App extends Vue {
@ProvideReactive(s) private bar = 'deeper lorry'
}
</script>
子组件:
<script>
@Component
export default class InjectComponent extends Vue {
@InjectReactive(s) private baz!: string
}
</script>
以上为文档中罗列的用法,以后项目过程中遇到了别的会回来更新.
敬请指正.