你们好,我是神三元,这一次,让咱们来把React服务端渲染(Server Side Render,简称“SSR”)学个明明白白。html
什么是服务端渲染?前端
废话很少说,直接起一个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
所以,CSR和SSR最大的区别在于前者的页面渲染是JS负责进行的,然后者是服务器端直接返回HTML让浏览器直接渲染。express
为何要使用服务端渲染呢?浏览器
SSR的出现,就是为了解决这些传统CSR的弊端。服务器
刚刚起的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代码来控制。因而服务端返回的代码变成了这样:
那么这个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')) 复制代码
这时候控制台会报错,
// 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中很是重要的部分。但愿对你们有所帮助,也欢迎你们在评论区交流。