zoukankan      html  css  js  c++  java
  • 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",
    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
    ');
                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
      => http://localhost:' + port + '/
    CTRL + 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
    來源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
  • 相关阅读:
    KNN算法
    mysql必须知道的
    励志的演讲
    30条程序员名言警句
    别人的文章:为什么软件开发,人多,事少,还会工作量大?
    分享一个比较受启发的文章“学历代表过去,能力代表现在,学习力代表未来”
    mvc 学前必知
    启动docker容器 防火墙问题报错 ! -i docker0' failed: iptables: No chain/target/match by that name.
    git指令详解总结
    git reset 版本回退的三种用法总结
  • 原文地址:https://www.cnblogs.com/m2maomao/p/7766034.html
Copyright © 2011-2022 走看看