From 10c4b1550f7b8f73bd9a59476e6448ec207c3f45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Chopin?= Date: Tue, 21 Feb 2017 17:10:19 +0000 Subject: [PATCH] Use HTML template + optimise cache control + gzip --- lib/build.js | 16 +++++++++++--- lib/nuxt.js | 33 +++++++++++++++++------------ lib/render.js | 41 +++++++++++++++++++++++------------- lib/views/app.html | 19 ----------------- lib/views/app.template.html | 9 ++++++++ lib/views/error.html | 6 +++--- lib/webpack/client.config.js | 17 ++++++++++++++- package.json | 4 ++++ 8 files changed, 90 insertions(+), 55 deletions(-) delete mode 100644 lib/views/app.html create mode 100644 lib/views/app.template.html diff --git a/lib/build.js b/lib/build.js index 5524cf32ef..488fda67b9 100644 --- a/lib/build.js +++ b/lib/build.js @@ -51,9 +51,9 @@ const defaults = { analyze: false, publicPath: '/_nuxt/', filenames: { - css: 'style.css', - vendor: 'vendor.bundle.js', - app: 'nuxt.bundle.js' + css: 'style.[hash].css', + vendor: 'vendor.bundle.[hash].js', + app: 'nuxt.bundle.[chunkhash].js' }, vendor: [], loaders: [], @@ -377,6 +377,16 @@ function createWebpackMiddleware () { this.webpackHotMiddleware = pify(require('webpack-hot-middleware')(clientCompiler, { log: () => {} })) + clientCompiler.plugin('done', () => { + const fs = this.webpackDevMiddleware.fileSystem + const filePath = join(clientConfig.output.path, 'index.html') + if (fs.existsSync(filePath)) { + const template = fs.readFileSync(filePath, 'utf-8') + this.appTemplate = _.template(template, { + interpolate: /{{([\s\S]+?)}}/g + }) + } + }) } function webpackWatchAndUpdate () { diff --git a/lib/nuxt.js b/lib/nuxt.js index b055946ffe..4ae4be41a2 100644 --- a/lib/nuxt.js +++ b/lib/nuxt.js @@ -2,10 +2,9 @@ import _ from 'lodash' import co from 'co' +import compression from 'compression' import fs from 'fs-extra' import pify from 'pify' -import ansiHTML from 'ansi-html' -import serialize from 'serialize-javascript' import Server from './server' import * as build from './build' import * as render from './render' @@ -13,7 +12,6 @@ import generate from './generate' import serveStatic from 'serve-static' import { resolve, join } from 'path' import * as utils from './utils' -utils.setAnsiColors(ansiHTML) class Nuxt { @@ -63,27 +61,34 @@ class Nuxt { if (fs.existsSync(join(this.srcDir, 'middleware'))) { this.options.middleware = true } - // Template - this.appTemplate = _.template(fs.readFileSync(resolve(__dirname, 'views', 'app.html'), 'utf8'), { - imports: { serialize } - }) - this.errorTemplate = _.template(fs.readFileSync(resolve(__dirname, 'views', 'error.html'), 'utf8'), { - imports: { - ansiHTML, - encodeHtml: utils.encodeHtml - } - }) // renderer used by Vue.js (via createBundleRenderer) this.renderer = null // For serving static/ files to / this.serveStatic = pify(serveStatic(resolve(this.srcDir, 'static'))) // For serving .nuxt/dist/ files (only when build.publicPath is not an URL) - this.serveStaticNuxt = pify(serveStatic(resolve(this.dir, '.nuxt', 'dist'))) + this.serveStaticNuxt = pify(serveStatic(resolve(this.dir, '.nuxt', 'dist'), { + maxAge: (this.dev ? 0 : '1y') // 1 year in production + })) + // gzip for production + if (!this.dev) { + this.gzipMiddleware = pify(compression({ + threshold: 0 + })) + } // Add this.Server Class this.Server = Server // Add this.build build.options.call(this) // Add build options this.build = () => co(build.build.bind(this)) + // Template + if (!this.dev && this.renderer) { + this.appTemplate = _.template(fs.readFileSync(resolve(this.dir, '.nuxt', 'dist', 'index.html'), 'utf8'), { + interpolate: /{{([\s\S]+?)}}/g + }) + } + this.errorTemplate = _.template(fs.readFileSync(resolve(__dirname, 'views', 'error.html'), 'utf8'), { + interpolate: /{{([\s\S]+?)}}/g + }) // Add this.render and this.renderRoute this.render = render.render.bind(this) this.renderRoute = render.renderRoute.bind(this) diff --git a/lib/render.js b/lib/render.js index fabc1da1ec..e1db3d9fbd 100644 --- a/lib/render.js +++ b/lib/render.js @@ -1,10 +1,13 @@ 'use strict' const debug = require('debug')('nuxt:render') +import ansiHTML from 'ansi-html' import co from 'co' -import { urlJoin, getContext } from './utils' +import serialize from 'serialize-javascript' +import { getContext, setAnsiColors, encodeHtml } from './utils' // force blue color debug.color = 4 +setAnsiColors(ansiHTML) export function render (req, res) { if (!this.renderer && !this.dev) { @@ -12,7 +15,7 @@ export function render (req, res) { process.exit(1) } /* istanbul ignore if */ - if (!this.renderer) { + if (!this.renderer || !this.appTemplate) { return new Promise((resolve) => { setTimeout(() => { resolve(this.render(req, res)) @@ -27,6 +30,8 @@ export function render (req, res) { // Call webpack middleware only in development yield self.webpackDevMiddleware(req, res) yield self.webpackHotMiddleware(req, res) + } else { + yield self.gzipMiddleware(req, res) } // If base in req.url, remove it for the middleware and vue-router if (self.options.router.base !== '/' && req.url.indexOf(self.options.router.base) === 0) { @@ -62,8 +67,14 @@ export function render (req, res) { return html }) .catch((err) => { + const html = this.errorTemplate({ + error: err, + stack: ansiHTML(encodeHtml(err.stack)) + }) res.statusCode = 500 - res.end(this.errorTemplate({ err }), 'utf8') + res.setHeader('Content-Type', 'text/html; charset=utf-8') + res.setHeader('Content-Length', Buffer.byteLength(html)) + res.end(html, 'utf8') return err }) } @@ -76,21 +87,21 @@ export function renderRoute (url, context = {}) { // Call rendertoSting from the bundleRenderer and generate the HTML (will update the context as well) const self = this return co(function * () { - let app = yield self.renderToString(context) + let APP = yield self.renderToString(context) if (!context.nuxt.serverRendered) { - app = '
' + APP = '
' } - const publicPath = self.options.build.publicPath.indexOf('http') === 0 ? self.options.build.publicPath : urlJoin(self.options.router.base, self.options.build.publicPath) + const m = context.meta.inject() + let HEAD = m.meta.text() + m.title.text() + m.link.text() + m.style.text() + m.script.text() + m.noscript.text() + if (self.options.router.base !== '/') { + HEAD += `` + } + APP += `` const html = self.appTemplate({ - dev: self.dev, // Use to add the extracted CSS in production - baseUrl: self.options.router.base, - APP: app, - context: context, - files: { - css: urlJoin(publicPath, self.options.build.filenames.css), - vendor: urlJoin(publicPath, self.options.build.filenames.vendor), - app: urlJoin(publicPath, self.options.build.filenames.app) - } + HTML_ATTRS: 'n-head-ssr ' + m.htmlAttrs.text(), + BODY_ATTRS: m.bodyAttrs.text(), + HEAD, + APP }) return { html, diff --git a/lib/views/app.html b/lib/views/app.html deleted file mode 100644 index fcf4f15ede..0000000000 --- a/lib/views/app.html +++ /dev/null @@ -1,19 +0,0 @@ -<% var m = context.meta.inject() %> -> - - <%= m.meta.text() %> - <%= m.title.text() %> - <%= m.link.text() %> - <%= m.style.text() %> - <%= m.script.text() %> - <%= m.noscript.text() %> - <% if (baseUrl !== '/') { %><% } %> - <% if (!dev) { %><% } %> - - > - <%= APP %> - - - - - diff --git a/lib/views/app.template.html b/lib/views/app.template.html new file mode 100644 index 0000000000..3ef8d3b0fc --- /dev/null +++ b/lib/views/app.template.html @@ -0,0 +1,9 @@ + + + + {{ HEAD }} + + + {{ APP }} + + diff --git a/lib/views/error.html b/lib/views/error.html index e752bece5f..dd2f8d2d71 100644 --- a/lib/views/error.html +++ b/lib/views/error.html @@ -2,10 +2,10 @@ - Vue.js error + Nuxt.js error -

Vue.js error

-
<%= ansiHTML(encodeHtml(err.stack)) %>
+

Nuxt.js error

+
{{ stack }}
diff --git a/lib/webpack/client.config.js b/lib/webpack/client.config.js index 8cee34438e..2023e5b0fa 100644 --- a/lib/webpack/client.config.js +++ b/lib/webpack/client.config.js @@ -2,6 +2,9 @@ import { each } from 'lodash' import webpack from 'webpack' +import HTMLPlugin from 'html-webpack-plugin' +import ScriptExtHtmlWebpackPlugin from 'script-ext-html-webpack-plugin' +import PreloadWebpackPlugin from 'preload-webpack-plugin' import ExtractTextPlugin from 'extract-text-webpack-plugin' import ProgressBarPlugin from 'progress-bar-webpack-plugin' import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer' @@ -40,7 +43,7 @@ export default function () { }) // Webpack plugins config.plugins = (config.plugins || []).concat([ - // strip comments in Vue code + // Strip comments in Vue code new webpack.DefinePlugin(Object.assign(env, { 'process.env.NODE_ENV': JSON.stringify(this.dev ? 'development' : 'production'), 'process.BROWSER_BUILD': true, @@ -50,6 +53,18 @@ export default function () { new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', filename: this.options.build.filenames.vendor + }), + // Generate output HTML + new HTMLPlugin({ + template: resolve(__dirname, 'views/app.template.html') + }), + // Add defer to scripts + new ScriptExtHtmlWebpackPlugin({ + defaultAttribute: 'defer' + }), + // Add prefetch code-splitted routes + new PreloadWebpackPlugin({ + rel: 'prefetch' }) ]) diff --git a/package.json b/package.json index bd927a9b77..c68915f5a2 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "chalk": "^1.1.3", "chokidar": "^1.6.1", "co": "^4.6.0", + "compression": "^1.6.2", "css-loader": "^0.26.1", "debug": "^2.6.1", "extract-text-webpack-plugin": "2.0.0-beta.4", @@ -63,13 +64,16 @@ "glob": "^7.1.1", "hash-sum": "^1.0.2", "html-minifier": "^3.3.1", + "html-webpack-plugin": "^2.28.0", "lodash": "^4.17.4", "lru-cache": "^4.0.2", "memory-fs": "^0.4.1", "path-to-regexp": "^1.7.0", "pify": "^2.3.0", "post-compile-webpack-plugin": "^0.1.1", + "preload-webpack-plugin": "^1.2.0", "progress-bar-webpack-plugin": "^1.9.3", + "script-ext-html-webpack-plugin": "^1.7.1", "serialize-javascript": "^1.3.0", "serve-static": "^1.11.2", "url-loader": "^0.5.7",