我理解的 SPA

如题,SPA (Single Page Application - 单页应用) 这个话题已经在 Weex 社区讨论了有一段时间,在传统的前端开发领域中大家也在长期探讨这个话题。这里谈谈我个人的理解和看法。

SPA 的背景

SPA 往往和 Router、页面间通信、页面间数据共享这些词汇联系在一起,不少同学直接问到这些词汇,实际上都是以 SPA 为前提的,因为脱离 SPA 的概念,这些词汇将失去它原有的意义,或者变成了完全不同的东西。

那 SPA 不是理所当然、天经地义、不容挑战的吗?干嘛要脱离 SPA 的概念讨论这些问题?

我觉得不是

SPA 背后的命题是如何管理复杂的页面关系,最终构成一个产品的整体。传统的页面之间是通过简单粗暴的“页面跳转”和“浏览器前进/后退”建立联系的,SPA 提出的观念是在浏览器中模拟页面的跳转、切换和前进/后退,相关的很多周边命题也随之而生。

(其实你不觉得这也是个”全家桶”么……)

所以我们先把问题回归到如何管理复杂的页面关系

如何管理复杂的页面关系

我觉得有以下几个子命题 —— 在这之前,我想先把接下来所有的讨论,从形态上收敛到手机应用,PC 端的暂时不涉及。

如何拆分页面

怎么把一个完整的产品以页面为维度进行拆分,这里是有学问的。

其中最关键的一个认知问题就是页面的颗粒度是否就是整个手机屏幕同时可以呈现的所有内容?

如果是,那么页面本身的结构就是简单的一维模型,即所有的页面都可以完全独立工作运行。

如果不是,那么有可能某个页面是另外一个页面的一部分,两者之间有包含关系。这样的话页面结构就变成了多维的。

在 PC 上,答案显然是后者,因为 PC 的屏幕比手机要大得多,所有页面都要整屏更换和依赖,显然是不合理的,但是在手机屏幕上,我们有机会简化为前者。

如何在工程上把不同的页面解耦

也可以从另外一个角度讲,不说怎么解耦,而是不同的页面之间保留了哪些耦合。

这个地方的设计直接决定了大规模并行研发产品的可能性和实际的效率效果

独立页面之间是如何建立联系的

  • 如何跳转和切换:在手机上常见的跳转和切换就是 navigator 和 tabbar 两种模式,一种是产生历史记录的,有栈式结构的;另一种则是平级切换的,页面之间是并列关系
  • 如何传递信息:开发者是否有机会在一个页面调用到另外一个页面的方法或接口
  • 如何共享数据:用户在前一个页面提交了个人信息,如何体现在下一个页面上

