深入 Webpack5 等构建工具系列一 - webpack 初体验

811 阅读16分钟

这是我参与8月更文挑战的第13天,活动详情查看:8月更文挑战

写在前面

本系列文章核心内容:

  • webpack 核心配置深入解析;
  • webpack 常用 LoadersPlugins 深入学习;
  • 自定义 webpack 中自己的 LoadersPlugins
  • Babel 各种用法以及 polyfillTypeScript 的支持;
  • ESLint 的配置规则以及在 VSCodewebpack 中的使用;
  • 各种性能优化方案:打包抽取分包、Tree Shaking、动态链接库、CDNgzip 压缩等等;
  • webpack 模块化原理解析、打包原理实现;
  • 掌握其它流行构建工具:gulprollupVite

一 (了解)为什么会出现 webpack

1.1 前端发展的几个阶段

  • 无论是作为专业的开发者还是接触互联网的普通人,其实都能深刻地感知到 Web 前端的发展是非常快速的
    • 对于开发者来说我们会更加深有体会;
    • 从后端渲染的 JSPPHP,到前端原生 JavaScript,再到 jQuery 开发,再到目前的三大框架 VueReactAngular
    • 开发方式也从原来的 JavaScriptES5 语法,到 ES6ES7ES8ES9ES10ES11ES12,到 TypeScript,包括编写 CSS 的预处理器 lessscss 等等;

前端发展的几个阶段

1.2 前端开发的复杂化

  • 前端开发目前我们面临哪些复杂的问题呢?
    • 比如开发过程中我们需要通过模块化的方式来开发;
    • 比如会使用一些高级特性来加快我们的开发效率或安全性,比如通过 ES6+TypeScript 开发脚本逻辑,通过 sassless 等方式编写 css 样式代码;
    • 比如开发过程中,我们还希望实时地监听文件的变化并且反映到浏览器上,提高开发的效率;
    • 比如开发完成后我们还需要将代码进行压缩、合并以及其它相关的优化
    • 等等

1.3 前端三个框架的脚手架

  • 目前前端流行的三大框架:VueReactAngular
    • 但是事实上,这三大框架的创建过程都是借助于脚手架(CLI)的;
    • 事实上 Vue-CLIcreate-react-appAngular-CLI 都是基于 webpack 来帮助我们支持模块化、lessTypeScript、打包优化等的;
    • 三个框架中关于 webpack 配置的文件:

三大框架中关于 webpack 的配置文件

可见,我们日常工作根本是无法离开 webpack 的。

二 (理解)webpack 是什么以及工作中应用

2.1 webpack 是什么

Webpack is a static module bundler for modern JavaScript applications.

  • webpack 是一个静态的模块化打包工具,用于现代的 JavaScript 应用程序;
  • 我们来对上面的解释进行拆解:
    • 打包(bundler):webpack 可以帮助我们进行打包,所以它是一个打包工具;
    • 静态的(static):这样表述的原因是我们最终可以将代码打包成最终的静态资源(部署到静态服务器);
    • 模块化(module):webpack 默认支持各种模块化开发,ES ModuleCommonJSAMD 等;
    • 现代的(modern):我们前面说过,正是因为现代前端开发面临各种各样的问题,才催生了 webpack

webpack 官网图片:

webpack 打包过程总览

2.2 工作中的 webpack

  • 日常工作中,比如在开发 vuereactangular 等项目的过程中,我们需要一些特殊的配置,比如给某些目录结构起别名,让我们的项目支持 sassless 等预处理器,希望在项目中手动的添加 TypeScript 的支持,都需要对 webpack 进行一些特殊的配置工作。
  • 除了日常工作之外,如果我们希望在原有的脚手架上进行一些自己的特殊配置以提升性能,比如:安装性能分析工具使用 gzip 压缩代码引用 cdn 的资源公共代码抽取等等,甚至包括编写属于自己的 loaderplugin
  • 对于想要在前端领域进阶成为高级前端开发工程师,甚至是架构师的前端开发者来说,webpack 等构建工具是必须学习的,包括其中的一些高级特性和原理,都是要熟练掌握的。企业在招聘高级前端工程师或者架构师时,必然会对 webpack 和其它的构建工具有比较高的要求。

