阐述 Vue SSR 应用在 Docker/Kubernetes 等容器化环境下的部署策略,包括镜像构建和性能优化。

各位靓仔靓女,晚上好!我是今天的主讲人,很高兴能和大家聊聊 Vue SSR 应用在容器化环境下的部署那些事儿。今天咱们的目标,就是把 Vue SSR 应用装进 Docker,再搬到 Kubernetes 上溜达溜达,顺便让它跑得飞快!

第一章:SSR 与容器化的完美邂逅

首先,咱们得明白,为啥要把 Vue SSR 应用塞到容器里。

  • 环境一致性: 保证开发、测试、生产环境一模一样,再也不怕“在我电脑上跑得好好的”这种玄学问题。
  • 易于部署和扩展: Docker 镜像就像一个打包好的快递,随处可部署。Kubernetes 更是个超级调度员,能帮你自动扩容,应对突如其来的流量高峰。
  • 资源隔离: 每个容器都像一个独立的小房间,互不干扰,保证应用安全稳定。

所以,SSR 和容器化,简直就是天作之合!

第二章:Docker 镜像构建:从零开始,打造专属战舰

咱们一步一步来,先创建一个简单的 Vue SSR 应用。

vue create ssr-demo
cd ssr-demo
vue add @vue/cli-plugin-typescript
vue add @vue/cli-plugin-eslint
vue add @vue/cli-plugin-vuex
vue add @vue/cli-plugin-router
vue add @vue/cli-plugin-pwa
vue add @vue/cli-plugin-unit-jest
vue add @vue/cli-plugin-e2e-cypress
vue add @vue/cli-plugin-babel
vue add @vue/cli-plugin-pwa
vue add @vue/cli-plugin-eslint
vue add @vue/cli-plugin-router
vue add @vue/cli-plugin-vuex
vue add @vue/cli-plugin-typescript
vue add @vue/cli-plugin-unit-jest
vue add @vue/cli-plugin-e2e-cypress
vue add @vue/cli-service
npm install vue vue-server-renderer vue-router vuex axios -S
npm install webpack webpack-dev-middleware webpack-hot-middleware cross-env -D

修改package.json增加ssr启动命令

  "scripts": {
    "ssr:build": "npm run build && node build/setup-dev-server.js",
    "ssr:start": "cross-env NODE_ENV=production node server.js"
  },

添加一个简单的ssr服务端server.js

const express = require('express');
const fs = require('fs');
const { createBundleRenderer } = require('vue-server-renderer');

const app = express();

const template = fs.readFileSync('./dist/index.html', 'utf-8');
const bundle = require('./dist/vue-ssr-server-bundle.json');
const clientManifest = require('./dist/vue-ssr-client-manifest.json');

const renderer = createBundleRenderer(bundle, {
  template,
  clientManifest,
  runInNewContext: false
});

app.use(express.static('./dist'));

app.get('*', (req, res) => {
  const context = {
    url: req.url
  };

  renderer.renderToString(context, (err, html) => {
    if (err) {
      console.error(err);
      return res.status(500).send('Server Error');
    }
    res.send(html);
  });
});

const port = 3000;
app.listen(port, () => {
  console.log(`Server started at http://localhost:${port}`);
});

修改vue.config.js增加ssr配置

const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
const nodeExternals = require('webpack-node-externals')

const TARGET_NODE = process.env.WEBPACK_TARGET === 'node'
const target = TARGET_NODE ? 'server' : 'client'

module.exports = {
  css: {
    extract: false
  },
  configureWebpack: () => ({
    entry: `./src/entry-${target}.js`,
    devtool: 'source-map',
    target: TARGET_NODE ? 'node' : 'web',
    node: TARGET_NODE ? undefined : false,
    output: {
      libraryTarget: TARGET_NODE ? 'commonjs2' : undefined
    },
    externals: TARGET_NODE
      ? nodeExternals({
          allowlist: [/.css$/]
        })
      : undefined,
    optimization: {
      splitChunks: undefined
    },
    plugins: [TARGET_NODE ? new VueSSRServerPlugin() : new VueSSRClientPlugin()]
  }),
  chainWebpack: config => {
    config.module
      .rule('vue')
      .use('vue-loader')
      .tap(options => {
        return Object.assign(options, {
          optimizeSSR: false
        })
      })
  }
}

修改src/app.vue

<template>
  <div id="app">
    <h1>{{ msg }}</h1>
  </div>
</template>

<script>
export default {
  name: 'App',
  data() {
    return {
      msg: 'Hello, Vue SSR!'
    };
  }
};
</script>

创建src/entry-client.js

import { createApp } from './main'

const { app, router } = createApp()

router.onReady(() => {
  app.$mount('#app')
})

创建src/entry-server.js

import { createApp } from './main'

export default context => {
  return new Promise((resolve, reject) => {
    const { app, router } = createApp()

    router.push(context.url)

    router.onReady(() => {
      const matchedComponents = router.getMatchedComponents()
      if (!matchedComponents.length) {
        return reject({ code: 404 })
      }

      resolve(app)
    }, reject)
  })
}

