非常教程

Lavas参考手册

进阶教程

Skeleton 和 App Shell 模型

App Shell 模型是架构 PWA 的一种方式,它能够可靠且即时地让站点快速加载到用户屏幕上,获得与本地 APP 相似的体验。

Skeleton 和 App Shell 模型

如果您阅读过为 Vue 项目添加骨架屏一文,可能已经了解了骨架屏 (Skeleton)的作用其实和 App Shell 非常类似,解决的都是在页面加载时尽量缩短白屏,给用户提供更好的页面切换体验。

在实际实现的过程中,Lavas根据渲染模式 (SPA 和 SSR) 的不同,分别使用两种不同的解决方案。在 SPA 模式下使用 Skeleton,在 SSR 模式下使用 App Shell。下面将分别讲述它们的配置和使用方式。

SPA 模式下的 Skeleton

如果您关注过构建后的 index.html 页面,应该发现 Skeleton 的内容会被直接注入到页面中。换言之,在页面的 HTML 结构中就已经包含了 Skeleton,后续渲染成功后再进行替换。如果配合 Service Worker,则可以将 HTML 文件缓存起来,在页面切换时快速展现,达成目标。

因此我们的思路是:把 index.html 添加到 Service Worker 的预缓存列表中,并且告知 Service Worker在请求某个页面时,从缓存中把 HTML 返回出来供前端直接展现。后续的加载和替换 Skeleton 由 js 继续走正常流程完成,就无需我们额外关心了。

为了实现思路,我们需要进行的操作有两个步骤:

  1. Skeleton 本身的编写
  2. 配置 Service Worker,添加预缓存文件列表

编写 Skeleton

Skeleton 的位置在 /core/Skeleton.vue

123456789101112131415<template>
    <div class="skeleton-wrapper">
        <!-- skeleton content -->
    </div>
</template>

<script>
export default {
    name: 'skeleton'
};
</script>

<style lang="stylus" scoped>
// skeleton content style rules
</style>

Skeleton.vue 按照普通 Vue 页面组件的开发方式进行开发即可。一般情况在 <template> 中我们会使用一些与实际页面布局和颜色接近的图片来充当 Skeleton 的内容,替代加载时的白屏。如果图片尺寸不太大的话,这里尤其推荐使用 base64 编码直接写入,避免再进行网络请求,保证离线可用。

因为是个完整的 Vue 组件,所以还需要包括 <script>, <style> 两部分。<script> 方面,一般 Skeleton 是静态的,所以并不需要什么额外的操作;而 <style> 则把 Skeleton 的内容样式编写完整即可。

添加预缓存文件列表

下一步我们把构建生成的 index.html 加到 Service Worker 的预缓存列表中去,参照 Lavas 中的 Service Worker 章节,在 /lavas.config.js 中的 serviceWorker 部分作如下配置:

1234567serviceWorker: {
    // ...
    globPatterns: [
        '**/*.{html,js,css,eot,svg,ttf,woff}'
    ],
    // ...
}

*.html 包括其中,即可让 index.html 包含在预缓存列表中了。

按照路由路径显示独立 Skeleton

之前我们展示了 SPA 下如何使用唯一的一个 Skeleton。但是在实际使用过程中,多个页面很难抽象出一个统一的 Skeleton,只能用类似 loading 的方式兼顾。

lavas-core-vue@1.0.6 支持 SPA 下每个按照不同路由路径展示不同 Skeleton,同时兼容老版本。所以如果想体验这一特性,可以升级已有 Lavas 模版项目中的 lavas-core-vue 依赖。

lavas.config.js 中新增配置项 skeleton,包含如下属性:

  • enable 布尔值,可以关闭整个 Skeleton 特性
  • routes 数组,路由路径和多个 Skeleton 的对应关系,每个对象如下:
    • path 必填,字符串或者正则,路由路径
    • componentPath 必填,Skeleton 组件路径
    • skeletonId 选填,默认按照 Skeleton 组件名生成 DOM 元素 id。可以指定以避免和页面已有元素 id 重复

例如使用如下配置,在构建时将向 HTML 中插入三个 Skeleton 内容,包含各自样式,DOM 结构以及一段按照路由显示对应 Skeleton 的 JS 代码。 实际运行时,访问 /test 将展示 TestSkeleton.vue 的内容,访问 /test2 将展示 TestSkeleton2.vue 的内容,其余路由将展示默认的 Skeleton.vue 内容。

