转载地址: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 。下面我列一些我做完调研后自己的一些看法
- 首先,TypeScript 具有类型系统,且是 JavaScript 的超集。 JavaScript 能做的,它能做。JavaScript 不能做的,它也能做。
- 其次,TypeScript 已经比较成熟了,市面上相关资料也比较多,大部分的库和框架也读对 TypeScript 做了很好的支持。
- 然后,保证优秀的前提下,它还在积极的开发完善之中,不断地会有新的特性加入进来
- JavaScript 是弱类型并且没有命名空间,导致很难模块化,使得其在大型的协作项目中不是很方便
- vscode、ws 等编辑器对 TypeScript 支持很友好
- 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 }
interface SrvItem { val: string, key: string }
interface SrvType { name: string, key: string, state?: StateEnum, item: Array<SrvItem> }
const types: SrvType = { name: '', key: '', item: [] }
|
配合好编辑器,如果不按照定义好的类型来的话,编辑器本身就会给你报错,而不会等到编译才来报错
- 命令空间 + 接口申明更方便类型校验,防止代码的不规范
比如,你在一个 ajax.d.ts 文件定义了 ajax 的返回类型
1 2 3 4 5 6 7 8 9 10 11 12 13
| declare namespace Ajax { 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 2 3 4 5
| function foo<T> (arg: T): T { return arg } let output = foo('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 }
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) }, timeout: 30000, withCredentials: true, responseType: 'json', xsrfCookieName: 'XSRF-TOKEN', xsrfHeaderName: 'X-XSRF-TOKEN', maxRedirects: 5, maxContentLength: 2000, validateStatus: function (status: number) { return status >= 200 && status < 300 }, 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 { 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({})
里面写 props
、data
等调用起来极其不方便,而 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
@Watch('child') onChildChanged (val: string, oldVal: string) {} @Watch('person', { immediate: true, deep: true }) onPersonChanged (val: Person, oldVal: Person) {}
}
|
解析之后会变成
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
@State foo @Getter bar @Action baz @Mutation qux
created () { this.stateFoo this.stateBar this.getterFoo this.actionFoo({ value: true }) this.mutationFoo({ value: true }) this.moduleGetterFoo } }
|
到这里,ts 在 .vue 文件中的用法介绍的也差不多了。我也相信小伙伴看到这,对其大致的语法糖也有了一定的了解了