前端学习笔记(十三)--webpack学习

1,093 阅读17分钟

今天学习 webpack 4,希望没学错版本。
学习材料是官方文档

1. webpack 介绍

webpack 是一个模块打包工具,从一个入口模块开始,生成依赖图。最后把整个项目打包成多个浏览器可用的文件。

1.1 webpack 安装

  1. 用 npm 安装即可,安装到开发版本。
  2. webpack 4 版本还需要安装 webpack-cli
    fsevents 警告可无视,那是 mac 的包,属于 optional dependencies。
  3. 常用的 npm 脚本,之后解释:

1.2. webpack 相关核心概念

先对重要概念做一个简单介绍。

1.2.1 入口(entry)

webpack 从入口模块开始,构建出一个完整的依赖图。默认情况下,该入口文件为 src/index.js
可以通过 webpack.config.js 文件修改。

// webpack.config.js
module.exports = {
    entry: "xxx/xxx/myEntry.js"
}

1.2.2 输出(output)

output 属性指明输出文件的路径,以及文件名。
主要输出文件的默认路径以及文件名为 dist/main.js,其他文件默认路径也是在 dist 文件夹下。

let path = require("path"); // node.js 核心模块,用于操作路径
module.exports = {
    entry: "xxx/xxx/myEntry.js",
    output: {
        path: path.resolve(__dirname, "dist"),
        filename: "my-webpack.bundle.js"
    }
}

1.2.3 loader

其实 webpack 本身只能处理 js 和 json 文件。是 loader 让其能够处理各种其他文件,将各种文件类型转换为有效模块

let path = require("path");
module.exports = {
    entry: "xxx/xxx/myEntry.js",
    output: {
        path: path.resolve(__dirname, "dist"),
        filename: "my-webpack.bundle.js",
    }
    module: {
    	"rules": [ // 这是列表
            {"test": /\.txt$/, "use": "raw-loader"} // 正则表达式不加引号
        ]
    }
}
  1. 如上,在 module.rules 里定义 loader,test 表示这个 loader 要对哪个或哪些文件处理,use 表示要用哪个 loader 进行类型转换。
  2. 以上代码表示,当 webpack 执行处理时遇到某个 require 或者 import 中包含 .txt 的路径时,先使用 raw-loader 进行转换。

1.2.4 插件(pulgin)

loader 用于转换文件,而插件用于范围更广的任务,打包优化,资源管理等等。

let downloadedHTMLPulgin = require("downloaded-plugin"); // 通过 npm 安装的插件
let webpack = reuire("webpack"); // 这是用于访问内置插件,webpack 本身也提供了很多内置插件

module.exports = {
    module: {
    	"rules": [ // 这是列表
            {"test": /\.txt$/, "use": "raw-loader"} // 正则表达式不加引号
        ]
    }
   pulgins: [
       new downloadedHTMLPlugin({template: 'src/index.html'}) // html 插件处理 html 文件
   ]
}

1.2.5 模式(mode)

webpack 对不同模式环境有优化,通过 mode 设置模式。
参数为 developmentproductionnone 之一。

module.exports = {
    mode : "production"
}

1.2.6 浏览器兼容性

webpack 兼容所有 ES5 以上的浏览器。(IE8 及以下不支持)
如果要使用 import() 和 require.ensure()(即动态导入),则浏览器还需要支持 promise(ES6)。

2. 基础配置

2.1 起步

  1. 创建 ./index.html./src/index.js

    可以看到 index.js 里隐式引用了 lodash,使用 _ 全局变量。
    依赖是从 index.html 的头部引入的外部链接。
    如果不使用 webpack,这会有问题:
    1. 没法直接看出来引用了外部库。
    2. 依赖不存在,或者引用顺序不对的情况,会出错。
    3. 引用了但是没使用的情况,浏览器就会下载无用代码。
  2. 在 package.json 里把包改成私有的,且移除 main 属性,防止意外发布。

2.2 创建一个 bundle

  1. 首先对项目目录介绍。
    • ./src 指的是源代码,是用于书写编辑的代码。
    • ./dist 指的是分发代码,是构建过程产生的最优化代码,最终将在浏览器中运行。
  2. 于是将 index.html 放入 dist 文件夹中。
  3. 安装 lodash 的 npm 包。
  4. 在 index.js 里 import 刚安装的 lodash 包。
  5. 更新 index.html 文件,删除之前的所有引用。包括对 index.js 的引用
  6. 在 index.html 中添加对一个叫 main.js 的引用,这个 js 文件将由 webpack 自动打包生成

    既然是生成的文件,所以也将要在 dist 目录中。
  7. 由于在 index.js 文件中显式 import 了 lodash,webpack 能够以此生成依赖图,并实行优化。
  8. 执行 npx webpack 进行构建。
    npx 是运行包的命令,这里表示运行 webpack。
  9. 构建成功后可以看到 dist 里生成了 main.js,运行 index.html 确实没问题。

