ホームページ  >  記事  >  ウェブフロントエンド  >  Webpackを使用してファイルパッケージ化を実装する方法

Webpackを使用してファイルパッケージ化を実装する方法

亚连
亚连オリジナル
2018-06-13 10:42:342386ブラウズ

この記事では、主に webpack ファイルのパッケージ化メカニズム (概要) について詳しく説明します。

まえがき

最近、フロントエンドのモジュール化をもっと理解したいと思って、Webpack のいくつかの知識ポイントを再確認しています。最近、webpack でパッケージ化されたファイルを渡しました。この記事を読んで、JS ファイルをパッケージ化する方法をより深く理解していただければ幸いです。 ?

  1. Webpack で複数のファイルをコード分割するにはどうすればよいですか?

  2. ファイルのパッケージ化における webpack1 と webpack2 の違いは何ですか?

  3. webpack2 ツリーシェイキングを行う方法?

  4. webpack3 スコープホイスティングを行う方法?

  5. この記事のすべてのサンプルコードはすべて私の Github にあります。興味がある場合は、チェックしてください。

    git clone https://github.com/happylindz/blog.git
    cd blog/code/webpackBundleAnalysis
    npm install
webpack 単一のファイルをパッケージ化するには?

まず第一に、webpack は現在の主流のフロントエンド モジュラー ツールです。webpack が最初に普及したとき、私たちは多くの場合、処理されたすべてのファイルを webpack を通じてバンドル ファイルにパッケージ化しました。 npm run build:single でパッケージ化の効果を確認します。 パッケージ化の内容はおおよそ次のとおりです (簡略化されています):
// src/single/index.js
var index2 = require('./index2');
var util = require('./util');
console.log(index2);
console.log(util);

// src/single/index2.js
var util = require('./util');
console.log(util);
module.exports = "index 2";

// src/single/util.js
module.exports = "Hello World";

// 通过 config/webpack.config.single.js 打包
const webpack = require('webpack');
const path = require('path')

module.exports = {
 entry: {
 index: [path.resolve(__dirname, '../src/single/index.js')],
 },
 output: {
 path: path.resolve(__dirname, '../dist'),
 filename: '[name].[chunkhash:8].js'
 },
}
比較的無関係なコードを削除した後、メインのコードが残ります:

