CommonJS
- 模块划分:
const fs = require('fs') // ①核心模块
const sayName = require('./hello.js') //② 文件模块
const crypto = require('crypto-js') // ③第三方自定义模块
- ① nodejs 底层的
核心模块。 - ② 我们编写的
文件模块,比如上述 sayName。 - ③ 我们通过 npm 下载的第三方
自定义模块,比如 crypto-js。
- 模块识别:
- 首先像 fs ,http ,path 等标识符,会被作为 nodejs 的
核心模块。 - ./ 和 ../ 作为相对路径的文件模块, / 作为绝对路径的
文件模块。 - 非路径形式也非核心模块的模块,将作为
自定义模块。
- 模块的处理:
优先从缓存中加载
缓存中没有的话,判断模块名有没有带路径,如果模块中有路径,加载文件模块
模块名没有路径,优先加载核心模块,如果不是核心模块,则加载第三方自定义模块
核心模块的优先级仅次于缓存加载,在 Node 源码编译中,已被编译成二进制代码,所以加载核心模块,加载过程中速度最快。- ./ ,../ 和 / 开始的标识符,会被当作
文件模块处理,require方法会将路径转换成真实路径,并以真实路径作为索引。- require方法:
- 作为文件:找到该路径中父文件夹的目录,再查找
同名文件,如果没找到,且没有后缀的话则尝试添加.js、.json或.node拓展名查找。 - 作为文件夹:如果还是没有找到,会找到
同名文件夹的目录,找到后,会找 package.json 下 main 属性指向的文件,如果没有则会继续查找 会找到同名文件夹下的index.js、index.json、index.node文件。如果依旧不存在,则会报错。
- 作为文件:找到该路径中父文件夹的目录,再查找
- require方法:
自定义模块:- 在当前目录下的 node_modules 目录查找。
- 如果没有
同名文件,则尝试添加.js、.json或.node拓展名查找。 - 如果还是没有,会查找
同名文件夹,找到后,会找 package.json 下 main 属性指向的文件。如果没有,则会继续查找index.js、index.json、index.node文件。 - 如果没有,在父级目录的 node_modules 查找,如果没有则沿着路径向上递归,直到根目录下的 node_modules 目录。
- 循环引用问题
a.js文件
const getMes = require('./b')
console.log('我是 a 文件')
exports.say = function(){
const message = getMes()
console.log(message)
}
b.js文件
const say = require('./a')
const object = {
name:'《React进阶实践指南》',
author:'我不是外星人'
}
console.log('我是 b 文件')
module.exports = function(){
return object
}
- 主文件
main.js
const a = require('./a')
const b = require('./b')
console.log('node 入口文件')
接下来终端输入 node main.js 运行 main.js,效果如下:
- ① 首先执行
node main.js,那么开始执行第一行require(a.js); - ② 那么首先判断
a.js有没有缓存,因为没有缓存,先加入缓存,然后执行文件 a.js (需要注意 是先加入缓存, 后执行模块内容); - ③ a.js 中执行第一行,引用 b.js。
- ④ 那么判断
b.js有没有缓存,因为没有缓存,所以加入缓存,然后执行 b.js 文件。 - ⑤ b.js 执行第一行,再一次循环引用
require(a.js)此时的 a.js 已经加入缓存,直接读取值。接下来打印console.log('我是 b 文件'),导出方法。 - ⑥ b.js 执行完毕,回到 a.js 文件,打印
console.log('我是 a 文件'),导出方法。 - ⑦ 最后回到
main.js,打印console.log('node 入口文件')完成这个流程。
不过这里我们要注意问题:
- 如上第 ⑤ 的时候,当执行 b.js 模块的时候,因为 a.js 还没有导出
say方法,所以 b.js 同步上下文中,获取不到 say。
我用一幅流程图描述上述过程:
为了进一步验证上面所说的,我们改造一下 b.js 如下:
const say = require('./a')
const object = {
name:'《React进阶实践指南》',
author:'我不是外星人'
}
console.log('我是 b 文件')
console.log('打印 a 模块' , say)
setTimeout(()=>{
console.log('异步打印 a 模块' , say)
},0)
module.exports = function(){
return object
}
打印结果:
- 第一次打印 say 为空对象。
- 第二次打印 say 才看到 b.js 导出的方法。
那么如何获取到 say 呢,有两种办法:
- 一是用动态加载 a.js 的方法。
延迟加载,用的时候再 require
- 二个就是如上放在异步中加载。
AMD
-
AMD概念:
- AMD是异步加载,推崇前置依赖,提前加载;下载完成后就直接执行依赖模块 (依赖模块的执行顺序和书写的顺序不一定一致 )。
- 所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行;
-
AMD规范:
定义暴露模块:
//定义没有依赖的模块 define(function(){ return 模块 })//定义有依赖的模块 define(['module1', 'module2'], function(m1, m2){ return 模块 })引入使用模块:
require(['module1', 'module2'], function(m1, m2){ 使用m1/m2 })
CMD
-
CMD规范 -CMD是异步加载,推崇就近依赖,加载完某个依赖模块后并不执行,只是下载而已,遇到 require 语句 的时候 才执行对应的模块 (模块的执行顺序就和书写的顺序一致 )
定义暴露模块:
//定义没有依赖的模块 define(function(require, exports, module){ exports.xxx = value module.exports = value })//定义有依赖的模块 define(function(require, exports, module){ //引入依赖模块(同步) var module2 = require('./module2') //引入依赖模块(异步) require.async('./module3', function (m3) { }) //暴露模块 exports.xxx = value })引入使用模块:
define(function (require) { var m1 = require('./module1') var m4 = require('./module4') m1.show() m4.show() })
ESM
-
概念:
- ESM是异步加载,编译时输出接口,输出的是值的
只读引用。 - 遇到模块加载命令 import,就会生 成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载 的那个模块里面去取值。
- ESM是异步加载,编译时输出接口,输出的是值的
-
优势:
- 借助
Es Module的静态导入导出的优势,实现了tree shaking。 Es Module还可以import()懒加载方式实现代码分割。
定义暴露模块:
export default function add() {}; export default {}; export const name = 'xx'; export {}; export function add() {}; // 重命名 const age = 18; export { age as myAge };export default xxmodule 的默认导出。xx可以是函数、对象、变量。export xx,xx可以是变量的声明、对象。
注意:default 是 export 的语法糖,是导出对象的一个默认属性,相当于
export { default: 'xx' }或者export const default = 'xx';引入使用模块:
import module from 'xx'; // module 为 export default 导出的内容; import { name } from 'xx'; // name export 导出的内容; // 混合导出 import module, { name } from 'xx'; // 全部导出 import * from 'xx'; // module 为 export + export default 导出的内容; // 重命名 import { name as MyName } from 'xx'; - 借助
-
循环引用问题
遇到模块加载命令import时不会去执行模块,只是生成一个指向被加载模块的引用,需要开发者保证真正取值时能够取到值,只要引用是存在的,代码就能执行。
a.mjsimport {bar} from './b.mjs'; export function foo() { bar(); console.log('执行完毕'); } foo();b.mjsimport {foo} from './a.mjs'; export function bar() { if (Math.random() > 0.5) { foo(); } }main.mjsimport * as a from './a.mjs'; // 1. moduleA: 导出 foo // 2. moduleA:执行 foo(); // 3. moduleA:执行 bar() // 4. 开始执行 b模块 // 5. moduleB:导出 bar // 6. moduleA:调用 bar() // 7. moduleA:根据 Math.random() > 0.5 判断 是否调用foo,如果为 true 调用 foo(), 执行 2,3,4,5,6 // 8. moduleA:如果 Math.random() > 0.5 为false, 执行 ’console.log('执行完毕');', 并跳出调用栈。