• 作者:老汪软件技巧
  • 发表时间: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 组件真正“ 动” 起来