隨着前端工程規模的增長,各類第三方與自有依賴包的關係也日趨複雜。這時候可能產生什麼問題,又該如何解決呢?這裏分享咱們前端團隊的一些實踐。前端
安裝依賴包,對於前端開發者來講不過就是一句 npm install xxx
的事。那麼,單純靠這種方式給一個項目安裝了不少依賴,就算是複雜的依賴關係嗎?這裏咱們這樣定義「複雜」:node
若是純粹只靠 npm install
,那麼全部的包都必須發佈到 NPM 以後才能被其餘的包更新。在「聯調」這些包的時候,每次稍有更改都走一遍正式的發佈流程,無疑是很是繁瑣而影響效率的。咱們有什麼現成的工具來解決這個問題呢?git
提到管理多個包之間的依賴關係,不少同窗應該能立刻想到很多現成的工具,好比:github
這裏的「萬惡之源」就是 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。怎麼會出現這種狀況呢?舉一個例子:編輯器
這時候就出現了循環依賴。雖然 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 下。但這時的連接關係很容易被破壞,考慮下面的工做流:
npm publish
了 editor 與 editor-ui 的新版本。npm install editor editor-ui
並提交相應的改動。Boom!執行了最後一步後,不光 app 與 editor 之間的連接關係會被破壞,editor 與 editor-ui 之間的連接關係也會被破壞。這就是軟連接的壞處了:下游的變動也會影響上游。這時,你須要從新作一次 lerna bootstrap
與 npm link
才能把這些依賴關係從新創建好,對於頻繁迭代的業務項目來講,這是至關棘手的。對這個問題,咱們提出的變通方案包括兩部分:
npm 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,以保證依賴結構的穩定性。如何使用這個命令呢?只須要這樣:
npm pack
。base.tgz
以後,在 Lerna 管理的 editor 包下運行 npm install path/to/base.tgz
。lerna bootstrap
保證連接關係正確。pack 的好處在於避開了軟連接的坑,還能更真實地模擬一個包從發佈到安裝的流程,這對於保證發佈的包可以正常安裝使用來講,是頗有用的。
前端的工程化還在演化之中,從最簡單的 npm install
到各色命令與工具,相信將來的趨勢必定是可以讓咱們更加省心地維護好更大規模的項目,也但願文中的一些實踐可以對前端同窗有所幫助。