12345678910111213141516171819// lavas.config.js

skeleton: {
    routes: [
        {
            path: '/test',
            skeletonId: 'my-skeleton',
            componentPath: 'core/TestSkeleton.vue'
        },
        {
            path: '/test2',
            componentPath: 'core/TestSkeleton2.vue'
        },
        {
            path: '*',
            componentPath: 'core/Skeleton.vue'
        }
    ]
}

加快 Skeleton 的渲染

如果我们打开 Chrome 的性能面板,观察 SPA 的时间线:

Skeleton 和 App Shell 模型

可以看出尽管已经将 Skeleton 所需的样式内联到了 HTML 中,HTML 解析完成后仍然不能立即开始渲染 Skeleton,必须等到样式表(index.css)加载之后才开始渲染。

样式表阻塞渲染在大部分场景下都是合理的,浏览器为了避免用户看到内容在样式加载前后的闪烁 (FOUC)。但是既然使用了 Skeleton,这样做就显得多余了。而且如果样式表体积很大,白屏时间将大大增加,Skeleton 并没有发挥理想中的作用。

为此,在 lavas-core-vue@1.1.5 中,我们使用了异步加载样式表加快 Skeleton 的展现。更多细节可查看 ISSUE,其中也包含了老版本模板的升级指导。

可以看到效果十分明显:

Skeleton 和 App Shell 模型

最后,如果想关闭这个特性,可以在 lavas.config.js 中:

12345// lavas.config.js

skeleton: {
    asyncCSS: false // 关闭异步加载样式表
}

SSR 模式的 App Shell

Skeleton 之所以没法在 SSR 模式下生效,原因主要有这么两个:

  1. 构建完成的最终目录中并不存在 index.html,所以无法将 Skeleton 加入其中,也无法将其添加到预缓存文件列表中
  2. 所有首屏请求都由服务端渲染,假设将这些返回都以 HTML 的形式存储到缓存中,那么随着 URL 的细微变化,缓存的数量是无穷的

我们虽然无法像 SPA 那样把 index.html 缓存起来,但我们可以把他们共同的外壳 (App Shell) 剥离成独立路由,由 Service Worker 请求并缓存。之后拦截每次 HTML 请求都返回这个外壳,再进行前端渲染,就可以实现和 Skeleton 相同的效果了。

Skeleton 和 App Shell 模型

在这种模式下,只要缓存中存在这个外壳,程序就没有请求服务端 HTML 的必要,只需要请求 API 接口获取数据即可。(如果开发者的应用对于 API 的实时性要求不高,甚至 API 请求也可以进行缓存。) 无论 API 如何处理,对服务端来说,使用 Vue SSR 流程处理 HTML 请求仅限于浏览器缓存中不存在外壳时,也就是第一次访问时。后续访问即便是刷新页面,因为外壳的存在也不会再走 SSR 流程了,这和传统的 SSR 模式是不同的。

关于这个模式,Lavas 在知乎专栏的文章 SSR 架构项目实现离线可用(思路&案例) 和 在 Vue SSR 中使用 Service Worker 有更详细的介绍。

实现方式方面,和 Skeleton 模式类似,我们同样需要完成两个步骤:

  1. App Shell 本身的编写
  2. 配置 Service Worker,添加预缓存文件列表

编写 App Shell

首先我们需要开发一个 App Shell。我们创建一个 /pages/Appshell.vue,并编写如下内容:

123456789101112131415161718<template>
</template>

<script>
export default {
    name: 'appshell',
    metaInfo: {
        title: 'Lavas',
        meta: [
            {name: 'keywords', content: 'lavas PWA'},
            {name: 'description', content: '基于 Vue 的 PWA 解决方案,帮助开发者快速搭建 PWA 应用,解决接入 PWA 的各种问题'}
        ],
        bodyAttrs: {
            'empty-appshell': undefined
        }
    }
};
</script>

metaInfo 是 Lavas 内部使用的 vue-meta 的默认配置 key,用来传递各类页面元信息,开发者可以根据项目的具体情况自行修改。其中最重要的 empty-appshell 虽然值是 undefined,但因为 Lavas 的内部判断机制,不能修改或者删除,否则将导致 App Shell 无法工作。

和 Skeleton 不同,App Shell 不需要编写内容和样式,而是直接复用全局框架的样式 (/core/App.vue),因此这里不需要 <style><template> 内容也直接留空就可以。

配置 Service Worker

