Введение
В последние годы существует два чрезвычайно популярных способа рендеринга веб-страниц: одностраничные приложения и рендеринг на стороне сервера.
Существует несколько инструментов и шаблонов, которые помогают нам настроить проект React для создания SPA, например, знаменитый create-react-app
и vite. Но когда мы говорим о SSR, мы обычно имеем в виду фреймворки, такие как Next.js, Remix и Razzle.
Однако, хотя существует множество статей и туториалов о том, как перенести существующее React-приложение на Next.js, не так много материалов о том, как преобразовать текущий проект с React на SSR без использования фреймворка.
В этом руководстве мы вместе рассмотрим, как можно преобразовать React SPA с помощью Vite в SSR.
Что мы будем использовать?
В этом уроке мы будем использовать следующие технологии для создания SSR-приложения:
- React — react — это инструмент для создания компонентов пользовательского интерфейса.
- React Router — помогает управлять навигацией между страницами различных компонентов в приложении react
- Vite — инструмент сборки, использующий доступность ES-модулей в браузере и компиляцию в нативный бандлер
- h3 — минималистичный и простой фреймворк для node.js
- sirv — простое и удобное промежуточное программное обеспечение для обслуживания статических файлов
- listhen — элегантный http listener
Предварительные условия
Прежде чем продолжить, вам понадобятся
- Node
- Yarn
- TypeScript
- React
Кроме того, ожидается, что вы обладаете базовыми знаниями об этих технологиях.
Создание проекта Vite
В качестве первого шага создайте каталог проекта и перейдите в него:
yarn create vite react-ssr --template react-ts
cd react-ssr
Далее установим маршрутизатор react:
yarn add react-router-dom
Теперь мы можем создать наши страницы внутри src/pages/
:
// @/src/pages/Home.tsx
export const Home = () => {
return <div>This is the Home Page</div>;
};
// @/src/pages/Other.tsx
export const Home = () => {
return <div>This is the Other Page</div>;
};
// @/src/pages/NotFound.tsx
export const NotFound = () => {
return <div>Not Found</div>;
};
Затем мы переименуем наш App.tsx
в router.tsx
и, как вы уже догадались, именно в этом файле мы определим каждый из маршрутов нашего приложения:
// @/src/router.tsx
import { Routes, Route } from "react-router-dom";
import { Home } from "./pages/Home";
import { Other } from "./pages/Other";
import { NotFound } from "./pages/NotFound";
export const Router = () => {
return (
<Routes>
<Route index element={<Home />} />
<Route path="/other" element={<Other />} />
<Route path="*" element={<NotFound />} />
</Routes>
);
};
Когда страницы нашего приложения созданы, а маршруты определены, мы можем приступить к работе над нашими входными файлами.
В настоящее время единственным входным файлом в нашем проекте является main.tsx
, который мы переименуем в entry-client.tsx
. Этот файл будет отвечать за точку входа в связку браузера и будет выполнять гидратацию страницы.
// @/src/entry-client.tsx
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import { Router } from "./router";
ReactDOM.hydrateRoot(
document.getElementById("app") as HTMLElement,
<BrowserRouter>
<Router />
</BrowserRouter>
);
Следующий файл входа, который мы создадим, это entry-server.tsx
, в котором мы экспортируем функцию render()
, которая будет получать местоположение (путь) в аргументах, затем рендерить страницу, которая была запрошена, и в конце рендерить в строку (которая позже будет добавлена в index.html
на сервере узла).
// @/src/entry-server.tsx
import ReactDOMServer from "react-dom/server";
import { StaticRouter } from "react-router-dom/server";
import { Router } from "./router";
interface IRenderProps {
path: string;
}
export const render = ({ path }: IRenderProps) => {
return ReactDOMServer.renderToString(
<StaticRouter location={path}>
<Router />
</StaticRouter>
);
};
И последнее, но не менее важное: нам нужно внести изменения в index.html
, чтобы он выглядел следующим образом:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite SSR + React + TS</title>
</head>
<body>
<div id="app"><!--ssr-outlet--></div>
<script type="module" src="/src/entry-client.tsx"></script>
</body>
</html>
Когда клиентская часть нашего приложения создана, мы можем перейти к следующему шагу.
Создание сервера Node
Прежде чем начать писать код, нам нужно установить необходимые зависимости:
yarn add h3 sirv listhen
Сервер node будет отвечать за обслуживание нашего приложения в среде разработки и производства. Но эти две среды совершенно разные, и каждая из них имеет свои требования.
Идея заключается в том, что в среде разработки мы будем использовать vite во всем процессе, то есть он будет использоваться как сервер разработки, он будет преобразовывать html и рендерить страницу.
В то время как в производственной среде мы хотим обслуживать статические файлы, которые будут находиться в папке dist/client/
, а также JavaScript, который мы будем запускать для рендеринга страниц, будет находиться в dist/server/
, и именно его мы будем использовать. Вот пример:
// @/server.js
import fs from "fs";
import path from "path";
import { createApp } from "h3";
import { createServer as createViteServer } from "vite";
import { listen } from "listhen";
import sirv from "sirv";
const DEV_ENV = "development";
const bootstrap = async () => {
const app = createApp();
let vite;
if (process.env.NODE_ENV === DEV_ENV) {
vite = await createViteServer({
server: { middlewareMode: true },
appType: "custom",
});
app.use(vite.middlewares);
} else {
app.use(sirv("dist/client", {
gzip: true,
})
);
}
app.use("*", async (req, res, next) => {
const url = req.originalUrl;
let template, render;
try {
if (process.env.NODE_ENV === DEV_ENV) {
template = fs.readFileSync(path.resolve("./index.html"), "utf-8");
template = await vite.transformIndexHtml(url, template);
render = (await vite.ssrLoadModule("/src/entry-server.tsx")).render;
} else {
template = fs.readFileSync(
path.resolve("dist/client/index.html"),
"utf-8"
);
render = (await import("./dist/server/entry-server.js")).render;
}
const appHtml = await render({ path: url });
const html = template.replace(`<!--ssr-outlet-->`, appHtml);
res.statusCode = 200;
res.setHeader("Content-Type", "text/html").end(html);
} catch (error) {
vite.ssrFixStacktrace(error);
next(error);
}
});
return { app };
};
bootstrap()
.then(async ({ app }) => {
await listen(app, { port: 3333 });
})
.catch(console.error);
После объяснения работы сервера node и приведенного примера мы можем добавить следующие скрипты в package.json
:
{
"dev": "NODE_ENV=development node server",
"build": "yarn build:client && yarn build:server",
"build:client": "vite build --outDir dist/client",
"build:server": "vite build --ssr src/entry-server.tsx --outDir dist/server",
"serve": "NODE_ENV=production node server"
}
Это скрипты, которые позволяют запустить приложение. Если вы хотите запустить среду разработки, просто запустите yarn dev
, если вы хотите собрать приложение, просто используйте yarn build
, а yarn serve
— для запуска производственной среды.
Если вы перейдете по адресу http://localhost:3333
, у вас должно быть запущено веб-приложение.
Заключение
Как всегда, я надеюсь, что статья показалась вам интересной, и что она помогла вам перевести существующее приложение с React with Vite на SSR более простым и удобным способом.
Если вы нашли ошибку в статье, пожалуйста, сообщите мне об этом в комментариях, чтобы я мог ее исправить. Прежде чем закончить, если вы хотите получить доступ к исходному коду этой статьи, я оставляю здесь ссылку на репозиторий github.
Хорошего дня!