[Расширенный веб-пакет] Используйте babel, чтобы избежать зависимостей модуля времени выполнения компиляции веб-пакета.

Node.js внешний интерфейс Webpack Babel
[Расширенный веб-пакет] Используйте babel, чтобы избежать зависимостей модуля времени выполнения компиляции веб-пакета.

введение

Babel — очень мощный инструмент, и его роль гораздо шире, чем наше обычное преобразование синтаксиса ES6 -> ES5. На пути к продвинутому интерфейсу понимание и изучение Babel и его гибкого режима плагинов дадут интерфейсу больше возможностей.

В этой статье Babel используется для решения проблемы в реальном проекте путем написания подключаемого модуля Babel.

Соответствующий код этой статьи был размещен на github:babel-plugin-import-customized-require

1. Возникшие проблемы

Недавно я столкнулся с такой проблемой в проекте: мы знаем, что использование веб-пакета в качестве инструмента сборки автоматически поможет нам построить зависимости по умолчанию; но в коде проекта некоторые зависимости являются зависимостями времени выполнения/зависимостями времени компиляции (понятно Для чистой модульности внешнего интерфейса, такой как requirejs и seajs), отсутствие обработки этой зависимости приведет к ошибкам компиляции веб-пакета.

Зачем вам нужны зависимости не во время компиляции? Например, в текущем бизнес-модуле (отдельный репозиторий кода веб-пакета) я полагаюсь на управляющий код общего бизнес-модуля.

// 这是home业务模块代码
// 依赖了common业务模块的代码
import log from 'common:util/log.js'

log('act-1');

Однако это может быть связано с тем, что технологический стек не унифицирован, или потому, что унаследованные проблемы общего бизнес-кода не могут быть рефакторингом, или это просто для разделения и правила бизнес-модулей... Короче говоря, эта часть зависимостей модулей не может быть решено во время компиляции веб-пакета, но его необходимо разместить

Чтобы решить проблему, связанную с тем, что веб-пакет не может разрешить эту зависимость модуля во время компиляции, можно ввести новый синтаксис для этой зависимости не во время компиляции, например следующий:

// __my_require__是我们自定义的前端require方法
var log = __my_require__('common:util/log.js')

log('act-1');

Но это привело к расколу в форме нашего кода, и использование спецификации дает нам надежду, что мы все еще можем использовать стандартный синтаксис ESM для одинакового обращения с ними.

Мы по-прежнему хотим иметь возможность писать такой код:

// 标准的ESM语法
import * as log from 'common:util/log.js';

log('act-1');

Кроме того, вы также можете рассмотреть возможность использования внешней конфигурации, предоставляемой веб-пакетом, чтобы избежать упаковки некоторых модулей веб-пакетом. Однако важная проблема заключается в том, что в существующем общем коде присутствует набор интерфейсного модульного синтаксиса, и есть некоторые проблемы с интеграцией кода, скомпилированного webpack, с существующим модом. Поэтому у этого метода есть и недостатки.

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

  • Возможность использовать синтаксис ESM в коде для анализа ссылок на модули не во время компиляции.
  • Поскольку webpack попытается упаковать зависимость, она должна быть корректной во время компиляции.

2. Решения

Основываясь на вышеуказанных целях, во-первых, нам нужен способ определения зависимостей времени выполнения, которые не нужно компилировать. Напримерutil/recordЭтот модуль, если он является зависимостью времени выполнения, может ссылаться на стандартный синтаксис и добавлять идентификатор к имени модуля:runtime:util/record. Эффект следующий:

// 下面这两行是正常的编译期依赖
import React from 'react';
import Nav from './component/nav';

// 下面这两个模块,我们不希望webpack在编译期进行处理
import record from 'runtime:util/record';
import {Banner, List} from 'runtime:ui/layout/component';

Во-вторых, хотя идентификация уже может сообщить разработчикам, какие модули в коде являются зависимостями, которые вебпак должен упаковать, а какие не зависят от времени компиляции; но вебпак не знает, он только получит исходный код модуля, проанализирует импорт синтаксис, чтобы получить зависимости, а затем попытаться загрузить зависимые модули. Но тут вебпак ошарашен, потому какruntime:util/recordТакой модуль является зависимостью времени выполнения и не может быть найден во время компиляции. Затем должен быть способ сделать webpack «невидимыми» зависимостями не во время компиляции.

Наконец, чтобы получить зависимости не во время компиляции, поскольку браузер еще не поддерживает синтаксис импорта ESM, его необходимо изменить на наш собственный синтаксис зависимости модуля при работе на внешнем интерфейсе.

3. Используйте babel для анализа исходного кода

3.1 Введение в инструменты, связанные с Babel

Студенты, которые мало знают о babel и механизме подключаемых модулей, могут сначала прочитать эту часть для краткого понимания.

Babel — это мощный компилятор javascript, который может преобразовывать исходный код в AST (абстрактное синтаксическое дерево) посредством лексического и синтаксического анализа. Путем преобразования AST исходный код может быть изменен, и, наконец, измененный AST может быть преобразован в целевой код.