2.3 import 的写法支持

因为最终使用的是生成的 main.js,因此 index.js 里可以使用 import 和 export,即使浏览器不支持 ES6。(但是 ES6 的其他语法,webpack 并没有对应转换)

2.4 使用配置文件

尽管从 webpack 4 开始,可以不使用配置文件。但是大多数项目还是需要复杂的配置的。

  1. 在项目主目录里创建 webpack.config.js 文件。
  2. 运行 npx webpack --config webpack.config.js,其实不用加参数这就是默认行为(就像刚刚做的)。不过可以通过这个参数使用其他配置文件。

2.5 npm 脚本使用

就像之前提到的:

"scripts": {
        "build": "webpack --config webpack.config.js"
}

使用 npm run build,无需使用 npx,因为在 npm scripts 环境中也可以使用包名。

3. 资源管理

webpack 的一大优点就是可以打包除了 JS 类型以外的文件。这部分讲如何处理这些文件。

3.1 CSS 文件

  1. 先 npm 安装,然后在配置文件中加入这些 loader。
module: {
        rules: [
            {
                test: /\.css$/,
                use: [
                    "style-loader",
                    "css-loader"
                ]
            }![](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/193988eb84bf4079898f1abf4d52f3d1~tplv-k3u1fbpfcp-watermark.image)
        ]
    }
  1. index.js 中 import 要用到的 css 文件(放在 src 目录中)。

3.2 图像文件

  1. 首先自然是下载 file-loader 并写入配置文件。
  2. 在 index.js 里导入,作为 url 使用。

3.3 字体文件

  1. file-loaderurl-loader 可以加载任何类型的文件。在配置文件里加入以下代码。
  2. 然后直接在 css 里使用 @font-face 引用字体文件即可。

3.4 数据文件

3.4.1 JSON 文件

JSON 文件是默认支持,不需要导入 loader。

3.4.2 其他数据文件

像是 CSV/TSV,XML 都有对应的 csv-loaderxml-loader
被导入 js 文件后,这些都会被解析成为可直接用的对象。

4. 输出管理

  • 如果说资源管理是对 src 文件夹的操作,那么输出管理就是对 dist 文件夹的操作。
    之前我们是通过手动往 index.html 引用 main.js 来处理,那如果有多个 bundle.js 怎么办,不可能一一手动添加删除吧,能不能自动生成 index.html 呢,可以。这个问题可以通过一些插件解决

4.1 准备

  1. 往 src 里添加另一个 js 文件。如 print.js。更改 index.js 导入 print.js 里的函数。
  2. 更改 index.html 导入将要生成的 JS 文件。
  3. 更改配置文件,注意 entry 和 output 对于多个文件的写法:
  4. 此时可以 build 一下,没有问题。

4.2 HtmlWebpackPlugin 使用

以上操作当然没有问题,但是如果我们想要更改入口名字了,或者添加新的入口怎么办。我们就要用 html-webpack-plugin 插件完成。

  1. 首先自然是使用 npm/yarn 安装插件,插件名字 html-webpack-plugin
    这里注意三点:
    • HtmlWebpackPlugin 是默认导出。
    • plugins 的值是列表。
    • title 指的就是生成 index.html 文件的 title。
  2. HtmlWebpackPlugin 会在 dist 里生成 index.js 文件,如果 dist 里有别的 index.js,会被这次自动生成的覆盖。
  3. 再次构建,看看结果(自动生成的文件空格会被压缩,这里展开了):
    自动添加了生成的 js 文件。

4.3 dist 文件夹清理

有些时候 dist 文件夹会遗留一些之前生成的文件,之后生成不再输出这些文件。于是就要在每次构建之前进行清理。使用 clean-webpack-plugin 插件。

  1. 安装。
  2. 配置,注意导入时用大括号,这个函数不是默认导出,因此函数名也不能写错。
  3. 构建。

5. 开发环境配置

为了让开发环境更加轻松要做的事。

  • 首先在配置文件中加入 mode 属性,值为 "development"。

