http://www.ruanyifeng.com/blog/2012/10/javascript_module.html

本文内容

  • 引入
  • 模块化
    • 最初写法
    • 对象写法
    • 立即执行函数写法
    • 放大模式
    • 宽放大模式
    • 把模块作为参数
  • 模块化规范:CommonJS 和 Asynchronous Module Definition
    • CommonJS
    • Asynchronous Module Definition
  • 用 require.js 加载模块

 

引入


1995 年 Javascript 诞生时,只是一种为了用于交互的简单的网页脚本语言,像如果你忘记填写用户名,就跳出一个警告,当时的网速只有 28Kbps,这样简单的交互也让 Web 服务器做显然不合适。

如今,Javascript 几乎无所不能,从前端到后端,各种匪夷所思、令人瞠目结舌的用途,程序员用它完成越来越庞大的项目,代码复杂度也在直线飙升。单个网页包含 10000 行 Javascript 代码,早就司空见惯。2010 年,一个工程师透露:Gmail 代码长度是 443000 行!另外,Web 应用程序越来越像桌面应用程序,需要一个团队分工协作、进度管理、单元测试等等,开发者不得不使用软件工程的方法,管理网页的业务逻辑。

编写和维护越来越庞大复杂的代码将越来越困难。Javascript 模块化编程,已经成为一个迫切的需求。理想情况下,开发者只需要实现核心的业务逻辑,其他都可以加载别人已经写好的模块。但是,Javascript 不是一种模块化编程语言,其语法不支持“类”,更别说“模块”了。

正在制定中的 ECMAScript标准 第六版,将正式支持“类”和“模块”,但还需要很长时间才能投入实用。

鉴于此,Javascript 社区做了很多努力。本文总结了当前"Javascript 模块化编程"的最佳实践。但是,在此之前,你最好还是体会一下如何用 Javascript 模拟“类”,这是进行 Javascript 模块化编程和其面向对象编程的基础,进而使用面向对象的继承、接口,甚至是设计模式。

 

模块化


最初写法

该方法,你肯定不陌生,但凡是开始写 Javascript 代码的人,都是从这种方法开始的。几个在一起的函数就是一个模块。如下所示:

  function m1(){

    // do something

  }

 

  function m2(){

    // do something

  }

上面函数 m1 和 m2,组成一个模块。使用的时候,直接调用就行了。

该方法的缺点很明显:“污染”了全局变量,因为不能保证是否与其他模块发生变量名的冲突,而且模块成员之间看不出直接关系。

对象写法

为了解决上面写法的缺点,可以把模块写成一个对象,所有的模块成员都放到这个对象里。

  var module1 = new Object({

    _count : 0,

    m1 : function (){

      // do something

    },

    m2 : function (){

      // do something

    }

  });

现在函数 m1 和 m2 都封装在 module1 对象里。使用时直接调用就行,如下所示:

  module1.m1();

这样做的问题是,会暴露所有模块成员(属性和方法),内部状态可以被外部修改。如下所示,外部可以直接修改内部计数器的值:

  module1._count = 5;

立即执行函数写法

使用“立即执行函数(Immediately-Invoked Function Expression,IIFE)”,可以达到不暴露私有成员的目的。

  var module1 = (function(){

    var _count = 0;

    var m1 = function(){

      // do something

    };

    var m2 = function(){

      // do something

    };

    return {

      m1 : m1,

      m2 : m2

    };

  })();

这样外部代码就无法修改内部变量 _count

  console.info(module1._count); //undefined

这就是 module1 的基本写法,下面对这种写法改进。

放大模式

如果一个模块很大,就必须拆分;或是,一个模块继承另一个模块,此时,有必要采用“放大模式”。

  var module1 = (function (mod){

    mod.m3 = function () {

      // do something

    };

    return mod;

  })(module1);

上面的代码为 module1 模块添加了一个新方法 m3,然后返回新的 module1 模块。

宽放大模式

实际中,模块的各个部分通常都是从网上获取,有时无法知道哪个部分会先加载。如果采用上一节的写法,第一个执行的部分有可能加载一个不存在对象,这时就需要采用“宽放大模式”。

  var module1 = ( function (mod){

    // do something

    return mod;

  })(window.module1 || {});

