From 1c7578a2ca8aad433bdbeeb1dc95e6e3b14138fd Mon Sep 17 00:00:00 2001 From: levym Date: Thu, 9 Nov 2017 13:41:15 +0000 Subject: [PATCH] Implement hot-reloading without browser refresh. * Add dynamic component loading * Add webpack code-splitting * Add manifest and vendor bundles --- template/config/webpack.config.base.js | 1 + template/config/webpack.config.prod.js | 15 +++++++++++-- template/package-lock.json | 15 +++++++++++-- template/package.json | 4 +++- template/src/main.ts | 19 +++++++++++------ template/src/router.ts | 21 +++++++++++++++++++ template/src/util/hot-reload.ts | 20 ++++++++++++++++++ .../typings/vue-hot-reload-api/index.d.ts | 7 +++++++ 8 files changed, 91 insertions(+), 11 deletions(-) create mode 100644 template/src/util/hot-reload.ts create mode 100644 template/typings/vue-hot-reload-api/index.d.ts diff --git a/template/config/webpack.config.base.js b/template/config/webpack.config.base.js index 7299010..d682d7e 100644 --- a/template/config/webpack.config.base.js +++ b/template/config/webpack.config.base.js @@ -8,6 +8,7 @@ let config = { output: { path: helpers.root('/dist'), filename: 'js/[name].[hash].js', + chunkFilename: 'js/[name].[hash].js', publicPath: '/' }, devtool: 'source-map', diff --git a/template/config/webpack.config.prod.js b/template/config/webpack.config.prod.js index abc3ab2..0b99308 100644 --- a/template/config/webpack.config.prod.js +++ b/template/config/webpack.config.prod.js @@ -1,5 +1,6 @@ const glob = require('glob'), path = require('path'), + CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin'), UglifyJsPlugin = require('webpack/lib/optimize/UglifyJsPlugin'), HtmlWebpackPlugin = require('html-webpack-plugin'), CompressionPlugin = require('compression-webpack-plugin'), @@ -72,6 +73,16 @@ webpackConfig.module.rules[0].options = { }; webpackConfig.plugins = [...webpackConfig.plugins, + new CommonsChunkPlugin({ + name: 'vendor', + minChunks: function(module){ + return module.context && module.context.indexOf('node_modules') !== -1; + } + }), + new CommonsChunkPlugin({ + name: 'manifest', + minChunks: Infinity + }), extractSass, purifyCss, new HtmlWebpackPlugin({ @@ -92,12 +103,12 @@ webpackConfig.plugins = [...webpackConfig.plugins, } }), new UglifyJsPlugin({ - include: /\.min\.js$/, + include: /\.js$/, minimize: true }), new CompressionPlugin({ asset: '[path].gz[query]', - test: /\.min\.js$/ + test: /\.js$/ }), new DefinePlugin({ 'process.env': env diff --git a/template/package-lock.json b/template/package-lock.json index 6c71055..7d2f64b 100644 --- a/template/package-lock.json +++ b/template/package-lock.json @@ -22,6 +22,12 @@ "integrity": "sha512-w+LjztaZbgZWgt/y/VMP5BUAWLtSyoIJhXyW279hehLPyubDoBNwvhcj3WaSptcekuKYeTCVxrq60rdLc6ImJA==", "dev": true }, + "@types/webpack-env": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.13.2.tgz", + "integrity": "sha512-pjbzi3A1Y4iLpNdNZNG4loIZKtYOnpCQY82bnsHi9lQXl4f3ul0TDsd1fUd10jbgCXR5bwaP4Ffy1BDLuEZpaQ==", + "dev": true + }, "abbrev": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", @@ -10451,8 +10457,7 @@ "uiv": { "version": "0.12.3", "resolved": "https://registry.npmjs.org/uiv/-/uiv-0.12.3.tgz", - "integrity": "sha512-aJ8x3tVooSaUIpEf8S+dZ9OlXaX+9LcdnEi1N1H55SBGaXipO6awtPIY0Bsrqp727P1pfGyjwlbZKMqTqCmogw==", - "dev": true + "integrity": "sha512-aJ8x3tVooSaUIpEf8S+dZ9OlXaX+9LcdnEi1N1H55SBGaXipO6awtPIY0Bsrqp727P1pfGyjwlbZKMqTqCmogw==" }, "ultron": { "version": "1.0.2", @@ -10787,6 +10792,12 @@ "resolved": "https://registry.npmjs.org/vue-class-component/-/vue-class-component-6.1.0.tgz", "integrity": "sha512-kTtT2YgWPBpyiJFTvLXA6gOHVzMYxF9IKwMEPRRh1aF24m8dLrpXSmvheQU9zoSEEtUza44a1tZhDEVC1l+KCg==" }, + "vue-hot-reload-api": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.2.0.tgz", + "integrity": "sha512-Hn0jdFiNfNx4+Nxz1vokYUsP3mwpn9r/XomLrXA+KafYaiR7LNQG1fxtpVklD4KxD5GluXRiSoWNDf0H9BdsJw==", + "dev": true + }, "vue-property-decorator": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/vue-property-decorator/-/vue-property-decorator-6.0.0.tgz", diff --git a/template/package.json b/template/package.json index 1d3057a..e8eec39 100644 --- a/template/package.json +++ b/template/package.json @@ -20,7 +20,7 @@ "coverage:run": "cross-env NODE_ENV=development karma start karma.coverage.js", "dev": "webpack-dev-server --hot --inline", "lint": "tslint src/**/*.ts", - "serve": "http-server dist/ -o", + "serve": "http-server dist/ -g -o", "test": "cross-env NODE_ENV=development karma start karma.unit.js", "test:debug": "cross-env NODE_ENV=development karma start karma.debug.js", "test:watch": "cross-env NODE_ENV=development karma start karma.unit.js --singleRun=false --auto-watch" @@ -37,6 +37,7 @@ "@types/mocha": "~2.2.44", "@types/node": "~8.0.47", "@types/sinon": "~2.3.7", + "@types/webpack-env": "^1.13.2", "autoprefixer": "^7.1.6", "awesome-typescript-loader": "~3.3.0", "bootstrap-sass": "^3.3.7", @@ -84,6 +85,7 @@ "tslint-loader": "~3.5.3", "typescript": "~2.6.1", "url-loader": "^0.6.2", + "vue-hot-reload-api": "^2.2.0", "webpack": "~3.8.1", "webpack-dev-server": "~2.9.4" } diff --git a/template/src/main.ts b/template/src/main.ts index a69eeac..2b76ae4 100644 --- a/template/src/main.ts +++ b/template/src/main.ts @@ -1,19 +1,26 @@ import Vue from 'vue'; import VueRouter from 'vue-router'; +import { isHot, makeHot, reload } from './util/hot-reload'; +import { createRouter } from './router'; + +const navbarComponent = () => import('./components/navbar').then(({ NavbarComponent }) => NavbarComponent); +// const navbarComponent = () => import(/* webpackChunkName: 'navbar' */'./components/navbar').then(({ NavbarComponent }) => NavbarComponent); import './sass/main.scss'; -import { HomeComponent } from './components/home'; -import { AboutComponent } from './components/about'; -import { ListComponent } from './components/list'; -import { NavbarComponent } from './components/navbar'; +if (process.env.ENV === 'development' && isHot()) { + const navbarModuleId = './components/navbar'; -import { createRouter } from './router'; + // first arguments for `module.hot.accept` and `require` methods have to be static strings + // see https://github.com/webpack/webpack/issues/5668 + makeHot(navbarModuleId, navbarComponent, + module.hot.accept('./components/navbar', () => reload(navbarModuleId, (require('./components/navbar')).NavbarComponent))); +} new Vue({ el: '#app-main', router: createRouter(), components: { - 'navbar': NavbarComponent + 'navbar': navbarComponent } }); diff --git a/template/src/router.ts b/template/src/router.ts index 5e43ef8..c44942c 100644 --- a/template/src/router.ts +++ b/template/src/router.ts @@ -1,9 +1,30 @@ import Vue from 'vue'; import VueRouter, { Location, Route, RouteConfig } from 'vue-router'; +import { isHot, makeHot, reload } from './util/hot-reload'; const homeComponent = () => import('./components/home').then(({ HomeComponent }) => HomeComponent); const aboutComponent = () => import('./components/about').then(({ AboutComponent }) => AboutComponent); const listComponent = () => import('./components/list').then(({ ListComponent }) => ListComponent); +// const homeComponent = () => import(/* webpackChunkName: 'home' */'./components/home').then(({ HomeComponent }) => HomeComponent); +// const aboutComponent = () => import(/* webpackChunkName: 'about' */'./components/about').then(({ AboutComponent }) => AboutComponent); +// const listComponent = () => import(/* webpackChunkName: 'list' */'./components/list').then(({ ListComponent }) => ListComponent); + +if (process.env.ENV === 'development' && isHot()) { + const homeModuleId = './components/home'; + const aboutModuleId = './components/about'; + const listModuleId = './components/list'; + + // first arguments for `module.hot.accept` and `require` methods have to be static strings + // see https://github.com/webpack/webpack/issues/5668 + makeHot(homeModuleId, homeComponent, + module.hot.accept('./components/home', () => reload(homeModuleId, (require('./components/home')).HomeComponent))); + + makeHot(aboutModuleId, aboutComponent, + module.hot.accept('./components/about', () => reload(aboutModuleId, (require('./components/about')).AboutComponent))); + + makeHot(listModuleId, listComponent, + module.hot.accept('./components/list', () => reload(listModuleId, (require('./components/list')).ListComponent))); +} Vue.use(VueRouter); diff --git a/template/src/util/hot-reload.ts b/template/src/util/hot-reload.ts new file mode 100644 index 0000000..87c89e5 --- /dev/null +++ b/template/src/util/hot-reload.ts @@ -0,0 +1,20 @@ +import Vue, { Component } from 'vue'; +import * as api from 'vue-hot-reload-api'; + +export const isHot = () => module.hot; + +export async function makeHot(id: string, componentLoader: () => Promise, acceptFunc: void) { + if (isHot()) { + api.install(Vue); + if (!api.compatible) { + throw new Error('vue-hot-reload-api is not compatible with the version of Vue you are using.'); + } + + const loadedComponent = await componentLoader(); + api.createRecord(id, loadedComponent); + } +} + +export function reload(id: string, component: Component) { + api.reload(id, component); +} diff --git a/template/typings/vue-hot-reload-api/index.d.ts b/template/typings/vue-hot-reload-api/index.d.ts new file mode 100644 index 0000000..2e81ae1 --- /dev/null +++ b/template/typings/vue-hot-reload-api/index.d.ts @@ -0,0 +1,7 @@ +declare module 'vue-hot-reload-api' { + import Vue, { Component } from 'vue'; + export function install(Vue): void; + export function compatible(): boolean; + export function createRecord(id: string, component: Component): void; + export function reload(id: string, component: Component): void; +}