影子的知识库

影子的知识库

  • 知识库
  • GitHub

›Node 基础

JVM系列

  • JVM内存区域
  • 对象创建-布局-访问
  • 内存溢出实战
  • 内存区域回收
  • 四大引用
  • 垃圾回收算法
  • HotSpot回收算法细节

Java系列

  • java注解
  • springboot请求参数绑定
  • springboot请求参数校验框架
  • YAML语法
  • 动态代理
  • classpath和java命令
  • springboot-aop编程
  • springboot统一异常处理
  • springboot数据库和事务
  • springboot拦截器
  • springboot中的web配置
  • docker的简单开发
  • springboot自动配置
  • 数据库的隔离级别
  • springboot监控
  • java类加载
  • java-agent的相关内容
  • 类加载器详解
  • java的SecurityManager
  • maven学习

Node

    JS 基础

    • 语法基础和数据类型
    • 数据类型转换
    • 语句 表达式 运算符
    • 变量与对象
    • 函数
    • 数据处理
    • 常用 API
    • 重点知识

    ES6

    • 块级作用域
    • 字符串和正则表达式
    • 函数
    • 对象
    • Symbol
    • Set和Map
    • 迭代器和生成器
    • 类
    • 数组
    • Promise

    Node 基础

    • 模块系统
    • package.json
    • 内置对象
    • npm脚本的使用
    • Buffer
    • Stream
    • 事件循环机制
    • 示例代码

    stream系列

    • 流的缓冲
    • 可读流
    • 可写流
    • 双工流和转换流
    • 自定义流

后期计划

  • 学习计划
  • 专题研究计划
Edit

本文内容

本文主要记录 nodeJS 中的模块系统,让我们对 node 的运行有更清晰的认识

主要参考:

  • 结合源码分析 Node.js 模块加载与运行原理
  • require() 源码解读

模块

  • 每个文件都被视为一个独立的模块

  • 假设我们有两个文件 hello.js 和 world.js,内容如下:

    // world.js
    function world() {
        return 'world';
    }
    exports.world = world;
    console.log('rerere');
    
    // hello.js
    var world = require('./world');
    console.log(world.world());
    
    // node hello.js 后,输出
    // rerere
    // world
    
  • node xxx.js 最终调用了 Module._load,可以认为实际上等同于在 JS 里执行的 require(xxx.js),因此我们上面的输出有两行,其中一行是执行了 world.js 里的一条输出语句后输出的。

require

在执行模块代码之前,Node.js 会使用一个如下的模块封装器将其封装:

(function(exports, require, module, __filename, __dirname) {
// 模块的代码实际上在这里
});

可以看到如上有 5 个变量,在我们的模块代码里都是可以用这 5 个变量的,这些都是 node 的模块加载时注入进来的对象,下面我们来分析一下 require 的过程。

Module 源码

require 定义:

Module.prototype.require = function(path) {
  assert(path, 'missing path');
  assert(util.isString(path), 'path must be a string');
  return Module._load(path, this);
};

可以看到 require 是 Module 原型上的一个属性,最终调用了 Module._load(path, this);,

Module 定义:

function Module(id, parent) {
  this.id = id;
  this.exports = {};
  this.parent = parent;
  if (parent && parent.children) {
    parent.children.push(this);
  }

  this.filename = null;
  this.loaded = false;
  this.children = [];
}

Module._load 定义:

Module._load = function(request, parent, isMain) {
  if (parent) {
    debug('Module._load REQUEST  ' + (request) + ' parent: ' + parent.id);
  }

  var filename = Module._resolveFilename(request, parent);

  var cachedModule = Module._cache[filename];
  if (cachedModule) {
    return cachedModule.exports;
  }

  if (NativeModule.exists(filename)) {
    // REPL is a special case, because it needs the real require.
    if (filename == 'repl') {
      var replModule = new Module('repl');
      replModule._compile(NativeModule.getSource('repl'), 'repl.js');
      NativeModule._cache.repl = replModule;
      return replModule.exports;
    }

    debug('load native module ' + request);
    return NativeModule.require(filename);
  }

  var module = new Module(filename, parent);

  if (isMain) {
    process.mainModule = module;
    module.id = '.';
  }

  Module._cache[filename] = module;

  var hadException = true;

  try {
    module.load(filename);
    hadException = false;
  } finally {
    if (hadException) {
      delete Module._cache[filename];
    }
  }

  return module.exports;
};
  • 接收 3 个参数,分别是:模块路径(相对或者绝对路径等后续可以解析到文件具体位置的一个路径)、模块的父模块、是否主模块(bool值,我们 node xxx.js 时,xxx.js 就是主模块)
  • var module = new Module(filename, parent);:加载模块时,为我们生成一个模块对象,这个模块对象就是我们 require('模块名称') 的那个模块
  • 最后返回 module.exports 也就是模块对象的 exports 属性
  • 加载模块逻辑在 module.load(filename)

