RequireJS 教程:模块化开发的基石
目录
- 为什么需要 RequireJS? - 解决什么问题
- 核心概念 - 理解模块、依赖和异步加载
- 第一个 RequireJS 项目 - 从零开始搭建
data-main入口 - 项目的起点define函数 - 定义模块require函数 - 加载模块- 模块的写法 - 不同类型的模块定义
- 配置
require.config- 自定义路径和别名 - 实战演练:构建一个简单的应用 - 一个模块化的计算器
- RequireJS 与其他库(如 jQuery)的配合
- 总结与最佳实践
为什么需要 RequireJS?
在 RequireJS 出现之前,我们通常通过在 HTML 中通过 <script> 标签引入多个 JS 文件,这种方式会带来几个严重问题:

- 依赖混乱:
a.js依赖b.js和c.js,你必须确保b.js和c.js在a.js之前被加载,文件一多,依赖关系就会变得非常复杂,难以维护。 - 全局命名空间污染:所有变量和函数都暴露在全局作用域(
window对象)下,很容易造成命名冲突。 - 请求过多:每个
<script>标签都会发起一次 HTTP 请求,当项目变大,文件数量增多时,会严重影响页面加载性能。 - 代码组织差:所有代码都混在一个或几个大文件里,缺乏清晰的边界和结构。
RequireJS 的出现就是为了解决这些问题,它是一种 JavaScript 模块加载器,其核心思想是:
让 JavaScript 代码模块化,并按需异步加载。
核心概念
在深入代码之前,必须理解 RequireJS 的几个核心概念:
- 模块:一个模块就是一个独立的 JavaScript 文件,它封装了特定的功能,拥有自己的作用域,不会污染全局命名空间,模块可以导出(
return)一些变量、函数或对象,供其他模块使用。 - 依赖:一个模块可能需要使用其他模块的功能,这些被它所依赖的模块,就是它的依赖项,一个
dialog.js模块可能依赖于jquery.js和template.js。 - 异步加载:RequireJS 采用异步方式加载所有依赖模块,这意味着浏览器不会因为某个 JS 文件未加载完成而阻塞页面渲染,从而提升用户体验。
第一个 RequireJS 项目
让我们从一个最简单的项目开始。

项目结构
my-project/
├── index.html
├── js/
│ ├── main.js
│ └── lib/
│ └── require.js
└── modules/
└── sayHello.js
步骤 1: 准备 RequireJS 文件
你需要从 RequireJS 官网 下载 require.js 文件,并将其放入 js/lib/ 目录。
步骤 2: 创建 HTML 文件 (index.html)
在 index.html 中,我们通过一个特殊的 data-main 属性来引入 RequireJS。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">RequireJS Demo</title>
<!-- 1. 引入 require.js 文件 -->
<!-- data-main 属性指定了项目的入口模块 -->
<script data-main="js/main" src="js/lib/require.js"></script>
</head>
<body>
<h1>RequireJS Demo</h1>
<p>打开浏览器的开发者工具,查看控制台输出。</p>
</body>
</html>
注意:
src="js/lib/require.js":告诉浏览器去加载 RequireJS 库。data-main="js/main":这是关键,RequireJS 在加载完自身后,会自动去加载data-main指定的文件,这里我们指定了js/main.js,RequireJS 会自动为其加上.js后缀。
步骤 3: 创建入口模块 (js/main.js)
main.js 是我们应用的入口,它负责加载其他模块并启动应用。

