应用场景
简单来说, 骨架屏(skeleton screen) 就是一个页面从 html 下载完成到 JS 下载完成并且执行数据渲染这两个时间点之间暂时渲染页面基本结构的方案.
就我的理解, 骨架屏优化是有一定场景的, 包括且不限于以下几种情况:
有懒加载机制的 SPA 路由
多页面程序的首页渲染
SPA 中的非懒加载路由, 但是数据量很大, 完全 load 并渲染数据需要花较长时间
上图形象地解释了两个多页面程序之间的切换, 用 Skeleton Screen 去优化用户观感的方案.
Skeleton 渲染是在前端项目编译时完成的, 与之相对应的是组件在浏览器 runtime 实时渲染成 DOM, 从技术上讲就是在服务器端预先把组件布局和数据渲染成 HTML 字符串并且注入 HTML 文件中. 这需要服务器端渲染 (ssr) 的支持. 如 react 和 vue.js 这样依赖虚拟 DOM 的前端框架天然是支持服务器端渲染的. 因为只需要一个 JavaScript 在服务器端的执行环境(例如 V8 引擎) 就可以轻易创建虚拟 Dom 并且映射为 DOM tree. 基于虚拟 dom 的服务器端渲染, 最早起源于 react, 可以参考 strikingly 的技术博客
服务器端渲染
服务器端渲染作为 skeleton screen 的基本技术栈, 可以参考各前端框架的官方文档, 例如 vue-ssr guide https://ssr.vuejs.org/zh/guide/ . 主要是依赖 https://registry.npmjs.org/vue-server-renderer 这个库来实现
拿 vue.JS 组件来说, 在 node.JS 中渲染为 HTML 字符串可以简单分为几步:
- // 第 1 步: 创建一个 Vue 实例
- const Vue = require('vue')
- const App = new Vue({
- // el: 是不需要的, 因为一旦设置目标 el, 就会涉及 document 操作
- template: `<div>Hello World</div>`
- })
- // 第 2 步: 创建一个 renderer
- const renderer = require('vue-server-renderer').createRenderer()
- // 第 3 步: 将 Vue 实例渲染为 HTML
- renderer.renderToString(App, (err, HTML) => {
- if (err) throw err
- console.log(HTML)
- // => <div data-server-rendered="true">Hello World</div>
- })
上面的官方 DEMO 很简单, 直接在 Node.JS 脚本文件中定义 vue 组件. 但实际应用中我们的组件一般都比较复杂, 可能是一个入口 App.vue, 依赖了好几个组件, 所以要用另外的方式去引入 new Vue 产生的 vm 对象. 这个 vm 对象作为 renderer.renderToString 函数的第一个参数是最核心的. 实际上通过跟踪 vue-server-renderer 的源码, ssr 还是依靠 vue 组件的 render 函数来完成大部分工作 ,render 函数是 vue 组件的核心函数.
- | installSSRHelpers(component);
- renderToString -> createRenderFunction -> | normalizeRender(component); // get component render function
- | renderNode(component._render(), true, context);
- // call component render to VNode.
几个坑
document not defined
服务器端渲染并不等同于在服务器端启动一个浏览器进程, 所以无法获取浏览器窗体中的 Windows,document 等全局对象. 一旦服务器端渲染的 JS 脚本中涉及到 document 操作, 就会报这个 document not defined 的错误.
所以要么就在 vue 组件中或者所有依赖包中用到 document 的地方都要做检测, 要么就在服务器端渲染之前给 global 对象加上 document 定义 GitHub issue
unexpected token =
在 vue-server-renderer 的执行过程中, 提供了把 vue 组件渲染成 HTML 片段并且插入某个 template HTML 中的 API, 在创建 Renderer 的时候传入一个 template. 后来发现 ssr 的 template 中是不能包含类似这种 <%= ddd> 符号的, 也就是 template 必须得符合 vue 的 template 语法.
- // 最后 ssr 就会输出 Vue 组件的内容, 并且注入到 template 中 注释 <!--vue-ssr-outlet--> 的地方
- const renderer = createRenderer({
- template: require('fs').readFileSync('./index.template.html', 'utf-8')
- })
- const context = { title: 'Hello' }
- renderer.renderToString(App, context, (err, HTML) => {
- // 页面模板中 {{ title }} 将会是 "Hello"
- })
具体参考 ssr 使用页面模板. 因为有的后端程序员习惯了 asp 或者 jsp 的 template 语法, 以 <%= 等作为数据插值表达式的开头, 会引起 vue ssr 的编译失败.
写在后面
综上, 这次我只是简单分析记录了下 Skeleton 骨架屏实现的第一部分, 就是 server side render, 这是预先渲染的技术前提. 实际上, 这种后端拼接 HTML 字符串的活在 PHP 年代, python server 中, 甚至 node.JS server 中我们早就干过了(ejs 或者 jade). 本质上都是拼接 HTML 字符串, 提高 SEO 和首屏响应.
在 GitHub 上有许多基于服务器端渲染的静态网站生成器或者 博客工具, 可以实现比较流畅的浏览体验, 例如: https://github.com/vuejs/vuepress .
关于骨架屏的实现, 业内已经有很成熟的方案, 既有基于 ssr 的, 也有直接基于浏览器内核起个进程预渲染的, 完全不用 vue 的 ssr 技术栈. 下一次再具体分享下后者
参考阅读:
vue ssr guide https://v1.vuepress.vuejs.org
饿了么升级 PWA
vue.JS 开发系列二: render 函数 https://www.jianshu.com/p/1093f2d1f9ec
来源: https://juejin.im/post/5c7bab75518825629b4310a4