likes
comments
collection
share

基于docker部署微前端项目的入门实践指南🐋

作者站长头像
站长
· 阅读数 17

🚢 前言

这是一篇讲述如何基于docker部署微前端项目的文章。本文章会先从理论角度去描述部署方式,然后再通过代码去实现部署的逻辑和细节。

本文适合以下人群阅读:

  1. 刚认识docker,想通过实践进一步学习docker的前端 er
  2. 想了解如何用docker部署微前端项目的前端 er

本文中所部署的微前端项目是micro-fe,来源于之前写的一篇讲述如何接入微前端的文章给 vue-element-admin 接入 qiankun 的微前端开发实践总结 🍀

🚀 阅前须知:关于微前端项目的仓库模式

关于微前端项目有两种仓库管理模式:

  • 多仓库模式主应用子应用分别放在不同的仓库里。
  • 单仓库模式主应用子应用分别放在同一个的仓库里,但分开放在不同的目录里。

本文推荐使用多仓库模式管理模式。因为多仓库模式单仓库模式有以下优点:

  1. git独立

    • 分支管理相对简洁:不会出现多个分支分别对应主应用子应用的开发特性。
    • 提交记录不密集:因为只有一个应用的提交而不是所有应用的提交。
    • 创建Releaselabel不混乱:如果存在创建Releaselabel的操作,则需要加上前缀去分辨不同应用,不然会出现多个同名的版本。
  2. CICD流水线独立:只有对应当前应用的流水线,不会出现多条对应不同应用的流水线。且如果使用单仓库模式,则流水线的触发条件需要更加复杂,不能出现一个应用更新触发流水线导致所有应用进入CICD环节。

在下面的部署教程中,会以多仓库模式为前提来进行分析讲述。

🚠 当基于 docker 部署单体前端项目时,我们是怎么做的

所谓学会走才能学会跑。在学习部署微前端项目之前,我们先了解一下怎么基于docker部署单体前端项目。通常我们把打包好的前端制品文件放在服务器里的nginx上,且对nginx的配置文件里做路径配置,用于区分获取前后端数据的请求,如下代码所示:

server{
  # 处理获取前端的请求
  location / {
    # 前端制品文件存放在 /usr/share/nginx/html 里
    # 当nginx接收到路径名为/index.css的客户端请求时,把/usr/share/nginx/html/index.css文件返回给客户端
    root   /usr/share/nginx/html;
    index  index.html index.htm;
  }

  # 如果路径以/api/开头,则判断该请求是获取后端数据的请求,因此把该请求通过反响代理引往后端
  # 当以普通路径前缀匹配时,最长路径会优先匹配,因此即使路径为/api/xx的请求也符合上面的路径格式,也会以此处规则进行处理
  location /api/ {
    # proxy_pass中定义后端的uri
    # 当nginx接收到路径名为/api/login的客户端请求时,nginx会进行反向代理的过程:
    #   1. 向host为http://backend的后端发出/api/login的请求,
    #   2. 在接收到响应后,把响应返回给客户端
    proxy_pass http://backend/;
  }
}

在配置下,nginx处理请求的流向效果如下所示:

基于docker部署微前端项目的入门实践指南🐋

那么,部署单体前端项目这一过程其实就是在服务器启动一个配置好路径分发存放前端制品nginx容器(不一定非要nginx,看团队的选择)。接下来通过代码来实现这一过程:

🚟 1. 提供 nginx 配置文件

在前端代码项目中,提供用于配置nginx的配置文件nginx.conf,如下所示:

# nginx/nginx.conf
server {
  listen       80;
  listen  [::]:80;
  # 开启日志记录
  access_log  /var/log/nginx/host.access.log  main;

  location / {
    root   /usr/share/nginx/html;
    index  index.html index.htm;
  }

  location /api/ {
    proxy_pass: http://backend/
  }

  error_page   500 502 503 504  /50x.html;
  location = /50x.html {
    root   /usr/share/nginx/html;
  }
}

🚟 2. 提供用于生成镜像的Dockerfile文件

Dockerfile文件用于对前端项目生成镜像,服务器通过镜像生成容器去运行

