mirror of
https://github.com/vuejs/vue-cli.git
synced 2026-04-20 03:00:57 -05:00
feat: support multi-page app via pages option
This commit is contained in:
@@ -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>/)
|
||||
|
||||
|
||||
170
packages/@vue/cli-service/__tests__/multiPage.spec.js
Normal file
170
packages/@vue/cli-service/__tests__/multiPage.spec.js
Normal 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()
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
jest.setTimeout(45000)
|
||||
jest.setTimeout(60000)
|
||||
|
||||
const path = require('path')
|
||||
const fs = require('fs-extra')
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
19
yarn.lock
19
yarn.lock
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user