предисловие
Из внешнего интерфейса (React
,Vue
,Angelar
) с момента своего появления каждый фреймворк несет в себе разную идею и делится на три лагеря, использовавшиеся ранееJQuery
Прошли те времена, когда каждая страница былаHTML
, ввести соответствующиеJS
,CSS
, пока вHTML
написано наDOM
. Из-за этого каждый раз, когда пользователь заходит, из-заHTML
имеютDOM
Существование , по чувству реакции пользователя не очень медленно.
Но так как используется фреймворк, сколько бы в нем ни было страниц, это одна-единственная страница, т. е.SPA
.HTML
все вDOM
элемент, который необходимо загрузить на клиентеjs
После этого выполните, вызвавReact.render()
Его можно только отрендерить, поэтому есть много сайтов, которые долго заходят.loading
анимация.
Для того, чтобы решить эту не очень дружественную проблему, в сообществе было предложено множество решений, таких как预渲染
,SSR
,同构
.
Конечно, в этой статье в основном рассказывается о построении нуля.Изоморфизм рендеринга сервера React.
Опции
Вариант 1. Используйте выбранный сообществом фреймворк Next.js.
Next.js
легкийReact
Каркас приложения рендеринга на стороне сервера. Желающие могут пройтиNext.js
Исследование на официальном сайте.
Схема 2 Изоморфизм
Есть две схемы изоморфизма:
Выполнить после экранирования кода на стороне узла и кода React через Babel
let app = express();
app.get('/todo', (req, res) => {
let html = renderToString(
<Route path="/" component={ IComponent } >
<Route path="/todo" component={ AComponent }>
</Route>
</Route>)
res.send( indexPage(html) )
}
})
Здесь нужно решить две проблемы:
-
Node
передняя часть не поддерживаетсяimport
синтаксис, который необходимо ввестиbabel
служба поддержки. -
Node
Не удалось проанализировать синтаксис тега.
так выполнитьNode
, вам нужно использоватьbabel
Чтобы сбежать, если есть ошибка, нет возможности ее проверить.Лично это делать не рекомендуется.
Так вот второй вариант
вебпак для компиляции
использоватьwebpack
Упакуйте два кода, один дляNode
Выполняется рендеринг сервера, а один используется для рендеринга браузера.
Ниже приводится подробное описание.
Создайте сервер узла
Из-за привычек использования часто используетсяEgg
кадр, иKoa
даEgg
Базовая структура, поэтому здесь мы используемKoa
Каркас для сервисного строительства.
Создайте самый простойNode
Служить.
const Koa = require('koa');
const app = new Koa();
app.listen(3000, () => {
console.log("服务器已启动,请访问http://127.0.0.1:3000")
});
настроить веб-пакет
Как мы все знаем,React
Код должен быть упакован и скомпилирован перед тем, как его можно будет выполнить, и только часть кода, работающего на сервере и на клиенте, одинакова, и даже некоторые коды вообще не нужно упаковывать. необходимо разделить клиентский код и код, работающий на сервере, на дваwebpack
настроить
webpack
передавать один и тот же код через разныеwebpack
конфигурации соответственноserverConfig
а такжеclientConfig
, упакованный в виде двух кодов.
конфигурация serverConfig и clientConfig
пройти черезwebpackИз документации мы знаем, что webpack может компилировать не только веб-код, но и другой контент.
Здесь мы будемtarget
установить какnode
.
Настройте файл входа и место выхода:
const serverConfig = {
target: 'node',
entry: {
page1: './web/render/serverRouter.js',
},
resolve,
output: {
filename: '[name].js',
path: path.resolve(__dirname, './app/build'),
libraryTarget: 'commonjs'
}
}
Внимание ⚠
Конфигурация сервера должна быть настроенаlibraryTarget
,настраиватьcommonjs
илиumd
, для серверной частиrequire
цитата, иначеrequire
значение{}
.
Здесь нет разницы между конфигурацией клиента и сервера, настройка не требуетсяtarget
(По умолчаниюweb
среда), другие входные и выходные файлы несовместимы.
const clientConfig = {
entry: {
page1: './web/render/clientRouter.js'
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, './public')
}
}
настроить бабел
Так как пакетReact
код, поэтому также необходимо настроитьbabel
.
новый.babelrc
документ.
{
"presets": ["@babel/preset-react",
["@babel/preset-env",{
"targets": {
"browsers": [
"ie >= 9",
"ff >= 30",
"chrome >= 34",
"safari >= 7",
"opera >= 23",
"bb >= 10"
]
}
}]
],
"plugins": [
[
"import",
{ "libraryName": "antd", "style": true }
]
]
}
Эта конфигурация используется сервером и клиентом для обработки.React
И сбежал какES5
и проблемы совместимости браузера.
Обработка проблем со ссылками на стороне сервера
использование сервераCommonJS
Спецификации, а серверный код собирать не нужно, поэтому зависимости в node_modules не нужно упаковывать, поэтому с помощьюwebpack
сторонние модулиwebpack-node-externals
После такой обработки размер двух построенных файлов уже сильно отличается.
обрабатывать css
Разница между сервером и клиентом может заключаться в обработке по умолчанию, необходимостиCSS
Извлекается отдельно как файл и обрабатываетсяCSS
приставка.
Конфигурация сервера
{
test: /\.(css|less)$/,
use: [
{
loader: 'css-loader',
options: {
importLoaders: 1
}
},
{
loader: 'less-loader',
}
]
}
Конфигурация клиента
{
test: /\.(css|less)$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
},
{
loader: 'css-loader'
},
{
loader: 'postcss-loader',
options: {
plugins: [
require('precss'),
require('autoprefixer')
],
}
},
{
loader: 'less-loader',
options: {
javascriptEnabled: true,
// modifyVars: theme //antd默认主题样式
}
}
],
}
Различия между клиентской рендерингом и кодом маршрутизации бокового рендеринга сервера в SSR
выполнитьReact
изSSR
Архитектура, нам нужно выполнить один и тот же код на стороне клиента и на стороне сервера, но здесь каждый выполняет его один раз, исключая код на стороне маршрутизации, причина этого в основном в том, что клиент использует адресную строку для отображения разных компоненты , и сервер отображает компонент через путь запроса.
Поэтому на стороне клиента мы используемBrowserRouter
Для настройки маршрутизации используйте на стороне сервераStaticRouter
для настройки маршрутизации.
Конфигурация клиента
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from "react-router-dom";
import Router from '../router';
function ClientRender() {
return (
<BrowserRouter >
<Router />
</BrowserRouter>
)
}
Конфигурация сервера
import React from 'react';
import { StaticRouter } from 'react-router'
import Router from '../router.js';
function ServerRender(req, initStore) {
return (props, context) => {
return (
<StaticRouter location={req.url} context={context} >
<Router />
</StaticRouter>
)
}
}
export default ServerRender;
Снова настройте Node для серверного рендеринга
Настроенный выше сервер просто запускает службу без дополнительной настройки.
Представляем ReactDOMServer
const Koa = require('koa');
const app = new Koa();
const path = require('path');
const React = require('react');
const ReactDOMServer = require('react-dom/server');
const koaStatic = require('koa-static');
const router = new KoaRouter();
const routerManagement = require('./app/router');
const manifest = require('./public/manifest.json');
/**
* 处理链接
* @param {*要进行服务器渲染的文件名默认是build文件夹下的文件} fileName
*/
function handleLink(fileName, req, defineParams) {
let obj = {};
fileName = fileName.indexOf('.') !== -1 ? fileName.split('.')[0] : fileName;
try {
obj.script = `<script src="${manifest[`${fileName}.js`]}"></script>`;
} catch (error) {
console.error(new Error(error));
}
try {
obj.link = `<link rel="stylesheet" href="${manifest[`${fileName}.css`]}"/>`;
} catch (error) {
console.error(new Error(error));
}
//服务器渲染
const dom = require(path.join(process.cwd(),`app/build/${fileName}.js`)).default;
let element = React.createElement(dom(req, defineParams));
obj.html = ReactDOMServer.renderToString(element);
return obj;
}
/**
* 设置静态资源
*/
app.use(koaStatic(path.resolve(__dirname, './public'), {
maxage: 0, //浏览器缓存max-age(以毫秒为单位)
hidden: false, //允许传输隐藏文件
index: 'index.html', // 默认文件名,默认为'index.html'
defer: false, //如果为true,则使用后return next(),允许任何下游中间件首先响应。
gzip: true, //当客户端支持gzip时,如果存在扩展名为.gz的请求文件,请尝试自动提供文件的gzip压缩版本。默认为true。
}));
/**
* 处理响应
*
* **/
app.use((ctx) => {
let obj = handleLink('page1', ctx.req, {});
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>koa-React服务器渲染</title>
${obj.link}
</head>
<body>
<div id='app'>
${obj.html}
</div>
</body>
${obj.script}
</html>
`
})
app.listen(3000, () => {
console.log("服务器已启动,请访问http://127.0.0.1:3000")
});
Это включает в себяmanifest
файл, этот файлwebpack
плагинwebpack-manifest-plugin
Сгенерировано, в котором содержится компиляционные адреса и файлы. Вероятно, структура такова:
{
"page1.css": "page1.css",
"page1.js": "page1.js"
}
мы знакомим его сclientConfig
, добавьте следующую конфигурацию:
...
plugins: [
// 提取样式,生成单独文件
new MiniCssExtractPlugin({
filename: `[name].css`,
chunkFilename: `[name].chunk.css`
}),
new ManifestPlugin()
]
В приведенном выше коде на стороне сервера мы имеемServerRender.js
Каррирование делается для того, чтобы мыServerRender
, используя идентифицируемый на стороне сервераStaticRouter
и настроитьlocation
параметры, при этомlocation
обязательный параметрURL
.
Поэтому нам нужноrenderToString
пройти вreq
, чтобы сервер мог корректно разобрать компонент React.
let element = React.createElement(dom(req, defineParams));
obj.html = ReactDOMServer.renderToString(element);
пройти черезhandleLink
, мы можем получитьobj
, который содержит три параметра,link
(css
Связь),script
(JS
ссылка) иhtml
(генерироватьDom
элемент).
пройти черезctx.body
оказыватьhtml
.
renderToString()
БудуReact
Элемент рендерится в исходноеHTML
середина. Эту функцию следует использовать только на сервере.React
вернетHTML
нить. Вы можете использовать этот метод для генерации на сервереHTML
, и отправьте разметку по первоначальному запросу, чтобы ускорить загрузку страницы и позволить поисковым системам сканировать вашу страницу дляSEO
Цель.
Если вызывается на узле, который уже имеет этот флаг отображения серверомReactDOM.hydrate()
,React
Он будет сохранен, и будут присоединены только обработчики событий, что даст вам очень эффективную первую загрузку.
renderToStaticMarkup()
похожий наrenderToString
, Кроме этого не создаетReact
дополнительно для внутреннего пользованияDOM
такие свойства, какdata-reactroot
. если вы хотите использоватьReact
Как простой генератор статических страниц, это полезно, потому что удаление дополнительных атрибутов может сэкономить несколько байтов.
Однако, если этот метод используется после просмотра, весь контент, отображаемый сервером, будет заменен, что приведет к мерцанию страницы, поэтому использовать этот метод не рекомендуется.
renderToNodeStream()
БудуReact
Элемент отображается в исходном виде.HTML
середина. возвращает читаемый поток (stream
), то есть выходHTML
нить. этот поток(stream
) выходHTML
точно эквивалентноReactDOMServer.renderToString
что будет возвращено.
Мы также можем использовать вышеуказанноеrenderToNodeSteam
Превратить его вниз:
let element = React.createElement(dom(req, defineParams));
ctx.res.write('
<html>
<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>koa-React服务器渲染</title>
</head><body><div id="app">');
// 把组件渲染成流,并且给Response
const stream = ReactDOMServer.renderToNodeStream(element);
stream.pipe(ctx.res, { end: 'false' });
// 当React渲染结束后,发送剩余的HTML部分给浏览器
stream.on('end', () => {
ctx.res.end('</div></body></html>');
});
renderToStaticNodeStream()
похожий наrenderToNodeStream
, за исключением того, что это не создастReact
дополнительно для внутреннего пользованияDOM
такие свойства, какdata-reactroot
. если вы хотите использоватьReact
Как простой генератор статических страниц, это полезно, потому что удаление дополнительных атрибутов может сэкономить несколько байтов.
этот поток(stream
) выходHTML
точно эквивалентноReactDOMServer.renderToStaticMarkup
что будет возвращено.
добавить редукцию управления состоянием
Вышеупомянутое разрабатывает статический веб-сайт или уже относительно простой проект.OK
Но для сложных проектов этого далеко не достаточно.Тут к этому добавим глобальное управление состояниемRedux
.
В серверном рендеринге порядок синхронный, поэтому для рендеринга данных первого экрана во время рендеринга данные должны быть подготовлены заранее.
- Получите данные заранее
- инициализировать магазин
- Отображение компонентов на основе маршрутов
- Объединяйте данные и компоненты для создания HTML и возвращайте его одновременно
Для клиента добавитьredux
и регулярныйredux
Не очень разница, просто дляstore
добавлен инициалwindow.__INIT_STORE__
.
let initStore = window.__INIT_STORE__;
let store = configStore(initStore);
function ClientRender() {
return (
<Provider store={store}>
<BrowserRouter >
<Router />
</BrowserRouter>
</Provider>
)
}
Для сервера после завершения начального сбора данных можно использоватьPromise.all()
Чтобы сделать параллельные запросы, когда запрос заканчивается, заполните данные дляscript
внутри этикетки, названнойwindow.__INIT_STORE__
.
`<script>window.__INIT_STORE__ = ${JSON.stringify(initStore)}</script>`
Затем на стороне сервераstore
Переконфигурировать.
function ServerRender(req, initStore) {
let store = CreateStore(JSON.parse(initStore.store));
return (props, context) => {
return (
<Provider store={store}>
<StaticRouter location={req.url} context={context} >
<Router />
</StaticRouter>
</Provider>
)
}
}
Организовать Коа
Учитывая удобство дальнейшей разработки, добавить следующие функции:
- Функция маршрутизатора
- HTML-шаблон
Добавить Коа-маршрутизатор
/**
* 注册路由
*/
const router = new KoaRouter();
const routerManagement = require('./app/router');
...
routerManagement(router);
app.use(router.routes()).use(router.allowedMethods());
Для того, чтобы при разработке интерфейс был штатным, все маршруты упоминаются в новом файле для записи. И гарантировать следующий формат:
/**
*
* @param {router 实例化对象} router
*/
const home = require('./controller/home');
module.exports = (router) => {
router.get('/',home.renderHtml);
router.get('/page2',home.renderHtml);
router.get('/favicon.ico',home.favicon);
router.get('/test',home.test);
}
Шаблоны процессов
Будуhtml
Вставлять его в код не очень дружелюбно, поэтому здесь также представлен шаблон сервиса.koa-nunjucks-2
.
В то же время на нем размещен слой промежуточного программного обеспечения для передачи параметров и обработки различных ссылок на статические ресурсы.
...
const koaNunjucks = require('koa-nunjucks-2');
...
/**
* 服务器渲染,渲染HTML,渲染模板
* @param {*} ctx
*/
function renderServer(ctx) {
return (fileName, defineParams) => {
let obj = handleLink(fileName, ctx.req, defineParams);
// 处理自定义参数
defineParams = String(defineParams) === "[object Object]" ? defineParams : {};
obj = Object.assign(obj, defineParams);
ctx.render('index', obj);
}
}
...
/**
* 模板渲染
*/
app.use(koaNunjucks({
ext: 'html',
path: path.join(process.cwd(), 'app/view'),
nunjucksConfig: {
trimBlocks: true
}
}));
/**
* 渲染Html
*/
app.use(async (ctx, next) => {
ctx.renderServer = renderServer(ctx);
await next();
});
Когда пользователь обращается к серверу, вызываяrenderServer
функция, обработка цепочки, выполнение до конца, вызовctx.render
Завершить рендеринг.
/**
* 渲染react页面
*/
exports.renderHtml = async (ctx) => {
let initState = ctx.query.state ? JSON.parse(ctx.query.state) : null;
ctx.renderServer("page1", {store: JSON.stringify(initState ? initState : { counter: 1 }) });
}
exports.favicon = (ctx) => {
ctx.body = null;
}
exports.test = (ctx) => {
ctx.body = {
data: `测试数据`
}
}
оkoa-nunjucks-2
в рендерингеHTML
, будут< >
Поэтому для обеспечения безопасности нам также необходимо фильтровать данные, которые мы передаем.
<!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>koa-React服务器渲染</title>
{{ link | safe }}
</head>
<body>
<div id='app'>
{{ html | safe }}
</div>
</body>
<script>
window.__INIT_STORE__ = {{ store | safe }}
</script>
{{ script | safe }}
</html>
структура документа
├── README.md
├── app //node端业务代码
│ ├── build
│ │ ├── page1.js
│ │ └── page2.js
│ ├── controller
│ │ └── home.js
│ ├── router.js
│ └── view
│ └── index.html
├── index.js
├── package.json
├── public //前端静态资源
│ ├── manifest.json
│ ├── page1.css
│ ├── page1.js
│ ├── page2.css
│ └── page2.js
├── web //前端源码
│ ├── action //redux -action
│ │ └── count.js
│ ├── components //组件
│ │ └── layout
│ │ └── index.jsx
│ ├── pages //主页面
│ │ ├── page
│ │ │ ├── index.jsx
│ │ │ └── index.less
│ │ └── page2
│ │ ├── index.jsx
│ │ └── index.less
│ ├── reducer //redux -reducer
│ │ ├── counter.js
│ │ └── index.js
│ ├── render //webpack入口文件
│ │ ├── clientRouter.js
│ │ └── serverRouter.js
│ ├── router.js //前端路由
│ └── store //store
│ └── index.js
└── webpack.config.js
наконец
В настоящее время эту архитектуру можно запустить только вручную.Koa
обслуживание и запускwebpack
.
Если вам нужно запустить Koa и webpack вместе, здесь затронута другая тема, вы можете проверить статью, которую я написал в начале, здесь.
"Сао Ниан, а как насчет Коа и Вебпака?》
Если вам нужно понять, что функционирует полный сервер, проверьте мою предыдущую статью.
"Как создать надежный и стабильный веб-сервер》
наконецGITHUB
Адрес следующий:
Рендеринг сервера React на основе koa
Использованная литература:
"Реагировать на китайскую документацию》
"Документация Webpack на китайском языке》
"Принцип изоморфизма (SSR) в React》
"Redux》