webpack5使用笔记

103 阅读15分钟

webpack 安装,入门上手

安装 webpack 、 webpack-cli

npm install webpack webpack-cli

使用 npx 模块进行打包,webpack 会默认用 src/index.js 作为入口打包,输出 build 目录

npx webpack

webpack 配置文件

如果项目根目录创建了 webpack.config.js 文件,webpack 会默认使用此文件做为打包配置

image.png

如果要指定其他配置文件,可通过创建快捷命令传入 config 参数指定文件

image.png

配置文件,导出一个对象

module.exports = {
  entry: './src/index.js',//入口文件
  output: {//输出文件配置
    filename: 'build.js',//输出文件名
    path: path.resolve(__dirname, 'dist')//输出文件路径
  }
}

css-loader、style-loader、less-loader 使用

在使用 loader 之前,我们先了解清楚,为什么要使用 loader 和 loader 是什么? webpack 官网解释的很清楚

webpack 只能理解 JavaScript 和 JSON 文件,这是 webpack 开箱可用的自带能力。loader 让 webpack 能够去处理其他类型的文件,并将它们转换为有效 模块,以供应用程序使用,以及被添加到依赖图中

loader 可以通过行内 loader 和配置文件 loader 使用,其中行内 loader 的使用,只会对使用模块的文件进行处理,从而可以做定制化处理

import 'css-loader!../css/login.css'

配置文件的 loader 使用,可以是多种写法,也可以通过 use 传参进行配置

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist')
  },
  module: {
    rules: [
      // {
      //   test: /\.css$/, // 一般就是一个正则表达式,用于匹配我们需要处理的文件类型
      //   use: [
      //     {
      //       loader: 'css-loader'
      //     }
      //   ]
      // },
      // {
      //   test: /\.css$/,
      //   loader: 'css-loader'
      // },
      {
        test: /\.css$/,
        use: ['css-loader']
      }
    ]
  }
}

css-loader 只是处理 css 文件,如果需要展示在页面中还需要使用 style-loader,loader 处理是从右到左进行一次处理的, 如果是其他类型样式文件,需要使用对应 loader 处理完成之后再交给 css-loader 处理,比如 less

module: {
    rules: [
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      },
      {
        test: /\.less$/,
        use: ['style-loader', 'css-loader', 'less-loader']
      }
    ]
  }

browserslistrc

.browserslistrc 前端工程化通过 babel/postcss 等工具实现兼容 JS、CSS,利用条件兼容浏览器平台(通过caniuse.com 查看市场主流浏览器兼容平台),而 browserslistrc 工作原理也是通过 browserslistrc 配置查找 node_modules 里 browserslistrc 包导入的 caniuse 模块对应浏览器平台

image.png

> 1% 
last 2 version
not dead

> 1% 表示要匹配到市面占有率1%的浏览器平台,通过 Browser usage table 查看浏览器市场占有率

last 2 version 表示某一个平台最后最新的两个版本

dead 表示已经"死亡"的浏览器版本,即多久时间没有更新

image.png

通过 npx browserslistrc 查看配置文件返回的浏览器平台

image.png

postcss、postcss-loader

postcss 是 javascript 转换样式的工具,autoprefixer.github.io 可以查看对应市场占有率平台兼容的 CSS 前缀样式,或者通过 autoprefixer 和 postcss-cli 插件完成 browserslist 配置下的浏览器平台兼容指定样式文件

npx postcss --use autoprefixer -o [输出文件] [兼容文件]

postcss-loader 应该在加完前缀之后再交给 css-loader 处理,而 postcss-loader 也需要通过 autoprefixer 进行加前缀的操作。postcss 也可以通过集成预设插件 postcss-preset-env 处理,postcss-preset-env 集成了 autoprefixer,那么我们就不需要安装 autoprefixer 了,直接使用 postcss-preset-env 就行

module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader',
          {
              loader: 'postcss-loader',
              options:{
                  postcssOptions: {
                      plugins: ['postcss-preset-env']
                  }
              }
          }
        ]
      },
      {
        test: /\.less$/,
        use: [
          'style-loader',
          'css-loader',
          {
              loader: 'postcss-loader',
              options:{
                  postcssOptions: {
                      plugins: ['postcss-preset-env']
                  }
              }
          },
          'less-loader'
        ]
      }
    ]
  }

熟悉 babel 的同学应该知道 babel 的预设可以通过设置文件 babel.config.js 实现,那么 postcss 当然也可以通过设置 postcss.config.js 实现啦

image.png

image.png

那么 webpack 中的 postcss 配置我们就可以简写成这样了,webpack 会自动去查找 postcss.config.js 文件进行相应的配置

module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader',
          'postcss-loader'
      },
      {
        test: /\.less$/,
        use: [
          'style-loader',
          'css-loader',
          'postcss-loader',
          'less-loader'
        ]
      }
    ]
  }

这么做之后还有一个问题,如果入口样式文件中使用 @import 导入了另外一个样式文件,postcss-loader 基于筛选条件并不会对其做额外的处理,只会原封不动的交给 css-loader , 而 css-loader 拿到样式文件后,并不会回头再交给 postcss-loader 处理,只会往下交给 style-loader 了,因此我们需要给 css-loader 添加配置 importLoaders,他的值表示需要往前几个 loader 处理

{
    test: /\.css$/,
    use: [
      'style-loader',
      {
        loader: 'css-loader',
        options: {
          importLoaders: 1
        }
      },
      'postcss-loader'
    ]
  }

file-loader

file-loader 是用来解析图片路径的,当模块导入了图片,webpack5当中使用 file-loader 是默认的 ESModule 模式,所以当用 require 导入模块时,要加上 default 属性,或者通过配置 esModule:false,又或者使用 ESModule 模式导入图片

当样式文件中使用背景图片时,样式文件会交给 css-loader 处理,css-loader 在遇到 background: url 时,会默认处理成 require 导入,图片处理成了 ESModule 模式,图片加载不出来,所以在 css-loader 配置中,我们也需要配置 esModule:false 选项

