TypeScript + 大型项目实战

写在前面

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

项目要作的事情大体分为如下几个大模块css

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

每个模块要作的事情也不少,因为牵扯到公司业务,具体要作的一些事情这里我就不一一列举了,反正项目总体规模仍是很大的。html

1、关于选型

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

那么问题来了,为何大型项目非得用 TypeScript 呢,ES六、7 不行么?vue

其实也没说不行,只不过我我的更倾向在一些协做开发的大型项目中使用 TypeScript 。下面我列一些我作完调研后本身的一些见解java

  1. 首先,TypeScript 具备类型系统,且是 JavaScript 的超集。 JavaScript 能作的,它能作。JavaScript 不能作的,它也能作。node

  2. 其次,TypeScript 已经比较成熟了,市面上相关资料也比较多,大部分的库和框架也读对 TypeScript 作了很好的支持。python

  3. 而后,保证优秀的前提下,它还在积极的开发完善之中,不断地会有新的特性加入进来webpack

  4. JavaScript 是弱类型而且没有命名空间,致使很难模块化,使得其在大型的协做项目中不是很方便ios

  5. vscode、ws 等编辑器对 TypeScript 支持很友好

  6. 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: []
    }
    复制代码

    配合好编辑器,若是不按照定义好的类型来的话,编辑器自己就会给你报错,而不会等到编译才来报错

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

    好比,你在一个 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)
    })
    复制代码
  8. 可使用 泛型 来建立可重用的组件。好比你想建立一个参数类型和返回值类型是同样的通用方法

    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 的好处,这里我就不一一列举了,感兴趣的小伙伴能够本身去查资料

2、基础建设

一、初始化结构

我这边使用的是最新版本脚手架 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 配置
复制代码

三、模块改造

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

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.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 放到 vuexactions 中作统一管理

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' @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 文件中的一些写法

3、vue 中 ts 的用法

一、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

举个🌰

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

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

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

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,否则你定义一个变量就必须给它一个初始值。
  • 千万管理好你的路由层级,否则到时连正则都拯救不了你
  • 业务层面千万作好类型检测或者枚举定义,这样不只便利了开发,还能在出了问题的时候迅速定位
  • 跨模块使用 vuex,请直接使用 rootGetters
  • 若是你须要改造某组件库主题,请单开一个文件进行集中管理,别一个组件分一个文件去改动,否则编译起来速度堪忧
  • 可以复用团队其余人开发好的东西,尽可能别去开发第二遍,否则到时浪费的可能就不是单纯的开发时间,还有 code review 的时间

诸如此类的还有一堆,但更多的得大家本身去探寻。接下来,我将谈谈大型项目中团队协做的一些规范

4、如何进行团队协做

一个大的项目,确定是多人一块儿并行,里面不只有前端团队的合做,还有与产品同窗的需求探(si)讨(bi),以及和后端同窗的联调,甚至于还须要本身或者依靠 SRE 进行一些服务的配置。

一、前端开发规范

既然项目是基于 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. 文件命名规范

单词小写,单词之间用 '-' 分隔,如图

名词在前,动词在后,如图

相同模块描述在前,不一样描述在后

二、与产品 + 后端等协做

千万记住如下三点:

  1. 要有礼貌的探(si)讨(bi)

  2. 要颇有礼貌的探(si)讨(bi)

  3. 要很是有礼貌的探(si)讨(bi)

具体细节我曾在知乎里面有过回答,这里不赘述了。传送门:先后端分离,后台返回的数据前端无法写,怎么办?

三、人效提高

上一个点,谈了一下开发层面的的协做。这里,谈一谈人效提高。

你们都知道,一个项目是否可以在预约的期限中完成开发 + 联调 + 测试 + 上线,最重要的由于就是每一个人作事的效率。咱们不能保证你们效率都很高,但咱们得保障本身的开发效率。

需求一下来,首先咱们得保证的就是本身对需求的认知。通常对于老手来讲,把需求过一遍内心就大体清楚作完这个需求大概须要多少时间,而新手则永远对完成时间没有一个很好的认知。

那么,如何提高本身的开发效率呢?

  • 把需求拆分红模块
  • 把模块中的东西再次拆分红小细节
  • 评估小细节自身的开发时间
  • 评估小细节中某些可能存在的风险点的开发时间
  • 评估联调时间
  • 预留测试 + 修复 BUG 的时间节点
  • 预留 deadline (通常来讲是 1 *(1 + 0.2))
  • 安排好本身的开发节点,以 1D(一天)做为单位
  • 记录好风险缘由、风险点以及对应的规避方案
  • 如若预感要延期,需及时给出补救方案(好比:加班)
  • 记录 BUG 数量,以及对应的 BUG 人员(真的不是为了甩锅)

总结

文章到这也差很少了。聊了聊项目立项前的选型,也聊了聊项目初期的基础建设,还聊了聊 ts 在 .vue 中的使用,甚至项目开发中团队协做的一些事情也有聊。但毕竟文笔有限,不少点并不能娓娓道来,大多都是点到为止。若是以为文章对小伙伴们有帮助的话,请不要吝啬你手中的赞

若是小伙伴大家想了解更多的话,欢迎加入鄙人的交流群:731175396

我的准备从新捡回本身的公众号了,以后每周保证一篇高质量好文,感兴趣的小伙伴能够关注一波。

最后的最后

美团 基础研发平台/前端技术中心 上海侧招人啦 ~~~

前端开发 高级/资深

岗位福利: 15.5薪,15.5寸Mac,薪资25K-45K,股票期权。

工做职责:

  1. 负责web前端架构设计及代码的实现
  2. 分析和发现系统中的可优化点,提升可靠性和性能
  3. 经常使用的 Javascript 模块封装和性能优化,更新和维护公司前端开发组件库
  4. 研究业界最新技术及其应用,解决创新研发过程当中的关键问题和技术难点

职位要求:

  1. 精通 Javascript、H五、Sass/Less 和 HTML 前端模板引擎
  2. 熟悉 ECMAScript,CommonJS,Promise,TypeScript 等标准,熟练使用Git
  3. 精通面向对象的 JavaScript 开发,参与或设计过 JS 框架或公共组件开发经验
  4. 熟练使用 Vue.js 或 React.js 框架,并研究过其源码实现,熟悉数据驱动原理
  5. 对 Javascript 引擎实现机制、浏览器渲染性能有比较深刻的研究
  6. 熟悉 Node.js,了解 PHP/java/python 等后端语言之一
  7. 熟悉 gulp,webpack 等前端构建工具,会搭建项目脚手架提高开发效率
  8. 具备较好的问题解决能力、理解能力及学习能力,较好的协做能力和团队精神
  9. 良好的自我驱动力,不拘泥于手头工做,敢于探索新技术并加以应用

加分项:

  1. 熟悉Node.js语言
  2. 有开源做品或技术博客
  3. 技术社区活跃分子
  4. Github上有独立做品
  5. Geek控,对技术有狂热兴趣和追求
  6. 一线互联网公司经验

对以上职位感兴趣的同窗欢迎先加群:731175396,后联系我了解更多,或者直接投简历到我邮箱 xuqiang13@meituan.com

相关文章
相关标签/搜索