前端工程质量保障体系实践

Daotin 于 2022-10-22 发布 编辑

作者:曾静益

前端工程化指使用软件工程的技术和方法将前端的开发流程、技术、工具、经验等规范化、标准化,其主要目的是提高效率和降低成本,即提高开发过程中的效率,减少不必要的工作。

No CodeLow Code技术方案。例如,对于 2020 年的“双 11”活动,淘宝内部成立了研发效率专项团队,可以通过设计稿生成代码的 D2C(Design to Code)平台 imgcook 提升了研发效率,承接了 90.4%的新模块代码的智能生成任务,代码可用率达到 79.26%,相比传统模块的开发模式,通过设计稿生成代码技术将编码效率(模块复杂度和研发耗时比)提升了 68%,固定人力单位时间模块需求吞吐量增加约 1.5 倍。

未来的方向——智能化

前端智能化”是一种新的概念和技术,当前,5G、人工智能、大数据、云计算、物联网、预测性维护、机器视觉、地理信息等技术以各种各样的形式被应用到前端领域。JavaScript 作为一门编程语言,之所以有如此蓬勃的生命力,是因为前端开发人员不断突破、创新,不断拓展前端的边界,将新兴的技术不断应用到前端。

前端工程质量保障就是以前端工程化为前提,建立一套有计划、有系统的方法。简单来说,前端工程质量保障是一套体系化的解决方案而不是某种技术,它能够覆盖前端的研发生命周期,包括工程搭建、功能研发、测试保障、发布上线、运行维护。

在进行组件建设时,应当遵循以下原则:

持续集成是一种软件开发实践。通常,开发团队的每个成员每天至少集成一次自己的工作,这意味着每天都会发生多次集成。每次集成都会通过自动化构建(包括编译、发布、自动化测试)来验证,从而尽早地发现集成错误。
持续集成的宗旨是避免集成问题,提升软件质量并缩短交付时间。

前端的系统部署起初是一个非常简单的流程——直接将静态文件提交到服务器即可。随着互联网的发展,企业为了提升用户体验,开始追求交付质量、性能及系统稳定性。例如,为了提升网站性能,把静态资源和动态网页分集群部署,静态资源会被部署到 CDN 节点上;为了控制新功能上线的影响面,做代码灰度、A/B 分流;当新功能上线出现缺陷时,要能够快速进行代码回滚。

随着系统部署需要考量的方面越来越多,单纯依赖人工进行管理的成本将非常高昂,并且非常容易出错。因此,需要借助平台化的能力来串联发布能力,将发布链路中的各个环节结合起来,由此诞生了部署平台。

借助部署平台,开发人员可以通过可视化的方式进行一键部署,直接发布任务。在整个发布过程中,平台会将各个环节的运行信息和日志返回。借助部署平台,开发人员在跟踪发布流程和排查发布问题等方面的体验和效率将大大提升。

页面监控包括性能监控、异常监控、白屏监控、卡顿监控、用户行为监控。

资源问题主要包括加载失败、加载时间过长、资源劫持等。

常见解决方案是全局监听资源加载事件,当资源加载失败时,将资源链接降级到备用资源尝试重新加载,并将对应事件上报到前端监控系统,前端开发人员和网站可靠性工程师(Site Reliability Engineer,SRE)根据报错的资源、数量、地区等,进行综合排查定位,看是个例问题还是 CDN 出现问题,最后给出对应解决方案。

PostCSS是一个用 JavaScript 实现的 CSS 转换、处理工具。它本质上是一个平台,只提供基础能力,对 CSS 的处理能力是通过插件的形式实现的。

Autoprefixer能够根据 Can I Use 解析 CSS 并且为其添加浏览器厂商前缀。

postcss-modules是 CSS 样式隔离的解决方案。

postcss-pxtorem 是 CSS 单位转换的工具,它可以将 CSS 中的 px 转换成 rem,常用于移动端的页面应用

前端部署

