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

TypeScript + 大型项目实战

写在前面


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

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

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

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

一、关于选型

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

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



其实也没说不行,只不过我个人更倾向在一些协作开发的大型项目中使用 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
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
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
├── 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
├── 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
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
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
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
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
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
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
<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
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
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 文件中的用法介绍的也差不多了。我也相信小伙伴看到这,对其大致的语法糖也有了一定的了解了