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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 170 additions & 0 deletions
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 99 additions & 26 deletions
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

Lines changed: 4 additions & 0 deletions
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

Lines changed: 1 addition & 1 deletion
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy