Русский
Русский
English
Статистика
Реклама

Namespaces в JavaScript (часть II, заключительная)

В своей прошлой статье я прикидывал, какие namespace'ы мне нужны для упорядочивания кода в ES6-модулях. В этой статье я описываю, какие namespace'ы у меня получились и как их использовать при порождении объектов и разрешении зависимостей (dependency injection).

Для чего нужны namespace'ы

Для тех, кто не видит важность namespace'ов, могу привести в пример почтовые адреса:

  • страна / область / город / улица / дом / квартира

или адресацию в файловой системе:

  • /var/lib/dpkg/status

В общем, namespace'ы нужны для упорядочивания большого количества однотипных элементов: жителей планеты, файлов на диске, доменов в интернете, кода в крупных приложениях. Если вы не согласны с этим утверждением, лучше дальше не читайте - вы точно не поймёте, зачем вам все эти навороты, потеряете время и испортите себе настроение.

Дуальный характер JavaScript'а

Отличие JavaScript'а от остальных языков программирования в том, что JS-программы исполняются в двух очень различных средах:

  • браузер: с ориентацией преимущественно на сетевые ресурсы;

  • сервер: с ориентацией преимущественно на файловые ресурсы;

Один и тот же ES6-модуль может быть адресован в абсолютной нотации таким образом (браузер и сервер):

http://demo.com/node_modules/@vendor/package/src/module.mjs/home/alex/demo/node_modules/@vendor/package/src/module.mjs

Текущая проблема адресации ES6-модулей

В nodejs-приложениях относительная адресация элементов кода идёт относительно каталога ./node_modules/:

import ModuleDefault from '@vendor/module/src/Module.mjs';

а в браузерных приложениях такая адресация приводит к ошибке:

Uncaught (in promise) TypeError: Failed to resolve module specifier "@vendor/module/src/Module.mjs". Relative references must start with either "/", "./", or "../".

Если же изменить адрес на ./@vendor/module/src/Module.mjs, как того требует браузерная среда, то в nodejs-приложениях получим ошибку:

