内容简介:大家好,我是神三元,这一次,让我们来把React服务端渲染(Server Side Render,简称“SSR”)学个明明白白。什么是服务端渲染?废话不多说,直接起一个express服务器。
大家好,我是神三元,这一次,让我们来把React服务端渲染(Server Side Render,简称“SSR”)学个明明白白。
一. SSR vs CSR
什么是服务端渲染?
废话不多说,直接起一个express服务器。
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。而且打开网页源代码:
也能够完成显示。
这就是服务端渲染。其实非常好理解,就是服务器返回一堆html字符串,然后让浏览器显示。
与服务端渲染相对的是客户端渲染(Client Side Render)。那什么是客户端渲染? 现在创建一个新的React项目,用脚手架生成项目,然后run起来。 这里你可以看到React脚手架自动生成的首页。
然而打开网页源代码。
body中除了兼容处理的noscript标签之外,只有一个id为root的标签。那首页的内容是从哪来的呢?很明显,是下面的script中拉取的JS代码控制的。
因此,CSR和SSR最大的区别在于前者的页面渲染是JS负责进行的,而后者是服务器端直接返回HTML让浏览器直接渲染。
为什么要使用服务端渲染呢?
传统CSR的弊端:
- 由于页面显示过程要进行JS文件拉取和React代码执行,首屏加载时间会比较慢。
- 对于SEO(Search Engine Optimazition,即搜索引擎优化),完全无能为力,因为搜索引擎爬虫只认识html结构的内容,而不能识别JS代码内容。
SSR的出现,就是为了解决这些传统CSR的弊端。
二、实现React组件的服务端渲染
刚刚起的express服务返回的只是一个普通的html字符串,但我们讨论的是如何进行React的服务端渲染,那么怎么做呢? 首先写一个简单的React组件:
// 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中非常重要的部分。希望对大家有所帮助,也欢迎大家在评论区交流。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- react同构实践——实现自己的同构模板
- 如何构建一个WEB同构应用
- React Native 三端同构实战
- React服务端渲染(前后端路由同构)
- React 中同构(SSR)原理脉络梳理
- 打造可降级的React服务端同构框架
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。