Недавно реализован прямолинейный изоморфный шаблон React.React Isomophic, из коробки.
Этот шаблон поддерживает Koa2 + React + React Router + Redux + Less.
Почему рендеринг на стороне сервера и изоморфизм
В традиционной модели разработки SPA весь рендеринг страницы размещается на стороне клиента, что привело к некоторым болевым точкам, которые было трудно решить: долгое время белого экрана на первом экране, недружелюбное SEO и т. д.
И рендеринг на стороне сервера может решить эти традиционные болевые точки:
- Сервер напрямую выводит HTML-документ, облегчая поисковым системам чтение содержимого страницы, что выгодно для SEO.
- Страница может отображаться напрямую без необходимости выполнения клиентом JS, что значительно сокращает время белого экрана страницы.
Преимущества рендеринга на стороне сервера были описаны выше, но как насчет изоморфизма?
Благодаря появлению Nodejs сервер также имеет возможность запускать JavaScript, так что React, который мы изначально запускали на внешнем интерфейсе, также может работать на внутреннем. Оба конца имеют общий набор логического кода, что снижает объем разработки и позволяет избежать несоответствия внешнего и внутреннего рендеринга страниц.
Поддержка React API
React предоставляет четыре API для вывода виртуального DOM в виде текста HTML.
Первые два метода доступны как на стороне браузера, так и на стороне сервера:
Следующие два метода будут выводить текст в виде потока, поэтому их можно запускать только на стороне сервера.
Разница между методом renderToString и renderToStaticMarkup:
Последний не добавит никаких свойств React в созданный узел DOM, а это означает, что созданная страница не будет иметь адаптивных функций.renderToStaticMarkup
Метод подходит для создания чисто статических страниц.
Мне нужно продолжать использовать адаптивные функции React на стороне клиента, поэтому я выбралrenderToString
метод.
Пусть сервер поддерживает JSX
Невозможно запустить JSX напрямую в среде Node, но мы можем использовать babel-register для поддержки файлов формата jsx на стороне сервера.
Введите babel-register в самом начале всей программы node:
// app.js
require('babel-register')({
presets: [
'es2015',
'react',
'stage-2',
],
plugins: [
['transform-runtime', {
polyfill: false,
regenerator: true,
}],
],
extensions: ['.jsx', '.js'],
});
const Koa = require('koa')
const app = new Koa();
//...
//...
Это будет нормально обрабатывать файлы с суффиксом .jsx и синтаксисом jsx. Он также поддерживает синтаксис ES6, такой как импорт.
Строительство каркаса проекта
Первый шаг в реализации рендеринга на стороне сервера начинается с создания внутреннего проекта. Чтобы снизить стоимость строительства Node-проектов, здесь я рекомендую использоватьkoa-generator.
На основе скаффолдинга создайте три новые папки:
-
/build
: Конфигурация запуска Webpack, в основном используемая для упаковки статических ресурсов, необходимых браузеру. -
/common
: В основном компоненты React и логический код. Эта часть кода является общей для фронтенда и бекенда. Обычно мы выставляем корневой компонент React как вход во всю общую папку. -
/client
: файл, который работает только на стороне браузера.
├── app.js # 程序入口文件
├── bin
│ └── www # 程序启动脚本
├── build # Webpack 配置,用于打包前端静态资源
├── client # 只在浏览器端运行的代码
| └── index.jsx # 浏览器端的入口文件
├── common # 客户端和服务端共享代码, React 同构代码
| └── App.jsx # React 根组件
├── controllers # Controllers
├── public # 静态资源文件
├── routes # Koa 路由
└── views # 页面模板文件
└── index.jsx # ejs 渲染模板
Реализовать рендеринг на стороне сервера
Реализация рендеринга на стороне сервера React в одиночку проста: используйте React’srenderToString
API выводит виртуальный DOM в виде строки HTML, а затем возвращает ее браузеру, когда браузер запрашивает страницу.
// Node 服务端 ./routes/index.jsx
import Router from 'koa-router';
import { renderToString } from 'react-dom/server';
import App from '../common/App';
const router = Router();
/**
* 为了配合浏览器端的 React-Router 中 BrowserRouter 路由
* 我们使用了 '*' 来匹配任何 URL,以返回同样的 HTML。
*/
router.get('/', async (ctx) => {
// 因为使用了 babel-register,因此我们可以直接使用 jsx 语法。
// 将根组件 App 输出为 html 字符串。
const content = renderToString(<App/>);
await ctx.render('index', {
html: content, // 将 html 字符串通过 ejs 模板渲染出来
});
});
export default router;
Соответствующий файл index.ejs:
<!-- index.ejs -->
<!DOCTYPE html>
<html>
<head>
<title>React Isomorphic</title>
<link rel='stylesheet' href='/css/style.css' />
</head>
<body>
<!-- koa-router 将 react 虚拟dom 输出为字符串,渲染在 html 变量中 -->
<div id="app"><%- html %></div>
<script src="/js/app.js"></script>
</body>
</html>
Таким образом, когда браузер запрашивает страницу, сервер возвращает HTML с полной структурой DOM.
Рендеринг на стороне сервера отвечает только за рендеринг содержимого первого экрана.Часто после рендеринга первого экрана на странице все еще будет много взаимодействия и асинхронных операций.Поэтому нам все еще нужно упаковать JS и внедрить соответствующий HTML в том же, что и в предыдущих файлах для разработки интерфейса.JS.
/client/index.jsx
Что касается входного JS-файла всего браузера, его содержание примерно следующее:
// ./client/index.ejs
import React, { Component } from 'react';
import { hydrate } from 'react-dom';
import { BrowserRouter, withRouter } from 'react-router-dom';
// 用 hydrate 来替代 render
hydrate(
<App></App>,
document.querySelector('#app'),
);
React v16 предоставляет новый API рендеринга для оптимизации рендеринга верхней части сгиба специально для рендеринга на стороне сервера:ReactDOM.hydrate(Если на узле есть разметка, визуализируемая сервером, React сохранит существующую модель DOM и привяжет только обработчики событий для достижения оптимальной производительности рендеринга в верхней части страницы). Мы будем использовать гидрат вместо рендера.
На этом простейший рендеринг на стороне сервера полностью построен.
Использование с React-маршрутизатором
React Router на стороне браузера поддерживает два режима:
- Hash Router: Маршрутизатор на основе хэша URL.
- Маршрутизатор браузера: это больше похоже на настоящий переход по ссылке, но требует поддержки сервера.
Здесь я выбрал Browser Router, который даст мне более реалистичные ощущения при прыжках с React Router.
Здесь нам нужно разобраться со стороной браузера и со стороной сервера отдельно, для стороны браузера:
// ./client/index.ejs
import React, { Component } from 'react';
import { hydrate } from 'react-dom';
import { BrowserRouter, withRouter } from 'react-router-dom';
// 用 hydrate 来替代 render
hydrate(
<BrowserRouter>
<App></App>
</BrowserRouter>,
document.querySelector('#app'),
);
React-Router предоставляет выделенный StaticRouter для рендеринга на стороне сервера.Мы передаем запрошенный браузером путь к странице в качестве параметра в параметре местоположения StaticRouter, и StaticRouter поможет нам вернуть различные узлы виртуального дома. Затем мы используемrenderToString
метод преобразования в строку html.
В то же время нам также необходимоrouter.get('/')
изменить наrouter.get('*')
. Запрос будет иметь любой путь, который входит в маршруты процесса.
// Node /routes/index.jsx
import Router from 'koa-router';
import { renderToString } from 'react-dom/server';
import { StaticRouter } from 'react-router-dom'
const router = Router();
/**
* 为了配合浏览器端的 React-Router 中 BrowserRouter 路由
* 我们使用了 '*' 来匹配任何来自浏览器的请求,这样保证了任何路径的请求都将得到该路由的处理。
*/
router.get('*', async (ctx) => {
const context = {};
const content = (
<StaticRouter location={ctx.url} context={context}>
<App/>
</StaticRouter>
);
await ctx.render('index', {
html: content,
});
});
export default router;
Рендеринг на стороне сервера с помощью Redux
Идея реализации рендеринга на стороне сервера Redux также очень понятна, то есть когда Store инициализируется на стороне сервера, Store также передается клиенту.
Шаг 1. Создайте начальный магазин на сервере
Расширьте файл маршрутизации Koa:
// server-side ./routes/index.jsx
import Router from 'koa-router';
import { renderToString } from 'react-dom/server';
import { StaticRouter } from 'react-router-dom';
import { Provider } from 'react-redux';
import { createStore } from 'redux'
const router = Router();
router.get('*', async (ctx) => {
// 在服务端初始化 store 的数据
const store = createStore(state => state, {
name: 'Pspgbhu',
site: 'http://pspgbhu.me',
});
const context = {};
const content = (
<StaticRouter location={ctx.url} context={context}>
{/* 增加 Provider */}
<Provider store={store}>
<App/>
</Provider>
</StaticRouter>
);
// 获取 store 数据对象
const preloadedState = store.getState();
await ctx.render('index', {
html: content,
state: preloadedState, // 将 store 数据传递给 ejs 模板引擎
});
});
export default router;
Шаг 2: Механизм шаблонов отображает исходное хранилище на странице.
Механизм шаблонов назначает данные хранилища с маршрутизатора koa наwindow.__INITIAL_STATE_
под объект.
<!-- index.ejs -->
<!DOCTYPE html>
<html>
<head>
<title>React Isomorphic</title>
<link rel='stylesheet' href='/css/style.css' />
</head>
<body>
<div id="app"><%- html %></div>
<script>
// 将服务端的 store 对象赋值给该变量
window.__INITIAL_STATE_ = <%- state %>;
</script>
<script src="/js/app.js"></script>
</body>
</html>
Шаг 3: Клиент получает начальное значение хранилища Redux.
// client-side index.jsx
import React from 'react';
import { hydrate } from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import App from '../common/App';
// 通过服务端注入的全局变量得到初始的 state
const preloadedState = window.__INITIAL_STATE_;
const store = createStore(state => state, preloadedState);
hydrate(
<Provider store={store}>
<BrowserRouter>
<App></App>
</BrowserRouter>
</Provider>,
document.querySelector('#app'),
);
Работа с файлами стилей
Обычно, когда мы разрабатываем интерфейс, мы напрямую внедряем css, less и другие файлы стилей в jsx.
// app.jsx
import './style/app.less';
import './style/style.css';
Однако сервер не может правильно обработать эти файлы, что приведет к тому, что сервер сообщит об ошибке. Отрисовка самого HTML-документа не требует участия стилей CSS, поэтому мы можем найти способ игнорировать эти файлы.
Мы можем игнорировать некоторые файлы с фиксированными суффиксами через плагин babel-register babel-plugin-transform-require-ignore. Давайте дополним конфигурацию bable-register в app.js.
// app.js
require('babel-register')({
presets: [
'es2015',
'react',
'stage-2',
],
plugins: [
['transform-runtime', {
polyfill: false,
regenerator: true,
}],
// babel-plugin-transform-require-ignore 插件
// 可以使 node 忽略一些固定的后缀文件。
[
'babel-plugin-transform-require-ignore', {
extensions: ['.less', '.sass', '.css'],
},
],
],
extensions: ['.jsx', '.js'],
});
Пакетирование статических ресурсов в средах разработки и производства
Приведенный выше контент посвящен рендерингу на стороне сервера.Когда страница выполняется на стороне браузера, статические ресурсы JS по-прежнему необходимо загружать в браузер. Обычно мы используем Webpack для упаковки этих статических ресурсов.
Статические ресурсы в производственной среде
Упаковка статических ресурсов в рабочей среде очень проста: вы можете напрямую использовать webpack для упаковки соответствующих JS и CSS, а затем напрямую ссылаться на соответствующие статические ресурсы в шаблоне ejs.
Статические ресурсы в среде разработки
В среде разработки, поскольку код проекта необходимо часто менять, горячее обновление кода очень необходимо. Горячее обновление кода Node на стороне сервера можно легко выполнить с помощью nodemon.
Для горячего обновления внешних статических ресурсов я использую веб-пакет для просмотра соответствующего исходного кода.После обнаружения каждого изменения кода веб-пакет автоматически перестраивает код.
Кроме того, в среде разработки я упакую все статические ресурсы в/.dev
каталог вместо/build
Под содержанием. И установите следующее в app.js
/**
* 非生产环境下引用 .dev 目录作为静态资源目录。
* .dev 目录下的资源会优先于 public 文件下的资源。
*/
if (process.env.NODE_ENV !== 'production') {
app.use(serve(path.join(__dirname, '.dev')));
}
app.use(serve(path.join(__dirname, 'public')));
Это отличает ссылки на статические ресурсы в разных средах.
Некоторые моменты, на которые следует обратить внимание
1. Различия в жизненном цикле
На основе изоморфизма, несмотря на то, что и сервер Node, и браузер совместно используют весь/common
Компоненты React в каталоге, но все же есть некоторые отличия в конкретной операции.
На сервере узла наступает жизненный цикл компонента только.componentWillMount()
.
Со стороны браузера компоненты имеют полный жизненный цикл.
2. В среде Node нет других глобальных объектов или методов, специфичных для браузера, таких как объект окна.
Обычно мы используем объект окна и некоторые другие специфичные для браузера глобальные объекты (такие как документ и т. д.) более или менее, когда мы занимаемся интерфейсной разработкой. Эти два объекта недоступны в среде Node. Это вызовет ошибку. на стороне узла.
Так нельзя ли использовать эти объекты в компонентах? конечно, нет. Как упоминалось выше, в среде Node жизненный цикл компонента может идти только доcomponentWillMount()
, и последующие функции жизненного цикла вызываться не будут. Таким образом, мы можем поместить эти специфичные для браузера объекты и методы вcomponentWillMount()
Используется в последующих функциях жизненного цикла, например, вcomponentDidMount()
используется вdocument.querySelector('#app')
метод.
напиши в конце
В целом, поскольку React и его семейство сегментов очень хорошо поддерживают рендеринг на стороне сервера, общая реализация рендеринга на стороне сервера относительно проста. И реализация тоже очень гибкая, уж точно не только такое мышление в этой статье.
Также недавно рефакторил мойблог, основан на рендеринге на стороне сервера React, опыт по-прежнему хорош, вы можете испытать его.