直接抄就能跑的超通用前端 docker 构建流程
这套方案实际上基本上可以满足绝大多数的前端项目构建需求了。只要你不是月活百万的 toC 项目,基本上这套方案然后小修小改一下都没问题。核心就是 docker 跑镜像时用 shell 实时替换环境变量。这个方案的上限其实很高,因为在脚本里能做很多事情,比如环境变量里做一些开关,然后把静态资源的挂载目录切换到一些 CDN 之类的,拓展起来就更方便一些。作者:HOHO链接:https://juejin.
怎么简化?
其实之前那套方案里可以优化的地方有两个,这两个就是本文要优化的重点:
- 增加环境变量的操作过于复杂:每次要添加一个新的环境变量,就要从 DockerFile 开始一路改到 index.html,要改很多东西。
- 构建流程不统一:前端静态资源的打包和 docker 镜像的构建是分开的,需要打包的机器上有前端的开发环境,这实际上对运维、交付、部署同事是有一定阻力的,能不能将其完全合并在一起?
针对问题1,我们分析一下就可以发现,前端的 docker 镜像实际上只有两个环境变量是必填的,就是 部署相对目录 和 后端服务地址。为什么这两个是必填的可以看 这篇文章。
所以如果你的项目稍微上一点规模,那么还是更推荐把这些搞到后端一个接口里的,这样更灵活,不用重启服务就能更新配置。后面也可以对接到管理页面。这样前端就只需要配置上面那两个必填环境变量就行了。
针对问题2,我们可以使用 Docker 的多阶段打包来把前端资源的构建整合到 docker 镜像的构建流程里。这样只要环境里有 docker 就可以跑出来前端镜像,方便了不少。并且这样更有利于 CICD 的对接,只需要配一下 docker 构建和缓存就可以了。
OK,分析到这里就可以了,下面我们直接进入正题:
1、环境变量配置
在 这篇文章 我们使用了请求 header 的方案把环境变量从 nginx 转发到了前端环境里。这次我们来实践一个新方案:shell 脚本替换法。两种方案各有优劣,大家按自己喜好选择即可。
首先是常规的 nginx 配置文件,这两个文件会被塞到 docker 镜像里:
nginx/nginx.conf
ini
代码解读
复制代码
user nginx; worker_processes auto; load_module modules/ngx_nchan_module.so; load_module modules/ngx_http_headers_more_filter_module.so; error_log /var/log/nginx/error.log warn; pid /var/run/nginx.pid; events { use epoll; worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; more_clear_headers 'Server'; log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; access_log /var/log/nginx/access.log main; sendfile on; tcp_nopush on; tcp_nodelay on; client_header_timeout 15; client_body_timeout 15; send_timeout 15; keepalive_timeout 65; gzip on; gzip_min_length 1k; gzip_buffers 4 16k; gzip_http_version 1.1; gzip_comp_level 6; gzip_types text/plain application/javascript application/x-javascript text/javascript text/css application/xml; gzip_vary on; include /etc/nginx/conf.d/server.conf; }
nginx/server.conf
ini
代码解读
复制代码
server { listen 80; server_name localhost; client_max_body_size 2048m; client_body_buffer_size 10m; include mime.types; types { application/javascript mjs; } location / { root /usr/share/nginx/html; index index.html index.htm index.shtml; try_files $uri $uri/ /index.html; if ($request_filename ~* .*\.html$) { add_header Cache-Control "no-cache, no-store"; } } location /webapi/ { proxy_pass {HOHO_BACKEND_URL}; proxy_connect_timeout 240s; proxy_read_timeout 240s; proxy_send_timeout 240s; } # 定义静态资源的根目录 root /app/static/; location /static/ { alias /app/robot_data/; expires 1h; add_header Cache-Control "static"; } location /webws/ { proxy_pass {HOHO_BACKEND_URL}; proxy_http_version 1.1; proxy_connect_timeout 240s; proxy_read_timeout 240s; proxy_send_timeout 240s; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "Upgrade"; proxy_set_header X-real-ip $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; } error_page 405 =200 $uri; error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } }
没什么好说的,其中有一些小细节大家可以参考一下:
- nginx.conf 开头使用了两个
load_module
来移除请求里的serve: nginx
响应头。一些安全和合规测试可能会有这个要求。注意要用这个 nginx 插件的话我们用的 docker 基底镜像就不是官方的 nginx 镜像而是rookiezoe/nginx
。 - serve.conf 中提供了三种常见的需求,分别是:代理后端接口、代理静态资源、代理 websocket 服务。其中静态资源是通过 docker 挂载存储卷到
app/static/
目录的方式实现的。
nginx 的配置中使用了 {HOHO_BACKEND_URL}
来标记了后端服务地址的占位符。我们稍后就会把这个字符串替换为实际的后端地址。
看完了后端我们来看一下前端,首先是 index.html,其中通过 base 标签指定了当前的相对文件路径:
html
代码解读
复制代码
<head> <base href="%VITE_PATH_BASENAME%" /> <script> window.CONFIG = { PATH_BASENAME: '%VITE_PATH_BASENAME%', } </script> </head>
这个配置相比于之前的 request header 获取参数的方案可以说是简单了很多。注意其中把配置注入到了 window.CONFIG 上,这样路由就可以配置这个路径了:
src/global.d.ts
php
代码解读
复制代码
declare const CONFIG: { /** 前端路由前缀 */ PATH_BASENAME: string; };
src/route.ts
ts
代码解读
复制代码
import { createBrowserRouter } from 'react-router-dom'; export default createBrowserRouter(routes, { basename: CONFIG.PATH_BASENAME, });
这里你可能会好奇,不是使用 docker 环境变量么?你这用的是 VITE 的啊。确实是这样的,因为我们的 vite env 配置如下:
.env
ini
代码解读
复制代码
VITE_PATH_BASENAME="/"
.env.production
ini
代码解读
复制代码
# 线上环境的配置由 docker env 提供,所以这里要把本地开发环境使用的配置从包里去掉 VITE_PATH_BASENAME="{HOHO_BASE_URL}"
我们本地开发的时候,相对目录就直接是 \
,而生产打包之后,这个 %VITE_PATH_BASENAME%
就会被替换成 {HOHO_BASE_URL}
,最终在 docker 镜像启动的时候被 shell 脚本替换成环境变量。
注意别忘了把 vite 配置成相对目录模式:
js
代码解读
复制代码
export default defineConfig({ base: './', });
OK,现在我们来介绍最终的 docker 启动脚本:
sh
代码解读
复制代码
#!/bin/sh set -e # 替换 nginx 配置文件中的环境变量 sed -i "s|{HOHO_BACKEND_URL}|${HOHO_BACKEND_URL}|g" /etc/nginx/conf.d/server.conf sed -i "s|{HOHO_BASE_URL}|${HOHO_BASE_URL}|g" /etc/nginx/conf.d/server.conf # 替换 index.html 文件中的环境变量 sed -i "s|{HOHO_BASE_URL}|${HOHO_BASE_URL}|g" /usr/share/nginx/html/index.html # 在控制台打印当前使用的环境变量 echo "[ENV] frontend deploy path: $HOHO_BASE_URL" echo "[ENV] backend proxy path: $HOHO_BACKEND_URL" # 显示 nginx 启动成功 echo "[DONE] nginx started successfully!" # 启动 Nginx exec nginx -g 'daemon off;'
很简单对吧,用 sed 把上面三个文件中的占位符 {HOHO_BASE_URL}
和 {HOHO_BACKEND_URL}
替换成环境变量中的实际值。最后前台模式启动 nginx。
注意开头的 set -e
,不加的话你直接 docker run
以前台模式启动容器时就没办法按 ctrl + c
来关闭服务了。
2、Docker 构建合并前端打包流程
通常情况下,我们都是直接本机执行 npm run build 来打包出 dist 文件,然后 COPY 到 docker context 中。但是实际上我们可以直接在 docker 构建流程中实现这个流程:
DockerFile
DockerFile
代码解读
复制代码
FROM node:18.19.1-slim as build-stage ENV NODE_OPTIONS="--max-old-space-size=4096" RUN npm install -g pnpm WORKDIR /app COPY package.json pnpm-lock.yaml ./ RUN pnpm install --frozen-lockfile COPY . . RUN pnpm run build FROM rookiezoe/nginx:1.24.0 as prod-stage COPY --from=build-stage /app/dist /usr/share/nginx/html COPY --from=build-stage /app/package.json /usr/share/nginx/html/node-package.json ENV HOHO_BASE_URL=/ COPY ./nginx/nginx.conf /etc/nginx/nginx.conf COPY ./nginx/server.conf /etc/nginx/conf.d/server.conf COPY ./nginx/start.sh /usr/local/bin/start.sh RUN chmod +x /usr/local/bin/start.sh EXPOSE 80 ENTRYPOINT ["/usr/local/bin/start.sh"]
其实就是先启动一个 node 的构建容器,打包好之后把这个容器中的 dist 目录拷贝到基于 nginx 的生产镜像中。这里我使用了 pnpm,还配置了个更大的构建内存,你不用的话直接删掉就行。
注意在生产镜像中给前端的相对目录配置了一个默认值 /,因为大部分前端都是部署在根目录下。这样我们通常情况下只配置一个后端地址 HOHO_BACKEND_URL
即可。
docker build 中的缓存
上面的 DockerFile 中用分层策略进行了一个构建缓存,就是这几行:
sql
代码解读
复制代码
COPY package.json pnpm-lock.yaml ./ RUN pnpm install --frozen-lockfile COPY . . RUN pnpm run build
这样的话就可以实现,如果 package.json
或 pnpm-lock.yaml
没有变化的话,就直接 run build 而不下载依赖。这个是 docker 的默认行为,具体可以看 Build cache invalidation | Docker Docs。
不过上面这个缓存只会在本机中才能实现,对于 CICD 这种每次构建都是新机器的情况下就没效果了。在 CICD 这种场景,我们可以使用 挂载缓存目录 和 使用外部缓存 这两个方案。具体内容展开就太长了,我们这里就按下不表了。
总结
这套方案实际上基本上可以满足绝大多数的前端项目构建需求了。只要你不是月活百万的 toC 项目,基本上这套方案然后小修小改一下都没问题。
核心就是 docker 跑镜像时用 shell 实时替换环境变量。这个方案的上限其实很高,因为在脚本里能做很多事情,比如环境变量里做一些开关,然后把静态资源的挂载目录切换到一些 CDN 之类的,拓展起来就更方便一些。
原文链接:https://juejin.cn/post/7443684199036977162
更多推荐
所有评论(0)