本文須要 monorepo 基礎知識前端
在 monorepo 中,純 web 項目的產物輸出無疑是簡單的。Webpack 將全部的資源很好的打包/分割/分類並輸出至 dist/output 文件夾中,後續的產物處理時的視角只用集中在這個最終的 dist/outpu 文件夾中便可。node
但 Node.js 項目,或者更具體的說 Node.js server 項目則會有比較大的不一樣,常規項目中是不包含 bundle 環節的。他的最終產物在某種意義上是包含全部 runtime 中會使用到的依賴的,即 node_modules 中安裝的依賴也是最終產物的一部分。這在 multirepo 的狀況下仍是比較好處理的,利用包管理工具的 production only 的選項來安裝依賴,再將整個 repo 打包上傳轉移至 docker 等真實生產環境的容器中便可。在 monorepo 的場景下,打包整個 repo 顯然是會有問題的,由於太大了,他安裝的依賴每每是整個 monorepo 緯度的。因此大力智能前端的 monorepo 工具鏈在自身進化的過程當中也在不斷的嘗試去優化 Node.js 項目的產物輸出。git
# packages/xxxx/build.sh
#!/bin/bash
# 刪除node_modules軟連接
rm -rf node_modules output static_resources
# 安裝依賴
# 安裝的時候使用當前項目的 yarn.lock 文件
cp -r ../../yarn.lock yarn.lock
# 刪除根目錄的 package.json,取消 yarn workspace 的狀態
rm -rf ../../package.json
# 第一次可能會失敗,再來一次就成功了,好像是 yarn 處理本地 tgz 的一個BUG
yarn install --registry=http://our.own.registry
yarn install --registry=http://our.own.registry
# 建立輸出目錄
yarn run build
# 賦予bootstrap.sh可執行權限
chmod a+x bootstrap.sh
# 拷貝文件
mkdir output
cp -r $(ls | grep -v output) output
mkdir static_resources
複製代碼
最開始時,大力智能前端的 monorepo 仍是以純 yarn 的形式來維護。上圖 build 腳原本自於 packages/xxxx/build.sh
,在 root 的 build 腳本中,會根據環境變量來調用指定 package 的 build 腳本。簡單來講,這個腳本將這個項目基於 monorepo 的影響所有刪除,而後將頂層 lock 複製到項目中並執行 yarn,從而得到一個僅該項目使用到的依賴的 node_modules 並保證了獨立後的版本 lock 是與 monorepo 狀況下一致。下面簡單解釋一下三個關鍵命令。github
rm -rf node_modules output static_resource
首先刪除 node_modules,由於 yarn 在 workspace 狀態下有 hoist 的邏輯,有可能會將部分本項目的依賴安裝在 root/node_modules 下。因此當前狀態下(yarn 以 workspace 模式安裝完了 monorepo 全部依賴),項目內的 node_modules 是不可信的,須要刪除。同時 output 與 static_resource 是在 workspace 狀態下有可能會生成的產物文件(某些包可能存在 postinstall 邏輯被執行),爲了減小干擾,也一併刪除。web
cp -r ../../yarn.lock yarn.lock & rm -rf ../../package.json
將根目錄的 lock 文件複製到當前 package 下,這樣能保證明際安裝時 resolve 的版本號能和 monorepo 狀況下安裝的一致。(實踐證實,yarn.lock 中存在多餘的信息時,並不會影響實際安裝結果)docker
同時將 monorepo root 下的 package.json 刪掉,能夠避免 yarn 誤認爲仍在 monorepo 的場景中,不然會影響 yarn install 的不少默認行爲。npm
yarn install --registry=``http://bnpm.byted.org
x2在 root 的 build 腳本中執行 install 的時候,root package 其實還有一個 postinstall 的邏輯,把 local package 的版本號,改爲 file:../../xxx.tar.gz 的形式(具體原理詳見 Selective dependency resolutions 與 yarn add)。執行 yarn 兩次是爲了解決安裝手動寫入的本地 tar.gz 的問題。json
# packages/xxxx/build.sh
#!/bin/bash
cd ../..
yarn deploy:node @xxx/xxx # === yarn isolate-workspace -o output -w @xxx/xxx
cd output
yarn install --registry=http://bnpm.byted.org
# 賦予bootstrap.sh可執行權限
chmod a+x bootstrap.sh
cd ..
mkdir static_resources
複製代碼
在 19 年 12 月,junchen 同窗在給 monorepo 引入 lerna 的同時,也引入了 isolate-workspace(yarn-workspace-isolator)來完成 Node 項目的部署。isolate-workspace 將原來腳本完成的功能收斂的同時,也完成了 monorepo 內部依賴的狀況的適配。這裏詳細說一下 isolate-workspace 內部作了些什麼。bootstrap
首先就是最常規的將目標 package 轉移至產物目錄下,這時產物目錄下就像是一個正常的單 package repo,而後開始基於 yarn root 下 workspace 字段,遍歷目標的依賴項中是否存在在 monorepo 內的依賴。若是有就記錄下來,同時將 package.json 中 dependecy/devDependency 中的對應字段改寫爲 "@xxx/yyy": "file: path/to/output/node_modules/@xxx/yyy"。對於這樣的寫法 yarn 將會視其爲本地的一個依賴,在後續 install 時,將會視做一個已經下載好的包,而且會去安裝這個本地包的依賴項。bash
同理於上述步驟,workspace 內的全部被依賴到的 package 將會被放置於產物文件夾的 node_modules 內,同時若是他們的依賴中有 monorepo 內的依賴,也會被同理處理。
可是這裏有兩個須要說明的小問題
# packages/xxxx/build.sh
#!/bin/bash
node ../../common/scripts/install-run-rush.js deploy-node -target @xxx/xxx --output output
cd ../../output
# 賦予bootstrap.sh可執行權限
chmod a+x bootstrap.sh
複製代碼
在 21 年初,大力智能前端的 monorepo 由 yarn + lerna 遷移至 rush + pnpm。這個工具鏈的遷移也形成了原來 isolate-workspace 的沒法使用,由於 pnpm 對 node_modules 的組織形式不同了,並且管理 workspace/project 的方式也不一樣了。幸運的是 rush 自己就提供了一套用來生成 Node.js 部署產物的功能,即 rush deploy,但由於 rush deploy 的最終產物中保留了 monorepo 內的文件分佈形式,要讓整個部署完成,同時知足雲平臺的一些規範,咱們仍是要在 rush deploy 的先後添加一些額外的邏輯,因此咱們添加了一個自有的 rush command rush deploy-node
。
在遷移 rush deploy 的同時,咱們發現多數後臺 Node.js 項目中存在一些基於約定而產生的樣板代碼。好比 admin_node 會伴隨存在一個 admin_web,web 中的產物在完成目錄轉移以後,由 node 項目進行靜態文件 serve。因此咱們把這個 web/node 伴生關係的邏輯寫在了統一的腳本中,方便後期維護。
Rush deploy 是一整套完整部署工具,同時支持一個 monorepo 存在多種不同的 deploy scenario。大力智能前端的 monorepo 中,咱們使用了最簡單的形式,主要依賴了 deploy 中將整個 monorepo 以及其已經安裝好的依賴部分獨立的能力。
Rush Deploy 會將目標 package 以及目標 package 在 monorepo 內的依賴以及已經安裝好的依賴輸出至產物文件夾中,同時保留了原來 monorepo 的總體文件目錄結構。換個角度來講,能夠理解爲基於目標 package 將 monorepo 內不相關,沒有依賴到或使用到的文件所有刪除,而後輸出整個 monorepo 目錄。
這裏要額外提一句的是,deploy 有默認的 filter 邏輯。即基於 .npmignore 來判斷目標 package 的那些文件須要被轉移至產物文件夾中,並且在 .npmignore 文件不存在時,.gitignore 文件會生效。咱們的 Node 場景中絕大部分都是基於 Typescript 的 server 項目,須要編譯成 js 來最終運行,並且項目中沒有 .npmignore 文件。因此咱們的作法是將 deploy 配置文件中的 "includeNpmIgnoreFiles"
設置爲 true,而後在 rush deploy 命令執行以前會自動加上包含 !*.js
與 !**/*.js
的 .npmignore 來保證 ts complier 產出 js 會被最終轉移至產物文件夾中。
更多 rush deploy 的用法能夠參考官方文檔 rushjs.io/pages/maint…
在內部部署的狀況下,咱們有不少依賴於產物文件下根目錄的文件的約定,好比 xsettings.txt,bootstrap.sh 等。轉變爲 rush deploy 的形式後,目標 package 再也不存在在產物目錄的根目錄中,因此咱們給每一種約定都寫了相應的 facading 邏輯來保證目標 package 的約定文件像原來同樣出如今產物目錄的根目錄中。這樣一來,Node 項目在具體 PaaS 雲平臺或其餘平臺上的運行時配置就不須要改變。
// facade xsettings.txt
const sourceSettingsPyPath = path.resolve(
monoRoot,
mainProjectSubPath,
'xsettings.txt',
);
const targetSettingsPyPath = path.resolve(distDirname, 'xsettings.txt');
if (fs.existsSync(sourceSettingsPyPath)) {
logger.info(
'[START] copy xsettings.txt ' +
`${sourceSettingsPyPath} -> ${targetSettingsPyPath}`,
);
await fs.copyFile(sourceSettingsPyPath, targetSettingsPyPath);
logger.info('[DONE] copy xsettings.txt');
}
// facade bootstrap.sh
if (fs.existsSync(path.join(mainProjectSubPath, 'bootstrap.sh'))) {
const facadeBootstrapShellPath = path.resolve(distDirname, 'bootstrap.sh');
logger.info(`[START] create bootstrap.sh ${facadeBootstrapShellPath}`);
await fs.outputFile(
facadeBootstrapShellPath,
`#!/bin/bash source ${path.join(mainProjectSubPath, 'bootstrap.sh')} `,
);
logger.info('[DONE] create bootstrap.sh');
}
// facade bootstrap.js
if (fs.existsSync(path.join(mainProjectSubPath, 'bootstrap.js'))) {
const facadeBootstrapJsPath = path.resolve(distDirname, 'bootstrap.js');
logger.info(`[START] create bootstrap.js ${facadeBootstrapJsPath}`);
await fs.outputFile(
facadeBootstrapJsPath,
` require('./${path.join(mainProjectSubPath, 'bootstrap.js')}') `,
);
logger.info('[DONE] create bootstrap.js');
}
複製代碼
基於 Rush deploy 的遷移以後,咱們解決了 lock 不生效以及 install 屢次帶來的 CI/CD 效率問題。整體來講效率與穩定性的收益是很是可觀的。
歡迎關注「 字節前端 ByteFE 」 簡歷投遞聯繫郵箱「tech@bytedance.com」