module.load 定义:

Module.prototype.load = function(filename) {
  debug('load %j for module %j', filename, this.id);

  assert(!this.loaded);
  this.filename = filename;
  this.paths = Module._nodeModulePaths(path.dirname(filename));

  const extension = findLongestRegisteredExtension(filename);
  // allow .mjs to be overridden
  if (filename.endsWith('.mjs') && !Module._extensions['.mjs']) {
    throw new ERR_REQUIRE_ESM(filename);
  }
  Module._extensions[extension](this, filename);
  this.loaded = true;

  const ESMLoader = asyncESM.ESMLoader;
  const url = `${pathToFileURL(filename)}`;
  const module = ESMLoader.moduleMap.get(url);
  // Create module entry at load time to snapshot exports correctly
  const exports = this.exports;
  // Called from cjs translator
  if (module !== undefined && module.module !== undefined) {
    if (module.module.getStatus() >= kInstantiated)
      module.module.setExport('default', exports);
  } else {
    // Preemptively cache
    // We use a function to defer promise creation for async hooks.
    ESMLoader.moduleMap.set(
      url,
      // Module job creation will start promises.
      // We make it a function to lazily trigger those promises
      // for async hooks compatibility.
      () => new ModuleJob(ESMLoader, url, () =>
        new ModuleWrap(url, undefined, ['default'], function() {
          this.setExport('default', exports);
        })
      , false /* isMain */, false /* inspectBrk */)
    );
  }
};
  • 加载的关键:Module._extensions[extension](this, filename);,根据扩展名来决定加载的逻辑,这里我们只看 js 的逻辑

js 加载逻辑:

Module._extensions['.js'] = function(module, filename) {
  if (filename.endsWith('.js')) {
    const pkg = readPackageScope(filename);
    if (pkg && pkg.data && pkg.data.type === 'module') {
      if (warnRequireESM) {
        const parentPath = module.parent && module.parent.filename;
        const basename = parentPath &&
            path.basename(filename) === path.basename(parentPath) ?
          filename : path.basename(filename);
        process.emitWarning(
          'require() of ES modules is not supported.\nrequire() of ' +
          `${filename} ${parentPath ? `from ${module.parent.filename} ` : ''}` +
          'is an ES module file as it is a .js file whose nearest parent ' +
          'package.json contains "type": "module" which defines all .js ' +
          'files in that package scope as ES modules.\nInstead rename ' +
          `${basename} to end in .cjs, change the requiring code to use ` +
          'import(), or remove "type": "module" from ' +
          `${path.resolve(pkg.path, 'package.json')}.`,
          undefined,
          undefined,
          undefined,
          true
        );
        warnRequireESM = false;
      }
      throw new ERR_REQUIRE_ESM(filename);
    }
  }
  const content = fs.readFileSync(filename, 'utf8');
  module._compile(content, filename);
};
  • 关键点:module._compile(content, filename);

module._compile 逻辑:

Module.prototype._compile = function(content, filename) {
  let moduleURL;
  let redirects;
  if (manifest) {
    moduleURL = pathToFileURL(filename);
    redirects = manifest.getRedirector(moduleURL);
    manifest.assertIntegrity(moduleURL, content);
  }

  maybeCacheSourceMap(filename, content, this);
  const compiledWrapper = wrapSafe(filename, content, this);

  let inspectorWrapper = null;
  if (getOptionValue('--inspect-brk') && process._eval == null) {
    if (!resolvedArgv) {
      // We enter the repl if we're not given a filename argument.
      if (process.argv[1]) {
        resolvedArgv = Module._resolveFilename(process.argv[1], null, false);
      } else {
        resolvedArgv = 'repl';
      }
    }

    // Set breakpoint on module start
    if (!hasPausedEntry && filename === resolvedArgv) {
      hasPausedEntry = true;
      inspectorWrapper = internalBinding('inspector').callAndPauseOnStart;
    }
  }
  const dirname = path.dirname(filename);
  const require = makeRequireFunction(this, redirects);
  let result;
  const exports = this.exports;
  const thisValue = exports;
  const module = this;
  if (requireDepth === 0) statCache = new Map();
  if (inspectorWrapper) {
    result = inspectorWrapper(compiledWrapper, thisValue, exports,
                              require, module, filename, dirname);
  } else {
    result = compiledWrapper.call(thisValue, exports, require, module,
                                  filename, dirname);
  }
  hasLoadedAnyUserCJSModule = true;
  if (requireDepth === 0) statCache = null;
  return result;
};
  • 关键点:result = inspectorWrapper(compiledWrapper, thisValue, exports, require, module, filename, dirname);
  • 上面那行就是模块封装器的过程。

