Как backend разработчики frontend писали (Vue + TS + Webpack)

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

У нас в команде есть пару проектов, для которых есть старые frontend. Написаны все они на разных технологиях, но объединяет их одно: нежелание кого-либо туда лезть и что-то править. Команде там кажется страшно, непонятно и неудобно. Любая доработка превращается в головную боль. В очередном проекте нам хотелось не допустить такого развития событий, и, кажется, у нас получилось.

Данная статья предназначена не для полноценных frontend разработчиков, а для членов команд, которым требуется реализовать небольшой frontend не имея должной экспертизы в этом вопросе. И сделать это так, чтобы каждый новый сотрудник без глубокого погружения мог сразу делать небольшие доработки.

В рамках этой статьи мы построим скелет будущего frontend приложения. На основе этого скелета и наших ошибок, о которых мы рассказали далее, можно брать и пилить проект с низким порогом вхождения (опробовали на новых сотрудниках).

Мы постарались написать приложение так, чтобы backend разработчикам было наиболее привычно и комфортно. Классы, интерфейсы, наследование, типизация, вот это вот всё… И, конечно же, чтобы визуально это смотрелось красиво и современно. Для всех этих целей мы выбрали Vue и TS. Перед началом работ советую ознакомиться с документацией по vue и vue router

Итак, начнём…

1. Скелет проекта

Нам потребуются установленные Node.js и npm (диспетчер пакетов Node.js). Напоминаю, что безопаснее пользоваться дистрибутивами и пакетами, которые вышли раньше 24 февраля 22 года.

curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash -
sudo apt-get install -y nodejs

Нам понадобятся:

  • Vue (документация тут)

  • TypeScript

  • Axios (для запросов к серверу)

  • Vue Router (поддержка роутинга во Vue о которой можно почитать тут)

  • Vuex (позволяет общаться компонентам между собой. Можно ознакомиться тут)

  • CSS Pre-processors

  • Linter / Formatter (анализ качества вашего кода). Например: eslint или tslint.

  • Пакеты vue-class-component и vue-property-decorator для того чтобы привести к классо-ориентированному виду, который все мы так любим

  • UI Framework с которым мы будем работать, для того чтобы не было мучительно больно изобретать велосипеды, рисовать кнопочки и заниматься другими трудоемкими вещами. Мне приходилось работать со следующими фреймворками:
    - boostrap-vue (показался не удобным)
    - element-ui (большое разнообразие компонентов)
    - vuetify (достаточное количество компонентов и хорошая документация)
    Для своего проекта мы выбрали element ui из-за обилия различных компонентов.

Все это можно легко поставить и развернуть при помощи vue-cli, но мы в команде выбрали другой путь. После всем нам известных событий, много библиотек стали тянуть транзитивно вредоносные зависимости. Поэтому команда приняла решение обойтись без vue-cli и использовать webpack для более очевидного управления зависимостями и более гибкой сборки проекта.

Пример получившегося package.json:
{
 "name": "hello-world",
 "version": "1.0.0",
 "scripts": {
   "build:dev": "npx webpack",
   "build:prod": "npx webpack --env production",
   "lint": "eslint . --ext .ts",
   "lint:fix": "npm run lint -- --fix",
   "serve": "npx webpack serve"
 },
 "dependencies": {
   "axios": "0.25.0",
   "element-ui": "2.15.6",
   "ts-jenum": "2.2.2",
   "vue": "2.6.14",
   "vue-axios": "3.4.0",
   "vue-cookies": "1.7.4",
   "vue-router": "3.5.3",
   "vuex": "3.6.2"
 },
 "devDependencies": {
   "@babel/core": "7.17.0",
   "@babel/preset-env": "7.16.11",
   "@babel/preset-typescript": "7.16.7",
   "@babel/runtime": "7.17.0",
   "@types/webpack-env": "1.16.3",
   "@typescript-eslint/eslint-plugin": "5.21.0",
   "@typescript-eslint/parser": "5.21.0",
   "@vue/eslint-config-typescript": "10.0.0",
   "babel-loader": "8.2.3",
   "babel-preset-vue": "2.0.2",
   "clean-webpack-plugin": "4.0.0",
   "css-loader": "6.6.0",
   "eslint": "^8.14.0",
   "eslint-plugin-vue": "^8.7.1",
   "eslint-webpack-plugin": "^3.1.1",
   "file-loader": "6.2.0",
   "html-webpack-plugin": "5.5.0",
   "mini-css-extract-plugin": "2.5.3",
   "sass": "1.49.7",
   "sass-loader": "12.4.0",
   "ts-loader": "9.2.6",
   "tsconfig-paths-webpack-plugin": "3.5.2",
   "typescript": "4.5.5",
   "url-loader": "4.1.1",
   "vue-class-component": "7.2.6",
   "vue-loader": "15.9.8",
   "vue-property-decorator": "9.1.2",
   "vue-template-compiler": "2.6.14",
   "webpack": "5.68.0",
   "webpack-cli": "4.9.2",
   "webpack-dev-server": "4.7.4"
 },
 // Избавляемся от вредоносных версий. Работает только с npm > 8.3
 "overrides": {
   "node-ipc@>9.2.1 <10": "9.2.1",
   "node-ipc@>10.1.0": "10.1.0"
 }
} 

Для установки всех пакетов на основе package.json достаточно выполнить команду npm i

Следующим шагом модифицируем конфиг для анализа нашего кода .eslintrc.js. Правила, описанные ниже, это лишь субъективное мнение автора, каждый волен настроить их под свой вкус и цвет.

.eslintrc.js
module.exports = {
   root: true,
   env: {
       node: true
   },
   // Подключаем рекомендованные правила
   "extends": [
       "plugin:vue/recommended",
       'eslint:recommended',
       "@vue/typescript/recommended"
   ],
   parser: "@typescript-eslint/parser",
   parserOptions: {
       ecmaVersion: 2020,
       project: ["./tsconfig.json"],
   },
   // Дополняем рекомендованные правила своими
   rules: {
       // Отключаем вывод в консоль для прода
       "no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
       // Отключаем дебаг для прода
       "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
       // Отключаем for in для массивов
       "@typescript-eslint/no-for-in-array": "warn",
       // Не ставим await в return
       "no-return-await": "warn",
       // Никаких any
       "@typescript-eslint/no-explicit-any": "warn",
       // Настраиваем отступы
       "indent": ["warn", 4],
       // Нет лишним пробелам
       "no-multi-spaces": "warn",
       // Пробелы перед/после ключевых слов
       "keyword-spacing": [2, {"before": true, "after": true}],
       // Проверка типов при сложении
       "@typescript-eslint/restrict-plus-operands": "warn",
       // Сравнение через тройное равно
       "eqeqeq": "warn",
       // Длинна строки кода
       "max-len": ["warn", { "code": 160 }],
       // Предупреждаем о забытых await
       "require-await": "warn",
       // Предупреждаем о забытых фигурных скобках
       "curly": "warn",
       // Максимальное количество классов в файле
       "max-classes-per-file": ["warn", 2],
       // Двойные кавычки
       "quotes": ["warn", "double"],
       // Проверка точек с запятой
       "semi": ["warn", "always"]
   }
}

Для проверки кода через eslint достаточно будет выполнить код: npm run lint

2. Сборка проекта

Перейдём к самой ужасной части: сборке проекта на webpack. На самом деле это не так страшно, как выглядит на первый взгляд. Есть отличная документация по каждому используемому плагину. Поэтому я приложу код сборки проекта с небольшими комментариями. Актуально для webpack 5 версии.

webpack.config.js
const path = require("path");
const { DefinePlugin } = require("webpack");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { VueLoaderPlugin }  = require("vue-loader");
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
const ESLintPlugin = require('eslint-webpack-plugin');

