国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 編程 > JavaScript > 正文

詳解React服務(wù)端渲染從入門到精通

2019-11-19 11:55:53
字體:
供稿:網(wǎng)友

前言

這篇文章是我自己在搭建個人網(wǎng)站的過程中,用到了服務(wù)端渲染,看了一些教程,踩了一些坑。想把這個過程分享出來。
我會盡力把每個步驟講明白,將我理解的全部講出來。

文中的示例代碼來自于這個倉庫,也是我正在搭建的個人網(wǎng)站,大家可以一起交流一下。示例代碼因為簡化,與倉庫代碼有些許出入

本文中用到的技術(shù)
React V16 | React-Router v4 | Redux | Redux-thunk | express

React 服務(wù)端渲染

服務(wù)端渲染的基本套路就是用戶請求過來的時候,在服務(wù)端生成一個我們希望看到的網(wǎng)頁內(nèi)容的HTML字符串,返回給瀏覽器去展示。

瀏覽器拿到了這個HTML之后,渲染出頁面,但是并沒有事件交互,這時候瀏覽器發(fā)現(xiàn)HTML中加載了一些js文件(也就是瀏覽器端渲染的js),就直接去加載。

加載好并執(zhí)行完以后,事件就會被綁定上了。這時候頁面被瀏覽器端接管了。也就是到了我們熟悉的js渲染頁面的過程。

需要實現(xiàn)的目標(biāo):

  • React組件服務(wù)端渲染
  • 路由的服務(wù)端渲染
  • 保證服務(wù)端和瀏覽器的數(shù)據(jù)唯一
  • css的服務(wù)端渲染(樣式直出)

一般的渲染方式

  • 服務(wù)端渲染:服務(wù)端生成html字符串,發(fā)送給瀏覽器進(jìn)行渲染。
  • 瀏覽器端渲染:服務(wù)端返回空的html文件,內(nèi)部加載js完全由js與css,由js完成頁面的渲染

優(yōu)點與缺點

服務(wù)端渲染解決了首屏加載速度慢以及seo不友好的缺點(Google已經(jīng)可以檢索到瀏覽器渲染的網(wǎng)頁,但不是所有搜索引擎都可以)

但增加了項目的復(fù)雜程度,提高維護(hù)成本。

如果非必須,盡量不要用服務(wù)端渲染

整體思路

需要兩個端:服務(wù)端、瀏覽器端(瀏覽器渲染的部分)

第一: 打包瀏覽器端代碼

第二: 打包服務(wù)端代碼并啟動服務(wù)

第三: 用戶訪問,服務(wù)端讀取瀏覽器端打包好的index.html文件為字符串,將渲染好的組件、樣式、數(shù)據(jù)塞入html字符串,返回給瀏覽器

第四: 瀏覽器直接渲染接收到的html內(nèi)容,并且加載打包好的瀏覽器端js文件,進(jìn)行事件綁定,初始化狀態(tài)數(shù)據(jù),完成同構(gòu)

React組件的服務(wù)端渲染

讓我們來看一個最簡單的React服務(wù)端渲染的過程。

要進(jìn)行服務(wù)端渲染的話那必然得需要一個根組件,來負(fù)責(zé)生成HTML結(jié)構(gòu)

import React from 'react';import ReactDOM from 'react-dom';ReactDOM.hydrate(<Container />, document.getElementById('root'));

當(dāng)然這里用ReactDOM.render也是可以的,只不過hydrate會盡量復(fù)用接收到的服務(wù)端返回的內(nèi)容,

來補(bǔ)充事件綁定和瀏覽器端其他特有的過程

引入瀏覽器端需要渲染的根組件,利用react的 renderToString API進(jìn)行渲染

import { renderToString } from 'react-dom/server'import Container from '../containers'// 產(chǎn)生htmlconst content = renderToString(<Container/>)const html = `  <html>   <body>${content}</body>  </html>`res.send(html)

在這里,renderToString也可以替換成renderToNodeStream,區(qū)別在于前者是同步地產(chǎn)生HTML,也就是如果生成HTML用了1000毫秒,

