Skip to content

Commit 869f005

Browse files
committed
feat: support multi-page app via pages option
1 parent f0fd375 commit 869f005

File tree

7 files changed

+281
-43
lines changed

7 files changed

+281
-43
lines changed

packages/@vue/cli-service/__tests__/build.spec.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ test('build', async () => {
2626
const index = await project.read('dist/index.html')
2727
// should split and preload app.js & vendor.js
2828
expect(index).toMatch(/<link [^>]+js\/app[^>]+\.js rel=preload>/)
29-
expect(index).toMatch(/<link [^>]+js\/vendors~app[^>]+\.js rel=preload>/)
29+
expect(index).toMatch(/<link [^>]+js\/chunk-vendors[^>]+\.js rel=preload>/)
3030
// should preload css
3131
expect(index).toMatch(/<link [^>]+app[^>]+\.css rel=preload>/)
3232

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
jest.setTimeout(30000)
2+
3+
const path = require('path')
4+
const portfinder = require('portfinder')
5+
const { defaultPreset } = require('@vue/cli/lib/options')
6+
const { createServer } = require('http-server')
7+
const create = require('@vue/cli-test-utils/createTestProject')
8+
const serve = require('@vue/cli-test-utils/serveWithPuppeteer')
9+
const launchPuppeteer = require('@vue/cli-test-utils/launchPuppeteer')
10+
11+
async function makeProjectMultiPage (project) {
12+
await project.write('vue.config.js', `
13+
module.exports = {
14+
pages: {
15+
index: { entry: 'src/main.js' },
16+
foo: { entry: 'src/foo.js' },
17+
bar: { entry: 'src/bar.js' }
18+
},
19+
chainWebpack: config => {
20+
const splitOptions = config.optimization.get('splitChunks')
21+
config.optimization.splitChunks(Object.assign({}, splitOptions, {
22+
minSize: 10000
23+
}))
24+
}
25+
}
26+
`)
27+
await project.write('src/foo.js', `
28+
import Vue from 'vue'
29+
new Vue({
30+
el: '#app',
31+
render: h => h('h1', 'Foo')
32+
})
33+
`)
34+
await project.write('src/bar.js', `
35+
import Vue from 'vue'
36+
import App from './App.vue'
37+
new Vue({
38+
el: '#app',
39+
render: h => h(App)
40+
})
41+
`)
42+
const app = await project.read('src/App.vue')
43+
await project.write('src/App.vue', app.replace(
44+
`import HelloWorld from './components/HelloWorld.vue'`,
45+
`const HelloWorld = () => import('./components/HelloWorld.vue')`
46+
))
47+
}
48+
49+
test('serve w/ multi page', async () => {
50+
const project = await create('e2e-multi-page-serve', defaultPreset)
51+
52+
await makeProjectMultiPage(project)
53+
54+
await serve(
55+
() => project.run('vue-cli-service serve'),
56+
async ({ page, url, helpers }) => {
57+
expect(await helpers.getText('h1')).toMatch(`Welcome to Your Vue.js App`)
58+
59+
await page.goto(`${url}/foo.html`)
60+
expect(await helpers.getText('h1')).toMatch(`Foo`)
61+
62+
await page.goto(`${url}/bar.html`)
63+
expect(await helpers.getText('h1')).toMatch(`Welcome to Your Vue.js App`)
64+
}
65+
)
66+
})
67+
68+
let server, browser, page
69+
test('build w/ multi page', async () => {
70+
const project = await create('e2e-multi-page-build', defaultPreset)
71+
72+
await makeProjectMultiPage(project)
73+
74+
const { stdout } = await project.run('vue-cli-service build')
75+
expect(stdout).toMatch('Build complete.')
76+
77+
// should generate the HTML pages
78+
expect(project.has('dist/index.html')).toBe(true)
79+
expect(project.has('dist/foo.html')).toBe(true)
80+
expect(project.has('dist/bar.html')).toBe(true)
81+
82+
const assertSharedAssets = file => {
83+
// should split and preload vendor chunk
84+
expect(file).toMatch(/<link [^>]+js\/chunk-vendors[^>]+\.js rel=preload>/)
85+
// should split and preload common js and css
86+
expect(file).toMatch(/<link [^>]+js\/chunk-common[^>]+\.js rel=preload>/)
87+
expect(file).toMatch(/<link [^>]+chunk-common[^>]+\.css rel=preload>/)
88+
// should load common css
89+
expect(file).toMatch(/<link href=\/css\/chunk-common\.\w+\.css rel=stylesheet>/)
90+
// should load common js
91+
expect(file).toMatch(/<script [^>]+src=\/js\/chunk-vendors\.\w+\.js>/)
92+
expect(file).toMatch(/<script [^>]+src=\/js\/chunk-common\.\w+\.js>/)
93+
}
94+
95+
const index = await project.read('dist/index.html')
96+
assertSharedAssets(index)
97+
// should preload correct page file
98+
expect(index).toMatch(/<link [^>]+js\/index[^>]+\.js rel=preload>/)
99+
expect(index).not.toMatch(/<link [^>]+js\/foo[^>]+\.js rel=preload>/)
100+
expect(index).not.toMatch(/<link [^>]+js\/bar[^>]+\.js rel=preload>/)
101+
// should prefetch async chunk js and css
102+
expect(index).toMatch(/<link [^>]+css\/0\.\w+\.css rel=prefetch>/)
103+
expect(index).toMatch(/<link [^>]+js\/0\.\w+\.js rel=prefetch>/)
104+
// should load correct page js
105+
expect(index).toMatch(/<script [^>]+src=\/js\/index\.\w+\.js>/)
106+
expect(index).not.toMatch(/<script [^>]+src=\/js\/foo\.\w+\.js>/)
107+
expect(index).not.toMatch(/<script [^>]+src=\/js\/bar\.\w+\.js>/)
108+
109+
const foo = await project.read('dist/foo.html')
110+
assertSharedAssets(foo)
111+
// should preload correct page file
112+
expect(foo).not.toMatch(/<link [^>]+js\/index[^>]+\.js rel=preload>/)
113+
expect(foo).toMatch(/<link [^>]+js\/foo[^>]+\.js rel=preload>/)
114+
expect(foo).not.toMatch(/<link [^>]+js\/bar[^>]+\.js rel=preload>/)
115+
// should not prefetch async chunk js and css because it's not used by
116+
// this entry
117+
expect(foo).not.toMatch(/<link [^>]+css\/0\.\w+\.css rel=prefetch>/)
118+
expect(foo).not.toMatch(/<link [^>]+js\/0\.\w+\.js rel=prefetch>/)
119+
// should load correct page js
120+
expect(foo).not.toMatch(/<script [^>]+src=\/js\/index\.\w+\.js>/)
121+
expect(foo).toMatch(/<script [^>]+src=\/js\/foo\.\w+\.js>/)
122+
expect(foo).not.toMatch(/<script [^>]+src=\/js\/bar\.\w+\.js>/)
123+
124+
const bar = await project.read('dist/bar.html')
125+
assertSharedAssets(bar)
126+
// should preload correct page file
127+
expect(bar).not.toMatch(/<link [^>]+js\/index[^>]+\.js rel=preload>/)
128+
expect(bar).not.toMatch(/<link [^>]+js\/foo[^>]+\.js rel=preload>/)
129+
expect(bar).toMatch(/<link [^>]+js\/bar[^>]+\.js rel=preload>/)
130+
// should prefetch async chunk js and css
131+
expect(bar).toMatch(/<link [^>]+css\/0\.\w+\.css rel=prefetch>/)
132+
expect(bar).toMatch(/<link [^>]+js\/0\.\w+\.js rel=prefetch>/)
133+
// should load correct page js
134+
expect(bar).not.toMatch(/<script [^>]+src=\/js\/index\.\w+\.js>/)
135+
expect(bar).not.toMatch(/<script [^>]+src=\/js\/foo\.\w+\.js>/)
136+
expect(bar).toMatch(/<script [^>]+src=\/js\/bar\.\w+\.js>/)
137+
138+
// assert pages work
139+
const port = await portfinder.getPortPromise()
140+
server = createServer({ root: path.join(project.dir, 'dist') })
141+
142+
await new Promise((resolve, reject) => {
143+
server.listen(port, err => {
144+
if (err) return reject(err)
145+
resolve()
146+
})
147+
})
148+
149+
const url = `http://localhost:${port}/`
150+
const launched = await launchPuppeteer(url)
151+
browser = launched.browser
152+
page = launched.page
153+
154+
const getH1Text = async () => page.evaluate(() => {
155+
return document.querySelector('h1').textContent
156+
})
157+
158+
expect(await getH1Text()).toMatch('Welcome to Your Vue.js App')
159+
160+
await page.goto(`${url}foo.html`)
161+
expect(await getH1Text()).toMatch('Foo')
162+
163+
await page.goto(`${url}bar.html`)
164+
expect(await getH1Text()).toMatch('Welcome to Your Vue.js App')
165+
})
166+
167+
afterAll(async () => {
168+
await browser.close()
169+
server.close()
170+
})

packages/@vue/cli-service/__tests__/serve.spec.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
jest.setTimeout(45000)
1+
jest.setTimeout(60000)
22

33
const path = require('path')
44
const fs = require('fs-extra')

packages/@vue/cli-service/lib/config/app.js

+99-26
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,8 @@ module.exports = (api, options) => {
1313

1414
// HTML plugin
1515
const resolveClientEnv = require('../util/resolveClientEnv')
16-
const htmlPath = api.resolve('public/index.html')
16+
1717
const htmlOptions = {
18-
// use default index.html
19-
template: fs.existsSync(htmlPath)
20-
? htmlPath
21-
: path.resolve(__dirname, 'index-default.html'),
2218
templateParameters: (compilation, assets, pluginOptions) => {
2319
// enhance html-webpack-plugin's built in template params
2420
let stats
@@ -51,26 +47,91 @@ module.exports = (api, options) => {
5147
})
5248
}
5349

54-
webpackConfig
55-
.plugin('html')
56-
.use(require('html-webpack-plugin'), [htmlOptions])
57-
58-
// inject preload/prefetch to HTML
59-
const PreloadPlugin = require('preload-webpack-plugin')
60-
webpackConfig
61-
.plugin('preload')
62-
.use(PreloadPlugin, [{
63-
rel: 'preload',
64-
include: 'initial',
65-
fileBlacklist: [/\.map$/, /hot-update\.js$/]
66-
}])
67-
68-
webpackConfig
69-
.plugin('prefetch')
70-
.use(PreloadPlugin, [{
71-
rel: 'prefetch',
72-
include: 'asyncChunks'
73-
}])
50+
// resolve HTML file(s)
51+
const HTMLPlugin = require('html-webpack-plugin')
52+
const PreloadPlugin = require('@vue/preload-webpack-plugin')
53+
const multiPageConfig = options.pages
54+
const htmlPath = api.resolve('public/index.html')
55+
const defaultHtmlPath = path.resolve(__dirname, 'index-default.html')
56+
57+
if (!multiPageConfig) {
58+
// default, single page setup.
59+
htmlOptions.template = fs.existsSync(htmlPath)
60+
? htmlPath
61+
: defaultHtmlPath
62+
63+
webpackConfig
64+
.plugin('html')
65+
.use(HTMLPlugin, [htmlOptions])
66+
67+
// inject preload/prefetch to HTML
68+
webpackConfig
69+
.plugin('preload')
70+
.use(PreloadPlugin, [{
71+
rel: 'preload',
72+
include: 'initial',
73+
fileBlacklist: [/\.map$/, /hot-update\.js$/]
74+
}])
75+
76+
webpackConfig
77+
.plugin('prefetch')
78+
.use(PreloadPlugin, [{
79+
rel: 'prefetch',
80+
include: 'asyncChunks'
81+
}])
82+
} else {
83+
// multi-page setup
84+
webpackConfig.entryPoints.clear()
85+
86+
const pages = Object.keys(multiPageConfig)
87+
88+
pages.forEach(name => {
89+
const {
90+
entry,
91+
template = `public/${name}.html`,
92+
filename = `${name}.html`
93+
} = multiPageConfig[name]
94+
// inject entry
95+
webpackConfig.entry(name).add(api.resolve(entry))
96+
97+
// inject html plugin for the page
98+
const pageHtmlOptions = Object.assign({}, htmlOptions, {
99+
chunks: ['chunk-vendors', 'chunk-common', name],
100+
template: fs.existsSync(template) ? template : defaultHtmlPath,
101+
filename
102+
})
103+
104+
webpackConfig
105+
.plugin(`html-${name}`)
106+
.use(HTMLPlugin, [pageHtmlOptions])
107+
})
108+
109+
pages.forEach(name => {
110+
const { filename = `${name}.html` } = multiPageConfig[name]
111+
webpackConfig
112+
.plugin(`preload-${name}`)
113+
.use(PreloadPlugin, [{
114+
rel: 'preload',
115+
includeHtmlNames: [filename],
116+
include: {
117+
type: 'initial',
118+
entries: [name]
119+
},
120+
fileBlacklist: [/\.map$/, /hot-update\.js$/]
121+
}])
122+
123+
webpackConfig
124+
.plugin(`prefetch-${name}`)
125+
.use(PreloadPlugin, [{
126+
rel: 'prefetch',
127+
includeHtmlNames: [filename],
128+
include: {
129+
type: 'asyncChunks',
130+
entries: [name]
131+
}
132+
}])
133+
})
134+
}
74135

75136
// copy static assets in public/
76137
if (fs.existsSync(api.resolve('public'))) {
@@ -87,7 +148,19 @@ module.exports = (api, options) => {
87148
if (isProd) {
88149
webpackConfig
89150
.optimization.splitChunks({
90-
chunks: 'all'
151+
chunks: 'all',
152+
name: (m, chunks, cacheGroup) => `chunk-${cacheGroup}`,
153+
cacheGroups: {
154+
vendors: {
155+
test: /[\\/]node_modules[\\/]/,
156+
priority: -10
157+
},
158+
common: {
159+
minChunks: 2,
160+
priority: -20,
161+
reuseExistingChunk: true
162+
}
163+
}
91164
})
92165
}
93166
})

packages/@vue/cli-service/lib/options.js

+4
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const schema = createSchema(joi => joi.object({
99
productionSourceMap: joi.boolean(),
1010
parallel: joi.boolean(),
1111
devServer: joi.object(),
12+
pages: joi.object(),
1213

1314
// css
1415
css: joi.object({
@@ -65,6 +66,9 @@ exports.defaults = () => ({
6566
// enabled by default if the machine has more than 1 cores
6667
parallel: require('os').cpus().length > 1,
6768

69+
// multi-page config
70+
pages: undefined,
71+
6872
css: {
6973
// extract: true,
7074
// modules: false,

packages/@vue/cli-service/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"dependencies": {
2424
"@vue/cli-overlay": "^3.0.0-beta.11",
2525
"@vue/cli-shared-utils": "^3.0.0-beta.11",
26+
"@vue/preload-webpack-plugin": "^1.0.0",
2627
"@vue/web-component-wrapper": "^1.2.0",
2728
"address": "^1.0.3",
2829
"autoprefixer": "^8.4.1",
@@ -47,7 +48,6 @@
4748
"ora": "^2.1.0",
4849
"portfinder": "^1.0.13",
4950
"postcss-loader": "^2.1.5",
50-
"preload-webpack-plugin": "^3.0.0-alpha.1",
5151
"read-pkg": "^3.0.0",
5252
"semver": "^5.5.0",
5353
"slash": "^2.0.0",

0 commit comments

Comments
 (0)