Module Of Front End

Table of Contents

前端模块化

1995 年 5 月, Brendan Eich 只用了 10 天,就设计完成了 JavaScript 的第一版,so 对于 JavaScript 的规范化,不要要求太多,比如,它早期根本没有考虑模块化……

随着 web2.0 的发展, Ajax 技术得到广泛应用,各种前端库层出不穷, 前端代码日益膨胀 , JavaScript 极其简单的代码组织规范已经 hold 不住如此庞大规模的代码了。

下面一起来看一下, JavaScript 模块化规范的血泪历程!

模块化的基础

1. 函数封装

最开始的模块就是在一个文件里面编写几个相关的函数,如此用时加载函数所在文件,直接调用即可。

function fn1() {
    // ....
}

function fn2() {
    // ....
}

缺点 :污染了全局变量,容易引起命名冲突,而且模块成员之间没什么关系。

2. 对象封装

为了解决上述问题,可以把所有的模块成员封装在一个对象中。如下:

var moduleInObj = {
    var1: 'Tom',
    var2: 'Jerry',

    fn1: function() {
        // ....
    },
    fn2: function() {
        // ....
    }
}

如此,在需要模块时,引用包含模块的文件,通过 对象名.模块名 调用即可。

缺点 :外部可以随意修改内部成员,如 moduleInObj.var2 = 'Bach'moduleInObj.fn2 = function() { //....} ,不安全啊。

3. 立即执行函数封装

为了隐藏细节,可以使用立即执行函数,如下:

var moduleInIIEF = (function() {
    var var1 = 'Tom';
    var var2 = 'Jerry';

    function fn1() {
        // ....
    }

    function fn2() {
        // ....
    }

    // 只暴露想要暴露的变量或函数
    return {
        var1: var1,
        fn2: fn2
    }    
})

如此,在模块外部无法修改没有暴露出来的变量、函数。

总结

上述做法就是模块化的基础了,现在,让我们来看一下当前通行的 JavaScript 模块规范。

服务器端模块化

CommonJS

CommonJS 是 NodeJS 在服务端的模块化规范:

  • 一个单独的文件就是一个模块,每个模块都是一个独立的作用域;
  • 模块只有一个出口 – module.exports 对象,把模块需要暴露的内容放入该对象;
  • 加载模块使用 require 方法,该方法读取一个文件并执行,返回文件内部的 module.exports 对象。
// moduleInCommonJS.js

var var1 = 'Tom';
var var2 = 'Jerry';

function fn1() {
    // ....
}

function fn2() {
    // ....
}

// 通过 module.exports 对象暴露模块内容
module.exports = {
    fn1: fn1,
    fn2: fn2
}

如此,当需要使用模块功能时,如下操作:

// 使用 require 加载 moduleInCommonJS
var moudleInCommonJS = require('./moduleInCommonJS.js');

// 调用模块的函数
moduleInCommonJS.fn1();

注意,不同的实现对 require 时的路径有不同要求,通常可以省略 .js 扩展名,可以使用相对路径,也可以使用绝对路径。对于系统内置模块,甚至可以不写路径,直接使用模块名。

CommonJS 中的 require 加载是 同步的 ,模块系统需要 同步读取 模块文件的内容(服务端内的硬盘读写速度很快),并编译执行以得到模块接口。

然而,浏览器端就尴尬了 -_||

浏览器端模块化

浏览器端,加载 JavaScript 文件的方式,是在文档头部插入 <script> 标签,但是脚本标签天生 异步 ,传统的 CommonJS 模块在浏览器环境中就变成了 普通的 .js 文件 – 即没有避免全局变量污染,也没有解决依赖性。

那么,浏览器端的模块化,到底如何去规范呢??

解决思路之一:开发一个服务器端组件,对模块代码作静态分析,将 模块与它的依赖列表 一起返回给浏览器端。这需要服务器安装额外的组件,并因此要调整一系列底层架构。

解决思路之二:用一套标准模板来封装模块定义,但是如何定义,如何加载呢?

AMD·RequireJS

RequireJS

AMD(Asynchronous Module Definition)异步模块定义,是一个浏览器端模块化开发的规范,是 RequireJS 在推广过程中对模块定义的规范化产出。

注意, AMD 并不是 JavaScript 原生支持,使用 AMD 需要引入对应的库函数 – RequireJS 。

RequireJS 主要解决两个问题:模块的 异步加载依赖性

1. 定义模块

RequireJS 定义了一个全局函数 define ,用来定义模块,语法如下:

define(id?, dependencies?, factory);

其中:

  • id ,可选参数,用来定义模块的标识,缺省为脚本文件名;
  • dependencies ,是一个当前模块依赖的模块名称数组;
  • factory ,工厂方法,模块初始化要执行的 函数或对象 ,如果为函数,它应该只被执行一次;如果是对象,它应该为模块的输出值。

2. 加载模块

AMD 推崇 依赖前置 ,在定义模块的时候就要声明其依赖的模块。

在页面上使用 require 函数加载模块,语法如下:

require(dependencies?, callback);

其中, require() 函数接受两个参数:

  • dependencies ,表示所以来的模块数组;
  • callback ,是一个回调函数,当依赖的模块都加载成功后,被调用。加载的模块会以参数形式传入该函数,从而在回调函数内部就可以使用这些模块。

3. 示例代码

来看一个简单的 RequireJS 示例代码。

假设文件的目录架构,如下:

Proj
|
|-- js
|   |-- lib
|       |-- jquery.js
|       |-- require.js
|
|-- index.html
|-- main.js

index.html 头部引入 require.js 文件,如下:

<scrpit data-main="./main" src="js/lib/require.js" defer async></scrpit>

其中,当 require.js 加载的时候会检查 data-main 属性,以获取 入口脚本 (这里是 main.js )。

编写 main.js 即可,如下:

require.config({
    baseUrl: './js/',
    paths: {
        'jquery': 'lib/jquery'
    }
});

require(['jquery'], function($) {
    // jquery → $
    $(document).ready(() => {
        console.log('Hello World.');
    })
})

CMD·SeaJS

SeaJS

CMD(Common Module Definition)通用模块定义,是国内发展出来的浏览器端模块化开发规范,相似, CMD 是 SeaJS 在推广过程中对模块定义的规范化产出。

1. 定义模块

SeaJS 也是为了解决模块的依赖性,语法如下:

define(id?, deps?, factory)

其中, factory 接受三个参数 – function(require, exports, module) ,如下:

  • 第一个参数 require(id) 是一个方法,接受模块标识作为唯一此参数,用来获取其他模块提供的接口;
  • 第二个参数 exports 是一个对象,用来向外提供模块接口;
  • 第三个参数 module 是一个对象,上面存储了与当前模块相关联的一些属性和方法。

2. 加载模块

CMD 推崇 就近依赖 ,只有在用到某个模块的时候再去 require 加载。

3. 示例代码

来看一个简单的 SeaJS 示例代码:

// → hello.js
define(function(require, exports, module) {
    var $ = require('jquery');

    exports.sayHello = function() {
        $('#hello').toogle('slow');
    };
});

通过 SeaJS 来加载使用上面的模块,如下:

seajs.config({
    alias: {
        'jquery': 'http:/modules.seajs.org/jquery/1.7.2/jquery.js'
    }
});

seajs.use(['./hello', 'jquery'], function(hello, $) {
    $('#beautiful-sea').click(hello.sayHello);
});

4. AMD vs CMD

AMD 和 CMD 的模块加载都是异步的,最大的区别是对 依赖模块的执行时机 处理不同。

AMD 在加载模块完成后就会执行该模块,所有模块都加载执行完成后,进入 require 的回调函数执行主逻辑。如此,依赖模块的执行顺序和书写顺序便无法保证一致 – 谁下载快谁先执行 ,但是主逻辑一定在所有依赖加载完成后才执行。

CMD 加载完某个依赖模块后并不执行,只是下载而已,在所有依赖模块加载完成后进入主逻辑,遇到 require 语句的时候才执行对应的模块,这样模块的执行顺序和书写顺序是完全一致的。

一般来说, AMD 用户体验好,因为没延迟,依赖模块提前执行了; CMD 性能号,因为只有用户需要的时候才执行。

ES6 模块化1

ES6 模块的设计思想是尽量的 静态化 ,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。 CommondJS 和 AMD 模块,都只能在运行时确定这些东西。比如, CommonJS 模块就是对象,输入时必须查找对象属性。

1. ES6 模块的定义和加载

ES6 的模块化语法: export 命令用于规定模块的对外接口, import 命令用于输入其他模块提供的功能。如下:

// 定义模块 math.js
var basicNum = 0;
var add = function(a, b) {
    return a + b;
};

export {basicNum, add};

如需用到该模块,使用 import 引用即可,如下:

// 引用模块
import { basicNum, add } from './math';

function test(ele) {
    ele.textContent = add(99 + basicNum);
}

注意,使用 import 命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到 export default 命令,为模块指定默认输出。

// export-default.js
export default function() {
    console.log('Hello World');
}

如此,其他模块加载该模块的时候, import 命令可以为该匿名函数指定任意名字,如:

// import-default.js
import customName from './export-default';

customName();                   // → 'Hello World'

2. ES6 模块和 CommonJS 模块的差异

它们有两个重大差异:

  • CommonJS 模块输出的是一个 值的拷贝 ,ES6 模块输出的是 值的引用
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

来看一个代码示例:

// lib.js
export let conuter = 3;
export function incCounter() {
    counter++;
}

// 在 main.js 中加载 lib.js
import { counter, incCounter } from './lib';

console.log(counter);           // → 3

incCounter();
console.log(counter);           // → 4

可见,ES6 模块的运行机制与 CommonJS 不一样。ES6 模块是 动态引用 ,并不会缓存值,模块里面的变量绑定其所在的模块。

第二个差异是因为 CommonJS 加载的是一个对象(即 module.exports 属性),该对象只有在脚本运行完才会生成;而 ES6 模块不是对象,它的对外接口只是已经静态定义,在代码静态解析阶段就会生成。

3. 总结一下

  • CommonJS 规范主要用于服务端编程,加载模块是同步的,并不适合浏览器环境,因为同步意味着阻塞加载,浏览器资源是异步加载的,因此有了 AMD 、CMD 解决方案;
  • AMD 规范在浏览器环境中异步加载模块,而且可以并行加载多个模块。不过,AMD 规范开发成本高,代码的阅读和书写比较困难,模块定义方式的语义不顺畅;
  • CMD 规范与 AMD 规范很相似,都用于浏览器编程,依赖就近,延迟执行,可以很容易在 Node.js 中运行。但是依赖 SPM 打包,模块的加载逻辑偏重;
  • ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。

Footnotes:

Date: 2020-01-19 Sun 19:49

Author: Jack Liu

Created: 2020-02-08 Sat 21:26

Validate