最初の webpack はすべてのモジュール (単純に3 つのファイルとエントリ モジュールの合計 4 つのモジュールがあり、それらを配列に配置し、モジュールという名前を付けて、モジュールの最後に渡します。 moduleId としてマークされた配列。

    自己実行関数にモジュールを渡します。 自己実行関数には、installedModules によってロードされたモジュールとモジュールロード関数が含まれます。 最後に、エントリ モジュールがロードされて返されます。
  1. __webpack_require__ モジュールのロードでは、まず、installedModules がロードされているかどうかを確認します。ロードされると、モジュールがロードされていない場合は、 modules[moduleId].call(module.exports, module, module.exports, __webpack_require__) モジュールを実行し、module.exports を返します。
  2. それはとても簡単ですよね? 注意すべき点は次のとおりです:
  3. 各モジュール Webpack は 1 回だけロードされるため、繰り返しロードされるモジュールは 1 回だけ実行され、ロードされたモジュールは配置されます。次回、installedModules でモジュールの値が必要な場合は、そこから直接取得してください。

    モジュールの ID は、配列添字を介して 1 対 1 で直接照合されるため、シンプルさと一意性が保証されます。ファイル名やファイル パスなどの他のメソッドを使用するのはより面倒です。名前が重複していて一意ではない場合、ファイル パスによりファイル サイズが増加し、そのパスがフロントエンドに公開されるため、十分に安全ではありません。
  1. modules[moduleId].call(module.exports, module, module.exports, __webpack_require__) は、モジュールがロードされるときに、これが module.exports を指し、デフォルトのパラメーターが渡されることを保証します。あまり多くの説明は必要ありません。
  2. Webpack で複数のファイルをコード分割するにはどうすればよいですか?
  3. webpack の単一ファイルのパッケージ化方法は、いくつかの単純なシナリオには十分に対応できますが、コードがカットされていない場合、サードパーティのライブラリ (jQuery) またはフレームワーク (React) とビジネス コードがすべて統合されてしまう複雑なアプリケーションを開発しています。 Together にパッケージ化されていると、ユーザーのページへのアクセスが非常に遅くなり、キャッシュが効果的に使用されなくなる可能性があります。

  4. それでは、webpack のマルチファイル エントリはどのようにしてコードカットを実行するのでしょうか? まず簡単な例を書いてみましょう:
// dist/index.xxxx.js
(function(modules) {
 // 已经加载过的模块
 var installedModules = {};

 // 模块加载函数
 function __webpack_require__(moduleId) {
 if(installedModules[moduleId]) {
  return installedModules[moduleId].exports;
 }
 var module = installedModules[moduleId] = {
  i: moduleId,
  l: false,
  exports: {}
 };
 modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
 module.l = true;
 return module.exports;
 }
 return __webpack_require__(__webpack_require__.s = 3);
})([
/* 0 */
(function(module, exports, __webpack_require__) {
 var util = __webpack_require__(1);
 console.log(util);
 module.exports = "index 2";
}),
/* 1 */
(function(module, exports) {
 module.exports = "Hello World";
}),
/* 2 */
(function(module, exports, __webpack_require__) {
 var index2 = __webpack_require__(0);
 index2 = __webpack_require__(0);
 var util = __webpack_require__(1);
 console.log(index2);
 console.log(util);
}),
/* 3 */
(function(module, exports, __webpack_require__) {
 module.exports = __webpack_require__(2);
})]);

ここでは、2 つのエントリ pageA と pageB と 3 つのライブラリ util を次のように定義します。

両方の入り口で utilB を使用するため、それを別のファイルに抽出して、ユーザーが pageA と pageB にアクセスするときに、それぞれの入り口ファイルに存在する代わりに共通モジュール utilB をロードできるようにしたいと考えています。

    ページ B の utilC は、ページが最初に読み込まれるときは必要ありません。utilC が大きい場合、ページの読み込み時に utilC を直接読み込むのではなく、ユーザーが特定の条件に達したとき (ボタンをクリックするなど) にのみ必要です。ボタン) utilC を非同期でロードするには、utilC を別のファイルに抽出し、ユーザーが必要なときにそのファイルをロードする必要があります。
  1. それでは、Webpack をどのように設定しますか?
  2. // src/multiple/pageA.js
    const utilA = require('./js/utilA');
    const utilB = require('./js/utilB');
    console.log(utilA);
    console.log(utilB);
    
    // src/multiple/pageB.js
    const utilB = require('./js/utilB');
    console.log(utilB);
    // 异步加载文件,类似于 import()
    const utilC = () => require.ensure(['./js/utilC'], function(require) {
     console.log(require('./js/utilC'))
    });
    utilC();
    
    // src/multiple/js/utilA.js 可类比于公共库,如 jQuery
    module.exports = "util A";
    
    // src/multiple/js/utilB.js
    module.exports = 'util B';
    
    // src/multiple/js/utilC.js
    module.exports = "util C";

    単に複数のエントリを設定するだけでは十分ではありません。これにより、2 つのバンドル ファイルが生成され、pageA と pageB に必要なすべてのコンテンツが配置されます。コードカットを実現するには、webpack Built を使用する必要があります。 -in プラグイン CommonsChunkPlugin。

  3. まず、Webpack の実行にはランタイム コードの一部があります。つまり、前の単一ファイルの __webpack_require__ と同様に、コードのこの部分はすべてのファイルの前にロードされる必要があります。初期化コードのこの部分がないと、ロードされたコードは認識されなくなり、動作しなくなります。
new webpack.optimize.CommonsChunkPlugin({
 name: 'vendor',
 minChunks: 2,
})

这段代码的含义是,在这些入口文件中,找到那些引用两次的模块(如:utilB),帮我抽离成一个叫 vendor 文件,此时那部分初始化工作的代码会被抽离到 vendor 文件中。

new webpack.optimize.CommonsChunkPlugin({
 name: 'manifest',
 chunks: ['vendor'],
 // minChunks: Infinity // 可写可不写
})

这段代码的含义是在 vendor 文件中帮我把初始化代码抽离到 mainifest 文件中,此时 vendor 文件中就只剩下 utilB 这个模块了。你可能会好奇为什么要这么做?

因为这样可以给 vendor 生成稳定的 hash 值,每次修改业务代码(pageA),这段初始化时代码就会发生变化,那么如果将这段初始化代码放在 vendor 文件中的话,每次都会生成新的 vendor.xxxx.js,这样不利于持久化缓存,如果不理解也没关系,下次我会另外写一篇文章来讲述这部分内容。

