前端模块化规范

2021/02/24 javascript

记录一下前端各种模块化规范的区别和特性。

1、ES6模块

ES6模块是ECMAScript 2015 提出的模块化规范。他有严格的申明要求。

  1. 必须在文件的首部
  2. 不允许使用变量或表达式
  3. 不允许被嵌入到其他语句中

1.1 使用方式

  1. 直接使用 <script> 标签加载 <script src='xxx' type='module'><>
  2. 通过import静态引入 import xxx from "module-xxx";

1.2 使用注意

  • ES6模块强制自动采用严格模式,所以说不管有没有“user strict”声明都是一样的,换言之,编写代码的时候不必再刻意声明了;
  • 虽然大部分主流浏览器支持 ES6 模块,但是和引入普通 JS 的方式略有不同,需要在对应 script 标签中将属性 type 值设置为“module”才能被正确地解析为 ES6 模块;
  • 在 Node.js 下使用 ES6 模块则需要将文件名后缀改为“.mjs”,用来和 Node.js 默认使用的 CommonJS 规范模块作区分。

1.3 特性

ES6 模块有两个重要特性一定要掌握,一个是值引用,另一个是静态声明

  • 值引用:值引用是指export语句输出的接口与其对应的值是动态绑定的关系,意思是通过他获取的值会随着模块内部的修改而改变,类似于对象的浅拷贝。
  • 静态声明:静态分析是指不需要执行代码,只从字面量上对代码进行分析。好处是方便优化代码体积,比如通过Tree-shaking操作消除模块中没有被引用或者执行结果不会被用到的无用代码。

1.4 动态导入

静态导入可以直接import,动态导入就需要import()了。调用 import()函数传入模块路径,得到一个Promise对象。

let promise = import("module-promise");

同时动态导入还支持await / async

let promise = await import("module-promise");

webpack打包的时候,在调用了import()的地方,请求的模块及其子模块会被分到单独的chunk里面。

2、CommonJs

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

CJS规定每一个文件就是一个模块,有独立的作用域,模块内部的this也只是当前模块。

CJS导出的对象具有以下属性

  1. id模块标识符,通常是带有绝对路径的模块文件名
  2. filename模块的文件名,带有绝对路径
  3. loaded返回一个布尔值,表示是否完成加载。
  4. parent返回一个对象,表示调用该模块的模块。
  5. children返回一个数组,表示该模块用到的其他模块。
  6. exports表示模块对外输出的值。

使用

// a.js 
const fn = function () {
    console.log('Hello');
}
module.exports = fn;

// b.js
const fn = require('./a');
fn(); // Hello

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

2.1 特性

CJS的特性和ES6的恰恰相反,它采用的是值拷贝和动态声明。

  • 值拷贝:一旦输出了一个值,模块内部的变化就不会影响到这个值了。

  • 动态声明:可以随意在表达式语句中引用模块。

3、AMD

AMD是在ES6出来之前流行的浏览器模块化方案。

AMD规定只定义了一个全局函数define,通过它可以定义和引用模块,它有3个参数:

define(id?, dependencies?, factory)
  • 第 1 个参数 id 为模块的名称,该参数是可选的。如果没有提供该参数,模块的名字应该默认为模块加载器请求的指定脚本的名字;如果提供了该参数,模块名必须是“顶级”的和绝对的(不允许相对名字)。
  • 第 2 个参数 dependencies 是个数组,它定义了所依赖的模块。依赖模块必须根据模块的工厂函数优先级执行,并且执行的结果应该按照依赖数组中的位置顺序以参数的形式传入(定义中模块的)工厂函数中。
  • 第 3 个参数 factory 为模块初始化要执行的函数或对象。如果是函数,那么该函数是单例模式,只会被执行一次;如果是对象,此对象应该为模块的输出值。

3.1 特性

AMD的重要特性就是异步加载,同步并发加载依赖的模块,当所有的模块都加载完成后,再执行当前模块的回调函数。

由于AMD不是浏览器支持的模块化规范,所以AMD的实现由第三方库来实现,比如RequireJs。它的核心是两个全局函数 define 和 require,define 函数可以将依赖注入队列中,并将回调函数定义成模块;require 函数主要作用是创建 script 标签请求对应的模块,然后加载和执行模块。

4、CMD

CMD(Common Module Definition,通用模块定义)是基于浏览器环境制定的模块规范。

CMD定义模块是通过一个全局的defind实现的,但是只有一个参数。如果这个参数是对象,那么模块导出的就是对象;如果这个参数为函数,那么这个函数会被传入 3 个参数 require 、 exports 和 module。

define(factory);

CMD 最大的特点就是懒加载,不需要在定义模块的时候声明依赖,可以在模块执行时动态加载依赖。当然还有一点不同,那就是CMD同时支持同步加载模块和异步加载模块。

用一句话来形容就是,它整合了 CommonJS 和 AMD 规范的特点。遵循 CMD 规范的代表开源项目是 sea.js ,它的实现和 requirejs 没有本质差别。

5、UMD

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

实现原理:

  1. 先判断是否支持 Node.js 模块格式(exports 是否存在),存在则使用 Node.js 模块格式;
  2. 再判断是否支持 AMD(define 是否存在),存在则使用 AMD 方式加载模块;
  3. 若前两个都不存在,则将模块公开到全局(Window 或 Global)。

6、ES5环境下编写模块

模块的核心就是创建独立作用域,要实现这个目的,就需要通过函数来实现。可以通过定义一个立即执行函数来返回一个对象。

Search

    Table of Contents