自从 JavaScript 诞生以来,人们一直将它作为一种网页脚本语言,一般只用作表单校验跟页面特效,由于被仓促的创造出来,所以它自身的各种陷阱和缺点也被编程人员所诟病。随着 Web2.0 的流行,各种前端库和框架被开发出来,随后随着更多的用户需求在前端被实现,JavaScript 也从表单校验跃迁到应用开发的级别上。在这个过程中,它大致经历了工具类库、组件库、前端框架、前端应用的变迁。
随着业务发展,为了能够更好的组织业务逻辑,JavaScript 有了模块化的需求。
一般的,我们在 JavaScript 中通过 script 标签引入代码的方式来实现模块化,但这样往往缺乏组织能力跟约束能力,对于开发者而言需要人为的对代码进行约束,避免变量、函数冲突等问题。
模块化的理解
什么是模块化
- 将代码按照功能进行封装,遵循单一职责。
- 模块内部数据私有, 通过向外暴露的变量、方法与外部通信。
模块化的好处
- 避免命名冲突(减少命名空间污染)
- 更好的分离, 按需加载
- 更高复用性
- 高可维护性
模块化方案
- 全局 function 模式,根据功能将模块封装成不同的全局函数。
弊端:污染全局命名空间,容易造成命名冲突,模块之间的关系模糊COPY1
2
3
4
5
6
7
8
9
10
11
12funtion defaultValue(){
return {
a: 2
}
}
function sum(a, b){
return a + b
}
const value = defaultValue();
const a = sum(value.a, 2) - nameSpace 模式,对对象进行封装。减少全局变量,解决命名冲突。
弊端:数据不安全,外部可以直接对模块内的数据进行修改COPY1
2
3
4
5
6
7
8
9
10
11
12
13
14
15window.__commonFuc = {
a: 1,
defaultValue(){
xxx
},
sum(a,b){
return a + b
}
}
const module = window.__commonFuc
const data = module.defaultValue()
console.log(module.x) // 1
module.x = 2 // 模块内的数据被修改 - IIFE 立即执行函数,数据是私有的, 外部只能通过暴露的方法操作。通过闭包实现
弊端:无法处理模块相互依赖的场景COPY1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32(function(window){
var x = 1;
function api(){
xxx
}
function setX(v){
x = v
}
function getX(){
return x
}
window.__Module = {
x,
setX,
getX,
api,
}
})(window)
const m = window.__Module
// 修改函数作用域内变量的值
m.setX(10)
console.log(m.getX()) // 10
// 这里改的是对象属性的值,不是修改的模块内部的data
m.x = 2
console.log(m.getX()) // 10
模块化规范
CommonJs
概述
CommonJs 规范主要用于 Node.js,每一个文件就是一个模块,有自己的作用域,每个文件中的变量、函数、类都是私有的。
特点
- 导出的是值的拷贝
- 每个文件就是一个模块,独立作用域,不污染全局作用域
- 根据导入的顺序加载
- 支持缓存,模块可以多次加载,但只会在首次加载时运行一次
使用
导出:使用 module.export 导出,也可以简写为 exports。CommonJS 规范规定,每个模块内部,module 变量代表当前模块。这个变量是一个对象,它的 exports 属性(即 module.exports)是对外的接口。加载某个模块,其实是加载该模块的 module.exports属性。
COPY1
2
3
4
5
6
7
8
9
10
11
12
13// module.export 导出
function add(a, b) {
return a + b;
}
module.export = {
add: add,
};
// exports 导出
export.add = function(a, b){
return a + b;
}导入:使用 require 同步加载模块
COPY1
2
3// 导入
var math = require("./math");
math.add(2, 5);模块重命名
COPY1
2// 使用新的求和方法 newAdd
let { add: newAdd } = require("./math");
加载机制
CommonJS 模块的加载机制是,输入的是被输出的值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。这部分跟 ES6 模块化有所不同。在服务器端运行时同步加载,在浏览器端提前编译打包处理方式
1 | // lib.js |
1 | // main.js |
在执行 incCounter 前后,lib.js 中的 counter 始终为 3,可以得知 CommonJS 输出的是值的拷贝。
ES6 模块化
概述
ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,旨在成为浏览器和服务器通用的模块解决方案。其模块功能主要由两个命令构成:export 和 import。export 命令用于规定模块的对外接口,import 命令用于输入其他模块提供的功能。
特点
- 浏览器环境默认支持
- 输出的是值的引用
- 动态引用,不会缓存值
- 静态化,编译时就能确定模块间的依赖关系,以及输入输出的变量,可以 tree shaking
使用
- 导出:使用 export 导出
1 | // math.js |
- 导入:使用 import 导入
1 | import { add, total } from "./math"; |
- 模块重命名
1 | import * as newAdd from "./math"; |
加载机制
模块加载方式取决于所处的环境,Node.js 环境中同步加载,浏览器端异步加载
原理
ES Module
从加载入口模块到所有模块实例的执行主要经历了三步:构建、实例化和运行。
在构建时,根据 import
分析模块间的依赖关系,这点不同于 CommonJS
,在浏览器中运行时加载会主线程长期被占用,ES Module
在构建过程不会实例化和执行任何的js代码,也就是所谓的 静态解析 过程。
但是但是,在日常开发中我们发现以CommonJs
模式引入的插件也可以跑在浏览器中,这是为什呢?
概括的来说,webpack
会根据webpack.config.js入口文件识别模块依赖,统一对模块进行分析,通过转换、编译,打包成最终的文件。最终文件中的模块实现是基于webpack自己实现的 webpack_require
(es5代码),所以打包后的文件可以跑在浏览器上。即 webpack会对各种模块进行语法分析,并做转换编译
来看看webpack
对此做了些什么:
简单的对webpack进行配置:
COPY1
2
3
4
5
6
7
8
9
10
11
12// webpack.config.js
const path = require('path');
module.exports = {
mode: 'development',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, './dist'),
}
};创建一个EsModule格式的模块并导入
COPY1
2
3
4
5
6
7
8
9
10
11
// src/add
export default function(a, b) {
let { name } = { name: 'hello world,'};
return name + a + b;
}
// src/main.js
import add from './add'
console.log('求和', add(1, 2))打包完成后的
bundle.js
文件输出如下:
1 |
|
至此,打包后的代码直接跑在浏览器中,是因为webpack通过__webpack_require__ 函数模拟了模块的加载,并使用module.exports
导出。
import()
import()
函数在 ES2020 增加的可以异步动态加载模块的机制,import()
函数与所加载的模块没有静态连接关系,所以不能进行静态解析,也不能进行tree shaking,这点也是与import语句不相同之一。import()
的返回值是promise对象,可以使用 .then
和.catch
方法进行接收数据处理,import()
加载模块成功以后,这个模块会作为一个对象,当作 then
方法的参数。因此,可以使用对象解构赋值的语法,获取输出接口。
允许模块路径动态生成import()
可以放在任何地方,因为它是运行时执行的,什么时候执行到它,就什么时候进行指定模块的加载,所以它可以在条件语句和函数中进行动态的加载。
1 | if(true){ |
简单使用:
定义一个需要按需加载的模块 a.js
COPY1
2
3export default function a() {
console.log('我是模块 a');
}引入模块 a
COPY1
2
3import('./a').then(({ default: a }) => {
console.log(a);
});