看看 SPA 是怎么解决这些问题的

  • 拆分页面的方式:如最一开始所描述的,SPA 会在浏览器中模拟页面切换和跳转,因此他需要路由控制,而控制的方式就是把每个页面都定义一个 URL,当页面切换或跳转时,URL 就会发生相应的变化,同理我们直接打开不同 URL 的时候,浏览器可以准确定位到不同的页面。常见的 URL 区分方式包括不同的路径 (path)、不同的参数 (query) 和不同的锚点 (hash) 等。所以如果父页面中有子页面存在不同的状态,则 URL 设计应该在父页面的基础上追加不同的 path/query/hash 来达到定位子页面状态的目的。
  • 工程耦合度:首先 SPA 可以通过 URL 精准定位到不同的页面,就决定了它的路由规则是中心化约定的,当页面比较多了之后,为了避免冲突,每新建一个或一组页面都要有中心化注册的过程;另外每个页面中的代码从资源加载和管理的角度是中心化耦合的 (当然有具体优化的解法,比如按需加载等,这里不展开了)
  • 页面之间的联络方式
    • 跳转和切换:主要是通过代理浏览器的跳转、前进、后退、替换等行为,然后自行在页面内完成相应的变化,几个明显的入口包括 <a> 链接、location 设置、历史管理 API等
    • 信息传递:典型的场景就是子页面和父页面之间同步变化,一般通过在 SPA 框架层面提供通信机制,而且通常是消息广播的模型
    • 数据共享:现在普遍被接受和认同的是基于 Flux 架构的设想,开辟一块全局共享的 Store,这样应用从 A 页面切换或跳转到 B 页面的时候,可以通过这块全局的 Store 来同步数据和状态
  • SPA 这样的方案的特点或局限
    • 过渡体验:SPA 因为代理了所有页面切换和跳转的行为,所有整个页面之间过渡的过程是可以定制的,比如左滑右滑、淡入淡出等等,结合一些 CSS3 变换的效果,可以做得很酷炫,传统页面跳转是很难做出这种效果的
    • 首次加载:因为有中心化的路由管理等相关逻辑,同时 SPA 首页首次加载往往需要消耗更长的时间,如果用户的回头率不高的话,总体上这个其实是个劣势
    • 长效 JS Runtime:这意味着用户会长期开着同一个页面,JS 的内存控制是一个非常敏感的命题,一不小心写出个内存泄漏,整个应用就会越跑越慢直到存尽应亡
    • 需要手动处理 Security/Privacy 等问题:因为本质上所有的页面即便设计和研发商再独立,它也是运行在同一个浏览器页面中,没有绝对的信息和资源的隔离

手机淘宝是怎么解决这些问题的

手机淘宝的工程传统是一种非常简单粗暴直接有效的理念 —— 可能你看下来会有这种感觉

首先我们没有实践 SPA —— 准确的讲鲜有成功实践的 SPA 案例,但解决上述问题有一些自己细节上的思考

  • 拆分页面的方式:没有子页面的概念,父页面某个地方不同的状态均由子页面自行设计体现,不做中心化设计和管理
  • 工程耦合度:每个页面都是完全不同的浏览器实例,所以工程上是以页面为颗粒度完全解耦的,不同页面可以使用完全不同的前端框架、资源策略、内部通信方式等
  • 页面之间的联络方式
    • 跳转和切换:就是浏览器的跳转,额外的我们会通过 SPM/SCM 参数在服务端直接统计出流量转化的关系,是纯天然无污染的,SPA 做这个事情就需要另外想办法了
    • 信息传递:因为没有父页面子页面的问题了,所以这种需求几乎是不存在的,如果真的有父子页面的关系存在,一般会是 <iframe> 可以使用 Web Messaging API 来进行通信,但总体上确实场景非常少
    • 数据共享:首先 Flux 的全局 Store 应该不太排的上用场,因为页面之间是完全隔离的,所以这里有三个套路:1 是通过 URL 传参数;2 是通过 W3C 的一系列本地持久化存储机制,包括 cache/localStorage/WebSQL/IndexedDB/appcache 等;3 可以通过 hybrid API 或服务器记录状态并进行中转。这里额外强调一下,1 和 3 是我们使用最多的方式,2 反而用得很少,因为对于手机淘宝这种体量的产品,本地空间很快就被塞爆了,所以业务上重度依赖这种技术不是很明智的选择
  • 手机淘宝方案的特点或局限
    • native过渡效果:页面之间的过渡效果是不可定制的,这是一个局限
    • 资源重复加载:不会面临首次加载内容过多的问题,但是不同的页面资源会重复加载,当然就像 SPA 的方案一样,这里也有很多预加载或缓存的上层机制可以缓解这一矛盾
    • 短效 JS Runtime:比较省心,反正页面跳走之后旧页面的相关资源就彻底回收了
    • 天生 Security/Privacy:页面之间没有直接共享的系统资源,而且基于 W3C 的同源策略,所以也比较省心

所以大家会发现,我们为了更好的工程实践和大规模并行研发方面做了一定的取舍

Weex 打算怎么解决这些问题

