1 引言
1.1 什么是单页面应用?
单页应用(single-page application,缩写 SPA)是一种网络应用程序或网站的模型,它通过动态重写当前页面来与用户交互,而非传统的从服务器重新加载整个新页面。这种方法避免了页面之间切换打断用户体验,使应用程序更像一个桌面应用程序。在单页应用中,所有必要的代码(HTML、JavaScript和CSS)都通过单个页面的加载而检索,或者根据需要(通常是为响应用户操作)动态装载适当的资源并添加到页面。
1.2 什么是 SEO?
搜索引擎优化,又称为 SEO,即 Search Engine Optimization,它是一种通过分析搜索引擎的排名规律,了解各种搜索引擎怎样进行搜索、怎样抓取互联网页面、怎样确定特定关键词的搜索结果排名的技术。搜索引擎采用易于被搜索引用的手段,对网站进行有针对性的优化,提高网站在搜索引擎中的自然排名,吸引更多的用户访问网站,提高网站的访问量,提高网站的销售能力和宣传能力,从而提升网站的品牌效应。
1.3 什么是客户端渲染?
数据由浏览器通过请求数据动态获得,再通过 JS 将数据填充到 DOM 元素上,最终显示在页面中。
1.4 什么是服务端渲染?
服务端渲染的模式下,当用户第一次请求页面时,由服务器把需要的组件或页面渲染成 HTML 字符串,然后把它返回给客户端。客户端拿到之后可以直接渲染然后呈现给用户的 HTML 内容,不需要为了生成 DOM 内容自己再去跑一遍 JS 代码。
1.5 流行的框架有什么问题?
比如 Vue.js、React.js 一般都用来编写单页面应用,由于页面显示过程要进行 JS 文件拉取和框架的代码执行,首屏加载时间会比较慢,也不利于 SEO。
1.6 为什么会有服务端渲染?
- 更好的 SEO
Google 的搜索引擎不仅可以解析 HTML 标签,还可以解析 JS,国内的搜索引擎只能够解析 HTML标签,包括百度不能解析 JS。
- 更快的内容到达时间
特别是对于缓慢的网络情况或运行缓慢的设备。无需等待所有的 JavaScript 都完成下载并执行,才显示服务器渲染的标记,所以用户将会更快速地看到完整渲染的页面,可以产生更好的用户体验,有利于优化首屏的渲染。
1.7 服务端渲染的缺点?
代码复杂度增加。为了实现服务端渲染,应用代码中需要兼容服务端和客户端两种运行情况,而一部分依赖的外部扩展库却只能在客户端运行,需要对其进行特殊处理,才能在服务器渲染应用程序中运行。
需要更多的服务器负载均衡。由于服务器增加了渲染HTML的需求,使得原本只需要输出静态资源文件的 nodejs 服务,新增了数据获取的 IO 和渲染 HTML 的 CPU 占用,如果流量突然暴增,有可能导致服务器宕机,因此需要使用响应的缓存策略和准备相应的服务器负载。
涉及构建设置和部署的更多要求。与可以部署在任何静态文件服务器上的完全静态单页面应用程序 (SPA) 不同,服务器渲染应用程序,需要处于 Node.js server 运行环境。
1.8 什么样的项目适合服务端渲染?
企业或项目在乎在搜索引擎中的排名,或者是一些项目希望能够被搜索到,比如个人博客。
2 React 服务端渲染的实现
将使用 React + Webpack + Express 进行搭建项目。
react-dom/server 包里面有两个方法 renderToString,renderToStaticMarkup,它们的主要作用是将 React Component 转化为 HTML 字符串。
- renderToString:将 React 元素渲染到其初始 HTML 中。 该函数应该只在服务器上使用。 React 将返回一个 HTML 字符串。 您可以使用此方法在服务器上生成 HTML ,并在初始请求时发送标记,以加快网页加载速度,并允许搜索引擎抓取你的网页以实现 SEO 目的。
- renderToStaticMarkup:类似于 renderToString ,除了这不会创建 React 在内部使用的额外DOM属性,如 data-reactroot。 如果你想使用React 作为一个简单的静态页面生成器,这很有用,因为剥离额外的属性可以节省一些字节。
ReactDOMServer – React 中文文档 v16.6.3
2.1 项目搭建
// 初始化项目
npm init -y
// 安装 webpack react reac-dom
npm i webpack webpack-cli react react-dom
// 安装 babel 相关的插件
npm i @babel/core @babel/preset-env @babel/preset-rea@babel/polyfill
// 安装其他插件
npm i html-webpack-plugin mini-css-extract-plugin
// 安装 express
npm i express
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, './dist'),
filename:'[name].js',
libraryTarget: 'umd'
},
module: {
rules: [
{
test: /.(png|jpg|gif|jpeg)$/,
use: [
{
loader: 'file-loader',
options: {
name: '[name]_[hash:8].[ext]',
}
}
]
},
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader']
},
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
//inlineSource: '.css$',
template: path.join(__dirname, './src/index.html'),
filename: 'index.html',
chunks: ['main'],
inject: true
}),
new MiniCssExtractPlugin({
filename: '[name].css',
})
]
}
.babelrc
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"edge": "17",
"firefox": "60",
"chrome": "67",
"safari": "11.1",
},
"useBuiltIns": "usage",
"corejs": 2
}
],
"@babel/preset-react",
]
}
src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
function App () {
return (
<div>
<h1>hello ssr</h1>
</div>
)
}
ReactDOM.render(<App />, document.getElementById('app'));
src/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
</body>
</html>
根目录下新建 server.js 用于服务端的实现
//引入express
const express = require('express')
const ssrServer = (port) => {
//创建应用对象
const app = express()
// 设置静态资源目录,可将静态资源放在服务器上
app.use(express.static(__dirname + '/dist'))
//创建路由规则
app.get('/',(request,response)=>{
//设置响应
response.send("hello express")
})
app.listen(port, () => {
console.log(`服务已经启动,${port} 端口监听中,请访问 http://localhost:${port}`)
})
}
ssrServer(3000)
2.2 服务端渲染的基本实现
在服务器端渲染 React 组件时,因为服务端没有DOM
,无法使用React.render()
方法,我们可以在服务端使用 renderToString
返回 HTML 字符串。
//引入express
const express = require('express')
const { renderToString } = require('react-dom/server')
const Index = require('./dist/main')
const ssrServer = (port) => {
//创建应用对象
const app = express()
// 设置静态资源目录,可将静态资源放在服务器上
app.use(express.static(__dirname + '/dist'))
//创建路由规则
app.get('/',(request,response)=>{
//设置响应
response.send("hello express")
})
app.get('/ssr',(request,response)=>{
//返回一个完整的可以直接解析的html
const html = renderMarkUp(renderToString(Index))
response.status(200).send(html)
})
app.listen(port, () => {
console.log(`服务已经启动,${port} 端口监听中,请访问 http://localhost:${port}`)
})
}
ssrServer(3000)
// 创建html模板
const renderMarkUp = (str) => {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">${str}</div>
</body>
</html>`
}
然后我们执行下server.js, 会发现报如下错误
因为在 server.js 中利用 renderToString
方法,然后浏览器中有一个全局内置对象,比如 window,在 node 环境是不能够被识别的,node 能够识别的全局对象有 global,所以我们可以在 server.js 中进行处理。
if (typeof self === 'undefined') {
global.self = {}
}
重新执行 server.js 文件,成功!!!
能够看到页面源代码中含有显示的内容
这样一个简单的服务端渲染就成功啦。一些 CSS、Image、SVG 等资源,可以通过相应的 Modules 来转为 Server 端可以识别的内容,这里就不做讨论。
如果我们给 src/index.js 添加一个按钮,给按钮绑定事件,希望点击按钮可以弹出事件。
const React = require('react')
const ReactDOM = require('react-dom')
function App () {
const modify = () => {
alert('点击啦')
}
return (
<div>
<button onClick={modify}>点击</button>
<h1>hello ssr</h1>
</div>
)
}
// ReactDOM.render(<App />, document.getElementById('app'));
module.exports = <App />
重新运行项目,点击按钮,我们所期待的弹窗并未出现,并且源码中并未给按钮绑定事件。
因为 renderToString
只会解析基本的 HTML DOM
元素,并不会解析元素上附加的事件,只负责视图层,不负责行为,不会解析JS 代码,所以会忽略掉 onClick
这个事件。
onClick
是个事件,在我们通常所写的代码中(即非 SSR
), React
是通过对元素进行 addEventListener
来进行事件的注册,也就是通过 js
来触发事件,并调用相应的方法,而服务器端显然是无法完成这个操作的,除此之外,一些与浏览器相关的操作也都是无法在服务器端完成的。
那么一些行为交互服务端渲染不能够实现,需要在客户端做交互,如何解决?这就涉及同构。
2.3 React 同构
客户端与服务端使用同样的组件,同一份代码。服务端负责首次渲染,后续的行为与交互给客户端执行。
使用 create-react-app 创建项目 isomorphism
将 express 和 isomorphism 项目的配置文件结合
将 isomorphism 项目编译打包后的文件通过 express 公开出来
app.use(‘/‘, express.static(‘public’));
安装相关插件
yarn add express @babel/preset-env @babel/preset-react
src/App.js
function App() {
const modify = () => {
alert('点击啦')
}
return (
<div>
<button onClick={modify}>点击</button>
<h1>hello ssr</h1>
</div>
)
}
export default App;
src/server.js
import express from 'express';
import App from './App';
import React from 'react';
import { renderToStaticMarkup, renderToString } from 'react-dom/server';
const app = express();
app.get('/',(request,response)=>{
const html = renderToString(<App />);
response.send(html);
})
app.use('/', express.static('public'));
app.listen(3000, () => {
console.log("服务已经启动,3000 端口监听中,请访问 http://localhost:3000")
})
现在还是不能够做到绑定事件,因为服务端渲染需要把一些静态资源发送给客户端,我们先 build 下项目。现在 server.js 中返回客户端的不能够是 App renderToString 后的结果了,我们需要把代码注入到public/index.html 中。
src/server.js
import express from 'express';
import App from './App';
import React from 'react';
import { renderToStaticMarkup, renderToString } from 'react-dom/server';
import fs from 'fs';
const app = express();
app.get('/',(request,response)=>{
// 读取build/index.html文件的内容
const html = fs.readFileSync('../build/index.html')
const content = renderToString(<App />);
// 替换index.html中的数据
response.send(html.toString().replace('<div id="root"></div>', `<div id="root">${content}</div>`));
})
app.use('/', express.static('build'));
app.listen(3000, () => {
console.log("服务已经启动,3000 端口监听中,请访问 http://localhost:3000")
})
就可以成功点击按钮啦!!!
同构的基本流程如下图所示:
一般情况下在 react
代码中会使用 react-router
进行路由的管理,当项目比较大的时候,通常我们会使用 redux
来对项目进行数据状态的管理,对 React 同构还需要进行路由同构,数据状态的同构,这里就不叙述了。
3 Next.js的介绍
4 Jupiter VS Next.js
Jupiter 与 Next.js的 SSR 比较
Jupiter | Next | |
---|---|---|
开发无感知 | √ | × |
状态管理 | √ | √ |
降级处理 | √ | × |
react 同构 | √ | √ |
前端路由 | √ | √ |
后端路由 | √ | √ |
SPR缓存 | √ | × |
SSG | × | √ |
结合内部运维 | √ | × |
参考链接
- 本文作者: étoile
- 版权声明: 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。转载请注明出处!