把模块作为参数

独立性是模块的重要特点,模块内部最好不与程序的其他部分直接交互。为了在模块内部使用全局变量,必须显式地将其他变量输入到模块。

  var module1 = (function ($, YAHOO) {

    // do something

  })(jQuery, YAHOO);

这个 module1 模块需要使用 jQuery 和 YUI,就把这两个现在流行的库(模块)作为参数输入到你的 module1。这样,除了保证模块的独立性,还使模块之间的依赖关系更明显。参见 Ben Cherry:JavaScript Module Pattern: In-Depth

以上就是 Javascript 模块的基本写法和需要注意的地方,接下来,说明如何规范地使用模块。

 

模块化规范:CommonJS 和 Asynchronous Module Definition


模块规范

为什么要规范?有了模块后,我们就可以更方便地使用别人的代码,需要什么,就加载什么。但这样做的前提是,大家必须以同样的方式编写模块,否则你有你的写法,我有我的写法,那岂不是乱套了!Javascript 目前还没有官方规范,但通行规范有两种:CommonJS 服务器端模块化规范和 AMD 客户端模块化规范。

CommonJS 服务器端模块化规范

2009年,美国程序员 Ryan Dahl 创造了 node.js 项目,将 Javascript 语言用于服务器端编程。node.js 模块系统就是参照 CommonJS 实现的。这标志着“Javascript模块化编程”正式诞生。因为老实说,在浏览器环境下,没有模块也不是特别大的问题,毕竟网页程序的复杂性有限;但是在服务器端,一定要有模块,与操作系统和其他应用程序互动,否则根本没法编程。

在 CommonJS 中,有一个全局性方法 require(),用于加载模块。假定有一个数学模块 math.js,可以像下面这样加载。

  var math = require('math');

然后,就可以调用模块提供的方法:

  var math = require('math');

  math.add(2,3); // 5

本文主要针对浏览器的客户端脚本模块化编程,因此,对 node.js,以及 CommonJS 不多做介绍。

有了服务器端模块后,很自然地,大家就想要客户端模块。而且最好两者能够兼容,一个模块不用修改,在服务器和浏览器都可以运行。但是很可惜,由于一个重大的局限,使得 CommonJS 规范不适用于浏览器环境。如上代码所示,如果在浏览器中运行,会存在一个很大的问题,看出来了吗?

  var math = require('math');

  math.add(2, 3);

显然,math.add(2, 3) 必须在 require('math') 成功执行后才能运行,也就是说,math.add 必须等 math.js 通过网络成功加载后,才能执行。这样,如果加载时间很长,整个应用就会停在那里一直等待。

这对服务器端脚本没问题,因为模块就在本地硬盘,加载的速度就是读取磁盘的速度。但对于浏览器来说,这个问题大了,因为相对浏览器,模块都存放在服务器端,等待时间取决于网速。这样,浏览器很可能处于“假死”状态。

因此,浏览器端的模块,不能采用"同步加载"(synchronous),只能采用"异步加载"(asynchronous)。这就是AMD规范诞生的背景。

Asynchronous Module Definition(AMD)客户端模块化规范

AMD 是“异步模块定义”。它采用异步方式加载模块。所有依赖这个模块的语句都放在回调函数中,等到模块成功加载后,回调函数才会运行。AMD 虽然也采用 require 加载模块,但不同于CommonJS,它两个参数:

  require([module], callback);

其中,

  • 第一个参数 [module],是一个数组,要加载模块的名称。
  • 第二个参数 callback,是成功加载后的回调函数。

前面代码可以改行如下所示:

  require(['math'], function (math) {

    math.add(2, 3);

  });

math 模块 和 math.add 方法不是同步的,所以浏览器不会“假死”。显然,AMD 比较适合浏览器环境。

目前,主要有两个 Javascript 库实现了 AMD 规范:require.jscurl.js。本文介绍非常流行的库 require.js。

 

用 require.js 加载模块


为什么要使用 require.js?

