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来处理。

相关文章
相关标签/搜索