internal/process/esm_loader.js:74    internalBinding('errors').triggerUncaughtException(                              ^Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/.../@vendor/module/src/Module.mjs' imported from /.../test.mjs    at finalizeResolution (internal/modules/esm/resolve.js:276:11)    at moduleResolve (internal/modules/esm/resolve.js:699:10)

Это приводит к тому, что мы не имеем возможности использовать один и тот же код со статическим оператором import , ссылающийся на модули в сторонних пакетах, одновременно и для браузерных приложений, и для nodejs-приложений.

Подробнее

Т.е., если у нас есть модуль ClassA.mjs в пакете packageA:

export default class ClassA {}

И есть модуль ClassB.mjs в пакете packageB, в котором мы импортируем содержимое из модуля packageA/ClassA.mjs:

import ClassA from 'packageA/ClassA.mjs'export default class ClassB {}

то мы не сможем выполнить этот код в браузере, а код:

import ClassA from './packageA/ClassA.mjs'export default class ClassB {}

в nodejs-приложении.

Теоретически, мы сможем и там, и там выполнить код:

import ClassA from '/packageA/ClassA.mjs'export default class ClassB {}

но для такого варианта мы должны будем выложить все npm-пакеты в корень файловой системы сервера, на котором выполняется nodejs-приложение.

Адресация ES6-модуля на основе путей

В предыдущей статье я приводил основание для преобразования путей к модулям из вида:

@vendor/package1/src/module1.js

к виду:

@vendor/package1/module1

У этого преобразования есть существенный недостаток - адрес содержит разделитель "/", который не может присутствовать (без экранирования) в идентификаторах JS-кода (именах переменных, функций, классов):

const obj = {name, version, Path/Like/Id}

Независимая адресация ES6-модуля

В PHP namespace'ы появились с версии 5.3, а до этого программисты использовали подход, применяемый, например, в Zend1 (old style):

class My_Long_NestedComponent_ClassName {}

Этот подход неплохо зарекомендовал себя в PHP (до появления встроенных в язык namespace'ов), поэтому его вполне можно перенести на JS, используя при именовании пространств символы, разрешённые для идентификаторов JS-кода:

const obj = {name, version, Zend_Like_Id}

К тому же у него есть дополнительный плюс по отношению к адресации на основе путей.

Имя простого npm-пакета (package) не может содержать разделителей (/), максимум, что можно - поместить пакет в scope (@vendor/package). Если в процессе разработки наш пакет разросся и мы захотим разделить его на несколько пакетов, то нам придётся переделывать адреса для всех элементов кода:

package/module1/region/area/path/to/module // old 'package'package_module1/region/area/path/to/module // new 'package_module1'package_module1_region/area/path/to/module // new 'package_module1_region'

Адресация в стиле Zend1 лишена этого недостатка. Нам нужно подставить правила маппинга для соответствующих узлов идентификатора:

Package => ./node_modules/packagePackage_Module1 => ./node_modules/package_mod1Package_Module1_Region => ./node_modules/package_mod1_reg

после чего путь к искомому модулю определяется по наибольшей длине имеющегося маппинга:

Package_Module2_Mod => ./node_modules/package/Module2/Mod.jsPackage_Module1_Sub_Mod => ./node_modules/package_mod1/Sub/Mod.jsPackage_Module1_Region_Area => ./node_modules/package_mod1_reg/Area.js

В общем, решение, проверенное временем, и гораздо лучше адресации на основе путей.

Адресация кода внутри ES6-модуля

ES6-модуль предоставляет доступ к своему содержимому через инструкцию export. Допустим, что у нас есть пространство Ns_App_Plugin c маппингом на путь ./node_modules/@vendor/app_plugin/src, в котором определён ES6-модуль ./Shared/Util.mjs с таким экспортом:

export function fn() {};export class Clazz {};export default {fn, Clazz};

Используя namespace с независимой от пути адресацией, можно адресовать сам модуль как:

Ns_App_Plugin_Shared_Util

а используя символ "#" (по аналогии с подобным символом в URL), можно адресовать соответствующий экспорт внутри модуля:

Ns_App_Plugin_Shared_Util#fnNs_App_Plugin_Shared_Util#ClazzNs_App_Plugin_Shared_Util#default

Символ "#" так же, как и символ "/", не может использоваться в идентификаторах JS-кода без экранирования. Вместо него можно было бы использовать символ "$", но я его приберёг для другого (об этом чуть ниже).

Default export

В Java каждый класс располагается в отдельном java-файле. В PHP можно в одном php-файле можно разместить несколько классов, но, как правило, придерживаются аналогичного подхода: один класс - один файл.

В ES6-модуле также можно размещать один элемент кода (класс, функция, объект) на файл и экспортировать его по-умолчанию:

export default function Ns_App_Plugin_Shared_Util() {}

Если придерживаться этого правила - "один файл - один экспорт", то у нас пропадает необходимость в использовании символа "#". В зависимости от контекста адрес Ns_App_Plugin_Shared_Util может означать как ES6-модуль, так и default экспорт из этого модуля.

Порождение объектов

В своём приложении мы, как правило, оперируем элементами кода не на уровне модулей, а на уровне экспорта этих модулей. Т.е., нам в коде нужны не сами модули, а объекты, функции и классы, экспортируемые модулями. Иногда нам в приложении нужен один единственный экземпляр какого-то объекта (конфигурация приложения), а в некоторых случаях нам нужно создавать новый объект при помощи функции или класса. Первый вариант называется одиночкой, а второй - фабрика (различие).

Допустим, у нас в приложении есть контейнер, в который мы можем помещать объекты двух типов. Пусть одиночки в контейнере у нас адресуются строкой, начинающейся с прописной буквы (dbConfig), а фабрики - такой же строкой, только с двумя знаками доллара на конце - dbTransaction$$. Тогда запросы к контейнеру могли бы выглядеть так:

const cfg1 = container.get('dbConfig');const cfg2 = container.get('dbConfig');const trn1 = container.get('dbTransaction$$');const trn2 = container.get('dbTransaction$$');cfg === cfg2;   // truetrn1 === trn2;   // false

Дпустим, что у нас имена модулей начинаются с заглавной буквы (чтобы отличать идентификатор объекта в контейнере от идентификатора модуля). Тогда мы могли бы таким образом идентифицировать объекты, поставляемые контейнером:

const singleton = container.get('dbConfig');const newInstance = container.get('dbTransaction$$');const module = container.get('Ns_App_Plugin_Shared_Util');const defExport = container.get('Ns_App_Plugin_Shared_Util#');const defExportSingleton = container.get('Ns_App_Plugin_Shared_Util$');const defExportNewInstance = container.get('Ns_App_Plugin_Shared_Util$$');

Dependency Injection

Пространство имён с независимой от файловой структуры адресацией позволяет отвязать логическую структуру кода от его файлового представления и перейти от импорта файлов к декларации зависимостей.

Чуть больше года назад я уже делал реализацию DI-контейнера и даже опубликовал его описание на Хабре. Но на тот момент я не понимал значения ES6-модуля как элемента JS-кода. Такое понимание пришло только при написании статьи "Javascript: исходный код и его отображение при отладке" (это понимание и есть практическая ценность статьи, о которой интересовался коллега @Zenitchik). Предыдущая реализация моего контейнера позволяла вставлять только одиночные зависимости и зависимости на основе фабрик (вновь порождённые объекты). В текущей реализации контейнер позволяет получать доступ к следующими элементам:

  • singleton, добавленный в контейнер вручную: dbConfig;

  • новый объект, созданный при помощи фабрики, добавленной в контейнер вручную: dbTransaction$$;

  • ES6-модуль: Ns_App_Module;

  • отдельный экспорт: Ns_App_Module#name (именованный) и Ns_App_Module# (default);

  • singleton, созданный из default-экспорта модуля: Ns_App_Module$;

  • singleton, созданный из именованного экспорта модуля: Ns_App_Module#name$;

  • новый объект, созданный при помощи фабрики, полученной из default-экспорта модуля: Ns_App_Module$$;

  • новый объект, созданный при помощи фабрики, полученной из именованного экспорта модуля: Ns_App_Module#name$$;

Таким образом, на данным момент я могу использовать в своих JS-приложениях namespace'ы, аналогичные тем, которые есть в PHP и Java, а типовой модуль моего приложения выглядит примерно так:

export default function Ns_App_Plugin_Fn(spec) {    const singleton = spec.dbConfig;    const newInstance = spec.dbTransaction$$;    const module = spec.Ns_App_Plugin_Shared_Util;    const defExport = spec['Ns_App_Plugin_Shared_Util#'];    const defExportSingleton = spec.Ns_App_Plugin_Shared_Util$;    const defExportNewInstance = spec.Ns_App_Plugin_Shared_Util$$;    // ...}

или так:

export default class Ns_App_Plugin_Class {    constructor(spec) {        const singleton = spec.dbConfig;        // ...    }}

И этот код может использоваться без изменений как в nodejs, так и в браузере.

Недостатки

Наследование

При данном подходе не получается использовать наследование:

export default class Ns_App_Plugin_Mod extends Ns_App_Plugin_Base {}

вместо этого приходится использовать композицию, что для моих задач считаю приемлемым.

Расширение

Не всегда получается загрузить ES6-модули в виде файлов с расширением *.js. Появляется ошибка:

(node:506150) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension./home/alex/.../src/Server.js:2import $path from 'path';^^^^^^

Для стабильности лучше размещать все ES6-модули в файлах с расширением *.mjs. В этом случае загрузчик принудительно использует схему ES6.

Демо

Я подготовил небольшой демо-проект, в котором есть два модуля:

@flancer64/demo_teqfw_di_mod_main@flancer64/demo_teqfw_di_mod_plugin

В каждом из которых по три скрипта:

./src/Front.mjs./src/Server.mjs./src/Shared.mjs

Front-скрипты и server-скрипты используют shared-скрипты, а скрипты main-модуля используют скрипты plugin-модуля.

В README проекта расписаны обе стороны, а здесь даю краткое описание взаимодействия элементов со стороны фронта.

Загрузка контейнера, его настройка, получение из контейнера объекта для запуска на фронте:

<script type="module">    const baseUrl = location.href;    // load DI container as ES6 module (w/o namespaces)    import(baseUrl + 'node_modules/@teqfw/di/src/Container.mjs').then(async (modContainer) => {        // init container and setup namespaces mapping        /** @type {TeqFw_Di_Container} */        const container = new modContainer.default();        const pathMain = baseUrl + 'node_modules/@flancer64/demo_teqfw_di_mod_main/src';        const pathPlugin = baseUrl + 'node_modules/@flancer64/demo_teqfw_di_mod_plugin/src';        container.addSourceMapping('Demo_Main', pathMain, true, 'mjs');        container.addSourceMapping('Demo_Main_Plugin', pathPlugin, true, 'mjs');        // get main front as singleton        /** @type {Demo_Main_Front} */        const frontMain = await container.get('Demo_Main_Front$');        frontMain.out('#main', '#plugin');    });</script>

Код основного фронт-объекта с подтягиванием зависимостей в конструкторе через spec объект:

// frontend code cannot use traditional npm packages but can use browser APIexport default class Demo_Main_Front {    /** @type {Demo_Main_Shared} */    singleMainShared    /** @type {Demo_Main_Plugin_Front} */    instPluginFront    constructor(spec) {        // get default export as singleton (class)        this.singleMainShared = spec.Demo_Main_Shared$;        // get default export as new instance (class)        this.instPluginFront = spec.Demo_Main_Plugin_Front$$;    }    //...}

Код shared-объекта из plugin-пакета:

// shared code cannot use traditional npm packages and cannot use browser APIexport default function Demo_Main_Plugin_Shared(param) {    return {pluginShared: param};}

Резюме

Вот он - @teqfw/di

Источник: habr.com
К списку статей
Опубликовано: 17.11.2020 22:21:17
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Разработка веб-сайтов

Javascript

Es6

Modules

Dependency injection

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru