React-SSR浅析

React SSR 同构浅析

客户端渲染(CSR)

概念

传统 react项目,在浏览器打开项目,查看源代码,可以看到 index.js 页面里面只有一个基本的 DOM 节点,下面引入了一个 js 文件。而项目的所有内容就是通过这个 js 来渲染,通过禁止浏览器的 javascript 功能可以看到浏览器提示要 开启 javascript 功能的提示,没有项目的内容生成。

CSR 过程:

1
浏览器发送请求 => 服务器返回HTML => 浏览器发送 bundle.js 请求 => 服务器返回 bundle.js => 浏览器运行 React 代码 => 页面准备就绪

优势与劣势

优势:前后端分离,前端 ajax 请求数据,后端内部处理好逻辑返回 JSON 给前端处理。代码在客户端运行,性能良好

劣势:首屏加载速度慢。SEO 差(百度爬虫不能识别 js 文件)

服务器渲染(SSR)

概念

SSR 过程:

1
浏览器发送请求 => 服务器运行 React 代码生成页面 => 服务器返回页面

优势与劣势

优势:首屏速度快

劣势:服务器性能需求大

renderToString

1
2
3
4
5
6
7
8
9
10
11
12
import {renderToString} from 'react-dom/server';
const content = renderToString((
// 组件
))

app.get('/',(req,res,next)=>{
res.send(
`<html>
<div>${content}</div>
</html>`
)
})

路由 React-router-config

可识别嵌套路由,内置 ssr 方案

简单使用

服务器上呈现的路由都是无状态的,所以我们将应用程序包装在无状态的 <StaticRouter> e而不是<BrowserRouter>。从服务器传入请求的路由,以便路由可以匹配。

1
2
3
4
5
6
7
8
9
// client
<BroswerRouter>
<App />
</BroswerRouter>

// server
<StaticRouter location={req.url} context={context}>
<App />
</StaticRouter>

<Redirect>客户端呈现 a 标签的时候,浏览器历史记录会更改状态,我们获得新的屏幕。在静态服务器 换机中,我们无法更改应用程序的状态。我们可以使用 context 来找出需要渲染的结果。我们找到了 context.url 就知道应用是否被重定向了。

1
2
3
4
5
6
7
8
9
10
11
const context = {}
const markup = ReactDOMServer.renderToString(
<StaticRouter location={req.path} context={context}>
<App />
</StaticRouter>
)
if(context.url){
redirect(301,context.url)
}else{

}

添加应用特定上下文信息

路由器只会添加 context.url,如果希望重定向 301或者 302 可以在希望呈现的 UI 的某个特定分支发送 404 响应,或者如果未授权则发送 401。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function RedirectWithStats({from,to,status}){
return(
<Route render={({staticContext})=>{
if(staticContext) staticContext.status = status;
return <Redirect from={from} to={to} />
}}
/>
)
}

// somewhere in app

function App(){
return(
<Switch>
<RedirectWithStatus status={301} from='/users' to='/profiles' />
<RedirectWithStatus status={302} from='/courses' to='/dashboard' />
</Switch>
)
}

// server
const context = {};
const markup = ReactDOMServer.renderToString(
<StaticRouter context={context}>
<App />
</StaticRouter>
)
if(context.url){
redirect(context.status,context.url)
}

404,401 或者任何其他状态

跟上面的操作差不多,可以创建一个组件,添加一些上下文并在应用程序中的任何位置呈现它以获取不同的状态码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function Status({code,children}){
return(
<Route
render={({staticContext})=>{
if(staticContext) staticContext.status = code
return children;
}}
/>
)
}

function NotFound(){
return(
<Status code={404}>
<div>
<h1>Sorry,can't find that.</h1>
</div>
</Status>
)
}
// somewhere else

<Switch>
<Route path='/about' component={About} />
<Route path='/dashboard' component={Dashboard} />
<Route component={NotFound}>
</Switch>

组合

这不是一个真正的应用程序,但是它显示了将所有这些组合在一起所需要的所有常规部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// server
import {createServer} from 'http';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import {StaticRouter} from 'react-router';
import App from './App';

createServer((req,res)=>{
const context = {};
const html = ReactDOMServer.renderToString(
<StaticRouter location={req.url} context={context}>
<App />
</StaticRouter>
)
if(context.url){
res.writeHead(301,{
Location:context.url
})
res.send()
}else{
res.write(`<!doctype html><div id="app">${html}</div>`)
res.send();
}
}).listen(3000)


