Node 中如何引入一个模块及其细节

栏目: IT技术 · 发布时间: 5年前

内容简介:在虽然它们在平常使用中仅仅是引入与导出模块,但稍稍深入,便可见乾坤之大。在业界可用它们做一些比较 trick 的事情,虽然我不大建议使用这些黑科技,但稍微了解还是很有必要。当我们使用

本文收录于 GitHub 山月行博客: shfshanyue/blog ,内含我在实际工作中碰到的问题、关于业务的思考及在全栈方向上的学习

node 环境中,有两个内置的全局变量无需引入即可直接使用,并且无处不见,它们构成了 nodejs 的模块体系: modulerequire 。以下是一个简单的示例

const fs = require('fs')

const add = (x, y) => x + y

module.exports = add

虽然它们在平常使用中仅仅是引入与导出模块,但稍稍深入,便可见乾坤之大。在业界可用它们做一些比较 trick 的事情,虽然我不大建议使用这些黑科技,但稍微了解还是很有必要。

  1. 如何在不重启应用时热加载模块?如 require 一个 json 文件时会产生缓存,但是重写文件时如何 watch
  2. 如何通过不侵入代码进行打印日志
  3. 循环引用会产生什么问题?

module wrapper

当我们使用 node 中写一个模块时,实际上该模块被一个函数包裹,如下所示:

(function(exports, require, module, __filename, __dirname) {
  // 所有的模块代码都被包裹在这个函数中
  const fs = require('fs')

  const add = (x, y) => x + y

  module.exports = add
});

因此在一个模块中自动会注入以下变量:

exports
require
module
__filename
__dirname

Node 中如何引入一个模块及其细节

module

调试最好的办法就是打印,我们想知道 module 是何方神圣,那就把它打印出来!

const fs = require('fs')

const add = (x, y) => x + y

module.exports = add

console.log(module)

Node 中如何引入一个模块及其细节

  • module.id : 如果是 . 代表是入口模块,否则是模块所在的文件名,可见如下的 koa
  • module.exports : 模块的导出

Node 中如何引入一个模块及其细节

module.exports 与 exports

module.exportsexports 有什么关系?

从以下源码中可以看到 module wrapper 的调用方 module._compile 是如何注入内置变量的,因此根据源码很容易理解一个模块中的变量:

  • exports : 实际上是 module.exports 的引用
  • require : 大多情况下是 Module.prototype.require
  • module
  • __filename
  • __dirname : path.dirname(__filename)
// <node_internals>/internal/modules/cjs/loader.js:1138

Module.prototype._compile = function(content, filename) {
  // ...
  const dirname = path.dirname(filename);
  const require = makeRequireFunction(this, redirects);
  let result;

  // 从中可以看出:exports = module.exports
  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);
  }
  // ...
}

require

通过 node 的 REPL 控制台,或者在 VSCode 中输出 require 进行调试,可以发现 require 是一个极其复杂的对象

Node 中如何引入一个模块及其细节

从以上 module wrapper 的源码中也可以看出 requiremakeRequireFunction 函数生成,如下

// <node_internals>/internal/modules/cjs/helpers.js:33

