简介

现代软件开发,模块化编程是一个必然的演进历程,在前端领域模块化编程也是目前研发的主流编程范式。

模块化

通过模块化可以有以下一些收益:

  • 通过把功能划分到各个模块之后,每个模块都是独立的,通常一个模块有自己的功能和职责,能够方便让开发者理解并使用;
  • 划分模块后,会定义统一模块接口,来对外暴露功能。这使得所有开发者定义模块和引用模块的方式都是一致的
  • 模块化后,通用的功能的模块在开发者直接来回共享,可以在不同项目进行复用

当所有开发者使用一致的模块标准规范后,模块共享及使用变得非常便利,这也利于开源软件的发展。在 JavasScript 语言中,模块化在低层次同时解决了全局命名空间被污染的历史问题。

JavaScript 的模块有一些特点:

  • 模块无需直接引用全局对象,全局对象可以作为模块依赖项引入,这也是标准做法;
  • 模块会显式列出其依赖项并获取这些依赖项的句柄 (Handle),在标准模块开发时,通常把依赖引入放在文件的开头;
  • 模块通常有两个必要接口:一个是定义模块接口,另一个是模块导入导出接口。导入导出接口一般成对出现。导入的模块,通常也只能使用模块导出接口指定的值;
  • 模块的颗粒度可以是任意大小,这非常适合组建健壮的大型应用。

RequireJS

在前端制定模块系统标准之前,已经有一些优秀模块库实现了模块化的功能。例如在浏览器环境下的 RequireJS,通常也被叫做模块加载器,它是一款非常经典的 AMD 模块规范的实现库。

RequireJS 也支持在其它环境,例如:Rhino 和 Node。不过,浏览器端是它的最为广泛的应用。

模块加载

模块化的加载与传统的 <script> 标签加载 JavaScript 文件不同,模块化加载鼓励使用模块 ID 来加载 JavaScript 文件而不是使用 script 标记的 URL。

JavaScript 文件的路径会被提前或动态建立起和自身为模块的 ID 映射。开发者使用模块,只需关注模块 ID 而无需关注该模块在磁盘的具体位置。

当然通过模块 ID 来加载 JavaScript 文件,并不意味着一个模块 ID 对应一个 JavaScript 文件,通常会通过优化工具将多个模块打包成单一 JavaScript 文件。

假设我们有一个如下文件结构的项目:

.
├── app
│   └── sub.js
├── entry.js
├── index.html
└── lib
    └── require.js

app/sub.js 的内容如下:

define({
  color: 'red'
});

浏览器环境中,我们需要在 index.html 中优先引入 RequireJS 文件:

<script data-main="entry.js" src="lib/require.js"></script>

entry.js 是加载 lib/require.js 完成后的启动脚本。我们的全局配置可以在 entry.js 中完成。

在 entry.js 中:

requirejs.config({
  // 默认任意模块 ID 都从 lib 目录中加载  
  baseUrl: 'lib',
  // 此外,如果模块 ID 以 "app" 开头,则会从 app 目录加载
  // paths 配置相对于 baseUrl 的路径规则,同时不要包含文件后缀 (.js),因为 paths 的配置支持指向目录
  paths: {
    app: '../app'
  }
});

// 开始执行应用逻辑
require(['app/sub'], function (sub) {
  // 当 app/sub 模块加载完成之后,当前函数被执行
  console.log(sub)
  // output: {color: 'red'}
});

RequireJS 为了加载速度对依赖的加载都是无序的,正式的来说,他们都是异步加载的。但回调函数一定是在他们加载完成之后再执行的,RequireJS 保证我们使用依赖时一定是正确的。

如何加载模块?

RequireJS 把依赖模块文件处理成 script 标签,因此你可以在 header 里看到它:

<script type="text/javascript" charset="utf-8" async data-requirecontext="_" data-requiremodule="entry" src="./entry.js"></script>
<script type="text/javascript" charset="utf-8" async data-requirecontext="_" data-requiremodule="app/sub" src="lib/../app/sub.js"></script>
加载远程模块

