为了账号安全,请及时绑定邮箱和手机立即绑定

2021 年当我们聊前端部署时,我们在聊什么

标签:
架构

先上灵魂拷问

在文章之前先抛一些灵魂拷问

  • 前端代码从 tsx/jsx 到部署上线被用户访问中间大致会经历哪些过程

  • 上述过程中分别都有哪些考虑、指标和优化点以满足复杂的业务需求

  • 可能大部分同学都知道强缓存/协商缓存那前端各种产物HTML、JS、CSS、IMAGES 等应该用什么缓存策略以及为什么

    • 若使用协商缓存但静态资源却不频繁更新如何避免协商过程的请求浪费

    • 若使用强缓存那静态资源如何更新

  • 配套的前端静态资源应该如何组织

  • 配套的自动化构建 & 部署过程如何与 CDN 结合

  • 如何避免前端上线影响未刷新页面的用户

  • 刚上线的版本发现有阻塞性 bug如何做到秒级回滚而非再次部署等 20 分钟甚至更久

  • 如何实现一个预发环境除了前端资源外都是线上环境将变量控制前端环境内

  • 部署环节如何方便配套做 AB 测试等

  • 如何实现一套前端代码发布成多套环境产物

  • 如何实现按 feature 发布产物供用户使用并逐步扩大 feature 灰度将影响减到最小即线上同时存在多 feature 产物

  • CDN 域名突然挂了如何实现秒级 CDN 降级修补而非再次全部业务重新部署一次

本文将会带着这些问题试着一起探索在2021年系统化的前端部署解决方案。

静态资源组织

一个简单的页面

先从简单的静态页面开始众所周知前端资源由 HTMLJavaScriptCSS 三剑客组成假设我们有一个简单的页面用Nginx作为 Web 服务器资源组织结构大概如下

https://img2.sycdn.imooc.com/61646bc00001c20a34011563.jpg

此时 只需将 HTMLJavaScriptCSS 等静态资源通过 FTP 等软件上传到 Web 服务器如 Nginx某目录将 Nginx 启动做简单配置即可让用户访问。

https://img3.sycdn.imooc.com/61646bc10001209b07560354.jpg

用户一访问状态 200页面渲染出来前端十分简单对不对

利用缓存

但仔细观察用户每次访问都会请求 foo.css, bar.css 等静态文件即使该文件并无变更。对带宽甚是浪费对页面首屏性能等也有影响。于是在网络带宽紧张的互联网早期计算机先贤们在 HTTP 协议上制定了多种缓存策略。

浏览器缓存浏览器缓存(Brower Caching)是浏览器对之前请求过的文件进行缓存以便下一次访问时重复使用节省带宽提高访问速度降低服务器压力。

协商缓存

一种策略是浏览器先问问服务器有没有变化没变化就用旧资源。毕竟"问一问"的通信成本远小于每次重新加载资源的成本。大致流程如下

协商缓存: 向服务器发送请求服务器会根据这个请求的 Request Header 的一些参数来判断是否命中协商缓存如果命中则返回 304 状态码并带上新的 Response Header 通知浏览器从缓存中读取资源

https://img1.sycdn.imooc.com/61646bc1000120d415140928.jpg

此时使用协商缓存后Network 大致变成了这样

https://img4.sycdn.imooc.com/61646bc2000174e307580346.jpg

注协商缓存一般可在服务端通过设置 Last-ModifedETagResponseHeader 实现。
304 状态码表示资源未发生变更可使用浏览器缓存。

强缓存

这样通过协商缓存我们大幅优化了资源未变更时的网络请求节约大量带宽网站首屏性能也有不错的提升美滋滋
然而仔细观察发现仍然有协商的过程一百个静态文件就有一百个协商请求。在资源未发生变更时追求极致的我们也应该优化掉这个协商请求毕竟没有买卖就没有伤害
和协商缓存对应的是使用强缓存大概过程如下

强缓存浏览器不会向服务器发送任何请求直接从本地缓存中读取文件并返回Status Code: 200 OK

此时强缓存的大致对话过程如图

https://img1.sycdn.imooc.com/61646bc20001cc2611420876.jpg

注意缓存生效期间浏览器是【自言自语】和服务器无关。

此时设置强缓存后Network 大致变成了这样

https://img4.sycdn.imooc.com/61646bc20001e2a009600334.jpg

