CI/CD时代的前端工程部署实践

大家好,我是老纪。

本文简要总结下我们的前端工程部署实践。

前端类型

前端工程通常分为两种:

  1. 纯前端应用:
    • 这种类型的前端应用在客户端(通常是浏览器)上运行,但仍可能需要与服务器进行数据交互,例如通过API获取数据或与后端进行通信。
    • 构建产物通常是由 HTML、CSS 和 JavaScript 组成的静态文件,可以直接部署到任何支持静态文件托管的服务器或云服务上。
    • 典型的纯前端应用包括静态网站、SPA(单页应用)、移动应用的前端部分等。
  2. 需要服务端渲染(SSR)的应用:
    • 这种类型的前端应用需要与服务器进行实时交互,并且可能需要在服务器上进行一些数据处理或模板渲染,然后将结果返回给客户端。
    • 构建产物通常包括前端代码以及服务器端代码,前端代码用于与用户交互,服务器端代码用于处理请求、生成页面内容等。
    • 典型的需要服务端渲染的应用包括MPA(多页应用)、部分渲染的应用(如Next.jsNuxt.js等框架支持的应用)、需要SEO优化的应用等。

对我们而言,大部分前端工程都是SPA,仅有官网相关的项目,涉及到SEO优化,选择了SSR框架(如Node.jsNext.jsNuxt.jsDenoFresh)等。

部署方式

传统的Java Web项目在早期以传统交付方式为主,这意味着前端和后端的代码在部署时会被打包成一个WAR(Web Application Archive)文件,然后部署到Java Web容器中(如Tomcat、Jetty等)。这种方式的优点是部署简单,只需将WAR文件放入指定目录即可,但缺点也很明显,比如前后端代码耦合度高、部署不够灵活等。

随着前端技术的发展和前后端分离的趋势,现代的Web项目采用了更为灵活的部署方式,例如使用前端构建工具(如Webpack、Gulp、Vite等)来构建前端代码,并将前端代码和后端代码分别部署到不同的服务器或服务上。

我们团队使用Docker将前端产物打包成一个容器,然后通过容器编排工具K8S(Kubernetes)进行部署和管理,实现弹性伸缩和高可用性,比如我们通常会部署3个节点。

由于我司内部使用GitLab进行代码管理,所以CI/CD自然而然地使用了GitLab自带的GitLab CI来实现自动化构建、测试、部署。

对GitLab CI有兴趣的同学,可以参考我之前的两篇文章《持续集成之.gitlab-ci.yml篇(上)》、《持续集成之.gitlab-ci.yml篇(下)》。

制作镜像

纯前端镜像

纯前端项目的部署非常简单,它只需要一个Web容器即可,目前通用的静态资源服务器有NginxApache等,我们以Nginx为主。

我们选用一个Nginx镜像,以下是个Dockerfile示例:

dockerfile 复制代码
FROM nginx:brotli

COPY ./ /usr/share/nginx/html

EXPOSE 80

nginx:brotli是我们重新编译过的一个带有br压缩模块的镜像。如果不用br压缩,也可以考虑其它alpine的镜像。

比如完整版本的镜像体积可能有60M+:

alpine的则是15M+,更小的甚至是5M左右:

纯前端工程,只需要将HTML等前端产物复制到/usr/share/nginx/html这个目录下,这个镜像就算制作完成了。

我们的Dockerfile之所以如此简单,是因为通常将构建步骤(npm run build)拆分到GitLab CI的前置任务中:

在当前步骤(docker),只需要从缓存中拉取到构建的产物就可以了。

如果没有前置步骤,Dockerfile里就要考虑二级构建,以下是个示例:

dockerfile 复制代码
# 第一阶段:构建阶段
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

# 第二阶段:运行阶段
FROM nginx:1.19.2-alpine
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

SSR镜像

服务端渲染的应用,就只能选择对应的服务器语言的镜像,它与只提供REST API的纯服务器的镜像没有区别。

Node.js的项目为例,它的镜像大体是这样的:

dockerfile 复制代码
FROM node:18.16.0-alpine

ADD ./ /app/
WORKDIR /app

ENV NODE_ENV production
ENV HOST 0.0.0.0

EXPOSE 80

CMD ["node", "dist/src/main.js"]

它在docker这一步里用到的产物,也是从前端步骤得来的:

与纯前端镜像稍微有些不同的是,除了buildnpm run build)外,还有一步是移除不必要的node_modules文件,目的是为了减少镜像体积。其实就是执行了以下脚本:

bash 复制代码
if [ "$NPM_TYPE" = "yarn" ]; then
  yarn install --production;
elif [ "$NPM_TYPE" = "pnpm" ]; then
  pnpm install --production;
else
  npm install --omit=dev --legacy-peer-deps;
fi

