Понимание (всех) «модульных» форматов и инструментов JavaScript

o1-iqeyqme6vgoqcdvfycgxjodq.png

Доброго времени суток, друзья!

Представляю вашему вниманию перевод статьи «Understanding (all) JavaScript module formats and tools» автора Dixin.

При создании приложения часто возникает желание разделить код на части, логические или функциональные блоки (модули). Однако JavaScript изначально не имел поддержки модулей. Это привело к появлению различных модульных технологий. В настоящей статье обсуждаются все основные понятия, шаблоны, библиотеки, синтаксис и инструменты для работы с модулями в JavaScript.


IIFE модуль: шаблон JS модуля


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

    // определяем глобальные переменные
    let count = 0
    const increase = () => ++count
    const reset = () => {
        count = 0
        console.log('Счетчик сброшен.')
    }

    // используем глобальные переменные
    increase()
    reset()


Для того, чтобы избежать загрязнения глобального пространства имен, можно использовать анонимную функцию:

    (() => {
        let count = 0
        // ...
    })


Вуаля, глобальных переменных больше нет. Однако код внутри функции не выполняется.

IIFE: немедленно вызываемое функциональное выражение


Для того, чтобы выполнить код внутри функции f, ее необходимо вызвать с помощью () как f(). Для выполнения кода внутри анонимной функции (() => {}) следует также использовать (). Это выглядит так (() => {})():

    (() => {
        let count = 0
        // ...
    })()


Это называется IIFE (немедленно вызываемым функциональным выражением). Модуль может быть определен следующим образом:

    // определяем IIFE модуль
    const iifeCounterModule = (() => {
        let count = 0
        return {
            increase: () => ++count,
            reset: () => {
                count = 0
                console.log('Счетчик сброшен.')
            }
        }
    })()

    // используем IIFE модуль
    iifeCounterModule.increase()
    iifeCounterModule.reset()


Мы оборачиваем код модуля в IIFE. Анонимная функция возвращает объект. Это заменяет интерфейс экспорта. Присутствует только одна глобальная переменная — название модуля (или его пространство имен). Впоследствии название модуля может использоваться для его вызова (экспорта). Это называется шаблоном JS модуля.

Примеси импорта


При определении модуля могут потребоваться некоторые зависимости. При использовании модульного шаблона каждый зависимый модуль — глобальная переменная. Зависимые модули могут определяться внутри анонимной функции или передаваться ей в качестве аргументов:

    // определяем IIFE модуль с зависимостями
    const iifeCounterModule = ((dependencyModule1, dependencyModule2) => {
        let count = 0
        return {
            increase: () => ++count,
            reset: () => {
                count = 0
                console.log('Счетчик сброшен.')
            }
        }
    })(dependencyModule1, dependencyModule2)


Ранние версии популярных библиотек, таких как jQuery, использовали этот шаблон (в последней версии jQuery используется UMD модуль).

Открытый модуль: шаблон открытого JS модуля


Шаблон открытого модуля был придуман Christian Heilmann. Этот шаблон также является IIFE, но акцент в нем делается на определении всех интерфейсов как локальных переменных внутри анонимной функции:

    // определяем открытый модуль
    const revealingCounterModule = (() => {
        let count = 0
        const increase = () => ++count
        const reset = () => {
            count = 0
            console.log('Счетчик сброшен.')
        }

        return {
            increase,
            reset
        }
    })()

    // используем открытый модуль
    revealingCounterModule.increase()
    revealingCounterModule.reset()


Такой синтаксис облегчает понимание того, за что отвечает (или что делает) каждый интерфейс.

CJS модуль: CommonJS модуль или Node.js модуль


CommonJS, первоначально названный ServerJS, это шаблон для определения и использования модулей. Он встроен в Node.js. По умолчанию каждый JS файл — это CJS. Переменные module и exports обеспечивают экспорт модуля (файла). Функция require обеспечивает загрузку и использование модуля. Следующий код демонстрирует определение модуля счетчика на синтаксисе CommonJS:

    // определяем CommonJS модуль: commonJSCounterModule.js
    const dependencyModule1 = require('./dependencyModule1')
    const dependencyModule2 = require('./dependencyModule2')

    let count = 0
    const increase = () => ++count
    const reset = () => {
        count = 0
        console.log('Счетчик сброшен.')
    }

    exports.increase = increase
    exports.reset = reset
    // или (эквивалентно)
    module.exports = {
        increase,
        reset
    }


