webpack作为每一个前端人都直接或间接使用的前端模块化打包工具,十分有必要去好好学习。
能干什么(大大提高开发效率和灵活性)
- 让项目支持scss、less、es6、ts等浏览器目前无法处理的功能
- 丰富脚手架功能(如配置gzip,引用cdn资源,抽取公共代码,自定义loader和plugin实现定制化功能)
如何工作
最常用的方式就是在package.json文件的scripts里添加对应的script,去读取webpack.config.js(可以配置别的文件webpack --config ${fliePath})文件,从配置的入口(entry)开始,一层一层的去寻找所需要的依赖,得到一个完整的依赖关系图,这个依赖关系图会包括当前项目所需要的所有模块,然后根据这个关系,去遍历项目,打包一个个模块(使用不同的loader处理不同的文件类型)
webpack的模块化
- CommonJS模块化实现原理
// 定义一个以模块路径为键,模块函数为值的对象
var __webpack_modules__ = {
'${modulePath}': (function (module) {
const hello = (msg) => {
return `hello,${msg}`
}
//...
module.exports = {
hello,
...//
}
})
}
// 定义一个缓存模块的对象
var __webpack_module_cache__ = {};
// 定义一个加载模块的函数
function __webpack_require__(moduleId) {
// 1.判断缓存中是否已经加载过
if (__webpack_module_cache__[moduleId]) {
return __webpack_module_cache__[moduleId].exports;
}
// 2.给module变量和__webpack_module_cache__[moduleId]赋值了同一个对象
var module = __webpack_module_cache__[moduleId] = { exports: {} };
// 3.加载执行模块
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
// 4.导出module.exports
return module.exports;
}
// 具体开始执行代码逻辑
!function () {
// 根据modulePath加载
const { hello, ... } = __webpack_require__("${modulePath}");
console.log(hello("webpack"));
}();
- ES Module实现原理
var __webpack_modules__ = {
"${entryFilePath}": (function (__unused_webpack_module, __webpack_exports__, __webpack_require__) {
// 使用r函数给模块打标签,设置其__esModule标识
__webpack_require__.r(__webpack_exports__);
var _js_hello__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("${modulePath}");
console.log(_js_hello__WEBPACK_IMPORTED_MODULE_0__.hello('webpack'));
}),
"${modulePath}": (function (__unused_webpack_module, __webpack_exports__, __webpack_require__) {
__webpack_require__.r(__webpack_exports__);
// 调用了d函数: 给exports设置了一个代理definition
// exports对象中本身是没有对应的函数
__webpack_require__.d(__webpack_exports__, {
"hello": function () { return hello; },
...
});
const hello = (msg) => {
return `hello,${msg}`
}
...
})
}
var __webpack_module_cache__ = {};
function __webpack_require__(moduleId) {
if (__webpack_module_cache__[moduleId]) {
return __webpack_module_cache__[moduleId].exports;
}
var module = __webpack_module_cache__[moduleId] = {
exports: {}
};
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
return module.exports;
}
!function () {
// __webpack_require__这个函数对象添加了一个属性: d -> 值function
__webpack_require__.d = function (exports, definition) {
for (var key in definition) {
if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
//给exports设置了一个代理,通过传入的definition去获取对应的模块
Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
}
}
};
}();
!function () {
// __webpack_require__这个函数对象添加了一个属性: o -> 值function
__webpack_require__.o = function (obj, prop) {
return Object.prototype.hasOwnProperty.call(obj, prop);
}
}();
!function () {
// __webpack_require__这个函数对象添加了一个属性: r -> 值function
__webpack_require__.r = function (exports) {
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
}();
__webpack_require__("${entryFilePath}");
出口和入口的配置
const path = require('path')
module.exports = {
// entry: "./src/index.js", //配置入口文件,webpack会从该文件开始读取对应的文件进行操作
entry: {
index: "./src/index.js",
main: "./src/main.js"
},
// [https://webpack.js.org/configuration/output/]
output: {
filename: "[name].bundle.js", // 打包生成的文件名
path: path.resolve(__dirname, "./build"), //必须配置绝对路径
publicPath: './', //设置打包后文件的前缀
chunkFilename: "[name].[chunkhash:6].chunk.js"
}
}
模块热替换Hot Module Replacement
HMR(基于webpack-dev-server)可以让我们边修改文件,对应的模块就产生对应的变化,大大提升开发效率。
// ...
module.exports = {
// ...
devServer: {
// 配置参考(https://webpack.js.org/configuration/dev-server/#root)
hot: 'only',
host: '0.0.0.0',
port: 8888,
open: true,
compress: true,
historyApiFallback: true,
proxy: {
//基于http-proxy-middleware实现的代理功能,用于解决开发时请求的跨域问题
'/api': {
target: `${serverUrl}`,
pathRewrite: { '^/api': '' },
secure: false,
changeOrigin: true,
},
}
},
}
模块解析(Resolve)配置
const path = require('path');
module.exports = {
//...
resolve: {
alias: {
'~': path.resolve(__dirname, "./src"),
"pages": path.resolve(__dirname, "./src/pages")
},
extensions: ['.js', '.json', '.jsx', '.ts', '.vue'],
mainFiles: ['index'],
},
};
rule的配置
- test属性:匹配对应的资源,使用正则表达式
- use属性:对应一个数组
[useEntry],useEntry又是一个对象,里面置顶loader和options,use里书写的loader一定要遵从从后往前的顺序书写,webpack是依次从后往前使用loader来解析对应test的资源的
常用Loader(用于转换一些特定的类型模块)
style-loader,css-loader,postcss-loader,less-loader(sass-loader)
这一串loader都是处理样式书写的loader,
module: {
rules: [
{
test: /\.css$/,
use: [
"style-loader", //将css文件插入到页面中
{
loader: "css-loader",
options: {
sourceMap: false,
importLoaders: 2
modules: true,
localIdentName: '[name]_[local]_[hash:base64:5]'
} // 具体配置查看`https://webpack.js.org/loaders/css-loader/#options`
}, //解析css文件
"postcss-loader", // 配合postcss.config.js文件根据browserslist对不同的浏览器处理样式
"less-loader", // 将less文件转换成css文件
]
}
]
}
//postcss.config.js
module.exports = {
plugins: [
'postcss-preset-env'
]
}
file-loader,url-loader,row-loader到Asset Modules
处理文件解析的loader,webpack4主要是file-loader,url-loader,row-loader三个loader的使用。到了webpack5无需安装解析文件的loader,它内部提供了Asset Modules(Asset Modules | webpack)
rules: [
// webpck4
{
test: /\.(png|jpe?g|gif|svg)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 4096,
name: 'img/[name].[hash:8].[ext]'
}
}
]
},
// webpack5
{
test: /\.(png|jpe?g|gif|svg)$/,
type: 'asset',
paser: {
dataUrlCondition: {
maxSize: 4096
}
},
generator: {
filename: 'img/[name].[hash:8][ext]'
}
},
{
test: /\.ttf|eot|woff2?$/i,
type: 'asset/resource',
generator: {
filename: 'font/[name].[hash:8][ext]'
}
},
{
test: /\.svg$/,
type: 'asset/inline', // 导出 data URI的资源
},
]
babel-loader
babel是一个javascript编译器。它能将es6的语法做到向后兼容,这样我们就可以放心的使用js的新特性,不用担心语法转换的问题。
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
// 配置建议使用babel.config.js方式
//option: {
// presets: ["babel/preset-env"]
//}
},
}
]
// babel.config.js
module.exports = {
presets: [
// https://babeljs.io/docs/en/babel-preset-env
["babel/preset-env"],
['@babel/preset-react'],
["@babel/preset-typescript"]
],
plugins: ['@babel/plugin-proposal-class-properties']
}
vue-loader
vue-loader(它不是一个简单的源转换加载器,它会使用自己的专用加载器链处理每个语言块,最后将这些模块组成最终的模块)配合VueLoaderPlugin解析vue文件
// ...
const VueLoaderPlugin = require('vue-loader/lib/plugin');
module.exports = {
// ...
module: {
rules: [
// ...
{
test: /\.vue$/,
use: "vue-loader"
}
]
},
plugins: [
new VueLoaderPlugin()
]
}
常用plugin(丰富webpack的功能,如打包优化、环境变量处理、资源管理等)
- clean-webpack-plugin: 用于删除文件夹
- html-webpack-plugin: 帮助创建HTML文件
- DefinePlugin: 用于定义环境变量(webpack自带)
- copy-webpack-plugin:将已经存在的单个文件或整个目录复制到构建目录。
- MiniCssExtractPlugin:为每个包含css的js文件创建一个单独的css文件
- TerserWebpackPlugin:用于精简js代码体积(生产环境开启)
- CssMinimizerPlugin:用于压缩css代码体积(生产环境开启)
- ModuleConcatenationPlugin(依赖于esModule):webpack自带的提升作用域插件(生产环境开启)
- PurgeCSSPlugin:用于清除未使用的css代码
- CompressionPlugin:对文件进行编码压缩(gzip)
- webpack-bundle-analyzer:分析打包后各个文件体积大小(优化打包使用)
const path = require('path')
const glob = require('glob')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { DefinePlugin } = require('webpack')
const CopyPlugin = require('copy-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const TerserPlugin = require("terser-webpack-plugin")
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const PurgeCssPlugin = require('purgecss-webpack-plugin')
const CompressionPlugin = require('compression-webpack-plugin')
const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer")
const appDir = process.cwd()
const resolveApp = (relativePath) => path.resolve(appDir, relativePath)
module.exports = {
// ...
module: {
rule: [
// ...
{
test: /\.css$/i,
use: [MiniCssExtractPlugin.loader, "css-loader"],
},
]
},
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
test: /\.js(\?.*)?$/i,
parallel: true,
extractComments: false,
terserOptions: {
compress: {
arguments: false,
dead_code: true
},
keep_classnames: true,
keep_fnames: false,
},
}),
],
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'myApp',
template: 'public/index.html'
}),
new DefinePlugin({
BASE_URL: '"/manage/"',
'process.env': {
NODE_ENV: '"development"',
}
}),
new CopyPlugin({
patterns: [
{ from: 'source', to: 'dest' },
{
from: 'public',
globOptions: {
ignore: ['**/index.html', '**/.DS_Store']
}
}
]
}),
new MiniCssExtractPlugin({
filename: "css/[name].[contenthash:6].css"
}),
new CssMinimizerPlugin(),
new webpack.optimize.ModuleConcatenationPlugin(),
new PurgeCssPlugin({
paths: glob.sync(`${resolveApp("./src")}/**/*`, {nodir: true}),
safelist: function() {
return {
standard: ["body", "html"]
}
}
}),
new CompressionPlugin({
test: /\.(css|js)$/i,
algorithm: "gzip",
threshold: 0,
minRatio: 0.8
}),
new BundleAnalyzerPlugin()
],
}
Code Splitting(代码分离)
- 利用多入口方式分离
module.exports = {
entry: {
main: "./src/main.js",
index: "./src/index.js"
},
output: {
path: path.resolve(__dirname, "./build"),
filename: "[name].bundle.js",
},
}
- 利用SplitChunksPlugin分割代码
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
optimization: {
chunkIds: 'named',
splitChunks: {
chunks: 'all',
miniSize: 2000, // 将包拆分的最小大小
maxSize: 3000, // 将大于maxSize的包, 拆分成不小于minSize的包
minChunks: 1,
maxInitialRequests: 30,
cacheGroups: {
// 配置缓存组
vendor: {
test: /[\\/]node_modules[\\/]/,
filename: "js/[id]_vendors.js",
priority: -10
},
default: {
minChunks: 2,
filename: "common_[id].js",
priority: -20
}
}
}
}
}
Tree Shaking
- usedExports:通过标记某些函数是否被使用,之后通过Terser来进行优化
optimization: {
usedExports: true,
// ...
},
- sideEffects:告知webpack某模块是否有副作用
设置CDN优化
- 将所有静态资源放在CDN服务器上
// 设置output的publibPath
publicPath: `${cdnUrl}`
- 一些第三方的资源放在CDN服务器上
// 配置 externals将对应的包抽离出来, 然后在模版html上加入对应的cdn资源(对应的cdn资源可以去官网查找)
module.exports = {
...
externals: {
lodash: "_",
dayjs: "dayjs"
}
}