webpack5之核心配置梳理

1,740 阅读26分钟

webpack系列目录

  1. webpack5之核心配置梳理
  2. webpack5之模块化原理
  3. webpack5之Babel/ESlint/浏览器兼容
  4. webpack5之性能优化
  5. webpack5之Loader和Plugin的实现
  6. webpack5之核心源码解析

安装依赖 webpack webpack-cli

webpack-cli 安装包并不是必须依赖,比如在vue-cli或者react-cli中均不会使用,该安装包主要在使用webpack命令打包时会调用node_modules/.bin/webpack,而该命令又依赖webpack-cli,比如执行命令webpack 配置命令 配置文件,该命令中的配置命令和配置文件参数均需要webpack-cli参与,而webpack-cli又依赖webpack打包。我们也直接可以在文件中引入webpack,比如直接调用webpack函数并传入配置命令和配置文件等参数来打包,而不依赖webpack-cli。

当执行 webpack 命令时默认执行当前目录下的 webpack.config.js 配置文件,如果没有会去执行 ./src/index.js 入口配置文件。比如执行 webpack --entry ./src/main.js --output-path ./build,指定打包入口和打包出口,主要配置的相关命令参数和功能如下

--entrystring[]应用程序的入口文件,例如 ./src/main.js
--config, -cstring[]提供 webpack 配置文件的路径,例如 ./webpack.config.js
--config-namestring[]要使用的配置名
--namestring配置名称,在加载多个配置时使用
--colorboolean启用控制台颜色
--merge, -mboolean使用 webpack-merge 合并两个配置文件,例如 -c ./webpack.config.js -c ./webpack.test.config.js
--envstring[]当它是一个函数时,传递给配置的环境变量

具体其他命令参数可参考官方文档 webpack.docschina.org/api/cli/#fl…

核心配置

mode

提供 mode 配置选项,告知 webpack 使用相应模式的内置优化。 有三个值'none' | 'development' | 'production',默认为 'production' 。对应的相关说明如下。

选项描述
development会将 DefinePlugin 中 process.env.NODE_ENV 的值设置为 development. 为模块和 chunk 启用有效的名。
production会将 DefinePlugin 中 process.env.NODE_ENV 的值设置为 production。为模块和 chunk 启用确定性的混淆名称,FlagDependencyUsagePluginFlagIncludedChunksPluginModuleConcatenationPluginNoEmitOnErrorsPlugin 和 TerserPlugin 。
none不使用任何默认优化选项

不同的 mode 会有不同的默认配置,目前在官网暂时没找到默认配置的展示,只能通过查看 webpack default options (source code)

module.exports = {
  mode: 'production'  // 生产环境
  // mode: 'development'  // 开发环境
  // mode: 'node'  // 无配置
}

entry

entry 就是编译打包的配置入口,可以入参字符串、对象、数组

  • 入参为字符串
module.exports = {
  entry: './src/main.js'
}
  • 入参为对象
module.exports = {
  entry: {
    main: './src/main.js',
    index: './src/index.js'
  }
}

output

output 就是最终打包输出的文件

主要参数配置如下

  • filename 输出文件名称
  • path 打包输出文件目录
  • clean 打包前会将打包目录删除

注意:配置output.path需要取绝对路径,因此可引入node自带的 path 库来获取当前打包输出文件目录的绝对路径。

const path = require('path')
const resolve = (src) => {  
  return path.resolve(__dirname, src)
}
module.exports = {  
  entry: './src/main.js',  
  output: {    
    filename: 'bundle.js',    
    path: resolve('build'),    
    clean: true  
  }
}

devtool

此选项控制是否生成,以及如何生成source mapsource map是什么呢,在我们生产环境上,当产生报错时我们很难去定位报错的具体位置,因为生产环境下代码已经被一些babel,loader,压缩工具给丑化和压缩了,此时调试就特别困难,即使在开发环境,代码就是被编译过了的与源代码也是有很多不一致的地方。那如何让报错能够指定到对应的源文件且能准确的对应到具体的错行那行代码上,那答案就是source map,它能够将编译后的代码映射到源文件上。那如何使用source map呢,很简单,首先会根据源文件生成source map文件,我们可以通过webpack配置生成source map文件,之后在编译后的代码最后加入一行注释,指向source-map文件,注释内容为 //# sourceMappingURL=bundle.js.map 。浏览器会根据我们的注释,寻找soure map文件,并根据source map文件还原源代码,便于我们去调试。

在我们浏览器中读取soure map功能是默认开启的,在chrome中,在如下图位置开启

那source map是怎么样的呢,我们通过webpack配置devtool 属性为 'source-map'

module.exports = {
  //...
  mode: 'development',
  devtool: 'source-map'
  //...
}

