Merge pull request #3060 from nuxt/feat/asyncChunks

feat: make optimization and splitChunks configurable
This commit is contained in:
Sébastien Chopin 2018-03-20 13:31:32 +01:00 committed by GitHub
commit d1d637f0c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 147 additions and 76 deletions

View File

@ -33,6 +33,7 @@ if (argv.help) {
--analyze, -a Launch webpack-bundle-analyzer to optimize your bundles. --analyze, -a Launch webpack-bundle-analyzer to optimize your bundles.
--spa Launch in SPA mode --spa Launch in SPA mode
--universal Launch in Universal mode (default) --universal Launch in Universal mode (default)
--generate Generate static version after build
--config-file, -c Path to Nuxt.js config file (default: nuxt.config.js) --config-file, -c Path to Nuxt.js config file (default: nuxt.config.js)
--help, -h Displays this message --help, -h Displays this message
`) `)
@ -65,14 +66,14 @@ const close = () => {
process.exit(0) process.exit(0)
} }
if (options.mode !== 'spa') { if (!argv.generate) {
// -- Build for SSR app -- // -- Build only --
builder builder
.build() .build()
.then(() => close()) .then(() => close())
.catch(Utils.fatalError) .catch(Utils.fatalError)
} else { } else {
// -- Build for SPA app -- // -- Build and generate --
const s = Date.now() const s = Date.now()
nuxt.hook('generate:distRemoved', () => debug('Destination folder cleaned')) nuxt.hook('generate:distRemoved', () => debug('Destination folder cleaned'))
@ -92,9 +93,6 @@ if (options.mode !== 'spa') {
} }
}) })
// Disable minify to get exact results of nuxt start
nuxt.options.generate.minify = false
// Generate dist for SPA static deployment // Generate dist for SPA static deployment
new Generator(nuxt, builder).generate({ build: true }).then(() => { new Generator(nuxt, builder).generate({ build: true }).then(() => {
close() close()

View File

@ -5,7 +5,7 @@ import '<%= relativeToBuild(resolvePath(c.src || c)) %>'
<% }) %> <% }) %>
<%= Object.keys(layouts).map(key => { <%= Object.keys(layouts).map(key => {
if (splitPages) { if (splitChunks.layouts) {
return `const _${hash(key)} = () => import('${layouts[key]}' /* webpackChunkName: "${wChunk('layouts/' + key)}" */).then(m => m.default || m)` return `const _${hash(key)} = () => import('${layouts[key]}' /* webpackChunkName: "${wChunk('layouts/' + key)}" */).then(m => m.default || m)`
} else { } else {
return `import _${hash(key)} from '${layouts[key]}'` return `import _${hash(key)} from '${layouts[key]}'`
@ -14,7 +14,7 @@ import '<%= relativeToBuild(resolvePath(c.src || c)) %>'
const layouts = { <%= Object.keys(layouts).map(key => `"_${key}": _${hash(key)}`).join(',') %> } const layouts = { <%= Object.keys(layouts).map(key => `"_${key}": _${hash(key)}`).join(',') %> }
<% if (splitPages) { %>let resolvedLayouts = {}<% } %> <% if (splitChunks.layouts) { %>let resolvedLayouts = {}<% } %>
export default { export default {
head: <%= serialize(head).replace('head(', 'function(').replace('titleTemplate(', 'function(') %>, head: <%= serialize(head).replace('head(', 'function(').replace('titleTemplate(', 'function(') %>,
@ -78,7 +78,7 @@ export default {
} }
}, },
<% } %> <% } %>
<% if (splitPages) { %> <% if (splitChunks.layouts) { %>
setLayout (layout) { setLayout (layout) {
if (!layout || !resolvedLayouts['_' + layout]) layout = 'default' if (!layout || !resolvedLayouts['_' + layout]) layout = 'default'
this.layoutName = layout this.layoutName = layout
@ -112,6 +112,7 @@ export default {
return this.layout return this.layout
}, },
loadLayout(layout) { loadLayout(layout) {
if (!layout || !layouts['_' + layout]) layout = 'default'
return Promise.resolve(layouts['_' + layout]) return Promise.resolve(layouts['_' + layout])
} }
<% } %> <% } %>

View File

@ -8,7 +8,7 @@ import Router from 'vue-router'
components.push({ _name: route._name, component: route.component, name: route.name, chunkName: route.chunkName }) components.push({ _name: route._name, component: route.component, name: route.name, chunkName: route.chunkName })
res += tab + '{\n' res += tab + '{\n'
res += tab + '\tpath: ' + JSON.stringify(route.path) res += tab + '\tpath: ' + JSON.stringify(route.path)
res += (route.component) ? ',\n\t' + tab + 'component: ' + (splitPages ? route._name : `() => ${route._name}.default || ${route._name}`) : '' res += (route.component) ? ',\n\t' + tab + 'component: ' + (splitChunks.pages ? route._name : `() => ${route._name}.default || ${route._name}`) : ''
res += (route.redirect) ? ',\n\t' + tab + 'redirect: ' + JSON.stringify(route.redirect) : '' res += (route.redirect) ? ',\n\t' + tab + 'redirect: ' + JSON.stringify(route.redirect) : ''
res += (route.name) ? ',\n\t' + tab + 'name: ' + JSON.stringify(route.name) : '' res += (route.name) ? ',\n\t' + tab + 'name: ' + JSON.stringify(route.name) : ''
res += (route.children) ? ',\n\t' + tab + 'children: [\n' + recursiveRoutes(routes[i].children, tab + '\t\t', components) + '\n\t' + tab + ']' : '' res += (route.children) ? ',\n\t' + tab + 'children: [\n' + recursiveRoutes(routes[i].children, tab + '\t\t', components) + '\n\t' + tab + ']' : ''
@ -24,7 +24,7 @@ const _routes = recursiveRoutes(router.routes, '\t\t', _components)
const chunkName = wChunk(route.chunkName) const chunkName = wChunk(route.chunkName)
const name = route._name const name = route._name
if (splitPages) { if (splitChunks.pages) {
return `const ${name} = () => import('${path}' /* webpackChunkName: "${chunkName}" */).then(m => m.default || m)` return `const ${name} = () => import('${path}' /* webpackChunkName: "${chunkName}" */).then(m => m.default || m)`
} else { } else {
return `import ${name} from '${path}'` return `import ${name} from '${path}'`

View File

@ -211,7 +211,7 @@ export default class Builder {
.map(ext => ext.replace(/^\./, '')) .map(ext => ext.replace(/^\./, ''))
.join('|'), .join('|'),
messages: this.options.messages, messages: this.options.messages,
splitPages: this.options.build.splitPages, splitChunks: this.options.build.splitChunks,
uniqBy: _.uniqBy, uniqBy: _.uniqBy,
isDev: this.options.dev, isDev: this.options.dev,
debug: this.options.debug, debug: this.options.debug,

View File

@ -6,6 +6,7 @@ import webpack from 'webpack'
import HTMLPlugin from 'html-webpack-plugin' import HTMLPlugin from 'html-webpack-plugin'
import StylishPlugin from 'webpack-stylish' import StylishPlugin from 'webpack-stylish'
import BundleAnalyzer from 'webpack-bundle-analyzer' import BundleAnalyzer from 'webpack-bundle-analyzer'
import HtmlWebpackInlineSourcePlugin from 'html-webpack-inline-source-plugin'
import Debug from 'debug' import Debug from 'debug'
import base from './base.config' import base from './base.config'
@ -42,6 +43,7 @@ export default function webpackClientConfig() {
filename: 'index.spa.html', filename: 'index.spa.html',
template: this.options.appTemplatePath, template: this.options.appTemplatePath,
inject: true, inject: true,
inlineSource: /runtime.*\.js$/,
chunksSortMode: 'dependency' chunksSortMode: 'dependency'
}) })
) )
@ -57,6 +59,12 @@ export default function webpackClientConfig() {
) )
} }
// Enhances html-webpack-plugin functionality by adding the inlineSource option.
// https://github.com/DustinJackson/html-webpack-inline-source-plugin
if (!this.options.dev) {
config.plugins.push(new HtmlWebpackInlineSourcePlugin())
}
// Generate vue-ssr-client-manifest // Generate vue-ssr-client-manifest
config.plugins.push( config.plugins.push(
new VueSSRClientPlugin({ new VueSSRClientPlugin({
@ -78,48 +86,59 @@ export default function webpackClientConfig() {
) )
) )
// Optimization // -- Optimization --
config.optimization.splitChunks = { config.optimization = this.options.build.optimization
chunks: 'all',
// TODO: remove spa after https://github.com/jantimon/html-webpack-plugin/issues/878 solved // TODO: remove spa check after https://github.com/jantimon/html-webpack-plugin/issues/878 solved
name: this.options.dev || this.options.mode === 'spa', if (this.options.dev || this.options.mode === 'spa') {
config.optimization.splitChunks.name = true
}
// ... Explicit cache groups
// Explicit cache groups
cacheGroups: {
// Vue.js core modules // Vue.js core modules
vue: { if (this.options.build.splitChunks.vue) {
config.optimization.splitChunks.cacheGroups.vue = {
test: /node_modules\/(vue|vue-loader|vue-router|vuex|vue-meta)\//, test: /node_modules\/(vue|vue-loader|vue-router|vuex|vue-meta)\//,
chunks: 'initial', chunks: 'initial',
name: 'vue', name: 'vue',
priority: 10, priority: 10,
enforce: true enforce: true
}, }
}
// Common modules which are usually included in projects // Common modules which are usually included in projects
common: { if (this.options.build.splitChunks.common) {
config.optimization.splitChunks.cacheGroups.common = {
test: /node_modules\/(core-js|babel-runtime|lodash|es6-promise|moment|axios|webpack|setimediate|timers-browserify|process)\//, test: /node_modules\/(core-js|babel-runtime|lodash|es6-promise|moment|axios|webpack|setimediate|timers-browserify|process)\//,
chunks: 'initial', chunks: 'initial',
name: 'common', name: 'common',
priority: 9 priority: 9
}, }
}
// Generated templates // Generated templates
main: { if (this.options.build.splitChunks.main) {
config.optimization.splitChunks.cacheGroups.main = {
test: /\.nuxt\//, test: /\.nuxt\//,
chunks: 'initial', chunks: 'initial',
name: 'main', name: 'main',
priority: 8 priority: 8
}, }
}
// Other vendors inside node_modules // Other vendors inside node_modules
vendor: { if (this.options.build.splitChunks.vendor) {
config.optimization.splitChunks.cacheGroups.vendor = {
test: /node_modules\//, test: /node_modules\//,
chunks: 'initial', chunks: 'initial',
name: 'vendor', name: 'vendor',
priority: 8 priority: 8
} }
} }
}
// Create additional runtime chunk for cache boosting // Create additional runtime chunk for cache boosting
config.optimization.runtimeChunk = true config.optimization.runtimeChunk = this.options.build.splitChunks.runtime
// -------------------------------------- // --------------------------------------
// Dev specific config // Dev specific config

View File

@ -18,7 +18,6 @@ export default {
build: { build: {
analyze: false, analyze: false,
profile: process.argv.includes('--profile'), profile: process.argv.includes('--profile'),
splitPages: true,
maxChunkSize: false, maxChunkSize: false,
extractCSS: false, extractCSS: false,
cssSourceMap: undefined, cssSourceMap: undefined,
@ -31,6 +30,20 @@ export default {
}, },
styleResources: {}, styleResources: {},
plugins: [], plugins: [],
optimization: {
splitChunks: {
chunks: 'all',
name: false,
cacheGroups: {}
}
},
splitChunks: {
pages: true,
vendor: true,
commons: true,
runtime: true,
layouts: false
},
babel: { babel: {
babelrc: false babelrc: false
}, },
@ -99,7 +112,7 @@ export default {
duration: 5000, duration: 5000,
rtl: false rtl: false
}, },
loadingIndicator: {}, loadingIndicator: false,
transition: { transition: {
name: 'page', name: 'page',
mode: 'out-in', mode: 'out-in',
@ -134,8 +147,10 @@ export default {
fallback: false fallback: false
}, },
render: { render: {
bundleRenderer: {}, bundleRenderer: {
resourceHints: true, shouldPrefetch: () => false
},
resourceHints: undefined,
ssr: undefined, ssr: undefined,
http2: { http2: {
push: false, push: false,

View File

@ -103,17 +103,14 @@ Options.from = function (_options) {
options.store = true options.store = true
} }
// SPA loadingIndicator
if (options.loadingIndicator) {
// Normalize loadingIndicator // Normalize loadingIndicator
if (!isPureObject(options.loadingIndicator)) { if (!isPureObject(options.loadingIndicator)) {
options.loadingIndicator = { name: options.loadingIndicator } options.loadingIndicator = { name: options.loadingIndicator }
} }
// Apply default hash to CSP option // Apply defaults
if (options.render.csp === true) {
options.render.csp = { hashAlgorithm: 'sha256' }
}
// Apply defaults to loadingIndicator
options.loadingIndicator = Object.assign( options.loadingIndicator = Object.assign(
{ {
name: 'pulse', name: 'pulse',
@ -122,6 +119,12 @@ Options.from = function (_options) {
}, },
options.loadingIndicator options.loadingIndicator
) )
}
// Apply default hash to CSP option
if (options.render.csp === true) {
options.render.csp = { hashAlgorithm: 'sha256' }
}
// cssSourceMap // cssSourceMap
if (options.build.cssSourceMap === undefined) { if (options.build.cssSourceMap === undefined) {
@ -138,6 +141,11 @@ Options.from = function (_options) {
options.debug = options.dev options.debug = options.dev
} }
// Resource hints
if (options.render.resourceHints === undefined) {
options.render.resourceHints = !options.dev
}
// Normalize ignore // Normalize ignore
options.ignore = options.ignore ? [].concat(options.ignore) : [] options.ignore = options.ignore ? [].concat(options.ignore) : []

View File

@ -43,12 +43,16 @@ export default class MetaRenderer {
HEAD: '', HEAD: '',
BODY_SCRIPTS: '' BODY_SCRIPTS: ''
} }
// Get vue-meta context // Get vue-meta context
const m = await this.getMeta(url) const m = await this.getMeta(url)
// HTML_ATTRS // HTML_ATTRS
meta.HTML_ATTRS = m.htmlAttrs.text() meta.HTML_ATTRS = m.htmlAttrs.text()
// BODY_ATTRS // BODY_ATTRS
meta.BODY_ATTRS = m.bodyAttrs.text() meta.BODY_ATTRS = m.bodyAttrs.text()
// HEAD tags // HEAD tags
meta.HEAD = meta.HEAD =
m.meta.text() + m.meta.text() +
@ -57,28 +61,42 @@ export default class MetaRenderer {
m.style.text() + m.style.text() +
m.script.text() + m.script.text() +
m.noscript.text() m.noscript.text()
// BODY_SCRIPTS // BODY_SCRIPTS
meta.BODY_SCRIPTS = m.script.text({ body: true }) + m.noscript.text({ body: true }) meta.BODY_SCRIPTS = m.script.text({ body: true }) + m.noscript.text({ body: true })
// Resources Hints // Resources Hints
meta.resourceHints = '' meta.resourceHints = ''
// Resource Hints
const clientManifest = this.renderer.resources.clientManifest const clientManifest = this.renderer.resources.clientManifest
const shouldPreload = this.options.render.bundleRenderer.shouldPreload || (() => true)
const shouldPrefetch = this.options.render.bundleRenderer.shouldPrefetch || (() => true)
const runtimeRegex = /runtime.*\.js$/
if (this.options.render.resourceHints && clientManifest) { if (this.options.render.resourceHints && clientManifest) {
const publicPath = clientManifest.publicPath || '/_nuxt/' const publicPath = clientManifest.publicPath || '/_nuxt/'
// Pre-Load initial resources
// Preload initial resources
if (Array.isArray(clientManifest.initial)) { if (Array.isArray(clientManifest.initial)) {
meta.resourceHints += clientManifest.initial meta.resourceHints += clientManifest.initial
.filter(file => shouldPreload(file) && !(runtimeRegex.test(file)))
.map( .map(
r => `<link rel="preload" href="${publicPath}${r}" as="script" />` r => `<link rel="preload" href="${publicPath}${r}" as="script" />`
) )
.join('') .join('')
} }
// Pre-Fetch async resources
// Prefetch async resources
if (Array.isArray(clientManifest.async)) { if (Array.isArray(clientManifest.async)) {
meta.resourceHints += clientManifest.async meta.resourceHints += clientManifest.async
.filter(file => shouldPrefetch(file))
.map(r => `<link rel="prefetch" href="${publicPath}${r}" />`) .map(r => `<link rel="prefetch" href="${publicPath}${r}" />`)
.join('') .join('')
} }
// Add them to HEAD // Add them to HEAD
if (meta.resourceHints) { if (meta.resourceHints) {
meta.HEAD += meta.resourceHints meta.HEAD += meta.resourceHints
@ -87,7 +105,9 @@ export default class MetaRenderer {
// Emulate getPreloadFiles from vue-server-renderer (works for JS chunks only) // Emulate getPreloadFiles from vue-server-renderer (works for JS chunks only)
meta.getPreloadFiles = () => meta.getPreloadFiles = () =>
clientManifest.initial.map(r => ({ clientManifest.initial
.filter(file => shouldPreload(file) && !(runtimeRegex.test(file)))
.map(r => ({
file: r, file: r,
fileWithoutQuery: r, fileWithoutQuery: r,
asType: 'script', asType: 'script',

View File

@ -79,6 +79,7 @@
"glob": "^7.1.2", "glob": "^7.1.2",
"hash-sum": "^1.0.2", "hash-sum": "^1.0.2",
"html-minifier": "^3.5.11", "html-minifier": "^3.5.11",
"html-webpack-inline-source-plugin": "^0.0.10",
"html-webpack-plugin": "^3.0.6", "html-webpack-plugin": "^3.0.6",
"launch-editor-middleware": "^2.2.1", "launch-editor-middleware": "^2.2.1",
"lodash": "^4.17.5", "lodash": "^4.17.5",

View File

@ -6,7 +6,8 @@ export default class Browser {
this.browser = await puppeteer.launch( this.browser = await puppeteer.launch(
Object.assign( Object.assign(
{ {
args: ['--no-sandbox', '--disable-setuid-sandbox'] args: ['--no-sandbox', '--disable-setuid-sandbox'],
executablePath: process.env.PUPPETEER_EXECUTABLE_PATH
}, },
options options
) )

View File

@ -3345,6 +3345,14 @@ html-tags@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-2.0.0.tgz#10b30a386085f43cede353cc8fa7cb0deeea668b" resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-2.0.0.tgz#10b30a386085f43cede353cc8fa7cb0deeea668b"
html-webpack-inline-source-plugin@^0.0.10:
version "0.0.10"
resolved "https://registry.yarnpkg.com/html-webpack-inline-source-plugin/-/html-webpack-inline-source-plugin-0.0.10.tgz#89bd5f761e4f16902aa76a44476eb52831c9f7f0"
dependencies:
escape-string-regexp "^1.0.5"
slash "^1.0.0"
source-map-url "^0.4.0"
html-webpack-plugin@^3.0.6: html-webpack-plugin@^3.0.6:
version "3.0.6" version "3.0.6"
resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-3.0.6.tgz#d35b0452aae129a8a9f3fac44a169a625d8cf3fa" resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-3.0.6.tgz#d35b0452aae129a8a9f3fac44a169a625d8cf3fa"