创建build/setup-dev-server.js

const fs = require('fs')
const path = require('path')
const webpack = require('webpack')
const chokidar = require('chokidar')
const webpackDevMiddleware = require('webpack-dev-middleware')
const webpackHotMiddleware = require('webpack-hot-middleware')

const serverConfig = require('../webpack.config.js')
const clientConfig = require('../webpack.config.js')

module.exports = function setupDevServer (app, templatePath, cb) {
  let bundle
  let template
  let clientManifest

  const update = () => {
    if (bundle && clientManifest) {
      cb(bundle, {
        template,
        clientManifest
      })
    }
  }

  // 监听构建客户端的 webpack 配置
  const clientCompiler = webpack(clientConfig)
  const devMiddleware = webpackDevMiddleware(clientCompiler, {
    publicPath: clientConfig.output.publicPath,
    writeToDisk: true
  })
  app.use(devMiddleware)
  clientCompiler.hooks.done.tap('webpack-client-done', () => {
    clientManifest = JSON.parse(
      fs.readFileSync(path.join(clientConfig.output.path, 'vue-ssr-client-manifest.json'), 'utf-8')
    )
    update()
  })

  // 热更新
  app.use(webpackHotMiddleware(clientCompiler))

  // 监听构建服务端的 webpack 配置
  const serverCompiler = webpack(serverConfig)
  serverCompiler.outputFileSystem = fs
  serverCompiler.watch({}, (err, stats) => {
    if (err) throw err
    if (stats.hasErrors()) return

    bundle = JSON.parse(fs.readFileSync(path.join(serverConfig.output.path, 'vue-ssr-server-bundle.json'), 'utf-8'))
    update()
  })

  return update
}

好了,基础项目准备完毕。接下来,就是编写 Dockerfile 了!

# 使用 Node.js 官方镜像作为基础镜像
FROM node:16-alpine

# 设置工作目录
WORKDIR /app

# 复制 package.json 和 package-lock.json
COPY package*.json ./

# 安装依赖
RUN npm install

# 复制项目文件
COPY . .

# 构建 SSR 应用
RUN npm run ssr:build

# 暴露端口
EXPOSE 3000

# 启动命令
CMD [ "npm", "run", "ssr:start" ]

咱们来解读一下:

  • FROM node:16-alpine:选择一个轻量级的 Node.js 镜像,减少镜像体积。
  • WORKDIR /app:设置工作目录,后续操作都在 /app 目录下进行。
  • COPY package*.json ./:复制 package.jsonpackage-lock.json,利用 Docker 缓存,加速依赖安装。
  • RUN npm install:安装项目依赖。
  • COPY . .:复制所有项目文件到镜像中。
  • RUN npm run ssr:build:构建 SSR 应用。
  • EXPOSE 3000:暴露 3000 端口,允许外部访问。
  • CMD [ "npm", "run", "ssr:start" ]:设置容器启动命令。

有了 Dockerfile,咱们就可以构建镜像了!

docker build -t vue-ssr-app:latest .

这条命令会创建一个名为 vue-ssr-app,标签为 latest 的镜像。

接下来,运行一下看看:

docker run -p 3000:3000 vue-ssr-app:latest

打开浏览器,访问 http://localhost:3000,如果看到熟悉的 Vue SSR 页面,恭喜你,成功了!

第三章:Kubernetes 部署:让应用在云端起舞

有了 Docker 镜像,咱们就可以把它部署到 Kubernetes 上了。

首先,创建一个 Deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: vue-ssr-deployment
spec:
  replicas: 2  # 副本数量
  selector:
    matchLabels:
      app: vue-ssr-app
  template:
    metadata:
      labels:
        app: vue-ssr-app
    spec:
      containers:
      - name: vue-ssr-container
        image: vue-ssr-app:latest
        ports:
        - containerPort: 3000

这个 Deployment 会创建两个 Pod,每个 Pod 都运行一个 vue-ssr-app 容器。

然后,创建一个 Service:

apiVersion: v1
kind: Service
metadata:
  name: vue-ssr-service
spec:
  selector:
    app: vue-ssr-app
  ports:
  - protocol: TCP
    port: 80
    targetPort: 3000
  type: LoadBalancer

这个 Service 会将外部流量转发到 vue-ssr-app Pod 的 3000 端口。type: LoadBalancer 表示使用负载均衡器,方便外部访问。

将这两个 YAML 文件保存为 deployment.yamlservice.yaml,然后执行以下命令:

kubectl apply -f deployment.yaml
kubectl apply -f service.yaml

Kubernetes 就会按照你的指示,创建 Deployment 和 Service。稍等片刻,就可以通过 Service 的外部 IP 访问你的 Vue SSR 应用了!

第四章:性能优化:让你的 SSR 应用飞起来

光能跑起来还不够,咱们还得让它跑得更快!

  1. 缓存: SSR 应用的一大痛点就是性能。每次请求都重新渲染,CPU 压力山大。所以,缓存是必须的!

    • 页面缓存: 将渲染好的 HTML 页面缓存起来,下次直接返回,避免重复渲染。可以使用 Redis、Memcached 等缓存方案。
    • 组件缓存: Vue 提供了 keep-alive 组件,可以缓存组件状态,减少组件重新渲染的开销。
    • CDN 缓存: 将静态资源(JS、CSS、图片)放到 CDN 上,加速访问速度。
  2. 代码优化: 代码写得好,性能自然高。

    • 减少 Bundle 体积: 使用 Webpack 的 Code Splitting 功能,将代码分割成多个小块,按需加载。
    • 优化组件渲染: 避免在组件中进行复杂的计算,尽量使用 Pure Components。
    • 懒加载: 对于非首屏需要的组件,可以使用懒加载,减少首屏渲染时间。
  3. 服务器优化: 服务器性能也很重要。

    • 使用高性能服务器: 选择 CPU、内存都比较好的服务器。
    • 使用 Node.js 集群: 利用 Node.js 的 Cluster 模块,启动多个 Node.js 进程,充分利用多核 CPU。
    • Gzip 压缩: 对返回的 HTML、JS、CSS 进行 Gzip 压缩,减少网络传输时间。
  4. Kubernetes 优化: 在 Kubernetes 环境下,也有一些优化技巧。

    • 资源限制: 为每个容器设置 CPU 和内存限制,避免资源争抢。
    • Horizontal Pod Autoscaling (HPA): 根据 CPU 使用率自动调整 Pod 数量,应对流量高峰。
    • 节点亲和性: 将 SSR 应用部署到性能较好的节点上。

咱们来举个例子,如何在 Node.js 中使用 Redis 缓存页面:

首先,安装 Redis:

npm install redis --save

然后,修改 server.js

const express = require('express');
const fs = require('fs');
const { createBundleRenderer } = require('vue-server-renderer');
const redis = require('redis');

const app = express();

const template = fs.readFileSync('./dist/index.html', 'utf-8');
const bundle = require('./dist/vue-ssr-server-bundle.json');
const clientManifest = require('./dist/vue-ssr-client-manifest.json');

const renderer = createBundleRenderer(bundle, {
  template,
  clientManifest,
  runInNewContext: false
});

const redisClient = redis.createClient({
  host: 'your-redis-host', // 替换成你的 Redis 地址
  port: 6379
});

redisClient.on('error', err => {
  console.error('Redis error:', err);
});

app.use(express.static('./dist'));

app.get('*', (req, res) => {
  const cacheKey = req.url;

  redisClient.get(cacheKey, (err, cachedHtml) => {
    if (err) {
      console.error('Redis get error:', err);
    }

    if (cachedHtml) {
      console.log('Serving from cache:', cacheKey);
      return res.send(cachedHtml);
    }

    const context = {
      url: req.url
    };

    renderer.renderToString(context, (err, html) => {
      if (err) {
        console.error(err);
        return res.status(500).send('Server Error');
      }

      redisClient.setex(cacheKey, 60, html); // 缓存 60 秒
      res.send(html);
    });
  });
});

const port = 3000;
app.listen(port, () => {
  console.log(`Server started at http://localhost:${port}`);
});

这段代码会在每次请求时,先从 Redis 中查找缓存。如果找到缓存,直接返回;否则,渲染页面,并将渲染结果缓存到 Redis 中。

第五章:监控与日志:随时掌握应用状态

部署之后,别忘了监控和日志!

  • 监控: 监控 CPU 使用率、内存使用率、请求响应时间等指标,及时发现问题。可以使用 Prometheus、Grafana 等工具。
  • 日志: 收集应用日志,方便排查错误。可以使用 ELK Stack (Elasticsearch, Logstash, Kibana) 等工具。

总结:

咱们今天聊了 Vue SSR 应用在 Docker/Kubernetes 环境下的部署策略,包括镜像构建、Kubernetes 部署和性能优化。希望这些内容能帮助大家更好地部署和管理 Vue SSR 应用。记住,容器化只是手段,最终目的是为了提升应用的可用性、可扩展性和可维护性。

一些额外的 Tips:

优化方向 优化策略
镜像构建 1. 使用多阶段构建,减少最终镜像体积。 2. 利用 Docker 缓存,加速构建过程。 3. 选择合适的 Node.js 基础镜像。
Kubernetes 1. 合理设置资源限制。 2. 使用 HPA 自动扩缩容。 3. 使用节点亲和性,将应用部署到合适的节点上。 4. 使用 Readiness Probe 和 Liveness Probe,保证应用健康状态。
性能优化 1. 使用缓存(页面缓存、组件缓存、CDN 缓存)。 2. 优化代码(减少 Bundle 体积、优化组件渲染、懒加载)。 3. 优化服务器(使用高性能服务器、使用 Node.js 集群、Gzip 压缩)。

好了,今天的分享就到这里,谢谢大家!如果有什么问题,欢迎随时提问。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注