module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          {
            loader: 'css-loader',
            options: {
              importLoaders: 1,
              esModule: false
            }
          },
          'postcss-loader'
        ]
      },
      {
        test: /\.less$/,
        use: [
          'style-loader',
          'css-loader',
          'postcss-loader',
          'less-loader'
        ]
      },
      {
        test: /\.(png|svg|gif|jpe?g)$/,
        use: [
          {
            loader: 'file-loader',
            options: {
              esModule: false // 不转为 esModule
            }
          }
        ]
      }
    ]
  }

file-loader 可以根据 output 配置输出指定路径或者直接配置 name 简写占位符输出 文件名称[ext]: 扩展名 [name]: 文件名 [hash]: 文件内容

{
    test: /\.(png|svg|gif|jpe?g)$/,
    use: [
      {
        loader: 'file-loader',
        options: {
          name: 'img/[name].[hash:6].[ext]',
          // outputPath: 'img'
        }
      }
    ]
}

url-loader

url-loader 也是处理图片静态资源的,跟 file-loader 的区别就是,url-loader 处理的文件,会直接转成 base64 资源加载,而不会像 file-loader 一样,将资源拷贝到指定的目录,分开请求,这样做的好处就是在客户端加载时减少对资源的请求次数;url-loader 内部其实也可以调用 file-loader,通过 limit 设置值,比值大时拷贝,比值小时就转为 base64

{
    test: /\.(png|svg|gif|jpe?g)$/,
    use: [
      {
        loader: 'url-loader',
        options: {
          name: 'img/[name].[hash:6].[ext]',
          limit: 25 * 1024//大于25KB拷贝,否则转base64
        }
      }
    ]
}

webpack5 内置asset模块

asset 模块是 webpack5 版本的内置模块,可以用来简化或者替代资源模块处理 loader(file-loader/url-loader...)的使用,通过添加 type 属性设置对应的 loader 处理

  • asset/resource -->file-loader( 输出路径 )
  • asset/inline --->url-loader(所有 data uri)
  • asset/source --->raw-loader
  • asset (parser )

asset 可以在 output 中通过添加 assetModuleFilename 属性设置打包出来的文件指定路径,但是不建议在这里设置,因为有时候不同资源类型不应该放在同一文件目录下,因此我们可以设置 asset ,通过配置完成对应 loader 的相同处理

     // {
     //   test: /\.(png|svg|gif|jpe?g)$/,
     //   type: 'asset/resource',
     //   generator: {
     //     filename: "img/[name].[hash:4][ext]"
     //   }
     // },
     // {
     //   test: /\.(png|svg|gif|jpe?g)$/,
     //   type: 'asset/inline'
     // }
     {
       test: /\.(png|svg|gif|jpe?g)$/,
       type: 'asset',
       generator: {
         filename: "img/[name].[hash:4][ext]"
       },
       parser: {
         dataUrlCondition: {
           maxSize: 30 * 1024//对应 url-loader 的 limit
         }
       }
     },
     {
       test: /\.(ttf|woff2?)$/,
       type: 'asset/resource',// asset 处理字体,拷贝到指定路径就行,注意引用路径和打包路径的一致
       generator: {
         filename: 'font/[name].[hash:3][ext]'
       }
     }

webpack 插件使用

webpack 插件与 loader 是有本质上的区别的,loader 是转换或处理指定类型文件时执行,而 plugin 可以做更多的事情,plugin 贯穿整个 webpack 打包流程,可以在任意时机执行(打包开始时/打包结束时...),plugins 里面的每个插件,或者说每个类,都是通过实例化或实例化传参实现的

插件的使用方法,如果是第三方插件,下载插件(npm install clean-webpack-plugin)后,导入插件,然后在导出对象新增 plugins 数组,使用插件

const { CleanWebpackPlugin } = require('clean-webpack-plugin')

plugins: [
   new CleanWebpackPlugin()
]

html-webpack-plugin

html-webpack-plugin 官方文档 webpack.docschina.org/plugins/htm…

html-webpack-plugin 在打包过程中会自动生成模板文件,生成的模板是哪来的呢,如果在没有任何配置的情况下,会根据依赖文件中的默认 ejs 模板生成模板

image.png

ejs 模板中有一个 htmlWebpackPlugin.options.title ,因此我们可以通过配置 htmlWebpackPlugin 的内置属性变量改变 title

image.png

如果说我们需要对照指定模板生成文件,htmlWebpackPlugin 也可以帮我们实现,而在一些主流框架脚手架生成的模板文件中,会有一些全局变量的使用,我们需要用到 webpack 内置的另一个插件 DefinePlugin,这个插件是 webpack 内置的,不需要再次下载,用来定义模板中的全局变量

在使用DefinePlugin插件时,有可能会遇到奇怪的坑,他会把定义的字符串里面的数据返回给模板,这样打包过程中就有可能会报一些语法错误,当遇到此类问题时,我们就需要注意了,在定义的变量当中,再次定义一个字符串给到模板

const { DefinePlugin } = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')

plugins: [
    new HtmlWebpackPlugin({
      title: 'html-webpack-plugin',
      template: './public/index.html'
    }),
    new DefinePlugin({
      BASE_URL: '"./"'
    })
]

copy-webpack-plugin

字面意思就能看出来是拷贝静态资源目录使用的插件,webpack 版本的更新,5 跟 4 会有所不同

const CopyWebpackPlugin = require('copy-webpack-plugin')

new CopyWebpackPlugin({
      patterns: [
        {
          from: 'public', //从哪开始拷贝
          globOptions: { //webpack5 重复资源会导致报错,此属性表示不拷贝
            ignore: ['**/index.html'] // **/ 表示从当前 public 文件夹下去找
          }
        }
      ]
})

babel

babel 的作用是处理 JS 的兼容,通常用来转换 JSX/TS/ES6+ 等一些浏览器不能直接使用的语法的,使用 babel 之前需要安装核心插件 @babel/core

