AMD和RequireJS初识----优化Web应用前端(按需动态加载JS)

RequireJS是一个非常小巧的JavaScript模块载入框架,是AMD规范最好的实现者之一。最新版本的RequireJS压缩后只有14K,堪称非常轻量。它还同时可以和其他的框架协同工作,使用RequireJS必将使您的前端代码质量得以提升。

一、AMD 介绍

前端开发在近一两年发展的非常快,JavaScript作为主流的开发语言得到了前所未有的热捧。大量的前端框架出现了,这些框架都在尝试着解决一 些前端开发中的共性问题,但是实现又不尽相同。在这个背景下,CommonJS社区诞生了,为了让前端框架发展的更加成熟,CommonJS鼓励开发人员 一起在社区里为一些完成特定功能的框架制定规范。AMD(Asynchronous Module Definition)就是其中的一个规范。

传统JavaScript代码的问题

让我们来看看一般情况下JavaScript代码是如何开发的:通过<script>标签来载入JavaScript文件,用全局变量 来区分不同的功能代码,全局变量之间的依赖关系需要显式的通过指定其加载顺序来解决,发布应用时要通过工具来压缩所有的JavaScript代码到一个文 件。当Web项目变得非常庞大,前端模块非常多的时候,手动管理这些全局变量间的依赖关系就变得很困难,这种做法显得非常的低效。

AMD(Asynchronous Module Definition)的引入

从名称上看便知它是适合script tag的。也可以说AMD是专门为浏览器中JavaScript环境设计的规范。它吸取了CommonJS的一些优点,但又不照搬它的格式。开始AMD作为CommonJS的transport format 存在,因无法与CommonJS开发者达成一致而独立出来。它有自己的wiki 和讨论组 。

AMD提出了一种基于模块的异步加载JavaScript代码的机制,它推荐开发人员将JavaScript代码封装进一个个模块,对全局对象的依 赖变成了对其他模块的依赖,无须再声明一大堆的全局变量。通过延迟和按需加载来解决各个模块的依赖关系。模块化的JavaScript代码好处很明显,各 个功能组件的松耦合性可以极大的提升代码的复用性、可维护性。这种非阻塞式的并发式快速加载JavaScript代码,使Web页面上其他不依赖 JavaScript代码的UI元素,如图片、CSS以及其他DOM节点得以先加载完毕,Web页面加载速度更快,用户也得到更好的体验。

CommonJS的AMD规范中只定义了一个全局的方法,如清单1所示。

define(id?, dependencies?, factory);  

该方法用来定义一个JavaScript模块,开发人员可以用这个方法来将部分功能模块封装在这个define方法体内。

id表示该模块的标识,为可选参数。

dependencies是一个字符串Array,表示该模块依赖的其他所有模块标识,模块依赖必须在真正执行具体的factory方法前解决,这 些依赖对象加载执行以后的返回值,可以以默认的顺序作为factory方法的参数。dependencies也是可选参数,当用户不提供该参数时,实现 AMD的框架应提供默认值为[“require”,”exports”,“module”]。

factory是一个用于执行改模块的方法,它可以使用前面dependencies里声明的其他依赖模块的返回值作为参数,若该方法有返回值,当该模块被其他模块依赖时,返回值就是该模块的输出。

CommonJS在规范中并没有详细规定其他的方法,一些主要的AMD框架如RequireJS、curl、bdload等都实现了define方法,同时各个框架都有自己的补充使得其API更实用。

AMD设计出一个简洁的写模块API:

define(id?, dependencies?, factory);

 其中:

  • id: 模块标识,可以省略。
  • dependencies: 所依赖的模块,可以省略。
  • factory: 模块的实现,或者一个JavaScript对象。
id遵循CommonJS Module Identifiers 。dependencies元素的顺序和factory参数一一对应。
以下是使用AMD模式开发的简单三层结构(基础库/UI层/应用层):
1,定义无依赖的模块(base.js)
define(function() {
    return {
        mix: function(source, target) {
        }
    };
});
2,定义有依赖的模块(ui.js,page.js)
ui.js
define(['base'], function(base) {
    return {
        show: function() {
            // todo with module base
        }
    }
});

page.js

define(['data', 'ui'], function(data, ui) {
    // init here
});
3,定义数据对象模块(data.js)
define({
    users: [],
    members: []
});
细心的会发现,还有一种没有出现,即具名模块
 4,具名模块
define('index', ['data','base'], function(data, base) {
    // todo
});
具名模块多数时候是不推荐的,一般由打包工具合并多个模块到一个js文件中时使用。
前面提到dependencies元素的顺序和factory一一对应,其实不太严谨。AMD开始为摆脱CommonJS的束缚,开创性的提出了自己的模块风格。但后来又做了妥协,兼容了 CommonJS Modules/Wrappings 。即又可以这样写
5,包装模块
define(function(require, exports, module) {
    var base = require('base');
    exports.show = function() {
        // todo with module base
    }
});
不考虑多了一层函数外,格式和Node.js是一样的。使用require获取依赖模块,使用exports导出API。
除了define外,AMD还保留一个关键字require。require 作为规范保留的全局标识符,可以实现为 module loader。也可以不实现。
目前,实现AMD的库有RequireJS 、curl 、Dojo 、bdLoadJSLocalnet 、Nodules 等。
也有很多库支持AMD规范,即将自己作为一个模块存在,如MooTools 、jQuery 、qwery 、bonzo  甚至还有 firebug 。