Вот как этот модуль используется:

    // используем CommonJS модуль
    const {
        increase,
        reset
    } = require('./commonJSCounterModule')
    increase()
    reset()
    // или
    const commonJSCounterModule = require('./commonJSCounterModule')
    commonJSCounterModule.increase()
    commonJSCounterModule.reset()


В среде выполнения (движке) Node.js этот шаблон используется путем оборачивания кода внутри файла в функцию, которой в качестве параметров передаются переменные exports, module и функция require:

    // определяем CommonJS модуль
    (function(exports, require, module, __filename, __dirname) {
        const dependencyModule1 = require('./dependencyModule1')
        const dependencyModule2 = require('./dependencyModule2')

        let count = 0
        const increase = () => ++count
        const reset = () => {
            count = 0
            console.log('Счетчик сброшен.')
        }

        module.exports = {
            increase,
            reset
        }

        return module.exports
    }).call(thisValue, exports, require, module, filename, dirname)

    // используем CommonJS модуль
    (function(exports, require, module, __filename, __dirname) {
        const commonJSCounterModule = require('./commonJSCounterModule')
        commonJSCounterModule.increase()
        commonJSCounterModule.reset()
    }).call(thisValue, exports, require, module, filename, dirname)


AMD модуль или RequireJS модуль


AMD (асинхронное определение модуля) — это шаблон для определения и использования модулей. Он используется в библиотеке RequireJS. AMD содержит функцию define для определения модуля, которая принимает название модуля, названия зависимостей и фабричную функцию:

    // определяем AMD модуль
    define('amdCounterModule', ['dependencyModule1', 'dependencyModule2'], (dependencyModule1, dependencyModule2) => {
        let count = 0
        const increase = () => ++count
        const reset = () => {
            count = 0
            console.log('Счетчик сброшен.')
        }

        return {
            increase,
            reset
        }
    })


Он также содержит функцию require для использования модуля:

    // используем AMD модуль
    require(['amdCounterModule'], amdCounterModule => {
        amdCounterModule.increase()
        amdCounterModule.reset()
    })


require AMD отличается от require CommonJS тем, что в качестве аргументов функции принимает названия модулей и сами модули.

Динамическая загрузка


Функция define также имеет другое назначение. Она принимает функцию обратного вызова и передает похожую на CommonJS require этой функции. Внутри функции обратного вызова require вызывается для динамической загрузки модуля:

    // используем динамический AMD модуль
    define(require => {
        const dynamicDependencyModule1 = require('dependencyModule1')
        const dynamicDependencyModule2 = require('dependencyModule2')

        let count = 0
        const increase = () => ++count
        const reset = () => {
            count = 0
            console.log('Счетчик сброшен.')
        }

        return {
            increase,
            reset
        }
    })


AMD модуль из CommonJS модуля


Приведенная выше функция define, кроме require, может принимать в качестве аргументов переменные exports и module. Поэтому внутри define может выполняться код из CommonJS:

    // определяем AMD модуль с CommonJS кодом
    define((require, exports, module) => {
        // CommonJS код
        const dependencyModule1 = require('dependencyModule1')
        const dependencyModule2 = require('dependencyModule2')

        let count = 0
        const increase = () => ++count
        const reset = () => {
            count = 0
            console.log('Счетчик сброшен.')
        }

        exports.increase = increase
        exports.reset = reset
    })

    // используем AMD модуль с CommonJS кодом
    define(require => {
        // CommonJS код
        const counterModule = require('amdCounterModule')
        counterModule.increase()
        counterModule.reset()
    })


UMD модуль: универсальное определение модуля или UmdJS модуль


UMD (универсальное определение модуля) — набор шаблонов для обеспечения работы модуля в разных средах выполнения.

UMD для AMD (RequireJS) и браузера


