記一次在老掉牙的Vue2項目中引入TypeScript和組合式Api和vueuse來改善大家伙的開發(fā)體驗的艱辛歷程

封面

原由

現(xiàn)有的一個項目2年前創(chuàng)建的介粘,隨著時間流逝淑际,代碼量已經(jīng)暴增到了將近上萬個文件,但是工程化已經(jīng)慢慢到了不可維護(hù)的狀態(tài)炊昆,想給他來一次大換血敏晤,但是侵入式代碼配置太多了……贱田,最終以一種妥協(xié)的方式引入了TypeScript、組合式Api嘴脾、vueuse男摧,提升了項目的工程化規(guī)范程度蔬墩,整個過程讓我頗有感概,記錄一下耗拓。

先配置TypeScript相關(guān)的

一些庫的安裝和配置

  1. 由于webpack的版本還是3.6拇颅,嘗試數(shù)次升級到4、5都因為大量的配置侵入性代碼的大量修改工作放棄了乔询,所以就直接找了下面這些庫
npm i -D ts-loader@3.5.0 tslint@6.1.3 tslint-loader@3.6.0 fork-ts-checker-webpack-plugin@3.1.1
  1. 接下來就是改webpack的配置了樟插,修改main.js文件為main.ts,并在文件的第一行添加// @ts-nocheckTS忽略檢查此文件,在webpack.base.config.js的入口中相應(yīng)的改為main.ts
  2. webpack.base.config.jsresolve中的extensions中增加.ts.tsx,alias規(guī)則中增加一條'vue$': 'vue/dist/vue.esm.js'
  3. webpack.base.config.js中增加plugins選項添加fork-ts-checker-webpack-plugin竿刁,將ts check的任務(wù)放到單獨的進(jìn)程中進(jìn)行黄锤,減少開發(fā)服務(wù)器啟動時間
  4. webpack.base.config.js文件的rules中增加兩條配置和fork-ts-checker-webpack-plugin的插件配置
{
  test: /\.ts$/,
  exclude: /node_modules/,
  enforce: 'pre',
  loader: 'tslint-loader'
},
{
  test: /\.tsx?$/,
  loader: 'ts-loader',
  exclude: /node_modules/,
  options: {
    appendTsSuffixTo: [/\.vue$/],
    transpileOnly: true // disable type checker - we will use it in fork plugin
  }
},,
// ...
plugins: [new ForkTsCheckerWebpackPlugin()], // 在獨立進(jìn)程中處理ts-checker,縮短webpack服務(wù)冷啟動食拜、熱更新時間 https://github.com/TypeStrong/ts-loader#faster-builds
  1. 根目錄中增加tsconfig.json文件補充相應(yīng)配置鸵熟,src目錄下新增vue-shim.d.ts聲明文件

tsconfig.json

{
  "exclude": ["node_modules", "static", "dist"],
  "compilerOptions": {
    "strict": true,
    "module": "esnext",
    "outDir": "dist",
    "target": "es5",
    "allowJs": true,
    "jsx": "preserve",
    "resolveJsonModule": true,
    "downlevelIteration": true,
    "importHelpers": true,
    "noImplicitAny": true,
    "allowSyntheticDefaultImports": true,
    "moduleResolution": "node",
    "isolatedModules": false,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "lib": ["dom", "es5", "es6", "es7", "dom.iterable", "es2015.promise"],
    "sourceMap": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
    },
    "pretty": true
  },
  "include": ["./src/**/*", "typings/**/*.d.ts"]
}

vue-shim.d.ts

declare module '*.vue' {
  import Vue from 'vue'
  export default Vue
}

路由配置的改善

原有路由配置是通過配置pathnamecomponent负甸,這樣在開發(fā)和維護(hù)的過程中有一些缺點:

  1. 使用的時候可能出現(xiàn)使用path或者使用name不規(guī)范不統(tǒng)一的情況
  2. 開發(fā)人員在維護(hù)老代碼的時候查找路由對應(yīng)的單文件不方便
  3. 要手動避免路由的namepath不與其他路由有沖突

將所有的路由的路徑按照業(yè)務(wù)抽離到不同的枚舉中流强。在枚舉中定義可以防止路由 path 沖突,也可以將枚舉的 key 定義的更加語義化惑惶,又可以借助Typescript的類型推導(dǎo)能力快速補全煮盼,在查找路由對應(yīng)單文件的時候可以一步到位

為什么不用name,因為name只是一個標(biāo)識這個路由的語義带污,當(dāng)我們使用枚舉類型的path之后僵控,枚舉的Key就足以充當(dāng)語義化的路徑path這個name屬性就沒有存在的必要了,我們在聲明路由的時候就不需要聲明name屬性鱼冀,只需要pathcomponent字段就可以了