function makeRequireFunction(mod, redirects) {
  const Module = mod.constructor;

  let require;
  if (redirects) {
    // ...
  } else {
    // require 实际上是 Module.prototype.require
    require = function require(path) {
      return mod.require(path);
    };
  }

  function resolve(request, options) { // ... }

  require.resolve = resolve;

  function paths(request) {
    validateString(request, 'request');
    return Module._resolveLookupPaths(request, mod);
  }

  resolve.paths = paths;

  require.main = process.mainModule;

  // Enable support to add extra extension types.
  require.extensions = Module._extensions;

  require.cache = Module._cache;

  return require;
}

关于 require 更详细的信息可以去参考官方文档: Node API: require

require(id)

require 函数被用作引入一个模块,也是平常最常见最常用到的函数

// <node_internals>/internal/modules/cjs/loader.js:1019

Module.prototype.require = function(id) {
  validateString(id, 'id');
  if (id === '') {
    throw new ERR_INVALID_ARG_VALUE('id', id,
                                    'must be a non-empty string');
  }
  requireDepth++;
  try {
    return Module._load(id, this, /* isMain */ false);
  } finally {
    requireDepth--;
  }
}

require 引入一个模块时,实际上通过 Module._load 载入,大致的总结如下:

  1. 如果 Module._cache 命中模块缓存,则直接取出 module.exports ,加载结束
  2. 如果是 NativeModule ,则 loadNativeModule 加载模块,如 fshttppath 等模块,加载结束
  3. 否则,使用 Module.load 加载模块,当然这个步骤也很长,下一章节再细讲
// <node_internals>/internal/modules/cjs/loader.js:879

Module._load = function(request, parent, isMain) {
  let relResolveCacheIdentifier;
  if (parent) {
    // ...
  }

  const filename = Module._resolveFilename(request, parent, isMain);

  const cachedModule = Module._cache[filename];

  // 如果命中缓存,直接取缓存
  if (cachedModule !== undefined) {
    updateChildren(parent, cachedModule, true);
    return cachedModule.exports;
  }

  // 如果是 NativeModule,加载它
  const mod = loadNativeModule(filename, request);
  if (mod && mod.canBeRequiredByUsers) return mod.exports;

  // Don't call updateChildren(), Module constructor already does.
  const module = new Module(filename, parent);

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

  Module._cache[filename] = module;
  if (parent !== undefined) { // ... }

  let threw = true;
  try {
    if (enableSourceMaps) {
      try {
        // 如果不是 NativeModule,加载它
        module.load(filename);
      } catch (err) {
        rekeySourceMap(Module._cache[filename], err);
        throw err; /* node-do-not-add-exception-line */
      }
    } else {
      module.load(filename);
    }
    threw = false;
  } finally {
    // ...
  }

  return module.exports;
};

require.cache

当代码执行 require(lib) 时,会执行 lib 模块中的内容,并作为一份缓存,下次引用时不再执行模块中内容。

这里的缓存指的就是 require.cache ,也就是上一段指的 Module._cache

// <node_internals>/internal/modules/cjs/loader.js:899

require.cache = Module._cache;

这里有个小测试:

有两个文件: index.jsutils.jsutils.js 中有一个打印操作,当 index.js 引用 utils.js 多次时, utils.js 中的打印操作会执行几次。代码示例如下

index.js

// index.js

// 此处引用两次
require('./utils')
require('./utils')

utils.js

// utils.js
console.log('被执行了一次')

答案是只执行了一次,因此 require.cache ,在 index.js 末尾打印 require ,此时会发现一个模块缓存

// index.js

require('./utils')
require('./utils')

console.log(require)

Node 中如何引入一个模块及其细节

那回到本章刚开始的问题:

如何不重启应用热加载模块呢?

答: 删掉 Module._cache ,但同时会引发问题,如这种 一行 delete require.cache 引发的内存泄漏血案

所以说嘛,这种黑魔法大幅修改核心代码的东西开发环境玩一玩就可以了,千万不要跑到生产环境中去,毕竟黑魔法是不可控的。

总结

  1. 模块中执行时会被 module wrapper 包裹,并注入全局变量 requiremodule
  2. module.exportsexports 的关系实际上是 exports = module.exports
  3. require 实际上是 module.require
  4. require.cache 会保证模块不会被执行多次
  5. 不要使用 delete require.cache 这种黑魔法

关注我

本文收录于 GitHub 山月行博客: shfshanyue/blog ,内含我在实际工作中碰到的问题、关于业务的思考及在全栈方向上的学习

欢迎关注公众号【全栈成长之路】,定时推送 Node 原创及全栈成长文章

<figure>

<img width="240" src="https://shanyue.tech/qrcode.jpg" alt="欢迎关注">

<figcaption>欢迎关注全栈成长之路</figcaption>

</figure>


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Spring Into HTML and CSS

Spring Into HTML and CSS

Molly E. Holzschlag / Addison-Wesley Professional / 2005-5-2 / USD 34.99

The fastest route to true HTML/CSS mastery! Need to build a web site? Or update one? Or just create some effective new web content? Maybe you just need to update your skills, do the job better. Welco......一起来看看 《Spring Into HTML and CSS》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

URL 编码/解码
URL 编码/解码

URL 编码/解码

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具