Следующий код обеспечивает работу модуля как в AMD (RequireJS), так и в браузере:

    // определяем UMD модуль для AMD (RequireJS) и браузера
    ((root, factory) => {
        // обнаруживаем функцию define AMD/RequireJS
        if (typeof define === 'function' && define.amd) {
            // если присутствует, вызываем фабричную функцию с ее помощью
            define('umdCounterModule', ['dependencyModule1', 'dependencyModule2'], factory)
        } else {
            // если отсутствует, вызываем фабричную функцию напрямую
            // импортируемые зависимости являются глобальными переменными (свойствами объекта Window)
            // как и экспортируемый модуль
            root.umdCounterModule = factory(root.dependencyModule1, root.dependencyModule2)
        }
    })(typeof self !== undefined ? self : this, (dependencyModule1, dependencyModule2) => {
        // код модуля
        let count = 0
        const increase = () => ++count
        const reset = () => {
            count = 0
            console.log('Счетчик сброшен')
        }

        return {
            increase,
            reset
        }
    })


Выглядит сложно, но это всего лишь IIFE. Анонимная функция определяет наличие функции define из AMD/RequireJS.

  • Если define обнаружена, фабричная функция вызывается через нее.
  • Если define не обнаружена, фабричная функция вызывается напрямую. В этот момент аргумент root — это объект Window браузера. Он получает зависимые модули из глобальных переменных (свойств объекта Window). Когда factory возвращает модуль, он также становится глобальной переменной (свойством объекта Window).


UMD для AMD (RequireJS) и CommonJS (Node.js)


Следующий код обеспечивает работу модуля как в AMD (RequireJS), так и в CommonJS (Node.js):

    (define => define((require, exports, module) => {
        // код модуля
        const dependencyModule1 = require("dependencyModule1")
        const dependencyModule2 = require("dependencyModule2")

        let count = 0
        const increase = () => ++count
        const reset = () => {
            count = 0
            console.log("Count is reset.")
        }

        module.export = {
            increase,
            reset
        }
    }))( // обнаруживаем переменные module и exports из CommonJS/Node.js
        // также обнаруживаем функцию define из AMD/RequireJS
        typeof module === 'object' && module.exports && typeof define !== 'function'
            ? // CommonJS/Node.js. Вручную создаем функцию define
                factory => module.exports = factory(require, exports, module)
            : // AMD/RequireJS. Используем функцию define
                define)


Не пугайтесь, это снова всего лишь IIFE. При вызове анонимной функции, происходит «оценка» ее аргумента. Оценивание аргумента позволяет определить среду выполнения (определяется наличие переменных module и exports из CommonJS/Node.js, а также функции define из AMD/RequireJS).

  • Если средой выполнения является CommonJS/Node.js, аргумент анонимной функции вручную создает функцию define.
  • Если средой выполнения является AMD/RequireJS, аргументом анонимной функции является функция define из этой среды. Выполнение анонимной функции гарантирует работу функции define. Внутри анонимной функции для создания модуля вызывается функция define.


ES модуль: ECMAScript2015 или ES6 модуль


В 2015 году в 6 версии спецификации JS был представлен новый модульный синтаксис. Данная сецификаци получила название ECMAScript 2015 (ES2015) или ECMAScript 6 (ES6). Основа нового синтаксиса — ключевые слова import и export. Следующий код демонстирует использование ES модуля для именованного и «дефолтного» (по умолчанию) импорта/экспорта:

    // определяем ES модуль: esCounterModule.js или esCounterModule.mjs
    import dependencyModule1 from './dependencyModule1.mjs'
    import dependencyModule2 from './dependencyModule2.mjs'

    let count = 0
    // именованный экспорт
    export const increase = () => ++count
    export const reset = () => {
        count = 0
        console.log('Счетчик сброшен.')
    }
    // или экспорт по умолчанию
    export default {
        increase,
        reset
    }


Для использования модульного файла в браузере необходимо добавить тег . Для использования этого модуля в Node.js меняем его расширение на .mjs:

    // использование ES модуля
    // импорт из именованного экспорта
    import {
        increase,
        reset
    } from './esCounterModule.mjs'
    increase()
    reset()
    // или импорт из экпорта по умолчанию
    import esCounterModule from './esCounterModule.mjs'
    esCounterModule.increase()
    esCounterModule.reset()

Для обратной совместимости в браузере можно добавить тег


ES динамический модуль: ECMAScript2020 или ES11 динамический модуль