demo

export enum ROUTER {
  Home = '/xxx/home',
  About = '/xxx/about',
}

export default [
  {
    path: ROUTER.Home,
    component: () => import( /* webpackChunkName:'Home' */ 'views/Home')
  },
  {
    path: ROUTER.About,
    component: () => import( /* webpackChunkName:'About' */ 'views/About')
  }
]

常量和枚舉

之前在我們項目中也是通過把所有的常量抽離到services/const中進(jìn)行管理报破,現(xiàn)在集成了Typescript之后,我們就可以在之后項目在services/constant中進(jìn)行管理常量千绪,在services/enums中管理枚舉充易。

比如常見的接口返回的code就可以聲明為枚舉,就不用在使用的時候還需要手寫if (res.code === 200)類似的判斷了荸型,可以直接通過聲明好的RES_CODE枚舉直接獲取到所有的接口返回code類型

// services/enums/index.ts
/** RES_CODE Enum */
export enum RES_CODE {
  SUCCESS = 200
  // xxx
}

比如storagekey我們就可以聲明在services/constant/storage.ts

/** userInfo-storageKey */
export const USERINFO_STORE_KEY = 'userInfo'

/** 與用戶相關(guān)的key可以通過構(gòu)造一個帶業(yè)務(wù)屬性參數(shù)的純函數(shù)來聲明 */
export const UserSpecialInfo = (userId: string) => {
  return `specialInfo-${userId}`
}

類型聲明文件規(guī)范

全局類型聲明文件統(tǒng)一在根目錄的typings文件夾中維護(hù)(可復(fù)用的數(shù)據(jù)類型)

比較偏業(yè)務(wù)中組裝數(shù)據(jù)過程中的類型直接在所在組件中維護(hù)即可(不易復(fù)用的數(shù)據(jù)結(jié)構(gòu))

接口中的類型封裝

請求基類封裝邏輯

在 utils 文件夾下新增requestWrapper.ts文件盹靴,之后所有的請求基類方法封裝可以在此文件中進(jìn)行維護(hù)

// src/utils/requestWrapper.ts
import { AxiosResponse } from 'axios'
import request from '@/utils/request'

// 請求參數(shù)在之后具體封裝的時候才具體到某種類型,在此使用unknown聲明瑞妇,返回值為泛型S稿静,在使用的時候填充具體類型
export function PostWrapper<S>(
  url: string,
  data: unknown,
  timeout?: number
) {
  return (request({
    url,
    method: 'post',
    data,
    timeout
  }) as AxiosResponse['data']) as BASE.BaseResWrapper<S> // BASE是在typings中定義的一個命名空間 后面會有代碼說明
}

在具體的業(yè)務(wù)層進(jìn)行封裝后的使用

api/user中新建一個index.ts文件,對比之前的可以做到足夠簡潔辕狰,也可以提供類型提示改备,知曉這個請求是什么請求以及參數(shù)的參數(shù)以及返回值

import { PostWrapper } from '@/utils/requestWrapper'

// 此處只需要在注釋中標(biāo)注這個接口是什么接口,不需要我們通過注釋來標(biāo)識需要什么類型的參數(shù)蔓倍,TS會幫我們完成, 只需要我們填充請求參數(shù)的類型和返回參數(shù)的類型即可約束請求方法的使用
/** 獲取用戶信息 */
export function getUserInfo(query: User.UserInfoReqType) {
  return PostWrapper<User.UserInfoResType>(
    '/api/userinfo',
    query
  )
}
  • 需要提供類型支持的接口悬钳,需要聲明在api/**/*.ts文件中盐捷,并通過給對應(yīng)的function標(biāo)注參數(shù)請求類型和響應(yīng)類型
  • 如果結(jié)構(gòu)極為簡潔,可以不需要在typings/request/*.d.ts中維護(hù)默勾,直接在封裝接口處聲明類型即可碉渡,如果參數(shù)稍多,都應(yīng)在typings/request/*.d.ts中維護(hù)灾测,避免混亂

現(xiàn)在業(yè)務(wù)中的服務(wù)端的接口返回的基本都是通過一層描述性對象包裹起來的爆价,業(yè)務(wù)數(shù)據(jù)都在對象的request字段中,基于此我們封裝接口就在typings/request/index.d.ts中聲明請求返回的基類結(jié)構(gòu)媳搪,在具體的xxx.d.ts中完善具體的請求類型聲明铭段,例如user.d.ts中的一個報錯的接口,在此文件中聲明全局的命名空間User來管理所有此類作業(yè)接口的請求和響應(yīng)的數(shù)據(jù)類型

typings/request/index.d.ts

import { RES_CODE } from '@/services/enums'

declare global {
  // * 所有的基類在此聲明類型
  namespace BASE {
    // 請求返回的包裹層類型聲明提供給具體數(shù)據(jù)層進(jìn)行包裝
    type BaseRes<T> = {
      code: RES_CODE
      result?: T
      info?: string
      time: number
      traceId: string
    }
    type BaseResWrapper<T> = Promise<BASE.BaseRes<T>>
    // 分頁接口
    type BasePagination<T> = {
      content: T
      now: string
      page: number
      size: number
      totalElements: number
      totalPages: number
    }
  }

typings/request/user.d.ts

declare namespace User {

/** 響應(yīng)參數(shù) */
type UserInfoResType = {
  id: number | string
  name: string
  // ...
}

