Webpack 入门
已发表: 2022-03-10在 JavaScript 中引入模块化的早期,没有原生支持在浏览器中运行模块。 使用 CommonJS 蓝图在 Node.js 中实现了对模块化编程的支持,并被那些使用 JavaScript 构建服务器端应用程序的人采用。
它还具有开发大型 Web 应用程序的前景,因为开发人员可以通过以更模块化的模式编写代码来避免命名空间冲突并构建更易于维护的代码库。 但是仍然存在一个挑战:模块不能在通常执行 JavaScript 的 Web 浏览器中使用。
为了解决这个问题,我们编写了诸如 webpack、Parcel、Rollup 和 Google 的 Closure Compiler 之类的模块捆绑器来创建优化的代码包,供最终用户的浏览器下载和执行。
“捆绑”你的代码是什么意思?
捆绑代码是指将多个模块组合和优化为一个或多个可用于生产的捆绑包。 这里提到的捆绑可以更好地理解为整个捆绑过程的最终产品。
在本文中,我们将重点关注 webpack,这是一个由 Tobias Koppers 编写的工具,随着时间的推移,它已经发展成为 JavaScript 工具链中的主要工具,经常用于大大小小的项目中。
注意:要从本文中受益,最好熟悉 JavaScript 模块。 你还需要在本地机器上安装Node ,这样你就可以在本地安装和使用 webpack。
什么是 webpack?
webpack 是一个用于 JavaScript 应用程序的高度可扩展和可配置的静态模块打包器。 凭借其可扩展性,您可以插入外部加载器和插件来实现您的最终目标。
如下图所示,webpack 从根入口点遍历您的应用程序,构建由直接或间接作用于根文件的依赖项组成的依赖关系图,并生成组合模块的优化包。
要了解 webpack 是如何工作的,我们需要了解它使用的一些术语(查看 webpack Glossary。这个术语在本文中经常使用,并且在 webpack 的文档中也经常引用。
- 块
块是指从模块中提取的代码。 此代码将存储在一个块文件中。 当使用 webpack 执行代码拆分时,通常会使用块。 - 模块
模块是应用程序的分解部分,您可以导入它们以执行特定任务或功能。 Webpack 支持使用 ES6、CommonJS 和 AMD 语法创建的模块。 - 资产
资产一词通常在 webpack 和其他捆绑器中经常使用。 它指的是在构建过程中捆绑的静态文件。 这些文件可以是从图像到字体甚至视频文件的任何内容。 随着您进一步阅读本文,您将看到我们如何使用加载器来处理不同的资产类型。
推荐阅读: Webpack - 详细介绍
一旦我们了解了 webpack 是什么以及它使用了什么术语,让我们看看它们如何应用于为演示项目组合配置文件。
注意:你还需要安装webpack-cli
才能在你的机器上使用 webpack。 如果未安装,终端将提示您安装它。
webpack 配置文件
除了在终端中使用 webpack-cli,您还可以通过配置文件在项目中使用 webpack。 但是使用最新版本的 webpack,我们可以在我们的项目中使用它而无需配置文件。 我们可以使用webpack
作为package.json
文件中命令之一的值——不带任何标志。 这样,webpack 将假定您项目的入口点文件位于src
目录中。 它会将入口文件捆绑并输出到dist
目录。
下面的示例package.json
文件就是一个示例。 在这里,我们使用 webpack 打包应用程序,无需配置文件:
{ "name" : "Smashing Magazine", "main": "index.js", "scripts": { "build" : "webpack" }, "dependencies" : { "webpack": "^5.24.1" } }
当运行上面文件中的 build 命令时,webpack 会将src/index.js
目录中的文件打包并输出到dist
目录中的main.js
文件中。 然而,webpack 比这灵活得多。 我们可以通过编辑带有-- config
标志的配置文件来更改入口点、调整输出点并优化许多其他默认行为。
一个示例是上面package.json
文件中修改后的构建命令:
"build" : "webpack --config webpack.config.js"
上面,我们添加了--config
标志并将webpack.config.js
指定为具有新 webpack 配置的文件。
webpack.config.js
文件还不存在。 所以我们需要在我们的应用程序目录中创建它,并将下面的代码粘贴到文件中。
# webpack.config.js const path = require("path") module.exports = { entry : "./src/entry", output : { path: path.resolve(__dirname, "dist"), filename: "output.js" } }
上面的文件仍然配置 webpack 来打包你的 JavaScript 文件,但是现在我们可以定义一个自定义的入口和输出文件路径,而不是 webpack 使用的默认路径。
关于 webpack 配置文件的一些注意事项:
- webpack 配置文件是一个 JavaScript 文件,编写为 JavaScript CommonJS 模块。
- webpack 配置文件导出具有多个属性的对象。 这些属性中的每一个都用作捆绑代码时配置 webpack 的选项。 一个例子是
mode
选项:-
mode
在配置中,此选项用于在捆绑期间设置NODE_ENV
值。 它可以具有production
或development
价值。 如果未指定,它将默认为none
。 同样重要的是要注意 webpack 根据mode
值以不同的方式捆绑您的资产。 例如,webpack 在开发模式下自动缓存你的包,以优化和减少包时间。 请参阅 webpack 文档的模式部分,以查看在每种模式中自动应用的选项的更改日志。
-
webpack 概念
通过 CLI 或通过配置文件配置 webpack 时,有四个主要概念用作选项。 本文的下一部分将重点介绍这些概念,并在构建演示 Web 应用程序的配置时应用它们。
请注意,下面解释的概念与其他模块捆绑器有一些相似之处。 例如,当使用带有配置文件的 Rollup 时,您可以定义一个输入字段来指定依赖关系图的入口点,一个输出对象配置生成的块的放置方式和位置,还可以定义一个插件对象用于添加外部插件。
入口
配置文件中的entry字段包含 webpack 开始构建依赖图的文件的路径。 从这个入口文件,webpack 将继续处理直接或间接依赖于入口点的其他模块。
您的配置的入口点可以是具有单个文件值的 Single Entry 类型,类似于以下示例:
# webpack.configuration.js module.exports = { mode: "development", entry : "./src/entry" }
入口点也可以是多主入口类型,具有包含多个入口文件路径的数组,类似于以下示例:
# webpack.configuration.js const webpack = require("webpack") module.exports = { mode: "development", entry: [ './src/entry', './src/entry2' ], }
输出
顾名思义,配置的输出字段是创建的包所在的位置。 当您有多个模块时,此字段会派上用场。 您可以指定自己的文件名,而不是使用 webpack 生成的名称。
# webpack.configuration.js const webpack = require("webpack"); const path = require("path"); module.exports = { mode: "development", entry: './src/entry', output: { filename: "webpack-output.js", path: path.resolve(__dirname, "dist"), } }
装载机
默认情况下,webpack 只理解应用程序中的 JavaScript 文件。 但是,webpack 将作为模块导入的每个文件都视为依赖项,并将其添加到依赖关系图中。 为了处理静态资源,例如图像、CSS 文件、JSON 文件,甚至存储在 CSV 中的数据,webpack 使用加载器将这些文件“加载”到包中。
加载器足够灵活,可以用于很多事情,从转译 ES 代码到处理应用程序的样式,甚至使用 ESLint 对代码进行 linting。
在您的应用程序中使用加载程序有三种方法。 其中之一是通过内联方法直接将其导入文件中。 例如,为了最小化图像大小,我们可以直接在文件中使用image-loader
加载器,如下所示:
// main.js import ImageLoader from 'image-loader'
使用加载器的另一个首选选项是通过您的 webpack 配置文件。 这样,您可以使用加载器执行更多操作,例如指定要应用加载器的文件类型。 为此,我们创建一个rules
数组并在一个对象中指定加载器,每个加载器都有一个测试字段,其中的正则表达式匹配我们想要应用加载器的资产。
例如,在前面的示例中直接导入image-loader
,我们可以在 webpack 配置文件中使用它,并使用文档中最基本的选项。 这将如下所示:
# webpack.config.js const webpack = require("webpack") const path = require("path") const merge = require("webpack-merge") module.exports = { mode: "development", entry: './src/entry', output: { filename: "webpack-output.js", path: path.resolve(__dirname, "dist"), }, module: { rules: [ { test: /\.(jpe?g|png|gif|svg)$/i, use: [ 'img-loader' ] } ] } }
仔细查看包含上述image-loader
的对象中的test
字段。 我们可以发现匹配所有图像文件的正则表达式: jp(e)g
、 png
、 gif
和svg
格式。
使用 Loaders 的最后一种方法是通过带有--module-bind
标志的 CLI。
awesome-webpack 自述文件包含一个详尽的加载器列表,您可以将它们与 webpack 一起使用,每个加载器都分为它们执行的操作类别。 以下只是一些您可能会在您的应用程序中找到方便的加载器:
- Responsive-loader在添加图像以适应您的响应式网站或应用程序时,您会发现此加载器非常有用。 它从单个图像创建多个不同尺寸的图像,并返回一个与图像匹配的
srcset
,以便在适当的显示屏幕尺寸下使用。 - 通天塔装载机
这用于将 JavaScript 代码从现代 ECMA 语法转换为 ES5。 - GraphQL 加载器
如果你是 GraphQL 爱好者,你会发现这个加载器非常有用,因为它加载了包含 GraphQL 模式、查询和突变的.graphql
文件——以及启用验证的选项。
插件
插件的使用允许 webpack 编译器对捆绑模块产生的块执行任务。 尽管 webpack 不是任务运行器,但通过插件,我们可以执行一些自定义操作,这些操作在捆绑代码时加载器无法执行。
webpack 插件的一个例子是 webpack 内置的ProgressPlugin 。 它提供了一种自定义编译过程中在控制台中打印的进度的方法。
# webpack.config.js const webpack = require("webpack") const path = require("path") const merge = require("webpack-merge") const config = { mode: "development", entry: './src/entry', output: { filename: "webpack-output.js", path: path.resolve(__dirname, "dist"), }, module: { rules: [ { test: /\.(jpe?g|png|gif|svg)$/i, use: [ 'img-loader' ] } ] }, plugins: [ new webpack.ProgressPlugin({ handler: (percentage, message ) => { console.info(percentage, message); }, }) ] } module.exports = config
通过上面配置的 Progress 插件,我们提供了一个处理函数,它会在编译过程中将编译百分比和消息打印到控制台。
下面是一些来自 awesome-webpack 自述文件的插件,您可以在 webpack 应用程序中找到它们。
- 离线插件
该插件首先利用服务工作者或可用的 AppCache 为 webpack 托管项目提供离线体验。 - Purgecss-webpack-plugin
这个插件在尝试优化你的 webpack 项目时会派上用场,因为它会在编译期间删除应用程序中未使用的 CSS。
至此,我们已经为一个相对较小的应用程序设置了第一个 webpack 配置。 让我们进一步考虑如何在我们的应用程序中使用 webpack 做某些事情。
处理多个环境
在您的应用程序中,您可能需要为开发或生产环境配置不同的 webpack。 例如,您可能不希望 webpack 每次对生产环境中的持续集成管道进行新部署时都输出轻微的警告日志。
正如 webpack 和社区所推荐的,有几种方法可以实现这一点。 一种方法是将配置文件转换为导出返回对象的函数。 这样,webpack 编译器会将当前环境作为第一个参数传递给函数,其他选项作为第二个参数。
如果您希望根据当前环境执行一些不同的操作,这种处理 webpack 环境的方法会派上用场。 但是,对于具有更复杂配置的大型应用程序,您最终可能会得到一个包含大量条件语句的配置。
下面的代码片段显示了如何使用functions
方法在同一文件中处理production
和development
环境的示例。
// webpack.config.js module.exports = function (env, args) { return { mode : env.production ? 'production' : 'development', entry: './src/entry', output: { filename: "webpack-output.js", path: path.resolve(__dirname, "dist"), }, plugins: [ env.development && ( new webpack.ProgressPlugin({ handler: (percentage, message ) => { console.info(percentage, message); }, }) ) ] } }
通过上面代码片段中的导出函数,您将看到传递给函数的env
参数如何与三元运算符一起使用来切换值。 它首先用于设置 webpack 模式,然后它也用于仅在开发模式下启用 ProgressPlugin。
处理生产和开发环境的另一种更优雅的方法是为这两个环境创建不同的配置文件。 完成此操作后,我们可以在打包应用程序时将它们与package.json
脚本中的不同命令一起使用。 看看下面的片段:
{ "name" : "smashing-magazine", "main" : "index.js" "scripts" : { "bundle:dev" : "webpack --config webpack.dev.config.js", "bundle:prod" : "webpack --config webpack.prod.config.js" }, "dependencies" : { "webpack": "^5.24.1" } }
在上面的package.json
中,我们有两个脚本命令,每个命令都使用不同的配置文件来处理捆绑应用程序资产时的特定环境。 现在,您可以在开发模式下使用npm run bundle:dev
捆绑您的应用程序,或者在创建生产就绪包时使用npm run bundle:prod
捆绑您的应用程序。
使用第二种方法,您可以避免从函数返回配置对象时引入的条件语句。 但是,现在您还必须维护多个配置文件。
拆分配置文件
此时,我们的 webpack 配置文件有 38 行代码(LOC)。 这对于具有单个加载器和单个插件的演示应用程序来说非常好。
但是对于一个更大的应用程序,我们的 webpack 配置文件肯定会更长,有几个加载器和插件,每个都有它们的自定义选项。 为了保持配置文件的干净和可读性,我们可以将配置拆分为多个文件中的较小对象,然后使用 webpack-merge 包将配置对象合并到一个基本文件中。
要将其应用于我们的 webpack 项目,我们可以将单个配置文件拆分为三个较小的文件:一个用于加载程序,一个用于插件,最后一个文件作为基础配置文件,我们将其他两个文件放在一起。
创建一个webpack.plugin.config.js
文件并将下面的代码粘贴到其中以使用带有附加选项的插件。
// webpack.plugin.config.js const webpack = require('webpack') const plugin = [ new webpack.ProgressPlugin({ handler: (percentage, message ) => { console.info(percentage, message); }, }) ] module.exports = plugin
上面,我们有一个从webpack.configuration.js
文件中提取的插件。
接下来,使用以下代码为 webpack 加载器创建一个webpack.loader.config.js
文件。
// webpack.loader.config.js const loader = { module: { rules: [ { test: /\.(jpe?g|png|gif|svg)$/i, use: [ 'img-loader' ] } ] } }
在上面的代码块中,我们将 webpack img-loader
移到了一个单独的文件中。
最后,创建一个webpack.base.config.js
文件,其中 webpack 应用程序的基本输入和输出配置将与上面创建的两个文件一起保存。
// webpack.base.config.js const path = require("path") const merge = require("webpack-merge") const plugins = require('./webpack.plugin.config') const loaders = require('./webpack.loader.config') const config = merge(loaders, plugins, { mode: "development", entry: './src/entry', output: { filename: "webpack-output.js", path: path.resolve(__dirname, "dist"), } }); module.exports = config
看一眼上面的 webpack 文件,您可以观察到它与原始webpack.config.js
文件相比有多紧凑。 现在配置的三个主要部分已被分解为较小的文件,可以单独使用。
优化大型构建
当您在一段时间内继续处理您的应用程序时,您的应用程序肯定会在功能和大小方面变得更大。 发生这种情况时,将创建新文件,修改或重构旧文件,并安装新的外部包——所有这些都会导致 webpack 发出的包大小增加。
默认情况下,如果您的配置模式设置为production
,webpack 会自动尝试为您优化包。 例如,默认情况下 webpack 应用的一种技术(从 webpack 4+ 开始)来优化和减小包大小是 Tree-Shaking。 本质上,它是一种用于删除未使用代码的优化技术。 在捆绑期间的简单级别,导入和导出语句用于在将未使用的模块从发出的捆绑包中删除之前检测它们。
您还可以通过将具有某些字段的optimization
对象添加到配置文件中来手动优化您的应用程序包。 webpack 文档的优化部分包含完整的字段列表,您可以在optimization
对象中使用这些字段来优化您的应用程序。 让我们考虑 20 个记录字段中的一个。
-
minimize
这个布尔字段用于指示 webpack 最小化包大小。 默认情况下,webpack 将尝试使用 TerserPlugin 来实现这一点,TerserPlugin 是 webpack 附带的代码压缩包。
缩小适用于通过从代码中删除不必要的数据来最小化您的代码,这反过来又会减少处理后产生的代码大小。
我们还可以通过在optimization
对象中添加minimizer
器数组字段来使用其他首选的缩小器。 一个例子是下面使用 Uglifyjs-webpack-plugin。
// webpack.config.js const Uglify = require("uglifyjs-webpack-plugin") module.exports = { optimization { minimize : true, minimizer : [ new Uglify({ cache : true, test: /\.js(\?.*)?$/i, }) ] } }
上面, uglifyjs-webpack-plugin
被用作一个缩小器,有两个非常重要的选项。 首先,启用cache
意味着 Uglify 只会在现有文件是新更改时缩小它们,并且test
选项指定我们要缩小的具体文件类型。
注意: uglifyjs-webpack-plugin 提供了一个完整的列表,列出了在使用它来缩小代码时可用的选项。
一点优化演示
让我们手动尝试通过在更大的项目中应用一些字段来优化演示应用程序以查看差异。 虽然我们不会深入优化应用程序,但我们会看到在development
模式下运行 webpack 与在production
模式下运行时包大小的差异。
对于这个演示,我们将使用一个使用 Electron 构建的桌面应用程序,它的 UI 也使用 React.js——所有这些都与 webpack 捆绑在一起。 Electron 和 React.js 听起来像是一个非常重的组合,可能会生成更大的包。
注意:如果您是第一次学习Electron ,本文将深入了解Electron是什么以及如何使用它来构建跨平台桌面应用程序。
要在本地试用演示,请从 GitHub 存储库克隆应用程序并使用以下命令安装依赖项。
# clone repository git clone https://github.com/vickywane/webpack-react-demo.git # change directory cd demo-electron-react-webpack # install dependencies npm install
桌面应用程序相当简单,只有一个页面使用 styled-components 设置样式。 当使用yarn start
命令启动桌面应用程序时,单个页面会显示从 CDN 获取的图像列表,如下所示。
让我们先创建这个应用程序的开发包,无需任何手动优化来分析最终包的大小。
从项目目录中的终端运行yarn build:dev
将创建开发包。 此外,它还会将以下统计信息打印到您的终端:
该命令将向我们显示整个编译和发出的包的统计信息。
请注意mainRenderer.js
块的大小为 1.11 Mebibyte(约 1.16 MB)。 mainRenderer
是 Electron 应用程序的入口点。
接下来,让我们在webpack.base.config.js
文件中添加 uglifyjs-webpack-plugin 作为已安装的插件,以进行代码压缩。
// webpack.base.config.js const Uglifyjs = require("uglifyjs-webpack-plugin") module.exports = { plugins : [ new Uglifyjs({ cache : true }) ] }
最后,让我们在production
模式下将应用程序与 webpack 捆绑在一起。 从终端运行yarn build:prod
命令会将以下数据输出到终端。
这次记下mainRenderer
块。 它已经下降到惊人的 182 千字节(大约 186 KB),这是之前发出的mainRenderer
块大小的 80% 以上!
让我们使用 webpack-bundler-analyzer 进一步可视化发出的包。 使用yarn add webpack-bundle-analyzer
命令安装插件并修改webpack.base.config.js
文件以包含添加插件的以下代码。
// webpack.base.config.js const Uglifyjs = require("uglifyjs-webpack-plugin"); const BundleAnalyzerPlugin = require("webpack-bundle-analyzer"); .BundleAnalyzerPlugin; const config = { plugins: [ new Uglifyjs({ cache : true }), new BundleAnalyzerPlugin(), ] }; module.exports = config;
从终端运行yarn build:prod
以重新捆绑应用程序。 默认情况下,webpack-bundle-analyzer 将启动一个 HTTP 服务器,在浏览器中提供可视化的 bundle 概览。
从上图中,我们可以看到发出的包和包内文件大小的可视化表示。 在视觉上,我们可以观察到在node_modules
文件夹中,最大的文件是react-dom.production.min.js
,其次是stylis.min.js
。
使用分析器可视化的文件大小,我们将更好地了解哪些已安装的软件包贡献了包的主要部分。 然后我们可以寻找优化它的方法或用更轻的包替换它。
注意: webpack-analyzer-plugin文档列出了其他可用于显示从发出的包创建的分析的方法。
webpack 社区
webpack 的优势之一是其背后有庞大的开发者社区,这对于第一次尝试 webpack 的开发者来说非常有用。 就像这篇文章一样,有几篇文章、指南和资源以及在使用 webpack 时作为很好的指南的文档。
例如,来自 webpack 博客的构建性能指南包含有关优化 webpack 构建的技巧,而 Slack 的案例研究(虽然有点旧)解释了如何在 Slack 优化 webpack。
一些社区资源解释了 webpack 文档的部分内容,为您提供了示例演示项目,以展示如何使用 webpack 的功能。 一个例子是一篇关于 Webpack 5 Module Federation 的文章,它解释了如何在 React 应用程序中使用 webpack 的新 Module Federation 功能。
概括
经过七年的存在,webpack 已经真正证明了自己是大量项目使用的 JavaScript 工具链的重要组成部分。 本文仅简要介绍了利用 webpack 的灵活和可扩展性可以实现的目标。
下次你需要为你的应用程序选择一个模块打包器时,希望你能更好地理解 Webpack 的一些核心概念,它解决的问题,以及设置你的配置文件的步骤。
关于 SmashingMag 的进一步阅读:
- Webpack - 详细介绍
- 使用 Webpack 和 Workbox 构建 PWA
- 使用 Webpack 为现代 React 项目设置 TypeScript
- 如何利用机器:与任务运行者一起提高工作效率