在命令行终端使用 babel 时,必须安装 @babel/cli ,一般在使用 babel 转化 js 语法时,会用到 babel 的集合 @babel/preset-env

npx babel src --out-dir [输出路径] --plugin=[要使用的转换babel插件]

babel-loader

在单独使用 babel-loader 时,打包后文件并不会对 js 做任何处理,需要通过设置插件,这里直接使用预设处理

{
    test: /\.js$/,
    use: [
        {
            loader: 'babel-loader',
            options: {
                presets: ['@babel/preset-env']
            }
        }
    ]
}

还可以通过配置兼容特定浏览器版本

{
    test: /\.js$/,
    use: [
        {
            loader: 'babel-loader',
            options: {
                presets: [
                    [
                        '@babel/preset-env',
                        { targets: 'chrome 91' }
                    ]
                ]
            }
        }
    ]
}

一般都是像 postcss 一样通过配置 .browserslistrc 文件处理兼容,postcss 可以单独拿出来通过单独的文件配置(postcss.config.js),babel-loader 也可以通过配置文件 (babel.config.(js/mjs/cjs) 或者 babelrc.(json/js)) 达到一样的目的,这里推荐使用配置 babel.config.js

//babel.config.js
module.exports = {
  plugins: [
    require('postcss-preset-env')
  ]
}
{
    test: /\.js$/,
    use: ['babel-loader']
}

@babel/polyfill

@babel/preset-env 预设并不能完成所有功能的转换,比如使用 promise 时,IE9 以下版本是没办法识别的,需要通过填充一个 promise 方法去实现,polyfill 就是帮我们处理这件事情的,而 webpack5 为了提高打包效率,需要自己按照需求配置

babel 在升级之后不建议直接下载 @babel/polyfill 去使用( babeljs.io/docs/en/bab… ),而是通过使用 core-js 和 regenerator-runtime

module.exports = {
  presets: [
    [
      '@babel/preset-env',
      {
        // false: 不对当前的JS处理做 polyfill 的填充
        // usage: 依据用户源代码当中所使用到的新语法进行填充
        // entry: 依据我们当前筛选出来的浏览器决定填充什么
        useBuiltIns: 'entry',
        corejs: 3
      }
    ]
  ]
}

webpack-dev-server

当更新文件内容时,客户端也需要同步更新,如果是通过启动打包文件的模板文件启动的客户端,可以配置命令行,监听文件实现

"build": "webpack --config lg.webpack.js --watch"

也可以通过配置文件实现监听,跟 entry 同级添加 watch: true,默认为 false ,那么为什么是 false 呢,因为他是有性能问题的,监听了之后的跟新就相当于每次更新文件,项目都是所有文件重新打包,不能实现局部更新,那么 webpack 既然这么主流,当然是有解决办法的

安装 webpack-dev-server

npm i webpack-dev-server -D

配置启动服务命令

"serve": "webpack serve --config [配置文件路径]"

启动服务后会发现并没有生成 dist 打包后的目录文件,因为 webpack-dev-server 会将数据运行在内存里面,不需要再进行磁盘读写

webpack-dev-middleware

此插件可以配合 node 启动服务编译 webpack 配置,可以实现定制化,让 webpack 更加灵活

const express = require('express')
const webpackDevMiddleware = require('webpack-dev-middleware')
const webpack = require('webpack')

const app = express()

// 获取配置文件
const config = require('./webpack.config.js')
const compiler = webpack(config)

app.use(webpackDevMiddleware(compiler))

// 开启端口上的服务
app.listen(3000, () => {
  console.log('服务运行在 3000端口上')
})

webpack5 HMR

HMR (Hot Module Replacement)也就是所谓的热替换/热更新,webpack 提供的这个功能也是最让开发者惊喜的功能之一,可以在更新模块时,不影响其他模块的前提下,做到局部更新

默认 devServer.hot 为 true 就表示开启热更新,但是开发模式下会跟 .browserslistrc 有冲突,我们需要再通过配置给他屏蔽掉

target: 'web',
devServer: {
    hot: true
}

但是这样配置之后,浏览器还是会通过刷新整个页面去更新,我们还需要在入口文件告诉 webpack 需要实现热更新的模块,还可以通过回调的方式做一些更新的后续操作

if (module.hot) {
  module.hot.accept(['./title.js'], () => {
    console.log('title.js模块更新')
  })
}

react HMR 实现之前,我们需要配置对应的 loader 去处理 react 文件,也就是 jsx ,因为之前都是通过 babel-loader 处理,所以我们可以通过 babel 的预设处理 react 模块

{
    test: /\.jsx?$/,
    use: ['babel-loader']
}
//babel.config.js
module.exports = {
  presets: [
    ['@babel/preset-env'],
    ['@babel/preset-react'],
  ]
}

react HMR 实现需要用到插件 @pmmmwh/react-refresh-webpack-plugin,此插件是跟 react-refresh 配套使用的,核心也就是 react-refresh ,插件我们知道怎么去写配置,而 react-refresh 是处理 react 模块文件的,而 react 模块文件我们是用 babel-loader 处理的,所以我们可以在 babel 的单独配置文件中去配置 react-refresh

const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin')
plugins: [
    new ReactRefreshWebpackPlugin()
]
//babel.config.js
module.exports = {
  presets: [
    ['@babel/preset-env'],
    ['@babel/preset-react'],
  ],
  plugins: [
    ['react-refresh/babel']
  ]
}

vue HMR 的实现,参考官网,vue-loader 可以支持 vue 组件的 HMR,提供开箱即用体验,但是我们还是要注意下版本的使用,vue-loader 16版本是针对 vue3 版本的,vue2 使用16以下版本,而14跟15版本使用又有所不同,14版本可以直接使用,15版本需要手动导入 vue-loader 的插件再使用,否则打包会报错

const VueLoaderPlugin = require('vue-loader/lib/plugin')

