演进历程

无模块化

面临问题

  • 需要在页面中加载不同的 JS: 动画、组件、格式化
  • 多种 js 文件会被分在不同的文件中
  • 不同的文件又被同一个模板所引用

手动拆分各文件

IIFE

作用域

利用函数的块级作用域 - 隔离区

1
2
3
4
5
6
7
8
9
const iifeModule = (() => {
let count = 0;
const increase = () => ++count;
const reset = () => {
count = 0;
};
console.log(count);
increase();
})();

问题 1: 独立模块本身的额外依赖,如何优化

依赖其他模块的传参型

1
2
3
4
5
6
7
8
9
const iifeModule = ((dependencyModule1, dependencyModule2) => {
let count = 0;
const increase = () => ++count;
const reset = () => {
count = 0;
};
console.log(count);
increase();
})(dependencyModule1, dependencyModule2);

问题 2: jquery 或者其他很多开源框架的模块加载方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const iifeModule = ((dependencyModule1, dependencyModule2) => {
let count = 0;
const increase = () => ++count;
const reset = () => {
count = 0;
};
console.log(count);
increase();
return {
increase,
reset,
};
})(dependencyModule1, dependencyModule2);
iifeModule.increate();
iifeModule.increate();

总结: 揭示模式 revealing => 上层无需了解底层实现,仅关注抽象 => 框架

模块化

Commonjs

  • 通过 require 去引入外部模块
  • 通过 module + exports 去对外暴露接口
  • 最开始 CJS 中,thisexportsmodule.exports 是同一个东西,最后导出 module.exports
  • 对模块的浅拷贝
  • 同步运行,不适合前端
1
2
3
4
5
6
7
8
9
this.a = 1;
exports.b = 2;
exports = { c: 3 };
module.exports = { d: 4 };
exports.e = 5;
this.f = 6;

// 最终结果: this: { a: 1, b: 2, f: 6} exports: {c: 3, e: 5} module.exports: { d: 4 }
// 最终导出: { d: 4 };
1
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
33
34
35
36
37
38
39
40
41
42
43
44
// main.js
const dependencyModule1 = require("./dependencyModule1");
const dependencyModule2 = require("./dependencyModule2");

let count = 0;
const increase = () => ++count;
const reset = () => {
count = 0;
};
console.log(count);
increase();

exports.increase = increase;
exports.reset = increase;

module.exports = {
increase,
reset,
};

// exe
const { increase, reset } = require("./main.js")(
// 复合使用
function (thisValue, exports, require, module) {
const dependencyModule1 = require("./dependencyModule1");
const dependencyModule2 = require("./dependencyModule2");

// 业务逻辑……
}
).call(thisValue, exports, require, module);

// 一些开源项目为何要把全局、指针以及框架本身引用作为参数
(function (window, $, undefined) {
const _show = function () {
$("#app").val("hi test");
};
window.webShow = _show;
})(window, jQuery);

// 阻断思路
// window - 1. 全局作用域转化成局部作用域,提升执行效率 2. 编译时优化
(function (c) {})(window); // window会被优化成c
// jquery - 1. 独立定制复写和挂载 2.防止全局串扰
// undefined - 防止重写

优点: CommonJs 率先在服务端实现了,从框架层面解决了依赖、全局变量污染的问题
缺点: 针对了服务端的解决方案。异步拉取依赖处理不是很完美

AMD

通过异步加载 + 允许制定回调函数
经典实现框架: require.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// define定义模块
define(id, [depends], callback);
// require进行加载
require([module], callback);

define("amdModule", ["dependencyModule1", "dependencyModule2"], (
dependencyModule1,
dependencyModule2
) => {
// 业务逻辑……
});

require(["amdModule"], (amdModule) => {
amdModule.increase();
});

问题 2: 如何在 AMDModule 中兼容已有代码

1
2
3
4
5
define("amdModule", [], (require) => {
const dependencyModule1 = require("./dependencyModule1");
const dependencyModule2 = require("./dependencyModule2");
// 业务逻辑……
});

