Nodejs 第九章(模块化)

Nodejs 模块化规范遵循两套一套CommonJS规范另一套esm规范

CommonJS 规范

引入模块(require)支持四种格式

  1. 支持引入内置模块例如 http os fs child_process 等nodejs内置模块
  2. 支持引入第三方模块express md5 koa
  3. 支持引入自己编写的模块 ./ ../ 等
  4. 支持引入addon C++扩展模块 .node文件
js 复制代码
const fs = require('node:fs');  // 导入核心模块
const express = require('express');  // 导入 node_modules 目录下的模块
const myModule = require('./myModule.js');  // 导入相对路径下的模块
const nodeModule = require('./myModule.node');  // 导入扩展模块

导出模块exports 和 module.exports

js 复制代码
module.exports = {
  hello: function() {
    console.log('Hello, world!');
  }
};

如果不想导出对象直接导出值

js 复制代码
module.exports = 123

ESM模块规范

引入模块 import 必须写在头部

注意使用ESM模块的时候必须开启一个选项 打开package.json 设置 type:module

js 复制代码
import fs from 'node:fs'

如果要引入json文件需要特殊处理 需要增加断言并且指定类型json node低版本不支持

js 复制代码
import data from './data.json' assert { type: "json" };
console.log(data);

加载模块的整体对象

js 复制代码
import * as all from 'xxx.js'

动态导入模块

import静态加载不支持掺杂在逻辑中如果想动态加载请使用import函数模式

js 复制代码
if(true){
    import('./test.js').then()
}

模块导出

  • 导出一个默认对象 default只能有一个不可重复export default
js 复制代码
export default {
    name: 'test',
}
  • 导出变量
js 复制代码
export const a = 1

Cjs 和 ESM 的区别

  1. Cjs是基于运行时的同步加载,esm是基于编译时的异步加载
  2. Cjs是可以修改值的,esm值并且不可修改(可读的)
  3. Cjs不可以tree shaking,esm支持tree shaking
  4. commonjs中顶层的this指向这个模块本身,而ES6中顶层this指向undefined

nodejs部分源码解析

.json文件如何处理
js 复制代码
Module._extensions['.json'] = function(module, filename) {
  const content = fs.readFileSync(filename, 'utf8');

  if (policy?.manifest) {
    const moduleURL = pathToFileURL(filename);
    policy.manifest.assertIntegrity(moduleURL, content);
  }

  try {
    setOwnProperty(module, 'exports', JSONParse(stripBOM(content)));
  } catch (err) {
    err.message = filename + ': ' + err.message;
    throw err;
  }
};

使用fs读取json文件读取完成之后是个字符串 然后JSON.parse变成对象返回

.node文件如何处理
js 复制代码
Module._extensions['.node'] = function(module, filename) {
  if (policy?.manifest) {
    const content = fs.readFileSync(filename);
    const moduleURL = pathToFileURL(filename);
    policy.manifest.assertIntegrity(moduleURL, content);
  }
  // Be aware this doesn't use `content`
  return process.dlopen(module, path.toNamespacedPath(filename));
};

发现是通过process.dlopen 方法处理.node文件

.js文件如何处理
js 复制代码
Module._extensions['.js'] = function(module, filename) {
  // If already analyzed the source, then it will be cached.
  //首先尝试从cjsParseCache中获取已经解析过的模块源代码,如果已经缓存,则直接使用缓存中的源代码
  const cached = cjsParseCache.get(module);
  let content;
  if (cached?.source) {
    content = cached.source; //有缓存就直接用
    cached.source = undefined;
  } else {
    content = fs.readFileSync(filename, 'utf8'); //否则从文件系统读取源代码
  }
  //是不是.js结尾的文件
  if (StringPrototypeEndsWith(filename, '.js')) {
    //读取package.json文件
    const pkg = readPackageScope(filename);
    // Function require shouldn't be used in ES modules.
    //如果package.json文件中有type字段,并且type字段的值为module,并且你使用了require 
    //则抛出一个错误,提示不能在ES模块中使用require函数
    if (pkg?.data?.type === 'module') {
      const parent = moduleParentCache.get(module);
      const parentPath = parent?.filename;
      const packageJsonPath = path.resolve(pkg.path, 'package.json');
      const usesEsm = hasEsmSyntax(content);
      const err = new ERR_REQUIRE_ESM(filename, usesEsm, parentPath,
                                      packageJsonPath);
      // Attempt to reconstruct the parent require frame.
      //如果抛出了错误,它还会尝试重构父模块的 require 调用堆栈
      //,以提供更详细的错误信息。它会读取父模块的源代码,并根据错误的行号和列号,
      //在源代码中找到相应位置的代码行,并将其作为错误信息的一部分展示出来。
      if (Module._cache[parentPath]) {
        let parentSource;
        try {
          parentSource = fs.readFileSync(parentPath, 'utf8');
        } catch {
          // Continue regardless of error.
        }
        if (parentSource) {
          const errLine = StringPrototypeSplit(
            StringPrototypeSlice(err.stack, StringPrototypeIndexOf(
              err.stack, '    at ')), '\n', 1)[0];
          const { 1: line, 2: col } =
              RegExpPrototypeExec(/(\d+):(\d+)\)/, errLine) || [];
          if (line && col) {
            const srcLine = StringPrototypeSplit(parentSource, '\n')[line - 1];
            const frame = `${parentPath}:${line}\n${srcLine}\n${
              StringPrototypeRepeat(' ', col - 1)}^\n`;
            setArrowMessage(err, frame);
          }
        }
      }
      throw err;
    }
  }
  module._compile(content, filename);
};

