写在前面
TypeScript 已经出来很久了,很多大公司很多大项目也都在使用它进行开发。上个月,我这边也正式跟进一个对集团的大型运维类项目。
项目要做的事情大致分为以下几个大模块
一站式管理平台
规模化运维能力
预案平台
巡检平台
全链路压测等
每一个模块要做的事情也很多,由于牵扯到公司业务,具体要做的一些事情这里我就不一一列举了,反正项目整体规模还是很大的。
一、关于选型
在做了一些技术调研后,再结合项目之后的开发量级以及维护成本。最终我和同事在技术选型上得出一致结论,最终选型定为 Vue 最新全家桶 + TypeScript。
那么问题来了,为什么大型项目非得用 TypeScript 呢,ES6、7 不行么?
其实也没说不行,只不过我个人更倾向在一些协作开发的大型项目中使用 TypeScript 。下面我列一些我做完调研后自己的一些看法
首先,TypeScript 具有类型系统,且是 JavaScript 的超集。 JavaScript 能做的,它能做。JavaScript 不能做的,它也能做。
其次,TypeScript 已经比较成熟了,市面上相关资料也比较多,大部分的库和框架也读对 TypeScript 做了很好的支持。
然后,保证优秀的前提下,它还在积极的开发完善之中,不断地会有新的特性加入进来
JavaScript 是弱类型并且没有命名空间,导致很难模块化,使得其在大型的协作项目中不是很方便
vscode、ws 等编辑器对 TypeScript 支持很友好
TypeScript 在组件以及业务的类型校验上支持比较好,比如
// 定义枚举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: [] }
配合好编辑器,如果不按照定义好的类型来的话,编辑器本身就会给你报错,而不会等到编译才来报错
命令空间 + 接口申明更方便类型校验,防止代码的不规范
比如,你在一个 ajax.d.ts 文件定义了 ajax 的返回类型
declare namespace Ajax { // axios 返回数据 export interface AxiosResponse { data: AjaxResponse } // 请求接口数据 export interface AjaxResponse { code: number, data: object | null | Array<any>, message: string } }
然后在请求的时候就能进行使用
this.axiosRequest({ key: 'idc' }).then((res: Ajax.AjaxResponse) => { console.log(res) })
可以使用 泛型 来创建可重用的组件。比如你想创建一个参数类型和返回值类型是一样的通用方法
function foo<T> (arg: T): T { return arg }let output = foo('string') // type of output will be 'string'
再比如,你想使用泛型来锁定代码里使用的类型
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 进行项目初始化的,初始化选项如下
生成的目录结构如下
├── 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、改造后的结构
显然这些是不能够满足正常业务的开发的,所以我这边做了一版基础建设方面的改造。改造完后项目结构如下
├── 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
中写入
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
相关配置(只列举了一小部分,具体请小伙伴们自己根据自身业务进行配置)
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
中做一些全局的拦截操作,这里我在拦截器里统一处理了取消重复请求,如果你的业务不需要,请自行去掉
import axios from 'axios'import config from './config'// 取消重复请求let pending: Array<{ url: string, cancel: Function}> = []const cancelToken = axios.CancelTokenconst 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
文件中写入
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 大致目录如下
├── home // 主目录 ├── index.ts // vuex state getters mutations action 管理 ├── interface.ts // 接口管理 └── index.ts // vuex 主入口
在 home/interface.ts
中管理相关模块的接口
export interface HomeContent { name: string m1?: boolean }export interface State { count: number, test1?: Array<HomeContent> }
然后在 home/index.ts
定义相关 vuex
模块内容
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 }
然后我们就能在页面中使用了啦
<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'@Componentexport 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 fromvue-class-component
举个
import { Component, Prop, Watch, Vue } from 'vue-property-decorator'@Componentexport 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 地址,这里就不一一做说明了}
解析之后会变成
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 文件中使用上的不便的问题。
copy 一个官方的
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')@Componentexport 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 文件中的用法介绍的也差不多了。我也相信小伙伴看到这,对其大致的语法糖也有了一定的了解了
3、一些建议
如果定义了
.d.ts
文件,请重新启动服务让你的服务能够识别你定义的模块,并重启 vscode 让编辑器也能够识别(真的恶心)设置好你的
tsconfig
,比如记得把strictPropertyInitialization
设为 false,不然你定义一个变量就必须给它一个初始值。千万管理好你的路由层级,不然到时连正则都拯救不了你
业务层面千万做好类型检测或者枚举定义,这样不仅便利了开发,还能在出了问题的时候迅速定位
跨模块使用 vuex,请直接使用
rootGetters
如果你需要改造某组件库主题,请单开一个文件进行集中管理,别一个组件分一个文件去改动,不然编译起来速度堪忧
能够复用团队其他人开发好的东西,尽量别去开发第二遍,不然到时浪费的可能就不是单纯的开发时间,还有 code review 的时间
诸如此类的还有一堆,但更多的得你们自己去探寻。接下来,我将谈谈大型项目中团队协作的一些规范
四、如何进行团队协作
一个大的项目,肯定是多人一起并行,里面不仅有前端团队的合作,还有与产品同学的需求探(si)讨(bi),以及和后端同学的联调,甚至于还需要自己或者依靠 SRE 进行一些服务的配置。
1、前端开发规范
既然项目是基于 vue + ts 的且是多人协作,那么开发规范肯定是必须的,这样可以让并行开发变的容易起来。下面,我从当时我制定的规范中抽出一些给小伙伴们做个参考(仅做参考哈)
i. 页面开发摆放顺序
HTML
TypeScript
CSS
<template></template><script lang="ts"></script><style lang="scss"></style>
ii. CSS 规则(使用 BEM 命名规则避免样式冲突,不使用 scoped)
<template> <div class="home"> <div class="home__count">{{ count }}</div> <div class="home__input"></div> </div></template><style lang="scss">.home { text-align: center; &__count {} &__input {} }</style>
iii. vue 文件中 TS 上下文顺序
data
@Prop
@State
@Getter
@Action
@Mutation
@Watch
生命周期钩子
beforeCreate(按照生命周期钩子从上到下)
created
beforeMount
mounted
beforeUpdate
updated
activated
deactivated
beforeDestroy
destroyed
errorCaptured(最后一个生命周期钩子)
路由钩子
beforeRouteEnter
beforeRouteUpdate
beforeRouteLeave
computed
methods
组件引用,mixins,filters 等放在 @Component 里面
<script lang="ts">@Component({ components: { HelloWorld }, mixins: [ Emitter ] })export default class Home extends Vue { city: string = '上海' @Prop({ type: [ Number, String ], default: 16 }) size: number | string @State('state') state: StateInterface @Getter('count') count: Function @Action('getTodayWeather') getTodayWeather: Function @Mutation('DECREMENT') DECREMENT: Function @Watch('count') onWatchCount (val: number) { console.log('onWatchCount', val) } // computed get styles () {} created () {} mounted () {} destroyed () {} // methods getCityWeather (city: string) {} }</script>
iv. vuex 模块化管理
store 下面一个文件夹对应一个模块,每一个模块都有一个 interface 进行接口管理,具体例子上文中有提到
v. 路由引入姿势
路由懒加载,上文中也有例子
vi. 文件命名规范
单词小写,单词之间用 '-' 分隔,如图
名词在前,动词在后,如图
相同模块描述在前,不同描述在后
2、与产品 + 后端等协作
千万记住以下三点:
要有礼貌的探(si)讨(bi)
要很有礼貌的探(si)讨(bi)
要非常有礼貌的探(si)讨(bi)
具体细节我曾在知乎里面有过回答,这里不赘述了。传送门:前后端分离,后台返回的数据前端没法写,怎么办?
3、人效提升
上一个点,谈了一下开发层面的的协作。这里,谈一谈人效提升。
大家都知道,一个项目是否能够在预定的期限中完成开发 + 联调 + 测试 + 上线,最重要的因为就是每个人做事的效率。我们不能保证大家效率都很高,但我们得保障自己的开发效率。
需求一下来,首先我们得保证的就是自己对需求的认知。一般对于老手来说,把需求过一遍心里就大致清楚做完这个需求大概需要多少时间,而新手则永远对完成时间没有一个很好的认知。
那么,如何提升自己的开发效率呢?
把需求拆分成模块
把模块中的东西再次拆分成小细节
评估小细节自身的开发时间
评估小细节中某些可能存在的风险点的开发时间
评估联调时间
预留测试 + 修复 BUG 的时间节点
预留 deadline (一般来说是 1 *(1 + 0.2))
安排好自己的开发节点,以 1D(一天)作为单位
记录好风险原因、风险点以及对应的规避方案
如若预感要延期,需及时给出补救方案(比如:加班)
记录 BUG 数量,以及对应的 BUG 人员(真的不是为了甩锅)
总结
文章到这也差不多了。聊了聊项目立项前的选型,也聊了聊项目初期的基础建设,还聊了聊 ts 在 .vue 中的使用,甚至项目开发中团队协作的一些事情也有聊。但毕竟文笔有限,很多点并不能娓娓道来,大多都是点到为止。如果觉得文章对小伙伴们有帮助的话,请不要吝啬你手中的赞
原文链接:https://my.oschina.net/qiangdada/blog/1861102