Всем привет! Меня зовут Андрей Яковенко и я являюсь руководителем группы Frontend-разработки. Сегодня хочу рассказать о способе позволяющем автоматизировать и, что немаловажно, упростить разработку виджетов для CMS посредством использования webpack.

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

Кому интересно, добро пожаловать под кат.

Немного предыстории

В один момент времени у меня появилась необходимость написания виджетов на базе React для SharePoint 2013. С точки зрения разработки frontend части виджетов SharePoint ничем не отличается от разработки виджетов для любой другой CMS, например Drupal, WordPress или Sitefinity. Т.е. интеграция фронта осуществляется путем вставки кода разметки и, непосредственно, сценариев.

При разработке виджетов на ванильном JS или jQuery достаточно разрабатывать один сценарий для отдельного виджета, однако тогда у нас встает вопрос о дублировании кода и в конечном итоге встает вопрос использования какого-нибудь RequireJS.

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

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

Решение

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

Но перед тем как приступить к разработке было решено, в первую очередь, разобраться со структурой. Я выбрал следующую:

scripts – содержит все необходимые сценарии сборки
src/components – содержит общую библиотеку компонентов
src/widgets – содержит разрабатываемые виджеты

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

В первую очередь необходимо выбрать бандлер. Мой выбор пал на webpack, т.к. он имеет тонкую конфигурацию, а также в его экосистему входит webpack-dev-server, который позволяет без усилий увидеть изменения кода.
Во вторых хотелось сделать единую точку входа для сценариев управления сборкой или простой командный интерфейс (CLI). Для этого был выбран commander.js, благодаря простоте его использования.

CLI

Для того чтобы понять как будет работать наш командный интерфейс достаточно взглянуть на его схему:

Всего у него две команды: widget и components.

Команда widget должна принимать имя виджета в качестве аргумента и флаг типа сборки для агрегации поведения – запускать webpack-dev-server или просто собрать виджет для прода.

Команда components просто собирает библиотеку компонентов для прода.

Теперь когда разобрались с тем как все будет устроено можно перейти к более интересной части, а именно к коду. Код, кстати, будет представлен как TypeScript.

Реализация

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

Базово файл конфигурации может выглядеть, например, так:

export enum BuildType {
    widget,
    components,
}

function makeConfig(name: string, type: BuildType, isDev: boolean) {
    const entryPoint = type === BuildType.widget
        ? `src/widgets/${name}/entry.point.ts`
        : `src/components/entry.point.ts`;

    const mode = isDev ? 'development' : 'production';

    const config = {
        mode,
        entry: [entryPoint],
        output: {
            path: `dist/${name}`,
        },
        // other webpack config options
    };

    return config;
}

export default makeConfig;

Здесь мы можем заметить несколько параметров:

  • name – имя сущности которую мы собираем (например, имя компонента)

  • type – тип сборки сущносности (виджет или компоненты)

  • isDev – флаг, определяющий какую сборку мы делаем (дев или продакшен)

Пока ничего сложного нет, однако для того чтобы собрать компоненты и использовать их как библиотеку нам необходимо внести изменения в секцию output. А конкретно, указать свойство library (подробнее о ней можно узнать в документации).

После этого файл конфигурации будет выглядеть так:

export enum BuildType {
    widget,
    components,
}

function makeConfig(name: string, type: BuildType, isDev: boolean) {
    const entryPoint = type === BuildType.widget
        ? `src/widgets/${name}/entry.point.ts`
        : `src/components/entry.point.ts`;

    const mode = isDev ? 'development' : 'production';

    const config = {
        mode,
        entry: [entryPoint],
        output: {
            path: `dist/${name}`,
        },
        // other webpack config options
    };

    if (type === BuildType.components) {
        output.library = name;
    }

    return config;
}

export default makeConfig;

Теперь, в качестве вишенки на торте нашей конфигурации ради которой все было затеяно, необходимо указать библиотеки которые мы будем использовать в качестве внешних зависимостей. Это позволить исключить их из сборки и избавит от лишнего дублирования. Для этого у webpack предусмотрен параметр externals (подробнее о нем можно узнать в документации). Правда он нам нужен только тогда, когда мы делаем сборку под прод, иначе webpack всегда будет исключать указанные модули из сборки.

В секцию externals можно указать любые модули, будь то lodash, React или jQuery. Их код будет исключен из сборки, но при этом их нужно подключать на страницу глобально, или явно указывать путь до модулей на стороне сервера или в web’е.

Итоговым вариантом сборки можно считать следующий код:

export enum BuildType {
    widget,
    components,
}

