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中很是重要的部分。但愿对你们有所帮助,也欢迎你们在评论区交流。

相关文章
相关标签/搜索