Life Will Change Lyn
  1. 1 Life Will Change Lyn
  2. 2 Libertus Chen-U
  3. 3 Flower Of Life 发热巫女
  4. 4 Hypocrite Nush
  5. 5 Time Bomb Veela
  6. 6 Last Surprise Lyn
  7. 7 Warcry mpi
  8. 8 かかってこいよ NakamuraEmi
  9. 9 BREAK IN TO BREAK OUT Lyn
  10. 10 One Last You Jen Bird
2018-07-22 21:46:12

Node的模块与包管理机制

平时前端虽然没什么机会用Node写后端业务,但在前端工程化中,一定离不开Node的模块管理。最近计划从Node开始学后端相关的一些思想,第一篇文章就从距离前端最近的点开始。

CommonJS

Node的模块系统实现基于该规范之上,正式进入Node模块系统之前有必要先对该规范的模块相关做一个简单介绍,使用上分为如下三个部分。

模块引用

通过上下文提供的require方法将一个模块引入到当前上下文中。

模块标识

既然是通过require引入一个特定模块,当然需要一个参数分辨模块,该参数就是模块标识。规范推荐该标识由小驼峰字符串命名,或者传入路径字符串去查询。

模块定义

既然已经知道了如何引用模块,那么该模块的具体结构肯定要有对应的约束。具体来讲,每个文件都是一个模块,其内部实现了一个module对象来记录该模块相关的信息,而其中的exports属性就代表着该模块需要被导出的内容。

可以看到如果不考虑原理只考虑使用的话相当简单,require实现入口,exports实现出口,module实现交流。Node的模块实现就是基于该原则,但并没有完全照搬,而是在此基础上根据自身需要进行一定的改造。下面就来具体分析Node的模块机制。

Node模块机制

首先需要知道,Node中的模块可以分为如下三类,根据模块类型不同,其使用方式也有差别。

  • 核心模块
  • 自定义模块
  • 文件模块

核心模块是Node本身就提供的模块,会在Node源码编译时就编译为二进制执行文件并在Node进程启动时加载进内存。

自定义模块和文件模块从加载形式上可以分为一类,都是在运行时动态加载,区别在于文件查找的策略不同。下面就一步步分析,一个模块从引入到执行究竟发生了什么。

缓存检查

首先需要知道Node对引入过的模块都会缓存,并且缓存的优先级最高,所以每一次引入模块时都会首先检查缓存,如果存在则直接加载。如果不存在,则进入以下模块定位步骤。

模块标识分析与定位

之前也提到过引入特定模块是从模块标识开始的,根据该标识来定位模块的具体位置。针对不同模块类型,定位策略也有区别,具体如下:

  • 核心模块:由于在进程启动时已经加载进内存,必然已获取定位,并且加载速度理应是最快的。
  • 自定义模块:该类模块和核心模块相似的没有路径,可能是一个文件或是包,则定位依据引入该自定义模块的模块路径执行。该值为module.paths,其生成规则为当前文件目录下的node_modules目录并递归向上形成数组。可以看到,查找速度跟当前模块的路径深度相关,自定义模块的加载速度也是相对最慢的。
  • 文件模块:该类模块的模块标识会带上绝对路径或相对路径,则定位会先将其转为真实路径并以此为索引去查找。由于获取了直接位置,加载速度会快于自定义模块。

除了以上的查找策略外,Node还提供了一些细节功能如下:

  • 拓展名分析:模块标识通常是不带拓展名的,Node会按照js,json,node的次序依次补全文件名再进入定位步骤。
  • 目录分析:定位步骤结束后可能没有得到文件,但是得到了一个同名的目录,则Node会将该目录当做模块包处理。具体来说就是进入该目录查询package.json并根据main属性查找文件,如果main属性指定错误或者没有package.json则查找index文件。

模块编译

定位到文件后就进入到模块的编译阶段。根据文件类型的不同,也会采取不同的策略。

JS或其他文件

获取文件内容并通过闭包进行头尾包装,具体如下:

(function(exports, require, module, __filename, __dirname) {
    // ...具体内容
});

然后Node将包装后的代码传入对应参数并执行,再将exports返回给调用模块,这样就做到了暴露exports并保持其余属性不可用。

C/C++文件

该类拓展文件以.node结尾,Node调用process.dlopen执行,这里就不具体分析了,因为我也不懂C/ C++。

JSON文件

Node使用fs模块读取文件内容后再通过JSON.parse得到对象并赋值给exports供模块使用。

至此就完成了Node对CommonJS模块规范的实现。但需要注意这部分主要是针对自定义模块和文件模块的实现,而核心模块需要经过被编译成二进制文件的过程,这部分内容涉及到C/C++相关,就不在这里记录了。

Node包管理机制

在Node的模块机制之下,已经可以实现文件与文件间的交流。但大多数时候,我们需要由多个模块结合实现一些功能并形成的一个可供调用的大模块。这个大模块也就是现在称的三方包了,Node包管理机制也是基于CommonJS实现的,其定义也很简单,分为包结构和包描述。

包结构

一个包是由多个文件组成的,为了便于外部调用,规范推荐目录组织形式如下。

  • package.json: 包描述
  • bin: 存放可执行二进制文件
  • lib: 存放JS代码
  • doc: 存放文档
  • test: 存放测试

当然你也可以不按照该推荐组织代码,但遵守规则更容易让使用者理解包的详细内容并放心使用。

包描述

这部分内容和前端平时打交道就比较多了,实际上就是package.json的内部组织形式,其中每个字段都用来描述该包的信息。大多数前端项目的起手也是从package.json开始的。一个包开发完成之后就可以通过npm管理其发布了,npm前端也用的非常多了就不详细介绍了。

小结

Node的模块管理非常简单,但其意义却是相当重要的。其不仅补全了ES6之前JS本身缺乏模块管理的问题,还通过包管理规范提供一个管理三方模块的平台充分借助开源力量迅速发展,还为前端的工程化管理打下坚实基础,现在甚至可以说后端可以没有Node,但前端不能没有。

-- EOF --

添加在分类「 前端开发 」下,并被添加 「Node.js」「工程化」 标签。