除了通过路径引用模块外,可以加载远程模块:

require.config({
  paths: { 
    dayjs: 'https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-M/dayjs/1.10.8/dayjs.min' 
  }
});

require(['dayjs'], function(dayjs) {
  console.log(dayjs().format("YYYY-MM-DD HH:mm:ss"));
  // output: 2023-03-21 21:41:30
});

但是你不能以下面的方式去加载,它会抛出异常:

// ⚠️ 不要这么去做,它会抛出异常。
require(['https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-M/dayjs/1.10.8/dayjs.min'], function (dayjs) {});

模块定义和导出

在上面的app/sub.js文件,我们已经看到了模块的定义方式。RequireJS 通过 define 函数来定义模块,最简单的值模块定义如下:

define({
  id: 0,
  color: "black",
  size: "unisize"
});

该模块返回了一个对象值,也可以定义一个函数模块:

define(function() {
  return function(title) {
    window.title = title;
  } 
});

也可以加上依赖项:

define("app/goods", ["app/my/cart", "app/my/inventory"], function(cart, inventory) {
  const goods = {
    id: 0,
    color: "black",
    size: "unisize",
    addToCart: function() {
      inventory.decrement(this);
      cart.add(this);
    }
  }
  return goods;
});

我们定义了一个具名的模块 app/goodsapp/my/cartapp/my/inventory 是当前模块所需依赖项数组。最后,提供了一个回调函数,其参数为依赖项提供的对象,并和依赖数组项顺序一致。模块的回调函数会在两个依赖加载完成之后执行。

通常不会定义具名模块,因为这样会损失可移植性。模块加载的时候默认根据路径匹配到模块文件的磁盘位置。

也可以把 app/goods.js 文件定义为一个匿名模块:

define(["app/my/cart", "app/my/inventory"], function(cart, inventory) {
  const goods = {
    id: 0,
    color: "black",
    size: "unisize",
    addToCart: function() {
      inventory.decrement(this);
      cart.add(this);
    }
  }
  return goods;
});

现在它的文件结构如下:

.
├── app
│   ├── goods.js
│   ├── my
│   │   ├── cart.js
│   │   └── inventory.js
│   └── sub.js
├── entry.js
├── index.html
└── lib
    └── require.js

从 RequireJS 支持 CommmonJS 模块方式,因此,无需提供依赖数组,可以直接在包裹的 define 里,通过 require 函数引入依赖:

define(function (require, exports, module) {
  const myCart = require('app/my/cart');
  const myInventory = require('app/my/inventory');
  return {
    id: 0,
    color: "black",
    size: "unisize",
    addToCart: function () {
      myInventory.decrement(this);
      myCart.add(this);
    }
  }
});

有一些 CommonJS 系统,主要是 Node,允许通过将导出值分配为 module.exports ,来设置模块对外暴露值。RequireJS 支持该习惯用法:

define(function (require, exports, module) {
  const myCart = require('app/my/cart');
  const myInventory = require('app/my/inventory');
  module.exports = {
    id: 0,
    color: "black",
    size: "unisize",
    addToCart: function () {
      myInventory.decrement(this);
      myCart.add(this);
    }
  }
});

这和直接 return 该对象是一样的效果。

模块导入

在上面已经看到了导入模块接口的实现,RequireJS 通过 require 函数来导入模块:

require(["app/goods"], function(goods) {
  console.log(goods)
  // output: {id: 0, addToCart: f }
});

需要注意的是如果我们没有指定 paths: { app: '../app'} 配置,那么依赖模块 app/goods 会被解析到 lib/app/goods.js 的错误文件路径。

循环依赖

在软件设计上来说,循环依赖 (Circular dependency) 是一种反模式 (anti-pattern)。可能导致阻止自动垃圾收集器释放内存。尽管如此,模块一旦很多之后,模块之间的相互引用可能不可避免的发生。通常这种情况,模块系统都有相应的策略来应对。