module: {
    rules: [
      {
        test: /\.vue$/,
        use: ['vue-loader']
      }
    ]
},
plugins: [
    new VueLoaderPlugin()
]

output 中 publicPath 属性解析和 devServer 常用属性解析

webpack 打包出来的资源,会被模板文件导入,导入的文件路径就相当于 域名 + publicPath + filename

如果是打包后通过模板本地启服务,给 publicPath 赋值 './' 就是把绝对路径改成相对路径,这样才能找到 filename 文件,但如果是通过 webpack server 启的服务,他是相对于项目的目录,是找不到 filename 的值所对应的文件的

output: {
    filename: 'js/main.js',
    path: path.resolve(__dirname, 'dist'),
    publicPath: '/'
}

devServer 的属性都是为 webpack 启动 server 服务时所配置

devServer 中也可以配置 publicPath ,它的含义就是我们在起用 server 服务时,去 publicPath 定义的目录中加载资源,而打包产出的目录是 output 中的 publicPath 定义的,所以官方也是建议两个值最好是指向同一目录

contentBase 属性的作用是我们打包之后的资源如果说依赖其它的资源,此时就告知去哪找,比如有个 JS 文件不希望被打包,但是依旧要被使用时,我们是直接通过 script 标签去引入的

image.png

image.png

如果配置了 publicPath 目录,不配置 contentBase 是会找不到文件的,contentBase 一般是绝对路径

contentBase 跟 watchContentBase 也是配套使用的,配置后,更改依赖的静态资源后才可以做到即时更新

devServer: {
    hot: true,
    publicPath: '/lg',
    contentBase: path.resolve(__dirname, 'public'),
    watchContentBase: true
}

devServer 其他配置属性

devServer配置 这里主要讲讲 compress、historyApiFallback、proxy

在没有使用 compress 之前请求的资源大小

image.png

使用 compress 之后请求资源的大小,会在加上压缩资源的请求头,然后服务端会进行压缩,实际大小还是1.6M image.png

image.png

historyApiFallback 的作用官网已经说明,这里描述以下应用场景:

当使用 history 路由模式的时候,跳转路由然后刷新页面,这个时候是去服务端请求带路由的链接,而服务端是没有这个资源的,所以会报 404 ,这个时候 historyApiFallback 就起到他的作用了

devServer: {
    hot: true,
    port: 4000,//服务端口
    open: false,//启动服务时打开浏览器
    compress: true,
    historyApiFallback: true
}

proxy 的作用是代理服务的,一般本地跑项目是我们的域名是 localhost,这个时候如果我们要去请求其他域名下的接口时会存在跨域的问题,proxy 就是解决这个问题的

proxy 的属性是一个标志,这个标志的含义就是当请求接口遇到 '/api' 时,就会在前面自动补全域名,就相当于 'api.github.com/api', 如果不想要 '/api' 后缀,可以使用 pathRewrite 属性,也就相当于替换操作

最后加上 changeOrigin,这个属性就是避免服务端不允许不同域名的客户端请求,changeOrigin 可以'欺骗'接口服务,让我们请求拿到数据

devServer: {
    hot: true,
    hotOnly: true,
    port: 4000,
    open: false,
    compress: true,
    historyApiFallback: true,
    proxy: {
      '/api': {
        target: 'https://api.github.com',
        pathRewrite: { "^/api": "" },
        changeOrigin: true
      }
    }
}

resolve

webpack resolve 模块可以配置项目中的模块解析规则,可以配置 extensions 补全模块后缀名,他会自动去项目中查找对应模块,或者从 node_modules 中查找,或者配置 modules 告诉 webpack 解析模块时应该搜索的目录,他的默认值就是 node_modules

resolve.alias 可以使用路径别名,配置的路径可以使用属性代替

resolve: {
    extensions: [".js", ".json", '.ts', '.jsx', '.vue'], 
    alias: {
      '@': path.resolve(__dirname, 'src')
    }
}
//模块使用
import Home from '@/components/Home'
import About from '@/components/About'

sourceMap

在解析 sourceMap 之前,先熟悉下 mode 的值,生产模式(production)和开发模式(development),生产模式下打包 webpack 会对代码进行压缩,不利于调试;开发模式可以通过配置 devtool 调试开发代码,默认值是 'eval'

设置 devtool 值为 source-map 后打包会生产 .map 后缀的文件,映射打包出来的文件跟源代码之间的关系,设置不同的 sourceMap 值,不一定会生成 .map 文件,有可能会在打包文件中直接映射成 base64 的文件,从而减少请求资源,比如 eval-source-map、inline-suorce-map

devtool详细配置

webpack TS编译

webpack 编译 ts 模块可以使用 ts-loader,而 ts-loader 只会将 ts 语法转译成 js 语法,并不会做一些 polyfill 的代码填充操作,所以如果我们需要做一些填充操作的话可以使用 babel-loader 的专门针对 ts 的预设插件 @babel/preset-typescript ,这样我们就可以在 babel 的单独配置文件中去配置他

//babel.config.js
module.exports = {
  presets: [
    ['@babel/preset-env', {
      useBuiltIns: 'usage',
      corejs: 3
    }],
    ['@babel/preset-typescript']
  ]
}
//webpack.config.js
module: {
    rules: [
      {
        test: /\.ts$/,
        use: ['babel-loader']
      }
    ]
}

ts-loader 在打包时就会对 ts 语法做校验,进行错误拦截,而 babel-loader 并不会拦截,这样我们可以做一个折中的方案,可以在打包命令之前进行 tsc 校验操作

"scripts": {
    "build": "npm run ck && webpack",
    "ck": "tsc --noEmit"
}

webpack 区分环境打包

webpack 可以使用 --env 传递参数,把所有的配置文件统一放到一个文件夹(config)中,这里需要注意的点就是路径问题

entry 的相对路径是相对 context 的路径指向,而 context 的默认指向是启动命令 --config 后面的路径,所以 entry 配置不改也不会报错,但是其他路径可能会有问题,因此我们新建一个 path.js 专门用来把路径指向项目根目录

