# 前端模块化演进之路

有这样一个场景，客户端运行很久，但是法务部和数据部需要收集用户的一些信息，这些信息收集好之后需要进行相应的数据处理，之后上报到服务端。客户端提供一个纯粹的 JS 执行引擎，不需要 WebView 容器。iOS 端有成熟的 JavaScriptCore、Android 可以使用 V8 引擎。这样一个引擎配套有一个 SDK，访问 Native 的基础能力和数据运算能力，可以看成是一个阉割版的 Hybrid SDK 额外增加了一些数据处理能力。

问题结束了吗？处理逻辑的时候还需要用到2个库：[cheerio](https://github.com/cheeriojs/cheerio) 和 [sql](https://github.com/nearform/sql)。因为都是 Node 工程，所以纯粹的 JS 环境是没办法直接执行。所以需求就进行了转变 ———— 将 Node 项目打包成 UMD 规范。这样就可以在纯粹的 JS 环境下运行。接下来的文章就分析下各种规范。其实也就是前端模块化演进之路的几种规范。




## 前端模块化演进之路开发的价值

随着互联网的飞速发展，前端开发越来越复杂。本文将从实际项目中遇到的问题出发，讲述模块化能解决哪些问题，以及以 Sea.js 为例讲解如何进行前端的模块化开发。

1. 恼人的命名冲突
我们从一个简单的习惯出发。我做项目时，常常会将一些通用的、底层的功能抽象出来，独立成一个个函数，比如

```Javascript
function each(arr) {
  // 实现代码
}

function log(str) {
  // 实现代码
}
```
并像模像样的将这些代码抽取出来并统一到 `util.js` 中，在需要使用的地方引入该文件，看起来很棒，团队内的同事很感激我提供了这么便利的工具包。

直到团队越来越大，开始有人抱怨

> 小杨：我定义了一个 each 方法遍历对象，但是 util.js 中已经存在一个 each 方法，每次都需要改方法名，我只能叫 eachObject 方法。<br>张三：我定义了一个 log 方法，可是王武的代码出问题了，谁来看看？

抱怨越来越多，最后参照 Java 的方式，引入**命名空间**解决问题。于是  util.js 代码变成了

```Javascript
var org = {};
org.Utils = {};

org.Utils.each = function (arr) {
  // 实现代码
};

org.Utils.log = function (str) {
  // 实现代码
};
```

可能看上去的代码很 low，其实命名空间在前端领域的布道者是 Yahoo！的 YUI2 项目，看看下面的代码，是 Yahoo！的一个开源项目

```Javascript
if (org.cometd.Utils.isString(response)) {
  return org.cometd.JSON.fromJSON(response);
}
if (org.cometd.Utils.isArray(response)) {
  return response;
}
```

通过命名空间虽然可以极大的解决冲突问题，但是每次在调用一个方法时都需要写一大堆命名空间相关的代码，剥夺了编码乐趣。

另一种方式是一个自执行函数来实现。

```Javascript
(function (args) {
  //...
})(this);
```

2. 繁琐的文件依赖

继续上述场景，很多情况下都需要开发 UI 层通用组件，这样项目组就不需要重复造轮子。其中有一个高频使用的组件就是 dialog.js 
```Javascript
<script src="util.js"></script>
<script src="dialog.js"></script>
<script>
  org.Dialog.init({ /* 传入配置 */ });
</script>
```

虽然公共组做项目都会编写使用文档、发送邮件告知全员（项目地址、使用方式等），但是还是有人问「为什么 dialog.js 有问题」，最后排查的结果基本都是没有引入 util.js 

```Javascript
<script src="dialog.js"></script>
<script>
  org.Dialog.init({ /* 传入配置 */ });
</script>
```

**命名冲突**和**文件依赖**是前端开发中2个经典问题，经过开发者不断的思考和研究，诞生了模块化的解决方案，以 CMD 为例

```Javascript
define(function(require, exports) {
    exports.each = function (array) {
        // ...
    };
    exports.log = function(message) {
        // ...    
    };
});
```

通过 exports 就可以向外提供接口， dialog.js 代码变成

```Javascript
define(function(require, exports) {
    var util = require('./util.js')

    exports.init = function () {
        // ...
    };
});
```

使用的时候可以通过 `require('./util.js')` 获取到 util.js 中通过 exports 暴露的接口。 require 的方式在其他很多语言中都有解决方案：include、




## 模块化的好处

1. 模块的版本管理：通过别名等配置，配合构建工具，可以轻松实现模块的版本管理
2. 提高可维护性： 模块化可以实现每个文件的职责单一，非常有利于代码的维护。
3. 前端性能优化： 对于前端开发来说，异步加载模块对于页面性能非常有益。
4. 跨环境共享模块： CMD 模块定义规范与 NodeJS 的模块规范非常相近，所以通过 Sea.JS 的 NodeJS 版本，可以方便的实现模块的跨服务器和浏览器共享。


## CommonJS 规范

CommonJS 是服务器端模块的规范。NodeJS 采用了这个规范。CommonJS 加载模块是同步的，所以只有加载完成后才能执行后面的操作。
因为服务器的特点，加载的模块文件一般都存在在本地硬盘，所以加载起来比较快，不用考虑异步的方式。

CommonJS 模块化的饿规范中，每个文件都是一个模块，拥有独立的作用域、变量、以及方法等，对其他模块不可见。 CommonJS 规范规定，每个模块内部， **module** 变量表示当前模块，它是一个对象，它的 **exports** 属性是对外的接口，加载某个模块，其实是加载该模块的 module.exports 属性，require 方法用于加载模块。


```Javascript
// Person.js
function Person () {
    this.eat = function () {
        console.log('eat something')
    }

    this.sleep = function () {
        console.log('sleep')
    }
}

var person = new Person();
exports.person = person;
exports.name = name;

// index.js
let person = require('./Person').person;
person.eat()
```

### CommonJS 与 ES6 模块的差异

1. CommonJS 模块输出的是**值的拷贝**，ES6 模块输出的是**值的引用**
2. CommonJS 模块是运行时加载，ES6 模块是编译时输出接口

CommonJS 模块导出的是一个对象（module.exports 属性），该对象只在脚本运行完才会生成。
ES6 的模块机制是 JS 引擎对脚本进行静态分析的时候，遇到模块加载命令 import，就会生成一个只读引用，等到脚本真正执行时，再根据这个只读引用到被加载的模块中取值，




## AMD 规范

AMD（Asynchronous Module Definition） 是在 Require.JS 推广的过程中对模块定义的规范化产出。AMD 推崇依赖前置。它是 CommonJS 模块化规范的超集，作用在浏览器上。它的特点是异步，利用了浏览器的并发能力，让模块的依赖阻塞变少。

AMD 的 API
```Javascript
define(id?, dependencyies?, factory);
```

id 是模块的名字，是可选参数。 dependencies 指定了该模块所依赖的模块列表，是一个数组，也是可选参数。每个依赖的模块的输出都将作为参数依次传入 factory 中。

```Javascript
require([module], callback)
```

**AMD 规范允许输出模块兼容 CommonJS 规范，这时 define 方法如下**

```Javascript
define(['module1', 'module2'], function(module1, module2) {
  function foo () {
    // ...
  }
  return { foo: foo };
});
```

```Javascript
define(function(require, exports, module) {
  var requestedModule1 = require('./module1')
  var requestedModule2 = require('./module2')

  function foo () {
    // ...
  }
  return { foo: foo };
});
```

优点： 适合在浏览器环境中加载模块，可以实现并行加载多个模块
缺点： 提高了开发成本，并不能按需加载，而是提前加载所有的依赖




## CMD 规范

CMD 是 Sea.JS 推广的过程中对模块定义的规范化产出。CMD 推崇依赖就近。
CMD 规范尽量保持简单，并与 CommonJS 规范中的 Module 保持兼容，通过 CMD 规范编写的模块，可以在 NodeJS 中运行。

[CMD 模块定义规范 ](https://github.com/seajs/seajs/issues/242)

CMD 中 require 依赖的描述用数组，则是异步加载，如果是单个依赖使用字符串，则是同步加载。

AMD 是 RequireJS 在推广过程中对模块定义的规范化产出，CMD是SeaJS 在推广过程中被广泛认知。SeaJS 出自国内蚂蚁金服玉伯。二者的区别，玉伯在12年如是说：

RequireJS 和 SeaJS 都是很不错的模块加载器，两者区别如下：

1. 两者定位有差异。RequireJS 想成为浏览器端的模块加载器，同时也想成为 Rhino / Node 等环境的模块加载器。SeaJS 则专注于 Web 浏览器端，同时通过 Node 扩展的方式可以很方便跑在 Node 服务器端

2. 两者遵循的标准有差异。RequireJS 遵循的是 AMD（异步模块定义）规范，SeaJS 遵循的是 CMD （通用模块定义）规范。规范的不同，导致了两者API 的不同。SeaJS 更简洁优雅，更贴近 CommonJS Modules/1.1 和 Node Modules 规范。

3. 两者社区理念有差异。RequireJS 在尝试让第三方类库修改自身来支持 RequireJS，目前只有少数社区采纳。SeaJS 不强推，而采用自主封装的方式来“海纳百川”，目前已有较成熟的封装策略。

4. 两者代码质量有差异。RequireJS 是没有明显的 bug，SeaJS 是明显没有 bug。

5. 两者对调试等的支持有差异。SeaJS 通过插件，可以实现 Fiddler 中自动映射的功能，还可以实现自动 combo 等功能，非常方便便捷。RequireJS无这方面的支持。

6. 两者的插件机制有差异。RequireJS 采取的是在源码中预留接口的形式，源码中留有为插件而写的代码。SeaJS 采取的插件机制则与 Node 的方式一致开放自身，让插件开发者可直接访问或修改，从而非常灵活，可以实现各种类型的插件。




## UMD 规范

UMD（Universal Module Definition）是随着大前端的趋势产生，希望提供一个前后端跨平台的解决方案（支持 AMD、CMD、CommonJS 模块方式）。

实现原理：
1. 先判断是否支持 Node.js 模块格式（exports 是否存在），存在则使用 Node.js 模块格式
2. 再判断是否支持 AMD 模块格式（define 是否存在），存在则使用 AMD 模块格式
3. 前2个都不存在则将模块公开到全局（window 或 global）

```Javascript
// if the module has no dependencies, the above pattern can be simplified to
(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD. Register as an anonymous module.
        define([], factory);
    } else if (typeof exports === 'object') {
        // Node. Does not work with strict CommonJS, but
        // only CommonJS-like environments that support module.exports,
        // like Node.
        module.exports = factory();
    } else {
        // Browser globals (root is window)
        root.returnExports = factory();
  }
}(this, function () {

    // Just return a value to define the module export.
    // This example returns an object, but the module
    // can return a function as the exported value.
    return {};
}));
```

可能有些人就要问了，为什么在上面的判断中写了 AMD，怎么没有 CMD？ 😂 因为前端构建工具 Webpack 不可识别 CMD 规范，使用 CMD 就需要引用工具，比如 Sea.JS

讲道理，如果想判断 CMD，那 UMD 代码如何写？

```Javascript
(function(root, factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD. Register as an anonymous module.
        define([], factory);
    } else if (typeof define === 'function' && define.cmd) {
        // CMD
        define(function(require, exports, module) {
            module.exports = factory()
        })
    } else if (typeof exports === 'object') {
        // Node. Does not work with strict CommonJS, but
        // only CommonJS-like environments that support module.exports,
        // like Node.
        module.exports = factory();
    } else {
        // Browser globals (root is window)
        root.returnExports = factory();
    }
}(this, function() {
    // Just return a value to define the module export.
    // This example returns an object, but the module
    // can return a function as the exported value.
    return {};
}))
```

![模块化](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-02-JSModule.png)



## 回到正题

Cheerio 如何打包到普通的 JS 执行环境中。

Webpack 支持的模块化参数如下图所示：
![Webpack 模块化参数](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-02-WebpackModuleOptions.png)

借助 Webpack 可以方便的打出一个 umd 规范的包。

```Javascript
module.exports = {
    entry: './src/cheerio.js',
    output: {
        filename: 'cheerio.js',
        // export to AMD, CommonJS, or window
        libraryTarget: 'umd',
        // the name exported to window
        library: 'cheerio',
        globalObject: 'this'
  }
}
```




## 总结

手机端（无论 iOS 还是 Android）的底层渲染内核都是类 Chrome v8 引擎。v8 引擎在执行 JS 代码时，是将代码先以 MacroAssembler 汇编库在内存中先编译成机器码再送往 CPU 执行的，并不是像其它 JS 引擎那样解析一行执行一行。所以，静态加载的 ES6 模块规范，更有助于 v8 引擎发挥价值。而运行时加载的 CommonJS、AMD、CMD 规范等，均不利于 v8 引擎施展拳脚。

在 NodeJS 开发项目中，Node9 已经支持 ES6 语法，完全可以使用 ES6 模块规范。NodeJS 的诞生，本身就基于 Google 的 v8 引擎，没有理由不考虑发挥 v8 的最大潜能。

在浏览器 JS 开发项目中，因为从服务器加载文件需要时间，使用 CommonJS 规范肯定是不合适了。至于是使用原生的 ES 模块规范，还是使用 Sea.js，要看具体场景。如果想页面尽快加载，Sea.js 适合；如果是单页面网站，适合使用原生的 ES6 模块规范。还有一点，浏览器并非只有 Chrome 一家，对于没有使用 v8 引擎的浏览器，使用 ES6 原生规范的优势就又减少了一点。