【个人网站】本文中用到的技术ReactV16|

优采云 发布时间: 2021-08-04 18:26

  【个人网站】本文中用到的技术ReactV16|

  欢迎访问个人网站:

  前言

  这个文章是我自己搭建自己的网站的过程,使用了服务端渲染,看了一些教程,踩了一些坑。我想分享这个过程。

  我会尽量把每一步都解释清楚,把我理解的都说出来。

  文章中的示例代码来自这个仓库,也是我正在搭建的个人网站。你可以一起分享。由于简化,示例代码与仓库代码略有不同。另外,我也在README中记录了在使用服务端渲染过程中遇到的一些坑。

  本文使用的技术

  React V16 |反应路由器 v4 |还原 | Redux-thunk |快递

  React 服务端渲染

  服务端渲染的基本套路是当用户请求时,在服务端生成一个我们想要看到的网页内容的HTML字符串返回给浏​​览器显示。

  浏览器拿到HTML后,渲染页面,但是没有事件交互。这时候浏览器发现html里面加载了一些js文件(也就是浏览器端渲染的js),直接加载了。

  加载并执行后,事件会被绑定。此时页面被浏览器接管。也就是我们熟悉的js渲染页面的过程。

  要实现的目标:通用渲染方法的优缺点

  服务端渲染解决了首屏加载速度慢和SEO不友好的缺点(谷歌已经可以检索浏览器渲染的网页,但不是所有搜索引擎都可以)

  但它增加了项目的复杂性并增加了维护成本。

  如非必要,尽量不要使用服务端渲染

  总体思路

  需要两端:服务器端和浏览器端(浏览器渲染的部分)

  首先:打包浏览器端代码

  二:打包服务端代码,启动服务

  第三:用户访问,服务器读取浏览器端打包的index.html文件为字符串,将渲染的组件、样式、数据塞进html字符串,返回给浏览器

  第四:浏览器直接渲染接收到的html内容,加载打包好的浏览器端js文件,进行事件绑定,初始化状态数据,完成同构

  React 组件的服务端渲染

  来看看最简单的 React 服务端渲染过程。

  对于服务端渲染,必须需要一个根组件来生成 HTML 结构

  import React from 'react';

import ReactDOM from 'react-dom';

ReactDOM.hydrate(, document.getElementById('root'));

  当然,这里也可以使用 ReactDOM.render,但是 hydr 会尝试重用接收到的服务器返回的内容。

  在浏览器端补充事件绑定等独特的流程

  引入浏览器端需要渲染的根组件,并使用react的renderToString API进行渲染

  import { renderToString } from 'react-dom/server'

import Container from '../containers'

// 产生html

const content = renderToString()

const html = `

${content}

`

res.send(html)

  这里的renderToString也可以换成renderToNodeStream。区别在于前者是同步生成HTML,即如果生成HTML需要1000毫秒,

  然后内容会在1000毫秒后返回给浏览器,显然时间太长了。后者是流的形式,渲染结果塞进响应对象,和出来一样多

  返回给浏览器多少可以相对减少时间消耗

  路由的服务端渲染

  正常情况下,我们的应用不可能只有一页,肯定会有路由跳转。我们一般是这样使用的:

  import { BrowserRouter, Route } from 'react-router-dom'

const App = () => (

{/*...Routes*/}

)

  但这是在浏览器端渲染时的用法。做服务端渲染时,需要用StaticRouter替换BrowserRouter

  不同的是,BrowserRouter 会使用 HTML5 提供的历史 API 来保持页面与 URL 同步,而 StaticRouter

  网址不会改变

  import { createServer } from 'http'

import { StaticRouter } from 'react-router-dom'

