feat: support multi-page app via pages option

This commit is contained in:
Evan You
2018-05-28 19:38:33 -04:00
parent f0fd375333
commit 869f00513e
7 changed files with 279 additions and 41 deletions

View File

@@ -26,7 +26,7 @@ test('build', async () => {
const index = await project.read('dist/index.html')
// should split and preload app.js & vendor.js
expect(index).toMatch(/<link [^>]+js\/app[^>]+\.js rel=preload>/)
expect(index).toMatch(/<link [^>]+js\/vendors~app[^>]+\.js rel=preload>/)
expect(index).toMatch(/<link [^>]+js\/chunk-vendors[^>]+\.js rel=preload>/)
// should preload css
expect(index).toMatch(/<link [^>]+app[^>]+\.css rel=preload>/)

View File

@@ -0,0 +1,170 @@
jest.setTimeout(30000)
const path = require('path')
const portfinder = require('portfinder')
const { defaultPreset } = require('@vue/cli/lib/options')
const { createServer } = require('http-server')
const create = require('@vue/cli-test-utils/createTestProject')
const serve = require('@vue/cli-test-utils/serveWithPuppeteer')
const launchPuppeteer = require('@vue/cli-test-utils/launchPuppeteer')
async function makeProjectMultiPage (project) {
await project.write('vue.config.js', `
module.exports = {
pages: {
index: { entry: 'src/main.js' },
foo: { entry: 'src/foo.js' },
bar: { entry: 'src/bar.js' }
},
chainWebpack: config => {
const splitOptions = config.optimization.get('splitChunks')
config.optimization.splitChunks(Object.assign({}, splitOptions, {
minSize: 10000
}))
}
}
`)
await project.write('src/foo.js', `
import Vue from 'vue'
new Vue({
el: '#app',
render: h => h('h1', 'Foo')
})
`)
await project.write('src/bar.js', `
import Vue from 'vue'
import App from './App.vue'
new Vue({
el: '#app',
render: h => h(App)
})
`)
const app = await project.read('src/App.vue')
await project.write('src/App.vue', app.replace(
`import HelloWorld from './components/HelloWorld.vue'`,
`const HelloWorld = () => import('./components/HelloWorld.vue')`
))
}
test('serve w/ multi page', async () => {
const project = await create('e2e-multi-page-serve', defaultPreset)
await makeProjectMultiPage(project)
await serve(
() => project.run('vue-cli-service serve'),
async ({ page, url, helpers }) => {
expect(await helpers.getText('h1')).toMatch(`Welcome to Your Vue.js App`)
await page.goto(`${url}/foo.html`)
expect(await helpers.getText('h1')).toMatch(`Foo`)
await page.goto(`${url}/bar.html`)
expect(await helpers.getText('h1')).toMatch(`Welcome to Your Vue.js App`)
}
)
})
let server, browser, page
test('build w/ multi page', async () => {
const project = await create('e2e-multi-page-build', defaultPreset)
await makeProjectMultiPage(project)
const { stdout } = await project.run('vue-cli-service build')
expect(stdout).toMatch('Build complete.')
// should generate the HTML pages
expect(project.has('dist/index.html')).toBe(true)
expect(project.has('dist/foo.html')).toBe(true)
expect(project.has('dist/bar.html')).toBe(true)
const assertSharedAssets = file => {
// should split and preload vendor chunk
expect(file).toMatch(/<link [^>]+js\/chunk-vendors[^>]+\.js rel=preload>/)
// should split and preload common js and css
expect(file).toMatch(/<link [^>]+js\/chunk-common[^>]+\.js rel=preload>/)
expect(file).toMatch(/<link [^>]+chunk-common[^>]+\.css rel=preload>/)
// should load common css
expect(file).toMatch(/<link href=\/css\/chunk-common\.\w+\.css rel=stylesheet>/)
// should load common js
expect(file).toMatch(/<script [^>]+src=\/js\/chunk-vendors\.\w+\.js>/)
expect(file).toMatch(/<script [^>]+src=\/js\/chunk-common\.\w+\.js>/)
}
const index = await project.read('dist/index.html')
assertSharedAssets(index)
// should preload correct page file
expect(index).toMatch(/<link [^>]+js\/index[^>]+\.js rel=preload>/)
expect(index).not.toMatch(/<link [^>]+js\/foo[^>]+\.js rel=preload>/)
expect(index).not.toMatch(/<link [^>]+js\/bar[^>]+\.js rel=preload>/)
// should prefetch async chunk js and css
expect(index).toMatch(/<link [^>]+css\/0\.\w+\.css rel=prefetch>/)
expect(index).toMatch(/<link [^>]+js\/0\.\w+\.js rel=prefetch>/)
// should load correct page js
expect(index).toMatch(/<script [^>]+src=\/js\/index\.\w+\.js>/)
expect(index).not.toMatch(/<script [^>]+src=\/js\/foo\.\w+\.js>/)
expect(index).not.toMatch(/<script [^>]+src=\/js\/bar\.\w+\.js>/)
const foo = await project.read('dist/foo.html')
assertSharedAssets(foo)
// should preload correct page file
expect(foo).not.toMatch(/<link [^>]+js\/index[^>]+\.js rel=preload>/)
expect(foo).toMatch(/<link [^>]+js\/foo[^>]+\.js rel=preload>/)
expect(foo).not.toMatch(/<link [^>]+js\/bar[^>]+\.js rel=preload>/)
// should not prefetch async chunk js and css because it's not used by
// this entry
expect(foo).not.toMatch(/<link [^>]+css\/0\.\w+\.css rel=prefetch>/)
expect(foo).not.toMatch(/<link [^>]+js\/0\.\w+\.js rel=prefetch>/)
// should load correct page js
expect(foo).not.toMatch(/<script [^>]+src=\/js\/index\.\w+\.js>/)
expect(foo).toMatch(/<script [^>]+src=\/js\/foo\.\w+\.js>/)
expect(foo).not.toMatch(/<script [^>]+src=\/js\/bar\.\w+\.js>/)
const bar = await project.read('dist/bar.html')
assertSharedAssets(bar)
// should preload correct page file
expect(bar).not.toMatch(/<link [^>]+js\/index[^>]+\.js rel=preload>/)
expect(bar).not.toMatch(/<link [^>]+js\/foo[^>]+\.js rel=preload>/)
expect(bar).toMatch(/<link [^>]+js\/bar[^>]+\.js rel=preload>/)
// should prefetch async chunk js and css
expect(bar).toMatch(/<link [^>]+css\/0\.\w+\.css rel=prefetch>/)
expect(bar).toMatch(/<link [^>]+js\/0\.\w+\.js rel=prefetch>/)
// should load correct page js
expect(bar).not.toMatch(/<script [^>]+src=\/js\/index\.\w+\.js>/)
expect(bar).not.toMatch(/<script [^>]+src=\/js\/foo\.\w+\.js>/)
expect(bar).toMatch(/<script [^>]+src=\/js\/bar\.\w+\.js>/)
// assert pages work
const port = await portfinder.getPortPromise()
server = createServer({ root: path.join(project.dir, 'dist') })
await new Promise((resolve, reject) => {
server.listen(port, err => {
if (err) return reject(err)
resolve()
})
})
const url = `http://localhost:${port}/`
const launched = await launchPuppeteer(url)
browser = launched.browser
page = launched.page
const getH1Text = async () => page.evaluate(() => {
return document.querySelector('h1').textContent
})
expect(await getH1Text()).toMatch('Welcome to Your Vue.js App')
await page.goto(`${url}foo.html`)
expect(await getH1Text()).toMatch('Foo')
await page.goto(`${url}bar.html`)
expect(await getH1Text()).toMatch('Welcome to Your Vue.js App')
})
afterAll(async () => {
await browser.close()
server.close()
})

View File

@@ -1,4 +1,4 @@
jest.setTimeout(45000)
jest.setTimeout(60000)
const path = require('path')
const fs = require('fs-extra')

View File

@@ -13,12 +13,8 @@ module.exports = (api, options) => {
// HTML plugin
const resolveClientEnv = require('../util/resolveClientEnv')
const htmlPath = api.resolve('public/index.html')
const htmlOptions = {
// use default index.html
template: fs.existsSync(htmlPath)
? htmlPath
: path.resolve(__dirname, 'index-default.html'),
templateParameters: (compilation, assets, pluginOptions) => {
// enhance html-webpack-plugin's built in template params
let stats
@@ -51,26 +47,91 @@ module.exports = (api, options) => {
})
}
webpackConfig
.plugin('html')
.use(require('html-webpack-plugin'), [htmlOptions])
// resolve HTML file(s)
const HTMLPlugin = require('html-webpack-plugin')
const PreloadPlugin = require('@vue/preload-webpack-plugin')
const multiPageConfig = options.pages
const htmlPath = api.resolve('public/index.html')
const defaultHtmlPath = path.resolve(__dirname, 'index-default.html')
// inject preload/prefetch to HTML
const PreloadPlugin = require('preload-webpack-plugin')
webpackConfig
.plugin('preload')
.use(PreloadPlugin, [{
rel: 'preload',
include: 'initial',
fileBlacklist: [/\.map$/, /hot-update\.js$/]
}])
if (!multiPageConfig) {
// default, single page setup.
htmlOptions.template = fs.existsSync(htmlPath)
? htmlPath
: defaultHtmlPath
webpackConfig
.plugin('prefetch')
.use(PreloadPlugin, [{
rel: 'prefetch',
include: 'asyncChunks'
}])
webpackConfig
.plugin('html')
.use(HTMLPlugin, [htmlOptions])
// inject preload/prefetch to HTML
webpackConfig
.plugin('preload')
.use(PreloadPlugin, [{
rel: 'preload',
include: 'initial',
fileBlacklist: [/\.map$/, /hot-update\.js$/]
}])
webpackConfig
.plugin('prefetch')
.use(PreloadPlugin, [{
rel: 'prefetch',
include: 'asyncChunks'
}])
} else {
// multi-page setup
webpackConfig.entryPoints.clear()
const pages = Object.keys(multiPageConfig)
pages.forEach(name => {
const {
entry,
template = `public/${name}.html`,
filename = `${name}.html`
} = multiPageConfig[name]
// inject entry
webpackConfig.entry(name).add(api.resolve(entry))
// inject html plugin for the page
const pageHtmlOptions = Object.assign({}, htmlOptions, {
chunks: ['chunk-vendors', 'chunk-common', name],
template: fs.existsSync(template) ? template : defaultHtmlPath,
filename
})
webpackConfig
.plugin(`html-${name}`)
.use(HTMLPlugin, [pageHtmlOptions])
})
pages.forEach(name => {
const { filename = `${name}.html` } = multiPageConfig[name]
webpackConfig
.plugin(`preload-${name}`)
.use(PreloadPlugin, [{
rel: 'preload',
includeHtmlNames: [filename],
include: {
type: 'initial',
entries: [name]
},
fileBlacklist: [/\.map$/, /hot-update\.js$/]
}])
webpackConfig
.plugin(`prefetch-${name}`)
.use(PreloadPlugin, [{
rel: 'prefetch',
includeHtmlNames: [filename],
include: {
type: 'asyncChunks',
entries: [name]
}
}])
})
}
// copy static assets in public/
if (fs.existsSync(api.resolve('public'))) {
@@ -87,7 +148,19 @@ module.exports = (api, options) => {
if (isProd) {
webpackConfig
.optimization.splitChunks({
chunks: 'all'
chunks: 'all',
name: (m, chunks, cacheGroup) => `chunk-${cacheGroup}`,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
common: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
})
}
})

View File

@@ -9,6 +9,7 @@ const schema = createSchema(joi => joi.object({
productionSourceMap: joi.boolean(),
parallel: joi.boolean(),
devServer: joi.object(),
pages: joi.object(),
// css
css: joi.object({
@@ -65,6 +66,9 @@ exports.defaults = () => ({
// enabled by default if the machine has more than 1 cores
parallel: require('os').cpus().length > 1,
// multi-page config
pages: undefined,
css: {
// extract: true,
// modules: false,

View File

@@ -23,6 +23,7 @@
"dependencies": {
"@vue/cli-overlay": "^3.0.0-beta.11",
"@vue/cli-shared-utils": "^3.0.0-beta.11",
"@vue/preload-webpack-plugin": "^1.0.0",
"@vue/web-component-wrapper": "^1.2.0",
"address": "^1.0.3",
"autoprefixer": "^8.4.1",
@@ -47,7 +48,6 @@
"ora": "^2.1.0",
"portfinder": "^1.0.13",
"postcss-loader": "^2.1.5",
"preload-webpack-plugin": "^3.0.0-alpha.1",
"read-pkg": "^3.0.0",
"semver": "^5.5.0",
"slash": "^2.0.0",

View File

@@ -777,13 +777,6 @@
core-js "^2.5.3"
regenerator-runtime "^0.11.1"
"@babel/runtime@^7.0.0-beta.44":
version "7.0.0-beta.49"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.0.0-beta.49.tgz#03b3bf07eb982072c8e851dd2ddd5110282e61bf"
dependencies:
core-js "^2.5.6"
regenerator-runtime "^0.11.1"
"@babel/template@7.0.0-beta.44":
version "7.0.0-beta.44"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.0.0-beta.44.tgz#f8832f4fdcee5d59bf515e595fc5106c529b394f"
@@ -1002,6 +995,10 @@
source-map "^0.5.6"
vue-template-es2015-compiler "^1.6.0"
"@vue/preload-webpack-plugin@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@vue/preload-webpack-plugin/-/preload-webpack-plugin-1.0.0.tgz#08f156532909824da2aad258e151742d1e8f822e"
"@vue/test-utils@^1.0.0-beta.16":
version "1.0.0-beta.16"
resolved "https://registry.yarnpkg.com/@vue/test-utils/-/test-utils-1.0.0-beta.16.tgz#dcf7a30304391422e382b5f97db6eb9508112906"
@@ -2866,7 +2863,7 @@ copy-webpack-plugin@^4.5.1:
p-limit "^1.0.0"
serialize-javascript "^1.4.0"
core-js@^2.4.0, core-js@^2.5.3, core-js@^2.5.6:
core-js@^2.4.0, core-js@^2.5.3:
version "2.5.7"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e"
@@ -8441,12 +8438,6 @@ postcss@^6.0.0, postcss@^6.0.1, postcss@^6.0.20, postcss@^6.0.22:
source-map "^0.6.1"
supports-color "^5.4.0"
preload-webpack-plugin@^3.0.0-alpha.1:
version "3.0.0-alpha.3"
resolved "https://registry.yarnpkg.com/preload-webpack-plugin/-/preload-webpack-plugin-3.0.0-alpha.3.tgz#ecf0488a9f29b58e8e0ff295c7f6f11eb6c98efe"
dependencies:
"@babel/runtime" "^7.0.0-beta.44"
prelude-ls@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"