从头开始,完全理解服务端渲染原理

640?wx_fmt=gif 640?wx_fmt=png 640?wx_fmt=png因为微信外链限制,推荐阅读等连接没法点击,可点击阅读原文跳转至原文,查看外链。 640?wx_fmt=png 640
 
 

你们好,我是神三元,这一次,让咱们来以React为例,把服务端渲染(Server Side Render,简称“SSR”)学个明明白白。javascript

这里附上这个项目的github地址:
https://github.com/sanyuan0704/react-ssrcss

欢迎你们点star,提issue,一块儿进步!html

part1:实现一个基础的React组件SSR

这一部分来简要实现一个React组件的SSR。前端

一. SSR vs CSR

什么是服务端渲染?java

废话很少说,直接起一个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')})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

640?wx_fmt=png

也可以完成显示。


这就是服务端渲染。其实很是好理解,就是服务器返回一堆html字符串,而后让浏览器显示。webpack

与服务端渲染相对的是客户端渲染(Client Side Render)。那什么是客户端渲染?
如今建立一个新的React项目,用脚手架生成项目,而后run起来。
这里你能够看到React脚手架自动生成的首页。ios

640?wx_fmt=png

然而打开网页源代码。


640?wx_fmt=png

body中除了兼容处理的noscript标签以外,只有一个id为root的标签。那首页的内容是从哪来的呢?很明显,是下面的script中拉取的JS代码控制的。


所以,CSR和SSR最大的区别在于前者的页面渲染是JS负责进行的,然后者是服务器端直接返回HTML让浏览器直接渲染。git

为何要使用服务端渲染呢?

640?wx_fmt=png

传统CSR的弊端:


  1. 因为页面显示过程要进行JS文件拉取和React代码执行,首屏加载时间会比较慢。

  2. 对于SEO(Search Engine Optimazition,即搜索引擎优化),彻底无能为力,由于搜索引擎爬虫只认识html结构的内容,而不能识别JS代码内容。

SSR的出现,就是为了解决这些传统CSR的弊端。

2、实现React组件的服务端渲染

刚刚起的express服务返回的只是一个普通的html字符串,但咱们讨论的是如何进行React的服务端渲染,那么怎么作呢?
首先写一个简单的React组件:

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

  )
}
export default Home

如今的任务就是将它转换为html代码返回给浏览器。
总所周知,JSX中的标签实际上是基于虚拟DOM的,最终要经过必定的方法将其转换为真实DOM。虚拟DOM也就是JS对象,能够看出整个服务端的渲染流程就是经过虚拟DOM的编译来完成的,所以虚拟DOM巨大的表达力也可见一斑了。

而react-dom这个库中恰好实现了编译虚拟DOM的方法。作法以下:

// server/index.jsimport 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')})
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组件是服务端渲染。
固然,这只是一个很是简陋的SSR,事实上对于复杂的项目而言是无能为力的,在以后会一步步完善,打造出一个功能完整的React的SSR框架。

part2: 初识同构

一.引入同构

其实前面的SSR是不完整的,平时在开发的过程当中不免会有一些事件绑定,好比加一个button:

// containers/Home.jsimport React from 'react';const Home = () => {  return (    <div>      <div>This is sanyuan</div>      <button onClick={() => {alert('666')}}>click</button>    </div>  )}export default Home
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代码来控制。因而服务端返回的代码变成了这样:

640?wx_fmt=png

有没有发现和以前的区别?区别就是多了一个script标签。而它拉取的JS代码就是来完成同构的。


那么这个index.js咱们如何生产出来呢?

在这里,要用到react-dom。具体作法其实就很简单了:

//client/index. jsimport React from 'react';import ReactDom from 'react-dom';import Home from '../containers/Home';ReactDom.hydrate(<Home />, document.getElementById('root'))
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.jsconst 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.jsmodule.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"  },
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'));
app.use(express.static('public'));

如今前端的script就能拿到控制浏览器的JS代码啦。

绑定事件完成!

如今来初步总结一下同构代码执行的流程:

640?wx_fmt=png

二.同构中的路由问题

如今写一个路由的配置文件:

// Routes.jsimport 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>)
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'))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'))

这时候控制台会报错,

640?wx_fmt=png

由于在Routes.js中,每一个Route组件外面包裹着一层div,但服务端返回的代码中并无这个div,因此报错。如何去解决这个问题?须要将服务端的路由逻辑执行一遍。


// server/index.jsimport 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')});
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.jsimport Routes from '../Routes'import { renderToString } from 'react-dom/server';//重要是要用到StaticRouterimport { 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>  `}
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来处理。

part3: 同构项目中引入Redux

这一节主要是讲述Redux如何被引入到同构项目中以及其中须要注意的问题。

从新回顾一下redux的运做流程:

640?wx_fmt=png

再回顾一下同构的概念,即在React代码客户端和服务器端各自运行一遍。


1、建立全局store

如今开始建立store。
在项目根目录的store文件夹(总的store)下:

import {createStore, applyMiddleware, combineReducers} from 'redux';import thunk from 'redux-thunk';import { reducer as homeReducer } from '../containers/Home/store';//合并项目组件中store的reducerconst reducer = combineReducers({  home: homeReducer})//建立store,并引入中间件thunk进行异步操做的管理const store = createStore(reducer, applyMiddleware(thunk));//导出建立的storeexport default storefrom 'redux';
import thunk from 'redux-thunk';
import { reducer as homeReducer } from '../containers/Home/store';
//合并项目组件中store的reducer
const reducer = combineReducers({
  home: homeReducer
})
//建立store,并引入中间件thunk进行异步操做的管理
const store = createStore(reducer, applyMiddleware(thunk));

