深入 webpack 打包机制的介绍

前言:

我最近需要整理一下 webpack 这个前端构建工具的相关知识,希望对前端工程化的和模块化有更多的理解,我以前对 webpack打包机制感到非常的困惑,也没有深入的理解, 都是浅尝辄止, 最近看到了相关的文章介绍,并对webpackjs 打包有了深入的理解;

这篇文章会帮助你理解如下的问题:

1.webpack 当个文件如何进行打包?

2.webpack 多个文件如何进行代码切割?

3.webpack2 如何做到 tree shaking?

4.webpack3 如何做到 scope hoisting(提升)?

1.webpack 当个文件如何进行打包?

首先现在作为主流的前端模块化工具,在 webpack 刚刚才开始流行起来的时候,我们经常看到 webpack 将所有处理文件全部打包成一个 bundle文件,现在我们看看下面的例子

// src/single/index.js
let index2 = require("./index2");
let util = require("./util");
console.log(index2);
console.log(util);

// src/single/index2.js
let util = require("./util");
console.log(util);
module.exports = "index2 js";

// src/single/util.js
module.exports = "hello liyao";

// 通过 webpack.config.js中的配置
const path = require("path");
const webpack = require("webpack");

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 来进行打包,我们将会得到下面的一段代码块

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

我们从上面知道 webpack 将所有的模块都包含在一个函数中,并传入默认的参数,这里有三个文件再加上一个入口模块一共有四个模块,将他们放在一个数组中,命名为 modules, 并通过数组的下标来作为 moduleId

modules 传入一个自执行的函数中, 自执行的函数中包含一个 installModules 已经加载过的模块和一个模块加载函数, 最后加载入口模块并返回;

__webpack_raquire__ 模块加载, 先判断 installModules 中是否被加载过了,加载过了会直接就放回 exports 数据,没有加载过模块就通过 modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 执行模块并且将 module.exports 给放回;

根据上面的解释, 我们需要注意的是:

  1. 每个模块 webpack 只会加载一次, 所以重复加载的模块只会执行一次,加载过的模块会放到 installModules, 下次需要该模块会从其中直接获取。

  2. 模块的 id 直接通过数组下标去一一对应的,这样保证简单唯一,通过其他的方式比如文件名或者是文件路径的方式就比较麻烦, 因为文件名可能会出现重名,不唯一,文件路径则会增大文件体积,并且将路径暴露给前端, 不够安全;

  3. modules[moduleId].call(module.exports, module, module.exports, __module_require__), 保证了模块加载时, this 的指向 module.exports 并且传入默认的参数

webpack 多个文件如何进行代码切割?

webpack 单文件打包的方式应付一些简单场景就足够了, 但是我们在开发一些复杂的应用时,如果没有对代码进行切割,将第三方库 或 框架 和业务代码全部打包在一起的话,就会导致用户访问页面的速度很慢, 不能有效的利用缓存;那么网站的体验就会很差;
那么 webpack 多个文件入口如何进行代码切割呢?那么我们先来看下面一个例子:

// src/multiple/page1.js
const utilA = require("./js/utilA");
// 模块B 被引用了两次:<1>
const utilB = require("./js/utilB");
console.log(utilA);
console.log(utilB);

// src/multiple/page2.js
const utilB = require("./js/utilB"); // <2>;
console.log(utilB);
// 异步加载文件, 类似于 import()
const utilC = () => {
  return require.ensure(["./js/utilC"], function(require) {
    console.log(require("./js/utilC"));
  });
};
utilC();

// src/multiple/js/utilA.js 可类比于公共库;
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, 那么我们希望做到:

  1. 两个入口都是用到 utilB, 我们希望把它抽离成单个文件,并且当用户访问 pageA 和 pageB 是时候都可 j 加载 utilB 这个公共的模块 e 而不是存在各自的入口文件中。

  2. pageB 中 utilC 不是页面,一开始加载时候需 k 考虑的内容,假如 utilC 很大,我们不希望页面加载时就直接加载 utilC,而是当用户达到某个条件(如: 点击按钮) 才去异步加载 utilC, 这时候我们需将 utilC 抽离成单独的文件, 当用户需要的时候我们再去加载这个文件

那么我们的 webpack 的配置该如何的去配置呢?
// 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.CommonChunkPlugin({
      name: "vendor",
      minChunks: 2
    }),
    new webpack.optimize.CommonChunkPlugin({
      name: "manifest",
      chunks: ["vendor"]
    })
  ]
};

