
演进历程
无模块化
面临问题
- 需要在页面中加载不同的 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 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 中,
this
,exports
,module.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;
|
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
| 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, };
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);
(function (c) {})(window);
|
优点: CommonJs 率先在服务端实现了,从框架层面解决了依赖、全局变量污染的问题
缺点: 针对了服务端的解决方案。异步拉取依赖处理不是很完美
AMD
通过异步加载 + 允许制定回调函数
经典实现框架: require.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| define(id, [depends], callback);
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
| (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(); })( typeof module === "object" && module.exports && typeof define !== "function" ? factory => module.exports = factory(require, exports, module) : define )
|
优点: 适合在浏览器中加载异步模块的方案
缺点: 引入成本;回调无法做到绝对的异步
CMD
按需加载,异步运行
主要应用框架: sea.js
1 2 3 4 5 6 7
| define("module", (require, exports, module) => { let $ = require("jquery");
let dependencyModule1 = require("./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
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口
- CommonJS 模块的 require()是同步加载模块,ES6 模块的 import 命令是异步加载,有一个独立的模块依赖的解析阶段
- CommonJS 是对模块的浅拷贝,ES6 Module 是对模块的引入,即 ES6 Module 只存只读,不能改变其值,具体点就是指针指向不能变,类似 const 。commonjs 的 this 指向当前模块,ES6 的 this 指向 undefined
- 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 框架的组件化 + 设计模式