Из-за нехватки места в этой статье мы не будем слишком много рассказывать о компиляторе или AST, но если вы изучили принципы компиляции, вы должны быть знакомы с лексическим анализом, синтаксическим анализом, токеном и AST. Неважно, если вы этого не знали раньше, примерно можно понять так: babel — это компилятор, который умеет преобразовывать исходный код javascript в специальную структуру данных, эта структура данных — дерево, или AST, т. е. a Хорошее представление структуры исходного кода. AST Babel основан наESTreeиз.

Например,var alienzhou = 'happy'Это утверждение, после обработки Babel, его AST, вероятно, выглядит следующим образом

{
    type: 'VariableDeclaration',
    kind: 'var',
    // ...其他属性
    decolarations: [{
        type: 'VariableDeclarator',
        id: {
            type: 'Identifier',
            name: 'alienzhou',
            // ...其他属性
        },
        init: {
            type: 'StringLiteral',
            value: 'happy',
            // ...其他属性
        }
    }],
}

Эта часть узла AST указывает, что это оператор объявления переменной, использующийvarКлючевое слово, где атрибуты id и init — это два узла AST, которые представляют собой идентификатор (Identifier) ​​с именем алиенжоу и строковый литерал (StringLiteral) со значением happy.

Здесь мы кратко расскажем, как использовать babel и некоторые из предоставляемых им библиотек для анализа и модификации AST. Создание AST может быть выполнено с помощьюbabel-coreметоды, такие как:

const babel = require('babel-core');
const {ast} = babel.transform(`var alienzhou = 'happy'`);

Затем просмотрите AST и найдите конкретный узел для изменения. Babel также предоставляет нам метод обхода AST:

const traverse = require('babel-traverse').default;

Доступ к узлу AST в Babel используетрежим визора, вы можете указать тип узла AST следующим образом, чтобы получить доступ к нужному узлу AST:

traverse(ast, {
    StringLiteral(path) {
        console.log(path.node.value)
        // ...
    }
})

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

let visitor = {
    StringLiteral(path) {
        console.log(path.node.value)
        path.replaceWith(
            t.stringLiteral('excited');
        )
    }
};
traverse(ast, visitor);

Обратите внимание, что AST является изменяемым объектом, и все операции узла будут изменены в исходном AST.

Эта статья не будет подробно знакомить с API-интерфейсами babel-core и babel-traverse, но поможет друзьям, которые не общались с ними, быстро разобраться в них.

Поскольку большинство проектов веб-пакетов используют babel в загрузчике, вам нужно только предоставить подключаемый модуль babel для обработки синтаксиса зависимостей не во время компиляции. Плагин babel на самом деле экспортирует метод, который возвращает объект посетителя, о котором мы упоминали выше.

Тогда мы можем сосредоточиться на письме посетителя.

3.2 Напишите подключаемый модуль Babel для разрешения зависимостей, не связанных с временем компиляции

Синтаксис импорта ESM в тип узла AST:ImportDeclaration:

export default function () {
    return {
        ImportDeclaration: {
            enter(path) {
                // ...
            }
            exit(path) {
                let source = path.node.source;
                if (t.isStringLiteral(source) && /^runtime:/.test(source.value)) {
                    // ...
                }
            }
        }
    }
}

В методе ввода необходимо собрать информацию о синтаксисе ImportDeclaration; в методе выхода определить, является ли текущий ImportDeclaration зависимостью, не зависящей от времени компиляции, и если да, то выполнить преобразование синтаксиса.

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

import util from 'runtime:util';
import * as util from 'runtime:util';
import {util} from 'runtime:util';
import {util as u} from 'runtime:util';
import 'runtime:util';

Соответствует трем типам спецификаторов:

  • Спецификатор импорта:import {util} from 'runtime:util',import {util as u} from 'runtime:util';
  • Спецификатор импорта по умолчанию:import util from 'runtime:util'
  • ImportNamespaceSpecifier:import * as util from 'runtime:util'

import 'runtime:util'нет спецификатора в

На основе ImportDeclaration вы можете просматривать дочерние узлы Здесь создается новый посетитель для доступа к Specifier и сбора различных синтаксисов:

const specifierVisitor = {
    ImportNamespaceSpecifier(_path) {
        let data = {
            type: 'NAMESPACE',
            local: _path.node.local.name
        };

        this.specifiers.push(data);
    },

    ImportSpecifier(_path) {
        let data = {
            type: 'COMMON',
            local: _path.node.local.name,
            imported: _path.node.imported ? _path.node.imported.name : null
        };

        this.specifiers.push(data);
    },

    ImportDefaultSpecifier(_path) {
        let data = {
            type: 'DEFAULT',
            local: _path.node.local.name
        };

        this.specifiers.push(data);
    }
}

Перейдите с помощью спецификатораVisitor в ImportDeclaration:

export default function () {
    // store the specifiers in one importDeclaration
    let specifiers = [];
    return {
        ImportDeclaration: {
            enter(path) {
                path.traverse(specifierVisitor, { specifiers });
            }
            exit(path) {
                let source = path.node.source;
                if (t.isStringLiteral(source) && /^runtime:/.test(source.value)) {
                    // ...
                }
            }
        }
    }
}