和 Skeleton 不同,这里要添加的预缓存并不是一个独立的文件,而是一个路由 (如 /appshell)。我们使用 /lavas.config.jsserviceWorker 段的 appshellUrl 配置项先进行声明,之后才可以正常使用。

123456// ...
serviceWorker: {
    // swSrc, swDest, globDirectory, globPatterns, globIgnores, dontCacheBustUrlsMatching..
    appshellUrl: '/appshell'
}
// ...

多个 App Shell 的支持 (扩展)

默认情况 Lavas 会帮助开发者完成一个 App Shell 的注册。如果开发者有需求要支持多个 App Shell,需要额外进行一些操作。

相比单个 App Shell 的开发,多个 App Shell 的开发步骤有三个:

  1. 多个 App Shell 本身的编写 和单个 App Shell 基本相同,不再复述。
  2. 配置 Service Worker,添加预缓存文件列表 appshellUrl 参数 只需要 填写适用面最广的 App Shell 路径。 举例来说,存在两个 App Shell,A 适用于 /user 开头的路由,B 适用于剩余的路由。那么 B 就是适用面广的 App Shell,它的访问地址 (如 /appshell/B ) 应该被填入 appshellUrl
  3. 编写 Service Worker 模板

Service Worker 模板位于 /core/service-worker.js,注册 App Shell 我们需要使用 WorkBox 的 registerNavigationRoute 方法,如下:

123workboxSW.router.registerNavigationRoute('/appshell/B', {
    whitelist: /^\/user/
});

registerNavigationRoute 方法是 WorkBox 提供的一个快捷方法 (API),它的作用是在 HTML 请求 (request.mode === 'navigate') 时使用参数内容 (/appshell/B) 作为响应返回,而不真正发起网络请求。因此现在所有页面加载之前都会由 Service Worker 返回 index.html,而其中包含的 Skeleton 则替代了白屏,成为提升体验的关键。

那为什么单个 App Shell 时不需要编写这句代码呢?

很简单,因为 Lavas 帮助开发者自动生成了这句代码。Lavas 获取 appshellUrl 参数并自动生成调用,如下:

1workboxSW.router.registerNavigationRoute('/appshell/A')

综上,开发者只需要把普适的 /appshell/A 编写在 appshellUrl 配置项中,再自行编写 /appshell/B 的注册即可。

Skeleton 和 App Shell 的差异 (扩展)

提示:这部分内容由 Lavas 内部处理,并不需要开发者进行参与,仅仅作为解答开发者疑问的扩展阅读存在。

除了渲染模式不同之外,开发者是否存在一个疑问:为什么 Skeleton 需要把内容和样式都编写在内,而 App Shell 直接留空即可?

这和 Lavas 内部的构建机制以及 Vue 渲染页面有关。

  • Skeleton Skeleton 虽然在源代码中以 Vue 页面组件存在,但在实际构建过程中通过内置的 vue-skeleton-webpack-plugin 将内容抽取出来,填入构建生成的 index.html 中。因此在构建完成的项目中并不能看到 Skeleton 单独存在,而是作为 HTML 的一部分。 12345678<!-- dist/index.html --> <div id="app"> <div class="skeleton-wrapper" data-v-xxxxx> <!-- skeleton content --> </div> </div> <script src="xxx"> <script src="xxx"> Vue 在实际渲染的时候,在渲染之前先展现 HTML 本来的内容,于是 Skeleton 被一并展现了;当渲染完成后,通过 app.$mount('#app') 将结果替换掉 #app 内本来的内容 (Skeleton),表现为真实内容替换了 Skeleton。所以从这个角度不难理解,Skeleton 一定需要包含头尾等元素。
  • App Shell 和 Skeleton 相同,App Shell 也以 Vue 页面组件存在于源码。但因为处于 /pages 目录下,因此不但拥有单独路由,编译过后也能以独立文件存在。 在 Vue 渲染时,它作为 App.vue<router-view> 内部的内容被替换,而头尾是在 App.vue中,<router-view> 之外的,因此 App Shell 不需要包含头尾。而因为页面未加载,数据请求也未发送,所以 App Shell 通常就是空的。
Lavas

Lavas 是一套基于 Vue 的 PWA 解决方案,能够帮助开发者快速搭建 PWA 应用,解决接入 PWA 的各种问题,对提升用户体验,用户留存率等有明显提升,且开发者无须过多的关注 PWA 开发本身。

Lavas目录

1.基础教程
2.进阶教程
3.webpack 相关
4.更多