react_ssr笔记

react 实现 ssr 和原理讲解

使用 react 实现 ssr,并讲解实现 ssr 的原理。

服务端渲染

服务端渲染很简单,就是在服务端生成 html 代码字符串,然后返回给浏览器。
所以对前端来说就是一个请求。
本课程使用 express 启动服务。

在服务端写 react 代码

首先同样需要先安装 react 的包,babel 进行转义,不然因为写的是 node 代码,连 import 都不能直接使用,使用了 babel 这些,自然就需要使用 webpack 进行打包转义了。

webpack不仅可以打包前端代码,服务端代码也可以,只是在webpack的配置文件中,需要添加target:'node',告诉 webpack 这是服务端代码,不然像path这样的工具包会被打包进代码中,在服务端path工具全局环境中的,是可以直接使用的。

1
npm i webpack webpack-cli react react-dom babel-loader babel-core babel-preset-react@latest babel-preset-stage-0 babel-preset-env --save

babel 是个很好的转义工具,但是各种转义都需要安装对应的 preset,比如上面的 react,stage-0 语法等。

webpack-node-externals

虽然上面通过设置target可以避免 node 环境中已有包不会被打包到代码中,但是node_modules中安装的第三方包比如 express 还是被打包到引用它的文件中。这样会导致 express 被重复打包。
使用 webpack-node-externals 可以避免这种情况发生,这会保证各个文件中保持require('express')的写法。(有点像前端优化的动态链接库,自我感觉)

️⚠️ 不做这个处理,打包会报警告!

使用 renderToString 服务端渲染 react 组件

1
2
3
4
5
6
7
import React from "react";
import { renderToString } from "react-dom/server";
import Home from "./Home";

...

renderToString(<Home />)

前端渲染(CSR)和服务端渲染(SSR)的优缺点

SSR 有利于 SEO,但是对服务端性能消耗比较大。

webapck.server.js

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
const path = require("path");
const nodeExternals = require("webpack-node-externals");
module.exports = {
target: "node", // 指定运行环境是服务端环境
mode: "development", // 指定开发
entry: "./src/index.js",
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "build"),
},
externals: [nodeExternals()],
module: {
rules: [
{
test: /\.js?$/,
loader: "babel-loader",
exclude: ["/node_modules/"],
options: {
// 指定需要转义的内容,需要安装对应的babel插件。
presets: [
"react",
"stage-0",
/* 如何根据环境进行适配 */
[
"env",
{
targets: {
browsers: ["last 2 versions"], // 在打包编译的过程中会去兼容所有浏览器的最后两个版本,当然可以设置3个版等等。
},
},
],
],
},
},
],
},
};

自动打包和服务自动启动

打包和启动是两个命令,前端由 webpack-dev-server 可以帮忙解决这个问题,但是服务端,目前还没有这样的工具,只能把代码用 webpack 打包,然后用 node 启动,命令如下:

1
2
3
4
"scripts": {
"start": "node ./build/bundle.js",
"build": "webpack --config webpack.server.js"
},

解决上面的问题也很简单,webpack自带了检测功能,增加--watch参数即可;
node需要借助一个工具nodemon,nodemon启动的node服务可以自动检测启动文件所在目录是否发生变化,如果发生变化则自动重启服务。
nodemon也可以设置监控多个文件夹。

nodemon还有一个同类工具叫supervisor

1
npm i nodemon -g

修改后启动配置

1
2
3
4
"scripts": {
"start": "nodemon --watch build ./build/bundle.js",
"build": "webpack --config webpack.server.js --watch"
},

使用 npm-run-all 进一步提升开发效率

上面的配置虽然实现了自动打包,服务自动启动的功能,但是还是需要打开两个命令窗口,还是很麻烦。
使用 npm-run-all 就可以只打开一个窗口就可以了。

1
sudo npm i npm-run-all -g

配置如下:

1
2
3
4
5
"scripts": {
"dev": "npm-run-all --parallel dev:**",
"dev:start": "nodemon --watch build ./build/bundle.js",
"dev:build": "webpack --config webpack.server.js --watch"
},
  • parallel 指定并行执行
  • dev:**指定并行执行以”dev:“开头的所有 npm 命令

这样执行npm run dev两个服务就都启动了。

同构