那么就會在1000毫秒之后才將內(nèi)容返回給瀏覽器,顯然耗時過長。而后者則是以流的形式,將渲染結(jié)果塞給response對象,就是出來多少就

返回給瀏覽器多少,可以相對減少耗時

路由的服務(wù)端渲染

一般場景下,我們的應(yīng)用不可能只有一個頁面,肯定會有路由跳轉(zhuǎn)。我們一般這么用:

import { BrowserRouter, Route } from 'react-router-dom'const App = () => (  <BrowserRouter>    {/*...Routes*/}  <BrowserRouter/>)

但這是瀏覽器端渲染時候的用法。在做服務(wù)端渲染時,需要使用將BrowserRouter 替換為 StaticRouter
區(qū)別在于,BrowserRouter 會通過HTML5 提供的 history API來保持頁面與URL的同步,而StaticRouter
則不會改變URL

import { createServer } from 'http'import { StaticRouter } from 'react-router-dom'createServer((req, res) => {  const html = renderToString(    <StaticRouter      location={req.url}      context={{}}    >      <Container />    <StaticRouter/>)})

這里,StaticRouter要接收兩個屬性:

  • location: StaticRouter 會根據(jù)這個屬性,自動匹配對應(yīng)的React組件,所以才會實現(xiàn)刷新頁面,服務(wù)端返回的對應(yīng)路由的組與瀏覽器端保持一致
  • context: 一般用來傳遞一些數(shù)據(jù),相當(dāng)于一個載體,之后講到樣式的服務(wù)端渲染的時候會用到

Redux同構(gòu)

數(shù)據(jù)的預(yù)獲取以及脫水與注水我認(rèn)為是服務(wù)端渲染的難點。

這是什么意思呢?也就是說首屏渲染的網(wǎng)頁一般要去請求外部數(shù)據(jù),我們希望在生成HTML之前,去獲取到這個頁面需要的所有數(shù)據(jù),然后塞到頁面中去,這個過程,叫做“脫水”(Dehydrate),生成HTML返回給瀏覽器。瀏覽器拿到帶著數(shù)據(jù)的HTML,去請求瀏覽器端js,接管頁面,用這個數(shù)據(jù)來初始化組件。這個過程叫“注水”(Hydrate)。完成服務(wù)端與瀏覽器端數(shù)據(jù)的統(tǒng)一。

為什么要這么做呢?試想一下,假設(shè)沒有數(shù)據(jù)的預(yù)獲取,直接返回一個沒有數(shù)據(jù),只有固定內(nèi)容的HTML結(jié)構(gòu),會有什么結(jié)果呢?

第一:由于頁面內(nèi)沒有有效信息,不利于SEO。

第二:由于返回的頁面沒有內(nèi)容,但瀏覽器端JS接管頁面后回去請求數(shù)據(jù)、渲染數(shù)據(jù),頁面會閃一下,用戶體驗不好。

我們使用Redux來管理狀態(tài),因為有服務(wù)端代碼和瀏覽器端代碼,那么就分別需要兩個store來管理服務(wù)端和瀏覽器端的數(shù)據(jù)。

組件的配置

組件要在服務(wù)端渲染的時候去請求數(shù)據(jù),可以在組件上掛載一個專門發(fā)異步請求的方法,這里叫做loadData,接收服務(wù)端的store作為參數(shù),然后store.dispatch去擴(kuò)充服務(wù)端的store。

class Home extends React.Component {  componentDidMount() {    this.props.callApi()  }  render() {    return <div>{this.props.state.name}</div>  }}Home.loadData = store => { return store.dispatch(callApi())}const mapState = state => stateconst mapDispatch = {callApi}export default connect(mapState, mapDispatch)(Home)

路由的改造

