diff --git a/README.md b/README.md index e9b4dfd..9c65b9e 100644 --- a/README.md +++ b/README.md @@ -6,15 +6,17 @@ vue-skeleton-webpack-plugin [![NPM](https://nodei.co/npm/vue-skeleton-webpack-plugin.png?downloads=true&downloadRank=true&stars=true)](https://nodei.co/npm/vue-skeleton-webpack-plugin/) -基于 vue 的 webpack 插件,为单页/多页应用生成 skeleton,提升首屏展示体验。 +这是一个基于 Vue 的 webpack 插件,为单页/多页应用生成骨架屏 skeleton,减少白屏时间,在页面完全渲染之前提升用户感知体验。 ## 基本实现 -参考了[Ele.me的这篇文章](https://medium.com/elemefe/upgrading-ele-me-to-progressive-web-app-2a446832e509), +参考了[饿了么的 PWA 升级实践](https://huangxuan.me/2017/07/12/upgrading-eleme-to-pwa/)一文, 使用服务端渲染在构建时渲染 skeleton 组件,将 DOM 和样式内联到最终输出的 html 中。 另外,为了开发时调试方便,会将对应路由写入`router.js`中,可通过`/skeleton`路由访问。 +插件具体实现可参考[我的这篇文章](https://xiaoiver.github.io/coding/2017/07/30/%E4%B8%BAvue%E9%A1%B9%E7%9B%AE%E6%B7%BB%E5%8A%A0%E9%AA%A8%E6%9E%B6%E5%B1%8F.html) + ## 使用方法 安装: @@ -22,6 +24,11 @@ vue-skeleton-webpack-plugin npm install vue-skeleton-webpack-plugin ``` +运行测试用例: +```bash +npm run test +``` + 在 webpack 中引入插件: ```js // webpack.conf.js diff --git a/src/index.js b/src/index.js index 7ae9864..6c76d44 100644 --- a/src/index.js +++ b/src/index.js @@ -13,12 +13,6 @@ const DEFAULT_PLUGIN_OPTIONS = { insertAfter: '
' }; -const DEFAULT_LOADER_OPTIONS = { - importTemplate: 'import [nameCap] from \'@/pages/[nameCap].vue\';', - routePathTemplate: '/skeleton-[name]', - insertAfter: 'routes: [' -}; - class SkeletonPlugin { constructor(options = {}) { @@ -43,10 +37,12 @@ class SkeletonPlugin { compiler.plugin('compilation', compilation => { + // add listener for html-webpack-plugin compilation.plugin('html-webpack-plugin-before-html-processing', (htmlPluginData, callback) => { let usedChunks = htmlPluginData.plugin.options.chunks; let entryKey; + // find current processing entry if (Array.isArray(usedChunks)) { entryKey = Object.keys(skeletonEntries); @@ -56,6 +52,7 @@ class SkeletonPlugin { entryKey = 'app'; } + // set current entry & output in webpack config webpackConfig.entry = skeletonEntries[entryKey]; webpackConfig.output.filename = `skeleton-${entryKey}.js`; @@ -76,8 +73,7 @@ class SkeletonPlugin { static loader(ruleOptions = {}) { return Object.assign(ruleOptions, { loader: require.resolve('./loader'), - options: Object.assign({}, DEFAULT_LOADER_OPTIONS, - Object.assign({}, ruleOptions.options)) + options: Object.assign({}, ruleOptions.options) }); } } diff --git a/src/loader.js b/src/loader.js index 6d58d88..1028068 100644 --- a/src/loader.js +++ b/src/loader.js @@ -1,5 +1,7 @@ /** * @file loader + * @desc Insert route of skeleton into router.js, so that developer can + * visit route path in dev mode to debug skeleton components * @author panyuqi */ @@ -8,34 +10,47 @@ const loaderUtils = require('loader-utils'); const insertAt = require('./util').insertAt; +const DEFAULT_LOADER_OPTIONS = { + // template of importing skeleton component + importTemplate: 'import [nameCap] from \'@/pages/[nameCap].vue\';', + // template of route path + routePathTemplate: '/skeleton-[name]', + // position to insert route object in router.js file + insertAfter: 'routes: [' +}; + const ENTRY_NAME_HOLDER = /\[name\]/gi; const ENTRY_NAME_CAP_HOLDER = /\[nameCap\]/gi; module.exports = function (source) { - const options = loaderUtils.getOptions(this); + const options = Object.assign({}, DEFAULT_LOADER_OPTIONS, loaderUtils.getOptions(this)); let {entry, importTemplate, routePathTemplate, insertAfter} = options; - // position to insert in router.js + // find position to insert in router.js let routesPos = source.indexOf(insertAfter) + insertAfter.length; - if (!Array.isArray(entry)) { - entry = [entry]; - } + entry = Array.isArray(entry) ? entry : [entry]; entry.forEach(entryName => { - // capitalize first letter + // capitalize first letter in entryName eg.skeleton -> Skeleton let entryCap = entryName.replace(/([a-z])(.*)/, (w, firstLetter, rest) => firstLetter.toUpperCase() + rest); - // route path - let skeletonRoutePath = routePathTemplate.replace(ENTRY_NAME_HOLDER, entryName) - .replace(ENTRY_NAME_CAP_HOLDER, entryCap); - let importExpression = importTemplate.replace(ENTRY_NAME_HOLDER, entryName) - .replace(ENTRY_NAME_CAP_HOLDER, entryCap); + + // replace placeholder in routeTpl and importTpl + let [skeletonRoutePath, importExpression] = [routePathTemplate, importTemplate] + .map(pathStr => pathStr.replace(ENTRY_NAME_HOLDER, entryName) + .replace(ENTRY_NAME_CAP_HOLDER, entryCap)); + + // route object to insert let routeExpression = `{ path: '${skeletonRoutePath}', name: '${entryName}-skeleton', component: ${entryCap} },`; + + // insert route object into routes array source = insertAt(source, routeExpression, routesPos); + + // insert import sentence in the head source += importExpression; }); diff --git a/src/ssr.js b/src/ssr.js index d32ee08..2adbc89 100644 --- a/src/ssr.js +++ b/src/ssr.js @@ -1,5 +1,6 @@ /** * @file ssr + * @desc Use vue ssr to render skeleton components. The result contains html and css. * @author panyuqi */ @@ -20,7 +21,7 @@ module.exports = serverWebpackConfig => new Promise((resolve, reject) => { console.log(`Generate skeleton for ${outputBasename}...`); - // extract css + // extract css into a single file serverWebpackConfig.plugins.push(new ExtractTextPlugin({ filename: outputCssBasename })); @@ -47,8 +48,9 @@ module.exports = serverWebpackConfig => new Promise((resolve, reject) => { let bundle = mfs.readFileSync(outputPath, 'utf-8'); let skeletonCss = mfs.readFileSync(outputCssPath, 'utf-8'); + // create renderer with bundle let renderer = createBundleRenderer(bundle); - // ssr skeleton + // use vue ssr to render skeleton renderer.renderToString({}, (err, skeletonHtml) => { if (err) { reject(err);