JS模块化发展背景与历史变迁
无模块化 --> 语法侧优化IIFE --> 框架层面CommonJS --> 异步AMD --> 按需加载CMD
--> 工程化、组件化
背景
JS本身简单的页面设计:页面动画 + 表单提交并无模块化 or 命名空间的概念
JS的模块化需求日益增长
幼年期:无模块化时期
-
开始需要再页面中增加一些不同的JS文件:动画、表单、格式化
-
多种js文件为了可读性和可维护性被分在不同的文件中
-
不同的文件又被同一个模版引用
<script scr='jquery.js'></script><script scr='main.js'></script><script scr='dep1.js'></script> // ....此方式是被认可的:
文件分离是模块化的第一步
问题出现:
- 污染全局作用域 > 不能写重名函数等造成冲突 => 不利于大型项目的开发以及多人团队的共建
成长期:模块化的雏形 - IIFE(语法侧的优化)
本质是:作用域的把控
🌰:
// 定义一个全局变量
let conut = 0;
// 代码块1
const increase = () => ++count;
// 代码块2
const reset = () => {
count = 0;
}
increase();
reset();
// 下一次调用时,总会被reset
利用函数块级作用域
(() => {
let count = 0;
// ...
})
仅定义了一个函数,如果立即执行
(() => {
let count = 0;
//...
})();
初步实现了最最简单的模块
利用此思想尝试去定义一个最简单的模块
面试题:利用函数作用域控制一个简单的模块
// 封装:
const iifeModule = (() => {
let count = 0;
return {
increase: () => ++count;
reset: () => {
count = 0;
}
}
})();
// 使用:
iifeModule.increase();
iifeModule.reset();
- 对外暴露了方法
- 变量没有污染全局作用域
追问:有额外依赖时,如何优化IIFE相关代码? ==> 参数调配
优化1: 依赖其他模块的IIFE
const iifeModule = ((dependencyModule1, dependencyModule2) => { let cont = 0; return { increase: () => ++count; reset: () => { count = 0; } } })(dependencyModule1, dependencyModule2); iifeModule.increase(); iifeModule.reset();源于jquery的解决办法
面试:了解早期jquery的依赖处理以及模块加载方案嘛?/ 了解IIFE是如何解决多方依赖的问题
答:IIFE + 传参调配
实际上,jquery等框架其实应用了revealing的写法:思想上为强调API的使用方式和接口,不会暴露内部内容 revealing 模块模式:只返回一个对象,其属性是私有数据和成员的引用
const iifeModule = ((dependencyModule1, dependencyModule2) => {
// 内部内容不会被第三方知道
let cont = 0;
const increase = () => ++count;
const reset = () => {
count = 0;
}
// 只暴露接口:
return {
increase, reset
}
})(dependencyModule1, dependencyModule2);
iifeModule.increase();
iifeModule.reset();
成熟期:
CJS - CommonJS
Node.js制定的一套方案,特征:
- 通过module + exports 对对外暴露接口
- 通过require来调用其他模块
模块组织方式:三大块:引入、处理、暴露
main.js文件
// 引入部分
const dependencyModule1 = require('./dependencyModule1');
const dependencyModule2 = require('./dependencyModule2');
// 处理部分
let cont = 0;
const increase = () => ++count;
const reset = () => {
count = 0;
}
// 做一些跟引入依赖相关事宜...
// 暴露接口部分
exports.increase = increase;
exports.reset = reset;
module.exports = {
increase, reset;
}
模块使用方式:
const { increase, rest } = require('./main.js')
increase();
reset();
可能被问到的问题:
实际执行处理:
应用场景:公司减少第三方依赖,自己写框架
(function (thisValue, exports, require, module) {
const dependencyModule1 = require('./dependencyModule1');
const dependencyModule2 = require('./dependencyModule2');
// 业务逻辑...
}).call(thisValue, exports, require, module);
- 优点:CommonJS率先在服务端实现了,从框架层面解决依赖、全局变量污染问题
- 缺点:CommonJS主要针对了服务端的解决方案 --> 同步引入加载,对于异步拉取依赖的处理不是那么友好
追问:为什么没有?
同步加载是没有问题的,比较快,硬盘端读写
优化异步:import require require.ensure
新的问题 -- 异步依赖
AMD规范
通过异步加载 + 允许制定回调函数
经典实现框架:require.js
新增定义方式:提供了两种方式:define,require
// 通过define来定义一个模块,利用require 进行加载
/*
define
pramas: 模块名,依赖模块,工厂方法
*/
define(id, [depends], callback);
require([module], callback);
模块定义方式:依赖1、依赖2加载完成之后再去走cb业务逻辑,即便是异步
define('amdModule', ['dependencyModule1', 'dependencyModule2', (dependencyModule1, dependencyModule2) => {
// 业务逻辑
let cont = 0;
const increase = () => ++count;
const reset = () => {
count = 0;
}
return {
increase, reset
}
})
引入模块:引入的部分也是正常处理
require(['amdModule'], amdModule => {
require.increase();
})
面试题2: 如果在AMDmodule中想兼容已有代码,怎么办?
答:IIFE可以,也可以:
define('amdModule', [], require => {
// 引入部分
const dependencyModule1 = require(./dependencyModule1);
const dependencyModule2 = require(./dependencyModule2);
// 处理部分
let cont = 0;
const increase = () => ++count;
const reset = () => {
count = 0;
}
// 做一些跟引入依赖相关事宜...
return {
increase, reset
}
})
面试题3: AMD中使用revealing?
// define(id, [], (require, export, module))
define((require, export, module) => {
// 引入部分
const dependencyModule1 = require(./dependencyModule1);
const dependencyModule2 = require(./dependencyModule2);
// 处理部分
let cont = 0;
const increase = () => ++count;
const reset = () => {
count = 0;
}
// 做一些跟引入依赖相关事宜...
export.increase = increase();
export.reset = reset();
module.exports = {
increase, reset;
}
})
define(id, [], require => {
const otherModule = require('amdMnodule');
otherModule.increase();
otherModule.reset();
})
面试题4:兼容AMD&CJS / 如何判断是AMD还是CJS
判断条件中 typeof module === "object" && module.exports 判断是否是模块化的东西;二者区别在于CJS没有define方法
UMD的出现
// AMD define
(define('amdModule', ['dependencyModule1', 'dependencyModule2', (dependencyModule1, dependencyModule2) => {
// 业务逻辑
let cont = 0;
const increase = () => ++count;
const reset = () => {
count = 0;
}
return {
increase, reset
}
}))(
// 目标是一次性区分CJS和AMD,一个异步函数,一个直接引入
typeof module === "object"
&& module.exports
&& typeof define !== "function" // 只有AMD用define去定义异步模块
? // 是CommonJS,直接执行工程函数
factory => module.exports = factory(require, exports, moudle)
: // 是AMD
define
) // 立即执行,完整的操作
优点:适合在浏览器中加载异步模块,可以并行加载多个
缺点:会有引入成本,不能按需加载
CMD 规范
按需加载:代码A执行的时候再加载依赖A
主要应用的框架:sea.js
define('module', [], (require, exports, module) => {
// 不在初始化的时候加载依赖,而是使用的时候加载
let $ = require('jquery');
// jquery 相关逻辑
let dependencyModule1 = requre('./dependencyModule1');
// dependencyModule1相关逻辑
})
- 优点:按需加载,依赖就近
- 缺点:依赖于打包,同时加载逻辑存在于每个模块中,会扩大模块的体积
面试题5: AMD & CMD区别
答:依赖就近,按需加载
ES6模块化
走向新时代
新增定义:
引入关键字 -- import
到处关键字 -- export
模块引入、导出和定义的地方:
// 引入区域
import dependencyModule1 from './dependencyModule1.js'
import dependencyModule2 from './dependencyModule2.js'
// 实现代码逻辑
let count = 0;
const increase = () => ++count;
const reset = () => {
count = 0;
}
// 导出区域
// 方式1:
export const incrase = () => ++count;
export const reset = () => {
count = 0;
}
// 方式2:
export default { increase, reset }
模版引入的地方:
<script type='module' scr='esModule.js'></script>
node中:
import { increase, reset } from './esModule.mjs';
increase();
reset();
import esModule from './esModule.mjs'
esModule.incrase();
esModule.reset();
面试题6: import如何动态模块
本质上是在考察export promise
在new promise 中的回调去做
ES11原生解决方案:
import('./esModule.js').then(dynamicEsModule => {
dynamicEsModule.increase();
})
- 优点(重要性):通过一种最统一的形态去整合了JS的模块化
- 缺点(局限性):本质上还是做了一个运行时的依赖分析
解决模块化的新思路 - 前端工程化
背景
根本问题 - 运行时进行依赖分析
导致结果:前端的模块化处理方案依赖于运行时分析
解决方案:线下执行 - 打包/预编译的方式
grunt gulp webpack
假设:有个文件:
<!doctype html>
<script src="main.js"></script>
<script>
// 给构建工具一个标示位,可以替换的东西
require.config(__FRAME_CONFIC___)
</script>
<script>
require(['a', 'e'], ()=> {
// 业务处理
})
</script>
</html>
解析处理:
define('a', () => {
let b = require('b');
let c = require('c');
export.run = () {
// run
}
})
工程化实现
Step1: 扫描依赖关系表:
{
a: ['b', 'c'],
b: ['d'], // 假设依赖d
e: [] // 没依赖或者全局依赖
}
Step2:重新生成依赖数据模版:
<!doctype html>
<script src="main.js"></script>
<script>
// 构建工具生成数据
require.config({
"deps": {
a: ['b', 'c'],
b: ['d'], // 假设依赖d
e: [] // 没依赖或者全局依赖
}
})
</script>
<script>
require(['a', 'e'], ()=> {
// 业务处理
})
</script>
</html>
Step3:执行工具,采用模块化方案解决模块化处理依赖
define('a', ['b','c'], () => {
// 执行代码
export.run = function () {}
})
优点:
- 构建时生成配置,运行时执行
- 最终转化成执行处理依赖
- 可以拓展
预编译不再存在异步