Commit 1b078be1 authored by Evan You's avatar Evan You

Update to use latest SSR features

- Use vue-server-renderer 2.2.0 + vue-ssr-webpack-plugin to handle Webpack code-split bundle - Use vue-router 2.2.0 to handle async components and async route hooks - Use vue-loader 10.2.0 + vue-style-loader 2.0 for inline critical CSS + better style split - Use vue-srr-html-stream to simplify streaming usage.
parent 6f0c0fef
......@@ -28,7 +28,7 @@ module.exports = function setupDevServer (app, opts) {
const filePath = path.join(clientConfig.output.path, 'index.html')
if (fs.existsSync(filePath)) {
const index = fs.readFileSync(filePath, 'utf-8')
opts.indexUpdated(index)
opts.templateUpdated(index)
}
})
......@@ -38,13 +38,15 @@ module.exports = function setupDevServer (app, opts) {
// watch and update server renderer
const serverCompiler = webpack(serverConfig)
const mfs = new MFS()
const outputPath = path.join(serverConfig.output.path, serverConfig.output.filename)
serverCompiler.outputFileSystem = mfs
serverCompiler.watch({}, (err, stats) => {
if (err) throw err
stats = stats.toJson()
stats.errors.forEach(err => console.error(err))
stats.warnings.forEach(err => console.warn(err))
opts.bundleUpdated(mfs.readFileSync(outputPath, 'utf-8'))
// read bundle generated by vue-ssr-webpack-plugin
const bundlePath = path.join(serverConfig.output.path, 'vue-ssr-bundle.json')
opts.bundleUpdated(JSON.parse(mfs.readFileSync(bundlePath, 'utf-8')))
})
}
......@@ -4,9 +4,9 @@ const vueConfig = require('./vue-loader.config')
module.exports = {
devtool: '#source-map',
entry: {
app: './src/client-entry.js',
app: './src/entry-client.js',
vendor: [
'es6-promise',
'es6-promise/auto',
'firebase/app',
'firebase/database',
'vue',
......
......@@ -2,7 +2,6 @@ const webpack = require('webpack')
const base = require('./webpack.base.config')
const vueConfig = require('./vue-loader.config')
const HTMLPlugin = require('html-webpack-plugin')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const SWPrecachePlugin = require('sw-precache-webpack-plugin')
const config = Object.assign({}, base, {
......@@ -12,7 +11,7 @@ const config = Object.assign({}, base, {
})
},
plugins: (base.plugins || []).concat([
// strip comments in Vue code
// strip dev-only code in Vue source
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
'process.env.VUE_ENV': '"client"'
......@@ -29,30 +28,14 @@ const config = Object.assign({}, base, {
})
if (process.env.NODE_ENV === 'production') {
// Use ExtractTextPlugin to extract CSS into a single file
// so it's applied on initial render.
// vueConfig is already included in the config via LoaderOptionsPlugin
// here we overwrite the loader config for <style lang="stylus">
// so they are extracted.
vueConfig.loaders = {
stylus: ExtractTextPlugin.extract({
loader: 'css-loader!stylus-loader',
fallbackLoader: 'vue-style-loader' // <- this is a dep of vue-loader
})
}
config.plugins.push(
new ExtractTextPlugin('styles.[hash].css'),
// this is needed in webpack 2 for minifying CSS
new webpack.LoaderOptionsPlugin({
minimize: true
}),
// minify JS
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
}
}),
// auto generate service worker
new SWPrecachePlugin({
cacheId: 'vue-hn',
filename: 'service-worker.js',
......
const webpack = require('webpack')
const base = require('./webpack.base.config')
const VueSSRPlugin = require('vue-ssr-webpack-plugin')
module.exports = Object.assign({}, base, {
target: 'node',
devtool: false,
entry: './src/server-entry.js',
entry: './src/entry-server.js',
output: Object.assign({}, base.output, {
filename: 'server-bundle.js',
libraryTarget: 'commonjs2'
......@@ -19,6 +19,7 @@ module.exports = Object.assign({}, base, {
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
'process.env.VUE_ENV': '"server"'
})
}),
new VueSSRPlugin()
]
})
......@@ -26,6 +26,7 @@
"vue": "^2.1.10",
"vue-router": "^2.1.0",
"vue-server-renderer": "^2.1.10",
"vue-ssr-html-stream": "^1.0.0",
"vuex": "^2.1.0",
"vuex-router-sync": "^4.0.2"
},
......@@ -34,7 +35,6 @@
"buble": "^0.15.1",
"buble-loader": "^0.4.0",
"css-loader": "^0.26.0",
"extract-text-webpack-plugin": "^2.0.0-beta.3",
"file-loader": "^0.9.0",
"html-webpack-plugin": "^2.24.1",
"rimraf": "^2.5.4",
......@@ -42,7 +42,8 @@
"stylus-loader": "^2.4.0",
"sw-precache-webpack-plugin": "^0.7.0",
"url-loader": "^0.5.7",
"vue-loader": "^10.0.2",
"vue-loader": "^10.2.0",
"vue-ssr-webpack-plugin": "^1.0.0",
"vue-template-compiler": "^2.1.8",
"webpack": "^2.2.0",
"webpack-dev-middleware": "^1.8.4",
......
......@@ -3,7 +3,7 @@ const path = require('path')
const express = require('express')
const favicon = require('serve-favicon')
const compression = require('compression')
const serialize = require('serialize-javascript')
const HTMLStream = require('vue-ssr-html-stream')
const resolve = file => path.resolve(__dirname, file)
const isProd = process.env.NODE_ENV === 'production'
......@@ -13,12 +13,12 @@ const serverInfo =
const app = express()
let indexHTML // generated by html-webpack-plugin
let template // generated by html-webpack-plugin
let renderer // created from the webpack-generated server bundle
if (isProd) {
// in production: create server renderer and index HTML from real fs
renderer = createRenderer(fs.readFileSync(resolve('./dist/server-bundle.js'), 'utf-8'))
indexHTML = parseIndex(fs.readFileSync(resolve('./dist/index.html'), 'utf-8'))
renderer = createRenderer(require('./dist/vue-ssr-bundle.json'), 'utf-8')
template = fs.readFileSync(resolve('./dist/index.html'), 'utf-8')
} else {
// in development: setup the dev server with watch and hot-reload,
// and update renderer / index HTML on file change.
......@@ -26,8 +26,8 @@ if (isProd) {
bundleUpdated: bundle => {
renderer = createRenderer(bundle)
},
indexUpdated: index => {
indexHTML = parseIndex(index)
templateUpdated: _template => {
template = _template
}
})
}
......@@ -42,69 +42,46 @@ function createRenderer (bundle) {
})
}
function parseIndex (template) {
const contentMarker = '<!-- APP -->'
const i = template.indexOf(contentMarker)
return {
head: template.slice(0, i),
tail: template.slice(i + contentMarker.length)
}
}
const serve = (path, cache) => express.static(resolve(path), {
maxAge: cache && isProd ? 60 * 60 * 24 * 30 : 0
})
app.use(compression({ threshold: 0 }))
app.use(favicon('./public/logo-48.png'))
app.use('/service-worker.js', serve('./dist/service-worker.js'))
app.use('/manifest.json', serve('./manifest.json'))
app.use('/dist', serve('./dist'))
app.use('/public', serve('./public'))
app.use('/manifest.json', serve('./manifest.json'))
app.use('/service-worker.js', serve('./dist/service-worker.js'))
app.get('*', (req, res) => {
if (!renderer) {
return res.end('waiting for compilation... refresh in a moment.')
}
const s = Date.now()
res.setHeader("Content-Type", "text/html")
res.setHeader("Server", serverInfo)
var s = Date.now()
const context = { url: req.url }
const renderStream = renderer.renderToStream(context)
renderStream.once('data', () => {
res.write(indexHTML.head)
})
renderStream.on('data', chunk => {
res.write(chunk)
})
renderStream.on('end', () => {
// embed initial store state
if (context.initialState) {
res.write(
`<script>window.__INITIAL_STATE__=${
serialize(context.initialState, { isJSON: true })
}</script>`
)
}
res.end(indexHTML.tail)
console.log(`whole request: ${Date.now() - s}ms`)
})
renderStream.on('error', err => {
if (err && err.code === '404') {
const errorHandler = err => {
if (err && err.code === 404) {
res.status(404).end('404 | Page Not Found')
return
}
} else {
// Render Error Page or Redirect
res.status(500).end('Internal Error 500')
console.error(`error during render : ${req.url}`)
console.error(err)
})
}
}
const context = { url: req.url }
const htmlStream = new HTMLStream({ template, context })
renderer.renderToStream(context)
.on('error', errorHandler)
.pipe(htmlStream)
.on('end', () => console.log(`whole request: ${Date.now() - s}ms`))
.pipe(res)
})
const port = process.env.PORT || 8080
......
import 'es6-promise/auto'
import { app, store } from './app'
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__)
// actually mount to DOM
app.$mount('#app')
// wait until router has resolved all async before hooks
// and async components...
router.onReady(() => {
// actually mount to DOM
app.$mount('#app')
})
// service worker
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
......
......@@ -10,23 +10,23 @@ const isDev = process.env.NODE_ENV !== 'production'
export default context => {
const s = isDev && Date.now()
return new Promise((resolve, reject) => {
// set router's location
router.push(context.url)
const matchedComponents = router.getMatchedComponents()
// wait until router has resolved possible async hooks
router.onReady(() => {
const matchedComponents = router.getMatchedComponents()
// no matched routes
if (!matchedComponents.length) {
return Promise.reject({ code: '404' })
reject({ code: 404 })
}
// Call preFetch hooks on components matched by the route.
// A preFetch hook dispatches a store action and returns a Promise,
// which is resolved when the action is complete and store state has been
// updated.
return Promise.all(matchedComponents.map(component => {
if (component.preFetch) {
return component.preFetch(store)
}
Promise.all(matchedComponents.map(component => {
return component.preFetch && component.preFetch(store)
})).then(() => {
isDev && console.log(`data pre-fetch: ${Date.now() - s}ms`)
// After all preFetch hooks are resolved, our store is now
......@@ -35,7 +35,9 @@ export default context => {
// inline the state in the HTML response. This allows the client-side
// 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
context.state = store.state
resolve(app)
}).catch(reject)
})
})
}
......@@ -3,9 +3,22 @@ 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'
// We are using Webpack code splitting here so that each route's associated
// component code is loaded on-demand only when the route is visited.
// It's actually not really necessary for a small project of this size but
// the goal is to demonstrate how to do it.
//
// Note that the dynamic import syntax should actually be just `import()`
// but buble/acorn doesn't support parsing that syntax until it's stage 4
// so we use the old System.import here instead.
//
// If using Babel, `import()` can be supported via
// babel-plugin-syntax-dynamic-import.
const createListView = name => () =>
System.import('../views/CreateListView').then(m => m.createListView(name))
const ItemView = () => System.import('../views/ItemView.vue')
const UserView = () => System.import('../views/UserView.vue')
export default new Router({
mode: 'history',
......
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment