RequireJS 完全指南:从入门到精通的模块化开发实践
虽然现在 ES Modules(ESM)已经成为前端模块化的主流,但在很多老项目、遗留系统中,RequireJS 依然是核心的模块加载器。最近帮团队重构一个老项目,又重新梳理了一遍 RequireJS 的完整用法,发现它的很多设计思想至今依然值得学习。
今天就结合官方文档和实战经验,把 RequireJS 从入门到精通的所有内容整理出来,从基本用法到高级配置,再到避坑指南,一篇文章讲透。
一、先搞懂:RequireJS 是什么?解决了什么问题?
RequireJS 是一个轻量级的 JavaScript 文件和模块加载器,是 AMD(Asynchronous Module Definition,异步模块定义)规范的最佳实现者之一。最新版本压缩后仅约 14KB,非常轻量,且兼容性极佳:
| 浏览器 |
兼容性 |
| IE 6+ |
✅ |
| Firefox 2+ |
✅ |
| Safari 3.2+ |
✅ |
| Chrome 3+ |
✅ |
| Opera 10+ |
✅ |
它不仅优化了浏览器端的模块加载,也能在 Node.js、Rhino 等 JavaScript 环境中使用。
传统开发的痛点
在没有模块化加载器的时代,我们是这样写代码的:
1 2 3 4 5 6 7 8
| <script src="js/jquery.js"></script> <script src="js/underscore.js"></script> <script src="js/backbone.js"></script> <script src="js/a.js"></script> <script src="js/b.js"></script> <script src="js/c.js"></script>
|
这种方式带来了三个致命问题:
- JS 阻塞页面渲染:script 标签默认同步加载,会阻塞 DOM 渲染,导致页面白屏;
- 全局变量污染:所有模块的变量都挂在 window 上,容易命名冲突;
- 依赖关系混乱:必须手动保证 script 标签的加载顺序(比如 jQuery 必须在插件之前加载),维护成本极高。
RequireJS 的核心价值
RequireJS 就是为了解决这些问题而生的,它能带来:
- ✅ 异步加载,不阻塞渲染:模块并行加载,页面先显示内容;
- ✅ 模块化开发,避免全局污染:每个模块有独立作用域,通过 define/require 管理依赖;
- ✅ 自动管理依赖关系:不用手动维护 script 标签顺序,RequireJS 自动处理;
- ✅ 支持 CDN fallback:远程 CDN 加载失败时,自动切换到本地文件;
- ✅ 内置优化工具:r.js 可以将多个模块合并压缩,提升生产环境性能。
二、快速上手:5分钟写出第一个 RequireJS 模块
1. 下载与引入
去 RequireJS 官网 下载 require.js,放到项目的 js 目录下。
2. 目录结构建议
为了方便管理,推荐这样的目录结构:
1 2 3 4 5 6 7 8 9 10 11
| project/ ├── index.html └── js/ ├── require.js # RequireJS 核心文件 ├── main.js # 入口文件(data-main 指定) ├── app/ # 业务模块 │ ├── a.js │ └── b.js └── lib/ # 第三方库 ├── jquery.js └── underscore.js
|
3. 入口文件:data-main
在 HTML 中引入 require.js,并通过 data-main 属性指定入口文件:
1 2 3 4 5 6 7 8 9 10 11
| <!DOCTYPE html> <html> <head> <title>RequireJS 快速上手</title> <script data-main="js/main" src="js/require.js"></script> </head> <body> <span>页面内容不会被阻塞!</span> </body> </html>
|
注意:data-main 中的路径可以省略 .js 后缀,RequireJS 会自动补全。
4. 定义模块:define()
用 define() 函数定义一个模块,比如 js/app/a.js:
1 2 3 4 5 6 7 8
| define(function() { return { sayHello: function() { alert('Hello RequireJS!'); } }; });
|
如果模块有依赖,把依赖放在数组里:
1 2 3 4 5 6 7 8
| define(['jquery'], function($) { return { init: function() { $('body').css('background', '#f0f0f0'); } }; });
|
5. 加载模块:require()
在入口文件 js/main.js 中,用 require() 加载并使用模块:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| require.config({ baseUrl: 'js', paths: { 'jquery': 'lib/jquery', 'app-a': 'app/a' } });
require(['jquery', 'app-a'], function($, appA) { appA.sayHello(); $('body').append('<p>模块加载成功!</p>'); });
|
三、核心配置:require.config() 详解
require.config() 是 RequireJS 的核心,通过它可以灵活配置模块的加载规则。以下是最常用的配置选项:
1. baseUrl:模块根路径
baseUrl 是所有模块查找的根路径,默认是 data-main 指定的文件所在目录,或者 HTML 页面所在目录。
1 2 3
| require.config({ baseUrl: 'js' });
|
2. paths:模块路径映射
paths 用于给模块起别名,或者配置远程 CDN 地址,还支持多个路径 fallback(CDN 失败时自动加载本地文件):
1 2 3 4 5 6 7 8 9 10 11
| require.config({ paths: { 'underscore': 'lib/underscore', 'jquery': [ 'https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min', 'lib/jquery' ] } });
|
注意:paths 中的路径不要加 .js 后缀,RequireJS 会自动补全。
3. shim:兼容非 AMD 模块
很多老库(比如旧版 jQuery、Backbone)没有用 define() 定义模块,这时候就需要 shim 来“垫”成可用的 AMD 模块。
shim 有两个核心配置:
deps:模块的依赖数组;
exports:模块暴露的全局变量名。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| require.config({ shim: { 'underscore': { exports: '_' }, 'backbone': { deps: ['jquery', 'underscore'], exports: 'Backbone' }, 'jquery.form': ['jquery'] } });
|
重要提醒:shim 仅用于非 AMD 模块,已经用 define() 定义的模块不要用 shim,否则会出问题。
4. bundles:模块打包配置
bundles 用于配置“多个模块打包在一个文件中”的场景,告诉 RequireJS 哪些模块在同一个 bundle 里:
1 2 3 4 5 6
| require.config({ bundles: { 'primary': ['main', 'util', 'text!template.html'], 'secondary': ['text!secondary.html'] } });
|
当加载 util 时,RequireJS 会自动加载 primary 这个 bundle 文件。
5. map:模块版本映射
map 可以让不同的模块加载不同版本的依赖,解决版本冲突问题:
1 2 3 4 5 6 7 8
| require.config({ map: { '*': { 'foo': 'foo1.2' }, 'some/oldmodule': { 'foo': 'foo1.0' } } });
|
6. 其他常用配置
1 2 3 4 5 6
| require.config({ waitSeconds: 15, urlArgs: 'v=1.0', enforceDefine: true, nodeIdCompat: true });
|
四、高级用法:解决复杂场景问题
1. 循环依赖处理
如果出现“a 依赖 b,b 依赖 a”的循环依赖,RequireJS 会在 b 的回调中给 a 传 undefined,这时候需要用 require() 动态获取 a:
1 2 3 4 5 6 7
| define(['require', 'a'], function(require, a) { return function(title) { return require('a').doSomething(); }; });
|
建议:尽量避免循环依赖,这通常意味着模块设计有问题。
2. 加载插件:text、domReady、i18n
RequireJS 支持丰富的加载插件,这里介绍三个最常用的:
(1)text.js:加载文本文件(HTML/CSS 模板)
下载 text.js,放到项目中,用 text! 前缀加载文本:
1 2 3 4 5 6 7 8
| define(['text!templates/user.html'], function(userTemplate) { return { render: function() { $('body').append(userTemplate); } }; });
|
(2)domReady.js:等待 DOM 加载完成
下载 domReady.js,用 domReady! 前缀确保回调在 DOM 就绪后执行:
1 2 3 4
| require(['domReady!', 'jquery'], function(doc, $) { $('body').css('color', 'red'); });
|
(3)i18n.js:国际化支持
下载 i18n.js,创建如下目录结构:
1 2 3 4 5
| js/ └── nls/ ├── colors.js # 根语言包 └── fr-fr/ └── colors.js # 法语语言包
|
根语言包 js/nls/colors.js:
1 2 3 4 5 6 7
| define({ root: { red: 'red', blue: 'blue' }, 'fr-fr': true });
|
法语语言包 js/nls/fr-fr/colors.js:
1 2 3 4
| define({ red: 'rouge', blue: 'bleu' });
|
使用国际化模块:
1 2 3 4 5 6 7 8 9
| require.config({ config: { i18n: { locale: 'fr-fr' } } });
require(['i18n!nls/colors'], function(colors) { console.log(colors.red); });
|
3. 错误处理
RequireJS 提供了三种错误处理方式:
(1)全局错误监听
1 2 3 4 5
| requirejs.onError = function(err) { console.error('加载错误类型:', err.requireType); console.error('失败的模块:', err.requireModules); throw err; };
|
(2)require() 的错误回调
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| require(['jquery'], function($) { }, function(err) { var failedId = err.requireModules[0]; if (failedId === 'jquery') { requirejs.undef(failedId); requirejs.config({ paths: { jquery: 'lib/jquery' } }); require(['jquery'], function($) { }); } });
|
(3)paths 数组 fallback
前面已经介绍过,paths 可以配置多个路径,自动 fallback:
1 2 3 4 5 6 7 8
| require.config({ paths: { jquery: [ 'https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min', 'lib/jquery' ] } });
|
4. 多版本模块共存
通过 context 配置,可以在同一个页面加载多个版本的模块:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| var reqV1 = require.config({ context: 'version1', baseUrl: 'js/v1' }); reqV1(['jquery'], function($) { console.log('jQuery v1 版本:', $.fn.jquery); });
var reqV2 = require.config({ context: 'version2', baseUrl: 'js/v2' }); reqV2(['jquery'], function($) { console.log('jQuery v2 版本:', $.fn.jquery); });
|
5. 在 Node.js 中使用 RequireJS
RequireJS 也可以在 Node.js 中使用,实现前后端模块复用:
Node.js 中使用:
1 2 3 4 5 6 7 8 9 10
| var requirejs = require('requirejs');
requirejs.config({ nodeRequire: require, baseUrl: __dirname });
requirejs(['./app/a'], function(appA) { appA.sayHello(); });
|
五、生产环境优化:用 r.js 合并压缩
RequireJS 自带了一个强大的优化工具 r.js,可以将多个模块合并压缩成一个文件,减少 HTTP 请求,提升性能。
1. 安装 r.js
1 2 3 4
| npm install -g requirejs
npm install requirejs --save-dev
|
2. 编写构建配置文件
创建 build.js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| ({ baseUrl: 'js', paths: { jquery: 'lib/jquery' }, shim: { underscore: { exports: '_' } }, name: 'main', out: 'dist/main-built.js', optimize: 'uglify2', removeCombined: true, preserveLicenseComments: false })
|
3. 执行构建
构建完成后,会生成 dist/main-built.js,这是一个合并压缩后的文件。
4. 生产环境使用
在 HTML 中直接引入构建后的文件:
1 2 3 4 5 6 7
| <script src="js/require.js"></script> <script> require.config({ baseUrl: 'js' }); require(['dist/main-built']); </script>
|
六、避坑指南:90%的人都会踩的坑
1. data-main 的异步问题
data-main 指定的文件是异步加载的,不要在它之后立即写 require():
1 2 3 4 5
| <script data-main="js/main" src="js/require.js"></script> <script> require(['jquery'], function($) { }); </script>
|
正确写法:把所有 require() 都放在 main.js 里,或者用嵌套 require():
1 2 3 4 5 6 7
| <script src="js/require.js"></script> <script> require(['js/config'], function() { require(['jquery', 'app-a'], function($, appA) { }); }); </script>
|
2. 模块定义的规范
- 一个文件一个模块:不要在一个文件里写多个
define();
- 不要手动给模块命名:
define('moduleName', [], function() {}) 会降低模块可移植性,让优化工具自动命名;
- 相对路径的正确使用:在
define() 中用相对路径时,要确保 require 是依赖之一:1 2 3
| define(['require', './relative/module'], function(require) { var mod = require('./relative/module'); });
|
3. shim 的使用限制
shim 仅用于非 AMD 模块,已经用 define() 定义的模块不要用;
- 不要混合使用 CDN 和 shim:如果 jQuery 从 CDN 加载,shim 配置的 Backbone 可能会在 jQuery 加载前执行,导致错误。解决方案:构建时把 jQuery 内联到打包文件中。
4. 路径配置的注意事项
- 不要在
paths 中写 .js 后缀;
- 如果模块 ID 以
/ 开头、包含协议(http://)、以 .js 结尾,会直接作为 URL 处理,不经过 baseUrl 和 paths 配置。
七、写在最后
虽然现在 ES Modules(ESM)已经成为前端模块化的标准,Webpack、Vite 等构建工具也大行其道,但 RequireJS 依然有它的价值:
- 很多老项目、遗留系统还在使用 RequireJS,维护和重构都需要它;
- 它的异步加载、依赖管理思想,对理解模块化开发依然有很大帮助;
- 在一些不需要构建工具的简单场景,RequireJS 依然是轻量高效的选择。
技术迭代很快,但核心思想是相通的。希望这篇文章能帮你彻底搞懂 RequireJS,无论是维护老项目还是学习模块化思想,都能有所收获。