javascript-module.svg

在 RequireJS 中,A 模块引用了 B 模块,而 B 模块也需要依赖 A 模块。那么 B 模块可以这么去定义:

// b.js
define(["require", "a"], function(require, a) {
  // 如果 a 被 b 引用。此时,a 的值为 null
  return function(title) {
    return require("a").doSomething();
  }
});

让我们再来看看 A 模块的定义:

// a.js
define(["b"], function(bFunc) {
  bFunc("page title");
  return {
    doSomething() {}
  }
});

相比非循环依赖的加载,在 B 模块中使用 A 模块的方法,需要通过 require 来主动加载 A 模块。虽然解决了循环依赖的问题,但其实只是针对某一模块来解决的,并非一致的。

我们更希望不用确认哪个模块,都使用统一方式来解决循环依赖的问题,同时它也是一种模块定义方式。所以,我们可以使用 CommmonJS 模块方式:

// b.js
define(function(require, exports, module) {
    // 如果 a 使用 exports ,那么我们在这里有一个真实对象引用。
    // 然后,我们不能使用任何 a 的属性,直到 b 返回一个值
    const a = require("a");
    exports.foo = function () {
        return a.doSomething();
    };
});

exports 为模块创建一个空对象,该对象可立即供其他模块引用。同样 A 模块也可以使用同样的方式来定义,这种方式和 CommonJS 标准模块已经非常的相似了。

你可以在这里找到上面的示例。

CommonJS 模块

CommonJS 模块是的一种规范,它定义了一种模块格式。在前端领域使用最广的是 Node.js 环境下的对它的实现。

在 Node 环境中,我们无需对文件使用类似 RequireJS 的 define 函数包裹模块,每个模块在执行之前,都会被一个函数包裹器包裹,类似下面代码:

(function(exports, require, module, __filename, __dirname) {
// Module code actually lives in here
});

为此,Node 会做一些事情:

  • 保持顶级变量 (var、const、let 定义的) 作用于模块范围,而不是 global 对象;
  • 帮助提供一些全局搜索变量,这些变量可以指向到当前模块。例如:
    • moduleexport 对象,可以用于导出模块值;
    • __filename__dirname 变量包含模块的绝对路径文件名称和目录路径。

如果打印 module,你会看到它的全部属性:

Module {
  id: '/Users/lumin/Desktop/tmp/commonjs/a.js',
  path: '/Users/lumin/Desktop/tmp/commonjs',
  exports: {},
  filename: '/Users/lumin/Desktop/tmp/commonjs/a.js',
  loaded: false,
  children: [],
  paths: [
    '/Users/lumin/Desktop/tmp/commonjs/node_modules',
    '/Users/lumin/Desktop/tmp/node_modules',
    '/Users/lumin/Desktop/node_modules',
    '/Users/lumin/node_modules',
    '/Users/node_modules',
    '/node_modules'
  ]
}

其中 exportsmodule 对象的一个属性,所以下面的代码是一样:

exports.greeting = 'hello'
// same with
module.exports.greeting = 'hello'

如果在 Node 环境编写重写 RequireJS 下的 app/goods.js 代码会是下面的样子:

const myCart = require('./app/my/cart');
const myInventory = require('./app/my/inventory');

module.exports = {
  id: 0,
  color: "black",
  size: "unisize",
  addToCart: function () {
    myInventory.decrement(this);
    myCart.add(this);
  }
}

由于 Node 的路径解析规则,为了正确引入模块需要添加./来解析正确的路径。

模块导出

上面了解到 CommonJS 无需开发者手动定义模块,即不需要模块包裹函数,只需要通过 exports 来导出模块。

module.exports = {}

这里需要注意的是 module.exports 默认是个空对象,这意味着如果你给 exports 赋值,将替换该空对象。所以执行以下代码,将并不会导出模块:

// 该模块不会正常导出。
exports = {}

