浅谈JavaScript 模块化

参考资料

Modules/1.0——维基百科
CommonJS Modules/1.0——伯乐在线
js模块化——博客园
Javascript模块化编程系列——阮一峰
《ECMAScript 6 入门》——阮一峰

前言

本人菜鸟,入IT只为当鼓励师。本编文章意在简单总结一下 什么是模块化,模块化的优点, js模块化 的发展历史,关于 js模块化 的一些规范 等等。

一、什么是模块化

根据百度百科说法:模块化是指解决一个复杂问题时自顶向下逐层把系统划分成若干模块的过程,有多种属性,分别反映其内部特性。

晕了,这是什么嘛。

简单的说就是,我们实现一个应用时(不管是web、桌面还是移动端),通常都会按照不同的功能,分割成不同的模块来编写,编写完之后按照某种方式组装起来成为一个整体,最终实现整个系统的功能。

所以,如果一个团队一起做一个复杂的应用,肯定要分模块分工合作(一个人战斗不太现实)。这时,有很多需要注意的点就出现了:

  • 模块中定义的资源不应该污染全局环境,否则多人协作困难且容易出错。
  • 各个模块可独立工作,即便单组模块出现故障也不影响整个系统工作
  • 各模块不能全部预先加载,应该实现按需自动加载。确保每个模块高效运行,又能节约资源,提高效率。

C、C++、Java、PHP等等编程语言本身就拥有可以实现模块化的指令或方法,有了这些指令或方法,就可以把子功能写在另外的文件上,需要用到的时候直接引入即可。举下例子:

  • c使用 #include 包含.h文件
  • php中使用 require_once 包含.php文件
  • java使用 import 导入包

抛开C、C++、Java、PHP这些不说,就说前端领域,认真想想,其实 html css 也实现了模块化。

  • html 中的 <frame> <iframe> <frameset>(但好像不推荐使用)


  • css 中有 @import " /.css " 指令可以导入其他css

那 JavaScript 呢?带着疑问,下面会介绍js模块化的发展历程。(大神请无视)

二、模块化的优点

可维护性:

  • 多人协作互不干扰
  • 灵活架构,焦点分离
  • 方便模块间组合、分解 、解耦
  • 方便单个模块功能调试、升级

可测试性:

  • 可分单元测试

三、前端的模块化思想的发展

3.1 那年的诞生——1995

1995年,JavaScript正式发布,当时它只是作为一种客户端脚本语言,目的是 将 不涉及后端数据的、简单的 表单有效性验证 转移到客户端完成,减少客户端向服务端的请求数。那时的JavaScript只是服务端工程师在使用,他们或许只需在页面上随便写几句js代码就能满足需求。

if (xxx) {
  // ......
} else {
  // ......
}
element.onsubmit= function () {
  //......
}

代码可能像这样子,从上到下执行就行了,没有什么模块的规范。

3.2 模块萌芽