В последней 11 версии спецификации JS 2020 года представлена встроенная функция import для динамического использования ES модулей. Данная функция возвращает промис, поэтому использовать модуль можно с помощью then:

    // используем динамический ES модуль с интерфейсом промисов, импорт из именованного экспорта
    import('./esCounterModule.js').then(({
        increase,
        reset
    }) => {
        increase()
        reset()
    })
    // или импорт из экспорта по умолчанию
    import('./esCounterModule.js').then(dynamicESCounterModule => {
        dynamicESCounterModule.increase()
        dynamicESCounterModule.reset()
    })


Благодаря тому, что функция import возвращает промис, в ней может использоваться ключевое слово await:

    // используем динамический ES модуль с async/await
    (async () => {
        // импорт из именованного экспорта
        const {
            increase,
            reset
        } = await import('./esCounterModule.js')
        increase()
        reset()

        // или импорт из экспорта по умолчанию
        const dynamicESCounterModule = await import('./esCounterModule.js')
        dynamicESCounterModule.increase()
        dynamicESCounterModule.reset()
    })


Системный модуль: SystemJS модуль


SystemJS — это библиотека для обеспечения работы ES модулей в старых браузерах. Например, следующий модуль написан с использованием синтаксиса ES6:

    // определяем ES модуль
    import dependencyModule1 from "./dependencyModule1.js"
    import dependencyModule2 from "./dependencyModule2.js"
    dependencyModule1.api1()
    dependencyModule2.api2()

    let count = 0
    // именованный экспорт
    export const increase = function() {
        return ++count
    }
    export const reset = function() {
        count = 0
        console.log("Count is reset.")
    }
    // или экспорт по умолчанию
    export default {
        increase,
        reset
    }


Этот код не будет работать в браузерах, не поддерживающих синтаксис ES6. Одним из решений данной проблемы является транспиляция кода с помощью интерфейса System.register библиотеки SystemJS:

    // определяем SystemJS модуль
    System.register(['./dependencyModule1', './dependencyModule2'], function(exports_1, context_1) {
        'use strict'
        var dependencyModule1_js_1, dependencyModule2_js_1, count, increase, reset
        var __moduleName = context_1 && context_1.id
        return {
            setters: [
                function(dependencyModule1_js_1_1) {
                    dependencyModule1_js_1 = dependencyModule1_js_1_1
                },
                function(dependencyModule2_js_1_1) {
                    dependencyModule2_js_1 = dependencyModule2_js_1_1
                }
            ],
            execute: function() {
                dependencyModule1_js_1.default.api1()
                dependencyModule2_js_1.default.api2()
                count = 0
                // именованный экспорт
                exports_1('increase', increase = function() {
                    return ++count
                })
                exports_1('reset', reset = function() {
                    count = 0
                    console.log('Счетчик сброшен.')
                })
                // или экспорт по умолчанию
                exports_1('default', {
                    increase,
                    reset
                })
            }
        }
    })


Нового модульного синтаксиса ES6 больше нет. Зато код будет прекрасно работать в старых браузерах. Эта транспиляция может быть выполнена автоматически с помощью Webpack, TypeScript и т.д.

Динамическая загрузка модуля


SystemJS также содержит функцию import для динамического импорта:

    // используем SystemJS модуль с промисами
    System.import('./esCounterModule.js').then(dynamicESCounterModule => {
        dynamicESCounterModule.increase()
        dynamicESCounterModule.reset()
    })


Webpack модуль: транспиляция и сборка CJS, AMD и ES модулей


Webpack — это сборщик модулей. Его транспилятор объединяет CommonJS, AMD и ES модули в единый сбалансированный модульный шаблон и собирает весь код в один файл. Например, в следующих 3 файлах определяются 3 модуля с помощью различного синтаксиса:

    // определяем AMD модуль: amdDependencyModule1.js
    define('amdDependencyModule1', () => {
        const api1 = () => {}
        return {
            api1
        }
    })

    // определяем CommonJS модуль: commonJSDependencyModule2.js
    const dependencyModule2 = require('./commonJSDependencyModule2')
    const api2 = () => dependencyModule1.api1()
    exports.api2 = api2

    // определяем ES модуль: esCounterModule.js
    import dependencyModule1 from './amdDependencyModule1'
    import dependencyModule2 from './commonJSDependencyModule2'

    let count = 0
    const increase = () => ++count
    const reset = () => {
        count = 0
        console.log('Счетчик сброшен.')
    }

    export default {
        increase,
        reset
    }


