https://juejin.cn/post/6943468761575849992
https://www.cnblogs.com/lzkwin/p/11878509.html

前言:webpack到底将代码编译成了什么

1.commonJS下的打包结果

webpack4打包结果

index.js 引入

  1. const sayHello = require('./hello')
  2. console.log(sayHello('leah'))

hello.js 导出

  1. module.exports = function (name) {
  2. return 'hello' + name
  3. }

webpack4执行打包结果

  1. (function(modules) { // webpackBootstrap
  2. // 缓存已经加载过的 module 的 exports
  3. var installedModules = {};
  4. // __webpack_require__ 与 commonjs 的 require类似,它是 webpack加载函数,用来加载webpack定义的模块,返回exports导出对象
  5. function __webpack_require__(moduleId) {
  6. // 如果缓存中存在当前模块就直接返回
  7. if(installedModules[moduleId]) {
  8. return installedModules[moduleId].exports;
  9. }
  10. //第一次加载时, 初始化时模块对象,并将当前模块进行缓存
  11. var module = installedModules[moduleId] = {
  12. i: moduleId, // 模块id
  13. l: false, // 标记是否已加载
  14. exports: {} // 模块导出对象
  15. };
  16. // 执行模块函数,改变模块包裹函数内部的this指向,module当前模块对象的引用,module.exports 模块导出对象的引用,__webpack_require__ 用于在模块中加载其他模块
  17. modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
  18. // 标记已加载模块
  19. module.l = true;
  20. return module.exports;
  21. }
  22. // 加载入口模块
  23. return __webpack_require__(__webpack_require__.s = "./src/index.js");
  24. })
  25. ({
  26. "./src/hello.js":
  27. (function(module, exports) {
  28. eval("module.exports = function (name) {\n return 'hello' + name\n}\n\n//# sourceURL=webpack:///./src/hello.js?");
  29. }),
  30. "./src/index.js":
  31. (function(module, exports, __webpack_require__) {
  32. eval("const sayHello = __webpack_require__(/*! ./hello */ \"./src/hello.js\")\nconsole.log(sayHello('leah'))\n\n//# sourceURL=webpack:///./src/index.js?");
  33. })
  34. });

上面代码的核心骨架其实就是一个IIFE (立即调用函数表达式),简化如下:

  1. (function(modules){
  2. // ...
  3. })({
  4. path1: function1,
  5. path2: function2
  6. })

这个立即执行函数接受一个对象 modules 作为参数

  1. ({
  2. "./src/hello.js":
  3. (function(module, exports) {
  4. eval("module.exports = function (name) {\n return 'hello' + name\n}\n\n//# sourceURL=webpack:///./src/hello.js?");
  5. }),
  6. "./src/index.js":
  7. (function(module, exports, __webpack_require__) {
  8. eval("const sayHello = __webpack_require__(/*! ./hello */ \"./src/hello.js\")\nconsole.log(sayHello('leah'))\n\n//# sourceURL=webpack:///./src/index.js?");
  9. })
  10. });

这个modules包含了所有打包后的模块,key 为依赖文件路径, value 是一个简单处理过后的函数,函数内部的代码不完全等同于是我们编写的源码,而是被webpack包裹后的内容。 这就是modules接收到的数据。

需要将require方法改写成webpack_require方法,因为浏览器端不支持require方法。

它的执行过程如下:

  1. 加载模块并判断是否有缓存,如果有缓存就返回缓存的模块的export对象,也就是 module.exports
  2. 否则就是第一次加载,初始化模块对象,并进行缓存
  3. 执行文件路径对应的模块函数
  4. 将这个模块标记为已加载
  5. 执行完模块返回该模块的 export 对象

webpack5打包结果

  1. (() => { // webpackBootstrap
  2. // 将原本传入的key value 文件对象直接放到了函数里面
  3. var __webpack_modules__ = ({
  4. "./src/hello.js":
  5. ((module) => {
  6. eval("module.exports = function (name) {\n return 'hello' + name\n}\n\n//# sourceURL=webpack://webpack-demo/./src/hello.js?");
  7. }),
  8. "./src/index.js":
  9. ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {
  10. eval("const sayHello = __webpack_require__(/*! ./hello */ \"./src/hello.js\")\nconsole.log(sayHello('leah'))\n\n//# sourceURL=webpack://webpack-demo/./src/index.js?");
  11. })
  12. });
  13. // 缓存模块
  14. var __webpack_module_cache__ = {};
  15. // 模块加载函数
  16. function __webpack_require__(moduleId) {
  17. // 如果缓存中存在当前模块就直接返回
  18. var cachedModule = __webpack_module_cache__[moduleId];
  19. if (cachedModule !== undefined) {
  20. return cachedModule.exports;
  21. }
  22. // 第一次加载时, 初始化时模块对象,并将当前模块进行缓存
  23. var module = __webpack_module_cache__[moduleId] = {
  24. // no module.id needed
  25. // no module.loaded needed
  26. exports: {}
  27. };
  28. // 执行模块函数,module当前模块对象的引用,module.exports 模块导出对象的引用,__webpack_require__ 用于在模块中加载其他模块
  29. __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
  30. // Return the exports of the module
  31. return module.exports;
  32. }
  33. // startup
  34. // Load entry module and return exports
  35. // This entry module can't be inlined because the eval devtool is used.
  36. var __webpack_exports__ = __webpack_require__("./src/index.js");
  37. })()
  38. ;

webpack5 将function改成了箭头函数,这样this指向就更加清晰,放弃了原来的 call方法调用。

将原本通过modules传递的参数的 key value 直接写到了函数中。

  1. var __webpack_modules__ = ({
  2. "./src/hello.js":
  3. ((module) => {
  4. eval("module.exports = function (name) {\n return 'hello' + name\n}\n\n//# sourceURL=webpack://webpack-demo/./src/hello.js?");
  5. }),
  6. "./src/index.js":
  7. ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {
  8. eval("const sayHello = __webpack_require__(/*! ./hello */ \"./src/hello.js\")\nconsole.log(sayHello('leah'))\n\n//# sourceURL=webpack://webpack-demo/./src/index.js?");
  9. })
  10. });

打包结果更加简洁。

2.ES Module规范下的打包结果

webpack4 的打包结果

  1. (function(modules) { // webpackBootstrap
  2. // The module cache
  3. var installedModules = {};
  4. // The require function
  5. function __webpack_require__(moduleId) {
  6. // Check if module is in cache
  7. if(installedModules[moduleId]) {
  8. return installedModules[moduleId].exports;
  9. }
  10. // Create a new module (and put it into the cache)
  11. var module = installedModules[moduleId] = {
  12. i: moduleId,
  13. l: false,
  14. exports: {}
  15. };
  16. // Execute the module function
  17. modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
  18. // Flag the module as loaded
  19. module.l = true;
  20. // Return the exports of the module
  21. return module.exports;
  22. }
  23. // define __esModule on exports
  24. __webpack_require__.r = function(exports) {
  25. if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
  26. Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
  27. }
  28. Object.defineProperty(exports, '__esModule', { value: true });
  29. };
  30. // Load entry module and return exports
  31. return __webpack_require__(__webpack_require__.s = "./src/index.js");
  32. })
  33. ({
  34. "./src/hello.js":
  35. (function(module, __webpack_exports__, __webpack_require__) {
  36. eval("__webpack_require__.r(__webpack_exports__);\n// module.exports = function (name) {\n// return 'hello' + name\n// }\n\nconst sayHello = function (name) {\n return 'hello' + name\n}\n\n/* harmony default export */ __webpack_exports__[\"default\"] = (sayHello);\n\n//# sourceURL=webpack:///./src/hello.js?");
  37. }),
  38. "./src/index.js":
  39. (function(module, __webpack_exports__, __webpack_require__) {
  40. eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _hello__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./hello */ \"./src/hello.js\");\n// const sayHello = require('./hello')\n// console.log(sayHello('leah'))\n\n\nconsole.log(Object(_hello__WEBPACK_IMPORTED_MODULE_0__[\"default\"])('leah'))\n\n//# sourceURL=webpack:///./src/index.js?");
  41. })
  42. });

webpack5的打包结果

  1. (() => { // webpackBootstrap
  2. var __webpack_modules__ = ({
  3. "./src/hello.js":
  4. ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
  5. eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n// module.exports = function (name) {\n// return 'hello' + name\n// }\n\nconst sayHello = function (name) {\n return 'hello' + name\n}\n\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (sayHello);\n\n//# sourceURL=webpack://webpack-demo/./src/hello.js?");
  6. }),
  7. "./src/index.js":
  8. ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
  9. eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _hello__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./hello */ \"./src/hello.js\");\n// const sayHello = require('./hello')\n// console.log(sayHello('leah'))\n\n\nconsole.log((0,_hello__WEBPACK_IMPORTED_MODULE_0__.default)('leah'))\n\n//# sourceURL=webpack://webpack-demo/./src/index.js?");
  10. })
  11. });
  12. // The module cache
  13. var __webpack_module_cache__ = {};
  14. // The require function
  15. function __webpack_require__(moduleId) {
  16. // Check if module is in cache
  17. var cachedModule = __webpack_module_cache__[moduleId];
  18. if (cachedModule !== undefined) {
  19. return cachedModule.exports;
  20. }
  21. // Create a new module (and put it into the cache)
  22. var module = __webpack_module_cache__[moduleId] = {
  23. // no module.id needed
  24. // no module.loaded needed
  25. exports: {}
  26. };
  27. // Execute the module function
  28. __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
  29. // Return the exports of the module
  30. return module.exports;
  31. }
  32. /* webpack/runtime/make namespace object */
  33. (() => {
  34. // define __esModule on exports
  35. __webpack_require__.r = (exports) => {
  36. if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
  37. Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
  38. }
  39. Object.defineProperty(exports, '__esModule', { value: true });
  40. };
  41. })();
  42. var __webpack_exports__ = __webpack_require__("./src/index.js");
  43. })()
  44. ;

可以看到esmodule的打包结果和 commonjs的打包结果没有太大区别,唯一的不同是处理后的模块代码.

  1. ({
  2. "./src/hello.js":
  3. (function(module, __webpack_exports__, __webpack_require__) {
  4. eval("__webpack_require__.r(__webpack_exports__);\n// module.exports = function (name) {\n// return 'hello' + name\n// }\n\nconst sayHello = function (name) {\n return 'hello' + name\n}\n\n/* harmony default export */ __webpack_exports__[\"default\"] = (sayHello);\n\n//# sourceURL=webpack:///./src/hello.js?");
  5. }),
  6. "./src/index.js":
  7. (function(module, __webpack_exports__, __webpack_require__) {
  8. eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _hello__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./hello */ \"./src/hello.js\");\n// const sayHello = require('./hello')\n// console.log(sayHello('leah'))\n\n\nconsole.log(Object(_hello__WEBPACK_IMPORTED_MODULE_0__[\"default\"])('leah'))\n\n//# sourceURL=webpack:///./src/index.js?");
  9. })
  10. });

对比上面的Commonjs规范处理结果

  1. ({
  2. "./src/hello.js":
  3. (function(module, exports) {
  4. eval("module.exports = function (name) {\n return 'hello' + name\n}\n\n//# sourceURL=webpack:///./src/hello.js?");
  5. }),
  6. "./src/index.js":
  7. (function(module, exports, __webpack_require__) {
  8. eval("const sayHello = __webpack_require__(/*! ./hello */ \"./src/hello.js\")\nconsole.log(sayHello('leah'))\n\n//# sourceURL=webpack:///./src/index.js?");
  9. })
  10. });

ES6 module 传入的参数 function(module, webpack_exports, webpack_require),

Commonjs 传入的参数 function(module, exports),

webpack_require.r()函数的作用是给webpack_exports添加一个__esModule为true的属性,表示这是一个 ES6 module。

如果该export对象是 ES6 module,则返回module[‘default’],即export default对应的变量。如果不是 ES6 module 则直接返回export。

3.按需加载下的打包结果

webpack5打包结果

在index.js文件中适用动态引入的方法引入 hello.js

  1. import('./hello').then(sayHello => {
  2. console.log(sayHello('leah'))
  3. })

安装动态引入的babel插件

  1. npm install babel-plugin-dynamic-import-webpack

在 webpack.config.js中添加配置

  1. module.exports = {
  2. module: {
  3. rules:[
  4. {
  5. test: /\.js$/,
  6. exclude: /node_modules/,
  7. loader: "babel-loader",
  8. options: {
  9. "plugins":[
  10. "dynamic-import-webpack"
  11. ]
  12. }
  13. }
  14. ]
  15. }
  16. }

执行 npm run build 进行打包

image.png

可以看到此时打包结果是两个文件,一个是主文件main,一个是 src_hello.js文件,这个src_hello.js文件对应的代码就是动态引入的代码。

