Возвращаем базы к жизни

За последний год истории веб-разработки ясно одно, веб-приложения с каждым днем ​​становятся все сложнее. Необходимость создавать сложные и масштабируемые приложения более простым способом позволила появиться фантастическим инструментам, таким как React, Angular, Vue и т. д., но в настоящее время, даже с этими инструментами в игре, разработка крупномасштабных веб-приложений является сложной задачей, поскольку нам необходимо одновременно решать разные проблемы, такие как управление состоянием, выборка данных, стилизация, производительность и другие сложные задачи. Вот тут-то и появляется Remix Run.

Remix Run — это платформа, созданная для разработки полнофункциональных веб-приложений, которая предоставляет набор инструментов и абстракций для платформы веб-браузера, направленной на создание высокопроизводительных, доступных и масштабируемых сайтов.

Настоящая проблема современных веб-технологий

Ранее мы упоминали, что такие инструменты, как React, позволили инженерам создавать множество различных типов веб-приложений, и это здорово, но в тени скрывается проблема. По мере того, как Интернет стал массовым, на эти веб-сайты заходили разные пользователи, и возникли некоторые опасения.

Эти опасения можно резюмировать следующим образом:

  • Мы можем контролировать наши серверы, делать их быстрее и использовать на них лучшие технологии, но мы не можем контролировать сеть пользователя и узкие места, которые они создают.
  • Современные веб-фреймворки работают против основ сети, а не работают с ними, и поэтому нам нужно много кода JavaScript на стороне клиента, чтобы запускать наши приложения, что делает первый пункт еще хуже.
  • Есть много пользователей, чье соединение необходимо улучшить, чтобы получить доступ ко всему веб-сайту за приличное время. Если наш первоначальный пакет огромен, это начальное время будет действительно огромным, и пользовательский опыт будет уменьшен.

Чтобы решить эти проблемы, Remix вернул к жизни основы, используя модель сервер/клиент (включая отделение исходного кода от контента/данных), работая с основами сети (браузеры, HTTP и HTML) и использование JavaScript для улучшения взаимодействия с пользователем (поэтому наши сайты будут работать и без JavaScript).

Что такое Ремикс под капотом?

Цитируя документацию Remix, Remix — это сочетание четырех вещей:

  1. Компилятор.
  2. Обработчик HTTP на стороне сервера.
  3. Серверная структура.
  4. Фреймворк браузера.

Под капотом Remix использует ESBUILD в качестве компилятора для создания обработчика HTTP сервера, сборки браузера с его активами и манифеста активов. Таким образом, мы можем развернуть наше приложение на любом хостинге JavaScript. Это точка входа для каждого приложения Remix.

Мы можем сказать, что Remix работает аналогично классическим веб-фреймворкам MVC, таким как Ruby on Rails, но он реализует только представление и контроллеры, оставляя модель на ваше усмотрение, поэтому вы можете свободно использовать любые базы данных, ORM и т. д.

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

Если вы не знакомы с фреймворками MVC, вы можете использовать другую аналогию для понимания Remix:

Модули Remix Route — это компоненты React с собственным маршрутом API, которые умеют обращаться к серверу для загрузки и отправки данных.

Чтобы заставить Route Module работать, Remix реализовал в нем функции экспорта: наиболее важными из них являются loader, action и default (компонент пользовательского интерфейса).

Мы можем увидеть пример здесь.

export const loader = async ({ params }: LoaderArgs) => {
  const post = await getPost(params.slug);

  return json({ post });
};

export async function action({ request }: ActionArgs) {
  const form = await request.formData();
  const errors = validate(form);

  if (errors) {
    return json({ errors });
  }

  await createPost({ title: form.get("title"), content: form.get("content") });
  return json({ ok: true });
}

const PostSlug = () => {
  const { post } = useLoaderData<typeof loader>();
 const actionData = useActionData<typeof action>();

  return (
    <main className="mx-auto max-w-4xl">
      <h1 className="my-6 border-b-2 text-center text-3xl">
        Post title: {post?.title}
      </h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />

   <Form method="post">
        <input name="title" />
        <textarea name="content" />
        <button type="submit">Create New Project</button>
      </Form>
   {actionData?.errors ? (
        <ErrorMessages errors={actionData.errors} />
      ) : null}
    </main>
  );
};

