1、webpack简化源码剖析

webpack 在执行npx webpack进行打包后,都干了什么事情?

  1. (function (modules) {
  2. var installedModules = {};
  3. function __webpack_require__(moduleId) {
  4. if (installedModules[moduleId]) {
  5. return installedModules[moduleId].exports;
  6. }
  7. var module = (installedModules[moduleId] = {
  8. i: moduleId,
  9. l: false,
  10. exports: {}
  11. }); modules[moduleId].call(
  12. module.exports, module, module.exports, __webpack_require__
  13. );
  14. module.l = true; return module.exports;
  15. }
  16. return __webpack_require__((__webpack_require__.s = "./index.js"));
  17. })({
  18. "./index.js": function (module, exports) {
  19. eval(
  20. '// import a from "./a";\n\nconsole.log("hello word");\n\n\n//# sourceURL=webpack:///./index.js?'
  21. )
  22. },
  23. "./a.js": function (module, exports) {
  24. eval(
  25. '// import a from "./a";\n\nconsole.log("hello word");\n\n\n//#sourceURL = webpack:///./index.js?'
  26. )
  27. },
  28. "./b.js": function (module, exports) {
  29. eval(
  30. '// import a from "./a";\n\nconsole.log("hello word");\n\n\n//#sourceURL = webpack:///./index.js?'
  31. );
  32. }
  33. });

使用webpack_require来实现内部的模块化,把代码都缓存在installedModules中,代码文件是以对象的形式传递进来,key是路径,value是包裹的代码字符串,并且代码内的require都被替换成了webpack_require

打包原理:

  • 1、读取webpack的配置参数;
  • 2、启动webpack,创建Compiler对象并开始解析项目;
  • 3、从入口文件(entry)开始解析,并且找到其导入的依赖模块,递归遍历分析,形成依赖关系树;
  • 4、对不同文件类型的依赖模块文件使用对应的Loader进行编译,最终转为Javascript文件;
  • 5、整个过程中webpack会通过发布订阅模式,向外抛出一些hooks,而webpack的插件即可通过监听这些关键的事件节点,执行插件任务进而达到干预输出结果的目的。

    2、webpack打包原理

image.png

const compier = webpack(options)
compier.run()

创建一个webpack

  • 接收一份配置(webpack.config.js)
  • 分析出入口模块位置
    • 读取入口模块的内容,分析内容
    • 哪些是依赖
    • 哪些是源码
      • es6,jsx,处理需要编译浏览器能够执行
    • 分析其他模块
  • 拿到对象数据结构
    • 模块路径
    • 处理好的内容
  • 创建bundle.js
    • 启动器函数,来补充代码里有可能出现的module exports require,让浏览器能够顺利的执行

      3、实现简单的打包

3.1 准备

目标是将依赖的ES6Module打包为一个可以在浏览器中运行的一个JS文件(bundle.js)
image.png

  1. // index.js
  2. import { str } from "./a.js";
  3. import { str2 } from "./b.js";
  4. console.log(`${str2} hello ${str}`);
  5. // a.js
  6. export const str = "a";
  7. // b.js
  8. export const str = "b";

bundle.js
image.png

3.2 实现步骤

3.2.1 模块分析

读取入口文件,分析代码

  1. const fs = require("fs");
  2. const parse= entry => {
  3. const content = fs.readFileSync(entry, "utf-8");
  4. console.log(content);
  5. };
  6. // 入口文件
  7. parse("./index.js");

模块的分析相当于对读取的文件代码字符串进行解析。这一步其实和高级语言的编译过程一致。需要将模块解析为抽象语法树AST。我们借助babel/parser来完成。

添加依赖:

  1. yarn add @babel/parser
  2. yarn add @babel/traverse
  3. yarn add @babel/core
  4. yarn add @babel/preset-env

转化成ast语法树

  1. const ast = parser.parse(content, {
  2. sourceType: "module",
  3. });

收集依赖

  1. // 依赖
  2. traverse(ast, {
  3. ImportDeclaration({ node }) {
  4. path.dirname(entryFile); //./src/index.js
  5. const newPathName = "./" + path.join(path.dirname(entryFile), node.source.value);
  6. dependencies[node.source.value] = newPathName;
  7. },
  8. });
  1. <br />es6转es5
  1. const { code } = transformFromAst(ast, null, {
  2. presets: ["@babel/preset-env"],
  3. });

完整代码:

  1. parse(entryFile) {
  2. // 如何读取模块的内容
  3. const content = fs.readFileSync(entryFile, "utf-8");
  4. const ast = parser.parse(content, {
  5. sourceType: "module",
  6. });
  7. const dependencies = {};
  8. traverse(ast, {
  9. ImportDeclaration({ node }) {
  10. path.dirname(entryFile); //./src/index.js
  11. // 处理路径
  12. const newPathName ="./" + path.join(path.dirname(entryFile), node.source.value);
  13. dependencies[node.source.value] = newPathName;
  14. },
  15. });
  16. const { code } = transformFromAst(ast, null, {
  17. presets: ["@babel/preset-env"],
  18. });
  19. const res = {
  20. entryFile,
  21. dependencies,
  22. code,
  23. };
  24. console.log(res)
  25. }
  26. parse("./src/index.js")

运行结果:
image.png

3.2.2 依赖模块收集