你可以如下进行导出:

module.exports = exports = {};
// 或导出一个函数
module.exports = exports = () => {}

由于 CommonJS 模块是运行时语法,它导出的都是普通数据类型。当其导出一个对象时,你可以在导入时直接通过 ES6 解构赋值来使用对象属性。

// a.js
// module export
exports = {
  aFunc: () => {},
  bFunc: () => {}
}
// b.js
// module import
const { aFunc } = require("./a.js");

模块导入

Node 环境下,CommonJS 的模块导入语法如下:

const a = require("a-module");
const b = require("b-module");
a.doSomething();
b.doSomething();

可以看到相比 RequireJS 的模块导入,CommonJS 模块导入没有回调,这是 CommonJS 模块和 AMD 模块很大的一个区别,AMD 支持异步模块加载。

AMD 规范强烈要求模块是基于需要回调的需求,这是因为动态计算依赖项可能会异步加载,所以上面可以看到 define 定义的模块都是通过回调方式来执行的。

循环依赖

Node 允许循环依赖的存在,即便它是反模式的。

因为 exports 有默认值,因此如果存在循环依赖,也能立即返回一个空对象,不至于引发程序异常。下面提供了一个示例:

a.js

console.log('a starting');
exports.done = false;
const b = require('./b.js');
console.log('in a, b.done = %j', b.done);
exports.done = true;
console.log('a done');

b.js

console.log('b starting');
exports.done = false;
const a = require('./a.js');
console.log('in b, a.done = %j', a.done);
exports.done = true;
console.log('b done');

main.js

console.log('main starting');
const a = require('./a.js');
const b = require('./b.js');
console.log('in main, a.done = %j, b.done = %j', a.done, b.done);

它的执行顺序如下:

  1. main.js 加载时 a.js,则 a.js 依次加载 b.js
  2. b.js 尝试加载 a.js。 为了防止无限循环,将导出对象的未完成副本 a.js 返回给 b.js 模块。
  3. b.js 然后完成加载,并将其 exports 对象提供给 a.js 模块。

main.js 加载两个模块时,它们都已完成。因此,该程序的输出将是:

$ node main.js
main starting
a starting
b starting
in b, a.done = false
b done
in a, b.done = true
a done
in main, a.done = true, b.done = true

我们假设一个 require() 方法实现,它的实际执行和 Node require 方法非常相似,你可以在这里看到其开源的执行源码。

在当调用 require('./b.js') 时,b.js 会如下执行:

function require(/* ... */) {
  const module = { exports: {}, loaded: false, /* ... */ };
 // 1. 如果当前模块没有加载完成,那么立即返回该对象
  if(!module.loaded) {
    return module.exports;
  }

 // 模块函数包裹器   
  ((module, exports) => {
    // 这里是 b.js 编写模块代码
    
    console.log('b starting');
    exports.done = false;
    // 由于 1 步骤,a.js 模块未加载完成,因此 a.js 直接返回了默认 module.exports 的值,此时它被绑定了 exports.done = false;
    
    const a = require('./a.js');
    // 所以,a.done 为 false
    console.log('in b, a.done = %j', a.done);
    exports.done = true;
    console.log('b done');
    
  })(module, module.exports);

  module.loaded = true;

  return module.exports;
}

Node 在实现 CommonJS 标准解决循环依赖的方式和 RequireJS 类似,通过直接返回一个空对象来避免执行异常而终止程序运行。这也侧面说明了即便循环依赖是反模式的,但 Node 依然允许循环依赖的存在。不过站在开发者角度来说,必须清晰认识到它的执行流程,否则容易出现难以发现的意外逻辑执行。

AMD 模块

AMD (Asynchronous Module Definition) 模块规范是一种异步加载规范。RequireJS 便是基于 AMD 规范实现的模块加载库,所以上面的 RequireJS 示例便是 AMD 的模块定义及导入导出语法。

模块定义

语法:

define(id?, dependencies?, factory);

