Модули ES

Nuxt используют нативные модули ES.

Это руководство поможет вам понять, что такое модули ES (ESM) и как сделать приложение 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 теперь можно использовать нативный модуль ESM в Node.js. Это означает, что сам Node.js может обрабатывать JavaScript с использованием синтаксиса ESM, хотя по умолчанию он этого не делает. Два наиболее распространенных способа включить синтаксис ESM:

  • установите "type": "module" в package.json и продолжайте использовать расширение .js
  • используйте расширения файлов .mjs (рекомендуется)

Это то, что мы делаем для Nitro в Nuxt; мы получаем на выходе файл .output/server/index.mjs. Это говорит Node.js, что этот файл нужно рассматривать как нативный модуль ES.

Что такое допустимые импорты в контексте Node.js?

Когда вы делаете import модуля, а не require, Node.js разрешает его по-другому. Например, когда вы импортируете sample-library, Node.js будет искать не main, а exports или module запись в package.json этой библиотеки.

Это также справедливо для динамического импорта, например const b = await import('sample-library').

Node поддерживает следующие виды импорта (см. документацию):

  1. файлы, заканчивающиеся на .mjs - ожидается, что они будут использовать синтаксис ESM
  2. файлы, заканчивающиеся на .cjs - ожидается, что они будут использовать синтаксис CJS
  3. файлы, заканчивающиеся на .js - ожидается, что они будут использовать синтаксис CJS, если только их package.json не имеет "type": "module"

Какие могут быть проблемы?

Долгое время авторы модулей создавали сборки с ESM-синтаксисом, но использовали соглашения вроде .esm.js или .es.js, которые они добавляли в поле module в своем package.json. До сих пор это не было проблемой, поскольку они использовались только сборщиками, такими как webpack, которые не особо заботятся о расширении файла.

Однако если вы попытаетесь импортировать пакет с файлом .esm.js в ESM-контексте Node.js, это не сработает, и вы получите ошибку следующего вида:

Terminal
(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:

Terminal
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.exports или exports для предоставления экспорта по умолчанию:

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

Обычно это работает хорошо, если мы делаем require такой ​​зависимости:

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

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

Node.js в собственном режиме ESM, typescript с включенным esModuleInterop и упаковщики, такие как webpack, предоставляют механизм совместимости, чтобы мы могли импортировать такую ​​библиотеку по умолчанию. Этот механизм часто называют "interop require default":

import pkg from 'cjs-pkg'

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

Однако из-за сложностей определения синтаксиса и различных форматов пакетов всегда существует вероятность того, что взаимодействие по умолчанию не сработает, и мы получим что-то вроде этого:

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' } }

В этом случае нам необходимо вручную настроить экспорт по умолчанию:

// Статический импорт
import { default as pkg } from 'cjs-pkg'

// Динамический импорт
import('cjs-pkg').then(m => m.default || m).then(console.log)

Для обработки более сложных ситуаций и повышения безопасности мы рекомендуем и используем внутри Nuxt mlly, которая может сохранять именованные экспорты.

import { interopDefault } from 'mlly'

// Предположим, что форма - { 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.
    Это означало бы установку "type": "module" в package.json и обеспечение того, чтобы собранная библиотека использовала синтаксис ESM. Однако вы можете столкнуться с проблемами с зависимостями - и этот подход означает, что библиотека может быть использована только в контексте ESM.

Миграция

Первым шагом при переходе от CJS к ESM является изменение любого require на import:

module.exports = ...

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

В модулях ESM, в отличие от CJS, глобальные переменные require, require.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.
  • Использовать новое поле exports для условного экспорта. (Подробнее).
{
  "exports": {
    ".": {
      "import": "./dist/mymodule.mjs"
    }
  }
}