Бэкэнд в однофайловых компонентах VueJS

Однажды, читая документацию по Vue Loader, наткнулся на интересное нововведение в 15 версии. Речь идет о кастомных блоках, которые можно внедрить в однофайловые компоненты Vue. В примере показано, как можно получить доступ к содержанию этого блока непосредственно в компоненте. Я сначала как бы не придал особой ценности этой возможности, но потом подумал, хм..., а если туда запихнуть бэк связанным с этим куском фронта… И понеслось...


Бэк у меня на то время (год назад) был на php. Для начала, я решил посмотреть, как мой любимый редактор PhpStorm справится со вставкой кода php в этот блок. Как ни старался, речи о какой-либо подсветки кода и прочих автокомплитных функций не шло. Думаю, напишу ка я issue в тех поддержку JetBrains. Через некоторое время мне пришел отрицательный ответ, о какой-либо возможности это настроить, но прислали инструкцию как это настроить для javascript. Ну думаю ладно, идею всё равно надо попробовать реализовать. Ранее никогда мне не приходилось разрабатывать что-либо для Webpack'а. День изучал документацию, и за последующие два вечера разработал Loader и плагин. Всё это работало, но без элементарной подсветки синтаксиса в кастомных блоках .vue, код php приносил только боль...


Шло время. Потихоньку знакомясь с nodejs и следя за change логом изменений в новых версиях, нахождению полезных и готовых решений для себя, я начал понимать, что при выборе — на чем писать бэк, я буду использоваться все же ноду. Запуск несколько копий приложений на ноде и разруливание нагрузки на эти копии используя ngnix, давали лучшие результаты. Недавно вернулся к этой теме и доработал лоадер и плагин.


Начну с шаблона


Шаблон для бэкэнда


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


Шаблон бэкэнда
const WEB_PORT = 314;

const Koa = require('koa');
var Router = require('koa-router');

const app = new Koa();
var router = new Router();

app
    .use(router.routes())
    .use(router.allowedMethods());

const body = require('koa-json-body')({ limit: '10kb' });

app.listen(WEB_PORT);

app.context.db = require('../lib/db.js');
/*{{endpoints}}*/

/*{{endpoints}}*/ — это то место, куда будет вставляться код из кастомных блоков


Webpack loader


Код загрузчика
var loaderUtils = require("loader-utils");
var path = require('path');

const id = 'gavrilow_backend_plugin';

exports.default = function (source) {
    this.cacheable();

    // Отправляем данные далее следующему загрузчику
    // ВАЖНО!!! Отправляем пустую строку, иначе все что отправим попадет в конечную сбрку
    this.async()(null, '');

    // Удаляем все переносы строк. Их очень много.
    const _source = source.replace(/^\n/img, '');

    // Путь к файлу в котором содержится Custom Block [blockType=backend]
    const file_path = this.resourcePath;

    // this._compiler - глобальный объект, который доступен из плагина
    if (this._compiler[id] === undefined)
        this._compiler[id] = {
            change: true,
            arr: []
        };

    var fp_exists = false;

    // Перебираем массив и ищем ранее добавленный код из Custom Blocks vue
    // Идентификатор блока - полный путь файлу.
    for (let i = this._compiler[id].arr.length - 1; i >= 0; i--) {
        if (this._compiler[id].arr[i].file_path === file_path) {
            fp_exists = true;

            // если нашли, то сравним с прошлой версией.
            if (this._compiler[id].arr[i].data !== _source) {
                // если есть изменения то сохраяем исменения в объект и для палагина выставляем флаг, что были изменения
                this._compiler[id].arr[i].data = _source;
                this._compiler[id].change = true;
            }

            break;
        }
    }

    if (fp_exists) return; // Если выше был заход в первое условие в цикле, то выходим

    // Добавлеме новый объект в массив, содержащий тест Custom Blocks и полный поуть к файлу
    // и сигнализируем флагом [ change = true ] для плагина что есть изменения.
    this._compiler[id].change = true;
    this._compiler[id].arr.push({
        file_path: file_path,
        data: _source
    });
};

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


Webpack plugin


Код плагина
const fs = require('fs');
const util = require('util');

const readFile = util.promisify(fs.readFile);
const writeFile = util.promisify(fs.writeFile);

var footer_header_template;

