阅读 1796

威博派克,存在的意义

新蜂商城开源仓库(内涵 Vue 2.x 和 Vue 3.x 的 H5 商城开源代码,带服务端 API 接口):github.com/newbee-ltd

Vue 3.x + Vant 3.x + Vue-Router 4.x 高仿微信记账本开源地址(带服务端 API 接口):github.com/Nick930826/…

React + Vite 2.0 + Zarm 高仿微信记账本开源地址(带服务端 API 接口)以及学习文档:github.com/Nick930826/…

题外话

想要摧毁一个人,就让他去装修。

大家好,我是失踪人口尼克陈,6月份开始家里装修,抽不出时间写东西。最近装修接近收尾,并且公司项目也没有非常忙,抽出时间写一篇文章,尽自己绵薄之力,为社区做一点小贡献。

本次文章主要是想和大家聊聊 Webpack 存在的意义,以及打包后,代码是如何运行的。

准备工作

我们闭着眼睛,盲写一下最简单的 Webpack 项目初始化过程。

image.png

首先找一个自己最爱的目录,新建一个文件夹,通过 npm init -y 初始化一个项目工程,如下所示:

WeChatfc01a8709cc8347fd5a74f58e4890120.png

再安装两个耳熟能详的 Webpack 插件,如下所示:

yarn add webpack webpack-cli -D
复制代码

接下来在根目录下新建一个 webpack 配置文件 webpack.config.js 添加内容如下:

const path = require('path');

module.exports = {
  mode: 'development', // 开发模式
  entry: './src/index.js', // 入口文件
  output: {
    filename: '[name].js', // 输出文件名,默认是 main.js
    path: path.join(__dirname, 'dist') // 打包后输出文件的地址
  }
}
复制代码

最后在根目录下新建 src 文件夹,在文件夹内新建两个文件,分别是 index.jstool.js,内容分别如下:

// index.js
import MoonFestival from './tool';
MoonFestival();
复制代码
// tool.js
export default () => {
  document.body.innerHTML = '中秋节快乐🎑'
};
复制代码

修改 package.json ,如下所示:

"scripts": {
  "build": "webpack --config webpack.config.js"
},
复制代码

最后狠狠滴运行 npm run build,你会发现根目录下多了一个 dist 文件夹,如下所示:

image.png

你可以试着手动新建一个 index.html ,引入 main.js 如下所示:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>威博派克</title>
</head>
<body>
  <script src="./dist/main.js"></script>
</body>
</html>
复制代码

用浏览器打开,如下所示:

image.png

上述步骤的目的,就是通过 webpack 打包一份构建后的代码,接下来我们分析一波构建后的代码是如何在浏览器上运行的。

引入打包前的脚本

我们不妨尝试引入打包前 src 目录下的脚本,修改 index.html 如下所示:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>威博派克</title>
</head>
<body>
  <script src="./src/index.js"></script>
</body>
</html>
复制代码

默认使用谷歌浏览器打开,这里推荐大家一个 VSCode 插件 Live Server,它的作用是可以直接启动一个 Web 服务。

image.png

我们选中 index.html 文件,点击 VSCode 右下角的「Go Live」按钮,默认启动一个 http://127.0.0.1:5000 的服务,如下所示:

image.png

报错了,因为浏览器默认不支持 ES Module 模块化的 importexport 等关键字。但是有一个小技巧可以让其支持 ES Module ,我们给 index.htmlscript 标签添加一个 type="module" 属性,再次刷新浏览器,如下所示:

image.png

报错没有了,该属性让浏览器支持以 ES Module 规范的模块化开发形式,但是我们看看这个属性的浏览器支持情况:

image.png

支持情况并不是很乐观,所以催生出了一些列的构建项目工具,grant、gulp、fis3、rollup、parcel、webapck等等,从中脱引而出的便是 Webpack

Webpack 干了啥

鉴于上述,想用 ES Module 的形式开发代码,又想要代码能在各大浏览器稳定地运行。那么只有一种方式,写代码的时候用 ES Module 形式,上线的时候通过打包工具打包成兼容各大浏览器的模块化形式。