image.png

//path.js
const path = require('path')

const appDir = process.cwd()

const resolveApp = (relativePath) => {
  return path.resolve(appDir, relativePath)
}

module.exports = resolveApp

webpack.common.js 为 webpack 公共配置文件,通过导出函数,接收 --env 传递的环境参数区分配置,在通过 webpack-merge 合并配置文件

//webpack.common.js
const resolveApp = require('./paths')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { merge } = require('webpack-merge')

// 导入其它的配置
const prodConfig = require('./webpack.prod')
const devConfig = require('./webpack.dev')

// 定义对象保存 base 配置信息
const commonConfig = {
  entry: './src/index.js',  // 相对 context 的路径,context 默认值是启动命令 --config 后面的路径
  resolve: {
    extensions: [".js", ".json", '.ts', '.jsx', '.vue'],
    alias: {
      '@': resolveApp('./src')
    }
  },
  output: {
    filename: 'js/main.js',
    path: resolveApp('./dist')
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          {
            loader: 'css-loader',
            options: {
              importLoaders: 1,
              esModule: false
            }
          },
          'postcss-loader'
        ]
      },
      {
        test: /\.less$/,
        use: [
          'style-loader',
          'css-loader',
          'postcss-loader',
          'less-loader'
        ]
      },
      {
        test: /\.(png|svg|gif|jpe?g)$/,
        type: 'asset',
        generator: {
          filename: "img/[name].[hash:4][ext]"
        },
        parser: {
          dataUrlCondition: {
            maxSize: 30 * 1024
          }
        }
      },
      {
        test: /\.(ttf|woff2?)$/,
        type: 'asset/resource',
        generator: {
          filename: 'font/[name].[hash:3][ext]'
        }
      },
      {
        test: /\.jsx?$/,
        use: ['babel-loader']
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: 'copyWebpackPlugin',
      template: './public/index.html'
    })
  ]
}

module.exports = (env) => {
  const isProduction = env.production

  process.env.NODE_ENV = isProduction ? 'production' : 'development'//赋值提供给 babel 使用

  // 依据当前的打包模式来合并配置
  const config = isProduction ? prodConfig : devConfig

  const mergeConfig = merge(commonConfig, config)

  return mergeConfig
}

生产环境配置文件

//webpack.prod.js
const CopyWebpackPlugin = require('copy-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')

module.exports = {
  mode: 'production',
  plugins: [
    new CleanWebpackPlugin(),
    new CopyWebpackPlugin({
      patterns: [
        {
          from: 'public',
          globOptions: {
            ignore: ['**/index.html']
          }
        }
      ]
    })
  ]
}

开发环境合并配置时,要注意以下之前 react 使用热更新时,babel 的配置文件中配置了 react-refresh, 所以 babel 配置文件中,我们也要区分一下环境

//webpack.dev.js
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin')

module.exports = {
  mode: 'development',
  devtool: 'cheap-module-source-map',
  target: 'web',
  devServer: {
    hot: true,
    hotOnly: true,
    port: 4000,
    open: false,
    compress: true,
    historyApiFallback: true,
    proxy: {
      '/api': {
        target: 'https://api.github.com',
        pathRewrite: { "^/api": "" },
        changeOrigin: true
      }
    }
  },
  plugins: [
    new ReactRefreshWebpackPlugin()
  ]
}
//babel.config.js
const presets = [
  ['@babel/preset-env'],
  ['@babel/preset-react'],
]

const plugins = []

console.log(process.env.NODE_ENV, '<------')

// 依据当前的打包模式来决定plugins 的值 
const isProduction = process.env.NODE_ENV === 'production'
if (!isProduction) {
  plugins.push(['react-refresh/babel'])
}

module.exports = {
  presets,
  plugins
}

命令行脚本配置

//package.json
"scripts": {
    "build2": "webpack --config ./config/webpack.common.js --env production",
    "serve2": "webpack serve --config ./config/webpack.common.js --env development"
}

webpack 代码拆分

webpack 可以支持多入口打包,进行分包

entry: {
    main1: './src/main1.js',
    main2: './src/main2.js'
}

如果项目木跨有引入其他第三方依赖模块,比如 lodash 和 jq 可以单独拎出来进行打包操作

entry: {
    main1: { import: './src/main1.js', dependOn: 'shared' },
    main2: { import: './src/main2.js', dependOn: 'shared' },
    shared: ['lodash', 'jquery']
}

但是这样配置之后直接 npm run build 打生产包时会生成一个 txt 文件,生产是不需要的,可以通过配置 optimization 下的插件 TerserWebpackPlugin 删除注释文件,这个插件是 webpack 5以上版本内置的,不用下载,直接引入使用就行

image.png

const TerserPlugin = require("terser-webpack-plugin");

optimization: {
     minimizer: [
       new TerserPlugin({
         extractComments: false,
       }),
     ]
 }

常见的第三种分离代码的方式也是通过配置 optimization 的 SplitChunksPlugin 进行拆分,这样就可以还是用 index.js 文件作为入口,webpack 自动帮我们拆分代码

optimization: {
    minimizer: [
      new TerserPlugin({
        extractComments: false,
      }),
    ],
    splitChunks: {
      chunks: 'all'
    }
}

image.png

SplitChunks 配置

SplitChunks 有他的默认配置

module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks: 'async', // 默认打包异步引入模块,'initial' 同步,'all' 都打包
      minSize: 20000, // 大于 20000 字节的第三方模块单独打包,配套还有 maxSize 一般设置minSize 一样的值
      minRemainingSize: 0,
      minChunks: 1, // 最小引入次数,最少被引入过1次的模块才被打包,使用时注意 minSize/maxSize 会优先
      maxAsyncRequests: 30,
      maxInitialRequests: 30,
      enforceSizeThreshold: 50000,
      cacheGroups: {
        defaultVendors: {
          test: /[\\/]node_modules[\\/]/,// 正则匹配规则
          priority: -10,
          reuseExistingChunk: true,
        },
        default: {
          minChunks: 2,// 最少引入次数
          priority: -20,
          reuseExistingChunk: true,
        },
      },
    },
  },
};

defaultVendors 配置表示,node_modules 里被引入的模块单独打包;default 表示最少被引入过两次的第三方依赖单独打包;他们还可以通过配置 filename 定义打包出来的文件格式

cacheGroups: {
    syVendors: {
      test: /[\\/]node_modules[\\/]/,
      filename: 'js/[id]_vendor.js',
      priority: -10,
    },
    default: {
      minChunks: 2,
      filename: 'js/syy_[id].js',
      priority: -20,
    }
}

如果两个配置都满足的情况下,就会根据 priority 值的大小去规定具体打包成哪个格式文件,官方也是用负数去判断,值越大权重越高

webpack 动态导入

webpack 动态导入的模块,默认情况下会单独打包,我们可以通过配置 optimization.chunkIds 定义打包文件的规则

natural 当前文件的名称是按自然数进行编号排序,如果某个文件当前次不再被依赖那么重新打包时序号都会变----不推荐使用

optimization: {
    chunkIds: 'natural',
    minimizer: [
      new TerserPlugin({
        extractComments: false,
      }),
    ]
}

image.png

named 会根据文件名字进行打包,产出的文件名对应模块文件名----建议开发模式下使用

optimization: {
    chunkIds: 'named',
    minimizer: [
      new TerserPlugin({
        extractComments: false,
      }),
    ]
}

image.png

deterministic 在不同的编译中不变的短数字 id。有益于长期缓存。在生产模式中会默认开启----生产使用

optimization: {
    chunkIds: 'deterministic',
    minimizer: [
      new TerserPlugin({
        extractComments: false,
      }),
    ]
}

image.png

通常 bundle.js 提供的 html 模板导入使用,因此我们可以通过 chunkFilename 配置打包模块的别名

output: {
    filename: 'js/[name].bundle.js',
    path: resolveApp('./dist'),
    chunkFilename: 'js/chunk_[name].js'
}

image.png

因为使用的 deterministic 模式,因此 id 跟 name 的值是一样的,可以使用魔法注释改变 name 值

// index.js
import(/*webpackChunkName: "title"*/'./title')

console.log('index.js代码')

image.png

runtimeChunk

optimization.runtimeChunk 的默认值是 false ,他的作用就是会把模块 import/require 等依赖关系打包成单独文件,这样做的好处就是可以缓存没有变化的文件

可以通过给文件添加 hash 值,更改文件内容,查看 hash 值变化

output: {
    filename: 'js/[name].[contenthash:8].bundle.js',
    path: resolveApp('./dist'),
},
optimization: {
    runtimeChunk: true,
    minimizer: [
      new TerserPlugin({
        extractComments: false,
      }),
    ]
}

代码懒加载

入口文件如果直接导入模块,因为设置 optimization ,webpack 会将模块单独打包成文件,在浏览器请求数据加载资源初始化时,会将同时加载模块资源,这样并不太友好,因为我们不需要初始化就去加载他,因此我们可以按需加载

const oBtn = document.createElement('button')
oBtn.innerHTML = '点击加载元素'
document.body.appendChild(oBtn)

// 按需加载
oBtn.addEventListener('click', () => {
  import('./utils').then(({ default: element }) => {
    console.log(element)
    document.body.appendChild(element)
  })
})

这样虽然打包结果文件是一样的,因为并没有修改 webpack 配置文件,但是可以通过点击按钮时再去加载模块文件

我们还可以通过 prefetch/preLoad 预获取/预加载模块;prefetch 也会在浏览器请求资源初始化时加载模块资源,但是他是在浏览器空闲时再去加载资源的;preload chunk 会在父 chunk 中立即请求,用于当下时刻

const oBtn = document.createElement('button')
oBtn.innerHTML = '点击加载元素'
document.body.appendChild(oBtn)

// 按需加载
oBtn.addEventListener('click', () => {
  import(
    /*webpackChunkName:'utils' */
    /*webpackPreFetch:true */
    './utils').then(({ default: element }) => {
      console.log(element)
      document.body.appendChild(element)
    })
})

第三方拓展设置 CDN

在模块引入了 lodash ,并且 webpack 配置了 splitChunks 的情况下,lodash 会被打包,如果我们不想去打包他,通过 script 标签引入 cdn 的资源,因为这些资源通常是不会经常改动的,这样我们可以优化打包效率,那么我们可以去对应模块的官网去找找看有没有对应的 cdn 服务的资源链接,然后我们可以通过 webpack 的外部拓展 externals 剥离依赖模块,对应的值也是依赖模块导出的值

externals: {
    lodash: '_'
}
//index.html 模板文件
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>

打包 Dll

在通常情况下,比如项目中 vue/react 等依赖,我们不想使用 cdn 的方式应用,而这些依赖又是不经常变动的模块,我们可以使用 webpack 的 DllPlugin 打包一个 Dll 模块

注意:这是单独出来的,只是为了打包出一个 dll 模块供后续使用,跟项目 webpack 配置无关

const path = require('path')
const webpack = require('webpack')
const TerserPlugin = require('terser-webpack-plugin')

module.exports = {
  mode: "production",
  entry: {
    react: ['react', 'react-dom']
  },
  output: {
    path: path.resolve(__dirname, 'dll'),
    filename: 'dll_[name].js',
    library: 'dll_[name]'
  },
  optimization: {
    minimizer: [
      new TerserPlugin({
        extractComments: false
      }),
    ],
  },
  plugins: [
    new webpack.DllPlugin({
      name: 'dll_[name]',
      path: path.resolve(__dirname, './dll/[name].manifest.json')
    })
  ]
}

