项目使用的是Vue全家桶系列(vue, vuex, vue-router)构建的, 项目代码量和业务复杂度仍是有一些. 刚开始人少时, 代码写起来仍是没有问题的, 慢慢的, 随着人员的增多, 会发现你们的代码"风格"各异:html
凡此种种, 想起以前看过的一段话"欠的债, 早晚要还的". 有没有办法能够约束下这些"风格"各异的代码, 而且对当前工程代码影响不是很大的? => TypeScript, 也许能够试试.前端
TypeScript 具备类型系统,且是 JavaScript 的超集,TypeScript 在 2018年 势头迅猛,可谓遍地开花。vue
Vue3.0 将使用 TS 重写,重写后的 Vue3.0 将更好的支持 TS。 2019 年 TypeScript 将会更加普及,可以熟练掌握 TS,并使用 TS 开发过项目,将更加成为前端开发者的优点。node
所以, 这个技能必需要学会, 因此也就边学边实践, 并逐步引用到项目中实战. 预计在12月的版本中, 将其中一个小的vue项目中所有改用TypeScript.git
由于公司是内网环境, 不可访问外网. So, 只能回来再将代码复写一回了. 估计更新会比较慢.github
练手项目地址: vue-typescript-skillsvue-router
使用@vue/cli 3.0建立typescript工程.vuex
D:\vueProjects>vue create vue-typescript Vue CLI v3.9.2 ┌───────────────────────────┐ │ Update available: 4.0.5 │ └───────────────────────────┘ ? Please pick a preset: Manually select features ? Check the features needed for your project: Babel, TS, Router, Vuex, CSS Pre-processors, Linter, Unit, E2E ? Use class-style component syntax? Yes ? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? Yes ? Use history mode for router? (Requires proper server setup for index fallback in production) No ? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Less ? Pick a linter / formatter config: Standard ? Pick additional lint features: (Press <space> to select, <a> to toggle all, <i> to invert selection)Lint on save ? Pick a unit testing solution: Jest ? Pick a E2E testing solution: Cypress ? Where do you prefer placing config for Babel, PostCSS, ESLint, etc.? In dedicated config files ? Save this as a preset for future projects? Yes ? Save preset as: vue-typescript 复制代码
安装成功后, 便可运行本地开发环境了.vue-cli
安装成功后, 会生成以下目录 :typescript
这里咱们重点关注下4个文件:
eslint示例和解释可参考: .eslintrc 文件示例和解释
在对支持typescript, 可能会新增或修改2个配置,
'extends': [ 'plugin:vue/essential', '@vue/standard', '@vue/typescript' ] 复制代码
overrides: [ { files: [ '**/__tests__/*.{j,t}s?(x)', '**/tests/unit/**/*.spec.{j,t}s?(x)' ], env: { jest: true } } ] 复制代码
这主要用于复写eslint配置配置, 此处是针对__tests__, 以及tests/units目录下的js/ts/jsx/ts文件, 修改配置规则, 此处是修改env
为jest:true
ts的语言服务须要.d.ts
文件来识别类型,这样才能作到相应的语法检查和智能提示. 咱们本身编写的.d.ts
文件直接放在项目的目录下,ts本身会去识别,不用咱们作什么操做,更加详细的资料能够看一下TypeScript-声明文件
.vue
文件, 并将全部导入的.vue
文件都按VueConstructor<Vue>
处理若是一个目录下存在一个tsconfig.json文件,那么它意味着这个目录是TypeScript项目的根目录。 tsconfig.json文件中指定了用来编译这个项目的根文件和编译选项。 一个项目能够经过如下方式之一来编译:
tsconfig.json中详细配置项及说明, 请移步至: TypeScript-项目配置-tsconfig.json
此规则集是Vue-TypeScript项目的基本配置。除了设置解析器和插件选项外,它还会关闭规则集中的一些冲突规则eslint:recommended。所以,当与其余可共享配置一块儿使用时,此配置应放在extends数组的末尾。 例如:
// .eslintrc.js: module.exports = { extends: [ 'plugin:vue/essential', 'eslint:recommended', '@vue/typescript' ] } 复制代码
在此以前, 咱们可能须要先了解下ES7装饰器(Decorator)在Javascript中的使用
所以, 会有如下写法上的改变
@Component(options) options 中须要配置 decorator 库不支持的属性, 如: components, filters, directives等
示例:
<template> <div> <input-demo :demo="demo"></input-demo> </div> </div> </template> <script lang="ts"> import Component from 'vue-class-component' import { Emit, Inject, Model, Prop, Provide, Ref, Vue, Watch, PropSync } from 'vue-property-decorator' import InputDemo from './InputDemo.vue' @Component({ components: { InputDemo } }) export default class Demo extends Vue { // data count = 0 demo = '123' mounted () { window.console.log('bar=> ', this.bar) window.console.log('foo=> ', this.foo) window.console.log('optional=> ', this.optional) } } </script> 复制代码
在使用Vue进行开发时咱们可能须要用到混合,在TypeScript中, 咱们能够这么写
在如下示例中mixins/index.ts中, 咱们在data中添加了一个属性mixinVal, 值为: 'Hello Mixin'
// 定义要混合的类 mixins/index.ts import Vue from 'vue' import Component from 'vue-class-component' @Component // 必定要用Component修饰 export default class myMixins extends Vue { mixinVal: string = 'Hello Mixin' } 复制代码
而后, 在其余组件中使用它
<template> <div> <hello-world msg='hello world'></hello-world> </div> </template> <script lang="ts"> import Vue from 'vue' import Component, { mixins } from 'vue-class-component' import { Emit, Inject, Model, Prop, Provide, Watch, PropSync, Ref } from 'vue-property-decorator' import HelloWorld from '../components/HelloWorld.vue' import mixinDemo from './mixin' @Component({ components: { HelloWorld // 组件注入 } }) export default class App extends mixins(mixinDemo) { // data message = 'hello' mounted () { // 此时, 就可使用this.mixinVal window.console.log('mixinVal => ', this.mixinVal) // 输出: 'Hello Mixin' } } </script> 复制代码
export default class App extends Vue { // data message = 'hello' name = 'dmax' child: number | string = 'james' } 复制代码
等价于:
export default { name: 'App', data () { return { message: 'hello', name: 'dmax', child: 'james' } } } 复制代码
// 计算属性 get msg () { return 'computed ' + this.message } 复制代码
等价于:
computed: { msg(){ return 'computed ' + this.message } } 复制代码
@Watch(path: string, options: WatchOptions = {})
@Watch 装饰器接收两个参数:
@Watch('child') onChildChanged (val: string, oldVal: string) { if (val !== oldVal) { window.console.log(val) } } 复制代码
等价于:
watch: { 'child': { handler: 'onChildChanged', immediate: false, deep: false } }, method: { onChildChanged(val, oldVal) { if (val !== oldVal) { window.console.log(val) } } } 复制代码
也能够写成: @Watch('child', { immediate: true, deep: true })
, 等价于:
watch: { 'child': { handler: 'onChildChanged', immediate: true, deep: true } }, method: { onChildChanged(val, oldVal) { if (val !== oldVal) { window.console.log(val) } } } 复制代码
@Model Vue组件提供model: {prop?: string, event?: string} 让咱们能够定制prop和event. 默认状况下, 一个组件上的v-model会:
value
用做 prop
input
用做 event
,可是一些输入类型好比单选框和复选框按钮可能想使用 value prop来达到不一样的目的。使用model选项能够回避这些状况产生的冲突。下面是Vue官网的例子
Vue.component('my-checkbox', { model: { prop: 'checked', event: 'change' }, props: { // this allows using the `value` prop for a different purpose value: String, // use `checked` as the prop which take the place of `value` checked: { type: Number, default: 0 } }, // ... }) <my-checkbox v-model="foo" value="some value"></my-checkbox> 复制代码
上述代码至关于:
<my-checkbox :checked="foo" @change="val => { foo = val }" value="some value"> </my-checkbox> 复制代码
即foo双向绑定的是组件的checke, 触发双向绑定数值的事件是change
使用vue-property-decorator提供的@Model改造上面的例子.
Parent.vue
<template> <div> <child v-model="price"></child> <div> v-model(price) => {{price}} </div> </div> </template> <script lang="ts"> import { Vue, Component, Prop } from 'vue-property-decorator' import Child from './Child.vue' @Component({ components: { Child } }) export default class Parent extends Vue { price = 'hello price' } </script> 复制代码
Child.vue
<template> <div> <input type="text" :value="value" @input="changed"/> </div> </template> <script lang="ts"> import { Vue, Component, Prop, Model, Emit } from 'vue-property-decorator' @Component export default class Child extends Vue { @Model('input') value!: boolean @Emit('input') changed (ev:any) { return ev.target.value } } </script> 复制代码
最终效果可能为:
也能够经过clone git库 vue-typescript-skills, 运行本地服务后进入http://localhost:8080/#/model, 看到效果.
@Prop(options: (PropOptions | Constructor[] | Constructor) = {})
@Prop装饰器接收一个参数,这个参数能够有三种写法:
示例:
@Component export default class Hello extends Vue { // child, 必传, child! => 表示不须要构建器进行初始化 @Prop({ type: [String, Number], required: true }) readonly child!: string | number // propA, 非必传, 类型能够是number | undefined @Prop(Number) readonly propA: number | undefined // propB, 非必传, 类型能够是number | undefined, propB! => 表示不须要构建器进行初始化 @Prop({ default: 'default value' }) readonly propB!: string // propC, 非必传, 构建器能够是String|Boolean, 值类型能够为: string | boolean | undefined @Prop([String, Boolean]) readonly propC: string | boolean | undefined } 复制代码
等价于:
export default { name: 'Hello', props: { child: { required: true, type: [String, Number] }, propA: { type: Number }, propB: { required: false, type: String, default: 'default value' }, propC: { type: [String, Boolean] } } } 复制代码
@PropSync(propName: string, options: (PropOptions | Constructor[] | Constructor) = {})
@PropSync装饰器与@prop用法相似,两者的区别在于: @PropSync 装饰器接收两个参数:
@PropSync 会生成一个新的计算属性。 示例:
import { Vue, Component, PropSync } from 'vue-property-decorator' @Component export default class MyComponent extends Vue { @PropSync('name', { type: String }) syncedName!: string } 复制代码
等价于
props: { name: { type: String } }, computed: { syncedName: { get() { return this.name }, set(value) { this.$emit('update:name', value) } } } 复制代码
@PropSync须要配合父组件的.sync
修饰符使用
@Emit(event?: string)
import { Vue, Component, Emit } from 'vue-property-decorator' @Component export default class MyComponent extends Vue { count = 0 @Emit('reset') public resetCount() { this.count = 0 } @Emit() public addToCount (n: number) { this.count += n } @Emit() public returnValue () { return 10 } @Emit() public onInputChange (e:any) { return e.target.value } @Emit() public promise () { return new Promise(resolve => { setTimeout(() => { resolve(20) }, 0) }) } } 复制代码
等价于
export default { data() { return { count: 0 } }, methods: { addToCount(n) { this.count += n 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, e) }, promise() { const promise = new Promise(resolve => { setTimeout(() => { resolve(20) }, 0) }) promise.then(value => { this.$emit('promise', value) }) } } } 复制代码
@Provide接收一个参数:
若是为了不命名冲突, 可使用 ES6 的 Symbol 特性做为 key
@Inject 装饰器一个参数, 该参数有两种要能:
示例:
import { Component, Inject, Provide, Vue } from 'vue-property-decorator' const symbol = Symbol('baz') @Component export class MyComponent extends Vue { @Inject() readonly foo!: string @Inject('bar') readonly bar!: string @Inject({ from: 'optional', default: 'default' }) readonly optional!: string @Inject(symbol) readonly baz!: string @Provide() foo = 'foo' @Provide('bar') baz = 'bar' } 复制代码
等价于:
const symbol = Symbol('baz') export const MyComponent = Vue.extend({ inject: { foo: 'foo', bar: 'bar', optional: { from: 'optional', default: 'default' }, [symbol]: symbol }, data() { return { foo: 'foo', baz: 'bar' } }, provide() { return { foo: this.foo, bar: this.baz } } }) 复制代码
顾名思义就是响应式的注入, 会同步更新到子组件中. 好比下例能够实如今 input 中的输入实时注入到子组件中. 示例: Parent.vue
<template> <div> <input type="text" v-model="bar"> <Child /> </div> </template> <script lang="ts"> import { Vue, Component, Prop, ProvideReactive } from 'vue-property-decorator' import Child from './Child.vue' @Component({ components: { Child } }) export default class Parent extends Vue { @ProvideReactive() private bar = 'deeper lorry' } </script> 复制代码
Child.vue
<template> <div > InjectReactive: {{bar}} </div> </template> <script lang="ts"> import { Vue, Component, Prop, InjectReactive } from 'vue-property-decorator' @Component export default class Child extends Vue { @InjectReactive() private bar!: string } </script> 复制代码
最终效果可能以下:
也能够经过clone git库 vue-typescript-skills, 运行本地服务后进入http://localhost:8080/#/provide, 看到效果.
@Ref(refKey?: string)
@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; value = 'lorry' mounted() { window.console.log(this.inputName); // <input type="text"> } } </script> 复制代码
等价于:
<template> <div> <span>Name:</span> <input type="text" v-model="value" ref='name' /> </div> </template> <script lang="ts"> @Component export default { data(){ return { value: 'lorry' } }, computed: { inputName(){ return this.$refs.name } }, mounted() { window.console.log(this.inputName); // <input type="text"> } } </script> 复制代码
directives 具体的介绍能够看 Vue 的官方介绍.
示例:
<template> <span v-demo:foo.a="1+1">test</span> </template> <script lang="ts"> @Component({ directives: { demo: { bind(el:any, binding:any, vnode:any) { var s = JSON.stringify el.innerHTML = 'name: ' + s(binding.name) + '<br>' + 'value: ' + s(binding.value) + '<br>' + 'expression: ' + s(binding.expression) + '<br>' + 'argument: ' + s(binding.arg) + '<br>' + 'modifiers: ' + s(binding.modifiers) + '<br>' + 'vnode keys: ' + Object.keys(vnode).join(', ') }, } }, }) export default class App extends Vue {} </script> 复制代码
在练习时, 发如今定义组件时漏写@Component装饰器时, 会致使运行data/prop/model属性报错:
Property or method "hello" is not defined on the instance but referenced during render 复制代码
错误源代码:
<template> <div> {{hello}} </div> </template> <script lang="ts"> import { createNamespacedHelpers } from 'vuex' import Component from 'vue-class-component' import { Emit, Inject, Model, Prop, Provide, Ref, Vue, Watch, PropSync } from 'vue-property-decorator' const { mapState, mapActions } = createNamespacedHelpers('myMod') // @Component ===> 注意此处的@Component装饰器被注释了, 起用此行, 便可解决异常 export default class UseVuex extends Vue { hello:string = this.$store.state.myMod.someField } </script> 复制代码
Component
装饰器如工程当中有使用到window.SystemJS, 若是在ts中不声明直接使用, 会提示错误. 诸如此类, 就须要对全局变量/方法进行合适的类型声明.
在 src 下的 shims-tsx.d.ts
中加入须要声明的代码, 以下所示:
declare global { interface Window { SystemJS: any; // 若是不肯定类型, 可定义为any } } 复制代码
import .vue
的文件的时候,要补全 .vue
的后缀,不然会提示语法错误或找不到模块
经过这几天的尝试和试验, 整体来讲, 有一点吸引力的, 毕竟vue的写法也很随意, 多加入一些强制性的校验, 项目代码的健壮性应该会加强很多. 后续会慢慢在项目中推行, 也会慢慢进入踩坑中, 后续再持续更新, 敬请关注!