另外 webpack 默认会抽离异步加载的代码,这个不需要你做额外的配置,pageB 中异步加载的 utilC 文件会直接抽离为 chunk.xxxx.js 文件。

所以这时候我们页面加载文件的顺序就会变成:

mainifest.xxxx.js // 初始化代码
vendor.xxxx.js // pageA 和 pageB 共同用到的模块,抽离
pageX.xxxx.js  // 业务代码 
当 pageB 需要 utilC 时候则异步加载 utilC

执行 npm run build:multiple 即可查看打包内容,首先来看下 manifest 如何做初始化工作(精简版)?

// dist/mainifest.xxxx.js
(function(modules) { 
 window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules) {
 var moduleId, chunkId, i = 0, callbacks = [];
 for(;i < chunkIds.length; i++) {
  chunkId = chunkIds[i];
  if(installedChunks[chunkId])
  callbacks.push.apply(callbacks, installedChunks[chunkId]);
  installedChunks[chunkId] = 0;
 }
 for(moduleId in moreModules) {
  if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
  modules[moduleId] = moreModules[moduleId];
  }
 }
 while(callbacks.length)
  callbacks.shift().call(null, __webpack_require__);
 if(moreModules[0]) {
  installedModules[0] = 0;
  return __webpack_require__(0);
 }
 };
 var installedModules = {};
 var installedChunks = {
 4:0
 };
 function __webpack_require__(moduleId) {
 // 和单文件一致
 }
 __webpack_require__.e = function requireEnsure(chunkId, callback) {
 if(installedChunks[chunkId] === 0)
  return callback.call(null, __webpack_require__);
 if(installedChunks[chunkId] !== undefined) {
  installedChunks[chunkId].push(callback);
 } else {
  installedChunks[chunkId] = [callback];
  var head = document.getElementsByTagName(&#39;head&#39;)[0];
  var script = document.createElement(&#39;script&#39;);
  script.type = &#39;text/javascript&#39;;
  script.charset = &#39;utf-8&#39;;
  script.async = true;
  script.src = __webpack_require__.p + "" + chunkId + "." + ({"0":"pageA","1":"pageB","3":"vendor"}[chunkId]||chunkId) + "." + {"0":"e72ce7d4","1":"69f6bbe3","2":"9adbbaa0","3":"53fa02a7"}[chunkId] + ".js";
  head.appendChild(script);
 }
 };
})([]);

与单文件内容一致,定义了一个自执行函数,因为它不包含任何模块,所以传入一个空数组。除了定义了 __webpack_require__ ,还另外定义了两个函数用来进行加载模块。

首先讲解代码前需要理解两个概念,分别是 module 和 chunk

  1. chunk 代表生成后 js 文件,一个 chunkId 对应一个打包好的 js 文件(一共五个),从这段代码可以看出,manifest 的 chunkId 为 4,并且从代码中还可以看到:0-3 分别对应 pageA, pageB, 异步 utilC, vendor 公共模块文件,这也就是我们为什么不能将这段代码放在 vendor 的原因,因为文件的 hash 值会变。内容变了,vendor 生成的 hash 值也就变了。

  2. module 对应着模块,可以简单理解为打包前每个 js 文件对应一个模块,也就是之前 __webpack_require__ 加载的模块,同样的使用数组下标作为 moduleId 且是唯一不重复的。

那么为什么要区分 chunk 和 module 呢?

首先使用 installedChunks 来保存每个 chunkId 是否被加载过,如果被加载过,则说明该 chunk 中所包含的模块已经被放到了 modules 中,注意是 modules 而不是 installedModules。我们先来简单看一下 vendor chunk 打包出来的内容。

// vendor.xxxx.js
webpackJsonp([3,4],{
 3: (function(module, exports) {
 module.exports = &#39;util B&#39;;
 })
});

在执行完 manifest 后就会先执行 vendor 文件,结合上面 webpackJsonp 的定义,我们可以知道 [3, 4] 代表 chunkId,当加载到 vendor 文件后,installedChunks[3] 和 installedChunks[4] 将会被置为 0,这表明 chunk3,chunk4 已经被加载过了。

webpackJsonpCallback 一共有两个参数,chuckIds 一般包含该 chunk 文件依赖的 chunkId 以及自身 chunkId,moreModules 代表该 chunk 文件带来新的模块。

var moduleId, chunkId, i = 0, callbacks = [];
for(;i < chunkIds.length; i++) {
 chunkId = chunkIds[i];
 if(installedChunks[chunkId])
 callbacks.push.apply(callbacks, installedChunks[chunkId]);
 installedChunks[chunkId] = 0;
}
for(moduleId in moreModules) {
 if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
 modules[moduleId] = moreModules[moduleId];
 }
}
while(callbacks.length)
 callbacks.shift().call(null, __webpack_require__);
if(moreModules[0]) {
 installedModules[0] = 0;
 return __webpack_require__(0);
}

简单说说 webpackJsonpCallback 做了哪些事,首先判断 chunkIds 在 installedChunks 里有没有回调函数函数未执行完,有的话则放到 callbacks 里,并且等下统一执行,并将 chunkIds 在 installedChunks 中全部置为 0, 然后将 moreModules 合并到 modules。

这里面只有 modules[0] 是不固定的,其它 modules 下标都是唯一的,在打包的时候 webpack 已经为它们统一编号,而 0 则为入口文件即 pageA,pageB 各有一个 module[0]。

然后将 callbacks 执行并清空,保证了该模块加载开始前所以前置依赖内容已经加载完毕,最后判断 moreModules[0], 有值说明该文件为入口文件,则开始执行入口模块 0。

上面解释了一大堆,但是像 pageA 这种同步加载 manifest, vendor 以及 pageA 文件来说,每次加载的时候 callbacks 都是为空的,因为它们在 installedChunks 中的值要嘛为 undefined(未加载), 要嘛为 0(已被加载)。installedChunks[chunkId] 的值永远为 false,所以在这种情况下 callbacks 里根本不会出现函数,如果仅仅是考虑这样的场景,上面的 webpackJsonpCallback 完全可以写成下面这样:

var moduleId, chunkId, i = 0, callbacks = [];
for(;i < chunkIds.length; i++) {
 chunkId = chunkIds[i];
 installedChunks[chunkId] = 0;
}
for(moduleId in moreModules) {
 if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
 modules[moduleId] = moreModules[moduleId];
 }
}
if(moreModules[0]) {
 installedModules[0] = 0;
 return __webpack_require__(0);
}

但是考虑到异步加载 js 文件的时候(比如 pageB 异步加载 utilC 文件),就没那么简单,我们先来看下 webpack 是如何加载异步脚本的:

