隨着應用的龐大,項目中 JavaScript 的代碼也會愈來愈臃腫,這時候許多 JavaScript 的語言弊端就會愈發明顯,而 TypeScript 的出現,就是着力於解決 JavaScript 語言天生的弱勢:靜態類型。javascript
前端開發 QQ 羣:377786580html
這篇文章首發於個人我的博客 《據說》,系列目錄:前端
在上一篇文章 《從 JavaScript 到 TypeScript 5 - express 路由進化》 咱們介紹瞭如何使用裝飾器將 express
的 router
實現的更加優雅。在這篇文章中,咱們將介紹如何在 Vue 中引入 TypeScript。vue
Vue 在 官方文檔中有一節簡單的介紹瞭如何引入 TypeScript,惋惜文檔太過簡單,真正投入生產還有許多的細節沒有介紹。java
咱們對此進行了一系列探索,最後咱們的風格是這樣的:node
import { Component, Prop, Vue, Watch } from 'vue-property-decorator' import { State, Action, Mutation, namespace } from 'vuex-class' import Toast from 'components/Toast.vue' const userState = namespace('business/user', State) @Component({ components: { Toast }, }) export default class extends Vue { // data title = 'demo' @Prop({ default: '' }) text: string // store @userState userId // computed get name (): boolean { return this.title + this.text } // watch @Watch('text') onChangeText () { } // hooks mounted() { } }
大致來講,Vue 引入 TypeScript 能夠用到這些生態庫:jquery
vue-class-component
上加強更多的結合 Vue 特性的裝飾器vue-class-component
提供 Vuex
的綁定Vuex
和 TypeScript 結合下面咱們一步步來介紹 Vue 如何引入 TypeScriptwebpack
TypeScript 爲 Webpack 提供了 ts-loader
:git
npm i ts-loader --save-dev
webpack 配置以下:github
{ module: { rules: [ { test: /\.vue$/, loader: 'vue-loader', options: { /* ... */ }, }, { test: /\.ts$/, loader: 'ts-loader', options: { appendTsSuffixTo: [/\.vue$/], } }, ], } }
ts-loader
會檢索當前目錄下的 tsconfig.json
文件,若是找不到會一層層往上找。
這裏有一份參考的 tsconfig.json
配置:
{ // 編譯選項 "compilerOptions": { // 輸出目錄 "outDir": "./output", // 是否包含能夠用於 debug 的 sourceMap "sourceMap": true, // 以嚴格模式解析 "strict": true, // 採用的模塊系統 "module": "esnext", // 如何處理模塊 "moduleResolution": "node", // 編譯輸出目標 ES 版本 "target": "es5", // 容許從沒有設置默認導出的模塊中默認導入 "allowSyntheticDefaultImports": true, // 將每一個文件做爲單獨的模塊 "isolatedModules": false, // 啓用裝飾器 "experimentalDecorators": true, // 啓用設計類型元數據(用於反射) "emitDecoratorMetadata": true, // 在表達式和聲明上有隱含的any類型時報錯 "noImplicitAny": false, // 不是函數的全部返回路徑都有返回值時報錯。 "noImplicitReturns": true, // 從 tslib 導入外部幫助庫: 好比__extends,__rest等 "importHelpers": true, // 編譯過程當中打印文件名 "listFiles": true, // 移除註釋 "removeComments": true, "suppressImplicitAnyIndexErrors": true, // 容許編譯javascript文件 "allowJs": true, // 解析非相對模塊名的基準目錄 "baseUrl": "./", // 指定特殊模塊的路徑 "paths": { "jquery": [ "node_modules/jquery/dist/jquery" ] }, // 編譯過程當中須要引入的庫文件的列表 "lib": [ "dom", "es2015", "es2015.promise" ] } }
因爲 TypeScript 默認並不支持 *.vue
後綴的文件,因此在 vue 項目中引入的時候須要建立一個 vue-shims.d.ts
文件,放在項目項目對應使用目錄下,例如 src/vue-shims.d.ts
declare module "*.vue" { import Vue from "vue"; export default Vue; }
意思是告訴 TypeScript *.vue
後綴的文件能夠交給 vue
模塊來處理。
而在代碼中導入 *.vue
文件的時候,須要寫上 .vue
後綴。緣由仍是由於 TypeScript 默認只識別 *.ts
文件,不識別 *.vue
文件:
import Component from 'components/component.vue'
vue-class-component 對 Vue 組件進行了一層封裝,讓 Vue 的組件語法在結合了 TypeScript 語法以後更加扁平化:
<template> <div> <input v-model="msg"> <p>msg: {{ msg }}</p> <p>computed msg: {{ computedMsg }}</p> <button @click="greet">Greet</button> </div> </template> <script lang="ts"> import Vue from 'vue' import Component from 'vue-class-component' @Component export default class App extends Vue { // 初始化數據 msg = 123 // 聲明週期鉤子 mounted () { this.greet() } // 計算屬性 get computedMsg () { return 'computed ' + this.msg } // 方法 greet () { alert('greeting: ' + this.msg) } } </script>
上面的代碼和下面沒有引入 vue-class-component
的語法同樣:
export default { data () { return { msg: 123 } } // 聲明週期鉤子 mounted () { this.greet() } // 計算屬性 computed: { computedMsg () { return 'computed ' + this.msg } } // 方法 methods: { greet () { alert('greeting: ' + this.msg) } } }
vue-property-decorator 是在 vue-class-component
上加強了更多的結合 Vue 特性的裝飾器,新增了這 7 個裝飾器:
@Emit
@Inject
@Model
@Prop
@Provide
@Watch
@Component
(從 vue-class-component
繼承)這裏僅列舉經常使用的 @Prop/@Watch/@Component
:
import { Component, Emit, Inject, Model, Prop, Provide, Vue, Watch } from 'vue-property-decorator' @Component export class MyComponent extends Vue { @Prop() propA: number = 1 @Prop({ default: 'default value' }) propB: string @Prop([String, Boolean]) propC: string | boolean @Prop({ type: null }) propD: any @Watch('child') onChildChanged(val: string, oldVal: string) { } }
至關於:
export default { props: { checked: Boolean, propA: Number, propB: { type: String, default: 'default value' }, propC: [String, Boolean], propD: { type: null } } methods: { onChildChanged(val, oldVal) { }, }, watch: { 'child': { handler: 'onChildChanged', immediate: false, deep: false }, } }
vuex-class 在 vue-class-component
上,提供了 Vuex
的綁定的裝飾器語法。
咱們編寫一個簡單的 Vuex store:
import Vue from 'vue' import Vuex from 'vuex' // vuex const store = new Vuex.Store({ state: { name: 'linkFly' } modules: { demo: { // 帶命名空間 namespaced: true, state: { count: 0 }, mutations: { increment (state, n?: number) { if (n != null ) state.count = n else state.count++ } }, actions: { increment ({ commit }) { commit.commit('increment') } } } } }) const app = new Vue({ el: '#app', store, template: `<div class="app"></div>` })
使用 vuex-class
以後:
import { Component, Vue } from 'vue-property-decorator' import { State, Getter, Action, Mutation, namespace } from 'vuex-class' const ModuleState = namespace('demo', State) const ModuleAction = namespace('demo', Action) const ModuleMutation = namespace('demo', Mutation) @Component export class MyComp extends Vue { @ModuleState('count') count @ModuleAction increment @ModuleMutation('increment') mutationIncrement @State name created () { this.name // -> store.state.name => linkFly this.count // -> store.state.demo.count => 0 this.increment() // -> store.dispatch('demo/increment') this.mutationIncrement(2) // -> store.commit('demo/increment', 2) } }
因爲使用 vue-class-component
,在 Vue 組件中咱們已經感覺到了裝飾器的強大語法糖,因而咱們還但願在 Vuex Store 中也能使用裝飾器的語法: vuex-ts-decorators 就是幹這個事情的。
vuex-ts-decorators
可讓你結合裝飾器來編寫 Vuex Store。
因爲 vuex-ts-decorators
提供的包是未經編譯的 *.ts
代碼,若是你排除了 node_modules
的編譯,則須要在 ts-loader
中單獨加上對它的編譯:
{ test: /\.ts$/, loader: 'ts-loader', // 加上對 vuex-ts-decorators 這個包的編譯 exclude: [/node_modules/, /node_modules\/(?!vuex-ts-decorators)/], options: { appendTsSuffixTo: [/\.vue$/], } },
而後就能夠愉快使用它來編寫 Vuex Store 了。
import { module, action, mutation } from 'mfe-vuex-ts-decorators' type actions = { } type mutations = { // 定義對應 mutations 的參數類型 incrementMutation: number } type TypedDispatch = <T extends keyof actions>(type: T, value?: actions[T]) => Promise<any[]>; type TypedCommit = <T extends keyof mutations>(type: T, value?: mutations[T]) => void; @module({ store: false, namespaced: true }) class demo { // 用於類型檢查,使 commit/dispatch 的方法能夠找到而且能夠被類型檢查 dispatch: TypedDispatch commit: TypedCommit // state count = 0 // getter get countGetter(): string { return this.count } // action @action increment() { this.commit('incrementMutation'); } // mutation @mutation incrementMutation(payload?: mutations['incrementMutation']) { if (n != null ) this.count = payload else this.count++ } }
vuex-ts-decorators
文檔沒有說起 @module
裝飾器的參數:
@module({ store?: boolean = false; // 是否自動掛載到 Vuex Store 下,若是爲 false 則 modules?: Object; // 子 modules namespaced?: boolean; // 命名空間 } | (any, { decorators: any } => any)) // 也可使用函數
vuex-ts-decorators
這個項目目前最後一次更新時間是 2017 年 2 月 21 日,原做者說本身正在開發對應的新項目,這個項目已經再也不維護,可是目前尚未看到新項目的進展。
但能夠確定的是,原做者已經再也不維護 vuex-ts-decorators
這個項目。
咱們的作法是在這個項目上從新 fork 一個新項目,而且修正了遇到的隱藏 bug。後續咱們會本身維護這個項目。(fork 到了內部項目中,後續在這個項目基礎上進行二次開發,到時候會公佈出來,固然,其實它的功能很簡單徹底能夠本身開發一個)
目前已知 bug:
當使用 @module({ store: false })
後,被包裝的 class 會返回處理後的 Vuex Store,結構爲: { state: any, getters: any, modules: any, actions: any, mutations: any }
。
若是最後 new Vuex.Store({ options: object })
的時候傳遞的不是 @module
包裝後的 Vuex Store(例如對這個 Vuex Store 作了一層深拷貝),則會致使 mutations
中 this 丟失。
bug 點在 這一行,處理辦法:
store.mutations[name] = (state: any, ...args: any[]) => { method.apply(store.state, args); // 替換爲 method.apply(state, args); }
緣由是由於 vuex-ts-decorators
一直在本身內部引用着生成的 Vuex Store,若是最終 new Vuex.Store()
的時候沒有傳遞它本身生成的 Vuex Store 就會致使引用不正確。
圍繞 vue-class-component
展開的生態圈,如同一把利劍,經過各類裝飾器,可以結合 TypeScript 將 Vue 武裝的更增強大。在這篇文章發佈之際,很巧的是:Vue 做者尤雨溪也宣佈在 Vue 2.5 之後將全面支持 TypeScript(譯文看這裏),而且聲明Vue 將盡力作到和 vue-class-component 兼容。
前端的場景已經再也不像當年同樣簡單的切圖、畫簡單的樣式和寫簡單的腳本。應用愈來愈龐大,需求愈來愈複雜。TypeScript 有一個很好的切入點:從語言的角度解決了大型 Web 應用的靜態類型約束痛點。
相信隨着時間的推移,會有愈來愈多的人和框架加入到 TypeScript 大軍。
至此,咱們的 《從 JavaScript 到 TypeScript》 系列文章已經結束。最後,Welcome to TypeScript!
TypeScript 中文網:https://tslang.cn/
TypeScript 視頻教程:《TypeScript 精通指南》