createServer((req, res) => {

const html = renderToString(

)

})

  这里,StaticRouter 会收到两个属性:

  Redux 同构

  数据的预获取、脱水和注水是服务端渲染的难点。

  这是什么意思?也就是说,第一屏渲染的网页一般需要请求外部数据。我们希望在生成 HTML 之前获取此页面所需的所有数据。

  然后把它塞进页面。这个过程称为“脱水”,生成 HTML 并将其返回给浏览器。浏览器获取带有数据的 HTML,

  去请求浏览器端js,接管页面,用这个数据初始化组件。这个过程被称为“水合”。完成服务器和浏览器数据的统一。

  你为什么要这样做?试想一下,如果没有数据的预取,直接返回一个没有数据只有固定内容的HTML结构会是什么结果?

  第一:由于页面上没有有效信息,不利于SEO。

  第二:由于返回的页面没有内容,但是浏览器端JS接管页面,然后返回请求数据渲染数据,页面会闪退,用户体验不好。

  我们使用Redux来管理状态,因为有服务端代码和浏览器端代码,那么需要两个store来分别管理服务端和浏览器端的数据。

  组件配置

  组件在服务端渲染时需要请求数据。可以在组件上挂载一个特殊的异步请求方法,这里称为loadData,接收服务器端的store作为参数。

  然后store.dispatch在服务器端扩展store。

  class Home extends React.Component {

componentDidMount() {

this.props.callApi()

}

render() {

return {this.props.state.name}

}

}

Home.loadData = store => {

return store.dispatch(callApi())

}

const mapState = state => state

const mapDispatch = {callApi}

export default connect(mapState, mapDispatch)(Home)

  路由重构

  因为服务端需要根据路由判断当前渲染的是哪个组件,所以此时可以发送异步请求。所以路由也需要配置支持loadData方法。在服务端渲染时,

  路由的渲染可以使用react-router-config库,用法如下(重点在路由上挂载loadData方法):

  import { BrowserRouter } from 'react-router-dom'

import { renderRoutes } from 'react-router-config'

import Home from './Home'

export const routes = [

{

path: '/',

component: Home,

loadData: Home.loadData,

exact: true,

}

]

const Routers =

{renderRoutes(routes)}

  从服务器获取数据

  到达服务器时,需要判断匹配路由中的所有组件是否都有loadData方法,如果有则调用。

  在服务器端传入商店,扩展服务器端的商店。同时需要注意的是,一个页面可能由多个组件组成,每个请求都会被发送,这意味着我们必须等待所有的请求发送完才能返回HTML。

  import express from 'express'

import serverRender from './render'

import { matchRoutes } from 'react-router-config'

import { routes } from '../routes'

import serverStore from "../store/serverStore"

const app = express()

app.get('*', (req, res) => {

const context = {css: []}

const store = serverStore()

// 用matchRoutes方法获取匹配到的路由对应的组件数组

const matchedRoutes = matchRoutes(routes, req.path)

const promises = []

for (const item of matchedRoutes) {

if (item.route.loadData) {

const promise = new Promise((resolve, reject) => {

item.route.loadData(store).then(resolve).catch(resolve)

})

promises.push(promise)

}

}

// 所有请求响应完毕,将被HTML内容发送给浏览器

Promise.all(promises).then(() => {

// 将生成html内容的逻辑封装成了一个函数,接收req, store, context

res.send(serverRender(req, store, context))

})

})

  细心的同学可能已经注意到,我已经将上面的每个 loadData 都包裹在了一个 promise 中。

  const promise = new Promise((resolve, reject) => {

item.route.loadData(store).then(resolve).catch(resolve)

console.log(item.route.loadData(store));

})

promises.push(promise)

  这是为了容错。一旦请求出错,下面的 Promise.all 方法就不会被执行,所以包裹一层 promise 的目的是即使请求出错也能解决,不会影响 Promise.all 方法。

  也就是说只有有错误请求的组件才会有数据,其他组件不受影响。

  注入数据

  我们的请求已经发出了,在组件的loadData方法中也扩展了服务端的存储,这样就可以把服务端的数据取出来注入HTML返回给浏览器了。

  看serverRender方法

  const serverRender = (req, store, context) => {

// 读取客户端生成的HTML

const template = fs.readFileSync(process.cwd() + '/public/static/index.html', 'utf8')

const content = renderToString(

)

// 注入数据

const initialState = `

window.context = {

INITIAL_STATE: ${JSON.stringify(store.getState())}

}

`

return template.replace('', content)

.replace('', initialState)

}

  浏览器使用从服务器获取的数据来初始化store

  经过上面的过程,我们已经可以从window.context中获取到服务端预取的数据了。这个时候我们需要做的就是利用这些数据来初始化浏览器端的存储。保证两端数据的统一。

  import { createStore, applyMiddleware, compose } from 'redux'