//导出建立的store
export default store

2、组件内action和reducer的构建

Home文件夹下的工程文件结构以下:

640?wx_fmt=png

在Home的store目录下的各个文件代码示例:


//constants.jsexport const CHANGE_LIST = 'HOME/CHANGE_LIST';
export const CHANGE_LIST = 'HOME/CHANGE_LIST';
//actions.jsimport axios from 'axios';import { CHANGE_LIST } from "./constants";//普通actionconst changeList = list => ({  type: CHANGE_LIST,  list});//异步操做的action(采用thunk中间件)export const getHomeList = () => {  return (dispatch) => {    return axios.get('xxx')      .then((res) => {        const list = res.data.data;        console.log(list)        dispatch(changeList(list))      });  };}
import axios from 'axios';
import { CHANGE_LIST } from "./constants";

//普通action
const changeList = list => ({
  type: CHANGE_LIST,
  list
});
//异步操做的action(采用thunk中间件)
export const getHomeList = () => {
  return (dispatch) => {
    return axios.get('xxx')
      .then((res) => {
        const list = res.data.data;
        console.log(list)
        dispatch(changeList(list))
      });
  };
}
//reducer.jsimport { CHANGE_LIST } from "./constants";const defaultState = {  name: 'sanyuan',  list: []}export default (state = defaultState, action) => {  switch(action.type) {    default:      return state;  }}
import { CHANGE_LIST } from "./constants";

const defaultState = {
  name'sanyuan',
  list: []
}

export default (state = defaultState, action) => {
  switch(action.type) {
    default:
      return state;
  }
}
//index.jsimport  reducer  from "./reducer";//这么作是为了导出reducer让全局的store来进行合并//那么在全局的store下的index.js中只需引入Home/store而不须要Home/store/reducer.js//由于脚手架会自动识别文件夹下的index文件export {reducer}
import  reducer  from "./reducer";
//这么作是为了导出reducer让全局的store来进行合并
//那么在全局的store下的index.js中只需引入Home/store而不须要Home/store/reducer.js
//由于脚手架会自动识别文件夹下的index文件
export {reducer}

3、组件链接全局store

下面是Home组件的编写示例。

import React, { Component } from 'react';import { connect } from 'react-redux';import { getHomeList } from './store/actions'class Home extends Component {  render() {    const { list } = this.props    return list.map(item => <div key={item.id}>{item.title}</div>)  }}const mapStateToProps = state => ({  list: state.home.newsList,})const mapDispatchToProps = dispatch => ({  getHomeList() {    dispatch(getHomeList());  }})//链接storeexport default connect(mapStateToProps, mapDispatchToProps)(Home);'react';
import { connect } from 'react-redux';
import { getHomeList } from './store/actions'

class Home extends Component {
  render() {
    const { list } = this.props
    return list.map(item => <div key={item.id}>{item.title}</div>)
  }
}

const mapStateToProps = state => ({
  list: state.home.newsList,
})

const mapDispatchToProps = dispatch => ({
  getHomeList() {
    dispatch(getHomeList());
  }
})
//链接store
export default connect(mapStateToProps, mapDispatchToProps)(Home);

对于store的链接操做,在同构项目中分两个部分,一个是与客户端store的链接,另外一部分是与服务端store的链接。都是经过react-redux中的Provider来传递store的。

客户端:

//src/client/index.jsimport React from 'react';import ReactDom from 'react-dom';import {BrowserRouter, Route} from 'react-router-dom';import { Provider } from 'react-redux';import store from '../store'import routes from '../routes.js'const App = () => {  return (    <Provider store={store}>      <BrowserRouter>        {routes}      </BrowserRouter>    </Provider>  )}ReactDom.hydrate(<App />, document.getElementById('root'))
import React from 'react';
import ReactDom from 'react-dom';
import {BrowserRouter, Route} from 'react-router-dom';
import { Provider } from 'react-redux';
import store from '../store'
import routes from '../routes.js'

const App = () => {
  return (
    <Provider store={store}>
      <BrowserRouter>
        {routes}
      </BrowserRouter>
    </Provider>

  )
}

ReactDom.hydrate(<App />, document.getElementById('root'))

服务端:

//src/server/index.js的内容保持不变//下面是src/server/utils.jsimport Routes from '../Routes'import { renderToString } from 'react-dom/server';import { StaticRouter } from 'react-router-dom'; import { Provider } from 'react-redux';import React from 'react'export const render = (req) => {  const content = renderToString(    <Provider store={store}>      <StaticRouter location={req.path} >        {Routes}      </StaticRouter>    </Provider>  );  return `    <html>      <head>        <title>ssr</title>      </head>      <body>        <div id="root">${content}</div>        <script src="/index.js"></script>      </body>    </html>  `}
//下面是src/server/utils.js
import Routes from '../Routes'
import { renderToString } from 'react-dom/server';
import { StaticRouter } from 'react-router-dom'
import { Provider } from 'react-redux';
import React from 'react'

export const render = (req) => {
  const content = renderToString(
    <Provider store={store}>
      <StaticRouter location={req.path} >
        {Routes}
      </StaticRouter>
    </Provider>

  );
  return `
    <html>
      <head>
        <title>ssr</title>
      </head>
      <body>
        <div id="root">${content}</div>
        <script src="/index.js"></script>
      </body>
    </html>
  `

}

4、潜在的坑

其实上面这样的store建立方式是存在问题的,什么缘由呢?

上面的store是一个单例,当这个单例导出去后,全部的用户用的是同一份store,这是不该该的。那么这么解这个问题呢?

在全局的store/index.js下修改以下:

//导出部分修改export default  () => {  return createStore(reducer, applyMiddleware(thunk))}
export default  () => {
  return createStore(reducer, applyMiddleware(thunk))
}

这样在客户端和服务端的js文件引入时其实引入了一个函数,把这个函数执行就会拿到一个新的store,这样就能保证每一个用户访问时都是用的一份新的store。

part4: 异步数据的服务端渲染方案(数据注水与脱水)

1、问题引入

在日常客户端的React开发中,咱们通常在组件的componentDidMount生命周期函数进行异步数据的获取。可是,在服务端渲染中却出现了问题。

如今我在componentDidMount钩子函数中进行Ajax请求:

import { getHomeList } from './store/actions'  //......  componentDidMount() {    this.props.getList();  }  //......  const mapDispatchToProps = dispatch => ({    getList() {      dispatch(getHomeList());    }})from './store/actions'
  //......
  componentDidMount() {
    this.props.getList();
  }
  //......
  const mapDispatchToProps = dispatch => ({
    getList() {
      dispatch(getHomeList());
    }
})
//actions.jsimport { CHANGE_LIST } from "./constants";import axios from 'axios'const changeList = list => ({  type: CHANGE_LIST,  list})export const getHomeList = () => {  return dispatch => {    //另外起的本地的后端服务    return axiosInstance.get('localhost:4000/api/news.json')      .then((res) => {        const list = res.data.data;        dispatch(changeList(list))      })  }}//reducer.jsimport { CHANGE_LIST } from "./constants";const defaultState = {  name: 'sanyuan',  list: []}export default (state = defaultState, action) => {  switch(action.type) {    case CHANGE_LIST:      const newState = {        ...state,        list: action.list      }      return newState    default:      return state;  }}
import { CHANGE_LIST } from "./constants";
import axios from 'axios'

const changeList = list => ({
  type: CHANGE_LIST,
  list
})

export const getHomeList = () => {
  return dispatch => {
    //另外起的本地的后端服务
    return axiosInstance.get('localhost:4000/api/news.json')
      .then((res) => {
        const list = res.data.data;
        dispatch(changeList(list))
      })
  }
}
//reducer.js
import { CHANGE_LIST } from "./constants";

const defaultState = {
  name'sanyuan',
  list: []
}

export default (state = defaultState, action) => {
  switch(action.type) {
    case CHANGE_LIST:
      const newState = {
        ...state,
        list: action.list
      }
      return newState
    default:
      return state;
  }
}

好,如今启动服务。

640?wx_fmt=png

如今页面可以正常渲染,可是打开网页源代码。


640?wx_fmt=png

源代码里面并无这些列表数据啊!那这是为何呢?


让咱们来分析一下客户端和服务端的运行流程,当浏览器发送请求时,服务器接受到请求,这时候服务器和客户端的store都是空的,紧接着客户端执行componentDidMount生命周期中的函数,获取到数据并渲染到页面,然而服务器端始终不会执行componentDidMount,所以不会拿到数据,这也致使服务器端的store始终是空的。换而言之,关于异步数据的操做始终只是客户端渲染。

如今的工做就是让服务端将得到数据的操做执行一遍,以达到真正的服务端渲染的效果。

2、改造路由

在完成这个方案以前须要改造一下原有的路由,也就是routes.js

