Почему я ненавижу JS или как я в 15 лет переписывал свой интернет-магазин

от автора

Эта статья — можно сказать продолжение моего развития в программировании. Вот начало моего развития. Советую его прочитать.

Я написал интернет-магазин для магазинов мяса на Blazor Server(это не WebAssembly). Там фактически нет фронта, есть только бекенд. Всё события и изменения в DOM-дереве передаются на сервер через WebSocket и сервер генерирует новый html и шлет обратно. То есть onclick можно обработать прямо из C# и там же скачать, к примеру данные из базы. Это очень удобно, никаких проблем с HTTP запросами! Blazor абстрагирует меня от этого. Кстати, в React 17 появилась така штука как React Server Components, это чисто копия подхода Blazor. Теперь можно написать компонент и с него же обращаться, в базу, например.

Но, поначалу все было очень классно и удобно, что позволило мне быстрее написать интернет-магазин, но потом возникли проблемы.

Например: нужно сделать колесо фортуны, где можно выиграть продукт. Сами подарки можно изменять в программе. Я нашёл готовое колесо, которое рисуется в canvas. Когда оно докручивается, вызывается функция в JS, из неё мне как-то надо передать в C#, что колесо докрутилось и какой продукт выпал человеку. Это делается дико сложно.

Была проблема даже с инициализацией колеса, потому что код вызывался до того как данные скачались с базы и колесо было пустым. Пришлось брать содержимое файла JS локально с сервера и потом, когда данные загрузились, вызывать его из C# c помощью JsInterprop. Короче полный костыль, кстати когда я это делал, я обнаружил серьёзную уязвимость, с помощью которой смог попасть в панель хостинга.

Из-за таких сильных ограничений я решил переписать свой интернет-магазин на React + Node. Вот, я думал, будет классно, это же тренд, удобнее моего C#. Круто! Но, как оказалось, очень многого в JS-инфраструктуре нет или хуже, чем в. NET.

Я уже немного знал про Node. Умел не много. Максимум — это получить post запрос с формы и отобразить ответ, ну и pug минимально. То есть до нормального «hello world» не дотягивает. Много читал про React и его безграничные возможности.

Ну что же, начинаем! Запустил Visual Studio Code, создал проект. Так..СТОП. Где подсказки? Почему не работает Intellisense? Ах.. Это же JS. Спас Microsoft с их Typescript-ом, был доволен работающим intellisense.

Бекенд

React начально освоил легко. Стал делать сервер. Стандартно express. Хм.. Как же работать с базой(MS SQL Server). Не напрямую же? Так, ищу ORM. Что там у нас есть? Какие-то легенькие bookshelf, waterline, в которых не даже миграций. Что осталось? Sequelize с ужастным синтаксисом и без active record? О! TypeORM. Эта ORM была создана наподобие NHibernate и Entity Framework. Это же то, что мне нужно!

Сразу стало неудобно. Вот как я делаю запрос в Entity Framework Core:

User FoundUser = new Context().Users.Where(user => user.Name == "Вася")   .FirstOrDefault();

Вот так это делается в TypeORM:

const found_user = await connection     .getRepository(User)     .createQueryBuilder("user")     .where("user.name = :name", { id: "Вася" })     .getOne();

Как же это неудобно. В случае TypeORM, IDE не подскажет мне, что я неправильно написал название свойства, я узнаю это уже во время выполнения потому, что в случае JS — это просто строка для компилятора, а в случае C# — это лямбда выражение, которое принимает параметр user с типом User, и оно мне подскажет название свойства, а если я ошибусь, то я даже скомпилировать не успею, как IDE мне подскажет.

Особенно с увеличением сложности запроса это помогает еще больше.

Это все из-за того, что в C# есть гениальная штука: Деревья выражений. С помощью нее можно разобрать обычное лямбда-выражение и преобразовать его в SQL запрос! В JS такого, к сожалению, нет. Но это можно пережить, хотя с болью и страданиями.

Теперь настроим миграции. Я создал для примера класс User.

import {Entity, PrimaryGeneratedColumn, Column} from "typeorm";  @Entity() export class User {      @PrimaryGeneratedColumn()     id: number;      @Column()     firstName: string;      @Column()     lastName: string;      @Column()     age: number; }

Создадим миграцию. Не работает. Так, наверное не так пути к папкам сущностей и миграций прописаны? Попробовал все возможные варианты. Все та же ошибка.

