高级前端人员的进阶之路 - 模块化编程

 Javascript不是一种模块化编程语言,它不支持""(class),更遑论"模块"(module),然而模块化编程,已经成为一个迫切的需求。

* 模块化的写法

基本写法

一:原始写法

 模块就是实现特定功能的一组方法,只要把不同的函数(以及记录状态的变量)简单地放在一起,就算是一个模块。

function f1() {...}
function f2() {...}

 上面的函数f1()和f2(),组成一个模块。使用的时候,直接调用就行了。
 这种做法的缺点很明显:

  1. "污染"了全局变量;
  2. 无法保证不与其他模块发生变量名冲突;
  3. 而且模块成员之间看不出直接关系。

二:对象写法

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

var module = new Object {
  _count: 0;
  f1: function(){
    // some code
  }
  f2: function() {
    // some code
  }
}

这样,我们调用方式就变成了

module.f1();

 这么写还有缺点:暴露了所有的模块成员,内部的状态可以被外部改变。比如

module._count = 5;

三:立即执行函数写法

 使用【立即执行函数】(Immediately-Invoked Function Expression,IIFE),可以达到不暴露成员变量的目的

var module = (function() {
  var _count = 0;
  var f1 = function(){
    // some code
  };
  var f2 = function(){
    // some code
  };
  
  return {
    f1: f1,
    f2: f2
  }                    
})();
console.log(module._count);   // undefined

 这样,外部代码无法读取内部的_count变量

放大模式

如果一个模块很大,必须分成几个部分,或者一个模块需要继承另一个模块,这时就有必要采用"放大模式"(augmentation)。

 为module1模块添加了一个新方法m3(),然后返回新的module1模块

var module = function(mod){
  var mod.f3 = function(){
    // some code
  }
  return mod;
})(module);

 在浏览器环境中,模块的各个部分通常都是从网上获取的,有时无法知道哪个部分会先加载。如果采用上一节的写法,第一个执行的部分有可能加载一个不存在空对象,为了更严谨一点,这时就要采用"宽放大模式"。

var module = (function(mod){
  // some code
  return mod;
})(window.module || {});

 这样,立即执行函数"的参数就可以是空对象

输入全局变量

  1. 独立性是模块的重要特点,模块内部最好不与程序的其他部分直接交互。
  2. 为了在模块内部调用全局变量,必须显式地将其他变量输入模块
var module = (function($, YAHOO){
  // some code
})(jQuery,YAHOO);

 上面的module1模块需要使用jQuery库和YUI库,就把这两个库(其实是两个模块)当作参数输入module1。这样做除了保证模块的独立性,还使得模块之间的依赖关系变得明显。

* 模块化的规范

为什么模块很重要?因为有了模块,我们就可以更方便地使用别人的代码,想要什么功能,就加载什么模块。但是,这样做有一个前提,那就是大家必须以同样的方式编写模块,否则你有你的写法,我有我的写法,岂不是乱了套!

CommonJs规范

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

 node.js的 模块系统 ,就是参照 CommonJS 规范实现的。在CommonJS中,有一个全局性方法require(),用于加载模块。假定有一个数学模块math.js,就可以像下面这样加载。

var math = require('math');

然后就可以调用模块定义的方法:

math.add(2, 3);

require()就是用于加载模块的。

AMD规范

AMD 是"Asynchronous Module Definition"的缩写,意思就是"异步模块定义",它采用异步方式加载模块。
 模块的加载不影响它后面语句的运行。我们将“后面的语句”分两部分:

  1. 与该模块无关的语句,受异步模块定义,不受模块加载的影响;
  2. 所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。

AMD采用 require() 加载模块,他有两个参数:

require([modules], callback);

第一个参数 [modules] 是一个数组,里面的成员是要加载的模块,第二个参数是callback, 表示加载之后的回调函数
所以,我们的模块化代码书写方式按照AMD规范就变成如下形式:

require([math], function(math){
  math.add(2, 3)
})

 math.add()与math模块加载不是同步的,浏览器不会发生假死。所以很显然,AMD比较适合浏览器环境

* 模块化的使用 - require.js

一: 为什么要用 require.js

 最早的时候,所有Javascript代码都写在一个文件里面,只要加载这一个文件就够了。后来,代码越来越多,一个文件不够了,必须分成多个文件,依次加载。相信很多小伙伴都这么写过:

<html>
  <head>
  ...
  </head>
  <body>
  ...
    <script src="jquery.js"></script>
    <script src="base.js"></script>
    <script src="core.js"></script>
    <script src="bootstrap.js"></script>
    <script src="component.js"></script>
    <script src="dialog.js"></script>
    <script src="event.js"></script>
  </body>
</html>

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

  1. 加载的时候,浏览器会停止网页渲染,加载文件越多,网页失去响应的时间就会越长;
  2. 由于js文件之间存在依赖关系,因此必须严格保证加载顺序(比如上例的jquery.js要在bootstrap.js的前面),依赖性最大的模块一定要放到最后加载,当依赖关系很复杂的时候,代码的编写和维护都会变得困难。

