node怎么在地址栏显示id_源码角度分析Node模块加载

341a3679b22f53551350b9054978a161.png

源码角度分析Node模块加载

前言

在前端开发过程中Node无处不在,无论你是不是专业的Node开发工程师还是Vue、React等框架开发者,Node都无处不在。了解Node的模块加载对于你开发过程中会有很大帮助,例如:

<!-- A.js -->
class A {}
module.exports = A;

<!-- B.js -->
const A = require('A.js');
class B {}

<!-- C.js -->
const A = require('A');
class C {}

针对于上面的情况你可能疑惑几点,第一点A模块到底被加载了几次,第二点import A from 'A'和import A from 'A.js'是怎么加载到A的,第三点为什么A和A.js都可以加载到。。。。。

本文带你从源码角度解开模块加载面纱。

以下所有讨论函数都在Node源码的:node-master/lib/internal/modules/cjs/loader.js,为了方便理解我会屏蔽一些细节

模块加载require函数

Node在导入包的时候使用require函数,require函数定义如下:

Module.prototype.require = function(id) {
  validateString(id, 'id');
  return Module._load(id, this, false);
};

例如我们require('A')的时候,首先A作为id传入require函数,判断id合法性,在合法的前提下调用Module._load()函数

Module._load函数

_load函数定义如下:

Module._load = function(request, parent, isMain) {
  const filename = Module._resolveFilename(request, parent, isMain);
  const cachedModule = Module._cache[filename];
  if (cachedModule !== undefined) {
    return cachedModule.exports;
  }
  const mod = loadNativeModule(filename, request);
  if (mod && mod.canBeRequiredByUsers) return mod.exports;
  const module = new Module(filename, parent);
  Module._cache[filename] = module;
  module.load(filename);
  return module.exports;
}

以上代码省略很多细节。

当调用_load函数的时候首先传入request就是require('A')的A,然后调用_resolveFilename得到filename,首先判断,判断步骤如下:

  1. 第一步:执行const cachedModule = Module._cache[filename];查看filename是否在缓存中也就是说是否之前加载过了,如果加载直接返回结果
  2. 第二步:执行const mod = loadNativeModule(filename, request);查看filename是否是原生模块(node的http、net、path模块等),如果是则返回结果。
  3. 第三步:执行const module = new Module(filename, parent);Module._cache[filename] = module;添加该模块到缓存然后调用module.load(filename);并加载该模块(下面详细讲解这部分加载过程),加载成功后返回。

关于_resolveFilename函数需要特别关注一下,了解它的细节你会知道为什么A和A.js都可以require成功。

Module._resolveFilename

_resolveFilename定义如下:

Module._resolveFilename = function(request, parent, isMain, options) {
  let paths;
  Module._resolveLookupPaths(request, parent);
  const filename = Module._findPath(request, paths, isMain, false);
  if (filename) return filename;
  const requireStack = [];
  for (let cursor = parent;
    cursor;
    cursor = cursor.parent) {
    requireStack.push(cursor.filename || cursor.id);
  }
  let message = `Cannot find module '${request}'`;
  const err = new Error(message);
  err.code = 'MODULE_NOT_FOUND';
  err.requireStack = requireStack;
  throw err;
};

该函数当收到一个A参数后,首先它会调用_resolveLookupPaths函数这个函数会返回当前目录、父目录、父父目录。。。一直到根目录的node_modules的文件目录,在我的电脑上返回为:

paths = [
    '/Users/xx/mygithub/this-spring-blog/node/1/node_modules',
    '/Users/xx/mygithub/this-spring-blog/node/node_modules',
    '/Users/xx/mygithub/this-spring-blog/node_modules',
    '/Users/xx/mygithub/node_modules',
    '/Users/xx/node_modules',
    '/Users/node_modules',
    '/node_modules'
  ]

也就是说当我们require('A')的时候不仅在当前目录下面找,还会去paths所有的node_modules去找,一直找到根目录。

将所有的paths找到后调用const filename = Module._findPath(request, paths, isMain, false);在这里会确定到底有没有filename,如果有就返回,如果没有就会执行下面的throw err。

Module._findPath函数定义如下:

Module._findPath = function(request, paths, isMain) {
  for (let i = 0; i < paths.length; i++) {
    const curPath = paths[i];
    if (curPath && stat(curPath) < 1) continue;
    const basePath = resolveExports(curPath, request, absoluteRequest);
    let filename;

    const rc = stat(basePath);
      if (!filename) {
        // Try it with each of the extensions
        if (exts === undefined)
          exts = ObjectKeys(Module._extensions);
        filename = tryExtensions(basePath, exts, isMain);
      }
    }

    if (!filename && rc === 1) {  // Directory.
      // try it with each of the extensions at "index"
      if (exts === undefined)
        exts = ObjectKeys(Module._extensions);
      filename = tryPackage(basePath, exts, isMain, request);
    }

    if (filename) {
      Module._pathCache[cacheKey] = filename;
      return filename;
    }
  }

  return false;
};

该函数在遍历paths过程中,假设你写的是require('A')那么在执行exts = ObjectKeys(Module._extensions);时候他会判断paths中每个路径下,是否存在A.js或者A.json或者A.node,这也就是为什么说我们写require('A')仍然可以准确加载A.js原因。需要说明一点Module._extensions在node的源码中定义为:

Module._extensions['.json'] = function(module, filename) {...}
Module._extensions['.js'] = function(module, filename) {...}
Module._extensions['.node'] = function(module, filename) {...}

所以ObjectKeys返回.node、.js、.json。当通过调用filename = tryPackage(basePath, exts, isMain, request);找到后就会返回filename。

Module.load函数

函数人定义如下:

Module.prototype.load = function(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);
  const exports = this.exports;
  ESMLoader.moduleMap.set(
  url,
  () => new ModuleJob(ESMLoader, url, () =>
    new ModuleWrap(url, undefined, ['default'], function() {
        this.setExport('default', exports);
    })
    , false /* isMain */, false /* inspectBrk */)
  );
};

通过上面的了解_resolveFilename会提供一个filename,如果在缓存和原生模块都没有,那么就要去重新加载,这就是load的作用。

load函数拿到filename后首先会判断是.json、.node还是.js,然后调用 Module._extensionsextension;这个方法去编译,编译成功后导出该函数,并记录缓存模块。

这里以js为例看一下Module._extensionsextension函数实现:

Module._extensions['.js'] = function(module, filename) {
  const content = fs.readFileSync(filename, 'utf8');
  module._compile(content, filename);
};

拿到filename后通过fs模块读进内容通过调用_compile进行编译然后输出可用模块。

总结

node模块加载过程:

  1. 首先判断该文件是否存在,查的过程是遍历当前目录一直到根目录的node_modules是否存在A.json、A.js、A.node,当然如果你写拓展名了可能会直接按照你写的拓展名加载,这也是加快加载速度的一个方法。如果存在下一步,不存在报错(例如:Error: Cannot find module 'hhhttp')。
  2. 缓存模块是否存在,如果存在返回,不存在下一步
  3. 判断原生模块是否存在,如果存在返回,不存在下一步
  4. 调用._load方法加载该模块并加入缓存中

node模块加载过程:

  1. 缓存模块是一个非常好的设计,避免多次加载同时可以快速加载模块
  2. 通过直接写出拓展名可以加快模块加载速度
  3. 可以提供一个卸载模块逻辑,这样可以把不必要模块卸载减小内存开销。

github

github地址(记录所有学习和项目相关)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值