// client
import ReactDOM from 'react-dom';
import {BrowserRouter} from 'react-router-dom';
import App from './App';
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>
)

★数据的加载

在页面呈现之前加载数据,React Router 导出 matchPath 它在内部使用的静态函数,以匹配路由的位置。可以在服务器上使用此功能来帮助确定在呈现之前数据的依赖项。

这种方法的要求是依赖于静态路由的配置,用于在呈现之前根据路由匹配来加载对应的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// routes.js
const routes = {
{
path:'/',
component:Root,
loadData:()=> getSomeData()
}
}

import {router} from './routes'
function App(){
return(
<Switch>
{routes.map(route=>(
<Route {...route}/>
))}
</Switch>
)
}

// server
import {matchPath} from 'react-router-dom';

const promises = [];
routers.some(route=>{
const match = matchPath(req.path,route);
if(match) promises.push(route.loadData(match))
return match
})

Promise.all(promises).then(data=>{

})

Webpack 配置

公共部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module.exports = {
module: {
rules: [{
test: /\.js?$/,
loader: 'babel-loader',
exclude: /node_modules/,
options: {
presets: ['react', 'stage-0', ['env', {
// 版本兼容大部分浏览器最后2个
targets: {
browsers: ['last 2 versions']
}
}]]
}
}]
}
}

服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const path = require('path');
// 不打包 node modules
const nodeExternals = require('webpack-node-externals');
const merge = require('webpack-merge');
const config = require('./webpack.base.js');

const serverConfig = {
// 指明服务器
target: 'node',
mode: 'development',
entry: './src/server/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'build')
},
externals: [nodeExternals()],
module: {
rules: [{
test: /\.css?$/,
// 在高阶组件中通过注水跟脱水 css 来获取 css 代码,这个loader 提供了 ._getCss() 函数
use: ['isomorphic-style-loader', {
loader: 'css-loader',
options: {
importLoaders: 1,
modules: true,
localIdentName: '[name]_[local]_[hash:base64:5]'
}
}]
}]
}
}

module.exports = merge(config, serverConfig);

webpack-node-externals

不打包引入 的 node modules

isomorphic-style-loader

服务端打包 CSS 可以通过这个插件在对应的 DOM 元素上生成 class 类名,然后返回 CSS 样式代码。在高阶组件中,利用插件内置 API ._getCss()将CSS 数据通过注水与脱水的方式在服务端获取,将这些字符串格式的 css 添加到服务器渲染的 HTML 就可以完成 CSS 服务器渲染

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const path = require('path');
const merge = require('webpack-merge');
const config = require('./webpack.base.js');

const clientConfig = {
mode: 'development',
entry: './src/client/index.js',
output: {
filename: 'index.js',
path: path.resolve(__dirname, 'public')
},
module: {
rules: [{
test: /\.css?$/,
use: ['style-loader', {
loader: 'css-loader',
options: {
importLoaders: 1,
modules: true,
localIdentName: '[name]_[local]_[hash:base64:5]'
}
}]
}]
}
}

module.exports = merge(config, clientConfig);

package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
{
"name": "server",
"version": "1.0.0",
"description": "react-ssr",
"main": "index.js",
"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",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"react",
"ssr"
],
"author": "lbh",
"license": "ISC",
"dependencies": {
"axios": "^0.18.0",
"babel-core": "^6.26.3",
"babel-loader": "^7.1.5",
"babel-preset-env": "^1.7.0",
"babel-preset-react": "^6.24.1",
"babel-preset-stage-0": "^6.24.1",
"css-loader": "^2.1.1",
"express": "^4.16.4",
"express-http-proxy": "^1.5.1",
"isomorphic-style-loader": "^5.0.1",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-helmet": "^5.2.0",
"react-redux": "^7.0.1",
"react-router-config": "^5.0.0",
"react-router-dom": "^5.0.0",
"redux": "^4.0.1",
"redux-thunk": "^2.3.0",
"style-loader": "^0.23.1",
"webpack": "^4.29.6",
"webpack-cli": "^3.3.0",
"webpack-merge": "^4.2.1",
"webpack-node-externals": "^1.7.2"
}
}

nodemon

一个本地服务器,可以监控文件的变化来重启服务器,全局安装

npm-run-all

并行运行同名前缀的所有 script 语句,全局安装

0%