Vue with TypeScript

若是說,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

首先,天然是安裝,typescript 和其餘依賴沒有什麼不一樣,直接經過 npm 安裝就能夠了。由於項目以前用的是 webpack,因此還要裝上另外兩個 loader:awesome-typescript-loadersource-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

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: 數組類型,排除的庫名

如不設定 filesinclude,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 中,須要同時設置 inlineSourceMapsourceMap
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, noImplicitThisstrictNullChecks (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 的方式來定義模塊查找的方式。

Tslint

同 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 默認都是警告類型,這樣對作遷移也比較方便,也能夠在配置中將提示類型從警告改成錯誤。

配置差很少完了,剩下就是碼代碼了。

Vue 中使用 typescript 須要注意的問題

定義組件

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 服務器渲染會爲某些須要動態獲取數據的組件添加額外的方法,並在服務端接受到請求後調用,這個方法的名字能夠是任意的(一般是 preFetchasyncData)。一樣的,它並無在 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 須要接受一個對象做爲參數,其中包含了 commitdispatch 方法。在 Redux 中,這個參數是 store,但在 vue 中,它的類型是 ActionContext<S, R>

同時,能夠看到剛剛的 preFetch 方法的簽名是 storerouter。儘管,store 中也包含 commitdispatch 方法,但它的類型是 Store<R>。這能夠在原先的 js 中順利運行,但在 ts 中,類型不一樣是會報錯的。因此,這時你須要一箇中間方法將傳入的 Store<R> 類型轉換爲 ActionContext<S, R>

這裏推薦你們借鑑 vuex-typescriptgetStoreAccessors 的實現方法。(本身寫得不太好,不夠通用,就不貼出來了)

服務端渲染永遠返回新實例

在以前一篇關於 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 下面的每一個模塊也有着一樣的問題。

記住:在服務器渲染中,老是經過方法返回新的實例。

其餘問題

IDE

首先,最直觀的體會就是 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 中的 typeRootstypes 屬性就行修改。

typeRoots 用於修改查找定義文件的位置,而 types 則是選擇引入哪些定義文件,不填則默認不設限制,即 typeRoots 下全部定義文件。

export default 沒法同 ES6 對象字面量加強同時使用

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 結合得很是棒。

首發於我的博客歡迎訂閱

相關文章
相關標籤/搜索