:defined
CSS псевдо-класс :defined находит любой элемент, который был определён, включая любой стандартный встроенные в браузер элемент и пользовательские элементы (то есть определённые с помощью метода CustomElementRegistry.define()).
/* Находит любой элемент, который был определён */
:defined {
font-style: italic;
}
/* Выбирает все элементы simple-custom, если пользовательский элемент simple-custom был определён */
simple-custom:defined {
display: block;
}
По мере роста нашего приложения, мы обычно хотим разделить его на много файлов, так называемых «модулей».
Модуль обычно содержит класс или библиотеку с функциями.
Имеется страница index.html:
<!DOCTYPE html>
<html>
<body>
<div id="app"></div>
<script type="module" src="index.mjs"></script>
<!--обратите внимание на тип скрипта "module" -->
</body>
</html>
Есть файл index.mjs, представляющий собой модуль, подключённый к странице:
import { doSomething } from './utils.mjs';
doSomething('Make me a sandwich');
Этот модуль, в свою очередь, импортирует функцию doSomething из модуля utils.mjs
export const doSomething = message => {
console.log('No.');
};
Это — чистый JavaScript! Никакого конвейера сборки проекта, никакого 400-строчного конфигурационного файла Webpack, никакого Babel, никаких плагинов и пресетов, и никаких дополнительных 45 npm-модулей. Только вы и ваш код.
Итак. Плюсы ES6-модулей — это чистый JavaScript, это отсутствие сборки, конфигурирования проекта, это отказ от ставших ненужными зависимостей.
А что ещё?
Есть же что-то ещё?
Нет, больше ничего.
Модуль – это просто файл. Один скрипт – это один модуль.
Модули могут загружать друг друга и использовать директивы export и import, чтобы обмениваться функциональностью, вызывать функции одного модуля из другого:
-
export отмечает переменные и функции, которые должны быть доступны вне текущего модуля.
-
import позволяет импортировать функциональность из других модулей.
Например, если у нас есть файл sayHi.js, который экспортирует функцию
// sayHi.js
export function sayHi(user) {
alert(`Hello, ${user}!`);
}
…Тогда другой файл может импортировать её и использовать:
// main.js
import {sayHi} from './sayHi.js';
alert(sayHi); // function...
sayHi('John'); // Hello, John!
Директива import загружает модуль по пути ./sayHi.js относительно текущего файла и записывает экспортированную функцию sayHi в соответствующую переменную.
Так как модули поддерживают ряд специальных ключевых слов, и у них есть ряд особенностей, то необходимо явно сказать браузеру, что скрипт является модулем, при помощи атрибута
<script type="module">.
Чем отличаются модули от «обычных» скриптов?
Есть основные возможности и особенности, работающие как в браузере, так и в серверном JavaScript.
- Всегда «use strict»
В модулях всегда используется режим use strict. Например, присваивание к необъявленной переменной вызовет ошибку.
<script type="module">
a = 5; // ошибка
</script>
- Своя область видимости переменных
Каждый модуль имеет свою собственную область видимости. Другими словами, переменные и функции, объявленные в модуле, не видны в других скриптах.
Если один и тот же модуль используется в нескольких местах, то его код выполнится только один раз, после чего экспортируемая функциональность передаётся всем импортёрам.
Во-первых, если при запуске модуля возникают побочные эффекты, например выдаётся сообщение, то импорт модуля в нескольких местах покажет его только один раз – при первом импорте:
// alert.js
alert("Модуль выполнен!");
// Импорт одного и того же модуля в разных файлах
// 1.js
import `./alert.js`; // Модуль выполнен!
// 2.js
import `./alert.js`; // (ничего не покажет)
На практике, задача кода модуля – это обычно инициализация, создание внутренних структур данных, а если мы хотим, чтобы что-то можно было использовать много раз, то экспортируем это.
Теперь более продвинутый пример.
Давайте представим, что модуль экспортирует объект:
// admin.js
export let admin = {
name: "John"
};
Если модуль импортируется в нескольких файлах, то код модуля будет выполнен только один раз, объект admin будет создан и в дальнейшем будет передан всем импортёрам.
Все импортёры получат один-единственный объект admin:
// 1.js
import {admin} from './admin.js';
admin.name = "Pete";
// 2.js
import {admin} from './admin.js';
alert(admin.name); // Pete
// Оба файла, 1.js и 2.js, импортируют один и тот же объект
// Изменения, сделанные в 1.js, будут видны в 2.js
Ещё раз заметим – модуль выполняется только один раз. Генерируется экспорт и после передаётся всем импортёрам, поэтому, если что-то изменится в объекте admin, то другие модули тоже увидят эти изменения.
Такое поведение позволяет конфигурировать модули при первом импорте. Мы можем установить его свойства один раз, и в дальнейших импортах он будет уже настроенным.
Например, модуль admin.js предоставляет определённую функциональность, но ожидает передачи учётных данных в объект admin извне:
// admin.js
export let admin = { };
export function sayHi() {
alert(`Ready to serve, ${admin.name}!`);
}
В init.js, первом скрипте нашего приложения, мы установим admin.name. Тогда все это увидят, включая вызовы, сделанные из самого admin.js:
// init.js
import {admin} from './admin.js';
admin.name = "Pete";
Другой модуль тоже увидит admin.name:
// other.js
import {admin, sayHi} from './admin.js';
alert(admin.name); // Pete
sayHi(); // Ready to serve, Pete!
Объект import.meta содержит информацию о текущем модуле.
Содержимое зависит от окружения. В браузере он содержит ссылку на скрипт или ссылку на текущую веб-страницу, если модуль встроен в HTML:
<script type="module">
alert(import.meta.url); // ссылка на html страницу для встроенного скрипта
</script>
В модуле на верхнем уровне this не определён (undefined).
Сравним с не-модульными скриптами, там this – глобальный объект:
<script>
alert(this); // window
</script>
<script type="module">
alert(this); // undefined
</script>
Модули всегда выполняются в отложенном (deferred) режиме, точно так же, как скрипты с атрибутом defer. Это верно и для внешних и встроенных скриптов-модулей.
Другими словами:
-
загрузка внешних модулей, таких как <script type="module" src="...">, не блокирует обработку HTML.
-
модули, даже если загрузились быстро, ожидают полной загрузки HTML документа, и только затем выполняются.
-
сохраняется относительный порядок скриптов: скрипты, которые идут раньше в документе, выполняются раньше.
Как побочный эффект, модули всегда видят полностью загруженную HTML-страницу, включая элементы под ними.
Например:
<script type="module">
alert(typeof button); // object: скрипт может 'видеть' кнопку под ним
// так как модули являются отложенными, то скрипт начнёт выполнятся только после полной загрузки страницы
</script>
Сравните с обычным скриптом ниже:
<script>
alert(typeof button); // Ошибка: кнопка не определена, скрипт не видит элементы под ним
// обычные скрипты запускаются сразу, не дожидаясь полной загрузки страницы
</script>
<button id="button">Кнопка</button>
Внешние скрипты с атрибутом type="module" имеют два отличия:
- Внешние скрипты с одинаковым атрибутом src запускаются только один раз:
<!-- скрипт my.js загрузится и будет выполнен только один раз -->
<script type="module" src="my.js"></script>
<script type="module" src="my.js"></script>
- Внешний скрипт, который загружается с другого домена, требует указания заголовков CORS.
Другими словами, если модульный скрипт загружается с другого домена, то удалённый сервер должен установить заголовок Access-Control-Allow-Origin означающий, что загрузка скрипта разрешена.
<!-- another-site.com должен указать заголовок Access-Control-Allow-Origin -->
<!-- иначе, скрипт не выполнится -->
<script type="module" src="http://another-site.com/their.js"></script>
(Запускайте свой Live Сервер через VSCode и все будет работать)
HomeWork:
Нужно создать красный круг. По нажатию он (круг) едет вправо на 100 пикселей. Как это будем делать?
У нас должно быть 2 файла
- DOM.js
- script.js
- В DOM.js
В этом файле у нас будет класс DOM, который в свой конструктор принимает сам document Этот класс мы экспортируем
- В script.js
Мы импортируем наш класс DOM Создаем класс Circle, который наследуется от DOM
В классе Circle у нас должно быть:
- конструктор, который принимает ширину и высоту нашего круга (можете в конструктор передавать еще что-то)
- метод spawnCircle, который будет устанавливать для круга ширину, высоту, цвет и спавнить его на страницу
- метод setClickAction. Тут описываем логику нажатия на круг и его перемещение. При вызове этот метод устанавливем обработчик события на круг
В этом же файле делаем фабрику, которая будет создавать круг