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

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

1、AMD 介绍
css

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

传统JavaScript代码的问题前端

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

AMD(Asynchronous Module Definition)的引入jquery

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

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

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

define(id?, dependencies?, factory);  

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

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 。

2、RequireJS

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

一、简单示例

固然也不会有阻塞(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。

二、写一个本身的模块:选择器

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

本例目的:  

  • 一、使用baseUrl来配置模块根目录,baseUrl能够是绝对路径也能够是相对路径。
  • 二、使用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会自动识别依赖,且把它们都下载下来后再进行回调。

说明和其余问题

一、路径与后缀名

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

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

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

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

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

不会冲突。通常比较规范的类库,都会给本身的js加上命名空间。好比 wojilu 旧有的 wojilu.common.js ,其实就是放在 wojilu 命名空间中(固然是经过更原始的方式实现命名空间的)。

在经过 RequireJS 加载这些第三方的 js 的时候,彻底不要有任何担心。

固然,若是第三方类库可以使用 RequireJS 的方式进行改造,那是最好。好比 wojilu 中大多数js 都按照 RequireJS 的方式进行了改造。可是,若是你不改造,也是彻底没关系的。

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

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

参考:

相关文章
相关标签/搜索