彻底理解React 之React SSR、React服务端渲染,
技术栈: React16.x + React-router4.x + React-redux5.x + Redux-thunk2.x + express4.x
前言
前段时间研究了下React SSR,SEO。后面就想把整个过程总结一下,同时也加深自己对其的理解 。(好慌!第一次写简书) 关于服务端渲染的优缺点,vue服务端渲染官方文档讲的很清楚。网上关于React的SSR也很多,但都不够详细。不过这篇文章我将详细的一步一步介绍、深入配置React SSR、让每个看到的人都能看懂。 SSR对于大部分场景最主要还是两点 提高首屏加载速度 和方便SEO.为了快速构建开发环境,这里直接从githup上下载了一个别人写好的 使用create-react-app搭建的项目 。传送门(https://github.com/wujiabk/zhaopinApp)进行从零搭建服务端渲染,欢迎交流。
为什么使用服务器端渲染(SSR)?
与传统 SPA(Single-Page Application - 单页应用程序)相比,服务器端渲染(SSR)的优势主要在于:
1.更好的 SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面。
2.解决首屏白屏问题
3.顺应时代潮流(开玩笑啦!)
开始!
准备工作:
// 简单修改index.js代码,把路由抽离到app.js中,如下:
import React from 'react';
import ReactDOM from 'react-dom';
import {
createStore,
applyMiddleware,
compose
} from "redux";
import { Provider } from "react-redux";
import thunk from "redux-thunk";
import {
BrowserRouter
} from "react-router-dom";
import reducers from "./reducer";
import Routers from './router'
// 引入antd css
import 'antd-mobile/dist/antd-mobile.css';
const store = createStore(reducers,compose(
applyMiddleware(thunk),
));
ReactDOM.render(
<Provider store={store}>
<BrowserRouter>
<Routers/>
</BrowserRouter>
</Provider>,
document.getElementById('root')
);
项目上线一般都是前端build好包,后台去部署,ok,咱们打包下。
image.png
此时项目中多出来一个build文件,里面有asset-manifest.json的文件,里面有咱们打包后js、css的路径,仔细的同学可以看到每个路径后面都有一个哈希值,哈希值每次打包的都不一样,来区分版本。我们从找个入口就可以提取到我们打包后的代码,为以后做服务端渲染做铺垫!
image.png
正式开始
1.首先我们要看到build以后的文件,需要在本地搭建一个server,修改server文件的server.js,如下:
const express = require("express");
const bodyParser = require("body-parser");
const cookieParser = require("cookie-parser");
const userRoute = require("./userRoute");
const app = express();
const path = require('path');
app.use(cookieParser());
app.use(bodyParser.json());
// 用户接口模块
app.use("/user",userRoute);
// 映射到build后的路径
//设置build以后的文件路径 项目上线用
app.use((req, res, next) => {
if (req.url.startsWith('/user/') || req.url.startsWith('/static/')) {
return next()
}
return res.sendFile(path.resolve('build/index.html'))
})
app.use('/', express.static(path.resolve('build')))
app.listen("9000",function(){
console.log("open Browser http://localhost:9000");
});
然后配置package.json,加一条命令,scripts如下:
"scripts": {
"start": "node scripts/start.js",
"build": "node scripts/build.js",
"test": "node scripts/test.js --env=jsdom",
"server1": "nodemon server/server.js"
},
然后运行cnpm run server1,打开http://localhost:9000/
可以看到项目已经运行成功,此时我们的端口是9000和开发端口3000已经没关系了。并且代码运行的都是build以后的代码
想要做到首屏SSR,就需要后台返回首屏login页面所有的jsx代码,因为nodejs不支持es6、es7语法,所以需要手动配置下,ok:
.... 运行cnpm install babel-cli --save
这个是babel命令行工具,我们要用到它里面的babel-node
修改package.json里面scripts,如下:
"scripts": {
"start": "node scripts/start.js",
"build": "node scripts/build.js",
"test": "node scripts/test.js --env=jsdom",
"server": "cross-env NODE_ENV=test nodemon --exec babel-node server/server.js",
"server1": "nodemon server/server.js"
},
设置node环境为test,默认为node 修改为nodemon --exec babel-node
需要注意:
cross-env能跨平台地设置及使用环境变量
大多数情况下,在windows平台下使用类似于: NODE_ENV=production的命令行指令会卡住,windows平台与POSIX在使用命令行时有许多区别(例如在POSIX,使用$ENV_VAR,在windows,使用%ENV_VAR%。。。)
cross-env让这一切变得简单,不同平台使用唯一指令,无需担心跨平台问题
运行:cnpm i --save-dev cross-env 。现在服务端已经支持es6、es7语法了。
但是现在还不支持jsx语法,既然客户端可以用babel支持,服务端当然也可以了,我们新建.babelrc 里面写入、
{
"presets": [
"react-app"
],
"plugins": [
"transform-decorators-legacy"
]
}
ReactDOMServer.renderToString
这里简单解释下,React.createElement把React类进行实例化,实例化后的组件就可以进行mount操作了,在浏览器环境我们是使用ReactDOM.render()来进行挂载操作的。ReactDOMServer.renderToString则是把React实例渲染成HTML标签。接下里就需要吧index.js里面代码搬到server.js里面,渲染成html返回给前端就OK!
进行代码改造,完成后如下:
const express = require("express");
const bodyParser = require("body-parser");
const cookieParser = require("cookie-parser");
const userRoute = require("./userRoute");
const app = express();
const path = require('path');
app.use(cookieParser());
app.use(bodyParser.json());
/**
* 插入react代码 进行服务端改造
*/
import React from 'react';
import ReactDOM from 'react-dom';
import {
createStore,
applyMiddleware,
compose
} from "redux";
import { Provider } from "react-redux";
import thunk from "redux-thunk";
// 引入renderToString
import { renderToString, renderToStaticMarkup } from 'react-dom/server';
// 服务端是没有BrowserRouter 所以用StaticRouter
import { StaticRouter } from "react-router-dom";
// 引入reducer
import reducers from "../src/reducer";
// 引入前端路由
import Routers from '../src/router'
const store = createStore(reducers, compose(
applyMiddleware(thunk),
));
// 用户接口模块
app.use("/user", userRoute);
// 映射到build后的路径
//设置build以后的文件路径 项目上线用
app.use((req, res, next) => {
if (req.url.startsWith('/user/') || req.url.startsWith('/static/')) {
return next()
}
const context = {}
const frontComponents = renderToString(
(<Provider store={store}>
<StaticRouter
location={req.url}
context={context}>
<Routers />
</StaticRouter>
</Provider>)
)
res.send(frontComponents)
// return res.sendFile(path.resolve('build/index.html'))
})
app.use('/', express.static(path.resolve('build')))
app.listen("9000", function () {
console.log("open Browser http://localhost:9000");
});
可以看到我们将前端代码用renderToString()处理后返回给前端,这样前端就能接收到首屏加载所需要的代码了,但是需要注意的是,此时控制台报错了。
image.png
这个报错意思就是后端不支持css,所以我们要做处理。
运行 cnpm install css-modules-require-hook --save
配置代码:
module.exports = {
generateScopedName: '[name]__[local]___[hash:base64:5]',
}
然后在服务端server.js引入css配置进行代码改造,最终如下:
需要注意的此处有一小坑,csshook必须放在第一行,代码初始化的时候先引入css
// 处理css
import csshook from 'css-modules-require-hook/preset';
const express = require("express");
const bodyParser = require("body-parser");
const cookieParser = require("cookie-parser");
const userRoute = require("./userRoute");
const app = express();
const path = require('path');
app.use(cookieParser());
app.use(bodyParser.json());
/**
* 插入react代码 进行服务端改造
*/
import React from 'react';
import ReactDOM from 'react-dom';
import {
createStore,
applyMiddleware,
compose
} from "redux";
import { Provider } from "react-redux";
import thunk from "redux-thunk";
// 引入renderToString
import { renderToString, renderToStaticMarkup } from 'react-dom/server';
// 服务端是没有BrowserRouter 所以用StaticRouter
import { StaticRouter } from "react-router-dom";
// 引入reducer
import reducers from "../src/reducer";
// 引入前端路由
import Routers from '../src/router'
const store = createStore(reducers, compose(
applyMiddleware(thunk),
));
// 用户接口模块
app.use("/user", userRoute);
// 映射到build后的路径
//设置build以后的文件路径 项目上线用
app.use((req, res, next) => {
if (req.url.startsWith('/user/') || req.url.startsWith('/static/')) {
return next()
}
const context = {}
const frontComponents = renderToString(
(<Provider store={store}>
<StaticRouter
location={req.url}
context={context}>
<Routers />
</StaticRouter>
</Provider>)
)
res.send(frontComponents)
// return res.sendFile(path.resolve('build/index.html'))
})
app.use('/', express.static(path.resolve('build')))
app.listen("9000", function () {
console.log("open Browser http://localhost:9000");
});
处理好css后,运行cnpm run server,此时看到报错:
image.png
这个错误的意思就是服务端还没有处理图片问题,ok,让我们处理下
运行 cnpm install asset-require-hook --save 成功后引入配置,改造服务端server.js代码,改造后如下:
// 处理css
import csshook from 'css-modules-require-hook/preset';
// 处理图片
import assethook from 'asset-require-hook';
assethook({
extensions: ['png', 'jpg']
});
const express = require("express");
const bodyParser = require("body-parser");
const cookieParser = require("cookie-parser");
const userRoute = require("./userRoute");
const app = express();
const path = require('path');
app.use(cookieParser());
app.use(bodyParser.json());
/**
* 插入react代码 进行服务端改造
*/
import React from 'react';
import ReactDOM from 'react-dom';
import {
createStore,
applyMiddleware,
compose
} from "redux";
import { Provider } from "react-redux";
import thunk from "redux-thunk";
// 引入renderToString
import { renderToString, renderToStaticMarkup } from 'react-dom/server';
// 服务端是没有BrowserRouter 所以用StaticRouter
import { StaticRouter } from "react-router-dom";
// 引入reducer
import reducers from "../src/reducer";
// 引入前端路由
import Routers from '../src/router'
const store = createStore(reducers, compose(
applyMiddleware(thunk),
));
// 用户接口模块
app.use("/user", userRoute);
// 映射到build后的路径
//设置build以后的文件路径 项目上线用
app.use((req, res, next) => {
if (req.url.startsWith('/user/') || req.url.startsWith('/static/')) {
return next()
}
const context = {}
const frontComponents = renderToString(
(<Provider store={store}>
<StaticRouter
location={req.url}
context={context}>
<Routers />
</StaticRouter>
</Provider>)
)
res.send(frontComponents)
// return res.sendFile(path.resolve('build/index.html'))
})
app.use('/', express.static(path.resolve('build')))
app.listen("9000", function () {
console.log("open Browser http://localhost:9000");
});
刷新页面后,打开Network下面的response 可以看到服务端已经把前端html成功的返回
<div data-reactroot=""><div class="bigbox"><div class="logo-container"><img src="72ba71de2dfbe02c990266c62394b476.png" alt=""/></div><h1 style="color:red;text-align:center">React SSR </h1><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><div class="am-wingblank am-wingblank-lg"><div class="am-list"><div class="am-list-header"></div><div class="am-list-body"><div class="am-list-item am-input-item am-list-item-middle"><div class="am-list-line"><div class="am-input-label am-input-label-5"><i class="iconfont icon-yonghu c-blue"></i></div><div class="am-input-control"><input type="text" placeholder="请输入用户名" value=""/></div></div></div><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><div class="am-list-item am-input-item am-list-item-middle"><div class="am-list-line"><div class="am-input-label am-input-label-5"><i class="iconfont icon-mima c-blue" style="font-size:19px"></i></div><div class="am-input-control"><input type="password" placeholder="请输入密码" value=""/></div></div></div></div></div><div class="am-whitespace am-whitespace-md"></div><div class="ta-right"><a href="/">忘记密码?</a></div><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><a role="button" class="am-button am-button-primary" aria-disabled="false"><span>登 录</span></a><div class="am-whitespace am-whitespace-md"></div><a role="button" class="am-button am-button-primary" aria-disabled="false"><span>注 册</span></a><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div></div></div></div>
但是看页面
image.png
明显不是我们想要的结果,这是为什么呢?仔细想想,一个完整的页面,他是需要html body head footer 等标签元素构成的,所以现在缺少了一个支持页面的骨架,ok,那我们就在server端加上骨架返回给前端,对server.js进行改造如下:
// 处理css
import csshook from 'css-modules-require-hook/preset';
// 处理图片
import assethook from 'asset-require-hook';
assethook({
extensions: ['png', 'jpg']
});
const express = require("express");
const bodyParser = require("body-parser");
const cookieParser = require("cookie-parser");
const userRoute = require("./userRoute");
const app = express();
const path = require('path');
app.use(cookieParser());
app.use(bodyParser.json());
/**
* 插入react代码 进行服务端改造
*/
import React from 'react';
import ReactDOM from 'react-dom';
import {
createStore,
applyMiddleware,
compose
} from "redux";
import { Provider } from "react-redux";
import thunk from "redux-thunk";
// 引入antd css
import 'antd-mobile/dist/antd-mobile.css';
// 引入renderToString
import { renderToString, renderToStaticMarkup } from 'react-dom/server';
// 服务端是没有BrowserRouter 所以用StaticRouter
import { StaticRouter } from "react-router-dom";
// 引入reducer
import reducers from "../src/reducer";
// 引入前端路由
import Routers from '../src/router'
const store = createStore(reducers, compose(
applyMiddleware(thunk),
));
// 用户接口模块
app.use("/user", userRoute);
// 映射到build后的路径
//设置build以后的文件路径 项目上线用
app.use((req, res, next) => {
if (req.url.startsWith('/user/') || req.url.startsWith('/static/')) {
return next()
}
const context = {}
const frontComponents = renderToString(
(<Provider store={store}>
<StaticRouter
location={req.url}
context={context}>
<Routers />
</StaticRouter>
</Provider>)
)
// 新建骨架
const _frontHtml = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<title>人才市场</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root">${frontComponents}</div>
</body>
</html>`
res.send(_frontHtml)
// return res.sendFile(path.resolve('build/index.html'))
})
app.use('/', express.static(path.resolve('build')))
app.listen("9000", function () {
console.log("open Browser http://localhost:9000");
});
此时刷新页面后,打开Network下面的response 可以看到服务端已返回结果是
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<title>人才市场</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"><div data-reactroot=""><div class="bigbox"><div class="logo-container"><img src="72ba71de2dfbe02c990266c62394b476.png" alt=""/></div><h1 style="color:red;text-align:center">React SSR </h1><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><div class="am-wingblank am-wingblank-lg"><div class="am-list"><div class="am-list-header"></div><div class="am-list-body"><div class="am-list-item am-input-item am-list-item-middle"><div class="am-list-line"><div class="am-input-label am-input-label-5"><i class="iconfont icon-yonghu c-blue"></i></div><div class="am-input-control"><input type="text" placeholder="请输入用户名" value=""/></div></div></div><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><div class="am-list-item am-input-item am-list-item-middle"><div class="am-list-line"><div class="am-input-label am-input-label-5"><i class="iconfont icon-mima c-blue" style="font-size:19px"></i></div><div class="am-input-control"><input type="password" placeholder="请输入密码" value=""/></div></div></div></div></div><div class="am-whitespace am-whitespace-md"></div><div class="ta-right"><a href="/">忘记密码?</a></div><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><a role="button" class="am-button am-button-primary" aria-disabled="false"><span>登 录</span></a><div class="am-whitespace am-whitespace-md"></div><a role="button" class="am-button am-button-primary" aria-disabled="false"><span>注 册</span></a><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div></div></div></div></div>
</body>
</html>
我们的代码已经成功有了骨架,ok,离成功更进一步!
但是细心的同学一定发现,此时页面还不是我们想要的结果,因为我们只是生成了html,并没有引入css 和 js ,文章开头我们说过build以后有一个asset-manifest.json的文件,里面有我们想要的css和js,我们引入它,就可以拿到每次build以后最新的代码,对server.js进行代码改造如下:
// 处理css
import csshook from 'css-modules-require-hook/preset';
// 处理图片
import assethook from 'asset-require-hook';
assethook({
extensions: ['png', 'jpg']
});
const express = require("express");
const bodyParser = require("body-parser");
const cookieParser = require("cookie-parser");
const userRoute = require("./userRoute");
const app = express();
const path = require('path');
app.use(cookieParser());
app.use(bodyParser.json());
/**
* 插入react代码 进行服务端改造
*/
import React from 'react';
import ReactDOM from 'react-dom';
import {
createStore,
applyMiddleware,
compose
} from "redux";
import { Provider } from "react-redux";
import thunk from "redux-thunk";
// 引入antd css
import 'antd-mobile/dist/antd-mobile.css';
// 引入renderToString
import { renderToString, renderToStaticMarkup } from 'react-dom/server';
// 服务端是没有BrowserRouter 所以用StaticRouter
import { StaticRouter } from "react-router-dom";
// 引入reducer
import reducers from "../src/reducer";
// 引入前端路由
import Routers from '../src/router';
// 引入css 和 js
import buildPath from '../build/asset-manifest.json';
const store = createStore(reducers, compose(
applyMiddleware(thunk),
));
// 用户接口模块
app.use("/user", userRoute);
// 映射到build后的路径
//设置build以后的文件路径 项目上线用
app.use((req, res, next) => {
if (req.url.startsWith('/user/') || req.url.startsWith('/static/')) {
return next()
}
const context = {}
const frontComponents = renderToString(
(<Provider store={store}>
<StaticRouter
location={req.url}
context={context}>
<Routers />
</StaticRouter>
</Provider>)
)
// 新建骨架
const _frontHtml = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<title>人才市场</title>
<link rel="stylesheet" type="text/css" href="/${buildPath['main.css']}">
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root">${frontComponents}</div>
<script src="/${buildPath['main.js']}"></script>
</body>
</html>`
res.send(_frontHtml)
// return res.sendFile(path.resolve('build/index.html'))
})
app.use('/', express.static(path.resolve('build')))
app.listen("9000", function () {
console.log("open Browser http://localhost:9000");
});
刷新页面后可以看到基本是我们想要的页面了,可以看到后台已经返回给一个完整的html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<title>人才市场</title>
<link rel="stylesheet" type="text/css" href="/static/css/main.f4dbdb58.css">
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"><div data-reactroot=""><div><div class="am-navbar am-navbar-dark"><div class="am-navbar-left" role="button"></div><div class="am-navbar-title">消息列表</div><div class="am-navbar-right"></div></div><div class="mt-45 mb-50"><div>msgpages</div></div><div class="am-tab-bar"><div class="am-tabs am-tabs-horizontal am-tabs-bottom"><div class="am-tabs-content-wrap" style="touch-action:pan-x pan-y;position:relative;left:-200%"><div class="am-tabs-pane-wrap am-tabs-pane-wrap-inactive"></div><div class="am-tabs-pane-wrap am-tabs-pane-wrap-inactive"><div class="am-tab-bar-item"></div></div><div class="am-tabs-pane-wrap am-tabs-pane-wrap-active"><div class="am-tab-bar-item"></div></div><div class="am-tabs-pane-wrap am-tabs-pane-wrap-inactive"><div class="am-tab-bar-item"></div></div></div><div class="am-tabs-tab-bar-wrap"><div class="am-tab-bar-bar" style="background-color:white"><div class="am-tab-bar-tab"><div class="am-tab-bar-tab-icon" style="color:#888"><img class="am-tab-bar-tab-image" src="a12c878ee5f7d318376da191b5b76ef7.png" alt="BOSS"/></div><p class="am-tab-bar-tab-title" style="color:#888">BOSS</p></div><div class="am-tab-bar-tab"><div class="am-tab-bar-tab-icon" style="color:#888"><img class="am-tab-bar-tab-image" src="c6c95d08bf1b9a888cce1053bfa2bf18.png" alt="牛人"/></div><p class="am-tab-bar-tab-title" style="color:#888">牛人</p></div><div class="am-tab-bar-tab"><div class="am-tab-bar-tab-icon" style="color:#108ee9"><img class="am-tab-bar-tab-image" src="f73fc85762cfbe7999c792ce031c7fce.png" alt="消息"/></div><p class="am-tab-bar-tab-title" style="color:#108ee9">消息</p></div><div class="am-tab-bar-tab"><div class="am-tab-bar-tab-icon" style="color:#888"><img class="am-tab-bar-tab-image" src="3bf7c1d72788277ba13047821af2d180.png" alt="我的"/></div><p class="am-tab-bar-tab-title" style="color:#888">我的</p></div></div></div></div></div></div></div></div>
<script src="/static/js/main.7993f0a1.js"></script>
</body>
</html>
基本上React 服务端渲染就结束了!恭喜你,你已经掌握它了。
到现在我们不妨回头想想,做SSR是为了什么,无非就是SEO,其实我们可以在_frontHtml拼接的时候做很多SEO优化,举个小栗子:
const urlObj = {
'/login': '我是登陆xxxxx',
'/register': '我是注册xxxx',
'/xxx': 'xxxxxx.....'
}
// 新建骨架
const _frontHtml = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<title>${urlObj[req.url]}</title>
<link rel="stylesheet" type="text/css" href="/${buildPath['main.css']}">
<meta name="keywords" content="seo、seo、seo、seo,搜到我吧!">
<meta name="description" content="${urlObj[req.url]}">
<meta name="author" content="你的大名">
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root">${frontComponents}</div>
<script src="/${buildPath['main.js']}"></script>
</body>
</html>`
结束....................
提升 ------ React16 服务端渲染带来新的API
renderToNodeStream
renderToNodeStream支持直接渲染到节点流。渲染到流可以减少你的内容的第一个字节(TTFB)的时间,在文档的下一部分生成之前,将文档的开头至结尾发送到浏览器。 当内容从服务器流式传输时,浏览器将开始解析HTML文档。速度是renderToString的三倍,ok,让我们吧server.js简单改造下。
// 处理css
import csshook from 'css-modules-require-hook/preset';
// 处理图片
import assethook from 'asset-require-hook';
assethook({
extensions: ['png', 'jpg']
});
const express = require("express");
const bodyParser = require("body-parser");
const cookieParser = require("cookie-parser");
const userRoute = require("./userRoute");
const app = express();
const path = require('path');
app.use(cookieParser());
app.use(bodyParser.json());
/**
* 插入react代码 进行服务端改造
*/
import React from 'react';
import ReactDOM from 'react-dom';
import {
createStore,
applyMiddleware,
compose
} from "redux";
import { Provider } from "react-redux";
import thunk from "redux-thunk";
// 引入antd css
import 'antd-mobile/dist/antd-mobile.css';
// 引入renderToString
import { renderToString, renderToNodeStream } from 'react-dom/server';
// 服务端是没有BrowserRouter 所以用StaticRouter
import { StaticRouter } from "react-router-dom";
// 引入reducer
import reducers from "../src/reducer";
// 引入前端路由
import Routers from '../src/router';
// 引入css 和 js
import buildPath from '../build/asset-manifest.json';
const store = createStore(reducers, compose(
applyMiddleware(thunk),
));
// 用户接口模块
app.use("/user", userRoute);
// 映射到build后的路径
//设置build以后的文件路径 项目上线用
app.use((req, res, next) => {
if (req.url.startsWith('/user/') || req.url.startsWith('/static/')) {
return next()
}
const urlObj = {
'/login': '我是登陆xxxxx',
'/register': '我是注册xxxx',
'/xxx': 'xxxxxx.....'
}
res.write(`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<title>${urlObj[req.url]}</title>
<link rel="stylesheet" type="text/css" href="/${buildPath['main.css']}">
<meta name="keywords" content="seo、seo、seo、seo,搜到我吧!">
<meta name="description" content="${urlObj[req.url]}">
<meta name="author" content="你的大名">
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root">`)
const context = {}
const frontComponents = renderToNodeStream(
(<Provider store={store}>
<StaticRouter
location={req.url}
context={context}>
<Routers />
</StaticRouter>
</Provider>)
)
// 推送的前端
// end表示节点流还没有结束
frontComponents.pipe(res, { end: false })
// 监听事件结束后 把剩下的流推过去
frontComponents.on('end', _ => {
res.write(`</div>
<script src="/${buildPath['main.js']}"></script>
</body>
</html>`)
res.end()
})
})
app.use('/', express.static(path.resolve('build')))
app.listen("9000", function () {
console.log("open Browser http://localhost:9000");
});
用时我们要注意 前端render方法要改为hydrate方法避免报错
import React from 'react';
import ReactDOM from 'react-dom';
import {
createStore,
applyMiddleware,
compose
} from "redux";
import { Provider } from "react-redux";
import thunk from "redux-thunk";
import { BrowserRouter } from "react-router-dom";
import reducers from "./reducer";
import Routers from './router'
// 引入antd css
import 'antd-mobile/dist/antd-mobile.css';
const store = createStore(reducers, compose(
applyMiddleware(thunk),
));
ReactDOM.hydrate(
<Provider store={store}>
<BrowserRouter>
<Routers />
</BrowserRouter>
</Provider>,
document.getElementById('root')
);
刷新页面,可以看到跟刚才一模一样,但是速度是之前的三倍左右(官方是这么说的)
总结
我们已经学会了React15 和 React16的服务端渲染,并可以做小小的seo优化。
大概可以总结为两点:
1.搭建node环境,可以正式访问到线上文件(build包)
2.使用renderToString 把骨架拼接好返回给前端
但是中间需要注意的点很多处理css jsx 图片 babel 。
完结
觉得写的不错的小伙伴记得点赞+关注哦!-.-....