# 新建构建阶段build-stage,指定node为基础镜像,该阶段用于生成前端制品文件
FROM node:16-alpine as build-stage
# 指定工作目录/app用于存放前端制品,以便在COPY,RUN以及下一个构建阶段中使用。
WORKDIR /app
# 复制package.json、package-lock.json、yarn.lock到工作目录里,COPY最后一个参数dest如果是相对路径,则会以工作目录有作为基准,复制到WORKDIR/<dest>里
COPY package*.json yarn.lock ./
# 下载依赖
RUN yarn install
COPY . .
# 生成制品
RUN yarn build

# 新建构建阶段deploy-stage,指定nginx为基础镜像,该阶段用于配置和运行nginx
FROM nginx:stable-alpine as deploy-stage
# 把build-stage阶段中的前端制品和nginx.conf配置文件复制到nginx的指定路径里
# 这样子就可以设置nginx的配置文件,而配置文件中的默认匹配路径是/usr/share/nginx/html,也就是存放前端制品的路径
COPY --from=build-stage /app/build/ /usr/share/nginx/html
COPY --from=build-stage /app/nginx/nginx.conf /etc/nginx/conf.d/default.conf
# 指定容器在运行过程时监听80端口
EXPOSE 80
# 指定在容器开启运行时,通过运行以下命令行指令开启nignx
CMD ["nginx", "-g", "daemon off;"]

对于上述的Dockerfile需要提及以下注意点

  1. build-stage的逻辑中,是先复制package.jsonyarn.lock且安装依赖后,然后再把所有项目文件复制到工作目录。为什么不直接把所有项目文件复制到工作目录然后安装依赖呢?

    这里需要提及到Dockerfile的镜像以及缓存机制,篇幅较长,因此放在下面的 🚃 进阶- 🚌 1. 如何写好Dockerfile 章节中解释。

  2. 关于DockerfileEXPOSE指令,用于指定容器在运行过程中监听特定端口号。该指令不会开放指定端口到宿主机->容器的映射表,如果要执行此操作请使用docker run -p去手动指定。

    对于EXPOSE的描述里更多的可看此处

🚟 3. 在宿主机构建镜像且运行容器

完成上面的操作后,在宿主机依次执行以下步骤即可部署单体前端项目:

  1. 拉取前端项目代码:通过git clone复制前端项目代码
  2. 生成镜像:进入前端项目的根目录,运行命令行docker build -t imageName .去生成镜像。命令行中的最后一个参数用于指定Dockerfile的位置。
  3. 生成容器且运行:运行命令行docker run -d --name containerName imageName,以对指定的镜像生成容器且在后台运行。

至此,一个单体前端项目的部署过程已经介绍完毕了。当然在实践中还要考虑其他细节,但主要操作流程已经在本节中讲述清楚了。

🚆 那么,如何部署微前端项目呢

一个微前端项目中包括一个主应用和数个子应用。我们可以用上节中分析的部署单体前端方式把主应用子应用都生成对应的nginx容器放在服务器里,在主应用nginx容器里配置路径规则转发获取子应用资源的请求。如下图所示:

基于docker部署微前端项目的入门实践指南🐋

客户端发出路径前缀为/vue-app/的请求,则是请求VueApp子应用的资源;发出前缀为/react-app/的请求,则是请求ReactTSApp子应用的资源。这些请求都会由主应用nginx容器做反向代理处理。

实现图中的多容器部署需要补充一些额外的细节。接下来我们分开主应用子应用来列出这些额外细节:

🚊 主应用细节

1. 修改子应用的加载链接

这里的主应用registerMicroAppsstart子应用进行加载和注册,在registerMicroApps的相关代码中用以下写法:

const isProd = process.env.NODE_ENV === "production";

registerMicroApps([
  {
    name: "react app",
    // 如果是生产环境则用特定前缀路径,当客户端发出该请求时,服务器中的主应用nginx容器收到请求会做反向代理分发到后端
    entry: isProd ? `//${location.host}/react-app/` : "//localhost:3001",
    container: "#app-react",
    loader,
    activeRule: "/app-react/index",
    props: {
      basepath: "/app-react/index",
    },
  },
  {
    name: "vue app",
    entry: isProd ? `//${location.host}/vue-app/` : "//localhost:3002",
    container: "#app-vue",
    loader,
    activeRule: "/app-vue/index",
    props: {
      basepath: "/app-vue/index",
    },
  },
  // ...省略其余子应用
]);

