渐进增强:脚本失效时,页面仍然可用
卷中目录
增强是加在可用之上的,不是先缺一块再补上
渐进增强的顺序是:先有一份能完成任务的文档,再用样式改善阅读,再用脚本改善速度和反馈。基础层在脚本没有运行时就已经可用。若页面先是一个空节点,内容、校验和提交全靠脚本插进去,脚本一旦失效,用户得到的就是空白或一枚永远转着的指示。那不叫增强失败,那叫基础层不存在。

脚本会以多种方式失效,而且多数时候并不是用户亲手关掉脚本。请求被网络中断,包只到了一半。内容安全策略拒绝了内联或某个来源。语法或新接口在当前引擎里直接抛错,后面的监听没有注册。依赖的第三方超时,初始化停在半路。扩展改写了文档。服务工作线程返回了一份过期的空壳。这些情况下,浏览器不会去显示 noscript 里的备用内容。noscript 只在脚本被关闭时出现。把备用界面只放进 noscript,挡不住真正常见的失败:脚本开始执行,然后中途出错。
因此备用界面必须就是文档本身。脚本的工作是在这份文档上添加能力,添加失败时,文档仍能提交、阅读和离开。
文档层把任务写成表单和链接
能用链接完成的跳转,不要写成靠点击监听的区块。链接有地址,可以被打开、复制、在新标签页使用,脚本不运行时仍然前往目的地。按钮用于改变当前页的数据或状态。区块配上点击监听,键盘和脚本失效时都没有行为,可访问名称也常常缺失。
会修改数据的操作使用表单。表单带有 action 和 method,字段带有 name,提交控件是真正的提交按钮,而不是一枚 type 被改成普通按钮、只靠脚本发请求的元素。服务端收到的是标准的表单字段,校验在服务端再做一次。客户端校验只是提前说明,不能代替服务端,因为脚本可能没跑,请求也可能不从这页面发出。
提交之后遵循重定向到结果或回到带错误说明的同一表单,而不是停在一个只有脚本才懂的中间响应上。错误说明写在文档里,放在表单前面,逐条对应字段,并与字段上的提示连在一起。不要只靠颜色。脚本可以在此之外把焦点移到错误摘要,这是增强;没有脚本时,用户刷新后仍能读到那段由服务端输出的说明。
身份和跨站请求的校验放在服务端会话里,不依赖脚本设置某个头才算数。基础层的表单提交同样要带上服务端能识别的校验字段,否则一关闭脚本,用户要么无法提交,要么绕过了本来该有的校验。两种结果都说明校验被放错了层。
用发布表单看基础层长什么样
一篇稿件的编辑页,基础层可以是这样:标题、正文、分类是带标签的字段,正文是文本区域而不是一枚需要脚本才出现的编辑器。封面是文件字段,表单的编码类型允许文件。保存草稿和提交审阅是两枚提交按钮,用不同的值区分意图。服务端检查标题是否为空、正文是否超过长度、文件类型是否在允许范围内,然后要么保存并重定向到详情,要么返回同一表单,字段值还在,错误写在顶部。
这条路径上没有任何必须先执行的脚本。网络慢时,用户看到的是浏览器自己的提交过程,而不是一枚脚本绘制的指示。重复提交可能发生,服务端用幂等键或唯一约束避免两篇相同稿件,而不是假设客户端会把按钮禁用掉。禁用按钮是增强,约束才是基础。
增强层可以在此之上做这些事:提交前检查必填并在字段旁显示说明;用脚本发送请求,成功后在当前页更新状态,失败时保留用户已经输入的内容;给正文加上工具条。工具条必须把结果写回那个文本区域。脚本失效时,用户在文本区域里写的字,就是会被提交的字。若编辑器把内容放在一个没有名字的节点里,文本区域始终为空,基础层的提交就会丢掉正文。这是增强破坏基础层的常见方式。
封面的拖拽和预览同样是增强。文件字段仍然在,用户可以选择文件并随表单提交。预览脚本出错时,不要把文件字段隐藏掉。上传进度是说明,不是唯一的提交通道。
样式失效时阅读顺序还在
样式是往上加的一层,不是基础。这里的基础仍然是文档。样式没有加载时,源顺序应当就是阅读顺序:标题、说明、字段、错误、提交。不要用源顺序去迁就某一种视觉排列,再靠样式把视觉调回来,否则样式一失效,用户会先看见页脚或一串操作,最后才看见标题。
网格、粘性栏和容器查询都是增强。它们可以改变外观,不应把唯一的入口移出文档流或移到源顺序的末尾却只靠视觉摆到顶部。键盘顺序跟随源顺序。视觉上在顶部、焦点时要走完整页才到的按钮,即使用户开着样式也会出问题;样式失效时这个问题只是变得更明显。
字号使用相对单位,让用户的字号设置能够放大正文。颜色对比在基础样式里就足够,不要把可读对比留到一段可能会失败的主题脚本里。图标若用字体或伪元素画出,旁边要有文本。脚本和字体都失效时,只剩图标的按钮没有名称。
布局可以在窄视口下改为线性,这是样式内部的适应。适应仍然不是基础层。基础层是没有这套样式时,线性的文档已经能用。若某条关键说明只用背景图承载,样式被拦截时说明就消失了。说明应是文本节点。
脚本层只在能力存在时打开
检测能力,而不是检测浏览器名称。需要某项接口时,判断它是否存在,再启用对应的增强。判断失败就什么都不做,基础表单继续工作。不要因为名称匹配某个新引擎就假设整包脚本都能跑。引擎可以被标识,也可以被用户改,和这一页的脚本能否执行完不是一回事。
监听应注册在文档已经具备的元素上。若脚本预期自己去创建表单,创建过程一旦抛错,页面上就没有表单。若表单来自服务端,脚本只是给它加监听,抛错发生在监听之前或之中时,原生提交还在。这是把增强挂在已有元素上的主要好处。
用脚本拦截提交时,只在增强路径确实可用时拦截。请求失败、抛错或超时,应回到原生提交,或在页面上留下与服务端相同标准的错误,并允许再次提交。拦截了默认行为又在请求层失败,用户会停在一个没有整页回退的状态,这比不拦截更差。保存用户输入的本地草稿可以作为增强:意外离开时帮助恢复。不能把本地草稿当成唯一存储。服务端的草稿才是换一台设备、换一个引擎之后仍然在的那一份。
模块脚本在不支持的环境里不会执行。若基础内容写在文档里,这没有关系。若基础内容靠模块去渲染,旧环境得到空白。给模块配一份传统脚本去渲染同一块空白,仍然是两套都可能失败的客户端渲染。把内容放进首次响应的文档,两套脚本都可以缺席。
部分失效比全部关闭更常见
全部关闭脚本时,问题通常很明显:没有交互增强,但表单还在。部分失效更难看见。包里靠前的代码抛错,后面的增强没有注册,可是另一段独立脚本已经改写了提交按钮的类型,原生提交也不工作了。审查时把会破坏基础层的改动和纯粹的增强分开。破坏基础层的包括:去掉表单、去掉字段名、把提交按钮改成不会提交的类型、在脚本里清空文本区域、用空节点替换掉服务端输出的正文。
服务工作线程若在离线时返回应用外壳,要确认外壳里仍有到达基础功能的路径,而不是一份缓存的空节点。外壳若依赖一套已下线的脚本接口,用户会卡在旧外壳里。版本更新时,先保证文档响应本身可用,再更新外壳。不要让缓存策略比文档层更激进。
第三方脚本单独放在不影响基础的位置。统计、客服挂件、分享条加载失败或执行很慢时,不应挡住表单的解析和提交。把它们放在不会阻塞主路径的加载方式里,并假设它们不会出现。页面的可访问名称、主提交和主内容都不能来自第三方。
怎样验证脚本失效,而不是只验证关掉开关
至少做三种检查。其一是引擎提供的禁用脚本。此时应能完成阅读和提交,noscript 里的补充说明如果有,应当出现,但任务不能只存在于那里。第二种是允许脚本,但在包的开头抛出异常,模拟语法或运行错误。此时 noscript 不会出现,表单必须仍然能提交,字段值必须仍然会被服务端保存。第三种是拦截脚本地址,模拟网络失败,同时保留文档。结果应与第二种相同。
再加一种部分失败:让增强用的请求接口返回错误,确认界面不显示成功,已输入的内容还在,用户可以改用整页提交。只测禁用脚本,测不到拦截默认行为之后的失败。只测请求成功,测不到基础层。
也要在样式表失败时走一遍源顺序。用键盘从页首走到提交,确认中间没有只靠样式才可见、实际上无法聚焦的关键控件,也没有源顺序靠前却被视觉藏住的字段。放大字号后基础提交仍在视口流程里,不被粘性装饰挡住。这些检查和视觉回归一起做,但视觉回归代替不了它们,因为视觉回归通常开着完整的脚本和样式。
增强可以很丰富,只要拆得下来
预览、自动保存、提及某人、粘贴图片、离线编辑,都可以做。每一项要能说出它失效时的基础行为是什么。预览失效,文本区域里的原文仍会被发布。自动保存失效,用户仍能主动保存。提及失效,正文里仍可以写对方的名字,或退回为普通文本。粘贴图片失效,文件字段仍能选择图片。离线编辑失效,在线时的表单仍是完整的。说不出基础行为的功能,就会在实现时把基础行为换成自己。
团队约定可以写得很具体。新页面的首次响应里包含主内容和主任务,不依赖脚本补全。所有会改数据的控件都在表单里,服务端校验与客户端说明使用同一套规则文本。脚本只通过能力检测注册。拦截提交必须有失败回退。编辑器一类的组件必须把值同步回有名字的字段。代码评审时看到空节点配装载指示,就要求改成服务端输出内容,装载指示只用于增强路径上的局部刷新。
这样做的结果不是界面变简陋。样式和脚本都在时,体验可以和完全由客户端驱动的版本一样顺。差别在失败的那一次:网络差、扩展冲突、一次发布里的脚本错误,用户仍然能把稿件交出去。可用性若只在演示环境的完整资源下成立,它还没有被增强,只是被假设了。
见字如晤