模块化 require.Js的诞生就是为了解决这两个问题:

  1. 实现js文件的异步加载,避免浏览器假死
  2. 管理模块之间的依赖,便于模块的维护

二: require.js 的加载

  在官网下载最新版本,将其放在js子目录下,index.html文件引入

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

加载这个问价同样会面临页面假死的现象,所以,我们通常这么做:

  1. 将加载语句放在网页(<body>)后面加载;
  2. 增加 defer anysc="true" 属性,如下
<script src="js/require.js" defer async="true"></script>

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

三:主模块的加载与写法

 require.js已经加载完成,下一步就是要加载我们自己的代码。假如文件为main.js,存放在js子目录下,我们用data-main属性引入:

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

 这里的 main.js,我们称之为“主模块”;意思就是整个网页的入口,它有点像C语言的main()函数,所有代码都从这儿开始运行。
 如果我们的代码不依赖任何其他模块,那么可以直接写入javascript代码。

// main.js
alert("load success");

 但如果真这样,就没必要使用require.js了。真正常见的情况是,主模块依赖于其他模块,这时就要使用AMD规范定义的的require()函数。试写一个如下:

// main.js
require(['moduleA', 'moduleB', 'moduleC'], function(moduleA, moduleB, moduleC){
  // some code
})

 这样,在主模块main.js中,依赖了三个模块 moduleA,moduleB,moduleC当这些模块加载完成之后,调起回调函数,我们将这些模块以参数的形式传入回调函数,从而在回调函数中就可以使用这些模块了。
require()异步加载moduleA,moduleBmoduleC,浏览器不会失去响应;它指定的回调函数,只有前面的模块都加载成功后,才会运行,解决了依赖性的问题。

一个实例
 假定主模块依赖jqueryunderscorebackbone这三个模块,main.js就可以这样写:

// main.js
require(['jquery', 'underscore', 'backbone'], function($, _, Backbone){
  // some code
})

require.js会先加载jQuery、underscorebackbone,然后再运行回调函数。主模块的代码就写在回调函数中。

四:模块的加载

1. 默认加载行为

 我们引入jqueryunderscorebackbone之后,require.js会默认为在与main.js的同级目录下有jquery.jsunderscore.jsbackbone.js这几个文件,然后自动加载。如果找不到,就会加载失败,引发异常。然而我们常引入且使用的是.min.js文件,那么就需要我们自定义。

2. 自定义加载行为

 使用require.config()方法,我们可以对模块的加载行为进行自定义。require.config()就写在主模块(main.js)的头部。参数就是一个对象,这个对象的paths属性指定各个模块的加载路径。另外,require.config() 提供了baseUrl来索引文件,默认的就是main.js所在路径。

  1. 默认以main.js的相对路径baseUrl加载
// main.js 头部
require.config({
  paths: {
    "jquery": "juery.min",
    "underscore": "lib/underscore.min",
    "backbone": "lib/backbone.min"
  }

});

 在上方的代码中,jquery.min,js的路径与main.js在同一个目录(js子目录),underscore.min.jsbackbone.min.js则在js目录的子目录lib中。

  1. 直接修改基目录baseUrl
// main.js 头部
require.config({
  baseUrl: "js/lib",
  paths: {
    "jquery": "../juery.min",
    "underscore": "underscore.min",
    "backbone": "backbone.min"
  }

});
  1. 如果某个模块在另一台主机上,也可以直接指定它的网址
// main.js 头部
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()函数来定义

  1. 新增一个模块,它不依赖其他模块,那么可以直接在define()函数中定义
// math.js
define(function(){
  var add = function(x, y){
    return x + y;
  }
  return {
    add: add
  };
});

 使用加载方式如下:

require(['math'],function(math){
  alert(math.add(1, 2));
});
  1. 如果新增的模块需要依赖其他模块
define(['myLib'], function(myLib){
  function f1(){
    myLib.doSomething();
  }
  
  return {
    f1: f1
  }
})

 当require()函数加上上面这个模块的时候,就会先加载myLib.js文件

六:加载非规范的模块

 理论上,require.js加载的模块,必须是按照AMD规范、用define()函数定义的模块,但是实际上,虽然已经有一部分流行的函数库(比如jQuery)符合AMD规范,更多的库并不符合。
 加载这样的库,需要先用require.config()方法定义它们的一些特性。在本文中提到的underscorebackbone这两个库,就没有采用AMD来编写,使用时我们必须先定义它们的特性:

require.config({
  shim: {
    'underscore': {
      exports: '_'
    },

    'backbone': {
      deps: ['underscore', 'jquery'],
      exports: 'Backbone'
    }
  }
})

require.config()接受一个配置对象,这个对象除了有前面说过的paths属性之外,还有一个shim属性,专门用来配置不兼容的模块

每个模块都至少要定义:

  1. exports值(输出的变量名),定义这个模块外部调用时的名称;
  2. deps数组,表明该模块的依赖列表。 当然,如果没有依赖可以省略。

如下,我们可以这样定义一个jquery的依赖模块

require.config({
  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
})

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

require(['text!review.txt', 'image!cat.jpg'], function(review, cat){
  console.log(review);
  document.body.appendChild(cat);
})

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容