🌑

Mocha's Blog

目录
  1. 你真的会 Import 吗
    1. 其他用法
  2. Export 有几种写法?
    1. 第一种写法
    2. 第二种写法
    3. 其他用法
  3. 拓展 - 前端模块化

再看ESModule

发布时间:2023年10月13日

ESModule 作为 ECMA 推出的通用模块化标准,随着在 NodeJs 场景下的普及以及浏览器的原生支持,目前已覆盖前端开发所有的场景,但你真的了解它所有的使用方法吗?
这篇分享,让我们从几个实际发生的场景出发去探索:

你真的会 Import 吗

image.png

在上图代码里,我们实现 在 Utils.ts 中输出一些通用的工具类,在其他文件中使用其中方法时,有如下两块种引入方式,哪种方式才是正确的?

import { isValidData, getRequiredRule } from './Utils';
import Utils from './Utils';

const  { isValidData, getRequiredRule } = Utils;

我们在使用如 antdProComponents 之类组件库时一般都使用第一种的写法,因此可能默认带入解构的思维惯性,但这种写法是错误的,应该按照第二种方式来做。
要解答这个问题,我们先看下 MDN 上关于使用 export 默认导出内容场景下的例子:

// 默认导出
export default expression;
export default function () {} // also class, function*
export default function name1() {} // also class, function*
export { name1 as default,};

在实例中,有一个语法可能会引起你的注意: export { name as default, ... },这个语法其实和 export default name 是完全等价的,也就是说 default 其实只是 export 的一个保留字。
看到这里我们再回过头来看上图中的 Utils.ts ,其导出实际上可以写成这样

const utils = { isValidData, getRequiredRule, getMaxLengthRule };

export { utils as default };

我们再看下 MDN 上关于 import 引入 default 的例子中,可以看到两种写法:

// 写法一
import Utils from "./Utils";

// 写法二
import { default as Utils } from "./Utils";

将两者结合起来看,问题就很简单了,我们在 Utils.ts 中通过只暴露了 default 保留字指向的值,而并没有暴露 key 为 isValidData 等内容的值,所以无法直接通过解构的方式来拿到

在 JavaScript 中,一个文件被视为一个 模块,每个模块都可以有其自己的 export,不论我们如何组织 export 内容,这些内容最终都会被组织到一个大的 export 中,通过 export { xxx as default, yyy, zzz } 的方式暴露给其他模块使用。

其他用法

// 将其他模块内容作为一个整体导入
import * as FullImport from 'library';

// 导入时解构并重命名
import { originExportName as newImportName } from 'library';

// 将模块作为副作用引入,不导入模块的任何值,仅运行文件代码,如 less 文件
import './effect.ts'

// 动态导入默认 export 时,需要显式的解构 default 再使用
const { default: defaultExport } = await import('./library.js');

Export 有几种写法?

export 模块,我们还是拿之前的 Utils.ts 来举例

image.png

在前面提到过 export default 等于 export { xx as default } ,那么我们如果想通过结构的方式拿到对应函数就很简单了 —— 不要通过 default 暴露,有两种写法:

  1. 独自导出,即每一个函数单独导出,如:export const isValidData = () => {}
  2. 统一导出,即在底部统一进行导出,如:export { isValidData }
    按照上述方法,我们就可以在业务项目中通过 import { isValidData } from './Utils' 的方式拿到对应的工具函数。

至此可能还有一个问题,怎么在 Utils.ts 中把 format.js 的方法也暴露出去并能以结构的方式拿到?
很简单,全模块引入,再导出,那么就会出现下面这种写法:

image.png

但在实际编写代码时,编辑器会报错,通过查看文档和思考的话,我们会发现 export defaultexport {} 导出的内容完全是两个概念:

  • export default 中暴露出去的是一个变量,这个变量可以是常量、函数、类,因此可以带有计算逻辑,比如:export default { ...FormatUtils }
  • 普通 export 导出的是变量列表,这个列表中的变量在导出时应当是确定的,不能带有计算逻辑
    有了以上共识就可以看下,如果想实现我们先聚合再导出的目的,用 export 有几种写法了

第一种写法

前面提到使用 export default 时可以进行计算,那么第一种写法就有,如下:

import FormatUtils from './format';

const isValidData = () => { ... };

export default {
  isValidData,
  ...FormatUtils
}
import Utils from './Utils';

const {
  isValidData,
  formatXxx
} = Utils;

借助 default 变量可以进行计算的特点,我们实现了聚合导出的目的,但没有实现在 import 时解构引用的能力

第二种写法

前面在 import 的其他用法中提到聚合导入的 import * 语法,那么肯定有与之对应的 export * 语法,借助这个语法,我们可以将 format.ts 文件中提供的方法聚合至 Utils.ts 文件的导出变量之中,写法如下:

export * from './format';

const isValidData = () => { ... };

export { isValidData }
import {
  isValidData,
  formatXxx
} from './Utils';

这样我们在既实现聚合导出的目的基础上也实现了导入时的解构能力。

其他用法

// 模块内变量导出时重命名变量
export { variable1 as name1, variable2 as name2 }

// 从某对象中解构导出并重命名
export const { name1, name2: rename } = Object

// 导出模块合集并聚合至当前模块的导出列表内,引入时以变量原始名称进行引入
export * from './format'

//导出模块合集并集合至一个变量聚合至当前模块的导出列表内,引入时以 as 后的名称进行引入
export * as FormatUtils from './format'

// 导出模块合集、重命名并聚合在当前模块导出列表内,引入时以 as 后的名称进行引入
export { default as FormatDefault, import1 as export1, import2 as export2 } from './format'

拓展 - 前端模块化

前端模块化一共有以下几种实现:

  • CommonJS:适用于服务端,加载本地文件,运行时加载,支持动态加载模块,语法 module.exports = ,在 NodeJS 中可以以 .cjs 作为文件后缀
  • AMD(Asynchronous Module Definition):异步模块定义,以异步方式加载模块,不阻塞后面语句执行,通过 define 关键字定义模块,以 return 关键字返回导出变量,以 require 关键字引入模块,需要引入 require.js 作为运行时实现模块的运行
// amd define
define(function() {
  return {
    hello: function() {
      return "hello module";
    },
  };
});

// amd require
define(function() {
  const helloModule = require('./module.js');

  helloModule.hello();
});
  • CMD(Common Module Definition):通用模块定义,以 define 关键字定义模块,通过对 define callbackexports 参数的调用实现暴露,以 require 实现调用,需要引入 sea.js 作为运行时实现模块运行
// cmd define
define(function(require, exports, module) {
  exports.hello = function() {
    return "hello module";
  };
});

// cmd require
define(function(require, exports, module) {
  const helloModule = require("./module.js");

  helloModule.hello()
});
  • UMD(Universal Module Definition):通用模块定义,没有引入新的模块定义概念,本质上是一个兼容 CommonJSAMD 两种模块的运行时,目前前端项目打包基本都是以此为基准
(function(global, factory) {
  typeof exports === "object" && typeof module !== "undefined"
    ? (module.exports = factory())
    : typeof define === "function" && define.amd
    ? define(factory)
    : ((global = global || self), (global.myBundle = factory()));
})(this, function() {
  "use strict";

  var main = () => {
    return "hello world";
  };

  return main;
});
  • ESModule:ECMAScript 官方标准化模块系统,编译时的模块化系统,浏览器原生支持,在引入时不支持本地文件类型引入,必须是一个标准的 url,目前 NodeJS 已经支持 ESM,在 NodeJS 项目中文件通常以 .mjs 为后缀

Powered By Hexo.js Hexo and Minima. Support By Oracle & Docker-Compose.