requirejs模块化工具

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 标签瀑布流 -->
<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>
<!-- ... 更多 script 标签 -->

这种方式带来了三个致命问题:

  1. JS 阻塞页面渲染:script 标签默认同步加载,会阻塞 DOM 渲染,导致页面白屏;
  2. 全局变量污染:所有模块的变量都挂在 window 上,容易命名冲突;
  3. 依赖关系混乱:必须手动保证 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>
<!-- data-main 指定入口文件,会在 require.js 加载后自动执行 -->
<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
// 定义一个依赖 jquery 的模块
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', // 给 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' // 所有模块都从 js/ 目录下查找
});

2. paths:模块路径映射

paths 用于给模块起别名,或者配置远程 CDN 地址,还支持多个路径 fallback(CDN 失败时自动加载本地文件):

1
2
3
4
5
6
7
8
9
10
11
require.config({
paths: {
// 给本地模块起别名
'underscore': 'lib/underscore',
// 配置 CDN + 本地 fallback
'jquery': [
'https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min',
'lib/jquery' // CDN 失败时加载本地文件
]
}
});

注意: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:没有依赖,暴露全局变量 _
'underscore': {
exports: '_'
},
// 旧版 backbone:依赖 jquery 和 underscore,暴露全局变量 Backbone
'backbone': {
deps: ['jquery', 'underscore'],
exports: 'Backbone'
},
// jQuery 插件:依赖 jquery,不需要 exports
'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: {
// 所有模块都用 foo1.2
'*': { 'foo': 'foo1.2' },
// 只有 some/oldmodule 用 foo1.0
'some/oldmodule': { 'foo': 'foo1.0' }
}
});

6. 其他常用配置

1
2
3
4
5
6
require.config({
waitSeconds: 15, // 加载超时时间(秒),默认7秒
urlArgs: 'v=1.0', // 给所有模块 URL 加参数,用于缓存刷新
enforceDefine: true, // 强制要求模块用 define() 定义,便于错误检测
nodeIdCompat: true // 兼容 Node.js 的模块 ID(忽略 .js 后缀)
});

四、高级用法:解决复杂场景问题

1. 循环依赖处理

如果出现“a 依赖 b,b 依赖 a”的循环依赖,RequireJS 会在 b 的回调中给 a 传 undefined,这时候需要用 require() 动态获取 a:

1
2
3
4
5
6
7
// b.js
define(['require', 'a'], function(require, a) {
return function(title) {
// 动态获取 a(此时 a 已经加载完成)
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) {
// userTemplate 就是 templates/user.html 的内容
return {
render: function() {
$('body').append(userTemplate);
}
};
});

(2)domReady.js:等待 DOM 加载完成

下载 domReady.js,用 domReady! 前缀确保回调在 DOM 就绪后执行:

1
2
3
4
require(['domReady!', 'jquery'], function(doc, $) {
// 这里的代码会在 DOM 加载完成后执行
$('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); // 输出 'rouge'
});

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) {
// 失败回调:CDN 加载失败时,切换到本地
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
// 加载版本1
var reqV1 = require.config({
context: 'version1',
baseUrl: 'js/v1'
});
reqV1(['jquery'], function($) {
console.log('jQuery v1 版本:', $.fn.jquery);
});

// 加载版本2
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 中使用,实现前后端模块复用:

1
2
# 安装 requirejs
npm install requirejs

Node.js 中使用:

1
2
3
4
5
6
7
8
9
10
var requirejs = require('requirejs');

requirejs.config({
nodeRequire: require, // 传入 Node.js 的 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', // 压缩方式:uglify2(默认)、none(不压缩)
removeCombined: true, // 移除已合并的文件
preserveLicenseComments: false // 移除 license 注释
})

3. 执行构建

1
r.js -o build.js

构建完成后,会生成 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
<!-- 错误写法:main.js 可能还没加载完,config 还没执行 -->
<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() {
// config 加载完成后再加载业务模块
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 处理,不经过 baseUrlpaths 配置。

七、写在最后

虽然现在 ES Modules(ESM)已经成为前端模块化的标准,Webpack、Vite 等构建工具也大行其道,但 RequireJS 依然有它的价值:

  • 很多老项目、遗留系统还在使用 RequireJS,维护和重构都需要它;
  • 它的异步加载、依赖管理思想,对理解模块化开发依然有很大帮助;
  • 在一些不需要构建工具的简单场景,RequireJS 依然是轻量高效的选择。

技术迭代很快,但核心思想是相通的。希望这篇文章能帮你彻底搞懂 RequireJS,无论是维护老项目还是学习模块化思想,都能有所收获。


requirejs模块化工具
https://cszy.top/2015-12-30 requirejs模块化工具/
作者
csorz
发布于
2015年12月30日
许可协议