写点什么

飞书深诺前端 SPA 敏捷部署架构方案

  • 2023-10-12
    上海
  • 本文字数:4178 字

    阅读完需:约 14 分钟

一、背景介绍

飞书深诺集团是专注海外数字营销解决方案的专业服务提供商,为有全球化营销需求的企业提供标准 &定制相结合的全链路服务产品,满足游戏、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 的新架构敏捷部署方案的设计如下:

全流程时序图


各步骤详解

  1. 开发 -> 构建 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,对服务器资源占用较少。

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

  3. 配置中心 -> 生成 html

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


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

简单的 tpl 模版示例如下:

<!-- 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 版本的变量抽出,实现项目对微前端引用版本的管理。


  1. 构建包含 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、注册中心、发布系统等,均在不同的操作界面上,割裂感比较严重,可以集成在一个系统进行处理。

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


作者

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

用户头像

用技术驱动营销 2020-05-21 加入

跨境数字化营销服务专家 用数字有效连接中国企业和全球消费者

评论

发布
暂无评论
飞书深诺前端 SPA 敏捷部署架构方案_前端_飞书深诺技术团队_InfoQ写作社区