vue-skeleton-webpack-plugin 介绍

info

这是一个基于 vue 的 webpack 插件,为单页和多页应用生成 skeleton,提升首屏展示体验。

如果您还不了解 skeleton,可以参考App Skeleton 介绍一文。

github 地址:https://github.com/lavas-project/vue-skeleton-webpack-plugin

问题背景

参考饿了么的 PWA 升级实践一文,我们希望在构建时渲染 skeleton 组件,将渲染 DOM 插入 html 的挂载点中,同时将使用的样式通过 style 标签内联。这样在前端 JS 渲染完成之前,用户将看到页面的大致骨架,感知到页面是正在加载的。

我们当然可以选择在开发时直接将页面骨架内容写入 html 模版中,但是这会带来两个问题:

  1. 开发 skeleton 与其他组件体验不一致。
  2. 多页应用中多个页面可能共用同一个 html 模版,而又有独立的 skeleton。

下面我们将看看插件在具体实现中是如何解决这两个问题的。

实现思路

我们希望能够保证一致的开发体验,开发 skeleton 和其他组件没有任何不同。而且开发者不需要关心渲染结果是如何被注入 html 的。
下面我们将从渲染和注入这两方面展开介绍。

渲染组件

我们使用 vue 的服务端渲染功能,接受 webpack 配置对象作为输入,输出渲染的 DOM 和样式。

一个典型的用于服务端渲染的 webpack 配置对象如下,其中 entry 入口文件中使用了 skeleton 组件:

  1. {
  2. target: 'node',
  3. devtool: false,
  4. entry: resolve('./src/entry-skeleton.js'), // 多页应用中传入数组
  5. output: Object.assign({}, baseWebpackConfig.output, {
  6. libraryTarget: 'commonjs2'
  7. }),
  8. externals: nodeExternals({
  9. whitelist: /\.css$/
  10. }),
  11. plugins: []
  12. }

info

多页中的 webpack 配置对象示例,可参考多页测试用例或者Lavas MPA 模版

webpack 将使用传入的配置对象进行编译,由于我们不需要将最终产物保存在硬盘中,使用内存文件系统memory-fs能够减少不必要的I/O开销。最终会生成一个 bundle 文件,使用createBundleRenderer创建一个 renderer,就可以在 Node.js 环境得到渲染结果了。

  1. const createBundleRenderer = require('vue-server-renderer').createBundleRenderer;
  2. let bundle = mfs.readFileSync(outputPath, 'utf-8');
  3. // 创建 renderer
  4. let renderer = createBundleRenderer(bundle);
  5. // 渲染得到 html
  6. renderer.renderToString({}, (err, skeletonHtml) => {
  7. if (err) {
  8. reject(err);
  9. }
  10. else {
  11. resolve({skeletonHtml, skeletonCss});
  12. }
  13. });

另外,为了将样式从 JS 文件中分离,我们使用了 ExtractTextPlugin插件,将样式内容输出到单独的文件中。

  1. // vue-skeleton-webpack-plugin/src/ssr.js
  2. // 加入 ExtractTextPlugin 插件
  3. serverWebpackConfig.plugins.push(new ExtractTextPlugin({
  4. filename: outputCssBasename
  5. }));

至此,我们已经得到了全部渲染结果,剩下的就是注入时机了。

注入渲染结果

关于渲染结果的注入时机,我们参考html-webpack-plugin的事件说明,选择在html-webpack-plugin-before-html-processing事件回调函数中进行。

渲染结果中包含 DOM 结构和样式两部分,样式可以直接插入</head>之前,而 DOM 的插入与挂载点相关,默认使用<div id="app">,当然插件使用者可以通过参数传入。

在多页应用中,相比单页情况会变的稍稍复杂。多页项目中通常会引入多个 html-webpack-plugin,例如我们在Lavas MPA 模版中使用的 multipage插件就是如此,这就会导致html-webpack-plugin-before-html-processing事件被多次触发。我们需要在每次事件触发时识别出当前处理的入口文件,执行 webpack 编译当前页面对应的入口文件,渲染对应的 skeleton 组件。