export default PostSlug;

loaders работают только на сервере и используются для предоставления данных вашим компонентам пользовательского интерфейса в запросах GET. actions запускаются на сервере, но используются для обработки POST, PUT, PATCH и DELETE.

Кроме того, большая часть приложения работает до загрузки JavaScript в браузере, что делает приложения Remix устойчивыми к сетевым условиям.

Remix передает документ в браузер, а затем наполняет страницу модулями JavaScript сборки браузера. Таким образом, Remix эмулирует браузер.

Remix имеет некоторые встроенные оптимизации для навигации на стороне клиента, а также предоставляет некоторые компоненты и хуки React как часть API на стороне клиента, которые мы можем использовать для создания богатого пользовательского опыта без изменения фундаментальной модели HTML и браузеров.

Вот почему мы говорим, что Remix создан на основе React JS.

Самое приятное, что они доступны прямо из коробки

Хватит теории, давайте запачкаем руки кодом.

Как использовать Remix в реальном проекте

Давайте начнем с создания Remix Project. Для этой цели Remix предоставляет нам инструмент командной строки для разработчиков и различные технологические стеки (официальные стеки поставляются с общими вещами, необходимыми для рабочего приложения).

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

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

Для создания нового проекта Remix мы используем remix-create. Откройте свой терминал и выполните эту команду:

$ npx create-remix@latest

Чтобы использовать один из технологических стеков, используйте флаг --template или используйте интерактивное меню CLI.

После запуска сценария установки вам будет задано несколько вопросов для создания проекта.

? Where would you like to create your app? remix-demo-project
? What type of app do you want to create? Just the basics
? Where do you want to deploy? Choose Remix App Server if you're unsure; it's easy to
 change deployment targets. Remix App Server
? TypeScript or JavaScript? TypeScript
? Do you want me to run `npm install`? Yes

Давайте откроем созданный каталог и изучим, что у нас есть. В моем случае я назвал remix-demo-project. Вы должны увидеть древовидную структуру, подобную этой:

remix-demo-project
├── README.md
├── app
│   ├── entry.client.tsx
│   ├── entry.server.tsx
│   ├── root.tsx
│   └── routes
│       └── index.tsx
├── package-lock.json
├── package.json
├── public
│   └── favicon.ico
├── remix.config.js
├── remix.env.d.ts
└── tsconfig.json

Каталог приложений — это место, где находится ваше приложение Remix. Внутри вы можете найти файл root.tsx, который является нашим корневым компонентом, в котором мы должны отображать элемент `‹html›‹/html›` наряду с другими компонентами, которые Remix предоставляет нам из коробки.

//root.tsx

const App() = () => {
  return (
    <html lang="en">
      <head>
        <Meta />
        <Links />
      </head>
      <body>
        <Outlet />
        <ScrollRestoration />
        <Scripts />
        <LiveReload />
      </body>
    </html>
  );
}

export default App;

Здесь мы можем найти еще два файла. Файл, отвечающий за гидратацию наших компонентов React после загрузки приложения в браузере app/entry.client.tsx, и файл, отвечающий за рендеринг нашего приложения React на сервере и отправку его в качестве ответа app/entry.server.tsx.

И последнее, но не менее важное: у нас также есть package.json со всеми доступными сценариями для запуска, сборки и развертывания нашего приложения.

Давайте запустим наше приложение в режиме разработки, чтобы мы могли начать играть с ним.

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

$ npm run dev
Remix App Server started at http://localhost:3000 (http://192.168.1.173:3000)

Прежде всего, давайте изучим систему маршрутизации, так как это самый фундаментальный блок в каждом приложении Remix.

Маршрутизация в ремиксе

Маршрутизация — самая важная концепция в Remix, поскольку все в Remix начинается с ваших маршрутов.

Полная документация доступна здесь.

Самый распространенный способ создания маршрута в Remix — через файловую систему. Это называется маршрутизацией на основе файлов. Чтобы создать маршрут, нам нужно создать файл в каталоге app/routes.