服务端渲染时,如果在 react 组件上增加事件,当渲染到浏览器上时,事件是无法被绑定到组件上的。
这该怎么解决呢?这就需要使用同构

同构:就是一套 react 代码,在服务端执行一次,然后在客户端再执行一次。

让 react 组件在客户端再运行一次

这就比较简单了,这就是正常的前端 react 开发,把 react 组件和挂载打包后,在入口 html 引用就可以了。打包后的文件就是 js 吗,让打包的组件在客户端再挂载一次。
不同的是,这次挂载用的不是 ReactDom.render 而是 ReactDom.hydrate()方法。因为 react 组件已经被服务端渲染了一遍,做同构,就不要使用 render,而是使用 dydrate

服务端渲染挂载组件两边不要有文本节点,也就是把 react 组件转换成字符串 A 后,传给 html 字符串 B 时,A 要和它的父元素在一行,不要有空格、换行,挨着写就行了。

1
<div id="root">${renderToString(<Home />)}</div>
  1. 前端执行的 js 包入口

// src/client/index.js

1
2
3
4
5
import React from "react";
import ReactDom from "react-dom";
import Home from "../Home";

ReactDom.hydrate(<Home />, document.getElementById("root"));
  1. 配置 webpack 打包前端 js 包

webpack.server.jsnode的相关配置删除即可。把js文件打包到public文件夹中。

  1. 增加打包命令
1
"dev:build:client": "webpack --config webpack.client.js --watch"
  1. 在首页 html 中引入 client 打包的静态 js

使用expressstatic中间件,把public文件夹设置成静态资源请求目录。

1
2
3
4
5
/**
* 指定请求的静态资源都去public文件夹中获取
* 并且不用写public路径,如下面的index
* */
app.use(express.static("public"));
1
2
3
4
5
6
7
8
9
10
11
app.get("/", (req, res) => {
res.send(`<html>
<head>
<title>ssr</title>
</head>
<body>
<div id="root">${renderToString(<Home />)}</div>
</body>
<script src="/index.js"></script>
</html>`);
});

经过上述的修改,按钮的事件就被绑定到元素上了,点击就有反应了。

react 说白了就是 dom 操作,所以客户端再绑定一次,元素就被 dom 动态的又创建一次,事件什么的当然有了。

webpack 复用公共部分

由于webpack.server.jswebpack.client.js大部分都相同,所以可以抽出公共部分,两个“继承”后各自扩展就行了;使用webpack-merge可以达到这个目的。

1
npm i webpack-merge

服务端路由

路由同样需要进行同构操作,但服务端路由不是使用BrowserRouter,而是需要使用StaticRouter

首先安装 react 的 router 包react-router-domBrowserRouterStaticRouter是这个包中的两个模块

1
npm i react-router-dom --save

改造客户端和服务端使用路由进行渲染

前端使用BrowserRouter进行路由,改造client中的index.js

1
2
3
4
5
6
7
8
import React from "react";
import ReactDom from "react-dom";
import { BrowserRouter } from "react-router-dom";
import Routes from "../Routes";

const App = <BrowserRouter>{Routes}</BrowserRouter>;

ReactDom.hydrate(<App />, document.getElementById("root"));

服务端代码改造

1
2
3
4
5
6
7
8
<div id="root">
$
{renderToString(
<StaticRouter location={req.path} context={{}}>
{Routes}
</StaticRouter>,
)}
</div>

StaticRouterBrowserRouter不同,它不能自动感知 url 中的路由信息,所以需要设置location属性来告知服务端的 router 当前的路径是什么。

多个路由时的处理
因为 react 是单页面,所以服务端的路由需要都指定到入口文件上。比如增加了一个 login 页面,不能增加 get(/login),把入口的路由改成全部(*)即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
app.get("*", (req, res) => {
res.send(`<html>
<head>
<title>ssr</title>
</head>
<body>
<h1>hello,this is a ssr content.</h1>
<h1>下面是home组件的内容</h1>
<div id="root">${renderToString(
<StaticRouter location={req.path} context={{}}>
{Routes}
</StaticRouter>,
)}</div>
</body>
<script src="/index.js"></script>
</html>`);
});

同构 redux

加上 redux 比较简单,和正常的开发一样就行。客户端和服务端同样都做相同处理,不然报错。

1
npm i react-redux redux redux-thunk

相关代码:

1
2
3
4
5
6
7
8
9
10
const reducer = (state = { name: "温木" }, action) => {
return state;
};
const store = createStore(reducer, applyMiddleware(thunk));

<Provider store={store}>
<StaticRouter location={req.path} context={{}}>
{Routes}
</StaticRouter>
</Provider>;

抽离公共store

由于服务端和客户端的 store 是一样的,因此可以把公共的部分抽成公共的模块, 但是需要注意的是,createStore应该每次调动时都生成新的 store,不然所有的服务就会调用的是同一个 store

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk";

const reducer = (state = { name: "温木" }, action) => {
return state;
};

export default function getStore() {
return createStore(reducer, applyMiddleware(thunk));
}

// 下面是错误代码,下面的写法会让所有的服务共用一个store
// const store = createStore(reducer, applyMiddleware(thunk));
// export default store;

Warning: Expected server HTML to contain a matching <div> in <div>.
引起这种 bug 我遇到有两种:

  1. 只在客户端渲染使用了路由,服务端渲染没有使用,服务端加上即可
  2. 服务端在渲染是多嵌套了 div,比如我遇到的就是把 html 字符串封装成了方法 render,但是原来的 html 字符串忘记删除,在 html 字符串中又调用了 render,所以导致了这个警告。

服务端渲染时加载 store 数据

通常情况下,我们请求数据都在componentDidMount声明周期中进行,但是声明周期函数componentDidMount(useEffect也一样)在服务端渲染时是不执行的,只有在客户端渲染时才会触发。因此React为服务端渲染提供了专门的数据加载方法。

详情参考react routerServer RenderingData Loading部分

  1. 首先每个组件需要增加一个静态的loadData函数,用于服务端渲染时获取组件的异步数据
  2. 给路由增加loadData属性,官网推荐用一个数组 map 输出
  3. 服务端渲染时,绑定 store 之前,使用matchPath向 store 中填充数据,不过 matchPath 只能匹配一层路由,多级路由需要使用react-router-configmatchRoutes,
  4. 由于获取数据是异步,所以需要数据请求结束后借助Promise.all的回调返回html字符串(注意:actionloadData中要把promise对象透传出去,把promise对象return出去)

node 不能使用 fetch
fetch 在 node 中不能使用,所以有服务端渲染时最好使用axios发请求。
切记loadData要把结果return

同构存在的问题!!!

由于同构服务端渲染一遍,客户端又渲染了一遍,那就有一个问题了,当服务端把数据已经渲染了,异步数据也拿到了,由于客户端还会再执行一遍,那数据还会再执行并渲染一般,那前端由的延迟不是还是能感觉到吗,这不是脱裤放屁了,当禁用掉js,由于前端js无法执行。渲染异步请求的数据的部分会是空白;(当然 SEO 不影响,因为数据确实已经在页面了)

如何解决这个问题呢?
这需要用到数据的注水和脱水。

说白了就是当服务端已经拿到的state数据放到window的全局变量里面,当前端再创建store的时候,取出来全局中存储的state数据传给store当做默认值,这就没抖动了。
都是笨方法新概念,听着挺高大上

创建 store 时候,服务端和客户端分开使用两个方法就行了。

1
2
3
4
5
6
7
8
9
export function getStore() {
return createStore(reducer, applyMiddleware(thunk));
}

// 脱水
export function getClientStore() {
const defaultState = window.context.state;
return createStore(reducer, defaultState, applyMiddleware(thunk));
}

那客户端不发出请求不行了吗?
不行,因为服务端渲染只是渲染首屏 A,当手动输入页面路径首先访问 B 时,再回到 A 这是就无法获取 A 的数据了,所以客户端的请求不能省略;为了避免客户端重复发出请求,可以加个判断,有数据就不请求。

让客户端的请求都通过node中间件

到目前为止,action发出的请求是直接访问服务器的,既然搭建了 node 中间件,所有的请求应该都通过中间件发出去,客户端不应该直接和服务端进行交互。
实现也简单,因为现在有了 node 服务,如果发出的请求是相对路径,那么都会被 node 服务接收,我们只要制定好规则,把获取数据的请求都转到数据服务器就行了,说白了就是加个代理。
代理需要用到express-http-proxy,通过这个插件可以轻松的把请求代理到指定的服务器。

