最近几年,随着前后端分离、单页面应用的崛起,网页正变得越来越应用化。移动互联网端的发展更是助长了这个趋势——对于交互、性能不敏感的场景,Web App在开发成本、跨平台兼容上有着明显优势。
但在这火爆的行情背后,很多时候从产品经理到设计甚至开发,对Web平台的特性并没有足够的了解与警觉,导致最终产品成了既不App也不Web的四不像,不仅拖累用户体验,开发团队也容易无所适用。
这篇文章希望围绕着Web的特性,探讨Web App与Native App的不同,帮助读者在项目中尽早地识别出可能出现问题的场景。
Web App is NOT App
Web App 也要按照基本法
Web App或者单页面应用(Single Page Application),名字里带个App,听起来颇有鸟枪换炮的味道,无论是工程师还是利益关系人(产品经理、客户),难免会有和尚摸得我摸不得,App做得我做不得?
的错觉。更有甚者,把原生App的设计稿P上个浏览器地址栏就丢给团队,让照着做个Web App出来。如果这是你遇到的情况,那么恭喜,你已经在被坑的路上了
俗话说入乡随俗,既然运行在浏览器上,即使Web App能很大程度上提供接近App的体验,但有几点,是从根本上不同于App而又必须考虑的:
1. 应用入口与访问路径
App对用户的访问路径拥有近乎绝对的控制权:用户从哪些地方进入应用(图标、通知栏、分享链接)有限且可预测。而每一页提供的操作入口之和,都是用户当前可访问的子路径的全集。
而Web对用户的访问路径控制权近乎为0:任何页面的URL都是对用户可见的,用户可以添加书签,分享给亲友——任何页面都可能被直接访问,必须在设计和架构层面考虑。
鉴权和被访问并不矛盾:本文中被访问严格来讲是被用户请求,而鉴权或重定向则是对请求的处理
2. 刷新、后退、前进
这三个操作是所有Web浏览器工具栏的标准配置,而App多数情况下只需要考虑返回操作。
最麻烦是刷新:它会清空页面内存、重新发起请求、重新执行脚本。这意味着Web端跨页面的内存数据是不可靠的,如果A页面依赖了B页面放在内存中的数据,下次刷新时得到的只会是undefined
。
3. 上下文隔离
用户可能在应用或站点上进行一系列的、跨页面的操作,这些用户行为称为上下文
,而操作产生的数据(比如用户选择或表单输入),本文接下来称作上下文数据
。
由于App对路径拥有绝对的控制权且不考虑刷新,某种程度上说App打开新的视图和在网页上打开模态窗的成本是一样的,下游页面可以假定上游页面生成的内存数据一定存在,而这些数据完全通过代码存取,对数据结构没有限制,因此App端的上下文数据是复杂、可依赖的。
反观Web,由于每个页面都可以被直接访问、刷新、甚至是复制链接到另一台设备上访问,存储在内存、LocalStorage中的数据都因此变得不可依赖,唯一能可靠传递的上下文信息仅限于URL可以表征的数据,在没有服务器帮助的情况下,Web端的上下文数据是简单或不可依赖的。
现代移动端浏览器提供了一种应用模式,可以屏蔽浏览器工具栏,让页面更像原生App。然而现阶段真实应用的案例并不多见,并且至少有两个问题:
后退无法完全屏蔽:安卓平台的系统
返回
键等同于浏览器的后退
。移动端随时吃紧的内存可能导致当前不活动的页面被内存回收,下次打开时重新加载——相当于一次刷新。
为什么URL重要?
上面三点其实都围绕着核心——URL,某种程度上说,整个Web世界都是被URL标识并驱动的,每个URL都应该定位到相应的资源
而现实的情况是:可能你在做一个面向消费者(以下称2C
)的项目,但需求方压根就没提URL和刷新的事;也可能你做的只是一个后台管理系统,不会有人在乎这东西,甚至会有一些和URL的定位特性相悖的需求。
这是最可怕的事,也是我写这篇文章的初衷:在真正遇到麻烦前,很少有人(甚至包括资深的前端工程师)会全面的思考这类问题,等掉到坑里的时候才发现积重难返。
在不假思索地接受这些设定前,希望你能仔细思考以下几个问题:
客户对URL与Web体验间的关系有无概念?
客户是否能把浏览器工具栏操作与真实的业务场景联系起来?
案例:某2C项目
在项目初期,询问客户对刷新的看法时,客户明确表示所有页面,刷新一律回首页
,因为客户所参考的另一个竞品网站就是这么做的。作为技术团队,很容易就此得出不必兼容刷新
的结论。
然而随着了解的深入,我们发现客户有如下需求:
支持邮件推销:在发给用户的邮件里会附带某个产品的链接,打开链接希望用户能看到相关产品页面
支持某些页面保存链接:在订单完成后,会为用户生成一个含有二维码的凭证页作为取货凭证。用户可以保存这个页面的链接,并在取货时(可能是几天后)直接打开。
以上两个需求,从技术角度看和支持刷新是等价的:都要求能仅通过URL定位并展示页面资源。
然而前后沟通的结果却完全不同,如果技术团队按照最开始的结果做架构,到最后一定会付出代价。
回过头看,一开始客户给出的答案并非刚性需求
,背后没有业务价值,只是因为看到别人这么做而已,甚至客户还以为自己主动削减了需求,为开发团队省了事。而这样的需求,后期改变的可能性和弹性都是非常大的。
后面两条,则是包含业务价值的刚性需求
,直接影响到客户利益,几乎没有任何讨价还价的余地。
要避免这类问题,最重要的是警惕涉及技术架构的非刚性需求
,从多种角度进行确认,引导并挖掘出更多的相关场景。客户是纯业务的,对不同业务场景间的技术共性没有概念。而如何引导客户、挖掘真实需求,则是团队专业性最直接的体现。
在上面这个案例中,当客户说”不用考虑刷新,一律回首页“的时候,团队只要多问一句”那有没有需要直接用URL打开的页面呢?“,就很可能引导出更真实的需求。客户并不知道两者在技术上等价,开发团队这时候需要承担起引导的责任。
小心模态窗
一个典型的模态窗(Modal)如下图所示:
上下文的黑魔法
它通过模仿窗口的方式,允许用户在不脱离当前页面上下文的情况下进行操作。同时它也有一定的阻塞性,可用来控制用户的操作流程,比如有的电商网站会通过模态窗来引导用户先选择所在地区再继续购物。
之前我们讲过Web页面间的上下文是强隔离的,而模态窗在不脱离上下文的前提下能承载的内容非常可观,可观到什么承度呢?如果把模态窗的长宽设置为填满当前页面,那它看起来会和新的页面没什么两样!相当于额外100%的内容承载力!
这既提供了设计上的灵活性,也潜藏着风险:既然模态窗可以承载和新页面一样多的内容,为什么不直接用它代替页面呢?和跳转到新页面相比,它还可以保持当前页的所有状态:表单内容、滚动位置……
谨防超载
问题在于,如果需要用URL定位到模态窗中的内容,渲染的顺序一定是先有模态依附的页面主体,再有模态窗。URL不仅需要携带模态窗的相关信息,连页面主体的也得带上。要是模态套模态,套得越深,还原页面的所需信息越复杂,离URL友好也就越远。
这就像平房和楼房,虽然楼房利用了纵向空间,但想进门一定要从底楼一层层上去。空中楼阁是不存在的,模态窗也不可能脱离自己依附的主体独立存在。
案例:某后台管理系统
在我刚毕业的时候接触过一个后台管理系统,由于是小公司,完全没有Web设计的相关经验。
那个系统的设计几乎是完全忽略URL的,一开始是翻页等重要信息没有体现在URL中,导致资源无法定位,一旦脱离某个模块,则在该模块下的所有操作都得重来。
为了解决这个问题,设计(其实是非设计出身的领导)给出的方案是用模态窗——既然找回上下文困难,不脱离上下文不就行了。
然而这时候的模态就像毒品:想要查看设备的子设备列表?开个模态窗;想看某个子设备的运行状况?再开个模态窗。就这样模态窗套模态窗,有碍观瞻是小事,当某天你意识到Web的世界还有URL这回事的时候,或者客户报怨说想通过打开保存的链接就能看到某个子设备的时候,要改回来已经几乎不可能了。
要避免类似的问题,至少需要在设计阶段(或开发review设计时)注意以下两点:
1. 控制内容复杂度
模态承载的内容和功能应尽量单一。如果是希望用URL定位的资源,尽量不要设计到模态窗中
例如登录窗、或者展示商家地图位置,都是适合模态窗的场景。而像订单这样的资源,由于资源间关系复杂,很可能再出现显示子资源或关联资源的需求,用模态窗展示的话将来就很容易嵌套。
有种特殊情况是资源本身有URL友好的页面显示,为了用户体验在其它地方也用模态窗展示该资源的信息,这样是没有问题的,因为模态窗并不需要被URL定位
2. 考虑新标签中打开链接
不想脱离当面页面又想访问其它资源?其实Web早就想到了,这就是你每天都在使用的新标签页打开链接
比如订单列表页,每个订单要有订单详情的功能入口,而用户可能是通过复杂的搜索条件以及漫长的鼠标滚动才看到当前的列表项,并不希望脱离当前页面。把订单详情放模态?缺点前面已经讲过了,更好的方式是在新窗口中打开订单详情页,用户不仅不会脱离当前页面,甚至可以同时打开多个子页面,比不能并发的模态高到不知道哪里去了。
伪页面
如果说内容超载的问题主要发生在桌面端,那在移动端还有另一个涉及模态窗的虚胖
问题,个人称这种设计为伪页面
,如图所示:
第一个页面是主页,点击箭头所示的日期会打开选择日期的模态窗。你可以亲自上携程移动端查看并操作。
类似设计移动端非常常见:由于屏幕太小,有些功能本质虽然单一(如城市选择、日期选择,本质上都是个选择框),体现在UI上仍然需要占满全屏。但这些功能(特别是表单类操作)又绝对不能脱离上下文存在,最终只能选择用模态窗来处理。
这就产生了矛盾:全屏显示的功能很容易给用户“这是一个页面”的错觉,而实际上它并不是。这个矛盾在面对刷新
、后退
、前进
等操作时尤其明显:
当模态窗打开的时候,点击刷新,要不要重新打开模态窗?
当模态窗打开的时候,浏览器后退是关闭模态窗还是后退到真实的上一页?
如果后退可以关闭模态窗,那前进是否可以打开它?
感兴趣的朋友不妨在刚才携程的链接上将以上几点操作一番,亲身感受。
这些细节是需求分析和设计阶段很难考虑全面的,往往遇到问题了才开始头痛医头脚痛医脚,最终无论对用户体验还是技术团队,都很容易造成伤害。
解决方案?
个人对此并没有十全十美的解决方案:全屏显示的需求与功能并非页面的本质,这两者都无法轻易解决
。假如本文能让你在正式开发前就想到这个问题,对我来说已经是功德一件了。
不过也并非完全没有办法,既占满屏幕,又能暗示背景上下文的设计从iOS 7就开始流行了——没错,就是毛玻璃效果
粗略地改了一下携程的日期选择界面样式,如下图所示:
通过毛玻璃效果消除了弹出层是新页面的错觉后,用户就不容易对刷新
、后退
等操作出现错误的期望。
加载资源之What - When - How
要做到URL友好,一个必要条件就是页面能自行加载所有需要的外部资源。
What
什么是外部资源?不就是服务器端数据吗?对,但是不全对。
随着接口的无状态化,Web App会保存一些原本在服务器端存在的状态,例如登录信息。对每个页面而言,这些跨页面共享的状态也应该视作页面的外部资源
。
这听起来有点奇怪:页面就是Web App的一部分,却要把Web App的状态视作外部资源
?
其实很好理解,核心仍然在于:组成Web App的每个页面,其上下文是隔离的。
还记得文章开头的基本差异第三条吧?以用户登录信息为例,即使你把信息放进了localStorage以保证刷新后可访问,也挡不住用户在其它浏览器访问同一个链接,甚至干脆把链接分享给别人——这意味你不能假定这些数据的可靠性,对单个页面而言,Web App的状态与服务器资源一样是不可知的黑盒。
因此,即使你可以像访问内存一样用同步代码访问localStorage,我仍然建议你把Web App的状态管理与API请求放到相同的层级进行考量,这样能更好地反映对问题本质的抽象。
插播技术观点一则:localStorage规范同步IO更像是历史遗留问题。未来一定会被异步存取的标准或第三方库取代(火狐就搞了一个异步的localforage),将应用状态与API同等对待的向前兼容性更优。
When
这个问题看起来没什么好说的——当然是在页面加载时。
但如果需求方给你的是App的设计稿,那请务必注意:App的设计有可能是在上个页面加载下一页的数据,再根据结果决定是否跳转到下个页面;而Web由于URL的独立可访问性,通常是先跳转到目标页面再加载数据
这带来的第一个差异就是Loading进度的显示问题:App可以在上一个页面显示Loading,而Web则不建议这么做。原因很简单,页面自己能加载资源,上个页面再帮它加载,结果一定是要么浪费资源,要么增加复杂度。
目前多数App的设计和Web一样是先跳转再加载,然而先加载再跳转的设计也是存在的,比如下图国外某航空公司App:
点击“Find flights”,会出现如图所示的小菊花,等到加载结束才跳离当前页面。
而携程移动版(无论Web还是App)就是标准的页面自行显示loading:
How about failed?
第二个差异则是如何处理加载失败。在App端,如果由上一页面加载下个页面的数据,则错误的处理(例如弹窗)也会在上个页面显示,下游页面从设计上基本不会考虑加载失败时的渲染问题。
上图是点击"Find flights"请求失败后,页面没有跳转,直接在当前页提示错误。
作为对比,携程的移动端如图:
从以上截图也可以看出来,国外航空App的设计如果做成网站,是很难保证URL友好的。稍微总结一下的话就是:
Web由于URL的独立可访问,每个页面都必须考虑加载中、加载失败的时候如何显示,如果设计给你的只有成功的场景,请马上找他重新核对需求。
结语
苟利老板亏盈以,岂因祸福避趋之
工程即妥协,有时候为了更好的用户体验,上述任何一点都可能牺牲。本文的目的不是形成教条,而是帮助你在权衡利弊时能想得更加清楚。
更何况,无论是Web还是App,都在高速发展并相互影响。浏览器赋予开发者更加丰富的、接近App的接口,而App也在借鉴Web的资源定位机制。
另一方面,本文所讨论的其实是处于设计与技术间的灰色地带,甚至需要工程师去挑战和影响设计。这是因为团队分工程度越高,这些灰色地带的衔接就越可能导致项目失败。个人非常鼓励程序员,特别是前端工程师去思考业务和设计,乃至于输出影响力。前端开发的本质是data -> design
的函数,只有深入地理解输入与输出,甚至主动出击修正偏差,才能交付真正的价值。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。