前言
首先我需要了解一下,为什么需要前端模块化呢?
Web初期,也不需要什么项目打包,前端工程化只需要三个文件,HTML,JS,CSS,三个大模块,那时候就能很好的满足需求了。
后来业务需求不断累加,业务也日益复杂化,所以导致了JS代码日益膨胀,JS文件越来越大很难进行维护了,所以我们自然会想到,将JS代码进行切分,这里提到的切分 实际上就是对 JS 代码的模块化
目的
模块化主要是用来抽离公共代码,隔离作用域,避免变量冲突等。
模块化 代码管理/编译,业务分离的基本单元 模块化的本质上 是一种提供对外通信接口,进行代码切分/组成的管理方式,其呈现的方式因不同的模块化方案而不同,基本是以文件粒度区分
本质上就是 使用一个函数 将变量挂载 到函数上 返回出去
module.exports = xxx; // 变量 函数等等
模块化的演进过程
全局函数阶段
例子
function m1() {
// ...
}
function m2() {
// ...
}
问题
污染全局命名空间,容易引起命名冲突,而且模块成员之间看不出直接关系
NameSpace 命名空间阶段
🌰例子
const myModule = {
data: 'data',
do() {
console.log('do something')
},
};
myModule.data = null;
myModule.do(); // 执行方法
现代模块化规范
以文件为模块,有自己的作用域
在一个文件里面定义的变量,函数,类,都有私有的,对其他文件不可见
- CommonJS
- AMD
- UMD
CommonJS
适用于服务器端
特点:
- 文件作用域
- 缓存
- 同步加载
AMD
Asynchronous Module Deinition
AMD 代表的是肯定是 ReuquireJS requied('fs')
James Burke 觉得 CMJ 很好,但是在浏览器上玩不转,所以提出一个 AMD 规范,异步不会阻塞浏览器, 适用于客户端
- 文件作用域
- 非同步加载
define(id?, depencies?, factory); // id 模块,depencies 模块的内部依赖
define('foo', ['utils', 'bar'], function(utils, bar) {
utils.add(1, 2)
return {
name: 'foo'
}
})
UMD
Universal Module Deinition 前后端跨平台的模块化解决方案
实现原理
- 先判断是否支持
Node.js模块格式 (exports 是否存在),存在就 Node.js 模块格式 - 再判断是否支持
AMD(define是否存在),存在则使用 AMD 方式加载模块 - 前两个都不存在,则将模块公开到全局(window 或者 global)
发展历程总结
字节模块化真题 实现一个符合 AMD 的 require.js
- 实现一个 可移植直接配置依赖路径
RequestJs.config({ paths: {
'jQuery': 'https://www.jQuery.cdn.min.js'
} }) // 传入一个 路劲对象 然后 注册
RequestJs(['jQuery'], function(jquery) {
// ...
})
- 加载模块
RequestJs(['jQuery'], function(jquery) {
// ...
})
- 定义模块
RequestJs('jQuery', [], function(jquery) {
// ...
})
1. 例子行为
// RequiredJS
define('a', function() {
console.log('a load')
return {
run: function() {console.log('a run')}
}
})
define('b', function() {
console.log('b load')
return {
run: function() {console.log('a run')}
}
})
require(['a', 'b'], function(a, b) {
console.log('main run') // 这行不是最先执行的 先执行 a,b,main,run
a.run(); // a 模块加载
b.run(); // b 模块加载
})
需要实现的内容
- require 时候加载依赖的模块 require 的时候就把数组中的内容加载进来了
- 然后执行回调时候的函数
- 最后执行 define 中 return 的内容
开始实现
2. 定义 define
const def = new Map(); // Map 数据结构
// 定义模块,触发时机 是在 require 时候 所以 -> 收集
rjDefine = (name, deps, factory) => {
// todo 参数之间判断 互换等等
def.set(name, { name, deps, factory }); // 存在一个 map 结构的对象中
}
3. 使用 define
rjDefine('a', ['lodash'], function() {
console.log('moduleA load');
return {
str: function() {
console.log('module A return data')
return _.repeat('>>>>>', 20)
}
}
})
4. require 加载资源方法
// 加资源拼接加载到 HTML 上
const __load = url => {
return new Promise((resolve, reject) => {
const head = document.getElementsByTagName('head')[0];
const node = document.createElement('script');
node.type = 'text/javascript'
node.src = url;
node.async = true;
node.onload = resolve;
node.onerror = reject;
head.appendChild(node);
})
}
// 触发加载的地方
rjRequire = (deps, factory) => {
return new Promise((resolve, reject) => {
Promise.all(deps.map(dep => {
// 加载依赖 判断当前的依赖是否来自于 CDN的
if (defaultOptions.paths[dep]) {
console.log(defaultOptions.paths[dep])
return __import(defaultOptions.paths[dep]);
}
return __load(__getUrl(dep)).then(() => {
const { deps, factory } = def.get(dep);
if (deps.length === 0) {
return factory(null); // 如果获取依赖的时候 他没有依赖其他的库
}
// 如果他依赖了其他的库 define的时候 依赖了 其他的库
// 执行递归
return rjRequire(deps, factory)
});
})).then(resolve, reject)
}).then(instances => factory(...instances))
}
5. 结果
<!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>hello amd</title>
<script src="https://cdn.bootcdn.net/ajax/libs/systemjs/6.10.2/system.js"></script>
<script src="../rj.js"></script>
</head>
<body>
<div id="app"></div>
<div id="app2"></div>
<script>
rj.config({
paths: {
lodash: 'https://cdn.bootcdn.net/ajax/libs/lodash.js/4.17.21/lodash.js'
}
})
rjRequire(['lodash', 'a'], function(_, a) {
console.log('require run');
document.querySelector('#app').innerHTML = _.random(5)
document.querySelector('#app2').innerHTML = a.str();
})
</script>
</body>
</html>