dll_react.js 就是 react 和 react-dom 的打包文件,react.manifest.json 是主要加载文件,通过插件查找 json 文件,再反向查找所对应的依赖资源

image.png

image.png

那怎么使用 Dll 的资源呢,我们需要用到 DllReferencePlugin ,通过索引到 json 文件,然后通过配置上下文链接到库文件

plugins: [
    new HtmlWebpackPlugin({
      title: 'copyWebpackPlugin',
      template: './public/index.html'
    }),
    new webpack.DllReferencePlugin({
      context: resolveApp('./'),
      manifest: resolveApp('./dll/react.manifest.json')
    })
]

这样配置之后打包文件不会去打包 react.manifest.json 中的依赖文件(即使设置了 splitChunks),但是还是会有问题,打包出来的模板文件并没有帮我们去引入 dll 的资源

image.png

image.png

这里我们可以使用 add-asset-html-webpack-plugin 这个插件去帮我们做这件事情,他会帮我们把 dll 资源一起加载进打包文件中

注:vue-cli/create-react-app 脚手架工具会帮我们做这些事情,不需要手动配置

const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin')

plugins: [
    new HtmlWebpackPlugin({
      title: 'copyWebpackPlugin',
      template: './public/index.html'
    }),
    new webpack.DllReferencePlugin({
      context: resolveApp('./'),
      manifest: resolveApp('./dll/react.manifest.json')
    }),
    new AddAssetHtmlPlugin({
      outputPath: 'js',
      filepath: resolveApp('./dll/dll_react.js')
    })
]

image.png

css 的抽离压缩和 JS 的代码压缩

css 的抽离我们要用到 mini-css-extract-plugin ,在生产模式下配置

//webpack.prod.js
const MiniCssExtractPlugin = require("mini-css-extract-plugin")

module.exports = {
  mode: 'production',
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'css/[name].[hash:8].css'
    })
  ]
}

配置后,依据官方文档,我们还需要使用内置的 loader 去替代 style-loader;同样是生产环境配置,因此我们需要区分环境,通过函数传参的模式

const resolveApp = require('./paths')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { merge } = require('webpack-merge')
const TerserPlugin = require("terser-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin")

// 导入其它的配置
const prodConfig = require('./webpack.prod')
const devConfig = require('./webpack.dev')

// 定义对象保存 base 配置信息
const commonConfig = (isProduction) => {
  return {
    entry: {
      index: './src/index.js'
    },
    resolve: {
      extensions: [".js", ".json", '.ts', '.jsx', '.vue'],
      alias: {
        '@': resolveApp('./src')
      }
    },
    output: {
      filename: 'js/[name].[contenthash:8].bundle.js',
      path: resolveApp('./dist'),
    },
    optimization: {
      runtimeChunk: true,
      minimizer: [
        new TerserPlugin({
          extractComments: false,
        }),
      ]
    },
    module: {
      rules: [
        {
          test: /\.css$/,
          use: [
            isProduction ? MiniCssExtractPlugin.loader : 'style-loader',
            {
              loader: 'css-loader',
              options: {
                importLoaders: 1,
                esModule: false
              }
            },
            'postcss-loader'
          ]
        },
        {
          test: /\.less$/,
          use: [
            'style-loader',
            'css-loader',
            'postcss-loader',
            'less-loader'
          ]
        },
        {
          test: /\.(png|svg|gif|jpe?g)$/,
          type: 'asset',
          generator: {
            filename: "img/[name].[hash:4][ext]"
          },
          parser: {
            dataUrlCondition: {
              maxSize: 30 * 1024
            }
          }
        },
        {
          test: /\.(ttf|woff2?)$/,
          type: 'asset/resource',
          generator: {
            filename: 'font/[name].[hash:3][ext]'
          }
        },
        {
          test: /\.jsx?$/,
          use: ['babel-loader']
        }
      ]
    },
    plugins: [
      new HtmlWebpackPlugin({
        title: 'copyWebpackPlugin',
        template: './public/index.html'
      })
    ]
  }
}

module.exports = (env) => {
  const isProduction = env.production

  process.env.NODE_ENV = isProduction ? 'production' : 'development'

  // 依据当前的打包模式来合并配置
  const config = isProduction ? prodConfig : devConfig

  const mergeConfig = merge(commonConfig(isProduction), config)

  return mergeConfig
}

压缩 css 需要插件 css-minimizer-webpack-plugin ,同样生产模式配置

//webpack.prod.js
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin")

optimization: {
    minimizer: [
      new CssMinimizerPlugin()
    ]
}

JS 的代码压缩,生产模式下,webpack 其实已经帮我们做了,可以使用内置的 terser 去体验代码压缩,也可以参照 terser-webpack-plugin 定制化配置 webpack 的 JS 压缩

npx terser js/file1.js js/file2.js \
         -o foo.min.js -c -m \
         --source-map "root='http://foo.com/src',url='foo.min.js.map'"

scope hoisting

作用域提升,优化打包后体积,对应 webpack 插件 ModuleConcatenationPlugin ,此插件生产模式打包默认开启,是 webpack 内置插件,并且他基于 ESModule 的静态语法分析,通过将多个模块合并成一个模块进行作用域提升

// const webpack = require('webpack')

module.exports = {
  mode: 'production',
  plugins: [
    // new webpack.optimize.ModuleConcatenationPlugin()
  ]
}

usedExports 处理 Tree Shaking

usedExports 做为 webpack 的 Tree Shaking 使用,usedExports 开启的情况下,会标记未使用的导出内容,然后我们可以使用 TerserPlugin 实现树摇操作,TerserPlugin 需要开启 minimize

util.js 导出两个函数

export function foo1(num1, num2) {
  return num1 + num2
}

export function foo2(a, b) {
  return a * b
}

入口 index.js 文件

import { foo1 } from './utils'

console.log(foo1(10, 20))

开启 usedExports 未使用 TerserPlugin,使用开发模式打包出来的文件

image.png

使用 minimize 后,文件就没有 foo2 函数了

image.png