function makeConfig(name: string, type: BuildType, isDev: boolean) {
    const entryPoint = type === BuildType.widget
        ? `src/widgets/${name}/entry.point.ts`
        : `src/components/entry.point.ts`;

    const mode = isDev ? 'development' : 'production';

    const config = {
        mode,
        entry: [entryPoint],
        output: {
            path: `dist/${name}`,
        },
        // other webpack config options
    };

    if (type === BuildType.components) {
        output.library = name;
    }

    if (!isDev) {
        config.externals = {
            'components': 'components',
            // other vendors
        };
    }

    return config;

}

export default makeConfig;

После того как мы определились с конфигурацией webpack мы можем перейти к разработке собственных сценариев.

Сценарии сборки
Написание собственных сценариев сборки достаточно простая задача. Они нам нужны чтобы управлять webpack и webpack-dev-server. Такие собственные сценарии имеются, например, в react-scripts или vue-cli, а также об этом можно прочитать в официальной документации webpack.

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

import webpack, { Configuration } from 'webpack';

export default async (config: Configuration) => {
    const compiler = webpack(config);

    return new Promise((resolve, reject) => {
        compiler.run((err, stats) => {
            if (err) {
                return reject(err);
            }

            const messages = stats.toJson();

            if (messages && messages.errors && messages.errors.length) {
                // Only keep the first error. Others are often indicative
                // of the same problem, but confuse the reader with noise.
                if (messages.errors.length > 1) {
                    messages.errors.length = 1;
                }

                return reject(new Error(messages.errors.join('\n\n')));

            }

            if (
                process.env.CI &&
                (typeof process.env.CI !== 'string' ||
                process.env.CI.toLowerCase() !== 'false') &&
                (messages && messages.warnings && messages.warnings.length)
            ) {
                console.log(
                    '\nTreating warnings as errors because process.env.CI = true.\n' +
                    'Most CI servers set it automatically.\n'
                );

                return reject(new Error(messages.warnings.join('\n\n')));
            }

            return resolve({
                stats,
                warnings: messages ? messages.warnings || [] : [],
            });
        });
    });
}

А минималистичный сценарий для разворачивания webpack-dev-server имеет следующий вид:

import webpack, { Configuration } from 'webpack';
import WebpackDevServer from 'webpack-dev-server';

export default async (config: Configuration) => {
    config.plugins.push(new webpack.ProgressPlugin());

    const compiler = webpack(config);

    const devServerOptions: WebpackDevServer.Configuration = {
        ...config.devServer,
    };

    const server = new WebpackDevServer(compiler, devServerOptions);

    server.listen(config.devServer.port || 8080, function () {
        console.log('The server was started');
    });
}

После того как мы разработали сценарии сборки, мы можем перейти к реализации командного интерфейса.

CLI
Как было сказано ранее для реализации командного интерфейса решено было использовать commander.js.
Так как у нас всего две команды, мы можем воспользоваться упрощенным интерфейсом и реализовать простое приложение:

import { program } from 'commander';

program
    .command('components', 'build components library');

program
    .command('widget', 'build widget by name')
    .argument('<name>')
    .option('-d, --dev', 'Use dev mode');

program
    .parse();

Данные инструкции укажут интерфейсу что у него есть команда components и команда widgets, имеющая всего один аргумент name и флаг dev-сборки. А метод parse позволит интерфейсу распознать переданные аргументы.

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

import { program } from 'commander';
import makeConfig, { BuildType } from './webpack.config';
import prod from './prod.compiler';
import dev from './dev.compiler';

let config = null;

program
    .command('components', 'build components library')
    .action(() => {
        config = makeConfig('components', BuildType.components, false);

        prod(config);
    });

program
    .command('widget', 'build widget by name')
    .argument('<name>')
    .option('-d, --dev', 'Use dev mode')
    .action((name, options) => {
        const { dev: isDev } = options;

        config = makeConfig(name, BuildType.widget, isDev);

        (isDev ? dev : prod)(config);
    })

program
    .parse();

Теперь, выполнив любую команду наш CLI сможет собрать библиотеку компонентов, любой виджет, а также убрать из сборок код, который будет задублирован.

Заключение

Таким образом мы рассмотрели альтернативный способ сборки виджетов посредством webpack. Этот способ позволяет взять контроль над сборкой в свои руки. Однако, материалы представленные здесь не являются готовым решением, но при этом описывают сам концепт решения нетривиальной задачи, а именно – оптимизации.

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

А посему у нас остается один вариант – разбить на модули так, чтобы каждый разработанный виджет имел наименьшее число включенных зависимостей.

Комментарии (1)


  1. chernish2
    23.12.2021 22:11

    Macron 1!