1
npm i express-http-proxy

我这里做的约定是,所有数据的请求都以/api开头。

1
2
3
4
5
6
7
8
9
10
// 设置代理,代理只能处理客户端发出的请求,服务端的请求不走这块,需要额外处理
app.use(
"/api",
proxy("https://www.easy-mock.com", {
https: true,
proxyReqPathResolver: function(req) {
return `/mock/5d7073309fb10a1868876f68/react_ssr/api${req.url}`;
},
}),
);

⚠️ 服务端渲染的时候有问题。

比如我当时报错信息是:Error: connect ECONNREFUSED 127.0.0.1:80

因为action中的请求链接现在写的相对路径(比如:/api/list.json),当浏览器发出请求的时候,浏览器会想网站挂载的服务器发出请求,也就是使用网站的域名和端口。
但是服务端在发出请求的时候,由于这本身就在服务端,所以对服务器来说,这是请求根目录下的api目录下list.json文件,由于根目录下没有这个文件,所以就报错了,请求接口同样的道理。
要解决这个问题也很简单,就是大家都能想到的最笨的方法。
做个判断处理,服务端发起的请求就把请求路径写成数据服务器的绝对路径,比如:https://www.baidu.com/data/api/list.json,如果是客户端发起的请求就写相对路径/api/list.json;

1
2
3
4
let url = `/api/list`;
if (isServer) {
url = "https://www.easy-mock.com/mock/5d7073309fb10a1868876f68/react_ssr/api/list";
}

这样做很丑陋,如果这样处理,首屏上用到的异步数据 action 都要这么处理,太 low 了。使用axiosinstance可以改善这个问题(其实差别不大,还是需要判断)。

原理就是创建两个axios的实例,一个客户端的,一个服务端的,把上面的 url 换成两个实例就行了。axios的实例可以指定发出请求的前缀内容,所以方法中的url可以写成相对路径,拼接工作嫁给了axios的实例来完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// client
import axios from "axios";
export default axios.create({
baseURL: "/",
});

// server
import axios from "axios";
export default axios.create({
baseURL: "https://www.easy-mock.com/mock/5d7073309fb10a1868876f68/react_ssr",
});

// 调用
let request = isServer ? ServerAxios : ClientAxios;
request.get("/api/list");

不使用判断更优雅的解决
由于客户端渲染和服务端渲染分别使用各自的stroe,因此可以在创建store的时候,绑定axios的实例,这样就不用传参判断是否是客户端还是服务端请求了。
store绑定axios实例,需要用到redux-thunkwithExtraArgument方法,这个方法允许给thunk传递一个参数,这个参数在异步调用的时候可以使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// store绑定axios实例
import ClientAxios from "../client/request";
import ServerAxios from "../server/request";

export function getStore() {
return createStore(reducer, applyMiddleware(thunk.withExtraArgument(ClientAxios)));
}

export function getClientStore() {
const defaultState = window.context.state;
return createStore(reducer, defaultState, applyMiddleware(thunk.withExtraArgument(ServerAxios)));
}

// action中调用
export function fetchHomeList() {
// 第三个参数就是绑定的axios实例
return (dispatch, getState, axiosInstance) => {
return axiosInstance.get("/api/list").then(response => {
const { data } = response;
dispatch(setHome(data.data));
});
};
}

有没有想过为什么客户端和服务端渲染不用同一个 store 呢?,这样还不用注水和脱水呢!
因为客户端和服务端运行环境不同,一个运行客户端也就是浏览器中,一个运行在 node 环境中,变量怎么可能共享呢,哈哈 😀

node代理时没有携带解决cookie问题

服务端代理请求时,携带上客户单请求上的cookie就行了;更新创建axios实例的地方

1
2
3
4
5
6
7
8
import axios from "axios";
export default req =>
axios.create({
baseURL: "https://www.easy-mock.com/mock/5d7073309fb10a1868876f68/react_ssr",
headers: {
cookie: req.get("cookie") || "",
},
});

共用部分实现,比如Header

