引言
JavaScript 诞生于 1995 年,最初只是浏览器中的一门"小脚本语言",用于完成表单验证、动画特效等简单任务。彼时,一个网页可能只有几十行 JavaScript 代码,模块化根本不是问题。但随着 Web 应用从"网页"进化到"应用"(SPA、PWA、SSR),代码量从几十行膨胀到几十万行,模块化与工程化成为前端开发的基石。
本文将系统梳理 JavaScript 模块化的演进历史,深入剖析 CommonJS、ES Module 两大主流规范,详解 Webpack、Vite、Rollup、esbuild 等构建工具的原理与用法,并涵盖 Babel、TypeScript、代码分割、环境管理和工程化最佳实践。每个知识点均配有完整可运行的代码示例。
1. 模块化演进历史
1.1 全局变量模式(远古时代)
最早的 JavaScript 代码全部写在全局作用域中,通过全局变量共享数据。
<!-- a.js -->
<script>
var name = 'Alice';
function greet() {
console.log('Hello, ' + name);
}
</script>
<!-- b.js -->
<script>
var name = 'Bob'; // 覆盖了 Alice!
function greet() {
console.log('Hi, ' + name);
}
</script>问题:全局命名冲突、依赖关系隐式、无作用域隔离。
1.2 命名空间模式
通过对象字面量将代码分组,减少全局变量数量。
// namespace.js
var MyApp = {
config: { apiUrl: '/api' },
utils: {
formatDate: function (date) {
return date.toISOString().slice(0, 10);
},
},
models: {},
views: {},
};
// 使用
MyApp.utils.formatDate(new Date()); // '2024-09-04'进步:减少全局变量数。不足:仍无法避免内部属性被外部篡改,依赖顺序仍靠手动管理。
1.3 IIFE 模块模式(立即执行函数)
利用 JavaScript 的函数作用域实现私有变量,是模块化的雏形。
// iife-module.js
var UserModule = (function () {
// 私有变量
var users = [];
var apiKey = 'secret123';
// 私有函数
function validateEmail(email) {
return email.includes('@');
}
// 公开 API
return {
addUser: function (name, email) {
if (!validateEmail(email)) throw new Error('Invalid email');
users.push({ id: users.length + 1, name, email });
},
listUsers: function () {
return users.map(function (u) {
return { id: u.id, name: u.name };
});
},
getCount: function () {
return users.length;
},
};
})();
// 使用
UserModule.addUser('Alice', 'alice@example.com');
UserModule.addUser('Bob', 'bob@example.com');
console.log(UserModule.getCount()); // 2
console.log(UserModule.apiKey); // undefined — 私有变量不可访问IIFE 变体——揭示模块模式(Revealing Module Pattern):
var Counter = (function () {
var count = 0;
function increment() {
count++;
}
function decrement() {
count--;
}
function getValue() {
return count;
}
// 在 return 时选择暴露哪些
return {
increment: increment,
decrement: decrement,
getValue: getValue,
};
})();1.4 AMD(Asynchronous Module Definition)
随着浏览器端代码日益复杂,需要异步加载模块。AMD 规范的代表实现是 RequireJS。
// 定义模块 — define(id?, dependencies?, factory)
define('math', ['dependency'], function (dep) {
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
return { add: add, subtract: subtract };
});
// 使用模块
require(['math'], function (math) {
console.log(math.add(2, 3)); // 5
});优点:异步加载、依赖前置声明。
缺点:语法冗余、回调嵌套、不适用于服务器端。
1.5 CommonJS(Node.js 原生)
Node.js 诞生后,服务端需要同步加载模块,CommonJS 应运而生。
// math.js
const add = (a, b) => a + b;
const subtract = (a, b) => a - b;
module.exports = { add, subtract };
// app.js
const math = require('./math');
console.log(math.add(2, 3)); // 5特点:同步加载、运行时执行、模块缓存、适用于服务器。后面有专门章节详解。
1.6 UMD(Universal Module Definition)
UMD 是一种兼容浏览器和 Node.js 的"万能"模块格式,本质上是 AMD + CommonJS + 全局变量的组合。
// umd-module.js
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD (RequireJS)
define(['lodash'], factory);
} else if (typeof module === 'object' && module.exports) {
// CommonJS (Node.js)
module.exports = factory(require('lodash'));
} else {
// 浏览器全局变量
root.MyModule = factory(root._);
}
})(this, function (_) {
var myModule = {
sayHello: function (name) {
return 'Hello, ' + name;
},
toUpper: function (str) {
return _.toUpper(str);
},
};
return myModule;
});1.7 ES Module(ES2015 标准)
ECMAScript 2015(ES6)从语言层面定义了模块系统,成为 JavaScript 的官方标准。后面有专门章节详解。
演进总结表
| 阶段 | 模式 | 加载方式 | 适用范围 |
|---|---|---|---|
| 1995–2005 | 全局变量 | 同步 | 浏览器 |
| 2005–2010 | IIFE / 命名空间 | 同步 | 浏览器 |
| 2010–2015 | AMD (RequireJS) | 异步 | 浏览器 |
| 2009–至今 | CommonJS | 同步 | Node.js |
| 2010–至今 | UMD | 通用 | 浏览器 + Node |
| 2015–至今 | ES Module | 静态/异步 | 浏览器 + Node |
2. CommonJS
CommonJS 是 Node.js 默认的模块系统,也是 npm 生态的基石。
2.1 module.exports 与 exports
每个文件都是一个模块,模块内部有一个 module 对象,module.exports 决定导出的内容。
// ----- greeter.js -----
function greet(name) {
return `Hello, ${name}!`;
}
const version = '1.0.0';
// 方式一:给 module.exports 赋值
module.exports = {
greet: greet,
version: version,
};
// 方式二:给 exports 添加属性
// exports.greet = greet;
// exports.version = version;
// 注意:不能直接 exports = { ... },这会切断 exports 与 module.exports 的引用关系
// 以下写法是**错误**的:
// exports = { greet, version }; // ❌ — 不会导出任何内容// ----- app.js -----
const greeter = require('./greeter');
console.log(greeter.greet('Alice')); // Hello, Alice!
console.log(greeter.version); // 1.0.0exports vs module.exports:exports 初始时是 module.exports 的引用。给 exports 添加属性等价于给 module.exports 添加属性。但直接给 exports 重新赋值会切断引用。
// 等价
exports.a = 1;
module.exports.a = 1;
// 不等价
exports = { a: 1 }; // ❌ module.exports 仍然是 {}
module.exports = { a: 1 }; // ✅2.2 require() 同步加载
require() 是同步操作,模块代码在 require 调用时同步执行并返回结果。
// ----- timer.js -----
console.log('Timer module is loading...');
const start = Date.now();
function elapsed() {
return Date.now() - start;
}
module.exports = { elapsed };
// ----- main.js -----
console.log('Before require');
const timer = require('./timer'); // 同步阻塞,直到 timer.js 执行完毕
console.log('After require');
console.log(timer.elapsed()); // 毫秒数输出顺序:
Before require
Timer module is loading...
After require
0 (或很小的数值)2.3 模块缓存
Node.js 会缓存已加载的模块,同一个模块多次 require 只会执行一次。
// ----- counter.js -----
let count = 0;
module.exports = {
increment: () => ++count,
getCount: () => count,
};
// ----- cache-demo.js -----
const c1 = require('./counter');
const c2 = require('./counter'); // 从缓存中取,不重新执行
c1.increment(); // count = 1
c1.increment(); // count = 2
console.log(c2.getCount()); // 2 — 因为 c1 和 c2 指向同一个模块实例
console.log(c1 === c2); // true可以通过 delete require.cache[modulePath] 清除指定模块的缓存(通常只在开发热更新场景下使用)。
2.4 循环依赖处理
当两个或多个模块相互引用时,CommonJS 通过"返回当前已导出的部分"来处理。
// ----- a.js -----
console.log('a.js 开始执行');
const b = require('./b');
console.log('a.js 中, b.done =', b.done);
exports.done = true;
console.log('a.js 执行完毕');
// ----- b.js -----
console.log('b.js 开始执行');
const a = require('./a');
console.log('b.js 中, a.done =', a.done);
exports.done = true;
console.log('b.js 执行完毕');
// ----- main.js -----
console.log('main.js 开始');
const a = require('./a');
console.log('main.js 中, a.done =', a.done);执行 node main.js 输出:
main.js 开始
a.js 开始执行
b.js 开始执行
b.js 中, a.done = undefined ← 关键!a 还没执行完,只能拿到部分导出的对象
b.js 执行完毕
a.js 中, b.done = true
a.js 执行完毕
main.js 中, a.done = true循环依赖是设计上应避免的模式。如果无法避免,注意在加载顺序上做适当安排。
2.5 CommonJS 与 ES Module 的互操作
Node.js 支持两种模块系统并存,但它们的互操作有一些规则。
在 ESM 中引入 CJS(推荐方式):
// ----- cjs-module.cjs -----
module.exports = {
name: 'CommonJS Module',
greet(name) {
return `Hi, ${name}`;
},
};
// ----- esm-module.mjs -----
import cjsModule from './cjs-module.cjs';
// 或者命名空间导入
import * as cjsNS from './cjs-module.cjs';
console.log(cjsModule.name); // CommonJS Module
console.log(cjsModule.greet('Alice')); // Hi, Alice默认情况下,CJS 的 module.exports 会被当作 ESM 的 default export。命名空间导入 (import *) 会将 CJS 的所有键作为命名导出。
在 CJS 中引入 ESM(必须用动态 import):
// ----- esm-module.mjs -----
export const name = 'ES Module';
export function greet(name) {
return `Hello, ${name}`;
}
// ----- cjs-app.cjs -----
async function main() {
const esm = await import('./esm-module.mjs');
console.log(esm.name); // ES Module
console.log(esm.greet('Bob')); // Hello, Bob
}
main();注意:在 CommonJS 中不能使用 require() 加载 ESM 模块,必须使用动态的 import()。
3. ES Module(ESM)
ES Module 是 ECMAScript 官方标准,目前所有现代浏览器和 Node.js(≥12)都已支持。
3.1 export 语法
命名导出(Named Export)
// ----- math.js -----
// 方式一:声明时直接导出
export const PI = 3.14159;
export function add(a, b) {
return a + b;
}
export class Calculator {
multiply(a, b) {
return a * b;
}
}
// 方式二:声明后统一导出
const E = 2.71828;
function subtract(a, b) {
return a - b;
}
export { E, subtract };
// 方式三:重命名导出
function internalFn() {}
export { internalFn as publicFn };默认导出(Default Export)
每个模块最多有一个默认导出。
// ----- logger.js -----
export default function log(message) {
console.log(`[LOG] ${message}`);
}
// 也可以导出对象/类
export default class Logger {
info(msg) {
console.log(`[INFO] ${msg}`);
}
}
// 或混合
const version = '1.0';
export default version;
export const author = 'Alice';聚合导出(Re-export / Barrel Export)
常用于创建"入口文件"统一导出多个模块。
// ----- utils/index.js -----
// 直接聚合导出(不引入当前作用域)
export { default as formatDate } from './formatDate';
export { validateEmail } from './validate';
export { isNumeric } from './isNumeric';
// 重新导出所有命名导出
export * from './stringUtils';
// 重新导出并排除某个
export * from './numbers';
export { add } from './math'; // 如果 numbers 中也有 add,这里会报冲突。可用 as 解决3.2 import 语法
命名导入
// 导入特定命名
import { PI, add, Calculator } from './math';
// 导入并重命名
import { add as addNumbers } from './math';
// 导入所有命名(命名空间导入)
import * as math from './math';
console.log(math.PI);
// 导入默认 + 命名
import log, { author } from './logger';默认导入
import log from './logger';
import Logger from './logger';
import { default as log } from './logger'; // 等价动态 import()
import() 是一个类似函数的表达式,返回 Promise,用于按需加载。
// ----- 按需加载模块 -----
async function loadChart() {
try {
const chartModule = await import('./charts/barChart.js');
const chart = new chartModule.BarChart('#container');
chart.render(data);
} catch (err) {
console.error('模块加载失败', err);
}
}
// 或在事件回调中使用
button.addEventListener('click', async () => {
const { showToast } = await import('./toast.js');
showToast('加载成功!');
});import.meta
import.meta 是一个对象,包含当前模块的元信息。
// ----- 获取当前模块 URL -----
console.log(import.meta.url);
// 浏览器: https://example.com/js/app.js
// Node.js: file:///home/user/project/src/app.js
// ----- Node.js 中获取当前文件路径 -----
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// ----- import.meta.resolve (Stage 4) -----
// 解析模块标识符的绝对 URL,不实际加载
// const specifier = await import.meta.resolve('./utils.js');3.3 静态分析 vs 动态执行
ES Module 最大的设计特点是静态结构——import 和 export 必须在模块的顶层作用域,不能放在条件语句或函数中。
// ✅ 正确 — 顶层作用域
import { add } from './math.js';
// ❌ 错误 — 不能在条件语句中
if (needMath) {
import { add } from './math.js'; // SyntaxError
}
// ✅ 动态导入则用 import()
if (needMath) {
const { add } = await import('./math.js');
}
// ❌ 错误 — 不能在函数中
function load() {
export const x = 1; // SyntaxError
}静态分析带来的优势:
- Tree Shaking:构建工具可在编译时确定哪些导出被使用,移除未使用的代码
- 更快的查找:导入导出关系的确定在解析阶段完成,不需运行代码
- 循环依赖更可靠:ESM 使用"实时绑定"(live bindings),导入的是变量引用而非值拷贝
3.4 Tree Shaking 原理
Tree Shaking 依赖于 ES Module 的静态结构。构建工具(如 Webpack、Rollup)在打包时分析模块依赖图,移除没有被使用的导出。
// ----- math.js -----
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
export function multiply(a, b) {
return a * b;
}
export function divide(a, b) {
return a / b;
}
// ----- app.js -----
import { add } from './math.js';
console.log(add(2, 3));在 Tree Shaking 后,subtract、multiply、divide 被移除,最终打包产物只包含 add 函数。
实际生效条件:
- 使用 ES Module 语法(
import/export) - 在
package.json中设置"sideEffects": false或精确指定有副作用的文件 - 构建工具配置正确的 production 模式
// package.json
{
"sideEffects": [
"*.css",
"*.scss",
"./src/polyfills.js"
]
}3.5 严格模式
ES Module 自动启用严格模式(strict mode),无法取消。
// 在 ESM 中,以下行为都会报错
x = 10; // ReferenceError: x is not defined
delete x; // SyntaxError
function fn(a, a) {} // SyntaxError: duplicate parameter
with (obj) { ... } // SyntaxError这也意味着 this 在模块顶层是 undefined 而不是 window。
3.6 浏览器中的 ESM
现代浏览器原生支持 <script type="module">。
<!-- 作为入口模块 -->
<script type="module" src="/src/main.js"></script>
<!-- 内联模块 -->
<script type="module">
import { createApp } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js';
console.log('ES Module 已加载');
</script>
<!-- 常规 script vs module 对比 -->
<script src="lib.js"></script> <!-- 默认 defer=false,阻塞渲染 -->
<script type="module" src="app.js"></script> <!-- 默认 defer,不阻塞渲染 -->关键特性:
- 模块默认是
defer的,按顺序执行,不阻塞 HTML 解析 - 模块代码只执行一次
- 模块使用 CORS 策略,跨域需设置响应头
Access-Control-Allow-Origin - 模块可以引用其他模块,使用相对路径或绝对 URL
- 不支持裸模块名(bare import specifier),如
import _ from 'lodash'— 浏览器不知道该去哪里找,需要 importmap
importmap(浏览器端解决 bare import 的方案):
<script type="importmap">
{
"imports": {
"lodash": "https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js",
"vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.js"
}
}
</script>
<script type="module">
import _ from 'lodash';
import { createApp } from 'vue';
console.log(_.join(['Hello', 'World'], ' '));
</script>3.7 Node.js 中的 ESM
在 Node.js 中使用 ESM,有两种方式:
方式一:文件扩展名
.mjs— 始终视为 ESM.cjs— 始终视为 CommonJS.js— 取决于package.json中的type字段
方式二:package.json type 字段
{
"name": "my-project",
"type": "module", // 所有 .js 文件视为 ESM
"exports": {
".": "./src/index.js",
"./utils": "./src/utils.js"
}
}如果 type: "module",但某个文件需要保持 CommonJS,使用 .cjs 扩展名。
Node.js ESM 与 CJS 差异速查:
| 特性 | CommonJS | ES Module |
|---|---|---|
| 默认扩展名 | .js (type: commonjs) |
.mjs 或 .js (type: module) |
require() |
✅ | ❌ |
import |
❌ | ✅ |
__dirname |
✅ | ❌ (可用 import.meta.url 计算) |
__filename |
✅ | ❌ |
顶层 await |
❌ | ✅ |
| JSON 导入 | require('./data.json') |
import data from './data.json' (需 flag) |
| 循环依赖 | 返回部分导出 | Live binding,更可靠 |
Node.js 中实现 __dirname 和 __filename:
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
console.log(__dirname); // 与 CJS 中的 __dirname 行为一致顶层 await:
// 只能在 ESM 模块中使用
const response = await fetch('https://api.example.com/config');
const config = await response.json();
export default config;4. 打包工具
4.1 Webpack
Webpack 是目前最成熟的打包工具,核心概念包括 entry、output、loader、plugin。
基本配置
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'production', // development | production | none
entry: './src/index.js', // 入口文件
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash].js', // 带内容哈希的文件名
clean: true, // 构建前清理 dist 目录
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
},
},
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'], // 从右到左执行
},
{
test: /\.(png|jpg|svg)$/,
type: 'asset/resource', // Webpack 5 内置资源模块
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
optimization: {
splitChunks: {
chunks: 'all', // 代码分割
},
},
};Loader 的作用
Loader 让 Webpack 能处理非 JavaScript 文件。常见 loader:
| Loader | 用途 |
|---|---|
babel-loader |
转译 ES6+ 到 ES5 |
css-loader |
解析 CSS 中的 @import 和 url() |
style-loader |
将 CSS 注入到 DOM 的 <style> 标签 |
sass-loader |
编译 SCSS/SASS |
ts-loader |
编译 TypeScript |
file-loader |
处理图片/字体文件 |
url-loader |
小文件转 Base64 data URL |
Plugin 的作用
Plugin 用于执行更广泛的任务,如打包优化、资源管理、环境变量注入。
// 常用 plugins
const HtmlWebpackPlugin = require('html-webpack-plugin'); // 生成 HTML
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); // 提取 CSS 文件
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); // 压缩 CSS
const { DefinePlugin } = require('webpack'); // 定义全局变量
new DefinePlugin({
'process.env.API_URL': JSON.stringify('https://api.example.com'),
}),HMR(Hot Module Replacement)
开发时只替换变化的模块,不刷新页面,保留应用状态。
// webpack.config.js (dev)
module.exports = {
devServer: {
hot: true, // 启用 HMR
port: 3000,
historyApiFallback: true, // SPA 路由支持
},
};模块需要自己声明是否接受 HMR:
if (module.hot) {
module.hot.accept('./component.js', () => {
console.log('Component updated!');
render(); // 重新渲染
});
}4.2 Vite
Vite 利用浏览器原生 ESM 实现极速开发体验。
基本原理
- 开发环境:基于 ESM 的 Dev Server,按需编译,无需打包
- 生产环境:使用 Rollup 打包以获得更好的 Tree Shaking 和代码优化
基本配置
// vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import path from 'path';
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
server: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
},
},
},
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['vue', 'vue-router', 'pinia'],
},
},
},
},
});Vite vs Webpack 对比
| 特性 | Vite | Webpack |
|---|---|---|
| 开发服务器启动 | 秒级(无需打包) | 慢(需全量打包) |
| HMR 速度 | 模块级(仅编译变更文件) | 模块级(但需重新打包) |
| 生产构建 | 基于 Rollup | 自带打包器 |
| 配置复杂度 | 简洁(内置常见 loader) | 繁琐(需手动配置 loader) |
| 插件生态 | 丰富但不如 Webpack | 最丰富 |
| 兼容性 | 需要现代浏览器(开发时) | 更好(通过 loader 转译) |
| 适用场景 | 新项目、Vue/React SPA | 大型复杂项目、需要精细控制 |
4.3 Rollup
Rollup 专注于 ES Module 打包,以卓越的 Tree Shaking 能力著称,是库打包的首选。
// rollup.config.js
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import babel from '@rollup/plugin-babel';
import terser from '@rollup/plugin-terser';
export default {
input: 'src/index.js',
output: [
{
file: 'dist/bundle.cjs.js',
format: 'cjs', // CommonJS
},
{
file: 'dist/bundle.esm.js',
format: 'es', // ES Module
},
{
file: 'dist/bundle.umd.js',
format: 'umd',
name: 'MyLibrary',
},
],
plugins: [
resolve(), // 解析 node_modules
commonjs(), // 将 CJS 转为 ESM
babel({ babelHelpers: 'bundled' }),
terser(), // 压缩输出
],
external: ['react', 'react-dom'], // 外部依赖,不打包
};4.4 esbuild
esbuild 是用 Go 语言编写的极速构建工具,速度是传统工具的 10-100 倍。
// esbuild 使用示例(CLI)
// $ esbuild src/index.js --bundle --outfile=dist/bundle.js --minify
// 编程式使用
const esbuild = require('esbuild');
esbuild.build({
entryPoints: ['src/index.js'],
bundle: true,
outfile: 'dist/bundle.js',
minify: true,
sourcemap: true,
target: ['chrome58', 'firefox57', 'safari11', 'edge16'],
loader: {
'.js': 'jsx',
'.ts': 'ts',
'.css': 'css',
},
}).then(result => {
console.log('构建成功!');
}).catch(() => process.exit(1));esbuild 通常作为底层工具被 Vite、Snowpack、Turbopack 等使用,而非直接代替 Webpack 作为应用打包工具。
4.5 对比表
| 工具 | 语言 | 速度 | 核心优势 | 适用场景 |
|---|---|---|---|---|
| Webpack | JS | 中等 | 生态最丰富、配置灵活 | 大型应用、需要精细控制 |
| Vite | Go + JS | 极快 | 开发服务器秒启动、HMR 快 | 现代 SPA 开发 |
| Rollup | JS | 中等 | Tree Shaking 最佳、多格式输出 | 库/组件库打包 |
| esbuild | Go | 最快 | 构建速度极快 | 作为基础设施、简单项目 |
| Turbopack | Rust | 最快 | Rust 原生、增量构建 | (发展初期)下一代 Webpack |
| Parcel | JS | 快 | 零配置 | 小型项目、快速原型 |
5. Babel 与 TypeScript
5.1 Babel
Babel 是 JavaScript 编译器,将 ES6+ 代码转换为向后兼容的版本。
@babel/preset-env
智能预设,根据目标环境自动确定需要的转译和 polyfill。
// babel.config.js
module.exports = {
presets: [
['@babel/preset-env', {
targets: {
browsers: ['> 1%', 'last 2 versions', 'not dead'],
// 或指定 Node.js 版本
// node: '14',
},
useBuiltIns: 'usage', // 按需引入 polyfill
corejs: 3, // core-js 版本
modules: false, // 保留 ES Module 语法(让 Webpack 做 Tree Shaking)
}],
],
plugins: [
// 如果有需要,添加单独插件
'@babel/plugin-transform-runtime', // 复用辅助函数,减小体积
],
};Polyfill 方案
| 方案 | 配置 | 说明 |
|---|---|---|
useBuiltIns: 'entry' |
在入口手动 import 'core-js' |
引入目标环境缺失的所有 polyfill |
useBuiltIns: 'usage' |
自动检测使用 | 根据代码中用到的 API 按需引入 |
useBuiltIns: false |
不处理 | 需要自己管理 polyfill |
@babel/plugin-transform-runtime |
作为插件 | 不污染全局,适合库开发 |
5.2 TypeScript
TypeScript 是为 JavaScript 添加类型系统的超集。
// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist",
"rootDir": "./src",
"declaration": true, // 生成 .d.ts 类型声明
"sourceMap": true,
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"jsx": "react-jsx"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}Babel vs TypeScript Compiler
| 特性 | @babel/preset-typescript |
tsc (TypeScript Compiler) |
|---|---|---|
| 类型检查 | ❌(只移除类型语法) | ✅ |
| 编译速度 | 快(仅转译) | 慢(类型检查 + 转译) |
生成 .d.ts |
❌ | ✅ |
| 转译能力 | ✅(Babel 生态) | 有限 |
| 工作流 | Babel + 单独 tsc --noEmit |
单工具 |
推荐工作流:
// package.json
{
"scripts": {
"build:types": "tsc --emitDeclarationOnly", // 仅生成类型声明
"build:js": "babel src --out-dir dist", // Babel 转译
"build": "npm run build:types && npm run build:js",
"type-check": "tsc --noEmit" // 单独类型检查
}
}或者在 Vite 中通过 vite-plugin-checker 实现并行类型检查:
// vite.config.js
import checker from 'vite-plugin-checker';
export default defineConfig({
plugins: [
checker({ typescript: true }), // 在独立线程中运行 tsc --noEmit
],
});6. 代码分割
代码分割(Code Splitting)将代码拆分成多个小块,按需加载,减少首屏加载体积。
6.1 动态 import() 实现懒加载
// ----- 图片懒加载模块 -----
// heavy-image-gallery.js
export function initGallery(containerId) {
console.log(`初始化图片画廊: ${containerId}`);
// ... 大量的库代码
}
// ----- 按需加载 -----
document.getElementById('show-gallery').addEventListener('click', async () => {
// import() 返回 Promise,Webpack/Vite 会自动分割代码
const { initGallery } = await import('./heavy-image-gallery.js');
initGallery('gallery-container');
});6.2 React.lazy + Suspense
React 16.6 引入了 React.lazy 和 Suspense。
// ----- App.jsx -----
import React, { Suspense, lazy } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
// 懒加载组件
const HomePage = lazy(() => import('./pages/HomePage'));
const AboutPage = lazy(() => import('./pages/AboutPage'));
const DashboardPage = lazy(() => import('./pages/DashboardPage'));
// 加载指示器
function PageLoader() {
return <div className="page-loader">Loading...</div>;
}
function App() {
return (
<BrowserRouter>
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="/dashboard" element={<DashboardPage />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
export default App;Webpack 打包时,每个 lazy() 的组件会被分割成独立的 chunk(代码块)。
6.3 路由级代码分割
// 配合 React Router v6
import { lazy, Suspense } from 'react';
import { Routes, Route, Outlet } from 'react-router-dom';
const Users = lazy(() => import('./pages/Users'));
const UserDetail = lazy(() => import('./pages/UserDetail'));
const Settings = lazy(() => import('./pages/Settings'));
function SuspenseLayout() {
return (
<div>
<header>导航栏</header>
<Suspense fallback={<div>加载中...</div>}>
<Outlet /> {/* 路由出口,懒加载子路由 */}
</Suspense>
</div>
);
}
function AppRoutes() {
return (
<Routes>
<Route element={<SuspenseLayout />}>
<Route path="/users" element={<Users />} />
<Route path="/users/:id" element={<UserDetail />} />
<Route path="/settings" element={<Settings />} />
</Route>
</Routes>
);
}6.4 预加载与预获取
<link rel="preload"> 和 <link rel="prefetch"> 可以帮助浏览器在空闲时提前加载资源。
Webpack 中的魔法注释:
// 预获取(prefetch)— 浏览器空闲时加载,用于"将来可能用到"的资源
const AboutPage = import(/* webpackPrefetch: true */ './pages/AboutPage');
// 预加载(preload)— 当前页面需要立即用到的资源
const ChartLib = import(/* webpackPreload: true */ './lib/chart');
// 自定义 chunk 名称
const LazyComponent = import(/* webpackChunkName: "dashboard" */ './Dashboard');
// 在构建工具中也会自动注入对应的 <link>Vite 中的预加载:
// Vite 自动处理动态 import 的预加载,也可手动配置
// vite.config.js
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
// 将第三方库自动分割
if (id.includes('lodash')) return 'vendor_lodash';
if (id.includes('react')) return 'vendor_react';
}
},
},
},
},
});浏览器原生:
<!-- 预加载关键 CSS -->
<link rel="preload" href="/styles/critical.css" as="style">
<!-- 预获取非关键 JS -->
<link rel="prefetch" href="/scripts/analytics.js">
<!-- 预加载字体 -->
<link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin>7. 环境变量与配置管理
7.1 .env 文件
在项目根目录使用 .env 文件管理不同环境的配置。
# .env — 所有环境共享
APP_NAME=MyApp
VERSION=1.0.0
# .env.development — 开发环境
API_URL=http://localhost:3000/api
DEBUG=true
# .env.production — 生产环境
API_URL=https://api.example.com
DEBUG=false
# .env.local — 本地覆盖,不提交到 Git
# (已在 .gitignore 中忽略)
DB_PASSWORD=local_dev_only7.2 Vite 的环境变量
Vite 使用 import.meta.env 暴露环境变量。
// Vite 中环境变量必须以 VITE_ 开头才会暴露给客户端
console.log(import.meta.env.VITE_API_URL);
console.log(import.meta.env.MODE); // development | production
console.log(import.meta.env.PROD); // boolean
console.log(import.meta.env.DEV); // boolean
console.log(import.meta.env.SSR); // boolean// vite.config.js
import { defineConfig, loadEnv } from 'vite';
export default defineConfig(({ mode }) => {
// 加载当前模式的 env
const env = loadEnv(mode, process.cwd(), '');
return {
define: {
// 自定义全局常量
__APP_VERSION__: JSON.stringify(env.APP_VERSION),
},
server: {
proxy: {
'/api': {
target: env.API_URL,
},
},
},
};
});7.3 Webpack 的环境变量
Webpack 使用 DefinePlugin 或 EnvironmentPlugin。
// webpack.config.js
const webpack = require('webpack');
module.exports = (env, argv) => {
const isDev = argv.mode === 'development';
return {
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(argv.mode),
'process.env.API_URL': JSON.stringify(
isDev ? 'http://localhost:3000/api' : 'https://api.example.com'
),
__DEV__: isDev,
}),
// 简写:自动读取 .env 中的变量
new webpack.EnvironmentPlugin(['NODE_ENV', 'API_URL']),
],
};
};在代码中直接使用:
if (process.env.NODE_ENV === 'development') {
console.log('开发模式');
}
const API_BASE = process.env.API_URL;7.4 不同环境配置策略
// package.json——多环境脚本
{
"scripts": {
"dev": "vite", // 开发
"build:dev": "vite build --mode development", // 开发环境构建
"build:staging": "vite build --mode staging", // 预发布构建
"build:prod": "vite build --mode production", // 生产构建
"analyze": "ANALYZE=true vite build", // 分析构建产物
"build:legacy": "vite build --mode legacy" // 兼容旧浏览器
}
}// 创建不同环境的构建配置
// vite.config.js
export default defineConfig(({ mode }) => {
const configs = {
development: {
sourcemap: true,
minify: false,
},
staging: {
sourcemap: true,
minify: 'esbuild',
},
production: {
sourcemap: false,
minify: 'terser',
},
legacy: {
target: ['ie >= 11'],
plugins: [legacyPlugin()],
},
};
return {
...configs[mode] || configs.development,
// 公共配置
plugins: [vue(), ...(configs[mode]?.plugins || [])],
};
});8. 工程化最佳实践
8.1 ESLint + Prettier 代码规范
ESLint 负责代码质量与逻辑错误,Prettier 负责代码格式。
// .eslintrc.json
{
"env": {
"browser": true,
"es2021": true,
"node": true
},
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:@typescript-eslint/recommended",
"prettier" // 必须放在最后,覆盖 ESLint 的格式规则
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": ["react", "@typescript-eslint"],
"rules": {
"no-console": "warn",
"react/react-in-jsx-scope": "off",
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }]
}
}// .prettierrc
{
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"tabWidth": 2,
"printWidth": 100,
"arrowParens": "always",
"endOfLine": "lf"
}集成到 VS Code:
// .vscode/settings.json
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"]
}8.2 Husky + lint-staged Git 钩子
在提交代码前自动检查和格式化。
# 安装
npx husky-init
npm install lint-staged --save-dev// package.json
{
"scripts": {
"prepare": "husky install",
"lint-staged": "lint-staged"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"],
"*.{json,md,css,scss}": ["prettier --write"]
}
}# .husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged# .husky/commit-msg — 配合 commitlint 校验提交信息
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no -- commitlint --edit $1// commitlint.config.js
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2, 'always',
['feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore', 'perf'],
],
},
};8.3 包管理
三种主流包管理器对比:
| 特性 | npm | yarn | pnpm |
|---|---|---|---|
| 安装速度 | 慢 | 中等 | 快(硬链接 + 全局存储) |
| 磁盘占用 | 高 | 高 | 低(共享存储) |
| 锁文件 | package-lock.json |
yarn.lock |
pnpm-lock.yaml |
| workspace 支持 | ✅ | ✅ | ✅(原生支持) |
| 严格模式 | ❌ | ❌ | ✅(幽灵依赖隔离) |
pnpm 的优势:
# pnpm 使用内容寻址的文件系统,相同版本的包只存一份
# 所有项目共享,通过硬链接引用
# 安装依赖
pnpm install
# 添加依赖
pnpm add lodash
# 添加开发依赖
pnpm add -D typescript
# monorepo 工作区
pnpm -F @project/ui build# pnpm-workspace.yaml
packages:
- 'packages/*'
- 'apps/*'8.4 monorepo 方案简介
Monorepo 将多个相关项目放在同一个仓库中管理。
常见工具:
| 工具 | 特点 |
|---|---|
| pnpm workspace | 原生支持、轻量 |
| Turborepo | 增量构建、缓存、智能任务调度 |
| Nx | 完整 monorepo 解决方案,含生成器、依赖图 |
| Changesets | 版本管理与发布 |
Turborepo 示例:
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": [".env"],
"pipeline": {
"build": {
"dependsOn": ["^build"], // 依赖项目先构建
"outputs": ["dist/**", ".next/**"],
"cache": true
},
"test": {
"dependsOn": ["build"],
"outputs": []
},
"lint": {
"outputs": []
},
"dev": {
"cache": false,
"persistent": true
}
}
}8.5 CI/CD 基础
GitHub Actions 示例:
# .github/workflows/ci.yml
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
lint-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Lint
run: pnpm lint
- name: Type Check
run: pnpm type-check
- name: Test
run: pnpm test -- --coverage
- name: Upload coverage
uses: codecov/codecov-action@v3
build-and-deploy:
needs: lint-and-test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install
run: pnpm install
- name: Build
run: pnpm build
env:
VITE_API_URL: ${{ secrets.VITE_API_URL }}
- name: Deploy to Vercel
uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
vercel-args: '--prod'总结
JavaScript 模块化经历了从全局变量到原生 ES Module 的漫长演进。在当今的前端工程化实践中:
- 模块规范选择:新项目优先使用 ES Module;Node.js 后端项目可混用 CJS 和 ESM
- 构建工具选择:SPA 应用选 Vite;大型复杂项目选 Webpack;库/组件库选 Rollup
- TypeScript:强烈推荐使用,搭配 ESLint + Prettier 规范代码质量
- 代码分割:利用动态
import()和路由级分割优化性能 - 工程化基建:ESLint、Husky、CI/CD 是项目质量的基础保障
- Monorepo:多包项目优先考虑 pnpm workspace + Turborepo
模块化和工程化不是目的,而是手段——最终目标是为用户交付高质量、高性能的 Web 应用。
参考资源