Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

如何增强单页应用的体验 #35

Open
xufei opened this issue Aug 1, 2016 · 24 comments
Open

如何增强单页应用的体验 #35

xufei opened this issue Aug 1, 2016 · 24 comments

Comments

@xufei
Copy link
Owner

xufei commented Aug 1, 2016

什么是单页应用

所谓单页应用,指的是在一个页面上集成多种功能,甚至整个系统就只有一个页面,所有的业务功能都是它的子模块,通过特定的方式挂接到主界面上。它是AJAX技术的进一步升华,把AJAX的无刷新机制发挥到极致,因此能造就与桌面程序媲美的流畅用户体验。

单页应用的优势

操作体验流畅,媲美本地应用的感觉,切换过程中不会频繁有被“打断”的感觉。
因为界面框架都在本地,与服务端的通讯基本只有数据,所以便于迁移,可以用比较小的代价,迁移成桌面产品,或者各种移动端Hybrid产品。

单页应用的弱点

  • 对搜索引擎不友好
  • 开发难度相对较高

如何尽可能增强单页应用的操作体验?

- 路由的规划
- 推送的使用
- 断线重连机制
- 操作补偿机制
- 本地缓存
- 热更新
- 良好的内存管理
- 服务端预渲染

1. 什么叫做路由?

路由可以理解为url与界面状态的对应关系。

我们需要注意到,在理想状态下,url和界面状态应当是精确对应的。比如说,对同一个用户来说,两次使用同一个url所打开的界面,其状态应当是完全一致的。对于同一个界面,进行相同的操作之后,url应当能够精确反馈当前状态。

但是我们需要注意到,细碎操作如果都需要跟路由保持同步,会是一个非常繁琐的事情,所以在设计过程中应当加以取舍,舍弃那些过于细碎的状态与路由的同步。

2. 服务端推送

推送的意思是,某些情况下,即使页面开着不动,服务端也主动发送消息过来,让界面能够有所体现。通常我们会使用WebSocket之类的技术来实现这种体验。

有时候,我们可能会看到一些在页面上使用推送的场景,最常见的是即时消息。

比如说,我们在应用里加一个聊天窗口,其他人发一条消息,自己这边能够实时展现出来。

如果是为了极致的用户体验,我们可以把整个应用的业务变更都使用推送,比如:

我在查看某条任务的时候,有人修改了这条任务,我这里应该不需要做什么操作,就能自动体现出他的修改。

如果对全业务的变更都做推送管理,使用体验会大为加强,但是,实现难度和代码复杂度会急剧上升。

3. 断线重连机制

我们如何判断一个单页面产品的技术水准呢?可以通过这样一种方式:

连续开几天不关,不需要通过“刷新”这个操作来解决一些常见问题。

为什么这个事情能够体现技术水准呢?因为要把这个事情做到极致,需要把这几件事情做好:

  • 断线重连机制
  • 良好的内存管理
  • 版本的自动升级

因为移动办公普及之类的情况,导致我们可能需要面对一些情况,比如,切换了网络,电脑休眠再打开之类,当再次联网的时候,就需要去重新链接,并且,对这个过程中发生的业务变更进行“补课”,然后逐一应用到界面上来,把界面调整到最新状态。

4. 操作补偿机制

什么是操作补偿呢?

从逻辑上来讲,当我们在界面上操作,创建一条任务的时候,新的这条任务不应当立刻显示出来,而是应当等到服务端确认成功了,才加到界面上。但很可能我们的网络不好,这一步用户要等很久。从用户体验的角度,这样是不好的,所以我们可以先把界面放上去,然后等创建成功的消息回来之后,再把一些唯一标识之类的东西回填到内存数据中。

单步的操作补偿还算是不太难,如果有多步的话,就非常麻烦了,举个极端情况的例子来说,用户新增了一条任务,服务端还没返回的时候,他就立刻在这条任务下创建子任务,但子任务这时候没有父任务的id,如果想给这步也做操作补偿,就比较麻烦了。甚至说,连续进行了几步操作之后,发现之前的操作失败了,后续处理会非常复杂。

5. 本地缓存的使用

上面提到,如果多步连续操作中间出现了失败,局面会比较尴尬,比如你填了好多东西,提交的时候才发现网络坏了,那就非常头疼,这时候,用户会非常期望这些数据能够保存下来,等网络好了再重新尝试。

我们可以使用本地缓存来临时存储这些数据。如果这个层面做到极致,能够结合良好设计的操作补偿机制,甚至可以让用户脱机使用我们的应用,把所有产生的这些变更都缓存,等到联网的时候再批量同步合并回去。

6. 热更新

前面提到,用户有可能长期开着我们的应用,然后中间一直没有刷新。正常情况下,业务变更都应当会被全部推送过来,界面所反馈的状况始终是最新的,符合现状的。但我们需要考虑到另外一个问题,系统升级怎么办?