三 (理解)webpack 会被 Vite 取代吗

3.1 webpackVite

  • 我们来提一个问题:webpack 会被 Vite 取代吗?
    • Vite 推出后确实引起了很多的反响,也有很多人看好 Vite 的发展(俺也是);
  • 但是目前 Vite 取代 webpack 还有很长的路要走
    • 目前 vue 的项目支持使用 Vite,也支持使用 webpack
    • ReactAngular 的脚手架目前没有支持,暂时也没有转向 Vite 的打算;
    • Vite 最终打包的过程,依然需要借助 rollup 来完成;
  • Vite 的核心思想并不是首创
    • 事实上,Vite 的很多思想和之前的 snowpack 是重合的,而且相对目前来说,snowpack 会更加成熟;
    • 当然,后续发展来看,Vite 可能会超越 snowpack
  • webpack 的更新迭代
    • webpack 在发展的过程中,也会不断改进自己,借鉴其它工具的一些优势和思想;
    • 在这么多年的发展中,无论是自身的优势还是生态都是非常强大的;

红元老师的个人观点:

  • 学习任何东西,重要的是学习核心思想:
    • 我们不能学会了 JavaScript,有一天学习 TypeScriptJava 或者其它语言,所有的内容都是从零开始的;
    • 我们在掌握了 React 之后,再去学习 VueAngular 框架,也应该是可以快速上手的;
  • 任何工具的出现,都是更好地服务于我们开发
    • 无论是 Vite 的出现,还是以后新的工具的出现,不要有任何排斥的思想;
    • 我们要深刻地明白,工具都是为了更好地给我们提供服务;
    • 不可能出现了某个工具,让我们的开发效率变得更低,而这个工具却可以变得非常流行,这是不存在的;
  • 在后续的学习中,也会在讲完 webpack 核心知识后学习 gulprollupVite 工具。

四 (掌握)如何阅读 webpack 官方文档

  • webpack 官网地址:webpack.js.org
    • webpack 的中文官网地址:webpack.docschina.org
    • DOCUMENTATION:文档,也是我们最关注的;
  • 点击 DOCUMENTATION 来到文档页:
    • Api:提供相关的接口,可以自定义编译的过程(比如自定义 loaderplugin 时就可以来这个位置查阅);
    • Concepts:概念,主要是介绍一些 webpack 的核心概念,比如入口、出口、LoadersPlugins 等等,但是这里并没有对它们进行详细解析,详细的解析以及配置要去 Configuration 中查阅;
    • Configuration:配置,webpack 详细的配置选项,都可以在这里查询到,更多的时候是作为查询手册;
    • Guides:指南,更像是 webpack 提供的教程,我们可以按照这个教程一步步去学习 webpack 的使用;
    • Loaderswebpack 的核心之一,常见的 loader 都可以在这里查询到用法,比如 css-loaderbabel-loaderless-loader 等等;
    • Migrate:迁移,可以通过这里的教程将 webpac4 迁移到 webpack5 等等;
    • Pluginswebpack 的核心之一,常见的 plugin 都可以在这里查询到用法,比如 BannerPluginCleanWebpackPluginMiniCssExtractPlugin 等等;

五 (掌握)webpackwebpack-cli 的关系和安装

5.1 webpack 的依赖

  • webpack 的运行时依赖 Node 环境,所以电脑上必须有 Node 环境:
    • 需要先安装 Node.js,并且同时会安装 npm
    • 我当前电脑上的 node 版本是 v15.4.0npm 版本是 7.0.15(你也可以使用 nvmn 来管理 node 版本);
    • Node 的官网地址:nodejs.org

5.2 webpack 的安装

  • webpack 的安装目前分为两个:webpackwebpack-cli
    • 实际上,webpack-cli 不是必须安装的,像 vuereact 框架中就都没有安装,它们都是自定义了 cli,我们也可以自定义。只不过当我们使用某些命令,比如 webpack --config w.config.js 时,--config 参数的解析以及 w.config.js 文件的加载默认都是交给 webpack-cli 去执行的。
  • 那么它们是什么关系呢?
    • 执行 webpack 命令,会执行 node_modules 下的 .bin 目录下的 webpack
    • webpack 在执行时是依赖 webpack-cli 的,如果没有安装就会报错;
    • webpack-cli 中的 runCli() 函数执行时,才是真正利用 webpack 进行编译和打包的过程;
    • 所以在安装 webpack 时,我们需要同时安装 webpack-cli(但第三方的脚手架事实上是没有使用 webpack-cli 的,而是类似于自己的 vue-service-cli 的东西);

