1. Почему рендеринг на стороне сервера
С итеративной зрелостью стека фронтенд-технологий и инструментальной цепочки тенденция фронтенд-инжиниринга и модульности становится все более и более очевидной. такие как React, Vue и Angular.Одностраничное приложение (SPA), созданное с помощью этого типа фреймворка, имеет преимущества хорошей производительности рендеринга и высокой ремонтопригодности. Но это также приносит два недостатка:
1.首屏加载时间过长
2.不利于SEO
В отличие от традиционных веб-проектов, которые напрямую получают HTML-код, отображаемый на стороне сервера, одностраничные приложения используют JavaScript для генерации HTML-кода на стороне клиента для отображения содержимого.Пользователям необходимо дождаться завершения синтаксического анализа и выполнения JS, прежде чем они смогут увидеть страницы, что увеличивает время загрузки первого экрана, влияет на взаимодействие с пользователем. Кроме того, когда поисковые системы сканируют HTML-файлы веб-сайта, HTML-код одностраничного приложения не имеет содержимого, что влияет на ранжирование в поиске. Чтобы устранить эти два дефекта, индустрия использует традиционное прямое HTML-решение на стороне сервера и предлагает выполнять код интерфейсной среды (React/Vue/Angular) на стороне сервера для создания HTML, а затем вернуть визуализированный HTML-код клиенту, чтобы реализовать внешний интерфейс CSR для серверного рендеринга фреймворка.
В этой статье используется простая демонстрация для объяснения основных принципов рендеринга на стороне сервера React (SSR).После прочтения этой статьи читатели смогут освоить:
рендеринг на стороне сервера
基本概念和原理
в проекте ССР
渲染组件
в проекте ССР
使用路由
в проекте ССР
使用redux
2. Отрисовка компонентов в проекте SSR
1. Используйте узел для рендеринга на стороне сервера
Мы используем экспресс для запуска сервера Node для базового рендеринга на стороне сервера. Сначала установите проект узла инициализации и установите экспресс
npm init
npm install express –save
Создайте файл app.js в корневом каталоге, прослушивайте запросы через порт 3000 и возвращайте некоторый HTML-код при запросе корневого каталога.
const express = require('express')
const app = express()
app.get('/', (req,res) => res.send(`
<html>
<head>
<title>ssr demo</title>
</head>
<body>
Hello world
</body>
</html>
`))
app.listen(3000, () => console.log('Exampleapp listening on port 3000!'))
Перейдите в корневой каталог проекта и запустите node app.js, чтобы запустить проект.
Щелкните правой кнопкой мыши, чтобы просмотреть исходный код веб-страницы. Это HTML-код, непосредственно возвращаемый сервером. Мы завершили базовый рендеринг на стороне сервера. Если мы откроем проект реакции и просмотрим исходный код веб-страницы, мы обнаружим, что в коде нет HTML, соответствующего содержимому страницы, потому что одностраничное приложение SPA, созданное с помощью реакции, динамически генерирует HTML, выполняя JS на стороне клиента Исходный HTML В файле нет соответствующего содержимого.
2. Напишите код React на стороне сервера
Мы запустили сервер Node, следующим шагом нам нужно написать код React на сервере, мы создаем фрагмент кода React, подобный этому, и ссылаемся на него в app.js.
import React from 'react'
const Home = () =>{
return <div>home</div>
}
export default Home
Однако этот код не будет работать успешно, потому что запуск кода React непосредственно на стороне сервера не будет работать по нескольким причинам:
Node не распознает импорт и экспорт, которые относятся к синтаксису esModule, и Node следует спецификации common.js.
Node не распознает синтаксис JSX, нам нужно использовать веб-пакет для упаковки и преобразования проекта, чтобы сделать его синтаксис, который может распознавать Node.
Для того, чтобы код заработал, нам нужно установить вебпак и настроить его
npm install webpack webpack-cli –save
Установите веб-пакет и веб-пакет-клиСоздайте файл конфигурации webpack.server.js в корневом каталоге и настройте его
const path = require('path') //node的path模块
const nodeExternals = require('webpack-node-externals')
module.exports = {
target:'node',
mode:'development', //开发模式
entry:'./app.js', //入口
output: { //打包出口
filename:'bundle.js', //打包后的文件名
path:path.resolve(__dirname,'build') //存放到根目录的build文件夹
},
externals: [nodeExternals()], //保持node中require的引用方式
module: {
rules: [{ //打包规则
test: /\.js?$/, //对所有js文件进行打包
loader:'babel-loader', //使用babel-loader进行打包
exclude: /node_modules/,//不打包node_modules中的js文件
options: {
presets: ['react','stage-0',['env', {
//loader时额外的打包规则,对react,JSX,ES6进行转换
targets: {
browsers: ['last 2versions'] //对主流浏览器最近两个版本进行兼容
}
}]]
}
}]
}
}
3. Установите соответствующий бабел
npm install babel-loader babel-core –-save
npm install babel-preset-react –-save
npm install babel-preset-stage-0 –-save
npm install babel-preset-env –-save
npm install webpack-node-externals –-save
4. Бегиwebpack --config webpack.server.js
5. Запустите упакованный файлnode ./build/bundle.js
Что касается использования веб-пакета, читатели, которые относительно незнакомы, могут обратиться к нашему официальному аккаунту: «Введение в веб-пакет».
Tickets.WeChat.QQ.com/Yes/3n KL лет…
3. Используйте renderToString для рендеринга компонентов
После того, как JSX и ES6 упакованы и преобразованы вебпаком, мы все еще не можем правильно запустить наш код.Ранее при рендеринге DOM на стороне клиента мы использовали следующий код, но этот код нельзя запустить на стороне сервера.
import Home from './src/containers/Home'
import ReactDom from 'react-dom'
ReactDom.render(<Home/>, document.getElementById('root')) //服务端没有DOM
Нам нужно использовать метод renderToString, предоставленный react-dom, чтобы отобразить компонент в строку и вставить его в HTML, возвращаемый клиенту.
import express from 'express'
import React from 'react'//引入React以支持JSX的语法
import { renderToString } from 'react-dom/server'//引入renderToString方法
import Home from'./src/containers/Home'
const app= express()
const content = renderToString(<Home/>)
app.get('/',(req,res) => res.send(`
<html>
<head>
<title>ssr demo</title>
</head>
<body>
${content}
</body>
</html>
`))
app.listen(3001, () => console.log('Exampleapp listening on port 3001!'))
Перепакуйте и перезапустите сервер, и мы сможем увидеть компоненты, отображаемые сервером на странице.
4. Webpack автоматически упаковывает и автоматически перезагружает сервер
Напишите здесь, у нас есть небольшая оптимизация для предыдущего узла и WebPack. До этого нам нужно повторно выполнить WebPack-Config webpack.server.js и узел ./build/bundle каждый раз, когда мы изменили проект. ,Js для перезапуска Проект, теперь мы делаем некоторые изменения в сценарии в файле Package.json, чтобы сервер мог перезагрузить и пакет
Добавьте --watch после webpack --config webpack.server.js, чтобы реализовать автоматический мониторинг и упаковку webpack.Когда файлы, которые необходимо упаковать, изменяются, webpack автоматически переупаковывает
Установите nodemon, nodemon — это аббревиатура nodemonitor, nodemon может помочь нам отслеживать изменения файлов и автоматически перезапускать сервер, нам нужно запустить
npm install nodemon –g
Установите nodemon и добавьте эти два предложения в элемент конфигурации скрипта package.json:
"scripts":{
"dev": "nodemon--watch build --exec node \"./build/bundle.js\"",
"build": "webpack--config webpack.server.js --watch"
},
После выполнения двух вышеуказанных конфигураций мы открываем два терминала и запускаем npm run dev и npm run build соответственно, чтобы завершить автоматическую упаковку проекта и перезапустить сервер.
3. Установите npm-run-all, чтобы еще больше упростить процесс:
бегатьnpm install npm-run-all –g
Установите npm-run-all и настройте package.json.
"scripts": {
"dev": "npm-run-all--parallel dev:**",
"dev:start": "nodemon--watch build --exec node \"./build/bundle.js\"",
"dev:build": "webpack--config webpack.server.js --watch"
},
Мы добавляем префикс dev к исходному запуску и сборке, указывая, что это команда, используемая средой разработки.Нам не нужно выполнять эти две команды для мониторинга онлайн-среды. После завершения настройки запустите npm run dev, и мы завершили автоматическую упаковку и перезагрузку сервера.Каждый раз, когда мы вносим изменения в код, нам нужно только обновить страницу, чтобы увидеть эффект, и нам не нужно вручную переупаковать и перезапустить сервер как раньше.
5. Понятие изоморфизма
В приведенном выше процессе мы отобразили компонент на странице, давайте привяжем событие клика к компоненту.
import React from 'react'
const Home= () =>{
return (
<div>
<div>home</div>
<button onClick={()=>{alert('click')}}>click</button>
</div>)
}
export default Home
Запустив код и обновив страницу, мы обнаружим, что соответствующее событие щелчка не выполняется. Это связано с тем, что renderToString только отображает содержимое компонента без привязки события. Чтобы привязать событие к компоненту на странице,Нам нужно выполнить код React один раз на стороне сервера и еще раз на стороне клиента.Такой способ совместного использования набора кода на стороне сервера и на стороне клиента называется изоморфизмом.
Мы вводим код React, выполняемый клиентом для страницы через тег
import express from 'express'
import React from 'react'//引入React以支持JSX的语法
import { renderToString } from'react-dom/server'//引入renderToString方法
import Home from './src/containers/Home'
const app = express()
app.use(express.static('public'));
//使用express提供的static中间件,中间件会将所有静态文件的路由指向public文件夹
const content = renderToString(<Home/>)
app.get('/',(req,res)=>res.send(`
<html>
<head>
<title>ssr demo</title>
</head>
<body>
${content}
<script src="/index.js"></script>
</body>
</html>
`))
app.listen(3001, () =>console.log('Example app listening on port 3001!'))
Затем нам нужно написать наш index.js (код React на стороне клиента), мы пытаемся создать index.js в общей папке и написать код React, но эти коды React не будут работать, потому что нам также нужно использовать веб-пакет для клиентская сторона реагирует на упаковку.
6. Выполнение кода React на стороне клиента
Давайте сначала настроим структуру каталогов, создадим новую папку клиента в папке src для хранения клиентского кода, создадим новый файл webpack.client.js в корневом каталоге в качестве файла конфигурации веб-пакета для кода React на стороне клиента, а общедоступный папка будет использоваться для хранения упакованного клиентского кода webpack; создайте новую папку сервера для хранения серверного кода, переместите исходное содержимое app.js в index.js в папке сервера и измените запись webpack.server .js. Создайте новую папку-контейнер для хранения кода React.
Давайте начнем писать элементы конфигурации веб-пакета на стороне клиента и напишем следующий код в webpack.client.js:
const path = require('path') //node的path模块
module.exports = {
mode:'development', //开发模式
entry:'./src/client/index.js', //入口
output: { //打包出口
filename:'index.js', //打包后的文件名
path:path.resolve(__dirname,'public') //存放到根目录的build文件夹
},
module: {
rules: [{ //打包规则
test: /\.js?$/, //对所有js文件进行打包
loader:'babel-loader', //使用babel-loader进行打包
exclude: /node_modules/, //不打包node_modules中的js文件
options: {
presets: ['react','stage-0',['env', {
//loader时额外的打包规则,这里对react,JSX进行转换
targets: {
browsers: ['last 2versions'] //对主流浏览器最近两个版本进行兼容
}
}]]
}
}]
}
}
Заодно модифицируем секцию script в package.json
"scripts": {
"dev": "npm-run-all--parallel dev:**",
"dev:start": "nodemon--watch build --exec node \"./build/bundle.js\"",
"dev:build:server": "webpack--config webpack.server.js --watch",
"dev:build:client": "webpack--config webpack.client.js --watch"
},
Перезапустите npm run dev, мы завершили автоматическую упаковку кода сервера и клиента, обновите страницу, и вы увидите, что событие было успешно привязано
Здесь сообщается предупреждение. Причина в том, что когда рендеринг на стороне сервера выполняется в React 16, метод render() должен быть заменен методом hydr(). Хотя вы все еще можете использовать render() для рендеринга HTML в React16, дабы исключить ошибки, лучше всего заменить на hydr()
Подробнее о гидратах см. в этом обсуждении: https://www.wengbi.com/thread_50584_1.html.
7. Оптимизация и доработка Webpack
Мы написали в проекте два конфигурационных файла вебпака, на самом деле в этих двух конфигурационных файлах много общих частей, которые мы должны извлечь из общих частей, чтобы уменьшить избыточность кода. Мы устанавливаем модуль webpack-merge, чтобы помочь нам извлечь общие элементы конфигурации webpack.
Создайте новый файл webpack.base.js, переместите сюда общие элементы конфигурации из webpack.server.js и webpack.client.js и экспортируйте их через module.exports.
module.exports = {
module: {
rules: [{
test: /\.js?$/,
loader:'babel-loader',
exclude: /node_modules/,
options: {
presets: ['react','stage-0',['env', {
targets: {
browsers: ['last 2versions']
}
}]]
}
}]
}
}
2. В webpack.server.js и webpack.client.js объедините и экспортируйте общедоступные элементы конфигурации и текущие элементы конфигурации с помощью метода слияния.
//webpack.client.js配置
const path = require('path')
const merge = require('webpack-merge')
const config = require('./webpack.base.js')
const clientConfig = {
mode:'development',
entry:'./src/client/index.js',
output: {
filename:'index.js',
path:path.resolve(__dirname,'public')
},
}
module.exports = merge(config,clientConfig)
//webpack.server.js配置
const path = require('path')
const nodeExternals = require('webpack-node-externals')
const merge = require('webpack-merge')
const config = require('./webpack.base.js')
const serverConfig = {
target:'node',
mode:'development',
entry:'./app.js',
output: {
filename:'bundle.js',
path:path.resolve(__dirname,'build')
},
externals: [nodeExternals()],
}
module.exports = merge(config,serverConfig)
Резюме абзаца:
В этом разделе рассказывается, как выполнять базовый рендеринг компонентов и привязку событий на стороне сервера. Благодаря объяснению в этом разделе читатели должны понять основную идею React SSR - изоморфизм. Так называемый изоморфизм представляет собой набор Код React на стороне сервера выполняет операцию создания HTML, а клиентская сторона выполняет код, чтобы взять на себя работу страницы, так что страница обладает преимуществами SSR и CSR.
Обобщите шаги компонентов рендеринга на стороне сервера:
Создать проект узла
Напишите код React на стороне сервера и используйте webpack для упаковки и компиляции, а также используйте метод renderToString для рендеринга компонентов в HTML.
Напишите код React, который должен выполнить клиент, и используйте webpack для упаковки и компиляции, импортируйте страницу через тег сценария и возьмите на себя управление страницей.
3. Используйте маршрутизацию в проектах SSR
1. Используйте маршрутизацию на стороне клиента
Аналогично, при использовании маршрутизации нам необходимо настроить маршрутизацию на стороне сервера и на стороне клиента, причина будет объяснена ниже. Сначала мы настраиваем маршрутизацию клиента и устанавливаем react-router.
npm install react-router-dom —save
Затем мы создаем Router.js в папке src для хранения записей маршрутизации.
import React from 'react' //引入React以支持JSX
import { Route } from 'react-router-dom' //引入路由
import Home from './containers/Home' //引入Home组件
export default (
<div>
<Route path="/" exact component={Home}></Route>
</div>
)
Измените index.js в папке клиента, используйте BrowserRouter и импортируйте записи маршрутизации.
import React from 'react'
import ReactDom from 'react-dom'
import { BrowserRouter } from'react-router-dom'
import Router from'../Routers'
const App= () => {
return (
<BrowserRouter>
{Router}
</BrowserRouter>
)
}
ReactDom.hydrate(<App/>, document.getElementById('root'))
Запустите код, обновите страницу, и в консоли вы увидите ошибку:
Это связано с тем, что когда мы используем маршрутизацию в Router.js, внешний уровень должен установить div, но внешний HTML-слой сервера не имеет этого div, что приводит к различному содержимому страницы, отображаемой клиентом, и отображаемой страницы. сервером, поэтому сообщается об ошибке, поэтому нам нужно снова настроить маршрутизацию на стороне сервера, чтобы сделать контент, отображаемый на стороне сервера и на стороне клиента, согласованным (конечно, если вы добавите еще один div непосредственно в HTML возвращаемый сервером, вы можете временно решить эту ошибку, но если вы не пропишете маршрутизацию на стороне сервера, на следующих шагах возникнут другие ошибки)
2. Используйте маршрутизацию на стороне сервера
Измените index.js в папке сервера и введите здесь маршрутизацию на стороне сервера. На стороне сервера нам нужно использовать StaticRouter для замены BrowserRouter. StaticRouter — это компонент маршрутизации, предоставляемый React-Router для рендеринга на стороне сервера. Поскольку StaticRouter не может воспринимать URL-адрес текущей страницы страницы, как BrowserRouter, нам нужно передать in location={ URL текущей страницы}, кроме того, при использовании StaticRouter необходимо передавать параметр контекста, который используется для передачи параметра при отрисовке на стороне сервера.
import express from 'express'
import React from 'react'//引入React以支持JSX的语法
import { renderToString } from 'react-dom/server'//引入renderToString方法
import { StaticRouter } from 'react-router-dom'
import Router from '../Routers'
const app = express()
app.use(express.static('public'));
//使用express提供的static中间件,中间件会将所有静态文件的路由指向public文件夹
app.get('/',(req,res)=>{
const content = renderToString((
//在服务端我们需要使用StaticRouter来替代BrowserRouter
//传入当前path
//context为必填参数,用于服务端渲染参数传递
<StaticRouter location={req.path} context={{}}>
{Router}
</StaticRouter>
))
res.send(`
<html>
<head>
<title>ssr demo</title>
</head>
<body>
<div id="root">${content}</div>
<script src="/index.js"></script>
</body>
</html>
`)
})
app.listen(3001, () => console.log('Exampleapp listening on port 3001!'))
Затем снова откройте нашу страницу, ошибки не будет.
3. Реализовать многостраничный переход по ссылке
Создаем компонент входа
и добавьте маршрут для компонента входа в Routers.js
import React from'react' //引入React以支持JSX
import { Route } from'react-router-dom' //引入路由
import Home from'./containers/Home' //引入Home组件
import Login from'./containers/Login' //引入Login组件
exportdefault (
<div>
<Route path="/" exact component={Home}></Route>
<Route path="/login" exact component={Login}></Route>
</div>
)
Кроме того, нам нужно изменить маршрут в src/server/index.js с соответствия '/' на '*', иначе при посещении http://localhost:3001/login будет выдана ошибка 404, поскольку маршрут не может быть сопоставлено.
import express from 'express'
import React from 'react'//引入React以支持JSX的语法
import { renderToString } from 'react-dom/server'//引入renderToString方法
import { StaticRouter } from 'react-router-dom'
import Router from '../Routers'
const app= express()
app.use(express.static('public'));
//使用express提供的static中间件,中间件会将所有静态文件的路由指向public文件夹
app.get('*',(req,res)=>{
const content = renderToString((
<StaticRouter location={req.path} context={{}}>
{Router}
</StaticRouter>
))
res.send(`
<html>
<head>
<title>ssr demo</title>
</head>
<body>
<div id="root">${content}</div>
<script src="/index.js"></script>
</body>
</html>
`)
})
app.listen(3001, () =>console.log('Exampleapp listening on port 3001!'))
Мы можем немного извлечь часть приведенного выше кода, генерирующую HTML, и создать новый файл utils.js в папке сервера для хранения кода, генерирующего HTML.
import React from 'react'//引入React以支持JSX的语法
import { renderToString } from 'react-dom/server'//引入renderToString方法
import { StaticRouter } from 'react-router-dom'
import Router from '../Routers'
export const render = (req) => {
const content = renderToString((
<StaticRouter location={req.path} context={{}}>
{Router}
</StaticRouter>
));
return`
<html>
<head>
<title>ssr demo</title>
</head>
<body>
<div id="root">${content}</div>
<script src="/index.js"></script>
</body>
</html>
`
}
Исходный server/index.js можно изменить на следующий вид
import express from 'express'
import { render } from './utils'
const app = express()
app.use(express.static('public'));
//使用express提供的static中间件,中间件会将所有静态文件的路由指向public文件夹
app.get('*',(req,res)=>{
res.send(render(req))
})
app.listen(3001, () => console.log('Exampleapp listening on port 3001!'))
После выполнения вышеуказанных шагов мы используем тег Link для реализации функции навигации.Нам нужно создать компонент панели навигации и сослаться на этот компонент панели навигации дома и войти в систему.Для компонентов многократного использования мы помещаем его в папку src Создать компонент папку для хранения общедоступных компонентов и создайте header.js под компонентом в качестве нашего компонента панели навигации.
import React from 'react'
import { Link } from 'react-router-dom'
const Header = () => {
return (
<div>
<Link to='/'>Home </Link>
<Link to='/login'>Login</Link>
</div>
)
}
export default Header
Затем мы обращаемся к компоненту панели навигации в компоненте Home и компоненте Login соответственно, сохраняем код, обновляем страницу, и теперь мы можем выполнять переходы маршрутизации на странице.
Стоит отметить, что браузер запрашивает файл подкачки только при первом входе на страницу, и тогда операция переключения маршрутов не будет повторно запрашивать страницу, потому что переход маршрута страницы уже является переходом маршрута React на стороне клиента. .
Резюме абзаца:
В этом разделе рассказывается, как использовать маршрутизацию в проекте SSR. Нам необходимо настроить маршрутизацию на сервере и клиенте, чтобы добиться нормального перехода по страницам. По причине настройки двух маршрутов автор понимает, что
1. Маршрутизация на стороне сервера заключается в поиске соответствующего файла веб-страницы при первом входе на страницу.
2. Маршрутизация на стороне клиента позволяет маршрутизации React взять на себя управление страницей для достижения перехода без обновления.
3. Если серверная сторона не пропишет маршрут, это приведет к несогласованности содержимого страницы и появлению ошибки
Кроме того, мы должны отметить, что только при первом входе на страницу браузер будет использовать маршрутизацию на стороне сервера для запроса файла веб-страницы. маршрутизация для достижения перехода без обновления.
В-четвертых, используйте избыточность в проектах SSR.
В этом разделе объясняется, как использовать избыточность в проекте SSR, что является трудным моментом в проекте.Точно так же нам нужно выполнить код избыточности на стороне клиента и на стороне сервера один раз, и причина будет объяснена ниже.
1. Установите Redux и промежуточное ПО Redux
npm install redux –save
npm install react-redux–save
npm install redux-thunk–save
2. Используйте избыточность на стороне клиента
Далее мы выполним ряд рутинных операций, а использование redux, redux-thunk и react-redux здесь подробно обсуждаться не будет. Мы используем избыточность в клиентском коде (/client/index.js) для создания хранилища и редуктора, настройки переходника промежуточного программного обеспечения и передачи хранилища компоненту.
import React from 'react'
import ReactDom from 'react-dom'
import { BrowserRouter } from 'react-router-dom'
import Routers from '../Routers'
import { createStore,applyMiddleware } from 'react'
import { Provider } from 'react-redux'
import thunk from 'redux-thunk'
const reducer = (state,action) => {
return state
}
const store = createStore(reducer,applyMiddleware(thunk))
const App = () => {
return (
<Provider store={store}>
<BrowserRouter>
{Routers}
</BrowserRouter>
</Provider>
)
}
ReactDom.hydrate(<App/>,document.getElementById('root'))
В дочернем компоненте (Home) мы используем метод подключения в react-redux для подключения к хранилищу.
import React from 'react'
import Header from '../../component/header'
import { connect } from 'react-redux'
const Home= () =>{
return (
<div>
<Header/>
<div>{props.name}</div>
<button onClick={()=>{alert('click')}}>click</button>
</div>)
}
const mapStateToProps = state => ({
name:state.name
})
export default connect(mapStateToProps,null)(Home)
После написания редукционного кода клиента мы можем обновить страницу, чтобы увидеть эффект.
Вы можете видеть, что на странице будет сообщено об ошибке, потому что при посещении http://localhost:3001/ вы сначала вводите index.js в папке сервера, а index.js отображает компонент Home. Когда компонент Home вызывает хранилище. Когда данные в коде на стороне сервера (server/until. redux передаются компоненту
3. Использование Redux на стороне сервера
Точно так же мы также вводим Redux в серверный код.
import React from 'react'//引入React以支持JSX的语法
import { renderToString } from 'react-dom/server'//引入renderToString方法
import { StaticRouter } from 'react-router-dom'
import Router from '../Routers'
import { createStore,applyMiddleware } from 'redux'
import { Provider } from 'react-redux'
import thunk from 'redux-thunk'
export const render = (req) => {
const reducer = (state = { name:'CJW' },action) => {
return state
}
const store= createStore(reducer,applyMiddleware(thunk))
const content = renderToString((
<Provider store={store}>
<StaticRouter location={req.path} context={{}}>
{Router}
</StaticRouter>
</Provider>
));
return`
<html>
<head>
<title>ssr demo</title>
</head>
<body>
<div id="root">${content}</div>
<script src="/index.js"></script>
</body>
</html>
`
}
Однако при написании хранилища на стороне сервера есть подводные камни. Хранилище, созданное createStore, является одноэлементным хранилищем. Такая запись на стороне сервера приведет к тому, что все пользователи будут совместно использовать хранилище, поэтому мы инкапсулируем шаг создания хранилища в метод, каждый раз, когда оба вызова возвращают новое хранилище. Кроме того, мы можем извлечь эту часть кода для создания магазина и обратиться к серверу и клиенту соответственно, чтобы уменьшить избыточность кода.
Мы создаем папку магазина в каталоге src и создаем index.js в папке магазина, чтобы хранить код для создания магазина.
import { createStore,applyMiddleware } from'redux'
import thunk from 'redux-thunk'
const reducer = (state = { name:'CJW' }, action) => {
return state
}
const getStore = () => {
return createStore(reducer,applyMiddleware(thunk))
}
export default getStore
Введите метод getStore как в client/index.js, так и в server/utils.js, чтобы удалить исходный код для создания хранилища.
import React from 'react'
import ReactDom from 'react-dom'
import { BrowserRouter } from 'react-router-dom'
import { Provider } from 'react-redux'
import Routers from '../Routers'
import getStore from '../store'
const App = () => {
return (
<Provider store={getStore()}>
<BrowserRouter>
{Routers}
</BrowserRouter>
</Provider>
)
}
ReactDom.hydrate(<App/>,document.getElementById('root'))
Это просто для того, чтобы показать простое создание хранилища.В реальном использовании нам нужно создать стандартное хранилище, чтобы добиться разделения редьюсера, хранилища и действия, но в качестве простой демонстрации эти операции здесь выполняться не будут.
4. Запрашивайте данные асинхронно
Мы устанавливаем axios для облегчения наших асинхронных запросов
npm install axios --save
Так как thunk установлен, мы можем отправлять асинхронные запросы в действии, что также является основным содержанием thunk, поэтому я не буду объяснять это слишком подробно. Измените index.js в домашней папке, код выглядит следующим образом (интерфейс, запрошенный моими axios, вернет список, читатели могут запросить интерфейс в своем собственном проекте или запросить различные общедоступные API)
import React from 'react'
import Header from '../../component/header'
import { connect } from 'react-redux'
import axios from 'axios'
class Home extends React.Component {
//在componentDidMount中发送异步请求
componentDidMount(){
this.props.getList()
}
render(){
console.log(this.props.list)
return (
<div>
<Header/>
{ this.props.list?
<div>
{this.props.list.map(item=>(
<div>{item.title}</div>
))}
</div>:''}
<button onClick={()=>{alert('click')}}>click</button>
</div>)
}
}
//使用redux-thunk,在action中写axios并dispatch
const getData = () => {
return (dispatch) => {
//接收来自mapDispatchToProps的dispatch方法
axios.get('http://异步请求的接口)
.then((res)=>{
const list = res.data.data
dispatch({type:'CHANGE_LIST',list:list})
})
}
}
const mapStateToProps = state => ({
name:state.name,
list:state.list
})
const mapDispatchToProps = dispatch => ({
getList(){
//调用dispatch时会自动执行getData里return的方法
dispatch(getData())
}
})
export default connect(mapStateToProps , mapDispatchToProps)(Home)
После долгого метания мы сохранили код, обновили страницу, и мы могли видеть, что содержимое асинхронного запроса успешно отобразилось на странице, однако мы щелкнули правой кнопкой мыши исходный код страницы и обнаружили, что там не было HTML, соответствующего содержимому.Это связано с тем, что componentDidMount() не запускается, когда код React выполняется на стороне сервера, поэтому хранилище на стороне сервера всегда пусто.
5. Заполнить данные с помощью loadData
Для того, чтобы HTML, возвращаемый клиенту, содержал данные асинхронного запроса, по сути, нам нужно заполнить текущее хранилище данными по разным страницам, для этого нам необходимо выполнить следующие два условия:
1. Код на стороне сервера может совпадать с запросом axios в соответствующем компоненте при входе на страницу
2. Соответствующий компонент может передать данные, полученные из запроса axios, в хранилище на стороне сервера.
Для этой проблемы React-Router предоставил несколько методов для SSR (см. официальную документацию: https://reacttraining.com/react-router/web/guides/server-rendering), нам необходимо выполнить следующие шаги:
Изменить записи маршрутизации (Router.js) с экспорта компонентов на экспорт массивов.
import Home from './containers/Home' //引入Home组件
import Login from './containers/Login' //引入Login组件
export default [
{
path:'/',
component:Home, //渲染Home组件
exact:true, //严格匹配
loadData:Home.loadData, //传入loadData方法
key:'Home' //用于后续循坏时提供key
},
{
path:'/login',
component:Login,
exact:true,
key:'login'
}
]
2. Измените util.js и index.js в папках сервера и клиента соответственно.
import React from 'react'//引入React以支持JSX的语法
import { renderToString } from 'react-dom/server'//引入renderToString方法
import { StaticRouter,Route } from 'react-router-dom'
import { Provider } from 'react-redux'
import routers from '../Routers'
import getStore from '../store'
export const render = (req) => {
const store= getStore()
const content = renderToString((
<Provider store={store}>
<StaticRouter location={req.path} context={{}}>
<div>
{routers.map(router=> (
<Route{...router}/>
))}
</div>
</StaticRouter>
</Provider>
));
return`
<html>
<head>
<title>ssr demo</title>
</head>
<body>
<div id="root">${content}</div>
<script src="/index.js"></script>
</body>
</html>
`
}
import React from 'react'
import ReactDom from 'react-dom'
import { BrowserRouter,Route } from 'react-router-dom'
import { Provider } from 'react-redux'
import routers from '../Routers'
import getStore from '../store'
const App = () => {
return (
<Provider store={getStore()}>
<BrowserRouter>
<div>
{routers.map(router=> (
<Route{...router}/>
))}
</div>
</BrowserRouter>
</Provider>
)
}
ReactDom.hydrate(<App/>,document.getElementById('root'))
3. Введите метод matchPath в server/index.js для соответствия текущему маршруту страницы и выполните соответствующий метод loadData.
import React from 'react'//引入React以支持JSX的语法
import { renderToString } from 'react-dom/server'//引入renderToString方法
import { StaticRouter,Route } from 'react-router-dom'
import { Provider } from 'react-redux'
import routers from '../Routers'
import getStore from '../store'
export const render = (req) => {
const matchRoutes = []
routers.some(route=> {
matchPath(req.path, route) ? matchRoutes.push(route) : ''
})
console.log(matchRoutes)
const store = getStore()
const content = renderToString((
<Provider store={store}>
<StaticRouter location={req.path} context={{}}>
<div>
{routers.map(router=> (
<Route{...router}/>
))}
</div>
</StaticRouter>
</Provider>
));
return`
<html>
<head>
<title>ssr demo</title>
</head>
<body>
<div id="root">${content}</div>
<script src="/index.js"></script>
</body>
</html>
`
}
Эти три шага нужно рассматривать вместе.На этом шаге наша цель - выполнить метод loadData компонента (метод будет реализован ниже) при входе в компонент.Метод loadData будет получать данные, запрошенные axios, и передавать данные в хранилище на стороне сервера.
Мы можем распечатать соответствующие элементы маршрутизации, а содержимое элементов маршрутизации — это записи маршрутизации, которые мы настроили в Router.js.
Теперь давайте реализуем метод loadData
import React from 'react'
import Header from '../../component/header'
import { connect } from 'react-redux'
import axios from 'axios'
class Home extends React.Component {
//在componentDidMount中发送异步请求
componentDidMount(){
this.props.getList()
}
render(){
console.log(this.props.list)
return (
<div>
<Header/>
{ this.props.list?
<div>
{this.props.list.map(item=>(
<div>{item.title}</div>
))}
</div>:''}
<button onClick={()=>{alert('click')}}>click</button>
</div>)
}
}
Home.loadData = (store) => {
store.dispatch(getData())
}
//使用redux-thunk,在action中写axios并dispatch
const getData = () => {
return (dispatch) => {
//接收来自mapDispatchToProps的dispatch方法
axios.get('接口地址')
.then((res)=>{
const list = res.data.data
dispatch({type:'CHANGE_LIST',list:list})
})
}
}
const mapStateToProps = state => ({
name:state.name,
list:state.list
})
const mapDispatchToProps = dispatch => ({
getList(){
//调用dispatch时会自动执行getData里return的方法
dispatch(getData())
}
})
export default connect(mapStateToProps , mapDispatchToProps)(Home)
В методе loadData мы напрямую получаем хранилище на стороне сервера и вызываем метод dispatch хранилища для обновления данных хранилища, но просто написав таким образом, мы все равно получим пустой контент. Это связано с тем, что запрос axios является асинхронной операцией, а рендеринг на стороне сервера был выполнен до запроса. Здесь мы используем обещания, чтобы исправить наш порядок выполнения. Поскольку сам axios является объектом обещания, мы можем вернуть объект axios. метод loadData, отправка также возвращает, так что мы можем получить этот промис в server/util.js и вызвать метод Promise.all для рендеринга HTML после выполнения всех асинхронных операций, изменить server/util.js и завершить наш Заключительный этап
import React from 'react'//引入React以支持JSX的语法
import { renderToString } from 'react-dom/server'//引入renderToString方法
import { StaticRouter,Route,matchPath } from 'react-router-dom'
import { Provider } from 'react-redux'
import routers from '../Routers'
import getStore from '../store'
export const render = (req,res) => {
//将res传入以使用res.send()方法
const store = getStore()
const matchRoutes = []
const promises = []
routers.some(route=> {
matchPath(req.path, route) ? matchRoutes.push(route) : ''
})
matchRoutes.forEach( item=> {
promises.push(item.loadData(store))
})
Promise.all(promises).then(()=>{
//可以console一下看到当前的store已经有数据
console.log(store.getState())
const content = renderToString((
<Provider store={store}>
<StaticRouter location={req.path} context={{}}>
<div>
{routers.map(router=> (
<Route{...router}/>
))}
</div>
</StaticRouter>
</Provider>
));
res.send(`
<html>
<head>
<title>ssrdemo</title>
</head>
<body>
<div id="root">${content}</div>
<script src="/index.js"></script>
</body>
</html>
`)
})
}
На этом этапе мы, наконец, можем правильно использовать избыточность на стороне сервера.После прохождения бесчисленных ям мы, наконец, можем построить базовый проект SSR. Объяснение рендеринга на стороне сервера на этом заканчивается.
Резюме абзаца:
В этом разделе описывается, как использовать Redux в проектах SSR и использовать axios для асинхронной выборки данных. Как и рендеринг компонентов и маршрутизация, нам нужно использовать Redux один раз на стороне сервера и на стороне клиента. Причина в том, что рендеринг на стороне сервера будет только пройти через жизненный цикл один раз и остановится после первого рендера. Асинхронный запрос данных не будет запущен в функции componentDidMount.Даже если мы инициируем запрос данных заранее, полученные данные не могут снова вызвать рендеринг из-за асинхронного возврата, поэтому HTML, возвращаемый клиенту, по-прежнему не содержит данных асинхронного запроса. . В этом случае мы модифицируем маршрут, чтобы соответствующий метод loadData мог сопоставляться каждый раз при входе на соответствующую страницу, внедряем данные в хранилище на стороне сервера и размещаем этап генерации HTML после завершения асинхронного запроса.
5. Вывод
Благодаря введению этой статьи читатели должны иметь предварительное представление о рендеринге React на стороне сервера, быть в состоянии выполнять базовый рендеринг компонентов, переходы маршрутизации и использовать избыточность на стороне сервера, а также лучше понимать роль веб-пакета и Node. в проектах React. Лучшее понимание. В отрасли идет много дискуссий о выборе SSR.Хотя SSR может решить недостатки традиционных проектов SPA, которые слишком долго загружают первый экран и не способствуют SEO, SSR также приносит много проблем, таких как большая нагрузка на сервер и сложная реализация. Если в конечном итоге нет стремления к времени загрузки первого экрана и SEO, мы можем выбрать более легкое решение, такое как использование webpack и react-router для разделения кода, чтобы сократить время загрузки первого экрана; использование prerender для предварительного рендеринга чтобы оптимизировать рейтинг проекта в поисковой системе, рендеринг на стороне сервера не должен выполняться изоморфно для всего проекта. Возможно, изоморфная SSR пока не является идеальным решением, но в бесконечной погоне за производительностью и опытом большинство фронтенд-техников обсуждение и итерация SSR будут продолжаться еще долгое время.
Справочная статья:
«Внешний и внутренний рендеринг и изоморфный рендеринг»: https://blog.csdn.net/qizhiqq/article/details/70904799
«Руководство по Vue-SSR»: https://ssr.vuejs.org/zh/#
«Серверный механизм рендеринга next.js»: https://www.jianshu.com/p/a3bce57e7349
В приведенных выше статьях был проведен углубленный анализ принципов, преимуществ и недостатков SSR, чтобы помочь читателям понять сценарии применения и ограничения SSR. имеюточень высокоисходная величина
Статья писалась почти 3 месяца до и после, и я надеюсь, что она может вам помочь.Эта статья суммирует курс г-на Делла Ли по рендерингу на стороне сервера React.Курс г-на Делла объясняет глубокие вещи простым языком и также учтены все детали, за которыми очень стоит следить. Автор делает общий вывод на основе курса, исследует и обдумывает некоторые смутные детали курса, надеясь вдохновить читателей.Кроме того, я также надеюсь, что все поддержат курс Делл Ли.Ведь просто чтение статья не ССР не может быть полностью освоена. Я также надеюсь, что читатели смогут обратить внимание на публичный аккаунт фронтенд-команды нашей компании и будут регулярно продвигать обмен технологиями~