如果缓存过这个模块就直接从缓存中读取,如果没有缓存就从fs读取文件,并且判断如果是cjs但是type为module就报错,并且从父模块读取详细的行号进行报错,如果没问题就调用 compile

js 复制代码
Module.prototype._compile = function(content, filename) {
  let moduleURL;
  let redirects;
  const manifest = policy?.manifest;
  if (manifest) {
    moduleURL = pathToFileURL(filename);
    //函数将模块文件名转换为URL格式
    redirects = manifest.getDependencyMapper(moduleURL);
    //redirects是一个URL映射表,用于处理模块依赖关系
    manifest.assertIntegrity(moduleURL, content); 
    //manifest则是一个安全策略对象,用于检测模块的完整性和安全性
  }
  /**
   * @filename {string}  文件名
   * @content {string}   文件内容
   */
  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]) {
        try {
          resolvedArgv = Module._resolveFilename(process.argv[1], null, false);
        } catch {
          // We only expect this codepath to be reached in the case of a
          // preloaded module (it will fail earlier with the main entry)
          assert(ArrayIsArray(getOptionValue('--require')));
        }
      } else {
        resolvedArgv = 'repl';
      }
    }

    // Set breakpoint on module start
    if (resolvedArgv && !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 SafeMap();
  if (inspectorWrapper) {
    result = inspectorWrapper(compiledWrapper, thisValue, exports,
                              require, module, filename, dirname);
  } else {
    result = ReflectApply(compiledWrapper, thisValue,
                          [exports, require, module, filename, dirname]);
  }
  hasLoadedAnyUserCJSModule = true;
  if (requireDepth === 0) statCache = null;
  return result;
};

首先,它检查是否存在安全策略对象 policy.manifest。如果存在,表示有安全策略限制需要处理 将函数将模块文件名转换为URL格式,redirects是一个URL映射表,用于处理模块依赖关系,manifest则是一个安全策略对象,用于检测模块的完整性和安全性,然后调用wrapSafe

js 复制代码
function wrapSafe(filename, content, cjsModuleInstance) {
  if (patched) {
    const wrapper = Module.wrap(content);
    //支持esm的模块 
    //import { a } from './a.js'; 类似于eval
    //import()函数模式动态加载模块
    const script = new Script(wrapper, {
      filename,
      lineOffset: 0,
      importModuleDynamically: async (specifier, _, importAssertions) => {
        const loader = asyncESM.esmLoader;
        return loader.import(specifier, normalizeReferrerURL(filename),
                             importAssertions);
      },
    });

    // Cache the source map for the module if present.
    if (script.sourceMapURL) {
      maybeCacheSourceMap(filename, content, this, false, undefined, script.sourceMapURL);
    }
    //返回一个可执行的全局上下文函数
    return script.runInThisContext({
      displayErrors: true,
    });
  }

wrapSafe调用了wrap方法

js 复制代码
let wrap = function(script) {
  return Module.wrapper[0] + script + Module.wrapper[1];
};
//(function (exports, require, module, __filename, __dirname) {
 //const xm = 18
//\n});
const wrapper = [
  '(function (exports, require, module, __filename, __dirname) { ',
  '\n})',
];

wrap方法 发现就是把我们的代码包装到一个函数里面

//(function (exports, require, module, __filename, __dirname) {

//const xm = 18 我们的代码

//\n});

然后继续看wrapSafe函数,发现把返回的字符串也就是包装之后的代码放入nodejs虚拟机里面Script,看有没有动态import去加载,最后返回执行后的结果,然后继续看_compile,获取到wrapSafe返回的函数,通过Reflect.apply调用因为要填充五个参数[exports, require, module, filename, dirname],最后返回执行完的结果。

相关推荐
橘右溪13 小时前
Node.js核心模块及Api详解
node.js
在下千玦1 天前
#管理Node.js的多个版本
node.js
你的人类朋友1 天前
MQTT协议是用来做什么的?此协议常用的概念有哪些?
javascript·后端·node.js
还是鼠鼠1 天前
Node.js中间件的5个注意事项
javascript·vscode·中间件·node.js·json·express
南通DXZ2 天前
Win7下安装高版本node.js 16.3.0 以及webpack插件的构建
前端·webpack·node.js
你的人类朋友2 天前
浅谈Object.prototype.hasOwnProperty.call(a, b)
javascript·后端·node.js
前端太佬2 天前
暂时性死区(Temporal Dead Zone, TDZ)
前端·javascript·node.js
Mintopia2 天前
Node.js 中 http.createServer API 详解
前端·javascript·node.js
你的人类朋友2 天前
CommonJS模块化规范
javascript·后端·node.js
Mintopia2 天前
Node.js 中 fs.readFile API 的使用详解
前端·javascript·node.js