main.js

  1. (() => { // webpackBootstrap
  2. var __webpack_modules__ = ({
  3. "./src/index.js":
  4. ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {
  5. eval("// const sayHello = require('./hello')\n// console.log(sayHello('leah'))\n// import sayHello from './hello';\n// import sayHello2 from './hello2'\n// import sayHello from './hello'\n// console.log(sayHello('leah'))\nnew Promise(function (resolve) {\n __webpack_require__.e(/*! require.ensure */ \"src_hello_js\").then((function (require) {\n resolve(__webpack_require__(/*! ./hello */ \"./src/hello.js\"));\n }).bind(null, __webpack_require__)).catch(__webpack_require__.oe);\n}).then(function (sayHello) {\n console.log(sayHello('leah'));\n});\n\n//# sourceURL=webpack://webpack-demo/./src/index.js?");
  6. })
  7. });
  8. var __webpack_module_cache__ = {};
  9. // 模块加载函数
  10. function __webpack_require__(moduleId) {
  11. // 如果缓存中存在当前模块就直接返回
  12. var cachedModule = __webpack_module_cache__[moduleId];
  13. if (cachedModule !== undefined) {
  14. return cachedModule.exports;
  15. }
  16. // 第一次加载时, 初始化时模块对象,并将当前模块进行缓存
  17. var module = __webpack_module_cache__[moduleId] = {
  18. // no module.id needed
  19. // no module.loaded needed
  20. exports: {}
  21. };
  22. // 执行模块函数,改变模块包裹函数内部的this指向,module当前模块对象的引用,module.exports 模块导出对象的引用,__webpack_require__ 用于在模块中加载其他模块
  23. __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
  24. // Return the exports of the module
  25. return module.exports;
  26. }
  27. // expose the modules object (__webpack_modules__)
  28. __webpack_require__.m = __webpack_modules__;
  29. /* webpack/runtime/ensure chunk */
  30. (() => {
  31. __webpack_require__.f = {};
  32. // This file contains only the entry chunk.
  33. // The chunk loading function for additional chunks 实例化一个Promise.all 异步插入script脚本
  34. __webpack_require__.e = (chunkId) => {
  35. return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {
  36. __webpack_require__.f[key](chunkId, promises);
  37. return promises;
  38. }, []));
  39. };
  40. })();
  41. /* webpack/runtime/hasOwnProperty shorthand */
  42. (() => {
  43. __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
  44. })();
  45. /* webpack/runtime/load script */
  46. (() => {
  47. var inProgress = {};
  48. var dataWebpackPrefix = "webpack-demo:";
  49. // loadScript function to load a script via script tag
  50. // 通过script标记加载脚本
  51. __webpack_require__.l = (url, done, key, chunkId) => {
  52. // 如果已经加载就返回
  53. if(inProgress[url]) { inProgress[url].push(done); return; }
  54. var script, needAttach;
  55. if(key !== undefined) {
  56. var scripts = document.getElementsByTagName("script");
  57. for(var i = 0; i < scripts.length; i++) {
  58. var s = scripts[i];
  59. if(s.getAttribute("src") == url || s.getAttribute("data-webpack") == dataWebpackPrefix + key) { script = s; break; }
  60. }
  61. }
  62. // 如果script 标签不存在就创建
  63. if(!script) {
  64. needAttach = true;
  65. // 生成一个 script 标签
  66. script = document.createElement('script');
  67. script.charset = 'utf-8';
  68. // script 标签设置一个 2 分钟的超时时间
  69. script.timeout = 120;
  70. if (__webpack_require__.nc) {
  71. script.setAttribute("nonce", __webpack_require__.nc);
  72. }
  73. script.setAttribute("data-webpack", dataWebpackPrefix + key);
  74. // URL 是 需要动态导入模块的 URL。
  75. script.src = url;
  76. }
  77. inProgress[url] = [done];
  78. var onScriptComplete = (prev, event) => {
  79. // avoid mem leaks in IE.
  80. script.onerror = script.onload = null;
  81. clearTimeout(timeout);
  82. var doneFns = inProgress[url];
  83. delete inProgress[url];
  84. script.parentNode && script.parentNode.removeChild(script);
  85. doneFns && doneFns.forEach((fn) => (fn(event)));
  86. if(prev) return prev(event);
  87. }
  88. ;
  89. var timeout = setTimeout(onScriptComplete.bind(null, undefined, { type: 'timeout', target: script }), 120000);
  90. script.onerror = onScriptComplete.bind(null, script.onerror);
  91. script.onload = onScriptComplete.bind(null, script.onload);
  92. // 然后添加到页面中 document.head.appendChild(script),开始加载模块。
  93. needAttach && document.head.appendChild(script);
  94. };
  95. })();
  96. /* webpack/runtime/jsonp chunk loading */
  97. (() => {
  98. // no baseURI
  99. // object to store loaded and loading chunks
  100. // undefined = chunk not loaded, null = chunk preloaded/prefetched
  101. // [resolve, reject, Promise] = chunk loading, 0 = chunk loaded
  102. // 用来保存已经加载和正在加载的模块
  103. // undefined表示chunk未加载,null表示chunk preloaded/prefetched
  104. // Promise = 模块正在加载, 0 表示已经加载
  105. var installedChunks = {
  106. "main": 0 // main模块已经加载
  107. };
  108. /**
  109. *
  110. * __webpack_require__.f的作用是判断模块是否已加载(或加载中),如果都不是,就利用jsonp加载模块。
  111. */
  112. __webpack_require__.f.j = (chunkId, promises) => {
  113. // JSONP chunk loading for javascript
  114. var installedChunkData = __webpack_require__.o(installedChunks, chunkId) ? installedChunks[chunkId] : undefined;
  115. if(installedChunkData !== 0) { // 0 表示已经加载".
  116. // a Promise means "currently loading
  117. // promise表示正在加载".
  118. if(installedChunkData) {
  119. // 将这个加载中的 Promise 推入 promises 数组。
  120. promises.push(installedChunkData[2]);
  121. } else {
  122. if(true) { // all chunks have JS
  123. // setup Promise in chunk cache
  124. //就新建一个 Promise,用于加载需要动态导入的模块。
  125. var promise = new Promise((resolve, reject) => (installedChunkData = installedChunks[chunkId] = [resolve, reject]));
  126. // 将这个加载中的 Promise 推入 promises 数组。
  127. promises.push(installedChunkData[2] = promise); // installedChunkData = [resolve, reject,promise],例如installedChunkData保存新创建的promise以及他的resolve和reject promises => [promise]
  128. // start chunk loading
  129. // 开始加载模块
  130. var url = __webpack_require__.p + __webpack_require__.u(chunkId);
  131. // create error before stack unwound to get useful stacktrace later
  132. // 在堆栈展开之前创建错误,以便稍后获得有用的stacktrace
  133. var error = new Error();
  134. var loadingEnded = (event) => {
  135. if(__webpack_require__.o(installedChunks, chunkId)) {
  136. installedChunkData = installedChunks[chunkId];
  137. if(installedChunkData !== 0) installedChunks[chunkId] = undefined;
  138. if(installedChunkData) {
  139. var errorType = event && (event.type === 'load' ? 'missing' : event.type);
  140. var realSrc = event && event.target && event.target.src;
  141. error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
  142. error.name = 'ChunkLoadError';
  143. error.type = errorType;
  144. error.request = realSrc;
  145. installedChunkData[1](error);
  146. }
  147. }
  148. };
  149. // 加载模块逻辑
  150. __webpack_require__.l(url, loadingEnded, "chunk-" + chunkId, chunkId);
  151. } else installedChunks[chunkId] = 0;
  152. }
  153. }
  154. };
  155. // install a JSONP callback for chunk loading
  156. var webpackJsonpCallback = (parentChunkLoadingFunction, data) => {
  157. var [chunkIds, moreModules, runtime] = data;
  158. // add "moreModules" to the modules object,
  159. // then flag all "chunkIds" as loaded and fire callback
  160. var moduleId, chunkId, i = 0;
  161. for(moduleId in moreModules) {
  162. // 如果在 moreModules 中能找到 moduleId 属性就代表
  163. if(__webpack_require__.o(moreModules, moduleId)) {
  164. __webpack_require__.m[moduleId] = moreModules[moduleId];
  165. }
  166. }
  167. if(runtime) runtime(__webpack_require__);
  168. if(parentChunkLoadingFunction) parentChunkLoadingFunction(data);
  169. for(;i < chunkIds.length; i++) {
  170. chunkId = chunkIds[i];
  171. if(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {
  172. installedChunks[chunkId][0]();
  173. }
  174. installedChunks[chunkIds[i]] = 0; // 把模块标记为已加载
  175. }
  176. }
  177. // 把模块数据放到 self["webpackChunkwebpack_demo"] 数组中
  178. var chunkLoadingGlobal = self["webpackChunkwebpack_demo"] = self["webpackChunkwebpack_demo"] || [];
  179. chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
  180. chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));
  181. })();
  182. })()
  183. ;

src_hello.js

  1. (self["webpackChunkwebpack_demo"] = self["webpackChunkwebpack_demo"] || []).push([["src_hello_js"],{
  2. "./src/hello.js":
  3. ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
  4. eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n// module.exports = function (name) {\n// return 'hello' + name\n// }\nvar sayHello = function sayHello(name) {\n return 'hello' + name;\n};\n\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (sayHello);\n\n//# sourceURL=webpack://webpack-demo/./src/hello.js?");
  5. })
  6. }]);

main 文件代码有点长,其核心方法有以下几个:

  1. **__webpack_require__.f.j **和 **__webpack_require__.l** 还有 **webpackJsonpCallback**

webpack_require.f.j 和 webpack_require.l 的作用是判断模块是否已经加载,(在webpack4中叫webpack_require.e,webpack5 将它拆分成了两个方法) ,若没有加载就通过 jsonp 加载模块文件,加载完模块文件就执行 webpackJsonpCallback 函数将模块存在全局对象 self上,也就是window上。(webpack4 的写法是(window[“webpackJsonp”] = window[“webpackJsonp”] || []).push([[1],)。

4.tree-shaking 下的打包结果

仔细观察上面的ESModule打包结果,就会发现还有个两个不同于Commonjs规范下的打包属性,harmony default export 和 harmony import,这是干什么的呢?

如果有对大名鼎鼎的 tree-shaking 了解过的话,就知道 tree-shaking 正是借助于 es6 module 静态分析的特点,在编译阶段将代码的使用情况进行了标注,然后在生产模式下对没用到的代码进行摇树闪电,具体是怎么标注的呢?我们看下面的代码打包结果:

webpack4打包结果

在 util.js 中导出两个方法

  1. export function func1() {
  2. return 'func1'
  3. }
  4. export function func2() {
  5. return 'func2'
  6. }

在 index.js 中导入

  1. import {
  2. func1,
  3. func2
  4. } from './util'
  5. let result1 = func1()
  6. console.log(result1)

在webpack.config.js中开启生产模式,这是因为webpack在生产模式下才会开启摇树,同时为了看到webpack 对代码使用情况的标注,暂时先关闭optimization,配置如下:

  1. mode: 'production',
  2. optimization:{
  3. minimize:false,
  4. concatenateModules: false,
  5. },

我记得之前是要关闭掉babel的模块转换功能,保留原有的es6语法,助于tree-shaking静态分析

  1. const config = {
  2. presets: [
  3. [
  4. '[@babel/preset-env]',
  5. {
  6. modules: false
  7. }
  8. ]
  9. ]
  10. };

npm run build 查看打包结果

精简之后的代码如下:

  1. (function(modules) { // webpackBootstrap
  2. })
  3. ([
  4. (function(module, __webpack_exports__, __webpack_require__) {
  5. "use strict";
  6. /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return func1; });
  7. /* unused harmony export func2 */
  8. function func1() {
  9. return 'func1';
  10. }
  11. function func2() {
  12. return 'func2';
  13. }
  14. }),
  15. ]);
  16. (() => {
  17. /* harmony import */ var _util__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(293);
  18. // tree-shaking
  19. let result1 = (0,_util__WEBPACK_IMPORTED_MODULE_0__/* .func1 */ .w)();
  20. console.log(result1);
  21. })();

可以看到被使用过的 export 标记为 / harmony export ([type]) / ,其中 [type] 和 webpack 内部有关,可能是 binding、immutable 等等;

没被使用过的 export 标记为 / unused harmony export [FuncName] / ,其中 [FuncName] 是 export 的方法名称;

所有的 import 标记为 / harmony import /

接下来我们看一下摇树之后的代码结构,修改webpack配置,开启摇树

  1. mode: 'production',
  2. optimization:{
  3. usedExports:true, // Webpack 将识别出它认为没有被使用的代码,并在最初的打包步骤中给它做标记。
  4. minimize:true, // 支持删除死代码的压缩器
  5. concatenateModules: false,
  6. },

打包之后的代码

  1. !function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=1)}([function(e,t,n){"use strict";function r(){return"func1"}n.d(t,"a",(function(){return r}))},function(e,t,n){"use strict";n.r(t);var r=n(0);let o=Object(r.a)();console.log(o)}]);

可以看到只打包了func1, func2没有被打包。

webpack5 打包结果

代码分析阶段:

  1. (() => { // webpackBootstrap
  2. "use strict";
  3. var __webpack_modules__ = ({
  4. 293:
  5. ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
  6. /* harmony export */ __webpack_require__.d(__webpack_exports__, {
  7. /* harmony export */ "w": () => (/* binding */ func1)
  8. /* harmony export */ });
  9. /* unused harmony export func2 */
  10. function func1() {
  11. return 'func1';
  12. }
  13. function func2() {
  14. return 'func2';
  15. }
  16. })
  17. });
  18. var __webpack_exports__ = {};
  19. (() => {
  20. /* harmony import */ var _util__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(293);
  21. // tree-shaking
  22. let result1 = (0,_util__WEBPACK_IMPORTED_MODULE_0__/* .func1 */ .w)();
  23. console.log(result1);
  24. })();
  25. })()

摇树结果:

  1. (()=>{"use strict";var r={293:(r,e,t)=>{function o(){return"func1"}t.d(e,{w:()=>o})}},e={};function t(o){var n=e[o];if(void 0!==n)return n.exports;var c=e[o]={exports:{}};return r[o](c,c.exports,t),c.exports}t.d=(r,e)=>{for(var o in e)t.o(e,o)&&!t.o(r,o)&&Object.defineProperty(r,o,{enumerable:!0,get:e[o]})},t.o=(r,e)=>Object.prototype.hasOwnProperty.call(r,e),(()=>{let r=(0,t(293).w)();console.log(r)})()})();

可以看到webpack4 和webpack5的处理结果都差不多,都去掉了未使用的地方。官方解释是webpack现在可以针对导出的嵌套模块进行访问,重新导出对象时,可以改善tree shaking。

TODO:差异分析

webpack5新特性

https://juejin.cn/post/7123969545387114509
https://juejin.cn/post/6844903795286081550

1.更好的tree-shaking体验

2.优化持久缓存

3.nodejs的polyfill脚本被移除

一、webpack工作原理

https://juejin.cn/post/6859538537830858759#heading-9