最初,所有 Javascript 代码都写在一个文件里面,只要加载这一个文件就够了。可是后来,代码越来越多,一个文件显然不行了,必须拆分,依次加载。下面网页代码,你肯定不陌生。

  <script src="1.js"></script>

  <script src="2.js"></script>

  <script src="3.js"></script>

  <script src="4.js"></script>

  <script src="5.js"></script>

  <script src="6.js"></script>

该代码依次加载多个 js 文件。这样的写法有很大的缺陷:

  • 加载 js 文件时,浏览器会停止网页渲染,加载文件越多,网页失去响应的时间就越长。
  • 由于 js 文件之间是有依赖关系的,必须严格保证加载顺序。依赖性最大的模块一定要最后加载,当依赖关系很复杂的时候,代码的编写和维护会变得很困难。

require.js 就是为了解决这些问题而产生的:

  • 实现 js 文件的异步加载,避免网页失去响应。
  • 管理模块之间的依赖性,便于代码的编写和维护。

加载 require.js

先在官方网站下载最新版本的 require.js。下载后,假定把它放在 js 子目录下,用下面代码加载:

  <script src="js/require.js"></script>

可加载这个文件本身也可能因为网络或是服务器问题,不能成功加载,而造成网页失去响应。解决方法有两个,一个是把它放在网页底部加载,另一个是写成下面这样:

  <script src="js/require.js" defer async="true" ></script>

async 属性表明文件需要异步加载,避免网页失去响应。IE 不支持这个属性,只支持 defer,所以把 defer 也写上。

自定义主模块

加载 require.js 后,就要加载我们自己的代码了。假定我们自己的代码文件是 main.js,它是你的“主模块”,也放在 js 目录下面。那么,只需要写成下面这样就行了:

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

data-main 属性的作用是,指定网页程序的主模块。在上例中,就是 js 目录下面的 main.js,这个文件会第一个被 require.js 加载。由于 require.js 默认的文件后缀名是 js,所以可以把 main.js 简写成 main。

main.js 是“主模块”,意味着是整个网页的入口代码,有点像 C 语言的 main() 函数,所有代码都从这儿开始运行。

如果我们的代码不依赖任何其他模块,那么可以直接写入 javascript 代码。

  // main.js

  alert("加载成功!");

要这么写就没必要使用 require.js。实际的情况是,主模块依赖于其他模块,这就需要使用 AMD 规范定义的的 require 函数了。

  // main.js

  require(['moduleA', 'moduleB', 'moduleC'], function (moduleA, moduleB, moduleC){

    // some code here

  });

require 函数有两个参数,其中,

  • 第一个参数,是一个数组,表示所依赖的模块,本代码是 ['moduleA', 'moduleB', 'moduleC'],即主模块依赖三个模块。
  • 第二个参数,是一个回调函数,当指定的所有模块都成功加载后,才会被调用。加载的模块以参数形式传入该函数,从而在回调函数中使用这些模块。

这样,require 异步加载 moduleA,moduleB 和 moduleC,浏览器不会失去响应;指定了回调函数,只有当所有模块都加载成功才会运行,解决了 js 依赖性的问题。

假设,主模块依赖 jquery、underscore 和 backbone 三个模块,main.js 可以这样写:

  require(['jquery', 'underscore', 'backbone'], function ($, _underscore, Backbone){

    // some code here

  });

自定义加载

默认情况下,require.js 假设你的模块与 main.js 在同一目录,文件名分别为 jquery.js,underscore.js 和 backbone.js,然后自动加载。

使用 require.config 方法,我们可以自定义对模块加载的行为。require.config 写在主模块(main.js)的顶部。其参数是一个 paths 对象,指定各个模块的加载路径。如下所示:

  require.config({

    paths: {

      "jquery": "jquery.min",

      "underscore": "underscore.min",

      "backbone": "backbone.min"

    }

  });

上面代码表明你的三个模块文件与 main.js 在同一个 js 目录。如果这些模块在其他目录,如 js/lib 目录,则有两种写法。要么逐一指定,要么直接指定基目录。

  require.config({

    paths: {

      "jquery": "lib/jquery.min",

      "underscore": "lib/underscore.min",

      "backbone": "lib/backbone.min"

    }

  });

或是,

  require.config({

        baseUrl: "js/lib",

    paths: {

      "jquery": "jquery.min",

      "underscore": "underscore.min",

      "backbone": "backbone.min"

    }

  });

如果某个模块在另一台主机上,也可以直接指定它的网址,比如:

  require.config({

    paths: {

      "jquery": "https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min"

    }

  });

require.js 要求,每个模块是一个单独的 js 文件。但这样,加载多个模块就会发出多个 HTTP 请求,影响网页的加载速度。因此,require.js 提供了一个优化工具,当模块部署完毕以后,该工具将多个模块合并在一个文件中,减少 HTTP 请求次数。

加载 AMD 规范的模块

require.js 加载的模块必须采用 AMD 规范。具体来说,模块必须采用 define 函数来定义。如果一个模块不依赖其他模块,那么可以直接定义在define()函数之中。

假设,有一个 math.js 文件,定义了一个 math 模块。如下所示:

  // math.js

  define(function (){

    var add = function (x,y){

      return x+y;

    };

    return {

      add: add

    };

  });

那么,加载该模块的方法如下所示:

  // main.js

  require(['math'], function (math){

    alert(math.add(1,1));

  });

如果 math.js 模块还依赖其他模块,那么,define 函数的第一个参数(类型是数组),指明该模块的依赖性。

  define(['myLib'], function(myLib){

    function foo(){

      myLib.doSomething();

    }

    return {

      foo : foo

    };

  });

当 require 函数加载 math.js 前,会先加载 myLib.js 文件。

加载非 AMD 规范的模块

理论上,require.js 加载的模块必须符合 AMD 规范,即用 define 函数定义的模块。但实际情况是,虽然已经有一部分流行的函数库(比如 jQuery)符合 AMD 规范,更多的库并不符合。那么,require.js 是否能够加载非规范的模块呢?当然可以。加载非规范模块前,要先用 require.config 方法,定义它们的一些特征。

例如,上面三个模块:jquery.js、underscore.js 和 backbone.js,其中,jQuery.js 符合规范,而 underscore 和 backbone 这两个库不符合。如果要加载它们的话,必须先定义它们的特征。

  require.config({

    shim: {

      'underscore':{

        exports: '_'

      },

      'backbone': {

        deps: ['underscore', 'jquery'],

        exports: 'Backbone'

      }

    }

  });

require.config 接受一个配置对象 shim,专门用来配置不兼容的模块。具体来说,每个模块要定义:

  • exports值(输出的变量名),表明这个模块外部调用时的名称。
  • deps数组,表明该模块的依赖性。

比如,jQuery 插件可以这样定义:

  shim: {

    'jquery.scroll': {

      deps: ['jquery'],

      exports: 'jQuery.fn.scroll'

    }

  }

require.js 插件

require.js 还提供一系列插件,实现一些特定的功能。

domready 插件,可以让回调函数在页面 DOM 结构加载完成后再运行。

  require(['domready!'], function (doc){

    // called once the DOM is ready

  });

text 和 image 插件,则是允许require.js加载文本和图片文件。

  define([

    'text!review.txt', 

    'image!cat.jpg'

    ],

    function(review,cat){

      console.log(review);

      document.body.appendChild(cat);

    }

  );

类似的插件还有 json 和 mdown,用于加载 json 文件和 markdown 文件。

