
Эта статья - можно сказать продолжение моего развития в программировании. Вот начало моего развития. Советую его прочитать.
Я написал интернет-магазин для магазинов мяса на 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-экосистема оставила очень негативные ощущения, даже не уверен, стоит ли продолжать писать новый интернет-магазин или может поправить код, сделать нормальную архитектуру старого.
Спасибо за прочтение статьи! Хочу сказать, что это лично мое мнение, оно может поменяться, и у вас может быть совсем другой опыт. Если вы только начинаете изучать программирование, не стоит воспринимать мой опыт как истину, подвергайте все сомнению.