Server Side Rendering (SSR): как добавить в Vue 3 + Vite приложение
Технический директор компании vverh.digital. JavaScript программист, любитель Kotlin и Swift. При разработке на реактивных фреймворках многие забывают о том, что итоговое приложение – это что-то ближе к SPA, а не классический сайт как «на WordPress». И когда дело доходит до SEO-продвижения, многие хватаются за голову, потому что поисковые системы плохо работают с такими ресурсами. Поэтому давайте сегодня познакомимся с технологией SSR, которая решит данную проблему. SSR – (с англ. Server Side Rendering) технология, позволяющая выполнять на сервере JavaScript код для достижения каких-либо целей. SSR в первую очередь необходим для продвижения сайта в интернете. Есть такое направление в маркетинге как SEO. И чаще всего, SSR необходим именно для этого. SEO – (с англ. Search Engine Optimization) это оптимизация сайта под нужды поисковой системы. Само по себе, SEO продвижение – это целое самостоятельное направление в маркетинге с большой концентрацией капитала бизнеса, поэтому для многих это очень важная тема. Особенно если бизнес генерирует деньги в интернете. Видите ли, когда поисковый робот делает запрос к сайту, сделанному на реактивных фреймворках по типу: Vue, React, Angular, то он видит примерно это: Никакого контента, только полупустой HTML. Хотя, если зайти на сайт с точки зрения обычного человека, мы увидим много текста и картинки. А вот та же самая страница, но с уже включенным SSR: Как видите, тут контент есть. Что вообще происходит? Все просто – сайты, сделанные на JavaScript, обычно, инициализируются в реальном времени на стороне клиента. То есть, в браузере у пользователя. Поэтому поисковая система просто не может грамотно считать контент сайта, и значит, дать ему корректное место в поисковой выдаче. Тут вряд ли можно рассчитывать на первое место. Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека фронтендера» Интересно, перейти к каналу Раздел для тех, кто где-то видел или читал какие-то новости на этому тему. Да, вы в целом правы. Но есть огромное но… Поисковые системы это делают крайне неохотно, в том же Google можно ждать индексации сайта неделями, а то и месяцами. SEO-специалисты, как представители бизнеса, просто затюкают бедного программиста разными вопросами. Ведь им нужно быстро, здесь и сейчас. Дьявол кроется в деталях. Чтобы поисковой системе проиндексировать JavaScript-сайт, ей нужны большие мощности. Сначала нужно сделать запрос к сайту, понять, что тут нет контента и это JavaScript-сайт. После этого надо выкачать сайт, куда-то сложить, запустить исполнительную среду JavaScript и только потом считать контент. А теперь представьте классический сайт на PHP, C#, Python. Сделал запрос – получил контент. Все. С помощью Node.js. Не любите Node.js? Извините, других способов у нас для вас нет. Хотя внутри Node.js за исполнение JavaScript отвечает движок V8, можете его скачать с GitHub и засунуть в свой проект. Только учтите: V8 написан на С++. Как вы свяжите между собой кучу инструментов, мы представляем лишь примерно, но точно можем сказать что вам будет очень «весело». Технически, возможно добавить SSR и в Laravel + Vue проект (помним, Laravel это PHP), но это будет выглядеть как-то так. Сомнительный монолит получится. Да и вам все равно потребуется Node.js, как ни крути. Так что, будем работать с Node.js. Перед тем как начать, мы с вами сейчас создадим простое двухстраничное Vue-приложение. Это нужно лишь для того, чтобы вы поняли принцип рендеринга контента. Можете взять свое, но лучше давайте начнем вместе с простой базы, так вы сделаете меньше ошибок и будет понятно, что за что отвечает. А иначе, вопросов будет просто миллион. Примечание Автор использует Node.js 16.15.0. Инициализируем Vue приложение с помощью команды: Далее нам зададут некоторые вопросы, отвечаем на них: Теперь переходим в папку с проектом, устанавливаем пакеты и запускаем приложение в режиме разработки (команды вводите по порядку): У нас с вами появился такой проект, который нужен: Мы имеем Если мы сейчас нажмем в браузере «Посмотреть код страницы», то не увидим никакого текста в нашем базовом приложении: Хотя в компонентах текст есть: Компонент TheWelcome.vue, он вызывается внутри HomeView.vue. Для начала идем в package.json и добавляем туда строчку: Должно получится как-то так: Это для того чтобы в Node.js файлах использовать конструкцию Теперь надо создать сервер. Пусть будет Express: В папке src создайте файл server.js Давайте обсудим, что же здесь написано. Это очень важно. На 22 строке функция По идее, если через Node.js вызвать файл: в котором будем Вообще, вся магия происходит с 40 по 51 строчку. В первую очередь, с помощью функции И между Должно выйти так: index.html В Кого-то может смутить пустая переменная За сам рендер JavaScript отвечает функция В папке main-server.js По документации Vite, функция Сама функция Внутри К сожалению, это еще не все (хотя уже финишная прямая). Даже если мы сейчас попытаемся запустить сервер, то ничего хорошего не произойдет. Нам надо еще перенастроить наш Смотрите на 5 строку, раздел Поэтому мы должны поменять router/index.js Теперь можете запустить наше творение командой: Во-первых, сервер запустился под адресом Можете проскролить вправо и там будет еще контент из компонентов Первый заход на сайт будет отдавать контент компонента, на который мы попали. На клиентской стороне реактивность сохраняется за счет гидрации. Во-вторых, если нажимать F5, то как-то некрасиво встают стили. Мы это исправим за счет манифеста. В режиме разработки мы поработаем и так, а для production сделаем все чуть красивее. В-третьих, если вы меняете файлы, Vite подхватывает изменения и делает Hot Reload. Ну, кроме файла P.S: Это по желанию. Замените содержимое server.js Тут не так много правок, как может показаться. В самом вверху мы добавили переменную С помощью этой переменной мы будем понимать в каком режиме мы сейчас функционируем. По-хорошему, для production нужно заранее собрать наше приложение через Vite. После сборки оно помещается в папку Теперь давайте соберем наше приложение в боевом режиме. Давайте внесем корректировки в Добавить в package.json Должно получится так: package.json Теперь вместо: Можно использовать: А для production есть команда: Но только перед тем как ее запустить, выполните команду: Ибо без сборки нечего «обслуживать». В целом на этом все, можем поздравить вас с реализацией своего SSR без всяких фреймворков. Вообще, если лень проходить по этому туториалу и все кажется слишком сложным, то можно рассмотреть готовые инструменты для внедрения SSR. Например, в рамках Vue 3 существуют такие инструменты как Nuxt и Quasar. Данные инструменты позволяют не создавать всякие Express сервера, а просто работать с Vue, как привыкли. Минусы такого подхода лишь в том, что не вы сами настраиваете Express сервер, а разработчик фреймворка. Поэтому, вы, как программист, придерживаетесь чужой логики (но это не всегда плохо). *** Надеемся, этот душный туториал не прошел зря, и вы научились магии SSR в JavaScript. Вот ссылка готового проекта на GitHub. Если будут вопросы, пишите в комментариях, автор постарается помочь. Максим Колмогоров
Что такое SSR
Зачем нужен SSR и что такое SEO
Автор не прав, поисковые системы индексируют JavaScript сайты…
Как рендерить JavaScript на сервере
Добавляем SSR во Vue приложение
Создаем Vue приложение
npm init vue@latest
cd “ваше название” npm install npm run dev
App.vue
как шаблон и несколько страниц добавленных через router/index.js
: HomeView.vue
и AboutView.vue
.Создаем сервер для рендеринга JavaScript
"type": "module",
import
.
npm install express
server.js
со следующим содержимым:
// Node.js utility import path from 'path' import fs from 'fs' import { fileURLToPath } from 'url' // Vite import { createServer } from 'vite' // Express import express from 'express' // Helpers const __dirname = path.dirname(fileURLToPath(import.meta.url)) const resolve = (p) => path.resolve(__dirname, p) const getIndexHTML = async () => { const indexHTML = resolve('../index.html') const html = await fs.promises.readFile(indexHTML, 'utf-8') return html } async function start() { const manifest = null const ssrServer = resolve('./main-server.js') const app = express() const router = express.Router() const vite = await createServer({ server: { middlewareMode: true }, appType: 'custom' }) app.use(vite.middlewares) // Ловим все запросы, а вообще можно продублировать тут // логику из src/router.js router.get('/*', async (req, res, next) => { try { const url = req.url let template = await getIndexHTML() template = await vite.transformIndexHtml(url, template) let render = (await vite.ssrLoadModule(ssrServer)).render const [appHtml, preloadLinks] = await render(url, manifest) const html = template .replace(`<!--preload-links-->`, preloadLinks) .replace('<!--app-html-->', appHtml) res.status(200).set({ 'Content-Type': 'text/html' }).end(html) } catch (e) { vite.ssrFixStacktrace(e) next(e) } }) // Routes app.use('/', router) app.listen(3000, () => { console.log('Сервер запущен') }) } start()
start()
запускает Express-сервер, предварительно запуская внутри себя Vite-сервер на 29 строке. Сам Vite-сервер – это некое дополнительное приложение, которое умеет компилировать Vue-файлы.
node index.js
import
файла с расширением .vue
, то произойдет ошибка, так как нам нужно заранее предсобрать наше приложение особым способом через Vite (что мы и делаем).getIndexHTML()
, которую мы чуть выше реализовали. Мы берем наш index.html
из корня проекта, для того чтобы через регулярные выражения в нужное место установить отрендеренный контент. Да, нам нужно немного модернизировать index.html
. Для этого вставьте под тег title
конструкцию:
<!--preload-links-->
<div id="app"></div>
конструкцию:
<!--app-html-->
preload-links
полетят стили и еще всякие полезные ссылки, собираемые Vite. А в app-html
, собранное с помощью SSR, – приложение.manifest
. Все так и должно быть. Это не конечный вид файла, и чтобы вас не запутать, мы даем информацию постепенно.vite.ssrLoadModule()
. В нее мы передаем путь до нашей специальной версии приложения – entry point
для SSR. Да, мы сейчас говорим про файл main-server.js
, которого у вас еще нету. src
создайте еще один файл main-server.js
с таким содержимым:
// Node.js import { basename } from 'node:path' // Vue SSR import { createSSRApp } from 'vue' import { renderToString } from 'vue/server-renderer' // App import App from './App.vue' import router from './router/index.js' export async function render(url, manifest = null) { const app = createSSRApp(App) app.use(router) await router.push(url) await router.isReady() // ctx - context. Плагин @vitejs/plugin-vue // https://vitejs.dev/guide/ssr.html#generating-preload-directives const ctx = { modules: [] } const html = await renderToString(app) let preloadLinks = '' if (manifest) { renderPreloadLinks(ctx.modules, manifest) } return [html, preloadLinks] } function renderPreloadLinks(modules, manifest) { let links = '' const seen = new Set() modules.forEach((id) => { const files = manifest[id] if (files) { files.forEach((file) => { if (!seen.has(file)) { seen.add(file) const filename = basename(file) if (manifest[filename]) { for (const depFile of manifest[filename]) { links += renderPreloadLink(depFile) seen.add(depFile) } } links += renderPreloadLink(file) } }) } }) return links } function renderPreloadLink(file) { if (file.endsWith('.js')) { return `<link rel="modulepreload" crossorigin href="${file}">` } else if (file.endsWith('.css')) { return `<link rel="stylesheet" href="${file}">` } else if (file.endsWith('.woff')) { return ` <link rel="preload" href="${file}" as="font" type="font/woff" crossorigin>` } else if (file.endsWith('.woff2')) { return ` <link rel="preload" href="${file}" as="font" type="font/woff2" crossorigin>` } else if (file.endsWith('.gif')) { return ` <link rel="preload" href="${file}" as="image" type="image/gif">` } else if (file.endsWith('.jpg') || file.endsWith('.jpeg')) { return ` <link rel="preload" href="${file}" as="image" type="image/jpeg">` } else if (file.endsWith('.png')) { return ` <link rel="preload" href="${file}" as="image" type="image/png">` } else { return '' } }
vite.ssrLoadModule()
возвращает другие экспортируемые функции из передаваемого файла. Поэтому внутри main-server.js
мы объявляем функцию render()
и в ней напишем классический SSR сервер из документации Vue.js.render()
будет вызываться из файла server.js
.main-server.js
у многих могут вызывать вопросы две функции: renderPreloadLinks()
и renderPreloadLink()
. Хотя они и выглядят страшно, но на самом деле выполняют простую роль: они помогают нам и подготавливают ссылки на .css
файлы. Все ссылки на чанки стилей будут находиться в манифесте. Мы его просто тут читаем. Понимаем, вопросов много, но пока у нас нет манифеста, мы его сделаем чуть позже, и все станет сразу в разы понятней. router/index.js
. Для этого откройте этот файл.history
. Тут используется функция createWebHistory()
. Под капотом у этой функции есть использование глобальных переменных document
и window
. Только вот беда: когда мы будем собирать наше приложение через SSR-мод с помощью Node.js, мы не сможем обратиться к этим переменным. Просто потому, что в Node.js нет их. Вместо window
в Node.js есть global
и process
, но там совсем другое содержимое. А document
вообще является DOM API, которого тем более там нет… это же не браузер.createWebHistory()
на сreateMemoryHistory()
, но только для SSR, дабы в обычном режиме приложение не сломалось. Поэтому модернизируйте файл router/index.js
таким способом:
import { createRouter, createWebHistory, createMemoryHistory } from 'vue-router' import HomeView from '../views/HomeView.vue' const baseUrl = import.meta.env.BASE_URL const history = import.meta.env.SSR ? createMemoryHistory(baseUrl) : createWebHistory(baseUrl) const router = createRouter({ history, routes: [ { path: '/', name: 'home', component: HomeView }, { path: '/about', name: 'about', component: () => import('../views/AboutView.vue') } ] }) export default router
node ./src/server.js
localhost:3000
, и если вы перейдете на него и откроете исходный код, то увидите результат своего труда:server.js
… тут, если хотите, то же самое – надо поставить nodemon и запускать server.js
уже через него. Как-то так:
npm install -g nodemon nodemon ./src/server.js
Финал: сборка для production
server.js
на новое:
// Node.js utility import path from 'path' import fs from 'fs' import { fileURLToPath } from 'url' // Vite import { createServer } from 'vite' // Express import express from 'express' // eslint-disable-next-line no-undef const isProd = process.env.NODE_ENV === 'production' // Helpers const __dirname = path.dirname(fileURLToPath(import.meta.url)) const resolve = (p) => path.resolve(__dirname, p) const getIndexHTML = async () => { const indexHTML = isProd ? resolve('../dist/client/index.html') : resolve('../index.html') const html = await fs.promises.readFile(indexHTML, 'utf-8') return html } async function start() { const manifest = isProd ? JSON.parse(fs.readFileSync(resolve('../dist/client/ssr-manifest.json'), 'utf-8')) : null const ssrServer = isProd ? resolve('../dist/server/main-server.js') : resolve('./main-server.js') const app = express() const router = express.Router() const vite = await createServer({ // eslint-disable-next-line no-undef root: process.cwd(), server: { middlewareMode: true }, appType: 'custom' }) if (isProd) { app.use(express.static('dist/client', { index: false })) } app.use(vite.middlewares) // Ловим все запросы, а вообще можно продублировать тут // логику из src/router.js router.get('/*', async (req, res, next) => { try { const url = req.url let template = await getIndexHTML() template = await vite.transformIndexHtml(url, template) let render = (await vite.ssrLoadModule(ssrServer)).render const [appHtml, preloadLinks] = await render(url, manifest) const html = template .replace(`<!--preload-links-->`, preloadLinks) .replace('<!--app-html-->', appHtml) res.status(200).set({ 'Content-Type': 'text/html' }).end(html) } catch (e) { vite.ssrFixStacktrace(e) next(e) } }) // Routes app.use('/', router) app.listen(3000, () => { console.log('Сервер запущен') }) } start()
isProd
.dist
и мы будем просто тянуть файлы оттуда. Посмотрите на строки 20, 28 и 32. Тут как раз у нас еще и манифест появился.package.json
:
"dev": "node ./src/server.js", "serve": "NODE_ENV=production node ./src/server.js", "build": "npm run build:client && npm run build:server", "build:client": "vite build --ssrManifest --outDir dist/client", "build:server": "vite build --ssr src/main-server.js --outDir dist/server",
node ./src/server.js
npm run dev
npm run serve
npm run build
Бонус: альтернативные способы внедрения SSR
Итог
Материалы по теме
- 1 views
- 0 Comment