转载地址:https://juejin.im/post/5b54886ce51d45198f5c75d7

TypeScript + 大型项目实战

写在前面


TypeScript 已经出来很久了,很多大公司很多大项目也都在使用它进行开发。上个月,我这边也正式跟进一个对集团的大型运维类项目。

项目要做的事情大致分为以下几个大模块

  • 一站式管理平台
  • 规模化运维能力
  • 预案平台
  • 巡检平台
  • 全链路压测等

每一个模块要做的事情也很多,由于牵扯到公司业务,具体要做的一些事情这里我就不一一列举了,反正项目整体规模还是很大的。

一、关于选型

在做了一些技术调研后,再结合项目之后的开发量级以及维护成本。最终我和同事在技术选型上得出一致结论,最终选型定为 Vue 最新全家桶 + TypeScript。

那么问题来了,为什么大型项目非得用 TypeScript 呢,ES6、7 不行么?

![](https://user-gold-cdn.xitu.io/2018/7/22/164c239019ab71a9?imageView2/0/w/1280/h/960/format/webp/ignore-error/1)

其实也没说不行,只不过我个人更倾向在一些协作开发的大型项目中使用 TypeScript 。下面我列一些我做完调研后自己的一些看法

  1. 首先,TypeScript 具有类型系统,且是 JavaScript 的超集。 JavaScript 能做的,它能做。JavaScript 不能做的,它也能做。
  2. 其次,TypeScript 已经比较成熟了,市面上相关资料也比较多,大部分的库和框架也读对 TypeScript 做了很好的支持。
  3. 然后,保证优秀的前提下,它还在积极的开发完善之中,不断地会有新的特性加入进来
  4. JavaScript 是弱类型并且没有命名空间,导致很难模块化,使得其在大型的协作项目中不是很方便
  5. vscode、ws 等编辑器对 TypeScript 支持很友好
  6. TypeScript 在组件以及业务的类型校验上支持比较好,比如
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 定义枚举
const enum StateEnum {
TO_BE_DONE = 0,
DOING = 1,
DONE = 2
}

// 定义 item 接口
interface SrvItem {
val: string,
key: string
}

// 定义服务接口
interface SrvType {
name: string,
key: string,
state?: StateEnum,
item: Array<SrvItem>
}

// 然后定义初始值(如果不按照类型来,报错肯定是避免不了的)
const types: SrvType = {
name: '',
key: '',
item: []
}

配合好编辑器,如果不按照定义好的类型来的话,编辑器本身就会给你报错,而不会等到编译才来报错

  1. 命令空间 + 接口申明更方便类型校验,防止代码的不规范

比如,你在一个 ajax.d.ts 文件定义了 ajax 的返回类型

1
2
3
4
5
6
7
8
9
10
11
12
13
declare namespace Ajax {
// axios 返回数据
export interface AxiosResponse {
data: AjaxResponse
}

// 请求接口数据
export interface AjaxResponse {
code: number,
data: object | null | Array<any>,
message: string
}
}

然后在请求的时候就能进行使用

1
2
3
this.axiosRequest({ key: 'idc' }).then((res: Ajax.AjaxResponse) => {
console.log(res)
})
  1. 可以使用 泛型 来创建可重用的组件。比如你想创建一个参数类型和返回值类型是一样的通用方法
1
2
3
4
5
function foo<T> (arg: T): T {
return arg
}
let output = foo('string') // type of output will be 'string'

再比如,你想使用泛型来锁定代码里使用的类型

1
2
3
4
5
6
7
8
9
10
11
12
interface GenericInterface<T> {
(arg: T): T
}

function foo<T> (arg: T): T {
return arg
}

// 锁定 myFoo 只能传入 number 类型的参数,传其他类型的参数则会报错
let myFoo: GenericInterface<number> = foo
myFoo(123)

总之,还有很多使用 TypeScript 的好处,这里我就不一一列举了,感兴趣的小伙伴可以自己去查资料

二、基础建设

1、初始化结构

我这边使用的是最新版本脚手架 vue-cli 3 进行项目初始化的,初始化选项如下

生成的目录结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
├── public                          // 静态页面
├── src // 主目录
├── assets // 静态资源
├── components // 组件
├── views // 页面
├── App.vue // 页面主入口
├── main.ts // 脚本主入口
├── registerServiceWorker.ts // PWA 配置
├── router.ts // 路由
├── shims-tsx.d.ts // 相关 tsx 模块注入
├── shims-vue.d.ts // Vue 模块注入
└── store.ts // vuex 配置
├── tests // 测试用例
├── .postcssrc.js // postcss 配置
├── package.json // 依赖
├── tsconfig.json // ts 配置
└── tslint.json // tslint 配置

2、改造后的结构

显然这些是不能够满足正常业务的开发的,所以我这边做了一版基础建设方面的改造。改造完后项目结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
├── public                          // 静态页面
├── scripts // 相关脚本配置
├── src // 主目录
├── assets // 静态资源
├── filters // 过滤
├── lib // 全局插件
├── router // 路由配置
├── store // vuex 配置
├── styles // 样式
├── types // 全局注入
├── utils // 工具方法(axios封装,全局方法等)
├── views // 页面
├── App.vue // 页面主入口
├── main.ts // 脚本主入口
├── registerServiceWorker.ts // PWA 配置
├── tests // 测试用例
├── .editorconfig // 编辑相关配置
├── .npmrc // npm 源配置
├── .postcssrc.js // postcss 配置
├── babel.config.js // preset 记录
├── cypress.json // e2e plugins
├── f2eci.json // 部署相关配置
├── package.json // 依赖
├── README.md // 项目 readme
├── tsconfig.json // ts 配置
├── tslint.json // tslint 配置
└── vue.config.js // webpack 配置

3、模块改造

接下来,我将介绍项目中部分模块的改造

i、路由懒加载

这里使用了 webpack 的按需加载 import,将相同模块的东西放到同一个 chunk 里面,在 router/index.ts 中写入

1
2
3
4
5
6
7
8
9
10
11
import Vue from 'vue'
import Router from 'vue-router'

Vue.use(Router)

export default new Router({
routes: [
{ path: '/', name: 'home', component: () => import(/* webpackChunkName: "home" */ 'views/home/index.vue') }
]
})

ii、axios 封装

utils/config.ts 中写入 axios 相关配置(只列举了一小部分,具体请小伙伴们自己根据自身业务进行配置)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import http from 'http'
import https from 'https'
import qs from 'qs'
import { AxiosResponse, AxiosRequestConfig } from 'axios'

const axiosConfig: AxiosRequestConfig = {
baseURL: '/',
// 请求后的数据处理
transformResponse: [function (data: AxiosResponse) {
return data
}],
// 查询对象序列化函数
paramsSerializer: function (params: any) {
return qs.stringify(params)
},
// 超时设置s
timeout: 30000,
// 跨域是否带Token
withCredentials: true,
responseType: 'json',
// xsrf 设置
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',
// 最多转发数,用于node.js
maxRedirects: 5,
// 最大响应数据大小
maxContentLength: 2000,
// 自定义错误状态码范围
validateStatus: function (status: number) {
return status >= 200 && status < 300
},
// 用于node.js
httpAgent: new http.Agent({ keepAlive: true }),
httpsAgent: new https.Agent({ keepAlive: true })
}

export default axiosConfig

接下来,需要在 utils/api.ts 中做一些全局的拦截操作,这里我在拦截器里统一处理了取消重复请求,如果你的业务不需要,请自行去掉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import axios from 'axios'
import config from './config'

// 取消重复请求
let pending: Array<{
url: string,
cancel: Function
}> = []
const cancelToken = axios.CancelToken
const removePending = (config) => {
for (let p in pending) {
let item: any = p
let list: any = pending[p]
// 当前请求在数组中存在时执行函数体
if (list.url === config.url + '&' + config.method) {
// 执行取消操作
list.cancel()
// 从数组中移除记录
pending.splice(item, 1)
}
}
}

const service = axios.create(config)

// 添加请求拦截器
service.interceptors.request.use(
config => {
removePending(config)
config.cancelToken = new cancelToken((c) => {
pending.push({ url: config.url + '&request_type=' + config.method, cancel: c })
})
return config
},
error => {
return Promise.reject(error)
}
)

// 返回状态判断(添加响应拦截器)
service.interceptors.response.use(
res => {
removePending(res.config)
return res
},
error => {
return Promise.reject(error)
}
)

export default service

为了方便,我们还需要定义一套固定的 axios 返回的格式,这个我们直接定义在全局即可。在 types/ajax.d.ts 文件中写入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
declare namespace Ajax {
// axios 返回数据
export interface AxiosResponse {
data: AjaxResponse
}

// 请求接口数据
export interface AjaxResponse {
code: number,
data: any,
message: string
}
}

接下来,我们将会把所有的 axios 放到 vuex 的 actions 中做统一管理

iii、vuex 模块化管理

store 下面,一个文件夹代表一个模块,store 大致目录如下

1
2
3
4
├── home                            // 主目录
├── index.ts // vuex state getters mutations action 管理
├── interface.ts // 接口管理
└── index.ts // vuex 主入口

home/interface.ts 中管理相关模块的接口

1
2
3
4
5
6
7
8
9
export interface HomeContent {
name: string
m1?: boolean
}
export interface State {
count: number,
test1?: Array<HomeContent>
}

然后在 home/index.ts 定义相关 vuex 模块内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import request from '@/service'
import { State } from './interface'
import { Commit } from 'vuex'

interface GetTodayWeatherParam {
city: string
}

const state: State = {
count: 0,
test1: []
}

const getters = {
count: (state: State) => state.count,
message: (state: State) => state.message
}

const mutations = {
INCREMENT (state: State, num: number) {
state.count += num
}
}

const actions = {
async getTodayWeather (context: { commit: Commit }, params: GetTodayWeatherParam) {
return request.get('/api/weatherApi', { params: params })
}
}

export default {
state,
getters,
mutations,
actions
}

然后我们就能在页面中使用了啦

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<template>
<div class="home">
<p>{{ count }}</p>
<el-button type="default" @click="INCREMENT(2)">INCREMENT</el-button>
<el-button type="primary" @click="DECREMENT(2)">DECREMENT</el-button>
<el-input v-model="city" placeholder="请输入城市" />
<el-button type="danger" @click="getCityWeather(city)">获取天气</el-button>
</div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
import { State, Getter, Mutation, Action } from 'vuex-class'

@Component
export default class Home extends Vue {
city: string = '上海'

@Getter('count') count: number
@Mutation('INCREMENT') INCREMENT: Function
@Mutation('DECREMENT') DECREMENT: Function
@Action('getTodayWeather') getTodayWeather: Function

getCityWeather (city: string) {
this.getTodayWeather({ city: city }).then((res: Ajax.AjaxResponse) => {
const { low, high, type } = res.data.forecast[0]
this.$message.success(`${city}今日:${type} ${low} - ${high}`)
})
}
}
</script>

至于更多的改造,这里我就不再介绍了。接下来的小节将介绍一下 ts 在 vue 文件中的一些写法

三、vue 中 ts 的用法

1、vue-property-decorator

这里单页面组件的书写采用的是 vue-property-decorator 库,该库完全依赖于 vue-class-component ,也是 vue 官方推荐的库。

单页面组件中,在 @Component({}) 里面写 propsdata 等调用起来极其不方便,而 vue-property-decorator 里面包含了 8 个装饰符则解决了此类问题,他们分别为

  • @Emit 指定事件 emit,可以使用此修饰符,也可以直接使用 this.$emit()
  • @Inject 指定依赖注入)
  • @Mixins mixin 注入
  • @Model 指定 model
  • @Prop 指定 Prop
  • @Provide 指定 Provide
  • @Watch 指定 Watch
  • @Component export from vue-class-component
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import {
Component, Prop, Watch, Vue
} from 'vue-property-decorator'

@Component
export class MyComponent extends Vue {
dataA: string = 'test'

@Prop({ default: 0 })
propA: number

// watcher
@Watch('child')
onChildChanged (val: string, oldVal: string) {}
@Watch('person', { immediate: true, deep: true })
onPersonChanged (val: Person, oldVal: Person) {}

// 其他修饰符详情见上面的 github 地址,这里就不一一做说明了
}

解析之后会变成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
export default {
data () {
return {
dataA: 'test'
}
},
props: {
propA: {
type: Number,
default: 0
}
},
watch: {
'child': {
handler: 'onChildChanged',
immediate: false,
deep: false
},
'person': {
handler: 'onPersonChanged',
immediate: true,
deep: true
}
},
methods: {
onChildChanged (val, oldVal) {},
onPersonChanged (val, oldVal) {}
}
}

2、vuex-class

vuex-class 是一个基于 Vue、Vuex、vue-class-component 的库,和 vue-property-decorator 一样,它也提供了4 个修饰符以及 namespace,解决了 vuex 在 .vue 文件中使用上的不便的问题。

  • @State
  • @Getter
  • @Mutation
  • @Action
  • namespace

copy 一个官方的🌰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import Vue from 'vue'
import Component from 'vue-class-component'
import {
State,
Getter,
Action,
Mutation,
namespace
} from 'vuex-class'

const someModule = namespace('path/to/module')

@Component
export class MyComp extends Vue {
@State('foo') stateFoo
@State(state => state.bar) stateBar
@Getter('foo') getterFoo
@Action('foo') actionFoo
@Mutation('foo') mutationFoo
@someModule.Getter('foo') moduleGetterFoo

// If the argument is omitted, use the property name
// for each state/getter/action/mutation type
@State foo
@Getter bar
@Action baz
@Mutation qux

created () {
this.stateFoo // -> store.state.foo
this.stateBar // -> store.state.bar
this.getterFoo // -> store.getters.foo
this.actionFoo({ value: true }) // -> store.dispatch('foo', { value: true })
this.mutationFoo({ value: true }) // -> store.commit('foo', { value: true })
this.moduleGetterFoo // -> store.getters['path/to/module/foo']
}
}

到这里,ts 在 .vue 文件中的用法介绍的也差不多了。我也相信小伙伴看到这,对其大致的语法糖也有了一定的了解了