【redux】詳解react/redux的服務端渲染:頁面性能與SEO


 
 

亟待解決的疑問

為什么服務端渲染首屏渲染快?(對比客戶端首屏渲染)

 
react客戶端渲染的一大痛點就是首屏渲染速度慢問題,因為react是一個單頁面應用,大多數的資源需要在首次渲染前就加載好,這較大程度地拖慢了首屏渲染速度。有一些方式能夠較好地解決這個問題:
 
1.webpack的按需加載(代碼分割)http://www.css88.com/doc/webpack2/guides/code-splitting/ (這與本篇文章沒有太大關系,所以我只丟鏈接)
2.我們這篇文章提到的react/redux的服務端渲染
 
客戶端渲染,服務端渲染具體的渲染過程的比較:

 

無論是客戶端渲染,服務端渲染,它們都包含三個主體過程:
a:下載JS/CSS代碼
b:請求數據
c:渲染頁面
客戶端渲染:a -> b ->c (a,b,c都在客戶端進行)
服務端渲染:b -> c ->a (b,c在服務端進行,最后的a在客戶端進行)
 
服務端渲染改變了a,b,c三個過程的執行順序和執行方
 
為什么服務端渲染首屏渲染快
 
1.相比於客戶端首屏渲染,服務端首屏渲染不需要在客戶端下載JS/CSS代碼請注意我說的是“首屏”),客戶端接受服務端內容的時候,接受到的已經是完整的可視頁面
2.服務端在內網請求數據(拉取數據),數據響應速度是很快的,而對於客戶端渲染,外網http請求開銷大,且受到具體的網絡環境的限制
 
兩個注意要點:“首屏”“可視”
 
上面我在服務端首屏渲染中,強調了兩個詞:“首屏”“可視”
1.服務端只做首屏的渲染,后續的渲染過程都移交客戶端處理,這是為了減少服務器的負擔 (這個首屏渲染不需要在客戶端下載JS代碼)
2.服務端渲染的是“可視”頁面,沒錯,就是字面意思,這個頁面就“只是用來看的”,沒有具體的交互功能!!,因為我們的JS代碼還沒下載好呀,而當具體的JS代碼在客戶端下載好並執行后,這個頁面才具有了完整的交互功能
 
