详解react/redux的服务端渲染:页面性能与SEO

时间:2022-10-26 16:14:15
 
 

亟待解决的疑问

为什么服务端渲染首屏渲染快?(对比客户端首屏渲染)

 
react客户端渲染的一大痛点就是首屏渲染速度慢问题,因为react是一个单页面应用,大多数的资源需要在首次渲染前就加载好,这较大程度地拖慢了首屏渲染速度。有一些方式能够较好地解决这个问题:
 
1.webpack的按需加载(代码分割)http://www.css88.com/doc/webpack2/guides/code-splitting/ (这与本篇文章没有太大关系,所以我只丢链接)
2.我们这篇文章提到的react/redux的服务端渲染
 
客户端渲染,服务端渲染具体的渲染过程的比较:
详解react/redux的服务端渲染:页面性能与SEO
无论是客户端渲染,服务端渲染,它们都包含三个主体过程:
a:下载JS/CSS代码
b:请求数据
c:渲染页面
客户端渲染:a -> b ->c (a,b,c都在客户端进行)
服务端渲染:b -> c ->a (b,c在服务端进行,最后的a在客户端进行)
 
服务端渲染改变了a,b,c三个过程的执行顺序和执行方
 
为什么服务端渲染首屏渲染快
 
1.相比于客户端首屏渲染,服务端首屏渲染不需要在客户端下载JS/CSS代码请注意我说的是“首屏”),客户端接受服务端内容的时候,接受到的已经是完整的可视页面
2.服务端在内网请求数据(拉取数据),数据响应速度是很快的,而对于客户端渲染,外网http请求开销大,且受到具体的网络环境的限制
 
两个注意要点:“首屏”和“可视”
 
上面我在服务端首屏渲染中,强调了两个词:“首屏”“可视”
1.服务端只做首屏的渲染,后续的渲染过程都移交客户端处理,这是为了减少服务器的负担 (这个首屏渲染不需要在客户端下载JS代码)
2.服务端渲染的是“可视”页面,没错,就是字面意思,这个页面就“只是用来看的”,没有具体的交互功能!!,因为我们的JS代码还没下载好呀,而当具体的JS代码在客户端下载好并执行后,这个页面才具有了完整的交互功能
 
更详细的资料:Node直出理论与实践总结(详细:https://github.com/joeyguo/blog/issues/8
 
上文中描述的客户端渲染和服务端渲染,实际上对应了两种Web构建模式:前后分离模式和直出模式
 
模式一:前后分离模式(对应客户端渲染)
详解react/redux的服务端渲染:页面性能与SEO
模式二:直出模式(对应服务端渲染)
详解react/redux的服务端渲染:页面性能与SEO
 
 
 
最后对用服务端做react的首屏渲染做个比喻:在一场接力赛跑里,第一棒(首屏渲染)是尤为重要的,所以教练让一位健壮敏捷的小伙(服务端)来接,而当这位小伙把棒交给下一位选手后(客户端),他的任务(首屏渲染)也就结束了,而所有剩下的工作都交给这下一位伙伴去做了。
 
(体育差,可能比喻得不太好,见谅~~)
 

为什么服务端渲染有利于SEO?(对比客户端渲染)

 
原因很简单,因为客户端渲染全部依赖于虚拟DOM,而搜索引擎爬不到虚拟DOM主要是国内搜索引擎
 
为了直观地表述这一点,让我们看服务端渲染/客户端渲染下demo的源代码吧!这是我下面将要展示的demo的截图:
详解react/redux的服务端渲染:页面性能与SEO
这是客户端渲染时候的源代码:
详解react/redux的服务端渲染:页面性能与SEO
没错,在根div节点下一点HTML都看不到!这会让国内的搜索引擎非常苦恼,因为搜不到
但是当使用服务端做首屏渲染的时候它的源代码就变成了这样:
详解react/redux的服务端渲染:页面性能与SEO
这样搜索引擎就能搜到啦!(具体代码下面介绍)
 
是不是搜索引擎都爬不到虚拟DOM呢?NO!!
 
国外:谷歌可以,雅虎可以,BING可以,Duck Duck Go可以
国内:百度不可以。。。
 
具体看这篇文章:SEO vs. React: Web Crawlers are Smarter Than You Think
放一下文章中爬虚拟DOM的截图:
这是BING:
详解react/redux的服务端渲染:页面性能与SEO
这是雅虎:
  详解react/redux的服务端渲染:页面性能与SEO
这是百度
    详解react/redux的服务端渲染:页面性能与SEO 
 
综上,在国内做react产品,服务端首屏渲染还是很重要滴~~
 

服务端渲染的具体的代码

 
我们的src目录由三部分组成:common,client和server,利用express框架开启服务器
详解react/redux的服务端渲染:页面性能与SEO
 
展开后:
详解react/redux的服务端渲染:页面性能与SEO
 
它们间的关系如下:
详解react/redux的服务端渲染:页面性能与SEO
【注意】client和server部分的代码是本文着重要讲解的部分,common的部分就一笔带过了
 
好,先放我们的代码:
 
common部分:
 
action/index.js :
export const increment = () => {
return { type:'INCREMENT' }
} export const decrement = () => {
return { type:'DECREMENT' }
}
Reducer/index.js :
 
import { combineReducers } from 'redux'

const initState = { number:0 }
const counterReducer = (state = initState, action) => {
const { number } = state
switch (action.type) {
case 'INCREMENT':
return { number:number + 1}
case 'DECREMENT':
return { number:number - 1}
default:
return state
}
} export default combineReducers({ counterReducer })
store/index.js :
 
import { createStore } from 'redux'
import reducer from '../reducer' export default (preloadedState={}) => {
const store = createStore(
reducer,
preloadedState
)
return store
}
containner/index.js :
 
import React from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import ComponentApp from '../component'
import * as NumberActions from '../action' const mapStateToProps = state => {
return { number:state.counterReducer.number }
} const mapDispatchToProps = dispatch => {
return bindActionCreators(NumberActions,dispatch)
}
export default connect(mapStateToProps, mapDispatchToProps)(ComponentApp)
component/index.js :
 
import React from 'react'

class ComponentApp extends React.Component{
render () {
const { number, increment, decrement } = this.props
return (
<div>
<h1>{number}</h1>
<button onClick={increment}>增1</button>
<button onClick={decrement}>减1</button>
</div>
)
}
} export default ComponentApp
server部分:
 
server/server.js :
 
import React from 'react'
import path from'path'
import ReactDOMServer from 'react-dom/server'
import { Provider } from 'react-redux' import createStore from '../common/store'
import App from '../common/container' /************ 这部分代码参考自webpack-dev-middleware的官方文档 ************/
var express = require("express");
var webpackDevMiddleware = require("webpack-dev-middleware");
var webpack = require("webpack");
var webpackConfig = require('../../webpack.config.js'); var app = express(); var compiler = webpack(webpackConfig);
app.use(webpackDevMiddleware(compiler, {
// 这个publicPath参数要和webpack.config.js的`output.publicPath`参数保持一致
publicPath:webpackConfig.output.publicPath
})); /************ 这部分代码参考自webpack-dev-middlemare的官方文档 ************/
//链接 https://webpack.js.org/guides/development/#webpack-dev-middleware
/*
renderFullPage函数,渲染完整的首屏可视页面(这个页面渲染完毕后将被发送到客户端)
第一个参数是被转成字符串的APP,要将其插入入口HMTL文件中
第二个参数是初始化的state,将其放入window对象中以便在发送到客户端后能通过window.__INITIAL_STATE__取用
*/ const renderFullPage = (html, preloadState) => {
return `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<title>React App</title>
</head>
<body>
<div id="root">${html}</div>
<script>window.__INITIAL_STATE__ =${JSON.stringify(preloadState)}</script>
<script src="/static/bundle.js"></script>
</body>
</html>`
} const handleRender = (req, res) => {
// 初始化store,有两个作用:1.放入Provider的store属性中 2. 通过store.getState()获取初始化的state
const store = createStore()
// 将APP转成字符串
const html = ReactDOMServer.renderToString(
<Provider store={store}>
<App />
</Provider>
)
// 取得初始化的state
const preloadState = store.getState()
// 将渲染完整的首屏可视页面(字符串)发送到客户端显示
res.send(renderFullPage(html, preloadState))
} // 注册中间件函数,每当从客户端接收到请求的时候,运行handleRender函数
app.use(handleRender) // 监听3000端口
app.listen(3000, (error) => {
if (error) {
console.error(error)
}
})
 
server/index.js :
// 确保在node环境下能编译es6(es2015)和JSX(react)的语法
require('babel-core/register')({
presets: ['es2015', 'react']
})
require('./server')
client部分:
 
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import createStore from '../common/store'
import App from '../common/container' // 取得服务端发送过来的初始化state
const initialState = window.__INITIAL_STATE__
// 初始化store
const store = createStore(initialState)
// reactDOM渲染
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
 
【注意】:
1.对express框架不太熟悉的同学可看一下express的文档http://www.expressjs.com.cn/4x/api.html
2.我上面的例子和redux官方文档的例子大致相同,更详细的介绍请看这里:http://redux.js.org/docs/recipes/ServerRendering.html
 
demo如下,点击按钮让数字加一或减一
详解react/redux的服务端渲染:页面性能与SEO
【注意】采用客户端渲染和服务端渲染demo无大差异,区别在于首屏渲染的速度(服务端渲染要快)
 
React/redux服务端渲染的整体思路:
详解react/redux的服务端渲染:页面性能与SEO
【注意】最后客户端渲染的时候,因为服务端已经做了首屏渲染,所以这里不再重复渲染页面,而只挂载监听器,具体请看下面:
 

如何理解两个渲染过程?(ReactDOMServer.renderToString和 reactDOM.render的联系)

一开始让我感到疑惑的就是这两个过程,因为单从代码上看似乎我们做了两次重复的渲染,但实际上却并不是这样。
 
renderToString会将虚拟DOM转化成一段带有“标记”(markup)的HTML字符串,“标记”包括 id和校验和两部分,见下图:
详解react/redux的服务端渲染:页面性能与SEO
 
这段HTML字符串发送到客户端后,在调用ReactDOM.render()时候,将根据校验和(data-react-checksum)判断是否需要重新render:
 
1.校验和相同,只挂载事件监听器,不重新render
2.校验和不同,重新render
 
这告诉我们:当服务端/客户端共用APP的虚拟DOM的前提下,是不会有冗余的重渲染的
 
react文档原文:
Render a React element to its initial HTML. This should only be used on the server. React will return an HTML string. You can use this method to generate HTML on the server and send the markup down on the initial request for faster page loads and to allow search engines to crawl your pages for SEO purposes.
 
If you call ReactDOM.render() on a node that already has this server-rendered markup, React will preserve it and only attach event handlers, allowing you to have a very performant first-load experience.
 

为什么要把state(redux)从服务端传到客户端?

保证前后端数据的一致性
 

解决服务端渲染代码中的“痛点”

在node环境运行ES6语法和JSX语法——babel-core/register的使用

在做服务端渲染的时候,让我蛋疼的莫过于在server.js中,babel-loader插件和.babelrc文件失效了
 
我原本配置了.babelrc文件和wepack的babel-loader插件,可它们是针对浏览器环境的,在node环境下失效了,换而言之,我遭遇了无法在我的server.js中使用ES6语法和JSX语法的问题。
 
服务端ES6语法编译失败(注:这是在配好了.babelrc文件和wepack的babel-loader插件前提下发生的)
详解react/redux的服务端渲染:页面性能与SEO
服务端JSX语法(react)编译失败
详解react/redux的服务端渲染:页面性能与SEO
所以我在server.js中加了这一段代码:
require('babel-core/register')({
presets: ['es2015', 'react']
})
然后,编译成功!
详解react/redux的服务端渲染:页面性能与SEO
【注意】redux官方文档里还有其他的解决方法,原理类似,想了解更多请看redux官方文档http://redux.js.org/docs/recipes/ServerRendering.html
 

使发送到客户端的页面能访问打包后的bundle.js—— webpack.output.publicPath的使用

webpackDevMiddleware中的publicPath参数要和webpack.output.publicPath中的参数保持一致
 
例如:
这是我在webpack.config.js中的output参数:(关键在于publicPath)
output:{
filename:'bundle.js',
path:path.join(__dirname,'dist'),
publicPath: '/static'
}
这是我在server.js中的webpackDevMiddleware中的publicPath参数相关代码:
var webpackConfig = require('../../webpack.config.js');
// 省略其他内容
app.use(webpackDevMiddleware(compiler, {
publicPath:webpackConfig.output.publicPath // Same as `output.publicPath` in most cases.
}));
 
然后我们在输出的HTML页面中就可以通过指定的'/static目录去访问被webpack打包后的bundle.js文件了
详解react/redux的服务端渲染:页面性能与SEO
 

参考资料:文章标题,作者和链接(按先后顺序)

 
React同构直出优化总结 —— joeyguo
 
Node直出理论与实践总结 —— joeyguo
 
SEO vs. React: Web搜索引擎比你想的要聪明(需要*)——Patrick Hund
 
redux文档服务端渲染章节
 
react文档 ReactDOMServer的API
 
 
详解react/redux的服务端渲染:页面性能与SEO

详解react/redux的服务端渲染:页面性能与SEO的更多相关文章

  1. 【redux】详解react&sol;redux的服务端渲染:页面性能与SEO

        亟待解决的疑问 为什么服务端渲染首屏渲染快?(对比客户端首屏渲染)   react客户端渲染的一大痛点就是首屏渲染速度慢问题,因为react是一个单页面应用,大多数的资源需要在首次渲染前就加载 ...

  2. vuejs服务端渲染更好的SEO&comma;SSR完全指南Nuxt&period;js静态站生成器

    vuejs服务端渲染更好的SEO,SSR完全指南Nuxt.js静态站生成器SSR 完全指南https://cn.vuejs.org/v2/guide/ssr.html在 2.3 发布后我们发布了一份完 ...

  3. React&lpar;0&period;13&rpar; 服务端渲染的两个函数

    1.React.renderToString 函数,  参数是组件,返回一个字符串 <!DOCTYPE html> <html> <head> <title& ...

  4. react&plus;laravel与服务端渲染的几点思考

    一.前后端完全分离 1.用React.js做MVC中的V,剩下的交给Laravel 2.Laravel用来做API接口开发. 3.好处:实现了前后端开发的分离,从而加快前后端开发效率.另外若是多端的如 ...

  5. Asp&period;Net MVC 模型验证详解-实现客户端、服务端双重验证

    概要 在asp.net webform开发中经常会对用户提交输入的信息进行校验,一般为了安全起见大家都会在客户端进行Javascript(利于交互).服务端双重校验(安全).书写校验代码是一个繁琐的过 ...

  6. 解析Nuxt&period;js Vue服务端渲染摸索

    本篇文章主要介绍了详解Nuxt.js Vue服务端渲染摸索,写的十分的全面细致,具有一定的参考价值,对此有需要的朋友可以参考学习下.如有不足之处,欢迎批评指正. Nuxt.js 十分简单易用.一个简单 ...

  7. 教你如何在React及Redux项目中进行服务端渲染

    服务端渲染(SSR: Server Side Rendering)在React项目中有着广泛的应用场景 基于React虚拟DOM的特性,在浏览器端和服务端我们可以实现同构(可以使用同一份代码来实现多端 ...

  8. react服务端渲染框架

    客户端渲染 加载一个空的html页面,然后请求一个打包的js文件,然后再客户端执行这个js文件 动态生成html内容然后插入到DOM元素上,在源代码查询中也只能看到空的html文档 没有任何其他内容 ...

  9. 6&period;前端基于react&comma;后端基于&period;net core2&period;0的开发之路(6) 服务端渲染(SSR)

    0.源码地址 https://gitee.com/teambp/ScaffoldClient  这个地址下载对应源码. 1.服务端渲染是啥? 就是在服务器进行页面渲染(废话),当页面展示后,显示的就是 ...

随机推荐

  1. Websocket 概述

    WebSocket protocol 是HTML5一种新的协议.它实现了浏览器与服务器全双工通信(full-duplex). [[ from websocket是什么原理? ]] 一.WebSocke ...

  2. ubuntu双网卡bonding配置&lpar;转&rpar;

    1.安装软件 apt-get install ifenslave 2.修改配置文件 /etc/network/interfaces auto lo iface lo inet loopback ifa ...

  3. 批处理操作mysql数据库

    批处理操作mysql数据库 1.使用批处理自动登录mysql数据库 @echo offcd C:\program files\mysql\mysql server 5.5\binmysql -u ro ...

  4. boa介绍文档

    http://wenku.baidu.com/view/873aa903cc175527072208ce.html?re=view

  5. mysql TRUNCATE

    保留小数点 select truncate(field1,2) from table1 field3 字段类型为decimal(20,3)

  6. php hook 之简单例子

    <?php// 应用单例模式// 建立相应的 plugins 文件夹,并建立 .php 文件放在里面class plugin{    public $actions;    public $fi ...

  7. Elkstack2&period;0部署

    部署步骤如下: 1.1 资源拷贝 1 jdk1.8 2 kafka 3 kafka-manager 1.2 jvm 配置 vim /etc/profile.d/java.sh JAVA_HOME=/u ...

  8. thinkphp 5&period;0 模块设计

    模块设计 5.0版本对模块的功能做了灵活设计,默认采用多模块的架构,并且支持单一模块设计,所有模块的命名空间均以app作为根命名空间(可配置更改). 目录结构 标准的应用和模块目录结构如下: ├─ap ...

  9. Oracle DB 使用子查询来解决查询

    • 定义子查询 • 描述子查询可以解决的问题类型 • 列出子查询的类型 • 编写单行和多行子查询   • 子查询:类型.语法和准则 • 单行子查询: – 子查询中的组函数 – 带有子查询的HAVING ...

  10. Groovy 学习手册(2)

    二. 工具 1. 控制台 groovyConsole: Groovy 控制台是一个非常易于使用和简单的轻量级的编辑器.你可以在里面做很多事情. 在编辑器里面可以书写代码,Windows 下,按下Ctr ...