Heim > Artikel > Web-Frontend > So verwenden Sie Webpack zur Optimierung von Ressourcenmethoden und -techniken
Bei der Optimierung von Front-End-Anwendungen ist es äußerst wichtig, die Größe der geladenen Ressourcen zu kontrollieren. Meistens können wir die Größe kontrollieren, Ressourcen aufteilen und während des Paketierungs- und Kompilierungsprozesses wiederverwenden. In diesem Artikel wird hauptsächlich die Verwendung von Webpack zur Optimierung von Ressourcen vorgestellt. Der Herausgeber hält es für recht gut, daher werde ich es jetzt mit Ihnen teilen und als Referenz verwenden. Folgen wir dem Herausgeber und schauen wir uns das an. Ich hoffe, es kann allen helfen.
Vorwort
Dieser Artikel basiert hauptsächlich auf der Webpack-Verpackung und verwendet in React, Vue und anderen Ökosystemen entwickelte Single-Page-Anwendungen als Beispiele, um den Umgang mit Ressourcen und Cache auf der Webpack-Verpackungsebene zu veranschaulichen Das Wichtigste, was wir tun müssen, ist die Konfiguration des Webpacks zu optimieren und auch einige kleine Änderungen am Geschäftscode vorzunehmen.
Gleichzeitig kann das Plug-in (webpack-contrib/webpack-bundle-analyzer) zur Analyse gepackter Ressourcen verwendet werden. In diesem Artikel gibt es natürlich viele optionale Analyse-Plug-ins. Dieses Plug-In wird hauptsächlich als Beispiel verwendet.
TIPP: Webpack-Version @3.6.0
1. Verpackungsumgebung und Codekomprimierung
Zuerst haben wir eine grundlegende Webpack-Konfiguration:
// webpack.config.js const path = require('path'); const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; const PROJECT_ROOT = path.resolve(__dirname, './'); module.exports = { entry: { index: './src/index.js' }, output: { path: path.resolve(__dirname, 'dist'), filename: '[name].[chunkhash:4].js' }, module: { rules: [ { test: /\.js[x]?$/, use: 'babel-loader', include: PROJECT_ROOT, exclude: /node_modules/ } ] }, plugins: [ new BundleAnalyzerPlugin() ], resolve: { extensions: ['.js', '.jsx'] }, };
Nach der Ausführung des Pakets können Sie sehen, dass die js eines Projekts mehr als 1 Mio. beträgt:
Hash: e51afc2635f08322670b Version: webpack 3.6.0 Time: 2769ms Asset Size Chunks Chunk Names index.caa7.js 1.3 MB 0 [emitted] [big] index
Zu diesem Zeitpunkt müssen Sie nur die Plug-Ins „DefinePlugin“ und „UglifyJSPlugin“ hinzufügen, um sie zu reduzieren Viel Volumen. Fügen Sie Plugins hinzu:
// webpack.config.js ... { ... plugins: [ new BundleAnalyzerPlugin(), new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production') }), new UglifyJSPlugin({ uglifyOptions: { ie8: false, output: { comments: false, beautify: false, }, mangle: { keep_fnames: true }, compress: { warnings: false, drop_console: true }, } }) ] ... }
Sie können die Verpackungsausgabe zu diesem Zeitpunkt sehen:
Hash: 84338998472a6d3c5c25 Version: webpack 3.6.0 Time: 9940ms Asset Size Chunks Chunk Names index.89c2.js 346 kB 0 [emitted] [big] index
Die Codegröße wurde von 1,3 Mio. auf 346 KB reduziert.
(1) DefinePlugin
DefinePlugin ermöglicht die Erstellung einer globalen Konstante, die zur Kompilierungszeit konfiguriert werden kann. Dies kann nützlich sein, um unterschiedliche Verhaltensweisen für Builds im Entwicklungsmodus und im Releasemodus zu ermöglichen. Wenn Sie die Protokollierung in einem Entwicklungs-Build, aber nicht in einem Release-Build durchführen, können Sie globale Konstanten verwenden, um zu entscheiden, ob eine Protokollierung durchgeführt werden soll oder nicht. Hier kommt DefinePlugin ins Spiel: Richten Sie es ein und vergessen Sie die Regeln für Entwicklung und Release-Builds.
In unserem Geschäftscode und Paketcode von Drittanbietern müssen wir „process.env.NODE_ENV“ häufig für unterschiedliche Verarbeitungen beurteilen, aber in der Produktionsumgebung benötigen wir offensichtlich keine Nicht-Produktion ` Verarbeitungsteil.
Hier setzen wir „process.env.NODE_ENV“ auf „JSON.stringify('produktion“)“, was bedeutet, dass die Verpackungsumgebung auf die Produktionsumgebung eingestellt ist. Anschließend können Sie mit dem Plug-in „UglifyJSPlugin“ beim Packen für die Produktionsumgebung redundanten Code entfernen.
(2) UglifyJSPlugin
UglifyJSPlugin wird hauptsächlich zum Parsen und Komprimieren von JS-Code verwendet. Es basiert auf „uglify-es“ und verfügt über eine Vielzahl von Konfigurationsoptionen: https : //github.com/webpack-contrib/uglifyjs-webpack-plugin.
Durch die Komprimierung des Codes und die Entfernung von Redundanz wird die Größe der verpackten Ressourcen erheblich reduziert.
2. Codeaufteilung/Laden bei Bedarf
In Einzelseitenanwendungen, die mit React oder Vue erstellt wurden, wird die Steuerung des Seitenroutings und der Ansichten durch das Frontend und das entsprechende Geschäft implementiert Logik ist alles im JS-Code.
Wenn ein Anwendungsdesign viele Seiten und Logik enthält, sind auch die endgültig generierten JS-Dateiressourcen recht groß.
Wenn wir jedoch eine Seite öffnen, die einer URL entspricht, ist nicht der gesamte JS-Code erforderlich. Es werden lediglich ein Hauptlaufzeitcode und der der Ansicht entsprechende Geschäftslogikcode benötigt diesen Teil des Codes in der nächsten Ansicht.
Daher besteht die Optimierung, die in dieser Hinsicht durchgeführt werden kann, darin, JS-Code bei Bedarf zu laden.
Lazy Loading oder On-Demand-Loading ist eine gute Möglichkeit, Webseiten oder Anwendungen zu optimieren. Diese Methode trennt Ihren Code tatsächlich an einigen logischen Haltepunkten und verweist dann nach Abschluss bestimmter Vorgänge in einigen Codeblöcken sofort auf andere neue Codeblöcke oder ist dabei, darauf zu verweisen. Dies beschleunigt das anfängliche Laden der App und verringert ihre Gesamtgröße, da bestimmte Codeblöcke möglicherweise nie geladen werden.
Webpack bietet dynamische Importtechnologie, um eine Codeaufteilung zu erreichen. Zuerst müssen Sie in der Konfiguration von Webpack die Konfiguration jedes geteilten Untermoduls konfigurieren:
// webpack.config.js const path = require('path'); const webpack = require('webpack'); const UglifyJSPlugin = require('uglifyjs-webpack-plugin'); const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; const PROJECT_ROOT = path.resolve(__dirname, './'); module.exports = { entry: { index: './src/index.js' }, output: { path: path.resolve(__dirname, 'dist'), filename: '[name].[chunkhash:4].js', chunkFilename: '[name].[chunkhash:4].child.js', }, module: { rules: [ { test: /\.js[x]?$/, use: 'babel-loader', include: PROJECT_ROOT, exclude: /node_modules/ } ] }, plugins: [ new BundleAnalyzerPlugin(), new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production') }), new UglifyJSPlugin({ uglifyOptions: { ie8: false, output: { comments: false, beautify: false, }, mangle: { keep_fnames: true }, compress: { warnings: false, drop_console: true }, } }), ], resolve: { extensions: ['.js', '.jsx'] }, };
Die Das Wichtigste, was definiert werden muss, ist der „chunkFilename“ in „output“, der der Dateiname des exportierten Split-Codes ist. Hier ist er auf „[name].[chunkhash:4].child.js“ festgelegt „name“ entspricht dem Modulnamen oder der Modul-ID und „chunkhash“ ist der Hash des Modulinhalts.
Danach bietet Webpack zwei Möglichkeiten zum dynamischen Importieren des Geschäftscodes:
`import('path/to/module') ->
`require.ensure(dependencies: String[], callback: function(require), errorCallback: function(error), chunkName: String)`
Für die neueste Version von Webpack wird hauptsächlich die Verwendung von „import()“ empfohlen (Hinweis: Import verwendet Promise, daher müssen Sie sicherstellen, dass der Code die Promise-Polyfüllung unterstützt).
// src/index.js function getComponent() { return import( /* webpackChunkName: "lodash" */ 'lodash' ).then(_ => { var element = document.createElement('p'); element.innerHTML = _.join(['Hello', 'webpack'], ' '); return element; }).catch(error => 'An error occurred while loading the component'); } getComponent().then(component => { document.body.appendChild(component); })
Sie können die Verpackungsinformationen sehen:
Hash: d6ba79fe5995bcf9fa4d Version: webpack 3.6.0 Time: 7022ms Asset Size Chunks Chunk Names lodash.89f0.child.js 85.4 kB 0 [emitted] lodash index.316e.js 1.96 kB 1 [emitted] index [0] ./src/index.js 441 bytes {1} [built] [2] (webpack)/buildin/global.js 509 bytes {0} [built] [3] (webpack)/buildin/module.js 517 bytes {0} [built] + 1 hidden module
Sie können sehen, dass der gepackte Code „index.316e.js“ und „lodash.89f0.child.js“ generiert hat Zwei Dateien, letztere wird durch „Importieren“ aufgeteilt.
`import` 它接收一个 `path` 参数,指的是该子模块对于的路径,同时还注意到其中可以添加一行注释 `/* webpackChunkName: "lodash" */`,该注释并非是无用的,它定义了该子模块的 name,其对应与 `output.chunkFilename` 中的 `[name]`。
`import` 函数返回一个 Promise,当异步加载到子模块代码是会执行后续操作,比如更新视图等。
(1)React 中的按需加载
在 React 配合 React-Router 开发中,往往就需要代码根据路由按需加载的能力,下面是一个基于 webpack 代码动态导入技术实现的 React 动态载入的组件:
import React, { Component } from 'react'; export default function lazyLoader (importComponent) { class AsyncComponent extends Component { state = { Component: null } async componentDidMount () { const { default: Component } = await importComponent(); this.setState({ Component: Component }); } render () { const Component = this.state.Component; return Component ? <Component {...this.props} /> : null; } } return AsyncComponent; };
在 `Route` 中:
<Switch> <Route exact path="/" component={lazyLoader(() => import('./Home'))} /> <Route path="/about" component={lazyLoader(() => import('./About'))} /> <Route component={lazyLoader(() => import('./NotFound'))} /> </Switch>
在 `Route` 中渲染的是 `lazyLoader` 函数返回的组件,该组件在 mount 之后会去执行 `importComponent` 函数(既:`() => import('./About')`)动态加载其对于的组件模块(被拆分出来的代码),待加载成功之后渲染该组件。
使用该方式打包出来的代码:
Hash: 02a053d135a5653de985 Version: webpack 3.6.0 Time: 9399ms Asset Size Chunks Chunk Names 0.db22.child.js 5.82 kB 0 [emitted] 1.fcf5.child.js 4.4 kB 1 [emitted] 2.442d.child.js 3 kB 2 [emitted] index.1bbc.js 339 kB 3 [emitted] [big] index
三、抽离 Common 资源
(1)第三方库的长缓存
首先对于一些比较大的第三方库,比如在 React 中用到的 react、react-dom、react-router 等等,我们不希望它们被重复打包,并且在每次版本更新的时候也不希望去改变这部分的资源导致在用户端重新加载。
在这里可以使用 webpack 的 CommonsChunkPlugin 来抽离这些公共资源;
CommonsChunkPlugin 插件,是一个可选的用于建立一个独立文件(又称作 chunk)的功能,这个文件包括多个入口 chunk 的公共模块。通过将公共模块拆出来,最终合成的文件能够在最开始的时候加载一次,便存起来到缓存中供后续使用。这个带来速度上的提升,因为浏览器会迅速将公共的代码从缓存中取出来,而不是每次访问一个新页面时,再去加载一个更大的文件。
首先需要在 entry 中新增一个入口用来打包需要抽离出来的库,这里将 `'react', 'react-dom', 'react-router-dom', 'immutable'` 都给单独打包进 `vendor` 中;
之后在 plugins 中定义一个 `CommonsChunkPlugin` 插件,同时将其 `name` 设置为 `vendor` 是它们相关联,再将 `minChunks` 设置为 `Infinity` 防止其他代码被打包进来。
// webpack.config.js const path = require('path'); const webpack = require('webpack'); const UglifyJSPlugin = require('uglifyjs-webpack-plugin'); const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; const PROJECT_ROOT = path.resolve(__dirname, './'); module.exports = { entry: { index: './src0/index.js', vendor: ['react', 'react-dom', 'react-router-dom', 'immutable'] }, output: { path: path.resolve(__dirname, 'dist'), filename: '[name].[chunkhash:4].js', chunkFilename: '[name].[chunkhash:4].child.js', }, module: { rules: [ { test: /\.js[x]?$/, use: 'babel-loader', include: PROJECT_ROOT, exclude: /node_modules/ } ] }, plugins: [ new BundleAnalyzerPlugin(), new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production') }), new UglifyJSPlugin({ uglifyOptions: { ie8: false, output: { comments: false, beautify: false, }, mangle: { keep_fnames: true }, compress: { warnings: false, drop_console: true }, } }), new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', minChunks: Infinity, }), ], resolve: { extensions: ['.js', '.jsx'] }, };
运行打包可以看到:
Hash: 34a71fcfd9a24e810c21 Version: webpack 3.6.0 Time: 9618ms Asset Size Chunks Chunk Names 0.2c65.child.js 5.82 kB 0 [emitted] 1.6e26.child.js 4.4 kB 1 [emitted] 2.e4bc.child.js 3 kB 2 [emitted] index.4e2f.js 64.2 kB 3 [emitted] index vendor.5fd1.js 276 kB 4 [emitted] [big] vendor
可以看到 `vendor` 被单独打包出来了。
当我们改变业务代码时再次打包:
Hash: cd3f1bc16b28ac97e20a Version: webpack 3.6.0 Time: 9750ms Asset Size Chunks Chunk Names 0.2c65.child.js 5.82 kB 0 [emitted] 1.6e26.child.js 4.4 kB 1 [emitted] 2.e4bc.child.js 3 kB 2 [emitted] index.4d45.js 64.2 kB 3 [emitted] index vendor.bc85.js 276 kB 4 [emitted] [big] vendor
vendor 包同样被打包出来的,然而它的文件 hash 却发生了变化,这显然不符合我们长缓存的需求。
这是因为 webpack 在使用 CommoChunkPlugin 的时候会生成一段 runtime 代码(它主要用来处理代码模块的映射关系),而哪怕没有改变 vendor 里的代码,这个 runtime 仍然是会跟随着打包变化的并且打入 verdor 中,所以 hash 就会开始变化了。解决方案则是把这部分的 runtime 代码也单独抽离出来,修改之前的 `CommonsChunkPlugin` 为:
// webpack.config.js ... new webpack.optimize.CommonsChunkPlugin({ name: ['vendor', 'runtime'], minChunks: Infinity, }), ...
执行打包可以看到生成的代码中多了 `runtime` 文件,同时即使改变业务代码,vendor 的 hash 值也保持不变了。
当然这段 `runtime` 实际上非常短,我们可以直接 inline 在 html 中,如果使用的是 `html-webpack-plugin` 插件处理 html,则可以结合 [`html-webpack-inline-source-plugin`](DustinJackson/html-webpack-inline-source-plugin) 插件自动处理其 inline。
(2)公共资源抽离
在我们打包出来的 js 资源包括不同入口以及子模块的 js 资源包,然而它们之间也会重复载入相同的依赖模块或者代码,因此可以通过 CommonsChunkPlugin 插件将它们共同依赖的一些资源打包成一个公共的 js 资源。
// webpack.config.js plugins: [ new BundleAnalyzerPlugin(), new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production') }), new UglifyJSPlugin({ uglifyOptions: { ie8: false, output: { comments: false, beautify: false, }, mangle: { keep_fnames: true }, compress: { warnings: false, drop_console: true }, } }), new webpack.optimize.CommonsChunkPlugin({ name: ['vendor', 'runtime'], minChunks: Infinity, }), new webpack.optimize.CommonsChunkPlugin({ // ( 公共chunk(commnons chunk) 的名称) name: "commons", // ( 公共chunk 的文件名) filename: "commons.[chunkhash:4].js", // (模块必须被 3个 入口chunk 共享) minChunks: 3 }) ],
可以看到这里增加了 `commons` 的一个打包,当一个资源被三个以及以上 chunk 依赖时,这些资源会被单独抽离打包到 `commons.[chunkhash:4].js` 文件。
执行打包,看到结果如下:
Hash: 2577e42dc5d8b94114c8 Version: webpack 3.6.0 Time: 24009ms Asset Size Chunks Chunk Names 0.2eee.child.js 90.8 kB 0 [emitted] 1.cfbc.child.js 89.4 kB 1 [emitted] 2.557a.child.js 88 kB 2 [emitted] vendor.66fd.js 275 kB 3 [emitted] [big] vendor index.688b.js 64.2 kB 4 [emitted] index commons.a61e.js 1.78 kB 5 [emitted] commons
却发现这里的 `commons.[chunkhash].js` 基本没有实际内容,然而明明在每个子模块中也都依赖了一些相同的依赖。
借助 webpack-bundle-analyzer 来分析一波:
可以看到三个模块都依赖了 `lodash`,然而它并没有被抽离出来。
这是因为 CommonsChunkPlugin 中的 chunk 指的是 entry 中的每个入口,因此对于一个入口拆分出来的子模块(children chunk)是不生效的。
可以通过在 CommonsChunkPlugin 插件中配置 `children` 参数将拆分出来的子模块的公共依赖也打包进 `commons` 中:
// webpack.config.js plugins: [ new BundleAnalyzerPlugin(), new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production') }), new UglifyJSPlugin({ uglifyOptions: { ie8: false, output: { comments: false, beautify: false, }, mangle: { keep_fnames: true }, compress: { warnings: false, drop_console: true }, } }), new webpack.optimize.CommonsChunkPlugin({ name: ['vendor', 'runtime'], minChunks: Infinity, }), new webpack.optimize.CommonsChunkPlugin({ // ( 公共chunk(commnons chunk) 的名称) name: "commons", // ( 公共chunk 的文件名) filename: "commons.[chunkhash:4].js", // (模块必须被 3个 入口chunk 共享) minChunks: 3 }), new webpack.optimize.CommonsChunkPlugin({ // (选择所有被选 chunks 的子 chunks) children: true, // (在提取之前需要至少三个子 chunk 共享这个模块) minChunks: 3, }) ],
查看打包效果:
其子模块的公共资源都被打包到 `index` 之中了,并没有理想地打包进 `commons` 之中,还是因为 `commons` 对于的是 entry 中的入口模块,而这里并未有 3 个 entry 模块共用资源;
在单入口的应用中可以选择去除 `commons`,而在子模块的 `CommonsChunkPlugin` 的配置中配置 `async` 为 `true`:
// webpack.config.js plugins: [ new BundleAnalyzerPlugin(), new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production') }), new UglifyJSPlugin({ uglifyOptions: { ie8: false, output: { comments: false, beautify: false, }, mangle: { keep_fnames: true }, compress: { warnings: false, drop_console: true }, } }), new webpack.optimize.CommonsChunkPlugin({ name: ['vendor', 'runtime'], minChunks: Infinity, }), new webpack.optimize.CommonsChunkPlugin({ // (选择所有被选 chunks 的子 chunks) children: true, // (异步加载) async: true, // (在提取之前需要至少三个子 chunk 共享这个模块) minChunks: 3, }) ],
查看效果:
子模块的公共资源都被打包到 `0.9c90.child.js` 中了,该模块则是子模块的 commons。
四、tree shaking
tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块系统中的静态结构特性,例如 import 和 export。这个术语和概念实际上是兴起于 ES2015 模块打包工具 rollup。
在我们引入一个依赖的某个输出的时候,我们可能需要的仅仅是该依赖的某一部分代码,而另一部分代码则是 `unused` 的,如果能够去除这部分代码,那么最终打包出来的资源体积也是可以有可观的减小。
首先,webpack 中实现 tree shaking 是基于 webpack 内部支持的 es2015 的模块机制,在大部分时候我们使用 babel 来编译 js 代码,而 babel 会通过自己的模块加载机制处理一遍,这导致 webpack 中的 tree shaking 处理将会失效。因此在 babel 的配置中需要关闭对模块加载的处理:
// .babelrc { "presets": [ [ "env", { "modules": false, } ], "stage-0" ], ... }
然后我们来看下 webpack 是如何处理打包的代码,举例有一个入口文件 `index.js` 和一个 `utils.js` 文件:
// utils.js export function square(x) { return x * x; } export function cube(x) { return x * x * x; } " "js // index.js import { cube } from './utils.js'; console.log(cube(10)); " 打包出来的代码: " // index.bundle.js /* 1 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; /* unused harmony export square */ /* harmony export (immutable) */ __webpack_exports__["a"] = cube; function square(x) { return x * x; } function cube(x) { return x * x * x; }
可以看到仅有 `cube` 函数被 `__webpack_exports__` 导出来,而 `square` 函数被标记为 `unused harmony export square`,然而在打包代码中既是 `square` 没有被导出但是它仍然存在与代码中,而如何去除其代码则可以通过添加 `UglifyjsWebpackPlugin` 插件来处理。
相关推荐:
Das obige ist der detaillierte Inhalt vonSo verwenden Sie Webpack zur Optimierung von Ressourcenmethoden und -techniken. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!