单个配置多个 entry 是不够的 -> 这样只会生成两个 bundle 文件,将 pageA 和 pageB 所需要的内容全部收入,跟单个入口文件并没有区别,要做到代码的切割,我们需要使用 webpackn 内置的插件 CommonsChunkPlugin。首先 webpack 执行存在的一部分运行时代码,即一部分初始化工作,就像之前单文件中的 __webpack_require__, 这个部分需要加载于所有文件之前,相当于初始化工作,少了这部分初始化代码,后面加载过来的代码就无法识别并工作了。

//  这个代码的含义是,在这些入口文件中,找到那些 ***引用两次的模块*** (如:utilB), 那么 utilB 会抽离成一个叫 vendor 文件。此时那部分初始化工作的代码会被抽离到 vendor 文件中。
new webpack.optimize.CommonsChunkPlugin({
  name: "vendor",
  minChunks: 2
});
// 这段代码的含义是在 vendor 文件中帮我把初始化代码抽离到 mainfest 文件中,此时 vendor 文件中就只剩下 utilB 这个模块了,那么我们为什么需要这么做呢?你可能会有这样的问题
new webpack.optimize.CommonsChunkPlugin({
  name: "manifest",
  chunks: ["vendor"]
  // minChunks: Infinity 可写可不写
});

我们为什么需要将 webpack 的初始化模块的代码抽离到 manifest 文件中呢?

因为这样可以给 vendor 生成稳定的 hash 值。每次修改业务代码(pageA), 这段初始化代码会发生变化。那么如果将这段初始化代码放在 vendor 文件中的话。每次都会生成新的
vendor.xxxx.js, 这样不利于持久化缓存,好像对这个概念不是很理解。没关系,我们后面会讲到这个问题, 另外 webpack 默认会抽离异步加载代码。这个不需要做额外的配置,pageB 中异步加载的 utilC 文件会直接抽离为 chunk.xxxx.js 文件

那么这个时候我们的页面加载顺序会是:

manifest.xxx.js; // 初始化代码
vendor.xxx.js; // pageA 和 pageB 共同用到的模块,抽离
pageX.xxx.js; // 业务代码
// 当pageB 需要加载 utilC 的使用就会异步加载 utilC

那么我们执行 webpack 打包, 我们看到打包之后的内容,我们来看看 manifest 文件如何做到初始化的工作?

// dist/mainfest.xxx.js
(function(modules) {
  // 在 window 对象中挂载了一个 webpack 的打包函数, 拿到 chunkIds, 和 modules
  //  <函数1>
  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
  };
  // <函数2>
  function __webpack_require__(moduleId) {
    // 和单文件一致
  }

  // requireEnsure
  __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("head")[0];
      var script = document.createElement("script");
      script.type = "text/javascript";
      script.charset = "utf-8";
      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

  • Chunk 代表生成后的 js 文件,一个chunkId对应一个打包好的 js 文件(共 5 个),从这段代码可以看出 manifest 的 chunkId 为 4,并且从代码中还可以看到 0-3 分别对应的 pageA, pageB,和异步加载的 utilC, vendor 公共模块,那么这个就是我们为什么不能将这段代码放在 vendor 的原因,因为文件的 hash 值会变,内容变了,vendor 生成的 hash 值也就变了
  • module 对应的模块,可以简单的理解为打包前每个 js 文件对应的一个模块, 也就是之前 __webpack_require__ 加载模块, 同样使用数组下标作为 moduleId 且是唯一不重复的

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

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

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

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

webpackJsonpCallback 一共存在两个参数, chunkIds 一般会包含 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);
}

简简单单来说 webpacJsonpCallback 做了哪些事情

  • 首先判断 chunkIds 在 installedChunks 里没有回调函数未执行完, 有的话则放到 callbacks 中, 并且等一下统一执行,j 将 chunkIds 在 installedChunks 中全部表为 0, 然后将 moreModules 合并到 modules 中。

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

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

