模块系统
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});
标识符加载
- 像 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