import Home from './containers/Home';import Login from './containers/Login';export default [{  path: "/",  component: Home,  exact: true,  loadData: Home.loadData,//服务端获取异步数据的函数  key: 'home'},{  path: '/login',  component: Login,  exact: true,  key: 'login'}}];from './containers/Home';
import Login from './containers/Login';

export default [
{
  path"/",
  component: Home,
  exacttrue,
  loadData: Home.loadData,//服务端获取异步数据的函数
  key: 'home'
},
{
  path'/login',
  component: Login,
  exacttrue,
  key'login'
}
}];

此时客户端和服务端中编写的JSX代码也发生了相应变化

//客户端//如下的routes变量均指routes.js导出的数组<Provider store={store}>  <BrowserRouter>      <div>        {            routers.map(route => {                <Route {...route} />            })        }      </div>  </BrowserRouter></Provider>
//如下的routes变量均指routes.js导出的数组
<Provider store={store}>
  <BrowserRouter>
      <div>
        {
            routers.map(route => {
                <Route {...route} />
            })
        }
      </div>
  </BrowserRouter>
</Provider>

//服务端<Provider store={store}>  <StaticRouter>      <div>        {            routers.map(route => {                <Route {...route} />            })        }      </div>  </StaticRouter></Provider>
<Provider store={store}>
  <StaticRouter>
      <div>
        {
            routers.map(route => {
                <Route {...route} />
            })
        }
      </div>
  </StaticRouter>
</Provider>

其中配置了一个loadData参数,这个参数表明了服务端获取数据的函数。每次渲染一个组件获取异步数据时,都会调用相应组件的这个函数。所以,在编写这个函数具体的代码以前,咱们有必要想清楚如何来针对不一样的路由来匹配不一样的loadData函数。

在server/utils.js中加入如下逻辑

  import { matchRoutes } from 'react-router-config';  //调用matchRoutes用来匹配当前路由(支持多级路由)  const matchedRoutes = matchRoutes(routes, req.path)  //promise对象数组  const promises = [];  matchedRoutes.forEach(item => {    //若是这个路由对应的组件有loadData方法    if (item.route.loadData) {      //那么就执行一次,并将store传进去      //注意loadData函数调用后须要返回Promise对象      promises.push(item.route.loadData(store))    }  })  Promise.all(promises).then(() => {      //此时该有的数据都已经到store里面去了      //执行渲染的过程(res.send操做)  }  )from 'react-router-config';
  //调用matchRoutes用来匹配当前路由(支持多级路由)
  const matchedRoutes = matchRoutes(routes, req.path)
  //promise对象数组
  const promises = [];
  matchedRoutes.forEach(item => {
    //若是这个路由对应的组件有loadData方法
    if (item.route.loadData) {
      //那么就执行一次,并将store传进去
      //注意loadData函数调用后须要返回Promise对象
      promises.push(item.route.loadData(store))
    }
  })
  Promise.all(promises).then(() => {
      //此时该有的数据都已经到store里面去了
      //执行渲染的过程(res.send操做)
  }
  )

如今就能够安心的写咱们的loadData函数,其实前面的铺垫工做作好后,这个函数是至关容易的。

import { getHomeList } from './store/actions'Home.loadData = (store) => {    return store.dispatch(getHomeList())}from './store/actions'

Home.loadData = (store) => {
    return store.dispatch(getHomeList())
}
//actions.jsexport const getHomeList = () => {  return dispatch => {    return axios.get('xxxx')      .then((res) => {        const list = res.data.data;        dispatch(changeList(list))      })  }}
export const getHomeList = () => {
  return dispatch => {
    return axios.get('xxxx')
      .then((res) => {
        const list = res.data.data;
        dispatch(changeList(list))
      })
  }
}

根据这个思路,服务端渲染中异步数据的获取功能就完成啦。

3、数据的注水和脱水

其实目前作了这里仍是存在一些细节问题的。好比当我将生命周期钩子里面的异步请求函数注释,如今页面中不会有任何的数据,可是打开网页源代码,却发现:

640?wx_fmt=png

数据已经挂载到了服务端返回的HTML代码中。那这就说明服务端和客户端的store不一样步的问题。


其实也很好理解。当服务端拿到store并获取数据后,客户端的js代码又执行一遍,在客户端代码执行的时候又建立了一个空的store,两个store的数据不能同步。

那如何才能让这两个store的数据同步变化呢?

首先,在服务端获取获取以后,在返回的html代码中加入这样一个script标签:

<script>  window.context = {    state: ${JSON.stringify(store.getState())}  }</script>
  window.context = {
    state: ${JSON.stringify(store.getState())}
  }
</script>

这叫作数据的“注水”操做,即把服务端的store数据注入到window全局环境中。
接下来是“脱水”处理,换句话说也就是把window上绑定的数据给到客户端的store,能够在客户端store产生的源头进行,即在全局的store/index.js中进行。

//store/index.jsimport {createStore, applyMiddleware, combineReducers} from 'redux';import thunk from 'redux-thunk';import { reducer as homeReducer } from '../containers/Home/store';const reducer = combineReducers({  home: homeReducer})//服务端的store建立函数export const getStore = () => {  return createStore(reducer, applyMiddleware(thunk));}//客户端的store建立函数export const getClientStore = () => {  const defaultState = window.context ? window.context.state : {};  return createStore(reducer, defaultState, applyMiddleware(thunk));}
import {createStore, applyMiddleware, combineReducers} from 'redux';
import thunk from 'redux-thunk';
import { reducer as homeReducer } from '../containers/Home/store';

const reducer = combineReducers({
  home: homeReducer
})
//服务端的store建立函数
export const getStore = () => {
  return createStore(reducer, applyMiddleware(thunk));
}
//客户端的store建立函数
export const getClientStore = () => {
  const defaultState = window.context ? window.context.state : {};
  return createStore(reducer, defaultState, applyMiddleware(thunk));
}

至此,数据的脱水和注水操做完成。可是仍是有一些瑕疵,其实当服务端获取数据以后,客户端并不须要再发送Ajax请求了,而客户端的React代码仍然存在这样的浪费性能的代码。怎么办呢?

仍是在Home组件中,作以下的修改:

componentDidMount() {  //判断当前的数据是否已经从服务端获取  //要知道,若是是首次渲染的时候就渲染了这个组件,则不会重复发请求  //若首次渲染页面的时候未将这个组件渲染出来,则必定要执行异步请求的代码  //这两种状况对于同一组件是都是有可能发生的  if (!this.props.list.length) {    this.props.getHomeList()  }}//判断当前的数据是否已经从服务端获取
  //要知道,若是是首次渲染的时候就渲染了这个组件,则不会重复发请求
  //若首次渲染页面的时候未将这个组件渲染出来,则必定要执行异步请求的代码
  //这两种状况对于同一组件是都是有可能发生的
  if (!this.props.list.length) {
    this.props.getHomeList()
  }
}

一路作下来,异步数据的服务端渲染仍是比较复杂的,可是难度并非很大,须要耐心地理清思路。

至此一个比较完整的SSR框架就搭建的差很少了,可是还有一些内容须要补充,以后会继续更新的。加油吧!

part5: node做中间层及请求代码优化

1、为何要引入node中间层?

其实任何技术都是与它的应用场景息息相关的。这里咱们反复谈的SSR,其实不到万不得已咱们是用不着它的,SSR所解决的最大的痛点在于SEO,但它同时带来了更昂贵的成本。不只由于服务端渲染须要更加复杂的处理逻辑,还由于同构的过程须要服务端和客户端都执行一遍代码,这虽然对于客户端并无什么大碍,但对于服务端倒是巨大的压力,由于数量庞大的访问量,对于每一次访问都要另外在服务器端执行一遍代码进行计算和编译,大大地消耗了服务器端的性能,成本随之增长。若是访问量足够大的时候,之前不用SSR的时候一台服务器可以承受的压力如今或许要增长到10台才能抗住。痛点在于SEO,但若是实际上对SEO要求并不高的时候,那使用SSR就大可没必要了。

那一样地,为何要引入node做为中间层呢?它是处在哪二者的中间?又是解决了什么场景下的问题?

在不用中间层的先后端分离开发模式下,前端通常直接请求后端的接口。但真实场景下,后端所给的数据格式并非前端想要的,但处于性能缘由或者其余的因素接口格式不能更改,这时候须要在前端作一些额外的数据处理操做。前端来操做数据自己无可厚非,可是当数据量变得庞大起来,那么在客户端就是产生巨大的性能损耗,甚至影响到用户体验。在这个时候,node中间层的概念便应运而生。

它最终解决的先后端协做的问题。

通常的中间层工做流是这样的:前端每次发送请求都是去请求node层的接口,而后node对于相应的前端请求作转发,用node去请求真正的后端接口获取数据,获取后再由node层作对应的数据计算等处理操做,而后返回给前端。这就至关于让node层替前端接管了对数据的操做。

640?wx_fmt=png

2、SSR框架中引入中间层

在以前搭建的SSR框架中,服务端和客户端请求利用的是同一套请求后端接口的代码,但这是不科学的。

对客户端而言,最好经过node中间层。而对于这个SSR项目而言,node开启的服务器原本就是一个中间层的角色,于是对于服务器端执行数据请求而言,就能够直接请求真正的后端接口啦。

//actions.js//参数server表示当前请求是否发生在node服务端const getUrl = (server) => {    return server ? 'xxxx(后端接口地址)' : '/api/sanyuan.json(node接口)';}//这个server参数是Home组件里面传过来的,//在componentDidMount中调用这个action时传入false,//在loadData函数中调用时传入true, 这里就不贴组件代码了export const getHomeList = (server) => {  return dispatch => {    return axios.get(getUrl(server))      .then((res) => {        const list = res.data.data;        dispatch(changeList(list))      })  }}
//参数server表示当前请求是否发生在node服务端
const getUrl = (server) => {
    return server ? 'xxxx(后端接口地址)' : '/api/sanyuan.json(node接口)';
}
//这个server参数是Home组件里面传过来的,
//在componentDidMount中调用这个action时传入false,
//在loadData函数中调用时传入true, 这里就不贴组件代码了
export const getHomeList = (server) => {
  return dispatch => {
    return axios.get(getUrl(server))
      .then((res) => {
        const list = res.data.data;
        dispatch(changeList(list))
      })
  }
}

在server/index.js应拿到前端的请求作转发,这里是直接用proxy形式来作,也能够用node单独向后端发送一次HTTP请求。

//增长以下代码import proxy from 'express-http-proxy';//至关于拦截到了前端请求地址中的/api部分,而后换成另外一个地址app.use('/api', proxy('http://xxxxxx(服务端地址)', {  proxyReqPathResolver: function(req) {    return '/api'+req.url;  }}));
import proxy from 'express-http-proxy';
//至关于拦截到了前端请求地址中的/api部分,而后换成另外一个地址
app.use('/api', proxy('http://xxxxxx(服务端地址)', {
  proxyReqPathResolverfunction(req{
    return '/api'+req.url;
  }
}));

3、请求代码优化

其实请求的代码仍是有优化的余地的,仔细想一想,上面的server参数实际上是不用传递的。

如今咱们利用axios的instance和thunk里面的withExtraArgument来作一些封装。

//新建server/request.jsimport axios from 'axios'const instance = axios.create({  baseURL: 'http://xxxxxx(服务端地址)'})export default instance//新建client/request.jsimport axios from 'axios'const instance = axios.create({  //即当前路径的node服务  baseURL: '/'})export default instance
import axios from 'axios'

const instance = axios.create({
  baseURL'http://xxxxxx(服务端地址)'
})

export default instance


//新建client/request.js
import axios from 'axios'

const instance = axios.create({
  //即当前路径的node服务
  baseURL: '/'
})

export default instance

而后对全局下store的代码作一个微调:

import {createStore, applyMiddleware, combineReducers} from 'redux';import thunk from 'redux-thunk';import { reducer as homeReducer } from '../containers/Home/store';import clientAxios from '../client/request';import serverAxios from '../server/request';const reducer = combineReducers({  home: homeReducer})export const getStore = () => {  //让thunk中间件带上serverAxios  return createStore(reducer, applyMiddleware(thunk.withExtraArgument(serverAxios)));}export const getClientStore = () => {  const defaultState = window.context ? window.context.state : {};   //让thunk中间件带上clientAxios  return createStore(reducer, defaultState, applyMiddleware(thunk.withExtraArgument(clientAxios)));}from 'redux';
import thunk from 'redux-thunk';
import { reducer as homeReducer } from '../containers/Home/store';
import clientAxios from '../client/request';
import serverAxios from '../server/request';

const reducer = combineReducers({
  home: homeReducer
})

export const getStore = () => {
  //让thunk中间件带上serverAxios
  return createStore(reducer, applyMiddleware(thunk.withExtraArgument(serverAxios)));
}
export const getClientStore = () => {
  const defaultState = window.context ? window.context.state : {};
   //让thunk中间件带上clientAxios
  return createStore(reducer, defaultState, applyMiddleware(thunk.withExtraArgument(clientAxios)));
}

如今Home组件中请求数据的action无需传参,actions.js中的请求代码以下:

export const getHomeList = () => {  //返回函数中的默认第三个参数是withExtraArgument传进来的axios实例  return (dispatch, getState, axiosInstance) => {    return axiosInstance.get('/api/sanyuan.json')      .then((res) => {        const list = res.data.data;        console.log(res)        dispatch(changeList(list))      })  }}const getHomeList = () => {
  //返回函数中的默认第三个参数是withExtraArgument传进来的axios实例
  return (dispatch, getState, axiosInstance) => {
    return axiosInstance.get('/api/sanyuan.json')
      .then((res) => {
        const list = res.data.data;
        console.log(res)
        dispatch(changeList(list))
      })
  }
}

至此,代码优化就作的差很少了,这种代码封装的技巧其实能够用在其余的项目当中,其实仍是比较优雅的。

part6: 多级路由渲染(renderRoutes)

如今将routes.js的内容改变以下:

import Home from './containers/Home';import Login from './containers/Login';import App from './App'//这里出现了多级路由export default [{  path: '/',  component: App,  routes: [    {      path: "/",      component: Home,      exact: true,      loadData: Home.loadData,      key: 'home',    },    {      path: '/login',      component: Login,      exact: true,      key: 'login',    }  ]}]from './containers/Home';
import Login from './containers/Login';
import App from './App'

//这里出现了多级路由
export default [{
  path'/',
  component: App,
  routes: [
    {
      path"/",
      component: Home,
      exacttrue,
      loadData: Home.loadData,
      key'home',
    },
    {
      path'/login',
      component: Login,
      exacttrue,
      key'login',
    }
  ]
}]

如今的需求是让页面公用一个Header组件,App组件编写以下:

import React from 'react';import Header from './components/Header';const  App = (props) => {  console.log(props.route)  return (    <div>      <Header></Header>    </div>  )}export default App;from 'react';
import Header from './components/Header';

const  App = (props) => {
  console.log(props.route)
  return (
    <div>
      <Header></Header>
    </div>

  )
}

export default App;

对于多级路由的渲染,须要服务端和客户端各执行一次。
所以编写的JSX代码都应有所实现:

//routes是指routes.js中返回的数组//服务端:<Provider store={store}>  <StaticRouter location={req.path} >    <div>      {renderRoutes(routes)}    </div>  </StaticRouter></Provider>//客户端:<Provider store={getClientStore()}>  <BrowserRouter>  <div>    {renderRoutes(routes)}  </div>  </BrowserRouter></Provider>
//服务端:
<Provider store={store}>
  <StaticRouter location={req.path} >
    <div>
      {renderRoutes(routes)}
    </div>
  </StaticRouter>

</Provider>

/
/客户端:
<Provider store={getClientStore()}>
  <BrowserRouter>
  <div>
    {renderRoutes(routes)}
  </
div>
  </BrowserRouter>
</Provider>

这里都用到了renderRoutes方法,其实它的工做很是简单,就是根据url渲染一层路由的组件(这里渲染的是App组件),而后将下一层的路由经过props传给目前的App组件,依次循环。

那么,在App组件就能经过props.route.routes拿到下一层路由进行渲染:

import React from 'react';import Header from './components/Header';//增长renderRoutes方法import { renderRoutes } from 'react-router-config';const  App = (props) => {  console.log(props.route)  return (    <div>      <Header></Header>      <!--拿到Login和Home组件的路由-->      {renderRoutes(props.route.routes)}    </div>  )}export default App;from 'react';
import Header from './components/Header';
//增长renderRoutes方法
import { renderRoutes } from 'react-router-config';

const  App = (props) => {
  console.log(props.route)
  return (
    <div>
      <Header></Header>
      <!--拿到Login和Home组件的路由-->
      {renderRoutes(props.route.routes)}
    </div>

  )
}

export default App;

至此,多级路由的渲染就完成啦。

part7: CSS的服务端渲染思路(context钩子变量)

1、客户端项目中引入CSS

仍是以Home组件为例

//Home/style.cssbody {  background: gray;}
body {
  background: gray;
}

如今,在Home组件代码中引入:

import styles from './style.css';from './style.css';

要知道这样的引入CSS代码的方式在通常环境下是运行不起来的,须要在webpack中作相应的配置。
首先安装相应的插件。

npm install style-loader css-loader --D
//webpack.client.jsconst path = require('path');const merge = require('webpack-merge');const config = require('./webpack.base');const clientConfig = {  mode: 'development',  entry: './src/client/index.js',  module: {    rules: [{      test: /\.css?$/,      use: ['style-loader', {        loader: 'css-loader',        options: {          modules: true        }      }]    }]  },  output: {    filename: 'index.js',    path: path.resolve(__dirname, 'public')  },}module.exports = merge(config, clientConfig);
const path = require('path');
const merge = require('webpack-merge');
const config = require('./webpack.base');

const clientConfig = {
  mode'development',
  entry'./src/client/index.js',
  module: {
    rules: [{
      test/\.css?$/,
      use: ['style-loader', {
        loader'css-loader',
        options: {
          modulestrue
        }
      }]
    }]
  },
  output: {
    filename'index.js',
    path: path.resolve(__dirname, 'public')
  },
}

module.exports = merge(config, clientConfig);
//webpack.base.js代码,回顾一下,配置了ES语法相关的内容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']          }        }]]      }    }]  }}
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']
          }
        }]]
      }
    }]
  }
}

