前端部署发展史

JavaScript/前端
378
0
0
2022-04-04

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

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

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

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

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

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

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

刀耕火种

一台跳板机

一台生产环境服务器

一份部署脚本

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

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

这时候,前端静态文件由 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 大致长这个样子

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,在部署流程中扮演的角色越来越重要。想着自己基础盘进一步扩大,前端又不禁开心地笑了

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

运维的 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 不太适合做这件事情)
  2. 跨域,跨域仍由运维在 nginx 中配置

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

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

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

使用多阶段构建优化镜像

这中间其实经历了不少坎坷,其中过程如何,详见我的另一篇文章: 如何使用 docker 部署前端应用。

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

  1. 构建镜像体积由 1G+ 变为 10M+
  2. 构建镜像时间由 5min+ 变为 1min (视项目复杂程度,大部分时间在构建时间与上传静态资源时间)
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 所有文件。充分利用镜像缓存,减少构建时间
  2. 多阶段构建,大大减小镜像体积

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

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

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

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

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

CI/CD 与 gitlab

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

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

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

  • CI,Continuous Integration,持续集成
  • CD,Continuous Delivery,持续交付

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

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

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

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

我的一个服务器渲染项目 shfshanyue/shici 以前在我的服务器中就是以

docker/docker-compose/gitlab-ci 的方式部署,有兴趣的可以看看它的配置文件

  • shfshanyue/shici:Dockerfile
  • shfshanyue/shici:docker-compose.yml
  • shfshanyue/shici:gitlab-ci.yml

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

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

另外,也可以试试 drone.ci,如何部署可以参考我以前的文章: github 上持续集成方案 drone 的简介及部署

使用 kubernetes 部署

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

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

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

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

构建镜像的服务器,提供容器服务的服务器,做持续集成的服务器是一台!

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

先来看看以前的流程:

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

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

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

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

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

  • 使用 k8s 部署你的第一个应用: Pod,Deployment 与 Service
  • 使用 k8s 为你的应用配置域名: Ingress
  • 使用 k8s 为你的域名加上 https

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

这个时候,gitlab-ci.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. 缓存,缓存由前端控制
  2. 跨域,跨域仍由运维控制,在后端 k8s 资源的配置文件中控制 Ingress

使用 helm 部署

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

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

于是有了 helm,如果用一句话解释它,那它就是一个带有模板功能的 k8s 资源配置文件。作为前端,你只需要填参数。更多详细的内容可以参考我以前的文章 使用 helm 部署 k8s 资源

假如我们使用 bitnami/nginx 作为 helm chart,前端可能写的配置文件长这个样子

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. 缓存,缓存由前端控制
  2. 跨域,跨域由后端控制,配置在后端 Chart 的配置文件 values.yaml 中

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

前端需要做的事情有:

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

那运维要做的事情呢

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

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

统一前端部署平台

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

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

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

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 部署你的前端应用

服务端渲染与后端部署

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

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

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

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

小结

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

道阻且长,行则将至。

原文作者:山月行---- GitHub发表
链接:
https://shanyue.tech/op/deploy-fe.html#
%E5%88%80%E8%80%95%E7%81%AB%E7%A7%8D