Я написал интернет-магазин для магазинов мяса на 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.
Сразу стало неудобно. Вот как я делаю запрос в 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 с миграциями, которые бы были независимы от базы данных.
Давайте теперь сделаем простой 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, и прочие библиотеки стали так сильно популярны и почему мне рекламируют их, если они неудобные, непрактичные, или ну ооочень многословные?
Blazor показался мне намного легче в изучении, чем React.
JS-экосистема оставила очень негативные ощущения, даже не уверен, стоит ли продолжать писать новый интернет-магазин или может поправить код, сделать нормальную архитектуру старого.
Спасибо за прочтение статьи! Хочу сказать, что это лично мое мнение, оно может поменяться, и у вас может быть совсем другой опыт. Если вы только начинаете изучать программирование, не стоит воспринимать мой опыт как истину, подвергайте все сомнению.
Но, поначалу все было очень классно и удобно, что позволило мне быстрее написать интернет-магазин, но потом возникли проблемы.
Например: нужно сделать колесо фортуны, где можно выиграть продукт. Сами подарки можно изменять в программе. Я нашёл готовое колесо, которое рисуется в 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-экосистема оставила очень негативные ощущения, даже не уверен, стоит ли продолжать писать новый интернет-магазин или может поправить код, сделать нормальную архитектуру старого.
Спасибо за прочтение статьи! Хочу сказать, что это лично мое мнение, оно может поменяться, и у вас может быть совсем другой опыт. Если вы только начинаете изучать программирование, не стоит воспринимать мой опыт как истину, подвергайте все сомнению.
Почему я ненавижу JS или как я в 15 лет переписывал свой интернет-магазин
Эта статья - можно сказать продолжение моего развития в программировании. Вот начало моего развития . Советую его прочитать. Я написал интернет-магазин для магазинов мяса на Blazor Server(это не...
habr.com