preload/prefetch + inline manifest

This commit is contained in:
Evan You
2018-01-08 23:35:53 -05:00
parent 2aad5b5f16
commit 01ce10733b
6 changed files with 291 additions and 0 deletions

View File

@@ -1,3 +1,4 @@
node_modules
template
packages/test
temp

View File

@@ -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()])

View File

@@ -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

View 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
})
}
}

View 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)
})
})
}
}

View File

@@ -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",