До сих пор, когда мы входим в узел ImportDeclaration, коллекция информации, связанной с операторами импорта, при выходе из узла может знать текущий узел, определяя, не компилируются ли зависимости. Поэтому, если компиляция зависит не только от необходимости собирать информацию о синтаксисе узла замены в соответствии с.

Создание нового узла может использовать виды Babel. Тем не менее, рекомендуемое использование Babel-шаблон, сделает код более простым и понятен. Следующий метод, основанный на различной информации импорта, генерирует другой код выполнения, в котором модуль переднего конца - это предположить __my_require__, требует пользовательских методов.

const template = require('babel-template');

function constructRequireModule({
    local,
    type,
    imported,
    moduleName
}) {

    /* using template instead of origin type functions */
    const namespaceTemplate = template(`
        var LOCAL = __my_require__(MODULE_NAME);
    `);

    const commonTemplate = template(`
        var LOCAL = __my_require__(MODULE_NAME)[IMPORTED];
    `);

    const defaultTemplate = template(`
        var LOCAL = __my_require__(MODULE_NAME)['default'];
    `);

    const sideTemplate = template(`
        __my_require__(MODULE_NAME);
    `);
    /* ********************************************** */

    let declaration;
    switch (type) {
        case 'NAMESPACE':
            declaration = namespaceTemplate({
                LOCAL: t.identifier(local),
                MODULE_NAME: t.stringLiteral(moduleName)
            });
            break;

        case 'COMMON':
            imported = imported || local;
            declaration = commonTemplate({
                LOCAL: t.identifier(local),
                MODULE_NAME: t.stringLiteral(moduleName),
                IMPORTED: t.stringLiteral(imported)
            });
            break;

        case 'DEFAULT':
            declaration = defaultTemplate({
                LOCAL: t.identifier(local),
                MODULE_NAME: t.stringLiteral(moduleName)
            });
            break;

        case 'SIDE':
            declaration = sideTemplate({
                MODULE_NAME: t.stringLiteral(moduleName)
            })

        default:
            break;
    }

    return declaration;
}

Наконец-то интегрировано в начального посетителя:

export default function () {
    // store the specifiers in one importDeclaration
    let specifiers = [];
    return {
        ImportDeclaration: {
            enter(path) {
                path.traverse(specifierVisitor, { specifiers });
            }
            exit(path) {
                let source = path.node.source;
                let moduleName = path.node.source.value;
                if (t.isStringLiteral(source) && /^runtime:/.test(source.value)) {
                    let nodes;
                    if (specifiers.length === 0) {
                        nodes = constructRequireModule({
                            moduleName,
                            type: 'SIDE'
                        });
                        nodes = [nodes]
                    }
                    else {
                        nodes = specifiers.map(constructRequireModule);
                    }
                    path.replaceWithMultiple(nodes);
                }
                specifiers = [];
            }
        }
    }
}

Затем дляimport util from 'runtime:util'Исходный код после изменения плагина babel становитсяvar util = require('runtime:util')['default'], код также будет выведен непосредственно webpack.

Таким образом, с помощью плагина babel мы достигли цели, поставленной в начале статьи.

4. Обработка динамического импорта

Внимательные читатели обязательно обнаружат, что мы решили только проблему статического импорта выше, поэтому не будет ли динамический импорт, подобный следующему, по-прежнему иметь вышеуказанные проблемы?

import('runtime:util').then(u => {
    u.record(1);
});

Да и проблемы еще будут. Поэтому дальше нам нужно разобраться с синтаксисом динамического импорта. Все, что вам нужно сделать, это добавить посетителю новый тип узла:

{
    Import: {
        enter(path) {
            let callNode = path.parentPath.node;
            let nameNode = callNode.arguments && callNode.arguments[0] ? callNode.arguments[0] : null;

            if (t.isCallExpression(callNode)
                && t.isStringLiteral(nameNode)
                && /^runtime:/.test(nameNode.value)
            ) {
                let args = callNode.arguments;
                path.parentPath.replaceWith(
                    t.callExpression(
                        t.memberExpression(
                            t.identifier('__my_require__'), t.identifier('async'), false),
                            args
                ));
            }
        }
    }
}

На этом этапе код динамического импорта выше будет заменен на:

__my_require__.async('runtime:util').then(u => {
    u.record(1);
});

Очень удобно.

5. Пишите в конце

Соответствующий код этой статьи был размещен на github:babel-plugin-import-customized-require

Эта статья начинается с требования о периоде компиляции веб-пакета и применяет babel, чтобы некоторые зависимости модулей в коде не обрабатывались в течение периода компиляции веб-пакета. На самом деле видно, что babel дает нам большие возможности.

Проблема, решенная в этой статье, является лишь небольшим требованием, возможно, у вас будет лучшее решение, однако здесь больше, чтобы показать гибкость и мощь Babel, который дает больше места и возможностей во внешнем интерфейсе во многих производных. также можно найти в различных областях. Я надеюсь, что эта статья может быть введением и расширить еще один способ мышления для вас, чтобы решить проблему.

использованная литература