前端主要的模块化加载方案

378 阅读8分钟

前端主要的模块化加载方案

  • ES Module

    ES6 规范中添加,ES6 模块化有两个重要特性:一个是值引用,另一个是静态声明

    值引用:

    值引用指的是通过 export 导出的值,与其对应的值存在引用关系,即通过该值,可以取到模块内部实时的值。

    // a.js
    export var a = '';
    setTimeout(() => (a = 'a'), 500);
    // b.js
    import { a } from './a.js';
    console.log(a); // ''
    setTimeout(() => console.log(a), 1000); // 'a'
    

    模块 a 导出变量 a,初始值为空字符串,500 毫秒后赋值为字符串 'a';模块 b 引用模块 a 并打印,控制台输出空字符串,1 秒后继续打印,控制台输出字符串 'a'。

    静态声明:

    ES6 模块对于引用声明有严格的要求,首先必须在文件的首部,不允许使用变量或表达式,不允许被嵌入到其他语句中。所以下面 3 种引用模块方式都会报错。

    // 必须首部声明
    let a = 1
    import { app } from './app';
    
    // 不允许使用变量或表达式
    import { 'a' + 'p' + 'p' } from './app';
    
    // 不允许被嵌入语句逻辑
    if (moduleName === 'app') {
      import { init } from './app';
    } else {
      import { init } from './bpp';
    }
    

    定义这些严格的要求可不仅仅是为了代码的可读性,更重要的是可以对代码进行静态分析。

    静态分析是指不需要执行代码,只从字面量上对代码进行分析。例如,在上面的错误代码中,有一段代码需要通过判断变量 moduleName 的值来加载对应的模块,这就意味着需要执行代码之后才能判断加载哪个模块,而 ES6 模块则不需要。这样做的好处是方便优化代码体积,比如通过 Tree-shaking 操作消除模块中没有被引用或者执行结果不会被用到的无用代码。

  • CommonJs

    CommonJS 最初名为 Server.js,是为浏览器之外的 JavaScript 运行环境提供的模块规范,最终被 Node.js 采用。

    CommonJS 规定每个文件就是一个模块,有独立的作用域。每个模块内部,都有一个 module 对象,代表当前模块。通过它来导出 API,它有以下属性:

    - modules.id 模块的识别符,通常是带有绝对路径的模块文件名;
    - modules.filename 模块的文件名,带有绝对路径;
    - modules.loaded 返回一个布尔值,表示模块是否已经完成加载;
    - modules.parent 返回一个对象,表示调用该模块的模块;
    - modules.children 返回一个数组,表示该模块要用到的其他模块;
    - modules.exports 表示模块对外输出的值。
    

    引用模块则需要通过 require 函数,它的基本功能是,读入并执行一个 JavaScript 文件,然后返回该模块的 exports 对象。

    特性:

    CommonJS 特性和 ES6 恰恰相反,它采用的是值拷贝动态声明。值拷贝和值引用相反,一旦输出一个值,模块内部的变化就影响不到这个值了。 仍然使用上面的例子,改写成 CommonJS 模块,在 Node.js 端运行,控制台会打印两个空字符串。

    // a.js
    var a = '';
    setTimeout(() => (a = 'a'), 500);
    module.exports = a;
    
    // b.js
    var a = require('./a.js');
    console.log(a); // ''
    setTimeout(() => console.log(a), 1000); // ''
    

    动态声明就很好理解了,就是消除了静态声明的限制,可以“自由”地在表达式语句中引用模块。

  • AMD (Async Module Definition)

    代表作requireJs,个人简易 Demo

    AMD 标准规范主要包含了以下几个内容:
    
    1. 模块的标识遵循 CommonJS Module Identifiers。
    2. 定义全局函数 define(id, dependencies, factory),用于定义模块。dependencies 为依赖的模块数组,在 factory 中需传入形参与之一一对应。
    3. 如果 dependencies 的值中有 require、exportsmodule,则与 CommonJS 中的实现保持一致。
    4. 如果 dependencies 省略不写,则默认为 ['require', 'exports', 'module'],factory 中也会默认传入三者。
    5. 如果 factory 为函数,模块可以通过以下三种方式对外暴漏 API:return 任意类型;exports.XModule = XModule、module.exports = XModule。
    6. 如果 factory 为对象,则该对象即为模块的导出值。
    

    其中第三、四两点,即所谓的 Modules/Wrappings,是因为 AMD 社区对于要写一堆回调这种做法颇有微辞,最后 RequireJS 团队妥协,搞出这么个部分兼容支持。 因为 AMD 符合在浏览器端开发的习惯方式,也是第一个支持浏览器端的 JavaScript 模块化解决方案,RequireJS 迅速被广大开发者所接受。 但有 CommonJS 珠玉在前,很多开发者对于要写很多回调的方式颇有微词。在呼吁高涨声中,RequireJS 团队最终妥协,搞出个 Simplified CommonJS wrapping(简称 CJS)的兼容方式,即上文的第三、四两点。但由于背后实际还是 AMD,所以只是写法上做了兼容,实际上并没有真正做到 CommonJS 的延迟加载。 与 CommonJS 规范有众多实现不同的是,AMD 只专注于 JavaScript 语言,且实现并不多,目前只有 RequireJS 和 Dojo Toolkit,其中后者已经停止维护。

    特性:

    它的重要特性就是异步加载。所谓异步加载,就是指同时并发加载所依赖的模块,当所有依赖模块都加载完成之后,再执行当前模块的回调函数。这种加载方式和浏览器环境的性能需求刚好吻合。

    由于 AMD 并不是浏览器原生支持的模块规范,所以需要借助第三方库来实现,其中最有名的就是 RequireJS。它的核心是两个全局函数 define 和 require:

    • define 函数可以将依赖注入队列中,并将回调函数定义成模块;
    • require 函数主要作用是创建 script 标签请求对应的模块
  • CMD (Common Module Definition)

    CMD(Common Module Definition,通用模块定义)是基于浏览器环境制定的模块规范。 代表作品为 alibaba 前端玉伯(王保平)编写的 SeaJs

    定义和引用 CMD 定义模块也是通过一个全局函数 define 来实现的,但只有一个参数,该参数既可以是函数也可以是对象:

    define(factory);
    

    如果这个参数是对象,那么模块导出的就是对象;如果这个参数为函数,那么这个函数会被传入 3 个参数 require 、 exports 和 module。

    define(function (require, exports, module) {
      //...
    });
    

    define 参数:

    • require: 一个函数,通过调用它可以引用其他模块,也可以调用 require.async 函数来异步调用模块
    • exports: 一个对象,当定义模块的时候,需要通过向参数 exports 添加属性来导出模块 API
    • module: 一个对象,它包含 3 个属性:
      • uri,模块完整的 URI 路径;
      • dependencies,模块的依赖;
      • exports,模块需要被导出的 API,作用同第二个参数 exports。

    example:

    定义了一个名为 increment 的模块,引用了 math 模块的 add 函数,经过封装后导出成 increment 函数。

    define(function (require, exports, module) {
      var add = require('math').add;
      exports.increment = function (val) {
        return add(val, 1);
      };
      module.id = 'increment';
    });
    

    特性:

    CMD 最大的特点就是懒加载,和上面示例代码一样,不需要在定义模块的时候声明依赖,可以在模块执行时动态加载依赖。当然还有一点不同,那就是 CMD 同时支持同步加载模块和异步加载模块。 用一句话来形容就是,它整合了 CommonJS 和 AMD 规范的特点。遵循 CMD 规范的代表开源项目是 sea.js

  • UMD (Universal Module Definition)

    本质上并不是一个真正的模块化方案,而是将 CommonJS 和 AMD 相结合,UMD(Universal Module Definition,统一模块定义)其实并不是模块管理规范,而是带有前后端同构思想的模块封装工具。通过 UMD 可以在合适的环境选择对应的模块规范。比如在 Node.js 环境中采用 CommonJS 模块管理,在浏览器端且支持 AMD 的情况下采用 AMD 模块,否则导出为全局函数。 UMD 作出了如下内容的规定:

    1. 优先判断是否存在 exports 方法,如果存在,则采用 CommonJS 方式加载模块;
    2. 其次判断是否存在 define 方法,如果存在,则采用 AMD 方式加载模块;
    3. 若前两个都不存在,则将模块公开到全局(Window 或 Global)。

    大致实现如下:

    (function (root, factory) {
      if (typeof define === 'function' && define.amd) {
        define([], factory);
      } else if (typeof exports === 'object') {
        module.exports, (module.exports = factory());
      } else {
        root.returnExports = factory();
      }
    })(this, function () {
      //。。。
      return {};
    });
    

总结

个人理解前端模块化是作为前端开发需要了解的必要内容,此文章内容为网络上一些讲的不错的关于模块化的文章,个人看后的理解以及归纳。有些描述性文字直接一比一copy过来的(我只能说作者的理解即是我的理解...),以便后续查阅,相关链接在末尾

参考资料