为了避免出现以上问题,开发人员开始选择在页面访问量处于低谷时部署系统,因为此时出错的概率比较小。访问量低谷期通常处于深夜,这将增加部署人员的负担,而且依然有可能出现问题。

按照覆盖式发布的部署流程,需要将修改后的两个文件上传到服务器,覆盖旧的文件。在上传的过程中,两个文件不可能同时被覆盖,必定存在着先后顺序,包括两种情况。

覆盖式发布存在一个没有办法避免的严重问题:当部署的代码出现问题时,服务器上的资源不能直接回滚到上一次的状态。开发人员必须在本地将代码回退到之前的状态,然后重新进行构建打包,重新上传资源才能实现回滚。整个回滚期间会耗费大量的时间和精力,并且在回滚完成之前,生产环境的故障依然存在。

非覆盖式发布有效解决了覆盖式发布的问题,在资源部署期间不会出现页面问题。同时,由于非覆盖式发布使用文件的哈希值作为名称后缀,避免了资源相互覆盖,当线上代码出现问题时可以快速进行回滚。它的缺点是会在服务器上积累大量的无用资源,该问题只需要定时清理即可解决。

CDN可以有效提高静态资源加载加速,大大提高网站的访问速度,从而提升用户体验。它提高资源加载速度的手段主要有两种:一种是就近分配加载资源最快的服务器响应下载请求,另一种是利用浏览器本身的缓存机制进行资源缓存。

静态资源被托管到 CDN 上,资源的更新和缓存策略就由 CDN 服务商制定,从而引发了资源更新问题。不同的 CDN 服务商会有不同的资源管理策略,但一般都会遵循 HTTP。CDN 通过控制 HTTP 响应头中的与缓存相关的字段来控制缓存策略,包括协商缓存和强缓存。

假设有一个名为 style.css 的资源,CDN 服务商通过强缓存的方式将其缓存到了浏览器端,过期时间设置为 1 天。那么在 1 天之内,style.css 无法进行更新。即使 CDN 服务商没有使用强缓存,或者用户手动清空了浏览器缓存重新加载,CDN 的资源下发也会存在延迟。在最新的 style.css 由源站下发到边缘节点之前,用户访问 CDN 边缘节点时获取的依然是旧版的 style.css。

浏览器在从 CDN 边缘节点尝试加载 style_2e87e.css 时,会发生两种情况。一种是 CDN 边缘节点已经收到了源站下发的 style_2e87e.css 资源,此时直接返回该资源,浏览器就能获取最新的资源。另一种是 CDN 边缘节点尚未收到源站下发的 style_2e87e.css 资源,此时会触发 CDN 的回源机制,同样可以返回最新的资源。

CDN 的回源指 CDN 服务器在收到一个资源请求时,发现自己没有缓存该资源,从而从它的上层服务器或者根服务器加载该资源的过程。回源有效解决了在源站的资源下发间隔内,边缘节点资源缺失的问题,进一步提升了 CDN 的稳定性。

A/B 测试指在项目工程中利用代码分支的能力控制代码后续的执行逻辑,从而为用户提供不同的页面或功能。

一般在保证功能不变的前提下,会针对某一功能设计两个版本进行 A/B 测试并收集数据,最终选定结果更好的版本。

灰度测试

硬件隔离,顾名思义就是从硬件层面上将代码隔离开来。通常来说,就是将新旧功能的代码部署到不同的机器上,直接在物理层面上将其隔离,相互之间不产生影响。

当用户访问页面时,会由流量入口对请求进行解析,根据灰度标识确定此次访问应该如何灰度分流。如果目标用户带有灰度标识,则将请求转发到灰度机器上;如果目标用户在不带有灰度标识,则将请求转发到正常机器上。

软件隔离指通过技术手段将代码进行隔离,从而进行灰度分流。例如,A/B 测试是软件隔离的一种方法。采用软件隔离的灰度分流方案能够在最大程度上利用机器资源、降低运行成本,但是由于灰度版本和普通版本在一台机器上,所以灰度版本在某些时候可能影响普通版本的使用。