好,如今在客户端CSS已经产生了效果。

640?wx_fmt=png

但是打开网页源代码:


640?wx_fmt=png

咦?里面并无出现任何有关CSS样式的代码啊!那这是什么缘由呢?很简单,其实咱们的服务端的CSS加载尚未作。接下来咱们来完成CSS代码的服务端的处理。


2、服务端CSS的引入

首先,来安装一个webpack的插件,

npm install -D isomorphic-style-loader

而后再webpack.server.js中作好相应的css配置:

//webpack.server.jsconst path = require('path');const nodeExternals = require('webpack-node-externals');const merge = require('webpack-merge');const config = require('./webpack.base');const serverConfig = {  target: 'node',  mode: 'development',  entry: './src/server/index.js',  externals: [nodeExternals()],  module: {    rules: [{      test: /\.css?$/,      use: ['isomorphic-style-loader', {        loader: 'css-loader',        options: {          modules: true        }      }]    }]  },  output: {    filename: 'bundle.js',    path: path.resolve(__dirname, 'build')  }}module.exports = merge(config, serverConfig);
const path = require('path');
const nodeExternals = require('webpack-node-externals');
const merge = require('webpack-merge');
const config = require('./webpack.base');

const serverConfig = {
  target'node',
  mode'development',
  entry'./src/server/index.js',
  externals: [nodeExternals()],
  module: {
    rules: [{
      test/\.css?$/,
      use: ['isomorphic-style-loader', {
        loader'css-loader',
        options: {
          modulestrue
        }
      }]
    }]
  },
  output: {
    filename'bundle.js',
    path: path.resolve(__dirname, 'build')
  }
}