二、RequireJS

RequireJS会让你以不同于往常的方式去写JavaScript。你将不再使用script标签在HTML中引入JS文件,以及不用通过script标签顺序去管理依赖关系。

1、简单示例

当然也不会有阻塞(blocking)的情况发生。好,以一个简单示例开始。

<!doctype html>
<html>
    <head>
        <title>requirejs入门(一)</title>
        <meta charset="utf-8">
        <!--引入require.js(实际上除了require.js,其它文件模块都不再使用script标签引入)--->
        <script data-main="main" src="require.js"></script>
     </head>
     <body>
    ...
     </body>
</html>

main.js

require.config({
    paths: {
        jquery: 'jquery-1.7.2'
    }
});

require(['jquery'], function($) {
    alert($().jquery);
});

main.js中就两个函数调用require.config和require。

  • require.config用来配置一些参数,它将影响到requirejs库的一些行为。require.config的参数是一个JS对象,常用的配置有baseUrl,paths等。
  • 这里配置了paths参数,使用模块名“jquery”,其实际文件路径jquery-1.7.2.js(后缀.js可以省略)。

这里require函数的第一个参数是数组,数组中存放的是模块名(字符串类型),数组中的模块与回调函数的参数一一对应。这里的例子则只有一个模块“jquery”。

  • 我们知道jQuery从1.7后开始支持AMD规范,即如果jQuery作为一个AMD模块运行时,它的模块名是“jquery”。注意“jquery”是固定的,不能写“jQuery”或其它。
  • 如果文件名“jquery-1.7.2.js”改为“jquery.js”就不必配置paths参数了。
  • require.config中config可以省略

jQuery中的支持AMD代码如下

if ( typeof define === "function" && define.amd && define.amd.jQuery ) {
	define( "jquery", [], function () { return jQuery; } );
}

我们知道jQuery最终向外暴露的是全局的jQuery和 $。如下

// Expose jQuery to the global object
window.jQuery = window.$ = jQuery;

 如果将jQuery应用在模块化开发时,其实可以不使用全局的,即可以不暴露出来。需要用到jQuery时使用require函数即可,

 把目录r1放到apache或其它web服务器上,访问index.html。

 网络请求如下

 我们看到除了require.js外main.js和jquery-1.7.2.js也请求下来了。而它们正是通过requirejs请求的。

 页面上会弹出jQuery的版本

 这是一个很简单的示例,使用requirejs动态加载jquery。

2、写一个自己的模块:选择器

为演示方便这里仅实现常用的三种选择器id,className,attribute。RequireJS使用define来定义模块。

本例目的:  

  • 1、使用baseUrl来配置模块根目录,baseUrl可以是绝对路径也可以是相对路径。
  • 2、使用define定义一个函数类型模块,RequireJS的模块可以是JS对象,函数或其它任何类型(CommonJS/SeaJS则只能是JS对象)

<!doctype html>
<html>
    <head>
        <title>requirejs入门(二)</title>
        <meta charset="utf-8">
        <style type="text/css">
            .wrapper {
                width: 200px;
                height: 200px;
                background: gray;
            }
        </style>
     </head>
     <body>
        <div class="wrapper"></div>
        <script data-main="js/main" src="require.js"></script>
     </body>
</html>

注意:

  • 把script标签放到了div的后面,因为要用选择器去获取页面dom元素,而这要等到dom ready后。
  • 因为把main.js放到js目录中,这里data-main的值须改为“js/main”(说明:data-main貌似可以省略????)

selector.js代码

define(function() {
    function query(selector,context) {
        var s = selector,
            doc = document,
            regId = /^#[w-]+/,
            regCls = /^([w-]+)?.([w-]+)/,
            regTag = /^([w*]+)$/,
            regNodeAttr = /^([w-]+)?[([w]+)(=(w+))?]/;        
        var context = 
                context == undefined ?
                document :
                typeof context == 'string' ?
                doc.getElementById(context.substr(1,context.length)) :
                context;                
        if(regId.test(s)) {
            return doc.getElementById(s.substr(1,s.length));
        }
        // 略...
    }
    return query;
});

define的参数为一个匿名函数,该匿名函数执行后返回query,query为函数类型。query就是选择器的实现函数。

main.js 如下

require.config({
    baseUrl: 'js'
});

require(['selector'], function(query) {
    var els = query('.wrapper');
    console.log(els)
});

require.config方法执行配置了baseUrl为“js”,baseUrl指的模块文件的根目录,可以是绝对路径或相对路径。这里用的是相对路径。相对路径指引入require.js的页面为参考点,一般是index.html。

