当然还有:AMD CMD (这里暂时就不详细说明)
CommonJS
的提出,弥补 Javascript 对于模块化,没有统一标准的缺陷。nodejs 借鉴了 CommonJS
的 Module ,实现了良好的模块化管理。
目前 CommonJS
广泛应用于以下几个场景:
Node
是 CommonJS 在服务器端一个具有代表性的实现;
我们很多的依赖包,都是基于CommonJS模块化规范去做的。
Browserify
是 CommonJS 在浏览器中的一种实现;
webpack
打包工具对 CommonJS 的支持和转换;也就是前端应用也可以在编译之前,尽情使用 CommonJS 进行开发。
在使用 CommonJS
规范下,有几个显著的特点。
在 commonjs
中每一个 js 文件都是一个单独的模块,我们可以称之为 module;
该模块中,包含 CommonJS 规范的核心变量: exports、module.exports、require;
exports 和 module.exports 可以负责对模块中的内容进行导出;
require 函数可以帮助我们导入其他模块(自定义模块、系统模块、第三方库模块)中的内容;
// main.js
var a = require('./a')
console.log('打印', a) // 打印 { value: '你好', say: [Function: say] }
// a.js
var value = '你好'
function say() {
console.log('你好呀')
}
exports.value = value
exports.say = say
// 实际上是在外层包装了一层,传入了 exports,require,module,__filename,__dirname 这些变量。
(function(exports,require,module,__filename,__dirname){
const sayName = require('./hello.js')
module.exports = function say(){
return {
name:sayName()
}
}
})
① 为 nodejs 底层的核心模块。(例如 fs path)
② 为我们编写的文件模块,比如上述 sayName
(绝对或相对路径的文件)
③ 为我们通过 npm 下载的第三方自定义模块,比如 crypto-js
。 (第三方依赖库)
缓存的模块(require过一次就会被缓存起来)
核心模块,绝对或相对路径文件模块(直接读取)
第三方模块
- 优先当前目录下的 node_modules
- 查找上一级文件夹直到根目录
- 找到 node_modules 查找同一名称的文件包。
- 查找对应包的
(package.json的main属性指向的文件
>index.js
>index.json
>index.node
)
require()
的文件未被缓存 ,就执行对应的文件;main.js
console.log('main1')
var a = require('./a')
console.log('main2')
var b = require('./b')
a.say()
console.log('main2')
a.js
const b = require('./b')
console.log('我是 a 文件', b)
exports.say = function () {
const getMes = require('./b')
const message = getMes()
console.log(message)
}
b.js
const say = require('./a')
const object = {
name: '你好呀',
}
console.log('我是 b 文件')
console.log('打印 a 模块', say)
setTimeout(() => {
console.log('异步打印 a 模块', say)
}, 0)
module.exports = function () {
return object
}
》 node ./main.js
看下输出结果
main1
我是 b 文件
打印 a 模块 {}
我是 a 文件 [Function]
main2
{
name: '你好呀',
}
main2
异步打印 a 模块 { say: [Function] }
why?
执行顺序可参考1.5.3的内容,CommonJS同步执行,其实也蛮好理解的。
先啰嗦几句:
s
。先打印看看
console.log(module.exports) // {}
console.log(exports) // {}
console.log(module.exports === exports) // true
这两个变量 全等于,那为什么会有两种写法?
// main.js
var a = require('./a')
var b = require('./b')
console.log('a', a) // a {}
console.log('b', b) // {name:"新的b对象"}
// a.js
exports={name:"新的a对象"}
// b.js
module.exports={name:"新的b对象"}
why? 其实这个地方很好理解,可以这样理解:
我们实际导出的是 `module.exports`
然后我们在当前模块又声明一个变量 `exports = module.exports ` 赋值了值引用。
所以两者 全等于
但是当我们 exports={name:"新的a对象"}, 改变了它的值引用,所以上述示例中,读取的是空对象。
其次,既然导出的是 `module.exports`, 那么可以不仅仅局限于导出对象,还可以导出其他类型的变量,例如数组 函数 字符串等等
我个人理解CommonJS是如何解决开头提到的痛点:
- 每个模块的变量都是独立的(因为有包装函数),输出数据包裹在对象中,来解决命名冲突的问题。
- 其次除了暴露的数据,内部变量私有,外部无法直接修改。
- 代码同步执行,配合上缓存,来解决依赖之间错综复杂的关系。
输出的数据和模块数据之间的关系?
// main.js
var a = require('./a')
console.log('a', a) // a { value: 1, obj: { a: 1 }, add: [Function: add] }
a.add()
console.log('a', a) // a { value: 1, obj: { a: 2 }, add: [Function: add] }
/* 我理解这里的导出的赋值逻辑类似于`=`; 原始数据类型赋复制的值 对象数据类型赋引用地址 */
// a.js
var value = 1
var obj = {
a: 1,
}
function add() {
value++
obj.a++
}
module.exports = {
value,
obj,
add,
}
Nodejs
借鉴了 Commonjs
实现了模块化 ,从 ES6
开始, JavaScript
才真正意义上有自己的模块化规范,
ES Module 的产生有很多优势,比如:
Es Module
的静态导入导出的优势,实现了 tree shaking
。Es Module
还可以 import()
懒加载方式实现代码分割。在 Es Module
中用 export
用来导出模块,import
用来导入模块。
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documenttitle>
head>
<body>
<script src="./main.js" type="module">script>
body>
html>
// main.js
import { increment, count } from './a.js'
increment()
console.log(count) // 2
// a.js
export let count = 1
export function increment() {
count++
}
ES Module 的引入和导出是静态的;
import
会自动提升到代码的顶层 ;
import
, export
不能放在块级作用域或条件语句中;
import
的导入名不能为字符串或在判断语句;
// 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')
}
解释:
1. 入口main.js
2. main.js 的 import 语句提升到顶部
3. (main文件的import say from './a')深度优先遍历,先进入到 `./a`
4. (a文件的import b from './b')深度优先遍历,先进入到 `./b`
5. 运行b.js 打印 `b模块加载`
6. 导出一个sayhello函数,返回
7. 继续执行a.js 打印 `a模块加载`
8. 导出say函数返回
9. 继续执行main.js, (main文件的import say1 from './b') 已经引入过了,直接跳过
9. 继续执行main.js,打印 `main.js开始执行`&&`main.js执行完毕`
输出:
b模块加载
a模块加载
main.js开始执行
main.js执行完毕
使用 import 被导入的模块运行在严格模式下。
使用 import 被导入的变量是只读的,可以理解默认为 const 装饰,无法被赋值
使用 import 被导入的变量是与原变量绑定/引用的,可以理解为 import 导入的变量无论是否为基本类型都是引用传递。
支持动态引入,import()
。import()
返回一个 Promise
对象, 返回的 Promise
的 then 成功回调中,可以获取模块的加载成功信息。
ES6 模块会在程序开始前先根据模块关系查找到所有模块,生成一个无环关系图,并将所有模块实例都创建好,这种方式天然地避免了循环引用的问题,当然也有模块加载缓存,重复 import 同一个模块,只会执行一次代码。
Commonjs
的特性如下:
Es module
的特性如下:
https://juejin.cn/post/6994224541312483336
https://juejin.cn/post/6844904080955932680
最后提一个问题,打包工具例如 webpack 如何应对这么多种模块化规范的呢?