- 作者:老汪软件技巧
- 发表时间:2024-09-24 04:02
- 浏览量:
定义
Server-Side Rendering我们称其为SSR,意为服务端渲染
指由服务侧完成页面的HTML结构拼接的页面处理技术,发送到浏览器,然后为其绑定状态与事件,成为完全可交互页面的过程
先来看看Web3个阶段的发展史:
优缺点
SSR主要解决了以下两种问题:
但是使用SSR同样存在以下的缺点:
Vue-SSR渲染流程例子1. 创建项目
创建三个文件touch server.js src/entry-client.ts src/entry-server.js
修改package.json运行脚本
"scripts": {
"dev": "node server", // 运行开发环境
}
然后需要把应用创建都改为函数的方式进行调用创建,因为在SSR环境下,和纯客户端不一样,服务器只会初始化一次,所以为了防止状态污染,每次请求必须是全新的实例
// src/main.ts
import { createSSRApp } from 'vue'
import App from './App.vue'
import { createRouter } from './router'
import { createPinia } from 'pinia'
export function createApp() {
const app = createSSRApp(App)
const router = createRouter()
const pinia = createPinia()
app.use(router)
app.use(pinia)
return { app, router, pinia }
}
router同理
// src/router/index
import { createRouter as _createRrouter, createMemoryHistory, createWebHistory, RouteRecordRaw } from 'vue-router'
const routes: RouteRecordRaw[] = [
...
]
export function createRouter() {
return _createRrouter({
history: import.meta.env.SSR ? createMemoryHistory() : createWebHistory(),
routes,
})
}
然后修改index.html,增加注释占位和客户端入口文件,在之后的服务端渲染时注入
"en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue + TStitle>
head>
<body>
<div id="app">div>
<script type="module" src="/src/main.ts">script>
<script type="module" src="/src/entry-client.ts" >script>
<script>
// 服务端获取的数据统一挂载到window上
window.__INITIAL_STATE__ = ''
script>
body>
2. 服务端启动文件3. 服务端入口文件4. 客户端入口文件5. 组件和页面6. 生产环境6.1 pacnakge.json6.2 服务端运行文件
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
import express from 'express'
import { createRequire } from 'module';
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const require = createRequire(import.meta.url);
const resolve = (p) => path.resolve(__dirname, p);
const createServer = async (isProd = process.env.NODE_ENV === 'production') => {
const app = express()
- const vite = await require('vite').createServer({
- server: {
- middlewareMode: true,
- },
- appType: 'custom'
- });
- app.use(vite.middlewares);
+ let vite;
+ if (isProd) {
+ app.use(require('compression')());
+ app.use(
+ require('serve-static')(resolve('./dist/client'), {
+ index: false
+ })
+ );
+ } else {
+ vite = await require('vite').createServer({
+ server: {
+ middlewareMode: true,
+ },
+ appType: 'custom'
+ });
+ app.use(vite.middlewares);
+ }
// 通过bulid --ssrManifest命令生成的静态资源映射需要在生产环境下引用
+ const manifest = isProd ? fs.readFileSync(resolve('./dist/client/ssr-manifest.json'), 'utf-8') :{}
app.use('*', async (req, res, next) => {
const url = req.originalUrl
try {
- let template = fs.readFileSync(
- resolve('index.html'),
- 'utf-8'
- )
- template = await vite.transformIndexHtml(url, template)
- const render = (await vite.ssrLoadModule('/src/entry-server.js')).render
- const [ appHtml, piniaState ] = await render(url)
+ let template, render
+ if (isProd) {
+ template = fs.readFileSync(resolve('./dist/client/index.html'), 'utf-8')
+ render = (await import('./dist/server/entry-server.js')).render
+ } else {
+ template = fs.readFileSync(
+ resolve('index.html'),
+ 'utf-8'
+ )
+ template = await vite.transformIndexHtml(url, template)
+ render = (await vite.ssrLoadModule('/src/entry-server.js')).render
+ }
+ const [ appHtml, preloadLinks, piniaState ] = await render(url, manifest)
const html = template
+ .replace(``, preloadLinks)
.replace(``, appHtml)
.replace(``, piniaState)
res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
} catch (error) {
vite?.ssrFixStacktrace(error)
next()
}
})
app.listen(5100)
}
createServer();
6.3 服务端入口文件React-SSR
node server 接收客户端请求,得到当前的req url path,然后在已有的路由表内查找到对应的组件,拿到需要请求的数据,将数据作为 props 、context或者store 形式传入组件,然后基于 react 内置的服务端渲染api renderToString() or renderToNodeStream() 把组件渲染为 html字符串或者 stream 流, 在把最终的 html 进行输出前需要将数据注入到浏览器端(注水),server 输出(response)后浏览器端可以得到数据(脱水),浏览器开始进行渲染和节点对比,然后执行组件的componentDidMount 完成组件内事件绑定和一些交互,浏览器重用了服务端输出的 html 节点,整个流程结束
例子
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( ,document.getElementById("root"));
以前看到的都是调用 render 方法,这里使用 hydrate 方法,它的作用是什么?
ReactDOM.hydrate
与 render() 相同,但它用于在 ReactDOMServer 渲染的容器中对 HTML 的内容进行 hydrate 操作。 React 会尝试在已有标记上绑定事件监听器。
我们都知道纯粹的 React 代码放在浏览器上是无法执行的,因此需要打包工具进行处理,这里我们使用 webpack ,下面我们来看看 webpack 客户端的配置:
mon.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(` React SSR ${content} `)
})
app.listen(3000);
代码解析:
你会发现一个奇怪的现象,为什么写 Node.js 代码使用的却是 ESModule 语法,是的没错,因为我们要在服务端解析 React 代码,作为同构项目,因此统一语法也是非常必要的。所以 Node.js 也需要配置相应的 webpack 编译文件:
webpack.server.js
const path = require("path");
const nodeExternals = require("webpack-node-externals");
const {merge} = require("webpack-merge");
const commonConfig = require("./webpack.common");
const serverConfig = {
target:"node", //为了不把nodejs内置模块打包进输出文件中,例如: fs net模块等;
mode: "development",
entry:"./src/server/index.js",
output:{
filename:"bundle.js",
path:path.resolve(__dirname,"../build")
},
externals:[nodeExternals()], //为了不把node_modules目录下的第三方模块打包进输出文件中,因为nodejs默认会去node_modules目录下去寻找和使用第三方模块。
};
module.exports = merge(serverConfig,commonConfig);
到此我们就完成了一个简单的同构项目,这里您应该会有几个疑问?
renderToString 有什么作用?为什么服务端加载了一次,客户端还需要再次加载呢?服务端加载了 React 输出的代码片段,客户端又执行了一次,这样是不是会加载两次导致资源浪费呢?ReactDOMServer.renderToString(element)
将 React 元素渲染为初始 HTML 。 React 将返回一个 HTML 字符串。你可以使用此方法在服务端生成 HTML ,并在首次请求时将标记下发,以加快页面加载速度,并允许搜索引擎爬取你的页面以达到 SEO 优化的目的。
为什么服务端加载了一次,客户端还需要再次加载呢?
原因很简单,服务端使用 renderToString 渲染页面,而 react-dom/server 下的 renderToString 并没有做事件相关的处理,因此返回给浏览器的内容不会有事件绑定,渲染出来的页面只是一个静态的 HTML 页面。只有在客户端渲染 React 组件并初始化 React 实例后,才能更新组件的 state 和 props ,初始化 React 的事件系统,让 React 组件真正“ 动” 起来