真实的国产乱ⅩXXX66竹夫人,五月香六月婷婷激情综合,亚洲日本VA一区二区三区,亚洲精品一区二区三区麻豆

成都創(chuàng)新互聯(lián)網(wǎng)站制作重慶分公司

React服務(wù)端如何渲染

這篇文章主要介紹了React服務(wù)端如何渲染,具有一定借鑒價(jià)值,感興趣的朋友可以參考下,希望大家閱讀完這篇文章之后大有收獲,下面讓小編帶著大家一起了解一下。

昌邑網(wǎng)站制作公司哪家好,找創(chuàng)新互聯(lián)公司!從網(wǎng)頁(yè)設(shè)計(jì)、網(wǎng)站建設(shè)、微信開(kāi)發(fā)、APP開(kāi)發(fā)、響應(yīng)式網(wǎng)站等網(wǎng)站項(xiàng)目制作,到程序開(kāi)發(fā),運(yùn)營(yíng)維護(hù)。創(chuàng)新互聯(lián)公司于2013年開(kāi)始到現(xiàn)在10年的時(shí)間,我們擁有了豐富的建站經(jīng)驗(yàn)和運(yùn)維經(jīng)驗(yàn),來(lái)保證我們的工作的順利進(jìn)行。專注于網(wǎng)站建設(shè)就選創(chuàng)新互聯(lián)公司。

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

React 服務(wù)端渲染

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

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

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

需要實(shí)現(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è)面的渲染

優(yōu)點(diǎn)與缺點(diǎn)

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

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

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

整體思路

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

第一: 打包瀏覽器端代碼

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

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

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

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

讓我們來(lái)看一個(gè)最簡(jiǎn)單的React服務(wù)端渲染的過(guò)程。

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

import React from 'react';
import ReactDOM from 'react-dom';

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

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

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

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

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

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

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

返回給瀏覽器多少,可以相對(duì)減少耗時(shí)

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

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

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

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

import { createServer } from 'http'
import { StaticRouter } from 'react-router-dom'
createServer((req, res) => {
  const html = renderToString(
    
      
    )

})

這里,StaticRouter要接收兩個(gè)屬性:

  • location: StaticRouter 會(huì)根據(jù)這個(gè)屬性,自動(dòng)匹配對(duì)應(yīng)的React組件,所以才會(huì)實(shí)現(xiàn)刷新頁(yè)面,服務(wù)端返回的對(duì)應(yīng)路由的組與瀏覽器端保持一致

  • context: 一般用來(lái)傳遞一些數(shù)據(jù),相當(dāng)于一個(gè)載體,之后講到樣式的服務(wù)端渲染的時(shí)候會(huì)用到

Redux同構(gòu)

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

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

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

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

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

我們使用Redux來(lái)管理狀態(tài),因?yàn)橛蟹?wù)端代碼和瀏覽器端代碼,那么就分別需要兩個(gè)store來(lái)管理服務(wù)端和瀏覽器端的數(shù)據(jù)。

組件的配置

組件要在服務(wù)端渲染的時(shí)候去請(qǐng)求數(shù)據(jù),可以在組件上掛載一個(gè)專門(mén)發(fā)異步請(qǐng)求的方法,這里叫做loadData,接收服務(wù)端的store作為參數(shù),然后store.dispatch去擴(kuò)充服務(wù)端的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)

路由的改造