module.exports = {
  mode: 'development',
  devtool: false,
  optimization: {
    usedExports: true,
    minimize: true,
    minimizer: [
      new TerserPlugin({
        extractComments: false,
      }),
    ]
  }
}

sideEffects 处理 Tree Shaking

sideEffects 告知 webpack 去辨识 package.json 中的 副作用 标记或规则,以跳过那些当导出不被使用且被标记不包含副作用的模块

例如在 title.js 和 utils.js 文件定义一个 window 对象,入口文件导入(注:导入文件,不导入文件模块,导入文件模块会使用副作用,sideEffects 不起作用), sideEffects 开启 title 文件副作用,那么就只有 title 文件的对象能被启用,utils 文件的 window 就没有了,当然如果 sideEffects 为 false 的话就是所有文件都不开启副作用模式,那么两个值就都是 undefined 了

//title.js
window.title = '前端开发'
//utils.js
window.title = '1111'
//index.js
import './utils'
import './title'

console.log(window.utils, '<------')//undefined
console.log(window.title, '<------')//前端开发
//package.json
"sideEffects": [
    "./src/title.js"
]

也可以在 webpack 的对应配置文件中去配置副作用

入口文件中导入 css ,打包过程中会被编译成 js,sideEffects 没有配置 css 文件的情况下,css 是加载不进来的,可以在 loader 中配置 sideEffects

module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          {
            loader: 'css-loader',
            options: {
              importLoaders: 1,
              esModule: false
            }
          },
          'postcss-loader'
        ],
        sideEffects: true,
      }
    ]
}

CSS Tree Shaking

css 的 Tree Shaking 操作需要借助第三方插件 purgecss-webpack-plugin,同时他还依赖 glob 库

npm install purgecss-webpack-plugin glob -D

根据文档提示,我们定位到 css 目录路径后,执行打包操作,样式文件中的一些标签样式也会被一起摇掉( body/html... ),因此我们可以配置不需要被摇掉的标签或样式,同时在生产模式下去配置

const CopyWebpackPlugin = require('copy-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const TerserPlugin = require("terser-webpack-plugin")
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin")
const PurgeCSSPlugin = require('purgecss-webpack-plugin')
const resolveApp = require('./paths')
const glob = require('glob')

module.exports = {
  mode: 'production',
  optimization: {
    usedExports: true, // 标记不被使用的函数
    minimize: true,
    minimizer: [
      new TerserPlugin({
        extractComments: false,
      }),
      new CssMinimizerPlugin()
    ]
  },
  plugins: [
    new CleanWebpackPlugin(),
    new CopyWebpackPlugin({
      patterns: [
        {
          from: 'public',
          globOptions: {
            ignore: ['**/index.html']
          }
        }
      ]
    }),
    new MiniCssExtractPlugin({
      filename: 'css/[name].css'
    }),
    new PurgeCSSPlugin({
      paths: glob.sync(`${resolveApp('./src')}/**/*`, { nodir: true }),
      safelist: function () {
        return {
          standard: ['body', 'html', 'test']// test为类选择器格式
        }
      }
    })
  ]
}

webpack 资源压缩

之前有在开发模式 devServer 中配置过 compress ,他会在向服务端请求资源时,告诉服务端支持的压缩模式,服务端会把对应压缩过的资源响应给客户端

image.png

image.png

在生产模式下,我们可以使用 compression-webpack-plugin 插件,实现资源压缩

new CompressionPlugin({
  test: /\.(css|js)$/,//匹配需要压缩的文件格式
  minRatio: 0.8,//压缩比 压缩后文件/压缩前文件
  threshold: 0,//超过值再进行压缩
  algorithm: 'gzip'//压缩算法
})

inline-chunk-html-plugin

inline-chunk-html-plugin 也是属于性能优化插件,例如 webpack 打包出来的文件是通过 script 标签引入文件,如果文件内容不多的情况下,我们可以直接将文件代码写入到模板文件中,这样可以再次减少资源的请求,inline-chunk-html-plugin 也必须依赖 html-webpack-plugin

const HtmlWebpackPlugin = require('html-webpack-plugin')
const InlineChunkHtmlPlugin = require('inline-chunk-html-plugin')

module.exports = {
  mode: 'production',
  plugins: [
    new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/runtime.*\.js/])
  ]
}

使用之前

image.png

使用之后

image.png

webpack 打包 library

打包自定义库一般可以使用 rollup ,webpack 也可以实现自定义的全局变量

const path = require('path')

module.exports = {
  mode: 'development',
  devtool: false,
  entry: './src/index.js',
  output: {
    filename: 'sy_utils.js',
    path: path.resolve(__dirname, 'dist'),
    libraryTarget: 'umd',
    library: 'syUtil', 
    globalObject: 'this' //通过 this 定义全局对象
  }
}

image.png

新建一个 html 模板使用他,通过定义的 syUtil 可以访问到入口文件导出的对象

image.png

webpack 打包时间和内容分析

webpack 使用插件 speed-measure-webpack-plugin 可以查看 loader 或者 plugin 处理的时长,使用此插件可能会跟 loader/plugin 存在冲突,我们要对应处理对应版本问题

使用实例对象的 warp 方法包裹配置对象

// 时间分析
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin")
const smp = new SpeedMeasurePlugin()

module.exports = (env) => {
  const isProduction = env.production

  process.env.NODE_ENV = isProduction ? 'production' : 'development'

  // 依据当前的打包模式来合并配置
  const config = isProduction ? prodConfig : devConfig

  const mergeConfig = merge(commonConfig(isProduction), config)

  return smp.wrap(mergeConfig)
}

image.png

使用 webpack-bundle-analyzer 可以查看打包出来的 bundle 大小和对应依赖关系,这样可以分析对应内容是否需要分包处理

配置打包后,webpack 会帮我们自动开启服务

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin

module.exports = {
  mode: 'production',
  plugins: [
    new BundleAnalyzerPlugin()
  ]
}

image.png