结合源码分析 Node.js 模块加载与运行原理


Node.js 的出现,让 JavaScript 脱离了浏览器的束缚,进入了广阔的服务端开发领域。而 Node.js 对 CommonJS 模块化规范的引入,则更是让 JavaScript成为了一门真正能够适应大型工程的语言。

在 Node.js 中使用模块非常简单,我们日常开发中几乎都有过这样的经历:写一段 JavaScript 代码,require 一些想要的包,然后将代码产物 exports 导出。但是,对于 Node.js 模块化背后的加载与运行原理,我们是否清楚呢。首先抛出以下几个问题:

  • Node.js 中的模块支持哪些文件类型?
  • 核心模块和第三方模块的加载运行流程有什么不同?
  • 除了 JavaScript 模块以外,怎样去写一个 C/C++ 扩展模块?
  • ……

本篇文章,就会结合 Node.js 源码,探究一下以上这些问题背后的答案。

1. Node.js 模块类型

在 Node.js 中,模块主要可以分为以下几种类型:

  • 核心模块:包含在 Node.js 源码中,被编译进 Node.js 可执行二进制文件 JavaScript 模块,也叫 native 模块,比如常用的 http, fs 等等
  • C/C++ 模块,也叫 built-in 模块,一般我们不直接调用,而是在 native module 中调用,然后我们再 require
  • native 模块,比如我们在 Node.js 中常用的 buffer,fs,os 等 native 模块,其底层都有调用 built-in 模块。
  • 第三方模块:非 Node.js 源码自带的模块都可以统称第三方模块,比如 express,webpack 等等。
    • JavaScript 模块,这是最常见的,我们开发的时候一般都写的是 JavaScript 模块
    • JSON 模块,这个很简单,就是一个 JSON 文件
    • C/C++ 扩展模块,使用 C/C++ 编写,编译之后后缀名为 .node

本篇文章中,我们会一一涉及到上述几种模块的加载、运行原理。

2. Node.js 源码结构一览

这里使用 Node.js 6.x 版本源码为例子来做分析。去 github 上下载相应版本的 Node.js 源码,可以看到代码大体结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
├── AUTHORS
├── BSDmakefile
├── BUILDING.md
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── COLLABORATOR_GUIDE.md
├── CONTRIBUTING.md
├── GOVERNANCE.md
├── LICENSE
├── Makefile
├── README.md
├── android-configure
├── benchmark
├── common.gypi
├── configure
├── deps
├── doc
├── lib
├── node.gyp
├── node.gypi
├── src
├── test
├── tools
└── vcbuild.bat

其中:

  • ./lib文件夹主要包含了各种 JavaScript 文件,我们常用的 JavaScript native 模块都在这里。
  • ./src文件夹主要包含了 Node.js 的 C/C++ 源码文件,其中很多 built-in 模块都在这里。
  • ./deps文件夹包含了 Node.js 依赖的各种库,典型的如 v8,libuv,zlib 等。

我们在开发中使用的 release 版本,其实就是从源码编译得到的可执行文件。如果我们想要对 Node.js 进行一些个性化的定制,则可以对源码进行修改,然后再运行编译,得到定制化的 Node.js 版本。这里以 Linux 平台为例,简要介绍一下 Node.js 编译流程。

首先,我们需要认识一下编译用到的组织工具,即 gyp。Node.js 源码中我们可以看到一个 node.gyp,这个