CNPM 的自定義包儲存層檔案系統簡稱 NFS,我猜是 NPM File System 的意思。
在之前《跟我一起部署和定製 CNPM——基礎部署》中提到過,CNPM 配置項裡面有一項配置 nfs
,它所對應的是一個 NFS 物件。
在同步 package 的時候,CNPM 會把源站的包下載到本地,然後傳給 NFS 物件相應的函式交予去處理,由 NFS 物件返回處理結束之後該包在我們自己部署的 CNPM 對應的包下載連結。
上面的這一套流程就給我們自定義包儲存提供了可能,比如我們可以把包同步到又拍雲端儲存、阿里雲 OSS 等地方去,也可以以二進位制的形式存入我們自己的資料庫(不推薦),甚至可以什麼都不用做直接放在本地,然後把本地檔案對外網暴露即可。
NFS 介面
NFS 的介面是實現定義好的,我們如果要寫一個自己的 NFS 類,只需要按照約定的介面實現他們的邏輯即可。
雖然我自己不喜歡,但是 NFS 的所有函式需要在菊花函式中被實現。
下面給出介面的定義:
function* upload(filepath, options)
filepath
:檔案路徑。options
key
:待上傳檔案的標識size
:待上傳檔案大小
function* uploadBuffer(fileBuffer, options)
fileBuffer
:待上傳檔案的 Bufferoptions
key
:待上傳檔案的標識size
:待上傳檔案的大小
function* remove(key)
key
: 檔案標識
function* download(key, savePath, options)
(可選實現)key
:檔案標識savePath
:儲存路徑options
timeout
:超時時間
function* createDownloadStream(key, options)
(可選實現)key
: 檔案標識options
timeout
:超時時間
- 返回一個
ReadStream
function[*] url(key)
(可選實現,可以不是菊花函式)key
: 檔案標識
OSS-CNPM 解析
這裡拿出一個 NFS 的官方實現阿里雲 OSS 版來作為解析。它的 Repo 是https://github.com/cnpm/oss-cnpm。
開啟 index.js 我們能看到,的確 OssWrapper
實現了上面的一些介面。
建構函式
在 function OssWrapper
裡面我們看到它 new
了 ali-oss 物件。
1 2 3 4 5 6 |
if (options.cluster) { options.schedule = options.schedule || 'masterSlave'; this.client = new oss.ClusterClient(options); } else { this.client = oss(options); } |
也就是說在各種上傳等函式裡面都是以這個 client
為主體做的事情的。
upload 和 uploadBuffer
首先我們看看 upload
函式,從外部傳進來檔案的 key
,NFS 物件將該檔案以 key
為名傳到 OSS 去,並返回該檔案上傳之後在 OSS 上的地址。
1 2 3 4 5 6 7 8 9 10 11 |
proto.upload = function* (filePath, options) { const key = trimKey(options.key); // https://github.com/ali-sdk/ali-oss#putname-file-options const result = yield this.client.put(key, filePath, { headers: this._defaultHeaders, }); if (this._mode === 'public') { return { url: result.url }; } return { key: key }; }; |
uploadBuffer
其實也一樣,引數第一個 fileBuffer
是一個檔案二進位制 Buffer 物件,而 ali-oss
包的 put
函式第二個引數既可以傳一個檔案路徑,也可以傳一個 Buffer,所以相當於把 upload
這個函式直接拿過來就能用了,於是就有了:
1 |
proto.uploadBuffer = proto.upload; |
remove、download 和 createDownloadStream
這兩個函式實際上也是直接呼叫了 ali-oss
的函式,並沒有什麼好講的,大家自己看看就好了。
url
這個函式無非就是判斷下有沒有自定義的 CDN 域名什麼的,根據不同的返回不同的網址而已。
trimKey
把 key
裡面帶的最前面的斜槓去掉。
我的 OSS-CNPM 隨意改造
上面一節解析了 oss-cnpm
這個包的程式碼,如果官方出的幾個 NFS 包不能滿足,大家也能自己去寫一個 CNPM 儲存層的包了。
我們公司的包是直接在 OSS 上面的,所以用 oss-cnpm
並沒有什麼不妥。
不過對於阿里系本身的公司門來說,OSS 並不是什麼大事兒,對於我們來說,OSS 的 bucket 資源還是蠻稀缺的,上次就達到上限了。所以我們目前的 NPM 包跟公司別的測試業務用的是同一個 bucket。
那麼問題來了:
oss-cnpm
直接把所有檔案放在根目錄下建資料夾,太亂了,而且的確是有小可能衝突的。而這個包又不能讓人自定義字首什麼什麼的。
於是我就自己 Fork 小小改裝了一下這個包,讓它適合我們公司自己。
改裝很簡單,在上傳的目錄中加一個資料夾字首。
動的是 trimKey
函式:
1 2 3 |
function trimKey(key) { return '_snpm_/' + (key ? key.replace(/^\//, '') : ''); } |
這下所有在我們內部 CNPM 裡面的包的連結都多了個 _snpm_/
的字首了。
CNPM 呼叫解析
上面解析了介面之後,我們來扒一扒什麼時候會呼叫上面實現的介面們吧,這樣就知道 CNPM 對於 NFS 使用的工作原理了。
controllers/registry/package/download.js
對於包下載來說,它的路由是:
1 |
/{package}/download/{package}-{version}.tgz |
然後在裡面判斷一下如果 NFS 物件有實現 url()
函式的話,先用 url()
函式生成對該包而言的真實下載連結。
讀出這個包的 registry 資訊,裡面如果沒有 dist
等引數的話直接 302 到剛生成的地址去。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
if (typeof nfs.url === 'function') { if (is.generatorFunction(nfs.url)) { url = yield nfs.url(common.getCDNKey(name, filename)); } else { url = nfs.url(common.getCDNKey(name, filename)); } } if (!row || !row.package || !row.package.dist) { if (!url) { return yield* next; } this.status = 302; this.set('Location', url); _downloads[name] = (_downloads[name] || 0) + 1; return; } |
接下去是涉及到上一章沒有提到過的一個配置引數,叫 downloadRedirectToNFS
,預設為 false
。如果該值為 true
的話並且剛才由 url()
函式生成了下載連結的話,也是直接 302 到真實下載連結去。
1 2 3 4 5 |
if (config.downloadRedirectToNFS && url) { this.status = 302; this.set('Location', url); return; } |
不過如果本身 registry 裡面就沒 key
這個選項的話也會直接用 url()
生成的連結給跳過去。如果沒有 url()
的連結,那麼直接用 registry 裡面的 tarball
欄位。
1 2 3 4 5 6 7 |
var dist = row.package.dist; if (!dist.key) { url = url || dist.tarball; this.status = 302; this.set('Location', url); return; } |
上面如果都跳過去了,那麼說明要開始呼叫事先寫好的 download
那兩個函式了,把檔案讀到 Buffer 裡面,然後把 Buffer 放到 Response 裡面傳回去。
controllers/registry/package/remove.js
對於刪除包來說,除了把包從資料庫刪掉之外,還要迴圈遍歷一遍這個包的所有版本,把所有版本的這個包都從 NFS 裡面刪除。
1 2 3 4 5 6 7 |
try { yield keys.map(function (key) { return nfs.remove(key); }); } catch (err) { logger.error(err); } |
這裡就呼叫了你事先寫好的 remove
了。當然你不實現也沒關係,最多是包的壓縮檔案不刪除而已。
controllers/registry/package/remove_version.js
這裡跟上一小節差不多,之前是刪除整個包,這裡是刪除包的某一個版本,所以就不用迴圈刪除了。
1 2 3 4 5 |
try { yield nfs.remove(key); } catch (err) { logger.error(err); } |
controllers/registry/package/save.js
然後就是使用者 $ npm publish
用的路由了,在一堆判斷之後,釋出傳過來的包被放在二進位制 Buffer 記憶體裡面:
1 2 |
var tarballBuffer; tarballBuffer = new Buffer(attachment.data, 'base64'); |
接下去又判斷來判斷去,最後交由 NFS 的 uploadBuffer
來上傳並得到結果。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var uploadResult = yield nfs.uploadBuffer(tarballBuffer, options); var dist = { shasum: shasum, size: attachment.length }; if (uploadResult.url) { dist.tarball = uploadResult.url; } else if (uploadResult.key) { dist.key = uploadResult.key; dist.tarball = uploadResult.key; } |
看到沒有,就是這裡記錄的它到底是 key
還是 tarball
了。
如果你的 upload
函式返回的是 { url: 'FOO' }
,那麼就是 tarball
設定成該值,在下載的時候會直接 302 到 tarball
所指的地址去;如果返回的是 { key: 'key' }
的話,會在 dist
裡面存個 key
,下載的時候判斷如果有 key
的話會把它傳進你的 createDownloadStream
或者 download
函式去交由你的函式生成包 Buffer 並傳回 Response。
controller/syncmoduleworker.js
這個檔案是從源端同步相關的一些邏輯了,這裡面有兩個操作。
一個是 unpublish
,呼叫的就是 NFS 的 remove
,不作詳談了。
另一個就是同步了。同步包會被打散成同步一個版本,然後把每個版本同步過來。在同步版本的時候先把包檔案下載到本地檔案 filepath
裡面去。
1 |
var r = yield urllib.request(downurl, options); |
urllib 是蘇千死馬他們自己寫的比較方便和適合他們自己的一個 http 請求庫。
上面的程式碼 options
裡面有一個檔案流,連結到 filepath
目錄的這個檔案去,相當於這一步就是把源端的包下載到本地 filepath
去了。
經過一堆 blahblah 的判斷(比如 SHASUM)之後,這個這個函式就會呼叫 NFS 的 upload
函式將本地檔名對應的檔案上傳到你所需要的地方去了。
1 2 3 4 5 6 |
try { result = yield nfs.upload(filepath, options); } catch (err) { logger.syncInfo('[sync_module_worker] upload %j to nfs error: %s', err); throw err; } |
其結果到底是
key
還是url
對於下載的影響跟前一小節一個道理。
小結
本章講了如何使用和自己定製一個 CNPM 的 NFS 層,讓包的走向跟著你的心走。在描述了開發規範和出示了樣例程式碼和改造小例子之後,又解析了這個 NFS 是如何在 CNPM 裡面工作的,上面已經提到了 2.12.2 版本中所有用到 NFS 的地方。
看了上面的解析之後會對 NFS 的工作流程有更深一層的瞭解,然後就不會有寫 NFS 層的時候有種心慌慌摸不著底的情況了。