Node中 ES模块 还能这样用?

629 阅读6分钟

本文正在参与技术专题征文Node.js进阶之路,点击查看详情

前言

Node.js 是一个开源和跨平台的 JavaScript 运行时环境,诞生于 2009 年。

有人觉得 2022 年,Node.js似乎没有以往那么火了,但事实并非如此。从Node.js源码更新的内容来看,Node.js正在经历一个从性能好向着易用、好用的转变。Node.js社区重点工作组Node.js Diagnostics Working Group的核心产出 async_hooks、profiling、tracing、dump debug、report等都是在易用性和好用性上做提升,致力于让每个Node.js开发者以更低门槛来提升Node.js应用的开发体验。

今天我们就来看下 ES模块 在node中的应用吧

介绍

  • ECMAScript 模块是来打包 JavaScript 代码以供重用的官方标准格式。 模块使用各种 import 和 export 语句定义。

    • 以下是 ES 模块导出函数的示例:

      // addTwo.mjs
      function addTwo(num) {
        return num + 2;
      }
      
      export { addTwo };
      
    • 以下是 ES 模块从 addTwo.mjs 导入函数的示例:

      // app.mjs
      import { addTwo } from './addTwo.mjs';
      
      // 打印: 6
      console.log(addTwo(4));
      
  • Node.js 完全支持当前指定的 ECMAScript 模块,并且提供它们与其原始模块格式 CommonJS 之间的互操作性。

启用 es

  • Node.js 有两个模块系统:CommonJS 模块和 ECMAScript 模块。

  • 可以通过 .mjs 文件扩展名、package.json "type" 字段、或 --input-type 标志告诉 Node.js 使用 ECMAScript 模块加载器。 在这些情况之外,Node.js 将使用 CommonJS 模块加载器。

    • 添加 type方式

      image.png

      • node 16.13之后package.json新增了type属性,commonjsmodule
        • commonjs 表示启用 commonjs模式,这也是node的默认模式
        • module 表示启用 es模式
      • 示例

      image.png

      • 注意

        • 当我们使用es模块之后,在引入es模块的时候需要手动加上后缀名,否则会报错(默认走的是common规范)

        image.png

      • 这样就好了

    • 运行时添加--input-type="module"

      image.png

      • 注意
        • 在运行时添加 --input-type="module" 执行 es 模块的代码,那么文件的后缀名需要改成mjs

协议支持

说明

  • 支持 file:node: 和 data: URL 协议。 除非使用自定义的 HTTPS 加载器,否则 Node.js 本身不支持像 'https://example.com/app.js' 这样的说明符

file: URL

  • 如果用于解析模块的 import 说明符具有不同的查询或片段,则会多次加载模块。
import './foo.mjs?query=1'; // 加载具有 "?query=1" 查询的 ./foo.mjs
import './foo.mjs?query=2'; // 加载具有 "?query=2" 查询的 ./foo.mjs
  • 也可以通过 /// 或 file:/// 引用。 鉴于网址和路径解析的差异(例如百分比编码细节),建议在导入路径时使用 url.pathToFileURL

data: 导入

  • data: URL 支持使用以下 MIME 类型导入:

    • text/javascript 用于 ES 模块
    • application/json 用于 JSON
    • application/wasm 用于 Wasm
  • data: URL 只为内置模块解析裸说明符绝对说明符。 解析相对说明符不起作用,因为 data: 不是特殊协议。 例如,尝试从 data:text/javascript,import "./foo"; 加载 ./foo 无法解析,因为 data: URL 没有相对解析的概念。 正在使用的 data: URL 示例是:

    import 'data:text/javascript,console.log("hello!");';
    import _ from 'data:application/json,"world!"';
    

node: 导入

  • 支持 node: URL 作为加载 Node.js 内置模块的替代方法。 此 URL 协议允许有效的绝对的 URL 字符串引用内置模块。
import fs from 'node:fs/promises';

导入断言

  • 导入断言提案为模块导入语句添加了内联语法,以便在模块说明符旁边传入更多信息。
import fooData from './foo.json' assert { type: 'json' };

const { default: barData } =
  await import('./bar.json', { assert: { type: 'json' } });
  • Node.js 支持以下 type 值,其断言是强制性的:

    断言 type用于
    'json'JSON 模块
  • 示例

    image.png

    • 根据上图可以看到,node通过添加 断言可以很方便的导出json数据