上一步开发的函数可以单独解析某一个模块,这一步我们需要开发一个函数从入口模块开始根据依赖关系进行递归解析。最后将依赖关系构成为依赖图(Dependency Graph)

  1. run() {
  2. const info = this.parse(this.entry);
  3. //递归处理所有依赖
  4. this.modules.push(info);
  5. for (let i = 0; i < this.modules.length; i++) {
  6. const item = this.modules[i];
  7. const { dependencies } = item;
  8. if (dependencies) {
  9. for (let j in dependencies) {
  10. this.modules.push(this.parse(dependencies[j]));
  11. }
  12. }
  13. }
  14. // 修改数据结构 数组转对象
  15. const obj = {};
  16. this.modules.forEach((item) => {
  17. obj[item.entryFile] = {
  18. dependencies: item.dependencies,
  19. code: item.code,
  20. };
  21. });
  22. console.log(obj);
  23. }

运行结果:
image.png

3.2.3 生成bundle文件

我们需要将刚才编写的执行函数和依赖图合成起来输出最后的打包文件。

浏览器中没有exports对象与require方法所以直接执行一定会报错。

所以我们需要模拟一下exports和require;

从上面的运行结果图中我们知道读出来的内部代码是一个字符串,所以我们采用eval让字符串代码执行;

require函数的功能比较简单,就是根据提供的file名称加载对应的模块。

对应代码如下:

  1. file(code) {
  2. const filePath = path.join(this.output.path, this.output.filename);
  3. const newCode = JSON.stringify(code);
  4. // 生成 bundle代码
  5. const bundle = `(function(modules){
  6. function require(module){
  7. function newRequire(relativePath){
  8. return require(modules[module].dependencies[relativePath])
  9. }
  10. // 默认exports不存在,所以这里定义一个对象
  11. var exports = {};
  12. (function(require,exports,code){
  13. eval(code)
  14. })(newRequire,exports,modules[module].code)
  15. return exports;
  16. }
  17. require('${this.entry}')
  18. })(${newCode})`;
  19. fs.writeFileSync(filePath, bundle, "utf-8");
  20. }

4、完整代码

  1. const fs = require("fs");
  2. const path = require("path");
  3. const parser = require("@babel/parser");
  4. const traverse = require("@babel/traverse").default;
  5. const { transformFromAst } = require("@babel/core");
  6. module.exports = class webpack {
  7. constructor(options) {
  8. this.entry = options.entry;
  9. this.output = options.output;
  10. this.modules = [];
  11. }
  12. run() {
  13. const info = this.parse(this.entry);
  14. //递归处理所有依赖
  15. this.modules.push(info);
  16. for (let i = 0; i < this.modules.length; i++) {
  17. const item = this.modules[i];
  18. const { dependencies } = item;
  19. if (dependencies) {
  20. for (let j in dependencies) {
  21. this.modules.push(this.parse(dependencies[j]));
  22. }
  23. }
  24. }
  25. // 修改数据结构 数组转对象
  26. const obj = {};
  27. this.modules.forEach((item) => {
  28. obj[item.entryFile] = {
  29. dependencies: item.dependencies,
  30. code: item.code,
  31. };
  32. });
  33. console.log(obj);
  34. // 代码生成,文件生成
  35. this.file(obj);
  36. }
  37. parse(entryFile) {
  38. // 如何读取模块的内容
  39. const content = fs.readFileSync(entryFile, "utf-8");
  40. const ast = parser.parse(content, {
  41. sourceType: "module",
  42. });
  43. const dependencies = {};
  44. traverse(ast, {
  45. ImportDeclaration({ node }) {
  46. path.dirname(entryFile); //./src/index.js
  47. const newPathName =
  48. "./" + path.join(path.dirname(entryFile), node.source.value);
  49. dependencies[node.source.value] = newPathName;
  50. },
  51. });
  52. const { code } = transformFromAst(ast, null, {
  53. presets: ["@babel/preset-env"],
  54. });
  55. return {
  56. entryFile,
  57. dependencies,
  58. code,
  59. };
  60. }
  61. file(code) {
  62. const filePath = path.join(this.output.path, this.output.filename);
  63. const newCode = JSON.stringify(code);
  64. // 生成 bundle代码
  65. const bundle = `(function(modules){
  66. function require(module){
  67. function newRequire(relativePath){
  68. // 相对路径对应的真实路径
  69. return require(modules[module].dependencies[relativePath])
  70. }
  71. var exports = {};
  72. (function(require,exports,code){
  73. eval(code)
  74. })(newRequire,exports,modules[module].code)
  75. return exports;
  76. }
  77. require('${this.entry}')
  78. })(${newCode})`;
  79. fs.writeFileSync(filePath, bundle, "utf-8");
  80. }
  81. };

5、打包结果

运行命令node bundle.js

  1. (function(modules){
  2. function require(module){
  3. function newRequire(relativePath){
  4. return require(modules[module].dependencies[relativePath])
  5. }
  6. var exports = {};
  7. (function(require,exports,code){
  8. eval(code)
  9. })(newRequire,exports,modules[module].code)
  10. return exports;
  11. }
  12. require('./src/index.js')
  13. })({"./src/index.js":{"dependencies":{"./a.js":"./src/a.js","./b.js":"./src/b.js"},"code":"\"use strict\";\n\nvar _a = require(\"./a.js\");\n\nvar _b = require(\"./b.js\");\n\n// 分析 入口模块的\n// 内容 : 依赖模块(目的是模块的路径)\n// 内容 : 借助babel 处理代码 生成 代码片段\n// node\nconsole.log(\"\".concat(_b.str2, \" hello \").concat(_a.str));"},"./src/a.js":{"dependencies":{},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.str = void 0;\nvar str = \"a\";\nexports.str = str;"},"./src/b.js":{"dependencies":{},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.str2 = void 0;\nvar str2 = \"b\";\nexports.str2 = str2;"}})

引入打包的js文件,运行一下:
image.png
image.png