module.exports = merge(config, serverConfig);

它作了些什么事情?

再看看这行代码:

import styles from './style.css';from './style.css';

引入css文件时,这个isomorphic-style-loader帮咱们在styles中挂了三个函数。输出styles看看:

640?wx_fmt=png

如今咱们的目标是拿到CSS代码,直接经过styles._getCss便可得到。


那咱们拿到CSS代码后放到哪里呢?其实react-router-dom中的StaticRouter中已经帮咱们准备了一个钩子变量context。以下

//context从外界传入<StaticRouter location={req.path} context={context}>    <div>        {renderRoutes(routes)}    </div></StaticRouter>
<StaticRouter location={req.path} context={context}>
    <div>
        {renderRoutes(routes)}
    </div>

</StaticRouter>

这就意味着在路由配置对象routes中的组件都能在服务端渲染的过程当中拿到这个context,并且这个context对于组件来讲,就至关于组件中的props.staticContext。而且,这个props.staticContext只会在服务端渲染的过程当中存在,而客户端渲染的时候不会被定义。这就让咱们可以经过这个变量来区分两种渲染环境啦。

如今,咱们须要在服务端的render函数执行以前,初始化context变量的值:

let context = { css: [] }css: [] }

咱们只须要在组件的componentWillMount生命周期中编写相应的逻辑便可:

componentWillMount() {  //判断是否为服务端渲染环境  if (this.props.staticContext) {    this.props.staticContext.css.push(styles._getCss())  }}//判断是否为服务端渲染环境
  if (this.props.staticContext) {
    this.props.staticContext.css.push(styles._getCss())
  }
}

服务端的renderToString执行完成后,context的CSS如今已是一个有内容的数组,让咱们来获取其中的CSS代码:

//拼接代码const cssStr = context.css.length ? context.css.join('\n') : '';
const cssStr = context.css.length ? context.css.join('\n') : '';

如今挂载到页面:

//放到返回的html字符串里的header里面<style>${cssStr}</style>
<style>${cssStr}</style>
640?wx_fmt=png

网页源代码中看到了CSS代码,效果也没有问题。CSS渲染完成!


3、利用高阶组件优化代码

也许你已经发现,对于每个含有样式的组件,都须要在componentWillMount生命周期中执行彻底相同的逻辑,对于这些逻辑咱们是否可以把它封装起来,不用反复出现呢?

实际上是能够实现的。利用高阶组件就能够完成:

//根目录下建立withStyle.js文件import React, { Component } from 'react';//函数返回组件//须要传入第一个参数是须要装饰的组件//第二个参数是styles对象export default (DecoratedComponent, styles) => {  return class NewComponent extends Component {    componentWillMount() {      //判断是否为服务端渲染过程      if (this.props.staticContext) {        this.props.staticContext.css.push(styles._getCss())      }    }    render() {      return <DecoratedComponent {...this.props} />    }  }}
import React, { Component } from 'react';
//函数返回组件
//须要传入第一个参数是须要装饰的组件
//第二个参数是styles对象
export default (DecoratedComponent, styles) => {
  return class NewComponent extends Component {
    componentWillMount() {
      //判断是否为服务端渲染过程
      if (this.props.staticContext) {
        this.props.staticContext.css.push(styles._getCss())
      }
    }
    render() {
      return <DecoratedComponent {...this.props} />
    }
  }
}

而后让这个导出的函数包裹咱们的Home组件。

import WithStyle from '../../withStyle';//......const exportHome = connect(mapStateToProps, mapDispatchToProps)(withStyle(Home, styles));export default exportHome;from '../../withStyle';
//......
const exportHome = connect(mapStateToProps, mapDispatchToProps)(withStyle(Home, styles));
export default exportHome;

