Express.js上的React App的服务器端渲染

缺少任何有关如何为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(); // CSS for all rendered React components const insertCss = (...styles) => styles.forEach(style => css.add(style._getCss())); const dynamicRoutes = [...routes]; dynamicRoutes[0].routes = [...dynamicRoutes[0].routes, ...StaticRoutesConfig]; const appContent = ReactDOMServer.renderToString( <StyleContext.Provider value={{ insertCss }}> <Provider store={store} key="provider"> <StaticRouter location={location} context={context}> <ReduxAsyncConnect routes={dynamicRoutes} helpers={helpers}/> </StaticRouter> </Provider> </StyleContext.Provider> ); const helmet = Helmet.renderStatic(); fs.readFile(indexFile, 'utf8', (err, data) => { if (err) { console.log('Something went wrong:', err); return res.status(500).send('Oops, better luck next time!'); } data = data.replace('__STYLES__', [...css].join('')); data = data.replace('__LOADER__', ''); data = data.replace('<div id=app></div>', `<div id=app>${appContent}</div>`); data = data.replace('<div id="app"></div>', `<div id="app">${appContent}</div>`); data = data.replace('<title></title>', helmet.title.toString()); data = data.replace('<meta name="description" content=""/>', helmet.meta.toString()); data = data.replace('<script>__INITIAL_DATA__</script>', `<script>window.__INITIAL_DATA__ = ${JSON.stringify(store.getState())};</script>`); return res.send(data); }); }); store.close(); }); }); 

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) => { // eslint-disable-next-line no-underscore-dangle const removeCss = styles.map(style => style._insertCss()); return () => removeCss.forEach(dispose => dispose()); }; export const browserRender = () => { const dynamicRoutes = [...routes]; dynamicRoutes[0].routes = [...dynamicRoutes[0].routes, ...StaticRoutesConfig]; hydrate( <StyleContext.Provider value={{ insertCss }}> <Provider key="provider" store={store} > <ConnectedRouter history={history}> <ReduxAsyncConnect helpers={{}} routes={dynamicRoutes} /> </ConnectedRouter> </Provider> </StyleContext.Provider>, document.getElementById('app'), ); }; 

对于路由,使用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-loaderfile-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()-返回表示已加载模块的承诺。

所有亮点均已解决。

我使用本文介绍的所有内容进行了一个微型演示。

Source: https://habr.com/ru/post/zh-CN473210/


All Articles