Следующий код демонстрирует использование этого модуля:

    // использование ES модуля: index.js
    import counterModule from './esCounterModule'
    counterModule.increase()
    counterModule.reset()


Webpack способен объединить эти файлы, несмотря на то, что они представляют собой разные модульные системы, в один файл main.js:

    root
        dist
            main.js (сборка файлов, находящихся в папке src)
        src
            amdDependencyModule1.js
            commonJSDependencyModule2.js
            esCounterModule.js
            index.js
        webpack.config.js


Поскольку Webpack основан на Node.js, он использует модульный синтаксис CommonJS. В webpack.config.js:

    const path = require('path')

    module.exports = {
        entry: './src/index.js',
        mode: 'none', // не оптимизировать и не минифицировать код в целях сохранения читаемости
        output: {
            filename: 'main.js',
            path: path.resolve(__dirname, 'dist'),
        },
    }


Для транспиляции и сборки необходимо выполнить следующие команды:

    npm install webpack webpack-cli --save-dev
    npx webpack --config webpack.config.js


В результате Webpack создаст файл main.js. Следующий код из main.js отформатирован для улучшения читаемости:

    (function(modules) { // инициализация Webpack
        // кэш для сохранения модулей
        var installedModules = {}
        // функция require
        function require(moduleId) {
            // проверяем, есть ли модуль в кэше
            if (installedModules[moduleId]) {
                return installedModules[moduleId].exports
            }
            // создаем новый модуль и помещаем его в кэш
            var module = installedModules[moduleId] = {
                i: moduleId,
                l: false,
                exports: {}
            }
            // выполняем функцию module
            modules[moduleId].call(module.exports, module, module.exports, require)
            // помечаем модуль как загруженный
            module.l = true
            // возвращаем свойство exports модуля
            return module.exports
        }

        // привязываем объект modules (__webpack_modules__)
        require.m = modules
        // привязываем кэш с модулями
        require.c = installedModules
        // определяем геттер для сбалансированного экспорта
        require.d = function(exports, name, getter) {
            if (!require.o(exports, name)) {
                Object.defineProperty(exports, name, {
                    enumerable: true,
                    get: getter
                })
            }
        }
        // определяем __esModule в exports
        require.r = function(exports) {
            if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
                Object.defineProperty(exports, Symbol.toStringTag, {
                    value: 'Module'
                })
            }
            Object.defineProperty(exports, '__esModule', {
                value: true
            })
        }
        // создаем временный объект для хранения пространства имен
        // mode & 1: значение - идентификатор модуля, запрашиваем его
        // mode & 2: объединяем все свойства значения в объект ns (namespace)
        // mode & 4: возвращаем значение, когда объект ns уже существует
        // mode & 8|1: поведение аналогично require
        require.t = function(value, mode) {
            if (mode & 1) value = require(value)
            if (mode & 8) return value
            if ((mode & 4) && typeof value === 'object' && value && value.__esModule) return value
            var ns = Object.create(null)
            require.r(ns)
            Object.defineProperty(ns, 'default', {
                enumerable: true,
                value: value
            })
            if (mode & 2 && typeof value !== 'string')
                for (var key in value) require.d(ns, key, function(key) {
                    return value[key]
                }.bind(null, key))
            return ns
        }
        // фукнция getDefaultExport для обеспечения совместимости с несбалансированными модулями
        require.n = function(module) {
            var getter = module && module.__esModule ?
                function getDefault() {
                    return module['default']
                } :
                function getModuleExports() {
                    return module
                }
            require.d(getter, 'a', getter)
            return getter
        }
        // Object.prototype.hasOwnProperty.call
        require.o = function(object, property) {
            return Object.prototype.hasOwnProperty.call(object, property)
        }
        // __webpack_public_path__
        require.p = ''
        // загружаем входящий модуль и возвращаем exports
        return require(require.s = 0)
    })([
        function(module, exports, require) {
            'use strict'
            require.r(exports)
            // используем ES модуль: index.js
            var esCounterModule = require(1)
            esCounterModule['default'].increase()
            esCounterModule['default'].reset()
        },
        function(module, exports, require) {
            'use strict'
            require.r(exports)
            // определяем ES модуль: esCounterModule.js
            var amdDependencyModule1 = require.n(require(2))
            var commonJSDependencyModule2 = require.n(require(3))
            amdDependencyModule1.a.api1()
            commonJSDependencyModule2.a.api2()

            let count = 0
            const increase = () => ++count
            const reset = () => {
                count = 0
                console.log('Счетчик сброшен.')
            }

            exports['default'] = {
                increase,
                reset
            }
        },
        function(module, exports, require) {
            var result!(result = (() => {
                    // определяем AMD модуль: amdDependencyModule1.js
                    const api1 = () => {}
                    return {
                        api1
                    }
                }).call(exports, require, exports, module),
                result !== undefined && (module.exports = result))
        },
        function(module, exports, require) {
            // определяем CommonJS модуль: commonJSDependencyModule2.js
            const dependencyModule1 = require(2)
            const api2 = () => dependencyModule1.api1()
            exports.api2 = api2
        }
    ])


