Babel的奇妙冒险 @babel/babel-plugin-transform-modules-*

1,366 阅读6分钟

模块化的背景

早期 Javascript 程序很小,大多被用来执行独立的脚本任务,提供一定交互。现在有了运行大量 Javascript 脚本的复杂程序,还有一些被用在其他环境(例如 Node.js)。因此,有必要开始考虑提供一种将 JavaScript 程序拆分为可按需导入的单独模块的机制。

Javascript中的模块需要解决的问题是: 如何将一段代码封装到一个有用的单元中,以及如何注册其函数/为模块导出值,如何引用其他代码单元。

Babel模块化相关插件

  • babel-plugin-transform-modules-amd
  • babel-plugin-transform-modules-commonjs
  • babel-plugin-transform-modules-umd

传统方法

以Jquery为例,以前是如何定义代码段的?

// Jquery 示例
(function(window,undefined){
    window.$ = window.JQuery;
    var JQuery = function(selector){
    }
})(window)
  • 通过立即函数调用表达式(IIFE)定义
  • 对依赖关系的引用是通过通过HTML脚本标记加载的全局变量名称完成的。
  • 依赖关系的描述非常微弱:开发人员需要知道正确的依赖关系顺序。例如,包含Backbone的文件不能在jQuery标记之前。
  • 它需要额外的工具才能将一组脚本标签替换为一个标签,以优化部署。
  • 在大型项目上,这可能很难管理,尤其是当脚本开始以可能重叠和嵌套的方式具有许多依赖关系时。手写脚本标签的伸缩性不是很高,因此无法按需加载脚本。

CommonJS

CommonJS 原先叫做ServerJS,是js在服务端的规范,node使用的就是这种规范。根据CommonJS规范,一个单独的文件就是一个模块,require用来加载一个模块,exports用来向外部暴露该模块里的方法或属性,加载模块是同步的。

CommonJS规范

AMD (异步模块定义 Asynchronous Module Definition)

AMD规范制定了定义模块的规则,这样模块和模块的依赖可以被异步加载。这和浏览器的异步加载模块的环境刚好适应(浏览器同步加载模块会导致性能、可用性、调试和跨域访问等问题)。

AMD规范只定义了一个函数 "define",它是全局变量。函数的描述为:

define(id?, dependencies?, factory);

id是模块的名字,dependencies是依赖模块数组,工厂方法 factory 为模块初始化要执行的函数或对象

  • 通过调用define()来注册工厂函数,而不是立即执行它。
  • 将依赖项作为字符串值数组传递,不要获取全局变量。
  • 仅在加载并执行了所有依赖项后,才执行工厂函数。
  • 将相关模块作为参数传递给工厂函数。
// 全局函数 define 应该实现绑定 amd 属性,防止与现有的定义了define函数但不遵从AMD编程接口的代码相冲突
define.amd = {};

// 判定当前页面是否有 AMD 模块加载器
if (typeof define === "function" && define.amd) {
    // ...
}

requirejs

requireJS是基于AMD模块加载规范,使用回调函数来解决模块加载的问题。

requireJS是使用创建script元素,通过指定script元素的src属性来实现加载模块的。

CMD (通用模块定义 Common Module Definition)

CMD规范中,一个模块就是一个文件

define('hello', ['jquery'], function(require, exports, module) {
// require
  // require 是一个方法,接受 模块标识 作为唯一参数,用来获取其他模块提供的接口
  // require.async 方法用来在模块内部异步加载模块,并在加载完成后执行指定回调。callback 参数可选
  // require.resolve 使用模块系统内部的路径解析机制来解析并返回模块路径。该函数不会加载模块,只返回解析后的绝对路径
// exports
  // exports 是一个对象,用来向外提供模块接口
  // 也可以使用 return 直接向外提供接口
  // exports 仅仅是 module.exports 的一个引用。在 factory 内部给 exports 重新赋值时,并不会改变 module.exports 的值。因此给 exports 赋值是无效的,不能用来更改模块接口
//module
  // module 是一个对象,上面存储了与当前模块相关联的一些属性和方法
  // module.uri String 根据模块系统的路径解析规则得到的模块绝对路径
  // module.dependencies 是一个数组,表示当前模块的依赖
  // module.exports 当前模块对外提供的接口
  // module.exports 的赋值需要同步执行,不能放在回调函数里
});

// 判定当前页面是否有 CMD 模块加载器
if (typeof define === "function" && define.cmd) {
}

seajs

SeaJS是一个遵循 CMD 规范的模块加载器。

UMD(通用模块定义Universal Module Definition)

UMD提供了支持多种风格的通用模式,在兼容CommonJS和AMD规范的同时,还兼容全局引用的方式

// babel UT output code

(function (global, factory) {
  if (typeof define === "function" && define.amd) {
    // 判断是否支持AMD
    define(["exports"], factory);
  } else if (typeof exports !== "undefined") {
    // 否支持 Node.js 模块格式
    factory(exports);
  } else {
    var mod = {
      exports: {}
    };
    factory(mod.exports);
    global.input = mod.exports;
  }
})(typeof globalThis !== "undefined" ? globalThis : typeof self !== "undefined" ? self : this, function (_exports) {
  "use strict";

  Object.defineProperty(_exports, "__esModule", {
    value: true
  });
  _exports.default = void 0;
  var _default = {};
  _exports.default = _default;
});

ES6 模块

  • ES6 模块是通过静态分析编译时加载
  • 不需要UMD模块格式,将来服务器和浏览器都会支持 ES6 模块格式。
  • 将来浏览器的新 API 就能用模块格式提供,不再必须做成全局变量或者navigator对象的属性。
  • 不再需要对象作为命名空间(比如Math对象),未来这些功能可以通过模块提供。

ES6 模块

浏览器支持模块

最新的浏览器开始原生支持模块功能了,浏览器能够最优化加载模块,使它比使用库更有效率:使用库通常需要做额外的客户端处理。

  • html 使用 type="module" 的 script 标签
  • 原生JavaScript模块,使用 .mjs 扩展名, MIME-typejavascript/esm导入文件
    • 比较清晰,这可以指出哪些文件是模块,哪些是常规的 JavaScript。
    • 这能保证你的模块可以被运行时环境和构建工具识别,比如 Node.js 和 Babel
  • export import 语法和ES6一致
  • import 模块文件的路径是相对于站点根目录的相对路径,或者使用点语法意味当前路径
  • 通过本地加载Html 文件 (比如一个 file:// 路径的文件), 会遇到 CORS 错误,因为Javascript 模块安全性需要,要通过一个服务器来测试。
  • 模块自动采用严格模式,不管你有没有在模块头部加上"use strict";
  • 加载一个模块脚本时不需要使用 defer 属性,模块会自动延迟加载。

动态加载模块

将import()作为函数调用, 返回一个 promise, 模块对象为参数

import('/modules/myModule.mjs')
  .then((module) => {
    // Do something with the module.
  });

参考

webpack 模块

webpack 模块能够以各种方式表达它们的依赖关系,几个例子如下:

  • ES2015 import 语句
  • CommonJS require() 语句
  • AMD define 和 require 语句
  • css/sass/less 文件中的 @import 语句。
  • 样式(url(...))或 HTML 文件(<img src=...>)中的图片链接(image url)