Skip to content

模块系统

Node对引入过的模块都会进行缓存,以减少二次引入时的开销。不同的地方在于,浏览器仅仅缓存文件,而Node缓存的是编译和执行之后的对象。

模块的查找过程

  • 首先,Node在当前目录下查找package.json(CommonJS包规范定义的包描述文件),通过JSON.parse()解析出包描述对象,从中取出main属性指定的文件名进行定位。
  • 如果文件名缺少扩展名,将会进入扩展名分析的步骤。而如果main属性指定的文件名错误,或者压根没有package.json文件,Node会将index当做默认文件名,然后依次查找index.js、index.json、index.node。
  • 如果在目录分析的过程中没有定位成功任何文件,则自定义模块进入下一个模块路径进行查找。如果模块路径数组都被遍历完毕,依然没有查找到目标文件,则会抛出查找失败的异常。

模块的载入

定位到具体的文件后,Node会新建一个模块对象,然后根据路径载入并编译。对于不同的文件扩展名,其载入方法也有所不同:

❑ .js文件。通过fs模块同步读取文件后编译执行。

❑ .node文件。这是用C/C++编写的扩展文件,通过dlopen()方法加载最后编译生成的文件。

❑ .json文件。通过fs模块同步读取文件后,用JSON.parse()解析返回结果。

❑ 其余扩展名文件。它们都被当做.js文件载入

每一个编译成功的模块都会将其文件路径作为索引缓存在Module._cache对象上,以提高二次引入的性能。

Module._extensions会被赋值给require()的extensions属性,所以通过在代码中访问require.extensions可以知道系统中已有的扩展加载方式

commonJS

适用场景

  • Node 是 CommonJS 在服务器端一个具有代表性的实现
  • Browserify 是 CommonJS 在浏览器中的一种实现
  • webpack 打包工具对 CommonJS 的支持和转换

实现原理

  • module 记录当前模块信息
  • require 引入模块的方法
  • exports 当前模块导出的属性

在编译的过程中,文件内容被进行了头尾包装:

在头部添加了(function (exports, require,module, __filename, __dirname) {\n,在尾部添加了\n});

(function(exports,require,module,__filename,__dirname){
 ...
})

标识符加载

  • 像 fs ,http ,path 等标识符,会被作为 nodejs 的核心模块
  • ./../ 作为相对路径的文件模块/ 作为绝对路径的文件模块
  • 非路径形式也非核心模块的模块,将作为自定义模块

特性

  • require语法是同步的
  • require一个模块实际得到的是该模块的exports属性
  • 输出的是值的拷贝
  • 运行时加载

导入与导出

// 导入
// 核心模块
const fs = require("fs");

// 第三方模块
// npm install marked
const marked = require("marked");

// 用户模块(自己写的),正确的,正确的方式
// 注意:加载自己写的模块,相对路径不能省略 ./
const foo = require("./foo.js");

// 用户模块(自己写的),正确的(推荐),可以省略后缀名 .js
const bar = require("./bar");



// 导出
// `module.exports`有一个别名`exports`
console.log(exports === module.exports); // => true
exports.a = 123;
exports.b = 456;
exports.c = 789;
exports.fn = function() {};

// 导出单个成员:必须这么写
module.exports = function(x, y) {
  return x + y;
};

ES6 Module

特性

  • 输出的是值的引用
  • 模块识别器是URL,且只支持file://URL
  • ES Module加载js文件的过程是编译(解析)时加载的,而且是异步
  • 使用 Node 原生 ES6 模块需要将 js 文件后缀改成 mjs,或者 package.json 中的"type" 字段改为 "module"

导入与导出

// Named Exports | Named Imports
//------ lib.js ------
const sqrt = Math.sqrt;
function square(x) {
    return x * x;
}
function diag(x, y) {
    return sqrt(square(x) + square(y));
}
export {sqrt, square, diag};


export const name = 'value' // export直接加在声明前可以省略`{}`
import { name } from '...' // import必须要有`{}`

// Export List + Rename | Import List + Rename
export {
  name1,
  name2 as newName2
}
import {
  name1 as newName1,
  newName2
} from '...'

// Default Exports | Default Imports
export default 'value' // 一个模块只能有一个default export
import anyName from '...'
import { default as anyName } from '...' // Default Export的本质就是导出default这个name
// 虽然default export只能有一个,但是可以导出多个方法
export default {
  speak () {
    return 'moo'
  },
  eat () {
    return 'cow eats'
  },
  drink () {
    return 'cow drinks'
  }
}


// Aliasing Named Imports
import { speak as cowSpeak } from './cow.js'
import { speak as goatSpeak } from './goat.js'

// Namespace Imports
import * as cow from './cow.js' // 可以是任意别名
import * as goat from './goat.js'

// Rename Exports | NameImports
export { name as newName }
import { newName } from '...'

// Named + Default
export const name = 'value'
export default 'value'

AMD

鉴于网络的原因,CommonJS为后端JavaScript制定的规范并不完全适合前端的应用场景。

经过一段争执之后,AMD规范最终在前端应用场景中胜出。

它的全称是Asynchronous ModuleDefinition,即是“异步模块定义”

import()

  • ES6模块中的import是声明(statement),不像require()是函数,这导致import只能接字符串而无法使用计算变量

  • import()函数在CommonJS和ES6模块中都可用

  • import()函数是异步的,其返回值是一个Promise

参考

「万字进阶」深入浅出 Commonjs 和 Es Module