前端工程化 | 命名空间与模块化
前端工程化 | JS 的模块化
前端项目的模块化可以从三个角度理解,同时也反映了 JavaScript 文件组织方式逐层深入的发展过程:
1 - 函数的模块化
最原始的函数模块化方式即直接在 JS 文件中定义全局函数来实现特定功能
1 | function method1() {...} |
随着项目复杂化,这种方式会有函数命名冲突,全局变量污染的问题
在 ES6 标准之前,JS 语言并不支持类,因此需要通过特定写法对此进行优化,主要有两种方式:
命名空间(namepace)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16var MyNameSpace = MyNameSpace || {};
MyNameSpace.Module1 = function() {
function privateFunc() {
console.log("private function");
}
return {
publicFunc: function() {
console.log("public function");
privateFunc();
}
};
}();
MyNameSpace.Module1.publicFunc();
// public function
// private function立即执行函数(IIFE)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20(function(global) {
let data = "data"
function func1() {
console.log("function 1");
}
function func2() {
console.log("function 2");
privateFunc();
}
function privateFunc() {
console.log("private function");
}
global.MyIIFE = {func1, func2}
})(global)
MyIIFE.func1();
MyIIFE.func2();
// function 1
// function 2
// private function封装在匿名函数中的函数方法有独立作用域,需要主动暴露出接口供外部访问
IIFE 方式可以进一步通过传递对象参数管理依赖关系:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26var GlobalContainer = GlobalContainer || {};
GlobalContainer.Mod1 = (function(container) {
let mod = {
publicMeth: function() {
console.log("module 1");
}
}
container.Mod1 = mod;
return container.Mod1;
})(GlobalContainer);
GlobalContainer.Mod2 = (function(container) {
let mod = {
publicMeth: function() {
console.log("module 2 depends on module 1");
container.Mod1.publicMeth();
}
}
container.Mod2 = mod
return container.Mod2;
})(GlobalContainer);
GlobalContainer.Mod2.publicMeth();
// module 2 depends on module 1
// module 1
在 TypeScript 中,有提供封装好的 Namespace 机制,其内部实现正是 IIFE 方式
2 - JS 文件的模块化
函数层面的模块化一定程度上解决了全局污染和依赖管理的问题,但随着项目的复杂化, JavaScript 文件数量变得十分庞大,传统的 JS 引入方式依赖关系不清晰,需要人工按照严格的先后顺序引入 JS ,难以维护
因此,出现了文件层面的模块化方案,形成了如下模块化标准:
CommonJS
最早发布的常用模块化标准,是开发者社区提出的民间规范
1
2
3
4
5
6
7
8// 导出模块
module.exports = function() {
...
}
// 导入模块
const module1 = require('./module1');
...基于 CommonJS 标准的模块化代码支持 Node.js 环境和浏览器环境,但通常运行在服务器中,模块为运行时同步加载,即 JS 文件间的依赖关系在代码运行后才能确定
AMD (Asynchronous Module Definition)
运行在浏览器环境,模块异步加载,能够保证浏览器不会总是等待从服务器获取模块,避免阻塞卡死
1
2
3
4
5
6
7
8
9// 定义模块(依赖模块 mod1、mod2)
define(['mod1', 'mod2'], function(m1, m2) {
...
return module
})
// 引入模块(加载模块 mod1、mod2,完成后执行回调函数)
require(['mod1', 'mod2'], function(m1, m2) {
...
})CMD 可以理解为 CommonJS 与 AMD 特点的结合,模块同样为异步加载,专用于浏览器环境
EcmaScript Module(ESM)
ES6 原生提供的官方模块化标准,浏览器、Node、构建工具三种运行环境全部支持,模块为编译时静态加载,即 JS 文件间依赖关系在编译时确定,不需要运行代码
1
2
3
4
5
6
7
8// 导出模块
export function func1() {
...
}
// 导入模块
import { func1 } from './module1';
...由于模块编译时静态加载,ESM 代码可以进行 Tree-shaking 优化
现代前端项目基本均在构建工具环境中运行,而不同构建工具支持的代码模块化标准不同(如 Webpack 同时支持 CJS 和 ESM,而 rollup/esbuilder 仅支持 ESM),绝大多数情况下推荐使用 ESM 标准
3 - 模块的模块化(包管理)
模块的集合称为包
本质上,包就是对多个 JS 模块的进一步模块化聚合
为解决前端在某一方面的问题,社区中涌现出了很多第三方库和框架(如 axios、jQuery、vue/react),这些均可以看作是包
“库”和“框架”是从功能角度划分的概念,从结构的角度上说,它们都属于包
向开发者提供这些封装好的包时,用传统的 git 等方式共享会存在难以控制版本、管理依赖等弊端,因此出现了一些专门的包管理工具:
- npm
最常用的 Node.js 官方包管理器,
pnpm
yarn
4 - 打包/构建工具
Webpack
Rollup
小结
JS 的模块化标准解决了文件作用域的问题,并统一了文件的导入导出方式
模块化代码的运行环境有三种:
- 浏览器环境
- Node 环境
- 构建工具执行