2. 结合DockerNetwork修改nginx.conf配置文件

在展示nginx.conf代码之前,我们需要了解Docker Network这一概念:当我们成功启动一个容器时,该容器会接入到Docker Network实例里,如果docker run指令中有用--network参数指定网络,则接入到指定网络里,否则默认接入到bridge network里。我们可以通过docker network ls来查看当前存在哪些网络:

docker network ls
输出:
NETWORK ID     NAME      DRIVER    SCOPE
95a79d75ba56   bridge    bridge    local
ebb2b72a64a9   host      host      local
b3fb09dbb1d0   none      null      local

上面三个network是默认存在的网络,关于网络类型bridgenonehost的区别可查看#network-drivers

我们可以通过docker network inspect networkName指令查看指定network中有哪些接入的容器:

docker network inspect bridge
输出:
[
    {
        "Name": "bridge",
        // 省略其他字段
        // 在Containers里可以查看接入的容器
        "Containers": {
            "1e2d0761c5ad2154c7cfc2c9c912c094f3dda59623b1a992c1a66d9a14c4dedf": {
                "Name": "master-app",
                "EndpointID": "b206d19490336b0527496cf43c90b61befbd036b6c2d9909926124c0979458ea",
                "MacAddress": "02:42:ac:11:00:04",
                "IPv4Address": "172.17.0.4/16",
                "IPv6Address": ""
            },
            "2c94c287b6ff3ec6561864cbad86f4fbd57ed39ec89a4bbf432f245887332c50": {
                "Name": "vue-app",
                "EndpointID": "3ee5696cd3cc0424c9246d1c4e5c1c5de76a4f638b4317a6df5fd7e0c0cb13ab",
                "MacAddress": "02:42:ac:11:00:03",
                "IPv4Address": "172.17.0.3/16",
                "IPv6Address": ""
            },
            "e2299a334abdec2a01b0caca15cacf980dc3f9c55ea54519758789c8daa3742e": {
                "Name": "react-ts-app",
                "EndpointID": "ec876f84a999fb7f77cd8f7185e077bb597f0a50daa850b55729b4b9662ce50a",
                "MacAddress": "02:42:ac:11:00:02",
                "IPv4Address": "172.17.0.2/16",
                "IPv6Address": ""
            }
        },
    }
]

从上面的输出信息可知:

  • master-app,即主应用nginx容器的 ip 为 172.17.0.4
  • vue-app,即VueAppnginx容器的 ip 为 172.17.0.3
  • react-ts-app,即ReactTSAppnginx容器的 ip 为 172.17.0.2

在同一网络中,容器可以通过 ip 互相访问。例如,当VueAppnginx容器以 80 端口EXPOSE时,主应用nginx容器可通过 172.17.0.3 访问VueAppnginx容器

但在实践中,我们偏向于把需要通信的容器接入到自定义网络(通过docker network create networkName指令来创建)。因为自定义网络对比于默认bridge网络有一个优点:

  • User-defined bridges provide automatic DNS resolution between containers.

    Containers on the default bridge network can only access each other by IP addresses, unless you use the --link option, which is considered legacy. On a user-defined bridge network, containers can resolve each other by name or alias.

    引自Differences between user-defined bridges and the default bridge

引文中的意思是:自定义网络会对其接入的容器提供DNS自动解析: 在默认bridge网络中,容器可以通过 ip 访问其他容器。而在自定义网络,容器可以通过容器名或者昵称(docker run --network-alias指定)来访问别的容器。例如:主应用nginx容器可以通过http://vue-app去访问VueAppnginx容器

采用容器名作为域名访问容器可以说是非常方便。而且因迭代更新导致镜像变化,从而生成新的子应用nginx容器时,即使容器名字不变,但重新接入到网络时,容器 ip 可能会变化。此时如果再次通过固定 ip 访问子应用nginx容器则会访问失败。但采取容器名访问就不用担心这种情况。

因此,所有微前端项目中的主应用nginx容器子应用nginx容器,都接入到自定义网络里,就可以通过容器名作为域名访问彼此。在这前提下,我们的nginx.conf的写法如下所示:

server {
    listen       80;
    listen  [::]:80;

    access_log  /var/log/nginx/host.access.log  main;
    error_log  /var/log/nginx/host.error.log  error;

    location / {
        try_files $uri $uri/ /index.html;
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }
    # 对于路径前缀为/vue-app/的请求,反向代理到名为vue-app的容器
    location /vue-app/ {
        proxy_pass http://vue-app/;
    }
    # 对于路径前缀为/react-app/的请求,反向代理到名为react-ts-app的容器
    location /react-app/ {
        proxy_pass http://react-ts-app/;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}

至此,主应用的细节已经讲完。

🚊 子应用细节

子应用方面需要在打包过程中给引入资源的路径加上公共前缀。

对此,在使用vue-cli脚手架创建的VueApp应用中,vue.config.js配置如下所示:

const isProd = process.env.NODE_ENV === "production";

module.exports = defineConfig({
  transpileDependencies: true,
  // 加上公共路径前缀
  publicPath: isProd ? "/vue-app/" : undefined,
  //...省略其他不变的配置
});

对此,在使用cra脚手架创建的在ReactTSApp应用中,.rescriptsrc.js配置如下所示:

const isProd = process.env.NODE_ENV === "production";

module.exports = (
  isProd ? [] : [["use-stylelint-config", ".stylelintrc.js"]]
).concat({
  webpack: (config) => {
    // 加上公共路径前缀
    config.output.publicPath = isProd ? "/react-app/" : undefined;
    //...省略其他不变的配置
  },
});

至此,子应用的细节已讲完。

🚅 动手实践:基于Githb Action来实现CD以部署微前端项目

🚝 为什么要选择Githb Action来实现CD

不妨思考一个问题:CD流程要放在生产机器上执行吗?

大多数公司里,会用一台机器或者集群来负责CICD操作,在走完CICD流程后把制品上传到生产机器上部署。因为CICD操作是会占用机器的资源和算力的,如果CICD流程放在生产机器上运行,毫无疑问会降低生产机器处理外部请求的速度。因此要专门设立负责执行CICD的机器或集群。

但对于我们个人开发而言,手上往往只有一台自费购买的机器,而且机器配置还不怎么高,都是 1 核 1G 的(都是薅首购优惠的,薅完腾讯云就薅阿里云)。那如果我们要在自己的低配置机器上做微前端CD操作,同时跑主应用和好几个子应用CD流程,那机器机器会处于高负载的情况。我在自己的生产机器上跑自己的微前端项目(主应用+四个子应用)的CD流程时,CPU使用率达到 60%多,连在本地用SSH访问生产机器都失败。

但我们也没有另一台的机器用于跑CICD。在这种情况下就可以选择Github Action。对于公共仓库和私有仓库,他都可以分配机器来运行我们的CICD流程,我们只需要编写yml文件来指定CICD流程即可。我之前写过一篇关于Github Action的入门级别文章作为前端,要学会用 Github Action 给自己的项目加上 CICD,有兴趣的可以看看。这里就不再介绍Github Action的入门知识了,直接进入设计环节。

🚝 CD的设计与实现

在设计CD流程时,我们需要考虑两点:

  1. CD的执行时机
  2. CD执行什么操作

下面通过本次CD的流程图来展示上述两点:

基于docker部署微前端项目的入门实践指南🐋

首先准备docker.json文件,用于定义镜像名、镜像版本和容器名:

{
  "name": "react-ts-app",
  "version": "0.1.9"
}

为什么不取package.json中的nameversion字段来定义呢?因为我们每次迭代更新而生成新版本的镜像时,都要更新版本,如果版本取自package.json里的version,则会导致Dockerfile中的RUN yarn install指令对应的缓存失效。导致每次生成镜像时,即使应用依赖没变化,但因为package.json变化了导致要重新安装依赖。关于Dockerfile的缓存细节会放到下面的 🚃 进阶 - 🚌 1. 如何写好Dockerfile 章节中分析。

接下来编写yml文件对应上面的CD流程图:

name: CD
on:
  # 执行时机设计为master分支被推送时触发
  push:
    branches: master
jobs:
  # ReadInfo Job用于读取且导出package.json里的信息,用于下一个Job作为环境变量使用
  ReadInfo:
    runs-on: ubuntu-latest
    outputs:
      info: ${{ steps.read-info.outputs.info }}
    steps:
      # 拉取代码
      - name: Checkout repository
        uses: actions/checkout@v3
      # docker.json,把其内容即json字符串注入到存放输出结果的outputs.info
      - name: Read Info
        id: read-info
        run: |
          JSON=`cat ./$APP_PATH/docker.json`
          JSON="${JSON//'%'/''}"
          JSON="${JSON//$'\n'/''}"
          JSON="${JSON//$'\r'/''}"
          echo "info=$JSON" >> $GITHUB_OUTPUT
  CD:
    runs-on: ubuntu-latest
    needs: ReadInfo
    # 根据上一步ReadInfo Job中的输出结果info来注入环境变量
    env:
      # 容器名取自docker.json里的name
      Container: ${{fromJson(needs.ReadInfo.outputs.info).name}}
      # 镜像名和镜像标签取自docker.json里的name和version
      Image: ${{fromJson(needs.ReadInfo.outputs.info).name}}:${{fromJson(needs.ReadInfo.outputs.info).version}}
      # 镜像文件为docker.json里的name + 后缀.tar
      ImageFile: ${{fromJson(needs.ReadInfo.outputs.info).name}}.tar
      # 发布版本取自docker.json里的version
      Release: ${{fromJson(needs.ReadInfo.outputs.info).version}}
    steps:
      # 拉取代码
      - name: Checkout repository
        uses: actions/checkout@v3
      # 安装docker
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2
      # 创建docker镜像
      - name: Build Docker Image
        uses: docker/build-push-action@v3
        with:
          # 缓存类型使用gha,该类型能很好的和github action结合实现缓存镜像,更多细节可看 https://docs.docker.com/build/building/cache/backends/gha/
          cache-from: type=gha
          cache-to: type=gha,mode=max
          tags: ${{env.Image}}
          context: .
          load: true
      # 导出docker镜像文件
      - name: Export Image as Tar
        run: |
          docker images
          docker save ${{env.Image}} > ${{env.ImageFile}}
      # 创建Release版本且上传镜像文件
      - name: Create GitHub Release
        id: create_release
        uses: softprops/action-gh-release@v1
        with:
          token: ${{ secrets.PROJECT_ACCESS_TOKEN }}
          tag_name: ${{ env.Release }}
          name: ${{ env.Release }}
          draft: false
          prerelease: false
          # 附上镜像文件
          files: |
            ${{ env.ImageFile }}
      # 把镜像文件上传至生产服务器
      - name: Upload Image Tar to Deploy Server
        # easingthemes/ssh-deploy的原理时使用rsync+ssh上传文件
        uses: easingthemes/ssh-deploy@main
        env:
          SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_TOKEN }}
          ARGS: "-avzr --delete"
          SOURCE: ${{env.ImageFile}}
          REMOTE_HOST: ${{ secrets.REMOTE_HOST }}
          REMOTE_USER: ${{secrets.REMOTE_USER}}
          TARGET: ${{secrets.TARGET}}
      # 登陆进生产服务器进行操作
      - name: Run Docker Container
        uses: appleboy/ssh-action@master
        env:
          Network: microfe
        with:
          host: ${{secrets.REMOTE_HOST}}
          username: ${{secrets.REMOTE_USER}}
          key: ${{ secrets.DEPLOY_TOKEN }}
          # 步骤:
          # 1. 停止移除容器
          # 2. 移除已有镜像
          # 3. 进入存放镜像文件的目录且把镜像文件转为镜像
          # 4. 检测若不存在microfe网络,则创建microfe网络
          # 5. 根据镜像创建且运行容器,且把容器接入到microfe网络
          # 6. 移除镜像文件
          script: |
            ${{ format('docker ps -q --filter "name={0}" | grep -q . && docker rm -f {0}', env.Container) }}
            ${{ format('docker rmi -f $(docker images -q  --filter reference="{0}")', env.Container) }}
            ${{ format('cd {0}' , secrets.TARGET)}}
            ${{ format('docker load < {0}', env.ImageFile)}}
            ${{ format('docker network ls -q --filter "name={0}" | grep -q . || docker network create {0}',env.Network)}}
            ${{ format('docker run -d --name {0} --network {1} {2}',env.Container,env.Network,env.Image)}}
            ${{ format('rm {0}',env.ImageFile)}}