那么上面的解释,好像 pageA 这种同步加载 manifest, vendor 以及 pageA 文件来说,每次加载的时候 callbacks 都是为空的,因为他们在 installedChunks 中的值要么为 uundefined(未加载), 要么为 0 (已被加载), installedChunks[chunkId] 的值永远为 false, 所以在这种情况下 callbacks 里面不会出现函数,如果仅仅是考虑这样的场景,上面的 webpacJsonCallback 完全可以改为下面这样

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) {
  var installedChunkData = installedChunks[chunkId];
  if (installedChunkData === 0) {
    return new Promise(function(resolve) {
      resolve();
    });
  }
  // a Promise means "currently loading".
  if (installedChunkData) {
    return installedChunkData[2];
  }

  // setup Promise in chunk cache
  var promise = new Promise(function(resolve, reject) {
    installedChunkData = installedChunks[chunkId] = [resolve, reject];
  });
  installedChunkData[2] = promise;

  // start chunk loading
  var head = document.getElementsByTagName("head")[0];
  var script = document.createElement("script");
  script.type = "text/javascript";
  script.charset = "utf-8";
  script.async = true;
  script.timeout = 120000;

  if (__webpack_require__.nc) {
    script.setAttribute("nonce", __webpack_require__.nc);
  }
  script.src =
    __webpack_require__.p +
    "" +
    chunkId +
    "." +
    { "0": "15fddb8c", "1": "9b6201b1", "2": "292dfd7b", "3": "167999ad" }[
      chunkId
    ] +
    ".js";

  var timeout = setTimeout(onScriptComplete, 120000);
  script.onerror = script.onload = onScriptComplete;
  function onScriptComplete() {
    // avoid mem leaks in IE.
    script.onerror = script.onload = null;
    clearTimeout(timeout);
    var chunk = installedChunks[chunkId];
    if (chunk !== 0) {
      if (chunk) {
        chunk[1](new Error("Loading chunk " + chunkId + " failed."));
      }
      // 将该 chunk 变为未加载
      installedChunks[chunkId] = undefined;
    }
  }
  head.appendChild(script);

  return promise;
};

我们看到上面的异步加载,可以看出 webpack 是基于 promise 的, 所以需要引入 promise-polyfill,当脚本请求超时或者是加载失败,会将 installedChunks[chunkId] 清空, 当下次重新请求该 chunk 文件会重新加载,提高页面的容错性

大致分为三种情况(已经加载过,正在加载中以及从未加载过)
  1. 已经加载过该 Chunk 文件, 那就不用重新加载该 Chunk 了, 直接执行回调函数即可,可以理解假如页面中有两种操作需要加载
    异步脚本,但是两个脚本都依赖于公共模块, 那么第二次加载的时候发现之前第一次操作已经加载过了该 Chunk, 则不用获取异步脚本了, 以为该公共模块已经被执行过了。

  2. 从未被加载过,则动态的去插入 script 脚本去请求 js 文件, 这就为什么取名 webpackJsonpCallback, 因为跟 jsonp 的思想很类型, 所以这种异步脚本在做脚本错误监控时会经常出现 script error

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

我们通过 utiC 生成的 Chunk:
webpackJsonp(
  [0],
  [
    ,
    /* 0 */ /* 1 */
    /***/ function(module, exports) {
      module.exports = "utils C";

      /***/
    }
  ]
);
  //那么需要异步加载这个 Chunk:
  /***/ 6:
/***/ (function(module, exports, __webpack_require__) {

const utilB = __webpack_require__(0);
console.log(utilB);
// 异步加载文件,类似于 import()
const utilC = () => __webpack_require__.e/* require.ensure */(0).then((function (require) {
  console.log(__webpack_require__(1))
}).bind(null, __webpack_require__)).catch(__webpack_require__.oe);
utilC();

/***/ })

当 pageB 进行某些操作需要加载 utilC 时,就会执行 __webpack_require__.e(0).then(xxx), 代表需要加载的模块 chunkId(utilC), 异步加载 utilC 并将 callback 添加到 installedChunks 中, 然后当 utilC 的 Chunk 文件加载完毕后,chunkids 包含 0, 发 iinstalledChunks[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__);
在 webpack2 或者之后的版本中:

在 version2 中 moduleId[0] 不在为入口函数做保留,所以说明 moduleId[0] 不在是入口打包函数,取而代之的是 window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {}
那么传入的第三个参数 executeModules, 这个数组, 如果参数存在则说明它是入口函数模块, 然后在去执行

那么 webpack 的 tree shaking (官方解释)

这里简单解释下, tree shaking 在打包过程中将没有用的代码进行清除(dead code), 一般 dead code 具有一下特征:
  1. 代码不会被执行, 不可到达
  2. 代码执行结果不会被用到
  3. 代码只会影响死变量(只写不读)

首先,模块引入要基于 ES6 模块机制,不在使用 commonjs 规范,因为 es6 模块的依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析,然后清除没有用的代码, 而 commonjs 的依赖关系是要到运行时才能确定下来,其次需要开始 js 压缩, 使用 UglifysPlugin 这个插件对代码进行压缩,我们看下面的例子:

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