项目中总会有很多页面的共用部分,比如header,footer等等,这些如何像使用模板那样在一个地方写,而不需要在每个页面中都写一遍呢?
使用router多级路由就可以轻松解决这个问题。

  1. 写一个共用组件,如App.js,组件中放入公共部分,比如HeaderFooterMenu等等。
  2. App.js中增加各个组件的路由,但是为了能显示多级路由的内容,需要用到react-router-configrenderRoutes
  3. App组件作为路由对象的根对象,子集路由就是要在内容区域显示的各个子组件了。
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
// 路由对象
export default [
{
path: "/",
component: App,
key: "app",
routes: [
{
path: "/",
component: Home,
loadData: Home.loadData,
exact: true,
key: "home",
},
{
path: "/login",
component: Login,
exact: true,
key: "login",
},
],
},
];
// 服务端渲染注册路由的地方
<StaticRouter location={req.path} context={{}}>
<div>{renderRoutes(routes)}</div>
</StaticRouter>;

// 显示路由的地方,App组件
import routes from "./Routes";
//....省略部分
return (
<div>
<Header />
{renderRoutes(routes[0].routes)}
</div>
);
// 客户端注册路由的地方
<BrowserRouter>
<div>{renderRoutes(routes)}</div>
</BrowserRouter>;

由于一级路由渲染完组件后,路由会把路由对象的信息传递给组件,因此 App 公共组件不需要专门引入定义的 routes,在 props 中可以直接获取到

1
2
3
4
5
6
7
8
const App = memo(props => {
return (
<div>
<Header />
{renderRoutes(props.route.routes)}
</div>
);
});

通过axios给请求添加公共参数

1
2
3
4
5
6
7
8
9
10
11
12
13
export default axios.create({
baseURL: "/",
params: { secret: "abc" },
});

export default req =>
axios.create({
baseURL: "https://www.fastmock.site/mock/0564485c18844ec86d683c16b922e8ab/ssr",
headers: {
cookie: req.get("cookie") || "cookie",
},
params: { secret: "abc" },
});

404 页面

当访问的路由不存在时,我们需要显示404页面,这实现其实很简单。
写一个 404 的页面,然后在配置路由的地方配置下就行,但是配置只需要一个component属性即可。

1
2
3
4
5
6
7
8
9
10
11
{
path: "/",
component: App,
loadData: App.loadData,
key: "app",
routes: [
{
component: NotFound, // 这行就是404页面
},
],
},

当访问路由中没有的路由时,会自动跳转到 404 页面。

借助routercontext来返回 404 状态码

上面的 404 页面配置完后,虽然其作用了,但是 http 状态码是 200,为了更真实的显示,我们应该让找不到路由时 http 的状态码是404

StaticRouter组件传递context属性,当返回 html 字符串时判断 context 对象是否被标记访问的是 404 页面,比如添加了 notFound 属性,如果是,则设置状态码为 404

1
2
3
4
5
6
7
8
9
Promise.all(promises).then(() => {
// 用于404页面时增加标记用
const context = {};
const html = render(req, store, routes, context);
if (context.noFound) {
res.status(404);
}
res.send(html);
});

服务端重定向

前面我们已经做了,当退出登录时,使用Redirect调回到首页的功能,但是Redirect仅限于前端重定向,不能做服务端重定向;
在非登录状态下,如果我们直接手动访问其他页面,其实服务端是已经访问了的,只是客户端帮我们跳转到了首页。

服务端该如何重定向呢?
这仍需要借助routercontext对象,当我们有重定向时,context会被填充一些属性,通过打印 context 对象,可以发现,填充的属性有:

  • action 字符串,描述操作类型,重定向就是REPLACE
  • location 一个对象,描述重定向前的数据
  • url 字符串,描述重定向后的url

所以在服务端渲染时,可以像处理 404 时那样,判断 context 中的 action,如果是REPLACE则跳转到url指定的路径

1
2
3
4
5
if (context.action === "REPLACE") {
res.redirect(301, context.url);
} else if (context.noFound) {
res.status(404);
}

解决请求失败的问题

在循环组件并加载数据的地方再嵌套一层 promise,无论组件中加载数据的部分是成功还是失败都把结果传递下去,这样只有失败的组件不显示,成功的组件仍显示。
并且当有多个请求时,如果第一个失败了,后面的数据还没返回也不影响,因为 catch 也让触发的 resolve

1
2
3
4
5
6
7
8
9
10
11
matchedRoutes.forEach(item => {
if (item.route.loadData) {
const promise = new Promise((resolve, reject) => {
item.route
.loadData(store)
.then(resolve)
.catch(resolve);
});
promises.push(promise);
}
});