该脚本将package.jsondevDependencies的包都移除掉,因为这些是生产环境不需要的:

这也要求开发者务必养成良好的习惯,安装依赖包时想清楚应该添加到哪里。

配置文件

纯前端项目

对于纯前端项目,每个工程需要对应修改Nginx的配置文件。

try_files

最常见的是使用了history路由的SPA项目,需要在nginx.conf添加一句try_files

diff 复制代码
location / {
    root /usr/share/nginx/html;
+   try_files $uri $uri/ /index.html;
}

这段代码的意思是,当用户请求资源时,会到/usr/share/nginx/html下查找,首先尝试使用请求的URI($uri)作为文件路径,如果找不到文件,那么尝试将URI作为目录路径($uri/),如果还是找不到,那么最后尝试返回/index.html文件。

比如:/a.html会优先响应/usr/share/nginx/html/a.html,但如果没有找到,则响应/usr/share/nginx/html/index.html

这个配置是history路由所必需的。

静态资源缓存

对于hash后的静态资源(JS、CSS)文件,我们完全可以设置为强缓存为一年甚至更长:

nginx 复制代码
location ~* \.(?:css|js)$ {
  root /usr/share/nginx/html;
  try_files $uri =404;
  expires 1y;
  access_log off;
  add_header Cache-Control "public";
}

需要注意的是,这条配置需要加到上面的location /之上。原因是为了避免这些静态资源被响应为index.html页面。

API

只要不是纯静态的Web页面,必然要与后端进行交互,如果不想让后端配置CORS跨域,则通常要在Nginx里配置API代理:

nginx 复制代码
server {
  listen       80;

  location /api/ {
    proxy_set_header Host $host; 
    proxy_set_header X-Real-IP $remote_addr; 
    proxy_pass http://server-svc/api/;
  }
}

这里的server-svc是K8S中的Service名称,对应到具体的后端服务器的容器。

通常来说,你在开发阶段使用Node.js配置了多少代理,这里就得添加多少个API

SSR项目

对于SSR项目,推荐使用YAML作为后端的配置文件,仍是在K8S的deployment.yaml中配置为具体的数据库连接、端口号等信息:

以下是一段挂载/app/config.yaml为上面配置的示例:

yaml 复制代码
spec:
  replicas: 1
  selector:
    matchLabels:
      app: web

  template:
    metadata:
      labels:
        app: web

    spec:
      containers:
      - name: web
        image: xxx/spacex/xx-web:7.0.9

        ports:
        - containerPort: 80

        volumeMounts:
        - name: config
          mountPath: /app/config.yaml
          subPath: server.yaml

白屏问题

使用容器化部署,一个最大的坑是白屏问题。其复现的条件为SPA项目,使用了路由懒加载,用户在旧页面停留时点击路由跳转,这时请求到旧路由的hash资源(JS或CSS,此时已不存在了),导致代码错误。我推荐的方案是在代码中捕获该错误,重新刷新页面。详情可参见我以前的文章《SPA项目频繁上线导致白屏?轻松帮你搞定!》。

这又引出一个常见的优化思路,我们的网站版本更新后,要不要通知用户,怎么通知用户刷新页面?

自动化使得我们的部署变得频繁,我常推荐我的团队在HTML注入相应的package.json的版本号,以便确认线上环境的版本,因为容器可能部署失败,也可能回滚,也可能Web页面的上层有CDN缓存,问题可能会很复杂。

而在考虑网站版本更新的问题后,我意识到处理可以更简单些。我在docker构建的步骤中,统一注入了一个环境变量VERSION,也就是当前CI流水线的tag标签,那么在Dockerfile里添加一行代码,写入一个携带版本号的文件:

dockerfile 复制代码
RUN echo $VERSION > /usr/share/nginx/html/version.txt

前端定时请求/version.txt,就能判断是否要通知用户刷新页面了。这种方式,是上述白屏解决方案的一个积极的补充。

总结

本文我们深入探讨了前端工程部署的实践经验。在现代Web开发中,灵活的部署方式至关重要。我们采用了DockerKubernetes技术,实现了前端工程的容器化部署,为项目提供了弹性伸缩和高可用性的保障。

在镜像制作和配置优化方面,我们对纯前端镜像和SSR镜像进行了详细的讨论和示例展示。同时,我们也深入探讨了容器化部署可能出现的问题,并提出了解决方案,如白屏问题的处理和版本更新的通知策略。

希望这些经验能给读者朋友们提供一些参考和启示。

相关推荐
崔庆才丨静觅1 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60612 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了2 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅2 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅3 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅3 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment3 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
金刚猿3 小时前
01_虚拟机中间件部署_root 用户安装 docker 容器,配置非root用户权限
docker·中间件·容器
崔庆才丨静觅3 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊3 小时前
jwt介绍
前端