JS模块:ESM

来自Wikioe
跳到导航 跳到搜索


关于[1]

ESM(ECMAScript Module)在 2015 年的时候出现于 ES-6 中,逐渐取代 AMD、UMD、CJM(CommonJS)。


参考:JS模块:ESM 与 CJM

一个模块(module)就是一个文件,模块间通过 export 和 import 互相加载。

export:导出[2]

export 语句:用于从模块中导出实时绑定的函数、对象或原始值,以便其他程序可以通过 import 语句使用它们。

—— 被导出的绑定值依然可以在本地进行修改。
—— 在使用 import 进行导入时,这些绑定值只能被导入模块所读取,但在 export 导出模块中对这些绑定值进行修改,所修改的值也会实时地更新。
  1. 导出单个接口
    // 导出变量名
    export let name1, name2, , nameN; // also var, const
    // 导出表达式
    export let name1 = , name2 = , , nameN; // also var, const
    // 导出方法
    export function FunctionName(){...}
    // 导出类
    export class ClassName {...}
    
  2. 导出多个接口
    export { name1, name2, , nameN };
    
  3. 重命名导出
    export { variable1 as name1, variable2 as name2, , nameN };
    
    • 在导入期间,必须使用相应对象的相同名称
  4. 解构导出并重命名
    export const { name1, name2: bar } = o;
    
  5. 默认导出
    export default expression;
    export default function () {  } // also class, function*
    export default function name1() {  } // also class, function*
    export { name1 as default,  };
    
    • 一个模块只能有一个默认导出
  6. 导出模块合集
    export * from ; // does not set the default export
    export * as name1 from ; // Draft ECMAScript® 2O21
    export { name1, name2, , nameN } from ;
    export { import1 as name1, import2 as name2, , nameN } from ;
    export { default } from ;
    

import:静态导入[3]

import 语句:用于导入由另一个模块导出的绑定。

—— 无论是否声明了 strict mode,导入的模块都运行在“严格模式”下。
—— 在浏览器中,import 语句只能在声明了 type="module" 的 <script> 的标签中使用。
  1. 导入单个接口
    import {myExport} from '/modules/my-module.js';
    
  2. 导入多个接口
    import {foo, bar} from '/modules/my-module.js';
    
  3. 重命名导入
    import {exportName1 as name1, exportName2 as name2} from '/modules/my-module.js';
    
  4. 导入一个模块作为“副作用”
    import '/modules/my-module.js';
    
    • 运行模块中的全局代码,但实际上不导入任何值
  5. 导入默认值
    // 仅导入默认值
    import myDefault from '/modules/my-module.js';
    
    // 导入默认值与其他接口
    import myDefault, * as myModule from '/modules/my-module.js';
    import myDefault, {foo, bar} from '/modules/my-module.js';
    
  6. 导入模块合集
    import * as myModule from '/modules/my-module.js';
    
    • 通过该对象的属性访问模块接口
  7. 重导出
    export { default as function1, function2 } from 'bar.js';
    
    等同于“联合使用 import、export”:
    import { default as function1, function2 } from 'bar.js';
    export { function1, function2 };
    

import():动态导入[4]

import() 方法:通常称为“动态导入”,是一个类似函数的表达式,它将返回一个 Promise —— 【常配合 async/await 使用】

—— 用于将 ECMAScript 模块异步、动态地加载到潜在的非模块环境中。
——仅在必要时使用“动态导入”——【“静态导入”更适合于加载初始依赖项,利于静态分析工具和“tree shaking”(树抖动)】
  1. 导入一个模块作为“副作用”
    (async () => {
      if (somethingIsTrue) {
        const {
          default: myDefault,
          foo,
          bar,
        } = await import("/modules/my-module.js");
      }
    })();
    
    • 运行模块中的全局代码,但实际上不导入任何值
  2. 响应用户操作按需导入
    const main = document.querySelector("main");
    for (const link of document.querySelectorAll("nav > a")) {
      link.addEventListener("click", (e) => {
        e.preventDefault();
    
        import("/modules/my-module.js")
          .then((module) => {
            module.loadPageInto(main);
          })
          .catch((err) => {
            main.textContent = err.message;
          });
      });
    }
    
  3. 根据环境导入不同的模块
    let myModule;
    
    if (typeof window === "undefined") {
      myModule = await import("module-used-on-server");
    } else {
      myModule = await import("module-used-in-browser");
    }
    
  4. 导入带有非文字说明符的模块
    Promise.all(
      Array.from({ length: 10 }).map((_, index) =>
        import(`/modules/module-${index}.js`),
      ),
    ).then((modules) => modules.forEach((module) => module.load()));
    
