自 Vue.js 官方推特第一次公开到现在,我们就一直在进行着将饿了么移动端网站升级为 Progressive Web App 的工作。直到近日在 Google I/O 2017 上登台亮相,才终于算告一段落。我们非常荣幸能够发布全世界第一个专门面向国内用户的 PWA,但更荣幸的是能与 Google、UC 以及腾讯合作,一起推动国内 web 与浏览器生态的发展。
多页应用、Vue、PWA?
对于构建一个希望达到原生应用级别体验的 PWA,目前社区里的主流做法都是采用 SPA,即单页面应用模型(Single-page App)来组织整个 web 应用,业内最有名的几个 PWA 案例 Twitter Lite、 Flipkart Lite、Housing Go 与 Polymer Shop 无一例外。
然而饿了么,与很多国内的电商网站一样,青睐多页面应用模型(MPA,Multi-page App)所能带来的一些好处,也因此在一年多将移动站从基于 Angular.js 的单页应用重构为目前的多页应用模型。团队最看重的优点莫过于页面与页面之间的隔离与解耦,这使得我们可以将每个页面当做一个独立的“微服务”来看待,这些服务可以被独立迭代,独立提供给各种第三方的入口嵌入,甚至被不同的团队独立维护。而整个网站则只是各种服务的集合而非一个巨大的整体。
与此同时,我们仍然依赖 Vue.js 作为 JavaScript 框架。Vue 除了是 React/Angular 这种“重型武器”的竞争对手外,其轻量与高性能的优点使得它同样可以作为传统多页应用开发中流行的 “jQuery/Zepto/Kissy + 模板引擎” 技术栈的完美替代。Vue 提供的组件系统、声明式与响应式编程更是提升了代码组织、共享、数据流控制、渲染等各个环节的开发效率。Vue 还是一个渐进式框架,如果网站的复杂度继续提升,我们可以按需、增量地引入 Vuex 或 Vue-Router 这些模块。万一哪天又要改回单页呢?(谁知道呢……)
2017 年,PWA 已经成为 web 应用新的风潮。我们决定试试,以我们现有的“Vue + 多页”的架构,能在升级 PWA 的道路上走多远,达到怎样的效果。
实现 “PRPL” 模式
“PRPL”(读作 “purple”)是 Google 的工程师提出的一种 web 应用架构模式,它旨在利用现代 web 平台的新技术以大幅优化移动 web 的性能与体验,对如何组织与设计高性能的 PWA 系统提供了一种高层次的抽象。我们并不准备从头重构我们的 web 应用,不过我们可以把实现 “PRPL” 模式作为我们的迁移目标。“PRPL”实际上是 Push/Preload、Render、Precache、Lazy-Load 的缩写,我们会在下文中展开它们的具体含义。
1. PUSH/PRELOAD,推送/预加载初始 URL 路由所需的关键资源。
无论是 HTTP2 Server Push 还是 <link rel="preload">
,其关键都在于,我们希望提前请求一些隐藏在应用依赖关系(Dependency Graph)较深处的资源,以节省 HTTP 往返、浏览器解析文档、或脚本执行的时间。比如说,对于一个基于路由进行 code splitting 的 SPA,如果我们可以在 webpack 清单、路由等入口代码(entry chunks)被下载与运行之前就把初始 URL,即用户访问的入口 URL 路由所依赖的代码用 Server Push 推送或 <link rel="preload">
进行提前加载。那么当这些资源被真正请求时,它们可能已经下载好并存在在缓存中了,这样就加快了初始路由所有依赖的就绪。
在多页应用中,每一个路由本来就只会请求这个路由所需要的资源,并且通常依赖也都比较扁平。饿了么移动站的大部分脚本依赖都是普通的 <script>
元素,因此他们可以在文档解析早期就被浏览器的 preloader 扫描出来并且开始请求,其效果其实与显式的 <link rel="preload">
是一致的。
我们还将所有关键的静态资源都伺服在同一域名下(不再做域名散列),以更好的利用 HTTP2 带来的多路复用(Multiplexing)。同时,我们也在进行着对 API 进行 Server Push 的实验。
2. RENDER,渲染初始路由,尽快让应用可被交互
既然所有初始路由的依赖都已经就绪,我们就可以尽快开始初始路由的渲染,这有助于提升应用诸如首次渲染时间、可交互时间等指标。多页应用并不使用基于 JavaScript 的路由,而是传统的 HTML 跳转机制,所以对于这一部分,多页应用其实不用额外做什么。
3. PRE-CACHE,用 Service Worker 预缓存剩下的路由
这一部分就需要 Service Worker 的参与了,Service Worker 是一个位于浏览器与网络之间的客户端代理,它以可拦截、处理、响应流经的 HTTP 请求,使得开发者得以从缓存中向 web 应用提供资源而闻名。不过,Service Worker 其实也可以主动发起 HTTP 请求,在“后台” 预请求与预缓存我们未来所需要的资源。
我们已经使用 Webpack 在构建过程中进行 .vue
编译、文件名哈希等工作,于是我们编写了一个 webpack 插件来帮助我们收集需要缓存的依赖到一个“预缓存清单”中,并使用这个清单在每次构建时生成新的 Service Worker 文件。在新的 Service Worker 被激活时,清单里的资源就会被请求与缓存,这其实与 SW-Precache 这个库的运行机制非常接近。
实际上,我们只对我们标记为“关键路由”的路由进行依赖收集。你可以将这些“关键路由”的依赖理解为我们整个应用的 “App Shell” 或者说“安装包”。一旦它们都被缓存,或者说成功安装,无论用户是在线离线,我们的 web 应用都可以从缓存中直接启动。对于那些并不那么重要的路由,我们则采取在运行时增量缓存的方式。我们使用的 SW-Toolbox 提供了 LRU 替换策略与 TTL 失效机制,可以保证我们的应用不会超过浏览器的缓存配额。
4. LAZY-LOAD 按需懒加载、懒实例化剩下的路由
懒加载与懒实例化剩下的路由对于 SPA 是一件相对麻烦点儿的事情,你需要实现基于路由的 code splitting 与异步加载。幸运的是,这又是一件不需要多页应用担心的事情,多页应用中的各个路由天生就是分离的。
值得说明的是,无论单页还是多页应用,如果在上一步中,我们已经将这些路由的资源都预先下载与缓存好了,那么懒加载就几乎是瞬时完成的了,这时候我们就只需要付出实例化的代价。
这四句话即是 PRPL 的全部了。有趣的是,我们发现多页应用在实现 PRPL 这件事甚至比单页还要容易一些
作者:山边小溪
出处:jizhula.com 记住啦:)
欢迎任何形式的转载,但请务必注明出处。