规范建议定义模块采用 ‘define(…)’ 的文字形式,以便与静态分析工具 (如构建工具) 一起正常工作。

模块导入

导入有以下几种语法:


require(String)

require(Array, Function)

require.toUrl(String)

由于在 RequireJS 中大篇幅介绍了 AMD 的使用,所以这个小节简单的描述了 AMD 的语法。注意的是通常 AMD 模块的 require 方法在回调里执行的。

amd 属性

规范定义了 AMD API 全局 define 函数中应该有 amd 的属性,示例:

define.amd = {
  multiverion: true,
}

最小定义:

define.amd = {}

因为这点,可用于区分一个模是否是 AMD 模块。

UMD 模块

UMD 是通用模块定义 (Universal Module Definition),它使同一文件能够被多个模块系统 (例如 CommonJS 和 AMD) 使用。不过,一旦 ES 模块成为唯一的模块标准,UMD 就过时了。

UMD 实现是通过一个 IIFE 来兼容 AMD 或 CommonJS 的导出工作:

(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD. Register as an anonymous module.
        define(['exports', 'b'], factory);
    } else if (typeof exports === 'object' && typeof exports.nodeName !== 'string') {
        // CommonJS
        factory(exports, require('b'));
    } else {
        // Browser globals
        factory((root.commonJsStrict = {}), root.b);
    }
}(typeof self !== 'undefined' ? self : this, function (exports, b) {
    // Use b in some fashion.

    // attach properties to the exports object to define
    // the exported module properties.
    exports.action = function () {};
}));

通常这样的包裹行为由一些打包工具完成。以 webpack 为例,我们给定一个入口文件 index.js

const title = "hello";
function greeting() {
  console.log('hello')
}

然后,当我们设置打包库类型为 umd 时,它的输出是这样的:

(function webpackUniversalModuleDefinition(root, factory) {
 if(typeof exports === 'object' && typeof module === 'object')
  module.exports = factory();
 else if(typeof define === 'function' && define.amd)
  define([], factory);
 else if(typeof exports === 'object')
  exports["myLib"] = factory();
 else
  root["myLib"] = factory();
})(self, () => {
return /******/ (() => { // webpackBootstrap
var __webpack_exports__ = {};
const title = "hello";
function greeting() {
  console.log('hello')
}
/******/  return __webpack_exports__;
/******/ })()
;
});

实现上基本是一致的,你可以在这里看到 webpack 的示例。

ES 模块

ECMAScript 2015 (ES6) 标准定义了 JavaScript 语言的模块规范,并在很多浏览器已实现该标准。符合 ES 标准的模块也被叫做 ES 模块。除了浏览器上的支持 ES Modules 外,Node 从 12.0.0 版本开始正式支持 ES modules

ES 模块设计目标

  • 默认导出受到青睐
  • 静态模块结构
  • 支持同步和异步加载
  • 支持模块之间的循环依赖

导出模块

ES 的模块导出语法如下:

export const title = "hello world";

export function greeting() { console.log("hello wrold") }

也可以通过一个 export 统一导出:

// modules/a.js
const title = "It's a title";

function greeting() { console.log("hello wrold") }

export { title, greeting }
导出默认模块

与 CommonJS 的模块导出不同,ES Module 必须添加 default 关键字来导出默认模块。

// modules/a.js
const title = "It's a title"

function greeting() { console.log("hello wrold") }

export { title, greeting }
export default greeting

我们也可以导出一个对象:

// ./modules/b.js

const order = {
  id: 0,
  amount: 100
}

export default order;

ES Modules 是修改 JavaScript 语言标准来实现的,所以它的模块导入导出必须使用语法关键字。一旦模块的语法不正确,那么在 JavaScript 在浏览器的静态分析阶段就会抛出异常。

合并模块导出

合并模块可以将多个子模块组合到一个父模块中。这可以使用父模块中以下表单的导出语法:

export * from 'x.js'
export { name } from 'xx.js'

