@akryum/vue-cli-plugin-ssr
Version:
Turn your app into an isomorphic SSR app!
186 lines (158 loc) • 5.49 kB
JavaScript
const express = require('express')
const { createBundleRenderer } = require('vue-server-renderer')
const fs = require('fs')
const favicon = require('serve-favicon')
const LRU = require('lru-cache')
const compression = require('compression')
const config = require('./config')
const DEFAULT_OPTIONS = {
prodOnly: false,
}
module.exports = (app, options) => {
options = Object.assign({}, DEFAULT_OPTIONS, options)
const isProd = process.env.NODE_ENV === 'production'
if (options.prodOnly && !isProd) return
const templatePath = config.templatePath
const { publicPath } = config.service.projectOptions
try {
// Vue bundle renderer
let renderer
// In development: wait for webpack compilation
// when receiving a SSR request
let readyPromise
const directives = config.directives
const lruCacheOptions = config.lruCacheOptions || {}
const defaultRendererOptions = {
cache: new LRU({
max: 1000,
maxAge: 1000 * 60 * 15,
...lruCacheOptions,
}),
runInNewContext: false,
inject: false,
directives,
shouldPrefetch: (file, type) => {
if (config.shouldNotPrefetch.indexOf(file) > -1) return false
if (type === 'script' || type === 'style') return true
},
shouldPreload: (file, type) => {
if (config.shouldNotPreload.indexOf(file) > -1) return false
if (type === 'script' || type === 'style') return true
},
}
if (isProd) {
// In production: create server renderer using template and built server bundle.
// The server bundle is generated by vue-ssr-webpack-plugin.
const template = fs.readFileSync(templatePath, 'utf-8')
const serverBundle = require(`${config.distPath}/vue-ssr-server-bundle.json`)
// The client manifests are optional, but it allows the renderer
// to automatically infer preload/prefetch links and directly add <script>
// tags for any async chunks used during render, avoiding waterfall requests.
const clientManifest = require(`${config.distPath}/vue-ssr-client-manifest.json`)
renderer = createBundleRenderer(serverBundle, {
...defaultRendererOptions,
template,
clientManifest,
})
} else {
// In development: setup the dev server with watch and hot-reload,
// and create a new renderer on bundle / index template update.
const { setupDevServer } = require('./dev-server')
readyPromise = setupDevServer({
server: app,
templatePath,
onUpdate: ({ serverBundle }, options) => {
// Re-create the bundle renderer
renderer = createBundleRenderer(serverBundle, {
...defaultRendererOptions,
...options,
})
},
})
}
if (config.applyDefaultServer) {
// Serve static files
const serve = (filePath, cache) => express.static(filePath, {
maxAge: cache && isProd ? config.staticCacheTtl : 0,
index: false,
})
// Serve static files
app.use(compression({ threshold: 0 }))
app.use(favicon(config.favicon))
if (config.api.hasPlugin('pwa')) {
app.use('/service-worker.js', serve(config.serviceWorkerPath))
}
const serveStaticFiles = serve(config.distPath, true)
app.use(publicPath, (req, res, next) => {
if (/index\.html/g.test(req.path)) {
next()
} else {
serveStaticFiles(req, res, next)
}
})
}
if (config.extendServer) {
config.extendServer(app)
}
// Render the Vue app using the bundle renderer
const renderApp = (req, res) => {
res.setHeader('Content-Type', 'text/html')
const context = Object.assign({
req,
url: req.url,
title: config.defaultTitle,
httpCode: 200,
}, config.extendContext && config.extendContext(req, res, process))
renderer.renderToString(context, (err, renderedHtml) => {
let html = renderedHtml
if (err || context.httpCode === 500) {
console.error(`error during render url : ${req.url}`)
// Render Error Page
let errorHtml = config.error500Html
? fs.readFileSync(config.error500Html, 'utf-8')
: '500 | Internal Server Error'
if (err) {
console.error(err)
if (!isProd) {
const errorMessage = `<pre>${err.stack}</pre>`
config.error500Html
? errorHtml = errorHtml.replace('<!--server-error-msg-->', errorMessage)
: errorHtml += errorMessage
}
if (config.onError) {
config.onError(err)
}
}
html = errorHtml
res.status(500)
} else {
res.status(context.httpCode)
}
if (config.onRender) {
config.onRender(res, context)
}
res.send(html)
})
}
// Process SSR requests
let ssr
if (isProd) {
ssr = renderApp
} else {
// In development: wait for webpack compilation
// when receiving a SSR request
ssr = (req, res) => {
readyPromise.then(() => renderApp(req, res)).catch(console.error)
}
}
app.get(`${publicPath}*`, (req, res, next) => {
if (config.skipRequests(req)) {
return next()
}
ssr(req, res)
})
return readyPromise
} catch (e) {
console.error(e)
}
}