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



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

    Представляю вашему вниманию перевод статьи «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
        }
    

    Для использования модульного файла в браузере необходимо добавить тег <script> и определить его как модуль: <script type="module" src="esCounterModule.js"></script>. Для использования этого модуля в Node.js меняем его расширение на .mjs:

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


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

        <script nomodule>
            alert('Не поддерживается.')
        </script>
    

    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.

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

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 5

      –7

      Ура, моя машина времени работает! Она доставляет статьи из 2016 года!

        –4
        Я бы порекомендовал попробовать hqjs.org это умный сервер который преобразует модули в es6 формат.
          –7
          Буду краток: javascript — полный п..... просто ужас.
            0

            На практике es6 модули очень сырые пока, к сожалению, особенно в TypeScript + Node. Поддержка для них в TypeScript весьма странная — например, TypeScript не умеет генерировать .mjs и всегда генерирует .js (помечено как wont fix в их GitHub issues), а для включения es6 модулей в TS приходится писать


            import x from “./a.mjs”;


            Да-да, вот так, с указанием расширения прямо в *.ts файлах. Костыль страшный.


            Есть https://www.npmjs.com/package/esm, который автор эпатажно определяет как «The brilliantly simple, babel-less, bundle-less ECMAScript module loader. Esm is the world’s most advanced ECMAScript module loader. This fast, production ready, zero dependency loader is all you need to support ECMAScript modules in Node 6+» — и который также костыль над предыдущим костылем.


            Поэтому в TypeScript для серверного кода в большинстве случаев приходится использовать module=commonjs. Для Webpack-а же — module=esnext, иначе не работают чанки.


            Во всем этом только одно радует: на TypeScript можно спокойно использовать import-export. Клюджи только на уровне скомпилированных файлов.

              +1
              Спасибо, очень занимательная статья

              PS. Небольшая опечатка
              CommonJS/NOde.js

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое