Commonjs 和 Es Module
一、模块化
<!-- 存在全局污染和依赖管理混乱问题 -->
<body>
<script src="./index.js"></script>
<script src="./home.js"></script>
<script src="./list.js"></script>
</body
全局污染
./home
和./list
中分别定义function name() {}
和var name = '小明'
,在./list
中添加打印name有可能输出一个函数依赖管理
./index
中无法调用./home
或者./list
中的方法,只能下面的可以调用上面的。
二、Commonjs
1、实现原理
// hello.js
let name = '《JavaScript高级程序设计》'
module.exports = function sayName (){
return name
}
// home.js 实现hello.js的导入以及导出
const sayName = require('./hello.js')
module.exports = function say(){
return {
name:sayName(),
author:'我不是外星人'
}
}
存在module
、exports
、require
三个变量,然而这三个变量是没有被定义的,但是可以在Commonjs的规范下每一个js模块上直接使用他们。
在编译过程冲,实际Commonjs对js的代码进行了首尾包装:
(function(exports,require,module,__filename,__dirname){
const sayName = require('./hello.js')
module.exports = function say(){
return {
name:sayName(),
author:'我不是外星人'
}
}
})
2、require文件加载流程
const fs = require('fs') // nodejs核心模块
const sayName = require('./hello.js') // 路径形式的文件模块
const crypto = require('crypto-js') // 第三方自定义模块
核心模块的处理
核心模块的优先级仅次于缓存加载,在Node
源码编译中,已被编译成二进制代码,因此加载过程中速度最快
路径形式的文件模块的处理
以./
、../
、和/
开始的标识符,会被当做文件模块处理,require()
方法会将路径转换成真实路径,并以真实路径作为索引,将编译后的结果缓存起来,第二次加载的时候会更快。
自定义模块的处理
查找规则:
- 在当前目录下的
node_mudules
目录查找 - 如果没有,在父级目录的
node_modules
查找,如果没有,就继续向父级目录查找node_modules
- 向上递归查找,直到根目录下的
node_modules
- 在查找过程中,会找
package,json
下main
属性指向的文件,如果没有package.json
,在node环境会依次查找index.js
、index.json
、index.node
3、require模块引入处理
Commonjs模块同步加载并执行模块文件,CommonJS模块在执行阶段分析模块依赖,采用深度优先遍历。
// a.js
console.log('a.js')
const getMes = require('./b')
console.log('我是 a 文件')
exports.say = function(){
const message = getMes()
console.log(message)
}
// b.js
console.log('b.js')
const say = require('./a')
const object = {
name:'《JavaScript高级程序设计》',
author:'我不是外星人'
}
console.log('我是 b 文件')
module.exports = function(){
return object
}
// main.js
const a = require('./a')
console.log('main.js')
const b = require('./b')
console.log('node 入口文件')
在终端输入 node main.js
,打印如下
PS D:\Code\Source\common-question> node ./cjs-es/main.js
a.js
b.js
我是 b 文件
我是 a 文件
main.js
node 入口文件
PS D:\Code\Source\common-question>
根据以上运行结果得出结论:
main.js
和a.js
模块都引用了b.js
模块,但是b.js
模块只执行了一次a.js
模块和b.js
模块相互引用,但是没有造成循环引用的情况- 主席那个顺序是父 --> 子 --> 父
3.1、require加载原理
module
:在Node中每一个js文件都是一个module,module上保存了exports等信息外,还有一个loaded表示该模块是否被加载。true
表示已经加载,false
表示还未加载。
Module
:以nodejs为例,整个系统运行之后,会用Module
缓存每一个模块加载的信息。
require源码大致如下:
// id 为路径标识符
function require(id) {
/* 查找 Module 上有没有已经加载的 js 对象*/
// require 会接收一个参数——文件标识符,然后分析定位文件,
// 分析过程我们上述已经讲到了,加下来会从 Module 上查找有没有缓存,
// 如果有缓存,那么直接返回缓存的内容
const cachedModule = Module._cache[id]
/* 如果已经加载了那么直接取走缓存的 exports 对象 */
if(cachedModule){
return cachedModule.exports
}
/* 创建当前模块的 module */
// 如果没有缓存,会创建一个 module 对象,缓存到 Module 上,然后执行文件,
// 加载完文件,将 loaded 属性设置为 true ,然后返回 module.exports 对象。
// 借此完成模块加载流程
const module = { exports: {} ,loaded: false , ...}
/* 将 module 缓存到 Module 的缓存属性中,路径标识符作为 id */
Module._cache[id] = module
/* 加载文件 */
runInThisContext(wrapper('module.exports = "123"'))(module.exports, require, module, __filename, __dirname)
/* 加载完成 *//
module.loaded = true
/* 返回值 */
// 模块导出就是 return 这个变量的其实跟 a = b 赋值一样,
// 基本类型导出的是值, 引用类型导出的是引用地址
return module.exports
}
// exports 和 module.exports 持有相同引用,因为最后导出的是 module.exports,
// 所以对 exports 进行赋值会导致 exports 操作的不再是 module.exports 的引用
3.2、require避免重复加载
从上面我们可以直接得出,require 如何避免重复加载的,首先加载之后的文件的 module
会被缓存到 Module
上,比如一个模块已经 require 引入了 a 模块,如果另外一个模块再次引用 a ,那么会直接读取缓存值 module ,所以无需再次执行模块。
3.3、require避免循环引用
① 首先执行 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 入口文件')
完成这个流程。
4、require动态加载
// a.js
console.log('我是 a 文件')
exports.say = function(){
const getMes = require('./b') // 动态加载b.js模块,require本质是一个函数
const message = getMes()
console.log(message)
}
// main.js
const a = require('./a')
a.say()
PS D:\Code\Source\common-question> node ./cjs-es/main.js
我是 a 文件
b.js
我是 b 文件
{ name: '《JavaScript高级程序设计》', author: '我不是外星人' }
PS D:\Code\Source\common-question>
5、exports和module.exports
exports.name = 'alien' // 此时 exports.name 是无效的
module.exports ={
name:'《React进阶实践指南》',
author:'我不是外星人',
say(){
console.log(666)
}
}
- 上述情况下 exports.name 无效,会被
module.exports
覆盖。
三、ES Module
1、导出export和导入import
1.1、export正常导出和import
// 导出模块:a.js
const name = '《JavaScript高级程序设计》'
const author = '我不是外星人'
export { name, author }
export const say = function (){
console.log('hello , world')
}
// 导入模块:main.js
// name , author , say 对应 a.js 中的 name , author , say
import { name , author , say } from './a.js'
export {}
,与变量名绑定,命名导出import {} from 'module'
,导入modeul
的命名导出import {}
内部的变量名要与export{}
完全匹配
1.2、默认导出export default
// 导出模块:a.js
const name = '《JavaScript高级程序设计》'
const author = '我不是外星人'
const say = function (){
console.log('hello , world')
}
export default {
name,
author,
say
}
// 导入模块:main.js
import mes from './a.js'
console.log(mes) //{ name: '《JavaScript高级程序设计》',author:'我不是外星人', say:Function }
export default anything
导入 module 的默认导出。anything
可以是函数,属性方法,或者对象。- 对于引入默认导出的模块,
import anyName from 'module'
, anyName 可以是自定义名称。
1.3、混合导入导出
// 导入模块:a.js
export const name = '《JavaScript高级程序设计》'
export const author = '我不是外星人'
export default function say (){
console.log('hello , world')
}
// 导入模块:main.js 第一种导入方式
import theSay , { name, author as bookAuthor } from './a.js'
console.log(
theSay, // ƒ say() {console.log('hello , world') }
name, // "《JavaScript高级程序设计》"
bookAuthor // "我不是外星人"
)
// 导入模块:main.js 第二种导入方式
import theSay, * as mes from './a'
console.log(
theSay, // ƒ say() { console.log('hello , world') }
mes // { name:'《JavaScript高级程序设计》' , author: "我不是外星人" ,default: ƒ say() { console.log('hello , world') } }
)
1.4、重属名导入
import { name as bookName , say, author as bookAuthor } from 'module'
console.log( bookName , bookAuthor , say ) //《React进阶实践指南》 我不是外星人
1.5、重定向导出
可以把当前模块作为一个中转站,一方面引入 module 内的属性,然后把属性再给导出去。
export * from 'module' // 第一种方式
export { name, author, ..., say } from 'module' // 第二种方式
export { name as bookName , author as bookAuthor , ..., say } from 'module' //第三种方式
- 第一种方式:重定向导出 module 中的所有导出属性, 但是不包括
module
内的default
属性。 - 第二种方式:从 module 中导入 name ,author ,say 再以相同的属性名,导出。
- 第三种方式:从 module 中导入 name ,重属名为 bookName 导出,从 module 中导入 author ,重属名为 bookAuthor 导出,正常导出 say 。
1.6、无需导入模块,只运行模块
import 'module'
1.7、动态导入
const promise = import('module')
2、ES Module特性
2.1、静态语法
ES Module的导入和导出是境内太的,import
会自动提升到代码的顶层,import
,export
不能放在块级作用域或条件语句中。
// 错误写法一
function say(){
import name from './a.js'
export const author = '我不是外星人'
}
// 错误写法二
isexport && export const name = '《React进阶实践指南》'
// 错误写法三
let name = 'Export'
import 'default' + name from 'module'
2.2、执行特性
CommonJS
模块同步加载并执行模块文件,ES Module
模块提前加载并执行模块文件。
ES Module
在预处理阶段分析模块,在执行阶段执行模块,两个阶段都采用深度优先遍历,执行顺序是子 --> 父
// main.js
console.log('main.js开始执行')
import say from './a'
import say1 from './b'
console.log('main.js执行完毕')
// a.js
import b from './b'
console.log('a模块加载')
export default function say (){
console.log('hello , world')
}
// b.js
console.log('b模块加载')
export default function sayhello(){
console.log('hello,world')
}
运行顺序如下
b模块加载
a模块加载
main.js开始执行
main.js执行完毕
2.3、导出绑定
使用 import 被导入的模块运行在严格模式下。
使用 import 被导入的变量是只读的,可以理解默认为 const 装饰,无法被赋值
使用 import 被导入的变量是与原变量绑定/引用的,可以理解为 import 导入的变量无论是否为基本类型都是引用传递。
3、import()动态导入
import()
可以动态使用,import()
返回一个Promise
对象,返回的Promise
的then
成功回调中,可以获取模块加载成功信息。
<script>
export default defineComponent({
name: 'CustomComponent',
props: {
step: {
type: Object,
default: () => ({}),
},
},
setup(props) {
const customRes = computed(() => {
return () => obj.res; // { res: import(./name.vue) }
});
return {
customRes,
};
},
render(h) {
return (
<div class="layout">
{h(this.customRes, {
props: {
step: this.step,
},
})}
</div>
);
},
});
</script>
<style lang="scss">
</style>
4、CommonJS和ES Module
4.1、commonjs
CommonJS
模块由JS运行时实现CommonJS
是单个值导出,本质上导出的是exports属性CommonJS
可以动态加载,对每一个加载都存在缓存,可以有效的解决循环引用的问题CommonJS
模块同步加载并执行模块文件
4.2、es module
ES Module
是静态的,不能放在块级作用域内,代码发生在编译时ES Module
的值是动态绑定的,可以通过导出方法修改,可以直接访问修改结果ES Module
可以导出多个属性和方法,可以单个导入导出,混合导入导出ES Module
提前(编译时)加载模块并执行模块文件,容易实现Tree Shaking 和 Code Splitting
四、其他模块规范
CommonJS:
Node.js的模块化规范,通过require引入模块,通过module.exports导出模块。(2009)
一个单独文件,就是一个模块
通过require引入模块,通过module.exports导出模块。
模块是同步加载的,适用于服务器端。(运行时加载)
模块加载时,会缓存模块的导出结果,再次加载时直接返回缓存结果。
模块导出的是值的浅拷贝,模块内部修改导出的值不会影响外部。
js// math.js function add(a, b) { return a + b; } module.exports = { add: add } // main.js const math = require('./math.js'); console.log(math.add(1, 2)); // 3
AMD:
RequireJS实现的模块化规范,通过define定义模块,通过require引入模块。(2010)
通过define定义模块,通过require引入模块。
模块是异步加载的,适用于浏览器端。
AMD兼容CommonJS规范。
延迟加载,按需加载。
// 定义 有依赖模块
// a.js
define([], function() {
const message = "Hello from Module 1";
return {
message,
};
})
// 引入 a.js
require(['./a.js'], function(a) {
console.log(a.message); // Hello from Module 1
})
CMD:
SeaJS实现的模块化规范,通过define定义模块,通过require引入模块。(2011)
通过define定义模块,通过require引入模块。
模块是异步加载的,适用于浏览器端。
CMD兼容CommonJS规范。
依赖就近
加载方式:seajs.use('./app.js')
js// a.js define(function(require, exports, module) { const message = "Hello from Module 1"; module.exports = { message }; }); // app.js define(function(require) { const a = require('./a'); console.log(a.message); return { name: '小易' } });
html<!-- // index.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>CMD Example</title> <script src="https://cdnjs.cloudflare.com/ajax/libs/seajs/3.0.0/sea.js"></script> </head> <body> <h1>Check the console for output</h1> <script> seajs.use('./app.js', app => { console.log(app.name) // 小易 }); </script> </body> </html>
UMD:
通用模块定义规范,兼容CommonJS、AMD和全局变量方式。(2012)
模块是异步加载的。
UMD兼容CommonJS和AMD规范,都不满足 => 全局变量方式。
适用于浏览器和Node.js环境
js(function (root, factory) { if (typeof define === 'function' && define.amd) { // AMD 模块系统 (RequireJS) define(factory); } else if (typeof module === 'object' && module.exports) { // CommonJS 模块系统 (Node.js) module.exports = factory(); } else { // 作为全局变量暴露 root.myModule = factory(); } }(this, function () { // 模块代码 const message = "Hello, UMD!"; return { getMessage: function() { return message; } }; }));
ES6 Module:
ES6引入的模块化规范,通过import引入模块,通过export导出模块。(2015)
通过export导出模块,通过import引入模块。
模块是静态加载的,在编译时确定模块的依赖关系。
适用于浏览器和Node.js环境(v6.10.3 版本就开始支持)。
不同于CommenJS,ES6 Module输出的是值的引用,而不是浅复制。
ES6 Module是官方提供的模块化方案,,其它都是社区实现的。(ES2015)
指定加载某个输出值,而不是整个模块,有利于代码分割和tree shaking。
import 导入的的变量存在 声明提升