【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

此时的文件目录

相关推荐
学习使我快乐012 小时前
JS进阶 3——深入面向对象、原型
开发语言·前端·javascript
bobostudio19953 小时前
TypeScript 设计模式之【策略模式】
前端·javascript·设计模式·typescript·策略模式
黄尚圈圈3 小时前
Vue 中引入 ECharts 的详细步骤与示例
前端·vue.js·echarts
浮华似水4 小时前
简洁之道 - React Hook Form
前端
正小安7 小时前
如何在微信小程序中实现分包加载和预下载
前端·微信小程序·小程序
_.Switch8 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
一路向前的月光8 小时前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   8 小时前
vite学习教程06、vite.config.js配置
前端·vite配置·端口设置·本地开发
长路 ㅤ   8 小时前
vue-live2d看板娘集成方案设计使用教程
前端·javascript·vue.js·live2d
Fan_web8 小时前
jQuery——事件委托
开发语言·前端·javascript·css·jquery