From DiskCache从硬盘中读取。
From MemoryCache从内存中读取速度最快。
注强缓存一般可在服务端通过设置 Cache-Control:max-ageExpiresResponseHeader 实现。

用上强缓存后协商的请求也被消灭了网站加载的性能达到极致了。美滋滋

附录协商缓存和强缓存详解

注校招生或客户端转前端同学关于强缓存/协商缓存的实现及使用先了解即可。
后续再熟练掌握。

缓存更新问题

鉴于页面index.html会频繁更新而静态资源则相对稳定。所以我们能推断出的一种缓存策略是 index.html 适合走协商缓存相对稳定 & 不常更新的静态资源JSCSSIMAGES 等应该消灭协商请求使用强缓存。
然而问题很快就来了都不让浏览器发请求但缓存还未到期我们发现有 bug想更新 foo.css 怎么办
又想设置尽量长的时间走缓存又想要能随时更新
又想马儿跑又不给马儿吃草

https://img1.sycdn.imooc.com/61646bc20001b7b506400398.jpg相信大家很快就能得出一种思路给资源加版本号比如通过 query 加版本号每次上线统一改版本号就搞定了。此时 HTML 变成如图

https://img1.sycdn.imooc.com/61646bc2000114f518041576.jpg

注意此时服务器内只有一份文件 foo.css 文件。

统一加版本号的优点是简单粗暴快捷但缺点则是假如我们只想更新 foo.cssbar.css 缓存也失效了又造成了带宽的浪费。
大家应该很快就能想到办法需要将文件内容与版本号URL绑定当文件内容发生变更时才变更版本号URL这样就能实现每个文件精确的缓存控制。
什么东西与文件内容相关呢 消息摘要算法 对文件求摘要信息摘要信息与文件内容一一对应就有了一种可以精确到单个文件粒度的缓存控制依据。现在我们把 URL 改成带文件摘要信息的

https://img1.sycdn.imooc.com/61646bc3000158ea12801015.jpg我们可以称这种这个方式为 query-hash后续发版上线时只有被变更文件的 URL 会更新实现了精确的缓存控制完美

注意此时服务器内只有一份文件 foo.css 文件。

覆盖式发布引发的问题

然而假如我们就按上述部署方案就上了线很快就会 Fatal 满天飞每次更新上线都可能会出现灾难。
我们回顾一下网站的静态文件只有一份部署在 Nginx 服务器某目录下并且通过 query-hash 的方式实现按文件做精确缓存控制问题出在哪了呢
回顾一下我们某次更新时更改了 foo.css 样式此时会将 HTML 中的foo.css url更新为最新的 hash并将服务器中存储的 foo.css & index.html 文件覆盖为最新V2版本看似HTML和静态资源都对应更新了但是没有考虑极端情况。那就是

  1. 先部署静态资源部署期间访问时会出现V1版本HTML访问到V2版本新静态资源并按V1-hash缓存起来。

  2. 先部署HTML部署期间访问时会出现V2版本HTML访问到V1版本旧静态资源并按V2-hash缓存起来。

如下图所示展示了不同版本HTML与不同版本静态资源互相匹配到出现的异常Case。

https://img1.sycdn.imooc.com/61646bc30001873012800447.jpg

绿色走向正常访问并建立缓存的路径。
红色走向先部署静态资源V2V1-HTML访问V2静态资源并缓存Case
黑色走向先部署HTMLV2V2-HTML访问V1资源并缓存Case

对于问题1会有两种子Case

  1. 用户本地有缓存此时无影响可正常访问。

  2. 用户本地无缓存则会将V2版本静态资源加载并按V1版本 hash 缓存起来。用户报错。当V2版本HTML部署完成后用户再次访问时恢复。

对于问题2则会出现严重的Case
V2 版本HTML会将V1版本静态资源按V2版本Hash缓存起来。此时页面会出错且缓存过期之前会持续报错。直到用户手动清除缓存或者缓存过期或者将来发布V3版本更新静态资源版本。否则用户会持续出错。
上面方案的问题起源于静态资源只有一份每次发布时都是覆盖式发布导致页面与静态资源出现匹配错误的情况解决问题方案也极其简单使用非覆盖式发布一种简单的改造方式是将文件摘要hash放置到URL 中即将 query-hash 改为 name-hash
此时 HTML 变成如图https://img1.sycdn.imooc.com/61646bc700019df112801175.jpg

这样每次部署时先全量部署静态资源再灰度部署页面就能比较完美的解决了缓存的问题。

此时服务器上会存在多份 foo.[$hash].css 文件

与 CDN 结合

现在我们开开心心将网站部署上线了但我们此时仍然将静态资源部署在 Nginx 服务器目录下然后新的问题来了随着时间推移非覆盖部署导致文件逐渐增加多硬盘逐渐吃紧。而且将文件存储在 Nginx Web服务器内某目录下深度的将 Nginx、网站、部署过程等强耦合在一起无法使用 CDN 技术。

CDN 是一种内容分发网络部署在应用层利用智能分配技术根据用户访问的地点按照就近访问的原则分配到多个节点来实现多点负载均衡。
简单来说用户就近访问访问速度更快大公司也无需搞一台超级带宽的存储服务器只需使用多台正常带宽的 CDN 节点即可。
而 CDN 的常见实现是有一台源站服务器多个 CDN 节点定时从源站同步。

那如何将 CDN 与 Nginx 等 Web 服务器结合呢
答案是将静态资源部署到 CDN 上再将 Nginx 上的流量转发到 CDN 上这种技术我们称之为『反向代理』。
此时用户访问时流量走向 & 研发构建部署过程大致如下

https://img1.sycdn.imooc.com/61646bca0001041712800645.jpg此时我们总体部署方案需要进一步做三步改造。

  1. 构建时依据环境变量将 HTML 中的静态资源地址加上 CDN 域名。

  2. 构建完成后将静态资源上传到 CDN

  3. 配置 Nginx 的反向代理将静态资源流量转发到 CDN

其中第 1、2 条涉及构建过程调整以 Webpack 为例我们需要做以下配置改造

a.  配置 `output` 为 `content-hash` & `publicPath`  
b.  配置 `Webpack-HTML-Plugin`复制代码

下面是一个配置示例

https://img2.sycdn.imooc.com/61646bca0001577112800802.jpg

// webpack.config.js
const CDN_HOST = process.env.CDN_HOST;// CDN 域名
const CDN_PATH = process.env.CDN_PATH; // CDN 路径
const ENV = process.env.ENV; // 当前的环境等等
const VERSION = process.env.VERSION; // 当前发布的版本

const getPublicPath = () => {
    // Some code here
    return `${CDN_HOST}/${CDN_PATH}/${ENV}/`;// 依据 ENV 等动态构造 publicPath
}

const publicPath = process.env.NODE_ENV === 'production' ? getPublicPath() : '.';

module.exports = {
    output: {
        filename: 'bundle.[name][contenthash:8].js',
        publicPath,
    },
    plugins: [
        new HtmlWebpackPlugin()
    ]
}

备注1我们往往会将一套代码部署到多套前端环境还需要在构建时注入当前部署相关环境变量如 stagingproddevpre等以便动态构建 publicPath
备注 2这里动态构造的 publicPath 里严格的将产物按环境 + 发布版本做了隔离 & 收敛。 某业务前端曾将所有环境的静态资源放到一起以Hash做区分。但疑似出现了文件名 + hash 冲突但文件内容不一样导致了线上事故。故墙裂建议严格对产物做物理隔离。
备注 3publicPath 详解webpack.docschina.org/configurati…
备注 4此处使用了 content-hashhashchunkhash 的区别请见详解webpack中的hash、chunkhash、contenthash区别
备注 5使用 contenthash 时往往会增加一个小模块后整体文件的 hash 都发生变化原因为Webpackmodule.id 默认基于解析顺序自增从而引发缓存失效。具体可通过设置 optimization.moduleIds 设置为 'deterministic'
具体详见 webpack 官方文档-缓存
注关于 Webpack 的配置校招生或客户端转前端同学前期了解即可后续建议深入学习。

  1. 构建完成后静态资源上传 CDN 源站

上传 CDN 源站往往通过 CLI 调用各种客户端工具上传此时要注意的是上传 CDN 依赖配置鉴权信息如 文件存储的 Bucket Name/accessKey、ftp的账号密码。这些鉴权信息不能直接写代码里否则可能会有事故风险想想为什么
第 3 步改造是 Nginx 层反向代理改造

反向代理reverse proxy是指以代理服务器来接受网络请求并将请求转发给内部的服务器并且将内部服务器的返回就像是二房东一样。
一句话解释反向代理 & 正向代理反向代理隐藏了真正的服务器正向代理隐藏了真正的客户端。
详见漫话如何给女朋友解释什么是反向代理

Nginx 可通过设置 proxy_pass 配置代理转发如

location ^~/static/ {
    proxy_pass $cdn;
}复制代码

具体详见 nginx 之 proxy_pass详解

注校招生或客户端转前端同学前期了解即可后续建议熟悉 ~ 掌握。

静态资源组织总结

最后回顾一下

  1. 为了最大程度利用缓存将页面入口(HTML)设置为协商缓存将 JavaScriptCSS 等静态资源设置为永久强缓存。

  2. 为了解决强缓存更新问题将文件摘要hash作为资源路径(URL)构成的一部分。

  3. 为了解决覆盖式发布引发的问题采用 name-hash 而非 query-hash 的组织方式具体需要配置 Wbpackoutput.filenamecontenthash

  4. 为了解决 Nginx 目录存储过大 + 结合 CDN 提升访问速度采用了 Nginx 反向代理+ 将静态资源上传到 CDN

  5. 为了上传 CDN我们需要按环境动态构造 publicPath + 按环境构造 CDN 上传目录并上传。

  6. 为了动态构造 publicPath 并且随构建过程插入到 HTML 中采用 Webpack-HTML-Plugin 等插件将编译好的带 hash + publicPath 的静态资源插入到 HTML 中。

  7. 为了保证上传 CDN 的安全我们需要一种机制管控上传 CDN 秘钥而非简单的将秘钥写到代码 / Dockerfile 等明文文件中。

简直是层层套娃

此时我们已经基本获得了一套相对完备的前端静态资源组织方案。

此时你可能已经发现了前端静态资源部署后还有被 Nginx 加工消费过程才能被用户访问到。

自动化构建

现在我们已经探索出一套静态资源组织的解决方案。现在探讨一下构建的过程。我们每次构建时大约需要进行这些步骤

  • 拉取远程仓库

  • 切换到 XX 分支

  • 代码安全检查非必选、单元测试等等

  • 安装 npm/yarn 依赖

    • 设置 node 版本

    • 设置 npm/yarn

    • 安装依赖等

  • 执行编译 & 构建

  • 产物检查比如检测打包后 JS 文件 / 图片大小、产物是否安全等保证产物质量非必选

  • 人工卡点非必选如必须 Leader 审批通过才能继续

  • 打包上传 CDN

  • 自动化测试非必选e2e

  • 配套剩余其他步骤

  • 通知构建完成

这其中迎面而来的问题有

  • 在什么环境执行构建

  • 如何保证每次构建部署环境相同

  • 由谁触发构建

  • 如何管理前面所述上传 CDN 等密钥不增加成本、保证安全、保证构建上传可靠性

  • 如何自动化触发构建 & 自动化执行上述步骤

假如每次都由人工执行估计发版日就守着编译打包了而且较为容易引发问题比如某步骤遗漏或顺序错了。

  • 如何提升构建速率

  • 构建完成如何通知研发同学构建完成了

灵魂拷问有没有

为了解决上面问题业界有一些解决方案

  • 保证环境一致性Docker

  • 按流程构建Jenkins

  • 自动化构建触发Gitlab webhook 通知

  • 开始构建通知依赖账号体系打通+ Gitlab Webhook

  • 构建完成通知依赖账号体系打通

业界的大致实现一般都为 Jenkins + Docker + GitlabWebHook比如下面是一些实践

前端项目自动化部署——超详细教程Jenkins、Github Actions
iDeploy-为前端团队构建部署工程化而开发的一个持续交付平台

此时还有一些其他问题
比如宇宙最重物质 node_modules 安装速度过慢的问题
如何提升 Build 构建速
上述往往在各大公司都有相对完善的构建系统 & 解决方案等各公司各不相同但大致类似故本文跳过该步骤。

前端发布服务 - 预发环境、版本管理秒级回滚、小流量、灰度、AB测试

假定我们静态资源组织完成也搞定了自动化构建部署也配好了 Nginx 的反向代理我们的网站终于第一次上线了。
但第二次第三次上线怎么办直接发到生产环境做回归测试的风险极大但又不能本地部署前端测试环境去连接后端生产库可以想想为什么所以我们需要一个预发Pre环境除了非测试人员访问不到之外其他所有环节都和生产环境保持一致
此时遇到第一个需求预发环境功能
假如我们某个功能是元旦零点发布跨年时守在服务器面前点发布万一 npm 抽风拉取依赖失败导致构建失败或者上线后发现有bug那就只能凉凉。
或者随着时间推移大家前端项目越积越大node_modules 质量逐渐超越银河系总质量构建的时间往往会超过二十分钟甚至更久。某天某次我们新上线了功能后却发现有致命阻塞性 bug收款后自动退款 1.5 倍想立即回滚版本那就且等着大眼瞪小眼的等它慢慢编译吧。这个时候才真的是时间就是金钱再编译慢点公司就破产啦。
此时有没有一种办法能在发现问题后立即将版本回滚呢并且这个回滚操作回滚的同学也不应该登陆服务器去做操作想想为什么。
此时遇到第二个需求版本管理功能。 即可提前将静态资源上线也需要保留每个历史版本并且能实现瞬间切换版本且切换过程不应该登陆服务器操作想想为什么。
其次是假定 PM 对功能不断优化想先灰度一部分用户或者想做一些 AB 测试比如给广东用户推广福建美食给重庆用户推广钵钵鸡。
此时我们有两种方案方案一是将把钵钵鸡和福建美食都打包到一份代码产物里再在运行时根据地域做切换。但很快你的代码产物里就有钵钵鸡冷锅串串热锅串串老妈兔头跷脚牛肉狼牙土豆以及福建美食等等会串味儿的对不对况且热锅串串和冷锅串串打包混到一起我就第一个不同意简直是对美食的亵渎所以方案一不可取。

实际上现实中往往会热锅串串冷锅串串这样完全不兼容的两份改动同时在线上运行做 AB 测试。

方案二是我们将热锅串串和冷锅串串分开打包让热锅不犯冷锅。再设计一些机制比如携带了香蕉糖果cookie的同学给跷脚牛肉锅讲港东话的同学福建美食锅四川地域的同学随机给火锅干锅汤锅鱼火锅。岂不乐哉
大家应该很容易发现这种机制是极其多变的大概率朝令夕改。难道我们每次想调整干锅、鱼火锅的比例就要登陆服务器做调整某天干锅卖完了但又没带电脑回家怎么办
此时遇到第三个需求随时调整的小流量测试、AB-Test测试、灰度上线等等功能
总结一下为了满足复杂的线上需求在部署层面总体来说需要预发环境、版本管理、小流量、灰度、AB测试等功能。

静态资源的加工

如前所述前端静态资源部署到 CDN 后有一道 Nginx 反向代理做转发的加工工序。事实上为了解决各种部署问题或为了提升性能人们往往而需要对静态资源做更多的加工工序。
比如部分 Web 应用为了提升首屏性能一种常见的方式为通过 BFF 层或通过后端直出 HTML并且在过程中注入若干信息如 userInfo、用户权限信息、灰度信息等等从而大幅降低前端登陆研发成本 & 降低首屏耗时。
下面是后端直出 HTML 的一种简要流程。

https://img1.sycdn.imooc.com/61646bed0001a96440562649.jpg

主要流程为前端构建出的 HTML 包含若干模板变量后端收到请求后通过各种 Proxy 层将 Cookie 转换成用户信息再按依据版本配置从 CDN 加载 index.html, 并使用模板引擎等方式将模板变量替换为用户信息最终吐回给浏览器的则是已经包含用户信息的 HTML

Pre 环境、灰度上线的常见实现

如前所述我们的静态资源为非覆盖式发布多次部署后线上存在若干版本静态资源。实现Pre环境/灰度上线的思路则是通过一定的机制让特定用户访问特定静态资源版本从而达到访问Pre/灰度上线的能力。

方案一 Nginx 层动态转发

一种常见的 Pre 机制是静态资源部署多个版本后开发者的通过 ModHeader 等浏览器插件在请求中携带特定 Header如xx-env=pre在 Nginx 层消费该 Header 并动态转发到对应环境的静态资源上实现访问 Pre 环境目的。此时除静态资源为特定版本外所有环境都是生产环境可以将变量范围控制在最小。

流程大致如图

https://img2.sycdn.imooc.com/61646bf1000167bd32982203.jpg

Nginx 可通过配置 rewrite 设置转发如下所示。

详情请查阅nginx配置rewrite指令详解

location /example {
    rewrite ^ $cdn/$http_x_xx_env/index.html break;
    proxy_pass $cdn/prod/index.html;
}

