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