Как 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/articles/675280/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *