飞书深诺前端 SPA 敏捷部署方案演进

一、背景介绍

飞书深诺集团是专注海外数字营销解决方案的专业服务提供商,为有全球化营销需求的企业提供标准&定制相结合的全链路服务产品,满足游戏、APP、电商、品牌等典型出海场景需求。到 2022 年,集团各产线前端单页面应用(下文直接用 SPA 代替)的数量,已经占前端项目总数量的三分之二以上,个别巨石应用会面临以下问题:

  • 应用单次构建-发布时间,已经超过了10min,从开发到生产上线部署,会经历 4 个环境 dev / test / pre / prod,总部署时间大于 40min 以上。
  • 应用接入了 6+ 其他产线提供的 MF(Module Federation) 微前端组件,任何微前端组件进行版本升级时,都需要重走 dev -> prod 4 个环境的构建发布流程。
  • 考虑到 test 环境可能同时有多个测试在多环境进行发布测试,这个量级又以倍数上升。

Life is short,将如此多的时间浪费在构建发布上是不值得的,解决此问题势在必行。

二、基本思路

解决思路拆解

根据上面信息可知,当前总构建发布时间

= 单次构建发布时间 * 环境个数

= 单次构建时间 * 环境个数 + 单次发布时间 * 环境个数

因为单次发布时间,一般只涉及到镜像的推送与发布,已基本没有可提升的空间,所以解决思路目前有两个:

  • 解决思路一:减少单次构建时间

  • 解决思路二:构建产物实现多环境复用,使公式由(* 环境个数)变为(* 1)

解决思路一:减少单次构建时间

目前业界针对减少单次构建时间,主要有以下常规主流方案:

  1. 使用 swc 或 esbuild 等新一代构建工具,带来极致的构建效率

  2. 应用构建缓存,本质上都是实现增量构建,主要有两种方案:

    • 利用 docker 构建缓存,将不变的步骤缓存起来
    • 利用 webpack 或 esbuild 本身构建缓存,提升打包速度
  3. 多进程优化,利用 webpack,实现多进程打包

以上方案业界内均有成熟实践,社区方案成熟度都还不错,也有较完善的文档进行支持,这里我们基于对 swc、esbuild 与 babel 在具体项目实践的横向对比,使用了 esbuild 替代原有的 babel,提升了单次构建时间。

解决思路二:构建产物多环境复用

构建各环境都能通用的服务镜像,并在不同环境部署的思路,并无新颖之处,在发布后端服务时,先构建服务镜像、再发布并在镜像运行时注入环境变量,是早已实现的通用方案。

我们能否借鉴后端构建发布模式,实现对前端构建产物的复用呢?

如果要部署一个 Java 服务或 Node.js 服务,流程往往是这样的:

根据上面时序图可以看到,在部署服务时,通过向推送至镜像仓库的通用 docker 镜像,注入不同的环境变量、以及配置中心的配置,来实现服务在不同环境的差异化。

通过时序图也可以看到,后端服务部署存在三个核心点:

  • 运行环境
  • 配置中心设置的全局变量
  • 通用软件制品(可以复用的编译后产物)

整个过程可以抽象理解为这样:

技术的底层逻辑上,前端后端没有本质区别,参考后端服务的构建思路,仔细分析 SPA 构建、打包后的产物,得到我们处理前端应用的方案,如下图所示:

通过以上设计,可以使 html 承担起全局变量维护及变量注入的功能,而将 SPA 的主要构建产物 js、css,变成通用制品,实现多环境复用。

为支持通用制品的构建、以及多环境 html 对通用制品的复用,需要对运维部署架构进行重新设计。

一般来说,可以将 SPA 的构建产物分为 3 个类别:

  • 抽出了包含变量的 index.html
  • 通用制品 js / css
  • Static 下的其他静态文件,如 jpg、png 等

传统部署方案是,将上面 3 个类别的构建产物,全部放到 nginx 的 docker 镜像内。(注:使用镜像的原因是方便运维实现多环境及灰度功能。)

当用户访问时,会通过 SLB 打到 K8s 集群,再由内部的 ingress-nginx 将流量分发到相应的 docker 镜像的 index.html 等静态资源,如下图所示:

