Skip to content

Commit

Permalink
feat: support multi-page app via pages option
Browse files Browse the repository at this point in the history
  • Loading branch information
yyx990803 committed May 28, 2018
1 parent f0fd375 commit 869f005
Show file tree
Hide file tree
Showing 7 changed files with 281 additions and 43 deletions.
2 changes: 1 addition & 1 deletion packages/@vue/cli-service/__tests__/build.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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>/)

Expand Down
170 changes: 170 additions & 0 deletions packages/@vue/cli-service/__tests__/multiPage.spec.js
Original file line number Diff line number Diff line change
@@ -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()
})
2 changes: 1 addition & 1 deletion packages/@vue/cli-service/__tests__/serve.spec.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
jest.setTimeout(45000)
jest.setTimeout(60000)

const path = require('path')
const fs = require('fs-extra')
Expand Down
125 changes: 99 additions & 26 deletions packages/@vue/cli-service/lib/config/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -51,26 +47,91 @@ module.exports = (api, options) => {
})
}

webpackConfig
.plugin('html')
.use(require('html-webpack-plugin'), [htmlOptions])

// 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$/]
}])

webpackConfig
.plugin('prefetch')
.use(PreloadPlugin, [{
rel: 'prefetch',
include: 'asyncChunks'
}])
// 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')

if (!multiPageConfig) {
// default, single page setup.
htmlOptions.template = fs.existsSync(htmlPath)
? htmlPath
: defaultHtmlPath

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'))) {
Expand All @@ -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
}
}
})
}
})
Expand Down
4 changes: 4 additions & 0 deletions packages/@vue/cli-service/lib/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion packages/@vue/cli-service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
Loading

0 comments on commit 869f005

Please sign in to comment.