diff --git a/lib/dev-build.js b/lib/dev-build.js index 71ef7e7..bf1dc61 100644 --- a/lib/dev-build.js +++ b/lib/dev-build.js @@ -1,11 +1,14 @@ const webpack = require('webpack'); const webpackDevMiddleware = require('webpack-dev-middleware'); +const {getFilenameFromUrl} = require('webpack-dev-middleware/lib/util'); const webpackHotMiddleware = require('webpack-hot-middleware'); const {reactive, computed} = require('@vue/reactivity'); const {createBundleRenderer} = require('vue-server-renderer'); const {createFsFromVolume, Volume} = require('memfs'); const path = require('path'); const consola = require('consola'); +const getETag = require('etag'); +const hasOwnProp = require('has-own-prop'); function Deferred() { this.isResolved = false; @@ -77,12 +80,56 @@ function createClientBuild(state, clientConfig) { stats: 'none', }); + const etagRegistry = new Map(); + + function computeEtags(assets) { + for (const assetId in assets) { + if (!hasOwnProp(assets, assetId)) { + continue; + } + + const fsPath = assets[assetId].existsAt; + const etag = getETag(assets[assetId].source()); + etagRegistry.set(fsPath, etag); + } + } + const middlewares = { - devMiddleware, + devMiddleware({url, headers}, response) { + const filename = getFilenameFromUrl( + devMiddleware.context.options.publicPath, + devMiddleware.context.compiler, + url, + ); + + const ifNoneMatch = headers['if-none-match']; + if (ifNoneMatch) { + const assetEtag = etagRegistry.get(filename); + if (assetEtag && assetEtag === ifNoneMatch) { + response.statusCode = 304; + response.end(); + return; + } + } + + response.send = result => { + const etag = etagRegistry.get(filename); + if (etag) { + response.setHeader('ETag', etag); + } + + response.setHeader('Cache-Control', 'nocache'); + response.end(result); + }; + + Reflect.apply(devMiddleware, this, arguments); + }, hotMiddleware: webpackHotMiddleware(clientCompiler), }; const onDone = stats => { + computeEtags(stats.compilation.assets); + stats = stats.toJson(); if (stats.errors.length > 0) { diff --git a/lib/init-server.js b/lib/init-server.js index cde9f07..d86365b 100644 --- a/lib/init-server.js +++ b/lib/init-server.js @@ -24,11 +24,16 @@ function initServer({ try { const html = await renderer.value.renderToString(context); + response.setHeader('Content-Type', 'text/html'); response.end(html); } catch (error) { const {code = 500, message = 'Internal Server Error'} = error; response.statusCode = code; - response.end(`Error: ${message}`); + response.setHeader('Content-Type', 'application/json'); + response.end(JSON.stringify({ + code, + message, + })); if (!error.code) { console.error(error); diff --git a/package-lock.json b/package-lock.json index 251e686..6c24974 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2486,6 +2486,11 @@ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, "events": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/events/-/events-3.2.0.tgz", @@ -3126,6 +3131,11 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, + "has-own-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-own-prop/-/has-own-prop-2.0.0.tgz", + "integrity": "sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==" + }, "has-symbols": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", diff --git a/package.json b/package.json index cfafe8c..09c9394 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,9 @@ "compression": "^1.7.4", "connect": "^3.7.0", "consola": "^2.15.0", + "etag": "^1.8.1", "get-port": "^5.1.1", + "has-own-prop": "^2.0.0", "lodash": "^4.17.20", "memfs": "^3.2.0", "minimist": "^1.2.5",