91超碰碰碰碰久久久久久综合_超碰av人澡人澡人澡人澡人掠_国产黄大片在线观看画质优化_txt小说免费全本

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

React服務端如何渲染

發布時間:2021-08-17 13:42:17 來源:億速云 閱讀:143 作者:小新 欄目:web開發

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

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

React 服務端渲染

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

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

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

需要實現的目標:

  • React組件服務端渲染

  • 路由的服務端渲染

  • 保證服務端和瀏覽器的數據唯一

  • css的服務端渲染(樣式直出)

一般的渲染方式

  • 服務端渲染:服務端生成html字符串,發送給瀏覽器進行渲染。

  • 瀏覽器端渲染:服務端返回空的html文件,內部加載js完全由js與css,由js完成頁面的渲染

優點與缺點

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

但增加了項目的復雜程度,提高維護成本。

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

整體思路

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

第一: 打包瀏覽器端代碼

第二: 打包服務端代碼并啟動服務

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

第四: 瀏覽器直接渲染接收到的html內容,并且加載打包好的瀏覽器端js文件,進行事件綁定,初始化狀態數據,完成同構

React組件的服務端渲染

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

要進行服務端渲染的話那必然得需要一個根組件,來負責生成HTML結構

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

ReactDOM.hydrate(<Container />, document.getElementById('root'));

當然這里用ReactDOM.render也是可以的,只不過hydrate會盡量復用接收到的服務端返回的內容,

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

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

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

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

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

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

路由的服務端渲染

一般場景下,我們的應用不可能只有一個頁面,肯定會有路由跳轉。我們一般這么用:

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

但這是瀏覽器端渲染時候的用法。在做服務端渲染時,需要使用將BrowserRouter 替換為 StaticRouter
區別在于,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 會根據這個屬性,自動匹配對應的React組件,所以才會實現刷新頁面,服務端返回的對應路由的組與瀏覽器端保持一致

  • context: 一般用來傳遞一些數據,相當于一個載體,之后講到樣式的服務端渲染的時候會用到

Redux同構

數據的預獲取以及脫水與注水我認為是服務端渲染的難點。

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

為什么要這么做呢?試想一下,假設沒有數據的預獲取,直接返回一個沒有數據,只有固定內容的HTML結構,會有什么結果呢?

第一:由于頁面內沒有有效信息,不利于SEO。

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

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

組件的配置

組件要在服務端渲染的時候去請求數據,可以在組件上掛載一個專門發異步請求的方法,這里叫做loadData,接收服務端的store作為參數,然后store.dispatch去擴充服務端的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 => 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 = <BrowserRouter>
  {renderRoutes(routes)}
<BrowserRouter/>

服務端獲取數據

到了服務端,需要判斷匹配的路由內的所有組件各自都有沒有loadData方法,有就去調用,傳入服務端的store,去擴充服務端的store。同時還要注意到,一個頁面可能是由多個組件組成的,會發各自的請求,也就意味著我們要等所有的請求都發完,再去返回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的目的是即使請求出錯,也會resolve,不會影響到Promise.all方法,也就是說只有請求出錯的組件會沒數據,而其他組件不會受影響。

注入數據

我們請求已經發出去了,并且在組件的loadData方法中也擴充了服務端的store,那么可以從服務端的數據取出來注入到要返回給瀏覽器的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>
 )
 // 注入數據
 const initialState = `<script>
  window.context = {
   INITIAL_STATE: ${JSON.stringify(store.getState())}
  }
</script>`
 return template.replace('<!--app-->', content)
  .replace('<!--initial-state-->', initialState)
}

瀏覽器端用服務端獲取到的數據初始化store

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

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方法,發送請求,擴充服務端store

  • 所有請求完成后,通過store.getState,獲取到服務端預獲取的數據,注入到window.context中

  • 瀏覽器渲染返回的HTML,加載瀏覽器端js,從window.context中取數據來初始化瀏覽器端的store,渲染組件

這里還有個點,也就是當我們從路由進入到其他頁面的時候,組件內的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傳遞過來的context,
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 <DecoratedComponent {...this.props}/>
  }
 }
}

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

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>
 )
 // 經過渲染之后,context.css內已經有了樣式
 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)
}

至此,服務端渲染就全部完成了。

感謝你能夠認真閱讀完這篇文章,希望小編分享的“React服務端如何渲染”這篇文章對大家有幫助,同時也希望大家多多支持億速云,關注億速云行業資訊頻道,更多相關知識等著你來學習!

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

扶绥县| 定安县| 陆川县| 郧西县| 沈阳市| 卢龙县| 将乐县| 永新县| 缙云县| 林西县| 泾源县| 盈江县| 民县| 唐海县| 黔江区| 涿州市| 屏东县| 苏州市| 兴义市| 中阳县| 沙坪坝区| 德令哈市| 龙泉市| 襄城县| 荣昌县| 襄樊市| 木里| 洪洞县| 依安县| 德安县| 郧西县| 大安市| 安远县| 新沂市| 马公市| 苗栗县| 岐山县| 商丘市| 临汾市| 军事| 四川省|