文章目录
-
- [1. 引言](#1. 引言)
- [2. 正文](#2. 正文)
-
- [2.1 从"脚本"到"模块"的进化](#2.1 从“脚本”到“模块”的进化)
- [2.2 ES Modules 核心语法实战](#2.2 ES Modules 核心语法实战)
- [2.3 构建工具(Webpack/Vite)的角色](#2.3 构建工具(Webpack/Vite)的角色)
- [3. 常见问题 (FAQ)](#3. 常见问题 (FAQ))
- [4. 总结](#4. 总结)
这一篇将解决前端工程化中最重要的问题之一:如何组织代码。在 ES Modules 出现之前,JavaScript 没有原生的模块系统,导致全局变量满天飞。这篇文章将带你彻底理解现代前端模块化方案。
1. 引言
在 ES6 出现之前,JavaScript 有一个著名的弱点:没有模块系统。
想象一下,如果你在一个网页里引入了 10 个 JS 文件:
html
<script src="utils.js"></script>
<script src="user.js"></script>
<script src="app.js"></script>
如果 utils.js 里定义了一个全局变量 name,而 user.js 里也定义了一个同名的 name,那么后引入的就会覆盖先引入的。这种"变量污染"和"依赖管理混乱",曾让大型前端项目维护起来举步维艰。
ES2015(ES6) 终于将模块化 带入了语言标准------这就是我们现在熟知的 ES Modules (ESM)。它让我们能够把代码拆分成一个个小文件,按需导入,互不干扰。
本文将带你掌握:
- 为什么模块化是现代前端工程的基石?
import和export的各种用法。- 为什么我们在开发时离不开 Webpack 或 Vite 等构建工具。
2. 正文
2.1 从"脚本"到"模块"的进化
1. 传统的"脚本"模式
在非模块化时代,每个 JS 文件都在全局作用域运行。文件 A 定义的变量,文件 B 能随便改,没有任何隐私可言。
2. 模块化的优势
ES Modules 将每个文件变成了一个独立的私有作用域。
- 隔离性:文件内部的变量默认不外泄,除非显式导出。
- 依赖明确:一眼就能看出这个文件依赖了哪些其他文件。
- 按需加载:配合构建工具,只加载当前页面需要的代码。
2.2 ES Modules 核心语法实战
ES Modules 主要通过 export 导出,通过 import 导入。注意:浏览器原生使用 ESM 时,<script> 标签需要加 type="module" 属性。
1. 命名导出
一个文件可以导出多个变量或函数。
javascript
// math.js
export const add = (a, b) => a + b;
export const sub = (a, b) => a - b;
导入时,名字必须与导出时一致,且需要用花括号 {}:
javascript
// main.js
import { add, sub } from './math.js';
console.log(add(1, 2)); // 3
2. 默认导出
一个文件只能有一个默认导出(通常用于导出主要功能或类)。导入时不需要花括号,且可以随意改名。
javascript
// User.js
export default class User {
constructor(name) {
this.name = name;
}
}
javascript
// main.js
import Person from './User.js'; // 随意命名为 Person
const u = new Person("Alice");
3. 导入导出混合与重命名
javascript
// main.js
// 给导入的 sub 改名为 minus
import { add, sub as minus } from './math.js';
// 全部导入到一个对象中(不推荐生产环境大量使用)
import * as MathUtil from './math.js';
console.log(MathUtil.add(1, 2));
4. 动态导入
静态 import 必须写在文件顶部。但有时候我们需要"按需"加载代码(比如点击某个按钮后才加载某个大模块)。这时可以使用 import() 函数,它返回一个 Promise。
javascript
button.addEventListener('click', async () => {
const module = await import('./heavyModule.js');
module.doSomething();
});
这是实现路由懒加载的核心技术。
2.3 构建工具(Webpack/Vite)的角色
你可能会问:"既然浏览器已经支持 type="module" 了,为什么我们还需要 Webpack、Vite 或 Rollup?"
因为直接使用裸 ESM 有几个问题:
- 兼容性:旧浏览器不支持。
- 网络请求多:如果有 100 个模块文件,浏览器会发 100 个 HTTP 请求,性能极差。
- 非 JS 资源 :浏览器无法直接理解
.vue、.tsx、.scss等文件。
构建工具做了什么?
- 打包 :把你的所有模块和依赖打包成一个或极少数的
bundle.js文件,减少请求。 - 转译:把你的 ES6+ 代码转译成浏览器能看懂的 ES5 代码。
- Tree Shaking :这是 ESM 带来的巨大红利。因为 ESM 是静态结构(
import必须在顶层),构建工具可以在打包时分析出哪些代码被使用了,哪些代码没被用到,然后把没用的代码裁剪掉,大幅减小包体积。
javascript
// 比如你从 lodash 导入了 10 个函数,但只用到了 1 个
import { debounce } from 'lodash';
// Webpack 会分析代码,最终打包结果里只包含 debounce 的代码,而不包含整个 lodash 库!
Webpack 构建流程(含 Tree Shaking)流程图
是
否
源码输入
JS/TS/CSS/资源文件
模块解析
识别 ESM/CJS 依赖
是否 ESM 规范?
静态分析
标记未使用导出
直接打包全部代码
Tree Shaking 优化
剔除死代码
模块转换
Babel/Terser 编译
代码分割
Code Splitting
资源压缩
JS/CSS 混淆压缩
产物输出
dist 目录
Vite 基于 Rollup,开发环境无打包、生产环境按需构建,Tree Shaking 更高效。
开发环境
生产环境
源码输入
ESM 优先
环境判断
无打包 Dev Server
按需编译单文件
Rollup 打包
静态分析依赖
热更新 HMR
实时反馈修改
Tree Shaking 深度优化
剔除未使用代码
代码分割 + 压缩
浏览器直接加载
产物输出
dist 目录
3. 常见问题 (FAQ)
Q1:require (CommonJS) 和 import (ESM) 有什么区别?
A:
require是 Node.js 早期使用的规范,是动态加载(运行时加载)。import是 ES6 标准,是静态加载(编译时确定依赖),这为 Tree Shaking 提供了可能。- 现在前端工程中,源代码统一写
import/export,Node.js 也在逐步全面拥抱 ESM。
Q2:我可以在 if 语句里写 import 吗?
A: 语法上的 import ... from ... 不可以,它必须放在顶层。但是你可以使用 import() 函数(动态导入),它可以写在任何地方,包括 if 语句里。
Q3:为什么我改了代码,浏览器刷新后还是旧的?
A: 这通常是因为模块文件被浏览器缓存了。在使用构建工具(如 Vite)开发时,通常有热更新(HMR)机制。如果是原生 ESM 开发,可以尝试在导入地址后加时间戳强制刷新,但这不适合生产环境。
4. 总结
模块化是现代前端工程的基石,它让我们从"写脚本"进化到了"写工程":
- ES Modules 提供了
import和export关键字,实现了文件的私有作用域和显式依赖。 - 命名导出适合工具库,默认导出适合组件或主类。
- 构建工具(Webpack/Vite)在兼容性、性能优化和资源处理上,弥补了原生 ESM 的短板。
最佳实践建议:
在业务开发中,默认使用 ES Modules 语法。不要在模块顶层写
var污染全局。善用动态import()实现路由懒加载,提升首屏加载速度。
下一篇预告 :JavaScript 不仅是脚本语言,它也是一门面向对象的语言。下一篇我们将深入讲解 Class(类)、继承以及私有字段。
如果觉得本文对你有帮助,请点赞👍、收藏⭐、关注👀,三连支持一下!
有问题欢迎在评论区留言:你在项目里是用 Webpack 还是 Vite?踩过哪些模块化的坑?