React服務端渲染探祕:2.初識同構

一.引入同構

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

// 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並無作事件相關的處理,所以返回給瀏覽器的內容不會有事件綁定。前端

那怎麼解決這個問題呢?node

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

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

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

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

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

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

//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:json

//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']
          }
        }]]
      }
    }]
  }
}

//package.json的script部分
  "scripts": {
    "dev": "npm-run-all --parallel dev:**",
    "dev:start": "nodemon --watch build --exec node \"./build/bundle.js\"",
    "dev:build:server": "webpack --config webpack.server.js --watch",
    "dev:build:client": "webpack --config webpack.client.js --watch"
  },
複製代碼

在這裏須要開啓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-router-config中renderRoutes來處理。

相關文章
相關標籤/搜索