模块导入

ES 的模块导入语法如下:

// index.js
import { title, greeting } from "./modules/a.js"

greeting();

console.log("title:", title)
// output: hello wrold

有的同学想在 import 的时候直接使用解构赋值语法来获取默认导出对象的属性,这是不可以的。

// 这是错误的语法,抛出异常,程序终止。
import { id } from './modules/b.js';

console.log(id) // 执行不到该语句

道理也很简单,这和命名模块的导入是语法冲突的。除外,还有一个重要原因是 ES Module 是静态结构的,这意味着编译时 (静态地) 便确定了导入和导出,我们不能在运行时来改变模块结构。

当 JS 引擎运行一个模块时,通常有以下四个步骤的表现:

  1. 解析:实现读取模块的源代码并检查语法错误;
  2. 加载:实现加载所有导入的模块 (递归)。这是尚未标准化的部分;
  3. 链接:对于每个新加载的模块,创建一个模块范围,并用该模块中声明的所有绑定填充它,包括从其他模块导入的东西。
    如果您尝试这样做的部分 import {cake} from “paleo”,但 paleo 模块实际上并未导出任何命名的 cake 内容,您将收到错误消息。
  4. 运行时:最后,运行每个最新已加载的模块​​的语句。到这个时候,import 处理已经完成,所以当执行到一行有 import 声明的代码时,什么也没有发生!

也正是因为静态模块结构,你不可以在 import 语句中使用变量:

// ⚠️ 不能使用变量
// Illegal syntax:
import foo from 'some_module' + SUFFIX;

由于 ES Module 的静态结构,才可以让一些静态分析工具能够在模块打包时,Tree Shaking 掉未引用的代码 (dead-code),以此优化代码体积。

因此要想运行正确可以如下修改:

// ./modules/b.js
const order = {
  id: 0,
  amount: 100
}
export const id = order.id;
export default order;

然后如下执行导入:

import order, { id } from './modules/b.js';

console.log(id)
console.log(order.id)
导入默认模块

如果模块导出使用了 default 关键字:

// ./modules/b.js
export default order;

那么模块的导入可以使用自定义模块别名:

import myOrder from './modules/b.js';
创建模块对象

如果依赖的模块没有导出默认模块,我们也可以通过创建模块对象,从而有效地给自己提供命名空间:

import * as myOrder from './modules/b.js';

而如果模块包含 export default 模块的导出,那么创建模块对象之后,默认模块会被指定到 default 上去:

import * as order from './modules/b.js';
console.log(order)
// output:
// Module {
//    default: { amount: 100,id: 0 },
//    id: 0
// }
console.log(order.default)
// output: {id: 0, amount: 100}

以下也是等价的:

import _,{ each, map } from 'lodash'
// eq
import {each, map, default as _} from 'lodash'
空导入

只加载模块,不导入任何东西。

import 'src/my_lib';
动态加载模块

ES Modules 的最新内容允许按需加载模块。它返回一个 promise,并导出一个 module 对象:

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

需要注意的是 import() 语法是模块加载 API 的一分部,它并不是 ES6 标准,因此,你需要考虑浏览器对它的支持度。

ES Module 导出只读视图

ES module 导出的是实时只读视图,如下修改导出视图将会抛出异常

// 导出 a.js
export let title = "It's a title"
// 导入
import { title } from "./modules/a.js";
// ⚠️ 你不能对 title 进行赋值,因为它是一个常量。
title = "read only";
// Uncaught TypeError: Assignment to constant variable.

循环依赖

ES Modules 也同样支持循环依赖的。因为其静态性,所以并不会像 CommonJS 一样发生循环依赖时只是简单的返回一个空对象。我们看下面一个例子:

//------ a.js ------
import {bar} from 'b'; // (1)
export function foo() {
    bar(); // (2)
}

//------ b.js ------
import {foo} from 'a'; // (3)
export function bar() {
    if (Math.random()) {
        foo(); // (4)
    }
}

