nuxt3中如何处理与ESM兼容的问题

nuxt3中如何处理与ESM兼容的问题

这篇教程将告诉你什么是ES Modules,以及如何在nuxt应用中处理与ESM兼容的问题。

一、问题背景

CommonJS Modules

CommonJS Modules(CJS)格式是由Node.js提出的,此格式允许独立模块之前共享功能。 你可以早就知道这个下面代码中的语法了。

const a = require('./a')

module.exports.a = a

像webpack和rollup这样的打包工具,是支持这种语法的,并且允许你在浏览器中使用cjs格式的模块。

ESM Syntax

大多数时候,当人们谈论ESM与CJS时,他们谈论的都是关于写模块时的语法区别。
例如:

import a from './a'

export { a }

在ECMAScript Modules(ESM)成为标准前(花了超过10年的时间), 类似webpack的工具和像TypeScript的语言就已经开始支持所谓的 ESM 语法了。然而它们还是与真正的标准有些关键的不同的地方,具体哪些不同在这里有说明

什么是’native’ ESM?

你可能已经长期使用ESM语法写应用了。毕竟浏览器和nuxt2都原生支持ESM语法。 在nuxt2中你的代码将被编译成合适的格式(服务端为CJS, 浏览器端为ESM格式)

当你使用安装在你的包里的模块时,与通常的使用方式相比还是有点区别的。 例如一个简单的库可能会同时提供CJS和ESM的版本,这样使用者可以根据自己的需要来导入使用。

{
  "name": "sample-library",
  "main": "dist/sample-library.cjs.js",
  "module": "dist/sample-library.esm.js"
}

这样在nuxt2中,打包工具webpack 可以在服务端构建环节使用模块中的CJS 格式的文件(‘main’), 在客户端构建环节使用ESM格式的文件(‘module’)

然而在Nodejs最近的版本中, 已经支持使用原生的ESM模块了。这就意味着Node.js自己本身就可以执行使用ESM语法的javascript了。 虽然这个特性不是默认就开启的。

有两种方式开启此特性:

  • 在你的package.json文件中添加type: 'module', 并且保持使用.js后缀。
  • 使用.mjs后缀名, 推荐使用这种。

在Node.js上下文中,哪种模块的引入方式是有效的呢?

当你使用import而不是require引入模块时。Node.js解析此模块时还是有区别的。例如,当你导入sample-library, Node.js将不会去寻找main指向的入口文件,而是查找模块库里的package.json文件中的exports或者module入口。

这种情况在动态导入模块的时候也是一样的 const b = await import('sample-library').

Node 支持下面几种导入

  1. .mjs结尾的文件 — 导入这种文件表示是期望使用的是ESM 语法
  2. .cjs结尾的文件 — 导入这种文件表示是期望使用的是CJS 语法
  3. .js结尾的文件 — 表示期望使用的是CJS语法,除非在他们的package.json中有type: 'module'属性。

上面这种导入逻辑会产生什么样的问题呢?

在很长的时间里,模块开发的作者通常在生成ESM语法的版本中, 在package.json的module字段中使用.esm.js或者.es.js文件后缀名为入口文件。这个习惯一直以来也没有什么问题,直到现在。 因为现在这些模块库会被使用在webpack这样的打包工具中,这些打包工具并不会特别处理这种后缀的入口文件。

然而,如果你尝试在一个node.js 的esm上下文中导入一个.esm.js的包,就会出现如下错误:

(node:22145) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension.
/path/to/index.js:1

export default {}
^^^^^^

SyntaxError: Unexpected token 'export'
    at wrapSafe (internal/modules/cjs/loader.js:1001:16)
    at Module._compile (internal/modules/cjs/loader.js:1049:27)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1114:10)
    ....
    at async Object.loadESM (internal/process/esm_loader.js:68:5)

如果你使用命名导入的方式导入一个ESM语法的版本的库时,Node.js会认为这个是CJS语法的,结果会报出如下错误:

file:///path/to/index.mjs:5
import { named } from 'sample-library'
         ^^^^^
SyntaxError: Named export 'named' not found. The requested module 'sample-library' is a CommonJS module, which may not support all module.exports as named exports.

CommonJS modules can always be imported via the default export, for example using:

