杰瑞科技汇

RequireJS教程,如何快速上手模块化开发?

RequireJS 教程:模块化开发的基石

目录

  1. 为什么需要 RequireJS? - 解决什么问题
  2. 核心概念 - 理解模块、依赖和异步加载
  3. 第一个 RequireJS 项目 - 从零开始搭建
  4. data-main 入口 - 项目的起点
  5. define 函数 - 定义模块
  6. require 函数 - 加载模块
  7. 模块的写法 - 不同类型的模块定义
  8. 配置 require.config - 自定义路径和别名
  9. 实战演练:构建一个简单的应用 - 一个模块化的计算器
  10. RequireJS 与其他库(如 jQuery)的配合
  11. 总结与最佳实践

为什么需要 RequireJS?

在 RequireJS 出现之前,我们通常通过在 HTML 中通过 <script> 标签引入多个 JS 文件,这种方式会带来几个严重问题:

RequireJS教程,如何快速上手模块化开发?-图1
(图片来源网络,侵删)
  • 依赖混乱a.js 依赖 b.jsc.js,你必须确保 b.jsc.jsa.js 之前被加载,文件一多,依赖关系就会变得非常复杂,难以维护。
  • 全局命名空间污染:所有变量和函数都暴露在全局作用域(window 对象)下,很容易造成命名冲突。
  • 请求过多:每个 <script> 标签都会发起一次 HTTP 请求,当项目变大,文件数量增多时,会严重影响页面加载性能。
  • 代码组织差:所有代码都混在一个或几个大文件里,缺乏清晰的边界和结构。

RequireJS 的出现就是为了解决这些问题,它是一种 JavaScript 模块加载器,其核心思想是:

让 JavaScript 代码模块化,并按需异步加载。

核心概念

在深入代码之前,必须理解 RequireJS 的几个核心概念:

  • 模块:一个模块就是一个独立的 JavaScript 文件,它封装了特定的功能,拥有自己的作用域,不会污染全局命名空间,模块可以导出(return)一些变量、函数或对象,供其他模块使用。
  • 依赖:一个模块可能需要使用其他模块的功能,这些被它所依赖的模块,就是它的依赖项,一个 dialog.js 模块可能依赖于 jquery.jstemplate.js
  • 异步加载:RequireJS 采用异步方式加载所有依赖模块,这意味着浏览器不会因为某个 JS 文件未加载完成而阻塞页面渲染,从而提升用户体验。

第一个 RequireJS 项目

让我们从一个最简单的项目开始。

RequireJS教程,如何快速上手模块化开发?-图2
(图片来源网络,侵删)

项目结构

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 是我们应用的入口,它负责加载其他模块并启动应用。

RequireJS教程,如何快速上手模块化开发?-图3
(图片来源网络,侵删)
// 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 指定的文件,这个文件通常被称为“主模块”或“入口模块”,它的职责是:

  1. 加载应用所需的核心模块。
  2. 初始化应用。

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
    };
});

插件用法:如 texti18n

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 应该在所有 requiredefine 调用之前执行,通常我们把它放在 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 主要做两件事:

  1. 导出:告诉 RequireJS 如何从非 AMD 模块中获取它的导出内容(通常是 window 上的全局变量)。
  2. 依赖:声明该非 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 有助于你更好地理解现代打包工具的工作原理。

分享:
扫描分享到社交APP
上一篇
下一篇