這是第 80 篇不摻水的原創,想獲取更多原創好文,請搜索公衆號關注咱們吧~ 本文首發於政採雲前端博客: npm 私庫從搭建到數據遷移最後容災備份的一些解決方案
按照國際慣例,正文開始以前,咱們先簡單介紹下目前市面上的 npm 私庫開源框架。前端
Verdaccio 是 sinopia 開源框架的一個分支。它提供了本身的小數據庫,以及代理其餘註冊中心的能力(例如。npmjs.org 網站),配置以及部署相對簡單,一步到"胃"。若是公司的私包比較少的話或者你想偷懶,能夠考慮一下。node
大名鼎鼎的 cnpm,想必各位早就感覺到了它的速度之「快」,沒錯,它的 register 服務就是淘寶鏡像。主要是基於Koa、MySQL 和簡單存儲服務的企業專用 npm 註冊和 web 服務,其中最強大的功能就是它的同步模塊機制(定時同步全部源 registry 的模塊、只同步已經存在於數據庫的模塊、只同步 popular 模塊)。mysql
後端開發的小夥伴應該比較熟悉。Nexus2 主要是用於 maven/gralde 倉庫的統一管理,而 Nexus3 則添加了npm插件,能夠對 npm 提供支持,其中 npm 倉庫有三種類型,分別是 hosted(私有倉庫)、proxy(代理倉庫)、group(組合倉庫)。web
整體來說,拋開 Nexus,雖然 Cnpmjs.org 在部署過程以及整體設計方案上相對於 Verdaccio 複雜的多,可是它提供更高的拓展性,定製性,能夠支持多種業務使用場景。接下來,咱們分別從 Cnpmjs.org 容器化部署、數據遷移、OSS 容災備份等內容,層層展開。sql
目前,公司的應用部署基本都是容器化部署,內部搭建了 ipaas 平臺,應用流程化部署以及一鍵發佈。而 Cnpmjs.org 也附帶了 Dockerfile 以及 docker-compose.yml 文件,因此,這裏大體講解下怎麼用 docker 部署吧。docker
FROM node:12 MAINTAINER zian yuanzhian@cai-inc.com # Working enviroment ENV \ CNPM_DIR="/var/app/cnpmjs.org" \ CNPM_DATA_DIR="/var/data/cnpm_data" # shell格式 # 在docker build 時運行 RUN mkdir -p ${CNPM_DIR} # 指定工做目錄:用 WORKDIR 指定的工做目錄,會在構建鏡像的每一層中都存在 WORKDIR ${CNPM_DIR} # 複製指令:從上下文目錄中複製目錄或文件到容器裏指定的路徑 COPY package.json ${CNPM_DIR} RUN npm set registry https://registry.npm.taobao.org RUN npm install --production COPY . ${CNPM_DIR} COPY docs/dockerize/config.js ${CNPM_DIR}/config/ # 聲明端口(7001爲register服務、7002爲web服務) EXPOSE 7001/tcp 7002/tcp # 匿名數據卷:在啓動容器時忘記掛載數據卷,會自動掛載到匿名卷。 VOLUME ["/var/data/cnpm_data"] RUN chmod +x ${CNPM_DIR}/docker-entrypoint_prod.sh # Entrypoint # exec格式 # 在docker run 時運行 # dockerfile存在多個 CMD 命令,僅最後一個生效 # CMD ["node", "dispatch.js"] CMD ["npm", "run", "prod"]
這裏把 CMD 命令修改成["npm", "run", "prod"]
,由於增長了一層不一樣環境的 shell 腳本,目前全局變量全都存放在這裏。shell
示例:docker-entrypoint_env.sh數據庫
export DB='db_cnpmjs' export DB_USRNAME='root' export DB_PASSWORD='123456' export DB_HOST='127.0.0.1' export BINDING_HOST='0.0.0.0' DEBUG=cnpm* node dispatch.js
version: '3' # docker版本 services: # 配置的容器列表 web: # 自定義,服務名稱 build: # 基於dockerfile構建鏡像(可增長args) context: . dockerfile: Dockerfile ## 依賴的Dockerfile文件 image: cnpmjs.org # 鏡像名稱或id volumes: - cnpm-files-volume:/var/data/cnpm_data ports: - "7001:7001" - "7002:7002"
<font color="red">注意點:一、全局配置文件路徑: /docs/dockerize/config.js ;二、bindingHost 爲 0.0.0.0 。</font>npm
docker-compose up -d
,即以守護進程模式形式啓動應用,而後打開瀏覽器入http://127.0.0.1:7002
,就會看到 web 頁面。執行 npm config set registry http://127.0.0.1:7001
可設置爲搭建的私庫的鏡像源地址,這裏推薦使用 nrm
,可自由切換 npm 源。展現站點以下圖:json
<font color="red">注意點:一、當你改變本地代碼以後,先執行 docker-compose build 構建新的鏡像,而後執行 docker-compose up -d 取代運行中的容器。</font>
因爲公司以前用的 Verdaccio 搭建的私庫,要切換使用新的 npm 私庫,意味着要把以前發佈過的私包所有遷移過來。大概統計了下,有400 多個 package,總共有 7000 多個版本,按照正常邏輯,作數據遷移首先會從數據庫下手,可是 Verdaccio 並不依賴數據庫。剛開始沒有一點頭緒,大概看了下 Cnpmjs.org 的源碼,瞭解到當咱們 publish 模塊時, 它是怎麼把 npm 模塊 的元數據存儲到數據庫,下面咱們一步步來揭開她的面紗。
經過路由文件(/routes/registry.js
)咱們很容易找到/controllers/registry/package/save.js
,這個文件即是咱們想要的。
核心代碼:
var pkg = this.request.body; // 這裏拿到npm模塊元數據,即package.json文件通過libnpmpublish模塊處理過的json數據 var username = this.user.name; // 當前用戶名 var name = this.params.name || this.params[0]; // npm模塊名 var filename = Object.keys(pkg._attachments || {})[0]; // npm模塊的壓縮後的文件名 var version = Object.keys(pkg.versions || {})[0]; // npm模塊的最新版本
// upload attachment // base64解碼,獲取模塊文件二進制數據。從libnpmpublish模塊瞭解到tardata.toString('base64'),即npm模塊文件流轉base64字符串 var tarballBuffer = Buffer.from(attachment.data, 'base64'); // 默認使用fs-cnpm,將npm模塊文件保存到本地,默認保存路徑:path.join(process.env.HOME, '.cnpmjs.org', 'nfs') var uploadResult = yield nfs.uploadBuffer(tarballBuffer, options); var versionPackage = pkg.versions[version]; var dist = { shasum: shasum, size: attachment.length }; // if nfs upload return a key, record it if (uploadResult.url) { dist.tarball = uploadResult.url; } else if (uploadResult.key) { dist.key = uploadResult.key; dist.tarball = uploadResult.key; } var mod = { name: name, version: version, author: username, package: versionPackage }; mod.package.dist = dist; // 模塊數據保存到數據庫 var addResult = yield packageService.saveModule(mod);
即只要咱們可以拿到 npm 模塊的元數據(即 package.json 被處理過的 json 數據),就能把模塊文件上傳到文件系統或者 OSS 服務,同時數據落庫。Verdaccio 有兩個 api 能夠拿到其私庫 npm 模塊全量數據和當前 npm 模塊的 json 數據,路徑分別是/-/verdaccio/packages
,/-/verdaccio/sidebar/$PKG$
,其中有 scope 的模塊的請求路徑是/-/verdaccio/sidebar/$SCOPE$/$PKG$
。
思路已經很明確了,開始動起來吧!新增 save_zcy.js 文件,基於原來的/controllers/registry/package/save.js
稍加改造下。
核心代碼:
// 請求遠程文件,並返回二進制流 const handleFiles = function (url) { return new Promise((resolve, reject) => { try { http.get(url, res => { res.setEncoding('binary') // 二進制 let files = '' res.on('data', chunk => { // 加載到內存 files += chunk }).on('end', () => { // 加載完 resolve(files) }) }) } catch (error) { reject(error) } }) }; // 獲取遠程模塊文件的二進制數據 yield handleFiles(dist.tarball).then(res => { // 利用 Buffer 轉爲對象 const tardata = Buffer.from(res, 'binary') pkg._attachments = {}; pkg._attachments[filename] = { 'content_type': 'application/octet-stream', 'data': tardata.toString('base64'), // 從緩衝區讀取數據,使用base64編碼並轉換成字符串 'length': tardata.length, }; }, error => { this.status = 400; this.body = { error, reason: error, }; return; });
接下來咱們把控制器 save_zcy.js 接入到 registry 服務的 app 路由上。
// 新增 fetchPackageZcy、savePackageZcy 控制器 app.get('/:name/:version', syncByInstall, fetchPackageZcy, savePackageZcy, getOneVersion); app.get('/:name', syncByInstall, fetchPackageZcy, savePackageZcy, listAllVersions);
控制器 fetchPackageZcy 做用是請求上面的 api(/-/verdaccio/sidebar/$SCOPE$/$PKG$ 或 /-/verdaccio/sidebar/$PKG$)來拉取對應模塊的 json 數據。
Ok,接下來咱們寫一個定時任務,每隔一段時間執行 npm install [name]
,這樣原來私庫的 npm 包都可以 install 並進入到上面的控制器邏輯,大功告成!
首先,簡單說明下爲何要作 OSS 容災備份,有如下幾點。
基於以上幾點,咱們整理了下容災備份方案:
<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/06b600e8297a434087f43bcad08c7f6d~tplv-k3u1fbpfcp-zoom-1.image" style="zoom:33%;" />
即發佈模塊文件時本地存儲,同時上傳到 oss 做爲備份,用到的插件分別是 fs-cnpm、oss-cnpm。
<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/df3ff3801cf74755a0c09be7d2a19029~tplv-k3u1fbpfcp-zoom-1.image" style="zoom:33%;" />
即下載模塊文件時,先判斷是不是私包(便是包名否有帶 scope),若是不是私包代理到上游 registry,如果私包先判斷服務器本地是否有該私包文件,若是不存在先去 oss 下載到本地 nfs 目錄下,若是存在則直接從 nfs 目錄找到模塊文件,而後讀取並寫到 downloads 目錄下,最後調用 fs.createReadStream 方法流讀取該文件。
isEnsureFileExists 即判斷模塊文件本地是否存在,代碼以下:
const mkdirp = require('mkdirp'); const fs = require('fs'); function ensureFileExists(filepath) { return function (callback) { fs.access(filepath, fs.constants.F_OK, callback); }; }
注意,在 oss 下載模塊文件到 nfs 以前,必定要先建立模塊文件目錄,方法以下:
const mkdirp = require('mkdirp'); function ensureDirExists(filepath) { return function (callback) { mkdirp(path.dirname(filepath), callback); }; }
Cnpmjs.org 原本就帶有郵件通知的功能,但只應用錯誤日誌上報。因爲咱們的私包大部分都是業務組件、工具等,有時候發佈正式版本的業務組件須要通知到業務組件的使用方。目前,咱們採用 maintainers 來維護,包含模塊的維護者及使用者。
示例:
"maintainers": [ { "name": "yuanzhian", "email": "yuanzhian@cai-inc.com" } ]
郵箱配置以下:
mail: { enable: true, appname: 'cnpmjs.org', from: process.env.EMAIL_HOST, host: 'smtp.mxhichina.com', service: 'qiye.aliyun', // 使用了內置傳輸發送郵件,查看支持列表:https://nodemailer.com/smtp/well-known/ port: 465, // SMTP 端口 secureConnection: true, // 使用了 SSL auth: { user: process.env.EMAIL_HOST, pass: process.env.EMAIL_PSD, // } }
將來,咱們還能夠在 Cnpmjs.org 上作不少定製化開發,好比接入公司內部權限系統、web 頁面重構、對接業務組件在線文檔等等。若是你正好也須要搭建 npm 私有庫,但願這篇文章對你有所幫助。
政採雲前端團隊(ZooTeam),一個年輕富有激情和創造力的前端團隊,隸屬於政採雲產品研發部,Base 在風景如畫的杭州。團隊現有 40 餘個前端小夥伴,平均年齡 27 歲,近 3 成是全棧工程師,妥妥的青年風暴團。成員構成既有來自於阿里、網易的「老」兵,也有浙大、中科大、杭電等校的應屆新人。團隊在平常的業務對接以外,還在物料體系、工程平臺、搭建平臺、性能體驗、雲端應用、數據分析及可視化等方向進行技術探索和實戰,推進並落地了一系列的內部技術產品,持續探索前端技術體系的新邊界。
若是你想改變一直被事折騰,但願開始能折騰事;若是你想改變一直被告誡須要多些想法,卻無從破局;若是你想改變你有能力去作成那個結果,卻不須要你;若是你想改變你想作成的事須要一個團隊去支撐,但沒你帶人的位置;若是你想改變既定的節奏,將會是「5 年工做時間 3 年工做經驗」;若是你想改變原本悟性不錯,但老是有那一層窗戶紙的模糊… 若是你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的本身。若是你但願參與到隨着業務騰飛的過程,親手推進一個有着深刻的業務理解、完善的技術體系、技術創造價值、影響力外溢的前端團隊的成長曆程,我以爲咱們該聊聊。任什麼時候間,等着你寫點什麼,發給 ZooTeam@cai-inc.com