随着前端技术的飞速发展,在前端项目中更加高效更加清晰地维护前端项目中的每个资源成了所有前端工程师的一个目标,继而出现了大量的解决方案。今天就简单一起回顾一下前端模块化的一个发展历史。
本期作者:领创集团-Ginee 前端工程师 曾祥瑞
石器时代:文件划分方案
在最早期的时候,前端没有完整且成熟的方案,但是大家希望能够将各自的业务或者功能独立出来维护。
所以在项目开发中,大家进行了一个约定,使用文件来划分模块。每个文件相当于是一个独立的模块,在使用某个模块的时候就将该模块引入到页面中,然后使用的引入模块的变量/函数
举个🌰
└─ project-1
├── a.js
├── b.js
└── index.html
// a.js
var name = 'a'
function a (){
console.log('this is module a')
}
// b.js
var name = 'b'
function b (){
console.log('this is module b')
}
// index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>石器时代:文件划分方案</title>
</head>
<body>
<script src="a.js"></script>
<script src="b.js"></script>
<script>
// 调用方法a
a() // this is module a
// 调用方法b
b() // this is module b
// 获取全局变量name
console.log(name) // b
</script>
</body>
</html>
缺点:
虽然有文件进行区分,但是各个模块也都是挂载在全局下的,所以会有大量的全局污染和命名冲突;
没有独立的空间,模块之间是可以随意相互访问或者进行修改
没有办法管理模块之间的依赖关系;
无法在代码层面进行检查,只能是人为约定,一旦项目变大或者人员变动,后续难以维护;
青铜时代:命名空间方案
大家觉得既然无法通过文件名称来区分,那就约定一下,我们每个独立的模块,只向全局暴露一个对象,然后将该模块的全部变量挂载在当前这个对象下。
// a.js
window.moduleA={
name: 'a',
fooA: function(){
console.log('this is module a')
}
}
// b.js
window.moduleA={
name: 'b',
fooB: function(){
console.log('this is module b')
}
}
// index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>青铜时代:命名空间方案</title>
</head>
<body>
<script src="a.js"></script>
<script src="b.js"></script>
<script>
// 调用方法a
moduleA.fooA() // this is module a
// 调用方法b
moduleB.fooB() // this is module b
// 修改变量
moduleB.name = 'c'
moduleA.name = 'd'
</script>
</body>
</html>
这样的书写方式也会暴露所有的模块成员,内部状态还是可以被外部改写,除了解决掉命名冲突之外,其他所有的问题,都还依然存在。
铁器时代:IIFE
// a.js
(function(){
var name = 'a'
windows.moduleA = {
name:name
}
})()
// b.js
(function($){
var name = 'b'
windows.moduleB = {
name:name
}
})(jQuery)
在使用依赖的时候需要判断依赖模块是否加载完成,不知道依赖之间的先后顺序。
蒸汽时代:模块化规范方案
如今前端的模块化加载方案已经发展的非常成熟了,下面就介绍一下我们目前日常开发会接触到的模块化规范方案。
4.1 CommonJS
CommonJS是2009年诞生的,它出现的目的是希望JS可以运行到更多地方,主要是服务端,nodejs采用了这种规范。之前的nodeJS是只支持CommonJS来实现模块的。Node verison 13.2.0 起开始正式支持 ES Modules 特性。
当然CommonJS目前也可用于浏览器端,但是需要使用 Browserify 进行提前编译打包。
CommonJS规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。
到此,我们可以看出来,CommonJs 规范解决了我们的最开始的痛点。
它具有模块变量作用域的能力,即使多个模块内部有相同的变量名称,也不会有冲突;
它有导出导入模块的方式,同时能够处理基本的依赖关系;
根据它的规范,可以得知模块之间的加载顺序;
特点:
每个文件都是一个模块,有独立的作用域,模块内部的代码运行在自己的作用域下,不会造成全局污染。
模块可以多次加载,但是只会在第一次加载的时候运行,将运行结果进行缓存,后续的加载都是进行缓存结果读取。
模块加载的顺序,按照其在代码中出现的顺序。
举个🌰:
└─ project-4
├── moduleA.js
├── moduleB.js
└── index.js
// index.js
require("./moduleA");
var moduleB = require("./moduleB");
console.log(moduleB);
// moduleA.js
var module = require("./moduleB");
setTimeout(()=>{console.log(moduleB)},1000);
// moduleB.js
var module = new Date().getTime();
module.exports = module;
// 执行结果是先打印出来一个时间戳,然后1s后打印一个相同的时间戳
到此,我们可以看出来,CommonJs 规范解决了我们的最开始的痛点。
它具有模块变量作用域的能力,即使多个模块内部有相同的变量名称,也不会有冲突;
它有导出导入模块的方式,同时能够处理基本的依赖关系;
根据它的规范,可以得知模块之间的加载顺序;
4.2 AMD
AMD是2010由RequireJS模块定义在其推广过程中的规范输出。AMD全称Asynchronous Module Definition 的意思就是“异步模块加载”。AMD 本身就是为了浏览器而设计的,CommonJS是'运行时同步加载模块',就是这个原因,可能会在加载大量资源的时候,相应时间很长,造成浏览器“假死”现象。
Require.js 实现了AMD 的规范,我们下面主要就是通过Require.js来举例。
特点:
采用异步方式加载模块,模块的加载不影响它后面语句的运行;
前置依赖,需要所有依赖加载完成后,才会执行定义好的回调函数;
可以一次定义多个依赖模块;
可在不转换代码的情况下直接在浏览器运行;
举个🌰:
└─ project-5
├── moduleA.js
├── moduleB.js
├── index.js
└── index.html
// index.html
<html>
<head>
<meta charset="UTF-8">
<title>AMD</title>
</head>
<body>
<!-- 需要先加载require.js -->
<script src="require.js"></script>
<script src="./index.js"></script>
</body>
</html>
// index.js
require(['./moduleA','./moduleB'],function(moduleA,moduleB){
console.log(moduleB)
})
// moduleA.js
define(function(require){
var moduleB = require('./moduleB')
setTimeout(()=>{console.log(moduleB)},1000);
})
// moduleB.js
define(function(){
var date = new Date().getTime();
return date
})
// 执行结果也是先打印出一个时间戳,然后在1s后打印一相同时间戳
其实整体使用下来发现,AMD 和 CommonJS 都可以解决掉之前说的问题,目前绝大多数的第三方库都会支持AMD规范。
AMD的默认异步以及需要在回调函数中来定义模块内容,使得它在使用中会相对复杂一些,当项目中模块划分维度过于细致的时候,会出现一个页面对JS文件请求次数过多的情况。导致效率降低。
4.3 CMD
CMD 是 sea.js 在推广过程中对模块定义的规范化产出。
由于CMD很多行为和AMD非常类型,就不再赘述了。
AMD和CMD区别点:
对于依赖模块:AMD是前置执行,CMD是延迟执行,不过RequireJS 从2.0开始,也已经支持可以延迟执行了。
CMD 推行的是依赖就近,按需加载;AMD是依赖前置。
4.4 UMD
如果我们书写的模块,需要同时运行在浏览器端也需要运行在Node.js 中,我们怎么来处理?
分别写一份AMD + 一份CommonJS 规范,分别运行在各自环境中的做法,实在是不可取。
所以它出现了,UMD(Universal Module Definition) 解决跨平台的方案。其实就是AMD和CommonJS的糅合。
其实UMD的原理比较简单,就是在运行时进行判断当前模块的类型。
CommonJS判断依据:node.js 的exports是否存在。
AMD判断依据:define 是否存在。
根据判断的结果来使用不同的方法进行导出即可。
4.5 ES Modules规范
ES Modules 是 ECMAScript 2015(ES6)中才定义的模块系统。在此之前,JavaScript 一直是没有模块体系的。
前面讲到的CommonJS、AMD、CMD都是在没有ES Modules之前由各个社区提出的解决方案,
但是随着官方推出ES Modules,模块化的“江山一统”指日可待!
特点:
编译时进行加载,完全静态化的方式进行模块加载;
模块输出的是值的引用,会随着原始值变化而变化;
export的导出的都是接口。import 引入的也是接口,不能对其进行修改;
import() 可以动态使用,加载模块;
ES Modules模块导入导出,主要是 import 和 export 这两个关键字。有多种组合的情况。
简单举个🌰
// 导入
import './moduleA.js'
import moduleA from './moduleA.js'
import {name} from './moduleA.js'
import moduleA,{name} from './moduleA.js'
import * as module from './moduleA.js'
import {name as moduleName} from './moduleA.js'
// 导出
export let name = 'name'
export { name1, name2, nameN };
export { variable1 as name1, variable2 as name2, …, nameN };
export default expression;
整篇文章是为了让大家对模块化的一个过程有所了解,如果需要具体的模块化使用方式,可以查询对应库的官网。
参考文献:https://developer.mozilla.org/zh-CN