JavaScript模块化-require.js,r.js和打包发布

JavaScript模块化和闭包JavaScript-Module-Pattern-In-Depth这两篇文章中,提到了模块化的基本思想,但是在实际项目中模块化和项目人员的分工,组建化开发,打包发布,性能优化,工程化管理都有密切的关系,这么重要的事情,在JavaScript大行其道的今天,不可能没有成熟的解决方案,所以从我的实践经验出发,从模块化讲到工程化,分享一下自己的经验。

这篇文章主要是讲require.js和r.js在项目中的使用,不会涉及到工程化问题,对此熟悉的看官可以略过此文。对于require.js基本用法不熟悉的朋友,可以看看这个blog:asynchronous_module_definition

JavaScript的模块化

流行的模块化解决方案现在有很多,主要分为以下几种规范

  • AMD:今天讨论的主题,AMD 规范是JavaScript开发的一次重要尝试,它以简单而优雅的方式统一了JavaScript的模块定义和加载机制,并迅速得到很多框架的认可和采纳。这对开发人员来说是一个好消息,通过AMD我们降低了学习和使用各种框架的门槛,能够以一种统一的方式去定义和使用模块,提高开发效率,降低了应用维护成本。
  • CommonJS:node.js的方式,在前端需要打包工具配合使用。在后端比较好用。
  • CMD & sea.js: 国内牛人搞的。LABjs、RequireJS、SeaJS 哪个最好用?为什么?

JavaScript的模块化需要解决下面几个问题

  • 定义模块
  • 管理模块依赖
  • 加载模块
  • 加载优化
  • 代码调试支持

为了直观的理解一下流行了很久的require.js和r.js是如何解决这些问题的,我们从一个例子入手吧。下载example-multipage-shim

代码结构

我们看一下基于requirejs的多页面项目的一个基本结构:


example-multipage-shim文件结构

下面我们看看如何解决js模块化的问题的。

定义模块

看一下base.js

define(function () {
    function controllerBase(id) {
        this.id = id;
    }

    controllerBase.prototype = {
        setModel: function (model) {
            this.model = model;
        },

        render: function (bodyDom) {
            bodyDom.prepend('<h1>Controller ' + this.id + ' says "' +
                      this.model.getTitle() + '"</h2>');
        }
    };

    return controllerBase;
});

使用define就可以定义了。不需要我们自己手动导出全局变量啦。

管理模块依赖

看一下c1.js

define(['./Base'], function (Base) {
    var c1 = new Base('Controller 1');
    return c1;
});

可以看到通过['./Base']注入依赖。
在看一下main1.js

define(function (require) {
    var $ = require('jquery'),
        lib = require('./lib'),
        controller = require('./controller/c1'),
        model = require('./model/m1'),
        backbone = require('backbone'),
        underscore = require('underscore');

    //A fabricated API to show interaction of
    //common and specific pieces.
    controller.setModel(model);
    $(function () {
        controller.render(lib.getBody());

        //Display backbone and underscore versions
        $('body')
            .append('<div>backbone version: ' + backbone.VERSION + '</div>')
            .append('<div>underscore version: ' + underscore.VERSION + '</div>');
    });
});

也可以通过require的方式(CommonJS风格)去加载依赖模块

加载模块

看一下如何启动,看看page1.html

<!DOCTYPE html>
<html>
    <head>
        <title>Page 1</title>
        <script src="js/lib/require.js"></script>
        <script>
            //Load common code that includes config, then load the app
            //logic for this page. Do the requirejs calls here instead of
            //a separate file so after a build there are only 2 HTTP
            //requests instead of three.
            requirejs(['./js/common'], function (common) {
                //js/common sets the baseUrl to be js/ so
                //can just ask for 'app/main1' here instead
                //of 'js/app/main1'
                requirejs(['app/main1']);
            });
        </script>
    </head>
    <body>
        <a href="page2.html">Go to Page 2</a>
    </body>
</html>

