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 |