5.1 source map

由于 webpack 被编译为 bundle.js,当文件出错时,error 和 warning 会被追溯到 bundle.js,而不是源文件。而 source-map 功能可以显示错误的来源。

  1. 在配置文件里添加 devtool 属性,值有多种选择,不同值的错误定位准确度和速度不一样,对比参考这里,但是现在先使用 "inline-source-map",准确度高,速度慢。
  2. 在 print.js 里写个 bug,构建后在浏览器里运行,发现报错会引用到源文件的位置。

6. 开发工具选择

开发工具的目的是,当我们每次保存文件后,都能够自动重新构建(并刷新页面),而不是每次都要手动运行 npm run build 再手动刷新页面。webpack 提供了三种开发工具:

  • webpack watch mode
  • webpack-dev-server
  • webpack-dev-middleware

6.1 wecpack watch mode(观察模式)

观察模式的作用是观察到目录中有文件变化的时候自动构建。
这是 webpack 提供的,直接在 npm scripts 里加入以下脚本即可:
然后运行该脚本,会发现命令行不退出了,此时改变文件会发现会自动开始构建。
(注:如果没有设置 cache 属性(即默认 true),index.html 会被删除并且不重新生成,因为 watch mode 的重新构建只构建改变过的文件,如果 HtmlWebpackPlugin 使用的模板没有改变,则不会生成)
(注2:如果使用 web-dev-server 不会有这个问题)

6.2 web-dev-server

