缺少任何有关如何为React应用程序进行服务器端渲染的完整手册,促使撰写本文。
当我遇到这个问题时,我可以使用
Next.js框架或使用
Express.js来选择两个选项。
为了在现成的大型OTT平台上购买它,Next.js花费了大约100个小时进行调查,但是由于问题太多,我们拒绝了它(我会再写一篇文章),可以选择
Express.js这样的小版本,关于我想告诉的。
本文演示的完整代码在
这里 。
让我们从初始任务以及我们所拥有的开始。
当时我们有:
任务:
- 该解决方案应使用数据完全呈现呈现的HTML
- 该解决方案应将对现有代码的影响降到最低,即体系结构应保持最少的更改。
- 继续使用项目中已经存在的库
- SEO元数据应与页面一起来自服务器
- 图像,样式和字体应上传到服务器并发送给客户端,而无需后续下载
- 客户端动态导入支持
- 避免重复代码以在服务器上进行预加载以及在客户端进行导航时
我们已经确定了任务,让我们弄清楚如何做到这一点。
从react的文档中,我们可以发现对于SSR,您可以使用
renderToString()和
hydrate()方法,但是下一步该怎么做?
renderToString-用于在我们的应用程序的服务器上生成HTML。
水合物 -用于在客户端和服务器上进行通用渲染。
资料载入
要在服务器端下载数据,我们使用
redux-connect库,该库允许您在调用第一个渲染之前下载所需的数据,这是我们需要的。 为此,请使用hoc asyncConnect。 在服务器端,它加载数据,并在路由时充当componentDidMount。
@asyncConnect([ { key: 'usersFromServer', promise: async ({ store: { dispatch } }) => { await dispatch(getUsersData()); return Promise.resolve(); }, }, ])
我们需要在服务器端创建一个Redux存储。 一切照常,只是在server.js文件中创建。
同样在服务器端,使用
redux-connect中的loadOnServer方法
,我们等待数据预加载。
使用renderToString,我们可以获取数据应用程序的HTML。
我们访问的数据可以使用getState()检索,并通过<script />标记添加到全局窗口对象。 然后从客户端上我们将获取数据并将其设置在门中。
看起来像这样
app.get('*', (req, res) => { const url = req.originalUrl || req.url; const history = createMemoryHistory({ initialEntries: [url], }); const store = configureStore(initialState, history); const location = parseUrl(url); const helpers = {}; const indexFile = path.resolve('./build/main.html'); store.runSaga(sagas).toPromise().then(() => { return loadOnServer({ store, location, routes, helpers }) .then(() => { const context = {}; if (context.url) { req.header('Location', context.url); return res.send(302) } const css = new Set();
2个道具传递给ReduxAsyncConnect组件:
第一个是我们的路线,第二个是助手(辅助功能),我们希望在整个应用程序中都可以访问它们,这是上下文的一种模拟。
对于服务器路由,请使用StaticRouter。
头盔库用于添加seo meta标签。 每个页面组件都有一个带标签的描述。
为了使标签立即从服务器发出,我们使用了
const helmet = Helmet.renderStatic(); helmet.title.toString() helmet.meta.toString()
路由必须重写为对象数组,看起来像这样。
export const StaticRoutesConfig = [ { key: 'usersGender', component: UsersGender, exact: true, path: '/users-gender/:gender', }, { key: 'USERS', component: Users, exact: true, path: '/users', }, { key: 'main', component: Users, exact: true, path: '/', }, { key: 'not-found', component: NotFound, }, ];
根据到达服务器的URL,React路由器返回所需的数据页面。
客户是什么样的?
这是主要的客户端文件。 您可以为客户端的每个页面添加搜索引擎分析和必须执行的代码。
浏览器/ index.js import 'babel-polyfill'; import { browserRender } from '../app/app'; browserRender();
App.js文件
const initialState = !process.env.IS_SERVER ? window.__INITIAL_DATA__ : {}; const history = process.env.IS_SERVER ? createMemoryHistory({ initialEntries: ['/'], }) : createBrowserHistory(); const store = configureStore(initialState, history); if (!process.env.IS_SERVER) { window.store = store; } const insertCss = (...styles) => {
对于路由,使用
connected-react-router / immutable中的ConnectedRouter。
对于服务器端渲染,我们不能使用
ract-router-dom并通过Switch正确描述我们的路由:
<Switch> <Route path="/about"> <About /> </Route> <Route path="/users"> <Users /> </Route> <Route path="/"> <Home /> </Route> </Switch>
取而代之的是,如前所述,我们有一个包含所描述路由的数组,为了将它们添加到应用程序中,我们需要使用
react-router-config :
App / index.js import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { Helmet } from 'react-helmet'; import { renderRoutes } from 'react-router-config'; import { getRouterLocation } from './selectors/router'; @connect(state => ({ location: getRouterLocation(state), }), null) export default class App extends Component { static propTypes = { location: PropTypes.shape().isRequired, route: PropTypes.shape().isRequired, }; render() { const { route } = this.props; return ( <div> {renderRoutes(route.routes)} </div> ); } }
在服务器上下载carnet,样式和字体
对于样式,使用了
isomorphic-style-loader ,因为通常的
slyle-loader在目标为“ node”的webpack中不起作用。
他将所有样式添加到DOM,因此,现成的漂亮页面来自服务器。 您可以将样式保存在单独的文件中,以便浏览器可以缓存样式。
为了在服务器上显示图像和下载字体,使用了webpack loader
base64-inline-loader 。
{ test: /\.(jpe?g|png|ttf|eot|otf|svg|woff(2)?)(\?[a-z0-9=&.]+)?$/, use: 'base64-inline-loader?limit=1000&name=[name].[ext]', },
它包含在图像和我们使用的所有类型的字体中。 结果,我们从base64服务器收到了一个代码,该代码显示了带有字体和图像的页面,而没有随后的加载。
对于客户端构建,使用了常规的
url-loader和
file-loader 。
{ test: /\.(eot|svg|otf|ttf|woff|woff2)$/, use: 'file-loader', }, { test: /\.(mp4|webm|png|gif)$/, use: { loader: 'url-loader', options: { limit: 10000, }, }, },
是的,这增加了已加载页面的大小,但是就下载速度而言,这对于用户而言并不是很明显。
在服务器上动态导入
React.js使用
React.lazy和React.suspense动态导入和显示加载器,但是它们不适用于SSR。
我们使用
react-loadable ,它做同样的事情。
import Loadable from 'react-loadable'; import Loader from './Loader'; const LoadableComponent = Loadable({ loader: () => import('./my-component'), loading: Loader, }); export default class App extends React.Component { render() { return <LoadableComponent/>; } }
在客户端上,此代码在服务器上显示加载程序,为了加载模块,您需要添加以下代码:
Loadable.preloadAll().then(() => { app.listen(PORT, () => { console.log(` Server is listening on port ${PORT}`); }); });
Loadable.preloadAll()-返回表示已加载模块的承诺。
所有亮点均已解决。
我使用
本文中
介绍的所有内容进行了一个微型演示。