Когда мы начали с нуля создавать новую веб-платформу в ESGgen (www.esggen.com), я хотел немного рассказать об этом путешествии и о том, как мы подошли к определенным вещам. Обычно я не пишу технические посты, но я решил попробовать что-то другое. В будущем я мог бы рассказать о таких вещах, как языковые пакеты, процесс сборки, конвейеры выпуска, стратегию ветвления, примечания к выпуску, управление версиями и многое другое. А теперь я начну с чего-то относительно простого, например, улучшенная настройка путей маршрутизатора…
Проблема
У нас будут сотни представлений и файлов в папке /views/*, что не редкость. Тем не менее, все вспомогательные файлы и конфигурации не живут рядом с ними. Таким образом, в типичной структуре папок (которую я видел слишком много раз и не принимаю в качестве решения) мы получаем такие вещи, как пути маршрутизатора, конфигурация хранилища, модульные тесты и активы, которые обычно находятся где-то в корне, а не быть вместе с определенным представлением, к которому они относятся. Это первый шаг в ад, когда приложение быстро растет и к проекту добавляются новые команды. Тем не менее, мы надеемся, что все не окажутся в аду PR-конфликта, который легко может вывести из строя лучшую команду разработчиков программного обеспечения.
Что мы пытаемся решить
- Пути конфигурации маршрутизатора определены в одном массивном файле, расположенном в файле /router/index.js, где и происходит вся магия.
- Путаница и когнитивные накладные расходы связаны с редактированием и пониманием сложных файлов, в которых может произойти что угодно.
- Перенесите ответственность за обслуживание путей маршрутизатора со «всех» на «человека, работающего над конкретным представлением».
- Разделите конфигурацию путей маршрутизатора на управляемые фрагменты, которые живут вместе с отвечающим за них представлением.
- Избавьтесь от необходимости обновлять приложение во многих местах, когда мы решим удалить или добавить новое представление.
- Упростите PR и создайте меньше возможностей для конфликтов при слиянии кода.
Решение
Я искал разные варианты, и у людей были разные предложения по решению этой проблемы. Тем не менее, ни один из них мне не понравился. В большинстве случаев требуется какая-то центральная конфигурация, которая будет импортировать эти разрозненные конфигурации маршрутизатора. В других случаях решение было запутанным и не соответствовало моим целям. Поэтому я решил использовать свой опыт создания правильных и сложных процессов сборки для интерфейсных приложений, чтобы решить эту проблему. Это было проще, чем я ожидал.
- Каждое представление имеет собственный файл /view/index.route, в котором настраиваются пути для этого конкретного представления.
- Имя представления диктует имена путей (не обязательно), что немного упрощает поиск и отладку.
- Сохраняйте формат конфигурации маршрутизатора близким к исходному (простой объект).
- Созданный процесс должен сгенерировать окончательный файл конфигурации маршрута на основе всех этих фрагментированных конфигураций из каждого представления.
- Он должен быть быстрым и не оказывать негативного влияния на производительность интерфейса браузера.
- Должен поддерживать статические пути (не динамически загружаемые представления) из конфигурации корневого маршрутизатора (файл шаблона).
Это часть моего пути к созданию простых приложений для пользователей, которые будут взаимодействовать с пользовательским интерфейсом, и инженеров-программистов, которые посвятят свою жизнь их созданию и поддержке. Эти простые решения имеют значение, когда команда увеличивается до 50 инженеров в 10 различных группах доставки. Мы все будем работать над одним репозиторием, разрабатывая 4 приложения одновременно, используя общее ядро. Это одна из тех вещей, которые обычно остаются позади. Не инженеры считают это неважным, и это никогда не делается позже, потому что команда продукта просит предоставить другую функцию.
Шаг 1: Определите шаблон router/index.js
Мы знаем, где будет жить конфигурация маршрутизатора и как она будет выглядеть (по крайней мере, приблизительно), поэтому мы можем определить пустой шаблон, используемый позже в процессе сборки для создания фактического файла.
Я решил создать шаблон как index.js, а окончательный файл называется index.built.js. Единственная причина в том, что его будет проще очищать после каждой сборки и добавлять в .gitignore. Конечно, вы можете решить поступить наоборот, как вам удобно.
/******* * THIS IS A TEMPLATE FILE FOR THE BUILD PROCESS * The below "marker" will be filled out every time the build process runs * It is like a pre-processor which will find all *.route files and create dynamic router configuration *******/ import { createRouter, createWebHistory } from 'vue-router' let routes = [ %ROUTES% ]; let router = createRouter({ history: createWebHistory(process.env.BASE_URL), routes }) export default router
Шаг 2: Создайте файл view/MyView/index.route
Каждое представление, имеющее путь и доступное в браузере, имеет этот файл в папке и включает все соответствующие конфигурации маршрутизатора. Так как я использую динамически загружаемые представления, я не занимаюсь статическим импортом и забочусь об этой опции. Можно настроить зависимость статического представления только в файле шаблона роутера, этого мне достаточно (в случае необходимости).
Как я уже говорил ранее, я хотел, чтобы локальный файл index.route был как можно ближе к исходному формату конфигурации маршрутизатора. Так что, если быть точным, это то же самое, просто литерал объекта JavaScript (вроде).
{ path: '/MyView', name: 'MyView', component: () => import(/* webpackChunkName: "MyView" */ '../views/MyView/index.vue') }, { path: '/MyView/SubView', name: 'MyView-SubView', component: () => import(/* webpackChunkName: "MyView-SubView" */ '../views/MyView/SubView/index.vue') }
Шаг 3: Создайте процесс для вывода окончательной конфигурации router/index.js
Я использую nodeJS и пользовательский Webpack (а не интерфейс командной строки VueJS по умолчанию) для создания всего внешнего интерфейса, поэтому у меня достаточно гибкости для запуска скриптов.
Каждая сборка для разработки, производства или наблюдения потребует от меня запуска скрипта, который будет захватывать все файлы *.route и генерировать окончательную конфигурацию маршрутизатора. Поэтому я создал скрипт preprocessor.js, который будет запускаться узлом, и он будет проходить через исходную папку, находить все файлы *.route, объединять выходные данные и сохранить во вновь созданный глобальный файл конфигурации маршрутизатора.
Да, я сделал это красиво. Мне нравятся чистые комментарии в консоли, и я действительно забочусь о производительности, поэтому я добавил несколько простых таймеров на случай, если это станет узким местом в будущем из-за большого количества файлов.
const helpers = require('./helpers.js'); console.log('\nGENERATE VUEJS ROUTER ***************************'); console.time('* Total Time'); let allRoutes = helpers.findFiles('src', '.route'); helpers.saveResults('src/router/index.js', allRoutes, '%ROUTES%'); console.timeEnd('* Total Time');
Шаг 4: Несколько функций, которые потребуются
Как видите, я извлек несколько дополнительных методов в файл helpers.js, так как я буду использовать их позже для создания других вещей, таких как языковые пакеты.
Я большой поклонник спецификации JSDoc, поэтому неудивительно, что все мои методы имеют надлежащее описание, соответствующее этой спецификации. Благодаря этому IDE помогает при написании кода подсказками и подсвечивает ошибки, если я пытаюсь передать функциям что-то, что не соответствует ожиданиям.
helpers.findFiles()
/** * Recursive function to parse all subfolders from a parent, find specific files and return finalOutput for future parsing * @param {string} startPath - root folder path * @param {string} filter - an extension of files we looking for * @param {string} finalOutput - object to be updated will parsed files we found */ exports.findFiles = function(startPath, filter, finalOutput=''){ // error handling, just in case if (!fs.existsSync(startPath)){ console.log("\x1b[31mn* Directory doesn't exist. ",startPath); return; } // do the job var files=fs.readdirSync(startPath); for(var i=0;i<files.length;i++){ var filename=path.join(startPath,files[i]); var stat = fs.lstatSync(filename); if (stat.isDirectory()){ finalOutput = findFiles(filename, filter, finalOutput); //recurse } else if (filename.indexOf(filter)>=0) { console.log('*',filename); let fileContent = fs.readFileSync(filename, 'utf8'); // just in case, check for comma at the end as it shouldn't be there. if (fileContent.slice(-1) === '}' || fileContent.slice(-1) === ']' ) { fileContent += ',\n' } finalOutput += fileContent }; }; return finalOutput }; exports { findFiles }
В этот файл можно было бы внести несколько улучшений, но я предполагаю, что разработчики достаточно умны, чтобы не допускать простых ошибок. Однако, если они это делают, а это происходит часто, мы можем выполнить некоторую проверку, чтобы убедиться, что форматирование файла правильное.
helpers.saveResults()
/** * It will take the given file and update the placeholder * @param {string} destinationFile - the template file * @param {string} content - what we want to write in the file * @param {string} placeholder - placeholder name * @param {string} [suffix] - optional extra suffix */ exports.saveResults = function (destinationFile, content, placeholder, suffix) { if (suffix) { suffix+='.'} else { suffix = ''} let extension = destinationFile.split('.').pop(); let generatedFilename = destinationFile.replace(extension, suffix+PROCESSED_SUFFIX+extension); fs.copyFile(destinationFile, generatedFilename, (err) => { if (err) throw err; let fileContent = fs.readFileSync(generatedFilename, 'utf8'); let finalContent = fileContent.replace(placeholder, content); fs.writeFile(generatedFilename, finalContent, function (err) { if (err) throw err; }); }); }
Это просто запишет вывод findfiles(), объединенную строку содержимого из всех найденных файлов *.route.
Шаг 5: Убери за собой!
Этот шаг необязателен, но после преобразования и запуска процесса сборки Webpack (это другой монстр, о котором я расскажу в будущем), мне на самом деле не нужен router.built.js. файл больше. Я очищаю все сгенерированные файлы, и поскольку все они содержат «.built». от их имени их легко удалить.
helpers.deleteFiles('src/', '.built.'); console.time('* Deleted all .built. files from /src folder.');
Результаты этого небольшого скрипта процесса сборки
В итоге я получил файл router/index.built.js, который содержит все конфигурации маршрутов из фрагментированных конфигураций, разбросанных по папкам представлений. Сценарий работает очень быстро, так как его выполнение занимает всего несколько миллисекунд, и при необходимости его можно без труда запускать перед каждой сборкой или просмотром. Для удобства я просто включил его в скрипт сборки preprocessor.js, который запускается перед любым действием сборки в приложении.
Пример окончательного файла router/index.js
/******* * THIS IS A TEMPLATE FILE FOR THE BUILD PROCESS, NOT THE FINAL FILE TO BE USED WITHE THE CODE! * The below "marker" will be filled out every time build process runs * It is like a pre-processor which will find all *.route files and create dynamic router configuration *******/ import { createRouter, createWebHistory } from 'vue-router' const routes = [ { path: '/about', name: 'About', component: () => import(/* webpackChunkName: "about" */ '../views/About.vue') }, { path: '/SecondView', name: 'SecondView', component: () => import(/* webpackChunkName: "SecondView" */ '../views/SecondView/index.vue') }, { path: '/Thirdview', name: 'Thirdview', component: () => import(/* webpackChunkName: "Thirdview" */ '../views/Thirdview/index.vue') }, { path: '/Thirdview/something', name: 'something', component: () => import(/* webpackChunkName: "something" */ '../views/Thirdview/something/index.vue') }, { path: '/Thirdview/nothing', name: 'Thirdview-nothing', component: () => import(/* webpackChunkName: "hirdview-nothing" */ '../views/Thirdview/nothing/index.vue') } ]; const router = createRouter({ history: createWebHistory(process.env.BASE_URL), routes }) export default router
Что мы можем сделать сейчас, так это создать в Webpack псевдоним, который будет гарантировать, что всякий раз, когда мы вызываем импорт файла конфигурации маршрутизатора, мы можем использовать router/index.js вместо router/index. .build.js. Поскольку есть только одно место, где мы будем вызывать эту конфигурацию, и она, скорее всего, никогда не изменится, вы можете пропустить ее. Я буду :)
Помните одну вещь: не бойтесь создавать пользовательские сценарии сборки, так как это дает очень много преимуществ, а люди редко так делают. Это не сложно, и все любят человека, который облегчил жизнь всей команде ;)
- РЕДАКТИРОВАТЬ
Через два дня после написания этого сообщения в блоге я понял, что могу автоматизировать весь процесс. В данный момент мне это не нужно, а полная автоматизация лишит меня некоторой гибкости, и как только я это сделаю, я пойму, что она мне нужна.
Идея заключалась в том, чтобы создать сценарий сборки, который проходит через все папки и подпапки /views/*, ищет все файлы index.vue и использует их пути для создания окончательного конфигурация маршрута маршрутизатора. Предполагается, что каждый элемент в наших представлениях общедоступен и доступен по пути, но он всегда будет связывать пути URL с фактической структурой папок. У этого подхода есть хорошие и плохие стороны, но его стоит учитывать.