Webpack 就为我们干了这件事情,通过 babel 工具,将 ES6 的代码转化为 ES5 的代码,目的是更好地兼容各大浏览器。Webpack 再通过 Node 操作,将代码转化为代码字符串,通过标记的形式将代码拆分成一个个模块,并通过 eval 执行这些 代码字符串

文字很无力,不足以让你理解打包后是如何成功运行在多个浏览器,接下来我们分析一下 /dist/main.js 是个什么东西。

瘦身

上述代码,打包后输出的 main.js 大概有 96 行,我们将一些代码注释去除之后,大概是下面这样:

(() => {
  "use strict";
  var __webpack_modules__ = ({
    "./src/index.js": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
      eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _tool_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./tool.js */ \"./src/tool.js\");\n\n(0,_tool_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"])();\n\n//# sourceURL=webpack://weibopaike/./src/index.js?");
    }),
    "./src/tool.js": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
      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/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (() => {\n  document.body.innerHTML = '中秋节快乐🎑'\n});\n\n//# sourceURL=webpack://weibopaike/./src/tool.js?");
      })
  });

  var __webpack_module_cache__ = {};
  function __webpack_require__(moduleId) {
    var cachedModule = __webpack_module_cache__[moduleId];
    if (cachedModule !== undefined) {
      return cachedModule.exports;
    }
    var module = __webpack_module_cache__[moduleId] = {
      exports: {}
    };
    __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
      return module.exports;
    }

  (() => {
    __webpack_require__.d = (exports, definition) => {
      for(var key in definition) {
        if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
          Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
        }
      }
    };
  })();

  (() => {
    __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
  })();

  (() => {
    __webpack_require__.r = (exports) => {
      if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
        Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
      }
      Object.defineProperty(exports, '__esModule', { value: true });
    };
  })();

  var __webpack_exports__ = __webpack_require__("./src/index.js");
})();
复制代码

乍一看,我滴妈,这都是些什么玩意儿。其实它就是命名复杂点,吓唬吓唬你,接下来我们掰开揉碎了讲。

image.png

再次瘦身

0、__unused_webpack_module 没用到,有点碍眼,先去掉。

1、将 __webpack_require__ 都换成 require,这里可以理解为 Webpack 内部自定义的 require 方法,用于引入模块。

2、将 __webpack_module_cache__ 清理掉,这里可以理解为一个模块缓存的对象,第一次加载模块的时候,将模块存储到 __webpack_module_cache__ 对象中,在下一次调用的时候就不用再重新执行 __webpack_modules__下的方法。

3、__webpack_require__.r 也可以暂时清理掉,它的作用是给 __webpack_exports__ 添加一个 __esModuletrue 的属性,表示这是一个 ES Module。主要是为了处理混合使用 ES ModuleCommonJS 的情况。

4、_tool_js__WEBPACK_IMPORTED_MODULE_0__ 替换成 tool 便于理解。

二次抽脂后,得出了一个骨瘦如柴的基础代码:

(() => {
  "use strict";
  var __webpack_modules__ = ({
    "./src/index.js": (( __webpack_exports__, require) => {
      eval("var tool = require( \"./src/tool.js\");\n\n(0,tool[\"default\"])();");
    }),
    "./src/tool.js": (( __webpack_exports__, require) => {
      eval("require.d(__webpack_exports__, {\n\"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n });\nconst __WEBPACK_DEFAULT_EXPORT__ = (() => {\n  document.body.innerHTML = '中秋节快乐🎑'\n});\n\n");
      })
  });

  function require(moduleId) {
    var module = {
      exports: {}
    };
    __webpack_modules__[moduleId](module.exports, require);
    return module.exports;
  }

  (() => {
    require.d = (exports, definition) => {
      for(var key in definition) {
        if(require.o(definition, key) && !require.o(exports, key)) {
          Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
        }
      }
    };
  })();

  (() => {
    require.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
  })();

  var __webpack_exports__ = require("./src/index.js");
})();
复制代码

我心一狠,想把它的腿也给“砍了”,require.d 方法的作用就给 __webpack_exports__ 定义导出变量用的。例如上述代码中,./src/tool.js 中的 eval 是这样的:

