Я смотрел это недавноReact SSR
Связанные вещи, запишите соответствующий контент здесь
Пример кода этой статьи был загружен наgithub, если интересно, см.Basic | SplitChunkV
Знакомство с React SSR
nodejs
следитьcommonjs
Спецификация, импорт и экспорт файлов следующие:
// 导出
module.exports = someModule
// 导入
const module = require('./someModule')
и мы обычно пишемreact
код следуетesModule
Стандартно импорт и экспорт файлов выглядит следующим образом:
// 导出
export default someModule
// 导入
import module from './someModule'
Так хочу, чтобыreact
Если код совместим с серверной частью, то сначала нужно решить проблему совместимости этих двух спецификаций.react
может быть напрямуюcommonjs
Спецификации пишутся, например:
const React = require('react')
На первый взгляд кажется, что это преобразование метода письма, и в этом нет никакой проблемы, но на самом деле это решает только одну из проблем.react
Общий код рендеринга в , т.е.jsx
,node
Я не знаю, я должен скомпилировать его один раз
render () {
// node是不认识 jsx的
return <div>home</div>
}
компиляция на стороне клиентаreact
Наиболее часто используемый кодwebpack
, можно использовать и серверную часть, здесь используйтеwebpack
имеет две роли:
- Буду
jsx
компилируется вnode
оригиналjs
код - Буду
exModule
код компилируется вcommonjs
из
webpack
Пример файла конфигурации выглядит следующим образом:
// webpack.server.js
module.exports = {
// 省略代码...
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/,
options: {
// 需要支持 react
// 需要转换 stage-0
presets: ['react', 'stage-0', ['env', {
targets: {
browsers: ['last 2 versions']
}
}]]
}
}
]
}
}
С этим файлом конфигурации вы можете с удовольствием писать код
Первый — это копия, которую нужно вывести на клиентreact
Код:
import React from 'react'
export default () => {
return <div>home</div>
}
Этот код очень прост, обычныйreact stateless
компоненты
Затем идет серверный код, отвечающий за вывод этого компонента на клиент:
// index.js
import http from 'http'
import React from 'react'
import { renderToString } from 'react-dom/server'
import Home from './containers/Home/index.js'
const container = renderToString(<Home />)
http.createServer((request, response) => {
response.writeHead(200, {'Content-Type': 'text/html'})
response.end(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="root">${container}</div>
</body>
</html>
`)
}).listen(8888)
console.log('Server running at http://127.0.0.1:8888/')
Приведенный выше код запускаетnode http
сервер, ответилhtml
Исходный код страницы, но по сравнению с обычнымnode
С точки зрения кода на стороне сервера здесь также представленreact
Связанные библиотеки
что мы обычно пишемReact
Код, чье действие для рендеринга страницы, на самом делеreact
вызов, связанный с браузеромAPI
в режиме реального времени, т.е. страница создаетсяjs
Манипулировать браузеромDOM API
Собран, серверная часть не может вызывать браузерAPI
, поэтому этот процесс не может быть выполнен, в это время вам нужно использоватьrenderToString
охватывать
renderToString
даReact
предусмотрено дляReact
Код преобразуется во что-то, что браузер может напрямую распознать.html
нитьAPI
, можно считать, что этоAPI
Делайте то, что браузер должен делать заранее, прямо на стороне сервераDOM
Струна собрана и переданаnode
вывод в браузер
Переменные в приведенном выше кодеcontainer
, что на самом деле выглядит следующим образомhtml
Нить:
<div data-reactroot="">home</div>
так,node
Отклик браузера нормальныйhtml
Строка может отображаться непосредственно браузером, потому что браузеру не нужно загружатьreact
код, размер кода меньше, и нет необходимости в сплайсинге в реальном времениDOM
String, просто выполните действие рендеринга страницы, чтобы скорость рендеринга на стороне сервера была выше.
Кроме того, кромеrenderToString
Помимо,React v16.x
Есть и другой, более мощныйAPI
:renderToNodeStream
renderToNodeStream
Поддерживается прямой рендеринг в потоки узлов. Рендеринг в поток может уменьшить первый байт вашего контента (TTFB
), отправить начало и конец документа в браузер до того, как будет сгенерирована следующая часть документа. Когда контент передается с сервера, браузер начинает анализироватьHTML
Документация, некоторые статьи называют этоAPI
Скорость рендерингаrenderToString
три раза (сколько раз я не проверял, но это правда, что скорость рендеринга в целом будет выше)
Итак, если вы используетеReact v16.x
, вы также можете написать:
import http from 'http'
import React from 'react'
// 这里使用了 renderToNodeStream
import { renderToNodeStream } from 'react-dom/server'
import Home from './containers/Home/index.js'
http.createServer((request, response) => {
response.writeHead(200, {'Content-Type': 'text/html'})
response.write(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="root">
`)
const container = renderToNodeStream(<Home />)
// 这里使用到了 数据流的概念,所以需要以流的形式进行传送数据
container.pipe(response, { end: false })
container.on('end', () => {
// 响应流结束
response.end(`
</div>
</body>
</html>
`)
})
}).listen(8888)
console.log('Server running at http://127.0.0.1:8888/')
Логический изоморфизм, связанный с BOM/DOM
имеютrenderToString / renderToNodeStream
После этого кажется, что рендеринг на стороне сервера находится в пределах досягаемости, но на самом деле это далеко не так, для следующегоreact
Код:
const Home = () => {
return <button onClick={() => { alert(123) }}>home</button>
}
Ожидается, что при нажатии кнопки в браузере появится подсказка123
Всплывающее окно, но если вы просто выполните описанный выше процесс, это событие не сработает, причина в том, чтоrenderToString
будет анализировать только основныеhtml DOM
элемент, и не будет анализировать события, прикрепленные к элементу, то есть он будет проигнорированonClick
этот инцидент
onClick
является событием, в коде, который мы обычно пишем (т.е. неSSR
),React
выполняется на элементеaddEventListener
Зарегистрироваться на мероприятие, то есть черезjs
Для запуска события и вызова соответствующего метода, и серверная сторона явно не может выполнить эту операцию, кроме того, некоторые операции, связанные с браузером, не могут быть выполнены на стороне сервера.
Однако они не влияютSSR
,SSR
Одна из целей — позволить браузеру отображать страницу быстрее, а взаимодействие с исполняемым файлом пользователя не должно следовать за страницей.DOM
Завершается одновременно, поэтому мы можем упаковать эту часть кода выполнения, связанного с браузером, вjs
Файл отправляется в браузер, и после отображения страницы в браузере он загружается и выполняется.js
, вся страница, естественно, имеет исполняемый файл
Для упрощения работы на стороне сервера введено следующееKoa
Поскольку на стороне браузера также необходимо запустить сноваHome
компонент, то вам нужно подготовить еще одну копию для использования браузеромHome
Файл пакета:
// client
import React from 'react'
import ReactDOM from 'react-dom'
import Home from '../containers/Home'
ReactDOM.render(<Home />, document.getElementById('root'))
Обычно пишется на стороне браузераReact
код, поставитьHome
Компонент снова упаковывается и отображается на узле страницы.
Кроме того, если вы используетеReact v16.x
, последнее предложение приведенного выше кода предлагает написать:
// 省略代码...
ReactDOM.hydrate(<Home />, document.getElementById('root'))
ReactDOM.render
а такжеReactDOM.hydrate
Основное различие между ними в том, что последний имеет меньшие накладные расходы на производительность (используется только для рендеринга на стороне сервера), более подробную информацию можно увидетьhydrate
Этот код нужно упаковать в кусокjs
Код отправляется на сторону браузера, поэтому здесь необходимо выполнить аналогичный изоморфный код на стороне клиента.webpack
Конфигурация:
// webpack.client.js
const path = require('path')
module.exports = {
// 入口文件
entry: './src/client/index.js',
// 表示是开发环境还是生产环境的代码
mode: 'development',
// 输出信息
output: {
// 输出文件名
filename: 'index.js',
// 输出文件路径
path: path.resolve(__dirname, 'public')
},
// ...
}
Этот файл конфигурации и файл конфигурации на стороне сервераwebpack.server.js
Это почти то же самое, просто удалите некоторые настройки, связанные с серверной частью.
Этот файл конфигурации объявляет, чтоHome
компоненты упакованы вpublic
каталог, имя файлаindex.js
, так что нам нужно только вывести на стороне сервераhtml
На странице загрузите в него этот файл:
// server
// 省略无关代码...
app.use(ctx => {
ctx.response.type = 'html'
ctx.body = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="root">${container}</div>
<!-- 引入同构代码 -->
<script src="/index.js"></script>
</body>
</html>
`
})
app.listen(3000)
дляHome
Для этого компонента он запускается один раз на стороне сервера, в основном черезrenderToString/renderToNodeStream
генерировать чистыйhtml
Элементы, но и запускаются на клиенте один раз, основные события были правильно зарегистрированы, комбинация обоих, вы можете интегрировать страницу из обычного взаимодействия, такой сервер и клиент запускают тот же набор операционного кода, также называемымизоморфизм
Изоморфизм маршрутизации (маршрутизатор)
решенные события и т. д.js
После изоморфности связанного кода маршрутизация также должна быть изоморфна.
Обычно вreact
код будет использоватьreact-router
Для управления маршрутизацией, здесь в изоморфном коде, передаваемом с сервера в браузер, можно еще следовать общей практике (HashRouter/BrowserRouter
), здесь сBrowserRouter
Например
Определение маршрута:
import React, { Fragment } from 'React'
import { Route } from 'react-router-dom'
import Home from './containers/Home'
import Login from './containers/Login'
export default (
<Fragment>
<Route path='/' exact component={Home}></Route>
<Route path='/login' exact component={Login}></Route>
</Fragment>
)
Введение кода на стороне браузера:
import React from 'react'
import ReactDOM from 'react-dom'
// 这里以 BrowserRouter 为例,HashRouter也是可以的
import { BrowserRouter } from 'react-router-dom'
// 引入定义的路由
import Routes from '../Routes'
const App = () => {
return (
<BrowserRouter>
{Routes}
</BrowserRouter>
)
}
ReactDOM.hydrate(<App />, document.getElementById('root'))
В основном при импорте маршрута на стороне сервера:
// 使用 StaticRouter
import { StaticRouter } from 'react-router-dom'
import Routes from '../Routes'
// ...
app.use(ctx => {
const container = renderToNodeStream(
<StaticRouter location={ctx.request.path} context={{}}>
{Routes}
</StaticRouter>
)
// ...
})
Маршрутизация на стороне сервера не имеет состояния, то есть некоторые операции маршрутизации не записываются, а изменения маршрутизации и статус маршрутизации на стороне браузера не могут быть автоматически изучены, потому что это все вещи браузера.React-router 4.x
предусмотрено для серверной частиStaticRouter
Для управления маршрутизацией этоAPI
через входящийlocation
Параметры для пассивного получения маршрута текущего запроса, чтобы сопоставлять и перемещаться по маршруту, см. подробнееStaticRouter
Изоморфизм состояний (состояние)
Когда проект относительно большой, мы обычно используемredux
Для управления состоянием данных проекта, чтобы обеспечить согласованность между состоянием сервера и состоянием клиента, также необходимо выполнить изоморфизм состояния.
Код сервера используется для всех пользователей, все пользователи должны открыть независимое состояние данных, в противном случае все пользователи используют одно и то же состояние.
// 这种写法在客户端可取,但在服务器端会导致所有用户共用了同一个状态
// export default createStore(reducer, applyMiddleware(thunk))
export default () => createStore(reducer, applyMiddleware(thunk))
Обратите внимание, что приведенный выше код экспортирует функцию вместоstore
объект, хотите получитьstore
Просто выполните эту функцию:
import getStore from '../store'
// ...
<Provider store={getStore()}>
<StaticRouter location={ctx.request.path} context={context}>
{Routes}
</StaticRouter>
</Provider>
Это гарантирует, что сервер будет регенерировать новый каждый раз, когда он получает запрос.store
, что эквивалентно тому, что каждый запрос получает новое независимое состояние
Вышеизложенное как раз решает проблему государственной независимости, ноSSR
Ключевым моментом синхронизации состояний является синхронизация асинхронных данных, таких как вызов общих интерфейсов данных, что является асинхронной операцией, если вы используете ее в клиенте.redux
Для выполнения асинхронных операций сделайте то же самое на стороне сервера.Хотя проект не сообщит об ошибке и страница может нормально отрисовываться, на самом деле эта часть асинхронно полученных данных отсутствует на странице, отрендеренной сервером.
Это легко понять.Хотя серверная сторона также может выполнять операции запроса интерфейса данных, поскольку запрос интерфейса является асинхронным, а рендеринг страницы синхронным, вполне вероятно, что когда сервер отвечает на выходную страницу, асинхронно запрошенные данные не были Возвращается, тогда на отображаемой странице, естественно, не будет данных.
Поскольку состояние данных теряется из-за проблемы асинхронного получения данных, если правильные данные, требуемые страницей, могут быть получены до того, как сервер ответит на страницу, проблема решена.
На самом деле здесь две проблемы:
- Вам нужно знать, какая страница в данный момент запрашивается, потому что данные, требуемые разными страницами, как правило, отличаются, и запрашиваемый интерфейс и логика обработки данных также различаются.
- Необходимо убедиться, что сервер получил данные от интерфейса до ответа на страницу, то есть обработанное состояние (
store
)
На первый вопрос,react-routerНа самом деле ужеSSR
Решение дается в терминахнастроить маршрут/маршрут-конфигурациякомбинироватьmatchPath, найдите на странице метод интерфейса запроса, требуемый соответствующим компонентом, и выполните:
Кроме того,react-router
который предоставилmatchPath
Может быть распознана только маршрутизация первого уровня.Для многоуровневой маршрутизации может быть идентифицирована только маршрутизация верхнего уровня, а маршрутизация подуровня будет игнорироваться, поэтому, если в проекте нет многоуровневой маршрутизации или сбора данных и обработка состояния выполняется в маршрутизации верхнего уровня, затем используйтеmatchPath
Проблем нет, иначе может быть проблема потери данных страницы при дочерней маршрутизации
По этому вопросу,react-router
также даетрешение, которым пользуется сам разработчикreact-router-config
предоставлено вmatchRoutes
заменитьmatchPath
Что касается второго вопроса, это на самом деле намного проще, т.js
Синхронизация общих асинхронных операций в коде, наиболее часто используемыхPromise
илиasync/await
может решить эту проблему
const store = getStore()
const promises = []
// 匹配的路由
const mtRoutes = matchRoutes(routes, ctx.request.path)
mtRoutes.forEach(item => {
if (item.route.loadData) {
promises.push(item.route.loadData(store))
}
})
// 这里服务器请求数据接口,获取当前页面所需的数据,填充到 store中用于渲染页面
await Promise.all(promises)
// 服务器端输出页面
await render(ctx, store, routes)
Однако после решения этой проблемы возникла другая проблема.
Я сказал раньше,SSR
Процесс, обеспечивающий согласованность состояния данных на стороне сервера и на стороне клиента.Согласно описанному выше процессу, на стороне сервера в конечном итоге будет выведена полная страница со статусом данных, но логика кода на стороне клиента чтобы сначала отобразить состояние без данных. полка страницы дляcomponentDidMount
В таких функциях ловушек инициируется запрос интерфейса данных для получения данных, выполняется обработка состояния, и конечная полученная страница согласуется с выводом на стороне сервера.
Затем в период до того, как клиентский код получит данные, состояние данных клиента фактически пусто, а состояние данных сервера завершено, поэтому состояние данных на обоих концах несовместимо, что вызовет проблемы.
Процесс решения этой задачи на самом деле представляет собой данныеобезвоживаниеа такжезакачка воды
На стороне сервера, когда сторона сервера запрашивает интерфейс для получения данных и обработки состояния данных (например,store
После обновления) сохраняйте это состояние и отвечайте на страницу на стороне сервераHTML
Когда состояние передается в браузер, этот процесс вызываетсяобезвоживание(Dehydrate
); на стороне браузера просто возьмите это напрямуюобезвоживаниеданные для инициализацииReact
компонент, то есть клиенту не нужно инициировать запрос на получение статуса обработки данных, потому что это уже сделал сервер, и статус обработки можно получить непосредственно с сервера.Этот процесс называетсязакачка воды(Hydrate
)
А серверная сторона объединяет состояние сhtml
Способ отправки его на сторону браузера обычно осуществляется через глобальные переменные:
ctx.body = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta httpquiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="root">${data.toString()}</div>
<!-- 从服务器端拿到脱水的数据状态 -->
<script>
window.context = {
state: ${JSON.stringify(store.getState())}
}
</script>
<!-- 引入同构代码 -->
<script src="/index.js"></script>
</body>
</html>
`
Затем, после того, как сторона браузера получит страницу, отправленную со стороны сервера, она может получить прямой доступ кwindow
Состояние получается на объекте, а затем используется это состояние для обновления состояния самого браузера:
export const getClientStore = () => {
// 从服务器端输出的页面上拿到脱水的数据
const defaultState = window.context.state
// 当做 store的初始数据(即注水)
return createStore(reducer, defaultState, applyMiddleware(thunk))
}
стиль импорта
Введение стилей относительно просто и может рассматриваться с двух точек зрения:
- вывод на стороне сервера
html
при документированииhtml
добавить<style>
Тег, строка стиля записывается внутри этого тега и отправляется клиенту вместе - вывод на стороне сервера
html
при документированииhtml
добавить<link>
этикетка, эта этикеткаhref
Указывает на файл стиля, этот файл стиля является файлом стиля страницы.
Общие идеи этих двух операций схожи, и они аналогичны процессу введения стилей при рендеринге на стороне клиента.webpack
,пройти черезloader
плагин будетreact
Стиль, написанный в компоненте, извлекается, и связанный с нимloader
Плагины обычно имеютcss-loader
,style-loader
,extract-text-webpack-plugin / mini-css-extract-plugin
, если использоватьcss
постпроцессор, то вам также может понадобитьсяsass-loader
илиless-loader
д., эти сложные ситуации здесь не рассматриваются, только самые основныеcss
представлять
встроенный стиль
Для первого использования встроенного стиля, непосредственного встраивания стиля в страницу, вам необходимо использоватьcss--loader
а такжеstyle-loader
,css-loader
можно продолжать использовать, ноstyle-loader
Из-за некоторой логики, связанной с браузером, его нельзя использовать на стороне сервера, но, к счастью, уже давно существует альтернативный плагин.isomorphic-style-loader, использование этого плагина такое же, какstyle-loader
Аналогично, но также поддерживает использование на стороне сервера
isomorphic-style-loaderбудет импортироватьcss
Файл преобразуется в объект для использования компонентом, некоторые свойства являются именем класса, а значением свойства является соответствующий класс.css
стили, так что вы можете напрямую вводить стили в компоненты на основе этих свойств.Кроме того, существует несколько методов,SSR
нужно позвонить в_getCss
метод получения строки стиля для передачи клиенту
Учитывая описанный выше процесс (скороcss
Суммирование стилей и преобразование в строки) — это общий процесс, поэтому этот проект подключаемого модуля активно предоставляет метод для упрощения этого процесса.HOC
Компоненты:withStyles.js
То, что делает этот компонент, также очень просто, в основном дляisomorphic-style-loader
два метода в:__insertCss
а также_getCss
предоставляет интерфейс дляContextВ качестве среды стили, на которые ссылается каждый компонент, передаются и, наконец, суммируются на сервере и клиенте, чтобы стили могли быть выведены на сервер и клиент.
Сервер:
import StyleContext from 'isomorphic-style-loader/StyleContext'
// ...
const css = new Set()
const insertCss = (...styles) => styles.forEach(style => css.add(style._getCss()))
const container = renderToNodeStream(
<Provider store={store}>
<StaticRouter location={ctx.request.path} context={context}>
<StyleContext.Provider value={{ insertCss }}>
{renderRoutes(routes)}
</StyleContext.Provider>
</StaticRouter>
</Provider>
)
Клиент:
import StyleContext from 'isomorphic-style-loader/StyleContext'
// ...
const insertCss = (...styles) => {
const removeCss = styles.map(style => style._insertCss())
return () => removeCss.forEach(dispose => dispose())
}
const App = () => {
return (
<Provider store={store}>
<BrowserRouter>
<StyleContext.Provider value={{ insertCss }}>
{renderRoutes(routes)}
</StyleContext.Provider>
</BrowserRouter>
</Provider>
)
}
использование этого компонента более высокого порядка,isomorphic-style-loaderизREADME.mdВыше ясно сказано, в основномContext(isomorphic-style-loader@5.0.1
Предыдущая версия является старой версиейContext API
,5.0.1
а потом новая версияContext API
) и компоненты более высокого порядкаHOC
использование
Стиль контура
Как правило, в большинстве производственных сред используется внешний стиль, использующий<link>
Ярлык может быть импортирован на страницу с помощью файла стиля, который фактически представлен вышеописанным охватом.js
Подход представляет собой ту же логику обработки, что и встроенное введение.CSS
Проще и понятнее, поток обработки сервера и клиента в основном одинаков.
mini-css-extract-pluginэто часто используемый стиль компонента извлеченияwebpack
Подключаемый модуль, поскольку этот подключаемый модуль по существу извлекает строку стиля из каждого компонента и интегрирует ее в файл стиля, простоJS Core
операции, поэтому нет аргументов на стороне сервера и на стороне браузера, поэтому нет необходимости в изоморфизме.Как использовать этот плагин на чистом клиенте раньше, как использовать сейчасSSR
используется в, тут особо нечего сказать
разделение кода
SSR
Важной целью является ускорение рендеринга первого экрана, поэтому меры по оптимизации исходного рендеринга на стороне клиента также должны бытьSSR
Одним из ключевых моментов является разделение кода
React
Есть много разделений кода, таких какbabel-plugin-syntax-dynamic-import,react-loadable,loadable-componentsЖдать
Библиотеки, которые обычно используются,react-loadable, но я столкнулся с некоторыми проблемами при его использовании, я хочу проверитьissues
Когда я узнал, что этот проект на самом делеissues
Он был закрыт, поэтому я забросил его и переключился на болееmodern
изloadable-components, эта библиотекаДокументацияполной и с учетомSSRи поддерживаетrenderToNodeStream
Отобразите страницу в виде , просто следуйте документацииok
Теперь очень легко начать. Я не буду здесь много говорить. Подробности см.SplitChunkV
Суммировать
SSR
По-прежнему проблематично настраивать не только конфигурацию на уровне внешнего интерфейса, но и вещи, связанные с внутренней программой, такие как состояние входа в систему, высокий уровень параллелизма, балансировка нагрузки, управление памятью и т. д.Winterвыражено дляSSR
Не очень оптимистично, в основном используется дляSEO
, не рекомендуется для рендеринга на стороне сервера, его можно использовать в нескольких сценах, а стоимость слишком высока.
Поэтому для реальной разработки я лично рекомендую использовать относительно зрелые колеса в отрасли напрямую, такие какReact
изNext.js,Vue
изNuxt.js