寫在前面
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.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
中做統一管理
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({})
裡面寫 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'
@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 是一個基於 Vue、Vuex、vue-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. 檔案命名規範
單詞小寫,單詞之間用 '-' 分隔,如圖
名詞在前,動詞在後,如圖
相同模組描述在前,不同描述在後
2、與產品 + 後端等協作
千萬記住以下三點:
-
要有禮貌的探(si)討(bi)
-
要很有禮貌的探(si)討(bi)
-
要非常有禮貌的探(si)討(bi)
具體細節我曾在知乎裡面有過回答,這裡不贅述了。傳送門:前後端分離,後臺返回的資料前端沒法寫,怎麼辦?
3、人效提升
上一個點,談了一下開發層面的的協作。這裡,談一談人效提升。
大家都知道,一個專案是否能夠在預定的期限中完成開發 + 聯調 + 測試 + 上線,最重要的因為就是每個人做事的效率。我們不能保證大家效率都很高,但我們得保障自己的開發效率。
需求一下來,首先我們得保證的就是自己對需求的認知。一般對於老手來說,把需求過一遍心裡就大致清楚做完這個需求大概需要多少時間,而新手則永遠對完成時間沒有一個很好的認知。
那麼,如何提升自己的開發效率呢?
- 把需求拆分成模組
- 把模組中的東西再次拆分成小細節
- 評估小細節自身的開發時間
- 評估小細節中某些可能存在的風險點的開發時間
- 評估聯調時間
- 預留測試 + 修復 BUG 的時間節點
- 預留 deadline (一般來說是 1 *(1 + 0.2))
- 安排好自己的開發節點,以 1D(一天)作為單位
- 記錄好風險原因、風險點以及對應的規避方案
- 如若預感要延期,需及時給出補救方案(比如:加班)
- 記錄 BUG 數量,以及對應的 BUG 人員(真的不是為了甩鍋)
總結
文章到這也差不多了。聊了聊專案立項前的選型,也聊了聊專案初期的基礎建設,還聊了聊 ts 在 .vue 中的使用,甚至專案開發中團隊協作的一些事情也有聊。但畢竟文筆有限,很多點並不能娓娓道來,大多都是點到為止。如果覺得文章對小夥伴們有幫助的話,請不要吝嗇你手中的贊
如果小夥伴你們想了解更多的話,歡迎加入鄙人的交流群:731175396
最後的最後
美團 基礎研發平臺/前端技術中心 上海側招人啦 ~~~
前端開發 高階/資深
崗位福利: 15.5薪,15.5寸Mac,薪資25K-45K,股票期權。
工作職責:
- 負責web前端架構設計及程式碼的實現
- 分析和發現系統中的可優化點,提高可靠性和效能
- 常用的 Javascript 模組封裝和效能優化,更新和維護公司前端開發元件庫
- 研究業界最新技術及其應用,解決創新研發過程中的關鍵問題和技術難點
職位要求:
- 精通 Javascript、H5、Sass/Less 和 HTML 前端模板引擎
- 熟悉 ECMAScript,CommonJS,Promise,TypeScript 等標準,熟練使用Git
- 精通物件導向的 JavaScript 開發,參與或設計過 JS 框架或公共元件開發經驗
- 熟練使用 Vue.js 或 React.js 框架,並研究過其原始碼實現,熟悉資料驅動原理
- 對 Javascript 引擎實現機制、瀏覽器渲染效能有比較深入的研究
- 熟悉 Node.js,瞭解 PHP/java/python 等後端語言之一
- 熟悉 gulp,webpack 等前端構建工具,會搭建專案腳手架提升開發效率
- 具有較好的問題解決能力、理解能力及學習能力,較好的協作能力和團隊精神
- 良好的自我驅動力,不拘泥於手頭工作,勇於探索新技術並加以應用
加分項:
- 熟悉Node.js語言
- 有開源作品或技術部落格
- 技術社群活躍分子
- Github上有獨立作品
- Geek控,對技術有狂熱興趣和追求
- 一線網際網路公司經驗
對以上職位感興趣的同學歡迎先加群:731175396,後聯絡我瞭解更多,或者直接投簡歷到我郵箱 xuqiang13@meituan.com