Давайте начнем с создания нашего корневого маршрута. Для этого создайте файл с именем index.tsx внутри app/routes и по умолчанию экспортируйте компонент React.

// app/routes/index.tsx

const Root = () => {
 return (
  <div>
   <h1>This is the route we should see at /</h1>
  </div>
 );
};

export default Root;

Затем в вашем браузере вы должны увидеть это.

Если вы хотите создать другие страницы, такие как /products или /cart, вы можете следовать тому же соглашению и создать файл внутри app/routes с именем маршрута.

После создания этого файла вы сможете перейти к /products в своем браузере и просмотреть страницу.

Теперь давайте создадим страницу для отображения конкретных продуктов по адресу /products/$productId. Для этого Remix позволяет нам использовать параметризованные маршруты в соответствии с тем же подходом, но на этот раз мы собираемся использовать другую концепцию — вложенную маршрутизацию.

Вложенная маршрутизация — это общая идея соединения сегментов URL-адреса с иерархией компонентов в пользовательском интерфейсе.

Во-первых, давайте изменим нашу страницу для доступных продуктов.

import { Link, Outlet } from '@remix-run/react';

const Products = () => {
 return (
  <div>
   <h1>List of Products</h1>
   <hr />
   <ul>
    <Link to='product1'>
     <li>Product 1</li>
    </Link>
    <Link to='product2'>
     <li>Product 2</li>
    </Link>
    <Link to='product3'>
     <li>Product 3</li>
    </Link>
   </ul>
   <main>
    <Outlet />
   </main>
  </div>
 );
};

export default Products;

Обратите внимание, что мы включили компонент <Outlet />, доступный в пакете @remix-run/react, он будет нашим заполнителем для дочерних маршрутов.

Теперь создайте два новых файла внутри app/routes/products/ следующим образом:

// app/routes/products/index.tsx 

const NoProducts = () => {
 return <div>No Products</div>;
};

export default NoProducts;

// app/routes/products/$productId.tsx

const Product = () => {
 return (
  <div>
   <h3>Product Details</h3>
  </div>
 );
};

export default Product;

Обратите внимание на $ перед именем файла и его местоположением. Это должно сообщить Remix, что этот файл вложен внутри /products маршрута и параметризован productId.

Теперь, если вы пойдете к /products, вы должны увидеть что-то вроде этого.

Мы видим, что компонент NoProducts отображается вместо нашего <Outlet />.

Но если вы нажмете на любую ссылку, вы увидите изменения URL и вместо компонента <Outlet /> мы увидим наш ProductDetails. Вот как работают вложенные и параметризованные маршруты. Теперь давайте отобразим некоторую информацию в соответствии с выбранным продуктом, изменив компонент app/routes/products/$productId.tsx.

Чтобы создать более реалистичный пример, я собираюсь использовать некоторые фиктивные данные (файл JSON) для сведений о продукте и представить новую концепцию — Извлечение данных.

Извлечение данных в Remix

Если вы помните, во введении мы упомянули, что в Remix ваш внешний компонент также является собственным маршрутом API, и он знает, как разговаривать с самим собой на сервере из браузера.

Чтобы получить данные, вам нужно экспортировать функцию loader из вашего модуля маршрута. Внутри этого loader вы пишете свою логику для получения данных (помните, что это выполняется на сервере) и возвращаете данные для отображения.

Давайте начнем с создания loader на странице наших продуктов для получения наших продуктов и использования useLoaderData для использования данных в нашем компоненте пользовательского интерфейса.

// app/routes/products.tsx

import { json } from '@remix-run/node';
import { Link, Outlet, useLoaderData } from '@remix-run/react';
import { fetchProducts } from '~/utils/fetchProducts';

export const loader = async () => {
 const products = await fetchProducts();

 return json({ products });
};

const Products = () => {
 const data = useLoaderData<typeof loader>();

 return (
  <div>
   <h1>List of Products</h1>
   <hr />
   <ul>
    {data.products.map((product) => (
     <Link to={product.link} key={product.link}>
      <li>{product.title}</li>
     </Link>
    ))}
   </ul>
   <main>
    <Outlet />
   </main>
  </div>
 );
};