第一,当百分比不变时必须确定每次命中的灰度用户是一致的。

基于以上两点,提供一个简单的百分比放量策略。在一般情况下,每个账户都会有唯一的数字 ID,用于标记用户身份信息。只需要对数字 ID 进行取模,就可以实现百分比放量。

发布回滚的方法非常简单。系统每次部署时都会将构建好的资源输出到 build 目录,开发人员只需使用 zip 命令将 build 下面所有的资源进行压缩即可。

window.performance会返回一个 Performance 类型的对象,其中,performance.timing 包含了各种与浏览器性能有关的时间数据,提供浏览器各处理阶段的耗时。

用户体验是所有网站都关注的问题之一,优化用户体验的前提是能评估当前的性能状况,找到短板。Google 提供了许多性能测量和性能报告工具,但对于刚接触开发的人员而言,大量的工具和指标令人应接不暇。部分开发人员只想评估网站的性能、判断用户体验的好坏,不需要成为性能专家。因此,Google 启动了 Web Vitals 计划,和 W3C 的 Web Performance Working Group 协作,致力于打造一组新的标准化 API 和指标,从而更准确地测量用户的网页性能体验。Web Vitals 计划的目的是简化性能评估的手段,帮助开发人员专注于最重要的指标,即核心 Web 指标。

最大内容绘制(Largest Contentful Paint,LCP)测量加载性能。为了提供良好的用户体验,LCP 应在页面首次开始加载后的 2.5s 内发生。

首次输入延迟(First Input Delay,FID)测量交互性。为了提供良好的用户体验,页面的 FID 应为 100ms 或更短。

累积布局偏移(Cumulative Layout Shift,CLS)测量视觉稳定性。为了提供良好的用户体验,页面的 CLS 值应保持在 0.1 或更少。

Google 提供开源工具库 web-vitals,开发人员可以在项目中引入、调用相应方法,获取对应的性能指标数据。

2020 年的指标构成侧重于用户体验方面的加载性能、交互性和视觉稳定性。

首字节时间(Time To First Byte,TTFB)代表浏览器接收第一字节的时间,可以用来解决服务器响应时间过长的问题

首次内容绘制(First Contentful Paint,FCP)代表从页面开始加载到页面内容的任何一部分呈现在屏幕上的时间,它是一个重要的、以用户为中心的衡量感知加载速度的指标

总阻塞时间(Total Blocking Time,TBT)指 FCP 与可交互时间之间的总时间,因为这期间主线程被阻塞的时间过长,无法对用户行为做出响应。每当出现长任务(在主线程上运行超过 50ms 的任务)时,主线程都被视作阻塞。如果阻塞时间过长(如超过 50ms),那么用户很可能注意到延迟,并认为页面缓慢或卡顿。

可交互时间(Time To Interactive,TTI)指页面从开始加载到主要子资源完成渲染,并能够快速、可靠地响应用户输入所需的时间。它能够有效评估当前页面需要多久才能进入可评估状态。

错误捕获

try catch 函数只能捕获当前执行的上下文中抛出的错误。如果被包裹的代码块脱离了当前执行的上下文,则 try catch 函数没有办法捕获错误。

开发人员在使用window.addEventListener('error')采集错误时,需要区分 JavaScript 错误和资源加载错误,可以利用linenocolno字段实现。当资源加载失败时,回调参数中的两个字段的值为 undefined。同时,也可以利用 instanceof 来判断,JavaScript 错误抛出的事件类型为 ErrorEvent,资源加载失败抛出的事件类型为 Event,ErrorEvent 为 Event 的子类型。

JavaScript 代码中还存在异步错误,这种错误没办法被上述三种方法捕获,需要使用addEventListener监听unhandledrejection

出于安全性考虑,浏览器只允许同域的脚本捕获具体错误信息,会对抛出的错误进行脱敏处理,以防止敏感信息泄露,此时捕获错误的 JavaScript 代码就无法有效获取错误信息。