И снова это всего лишь IIFE. Код из 4 файлов преобразован в массив из 4 функций. И этот массив передается анонимной функции в качестве параметра.

Babel модуль: транспиляция ES модуля


Babel — это еще один транспилятор для обеспечения работы ES6+ кода в старых браузерах. Приведенный выше ES6+ модуль может быть преобразован в Babel модуль следуюшим образом:

    // Babel
    Object.defineProperty(exports, '__esModule', {
        value: true
    })
    exports['default'] = void 0

    function _interopRequireDefault(obj) {
        return obj && obj.__esModule ? obj : {
            'default': obj
        }
    }

    // определяем ES модуль: esCounterModule.js
    var dependencyModule1 = _interopRequireDefault(require('./amdDependencyModule1'))
    var dependencyModule2 = _interopRequireDefault(require('./commonJSDependencyModule2'))
    dependencyModule1['default'].api1()
    dependencyModule2['default'].api2()

    var count = 0
    var increase = function() {
        return ++count
    }
    var reset = function() {
        count = 0
        console.log('Счетчик сброшен.')
    }

    exports['default'] = {
        increase: increase,
        reset: reset
    }


А вот код в index.js, демонстрирующий использование этого модуля:

    // Babel
    function _interopRequireDefault(obj) {
        return obj && obj.__esModule ? obj : {
            'default': obj
        }
    }

    // используем ES модуль: index.js
    var esCounterModule = _interopRequireDefault(require('./esCounterModule.js'))
    esCounterModule['default'].increase()
    esCounterModule['default'].reset()


Это транспиляция по умолчанию. Babel также умеет работать с другими инструментами.

Babel и SystemJS


SystemJS может использоваться как плагин для Babel:

    npm install --save-dev @babel/plugin-transform-modules-systemjs

Данный плагин должен быть добавлен в babel.config.json:

    {
        'plugins': ['@babel/plugin-transform-modules-systemjs'],
        'presets': [
            [
            '@babel/env',
                {
                    'targets': {
                        'ie': '11'
                    }
                }
            ]
        ]
    }


Теперь Babel может работать с SystemJS для транспиляции CommonJS/Node.js, AMD/RequireJS и ES модулей:

    npx babel src --out-dir lib


Результат:

    root
        lib
            amdDependencyModule1.js (транспилирован с помощью SystemJS)
            commonJSDependencyModule2.js (транспилирован с помощью SystemJS)
            esCounterModule.js (транспилирован с помощью SystemJS)
            index.js (транспилирован с помощью SystemJS)
        src
            amdDependencyModule1.js
            commonJSDependencyModule2.js
            esCounterModule.js
            index.js
        babel.config.json