# $http_x_xx_env 表示取自定义的 Request Header 字段 xx_env复制代码

注对于Nginx校招生或客户端转前端同学前期了解即可后续建议熟悉 ~ 掌握。

该方案优点为配置简单高效适用于工程师。
缺点为每个用户都需要手动配置不适用于移动端且无法让特定用户被动精确访问某版本比如 PM、KP 用户来配置 Header 成本过高。
同理也可以在 Nginx 层按一些其他规则处理实现灰度上线的能力。
如通过一定随机数 rewrite达到小范围随机灰度。
获取 ua 并 rewrite达到按浏览器定向灰度。
通过 Nginx GeoIP 获取地域信息达到按地域灰度。
但上述灰度方案配置复杂而灰度比例 / 范围往往会配置较多每次上线都需要运维登陆生产服务器修改较容易出各种事故。故不推荐使用仅供拓宽思路。

方案二 动态配置 + 服务端转发

但 Pre 环境或灰度往往需要精确定位某些特定人群如给特定PM、HR、远端报错的特定用户、KP用户 甚至给某个部门开 Pre环境等。上述同学工程背景相对缺失 / 较忙 / 通过移动端访问此时通过修改 Header 的方式不再适用。故我们仍然要寻找某种机制达到能方便随时调整 Pre/ 灰度范围又不用重新发版上线。既然需要按用户维度来定向此时就依赖后端帮忙处理了。

而为了能随时随地调整灰度 / Pre 策略而非依赖调整代码发版上线此时引入配置中心的概念。

配置中心一般是独立的平台 / SDK提供动态配置管理的解决方案提供功能有配置管理、版本管理、权限管理、灰度发布等等。后端应用通过接口消费故配置中心和后端解耦可以随时修改调整配置而非重新发版。 配置中心一般是配置一个 JSON 对象。 配置中心JSON对象人工维护容易引发问题故增加机器人来降低出错几率。

下图是依赖配置中心 + 服务端转发的流程图

https://img1.sycdn.imooc.com/61646bf40001f78a12800804.jpg

主要流程为

  1. 前端攻城狮同学部署多个版本静态资源到 CDN 上问题如何管控多版本静态资源。

  2. 后端收到请求后通过各种 Proxy 层将 Cookie 转换成用户信息。

  3. 后端读取配置中心数据依据用户信息判断给用户访问什么环境加载具体环境 index.html

  4. 后端返回给浏览器加工后的 index.html

  5. 若需添加具体 KP 等同学到 Pre 名单攻城狮同学只需调用机器人/Bot 等修改配置中心即可生效。

注意在上述架构下若线上某用户发生某些难以排查的问题也可发布特定的版本在配置中心修改后让用户访问特定版本页面从而简化排查问题的过程。

此时一些小流量配置AB实验版本管理其实也可以通过该方案实施。
该方案优点可以随时调整不用后端发版移动端也可生效。
该方案缺点

  1. 和服务端强绑定要求用户信息在所难免。

  2. 每次都需要从 CDN 加载 HTML 有一定性能浪费。但若缓存 HTML发版环节还要通知服务端总体增加复杂度。

  3. 若考虑 CDN 故障服务端做 CDN 降级会增加复杂度。

  4. 版本管理 / 小流量等为通用需求而该方案每个后端应用都需要开发或接入。

  5. 常见的配置中心又一般为 JSON 配置比较简陋和发版的多环境无法关联依赖人为配置有出错的风险如发版 v1.2501配置中心手动配置时手误改成了v1.2051

前端发布服务实现与设计

