如何管理前端項目中的複雜依賴關係

隨着前端工程規模的增長,各類第三方與自有依賴包的關係也日趨複雜。這時候可能產生什麼問題,又該如何解決呢?這裏分享咱們前端團隊的一些實踐。前端

何謂複雜依賴關係

安裝依賴包,對於前端開發者來講不過就是一句 npm install xxx 的事。那麼,單純靠這種方式給一個項目安裝了不少依賴,就算是複雜的依賴關係嗎?這裏咱們這樣定義「複雜」:node

  • 你須要本身維護多個不一樣的包,來在最下游的業務項目中使用。
  • 除了被下游業務依賴外,這些包之間也可能存在依賴關係,它們也可能依賴上游的包。
  • 不一樣的包可能位於不一樣的 Git 倉庫,還有各自獨立的測試、構建與發佈流程。

若是純粹只靠 npm install,那麼全部的包都必須發佈到 NPM 以後才能被其餘的包更新。在「聯調」這些包的時候,每次稍有更改都走一遍正式的發佈流程,無疑是很是繁瑣而影響效率的。咱們有什麼現成的工具來解決這個問題呢?git

社區工具 Takeaway

提到管理多個包之間的依賴關係,不少同窗應該能立刻想到很多現成的工具,好比:github

  • NPM 的 link 命令
  • Yarn 的 workspace 命令
  • Lerna 工具

這裏的「萬惡之源」就是 npm link 命令了。雖然熟悉它的同窗多半知道它有很多問題,但它確實能解決基本的連接問題。快速複習一下使用方式:假設你維護的下游業務項目叫作 app,上游的依賴叫作 dep,那麼要想作到「dep 一改動,app 就能同步更新」,只須要這樣:npm

# 1. 在 dep 所在路徑執行
npm link

# 2. 在 app 所在路徑執行
npm link dep
複製代碼

這樣就造成了 app 與 dep 之間基本的「連接」關係。只要進入 app 的 node_modules 查看一下,不難發現 NPM 其實就是替你創建了一個操做系統的「快捷方式」(軟連接)跳到 dep 下而已。在存在多個互相依賴的包的時候,手動維護這個連接關係很是麻煩並且容易出錯,這時候你能夠用社區的 yarn workspace 或 Lerna 來自動幫你管理這些包。因爲這兩者至關接近,在此咱們只介紹在咱們生產環境下使用的 Lerna 工具。json

Lerna 的使用也是很是傻瓜的,你只需按下面的風格把各個依賴包放在同一個目錄下就行,無需對它們具體的構建配置作任何改動:bootstrap

my-lerna-repo/
  package.json
  packages/
    dep-1/
      package.json
    dep-2/
      package.json
    dep-3/
      package.json
    ...
複製代碼

而後一句 lerna bootstrap 就可以自動處理好它們之間的依賴關係了——這裏每一個包的 package.json 均可以放心地寫上其它包的名字了(注意這裏依據的是 package.json 中的 name 字段,而非目錄名)。這樣,你能夠放心地把這些包放置在同一個 Git 倉庫裏管理,而不用擔憂繁瑣的初始化過程了——如今的 Babel 和 React 就是這麼幹的。bash

固然了,實際的場景並非有了現成的命令或者工具就萬事大吉了。下面總結一些實踐中的依賴管理經驗吧:app

循環依賴的產生與解除

在剛開始使用 Lerna 這樣的依賴管理工具時,一些同窗可能會傾向於把依賴拆分得很是零散。這時是有可能出現循環依賴的情形的——A 包依賴了 B,而 B 包又依賴了 A。怎麼會出現這種狀況呢?舉一個例子:編輯器

  1. 假設你在維護一個可複用的編輯器 editor 包。爲了更好的 UI 組件化,你把它的 UI 部分拆分紅了 editor-ui 包。
  2. editor-ui 的組件須要 editor 實例,所以你把 editor 列爲了 editor-ui 的依賴。
  3. editor 的 Demo 頁面中想要展現帶完整 UI 的應用,所以你把 editor-ui 列爲了 editor 的依賴。

這時候就出現了循環依賴。雖然 NPM 支持這種場景下的依賴安裝,可是它的出現會讓依賴關係變得難以理解,所以咱們但願儘可能作到直接避免它。這裏的好消息是,循環依賴多數都和不太符合直覺的需求有關,在上面的例子裏,做爲上游的 editor 包去依賴了下游的 editor-ui 包,這能夠在方案評審時就明確指出,並只需改成在 editor-ui 包中展現 Demo 頁便可——若是出現了循環依賴,大膽地運用「這個需求不合理」的否決權吧。

多依賴包的初始化和同步

咱們已經提到,lerna boostrap 可以正確地完成多個包的依賴安裝和連接操做。但這是否意味着一個裝載了多個包的 Lerna 倉庫,只要這條命令就可以讓這些包都正常地跑起來呢?這裏存在一點細節須要注意。

若是你管理的多個包先是配置了各自的構建和發佈命令,而後才經過 Lerna 合併到一塊兒的話,可能出現這樣的問題:它們在 package.main 字段下指定的入口都是形如 dist/index.js 下的構建後文件,但相應的產物代碼在如今通常是不提交到 Git 的。這時候拉下全新的代碼想要跑起來時,即使工具正確地處理了連接關係,仍然有可能出現某個子包沒法打包成功的狀況——這時,就去被依賴的包目錄下手動 npm run build 一次了。固然,在這種狀況下,更新了一個包的源碼後,也須要對這個包作一次 build 操做生成產物後,其它的包才能同步。雖然這並無多少理解上的困難,但每每形成一些沒必要要的困擾,故而在此特意說起。