webpack - 图2

  1. 初始化参数:从配置文件和Shell语句中读取与合并参数,得出最终的参数; ```javascript //通过此文件,需要解析编译用户配置的webpack.config.js文件

//1.需要找到当前执行名的路径 拿到webpack.config.js //1.1拿到文件路径 let path = require(‘path’) //1.2config配置文件 let config = require(path.resolve(__dirname)) //1.3编译配置文件 let Compiler = require(‘./lib/Compiler’) let compiler = new Compiler(config) //1.4运行 compiler.run()

  1. 2. 开始编译: 用上一步得到的参数初始化Complier对象,加载所有配置的插件,执行对象的run方法开始执行编译;
  2. ```javascript
  3. class Complier{
  4. constructor(config){
  5. this.config = config
  6. //需要保存入口文件的路径
  7. this.entryId //主模块路径 "./src/index.js"
  8. //需要保存所有模块的依赖
  9. this.module = {}
  10. //入口路径
  11. this.entry = config.entry
  12. //工作目录 是指执行打包命令的文件夹地址 比如在d:/aa/b目录下执行 npm run build 那么cwd就是d:/aa/b
  13. this.root = process.cwd()
  14. }
  15. buildModule(modulePath,isEntry){
  16. }
  17. emitFile(){
  18. }
  19. run(){
  20. //创建模块的依赖关系
  21. this.buildModule(path.resolve(this.root,this.entry),true) //true表示是主模块
  22. //发射一个文件 打包后的文件
  23. this.emitFile()
  24. }
  25. }
  26. module.exports = Complier
  1. 在Compiler文件中写主要的打包逻辑,拿到webpack.config.js里面的配置信息,解析入口,解析文件依赖关系,发射文件。

    1. getSource(modulePath){
    2. //拿到模块内容
    3. let content = fs.readFileSync(modulePath,'utf8')
    4. return content
    5. }
    6. //构建模块
    7. buildModule(modulePath,isEntry){
    8. //拿到路径对应的内容
    9. let source = this.getSource(modulePath)
    10. //模块id
    11. let moduleName = './'+path.relative(this.root, modulePath)
    12. console.log(sorce,moduleName) //sorce: let str = require('./a.js') console.log(str) moduleName: './src/index.js'
    13. }
    14. emitFile(){
    15. }
    16. run(){
    17. //创建模块的依赖关系
    18. this.buildModule(path.resolve(this.root,this.entry),true) //true表示是主模块
    19. //发射一个文件 打包后的文件
    20. this.emitFile()
    21. }
  2. 大概流程就是这样,构建模块时,我们需要拿到模块的内容(我们编写的源码)。这个通过getSource函数拿到即可。我们还需要拿到模块id

  3. 确定入口: 根据配置中的entry找出所有入口文件;
  4. 编译模块:从入口文件出发,调用所有配置的Loader对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
    下面需要把let str = require(‘./a.js’) 这种,/a.js转换成 ‘./src/a.js’ ,还有一个是将require方法改成 webpack_requireconsole.log,这就是解析语法树的工作
    parse方法需要安装几个包来解析,还需要看看ast的结构

    1. //解析源码
    2. //babylon 把源码转换成ast
    3. // @babel/traverse 对 AST树进行遍历编译,得到新的 AST树;
    4. //@babel/types
    5. //@babel/generator 通过 AST树生成 ES5 代码。
    6. parse(source,parentPath){ //AST解析语法树
    7. console.log(source,parentPath)
    8. let ast = babylon.parse(source)
    9. let dependencies = [] //存放依赖模块
    10. traverse(ast,{
    11. CallExpression(p){
    12. let node = p.node //对应的节点
    13. if(node.callee.name === 'require') {
    14. node.callee.name = "__webpack_require__" //改require名字
    15. let moduleName = node.arguments[0].value //取到引用模块的名字 a
    16. moduleName = moduleName + (path.extname(moduleName)?'': '.js') //拼接成./a.js
    17. moduleName = './'+path.join(parentPath,moduleName) // ./src/a.js
    18. dependencies.push(moduleName) //将这个依赖模块存入数组
    19. node.arguments = [traverse.stringLiteral(moduleName)] //改源码
    20. }
    21. }
    22. })
    23. let sourceCode = generator(ast).code
    24. return {sourceCode,dependencies}
    25. }
    26. //构建模块
    27. buildModule(modulePath,isEntry){
    28. //拿到路径对应的内容
    29. let source = this.getSource(modulePath)
    30. //模块id 'src/index.js'
    31. let moduleName = './'+path.relative(this.root, modulePath)
    32. console.log(sorce,moduleName) //sorce: let str = require('./a.js') console.log(str) moduleName: './src/index.js'
    33. if(isEntry){
    34. this.entryId = moduleName //保存入口文件名字
    35. }
    36. //解析需要把source源码进行改造 返回一个依赖列表 比如index.js文件里面引入了a.js,需要把这个a.js进行解析,a.js里面要是再引入b.js也要把b.js对应的内容解析
    37. let {sourceCode,dependencies} = this.parse(source,path.dirname(moduleName)) // path.dirname(moduleName)取父路径 .src
    38. console.log(sourceCode,dependencies)
    39. //把模块路径和模块中的内容对应起来
    40. this.modules[moduleName] = sourceCode
    41. //若依赖模块里面又依赖别的模块就需要递归解析
    42. dependencies.forEach(dep=>{
    43. this.buildModule(path.join(this.root,dep),false) //false表示不是主模块
    44. })
    45. }
    46. emitFile(){
    47. }
    48. run(){
    49. //创建模块的依赖关系
    50. this.buildModule(path.resolve(this.root,this.entry),true) //true表示是主模块
    51. console.log(this.modules,this.entryId)
    52. //发射一个文件 打包后的文件
    53. this.emitFile()
    54. }
  5. 完成模块编译: 在经过第4步使用Loader翻译完所有模块后,得到了每个模块被翻译后的最终内容以及他们之间的依赖关系;

  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的Chunk,再把每个Chunk转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;
  7. 输出完成: 在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

底层原理不太了解

vuex直接赋值是生效的,页面不会更新,

学习建议:底层原理,

计算机组成原理、

history模式:地址很像url,需要

二、Tree Shaking的使用和原理分析

https://juejin.im/post/6844903544756109319

1.Tree Sharking 原理

Tree-Shaking 最早是由 Rich Harris 在打包⼯具Rollup 提出并且实现的,其实在更早,Google Closure Compiler 也做过类似的事情。在 Webpack 2 中吸取了 Tree-Shaking 功能,并且在 Webpack 中得到实现。
⽆⽤代码消除(Dead Code Elimination - DCE)Tree-Shaking 的本质是消除⽆⽤的 JavaScript 代码。

Tree-Shaking 是 DCE 的⼀种新的实现,Javascript 同传统的编程语⾔不同的是,JavaScript 绝⼤多数情况是浏览器中执⾏,需要通过⽹络进⾏加载,然后解析 JavaScript ⽂件再执⾏。

Tree-Shaking 和传统的 DCE 的⽅法又不太⼀样,传统的 DCE 消除不可能执⾏的代码:

  • 程序中没有执⾏的代码,如不可能进⼊的分⽀,return 之后的语句等
  • 导致 dead variable 的代码,写⼊变量之后不再读取的代码

和 DCE 不同的是,Tree-Shaking 更关注于消除没有⽤到的代码。
Webpack 是基于 ES6 Modules 静态语法解析的构建⼯具,Tree-Shaking 之所以能够在 Webpack 实现,依靠 ES6 Modules 静态解析的特性。ES6 的模块声明保证了依赖关系是提前确定的,使得静态分析成为可能,这样在 Webpack 中代码不需要执⾏就可以知道是否被使⽤,⾃然就知道哪些是⽆⽤的代码了。
ES6 Modules 特点:

  • 只能作为模块顶层的语句出现
  • ES6 中 import 和 export 是显性声明的
  • import 的模块名只能是字符串常量
  • ES6 模块的依赖关系是可以根据 import 引⽤关系推导出来的。
  • ES6 模块的依赖关系与运⾏时状态⽆关,也就是说 import binding 是 immutable 的。

上⾯这些 ES6 Modules 的特点是 Tree-Shaking 的基础。

静态分析或者说依赖关系与运⾏时状态⽆关就是说:不执⾏代码就能知道引⽤了什么模块。⽤ cjs 的 require ⼀个模块,就是动态的,只有执⾏之后才知道引⽤的什么模块,这个就不能通过静态分析去做优化。
所以这也是为什么 Rollup 和 Webpack 都要⽤ ES6 Module 语法才能实现 Tree-Shaking

2.Tree Sharking 运⾏时


Webpack 的 Tree-Shaking 实际分了两步来实现:

  1. Webpakck 分析 ES6 Modules 的引⼊和使⽤情
    况,去除不使⽤的 import 引⼊
  2. 删除⽆⽤代码则是借助 terser-webpackplugin,这需要 mode: ‘production 才会被启⽤

3.Tree-Shaking 并不是银弹

Tree-Shaking 的局限性:

  • 必须遵循 ES6 Modules 规范,如果使⽤ cjs 规范则⽆法使⽤ Tree-Shaking 功能。
  • Tree-Shaking 只能处理顶层内容。⽐如类和对象内部都不会再被处理,这也是由于 JavaScript 动态语⾔特性导致的。(引⼊了⼀个 Class,只⽤了⾥⾯的某⼏个⽅法,其它⼏个没有被使⽤的⽅法 TreeShaking 不能把它⼲掉)
  • 副作⽤(Side Effect)代码。

副作⽤这个概念来源于函数式编程(FP)。

纯函数是没有副作⽤的,也不依赖外界环境或者改变外界环境。纯函数的定义是:对于相同的输⼊就有相同的输出,不依赖外部环境,也不改变外部环境(接受相同的输⼊,任何情况下输出都是⼀样的)。符合上⾯描述的函数就可以称为纯函数,不符合就是不纯的,不纯就具有副作⽤的,是可能对外界造成影响的。
例⼦:
// 函数内调⽤外部⽅法

  1. import { isNumber } from 'lodash-es'
  2. export function foo(obj) {
  3. return isNumber(obj) }
  4. // 直接使⽤全局对象
  5. function goto(url) {
  6. location.href = url
  7. }
  8. // 直接修改原型
  9. Array.prototype.hello = () => 'hello'

上⾯⼏种⽅式的代码都是有副作⽤的代码。
在 webpack 中因为不知道代码的内部做了什么,所
以不会被 Tree-Shaking 删除。
如何解决副作⽤:

  1. 代码消除副作⽤
  2. 配置 sideEffects 告诉 webpack 模块是安全的,
    不会带有副作⽤,可以放⼼优化

代码消除副作⽤

// 函数内调⽤外部⽅法
export function foo(isNumber, obj) {
return isNumber(obj) }
// 直接使⽤全局对象
function goto(location, url) {
location.href = url
}
配置 sideEffects
在 package.json 中使⽤ sideEffects 来配置哪些
⽂件中的代码具有副作⽤。从⽽对没有副作⽤的⽂
件代码使⽤ Tree-Shaking 进⾏优化。
// package.json
{
//…
“sideEffects”: [“./src/utils.js”] }
如果项⽬是个类库或者⼯具库,需要发布给其他项⽬使⽤,并且项⽬是使⽤ ES6 Modules 编写的,没有副作⽤,那么可以在该项⽬ package.json 设置sideEffects:false 来告诉使⽤该项⽬的 webpack 可以放⼼的对该项⽬进⾏ Tree-Shaking,⽽不必考虑副作⽤。

4.Tree-Shaking 与开发习惯

  • 要使⽤ Tree-Shaking 必然要保证引⽤的模块都是ES6 规范的,很多⼯具库或者类库都提供了 ES6 语法的库,例如 lodash 的 ES6 版本是lodash-es
  • 需引⼊模块,避免「⼀把梭」,例如我们要使⽤lodash 的 isNumber,可以使⽤ import isNumber from ‘lodash-es/isNumber’,⽽不是 import {isNumber} from ‘lodash-es’
  • 减少代码中的副作⽤代码

另外⼀些组件库,例如 AntDesign 和 ElementUI 这些组件库,本⾝⾃⼰开发了 Babel 的插件,通过插件的⽅式来按需引⼊模块,避免引⼊全部组件。

———————————————————- 旧 ————————————————————

对 tree-shaking 的了解

  • 虽然生产模式下默认开启,但是由于经过 babel 编译全部模块被封装成 IIFE (Immediately Invoked Function Expression:声明即执行的函数表达式)
  • IIFE 存在副作用无法被 tree-shaking 掉
  • 需要配置 { module: false }sideEffects: false
  • Tree-shaking 和传统的 DCE的方法又不太一样,传统的DCE 消灭不可能执行的代码,而Tree-shaking 更关注宇消除没有用到的代码。
  • rollup 和 webpack 的 shaking 程度不同,以一个 Class 为例子

Tree Shaking将没有使用的模块或者是模块中没有用到某个方法摇掉,这样来达到删除无用代码的目的。这个称为DEC。

传统的DEC,编译器可以判断出某些代码根本不影响输出,然后消除这些代码。

tree-shaking是利用代码压缩工具uglify完成了javascript的DCE(dead code elimination)。

  • 使用:
    • webpack 默认支持,在 .babelrc 里设置 modules: false 即可
    • production mode的情况下默认开启
  • 要求:
    • 必须是 ES6 的语法,CJS 的方式不支持
    • 需要tree shaking的模块代码是没有副作用的,否则tree shaking会失效
  • 副作用这个概念来源于函数式编程(FP),纯函数是没有副作用的,也不依赖外界环境或者改变外界环境。纯函数的概念是:接受相同的输入,任何情况下输出都是一样的。
  • 非纯函数存在副作用,副作用就是:相同的输入,输出不一定相同。或者这个函数会影响到外部变量、外部环境。

原理

简单来说一段js代码的执行过程,需要经历以下三个步骤:

  • V8通过源码进行词法分析,语法分析生成AST和执行上下文。
  • 根据AST生成计算机可执行的字节码。
  • 执行生成的字节码。

在JS的执行过程中,ES Module在第一步时就可以确认对应的依赖关系(编译阶段),并不需要执行就可以确认模块的导入、导出。
ES Module在js编译阶段就可以确定模块之间的依赖关系(import)以及模块的导出(export),所以我们并不需要代码执行就可以根据ESM确定模块之间的规则从而实现Tree Shaking,我们称之为静态分析特性
同理,对比commonjs模块,它依赖于代码的执行,需要在第三阶段执行完成代码之后才能确认模块的依赖关系。
自然也就不支持Tree Shaking。
关于ES Module中的动态引入dynamic import,因为它同样是动态需要js执行后才能确认的模块关系。自然也就无法支持Tree Shaking。

利用es6语法import静态导入的特点,有提升的作用,可以通过静态分析,确定模块的依赖关系,然后利用代码压缩工具uglify删除无用代码。

Tree Shaking 依赖ES6 的模块化语法,因为 ES6 模块化语法是静态的,可以进行静态分析。

ES6模块依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析,这就是tree-shaking的基础。

所谓静态分析就是不执行代码,从字面量上对代码进行分析,ES6之前的模块化,比如我们可以动态require一个模块,只有执行后才知道引用的什么模块,这个就不能通过静态分析去做优化。

使用:

实例分析

1.配置@babel/preset-env

@babel/preset-env是存在一个modules的配置参数,它的默认值是auto的时候进行了Tree Shaking。也可以在用 babel-preset-env时需要将它的modules配置为false,也可以开启摇树。

babel首先处理js文件,真正进行tree-shaking识别和记录的是webpack本身。删除多于代码是在uglify中执行的

babel的配置文件中有一个preset配置项:

  1. presets: [
  2. [
  3. '@babel/preset-env',
  4. {
  5. modules: false,
  6. },
  7. ],
  8. [
  9. "@babel/preset-react",
  10. ],
  11. ],

presets里面的env的options中有一个 modules: false,这是指示babel如何去处理import和exports等关键字,默认处理成require形式。如果加上此option,那么babel就不会吧import形式,转变成require形式。为webpack进行tree-shaking创造了条件。

2.关闭 optimization

webpack 在生产模式下才会开启摇树,所以需要把 mode 设置为 production。