重新打包后我们在打包输出文件除了bundle.js以外多了一个bundle.js.map文件

查看bundle.js文件我们可以看到文件最后注释 sourceMappingURL 指向了bundle.js.map 文件

那我们就可以肯定这个bundle.js.map文件就是我们要的source map文件,那这个source map文件长什么样,我们对它做一个格式化

{
  "version": 3,
  "file": "js/bundle.js",
  "mappings": "yBAIAA,QAAQC,IAAIC,KACZF,QAAQC,ICJCE,GDKTH,QAAQC,ICDCE,G",
  "sources": [
    "webpack://lyj-test-library/./src/main.js",
    "webpack://lyj-test-library/./src/js/priceFormat.js"
  ],
  "sourcesContent": [
    "// import { createComponent } from './js/component'\n// createComponent()\n\nimport { add, minus } from './js/priceFormat'\nconsole.log(abc)\nconsole.log(add(2, 3))\nconsole.log(minus(5, 3))\n",
    "const add = (a, b) => {\n  return a + b\n}\n\nconst minus = (a, b) => {\n  return a - b\n}\n\nexport {\n  add,\n  minus\n}"
  ],
  "names": [
    "console",
    "log",
    "abc",
    "a"
  ],
  "sourceRoot": ""
}

对里面每一个属性说明

  • version: 当前使用的版本,也是最新的第三版
  • sources: 从哪些文件转换过来的source-map和打包的代码(最初始的文件)
  • names: 转换前的变量和属性名称(如果使用的是development模式,就为空数组,也就不需要保留转换前的名称
  • mappings: source-map用来和源文件映射的信息(比如位置信息等),一串base64 VLQ(veriable- length quantity可变长度值)编码
  • file: 打包后的文件(浏览器加载的文件)
  • sourceContent: 转换前的具体代码信息(和sources是对应的关系)
  • sourceRoot: 所有的sources相对的根目录

目前webpack5为处理source map给我们提供了26种选项,我们可以查看官网webpack.docschina.org/configurati…,官网为处理source map的每一个选项都做了快慢比较和区别,下么我们介绍下几种在我们开发测试发布中主要用到的选项。

首先介绍下三种不会出现source map的配置

  • (none) : devtool属性缺省,为production默认配置,不会生成source map
  • eval : 为development默认配置,它不会生成source map,但是在eval最后面添加 //# sourceURL= 注释,在我们调试时浏览器会跟给我们生成一些目录,方便我们调试
  • false : 不生产source map文件,也不会生成跟source map有关的一些内容

下面再介绍能生成source map的选项

source-map

会生成一个bundle.js.map的source-map文件,在打包文件bundle.js最下面会有 //# sourceMappingURL=bundle.js.map 这样一条注释,它会帮我们指向source-map文件。并且在浏览器中打开错误能够定位到源代码的具体报错那一行,而且那一列开始报错也给我们指定出来了。

eval-source-map

该选项不会生成.map文件,但是source map会以DataURL的方式添加到evel最后面,如 //# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64 。

同样的该配置也能定位到报错源代码具体的行和列位置

inline-source-map

该选项也不会生成.map文件,但是它会在打包文件bundle.js最下面以DataURL方式添加到文件最底下,如 //# sourceMappingURL=data:application/json;charset=utf-8;base64 。

同样也能在浏览器中定位到源代码具体的行和列位置

cheap-soure-map

它跟source-map一样,会生成.map文件,也会在bundle.js最下面生成 //# sourceMappingURL=bundle.js.map 注释指向.map文件,不同的是一个低开销的生成方式,它没有列映射,因为在实际开发中我们定位到某一行就大概能分析出问题了。

但是这个选项有个问题,当我们使用loader对源码做了处理,该source map报错定位就处理了不那么好了,比如我们使用babel-loader处理我们的代码

当我们重新定位报错位置后发现报错位置与源代码不符合了

因此就出现了cheap-module-source-map配置。

cheap-module-source-map

该选择与cheap-source-map的区别是它会对使用loader的soucre map处理更好。我们还是使用babel-loader处理。重新打包后定位到报错位置可以看到现在报错位置和源码所在位置以完全符合。

hidden-source-map

该选项会生成source map文件,但是在bundle.js文件最下面 //# sourceMappingURL=bundle.js.map 这条注释会被删除,这时就引用不了source map文件,如果在bundle.js下面手动添加上面这条注释,就又会生效。

nosources-source-map

该选项会生成source map,在打包文件bundle.js下面也会有添加url注释,但是和source-map不同的是,bundle.js.map文件缺少了sourceContent属性

因此只能产生报错信息,不能生成源文件。

实际上webpack给了我们这么多的source map选项,是可以组合的,组合规则如下

  • inline-|hidden-|eval: 三个值时三选一
  • nosources: 可选值
  • cheap: 可选值,并且可以跟随 module 的值

模式规则总结 [inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map 。

那我们在不同的环境下如何最优使用呢

  • 开发阶段 : 推荐使用 source-map 或者 cheap-module-source-map
    • 这分别是vue和react使用的值,可以获取调试信息,方便快速开发
  • 测试阶段 : 推荐使用 source-map 或者 cheap-module-source-map
    • 测试阶段我们也希望在浏览器下看到正确的错误提示
  • 发布阶段 : false、缺省值(不写)\

context

默认使用 Node.js 进程的当前工作目录,但是推荐在配置中传入一个值。这使得你的配置独立于 CWD(current working directory, 当前工作目录)。

devServer

这是一个本地开发时候用的属性,我们需要安装 npm install webpack-dev-server -D ,这个依赖包,因为启动一个本地服务器需要使用该依赖,之后我们就可以通过 webpack serve 这个脚本启用本地服务器,webpack会给我我们开启一个新的端口

"scripts": {
  "serve": "webpack serve --open"  // --open参数可以帮我们自动打开默认浏览器
}

这是会在浏览器打开一个http://localhost:8085 的页面,并且我们可以打开浏览器控制台看到webpack-dev-server启动的打印提示,并且不会出现构建目录,而是放到了内存中。那这个安装包是怎么实现启动本地服务的呢。我们可以查看源码

它实际用到了express库,通过express去监听一个端口,来打开一个本地服务器。那么我们可以不可以不用webpack-dev-server 自己去使express来启动一个本地服务呢。那么我们需要依赖 webpack-dev-middleware 安装包,安装 npm install webpack-dev-middleware express -D ,新建一个sever.js文件

// server.js
const WebpackDevMiddleware = require('webpack-dev-middleware')
const Webpack = require('webpack')
const express = require('express')
const app = express()

const compile = Webpack(require('./webpack.config.js'))

const middleware = WebpackDevMiddleware(compile)

app.use(middleware)

app.listen(8888, () => {
  console.log('8888端口已启动')
})

用node启动该文件,执行node server.js启动后

可以看到能正常启动,浏览器页面也能正常运行,但是平常我们不太会自定义去启动服务,除非启动过程中需要我们输出一些自定义的内容。

HMR

上面这些过程当我们修改文件时会刷新整个页面,这样整个内存又得重新初始化,那能不能有一种技术让我们不刷新浏览器,值更新修改的部分呢,有那就是 HMR,HMR 全程 Hot Module Replace,意思就是模块热替换,模块热替换是指在应用程序运行过程中,替换、添加、删除模块,而无需重新刷新整个页面。

从官方提示从 webpack-dev-server v4 开始,HMR 是默认启用的。它会自动应用 webpack.HotModuleReplacementPlugin,这是启用 HMR 所必需的。因此当 hot 设置为 true 或者通过 CLI 设置 --hot,你不需要在你的 webpack.config.js 添加该插件。

{
  devServer: {
    hot: true
  }
}

但是我们在修改文件后还是会刷新整个页面,因为我们还需要去配置一些东西,我们需要在引入文件的地下添加如下配置。

if(module.hot) {
  module.hot.accept('./js/priceFormat', () => {
    console.log('priceFormat更新了')
  })
}

当我们修改priceFormat文件是,浏览器就不会刷新

可以看浏览器控制显示HMR成功了。如果我们在编写过程中代码报错,正常我们修改后页面会重新刷新,如果我们想保留错误信息,那么我们只需要配置 hot: 'only'

在webpack使用hotOnly: true,webpack5已放弃该属性使用hot: 'only'

{
  devServer: {
    hot: 'only'
  }
}

当我们重新修改错误代码后,可以看到浏览器并不会刷新并会将之前的错误提示保留了下来。

现在我们来看看对主流框架的HMR的实现

vue的HMR支持

在加载vue文件时候,我们用了vue-loader,其实vue-loader已经帮我们实现了HMR,我们可以来试一下,我们编写一个vue组件

当我们修改vue文件中msg的值由'hello vue2' 改为 'hello vue3'

可以看到浏览器并没有刷新,并且输出了'hello vue3',说明HMR效果有成效。

react的HMR支持

react的HMR需要我们使用两个插件,安装 npm install @pmmmwh/react-refresh-webpack-plugin react-refresh -D ,配置如下

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

我们再编写一个react组件,并重新编译

现在我们将msg的值123转为456

可以看到浏览器并没有刷新,且浏览器控制台和页面中值已重新赋值和渲染。

实现原理

到目前为止HRM的实现都成功配置,那我们现在来看下HRM的底层实现又是怎么样的呢。首先webpack-dev-server会创建两个服务:

提供静态资源的 服务(express)Socket服务(net.Socket)

  • express server负责直接提供静态资源的服务(打包后的资源直接被浏览器请求和解析)
  • Socket服务(net.Socket)
  • 当服务器监听到对应的模块发生变化时,会生成两个文件.json(manifest文件)和.js文件(update chunk)
  • 通过长连接,可以直接将这两个文件主动发送给客户端(浏览器)
  • 浏览器拿到两个新的文件后,通过HMR runtime机制,加载这两个文件,并且针对修改的模块进行更新

我们可以看如下原理图来理解

publicPath(webpack4可配置,在webpack5中已抛弃)

因为我们现在使用的是webpack5的版本,那我们只能在output属性中配置,此选项指定在浏览器中所引用到的资源js/css的前面路径。webpack-dev-server 也会默认从 publicPath 为基准,使用它来决定在哪个目录下启用服务,来访问 webpack 输出的文件。

output: {
    path: resolve('build'),
    filename: 'js/bundle.js',
    clean: true,
    publicPath: '/file'
}

现在重新启服务

我们可以看到,在localhost:8085根目录下并没有获取都编译的静态资源,而是未编译前的index.html模版,我们现在在后面添加一个 /file ,刷新后可以看到如下页面

现在在localhost:8085/file下能找到编译后的index.html文件了,并且js路径前也带上了 /fie。说明 output.publicPath 指定了静态资源如js/css的一个引入路径,以及服务能够访问的路径,在webpack4中一般我们会将 output.publicPath 和 devServer.publicPath 配置一样的,而在5的版本中,它将这两个属性合并到 output.publicPath 上了。

host

默认值是0.0.0.0,在同一个网段下的主机中,通过ip地址是可以访问的,如果设置成127.0.0.1或者localhost,在同一个网段下的主机中,通过ip地址是不能访问的。

localhost 和 0.0.0.0 的区别:

  • localhost:本 质上是一个域名,通常情况下会被解析成127.0.0.1
  • 127.0.0.1: 回环地址(Loop Back Address),表达的意思其实是我们主机自己发出去的包,直接被自己接收

如下我们设置为localhost或者127.0.01,我们只能在本地访问到这个地址

如果设置成0.0.0.0,那么我们可以在同一网段下,访问iPv4这个地址

port

即设置监听的端口,默认情况下是8080

open

即是否打开浏览器,默认false不打开,设置true就会打开

compress

即是否为静态文件开启gzip compression,默认false

当设置true开启,并清空浏览器缓存重新加载

可以看到bundle.js与原先对比小了很多,并且可以看到响应头带有gizp标志,表示这是资源已被gizp压缩了。

proxy

这个配置我们经常会用到,我们会用它来设置代理服务器来解决跨域问题,比如我们在http://localhost:8080 想访问http://locahost:3000 下的接口,因为端口不同所以存在跨域,那我们可以配置该属性来解决。基本属性介绍如下

  • target : 表示的是代理到的目标地址,比如 /api/user会被代理到 http://localhost:3000/api/user
  • pathRewrite : 默认情况下,我们的 /api 也会被写入到URL中,如果想删除,可以使用pathRewrite重写路径,比如'^/api': ''
  • secure : 默认情况下不接收转发到https的服务器上,如果希望支持,可以设置为false
  • changeOrigin :它表示是否更新代理后请求的headers中host地址,默认情况值是被代理到localhost: 3000,如果我们想要让它还是指向被代理地址,那么可以设置true
proxy: {  // 解决跨域,设置代理服务器
  '/api': {
    target: 'http://localhost:3000',
    changeOrigin: true,  // 它表示是否更新代理后请求的headers中host地址, 防止有些服务器的host校验,比如 localhost:8080的请求是从8080请求过来的,但是通过代理后变成从3000请求过来,通过配置此属性让代理后header中的host还是8080
    pathRewrite: {
      '^/api': ''
    },
    secure: false  // 可以转发到https服务器上
  }
}

historyApiFallback

该属性是开发中一个非常常见的属性,它主要的作用是解决SPA页面在路由跳转之后,进行页面刷新时,返回404的错误, 设置true会返回index.html 然后从当前目录下去找

{
  historyApiFallback: true,
  // historyApiFallback: {  // 也可以对象,自定义
    // rewrites: [
    //   { from: '/^/$/', to: '/index.html'}
    // ]
  // }
}

resolve

该属性用来配置引用模块该如何解析

  • extensions : 引用的文件名如果没有后缀,会自动添加后缀名
  • mainFiles : 配置当文件目录时,默认会找目录内的index文件
  • alias : 设置路径别名

相关配置如下

resolve: {
  extensions: ['.js', '.json', '.wasm', '.vue', '.jsx'],  // 自动添加后缀名
  mainFiles: ['index'],  // 当文件目录时,会找目录内的index文件
  alias: {  // 设置路径别名
    '@': resolve('src'),
    'pages': resolve('src/pages')
  }
}

module

module 处理项目中不同类型的模块

主要参数配置如下

rules

rules 属性对应类型是数组,里面放着不同类型的RuleRule是一个对象,对象中可以设置多个属性:

  • test: 用于对 resource(资源)进行匹配的,通常会设置成正则表达式

  • use: 对应的值时一个数组

    • UseEntry 是一个对象,可以通过对象的属性来设置一些其他属性:

      • loader: 必须有一个 loader属性,对应的值是一个字符串;
      • options: 可选的属性,值是一个字符串或者对象,值会被传入到loader中;
      • query: 目前已经使用options来替代\
    • 传递字符串(如: use:[ 'style-loader' ])是 loader 属性的简写方式(如:use:[{ loader: 'style-loader'}]);

  • loader: Rule.use: [ { loader } ] 的简写。

rules 不同的类型模块对应着不同 loader,而这些 loader 为这些模块提供相对应的转换规则,下面介绍几个常用的loader

1.处理样式类型文件

比如处理当js文件中引入css文件时,需要css-loader 来加载 css,安装 npm install css-loader -D

module.exports = {  
  //...
  module: {
    rules: [
      {
        test: /\.css$/,   // 通过正则匹配css文件
        use: [
          {
            loader: 'css-loader'
          }
        ]
        // 如果没有option,可以直接写成 use: ['css-loader']
        // 如果只有一个loader,也可以写成 loader: 'css-loader'
        // 但是最后推荐第一种写法,因为所有简写都会转化成第一种
      }
    ]
  }
}

当我们使用 css-loader 处理css文件并解析后,并不会将解析之后的css插入到页面,因此还需要另一个loader,style-loader,该loader就是将解析后的css存放到style中去,安装 style-loadernpm install style-loader -D

使用style-loader

module.exports = {  
  //...
  module: {
    rules: [
      {
        test: /\.css$/,   // 通过正则匹配css文件
        use: [
          {
            loader: 'style-loader'
          }
          {
            loader: 'css-loader'
          }
        ]
      }
    ]
  }
}

注意:配置的loader多个时,loader处理是又顺序的,它将会按数组从后往前处理,比如上面style-loader放css-loader前面是因为需要先处理css文件,解析后再将css放到style标签内。

在实际项目中我们可能会使用less、sass、stylus的预处理器来编写css样式,比如我们写一个less文件,就需要使用less-loader来加载less,安装lessless-loader: npm install less less-loader -D,配置如下

解析less文件需要less-loader,而less-loader又依赖less进行转换成css

module.exports = {  
  //...
  module: {
    rules: [
      //...
      {
        test: /\.less$/,   // 通过正则匹配less文件
        use: [
          { loader: 'style-loader' },
          { loader: 'css-loader' },
          { loader: 'less-loader' }
          // 因为less-loader解析完less文件后会转化成css文件,因此需要继续用css-loader和style-loader处理css文件
        ]
      }
    ]
  }
}

2.处理资源类型文件

在webpack5之前,当我们处理一些资源类文件时,如图片类型的文件时,我们会使用到一个file-loader,它能将通过 import/require 方式引入的文件资源,放到输出的文件夹内。安装 file-loader ,npm install file-loader -D ,并且配置file-loader

{
  test: /\.png|jpe?g|bmp|svg/i,  
  use: [    
    {      
      loader: 'file-loader'    
    }  
  ]
}

重新build后,会讲资源放在输出目录

如果我们想要将输出的资源文件名根据自定义规则来配置,可以通过PlaceHolders方式处理,webpack给我们提供了很多 PlaceHolders,我们介绍几个主要的PlaceHolders。

  • [ext]: 处理文件的扩展名
  • [name]: 处理文件的名称
  • [hash]: 文件的内容,使用MD4的散列函数处理,生成的一个128位的hash值(32个十六进制)
  • [contentHash]: 在file-loader中和[hash]结果是一致的
  • [hash:<length>]: 截图hash的长度,默认32个字符太长了
  • [path]: 文件相对于webpack配置文件的路径;

比如我们可以参照vue-clie处理资源类文件的配置

{  
  test: /\.png|jpe?g|bmp|svg/i,  
  use: [    
    {      
      loader: 'file-loader',      
      options: {        
        name: 'img/[name].[hash:8].[ext]',
        // outputPath: 'img'  // 可以通过img/方式新建存放资源文件夹也可以通过outputPath定义文件夹
      }    
    }  
  ]
}

重新打包后

当然除了file-loader用于处理资源类文件外,我们也可以使用url-loader。它与file-loader的区别是url-loader能将较小的文件转成base64的Url。安装 npm install url-loader -D ,并配置。

{  
  test: /\.png|jpe?g|bmp|svg/i,  
  use: [    
    {      
      loader: 'url-loader',      
      options: {        
        name: 'img/[name].[hash:8].[ext]'
      }    
    }  
  ]
}

重新打包后

可以看到资源都没放在输出文件夹内了,因为当前资源都转成base64存到bundle.js里面去了。但是实际开发中我会将大的图片依然是保留资源,小的图片会转成base64,因为小的图片转换base64之后可以和页面一起被请求,减少不必要的请求过程,如果大的转成base64反而会影响页面的请求速度。因此我们可以使用url-loader里的limit属性来配置

{  
  test: /\.png|jpe?g|bmp|svg/i,  
  use: [    
    {      
      loader: 'url-loader',      
      options: {        
        name: 'img/[name].[hash:8].[ext]',
        limit: 100 * 1024  // 不超出100kb的图片会被转成base64,超过100kb不会转
      }    
    }  
  ]
}

重新打包后可以看到a图片被转成base64,而b图片依然输出到打包目录下。

进入到webpack5之后,我们不再需要使用file-loader或者url-loader去处理资源文件了,我们可以使用webpack5自带的资源模块类型(asset module type),来代替上面用到的loader。

资源模块类型分为4类

  • asset/resource 发送一个单独的文件并导出 URL,之前通过使用 file-loader 实现
  • asset/inline 导出一个资源的 data URI,之前通过使用 url-loader 实现
  • asset/source 导出资源的源代码,之前通过使用 raw-loader 实现
  • asset 在导出一个 data URI 和发送一个单独的文件之间自动选择,之前通过使用 url-loader,并且配置资源体积limit限制实现

相关配置如下

{  
  test: /\.png|jpe?g|bmp|svg/i,  
  // type: 'asset/resource',  // 类似file-loader
  // type: 'asset/inline',    // 类似url-loader
  type: 'asset',              // 类似url-loader + limit 限制
  generator: {
    filename: 'img/[name].[hash:8][ext]'  // 这里注意[ext]包含了.
  },
  parser: {
    dataUrlCondition: {
      maxSize: 100 * 1024  // 不超出100kb的图片会被转成base64,超过100kb不会转
    }
  }
}

我们也可以在output.assetModuleFilename 配置资源存放路径,比如 output.assetModuleFilename: 'img/[name].[hash:8][ext]' ,但推荐还是将路径配置放在各自的loader内。

对于其他如字体资源我们可以用asset/resource来处理

{  
  test: /\.(ttf|eot|woff2?)$/i,
  type: 'asset/resource',
  generator: {
    filename: 'font/[name].[hash:6][ext]'  // 这里注意[ext]包含了.
  }
}

3.处理vue文件

当我们在使用vue框架时候我们会编写vue文件,那我们如何配置呢,那就是vue-loader,该loader专门用于配置.vue文件 ,安装 npm install vue npm install vue-loader -D ,因为我使用的vue3版本的所以vue2 依赖的用于template的模板编译的 vue-template-compiler 包不再需要,而是替换成 @vue/compile-sfc ,而该依赖包会在安装 vue3 版本时自动安装,webpack 配置如下。

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

相关vue代码如下

plugins

plugins 选项用于以各种方式自定义 webpack 构建过程,下面介绍几个常用的plugin

webpack自带插件

  • DefinePlugin 允许在编译时创建配置的全局常量

第三方插件

  • CleanWebpackPlugin 清理构建目录
  • HtmlWebpackPlugin 对HTML打包处理
  • CopyWebpackPlugin 对文件进行复制
  • MiniCssExtractPlugin 抽离css文件 

下面就简单介绍下插件的配置

CleanWebpackPlugin

该插件会在构建前对构建目录删除,安装 npm install clean-webpack-plugin -D,对应配置

const { CleanWebpackPlugin } = require('clean-webpack-plugin')
//...
{  
  plugins: [
    new CleanWebpackPlugin()
  ]
}

HtmlWebpackPlugin

该插件会对html文件进行处理打包,安装 npm install html-webpack-plugin -D ,对应配置

const HtmlWebpackPlugin = require('html-webpack-plugin')
//...
{  
  plugins: [
    //...
    new HtmlWebpackPlugin({
      title: '测试title'
    })  
  ]
}

重新打包后,会在构建目录创建一个index.html文件,并且自动添加了bundle.js

而这个index.html其实是通过 html-webpack-plugin 中的ejs模板进行构建,当我们没有自定义模板时,该插件会有默认模板 default_index.ejs 去创建,可查看插件源码可得知

但实际项目中我们会通过自定义模板去创建index.html,比如我们会在项目下创建public文件夹,在里面创建一个index.html文件。

里面包含了一些 <%= %> 语法为ejs填充模板,htmlWebpackPlugin.options.title 即为 htmlWebpackPlugin 插件入参,因此我们使用引入自定义配置

const HtmlWebpackPlugin = require('html-webpack-plugin')
//...
{  
  plugins: [
    //...
    new HtmlWebpackPlugin({
      title: '测试title',
      template: './public/index.html',
      inject: true,  // 设置打包资源注入位置
      cache: true, // 设置为true,只有当文件改变时,才会生成新的文件(默认值也是true)
      minify: {}
    })  
  ]
}

DefinePlugin

该插件为webpack内置插件,会在编译时创建配置的全局常量,比如我们的自定义模板中用到了BASE_URL 全局变量,因为我可以为这个变量做配置

const { DefinePlugin } = require('webpack')
//...
{  
  plugins: [
    //...
    new DefinePlugin({
      BASE_URL: "'./'"
    })
  ]
}

想要使用字符串值,必须在字符串外面再包一层引号,因为会取引号里面的一层数据。

CopyWebpackPlugin

该插件用于复制文件从一个文件夹到另一个文件夹,比如我们在 public 文件夹中包含 favicon.ico 文件,我们会将它复制到构建目录下。安装 npm install copy-webpack-plugin -D 。

  • 复制的匹配规则在 patterns 中设置
  • from: 设置从哪一个源中开始复制
  • to: 复制到的位置,可以省略,会默认复制到打包的目录下
  • globOptions: 设置一些额外的选项,其中可以编写需要忽略的文件
    • .DS_Store: mac目录下会自动生成的一个文件
    • index.html: 也不需要复制,因为我们已经通过 HtmlWebpackPlugin 完成了index.html的生成
const CopyWebpackPlugin = require('copy-webpack-plugin')
//...
{  
  plugins: [
    //...
    new CopyWebpackPlugin({
      patterns: [  // 匹配
        {
          from: './public',   // 拷贝目录或文件 默认到构建目录下
            globOptions: {  // 拷贝配置
            ignore: [  // 需要忽略的文件
              '**/index.html',  // 需要加 ** 表示from的目录下
              '**/.DS_Store'
            ]
          }
        },
      ],
    })
  ]
}

MiniCssExtractPlugin

该插件用于将css文件单独抽离,我们需要安装npm install mini-css-extract-plugin -D,配置插件如下

plugins: [
  new MiniCssExtractPlugin({
    filename: 'css/[name].[contenthash:6].css',
    chunkFilename: 'css/chunk.[name].[contenthash:6].css'
  })
]

除此我们还需要在loader中将style-loader生成环境中改为使用MiniCssExtractPlugin.loader

use: [
  isProduction ? MiniCssExtractPlugin.loader : 'style-loader',
  {
    loader: 'css-loader',
    options: { importLoaders: 1 }
  }
  'postcss-loader'
]

配置完重新打包,可以看到已经独立出css文件了

optimization

用于配置代码优化的属性,我们来介绍一下其中比较常用的属性

  • minimizer :用于压缩工具,可以添加一个或多个定制过的 TerserPlugin 实例
  • splitChunks : 通用分块策略(common chunk strategy)。可在 SplitChunksPlugin 页面中查看配置其行为的可用选项。
  • chunkIds : 告知 webpack 当选择模块 id 时需要使用哪种算法。将 optimization.chunkIds 设置为 false 会告知 webpack 没有任何内置的算法会被使用
    • natural : 按照数字的顺序使用id
    • named : development下的默认值,一个可读的名称的id
    • deterministic : 确定性的,在不同的编译中不变的短数字id
      • 在webpack4中是没有这个值的
      • 那个时候如果使用natural,那么在一些编译发生变化时,就会有问题
      • 开发过程中,我们推荐使用 named
      • 打包过程中,我们推荐使用 deterministic
  • runtimeChunk :配置runtime相关的代码是否抽取到一个单独的chunk中,runtime相关的代码指的是在运行环境中,对模块进行解析、加载、模块信息相关的代码,比如我们入口文件是main.js,引入了一个工具库utils.js,通过import加载utils.js的代码加载就是runtime完成的,有利于浏览器缓存,假如我们改变的utils.js工具库,但是我们的main.js和runtime就不会变动
    • true/multiple : 针对每个入口打包一个runtime文件
    • single : 打包一个runtime文件
    • 对象 : name属性决定runtimeChunk的名称

环境分离

在实际情况下,我们会对webpack.config.js做一个分离,即开发环境和生产环境,实现思路,我们将新建一个config文件夹,里面新建三个文件分别是,webpack.common.js ,webpack.dev.js ,webpack.pro.js 。我们会将公共的配置抽离到 webpack.common.js 这个文件中,开发相关的配置抽离到 webpack.dev.js 中,生产相关的配置抽离到 webpack.pro.js 中。

现在我们可以在package.json中分别为不同的环境配置对应的打包命令

"scripts": {
  "serve": "webpack serve --config ./config/webpack.common.js --env development",
  "build": "webpack --config ./config/webpack.common.js --env production"
}

可以看到我们现在配置了开发环境serve和生成命令build,并且都传入了 --env 这个参数,那么我们在webpack.common.js中不能使用 module.exports = {} 这种方式了,我们现在可以使用一个函数并返回配置对象的方式,返回的参数中,包含了 --env 后面携带的参数。

我们现在看到 webpack.common.js 里的module.exports 接收一个函数,当我们执行开发环境命令是,打印信息如下

可以看到返回的参数对象里包含了development这个key并且值为true,因此我们可以通过参数来判断是否是生产环境还是开发环境。下面是抽离的参考代码

// webpack.common.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { VueLoaderPlugin } = require('vue-loader')
const { DefinePlugin } = require('webpack')
const resolve = require('./path')

const { merge: webpackMerge } = require('webpack-merge')
const devConfig = require('./webpack.dev')
const proConfig = require('./webpack.pro')

const commonConfig = {
  context: resolve(''),
  entry: './src/main.js',
  output: {
    path: resolve('build'),
    filename: 'js/bundle.js',
    clean: true,
    // publicPath: '/file'
  },
  resolve: {
    extensions: ['.js', '.json', '.wasm', '.vue', '.jsx'],  // 自动添加后缀名
    mainFiles: ['index'],  // 当文件目录时,会找目录内的index文件
    alias: {  // 设置路径别名
      '@': resolve('src'),
      'pages': resolve('src/pages')
    }
  },
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: [
          'style-loader',
          {
            loader: 'css-loader',
            options: {
              importLoaders: 1  // css中引入css不会从postcss-loader开始而是从css-loader转换开始,因此该配置保证loader向上一层开始转换
            }
          },
          'postcss-loader'
        ]
      },
      {
        test: /\.less$/,
        use: [
          'style-loader',
          'css-loader',
          'postcss-loader',
          'less-loader'
        ]
      },
      {
        test: /\.png|jpe?g|bmp|svg/i,
        type: 'asset',
        generator: {
          filename: 'img/[name].[hash:8][ext]'
        },
        parser: {
          dataUrlCondition: {
            maxSize: 100 * 1024
          }
        }
      },
      {
        test: /\.(j|t)sx?$/,
        exclude: /node_modules/,
        use: [
          'babel-loader', 
        ]
      },
      {
        test: /\.vue$/,
        use: 'vue-loader'
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: '测试title',
      template: './public/index.html'
    }),
    new DefinePlugin({
      BASE_URL: "'./'"
    }),
    new VueLoaderPlugin()
  ]
}

