本篇接續 Yarn 2 和 Monorepo 提到的部屬的部分,因為 monorepo 裡包含了很多套件和網站,如果直接在根目錄執行 docker build
把整個 monorepo 打包成 Docker image 的話,勢必會做出大於 1 GB 而且內含一堆無用垃圾的 Docker image;為了要讓 Docker image 能夠最小化,必須只打包正式環境會需要用到的套件,確保不會浪費任何空間和時間。
我把建構 Docker image 的步驟分為:
這部分對於 Yarn 2 來說非常容易,在擴充套件裡可以直接讀取整個 monorepo 的狀態,可以參考 yarn workspaces focus
的原始碼,這個指令是用來只安裝特定 workspace 需要用到的套件,剛好和我需要的功能相同。
import { Configuration, Project, Cache } from '@yarnpkg/core';// 讀取設定const configuration = await Configuration.find(this.context.cwd, this.context.plugins);const { project } = await Project.find(configuration, this.context.cwd);// 取得指定的 workspaceconst workspace = project.getWorkspaceByIdent( structUtils.parseIdent('@foo/bar'),);const requiredWorkspaces = new Set([workspace]);for (const ws of requiredWorkspaces) { // scope 可以是 `dependencies`, `devDependencies` // 因為我們只需要正式環境會用到的套件,所以這邊只用 `dependencies` const deps = ws.manifest.getForScope('dependencies').values(); // 把相依的 workspace 新增到 `requiredWorkspaces` 裡 for (const dep of deps) { const workspace = project.tryWorkspaceByDescriptor(dep); if (workspace) { requiredWorkspaces.add(workspace); } }}// 接著把 project 裡所有 workspace 的 manifest (package.json) 都清理一遍for (const ws of project.workspaces) { // 如果這個 workspace 在正式環境會用到,那麼只清掉 `devDependencies` if (requiredWorkspaces.has(ws)) { ws.manifest.devDependencies.clear(); } else { // 否則就把所有的 dependencies 都清掉 ws.manifest.dependencies.clear(); ws.manifest.devDependencies.clear(); ws.manifest.peerDependencies.clear(); }}
透過上面的程式碼,就能得到正式環境需要用到的 workspaces。接下來,我會重跑一遍 yarn install
,因為已經有快取了,所以不需要花多少時間,這是為了產生更新後的 yarn.lock
,並了解有哪些 .yarn/cache
的檔案會被用到。
// 讀取現有快取const cache = await Cache.find(configuration);// 解析 dependencies// 這部分相當於 `yarn install` 裡的 `Resolution Step`await project.resolveEverything({ report, cache });// 下載 dependencies// 這部分相當於 `yarn install` 裡的 `Fetch Step`await project.fetchEverything({ report, cache });// 執行完上面兩個步驟後,就能產生新的 `yarn.lock`const newLockFile = project.generateLockFile();// 也能知道有哪些 cache 會被用到for (const file of cache.markedFiles) {}
除了相依套件外,還需要把 workspaces 的原始碼也複製到 Docker image 裡,為了精簡需要複製的檔案量,可以參考 yarn pack
的原始碼,我在這邊用 packUtils
取得檔案列表,然後再複製到指定的資料夾裡。
import { packUtils } from '@yarnpkg/plugin-pack';// `prepareForPack` 是用來執行 `prepack` 和 `postpack` 等 lifecycle hooks 的await packUtils.prepareForPack(workspace, { report }, async () => { // 取得檔案列表 const files = await packUtils.genPackList(workspace); // 如果想要把檔案壓成壓縮檔的話,可以用 `genPackStream` const stream = await packutils.genPackStream(workspace);});
最後,檔案會被分成兩個部分複製到不同的資料夾,一個是 manifests
,用來儲存 yarn install
需要用到的檔案,像是 package.json
, yarn.lock
和快取;另一個部分則是 workspaces 的原始碼,也就是上面 yarn pack
產生的結果。
以下是 Dockerfile
的範例:
FROM node:12-alpine AS builderWORKDIR /workspaceCOPY manifests ./RUN yarn install --immutableRUN rm -rf .yarn/cacheFROM node:12-alpineWORKDIR /workspaceCOPY --from=builder /workspace ./COPY packs ./CMD yarn workspace @foo/bar start
在寫完 Yarn 2 那篇文章後,我花了一些時間把內部使用的 Yarn 擴充套件整理了一下並開源,各位可以試用看看:yarn-plugin-docker-build。