/** 請求參數(shù) */
type UserInfoReqType = {
  id: number | string
  // ...
}

到此TypeScript相關(guān)的就結(jié)束了秦爆,接下來是組合式Api的

Vue2中使用組合式Api

  1. 安裝@vue/componsition-api
npm i @vue/componsition-api
  1. main.tsuse即可在.vue文件中使用組合式 API
import VueCompositionAPI from '@vue/composition-api'
// ...
Vue.use(VueCompositionAPI)

Vue2 中使用組合式 Api 中的一些注意事項

  1. 組合式 Api文檔序愚,不了解的小伙伴可以先參照文檔學(xué)習(xí)一下,在比較復(fù)雜的頁面等限,組件多的情況下組合式 API 相比傳統(tǒng)的Options API更靈活爸吮,可以把邏輯抽離出去封裝為單獨的use函數(shù),使組件代碼結(jié)構(gòu)更為清晰望门,也更方便復(fù)用業(yè)務(wù)邏輯形娇。
  2. 所有的組合式 Api 中的api都需要從@vue/composition-api中引入,然后使用export default defineComponent({ })替換原有的export default { }的寫法筹误,即可啟用組合式 Api 語法和Typescript的類型推導(dǎo)(script需要添加對應(yīng)的lang="ts"attribute)
  3. template中的寫法和Vue2中一致桐早,無需注意Vue3中的v-model和類似.native的事件修飾符在Vue3中取消等其他的break change
  4. 子組件中調(diào)用父組件中的方法使用setup(props, ctx)中的ctx.emit(eventName, params)即可,給Vue實例對象上掛載的屬性和方法都可以通過ctx.root.xxx來獲取厨剪,包括$route哄酝、$router等,為了使用方便推薦在setup中第一行就通過結(jié)構(gòu)來聲明ctx.root上的屬性,祷膳,如果之前在Vue實例對象上添加的有業(yè)務(wù)屬性相關(guān)的屬性或方法可以通過擴(kuò)展模塊vue/types/vue上的Vue接口來添加業(yè)務(wù)屬性相關(guān)的類型:

typings/common/index.d.ts

// 1. Make sure to import 'vue' before declaring augmented types
import Vue from 'vue'
// 2. Specify a file with the types you want to augment
//    Vue has the constructor type in types/vue.d.ts
declare module 'vue/types/vue' {
  // 3. Declare augmentation for Vue
  interface Vue {
    /** 當(dāng)前環(huán)境是否是IE */
    isIE: boolean
    // ... 各位根據(jù)自己的業(yè)務(wù)情況自行添加
  }
}
  1. 所有template中使用到的變量陶衅、方法、對象都需要在setupreturn直晨,其他的在頁面邏輯內(nèi)部使用的不需要return
  2. 推薦根據(jù)頁面展示元素和用戶與頁面的交互行為定義setup中的方法搀军,比較復(fù)雜的邏輯細(xì)節(jié)和對數(shù)據(jù)的處理盡量抽離到外部,保持.vue文件中的代碼邏輯清晰
  3. 在需求開發(fā)前勇皇,根據(jù)服務(wù)端接口數(shù)據(jù)的定義奕巍,來制定頁面組件中的數(shù)據(jù)和方法的接口,可以提前聲明類型儒士,之后在開發(fā)過程中實現(xiàn)具體的方法
  4. 在當(dāng)下的Vue2.6版本中通過@vue/composition-api使用組合式 Api 不能使用setup語法糖,待之后的Vue2.7版本release之后再觀察檩坚,其他的一些 注意事項和限制

基于 reactive 的 store 的風(fēng)格規(guī)范