那么我们引入没有用的变量,函数会被清除,未执行的代码会被清除,但是类的方法不会被清除, 因为 webpack 不会区分是定义在 classC 的 prototype 还是其他 Array 的 prototype 的,比如将 classC 写成这样,

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

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

那么 webpack3 是如何做到 scope hoisting (作用域提升)

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

  1. 按需加载的模块
  2. 使用 commonjs 规范的模块
  3. 被多个 entry 共享的模块

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

我们看这个例子比较典型, utilA 被 pageA 和 pageB 共享,utilB 被 pageB 单独加载, utilC 被 pageB 异步加载。那么 webpack3 生效,则需要 plugins 中添加 ModuleConcatenationPlugin。

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

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

// src/hoist/utilC.js
export const utilC = "util C";

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

// src/hoist/pageB.js
import { utilA } from "./utilA";
import { utilB, funcB } from "./utilB";

funcB();
import("./utilC").then(function(utilC) {
  console.log(utilC);
});
我们来看看这个配置如下:
const webpack = require("webpack");
const path = require("path");

module.exports = {
  entry: {
    pageA: path.resolve(__dirname, "../src/hoist/pageA.js"),
    pageB: path.resolve(__dirname, "../src/hoist/pageB.js")
  },
  output: {
    path: path.resolve(__dirname, "../dist"),
    filename: "[name].[chunkhash:8].js"
  },
  plugins: [
    // 需要使用这个插件
    new webpack.optimize.ModuleConcatenationPlugin(),

    new webpack.optimize.CommonsChunkPlugin({
      name: "vendor",
      minChunks: 2
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: "manifest",
      minChunks: Infinity
    })
  ]
};

那么在 webpack4 中有那些新的东西呢?

  1. 配置默认初始化一个些配置, 比如: entry 默认是 ./src
  2. 开发模式和发布模式, 插件默认内置
  3. CommonsChunk 配置简化
  4. 使用 ES6 语法,比如: Map, Set, includes
  5. 新增 WebAssembly 构建支持
  6. 如果要使用 webpack cli 命令, 需要单独安装 webpack-cli

默认配置:

在webpack4 中不再要求强制指定 entry 和 output 路径, 在 webpack4 会默认使用 ./src (entry), ./dist (output);

构建mode:

webpack4 配置, 必p配置 mode 属性,k可选值有 development, production, none,

  1. development 默认开启插件(无需配置):

  2. NamedModulesPlugin > optimization.namedModules

  3. development 模式, 使用 eval 构建 module, 用来提升构建速度

  4. webpack.DefinePlugin 插件 process.env.NODE_ENV 的值不需要再定义, 默认是 development

  5. production 默认开启插件(无需配置):

  6. NoEmitOnErrorsPlugin > optimization.noEmitOnErrors

  7. ModuleCOncatenationPlugin > optimization.concatenateModules

  8. webpack.DefinePlugin 插件 process.env.NODE_ENV 的值不需要再定义, 默认是 production

公共代码提取:

webpack3 的 commonsChunk hash 问题不是很优雅, 使用复杂, webpack4 中直接将 CommonsChunkPlugin 插件直接改为
optimization.splitChunksoptmization.runTimeChunk 两个配置

  • webapck3:

    plugins:[
      new webpack.optimize.CommonsChunkPlugin({ names: 'common'}),
      new webpack.optimize.CommonsChunkPlugin({ name: 'runtime', chunks:['common']})
    ]
    
  • webpack4

optimization: {
   splitChunks: {
     chunks: 'all',
     name: 'common',
   },
   runtimeChunk: {
     name: 'runtime',
   }
 }

压缩

压缩插件更新到 uglifyjs-webpack-plugin 1.0 版本,支持多进程压缩,缓存以及 es6 语法, 无需单独安装转换器, 当 mode='production' 默认开始时压缩, 无需配置, 可以通过, ``optimization.minimize 和 optimization.minimizer``` 自定义配置, 测试发现,第二次打包时间是第一次打包的一半左右

. optimization.minimize 是否启用压缩
. optimizatioon.minimizer 制定压缩库, 默认 uglifyjs-webpack-plugin 1.0

optimization: {
   minimize:true
}

配置优化

webpack4 提供了 sideEffects 的配置, 引入的第三方插件在 package.json 里面配置。sideEffects: fasle, 后, 据说是可以大幅度的减少打包出的体积; 目前初步了解 sideEffects 的信息: sideEffects: false, 标示该模块无副作用,当你需要导入,但不需要导出任何东西时,但需要导入时

【未完待续.......】

参考原文
参考案例(ts+react+mobx+react-router+antd+less)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269