模块具有的 5 个变量

  • exports:从 module._compile 逻辑中,有一行是 exports = this.exports;,而我们是使用这里的 this 指向我们要加载的模块 module,因此:exports = module.exports
  • require:传入的 require 函数(更深入的没有研究,实际上并不只是 Module.prototype.require)
  • module:模块对象本身
  • __filename:文件名(绝对路径)
  • __dirname:模块的父目录

以上属性在每个模块 js 文件中都可以访问

require 返回的就是该 module 的 module.exports 对象,每个模块的 exports 只是 module.exports 对象的一个引用。

加载 json

一个 json 就是一个对象,可以使用 require 直接加载为 node 的一个对象,例如:

# package.json
{
  "name": "node_start",
  "version": "1.0.0",
  "description": "",
  "main": "hello.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

var package = require('./package.json');
console.log(package);
// 输出如下
(py3.5) czp@:~/workspace/knowledge-base/demos/node_start$ node hello.js
{
  name: 'node_start',
  version: '1.0.0',
  description: '',
  main: 'hello.js',
  scripts: { test: 'echo "Error: no test specified" && exit 1' },
  author: '',
  license: 'ISC'
}

加载其它资源

实际上还可以加载 .node 等,我目前还不打算深入这一块儿,总之 require 是可以加载很多资源的,我们也可以扩展,就像上面分析到的 module.load 是可以根据扩展来决定不同的加载逻辑的,所以我们是可以补充扩展和修改扩展逻辑的,例如 vue 应该就是使用了这种方式(没有真正研究过,猜的)。

模块的加载位置和顺序

参考:npm 模块安装机制简介

如果我们加载文件模块的时候,没有找到对应名称的文件,node 会去尝试加载添加了后缀名的文件:.js .json .node。(也就是说,如果我们加载一个没有后缀名的文件,会首先当做 JS 来加载)

  • js:使用前文说的 js 加载逻辑,最后返回的是模块的 exports 对象。(module.exports)
  • json:将这个 json 文件当做一个 js 对象进行返回
  • node:预编译的 node 模块

文件路径:

  • require('/home/marco/foo.js'):加载的是绝对路径
  • require('./circle'):加载的是相对路径(相对于执行这个 require 的文件)
  • 没有 / ./ ../ 这些前缀的,要么加载的是核心模块,要么模块处于 node_modules 目录下面,会首先按照核心模块去加载,然后去 node_modules 去加载

node_modules 优先级:

  • 应用程序目录内的 node_modules
  • 父目录的 node_modules
  • 继续找父目录的 node_modules,直到根目录
  • 最后在全局安装的模块下寻找(npm get prefix 获取到的路径的 lib 目录下)

可以看出,当前目录的优先级最高,全局安装的优先级最低

目录路径:

加载一个模块的时候,如果提供的是目录,那么:

  • 找到该目录的 package.json,如果不存在,就去加载 index.js ,不存在就去加载 index.node,如果都没有这些文件,那么就加载失败,抛出异常
  • 如果有 package.json,那么就加载里面 main 属性定义的模块,如果没有定义 main,那么默认是 index.js

npm install

  • npm install:将 devdependencies 和 dependencies 的包安装到当前目录下的 node_modules 目录中。

  • npm install -g:等价于 npm install --global 将当前包(也就是运行这个命令所在的包),安装到全局模块中,全局模块的路径是:

    (py3.5) czp@:~/workspace/knowledge-base/demos/node_start$ npm get prefix
    /usr/local
    因此全局模块路径是 /usr/local/lib
    
  • npm install --production:等价于 NODE_ENV=production npm install,添加了 --production 或者 NODE_ENV 环境变量为 production,则不会下载 devdependencies 的依赖了。

  • 参数:

    • -P, --save-prod:这个是默认行为,安装的内容会加入到 dependencies
    • -D, --save-dev:安装内容会加到 devDependencies
    • -O, --save-optional:安装内容会加到 optionalDependencies
    • --no-save:只是安装到 node_modules 但是不加入到 package.json 了

过程:

  1. 发出npm install命令
  2. npm 向 registry 查询模块压缩包的网址
  3. 下载压缩包,存放在~/.npm目录
  4. 解压压缩包到当前项目的node_modules目录
Last updated on 11/8/2020
← Promisepackage.json →
  • 模块
  • require
  • 模块具有的 5 个变量
  • 加载 json
  • 加载其它资源
  • 模块的加载位置和顺序
    • 文件路径:
    • node_modules 优先级:
    • 目录路径:
    • npm install
影子的知识库
Docs
Getting Started (or other categories)Guides (or other categories)API Reference (or other categories)
Community
User ShowcaseStack OverflowProject ChatTwitter
More
BlogGitHub
Copyright © 2020 Cen ZhiPeng