我们看到首先用script标签引入require.js,然后使用requirejs加载模块,而这些模块本来也要用script标签引用的,所以说requirejs帮助我们管理文件加载的事情了。可以使用data-main属性去加载,详细说明可以看文档了。
我们看一下运行效果。

运行效果

可以看到requirejs帮助我们家在了所有模块,我们可以更好的组织JavaScript代码了。

优化加载

我们模块化代码以后,并不想增加请求的次数,这样会使网页的性能降低(这里是异步加载,但是浏览器异步请求的过多,还是有问题的),所以我们想合并一下代码。
使用r.js:

node r.js -o build.js
使用r.js

看看结果:


构建后

构建后我们的代码都经过处理了。

看看运行效果。

减少了请求

可见可以通过r.js帮助我们优化请求(通过合并文件)。

如何配置

  • requirejs如何配置,我们看看common.js
requirejs.config({
    baseUrl: 'js/lib', 从这个位置加载模块
    paths: {
        app: '../app' 
    },
    shim: {
        backbone: {
            deps: ['jquery', 'underscore'],
            exports: 'Backbone'
        },
        underscore: {
            exports: '_'
        }
    }
});
属性 意义
baseUrl 加载模块的位置
app:'../app' 像这样的'app/sub',在app目录下找sub模块
shim 全局导出的库,在这里包装

可以查看中文说明书看看更详细的说明。


  • r.js如何配置,我们看看build.js
    这里面有很全的配置说明example.build.js,过一下我们自己是怎么配置的。
{
    appDir: '../www',
    mainConfigFile: '../www/js/common.js',
    dir: '../www-built',
    modules: [
        //First set up the common build layer.
        {
            //module names are relative to baseUrl
            name: '../common',
            //List common dependencies here. Only need to list
            //top level dependencies, "include" will find
            //nested dependencies.
            include: ['jquery',
                      'app/lib',
                      'app/controller/Base',
                      'app/model/Base'
            ]
        },

        //Now set up a build layer for each main layer, but exclude
        //the common one. "exclude" will exclude nested
        //the nested, built dependencies from "common". Any
        //"exclude" that includes built modules should be
        //listed before the build layer that wants to exclude it.
        //The "page1" and "page2" modules are **not** the targets of
        //the optimization, because shim config is in play, and
        //shimmed dependencies need to maintain their load order.
        //In this example, common.js will hold jquery, so backbone
        //needs to be delayed from loading until common.js finishes.
        //That loading sequence is controlled in page1.html.
        {
            //module names are relative to baseUrl/paths config
            name: 'app/main1',
            exclude: ['../common']
        },

        {
            //module names are relative to baseUrl
            name: 'app/main2',
            exclude: ['../common']
        }

    ]
}

我们主要看modules下面定义的数组,实际上就是一个个文件的依赖关系,r.js会一用这里的关系,合并文件。详细的配置意义可以看文档

提示:r.js还可以优化css。

如何调试

前面代码被优化了以后,调试起来就痛苦了,这里我们可以使用sourcemap技术来调试优化后的代码。进行如下操作。

  1. 修改build.js,增加如下配置
   generateSourceMaps: true,
   preserveLicenseComments: false,
   optimize: "uglify2",
  1. 重新构建
node r.js -o build.js
  1. 打开浏览器支持
    这里最好用firefox浏览器,chrome从本地文件打开html不能正常使用sourcemap。直接用firefox浏览就可以了。


    firefox支持sourcemap

    可以看到可以加载非优化的代码,有人会问,这不要请求多次吗?优化一份,非优化一份,这样不是性能更差劲。其实只有你调试的时候,开启了这个功能才会请求对应的sourcemap文件,所以对用户来说并不浪费。

  2. 写一个server让chrome也支持
    chrome本身是支持source map的,就是从硬盘直接打开文件的权限有特殊处理。以file://开头的路径很多事情做不了。所以我们做一个简单的server吧。

在tools目录下增加一个server.js文件

var http = require('http'),
    url = require('url'),
    path = require('path'),
    fs = require('fs'),
    port = process.argv[2] || 8888,
    types = {
        'html': 'text/html',
        'js': 'application/javascript'
    };

