From 21b853db0f95eb343149ae15f7ba2392e43ac12b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Chopin?= Date: Thu, 3 Nov 2016 16:09:40 +0100 Subject: [PATCH 1/3] Lazy loading on --- build/webpack.base.config.js | 2 +- build/webpack.client.config.js | 3 +- build/webpack.server.config.js | 3 +- index.html | 5 ++-- server.js | 3 +- src/app.js | 4 +-- src/client-entry.js | 54 ++++++++++++++++++++++++++++++---- src/router/index.js | 8 +++-- src/server-entry.js | 5 +++- src/store/api.js | 10 +++---- src/views/CreateListView.js | 2 +- 11 files changed, 72 insertions(+), 27 deletions(-) diff --git a/build/webpack.base.config.js b/build/webpack.base.config.js index 6a8d229c9..a71e53668 100644 --- a/build/webpack.base.config.js +++ b/build/webpack.base.config.js @@ -5,7 +5,7 @@ module.exports = { devtool: '#source-map', entry: { app: './src/client-entry.js', - vendor: ['vue', 'vue-router', 'vuex', 'firebase', 'lru-cache', 'es6-promise'] + vendor: ['vue', 'vue-router', 'vuex', 'vuex-router-sync', 'firebase', 'es6-promise'] }, output: { path: path.resolve(__dirname, '../dist'), diff --git a/build/webpack.client.config.js b/build/webpack.client.config.js index caeaff7e4..e42dcf5e9 100644 --- a/build/webpack.client.config.js +++ b/build/webpack.client.config.js @@ -6,7 +6,8 @@ const config = Object.assign({}, base, { plugins: (base.plugins || []).concat([ // strip comments in Vue code new webpack.DefinePlugin({ - 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development') + 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'), + 'process.BROWSER': true }), // extract vendor chunks for better caching new webpack.optimize.CommonsChunkPlugin({ diff --git a/build/webpack.server.config.js b/build/webpack.server.config.js index 39a08056a..528838b5c 100644 --- a/build/webpack.server.config.js +++ b/build/webpack.server.config.js @@ -13,7 +13,8 @@ module.exports = Object.assign({}, base, { plugins: [ new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'), - 'process.env.VUE_ENV': '"server"' + 'process.env.VUE_ENV': '"server"', + 'process.BROWSER': false }) ] }) diff --git a/index.html b/index.html index 6de42fd59..0fa5ef957 100644 --- a/index.html +++ b/index.html @@ -6,11 +6,10 @@ - {{ STYLE }} + + {{ APP }} - - diff --git a/server.js b/server.js index 5e259772f..95c7572a5 100644 --- a/server.js +++ b/server.js @@ -19,9 +19,8 @@ const html = (() => { const template = fs.readFileSync(resolve('./index.html'), 'utf-8') const i = template.indexOf('{{ APP }}') // styles are injected dynamically via vue-style-loader in development - const style = isProd ? '' : '' return { - head: template.slice(0, i).replace('{{ STYLE }}', style), + head: template.slice(0, i), tail: template.slice(i + '{{ APP }}'.length) } })() diff --git a/src/app.js b/src/app.js index b207d43b7..44d12ac3e 100644 --- a/src/app.js +++ b/src/app.js @@ -17,11 +17,11 @@ Object.keys(filters).forEach(key => { // create the app instance. // here we inject the router and store to all child components, // making them available everywhere as `this.$router` and `this.$store`. -const app = new Vue({ +const app = { router, store, ...App // Object spread copying everything from App.vue -}) +} // expose the app, the router and the store. // note we are not mounting the app here, since bootstrapping will be diff --git a/src/client-entry.js b/src/client-entry.js index 8d2c302b8..eaf5b9792 100644 --- a/src/client-entry.js +++ b/src/client-entry.js @@ -1,9 +1,51 @@ require('es6-promise').polyfill() -import { app, store } from './app' +import Vue from 'vue' +import { app, store, router } from './app' -// prime the store with server-initialized state. -// the state is determined during SSR and inlined in the page markup. -store.replaceState(window.__INITIAL_STATE__) +// Get matched components by route and load them +const path = getLocation(router.options.base) +const resolveComponents = flatMapComponents(router.match(path), (Component, _, match, key, index) => { + if (typeof Component === 'function' && !Component.options) { + return new Promise(function (resolve, reject) { + const _resolve = (Component) => { + match.components[key] = Component + resolve(Component) + } + var res = Component(_resolve, reject) + if (res && res.then) { + res.then(_resolve).catch(reject) + } + }) + } + return Component +}) -// actually mount to DOM -app.$mount('#app') +Promise.all(resolveComponents) +.then((Components) => { + const _app = new Vue(app) + // prime the store with server-initialized state. + // the state is determined during SSR and inlined in the page markup. + store.replaceState(window.__INITIAL_STATE__) + _app.$mount('#app') +}) +.catch((err) => { + console.error('Cannot load components', err) +}) + +// Imported for vue-router +export function flatMapComponents (route, fn) { + return Array.prototype.concat.apply([], route.matched.map(function (m, index) { + return Object.keys(m.components).map(function (key) { + return fn(m.components[key], m.instances[key], m, key, index) + }) + })) +} + +// Imported from vue-router +export function getLocation (base) { + var path = window.location.pathname + if (base && path.indexOf(base) === 0) { + path = path.slice(base.length) + } + return (path || '/') + window.location.search + window.location.hash +} diff --git a/src/router/index.js b/src/router/index.js index 61bac2bbb..6db01dd65 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -3,9 +3,11 @@ import Router from 'vue-router' Vue.use(Router) -import { createListView } from '../views/CreateListView' -import ItemView from '../views/ItemView.vue' -import UserView from '../views/UserView.vue' +const createListView = process.BROWSER ? (type) => { + return (resolve) => { System.import('../views/CreateListView').then((createListView) => resolve(createListView(type))) } +} : require('../views/CreateListView') +const ItemView = process.BROWSER ? () => System.import('../views/ItemView.vue') : require('../views/ItemView.vue') +const UserView = process.BROWSER ? () => System.import('../views/UserView.vue') : require('../views/UserView.vue') export default new Router({ mode: 'history', diff --git a/src/server-entry.js b/src/server-entry.js index b56f4f0be..72c145c8a 100644 --- a/src/server-entry.js +++ b/src/server-entry.js @@ -1,5 +1,8 @@ +import Vue from 'vue' import { app, router, store } from './app' +const _app = new Vue(app) + const isDev = process.env.NODE_ENV !== 'production' // This exported function will be called by `bundleRenderer`. @@ -30,6 +33,6 @@ export default context => { // store to pick-up the server-side state without having to duplicate // the initial data fetching on the client. context.initialState = store.state - return app + return _app }) } diff --git a/src/store/api.js b/src/store/api.js index b03a563bb..3e98b817d 100644 --- a/src/store/api.js +++ b/src/store/api.js @@ -1,12 +1,10 @@ -import Firebase from 'firebase' -import LRU from 'lru-cache' - -const inBrowser = typeof window !== 'undefined' +const Firebase = require('firebase') +const LRU = process.BROWSER ? null : require('lru-cache') // When using bundleRenderer, the server-side application code runs in a new // context for each request. To allow caching across multiple requests, we need // to attach the cache to the process which is shared across all requests. -const cache = inBrowser +const cache = process.BROWSER ? null : (process.__API_CACHE__ || (process.__API_CACHE__ = createCache())) @@ -18,7 +16,7 @@ function createCache () { } // create a single api instance for all server-side requests -const api = inBrowser +const api = process.BROWSER ? new Firebase('https://hacker-news.firebaseio.com/v0') : (process.__API__ || (process.__API__ = createServerSideAPI())) diff --git a/src/views/CreateListView.js b/src/views/CreateListView.js index fcb110b28..986186ca5 100644 --- a/src/views/CreateListView.js +++ b/src/views/CreateListView.js @@ -3,7 +3,7 @@ import ItemList from '../components/ItemList.vue' // This is a factory function for dynamically creating root-level list views, // since they share most of the logic except for the type of items to display. // They are essentially higher order components wrapping ItemList.vue. -export function createListView (type) { +module.exports = function (type) { return { name: `${type}-stories-view`, // this will be called during SSR to pre-fetch data into the store! From 25a8e728fd9eba7db95eeac0441a5a220369f42c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Chopin?= Date: Thu, 3 Nov 2016 16:24:43 +0100 Subject: [PATCH 2/3] Fix style.css + update README --- README.md | 5 +++-- index.html | 1 + server.js | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 75a96823a..b571fb1bb 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # vue-hackernews-2.0 -HackerNews clone built with Vue 2.0 + vue-router + vuex, with server-side rendering. +HackerNews clone built with Vue 2.0 + vue-router + vuex, with server-side rendering and lazy-loading.

- +
Live Demo @@ -22,6 +22,7 @@ HackerNews clone built with Vue 2.0 + vue-router + vuex, with server-side render - Hot-reload in development - CSS extraction for production - Real-time List Updates with FLIP Animation +- Lazy loading of the components ## Architecture Overview diff --git a/index.html b/index.html index 0fa5ef957..dabac85dc 100644 --- a/index.html +++ b/index.html @@ -6,6 +6,7 @@ + {{ STYLE }} diff --git a/server.js b/server.js index 95c7572a5..5e259772f 100644 --- a/server.js +++ b/server.js @@ -19,8 +19,9 @@ const html = (() => { const template = fs.readFileSync(resolve('./index.html'), 'utf-8') const i = template.indexOf('{{ APP }}') // styles are injected dynamically via vue-style-loader in development + const style = isProd ? '' : '' return { - head: template.slice(0, i), + head: template.slice(0, i).replace('{{ STYLE }}', style), tail: template.slice(i + '{{ APP }}'.length) } })() From 4ac149c4dcaff4676d2c1223efea7c83ca29a58f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Chopin?= Date: Thu, 3 Nov 2016 18:33:41 +0100 Subject: [PATCH 3/3] revert index.html --- index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.html b/index.html index dabac85dc..6de42fd59 100644 --- a/index.html +++ b/index.html @@ -7,10 +7,10 @@ {{ STYLE }} - - {{ APP }} + +