1. 程式人生 > >JavaScript模塊化-require.js,r.js和打包發布

JavaScript模塊化-require.js,r.js和打包發布

tle github 程序發布 授權 versions windows aps append title

在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",
  2. 重新構建
    node r.js -o build.js
  3. 打開瀏覽器支持
    這裏最好用firefox瀏覽器,chrome從本地文件打開html不能正常使用sourcemap。直接用firefox瀏覽就可以了。

    技術分享
    firefox支持sourcemap


    可以看到可以加載非優化的代碼,有人會問,這不要請求多次嗎?優化一份,非優化一份,這樣不是性能更差勁。其實只有你調試的時候,開啟了這個功能才會請求對應的sourcemap文件,所以對用戶來說並不浪費。

  4. 寫一個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,我們可以完成構建和發布的功能。



作者:沈寅
鏈接:http://www.jianshu.com/p/7186e5f2f341
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請註明出處。

JavaScript模塊化-require.js,r.js和打包發布