http.createServer(function (request, response) {
    var uri = url.parse(request.url).pathname,
        filename = path.join(__dirname, '..', uri);
        console.log(filename);

    fs.exists(filename, function (exists) {
        if (!exists) {
            response.writeHead(404, {'Content-Type': 'text/plain'});
            response.write('404 Not Found\n');
            response.end();
            return;
        }

        var type = filename.split('.');
        type = type[type.length - 1];

        response.writeHead(200, { 'Content-Type': types[type] + '; charset=utf-8' });
        fs.createReadStream(filename).pipe(response);
    });
}).listen(parseInt(port, 10));

console.log('Static file server running at\n  => http://localhost:' + port + '/\nCTRL + C to shutdown');

开启chrome支持sourcemap

开启chrome的支持

使用node启动server

启动node server

浏览器中调试

chrome需要server支持

发布

这篇文章是来讲模块化的,和发布没啥关系,但是都写到这里了,就把程序发布出去吧,后面借着这篇文章讨论工程化的时候,可以在看看这篇文章的流程如何提高。
发布的方法无非这么几种:

  1. windows server的话直接远程过去,copy一下就好。web deploy这种工具也很好用。
  2. linux使用ftp到远程,再去copy一下。
  3. 使用rsync。

我们看一下第三种吧。我们用r.js优化了以后怎么发布到服务器上呢。我们按照Deployment-Techniques这个文章推荐的方法说一说。这个发布方法是在这些考虑下提出的。

  1. 构建后的代码不提交到版本控制。理由主要是为了好维护,提交前build一下很容易忘记,而且提交优化后的代码如果冲突了很难diff,merge。
  2. 使用r.js在server上生成构建后的代码也不好,因为r.js会删除目录再重新创建,所以如果项目很大,有一段时间服务就会有很多404错误。

所以我们想到了用增量更新的方法去同步文件夹。主要依赖rsync这个命令了。
文章推荐使用grunt工具来打包,然后再跑一个命令去同步文件夹。我们看看代码。

/**
 * Gruntfile.js
 */
module.exports = function(grunt) {
    // Do grunt-related things in here

    var requirejs = require("requirejs"),
        exec = require("child_process").exec,
        fatal = grunt.fail.fatal,
        log = grunt.log,
        verbose = grunt.verbose,
        FS = require('fs'),
        json5 = FS.readFileSync("./build.js", 'utf8'),
        JSON5 = require('json5'),
        // Your r.js build configuration
        buildConfigMain = JSON5.parse(json5);

    // Transfer the build folder to the right location on the server
    grunt.registerTask(
        "transfer",
        "Transfer the build folder to ../website/www-built and remove it",
        function() {
            var done = this.async();
            // Delete the build folder locally after transferring
            exec("rsync -rlv --delete --delete-after ../www-built ../website && rm -rf ../www-built",
                function(err, stdout, stderr) {
                    if (err) {
                        fatal("Problem with rsync: " + err + " " + stderr);
                    }
                    verbose.writeln(stdout);
                    log.ok("Rsync complete.");
                    done();
                });
        }
    );

    // Build static assets using r.js
    grunt.registerTask(
        "build",
        "Run the r.js build script",
        function() {
            var done = this.async();
            log.writeln("Running build...");
            requirejs.optimize(buildConfigMain, function(output) {
                log.writeln(output);
                log.ok("Main build complete.");
                done();
            }, function(err) {
                fatal("Main build failure: " + err);
            });

            // This is run after the build completes
            grunt.task.run(["transfer"]);
        }
    );
};

运行结果
可以看到新建了一个website文件夹,并把构建的中间文件同步到此文件夹下面了,而website文件是可以在远程服务器的,是不是很方便呢?

发布结果

上面的改动可以从这里下载到,大家可以把玩一下requirejs-deploy-demo

总结

可以看到,通过require.js,r.js可以很好的进行模块话的开发;使用grunt,rsync,我们可以完成构建和发布的功能。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容