跟我一起部署和定制 CNPM自定义包存储层 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
XadillaX
V2EX    Node.js

跟我一起部署和定制 CNPM自定义包存储层

  •  
  •   XadillaX 2016-07-22 17:12:43 +08:00 4427 次点击
    这是一个创建于 3367 天前的主题,其中的信息可能已经有所发展或是发生改变。

    原文链接: https://xcoder.in/2016/07/22/lets-cnpm-storage/

    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:待上传文件的 Buffer
      • options
        • 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 里面我们看到它 newali-oss 对象。

    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 上的地址。

    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; 

    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 函数:

    function trimKey(key) { return '_snpm_/' + (key ? key.replace(/^\//, '') : ''); } 

    这下所有在我们内部 CNPM 里面的包的链接都多了个 _snpm_/ 的前缀了。

    CNPM 调用解析

    上面解析了接口之后,我们来扒一扒什么时候会调用上面实现的接口们吧,这样就知道 CNPM 对于 NFS 使用的工作原理了。

    controllers/registry/package/download.js

    源码参考

    对于包下载来说,它的路由是:

    /{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 里面传回去。

    controllers/registry/package/remove.js

    源码参考

    对于删除包来说,除了把包从数据库删掉之外,还要循环遍历一遍这个包的所有版本,把所有版本的这个包都从 NFS 里面删除。

    try { yield keys.map(function (key) { return nfs.remove(key); }); } catch (err) { logger.error(err); } 

    这里就调用了你事先写好的 remove 了。当然你不实现也没关系,最多是包的压缩文件不删除而已。

    controllers/registry/package/remove_version.js

    源码参考

    这里跟上一小节差不多,之前是删除整个包,这里是删除包的某一个版本,所以就不用循环删除了。

    try { yield nfs.remove(key); } catch (err) { logger.error(err); } 

    controllers/registry/package/save.js

    源码参考

    然后就是用户 $ 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 。

    controller/sync_module_worker.js

    源码参考

    这个文件是从源端同步相关的一些逻辑了,这里面有两个操作。

    一个是 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 层的时候有种心慌慌摸不着底的情况了。

    目前尚无回复
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     1052 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 26ms UTC 18:24 PVG 02:24 LAX 11:24 JFK 14:24
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86