# 前端部署的发展历程

> 前端一说起刀耕火种，那肯定紧随着前端工程化这一话题。随着 react/vue/angular，es6+，webpack，babel，typescript 以及 node 的发展，前端已经在逐渐替代过去 script 引 cdn 开发的方式了，掀起了工程化这一大浪潮。得益于工程化的发展与开源社区的良好生态，前端应用的可用性与效率得到了很大提高。

- 发布于: 2020-03-11
- 更新于: 2026-09-30
- 原文: https://shanyue.tech/posts/frontend-engineering/deploy/
- 标签: frontend-engineering
- 专栏: 前端工程化

---

前端一说起刀耕火种，那肯定紧随着前端工程化这一话题。随着 `react`/`vue`/`angular`，`es6+`，`webpack`，`babel`，`typescript` 以及 `node` 的发展，前端已经在逐渐替代过去 `script` 引 `cdn` 开发的方式了，掀起了工程化这一大浪潮。得益于工程化的发展与开源社区的良好生态，前端应用的可用性与效率得到了很大提高。

前端以前是刀耕火种，那前端应用部署在以前也是刀耕火种。那前端应用部署的发展得益于什么，随前端工程化带来的副产品？

这只是一部分，而更重要的原因是 `devops` 的崛起。

为了更清晰地理解前端部署的发展史，了解部署时运维和前端（或者更广泛地说，业务开发人员）的职责划分，当每次前端部署发生改变时，可以思考两个问题

1. 缓存，前端应用中http 的 `response header` 由谁来配？得益于工程化发展，可以对打包后得到带有 hash 值的文件可以做永久缓存
1. 跨域，`/api` 的代理配置由谁来配？在开发环境前端可以开个小服务，启用 `webpack-dev-server` 配置跨域，那生产环境呢

这两个问题都是前端面试时的高频问题，但话语权是否掌握在前端手里

时间来到 `React` 刚刚发展起来的这一年，这时已经使用 `React` 开发应用，使用 `webpack` 来打包。但是前端部署，仍是刀耕火种