经过上述分析和论述,我们也逐步滤清了 Weex 在这个复杂问题上的思路:如何解决 Weex 中路由管理的问题?如何在 Weex 上进行 SPA 实践?如何让 Weex 页面之间通信或共享数据?其实面对这么多看似复杂混乱的问题,只要从上述几个角度抓住重点问题,提出关键解法,就可以把复杂问题逐一解开。

  • 拆分页面的方式:提供 <web>/<embed> 这样的组件,可以管理子页面,但这里我们延续了手机淘宝对待子页面状态定位的看法,不做中心化的设计,每个页面可以自由定义识别规则 —— 当然这个规则范围就不包含 path 了 —— 因为这需要模拟页面的跳转和切换,所以只有 query 和 hash 可以识别
  • 工程耦合度:每个页面独立研发,完全解耦,同时可以利用 SPM/SCM 数据统计机制,和手机淘宝的工程实践经验保持一致
  • 页面之间的联络方式
    • 跳转和切换:目前就两个入口 <a> 链接和 openURL() 方法,对于 <web>, <embed> 中产生的跳转和切换命令,我们目前还没有具体的设计和实现,将来可以提供类似 a[target] 的配置项,让页面跳转和切换的时候可以指定父页面或子页面作为目标
    • 信息传递:目前是没有办法传递的,未来可以设计一个类似 SPA 感觉的系统级的消息广播机制来满足业务上的需求
    • 数据共享:同样的我们推荐的也是手机淘宝的最佳实践:URL 参数传递、本地持久化存储、服务端中转;同时,由于 Weex 的 JS Runtime 是唯一的,这也就意味着我们未来是有机会在系统层面提供类似 Flux 架构中全局 Store 的东西供开发者使用,但是这里随之而来的隐患也是比较多的,而且一旦这个功能进入系统层面,所有的问题都会被无限放大,所以我们在这方面持谨慎态度。
  • 该方案整体的特点或局限
    • 过渡效果:我们不可能像 SPA 那样完全用 CSS3 变换来定制页面之间的过渡效果,但是有机会归纳抽象几种常见的效果,供业务方选择,这样页面之间的过渡效果不至于像传统的浏览器效果那么生硬
    • 资源重复加载:可以完全复用手机淘宝目前所有的最佳实践
    • 长效 JS Runtime、Security/Privacy 问题:这方面我们和 SPA 面临的问题是相同的,目前这方面的问题我们不算解决的特别好,有很多工作需要做。也是因为如此,我们上述的系统级 Flux 架构和全局 Store 的方案需要三思而行

接下来的 Actions

最后总结下来,如果大家认可我们上述的设想和取舍的话,Weex 在应用级别的工程实践上还欠缺这么几个地方:

  1. 提供子页面精确定位识别的最佳实践和必要的工具库或辅助库
  2. 提供类似 a[target] 的配置项,在页面跳转或切换时可以指定目标页面
  3. 系统级的全局消息广播机制,做到跨页面信息传递
  4. 抽象归纳几种常见的页面过渡效果,供上层业务选择和配置
  5. 有效控制长效 JS Runtime 存在的各种问题
  6. 最后的最后,非常谨慎的引入 Flux 架构中的全局 Store —— 其实系统级的 Store 和本地持久化存储只有一点点区别:就是 Store 是被进一步抽象且可以通过绑定机制自动触发视图更新的

总结

这里首先谈了谈个人对 SPA 的认识,同时觉得 SPA 背后的命题本质上是“如何管理复杂的页面关系”。然后列出了几个关键的维度,包括如何拆分页面、如何管理耦合、如何跳转和切换页面、页面间如何通信和数据共享等等,并对比了 SPA 在这方面的表现,和手机淘宝传统的实践经验和取舍判断,最后按照相同的思维模型得出了 Weex 在这方面的选择,列出了接下来的 Actions。在整个过程中,我们可能还是会在细节问题上展开更具体的讨论,届时我们可以再伺机探讨。

谢谢

向本文提出修改或勘误建议