由上一节的摇树机制我们得知,我们需要把 webpack 的代码压缩器关闭才能看到 webpack 对代码使用情况的标注,所以需要关闭 webpack 的 optimization。

  1. const path = require('path')
  2. module.exports = {
  3. entry: './src/index.js',
  4. output: {
  5. filename: 'bundle.js',
  6. path: path.resolve(__dirname, 'dist')
  7. },
  8. mode: 'production',
  9. optimization: {
  10. minimize: false,
  11. concatenateModules: false
  12. },
  13. devtool: false
  14. }

util.js

  1. export function usedFunction() {
  2. return 'usedFunction'
  3. }
  4. export function unusedFunction() {
  5. return 'unusedFunction'
  6. }

index.js

  1. import {
  2. usedFunction,
  3. unusedFunction
  4. } from './util'
  5. let result1 = usedFunction()
  6. // let result2 = unusedFunction()
  7. console.log(result1)

打包结果 bundle.js 主要部分(果然看到了 webpack 对代码使用情况额标注)

  1. /************************************************************************/
  2. /******/
  3. ([
  4. /* 0 */
  5. /***/
  6. (function(module, __webpack_exports__, __webpack_require__) {
  7. "use strict";
  8. /* harmony export (binding) */
  9. __webpack_require__.d(__webpack_exports__, "a", function() {
  10. return usedFunction;
  11. });
  12. /* unused harmony export unusedFunction */
  13. function usedFunction() {
  14. return 'usedFunction'
  15. }
  16. function unusedFunction() {
  17. return 'unusedFunction'
  18. }
  19. /***/
  20. }),
  21. /* 1 */
  22. /***/
  23. (function(module, __webpack_exports__, __webpack_require__) {
  24. "use strict";
  25. __webpack_require__.r(__webpack_exports__);
  26. /* harmony import */
  27. var _util__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(0);
  28. let result1 = Object(_util__WEBPACK_IMPORTED_MODULE_0__[ /* usedFunction */ "a"])()
  29. // let result2 = unusedFunction()
  30. console.log(result1)
  31. /***/
  32. })
  33. /******/
  34. ]);

显然:webpack 负责对代码进行标记,把 import & export 标记为 3 类:

  • 被使用过的 export 标记为 **/* harmony export ([type]) */** ,其中 [type] 和 webpack 内部有关,可能是 binding、immutable 等等;
  • 没被使用过的 export 标记为 **/* unused harmony export [FuncName] */** ,其中 [FuncName] 是 export 的方法名称;
  • 所有 import 标记为 **/ harmony import /**

3.开启 optimization

  1. const path = require('path')
  2. module.exports = {
  3. entry: './src/index.js',
  4. output: {
  5. filename: 'bundle.js',
  6. path: path.resolve(__dirname, 'dist')
  7. },
  8. mode: 'production',
  9. optimization: {
  10. minimize: true,
  11. concatenateModules: true
  12. },
  13. devtool: false
  14. }

打包结果

  1. ! function(e) {
  2. var t = {};
  3. function n(r) {
  4. if (t[r]) return t[r].exports;
  5. var o = t[r] = {
  6. i: r,
  7. l: !1,
  8. exports: {}
  9. };
  10. return e[r].call(o.exports, o, o.exports, n), o.l = !0, o.exports
  11. }
  12. n.m = e, n.c = t, n.d = function(e, t, r) {
  13. n.o(e, t) || Object.defineProperty(e, t, {
  14. enumerable: !0,
  15. get: r
  16. })
  17. }, n.r = function(e) {
  18. "undefined" != typeof Symbol && Symbol.toStringTag && Object.defineProperty(e, Symbol.toStringTag, {
  19. value: "Module"
  20. }), Object.defineProperty(e, "__esModule", {
  21. value: !0
  22. })
  23. }, n.t = function(e, t) {
  24. if (1 & t && (e = n(e)), 8 & t) return e;
  25. if (4 & t && "object" == typeof e && e && e.__esModule) return e;
  26. var r = Object.create(null);
  27. if (n.r(r), Object.defineProperty(r, "default", {
  28. enumerable: !0,
  29. value: e
  30. }), 2 & t && "string" != typeof e)
  31. for (var o in e) n.d(r, o, function(t) {
  32. return e[t]
  33. }.bind(null, o));
  34. return r
  35. }, n.n = function(e) {
  36. var t = e && e.__esModule ? function() {
  37. return e.default
  38. } : function() {
  39. return e
  40. };
  41. return n.d(t, "a", t), t
  42. }, n.o = function(e, t) {
  43. return Object.prototype.hasOwnProperty.call(e, t)
  44. }, n.p = "", n(n.s = 0)
  45. }([function(e, t, n) {
  46. "use strict";
  47. n.r(t);
  48. console.log("usedFunction")
  49. }]);

显然,会在代码标注的基础上进行代码精简,把没用的都删除。

实例分析总结

webpack 摇树分两步走:

  1. 标注代码使用情况
  2. 对未使用的代码进行删除

源码分析

代码静态分析,标注代码使用情况

通过搜索 webpack 源码,包含 harmony export 的部分,发现对 used export 和 unused export 的标注具体实现:

lib/dependencies/HarmoneyExportInitFragment.js
  1. class HarmonyExportInitFragment extends InitFragment {
  2. /**
  3. * @param {string} exportsArgument the exports identifier
  4. * @param {Map<string, string>} exportMap mapping from used name to exposed variable name
  5. * @param {Set<string>} unusedExports list of unused export names
  6. */
  7. constructor(
  8. exportsArgument,
  9. exportMap = EMPTY_MAP,
  10. unusedExports = EMPTY_SET
  11. ) {
  12. super(undefined, InitFragment.STAGE_HARMONY_EXPORTS, 1, "harmony-exports");
  13. this.exportsArgument = exportsArgument;
  14. this.exportMap = exportMap;
  15. this.unusedExports = unusedExports;
  16. }
  17. merge(other) {
  18. let exportMap;
  19. if (this.exportMap.size === 0) {
  20. exportMap = other.exportMap;
  21. } else if (other.exportMap.size === 0) {
  22. exportMap = this.exportMap;
  23. } else {
  24. exportMap = new Map(other.exportMap);
  25. for (const [key, value] of this.exportMap) {
  26. if (!exportMap.has(key)) exportMap.set(key, value);
  27. }
  28. }
  29. let unusedExports;
  30. if (this.unusedExports.size === 0) {
  31. unusedExports = other.unusedExports;
  32. } else if (other.unusedExports.size === 0) {
  33. unusedExports = this.unusedExports;
  34. } else {
  35. unusedExports = new Set(other.unusedExports);
  36. for (const value of this.unusedExports) {
  37. unusedExports.add(value);
  38. }
  39. }
  40. return new HarmonyExportInitFragment(
  41. this.exportsArgument,
  42. exportMap,
  43. unusedExports
  44. );
  45. }
  46. /**
  47. * @param {GenerateContext} generateContext context for generate
  48. * @returns {string|Source} the source code that will be included as initialization code
  49. */
  50. getContent({
  51. runtimeTemplate,
  52. runtimeRequirements
  53. }) {
  54. runtimeRequirements.add(RuntimeGlobals.exports);
  55. runtimeRequirements.add(RuntimeGlobals.definePropertyGetters);
  56. const unusedPart =
  57. this.unusedExports.size > 1 ?
  58. `/* unused harmony exports ${joinIterableWithComma(
  59. this.unusedExports
  60. )} */\n` :
  61. this.unusedExports.size > 0 ?
  62. `/* unused harmony export ${
  63. this.unusedExports.values().next().value
  64. } */\n` :
  65. "";
  66. const definitions = [];
  67. for (const [key, value] of this.exportMap) {
  68. definitions.push(
  69. `\n/* harmony export */ ${JSON.stringify(
  70. key
  71. )}: ${runtimeTemplate.returningFunction(value)}`
  72. );
  73. }
  74. const definePart =
  75. this.exportMap.size > 0 ?
  76. `/* harmony export */ ${RuntimeGlobals.definePropertyGetters}(${
  77. this.exportsArgument
  78. }, {${definitions.join(",")}\n/* harmony export */ });\n` :
  79. "";
  80. return `${definePart}${unusedPart}` ;
  81. }
  82. }

harmoney export

getContent 处理 exportMap,对原来的 export 进行 replace

  1. const definePart =
  2. this.exportMap.size > 0 ?
  3. `/* harmony export */ ${RuntimeGlobals.definePropertyGetters}(${
  4. this.exportsArgument
  5. }, {${definitions.join(",")}\n/* harmony export */ });\n` :
  6. "";
  7. return `${definePart}${unusedPart}` ;
  8. }

unused harmoney exports

getContent 处理 unExportMap,对原来的 export 进行 replace

  1. const unusedPart =
  2. this.unusedExports.size > 1 ?
  3. `/* unused harmony exports ${joinIterableWithComma(
  4. this.unusedExports
  5. )} */\n` :
  6. this.unusedExports.size > 0 ?
  7. `/* unused harmony export ${
  8. this.unusedExports.values().next().value
  9. } */\n` :
  10. "";

lib/dependencies/HarmonyExportSpecifierDependency.js

声明 used 和 unused,调用 harmoneyExportInitFragment 进行 replace 掉源码里的 export。

  1. HarmonyExportSpecifierDependency.Template = class HarmonyExportSpecifierDependencyTemplate extends NullDependency.Template {
  2. /**
  3. * @param {Dependency} dependency the dependency for which the template should be applied
  4. * @param {ReplaceSource} source the current replace source which can be modified
  5. * @param {DependencyTemplateContext} templateContext the context object
  6. * @returns {void}
  7. */
  8. apply(
  9. dependency,
  10. source,
  11. { module, moduleGraph, initFragments, runtimeRequirements, runtime }
  12. ) {
  13. const dep = /** @type {HarmonyExportSpecifierDependency} */ (dependency);
  14. const used = moduleGraph
  15. .getExportsInfo(module)
  16. .getUsedName(dep.name, runtime);
  17. if (!used) {
  18. const set = new Set();
  19. set.add(dep.name || "namespace");
  20. initFragments.push(
  21. new HarmonyExportInitFragment(module.exportsArgument, undefined, set)
  22. );
  23. return;
  24. }
  25. const map = new Map();
  26. map.set(used, `/* binding */ ${dep.id}`);
  27. initFragments.push(
  28. new HarmonyExportInitFragment(module.exportsArgument, map, undefined)
  29. );
  30. }
  31. };

lib/dependencies/HarmonyExportSpecifierDependency.js

传入 moduleGraph 获取所有 export 的 name 值

  1. /**
  2. * Returns the exported names
  3. * @param {ModuleGraph} moduleGraph module graph
  4. * @returns {ExportsSpec | undefined} export names
  5. */
  6. getExports(moduleGraph) {
  7. return {
  8. exports: [this.name],
  9. terminalBinding: true,
  10. dependencies: undefined
  11. };
  12. }

moduleGraph (建立 ES6 模块规范的图结构)