Создам-ка я проект из template-а typeorm. Создаем миграцию. Теперь выдает другую ошибку. Еще спустя несколько дней я смог создать миграцию. Фуух, неделю пытался заставить работать миграции, теперь все позади. А вот и нет. Решил я добавить поле phoneNumber. Делаю миграцию иии… Что я вижу? Изменения не найдены??!! Как? Причем первая миграция создалась без проблем. То есть пути все правильно уже прописаны, это template проект TypeORM. Спустя еще один миллион попыток и созданных проектов, наконец-то! Нашло изменения. Теперь нужно запустить эту миграцию, чтобы она применила изменения к базе. АААА!!! Пишет, что миграции новых нет, хотя только что их сгенерировало! Миграции в TypeORM просто не работают!!! Я перепробовал все, что можно, такая проблема уже есть в issues, я думал, ну ладно, может эта тупая ORM не работает с MS SQL Server, попробовал PostgreSQL — та же фигня. Я уже подумал просто забросить эту ноду и написать сервер на C#. Но тут нашелся выход.

MikroORM — это чудо! Она работает, в отличии от других ORM! Но она не работает с MS SQL Server. Буду теперь на PostgreSQL.

UPD: Кое-что забыл дописать. Оказывается, в Typescript можно менять AST дерево кода и заменить лямбда-выражение на SQL запрос, прямо как в C#. Есть проект для возможности написания Linq-подобных запросов в TypeORM. Называется typeorm-linq-repository

Ещё вспомнил очень большую проблему в ORM в JS(она есть у всех).

Посмотрите, как устроены миграции.

Есть некий класс с методами Up и Down.

Вот как миграции устроены в TypeORM:

import {MigrationInterface, QueryRunner} from "typeorm";  export class PostRefactoringTIMESTAMP implements MigrationInterface {      async up(queryRunner: QueryRunner): Promise<void> {         await queryRunner.query(`ALTER TABLE "post" RENAME COLUMN "title" TO "name"`);     }      async down(queryRunner: QueryRunner): Promise<void> {         await queryRunner.query(`ALTER TABLE "post" RENAME COLUMN "name" TO "title"`); // reverts things made in "up" method     } }

Вот как выглядит миграция в случае Entity Framework Core:

using Microsoft.EntityFrameworkCore.Migrations;   namespace HelloApp.Migrations {     public partial class InitialCreate : Migration     {         protected override void Up(MigrationBuilder migrationBuilder)         {             migrationBuilder.CreateTable(                 name: "Users",                 columns: table => new                 {                     Id = table.Column<int>(type: "int", nullable: false)                         .Annotation("SqlServer:Identity", "1, 1"),                     Name = table.Column<string>(type: "nvarchar(max)", nullable: true),                     Age = table.Column<int>(type: "int", nullable: false)                 },                 constraints: table =>                 {                     table.PrimaryKey("PK_Users", x => x.Id);                 });         }           protected override void Down(MigrationBuilder migrationBuilder)         {             migrationBuilder.DropTable(                 name: "Users");         }     } }

Разница между ними в том, что в случае TypeORM, миграция — это просто строка с SQL запросом, а в случае Entity Framework Core — это вызовы разных методов. И в чем же разница? А в том, что Entity Framework Core не зависит от базы данных, текущая структура у него есть, а в случае TypeORM, он полностью зависит от базы. Он не может узнать структуру из SQL строк. Он сравнивает текущее состояние в базе данных с тем, что есть в сущьностях, а Entity Framework Core может воспроизвести структуру базы по миграциям и сравнить её с новыми изменениями. То есть в TypeORM я просто не могу создавать миграции без базы! Это неудобно. В Entity Framework Core я могу создавать любые миграции вне зависимости от базы и потом только их применить. В JS я не нашёл ни одной ORM с миграциями, которые бы были независимы от базы данных.

Фронтенд

Теперь перейдем к React. Скажу так, нет ни одного шаблонизатора(я туда отношу и JSX), который хоть немного сравнится по удобству с Razor(шаблонизатором для Blazor).

Давайте теперь сделаем простой todo.

Для Blazor код компонента такой:

<ul class="todo"> @* Цикл по списку элементов *@ @foreach(TodoItem TodoItem in TodoItemsList) { <li class="todo__item"> @* Сам контент *@ @TodoItem.Content @* Эта кнопка вызывает функцию DeleteTodoItem и передает ей TodoItem агрумент *@ <button @onclick="(() => DeleteTodoItem(TodoItem))">Удалить</button> </li> } </ul> <div> @* Двусторонний биндинг к свойству класса, свойство меняется, если пользователь вводит текст, и текст в input меняется, если поменять свойство из метода *@ <input @bind="ValueToAdd" /> <button @onclick="AddTodoItem">Добавить</button> </div>  @code { public List<TodoItem> TodoItemsList = new(); // список элементов public class TodoItem { public string Content; } public void DeleteTodoItem(TodoItem TodoItem) { TodoItemsList.Remove(TodoItem); } public string ValueToAdd; public void AddTodoItem() { TodoItemsList.Add(new TodoItem { Content = ValueToAdd }); ValueToAdd = ""; } }

Я могу писать настоящий C# в Razor! Циклы — пожалуйста, условное ветвление — пожалуйста, и даже двусторонний биндинг.

Возьмем пример с React.

import React, {useState} from "react";  function Todo({ todo, index, removeTodo }) {   return (     <div className="todo"/>       {todo.text}       <div>         <button onClick={() => removeTodo(index)}>Удалить</button>       </div>     </div>   ); }  function TodoForm({ addTodo }) {   const [value, setValue] = useState("");    const addItem = () => {     addTodo(value);     setValue("");   };    return (   <input         type="text"         className="input"         value={value}         onChange={e => setValue(e.target.value)}       />   <button onClick={addItem} />   ); }  function App() {   const [todos, setTodos] = useState([]);    const addTodo = text => {     const newTodos = [...todos, { text }];     setTodos(newTodos);   };    const removeTodo = index => {     const newTodos = [...todos];     newTodos.splice(index, 1);     setTodos(newTodos);   };    return (     <div className="app">       <div className="todo-list">         {todos.map((todo, index) => (           <Todo             key={index}             index={index}             todo={todo}             removeTodo={removeTodo}           />         ))}         <TodoForm addTodo={addTodo} />       </div>     </div>   ); }  export default App;

Сразу заметно, что код React в 1.5 раза больше. Также, он значительно сложнее для понимания. А в Blazor можно еще вынести блок @code в отдельный файл и отделить логику от представления(конечно не полностью, но это лучше, чем все вместе)

Код React очень усложняет, то что в JSX нельзя сразу внедрить код JS, нужно обернуть это в фигурные скобки и там внутри вернуть JSX, это неудобно. В Razor это сделано гораздо лучше.

В JSX нет двустороннего биндинга, нужно писать className, а не просто class, что очень раздражает. Еще есть Angular и Vue, но там шаблонизаторы тоже не такие мощные, краткие и понятные как Razor.

Что меня бесило больше всего, так это работа с данными.

В Blazor можно легко сделать DI контейнер (в Angular тоже), в React же все построено вокруг паттернов CQRS, Event Sourcing, на бекенде эти паттерны используют только в сложных проектах, поскольку для простого проекта это принесет только вред, в React же этот подход пиарят сразу.

Есть еще Modx, но если я не ошибаюсь, это тот же Redux, только с сахаром в виде ООП да и сейчас Redux — де-факто стандарт. У меня возникает, как React, Redux, и прочие библиотеки стали так сильно популярны и почему мне рекламируют их, если они неудобные, непрактичные, или ну ооочень многословные?

Окончание

Сейчас, спустя 2 месяца, 1 месяц из которых я пытался найти нормальную ORM, мой новый интернет-магазин умеет только создать временного пользователя в базе(для хранения корзины в базе, а не в LocalStorage), с cookies и создание session-key я делал сам, и все! А интернет-магазин на Blazor я написал за те же два месяца! И там была моя самодельная CMS(хотя ей в итоге никто не пользовался), автоматическое распределение заказов по магазинам в зависимости от улицы, и также я успел написать дектопную программа для оператора на WPF, где можно смотреть пользователей, экспортировать в Excel, перенаправлять заказы на другую точку…

Blazor показался мне намного легче в изучении, чем React.

JS-экосистема оставила очень негативные ощущения, даже не уверен, стоит ли продолжать писать новый интернет-магазин или может поправить код, сделать нормальную архитектуру старого.

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


ссылка на оригинал статьи https://habr.com/ru/articles/576954/


Комментарии

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

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