一个足够复杂的工程,需要尽量将功能解耦。
什么叫解耦?简单来说,需要将不同的功能分开到不同的文件中,或不同的目录结构中,形成一个个模块,模块之间通过有限的接口交互,模块内部的数据变化对外部隐藏。
在node上,这一点表现的非常不错。node实现了CommonJS规范,每个JS文件就是一个模块,模块中的所有数据对外隐藏,仅通过Module.exports
暴露模块内的内容。
于是,模块化开发在node环境中已不成问题。
然而,JS语言可以运行的不仅仅是node环境,它还有一个非常非常重要,也很常见的运行环境——客户端浏览器。
在过去,JS在客户端中要做的事情十分简单,大体上是以下内容:
- 动画效果
- 表单验证
- 少量的ajax请求
但随着前端环境的变化,前后端分离开发越来越流行,截至目前为止,绝大部分互联网公司的后端开发人员,他们要做的事情,仅仅是提供数据接口(REST API),剩下所有与用户打交道的事情,都要交给前端完成。
这样一来,前端开发的复杂度陡然提升,现在,前端开发人员需要完成:
- 动画效果
- 表单验证
- 大量的ajax请求
- 整个网站的页面构建
- 客户端数据缓存和优化
- 客户端各种数据逻辑处理,并且对应到页面的变化
更可怕的是,随着单页应用程序的流行,前端开发人员需要在一个页面中,根据各种情况,组装不同的页面结构,同时还要保证数据之间尽量独立,不相互影响,这无疑是一个浩大的工程。
单页应用程序:整个网站只有一个页面,用户在整个浏览过程中的任何操作,都不会导致页面刷新。单页应用的用户体验非常好,用户就好像在使用一个本地程序一样。
上面这些情况,会直接导致一个结果,就是JS代码数量剧增,和过去的相比,完全不是一个数量级。
然而,JS代码量的剧增,给前端开发带来了前所未有的压力。在浏览器一侧,我们并没有像node环境那样的模块化开发能力,于是,不可避免会遇到下列问题:
- 如何将代码分离?
- 代码分离后,浏览器引用js的时候,如何处理它们之间的依赖关系?
- 代码分离后,浏览器引用js会导致额外的请求发生,如何解决效率问题?
只有将这些问题解决后,才能在客户端创建丰富、灵活、稳健的代码结构。
第三方模块化规范
我们首先遇到的第一个问题,就是如何在浏览器一侧,解决代码分离和依赖关系的问题。
CommonJS
尽管CommonJS是一个不错的规范,但是如果要在浏览器一侧去实现它,会遇到一些困难。
比如:
var math = require('math'); //导入math模块math.add(2,3); // 5
这段代码如果是在node环境中,没有任何问题,因为在node环境里,是可以读取文件的,require()
函数会去读取文件,得到模块的js代码并且运行。
但是在浏览器环境中,是没有办法读取文件的,在浏览器中,如果真的要实现CommonJS规范,就必须让require()
函数去发送一个网络请求,以得到js代码。这个过程会非常耗时,并且在那个年代,还没有异步(Promise),因此,CommonJS规范在浏览器一侧实现起来困难丛丛。
AMD规范
CommonJS是主要为了JS在后端(node环境)的表现制定的,他是不适合前端的,AMD(异步模块定义)出现了,它就主要为前端JS的表现制定规范。
AMD是”Asynchronous Module Definition”的缩写,意思就是”异步模块定义”。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
AMD也采用require()语句加载模块,但是不同于CommonJS,它要求两个参数:
require([module], callback);
第一个参数[module],是一个数组,里面的成员就是要加载的模块;第二个参数callback,则是加载成功之后的回调函数。如果将前面的代码改写成AMD形式,就是下面这样:
require(['math'], function (math) {
math.add(2, 3);
});
math.add()与math模块加载不是同步的,浏览器不会发生假死。所以很显然,AMD比较适合浏览器环境。目前,主要有两个Javascript库实现了AMD规范:require.js和curl.js。
requirejs教程: http://www.requirejs.cn/
CMD规范
前端大牛玉伯(http://caibaojian.com/)写了seajs,就是遵循他提出的CMD规范,与AMD蛮相近的,不过用起来感觉更加方便些,最重要的是中文版(http://seajs.org/docs/#docs)
seajs看起来像这样:
define(function(require,exports,module){ //...});
CMD和AMD的不同之处:
- 定位有差异。RequireJS 想成为浏览器端的模块加载器,同时也想成为 Rhino / Node 等环境的模块加载器。Sea.js 则专注于 Web 浏览器端,同时通过 Node 扩展的方式可以很方便跑在 Node 环境中。
- 遵循的规范不同。RequireJS 遵循 AMD(异步模块定义)规范,Sea.js 遵循 CMD (通用模块定义)规范。规范的不同,导致了两者 API 不同。Sea.js 更贴近 CommonJS Modules/1.1 和 Node Modules 规范。
- 推广理念有差异。RequireJS 在尝试让第三方类库修改自身来支持 RequireJS,目前只有少数社区采纳。Sea.js 不强推,采用自主封装的方式来“海纳百川”,目前已有较成熟的封装策略。
- 对开发调试的支持有差异。Sea.js 非常关注代码的开发调试,有 nocache、debug 等用于调试的插件。RequireJS 无这方面的明显支持。
- 插件机制不同。RequireJS 采取的是在源码中预留接口的形式,插件类型比较单一。Sea.js 采取的是通用事件机制,插件类型更丰富。
总结
无论是CommonJS,还是AMD,或者是CMD,它们都是为了实现模块化开发而制定的各种规范。
其中,CommonJS是在node环境中最常见的规范,而AMD和CMD则主要着力于客户端的模块化。
然而,这一切都在慢慢的成为历史,当ECMAScript6出世之后,模块化的规范,终于有了官方的标准。
ES6的模块化
在ES6中,提出了标准的模块化编程规范,目前,绝大多数支持ES6的浏览器,都已支持该规范。
它的大致规则如下:
- 一个JS文件,即一个模块
- 模块中所有的数据,除非导出,否则都是私有数据,仅能在模块中访问
- 使用export关键字导出模块
- 使用import关键字导入模块,导入时使用解构表达式得到导出的数据,否则,将使用导出的默认数据
- 一个模块可以有多个导出
- 导入的模块会被缓存
- 在页面中加载入口模块使用
<script type="module" src="入口模块">
遗憾的是,目前node的原生环境还暂时不支持ES6的模块化规范,但官方已经承诺将来会支持。
模块导出
你可以使用 export 关键字将已发布代码部分公开给其他模块。最简单方法就是将 export
放置在任意变量、函数或类声明之前,从模块中将它们公开出去,就像这样:
//mymodule.js// 导出数据export var color = "red";
export let name = "Nicholas";
export const magicNumber = 7;// 导出函数export function sum(num1, num2) { return num1 + num2;
}// 导出类export class Rectangle {
constructor(length, width) { this.length = length; this.width = width;
}
}// 此函数为模块私有function subtract(num1, num2) { return num1 - num2;
}// 定义一个函数……function multiply(num1, num2) { return num1 * num2;
}// ……稍后将其导出export {
multiply
};
上述代码,最终导出的内容是:
{
color: "red",
name: "Nicholas",
magicNumber: 7,
sum: function,
Rectangle: class,
multiply: function
}
值得注意的是,export
关键字必须放到声明语句前,而不能直接导出某个变量,比如export multiply
是错误的,如果要导出已有的变量,需要用对象字面量包装一下:export {multiply}
模块导入
一旦你有了包含导出的模块,就能在其他模块内使用 import 关键字来访问已被导出的功
能。 import 语句有两个部分,一是需要导入的标识符,二是需导入的标识符的来源模块。
此处是导入语句的基本形式:
//usemodule.jsimport { color, name, multiply } from "./mymodule.js";
这样,就从之前的模块中导入了部分数据。
你也可以用下面的方式导入模块中的所有东西:
//usemodule.jsimport * as all from "./mymodule.js"; // all可以是任意的名称//导入后,all是一个对象,包含模块中导出的所有内容
export 与 import 都有一个重要的限制,那就是它们必须被用在其他语句或表达式的外部。例如,以下代码有语法错误:
if (flag) { export flag; // 语法错误 } function tryImport() { import flag from "./example.js"; // 语法错误 }
导入导出默认值
在ES6中,为了简化导入导出语法,还提供了一种导入导出的方式——默认值,使用这种方式导入导出会更加简单,但要注意的是,每个模块只能导出一个默认值
导出默认值:
export default function(num1, num2) { return num1 + num2;
}
上面的代码类似于CommonJS中的module.exports:
//example.jsmodule.exports = function(num1, num2) { return num1 + num2;
}
在使用默认导出时,可以直接导出某个变量:
var color = "red";
export color; //错误,普通的导出语句,必须放到声明语句前,或对象字面量前export default color; //正确,默认导出可以使用这种方式
导入默认值:
你可以使用如下语法来从一个模块中导入默认值:
import sum from "./example.js";
这个导入语句从 example.js 模块导入了其默认值。注意此处并未使用花括号,与之前在非
默认的导入中看到的不同。本地名称 sum 被用于代表目标模块所默认导出的函数。这种语法
是最简洁的,而 ES6 的标准制定者也期待它成为在网络上进行导入的主要形式,这样你就能
导入已存在的对象。
对于既导出了默认值、又导出了一个或更多非默认的绑定的模块,你可以使用单个语句来导
入它的所有导出绑定。例如,假设你有这么一个模块:
//example.jsexport let color = "red";
export default function(num1, num2) { return num1 + num2;
}
你可以像下面这样使用 import 语句,来同时导入 color 以及作为默认值的函数:
import sum, { color } from "./example.js";console.log(sum(1, 2)); // 3console.log(color); // "red"
在浏览器中使用入口模块
在nodejs中,我们可以使用node app.js
来运行某个用于启动的模块,在浏览器中如何操作呢?