// 异步加载函数挂载在 __webpack_require__.e 上
__webpack_require__.e = function requireEnsure(chunkId, callback) {
 if(installedChunks[chunkId] === 0)
 return callback.call(null, __webpack_require__);
  
 if(installedChunks[chunkId] !== undefined) {
 installedChunks[chunkId].push(callback);
 } else {
 installedChunks[chunkId] = [callback];
 var head = document.getElementsByTagName(&#39;head&#39;)[0];
 var script = document.createElement(&#39;script&#39;);
 script.type = &#39;text/javascript&#39;;
 script.charset = &#39;utf-8&#39;;
 script.async = true;

 script.src = __webpack_require__.p + "" + chunkId + "." + ({"0":"pageA","1":"pageB","3":"vendor"}[chunkId]||chunkId) + "." + {"0":"e72ce7d4","1":"69f6bbe3","2":"9adbbaa0","3":"53fa02a7"}[chunkId] + ".js";
 head.appendChild(script);
 }
};

大致分为三种情况,(已经加载过,正在加载中以及从未加载过)

  1. 已经加载过该 chunk 文件,那就不用再重新加载该 chunk 了,直接执行回调函数即可,可以理解为假如页面有两种操作需要加载加载异步脚本,但是两个脚本都依赖于公共模块,那么第二次加载的时候发现之前第一次操作已经加载过了该 chunk,则不用再去获取异步脚本了,因为该公共模块已经被执行过了。

  2. 从未加载过,则动态地去插入 script 脚本去请求 js 文件,这也就为什么取名 webpackJsonpCallback ,因为跟 jsonp 的思想很类似,所以这种异步加载脚本在做脚本错误监控时经常出现 Script error,具体原因可以查看我之前写的文章:前端代码异常监控实战

  3. 正在加载中代表该 chunk 文件已经在加载中了,比如说点击按钮触发异步脚本,用户点太快了,连点两次就可能出现这种情况,此时将回调函数放入 installedChunks。

我们通过 utilC 生成的 chunk 来进行讲解:

webpackJsonp([2,4],{
 4: (function(module, exports) {
 module.exports = "util C";
 })
});

pageB 需要异步加载这个 chunk:

webpackJsonp([1,4],[
/* 0 */
 (function(module, exports, __webpack_require__) {
 const utilB = __webpack_require__(3);
 console.log(utilB);
 const utilC = () => __webpack_require__.e/* nsure */(2, function(require) {
  console.log(__webpack_require__(4))
 });
 utilC();
 })
]);

当 pageB 进行某种操作需要加载 utilC 时就会执行 __webpack_require__.e(2, callback) 2,代表需要加载的模块 chunkId(utilC),异步加载 utilC 并将 callback 添加到 installedChunks[2] 中,然后当 utilC 的 chunk 文件加载完毕后,chunkIds 包含 2,发现 installedChunks[2] 是个数组,里面还有之前还未执行的 callback 函数。

既然这样,那我就将我自己带来的模块先放到 modules 中,然后再统一执行之前未执行完的 callbacks 函数,这里指的是存放于 installedChunks[2] 中的回调函数 (可能存在多个),这也就是说明这里的先后顺序:

// 先将 moreModules 合并到 modules, 再去执行 callbacks, 不然之前未执行的 callback 依赖于新来的模块,你不放进 module 我岂不是得不到想要的模块
for(moduleId in moreModules) {
 if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
 modules[moduleId] = moreModules[moduleId];
 }
}
while(callbacks.length)
 callbacks.shift().call(null, __webpack_require__);

webpack1 和 webpack2 在文件打包上有什么区别?

经过我对打包文件的观察,从 webpack1 到 webpack2 在打包文件上有下面这些主要的改变:

首先,moduleId[0] 不再为入口执行函数做保留,所以说不用傻傻看到 moduleId[0] 就认为是打包文件的入口模块,取而代之的是 window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {} 传入了第三个参数 executeModules,是个数组,如果参数存在则说明它是入口模块,然后就去执行该模块。

if(executeModules) {
 for(i=0; i < executeModules.length; i++) {
 result = __webpack_require__(__webpack_require__.s = executeModules[i]);
 }
}

其次,webpack2 中会默认加载 OccurrenceOrderPlugin 这个插件,即你不用 plugins 中添加这个配置它也会默认执行,那它有什么用途呢?主要是在 webpack1 中 moduleId 的不确定性导致的,在 webpack1 中 moduleId 取决于引入文件的顺序,这就会导致这个 moduleId 可能会时常发生变化, 而 OccurrenceOrderPlugin 插件会按引入次数最多的模块进行排序,引入次数的模块的 moduleId 越小,比如说上面引用的 utilB 模块引用次数为 2(最多),所以它的 moduleId 为 0。

webpackJsonp([3],[
/* 0 */
 (function(module, exports) {
 module.exports = &#39;util B&#39;;
 })
]);

最后说下在异步加载模块时, webpack2 是基于 Promise 的,所以说如果你要兼容低版本浏览器,需要引入 Promise-polyfill ,另外为引入请求添加了错误处理。

__webpack_require__.e = function requireEnsure(chunkId) {
 var promise = new Promise(function(resolve, reject) {
 installedChunkData = installedChunks[chunkId] = [resolve, reject];
 });
 installedChunkData[2] = promise;
 // start chunk loading
 var head = document.getElementsByTagName(&#39;head&#39;)[0];
 var script = document.createElement(&#39;script&#39;);
 script.type = &#39;text/javascript&#39;;
 script.charset = &#39;utf-8&#39;;
 script.async = true;
 script.timeout = 120000;
 script.src = __webpack_require__.p + "" + chunkId + "." + {"0":"ae9c5f5f","1":"0ac69acb","2":"20651a9c","3":"0cdc6c84"}[chunkId] + ".js";
 var timeout = setTimeout(onScriptComplete, 120000);
 script.onerror = script.onload = onScriptComplete;
 function onScriptComplete() {
 // 防止内存泄漏
 script.onerror = script.onload = null;
 clearTimeout(timeout);
 var chunk = installedChunks[chunkId];
 if(chunk !== 0) {
  if(chunk) {
  chunk[1](new Error(&#39;Loading chunk &#39; + chunkId + &#39; failed.&#39;));
  }
  installedChunks[chunkId] = undefined;
 }
 };
 head.appendChild(script);
 return promise;
};

可以看出,原本基于回调函数的方式已经变成基于 Promise 做异步处理,另外添加了 onScriptComplete 用于做脚本加载失败处理。

在 webpack1 的时候,如果由于网络原因当你加载脚本失败后,即使网络恢复了,你再次进行某种操作需要同个 chunk 时候都会无效,主要原因是失败之后没把 installedChunks[chunkId] = undefined; 导致之后不会再对该 chunk 文件发起异步请求。

而在 webpack2 中,当脚本请求超时了(2min)或者加载失败,会将 installedChunks[chunkId] 清空,当下次重新请求该 chunk 文件会重新加载,提高了页面的容错性。

这些是我在打包文件中看到主要的区别,难免有所遗漏,如果你有更多的见解,欢迎在评论区留言。

webpack2 如何做到 tree shaking?

什么是 tree shaking,即 webpack 在打包的过程中会将没用的代码进行清除(dead code)。一般 dead code 具有一下的特征:

  1. 代码不会被执行,不可到达

  2. 代码执行的结果不会被用到

  3. 代码只会影响死变量(只写不读)

是不是很神奇,那么需要怎么做才能使 tree shaking 生效呢?

首先,模块引入要基于 ES6 模块机制,不再使用 commonjs 规范,因为 es6 模块的依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析,然后清除没用的代码。而 commonjs 的依赖关系是要到运行时候才能确定下来的。

其次,需要开启 UglifyJsPlugin 这个插件对代码进行压缩。

我们先写一个例子来说明:

// src/es6/pageA.js
import {
 utilA,
 funcA, // 引入 funcA 但未使用, 故 funcA 会被清除
} from &#39;./js/utilA&#39;;
import utilB from &#39;./js/utilB&#39;; // 引入 utilB(函数) 未使用,会被清除
import classC from &#39;./js/utilC&#39;; // 引入 classC(类) 未使用,不会被清除
console.log(utilA);

// src/es6/js/utilA.js
export const utilA = &#39;util A&#39;;
export function funcA() {
 console.log(&#39;func A&#39;);
}

// src/es6/js/utilB.js
export default function() {
 console.log(&#39;func B&#39;);
}
if(false) { // 被清除
 console.log(&#39;never use&#39;);
}
while(true) {}
console.log(&#39;never use&#39;);

// src/es6/js/utilC.js
const classC = function() {} // 类方法不会被清除
classC.prototype.saySomething = function() {
 console.log(&#39;class C&#39;);
}
export default classC;

打包的配置也很简单:

const webpack = require(&#39;webpack&#39;);
const path = require(&#39;path&#39;)
module.exports = {
 entry: {
 pageA: path.resolve(__dirname, &#39;../src/es6/pageA.js&#39;),
 },
 output: {
 path: path.resolve(__dirname, &#39;../dist&#39;),
 filename: &#39;[name].[chunkhash:8].js&#39;
 },
 plugins: [
 new webpack.optimize.CommonsChunkPlugin({
  name: &#39;manifest&#39;,
  minChunks: Infinity,
 }),
 new webpack.optimize.UglifyJsPlugin({
  compress: {
  warnings: false
  }
 })
 ]
}

通过 npm run build:es6 对压缩的文件进行分析:

// dist/pageA.xxxx.js
webpackJsonp([0],[
 function(o, t, e) {
 &#39;use strict&#39;;
 Object.defineProperty(t, &#39;__esModule&#39;, { value: !0 });
 var n = e(1);
 e(2), e(3);
 console.log(n.a);
 },function(o, t, e) {
 &#39;use strict&#39;;
 t.a = &#39;util A&#39;;
 },function(o, t, e) {
 &#39;use strict&#39;;
 for (;;);
 console.log(&#39;never use&#39;);
 },
 function(o, t, e) {
 &#39;use strict&#39;;
 const n = function() {};
 n.prototype.saySomething = function() {
  console.log(&#39;class C&#39;);
 };
 }
],[0]);

引入但是没用的变量,函数都会清除,未执行的代码也会被清除。但是类方法是不会被清除的。因为 webpack 不会区分不了是定义在 classC 的 prototype 还是其它 Array 的 prototype 的,比如 classC 写成下面这样:

const classC = function() {}
var a = &#39;class&#39; + &#39;C&#39;;
var b;
if(a === &#39;Array&#39;) {
 b = a;
}else {
 b = &#39;classC&#39;;
}
b.prototype.saySomething = function() {
 console.log(&#39;class C&#39;);
}
export default classC;

webpack 无法保证 prototype 挂载的对象是 classC,这种代码,静态分析是分析不了的,就算能静态分析代码,想要正确完全的分析也比较困难。所以 webpack 干脆不处理类方法,不对类方法进行 tree shaking。

更多的 tree shaking 的副作用可以查阅: Tree shaking class methods

webpack3 如何做到 scope hoisting?

scope hoisting,顾名思义就是将模块的作用域提升,在 webpack 中不能将所有所有的模块直接放在同一个作用域下,有以下几个原因:

  1. 按需加载的模块

  2. 使用 commonjs 规范的模块

  3. 被多 entry 共享的模块

在 webpack3 中,这些情况生成的模块不会进行作用域提升,下面我就举个例子来说明:

// src/hoist/utilA.js
export const utilA = &#39;util A&#39;;
export function funcA() {
 console.log(&#39;func A&#39;);
}

// src/hoist/utilB.js
export const utilB = &#39;util B&#39;;
export function funcB() {
 console.log(&#39;func B&#39;);
}

// src/hoist/utilC.js
export const utilC = &#39;util C&#39;;

// src/hoist/pageA.js
import { utilA, funcA } from &#39;./utilA&#39;;
console.log(utilA);
funcA();

// src/hoist/pageB.js
import { utilA } from &#39;./utilA&#39;;
import { utilB, funcB } from &#39;./utilB&#39;;

funcB();
import(&#39;./utilC&#39;).then(function(utilC) {
 console.log(utilC);
})

这个例子比较典型,utilA 被 pageA 和 pageB 所共享,utilB 被 pageB 单独加载,utilC 被 pageB 异步加载。

想要 webpack3 生效,则需要在 plugins 中添加 ModuleConcatenationPlugin。

webpack 配置如下:

const webpack = require(&#39;webpack&#39;);
const path = require(&#39;path&#39;)
module.exports = {
 entry: {
 pageA: path.resolve(__dirname, &#39;../src/hoist/pageA.js&#39;),
 pageB: path.resolve(__dirname, &#39;../src/hoist/pageB.js&#39;),
 },
 output: {
 path: path.resolve(__dirname, &#39;../dist&#39;),
 filename: &#39;[name].[chunkhash:8].js&#39;
 },
 plugins: [
 new webpack.optimize.ModuleConcatenationPlugin(),
 new webpack.optimize.CommonsChunkPlugin({
  name: &#39;vendor&#39;,
  minChunks: 2,
 }),
 new webpack.optimize.CommonsChunkPlugin({
  name: &#39;manifest&#39;,
  minChunks: Infinity,
 })
 ]
}

运行 npm run build:hoist 进行编译,简单看下生成的 pageB 代码:

webpackJsonp([2],{
 2: (function(module, __webpack_exports__, __webpack_require__) {
 "use strict";
 var utilA = __webpack_require__(0);
 // CONCATENATED MODULE: ./src/hoist/utilB.js
 const utilB = &#39;util B&#39;;
 function funcB() {
  console.log(&#39;func B&#39;);
 }
 // CONCATENATED MODULE: ./src/hoist/pageB.js
 funcB();
 __webpack_require__.e/* import() */(0).then(__webpack_require__.bind(null, 3)).then(function(utilC) {
  console.log(utilC);
 })
 })
},[2]);

通过代码分析,可以得出下面的结论:

  1. 因为我们配置了共享模块抽离,所以 utilA 被抽出为单独模块,故这部分内容不会进行作用域提升。

  2. utilB 无牵无挂,被 pageB 单独加载,所以这部分不会生成新的模块,而是直接作用域提升到 pageB 中。

  3. utilC 被异步加载,需要抽离成单独模块,很明显没办法作用域提升。

结尾

好了,讲到这差不多就完了,理解上面的内容对前端模块化会有更多的认知,如果有什么写的不对或者不完整的地方,还望补充说明,希望这篇文章能帮助到你。

上面是我整理给大家的,希望今后会对大家有帮助。

相关文章:

在vue-scroller中如何标记记录滚动位置

微信小程序审核没通过该怎么解决?

使用swiper如何改变滑动内容(详细教程)

使用Vue.js 2.0如何实现背景视频登录页面

以上がWebpackを使用してファイルパッケージ化を実装する方法の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。