Однажды, читая документацию по 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}}*/
Если был установлен флаг изменения массива лоадером, то происходит сборка конечного скрипта из всех доступных частей.
Как это всё попробовать
Там же и описание настроек.