鑒于在Vuex中接入TS的不便和Vuex使用場景的必要性着撩,在組合式 Api 中提供了一個最佳實踐:將需要響應(yīng)的數(shù)據(jù)聲明在一個ts文件中通過reactive包裹初始化對象诅福,暴漏出一個更新的方法,即可達(dá)到原有在Vuex中更新storestate的效果拖叙,使用computed可以達(dá)到getter的效果氓润,哪些組件需要對數(shù)據(jù)進(jìn)行獲取和修改只需要引入即可,更改直接就可以達(dá)到響應(yīng)效果薯鳍!

提供一份Demo咖气,各位對于這部分內(nèi)容的封裝可以見仁見智

// xxxHelper.ts
import { del, reactive, readonly, computed, set } from '@vue/composition-api'

// 定義store中數(shù)據(jù)的類型,對數(shù)據(jù)結(jié)構(gòu)進(jìn)行約束
interface CompositionApiTestStore {
  c: number
  [propName: string]: any
}

// 初始值
const initState: CompositionApiTestStore = { c: 0 }

const state = reactive(initState)

/** 暴露出的store為只讀挖滤,只能通過下面的updateStore進(jìn)行更改 */
export const store = readonly(state)

/** 可以達(dá)到原有Vuex中的getter方法的效果 */
export const upperC = computed(() => {
  return store.c.toUpperCase()
})

/** 暴漏出更改state的方法崩溪,參數(shù)是state對象的子集或者無參數(shù),如果是無參數(shù)就便利當(dāng)前對象斩松,將子對象全部刪除, 否則俺需更新或者刪除 */
export function updateStore(
  params: Partial<CompositionApiTestStore> | undefined
) {
  console.log('updateStore', params)
  if (params === undefined) {
    for (const [k, v] of Object.entries(state)) {
      del(state, `${k}`)
    }
  } else {
    for (const [k, v] of Object.entries(params)) {
      if (v === undefined) {
        del(state, `${k}`)
      } else {
        set(state, `${k}`, v)
      }
    }
  }
}

vueuse

vueuse是一個很好用的庫伶唯,具體的安裝和使用非常簡單,但是功能很多很強大惧盹,這部分我就不展開細(xì)說了乳幸,大家去看官方文檔吧!

總結(jié)

這次的項目升級實在是迫不得已钧椰,沒辦法的辦法粹断,項目已經(jīng)龐大無比還要兼容IE,用的腳手架及相關(guān)庫也都很久沒有更新版本嫡霞,在項目創(chuàng)建開始就已經(jīng)欠下了很多的技術(shù)債了瓶埋,導(dǎo)致后面開發(fā)維護(hù)人員叫苦不迭(其實就是我,項目是別個搞的秒际,逃…)悬赏,各位老大哥在新起項目的時候一定要斟酌腳手架和技術(shù)棧啊,不要前人挖坑后人填了……

如果你也在維護(hù)這樣的項目娄徊,并且也受夠了這種糟糕的開發(fā)體驗闽颇,可以參照我的經(jīng)驗來改造下你的項目,如果看過感覺對你有幫助寄锐,也請給個一鍵三連~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末兵多,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子橄仆,更是在濱河造成了極大的恐慌剩膘,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件盆顾,死亡現(xiàn)場離奇詭異怠褐,居然都是意外死亡,警方通過查閱死者的電腦和手機您宪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進(jìn)店門奈懒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來奠涌,“玉大人,你說我怎么就攤上這事磷杏×锍” “怎么了?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵极祸,是天一觀的道長慈格。 經(jīng)常有香客問我,道長遥金,這世上最難降的妖魔是什么浴捆? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮汰规,結(jié)果婚禮上汤功,老公的妹妹穿的比我還像新娘。我一直安慰自己溜哮,他們只是感情好滔金,可當(dāng)我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著茂嗓,像睡著了一般餐茵。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上述吸,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天忿族,我揣著相機與錄音,去河邊找鬼蝌矛。 笑死道批,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的入撒。 我是一名探鬼主播隆豹,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼茅逮!你這毒婦竟也來了璃赡?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤献雅,失蹤者是張志新(化名)和其女友劉穎碉考,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體挺身,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡侯谁,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片良蒸。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡技扼,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出嫩痰,到底是詐尸還是另有隱情,我是刑警寧澤窍箍,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布串纺,位于F島的核電站,受9級特大地震影響椰棘,放射性物質(zhì)發(fā)生泄漏纺棺。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一邪狞、第九天 我趴在偏房一處隱蔽的房頂上張望祷蝌。 院中可真熱鬧,春花似錦帆卓、人聲如沸巨朦。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽糊啡。三九已至,卻和暖如春吁津,著一層夾襖步出監(jiān)牢的瞬間棚蓄,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工碍脏, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留梭依,地道東北人。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓典尾,卻偏偏與公主長得像役拴,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子急黎,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容