前言
模块化是前端开发和工程化的重要组成部分,也是前端的基石。在早期没有前端的概念,更多是叫做 UED
(用户体验设计师)。鉴于前端功能比较简单,只需要完成与用户的基础交互即可。甚至很多站点基本都是静态页面,基本无需依赖数据完成交互,很多情况下基本由后端同学顺便写了
。
但随着时间的推移个人电脑的普及,底层计算机处理能力的提升,同时浏览器能力也日益强大,迫使 JavaScript
这门语言必须承担更多能力。
在 Web1.0
向着 Web2.0
大步迈进的时代,前端代码日趋膨胀,对于模块化的需求呼声越来越高。
所以我们也就能看到在 ESM
横空出世之前,出现了 CommonJS、AMD、CMD、UMD
这些主流的模块化设计思想,这是一个野蛮发展的时代。
温馨提示:码字不易,先赞后看,养成习惯!!!
1:什么是模块化
模块化是将代码解耦并且分割成一个个文件进行管理,将变量私有化,每个模块只对外暴露指定的 API
接口。
2:什么是模块
模块就是完成特定功能的单元。在开发场景中,一个模块就是实现特定功能的文件。
3:模块化的远古时期
3.1:函数时代
早期我们为了实现最简单的模块化,开发中大量使用到函数来实现。毕竟函数是 JavaScript
唯一的 Local Scope
。
function bar() { ... }
function foo() { ... }
实际开发中被很快被过渡,但是大量的 函数
和 变量
被挂载在 window
上,造成命名冲突。
3.2:命名空间时代
为了解决上面的问题,出现了命名空间这样的模块化设计
var obj = {
price1: 1,
price2: 2,
sum: function c() {
return this.price1 + this.price1
}
}
这种做法缓解了命名冲突的问题,但是随之而来的是安全问题。既然是一个对象,那么对象里的所有属性值本质上都是对外暴露,那么就有可能被意外更改。
3.3:IIFE时代(立即执行函数)
<!DOCTYPE html>
<html>
<head>
<title>IIFE</title>
</head>
<body>
<div class="font-show">IIFE</div>
</body>
// 引入自定义模块
<script type="text/javascript" src="module.js"></script>
// 使用
<script type="text/javascript">
akubelaModule.bar()
</script>
</html>
// module.js文件
(function (w) {
var msg1 = 'msg1'
var msg2 = 'msg2'
function foo() {
console.log('foo:', msg1)
}
function bar() {
foo()
console.log('bar:', msg2)
}
// 对外只暴露foo与bar这两个方法
w.akubelaModule = { foo, bar }
})(window)
利用 IIFE 基本上可以认为解决了命名冲突及安全问题,但是如果该模块需要依赖第三方模块或者自定义模块该如何处理?
3.4:IIFE增强时代
只需要将以上函数做简易的修改就可满足要求,如下:
<!DOCTYPE html>
<html>
<head>
<title>IIFE</title>
</head>
<body>
<div class="font-show">IIFE</div>
</body>
+ // 引入百度jQuery库
+ <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
// 引入自定义模块
<script type="text/javascript" src="module.js"></script>
// 使用
<script type="text/javascript">
akubelaModule.bar()
</script>
</html>
// module.js文件
(function (w, $) {
var msg1 = 'msg1'
var msg2 = 'msg2'
function foo() {
console.log('foo:', msg1)
}
function bar() {
foo()
console.log('bar:', msg2)
+ $('body').css('background', 'lightgreen')
}
// 对外只暴露foo与bar这两个方法
w.akubelaModule = { foo, bar }
})(window, jQuery)
运行:
上图我们能看见 msg1 与 msg2 都正常输出页面也正常渲染了,但是如果访问其他变量或函数就出现了报错或者 undefined。这就很好的解决了我们上面提到了问题。
IIFE 增强也是现代模块化的基石,缘起于此思想。
3.5:IIFE增强带来的问题
<!DOCTYPE html>
<html>
<head>
<title>IIFE增强</title>
</head>
<body>
<div class="font-show">IIFE增强</div>
</body>
// 引入百度jQuery库
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<script src="http://xxx1.baidu.com/n.js"></script>
<script src="http://xxx2.baidu.com/in.js"></script>
<script src="http://xxx3.baidu.com/min.js"></script>
<script src="http://xxx4.baidu.com/lnx.js"></script>
// 引入自定义模块
<script type="text/javascript" src="module1.js"></script>
<script type="text/javascript" src="module2.js"></script>
<script type="text/javascript" src="module3.js"></script>
<script type="text/javascript" src="module4.js"></script>
<script type="text/javascript" src="module5.js"></script>
<script type="text/javascript" src="module6.js"></script>
<script type="text/javascript" src="module7.js"></script>
<script type="text/javascript" src="module8.js"></script>
<script type="text/javascript" src="module9.js"></script>
<script type="text/javascript" src="module10.js"></script>
// 使用
<script type="text/javascript">
akubelaModule.bar()
akubelaModule.foo()
akubelaModule.bar1()
akubelaModule.bar2()
akubelaModule.bar3()
</script>
</html>
- 请问这些脚本文件的顺序能调换吗?
- 这些脚本之间依赖关系明确吗?
- 脚本这么多首屏不要了?
- 请你重构你敢吗?
带着这些问题我们看看前人是如何解决的
4:野蛮生长时代
4.1:CommonJS
1)历史
CommonJS 项目由 Mozilla 工程师 Kevin Dangoor 于 2009年1月 发发布了一篇 《What Server Side JavaScript needs》 ,最初名为 ServerJS。
在 2009年8月,这个项目被改名为 CommonJS 来展示其 API 的广泛的应用性。后由 nodejs 之父 Ryan Dahl 在 2009年11月 实现了 CommonJS。
之后 node 的应用越来越广。在 npm + node 的组合下,一统了前端的包管理模块的天下。
2)理念
每一个文件就是一个模块,有自己的作用域,文件内的所有对象都可以私有化,对外只暴露指定的 API
3)特点
1:同步加载
2:一次加载后后期再访问直接读取缓存
3:每个模块都有自己的作用域,不污染全局作用域
4)语法
// 引入
const module = require(path/moduleName)
// 暴露
module.exports = value
exports.xxx= value
5)基础实现
let fs = require('fs')
let path = require('path')
// v8虚拟机
let vm = require('vm')
function Module(p){
// 当前模块的标识(绝对路径作为模块的key)
this.id = p
// 模块将挂载到exports属性上
this.exports = {}
}
// js文件自加载的包装类,类似eval或者是settimeout
Module._wrapper = ['(function(exports,require,module){', '})']
//所有的加载策略
Module._extensions = {
'.js': function(module){
// 将模块用字符串拼接的形式包入一个函数中
let fn = Module._wrapper[0] + fs.readFileSync(module.id,'utf8') + Module._wrapper[1]
// 将fn挂载在module.exports上,同时传入参数
vm.runInThisContext(fn).call(module.exports, module.exports, req, module)
// 返回
return module.exports
},
'.json': function(module){
// 如果匹配到的扩展是json,直接返回该模块
return JSON.parse(fs.readFileSync(module.id,'utf8'))
},
'.node': 'xxx',
}
// 缓存对象
Module._cacheModule = {}
// 输入相对路径或模块名称,返回一个绝对路径
Module._resolveFileName = function(moduleId){
let p = path.resolve(moduleId)
// 通过路径直接去找该文件
try{
// 验证权限,有就返回
fs.accessSync(p)
// 找到直接返回
return p
}catch(e){
console.log(e)
}
//对象中所有的key做成一个数组[]
let arr = Object.keys(Module._extensions)
// 以上找不到的时候,加后缀再找
for(let i=0; i<arr.length; i++){
// 拼接完整路径
let file = p+arr[i]
try{
fs.accessSync(file)
return file
}catch(e){
console.log(e)
}
}
}
// load方法
Module.prototype.load = function (abPath) {
// 获取扩展名
let ext = path.extname(abPath);
// 找对应的扩展方法,并注入该模块对象
let content = Module._extensions[ext](this);
return content;
}
// require方法
function req(moduleId){
// 得到绝对路径
let p = Module._resolveFileName(moduleId)
// 查缓存
if(Module._cacheModule[p]){
// 存在返回
return Module._cacheModule[p].exports
}
// 没有缓存创建一个
let module = new Module(p)
Module._cacheModule[p] = module
//加载模块
module.exports = module.load(p)
return module.exports
}
6)基础实现
6.1)建立如下目录结构:
6.2)modules.js
var aaa = 10
function fff() {
aaa = aaa + 5
}
module.exports = {
aaa,
a:1,
b:2,
c(){
return this.a + this.b + 3
},
d: {
e:1
},
fff,
f() {
this.aaa = this.aaa + 10
}
}
6.3)index.js
let part = req('./modules.js')
console.log('a:', part.aaa)
console.log('f:', part.fff())
console.log('a:', part.aaa)
console.log('a:', part.a)
console.log('f:', part.f())
console.log('aaa:', part.aaa)
console.log('f:', part.fff())
console.log('aaa:', part.aaa)
6.4)node执行
先用原生 require 执行,再用自己写的 req 执行对比如下:
4.2:AMD(异步模块定义)
由于 CommonJS 在服务端的出色表现,开发者就萌生了将他移植到浏览器端的想法。
然而 CommonJS 的模块是同步加载的,要在浏览器端实现势必造成严重的体验问题。所以 AMD 规范应运而生。而大名鼎鼎的 RequireJS 就是 AMD 规范的实现。我们一般说的 RequireJS 也指 AMD 规范。
AMD 推崇的是依赖前置,提前执行
1)语法
define(id?, dependencies?, factory)
id
:指定义中模块的名字(可选)。如果没有提供该参数,模块的名字应该默认为模块加载器请求的指定脚本的名字。如果提供了该参数,模块名必须是“顶级”的和绝对的(不允许相同名字)。dependencies
:当前模块依赖的,已被模块定义的模块标识的数组字面量(可选)。factory
:一个需要进行实例化的函数或者一个对象。
1.1)暴露模块
// 定义没有依赖的模块
define(function(){
return ...
})
// 定义有依赖的模块
define(['module1', 'module2'], function(module1, module2){
return ...
})
1.2)引用模块
require(['module1', 'module2'], function(module1, module2){
...
})
2)引入RequireJS
AMD 是为了在浏览器宿主环境中实现异步加载模块化方案的规范之一,由于不是 JavaScript 原生支持,使用 AMD 规范进行页面开发需要用到对应的库函数。
2.1)创建目录结构
2.2)module1.js
define(function () {
var msg = 'module1'
function show() {
console.log('module1:', msg)
}
return { show }
})
2.3)module2.js
define(['module1', 'jquery'], function (module1, $) {
module1.show()
var msg = 'module2'
// 暴露对象
function showModule2() {
console.log('module2:', msg)
$('body').css('background', 'lightgreen')
}
return { showModule2 }
})
2.4)main.js
// main.js文件
(function() {
// 配置
require.config({
// baseUrl: 'modules/', // 基础地址,如果配置该参数路径指向就会从根目录开始
paths: {
// 映射: 模块标识名: 路径
module1: './module1',
module2: './module2',
jquery: './libs/jquery'
}
})
require(['module2'], function(module2) {
module2.showModule2()
})
})()
2.5)运行
4.3:CMD(常规模块定义)
CMD 规范是由 SeaJS 实现的,同时又是 SeaJS 在推广的过程中产生 CMD 的规范。
为什么会出现 CMD 这种规范呢?主要在于设计理念与实际开发中的不同需求导致。
AMD 推崇依赖前置,提前执行,CMD 遵循的是就近依赖,延迟执行。
SeaJS 的作者是玉伯。
1)语法
1.1)定义模块
// modules1.js文件
// 无依赖定义
define(function (require, exports, module) {
var msg = 'module1'
function show() {
console.log('module1:', msg)
}
// 单一暴露
exports.show = show
})
// modules2.js文件
// 有依赖定义
define(function (require, exports, module) {
var module1 = require('./module1')
module1.show()
// 暴露对象
module.exports = {
msg: 'module2'
}
})
// modules3.js文件
// 有依赖定义
define(function (require, exports, module) {
var module2 = require('./module2')
console.log('module2:', module2.msg)
var msg = 'module3'
module.exports = msg
})
// modules4.js文件
// 有依赖定义
define(function (require, exports, module) {
// 异步挂起(事件循环)
require.async('./module3', function (module3) {
console.log('异步模块3:', module3)
})
var module4 = 'module4'
function show() {
console.log('module4:', module4)
}
exports.show = show
})
// main.js文件
define(function (require) {
var m4 = require('./module4')
m4.show()
})
// html文件
<!DOCTYPE html>
<html>
<head>
<title>CMD</title>
</head>
<body>
<div>CMD</div>
</body>
<script type="text/javascript" src="./modules/libs/sea.js"></script>
<script type="text/javascript">
seajs.use('./modules/main')
</script>
</html>
1.2)下载引入SeaJS
目录结构
1.3)执行
4.4:ESM(ES6规范)
2015年6月,TC39 发布了 ES6规范 也就是 ESM 规范,ESM 是 JavaScript 官方的标准化模块系统,在 2017年 得到了大多数主流浏览器的支持。在 2018年5月 Firefox 60 发布之后,所有的主流浏览器就都已开始广泛支持原生 ESM,这是 ES当前提案。 而 NodeJs的 8.9 之后的版本就开始支持 ES6了,在 13.2 版本之后才开启默认支持运行 ES Modules。
可见 ECMA 有着极强的号召力,在规范发布后,以上提及的各种模块化的的规范开始步入生命的倒计时。前端生态开始全面拥抱 ESM。
该规范有什么优点呢?
1:官方规范、先天权威性
2:语言级别的支持
3:模块静态化
4:自带树摇buff
5:默认严格模式
6:语法简洁易理解
1)语法
1.1)export 导出
// 命名导出
export const dataType = (v) => {
return Object.prototype.toString.call(v).slice(8, -1).toLocaleLowerCase()
}
// 导出所有
export * form './module.js'
// 默认导出
export default xxx
1.2)import 导入
// 命名导入
import { dataType } from '@/utils/base'
// 导入模块中的所有变量
import * as module from './module.js'
// 默认导入
import xxx from './zhTw.js'
// 别名导入
import { foo as bar } from './math'
// 执行模块中的代码
import './module.js'
// 动态导入
xxx: () => import('@/views/login/indexTest.vue')
5:各规范对比
CommonJS | AMD | CMD | ESM | |
---|---|---|---|---|
发布时间 | 2009 | 2011 | 2011 | 2015 |
作者 | Mozilla-Kevin Dangoor | James Burke | 阿里-玉伯 | ECMA |
引入方式 | 同步 | 同步 | 同步/异步 | 同步/异步 |
使用场景 | 服务端 | 客户端/服务端 | 客户端/服务端 | 客户端/服务端 |
输出变量拷贝方式 | 值拷贝 | 值拷贝 | 值拷贝 | 引用拷贝 |
加载时机 | 运行时加载 | 运行时加载 | 运行时加载 | 编译时确定依赖,输出接口(静态化) |
特点 | 1:同步加载 2:首次次加载后缓存,后期引入直接读取缓存 |
依赖前置,提前执行 | 就近依赖,延迟执行 | 1:官方规范、先天权威性 2:语言级别的支持 3:模块静态化 4:自带树摇buff 5:默认严格模式 6:语法简洁易理解 |
6:总结
模块化到现在前前后后经历了将近15个年头,从远古时代的 全局函数 → 命名空间 → IIFE → IIFE增强
。
再到野蛮生长时代, CommonJS
主攻服务端。 AMD,CMD
主攻浏览器,是开发者自己定义的一种开发模式,因为设计的好用,逐渐被推广开最终形成规范。
在ESM
规范没有发布之前他们承担了前端世界的绝大部分实际开发需求,但是在 ESM
规范发布后他们的历史使命也正式宣告结束。
最后历史的车轮碾碎了那个野蛮的时代,前端模块的世界也就此拉开了新的序幕。
6:推荐好文
- 最佳实践 monorepo + pnpm + vue3 + element-plus 0-1 完整教程
- Vite+rollup项目如何大幅提升性能体验
- 面试官系列:请说说你对深拷贝、浅拷贝的理解
- 面试官系列:请你说说原型、原型链相关
- 面试官系列:请手写防抖或节流函数debounce/throttle
- 面试官系类:请手写instanceof
- 10分钟快速手写实现:call/apply
- 5分钟快速手写实现:bind
- 5分钟快速手写实现:new
- 10分钟入门SVG:SVG如何画出单身狗?来本文告诉你