TypeScript + 大型專案實戰

qiangdada發表於2018-07-22

寫在前面

TypeScript 已經出來很久了,很多大公司很多大專案也都在使用它進行開發。上個月,我這邊也正式跟進一個對集團的大型運維類專案。

專案要做的事情大致分為以下幾個大模組

  • 一站式管理平臺
  • 規模化運維能力
  • 預案平臺
  • 巡檢平臺
  • 全鏈路壓測等

每一個模組要做的事情也很多,由於牽扯到公司業務,具體要做的一些事情這裡我就不一一列舉了,反正專案整體規模還是很大的。

一、關於選型

在做了一些技術調研後,再結合專案之後的開發量級以及維護成本。最終我和同事在技術選型上得出一致結論,最終選型定為 Vue 最新全家桶 + TypeScript。

那麼問題來了,為什麼大型專案非得用 TypeScript 呢,ES6、7 不行麼?

TypeScript + 大型專案實戰

其實也沒說不行,只不過我個人更傾向在一些協作開發的大型專案中使用 TypeScript 。下面我列一些我做完調研後自己的一些看法

  1. 首先,TypeScript 具有型別系統,且是 JavaScript 的超集。 JavaScript 能做的,它能做。JavaScript 不能做的,它也能做。

  2. 其次,TypeScript 已經比較成熟了,市面上相關資料也比較多,大部分的庫和框架也讀對 TypeScript 做了很好的支援。

  3. 然後,保證優秀的前提下,它還在積極的開發完善之中,不斷地會有新的特性加入進來

  4. JavaScript 是弱型別並且沒有名稱空間,導致很難模組化,使得其在大型的協作專案中不是很方便

  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 的好處,這裡我就不一一列舉了,感興趣的小夥伴可以自己去查資料

TypeScript + 大型專案實戰

二、基礎建設

1、初始化結構

我這邊使用的是最新版本腳手架 vue-cli 3 進行專案初始化的,初始化選項如下

TypeScript + 大型專案實戰

生成的目錄結構如下

├── 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.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 檔案中的一些寫法

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

舉個?

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) {}
  }
}
複製程式碼

2、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 檔案中的用法介紹的也差不多了。我也相信小夥伴看到這,對其大致的語法糖也有了一定的瞭解了

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. 檔案命名規範

單詞小寫,單詞之間用 '-' 分隔,如圖

TypeScript + 大型專案實戰

名詞在前,動詞在後,如圖

TypeScript + 大型專案實戰

相同模組描述在前,不同描述在後

TypeScript + 大型專案實戰

2、與產品 + 後端等協作

千萬記住以下三點:

  1. 要有禮貌的探(si)討(bi)

  2. 要很有禮貌的探(si)討(bi)

  3. 要非常有禮貌的探(si)討(bi)

TypeScript + 大型專案實戰

具體細節我曾在知乎裡面有過回答,這裡不贅述了。傳送門:前後端分離,後臺返回的資料前端沒法寫,怎麼辦?

3、人效提升

上一個點,談了一下開發層面的的協作。這裡,談一談人效提升。

大家都知道,一個專案是否能夠在預定的期限中完成開發 + 聯調 + 測試 + 上線,最重要的因為就是每個人做事的效率。我們不能保證大家效率都很高,但我們得保障自己的開發效率。

需求一下來,首先我們得保證的就是自己對需求的認知。一般對於老手來說,把需求過一遍心裡就大致清楚做完這個需求大概需要多少時間,而新手則永遠對完成時間沒有一個很好的認知。

那麼,如何提升自己的開發效率呢?

  • 把需求拆分成模組
  • 把模組中的東西再次拆分成小細節
  • 評估小細節自身的開發時間
  • 評估小細節中某些可能存在的風險點的開發時間
  • 評估聯調時間
  • 預留測試 + 修復 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、H5、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

TypeScript + 大型專案實戰

相關文章