Краткое введение в Node.js: войдите, чтобы узнать
Два года назад я написал статью, знакомящую с системой модулей: Понимание концепции интерфейсных модулей: CommonJs и ES6Module. Знания в этой статье предназначены для новичков и относительно просты. Здесь мы также исправим несколько ошибок в статье:
[Модуль] и [Система модулей] — это две разные вещи. Модуль — это единица программного обеспечения, а система модулей — это набор синтаксиса или инструментов. Система модулей позволяет разработчикам определять и использовать модули в проектах.
Аббревиатура модуля ECMAScript — ESM или ESModule, а не ES6Module.
Базовые знания о модульной системе почти изложены в предыдущей статье, поэтому в этой статье основное внимание будет уделено внутренним принципам модульной системы и более полному представлению о различиях между различными модульными системами. Содержание предыдущей статьи находится в этой статье. больше не повторится.
Не все языки программирования имеют встроенную систему модулей, а JavaScript долгое время не имел системы модулей после своего рождения.
В среде браузера вы можете использовать тег <script>
только для добавления неиспользуемых файлов кода. Этот метод имеет глобальную область действия, что, можно сказать, полно проблем. В сочетании с быстрым развитием внешнего интерфейса этот метод не имеет значения. больше отвечает текущим потребностям. До появления официальной системы модулей фронтенд-сообщество создавало свою собственную стороннюю систему модулей. Наиболее часто используемые из них: асинхронное определение модуля AMD , универсальное определение модуля UMD и т. д. Конечно, самый известный из них — CommonJS .
Поскольку Node.js — это среда выполнения JavaScript, он может напрямую обращаться к базовой файловой системе. Поэтому разработчики приняли его и реализовали систему модулей в соответствии со спецификациями CommonJS.
Поначалу CommonJS можно было использовать только на платформе Node.js. С появлением инструментов упаковки модулей, таких как Browserify и Webpack, CommonJS наконец-то может работать на стороне браузера.
Лишь после выпуска спецификации ECMAScript6 в 2015 году появился формальный стандарт для модульной системы. Система модулей, построенная в соответствии с этим стандартом, для краткости называлась модулем ECMAScript (ESM). С тех пор ESM начала унифицироваться. среда Node.js и среда браузера. Конечно, ECMAScript6 предоставляет только синтаксис и семантику. Что касается реализации, то над ней должны поработать различные поставщики услуг браузеров и разработчики Node. Вот почему у нас есть артефакт Babel , которому могут позавидовать другие языки программирования. Реализация модульной системы — непростая задача. Node.js имеет относительно стабильную поддержку ESM только в версии 13.2.
Но несмотря ни на что, ESM — «сын» JavaScript, и в его изучении нет ничего плохого!
В эпоху «подсечного» земледелия для разработки приложений использовался JavaScript, а файлы сценариев можно было вводить только через теги сценариев. Одной из наиболее серьезных проблем является отсутствие механизма пространства имен, что означает, что каждый сценарий имеет одну и ту же область действия. В сообществе есть лучшее решение этой проблемы: модуль Revevaling.
const myModule = (() => { const _privateFn = () => {} const_privateAttr = 1 возвращаться { publicFn: () => {}, publicAttr: 2 } })() console.log(моймодуль) console.log(myModule.publicFn, myModule._privateFn)
Результаты бега следующие:
Этот шаблон очень прост: используйте IIFE для создания частной области и используйте возвращаемые переменные для предоставления. Внутренние переменные (такие как _privateFn, _privateAttr) недоступны из внешней области.
[модуль раскрытия] использует эти функции для сокрытия частной информации и экспорта API, которые должны быть доступны внешнему миру. Последующая модульная система также разработана на основе этой идеи.
На основе вышеизложенных идей разработайте загрузчик модулей.
Сначала напишите функцию, которая загружает содержимое модуля, оберните эту функцию в приватную область, а затем оцените ее с помощью eval() для запуска функции:
функция loadModule (имя файла, модуль, требуется) { const обернутыйSrc = `(функция (модуль, экспорт, требуется) { ${fs.readFileSync(имя файла, 'utf8)} }(модуль, модуль.экспорт, требуется)` оценка (обернутыйSrc) }
Как и [раскрытие модуля], исходный код модуля заключен в функцию. Разница в том, что в функцию также передается ряд переменных (модуль, модуль.экспорт, require).
Стоит отметить, что содержимое модуля считывается через [readFileSync]. Вообще говоря, вам не следует использовать синхронизированную версию при вызове API, затрагивающих файловую систему. Но на этот раз все по-другому, поскольку загрузка модулей через саму систему CommonJs должна быть реализована как синхронная операция, чтобы гарантировать возможность введения нескольких модулей в правильном порядке зависимостей.
Затем смоделируйте функцию require(), основная функция которой — загрузка модуля.
функция require(имямодуля) { const id = require.resolve(имямодуля) если (require.cache[id]) { вернуть require.cache[id].exports } // Метаданные модуля const modul = { экспорт: {}, ИДЕНТИФИКАТОР } //Обновление кеша require.cache[id] = модуль //Загружаем модуль loadModule(id,module,require) // Возвращаем экспортированные переменные return Module.exports } require.cache = {} require.resolve = (имямодуля) => { // Анализируем полный идентификатор модуля на основе имени модуля }
(1) После того, как функция получает имя модуля, она сначала анализирует полный путь к модулю и присваивает ему идентификатор.
(2) Если cache[id]
имеет значение true, это означает, что модуль загружен, и результат кэширования будет возвращен напрямую. (3) В противном случае среда будет настроена для первой загрузки. В частности, создайте объект модуля, включая экспорт (то есть экспортированный контент) и идентификатор (функция аналогична приведенной выше).
(4) Кэшируйте модуль, загруженный в первый раз. (5) Считайте исходный код из исходного файла модуля через loadModule. (6) Наконец, return module.exports
возвращает содержимое, которое вы хотите экспортировать.
При моделировании функции require есть очень важная деталь: функция require должна быть синхронной . Его функция заключается только в прямом возврате содержимого модуля и не использует механизм обратного вызова. То же самое относится и к require в Node.js. Следовательно, операция назначения для модуля.exports также должна быть синхронной. Если используется асинхронный вариант, возникнут проблемы:
// Что-то пошло не так setTimeout(() => { модуль.экспорт = функция () {} }, 1000)
Тот факт, что require является синхронной функцией, очень важно влияет на способ определения модулей, поскольку заставляет нас использовать только синхронный код при определении модулей, поэтому Node.js предоставляет для этой цели синхронные версии большинства асинхронных API.
В раннем Node.js была асинхронная версия функции require, но ее быстро удалили, поскольку она сильно усложняла функцию.
ESM является частью спецификации ECMAScript2015, которая определяет официальную систему модулей языка JavaScript для адаптации к различным средам выполнения.
По умолчанию Node.js рассматривает файлы с суффиксом .js как написанные с использованием синтаксиса CommonJS. Если вы используете синтаксис ESM непосредственно в файле .js, интерпретатор сообщит об ошибке.
Существует три способа преобразования интерпретатора Node.js в синтаксис ESM:
1. Измените расширение файла на .mjs;
2. Добавьте поле типа в последний файл package.json со значением «модуль»;
3. Строка передается в --eval
в качестве параметра или передается узлу через канал STDIN с флагом --input-type=module
например:
node --input-type=module --eval "import { sep } from 'node:path'; console.log(сентябрь);"
ESM можно анализировать и кэшировать как URL-адрес (что также означает, что специальные символы должны быть закодированы в процентах). Поддерживает протоколы URL, такие как file:
node:
и data:
файл: URL
Модуль загружается несколько раз, если спецификатор импорта, используемый для разрешения модуля, имеет разные запросы или фрагменты.
// Считается, что это два разных модуля import './foo.mjs?query=1'; импорт './foo.mjs?query=2';
данные: URL-адрес
Поддерживает импорт с использованием типов MIME:
text/javascript
для модулей ES
application/json
для JSON
application/wasm
для Wasm
import 'data:text/javascript,console.log("привет!");'; import _ from 'data:application/json,"world!"' Assert { type: 'json' };
data:URL
анализирует только голые и абсолютные спецификаторы встроенных модулей. Анализ относительных спецификаторов не работает, поскольку data:
не является специальным протоколом и не имеет концепции относительного анализа.
Утверждение импорта <br/>Этот атрибут добавляет встроенный синтаксис к оператору импорта модуля для передачи дополнительной информации рядом со спецификатором модуля.
импортировать fooData из './foo.json' Assert { type: 'json' }; const { default: barData } = await import('./bar.json', { Assert: { type: 'json' } });
В настоящее время поддерживается только модуль JSON, и синтаксис assert { type: 'json' }
является обязательным.
Импорт модулей Wash <br/>Импорт модулей WebAssembly поддерживается флагом --experimental-wasm-modules
, позволяющим импортировать любой файл .wasm как обычный модуль, а также поддерживать импорт их модулей.
// индекс.mjs импортировать * как M из './module.wasm'; консоль.журнал(М)
Используйте следующую команду для выполнения:
узел --experimental-wasm-modules index.mjs
Ключевое слово await можно использовать на верхнем уровне ESM.
// a.mjs экспортировать const Five = ждать Promise.resolve(5) // b.mjs импортировать {пять} из './a.mjs' console.log(пять) // 5
Как упоминалось ранее, разрешение зависимостей модулей в операторе импорта является статическим, поэтому у него есть два известных ограничения:
Идентификаторы модулей не могут ждать, пока время выполнения их создаст;
Операторы импорта модуля должны быть записаны в верхней части файла и не могут быть вложены в операторы потока управления;
Однако для некоторых ситуаций эти два ограничения, несомненно, слишком строгие. Например, есть относительно распространенное требование: отложенная загрузка :
Когда вы сталкиваетесь с большим модулем, вы хотите загружать этот огромный модуль только тогда, когда вам действительно нужно использовать определенную функцию в модуле.
Для этой цели ESM предоставляет механизм асинхронного внедрения. Эту вводную операцию можно выполнить с помощью оператора import()
во время работы программы. С синтаксической точки зрения это эквивалентно функции, которая получает идентификатор модуля в качестве параметра и возвращает обещание. После разрешения обещания можно получить проанализированный объект модуля.
Используйте пример циклической зависимости, чтобы проиллюстрировать процесс загрузки ESM:
// индекс.js импортировать * как foo из './foo.js'; импортировать * как панель из './bar.js'; console.log(фу); console.log(бар); // foo.js импортировать * как панель из './bar.js' экспорт пусть загружен = ложь; экспортировать константный бар = Бар; загружен = правда; //бар.js импортировать * как Foo из './foo.js'; экспорт пусть загружен = ложь; экспортировать const foo = Foo; загружено = правда
Сначала посмотрим на текущие результаты:
С помощью Loaded можно заметить, что оба модуля foo и bar могут регистрировать полную информацию о загруженном модуле. Но CommonJS — это другое дело. Должен быть модуль, который не может распечатать то, как он выглядит после полной загрузки.
Давайте углубимся в процесс загрузки, чтобы понять, почему возникает такой результат.
Процесс загрузки можно разделить на три этапа:
Первый этап: анализ
Второй этап: декларация
Третий этап: исполнение
Этап разбора:
Интерпретатор стартует с входного файла (то есть index.js), анализирует зависимости между модулями и отображает их в виде графа. Этот граф еще называют графом зависимостей.
На этом этапе мы фокусируемся только на операторах импорта и загружаем исходный код, соответствующий модулям, которые эти операторы хотят представить. И получить окончательный график зависимостей путем углубленного анализа. Для иллюстрации возьмите приведенный выше пример:
1. Начиная с index.js, найдите оператор import * as foo from './foo.js'
и перейдите к файлу foo.js.
2. Продолжайте анализ файла foo.js и найдите import * as Bar from './bar.js'
, перейдя таким образом к bar.js.
3. Продолжите анализ bar.js и найдите оператор import * as Foo from './foo.js'
, который образует циклическую зависимость. Однако, поскольку интерпретатор уже обрабатывает модуль foo.js, он не войдет в него. еще раз, а затем продолжите разбор модуля bar.
4. После анализа модуля bar обнаруживается, что оператор импорта отсутствует, поэтому он возвращается в foo.js и продолжает анализ. Оператор импорта снова не был найден, и был возвращен index.js.
5. import * as bar from './bar.js'
находится в index.js, но, поскольку bar.js уже проанализирован, он пропускается и выполнение продолжается.
Наконец, граф зависимостей полностью отображается с помощью подхода «в глубину»:
Фаза объявления:
Интерпретатор начинает с полученного графа зависимостей и объявляет каждый модуль по порядку снизу вверх. В частности, каждый раз, когда достигается модуль, выполняется поиск всех свойств, экспортируемых модулем, и идентификаторы экспортируемых значений объявляются в памяти. Обратите внимание, что на этом этапе выполняются только объявления и никакие операции присвоения не выполняются.
1. Интерпретатор запускается из модуля bar.js и объявляет идентификаторы loading и foo.
2. Проследите обратно до модуля foo.js и объявите идентификаторы загрузки и бара.
3. Мы подошли к модулю index.js, но в этом модуле нет оператора экспорта, поэтому идентификатор не объявлен.
После объявления всех идентификаторов экспорта еще раз просмотрите граф зависимостей, чтобы связать отношения между импортом и экспортом.
Видно, что между модулем, введенным при импорте, и значением, экспортируемым при экспорте, устанавливается связь привязки, подобная const. Импортирующая сторона может только читать, но не записывать. Более того, модуль bar, читаемый в index.js, и модуль bar, читаемый в foo.js, по сути, являются одним и тем же экземпляром.
Вот почему в результатах этого примера выводятся полные результаты синтаксического анализа.
Это фундаментально отличается от подхода, используемого системой CommonJS. Если модуль импортирует модуль CommonJS, система скопирует весь объект экспорта последнего и скопирует его содержимое в текущий модуль. В этом случае, если импортированный модуль изменяет свою собственную переменную копирования, пользователь не сможет увидеть новое значение. .
Этап исполнения:
На этом этапе движок выполнит код модуля. Доступ к графу зависимостей по-прежнему осуществляется снизу вверх, а файлы, к которым осуществляется доступ, выполняются один за другим. Выполнение начинается с файла bar.js, до foo.js и, наконец, до index.js. При этом значение идентификатора в таблице экспорта постепенно улучшается.
Этот процесс не сильно отличается от CommonJS, но на самом деле есть существенные различия. Поскольку CommonJS является динамическим, он анализирует граф зависимостей при выполнении связанных файлов. Поэтому, пока вы видите оператор require, вы можете быть уверены, что когда программа дойдет до этого оператора, все предыдущие коды были выполнены. Таким образом, оператор require не обязательно должен появляться в начале файла, он может появляться где угодно, а идентификаторы модулей также могут быть созданы из переменных.
Но ESM отличается. В ESM три вышеуказанных этапа отделены друг от друга, прежде чем он сможет выполнить код. Следовательно, операции внедрения модулей и экспорта модулей должны быть статическими. Не ждите, пока код выполнится.
Помимо нескольких отличий, упомянутых ранее, есть и некоторые отличия, на которые стоит обратить внимание:
При использовании ключевого слова импорта в ESM для разрешения относительных или абсолютных спецификаторов необходимо указать расширение файла и полностью указать индекс каталога ('./path/index.js'). Функция CommonJS require позволяет опустить это расширение.
По умолчанию ESM работает в строгом режиме, и этот строгий режим невозможно отключить. Таким образом, вы не можете использовать необъявленные переменные, а также не можете использовать функции, доступные только в нестрогом режиме (например, with).
CommonJS предоставляет некоторые глобальные переменные. Эти переменные нельзя использовать в ESM. Если вы попытаетесь использовать эти переменные, возникнет ошибка ReferenceError. включать
require
exports
module.exports
__filename
__dirname
Среди них __filename
относится к абсолютному пути к текущему файлу модуля, а __dirname
— к абсолютному пути к папке, в которой находится файл. Эти две переменные очень полезны при построении относительного пути к текущему файлу, поэтому ESM предоставляет некоторые методы для реализации функций двух переменных.
В ESM вы можете использовать объект import.meta
для получения ссылки, которая ссылается на URL-адрес текущего файла. В частности, путь к файлу текущего модуля получается через import.meta.url
. Формат этого пути аналогичен file:///path/to/current_module.js
. На основе этого пути строится абсолютный путь, выраженный __filename
и __dirname
:
импортировать { fileURLToPath } из 'url' импортировать { имя_каталога } из 'path' const __filename = fileURLToPath(import.meta.url) const __имя_каталога = имя_каталога(__имя_файла)
Он также может имитировать функцию require() в CommonJS.
импортировать { createRequire } из «модуля» const require = createRequire(import.meta.url)
В глобальной области ESM это значение не определено, но в системе модулей CommonJS это ссылка на экспорт:
//ESM console.log(this) // не определено // CommonJS console.log(this === экспортирует) // true
Как упоминалось выше, функцию CommonJS require() можно смоделировать в ESM для загрузки модулей CommonJS. Кроме того, вы также можете использовать стандартный синтаксис импорта для внедрения модулей CommonJS, но этот метод импорта может импортировать только те элементы, которые экспортируются по умолчанию:
import packageMain из 'commonjs-package' // Полностью возможно импортировать { метод } из 'commonjs-package' // Ошибка
Требование модуля CommonJS всегда рассматривает файлы, на которые он ссылается, как CommonJS. Загрузка модулей ES с помощью require не поддерживается, поскольку модули ES выполняются асинхронно. Но вы можете использовать import()
для загрузки модулей ES из модулей CommonJS.
Хотя ESM запущен уже 7 лет, node.js также стабильно его поддерживает. Когда мы разрабатываем библиотеки компонентов, мы можем поддерживать только ESM. Но для совместимости со старыми проектами также необходима поддержка CommonJS. Существует два широко используемых метода обеспечения поддержки экспорта библиотеки компонентов из обеих модульных систем.
Напишите пакеты в CommonJS или преобразуйте исходный код модуля ES в CommonJS и создайте файлы-оболочки модуля ES, которые определяют именованный экспорт. Используйте условный экспорт, импорт использует оболочку модуля ES, а require использует точку входа CommonJS. Например, в примере модуля
// пакет.json { "тип": "модуль", "экспорт": { "import": "./wrapper.mjs", "require": "./index.cjs" } }
Используйте расширения отображения .cjs
и .mjs
, поскольку использование только .js
либо по умолчанию будет использовать CommonJS, либо "type": "module"
приведет к тому, что эти файлы будут рассматриваться как модули ES.
// ./index.cjs экспорт.имя = 'имя'; // ./wrapper.mjs импортировать cjsModule из './index.cjs' экспортировать константное имя = cjsModule.name;
В этом примере:
// Используйте ESM, чтобы ввести import { name } из 'example' // Используйте CommonJS, чтобы ввести const { name } = require('example')
Имя, введенное обоими способами, является одним и тем же синглтоном.
Файл package.json может напрямую определять отдельные точки входа в модули CommonJS и ES:
// пакет.json { "тип": "модуль", "экспорт": { "импорт": "./index.mjs", "require": "./index.cjs" } }
Это можно сделать, если версии пакета CommonJS и ESM эквивалентны, например, потому что одна из них является транспилированным выводом другого, а управление состоянием пакета тщательно изолировано (или пакет не имеет состояния);
Причина, по которой статус является проблемой, заключается в том, что в приложении могут использоваться как версии пакета CommonJS, так и ESM, например, код реферера пользователя может импортировать версию ESM, тогда как для зависимости требуется версия CommonJS; Если это произойдет, в память будут загружены две копии пакета, поэтому возникнут два разных состояния. Это может привести к ошибкам, которые трудно устранить.
Помимо написания пакетов без сохранения состояния (например, если бы JavaScript Math был пакетом, он был бы без сохранения состояния, поскольку все его методы являются статическими), существуют способы изолировать состояние, чтобы его можно было использовать в потенциально загруженных CommonJS и ESM. экземпляры пакета:
Если возможно, включите все состояние в экземпляр объекта. Например, необходимо создать экземпляр Date в JavaScript, чтобы он содержал состояние. Если это пакет, он будет использоваться следующим образом:
импортировать дату из 'даты'; const someDate = новая дата(); // someDate содержит состояние; Дата не содержит
Ключевое слово new не требуется; функции пакета могут возвращать новые объекты или изменять переданные объекты для сохранения состояния вне пакета.
Изолируйте состояние в одном или нескольких файлах CommonJS, общих для версий пакета CommonJS и ESM. Например, точками входа для CommonJS и ESM являются index.cjs и index.mjs соответственно:
// index.cjs const state = require('./state.cjs') модуль.exports.state = состояние; // индекс.mjs импортировать состояние из «./state.cjs» экспортировать { состояние }
Даже если пример используется в приложении посредством запроса и импорта, каждая ссылка на пример содержит одно и то же состояние, и изменения состояния любой модульной системы будут применяться к обоим.
Если эта статья вам полезна, пожалуйста, поставьте ей лайк и поддержите ее. Ваш «лайк» — это мотивация для меня продолжать творить.
В этой статье приводится следующая информация:
официальная документация node.js
Шаблоны проектирования Node.js