防御性设计和开发

560次阅读  |  发布于2年以前

PART 1:理解防御性

定义

“防御性编程(Defensive programming)是防御式设计的一种具体体现,它是为了保证,对程序的不可预见的使用,不会造成程序功能上的损坏。它可以被看作是为了减少或消除墨菲定律效力的想法。” “防御式设计是考虑使用者可能会错误使用的所有情形,用设计手法避免错误使用,或是降低错误使用的机会。”(来自wiki)

简而言之,前端开发中的防御性就是防出错。这里的“错”不只是代码报错,而是影响用户使用和用户体验的全部问题。“防”不仅是预防,进一步追求弹复性。弹复性的定义:“系统能从故障中恢复并在面对故障时保持服务可靠性的持久性的能力”(参考阅读:https://www.bmc.com/blogs/resilience-engineering/)。前端开发不仅是简单还原产品设计,而实现更好的产品使用体验。否则就成了“中看不中用”。前端开发的工作和影响必须从实现层(中看)深入到体验层(中用)。

我们不能假设用户会按照产品预设的方式使用产品。在产品设计之初,我们也难以预见到用户所有使用场景和用法。只要在页面上放一个输入框,用户就可能输入任何值。正如此时此刻我在写这篇文章,如果断网,或者不小心cmd+w了,或者不小心F5......能不能保住我的文章,这就是一种防御性功能。严格说这不算是产品核心功能,但会直接影响产品的使用体验,甚至会导致用户弃用一款产品。所有这些伤害用户体验的事情不断积累,最后得到用户的反馈就是“难用”。用法越复杂的产品类似的体验问题越多。开发上的防御性是指保证程序正确执行。我们同样不能假设数据是干净的,入参是符合预期的,调用的方法是不会出错的等等。我们要尽量避免错误发生(这是预防),错误发生后不影响产品继续使用(这是纠正),错误发生后提示用户该怎么做(这是指引)。因此,防御不光是“防”,防御性体现在预防、纠正、指引三个方面

下图是一种良好的防御思路。通过防御性设计和开发最大程度的阻止“危机”的发生(后面会分析前端有哪些危机点)。当危机发生,如果没有任何防御性手段只能有两种可能的结果:一是用户自己理解问题是什么,反复尝试或通过求助解决问题,即便最终完成任务,体验也是相当糟糕的。二是直接放弃。通过“纠正(Correction)”和“指引(Direction)”能绕过危机。

(来自Udit Khandelwal的文章《Defensive Design Framework》)

预防 (后面会详细展开)
纠正 - 兼容范围尽可能广泛。例如输入标签时支持用空格、分号,逗号分隔;支持全角/半角数字;支持自动截空格等等。 - 纠正错误。一是错误信息易于理解,错误信息要具体明确有建设性,所以搞错误码治理很有意义。二是在保证正确性的前提下,有缺省值,让用户能继续使用。(注意避免隐藏问题,不让用户轻易看到报错的背后是问题要进行上报) - 前端实时校验的目的是为了提高提交的成功率,避免提交后再报错。操作表单追求的是完美提交成功率和一次性提交成功率。
指引 - 文案的引导。用易于理解的文案,解释并引导下一步操作。出错后不仅能让用户明白出了什么错,还要告诉用户怎么做。 - 建议和推荐。典型的AutoSuggestion组件,可以提高输入的准确性。高效的交互就是减少输入,尽可能让用户点选。 - 当错误发生后有降级方案或兜底措施。将用户导航到能继续使用的分支上。交互设计中往往只考虑理想的流程。开发中则有面向失败的设计,这是设计师不能忽略的。

防御点

实际情况远比想象的复杂。下图中列举了一些用户在使用链路上可能存在的“危机点”,这也是防御性设计和开发中着重要考虑的“防御点”。归纳为两大类问题:

  1. UI的防御性

2.代码的防御性

尤其对于专业用户的B端产品来说,我们要考虑什么样的人,在什么样的网络环境,乃至自然环境下,用着什么样的OS、CPU/GPU/内存、浏览器和显示器的使用我们的产品,还要考虑服务慢了挂了失效了怎么办(做好真不容易啊)。

PART 2:实现防御性

代码的防御性

防御的目的是确保程序能够正确的运行。前端开发面对的是一个大型的异步模型,一方面跟用户互动,用户会随时触发各种操作,有些是预期外的交互行为。另一方面跟服务端互动,受网络环境和服务端稳定性等各种不确定性因素影响。要保证在各种情况下不能阻断用户的使用,同时还能实现更好的交互体验。

1、前端防御性开发中的常见问题

分类 类型 具体问题
需求 功能实现问题 1. 错误的实现:不符合PRD 2. 不完整的实现:遗漏一些版本 / 应用场景下的不同实现 3. 流程缺失,如权限校验、授权、灰度等
UI和交互问题 1. 一致性问题:不符合设计稿 / 不符合设计规范 2. UI适配性问题
开发 逻辑问题 1. 判断条件有误 / 忽略了必要条件 2. 循环 / 递归的退出条件 3. 显隐逻辑和跳转逻辑控制 4. 缺少校验或错判参数类型 / 空值 / 边界条件 5. 缺少对默认值 / 缺省状态的校验 / 判断 / 处理 6. 接囗调用逻辑和组合关系 7. 忽略一些组件之间的联动关系
全局副作用 1. 变更公共代码,对其他部分产生影响 2. 变更配置文件 / 全局变量 3. 代码的冲突和污染 4. 基础库版本升级
容错问题 1. 错误输入 / 特殊字符 / 数据类型的容错 2. 接囗返回值的不确定性 3. 接囗请求失败的容错 4. 缺少error boundary,避免导致白屏 5. 错误要上报
表单校验问题 前端校验条件不全
编译 & 依赖问题 1. JS编译漏掉对一些语法的处理 2. 本地和发布构建有差异 3. 本地和线上依赖版本有差异
兼容性问题 1. Polyfill不全 2. CSS兼容性问题
文案问题 1. 文案错误 / 不准确 / 折行 2. 国际化不完整
“灵异”问题 难以解释,工程腐化的结果
数据 请求失败 1. 缺少或错误的入参 2. 参数结构不符合接囗文档 3. 请求失败缺少catch 4. 请求失败信息不全 / 不友好
字段问题 1. 返回的不确定:缺字段 / 类型不统一 / 空值 / 默认值 2. 接囗各种场景考虑不全 3. 用户数据差异
接囗变更 未及时同步变更和接囗版本变化
状态不全 加载态、空状态、错误提示等UI反馈不完整
系统问题 浏览器问题 1. GET / POST请求参数超出限制 2. Cookie / LocalStorage超出限制 3. 不符合同源策略 4. 触发浏览器Bug的一些写法
资源加载问题 CDN服务异常
请求失败 / 响应慢 Web服务异常
显示性能 加载慢 / 渲染慢
能耗 内存泄漏 / 重循环缺少优化 / CPU占用过高
交互性能 卡顿 / 闪烁 / 假死
接囗性能 1. 接囗延时 / 超时 2. 接囗的重复调用 / 接囗的冗余调用
安全缺陷 三方库/开源库 NPM包被恶意篡改、挂马屡有发生
敏感信息风险 (建议购买阿里云数据安全中心服务)
Web安全风险 XSS/CSRF/SSRF/SQL注入等

人是代码的创作者,提高代码防御性,写出高质量的代码,最终靠人。人需要通过工具增强能力,需要从代码评审中学习经验。

2、前端代码审查项

类型 审查项
通用 import的包是否符合要求
变量名是否可理解
用const / let声明变量
是否对方法的参数、组件的属性进行必要的检查
避免hardcode值,用常量替代
复杂的判断条件需要先赋值再判断
是否进行必要的数据类型转换
是否引入不需要的状态
链式调用要检查成员属性是否存在,或用?.
优化嵌套循环和多层判断
注掉代码不清除需加说明
是否存在未引用的方法
try{...} catch(err){}catch要有处理
异步请求处理“三态”:加载、空状态、错误处理
批量请求的接囗是否包含未使用的接囗
避免使用dva的subscriptions
避免引入多余的全局状态
API错误码有对应的具体文案
方法参数不大于5个
单个文件小于400行
是否遗漏国际化处理
消除ESLint报错
清除console.log
React 禁止写内联样式
禁止直接操作DOM
使用的是否是标准组件
Form表单的校验是否使用Field组件
Form提交是否对处理中进行处理
Form提交成功无论是关闭浮层还是跳转,都需要显示Message.success('...')
用useRef替代全局变量
组件内有循环要用useMemo
组件属性值是方法要用useCallback
绑定数据的组件是否有加载和空的状态
CSS class命名是否容易重名
禁用float和absolute布局
禁止固定宽/高,如需用min-width``min-height替代
禁止直接修改通用样式,采取覆写的方式
禁止直接修改标签样式,同上
业务代码禁止在全局定义CSS变量
UI自测 用UI Lint自测
支持放大 / 缩小两级
Git 分支用法是否规范
提交信息是否清晰
素材 是否使用无版权的图片或icon

《代码大全》第8章防御式编程对前端开发的启发:

方式 说明 对前端开发的启发
1.保护程序免遭非法数据的破坏 1. 检查所有来源于外部的数据 当从外部接口中获取数据时,应检查所获得的数据值,以确保它在允许的范围内。 2. 检查子程序所有输入参数的值 3. 决定如何处理错误的输入数据 1. 检查接囗数据字段(是否存在 / 数据类型 / 取值范围 / 缺省值) 2. 检查方法的属性参数(是否存在 / 数据类型 / 取值范围 / 缺省值)做必要的转换
2.断言 1. 建立自己的断言机制 2. 用错误处理代码处理预期发生的状况,用断言去处理那些不该发生的错误! 3. 利用断言来注解前条件和后条件 前条件(先验条件):调用方在调用子程序前,保证入参的合法性。 后条件(后验条件):子程序的返回结果保证合法性。 4. 避免将需要执行的子程序放到断言中 1. 对于得到的入参 / 外部数据 / 返回结果 进行检查。是否符合业务逻辑 2. 通过写断言,不仅可以提高防御性,还能提高可读性
3.错误处理 程序的健壮性:健壮性具体指的是应用在不正常的输入或不正常的外部环境下仍能表现出正常的程度。 健壮性的原则: 1、不断尝试采取措施来包容错误的输入以此让程序正常运转(对自己的代码要保守,对用户的行为要开放) 2、考虑各种各样的极端情况,“没有什么是不可能的” 3、即使终止执行,也要准确/无歧义的向用户展示全面的错误信息 4、抛出有助于debug的错误信息 1. 主动防御处理是有降级 / 容错处理,尽量不要走到error boundary。 2. 考虑到各种可能的输入修士,兼容全面。 3. mock各种极端数据进行测试。 4. 丰富捕获到的错误信息,包含更多上下文信息。
程序的正确性:和健壮性是有一定冲突的。健壮性尽可能不出错。正确性是宁可出错也不返回不准确的值。 1. 返回中立值 / 默认值:处理错误的最佳做法就是继续执行操作并简单的返回一个没有危害的值。 2. 换用下一个正确的数据:在轮询中,如返回数据有误就丢掉,进行下一轮查询。 3. 返回上一次正确的数据:同上,不跳过的也可以返回上一次正确的数据。 4. 选择最接近的合法值 5. 上报错误日志 6. 返回一个错误状态码 7. 启动错误处理子程序或对象 8. 显示对用户友好的出错消息 9. 正确性要求高的话,就直接退出程序 1. error boundary对错误的处理是消极防御,只是限制了错误的影响范围,上报和展示错误。 2. 展示给用户的错误信息需要加工。用户能看明白,有建设性。 3. 上报的错误日志对还原问题和调试友好。
4.异常处理 (出错后,调用方利用try/catch/finally捕获子程序异常,并进行善后处理) 1. 用异常通知程序的其他部分,发生了不可忽略的错误(无感...) 2. 只在真正例外的情况下才抛出异常(无感...) 3. 不能用异常来推卸责任:能在局部处理掉就在局部解决掉,不要简单抛出去。 4. 避免在构造函数和析构函数中抛出异常,除非你在同一个地方把它们捕获 (无感...) 5. 在恰当的抽象层次抛出异常:不要把底层的异常抛给高层的调用方,暴露具体实现的细节。 6. 异常消息中加入关于导致异常发生的全部信息 7. 避免使用空catch语句 8. 考虑创建一个集中的异常上报机制 9. 考虑异常的替换机制 当前有错误就直接抛出去,导致线上监控的错误信息质量不高,这个环节值得改进
5.建立隔栏 左侧外部接口数据假定是脏数据、不可信,通过中间这些类(子程序)构成隔栏,负责清理、验证数据,并返回可信的数据,最右侧的类(子程序)全部在假定数据干净(安全)的基础上工作,这样可以让大部分的代码无须再担负检查错误数据的职责 类似的适配器模式和门面模式用来隔离或适配变化,都是对不可控变化的防御。
6.辅助调试代码(Debugging Aids) 1. 在早期的引入辅助调试代码 2. 采用进攻式编程 “尽量让异常的情况在开发期间暴露出来,而在产品上线时自我恢复。”在开发阶段考虑到最坏的情况。 3. 发布时移除调试辅助的代码 1. console.log算是一种辅助调试代码,发布时清除。在复杂的调试场景下,有必要专门写一些辅助调试代码。 2. 利用chrome插件追踪变量和状态变化辅助调试。 3. 写单测是一进攻式编程。

UI 的防御性

防御点 措施
防白屏 1. 白屏监控 2. 资源加载失败重试 3. Service Worker的资源fallback机制 4. 模块都包装了error boundary 5. 兼容性探测和提示 6. 白屏提示信息
防慢 -- 网络慢 / 响应慢 / 渲染慢 / 执行慢 前端性能优化
防卡 -- 卡顿 / 假死
防布局错乱 前端响应式开发
防极端内容 -- 缺失 / 超长 / 连续字符 / 未转义
防一致性问题 1. 《设计规范》 2. UI走查工具、视觉回归测试
防UI状态不全
防样式污染 代码审核
防Chartjunk
防误操作 / 危险操作 .

1、B端产品响应式设计和开发的必要性

以阿里云云安全中心屏幕物理分辨率占比情况(如下图)为例,从结果看台式机或外接显示器的使用比较普遍。我平时开发所用的是1440(15寸本)仅占约9%。同时,用外接显示器会有各种用法,横着用,坚着用,分屏用等等(如图)。必须注意到:屏幕物理分辨率≠浏览器窗囗大小。不能简单的依据屏幕分辨率进行设计和开发。

(图片来自网上)

响应式网页设计的定义:“响应式网页设计(Responsive Web Design,缩写RWD),或称自适应网页设计、响应式网页设计、对应式网页设计。是一种网页设计的技术,这种设计可使网站在不同的设备(从桌面电脑显示器到手机或其他移动设备)上浏览时对应不同分辨率皆有适合的呈现,减少用户进行缩放、平移和滚动等操作行为。”

响应式开发不仅是布局的自适应,最终目的是让产品UI能自适应窗囗大小的变化,自适应内容的变化,均能有良好的呈现。用程序员的话讲就是UI的健壮性。而且尽量不用或少用media query,它只能定死一些breakpoint值,实现效果比较僵硬,应当充分利用CSS技术本身的灵活性。大貘老师近期有一篇详细介绍CSS防御式开发的文章值得仔细看看。

2、CSS开发中的防御规则

防御点 说明
1. 避免用“布局组件” 这里指的是用JavaScript实现的布局组件不要用。它会多出很多层没用的嵌套,同时把布局定义的很死,难以再用CSS控制。
2. 避免用JavaScript控制布局 永远没有原生的流畅,同时增加代码的复杂,容易用问题。除非解决一些必要的兼容性问题。
3. 避免用float / position: absolute / display: table等过时的布局技术 优先用Flexbox/Grids布局。你会说绝对定位还是有用的。你要强迫自己不用,经过反复尝试过发现绝对定位是最优的选择那就用。重要的是有这个“强迫自己”的过程。
4. 避免定高/定宽 固定宽/高最容易出现的问题是内容溢出。没必要通过定宽高对齐,可以利用Flexbox的位伸/收缩特性。一般情况下用最小宽/高、calc()、相对单位替代。同上要“强迫自己”不用。
5. 避免侵入性的写法 - 避免影响全局样式,如:* { ... }、:root {...} 、div { ....}等。 - 避免影响通用组件样式,如:.next-card {...},如果要定制单加一个class名。 - 不要直接修改全局CSS变量,把自己的CSS变量定义在模块的范围内。 - 不要写z-index:999。一般1~9,防止被遮挡10~99,绝对够用了。 - 不要在标签上定义style属性。不要在JS代码中做样式微调,这样今后无法统一升级CSS样式。 - 只有完全不可修改的样式才能用!important,利用选择器优先级调整样式。
6. 避免CSS代码的误改 / 漏改 - 将选择器拆开写,如.card-a, .card-b { ... },写时方便,改时难,修改时容易影响其它元素,不如分开写(除非像css reset这种特别确定的情况)。 - 将样式集中在一起,容易改错。保持CSS代码和文件在相应的层级上,同一模块的放一起。避免混入通用样式中,为了避免改错,允许适当冗余。 - 用@media时,会集中覆写一批元素的样式,更新样式时非常容易遗漏。所以必须拆开写,和对应模块的样式放在一起。不要集中放在文件底部,或是集中放在某一个文件里。 - 及时清除“死代码”。 - 定义样式要写全,微调样式要写具体,如:.mod { margin: 0; } /* 其它地方需要微调时 */ .biz-card .mod { margin-bottom: 16px; }
7. 避免CSS样式冲突 - 限定作用范围。如,.my-module .xxx { ... }。 - 业务代码中的样式要加前缀,或借鉴BEM命名方式。如:.overview-card-title { ... }。用CSS Module也可以。 - 注意选择器的精确性。级层过长过于复杂的CSS选择器会影响性能,但要注意:有时需要精确选择某些元素,如仅选择一级子元素,.overview-card-content > .item { ... }
8. 防止内容不对齐 应该说Flexbox侧重“对齐”,Grids是专为布局设计的。受字体、行高等因素影响(如图),用Flexbox实现对齐最可靠: 1、height / line-height 不可靠。 2、display:inline-block / vertical-align:middle不可靠。
9. 防止内容溢出 包括文字 / 图表等内容在宽度变化时或是英文版下容易出现溢出(如图)。 1、图表要支持自动 resize。 2、图片要限制大小范围,如:max-widthmax-height ``min(100px, 100%)max(100px, 100%) (注意:min() / max() 兼容性:chrome 79+ / safari 11 / firefox 75) 3、不要固定宽/高。(见规则3) 4、不要在容器元素定义overflow:hidden防止内容不可见。宁可难看关键信息也要显示全。 5、用min-width:0防止Flexbox项被内容撑开。例如:html如下,.canvas的style是JS写死的,不可避免的溢出了(如图)。
canvas
这种情况下.item可以定义为: display: flex; min-width: 0;
10. 防止内容过度拥挤 - 为了防止内容过长时紧帖到后面的内容,水平排列元素之间要设置间距,一般是8px。 - 如果用flexbox要加上gap。考虑到gap的兼容性:chrome 84,稳定起见用margin。
11. 防止内容被遮挡 定义负值时(负margin / top / left),小心内容被遮挡,避免这么定义。定义margin统一朝一个方向,向下和向右定义,再重置一下:last-childposition: relative 平时很常用,发生遮挡时会造成链接无法点击。
12. 防止可点击区域过小 小于32x32像素的可点击元素,通过下面的方式扩大可点击区域: .btn-text { position: relative; } /* 比 padding 副作用小 */ .btn-text::before { content: ''; position: absolute; top: -6px; left: -8px; right: -8px; bottom: -6px; }
13. 防止内容显示不全 / 被截断 - 在定义overflow:hidden时,就要考虑内容是否有被截断的可能。一般不要加在容器元素上。 - 防止长文字被生生截断,加省略号。UI实现过程中要对内容做出判断,哪些是不应该折行的,哪些是不应该省略的,如: white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
14. 防止该折行不折行 / 不该折行的折行 首先必须理解UI,折行有3种情况:哪些需要折行,哪些不能折行,哪些不能从中间断行。 1. 大部分情况需要折行,不能为了保持UI美观而损失内容的完整性。一般用overflow-wrap,尽量不要用~~word-wrap~~(不符CSS标准): overflow-wrap: break-word配合overflow-wrap,可再加上hyphens: auto(目前兼容性不够) 限定多行: -webkit-line-clamp: 3 2. 不能折行,如标题 / 列头 / 按钮等。开发中要理解内容,哪些元素不应该折行。 3. 避免表头折行。表格列数过多(>5列)时,会要求锁列,此时,th定义white-space: nowrap强制不折行。 4. 不能从中间断行的情况(如图):
15. 防止滚动链问题 浮层的场景下需要避免滚动链问题:子元素可滚动,如果父元素也有滚动区域,在子元素上滚动时,触顶/触底后,会影响父元素滚动。关掉浮层后,用户会发现页面滚到了其它位置。 overscroll-behavior: contain; overflow-y: auto; overflow-x: hidden; 注意:避免出现同时出现水平/垂直滚动条 兼容性:chrome 63+ / firefox 59+ / safari和edge不支持
16. 防止图片变形 - 图片被置于特定比例的容器中时,固定宽/高和约束最大宽/高,都可能会导致图片变形。 .head img { width: 100%; height: 100%; object-fit: cover; } - 在Flexbox容器内,图片高度会被自动拉伸。因为不要定义align-items,默认是stretch
17. 防止图片加载失败 需要考虑图片加载慢或加载失败的情景。在图片的容器上加边或加底色。
18. 防止CSS变量未引入 在标准化开发中,我们提倡使用全局的CSS变量。业务代码中,利用CSS变量也可以方便的进行全局的控制。在使用CSS变量时要加上缺省值。 font-size: var(--tab-item-text-size-s, 12px);
19. 防止CSS兼容性问题 - 不要加浏览器厂商前缀,让CSS预编译自动处理,像-webkit--moz-。 - 不要用仅特定浏览器厂商支持的属性。 - 充分测试,尤其是windows和safari。
20. Flexbox常见防御性写法 Flexbox的默认表现比较多,不能简单的定义display:flex,或是flex:1。 1. Flexbox容器元素通常要做如下定义:要支持多行(默认是单行),交叉轴上垂直居中(默认是stretch),主轴上采用space-between,将自由空间分配到相邻元素之间。一般都要写上: display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; 2. Flexbox的盒子元素要定义间距。
21. Grid常见防御性写法 - 不固定网格的宽度,用minmax(最小值,1fr)。 - 定义间距,如grid-gap: 8px。 - 不固定列数, 利用auto-fit / auto-fill自动适配。

写在最后

最后习惯性的想有没有通用解决方案,难点在于太紧帖具体业务实现。要实现通用:一种思路提供原子化的库,类似TailwindCSS,不需要太精通CSS。另外就是内置于标准组件中。默认具备自适应能力。接下来可以继续探索。

(图片来自网上)

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8