module.exports = (env) => {
  console.log(env)
  process.env.NODE_ENV = env.production ? 'production' : 'development'
  const mergeConfig = env.production ? proConfig : devConfig
  return webpackMerge(commonConfig, mergeConfig)
}
// webpack.dev.js
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin')

module.exports = {
  mode: 'development',
  devtool: 'cheap-module-source-map',
  devServer: {
    hot: 'only',
    host: '0.0.0.0',
    compress: true,
    proxy: {  // 解决跨域,设置代理服务器
      '/lyj': {
        target: 'http://localhost:3000',
        changeOrigin: true,  // 它表示是否更新代理后请求的headers中host地址, 防止有些服务器的host校验,比如 localhost:8080的请求是从8080请求过来的,但是通过代理后变成从3000请求过来,通过配置此属性让代理后header中的host还是8080
        pathRewrite: {  // 重写路径
          '^/lyj': ''
        },
        secure: false  // 可以转发到https服务器上
      }
    },
    historyApiFallback: true
  },
  plugins: [
    new ReactRefreshWebpackPlugin()
  ]
}
// webpack.pro.js
const TerserPlugin = require('terser-webpack-plugin')  // 压缩js代码插件 webpack5自带

module.exports = {
  mode: 'production',
  optimization: {
    minimizer: [
      new TerserPlugin({
        extractComments: false,  // 打包后的 LICENSE.txt 注释文件去掉
      })
    ]
  }
}