Когда мы начали с нуля создавать новую веб-платформу в 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 с фактической структурой папок. У этого подхода есть хорошие и плохие стороны, но его стоит учитывать.