把目录r2放到apache或其它web服务器上,访问index.html。

 网络请求如下

 main.js和selector.js都请求下来了。

selector.js下载后使用query获取页面class为“.wrapper”的元素,控制台输出了该元素。如下

3、写一个具有依赖的事件模块

具有依赖的事件模块event提供三个方法bind、unbind、trigger来管理DOM元素事件。

event依赖于cache模块,cache模块类似于jQuery的$.data方法。提供了set、get、remove等方法用来管理存放在DOM元素上的数据。

示例实现功能:  为页面上所有的段落P元素添加一个点击事件,响应函数会弹出P元素的innerHTML。

为了获取元素,用到了上一例写的selector.js。不在重复贴其代码

<!doctype html>
<html>
    <head>
        <title>requirejs入门(三)</title>
        <meta charset="utf-8">
        <style type="text/css">
            p {
                width: 200px;
                background: gray;
            }
        </style>
     </head>
     <body>
        <p>p1</p><p>p2</p><p>p3</p><p>p4</p><p>p5</p>
        <script data-main="js/main" src="require.js"></script>
     </body>
</html>

cache.js

define(function() {
    var idSeed = 0,
        cache = {},
        id = '_ guid _';
    // @private
    function guid(el) {
        return el[id] || (el[id] = ++idSeed);
    }
    return {
        set: function(el, key, val) {
            if (!el) {
                throw new Error('setting failed, invalid element');
            }
            var id = guid(el),
                c = cache[id] || (cache[id] = {});
            if (key) c[key] = val;
            return c;
        },
        // 略去...
    };
});

cache模块的写法没啥特殊的,与selector不同的是返回的是一个JS对象。

 event.js 如下

define(['cache'], function(cache) {    
    var doc = window.document,
        w3c = !!doc.addEventListener,
        expando = 'snandy' + (''+Math.random()).replace(/D/g, ''),
        triggered,
        addListener = w3c ?
            function(el, type, fn) { el.addEventListener(type, fn, false); } :
            function(el, type, fn) { el.attachEvent('on' + type, fn); },
        removeListener = w3c ?
            function(el, type, fn) { el.removeEventListener(type, fn, false); } :
            function(el, type, fn) { el.detachEvent('on' + type, fn); };

    // 略去...    
    return {
        bind : bind,
        unbind : unbind,
        trigger : trigger
    };
});

event依赖于cache,定义时第一个参数数组中放入“cache”即可。第二个参数是为函数类型,它的参数就是cache模块对象。
这样定义后,当require事件模块时,requirejs会自动将event依赖的cache.js也下载下来。

 main.js 如下

require.config({
    baseUrl: 'js'
});

require(['selector', 'event'], function($, E) {
    var els = $('p');
    for (var i=0; i<els.length; i++) {
        E.bind(els[i], 'click', function() {
            alert(this.innerHTML);
        });
    }
});

依然先配置了下模块的根目录js,然后使用require获取selector和event模块。
回调函数中使用选择器$(别名)和事件管理对象E(别名)给页面上的所有P元素添加点击事件。
注意:require的第一个参数数组内的模块名必须和回调函数的形参一一对应。

把目录r3放到apache或其它web服务器上,访问index.html。网络请求如下

 我们看到当selector.js和event.js下载后,event.js依赖的cache.js也被自动下载了。这时点击页面上各个P元素,会弹出对应的innerHTML。如下

 总结:
当一个模块依赖(a)于另一个模块(b)时,定义该模块时的第一个参数为数组,数组中的模块名(字符串类型)就是它所依赖的模块。
当有多个依赖模时,须注意回调函数的形参顺序得和数组元素一一对应。此时requirejs会自动识别依赖,且把它们都下载下来后再进行回调。

说明和其他问题

1、路径与后缀名

在 require 一个 js 文件的时候,一般不需要加上后缀名。如果加上后缀名,会按照绝对路径加载。没有后缀名,是按照下面的路径加载:

     <script data-main="js/main" src="js/require-jquery.js"></script>

也就是默认加载 data-main 指定的目录,即 js/main.js 文件所在的目录。当然,你可以通过配置文件修改。

2、define 定义模块方法只能用在独立的js文件中,不能在页面中直接使用。
否则会报 Mismatched anonymous define() module 错误。

3、和其他第三方js类库是否冲突?

不会冲突。一般比较规范的类库,都会给自己的js加上命名空间。比如 wojilu 旧有的 wojilu.common.js ,其实就是放在 wojilu 命名空间中(当然是通过更原始的方式实现命名空间的)。

在通过 RequireJS 加载这些第三方的 js 的时候,完全不要有任何担忧。

当然,如果第三方类库能够使用 RequireJS 的方式进行改造,那是最好。比如 wojilu 中大多数js 都按照 RequireJS 的方式进行了改造。但是,如果你不改造,也是完全不要紧的。

4、在代码中 require 一个文件多次,是否会导致浏览器反复加载?

不会,这是 RequrieJS 的优点,即使你反复 require 它,它只加载一次。

参考:

原文地址:https://www.cnblogs.com/JoannaQ/p/3362588.html