1.2 Node.js的包机制
在1.1节中笔者讲述了Node.js中以文件作为粒度的模块机制,接下来要讲的是一个或者多个模块结合起来的Node.js包机制。
1.2.1 CommonJS的包规范
CommonJS规范中的包(package)指的是一系列模块、代码以及其他资源文件的一个封装。它为CommonJS输出内容的交付、安装、管理等提供了一个方便的途径。
一个CommonJS包必然含有一个包描述文件,以及可选地包含了一些代码、资源等内容。本书中讲解的CommonJS规范为1.0规范。
1.包描述文件
一个遵循CommonJS规范的包必然包含一个包描述文件,并且它处于包根目录下,名为package.json。
在这个JSON文件中指定了这个包的一些必要信息,如表1-1所示。
表1-1 CommonJS包描述文件的必填字段
① 有关语义化版本规范的信息可以参阅http://semver.org/。
② 有关官方开源许可的更多信息可以参阅https://opensource.org/licenses/alphabetical。
③ 有关更复杂的依赖指定格式可参阅CommonJS的包规范Wiki:http://wiki.commonjs.org/wiki/Packages/1.0。
除了上述的必填字段之外,官方还指定了一些有用的选填字段,如表1-2所示。
表1-2 CommonJS包描述文件的选填字段
注意:遵循CommonJS规范的包管理器和运行程序会忽略包描述文件中的未知字段名(即未在上述规范中声明的字段),这些字段可以让开发者定义一些个性化的内容。
但是需要注意的是还有一些字段虽然未在上面列出,但是作为保留字段有可能会在未来被使用,开发者在定义个性化字段的时候应当避免使用这些保留字段。
保留字段如下:build、default、email、external、files、imports、maintainer、paths、platform、require、summary、test、using、downloads、uid、type。
下面给出一个简单的遵循CommonJS包规范的描述文件。
2.包格式
CommonJS包的格式是一个包含整个包目录(尤其是package.json文件)的ZIP格式压缩包,不过其有可能在未来版本的规范中有所不同。
3.包目录结构
一个遵循CommonJS规范的包目录特点如下:
· package.json在根目录下;
· 二进制文件应当在bin目录下;
· JavaScript源码应当在lib目录下;
· 文档应当在doc目录下;
· 单元测试文件应当在test目录下。
1.2.2 Node.js/NPM下的包
NPM在最开始的时候是Node.js Package Manager的缩写,即Node.js包管理器。后来因为前端也开始使用NPM进行包管理,所以它的意义就开始改变了,变成了JavaScript包管理器,其结构和原理也有了一些明显的改变。
1.包的路径和依赖
在1.1节中我们提到过,当Node.js在项目中执行require()并传入的是一个非路径类的字符串时,会按照层级在各node_modules文件夹下寻找相应的文件或者文件夹。
实际上Node.js的第三方包都放在该目录,而在项目根目录中会有一个package.json文件记录该项目会有哪些第三方包的依赖。
虽然大家用法各异,有人甚至在Git版本库中也会将该文件夹放进去,并且肆意修改node_modules目录下的源码,但实际上这样的方式是非常不值得推荐的。
通常的做法是在项目package.json中写上依赖信息,并在线上机器(或者部署机器)上再执行npm install进行依赖安装。如果有真的需要修改第三方库代码的,推荐在项目运行初始化或者合适的时机对它进行hack。
如果一个项目按照规范的写法去做,那么在项目目录下的node_modules目录被称为第三方包目录,也就是第三方开发者(也有可能是你自己)写的包会被安装到里面去。
2.NPM下的包描述文件
NPM中的项目目录和包的根目录下都需要有一个package.json文件来做一些定义。这里的package.json大体遵循CommonJS规范,但是与CommonJS的包规范相比,仍有比较多的不同之处。
注意,笔者在后面并不会特别强调Node.js项目的package.json文件与CommonJS规范相比有比较多的不同之处并加以解释。因为在实际应用中,Node.js在对包进行引入的时候对package.json中的相关信息依赖较少,反而是对node_modules这个目录更为依赖。开发者甚至只要在node_modules目录下新建一个文件或者文件夹,并于其中新建一个*.js的源码文件,如node_modules/foo/index.js,那么在项目中就可以直接通过require("foo")来引入这个模块了。
实际上,笔者在后面会强调的是package.json文件对于NPM使用的影响。由于Node.js开发中最常用的包管理系统就是NPM,因此经常在说明的时候会将两者等价起来。换而言之,如果哪天有人出了一个新的Node.js可以使用的包管理器,其需要其他样式的包描述文件,那么使用它管理包的时候就不一定再是本书中解说的package.json了。事实上Node.js中流行的包管理器的确不止NPM一种,就现在来说还有阿里巴巴Node.js生态中的CNPM、Facebook出品的Yarn,只不过就目前而言其包描述文件基本上与NPM一致。
这里讲讲NPM下的package.json与CommonJS规范下的package.json主要的几个不同点(对其部分字段进行比较),如表1-3所示。
表1-3 NPM中package.json与CommonJS包规范中package.json的部分字段比较
①@SCOPE/前缀主要用于包的命名空间,实现隔离效果。如大搜车公司内部有个私有包confbiu,那么我们就可以将这个包命名为@souche/confbiu。
其他更多的信息读者可以自行去NPM官网中查阅。
1.2.3 NPM与CNPM
本节对NPM与CNPM的分析,主要是针对它们的包安装目录结构的不同进行的。
由于本书主要写的内容是Node.js的原生依赖开发,因此下面给出一些针对Node.js开发者的建议,并简述了一些与前端开发依赖的区别。
1.NPM
NPM最初是由Isaac Z.Schlueter开发,用于进行Node.js的依赖包发布、管理和安装的。后来由于前端包管理系统Bower可能无法满足前端开发人员的一些需求,因此很多前端开发人员也开始用NPM管理前端的包依赖。不过,原来NPM的路径形式不是特别适合前端的开发,因此自从NPM 3开始,其安装形式就发生了一些变化。该变化除了为了适应突如其来的前端开发需求之外,还有一部分原因是为了解决Windows下开发时文件路径过长的问题。
(1) NPM 2
NPM的一个非常重要的分水岭在于版本2和版本3。
NPM 2的依赖安装路径是嵌套式(Nested)的,非常适合Node.js进行工具、后端的开发。
下面举个例子,我们的项目有一个依赖包bar的1.0.0版本依赖另一个包foo的^1.0.0版本,而我们项目的另一个依赖包baz的1.0.0版本依赖了foo的^1.1.0版本,那么通过NPM 2进行安装的包依赖目录就会如下所示:
其中bar和baz的代码里面执行require("foo")的时候分别引入的是不同的foo依赖副本,这样在它们中间就不会产生一些破坏性的关系。
举个简单的例子,如果bar的开发者觉得foo中有一个函数无法满足自己的需求,那么也许它会有这么一段代码:
这个时候如果使用NPM 2进行开发的话没有什么太大的问题,除非这个项目本身也引用了foo这个依赖,导致bar和baz指向的都是项目本身的foo路径。
但是如果使用NPM 3的话,代码会陷入一个不可预知的危险环境中。
所以通常来说,如果环境允许,并且你是一位Node.js开发者的话,还是推荐使用NPM 2进行包依赖,并且摈弃那些只支持NPM 3的包。当然,这种主观意愿还是因人而异的,这里只是笔者的一个建议而已。
(2) NPM 3+
这里的3+指的是NPM 3及其以上版本,并且有可能一直包括其未来的所有版本。
它的依赖安装模式是扁平化(Flatten)的,非常适合前端项目:需要优化到很小的代码空间占用量、打包的分析等;同时也解决了Windows中文件路径不允许过长的问题。
在NPM 3发布时的更新日志(Changelog)中我们能发现,它一再强调了扁平化这个特性。
你的依赖会被尽可能地扁平化。所有你的依赖、你依赖的依赖以及每一级的依赖都会被安装到你项目的根node_modules目录下,不再嵌套。只有在两个依赖存在冲突的情况下才会出现嵌套的情况。
也就是说它在解决省了一点点空间的问题、Windows安装路径过长的问题时,又带入了另一个冲突不可预知的问题。不过这种情况在前端开发中可以忽略不计,我们可以放心使用。并且在巨大的包量冲击下,一大拨新版本的依赖都开始使用了这个危险的特性,导致其开始不兼容NPM 2,使得很多Node.js开发者不得不转而使用NPM 3。
比如你在使用NPM 2安装一个依赖的时候,由于其依赖的依赖不会被安装到根node_modules下,导致其有些地方在执行require的时候找不到相应的包。
还有一个问题就是当你安装了一个项目的依赖的时候,会出现一个比较尴尬的情景:明明你自己项目的依赖可能只有十几个,但是在你的node_modules目录里很有可能会出现一两千个文件夹。这对于可能需要经常打开node_modules来调试一些依赖包的代码的开发者来说无疑是一种折磨。
不过这也许只是笔者个人的感受,如果读者对上述的一些问题着实无感的话,可以忽略上面的一些建议。
2.CNPM
CNPM是阿里巴巴集团的Node.js团队开发的一套安装更快的NPM工具,其Logo如图1-4所示。它由苏千和死马主导,内核使用了其自主研发的npminstall。
图1-4 CNPM的Logo
就算不使用国内的镜像,CNPM的依赖安装速度仍远远大于NPM。它的工作原理是,将一些包缓存到node_modules/.npminstall目录下,再以符号链接(Symbol Link)的形式将依赖目录连接到其对应的路径。这样相同版本的包在安装过程中实际上只有一份实体。
对于CNPM来说,除了上面所说的不同于NPM的点之外,它的分水岭在于CNPM 4.2以及4.3+。
在CNPM 4.2的版本里面,除去符号链接的特性外,它的目录结构与NPM 2一致,是嵌套式的。
后来由于NPM 3的影响实在太大,导致很多前端依赖以及部分Node.js依赖在NPM 2下无法正常工作,因此CNPM在4.3+的版本中为前端开发者加入了扁平化的支持。
不过为了使CNPM不仅仅服务于前端开发者,CNPM还结合了嵌套式依赖的特性。也就是说它除了完完全全按照CNPM 4.2版本的嵌套式目录结构安装了依赖之外,还会顺便将一些通过计算得到的所有依赖以及所有依赖的依赖的特定版本文件放了一份到node_modules目录下。这既解决了前端开发者的需求,又保留了原始Node.js开发者的需求。
不过,node_modules下目录成群的尴尬情景就成了无法解决的死结。
1.2.4 小结
本节主要讲了CommonJS规范下包的定义、入口文件和目录结构,以及其与NPM包管理程序的异同。Node.js主要使用NPM的一套规范来使用第三方依赖。
在1.2.3节中讲述了NPM 2与NPM 3的异同,并阐述了两个不同版本针对Node.js开发和前端开发的友好度的异同。这里推荐了既满足Node.js开发者又满足前端开发者需求的CNPM。
这在之后的Node.js原生依赖开发中会向开发者提供一些目录结构上的引导,并且希望读者自己写的依赖同时依赖NPM 2和NPM 3。不要过于依赖其嵌套式或者扁平化的特性,以致其在某些情况下出现不能使用的情景。
1.2.5 参考资料
[1]Packages/1.0:http://wiki.commonjs.org/wiki/Packages/1.0.
[2]package.json,Specifics of npm's package.json handling:https://docs.npmjs.com/files/package.json.
[3]Force npm 3 to install a node module always nested:https://github.com/npm/npm/issues/9809.
[4]npminstall:https://github.com/cnpm/npminstall.