Nodejs 第九章(模块化)

简介: Nodejs 第九章(模块化)

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

CommonJS 规范

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

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

导出模块exports 和 module.exports

module.exports = {
  hello: function() {
    console.log('Hello, world!');
  }
};

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

module.exports = 123

ESM模块规范

引入模块 import 必须写在头部

注意使用ESM模块的时候必须开启一个选项

打开package.json 设置 type:module

import fs from 'node:fs'

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

import data from './data.json' assert { type: "json" };
console.log(data);

加载模块的整体对象

import * as all from 'xxx.js'

动态导入模块

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

if(true){
    import('./test.js').then()
}

模块导出

  • 导出一个默认对象 default只能有一个不可重复export default
export default {
    name: 'test',
}
  • 导出变量
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文件如何处理
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文件如何处理
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文件如何处理
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

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

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方法

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],最后返回执行完的结果。

目录
相关文章
|
6天前
|
JavaScript Unix Linux
Nodejs 第十二章(path)
Nodejs 第十二章(path)
29 0
|
JavaScript 前端开发 测试技术
|
6天前
|
JavaScript 前端开发 API
游戏开发入门:Python后端与Vue前端的协同工作方式
【4月更文挑战第11天】使用Python后端(Flask或Django)和Vue.js前端开发游戏变得流行,能提高开发效率和可维护性。本文指导如何构建这样的项目,包括设置环境、创建虚拟环境、搭建后端API及前端Vue组件,强调前后端协作和API接口的重要性。这种架构促进团队合作,提升代码质量和游戏体验。
|
6天前
|
编译器 Go 持续交付
Go语言模块导入的实践与技巧:提升代码重用与模块化开发效率
【2月更文挑战第9天】在Go语言中,模块导入是实现代码重用和模块化开发的关键环节。本文将介绍Go语言中模块导入的实践与技巧,包括本地模块的导入、远程模块的导入、导入路径的解析与重定向、导入别名与包的重命名等,旨在帮助读者更加高效地进行Go语言的项目开发。
|
6天前
|
JavaScript 前端开发
【HarmonyOS 4.0 应用开发实战】TypeScript入门之模块化详讲
【HarmonyOS 4.0 应用开发实战】TypeScript入门之模块化详讲
34 0
|
6天前
|
存储 JavaScript 前端开发
第四章 模块和组件、模块化和组件化的理解
第四章 模块和组件、模块化和组件化的理解
|
6天前
|
JavaScript 算法
Nodejs 第十九章(pngquant)
Nodejs 第十九章(pngquant)
48 0
|
6天前
|
JavaScript 前端开发 开发工具
Nodejs 第二十二章(脚手架)
Nodejs 第二十二章(脚手架)
32 0
|
6天前
|
JavaScript 前端开发 开发者
CommonJS 模块编程(新思路方便复习笔记)
CommonJS 模块编程(新思路方便复习笔记)
33 0
|
6月前
|
监控 数据可视化 前端开发
一个.NetCore前后端分离、模块化、插件式的通用框架
一个.NetCore前后端分离、模块化、插件式的通用框架
117 0