Весь синтаксис AMD, CommonJS и ES модулей транспилирован в синтаксис SystemJS:

    // транспилируем AMD/RequireJS модуль в SystemJS модуль: lib/amdDependencyModule1.js
    System.register([], function(_export, _context) {
        'use strict'
        return {
            setters: [],
            execute: function() {
                // определяем AMD модуль: src/amdDependencyModule1.js
                define('amdDependencyModule1', () => {
                    const api1 = () => {}

                    return {
                        api1
                    }
                })
            }
        }
    })

    // транспилируем CommonJS/Node.js модуль в SystemJS модуль: lib/commonJSDependencyModule2.js.
    System.register([], function(_export, _context) {
        'use strict'
        var dependencyModule1, api2
        return {
            setters: [],
            execute: function() {
                // определяем CommonJS модуль: src/commonJSDependencyModule2.js
                dependencyModule1 = require('./amdDependencyModule1')

                api2 = () => dependencyModule1.api1()

                exports.api2 = api2
            }
        }
    })

    // транспилируем ES модуль в SystemJS модуль
    System.register(['./amdDependencyModule1', './commonJSDependencyModule2'], function(_export, _context) {
        var dependencyModule1, dependencyModule2, count, increase, reset
        return {
            setters: [function(_amdDependencyModule) {
                dependencyModule1 = _amdDependencyModule.default
            }, function(_commonJSDependencyModule) {
                dependencyModule2 = _commonJSDependencyModule.default
            }],
            execute: function() {
                // определяем ES модуль: src/esCounterModule.js
                dependencyModule1.api1()
                dependencyModule1.api2()
                count = 0

                increase = () => ++count

                reset = () => {
                    count = 0
                    console.log('Счетчик сброшен.')
                }

                _export('default', {
                    increase,
                    reset
                })
            }
        }
    })

    // транспилируем ES модуль в SystemJS модуль: lib/index.js
    System.register(['./esCounterModule'], function(_export, _context) {
        var esCounterModule
        return {
            setters: [function(_esCounterModule) {
                esCounterModule = _esCounterModule.default
            }],
            execute: function() {
                // используем ES модуль: src/index.js
                esCounterModule.increase()
                esCounterModule.reset()
            }
        }
    })


TypeScript модуль: транспиляция CJS, AMD, ES и SystemJS модулей


TypeScript поддерживает все разновидности синтаксиса JS, включая ES6. При транспиляции синтаксис ES6 модуля может быть сохранен или преобразован в другой формат, в том числе CommonJS/NOde.js, AMD/RequireJS, UMD/UmdJS или SystemJS согласно настройкам транспиляции в tsconfig.json:

    {
        'compilerOptions': {
            'module': 'ES2020' // None, CommonJS, AMD, System,
            UMD, ES6, ES2015, ESNext
        }
    }


Например:

    // TypeScript и ES модуль
    // с compilerOptions: { module: 'ES6' }. Транспилируем с сохранением синтаксиса
    import dependencyModule from './dependencyModule'
    dependencyModule.api()
    let count = 0
    export const increase = function() {
        return ++count
    }

    // с compilerOptions: { module: 'CommonJS' }. Транспилируем в CommonJS/Node.js модуль
    var __importDefault = (this && this.__importDefault) || function(mod) {
        return (mod && mod.__esModule) ? mod : {
            'default': mod
        }
    }
    exports.__esModule = true

    var dependencyModule_1 = __importDefault(require('./dependencyModule'))
    dependencyModule_1['default'].api()
    var count = 0
    exports.increase = function() {
        return ++count
    }

    // с compilerOptions: { module: 'AMD' }. Транспилируем в AMD/RequireJS модуль
    var __importDefault = (this && this.__importDefault) || function(mod) {
        return (mod && mod.__esModule) ? mod : {
            'default': mod
        }
    }
    define(['require', 'exports', './dependencyModule'], function(require, exports, dependencyModule_1) {
        'use strict'
        exports.__esModule = true

        dependencyModule_1 = __importDefault(dependencyModule_1)
        dependencyModule_1['default'].api()
        var count = 0
        exports.increase = function() {
            return ++count
        }
    })

    // с compilerOptions: { module: 'UMD' }. Транспилируем в UMD/UmdJS модуль
    var __importDefault = (this & this.__importDefault) || function(mod) {
            return (mod && mod.__esModule) ? mod : {
                'default': mod
            }
        }
        (function(factory) {
            if (typeof module === 'object' && typeof module.exports === 'object') {
                var v = factory(require, exports)
                if (v !== undefined) module.exports = v
            } else if (typeof define === 'function' && define.amd) {
                define(['require', 'exports', './dependencyModule'], factory)
            }
        })(function(require, exports) {
            'use strict'
            exports.__esModule = true

            var dependencyModule_1 = __importDefault(require('./dependencyModule'))
            dependencyModule_1['default'].api()
            var count = 0
            exports.increase = function() {
                return ++count
            }
        })

    // с compilerOptions: { module: 'System' }. Транспилируем в System/SystemJS модуль
    System.register(['./dependencyModule'], function(exports_1, context_1) {
        'use strict'
        var dependencyModule_1, count, increase
        car __moduleName = context_1 && context_1.id
        return {
            setters: [
                function(dependencyModule_1_1) {
                    dependencyModule_1 = dependencyModule_1_1
                }
            ],
            execute: function() {
                dependencyModule_1['default'].api()
                count = 0
                exports_1('increase', increase = function() {
                    return ++count
                })
            }
        }
    })