module.exports = env => {

 return {
   context: path.resolve(process.cwd(), "src"),
   devtool: env.production === true ? false : "eval-cheap-source-map",
   mode: env.production === true ? "production" : "development",
   performance: {
     hints: false,
   },
   // Точки входа
   entry: {
     "main": ["./ts/main.ts"]
   },

   // Что получаем на выходе
   output: {
     path: path.resolve(process.cwd(), "dist"),
     filename: "js/main.js",
     publicPath: !!process.env.WEBPACK_DEV_SERVER ? "/" : "./",
   },

   resolve: {
     plugins: [new TsconfigPathsPlugin()],
     extensions: [".ts", ".js", ".vue", ".json"],
     alias: {
       vue$: "vue/dist/vue.esm.js"
     }
   },

   // Dev сервер
   devServer: {
     devMiddleware: {
       index: true,
       publicPath: '/',
       writeToDisk: true
     },
     static: {
       directory: path.join(__dirname, 'dist')
     },
     port: 9000,
     hot: true
   },

   module: {
     rules: [
       // Загрузчик TS файлов
       {
         test: /\.tsx?$/,
         loader: "ts-loader",
         exclude: /node_modules/,
         options: {
           appendTsSuffixTo: [/\.vue$/]
         }
       },
       // Загрузчик vue файлов (хотя мы их не используем, но вдруг кому понадобится)
       {
         test: /\.vue$/,
         use: "vue-loader",
       },
       // Загрузчик изображений
       {
         test: /\.(png|jpg|gif|svg|ico)$/,
         loader: "file-loader",
         options: {
           name: "static/[name].[ext]?[hash]"
         }
       },
       // Загрузчик js файлов
       {
         test: /\.js$/,
         loader: "file-loader",
         exclude: /node_modules/,
         options: {
           name: "js/[name].[ext]"
         }
       },
       // Загрузчик стилей
       {
         test: /\.(css|sass|scss)$/,
         use: [
           // Минификатор стилей
           {
             loader: MiniCssExtractPlugin.loader,
             options: {
               publicPath: (resourcePath, context) => {
                 return path.relative(path.dirname(resourcePath), context) + "/";
               },
             },
           },
           // Загрузчик обычных css стилей
           "css-loader",
           // Sass-загрузчик
           {
             loader: "sass-loader"
           }
         ]
       }
     ]
   },

   plugins: [
     // Очищает build директорию
     new CleanWebpackPlugin(),

     // Формирует html. Подсовывает title, делает внедрение js в body
     new HtmlWebpackPlugin({
       inject: "body",
       template: "index.html",
       title: "Hello-world"
     }),

     // Запускает проверку кода через eslint
     new ESLintPlugin({
       extensions: "ts"
     }),

     new VueLoaderPlugin(),

     // Минифицирует стили
     new MiniCssExtractPlugin({
       filename: 'static/style.css'
     }),

     new DefinePlugin({
       __VUE_OPTIONS_API: JSON.stringify(true),
       VUE_PROD_DEVTOOLS: JSON.stringify(env.production !== true),
     }),
   ]
 }
};

По итогу первых двух пунктов получаем скелет нашего проекта, который можно посмотреть тут

3. Время собирать камни…

Не все решения, которые мы приняли в ходе разработки, были хороши. Было над чем поработать после, чтобы привести в подобающий вид. Собственно, подробнее дальше.

3.1. Дублирование кода и шаблона.

Большинство задач с переиспользованием кода во Vue решаются посредством композиции компонентов. Обычно приложение организуется в виде дерева вложенных компонентов.

Если мы видим, что у нас в другом месте намечается точно такая же функциональность, в первую очередь попробуйте выделить это в отдельный компонент.

Пример: был сложный компонент редактора документов на 2000 строк кода и на 3000 строк шаблона. В самом простом виде он выглядит так:

Пришла задача от бизнеса сделать ещё один редактор другого типа документов. Этот редактор отличается всего лишь на 25%. У нас решили эту задачу посредством наследования.

Что не так:

  1. Компонент на 2000 строк кода и 3000 строк шаблона это уже сигнал о том, что что-то не так. При открытии такого компонента хочется плакать.

  2. Наследование нас спасло от дублирования кода, но не от дублирования 3000 строк шаблона.

Решение: На рисунке выше можно уже увидеть, что блоки “Поля документов”, “История изменений”, “Статус документа”, “Связанные документы” и “Вложения” очень хорошо ложатся в отдельные компоненты. Всего лишь нужно вынести это всё в отдельный класс.

Если эти компоненты совпадают на 100% у обоих редакторов (Вложения, история изменений, связанные документы), то таким подходом мы избавились от дублирования и кода и шаблона для этой части.

Если компоненты отличаются немного, то можно сделать их настраиваемыми через Props.

После того, как редактор был разбит на мелкие компоненты мы получили:

  • Избавление от дублирования и кода и шаблона

  • Маленькие и понятные компоненты, которые имеют лишь единственную обязанность. Такие компоненты легко понимать и модифицировать.

При избавлении от дублирования лучше начинать именно с разбиения на компоненты, и только если это не помогает, переходить к другим вариантам.

3.2 Используйте Single File Component

Избавляемся от .vue, .ts и .html файлов и склеиваем их в один .ts файл. “Зачем это всё?” — спросите вы. Просто данный стиль ближе по духу разработчикам, не имевшим дело с frontend. Он менее пугающий. А также это позволяет посмотреть на все ресурсы компонента сразу в одном файле. Это просто удобнее чем, открывать три разных файла.

Если вы используете vue-cli вам потребуется в vue.config.ts проставить флаг runtimeCompiler: true
Склеенные компоненты выглядят следующим образом:

import { Component, Vue } from 'vue-property-decorator';

@Component({
 template: `
   <div class="about">
     <h1>This is an about page</h1>
   </div>
 `
})
export default class AboutView extends Vue {
    // Code…
}

3.3 Не изобретайте велосипедов

Есть простые UI frameworks с кучей готовых, красивых и функциональных компонентов. Взяв на вооружение такой, можно без больших усилий реализовать почти все что потребуется, затратив минимум усилий. Да, UI будет выглядеть немного шаблонно, но функционально.

У нас встречались свои велосипеды в виде каких-то таблиц и прочего. Это приводило к ужасному визуальному виду и множеству багов. В итоге свои компоненты-таблицы были удалены и прикручены таблицы из UI framework, с небольшой кастомизацией, а восторгу от красоты новых таблиц у пользователей продукта не было предела…

3.4 Взаимодействие компонентов

Основа работы Vue — однонаправленный поток данных. Это значит, что данные из компонентов верхних уровней передаются в компоненты нижних уровней через входные параметры (или props). А для обратной связи наверх используются события (дочерние компоненты уведомляют о произошедшем событии и, возможно, передают какие-то данные). А теперь рассмотрим пример приложения со следующей структурой компонентов:

Что делать, если потребуется передать данные из дочернего 1.1.1 компонента в дочерний 2.3.1? Для этого есть два подхода:

  1. Vuex

  2. Глобальная шина событий

Рассмотрим подробнее глобальную шину событий, так как это один из самых простых способов.

Глобальная шина событий.
Данный подход позволяет передавать событие из любого компонента в любой. Реализуется это посредством создания пустого экземпляра Vue и его импорта.

export const bus = new Vue();
// ComponentA.ts (импортируем шину и генерируем в неё события)
import { bus } from "bus.js";
bus.$emit("my-event"[, данные]);
// ComponentB.ts (импортируем шину и отслеживаем в ней события)
import { bus } from "bus.js";
bus.$on("my-event", this.myEventHandler);

