Commit 1003dd38 authored by Evan You's avatar Evan You

use html-webpack-plugin, better caching, handle 404

parent 95de8852
...@@ -3,11 +3,22 @@ const vueConfig = require('./vue-loader.config') ...@@ -3,11 +3,22 @@ const vueConfig = require('./vue-loader.config')
module.exports = { module.exports = {
devtool: '#source-map', devtool: '#source-map',
entry: './src/client-entry.js', entry: {
app: './src/client-entry.js',
vendor: [
'es6-promise',
'firebase/app',
'firebase/database',
'vue',
'vue-router',
'vuex',
'vuex-router-sync'
]
},
output: { output: {
path: path.resolve(__dirname, '../dist'), path: path.resolve(__dirname, '../dist'),
publicPath: '/dist/', publicPath: '/dist/',
filename: 'client-bundle.js' filename: '[name].[chunkhash].js'
}, },
module: { module: {
noParse: /es6-promise\.js$/, // avoid webpack shimming process noParse: /es6-promise\.js$/, // avoid webpack shimming process
......
const webpack = require('webpack') const webpack = require('webpack')
const base = require('./webpack.base.config') const base = require('./webpack.base.config')
const vueConfig = require('./vue-loader.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, { const config = Object.assign({}, base, {
resolve: { resolve: {
...@@ -12,16 +15,22 @@ const config = Object.assign({}, base, { ...@@ -12,16 +15,22 @@ const config = Object.assign({}, base, {
// strip comments in Vue code // strip comments in Vue code
new webpack.DefinePlugin({ 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')
}),
// extract vendor chunks for better caching
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
filename: '[name].[chunkhash].js'
}),
// generate output HTML
new HTMLPlugin({
template: 'src/index.template.html'
}) })
]) ])
}) })
if (process.env.NODE_ENV === 'production') { if (process.env.NODE_ENV === 'production') {
// Use ExtractTextPlugin to extract CSS into a single file // Use ExtractTextPlugin to extract CSS into a single file
// so it's applied on initial render // so it's applied on initial render.
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const SWPrecachePlugin = require('sw-precache-webpack-plugin')
// vueConfig is already included in the config via LoaderOptionsPlugin // vueConfig is already included in the config via LoaderOptionsPlugin
// here we overwrite the loader config for <style lang="stylus"> // here we overwrite the loader config for <style lang="stylus">
// so they are extracted. // so they are extracted.
...@@ -33,7 +42,7 @@ if (process.env.NODE_ENV === 'production') { ...@@ -33,7 +42,7 @@ if (process.env.NODE_ENV === 'production') {
} }
config.plugins.push( config.plugins.push(
new ExtractTextPlugin('styles.css'), new ExtractTextPlugin('styles.[hash].css'),
// this is needed in webpack 2 for minifying CSS // this is needed in webpack 2 for minifying CSS
new webpack.LoaderOptionsPlugin({ new webpack.LoaderOptionsPlugin({
minimize: true minimize: true
......
...@@ -19,10 +19,10 @@ ...@@ -19,10 +19,10 @@
"es6-promise": "^3.2.1", "es6-promise": "^3.2.1",
"express": "^4.14.0", "express": "^4.14.0",
"firebase": "^3.4.1", "firebase": "^3.4.1",
"html-webpack-plugin": "^2.24.1",
"lru-cache": "^4.0.1", "lru-cache": "^4.0.1",
"serialize-javascript": "^1.3.0", "serialize-javascript": "^1.3.0",
"serve-favicon": "^2.3.0", "serve-favicon": "^2.3.0",
"sw-precache-webpack-plugin": "^0.5.1",
"vue": "^2.0.0", "vue": "^2.0.0",
"vue-router": "^2.0.0", "vue-router": "^2.0.0",
"vue-server-renderer": "^2.0.0", "vue-server-renderer": "^2.0.0",
...@@ -31,14 +31,15 @@ ...@@ -31,14 +31,15 @@
}, },
"devDependencies": { "devDependencies": {
"autoprefixer": "^6.4.0", "autoprefixer": "^6.4.0",
"cross-env": "^2.0.0",
"css-loader": "^0.25.0",
"buble": "^0.14.2", "buble": "^0.14.2",
"buble-loader": "^0.3.2", "buble-loader": "^0.3.2",
"cross-env": "^2.0.0",
"css-loader": "^0.25.0",
"extract-text-webpack-plugin": "^2.0.0-beta.3", "extract-text-webpack-plugin": "^2.0.0-beta.3",
"file-loader": "^0.9.0", "file-loader": "^0.9.0",
"stylus": "^0.54.5", "stylus": "^0.54.5",
"stylus-loader": "^2.1.2", "stylus-loader": "^2.1.2",
"sw-precache-webpack-plugin": "^0.5.1",
"url-loader": "^0.5.7", "url-loader": "^0.5.7",
"vue-loader": "^9.7.0", "vue-loader": "^9.7.0",
"webpack": "^2.1.0-beta.25", "webpack": "^2.1.0-beta.25",
......
...@@ -3,11 +3,11 @@ const isProd = process.env.NODE_ENV === 'production' ...@@ -3,11 +3,11 @@ const isProd = process.env.NODE_ENV === 'production'
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const resolve = file => path.resolve(__dirname, file)
const express = require('express') const express = require('express')
const favicon = require('serve-favicon') const favicon = require('serve-favicon')
const serialize = require('serialize-javascript')
const compression = require('compression') const compression = require('compression')
const serialize = require('serialize-javascript')
const resolve = file => path.resolve(__dirname, file)
// https://github.com/vuejs/vue/blob/next/packages/vue-server-renderer/README.md#why-use-bundlerenderer // https://github.com/vuejs/vue/blob/next/packages/vue-server-renderer/README.md#why-use-bundlerenderer
const createBundleRenderer = require('vue-server-renderer').createBundleRenderer const createBundleRenderer = require('vue-server-renderer').createBundleRenderer
...@@ -16,13 +16,12 @@ const app = express() ...@@ -16,13 +16,12 @@ const app = express()
// parse index.html template // parse index.html template
const html = (() => { const html = (() => {
const template = fs.readFileSync(resolve('./index.html'), 'utf-8') const contentMarker = '<!-- APP -->'
const i = template.indexOf('{{ APP }}') const template = fs.readFileSync(resolve('./dist/index.html'), 'utf-8')
// styles are injected dynamically via vue-style-loader in development const i = template.indexOf(contentMarker)
const style = isProd ? '<link rel="stylesheet" href="/dist/styles.css">' : ''
return { return {
head: template.slice(0, i).replace('{{ STYLE }}', style), head: template.slice(0, i),
tail: template.slice(i + '{{ APP }}'.length) tail: template.slice(i + contentMarker.length)
} }
})() })()
...@@ -47,8 +46,8 @@ function createRenderer (bundle) { ...@@ -47,8 +46,8 @@ function createRenderer (bundle) {
}) })
} }
app.use(compression({threshold: 0})) app.use(compression({ threshold: 0 }))
app.use('/dist', express.static(resolve('./dist'))) app.use('/dist', express.static(resolve('./dist'), { maxAge: 60 * 60 * 24 * 30 }))
app.use(favicon(resolve('./src/assets/logo.png'))) app.use(favicon(resolve('./src/assets/logo.png')))
app.get('*', (req, res) => { app.get('*', (req, res) => {
...@@ -61,7 +60,9 @@ app.get('*', (req, res) => { ...@@ -61,7 +60,9 @@ app.get('*', (req, res) => {
const context = { url: req.url } const context = { url: req.url }
const renderStream = renderer.renderToStream(context) const renderStream = renderer.renderToStream(context)
res.write(html.head) renderStream.once('data', () => {
res.write(html.head)
})
renderStream.on('data', chunk => { renderStream.on('data', chunk => {
res.write(chunk) res.write(chunk)
...@@ -81,6 +82,10 @@ app.get('*', (req, res) => { ...@@ -81,6 +82,10 @@ app.get('*', (req, res) => {
}) })
renderStream.on('error', err => { renderStream.on('error', err => {
if (err && err.code === '404') {
res.status(404).end('404 | Page Not Found')
return
}
// Render Error Page or Redirect // Render Error Page or Redirect
res.status(500).end('Internal Error 500') res.status(500).end('Internal Error 500')
console.error(`error during render : ${req.url}`) console.error(`error during render : ${req.url}`)
......
...@@ -6,10 +6,8 @@ ...@@ -6,10 +6,8 @@
<meta name="mobile-web-app-capable" content="yes"> <meta name="mobile-web-app-capable" content="yes">
<meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui"> <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">
<link href='https://fonts.googleapis.com/css?family=Roboto:300,400,700' rel='stylesheet' type='text/css'> <link href='https://fonts.googleapis.com/css?family=Roboto:300,400,700' rel='stylesheet' type='text/css'>
{{ STYLE }}
</head> </head>
<body> <body>
{{ APP }} <!-- APP -->
<script src="/dist/client-bundle.js"></script>
</body> </body>
</html> </html>
...@@ -18,6 +18,6 @@ export default new Router({ ...@@ -18,6 +18,6 @@ export default new Router({
{ path: '/job/:page(\\d+)?', component: createListView('job') }, { path: '/job/:page(\\d+)?', component: createListView('job') },
{ path: '/item/:id(\\d+)', component: ItemView }, { path: '/item/:id(\\d+)', component: ItemView },
{ path: '/user/:id', component: UserView }, { path: '/user/:id', component: UserView },
{ path: '*', redirect: '/top' } { path: '/', redirect: '/top' }
] ]
}) })
...@@ -8,16 +8,22 @@ const isDev = process.env.NODE_ENV !== 'production' ...@@ -8,16 +8,22 @@ const isDev = process.env.NODE_ENV !== 'production'
// Since data fetching is async, this function is expected to // Since data fetching is async, this function is expected to
// return a Promise that resolves to the app instance. // return a Promise that resolves to the app instance.
export default context => { export default context => {
const s = isDev && Date.now()
// set router's location // set router's location
router.push(context.url) router.push(context.url)
const matchedComponents = router.getMatchedComponents()
const s = isDev && Date.now() // no matched routes
if (!matchedComponents.length) {
return Promise.reject({ code: '404' })
}
// Call preFetch hooks on components matched by the route. // Call preFetch hooks on components matched by the route.
// A preFetch hook dispatches a store action and returns a Promise, // A preFetch hook dispatches a store action and returns a Promise,
// which is resolved when the action is complete and store state has been // which is resolved when the action is complete and store state has been
// updated. // updated.
return Promise.all(router.getMatchedComponents().map(component => { return Promise.all(matchedComponents.map(component => {
if (component.preFetch) { if (component.preFetch) {
return component.preFetch(store) return component.preFetch(store)
} }
......
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