Модульный синтаксис ES, поддерживаемый TypeScript, получил название внешних модулей.

Внутренние модули и пространство имен


TypeScript также имеет ключевые слова module и namespace. Они называются внутренними модулями:

    module Counter {
        let count = 0
        export const increase = () => ++count
        export const reset = () => {
            count = 0
            console.log('Счетчик сброшен.')
        }
    }

    namespace Counter {
        let count = 0
        export const increase = () => ++count
        export const reset = () => {
            count = 0
            console.log('Счетчик сброшен.')
        }
    }


Оба транспилируются в JS объекты:

    var Counter(function(Counter) {
        var count = 0
        Counter.increase = function() {
            return ++count
        }
        Counter.reset = function() => {
            count = 0
            console.log('Счетчик сброшен.')
        }
    })(Counter || (Counter = {}))


TypeScript module и namespace могут иметь несколько уровней вложенности через разделитель .:

    module Counter.Sub {
        let count = 0
        export const increase = () => ++count
    }

    namespace Counter.Sub {
        let count = 0
        export const increase = () => ++count
    }


Sub module и sub namespace транспилируются в свойства объекта:

    var Counter(function(Counter) {
        var Sub(function(Sub) {
            var count = 0
            Sub.increase = function() {
                return ++count
            }
        })(Sub = Counter.Sub || (Counter.Sub = {}))
    })(Counter || (Counter = {}))


TypeScript module и namespace также могут использоваться в операторе export:

    module Counter {
        let count = 0
        export module Sub {
            export const increase = () => ++count
        }
    }

    module Counter {
        let count = 0
        export namespace Sub {
            export const increase = () => ++count
        }
    }


Приведенный код также транспилируется в sub module и sub namespace:

    var Counter
    (function(Counter) {
        var count = 0
        var Sub
        (function(Sub) {
            Sub.increase = function() {
                return ++count
            }
        })(Sub = Counter.Sub || (Counter.Sub = {}))
    })(Counter || (Counter = {}))


Заключение


Добро пожаловать в JS, который имеет 10+ систем/форматов модуляции/пространства имен:

  • IIFE модуль: шаблон JS модуля
  • Открытый модуль: шаблон открытого JS модуля
  • CJS модуль: CommonJS модуль или Node.js модуль
  • AMD модуль: асинхронное определение модуля или RequireJS модуль
  • UMD модуль: универсальное определение модуля или UmdJS модуль
  • ES модуль: ECMAScript2015 или ES6 модуль
  • ES динамический модуль: ECMAScript2020 или ES11 динамический модуль
  • Системный модуль: SystemJS модуль
  • Webpack модуль: транспиляция и сборка CJS, AMD и ES модулей
  • Babel модуль: транспиляция ES модуля
  • TypeScript модуль и пространство имен


К счастью, в настоящее время JS имеет стандартные встроенные инструменты для работы с модулями, поддерживаемые Node.js и всеми современными браузерами. Для старых браузеров можно использовать новый модульный синтаксис ES, транспилируя его в совместимый синтаксис с помощью Webpack/Babel/SystemJS/TypeScript.

Спасибо за потраченное время. Надеюсь, оно было потрачено не зря.

© Habrahabr.ru