尽管 import() 看起来像一个函数调用,但它只是一种特殊语法,只是恰好使用了括号(类似于 super())。

因此,不能将 import 复制到一个变量中,或者对其使用 call/apply —— 因为它不是一个函数!!!

import.meta:元数据属性[5]

import.meta是一个给 JavaScript 模块暴露特定上下文的元数据属性的对象。它包含了这个模块的信息,比如说这个模块的 URL。
  1. import.meta.url:当前模块的 URL 路径
  2. import.meta.scriptElement:(浏览器特有)加载模块的那个 <script> 元素

相关用法

聚合重导出

即:在一个模块中,聚合多个模块的导出

childModule1.js 中:

let myFunction = ...; // assign something useful to myFunction
let myVariable = ...; // assign something useful to myVariable
export {myFunction, myVariable};

childModule2.js 中:

let myClass = ...; // assign something useful to myClass
export myClass;

parentModule.js 中:聚合重导出

export { myFunction, myVariable } from 'childModule1.js';
export { myClass } from 'childModule2.js';

顶层模块中:从 parentModule.js 模块导入

import { myFunction, myVariable, myClass } from 'parentModule.js'

<script>中嵌入[6]

在浏览器中,import 语句只能在声明了 type="module" 的 script 的标签中使用。

相关属性:

  1. src:定义引用外部脚本的 URI,可以用来代替直接在文档中嵌入脚本。
    • 指定该属性的 <script> 元素标签内不应该再有嵌入的脚本;
  2. type:定义 <script> 元素包含或 src 引用的脚本语言。
    • 支持的 MIME 类型包括:text/javascript、text/ecmascript、application/javascript、application/ecmascript;
    • 如果 type 属性为“module”,代码会被当作“JavaScript 模块”
  3. async:如果存在该属性,那么“普通脚本”会被并行请求,并尽快解析和执行
    • 对于“模块脚本”:脚本及其所有依赖都会在延缓队列中执行,因此它们会被并行请求,并尽快解析和执行。
  4. defer:如果存在该属性,脚本将在文档完成解析后,触发 DOMContentLoaded 事件前执行
    • 该属性会阻止 DOMContentLoaded 事件,直到脚本被加载并且解析完成。
💡若需要异步加载,就必须使用 async / defer:

——(脚本的加载,都是异步进行)

—— async:
    1、“执行脚本”与“文档解析”同时进行;——(脚本加载完成,就中断“文档解析”转而“执行脚本”)
    2、脚本按照“加载顺序”执行(先加载完成,先执行)——【无法保证脚本间执行顺序】

—— defer:
    1、“执行脚本”在“文档解析”完成之后;
    2、脚本按照“定义顺序”执行(前面定义的,先执行)——【可以保证脚本间执行顺序】


📜默认情况下:

—— 对于“普通脚本”:浏览器是同步加载,即:渲染引擎遇到 <script> 标签就会停下来,等到脚本加载、执行完毕,再继续向下渲染。

—— 对于“模块脚本”:浏览器是异步加载(相当于使用了“defer”),此时可以同时使用“async”。

示例:

  1. 加载外部模块:
    <script type="module" src="./foo.js"></script>
    
    <!-- 等同于 -->
    
    <script type="module" src="./foo.js" defer></script>
    
  2. 加载外部模块,并尽快执行:
    <script type="module" src="./foo.js" async></script>
    
  3. 在内嵌脚本中加载模块:
    <script type="module">
        import utils from "./utils.js";
    
        // other code
    </script>
    

参考