本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!
继上篇提到的前端项目容器化部署,这一篇接着展开关于“容器重启后丢失原前端资源”的现象和解决方案的分享。相信结合这篇文章一起看,大家可以更加理解前端项目静态资源部署和容器化部署的本质并无区别~
静态资源丢失
相信搞过前端容器化部署的同学都一定会遇到同一个问题,那便是当某次正常的发版后,有个产品大哥突然急匆匆地跑过来说有线上用户反馈页面 404 了!这不由得让人为之一颤,不过回过神的我们仔细思考下,叫用户刷新一下页面,一切功能又恢复正常了!
没错,就是一个简单的刷新就能解决问题,但显然这并不是长久之计,无论是对用户还是对产品大佬,我们都没办法交代...到最后的结果,一定还是得我们来将这个问题彻底解决掉。那既然逃不掉,我们就来认真对待一下吧~首先分析一下引起整个问题的原因!
首先看问题的前提条件,静态资源全放在容器内的部署方式:
目前,我们在构建阶段结束后,将产物通过 Dockerfile 的 COPY 命令复制到一个 Nginx 的 html 路径中,并且 build 成一个 Docker 镜像,推到镜像仓库。然后再通过该镜像启动容器,容器启动则会运行 Nginx 的一个启动命令,以此来完成整个前端项目的部署工作。因此,当前我们所有前端静态资源都位于该容器中。
基于上述的部署方式,再看一下为何静态资源会丢失,如下图所示:
可以想象为以下场景:用户正在浏览单页应用,已经加载了 index.html
并加载了当前所需的 js、css 资源(采用异步加载组件 lazy 的方式打包),正在愉快的刷着内容。此时一个前端小佬为了解决一个线上问题,需要更新线上版本,于是 CI 结束后,重启容器对新版本进行发布...过了一会,用户点了个抽奖按钮即将要跳转到抽奖页面,但此时页面却报出了 404 的错误码...!
原因很简单,原本 index.html 所需要加载的抽奖组件代码 抽奖1.js
由于容器重启后已经不存在了(变成了抽奖2.js
)。但是此时 index.html 依然是发送链接为 xxx.com/assets/抽奖1.js
的请求到 nginx,那结果自然是拿不到对应资源从而报错了...
这里简单总结一下:前端单页应用项目通过容器化部署时,容器重启后将丢失当前的静态资源。如果有存量用户还在访问页面时,那么将可能会遇到 404 的问题。注意这里单页应用是个前提,多页应用反倒没了这样的担心,其中的原因我想你应该懂的~
静态资源持久化存储
其实容器重启后丢失资源的核心,就是没有持久化存储。当我们解决了资源的持久化存储这个问题后,不管我们如何重启容器,都不会再遇到丢失资源的问题了。这一步并不会特别复杂,而且解决方案应该特别多,所以大家不需要有太多的心理负担。接着,我将分享其中一种实现方案,我们接着往下看吧。
再次回顾之前的 CI/CD 流程图:
注意,本文我们依然关注 CD 部分(CI 部分如前一篇文章所说的那样,不用管其流程要怎么实现,我们主要明确打包机执行完 build 任务后,就会输出一份产物给到我们拿去部署)。上图是我之前分享实战前端项目 CICD 时的一张图,那时候介绍的 CD 方式是静态资源部署,也就是我们只管上传文件,web服务器那块是其他团队在管理的。
那为什么这里我又把这张图扒出来了呢?或者说他跟我刚才所提到的静态资源持久化部署有什么关系呢?相信我这么一问你就会明白,其实之前的上传静态资源的部署方式不就是一种 资源持久化 的部署吗?我们不需要重启容器,并且中间通过 rsync 实现文件的增量上传,这不正是我们想要的效果吗?
CDN和源站的概念
其实上传服务器好像没什么好说的了,就是把文件上传到一个长期存在的文件服务器而已。但我这里还是想提一嘴,那就是我对于文件服务器和所谓“CDN”的概念理解。
关于源站、CDN,我认为他们是两件不同的东西。之前在分享 CICD 文章时(包括上面的前端发布流程图),我都一直有提到,我们所上传的文件服务器就是源站;CDN 就是 CDN,并不属于同一类东西(我跟几个运维大佬求证过这个概念)。但是还有不少同学习惯性的说我把 xxx 扔到 CDN...其实不仅仅截图的如此,我遇到还有很多同学都这么说~
不能说这个说法错,确实有上CDN的项目资源都会存到CDN上,但至于这个CDN上的资源是怎么来的,是我们直接扔上去的吗?或许有个别的资源或者特殊处理的业务逻辑,确实是有将资源直接推到CDN的可能,但是对于大部分场景来说,我觉得这种说法不算特别严谨。
对于这个 CDN 的概念性问题,首先谈谈我的理解吧。当我们有项目用上 CDN 的时候,CDN 的服务器中确实会存在我们的资源文件,但 CDN 上的资源文件是怎么来的呢?是我们每次发布项目之后,通过某些他们提供的 API 进行文件推送吗?我感觉这不太现实,毕竟前端发版的频率、文件数量、文件体积情况多变,再加上接的 CDN 多,CDN分布的区域多的情况下,如果是我们每次部署都主动推送到 CDN 的话,我认为会是个不小的成本。
其次,现存的 CDN 厂商这么多,我们如果接了 N 个 CDN 厂商,岂不是要对接 N 个 CDN 厂商提供的不同 API 吗?那对于我们自己的人力成本来说也是一个不小的开销。所以我认为,我们并不会真正的“推送”文件到 CDN,也不存在说我们把资源主动扔到 CDN 上的说法。
那么回到我提的问题,CDN 上的资源文件是怎么来的呢?我个人认为是回源。大家都很熟悉浏览器缓存了,浏览器根据 http 的各种头信息进行资源缓存,当资源过期的时候再对其上游获取新的资源。为了方便大家理解,其实可以想象成 CDN 就是一个类比浏览器缓存的存在。当请求走到 CDN 获取资源,CDN 中没有、或者资源过期了,CDN则会继续往其上游请求资源,最后会回到我们文件服务器,也就是源站。
简单总结一下:常规 CDN 最终是通过回源获取资源的话,我们在部署阶段所上传的资源,其实只是上传到一个文件服务器(源站),并不直推CDN(当然不排除一些重要资源,如 index.html 可能会有策略主动推送)。
走远了哈。那么,再回到本文解决容器重启丢失资源的问题,其实我们再把资源上传到一个长久存在的文件服务器,不就解决了吗?这个长期存在的文件服务器,大家直接认为他是一个永远不重启的容器,这样是不是就一下理解了呢?
CI/CD 微改造
为了实现上述的上传文件到文件服务器,并且继续采用容器化部署前端项目的方式,我们需要对之前的打包方式、部署方式(部署前要推资源到源站),不过这一切也不复杂,大家接着往下看~
首先我们看看为什么需要改造、以及怎么进行改造。这里我通过一个普通项目的打包结果来说明一下。比如我用 vite 直接打包一个新项目如下:
基本上我们用默认配置的话,打出来就是这样的结构。根目录有一个 index.html
,也就是我们的单页入口,然后所有的 js 资源都会被打到 assets 目录下。此时我们看看 index.html
的内容,他通过相对路径的方式引用了对应打包后的资源。所以当我们加载这个 html 的时候,他会加载对应的 js 和 css 资源。
回顾一下容器化部署,上一篇讲容器化部署的时候,是通过 docker build 直接把这整个 dist 目录拷贝并打包成 一个 Docker 镜像。这样,当我们配置好对应的 nginx 后(将请求配置到 dist 包的 index.html
),nginx 监听到网络请求就会到对应的路径返回对应的资源文件,这样我们的容器化部署就算完成了。
但是如果按照上述的方式部署,就会出现本文需要解决的问题,便是重启容器会丢失了旧的资源,让还处于旧版资源运行时的前端页面中,会出现 404 的情况。接下来,我们先看看整体流程,捋一捋改造的思路:
如上图所示,我们在容器化部署前,除了将 dist 包打成镜像外,还进行了一个静态资源上传服务器的操作。并且,当我们将打包好的镜像启动成容器时,是通过文件服务器(源站)来加载静态资源的。这样一来,容器内的前端加载并不依赖容器内的资源了,而是依赖容器外(源站)的 资源,所以我们并不需要担心重启容器的时候会出现静态资源的丢失问题了。
一句话总结便是,容器化部署时只有 index.html
是有用的,其他的 assets 资源都是通过远程加载其他服务器的。要实现这一个过程其实也很简单,前文有提到 vite 打包完后的 index.html
通过相对路径加载资源的,现在只需要对这个相对路径处理一下便可实现这种“远程加载”的方式了。具体我通过一个小demo,来给大家讲解得更细一点。
还是刚才打包的项目,此时通过 preview
来预览打包后的网页效果:
这时候可以观察到页面正常加载:
这一点没问题,接下来我改动一下引用 css 的路径如下:
这时候再通过 preview
看效果,你应该可以猜到样式是有问题的:
好吧,不玩了,我把 css 文件扔到 test/assets 目录下再看就没事了:
通过这么一个小 demo,大家是否已经 get 到了我的用意了呢?只要我们把资源上传到一个源站,并且这个源站的资源可以通过链接的方式访问,此时我们只要把 index.html
和其他所有依赖其他资源文件的引用路径变成对应的“链接”就完事了呢?
大家留意 vite 文档也许会发现有这么一个介绍关于 进阶基础路径选项,这玩意可以很好的帮我们实现这个需求。他具体的作用、玩法,大家可以戳链接进去看看,我这里简单演示一下,大家一看效果就明白了。
参照其用法:
我对这个项目的 vite.config 进行一个 experimental.renderBuiltUrl
配置如下:
export default defineConfig({
plugins: [vue()],
experimental: {
renderBuiltUrl(filename, { hostType }) {
if (hostType !== 'html') return
const projectName = 'my-app/'
return 'https://my.assets.com/' + projectName + filename
},
}
})
假设 https://my.assets.com
这个就是我们把资源上传到的的源站的 origin,于是我便把 index.html
的文件中所引用的路径改成到源站的链接。并且我期望通过项目进行路径分割,也就是 origin + 项目名 + 文件名
这样的路径形式。我们再看看此时打包出来的效果:
此时,打包结果跟之前的区别便是,index.html
中,对 css、js 资源的引用路径不再是相对路径了,而是一个链接了。另外,只要我们也将产物上传到对应的服务器的对应路径上,资源持久化这件事算是完成了。
也就是说,当我们还是通过这个 index.html
使用容器化部署前端项目时,它加载的资源可能来自源站、可能来自CDN,但一定不会来自当前容器内的文件了,这时候再也不用担心重启容器后由于丢失旧的资源而导致用户端出现 404 错误了。
总结
本文主要介绍了容器化部署时, index.html
通过加载外部服务器的资源来解决容器重启时的资源丢失问题。当然解决方案有很多,不局限于此。本文通过一个对于前端同学来说比较简单的解决方案来解决这个问题,更多是想分享一下容器重启丢失资源后的轻量化处理。
不过个人认为这个解决方案需要建立在一定的基建基础上。起码在把静态资源上传到文件服务器,还有通过链接能正常访问这两件事不需要过度投入的前提下,这对于前端同学来说才算是简单的方案。另外还有大概了解下挂载盘等其他实现方案,不过根据当前团队发展情况和基建基础,选择一个最简单的实现方案才是明智之举。
另外还提了一下关于 CDN 和 源站的概念,如果有表述不妥的话,还请各位大佬指出,这只是我个人的一些拙见~
最后,很久之前就有搞前端 CICD 这块领域,也经历过纯静态资源部署、容器化部署的团队,再历经解决容器化部署资源持久化存储的过程,真心感觉前端项目的部署无非还是两个本质:静态资源、web服务器。或许有同学看过我上一篇文章,其中有提到:
相信看完本文的你,更加能体会到这段话的含义了。不管是容器化部署、还是上传静态资源到服务器的两种部署方式,本质上都没有区别。比起两种方式的技术实现区别,我依然认为更多的区别只是在于那个文件服务器的管辖权限在谁的手里而已。毕竟回到前端项目部署的本质,便是让用户能通过链接,在浏览器能愉快地浏览前端页面,仅此而已~