还记得你第一次发布生产的场景吗?
我已经完全忘记了… 印象中需要我紧盯发版的时候,我已是 24h On Call 的状态 😵💫
最开始做 Web 应用的时候,我还在 .jsp
里摸爬滚打,前端代码也在 Java
工程中,
发版就是部署 jar
包。偶尔后端的老哥还能帮我处理几个 Bug
后面又使用了一段时间的 PHP
,弱类型语言不用编译,当年最简陋的发版:
SSH
登录服务器,git pull
拉代码,发版结束…
SPA 应用的出世,前后端分离架构的流行,我才开始正式的接触 Build & Deploy 的流程。
归档构建产物
在最初没有标准化的 CI/CD 时候,
将前端的构建产物,例如 /dist
目录上传服务器是最常见的部署流程。
不论是公司分配的虚拟机,还是自己购买的 ECS,
大部分情况 Deploy 服务器内存都很小,无法在上面进行 Build。
本地打包、归档构建产物、上传归档文件、解压部署
这种构建部署流程,在公司资源少,项目小的场景中很是常见。
针对本地打包和归档构建,思路都是类似的:归档指定目录至根目录。
如果你使用 vite
,可以自定义一个归档插件,在构建完成后执行归档脚本:
脚本会将 dist/
目录归档成 ${env}_${pkgName}_${YYYY-MM-DD}.zip
归档文件创建成功后,剩下的就是将 *.zip
上传至服务器,
我们可以编写一个脚本来完成这一步:
上传完成后,便可登录服务器,将归档文件解压到对应的目录进行部署:
发布构建产物
上述 本地打包、归档构建产物、上传归档文件、解压部署,其实还挺麻烦的,
打包发布折腾的如此别扭的根因,无非就是服务器无法打包,只能本地打包。
既然如此,那我们本地打包后,将 /dist
通过 git push
提交上去,
然后登录服务器后执行 git pull
拉代码就可以完成部署呢?
当然不可以!
一个比较好的方式,是将产物强制推送到新分支,例如 github 的 gh-pages
:
这样构建产物单独一个分支,和源码完全隔离开,部署只需要每次拉取 gh-pages
分支的最新代码即可。
Docker
随着微服务、容器化的盛行,前端的部署也蹭上了“容器化”的列车。
前端容器化最简单的 Dockerfile
配置大概如下,使用 node
进行打包,使用 nginx
进行部署。
为了保证每次发版客户端都能立即生效,避免因浏览器缓存而引起的各种问题,
对 nginx.conf
增加些特殊配置:
html
不缓存;
- 其他静态资源文件,缓存30天;
嵌套路径
将前端部署在根路径非常简单,但是如果部署嵌套路径下,事情就会变得复杂。
拿 Vite + React18 + ReactRouter6
举例,假设我们有一个项目,
并期望将它们部署在嵌套路径下:foo
部署至 http://domain.com/foo
。
对于 Vite
而言,你需要设置它的 base: '/foo'
:
对于 ReactRouter6
而言,你还需要设置它的 basename="/foo"
:
如果你使用的是 VueRourter
就不需要了
配置完毕之后,当你的项目构建完毕之后,它就会正常的从 /foo
路径请求资源文件,
对于 Nginx
,我们还需要一些额外的配置:
更新提示
“解压文件”和“容器化”这两种部署方式,就是我们常说的:增量部署和全量部署。
增量部署 通常不会出什么大问题,因为每次发版之后,旧的静态资源文件还在,
全量部署 则不然,每次发版之后,旧的静态资源文件已全部删除。
如果你的项目使用了路由懒加载,用户在发版前已经打开了应用,
发版完成后,若用户点击了未访问过的路由,往往会出现白屏,
虽然我们已经设置了 index.html
不缓存,
但是需要用户刷新一次应用才会重新获取新版本的静态资源文件。
如何在前端部署成功之后给出更新提示?
想要给出更新提示,前端就需要知道当前客户端的版本号落后于最新版本号,
在每次路由变化时,获取最新版本号,比对新旧版本号是否一致。
这里的版本号不是指 major.minor.patch
,而是指 buildId
,
我们在构建时,将 { version: timestamp }
写入到 public/version.json
中,
通过 define
定义全局常量 define: { __APP_VERSION__: timestamp }
,
如果你也使用 vite
,我们可以简单编写一个插件来完成这一步:
我们还需要更改 nginx.conf
,设置 *.json
也不缓存:
使用 axios
或者 fetch
,在 vue-router
每次路由变化时,获取最新的版本号并进行比对,
由于 *.json
不会被缓存,所以我们总能获取到最新的文件:
react-router@6
并没有 beforeEach
这类钩子函数,通过封装 useNavigate
也可以实现这种拦截效果:
这样做虽然可以解决问题,但是大部分版本比对是无效的。
应该没有哪个项目天天发生产吧?
那么每次路由切换所产生的 Get /version.json
除了浪费带宽,没有任何实质的意义…
vite@4
提供了 vite:preloadError
用来处理加载报错,
所以我们只需要监听这个错误,而后封装一个容器组件即可,vue
同理: