JS应用程序,站点和其他资源正在变得越来越复杂,并且构建工具是Web开发的现实。 捆绑软件可帮助打包,编译和组织库。 Webpack是可以完美定制以构建客户端应用程序的强大而灵活的开源工具之一。
Maxim Sosnov(
crazymax11 )
-N1.RU中的前端主管将Webpack引入了多个大型项目,这些大型项目以前具有自己的自定义构建,并为此贡献了多个项目。 Maxim知道如何使用Webpack构建理想的捆绑包,快速进行配置并对其进行配置,以使配置保持清洁,维护和模块化。
解释与报告不同-它是proflink的改进版本。 在整个笔录中,复活节彩蛋散布在文章,插件,缩小词,选项,翻译者和说话者的言语证明上,而这些链接根本无法发表。 如果您收集了所有内容,则Webpack中的奖励级别将打开:-)
Webpack集成在典型项目中
通常,实现过程如下:开发人员在某处阅读了有关Webpack的文章,决定连接它,开始构建它,以某种方式起作用,一切开始,并且有一段时间webpack-config起作用-六个月,一年,两年。 在当地,一切都很好-太阳,彩虹和蝴蝶。 然后真正的用户来了:
-从移动设备无法加载您的网站。
-一切对我们都有效。 在当地,一切都很好!以防万一,开发人员去分析所有内容,发现对于移动设备,
捆绑包重7 MB,需要30秒才能加载 。 这并不适合任何人,开发人员开始寻找解决问题的方法-他可以连接装载程序或找到可以解决所有问题的魔术插件。 神奇地找到了这样的插件。 我们的开发人员转到webpack-config,尝试安装,但是代码行会干扰:
if (process.env.NODE_ENV === 'production') { config.module.rules[7].options.magic = true; }
该行的翻译如下:“如果将config组装用于生产,则采用第七条规则并在其中放置
magic = true
选项。” 开发人员不知道该怎么办以及如何解决。 在这种情况下,您需要一堆梦。
如何收集一捆梦?
首先,让我们定义它是什么。 首先,梦想捆绑包具有两个主要特征:
- 它有点重 。 重量越轻-用户越快获得有效的应用程序。 您不希望您的网站打开15秒钟。
- 用户只下载显示该站点当前页面所需的内容,而不下载更多内容!
为了减小包的大小,必须首先评估其大小。
评分捆绑包大小
最受欢迎的解决方案是
WebpackBundleAnalyzer插件。 它收集应用程序构建统计信息,并呈现一个交互式页面,您可以在其中查看每个模块的位置和重量。

如果这还不够,您可以使用
另一个插件来构建
依赖关系图 。

或
饼图 。

如果这还不够,并且您想将Webpack出售给营销人员,则可以
构建一个整个宇宙 ,其中每个点都是一个模块,就像宇宙中的星星一样。

有很多工具可以评估捆绑软件的大小并对其进行监控。 例如,如果捆绑包过重
,则Webpack配置中有
一个选项可使组件崩溃。 如果有2个不同版本的npm软件包,例如Lodash 4.15和Lodash 4.14,则有一个
重复的package-checker-webpack-plugin插件将阻止您
构建捆绑包。
如何减少捆绑
- 最明显的是插入UglifyJS,以便它最小化JavaScript。
- 使用特殊的加载器和插件来压缩和优化特定资源。 例如, css的css-nano或优化SVG的SVGO 。
- 通过gzip / brotli插件将所有文件直接压缩到Webpack。
- 其他工具。
现在,我们将了解如何从捆绑中扔掉多余的东西。
扔掉多余的
在带有
moment.js的流行示例中考虑这
一点 :
import moment from 'moment'
。 如果您使用一个空的应用程序,则将moment.js和
ReactDOM导入其中,然后将其传递给
WebpackBundleAnalyzer ,您将看到以下图片。

