灏天阁

webpack5之核心配置梳理

· Yin灏

安装依赖  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,指定打包入口和打包出口,主要配置的相关命令参数和功能如下

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

具体其他命令参数可参考官方文档  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 注释文件去掉
      }),
    ],
  },
};

- Book Lists -