可能部分同学对线上产物实行版本管理会误理解对代码增加版本管理如发版后手动 / 自动打Tag后续需要时再次发版部署即可满足需求。但如前所述通过源码做版本管理灵活性较差无法做到一键 &秒级切换版本不满足商业化环境多变 & 复杂的需要。
那么如何进行版本管理呢答案是对构建产物进行深层次加工 & 管理。
与此同时版本管理/小流量是前端部署的常见公共业务需求应该和业务后端服务脱离故这里提出一个新的公共服务纯用于前端部署相关此处将之称为 Page Server用于具体的 index.html 文件管理 & 承接 Nginx 流量或业务后端流量等。
同时鉴于版本管理、小流量策略等调整会特别频繁每次调整不应该都登录服务器故我们需要一个新的服务 & 界面用于操作管理版本、调整小流量等信息并且与上述 Page Server 同步此处我们将该服务称之为 Page Config Web
而我们的 Page Server 则可能会有很多个实例部署在多个集群上以满足跨国部署、多部门项目部署等要求。所以理想情况下 Page Config Web ****还要承接 PageServer 的创建、管理、配置等工作。所以 PageConfigWebPageServer 是 1 比 N 关系或M比N用于跨国部署等。
同时我们一个前端项目可能有多套前端环境PageSever 在固定集群算公共设施这些环境理论上都可以由一个或多个 PageServer 承载。故一个 PageServer 和多个前端环境是 1 比 1 或者 1 比 N 关系。
此时对于 Nginx 来的流量我们需要一种机制来区分该流量属于哪个环境实例比如通过 URL 来区分我们可以称之为 路由
最后为了保证上述服务的正确性和自动化构建部署新增版本完成后要同步到上述两个服务以确保版本管理的正确性。

最后大致的流程图如下

超大图预警

https://img4.sycdn.imooc.com/61646bf5000170a645484700.jpg

本质上来说相当于有一个公用的中间服务部署在多个集群上与构建发布过程深度绑定用于承接HTML 的流量并通过 Web 站点设置小流量规则、版本等等来满足多变的上线需求。
其中PageServer 在承载 HTML 服务时可做一些其他工作比如

  1. SSR

  2. CDN 降级用于 CDN 异常时直出 HTML 中将静态资源替换为可用的 CDN 站点。

  3. 404 处理

  4. 兜底页比如服务出现故障短时间内无法修复时出兜底

  5. 模板渲染如做模板替换将 query 替换到模板中等

  6. 特殊时期全局处理如注入全局样式将页面全局置灰

等等等等。

PageConfig Web 和 PageServer 中有构建后的所有版本信息理论上可以缓存每个版本的 HTML文件并且为了优化性能PageServer 中可将最新全量版本的 HTML 文件缓存到内存中最大程度提升响应速度其余版本存储到 Redis 等缓存中。

下面以发布一个正式版本 v.1.0.2502 并且回滚过程为例

  1. 代码合并触发自动化构建构建产物以环境env+版本env + 版本env+版本version + name-hash 方式组织并上传到 CDN。

  2. 构建完成后构建脚本通知攻城狮同学、同步 PageServer、PageConfig Web 服务有新版本 v.1.0.2502 。

  3. 攻城狮同学收到通知后到 PageConfig Web 站点发布新版本 v.1.0.2502 PRE并为该版本配置 PRE 环境小流量规则xx-env = pre。此时只有设置特定 Header 才能访问该版本。

  4. 若是 Nginx 直接转发则攻城狮通过设置 Header 访问 PRE 版本。

  5. 若是通过服务端转发攻城狮通过配置中心设置 PRE 白名单即可让用户访问 PRE 版本。

  6. 在 PRE 版本验收完成后攻城狮登录 PageConfig Web 站点发布正式版本 v.1.0.2502 不带小流量信息。此时立即生效。

  7. 生效后线上回归发现有 bug攻城狮立马登录 PageConfig Web 站点将版本回滚为上一版本v.1.0.2501 。此时立即生效。

关于部署的总结

静态资源组织部分

  1. 为了最大程度利用缓存将页面(HTML)设置为协商缓存将 JavaScript、CSS 等设置为永久强缓存。

  2. 为了解决强缓存更新问题将文件摘要hash作为资源路径(URL)构成的一部分。

  3. 为了解决覆盖式发布引发的问题采用 name-hash 而非 query-hash 的组织方式具体需要配置 webpack 的 output.filename 为 contenthash 方式。

  4. 为了解决 Nginx 目录存储过大 + 结合 CDN 提升访问速度采用了 Nginx 反向代理+ 将静态资源上传到 CDN。

  5. 为了上传 CDN我们需要按环境动态构造 publicPath + 按环境构造 CDN 上传目录并上传。

  6. 为了动态构造 publicPath 并且随构建过程插入到 HTML 中采用 Webpack-HTML-Plugin 等插件将编译好的带 hash + publicPath 的静态资源插入到 HTML 中。

  7. 为了保证上传 CDN 的安全我们需要一种机制管控上传 CDN 秘钥而非简单的将秘钥写到代码 / Dockerfile 等明文文件中。

自动化部署部分

为了提升部署效率100% 避免因部署出错需要设计 & 搭建自动化部署平台以 Docker 等保证环境的一致性以 Jenkins 等保证构建流程的串联。使用es-build等提升构建效率。

前端部署 & 静态资源加工

关于前端部署能总结出下面几个原则/要求

  1. 构建发布后不应该被覆盖。

  2. 构建发布后静态资源应当永久保存在服务器/CDN 上即只可读。

  3. 静态资源组织上每个版本应该按文件夹存储做到资源收敛。这样假如真要删除时可按版本删除。如某个版本代码泄密

// webpack.config.js
const CDN_HOST = process.env.CDN_HOST;// CDN 域名
const CDN_PATH = process.env.CDN_PATH''; // CDN 路径
const ENV = process.env.ENV; // 当前的环境等等
const VERSION = process.env.VERSION; // 当前发布的版本

const getPublicPath = () => {
    // Some code here
    return `${CDN_HOST}/${CDN_PATH}/${ENV}/${VERSION}/`;// 依据 ENV 等动态构造 publicPath
}

module.exports = {
    output: {
        filename: 'bundle.[name][contenthash].js',
        publicPath: getPublicPath(),
    },
    plugins: [
        new HtmlWebpackPlugin()
    ]
}复制代码

故 publicPath 应增加 version 字段

  1. 发布过程应该自动化开发人员不应该直接接触服务器。

  2. 版本切换时也应当不接触服务器。

  3. 版本切换能秒级生效。如 v0.2 切换 v0.3立即生效。

  4. 线上需要能同时生效多个版本满足 AB 测试、灰度、PRE 环境等小流量需求。

上述需求都相对复杂多变为了应对复杂的线上需求可以对静态资源做深度加工如通过服务端直出 HTML、通过配置中心实现按用户 PRE 等等。

前端发布服务

面对复杂的商业化需求方便多前端业务实现版本管理、灰度、PRE、AB 测试等小流量功能我们设计了一个中间服务 PageConfig Web & PageServer与 Nginx 和各种后端相结合达到配置即时生效的能力。

灵魂拷问的部分答案

Q: 前端代码从 tsx/jsx 到部署上线被用户访问中间大致会经历哪些过程
A: 经历本地开发、远程构建打包部署、安全检查、上传CDN、Nginx做流量转发、对静态资源做若干加工处理等过程。
Q可能大部分同学都知道强缓存/协商缓存那前端各种产物HTML、JS、CSS、IMAGES 等应该用什么缓存策略以及为什么

  • 若使用协商缓存但静态资源却不频繁更新如何避免协商过程的请求浪费

  • 若使用强缓存那静态资源如何更新

AHTML使用协商缓存静态资源使用强缓存使用name-hash非覆盖式发布解决静态资源更新问题。
Q配套的前端静态资源应该如何组织
A搭配 Webpack 的Webpack_HTML-Plugin & 配置 output publicPath等。
Q配套的自动化构建 & 部署过程如何与 CDN 结合
A自动化构建打包后将产物传输到对应环境 URL 的CDN上。
Q如何避免前端上线影响未刷新页面的用户
A使用name-hash方式组织静态资源先上线静态资源再上线HTML。
Q刚上线的版本发现有阻塞性 bug如何做到秒级回滚而非再次部署等 20 分钟甚至更久
AHTML文件使用非覆盖方式存储在CDN上搭建前端发布服务对 HTML 按版本等做缓存加工处理。当需要回滚时更改发布服务HTMl指向即可。
Q: CDN 域名突然挂了如何实现秒级 CDN 降级修补而非再次全部业务重新部署一次
A1: 将静态资源传输到多个 CDN 上并开发一个加载Script的SDK集成到HTML中。当发现CDN资源加载失败时逐步降级CDN域名。
A2在前端发布服务中增加HTML文本处理环节如增加CDN域名替换发生异常时在发布服务中一键设置即可。
Q如何实现一个预发环境除了前端资源外都是线上环境将变量控制前端环境内
A对静态资源做加工对HTML入口做小流量。
Q部署环节如何方便配套做 AB 测试等
A参见前端发布服务
Q如何实现一套前端代码发布成多套环境产物
A使用环境变量将当前环境、CDN、CDN_HOST、Version等注入环境变量中构建时消费 & 将产物上传不同的CDN即可。


作者字节架构前端

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
JAVA开发工程师
手记
粉丝
33
获赞与收藏
206

关注作者,订阅最新文章

阅读免费教程

  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消