随着ajax的概念被提出,前端有了主动发起请求的能力,一些业务开始向客户端方向偏移。网站逐渐变成“互联网应用程序”,嵌入网页的Javascript代码越来越庞大,越来越复杂。于是,一些问题就暴漏出来了:

  • 依赖关系不好管理。如果一个文件需要依赖另外一些文件中定义的东西时,这个文件依赖的所有文件都要在它之前导入。过于复杂的系统,依赖关系可能出现相互交叉的情况,依赖关系的管理就更加难了。
    // 如果main.js中要用到gameBg.js中定义的属性、方法或者对象时

    // 正确,gameBg.js要在main.js之前导入
    <script src="scripts/views/gameBg.js" type="text/javascript">
    <script src="scripts/main.js" type="text/javascript">
    
    // 报错,cannot find xxx of undefined
    <script src="scripts/views/gameBg.js" type="text/javascript">
    <script src="scripts/main.js" type="text/javascript">
    
    // 如果js文件很多呢?
    
  • 全局环境的污染。
    我在a.js中定义了一个全局变量 var a = 0,相当于定义在window上。
    你在b.js中用了我定义的全局变量,给它赋值 a = 1
    我又在c.js中用了这个全局变量,但我不知道你在b.js中修改过a的值。于是 if (a==0) { // ...... }。(出事了!)

  • 命名冲突
    项目中通常会把一些通用的函数封装成一个文件。
    我定义了一个函数:function func ( // ...... ) { }
    你也想实现类似功能,于是:function func2 ( // ...... ) { }
    他又想实现类似功能,于是:function func3 ( // ...... ) { }
    要避免命名冲突,只能靠你我他之间的沟通协作。

如果放着这些问题不解决,团队的工作重点与关注点就不只是系统的业务逻辑,还包括队内的沟通,这会阻碍着项目进度。而且当人数一多时(几十人甚至上千人一起开发同一个项目),沟通就变得非常困难且低效了。

于是,前人创造了很多方法来避免这些问题,尽最大的努力实现模块化:

3.2.1 避免全局环境污染的方法

  • 只创建一个全局变量作为当前应用的容器,把其他变量、方法加到该命名空间下。
    var Myapp = {};
    Myapp.location = "login";
    Myapp.info = {
    name: "flappybird",
    creator: "Dong Nguyen"
    };
    Myapp.startGame = function () {
    // ......
    };

  • 将代码写在一个匿名函数内部
    ( function () {
    // 局部变量和方法
    var variable1 = "I'm a variable in part";
    var func1 = function () {
    // ......
    };
    // 全局变量和方法
    window.variable2 = "I'm a variable in global";
    window.func2 = function () {
    // ......
    };
    })();

  • jquery风格匿名函数
    ( function (window) {
    // 通过给window添加属性而暴漏到全局
    window.jQuery = window.$ = jQuery;

        // 定义全局对象jQuery($)的相关内容
    })(window);
    

jQuery的封装风格曾被很多框架模仿。
这种方式用到了匿名函数包装代码(即第二种方法)。多出的点是,所依赖的外部变量可以传给这个函数,在函数内部就可以使用这些依赖了,然后把模块自身暴漏给window。
如果需要添加扩展,则可以作为jQuery的插件,把它挂载到$上。例如:fullpage.js插件。
这种风格虽然灵活了些,但并未解决根本问题:所需依赖还是得外部提前提供、还是增加了全局变量。

3.2.2 避免命名冲突的方法

  • java风格的命名空间,用多级命名空间来进行管理。于是编写代码和调用代码就变得这么长了。
    Myapp.utils.func1 = xxx;
    Myapp.tools.func1 = xxx;
    Myapp.tools.another.func1 = xxx;

  • 设置变量名的控制权让渡函数。
    有时候我们可能不只用到一种函数库或插件,当用到多个函数库时,由于库并不是一个人编写的,全局变量的命名冲突不是总能避免。如:jquery.js库 和 Prototype.js库,它们都用了$符号作为全局变量。同时导入两个库肯定会产生影响。
    但是jquery提供了noConflict()方法,可以让渡变量名的控制权。
    // 将变量$的控制权让渡给prototype.js
    jQuery.noConflict();
    // 使用jQuery
    jQuery("h1").text("我是标题");

    // 自定义一个更短的命名
    var jq = jQuery.noConflict();       
    jq("p").text("我是段落");
    

3.2.3 完善依赖关系的管理

后面提到的 require.js、sea.js 等 可以解决这个问题,这个后续再说。

3.2.4 推荐

想了解更多实现模块化的方法,可以拜读一下峰哥的文章:
Javascript模块化编程(一):模块的写法

3.2.5 模块化问题

当人们觉得再这样下去写代码槽糕透了的时候,他们就想运用模块化的思想,写好一个模块,要用就导入,导入后毫不影响原先的代码。这样就引发很多需要思考的问题:

  • 怎样安全地包装一个模块的代码?
  • 怎样唯一地标识一个模块?
  • 怎样优雅地把模块的API暴漏出去?
  • 怎样方便地使用所依赖的模块?

四、服务端 js 的诞生

4.1 nodejs

2009年,nodejs诞生,我们可以用 js 编写服务端的代码了。
在浏览器环境下,没有模块也不是特别大的问题,毕竟网页程序的复杂性有限;但是在服务器端,一定要有模块,与操作系统和其他应用程序互动,否则根本没法编程。
于是,CommonJS 社区制定了 Modules/1.0 规范(现在已经被1.1取代)。nodejs 采用了该规范,故以下用 nodejs 作为例子。

4.2 Modules/1.0

总结起来,Modules/1.0规范指出:

  • 模块需要提供顶级作用域的私有性。
  • 提供从其他模板导入单例对象到自身的能力
  • 提供导出自身API的能力

Modules/1.0规范的内容如下:

4.2.1 模块上下文

  • 在模块中存在一个自由变量"require",它是一个函数。这个"require"函数:
    ① 接收参数为:一个模块标识符。
    var example = require('./example.js');
    ② 返回:外部模块输出的API。
    // 变量example即为外部模块example.js输出的内容
    ③ 如果出现依赖闭环(正常情况,加载main.js时,遇到 var a = require(./a.js); 则去加载a.js;加载a.js时,遇到 var b = require(./b.js); 则去加载b.js;加载b.js时,遇到 var a = require(./a.js); 则去加载a.js。无线循环,这就产生了依赖闭环的问题),为了避免这个问题,规定每个模块只会被加载执行一次
    // main.js
    console.log("main start");
    var a = require(./a.js);
    var b = require(./b.js);
    console.log("main end");

    // a.js
    console.log("a start");
    var b = require(./b.js);
    console.log("a end");
    
    // b.js
    console.log("b start");
    var a = require(./a.js);
    console.log("b end");  
    
    /* 输出结果为:
    main start
    a start
    b start
    b end
    a end
    */
    
程序执行顺序

④ 如果请求模块失败,require函数应抛出一个错误。

  • 模块中存在一个名为"exports"的自由变量,它是一个对象,模板可把自身API加到其中。
    // 暴露message变量
    exports.message = "hi";
    // 暴露hello方法
    exports.say= function () {
    console.log("hello!");
    };
  • 模块必须使用"exports"对象来作为输出的唯一表示

4.2.2 模块标识符

  • 模块标识符是一个以正斜杠分隔的多个”term”组成的字符串。
  • 一个term必须是一个 驼峰格式的标识符,.字符(表示当前目录) 或者 ..字符串(表示上一级目录)。
  • 模块标识符可以不加文件扩展名,比如”.js”。
    var a = require(./a);
    // 相当于 var a = require(./a.js);
  • 模块标识符可以是 相对的 或者 顶级的 (top-level)。如果一个模块标识符的第一个term是 .字符(表示当前目录)或者 ..字符串(表示上一级目录),那么它是 相对的
  • 顶级标识符是概念上的模块命名空间的根。
  • 相对标识符是相对于在其内部调用了 require() 的模块的标识符来进行解析的。

五、服务端的模块化在前端领域的应用

既然服务端出了模块化方案 Modules/1.0 ,那么是不是可以把这个规范直接用在客户端啊?
只可惜,不能。出于以下原因:

  • 资源的加载方式与服务端完全不同。
    ① 服务端 require 一个模块,是直接从 硬盘 或 内存 中读取的。可以同步加载完成,等待时间就是硬盘的读取时间,那速度是很快的。
    ② 客户端,浏览器需要从服务端下载资源,花费的是请求所花的时间,取决于网速的快慢。若要等很长时间,浏览器会处于"假死"状态。例如:
    // 第二行math.add(1, 1),在第一行require('math')之后运行,因此必须等math.js加载完成。
    // 如果加载时间很长,整个应用就会停在那里等。
    var math = require('./math.js');
    math.add(1, 1);
    因此,浏览器端的模块,不能采用 "同步加载"(Sync),只能采用 "异步加载"(Async)。这就是 AMD规范(后面提及)诞生的背景。
  • 若浏览器加载资源的方式外层没有 function 包裹,变量会暴漏在全局上;而全局污染这个问题在服务端编程不如浏览器要求严格。例如:
    // 变量math 和 math.js中定义在全局作用域上的变量、方法 都会污染到全局。
    var math = require('./math.js');

既然如此,问题要怎么解决?于是乎,就像党派斗争一样,分裂了三种解决方案。

5.1 Modules/1.x

这一派人的意见是:

  • 在现有基础上改进来满足浏览器端的需要(function包装不污染全局、异步加载)。所以,他们制定了 Modules/Transport规范,提出:先通过工具,把现有模块代码转化为浏览器上使用的模块代码,然后再使用的方案。

典型的工具有:browserify。Browserify 可以让你使用类似于 node 的 require() 的方式来组织浏览器端的 Javascript 代码,通过 预编译 让前端 Javascript 可以直接使用 Node NPM 安装的一些库。难懂,那就直接看它的例子吧:

browserify的简单用法

所以,若采用这一派的规范,我们就可以直接像服务端一样编写代码了,编写完后,只需要用工具把它编译成浏览器使用的代码即可。

5.2 Modules/2.0

这一派人的意见是:

  • Modules/1.0固然不适合浏览器,但它里面的一些理念还是很好的,如:通过 require 来声明依赖。新的规范应该兼容这些。
  • AMD规范(请看 5.3) 也有它好的地方,如:模块的预先加载、通过
    return 可暴漏任意类型的数据,而不像 commonjs 那样 exports 只能为
    object。故 其中的一些观点 也应采纳。
  • 最终他们制定了一个 Modules/Wrappings规范,此规范指出了一个模块应该如何"包装",包含以下内容:
    ① 全局有一个 module 变量,用来定义模块。
    ② 通过module.declare方法来定义一个模块。
    ③ module.declare方法 只接收一个参数,那就是模块的 factory,它可以是函数,也可以是对象(如果是对象,那么模块输出就是此对象)。
    ④ 模块的 factory函数 传入三个参数:require、exports、module,用来引入其他依赖和导出本模块API。
    ⑤ 如果 factory函数 最后明确写有return数据,那么 return 的内容即为模块的输出;不写 return 默认返回undefined。

CMD/seajs

seajs 的作者 是 国内大牛 淘宝前端步道者 玉伯。seajs 全面拥抱
Modules/Wrappings规范,不用 RequireJS 那样回调的方式来编写模块。

它的特色和用法以后再来补充。(待续)

5.3 Modules/Async

这一派人的意见是:

  • 浏览器与服务器环境差别太大,不能沿用旧的模块标准。

  • 既然浏览器必须异步加载代码,那么模块在定义的时候就必须 指明所依赖的模块,然后 把本模块的代码写在回调函数里。模块的加载也是通过 下载—>回调 这样的过程来进行,这个思想就是AMD的基础。
    // AMD也采用require()语句加载模块,但是不同于CommonJS,它要求两个参数
    // 第一个参数[module],是一个数组,里面的成员就是要加载的模块
    // 第二个参数callback,则是加载成功之后的回调函数
    require([module], callback);

    // math.add()与math模块加载不是同步的,浏览器不会发生假死。AMD比较适合浏览器环境。
    require(['math'], function (math) {
        math.add(2, 3);
    });
    
  • 由于与原规范不合,最终从 CommonJs 中分裂了出去,独立制定了浏览器端的js模块化规范 AMD(Asynchronous Module Definition)

  • 目前,主要有两个Javascript库实现了AMD规范:require.jscurl.js

AMD/RequireJs

这里主要介绍 RequireJs,若想了解其用法,可以看我的另一篇文章:AMD/RequireJS 使用入门

六、ES6模块化标准

既然模块化开发的呼声这么高,作为官方的ECMA必然要有所行动,js模块化很早就列入草案,终于在2015年6月份发布了ES6正式版。

ES6只要增加了 exportimportmodule 等命令。具体用法以后再补充。

想了解更多关于ES6的东西,推荐大家阅读《ECMAScript 6 入门》,这是这本书的 网上教程

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

推荐阅读更多精彩内容