start
为什么要模块化
- 名称污染
- 依赖管理
主流的模块化
- CommonJS
- ES Module
当然还有:AMD CMD (这里暂时就不详细说明)
1. CommonJS
1.1 基础知识
CommonJS
的提出,弥补 Javascript 对于模块化,没有统一标准的缺陷。nodejs 借鉴了 CommonJS
的 Module ,实现了良好的模块化管理。
目前 CommonJS
广泛应用于以下几个场景:
-
Node
是 CommonJS 在服务器端一个具有代表性的实现;我们很多的依赖包,都是基于CommonJS模块化规范去做的。
-
Browserify
是 CommonJS 在浏览器中的一种实现; -
webpack
打包工具对 CommonJS 的支持和转换;也就是前端应用也可以在编译之前,尽情使用 CommonJS 进行开发。
1.2 显著特点
在使用 CommonJS
规范下,有几个显著的特点。
-
在
commonjs
中每一个 js 文件都是一个单独的模块,我们可以称之为 module; -
该模块中,包含 CommonJS 规范的核心变量: exports、module.exports、require;
-
exports 和 module.exports 可以负责对模块中的内容进行导出;
-
require 函数可以帮助我们导入其他模块(自定义模块、系统模块、第三方库模块)中的内容;
1.3 基础使用
// 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
1.4 CommonJS 模块整体是如何运行的。
// 实际上是在外层包装了一层,传入了 exports,require,module,__filename,__dirname 这些变量。
(function(exports,require,module,__filename,__dirname){
const sayName = require('./hello.js')
module.exports = function say(){
return {
name:sayName()
}
}
})
1.5 require
1.5.1 require 加载的文件类别
① 为 nodejs 底层的核心模块。(例如 fs path)
② 为我们编写的文件模块,比如上述 sayName
(绝对或相对路径的文件)
③ 为我们通过 npm 下载的第三方自定义模块,比如 crypto-js
。 (第三方依赖库)
1.5.2 require 的查找逻辑
-
缓存的模块(require过一次就会被缓存起来)
-
核心模块,绝对或相对路径文件模块(直接读取)
-
第三方模块
- 优先当前目录下的 node_modules
- 查找上一级文件夹直到根目录
- 找到 node_modules 查找同一名称的文件包。
- 查找对应包的
(package.json的main属性指向的文件
>index.js
>index.json
>index.node
)
1.5.3 require 模块引入与处理(代码执行的顺序,同步执行)
- 主文件,从上往下执行。
require()
的文件未被缓存 ,就执行对应的文件;- 如果有缓存,就不进入这个文件,继续向后执行。
1.5.4 测试
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同步执行,其实也蛮好理解的。
1.6 exports
先啰嗦几句:
- 单词请记好,加
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`, 那么可以不仅仅局限于导出对象,还可以导出其他类型的变量,例如数组 函数 字符串等等
1.7 个人总结
我个人理解CommonJS是如何解决开头提到的痛点:
- 每个模块的变量都是独立的(因为有包装函数),输出数据包裹在对象中,来解决命名冲突的问题。
- 其次除了暴露的数据,内部变量私有,外部无法直接修改。
- 代码同步执行,配合上缓存,来解决依赖之间错综复杂的关系。
1.8 其他情况的试验
输出的数据和模块数据之间的关系?
// 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,
}
2. ES Module
2.1 基础知识
Nodejs
借鉴了 Commonjs
实现了模块化 ,从 ES6
开始, JavaScript
才真正意义上有自己的模块化规范,
ES Module 的产生有很多优势,比如:
- 借助
Es Module
的静态导入导出的优势,实现了tree shaking
。 Es Module
还可以import()
懒加载方式实现代码分割。
在 Es Module
中用 export
用来导出模块,import
用来导入模块。
2.2 基本使用
<!-- index.html -->
<!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>Document</title>
</head>
<body>
<!-- 注意这里要加一个 type="module" -->
<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++
}
2.3 ES Module 如何运行
2.3.1 注意事项
-
ES Module 的引入和导出是静态的;
-
import
会自动提升到代码的顶层 ; -
import
,export
不能放在块级作用域或条件语句中; -
import
的导入名不能为字符串或在判断语句;
2.3.2 如何执行
// 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 总结
Commonjs
的特性如下:
- CommonJS 模块由 JS 运行时实现。
- CommonJS 是单个值导出,本质上导出的就是 exports 属性。
- CommonJS 是可以动态加载的,对每一个加载都存在缓存,可以有效的解决循环引用问题。
- CommonJS 模块同步加载并执行模块文件。
ES Module 总结
Es module
的特性如下:
- ES6 Module 静态的,不能放在块级作用域内,代码发生在编译时。
- ES6 Module 的值是动态绑定的,可以通过导出方法修改,可以直接访问修改结果。
- ES6 Module 可以导出多个属性和方法,可以单个导入导出,混合导入导出。
- ES6 模块提前加载并执行模块文件,
- ES6 Module 导入模块在严格模式下。
- ES6 Module 的特性可以很容易实现 Tree Shaking 和 Code Splitting。
优质的模块化相关的博客
-
https://juejin.cn/post/6994224541312483336
-
https://juejin.cn/post/6844904080955932680
end
- 看了很多优质的博客,才对模块化有一个初步的认识。
- 使用一个东西,肯定是希望能够借助它,解决我们遇到的痛点。模块化的出现,其实就是为了解决命名冲突和依赖管理。
- 学到这里,暂时就了解了两个模块化规范。
最后提一个问题,打包工具例如 webpack 如何应对这么多种模块化规范的呢?