React服務端渲染探祕:1.實現一個基礎的React組件SSR、初識同構

你們好,我是神三元,這一次,讓咱們來把React服務端渲染(Server Side Render,簡稱「SSR」)學個明明白白。html

一. SSR vs CSR

什麼是服務端渲染?前端

廢話很少說,直接起一個express服務器。node

var express = require('express')
var app = express()

app.get('/', (req, res) => {
 res.send(
 ` <html> <head> <title>hello</title> </head> <body> <h1>hello</h1> <p>world</p> </body> </html> `
 )
})

app.listen(3001, () => {
 console.log('listen:3001')
})
複製代碼

啓動以後打開localhost:3001能夠看到頁面顯示了hello world。並且打開網頁源代碼:react

也可以完成顯示。

這就是服務端渲染。其實很是好理解,就是服務器返回一堆html字符串,而後讓瀏覽器顯示。webpack

與服務端渲染相對的是客戶端渲染(Client Side Render)。那什麼是客戶端渲染? 如今建立一個新的React項目,用腳手架生成項目,而後run起來。 這裏你能夠看到React腳手架自動生成的首頁。web

然而打開網頁源代碼。

body中除了兼容處理的noscript標籤以外,只有一個id爲root的標籤。那首頁的內容是從哪來的呢?很明顯,是下面的script中拉取的JS代碼控制的。

所以,CSR和SSR最大的區別在於前者的頁面渲染是JS負責進行的,然後者是服務器端直接返回HTML讓瀏覽器直接渲染。express

爲何要使用服務端渲染呢?瀏覽器

傳統CSR的弊端:

  1. 因爲頁面顯示過程要進行JS文件拉取和React代碼執行,首屏加載時間會比較慢。
  2. 對於SEO(Search Engine Optimazition,即搜索引擎優化),徹底無能爲力,由於搜索引擎爬蟲只認識html結構的內容,而不能識別JS代碼內容。

SSR的出現,就是爲了解決這些傳統CSR的弊端。服務器

2、實現React組件的服務端渲染

剛剛起的express服務返回的只是一個普通的html字符串,但咱們討論的是如何進行React的服務端渲染,那麼怎麼作呢? 首先寫一個簡單的React組件:babel

// containers/Home.js
import React from 'react';
const Home = () => {
  return (
    <div> <div>This is sanyuan</div> </div>
  )
}
export default Home
複製代碼

如今的任務就是將它轉換爲html代碼返回給瀏覽器。 總所周知,JSX中的標籤實際上是基於虛擬DOM的,最終要經過必定的方法將其轉換爲真實DOM。

而react-dom這個庫中恰好實現了這個方法。作法以下:

// server/index.js
import express from 'express';
import { renderToString } from 'react-dom/server';
import Home from './containers/Home';

const app = express();
const content = renderToString(<Home />); app.get('/', function (req, res) { res.send( ` <html> <head> <title>ssr</title> </head> <body> <div id="root">${content}</div> </body> </html> ` ); }) app.listen(3001, () => { console.log('listen:3001') }) 複製代碼

啓動express服務,再瀏覽器上打開對應端口,頁面顯示出"this is sanyuan"。 到此,就初步實現了一個React組件是服務端渲染。

三.引入同構

其實上面的React組件是不完整的,平時在開發的過程當中不免會有一些事件綁定,好比加一個button:

// containers/Home.js
import React from 'react';
const Home = () => {
  return (
    <div> <div>This is sanyuan</div> <button onClick={() => {alert('666')}}>click</button> </div>
  )
}
export default Home
複製代碼

再試一下,你會驚奇的發現,事件綁定無效!那這是爲何呢?緣由很簡單,react-dom/server下的renderToString並無作事件相關的處理,所以返回給瀏覽器的內容不會有事件綁定。

那怎麼解決這個問題呢?

這就須要進行同構了。所謂同構,通俗的講,就是一套React代碼在服務器上運行一遍,到達瀏覽器又運行一遍。服務端渲染完成頁面結構,瀏覽器端渲染完成事件綁定。