> 本篇文章要求你有一定的 Docker，DevOps 以及前端工程化的知识储备。如果没有的话，本系列文章以及 [个人服务器运维指南](https://github.com/shfshanyue/blog#%E4%B8%AA%E4%BA%BA%E6%9C%8D%E5%8A%A1%E5%99%A8%E8%BF%90%E7%BB%B4%E6%8C%87%E5%8D%97) 中的 Docker 部分会对你有所帮助。

## 刀耕火种

一台跳板机

一台生产环境服务器

一份部署脚本

前端调着他的 `webpack`，开心地给运维发了部署邮件并附了一份部署脚本，想着第一次不用套后端的模板，第一次前端可以独立部署。想着自己基础盘进一步扩大，前端不禁开心地笑了

运维照着着前端发过来的部署邮件，一遍又一遍地拉着代码，改着配置，写着 `try_files`， 配着 `proxy_pass`。

这时候，前端静态文件由 `nginx` 托管，`nginx` 配置文件大致长这个样子

```nginx
server {
  listen 80;
  server_name shanyue.tech;

  location / {
    # 避免非root路径404
    try_files $uri $uri/ /index.html;
  }

  # 解决跨域
  location /api {
    proxy_pass http://api.shanyue.tech;
  }

  # 为带 hash 值的文件配置永久缓存
  location ~* \.(?:css|js)$ {
      try_files $uri =404;
      expires 1y;
      add_header Cache-Control "public";
  }

  location ~ ^.+\..+$ {
      try_files $uri =404;
  }
}
```

不过...经常有时候跑不起来

运维抱怨着前端的部署脚本没有标好 `node` 版本，前端嚷嚷着测试环境没问题

这个时候运维需要费很多心力放在部署上，甚至测试环境的部署上，前端也要费很多心力放在运维如何部署上。这个时候由于怕影响线上环境，上线往往选择在深夜，前端和运维身心俱疲

不过向来如此

鲁迅说，向来如此，那便对么。

**这个时候，无论跨域的配置还是缓存的配置，都是运维来管理，运维不懂前端。但配置方式却是前端在提供，而前端并不熟悉 nginx**

## 使用 docker 构建镜像

`docker` 的引进，很大程度地解决了部署脚本跑不了这个大BUG。**`dockerfile` 即部署脚本，部署脚本即 `dockerfile`**。这也很大程度缓解了前端与运维的摩擦，毕竟前端越来越靠谱了，至少部署脚本没有问题了 (笑

这时候，前端不再提供静态资源，而是提供服务，一个 `http` 服务

前端写的 `dockerfile` 大致长这个样子

```dockerfile
FROM node:alpine

# 代表生产环境
ENV PROJECT_ENV production
# 许多 package 会根据此环境变量，做出不同的行为
# 另外，在 webpack 中打包也会根据此环境变量做出优化，但是 create-react-app 在打包时会写死该环境变量
ENV NODE_ENV production
WORKDIR /code
ADD . /code
RUN npm install && npm run build && npm install -g http-server
EXPOSE 80

CMD http-server ./public -p 80
```

单单有 `dockerfile` 也跑不起来，另外前端也开始维护一个 `docker-compose.yaml`，交给运维执行命令 `docker-compose up -d` 启动前端应用。前端第一次写 `dockerfile` 与 `docker-compose.yaml`，在部署流程中扮演的角色越来越重要。想着自己基础盘进一步扩大，前端又不禁开心地笑了

```yaml
version: "3"
services:
  shici:
    build: .
    expose:
      - 80
```

运维的 `nginx` 配置文件大致长这个样子

```nginx
server {
  listen 80;
  server_name shanyue.tech;

  location / {
    proxy_pass http://static.shanyue.tech;
  }

  location /api {
    proxy_pass http://api.shanyue.tech;
  }
}
```

运维除了配置 `nginx` 之外，还要执行一个命令: `docker-compose up -d`

这时候再思考文章最前面两个问题

1. 缓存，由于从静态文件转换为服务，缓存开始交由前端控制 (但是镜像中的 `http-server` 不太适合做这件事情)
1. 跨域，跨域仍由运维在 `nginx` 中配置

前端可以做他应该做的事情中的一部分了，这是一件令人开心的事情

当然，前端对于 `dockerfile` 的改进也是一个慢慢演进的过程，那这个时候镜像有什么问题呢？

1. 构建镜像体积过大
1. 构建镜像时间过长

## 使用多阶段构建优化镜像

这中间其实经历了不少坎坷，其中过程如何，详见我的另一篇文章: [如何使用 docker 部署前端应用](https://juejin.im/post/5c83cbaa6fb9a04a0f65fdaa)。

其中主要的优化也是在上述所提到的两个方面

1. 构建镜像体积由 1G+ 变为 10M+
1. 构建镜像时间由 5min+ 变为 1min (视项目复杂程度，大部分时间在构建时间与上传静态资源时间)

```dockerfile
FROM node:alpine as builder

ENV PROJECT_ENV production
ENV NODE_ENV production

WORKDIR /code

ADD package.json /code
RUN npm install --production

ADD . /code

# npm run uploadCdn 是把静态资源上传至 oss 上的脚本文件，将来会使用 cdn 对 oss 加速
RUN npm run build && npm run uploadCdn

# 选择更小体积的基础镜像
FROM nginx:alpine
COPY --from=builder code/public/index.html code/public/favicon.ico /usr/share/nginx/html/
COPY --from=builder code/public/static /usr/share/nginx/html/static
```

那它怎么做的

1. 先 `ADD package.json /code`, 再 `npm install --production` 之后 `Add` 所有文件。充分利用镜像缓存，减少构建时间
1. 多阶段构建，大大减小镜像体积

另外还可以有一些小优化，如

- `npm cache` 的基础镜像或者 `npm` 私有仓库，减少 `npm install` 时间，减小构建时间
- `npm install --production` 只装必要的包

前端看着自己优化的 `dockerfile`，想着前几天还被运维吵，说什么磁盘一半的空间都被前端的镜像给占了，想着自己节省了前端镜像几个数量级的体积，为公司好像省了不少服务器的开销，想着自己的基础盘进一步扩大，又不禁开心的笑了

这时候再思考文章最前面两个问题

1. 缓存，缓存由前端控制，缓存在oss上设置，将会使用 cdn 对 oss 加速。此时缓存由前端写脚本控制
1. 跨域，跨域仍由运维在 `nginx` 中配置

## CI/CD 与 gitlab

此时前端成就感爆棚，运维呢？运维还在一遍一遍地上线，重复着一遍又一遍的三个动作用来部署

1. 拉代码
1. `docker-compose up -d`
1. 重启 nginx

运维觉得再也不能这么下去了，于是他引进了 `CI`: 与现有代码仓库 `gitlab` 配套的 `gitlab ci`

- `CI`，`Continuous Integration`，持续集成
- `CD`，`Continuous Delivery`，持续交付

重要的不是 `CI/CD` 是什么，重要的是现在运维不用跟着业务上线走了，不需要一直盯着前端部署了。这些都是 `CI/CD` 的事情了，它被用来做自动化部署。上述提到的三件事交给了 `CI/CD`

`.gitlab-ci.yml` 是 `gitlab` 的 CI 配置文件，它大概长这个样子

```yaml
deploy:
  stage: deploy
  only:
    - master
  script:
    - docker-compose up --build -d
  tags:
    - shell
```

`CI/CD` 不仅仅更解放了业务项目的部署，也在交付之前大大加强了业务代码的质量，它可以用来 `lint`，`test`，`package` 安全检查，甚至多特性多环境部署，我将会在我以后的文章写这部分事情

我的一个服务器渲染项目 [shfshanyue/shici](https://github.com/shfshanyue/shici) 以前在我的服务器中就是以 `docker`/`docker-compose/gitlab-ci` 的方式部署，有兴趣的可以看看它的配置文件

- [shfshanyue/shici:Dockerfile](https://github.com/shfshanyue/shici/blob/master/Dockerfile)
- [shfshanyue/shici:docker-compose.yml](https://github.com/shfshanyue/shici/blob/master/docker-compose.yml)
- [shfshanyue/shici:gitlab-ci.yml](https://github.com/shfshanyue/shici/blob/master/.gitlab-ci.yml)

如果你有个人服务器的话，也建议你做一个自己感兴趣的前端应用和配套的后端接口服务，并且配套 `CI/CD` 把它部署在自己的自己服务器上

而你如果希望结合 `github` 做 `CI/CD`，那可以试一试 `github` + `github action`

另外，也可以试试 `drone.ci`，如何部署可以参考我以前的文章: [github 上持续集成方案 drone 的简介及部署](https://juejin.im/post/5dc0b563f265da4cef190b8a)

## 使用 kubernetes 部署

随着业务越来越大，镜像越来越多，`docker-compose` 已经不太能应付，`kubernetes` 应时而出。这时服务器也从1台变成了多台，多台服务器就会有分布式问题

**一门新技术的出现，在解决以前问题的同时也会引进复杂性。**

k8s 部署的好处很明显: 健康检查，滚动升级，弹性扩容，快速回滚，资源限制，完善的监控等等

那现在遇到的新问题是什么？

**构建镜像的服务器，提供容器服务的服务器，做持续集成的服务器是一台！**

需要一个私有的镜像仓库，这是运维的事情，`harbor` 很快就被运维搭建好了，但是对于前端部署来说，复杂性又提高了

先来看看以前的流程:

1. 前端配置 `dockerfile` 与 `docker-compose`
1. 生产环境服务器的 `CI runner` 拉代码(可以看做以前的运维)，`docker-compose up -d` 启动服务。然后再重启 `nginx`，做反向代理，对外提供服务

以前的流程有一个问题: **构建镜像的服务器，提供容器服务的服务器，做持续集成的服务器是一台！**，所以需要一个私有的镜像仓库，一个能够访问 `k8s` 集群的持续集成服务器

流程改进之后结合 `k8s` 的流程如下

1. 前端配置 `dockerfile`，构建镜像，推到镜像仓库
1. 运维为前端应用配置 `k8s` 的资源配置文件，`kubectl apply -f` 时会重新拉取镜像，部署资源

运维问前端，需不需要再扩大下你的基础盘，写一写前端的 `k8s` 资源配置文件，并且列了几篇文章

- [使用 k8s 部署你的第一个应用: Pod，Deployment 与 Service](https://juejin.im/post/5db8c2b46fb9a020256692dc)
- [使用 k8s 为你的应用配置域名: Ingress](https://juejin.im/post/5db8da4b6fb9a0204520b310)
- [使用 k8s 为你的域名加上 https](https://juejin.im/post/5db8d94be51d4529f73e2833)

前端看了看后端十几个 k8s 配置文件之后，摇摇头说算了算了

这个时候，`gitlab-ci.yaml` 差不多长这个样子，配置文件的权限由运维一人管理

```yaml
deploy:
  stage: deploy
  only:
    - master
  script:
    - docker build -t harbor.shanyue.tech/fe/shanyue
    - docker push harbor.shanyue.tech/fe/shanyue
    - kubectl apply -f https://k8s-config.default.svc.cluster.local/shanyue.yaml
  tags:
    - shell
```

这时候再思考文章最前面两个问题

1. 缓存，缓存由前端控制
1. 跨域，跨域仍由运维控制，在后端 `k8s` 资源的配置文件中控制 `Ingress`

## 使用 helm 部署

这时前端与运维已不太往来，除了偶尔新起项目需要运维帮个忙以外

但好景不长，突然有一天，前端发现自己连个环境变量都没法传！于是经常找运维修改配置文件，运维也不胜其烦

于是有了 `helm`，如果用一句话解释它，那它就是一个带有模板功能的 `k8s` 资源配置文件。作为前端，你只需要填参数。更多详细的内容可以参考我以前的文章 [使用 helm 部署 k8s 资源](https://juejin.im/post/5dbf7909f265da4d4b5fe7b4)

假如我们使用 [bitnami/nginx](https://hub.helm.sh/charts/bitnami/nginx) 作为 `helm chart`，前端可能写的配置文件长这个样子

```yaml
image:
  registry: harbor.shanyue.tech
  repository: fe/shanyue
  tag: 8a9ac0

ingress:
  enabled: true
  hosts:
    - name: shanyue.tech
      path: /

  tls:
    - hosts:
        - shanyue.tech
      secretName: shanyue-tls

      # livenessProbe:
      #   httpGet:
      #     path: /
      #     port: http
      #   initialDelaySeconds: 30
      #   timeoutSeconds: 5
      #   failureThreshold: 6
      #
      # readinessProbe:
      #   httpGet:
      #     path: /
      #     port: http
      #   initialDelaySeconds: 5
      #   timeoutSeconds: 3
      #   periodSeconds: 5
```

这时候再思考文章最前面两个问题

1. 缓存，缓存由前端控制
1. 跨域，跨域由后端控制，配置在后端 Chart 的配置文件 `values.yaml` 中

到了这时前端和运维的职责所在呢？

前端需要做的事情有:

1. 写前端构建的 `dockerfile`，这只是一次性的工作，而且有了参考
1. 使用 `helm` 部署时指定参数

那运维要做的事情呢

1. 提供一个供所有前端项目使用的 `helm chart`，甚至不用提供，如果运维比较懒那就就使用 [bitnami/nginx](https://hub.helm.sh/charts/bitnami/nginx) 吧。也是一次性工作
1. 提供一个基于 `helm` 的工具，禁止业务过多的权限，甚至不用提供，如果运维比较懒那就直接使用 `helm`

这时前端可以关注于自己的业务，运维可以关注于自己的云原生，职责划分从未这般清楚

## 统一前端部署平台

后来运维觉得前端应用的本质是一堆静态文件，较为单一，容易统一化，来避免各个前端镜像质量的参差不齐。于是运维准备了一个统一的 `node` 基础镜像，做了一个前端统一部署平台，而这个平台可以做什么呢

1. `CI/CD`: 当你 push 代码到仓库的特定分支会自动部署
1. `http headers`: 你可以定制资源的 `http header`，从而可以做**缓存优化**等
1. `http redirect/rewrite`: 如果一个 `nginx`，这样可以配置 `/api`，解决跨域问题
1. `hostname`: 你可以设置域名
1. `CDN`: 把你的静态资源推到 CDN
1. `https`: 为你准备证书
1. `Prerender`: 结合 `SPA`，做预渲染

前端再也不需要构建镜像，上传 CDN 了，他只需要写一份配置文件就可以了，大致长这个样子

```yaml
build:
  command: npm run build
  dist: /dist

hosts:
  - name: shanyue.tech
    path: /

headers:
  - location: /*
    values:
      - cache-control: max-age=7200
  - location: assets/*
    values:
      - cache-control: max-age=31536000

redirects:
  - from: /api
    to: https://api.shanyue.tech
    status: 200
```

此时，前端只需要写一份配置文件，就可以配置缓存，配置 `proxy`，做应该属于前端做的一切，而运维也再也不需要操心前端部署的事情了

前端看着自己刚刚写好的配置文件，怅然若失的样子...

不过一般只有大厂会有这么完善的前端部署平台，如果你对它有兴趣，你可以尝试下 `netlify`，可以参考我的文章: [使用 netlify 部署你的前端应用](https://shanyue.tech/op/deploy-fe-with-netlify.html)

## 服务端渲染与后端部署

大部分前端应用本质上是静态资源，剩下的少部分就是服务端渲染了，服务端渲染的本质上是一个后端服务，它的部署可以视为后端部署

后端部署的情况更为复杂，比如

1. 配置服务，后端需要访问敏感数据，但又不能把敏感数据放在代码仓库。你可以在 `environment variables`， `consul` 或者 `k8s configmap` 中维护
1. 上下链路服务，你需要依赖数据库，上游服务
1. 访问控制，限制 IP，黑白名单
1. RateLimit
1. 等等

我将在以后的文章分享如何在 k8s 中部署一个后端

## 小结

随着 `devops` 的发展，前端部署越来越简单，可控性也越来越高，建议所有人都稍微学习一下 `devops` 的东西。

道阻且长，行则将至。