服务端渲染css配置

服务端渲染css需要使用到isomorphic-style-loader

1
npm install isomorphic-style-loader --save-dev

然后把style-loader改成isomorphic-style-loader

配置完后,下面的处理就和 404 页面的处理类似,当是服务端渲染时,就把css字符串添加到routercontext对象中,然后在服务端渲染那块,从context中拿出css字符串,拼接到head中即可。
如果获取css字符串呢?
首先把 css 引入方式配置成模块引入的方式。isomorphic-style-loader提供了_getCss()的方法可以获取到 css 字符串。
这样禁用掉 js 后,样式仍能直接有效。

1
2
3
4
// 判断是否是服务端渲染
if (staticContext) {
staticContext.css = styles._getCss();
}

但是这样有很多问题:

  1. 因为需要在每个组件中把css添加到context对象中,所以 css 会被后面的覆盖
  2. 不在路由中的组件没有context属性

解答:

  1. css 属性用数组类型
  2. 手动把context对象传递过去。因为App组件是在路由中的,所以可以在App中把conterxt传递给header组件中

使用高阶组件解决需要在每个组件中添加css字符串的问题

在每个组件中都需要把 css 字符串添加到 context 对象中很麻烦,可以写一个高阶组件,来解决这个问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React from "react";

export default (DecoratedComponent, styles) => {
return class newComponent extends React() {
componentDidMount() {
const { staticContext } = this.props;
if (staticContext) {
staticContext.css.push(styles._getCss());
}
}
render() {
return <DecoratedComponent {...this.props} />;
}
};
};

SEO

title 和 Description 主要是为了提高网站的点击率,对SEO意义不大。

使用react-helmet定制 title 和 Description

react-helmet不仅可以帮助动态生成titledescription,并且可以在服务端渲染时引用,让服务端渲染也是动态生成titledescription
在每个组件中使用helmet,则当路由到这个组件时,titledescription 就自动发生了变化。

1
2
3
4
5
6
7
8
9
10
// 拿到客户端渲染定义的helmet
const helmet = Helmet.renderStatic();

// 在html字符串中使用
const html = `
<head>
${helmet.title.toString()}
${helmet.meta.toString()}
</head>
`;

预渲染

当对首屏时间要求不高,但是对 SEO 要求较高时,可以使用预渲染方案来解决这个问题。
预渲染就是当访问的是人时,就是正常的客户端渲染,但当访问的是爬虫蜘蛛时,则走预渲染服务器,预渲染服务器去取项目的页面,把页面的dom结构生成后再返回给蜘蛛。
预渲染服务器是如何获取页面的 dom 结构呢?
其实就是把页面上生成的 html 获取过来,就像我们使用开发者工具查看 html 结构一样,预渲染服务器就是拿的这个 html 字符串。

搭建预渲染服务器

预渲染需要预渲染服务,蜘蛛访问的是预渲染服务器,不是我们直接的网站页面。
预渲染服务器需要用到prerender,这个插件可以帮助我们轻松搭建预渲染服务器。

1
$ npm install prerender
1
2
3
const prerender = require("prerender");
const server = prerender({ port: 3200 });
server.start();

访问预渲染服务器的时候需要通过以下方式:

1
http://localhost:3200/render?url=https://www.example.com/

请求参数url的内容就是要抓取的页面。

PreRender 原理

当访问预渲染服务器时,预渲染服务器会创建一个小浏览器,然后在这个小浏览器中访问 url 指定的页面,然后再在这个小浏览器中拿到html的内容,拿到后关掉这个小浏览器,把内容返回给蜘蛛。
由于这个过程,耗时可能比较长,但是蜘蛛只关心内容,不太关心响应时间,所以问题不大。

预渲染参考网站 https://prerender.io/
这个网站详细讲解了预渲染原理和使用方式,并提供了便捷的的使用接口。

如何判断是否是蜘蛛在访问

可以通过 nigix 进行判断,然后根据结果路由到不同的服务器上,如果是机器蜘蛛则路由到预渲染服务器,如果是客户在访问则路由到网站服务器

文章作者: wenmu
文章链接: http://blog.wangpengpeng.site/2020/01/09/react-ssr%E7%AC%94%E8%AE%B0/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 温木的博客
微信打赏