模块化是前端工程化的基石。早期开发者通过RequireJS实现了浏览器端的异步模块加载,而如今Webpack已经成为React项目的事实标准构建工具。两者的设计哲学完全不同:RequireJS是运行时加载器,Webpack是编译期打包器。理解这个差异,是从RequireJS迁移到Webpack的第一步,也是整个迁移过程中所有问题的根源。

一、RequireJS与Webpack的设计差异
RequireJS遵循AMD规范,模块在浏览器运行时按需加载。每个模块通过define函数声明依赖,加载器在运行时解析依赖关系、发起HTTP请求、执行回调。这种模式在HTTP/1.1时代解决了脚本阻塞和全局污染的问题,但代价是页面加载时会产生大量小请求,且依赖关系只有在运行时才能发现错误。
Webpack则把这一切搬到了构建阶段。它从入口文件出发,静态分析整个依赖树,把所有模块打包成一个或多个bundle文件。打包后的代码不再需要运行时加载器,依赖错误在编译时就能暴露。对于React这种组件树深层嵌套的框架来说,静态打包带来的确定性远比按需加载的灵活性重要。
另外一点关键区别是模块语法。RequireJS使用AMD的define和require,Webpack原生支持CommonJS和ES Module。React组件如果用ES Module的import和export语法编写,配合Webpack的Tree Shaking,还能剔除未使用的代码,这是AMD永远做不到的。
二、迁移前的准备工作:梳理现有模块结构
迁移不是直接换个配置文件就能完成的。动手之前,建议先做一次全面的模块依赖梳理。可以用r.js提供的优化器跑一次optimize,生成构建报告,列出所有模块及其依赖关系。重点排查三类问题:是否存在循环依赖、是否有通过requirejs.s.contexts这类内部API直接操作加载器的代码、以及是否有模块依赖路径不是通过配置而是硬编码的。
路径配置也需要提前整理。RequireJS的requirejs.config中定义的paths和shim是迁移的重点。例如原来的配置可能长这样:
// RequireJS 的配置
requirejs.config({
baseUrl: 'js',
paths: {
react: 'lib/react.min',
jquery: 'lib/jquery.min'
},
shim: {
jquery: { exports: '$' }
}
});这里的paths对应到Webpack就是resolve.alias,shim则由ProvidePlugin或imports-loader替代。把这张映射表提前列出来,迁移时会顺畅很多。
同时要统计AMD模块的写法差异。如果团队代码严格遵守AMD规范,每个文件都有define包裹,迁移可以脚本化处理;但如果存在大量CommonJS风格的require调用混用,就需要人工逐个确认。
三、核心迁移步骤:代码改造与配置搭建
1. 改写模块定义语法
AMD模块的标准写法是用define包裹依赖数组,迁移后要改成ES Module。以一个React组件为例,改造前后对比如下:
// 迁移前:AMD 写法
define(['react', 'jquery'], function(React, $) {
class Hello extends React.Component {
render() {
return React.createElement('div', null, 'Hello');
}
}
return Hello;
});
// 迁移后:ES Module 写法
import React from 'react';
import $ from 'jquery';
export default class Hello extends React.Component {
render() {
return <div>Hello</div>;
}
}依赖数组中的每一项变成一个import语句,return的值变成export default。如果模块返回的是多个导出项,改用多个export即可。这个过程可以用jscodeshift写一个自动化转换脚本,团队项目规模大的话非常值得投入。
2. 搭建Webpack基础配置
路径别名直接映射原RequireJS的paths配置:
const webpack = require('webpack');
const path = require('path');
module.exports = {
entry: './src/main.jsx',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
},
resolve: {
alias: {
react: path.resolve(__dirname, 'lib/react.min.js'),
jquery: path.resolve(__dirname, 'lib/jquery.min.js')
},
extensions: ['.js', '.jsx']
},
plugins: [
// 替代 RequireJS 的 shim 配置
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery'
})
],
module: {
rules: [
{ test: /\.jsx?$/, use: 'babel-loader', exclude: /node_modules/ }
]
}
};React组件使用JSX语法,必须配置babel-loader做转译。ProvidePlugin会把遇到的全局$自动注入jquery模块,这样那些没有显式声明依赖就使用$的老代码也能继续工作,减少一次性改动量。
3. 处理异步加载场景
RequireJS的require(['module'], callback)异步加载,在Webpack中对应import()动态导入。假设原来按需加载一个设置面板:
// 旧写法
require(['views/Settings'], function(Settings) {
$('#panel').mount(Settings);
});
// 新写法
import(/* webpackChunkName: "settings" */ './views/Settings')
.then(module => {
$('#panel').mount(module.default);
});Webpack会把动态导入的模块自动拆分成独立的chunk,按需下载,效果等同于RequireJS的按需加载,而且chunk的拆分和命名完全由构建工具管理。
四、迁移中常见的坑与解决方案
第一个高频问题是循环依赖。AMD对循环依赖有一定的运行时容错,而Webpack打包后循环依赖会导致某个模块拿到undefined的导出值。解决办法是重构依赖方向,把公共部分抽到第三个模块,或者把import改为在使用时才动态获取。
第二个坑是文本资源和样式文件。RequireJS有text插件加载模板,Webpack则需要对应的loader:html-loader、css-loader、file-loader等。React项目中模板通常已被JSX组件替代,但残留的HTML片段和CSS文件需要补充loader配置,否则构建会直接报错。
第三个问题是全局变量残留。有些老代码直接往window上挂载对象,绕过了模块系统。迁移后这类代码不会报错,但依赖关系变得不可追踪。建议用eslint的no-undef规则逐步清理,把全局变量改造成正式的模块导出。
最后是打包体积问题。RequireJS时代每个文件独立存在,改一个模块只需更新一个文件;Webpack全量打包后,任何改动都会使整个bundle缓存失效。解决办法是配置splitChunks把第三方库拆到独立chunk,并使用[contenthash]命名输出文件,让业务代码和框架代码的缓存互不影响。
五、迁移后的验证与收尾
迁移完成后不要急着删掉旧代码。建议先用Webpack Dev Server起一个本地环境,对照旧页面逐个功能点验证。可以借助webpack-bundle-analyzer生成依赖图谱,检查是否有模块被重复打包或者遗漏引用。
确认功能完整后,清理三类遗留物:r.js的构建配置文件、页面中残留的<script data-main>标签、以及全局的requirejs引用。把npm scripts统一改为Webpack构建命令,迁移工作才算真正收尾。整个过程建议分模块逐步推进,先迁移叶子节点模块,再迁移入口文件,风险最小,回滚也方便。