模块化

Nodejs支持两种模块规范:CommonJSESM

CommonJS

1{
2  "name": "xiaosu",
3  "version": "1.0.0",
4  "type": "commonjs"
5}

require 引入

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

  1. 引入本地自定义模块,通常使用相对路径引入。

  2. 引入第三方模块,如 expresslodashmd5koa 等。

  3. 引入内置模块,如 fs(用于文件系统操作)http(用户http服务器和客户端) 等。

  4. 引入 .node 文件.node 模块通常是使用 node-gyp 或其他编译工具编译的,它们允许 Node.js 代码调用本地系统级的库和功能。这些模块可以提供性能优化或访问操作系统级别特性的能力,这些通常是纯 JavaScript 无法做到的。

  5. 引入 JSON 文件,会同步将 JSON 文件内容解析为 JavaScript对象。

1// 导入相对路径下的模块
2const mtTest = require('./test')
1// 导入 node_modules 目录下的模块
2const md5 = require("md5");
3console.log(md5("123"));
1// 导入内置模块
2const fs = require("node:fs");
3console.log(fs);
1// 导入扩展模块
2const nodeModule = require("./myModule.node");
3console.log(nodeModule);
1// 导入json文件
2const json = require("./data.json");
3console.log(json.name);

:::

module.exports 导出

导出一个对象

1module.exports = {
2  name: 'xiaosu'
3};

导出值

1module.exports = 123

ESModule

1{
2  "name": "xiaosu",
3  "version": "1.0.0",
4  "type": "module"
5}

import 引入

引入模块 import 必须写在头部

1import fs from 'node:fs'

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

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

加载模块的整体对象

1import * as all from 'xxx.js'

动态导入模块

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

1if (true) {
2  import("./test.js").then((res) => {
3    console.log(res);
4  });
5}

export 导出

1export const num = 123;
2
3export default {
4  name: "xiaosu",
5};

commonjs 和 ESM 的区别

  1. commonjs 是基于运行时的同步加载,esm 是基于编译时的异步加载

  2. commonjs是可以修改值的,esm 值并且不可修改(可读的)

  3. commonjs 不可以 tree shaking,esm 支持tree shaking

  4. commonjs 中顶层的this指向这个模块本身,而ESM中顶层this指向undefined

nodejs部分源码解析

lib/internal/modules/cjs/loader.js

1// 使用fs读取json文件,读取完成之后是个字符串,然后通过JSONParse变成对象返回
2Module._extensions['.json'] = function(module, filename) {
3  const content = fs.readFileSync(filename, 'utf8');
4
5  const manifest = policy()?.manifest;
6  if (manifest) {
7    const moduleURL = pathToFileURL(filename);
8    manifest.assertIntegrity(moduleURL, content);
9  }
10
11  try {
12    setOwnProperty(module, 'exports', JSONParse(stripBOM(content)));
13  } catch (err) {
14    err.message = filename + ': ' + err.message;
15    throw err;
16  }
17};
1// 发现是通过process.dlopen方法处理.node文件
2Module._extensions['.node'] = function(module, filename) {
3  const manifest = policy()?.manifest;
4  if (manifest) {
5    const content = fs.readFileSync(filename);
6    const moduleURL = pathToFileURL(filename);
7    manifest.assertIntegrity(moduleURL, content);
8  }
9  // Be aware this doesn't use `content`
10  return process.dlopen(module, path.toNamespacedPath(filename));
11};
1// 如果缓存过这个模块就直接从缓存中读取,如果没有缓存就从fs读取文件,并且判断如果是cjs但是type为module就报错,并且从父模块读取详细的行号进行报错,如果没问题就调用 compile
2Module._extensions['.js'] = function(module, filename) {
3  // If already analyzed the source, then it will be cached.
4  // 首先尝试从 cjsParseCache 中获取已经解析过的模块源代码,如果已经缓存,则直接使用缓存中的源代码
5  const cached = cjsParseCache.get(module);
6  let content;
7  if (cached?.source) {
8    content = cached.source; // 有缓存就直接用
9    cached.source = undefined;
10  } else {
11    content = fs.readFileSync(filename, 'utf8'); // 否则从fs读取源代码
12  }
13  // 是不是.js结尾的文件
14  if (StringPrototypeEndsWith(filename, '.js')) {
15    // 读取package.json文件
16    const pkg = packageJsonReader.readPackageScope(filename) || { __proto__: null };
17    // Function require shouldn't be used in ES modules.
18    // 如果package.json文件中有type字段,并且type字段的值为module,并且你使用了require则抛出一个错误,提示不能在ES模块中使用require函数
19    if (pkg.data?.type === 'module') {
20      const parent = moduleParentCache.get(module);
21      const parentPath = parent?.filename;
22      const packageJsonPath = path.resolve(pkg.path, 'package.json');
23      const usesEsm = hasEsmSyntax(content);
24      const err = new ERR_REQUIRE_ESM(filename, usesEsm, parentPath,
25                                      packageJsonPath);
26      // Attempt to reconstruct the parent require frame.
27      // 如果抛出了错误,它还会尝试重构父模块的 require 调用堆栈,以提供更详细的错误信息。它会读取父模块的源代码,并根据错误的行号和列号,在源代码中找到相应位置的代码行,并将其作为错误信息的一部分展示出来。
28      if (Module._cache[parentPath]) {
29        let parentSource;
30        try {
31          parentSource = fs.readFileSync(parentPath, 'utf8');
32        } catch {
33          // Continue regardless of error.
34        }
35        if (parentSource) {
36          const errLine = StringPrototypeSplit(
37            StringPrototypeSlice(err.stack, StringPrototypeIndexOf(
38              err.stack, '    at ')), '\n', 1)[0];
39          const { 1: line, 2: col } =
40              RegExpPrototypeExec(/(\d+):(\d+)\)/, errLine) || [];
41          if (line && col) {
42            const srcLine = StringPrototypeSplit(parentSource, '\n')[line - 1];
43            const frame = `${parentPath}:${line}\n${srcLine}\n${
44              StringPrototypeRepeat(' ', col - 1)}^\n`;
45            setArrowMessage(err, frame);
46          }
47        }
48      }
49      throw err;
50    }
51  }
52  module._compile(content, filename);
53};

:::