3.5. Динамические компоненты.

Если у вас есть одна сущность, но отображать эту сущность нужно через разные компоненты, не надо делать для этого разные роуты. Это может привести к интересным последствиям.

Пример: Был компонент редактора документов. Пришел бизнес и сказал, что некоторые документы нужно отображать в другом редакторе. Реализовано это было посредством разных ссылок. По одной ссылке (editor/101) открывался один компонент редактора документов. По другой ссылке (another_editor/102) открывался другой компонент редактора документов.

Проблемы: пользователи часто из одного редактора (editor/101) меняли в адресе номер документа на другой документ (у них был в наличии нужный им номер документа) и получали не тот редактор, который должен был открыться. Серверная часть, конечно, валидировала это недоразумение, но ситуация для пользователя неприятная.
Решение: Указываем роут на компонент, который будет отвечать за выбор нужного редактора на основе какого-либо признака (рабочий пример добавил вместе со скелетом приложения тут)

@Component({
   template: `
     <component v-if="document"
                :key="document.id"
                :is="component"
                :document="document"></component>
   `
})
export default class DocumentEditor extends Vue {

   /** Документ */
   private document: Document | null = null;

   . . .

   /**
    * Возвращает компонент, который требуется показать клиенту,
    * на основе типа документа или какого-либо другого признака
    */
   private get component(): VueClass<Vue> {
       if (this.document?.type === "TYPE_ONE") {
           return AnotherEditor;
       }
       return CommonEditor;
   }
}

3.6. Глобальный обработчик событий.

Чтобы отобразить пользователю ошибку, по всему проекту встречались вот такие куски кода при каждом обращении к серверу (и не только):

try {
  ...
} catch (e) {
  this.$notify({
    title: "Ошибка",
    type: "error",
    message: e.message
  });
  throw e;
}

Все поведение этого кода сводится к тому что мы отлавливаем ошибку и отображаем её пользователю. Но можно добавить глобальный обработчик ошибок, который именно этим и будет заниматься.

/**
 * Глобальный обработчик ошибок Vue
 */
Vue.config.errorHandler = (err: Error & AxiosError, vm, info) => {
   Notification.error(getErrorMessage(err))
}

/**
 * Глобальный обработчик ошибок для промисов
 */
window.addEventListener("unhandledrejection", (event) => {
   Notification.error(getErrorMessage(event.reason));
});

/**
 * Извлекает сообщение об ошибке
 * @param error ошибка
 */
function getErrorMessage(error: Error & AxiosError) {
   return error.response?.data?.message ? error.response?.data?.message : error.message;
}

Больше не надо будет для этого добавлять блоки try ... catch(e) …, ведь теперь глобальный обработчик сам отловит любую ошибку и отобразит её пользователю. Приятным бонусом является то, что теперь можно отобразить пользователю текст с ошибкой просто кинув эту самую ошибку throw new Error("Не заполнен номер документа");

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

Источник: https://habr.com/ru/post/675280/


Интересные статьи

Интересные статьи

Нередко при работе с Bitrix24 REST API возникает необходимость быстро получить содержимое определенных полей всех элементов какого-то списка (например, лидов). Традиционн...
Всем привет. Если вы когда-либо работали с универсальными списками в Битрикс24, то, наверное, в курсе, что страница детального просмотра элемента полностью идентична странице редак...
Часть 1: Nuxt as fullstack server: frontend + backend API Server Часть 2: Additional SSR performance with Nuxt fullstack server Разработчики Nuxt предлагают 3 метода доступа к API: ...
Компьютерные игры существуют почти столько же, сколько и сами компьютеры. Хотя в это трудно поверить, текстовая адвенчура Zork была Fortnite-ом своего времени. Но Zork был ещё и чем-то большим....
Автокэширование в 1с-Битрикс — хорошо развитая и довольно сложная система, позволяющая в разы уменьшить число обращений к базе данных и ускорить выполнение страниц.