import thunk from 'redux-thunk'

import rootReducer from '../reducers'

const defaultStore = window.context && window.context.INITIAL_STATE

const clientStore = createStore(

rootReducer,

defaultStore,// 利用服务端的数据初始化浏览器端的store

compose(

applyMiddleware(thunk),

window.devToolsExtension ? window.devToolsExtension() : f=>f

)

)

  到此,服务端渲染的数据统一问题已经解决了,再来回顾一下整个过程:

  这里还有一点,就是当我们从路由进入其他页面时,不会执行组件中的loadData方法。只会在服务器刷新并渲染路由时执行。

  此时将没有数据。所以我们还是需要在componentDidMount中发送一个请求来解决这个问题。因为componentDidMount不会在服务端渲染和执行,

  所以不要担心重复请求。

  时尚的服务端渲染

  我们上面所做的只是让网页的内容在服务端渲染出来,但是在浏览器加载css之前不会添加样式,所以一开始返回的网页内容是没有样式的,页面仍然会闪烁。为了解决这个问题,我们需要让样式在服务端渲染的时候也返回。

  首先,在服务端渲染的时候,解析css文件的时候,不能使用style-loader,而应该使用isomorphic-style-loader。

  {

test: /\.css$/,

use: [

'isomorphic-style-loader',

'css-loader',

'postcss-loader'

],

}

  但是如何在服务端获取当前路由中的组件样式呢?回想一下我们在做路由的服务端渲染的时候,我们使用了StaticRouter,它会接收一个context对象,这个context对象可以作为一个载体来传递一些信息。用起来吧!

  思路是在渲染组件的时候接收组件中的context对象,获取组件样式,放到context中,服务端获取样式,插入到返回的HTML中的style标签中。

  让我们看看组件是如何读取样式的:

  import style from './style/index.css'

class Index extends React.Component {

componentWillMount() {

if (this.props.staticContext) {

const css = styles._getCss()

this.props.staticContext.css.push(css)

}

}

}

  路由中的组件可以在props中接收staticContext,也就是StaticRouter传递过来的上下文,

  isomorphic-style-loader 提供了一个 _getCss() 方法,它允许我们读取 css 样式并将其放入 staticContext 中。

  不在路由中的组件可以通过父组件传递props方法,或者用react-router的withRouter包裹起来

  其实这部分css提取逻辑可以写成高层组件,方便复用

  import React, { Component } from 'react'

export default (DecoratedComponent, styles) => {

return class NewComponent extends Component {

componentWillMount() {

if (this.props.staticContext) {

const css = styles._getCss()

this.props.staticContext.css.push(css)

}

}

render() {

return

}

}

}

  在服务器端,组件渲染后,上下文中已经有内容了。这时候我们处理样式返回给浏览器,然后样式就可以在服务端渲染了

  const serverRender = (req, store) => {

const context = {css: []}

const template = fs.readFileSync(process.cwd() + '/public/static/index.html', 'utf8')

const content = renderToString(

)

// 经过渲染之后,context.css内已经有了样式

const cssStr = context.css.length ? context.css.join('\n') : ''

const initialState = `

window.context = {

INITIAL_STATE: ${JSON.stringify(store.getState())}

}

`

return template.replace('', content)

.replace('server-render-css', cssStr)

.replace('', initialState)

}

  到此,服务端渲染完成。

  总结

  React 服务端渲染的最佳解决方案是 Next.js。如果你的应用不需要SEO优化,或者不太注重首屏渲染的速度,那么尽量不要使用服务端渲染。

  因为它会使项目复杂化。另外,除了服务端渲染,还有很多优化SEO的方式,比如预渲染。

0 个评论

要回复文章请先登录注册


官方客服QQ群

微信人工客服

QQ人工客服


线