vue-skeleton-webpack-plugin 介绍

vue-skeleton-webpack-plugin 介绍

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

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

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

问题背景

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

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

  • 开发 skeleton 与其他组件体验不一致。

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

实现思路

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

渲染组件

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

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

123456789101112{ target: 'node', devtool: false, entry: resolve('./src/entry-skeleton.js'), // 多页应用中传入数组 output: Object.assign{}, baseWebpackConfig.output, { libraryTarget: 'commonjs2' }), externals: nodeExternals{ whitelist: /\.css$/ }), plugins: [] }

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

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

12345678910111213const createBundleRenderer = require('vue-server-renderer').createBundleRenderer; let bundle = mfs.readFileSync(outputPath, 'utf-8' // 创建 renderer let renderer = createBundleRenderer(bundle // 渲染得到 html renderer.renderToString{}, (err, skeletonHtml) => { if (err) { reject(err } else { resolve{skeletonHtml, skeletonCss} } }

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

123456// vue-skeleton-webpack-plugin/src/ssr.js // 加入 ExtractTextPlugin 插件 serverWebpackConfig.plugins.push(new ExtractTextPlugin{ filename: outputCssBasename })

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

注入渲染结果

关于渲染结果的注入时机,我们参考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 组件。

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

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

开发模式下插入路由

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

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

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

1234567891011121314// router.js // 引入 skeleton 组件 import Skeleton from '@/pages/Skeleton.vue' // 插入routes routes: [ { path: '/skeleton', name: 'skeleton', component: Skeleton } // ...其余路由规则 ]

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

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

参数说明

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

SkeletonWebpackPlugin

  • webpackConfig 必填,渲染 skeleton 的 webpack 配置对象

SkeletonWebpackPlugin.loader

参数分为两类:

  • webpack模块规则,skeleton 对应的路由将被插入路由文件中,所以需要指定一个或多个路由文件,使用resource/include/test皆可指定 loader 应用的文件。

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

  • [name]entry保持一致

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

12345678{ resource: 'router.js', options: { entry: ['page1', 'page2'], importTemplate: 'import [nameCap] from \'@/pages/[nameCap].vue\';', routePathTemplate: '/skeleton-[name]' } }

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

贡献代码

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

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