export default Products;

А теперь давайте изменим наш $productId.tsx, чтобы отобразить сведения о продукте, используя тот же подход.

// app/routes/products/$productId.tsx

import type { LoaderArgs } from '@remix-run/node';
import { json } from '@remix-run/node';
import { fetchProductDetails } from '~/utils/fetchProducts';
import { useLoaderData } from '@remix-run/react';

export const loader = async ({ params }: LoaderArgs) => {
 const product = await fetchProductDetails(params.productId!);
 return json({ title: product.title, description: product.description });
};

const Product = () => {
 const data = useLoaderData<typeof loader>();

 return (
  <div>
   <h3>{data.title}</h3>
   <p>{data.description}</p>
  </div>
 );
};

export default Product;

Обратите внимание, что нам не нужно возвращать весь объект. Вместо этого мы можем вернуть только то, что нам нужно, а это означает, что клиентам будет доставлено меньше кода, как мы говорили в начале.

Как всегда, я рекомендую вам проверить документацию для каждого раздела, здесь у вас есть полное объяснение того, как получить данные.

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

Мутации в ремиксе

Давайте создадим форму для добавления новых товаров на нашу страницу. Для этого создайте страницу компонента в app/routes/products/new.tsx, чтобы включить форму для ввода новых продуктов. Способ записи данных на серверы в Remix — это действия, и, как и в случае с загрузчиками, нам нужно экспортировать функцию из модуля маршрута.

import type { ActionArgs } from '@remix-run/node';
import { json } from '@remix-run/node';
import { useActionData } from '@remix-run/react';
import { createProduct } from '~/utils/productUtils';

export const action = async ({ request }: ActionArgs) => {
 const body = await request.formData();
 const title = body.get('title') as string;
 const description = body.get('description') as string;

 const success = await createProduct(title, description);
 return json({ ok: success });
};

const NewProduct = () => {
 const data = useActionData<typeof action>();
 console.log({ data });

 return (
  <form method='post'>
   <input type='text' name='title' defaultValue='' />
   <input type='text' name='description' defaultValue='' />
   <button type='submit'>Add Product</button>
  </form>
 );
};

export default NewProduct;

После создания нового продукта мы должны увидеть его в нашем списке.

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

Теперь наше маленькое приложение выглядит уродливо, так что я думаю, самое время представить стиль в Remix.

Стайлинг в ремиксе

Чтобы добавить стили в Remix, нам нужно добавить <link rel="stylesheet"> на страницу, которую мы оформляем, и, как мы сделали с actions и loaders, мы можем включить эти ссылки в модуль маршрута, экспортировав функцию link.

Во-первых, нам нужны некоторые основные стили. Я собираюсь создать их в отдельной папке app/styles.

// app/styles/index.css

* {
 box-sizing: border-box;
 margin: 0;
 padding: 0;
}

.header {
 background-color: #ccc;
 padding: 8px;
}

А затем используйте их в нашей функции ссылки внутри нашего индексного компонента. После этого вы сможете увидеть наши основные стили, примененные на нашей индексной странице.

import type { LinksFunction } from '@remix-run/node';
import styles from '~/styles/index.css';

export const links: LinksFunction = () => {
 return [{ rel: 'stylesheet', href: styles }];
};

const Root = () => {
 return (
  <header className='header'>
   <h1>This is the route we should see at /</h1>
  </header>
 );
};

export default Root;

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

Если вы хотите узнать больше о стайлинге, вы можете прочитать это здесь. Но мы рассмотрели основы.

Заключение

На данный момент мы рассмотрели основы Remix Run, что это такое, что он делает и почему он был создан в первую очередь. Я действительно рекомендую вам пойти и проверить документацию и попробовать некоторые проекты самостоятельно.

Напомним, что истинная сила Remix заключается в концепции модуля маршрута, который представляет собой файл, в котором мы можем реализовать все функции, необходимые для создания наших веб-приложений, такие как выборка данных, мутации, стилизация, и т. д., путем экспорта функций.

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