JavaScript模塊化-require.js,r.js和打包發(fā)布

JavaScript模塊化和閉包JavaScript-Module-Pattern-In-Depth這兩篇文章中,提到了模塊化的基本思想,但是在實(shí)際項(xiàng)目中模塊化和項(xiàng)目人員的分工,組建化開(kāi)發(fā),打包發(fā)布,性能優(yōu)化,工程化管理都有密切的關(guān)系,這么重要的事情,在JavaScript大行其道的今天,不可能沒(méi)有成熟的解決方案,所以從我的實(shí)踐經(jīng)驗(yàn)出發(fā),從模塊化講到工程化,分享一下自己的經(jīng)驗(yàn)。

這篇文章主要是講require.js和r.js在項(xiàng)目中的使用,不會(huì)涉及到工程化問(wèn)題,對(duì)此熟悉的看官可以略過(guò)此文。對(duì)于require.js基本用法不熟悉的朋友,可以看看這個(gè)blog:asynchronous_module_definition

JavaScript的模塊化

流行的模塊化解決方案現(xiàn)在有很多,主要分為以下幾種規(guī)范

  • AMD:今天討論的主題,AMD 規(guī)范是JavaScript開(kāi)發(fā)的一次重要嘗試,它以簡(jiǎn)單而優(yōu)雅的方式統(tǒng)一了JavaScript的模塊定義和加載機(jī)制,并迅速得到很多框架的認(rèn)可和采納。這對(duì)開(kāi)發(fā)人員來(lái)說(shuō)是一個(gè)好消息,通過(guò)AMD我們降低了學(xué)習(xí)和使用各種框架的門(mén)檻,能夠以一種統(tǒng)一的方式去定義和使用模塊,提高開(kāi)發(fā)效率,降低了應(yīng)用維護(hù)成本。
  • CommonJS:node.js的方式,在前端需要打包工具配合使用。在后端比較好用。
  • CMD & sea.js: 國(guó)內(nèi)牛人搞的。LABjs、RequireJS、SeaJS 哪個(gè)最好用?為什么?

JavaScript的模塊化需要解決下面幾個(gè)問(wèn)題

  • 定義模塊
  • 管理模塊依賴(lài)
  • 加載模塊
  • 加載優(yōu)化
  • 代碼調(diào)試支持

為了直觀的理解一下流行了很久的require.js和r.js是如何解決這些問(wèn)題的,我們從一個(gè)例子入手吧。下載example-multipage-shim

代碼結(jié)構(gòu)

我們看一下基于requirejs的多頁(yè)面項(xiàng)目的一個(gè)基本結(jié)構(gòu):


example-multipage-shim文件結(jié)構(gòu)

下面我們看看如何解決js模塊化的問(wèn)題的。

定義模塊

看一下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就可以定義了。不需要我們自己手動(dòng)導(dǎo)出全局變量啦。

管理模塊依賴(lài)

看一下c1.js

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

可以看到通過(guò)['./Base']注入依賴(lài)。
在看一下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>');
    });
});

也可以通過(guò)require的方式(CommonJS風(fēng)格)去加載依賴(lài)模塊

加載模塊

看一下如何啟動(dòng),看看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標(biāo)簽引入require.js,然后使用requirejs加載模塊,而這些模塊本來(lái)也要用script標(biāo)簽引用的,所以說(shuō)requirejs幫助我們管理文件加載的事情了??梢允褂?code>data-main屬性去加載,詳細(xì)說(shuō)明可以看文檔了。
我們看一下運(yùn)行效果。

運(yùn)行效果

可以看到requirejs幫助我們家在了所有模塊,我們可以更好的組織JavaScript代碼了。

優(yōu)化加載

我們模塊化代碼以后,并不想增加請(qǐng)求的次數(shù),這樣會(huì)使網(wǎng)頁(yè)的性能降低(這里是異步加載,但是瀏覽器異步請(qǐng)求的過(guò)多,還是有問(wèn)題的),所以我們想合并一下代碼。
使用r.js:

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

看看結(jié)果:


構(gòu)建后

構(gòu)建后我們的代碼都經(jīng)過(guò)處理了。

看看運(yùn)行效果。

減少了請(qǐng)求

可見(jiàn)可以通過(guò)r.js幫助我們優(yōu)化請(qǐng)求(通過(guò)合并文件)。

如何配置

  • requirejs如何配置,我們看看common.js
requirejs.config({
    baseUrl: 'js/lib', 從這個(gè)位置加載模塊
    paths: {
        app: '../app' 
    },
    shim: {
        backbone: {
            deps: ['jquery', 'underscore'],
            exports: 'Backbone'
        },
        underscore: {
            exports: '_'
        }
    }
});
屬性 意義
baseUrl 加載模塊的位置
app:'../app' 像這樣的'app/sub',在app目錄下找sub模塊
shim 全局導(dǎo)出的庫(kù),在這里包裝

