mirror of
https://github.com/vuejs/vue-cli.git
synced 2026-03-14 05:01:26 -05:00
preload/prefetch + inline manifest
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
node_modules
|
||||
template
|
||||
packages/test
|
||||
temp
|
||||
|
||||
@@ -104,6 +104,22 @@ module.exports = (api, options) => {
|
||||
template: api.resolve('public/index.html')
|
||||
}])
|
||||
|
||||
// inject preload/prefetch to HTML
|
||||
const PreloadPlugin = require('../util/PreloadPlugin')
|
||||
webpackConfig
|
||||
.plugin('preload')
|
||||
.use(PreloadPlugin, [{
|
||||
rel: 'preload',
|
||||
include: 'initial'
|
||||
}])
|
||||
|
||||
webpackConfig
|
||||
.plugin('prefetch')
|
||||
.use(PreloadPlugin, [{
|
||||
rel: 'prefetch',
|
||||
include: 'asyncChunks'
|
||||
}])
|
||||
|
||||
webpackConfig
|
||||
.plugin('define')
|
||||
.use(require('webpack/lib/DefinePlugin'), [resolveClientEnv()])
|
||||
|
||||
@@ -82,6 +82,21 @@ module.exports = (api, options) => {
|
||||
minChunks: Infinity
|
||||
}])
|
||||
|
||||
// inline the manifest chunk into HTML
|
||||
webpackConfig
|
||||
.plugin('inline-manifest')
|
||||
.use(require('../util/InlineSourcePlugin'), [{
|
||||
include: /manifest\..*\.js$/
|
||||
}])
|
||||
|
||||
// since manifest is inlined, don't preload it anymore
|
||||
webpackConfig
|
||||
.plugin('preload')
|
||||
.tap(([options]) => {
|
||||
options.fileBlacklist = [/\.map$/, /manifest\..*\.js$/]
|
||||
return [options]
|
||||
})
|
||||
|
||||
// This instance extracts shared chunks from code splitted chunks and bundles them
|
||||
// in a separate chunk, similar to the vendor chunk
|
||||
// see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk
|
||||
|
||||
117
packages/@vue/cli-service/lib/util/InlineSourcePlugin.js
Normal file
117
packages/@vue/cli-service/lib/util/InlineSourcePlugin.js
Normal file
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
The MIT License (MIT)
|
||||
Copyright (c) 2016 Jan Nicklas
|
||||
https://github.com/DustinJackson/html-webpack-inline-source-plugin/blob/master/LICENSE
|
||||
|
||||
Modified by Yuxi Evan You
|
||||
*/
|
||||
|
||||
const path = require('path')
|
||||
const slash = require('slash')
|
||||
const sourceMapUrl = require('source-map-url')
|
||||
const escapeRegex = require('escape-string-regexp')
|
||||
|
||||
module.exports = class InlineSourcePlugin {
|
||||
constructor (options = {}) {
|
||||
this.options = options
|
||||
}
|
||||
|
||||
apply (compiler) {
|
||||
// Hook into the html-webpack-plugin processing
|
||||
compiler.plugin('compilation', compilation => {
|
||||
compilation.plugin('html-webpack-plugin-before-html-generation', (htmlPluginData, callback) => {
|
||||
callback(null, htmlPluginData)
|
||||
})
|
||||
compilation.plugin('html-webpack-plugin-alter-asset-tags', (htmlPluginData, callback) => {
|
||||
if (!this.options.include) {
|
||||
return callback(null, htmlPluginData)
|
||||
}
|
||||
const regex = this.options.include
|
||||
const result = this.processTags(compilation, regex, htmlPluginData)
|
||||
callback(null, result)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
processTags (compilation, regex, pluginData) {
|
||||
const processTag = tag => this.processTag(compilation, regex, tag)
|
||||
return {
|
||||
head: pluginData.head.map(processTag),
|
||||
body: pluginData.body.map(processTag),
|
||||
plugin: pluginData.plugin,
|
||||
chunks: pluginData.chunks,
|
||||
outputName: pluginData.outputName
|
||||
}
|
||||
}
|
||||
|
||||
processTag (compilation, regex, tag) {
|
||||
let assetUrl
|
||||
|
||||
// inline js
|
||||
if (tag.tagName === 'script' && regex.test(tag.attributes.src)) {
|
||||
assetUrl = tag.attributes.src
|
||||
tag = {
|
||||
tagName: 'script',
|
||||
closeTag: true,
|
||||
attributes: {
|
||||
type: 'text/javascript'
|
||||
}
|
||||
}
|
||||
|
||||
// inline css
|
||||
} else if (tag.tagName === 'link' && regex.test(tag.attributes.href)) {
|
||||
assetUrl = tag.attributes.href
|
||||
tag = {
|
||||
tagName: 'style',
|
||||
closeTag: true,
|
||||
attributes: {
|
||||
type: 'text/css'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (assetUrl) {
|
||||
// Strip public URL prefix from asset URL to get Webpack asset name
|
||||
const publicUrlPrefix = compilation.outputOptions.publicPath || ''
|
||||
const assetName = path.posix.relative(publicUrlPrefix, assetUrl)
|
||||
const asset = compilation.assets[assetName]
|
||||
// do not emit inlined assets
|
||||
delete compilation.assets[assetName]
|
||||
const updatedSource = this.resolveSourceMaps(compilation, assetName, asset)
|
||||
tag.innerHTML = (tag.tagName === 'script') ? updatedSource.replace(/(<)(\/script>)/g, '\\x3C$2') : updatedSource
|
||||
}
|
||||
|
||||
return tag
|
||||
}
|
||||
|
||||
resolveSourceMaps (compilation, assetName, asset) {
|
||||
let source = asset.source()
|
||||
const out = compilation.outputOptions
|
||||
// Get asset file absolute path
|
||||
const assetPath = path.join(out.path, assetName)
|
||||
// Extract original sourcemap URL from source string
|
||||
if (typeof source !== 'string') {
|
||||
source = source.toString()
|
||||
}
|
||||
const mapUrlOriginal = sourceMapUrl.getFrom(source)
|
||||
// Return unmodified source if map is unspecified, URL-encoded, or already relative to site root
|
||||
if (!mapUrlOriginal || mapUrlOriginal.indexOf('data:') === 0 || mapUrlOriginal.indexOf('/') === 0) {
|
||||
return source
|
||||
}
|
||||
// Figure out sourcemap file path *relative to the asset file path*
|
||||
const assetDir = path.dirname(assetPath)
|
||||
const mapPath = path.join(assetDir, mapUrlOriginal)
|
||||
const mapPathRelative = path.relative(out.path, mapPath)
|
||||
// Starting with Node 6, `path` module throws on `undefined`
|
||||
const publicPath = out.publicPath || ''
|
||||
// Prepend Webpack public URL path to source map relative path
|
||||
// Calling `slash` converts Windows backslashes to forward slashes
|
||||
const mapUrlCorrected = slash(path.join(publicPath, mapPathRelative))
|
||||
// Regex: exact original sourcemap URL, possibly '*/' (for CSS), then EOF, ignoring whitespace
|
||||
const regex = new RegExp(escapeRegex(mapUrlOriginal) + '(\\s*(?:\\*/)?\\s*$)')
|
||||
// Replace sourcemap URL and (if necessary) preserve closing '*/' and whitespace
|
||||
return source.replace(regex, (match, group) => {
|
||||
return mapUrlCorrected + group
|
||||
})
|
||||
}
|
||||
}
|
||||
139
packages/@vue/cli-service/lib/util/PreloadPlugin.js
Normal file
139
packages/@vue/cli-service/lib/util/PreloadPlugin.js
Normal file
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2017 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* Modified by Yuxi Evan You
|
||||
*/
|
||||
|
||||
const flatten = arr => arr.reduce((prev, curr) => prev.concat(curr), [])
|
||||
const getValues = obj => Object.keys(obj).map(key => obj[key])
|
||||
|
||||
const doesChunkBelongToHTML = (chunk, roots, visitedChunks) => {
|
||||
// Prevent circular recursion.
|
||||
// See https://github.com/GoogleChromeLabs/preload-webpack-plugin/issues/49
|
||||
if (visitedChunks[chunk.renderedHash]) {
|
||||
return false
|
||||
}
|
||||
visitedChunks[chunk.renderedHash] = true
|
||||
|
||||
for (const root of roots) {
|
||||
if (root.hash === chunk.renderedHash) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
for (const parent of chunk.parents) {
|
||||
if (doesChunkBelongToHTML(parent, roots, visitedChunks)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
const defaultOptions = {
|
||||
rel: 'preload',
|
||||
include: 'asyncChunks',
|
||||
fileBlacklist: [/\.map/]
|
||||
}
|
||||
|
||||
module.exports = class PreloadPlugin {
|
||||
constructor (options) {
|
||||
this.options = Object.assign({}, defaultOptions, options)
|
||||
}
|
||||
|
||||
apply (compiler) {
|
||||
const options = this.options
|
||||
compiler.plugin('compilation', compilation => {
|
||||
compilation.plugin('html-webpack-plugin-before-html-processing', (htmlPluginData, cb) => {
|
||||
let filesToInclude = ''
|
||||
let extractedChunks = []
|
||||
// 'asyncChunks' are chunks intended for lazy/async loading usually generated as
|
||||
// part of code-splitting with import() or require.ensure(). By default, asyncChunks
|
||||
// get wired up using link rel=preload when using this plugin. This behaviour can be
|
||||
// configured to preload all types of chunks or just prefetch chunks as needed.
|
||||
if (options.include === undefined || options.include === 'asyncChunks') {
|
||||
try {
|
||||
extractedChunks = compilation.chunks.filter(chunk => !chunk.isInitial())
|
||||
} catch (e) {
|
||||
extractedChunks = compilation.chunks
|
||||
}
|
||||
} else if (options.include === 'initial') {
|
||||
try {
|
||||
extractedChunks = compilation.chunks.filter(chunk => chunk.isInitial())
|
||||
} catch (e) {
|
||||
extractedChunks = compilation.chunks
|
||||
}
|
||||
} else if (options.include === 'all') {
|
||||
// Async chunks, vendor chunks, normal chunks.
|
||||
extractedChunks = compilation.chunks
|
||||
} else if (Array.isArray(options.include)) {
|
||||
// Keep only user specified chunks
|
||||
extractedChunks = compilation
|
||||
.chunks
|
||||
.filter((chunk) => {
|
||||
const chunkName = chunk.name
|
||||
// Works only for named chunks
|
||||
if (!chunkName) {
|
||||
return false
|
||||
}
|
||||
return options.include.indexOf(chunkName) > -1
|
||||
})
|
||||
}
|
||||
|
||||
const publicPath = compilation.outputOptions.publicPath || ''
|
||||
|
||||
// Only handle the chunk import by the htmlWebpackPlugin
|
||||
extractedChunks = extractedChunks.filter(chunk => doesChunkBelongToHTML(
|
||||
chunk, getValues(htmlPluginData.assets.chunks), {}))
|
||||
|
||||
flatten(extractedChunks.map(chunk => chunk.files)).filter(entry => {
|
||||
return this.options.fileBlacklist.every(regex => regex.test(entry) === false)
|
||||
}).forEach(entry => {
|
||||
entry = `${publicPath}${entry}`
|
||||
if (options.rel === 'preload') {
|
||||
// If `as` value is not provided in option, dynamically determine the correct
|
||||
// value depends on suffix of filename. Otherwise use the given `as` value.
|
||||
let asValue
|
||||
if (!options.as) {
|
||||
if (entry.match(/\.css$/)) asValue = 'style'
|
||||
else if (entry.match(/\.woff2$/)) asValue = 'font'
|
||||
else asValue = 'script'
|
||||
} else if (typeof options.as === 'function') {
|
||||
asValue = options.as(entry)
|
||||
} else {
|
||||
asValue = options.as
|
||||
}
|
||||
const crossOrigin = asValue === 'font' ? 'crossorigin="crossorigin" ' : ''
|
||||
filesToInclude += `<link rel="${options.rel}" as="${asValue}" ${crossOrigin}href="${entry}">\n`
|
||||
} else {
|
||||
// If preload isn't specified, the only other valid entry is prefetch here
|
||||
// You could specify preconnect but as we're dealing with direct paths to resources
|
||||
// instead of origins that would make less sense.
|
||||
filesToInclude += `<link rel="${options.rel}" href="${entry}">\n`
|
||||
}
|
||||
})
|
||||
if (htmlPluginData.html.indexOf('</head>') !== -1) {
|
||||
// If a valid closing </head> is found, update it to include preload/prefetch tags
|
||||
htmlPluginData.html = htmlPluginData.html.replace('</head>', filesToInclude + '</head>')
|
||||
} else {
|
||||
// Otherwise assume at least a <body> is present and update it to include a new <head>
|
||||
htmlPluginData.html = htmlPluginData.html.replace('<body>', '<head>' + filesToInclude + '</head><body>')
|
||||
}
|
||||
cb(null, htmlPluginData)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@
|
||||
"copy-webpack-plugin": "^4.3.1",
|
||||
"cross-spawn": "^5.1.0",
|
||||
"css-loader": "^0.28.7",
|
||||
"escape-string-regexp": "^1.0.5",
|
||||
"extract-text-webpack-plugin": "^3.0.2",
|
||||
"file-loader": "^1.1.6",
|
||||
"friendly-errors-webpack-plugin": "^1.6.1",
|
||||
@@ -45,6 +46,8 @@
|
||||
"read-pkg": "^3.0.0",
|
||||
"rimraf": "^2.6.2",
|
||||
"semver": "^5.4.1",
|
||||
"slash": "^1.0.0",
|
||||
"source-map-url": "^0.4.0",
|
||||
"string.prototype.padend": "^3.0.0",
|
||||
"uglifyjs-webpack-plugin": "^1.1.4",
|
||||
"url-loader": "^0.6.2",
|
||||
|
||||
Reference in New Issue
Block a user