@appjumpstart/mercury-vue
Version:
An Express/Connect-compatible middleware for easy Vue.js Server-Side Rendering (SSR)
174 lines (159 loc) • 7 kB
JavaScript
const { readFileSync } = require('fs')
const { join, dirname } = require('path')
const { oneLineTrim } = require('common-tags')
const mercuryWebpack = require('@appjumpstart/mercury-webpack')
const { createBundleRenderer } = require('vue-server-renderer')
const { pick } = require('accept-language-parser')
const { NODE_ENV } = process.env
module.exports = function mercuryVue (options) {
// Set the base directory as the directory containing the module that has
// imported this module.
const basedir = dirname(module.parent.filename)
// Destructure options into variables with defaults.
const {
// The ouput directory specified in the serverBundle's webpack config.
distPath = join(basedir, 'dist'),
// The path to the index.html that will be used as a page template.
templatePath = join(basedir, 'index.html'),
// A function used to generate and send the server-rendered response.
sendResponse = async (req, res, next, renderer) => {
try {
// Use the renderer to generate HTML and send it to the client.
res
.type('text/html')
.send(await renderer.renderToString({ url: req.url }))
} catch (err) {
next(err)
}
},
// A boolean describing whether to operate in development mode or not.
development = !NODE_ENV || NODE_ENV === 'development',
// The name of the directory within the dist directory used for static
// assets.
staticDir = 'static',
// The max amount of 100ms attempts that the middleware should attempt to
// wait for the renderer to be created.
rendererCheckAttempts = 600,
// A logger instance used to output information.
logger = console,
// An array of language codes that are supported by the application.
supportedLanguages = [],
// The language code to default to if a request's preferred language isn't
// supported by the application.
defaultLanguage = options.supportedLanguages[0]
} = options
// Create the keys array used to create the necessary renderers based on
// whether the serverConfig is a multi-compiler config or not.
const keys = supportedLanguages.length ? supportedLanguages : ['default']
// Update the renderer when the serverBundle or clientManifest changes.
let renderers = {}
let serverBundles = {}
let clientManifests = {}
function updateRenderer (key) {
renderers[key] = createBundleRenderer(serverBundles[key], {
runInNewContext: false,
template: readFileSync(templatePath, 'utf-8'),
basedir,
clientManifest: clientManifests[key]
})
}
let createRendererErr
let mercuryWebpackMiddlewares = {}
keys.forEach(key => {
// Set serverBundle and clientManifest paths.
const bundleName = key === 'default'
? 'vue-ssr-server-bundle.json'
: `vue-ssr-server-bundle.${key}.json`
const bundlePath = join(distPath, bundleName)
const manifestName = key === 'default'
? 'vue-ssr-client-manifest.json'
: `vue-ssr-client-manifest.${key}.json`
const manifestPath = join(distPath, staticDir, manifestName)
if (development) {
// Create an error message for the case when the renderer hasn't been
// created after the max number of check attempts.
createRendererErr = new Error(oneLineTrim`
Renderer was not created after ${rendererCheckAttempts * 100 / 1000}s.
`)
// Initialize the mercury-webpack middleware with hooks to update the
// renderer when webpack-dev-server has re-generated the serverBundle or
// clientManifest.
mercuryWebpackMiddlewares[key] = mercuryWebpack({
...options,
serverHook: function webpackServerHook (mfs) {
serverBundles[key] = JSON.parse(mfs.readFileSync(bundlePath))
updateRenderer(key)
},
clientHook: function webpackClientHook ({ fileSystem }) {
const manifest = fileSystem.readFileSync(manifestPath)
clientManifests[key] = JSON.parse(manifest)
updateRenderer(key)
}
})
} else {
// If not in development mode, use the pre-built serverBundle and
// clientManfiest to create the renderer.
serverBundles[key] = require(bundlePath)
clientManifests[key] = require(manifestPath)
updateRenderer(key)
}
})
async function mercuryVueMiddleware (err, req, res, next) {
if (err || req.method !== 'GET') {
// If there is an error or the request method is not GET, continue to the
// next middleware/handler since no processing needs to be done in those
// cases.
next(err)
} else {
try {
if (renderers[req.languageCode]) {
// If the renderer already exists, go ahead and generate the page and
// send it in the response.
sendResponse(req, res, next, renderers[req.languageCode])
} else {
// Notify the user that the middleware is waiting for the renderer to
// be created.
logger.info('Waiting for the renderer to be created...')
// Check for the renderer to be defined in 100ms intervals up until
// the max number of attempts is reached.
let attempts = 0
let rendererCheckInterval = setInterval(() => {
attempts++
if (renderers[req.languageCode]) {
clearInterval(rendererCheckInterval)
sendResponse(req, res, next, renderers[req.languageCode])
} else if (attempts === rendererCheckAttempts) {
clearInterval(rendererCheckInterval)
next(createRendererErr)
}
}, 100)
}
} catch (err) {
next(err)
}
}
}
// Return a passthrough middleware function that will optionally route the
// request through the mercury-webpack middleware if in development mode
// before routing the request through the mercury-vue middleware.
return function mercuryVuePassthrough (req, res, next) {
// Default to the single compiler MercuryWebpackMiddleware instance.
let mercuryWebpackMiddleware = mercuryWebpackMiddlewares['default']
// If there are multiple supported languages, try to determine the
// preferred language from the Accept-Language header. If the preferred
// language is supported, add it (or the default language) to the request so
// that the matching middleware can be used to serve the request.
if (supportedLanguages.length > 0) {
const headerValue = req.headers['accept-language']
const language = pick(supportedLanguages, headerValue, { loose: true })
req.languageCode = language || defaultLanguage
mercuryWebpackMiddleware = mercuryWebpackMiddlewares[req.languageCode]
}
if (mercuryWebpackMiddleware) {
const mercuryVueNext = err => mercuryVueMiddleware(err, req, res, next)
mercuryWebpackMiddleware(req, res, mercuryVueNext)
} else {
mercuryVueMiddleware(null, req, res, next)
}
}
}