巧用 RequireJS Optimizer 给传统的前端项目打包

r.js 本是 RequireJS 的一个附属产品,支持在 NodeJS、Rhino 等环境下运行 AMD 程序,并且其包含了一个名为 RequireJS Optimizer 的工具,可以为项目完成合并脚本等优化操作。

r.js 的介绍中明确写道它是 RequireJS 项目的一部分,和 RequireJS 协同工作。但我发现,RequireJS Optimizer 提供了丰富的配置参数,可以让我们完全跳出 AMD 和 RequireJS 程序的束缚,为我们的前端程序服务。


RequireJS Optimizer 常规用法

首先,简单介绍一下 RequireJS Optimizer 的“正派”用法 (以 NodeJS 环境为例):

事先写好一个配置文件,比如 config.js,它是 JSON 格式的,常用属性有:

{
    // 程序的根路径
    appDir: "some/path/trunk",
    // 脚本的根路径
    // 相对于程序的根路径
    baseUrl: "./js",
    // 打包输出到的路径
    dir: "../some/path/release",
    // 需要打包合并的js模块,数组形式,可以有多个
    // name 以 baseUrl 为相对路径,无需写 .js 后缀
    // 比如 main 依赖 a 和 b,a 又依赖 c,则 {name: 'main'} 会把 c.js a.js b.js main.js 合并成一个 main.js
    modules: [
        {name: 'main'}
        ...
    ]
    // 通过正则以文件名排除文件/文件夹
    // 比如当前的正则表示排除 .svn、.git 这类的隐藏文件
    fileExclusionRegExp: /^\./
}

然后运行:

node r.js -o config.js

这时 RequireJS Optimizer 就会:

  1. 把配置信息的 modules 下的所有模块建立好完整的依赖关系,再把相应的文件打包合并到 dir 目录
  2. 把所有的 css 文件中,使用 @import 语法的文件自动打包合并到 dir 目录
  3. 把其它文件复制到 dir 目录,比如图片、附件等等

我已经把 RequireJS 和 r.js 整套东西用到了 H5Slides 上。觉得蛮方便的。

不过工作中的前端开发工作并不是绝对理想化的,有些旧的项目,并不是 AMD 的模块化开发方式,而是传统的 js 程序,开发一个页面时可能需要一口气引入三到五个 css 文件、十来个 js 文件…… 上线的时候为了减少流量及 HTTP 请求数又需要把代码尽可能重用和合并。这个时候就需要一个方便快捷的打包工具帮助我们了,下面就介绍一下 RequireJS Optimizer 是如何完成这项工作的。

用到的几个关键参数

说到这里,必须要额外介绍几个 RequireJS Optimizer 的参数了:

modules[i].include

modules: [
    {
        name: "main",
        include: ["d", "e"]
    }
]

这里的 include 字段提供了“强制建立依赖关系”的功能,也就是说,即使在 main.js 的代码里没有依赖 d.js 和 e.js,它们也会在合并代码的时候插入到 main.js 的前面

skipModuleInsertion

在介绍这个参数之前需要说明的是,RequireJS Optimizer 有一个很智能的功能,就是为没有写明 define(...) 函数的模块代码自动将其放入 define(...) 之中。如果我们写明:

skipModuleInsertion: true

则这种处理将会被取消。

onBuildRead

这个参数可以定义一个函数,在处理每个 js 文件之前,会先对文件的文本内容进行预处理。比如下面这个例子里,我会把 main.js 里的代码全部清除:

onBuildRead: function (moduleName, path, contents) {
    if (moduleName === 'main') {
        contents = '/* empty code */';
    }
    return contents;
}

巧妙应用到传统项目

这时,我们的资源已经足够了。比如我现在的项目有:

1 个 html

  • index.html

代码:

<!doctype html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Index</title>
        <link rel="stylesheet" href="css/a.css">
        <link rel="stylesheet" href="css/b.css">
    </head>
    <body>

        ...

        <script src="js/a.js"></script>
        <script src="js/b.js"></script>
        <script src="js/c.js"></script>
    </body>
</html>

2 个 css

  • css/a.css
  • css/b.css

3 个 js

  • js/a.js
  • js/b.js
  • js/c.js

1 个图片文件夹

  • images

合并 css 文件

新建一个 css 文件,叫 css/main.css,内容为:

