【2】邂逅Vue3+SSR

Vue除了支持开发SPA应用之外,其实也是支持开发SSR应用的。 在Vue中创建SSR应用,需要调用createSSRApp函数,而不是createApp

  • createApp:创建应用,直接挂载到页面上
  • createSSRApp:创建应用,是在激活的模式下挂载应用

服务端用@vue/server-renderer包中的renderToString来进行渲染。

搭建SSR开发环境

需安装的依赖项:

  • npm i express
  • npm i --D nodemon 启动Node程序时并监听文件的变化,变化即刷新
  • npm i -D webpack webpack-cli webpack-merge webpack-node-externals 排除掉node_modules中所以的模块
  • npm i vue
  • npmi -D vue-loader 加载.vue文件
  • npmi -D babel-loader @babel/preset-env 加载JS文件,转换新语法

编写服务端代码

App.vue

js 复制代码
 <template>
  <div class="app" style="border: 1px solid red;">
    <h2>Vue App</h2>
    <div> {{ count }}</div>
    <button @click="addCount">+1</button>
  </div>
</template>

<script setup>
import { ref } from 'vue';

const count = ref(100);
const addCount = () => {
  count.value++;
}

</script> 

app.js

js 复制代码
import { createSSRApp } from "vue";

import App from "./App.vue";

//避免夸请求状态的污染,通过函数返回app实例,保证每个请求都会返回一个app实例
export default function createApp() {
  let app = createSSRApp(App);
  return app;
}

思考一个问题,为什么需要写一个函数来返回app实例

在SPA中,整个生命周期中只有一个App对象实例或一个Router对象实例或一个Store对象实例都是可以的,因为每个用户在使用浏览器访问SPA应用时,应用模块都会重新初始化,这也是一种单例模式。

然而,在SSR 环境下,App应用模块通常只在服务器启动时初始化一次。同一个应用模块会在多个服务器请求之间被复用,而我们的单例状态对象也一样,也会在多个请求之间被复用,比如:

  • 当某个用户对共享的单例状态进行修改,那么这个状态可能会意外地泄露给另一个在请求的用户。
  • 我们把这种情况称为:跨请求状态污染。

为了避免这种跨请求状态污染,SSR的解决方案是:

  • 可以在每个请求中为整个应用创建一个全新的实例,包括后面的router 和全局store等实例。
  • 所以我们在创建App或路由或Store对象时都是使用一个函数来创建,保证每个请求都会创建一个全新的实例。
  • 这样也会有缺点:需要消耗更多的服务器的资源。

server/index.js

js 复制代码
let express = require("express");
import createApp from "../app";
import { renderToString } from "@vue/server-renderer";

let server = express();

server.get("/", async (req, res) => {
  let app = createApp();
  let appStringHtml = await renderToString(app);
  res.send(
    `
      <!DOCTYPE html>
      <html lang="en">

      <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
      </head>

      <body>
        <h1>Vue Server Side Rendering</h1>
        <div id="app">${appStringHtml}</div>
      </body>

      </html>
    `
  );
});

server.listen(3000, () => {
  console.log("Server is running on 3000");
});

wepack配置以及脚本文件

wepack.config.js

js 复制代码
let path = require("path");
let nodeExternals = require("webpack-node-externals");
let { VueLoaderPlugin } = require("vue-loader/dist/index");
module.exports = {
  target: "node",
  mode: "development",
  entry: "./src/server/index.js",
  output: {
    filename: "server_bundle.js",
    path: path.resolve(__dirname, "../build/server"),
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        loader: "babel-loader",
        options: {
          presets: ["@babel/preset-env"],
        },
      },
      {
        test: /\.vue$/,
        loader: "vue-loader",
      },
    ],
  },
  plugins: [new VueLoaderPlugin()],
  resolve: {
    //添加了这些拓展名,项目中的导入的拓展名文件就不用写文件的后缀
    extensions: [".js", ".jsx", ".json", ".vue", ".wasm"],
  },
  externals: [nodeExternals()], //排除node_modules 中的包
};
js 复制代码
 "dev": "nodemon ./src/server/index.js",
  "build:server": "webpack --config ./config/server.config.js --watch",
  "start": "nodemon ./build/server/server_bundle.js"

当我们启动服务,发现已经返回了一个页面在浏览器上面,但是无法触发时间,这是因为该页面是静态的需要激活页面,也就是 Hydration

激活Hydration

首先在scr目录里面重新新建一个文件夹目录为scr/client/index.js 这里放置vue的初始化代码

js 复制代码
import { createApp } from "vue";
import App from "../App.vue";

//SPA
let app = createApp(App);
app.mount("#app");

然后在配置文件中配置客户端的打包配置文件

js 复制代码
let path = require("path");
let { VueLoaderPlugin } = require("vue-loader/dist/index");
module.exports = {
  target: "web",
  mode: "development",
  entry: "./src/client/index.js",
  output: {
    filename: "client_bundle.js",
    path: path.resolve(__dirname, "../build/client"),
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        loader: "babel-loader",
        options: {
          presets: ["@babel/preset-env"],
        },
      },
      {
        test: /\.vue$/,
        loader: "vue-loader",
      },
    ],
  },
  plugins: [new VueLoaderPlugin()],
  resolve: {
    //添加了这些拓展名,项目中的导入的拓展名文件就不用写文件的后缀
    extensions: [".js", ".jsx", ".json", ".vue", ".wasm"],
  },
};

将App.vue打包为一个客户端的client_bundle.js文件,用来激活应用,使页面有交互效果

将App.vue打包为一个服务器端的server_bundle.js文件,用来在服务器端动态生成页面的HTML

在server/index.js中添加静态文件部署

js 复制代码
let server = express();

server.get("/", async (req, res) => {
  let app = createApp();
  let appStringHtml = await renderToString(app);

  //部署静态资源
  server.use(express.static("build"));
  res.send(
    `
      <!DOCTYPE html>
      <html lang="en">

      <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
      </head>

      <body>
        <h1>Vue Server Side Rendering</h1>
        <div id="app">${appStringHtml}</div>
        <script src="/client/client_bundle.js"></script>
      </body>
      </html>
    `
  );
});

server_bundle.js渲染的页面+client_bundle.js文件进行Hydration

此时的文件目录

相关推荐
子兮曰5 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰5 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
前端小万5 天前
写公众号赚了 3000 块后,我做了一款叫 "一键成稿" 的软件
前端·微信小程序
爱勇宝5 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
三十而立洋5 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
卡布鲁5 天前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js
汉堡大王95275 天前
Jev:不是聊天机器人, 而是一个智能 if 语句
前端·人工智能·后端
梦想很大很大5 天前
从运行事实到回归证据:Workrun 的 Telemetry 与 Evaluation 实践
前端·人工智能·后端
计算机魔术师5 天前
Meta Muse agent 接入 Shopify 的 Shop Pay 实现代理式购物
前端
沙蒿同学5 天前
我用 Go 搭了一条 AI Agent 流水线:从 1 张商品图到一整套淘宝详情页
前端·javascript·后端