>웹 프론트엔드 >JS 튜토리얼 >webpack을 사용하여 파일 패키징을 구현하는 방법

webpack을 사용하여 파일 패키징을 구현하는 방법

亚连
亚连원래의
2018-06-13 10:42:342445검색

이 글은 주로 웹팩 파일 패키징 메커니즘에 대한 심층적인 이해를 소개합니다(요약). 이제 공유하고 참고하겠습니다.

머리말

최근에 저는 프론트엔드 모듈화에 대한 더 나은 이해를 얻기 위해 웹팩에 대한 몇 가지 지식을 다시 살펴보았습니다. 예전에는 웹팩 패키징 메커니즘에 대해 궁금했지만 깊게 이해하지 못했기 때문에 저는 이렇게 했습니다. 방금 간단하게 시도해 보았습니다. webpack 패키지 파일을 확인하고 JS 파일을 패키지하는 방법에 대한 더 깊은 이해를 얻으십시오. 이 기사가 독자의 이해에 도움이 되기를 바랍니다.

  1. webpack은 어떻게 단일 파일을 패키지합니까?

  2. webpack에서 여러 파일을 분할하는 코드를 작성하는 방법은 무엇입니까?

  3. 파일 패키징에서 webpack1과 webpack2의 차이점은 무엇인가요?

  4. webpack2 트리 쉐이킹을 수행하는 방법?

  5. webpack3 스코프 호이스팅을 수행하는 방법?

이 기사의 모든 샘플 코드는 모두 내 Github에 있습니다.

git clone https://github.com/happylindz/blog.git
cd blog/code/webpackBundleAnalysis
npm install

webpack 단일 파일을 패키지하는 방법은 무엇입니까?

우선, webpack은 현재 주류인 프론트엔드 모듈러 도구입니다. webpack이 처음 대중화되었을 때 우리는 종종 webpack을 통해 모든 처리된 파일을 번들 파일로 패키징했습니다. 먼저 간단한 예를 살펴보겠습니다.

// 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'
 },
}

npm run build:single을 통해 패키징 효과를 확인하세요. 패키징 내용은 대략 다음과 같습니다(단순화).

// 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);
})]);

상대적으로 관련 없는 코드를 제거한 후 기본 코드가 남습니다. 파일로 이해됨)은 함수로 래핑되고 기본 매개변수가 전달됩니다. 3개의 파일과 총 4개의 모듈에 대한 입력 모듈이 있습니다. 이를 배열에 넣고 이름을 모듈로 지정하고 하단을 통해 전달합니다. moduleId로 표시됩니다.

  1. 모듈을 자체 실행 함수에 전달합니다. 자체 실행 함수에는 installModules에 의해 로드된 모듈과 마지막으로 항목 모듈이 로드되고 반환됩니다.

  2. __webpack_require__ 모듈 로드, 먼저 설치된 모듈이 로드되었는지 확인합니다. 로드되면 내보내기 데이터가 직접 반환됩니다. 모듈이 로드되지 않은 경우 module[moduleId].call(module.exports, module, module.exports, __webpack_require__) 모듈을 실행하고 module.exports를 반환합니다.

  3. 매우 간단하지 않나요? 주의할 점은 다음과 같습니다.

각 모듈 웹팩은 한 번만 로드되므로 반복적으로 로드되는 모듈은 한 번만 실행되고 로드된 모듈은 배치됩니다. 다음 번에 installModules에서 모듈의 값이 필요하면 모듈에서 직접 가져오세요.

  1. 모듈의 ID는 배열 첨자를 통해 직접 1:1로 일치하므로 단순성과 고유성을 보장할 수 있습니다. 파일 이름이나 파일 경로 등 다른 방법을 사용하는 것이 더 번거롭습니다. 이름이 중복되고 고유하지 않은 경우 파일 경로는 파일 크기를 늘리고 경로를 프런트 엔드에 노출하므로 충분히 안전하지 않습니다.

  2. modules[moduleId].call(module.exports, module, module.exports, __webpack_require__)은 모듈이 로드될 때 이것이 module.exports를 가리키고 기본 매개변수가 전달되도록 보장합니다. 매우 간단하고 너무 많은 설명이 필요하지 않습니다.

  3. webpack에서 여러 파일을 분할하는 코드를 작성하는 방법은 무엇입니까?

  4. webpack의 단일 파일 패키징 방식은 일부 간단한 시나리오에는 충분하지만 일부 복잡한 애플리케이션을 개발 중이므로 코드가 잘리지 않으면 타사 라이브러리(jQuery)나 프레임워크(React) 및 비즈니스 코드가 모두 삭제됩니다. Together에 패키지되어 있으면 사용자가 페이지에 매우 느리게 액세스하게 되어 캐시를 효과적으로 사용할 수 없게 됩니다.

그렇다면 webpack 다중 파일 항목은 어떻게 코드 절단을 수행합니까? 먼저 간단한 예를 작성해 보겠습니다.

// 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";

여기서 두 개의 항목 pageA 및 pageB와 세 개의 라이브러리 util을 다음과 같이 정의합니다.

두 입구 모두 utilB를 사용하기 때문에 별도의 파일로 추출하여 사용자가 pageA와 pageB에 액세스할 때 각 항목 파일에 존재하는 대신 공통 모듈 utilB를 로드할 수 있기를 바랍니다.

  1. pageB의 utilC는 페이지가 처음 로드될 때 필요하지 않습니다. utilC가 큰 경우 페이지가 로드될 때 utilC를 직접 로드하지 않고 사용자가 특정 조건(예: 버튼) utilC를 비동기적으로 로드하려면 utilC를 별도의 파일로 추출한 후 사용자가 필요할 때 파일을 로드해야 합니다.

  2. 그럼 웹팩은 어떻게 구성하나요?

    // 通过 config/webpack.config.multiple.js 打包
    const webpack = require('webpack');
    const path = require('path')
    
    module.exports = {
     entry: {
     pageA: [path.resolve(__dirname, '../src/multiple/pageA.js')],
     pageB: path.resolve(__dirname, '../src/multiple/pageB.js'),
     },
     output: {
     path: path.resolve(__dirname, '../dist'),
     filename: '[name].[chunkhash:8].js',
     },
     plugins: [
     new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks: 2,
     }),
     new webpack.optimize.CommonsChunkPlugin({
      name: 'manifest',
      chunks: ['vendor']
     })
     ]
    }
  3. 단순히 여러 항목을 구성하는 것만으로는 충분하지 않습니다. 이렇게 하면 두 개의 번들 파일만 생성되고 페이지A 및 페이지B에 필요한 모든 콘텐츠가 저장됩니다. 코드 절단을 수행하려면 webpack을 사용해야 합니다. - 플러그인 CommonsChunkPlugin.

먼저 이전 단일 파일의 __webpack_require__ 처럼 webpack 실행시 런타임 코드 부분, 즉 초기화 작업의 부분이 있습니다. 이 부분의 코드는 모든 파일 이전에 로드되어야 하는데, 초기화 작업과 동일합니다. 초기화 코드의 이 부분이 없으면 로드된 코드는 더 이상 인식되지 않고 작동하지 않습니다.

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 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.