lib/ModuleGraph.js (该处代码量过多,不作展示)

  1. class ModuleGraph {
  2. constructor() {
  3. /** @type {Map<Dependency, ModuleGraphDependency>} */
  4. this._dependencyMap = new Map();
  5. /** @type {Map<Module, ModuleGraphModule>} */
  6. this._moduleMap = new Map();
  7. /** @type {Map<Module, Set<ModuleGraphConnection>>} */
  8. this._originMap = new Map();
  9. /** @type {Map<any, Object>} */
  10. this._metaMap = new Map();
  11. // Caching
  12. this._cacheModuleGraphModuleKey1 = undefined;
  13. this._cacheModuleGraphModuleValue1 = undefined;
  14. this._cacheModuleGraphModuleKey2 = undefined;
  15. this._cacheModuleGraphModuleValue2 = undefined;
  16. this._cacheModuleGraphDependencyKey = undefined;
  17. this._cacheModuleGraphDependencyValue = undefined;
  18. }
  19. // ...

在不同的处理阶段调用对应的 ModuleGraph 里面的 function 做代码静态分析,构建 moduleGraph 为 export 和 import 标注等等操作做准备。

Compilation

lib/Compilation.js (部分代码) 在 编译阶段 中将分析所得 的 module 入栈到 ModuleGraph。

  1. /**
  2. * @param {Chunk} chunk target chunk
  3. * @param {RuntimeModule} module runtime module
  4. * @returns {void}
  5. */
  6. addRuntimeModule(chunk, module) {
  7. // Deprecated ModuleGraph association
  8. ModuleGraph.setModuleGraphForModule(module, this.moduleGraph);
  9. // add it to the list
  10. this.modules.add(module);
  11. this._modules.set(module.identifier(), module);
  12. // connect to the chunk graph
  13. this.chunkGraph.connectChunkAndModule(chunk, module);
  14. this.chunkGraph.connectChunkAndRuntimeModule(chunk, module);
  15. // attach runtime module
  16. module.attach(this, chunk);
  17. // Setup internals
  18. const exportsInfo = this.moduleGraph.getExportsInfo(module);
  19. exportsInfo.setHasProvideInfo();
  20. if (typeof chunk.runtime === "string") {
  21. exportsInfo.setUsedForSideEffectsOnly(chunk.runtime);
  22. } else if (chunk.runtime === undefined) {
  23. exportsInfo.setUsedForSideEffectsOnly(undefined);
  24. } else {
  25. for (const runtime of chunk.runtime) {
  26. exportsInfo.setUsedForSideEffectsOnly(runtime);
  27. }
  28. }
  29. this.chunkGraph.addModuleRuntimeRequirements(
  30. module,
  31. chunk.runtime,
  32. new Set([RuntimeGlobals.requireScope])
  33. );
  34. // runtime modules don't need ids
  35. this.chunkGraph.setModuleId(module, "");
  36. // Call hook
  37. this.hooks.runtimeModule.call(module, chunk);
  38. }

总结分析

  1. webpack 在编译阶段将发现的 modules 放入 ModuleGraph
  2. HarmoneyExportSpecifierDependency 和 HarmoneyImportSpecifierDependency 识别 import 和 export 的 module
  3. HarmoneyExportSpecifierDependency 识别 used export 和 unused export
  4. used 和 unused
    1. 把 used export 的 export 替换为 / *harmony export ([type])* /
    2. 把 unused export 的 export 替换为 / *unused harmony export [FuncName]* /

三、splitChunks

触发这个splitChunksPlugin切割代码的有以下几点:

  1. 1.共用的module (node_module文件夹的那些模块)和公共的chunk(两个模块同时引入了a.js
  2. 2.输出的chunk体积大于30kb的(指还没有gzmin喔)
  3. 3.当加载请求数要求最大并行数小于或等于5
  4. 4.初始化页面,加载请求数要求最大并行请求要小于或等于3
  5. ps:想要满足第3和第4点,体积越大的代码块越容易
  1. splitChunks: {
  2. chunks: "async",
  3. minSize: 30000, // 模块的最小体积
  4. minChunks: 3, // 模块的最小被引用次数
  5. maxAsyncRequests: 5, // 按需加载的最大并行请求数
  6. maxInitialRequests: 3, // 一个入口最大并行请求数
  7. automaticNameDelimiter: '~', // 文件名的连接符
  8. name: true,
  9. cacheGroups: { // 缓存组
  10. vendors: {
  11. test: /[\\/]node_modules[\\/]/,
  12. priority: -10
  13. },
  14. default: {
  15. minChunks: 2,
  16. priority: -20,
  17. reuseExistingChunk: true
  18. }
  19. }
  20. }

cacheGroups:缓存组因该是SplitChunksPlugin中最有趣的功能了。在默认设置中,会将 node_mudules 文件夹中的模块打包进一个叫 vendors的bundle中,所有引用超过两次的模块分配到 default bundle 中。更可以通过 priority 来设置优先级。

chunks属性用来选择分割哪些代码块,可选值有:’all’(所有代码块),’async’(按需加载的代码块),’initial’(初始化代码块)。

四、image-webpack-loader图片压缩原理

使用

  • 要求:基于 Node 库的 imagemin 或者 tinypng API
  • 使用:配置 image-webpack-loader ( 一定要先写 ‘file-loader’ 才能使用 ‘image-webpack-loader’
  1. //在你的webpack.config.js中,在file-loader之后使用image-webpack-loader
  2. //对于要配置的每个优化程序,请在选项中指定相应的键
  3. module.exports = {
  4. module:{
  5. rules: [{
  6. test: /\.(gif|png|jpe?g|svg)$/i,
  7. use: [
  8. 'file-loader',
  9. {
  10. loader: 'image-webpack-loader',
  11. options: {
  12. mozjpeg: {
  13. progressive: true,
  14. quality: 65
  15. },
  16. // optipng.enabled: false will disable optipng
  17. optipng: {
  18. enabled: false,
  19. },
  20. pngquant: {
  21. quality: [0.65, 0.90],
  22. speed: 4
  23. },
  24. gifsicle: {
  25. interlaced: false,
  26. },
  27. // the webp option will enable WEBP
  28. webp: {
  29. quality: 75
  30. }
  31. }
  32. },
  33. ],
  34. }]
  35. }
  36. }

原理:

  • pngquant: 是一款PNG压缩器,通过将图像转换为具有alpha通道(通常比24/32位PNG文件小60-80%)的更高效的8位PNG格式,可显著减小文件大小
  • pngcrush:其主要目的是通过尝试不同的压缩级别和PNG过滤方法来降低PNG IDAT数据流的大小。
  • optipng:其设计灵感来自于pngcrush。optipng可将图像文件重新压缩为更小尺寸,而不会丢失任何信息。
  • tinypng:也是将24位png文件转化为更小有索引的8位图片,同时所有非必要的metadata也会被剥离掉

https://jkfhto.github.io/

五、url-loader 中设置 limit 大小来对图片处理,对小于 limit 的图片转化为 base64 格式

为什么要处理成base64格式?

base64格式的图片是文本格式,会随着html、js 、css的下载同时下载到本地,后续不会再向服务器发送http请求。

  1. 优点

(1)base64格式的图片是文本格式,占用内存小,转换后的大小比例大概为1/3,降低了资源服务器的消耗;

(2)网页中使用base64格式的图片时,不用再请求服务器调用图片资源,减少了服务器访问次数。(不是缓存)

  1. 缺点

(1)base64格式的文本内容较多,存储在数据库中增大了数据库服务器的压力;

(2)网页加载图片虽然不用访问服务器了,但因为base64格式的内容太多,所以加载网页的速度会降低,可能会影响用户的体验。

(3)base64无法缓存,要缓存只能缓存包含base64的文件,比如js或者css,这比直接缓存图片要差很多,而且一般HTML改动比较频繁,所以等同于得不到缓存效益。

因为base64的使用缺点,所以一般图片小于10kb的时候,我们才会选择使用base64图片,比如一些表情图片,太大的图片转换成base64得不偿失。当然,极端情况极端考虑。

6、babel-plugin-component模块按需引入

babel-plugin-component,我们可以只引入需要的组件,以达到减小项目体积的目的。

Tag、 Row、Col、Badge、Icon、Tooltip

  1. npm install babel-plugin-component -D
  1. plugins: [
  2. { src: '~/plugins/iview', ssr: true },
  3. ],
  4. babel: { // 按需加载
  5. plugins: [
  6. [
  7. 'component',
  8. {
  9. 'libraryName': 'iview',
  10. // 'styleLibraryName': 'theme-chalk'
  11. "libraryDirectory": "src/components",
  12. }
  13. ]
  14. ]
  15. transpile: [/^iview/]
  16. // loaders: [
  17. // { test: /iview.src.*?js$/, loader: 'babel' },
  18. // { test: /\.js$/, loader: 'babel', exclude: /node_modules/ }
  19. // ]
  20. }

plugins/iview.js

  1. import {
  2. Badge,
  3. Button,
  4. Col,
  5. Header,
  6. Icon,
  7. Input,
  8. Message,
  9. Modal,
  10. Page,
  11. Row,
  12. Tag,
  13. Tooltip,
  14. } from 'iview'
  15. // iview基础模块
  16. const components = {
  17. Badge,
  18. Button,
  19. Col,
  20. Header,
  21. Icon,
  22. Input,
  23. Message,
  24. Modal,
  25. Page,
  26. Row,
  27. Tag,
  28. Tooltip,
  29. }
  30. const iviewModule = {
  31. ...components,
  32. // 不能和html标签重复的组件,添加别名(除了Switch、Circle在使用中必须是iSwitch、iCircle,其他都可以不加"i")
  33. iButton: Button,
  34. iCol: Col,
  35. iHeader: Header,
  36. iInput: Input,
  37. }
  38. // 循环注册全局组件
  39. Object.keys(iviewModule).forEach(key => {
  40. Vue.component(key, iviewModule[key])
  41. })
  42. // 将iview模块挂载到vue对象上去
  43. // Vue.prototype.$Loading = LoadingBar
  44. Vue.prototype.$Message = Message
  45. Vue.prototype.$Modal = Modal
  46. // Vue.prototype.$Notice = Notice
  47. // Vue.prototype.$Spin = Spin

emmmm,体积并没有减小。不知道为什么。

六、cdn引入

浏览器从服务器上下载 CSS、js 和图片等文件时都要和服务器连接,而大部分服务器的带宽有限,如果超过限制,网页就半天反应不过来。而 CDN 可以通过不同的域名来加载文件,从而使下载文件的并发连接数大大增加,且CDN 具有更好的可用性,更低的网络延迟和丢包率 。

cdn的方式引入vue、vuex、axios、element-ui、vue-router等包

在所有使用vue的地方注释掉引入的vue等包,但是Vue.use(axios)、Vue.use(VueRoter)、Vue.use(vuex)等依然要使用,除了Vue.use(ElementUI), 如果加上这句话,还是会打包element-ui到vendor.js文件中

在webpack.base.conf.js 中忽视掉要通过cdn引入的第三方包

  1. //不打包以下文件,在html里面通过cdn的方式引入
  2. config.externals({
  3. 'vue': 'Vue',
  4. 'vue-router': 'VueRouter',
  5. 'Vuex': "Vuex",
  6. // 'store': 'store',
  7. 'axios': 'axios',
  8. 'element-ui': 'ELEMENT'
  9. })

index.html

  1. <script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.11/vue.min.js"></script>
  2. <script src="https://cdn.bootcdn.net/ajax/libs/vue-router/3.1.3/vue-router.min.js"></script>
  3. <script src="https://cdn.bootcdn.net/ajax/libs/vuex/4.0.0-beta.2/vuex.cjs.min.js"></script>
  4. <script src="https://cdn.bootcdn.net/ajax/libs/axios/0.19.2/axios.js"></script>
  5. <script src="https://cdn.bootcss.com/element-ui/2.12.0/index.js"></script>
  6. <script src="https://cdn.bootcss.com/element-ui/2.12.0/locale/zh-CN.js"></script>

具体参考这篇文章 https://www.jb51.net/article/164542.htm

cdn加速的原理

简单来说,CDN 的工作原理就是将您源站的资源缓存到位于全球各地的 CDN 节点上,用户请求资源时,根据就近原则近返回节点上缓存的资源,而不需要每个用户的请求都从您的源站获取,避免网络拥塞、缓解源站压力,保证用户访问资源的速度和体验。

七、代码分割

在以前,为了减少 HTTP 请求,通常我们会把所有的代码都打包成一个单独的 JS 文件。但是如果这个 JS 文件的体积太大的话,就会让整个请求体体积过大,从而降低请求响应的速度,那就得不偿失了。

这时,我们不妨把所有代码分成一块一块,需要某块代码的时候再去加载它;还可以利用浏览器的缓存,下次用到它的时候,直接从缓存中读取。很显然,这种方式可以加快我们网页的加载速度。

所以说,Code Splitting 其实就是把代码分成很多很多块(chunk)。

怎么做

代码切割的主要方式有两种:

  • entry 配置:通过多个 entry ⽂件来实现
  • 动态加载(按需加载):通过写代码时主动使⽤import() 或者 require.ensure 来动态加载
  • 抽取公共代码资源:使⽤SplitChunksPlugin 配置来抽取公共代码、html-webpack-externals-plugin 进⾏基础库分离

之所以把业务代码和第三方代码分离出来,是因为业务的需求是源源不断的,因此业务代码更新频率更高;相反,第三方库更新迭代相对较慢,有时还会锁版本,所以可以充分利用浏览器的缓存来加载这些第三方库。

而按需加载的使用场景,比如说「访问某个路由的时候再去加载相应的组件」,用户不一定一访问所有的路由,所以没必要把所有路由对应的组件都在开始的时候加载完毕。更典型的例子是「某些用户他们的权限只能访问特定的页面」,所以更没必要把他们没有权限的组件加载进来。

1.基础库分离

思路:将 react、React-dom 基础包通过 cdn 引⼊,不打⼊ bundle 中。

  1. // webpack.config.js
  2. module.exports = {
  3. //...
  4. plugins: [
  5. new HtmlWebpackExternalsPlugin({
  6. externals: [
  7. {
  8. module: 'react',
  9. entry: 'https://11.url.cn/now/lib/
  10. 16.2.0/react.min.js',
  11. global: 'React',
  12. },
  13. {
  14. module: 'react-dom',
  15. entry: 'https://11.url.cn/now/lib/
  16. 16.2.0/react-dom.min.js',
  17. global: 'ReactDOM',
  18. }
  19. ]
  20. })
  21. ]
  22. }

2.使⽤ SplitChunksPlugin

webpack 4 内 置 (webpack 4 之前使⽤CommonsChunkPlugin)。
splitChunks 默认配置:

  1. // webpack.config.js
  2. module.exports = {
  3. // ...
  4. optimization: {
  5. splitChunks: {
  6. chunks: 'async', // 三选⼀: 'initial'
  7. | 'all' | 'async' (默认)
  8. minSize: 30000, // 最⼩尺⼨,30K,development 下是10k,越⼤那么单个⽂件越⼤,chunk 数就会变少(针对于提取公共 chunk 的时候,不管再⼤也不会把动态加载的模块合并到初始化模块中)当这个值很⼤的时候就不会做公共部分的抽取了
  9. maxSize: 0, // ⽂件的最⼤尺⼨,0为不限制,优先级:maxInitialRequest/maxAsyncRequests < maxSize < minSize
  10. minChunks: 1, // 默认1,被提取的⼀个模块⾄少需要在⼏个 chunk 中被引⽤,这个值越⼤,抽取出来的⽂件就越⼩
  11. maxAsyncRequests: 5, // 在做⼀次按需加载的时候最多有多少个异步请求,为 1 的时候就不会抽取公共 chunk 了
  12. maxInitialRequests: 3, // 针对⼀个entry 做初始化模块分隔的时候的最⼤⽂件数,优先级⾼于 cacheGroup,所以为 1 的时候就不会抽取 initial common 了
  13. automaticNameDelimiter: '~', // 打包⽂件名分隔符
  14. name: true, // 拆分出来⽂件的名字,默认为true,表示⾃动⽣成⽂件名,如果设置为固定的字符串那么所有的 chunk 都会被合并成⼀个
  15. cacheGroups: {
  16. vendors: {
  17. test: /[\\/]node_modules[\\/]/, // 正则规则,如果符合就提取 chunk
  18. priority: -10, // 缓存组优先级,当⼀个模块可能属于多个 chunkGroup,这⾥是优先级
  19. },
  20. default: {
  21. minChunks: 2,
  22. priority: -20, // 优先级
  23. reuseExistingChunk: true, // 如果该chunk包含的modules都已经另⼀个被分割的chunk中存在,那么直接引⽤已存在的chunk,不会再重新产⽣⼀个
  24. }
  25. }
  26. }
  27. }
  28. }

splitChunks 默认配置对应的就是 chunk ⽣成的第⼆种情况:动态加载(按需加载):通过写代码时主动使⽤ import() 或者 require.ensure 来动态加载。

cacheGroups:缓存组应该是SplitChunksPlugin中最核心的功能了。默认有两个 cacheGroup:vendors 和 default (上⾯默认配置部分已经贴出)。
在默认设置中,会将 node_mudules 文件夹中的模块打包进一个叫 vendors的bundle中,所有引用超过两次的模块分配到 default bundle 中。更可以通过 priority 来设置优先级。

chunks属性用来选择分割哪些代码块,可选值有:’all’(所有代码块),’async’(按需加载的代码块),’initial’(初始化代码块)。

————————————————————旧————————————————————

准备工作(旧)

我用 React 写了一个 demo ,他在页面输出一句Hello world。

接下来,看看第一次打包情况:

webpack - 图3

可以看到,当前只有一个 chunk,也就是 app.js,它是一个 entry chunk。因为我们的 webpack 配置是这样子的:

  1. // webpack.config.js
  2. module.exports = {
  3. entry: {
  4. app: '../src/index.tsx' // entry chunk
  5. }
  6. }

app.js 包含了我们的第三方库 react 和 react-dom,以及我们的业务代码 src。

接下来我们把它们分离开来。

分离 Vendor

最简单的方法就是:增加一个 entry

  1. // webpack.config.js
  2. module.exports = {
  3. entry: {
  4. app: '../src/index.tsx',
  5. vendor: ['react', 'react-dom']
  6. }
  7. }

来分析一下打包:

webpack - 图4

虽然 vendor.js 这个 chunk 包含了我们想要的 react 和 react-dom,但是 app.js 却没有忽略他们。

这是因为,每个 entry 都有自己的依赖,我们想要把 react 和 re-dom 等第三方依赖提取出来,就需要找出它们相同的依赖,就像这样:

webpack - 图5

如果想要提取公共模块的话,就需要用到 optimization.splitChunks。

optimization.splitChunks

现在我们修改 webpack 配置:

  1. module.exports = {
  2. optimization: {
  3. splitChunks: {
  4. chunks: 'all',
  5. cacheGroups: {
  6. vendors: { test: /[\\/]node_modules[\\/]/, priority: -10 },
  7. },
  8. },
  9. },
  10. }

其中 splitChunks 中的配置具体可以参考 webpack 官网 ,我这里采用的配置是所有针对所有模块进行拆分,同时将 node_modules 中的依赖放到 vendors.js 里面,你也可以进行修改,只对异步模块进行拆分。

cacheGroups:缓存组因该是SplitChunksPlugin中最有趣的功能了。在默认设置中,会将 node_mudules 文件夹中的模块打包进一个叫 vendors的bundle中,所有引用超过两次的模块分配到 default bundle 中。更可以通过 priority 来设置优先级。

chunks属性用来选择分割哪些代码块,可选值有:’all’(所有代码块),’async’(按需加载的代码块),’initial’(初始化代码块)。

我们看下打包的结果:

webpack - 图6

我们可以看到,app.js 里面的 react 和 react-dom 已经拆分到了 vendors 中。

Dynamic Import(动态导入)

由于产品经理加了新的需求,我们的 demo 新增了路由。

同时我们的打包:

webpack - 图7

我们新增的 react-router 自动打包到了 vendors 中,但是我们的主包 app.js 却将所有路由文件都打包到一个文件中,这不符合我们的按需加载的想法。

React.lazy()

webpack 可以针对两种语法进行拆分:

  • ESM 的 import()语法
  • webpack.ensure

我们使用 React 官方的 React.lazy ,它是基于 webpack.ensure ,我们修改路由配置:

  1. import React, { FC, lazy } from 'react'
  2. import { Redirect, Route, Switch } from 'react-router'
  3. const Home = lazy(() => import('./home/home'))
  4. const Person = lazy(() => import('./person/person'))
  5. const School = lazy(() => import('./school/school'))
  6. const Root: FC = () => {
  7. return (
  8. <Switch>
  9. <Route path='/' exact render={() => <Redirect to='/home' />} />
  10. <Route path='/home' component={Home} />
  11. <Route path='/person' component={Person} />
  12. <Route path='/school' component={School} />
  13. </Switch>
  14. )
  15. }
  16. export default Root

在修改 webpack 配置文件

  1. module.exports = {
  2. output: {
  3. path: '../dist',
  4. filename: '[name].[chunkhash].bundle.js',
  5. chunkFilename: '[name].[chunkhash].bundle.js',
  6. }
  7. }

为每一个 chunk 添加了 hash,利于以后做缓存。 这里使用了 chunkFilename,它决定非入口 chunk 的名称。

如果你使用了 babel,需要安装 babel-plugin-syntax-dynamic-import 来解析 import() 语法,修改 .babelrc:

  1. {
  2. "plugins": ["syntax-dynamic-import"]
  3. }

看一下打包情况:

webpack - 图8

可以看到,除了主包 app.js 以外,已经额外分离出了三个单独的 chunk,分别对应了我们的三个路由组件。

但是引发了额外的问题,那便是之前在主包已经拆分好的 vendor,在 chunk 中失效了,某一些依赖是多个 chunk 公用的,这时候这些依赖理应在 vendor.js 中,而不应该是每一个 chunk 都有自己的依赖。

但其实问题不大,原因在于 webpack 在抽取公用模块的时候,会对被抽取的模块大小进行判断,模式最小被抽取的大小是 30kb,当然我们修改已达到最小细粒度的复用,这完全靠调用方自己把控。

这里我们把最小大小修改为 0,即所有模块都会被抽取,我们看一下打包后的样子:

webpack - 图9

分离业务公共模块

不单只是第三方依赖,通常我们在写业务代码的时候,也会抽离一些代码放到公共模块中。

细心的读者应该可以看到上图 3,4,5 chunk 里面都包含了 Button,如果类似的公共组件一多起来,就会产生很多重复的代码,所以我们也应该将这些重复代码打包到一个公共的模块里面去。

实现方式和上面一致:

  1. module.exports = {
  2. optimization: {
  3. splitChunks: {
  4. chunks: 'all',
  5. cacheGroups: {
  6. vendors: {
  7. test: /[\\/]node_modules[\\/]/,
  8. priority: 20,
  9. minSize: 0,
  10. },
  11. default: { minChunks: 2, priority: 10, reuseExistingChunk: true },
  12. },
  13. },
  14. },
  15. }

这样,当 webpack 打包的时候,在所有异步 chunk 中引入次数大于等于 2 的模块,webpack 就会把它打包到 default.js chunk 中。(由于 demo 中我们的公用组件大小太小,所以我对公用 chunk 大小修改 0 以方便观察)。

最后我们打包的结果是:

webpack - 图10

Perfect,这就是我们想要的效果。

ps:由于有一个 chunk 太小导致图中没有显示出来,实际上图中一共有 6 个子 chunk。

总结

你的 Code Splitting = webpack bundle analyzer + optimization.splitChunks + 你的分析

我们做代码切割的目的,就是为了充分利用浏览器的缓存,以及首屏的极限优化达到按需加载的效果。

八、优化打包体积

1. 开启gzip压缩

  1. //webpack.prod.js
  2. const CompressionPlugin = require("compression-webpack-plugin");
  3. module.exports = {
  4. plugins: [new CompressionPlugin()],
  5. };

2.css-minimizer-webpack-plugin

优化和压缩CSS

  1. //webpack.prod.js
  2. const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
  3. plugins:[
  4. new CssMinimizerPlugin(),
  5. ]

3. externals

防止将外部资源包打包到自己的bundle中
示例:从cdn引入jQuery,而不是把它打包
(1)index.html

  1. <script src="https://code.jquery.com/jquery-3.1.0.js" integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk=" crossorigin="anonymous" ></script>
  1. module.exports = {
  2. //...
  3. externals: {
  4. jquery: 'jQuery',
  5. },
  6. };

(3)这样就剥离了那些不需要改动的依赖模块

  1. import $ from 'jquery';

九、缓存

当把打包后dist目录部署到server上,浏览器就能够访问此server的网站及其资源。而获取资源是比较耗费时间的,这就是为什么浏览器使用一种名为缓存的技术。可以通过命中缓存,以降低网络流量,使网站加载速度更快,然后,如果我们在部署新版本时不更改资源的文件名,浏览器可能会认为它没有被更新,就会使用它的缓存版本。
所以我们需要将变动后的资源文件更改文件名,没有变动的资源(node_modules中的第三方文件)不更改包名称

十、优化构建速度

https://juejin.cn/post/7023242274876162084#heading-23

1. contenthash

[contenthash]将根据资源内容创建出唯一hash。当资源内容发生变化时,[contenthash]也会发生变化。

  1. //webpack.common.js
  2. output: {
  3. path: path.resolve(__dirname, "../dist"),
  4. filename: "[name].[contenthash:8].js",
  5. clean: true, //每次构建清除dist包
  6. }

十、babel 原理

https://segmentfault.com/a/1190000017879365?utm_source=sf-related

babel 的编译过程分为三个阶段:parsingtransforminggenerating,以 ES6 编译为 ES5 作为例子:

  1. ES6 代码输入;
  2. babylon 进行解析得到 AST;
  3. 用 babel-traverse 对 AST树进行遍历编译,完成对其的替换,删除或者增加节点,这个方法的参数为原始AST和自定义的转换规则,返回结果为转换后的AST。
  4. 用 babel-generator 通过 AST树生成 ES5 代码。
  5. Babel Types模块是一个用于 AST 节点的 Lodash 式工具库(译注:Lodash 是一个 JavaScript 函数工具库,提供了基于函数式编程风格的众多工具函数), 它包含了构造、验证以及变换 AST 节点的方法。 该工具库包含考虑周到的工具方法,对编写处理AST逻辑非常有用。

九、exports 与 module.exports 的区别

exports是module.exports的一个引用:

  1. console.log(exports === module.exports); //true
  2. exports.foo = 'bar';
  3. //等价于
  4. module.exports.foo = 'bar';
  5. 当给exports重新赋值后,exports!= module.exports.
  6. 最终return的是module.exports,无论exports中的成员是什么都没用。
  7. 真正去使用的时候:
  8. 导出单个成员:exports.xxx = xxx;
  9. 导出多个成员:module.exports 或者 modeule.exports = {};

十、es6模块与commonjs的区别

CommonJs输出的是一个值的拷贝,ES6 Module通过export {<变量>}输出的是一个变量的引用,export default输出的是一个值。

CommonJs运行在服务器上,被设计为运行时加载,即代码执行到那一行才回去加载模块,而ES6 Module是静态的输出一个接口,发生在编译的阶段。

CommonJs在第一次加载的时候运行一次并且会生成一个缓存,之后加载返回的都是缓存中的内容。

CommonJS

  1. 对于基本数据类型,属于复制。即会被模块缓存。同时,在另一个模块可以对该模块输出的变量重新赋值。
  2. 对于复杂数据类型,属于浅拷贝。由于两个模块引用的对象指向同一个内存空间,因此对该模块的值做修改时会影响另一个模块。
  3. 当使用require命令加载某个模块时,就会运行整个模块的代码。
  4. 当使用require命令加载同一个模块时,不会再执行该模块,而是取到缓存之中的值。也就是说,CommonJS模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存。
  5. 循环加载时,属于加载时执行。即脚本代码在require的时候,就会全部执行。一旦出现某个模块被”循环加载”,就只输出已经执行的部分,还未执行的部分不会输出。

ES6模块

  1. ES6模块中的值属于【动态只读引用】。
  2. 对于只读来说,即不允许修改引入变量的值,import的变量是只读的,不论是基本数据类型还是复杂数据类型。当模块遇到import命令时,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。
  3. 对于动态来说,原始值发生变化,import加载的值也会发生变化。不论是基本数据类型还是复杂数据类型。
  4. 循环加载时,ES6模块是动态引用。只要两个模块之间存在某个引用,代码就能够执行。

上面说了一些重要区别。现在举一些例子来说明每一点吧

CommonJS

  1. 对于基本数据类型,属于复制。即会被模块缓存。同时,在另一个模块可以对该模块输出的变量重新赋值。
  1. // b.js
  2. let count = 1
  3. let plusCount = () => {
  4. count++
  5. }
  6. setTimeout(() => {
  7. console.log('b.js-1', count)
  8. }, 1000)
  9. module.exports = {
  10. count,
  11. plusCount
  12. }
  13. // a.js
  14. let mod = require('./b.js')
  15. console.log('a.js-1', mod.count)
  16. mod.plusCount()
  17. console.log('a.js-2', mod.count)
  18. setTimeout(() => {
  19. mod.count = 3
  20. console.log('a.js-3', mod.count)
  21. }, 2000)
  22. node a.js
  23. a.js-1 1
  24. a.js-2 1
  25. b.js-1 2 // 1秒后
  26. a.js-3 3 // 2秒后

以上代码可以看出,b模块export的count变量,是一个复制行为。在plusCount方法调用之后,a模块中的count不受影响。同时,可以在b模块中更改a模块中的值。如果希望能够同步代码,可以export出去一个getter。

  1. // 其他代码相同
  2. module.exports = {
  3. get count () {
  4. return count
  5. },
  6. plusCount
  7. }
  8. node a.js
  9. a.js-1 1
  10. a.js-2 1
  11. b.js-1 2 // 1秒后
  12. a.js-3 2 // 2秒后, 由于没有定义setter,因此无法对值进行设置。所以还是返回2
  1. 对于复杂数据类型,属于浅拷贝。由于两个模块引用的对象指向同一个内存空间,因此对该模块的值做修改时会影响另一个模块。
  1. // b.js
  2. let obj = {
  3. count: 1
  4. }
  5. let plusCount = () => {
  6. obj.count++
  7. }
  8. setTimeout(() => {
  9. console.log('b.js-1', obj.count)
  10. }, 1000)
  11. setTimeout(() => {
  12. console.log('b.js-2', obj.count)
  13. }, 3000)
  14. module.exports = {
  15. obj,
  16. plusCount
  17. }
  18. // a.js
  19. var mod = require('./b.js')
  20. console.log('a.js-1', mod.obj.count)
  21. mod.plusCount()
  22. console.log('a.js-2', mod.obj.count)
  23. setTimeout(() => {
  24. mod.obj.count = 3
  25. console.log('a.js-3', mod.obj.count)
  26. }, 2000)
  27. node a.js
  28. a.js-1 1
  29. a.js-2 2
  30. b.js-1 2
  31. a.js-3 3
  32. b.js-2 3

以上代码可以看出,对于对象来说属于浅拷贝。当执行a模块时,首先打印obj.count的值为1,然后通过plusCount方法,再次打印时为2。接着在a模块修改count的值为3,此时在b模块的值也为3。

3.当使用require命令加载某个模块时,就会运行整个模块的代码。

4.当使用require命令加载同一个模块时,不会再执行该模块,而是取到缓存之中的值。也就是说,CommonJS模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存。

5.循环加载时,属于加载时执行。即脚本代码在require的时候,就会全部执行。一旦出现某个模块被”循环加载”,就只输出已经执行的部分,还未执行的部分不会输出。

  1. 3, 4, 5可以使用同一个例子说明
  2. // b.js
  3. exports.done = false
  4. let a = require('./a.js')
  5. console.log('b.js-1', a.done)
  6. exports.done = true
  7. console.log('b.js-2', '执行完毕')
  8. // a.js
  9. exports.done = false
  10. let b = require('./b.js')
  11. console.log('a.js-1', b.done)
  12. exports.done = true
  13. console.log('a.js-2', '执行完毕')
  14. // c.js
  15. let a = require('./a.js')
  16. let b = require('./b.js')
  17. console.log('c.js-1', '执行完毕', a.done, b.done)
  18. node c.js
  19. b.js-1 false
  20. b.js-2 执行完毕
  21. a.js-1 true
  22. a.js-2 执行完毕
  23. c.js-1 执行完毕 true true

仔细说明一下整个过程。

  1. 在Node.js中执行c模块。此时遇到require关键字,执行a.js中所有代码。
  2. 在a模块中exports之后,通过require引入了b模块,执行b模块的代码。
  3. 在b模块中exports之后,又require引入了a模块,此时执行a模块的代码。
  4. a模块只执行exports.done = false这条语句。
  5. 回到b模块,打印b.js-1, exports, b.js-2。b模块执行完毕。
  6. 回到a模块,接着打印a.js-1, exports, a.js-2。a模块执行完毕
  7. 回到c模块,接着执行require,需要引入b模块。由于在a模块中已经引入过了,所以直接就可以输出值了。
  8. 结束。

从以上结果和分析过程可以看出,当遇到require命令时,会执行对应的模块代码。当循环引用时,有可能只输出某模块代码的一部分。当引用同一个模块时,不会再次加载,而是获取缓存。

ES6模块

  1. es6模块中的值属于【动态只读引用】。只说明一下复杂数据类型。
  2. 对于只读来说,即不允许修改引入变量的值,import的变量是只读的,不论是基本数据类型还是复杂数据类型。当模块遇到import命令时,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。
  3. 对于动态来说,原始值发生变化,import加载的值也会发生变化。不论是基本数据类型还是复杂数据类型。
  1. // b.js
  2. export let counter = {
  3. count: 1
  4. }
  5. setTimeout(() => {
  6. console.log('b.js-1', counter.count)
  7. }, 1000)
  8. // a.js
  9. import { counter } from './b.js'
  10. counter = {}
  11. console.log('a.js-1', counter)
  12. // Syntax Error: "counter" is read-only

虽然不能将counter重新赋值一个新的对象,但是可以给对象添加属性和方法。此时不会报错。这种行为类型与关键字const的用法。

  1. // a.js
  2. import { counter } from './b.js'
  3. counter.count++
  4. console.log(counter)
  5. // 2
  1. 循环加载时,ES6模块是动态引用。只要两个模块之间存在某个引用,代码就能够执行。
  1. // b.js
  2. import {foo} from './a.js';
  3. export function bar() {
  4. console.log('bar');
  5. if (Math.random() > 0.5) {
  6. foo();
  7. }
  8. }
  9. // a.js
  10. import {bar} from './b.js';
  11. export function foo() {
  12. console.log('foo');
  13. bar();
  14. console.log('执行完毕');
  15. }
  16. foo();
  17. babel-node a.js
  18. foo
  19. bar
  20. 执行完毕
  21. // 执行结果也有可能是
  22. foo
  23. bar
  24. foo
  25. bar
  26. 执行完毕
  27. 执行完毕

由于在两个模块之间都存在引用。因此能够正常执行。

十一、loader

Webpack 开箱即⽤只⽀持 JS 和 JSON 两种⽂件类型,通过 Loaders 去⽀持其它⽂件类型并且把它们
转化成有效的模块,并且可以添加到依赖图中。
本⾝是⼀个函数,接受源⽂件作为参数,返回转换
的结果。

多 loader 时的执⾏顺序

• 多个 loader 串⾏执⾏
• 顺序从后到前

常⽤ Loader

• babel-loader:转換 ES6、ES7 等 JS 新特性语法
• css-loader:⽀持 CSS ⽂件的加载和解析• less-loader:将 less ⽂件转换成 css
• ts-loader:将 TS 转换成 JS
• file-loader:进⾏图⽚、字体等的打包
• raw-loader:将⽂件以字符串的形式导⼊
• thread-loader:多进程打包 JS 和 CSS

Loader 的⽤法

  1. module.exports = {
  2. //...
  3. module: {
  4. rules: [
  5. {
  6. // test 指定匹配规则
  7. test: /\.txt$/,
  8. // use 指定使⽤的 loader 名称
  9. use: 'raw-loader'
  10. }
  11. ]
  12. }
  13. }

十二、plugin

plugin插件⽤于 bundle ⽂件的优化,资源管理和环境变量注⼊,增强 webpack 的能⼒,作⽤于整个构建过程

常⽤ Plugin

• CommonsChunkPlugin:将 chunks 相同的模块代码提取成公共 js
• CleanWebpackPlugin:清理构建⽬录
• ExtractTextWebpackPlugin:将 CSS 从 bundle ⽂件⾥提取成⼀个独⽴的 CSS ⽂件
• CopyWebpackPlugin:将⽂件或者⽂件夹拷贝到构建的输出⽬
• HtmlWebpackPlugin:创建 html ⽂件去承载输出的 bundle
• UglifyjsWebpackPlugin:压缩 JS
• ZipWebpackPlugin:将打包出的资源⽣成⼀个 zip 包

Plugin 的⽤法

  1. module.exports = {
  2. //...
  3. // 放到 plugins 数组⾥
  4. plugins: [
  5. new HtmlWebpackPlugin({
  6. template: path.join(__dirname, 'src/
  7. index.html')
  8. })
  9. ]
  10. }

十三、devServer

// webpack.config.js

  1. module.exports = {
  2. //...
  3. devServer: {
  4. proxy: {
  5. '/api': {
  6. target: 'https://
  7. server.example.com',
  8. changeOrigin: true,
  9. pathRewrite: {
  10. '/api': ''
  11. }
  12. }
  13. }
  14. }
  15. }

十四、devtool配置 source map

常⽤配置
开发环境配置:source-map
线上环境配置:cheap-moudle-source-map
production环境就把 source-map 添加到 Error Reporting Tool(e.g. Sentry) 上。这样既不直接暴露源代码,也能⽅便解决 production 环境遇到的bug。

十五、Webpack 中的⽂件监听

⽂件监听是在发现源码发⽣变化时,⾃动重新构建出新的输出
⽂件 webpack 开启监听模式,有两种⽅式:
• webpack watch 模式。启动 webpack 命令时,带
上—watch参数
缺陷:每次需要⼿动刷新浏览器。
// package.json

  1. {
  2. "name": "my-project",
  3. "version": "1.0.0",
  4. "description": "",
  5. "main": "index.js",
  6. "scripts": {
  7. "build": "webpack",
  8. "watch": "webpack --watch"
  9. },
  10. "keywords": [],
  11. "author": "",
  12. "license": "ISC",
  13. "devDependencies": {}
  14. }

• 在配置 webpack. config.js 中设置 watch: true
原理:轮询判断⽂件的最后编辑时问是否变化。某个⽂件发⽣了变化,并不会⽴刻告诉监听者,⽽是先缓存起来,等 aggregatetimeout。

  1. // webpack.config.js
  2. module.exports = {
  3. //...
  4. // 默认为 false,也就是不开启
  5. watch: true,
  6. // 只有开启监听模式时,watchOptions 才能⽣效
  7. watchOptions: {
  8. // 不监听的⽂件或⽂件夹,⽀持正则表达式,默认为
  9. ignored: /node_modules/,
  10. // 监听到变化后会等 300ms 再去执⾏,默认
  11. 300ms
  12. aggregateTimeout: 300,
  13. // 判断⽂件是否发⽣变化是通过不停询问系统指定⽂
  14. 件有没有变化实现的,默认每秒问 1000
  15. poll: 1000
  16. }
  17. }

十六、Webpack HMR 以及其原理

  1. webpack-dev-server
  2. webpack-dev-middleware


webpack-dev-server = Express + webpack-devmiddleware(其实是 Express 的⼀个中间件)


使⽤ webpack-dev-server


webpack-dev-server 提供了⼀个简单的Web服务器以及使⽤HRM的功能

  1. // package.json
  2. // 添加 webpack-dev-server
  3. {
  4. "name": "my-project",
  5. "version": "1.0.0",
  6. "description": "",
  7. "main": "index.js",
  8. "scripts": {
  9. "build": "webpack",
  10. "watch": "webpack --watch",
  11. "dev": "webpack-dev-server --config webpack.dev.js --open"
  12. },
  13. "keywords": [],
  14. "author": "",
  15. "license": "ISC",
  16. "devDependencies": {}
  17. }
  1. // webpack.config.js
  2. module.exports = {
  3. //...
  4. devServer: {
  5. // 告诉 webpack-dev-server 要从dist⽬录中的⽂件提供⽂件 localhost:8080
  6. contentBase: './dist',
  7. // 启⽤ webpack HMR
  8. hot: true
  9. }
  10. }

配置了 devServer.hot: true webpack 会⾃动将webpack.HotModuleReplacementPlugin 插件添加。

使⽤ webpack-dev-middleware

webpack-dev-middleware是⼀个包装程序,它将通过webpack处理的⽂件发送到服务器。它在webpack-dev-server内部使⽤,但是如果需要,它可以作为单独的软件包使⽤,以允许进⾏更多⾃定义设置。

  1. // webpack.config.js
  2. module.exports = {
  3. //...
  4. output: {
  5. path: path.join(__dirname, 'dist'),
  6. filename: '[name]_[chunkhash:8].js',
  7. // 配置路径
  8. publicPath: '/',
  9. },
  10. }
  1. // server.js
  2. const express = require('express')
  3. const webpack = require('webpack')
  4. const webpackDevMiddleware =
  5. require('webpack-dev-middleware')
  6. const app = express()
  7. const config = require('./
  8. webpack.config.js')
  9. const compiler = webpack(config)
  10. app.use(webpackDevMiddleware(compiler, {
  11. publicPath: config.output.publicPath
  12. }))
  13. app.listen(3000, function () {
  14. console.log('Example app listening on port
  15. 3000!\n')
  16. })
  1. // package.json
  2. // 添加 node server.js 脚本命令
  3. {
  4. "name": "my-project",
  5. "version": "1.0.0",
  6. "description": "",
  7. "main": "index.js",
  8. "scripts": {
  9. "build": "webpack",
  10. "watch": "webpack --watch",
  11. "dev": "webpack-dev-server --config webpack.dev.js --open",
  12. "server": "node server.js"
  13. },
  14. "keywords": [],
  15. "author": "",
  16. "license": "ISC",
  17. "devDependencies": {}
  18. }

HMR 原理

HMR(模块热替换) 和 Live Reload(热重载)的区别:
• 热重载不能够保存状态(states)。当页⾯刷新之后,之前的状态全都丢失了,例⼦:点击按钮出现弹窗,当刷新之后,弹窗随即消失,要恢复之前状态,还需要再次点击按钮。
• 热重载会刷新浏览器。HMR 不会刷新浏览器,⽽是运⾏时对模块进⾏热替换,保证了状态不会丢失,提升了开发效率。
• HMR 是将⽂件保存在内存中,不是写⼊磁盘。
image.png
热更新最核⼼的是: HMR Server 和 HMR Runtime。
• HMR Server:服务端,⽤来将变化的 JS 模块通过websocket 通过给浏览器。
• HMR Runtime:浏览器,⽤于接受 HMR Server 传递的模块数据,浏览器端可以看到 .hotupdate.josn ⽂件返回过来。
第⼀步:webpack 构建出来的 bundle.js 本⾝不具备热更新的能⼒,HotModuleReplacementPlugin 的作⽤是修改 entry 属性,将 HMR runtime 注⼊到 bundle.js 中,使得 bundle.js 可以和 HMR Server 建 ⽴ websocket 通信连接。
第⼆步:webpack-dev-middleware 调⽤ webpack 暴露的 API 对⽂件系统 watch,当⽂件系统中某⼀个⽂件发⽣了修改,webpack 根据配置⽂件对模块进⾏重新编译打包,然后保存到内存中。
原来 webpack 将 bundle.js 文件打包到了内存中,不生成文件的原因就在于访问内存中的代码比访问文件系统中的文件更快,而且 也减少了代码写入文件的开销,这一切都归功于memory-fs,memory-fs 是 webpack-dev-middleware 的一个依赖库,webpack-dev-middleware 将 webpack 原本的 outputFileSystem 替换成了MemoryFileSystem 实例,这样代码就将输出到内存中。

第三步:webpack-dev-server 对静态⽂件的监听。当配置了 devServer.contentBase 的时候,Server 会监听这些配置⽂件中静态⽂件的变化,变化后会通知浏览器进⾏ Live Reload (热重载)。
第四步:webpack-dev-server 通过 SockJS 在浏览器和服务端之间建⽴⼀个 websocket 长连接,将webpack编译打包的各阶段状态信息告诉浏览器端(包括第三步中 Server 对静态⽂件变化的信息)。传递的信息是新模块的Hash 值。浏览器根据信息进⾏不同的操作,是刷新浏览器还是进⾏ HMR。
第五步:webpack-dev-server/client 端并不能够请求更新的代码,也不会执⾏热更模块操作,⽽把这些⼯作又交回给了 webpack。webpack/hot/dev-server 监听到 webpack-dev-server/client 传递过来的信息,调⽤ HMR runtime 的 check ⽅法,检测是否有更新。check 过程中会先调⽤ AJAX 向服务端请求是否有更新⽂件,如果有更新的⽂件列表(.hotupdate.josn)返回浏览器,则通过 JSONP 请求最新的模块代码,然后将代码(.hotupdate.js)返回给 HMR runtime,HMR runtime 会根据返回的新模块代码做进⼀步处理,可能是刷新页⾯,也可能是进⾏ HMR。
第六步:HMR runtime 对模块进⾏ HMR。HMR 只要分三个阶段,第⼀个阶段:找出旧的模块以及旧模块的依赖;第⼆个阶段:从内存中删除这些旧的模块以及旧的依赖;第三个阶段:将新的模块添加到 modules 中同时更新新模块的依赖,当下次调⽤ webpack_require(webpack 重写的 require ⽅法)⽅法的时候,就是获取到了新的模块代码了。如果在 HMR 过程中出现错误,HMR 则回退到Live Reload (热重载)。
[

](https://zhuanlan.zhihu.com/p/30669007)

总结:

https://zhuanlan.zhihu.com/p/30669007
第一步:webpack 对文件系统进行 watch 打包到内存中
webpack 将 bundle.js 文件打包到了内存中,不生成文件的原因就在于访问内存中的代码比访问文件系统中的文件更快,而且也减少了代码写入文件的开销
第二步:devServer 通知浏览器端文件发生改变

在这一阶段,sockjs 是服务端和浏览器端之间的桥梁,在启动 devServer 的时候,sockjs 在服务端和浏览器端建立了一个 webSocket 长连接,webpack-dev-server 调用 webpack api 监听 compile的 done 事件,当compile 完成后,webpack-dev-server通过 _sendStatus 方法将编译打包后的新模块 hash 值发送到浏览器端。

第三步:webpack-dev-server/client 接收到服务端消息做出响应
之所以可以通信是因为:HotModuleReplacementPlugin 修改 entry 属性,将 HMR runtime 注⼊到 bundle.js 中,使得 bundle.js 可以和 HMR Server 建 ⽴ websocket 通信连接。
webpack-dev-server/client 当接收到 type 为 hash 消息后会将 hash 值暂存起来,当接收到 type 为 ok 的消息后对应用执行 reload 操作.
如果配置了模块热更新,就调用 webpack/hot/emitter 将最新 hash 值发送给 webpack,然后将控制权交给 webpack 客户端代码。如果没有配置模块热更新,就直接调用 location.reload 方法刷新页面。
第四步:webpack 接收到最新 hash 值验证并请求模块代码
两个方法 hotDownloadUpdateChunk 和 hotDownloadManifest , 第二个方法是调用 AJAX 向服务端请求是否有更新的文件,如果有将发更新的文件列表返回浏览器端,而第一个方法是通过 jsonp 请求最新的模块代码,然后将代码返回给 HMR runtime,HMR runtime 会根据返回的新模块代码做进一步处理,可能是刷新页面,也可能是对模块进行热更新。
第五步:HotModuleReplacement.runtime 对模块进行热更新
分三个阶段,第⼀个阶段:找出旧的模块以及旧模块的依赖;第⼆个阶段:从内存中删除这些旧的模块以及旧的依赖;第三个阶段:将新的模块添加到 modules 中同时更新新模块的依赖,当下次调⽤ webpack_require(webpack 重写的 require ⽅法)⽅法的时候,就是获取到了新的模块代码了。如果在 HMR 过程中出现错误,HMR 则回退到Live Reload (热重载)。

001.谈谈你对webpack的看法:

webpack是一个模块打包工具,可以使用它管理项目中的模块依赖,并编译输出模块所需的静态文件。它可以很好地管理、打包开发中所用到的HTML,CSS,JavaScript和静态文件(图片,字体)等,让开发更高效。对于不同类型的依赖,webpack有对应的模块加载器,而且会分析模块间的依赖关系,最后合并生成优化的静态资源。

002.webpack的基本功能和工作原理?

代码转换:TypeScript 编译成 JavaScript、SCSS 编译成 CSS 等等
文件优化:压缩 JavaScript、CSS、HTML 代码,压缩合并图片等
代码分割:提取多个页面的公共代码、提取首屏不需要执行部分的代码让其异步加载
模块合并:在采用模块化的项目有很多模块和文件,需要构建功能把模块分类合并成一个文件
自动刷新:监听本地源代码的变化,自动构建,刷新浏览器
代码校验:在代码被提交到仓库前需要检测代码是否符合规范,以及单元测试是否通过
自动发布:更新完代码后,自动构建出线上发布代码并传输给发布系统。

003.webpack构建过程:

从entry里配置的module开始递归解析entry依赖的所有module, 每找到一个module,就会根据配置的loader去找对应的转换规则。

对module进行转换后,再解析出当前module依赖的module 这些模块会以entry为单位分组,一个entry和其所有依赖的module被分到一个组Chunk。

最后webpack会把所有Chunk转换成文件输出 在整个流程中webpack会在恰当的时机执行plugin里定义的逻辑

004.webpack打包原理:

将所有依赖打包成一个bundle.js,通过代码分割成单元片段按需加载。

005.什么是entry,output?

entry 入口,告诉webpack要使用哪个模块作为构建项目的起点,默认为./src/index.js

output 出口,告诉webpack在哪里输出它打包好的代码以及如何命名,默认为./dist

006.什么是loader,plugins?

  • loader:由于 webpack 只能识别 js,loader 相当于翻译官的角色,帮助 webpack 对其他类型的资源进行转译的预处理工作;
  • plugins:plugins 拓展了 webpack 的功能,由于 webpack 运行时会广播很多事件,plugin 可以监听这些事件,然后通过 webpack 提供的 API 来改变输出结果。

007.什么是bundle,chunk,module?

bundle是webpack打包出来的文件,chunk是webpack在进行模块的依赖分析的时候,代码分割出来的代码块。module是开发中的单个模块

008.npm打包时需要注意哪些?如何利用webpack来更好的构建?

完善基本信息;
定义依赖;
忽略文件;
打标签;

009.有哪些常见的Loader?他们是解决什么问题的?

file-loader:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件;

url-loader:和 file-loader 类似,但是能在文件很小的情况下以 base64 的方式把文件内容注入到代码中去;

source-map-loader:加载额外的 Source Map 文件,以方便断点调试;

image-loader:加载并且压缩图片文件;

babel-loader:把 ES6 转换成 ES5;

css-loader:加载 CSS,支持模块化、压缩、文件导入等特性;

style-loader:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSS;

eslint-loader:通过 ESLint 检查 JavaScript 代码;

010.webpack规范:

webpack默认遵循commonjs规范 module.exports

使用webpack进行打包时有两种模式:
开发模式:主要是用于测试,代码调试等;
生产模式:要考虑性能问题,要压缩 如果没有插件 就不会压缩;

默认情况下webpack的配置文件叫webpack.config.js,可以通过—config指定webpack的配置文件名

011.配置流程:

你可以尝试配置脚手架吗?可以

012.loader:

css需要两个loader来处理:css-loader style-loader

postcss-loader 他提供了一种方式用 JavaScript 代码来处理 CSS。它负责把 CSS 代码解析成抽象语法树结构(Abstract Syntax Tree,AST),再交由插件来进行处理。

-webkit-transform: rotate(45deg); transform: rotate(45deg);

mini-css-extract-plugin 以前都是之间引入内部样式,把css专门打包成一个css文件,在index.html文件中引入css

optimize-css-assets-webpack-plugin css压缩
terser-webpack-plugin css压缩 js不能压缩了,然后有一个插件,能压缩js
file-loader 是让webpack打包图片
url-loader可以让图片转化base64,也可以让webpack打包图片

webpack 默认情况下不支持js的高级语法,所以需要使用babel;
babel转化; npm i @babel/core @babel/preset-env babel-loader —save-dev

013.plugins:

html-webpack-plugin 根据模块生成一个html文件 此时不会在dist文件夹下面新建index文件了

我需要在public新建 index文件
根据这个模板文件 在内存中生成 index.html 然后自动引入bundle.js

clean-webpack-plugin 去掉没有用到的模块

014.loader与plugin的区别?

  • loader:由于 webpack 只能识别 js,loader 相当于翻译官的角色,帮助 webpack 对其他类型的资源进行转译的预处理工作;
  • plugins:plugins 拓展了 webpack 的功能,由于 webpack 运行时会广播很多事件,plugin 可以监听这些事件,然后通过 webpack 提供的 API 来改变输出结果。

    015.如何提高webpack打包速度

    scoup作用域范围
    dll
    多线程

    webpack 热更新原理

  • webpack 通过 memoryfs 把产物保存在内存里

  • 建立 socket,当代码更新的时候,通过ajax发送hash值的方式,通知前端用 jsonp 拉新的代码

https://zhuanlan.zhihu.com/p/30669007
https://github.com/careteenL/webpack-hmr
使用sockjs在浏览器端和服务端之间建立一个 websocket 长连接
打包后的文件并不在 output.path 目录下,而是打包到了内存中,不生成文件的原因就在于访问内存中的代码比访问文件系统中的文件更快,而且也减少了代码写入文件的开销,这一切都归功于memory-fs
先使用ajax请求Manifest即服务器这一次编译相对于上一次编译改变了哪些module和chunk。对比变化,如果有变化再通过jsonp获取这些已改动的module和chunk的代码。

webpack动态加载原理

webpack根据ES2015 loader 规范实现了用于动态加载的import()方法。
这个功能可以实现按需加载我们的代码,并且使用了promise式的回调,获取加载的包。

在代码中所有被import()的模块,都将打成一个单独的包,放在chunk存储的目录下。在浏览器运行到这一行代码时,就会自动请求这个资源,实现异步加载。

这里是一个简单的demo。

  1. import('lodash').then(_ => {
  2. // Do something with lodash (a.k.a '_')...
  3. })

可以看到,import()的语法十分简单。该函数只接受一个参数,就是引用包的地址,这个地址与es6的import以及CommonJS的require语法用到的地址完全一致。可以实现无缝切换【写个正则替换美滋滋】。

并且使用了Promise的封装,开发起来感觉十分自在。【包装一个async函数就更爽了】

然而,以上只是表象。

只是表象。

我在开发的时候就遇到了问题。场景是这样的:一个对象,存储的是各级的路由信息,及其对应的页面组件。为减少主包大小,我们希望动态加载这些页面。

同时使用了react-loadable来简化组件的懒加载封装。代码如下所示。

  1. function lazyLoad(path) {
  2. return Loadable({
  3. loader: () => import(path),
  4. loading: Spin,
  5. });
  6. }

然后我就开始开心的在代码中写上lazyLoad(‘./pages/xxx’)。果不其然,挂了。浏览器表示,没有鱼丸没有粗面,也不知道这个傻逼模块在哪里。

于是我查看了官方文档,发现有一个黄条提示。
emmm,看来问题出在这里了。

这个现象其实是与webpack import()的实现高度相关的。由于webpack需要将所有import()的模块都进行单独打包,所以在工程打包阶段,webpack会进行依赖收集。

此时,webpack会找到所有import()的调用,将传入的参数处理成一个正则,如:

  1. import('./app'+path+'/util') => /^\.\/app.*\/util$/

也就是说,import参数中的所有变量,都会被替换为【.*】,而webpack就根据这个正则,查找所有符合条件的包,将其作为package进行打包。

因此,如果我们直接传入一个变量,webpack就会把 (整个电脑的包都打包进来[不闹]) 认为你在逗他,并且抛出一个WARNING: Critical dependency: the request of a dependency is an expression。

所以import的正确姿势,应该是尽可能静态化表达包所处的路径,最小化变量控制的区域。

如我们要引用一堆页面组件,可以使用import(‘./pages/‘+ComponentName),这样就可以实现引用的封装,同时也避免打包多余的内容。

另外一个影响功能封装的点,是import()中的相对路径,是import语句所在文件的相对路径,所以进一步封装import时会出现一些麻烦。

因为import语句中的路径会在编译后被处理成webpack命令执行目录的相对路径.

ES6的import语法告诉我们,模块只能做静态加载。所谓静态加载,就是你不能写成如下形式:

  1. let filename = 'module.js';
  2. import {mod} from './' + filename.

也不能写成如下形式:

  1. if(condition) {
  2. import {mod} from './path1'
  3. } else {
  4. import {mod} from './path2'
  5. }

但是现在有新提案让import进行动态加载,虽然还没有被ECMA委员会批准,但是webpack已经开始用了。
大致用法是这样的:

  1. let filename = 'module.js';
  2. import('./' + filename). then(module =>{
  3. console(module);
  4. }).catch(err => {
  5. console(err.message);
  6. });
  7. //如果你知道 export的函数名
  8. import('./' + filename). then(({fnName}) =>{
  9. console(fnName);
  10. }).catch(err => {
  11. console(err.message);
  12. });
  13. //如果你用的是export default function()
  14. import('./' + filename). then(module =>{
  15. console(module.default);
  16. }).catch(err => {
  17. console(err.message);
  18. });
  19. //或者
  20. import('./' + filename). then(({default:fnName}) =>{
  21. console(fnName);
  22. }).catch(err => {
  23. console(err.message);
  24. });

这里有一点要注意的是:import的加载是加载的模块的引用。而import()加载的是模块的拷贝,就是类似于require(),怎么来说明?看下面的例子:
module.js 文件:

  1. export let counter = 3;
  2. export function incCounter() {
  3. counter++;
  4. }

main.js

  1. let filename = 'module.js';
  2. import('./' + filename).then(({counter, incCounter})=>{
  3. console.log(counter); //3
  4. incCounter();
  5. console.log(counter); //3
  6. });
  7. import {counter, incCounter} from './module.js';
  8. console.log(counter); //3
  9. incCounter();
  10. console.log(counter); //4

source map 原理探究

https://blog.fundebug.com/2018/10/12/understanding_frontend_source_map/
输入 -》 uglify-js将两者合并打包并且压缩 -》输出

npm install uglify-js -g
uglifyjs log.js main.js -o output.js —source-map “url=’/output.js.map’”

安装并执行后,我们得到了一个输出文件output.js,同时生成了一个Source Map文件output.js.map。

将生成的文件中每个单词位置对应的原位置保存起来。

但是大多数情况下处理后的文件行数都会少于源文件,特别是 js,使用 UglifyJS 压缩后的文件通常只有一行。基于此,没必要在每条映射中都带上输出文件的行号,转而在这些映射中插入来标识换行,可以节省大量空间。

同时,考虑到代码经常会有合并打包的情况,即输入文件不止一个,所以可以将输入文件抽取一个数组,记录时,只需要记录一个索引,还原的时候再到这个数组中通过索引取出文件的位置及文件名即可。

webpack5

https://juejin.cn/post/6844904169405415432#heading-0
https://juejin.cn/post/6850037264962027534

从 Tree Shaking 来走进 Babel 插件开发者的世界

https://blog.csdn.net/qq_34998786/article/details/121506863?spm=1001.2014.3001.5501

JavaScript 模块的循环加载

http://www.ruanyifeng.com/blog/2015/11/circular-dependency.html

webpack优化解决项目体积大、打包时间长、刷新时间长问题!


https://juejin.cn/post/6844904174937718792#heading-9

hash值(缓存时要特别注意)

我曾经在面试某厂的时候被问到怎么控制项目的版本?我以为是 package.json 里面的那个 version,就说有专门的控制工具,面试官说不对,他的意思是我一个项目打包后上线了,后来又改动了其中一个文件,我要怎么让浏览器知道文件改了,不能用之前缓存过的文件呢?我当时蒙了,面试官说可以给打包的文件加个hash值,这样每次文件改动都会生成不同的hash值,文件名就会不同,浏览器就知道了有文件被更新了,不会用之前缓存的了。如果不更改资源的文件名,浏览器可能会认为它没有被更新,就会使用它的缓存版本。

Webpack 文件指纹策略是将文件名后面加上 hash 值。特别在使用 CDN 的时候,缓存是它的特点与优势,但如果打包的文件名,没有 hash 后缀的话,你肯定会被缓存折磨的够呛 😂
例如我们在基础配置中用到的:filename: “[name][hash:8][ext]”
这里里面 [] 包起来的,就叫占位符,它们都是什么意思呢?请看下面这个表 👇🏻

占位符 解释
ext 文件后缀名
name 文件名
path 文件相对路径
folder 文件所在文件夹
hash 每次构建生成的唯一 hash 值
chunkhash 根据 chunk 生成 hash 值
contenthash 根据文件内容生成hash 值

表格里面的 hash、chunkhash、contenthash 你可能还是不清楚差别在哪

  • hash :任何一个文件改动,整个项目的构建 hash 值都会改变;
  • chunkhash:文件的改动只会影响其所在 chunk 的 hash 值;
  • contenthash:每个文件都有单独的 hash 值,文件的改动只会影响自身的 hash 值;
    1. //webpack.common.js
    2. output: {
    3. path: path.resolve(__dirname, "../dist"),
    4. filename: "[name].[contenthash:8].js",
    5. clean: true, //每次构建清除dist包
    6. }