事实证明,当您在日期中添加一天,一个小时,或者只是想使用moment.js将链接放在“ 15分钟内”时,您将连接整个
230 KB的代码 ! 为什么会发生这种情况以及如何解决?
此刻的语言环境加载
moment.js中有一个用于设置语言环境的函数:
function setLocale(locale) { const localePath = 'locale/' + locale + '.js'; this._currentLocale = require(localePath); }
从代码中可以看出,语言环境是沿着动态路径加载的,即 在运行时计算。 Webpack的行为巧妙,并尝试确保您的捆绑软件在代码执行期间不会崩溃:它会找到项目中所有可能的语言环境,并将其捆绑。 因此,该应用程序非常重。

解决方案非常简单-我们从Webpack中获取一个
标准插件 ,然后对它说:“如果您发现有人要下载很多语言环境,因为他们无法确定哪个语言环境,那么就选择俄语语言!”

Webpack仅会使用俄语,而WebpackBundleAnalyzer将显示54 Kb,这已经轻松了200 Kb。
消除死代码
我们感兴趣的下一个优化是
消除死代码 。 考虑下面的代码。
const cond = true; if (!cond) { return false; } return true; someFunction(42);
最终捆绑包中不需要此代码中的大多数行-具有条件的块也不会执行,返回后的函数也将不会执行。 您只需要离开就
return true
。 这正是消除死代码的本质:构建工具检测到无法执行的代码并将其剪切。 UglifyJS有一个很好的功能,可以做到这一点。
现在,让我们继续进行更高级的Dead代码消除
-Tree Shaking方法 。
摇树
假设我们有一个使用
Lodash的应用程序。 我强烈怀疑有人会使用整个Lodash。 最有可能利用了诸如
get ,
IsEmpty ,
unionBy之类的几个功能。
当我们进行树摇动时,我们希望Webpack“摇动”不必要的模块并将其丢弃,而我们只有必要的模块。 这是树在颤抖。
Webpack中树抖动的工作原理
假设您有这样的代码:
import { a } from './a.js'; console.log(a);
代码非常简单:从某个模块导入变量a并输出。 但是此模块中有两个变量:
a和
b 。 我们不需要变量
b ,我们想将其删除。
export const a = 3 export const b = 4
Webpack到达时,它将导入代码转换为以下代码:
var d = require(0); console.log(d["a"]);
我们的
import
变成了
require
,但是
console.log
没有改变。
Webpack依赖项转换为以下代码:
var a = 3; module.exports["a«] = a; /* unused harmony export b */ var b = 4;
Webpack保留了变量
a的导出,并删除了变量
b的导出,但是保留了变量本身,并用特殊注释对其进行了标记。 在转换后的代码中,不使用变量
b ,UglifyJS可以将其删除。
仅当您具有某种代码缩减器(例如UglifyJS或babel- minify)时,Webpack摇树才会起作用。
让我们考虑更有趣的情况-摇树不起作用。
当摇树不起作用时
案例1。您编写代码:
module.exports.a = 3; module.exports.b = 4;
通过Webpack运行代码,并且保持不变。 这是因为仅当您使用ES6模块时,捆绑程序才会组织树摇动。 如果使用CommonJS模块,则摇树将不起作用。
案例2。使用ES6模块编写代码并命名为export。
export const a = 3 export const b = 4
如果您的代码通过Babel运行,并且您未将
modules选项设置
为false ,那么Babel会将您的模块引入CommonJS,Webpack将再次无法执行Tree Shaking,因为它仅适用于ES6模块。
module.exports.a = 3; module.exports.b = 4;
因此,我们需要确保组装计划中没有人会渗透ES6模块。
案例3。假设我们有一个无用的类,什么也不做:
export class ShakeMe {}
。 而且,我们仍然不使用它。 当Webpack进行导入和导出时,Babel会将类转换为一个函数,并且捆绑程序将注意未使用该函数:
var ShakeMe = function () { function ShakeMe() { babelHelpers.classCallCheck(this, ShakeMe); } return ShakeMe; }();
看起来一切都应该不错,但是如果我们仔细看一看,就会发现在此函数内部有一个全局变量
babelHelpers
,从中可以调用某些函数。 这是一个
副作用 :UglifyJS看到正在调用某些全局函数,并且不会剪切代码,因为担心某些事情会中断。
当您编写类并通过Babel运行它们时,它们永远不会被削减。 如何解决? 有一个标准化的技巧-在函数之前添加注释
/*#__PURE__*/
:
var ShakeMe = function () { function ShakeMe() { babelHelpers.classCallCheck(this, ShakeMe); } return ShakeMe; }();
然后,UglifyJS将相信下一个函数是纯净的。 幸运的是,
Babel 7现在正在执行此操作,而在Babel 6中,到目前为止尚未删除任何内容。
规则:如果您在某处有副作用,那么UglifyJS不会做任何事情。
总结一下:
- 对于npm以来的大多数库,树摇不适用于大多数库 ,因为它们全部来自CommonJS并由旧的Babel构建。
- 对于那些已经为此准备好的库,例如Lodash-es,Date-fns和您的代码或库, 树抖动很可能会充分起作用 。
- UglifyJS参与了程序集。
- 二手的ES6模块。
- 无副作用。
我们找到了减轻重量的方法,现在让我们教它只加载必要的功能。
我们仅加载必要的功能
我们将这一部分分为两部分。 在第一部分中,
仅加载用户所需的代码 :如果用户访问您网站的主页,则他不会加载个人帐户页面。 第二,
代码中的更改导致最小的资源重载 。
我们只加载必要的代码
考虑一个虚构应用程序的结构。 它具有:

我们要解决的第一个问题是
发出通用代码 。 让我们将红色代码表示为所有页面的通用代码,将绿色圆圈表示为主页面和搜索页面。 其余数字并不是特别重要。

当用户从主页上进行搜索时,尽管他已经有了,但他将再次重新加载框和圈子。 理想情况下,我们希望看到这样的内容。

很好的是,Webpack 4已经有一个内置插件可以为我们完成此
任务-SplitChunksPlugin 。 该插件取出应用程序代码或节点模块代码,这些代码由一个单独的块中的多个块使用,同时确保具有公共代码的块将大于30 Kb,并且要加载页面,您需要下载不超过5个块。 该策略是最佳的:加载太小的块是无利可图的,加载太多的块很
长,而且效率甚至不及在http2上下载更少的块。 要在2个或3个版本的Webpack上重复此行为,我必须写20-30行具有未记录功能的行。 现在,这一问题正在一线解决。
CSS外卖
如果我们仍然在单独的文件中为每个块取出CSS,那就太好了。 为此,有一个现成的解决方案
-Mini-Css-Extract-Plugin 。 该插件仅出现在Webpack 4中,在此之前,还没有足够的解决方案来完成此任务-只有hack,痛苦和腿伤。 该插件
从异步块中删除了CSS,并且是
专门为此任务创建的,它可以完美执行。
最小可能的资源重载
我们将弄清楚如何确保在发布(例如)主页上的新促销块时,用户
能够重新加载代码的最小部分 。
如果我们有版本控制,一切都会很好。 这里是版本N的主页,在发行了促销块之后-版本N +1。 Webpack提供了使用散列的开箱即用的类似机制。 在Webpack收集了所有资产(在本例中为app.js)之后,它将计算其内容哈希并将其添加到文件名中以获取app [哈希] .js。 这是我们需要的
版本控制 。

让我们检查一下它是如何工作的。 打开散列,在主页上进行更改,然后查看主页代码是否确实已更改,我们将看到两个文件已更改:main和app.js。

为什么会发生这种情况,因为这是不合逻辑的? 要了解原因,让我们
看一下app.js。 它包括三个部分:
当我们更改main中的代码时,其内容和哈希会更改,这意味着
指向它的
链接在应用程序中也会更改。 该应用程序本身也将更改,需要重新启动。 解决此问题的方法是
将 app.js分为两个块:应用程序代码和webpack运行时以及到异步块的链接。 Webpack 4通过一个
runtimeChunk选项为我们完成了所有工作,该选项重量很小
-gzip少于2 KB。 为用户重新启动它实际上是毫无用处的。 仅使用以下一个选项启用RuntimeChunk:
optimization: { runtimeChunk: true }
在Webpack 3和2中,我们将写5-6行,而不是一行。 这不多,但仍然是不必要的麻烦。

一切都很棒,我们学会了建立链接和运行时! 让我们在main中编写一个新模块,释放它,然后-op! -现在,一般而言,一切都会重新开始。

为什么这样 让我们看看模块如何在webpack中工作。
Webpack模块
假设有代码添加模块
a ,
b ,
d和
e :
import a from 'a'; import b from 'b'; import d from 'd'; import e from 'e';
Webpack将导入转换为require:将
a ,
b ,
d和
e替换为require(0),require(1),require(2)和require(3)。
var a = require(0); var b = require(1); var d = require(2); var e = require(3);
想象一下经常发生的情况:您编写了一个新模块
import c from 'c';
并将其粘贴到中间的某个位置:
import a from 'a'; import b from 'b'; import c from 'c'; import d from 'd'; import e from 'e';
当Webpack处理所有内容时,它将新模块的导入转换为require(2):
var a = require(0); var b = require(1); var c = require(2); var d = require(3); var e = require(4);
模块
d和
e分别为2和3,将接收数字3和4-新的ID。 由此得出一个简单的结论:使用序列号作为id有点愚蠢,但是Webpack做到了。
不要将序列号用作唯一ID
要解决此问题,有一个内置的Webpack解决方案
-HashedModuleIdsPlugin :
new webpack.HashedModuleIdsPlugin({ hashFunction: 'md4′, hashDigest:'base64′, hashDigestLength: 4, }),
此插件从文件的绝对路径使用4个字符的
md4哈希代替数字ID。 有了它,我们的需求将变成这些:
var a = require('YmRl'); var b = require('N2Fl'); var c = require('OWE4′); var d = require('NWQz'); var e = require('YWVj');
出现了字母,而不是数字。 当然,有一个隐藏的问题-这是
哈希的
冲突 。 我们偶然发现它,建议您使用8个字符而不是4个字符。正确配置散列后,一切将按照我们最初想要的方式工作。
现在,我们知道如何收集梦s。
我们学会了收集,现在我们将继续努力。
如何快速组装梦境捆绑包?
在我们的N1.RU中,最大的应用程序包含10,000个模块,并且不进行优化就需要28分钟。 我们能够将组装速度加快到两分钟! 我们是如何做到的? 有3种方法可以加快任何计算速度,这三种方法均适用于Webpack。
程序集并行化
我们要做的第一件事是
并行化程序集 。 为此,我们有:
- HappyPackPlugin ,它将您的装载程序包装在其他装载程序中,并接受包装在单独进程中的所有计算。 例如,这允许并行化Babel和node-sass。
- 线程加载器 。 执行与HappyPackPlugin大致相同的操作,仅不使用进程,而是使用线程池。 切换到单独的线程是一项代价高昂的操作,请谨慎使用它,并且仅当您要包装占用大量资源的繁重操作(例如babel或node-sass)时,才应谨慎使用。 例如,对于加载json,不需要并行化,因为它可以快速加载。
- 您最可能使用的插件和加载器已经具有内置的并行化工具 -您只需要看一下即可。 例如,此选项在UglifyJS中 。
缓存构建结果
缓存程序集结果是加速Webpack组装的最有效方法。
我们拥有的第一个解决方案是
cache-loader 。 这是一个装入程序链的装入程序,并将针对特定装入程序链构建特定文件的结果保存到文件系统中。 在捆绑软件的下一个组合中,如果此文件位于文件系统上并且已通过此链处理,则cache-loader将获取结果,并且不会调用其后的那些加载器,例如Babel-loader或node-sass。
该图显示了组装时间。 蓝条-100%的构建时间,没有缓存加载器,并且慢了7%。 这是因为缓存加载器会花费额外的时间将缓存保存到文件系统。 在第二次大会上,我们已经获得了可观的利润-大会的速度提高了2倍。

第二种解决方案是更
复杂的-HardSourcePlugin 。 主要区别在于:缓存加载器只是一个只能在带有代码或文件的加载器链中运行的加载器,而HardSourcePlugin几乎可以完全访问Webpack生态系统,可以与其他插件和加载器一起运行,并且扩展了生态系统以进行少量缓存。 上图显示,在第一次启动时,构建时间增加了37%,但是在第二次启动时使用所有缓存,我们加快了5倍。

最好的部分是您可以同时使用这两种解决方案,这是我们在N1.RU上所做的。 请注意,因为缓存存在问题,我将在稍后讨论。
您已经使用的插件/加载程序可能具有
内置的缓存机制 。 例如,
babel-loader具有非常高效的缓存系统,但是由于某种原因,默认情况下它是关闭的。 相同的功能在
awesome-typeScript-loader中 。
UglifyJS插件也具有缓存,效果很好。 他使我们加速了几分钟。
现在的问题。
缓存问题
- 缓存可能无法正确验证 。
- 应用的解决方案可能无法与连接的插件,加载器,您的代码或彼此结合使用 。 在这方面,缓存加载器是一种简单且轻松的解决方案。 但是使用HardSourcePlugin时,您需要更加小心。
- 如果一切都坏了,很难登场 。 当缓存无法正常工作并且发生无法理解的错误时,将很难找出问题所在。
如何节省生产?
加快流程的最后一种方法是不执行流程的任何部分。 让我们考虑一下如何节省生产成本? 我们不能做什么? 答案很简单-
我们无能为力 ! 我们无权拒绝生产中的某些东西,但是我们可以在
dev中节省很多。
节省什么:
- 在我们需要它们之前, 不要收集源地图 。
- 使用样式加载器,而不是使用CSS移除和通过CSS加载器进行处理的酷方案。 样式加载器本身非常快,因为它使用css行并将其推入将该行插入样式标签的函数中。
- 您只能在浏览器列表中保留您专门使用的浏览器-最有可能是最后一个镶边 。 这将大大加速 。
- 完全放弃任何资源优化 :从UglifyJS,css-nano,gzip / brotli。
构建加速是并行化,缓存和拒绝计算。 通过遵循这三个简单步骤,您可以大大加快速度。
如何配置webpack?
我们已经弄清楚了如何组装梦想包以及如何快速组装它,现在我们将弄清楚如何配置Webpack,以免每次更改配置时都无法自拔。
项目中的配置演变
项目中的典型webpack-config路径以
简单的配置开始。 首先,您只需插入Webpack,Babel-loader,sass-loader,一切都很好。 然后,出乎意料的是,某些
条件出现
在process.env上 ,并插入了这些条件。 一,二,三,越来越多,直到添加了带有“魔术”选项的条件为止。 您了解一切都已经很糟糕了,最好只是
复制用于开发和生产
的配置 ,并进行两次更正。 一切都会更加清晰。 如果您有一个想法:“这里有什么问题吗?”,那么唯一可行的建议是
保持配置井然有序 。 我会告诉你我们如何做到的。
保持配置顺序
我们使用
webpack-merge包。 这是一个npm软件包,用于将多个配置组合为一个。 如果您对默认的合并策略不满意,可以对其进行自定义。
4 :
- Loaders.
- Plugins.
- Presets.
- Parts.
.
Plugin/Loader
, , API, , .
看起来像这样:
module.exports = function createPlugin(options) { return new Plugin(options); };
, , , . , url-loader :
, , , , , , . , , , , url-loader. :
function urlLoader(prefix = 'assets', limit = 100) { return { loader: 'url-loader', options: { limit, name: `${prefix}/[name].[hash].[ext]` } }; };
. , Loader .
Preset
webpack. , , , webpack, . — , , scss-:
{ test: /\.scss$/, use: [cssLoader, postCssLoader, scssLoader] }
.
Part
— , . , , . , :
entry: { app: './src/Frontend/app.js' }, output: { publicPath: '/static/cabinet/app/', path: path.resolve('www/static/app') },
:
- , , , json, , , splitChunks.
- dev , , js/css
- Part , output, publicPath, entry-point , , source map.
Webpack-merge . , . webpack-merge 3-7 , Babel-loader, . , .
.
, .
, webpack — .
, .
, !
— Frontend Conf . , — , , Frontend Conf ++ .
- ? FrontenConf ++ , 27 28 . 27 , 15 . — !