require.d(__webpack_exports__, {
	"default": () => (__WEBPACK_DEFAULT_EXPORT__)
});
const __WEBPACK_DEFAULT_EXPORT__ = (() => {
  document.body.innerHTML = '中秋节快乐🎑'
});
复制代码

它的作用相当于 __webpack_exports__["default"] = __WEBPACK_DEFAULT_EXPORT__。 所以咱们顺便把 require.d 也给“砍了”。。。 最终代码如下:

(() => {
  "use strict";
  var __webpack_modules__ = ({
    "./src/index.js": (( __webpack_exports__, require) => {
      eval("var tool = require( \"./src/tool.js\");\n\n(0,tool[\"default\"])();");
    }),
    "./src/tool.js": (( __webpack_exports__, require) => {
      eval("const __WEBPACK_DEFAULT_EXPORT__ = (() => {\n  document.body.innerHTML = '中秋节快乐🎑'\n});\n\n__webpack_exports__[\"default\"] = __WEBPACK_DEFAULT_EXPORT__");
      })
  });

  function require(moduleId) {
    var module = {
      exports: {}
    };
    __webpack_modules__[moduleId](module.exports, require);
    return module.exports;
  }

  var __webpack_exports__ = require("./src/index.js");
})();
复制代码

变成“人干”了,这会儿我们狠狠滴分析分析这 21 行代码。

逐行分析

首先,整体代码块是一个IIFE,就是 立即执行函数,之所以将打包后的代码块放入一个立即执行函数,是为了避免污染全局变量。

webpack_modules

我们所有的模块代码,都存在这个对象上,并且以文件名为 ,代码字符串为 ./src/index.jseval 内的代码如下:

var tool = require("./src/tool.js");

(0,tool["default"])();

复制代码

./src/tool.js 中的 eval 代码如下:

const __WEBPACK_DEFAULT_EXPORT__ = (() => {
  document.body.innerHTML = '中秋节快乐🎑'
});

__webpack_exports__["default"] = __WEBPACK_DEFAULT_EXPORT__;
复制代码

注意,上述代码都是字符串,必须通过 webpack_modules('./src/index.js')(xxx, xxx) 才能执行。

require

该方法是 Webpack 内部生成的一个模拟 CommonJS 的一个模块引入机制,它接受一个参数 moduleId,该参数便是模块的文件路径,如 ./src/index.js./src/tool.js

内部定义了一个对象:

var module = {
  exports: {}
};
复制代码

将上述 eval 代码字符串中的一些方法,赋值到 exports 这个对象中,如:

__webpack_exports__["default"] = __WEBPACK_DEFAULT_EXPORT__;
复制代码

最后 return 出去,如:

var tool = require("./src/tool.js");
复制代码

这里的 tool 便是 require 方法 return 回来的 module.exports,最后通过 (0,tool["default"])(); 执行了 document.body.innerHTML = '中秋节快乐🎑'

入口

我们上述全部业务的代码逻辑入口是 ./src/index.js,所以在 立即执行函数 的最后,执行入口:

var __webpack_exports__ = require("./src/index.js");
复制代码

通过 require 方法递归执行,将所有的模块串联到了一起,这便是整个 Webpack 打包后文件执行的机制。

我们不妨将上述被“砍”得骨瘦如柴的代码执行一下,如下所示:

image.png

又被我装到了。

逐行跑一遍代码

我们在 ./dist/main.js 代码的最顶部打个断点,如下所示:

(() => {
	debugger;
  "use strict";
  ....
})()
复制代码

看看浏览器内部是怎么执行这个代码的: Kapture 2021-09-17 at 11.49.00.gif

总结

代码中为了便于分析执行机制,只涉及了一个模块的引入,在平时开发的时候,会有大量的模块引入,和复杂的依赖关系,这就涉及到了更深的知识,Webpack 内部会有非常健全的模块分类机制,通过打标签的形式,标记每个模块,以及一些深层次的缓存优化机制,这就需要同学们再往深了学习了。 至少理解了上述原理之后,在被面试官问到 Webpack 相关的面试题,你不会一问三不知。可以从模块化的发展,聊到 Webpack 的打包机制。

文章分类
前端