2.客户端的模块化历程

一个足够复杂的工程,需要尽量将功能解耦。

什么叫解耦?简单来说,需要将不同的功能分开到不同的文件中,或不同的目录结构中,形成一个个模块,模块之间通过有限的接口交互,模块内部的数据变化对外部隐藏。

在node上,这一点表现的非常不错。node实现了CommonJS规范,每个JS文件就是一个模块,模块中的所有数据对外隐藏,仅通过Module.exports暴露模块内的内容。

于是,模块化开发在node环境中已不成问题。

然而,JS语言可以运行的不仅仅是node环境,它还有一个非常非常重要,也很常见的运行环境——客户端浏览器

在过去,JS在客户端中要做的事情十分简单,大体上是以下内容:

  1. 动画效果
  2. 表单验证
  3. 少量的ajax请求

但随着前端环境的变化,前后端分离开发越来越流行,截至目前为止,绝大部分互联网公司的后端开发人员,他们要做的事情,仅仅是提供数据接口(REST API),剩下所有与用户打交道的事情,都要交给前端完成。

这样一来,前端开发的复杂度陡然提升,现在,前端开发人员需要完成:

  1. 动画效果
  2. 表单验证
  3. 大量的ajax请求
  4. 整个网站的页面构建
  5. 客户端数据缓存和优化
  6. 客户端各种数据逻辑处理,并且对应到页面的变化

更可怕的是,随着单页应用程序的流行,前端开发人员需要在一个页面中,根据各种情况,组装不同的页面结构,同时还要保证数据之间尽量独立,不相互影响,这无疑是一个浩大的工程。

单页应用程序:整个网站只有一个页面,用户在整个浏览过程中的任何操作,都不会导致页面刷新。单页应用的用户体验非常好,用户就好像在使用一个本地程序一样。

上面这些情况,会直接导致一个结果,就是JS代码数量剧增,和过去的相比,完全不是一个数量级。

然而,JS代码量的剧增,给前端开发带来了前所未有的压力。在浏览器一侧,我们并没有像node环境那样的模块化开发能力,于是,不可避免会遇到下列问题:

  1. 如何将代码分离?
  2. 代码分离后,浏览器引用js的时候,如何处理它们之间的依赖关系?
  3. 代码分离后,浏览器引用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的不同之处:

  1. 定位有差异。RequireJS 想成为浏览器端的模块加载器,同时也想成为 Rhino / Node 等环境的模块加载器。Sea.js 则专注于 Web 浏览器端,同时通过 Node 扩展的方式可以很方便跑在 Node 环境中。
  2. 遵循的规范不同。RequireJS 遵循 AMD(异步模块定义)规范,Sea.js 遵循 CMD (通用模块定义)规范。规范的不同,导致了两者 API 不同。Sea.js 更贴近 CommonJS Modules/1.1 和 Node Modules 规范。
  3. 推广理念有差异。RequireJS 在尝试让第三方类库修改自身来支持 RequireJS,目前只有少数社区采纳。Sea.js 不强推,采用自主封装的方式来“海纳百川”,目前已有较成熟的封装策略。
  4. 对开发调试的支持有差异。Sea.js 非常关注代码的开发调试,有 nocache、debug 等用于调试的插件。RequireJS 无这方面的明显支持。
  5. 插件机制不同。RequireJS 采取的是在源码中预留接口的形式,插件类型比较单一。Sea.js 采取的是通用事件机制,插件类型更丰富。

总结

无论是CommonJS,还是AMD,或者是CMD,它们都是为了实现模块化开发而制定的各种规范。

其中,CommonJS是在node环境中最常见的规范,而AMD和CMD则主要着力于客户端的模块化。

然而,这一切都在慢慢的成为历史,当ECMAScript6出世之后,模块化的规范,终于有了官方的标准。

ES6的模块化

在ES6中,提出了标准的模块化编程规范,目前,绝大多数支持ES6的浏览器,都已支持该规范。

它的大致规则如下:

  1. 一个JS文件,即一个模块
  2. 模块中所有的数据,除非导出,否则都是私有数据,仅能在模块中访问
  3. 使用export关键字导出模块
  4. 使用import关键字导入模块,导入时使用解构表达式得到导出的数据,否则,将使用导出的默认数据
  5. 一个模块可以有多个导出
  6. 导入的模块会被缓存
  7. 在页面中加载入口模块使用<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来运行某个用于启动的模块,在浏览器中如何操作呢?

猜你喜欢

转载自blog.csdn.net/weixin_56492685/article/details/115236873