この記事では主に Webpack 永続キャッシュの実践について紹介し、参考にしていきます。
まえがき
最近、webpack が永続キャッシュをどのように実行するかを調べていましたが、まだいくつかの落とし穴があることに気づきました。この記事を読んだ後、それらを整理して要約することができます。
永続キャッシュとは何ですか? 永続キャッシュを行う理由は何ですか?
webpack はどのように永続キャッシュを行うのですか?
Webpack キャッシュに関するいくつかのメモ。
永続キャッシュ
まず第一に、フロントエンドとバックエンド、フロントエンドの HTML、CSS を分離するアプリケーションの現在の人気を踏まえて、永続キャッシュとは何なのかを説明する必要があります。 js は静的なリソース ファイルであることが多く、フォームはサーバー上に存在し、データはインターフェイスを通じて取得され、動的コンテンツが表示されます。これには、企業がフロントエンド コードをデプロイする方法の問題が関係するため、ページを最初にデプロイするか、それともリソースを最初にデプロイするかという更新デプロイの問題が関係します。
最初にページをデプロイし、次にリソースをデプロイします: 2 つのデプロイメントの間の時間間隔中に、ユーザーがページにアクセスすると、古いリソースが新しいページ構造にロードされ、リソースの古いバージョンがキャッシュされます。その結果、ユーザーは不規則なスタイルでページにアクセスし、手動で更新しない限り、ページはリソース キャッシュが期限切れになるまで不規則な状態のままになります。
最初にリソースをデプロイし、次にページをデプロイします。デプロイ時間間隔中に、古いバージョンのリソースのローカル キャッシュを持つユーザーが Web サイトにアクセスします。要求されたページは古いバージョンであり、リソース参照が変更されていないため、ブラウザーは直接 Web サイトにアクセスします。ローカル キャッシュを使用するため、これは通常の状況ですが、ローカル キャッシュを持たないユーザー、またはキャッシュの有効期限が切れたユーザーが Web サイトにアクセスすると、古いバージョンのページが新しいバージョンのリソースを読み込むため、ページ実行エラーが発生します。
そのため、オンライン コードを更新するときに、オンライン ユーザーがスムーズに移行して Web サイトを正しく開くことができるようにする展開戦略が必要です。
最初にこの回答を読むことをお勧めします: 大企業でフロントエンド コードを開発およびデプロイするにはどうすればよいですか?
上記の回答を読むと、ファイルが変更されるたびに生成されるハッシュ値が異なるため、現在のより成熟した永続キャッシュ ソリューションは静的リソースの名前の後にハッシュ値を追加することであることが大まかに理解できるでしょう。この利点は、以前のファイルを上書きしてオンライン ユーザー アクセスが失敗することを避けるために、ファイルを段階的に公開することです。
毎回公開される静的リソース (css、js、img) の名前が一意である限り、次のことが可能です:
HTML ファイルの場合: キャッシュを有効にせず、HTML を自分のサーバーに配置します。サーバーでは、サーバーのキャッシュをオフにします。サーバーは HTML ファイルとデータ インターフェイスのみを提供します
静的な js、css、画像、その他のファイルの場合: cdn とキャッシュを有効にし、静的リソースを cdn サービス プロバイダーにアップロードします。各リソースのパスは一意であるため、リソースが上書きされることはなく、オンライン ユーザー アクセスの安定性が確保されます。
更新がリリースされるたびに、静的リソース (js、css、img) が最初に cdn サービスに転送され、次に html ファイルがアップロードされます。これにより、古いユーザーが正常にアクセスできるようになるだけでなく、新しいユーザーもアクセスできるようになります。「新しいページ」を参照してください。
上記では、主流のフロントエンド永続キャッシュ ソリューションを簡単に紹介しましたが、なぜ永続キャッシュを行う必要があるのでしょうか?
ユーザーがブラウザを使用して初めてサイトにアクセスすると、ページにさまざまな静的リソースが導入されます。永続的なキャッシュを実現できれば、HTTP 応答ヘッダーに Cache-control または Expires フィールドを追加できます。キャッシュを増やすと、ブラウザはこれらのリソースを 1 つずつローカルにキャッシュできます。
ユーザーが次回以降の訪問中に同じ静的リソースを再度リクエストする必要があり、静的リソースの有効期限が切れていない場合、ブラウザはネットワーク経由でリソースをリクエストする代わりに、ローカル キャッシュを直接使用できます。
Webpack は永続キャッシュをどのように行うか
永続キャッシュを簡単に紹介した後、次のことが重要なポイントです。では、Webpack で永続キャッシュをどのように実行する必要がありますか? 次の 2 つのことを行う必要があります:
一意性を保証するつまり、パッケージ化されたコンテンツが一貫性がない限り、ハッシュ値も一貫性がなくなります。
ハッシュ値の安定性を確保するには、モジュールが変更されたときに、影響を受けるパッケージ化されたファイルのハッシュ値のみが変更され、モジュールに関係のないパッケージ化されたファイルのハッシュ値は変更されないことを保証する必要があります。
ハッシュ ファイル名は、永続的なキャッシュを実装するための最初のステップです。現在、webpack にはハッシュを計算する 2 つの方法 ([hash] と [chunkhash]) があります。
hash は、webpack がコンパイル中に毎回計算することを意味します。 process プロジェクト内のファイルが変更されたときに再作成される一意のハッシュ値を生成し、webpack が新しいハッシュ値を計算します。
chunkhashはモジュールに基づいて計算されたハッシュ値であるため、特定のファイルを変更してもそのハッシュ値のみに影響し、他のファイルには影響しません。
そのため、すべてのコンテンツを同じファイルにパックするだけであれば、ハッシュで満足できます。プロジェクトに解凍やモジュールのロードなどが含まれる場合は、更新のたびに関連するものだけを確実に保存するためにチャンクハッシュを使用する必要があります。ファイルのハッシュ値が変化します。
したがって、永続キャッシュを使用した Webpack 構成は次のようになります:
module.exports = { entry: __dirname + '/src/index.js', output: { path: __dirname + '/dist', filename: '[name].[chunkhash:8].js', } }
上記のコードの意味は、index.js をエントリ ポイントとして使用し、すべてのコードをindex.xxxx js という名前のファイルにパッケージ化して配置します。これで、プロジェクトを更新するたびに新しい名前のファイルを生成できるようになります。
単純なシナリオを扱う場合はこれで十分ですが、大規模な複数ページのアプリケーションでは、多くの場合、ページのパフォーマンスを最適化する必要があります:
ビジネス コードとサードパーティ コードを分離する: ビジネス コードを分離する理由コードが分離されている ビジネスコードは頻繁に更新され、サードパーティコードは更新と反復が遅いため、サードパーティコードから分離されています。そのため、サードパーティコード(ライブラリ、フレームワーク)を分離して、ブラウザのキャッシュを使用してサードパーティのコードをロードします。
オンデマンドでのロード: たとえば、React-Router を使用する場合、ユーザーが特定のルートにアクセスする必要がある場合、対応するコンポーネントがロードされます。そのため、ユーザーは最初にすべてのルーティング コンポーネントをダウンロードする必要はありません。地元。
マルチページ アプリケーションでは、ヘッダー、フッターなどの共通モジュールを抽出できることがよくあります。これにより、ページがジャンプしたときに、これらの共通モジュールはキャッシュ内に存在するため、代わりに直接読み込むことができます。さらにネットワーク要求を行う。
したがって、モジュールを解凍してモジュールにロードするには、webpack の組み込みプラグインである CommonsChunkPlugin が必要です。以下では、例を使用して webpack の構成方法を説明します。
この記事のコードは私の Github にありますので、ご興味があればダウンロードしてご覧ください:
git clone https://github.com/happylindz/blog.git cd blog/code/multiple-page-webpack-demo npm install
以下の内容を読む前に、私の前回の記事を読むことを強くお勧めします。 Webpack ファイルのパッケージ化メカニズムと Webpack ファイルについての理解 パッケージング メカニズムは、永続的なキャッシュをより適切に実装するのに役立ちます。
この例は大まかに次のように説明されています: pageA と pageB の 2 つのページで構成されています
// src/pageA.js import componentA from './common/componentA'; // 使用到 jquery 第三方库,需要抽离,避免业务打包文件过大 import $ from 'jquery'; // 加载 css 文件,一部分为公共样式,一部分为独有样式,需要抽离 import './css/common.css' import './css/pageA.css'; console.log(componentA); console.log($.trim(' do something ')); // src/pageB.js // 页面 A 和 B 都用到了公共模块 componentA,需要抽离,避免重复加载 import componentA from './common/componentA'; import componentB from './common/componentB'; import './css/common.css' import './css/pageB.css'; console.log(componentA); console.log(componentB); // 用到异步加载模块 asyncComponent,需要抽离,加载首屏速度 document.getElementById('xxxxx').addEventListener('click', () => { import( /* webpackChunkName: "async" */ './common/asyncComponent.js').then((async) => { async(); }) }) // 公共模块基本长这样 export default "component X";
上記のページ コンテンツには、基本的に、パブリック ライブラリの分割、オンデマンドのロード、およびパブリック モジュールの分割という 3 つのモジュール分割モードが含まれています。次に、次のステップは webpack を設定することです:
const path = require('path'); const webpack = require('webpack'); const ExtractTextPlugin = require('extract-text-webpack-plugin'); module.exports = { entry: { pageA: [path.resolve(__dirname, './src/pageA.js')], pageB: path.resolve(__dirname, './src/pageB.js'), }, output: { path: path.resolve(__dirname, './dist'), filename: 'js/[name].[chunkhash:8].js', chunkFilename: 'js/[name].[chunkhash:8].js' }, module: { rules: [ { // 用正则去匹配要用该 loader 转换的 CSS 文件 test: /.css$/, use: ExtractTextPlugin.extract({ fallback: "style-loader", use: ["css-loader"] }) } ] }, plugins: [ new webpack.optimize.CommonsChunkPlugin({ name: 'common', minChunks: 2, }), new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', minChunks: ({ resource }) => ( resource && resource.indexOf('node_modules') >= 0 && resource.match(/.js$/) ) }), new ExtractTextPlugin({ filename: `css/[name].[chunkhash:8].css`, }), ] }
最初の CommonsChunkPlugin はパブリック モジュールを抽出するために使用されます。これは、webpack ボスと言うのと同じです。モジュールが 2 回以上読み込まれている場合は、それを次の場所に移動するのを手伝ってください。共通チャンク、ここでの minChunks は 2 であり、粒度は最も小さくなります。実際の状況に応じて、モジュールを分割するために使用する回数を選択できます。
2 番目の CommonsChunkPlugin は、サードパーティのコードを抽出し、それらを抽出し、リソースが node_modules からのものであるかどうかを判断し、そうである場合は、それらがサードパーティのモジュールであることを意味し、それらを抽出するために使用されます。これは、webpack ボスに、node_modules ディレクトリからのモジュールがあり、その名前が .js で終わる場合は、ベンダー チャンクに移動してください、ベンダー チャンクが存在しない場合は、新しいチャンクを作成するように指示することと同じです。
この構成の利点は何ですか? ビジネスが成長するにつれて、サードパーティのコードを保存するための入り口を特別に構成する場合、webpack.config.js に依存することが多くなる可能性があります。
// 不利于拓展 module.exports = { entry: { app: './src/main.js', vendor: [ 'vue', 'axio', 'vue-router', 'vuex', // more ], }, }
3 番目の ExtractTextPlugin プラグインは、パッケージ化された js ファイルから css を抽出し、独立した css ファイルを生成するために使用されます。論理的には、スタイルを変更するだけの場合、ページを変更する機能はありません。 js ファイルのハッシュ値を絶対に変更したくない場合は、css と js を互いに分離し、相互に影響を与えないようにする必要があります。
webpack を実行した後のパッケージ化効果を確認できます:
├── css │ ├── common.2beb7387.css │ ├── pageA.d178426d.css │ └── pageB.33931188.css └── js ├── async.03f28faf.js ├── common.2beb7387.js ├── pageA.d178426d.js ├── pageB.33931188.js └── vendor.22a1d956.js
CSS と JS が分離されており、モジュール チャンクの一意性を確保するためにモジュールを分割していることがわかります。コードを更新するたびに、異なるハッシュ値。
一意性を確保するには、ハッシュ値の安定性を確保する必要があります。コード (モジュール、CSS) の特定の部分を変更することによってファイルのハッシュ値が変更されることは絶対に避けたいと考えます。明らかにそれは賢明ではないので、ハッシュ値の変更を最小限に抑えるにはどうすればよいでしょうか?
言い換えれば、Webpack のコンパイルでキャッシュの失敗を引き起こす要因を見つけ出し、それらを解決または最適化する方法を見つける必要があるということでしょうか?
チャンクハッシュ値の変更は、主に次の 4 つの部分によって引き起こされます:
モジュールを含むソースコード
webpack 実行を開始するために使用されるランタイムコード
webpack によって生成されたモジュール moduleid (モジュール ID と参照される依存モジュール ID を含む)
chunkID
これら 4 つの部分のいずれかの部分が変更される限り、生成されるチャンク ファイルは異なり、キャッシュは無効になります。 以下にまとめます。 4 つの部分。 1. はじめに:
1. ソースコードの変更:
显然不用多说,缓存必须要刷新,不然就有问题了
二、webpack 启动运行的 runtime 代码:
看过我之前的文章:深入理解 webpack 文件打包机制 就会知道,在 webpack 启动的时候需要执行一些启动代码。
(function(modules) { window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules) { // ... }; function __webpack_require__(moduleId) { // ... } __webpack_require__.e = function requireEnsure(chunkId, callback) { // ... script.src = __webpack_require__.p + "" + chunkId + "." + ({"0":"pageA","1":"pageB","3":"vendor"}[chunkId]||chunkId) + "." + {"0":"e72ce7d4","1":"69f6bbe3","2":"9adbbaa0","3":"53fa02a7"}[chunkId] + ".js"; }; })([]);
大致内容像上面这样,它们是 webpack 的一些启动代码,它们是一些函数,告诉浏览器如何加载 webpack 定义的模块。
其中有一行代码每次更新都会改变的,因为启动代码需要清楚地知道 chunkid 和 chunkhash 值得对应关系,这样在异步加载的时候才能正确地拼接出异步 js 文件的路径。
那么这部分代码最终放在哪个文件呢?因为我们刚才配置的时候最后生成的 common chunk 模块,那么这部分运行时代码会被直接内置在里面,这就导致了,我们每次更新我们业务代码(pageA, pageB, 模块)的时候, common chunkhash 会一直变化,但是这显然不符合我们的设想,因为我们只是要用 common chunk 用来存放公共模块(这里指的是 componentA),那么我 componentA 都没去修改,凭啥 chunkhash 需要变了。
所以我们需要将这部分 runtime 代码抽离成单独文件。
module.exports = { // ... plugins: [ // ... // 放到其他的 CommonsChunkPlugin 后面 new webpack.optimize.CommonsChunkPlugin({ name: 'runtime', minChunks: Infinity, }), ] }
这相当于是告诉 webpack 帮我把运行时代码抽离,放到单独的文件中。
├── css │ ├── common.4cc08e4d.css │ ├── pageA.d178426d.css │ └── pageB.33931188.css └── js ├── async.03f28faf.js ├── common.4cc08e4d.js ├── pageA.d178426d.js ├── pageB.33931188.js ├── runtime.8c79fdcd.js └── vendor.cef44292.js
多生成了一个 runtime.xxxx.js,以后你在改动业务代码的时候,common chunk 的 hash 值就不会变了,取而代之的是 runtime chunk hash 值会变,既然这部分代码是动态的,可以通过 chunk-manifest-webpack-plugin 将他们 inline 到 html 中,减少一次网络请求。
三、webpack 生成的模块 moduleid
在 webpack2 中默认加载 OccurrenceOrderPlugin 这个插件,OccurrenceOrderPlugin 插件会按引入次数最多的模块进行排序,引入次数的模块的 moduleId 越小,但是这仍然是不稳定的,随着你代码量的增加,虽然代码引用次数的模块 moduleId 越小,越不容易变化,但是难免还是不确定的。
默认情况下,模块的 id 是这个模块在模块数组中的索引。OccurenceOrderPlugin 会将引用次数多的模块放在前面,在每次编译时模块的顺序都是一致的,如果你修改代码时新增或删除了一些模块,这将可能会影响到所有模块的 id。
最佳实践方案是通过 HashedModuleIdsPlugin 这个插件,这个插件会根据模块的相对路径生成一个长度只有四位的字符串作为模块的 id,既隐藏了模块的路径信息,又减少了模块 id 的长度。
这样一来,改变 moduleId 的方式就只有文件路径的改变了,只要你的文件路径值不变,生成四位的字符串就不变,hash 值也不变。增加或删除业务代码模块不会对 moduleid 产生任何影响。
module.exports = { plugins: [ new webpack.HashedModuleIdsPlugin(), // 放在最前面 // ... ] }
四、chunkID
实际情况中分块的个数的顺序在多次编译之间大多都是固定的, 不太容易发生变化。
这里涉及的只是比较基础的模块拆分,还有一些其它情况没有考虑到,比如异步加载组件中包含公共模块,可以再次将公共模块进行抽离。形成异步公共 chunk 模块。有想深入学习的可以看这篇文章:Webpack 大法之 Code Splitting
webpack 做缓存的一些注意点
CSS 文件 hash 值失效的问题
不建议线上发布使用 DllPlugin 插件
CSS 文件 hash 值失效的问题:
ExtractTextPlugin 有个比较严重的问题,那就是它生成文件名所用的[chunkhash]是直接取自于引用该 css 代码段的 js chunk ;换句话说,如果我只是修改 css 代码段,而不动 js 代码,那么最后生成出来的 css 文件名依然没有变化。
所以我们需要将 ExtractTextPlugin 中的 chunkhash 改为 contenthash,顾名思义,contenthash 代表的是文本文件内容的 hash 值,也就是只有 style 文件的 hash 值。这样编译出来的 js 和 css 文件就有独立的 hash 值了。
module.exports = { plugins: [ // ... new ExtractTextPlugin({ filename: `css/[name].[contenthash:8].css`, }), ] }
如果你使用的是 webpack2,webpack3,那么恭喜你,这样就足够了,js 文件和 css 文件修改都不会影响到相互的 hash 值。那如果你使用的是 webpack1,那么就会出现问题。
具体来讲就是 webpack1 和 webpack 在计算 chunkhash 值得不同:
webpack1 在涉及的时候并没有考虑像 ExtractTextPlugin 会将模块内容抽离的问题,所以它在计算 chunkhash 的时候是通过打包之前模块内容去计算的,也就是说在计算的时候 css 内容也包含在内,之后才将 css 内容抽离成单独的文件,
那么就会出现:如果只修改了 css 文件,未修改引用的 js 文件,那么编译输出的 js 文件的 hash 值也会改变。
この点、webpack2ではパッケージ化されたファイルの内容に基づいてハッシュ値を計算するため、ExtractTextPluginがCSSコードを抽出した後になるため、上記のような問題は発生しません。残念ながらまだ webpack1 を使用している場合は、md5-hash-webpack-plugin プラグインを使用して webpack のハッシュ計算戦略を変更することをお勧めします。
オンラインパブリッシングに DllPlugin プラグインを使用することはお勧めできません
なぜそう言えるのでしょうか?なぜなら、最近友人が私のところに来て、なぜ彼らの指導者がオンラインでの DllPlugin プラグインの使用を許可していないのかと尋ねてきたからです。
DllPlugin 自体にはいくつかの欠点があります:
まず第一に、追加の Webpack 構成を構成する必要があるため、作業負荷が増加します。
ページの 1 つは非常に大規模なサードパーティの依存関係ライブラリを使用しており、他のページではそれを使用する必要はまったくありません。ただし、それを dll.js に直接パッケージ化する価値はなく、毎回ロードする必要があります。この無駄なコードは、webpack2 のコード分割機能を使用できません。
初めて開くときに dll ファイルをダウンロードする必要があります。多くのライブラリをバンドルしているため、dll ファイルは非常に大きくなり、最初のページの読み込み速度が非常に遅くなります。
ただし、これを dll ファイルにパッケージ化して、ブラウザーにキャッシュを読み取らせることもできます。これにより、たとえば、lodash 関数の 1 つを使用する場合に、キャッシュをリクエストする必要がなくなります。 dll を使用すると、lodash ファイル全体が入力されるため、無駄なコードが大量に読み込まれることになり、最初の画面のレンダリングに時間がかかります。
正しいアプローチは次のとおりだと思います:
React や Vue のような強い整合性を持つライブラリは、キャッシュ用のベンダーのサードパーティ ライブラリを生成できます。一般的な技術システムは 1 つのサイトで固定されているためです。 基本的に、統一された技術システムは使用されるため、キャッシュ用にベンダー ライブラリが生成されます。
antd や lodash などの機能コンポーネント ライブラリは、ツリー シェークによって削除でき、有用なコードのみが残ります。ベンダーのサードパーティ ライブラリに直接インポートしないでください。そうしないと、大量の無駄なコードが実行されます。
結論
さて、最近 webpack を読んで本当にたくさんのことを学んだ気がします。皆さんがこの記事から何かを得ることができれば幸いです。さらに、以前に書いた記事をもう一度お勧めします。これは、ファイル キャッシュのメカニズムをよりよく理解するのに役立ちます: Webpack ファイルのパッケージ化メカニズムの詳細な理解
上記は、皆さんのお役に立てれば幸いです。未来のみんなへ。
関連記事:
Ajaxクリックでデータリストを連続ロードする(グラフィックチュートリアル)
Ajax+Struts2は検証コード検証機能を実装する(グラフィックチュートリアル)
以上がWebpack 永続キャッシュの実践に関する簡単な説明の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。