Как разместить приложение Remix?
В этой статье вы узнаете все, что нужно знать для начала работы с Remix. Мы рассмотрим его плюсы и минусы, покажем, как создать приложение Remix, и, наконец, расскажем, как развернуть Remix.
Contents
Что такое Remix?
Remix – это современный веб-фреймворк с полным стеком для создания быстрых, удобных и устойчивых приложений. Он использует возможности рендеринга на стороне сервера (SSR), что позволяет ускорить загрузку и улучшить SEO для динамических веб-приложений.
С помощью Remix разработчики могут легко интегрировать логику на стороне клиента и на стороне сервера в единую кодовую базу. По сравнению с другими популярными фреймворками React, Remix многое делает по-другому. В том числе:
- Маршрутизация на основе файлов (файл в каталоге routes представляет определенный маршрут)
- Традиционная работа с формами (использование HTML и HTTP-запросов, аналогично PHP)
- Управление состоянием (состояние хранится только на сервере)
- Загрузка данных (отделена от компонентов)
Remix был создан в 2020 году и изначально был доступен по годовой лицензии. Позже, в октябре 2021 года, команда Remix решила открыть исходный код проекта. Теперь он доступен под лицензией MIT.
В конце 2022 года Remix была приобретена компанией Shopify за 2,1 миллиарда долларов.
Преимущества Remix
Давайте рассмотрим основные преимущества использования фреймворка Remix.
Навигация на основе файлов
Remix построен на базе React Router, мощного решения для маршрутизации на стороне клиента. Фактически, Remix был создан той же командой разработчиков, которая создала React Router.
Фреймворк использует файловую систему навигации, которая упрощает организацию кода. Она позволяет разработчикам связывать маршруты, компоненты и ресурсы с определенными файлами или каталогами.
Вот пример:
app/
└── routes/
├── $noteId.tsx // matches: /<noteId>/
├── $noteId_.destroy.tsx // matches: /<noteId>/destroy
├── $noteId_.edit.tsx // matches: /<noteId>/edit
├── _index.tsx // matches: /
└── create.tsx // matches: /create
Лучше всего то, что он поддерживает вложенную маршрутизацию через . Это сокращает время загрузки, улучшает обработку ошибок и многое другое!
Рендеринг на стороне сервера (SSR)
Remix использует возможности рендеринга на стороне сервера.
В ванильных приложениях React данные обычно извлекаются на стороне клиента, а затем внедряются в DOM. Это известно как рендеринг на стороне клиента (CSR).
Remix, с другой стороны, использует другой подход. Сначала он получает данные на внутреннем сервере, создает HTML с полученными данными, а затем передает их клиенту.
Рендеринг на стороне сервера, как правило, обеспечивает более высокую производительность и более удобные для SEO приложения.
Обработка форм
Remix возвращает обработку форм к основам.
Вместо того чтобы использовать кучу управляемых компонентов и JavaScript, он использует традиционные HTML-формы и HTTP-запросы.
Когда форма отправлена, она посылает HTTP-запрос на определенный маршрут, который затем обрабатывается на стороне сервера с помощью функции action()
. Это работает аналогично старому доброму PHP.
Это означает, что Remix не требует JavaScript для обработки форм. Хотя это и здорово, но может сделать валидацию форм и отображение ошибок немного сложнее.
Управление государством
В данном контексте состояние означает синхронизацию данных сервера и клиента.
Remix делает управление состояниями простым делом. Он устраняет необходимость в Redux, Zustand, React Query, Apollo или любой другой библиотеке управления состоянием на стороне клиента.
При использовании Remix все состояние обрабатывается сервером. Клиент практически не хранит состояние, поэтому процесс синхронизации не требуется.
Данные от сервера к клиенту передаются через различные функции, такие как loader()
и action()
.
Переходы и оптимистичный пользовательский интерфейс
Переходы Remix делают переход между страницами плавным и быстрым благодаря предварительной загрузке данных и использованию анимации.
Оптимистичный пользовательский интерфейс мгновенно отражает действия пользователя, делая сайты более отзывчивыми, поскольку показывает изменения еще до их подтверждения.
Переходы и оптимистичный пользовательский интерфейс значительно улучшают впечатления пользователя, уменьшая воспринимаемую задержку и обеспечивая мгновенную обратную связь.
Ограничения Remix
Несмотря на то, что Remix очень хорош, он имеет некоторые ограничения.
Комплекс
Remix – не самый простой в использовании фреймворк, и документация по нему (на момент написания статьи) не самая лучшая.
Фреймворк также делает много вещей, отличных от ванильного React. Если вы являетесь разработчиком React, вам может потребоваться некоторое время, чтобы разобраться в различных концепциях Remix.
Менее популярный
Remix – это все еще относительно новый фреймворк. Его публичный релиз датируется только октябрем 2021 года. Он менее зрелый и проверенный в боях, чем некоторые конкуренты, например Next.js.
Взглянув на звезды GitHub, мы видим, что Remix (27 тыс. звезд) гораздо менее популярен, чем Next.js (120 тыс. звезд). На момент написания статьи Remix не был принят ни одним технологическим гигантом, кроме Shopify.
Тесно соединенные
Приложения Remix имеют тесно связанный фронтенд и бэкенд. Хотя такой подход подходит для небольших проектов, некоторые разработчики предпочитают гибкость раздельного фронтенда и бэкенда.
Кроме того, разделение фронтенда и бэкенда может сделать ваш код более удобным для сопровождения и тестирования.
Нет SSG или ISR
Remix не поддерживает генерацию статических сайтов (SSG) или инкрементную статическую регенерацию (ISR).
Как развернуть приложение Remix?
В этом разделе мы создадим и развернем приложение Remix.
Пререквизиты
Чтобы следовать за вами, вам понадобятся:
- Базовое понимание TypeScript
- Опыт работы с Docker (и технологией контейнеризации)
- Node.js и JavaScript IDE, установленные на вашем компьютере
- На вашей машине установлен рабочий стол Docker Desktop
Обзор проекта
На протяжении всей статьи мы будем работать над веб-приложением для заметок. Веб-приложение позволит пользователям создавать, извлекать, редактировать и удалять заметки.
Для бэкенда мы будем использовать Back4app BaaS, а для фронтенда – фреймворк Remix. Как только фронтенд будет написан, мы развернем его на Back4app Containers.
Конечный продукт будет выглядеть примерно так:
Я предлагаю вам сначала разобраться с веб-приложением Notes. После этой статьи вы сможете развернуть свои собственные приложения Remix.
Бэкэнд
В этом разделе статьи мы будем использовать Back4app для создания бэкенда нашего приложения.
Создать приложение
Начните с входа в свою учетную запись Back4app (или создайте ее, если вам еще нужно ее получить).
Когда вы войдете в систему, вы будете перенаправлены на портал “Мои приложения”. Чтобы создать бэкэнд, сначала нужно создать приложение Back4app. Для этого нажмите “Создать новое приложение”.
Back4app предлагает два решения:
- Backend as a Service (BaaS) – полноценное решение для бэкенда.
- Containers as a Service (CaaS) – платформа для оркестровки контейнеров на базе Docker.
Поскольку мы работаем с бэкендом, выберите “Бэкенд как сервис”.
Назовите свое приложение, оставьте все остальное по умолчанию и нажмите “Создать”.
Платформе потребуется время, чтобы настроить все необходимое для вашего бэкенда. Это включает в себя базу данных, интерфейс приложения, масштабирование, безопасность и т. д.
Когда ваше приложение будет готово, вы будете перенаправлены на просмотр базы данных приложения в режиме реального времени.
База данных
Двигаясь дальше, давайте разберемся с базой данных.
Поскольку мы создаем относительно простое приложение, нам понадобится только одна модель – назовем ее Note
. Чтобы создать ее, нажмите кнопку “Создать класс” в верхней левой части экрана.
Назовите его Note
, включите опцию “Public Read and Write Enabled” и нажмите кнопку “Create class & add columns”.
Затем добавьте следующие столбцы:
+-----------+--------------+----------------+----------+
| Data type | Name | Default value | Required |
+-----------+--------------+----------------+----------+
| String | emoji | <leave blank> | yes |
+-----------+--------------+----------------+----------+
| String | title | <leave blank> | yes |
+-----------+--------------+----------------+----------+
| File | content | <leave blank> | no |
+-----------+--------------+----------------+----------+
Создав класс, заполните базу данных примерами. Создайте несколько заметок, указав эмодзи, заголовки и содержание. В качестве альтернативы импортируйте экспорт этой базы данных.
Отлично, вот и все!
Мы успешно создали бэкэнд без написания какого-либо кода.
Чтобы узнать больше о Backend as a Service, прочитайте статью Что такое Backend as a Service?
Frontend
В этом разделе статьи мы построим фронтенд нашего приложения, используя фреймворк Remix.
create-remix
Самый простой способ загрузить проект Remix – это утилита create-remix
. Эта утилита создает готовый к производству проект Remix.
Он позаботится о структуре каталогов и зависимостях, установит бандлер и т. д.
Создайте новый проект Remix, выполнив следующую команду:
$ npx create-remix@latest remix-notes
Initialize a new git repository? Yes
Install dependencies with npm? Yes
Если вы никогда не использовали
create-remix
, он будет установлен автоматически.
После создания проекта измените свой активный каталог на него:
$ cd remix-notes
Наконец, запустите сервер разработки:
$ npm run dev
Откройте свой любимый веб-браузер и перейдите на сайт http://localhost:5173. На экране должна появиться стандартная целевая страница Remix.
TailwindCSS
Чтобы сделать нашу жизнь немного проще, мы будем использовать TailwindCSS. TailwindCSS – это фреймворк, который позволяет быстро создавать фронтенды без написания обычного CSS.
Сначала установите его с помощью npm:
$ npm install -D tailwindcss postcss autoprefixer
Затем запустите команду tailwindcss init
:
$ npx tailwindcss init --ts -p
Это настроит ваш проект и создаст файл tailwind.config.ts в корне проекта.
Измените свойство content
следующим образом, чтобы Tailwind знал, в каких файлах будут использоваться классы утилиты:
// tailwind.config.ts
import type {Config} from "tailwindcss";
export default {
content: ["./app/**/*.{js,jsx,ts,tsx}"], // new
theme: {
extend: {},
},
plugins: [],
} satisfies Config;
Создайте новый файл tailwind.css в каталоге app со следующим содержимым:
/* app/tailwind.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
И наконец, импортируйте его в файл root.tsx с помощью ссылок
:
// app/root.tsx
// other imports
import {LinksFunction} from "@remix-run/node";
import stylesheet from "~/tailwind.css?url";
export const links: LinksFunction = () => [
{ rel: "stylesheet", href: stylesheet },
];
// ...
Вот и все!
Мы успешно установили TailwindCSS.
Маршруты
Наше веб-приложение будет иметь следующие конечные точки:
/
отображает все заметки/create
позволяет пользователям создавать заметки/
отображает конкретную заметку/
/delete позволяет пользователям удалять конкретные заметки.
Чтобы определить эти маршруты, создайте следующую структуру каталогов в папке app:
app/
└── routes/
├── $noteId.tsx
├── $noteId_.destroy.tsx
├── $noteId_.edit.tsx
├── _index.tsx
└── create.tsx
Как вы уже догадались, префикс $
используется для динамических параметров, а .
используется вместо /
.
Просмотров
Двигаясь дальше, давайте реализуем представления.
Чтобы сделать наш код более безопасным для типов, мы создадим интерфейс с именем Note
, который будет напоминать наш класс базы данных Note
.
Создайте папку с именем store и в ней создайте файл NoteModel.ts со следующим содержимым:
// app/store/NoteModel.ts
export default interface NoteModel {
objectId: string;
emoji: string;
title: string;
content: string;
createdAt: Date;
updatedAt: Date;
}
Затем вставьте код представления для _index.tsx, $noteId.tsx и create.tsx:
// app/routes/_index.tsx
import {Link, NavLink} from "@remix-run/react";
import NoteModel from "~/store/NoteModel";
const notes = [
{objectId: "1", emoji: "📝", title: "My First Note"},
{objectId: "2", emoji: "📓", title: "My Second Note"},
{objectId: "3", emoji: "📔", title: "My Third Note"},
] as NoteModel[];
export default function Index() {
return (
<>
<Link to={`/create`}>
<div className="bg-blue-500 hover:bg-blue-600 text-lg font-semibold text-white
px-4 py-3 mb-2 border-2 border-blue-600 rounded-md"
>
+ Create
</div>
</Link>
{notes.map(note => (
<NavLink key={note.objectId} to={`/${note.objectId}`}>
<div className="hover:bg-slate-200 text-lg font-semibold
px-4 py-3 mb-2 border-2 border-slate-300 rounded-md"
>
{note.emoji} {note.title}
</div>
</NavLink>
))}
</>
);
}
// app/routes/$noteId.tsx
import {Form} from "@remix-run/react";
import NoteModel from "~/store/NoteModel";
const note = {
objectId: "1", emoji: "📝", title: "My First Note", content: "Content here.",
createdAt: new Date(), updatedAt: new Date(),
} as NoteModel;
export default function NoteDetails() {
return (
<>
<div className="mb-4">
<p className="text-6xl">{note.emoji}</p>
</div>
<div className="mb-4">
<h2 className="font-semibold text-2xl">{note.title}</h2>
<p>{note.content}</p>
</div>
<div className="space-x-2">
<Form
method="post" action="destroy"
onSubmit={(event) => event.preventDefault()}
className="inline-block"
>
<button
type="submit"
className="bg-red-500 hover:bg-red-600 font-semibold text-white
p-2 border-2 border-red-600 rounded-md"
>
Delete
</button>
</Form>
</div>
</>
);
}
// app/routes/create.tsx
import {Form} from "@remix-run/react";
export default function NoteCreate() {
return (
<>
<div className="mb-4">
<h2 className="font-semibold text-2xl">Create Note</h2>
</div>
<Form method="post" className="space-y-4">
<div>
<label htmlFor="emoji" className="block">Emoji</label>
<input
type="text" id="emoji" name="emoji"
className="w-full border-2 border-slate-300 p-2 rounded"
/>
</div>
<div>
<label htmlFor="title" className="block">Title</label>
<input
type="text" id="title" name="title"
className="w-full border-2 border-slate-300 p-2 rounded"
/>
</div>
<div>
<label htmlFor="content" className="block">Content</label>
<textarea
id="content" name="content"
className="w-full border-2 border-slate-300 p-2 rounded"
/>
</div>
<div>
<button
type="submit"
className="bg-blue-500 hover:bg-blue-600 font-semibold
text-white p-2 border-2 border-blue-600 rounded-md"
>
Create
</button>
</div>
</Form>
</>
);
}
В этом коде нет ничего сложного. Все, что мы сделали, – это использовали JSX в сочетании с TailwindCSS для создания пользовательского интерфейса.
Как вы могли заметить, все компоненты не контролируются (мы не используем функцию useState()
). Кроме того, мы используем настоящую HTML-форму.
Это связано с тем, что в отличие от React, фреймворк Remix работает с формами аналогично PHP, используя HTTP-запросы.
Parse
Существует несколько способов подключения к бэкенду на базе Back4app. Вы можете использовать:
- RESTful API
- API GraphQL
- Parse SDK
Самый простой и надежный способ – это, конечно же, Parse SDK. Parse SDK – это набор средств разработки программного обеспечения, который предоставляет ряд классов и методов для удобного запроса и манипулирования данными.
Начните с установки Parse через npm:
$ npm install -i parse @types/parse
Создайте файл .env в корне проекта следующим образом:
# .env
PARSE_APPLICATION_ID=<your_parse_app_id>
PARSE_JAVASCRIPT_KEY=<your_parse_js_key>
Обеспечьте замену
<your_parse_app_id>
и<your_parse_js_key>
реальными учетными данными. Чтобы получить учетные данные, перейдите в свое приложение и выберите “Настройки приложения > Сервер и безопасность” на боковой панели.
Затем инициализируйте Parse в верхней части файла root.tsx следующим образом:
// app/root.tsx
// other imports
import Parse from "parse/node";
const PARSE_APPLICATION_ID = process.env.PARSE_APPLICATION_ID || "";
const PARSE_HOST_URL = "https://parseapi.back4app.com/";
const PARSE_JAVASCRIPT_KEY = process.env.PARSE_JAVASCRIPT_KEY || "";
Parse.initialize(PARSE_APPLICATION_ID, PARSE_JAVASCRIPT_KEY);
Parse.serverURL = PARSE_HOST_URL;
Мы создадим отдельный файл api/backend.ts, чтобы сохранить наши представления чистыми от логики взаимодействия с бэкендом.
Создайте api/backend.ts со следующим содержимым:
// app/api/backend.ts
import Parse from "parse/node";
import NoteModel from "~/store/NoteModel";
export const serializeNote = (note: Parse.Object<Parse.Attributes>): NoteModel => {
return {
objectId: note.id,
emoji: note.get("emoji"),
title: note.get("title"),
content: note.get("content"),
createdAt: new Date(note.get("createdAt")),
updatedAt: new Date(note.get("updatedAt")),
};
}
export const getNotes = async (): Promise<NoteModel[]> => {
// ...
}
export const getNote = async (objectId: string): Promise<NoteModel | null> => {
// ...
}
// Grab the entire file from:
// https://github.com/duplxey/back4app-containers-remix/blob/master/app/api/backend.ts
Наконец, измените представления, чтобы получать данные из бэкенда и манипулировать ими:
// app/routes/index.tsx
import {json} from "@remix-run/node";
import {Link, NavLink, useLoaderData} from "@remix-run/react";
import {getNotes} from "~/api/backend";
export const loader = async () => {
const notes = await getNotes();
return json({notes});
};
export default function Index() {
const {notes} = useLoaderData<typeof loader>();
return (
// ...
);
}
// app/routes/$noteId.tsx
import {getNote} from "~/api/backend";
import {json, LoaderFunctionArgs} from "@remix-run/node";
import {Form, useLoaderData} from "@remix-run/react";
import {invariant} from "@remix-run/router/history";
export const loader = async ({params}: LoaderFunctionArgs) => {
invariant(params.noteId, "Missing noteId param");
const note = await getNote(params.noteId);
if (note == null) throw new Response("Not Found", {status: 404});
return json({note});
};
export default function NoteDetails() {
const {note} = useLoaderData<typeof loader>();
return (
// ...
);
}
// app/routes/create.tsx
import {ActionFunctionArgs, redirect} from "@remix-run/node";
import {Form} from "@remix-run/react";
import {createNote} from "~/api/backend";
export const action = async ({request}: ActionFunctionArgs) => {
const formData = await request.formData();
const {emoji, title, content} = Object.fromEntries(formData)
as Record<string, string>;
const note = await createNote(emoji, title, content);
return redirect(`/${note?.objectId}`);
};
export default function NoteCreate() {
return (
// ...
);
}
// app/routes/$noteId_.destroy.tsx
import type {ActionFunctionArgs} from "@remix-run/node";
import {redirect} from "@remix-run/node";
import {invariant} from "@remix-run/router/history";
import {deleteNote} from "~/api/backend";
export const action = async ({params}: ActionFunctionArgs) => {
invariant(params.noteId, "Missing noteId param");
await deleteNote(params.noteId);
return redirect(`/`);
};
Сводка кодов:
- Для загрузки данных мы использовали функцию Remix
loader()
. После загрузки данных мы передали их в представление в формате JSON с помощью функцииjson()
.
- Мы использовали функцию Remix
action()
для обработки отправки формы (например,POST
).
NoteId
был передан в представления в качестве параметра.
Теперь приложение должно полностью работать и синхронизироваться с бэкэндом Back4app. Убедитесь, что все работает, создав несколько заметок, отредактировав их, а затем удалив.
Приложение Dockerize
В этом разделе мы докеризуем наш фронтенд Remix.
Dockerfile
Dockerfile – это текстовый файл, в котором описаны действия, которые должен выполнить движок Docker для создания образа.
Эти шаги включают в себя установку рабочего каталога, указание базового образа, передачу файлов, выполнение команд и многое другое.
Инструкции обычно изображаются в верхнем регистре, и за ними сразу следуют соответствующие аргументы.
Чтобы узнать больше обо всех инструкциях, ознакомьтесь со справочником Dockerfile.
Создайте в корне проекта файл Dockerfile со следующим содержимым:
# Dockerfile
FROM node:20
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["npm", "run", "start"]
Этот Dockerfile основан на образе node:20
. Сначала он устанавливает рабочий каталог, копирует package.json и устанавливает зависимости.
После этого он собирает проект, открывает порт 3000
и обслуживает приложение.
.dockerignore
При работе с Docker вы обычно стремитесь создавать образы как можно меньшего размера.
Поскольку наш проект содержит некоторые файлы, которые не должны быть в образе (например, .git,
build,
настройки IDE), мы их исключим. Для этого мы создадим файл .dockerignore, который работает аналогично файлу .gitignore.
Создайте файл .dockerignore в корне проекта:
# .dockerignore
.idea/
.cache/
build/
node_modules/
Адаптируйте .dockerignore в соответствии с потребностями вашего проекта.
Сборка и тестирование
Прежде чем отправлять образ Docker в облако, стоит протестировать его локально.
Сначала создайте изображение:
$ docker build -t remix-notes:1.0 .
Затем создайте контейнер, используя только что созданный образ:
$ docker run -it -p 3000:3000
-e PARSE_APPLICATION_ID=<your_parse_app_id>
-e PARSE_JAVASCRIPT_KEY=<your_parse_javascript_key>
-d remix-notes:1.0
Обеспечьте замену
<your_parse_app_id>
и<your_parse_javascript_key>
фактическими учетными данными.
Теперь ваше приложение должно быть доступно по адресу http://localhost:3000. Оно должно вести себя так же, как и до процесса докеризации.
Отправить на GitHub
Чтобы развернуть приложение в Back4app Containers, необходимо сначала разместить исходный код на GitHub. Для этого вы можете выполнить следующие шаги:
- Войдите в свою учетную запись GitHub (или зарегистрируйтесь).
- Создайте новый репозиторий GitHub.
- Перейдите в локальный проект и инициализируйте его:
git init
- Добавьте весь код в систему контроля версий:
git add .
- Добавьте удаленный источник с помощью
git remote add origin
- Зафиксируйте весь код с помощью
git commit -m "initial commit".
- Разместите код на GitHub
git push origin master
Развернуть приложение
В последнем разделе мы развернем фронтенд на контейнерах Back4app.
Войдите в свой аккаунт Back4app и нажмите кнопку “Создать новое приложение”, чтобы начать процесс создания приложения.
Поскольку мы сейчас разворачиваем приложение с контейнером, выберите “Контейнеры как служба”.
Далее необходимо связать свой аккаунт GitHub с Back4app и импортировать созданный ранее репозиторий. После подключения выберите репозиторий.
Back4app Containers позволяет выполнять расширенную настройку. Тем не менее, для нашего простого приложения будет достаточно следующих настроек:
- Название приложения: remix-notes (или выберите свое название)
- Переменные окружения:
PARSE_APPLICATION_ID
,PARSE_JAVASCRIPT_KEY
Используйте значения, которые вы использовали в файле .env для переменных окружения.
Закончив настройку развертывания, нажмите “Развернуть”.
Подождите несколько минут, пока развертывание не завершится. После развертывания нажмите на зеленую ссылку в левой части экрана, чтобы открыть приложение в браузере.
Для получения подробного руководства, пожалуйста, ознакомьтесь с документацией по Container Remix от Back4app.
Вот и все! Теперь ваше приложение успешно развернуто и доступно по указанной ссылке. Кроме того, Back4app выдал бесплатный SSL-сертификат для вашего приложения.
Заключение
Несмотря на то, что Remix – относительно новый фреймворк, он позволяет разработчикам создавать мощные полнофункциональные веб-приложения.
Фреймворк решает многие сложные задачи веб-приложений, такие как обработка форм, управление состояниями и многое другое.
В этой статье вы узнали, как создать и развернуть приложение Remix. Теперь вы должны уметь использовать Back4app для создания простого бэкенда и Back4app Containers для развертывания контейнерных приложений.
Исходный код проекта доступен на репо back4app-containers-remix.