├── .babelrc ├── .fecsignore ├── .gitignore ├── .postcssrc.js ├── .travis.yml ├── CHANGELOG.md ├── README.md ├── examples ├── index.js ├── multi-skeleton │ ├── index.html │ ├── src │ │ ├── Skeleton1.vue │ │ ├── Skeleton2.vue │ │ ├── entry-skeleton.js │ │ └── entry.js │ ├── utils.js │ ├── vue-loader.conf.js │ ├── webpack.base.conf.js │ └── webpack.config.js ├── multipage │ ├── README.md │ ├── index.html │ ├── src │ │ └── pages │ │ │ ├── page1 │ │ │ ├── Page1.skeleton.vue │ │ │ ├── entry-skeleton.js │ │ │ └── entry.js │ │ │ └── page2 │ │ │ ├── Page2.skeleton.vue │ │ │ ├── entry-skeleton.js │ │ │ └── entry.js │ ├── utils.js │ ├── vue-loader.conf.js │ ├── webpack.base.conf.js │ └── webpack.config.js ├── multipage2 │ ├── README.md │ ├── src │ │ └── pages │ │ │ ├── page1 │ │ │ ├── Page1.skeleton.vue │ │ │ ├── entry-skeleton.js │ │ │ ├── entry.js │ │ │ └── index.html │ │ │ └── page2 │ │ │ ├── Page2.skeleton.vue │ │ │ ├── entry-skeleton.js │ │ │ ├── entry.js │ │ │ └── index.html │ ├── utils.js │ ├── vue-loader.conf.js │ ├── webpack.base.conf.js │ └── webpack.config.js ├── multipage3 │ ├── README.md │ ├── src │ │ └── pages │ │ │ ├── page1 │ │ │ ├── Page1.skeleton.vue │ │ │ ├── entry-skeleton.js │ │ │ ├── entry.js │ │ │ └── index.html │ │ │ └── page2 │ │ │ ├── entry.js │ │ │ └── index.html │ ├── utils.js │ ├── vue-loader.conf.js │ ├── webpack.base.conf.js │ └── webpack.config.js ├── simple │ ├── .browserslistrc │ ├── .postcssrc.js │ ├── index.html │ ├── src │ │ ├── Skeleton.vue │ │ ├── entry-skeleton.js │ │ ├── entry.js │ │ ├── logo.jpg │ │ └── main.css │ ├── utils.js │ ├── vue-loader.conf.js │ ├── webpack.base.conf.js │ └── webpack.config.js ├── vue-cli3 │ ├── .browserslistrc │ ├── .gitignore │ ├── package.json │ ├── postcss.config.js │ ├── public │ │ ├── favicon.ico │ │ └── index.html │ ├── src │ │ ├── App.vue │ │ ├── Skeleton.vue │ │ ├── assets │ │ │ └── logo.png │ │ ├── components │ │ │ └── HelloWorld.vue │ │ ├── main.js │ │ └── skeleton.js │ └── vue.config.js └── webpack4 │ ├── .browserslistrc │ ├── .postcssrc.js │ ├── index.html │ ├── src │ ├── Skeleton.vue │ ├── entry-skeleton.js │ ├── entry.js │ ├── logo.jpg │ └── main.css │ ├── utils.js │ ├── webpack.base.conf.js │ └── webpack.config.js ├── package.json ├── src ├── index.js ├── loader.js ├── ssr.js └── util.js └── test ├── loader.test.js ├── multi-skeleton.test.js ├── multipage.test.js ├── multipage2.test.js ├── multipage3.test.js ├── simple.test.js ├── utils.js └── webpack4.test.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "targets": { 7 | "node": 4 8 | } 9 | } 10 | ] 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.fecsignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | lib 4 | npm-debug.log* 5 | coverage 6 | .nyc_output 7 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | 'autoprefixer': {} 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - stable 4 | - lts/* 5 | - 6.9 6 | env: 7 | - WEBPACK_VERSION=3 VUE_LOADER_VERSION=^14.0.0 8 | - WEBPACK_VERSION=4 VUE_LOADER_VERSION=^15.0.0 9 | install: 10 | - npm install --ignore-scripts 11 | - npm rm webpack 12 | - npm rm vue-loader 13 | - npm install webpack@$WEBPACK_VERSION vue-loader@$VUE_LOADER_VERSION --ignore-scripts || true 14 | script: 15 | - npm test -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Change History 2 | ============== 3 | 4 | ======= 5 | v1.2.2 6 | --- 7 | * [Feature] `insertAfter` 支持传入自定义 `Function` [ISSUE#26](https://github.com/lavas-project/vue-skeleton-webpack-plugin/issues/26) 8 | 9 | v1.2.1 10 | --- 11 | * [Feature] 支持 hot reload, 原有 `SkeletonWebpackPlugin.loader` 已废弃 12 | * [Fix] #38 13 | 14 | v1.1.18 15 | --- 16 | * [Feature] 支持 Webpack 4 #13 17 | * [Fix] #23 18 | 19 | v1.1.17 20 | --- 21 | * [Fix] 修复 cssExtract 带来的问题。 22 | 23 | v1.1.16 24 | --- 25 | * [Fix] 紧急修复 `entry` 传入数组导致编译失败的问题。 26 | 27 | v1.1.14 28 | --- 29 | * [Feature] 简化 `webpackConfig` 配置项,不需要提供完整的 webpack 配置对象,只需要包含 `entry` 指向 `entry-skeleton.js` 即可。 30 | 31 | v1.1.12 32 | --- 33 | * [Fix] [ISSUE#15](https://github.com/lavas-project/vue-skeleton-webpack-plugin/issues/15) 34 | 35 | v1.1.9 36 | --- 37 | * [Feature] 支持 SPA 下多个路由路径拥有各自的 Skeleton 38 | 39 | v1.1.8 40 | --- 41 | * [Fix] 针对`entry`名称中包含连字符的情况,增加`[nameHash]`占位符 42 | 43 | v1.1.7 44 | --- 45 | * [Fix] 由于不是所有编辑器都在文件末尾自动添加空行。Loader 为路由文件添加 import 语句时插入换行符。[ISSUE#5](https://github.com/lavas-project/vue-skeleton-webpack-plugin/issues/5) 46 | 47 | v1.1.6 48 | --- 49 | * [Feature] 支持新参数 quiet,在服务端渲染时是否需要输出信息到控制台 50 | * [Fix] 服务端渲染组件完成后,自动关闭 webpack 对内存文件系统的监听 51 | 52 | v1.1.4 53 | --- 54 | * 支持 webpack 3.0.0 以上,修复了不填写 entry 名称导致的 bug 55 | 56 | v1.1.2 57 | --- 58 | * 增加多页应用开发模式下,自动插入各个页面skeleton对应的路由 59 | * 增加loader多种参数配置 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | vue-skeleton-webpack-plugin 2 | =================== 3 | 4 | [![npm version](https://badge.fury.io/js/vue-skeleton-webpack-plugin.svg)](https://badge.fury.io/js/vue-skeleton-webpack-plugin) 5 | [![Build Status](https://travis-ci.org/lavas-project/vue-skeleton-webpack-plugin.svg?branch=master)](https://travis-ci.org/lavas-project/vue-skeleton-webpack-plugin) 6 | 7 | [![NPM](https://nodei.co/npm/vue-skeleton-webpack-plugin.png?downloads=true&downloadRank=true&stars=true)](https://nodei.co/npm/vue-skeleton-webpack-plugin/) 8 | 9 | 这是一个基于 Vue 的 webpack 插件,为单页/多页应用生成骨架屏 skeleton,减少白屏时间,在页面完全渲染之前提升用户感知体验。 10 | 11 | 支持 webpack@3 和 webpack@4,支持 Hot reload。 12 | 13 | ## 基本实现 14 | 15 | 参考了[饿了么的 PWA 升级实践](https://huangxuan.me/2017/07/12/upgrading-eleme-to-pwa/)一文, 16 | 使用服务端渲染在构建时渲染 skeleton 组件,将 DOM 和样式内联到最终输出的 html 中。 17 | 18 | 另外,为了开发时调试方便,会将对应路由写入`router.js`中,可通过`/skeleton`路由访问。 19 | 20 | 插件具体实现可参考[我的这篇文章](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) 21 | 22 | ## 使用方法 23 | 24 | 安装: 25 | ```bash 26 | npm install vue-skeleton-webpack-plugin 27 | ``` 28 | 29 | 运行测试用例: 30 | ```bash 31 | npm run test 32 | ``` 33 | 34 | 在 webpack 中引入插件: 35 | ```js 36 | // webpack.conf.js 37 | import SkeletonWebpackPlugin from 'vue-skeleton-webpack-plugin'; 38 | 39 | plugins: [ 40 | new SkeletonWebpackPlugin({ 41 | webpackConfig: { 42 | entry: { 43 | app: resolve('./src/entry-skeleton.js') 44 | } 45 | } 46 | }) 47 | ] 48 | ``` 49 | 50 | ## 参数说明 51 | 52 | ### SkeletonWebpackPlugin 53 | 54 | - webpackConfig *必填*,渲染 skeleton 的 webpack 配置对象 55 | - insertAfter *选填*,渲染 DOM 结果插入位置,默认值为字符串 `'
'` 56 | - 也可以传入 `Function`,方法签名为 `insertAfter(entryKey: string): string`,返回值为挂载点字符串 57 | - quiet *选填*,在服务端渲染时是否需要输出信息到控制台 58 | - router *选填* SPA 下配置各个路由路径对应的 Skeleton 59 | - mode *选填* 路由模式,两个有效值 `history|hash` 60 | - routes *选填* 路由数组,其中每个路由对象包含两个属性: 61 | - path 路由路径 `string|RegExp` 62 | - skeletonId Skeleton DOM 的 id `string` 63 | - minimize *选填* SPA 下是否需要压缩注入 HTML 的 JS 代码 64 | 65 | ### [DEPRECATED] SkeletonWebpackPlugin.loader 66 | 67 | 开发模式已经支持 hot reload,该参数不再需要。 68 | 69 | ## 示例 70 | 71 | ### Lavas 创建的项目 72 | 73 | 如果你的项目是使用 Lavas 创建的,可参考[Lavas Appshell模版](https://github.com/lavas-project/lavas-template-vue-appshell)和[Lavas MPA模版](https://github.com/lavas-project/lavas-template-vue-mpa) 中的应用。 74 | 75 | ### vue-cli 创建的项目 76 | 77 | 如果你的项目是使用 vue-cli 创建的,可以参考基于 Vue Webpack 模板应用这个插件的例子: 78 | SPA 中单个 Skeleton: 79 | * [Github](https://github.com/xiaoiver/skeleton-demo) 80 | * [Online Demo](https://xiaoiver.github.io/skeleton-demo/#/) 81 | 82 | SPA 中多个 Skeleton: 83 | * [Github](https://github.com/xiaoiver/multi-skeleton-demo) 84 | * [Online Demo](https://xiaoiver.github.io/multi-skeleton-demo/#/) 85 | 86 | ### 简单的 Vue + Webpack 应用 87 | 88 | 或者你可以参考[examples](https://github.com/lavas-project/vue-skeleton-webpack-plugin/tree/master/examples)下的测试用例,其中也包含了单页和多页情况,具体如下: 89 | * `/examples/simple` 最简单的 SPA,使用一个 Skeleton 90 | * `/examples/multi-skeleton` SPA,根据路由使用多个 Skeleton 91 | * `/examples/multipage` MPA,每个页面使用各自的 Skeleton,使用 `multipage-webpack-plugin` 92 | * `/examples/multipage2` MPA,每个页面使用各自的 Skeleton,使用多个 `html-webpack-plugin` 93 | * `/examples/multipage3` MPA,page1 使用 Skeleton,page2 不使用 94 | * `/examples/webpack4` SPA,使用 `webpack@4` 95 | * `/examples/vue-cli3` SPA,使用 `vue-cli@3` 创建的项目 96 | 97 | ## 常见问题 98 | 99 | ### Webpack4 100 | 101 | 插件需要使用与 Webpack 版本配套的插件进行样式分离。 102 | 103 | * Webpack 4 中使用 `mini-css-extract-plugin` 而非 `extract-text-webpack-plugin`,因此需要安装。 104 | * 安装 `vue-loader@^15.0.0` 并正确配置,可以参考 [vue-loader 文档](https://vue-loader.vuejs.org/zh/guide/extract-css.html#webpack-4)。 105 | 106 | ### 未开启样式分离 107 | 108 | 运行出现如下错误: 109 | > node_modules\memory-fs\lib\MemoryFileSystem.js:114 110 | > throw new MemoryFileSystemError(errors.code.ENOENT, _path); 111 | 112 | 由于插件使用了 Vue 服务端渲染在构建时渲染 skeleton 组件,将 DOM 和样式内联到最终输出的 html 中。 113 | 因此在给 skeleton 使用的 Webpack 配置对象中需要开启**样式分离**,将 skeleton 使用的样式从 JS 中分离出来。 114 | 115 | 在 Webpack 中样式分离是通过 [extract-text-webpack-plugin](https://doc.webpack-china.org/plugins/extract-text-webpack-plugin) 插件实现的。因此在 `webpack.skeleton.config` 中必须正确配置该插件。 116 | 117 | 以使用 vue-cli 创建的项目为例,如果你的 `webpack.skeleton.conf` 继承自 `webpack.base.conf`,在开发模式下是默认关闭样式分离的,因此需要修改,可参考[修改方案](https://github.com/lavas-project/vue-skeleton-webpack-plugin/issues/11#issuecomment-377845362)。 118 | 119 | ### 压缩注入的 HTML 和 CSS 120 | 121 | 使用 `html-webpack-plugin` 的 `minify` 选项,可以参考 [#36](https://github.com/lavas-project/vue-skeleton-webpack-plugin/issues/36)。 122 | 123 | ### 使用多个 Skeleton 时无法匹配当前路由路径? 124 | 125 | 用于匹配每个 `Skeleton` 的 `path` 选项可以填写字符串或者**正则**。 126 | 如果想匹配 `/page1?key=value` 这样的路由路径,可以直接写正则 `path: /^\/page1/`。可以参考 [#45] -------------------------------------------------------------------------------- /examples/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file examples index 3 | * @author panyuqi (pyqiverson@gmail.com) 4 | */ 5 | 6 | /* eslint-disable fecs-no-require */ 7 | 8 | const fs = require('fs'); 9 | const path = require('path'); 10 | const rimraf = require('rimraf'); 11 | 12 | cleanAllExamples(); 13 | 14 | function getExamples() { 15 | return fs.readdirSync(__dirname) 16 | .filter(readdirItem => fs.statSync(path.join(__dirname, readdirItem)).isDirectory()); 17 | } 18 | 19 | function cleanAllExamples() { 20 | let examplesDistPathNames = getExamples() 21 | .map(exampleName => path.join(__dirname, exampleName, 'dist')); 22 | 23 | for (let path of examplesDistPathNames) { 24 | fs.stat(path, (err, stat) => { 25 | if (err) { 26 | return; 27 | } 28 | if (stat.isDirectory()) { 29 | rimraf.sync(path); 30 | } 31 | }); 32 | } 33 | } 34 | 35 | exports.getExamples = getExamples; 36 | exports.cleanAllExamples = cleanAllExamples; 37 | -------------------------------------------------------------------------------- /examples/multi-skeleton/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 标题 6 | 7 | 8 | <% for (var jsFilePath of htmlWebpackPlugin.files.js) { %> 9 | 10 | <% } %> 11 | <% for (var cssFilePath of htmlWebpackPlugin.files.css) { %> 12 | 13 | <% } %> 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /examples/multi-skeleton/src/Skeleton1.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | 12 | 20 | -------------------------------------------------------------------------------- /examples/multi-skeleton/src/Skeleton2.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | 12 | 20 | -------------------------------------------------------------------------------- /examples/multi-skeleton/src/entry-skeleton.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file multi skeleton 3 | * @author panyuqi 4 | */ 5 | 6 | import Vue from 'vue'; 7 | import Skeleton1 from './Skeleton1'; 8 | import Skeleton2 from './Skeleton2'; 9 | 10 | export default new Vue({ 11 | components: { 12 | Skeleton1, 13 | Skeleton2 14 | }, 15 | template: ` 16 |
17 |
20 | ` 21 | }); 22 | -------------------------------------------------------------------------------- /examples/multi-skeleton/src/entry.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file empty entry 3 | * @author panyuqi 4 | */ 5 | 6 | import Vue from 'vue'; 7 | import Router from 'vue-router'; 8 | 9 | Vue.use(Router); 10 | 11 | let router = new Router({ 12 | mode: 'history', 13 | routes: [] 14 | }); 15 | 16 | export default new Vue({ 17 | router 18 | }); 19 | -------------------------------------------------------------------------------- /examples/multi-skeleton/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file utils 3 | * @author panyuqi (pyqiverson@gmail.com) 4 | */ 5 | 6 | 'use strict'; 7 | 8 | const path = require('path'); 9 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 10 | 11 | exports.assetsPath = function (newPath) { 12 | return path.posix.join('static', newPath); 13 | }; 14 | 15 | exports.cssLoaders = function (options) { 16 | options = options || {}; 17 | 18 | let cssLoader = { 19 | loader: 'css-loader', 20 | options: { 21 | minimize: process.env.NODE_ENV === 'production', 22 | sourceMap: options.sourceMap 23 | } 24 | }; 25 | 26 | // generate loader string to be used with extract text plugin 27 | function generateLoaders(loader, loaderOptions) { 28 | let loaders = [cssLoader]; 29 | if (loader) { 30 | loaders.push({ 31 | loader: loader + '-loader', 32 | options: Object.assign({}, loaderOptions, { 33 | sourceMap: options.sourceMap 34 | }) 35 | }); 36 | } 37 | 38 | // Extract CSS when that option is specified 39 | // (which is the case during production build) 40 | if (options.extract) { 41 | return ExtractTextPlugin.extract({ 42 | use: loaders, 43 | fallback: 'vue-style-loader' 44 | }); 45 | } 46 | 47 | return ['vue-style-loader'].concat(loaders); 48 | } 49 | 50 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html 51 | return { 52 | css: generateLoaders(), 53 | postcss: generateLoaders(), 54 | less: generateLoaders('less'), 55 | sass: generateLoaders('sass', {indentedSyntax: true}), 56 | scss: generateLoaders('sass'), 57 | stylus: generateLoaders('stylus'), 58 | styl: generateLoaders('stylus') 59 | }; 60 | }; 61 | 62 | // Generate loaders for standalone style files (outside of .vue) 63 | exports.styleLoaders = function (options) { 64 | let output = []; 65 | let loaders = exports.cssLoaders(options); 66 | 67 | Object.keys(loaders).forEach(function (extension) { 68 | output.push({ 69 | test: new RegExp('\\.' + extension + '$'), 70 | use: loaders[extension] 71 | }); 72 | }); 73 | return output; 74 | }; 75 | -------------------------------------------------------------------------------- /examples/multi-skeleton/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file vue-loader conf 3 | * @author panyuqi (pyqiverson@gmail.com) 4 | */ 5 | 6 | 'use strict'; 7 | 8 | const utils = require('./utils'); 9 | 10 | module.exports = { 11 | loaders: utils.cssLoaders({ 12 | sourceMap: false, 13 | extract: true 14 | }) 15 | }; 16 | -------------------------------------------------------------------------------- /examples/multi-skeleton/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file base conf 3 | * @author panyuqi (pyqiverson@gmail.com) 4 | */ 5 | 6 | 'use strict'; 7 | 8 | const utils = require('./utils'); 9 | const path = require('path'); 10 | const vueLoaderConfig = require('./vue-loader.conf'); 11 | 12 | function resolve(dir) { 13 | return path.join(__dirname, dir); 14 | } 15 | 16 | module.exports = { 17 | entry: { 18 | app: resolve('./src/entry.js') 19 | }, 20 | output: { 21 | path: resolve('dist'), 22 | filename: utils.assetsPath('js/[name].js'), 23 | chunkFilename: utils.assetsPath('js/[id].js') 24 | }, 25 | resolve: { 26 | extensions: ['.js', '.vue', '.json'], 27 | alias: { 28 | 'vue$': 'vue/dist/vue.esm.js', 29 | '@': resolve('src') 30 | } 31 | }, 32 | module: { 33 | rules: [ 34 | { 35 | test: /\.vue$/, 36 | use: [ 37 | { 38 | loader: 'vue-loader', 39 | options: vueLoaderConfig 40 | } 41 | ], 42 | include: [resolve('src')] 43 | }, 44 | { 45 | test: /\.js$/, 46 | loader: 'babel-loader', 47 | include: [resolve('src')] 48 | } 49 | ] 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /examples/multi-skeleton/webpack.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file skeleton conf 3 | * @author panyuqi (pyqiverson@gmail.com) 4 | */ 5 | 6 | /* eslint-disable fecs-no-require */ 7 | 8 | 'use strict'; 9 | 10 | const path = require('path'); 11 | const utils = require('./utils'); 12 | const merge = require('webpack-merge'); 13 | const baseWebpackConfig = require('./webpack.base.conf'); 14 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 15 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 16 | 17 | const SkeletonWebpackPlugin = require('../../lib'); 18 | 19 | function resolve(dir) { 20 | return path.join(__dirname, dir); 21 | } 22 | 23 | let webpackConfig = merge(baseWebpackConfig, { 24 | module: { 25 | rules: utils.styleLoaders({ 26 | sourceMap: false, 27 | extract: true 28 | }) 29 | // .concat(SkeletonWebpackPlugin.loader({ 30 | // resource: resolve('src/entry.js'), 31 | // options: { 32 | // entry: 'skeleton', 33 | // routePathTemplate: '/skeleton', 34 | // importTemplate: 'import [name] from \'./[name].vue\';' 35 | // } 36 | // })) 37 | }, 38 | devtool: false, 39 | plugins: [ 40 | 41 | new ExtractTextPlugin({ 42 | filename: utils.assetsPath('css/[name].css') 43 | }), 44 | 45 | new HtmlWebpackPlugin({ 46 | filename: utils.assetsPath('../index.html'), 47 | template: path.join(__dirname, './index.html'), 48 | inject: true, 49 | minify: { 50 | removeComments: true, 51 | collapseWhitespace: true, 52 | removeAttributeQuotes: true 53 | }, 54 | chunksSortMode: 'dependency' 55 | }), 56 | 57 | new SkeletonWebpackPlugin({ 58 | webpackConfig: { 59 | entry: { 60 | app: resolve('./src/entry-skeleton.js') 61 | } 62 | }, 63 | quiet: true, 64 | minimize: true, 65 | router: { 66 | mode: 'history', 67 | routes: [ 68 | { 69 | path: '/page1', 70 | skeletonId: 'skeleton1' 71 | }, 72 | { 73 | path: '/page2', 74 | skeletonId: 'skeleton2' 75 | } 76 | ] 77 | } 78 | }) 79 | ] 80 | }); 81 | 82 | module.exports = webpackConfig; 83 | -------------------------------------------------------------------------------- /examples/multipage/README.md: -------------------------------------------------------------------------------- 1 | ## 说明 2 | 3 | 这个多页面 DEMO 使用了 `multipage-webpack-plugin` 4 | 5 | 另一个 DEMO `examples/multipage2` 中同样是一个多页面项目,但是使用了多个 `html-webpack-plugin` -------------------------------------------------------------------------------- /examples/multipage/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 标题 6 | 7 | 8 | <% for (var jsFilePath of htmlWebpackPlugin.files.js) { %> 9 | 10 | <% } %> 11 | <% for (var cssFilePath of htmlWebpackPlugin.files.css) { %> 12 | 13 | <% } %> 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /examples/multipage/src/pages/page1/Page1.skeleton.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 16 | 17 | 31 | -------------------------------------------------------------------------------- /examples/multipage/src/pages/page1/entry-skeleton.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file simple skeleton 3 | * @author panyuqi 4 | */ 5 | 6 | import Vue from 'vue'; 7 | import Skeleton from './Page1.skeleton.vue'; 8 | 9 | export default new Vue({ 10 | components: { 11 | Skeleton 12 | }, 13 | template: '' 14 | }); 15 | -------------------------------------------------------------------------------- /examples/multipage/src/pages/page1/entry.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file empty entry 3 | * @author panyuqi 4 | */ 5 | 6 | import Vue from 'vue'; 7 | import Router from 'vue-router'; 8 | 9 | Vue.use(Router); 10 | 11 | let router = new Router({ 12 | mode: 'history', 13 | routes: [] 14 | }); 15 | 16 | export default new Vue({ 17 | router 18 | }); 19 | -------------------------------------------------------------------------------- /examples/multipage/src/pages/page2/Page2.skeleton.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 16 | 17 | 31 | -------------------------------------------------------------------------------- /examples/multipage/src/pages/page2/entry-skeleton.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file simple skeleton 3 | * @author panyuqi 4 | */ 5 | 6 | import Vue from 'vue'; 7 | import Skeleton from './Page2.skeleton.vue'; 8 | 9 | export default new Vue({ 10 | components: { 11 | Skeleton 12 | }, 13 | template: '' 14 | }); 15 | -------------------------------------------------------------------------------- /examples/multipage/src/pages/page2/entry.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file empty entry 3 | * @author panyuqi 4 | */ 5 | 6 | import Vue from 'vue'; 7 | import Router from 'vue-router'; 8 | 9 | Vue.use(Router); 10 | 11 | let router = new Router({ 12 | mode: 'history', 13 | routes: [] 14 | }); 15 | 16 | export default new Vue({ 17 | router 18 | }); 19 | -------------------------------------------------------------------------------- /examples/multipage/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file utils 3 | * @author panyuqi (pyqiverson@gmail.com) 4 | */ 5 | 6 | 'use strict'; 7 | 8 | const path = require('path'); 9 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 10 | 11 | exports.assetsPath = function (newPath) { 12 | return path.posix.join('static', newPath); 13 | }; 14 | 15 | exports.cssLoaders = function (options) { 16 | options = options || {}; 17 | 18 | let cssLoader = { 19 | loader: 'css-loader', 20 | options: { 21 | minimize: process.env.NODE_ENV === 'production', 22 | sourceMap: options.sourceMap 23 | } 24 | }; 25 | 26 | // generate loader string to be used with extract text plugin 27 | function generateLoaders(loader, loaderOptions) { 28 | let loaders = [cssLoader]; 29 | if (loader) { 30 | loaders.push({ 31 | loader: loader + '-loader', 32 | options: Object.assign({}, loaderOptions, { 33 | sourceMap: options.sourceMap 34 | }) 35 | }); 36 | } 37 | 38 | // Extract CSS when that option is specified 39 | // (which is the case during production build) 40 | if (options.extract) { 41 | return ExtractTextPlugin.extract({ 42 | use: loaders, 43 | fallback: 'vue-style-loader' 44 | }); 45 | } 46 | 47 | return ['vue-style-loader'].concat(loaders); 48 | } 49 | 50 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html 51 | return { 52 | css: generateLoaders(), 53 | postcss: generateLoaders(), 54 | less: generateLoaders('less'), 55 | sass: generateLoaders('sass', {indentedSyntax: true}), 56 | scss: generateLoaders('sass'), 57 | stylus: generateLoaders('stylus'), 58 | styl: generateLoaders('stylus') 59 | }; 60 | }; 61 | 62 | // Generate loaders for standalone style files (outside of .vue) 63 | exports.styleLoaders = function (options) { 64 | let output = []; 65 | let loaders = exports.cssLoaders(options); 66 | 67 | Object.keys(loaders).forEach(function (extension) { 68 | output.push({ 69 | test: new RegExp('\\.' + extension + '$'), 70 | use: loaders[extension] 71 | }); 72 | }); 73 | return output; 74 | }; 75 | -------------------------------------------------------------------------------- /examples/multipage/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file vue-loader conf 3 | * @author panyuqi (pyqiverson@gmail.com) 4 | */ 5 | 6 | 'use strict'; 7 | 8 | const utils = require('./utils'); 9 | 10 | module.exports = { 11 | loaders: utils.cssLoaders({ 12 | sourceMap: false, 13 | extract: true 14 | }) 15 | }; 16 | -------------------------------------------------------------------------------- /examples/multipage/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file base conf 3 | * @author panyuqi (pyqiverson@gmail.com) 4 | */ 5 | 6 | 'use strict'; 7 | 8 | const utils = require('./utils'); 9 | const path = require('path'); 10 | const vueLoaderConfig = require('./vue-loader.conf'); 11 | 12 | function resolve(dir) { 13 | return path.join(__dirname, dir); 14 | } 15 | 16 | module.exports = { 17 | entry: { 18 | page1: resolve('./src/pages/page1/entry.js'), 19 | page2: resolve('./src/pages/page2/entry.js') 20 | }, 21 | output: { 22 | path: resolve('dist'), 23 | filename: utils.assetsPath('js/[name].js'), 24 | chunkFilename: utils.assetsPath('js/[id].js') 25 | }, 26 | resolve: { 27 | extensions: ['.js', '.vue', '.json'], 28 | alias: { 29 | 'vue$': 'vue/dist/vue.esm.js', 30 | '@': resolve('src') 31 | } 32 | }, 33 | module: { 34 | rules: [ 35 | { 36 | test: /\.vue$/, 37 | use: [ 38 | { 39 | loader: 'vue-loader', 40 | options: vueLoaderConfig 41 | } 42 | ], 43 | include: [resolve('src')] 44 | }, 45 | { 46 | test: /\.js$/, 47 | loader: 'babel-loader', 48 | include: [resolve('src')] 49 | } 50 | ] 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /examples/multipage/webpack.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file skeleton conf 3 | * @author panyuqi (pyqiverson@gmail.com) 4 | */ 5 | 6 | /* eslint-disable fecs-no-require */ 7 | 8 | 'use strict'; 9 | 10 | const path = require('path'); 11 | const utils = require('./utils'); 12 | const merge = require('webpack-merge'); 13 | const baseWebpackConfig = require('./webpack.base.conf'); 14 | const MultipageWebpackPlugin = require('multipage-webpack-plugin'); 15 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 16 | 17 | const SkeletonWebpackPlugin = require('../../lib'); 18 | 19 | function resolve(dir) { 20 | return path.join(__dirname, dir); 21 | } 22 | 23 | let webpackConfig = merge(baseWebpackConfig, { 24 | module: { 25 | rules: utils.styleLoaders({ 26 | sourceMap: false, 27 | extract: true 28 | }).concat(SkeletonWebpackPlugin.loader({ 29 | include: [ 30 | resolve('src/pages/page1/entry.js'), 31 | resolve('src/pages/page2/entry.js') 32 | ], 33 | options: { 34 | entry: ['page1', 'page2'], 35 | routePathTemplate: '/[name]-skeleton', 36 | insertAfter: 'routes: [', 37 | importTemplate: 'import [name] from \'./[name].skeleton.vue\';' 38 | } 39 | })) 40 | }, 41 | devtool: false, 42 | plugins: [ 43 | 44 | new ExtractTextPlugin({ 45 | filename: utils.assetsPath('css/[name].css') 46 | }), 47 | 48 | new SkeletonWebpackPlugin({ 49 | webpackConfig: { 50 | entry: { 51 | page1: resolve('./src/pages/page1/entry-skeleton.js'), 52 | page2: resolve('./src/pages/page2/entry-skeleton.js') 53 | } 54 | } 55 | }), 56 | 57 | new MultipageWebpackPlugin({ 58 | bootstrapFilename: utils.assetsPath('js/manifest.js'), 59 | templateFilename: '[name].html', 60 | templatePath: resolve('dist'), 61 | htmlTemplatePath: resolve('./index.html'), 62 | htmlWebpackPluginOptions: { 63 | inject: true 64 | } 65 | }) 66 | ] 67 | }); 68 | 69 | module.exports = webpackConfig; 70 | -------------------------------------------------------------------------------- /examples/multipage2/README.md: -------------------------------------------------------------------------------- 1 | ## 说明 2 | 3 | 这个多页面 DEMO 使用了多个 `html-webpack-plugin` 4 | 5 | 另一个 DEMO `examples/multipage` 中同样是一个多页面项目,但是使用了 `multipage-webpack-plugin` -------------------------------------------------------------------------------- /examples/multipage2/src/pages/page1/Page1.skeleton.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 16 | 17 | 31 | -------------------------------------------------------------------------------- /examples/multipage2/src/pages/page1/entry-skeleton.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file simple skeleton 3 | * @author panyuqi 4 | */ 5 | 6 | import Vue from 'vue'; 7 | import Skeleton from './Page1.skeleton.vue'; 8 | 9 | export default new Vue({ 10 | components: { 11 | Skeleton 12 | }, 13 | template: '' 14 | }); 15 | -------------------------------------------------------------------------------- /examples/multipage2/src/pages/page1/entry.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file empty entry 3 | * @author panyuqi 4 | */ 5 | 6 | import Vue from 'vue'; 7 | import Router from 'vue-router'; 8 | 9 | Vue.use(Router); 10 | 11 | let router = new Router({ 12 | mode: 'history', 13 | routes: [] 14 | }); 15 | 16 | export default new Vue({ 17 | router 18 | }); 19 | -------------------------------------------------------------------------------- /examples/multipage2/src/pages/page1/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Page1 标题 6 | 7 | 8 | <% for (var jsFilePath of htmlWebpackPlugin.files.js) { %> 9 | 10 | <% } %> 11 | <% for (var cssFilePath of htmlWebpackPlugin.files.css) { %> 12 | 13 | <% } %> 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /examples/multipage2/src/pages/page2/Page2.skeleton.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 16 | 17 | 31 | -------------------------------------------------------------------------------- /examples/multipage2/src/pages/page2/entry-skeleton.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file simple skeleton 3 | * @author panyuqi 4 | */ 5 | 6 | import Vue from 'vue'; 7 | import Skeleton from './Page2.skeleton.vue'; 8 | 9 | export default new Vue({ 10 | components: { 11 | Skeleton 12 | }, 13 | template: '' 14 | }); 15 | -------------------------------------------------------------------------------- /examples/multipage2/src/pages/page2/entry.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file empty entry 3 | * @author panyuqi 4 | */ 5 | 6 | import Vue from 'vue'; 7 | import Router from 'vue-router'; 8 | 9 | Vue.use(Router); 10 | 11 | let router = new Router({ 12 | mode: 'history', 13 | routes: [] 14 | }); 15 | 16 | export default new Vue({ 17 | router 18 | }); 19 | -------------------------------------------------------------------------------- /examples/multipage2/src/pages/page2/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Page2 标题 6 | 7 | 8 | <% for (var jsFilePath of htmlWebpackPlugin.files.js) { %> 9 | 10 | <% } %> 11 | <% for (var cssFilePath of htmlWebpackPlugin.files.css) { %> 12 | 13 | <% } %> 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /examples/multipage2/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file utils 3 | * @author panyuqi (pyqiverson@gmail.com) 4 | */ 5 | 6 | 'use strict'; 7 | 8 | const path = require('path'); 9 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 10 | 11 | exports.assetsPath = function (newPath) { 12 | return path.posix.join('static', newPath); 13 | }; 14 | 15 | exports.cssLoaders = function (options) { 16 | options = options || {}; 17 | 18 | let cssLoader = { 19 | loader: 'css-loader', 20 | options: { 21 | minimize: process.env.NODE_ENV === 'production', 22 | sourceMap: options.sourceMap 23 | } 24 | }; 25 | 26 | // generate loader string to be used with extract text plugin 27 | function generateLoaders(loader, loaderOptions) { 28 | let loaders = [cssLoader]; 29 | if (loader) { 30 | loaders.push({ 31 | loader: loader + '-loader', 32 | options: Object.assign({}, loaderOptions, { 33 | sourceMap: options.sourceMap 34 | }) 35 | }); 36 | } 37 | 38 | // Extract CSS when that option is specified 39 | // (which is the case during production build) 40 | if (options.extract) { 41 | return ExtractTextPlugin.extract({ 42 | use: loaders, 43 | fallback: 'vue-style-loader' 44 | }); 45 | } 46 | 47 | return ['vue-style-loader'].concat(loaders); 48 | } 49 | 50 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html 51 | return { 52 | css: generateLoaders(), 53 | postcss: generateLoaders(), 54 | less: generateLoaders('less'), 55 | sass: generateLoaders('sass', {indentedSyntax: true}), 56 | scss: generateLoaders('sass'), 57 | stylus: generateLoaders('stylus'), 58 | styl: generateLoaders('stylus') 59 | }; 60 | }; 61 | 62 | // Generate loaders for standalone style files (outside of .vue) 63 | exports.styleLoaders = function (options) { 64 | let output = []; 65 | let loaders = exports.cssLoaders(options); 66 | 67 | Object.keys(loaders).forEach(function (extension) { 68 | output.push({ 69 | test: new RegExp('\\.' + extension + '$'), 70 | use: loaders[extension] 71 | }); 72 | }); 73 | return output; 74 | }; 75 | -------------------------------------------------------------------------------- /examples/multipage2/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file vue-loader conf 3 | * @author panyuqi (pyqiverson@gmail.com) 4 | */ 5 | 6 | 'use strict'; 7 | 8 | const utils = require('./utils'); 9 | 10 | module.exports = { 11 | loaders: utils.cssLoaders({ 12 | sourceMap: false, 13 | extract: true 14 | }) 15 | }; 16 | -------------------------------------------------------------------------------- /examples/multipage2/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file base conf 3 | * @author panyuqi (pyqiverson@gmail.com) 4 | */ 5 | 6 | 'use strict'; 7 | 8 | const utils = require('./utils'); 9 | const path = require('path'); 10 | const vueLoaderConfig = require('./vue-loader.conf'); 11 | 12 | function resolve(dir) { 13 | return path.join(__dirname, dir); 14 | } 15 | 16 | module.exports = { 17 | entry: { 18 | page1: resolve('./src/pages/page1/entry.js'), 19 | page2: resolve('./src/pages/page2/entry.js') 20 | }, 21 | output: { 22 | path: resolve('dist'), 23 | filename: utils.assetsPath('js/[name].js'), 24 | chunkFilename: utils.assetsPath('js/[id].js') 25 | }, 26 | resolve: { 27 | extensions: ['.js', '.vue', '.json'], 28 | alias: { 29 | 'vue$': 'vue/dist/vue.esm.js', 30 | '@': resolve('src') 31 | } 32 | }, 33 | module: { 34 | rules: [ 35 | { 36 | test: /\.vue$/, 37 | use: [ 38 | { 39 | loader: 'vue-loader', 40 | options: vueLoaderConfig 41 | } 42 | ], 43 | include: [resolve('src')] 44 | }, 45 | { 46 | test: /\.js$/, 47 | loader: 'babel-loader', 48 | include: [resolve('src')] 49 | } 50 | ] 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /examples/multipage2/webpack.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file skeleton conf 3 | * @author panyuqi (pyqiverson@gmail.com) 4 | */ 5 | 6 | /* eslint-disable fecs-no-require */ 7 | 8 | 'use strict'; 9 | 10 | const path = require('path'); 11 | const utils = require('./utils'); 12 | const merge = require('webpack-merge'); 13 | const baseWebpackConfig = require('./webpack.base.conf'); 14 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 15 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 16 | 17 | const SkeletonWebpackPlugin = require('../../lib'); 18 | 19 | function resolve(dir) { 20 | return path.join(__dirname, dir); 21 | } 22 | 23 | let webpackConfig = merge(baseWebpackConfig, { 24 | module: { 25 | rules: utils.styleLoaders({ 26 | sourceMap: false, 27 | extract: true 28 | }).concat(SkeletonWebpackPlugin.loader({ 29 | include: [ 30 | resolve('src/pages/page1/entry.js'), 31 | resolve('src/pages/page2/entry.js') 32 | ], 33 | options: { 34 | entry: ['page1', 'page2'], 35 | routePathTemplate: '/[name]-skeleton', 36 | insertAfter: 'routes: [', 37 | importTemplate: 'import [name] from \'./[name].skeleton.vue\';' 38 | } 39 | })) 40 | }, 41 | devtool: false, 42 | plugins: [ 43 | 44 | new ExtractTextPlugin({ 45 | filename: utils.assetsPath('css/[name].css') 46 | }), 47 | 48 | new SkeletonWebpackPlugin({ 49 | webpackConfig: { 50 | entry: { 51 | page1: resolve('./src/pages/page1/entry-skeleton.js'), 52 | page2: resolve('./src/pages/page2/entry-skeleton.js') 53 | } 54 | }, 55 | insertAfter: function(entryKey) { 56 | return `
`; 57 | } 58 | }), 59 | 60 | new HtmlWebpackPlugin({ 61 | filename: utils.assetsPath('../page1.html'), 62 | template: path.join(__dirname, './src/pages/page1/index.html'), 63 | chunks: ['page1'], // 或者使用 excludeChunks 64 | // excludeChunks: ['page2'], 65 | chunksSortMode: 'dependency' 66 | }), 67 | 68 | new HtmlWebpackPlugin({ 69 | filename: utils.assetsPath('../page2.html'), 70 | template: path.join(__dirname, './src/pages/page2/index.html'), 71 | chunks: ['page2'], 72 | // excludeChunks: ['page1'], 73 | chunksSortMode: 'dependency' 74 | }) 75 | ] 76 | }); 77 | 78 | module.exports = webpackConfig; 79 | -------------------------------------------------------------------------------- /examples/multipage3/README.md: -------------------------------------------------------------------------------- 1 | ## 说明 2 | 3 | 这个多页面 DEMO 使用了多个 `html-webpack-plugin` 4 | 5 | 另一个 DEMO `examples/multipage` 中同样是一个多页面项目,但是使用了 `multipage-webpack-plugin` 6 | 7 | page1 使用 Skeleton,page2 不使用。 -------------------------------------------------------------------------------- /examples/multipage3/src/pages/page1/Page1.skeleton.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 16 | 17 | 31 | -------------------------------------------------------------------------------- /examples/multipage3/src/pages/page1/entry-skeleton.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file simple skeleton 3 | * @author panyuqi 4 | */ 5 | 6 | import Vue from 'vue'; 7 | import Skeleton from './Page1.skeleton.vue'; 8 | 9 | export default new Vue({ 10 | components: { 11 | Skeleton 12 | }, 13 | template: '' 14 | }); 15 | -------------------------------------------------------------------------------- /examples/multipage3/src/pages/page1/entry.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file empty entry 3 | * @author panyuqi 4 | */ 5 | 6 | import Vue from 'vue'; 7 | import Router from 'vue-router'; 8 | 9 | Vue.use(Router); 10 | 11 | let router = new Router({ 12 | mode: 'history', 13 | routes: [] 14 | }); 15 | 16 | export default new Vue({ 17 | router 18 | }); 19 | -------------------------------------------------------------------------------- /examples/multipage3/src/pages/page1/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Page1 标题 6 | 7 | 8 | <% for (var jsFilePath of htmlWebpackPlugin.files.js) { %> 9 | 10 | <% } %> 11 | <% for (var cssFilePath of htmlWebpackPlugin.files.css) { %> 12 | 13 | <% } %> 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /examples/multipage3/src/pages/page2/entry.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file empty entry 3 | * @author panyuqi 4 | */ 5 | 6 | import Vue from 'vue'; 7 | import Router from 'vue-router'; 8 | 9 | Vue.use(Router); 10 | 11 | let router = new Router({ 12 | mode: 'history', 13 | routes: [] 14 | }); 15 | 16 | export default new Vue({ 17 | router 18 | }); 19 | -------------------------------------------------------------------------------- /examples/multipage3/src/pages/page2/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Page2 标题 6 | 7 | 8 | <% for (var jsFilePath of htmlWebpackPlugin.files.js) { %> 9 | 10 | <% } %> 11 | <% for (var cssFilePath of htmlWebpackPlugin.files.css) { %> 12 | 13 | <% } %> 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /examples/multipage3/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file utils 3 | * @author panyuqi (pyqiverson@gmail.com) 4 | */ 5 | 6 | 'use strict'; 7 | 8 | const path = require('path'); 9 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 10 | 11 | exports.assetsPath = function (newPath) { 12 | return path.posix.join('static', newPath); 13 | }; 14 | 15 | exports.cssLoaders = function (options) { 16 | options = options || {}; 17 | 18 | let cssLoader = { 19 | loader: 'css-loader', 20 | options: { 21 | minimize: process.env.NODE_ENV === 'production', 22 | sourceMap: options.sourceMap 23 | } 24 | }; 25 | 26 | // generate loader string to be used with extract text plugin 27 | function generateLoaders(loader, loaderOptions) { 28 | let loaders = [cssLoader]; 29 | if (loader) { 30 | loaders.push({ 31 | loader: loader + '-loader', 32 | options: Object.assign({}, loaderOptions, { 33 | sourceMap: options.sourceMap 34 | }) 35 | }); 36 | } 37 | 38 | // Extract CSS when that option is specified 39 | // (which is the case during production build) 40 | if (options.extract) { 41 | return ExtractTextPlugin.extract({ 42 | use: loaders, 43 | fallback: 'vue-style-loader' 44 | }); 45 | } 46 | 47 | return ['vue-style-loader'].concat(loaders); 48 | } 49 | 50 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html 51 | return { 52 | css: generateLoaders(), 53 | postcss: generateLoaders(), 54 | less: generateLoaders('less'), 55 | sass: generateLoaders('sass', {indentedSyntax: true}), 56 | scss: generateLoaders('sass'), 57 | stylus: generateLoaders('stylus'), 58 | styl: generateLoaders('stylus') 59 | }; 60 | }; 61 | 62 | // Generate loaders for standalone style files (outside of .vue) 63 | exports.styleLoaders = function (options) { 64 | let output = []; 65 | let loaders = exports.cssLoaders(options); 66 | 67 | Object.keys(loaders).forEach(function (extension) { 68 | output.push({ 69 | test: new RegExp('\\.' + extension + '$'), 70 | use: loaders[extension] 71 | }); 72 | }); 73 | return output; 74 | }; 75 | -------------------------------------------------------------------------------- /examples/multipage3/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file vue-loader conf 3 | * @author panyuqi (pyqiverson@gmail.com) 4 | */ 5 | 6 | 'use strict'; 7 | 8 | const utils = require('./utils'); 9 | 10 | module.exports = { 11 | loaders: utils.cssLoaders({ 12 | sourceMap: false, 13 | extract: true 14 | }) 15 | }; 16 | -------------------------------------------------------------------------------- /examples/multipage3/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file base conf 3 | * @author panyuqi (pyqiverson@gmail.com) 4 | */ 5 | 6 | 'use strict'; 7 | 8 | const utils = require('./utils'); 9 | const path = require('path'); 10 | const vueLoaderConfig = require('./vue-loader.conf'); 11 | 12 | function resolve(dir) { 13 | return path.join(__dirname, dir); 14 | } 15 | 16 | module.exports = { 17 | entry: { 18 | page1: resolve('./src/pages/page1/entry.js'), 19 | page2: resolve('./src/pages/page2/entry.js') 20 | }, 21 | output: { 22 | path: resolve('dist'), 23 | filename: utils.assetsPath('js/[name].js'), 24 | chunkFilename: utils.assetsPath('js/[id].js') 25 | }, 26 | resolve: { 27 | extensions: ['.js', '.vue', '.json'], 28 | alias: { 29 | 'vue$': 'vue/dist/vue.esm.js', 30 | '@': resolve('src') 31 | } 32 | }, 33 | module: { 34 | rules: [ 35 | { 36 | test: /\.vue$/, 37 | use: [ 38 | { 39 | loader: 'vue-loader', 40 | options: vueLoaderConfig 41 | } 42 | ], 43 | include: [resolve('src')] 44 | }, 45 | { 46 | test: /\.js$/, 47 | loader: 'babel-loader', 48 | include: [resolve('src')] 49 | } 50 | ] 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /examples/multipage3/webpack.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file skeleton conf 3 | * @author panyuqi (pyqiverson@gmail.com) 4 | */ 5 | 6 | /* eslint-disable fecs-no-require */ 7 | 8 | 'use strict'; 9 | 10 | const path = require('path'); 11 | const utils = require('./utils'); 12 | const merge = require('webpack-merge'); 13 | const baseWebpackConfig = require('./webpack.base.conf'); 14 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 15 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 16 | 17 | const SkeletonWebpackPlugin = require('../../lib'); 18 | 19 | function resolve(dir) { 20 | return path.join(__dirname, dir); 21 | } 22 | 23 | let webpackConfig = merge(baseWebpackConfig, { 24 | module: { 25 | rules: utils.styleLoaders({ 26 | sourceMap: false, 27 | extract: true 28 | }).concat(SkeletonWebpackPlugin.loader({ 29 | include: [ 30 | resolve('src/pages/page1/entry.js') 31 | ], 32 | options: { 33 | entry: ['page1'], 34 | routePathTemplate: '/[name]-skeleton', 35 | insertAfter: 'routes: [', 36 | importTemplate: 'import [name] from \'./[name].skeleton.vue\';' 37 | } 38 | })) 39 | }, 40 | devtool: false, 41 | plugins: [ 42 | 43 | new ExtractTextPlugin({ 44 | filename: utils.assetsPath('css/[name].css') 45 | }), 46 | 47 | new SkeletonWebpackPlugin({ 48 | webpackConfig: { 49 | entry: { 50 | // 仅 page1 使用 Skeleton 51 | page1: resolve('./src/pages/page1/entry-skeleton.js') 52 | } 53 | } 54 | }), 55 | 56 | new HtmlWebpackPlugin({ 57 | filename: utils.assetsPath('../page1.html'), 58 | template: path.join(__dirname, './src/pages/page1/index.html'), 59 | chunks: ['page1'], // 或者使用 excludeChunks 60 | // excludeChunks: ['page2'], 61 | chunksSortMode: 'dependency' 62 | }), 63 | 64 | new HtmlWebpackPlugin({ 65 | filename: utils.assetsPath('../page2.html'), 66 | template: path.join(__dirname, './src/pages/page2/index.html'), 67 | chunks: ['page2'], 68 | // excludeChunks: ['page1'], 69 | chunksSortMode: 'dependency' 70 | }) 71 | ] 72 | }); 73 | 74 | module.exports = webpackConfig; 75 | -------------------------------------------------------------------------------- /examples/simple/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | Last 2 versions 3 | IE 8 4 | -------------------------------------------------------------------------------- /examples/simple/.postcssrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | 'autoprefixer': {} 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /examples/simple/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 标题 6 | 7 | 8 | <% for (var jsFilePath of htmlWebpackPlugin.files.js) { %> 9 | 10 | <% } %> 11 | <% for (var cssFilePath of htmlWebpackPlugin.files.css) { %> 12 | 13 | <% } %> 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /examples/simple/src/Skeleton.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 17 | 18 | 33 | -------------------------------------------------------------------------------- /examples/simple/src/entry-skeleton.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file simple skeleton 3 | * @author panyuqi 4 | */ 5 | 6 | import Vue from 'vue'; 7 | import Skeleton from './Skeleton'; 8 | 9 | console.log('skeleton...') 10 | 11 | export default new Vue({ 12 | components: { 13 | Skeleton 14 | }, 15 | template: '' 16 | }); 17 | -------------------------------------------------------------------------------- /examples/simple/src/entry.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file empty entry 3 | * @author panyuqi 4 | */ 5 | 6 | import Vue from 'vue'; 7 | import Router from 'vue-router'; 8 | import './main.css'; 9 | 10 | console.log('entry...') 11 | 12 | Vue.use(Router); 13 | 14 | let router = new Router({ 15 | mode: 'history', 16 | routes: [] 17 | }); 18 | 19 | export default new Vue({ 20 | router 21 | }); 22 | -------------------------------------------------------------------------------- /examples/simple/src/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lavas-project/vue-skeleton-webpack-plugin/0f2a65dfa1defb3a10a716d86a06b19870e4ec12/examples/simple/src/logo.jpg -------------------------------------------------------------------------------- /examples/simple/src/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | } -------------------------------------------------------------------------------- /examples/simple/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file utils 3 | * @author panyuqi (pyqiverson@gmail.com) 4 | */ 5 | 6 | 'use strict'; 7 | 8 | const path = require('path'); 9 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 10 | 11 | exports.assetsPath = function (newPath) { 12 | return path.posix.join('static', newPath); 13 | }; 14 | 15 | exports.cssLoaders = function (options) { 16 | options = options || {}; 17 | 18 | let cssLoader = { 19 | loader: 'css-loader', 20 | options: { 21 | minimize: process.env.NODE_ENV === 'production', 22 | sourceMap: options.sourceMap 23 | } 24 | }; 25 | 26 | // generate loader string to be used with extract text plugin 27 | function generateLoaders(loader, loaderOptions) { 28 | let loaders = [cssLoader]; 29 | if (loader) { 30 | loaders.push({ 31 | loader: loader + '-loader', 32 | options: Object.assign({}, loaderOptions, { 33 | sourceMap: options.sourceMap 34 | }) 35 | }); 36 | } 37 | 38 | // Extract CSS when that option is specified 39 | // (which is the case during production build) 40 | if (options.extract) { 41 | return ExtractTextPlugin.extract({ 42 | use: loaders, 43 | fallback: 'vue-style-loader' 44 | }); 45 | } 46 | 47 | return ['vue-style-loader'].concat(loaders); 48 | } 49 | 50 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html 51 | return { 52 | css: generateLoaders(), 53 | postcss: generateLoaders(), 54 | less: generateLoaders('less'), 55 | sass: generateLoaders('sass', {indentedSyntax: true}), 56 | scss: generateLoaders('sass'), 57 | stylus: generateLoaders('stylus'), 58 | styl: generateLoaders('stylus') 59 | }; 60 | }; 61 | 62 | // Generate loaders for standalone style files (outside of .vue) 63 | exports.styleLoaders = function (options) { 64 | let output = []; 65 | let loaders = exports.cssLoaders(options); 66 | 67 | Object.keys(loaders).forEach(function (extension) { 68 | output.push({ 69 | test: new RegExp('\\.' + extension + '$'), 70 | use: loaders[extension] 71 | }); 72 | }); 73 | return output; 74 | }; 75 | -------------------------------------------------------------------------------- /examples/simple/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file vue-loader conf 3 | * @author panyuqi (pyqiverson@gmail.com) 4 | */ 5 | 6 | 'use strict'; 7 | 8 | const utils = require('./utils'); 9 | 10 | module.exports = { 11 | loaders: utils.cssLoaders({ 12 | sourceMap: false, 13 | extract: true 14 | }) 15 | }; 16 | -------------------------------------------------------------------------------- /examples/simple/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file base conf 3 | * @author panyuqi (pyqiverson@gmail.com) 4 | */ 5 | 6 | 'use strict'; 7 | 8 | const utils = require('./utils'); 9 | const path = require('path'); 10 | const vueLoaderConfig = require('./vue-loader.conf'); 11 | 12 | function resolve(dir) { 13 | return path.join(__dirname, dir); 14 | } 15 | 16 | module.exports = { 17 | entry: { 18 | app: resolve('./src/entry.js') 19 | }, 20 | output: { 21 | path: resolve('dist'), 22 | filename: utils.assetsPath('js/[name].js'), 23 | chunkFilename: utils.assetsPath('js/[id].js') 24 | }, 25 | resolve: { 26 | extensions: ['.js', '.vue', '.json'], 27 | alias: { 28 | 'vue$': 'vue/dist/vue.esm.js', 29 | '@': resolve('src') 30 | } 31 | }, 32 | module: { 33 | rules: [ 34 | { 35 | test: /\.vue$/, 36 | use: [ 37 | { 38 | // loader: 'vue-loader', 39 | // options: vueLoaderConfig 40 | loader: 'vue-loader', 41 | options: { 42 | extractCSS: true 43 | } 44 | } 45 | ], 46 | include: [resolve('src')] 47 | }, 48 | { 49 | test: /\.js$/, 50 | loader: 'babel-loader', 51 | include: [resolve('src')] 52 | }, 53 | { 54 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 55 | loader: 'url-loader', 56 | options: { 57 | limit: 10000, 58 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 59 | } 60 | } 61 | ] 62 | } 63 | }; 64 | -------------------------------------------------------------------------------- /examples/simple/webpack.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file skeleton conf 3 | * @author panyuqi (pyqiverson@gmail.com) 4 | */ 5 | 6 | /* eslint-disable fecs-no-require */ 7 | 8 | 'use strict'; 9 | 10 | const path = require('path'); 11 | const utils = require('./utils'); 12 | const merge = require('webpack-merge'); 13 | const baseWebpackConfig = require('./webpack.base.conf'); 14 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 15 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 16 | 17 | const SkeletonWebpackPlugin = require('../../lib'); 18 | 19 | function resolve(dir) { 20 | return path.join(__dirname, dir); 21 | } 22 | 23 | let webpackConfig = merge(baseWebpackConfig, { 24 | module: { 25 | rules: utils.styleLoaders({ 26 | sourceMap: false, 27 | extract: true 28 | }) 29 | .concat(SkeletonWebpackPlugin.loader({ 30 | resource: resolve('src/entry.js'), 31 | options: { 32 | entry: 'skeleton', 33 | routePathTemplate: '/skeleton', 34 | importTemplate: 'import [name] from \'./[name].vue\';' 35 | } 36 | })) 37 | }, 38 | devtool: false, 39 | plugins: [ 40 | 41 | new ExtractTextPlugin({ 42 | filename: utils.assetsPath('css/[name].css') 43 | }), 44 | 45 | new HtmlWebpackPlugin({ 46 | filename: utils.assetsPath('../index.html'), 47 | template: path.join(__dirname, './index.html'), 48 | inject: true, 49 | minify: { 50 | removeComments: true, 51 | collapseWhitespace: true, 52 | removeAttributeQuotes: true 53 | }, 54 | chunksSortMode: 'dependency' 55 | }), 56 | 57 | new SkeletonWebpackPlugin({ 58 | webpackConfig: { 59 | entry: { 60 | app: resolve('./src/entry-skeleton.js') 61 | // app: [resolve('./src/entry-skeleton.js')] 62 | } 63 | }, 64 | quiet: true 65 | }) 66 | ] 67 | }); 68 | 69 | module.exports = webpackConfig; 70 | -------------------------------------------------------------------------------- /examples/vue-cli3/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not ie <= 8 4 | -------------------------------------------------------------------------------- /examples/vue-cli3/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw* 22 | -------------------------------------------------------------------------------- /examples/vue-cli3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-cli3", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build" 8 | }, 9 | "dependencies": { 10 | "vue": "^2.5.17" 11 | }, 12 | "devDependencies": { 13 | "@vue/cli-service": "^3.0.1", 14 | "vue-template-compiler": "^2.5.17" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/vue-cli3/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /examples/vue-cli3/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lavas-project/vue-skeleton-webpack-plugin/0f2a65dfa1defb3a10a716d86a06b19870e4ec12/examples/vue-cli3/public/favicon.ico -------------------------------------------------------------------------------- /examples/vue-cli3/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | vue-cli3 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/vue-cli3/src/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | 19 | 29 | -------------------------------------------------------------------------------- /examples/vue-cli3/src/Skeleton.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | 17 | 24 | -------------------------------------------------------------------------------- /examples/vue-cli3/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lavas-project/vue-skeleton-webpack-plugin/0f2a65dfa1defb3a10a716d86a06b19870e4ec12/examples/vue-cli3/src/assets/logo.png -------------------------------------------------------------------------------- /examples/vue-cli3/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 39 | 40 | 41 | 57 | -------------------------------------------------------------------------------- /examples/vue-cli3/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | 4 | Vue.config.productionTip = false 5 | 6 | const app = new Vue({ 7 | components: { 8 | App, 9 | }, 10 | render: h => h(App), 11 | }); 12 | 13 | window.mountApp = () => { 14 | app.$mount('#app'); 15 | }; 16 | 17 | if (process.env.NODE_ENV === 'production') { 18 | if (window.STYLE_READY) { 19 | window.mountApp(); 20 | } 21 | } else { 22 | window.mountApp(); 23 | } 24 | -------------------------------------------------------------------------------- /examples/vue-cli3/src/skeleton.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Skeleton from './Skeleton.vue'; 3 | 4 | export default new Vue({ 5 | components: { 6 | Skeleton, 7 | }, 8 | render: h => h(Skeleton), 9 | }); 10 | -------------------------------------------------------------------------------- /examples/vue-cli3/vue.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const SkeletonWebpackPlugin = require('../../lib'); 3 | // const SkeletonWebpackPlugin = require('vue-skeleton-webpack-plugin'); 4 | 5 | module.exports = { 6 | configureWebpack: { 7 | plugins: [ 8 | new SkeletonWebpackPlugin({ 9 | webpackConfig: { 10 | entry: { 11 | app: path.join(__dirname, './src/skeleton.js'), 12 | }, 13 | }, 14 | minimize: true, 15 | quiet: true, 16 | }), 17 | ], 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /examples/webpack4/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | Last 2 versions 3 | IE 8 4 | -------------------------------------------------------------------------------- /examples/webpack4/.postcssrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | 'autoprefixer': {} 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /examples/webpack4/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 标题 6 | 7 | 8 | <% for (var jsFilePath of htmlWebpackPlugin.files.js) { %> 9 | 10 | <% } %> 11 | <% for (var cssFilePath of htmlWebpackPlugin.files.css) { %> 12 | 13 | <% } %> 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /examples/webpack4/src/Skeleton.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 17 | 18 | 33 | -------------------------------------------------------------------------------- /examples/webpack4/src/entry-skeleton.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file simple skeleton 3 | * @author panyuqi 4 | */ 5 | 6 | import Vue from 'vue'; 7 | import Skeleton from './Skeleton'; 8 | 9 | export default new Vue({ 10 | components: { 11 | Skeleton 12 | }, 13 | template: '' 14 | }); 15 | -------------------------------------------------------------------------------- /examples/webpack4/src/entry.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file empty entry 3 | * @author panyuqi 4 | */ 5 | 6 | import Vue from 'vue'; 7 | import Router from 'vue-router'; 8 | import './main.css'; 9 | 10 | Vue.use(Router); 11 | 12 | let router = new Router({ 13 | mode: 'history', 14 | routes: [] 15 | }); 16 | 17 | export default new Vue({ 18 | router 19 | }); 20 | -------------------------------------------------------------------------------- /examples/webpack4/src/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lavas-project/vue-skeleton-webpack-plugin/0f2a65dfa1defb3a10a716d86a06b19870e4ec12/examples/webpack4/src/logo.jpg -------------------------------------------------------------------------------- /examples/webpack4/src/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | } -------------------------------------------------------------------------------- /examples/webpack4/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file utils 3 | * @author panyuqi (pyqiverson@gmail.com) 4 | */ 5 | 6 | 'use strict'; 7 | 8 | const path = require('path'); 9 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 10 | 11 | exports.assetsPath = function (newPath) { 12 | return path.posix.join('static', newPath); 13 | }; 14 | 15 | exports.cssLoaders = function (options) { 16 | options = options || {}; 17 | 18 | let cssLoader = { 19 | loader: 'css-loader', 20 | options: { 21 | sourceMap: options.sourceMap, 22 | importLoaders: 1 23 | } 24 | }; 25 | 26 | // generate loader string to be used with extract text plugin 27 | function generateLoaders(loader, loaderOptions) { 28 | let loaders = [cssLoader, 'postcss-loader']; 29 | if (loader) { 30 | loaders.push({ 31 | loader: loader + '-loader', 32 | options: Object.assign({}, loaderOptions, { 33 | sourceMap: options.sourceMap 34 | }) 35 | }); 36 | } 37 | 38 | // Extract CSS when that option is specified 39 | // (which is the case during production build) 40 | if (options.extract) { 41 | return [MiniCssExtractPlugin.loader].concat(loaders); 42 | } 43 | 44 | return ['vue-style-loader'].concat(loaders); 45 | } 46 | 47 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html 48 | return { 49 | css: generateLoaders(), 50 | postcss: generateLoaders(), 51 | less: generateLoaders('less'), 52 | sass: generateLoaders('sass', {indentedSyntax: true}), 53 | scss: generateLoaders('sass'), 54 | stylus: generateLoaders('stylus'), 55 | styl: generateLoaders('stylus') 56 | }; 57 | }; 58 | 59 | // Generate loaders for standalone style files (outside of .vue) 60 | exports.styleLoaders = function (options) { 61 | let output = []; 62 | let loaders = exports.cssLoaders(options); 63 | 64 | Object.keys(loaders).forEach(function (extension) { 65 | output.push({ 66 | test: new RegExp('\\.' + extension + '$'), 67 | use: loaders[extension] 68 | }); 69 | }); 70 | return output; 71 | }; 72 | -------------------------------------------------------------------------------- /examples/webpack4/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file base conf 3 | * @author panyuqi (pyqiverson@gmail.com) 4 | */ 5 | 6 | 'use strict'; 7 | 8 | const utils = require('./utils'); 9 | const path = require('path'); 10 | 11 | function resolve(dir) { 12 | return path.join(__dirname, dir); 13 | } 14 | 15 | module.exports = { 16 | entry: { 17 | app: resolve('./src/entry.js') 18 | }, 19 | output: { 20 | path: resolve('dist'), 21 | filename: utils.assetsPath('js/[name].js'), 22 | chunkFilename: utils.assetsPath('js/[id].js') 23 | }, 24 | resolve: { 25 | extensions: ['.js', '.vue', '.json'], 26 | alias: { 27 | 'vue$': 'vue/dist/vue.esm.js', 28 | '@': resolve('src') 29 | } 30 | }, 31 | module: { 32 | rules: [ 33 | { 34 | test: /\.vue$/, 35 | use: [ 36 | { 37 | loader: 'vue-loader' 38 | } 39 | ], 40 | include: [resolve('src')] 41 | }, 42 | { 43 | test: /\.js$/, 44 | loader: 'babel-loader', 45 | include: [resolve('src')] 46 | }, 47 | { 48 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 49 | loader: 'url-loader', 50 | options: { 51 | limit: 10000, 52 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 53 | } 54 | } 55 | ] 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /examples/webpack4/webpack.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file skeleton conf 3 | * @author panyuqi (pyqiverson@gmail.com) 4 | */ 5 | 6 | /* eslint-disable fecs-no-require */ 7 | 8 | 'use strict'; 9 | 10 | const path = require('path'); 11 | const utils = require('./utils'); 12 | const merge = require('webpack-merge'); 13 | const baseWebpackConfig = require('./webpack.base.conf'); 14 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 15 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 16 | const VueLoaderPlugin = require('vue-loader/lib/plugin'); 17 | 18 | const SkeletonWebpackPlugin = require('../../lib'); 19 | 20 | function resolve(dir) { 21 | return path.join(__dirname, dir); 22 | } 23 | 24 | let webpackConfig = merge(baseWebpackConfig, { 25 | module: { 26 | rules: utils.styleLoaders({ 27 | sourceMap: false, 28 | extract: true 29 | }) 30 | // .concat(SkeletonWebpackPlugin.loader({ 31 | // resource: resolve('src/entry.js'), 32 | // options: { 33 | // entry: 'skeleton', 34 | // routePathTemplate: '/skeleton', 35 | // importTemplate: 'import [name] from \'./[name].vue\';' 36 | // } 37 | // })) 38 | }, 39 | devtool: false, 40 | plugins: [ 41 | 42 | new VueLoaderPlugin(), 43 | 44 | new MiniCssExtractPlugin({ 45 | filename: utils.assetsPath('css/[name].css') 46 | }), 47 | 48 | new HtmlWebpackPlugin({ 49 | filename: utils.assetsPath('../index.html'), 50 | template: path.join(__dirname, './index.html'), 51 | inject: true, 52 | minify: { 53 | removeComments: true, 54 | collapseWhitespace: true, 55 | removeAttributeQuotes: true 56 | }, 57 | chunksSortMode: 'dependency' 58 | }), 59 | 60 | new SkeletonWebpackPlugin({ 61 | webpackConfig: { 62 | entry: { 63 | app: resolve('./src/entry-skeleton.js') 64 | } 65 | }, 66 | quiet: true 67 | }) 68 | ] 69 | }); 70 | 71 | module.exports = webpackConfig; 72 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-skeleton-webpack-plugin", 3 | "version": "1.2.2", 4 | "description": "A webpack plugin for generating skeleton of pages", 5 | "main": "lib/index.js", 6 | "files": [ 7 | "lib" 8 | ], 9 | "scripts": { 10 | "build": "buble src -o lib", 11 | "lint": "fecs ./ --rule --type 'vue,js,css'", 12 | "prepublish": "npm run build", 13 | "test": "npm run build && nyc ava" 14 | }, 15 | "dependencies": { 16 | "loader-utils": "^1.1.0", 17 | "path-to-regexp": "^2.1.0", 18 | "uglify-es": "^3.3.4", 19 | "vue-server-renderer": "^2.3.4", 20 | "webpack-node-externals": "^1.6.0" 21 | }, 22 | "devDependencies": { 23 | "@vue/cli-service": "^3.0.1", 24 | "autoprefixer": "^8.2.0", 25 | "ava": "^0.18.2", 26 | "babel-cli": "^6.24.0", 27 | "babel-loader": "^7.1.1", 28 | "babel-polyfill": "^6.23.0", 29 | "babel-preset-env": "^1.3.2", 30 | "babel-register": "^6.23.0", 31 | "bluebird": "^3.4.7", 32 | "buble": "^0.15.2", 33 | "css-loader": "^0.28.0", 34 | "extract-text-webpack-plugin": "^3.0.0", 35 | "fecs": "^1.4.0", 36 | "file-loader": "^1.1.11", 37 | "html-webpack-plugin": "^3.2.0", 38 | "memory-fs": "^0.4.1", 39 | "mini-css-extract-plugin": "^0.4.0", 40 | "multipage-webpack-plugin": "^0.4.0", 41 | "nyc": "^10.3.2", 42 | "postcss-loader": "^2.1.5", 43 | "rimraf": "^2.6.0", 44 | "url-loader": "^1.0.1", 45 | "vue": "^2.3.3", 46 | "vue-loader": "^14.0.0", 47 | "vue-router": "^2.7.0", 48 | "vue-style-loader": "^3.0.1", 49 | "vue-template-compiler": "^2.3.4", 50 | "webpack": "^3.0.0", 51 | "webpack-merge": "^4.1.0" 52 | }, 53 | "peerDependencies": { 54 | "webpack": "^3.0.0 || ^4.0.0" 55 | }, 56 | "engines": { 57 | "node": ">=6.9" 58 | }, 59 | "ava": { 60 | "concurrency": 5, 61 | "failFast": true, 62 | "files": [ 63 | "test/*.test.js" 64 | ], 65 | "require": [ 66 | "babel-register" 67 | ] 68 | }, 69 | "author": "panyuqi", 70 | "license": "ISC" 71 | } 72 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file generate skeleton 3 | * @author panyuqi 4 | */ 5 | 6 | /* eslint-disable no-console, fecs-no-require */ 7 | 8 | const ssr = require('./ssr'); 9 | const {insertAt, isObject, isFunction, generateRouterScript} = require('./util'); 10 | 11 | const DEFAULT_PLUGIN_OPTIONS = { 12 | webpackConfig: {}, 13 | insertAfter: '
', 14 | quiet: false 15 | }; 16 | 17 | const DEFAULT_ENTRY_NAME = 'main'; 18 | 19 | const PLUGIN_NAME = 'VueSkeletonWebpackPlugin'; 20 | 21 | class SkeletonPlugin { 22 | 23 | constructor(options = {}) { 24 | this.options = Object.assign({}, DEFAULT_PLUGIN_OPTIONS, options); 25 | } 26 | 27 | apply(compiler) { 28 | let skeletons; 29 | // compatible with webpack 4.x 30 | if (compiler.hooks) { 31 | compiler.hooks.make.tapAsync(PLUGIN_NAME, (compilation, cb) => { 32 | if (!compilation.hooks.htmlWebpackPluginBeforeHtmlProcessing) { 33 | console.error('VueSkeletonWebpackPlugin must be placed after HtmlWebpackPlugin in `plugins`.'); 34 | return; 35 | } 36 | 37 | this.generateSkeletonForEntries(this.extractEntries(compiler.options.entry), compiler, compilation) 38 | .then(skeletonResults => { 39 | skeletons = skeletonResults.reduce((cur, prev) => Object.assign(prev, cur), {}); 40 | cb(); 41 | }) 42 | .catch(e => console.log(e)); 43 | 44 | compilation.hooks.htmlWebpackPluginBeforeHtmlProcessing.tapAsync(PLUGIN_NAME, (htmlPluginData, callback) => { 45 | this.injectToHtml(htmlPluginData, skeletons); 46 | callback(null, htmlPluginData); 47 | }); 48 | }); 49 | } 50 | else { 51 | compiler.plugin('make', (compilation, cb) => { 52 | this.generateSkeletonForEntries(this.extractEntries(compiler.options.entry), compiler, compilation) 53 | .then(skeletonResults => { 54 | skeletons = skeletonResults.reduce((cur, prev) => Object.assign(prev, cur), {}); 55 | cb(); 56 | }) 57 | .catch(e => console.log(e)); 58 | 59 | compilation.plugin('html-webpack-plugin-before-html-processing', (htmlPluginData, callback) => { 60 | this.injectToHtml(htmlPluginData, skeletons); 61 | callback(null, htmlPluginData); 62 | }); 63 | }); 64 | } 65 | } 66 | 67 | /** 68 | * format entries for all skeletons from options 69 | * 70 | * @param {Object} parentEntry entry in webpack.config 71 | * @return {Object} entries entries for all skeletons 72 | */ 73 | extractEntries(parentEntry) { 74 | let entry = Object.assign({}, this.options.webpackConfig.entry); 75 | let skeletonEntries; 76 | 77 | if (isObject(entry)) { 78 | skeletonEntries = entry; 79 | } 80 | else { 81 | let entryName = DEFAULT_ENTRY_NAME; 82 | 83 | if (isObject(parentEntry)) { 84 | entryName = Object.keys(parentEntry)[0]; 85 | } 86 | skeletonEntries = { 87 | [entryName]: entry 88 | }; 89 | } 90 | 91 | return skeletonEntries; 92 | } 93 | 94 | /** 95 | * find skeleton for current html-plugin in all skeletons 96 | * 97 | * @param {Object} htmlPluginData data for html-plugin 98 | * @param {Object} skeletons skeletons 99 | * @param {Object} target skeleton 100 | */ 101 | findSkeleton(htmlPluginData, skeletons = {}) { 102 | const usedChunks = Object.keys(htmlPluginData.assets.chunks); 103 | let entryKey; 104 | 105 | // find current processing entry 106 | if (Array.isArray(usedChunks)) { 107 | entryKey = Object.keys(skeletons).find(v => usedChunks.indexOf(v) > -1); 108 | } 109 | else { 110 | entryKey = DEFAULT_ENTRY_NAME; 111 | } 112 | 113 | return { 114 | name: entryKey, 115 | skeleton: skeletons[entryKey] 116 | }; 117 | } 118 | 119 | /** 120 | * inject HTML, CSS and JS 121 | * 122 | * @param {Object} htmlPluginData data for html-plugin 123 | * @param {Object} skeletons skeletons 124 | */ 125 | injectToHtml(htmlPluginData, skeletons = {}) { 126 | let {insertAfter} = this.options; 127 | const {name, skeleton} = this.findSkeleton(htmlPluginData, skeletons); 128 | if (!skeleton) { 129 | console.log('Empty entry for skeleton, please check your webpack.config.'); 130 | return; 131 | } 132 | const {html = '', css = '', script = ''} = skeleton; 133 | 134 | // insert inlined styles into html 135 | let headTagEndPos = htmlPluginData.html.lastIndexOf(''); 136 | htmlPluginData.html = insertAt(htmlPluginData.html, ``, headTagEndPos); 137 | 138 | // replace mounted point with ssr result in html 139 | if (isFunction(insertAfter)) { 140 | insertAfter = insertAfter(name); 141 | } 142 | let appPos = htmlPluginData.html.lastIndexOf(insertAfter) + insertAfter.length; 143 | htmlPluginData.html = insertAt(htmlPluginData.html, html + script, appPos); 144 | } 145 | 146 | /** 147 | * generate skeletons for all entries 148 | * 149 | * @param {Object} entries entries for all skeletons 150 | * @param {Object} compiler compiler 151 | * @param {Object} compilation compilation 152 | * @return {Promise} promise 153 | */ 154 | generateSkeletonForEntries(entries, compiler, compilation) { 155 | const {router, minimize, quiet} = this.options; 156 | 157 | return Promise.all(Object.keys(entries).map(entryKey => { 158 | let skeletonWebpackConfig = Object.assign({}, this.options.webpackConfig); 159 | 160 | // set current entry & output in webpack config 161 | skeletonWebpackConfig.entry = entries[entryKey]; 162 | if (!skeletonWebpackConfig.output) { 163 | skeletonWebpackConfig.output = {}; 164 | } 165 | skeletonWebpackConfig.output.filename = `skeleton-${entryKey}.js`; 166 | 167 | // inject router code in SPA mode 168 | let routerScript = ''; 169 | if (router) { 170 | const isMPA = !!(Object.keys(entries).length > 1); 171 | routerScript = generateRouterScript(router, minimize, isMPA, entryKey); 172 | } 173 | 174 | // server side render skeleton for current entry 175 | return ssr(skeletonWebpackConfig, { 176 | quiet, compilation, context: compiler.context 177 | }).then(({skeletonHTML, skeletonCSS}) => { 178 | return { 179 | [entryKey]: { 180 | html: skeletonHTML, 181 | css: skeletonCSS, 182 | script: routerScript 183 | } 184 | }; 185 | }); 186 | })); 187 | } 188 | 189 | static loader(ruleOptions = {}) { 190 | console.log('[DEPRECATED] SkeletonPlugin.loader is DEPRECATED now. Hot reload in dev mode is supported already, so you can remove this option.'); 191 | return Object.assign(ruleOptions, { 192 | loader: require.resolve('./loader'), 193 | options: Object.assign({}, ruleOptions.options) 194 | }); 195 | } 196 | } 197 | 198 | module.exports = SkeletonPlugin; 199 | -------------------------------------------------------------------------------- /src/loader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file loader 3 | * @desc Insert route of skeleton into router.js, so that developer can 4 | * visit route path in dev mode to debug skeleton components 5 | * @author panyuqi 6 | */ 7 | 8 | /* eslint-disable fecs-no-require */ 9 | 10 | const loaderUtils = require('loader-utils'); 11 | const {createHash} = require('crypto'); 12 | const insertAt = require('./util').insertAt; 13 | 14 | const DEFAULT_LOADER_OPTIONS = { 15 | // template of importing skeleton component 16 | importTemplate: 'import [nameHash] from \'@/pages/[nameCap].vue\';', 17 | // template of route path 18 | routePathTemplate: '/skeleton-[name]', 19 | // position to insert route object in router.js file 20 | insertAfter: 'routes: [' 21 | }; 22 | 23 | const ENTRY_NAME_HOLDER = /\[name\]/gi; 24 | const ENTRY_NAME_CAP_HOLDER = /\[nameCap\]/gi; 25 | const ENTRY_NAME_HASH_HOLDER = /\[nameHash\]/gi; 26 | 27 | module.exports = function (source) { 28 | const options = Object.assign({}, DEFAULT_LOADER_OPTIONS, loaderUtils.getOptions(this)); 29 | let {entry, importTemplate, routePathTemplate, insertAfter} = options; 30 | 31 | // find position to insert in router.js 32 | let routesPos = source.indexOf(insertAfter) + insertAfter.length; 33 | 34 | entry = Array.isArray(entry) ? entry : [entry]; 35 | 36 | entry.forEach(entryName => { 37 | // capitalize first letter in entryName eg.skeleton -> Skeleton 38 | let entryCap = entryName.replace(/([a-z])(.*)/, (w, firstLetter, rest) => firstLetter.toUpperCase() + rest); 39 | let entryHash = `_${(new Date()).getTime()}${createHash('md5').update(entryName + '-skeleton').digest('hex')}`; 40 | 41 | // replace placeholder in routeTpl and importTpl 42 | let [skeletonRoutePath, importExpression] = [routePathTemplate, importTemplate].map( 43 | pathStr => pathStr.replace(ENTRY_NAME_HOLDER, entryName) 44 | .replace(ENTRY_NAME_CAP_HOLDER, entryCap) 45 | .replace(ENTRY_NAME_HASH_HOLDER, entryHash) 46 | ); 47 | 48 | // route object to insert 49 | let routeExpression = `{ 50 | path: '${skeletonRoutePath}', 51 | name: 'skeleton-${entryName}', 52 | component: ${entryHash} 53 | },`; 54 | 55 | // insert route object into routes array 56 | source = insertAt(source, routeExpression, routesPos); 57 | 58 | // insert import sentence in the head 59 | source += importExpression; 60 | }); 61 | 62 | return source; 63 | }; 64 | -------------------------------------------------------------------------------- /src/ssr.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file ssr 3 | * @desc Use vue ssr to render skeleton components. The result contains html and css. 4 | * @author panyuqi 5 | */ 6 | 7 | /* eslint-disable no-console, fecs-no-require */ 8 | 9 | const path = require('path'); 10 | const webpack = require('webpack'); 11 | const webpackMajorVersion = require('webpack/package.json').version.split('.')[0]; 12 | const NodeTemplatePlugin = require('webpack/lib/node/NodeTemplatePlugin'); 13 | const NodeTargetPlugin = require('webpack/lib/node/NodeTargetPlugin'); 14 | const LoaderTargetPlugin = require('webpack/lib/LoaderTargetPlugin'); 15 | const LibraryTemplatePlugin = require('webpack/lib/LibraryTemplatePlugin'); 16 | const SingleEntryPlugin = require('webpack/lib/SingleEntryPlugin'); 17 | const MultiEntryPlugin = require('webpack/lib/MultiEntryPlugin'); 18 | const ExternalsPlugin = require('webpack/lib/ExternalsPlugin'); 19 | 20 | const createBundleRenderer = require('vue-server-renderer').createBundleRenderer; 21 | const nodeExternals = require('webpack-node-externals'); 22 | 23 | let MiniCssExtractPlugin; 24 | let ExtractTextPlugin; 25 | if (webpackMajorVersion === '4') { 26 | MiniCssExtractPlugin = require('mini-css-extract-plugin'); 27 | } 28 | else { 29 | ExtractTextPlugin = require('extract-text-webpack-plugin'); 30 | } 31 | 32 | module.exports = function renderSkeleton (serverWebpackConfig, {quiet = false, compilation, context}) { 33 | let {path: outputPath, publicPath: outputPublicPath} = compilation.outputOptions; 34 | // get entry name from webpack.conf 35 | let outputJSPath = path.join(outputPath, serverWebpackConfig.output.filename); 36 | let outputBasename = path.basename(outputJSPath, path.extname(outputJSPath)); 37 | let outputCssBasename = `${outputBasename}.css`; 38 | let outputCSSPath = path.join(outputPath, outputCssBasename); 39 | 40 | if (!quiet) { 41 | console.log(`Generate skeleton for ${outputBasename}...`); 42 | } 43 | 44 | let originalRules; 45 | if (webpackMajorVersion !== '4') { 46 | // if user passed in some special module rules for Skeleton, use it directly 47 | originalRules = compilation.options.module.rules; 48 | if (serverWebpackConfig.module && serverWebpackConfig.module.rules) { 49 | compilation.options.module.rules = serverWebpackConfig.module.rules; 50 | } 51 | else { 52 | // otherwise use rules from parent compiler 53 | let vueRule = compilation.options.module.rules.find(rule => { 54 | return rule.test && rule.test.test && rule.test.test('test.vue'); 55 | }); 56 | 57 | if (vueRule && vueRule.use && vueRule.use.length) { 58 | let vueLoader = vueRule.use.find(rule => { 59 | return rule.loader = 'vue-loader'; 60 | }); 61 | 62 | vueLoader.options.extractCSS = true; 63 | delete vueLoader.options.loaders; 64 | } 65 | } 66 | } 67 | 68 | const outputOptions = { 69 | filename: outputJSPath, 70 | publicPath: outputPublicPath 71 | }; 72 | 73 | const childCompiler = compilation.createChildCompiler('vue-skeleton-webpack-plugin-compiler', outputOptions); 74 | 75 | childCompiler.context = context; 76 | new LibraryTemplatePlugin(undefined, 'commonjs2').apply(childCompiler); 77 | new NodeTargetPlugin().apply(childCompiler); 78 | if (Array.isArray(serverWebpackConfig.entry)) { 79 | new MultiEntryPlugin(context, serverWebpackConfig.entry, undefined).apply(childCompiler); 80 | } 81 | else { 82 | new SingleEntryPlugin(context, serverWebpackConfig.entry, undefined).apply(childCompiler); 83 | } 84 | new LoaderTargetPlugin('node').apply(childCompiler); 85 | new ExternalsPlugin('commonjs2', serverWebpackConfig.externals || nodeExternals({ 86 | whitelist: /\.css$/ 87 | })).apply(childCompiler); 88 | if (webpackMajorVersion === '4') { 89 | new MiniCssExtractPlugin({ 90 | filename: outputCSSPath 91 | }).apply(childCompiler); 92 | } 93 | else { 94 | new ExtractTextPlugin({ 95 | filename: outputCSSPath 96 | }).apply(childCompiler); 97 | } 98 | 99 | return new Promise((resolve, reject) => { 100 | childCompiler.runAsChild((err, entries, childCompilation) => { 101 | if (childCompilation && childCompilation.errors && childCompilation.errors.length) { 102 | const errorDetails = childCompilation.errors.map(error => error.message + (error.error ? ':\n' + error.error : '')).join('\n'); 103 | reject(new Error('Child compilation failed:\n' + errorDetails)); 104 | } 105 | else if (err) { 106 | reject(err); 107 | } 108 | else { 109 | let bundle = childCompilation.assets[outputJSPath].source(); 110 | let skeletonCSS = ''; 111 | if (childCompilation.assets[outputCSSPath]) { 112 | skeletonCSS = childCompilation.assets[outputCSSPath].source(); 113 | } 114 | 115 | // delete JS & CSS files 116 | delete compilation.assets[outputJSPath]; 117 | delete compilation.assets[outputCSSPath]; 118 | delete compilation.assets[`${outputJSPath}.map`]; 119 | delete compilation.assets[`${outputCSSPath}.map`]; 120 | // create renderer with bundle 121 | let renderer = createBundleRenderer(bundle); 122 | // use vue ssr to render skeleton 123 | renderer.renderToString({}, (err, skeletonHTML) => { 124 | if (err) { 125 | reject(err); 126 | } 127 | else { 128 | if (webpackMajorVersion !== '4') { 129 | compilation.options.module.rules = originalRules; 130 | } 131 | resolve({skeletonHTML, skeletonCSS}); 132 | } 133 | }); 134 | } 135 | }); 136 | }); 137 | }; 138 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file utils 3 | * @author panyuqi 4 | */ 5 | 6 | /* eslint-disable fecs-no-require */ 7 | const pathToRegexp = require('path-to-regexp'); 8 | const uglifyJS = require('uglify-es'); 9 | 10 | const DEFAULT_PATH = '*'; 11 | 12 | module.exports = { 13 | insertAt: (origin, str, pos) => { 14 | return [ 15 | origin.slice(0, pos), 16 | str, 17 | origin.slice(pos) 18 | ].join(''); 19 | }, 20 | 21 | isObject: (obj) => Object.prototype.toString.call(obj).match('Object'), 22 | 23 | isFunction: (func) => func && Object.prototype.toString.call(func) === '[object Function]', 24 | 25 | routes2Reg: (routes) => { 26 | let reg; 27 | if (typeof routes === 'string') { 28 | reg = pathToRegexp(routes); 29 | } 30 | else if (routes instanceof RegExp) { 31 | return routes; 32 | } 33 | 34 | return reg; 35 | }, 36 | 37 | generateRouterScript: ({mode = 'history', routes = []}, minimize = false, isMPA, entryName) => { 38 | // format routes if it's an object 39 | if (module.exports.isObject(routes)) { 40 | routes = Object.keys(routes).map(key => { 41 | return { 42 | path: key, 43 | skeletonId: routes[key] 44 | }; 45 | }); 46 | } 47 | 48 | let skeletonsClause = []; 49 | let switchClause = []; 50 | 51 | if (isMPA) { 52 | routes = routes.filter(route => route.entryName === entryName); 53 | } 54 | 55 | routes.forEach(({skeletonId, path}, i) => { 56 | // convert route string to regexp 57 | path = path === '*' 58 | ? /^.*$/ 59 | : module.exports.routes2Reg(path); 60 | 61 | skeletonsClause.push(`{ 62 | id: '${skeletonId}', 63 | el: document.querySelector('#${skeletonId}') 64 | }`); 65 | 66 | switchClause.push(` 67 | ${i === 0 ? '' : 'else '}if (isMatched(${path}, '${mode}')) { 68 | showSkeleton('${skeletonId}'); 69 | } 70 | `); 71 | }); 72 | 73 | let sourceScript = ` 74 | var pathname = window.location.pathname; 75 | var hash = window.location.hash; 76 | var skeletons = [${skeletonsClause.join(',')}]; 77 | var isMatched = function(pathReg, mode) { 78 | if (mode === 'hash') { 79 | return pathReg.test(hash.replace('#', '')); 80 | } 81 | else if (mode === 'history') { 82 | return pathReg.test(pathname); 83 | } 84 | return false; 85 | }; 86 | var showSkeleton = function(skeletonId) { 87 | for (var i = 0; i < skeletons.length; i++) { 88 | var skeleton = skeletons[i]; 89 | if (skeletonId === skeleton.id) { 90 | skeleton.el.style = 'display:block;'; 91 | } 92 | else { 93 | skeleton.el.style = 'display:none;'; 94 | } 95 | } 96 | }; 97 | ${switchClause.join('')} 98 | `; 99 | 100 | // use uglify to minimize source code maybe in prod mode 101 | if (minimize) { 102 | let {error, code} = uglifyJS.minify(sourceScript); 103 | if (!error) { 104 | sourceScript = code.toString(); 105 | } 106 | else { 107 | console.error(error); 108 | } 109 | } 110 | return ``; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /test/loader.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file test loader.js 3 | * @author panyuqi (pyqiverson@gmail.com) 4 | */ 5 | 6 | /* eslint-disable fecs-use-standard-promise */ 7 | 8 | import 'babel-polyfill'; 9 | import * as path from 'path'; 10 | import Promise from 'bluebird'; 11 | import test from 'ava'; 12 | import { 13 | runWebpackCompilerMemoryFs, 14 | testFs, 15 | webpackMajorVersion 16 | } from './utils.js'; 17 | 18 | import simpleConfig from '../examples/simple/webpack.config.js'; 19 | 20 | const fs = testFs; 21 | 22 | const simpleExamplePath = path.resolve(__dirname, '../examples/simple'); 23 | const webpackBuildPath = path.resolve(simpleExamplePath, './dist'); 24 | 25 | const readFile = Promise.promisify(fs.readFile, {context: fs}); 26 | 27 | let webpackBuildStats = null; 28 | 29 | if (webpackMajorVersion === '4') { 30 | test.skip('will not be run', t => { 31 | t.fail(); 32 | }); 33 | } 34 | else { 35 | 36 | test.before('run webpack build first', async t => { 37 | webpackBuildStats = await runWebpackCompilerMemoryFs(simpleConfig); 38 | }); 39 | 40 | test('it should run successfully', async t => { 41 | let {stats, errors} = webpackBuildStats; 42 | t.falsy(stats.hasWarnings() && errors.hasWarnings()); 43 | }); 44 | 45 | test('it should insert skeleton route into routes', async t => { 46 | let htmlContent = await readFile(path.join(webpackBuildPath, 'static/js/app.js')); 47 | htmlContent = htmlContent.toString(); 48 | let insertedRoute = `path: '/skeleton',`; 49 | t.true(htmlContent.includes(insertedRoute)); 50 | }); 51 | } 52 | -------------------------------------------------------------------------------- /test/multi-skeleton.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file test multi-skeleton 3 | * @author panyuqi (pyqiverson@gmail.com) 4 | */ 5 | 6 | /* eslint-disable fecs-use-standard-promise */ 7 | 8 | import 'babel-polyfill'; 9 | import * as path from 'path'; 10 | import Promise from 'bluebird'; 11 | import test from 'ava'; 12 | import { 13 | runWebpackCompilerMemoryFs, 14 | testFs, 15 | webpackMajorVersion 16 | } from './utils.js'; 17 | 18 | import multipageConfig from '../examples/multi-skeleton/webpack.config.js'; 19 | 20 | const fs = testFs; 21 | 22 | const multipageExamplePath = path.resolve(__dirname, '../examples/multi-skeleton'); 23 | const webpackBuildPath = path.resolve(multipageExamplePath, './dist'); 24 | 25 | const readFile = Promise.promisify(fs.readFile, {context: fs}); 26 | 27 | let webpackBuildStats = null; 28 | 29 | if (webpackMajorVersion === '4') { 30 | test.skip('will not be run', t => { 31 | t.fail(); 32 | }); 33 | } 34 | else { 35 | 36 | test.before('run webpack build first', async t => { 37 | webpackBuildStats = await runWebpackCompilerMemoryFs(multipageConfig); 38 | }); 39 | 40 | test('it should run successfully', async t => { 41 | let {errors, warnings} = webpackBuildStats; 42 | t.falsy(errors.length && warnings.length); 43 | }); 44 | 45 | test('it should insert multi skeletons into index.html', async t => { 46 | let result = await readFile(path.join(webpackBuildPath, 'index.html')); 47 | let skeleton1DOM = '
{ 31 | t.fail(); 32 | }); 33 | } 34 | else { 35 | 36 | test.before('run webpack build first', async t => { 37 | webpackBuildStats = await runWebpackCompilerMemoryFs(multipageConfig); 38 | }); 39 | 40 | test('it should run successfully', async t => { 41 | let {errors, warnings} = webpackBuildStats; 42 | t.falsy(errors.length && warnings.length); 43 | }); 44 | 45 | test('it should insert relative skeleton into every page', async t => { 46 | let result = await Promise.all([ 47 | readFile(path.join(webpackBuildPath, 'page1.html')), 48 | readFile(path.join(webpackBuildPath, 'page2.html')) 49 | ]); 50 | t.true(result[0].toString().includes('Skeleton of Page1')); 51 | t.true(result[1].toString().includes('Skeleton of Page2')); 52 | }); 53 | 54 | test('it should insert skeleton route successfully', async t => { 55 | let page1JSContent = await readFile(path.join(webpackBuildPath, 'static/js/page1.js')); 56 | let insertedRoute = 'path: \'/page1-skeleton\''; 57 | t.true(page1JSContent.toString().includes(insertedRoute)); 58 | }); 59 | } 60 | -------------------------------------------------------------------------------- /test/multipage2.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file test multipage, use html-webpack-plugin 3 | * @author panyuqi (pyqiverson@gmail.com) 4 | */ 5 | 6 | /* eslint-disable fecs-use-standard-promise */ 7 | 8 | import 'babel-polyfill'; 9 | import * as path from 'path'; 10 | import Promise from 'bluebird'; 11 | import test from 'ava'; 12 | import { 13 | runWebpackCompilerMemoryFs, 14 | testFs, 15 | webpackMajorVersion 16 | } from './utils.js'; 17 | 18 | import multipageConfig from '../examples/multipage2/webpack.config.js'; 19 | 20 | const fs = testFs; 21 | 22 | const multipageExamplePath = path.resolve(__dirname, '../examples/multipage2'); 23 | const webpackBuildPath = path.resolve(multipageExamplePath, './dist'); 24 | 25 | const readFile = Promise.promisify(fs.readFile, {context: fs}); 26 | 27 | let webpackBuildStats = null; 28 | 29 | if (webpackMajorVersion === '4') { 30 | test.skip('will not be run', t => { 31 | t.fail(); 32 | }); 33 | } 34 | else { 35 | test.before('run webpack build first', async t => { 36 | webpackBuildStats = await runWebpackCompilerMemoryFs(multipageConfig); 37 | }); 38 | 39 | test('it should run successfully', async t => { 40 | let {stats, errors} = webpackBuildStats; 41 | t.falsy(stats.hasWarnings() && errors.hasWarnings()); 42 | }); 43 | 44 | test('it should insert relative skeleton into every page', async t => { 45 | let result = await Promise.all([ 46 | readFile(path.join(webpackBuildPath, 'page1.html')), 47 | readFile(path.join(webpackBuildPath, 'page2.html')) 48 | ]); 49 | t.true(result[0].toString().includes('Skeleton of Page1')); 50 | t.true(result[1].toString().includes('Skeleton of Page2')); 51 | }); 52 | 53 | test('it should insert skeleton route successfully', async t => { 54 | let page1JSContent = await readFile(path.join(webpackBuildPath, 'static/js/page1.js')); 55 | let insertedRoute = 'path: \'/page1-skeleton\''; 56 | t.true(page1JSContent.toString().includes(insertedRoute)); 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /test/multipage3.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file test multipage, use html-webpack-plugin 3 | * @author panyuqi (pyqiverson@gmail.com) 4 | */ 5 | 6 | /* eslint-disable fecs-use-standard-promise */ 7 | 8 | import 'babel-polyfill'; 9 | import * as path from 'path'; 10 | import Promise from 'bluebird'; 11 | import test from 'ava'; 12 | import { 13 | runWebpackCompilerMemoryFs, 14 | testFs, 15 | webpackMajorVersion 16 | } from './utils.js'; 17 | 18 | import multipageConfig from '../examples/multipage3/webpack.config.js'; 19 | 20 | const fs = testFs; 21 | 22 | const multipageExamplePath = path.resolve(__dirname, '../examples/multipage3'); 23 | const webpackBuildPath = path.resolve(multipageExamplePath, './dist'); 24 | 25 | const readFile = Promise.promisify(fs.readFile, {context: fs}); 26 | 27 | let webpackBuildStats = null; 28 | 29 | if (webpackMajorVersion === '4') { 30 | test.skip('will not be run', t => { 31 | t.fail(); 32 | }); 33 | } 34 | else { 35 | test.before('run webpack build first', async t => { 36 | webpackBuildStats = await runWebpackCompilerMemoryFs(multipageConfig); 37 | }); 38 | 39 | test('it should run successfully', async t => { 40 | let {stats, errors} = webpackBuildStats; 41 | t.falsy(stats.hasWarnings() && errors.hasWarnings()); 42 | }); 43 | 44 | test('it should insert skeleton into page1, but not page2', async t => { 45 | let result = await Promise.all([ 46 | readFile(path.join(webpackBuildPath, 'page1.html')), 47 | readFile(path.join(webpackBuildPath, 'page2.html')) 48 | ]); 49 | t.true(result[0].toString().includes('Skeleton of Page1')); 50 | t.false(result[1].toString().includes('Skeleton of Page2')); 51 | }); 52 | 53 | test('it should insert skeleton route successfully', async t => { 54 | let page1JSContent = await readFile(path.join(webpackBuildPath, 'static/js/page1.js')); 55 | let insertedRoute = 'path: \'/page1-skeleton\''; 56 | t.true(page1JSContent.toString().includes(insertedRoute)); 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /test/simple.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file a simple test case 3 | * @author panyuqi (pyqiverson@gmail.com) 4 | */ 5 | 6 | /* eslint-disable fecs-use-standard-promise */ 7 | 8 | import 'babel-polyfill'; 9 | import * as path from 'path'; 10 | import Promise from 'bluebird'; 11 | import test from 'ava'; 12 | import { 13 | runWebpackCompilerMemoryFs, 14 | testFs, 15 | webpackMajorVersion 16 | } from './utils.js'; 17 | 18 | import simpleConfig from '../examples/simple/webpack.config.js'; 19 | 20 | const fs = testFs; 21 | 22 | const simpleExamplePath = path.resolve(__dirname, '../examples/simple'); 23 | const webpackBuildPath = path.resolve(simpleExamplePath, './dist'); 24 | 25 | const readFile = Promise.promisify(fs.readFile, {context: fs}); 26 | 27 | let webpackBuildStats = null; 28 | 29 | if (webpackMajorVersion === '4') { 30 | test.skip('will not be run', t => { 31 | t.fail(); 32 | }); 33 | } 34 | else { 35 | test.before('run webpack build first', async t => { 36 | webpackBuildStats = await runWebpackCompilerMemoryFs(simpleConfig); 37 | }); 38 | 39 | test('it should run successfully', async t => { 40 | let {errors, warnings} = webpackBuildStats; 41 | t.falsy(errors.length && warnings.length); 42 | }); 43 | 44 | test('it should insert the ssr result of skeleton into mounted point', async t => { 45 | let htmlContent = await readFile(path.join(webpackBuildPath, 'index.html')); 46 | 47 | htmlContent = htmlContent.toString(); 48 | // ssr dom has been injected into mounted point 49 | t.true(htmlContent.includes('
.skeleton-header')); 53 | 54 | // emit images imported from Skeleton.vue 55 | t.true(htmlContent.includes('url(static/img/logo.90f7061.jpg)')); 56 | t.true(fs.existsSync(path.join(webpackBuildPath, 'static/img/logo.90f7061.jpg'))); 57 | }); 58 | 59 | test('it should use autoprefixer with postcss correctly.', async t => { 60 | let htmlContent = await readFile(path.join(webpackBuildPath, 'index.html')); 61 | htmlContent = htmlContent.toString(); 62 | 63 | // autoprefixer 64 | t.true(htmlContent.includes('display: -webkit-box;')); 65 | }); 66 | } 67 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file utils 3 | * @author panyuqi (pyqiverson@gmail.com) 4 | */ 5 | 6 | /* eslint-disable fecs-use-standard-promise, fecs-no-require */ 7 | 8 | const MFS = require('memory-fs'); 9 | const Promise = require('bluebird'); 10 | const webpack = require('webpack'); 11 | const webpackMajorVersion = require('webpack/package.json').version.split('.')[0]; 12 | 13 | const outputFileSystem = new MFS(); 14 | 15 | exports.runWebpackCompilerMemoryFs = function runWebpackCompiler(config) { 16 | if (webpackMajorVersion === '4') { 17 | config.mode = 'production'; 18 | } 19 | 20 | const compiler = webpack(config); 21 | 22 | compiler.outputFileSystem = outputFileSystem; 23 | const run = Promise.promisify(compiler.run, {context: compiler}); 24 | 25 | return run() 26 | .then(stats => { 27 | const compilation = stats.compilation; 28 | const {errors, warnings, assets, entrypoints} = compilation; 29 | 30 | const statsJson = stats.toJson(); 31 | 32 | return { 33 | assets, 34 | entrypoints, 35 | errors, 36 | warnings, 37 | stats, 38 | statsJson 39 | }; 40 | }); 41 | }; 42 | 43 | exports.testFs = outputFileSystem; 44 | 45 | exports.webpackMajorVersion = webpackMajorVersion; 46 | -------------------------------------------------------------------------------- /test/webpack4.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file a simple test case for webpack 4 3 | * @author panyuqi (pyqiverson@gmail.com) 4 | */ 5 | 6 | /* eslint-disable fecs-use-standard-promise */ 7 | 8 | import 'babel-polyfill'; 9 | import * as path from 'path'; 10 | import Promise from 'bluebird'; 11 | import test from 'ava'; 12 | import { 13 | runWebpackCompilerMemoryFs, 14 | testFs, 15 | webpackMajorVersion 16 | } from './utils.js'; 17 | 18 | const fs = testFs; 19 | 20 | const simpleExamplePath = path.resolve(__dirname, '../examples/webpack4'); 21 | const webpackBuildPath = path.resolve(simpleExamplePath, './dist'); 22 | 23 | const readFile = Promise.promisify(fs.readFile, {context: fs}); 24 | 25 | let webpackBuildStats = null; 26 | 27 | if (webpackMajorVersion === '4') { 28 | let simpleConfig = require('../examples/webpack4/webpack.config.js'); 29 | 30 | test.before('run webpack build first', async t => { 31 | webpackBuildStats = await runWebpackCompilerMemoryFs(simpleConfig); 32 | }); 33 | 34 | test('it should run successfully', async t => { 35 | let {errors, warnings} = webpackBuildStats; 36 | t.falsy(errors.length && warnings.length); 37 | }); 38 | 39 | test('it should insert the ssr result of skeleton into mounted point', async t => { 40 | let htmlContent = await readFile(path.join(webpackBuildPath, 'index.html')); 41 | 42 | htmlContent = htmlContent.toString(); 43 | // ssr dom has been injected into mounted point 44 | t.true(htmlContent.includes('
.skeleton-header')); 48 | 49 | // emit images imported from Skeleton.vue 50 | t.true(htmlContent.includes('url(static/img/logo.90f7061.jpg)')); 51 | t.true(fs.existsSync(path.join(webpackBuildPath, 'static/img/logo.90f7061.jpg'))); 52 | }); 53 | 54 | test('it should use autoprefixer with postcss correctly.', async t => { 55 | let htmlContent = await readFile(path.join(webpackBuildPath, 'index.html')); 56 | htmlContent = htmlContent.toString(); 57 | 58 | // autoprefixer 59 | t.true(htmlContent.includes('display: -webkit-box;')); 60 | }); 61 | } 62 | else { 63 | test.skip('will not be run', t => { 64 | t.fail(); 65 | }); 66 | } 67 | --------------------------------------------------------------------------------