5.2.1 安装命令

  • 全局安装:npm install webpack webpack-cli -g
  • 局部安装:npm install webpack webpack-cli -D

注:全局安装是安装到电脑上(电脑的很多地方都能用),局部安装是安装到当前项目中(只在当前项目中可用)。

关于使用 npm 命令下载东西卡住的问题,有以下几种解决方案:

  • npm 设置镜像
  • 使用 cnpm
  • 电脑连热点安装
  • 使用 VPN

六 (掌握)webpack 的基本打包过程

我们使用 Visual Studio Code 这一代码编辑器来编写代码。

Visual Studio Code 中安装扩展:Live Server,方便后面跑 html 页面。

创建项目“01_webpack初体验”,项目目录结构如下:

项目目录结构

相关文件的内容如下:

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script src="./src/index.js" type="module"></script>
</body>
</html>

index.js

// ES6 的模块化
import { sum, mul } from "./js/math.js";

console.log(sum(20, 30));
console.log(mul(20, 30));

math.js

/* ES6 的模块化 */
export const sum = (num1, num2) => {
  return num1 + num2;
}

export const mul = (num1, num2) => {
  return num1 * num2;
}

然后,在 index.js 文件中”右键“--”Open with Liver Server“,然后在打开的浏览器页面按 F12 打开开发者工具,点击”Console“标签切换到控制台,就可以看到程序运行的结果了:

控制台打印的程序运行结果

虽然代码成功运行了,也获得了预期的结果。但是这里仍然存在很大的问题:

  1. 首先,在使用 index.js 这个脚本时,我们必须告诉它它是个模块(type="module");

  2. 其次,当前所使用的浏览器是支持 ES6 的模块化的,但是一些旧的浏览器并不支持 ES6 的模块化;

  3. 此外,浏览器是不支持 CommonJS 的,如果使用 CommonJS 进行模块化,浏览器也会报错:

    比如在 js 目录下新建 format.js 文件,文件内容如下:

    const dateFormat = (date) => {
      return "2021-01-16";
    };
    
    const priceFormat = price => "100.00";
    
    // CommonJS 的模块化
    module.exports = {
      dateFormat,
      priceFormat
    };
    

    然后修改 index.js 的内容如下(新增了关于 CommonJS 模块化的使用):

    // ES6 的模块化
    import { sum, mul } from "./js/math.js";
    // CommonJS 的模块化
    const { dateFormat, priceFormat } = require('./js/format');
    
    console.log(sum(20, 30));
    console.log(mul(20, 30));
    
    console.log(dateFormat(123));
    console.log(priceFormat(123));
    

    再回到浏览器中,可以看到控制台报错了:

    控制台报错了

    requireCommonJS 的东西,可见,浏览器是不支持 CommonJS 的。

正是因为存在这些问题,我们才需要借助某些工具(如 webpack)对编写好的原生的代码进行打包,打包成浏览器能正常识别的其它代码,再将这些打包后的代码引入到 index.js 文件中去。

下面,我们就可以借助 webpack 对代码进行打包了,在这里,可以使用全局的 webpack,也可以使用当前项目中的 webpack。我们先使用全局的 webpack 对代码进行打包,在 Visual Studio Code 中打开终端,在命令行中输入 webpack 后回车(注意:前面已经安装好了 webpack),成功打包后结果如下:

打包成功后的输出信息

(注:使用 webpack 这一命令进行打包时,对于出现的 WARNING,现阶段先忽略。)

这时,我们来看项目目录结构,会发现多了一个 dist 文件夹(dist 其实就是 distribution,这里可以理解为“发布”的意思),下面生成了一个 main.js 文件:

打包后的项目目录结构

打开 main.js 文件,就可以看到 webpack 打包压缩后的代码了。

下面,我们再去 index.html 文件中引入这个 main.js 文件,用它替换掉之前的 index.js 文件,index.html 中修改后的代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <!-- <script src="./src/index.js" type="module"></script> -->
  <script src="./dist/main.js"></script>
</body>
</html>

注意,这个时候因为没有直接使用 ES6module,所以 script 中也不需要添加 type 属性了。然后,我们再回到浏览器中,可以看到控制台中输出了预期的结果:

控制台打印了预期的程序运行结果

可见,我们之前编写的原生的代码中,不管是 ES6 module 还是 CommonJS module,在经 webpack 打包后,都能在浏览器中正常运行起来了。

当然,上面打包后的代码中其实仍然存在着 ES6 的内容(比如箭头函数、const 等等),这是因为我们还没有对 babel-loader 进行配置,后面我们会讲到,babel 可以对代码进行一些转化。在没有配置 babel 的情况下,webpack 打包出来的就是一些 ES6 的代码。这在官方文档中也说明了:

Note that webpack will not alter any code other than import and export statements. If you are using other ES2015 features, make sure to use a transpiler such as Babel or Bublé via webpack's loader system.

七 (掌握)全局和局部 webpack 的不同用法

首先,我们需要知道一个叫 package.json 的文件,package.json 文件中记录了当前项目所依赖的库以及它们对应的版本信息,通常,我们在拿到一个项目后,会通过执行 npm install 命令来安装 package.json 中记录的各个相应版本的依赖(比如 webpackwebpack-cliaxiosvuereact 等等项目正常运行需要依赖的东西),以保证在任何电脑上运行的都是各个相同版本的依赖,就不会出现电脑因为某个依赖的版本不对而产生问题,即保证了版本的一致。

因此,在实际开发中,我们想要对某一东西做编译时,不会使用全局的 webpack,而是使用局部的 webpack,也就是说项目中应该单独安装 webpack。那怎么使用 package.json 来记录安装的 webpack 的版本呢?我们可以在命令行中执行 npm init 命令生成 package.json 文件:

创建 package.json 文件

注意:除了第一项“package name”进行了重命名设置(规避中文可能导致的错误),后续一直回车到底就行了。

这时,我们来看项目目录结构,会发现项目根目录下多了一个 package.json 文件:

项目当前的目录结构

package.json 文件可以帮助我们记录当前项目所依赖的东西的版本,也有很多其它的东西,因为这些东西属于 node 的范畴,这里就不详细讲了。

有了 package.json 文件后,我们再在当前项目中安装依赖,这些依赖的版本信息就会被记录到 package.json 文件中去。

我们通过以下命令安装 webpackwebpack-cli 两个开发时依赖:

npm install webpack webpack-cli -D

其中,-D 表示开发时依赖,是 --save-dev 的简写

安装完成后,项目根目录下会生成一个 node_modules 文件夹:

生成了 node_modules 目录

可以看到,node_modules 文件夹下有很多文件夹,这是因为 webpack 也依赖了很多东西。

下面,我们就可以使用 node_modules/.bin 目录下的 webpack 命令来执行当前项目中的 webpack 对代码进行打包了,在终端中(项目根目录下)执行如下命令:./node_modules/.bin/webpack,使用的就是局部的(当前项目中的) webpack 命令进行的打包(注意:如果直接输入 webpack 执行,使用的还是全局的 webpack)。

不难发现,这条命令有点长,有没有简单点的执行方式呢?当然,还有两种:

  1. 使用 npx webpack 命令;
  2. package.json 文件中的 "script" 一项内容中设置要执行的脚本命令的别名后,再使用 npm run 别名 命令,举例如下:

配置脚本 运行脚本

1 种方式的原理是:npx 会执行 node_modules 目录下的命令,比如当我们执行 npx webpack 时,实际就会执行 node_modules 目录下的 .bin 目录下的 webpackWindows 中是 webpack.cmd) 命令;第 2 种方式的原理是:执行 npm run script 时,会找到 package.jsonscripts 中对应"script"中的命令,然后会优先去找 node_modules 目录下的 .bin 目录下对应的命令。

八 实践中可能出现的问题:

  1. 浏览器控制台报错:“Uncaught SyntaxError: Cannot use import statement outside a module

    • 报错的意思:不能在模块外使用 import 语句;
    • 原因:script 标签中没有指定 type 属性的值为 module
    • 解决办法:在 script 标签中指定 type 属性的值为 module

    指定 script 元素的 type 为 module

  2. 浏览器控制台报错:“GET http://127.0.0.1:5500/01_webpack%E5%88%9D%E4%BD%93%E9%AA%8C/src/js/math net::ERR_ABORTED 404 (Not Found)

    • 报错的意思:未找到 math 文件;
    • 原因:编写原生的代码时,文件的后缀名必须加上(nodewebpack 中引用 js 文件时如果没有加后缀,会自动地去寻找对应的 js 文件,而在编写原生的代码时,则不会);
    • 解决办法:添加上文件的后缀名;

    原生代码中文件后缀名不能漏

  3. 浏览器控制台报错:“Uncaught ReferenceError: require is not defined

    • 报错的意思:require 未定义;
    • 原因:浏览器是不认识 CommonJS 的,所以使用 CommonJS 中的内容(如 requiremodule.exports 等)会报错;
    • 解决办法:借助某些工具(如 webpack)对编写好的原生的代码进行打包,打包成浏览器能正常识别的其它代码,再将这些打包后的代码引入到 index.js 文件中;
  4. 命令行中运行 webpack 这一命令时,出现如下报错:

    无法解析当前目录下的 src

    • 报错的意思:未能解析当前目录下的 src 目录;

    • 原因:当前目录下没有 src 目录或者有 src 目录但 src 目录下没有 index.js 文件,而 webpack 这一命令本质上会去寻找当前目录下的 src 目录下的 index.js./src/index.js)文件,然后将其作为入口,通过这一入口去查看对其它文件的引入,最后对其和引入的其它文件一起进行打包的。官方文档也有说明:

    webpack 开箱即用,可以无需使用任何配置文件。然而,webpack 会假定项目的入口起点为 src/index.js,然后会在 dist/main.js 输出结果,并且在生产环境开启压缩和优化。

    • 解决办法:切换到正确的项目目录下(项目目录下要存在 src 目录,src 目录下要存在 index.js 文件)再执行 webpack
  5. webpack 打包完代码,在 index.html 页面引入 dist 目录下的 main.js 后”右键“--”Open with Liver Server“,出现如下报错:

    main.js 中报错

    • 可能的原因:CommonJS 的导出写的有问题,比如 module.exports 漏写了“s”等等;
  6. 使用命令 npm uninstall webpack webpack-cli -g 卸载全局的 webpackwebpack-cli 后,仍然能查看到 webpack 的版本,截图如下:

    image-20210117204030056.png

    • 原因:webpack 的全局卸载没有成功,可能是 node 环境安装的问题;
    • 解决办法:如果是用的 nvm 管理的 node,可以尝试将其删除,改为直接去 node 官网下载 node 进行安装。可以先用命令 where webpack 寻找 webpack 的位置,找到后将其手动删除。

九 补充

  1. 如果当前项目中没有安装 webpackwebpack-cli,执行 webpack 命令时,则会执行全局安装的 webpackwebpack-cli。可以这样验证:先使用 npm uninstall webpack webpack-cli 将当前项目中的 webpackwebpack-cli 卸载掉(此时 node_modules 下的 .bin 目录下已经没有 webpack 相关的命令了),然后执行 npm run buildnpx webpack,可以发现还是能成功打包的;
  2. Windows 命令下面的可执行文件,后面会多一个 .cmd(也是批处理文件);
  3. npx webpacknpm run build(注:这里的“build”所设置的对应的命令是“webpack”)执行时,如果没有找到局部的 webpack 的话(比如未安装局部的 webpack),就会去找全局的 webpack
  4. 当使用 npm 安装一些全局的软件包后,不知道安装到了什么位置时,可以使用命令 npm root -g 进行查询,如果是 Windows 系统,通常会默认保存在以下位置:C:\Users\用户名\AppData\Roaming\npm\node_modules