Commit 402e1090 authored by Evan You's avatar Evan You

update setup to leverage client preloading

parent 1c85abcd
......@@ -5,8 +5,7 @@ const clientConfig = require('./webpack.client.config')
const serverConfig = require('./webpack.server.config')
module.exports = function setupDevServer (app, cb) {
let bundle
let template
let bundle, template, serverManifest, clientManifest
// modify client config to work with hot middleware
clientConfig.entry.app = ['webpack-hot-middleware/client', clientConfig.entry.app]
......@@ -25,12 +24,17 @@ module.exports = function setupDevServer (app, cb) {
app.use(devMiddleware)
clientCompiler.plugin('done', () => {
const fs = devMiddleware.fileSystem
const filePath = path.join(clientConfig.output.path, 'index.html')
if (fs.existsSync(filePath)) {
template = fs.readFileSync(filePath, 'utf-8')
if (bundle) {
cb(bundle, template)
}
const readFile = file => fs.readFileSync(path.join(clientConfig.output.path, file), 'utf-8')
template = readFile('index.html')
clientManifest = JSON.parse(readFile('vue-ssr-manifest-client.json'))
if (bundle && serverManifest) {
cb(bundle, {
template,
manifest: {
server: serverManifest,
client: clientManifest
}
})
}
})
......@@ -46,12 +50,19 @@ module.exports = function setupDevServer (app, cb) {
stats = stats.toJson()
stats.errors.forEach(err => console.error(err))
stats.warnings.forEach(err => console.warn(err))
const readFile = file => mfs.readFileSync(path.join(clientConfig.output.path, file), 'utf-8')
// read bundle generated by vue-ssr-webpack-plugin
const bundlePath = path.join(serverConfig.output.path, 'vue-ssr-bundle.json')
bundle = JSON.parse(mfs.readFileSync(bundlePath, 'utf-8'))
if (template) {
cb(bundle, template)
bundle = JSON.parse(readFile('vue-ssr-bundle.json'))
serverManifest = JSON.parse(readFile('vue-ssr-manifest-server.json'))
if (template && clientManifest) {
cb(bundle, {
template,
manifest: {
server: serverManifest,
client: clientManifest
}
})
}
})
}
......@@ -4,7 +4,7 @@ const base = require('./webpack.base.config')
const vueConfig = require('./vue-loader.config')
const HTMLPlugin = require('html-webpack-plugin')
const SWPrecachePlugin = require('sw-precache-webpack-plugin')
const VueSSRPlugin = require('vue-ssr-webpack-plugin').client
const VueSSRClientPlugin = require('vue-ssr-webpack-plugin').client
const config = merge(base, {
entry: './src/entry-client.js',
......@@ -36,18 +36,18 @@ const config = merge(base, {
new HTMLPlugin({
template: 'src/index.template.html'
}),
new VueSSRPlugin()
new VueSSRClientPlugin()
]
})
if (process.env.NODE_ENV === 'production') {
config.plugins.push(
// minify JS
// new webpack.optimize.UglifyJsPlugin({
// compress: {
// warnings: false
// }
// }),
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
}
}),
// auto generate service worker
new SWPrecachePlugin({
cacheId: 'vue-hn',
......
const webpack = require('webpack')
const merge = require('webpack-merge')
const base = require('./webpack.base.config')
const VueSSRPlugin = require('vue-ssr-webpack-plugin').server
const VueSSRServerPlugin = require('vue-ssr-webpack-plugin').server
module.exports = merge(base, {
target: 'node',
......@@ -22,6 +22,6 @@ module.exports = merge(base, {
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
'process.env.VUE_ENV': '"server"'
}),
new VueSSRPlugin()
new VueSSRServerPlugin()
]
})
module.exports = function createMapper (serverStats, clientStats) {
const fileMap = createFileMap(serverStats, clientStats)
return function mapFiles (files) {
const res = new Set()
for (let i = 0; i < files.length; i++) {
const mapped = fileMap.get(files[i])
for (let j = 0; j < mapped.length; j++) {
res.add(mapped[j])
}
}
return Array.from(res)
}
}
function createFileMap (serverStats, clientStats) {
const fileMap = new Map()
serverStats.assets
.filter(asset => /\.js$/.test(asset.name))
.forEach(asset => fileMap.set(asset.name, mapFile(asset.name, serverStats, clientStats)))
return fileMap
}
function mapFile (file, serverStats, clientStats) {
// 1. server files -> server chunk ids
const serverChunkIds = new Set()
serverStats.assets.forEach(asset => {
if (asset.name === file) {
asset.chunks.forEach(id => {
const chunk = serverStats.chunks.find(c => c.id === id)
if (!chunk.initial) {
serverChunkIds.add(id)
}
})
}
})
// 2. server chunks -> module identifiers
const moduleIdentifiers = []
serverStats.modules.forEach(module => {
if (module.chunks.some(id => serverChunkIds.has(id))) {
moduleIdentifiers.push(module.identifier)
}
})
// 3. module identifiers -> client chunk ids
const clientChunkIds = new Set()
moduleIdentifiers.forEach(identifier => {
const clientModule = clientStats.modules.find(m => m.identifier === identifier)
if (clientModule && clientModule.chunks.length === 1) { // ignore modules duplicated in multiple chunks
clientChunkIds.add(clientModule.chunks[0])
}
})
// 4. client chunks -> client files
const clientFiles = new Set()
Array.from(clientChunkIds).forEach(id => {
const chunk = clientStats.chunks.find(chunk => chunk.id === id)
if (!chunk.initial) {
chunk.files.forEach(file => clientFiles.add(file))
}
})
return Array.from(clientFiles)
}
......@@ -3,28 +3,8 @@ const path = require('path')
const express = require('express')
const favicon = require('serve-favicon')
const compression = require('compression')
const createFileMapper = require('./mapFiles')
const resolve = file => path.resolve(__dirname, file)
const serverStats = require('./dist/server-stats.json')
const clientStats = require('./dist/client-stats.json')
const clientInitialFiles = []
const clientAsyncFiles = []
clientStats.chunks.forEach(chunk => {
chunk.files.forEach(file => {
if (chunk.initial) {
clientInitialFiles.push(file)
} else {
clientAsyncFiles.push(file)
}
})
})
const clientInitialFileLinks = clientInitialFiles.map(file => {
return `<link rel="preload" href="/dist/${file}" as="${ /\.css$/.test(file) ? 'style' : 'script' }">`
}).join('')
const isProd = process.env.NODE_ENV === 'production'
const serverInfo =
`express/${require('express/package.json').version} ` +
......@@ -32,7 +12,7 @@ const serverInfo =
const app = express()
let renderer, mapFiles
let renderer
if (isProd) {
// In production: create server renderer using server bundle and index HTML
// template from real fs.
......@@ -41,26 +21,29 @@ if (isProd) {
// src/index.template.html is processed by html-webpack-plugin to inject
// build assets and output as dist/index.html.
const template = fs.readFileSync(resolve('./dist/index.html'), 'utf-8')
mapFiles = createFileMapper(serverStats, clientStats)
renderer = createRenderer(bundle, template)
renderer = createRenderer(bundle, {
template,
manifest: {
server: require('./dist/vue-ssr-manifest-server.json'),
client: require('./dist/vue-ssr-manifest-client.json')
}
})
} else {
mapFiles = () => []
// In development: setup the dev server with watch and hot-reload,
// and create a new renderer on bundle / index template update.
require('./build/setup-dev-server')(app, (bundle, template) => {
renderer = createRenderer(bundle, template)
require('./build/setup-dev-server')(app, (bundle, options) => {
renderer = createRenderer(bundle, options)
})
}
function createRenderer (bundle, template) {
function createRenderer (bundle, options) {
// https://github.com/vuejs/vue/blob/dev/packages/vue-server-renderer/README.md#why-use-bundlerenderer
return require('vue-server-renderer').createBundleRenderer(bundle, {
template,
return require('vue-server-renderer').createBundleRenderer(bundle, Object.assign(options, {
cache: require('lru-cache')({
max: 1000,
maxAge: 1000 * 60 * 15
})
})
}))
}
const serve = (path, cache) => express.static(resolve(path), {
......@@ -98,27 +81,6 @@ app.get('*', (req, res) => {
const context = { url: req.url }
renderer.renderToStream(context)
.on('error', errorHandler)
.on('beforeStart', () => {
// load the needed async chunk
const usedFiles = Object.keys(context._evaluatedFiles)
const neededFiles = mapFiles(usedFiles)
context.asyncChunks = neededFiles.map(file => {
return `<script src="/dist/${file}"></script>`
}).join('')
// preload initial chunks
context.head = (context.head || '') + clientInitialFileLinks
// prefetch async chunks
const laterFiles = clientAsyncFiles.map(file => {
if (neededFiles.indexOf(file) < 0) {
return `<link rel="prefetch" href="/dist/${file}" as="script">`
} else {
return ''
}
}).join('')
context.head += laterFiles
})
.on('end', () => console.log(`whole request: ${Date.now() - s}ms`))
.pipe(res)
})
......
......@@ -16,5 +16,5 @@ router.onReady(() => {
// service worker
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// navigator.serviceWorker.register('/service-worker.js')
navigator.serviceWorker.register('/service-worker.js')
}
......@@ -26,7 +26,7 @@ export default context => {
// which is resolved when the action is complete and store state has been
// updated.
Promise.all(matchedComponents.map(component => {
// return component.preFetch && component.preFetch(store)
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
......
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