添加prefetch
const assert = require('assert')
class PrefetchWebpackPluginPlugin {
constructor (options) {
const defaultOptions = {
/*
* asyncChunks
* initial
* allChunks
* allAssets
* */
include: 'asyncChunks',
/*
* default exclude sourcemap files
* */
fileBlacklist: [/\.map/]
}
this.options = Object.assign({}, defaultOptions, options)
}
extractChunks ({ compilation, optionsInclude }) {
try {
if (optionsInclude === undefined || optionsInclude === 'asyncChunks') {
return compilation.chunks.filter(chunk => {
if ('canBeInitial' in chunk) {
return !chunk.canBeInitial()
} else {
return !chunk.isInitial()
}
})
}
if (optionsInclude === 'initial') {
return compilation.chunks.filter(chunk => {
if ('canBeInitial' in chunk) {
return chunk.canBeInitial()
} else {
return chunk.isInitial()
}
})
}
if (optionsInclude === 'allChunks') {
// Async chunks, vendor chunks, normal chunks.
return compilation.chunks
}
if (optionsInclude === 'allAssets') {
// Every asset, regardless of which chunk it's in.
return [{ files: Object.keys(compilation.assets) }]
}
if (Array.isArray(optionsInclude)) {
// Keep only user specified chunks.
return compilation.chunks.filter((chunk) => chunk.name && optionsInclude.includes(chunk.name))
}
} catch (error) {
return compilation.chunks
}
throw new Error(`The 'include' option isn't set to a recognized value: ${optionsInclude}`)
}
insertLinksIntoHead ({ html, links = [] }) {
if (links.length === 0) {
return html
}
const prefetchLinks = links.map(item => `'<link rel="prefetch" href="'+ASSETS_HOST+'${item}'+'">'`).join('+')
const prefetchScript = `<script>document.write(${prefetchLinks});</script>`
if (html.includes('</head>')) {
return html.replace('</head>', `${prefetchScript}</head>`)
}
throw new Error(`The HTML provided did not contain a </head> or a <body>:\n\n${html}`)
}
addLinks (compilation, htmlPluginData) {
const options = this.options
const extractedChunks = this.extractChunks({
compilation,
optionsInclude: options.include
})
// Flatten the list of files.
const allFiles = extractedChunks.reduce((accumulated, chunk) => {
return accumulated.concat(chunk.files)
}, [])
const uniqueFiles = new Set(allFiles)
const filteredFiles = [...uniqueFiles].filter(file => {
return (
!this.options.fileWhitelist ||
this.options.fileWhitelist.some(regex => regex.test(file))
)
}).filter(file => {
return (
!this.options.fileBlacklist ||
this.options.fileBlacklist.every(regex => !regex.test(file))
)
})
// Sort to ensure the output is predictable.
const sortedFilteredFiles = filteredFiles.sort()
const links = []
const publicPath = compilation.outputOptions.publicPath || ''
for (const file of sortedFilteredFiles) {
links.push(`${publicPath}${file}`)
}
htmlPluginData.html = this.insertLinksIntoHead({
links,
html: htmlPluginData.html
})
return htmlPluginData
}
apply (compiler) {
compiler.hooks.compilation.tap(
this.constructor.name,
compilation => {
// This is set in html-webpack-plugin pre-v4.
let hook = compilation.hooks.htmlWebpackPluginAfterHtmlProcessing
if (!hook) {
const [HtmlWebpackPlugin] = compiler.options.plugins.filter(
(plugin) => plugin.constructor.name === 'HtmlWebpackPlugin')
assert(HtmlWebpackPlugin, 'Unable to find an instance of ' +
'HtmlWebpackPlugin in the current compilation.')
hook = HtmlWebpackPlugin.constructor.getHooks(compilation).beforeEmit
}
hook.tapAsync(
this.constructor.name,
(htmlPluginData, callback) => {
try {
callback(null, this.addLinks(compilation, htmlPluginData))
} catch (error) {
callback(error)
}
}
)
}
)
}
}
module.exports = PrefetchWebpackPluginPlugin
文件大小检测
const fs = require('fs-extra')
const path = require('path')
const logger = require('../utils/logger')
const { formatSize } = require('../utils')
/**
* @class SizeChecker
* 根据类型和阈值在编译完成后警告
* 接受{limit,typeList}参数
* @param limit:number 报警的文件大小阈值(上限),单位b
* @param typeList:array 检测的文件类型组成的数组
*/
class FileSizePlugin {
constructor (options) {
const { appName, appRoot, limit = 100 * 1024, typeList = [], ignoreList = [] } = options
this.appName = appName
this.limit = limit
this.typeList = typeList
this.ignoreList = ignoreList.map(item => { return { filepath: path.resolve(appRoot, item) } })
this.exceedList = []
}
/**
* 两个数组的差集
* @param {array} arr1
* @param {array} arr2
* @return {array}
*/
getDifference (arr1, arr2) {
return arr1.filter(v1 => arr2.findIndex(v2 => v1.filepath === v2.filepath) === -1)
}
/**
* 两个数组的交集
* @param {array} arr1
* @param {array} arr2
* @return {array}
*/
getIntersection (arr1, arr2) {
return arr1.filter(v1 => arr2.findIndex(v2 => v1.filepath === v2.filepath) !== -1)
}
apply (compiler) {
compiler.hooks.emit.tap('FileSizePlugin', compilation => {
// 检索每个(构建输出的)chunk
compilation.chunks.forEach(chunk => {
// 检索 chunk 中(内置输入的)的每个模块:
chunk.getModules().forEach(module => {
// 检索模块中包含的每个源文件路径
module.buildInfo && module.buildInfo.fileDependencies && module.buildInfo.fileDependencies.forEach(filepath => {
const reg = new RegExp(`.(${this.typeList.join('|')})$`)
if (reg.test(filepath)) {
const size = fs.statSync(filepath).size
if (size > this.limit) {
this.exceedList.push({ filepath, size })
}
}
})
})
})
if (this.exceedList.length > 0) {
// 在ignore list列出,成功被忽略的
const exceedAndIgnoreList = this.getIntersection(this.exceedList, this.ignoreList)
if (exceedAndIgnoreList.length > 0) {
console.log('\n')
const maxLength = Math.max(...exceedAndIgnoreList.map(v => v.filepath.length))
logger.done('以下文件已被忽略大小检查:')
exceedAndIgnoreList.forEach(v => {
logger.done(`- ${v.filepath.padEnd(maxLength + 4)} ${formatSize(v.size)}`)
})
}
// 项目没有用到的,但是在ignore list里列出
const notUsedButInWhiteList = this.getDifference(this.ignoreList, this.exceedList)
if (notUsedButInWhiteList.length > 0) {
console.log('\n')
logger.warn('以下文件未被使用或未超出限制:')
notUsedButInWhiteList.forEach(v => {
logger.warn(`- ${v.filepath}`)
})
}
// 超出限定大小,并且没有在ignore list列出
const notInIgnoreList = this.getDifference(this.exceedList, this.ignoreList)
if (notInIgnoreList.length > 0) {
const maxLength = Math.max(...notInIgnoreList.map(v => v.filepath.length))
console.log('\n')
logger.error(`以下文件超出限额 (${formatSize(this.limit)}):`)
notInIgnoreList.forEach(v => {
logger.error(`- ${v.filepath.padEnd(maxLength + 4)} ${formatSize(v.size)}`)
})
logger.error(`应用 [${this.appName}] 构建失败,请仔细检查图片或视频文件大小.`)
return process.exit(1)
}
} else if (this.ignoreList.length) {
console.log('\n')
logger.warn('以下文件未被使用或未超出限制:')
this.ignoreList.forEach(v => {
logger.warn(`- ${v.filepath}`)
})
}
})
}
}
module.exports = FileSizePlugin
...,
// 校验图片大小
new FileSizePlugin({
appName,
appRoot,
limit: 200 * 1024,
typeList: ['png', 'jpg', 'jpeg', 'gif', 'webp'],
ignoreList: appConfig.imagesIgnoreSizeList || []
}),
// 校验视频大小
new FileSizePlugin({
appName,
appRoot,
limit: 500 * 1024,
typeList: ['mp4', 'ogg', 'webm'],
ignoreList: appConfig.videosIgnoreSizeList || []
})