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