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/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/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!