我们当然可以推送一个通知:本系统已经升级了,请点击刷新。但能不能做得更好?这是有可能的,要达到这种目的,就要使用热更新这种手段,把代码的模块化和变更管理都做到极致,每次更新的代码模块也推送过来,并且作为补丁应用到当前系统上。这种机制对开发团队的水准要求很高。

7. 良好的内存管理

要想让用户能够长期开着应用,还需要管理好内存。

数据的变动、路由的切换、组件的创建与销毁,都会带来内存的变化。完美的内存控制是几乎做不到的,如果要追求这方面的极致,对开发过程的影响会非常大,很多情况下是不划算的,所以,可以做一些针对优化,把比较常规的问题解决掉,不用的东西及时销毁。

8. 服务端预渲染

作为一个单页应用,很经典的模式就是前后端完全分离,前端加载界面和逻辑,后端响应数据,前端根据这些数据,“生成”相应的变化。

注意到,我们这里有一个“生成”的过程,通常我们也会把这个过程称为“渲染”。它的机制就是根据数据生成对应的界面,如果是在浏览器侧生成这个界面,首先,加载界面模板或者逻辑,需要一次请求,然后,等这块准备好了,还需要去请求数据,这时候又多了一次网络请求。网络请求通常是比“生成界面”慢的,并且很可能这个时间不稳定,这时候就可能延误了用户第一次看到界面的时间。

虽然单页应用跟服务端渲染是存在矛盾的,但我们仍然可以部分优化这个事情,比如把某些页面由服务端直接代入数据生成。现在有一些开发框架也在尝试从另外一个层面解决这个问题,那就是对客户端和服务端渲染提取共性,使用合适的抽象方式来同时描述这两种机制,从而仅仅依靠配置的变更就可以切换渲染机制。

小结

我们提到了这些能够提升单页应用体验的方式,如果实现出来,肯定是可以让使用者非常愉悦的,但需要冷静权衡理想与现实之间的差距:

  • 我要做的是一个怎样的东西?
  • 我的开发团队是怎样的实力?
  • 我们有怎样的历史负担?
  • 值不值得这么做?
  • 能不能做得了?

本文中提到的这些体验增强方式,都是需要去权衡实现的,做得越多,所需要的技术掌控能力越强,出错概率也越高。

有一句著名的表达式:

E = MC^2

我们可以对此有不一样的解读:

Errors = (More Code) ^ 2
@xufei
Copy link
Owner Author

xufei commented Aug 1, 2016

这篇是近期做的分享,在讲的过程中,针对第二点:服务端推送,有过一些展开。

我最近大半年时间,没有像以前一样在视图层框架上花较多精力,而是着重于关注数据层解决方案。后面,会在这个层面分享一些东西

@xufei
Copy link
Owner Author

xufei commented Aug 1, 2016

这篇里面的每个点都可以展开很多话题,精力有限,无法一一述及。今年下半年的侧重点应该还是在数据层解决方案上。

@chaoren1641
Copy link

赞,前端和交互设计都需要了解的单页面体验

@0326
Copy link

0326 commented Aug 2, 2016

使用本地缓存还有一个好处就是页面初始化的时候可以直接先拉本地数据渲染对DOM结构有要求的数据,然后异步请求后端同步数据,这样可以让首屏渲染的更快

@CommanderXL
Copy link

听大哥娓娓道来~

@wnow20
Copy link

wnow20 commented Aug 2, 2016

坐等详细的讲解 👍

@zbinlin
Copy link

zbinlin commented Aug 3, 2016

好多都跟 stateless 和 immutable data 有关

@Hanruis
Copy link

Hanruis commented Aug 16, 2016

内存管理方面,感觉 backbone 要哭哈。backbone 没有提供组件的管理,很容易出现 zoombie view 。 而使用 vue ,angular ,react 这些,框架都做好了。所以感觉组件销毁这一块,需要手动处理的场景不多。

@jiangtao
Copy link

好文,很多点值思考

@Tonylvv
Copy link

Tonylvv commented Aug 22, 2016

学习

@ibufu
Copy link

ibufu commented Sep 13, 2016

经验告诉我,单页应用要做好各种loading动画。
如果没有seo需求,可以用加载动画替代服务端渲染。
就目前来说,服务端渲染对服务器的性能开销很大。

@mishe
Copy link

mishe commented Sep 14, 2016

学习了

@nitta-honoka
Copy link

关于热更新有什么实现方案吗?现在采用的方式是通过更新入口 HTML 的资源引用来进行应用版本更新的操作,意味着每次发版后,都需要用户手动刷新一下,更新 HTML 文件才能引用到新的资源。

@Hanruis
Copy link

Hanruis commented Sep 15, 2016

看了数值关联计算的文章之后,觉得 Rx 处理操作补偿的场景,应该也很方便。每个操作都描述成一个任务,或者事务并依赖 “父任务” 的操作补偿完成。 感觉像是 saga 的概念。

