原文連接:xcoder.in/2016/07/22/…javascript
CNPM 的自定義包存儲層文件系統簡稱 NFS,我猜是 NPM File System 的意思。java
在以前《跟我一塊兒部署和定製 CNPM——基礎部署》中提到過,CNPM 配置項裏面有一項配置 nfs
,它所對應的是一個 NFS 對象。node
在同步 package 的時候,CNPM 會把源站的包下載到本地,而後傳給 NFS 對象相應的函數交予去處理,由 NFS 對象返回處理結束以後該包在咱們本身部署的 CNPM 對應的包下載連接。git
上面的這一套流程就給咱們自定義包存儲提供了可能,好比咱們能夠把包同步到又拍雲存儲、阿里雲 OSS 等地方去,也能夠以二進制的形式存入咱們本身的數據庫(不推薦),甚至能夠什麼都不用作直接放在本地,而後把本地文件對外網暴露便可。github
NFS 的接口是實現定義好的,咱們若是要寫一個本身的 NFS 類,只須要按照約定的接口實現他們的邏輯便可。數據庫
雖然我本身不喜歡,可是 NFS 的全部函數須要在菊花函數中被實現。npm
下面給出接口的定義:xcode
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
: 文件標識這裏拿出一個 NFS 的官方實現阿里雲 OSS 版來做爲解析。它的 Repo 是 github.com/cnpm/oss-cn…。app
打開 index.js 咱們能看到,的確 OssWrapper
實現了上面的一些接口。函數
在 function OssWrapper
裏面咱們看到它 new
了 ali-oss 對象。
if (options.cluster) {
options.schedule = options.schedule || 'masterSlave';
this.client = new oss.ClusterClient(options);
} else {
this.client = oss(options);
}複製代碼
也就是說在各類上傳等函數裏面都是以這個 client
爲主體作的事情的。
首先咱們看看 upload
函數,從外部傳進來文件的 key
,NFS 對象將該文件以 key
爲名傳到 OSS 去,並返回該文件上傳以後在 OSS 上的地址。
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
這個函數直接拿過來就能用了,因而就有了:
proto.uploadBuffer = proto.upload;複製代碼
這兩個函數實際上也是直接調用了 ali-oss
的函數,並無什麼好講的,你們本身看看就行了。
這個函數無非就是判斷下有沒有自定義的 CDN 域名什麼的,根據不一樣的返回不一樣的網址而已。
把 key
裏面帶的最前面的斜槓去掉。
上面一節解析了 oss-cnpm
這個包的代碼,若是官方出的幾個 NFS 包不能知足,你們也能本身去寫一個 CNPM 存儲層的包了。
咱們公司的包是直接在 OSS 上面的,因此用 oss-cnpm
並無什麼不妥。
不過對於阿里系自己的公司門來講,OSS 並非什麼大事兒,對於咱們來講,OSS 的 bucket 資源仍是蠻稀缺的,上次就達到上限了。因此咱們目前的 NPM 包跟公司別的測試業務用的是同一個 bucket。
那麼問題來了:
oss-cnpm
直接把全部文件放在根目錄下建文件夾,太亂了,並且的確是有小可能衝突的。而這個包又不能讓人自定義前綴什麼什麼的。
因而我就本身 Fork 小小改裝了一下這個包,讓它適合咱們公司本身。
改裝很簡單,在上傳的目錄中加一個文件夾前綴。
動的是 trimKey
函數:
function trimKey(key) {
return '_snpm_/' + (key ? key.replace(/^\//, '') : '');
}複製代碼
這下全部在咱們內部 CNPM 裏面的包的連接都多了個 _snpm_/
的前綴了。
上面解析了接口以後,咱們來扒一扒何時會調用上面實現的接口們吧,這樣就知道 CNPM 對於 NFS 使用的工做原理了。
對於包下載來講,它的路由是:
/{package}/download/{package}-{version}.tgz複製代碼
而後在裏面判斷一下若是 NFS 對象有實現 url()
函數的話,先用 url()
函數生成對該包而言的真實下載連接。
讀出這個包的 registry 信息,裏面若是沒有 dist
等參數的話直接 302 到剛生成的地址去。
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 到真實下載連接去。
if (config.downloadRedirectToNFS && url) {
this.status = 302;
this.set('Location', url);
return;
}複製代碼
不過若是自己 registry 裏面就沒 key
這個選項的話也會直接用 url()
生成的連接給跳過去。若是沒有 url()
的連接,那麼直接用 registry 裏面的 tarball
字段。
var dist = row.package.dist;
if (!dist.key) {
url = url || dist.tarball;
this.status = 302;
this.set('Location', url);
return;
}複製代碼
上面若是都跳過去了,那麼說明要開始調用事先寫好的 download
那兩個函數了,把文件讀到 Buffer 裏面,而後把 Buffer 放到 Response 裏面傳回去。
對於刪除包來講,除了把包從數據庫刪掉以外,還要循環遍歷一遍這個包的全部版本,把全部版本的這個包都從 NFS 裏面刪除。
try {
yield keys.map(function (key) {
return nfs.remove(key);
});
} catch (err) {
logger.error(err);
}複製代碼
這裏就調用了你事先寫好的 remove
了。固然你不實現也不要緊,最可能是包的壓縮文件不刪除而已。
這裏跟上一小節差很少,以前是刪除整個包,這裏是刪除包的某一個版本,因此就不用循環刪除了。
try {
yield nfs.remove(key);
} catch (err) {
logger.error(err);
}複製代碼
而後就是用戶 $ npm publish
用的路由了,在一堆判斷以後,發佈傳過來的包被放在二進制 Buffer 內存裏面:
var tarballBuffer;
tarballBuffer = new Buffer(attachment.data, 'base64');複製代碼
接下去又判斷來判斷去,最後交由 NFS 的 uploadBuffer
來上傳並獲得結果。
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。
這個文件是從源端同步相關的一些邏輯了,這裏面有兩個操做。
一個是 unpublish
,調用的就是 NFS 的 remove
,不做詳談了。
另外一個就是同步了。同步包會被打散成同步一個版本,而後把每一個版本同步過來。在同步版本的時候先把包文件下載到本地文件 filepath
裏面去。
var r = yield urllib.request(downurl, options);複製代碼
urllib 是蘇千死馬他們本身寫的比較方便和適合他們本身的一個 http 請求庫。
上面的代碼 options
裏面有一個文件流,連接到 filepath
目錄的這個文件去,至關於這一步就是把源端的包下載到本地 filepath
去了。
通過一堆 blahblah 的判斷(好比 SHASUM)以後,這個這個函數就會調用 NFS 的 upload
函數將本地文件名對應的文件上傳到你所須要的地方去了。
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 層的時候有種心慌慌摸不着底的狀況了。