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

首发于我的博客欢迎订阅

相关文章
相关标签/搜索