手写兼容 CJS & AMD

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 判断关键step1. object还是function step2. exports? step3. define
(define('amdModule'), [], (require, export, module) => {
const dependencyModule1 = require('./dependencyModule1');
const dependencyModule2 = require('./dependencyModule2');

let count = 0;
const increase = () => ++count;
const reset = () => {
count = 0;
}
export.increase = increase();
})(
// 目标: 一次性区分CJS还是AMD
typeof module === "object"
&& module.exports
&& typeof define !== "function"
? // 是CJS
factory => module.exports = factory(require, exports, module)
: // AMD
define
)

优点: 适合在浏览器中加载异步模块的方案
缺点: 引入成本;回调无法做到绝对的异步

CMD

按需加载,异步运行
主要应用框架: sea.js

1
2
3
4
5
6
7
define("module", (require, exports, module) => {
let $ = require("jquery");
// jquery相关逻辑

let dependencyModule1 = require("./dependencyModule1");
// dependencyModule1相关逻辑
});

优点: 按需加载,依赖就近
缺点: 依赖打包,加载逻辑存在于每个模块中,扩大了模块体积,同时功能上依赖编译

ES6

import & export

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 引入区域
import dependencyModule1 from "./dependencyModule1";
import dependencyModule2 from "./dependencyModule2";

// 实现业务逻辑……
// 导出
export const increase = () => ++count;
export const reset = () => {
count = 0;
};

export default {
increase,
reset,
};

问题 1: 如何按需、动态加载模块:
es11 新特性 ==> Dynamic Module Imports

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import("./esModule.js").then((dynamicModule) => {
dynamicModule.increase();
});

// 在条件语句中动态导入
if (condition) {
import("./esModule.js").then((dynamicModule) => {
dynamicModule.increase();
});
}

// 函数内部动态导入
async function loadModule() {
try {
const module = await import("./esModule.js");
// 使用导入的模块
dynamicModule.increase();
} catch (error) {
console.error("模块加载失败:", error);
}
}

优点: 通过一种最终统一各端的形态,整合了 js 模块化的通用方案
局限性: 本质上还是运行时的依赖分析

ES6 vs CommonJS

  1. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口
  2. CommonJS 模块的 require()是同步加载模块,ES6 模块的 import 命令是异步加载,有一个独立的模块依赖的解析阶段
  3. CommonJS 是对模块的浅拷贝,ES6 Module 是对模块的引入,即 ES6 Module 只存只读,不能改变其值,具体点就是指针指向不能变,类似 const 。commonjs 的 this 指向当前模块,ES6 的 this 指向 undefined
  4. import 的接口是 read-only(只读状态),不能修改其变量值。 即不能修改其变量的指针指向,但可以改变变量内部指针指向。可以对 commonJS 对重新赋值(改变指针指向),但是对 ES6 Module 赋值会编译报错

新方案: 前端工程化

上述方案存在的根本问题: 运行时进行依赖分析
对此提出的解决方案: 编译时进行依赖分析

实现一个编译时依赖处理的思路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!doctype html>
<script src="main.js"></script>
<script>
// 给构建工具一个标识位
require.config(__FRAME_CONFIG__);
</script>
<script>
require(['a', 'e'], () => {
// 业务逻辑
})

define('a', () => {
let b = require('b')
let c = require('c')
})
</script>
</html>

工程化实现

扫描依赖关系表

1
2
3
4
5
{
a: ['b', 'c'],
b: ['d'],
e: []
}

根据依赖关系重制模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!doctype html>
<script src="main.js"></script>
<script>
// 构建工具生成数据
require.config({
"deps": {
a: ['b', 'c'],
b: ['d'],
e: []
}
});
</script>
<script>
require(['a', 'e'], () => {
// 业务逻辑
})

define('a', () => {
let b = require('b')
let c = require('c')
})
</script>
</html>

模块化解决方案处理

1
2
3
define('a', ['b', 'c'], () => {
export.run = () => {}
})

优点: 构建时生成配置,运行时去运行,最终转化成可执行的依赖处理,并可以拓展

完全体

webpack 为核心的前端工程化 + mvvm 框架的组件化 + 设计模式