前言
webpack就是一个函数,这个函数参数包含了一系列的配置,webpack根据这些配置执行查找、编译、转换、合并、压缩等一系列操作。
项目打包配置组成
1、config.js----根据环境区分打包配置
'use strict'
const path = require('path')
const API= ['user', 'proxy']
let proxy = {}
API.forEach(function (api) {
proxy['/' + api] = {
target: 'https://baidu.com',
changeOrigin: true,
pathRewrite: {}
}
proxy['/' + api].pathRewrite['^/' + api] = '/' + api})
// 是否构建生产包
const buildForProduction = process.argv[2].indexOf('prod') >= 0
// 前端灰度目录
const fv = process.argv[3] === 'v0' ? '' : `${process.argv[3]}/`
const output = 'admin'
const getLocalIPv4 = () => {
const networkInterfaces = require('os').networkInterfaces()
for(const name in networkInterfaces){
const networkInterface = networkInterfaces[name]
for(let i = 0; i < networkInterface.length; i++){
const alias = networkInterface[i]
if(alias.family === 'IPv4' && alias.address !== '127.0.0.1'
&& !alias.internal){
return alias.address
}
}
}
return 'localhost'
}
module.exports = {
dev: {
assetsSubDirectory: 'static',
assetsPublicPath: '/',
proxyTable: proxy,
host: 'localhost',
port: 8081,
autoOpenBrowser: false,
errorOverlay: true,
notifyOnErrors: true,
poll: false,
useEslint: true,
showEslintErrorsInOverlay: true,
devtool: 'cheap-module-eval-source-map',
cacheBusting: true,
cssSourceMap: true
},
build: {
// index.html路径
index: path.resolve(__dirname, `../base/${output}/index.html`),
// 资源路径
assetsRoot: path.resolve(__dirname, `../base/${output}`),
assetsSubDirectory: 'static',
assetsPublicPath: buildForProduction ?
`//baidu.com/${fv}base/${output}/` : `//baidu.com/${fv}base/${output}/`,
productionSourceMap: true, // 生产测试都需要打出map文件
devtool: buildForProduction? false : 'eval-source-map',
// 请先安装npm install --save-dev compression-webpack-plugin
productionGzip: true, // 先不压缩文件包为zip
productionGzipExtensions: ['js', 'css'],
needWorkboxSW: true, // 是否需要 workbox 配置 service worker
workboxConfig: null, // workbox 配置 null 表示默认配置
// 分析打包产物:`npm run build --report`
// npm run build --report dev v1 2023090101
bundleAnalyzerReport: process.env.npm_config_report
},
buildForProduction
}
2、build.js----webpack函数调用
'use strict'
require('./check-versions')()
process.env.NODE_ENV = 'production'
const ora = require('ora')
const rm = require('rimraf')
const path = require('path')
const chalk = require('chalk')
const webpack = require('webpack')
const config = require('./config')
const webpackConfig = require('./webpack.prod.conf')
const gulp = require('gulp')
const pump = require('pump')
const inject = require('gulp-inject-string')
// npm run build prod v0 2023090101
const projectVersion = process.argv[3] === 'v0' ? '' : `${process.argv[3]}`
const projectVersionPath = projectVersion === '' ? ''
: '/' + projectVersion
const buildConfig = new Map([
['dev', {
html: `<script src="//baidu.com/assets/sdks/consolex.min.js"></script>`,
ft: process.argv[4] || new Date().getTime()
}],
['prod', {
html: `<!-- ${new Date().toString()} -->`,
ft: process.argv[4] || new Date().getTime()
}]]).get((process.argv[2].split('@'))[0] || 'prod')
const workboxHOST = process.argv[2].indexOf('prod') >= 0
? '//baidu.com'
: '//baidu.com'
const serviceworkerHOST = process.argv[2].indexOf('prod') >= 0
? '/baidu.com'
: '//baidu.com'const output = 'base'// 修改前端目录,v1,v2,v0代表根目录
pump(
gulp.src(path.resolve(__dirname, '../src/router/index.js')),
inject.replace(/base: '\/.*\/*base/, `base: '${projectVersionPath}/admin`),
inject.replace(`base`, output),
gulp.dest(path.resolve(__dirname, '../src/router/')))
pump(
gulp.src(path.resolve(__dirname, '../src/assets/scripts/common/vp.js')),
inject.replace('window.ft = ', `window.ft = '${buildConfig.ft}' // `),
gulp.dest(path.resolve(__dirname, '../src/assets/scripts/common/'))
)
const spinner = ora('building for production...')
spinner.start()
rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
if (err) throw err
webpack(webpackConfig, (err, stats) => {
spinner.stop()
if (err) throw err
process.stdout.write(stats.toString({
colors: true,
modules: false,
children: false,
chunks: false,
chunkModules: false
}) + '\n\n')
if (stats.hasErrors()) {
console.log(chalk.red('Build failed with errors.\n'))
process.exit(1)
}
pump(
gulp.src(path.resolve(__dirname, `../admin/${output}/index.html`)),
inject.replace('<div id=app></div>', `<div id=app></div>${buildConfig.html}`),
inject.replace('service-worker.js', `${serviceworkerHOST}${projectVersionPath}/admin/${output}/service-worker.js?ft=${buildConfig.ft}`),
gulp.dest(path.resolve(__dirname, `../admin/${output}/`))
)
pump(
gulp.src(path.resolve(__dirname, `../admin/${output}/service-worker.js`)),
inject.replace('workbox.core.setCacheNameDetails',
`workbox.setConfig({modulePathPrefix: '${workboxHOST}${projectVersionPath}/assets/sdks/workbox',
debug: ${process.argv[2].indexOf('dev') >= 0}});\nworkbox.core.setCacheNameDetails`),
gulp.dest(path.resolve(__dirname, `../admin/${output}/`))
)
console.log(chalk.cyan(' Build complete.\n'))
console.log(chalk.yellow(
' Tip: built files are meant to be served over an HTTP server.\n' +
' Opening index.html over file:// won\'t work.\n'
))
console.warn(chalk.yellow('-打包成功了-\n'))
})})
3、webpack.base.conf.js----webpack基础配置
'use strict'
const path = require('path')
const utils = require('./utils')
const config = require('../config')
const vueLoaderConfig = require('./vue-loader.conf')
const webpack = require('webpack')
const { getGray } = require('../../assets/scripts/util')
const buildForProduction = process.argv[2].indexOf('prod') >= 0
function resolve(dir) {
return path.join(__dirname, '..', dir)
}
module.exports = {
context: path.resolve(__dirname, '../'),
entry: {
app: ['babel-polyfill', './src/index.js']
},
output: {
path: config.build.assetsRoot,
filename: '[name].js',
publicPath: process.env.NODE_ENV === 'production'
? config.build.assetsPublicPath
: config.dev.assetsPublicPath
},
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src'),
'STATIC': resolve('static')
}
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
options: vueLoaderConfig
},
{
test: /\.js$/,
loader: 'babel-loader',
include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')]
},
{
test: /\.svg$/,
loader: 'svg-sprite-loader',
include: [resolve('src/assets/svg')],
options: {
symbolId: '[name]'
}
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
include: [resolve('src/assets/images/big-page')],
loader: 'url-loader',
options: {
limit: 10,
publicPath: buildForProduction ? 'https://baidu.com' : 'https://baidu.test.com',
name: 'assets/images/projectName/big-page/[name].[ext]'
}
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
exclude: [resolve('src/assets/svg')],
loader: 'url-loader',
options: {
limit: 1024,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('media/[name].[hash:7].[ext]')
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
}
}
]
},
node: {
// prevent webpack from injecting useless setImmediate polyfill because Vue
// source contains it (although only uses it if it's native).
setImmediate: false,
// prevent webpack from injecting mocks to Node native modules
// that does not make sense for the client
dgram: 'empty',
fs: 'empty',
net: 'empty',
tls: 'empty',
child_process: 'empty'
},
plugins: [
new webpack.DefinePlugin({
'PROJECT_GRAY': JSON.stringify(getGray(process.argv[3])),
'TIME_STAMP': new Date().getTime()
})
],
externals: {}
}
4、webpack.prod.conf.js----webpack生产环境配置
'use strict'
const path = require('path')
const utils = require('./utils')
const webpack = require('webpack')
const config = require('../config')
const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.conf')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CleanWebpackPlugin = require('clean-webpack-plugin')
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
const WorkboxPlugin = require('workbox-webpack-plugin')
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin')
// 打包信息展示插件
const reportPlugin = []
let workboxHOST = process.argv[2].indexOf('prod') >= 0 ? '//baidu.com' : '//baidu.test.com'
const workboxPATH = process.argv[3] === 'v0' ? '' : `/${process.argv[3]}`
const output = 'admin'
// 打包分离map插件需要区分生产/测试环境
const buildEnv = process.argv[2].indexOf('prod') >= 0 ? 'production' : 'test'
if (config.build.bundleAnalyzerReport) {
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
reportPlugin.push(new BundleAnalyzerPlugin({ analyzerPort: 2018 }))
}
const webpackConfig = merge(baseWebpackConfig, {
mode: 'production',
module: {
rules: utils.styleLoaders({
sourceMap: config.build.productionSourceMap,
extract: true,
usePostCSS: true
})
},
devtool: config.build.productionSourceMap ? config.build.devtool : false,
output: {
path: config.build.assetsRoot,
filename: utils.assetsPath('js/[name].[chunkhash].js'),
chunkFilename: utils.assetsPath('js/[name].[chunkhash].js')
},
optimization: {
runtimeChunk: {
name: 'manifest'
},
minimizer: [
new UglifyJsPlugin({
cache: true,
parallel: true,
sourceMap: config.build.productionSourceMap,
uglifyOptions: {
warnings: false
}
}),
new OptimizeCSSPlugin({
cssProcessor: require('cssnano'),
canPrint: true,
cssProcessorOptions: config.build.productionSourceMap
? {
parser: require('postcss-safe-parser'),
map: { inline: false },
autoprefixer: true,
// 避免 cssnano 重新计算z-index
safe: true
}
:
{
parser: require('postcss-safe-parser'),
autoprefixer: true,
safe: true
}
})
],
splitChunks: {
chunks: 'all', // initial(初始块)、async(按需加载块)、all(全部块)
minSize: 20000,
minChunks: 1,
maxAsyncRequests: 1024,
maxInitialRequests: 1024,
name: true,
cacheGroups: {
default: false,
vue: {
name: 'vue',
chunks: 'initial', // 表示显示块的范围,有三个可选值:initial(初始块)、async(按需加载块)、all(全部块,默认)
minChunks: 1,
priority: 3,
reuseExistingChunk: false,
test: /vue/
},
router: {
name: 'router',
chunks: 'initial',
minChunks: 1,
priority: 3,
reuseExistingChunk: false,
test: /router|service/
},
vendorCore: {
name: 'vendor-core',
chunks: 'initial',
minChunks: 1,
priority: 3,
reuseExistingChunk: true,
test: /core-js/
},
vendor: {
name: 'vendor',
chunks: 'initial',
minChunks: 1,
priority: 2,
reuseExistingChunk: true,
test: /[\\/]node_modules[\\/]/
},
assets: {
name: 'assets',
chunks: 'all',
minChunks: 2,
priority: 3,
reuseExistingChunk: true,
test: path.resolve('src/assets')
},
swiper: {
name: 'swiper',
chunks: 'initial',
minChunks: 1,
priority: 3,
reuseExistingChunk: true,
test: /swiper|bscroll/i
},
components: {
name: 'components',
chunks: 'all',
minChunks: 2,
priority: 3,
reuseExistingChunk: true,
test: path.resolve('src/components')
},
styles: {
name: 'styles',
chunks: 'all',
minChunks: 2,
reuseExistingChunk: true,
enforce: true,
test: /\.(styl|scss|css)$/
}
}
}
},
plugins: [
new CleanWebpackPlugin([`${output}`], { root: path.resolve('') }),
// http://vuejs.github.io/vue-loader/en/workflow/production.html
new MiniCssExtractPlugin({
filename: utils.assetsPath('css/[name].[contenthash:12].css'),
allChunks: true,
ignoreOrder: true // Enable to remove warnings about conflicting order
}),
// generate dist index.html with correct asset hash for caching.
// you can customize output by editing /index.html
// see https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: config.build.index,
template: 'index.html',
inject: true,
minify: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true
// more options:
// https://github.com/kangax/html-minifier#options-quick-reference
},
// necessary to consistently work with multiple chunks via CommonsChunkPlugin
chunksSortMode: 'dependency',
isProduction: config.buildForProduction
}),
// keep module.id stable when vendor modules does not change
new webpack.HashedModuleIdsPlugin(),
// enable scope hoisting
new webpack.optimize.ModuleConcatenationPlugin(),
// copy custom static assets
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../static'),
to: config.build.assetsSubDirectory,
ignore: ['.*']
}
]),
new WorkboxPlugin.GenerateSW({
mode: 'production',
skipWaiting: true,
clientsClaim: true,
cleanupOutdatedCaches: true, // 清理过期缓存
swDest: 'service-worker.js', // 输出 Service worker 文件
navigateFallback: `${workboxHOST}${workboxPATH}/admin/index.html`, // 当路由匹配到一个未缓存的页面时重定向的路由
navigateFallbackAllowlist: [/\/admin(\/[\w-]*)+(\?.*)?$/i], // 未缓存页面重定向路由适用范围,白名单
navigateFallbackDenylist: [/^[^?]+\.(css|js|png|gif|jpg|eot|svg|ttf|woff|woff2|json|html)(\?.+)?$/i],
cacheId: 'admin',
maximumFileSizeToCacheInBytes: 3 * 1024 * 1024, // 3M
exclude: [/index\.html/i, /\.map/i, /(assets|static)\/.*/], // 排除 html (webpack 生成的为 cdn 地址,所以需要排除)、map 文件
additionalManifestEntries: [{
url: `${workboxHOST}${workboxPATH}/admin/index.html`,
revision: `${new Date().getTime()}`
}], // 补充预缓存 index.html 文件
runtimeCaching: [
{
// 缓存assets目录下JS资源
urlPattern: /^(https:)?\/\/(baidu)\.com\/assets\/.+\.min\.js(\?.+)?$/i,
handler: 'StaleWhileRevalidate',
options: {
cacheName: 'admin-assets-js',
cacheableResponse: {
statuses: [0, 200]
}
}
},
{
// 缓存assets目录下json资源
urlPattern: /^(https:)?\/\/(baidu)\.com\/assets\/.+\.json(\?.+)?$/i,
handler: 'NetworkFirst',
options: {
cacheName: 'admin-assets-json',
networkTimeoutSeconds: 2,
cacheableResponse: {
statuses: [0, 200]
}
}
},
{
// 缓存strapi接口
urlPattern: /^(https:)?\/\/(www|baidu)\.com\/(strapi|uploads)\/.+(\?.+)?$/i,
handler: 'NetworkFirst',
options: {
cacheName: 'admin-strapi',
networkTimeoutSeconds: 2,
cacheableResponse: {
statuses: [200]
}
}
},
{
// 项目下的 图片、字体、css、js 等资源
urlPattern: /^(https:)?\/\/(baidu)\.com\/v(\-)?\d+\/admin\/static(\/[\w-]+)*\/(\w|\-|\~)+\.\w+\.(css|js|png|gif|jpg|eot|svg|ttf|woff|woff2)(\?.+)?$/i,
handler: 'NetworkFirst',
options: {
cacheName: 'admin-cache-static',
cacheableResponse: {
statuses: [0, 200]
},
expiration: {
maxEntries: 120, // 缓存最大数量
maxAgeSeconds: 7 * 24 * 60 * 60, // 缓存过期时间为 7 天
}
}
},
{ // 项目下的libs资源
urlPattern: /\/admin\/static\/libs\/[\.\w\-\~]+?\.js(\?.+)?$/i,
handler: 'StaleWhileRevalidate',
options: {
cacheName: 'admin-cache2-static',
cacheableResponse: {
statuses: [200]
}
}
},
{
// 业务相关图片
urlPattern: /(https?:)?\/\/baidu.com(:\d+)?.*\.(jpg|png|jpeg|gif)\??.*$/,
handler: 'NetworkFirst',
options: {
cacheName: 'admin-external-static',
cacheableResponse: {
statuses: [0, 200]
},
expiration: {
maxEntries: 120, // 缓存最大数量
maxAgeSeconds: 7 * 24 * 60 * 60, // 缓存过期时间为 7 天
}
}
},
{ // 其它 css、js、html、json等资源
urlPattern: /\/(\w|\d|-)+\.(css|js|json|html)(\?.+)?$/i,
handler: 'NetworkFirst',
options: {
cacheName: 'admin-net-static',
networkTimeoutSeconds: 2,
cacheableResponse: {
statuses: [200]
}
}
},
{ // 其它 图片、字体等资源
urlPattern: /\/(\w|\d|-)+\.(png|gif|jpg|eot|svg|ttf|woff|woff2)(\?.+)?$/i,
handler: 'CacheFirst',
options: {
cacheName: 'admin-cache-static',
expiration: {
maxEntries: 100, // 缓存最大数量
maxAgeSeconds: 7 * 24 * 60 * 60, // 缓存过期时间为 7 天
},
}
}
]
}),
new HardSourceWebpackPlugin({
configHash: function(webpackConfig) {
return require('node-object-hash')({sort: false}).hash(webpackConfig);
},
environmentHash: {
root: process.cwd(),
directories: [],
files: ['package-lock.json', 'yarn.lock'],
},
//自动清除缓存
cachePrune: {
//缓存最长时间(默认7天)
maxAge: 7 * 24 * 60 * 60 * 1000
}
}),
...reportPlugin
]})
if (config.build.productionGzip) {
const CompressionWebpackPlugin = require('compression-webpack-plugin')
webpackConfig.plugins.push(
new CompressionWebpackPlugin({
asset: '[path].gz[query]',
algorithm: 'gzip',
test: new RegExp('\\.(' + config.build.productionGzipExtensions.join('|') + ')$'),
threshold: 10240,
minRatio: 0.8
})
)}
module.exports = webpackConfig
参考:
详解webpack4之splitchunksPlugin代码包分拆
从零开始配置webpack(基于webpack 4 和 babel 7版本)
webpack性能优化(2):splitChunks用法详解