按照构建通用制品的解决思路,为了实现 js / css 的共用,整体部署应改为:

如图所示:

  • 通用制品 js/css 构建 1 次后,即可多环境复用,可以部署在云存储空间内。(如阿里云 oss、腾讯云 cos)

  • 虽然仍需要根据不同环境,生成不同的 html 文件。但这个过程可以用 html 模版引擎替代完成,生成时间可以非常短(1s内)。

三、整体方案

根据以上思路,对 SPA 的新架构敏捷部署方案的设计如下:

全流程时序图

各步骤详解

开发 -> 构建 js / css

  • 开发时,需要注意项目中,所有根据环境变量变化而变化的变量,都要维护在 html 里。如 api 的 baseUrl, 在 test 环境为 test-xxx.xxx.com,在 prod 环境为 xxx.xxx.com,这时可以在 html 中维护一个变量 window.appConfig.gateway = 不同的值,然后在项目中直接使用 window.appConfig.gateway 即可。
  • 构建时,构建的 js / css,一定是抽离了所有受环境变量影响的变量,这样才能被多环境复用。
  • 构建时,我们用的 CI 为 Gitlab-runner,这里因为我们使用 k8s 集群,所以构建时,运维会自动拉起一个临时 runner 执行构建任务,执行后动态销毁该 runner,对服务器资源占用较少。

构建 js / css -> 上传云存储空间

  • 构建时,使用的 publicPath ,通常为 ${云存储空间的CDN域名}/${项目ID}/${版本号}/
  • 构建结束后,会将制品 js/css 资源传入云存储空间内,上传到 ${云存储空间的CDN域名}``/${项目ID}/${版本号}/ 目录下,与 publicPath 保持一致。
  • 上传成功后,${项目ID}/${版本号} 将作为唯一版本标识,来定位项目当前使用的 js、css 版本。

配置中心 -> 生成 html

前端同学可能会很少接触,但做过后端的同学都会对服务注册、发现及配置管理的概念非常熟悉,这里我们使用的配置中心是 Apollo [参考一]。

这里需要针对每个 SPA 项目,在配置中心的新建一个 app-id 应用,并配置一个基础 namespace,在该 namespace 中, 配置该项目的 tpl 变量(html 模版)及其他变量参数(如引用js的版本 version、api 调用的 baseUrl gateway_url 等)。

简单的 tpl 模版示例如下:

html 复制代码
<!-- version:{{version}} -->
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8"/>
    <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
    <meta name="viewport" content="width=device-width,initial-scale=1"/>
    <link rel="icon" href="/favicon.ico"/>
    <title>XX后台</title>
    <script>
          const env = '{{env}}';
          /** window.appConfig 可在js逻辑中直接取用 */
          window.appConfig = {
            env,
            gateway:'{{gateway_url}}',
          };
    </script>
    <link href="https://xxx.xxx.com/{{version}}/main.css" rel="stylesheet">
    <script defer="defer" src="https://xxx.xxx.com/{{version}}/main.js"></script>
</head>
<body>
    <div id="root"></div>
</body>
</html>

该 namespace 下其他变量分别为:envversiongateway_url

这里的原理是,可以用 nunjucks(Nunjucks) 模版引擎,将 tpl 和其他变量组合起来,生成新的 html。

这里有个拓展项,如果该项目引用了多个微前端项目,也可以将这些微前端 js 的 url,配置在 tpl 上,并仿照 version 变量,将微前端所用的 js 版本的变量抽出,实现项目对微前端引用版本的管理。

构建包含 html 和静态文件资源的 docker 镜像

如果需要发布最新构建版本的 js / css,可以先获取构建上传后的唯一标识 ${项目ID}/${版本号},并将 version 修改为该值。

发布时,执行顺序如下:

1)生成 html ,并放在 nginx 的 docker 容器里

比如需要引入最近构建版本的 js / css,可以先获取上一步构建上传后的唯一标识 ${项目ID}/${版本号},并将 version 修改为该值。

2)复制静态目录下的文件,放在 nginx 的 docker 容器里

针对一些必须放在域名根目录下的文件,如微信授权相关验证文件,是不能和 js / css 一样,放在 cdn 域名里处理的。

所以也需要放在 docker 容器里,这样就可以从域名根目录下读取到相应资源。

3)镜像构建完成,上传 Docker 仓库

传统的部署方式是,将静态资源上传到服务器上,直接用 nginx 代理,但这种方案已经过与简陋了。

这里之所以使用容器化部署,是可以基于 k8s + istio 进行灰度发布、流量治理及多环境部署。

比如要支持多环境,可以在配置中心的 app id 下,新建多个环境的 namespace,这样就可以在多个环境,构建发布不同的版本的 docker 镜像,并同时对进行多个版本的项目进行测试。

性能差异测试

这里主要针对云存储(CDN)域名的增加,带来的访问性能差异。

按照改造前方案,构建产物 html / js / css / static目录下的静态资源,都会打进 docker 镜像,共用一个域名。

但改造后, 通用制品 js / css 会上传至云存储的 CDN 域名,所以用户访问时,会多一个域名,多一步 DNS解析 + TCP 链接的时间,这个国内访问约多消耗 150ms 左右,国外访问约多消耗 350ms 左右。

但同时经我们测试,同样的静态资源,放在业务域名下并开启 CDN + 全球加速(会回源到 docker 镜像内),与放在云存储域名下并开启 CDN + 全球加速(会回源到云存储空间),下载速度还是有略微差异:

放在云存储域名下会略快一些(1m资源大约快60ms~200ms左右),猜测是回源时到docker镜像有带宽限制(购买的带宽是多少,就是多少),而回源至云存储空间无带宽限制。

所以静态资源放云存储空间会带来更快的下载速度,抵消了一部分增加新的 CDN 域名带来的连接耗时,付出这部分性能成本也是值得的。

四、总结及后续优化方向

该方案已经在公司稳定运行了一段时间,通过本方案,达成总构建发布时间 = 单次构建时间 * 1 + 单次发布时间(通常 < 15s) * 环境个数,又因为该方案将构建和发布解耦,在 SPA 部署时,带来了比较可观的时间收益:

  • 方案上线前,单个 SPA 在单个环境,部署时间约为4~10min;方案上线后,时间缩短至 2 min 以内,部署时间也不会受到应用本身大小限制。
  • 生产回滚时间可以做到 1min 内回滚,只需回滚上一次发布的镜像即可。
  • 假设公司一天在 dev / test / pre / prod 4个环境发布 100 个项目,平均每个项目发布时间按 6min 算,方案上线后,2min 即可实现发布,可以节约开发、测试小伙伴们约 7 个小时,基本相当于节省了一个人力。

同时,在实践过程中,我们也发现了一些有待改进的点:

  • 针对提升单次构建时间,可以在运维层面加入构建缓存或者尝试远程构建缓存,进一步缩短单次构建时间。
  • SPA 敏捷部署架构方案中的操作,涉及构建CI、注册中心、发布系统等,均在不同的操作界面上,割裂感比较严重,可以集成在一个系统进行处理。

这些将在后续的工作中逐渐调试和完善。

作者

马宗皓 (飞书深诺架构与平台技术,资深前端研发工程师)

相关推荐
学习ing小白1 小时前
JavaWeb - 5 - 前端工程化
前端·elementui·vue
真的很上进1 小时前
【Git必看系列】—— Git巨好用的神器之git stash篇
java·前端·javascript·数据结构·git·react.js
胖虎哥er1 小时前
Html&Css 基础总结(基础好了才是最能打的)三
前端·css·html
qq_278063712 小时前
css scrollbar-width: none 隐藏默认滚动条
开发语言·前端·javascript
.ccl2 小时前
web开发 之 HTML、CSS、JavaScript、以及JavaScript的高级框架Vue(学习版2)
前端·javascript·vue.js
小徐不会写代码2 小时前
vue 实现tab菜单切换
前端·javascript·vue.js
2301_765347542 小时前
Vue3 Day7-全局组件、指令以及pinia
前端·javascript·vue.js
ch_s_t2 小时前
新峰商城之分类三级联动实现
前端·html
辛-夷2 小时前
VUE面试题(单页应用及其首屏加载速度慢的问题)
前端·javascript·vue.js
田哥coder2 小时前
充电桩项目:前端实现
前端