更詳細的資料:Node直出理論與實踐總結(詳細:https://github.com/joeyguo/blog/issues/8
 
上文中描述的客戶端渲染和服務端渲染,實際上對應了兩種Web構建模式:前后分離模式和直出模式
 
模式一:前后分離模式(對應客戶端渲染)

 

模式二:直出模式(對應服務端渲染)

 

 
 
 
最后對用服務端做react的首屏渲染做個比喻:在一場接力賽跑里,第一棒(首屏渲染)是尤為重要的,所以教練讓一位健壯敏捷的小伙(服務端)來接,而當這位小伙把棒交給下一位選手后(客戶端),他的任務(首屏渲染)也就結束了,而所有剩下的工作都交給這下一位伙伴去做了。
 
(體育差,可能比喻得不太好,見諒~~)
 

為什么服務端渲染有利於SEO?(對比客戶端渲染)

 
原因很簡單,因為客戶端渲染全部依賴於虛擬DOM,而搜索引擎爬不到虛擬DOM主要是國內搜索引擎
 
為了直觀地表述這一點,讓我們看服務端渲染/客戶端渲染下demo的源代碼吧! 這是我下面將要展示的demo的截圖:

 

這是客戶端渲染時候的源代碼:

 

沒錯,在根div節點下一點HTML都看不到!這會讓國內的搜索引擎非常苦惱,因為搜不到
但是當使用服務端做首屏渲染的時候它的源代碼就變成了這樣:

 

這樣搜索引擎就能搜到啦!(具體代碼下面介紹)
 
是不是搜索引擎都爬不到虛擬DOM呢?NO!!
 
國外谷歌可以,雅虎可以,BING可以,Duck Duck Go可以
國內百度不可以。。。
 
具體看這篇文章:SEO vs. React: Web Crawlers are Smarter Than You Think
放一下文章中爬虛擬DOM的截圖:
這是BING:
這是雅虎
  
這是百度
      
 
綜上,在國內做react產品,服務端首屏渲染還是很重要滴~~
 

服務端渲染的具體的代碼

 
我們的src目錄由三部分組成:common,client和server,利用express框架開啟服務器
 
展開后:
 
它們間的關系如下:

 

【注意】client和server部分的代碼是本文着重要講解的部分,common的部分就一筆帶過了
 
好,先放我們的代碼:
 
common部分:
 
action/index.js :
 
export const increment = () => {
  return { type:'INCREMENT' }
}
 
export const decrement = () => {
  return { type:'DECREMENT' }
}

 

Reducer/index.js :
 
import { combineReducers } from 'redux'
 
const initState = { number:0 }
const counterReducer = (state = initState, action) => {
const { number } = state
switch (action.type) {
   case 'INCREMENT':
     return { number:number + 1}
   case 'DECREMENT':
     return { number:number - 1}
   default:
     return state
   }
}
 
export default combineReducers({ counterReducer })

 

store/index.js :
 
import { createStore } from 'redux'
import reducer from '../reducer'
 
export default (preloadedState={}) => {
  const store = createStore(
    reducer,
    preloadedState
   )
    return store
}
 

 

containner/index.js :
 
import React from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import ComponentApp from '../component'
import * as NumberActions from '../action'
 
const mapStateToProps = state => {
   return { number:state.counterReducer.number }
}
 
const mapDispatchToProps = dispatch => {
   return bindActionCreators(NumberActions,dispatch)
}
export default connect(mapStateToProps, mapDispatchToProps)(ComponentApp)

 

component/index.js :
 
import React from 'react'
 
class ComponentApp extends React.Component{
  render () {
    const { number, increment, decrement } = this.props
    return (
      <div>
        <h1>{number}</h1>
        <button onClick={increment}>增1</button>
        <button onClick={decrement}>減1</button>
      </div>
       )
       }
 }
 
 export default ComponentApp

 

server部分:
 
server/server.js :
 
import React from 'react'
import path from'path'
import ReactDOMServer from 'react-dom/server'
import { Provider } from 'react-redux'
 
import createStore from '../common/store'
import App from '../common/container'
 
/************ 這部分代碼參考自webpack-dev-middleware的官方文檔 ************/
var express = require("express");
var webpackDevMiddleware = require("webpack-dev-middleware");
var webpack = require("webpack");
var webpackConfig = require('../../webpack.config.js');
 
var app = express();
 
var compiler = webpack(webpackConfig);
app.use(webpackDevMiddleware(compiler, {
// 這個publicPath參數要和webpack.config.js的`output.publicPath`參數保持一致
publicPath:webpackConfig.output.publicPath
}));
 
/************ 這部分代碼參考自webpack-dev-middlemare的官方文檔 ************/
//鏈接 https://webpack.js.org/guides/development/#webpack-dev-middleware
/*
renderFullPage函數,渲染完整的首屏可視頁面(這個頁面渲染完畢后將被發送到客戶端)
第一個參數是被轉成字符串的APP,要將其插入入口HMTL文件中
第二個參數是初始化的state,將其放入window對象中以便在發送到客戶端后能通過window.__INITIAL_STATE__取用
*/
 
const renderFullPage = (html, preloadState) => {
return `<!doctype html>
          <html lang="en">
            <head>
              <meta charset="utf-8">
              <meta name="viewport" content="width=device-width, initial-scale=1">
              <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
              <title>React App</title>
            </head>
            <body>
              <div id="root">${html}</div>
              <script>window.__INITIAL_STATE__ =${JSON.stringify(preloadState)}</script>
              <script src="/static/bundle.js"></script>
            </body>
           </html>`
}
 
const handleRender = (req, res) => {
// 初始化store,有兩個作用:1.放入Provider的store屬性中 2. 通過store.getState()獲取初始化的state
const store = createStore()
// 將APP轉成字符串
const html = ReactDOMServer.renderToString(
<Provider store={store}> <App /> </Provider> ) // 取得初始化的state const preloadState = store.getState() // 將渲染完整的首屏可視頁面(字符串)發送到客戶端顯示 res.send(renderFullPage(html, preloadState)) } // 注冊中間件函數,每當從客戶端接收到請求的時候,運行handleRender函數 app.use(handleRender) // 監聽3000端口 app.listen(3000, (error) => { if (error) { console.error(error) } })

 

 
server/index.js :
// 確保在node環境下能編譯es6(es2015)和JSX(react)的語法
require('babel-core/register')({
  presets: ['es2015', 'react']
})
require('./server')

 

client部分:
 
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import createStore from '../common/store'
import App from '../common/container'
 
// 取得服務端發送過來的初始化state
const initialState = window.__INITIAL_STATE__
// 初始化store
const store = createStore(initialState)
// reactDOM渲染
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)

 

 
【注意】:
1.對express框架不太熟悉的同學可看一下express的文檔http://www.expressjs.com.cn/4x/api.html
2.我上面的例子和redux官方文檔的例子大致相同,更詳細的介紹請看這里:http://redux.js.org/docs/recipes/ServerRendering.html
 
demo如下,點擊按鈕讓數字加一或減一
【注意】采用客戶端渲染和服務端渲染demo無大差異,區別在於首屏渲染的速度(服務端渲染要快)
 
React/redux服務端渲染的整體思路:

 

【注意】最后客戶端渲染的時候,因為服務端已經做了首屏渲染,所以這里不再重復渲染頁面,而只掛載監聽器,具體請看下面:
 

如何理解兩個渲染過程?(ReactDOMServer.renderToString和 reactDOM.render的聯系)

一開始讓我感到疑惑的就是這兩個過程,因為單從代碼上看似乎我們做了兩次重復的渲染,但實際上卻並不是這樣。
 
renderToString會將虛擬DOM轉化成一段帶有“標記”(markup)的HTML字符串,“標記”包括 id和校驗和兩部分,見下圖:
 
這段HTML字符串發送到客戶端后,在調用ReactDOM.render()時候,將根據校驗和(data-react-checksum)判斷是否需要重新render:
 
1.校驗和相同,只掛載事件監聽器,不重新render
2.校驗和不同,重新render
 
這告訴我們:當服務端/客戶端共用APP的虛擬DOM的前提下,是不會有冗余的重渲染的
 
react文檔原文:
Render a React element to its initial HTML. This should only be used on the server. React will return an HTML string. You can use this method to generate HTML on the server and send the markup down on the initial request for faster page loads and to allow search engines to crawl your pages for SEO purposes.
 
If you call ReactDOM.render() on a node that already has this server-rendered markup, React will preserve it and only attach event handlers, allowing you to have a very performant first-load experience.
 

為什么要把state(redux)從服務端傳到客戶端?

保證前后端數據的一致性
 

解決服務端渲染代碼中的“痛點”

在node環境運行ES6語法和JSX語法——babel-core/register的使用

在做服務端渲染的時候,讓我蛋疼的莫過於在server.js中,babel-loader插件和.babelrc文件失效了
 
我原本配置了.babelrc文件和wepack的babel-loader插件,可它們是針對瀏覽器環境的,在node環境下失效了,換而言之,我遭遇了無法在我的server.js中使用ES6語法和JSX語法的問題。
 
服務端ES6語法編譯失敗(注:這是在配好了.babelrc文件和wepack的babel-loader插件前提下發生的)

 

服務端JSX語法(react)編譯失敗

 

所以我在server.js中加了這一段代碼:
require('babel-core/register')({
   presets: ['es2015', 'react']
})
然后,編譯成功!

 

【注意】redux官方文檔里還有其他的解決方法,原理類似,想了解更多請看redux官方文檔http://redux.js.org/docs/recipes/ServerRendering.html
 

使發送到客戶端的頁面能訪問打包后的bundle.js—— webpack.output.publicPath的使用 

webpackDevMiddleware中的publicPath參數要和webpack.output.publicPath中的參數保持一致
 
例如:
這是我在webpack.config.js中的output參數:(關鍵在於publicPath)
output:{
  filename:'bundle.js',
  path:path.join(__dirname,'dist'),
  publicPath: '/static'
}

 

這是我在server.js中的webpackDevMiddleware中的publicPath參數相關代碼:
var webpackConfig = require('../../webpack.config.js');
// 省略其他內容
app.use(webpackDevMiddleware(compiler, {
publicPath:webpackConfig.output.publicPath // Same as `output.publicPath` in most cases.
}));
 
然后我們在輸出的HTML頁面中就可以通過指定的'/static目錄去訪問被webpack打包后的bundle.js文件了

 

 

參考資料:文章標題,作者和鏈接(按先后順序)

 
React同構直出優化總結 —— joeyguo
 
Node直出理論與實踐總結 —— joeyguo
 
SEO vs. React: Web搜索引擎比你想的要聰明(需要翻牆)——Patrick Hund
 
redux文檔服務端渲染章節
 
react文檔 ReactDOMServer的API
 
 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM