一. 如何集成TS到当前技术栈
1.1 安装依赖
安装typescript和ts-loader,注意ts和ts-loader版本需要配套。由于当前技术栈的vue版本为2.5.2,建议typescript版本选择2.7.2,对应的ts-loader版本为2.0.3。
npm install --save-dev typescript@2.7.2 ts-loader@2.0.3
1.2 添加TypeScript配置文件
安装好相应依赖之后,需要添加TS的配置文件——tsconfig.json,主要告诉编译器要如何编译Ts文件。具体的配置如下:
{
"compilerOptions": {
"outDir": "./built/",
"sourceMap": true,
"strict": true,
"module": "es2015",
"moduleResolution": "node",
"target": "es5",
"experimentalDecorators": true,
"noImplicitAny": false,
"lib": ["es2016", "dom"],
"jsx": "preserve"
},
"include": [
"./src/**/*"
]
}
该配置可以根据需要自行调整。在配置的过程中碰到两个小问题,这里稍微提一下
- 如果想使用es7新属性,如Array.prototype.includes,async await等,则需要在lib里添加es2016。(lib主要是指定编译过程中需要引入的库文件)
- 如果是现有项目中添加Ts的话,建议把Ts的一些约束条件先放宽,重构到一定地步的时候再严格些。
1.3 添加webpack配置
接下来需要修改webpack文件,添加下列配置
{
test: /\.tsx?$/,
loader: 'ts-loader',
exclude: /node_modules/,
options: {
appendTsSuffixTo: [/\.vue$/],
}
}
如果想在SFC中使用TS的话,需要添加appendTsSuffixTo属性。
之后在extensions中添加ts文件
extensions: ['.js', '.vue', '.json', '.ts']
1.4 Vue SFC改造
1.4.1 修改入口文件
修改项目入口文件为main.ts。(这步一定要做,ts编译器通过main.ts去寻找依赖关系)
在入口文件修改为.ts之后,会出现下列问题
主要因为ts此时不能识别.vue文件,需要为其添加描述文件。
1.4.2 添加vue描述文件
描述文件如下:
declare module "*.vue" {
import Vue from "vue";
export default Vue;
}
现在ts能识别vue后缀的文件了。但有个需要注意的小细节就是,当导入vue后缀的文件时,不能省略后缀名(.vue)。
1.4.3 修改Vue SFC
修改SFC中的lang="ts",这样就完成了一个SFC文件的改造。但是仅仅这样的话,ts的类型推断不会起作用。
想让类型推断起作用,主要通过两种方法,由于第二种的写法和原SFC的写法差异较大(class的写法,后面有提及),所以这里只试验了第一种,选取了biz后端管理系统中的headernav组件进行改造,前后代码对比如下:
从上图中可以看出代码书写方式差别不算很大。
1.4.4 渐进式修改
此次重构的页面主要有biz后台管理系统的list/doctor,list/patient, HeadNav以及部分ajax请求文件。在重构的过程中, 发现这个过程可以是渐进式的。
在DoctorList.vue中,没有修改之前的js代码。
而在PatientList.vue中,我们重构了js-> ts
另外在api请求部分,把user.js -> user.ts
而ajax.js还是之前的js版本
即使是在.ts文件中,类型定义也可以不写,灵活度很高。
从重构部分页面的情况可以看到,整个项目中可以同时存在js和ts文件, 甚至在同一个.ts文件中,类型定义也是可选的。
由此可见,利用ts可以对项目进行渐进式的修改。
二.Ts在当前技术栈中的优势
2.1 静态类型检查
以PatientList.vue为例, 给loading属性添加布尔类型,如果之后不注意再给loading赋了其他类型的值,ts程序在编译时就会报错。
data () {
...
loading: true as Boolean
...
}
getList (data) {
...
this.loading = "true"
},
再来看一个稍微复杂点的场景,同样是在PatientList.vue中.
listParams () {
let self = this
let data: IListParams = {
limit: self.page.limit,
page: self.page.current,
datasource: 0,
real_name: '',
cellphone: '',
dxy_user_name: ''
}
if (self.searchConfirm.datasource > 0) {
data.realName = self.searchConfirm.datasource // error,data上没有realName
}
if (self.searchConfirm.cellphone) {
data.cellphone = 13211111111 // error,cellphone是string类型
}
...
...
return data
}
// 接口描述如下
export interface ISearchData {
datasource: number,
dxy_user_name: string,
cellphone: string,
real_name: string
}
export interface IListParams extends ISearchData{
limit: number,
page: number
}
这里使用IlistParams接口来描述data数据。如果给data上不存在的属性赋值,或者赋予的值不是相应类型的,在ts编译时都会报错。
2.2 VSCode的支持
VSCode对TypeScript的支持非常不错,主要体现在智能感知,补全和导航等方面。同样以PatientList.vue中的部分代码来进行说明
// interface描述
export interface IUserAPI {
...
getPatientList: (data: any) => Promise<IPatientResponse>
...
}
export interface IPatientResponse {
code: number,
msg: string,
success: boolean,
data: Array<IPatientInfo>,
paging: Ipaging
}
export interface IPatientInfo {
id: number,
cellphone: string | null,
datasource: number,
login_time: string | null,
nick_name: string | null,
real_name: string | null,
user_name: string | null
}
由于设置了getPatientList的返回值是IPatientResponse类型。所以VsCode能通过类型声明提供智能提示,如下所示
这给编码带来了很大的便利。如果想对类型声明了解更多,可以通过VSCode提供的导航功能,快速定位到类型声明的位置。
2.3 让代码更容易阅读和理解
静态类型让代码更加易于阅读和理解。对比TS版和JS版的getPatientList方法
JS版本:
getPatientList: data =>
ajax.request({
url: '/mgmt/users/patients/search',
data,
method: 'post'
})
Ts版本:
getPatientList: (data: IPatientRequest) => Promise<IPatientResponse>
export interface IPatientRequest {
page: number,
size: number
}
export interface IPatientResponse {
code: number,
msg: string,
success: boolean,
data: Array<IPatientInfo>,
paging: Ipaging
}
export interface IPatientInfo {
id: number,
cellphone: string | null,
datasource: number,
login_time: string | null,
nick_name: string | null,
real_name: string | null,
user_name: string | null
}
读js版本的时候,唯一可以确定的是这个方法接受一个参数,不能确定参数的类型。然后返回一个ajax.request执行后的结果。那么这个结果是什么类型,数据结构是什么样子,则完全没有头绪。
如果想放心调用这个函数,除了阅读源码或查看文档外,别无他法。由于文档的时效性和阅读源码所花销的时间,这两者都算不上一个好方法。
再来看Ts版本的,代码量比js的多一些。但是这些代码却提供了很多信息。
- 首先说明了参数是什么类型,且参数的数据结构也表达的很清晰。
- 返回值是什么类型,返回值的数据类型是什么样子的,包含哪些属性,这些都一清二楚。
通过这些信息,在不阅读源码和文档的情况下,也能很好的理解函数的表达的意思。还有一点需要注意的是,在.ts中类型声明是可选的,js版本的getPatientList同样在.ts文件中能很好的被编译。
2.4 支持ES6,7新特性
TypScript同样对新的ES6,7语法支持良好,在patientList.vue中,使用了几个常用的es7新语法—async,await,spread,发现它们的支持情况都非常好。
async/awiat:
async getList (data) {
...
const res = await API.getPatientList(data)
...
},
spread:
async getProductList (data: any) {
...
const res = await APICommon.getProductList(data)
if (res.code === 0) {
self.productList = [...self.productList, ...res.data.items] // spread
}
}
2.5 支持接口
接口会强制定义契约,不符合契约的部分,Ts编译器不会编译通过。这样虽然牺牲了部分灵活性,但是代码却更严谨。
举个例子说明一下,在api/user中定义了PatientResponse,PatientInfo接口来约束patient/list返回数据的类型。
export interface IPatientResponse {
code: number,
msg: string,
success: boolean,
data: Array<IPatientInfo>,
paging: Ipaging
}
export interface IPatientInfo {
id: number,
cellphone: string | null,
datasource: number,
login_time: string | null,
nick_name: string | null,
real_name: string | null,
user_name: string | null
}
export interface Ipaging {
total: number,
current: number,
pages: number,
size: number
}
在使用返回值的过程中,只能使用接口约束好的类型,否则编译阶段会报错。
export interface IPageQuery {
limit: number,
current: number,
total: number
}
page: {
limit: 10,
current: 1,
total: 10
} as IPageQuery
...
async getList (data) {
let self = this
self.loading = true
const res = await API.getPatientList(data)
if (res.code === 0) {
self.list = res.data
self.page.total = res.paging.total1 // error 接口契约中pagin没有total1属性
self.page.current = '10' //error‘ 接口契约中current的数据类型是number
}
}
...
2.6 利于重构
TypeScript在重构上的优势也很明显。在日常开发过程中,经常会出现后端接口更改的情况。这对于前端开发人员是很头痛的事情。往往需要在全局中搜索需要修改的变量,并且逐一替换。
如果使用Ts的话,就方便很多,举个例子,当后端修改数据结构时,例如把字段名paging修改为pages。
那现在需要做的事首先是在接口中修改paging->pages
export interface IPatientResponse {
...
paging: Ipaging // 之前
pages: Ipaging // 现在要求这样
}
修改完之后,由于VSCode对TS的支持,左边的资源目录树会提示出错误的地方。
然后通过VSCode中的F8快捷键跳转到错误位置。
修改完之后,继续F8跳过下一个错误的地方进行修改。(.vue文件不支持F2重命名,原因还未找到,如果有重命名功能,完成类似的重构会更加简单)
再举个例子,重构的时候经常也会需要更改函数的签名啥的,如果引用的地方没有去修改,那只能在运行时发现问题。所以出了bug调试起来还是很麻烦的,而Ts则不同,在编码的时候就能及时发现哪里出错了。
// 之前的接口描述
getPatientList: (data: IPatientRequest) => Promise<IPatientResponse>
// 现在修改成了这样
getPatientList: (data: IPatientRequest, config: Object) => Promise<IPatientResponse>
2.7 更早的错误提示
由于Ts是静态类型语言,进行编译的时候就能发现问题所在,而不用等到运行时,这样就能解决很多难定位的bug。
如我们在2.1提到的例子
data () {
...
loading: false
...
}
getList (data) {
...
this.loading = "true"
},
如果不小心给this.loading赋了其他类型的值。之后如果有其他需要根据loading的true/false值执行的逻辑,可能会引发问题,不好进行调试。而TS能在编译的时候就给出相应提示,做到快速定位问题。
再比如,以PatientList.vue中的代码为例,调用API.getPatientList的时候没有传入参数,Ts的编译器能马上就发现错误,把它们扼杀在编译阶段。
async getList (data) {
...
const res = await API.getPatientList()
...
}
三.Ts在当前技术栈中的劣势
3.1 迁出成本高
从前面章节的描述中,可以看出项目中迁入ts的成本不高,但是迁出的成本却相对很高。特别是和vue结合时,会有部分非常规写法。比如SFC的props中需要使用如下方法hack类型定义。
listItem: {
type: Array as () => string[],
required: true
}
另外如果你使用了基于类和装饰器的写法
import { Vue, Component, Prop } from "vue-property-decorator";
@Component
export default class HelloDecorator extends Vue {
@Prop() name!: string;
@Prop() initialEnthusiasm!: number;
enthusiasm = this.initialEnthusiasm;
increment() {
this.enthusiasm++;
}
decrement() {
if (this.enthusiasm > 1) {
this.enthusiasm--;
}
}
get exclamationMarks(): string {
return Array(this.enthusiasm + 1).join('!');
}
}
当使用过这些特性的时候,再想把typescript从项目中迁出,就非常麻烦了。
3.2 template部分支持不好
SFC中的template部分没有静态类型检查和IDE智能提醒。除非使用jsx书写dom。这点可能是目前vue+ts项目结合过程中最大的缺陷了。
如果在template中能进行静态类型检查,就再也不会出现下列种种熟悉的错误了。
但好消息是vue核心开发成员表示正在计划开发template的静态类型检查。等到支持那天,绝对会有更多使用ts的理由。
3.3 更多的编码量
由于要编写更多的类型定义,所以相应的代码量也会增多,比如2.3的例子中,js实现getPatientList只需要6行代码,而Ts版本光详细的类型声明就有20多行。
getPatientList: data =>
ajax.request({
url: '/mgmt/users/patients/search',
data,
method: 'post'
})
getPatientList: (data: IPatientRequest) => Promise<IPatientResponse>
export interface IPatientRequest {
page: number,
size: number
}
export interface IPatientResponse {
code: number,
msg: string,
success: boolean,
data: Array<IPatientInfo>,
paging: Ipaging
}
export interface IPatientInfo {
id: number,
cellphone: string | null,
datasource: number,
login_time: string | null,
nick_name: string | null,
real_name: string | null,
user_name: string | null
}
3.4 引入.vue文件问题
需要为引入的每个vue文件添加.vue后缀,而且需要在每个SFC文件中引入vue。这无形中增加很多代码量。
<script lang="ts">
import Vue from 'vue'
import HeaderNav from '../../components/HeaderNav.vue'
...
...
3.5 部分第三方库需要编辑类型定义文件
部分第三库可能没有类型定义文件,在ts文件中引入则会有问题。写了types的还好,只要去npm install @types/xxx就行了。而如果没有,则需要使用者自己去定义。如biz_system项目中引入的iview组件,在@types/下搜索不到它的类型声明文件,所有只能自己编辑它的类型声明。
declare module "iview" {
const iview: any;
export default iview;
}
这里声明的是最简单的any类型,如果需要更精细的控制,则需要书写更多代码。
四.下一步计划
4.1 vuex和typescript结合
由于时间原因,没有去探究vuex+typescript的可行性。但考虑到当前技术栈使用的是vuex3.x版本,已有支持vuex的types声明,所以typescript+vuex应该问题不大,但还是需要进一步去验证。
4.2 更多vue属性测试
除了实验一些基础的vue用法之外,更多vue高阶功能如mixin,自定义指令等并没有去测试,下一步工作需要去验证这些内容的可行性。
4.3 探究Class Component书写SFC的可行性
阅读的很多文章里,都提到了使用class component+装饰器去实现vue+typescript。但是由于代码改动量可能较大,之前也一直没有去实验。下一步工作想去验证它的优势在哪里?是否真的又能提供哪些优势?
4.4 使用TypeScript去重构ajax
通过之前对biz后端管理系统进行的部分重构,体会到了ts"强类型化"ajax接口(即为每个ajax请求的参数和返回对象类型做定制的接口)能带来的好处。不管是在code过程中的代码提示,还是修改接口数据之后重构的过程,ts都能带来很多好处。
五.参考文献
- 如何用typescript编写vue项目
- typescript入门教程
- typescript handbook
- 从 JavaScript 到 TypeScript 6 Vue 引入 TypeScript
- Vue.JS & Typescript: The Platform for our Responsive UI
- Why Typescript
- Why Typescript - 2
- Why Not Typescript
- Why use TypeScript, good and bad reasons
- Typescript with Javascript
- 关于Typescript
- 在vue项目中使用Typescript
- 认识Typescript
- TypeScript配置文件tsconfig简析