合格前端第十二彈-TypeScript + 大型項目實戰

寫在前面

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

項目要作的事情大體分爲如下幾個大模塊css

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

每個模塊要作的事情也不少,因爲牽扯到公司業務,具體要作的一些事情這裏我就不一一列舉了,反正項目總體規模仍是很大的。前端

1、關於選型

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

那麼問題來了,爲何大型項目非得用 TypeScript 呢,ES六、7 不行麼?java

其實也沒說不行,只不過我我的更傾向在一些協做開發的大型項目中使用 TypeScript 。下面我列一些我作完調研後本身的一些見解node

  1. 首先,TypeScript 具備類型系統,且是 JavaScript 的超集。 JavaScript 能作的,它能作。JavaScript 不能作的,它也能作。python

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

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

  4. JavaScript 是弱類型而且沒有命名空間,致使很難模塊化,使得其在大型的協做項目中不是很方便git

  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 文件中使用上的不便的問題。

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,後聯繫我

相關文章
相關標籤/搜索