那如何進行瀏覽器端的事件綁定呢?

惟一的方式就是讓瀏覽器去拉取JS文件執行,讓JS代碼來控制。因而服務端返回的代碼變成了這樣:

有沒有發現和以前的區別?區別就是多了一個script標籤。而它拉取的JS代碼就是來完成同構的。

那麼這個index.js咱們如何生產出來呢?

在這裏,要用到react-dom。具體作法其實就很簡單了:

//client/index. js
import React from 'react';
import ReactDom from 'react-dom';
import Home from '../containers/Home';

ReactDom.hydrate(<Home />, document.getElementById('root')) 複製代碼

而後用webpack將其編譯打包成index.js:

//webpack.client.js
const path = require('path');
const merge = require('webpack-merge');
const config = require('./webpack.base');

const clientConfig = {
  mode: 'development',
  entry: './src/client/index.js',
  output: {
    filename: 'index.js',
    path: path.resolve(__dirname, 'public')
  },
}

module.exports = merge(config, clientConfig);

//webpack.base.js
module.exports = {
  module: {
    rules: [{
      test: /\.js$/,
      loader: 'babel-loader',
      exclude: /node_modules/,
      options: {
        presets: ['@babel/preset-react',  ['@babel/preset-env', {
          targets: {
            browsers: ['last 2 versions']
          }
        }]]
      }
    }]
  }
}
複製代碼

在這裏須要開啓express的靜態文件服務:

const app = express();
app.use(express.static('public'));
複製代碼

如今前端的script就能拿到控制瀏覽器的JS代碼啦。

綁定事件完成!

如今來初步總結一下同構代碼執行的流程:

四.同構中的路由問題

如今寫一個路由的配置文件:

// Routes.js
import React from 'react';
import {Route} from 'react-router-dom'
import Home from './containers/Home';
import Login from './containers/Login'

export default (
  <div> <Route path='/' exact component={Home}></Route> <Route path='/login' exact component={Login}></Route> </div>
)
複製代碼

在客戶端的控制代碼,也就是上面寫過的client/index.js中,要作相應的更改:

import React from 'react';
import ReactDom from 'react-dom';
import { BrowserRouter } from 'react-router-dom'
import Routes from '../Routes'

const App = () => {
  return (
    <BrowserRouter> {Routes} </BrowserRouter>
  )
}
ReactDom.hydrate(<App />, document.getElementById('root')) 複製代碼

這時候控制檯會報錯,

由於在Routes.js中,每一個Route組件外面包裹着一層div,但服務端返回的代碼中並無這個div,因此報錯。如何去解決這個問題?須要將服務端的路由邏輯執行一遍。

// server/index.js
import express from 'express';
import {render} from './utils';

const app = express();
app.use(express.static('public'));
//注意這裏要換成*來匹配
app.get('*', function (req, res) {
   res.send(render(req));
});
 
app.listen(3001, () => {
  console.log('listen:3001')
});
複製代碼
// server/utils.js
import Routes from '../Routes'
import { renderToString } from 'react-dom/server';
//重要是要用到StaticRouter
import { StaticRouter } from 'react-router-dom'; 
import React from 'react'

export const render = (req) => {
  //構建服務端的路由
  const content = renderToString(
    <StaticRouter location={req.path} > {Routes} </StaticRouter>
  );
  return ` <html> <head> <title>ssr</title> </head> <body> <div id="root">${content}</div> <script src="/index.js"></script> </body> </html> `
}
複製代碼

如今路由的跳轉就沒有任何問題啦。

這篇文章中,總結了服務端與客戶端渲染的對比、簡單react組件的服務端渲染、經過事件綁定需求引入同構、如何來解決同構中的路由問題這四個方面的內容,也是SSR中很是重要的部分。但願對你們有所幫助,也歡迎你們在評論區交流。

相關文章
相關標籤/搜索