透過超過 100 個技巧的集合學習 Nuxt!

ES 模組

Nuxt 使用原生 ES 模組。

本指南將說明什麼是 ES 模組,以及如何使 Nuxt 應用程式(或上游函式庫)與 ESM 相容。

背景

CommonJS 模組

CommonJS (CJS) 是 Node.js 引入的一種格式,允許在隔離的 JavaScript 模組之間共享功能 (了解更多)。您可能已經熟悉此語法

const a = require('./a')

module.exports.a = a

像 webpack 和 Rollup 這樣的打包工具支援此語法,並允許您在瀏覽器中使用以 CommonJS 編寫的模組。

ESM 語法

大多數時候,當人們談論 ESM 與 CJS 時,他們談論的是一種用於編寫 模組 的不同語法。

import a from './a'

export { a }

在 ECMAScript 模組 (ESM) 成為標準之前(花了超過 10 年!),像 webpack 這樣的工具,甚至是像 TypeScript 這樣的語言,都開始支援所謂的 **ESM 語法**。但是,與實際規格有一些關鍵差異;這裡是 一個有用的說明

什麼是「原生」ESM?

您可能已經使用 ESM 語法編寫應用程式很長時間了。畢竟,它受到瀏覽器的原生支援,並且在 Nuxt 2 中,我們將您編寫的所有程式碼編譯為適當的格式(伺服器的 CJS,瀏覽器的 ESM)。

當將模組添加到您的套件時,情況會有些不同。一個範例函式庫可能會公開 CJS 和 ESM 版本,並讓我們選擇我們想要的版本

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

因此在 Nuxt 2 中,打包工具 (webpack) 會將 CJS 檔案 ('main') 拉入伺服器建置,並將 ESM 檔案 ('module') 用於客戶端建置。

但是,在最近的 Node.js LTS 版本中,現在可以在 Node.js 中 使用原生 ESM 模組。這表示 Node.js 本身可以使用 ESM 語法處理 JavaScript,儘管預設情況下它不會這樣做。啟用 ESM 語法的兩種最常見方法是

  • 在您的 package.json 中設定 "type": "module" 並繼續使用 .js 副檔名
  • 使用 .mjs 檔案副檔名(建議)

這就是我們對 Nuxt Nitro 所做的事情;我們輸出一個 .output/server/index.mjs 檔案。這告訴 Node.js 將此檔案視為原生 ES 模組。

在 Node.js 環境中,哪些是有效的引入?

當您 import 一個模組而不是 require 它時,Node.js 會以不同的方式解析它。例如,當您引入 sample-library 時,Node.js 將不會尋找 main,而是尋找該函式庫 package.json 中的 exportsmodule 條目。

動態引入也是如此,例如 const b = await import('sample-library')

Node 支援以下幾種引入方式(請參閱 文件

  1. .mjs 結尾的檔案 - 這些檔案預期使用 ESM 語法
  2. .cjs 結尾的檔案 - 這些檔案預期使用 CJS 語法
  3. .js 結尾的檔案 - 除非其 package.json 具有 "type": "module",否則這些檔案預期使用 CJS 語法

可能會有什麼樣的問題?

長期以來,模組作者一直在產生 ESM 語法建置,但使用諸如 .esm.js.es.js 之類的慣例,他們已將其添加到其 package.json 中的 module 欄位中。到目前為止,這還不是問題,因為它們僅被像 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)

如果您從 Node.js 認為是 CJS 的 ESM 語法建置中命名引入,則也可能會收到此錯誤

終端機
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)

排除 ESM 問題

如果遇到這些錯誤,則問題幾乎可以肯定出在上游函式庫中。他們需要修正他們的函式庫以支援被 Node 引入。

轉譯函式庫

同時,您可以告訴 Nuxt 不要嘗試引入這些函式庫,方法是將它們添加到 build.transpile

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

您可能會發現您需要新增這些函式庫正在匯入的其他套件。

函式庫別名

在某些情況下,您可能還需要手動將函式庫設定別名為 CJS 版本,例如

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

預設匯出

具有 CommonJS 格式的依賴項,可以使用 module.exportsexports 來提供預設匯出

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 之類的打包工具,都提供了一種相容性機制,讓我們可以使用預設匯入來引入此函式庫。此機制通常被稱為「interop require default」。

import pkg from 'cjs-pkg'

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

然而,由於語法偵測和不同打包格式的複雜性,總是存在 interop default 失敗並導致出現如下情況的可能性

import pkg from 'cjs-pkg'

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

另外,當使用動態匯入語法(在 CJS 和 ESM 檔案中)時,我們總是會遇到這種情況

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

在這種情況下,我們需要手動 interop 預設匯出

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

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

為了處理更複雜的情況並提高安全性,我們建議並在 Nuxt 內部使用 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. 您可以將 ESM 檔案重新命名為以 .mjs 結尾。
    這是建議且最簡單的方法。 您可能需要解決函式庫的依賴項問題,以及可能與您的建置系統相關的問題,但在大多數情況下,這應該可以解決您的問題。為了最大的明確性,也建議將您的 CJS 檔案重新命名為以 .cjs 結尾。
  2. 您可以選擇讓整個函式庫僅限 ESM.
    這表示要在您的 package.json 中設定 "type": "module",並確保您建置的函式庫使用 ESM 語法。但是,您可能會遇到與依賴項相關的問題 - 而這種方法表示您的函式庫只能在 ESM 環境中使用。

遷移

從 CJS 到 ESM 的初始步驟是將任何使用 require 的地方更新為使用 import

module.exports = ...

exports.hello = ...
const myLib = require('my-lib')

在 ESM 模組中,與 CJS 不同,requirerequire.resolve__filename__dirname 全域變數不可用,應替換為 import()import.meta.filename

import { join } from 'path'

const newDir = join(__dirname, 'new-dir')
const someFile = require.resolve('./lib/foo.js')

最佳實踐

  • 優先使用具名匯出而不是預設匯出。這有助於減少 CJS 衝突。(請參閱預設匯出章節)
  • 盡可能避免依賴 Node.js 內建物件和 CommonJS 或僅限 Node.js 的依賴項,以使您的函式庫可以在瀏覽器和 Edge Workers 中使用,而無需 Nitro polyfill。
  • 使用新的 exports 欄位和條件匯出。(閱讀更多)。
{
  "exports": {
    ".": {
      "import": "./dist/mymodule.mjs"
    }
  }
}