可以查看中文說(shuō)明書(shū)看看更詳細(xì)的說(shuō)明。


  • r.js如何配置,我們看看build.js
    這里面有很全的配置說(shuō)明example.build.js,過(guò)一下我們自己是怎么配置的。
{
    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下面定義的數(shù)組,實(shí)際上就是一個(gè)個(gè)文件的依賴(lài)關(guān)系,r.js會(huì)一用這里的關(guān)系,合并文件。詳細(xì)的配置意義可以看文檔

提示:r.js還可以?xún)?yōu)化css。

如何調(diào)試

前面代碼被優(yōu)化了以后,調(diào)試起來(lái)就痛苦了,這里我們可以使用sourcemap技術(shù)來(lái)調(diào)試優(yōu)化后的代碼。進(jìn)行如下操作。

  1. 修改build.js,增加如下配置
   generateSourceMaps: true,
   preserveLicenseComments: false,
   optimize: "uglify2",
  1. 重新構(gòu)建
node r.js -o build.js
  1. 打開(kāi)瀏覽器支持
    這里最好用firefox瀏覽器,chrome從本地文件打開(kāi)html不能正常使用sourcemap。直接用firefox瀏覽就可以了。


    firefox支持sourcemap

    可以看到可以加載非優(yōu)化的代碼,有人會(huì)問(wèn),這不要請(qǐng)求多次嗎??jī)?yōu)化一份,非優(yōu)化一份,這樣不是性能更差勁。其實(shí)只有你調(diào)試的時(shí)候,開(kāi)啟了這個(gè)功能才會(huì)請(qǐng)求對(duì)應(yīng)的sourcemap文件,所以對(duì)用戶(hù)來(lái)說(shuō)并不浪費(fèi)。

  2. 寫(xiě)一個(gè)server讓chrome也支持
    chrome本身是支持source map的,就是從硬盤(pán)直接打開(kāi)文件的權(quán)限有特殊處理。以file://開(kāi)頭的路徑很多事情做不了。所以我們做一個(gè)簡(jiǎn)單的server吧。

在tools目錄下增加一個(gè)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');

開(kāi)啟chrome支持sourcemap

開(kāi)啟chrome的支持

使用node啟動(dòng)server

啟動(dòng)node server

瀏覽器中調(diào)試

chrome需要server支持

發(fā)布

這篇文章是來(lái)講模塊化的,和發(fā)布沒(méi)啥關(guān)系,但是都寫(xiě)到這里了,就把程序發(fā)布出去吧,后面借著這篇文章討論工程化的時(shí)候,可以在看看這篇文章的流程如何提高。
發(fā)布的方法無(wú)非這么幾種:

  1. windows server的話(huà)直接遠(yuǎn)程過(guò)去,copy一下就好。web deploy這種工具也很好用。
  2. linux使用ftp到遠(yuǎn)程,再去copy一下。
  3. 使用rsync。

我們看一下第三種吧。我們用r.js優(yōu)化了以后怎么發(fā)布到服務(wù)器上呢。我們按照Deployment-Techniques這個(gè)文章推薦的方法說(shuō)一說(shuō)。這個(gè)發(fā)布方法是在這些考慮下提出的。

  1. 構(gòu)建后的代碼不提交到版本控制。理由主要是為了好維護(hù),提交前build一下很容易忘記,而且提交優(yōu)化后的代碼如果沖突了很難diff,merge。
  2. 使用r.js在server上生成構(gòu)建后的代碼也不好,因?yàn)閞.js會(huì)刪除目錄再重新創(chuàng)建,所以如果項(xiàng)目很大,有一段時(shí)間服務(wù)就會(huì)有很多404錯(cuò)誤。

所以我們想到了用增量更新的方法去同步文件夾。主要依賴(lài)rsync這個(gè)命令了。
文章推薦使用grunt工具來(lái)打包,然后再跑一個(gè)命令去同步文件夾。我們看看代碼。

/**
 * 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"]);
        }
    );
};

運(yùn)行結(jié)果
可以看到新建了一個(gè)website文件夾,并把構(gòu)建的中間文件同步到此文件夾下面了,而website文件是可以在遠(yuǎn)程服務(wù)器的,是不是很方便呢?

發(fā)布結(jié)果

上面的改動(dòng)可以從這里下載到,大家可以把玩一下requirejs-deploy-demo

總結(jié)

可以看到,通過(guò)require.js,r.js可以很好的進(jìn)行模塊話(huà)的開(kāi)發(fā);使用grunt,rsync,我們可以完成構(gòu)建和發(fā)布的功能。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容