Skip to content

Lazy-loading with SSR #65

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.

<p align="center">
<a href="https://vue-hn.now.sh" target="_blank">
<a href="https://vue-hn-lazy.now.sh" target="_blank">
<img src="https://cloud.githubusercontent.com/assets/499550/17546273/5aabc5fc-5eaf-11e6-8d6a-ad00937e8bd6.png" width="700px">
<br>
Live Demo
Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion build/webpack.base.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
3 changes: 2 additions & 1 deletion build/webpack.client.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
3 changes: 2 additions & 1 deletion build/webpack.server.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
]
})
4 changes: 2 additions & 2 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
54 changes: 48 additions & 6 deletions src/client-entry.js
Original file line number Diff line number Diff line change
@@ -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
}
8 changes: 5 additions & 3 deletions src/router/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
5 changes: 4 additions & 1 deletion src/server-entry.js
Original file line number Diff line number Diff line change
@@ -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`.
Expand Down Expand Up @@ -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
})
}
10 changes: 4 additions & 6 deletions src/store/api.js
Original file line number Diff line number Diff line change
@@ -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()))

Expand All @@ -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()))

Expand Down
2 changes: 1 addition & 1 deletion src/views/CreateListView.js
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down