@tiye
Copy link

tiye commented Sep 20, 2016

断线重连跟应用重新打开的过程, 简聊做过尝试, app 关闭时把内存 store 完整状态存储下来, 然后网络连上的一刻, 其实就是一样的抓取全部依赖的数据然后更新界面, 就是一致的代码了, 感觉已经到比较好了.

热部署的问题感觉比较难, 不管怎样, 加载两份代码, 有些内存总是没法回收掉的, 参考开发过程热替换代码的样子, 很可能代码本身占用的内存不断增加. 我觉得如果只是想优化对于用户而言的体验, 也许模仿 App 更新倒是跟好办法, 比如说用户从当前页面离开, visibility 变成 hidden, 这个时候自动刷新页面, 然后打开同样的路由.

@Hanruis
Copy link

Hanruis commented Sep 22, 2016

@jiyinyiyong 再想的细一点。这个刷新的操作要考虑一下,如果还有任务在跑,需要等待优先级比刷新(更新)更高任务的完成。比如连续几步的操作补偿, 上传文件等任务还在执行的时候,需要等待这些完成之再执行更新。

@tiye
Copy link

tiye commented Sep 22, 2016

@Hanruis 确实. 不过这样的话就要求升级页面的代码要和各种逻辑耦合, 挺有风险的事情. 可能还是想想别的办法比较好.

@Hanruis
Copy link

Hanruis commented Sep 22, 2016

@jiyinyiyong 要是 Rx 支持这类型的场景的话,就容易了(因为飞叔他们在用 Rx):声明一个任务,该任务依赖于当前 Rx 中正任务队列(仅当前,而不是特定的任务)的完成。

@Galen-Yip
Copy link

@jiyinyiyong 热更新总感觉没有一个很合理的方案。
倒是从交互方面去考虑,这里的前提是做好路由方案以及存储方案等等,让用户已友好的方式去刷新页面。毕竟刷新页面的成本是十分十分低的。

@tiye
Copy link

tiye commented Sep 26, 2016

@Galen-Yip 是的. 在代码层面做 GC 很难控制好, 不如刷新来得明确. 而且刷新过程其实可以考虑把路由, 草稿, 等等的信息存储在 localStorage, 以便做到对用户而言尽可能使无痕的.

@jsm1003
Copy link

jsm1003 commented Dec 26, 2016

要是有一个简单的demo把这些东西都实现了,并且加上注释该多好。。。

@zhiquan-yu
Copy link

Meteor 的 Optimistic UI 是增强 SPA 用户体验一个挺好的解决方案吧,特别是操作补偿,这个对用户体验的提升实在太关键了,不只是 SPA,原生应用也是这样,现在都是用各种 Loading 来缓解这个问题。

Optimistic UI with Meteor

Consistent ID generation and optimistic UI

When you insert documents into Minimongo from the client-side simulation of a Method, the _id field of each document is a random string. When the Method call is executed on the server, the IDs are generated again before being inserted into the database. If it were implemented naively, it could mean that the IDs generated on the server are different, which would cause undesirable flickering and re-renders in the UI when the Method simulation was rolled back and replaced with the server data. But this is not the case in Meteor!

Each Meteor Method invocation shares a random generator seed with the client that called the Method, so any IDs generated by the client and server Methods are guaranteed to be the same. This means you can safely use the IDs generated on the client to do things while the Method is being sent to the server, and be confident that the IDs will be the same when the Method finishes. One case where this is particularly useful is if you want to create a new document in the database, then immediately redirect to a URL that contains that new document’s ID.

@0326
Copy link

0326 commented Mar 8, 2017

mark

@alcat2008
Copy link

谈谈个人的看法

路由规划

路由是 url => data,但我遇到的大多数场景中 url 与 data 之间往往难以直接进行匹配。

就体验效果来说,SPA 提升最大的应用场景大都是交易型网站(应用),其目标是给用户桌面型应用的体验。

桌面应用和网站的区别是什么,个人感觉较为重要的一点是:桌面应用是封闭型的,已经规定好用户的使用方式,仅暴露特定的操作路径;而网站是开放型,其运行依赖于浏览器,得充分考虑到用户使用浏览器时的随意性,比如 后退 按钮,一个事务型交易结束后或者退出后,用户点击浏览器的 后退,这时该如何处理?

所以在做路由规划时,需要考虑各种偶然性,这点可以从原生 APP 的视角来考虑,可以应用类似 storybook 的形式去思考。即:每一个分支走下去都有一个回环,最终落在路由栈的栈底。

SPA 的未来

单页面应用是现在的主流趋势,而且愈发火热。不仅在 PC 端,在移动开发领域,React Native、Weex、小程序何尝不是另一种形式的 SPA ? 而且 Google 的 PWA 也着重强调了民工大大提到的几点问题:推送、本地缓存、热更新 ...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests