Server Side Rendering (SSR): как добавить в Vue 3 + Vite приложение
Большую массу PHP-программистов пугает тема SSR, ведь данная технология сильно сопряжена с JavaScript. Многие программисты рекомендуют использовать Node.js и готовые SSR фреймворки в обход любимому PHP.
И да, справедливости ради, уйти от PHP в данном случае и правда хороший вариант, хотя не все хотят это делать по каким-либо причинам. Так вот, данная статья призвана показать, как с помощью PHP и Node.js, внутри Laravel, реализовать полноценный SSR как в каком-нибудь готовом Node.
js фреймворке по типу Nuxt.js или Quasar. Сходите за чаем, будет весело (автору статьи).
Некоторые моменты будут показаны очень упрощено и, возможно, неказисто. Сделано это для упрощения статьи, но если захотите что-то обсудить или спросить, смело пишите свои комментарии.
Что такое SSR
SSR – это технология, которая позволяет исполнять на сервере JavaScript, а не в браузере, как это обычно происходит. SSR легко реализуется с помощью Node.js, так как там внутри есть все необходимые инструменты. Можно, конечно, обойтись без Node.js и вырвать из его исходников V8 (движок JavaScript на С++), но это уже совсем безбашенный вариант.
Если интересно почитать про SSR подробнее, то переходите по ссылке на нашу тематическую статью.
Перед стартом
Автор надеется что у Вас глобально установлен composer, PHP и Node.js. Автор использует PHP 8.1.1, Composer 2.4.4, Node 16.15.0. В статье используется Laravel 9, Vue 3 и Vite 3.0.0.
Подготовка
- Для начала, давайте создадим Laravel проект:
- composer create-project laravel/laravel example-app
- Теперь мы можем перейти в проект:
- cd example-app
- И запустим его с помощью команды:
- php artisan serve
- Хотя, запускать его нам пока не надо, но можете это сделать для проверки.
Теперь нам надо установить laravel/ui пакет, для того, чтобы потом добавить Vue.js в проект. Выполните команду:
- composer require laravel/ui
- После этого нам надо добавить, через artisan, уже непосредственно Vue.js:
- php artisan ui vue
Скорее всего, у Вас проект теперь использует Vue 3, а не Vue 2. А также Vite, а не Webpack, но это все не страшно, мы были к этому готовы. У нас будет Vue 3, а значит и возможность использовать composition API.
Теперь смотрите очень внимательно и выполняйте все действия правильно. Сначала установим пакеты, которые прилетели к нам в проект после установки прошлого пакета из composer. Для этого выполните команду:
- npm install
- Теперь устанавливаем Vue Router:
- npm install vue-router@4
Далее, нам надо создать что-то вроде простого базового проекта на Vue.js. Для этого поместите следующее содержимое в resources/views/welcome.blade.php (старое удалите):
Создайте в папке /resources/js файл router.js со следующим содержимым:
Создайте в папке /resources/js папку pages и внутри нее два компонента Home.vue и About.vue с таким содержимым:
- Теперь, создайте в папке /resources/js App.vue с таким содержимым:
- Тут как раз мы уже используем подход с setup, а значит можем использовать composition API (ну это так, для справки).
- И последний шаг, в папке /resources/js есть файл app.js, замените его содержимое на это:
- Когда Вы все сделали, запускайте команду:
- npm run build
- Потом, запускайте сервер с помощью:
- php artisan serve
- В браузере у Вас должен запуститься наш проект по адресу localhost:8000.
Можете посмотреть как работает router, нажимать на кнопку с переменной count. В целом этого хватит, теперь можно настраивать SSR.
- Ах да, если Вам не хватает привычной команды watch, можно добавить в раздел scripts в package.json свою команду с подобным эффектом:
- vite build —watch
- Пример:
Добавляем SSR
Если Вы откроете исходный код нашего проекта сейчас, то не увидите текста из компонентов:
Пора исправлять это гнусное недоразумение. Для этого мы напишем свой модуль на Node.js, который будет делать SSR. После этого из PHP мы будем вызывать Node.js и делать магию (надеемся чай уже выпит, далее потребуется что-то покрепче).
В папке /resources/js создайте файл server-app.js с таким содержимым:
Это копия нашего файла app.js, только настроенная под исполнение на сервере. Вместо createApp мы используем createSSRApp из библиотеки Vue.js. Функция renderToString из vue/server-renderer – это готовая функция, которая будет рендерить наше приложение на сервере. Подробнее можете почитать тут, в документации Vue.js по SSR.
A в асинхронную функцию main мы все обернули для того, чтобы использовать await в router и renderToString.
Это обязательно, если router не отработает (не будет готов), то Вы никогда не отрисуете нужный компонент по нужному маршруту.
А если не дождетесь ответа от renderToString, то и не увидите готовое приложение. И да, в нашем случае функция main автоматически вызывается, когда будет обращение к файлу.
Обратите внимание на строчку 9, там есть переменная requestUrl, которую мы заполняем из process.argv. Переменная процесса всегда доступна в Node.js, оттуда можно взять полезные вещи. Например, когда мы делаем так:
node index.js test
В process.argv[2] будет лежать test. Вот таким же способом из PHP мы будем передавать URL адрес, на которой совершился переход.
Ну и на 19 строчке есть process.stdout.write(), таким способом, мы отдадим результат из скрипта, который вызовем из shell_exec() в PHP. К слову, можно просто console.log() сделать в Node.js, тогда PHP тоже увидит результат.
Теперь, нам надо внести правки в router.js, который мы сделали пару минут назад. Измените его содержимое на следующее:
Здесь мы добавили изменения в начале файла – на 1, 3 и 4 строчке. Мы проверяем выполняется ли сейчас код на сервере, и если ответ утвердительный, то вместо createWebHistory используем createMemoryHistory. То есть, храним нужные маршруты в оперативной памяти, а не в истории браузера.
Вообще, внутри Node.js нет переменных window и document. Вместо window есть global, хотя ее содержимое даже близко к window не стоит. Ну а последней – document, нет и в помине, ибо это переменная дает доступ к API DOM дерева. Как Вы уже догадались, DOM дерева в Node.js нет.
Теперь идем в корень проекта, и в файле vite.config.js убираем строку:
vue: 'vue/dist/vue.esm-bundler.js',
К сожалению, это какое-то странное поведение Vite 3 и Vue 3, если Вы не уберете эту строчку, то Vite не увидит библиотеку vue/server-renderer. К слову, собрать приложение дальше нам это не помешает.
- Теперь, там же, добавьте две строчки в раздел laravel() под переменной input:
- ssr: 'resources/js/server-app.js',
ssrOutputDirectory: 'bootstrap/ssr', - Должно получиться так:
Переменная ssrOutputDirectory по умолчанию и так все отправит в bootstrap/ssr, но если Вы захотите, можете переопределить папку куда полетят собранные файлы (но пока это не делайте). Теперь модернизируем команду build в package.json. Давайте добавим туда сборку SSR сервера:
vite build && vite build —ssr
Теперь нам осталось модернизировать welcome.blade.php и web.php (пути в Laravel). Добавьте этот код в routes/web.php (прошлый можете удалить):
И в resources/views/welcome.blade.php удалите секцию:
- И добавьте следующий код:
- Должно получиться как-то так:
Строку:
/Users/maximkolmogorov/.nvm/versions/node/v16.15.0/bin/node
Измените на свой путь до Node.js. Вообще, обычно это:
/usr/local/bin/node
Но у автора по этому пути лежит Node.js 12, а нужно не меньше 16.15. Если что, можете установить нужную версию через NVM. Узнать версию Node.js у себя можно с помощью:
- node -v
- А путь до нее:
- npm config get prefix
- Главное к результату добавьте в конце /bin/node.
Как видно по коду, мы запускаем Node.js сервер из PHP с помощью shell_exec(). Почему именно shell_exec, а не exec? Просто exec вернет только последнюю строку вывода, а shell_exec вернет все. Хотя, внутри Node.
js мы возвращаем все через process.stdout.write(), и результат и так будет в одной большой строке, но если бы мы где-то выше использовали еще и console.
log() (то есть, создали бы две строки), то shell_exec наш обязательный выбор.
Как видите, вторым аргументом мы передаем наш собранный SSR сервер, а третий аргумент – это URL адрес, который мы пихаем в router.js. Главное в web.php дублировать все маршруты, которые есть в нашем Vue.js приложении.
- Теперь собираем все наше добро командой:
- npm run build
- И запускаем наш Laravel проект:
- php artisan serve
- Переходим на какую-нибудь страницу, открываем полный исходный код и видим наш долгожданный контент:
Итог
Думаем слова излишни. Если Вы дочитали до этого момента (не перематывая), то, скорее всего, передумали внедрять SSR таким способом. Все таки, это довольно болезненный процесс, при котором надо неплохо владеть PHP, Node.
js, инструментом сборки и фреймворком, который нужно рендерить (в нашем случае Vue.js). Автор статьи настоятельно рекомендует использовать готовые SSR фреймворки на базе Node.js: Nuxt.js, Quasar – для Vue.js, Next.
js, Gatsby – для React.
Под капотом у готовых инструментов для SSR в PHP (по типу spatie/laravel-server-side-rendering) внутри лежит подобная реализация и подход к работе с Node.js сервером и рендерингом контента. Вам в любом случае придется писать Node.js сервер.
Ну и данный плагин не протестирован вместе с Vite, у автора статьи не получилось подружить его вместе с ним. Видимо, если захочется использовать его, придется мигрировать с Vite на Webpack в рамках Laravel. А новые версии Laravel идут вместе с Vite, и выбора между ним и Webpack пока не предусмотрено.
Хотя, в том же Vue CLI можно выбирать.
Ну и напоследок, Вы же понимаете, что если отойти от реализации автора и придумать что-то другое, где программист будет в нескольких разных местах (в рамках одного PHP процесса) вызывать shell_exec(), то это чревато потерей производительности. Посмотрите на картинку выше. Все таки, SSR крайне тяжелая операция, которую лучше не использовать больше одного раза и не трогать лишний раз цепочку Node.js > V8 > HTML в одном проекте при выполнении PHP скрипта.
Спасибо за внимание, если понравилось… то круто. Хорошего дня!
My first time implementing SSR using Vue 3 and Vite
Server-side Rendering (SSR) is one of the methods to present web content to the user. Even though this method is quite old, I never had a chance to learn it by a code, only by the concept. In this article, I will try to provide an overview of SSR, with the goal of demonstrating its implementation through simple examples.
What is SSR
SSR refers to generating content in the browser while the fetching and rendering are done on the server. Since the content is rendered on the server, it becomes available to the user once the loading is complete.
However, for any interaction process, we need to handle it by doing the hydration process first. The HTML that the user receives is completely static, and there is a waiting time for the hydration process. This is called Time to Interactive.
You can read the explanation from the web.dev team for more information here.
SSR in Vue
Nuxt is a popular framework for handling SSR project. However, in this article, we won’t use Nuxt to implement the SSR. Instead, we will use:
- Vue 3 as the base client library
- Express for the back end
- Vite for the bundler
How to implement
For this article, we will use Stackblitz to implement our small project. You can check the full code here
Initiate the Project
Let’s initiate the project by using Vite vue-ts. Here is our directory, we will remove some files that we do not need.
- After that, we will add these files
— index.html
— server.js # Main application server
— src/ — main.ts # we will store the function to init our app here — entry-client.ts # This function mounts the app to a DOM element and will be used in the hydration process. — entry-server.ts # renders the app using the framework's SSR API, we will use it in server.js
Add Client Code
For our client-side code, we add main.js. The createSSRApp is a function that was introduced in Vue 3 to create a server-side rendering application. This function can be used to render the Vue application on the server.
import { createSSRApp } from 'vue';
import App from './App.vue'; export const createApp = () => { /** * use createSSRApp to render the Vue App on the server * and send it to the user to do the hydration process */ const app = createSSRApp(App); return { app, };
};
Then we add entry-client.ts to initiate our app on the client-side
import { createApp } from './main.js'; /** * initiate the Vue App for a client-side application */
const { app } = createApp();
app.mount('#app');
Also, let’s update the App.vue. Here we display a counter inside the button, which increments with every click.
{{ count }}
Add Server Code
The next step is to handle the server-side code. Add entry-server.ts and use the app from createSSRApp. Then, render the app to HTML using renderToString, which can then be sent to the client.
import { renderToString } from 'vue/server-renderer';
import { createApp } from './main'; /** * initiate the Vue App for a server-side application, * we use renderToString to render the app to HTML */ export const render = async () => { const { app } = createApp(); const html = await renderToString(app); return { html, };
};
Combine Client and Server
Now let’s handle the server. We will use express for our Node.js app
Next, add server.js. You can find this code guide in Vite SSR guide.
const express = require('express');
const fs = require('fs');
const path = require('path');
const { createServer } = require('vite'); async function initServer() { const app = express(); // Create Vite server in middleware mode and configure the app type as // 'custom', disabling Vite's own HTML serving logic so parent server // can take control const vite = await createServer({ server: { middlewareMode: true }, appType: 'custom', }); // Use vite's connect instance as middleware. If you use your own // express router (express.Router()), you should use router.use app.use(vite.middlewares); app.use('*', async (req, res) => { // 1. Read index.html let template = fs.readFileSync( path.resolve(__dirname, 'index.html'), 'utf-8' ); // 2. Apply Vite HTML transforms. This injects the Vite HMR client, // and also applies HTML transforms from Vite plugins, e.g. global // preambles from @vitejs/plugin-react template = await vite.transformIndexHtml(req.originalUrl, template); // 3. Load the server entry. ssrLoadModule automatically transforms // ESM source code to be usable in Node.js! There is no bundling // required, and provides efficient invalidation similar to HMR. const render = (await vite.ssrLoadModule('/src/entry-server.ts')).render; // 4. render the app HTML. This assumes entry-server.js's exported // `render` function calls appropriate framework SSR APIs, // e.g. ReactDOMServer.renderToString() const { html: appHtml } = await render(); // 5. Inject the app-rendered HTML into the template. const html = template.replace('', appHtml); // 6. Send the rendered HTML back. res.set({ 'Content-Type': 'text/html' }).end(html); }); return app;
} initServer().then((app) => app.listen(3000, () => { console.log('ready'); })
);
Then, let’s update our index.html. Add the placeholder and update the script source file to /src/entry-client.ts
Finally, update the dev scripts in package.json to node server.js
{ «scripts»: { «dev»: «node server.js» },
}
Conclusion
In this article, we already learn how to create simple SSR applications by using Vue, Vite, and Express. There are a lot of things that we can improve, some of them are:
Vite
Примечание
SSR конкретно относится к интерфейсным платформам (например, React, Preact, Vue и Svelte), которые поддерживают запуск одного и того же приложения в Node.js, предварительный рендеринг его в HTML и, наконец, его гидратацию на клиенте. Если вам нужна интеграция с традиционными серверными фреймворками, ознакомьтесь с Руководством по интеграции с серверной частью.
https://www.youtube.com/watch?v=RCDOqF0jLnU\u0026pp=ygVZU2VydmVyIFNpZGUgUmVuZGVyaW5nIChTU1IpOiDQutCw0Log0LTQvtCx0LDQstC40YLRjCDQsiBWdWUgMyArIFZpdGUg0L_RgNC40LvQvtC20LXQvdC40LU%3D
В следующем руководстве также предполагается, что у вас есть опыт работы с SSR в выбранной вами среде, и оно будет сосредоточено только на деталях интеграции, специфичных для Vite.
API низкого уровня
Это низкоуровневый API, предназначенный для авторов библиотек и фреймворков. Если ваша цель — создать приложение, обязательно сначала ознакомьтесь с плагинами и инструментами SSR более высокого уровня в разделе Awesome Vite SSR. Тем не менее, многие приложения успешно создаются непосредственно поверх собственного низкоуровневого API Vite.
Vite предоставляет встроенную поддержку рендеринга на стороне сервера (SSR). Игровая площадка Vite содержит примеры настроек SSR для Vue 3 и React, которые можно использовать в качестве справочных материалов для этого руководства:
Исходная структура #
Типичное приложение SSR будет иметь следующую структуру исходного файла:
— index.html
— server.js # main application server
— src/
— main.js # exports env-agnostic (universal) app code
— entry-client.js # mounts the app to a DOM element
— entry-server.js # renders the app using the framework's SSR API
index.html должен будет ссылаться на entry-client.js и включать заполнитель, где должна быть введена разметка, отображаемая сервером:
html
Вы можете использовать любой заполнитель вместо , если его можно точно заменить.
Условная логика #
Если вам нужно выполнить условную логику на основе SSR и клиента, вы можете использовать
jsif (import.meta.env.SSR) {
// … server only logic
}
Это статически заменяется во время сборки, поэтому это позволяет tree-shaking неиспользуемых branches.
Настройка сервера разработки #
При создании приложения SSR вы, вероятно, захотите иметь полный контроль над своим основным сервером и отделить Vite от производственной среды. Поэтому рекомендуется использовать Vite в режиме мидлвара. Вот пример с express:
server.js
jsimport fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
import express from 'express'
import { createServer as createViteServer } from 'vite'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
async function createServer() {
const app = express()
// Create Vite server in middleware mode and configure the app type as
// 'custom', disabling Vite's own HTML serving logic so parent server
// can take control
const vite = await createViteServer({
server: { middlewareMode: true },
appType: 'custom'
})
// use vite's connect instance as middleware
// if you use your own express router (express.Router()), you should use router.use
app.use(vite.middlewares)
app.use('*', async (req, res) => {
// serve index.html — we will tackle this next
})
app.listen(5173)
}
createServer()
Здесь vite — это экземпляр ViteDevServer. vite.middlewares — это экземпляр Connect, который можно использовать в качестве мидлвара в любой совместимой с Connect инфраструктуре Node.js.
Следующим шагом является реализация обработчика * для обслуживания отображаемого сервером HTML:
jsapp.use('*', async (req, res, next) => {
const url = req.originalUrl
try {
// 1. Read index.html
let template = fs.readFileSync(
path.resolve(__dirname, 'index.html'),
'utf-8'
)
// 2. Apply Vite HTML transforms. This injects the Vite HMR client, and
// also applies HTML transforms from Vite plugins, e.g. global preambles
// from @vitejs/plugin-react
template = await vite.transformIndexHtml(url, template)
// 3. Load the server entry. vite.ssrLoadModule automatically transforms
// your ESM source code to be usable in Node.js! There is no bundling
// required, and provides efficient invalidation similar to HMR.
const { render } = await vite.ssrLoadModule('/src/entry-server.js')
// 4. render the app HTML. This assumes entry-server.js's exported `render`
// function calls appropriate framework SSR APIs,
// e.g. ReactDOMServer.renderToString()
const appHtml = await render(url)
// 5. Inject the app-rendered HTML into the template.
const html = template.replace(«, appHtml)
// 6. Send the rendered HTML back.
res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
} catch (e) {
// If an error is caught, let Vite fix the stack trace so it maps back to
// your actual source code.
vite.ssrFixStacktrace(e)
next(e)
}
})
Сценарий dev в package.json также следует изменить, чтобы вместо него использовался сценарий сервера:
diff «scripts»: {
— «dev»: «vite»
+ «dev»: «node server»
}
Сборка для продакшена #
Чтобы отправить проект SSR в производство, нам необходимо:
- Произведите сборку клиента как обычно;
- Создайте сборку SSR, которую можно напрямую загрузить через import(), чтобы нам не приходилось проходить через Vite ssrLoadModule;
Наши скрипты в package.json будут выглядеть так:
json{
«scripts»: {
«dev»: «node server»,
«build:client»: «vite build —outDir dist/client»,
«build:server»: «vite build —outDir dist/server —ssr src/entry-server.js »
}
}
Обратите внимание на флаг —ssr, который указывает, что это сборка SSR. Также следует указать запись SSR.
Затем в server.js нам нужно добавить некоторую специфичную для производства логику, проверив process.env.NODE_ENV:
- Вместо того, чтобы читать корень index.html, используйте dist/client/index.html в качестве шаблона, так как он содержит правильные ссылки ресурсов на сборку клиента.
- Вместо await vite.ssrLoadModule('/src/entry-server.js'), используйте import('./dist/server/entry-server.js') (этот файл является результатом сборки SSR).
- Переместите создание и все использование сервера разработки vite за условные ветки только для разработки, затем добавьте мидлвар для обслуживания статических файлов для обслуживания файлов из dist/client.
Смотрите демо для рабочей установки Vue и React.
Создание директив предварительной загрузки #
vite build поддерживает флаг —ssrManifest, который будет генерировать ssr-manifest.json в выходном каталоге сборки:
diff- «build:client»: «vite build —outDir dist/client»,
+ «build:client»: «vite build —outDir dist/client —ssrManifest»,
Приведенный выше скрипт теперь сгенерирует dist/client/ssr-manifest.json для сборки клиента (да, манифест SSR создается из сборки клиента, потому что мы хотим сопоставить идентификаторы модулей с файлами клиента). Манифест содержит сопоставления идентификаторов модулей с соответствующими фрагментами и файлами активов.
https://www.youtube.com/watch?v=RCDOqF0jLnU\u0026pp=YAHIAQE%3D
Чтобы использовать манифест, фреймворки должны предоставить способ сбора идентификаторов модулей компонентов, которые использовались во время вызова рендеринга сервера.
@vitejs/plugin-vue поддерживает это из коробки и автоматически регистрирует используемые идентификаторы модулей компонентов в связанном контексте Vue SSR:
js// src/entry-server.js
const ctx = {}
const html = await vueServerRenderer.renderToString(app, ctx)
// ctx.modules is now a Set of module IDs that were used during the render
В рабочей ветке server.js нам нужно прочитать и передать манифест функции render, экспортируемой src/entry-server.js. Это даст нам достаточно информации для рендеринга директив предварительной загрузки для файлов, используемых асинхронными маршрутами! Полный пример см. в источнике демо.
Предварительный рендеринг / SSG #
Если маршруты и данные, необходимые для определенных маршрутов, известны заранее, мы можем предварительно преобразовать эти маршруты в статический HTML, используя ту же логику, что и производственная SSR. Это также можно рассматривать как форму создания статических сайтов (SSG). Смотрите демонстрационный сценарий предварительного рендеринга для рабочего примера.
Внешний SSR #
Зависимости «экстернализуются» из системы модулей преобразования Vite SSR по умолчанию при запуске SSR. Это ускоряет как разработку, так и сборку.
Если зависимость должна быть преобразована конвейером Vite, например, потому что функции Vite используются в них нетранспилированными, их можно добавить в ssr.noExternal.
Для связанных зависимостей они по умолчанию не экстернализуются, чтобы использовать преимущества HMR Vite. Если это нежелательно, например, для проверки зависимостей, как если бы они не были связаны, вы можете добавить его в ssr.external.
Работа с псевдонимами
Если вы настроили псевдонимы, которые перенаправляют один пакет на другой, вы можете вместо этого использовать псевдонимы для фактических пакетов node_modules, чтобы он работал для внешних зависимостей SSR. И Yarn, и pnpm поддерживают псевдонимы через префикс npm:.
Логика плагина, специфичная для SSR #
Некоторые фреймворки, такие как Vue или Svelte, компилируют компоненты в разные форматы в зависимости от клиента и SSR. Для поддержки условных преобразований Vite передает дополнительное свойство ssr в объект options следующих хуков плагина:
Example:
jsexport function mySSRPlugin() {
return {
name: 'my-ssr',
transform(code, id, options) {
if (options?.ssr) {
// perform ssr-specific transform…
}
}
}
}
Объект параметров в load и transform является необязательным, в настоящее время накопительный пакет не использует этот объект, но может расширить эти хуки дополнительными метаданными в будущем.
Примечание
До Vite 2.7 об этом сообщалось обработчикам плагинов с позиционным параметром ssr вместо использования объекта options. Все основные фреймворки и плагины обновлены, но вы можете найти устаревшие сообщения, используя предыдущий API.
Цель SSR #
Целью по умолчанию для сборки SSR является среда узла, но вы также можете запустить сервер в веб-воркере. Разрешение входа пакетов отличается для каждой платформы. Вы можете настроить цель как Web Worker, используя для ssr.target значение 'webworker'.
SSR бандл #
В некоторых случаях, таких как среды выполнения webworker, вы можете захотеть объединить сборку SSR в один файл JavaScript. Вы можете включить это поведение, установив для ssr.noExternal значение true. Это сделает две вещи:
- Рассматривать все зависимости как noExternal
- Выдавать ошибку, если какие-либо встроенные модули Node.js импортированы
Vite CLI #
Команды CLI $ vite dev и $ vite preview также можно использовать для приложений SSR. Вы можете добавить слой мидлваров SSR на сервер разработки с помощью configureServer и на сервер предварительного просмотра с помощью configurePreviewServer.
Примечание
Используйте пост-хук, чтобы ваше мидлвар SSR запускал мидлвар после Vite.
Формат SSR #
По умолчанию Vite генерирует пакет SSR в ESM. Существует экспериментальная поддержка настройки ssr.format, но это не рекомендуется.
Будущие усилия по разработке SSR будут основываться на ESM, а CommonJS останется доступным для обратной совместимости. Если использование ESM для SSR в вашем проекте невозможно, вы можете установить legacy.
buildSsrCjsExternalHeuristics: true для создания пакета CJS с использованием той же эвристики экстернализации Vite v2.
Server-Side Rendering | Vite
Экспериментальная функция
Поддержка SSR всё ещё находится в экспериментальной стадии и вы можете столкнуться с багами не поддерживаемыми случаями. Продолжайте на свой страх и риск.
Заметка
SSR спецификация относится к front-end фреймворкам (например, React, Preact, Vue, и Svelte), которые поддерживают запуск одного и того же приложения на Node.js, pre-rendering приложения в HTML и в завершении hdrating (гидратации) приложения на клиенте. Если вы идите интеграции с традиционными server-side фреймворками, взгляните сюда Backend Integration guide.
Текущее руководство также предполагает, что у вас уже есть предыдущий опыт работы с SSR в вашем любимом фреймворке, и этот гайд фокусируется только на Vite-specific деталях интеграции.
Low-level API
Это low-level (низкоуровневое) API предназначенное для авторов библиотек и фреймворков. Если ваша цель создать приложение, то сначала поищите higher-level (высокоуровневое решение) SSR плагины и инструменты в разделе Awesome Vite SSR. Тем не менее многие приложения успешно создаются непосредственно поверх нативного низкоуровнего (low-level) API Vite.
Vite предоставляет built-in (встроенную) поддержку для server-side rendering (SSR). Vite playground содержит пример SSR настройки для Vue 3 и React, что может быть использовано как референсы для текущего руководства:
Source Structure #
Типичное SSR приложение будет содержать следующую структуру файлов:
— index.html
— src/
— main.js # exports env-agnostic (universal) app code
— entry-client.js # mounts the app to a DOM element
— entry-server.js # renders the app using the framework's SSR API
index.html должен ссылаться на entry-client.js и включать в себя placeholder, куда будет вставлена сгенерированная сервером (server-rendered) разметка:
Вы можете использовать любой placeholder на ваше усмотрение, вместо , любой, который может быть заменён.
Условная логика #
Если вам нужно выполнить условную логику опираясь на том, где мы, на SSR или на клиенте, вы можете использовать это
if (import.meta.env.SSR) { }
Это статично замениться во время сборки, поэтому это позволяет tree-shaking неиспользуемые ветки.
Настраиваем Dev Server #
Когда собирается SSR приложение, вы вероятно захотите иметь полный контроль над вашим главным сервером и отделить Vite от production окружения. Поэтому рекомендуется использовать Vite в режиме middleware. Вот пример с express:
server.js
const fs = require('fs')
const path = require('path')
const express = require('express')
const { createServer: createViteServer } = require('vite') async function createServer() { const app = express() const vite = await createViteServer({ server: { middlewareMode: 'ssr' } }) app.use(vite.middlewares) app.use('*', async (req, res) => { }) app.listen(3000)
} createServer()
Здесь vite — это экземпляр ViteDevServer. vite.middlewares — это экземпляр Connect, который может быть использован как middleware в любом connect-compatible Node.js фреймворке.
Следующий шаг — это реализация * обработчика, чтобы сёрвить server-rendered HTML:
app.use('*', async (req, res) => { const url = req.originalUrl try { let template = fs.readFileSync( path.resolve(__dirname, 'index.html'), 'utf-8' ) template = await vite.transformIndexHtml(url, template) const { render } = await vite.ssrLoadModule('/src/entry-server.js') const appHtml = await render(url) const html = template.replace(«, appHtml) res.status(200).set({ 'Content-Type': 'text/html' }).end(html) } catch (e) { vite.ssrFixStacktrace(e) console.error(e) res.status(500).end(e.message) }
})
dev скрипт в package.json должен также быть заменён на использование server script:
«scripts»: {
— «dev»: «vite»
+ «dev»: «node server»
}
Сборка для Production #
Чтобы поставить SSR проект для production, нам нужно сделать следующее:
- Создать клиентский build как нормальный (обычный);
- Создать SSR build, который может быть напрямую загружен с помощью require() так что нам не нужно проходить через Vite's ssrLoadModule;
Наши скрипты в package.json будут выглядеть так:
{ «scripts»: { «dev»: «node server», «build:client»: «vite build —outDir dist/client», «build:server»: «vite build —outDir dist/server —ssr src/entry-server.js » }
}
Обратите внимание на —ssr флаг, который показывает, что это SSR build. Также мы должны указать SSR entry.
Затем, в server.js файле нам нужно добавить некоторую production specific логику, с помощью проверки process.env.NODE_ENV:
- Вместо того, чтобы читать рутовый index.html, используйте dist/client/index.html как шаблон, поскольку он содержит правильные ссылки на ресурсы (asset) для клиентской сборки.
- Вместо await vite.ssrLoadModule('/src/entry-server.js'), используйте require('./dist/server/entry-server.js') (этот файл результат SSR сборки).
- Переместите создание и использование vite dev server'а за пределы dev-only условий в коде, затем, добавьте статичные file serving middlewares, чтобы обрабатывать (сёрвить) файлы из dist/client.
Ссылки на Vue и React демо для рабочих настроек.
Generating Preload Directives #
vite build поддерживает флаг —ssrManifest, который генерирует ssr-manifest.json в build output директорию:
— «build:client»: «vite build —outDir dist/client»,
+ «build:client»: «vite build —outDir dist/client —ssrManifest»,
Скрипт выше сгенерирует dist/client/ssr-manifest.json для клиентской сборки (Да, SSR manifest генерируется из client build потому что нам нужно маппить module IDs к клиентским файлам). Manifest содержит маппинги ID модулей к их связанным чанкам и ассетам (ресурсам).
Чтобы использовать манифест, фреймворки должны предоставить путь для сбора ID модулей компонентов, которые были использованы во время вызова server render'а.
@vitejs/plugin-vue поддерживает это из коробки и автоматически регистрирует используемые ID модули компонентов в связанном Vue SSR context:
const ctx = {}
const html = await vueServerRenderer.renderToString(app, ctx)
В production ветке server.js мы должны прочитать и передать manifest в render функцию, которая экспортируется в src/entry-server.js. Это предоставит нам достаточную информацию для рендера preload directives для файлов используемых в async рутах! Смотрите demo source для полного примера.
Pre-Rendering / SSG #
Если руты (routes) и необходимые для них данные определены заранее, мы можем сделать pre-render этих путей в статичный HTML используя ту же логику, как и в production SSR. Это также можно рассматривать как вид Static-Site Generation (SSG). Смотрите demo pre-render script для наглядного примера.
SSR Externals #
Множество зависимостей поставляются как файлы ESM и CommonJS. Когда запускается SSR, зависимость, которая предоставляет сборку CommonJS и может быть «экстернализирована» из Vite's SSR transform / module system, чтобы улучшить скорость и dev и build.
Например, вместо того, чтобы извлекать в pre-bundled ESM версию React и затем трансформировать её обратно для Node.js-совместимым, более эффективно использовать вместо этого require('react'). Это также значительно увеличивает скорость сборки SSR bundle.
Vite выполняет автоматическую SSR экстернализацию с помощью следующей эвристики:
- Если у зависимости резолвнутый ESM entry point и его дефолтный Node entry point различные, вероятно дефолтный Node entry — это CommonJS build, который может быть экстернализирован. Например, vue будет автоматически экстернализирован потому что он поставляется как в ESM, так и в CommonJS сборках.
- В противном случае, Vite проверит, содержит ли entry point валидный ESM синтаксис — если нет, пакет вероятнее всего в формате CommonJS и будет экстернализирован. Например, react-dom будет автоматически экстернализирован, потому что он имеет только один entry, которая в формате CommonJS.
Если эта эвристика приводит к ошибкам, вы можете вручную настроить внешние параметры SSR, используя параметры конфигурации ssr.external иssr.noExternal.
В будущем эта эвристика, вероятно, будет улучшена, чтобы определять, включен ли в проекте type:» module «, так что Vite может также экстернализовать зависимости, которые поставляют сборки ESM, совместимые с Node, путем их импорта через динамический import () во время SSR.
Работа с Aliases
Если вы настроили aliases, которые перенаправляют один пакет на другой, то вероятно вы захотите связать актуальные node_modules пакеты, чтобы это работало для SSR экстернелизированных зависимостей. И Yarn и pnpm поддерживают aliasing через npm: префикс.
SSR-specific Plugin Logic #
Некоторые фреймворки, такие, как Vue или Svelte компилируют компоненты в разные форматы, в зависимости от того, пойдёт это на клиент или SSR. Чтобы поддерживать трансформаирование по условию, Vite передаёт дополнительный аргумент ssr в следующие хуки плагинов:
Пример:
export function mySSRPlugin() { return { name: 'my-ssr', transform(code, id, ssr) { if (ssr) { } } }
}
SSR Target #
По умолчанию, target для SSR сборки — это node environment, но вы также можете запустить сервер в Web Worker'е. Packages entry resolution разный для каждой платформы. Вы можете настроить target как Web Worker установив ssr.target в значение'webworker'.
SSR Bundle #
В некоторых случаях, таких как webworker runtimes, вы возможно захотите собрать ваш SSR build в один JavaScript файл. Вы можете активировать это поведение установив ssr.noExternal в значение true. Это сделает две вещи:
- Обработает все зависимости как noExternal
- Выведет ошибку, если импортируются какие-либо встроенные (built-in) Node.js модули