// js/main.js
require(['modules/sayHello'], function(sayHello) {
// sayHello 是从 modules/sayHello.js 模块加载进来的
// 它是一个函数,我们在这里调用它
sayHello('World');
});
代码解释:
require(['modules/sayHello'], function(sayHello) { ... }):这是require函数的基本用法。- 第一个参数
['modules/sayHello']是一个数组,列出了当前模块所依赖的所有模块。 - 第二个参数
function(sayHello) { ... }是一个回调函数。只有当require数组中列出的所有模块都加载成功后,这个回调函数才会被执行。 - 回调函数的参数
sayHello,与依赖数组中的'modules/sayHello'一一对应,它代表了modules/sayHello.js模块所导出的内容。
- 第一个参数
步骤 4: 创建业务模块 (modules/sayHello.js)
这个模块定义了一个简单的函数。
// modules/sayHello.js
// 使用 define 函数来定义一个模块
define(function() {
// define 的回调函数会返回这个模块要导出的内容
return function(name) {
console.log('Hello, ' + name + '!');
};
});
代码解释:
define(function() { ... }):这是define函数的基本用法,用于定义一个模块。define的回调函数会返回一个值,这个值就是该模块导出的内容,我们导出了一个匿名函数。
打开 index.html,你会在浏览器控制台看到 "Hello, World!",恭喜你,你已经成功运行了第一个 RequireJS 项目!
data-main 入口
data-main 是 RequireJS 的启动入口,当 RequireJS 加载后,它会立即请求 data-main 指定的文件,这个文件通常被称为“主模块”或“入口模块”,它的职责是:
- 加载应用所需的核心模块。
- 初始化应用。
define 函数
define 是用来定义模块的核心函数,它有几种常见的用法:
基本用法:无依赖
// 定义一个没有依赖的模块
define(function() {
var privateVar = "I'm private";
return {
publicMethod: function() {
console.log(privateVar);
}
};
});
常用用法:有依赖
这是最常见的方式,明确声明模块的依赖。
// 定义一个有依赖的模块
// 依赖 ['moduleA', 'moduleB']
define(['moduleA', 'moduleB'], function(moduleA, moduleB) {
// moduleA 和 moduleB 是依赖模块的导出内容
var myFunction = function() {
moduleA.doSomething();
moduleB.doSomethingElse();
};
return {
start: myFunction
};
});
插件用法:如 text 和 i18n
RequireJS 还支持加载非 JavaScript 文件,如模板(HTML)或国际化字符串,这需要借助插件。
// 使用 text! 插件加载 HTML 模板
define(['text!templates/myTemplate.html'], function(template) {
// template 变量里就是 myTemplate.html 文件的内容字符串
console.log(template);
// ... 使用模板字符串创建 DOM 元素等
});
require 函数
require 函数主要用于在非模块化文件(如 main.js)中加载模块并执行代码。
- 在模块内部:推荐使用
define,因为它可以清晰地声明模块的依赖关系,有利于代码的静态分析和优化。 - 在入口文件 (
main.js):使用require是标准做法,因为它通常不需要导出任何东西,只是为了启动应用。
require 的语法与 define 的依赖数组部分完全相同。
// require(依赖数组, 回调函数)
require(['moduleA', 'moduleB'], function(moduleA, moduleB) {
// 执行初始化逻辑
moduleA.init();
moduleB.init();
});
模块的写法
模块的写法非常灵活,主要取决于你如何组织代码。
简单对象模块
// myModule.js
define({
name: 'MyModule',
version: '1.0.0',
sayName: function() {
console.log(this.name);
}
});
函数模块
// myFunction.js
define(function() {
return function(greeting) {
console.log(greeting);
};
});
构造函数模块
// Person.js
define(function() {
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log('Hello, my name is ' + this.name);
};
return Person;
});
// main.js
require(['Person'], function(Person) {
var alice = new Person('Alice');
alice.sayHello(); // 输出: Hello, my name is Alice
});
配置 require.config
当项目变大时,模块路径会变得很长,而且可能会使用第三方库,这时就需要使用 require.config 来进行全局配置。
require.config 应该在所有 require 和 define 调用之前执行,通常我们把它放在 main.js 的最顶部。
基本配置:paths
paths 用于配置模块的路径,可以缩短模块 ID,并方便管理第三方库。
// js/main.js
require.config({
// 配置模块路径
paths: {
// 'jquery' 指向 'js/lib/jquery-3.6.0.min.js'
'jquery': 'lib/jquery-3.6.0.min',
// 'underscore' 指向 'js/lib/underscore-min.js'
'underscore': 'lib/underscore-min'
}
});
// 现在可以使用 'jquery' 和 'underscore' 作为模块 ID 了
require(['jquery', 'underscore'], function($, _) {
console.log('jQuery version:', $.fn.jquery);
console.log('Underscore version:', _.VERSION);
});
高级配置:shim
有些传统的 JS 库(如 underscore)没有采用 AMD(异步模块定义)规范,它们没有使用 define 来定义模块。shim 配置可以让 RequireJS 正确处理这些非 AMD 模块。
shim 主要做两件事:
- 导出:告诉 RequireJS 如何从非 AMD 模块中获取它的导出内容(通常是
window上的全局变量)。 - 依赖:声明该非 AMD 模块自身还依赖哪些其他模块。
示例:配置一个名为 myApp 的非 AMD 模块
假设我们有一个 app.js 文件,它依赖于 jquery,并且它会把一些方法挂载到 window.myApp 上。
// js/app.js (非 AMD 模块)
(function($) {
window.myApp = {
doSomething: function() {
console.log('Doing something with jQuery:', $);
}
};
})(jQuery);
在 main.js 中配置 shim:
// js/main.js
require.config({
paths: {
'jquery': 'lib/jquery-3.6.0.min',
'app': 'app' // 指向 js/app.js
},
shim: {
'app': {
// 'app' 模块的导出内容是 window.myApp
exports: 'myApp',
// 'app' 模块依赖于 'jquery'
deps: ['jquery']
}
}
});
// 使用 'app' 模块
require(['app'], function(myApp) {
myApp.doSomething(); // 可以正常工作
});
实战演练:构建一个简单的模块化计算器
让我们用 RequireJS 来构建一个简单的计算器应用。
项目结构
calculator/
├── index.html
├── js/
│ ├── main.js
│ └── lib/
│ └── require.js
└── modules/
├── calculator.js
├── display.js
└── buttons.js
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">RequireJS Calculator</title>
<script data-main="js/main" src="js/lib/require.js"></script>
<style>
#calculator { width: 200px; margin: 50px auto; border: 1px solid #ccc; padding: 10px; }
#display { width: 100%; height: 30px; margin-bottom: 10px; text-align: right; }
.button { width: 40px; height: 30px; margin: 2px; }
</style>
</head>
<body>
<div id="calculator">
<input type="text" id="display" readonly>
<div id="buttons-container"></div>
</div>
</body>
</html>
modules/calculator.js (核心逻辑)
// modules/calculator.js
define(['display', 'buttons'], function(Display, Buttons) {
function Calculator() {
this.display = new Display();
this.buttons = new Buttons();
this.buttons.setupClickHandlers(this.display);
}
return Calculator;
});
modules/display.js (显示逻辑)
// modules/display.js
define(function() {
function Display() {
this.element = document.getElementById('display');
}
Display.prototype.setValue = function(value) {
this.element.value = value;
};
Display.prototype.appendValue = function(value) {
this.element.value += value;
};
Display.prototype.clear = function() {
this.element.value = '';
};
return Display;
});
modules/buttons.js (按钮逻辑)
// modules/buttons.js
define(function() {
function Buttons() {
this.element = document.getElementById('buttons-container');
this.createButtons();
}
Buttons.prototype.createButtons = function() {
const buttonLabels = ['7', '8', '9', '+', '4', '5', '6', '-', '1', '2', '3', '*', 'C', '0', '=', '/'];
buttonLabels.forEach(label => {
const button = document.createElement('input');
button.type = 'button';
button.value = label;
button.className = 'button';
this.element.appendChild(button);
});
};
Buttons.prototype.setupClickHandlers = function(display) {
this.element.addEventListener('click', function(event) {
if (event.target.type === 'button') {
const value = event.target.value;
if (value === 'C') {
display.clear();
} else if (value === '=') {
// 这里简化处理,实际应用中应有表达式解析逻辑
try {
display.setValue(eval(display.element.value));
} catch (e) {
display.setValue('Error');
}
} else {
display.appendValue(value);
}
}
});
};
return Buttons;
});
js/main.js (入口)
// js/main.js
require.config({});
require(['modules/calculator'], function(Calculator) {
// 当所有依赖都加载完成后,启动计算器
new Calculator();
});
现在打开 index.html,你将看到一个功能完整的计算器,并且它的代码被清晰地分成了三个独立的模块。
RequireJS 与其他库(如 jQuery)的配合
jQuery 本身是支持 AMD 的,所以使用起来非常简单。
安装 jQuery
将 jQuery 文件(如 jquery-3.6.0.min.js)放入 js/lib/ 目录。
配置和使用
在 main.js 中配置路径,然后在其他模块中直接使用 'jquery' 作为依赖。
// main.js
require.config({
paths: {
'jquery': 'lib/jquery-3.6.0.min'
}
});
// some-module.js
define(['jquery'], function($) {
function init() {
$('#my-button').on('click', function() {
alert('Button clicked!');
});
}
return {
init: init
};
});
// main.js 中加载并初始化
require(['some-module'], function(someModule) {
someModule.init();
});
总结与最佳实践
- 使用
data-main:始终用它来指定应用的入口文件。 - 优先使用
define:在所有模块文件中都使用define来定义模块,并清晰地声明其依赖。 - 配置先行:在
main.js的开头使用require.config来管理所有路径和第三方库的shim配置。 - 模块职责单一:每个模块只做一件事,保持其功能内聚。
- 避免全局变量:确保你的模块只通过
return的方式导出内容,不要在模块内部污染全局作用域。 - 考虑构建工具:对于大型项目,通常会将 RequireJS 与构建工具(如 r.js)结合使用,r.js 可以将多个模块文件合并成一个或少数几个文件,并压缩代码,以优化生产环境的性能。
ES6 Modules 与模块打包器
虽然 RequireJS 是模块化发展史上的一个重要里程碑,但现在前端开发的主流已经转向了 ES6 Modules (ESM) 和 模块打包器(如 Webpack, Vite, Parcel)。
- ES6 Modules:提供了原生的、语言层面的模块化支持(
import/export),是未来的标准。 - 模块打包器:它们将你的所有模块(包括 CSS、图片等)打包成少量文件,利用浏览器缓存,并提供热更新、代码分割等高级功能。
RequireJS 还能用吗? 可以,但主要用于维护旧项目,对于新项目,更推荐使用现代工具链,RequireJS 所倡导的 模块化、按需加载 的思想,至今仍然是前端工程化的核心原则,理解 RequireJS 有助于你更好地理解现代打包工具的工作原理。
