TypeScript 已经出来好久了,不少大公司不少大项目也都在使用它进行开发。上个月,我这边也正式跟进一个对集团的大型运维类项目。javascript
项目要作的事情大体分为如下几个大模块css
每个模块要作的事情也不少,因为牵扯到公司业务,具体要作的一些事情这里我就不一一列举了,反正项目总体规模仍是很大的。html
在作了一些技术调研后,再结合项目以后的开发量级以及维护成本。最终我和同事在技术选型上得出一致结论,最终选型定为 Vue 最新全家桶 + TypeScript。前端
那么问题来了,为何大型项目非得用 TypeScript 呢,ES六、7 不行么?vue
其实也没说不行,只不过我我的更倾向在一些协做开发的大型项目中使用 TypeScript 。下面我列一些我作完调研后本身的一些见解java
首先,TypeScript 具备类型系统,且是 JavaScript 的超集。 JavaScript 能作的,它能作。JavaScript 不能作的,它也能作。node
其次,TypeScript 已经比较成熟了,市面上相关资料也比较多,大部分的库和框架也读对 TypeScript 作了很好的支持。python
而后,保证优秀的前提下,它还在积极的开发完善之中,不断地会有新的特性加入进来webpack
JavaScript 是弱类型而且没有命名空间,致使很难模块化,使得其在大型的协做项目中不是很方便ios
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 的好处,这里我就不一一列举了,感兴趣的小伙伴能够本身去查资料
我这边使用的是最新版本脚手架 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 配置
复制代码
显然这些是不可以知足正常业务的开发的,因此我这边作了一版基础建设方面的改造。改造完后项目结构以下
├── 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 配置
复制代码
接下来,我将介绍项目中部分模块的改造
这里使用了 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') }
]
})
复制代码
在 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.CancelToken
const removePending = (config) => {
for (let p in pending) {
let item: any = p
let list: any = pending[p]
// 当前请求在数组中存在时执行函数体
if (list.url === config.url + '&request_type=' + 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
中作统一管理
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' @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-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
举个🌰
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 地址,这里就不一一作说明了
}
复制代码
解析以后会变成
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) {}
}
}
复制代码
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')
@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 文件中的用法介绍的也差很少了。我也相信小伙伴看到这,对其大体的语法糖也有了必定的了解了
.d.ts
文件,请从新启动服务让你的服务可以识别你定义的模块,并重启 vscode 让编辑器也可以识别(真的恶心)tsconfig
,好比记得把 strictPropertyInitialization
设为 false,否则你定义一个变量就必须给它一个初始值。rootGetters
诸如此类的还有一堆,但更多的得大家本身去探寻。接下来,我将谈谈大型项目中团队协做的一些规范
一个大的项目,确定是多人一块儿并行,里面不只有前端团队的合做,还有与产品同窗的需求探(si)讨(bi),以及和后端同窗的联调,甚至于还须要本身或者依靠 SRE 进行一些服务的配置。
既然项目是基于 vue + ts 的且是多人协做,那么开发规范确定是必须的,这样可让并行开发变的容易起来。下面,我从当时我制定的规范中抽出一些给小伙伴们作个参考(仅作参考哈)
i. 页面开发摆放顺序
<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. 文件命名规范
单词小写,单词之间用 '-' 分隔,如图
名词在前,动词在后,如图
相同模块描述在前,不一样描述在后
千万记住如下三点:
要有礼貌的探(si)讨(bi)
要颇有礼貌的探(si)讨(bi)
要很是有礼貌的探(si)讨(bi)
具体细节我曾在知乎里面有过回答,这里不赘述了。传送门:先后端分离,后台返回的数据前端无法写,怎么办?
上一个点,谈了一下开发层面的的协做。这里,谈一谈人效提高。
你们都知道,一个项目是否可以在预约的期限中完成开发 + 联调 + 测试 + 上线,最重要的由于就是每一个人作事的效率。咱们不能保证你们效率都很高,但咱们得保障本身的开发效率。
需求一下来,首先咱们得保证的就是本身对需求的认知。通常对于老手来讲,把需求过一遍内心就大体清楚作完这个需求大概须要多少时间,而新手则永远对完成时间没有一个很好的认知。
那么,如何提高本身的开发效率呢?
文章到这也差很少了。聊了聊项目立项前的选型,也聊了聊项目初期的基础建设,还聊了聊 ts 在 .vue 中的使用,甚至项目开发中团队协做的一些事情也有聊。但毕竟文笔有限,不少点并不能娓娓道来,大多都是点到为止。若是以为文章对小伙伴们有帮助的话,请不要吝啬你手中的赞
若是小伙伴大家想了解更多的话,欢迎加入鄙人的交流群:731175396
我的准备从新捡回本身的公众号了,以后每周保证一篇高质量好文,感兴趣的小伙伴能够关注一波。
美团 基础研发平台/前端技术中心 上海侧招人啦 ~~~
前端开发 高级/资深
岗位福利: 15.5薪,15.5寸Mac,薪资25K-45K,股票期权。
工做职责:
职位要求:
加分项:
对以上职位感兴趣的同窗欢迎先加群:731175396,后联系我了解更多,或者直接投简历到我邮箱 xuqiang13@meituan.com