存在上下游的依賴管理

在真實場景中,依賴其實並不能徹底經過 Lerna 等工具管理,而是存在着上下游的區分的。這是什麼概念呢?以下圖:

通常來講,上游的基礎庫(如 Vue / Lodash 等)並不適合直接導入自有的宏倉庫中維護,而下游的具體業務項目多數也是與這些自有依賴獨立的,它們一樣在 Lerna 工具的控制範圍以外。這時,咱們仍然須要回到基本的 npm link 命令來創建本地的連接關係。但這可能會帶來更多的問題。例如,假設你在 Lerna 中管理 editor 與 editor-ui 兩個依賴,而業務項目 app 依賴了它們,這時候你不難把 editor 與 editor-ui 都 link 到 app 下。但這時的連接關係很容易被破壞,考慮下面的工做流:

  1. 你爲了修復 app 中 editor 的一些問題,更新了 editor 的代碼,並在本地驗證經過。
  2. npm publish 了 editor 與 editor-ui 的新版本。
  3. 你在 app 中 npm install editor editor-ui 並提交相應的改動。

Boom!執行了最後一步後,不光 app 與 editor 之間的連接關係會被破壞,editor 與 editor-ui 之間的連接關係也會被破壞。這就是軟連接的壞處了:下游的變動也會影響上游。這時,你須要從新作一次 lerna bootstrapnpm link 才能把這些依賴關係從新創建好,對於頻繁迭代的業務項目來講,這是至關棘手的。對這個問題,咱們提出的變通方案包括兩部分:

  • 能夠部署一個專門用於依賴安裝的業務項目環境。
  • 能夠編寫本身的 link 命令來替代 npm link

前者聽起來麻煩,但實際上只須要把 app 目錄複製一份便可。假設複製後獲得了 app-deps 目錄,那麼:

  • 將 editor-ui 與 editor 都 link 到 app 目錄下,使用它們在本地開發。
  • 在須要更新依賴版本時,在 app-deps 目錄下執行 npm install editor 便可。這不會 app 項目中破壞原有的連接關係。

固然,這時候 app 與 app-deps 之間的依賴可能不徹底同步——這個問題只要有 pull 代碼的習慣就能解決。另外的一種問題情形在於,若是下游的業務項目採用了 CNPM 等非 NPM 的包管理器來安裝依賴,那麼這時候原生的 link 命令容易失敗。仍是套用前面的例子,這時候咱們能夠在 editor 項目中創建 link 命令,來替代 npm link

// link.js
const path = require('path');
const { exec } = require('./utils'); // 建議將 childProcess.exec 封裝爲 Promise

const target = process.argv[2];
console.log('Begin linking……');

if(!target) {
    console.warn('Invalid link target');
    return;
}

const baseDir = path.join(__dirname, '../');
// 區分相對路徑與絕對路徑
const targetDepsDir = target[0] === '/'
    ? path.join(target, 'node_modules/my-editor')
    : path.join(__dirname, '../', target, 'node_modules/my-editor');

console.log(`${baseDir}${targetDepsDir}`);

exec(`rm -rf ${targetDepsDir} && ln -s ${baseDir} ${targetDepsDir}`)
.then(() => {
    console.log('🌈 Link done!');
})
.catch(err => {
    console.error(err);
    process.exit(1);
});
複製代碼

這樣只要在 editor 的 package.json 中增長一條 "link": "node ./link.js" 配置,就能經過 npm link path/to/app 的形式來完成連接了。這個連接操做跳過了很多中間步驟,所以比 NPM 原生的 link 速度要高得多,也能適配 CNPM 安裝的業務項目。

對於「自有依賴 → 下游業務」的情形,這兩個方式基本能保證開發節奏的順暢。但還有一個問題,就是「上游依賴 → 自有依賴」的時候,仍然可能須要折騰。這對應於什麼狀況呢?

通常來講,最上游的基礎庫應當是至關穩定的。可是你一樣可能須要修改甚至維護這樣的基礎庫。好比,咱們的 editor 編輯器依賴了咱們開源的歷史狀態管理庫 StateShot,這時候就須要本地連接 StateShot 到 editor 中了。

這個場景不能繼續前面的 npm link 套路嗎?固然能夠,不過上游的基礎庫並不須要頻繁的迭代來同步時,咱們建議使用 npm pack 命令來替代 link,以保證依賴結構的穩定性。如何使用這個命令呢?只須要這樣:

  1. 假設你有上游的 base 包,那麼在它的目錄下構建它以後,運行 npm pack
  2. pack 生成 base.tgz 以後,在 Lerna 管理的 editor 包下運行 npm install path/to/base.tgz
  3. lerna bootstrap 保證連接關係正確。

pack 的好處在於避開了軟連接的坑,還能更真實地模擬一個包從發佈到安裝的流程,這對於保證發佈的包可以正常安裝使用來講,是頗有用的。

總結

前端的工程化還在演化之中,從最簡單的 npm install 到各色命令與工具,相信將來的趨勢必定是可以讓咱們更加省心地維護好更大規模的項目,也但願文中的一些實踐可以對前端同窗有所幫助。

相關文章
相關標籤/搜索