这段代码有效,因为如上一节所述,导入是导出的视图。这意味着即使是不合格的导入(例如 bar 第 2 行和 foo 第 4 行)也是引用原始数据的间接。因此,面对循环依赖,无论您是通过非限定导入还是通过其模块访问命名导出都无关紧要:这两种情况都涉及间接寻址,并且它始终有效。

导入被提升 (hoisted)

模块导入被提升(内部移动到当前范围的开头)。因此,无论您在模块中的何处提及它们,以下代码都可以正常工作:

foo();

import { foo } from 'my_module';

虽然如此,但你最好将导入放置开头,以便提高可阅读性。

避免命名冲突

为了防止多个模块功能命名冲突,可以使用 as 关键字重命名模块的导出或导入:

export { title, greeting as newGreeting }
import { title as newTitle, newGreeting } from "/path/module";

import.meta 元数据

import.meta 保存了模块导入的元数据,例如当以下模块导入时:

import greeting, { title, myObj } from "./modules/a.js?a=1&b=2";

我在本地启的一个服务,可以看到我们能直接在 a.js 模块里,使用 import.meta 获取元数据:

// a.js
console.log(import.meta.url)
// output:
//{ resolve: f(), url: "http://127.0.0.1:5500/examples/es-modules/modules/a.js?a=1&b=2"}

除了 import.meta.url 属性外,还提供了一个解析 API 函数 import.meta.resolve() 用于解析模块,它返回一个可以用于模块导入的路径字符串。

同样在 Node 环境中也支持了 meta。

// a.js
console.log(import.meta.resolve("./b"))
// output: http://127.0.0.1:5500/examples/es-modules/modules/b

注意的是我们没有指定模块后缀名,解析的 url 也不包含后缀。目前为止,import.meta.resolve() 在 iOS 的 Safari 浏览器上还不被支持的。

HTML 里引入 ES 模块

需要注意在模块如果要在 html 里使用模块,必须把 script 标签标记为 module,否则会报异常。

<script type="module" src="./index.js"></script>

ES 和 CommonJS 互操作性

在 Node.js 环境,ES 模块和 CommonJS 模块可以相互引用。

下面是一个在 Node.js 环境中实现 ES 模块和 CommonJS 模块互操作的例子:

假设我们有一个 CommonJS 模块 “math.js”,它导出一个加法函数:

// math.js
function add(a, b) {
  return a + b;
}
module.exports = { add };
// index.js
import { add } from './math.js'; // 使用 ES 模块的 import 导入 math.js 模块
const math = require('./math.js'); // 使用 CommonJS 的 require 导入 math.js 模块

console.log(add(2, 3)); // 输出 5
console.log(math.add(2, 3)); // 输出 5

在上面的代码中,我们使用 ES 模块的 import 导入了 “math.js” 模块中的 add 函数。同时,我们也使用了 CommonJS 的 require 导入了 “math.js” 模块,并将其赋值给变量 math。然后,我们分别调用了这两个模块中的 add 函数,输出了它们的结果。

需要注意的是,在上面的代码中,我们使用了 Node.js 特有的 require 函数,而不是 ES6 的 import 函数。这是因为在 Node.js 中,ES6 的 import 函数只能在 .mjs 文件中使用,而不能在 .js 文件中使用。因此,我们需要同时使用 import 和 require 来实现 ES 模块和 CommonJS 模块的互操作。

参考资料:

> https://zh.wikipedia.org/wiki/模块化编程

> https://vibaike.com/109683/?ivk_sa=1024320u

> www.adequatelygood.com/JavaScript-Module-Pattern-In-Depth.html

> https://reflectoring.io/nodejs-modules-imports/

> https://hacks.mozilla.org/2015/08/es6-in-depth-modules/

> https://exploringjs.com/es6/ch_modules.html

> https://tc39.es/ecma262/multipage/ecmascript-language-scripts-and-modules.html#sec-ecmascript-language-scripts-and-modules