class gavrilow_backend_plugin {
    constructor(options) {
        this.options = options;
        this.logMess = '';
    }

    endLog(){
               this.logMess = '------ gavrilow-backend-plugin ------------------------------------------------------------------\n'
            +this.logMess;
        this.addLogMess('-------------------------------------------------------------------------------------------------');
        console.log(this.logMess);
        this.logMess = '';
    }

    addLogMess(mess){
        this.logMess += mess+'\n';
    }

    async prepareTemplate(){
        try {
            if (footer_header_template === undefined) {
                let contents = await readFile(this.options.backend_template, "utf-8");
                footer_header_template = contents.split(/^\/\*+?{{.*endpoints.*}}+?\*\/$/img);
                if (footer_header_template.length !== 2) {
                    footer_header_template = undefined;
                    this.addLogMess('Не удалось найти точку вставки блоков.');
                    this.endLog();
                    return false;
                } else return true;
            } else return true;
        } catch (err) {
            footer_header_template = undefined;
            throw err;
        }
    }

    apply(compiler) {
        compiler.hooks.emit.tapAsync(
            'gavrilow_backend_plugin',
            (compilation, callback) => {
                callback();

                if (this.options.backend_template === undefined || this.options.backend_template === '') {
                    this.addLogMess('Необходимо создать и/или указать файл-шаблон для бэкэнда...');
                    this.endLog();
                    return;
                }

                if (this.options.backend_output === undefined || this.options.backend_output === '') {
                    this.addLogMess('Необходимо указать путь и имя js файла для бэкэнда...');
                    this.endLog();
                    return;
                }

                if (!compiler.gavrilow_backend_plugin) {
                    this.addLogMess('В Вашем проекте нет ни одной секции для бекенда [ <backend>...</backend> ].');
                    this.endLog();
                    return;
                }

                (async ()=>{
                    try {
                        // Подготваливаем шаблон
                        if (!await this.prepareTemplate())
                            return;

                        // Если загрузчик не выставил флаг сигнализирующий о каких-либо изменений
                        if (!compiler.gavrilow_backend_plugin.change) return; // Если ничего для бэка не поменялось

                        // сбрасываем флаг
                        compiler.gavrilow_backend_plugin.change = false;

                        if (compiler.gavrilow_backend_plugin.arr.length === 0) {
                            this.addLogMess('По какой-то причине нет данных из секции [ <backend>...</backend> ]');
                            this.endLog();
                            return;
                        }

                        this.addLogMess('Собираем beckend: "'+this.options.backend_output+'"\n...');

                        // записываем все что выше /*{{endpoints}}*/ в шаблоне
                        var backend_js = footer_header_template[0]+"\n";

                        // конкатенация кусков кода из Custom Blocks
                        for (let i = 0; i < compiler.gavrilow_backend_plugin.arr.length; i++) {
                            backend_js +=compiler.gavrilow_backend_plugin.arr[i].data+"\n";

                            this.addLogMess('['+compiler.gavrilow_backend_plugin.arr[i].file_path+']');
                        }

                        // присоединяем все что ниже /*{{endpoints}}*/ в шаблоне
                        backend_js += footer_header_template[1];

                        // асинхронно записываем результат
                        await writeFile(this.options.backend_output, backend_js);

                    } catch (err) {
                        throw err;
                    } finally {
                        this.endLog();
                    }
                })();

            }
        );
    }
}

gavrilow_backend_plugin.loader = require.resolve('./loader');

module.exports = gavrilow_backend_plugin;

Плагин срабатывает по окончанию сборки проекта. Подготавливает шаблон разбивая его на 2 части: до /*{{endpoints}}*/ и после /*{{endpoints}}*/ Если был установлен флаг изменения массива лоадером, то происходит сборка конечного скрипта из всех доступных частей.




Как это всё попробовать


Проект залил на гитхаб.


Там же и описание настроек.

  • +10
  • 2,8k
  • 4
Поделиться публикацией

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

    +2
    Можно пояснить зачем это нужно?
    Из статьи понял что чтото кудато можно вставить, вот вам плагин и все…
      0
      Да вам проще репозиторий глянуть, автор там пример показал
      0
      Плюсик за энтузиазм, но по жизни неприменимо.
      Т.к как в народе уважается MVC и другие подходы.
        0
        Главный вопрос — зачем? Приведите хоть один use case.

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

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