一文梳理前端模块化方案
模块化方案汇总
写在前面
由于官方ECMAScript规范化卷时间比较早,他所规范的范畴较小,随着JavaScript的不断壮大,他的规范相对于其他语言来说就显得比较薄弱,ES6之前他有以下的一些缺陷:
- 没有模块系统
- 标准库较少
- 没有标准的接口
- 缺乏包管理系统
在ES6之前,JavaScript并没有统一的模块规范,但是JavaScript社区对其进行了探索,提出了AMD、UMD等模块规范 合格的模块化应该拥有以下特性:
- 独立性:独立完成功能,与外界环境隔离
- 完整性:能完成某个特定功能
- 可依赖:能够依赖其他模块
- 可被依赖:能被其他模块依赖
好在这么多年的发展,官方也逐渐在解决这些问题,但是在实际学习中,发现各种各样的模块规范让人容易搞混,我们来梳理下到目前为止JavaScript常见的模块规范吧,这也为后面学习规范化开发开源库打下基础。
原始模块
从定义上来说,一个函数即为一个模块,下面为JavaScript社区中原始模块代码
(function (mod,$){
function clone(){
//...
}
mod.clone = clone
})((window.clone = window.clone || {}), jquery)
这段代码是一个立即执行函数表达式(Immediately Invoked Function Expression,IIFE),用于定义一个模块(module)并将其中的 clone
函数添加到全局对象 window
中。
-
(function (mod, $) { ... })
: 这是一个匿名函数,接受两个参数mod
和$
。这个函数尚未被调用,只是定义了它。 -
window.clone = window.clone || {}
: 这是一个表达式,用于确保window.clone
对象已存在,如果不存在则创建一个空对象{}
。这个表达式的结果将作为第一个参数mod
传递给上述匿名函数。 -
jquery
:这是第二个参数,它用于传递 jQuery 库或对象给匿名函数,以便在函数内使用$
符号来表示 jQuery。 -
在匿名函数内部,定义了一个名为
clone
的函数。 -
最后,
mod.clone = clone
将clone
函数添加到mod
对象中,通常是一个模块或命名空间对象。在这种情况下,mod
被设置为window.clone
,所以window.clone
现在拥有一个名为clone
的函数。
这个代码的目的是将一个 clone
函数添加到全局命名空间中,使得其他部分的代码可以访问并使用这个函数。此外,确保在使用之前 jQuery 库已正确引入。
很明显这种模式下的致命缺点:
- jquery必须先于代码被引用,不然会报错,对于大型项目来说不可维护
一般库都会提供对这种模块的支持,因为这种模块可以直接通过script标签引用。
AMD(Asynchronous Module Definition)模块
该模块的中文名称是“异步模块定义”,他是一种异步模块加载规范,专门为浏览器端设计。AMD 规范由 Common JS 规范演进而来,前文介绍 Common JS 规范时说过,浏览器端无法直接使用 Common JS,需要使用 browserify 编译后才能运行。而 AMD 规范规范则专注于浏览器端。
AMD定义模块的方式如下:
define(id?, dependencies?, factory);
浏览器 并不支持 AMD模块,需要借助Require.js才能加载AMD模块,Require.js是AMD最常用的加载器,但是目前的新系统基本不再使用Require.js,因为大部分库都会提供对AMD的支持。 下文为AMD模块支持的代码
// clone.js
define(function(){
function clone(){
//...
}
return clone
})
引入该模块
require(['clone'],function(clone){
clone()
})
看下完整示例:
- 目录结构
2.index.html文件
首先在index.html中引入了RequireJS的cdn,data-main属性为requirejs要找的入口文件路径,也就是说所有逻辑和配置都写在data-main属性对应的文件中
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script src="https://cdn.bootcdn.net/ajax/libs/require.js/2.3.6/require.js" data-main="index.js" async ></script>
</body>
</html>
- index.js文件
index.js文件中主要导入依赖的模块和文件路径了,关于导入其他模块,require对象提供了config方法,其中的path属性指定了所有模块的名字和文件目录(requirejs要求一个模块为一个文件),这样requirejs就知道其他模块叫什么在哪里了,define方法是requirejs提供的定义模块的方法,第一个参数为该模块依赖的相关模块名,第二个参数为模块内部的执行逻辑函数,函数的参数为依赖的模块的传入对象。这样就可以在该模块中使用导入其他模块了。
require.config({
path:{
clone:'./clone', // 导入模块名clone,位置在./clone.js中
util:'./util' // 导入模块名util,位置在./util.js中
}
})
require(['clone'],function (clone) { // 该模块依赖clone模块,将其作为参数传入函数中,就可以在函数中调用了
clone()
})
- clone.js文件
define(['util'],function(util){ //clone模块依赖util模块,在其作用域中可调用util模块,define第一个参数为依赖的模块列表,若无则可不写第一个参数
function clone(source){
//...
console.log("clone函数执行")
util()
}
return clone
})
- util.js
define(function () { // 该模块并未依赖任何其他模块,第一个参数省略
function util() {
console.log("utils被执行")
}
return util
})
就这样 AMD规范就基本实现了,由于浏览器端有更好的ES模块方案替代,所以此方案基本不再使用,这里只做简单了解,感兴趣的可看阮一峰关于此模块方案的博客
CommonJS规范
CommonJS是运用在NodeJS环境中的一种同步模块加载规范,CommonJS的提出是为了弥补JavaScript当时没有模块标准的缺陷,开发者希望JavaScript拥有类似python和Java那样开发大型项目的基础能力,而不是让JavaScript仅仅停留在小脚本的阶段,甚至期望CommonJS API写出的应用能够具备跨宿主环境执行的能力,让其不仅能开发客户端,还能开发服务端,命令行工具,混合应用等,如今CommonJS的大部分规范依旧是草案,但是不可否认的是他是JavaScript最为重要的里程碑。
CommonJS模块规范如下:
define(function, require, exports, module){
//模块代码
}
在NodeJS中,此define函数为系统自动生成,无需开发者书写
- 定义模块
通过module.exports导出
function clone(){
//...
}
module.exports = clone
- 导入模块
- 通过require导入
const clone = require('./clone.js')
clone()
你需要注意的是
require
是赋值过程并且是运行时才执行,也就是同步加载require
可以理解为一个全局方法,因为它是一个方法所以意味着可以在任何地方执行。require
导入的是值的引用,也就是说可以多文件内共享对象
UMD (Universal Module Definition)规范
UMD是一种通用模块加载规范,他并不是一种新规范,而是对前面三种规范的整合,支持UMD规范的库可以在任意模块中工作
他的规范如下:
(function (root, factory) {
var clone = factory(root);
if(typeof define === 'function' && define.amd){
//AMD规范
define('clone',function () {
return clone
})
}else if(typeof exports === 'object'){
// CommonJS
module.exports = clone
}else {
// 原始模块
var _clone = root.clone;
clone.onConflict = function () {
if(root.clone === clone){
root.clone = _clone
}
return clone
};
root.clone = clone;
}
})(this,function (root) {
function clone() {
//...
}
return clone
})
关于此段代码的详细解释如下:
-
(function (root, factory) { ... })(this, function (root) { ... })
: 这是一个自执行函数,接受两个参数。第一个参数root
是当前环境的全局对象,通常是window
对象在浏览器中,或global
对象在 Node.js 中。第二个参数factory
是一个函数,用于创建一个名为 "clone" 的模块或变量。 -
var clone = factory(root);
: 在自执行函数内部,它调用factory
函数并传入root
作为参数,然后将返回的结果赋值给clone
变量。这个结果应该是一个包含 "clone" 功能的函数或对象。 -
if(typeof define === 'function' && define.amd){ ... }
: 这是一个条件语句,用于检查是否支持 AMD (Asynchronous Module Definition) 规范。如果当前环境支持 AMD 规范,它会执行下面的代码块。define('clone', function () { ... })
: 这里使用 AMD 规范定义一个名为 "clone" 的模块,其内容由匿名函数返回,这个匿名函数应该返回之前定义的clone
变量或模块。 -
else if(typeof exports === 'object'){ ... }
: 如果不支持 AMD 规范,它继续检查是否是 CommonJS 模块系统。如果是 CommonJS,它会执行下面的代码块。module.exports = clone
: 这里将之前定义的clone
变量或模块导出,使其可以被其他 CommonJS 模块引用。 -
else { ... }
: 如果既不支持 AMD 也不是 CommonJS,它会执行下面的代码块。var _clone = root.clone;
: 这里保存了原始的root.clone
到_clone
变量中。clone.onConflict = function () { ... }
: 定义了一个名为onConflict
的方法,这个方法的作用是在命名冲突时解决问题。如果root.clone
与当前的clone
相同,它会将root.clone
恢复为之前保存的_clone
,然后返回当前的clone
。root.clone = clone;
: 最后,它将当前的clone
赋值给全局的root.clone
,以确保可以在全局范围内访问到这个功能。
这段代码的作用是在不同的模块系统中定义一个名为 "clone" 的模块或变量,并根据模块系统的不同来适配定义方式。在 AMD 中定义模块,在 CommonJS 中导出模块,而在原始模块环境中则将功能赋值给全局变量。同时,它还提供了一个解决命名冲突的机制,以防止与其他模块或全局变量发生冲突。 =》 此段为chatgpt解释
ES Module模块
ES Module是ECMAScript2015带来的原生模块系统,主流浏览器都已支持,不兼容ESM的可以通过构建工具处理后使用
- 导出
export function clone(){}
// 或者
export default clone
- 导入
import { clone } from "./clone"
// 或者
import clone form "./clone"
关于ESM你需要知道的是
-
使用
import
语句导入模块时,模块会被静态加载,他是在编译时就已经确定了导入的模块; -
import
和import()
语句支持模块的默认导出和命名导出。 -
export
导出的也是引用。 => 似乎很多人说require是浅拷贝,但是实操后并未发现其是浅拷贝而同样是引用
若在script标签中使用 需要加上type="module"属性
ESM和CommonJS的区别
-
静态 vs. 动态导入:
- ES6模块系统是静态的,模块的依赖关系在编译时确定。这意味着所有导入语句必须在模块的顶层,并且不能放在条件语句或函数内部。
- CommonJS模块系统是动态的,模块的依赖关系可以在运行时确定。这允许你在条件下或函数内部导入模块。
-
导出方式:
- 在ES6模块系统中,你可以使用
export
和export default
导出模块的功能或值。 - 在CommonJS中,你可以通过
module.exports
导出一个值或对象。
- 在ES6模块系统中,你可以使用
-
导入方式:
- 在ES6模块系统中,你可以使用
import
关键字导入模块的特定功能或值,也可以重命名导入的项。 - 在CommonJS中,你可以使用
require
函数导入整个模块,并且你不能像ES6一样轻松重命名导入的项。
- 在ES6模块系统中,你可以使用
-
默认导出:
- ES6模块系统支持单一的默认导出,一个模块只能有一个默认导出。
- CommonJS模块系统没有内置的默认导出机制,但可以手动约定一个默认导出属性。
-
循环依赖:
- ES6模块系统能够更好地处理循环依赖情况,因为它在编译时解析依赖关系。
- CommonJS模块系统在处理循环依赖时可能会导致奇怪的行为,因为它在运行时解析依赖。
-
生态系统:
- ES6模块系统是ECMAScript的一部分,主要用于浏览器端和现代JavaScript开发。
- CommonJS模块系统主要用于Node.js和服务器端JavaScript开发。
转载自:https://juejin.cn/post/7281159309969915961