因為服務(wù)端要根據(jù)路由判斷當(dāng)前渲染哪個組件,可以在這個時候發(fā)送異步請求。所以路由也需要配置一下來支持loadData方法。服務(wù)端渲染的時候,路由的渲染可以使用react-router-config這個庫,用法如下(重點關(guān)注在路由上掛載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 = <BrowserRouter>  {renderRoutes(routes)}<BrowserRouter/>

服務(wù)端獲取數(shù)據(jù)

到了服務(wù)端,需要判斷匹配的路由內(nèi)的所有組件各自都有沒有l(wèi)oadData方法,有就去調(diào)用,傳入服務(wù)端的store,去擴(kuò)充服務(wù)端的store。同時還要注意到,一個頁面可能是由多個組件組成的,會發(fā)各自的請求,也就意味著我們要等所有的請求都發(fā)完,再去返回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方法獲取匹配到的路由對應(yīng)的組件數(shù)組 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)  } } // 所有請求響應(yīng)完畢,將被HTML內(nèi)容發(fā)送給瀏覽器 Promise.all(promises).then(() => {  // 將生成html內(nèi)容的邏輯封裝成了一個函數(shù),接收req, store, context  res.send(serverRender(req, store, context)) })})

細(xì)心的同學(xué)可能注意到了上邊我把每個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方法則不會執(zhí)行,所以包一層promise的目的是即使請求出錯,也會resolve,不會影響到Promise.all方法,也就是說只有請求出錯的組件會沒數(shù)據(jù),而其他組件不會受影響。

注入數(shù)據(jù)

我們請求已經(jīng)發(fā)出去了,并且在組件的loadData方法中也擴(kuò)充了服務(wù)端的store,那么可以從服務(wù)端的數(shù)據(jù)取出來注入到要返回給瀏覽器的HTML中了。

來看 serverRender 方法

const serverRender = (req, store, context) => { // 讀取客戶端生成的HTML const template = fs.readFileSync(process.cwd() + '/public/static/index.html', 'utf8') const content = renderToString(  <Provider store={store}>   <StaticRouter location={req.path} context={context}>    <Container/>   </StaticRouter>  </Provider> ) // 注入數(shù)據(jù) const initialState = `<script>  window.context = {   INITIAL_STATE: ${JSON.stringify(store.getState())}  }</script>` return template.replace('<!--app-->', content)  .replace('<!--initial-state-->', initialState)}

瀏覽器端用服務(wù)端獲取到的數(shù)據(jù)初始化store

經(jīng)過上邊的過程,我們已經(jīng)可以從window.context中拿到服務(wù)端預(yù)獲取的數(shù)據(jù)了,此時需要做的事就是用這份數(shù)據(jù)去初始化瀏覽器端的store。保證兩端數(shù)據(jù)的統(tǒng)一。

import { createStore, applyMiddleware, compose } from 'redux'import thunk from 'redux-thunk'import rootReducer from '../reducers'const defaultStore = window.context && window.context.INITIAL_STATEconst clientStore = createStore( rootReducer, defaultStore,// 利用服務(wù)端的數(shù)據(jù)初始化瀏覽器端的store compose(  applyMiddleware(thunk),  window.devToolsExtension ? window.devToolsExtension() : f=>f ))

至此,服務(wù)端渲染的數(shù)據(jù)統(tǒng)一問題就解決了,再來回顧一下整個流程:

  • 用戶訪問路由,服務(wù)端根據(jù)路由匹配出對應(yīng)路由內(nèi)的組件數(shù)組
  • 循環(huán)數(shù)組,調(diào)用組件上掛載的loadData方法,發(fā)送請求,擴(kuò)充服務(wù)端store
  • 所有請求完成后,通過store.getState,獲取到服務(wù)端預(yù)獲取的數(shù)據(jù),注入到window.context中
  • 瀏覽器渲染返回的HTML,加載瀏覽器端js,從window.context中取數(shù)據(jù)來初始化瀏覽器端的store,渲染組件

這里還有個點,也就是當(dāng)我們從路由進(jìn)入到其他頁面的時候,組件內(nèi)的loadData方法并不會執(zhí)行,它只會在刷新,服務(wù)端渲染路由的時候執(zhí)行。

這時候會沒有數(shù)據(jù)。所以我們還需要在componentDidMount中去發(fā)請求,來解決這個問題。因為componentDidMount不會在服務(wù)端渲染執(zhí)行,所以不用擔(dān)心請求重復(fù)發(fā)送。

樣式的服務(wù)端渲染

以上我們所做的事情只是讓網(wǎng)頁的內(nèi)容經(jīng)過了服務(wù)端的渲染,但是樣式要在瀏覽器加載css后才會加上,所以最開始返回的網(wǎng)頁內(nèi)容沒有樣式,頁面依然會閃一下。為了解決這個問題,我們需要讓樣式也一并在服務(wù)端渲染的時候返回。

首先,服務(wù)端渲染的時候,解析css文件,不能使用style-loader了,要使用isomorphic-style-loader。

{  test: //.css$/,  use: [    'isomorphic-style-loader',    'css-loader',    'postcss-loader'  ],}

但是,如何在服務(wù)端獲取到當(dāng)前路由內(nèi)的組件樣式呢?回想一下,我們在做路由的服務(wù)端渲染時,用到了StaticRouter,它會接收一個context對象,這個context對象可以作為一個載體來傳遞一些信息。我們就用它!

思路就是在渲染組件的時候,在組件內(nèi)接收context對象,獲取組件樣式,放到context中,服務(wù)端拿到樣式,插入到返回的HTML中的style標(biāo)簽中。

來看看組件是如何讀取樣式的吧:

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)   }  }}