这样是否是简洁不少了呢?未来对于愈来愈多的组件,采用这种方式也是彻底能够的。

part8: 作好SEO的一些技巧,引入react-helmet

这一节咱们来简单的聊一点SEO相关的内容。

1、SEO技巧分享

所谓SEO(Search Engine Optimization),指的是利用搜索引擎的规则提升网站在有关搜索引擎内的天然排名。如今的搜索引擎爬虫通常是全文分析的模式,分析内容涵盖了一个网站主要3个部分的内容:文本、多媒体(主要是图片)和外部连接,经过这些来判断网站的类型和主题。所以,在作SEO优化的时候,能够围绕这三个角度来展开。

对于文原本说,尽可能不要抄袭已经存在的文章,以写技术博客为例,东拼西凑抄来的文章排名通常不会高,若是须要引用别人的文章要记得声明出处,不过最好是原创,这样排名效果会比较好。多媒体包含了视频、图片等文件形式,如今比较权威的搜索引擎爬虫好比Google作到对图片的分析是基本没有问题的,所以高质量的图片也是加分项。另外是外部连接,也就是网站中a标签的指向,最好也是和当前网站相关的一些连接,更容易让爬虫分析。

固然,作好网站的门面,也就是标题和描述也是相当重要的。如:

640?wx_fmt=png

网站标题中不只仅包含了关键词,并且有比较详细和靠谱的描述,这让用户一看到就以为很是亲切和可靠,有一种想要点击的冲动,这就代表网站的 转化率比较高。


2、引入react-helmet

而React项目中,开发的是单页面的应用,页面始终只有一份title和description,如何根据不一样的组件显示来对应不一样的网站标题和描述呢?

实际上是能够作到的。

npm install react-helmet --save

组件代码:(仍是以Home组件为例)

import { Helmet } from 'react-helmet';//...render() {     return (      <Fragment>        <!--Helmet标签中的内容会被放到客户端的head部分-->        <Helmet>          <title>这是三元的技术博客,分享前端知识</title>          <meta name="description" content="这是三元的技术博客,分享前端知识"/>        </Helmet>        <div className="test">          {            this.getList()          }        </div>      </Fragment>    );//...from 'react-helmet';

//...
render() { 
    return (
      <Fragment>
        <!--Helmet标签中的内容会被放到客户端的head部分-->
        <Helmet>
          <title>这是三元的技术博客,分享前端知识</title>
          <meta name="description" content="这是三元的技术博客,分享前端知识"/>
        </Helmet>
        <div className="test">
          {
            this.getList()
          }
        </div>
      </Fragment>

    );
//...

这只是作了客户端的部分,在服务端仍须要作相应的处理。

其实也很是简单:

//server/utils.jsimport { renderToString } from 'react-dom/server';import {  StaticRouter } from 'react-router-dom'; import React from 'react';import { Provider } from "react-redux";import { renderRoutes } from 'react-router-config';import { Helmet } from 'react-helmet';export const render = (store, routes, req, context) => {  const content = renderToString(    <Provider store={store}>      <StaticRouter location={req.path} context={context}>        <div>          {renderRoutes(routes)}        </div>      </StaticRouter>    </Provider>  );  //拿到helmet对象,而后在html字符串中引入  const helmet = Helmet.renderStatic();  const cssStr = context.css.length ? context.css.join('\n') : '';  return  `    <html>      <head>        <style>${cssStr}</style>        ${helmet.title.toString()}        ${helmet.meta.toString()}      </head>      <body>        <div id="root">${content}</div>        <script>          window.context = {            state: ${JSON.stringify(store.getState())}          }        </script>        <script src="/index.js"></script>      </body>    </html>  `};
import { renderToString } from 'react-dom/server';
import {  StaticRouter } from 'react-router-dom'
import React from 'react';
import { Provider } from "react-redux";
import { renderRoutes } from 'react-router-config';
import { Helmet } from 'react-helmet';

export const render = (store, routes, req, context) => {
  const content = renderToString(
    <Provider store={store}>
      <StaticRouter location={req.path} context={context}>
        <div>
          {renderRoutes(routes)}
        </div>
      </StaticRouter>
    </Provider>

  );
  //拿到helmet对象,而后在html字符串中引入
  const helmet = Helmet.renderStatic();

  const cssStr = context.css.length ? context.css.join('\n') : '';

  return  `
    <html>
      <head>
        <style>${cssStr}</style>
        ${helmet.title.toString()}
        ${helmet.meta.toString()}
      </head>
      <body>
        <div id="root">${content}</div>
        <script>
          window.context = {
            state: ${JSON.stringify(store.getState())}
          }
        </script>
        <script src="/index.js"></script>
      </body>
    </html>
  `

};

如今来看看效果:

640?wx_fmt=png

网页源代码中显示出对应的title和description, 客户端的显示也没有任何问题,大功告成!


关于React的服务端渲染原理,就先分享到这里,内容仍是比较复杂的,对于前端的综合能力要求也比较高,可是坚持跟着学下来,必定会大有裨益的。相信你看了这一系列以后也有能力造出本身的SSR轮子,更加深入地理解这一方面的技术。

参考资料:

慕课网《React服务器渲染原理解析与实践》课程

往期 精彩回顾 从 URL 输入到页面展示到底发生了什么
你未必知道的 49 个 css 知识点 学习方法分享:为什么我一年半就能拿到大厂 offer


640?wx_fmt=png

640?wx_fmt=gif“阅读原文”,获取原文地址(能查阅外链)“ 在看”的永远18岁~ 640?wx_fmt=gif