import() 表达式

  • 动态的 import() 在 CommonJS 和 ES 模块中都受支持。 在 CommonJS 模块中它可以用来加载 ES 模块
  • 示例

image.png

import.meta

  • import.meta 元属性是包含url属性和resolve等方法的。

image.png

  • 使用es模块的时候__dirname__filename是无法使用的,可以通过内置url方法+import.meta.url实现对应功能
  • 示例
import path from 'node:path'
import url from 'node:url'
function filenameAndDirname(_url) {
    const filename = url.fileURLToPath(_url)
    const dirname = path.dirname(filename)
    return {
        filename,
        dirname
    }
}
const { filename, dirname } = filenameAndDirname(import.meta.url)
console.log(filename);
console.log(dirname);

image.png

与 CommonJS 的互操作性

import 声明

  • import 语句可以引用 ES 模块或 CommonJS 模块。 import 语句只允许在 ES 模块中使用,但 CommonJS 支持动态 import() 表达式来加载 ES 模块。

  • 当导入 CommonJS 模块 时,提供 module.exports 对象作为默认导出。 命名导出可能可用,由静态分析提供,以方便更好的生态系统兼容性。

es模块 导入 es模块

  • 示例

image.png

es模块 导入 commonJS模块

  • 示例

image.png

require

  • CommonJS 模块 require 总是将它引用的文件视为 CommonJS。

require 导入 CommonJS模块

  • 示例

image.png

  • 注意
    • 由于在package.json中设置了"type": "module",node会按照es模块解析代码
    • 所以app.js需要显示的改为app.cjs,表示当前文件按照CommonJS规范走

require 导入 es模块

  • 示例

image.png

  • 注意

    • require 导入 es模块 直接报错,写法问题
  • 不支持使用 require 加载 ES 模块,因为 ES 模块具有异步执行。 而是,使用 import() 从 CommonJS 模块加载 ES 模块。

CommonJS 命名空间

  • CommonJS 模块由可以是任何类型的 module.exports 对象组成。

  • 当导入 CommonJS 模块时,可以使用 ES 模块默认导入或其对应的语法糖可靠地导入

  • 示例一

image.png

  • 注意
    import { default as cjs } from './addTwo.cjs'
    // 下面的导入语句是上面的导入语句中
    // `{ default as cjsSugar }` 的 "语法糖"(等价但更方便):
    import { addTwo } from './addTwo.cjs'
    
  • 示例二

image.png

  • 注意
    • 由于在package.json中设置了"type": "module",node支持顶层await
    • CommonJS 模块的 ECMAScript 模块命名空间表示始终是使用 default 导出键指向 CommonJS module.exports 值的命名空间
  • 示例三

image.png

  • 注意
    • 当使用 import * as m from 'cjs' 或动态导入时,可以直接观察到此模块命名空间外来对象

CommonJS 兼容 JS生态写法

  • 为了更好地兼容 JS 生态系统中的现有用法,Node.js 还尝试确定每个导入的 CommonJS 模块的 CommonJS 命名导出,以使用静态分析过程将它们作为单独的 ES 模块导出提供。
  • 示例

image.png

  • 注意
    • 从上一个记录模块命名空间外来对象的示例中可以看出,name 导出是从 module.exports 对象复制出来的,并在导入模块时直接设置在 ES 模块命名空间上。

    • 未检测到这些命名导出的实时绑定更新或添加到 module.exports 的新导出。

    • 命名导出的检测基于通用语法模式,但并不总是正确地检测命名导出。 在这些情况下,使用上述默认导入形式可能是更好的选择。

    • 命名导出检测涵盖了许多常见的导出模式、再导出模式、以及构建工具和转译器输出。

ES 模块和 CommonJS 之间的差异

  • 没有 require、exports 或 module.exports
  • 没有 __filename 或 __dirname
  • 没有原生模块加载(es模块可以改用 module.createRequire() 或 process.dlopen。)
  • 没有 require.resolve
  • 没有 NODE_PATH
  • 没有 require.extension
  • 没有 require.cache

写在最后

  • node支持原生es模块能力,燥起来吧,解锁更多es新姿势
  • 虽然ES 模块和 CommonJS 之间仍然存在差异,绝大部分方法都有对应的es代替方法
  • 即使目前es不支持的属性比如(require.extensions),加载器钩子在未来依旧可以提供这个工作流
  • 共用es模块开发,大大减少了前端der的心智负担
  • 码字不易,点个攒👍吧