最后运行结果如下所示:

基于docker部署微前端项目的入门实践指南🐋

大家也可以访问micro-fe 的 CD 执行记录来查看其中的执行细节。

🚃 进阶

🚌 1. 如何写好Dockerfile

本节中的内容是Docker官网中总结出来的。如果在阅读以下内容后还想深入学习可点击阅读Optimizing builds with cache management

关于Dockerfile的分层机制

拿下面的Dockerfile代码进行分析:

FROM node:16-alpine
WORKDIR /app
COPY . .
RUN yarn install
RUN yarn build

Dockerfile中,FROM指令除了指定基础镜像外,还用于声明一个新的构建阶段构建阶段在执行结束后都会输出一个新的镜像作为产物(下称输出镜像)。输出镜像中有多个分层,每个分层都对应一条指令。拿上述Dockerfile代码来画图分析如下所示:

基于docker部署微前端项目的入门实践指南🐋

每一个分层都是基于前一个分层进行构建的。我们可以把整个输出镜像当作是一个存放分层的栈。每一个分层都可以会放到缓存里,如果下一次再次执行同样的Dockerfile代码指令时,如果缓存中有对应该指令的分层,可直接取出使用,从而跳过构建分层的过程,减少构建输出镜像的时间。但如果指令发生变化,例如,COPY所操作的文件发生变化,会导致缓存失效,需要重构对应的分层。如下所示:

基于docker部署微前端项目的入门实践指南🐋

而当一个指令发成变化导致分层重构后,该指令下面的所有指令都不能使用缓存分层只能全部重构。因为缓存中的分层是基于前一分层构建。如下所示:

基于docker部署微前端项目的入门实践指南🐋

如果我们在构建镜像时使用上述的Dockerfile,在每次迭代时,由于新增修改开发代码,会让COPY . .缓存失效从而导致对应的分层重构,继而执行yarn installyarn build两个过程。但如果我们采用下面的写法:

FROM node:16-alpine as build-stage
WORKDIR /app
COPY package*.json yarn.lock ./
RUN yarn install
COPY . .
RUN yarn build

这样子就可以在依赖不变的情况下,跳过yarn install环节,从而加快构建镜像的速度。

我们可以对比在上一章Github Action CD流水线下,执行RUN yarn install指令时,不使用缓存和使用缓存的对比:

不使用缓存:耗时 79 秒

基于docker部署微前端项目的入门实践指南🐋

使用缓存:无须耗时,直接跳过

基于docker部署微前端项目的入门实践指南🐋

至于更多关于如何充分利用缓存的写法可阅读How can I use the cache efficiently?

关于上述Dockerfile代码的进一步改进

对于以下Dockerfile代码,还存在改进的空间:如果我只改用于配置nginx规则的nginx.conf配置文件,其他的不做修改。那么是可以通过缓存机制来跳过yarn installyarn build环节的。

FROM node:16-alpine as build-stage
WORKDIR /app
COPY package*.json yarn.lock ./
RUN yarn install
COPY . .
RUN yarn build

FROM nginx:stable-alpine as deploy-stage
COPY --from=build-stage /app/dist/ /usr/share/nginx/html
COPY --from=build-stage /app/nginx/nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

对于上述代码,我们可以改进成以下代码:

FROM node:16-alpine as build-stage
WORKDIR /app
COPY package*.json yarn.lock ./
RUN yarn install
# COPY . .
# 把上面的COPY换成下面几行COPY指令。以明确指定复制除nginx以外的参与到yarn build的文件和目录
# 注意COPY指令在复制目录时会自动解包,只复制目录里面的子文件和子目录,因此目标对象必须是同名的目录
COPY src ./src
COPY public ./public
COPY .env .rescriptsrc.js tsconfig.json ./
RUN yarn build

FROM nginx:stable-alpine as deploy-stage
COPY --from=build-stage /app/build/ /usr/share/nginx/html
# COPY --from=build-stage /app/nginx/nginx.conf /etc/nginx/conf.d/default.conf
# 把上面的COPY换成下面的COPY指令。因为经过上面的调整,build-stage的工作目录里不存在nginx文件,需要直接从当前目录里复制过去
COPY nginx/nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

上面的写法也存在着缺点,就是需要写多行COPY指令以明确指定每一个参与生成制品的文件目录,缺一个都会导致制品失败。

为了查看上面写法的效果,我触发了下面三种情况的CD,并得出以下实验数据:

  1. package.json或者yarn.lock发生变化: 构建镜像时间为2m53s,其中RUN yarn install耗费85.5sRUN yarn build耗费25.6s,更多流水线细节可看此处
  2. package.jsonyarn.lock没变化,开发代码发生变化: 构建镜像时间为1m20s,其中RUN yarn install命中缓存无耗时,RUN yarn build耗费40.2s,更多流水线细节可看此处
  3. package.jsonyarn.lock没变化和开发代码没发生变化,nginx.conf发生变化: 构建镜像时间为10s,其中RUN yarn installRUN yarn build都命中缓存无耗时,多流水线细节可看此处

🚌 2. 如何实现回滚操作

回滚是指把服务器上的项目撤回到上一个版本,用于在上线新版本的项目后,发现紧急 bug。需要紧急手动撤回到旧版本的情景。接下来通过Github Action实现一个简单可用的Rollback回滚流水线。直接附上Rollback流水线的流程图:

基于docker部署微前端项目的入门实践指南🐋

接下来编写yml文件对应上面的Rollback流程图:

name: Rollback
on:
  workflow_dispatch:
    inputs:
      version:
        description: "choose a version to deploy"
        required: true
jobs:
  Rollback:
    runs-on: ubuntu-latest
    env:
      # 指定重新生成的容器名
      Container: vue3-ts-app
      # 指定镜像名
      Image: vue3-ts-app:${{ github.event.inputs.version }}
      # 指定要拉取的镜像文件
      ImageFile: vue3-ts-app.tar
      # 指定要从哪个发布版本拉取镜像文件
      Release: vue3-ts-app/${{ github.event.inputs.version }}
    steps:
      - name: Echo Input
        run: |
          echo "Version: $VERSION"
      # 从指定的发布版本里拉取镜像文件
      - name: Download ImageFile
        uses: dsaltares/fetch-gh-release-asset@master
        with:
          version: "tags/${{env.Release}}"
          file: ${{env.ImageFile}}
          token: ${{ secrets.GITHUB_TOKEN }}
      # 把镜像文件上传至生产服务器
      - name: Upload Image Tar to Deploy Server
        uses: easingthemes/ssh-deploy@main
        env:
          SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_TOKEN }}
          ARGS: "-avzr --delete"
          SOURCE: ${{env.ImageFile}}
          REMOTE_HOST: ${{ secrets.REMOTE_HOST }}
          REMOTE_USER: ${{secrets.REMOTE_USER}}
          TARGET: ${{secrets.TARGET}}
      # 登陆进生产服务器进行操作
      - name: Run Docker Container
        uses: appleboy/ssh-action@master
        env:
          Network: microfe
        with:
          host: ${{secrets.REMOTE_HOST}}
          username: ${{secrets.REMOTE_USER}}
          key: ${{ secrets.DEPLOY_TOKEN }}
          # 步骤:
          # 1. 停止移除容器
          # 2. 移除已有镜像
          # 3. 进入存放镜像文件的目录且把镜像文件转为镜像
          # 4. 检测若不存在microfe网络,则创建microfe网络
          # 5. 根据镜像创建且运行容器,且把容器接入到microfe网络
          # 6. 移除镜像文件
          script: |
            ${{ format('docker ps -q --filter "name={0}" | grep -q . && docker rm -f {0}', env.Container) }}
            ${{ format('docker rmi -f $(docker images -q  --filter reference="{0}")', env.Container) }}
            ${{ format('cd {0}' , secrets.TARGET)}}
            ${{ format('docker load < {0}', env.ImageFile)}}
            ${{ format('docker network ls -q --filter "name={0}" | grep -q . || docker network create {0}',env.Network)}}
            ${{ format('docker run -d --name {0} --network {1} {2}',env.Container,env.Network,env.Image)}}
            ${{ format('rm {0}',env.ImageFile)}}

最后的运行结果如下所示:

基于docker部署微前端项目的入门实践指南🐋

关于上图中Rollback流水线的更多执行细节可看此处

🚞 后记

这篇文章写到这里就结束了,如果觉得有用可以点赞收藏,如果有疑问可以直接在评论留言,欢迎交流 👏👏👏。