在路由內(nèi)的組件可以在props里接收到staticContext,也就是通過StaticRouter傳遞過來的context,
isomorphic-style-loader 提供了一個 _getCss() 方法,讓我們能讀取到css樣式,然后放到staticContext里。
不在路由之內(nèi)的組件,可以通過父級組件,傳遞props的方法,或者用react-router的withRouter包裹一下

其實這部分提取css的邏輯可以寫成高階組件,這樣就可以做到復(fù)用了

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 <DecoratedComponent {...this.props}/>  } }}

在服務(wù)端,經(jīng)過組件的渲染之后,context中已經(jīng)有內(nèi)容了,我們這時候把樣式處理一下,返回給瀏覽器,就可以做到樣式的服務(wù)端渲染了

const serverRender = (req, store) => { const context = {css: []} const template = fs.readFileSync(process.cwd() + '/public/static/index.html', 'utf8') const content = renderToString(  <Provider store={store}>   <StaticRouter location={req.path} context={context}>    <Container/>   </StaticRouter>  </Provider> ) // 經(jīng)過渲染之后,context.css內(nèi)已經(jīng)有了樣式 const cssStr = context.css.length ? context.css.join('/n') : '' const initialState = `<script>  window.context = {   INITIAL_STATE: ${JSON.stringify(store.getState())}  }</script>` return template.replace('<!--app-->', content)  .replace('server-render-css', cssStr)  .replace('<!--initial-state-->', initialState)}

至此,服務(wù)端渲染就全部完成了。

總結(jié)

React的服務(wù)端渲染,最好的解決方案就是Next.js。如果你的應(yīng)用沒有SEO優(yōu)化的需求,又或者不太注重首屏渲染的速度,那么盡量就不要用服務(wù)端渲染。

因為會讓項目變得復(fù)雜。此外,除了服務(wù)端渲染,SEO優(yōu)化的辦法還有很多,比如預(yù)渲染(pre-render)。

以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持武林網(wǎng)。

發(fā)表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發(fā)表
主站蜘蛛池模板: 疏勒县| 靖西县| 云和县| 南京市| 洪湖市| 兴义市| 攀枝花市| 彭泽县| 石城县| 河津市| 东乡族自治县| 潮安县| 承德县| 江山市| 江口县| 和平县| 商丘市| 新昌县| 竹溪县| 景谷| 恩施市| 搜索| 德安县| 杂多县| 团风县| 宁南县| 唐山市| 开封县| 广州市| 富源县| 彭水| 海宁市| 巢湖市| 松阳县| 尼勒克县| 鸡东县| 周口市| 宜都市| 桃源县| 柳江县| 二连浩特市|