查找当前处理的入口文件过程如下:

  1. // vue-skeleton-webpack-plugin/src/index.js
  2. // 当前页面使用的所有 chunks
  3. let usedChunks = htmlPluginData.plugin.options.chunks;
  4. let entryKey;
  5. // chunks 和所有入口文件的交集就是当前待处理的入口文件
  6. if (Array.isArray(usedChunks)) {
  7. entryKey = Object.keys(skeletonEntries);
  8. entryKey = entryKey.filter(v => usedChunks.indexOf(v) > -1)[0];
  9. }
  10. // 设置当前的 webpack 配置对象的入口文件和结果输出文件
  11. webpackConfig.entry = skeletonEntries[entryKey];
  12. webpackConfig.output.filename = `skeleton-${entryKey}.js`;
  13. // 使用配置对象进行服务端渲染
  14. ssr(webpackConfig).then(({skeletonHtml, skeletonCss}) => {});

开发模式下插入路由

由于 skeleton 的渲染结果在 JS 前端渲染完成后就会被替换,如何在开发时方便的查看呢?
使用浏览器开发工具设置断点,阻塞前端渲染可以做到,但如果能在开发模式中插入 skeleton 对应的路由规则,使多个页面的 skeleton 能像其他路由组件一样被访问,将使开发调试变得更加方便。

向路由文件中注入代码的工作将在 loader中完成。

首先明确注入内容,我们希望通过路由组件的形式访问 skeleton,那么首先需要引入各个 skeleton 组件,然后增加对应的路由规则。具体到注入的代码,类似这样:

  1. // router.js
  2. // 引入 skeleton 组件
  3. import Skeleton from '@/pages/Skeleton.vue'
  4. // 插入routes
  5. routes: [
  6. {
  7. path: '/skeleton',
  8. name: 'skeleton',
  9. component: Skeleton
  10. }
  11. // ...其余路由规则
  12. ]

在多页应用中,使用者可以通过占位符设置依赖语句和路由规则的模版,loader 在运行时会使用这些模版,用真实的 skeleton 名称替换掉占位符,插入多条语句

info

多页中的具体应用示例,可参考多页测试用例或者Lavas MPA 模版

参数说明

插件和 loader 使用的参数如下:

SkeletonWebpackPlugin

  • webpackConfig 必填,渲染 skeleton 的 webpack 配置对象
  • insertAfter 选填,渲染 DOM 结果插入位置,默认值为'<div id="app">'

SkeletonWebpackPlugin.loader

参数分为两类:

  1. webpack模块规则,skeleton 对应的路由将被插入路由文件中,所以需要指定一个或多个路由文件,使用resource/include/test皆可指定 loader 应用的文件。
  2. options 将被传入 loader 中的参数对象,包含以下属性:
    • entry 必填,支持字符串和数组类型,对应页面入口的名称
    • importTemplate 选填,引入 skeleton 组件的表达式,默认值为'import [nameCap] from \'@/pages/[nameCap].vue\';'
    • routePathTemplate 选填,路由路径,默认值为'/skeleton-[name]'
    • insertAfter 选填,路由插入位置,默认值为'routes: ['

importTemplateroutePathTemplate中可以使用以下占位符:

  • [name]entry保持一致
  • [nameCap] entry首字母大写

例如使用以下配置,将向路由文件router.js中插入'import Page1 from \'@/pages/Page1.vue\';''import Page2 from \'@/pages/Page2.vue\';'两条语句。
同时生成/skeleton-page1/skeleton-page2两条路由规则。

  1. {
  2. resource: 'router.js',
  3. options: {
  4. entry: ['page1', 'page2'],
  5. importTemplate: 'import [nameCap] from \'@/pages/[nameCap].vue\';',
  6. routePathTemplate: '/skeleton-[name]'
  7. }
  8. }

更多详细说明可参考 github上插件的参数说明部分

贡献代码

在开发中遇到任何问题,都欢迎提出 ISSUE讨论。

您也可以帮助我们完善测试用例