若是說,2017 年計算機領域的潮流是人工智能的話,那麼前端界的潮流想必就是 TypeScript 了。css
你們一聽到 ts 是強類型語言,想到 js 要像其餘語言那樣定義變量類型就頭疼,內心多少有些抵觸情緒。起初我也是這樣認爲的,寫的時候的確也是這樣。但在另外一方面,它強大的靜態分析功能會使你所寫的代碼更健壯,從而大大減小 bug 的發生機率,將 bug 掐死在搖籃裏。git
這樣的好東西就想嘗試着把它用到本身的項目裏。可當要將 ts 加入到現有的 vue 項目中時,忽然有無從下手的感受,總感受 ts 的類型和 vue 綁定數據的方式沒法有效地結合起來。同時,印象中一直聽到的都是 react 和 angular 的項目在使用 ts,尚未據說哪一個成功的 vue 項目是用 ts 開發的。(element 也不是。)es6
那是否是 vue 就不能同 ts 一塊兒用哪?一度我也這樣懷疑過,不過搜了波資料以後,發現 vue 官網已經給出瞭如何整合 ts 的教程。微軟這邊也有個 TypeScript-Vue-Starter,可是,這個 starter 也沒法解決組件屬性上的類型檢測。這令 ts 類型檢測的能力大大下降,而 vue 則是推薦另外一個官方工具 vue-class-component 來解決這個問題。
扯了那麼多,總結一句話就是:TS 和 Vue 能搞。
那麼,下面直接開搞。
首先,天然是安裝,typescript 和其餘依賴沒有什麼不一樣,直接經過 npm 安裝就能夠了。由於項目以前用的是 webpack,因此還要裝上另外兩個 loader:awesome-typescript-loader
和 source-map-loader
。
npm i typescript awesome-typescript-loader source-map-loader -S
有了 loader 那麼讓 webpack 去管理 ts 的文件也就垂手可得了。別忘了在 resolve
-> extensions
中添加 .ts
,讓 webpack 可以識別以 ts 結尾的文件。
// ... resolve: { // ... extensions: [".ts", ".js", ".json"] }, module: { rules: [ { test: /\.tsx?$/, loader: "awesome-typescript-loader" }, // ... ] } // ...
這樣 webpack 的配置就完成了,接着在根目錄下添加 tsconfig.json
文件來配置 ts。
tsconfig.json
所包含的屬性並很少,只有 7 個,ms 官方也給出了它的定義文件。但看起來並不怎麼舒服,這裏就翻譯整理一下。(如有誤,還請指出)
files
: 數組類型,用於表示由 ts 管理的文件的具體文件路徑
exclude
: 數組類型,用於表示 ts 排除的文件(2.0 以上支持 Glob)
include
: 數組類型,用於表示 ts 管理的文件(2.0 以上)
compileOnSave
: 布爾類型,用於 IDE 保存時是否生成編譯後的文件
extends
: 字符串類型,用於繼承 ts 配置,2.1 版本後支持
compilerOptions
: 對象類型,設置編譯的選項,不設置則使用默認配置,配置項比較多,後面再列
typeAcquisition
: 對象類型,設置自動引入庫類型定義文件(.d.ts
)相關,該對象下面有 3 個子屬性分別是:
enable
: 布爾類型,是否開啓自動引入庫類型定義文件(.d.ts
),默認爲 false
include
: 數組類型,容許自動引入的庫名,如:["jquery", "lodash"]
exculde
: 數組類型,排除的庫名
如不設定 files
和 include
,ts 默認是 exclude
之外的全部的以 .ts
和 .tsx
結尾的文件。若是,同時設置 files
的優先級最高,exclude
次之,include
最低。
上面都是文件相關的,編譯相關的都是靠 compilerOptions
設置的,接着就來看一看。
屬性名 | 值類型 | 默認值 | 描述 | |
---|---|---|---|---|
allowJs | boolean | false | 編譯時,容許有 js 文件 | |
allowSyntheticDefaultImports | boolean | module === "system" | 容許引入沒有默認導出的模塊 | |
allowUnreachableCode | boolean | false | 容許覆蓋不到的代碼 | |
allowUnusedLabels | boolean | false | 容許未使用的標籤 | |
alwaysStrict | boolean | false | 嚴格模式,爲每一個文件添加 "use strict" | |
baseUrl | string | 與 path 一同定義模塊查找的路徑,詳細參考這裏 |
||
charset | string | "utf8" | 輸入文件的編碼類型 | |
checkJs | boolean | false | 驗證 js 文件,與 allowJs 一同使用 |
|
declaration | boolean | false | 生成 .d.ts 定義文件 |
|
declarationDir | string | 生成定義文件的存放文件夾(2.0 以上) | ||
diagnostics | boolean | false | 是否顯示診斷信息 | |
downlevelIteration | boolean | false | 當 target 爲 ES5 或 ES3 時,提供對 for..of ,解構等的支持 |
|
emitBOM | boolean | false | 在輸出文件頭添加 utf-8 (BOM)字節標記 | |
emitDecoratorMetadata | boolean | false | 詳見 issue | |
experimentalDecorators | boolean | false | 容許註解語法 | |
forceConsistentCasingInFileNames | boolean | false | 不容許不一樣變量來表明同一文件 | |
importHelpers | boolean | false | 引入幫助(2.1 以上) | |
inlineSourceMap | boolean | false | 將 source map 一同生成到輸出文件中 | |
inlineSources | boolean | false | 將 ts 源碼生成到 source map 中,須要同時設置 inlineSourceMap 或 sourceMap |
|
isolatedModules | boolean | false | 將每一個文件做爲單獨的模塊 | |
jsx | string | "preserve" | jsx 的編譯方式 | |
jsxFactory | string | "React.createElement" | 定義 jsx 工廠方法,React.createElement 仍是 h (2.1 以上) |
|
lib | string[] | 引入庫定義文件,能夠是["es5", "es6", "es2015", "es7", "es2016", "es2017", "esnext", "dom", "dom.iterable", "webworker", "scripthost", "es2015.core", "es2015.collection", "es2015.generator", "es2015.iterable", "es2015.promise", "es2015.proxy", "es2015.reflect", "es2015.symbol", "es2015.symbol.wellknown", "es2016.array.include", "es2017.object", "es2017.sharedmemory", "esnext.asynciterable"](2.0 以上) | ||
listEmittedFiles | boolean | false | 顯示輸入文件名 | |
listFiles | boolean | false | 顯示編譯輸出文件名 | |
locale | string | 隨系統 | 錯誤信息的語言 | |
mapRoot | string | 定義 source map 的存放位置 | ||
maxNodeModuleJsDepth | number | 0 | 檢查引入 js 模塊的深度,需同 allowJs 一同使用 |
|
module | string | 指定模塊生成方式,["commonjs", "amd", "umd", "system", "es6", "es2015", "esnext", "none"] | ||
moduleResolution | string | 指定模塊解析方式,["classic" : "node"] | ||
newLine | string | 隨系統 | 行位換行符,"crlf" (windows) 或 "lf" (unix) | |
noEmit | boolean | false | 不顯示輸出 | |
noEmitHelpers | boolean | false | 不在輸出文件中生成幫助 | |
noEmitOnError | boolean | false | 出錯後,不輸出文件 | |
noFallthroughCasesInSwitch | boolean | false | switch 語句中,每一個 case 都要有 break |
|
noImplicitAny | boolean | false | 不容許隱式 any |
|
noImplicitReturns | boolean | false | 函數全部路徑都必須有顯示 return |
|
noImplicitThis | boolean | false | 不容許 this 爲隱式 any |
|
noImplicitUseStrict | boolean | false | 輸出中不添加 "use strict" | |
noLib | boolean | false | 不引入默認庫文件 | |
noResolve | boolean | false | 不編譯三斜槓或模塊引入的文件 | |
noUnusedLocals | boolean | false | 未使用的本地變量將報錯(2.0 以上) | |
noUnusedParameters | boolean | false | 未使用的參數將報錯(2.0 以上) | |
outDir | string | 定義輸出文件的文件夾 | ||
outFile | string | 合併輸出到一個文件 | ||
paths | object | 與 baseUrl 一同定義模塊查找的路徑,詳細參考這裏 |
||
preserveConstEnums | boolean | false | 不去除枚舉聲明 | |
pretty | boolean | false | 美化錯誤信息 | |
reactNamespace | string | "React" | 廢棄。改用jsxFactory |
|
removeComments | boolean | false | 去除註釋 | |
rootDir | string | 當前目錄 | 定義輸入文件根目錄 | |
rootDirs | string [] | 定義輸入文件根目錄 | ||
skipDefaultLibCheck | boolean | false | 廢棄。改用 skipLibCheck |
|
skipLibCheck | boolean | false | 對庫定義文件跳過類型檢查(2.0 以上) | |
sourceMap | boolean | false | 生成對應的 map 文件 | |
sourceRoot | string | 調試時源碼位置 | ||
strict | boolean | false | 同時開啓 alwaysStrict , noImplicitAny , noImplicitThis 和 strictNullChecks (2.3 以上) |
|
strictNullChecks | boolean | false | null 檢查(2.0 以上) |
|
stripInternal | boolean | false | 不輸出 JSDoc 註解 | |
suppressExcessPropertyErrors | boolean | false | 不提示對象外屬性錯誤 | |
suppressImplicitAnyIndexErrors | boolean | false | 不提示對象索引隱式 any 的錯誤 | |
target | string | "es3" | 輸出代碼 ES 版本,能夠是 ["es3", "es5", "es2015", "es2016", "es2017", "esnext"] | |
traceResolution | boolean | false | 跟蹤模塊查找信息 | |
typeRoots | string [] | 定義文件的文件夾位置(2.0 以上) | ||
types | string [] | 設置引入的定義文件(2.0 以上) | ||
watch | boolean | false | 監聽文件變動 |
通常狀況下,tsconfig.json 文件只需配置 compilerOptions
部分。
{ "compilerOptions": { "allowSyntheticDefaultImports": true, "module": "es2015", "removeComments": true, "preserveConstEnums": true, "sourceMap": true, "strict": true, "target": "es5", "lib": [ "dom", "es5", "es2015" ] } }
其中,allowSyntheticDefaultImports
是使用 vue 必須的,而設置 module
則是讓模塊交由 webpack 處理,從而可使用 webpack2 的搖樹。另外,加上allowJs
,這樣就能夠一點點將現有的 js 代碼轉換爲 ts 代碼了。
若是,你在 webpack 中設置過 resolve
-> alias
,那麼,在 ts config 中也須要經過 baseUrl
+ path
的方式來定義模塊查找的方式。
同 js 同樣,ts 也有本身的 lint —— tslint
。
npm i tslint tslint-loader -S
以前項目是經過 webpack 打包的,因此一併把 tslint-loader
也裝上,並修改 webpack loader 的配置。
// ... { test: /\.tsx?$/, enforce: 'pre', loader: 'tslint-loader' }, // ...
同時,在項目目錄下添加 tslint.json
文件。
{ "extends": "tslint:recommended", "rules": { // ... } }
有些推薦的配置和本身的習慣不太同樣,能夠經過 rules
去自定義(查看全部規則)。
tslint 默認都是警告類型,這樣對作遷移也比較方便,也能夠在配置中將提示類型從警告改成錯誤。
配置差很少完了,剩下就是碼代碼了。
this
在 vue 組件中很是常見,但 vue 組件的申明方式沒法讓 typescript 瞭解組件實例所包含的屬性。
export default Vue.component('blog', { template, created() { this.loadBrowserSetting(); this.loadNavList(); this.loadSocialLink(); }, computed: mapGetters(['isDesktop', 'navList', 'socialLinkList', 'title']), methods: mapActions(['loadBrowserSetting', 'loadNavList', 'loadSocialLink']), watch: { 'title': function() { setBlogTitle(this.title); } } });
因此,就須要經過繼承 vue 提供的 ComponentOptions
接口來申明組件所用到的每一個屬性,好比 methods
, getter
中的屬性等。
export interface IBlogContainer extends Vue { title: string; loadBrowserSetting: () => void; loadNavList: () => void; loadSocialLink: () => void; } export default Vue.component('blog', { template, created() { this.loadBrowserSetting(); this.loadNavList(); this.loadSocialLink(); }, computed: mapGetters(['isDesktop', 'navList', 'socialLinkList', 'title']), methods: mapActions(['loadBrowserSetting', 'loadNavList', 'loadSocialLink']), watch: { title() { setBlogTitle(this.title); }, }, } as ComponentOptions<IBlogContainer>);
看上去還不錯?但這還不是最終的方案,能夠更好,那就是一開始提到的 vue-class-component。
vue-class-component
既能夠用於 ts,也可以用於 js。它都讓你的組件定義文件變得至關清晰。將生命週期函數,data
, methods
中的方法直接定義在 class 上,而將其餘的組件 options
傳入註解中就能夠了。
@Component({ computed: mapGetters(['isDesktop', 'navList', 'socialLinkList', 'title']), methods: mapActions(['loadBrowserSetting', 'loadNavList', 'loadSocialLink']), template, watch: { title() { setBlogTitle((this as BlogContainer).title); }, }, }) class BlogContainer extends Vue { public title: string; public loadBrowserSetting: () => void; public loadNavList: () => void; public loadSocialLink: () => void; public created() { this.loadBrowserSetting(); this.loadNavList(); this.loadSocialLink(); } } export default Vue.component('blog', BlogContainer);
須要注意的是,全局組件仍是須要在最後調用 Vue.component
語法來聲明一下。
Vue 服務器渲染會爲某些須要動態獲取數據的組件添加額外的方法,並在服務端接受到請求後調用,這個方法的名字能夠是任意的(一般是 preFetch
或 asyncData
)。一樣的,它並無在 vue 的定義文件中被定義,因此,須要各自去定義它。
在同一個項目中,組件獲取數據的方法是相同的,因此能夠擴展示有的 vue 的類型定義,而不用一遍遍的重複申明。
// vue.d.ts import Vue from 'vue'; import { Store } from 'vuex'; import VueRouter from 'vue-router'; import { IRootState } from 'vuexModule/index'; declare global { interface Window { __INITIAL_STATE__: any } } declare module 'vue/types/options' { interface ComponentOptions<V extends Vue> { preFetch?: (store: Store<IRootState>, router?: VueRouter) => Promise<any> } }
一樣的方法也能夠用來擴展瀏覽器的定義文件,好比一些嘗試性的 API。
// pwa.d.ts interface ShareInfo { title: string, url?: string, text?: string } interface Navigator { readonly share: (o: ShareInfo) => Promise<void> }
再回到剛剛的組件服務器端獲取數據。
衆所周知,在使用 vuex 管理的系統獲取數據一般使用的是調一個 action 方法,然而,action 將變更傳遞到 mutation。其中,action 須要接受一個對象做爲參數,其中包含了 commit
和 dispatch
方法。在 Redux 中,這個參數是 store,但在 vue 中,它的類型是 ActionContext<S, R>
。
同時,能夠看到剛剛的 preFetch
方法的簽名是 store
和 router
。儘管,store
中也包含 commit
和 dispatch
方法,但它的類型是 Store<R>
。這能夠在原先的 js 中順利運行,但在 ts 中,類型不一樣是會報錯的。因此,這時你須要一箇中間方法將傳入的 Store<R>
類型轉換爲 ActionContext<S, R>
。
這裏推薦你們借鑑 vuex-typescript 中 getStoreAccessors
的實現方法。(本身寫得不太好,不夠通用,就不貼出來了)
在以前一篇關於 vue 2.3 SSR 升級手冊中有提到過,
由於 node 端服務啓動後,vue 的實例就被初始化完成,全部的請求會公用這同一個實例,這就可能形成混亂。因此爲每一個請求返回一個新的 vue 的實例是一個比較好的處理方法,router 和 store 一樣適用這個道理。
的確,我也這樣作了。但在此次升級過程當中,我仍是發現了原先的一個 bug,甚至能夠說是大 issue。
先來看一眼,原先的代碼
// vuex/index.js import modules from './module'; Vue.use(Vuex); const createStore = () => new Vuex.Store({ modules, strict: true }); export default createStore;
是否是以爲沒問題?返回的是一個方法,方法每次調用會返回一個新的 store 對象。的確!
繼續看下去
// vuex/module/index.js import browser from './browser'; import home from './home'; import aboutMe from './about-me'; import post from './post'; import site from './site'; import tags from './tags'; export default { browser, site, aboutMe, home, post, tags };
是否是發現什麼了?沒錯。問題就在於,store
的確是新的對象了,但 modules
由於是對象引用的關係,因此永遠是同一個。以此類推,modules
下面的每一個模塊也有着一樣的問題。
記住:在服務器渲染中,老是經過方法返回新的實例。
首先,最直觀的體會就是 webstorm 對 typescript 的支持很是差,代碼提示作的還不錯,但類型檢測,錯誤提示等等能夠說是幾乎沒有。而同是微軟出品的 vscode,天然在這些方面都有着良好的表現。
VScode,你值得擁有。
PS:沒用過的童鞋能夠用一下試試,真的好用。(用下來除了 git 操做比 ws 用起來麻煩一點,其餘都很棒,牆裂安利...)
.ts
之外類型的文件在 webpack 中能夠引入各式各樣的文件,只要你裝了相應的 loader,好比 json
, scss
, jpg
文件等等。但這些文件在 ts 裏引入時,就有問題了,ts 的模塊是沒法理解這些文件的,ts 的模塊只負責對 .tsx?
或 .jsx?
文件類型的編譯。
這時能夠添加一個定義文件來 hack 它。
// support-loader.d.ts declare module "*.json" { const value: any; export default value; } declare module "*.html" { const value: any; export default value; } declare module "*.jpg" { const value: any; export default value; } // ...
process.env
你們確定很熟悉 process.env
這個變量,這裏也就很少解釋了。雖然你們都熟悉它,但 ts 不瞭解它,不知道它是什麼類型,因此會報錯。
遇到這個問題,能夠經過安裝 @types/node
來解決。
npm install @types/node
Typescript 2.0 以後,ts 經過 npm 來安裝類定義文件(@types)。
Ts 會默認讀取項目下 node_modules 下面的 @types 中的類定義文件,也能夠經過以前提到的 tsconfig.json
中的 typeRoots
和 types
屬性就行修改。
typeRoots
用於修改查找定義文件的位置,而 types
則是選擇引入哪些定義文件,不填則默認不設限制,即 typeRoots
下全部定義文件。
ES6 中新增了一個特性是對象字面量的鍵能夠爲一個變量或一個表達式,像這樣
{ [key]: 'something' }
當它同 ES 6 模塊的默認導出同時使用時,babel-loader
工做正常,但在 awesome-typescript-loader
這裏就出了問題。
You may need an appropriate loader to handle this file type.
直接 export 動態對象字面量就會報錯,但將它們拆分開來就能夠了。(不是很理解其中的緣由,還望大神解惑)
// error... export default { [SomeAction](state) { /* ... */ } } // compile success const mutations = { [SomeAction](state) { /* ... */ } }; export default mutations;
ps:
typescript
版本爲 2.4.1,awesome-typescript-loader
版本爲 3.2.1。
至此,客戶端升級至 typescript 就完成了。(服務端由於類型定義的問題沒有所有轉換完成,還得再琢磨琢磨。)
總的來講,就如本文最初講,ts 從數據類型、結構入手,經過靜態類型檢測來加強你代碼的健壯性,從而避免 bug 的產生。
與此同時,vue 也有解決方案(vue-class-component)能夠與 ts 結合得很是棒。