【总结】1775- 聊聊前端模块化
在上古时期,曾经的 Web 开发者们,应该会因为在一个庞大的 JavaScript 文件中寻找一个小小的函数而感到绝望?或者因为修改一个变量而不得不查找整个代码库?
当下的前端开发中,webpack,rollup,vite 等构建打包工具大家应该都用的飞起了。它们都基于一个非常重要的概念 - 前端模块化。
在这篇文章中,我们将聊聊前端模块化的发展历程以及主流的一些方案。
什么是模块化前端模块化是指将一个大型的前端应用程序分解为小的、独立的模块,每个模块都有自己的功能和接口,可以被其他模块使用。前端模块化的出现是为了解决前端开发中代码复杂度和可维护性的问题。在前端模块化的架构下,开发人员可以更加专注于各自的模块开发,提高了代码的复用性和可维护性。
为什么需要前端模块化在传统的前端开发中,所有的代码都是写在同一个文件中,这样做的问题在于:
- 可维护性差:当应用程序变得越来越大时,代码变得越来越难以维护。
- 可重用性差:相同的代码可能会被多次复制和粘贴到不同的文件中,这样会导致代码冗余,增加了代码量。
- 可测试性差:在传统的前端开发中,很难对代码进行单元测试。
- 可扩展性差:在传统的前端开发中,很难对应用程序进行扩展。
全局 function 模式
将不同功能封装成不同的函数
function fetchData() {
...
}
function handleData() {
...
}
缺陷:这个是将方法挂在 window 下,会污染全局命名空间,容易引起命名冲突且数据不安全等问题。
全局 namespace 模式
既然全局 function 模式下,会有命名冲突等问题,那么我们可以通过对象来封装模块
var myModule = {
fetchData() {
...
},
handleData() {
...
}
};
缺陷:这个方案确实减少了全局变量,解决命名冲突的问题,但是外部可以直接修改模块内部的数据。
IIFE 模式,通过自执行函数创建闭包
function(global) {
var data = 1
function fetchData() {
...
}
function handleData() {
...
}
window.myModule = {fetchData, handleData}
}(window)
缺陷:这个方案下,数据是私有的,外部只能通过暴露的方法操作,但无法解决模块间相互依赖问题。
IIFE 模式增强,传入自定义依赖
我们可以通过传入依赖的方式来解决模块间引用的问题
function(global, otherModule) {
var data = 1
function fetchData() {
...
}
function handleData() {
...
}
window.myModule = {fetchData, handleData, otherApi: otherModule.api}
}(window, window.other_module)
缺陷:但仍然有以下几个缺点
- 多依赖传入时,代码阅读困难
- 无法支持大规模模块化开发
- 无特定语法支持,代码简陋
经过以上过程的演进,我们确实可以实现前端模块化开发了,但是仍然有几个问题,一是请求过多,我们都是通过 script 标签来引入各个模块文件的,依赖多个模块,那样就会发送多个请求。二是依赖模糊,很容易因为不了解模块之间的依赖关系导致加载先后顺序出错,模块之间的依赖关系比较难以管理,也没有明确的接口和规范。因此模块化规范应运而生。
模块化规范CommonJS
1. 概述
CommonJS 是一个 JavaScript 模块化规范,它最初是为了解决 JavaScript 在服务器端的模块化问题而提出的。是 NodeJS 的默认模块饭规范,该规范定义了模块的基本结构、模块的加载方式以及模块的导出和导入方式等内容。
2. 模块的基本结构
在 CommonJS 规范中,一个模块就是一个文件。每个文件都是一个独立的模块,文件内部定义的变量、函数和类等只在该文件内部有效。
每个模块都有自己的作用域,模块内部的变量、函数和类等只在该模块内部有效。如果想在其他模块中使用该模块内部的变量、函数和类等,需要将其导出。
3. 模块的加载方式
在 CommonJS 规范中,模块的加载方式是同步的。也就是说,当一个模块被引入时,会立即执行该模块内部的代码,并将该模块导出的内容返回给引入该模块的代码。
模块可以多次加载,第一次加载时会运行模块,模块输出结果会被缓存,再次加载时,会从缓存结果中直接读取模块输出结果。模块加载的顺序,按照其在代码中出现的顺序。模块输出的值是值的拷贝,类似 IIFE 方案中的内部变量。
这种同步加载方式可以保证模块内部的代码执行完毕后再执行外部代码,从而避免了异步加载所带来的一些问题。但同时也会影响页面加载速度,因此在浏览器端使用时需要注意。
4. 模块的导出和导入方式
在 CommonJS 规范中,一个模块可以通过module.exports
或者 exports
对象来导出内容。module.exports 是真正的导出对象,而 exports 对象只是对 module.exports 的一个引用。
一个模块可以导出多个内容,可以通过 module.exports 或者 exports 对象分别导出。例如:
// 导出一个变量
module.exports.name = 'Tom';
// 导出一个函数
exports.sayHello = function() {
console.log('Hello!');
};
在另一个模块中,可以通过 require 函数来引入其他模块,并访问其导出的内容。例如:
// 引入其他模块
var moduleA = require('./moduleA');
// 访问其他模块导出的变量
console.log(moduleA.name);
// 访问其他模块导出的函数
moduleA.sayHello();
5. 特点
- CommonJS 模块由 JS 运行时实现。
- CommonJS 模块输出的是值的拷贝,本质上导出的就是 exports 属性。
- CommonJS 是可以动态加载的,对每一个加载都存在缓存,可以有效的解决循环引用问题。
- CommonJS 模块同步加载并执行模块文件。
ES6 模块化
1. 概述
在 ES6 之前,JavaScript 并没有原生支持模块化,因此开发者们需要使用一些第三方库或者自己实现一些模块化方案来解决代码复用和管理问题。但是这些方案都有一些问题,比如命名冲突、依赖管理等。ES6 引入了 ESModule 模块化规范来解决这些问题。
ESModule 模块化规范是一种静态的模块化方案,它允许开发者将代码分割成小的、独立的模块,每个模块都有自己的作用域。ESModule 规范是基于文件的,每个文件都是一个独立的模块。
ESModule 的模块解析规则是基于 URL 解析规则的。当我们使用 import 语句导入一个模块时,模块加载器会根据 import 语句中指定的路径解析出对应的 URL,并将其作为唯一标识符来加载对应的模块文件。在浏览器中,URL 解析规则是基于当前页面的 URL 进行解析;在 Node.js 中,URL 解析规则是基于当前运行脚本的路径进行解析。
2. 模块的加载方式
ESModule 规范是基于文件的,每个文件都是一个独立的模块。在浏览器中,可以使用<script type="module">
标签来加载 ESModule 模块。在 Node.js 中,可以使用 import 关键字来加载 ESModule 模块。
<!-- 在浏览器中加载ESModule模块 -->
<script type="module" src="./module.js">
</script>
// 在Node.js中加载ESModule模块
import { name } from './module';
3. 模块的导出和导入方式
在 ESModule 中,使用 export 关键字将变量或者函数导出,使用 import 关键字导入其他模块中导出的变量或者函数。导出和导入方式有以下几种:
- 命名导出和命名导入
命名导出和命名导入是最常见的一种方式。可以将多个变量或者函数命名导出,也可以将多个变量或者函数命名导入。
// module.js
export const name = '张三';
export function sayHello() {
console.log('Hello');
}
// app.js
import { name, sayHello } from './module';
- 默认导出和默认导入
默认导出和默认导入是一种简单的方式,可以将一个变量或者函数作为默认导出,也可以将一个变量或者函数作为默认导入。
// module.js
export default 'Hello World';
// app.js
import message from './module';
- 混合命名和默认导出
混合命名和默认导出也是一种常见的方式,可以将多个变量或者函数命名导出,同时将一个变量或者函数作为默认导出。
// module.js
export const name = '张三';
export function sayHello() {
console.log('Hello');
}
export default 'Hello World';
// app.js
import message, { name, sayHello } from './module';
4. 特点:
- ES6 Module 静态的,不能放在块级作用域内,代码发生在编译时。
- ES6 模块输出的是值的引用,如果一个模块修改了另一个模块导出的值,那么这个修改会影响到原始模块。
- ES6 Module 可以导出多个属性和方法,可以单个导入导出,混合导入导出。
- ES6 模块提前加载并执行模块文件,
AMD
1. 概述
AMD 是 Asynchronous Module Definition 的缩写,即异步模块定义。它是由 RequireJS 的作者 James Burke 提出的一种模块化规范。AMD 规范的主要特点是:异步加载、提前执行。
2. 基本语法
在 AMD 规范中,一个模块通常由以下几个部分组成:
define(id?, dependencies?, factory);
其中:
-
id
:可选参数,表示模块标识符,一般为字符串类型。 -
dependencies
:可选参数,表示当前模块所依赖的其他模块。它是一个数组类型,每个元素表示一个依赖模块的标识符。 -
factory
:必需参数,表示当前模块的工厂函数。它是一个函数类型,用于定义当前模块的行为。
一个典型的 AMD 模块定义如下所示:
define('module1', ['module2', 'module3'], function(module2, module3) {
// 模块1的代码逻辑
return {
// 暴露给外部的接口
};
});
AMD 规范采用异步加载方式,它通过require
函数来加载一个或多个模块。require
函数接受一个数组类型的参数,每个元素表示一个待加载的模块标识符。当所有依赖模块加载完成后,require
函数才会执行回调函数。
require(['module1', 'module2'], function(module1, module2) {
// 所有依赖模块加载完成后执行的回调函数
});
AMD 模式可以用于浏览器环境,并且允许非同步加载模块,也可以根据需要动态加载模块。
CMD
1. 概述
CMD 是 Common Module Definition 的缩写,即通用模块定义。CMD 规范的主要特点是:按需加载、延迟执行。
2. 基本语法
//定义没有依赖的模块
define(function(require, exports, module){
exports.xxx = value
module.exports = value
})
//定义有依赖的模块
define(function(require, exports, module){
//引入依赖模块(同步)
var module2 = require('./module2')
//引入依赖模块(异步)
require.async('./module3', function (m3) {
})
//暴露模块
exports.xxx = value
})
// 引入该模块
define(function (require) {
var m1 = require('./module1')
var m4 = require('./module4')
m1.show()
m4.show()
})
CMD 规范专门用于浏览器端,模块的加载是异步的,模块使用时才会加载执行。CMD 规范整合了 CommonJS 和 AMD 规范的特点。
往期回顾
#
如何使用 TypeScript 开发 React 函数式组件?
# # # #6 个你必须明白 Vue3 的 ref 和 reactive 问题
# #
回复“加群”,一起学习进步