@import url(a.css);
@import url(b.css);

然后把 index.html 中的 2 个 <link> 标签改成一个

<link rel="stylesheet" href="css/main.css">

合并 js 文件

合并 js 文件的步骤略复杂些。首先也是新建一个 js 文件,叫 js/main.js:

document.write('<script src="js/a.js"></script>');
document.write('<script src="js/b.js"></script>');
document.write('<script src="js/c.js"></script>');

然后把 index.html 中的 3 个 <script> 标签改成一个

<script src="js/main.js"></script>

接下来就是配置打包工具的时间了。

禁止自动补齐 define(...) 的头尾

skipModuleInsertion: true

强制建立依赖

modules: [{name: 'main', include: ['a', 'b', 'c']}]

这样打包出来的 main.js 是这样的:

// code from a.js
// code from b.js
// code from c.js
document.write('<script src="js/a.js"></script>');
document.write('<script src="js/b.js"></script>');
document.write('<script src="js/c.js"></script>');

打包时去掉多余的 js/main.js 的代码

onBuildRead: function (moduleName, path, contents) {
    if (moduleName === 'main') {
        contents = '/* empty code */';
    }
    return contents;
}

这样的话,打包工具就会把 document.write(...) 的代码去掉,得到干净的

// code from a.js
// code from b.js
// code from c.js
/* empty code */

运行 node r.js -o config.js 就可以得到一个打包成功的项目了,并且打包前后的代码都可以正常运行

附件是这个项目例子的源代码:project.zip

9 条早期评论

  • 姓名
    新人小丁
    评论日期
    2013/04/01 11:18:01
    博主您好,我是在 http://ucren.com/blog/ 这里发现您的主题,非常喜欢,非常冒昧的问一句,我能讨得这个"字很大"的主题用用么?这个是我的博客地址http://phperzj.sinaapp.com/ 我一直在尝试将我的博客弄的字大点,简洁好看一点,如果您同意给我尝试用用 我保证1.不会涉及任何商业利益,并且在主页标明您的设计权 2.如果我有心想做任何相关修改并想发布看效果,我会提前征得您的同意。 在此,谢过,期待回复,敬礼!
  • 姓名
    囧克斯
    评论日期
    2013/04/06 04:43:35
    @新人小丁
    没问题!拿去用吧:)
  • 姓名
    TooBug
    评论日期
    2013/04/25 09:26:26
    收藏了好久,今天终于拜读了!其实就是用RequireJS Optimizer来完全前端的自动合并对么?

    之前也在找Grunt.js的一些方案,发现用use-min也可以实现,或者自己用concat + text replace也可以实现,总之这个问题的解决方案还是挺多的。
  • 姓名
    囧克斯
    评论日期
    2013/04/27 10:08:30
    没错,回过头来看,方案是很多的,现在这个方案算是从require.js走过来的,呵呵:)
  • 姓名
    囧克斯
    评论日期
    2014/07/03 04:52:33
    现在即使换了Grunt.js,我还是同样是用document.write的方式hack打包过程的
  • 姓名
    hadon
    评论日期
    2014/06/26 11:55:20
    楼主你好,问一个问题,在使用requirejs压缩合并的时候,遇到了一个问题,比如我在应用中引用jquery.js,但jquery.js只想压缩不想合并,因为jquery.js几乎每个页面都会引用,就没有必要合并压缩了,这样才能利用浏览器的缓存。
    所以我要怎么压缩合并才能达到这个功能呢?
  • 姓名
    囧克斯
    评论日期
    2014/07/03 04:51:01
    @hadon
    如果是这样的话,建议把jquery.js从依赖关系中单独摘出来,然后单独引用、压缩、部署。独立成一个模块或干脆拿到requirejs的体系外面都是可以的。
  • 姓名
    PA
    评论日期
    2015/09/17 08:26:37
    {
    excludeShallow: ['jquery'],
    }
    想问下谁知道在rhino环境下怎么运行?
  • 姓名
    mc-zone
    评论日期
    2015/01/18 11:43:35
    拜读文章,有问题想请教下:
    以您的文件为例,我在main.js中又使用了不少其他的项目,可能会有不同文件层次,于是都配置了path别名方便调取。但是在build的时候发现path还需要在config.js中再指定一遍。这样更改起来需要两边同步比较麻烦。有没有比较好的方法避免这一问题?