因?yàn)榉?wù)端要根據(jù)路由判斷當(dāng)前渲染哪個(gè)組件,可以在這個(gè)時(shí)候發(fā)送異步請(qǐng)求。所以路由也需要配置一下來(lái)支持loadData方法。服務(wù)端渲染的時(shí)候,路由的渲染可以使用react-router-config這個(gè)庫(kù),用法如下(重點(diǎn)關(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 = 
  {renderRoutes(routes)}

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

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

細(xì)心的同學(xué)可能注意到了上邊我把每個(gè)loadData都包了一個(gè)promise。

const promise = new Promise((resolve, reject) => {
 item.route.loadData(store).then(resolve).catch(resolve)
 console.log(item.route.loadData(store));
})
promises.push(promise)

這是為了容錯(cuò),一旦有一個(gè)請(qǐng)求出錯(cuò),那么下邊Promise.all方法則不會(huì)執(zhí)行,所以包一層promise的目的是即使請(qǐng)求出錯(cuò),也會(huì)resolve,不會(huì)影響到Promise.all方法,也就是說(shuō)只有請(qǐng)求出錯(cuò)的組件會(huì)沒(méi)數(shù)據(jù),而其他組件不會(huì)受影響。

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

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

來(lái)看 serverRender 方法

const serverRender = (req, store, context) => {
 // 讀取客戶端生成的HTML
 const template = fs.readFileSync(process.cwd() + '/public/static/index.html', 'utf8')
 const content = renderToString(
  
   
    
   
  
 )
 // 注入數(shù)據(jù)
 const initialState = ``
 return template.replace('', content)
  .replace('', initialState)
}

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

經(jīng)過(guò)上邊的過(guò)程,我們已經(jīng)可以從window.context中拿到服務(wù)端預(yù)獲取的數(shù)據(jù)了,此時(shí)需要做的事就是用這份數(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_STATE
const clientStore = createStore(
 rootReducer,
 defaultStore,// 利用服務(wù)端的數(shù)據(jù)初始化瀏覽器端的store
 compose(
  applyMiddleware(thunk),
  window.devToolsExtension ? window.devToolsExtension() : f=>f
 )
)

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

  • 用戶訪問(wèn)路由,服務(wù)端根據(jù)路由匹配出對(duì)應(yīng)路由內(nèi)的組件數(shù)組

  • 循環(huán)數(shù)組,調(diào)用組件上掛載的loadData方法,發(fā)送請(qǐng)求,擴(kuò)充服務(wù)端store

  • 所有請(qǐng)求完成后,通過(guò)store.getState,獲取到服務(wù)端預(yù)獲取的數(shù)據(jù),注入到window.context中

  • 瀏覽器渲染返回的HTML,加載瀏覽器端js,從window.context中取數(shù)據(jù)來(lái)初始化瀏覽器端的store,渲染組件

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

這時(shí)候會(huì)沒(méi)有數(shù)據(jù)。所以我們還需要在componentDidMount中去發(fā)請(qǐng)求,來(lái)解決這個(gè)問(wèn)題。因?yàn)閏omponentDidMount不會(huì)在服務(wù)端渲染執(zhí)行,所以不用擔(dān)心請(qǐng)求重復(fù)發(fā)送。

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

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

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

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

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

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

來(lái)看看組件是如何讀取樣式的吧:

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

其實(shí)這部分提取css的邏輯可以寫(xiě)成高階組件,這樣就可以做到復(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 
  }
 }
}

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

const serverRender = (req, store) => {
 const context = {css: []}
 const template = fs.readFileSync(process.cwd() + '/public/static/index.html', 'utf8')
 const content = renderToString(
  
   
    
   
  
 )
 // 經(jīng)過(guò)渲染之后,context.css內(nèi)已經(jīng)有了樣式
 const cssStr = context.css.length ? context.css.join('\n') : ''
 const initialState = ``
 return template.replace('', content)
  .replace('server-render-css', cssStr)
  .replace('', initialState)
}

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

感謝你能夠認(rèn)真閱讀完這篇文章,希望小編分享的“React服務(wù)端如何渲染”這篇文章對(duì)大家有幫助,同時(shí)也希望大家多多支持創(chuàng)新互聯(lián),關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道,更多相關(guān)知識(shí)等著你來(lái)學(xué)習(xí)!


標(biāo)題名稱:React服務(wù)端如何渲染
文章來(lái)源:http://weahome.cn/article/ipdjoh.html

其他資訊

在線咨詢

微信咨詢

電話咨詢

028-86922220(工作日)

18980820575(7×24)

提交需求

返回頂部