【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

此时的文件目录

相关推荐
广州华水科技6 分钟前
单北斗变形监测在水库安全中的应用与维护该如何实施?
前端
GIS好难学17 分钟前
0帧起手《Vue零基础教程》,从前端框架到GIS开发系列课程
前端·vue.js·前端框架
行走的陀螺仪20 分钟前
重绘和重排怎么触发?怎么优化?
前端·css·性能优化·css3·浏览器原理
尘世中一位迷途小书童23 分钟前
项目大扫除神器:Knip —— 将你的代码库“瘦身”到底
前端·架构·代码规范
StarkCoder24 分钟前
🚫求求你别再手动改类名了!Swift 自动混淆脚本上线,4.3 头发保卫战正式开始!
前端
LYFlied28 分钟前
Vue Vapor模式与AI时代前端发展的思考:虚拟DOM与框架的未来
前端·vue.js·人工智能·前端框架
江公望30 分钟前
VUE3 动态Prop 10分钟讲清楚
前端·javascript·vue.js
不会写DN31 分钟前
JavaScript call、apply、bind 方法解析
开发语言·前端·javascript·node.js
AAA简单玩转程序设计43 分钟前
Java Map遍历的“优雅”合集
java·前端
timeweaver44 分钟前
React Server Components 再曝高危漏洞:拒绝服务与源码泄露接踵而至
前端·安全