import pkg from 'sample-library';
const { named } = pkg;

    at ModuleJob._instantiate (internal/modules/esm/module_job.js:120:21)
    at async ModuleJob.run (internal/modules/esm/module_job.js:165:5)
    at async Loader.import (internal/modules/esm/loader.js:177:24)
    at async Object.loadESM (internal/process/esm_loader.js:68:5)

问题解决办法

如果你碰到这些问题,就需要模块的开发者去修复这个问题。但同时也可以使用其它解决办法。

Transpiling Libraries

如果出现这个情况,你可以在nuxt配置中的build.transpile选择中添加你依赖的库,这个就告诉nuxt不要尝试导入这些库。

export default defineNuxtConfig({
  build: {
    transpile: ['sample-library']
  }
})

添加完后,你可能会发现你还需要添加这个模块依赖的模块。

Aliasing Libraries

在某些情况下,你可能需要手动别名这个库的CJS版本,例如:

export default defineNuxtConfig({
  alias: {
    'sample-library': 'sample-library/dist/sample-library.cjs.js'
  }
})

默认的导出

如果使用的是CJS格式的依赖的话,你可以使用module.exports或者exports来进行模块的默认的导出:

// node_modules/cjs-pkg/index.js
module.exports = { test: 123 }
// or
exports.test = 123

如果使用或者依赖这种格式的模块一般需要使用 require进行导入:

// test.cjs
const pkg = require('cjs-pkg')

console.log(pkg) // { test: 123 }

原生ESM模式下的Node.js、开启esModuleInterop的typescript以及类似于webpack的打包工具等等都会提供对上面使用方式兼容的机制,所以我们可以使用ESM语法里的导入方式导入这种格式的模块库。这种机制通常被称为interop require default

import pkg from 'cjs-pkg'

console.log(pkg) // { test: 123 }

但是由于语法检测的复杂性和打包格式的不同,interop require default机制有时候也会有失败的时候,例如:

import pkg from 'cjs-pkg'

console.log(pkg) // { default: { test: 123 } }  多了一层default

这种失败的情况同样也会出现在动态导入的使用中

import('cjs-pkg').then(console.log) // [Module: null prototype] { default: { test: '123' } }

对于这种情况,就需要手动处理默认导出了:

// Static import
import { default as pkg } from 'cjs-pkg'

// Dynamic import
import('cjs-pkg').then(m => m.default || m).then(console.log)

为了更加安全的处理更为复杂的情况,推荐在nuxt3中使用mlly:

import { interopDefault } from 'mlly'

// Assuming the shape is { default: { foo: 'bar' }, baz: 'qux' }
import myModule from 'my-module'

console.log(interopDefault(myModule)) // { foo: 'bar', baz: 'qux' }

对库的开发的指导建议

对于库的开发者来说,解决ESM 兼容性的问题相对来说还是比较简单的。目前主要有两种解决方法:

  1. 你可以以.mjs后缀名来重命名你的ESM文件。
    这是一个推荐的而且也非常简单的方法。但同时你也不得不去解决你的库使用的依赖包中包含的此问题。但大多数情况下,这种方法就可以解决你的问题。 强烈建议你将CJS文件用.cjs后缀来命名。
  2. 你可以选择将你的库的入口变成ESM-only
    这种方法就是在你的package.json文件中设置type:'module' ,并且确保你的库中的代码都是使用的ESM语法。 但是可能你依赖的那个库会出现种问题。所以这种方式只有当你确定你的依赖库没有这种问题时才能使用。

语法迁移

从CJS语法迁移到ESM的第一步就要要将require换成import
模块导出:

// 之前
module.exports = ...

exports.hello = ...

// 之后
export default ...

export const hello = ...

模块导入:

// 之前
const myLib = require('my-lib')

// 之后
import myLib from 'my-lib'
// or
const myLib = await import('my-lib').then(lib => lib.default || lib)

与CJS语法标准不一样的是,在ESM Modules中,require,require.resolve,__filename__dirname这些关键字和常量不能使用了, 都替换成import()import.meta.filename.
例如:

// 之前
import { join } from 'path'

const newDir = join(__dirname, 'new-dir')

// 之后
import { fileURLToPath } from 'node:url'

const newDir = fileURLToPath(new URL('./new-dir', import.meta.url))
// 之前
const someFile = require.resolve('./lib/foo.js')

// 之后
import { resolvePath } from 'mlly'

const someFile = await resolvePath('my-lib', { url: import.meta.url })




版权声明:本文为凸然网站的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:nuxt3中如何处理与ESM兼容的问题

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值