Javascript 的模块化编程及加载模块【转载+整理】的更多相关文章

  1. JS模块化编程之加载器原理

    世面上有好多JavaScript的加载器,比如 sea.js, require.js, yui loader, labJs...., 加载器的使用范围是一些比较大的项目, 个人感觉如果是小项目的话可以 ...

  2. JavaScript之模块化编程

    前言 模块是任何大型应用程序架构中不可缺少的一部分,模块可以使我们清晰地分离和组织项目中的代码单元.在项目开发中,通过移除依赖,松耦合可以使应用程序的可维护性更强.与其他传统编程语言不同,在当前Jav ...

  3. Javascript的模块化编程

    随着网站逐渐变成"互联网应用程序",嵌入网页的Javascript代码越来越庞大,越来越复杂. 网页越来越像桌面程序,需要一个团队分工协作.进度管理.单元测试等等......开发者 ...

  4. 学习了一下javascript的模块化编程

    现在在我脑海里关于“模块化”的概念是这些词:简单.具有逻辑之美.易用.健壮.可扩展.似乎这些形容与我现在水平写出的代码有点格格不入啊. 所以今天想了解和简单的实践一下“模块化开发”. 1.首先学习一下 ...

  5. AngularJs 通过 ocLazyLoad 实现动态(懒)加载模块和依赖

    好了,现进入正题,在 AngularJs 实现动态(懒)加载主要是依赖于3个主JS文件和一段依赖的脚本. 实现的过程主要是引用3个主要的JS文件 <script src="angula ...

  6. AngularJS中多个ng-app(手动加载模块)

    1.当有多个ng-app时:(首先是要加载angularJS) <div ng-app=""> <p>姓名:<input type="tex ...

  7. ThinkPhp3.2 无法加载模块:Index

    http://localhost:444/admin/index.php/Index/index出错:无法加载模块:Index http://localhost:444/admin/index.php ...

  8. AngularJs 通过 ocLazyLoad 实现动态(懒)加载模块和依赖-转

    http://blog.csdn.net/zhangh8627/article/details/51752872 AngularJs 通过 ocLazyLoad 实现动态(懒)加载模块和依赖 标签:  ...

  9. 【转】js JavaScript 的性能优化:加载和执行

    JavaScript 的性能优化:加载和执行 转自:https://www.ibm.com/developerworks/cn/web/1308_caiys_jsload/ 随着 Web2.0 技术的 ...

随机推荐

  1. 网站前端优化技术 BigPipe分块处理技术

    前端优化已经到极致了么?业务还在为看到不停的而揪心么?还在为2秒率不达标苦恼么? 好吧我知道答案,大家一如既往的烦恼中... 那么接下来我们看看,facebook,淘宝,人人网,一淘都是怎么做前端优化 ...

  2. Revit手工创建族

    手工创建族 1.画两个参考平面. 图3001 2.点击族类型,添加参数. 图3002,3003 3.添加类型,为类型赋值. 3004 4.创建拉伸截面,完成后,可以三维查看. 3005 5.创建对齐, ...

  3. 线程池大小设置,CPU的核心数、线程数的关系和区别,同步与堵塞完全是两码事

    线程池应该设置多少线程合适,怎么样估算出来.最近接触到一些相关资料,现作如下总结. 最开始接触线程池的时候,没有想到就仅仅是设置一个线程池的大小居然还有这么多的学问,汗颜啊. 首先,需要考虑到线程池所 ...

  4. 咏南Mormot中间件接口

    咏南Mormot中间件接口 只使用了MORMOT的HTTPS.SYS作为通讯,数据引擎使用FIREDAC,数据序列/还原是自行封装. 客户端支持FDMemeTable和ClientDataSet数据集 ...

  5. C++点和箭头操作符用

    http://www.cnblogs.com/ManMonth/archive/2013/09/05/3302873.html C++点和箭头操作符用法区别 变量是对象的时候用“.”访问 变量是对象指 ...

  6. System.loadLibrary()的使用方法汇总

    当使用System.loadLibrary()调用 Dll,两种方法: 1.设定环境变量. 比如:所编辑的Dll在目录“D:/cppProjects/nativecode/release”内,将这个路 ...

  7. 用wifi来调试应用程序

    我们一般调试程序都是用的adb,这个adb其实是可以连接到某个端口的,只要我们的手机和电脑处于同一wifi环境下(你可以用电脑分出来的wifi),手机也接入同一端口就可以实现程序的无线调试了,终于可以 ...

  8. 获取客户端网卡MAC地址和IP地址实现JS代码

    获取客户端网卡MAC地址和IP地址实现JS代码 作者: 字体:[增加 减小] 类型:转载   获取客户端的一些信息,如IP和MAC,以结合身份验证,相信很多人都会这样做吧,我们这里用Javascrip ...

  9. [转]RSA,DSA等加解密算法介绍

    From : http://blog.sina.com.cn/s/blog_a9303fd90101cgw4.html 1)      MD5/SHA MessageDigest是一个数据的数字指纹. ...

  10. 样条之埃尔米特(Hermite)插值函数

    核心代码: ////////////////////////////////////////////////////////////////////// // 埃尔米特等距插值 /////////// ...