观察模式只会重新构建,但是每次重新构建后我们还需要手动刷新网页,webpack-dev-server 解决的就是这个问题。
(注:效果和观察模式 + live-server 一样,不过 live-server 不能解决观察模式里说的 index.html 的问题)

  1. npm 安装到 DevDependencies。
  2. 添加配置,告诉要刷新的网页的目录,即 dist:
  3. 添加 npm scripts:

    (注:webpack-cli 3 版本这个命令为 webpack-dev-server --open
    (注2:--open 的作用是构建后立刻在浏览器中打开网页)
    (注3:默认端口是 8080)
  4. 运行。 (注:webpack-dev-server 不会在 dist 中生成文件,而是使用内存,因此 dist 文件夹是空的)

6.3 webpack-dev-middleware

自定义版 web-dev-server,如果非要用的话参考这里

7. 模块热替换(HMR)

这是 webpack 的一大优点。可以在运行时更新所有的模块,而不需要完全刷新浏览器页面。

7.1 启用 HMR

  1. 修改配置。HMR 属于 webpack 内置插件,因此需要引入 webpack。
  2. 修改 index.js,以便当 print.js 变化时,告诉 webpack。
  3. 修改 print.js,可以看到控制台输出变成了这样:

    没有前缀的就是我们自己的输出。
  4. 但是此时发现,绑定到按钮触发事件的函数并没有刷新。因此还需要重新绑定才行。

7.2 loader 相关

  1. style-loader 内部含有 HMR 相关。因此修改 css 会自动热替换。
    (在测试的时候,顺便学到了 css 里背景颜色只能放在最后一层。以及好看的 screen 叠加模式)
  2. 很多库也会提供 loader,比如说 vue-loader,开箱即用。

8. tree-shaking

用于清理死代码的,具体见这里。死代码是被导入但是没有被使用的模块。

8.1 开启 tree shaking

optimization 设置里给 useExports 属性设置为 true 即可开启。

optimization: {
  usedExports: true
}

8.2 sideEffects

有些模块文件被导入之后并不需要被使用,而是在模块内部执行一些命令,不能被清理。那么需要对这些模块单独设置。保证这些模块不会被清理。

sideEffects: [
	"./xxx.js",
	"./xxx.js"
]

8.3 注意事项

tree shaking 只能用于 es6 语法的模块系统,因为需要静态分析。

9. 生产环境

生产环境和开发环境需要的配置很不同。因此需要单独的配置文件。
因此一共建立三个配置文件,一个通用,两个分别对应 dev 和 prod 的独立配置。并使用 webpack-merge 完成整合。文件名分别命名为:
名字最好不要改,这种命名会被 vscode 一些插件识别并加上图标。

9.1 配置文件更改

  1. 把devtool,devserver,HMR 相关配置和变量从 common 中移到 dev 中。
  2. dev 和 prod 各自写上 mode 的对应值。
  3. 使用 merge 引入 common 的内容。

9.2 npm scripts 更改

用 config 参数更改为对应的配置文件。build 一般对应生产环境。

9.3 mode 相关

指定的 mode 的值可以通过 process.env.NODE_ENV 访问(也就是环境变量)。很多库会参考这个值进行不同的优化。

  • 有些库会在 development 环境时生成日志和测试文件;
  • 有些库会在 production 环境时进行代码优化;
  • 此外,不同 mode 也会安装一些插件,也会被一些库用到。 因此指定 mode 很重要,不指定会有 warning。(注:不过在配置文件中无法访问这个环境变量,比如没法在 output 中根据 mode 环境改变生成文件名字)
    其他 js 文件可以访问 process.env.NODE_ENV

9.4 其他

9.4.1 压缩

生产环境会压缩输出文件。mode 选择为 production 后,webpack 4 会默认用 TerserPlugin 压缩文件。

9.4.2 source-map

生产环境仍然需要 source map,用于 debug 和基准测试。关于 source map 方法的选择,简单说不要选择以 inline- 和 eval- 开头的。具体还是参考之前 5.1 部分提到的文章。

9.4.3 css 压缩

webpack 5 的话这里是自动压缩的,webpack 4 则需要插件的辅助,诸如 optimize-css-assets-webpack-plugin。需要注意的点是,使用 css 压缩器后会覆盖掉默认的压缩设置,即 js 压缩就会没有了,因此还需要手动添加 js 压缩器(诸如 terser-webpack-plugin)。
(注:压缩 css 的诉求只有当你使用插件把 css 分离出来的时候有用,分离 css 需要插件 mini-css-extract-plugin

以上具体参考这个部分。注意压缩器插件写在 optimization.minimizer 里而不是 plugins

10. 代码分离

这是 webpack 另一个瞩目的优点(上一个这么说的是 HMR)。
把代码分成多个 bundle,然后按需只加载部分 bundle,或者并行加载多个 bundle,减少加载时间。

10.1 entry 分离

如果通过 entry 手动分离不同的 xxx.bundle.js,则会有一些问题:

  • 如果多个入口文件都引用了同一个模块,这些模块都会被引入到各自的 bundle 中,产生重复。(非入口文件不用担心)
  • 不够灵活,无法动态调整。

10.2 防止重复

为了解决第一个问题,重复引入。使用 SplitChunksPlugin 插件去重。 此插件是及集中在 webpack 4 中,直接在 optimization 中设置 splitChunks 即可。

optimization: {
	splitChunks: {
    	chunks: "all"
    }
}

构建后可以看到此插件把 lodash 分离出来为单独的一个 js 文件。
(之前提到的 css 分离插件也是属于代码分离的范围)

10.3 动态导入

有些时候是否导入模块或者导入模块的名字是运行时决定的等等,此时需要使用动态导入模块。
webpack 虽然支持给旧浏览器使用 import,但是 import() 因为使用了 promise,所以如果要在旧浏览器中使用需要 ployfill。

10.3.1 修改配置

首先只留一个入口文件,然后在 output 里添加 chunkFilename 属性,这个属性决定的是非入口文件的 bundle 的名字。

10.3.2 修改 index.js

不再静态导入 lodash。然后变成异步函数

需要注意的是红色箭头处的用法。

  • 注释会被解析,可以给生成的文件命名,这里的 webpackChunkName 即为配置文件里 output 里的非入口文件命名的 [name]的一部分。生成文件名为 vendors~lodash.bundle.js
  • import() 的解析结果不能被直接用,而是要专门提取 default 出来,这是动态导入 commonJS 模块独有的要求。静态导入如之前所见直接 import _ from require("lodash") 即可。非 commonJS 模块下一部分里有截图。

11. 懒加载/延迟加载 lazyload

也是一种代码分离的用法,把代码分离开来,某些代码只在用户进行某些操作后加载。比如说电商网站的长图片,只有当往下翻的时候,图片才会被加载。加快了应用的初始加载速度。
继续之前的例子,lodash.bundle.js 确实是实现了代码分离,但是实际上每次打开网页的时候都会被加载。因此我们创建一个按钮,只在按钮被点击的时候加载 lodash。

  1. 为了方便知道是否加载,创建了一个新的模块文件 print.js,被加载的时候会打印内容。
  2. 更改 index.js,注意此时由于不是 commonJS 模块,动态导入可以直接使用了。

    要素过多,一一解释:
    • index.js 和 print.js 一样都导入了 lodash,但是因为我们有在配置阶段使用 splitChunks 去重了,所以没关系。
    • 因此左边生成了 3 个文件,其中 vendors~app.bundle.js 就是去重阶段打包的 lodash。
    • 因为此时 import() 导入的 print.js 不是 commonJS 模块,不需要解析成 { default: xx } 格式。
  3. 打开网页可以看到,一开始控制台没有输出,说明 print.js 没有加载。点击后可以看到加载信息。

11.1 让用户知道正在加载

懒加载在等待加载(即这个例子中点击按钮后)的时候应该提供信息让用户知道正在加载,比如转圈圈啥的。

11.2 不同框架的建议

不同框架对于如何使用懒加载都有自己的建议,如 vue 的针对懒加载的建议

12. 缓存

使用 webpack 会生成一堆可部署文件在 dist 文件夹中,把这些文件部署在服务器中后,浏览器等 client 就可以访问这些资源了。其中获取资源会占用很多时间。因此浏览器会缓存很多资源。因此经常会有一些恼人的情况相信每个人都有经历过,就是服务器资源更新了,客户端还没有更新,除非更改文件名,你也不可能要求用户自己去按 ctrl+F5。
本部分就是为了让客户端能够继续使用缓存功能,但是当服务器更新的时候也能随着一起更新。

12.1 替换文件名

使用 output.filename 的 substitutions 属性。其实之前也用过,[name]就是之一。
[contenthash]可以帮我们决定唯一文件名,写法如下:
[contenthash] 使用整个文件计算出一个唯一的 hash 值,资源不变 hash 不变,资源变更 hash 变更。

12.2 chunk 分离

之前使用的 splitChunkPlugin 就是其中的一种。

12.2.1 runtimeChunk

optimization 里设置 runtimeChunk,会把所有的入口文件的 runtime 代码分离出来一个 chunk。而设置值为 single,则会把所有的 runtime chunk 打包为一个 runtime bundle。

12.2.2 splitChunkPlugin 深入

使用 splitChunkPlugin 的优点不只是减少了重复导入。因为这些打包的第三方库基本不会更改,因此分离出的chunk bundle的内容不会改,因此 hash 值不会改,因此客户端会缓存这些内容。
生产环境配置文件里改成如下,通过 cacheGroups 达到以上描述的目的:

12.3 模块标识符

有些时候,即使第三方库没有任何改变,vendor bundle 的 hash 值还是会改变。这是因为每个模块的 module.id 会根据解析顺序不同而变化,因此有些时候解析顺序变化了 hash 值也会变化(hash 值也会把 id 一起包括计算)。

  • 为了解决这个问题,我们要靠插件 HashedModuleIdsPlugin,这是 webpack 内置插件,使用和 HMR 一样的导入方法。不过这个要导入的是生产环境配置。

13. 最佳实践

其他部分超出我的能力了,直接跳到这一部分。虽然这部分其实也有部分不懂的

13.1 loader

给 loader 添加路径,以便减少路径判断。增加构建速度。

13.2 引导时间(bootstrap)

感谢 webpack 教我英文
每个 loader 和 plugin 的使用都会消耗时间,尽量减少工具的使用。

13.3 小就是快

尽量减少编译结果文件大小

13.4 cache loader

使用 cache-loader 启用持久化缓存,使用 npm scripts 里的 "postinstall" 清除缓存目录。 参考这篇文章。要注意只有开销大的 loader 需要用 cache-loader 来缓存,因为加载 cache-loader 本身也有性能损消耗。

13.5 开发环境

13.5.1 source map

大多数情况下,开发环境最佳选择是 cheap-module-eval-source-map

13.5.2 开发时避免生产环境的某些工具

比如说 terserPlugin 的压缩,对于开发环境就没有意义。主要避免以下工具:

  • TerserPlugin
  • ExtractTextPlugin
  • [hash] / [chunkhash]
  • AggressiveSplittingPlugin
  • AggressiveMergingPlugin
  • ModuleConcatenationPlugin

13.5.3 避免额外的优化步骤

webpack 的有些优化,对于小型代码库很好,但是对于大型代码库反而会出问题,可以禁用以下工具:

optimization: {
    removeAvailableModules: false,
    removeEmptyChunks: false,
    splitChunks: false,
}

13.5.4 不输出路径信息

当项目包含数千个模块的时候,输出路径信息会对垃圾回收有性能影响。

13.6 生产环境

13.6.1 source-map

是否真的需要 source-map?

13.6.2 工具影响

babel,TypeScript,sass 对于性能都会有一些影响。