解决以上问题的方法是为跨域脚本添加crossorigin="anonymous"配置,同时 CDN 服务器需要为 HTTP 响应报头Access-Control-Allow-Origin配置合理的值,此时就能正常地捕获 JavaScript 错误。

开源社区也提供了source-map工具模块,开发人员经过简单配置同样可以根据错误信息和 Source Map 文件解析出源代码的报错位置。

导致白屏的原因大致可以分为 JavaScript 执行错误和请求未返回两类。

将一个 DOM 树上的节点作为监控对象,判断目标节点的子节点是否可见。如果每次都递归遍历所有子节点是否可见,那么会严重损耗性能,浪费大量的时间。此时,可以使用深度优先算法进行优化。当某一层级的节点处于可见状态时,对于它所有的后代节点都可以不再进行判断。当某一层级的节点处于不可见状态时,需要判断其所有的后代节点是否都处于不可见状态。只要在遍历过程中发现某个子节点处于可见状态,就代表目标节点处于可见状态,即页面当前处于非白屏状态,此时便可结束遍历。

在捕获 JavaScript 错误成功后,需要对页面进行检测,判断当前界面的可视区域是否存在内容。

节点是否可见不是单纯由 HTML 的标签规范决定的,CSS 属性中的 display、opactiy 和 visibiilty 等属性也会影响节点的可见性。

此时,通过 addEventListener 监听 JavaScript 的异常逻辑,然后使用前文提到的 parseError 解析错误详情,同时利用 window.location.href 记录出错的页面地址,就能大致对白屏的原因进行定位。

浏览器提供了MutationEventMutationObserver两种方法,它们都可以监听 DOM 树的变化情况。MutationEvent 是同步触发的,每次变动都会触发一次调用,当 DOM 树频繁变更时,MutationEvent 会消耗大量的性能。基于性能优化的考虑,MutationObserver 作为 MutationEvent 的替代品诞生了。它是异步触发的,DOM 树的变动不会在第一时间触发监听事件,而是等到所有 DOM 变更都完成后才触发,所以 MutationObserver 比 Mutation events 的性能更好。

运营商主要劫持出省流量,因为部分运营商有省内流量考核,跨省访问会增加成本,所以劫持往往发生在省间传输上。

HTTPS请求实际上包含了两次 HTTP 请求。首先,服务端向第三方权威机构购买证书,第三方权威机构会颁布证书给服务端。然后,当客户端向服务端发起 HTTPS 请求时,服务端会将公钥返回给客户端。客户端在收到服务端的公钥后,请第三方权威机构校验公钥的合法性,第三方权威机构校验公钥并返回校验结果。如果校验结果有问题,则 HTTPS 传输无法继续;如果校验结果无误,则客户端生成一个随机值,这个随机值就是用于对称加密的密钥,即客户端密钥。接下来,客户端使用服务端的公钥对客户端密钥进行非对称加密,这样客户端密钥就变成密文了,此时 HTTPS 中的第一次 HTTP 请求结束。紧接着,客户端发起 HTTPS 中的第二次 HTTP 请求,将密文发送给服务端。服务端接收到客户端发来的密文后,会用自己的私钥对其进行非对称解密,解密之后的明文就是客户端密钥。然后服务端用客户端密钥对需要返回给客户端的数据进行对称加密形成密文,再将密文发送给客户端,客户端收到服务端发送来的密文,用客户端密钥对其进行对称解密,得到服务端发送的数据,至此,HTTPS 传输过程完成

webpack提供了 import 方法来对代码块进行异步加载。通过 import 加载的目标模块,在项目打包时会被抽离出来,单独作为一个文件打包。

开发人员借助 webpack 的按需加载能力,可以将页面进行拆分。用户在访问单个页面时只会加载需要的代码,这不仅节省了带宽,还提升了用户体验。如果开发人员使用了按需加载,那么在加载资源时,最好提供一些界面上的引导和反馈。