前言
所谓同构,简而言之就是,第一次访问后台服务时,后台直接把前端要显示的界面全部返回,而不是像 SPA
项目只渲染一个 <div id="root"></div>
剩下的都是靠 JavaScript
脚本去加载。这样一来可以大大减少首屏等待时间。
同构概念并不复杂,它也非项目必需品,但是探索它的原理却是必须的。
阅读本文需要你具备以下技术基础: Node.js
、 React
、 React Router
、 Redux
、 webpack
。
本文将分以下两部分去讲述:
- 同构思路分析,让你对同构有一个概念上的了解;
- 手写同构框架,深入理解同构原理。
同构思路
CSR 客户端渲染
CSR
客户端渲染,这个就是很好理解了,使用 React
, React Router
前端自己控制路由的 SPA
项目,就可以理解成客户端渲染。它有一个非常大的优势就是,只是首次访问会请求后台服务加载相应文件,之后的访问都是前端自己判断 URL
展示相关组件,因此除了首次访问速度慢些之外,之后的访问速度都很快。
执行命令: create-react-app react-csr
创建一个 React SPA
单页面应用项目 。
执行命令: npm run start
启动项目。
查看网页源代码: 只有一个 <div id="root"></div>
和 一些 script
脚本。最终呈现出来的界面却是这样的: 原理很简单,相信学习过 webpack
的同学都知道,那就是 webpack
把所有代码都打包成相应脚本并插入到 HTML
界面中,浏览器会解析 script
脚本,通过动态插入 DOM
的方式展示出相应界面。
客户端渲染的优劣势
客户端渲染流程如下:
优势:
- 前端负责渲染页面,后端负责实现接口,各自干好各自的事情,对开发效率有极大的提升;
- 前端在跳转界面的时候不需要请求后台,加速了界面跳转的速度,提高用户体验。
劣势:
- 由于需要等待
JS
文件加载以及后台接口数据请求因此首屏加载时间长,用户体验较差; - 由于大部分内容都是通过
JS
加载因此搜索引擎无法爬取分析网页内容导致网站无法SEO
。
SSR 服务端渲染
SSR
是服务端渲染技术,它本身是一项比较普通的技术, Node.js
使用 ejs
模板引擎输出一个界面这就是服务端渲染。每次访问一个路由都是请求后台服务,重新加载文件渲染界面。
同样我们也来创建一个简单的 Node.js
服务:
mkdir express-ssr
cd express-ssr
npm init -y
touch app.js
npm i express --save
app.js
const express = require('express')
const app = express()
app.get('/',function (req,res) {
res.send(
`<html> <head> <title>express ssr</title> </head> <body> <h1>Hello SSR</h1> </body> </html>`
)
})
app.listen(3000);
启动服务: node app.js
这就是最简单的服务端渲染一个界面了。**服务端渲染的本质就是页面显示的内容是服务器端生产出来的。**参考 前端进阶面试题详细解答
服务端渲染的优劣势
服务端渲染流程:
优势:
- 整个
HTML
都通过服务端直接输出SEO
友好; - 加载首页不需要加载整个应用的
JS
文件,首页加载速度快。
劣势:
- 访问一个应用程序的每个界面都需要访问服务器,体验对比
CSR
稍差。
我们会发现一件很有意思的事,服务端渲染的优点就是客户端渲染的缺点,服务端渲染的缺点就是客户端渲染的优点,反之亦然。那为何不将传统的纯服务端直出的首屏优势和客户端渲染站内跳转优势结合,以取得最优解?这就引出了当前流行的服务端渲染( Server Side Rendering
),或者称之为“同构渲染”更为准确。
同构渲染
所谓同构,通俗的讲,就是一套 React
代码在服务器上运行一遍,到达浏览器又运行一遍。
服务端渲染完成页面结构,客户端渲染绑定事件。它是在 SPA
的基础上,利用服务端渲染直出首屏,解决了单页面应用首屏渲染慢的问题。
同构渲染流程
简单同构案例
要实现同构,简单来说就是以下两步:
- 服务端要能运行
React
代码; - 浏览器同样运行
React
代码。
1、创建项目
mkdir react-ssr
cd react-ssr
npm init -y
2、项目目录结构分析
├── src
│ ├── client
│ │ ├── index.js // 客户端业务入口文件
│ ├── server
│ │ └── index.js // 服务端业务入口文件
│ ├── container // React 组件
│ │ └── Home
│ │ └── Home.js
│ │
├── config // 配置文件夹
│ ├── webpack.client.js // 客户端配置文件
│ ├── webpack.server.js // 服务端配置文件
│ ├── webpack.common.js // 共有配置文件
├── .babelrc // babel 配置文件
├── package.json
首先我们编写一个简单的 React
组件, container/Home/Home.js
import React from "react";
const Home = ()=>{
return (
<div>
hello world <br/>
<button onClick={
()=> alert("hello world")}>按钮</button>
</div>
)
}
export default Home;
安装客户端渲染的惯例,我们写一个客户端渲染的入口文件, client/index.js
import React from "react";
import ReactDom from "react-dom";
import Home from "../containers/Home";
ReactDom.hydrate(<Home/>,document.getElementById("root"));
// ReactDom.render(<Home/>,document.getElementById("root"));
以前看到的都是调用 render
方法,这里使用 hydrate
方法,它的作用是什么?
ReactDOM.hydrate
与
render()
相同,但它用于在ReactDOMServer
渲染的容器中对HTML
的内容进行hydrate
操作。React
会尝试在已有标记上绑定事件监听器。
我们都知道纯粹的 React
代码放在浏览器上是无法执行的,因此需要打包工具进行处理,这里我们使用 webpack
,下面我们来看看 webpack
客户端的配置:
webpack.common.js
module.exports = {
module:{
rules:[
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
}
]
}
}
.babelrc
{
"presets":[
["@babel/preset-env"],
["@babel/preset-react"]
]
}
webpack.client.js
const path = require("path");
const {
merge} = require("webpack-merge");
const commonConfig = require("./webpack.common");
const clientConfig = {
mode: "development",
entry:"./src/client/index.js",
output:{
filename:"index.js",
path:path.resolve(__dirname,"../public")
},
}
module.exports = merge(commonConfig,clientConfig);
代码解析:通过 entry
配置的入口文件,对 React
代码进行打包,最后输出到 public
目录下的 index.js
。
在以往,直接在 HTML
引入这个打包后的 JS
文件,界面就显示出来了,我们称之为纯客户端渲染。这里我们就不这样使用,因为我们还需要服务端渲染。
接下来,看看服务端渲染文件 server/index.js
import express from "express";
import {
renderToString } from "react-dom/server";
import React from "react";
import Home from "../containers/Home";
const app = express(); // {1}
app.use(express.static('public')) // {2}
const content = renderToString(<Home />); //{3}
app.get('/',function (req,res) {
// {4}
res.send(` <!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-wi