本文主要是多入口配置,希望能在无框架开发网页时提高开发效率,对代码进行打包优化。本文有什么需要改善的地方,还望各位多多指教。
本文关键词:
- babel7
- 多入口
- sass
- 图片处理
- 音视频处理
- 字体处理
- gzip
模块总览
目录结构大概如下:
|-build
|-create.js
|-utils.js
|-webpack.base.js
|-webpack.dev.js
|-webpack.prod.js
|-dist
|-src
|-.babelrc
|-.eslintrc.js
|-package.json
// webpack.base.js
const webpack = require('webpack')
const PurgecssPlugin = require('purgecss-webpack-plugin')
const rules = require('./webpack.rules.js')
const utils = require('./utils.js')
module.exports = {
entry: {},
resolve: {},
module: {},
externals: {},
plugins: []
}
// webpack.prod.js
const webpack = require('webpack')
const merge = require('webpack-merge')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CompressionPlugin = require('compression-webpack-plugin')
const configBase = require('./webpack.base.js')
const utils = require('./utils.js')
const configProd = {
mode: 'production',
devtool: 'none',
output: {},
optimization: {},
plugins: []
}
module.exports = merge(configBase, configProd)
// webpack.dev.js
const webpack = require('webpack')
const merge = require('webpack-merge')
const utils = require('./utils.js')
const configBase = require('./webpack.base.js')
const configDev = {
mode: 'development'
output: {},
devServer: {},
plugins: [],
module: {}
}
module.exports = merge(configBase, configDev)
// webpack.rules.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const devMode = process.env.NODE_ENV !== 'production'
const rules = []
module.exports = rules
后文省略 module.exports
等代码,不再赘述。
配置入口 entry
入口告诉 webapck 从哪个模块开始,根据依赖关系打包
- 单入口
entry: './src/index.js'
- 多入口
entry: {
index: './src/index/index.js'
}
对于多入口配置,可以用 glob 库来动态获取入口文件,如下:
// utils.js
const path = require('path')
const htmlWebpackPlugin = require('html-webpack-plugin')
const glob = require('glob') // 遍历目录
const devMode = process.env.NODE_ENV !== 'production'
/**
* 返回文件的绝对路径
* @param {string} dir 文件路径
* __dirname 获得当前执行文件所在目录的完整目录名(这里指的是 build 目录)
*/
function resolve(dir) {
return path.resolve(__dirname, dir)
}
//动态添加入口
function getEntry(globPath) {
var dirname, name
return glob.sync(globPath).reduce((acc, entry) => {
// name ./src/pages/index/index.js
// dirname ./src/pages/index
// basename index.js
dirname = path.dirname(entry)
name = dirname.slice(dirname.lastIndexOf('/') + 1)
acc[name] = entry
return acc
}, {})
}
function htmlPlugins() {}
module.exports = {
resolve,
getEntry,
htmlPlugins
}
// webpack.base.js
entry: utils.getEntry('./src/pages/*/index.js'),
配置 clean-webpack-plugin
在配置 output 之前配置这个插件是为,每次打包前可以删除 dist 目录,保证没有冗余文件。
// webpack.prod.js
const cleanWebpackPlugin = require('clean-webpack-plugin')
plugins: [
// 删除 dist 目录
new CleanWebpackPlugin({
// verbose Write logs to console.
verbose: false, //开启在控制台输出信息
// dry Use boolean "true" to test/emulate delete. (will not remove files).
// Default: false - remove files
dry: false
}),
]
配置出口 output
自定义输出文件的位置和名称
// webpack.dev.js
output: {
path: utils.resolve('../dist'),
// 包名称
filename: 'js/[name].js'
},
// webpack.prod.js
output: {
path: utils.resolve('../dist'),
// 包名称
filename: 'js/[name].[chunkhash:8].js',
// 块名,公共块名(非入口)
chunkFilename: 'js/[name].[chunkhash:8].js',
// 打包生成的 index.html 文件里面引用资源的前缀
// 也为发布到线上资源的 URL 前缀
// 使用的是相对路径,默认为 ''
publicPath: '.'
},
hash
文件名加入 hash,是为了更好的利用浏览器对静态文件的缓存。
- hash
即使文件内容没有改变,每次构建都产生一个新的哈希值,这显然不是我们想看到的。可以用在开发环境,但不建议用于生产环境。
- chunkhash
每个入口都有对应的哈希值,当入口依赖关系中有文件内容发生变化,该入口的哈希值才会发生变化。适用于生产环境。
- contenthash
根据包内容计算出哈希值,只要包内容不变,哈希值不变。适用于生产环境。
关于这三者的区别,网上也有相关文章,例如我查到的一篇 《webpack 中的 hash、chunkhash、contenthash 区别》 可以参考。
配置模式 mode
none、development、production,默认为 production
// webpack.prod.js
mode: 'production'
// webpack.dev.js
mode: 'development'
webpack4 针对不同模式,调用内置的优化策略,可以减少很多配置。参考 webpack 模式
配置解析策略 resolve
// webpack.base.js
resolve: {
// import 导入时别名,减少耗时的递归解析操作
alias: {
'@': resolve('../src'),
'assets': utils.resolve('../src/assets')
},
extensions: [
'.js',
'.json'
]
}
配置解析和转换文件的规则 module
给项目中不同的文件类型,配置相应的规则
// webpack.base.js
module: {
// 忽略大型的 library 可以提高构建性能
noParse: /jquery|lodash/,
rules: []
}
- js 解析规则
// webpack.rules.js
rules: [
{
test: /\.js$/,
use: ['babel-loader'],
// 不检查 node_modules 下的 js 文件
exclude: '/node_modules/'
}
]
// .babelrc
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": 3,
"modules": false
}
]
]
}
// pages/index/index.js
import 'core-js/stable'
import 'regenerator-runtime/runtime'
根据官网 Usage Guide 配置如上,这里采用的是 core-js@3 来实现 polyfill。因为 babel7 已经废弃 @babel/polyfill 和 core-js@2,不再更新。新的特性只会添加到 core-js@3,为了避免后续再改动,直接用 3。只是打出来的包大了点,这个自己平衡,如果觉得不爽,就还是用 @babel/polyfill。
关于这个 core-js@3 有篇文章 讲的挺清晰,可以参考。
- sass 解析规则
// webpack.rules.js
rules: [
{
test: /\.s[ac]ss$/i,
use: [
devMode
? 'style-loader'
: {
loader: MiniCssExtractPlugin.loader,
options: {
// you can specify a publicPath here
// by default it use publicPath in webpackOptions.output
publicPath: '../'
}
},
'css-loader',
'postcss-loader',
'sass-loader'
]
}
]
// webpack.prod.js
plugins: [
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css',
chunkFilename: 'css/[name].[contenthash:8].css'
}),
]
- html
// webpack.base.js
plugins: [...utils.htmlPlugins('./src/pages/*/index.html')]
// utils.js
function htmlPlugins(globPath) {
var dirname, name
return glob.sync(globPath).reduce((acc, entry) => {
dirname = path.dirname(entry)
name = dirname.slice(dirname.lastIndexOf('/') + 1)
acc.push(new htmlWebpackPlugin(htmlConfig(name, name)))
return acc
}, [])
}
function htmlConfig(name, chunks) {
return {
template: `./src/pages/${name}/index.html`,
filename: `${name}.html`,
// favicon: './favicon.ico',
// title: title,
inject: true,
chunks: [chunks],
minify: devMode
? false
: {
removeComments: true,
collapseWhitespace: true
}
}
}
- 图片
// webpack.rules.js
rules: [
{
test: /\.(png|jpe?g|gif)(\?.*)?$/,
use: [
{
loader: 'url-loader',
options: {
esModule: false,
limit: 4 * 1024,
name: 'img/[name].[hash:8].[ext]'
}
},
{
loader: 'img-loader',
options: {
plugins: [
require('imagemin-pngquant')({
speed: 2 // 1-11
}),
require('imagemin-mozjpeg')({
quality: 80 // 1-100
}),
require('imagemin-gifsicle')({
optimizationLevel: 1 // 1,2,3
})
]
}
}
]
},
{
test: /\.(svg)(\?.*)?$/,
use: [
{
loader: 'url-loader',
options: {
name: 'img/[name].[hash:8].[ext]'
}
}
]
},
]
用法:
background: url(~assets/index/icons/ic-star-16px.png);
import wukong from 'assets/index/wukong.jpg'
这里有有几个点要注意:
- url-loader 和 file-loader
如果配置了 limit,那么小于这个 limit 值的图片会被 url-loader 转换成 base64,超过的图片直接用 file-loader 处理。所以虽然规则里没出现 file-loader,但还是要安装。 - html-loader
用于处理 html 文件,这里主要是处理 html 文件里的图片。图片会通过 url-loader 处理,处理完再给图片 src 设置正确的路径或 base64。而 html 中要使用 webpack 的 alias 配置,需要在前面加上 ~,然后 url-loader 要配置esModule: false
才不会出错。
- 音视频和字体
// webpack.rules.js
rules: [
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 4 * 1024,
name: '[name].[hash:8].[ext]',
outputPath: 'media'
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 4 * 1024,
name: '[name].[hash:8].[ext]',
outputPath: 'font'
}
}
]
devserver
// webpack.dev.js
devServer: {
contentBase: utils.resolve('../src'), // 告诉服务器从哪个目录中提供内容
publicPath: '/', // 此路径下的打包文件可在浏览器中访问
port: '8090',
overlay: true, // 浏览器页面上显示错误
open: true, // 自动打开浏览器
// stats: "errors-only", //stats: "errors-only"表示只打印错误:
historyApiFallback: false, // 404 会被替代为 index.html
inline: true, // 内联模式,实时刷新
hot: true, // 开启热更新
proxy: {
'/api': {
target: 'https://example.com/',
changeOrigin: true,
pathRewrite: {}
}
}
},
plugins: [
//热更新
new webpack.HotModuleReplacementPlugin()
],
- 打包后文件的内存路径 = devServer.contentBase + output.publicPath + output.filename,只能通过浏览器来访问这个路由来访问内存中的 bundle
- 对于 publicPath,有两个用处:
- 像以上的被 webpack-dev-server 作为在内存中的输出目录。
- 被其他的 loader 插件所读取,修改 url 地址等。
devtool
// webpack.dev.js
devtool: 'cheap-eval-source-map',
// webpack.prod.js
devtool: 'none',
此选项控制是否生成,以及如何生成 source map。不同选项之间,官网 有更详细解释和对比。
optimization
// webpack.prod.js
optimization: {
runtimeChunk: {
name: 'manifest'
},
splitChunks: {
cacheGroups: {
vendors: {
name: 'vendors',
test: /[\\\/]node_modules[\\\/]/,
priority: -10,
chunks: 'initial' // 只对入口文件处理
},
vendors: {
name: 'chunk-common',
minChunks: 2,
priority: -20,
chunks: 'initial',
reuseExistingChunk: true
}
}
}
},
runtimeChunk 和 splitChunks 主要优化的点在于浏览器缓存,如果不考虑,也可以不加这个配置。
externals
// webpack.base.js
externals: {
'jquery': 'window.jquery'
},
作用:防止将某些 import 的包 (package) 打包到 bundle 中,而是在运行时 (runtime) 再去从外部获取这些扩展依赖。
没加 externals 配置,jq 通过 cdn 加载,直接在本地使用 $('#id')
打包没什么问题。但是,如果你在本地使用了模块化的 jq 插件,就加上面这个 externals 配置了。原因如下:
;(function(window, factory) {
if (typeof exports === 'object') {
module.exports = factory(require('jQuery'))
} else if (typeof define === 'function' && define.amd) {
define(['jQuery'], factory)
} else {
factory()
}
})(window, function($) {
$.fn.green = function() {
$(this).each(function() {
$(this).css('color', 'green')
})
}
})
上面的代码是一个简单 jq 插件,采用了 UMD 模块化方案。if (typeof exports === 'object')
这行代码会被 webpack 解析为 if (true)
,也就是说,webpack 编译后的代码,会执行 require('jquery')
,而本地并没有安装 jq,所以会报错,无法打包成功。
ProvidePlugin
plugins: [
// 自动加载模块,无需 import 或 require
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
'window.jQuery': 'jquery'
}),
]