likes
comments
collection
share

67行代码掌握webpack核心原理,你也能手撸一个”小webpack“~

作者站长头像
站长
· 阅读数 32

前言

webpack现如今已经更新到5.X的版本,但是对于一些中级工程师来说,对webpack的熟练度仅仅停留在会配置的阶段,在前端发展日益迅速的时代,仅仅是会用而不了解其原理会阻碍其职业发展,这篇文章就是带你更深入了解webpack打包原理,并实现其打包功能。

在我刚学会配置webpack的时候,用webpack打包出来的文件也曾想去读一读看一看,但是自己内心误以为会很难懂,所以直接放弃了,如今在回过头,其实并不难。用我的一句话总结就是:使用nodejs的fs模块来读取文件内容并创造出一个‘路径-代码块’的map,然后写进一个js文件里,在用eval执行它

webpack打包后具体是什么样

我们这里只看开发环境下webpack打包后的代码,可以很直观的看出webpack到底打包成了什么样,因为在生产环境下,webpack会默认开启代码压缩、treeshaking等优化手段,增加理解难度。 67行代码掌握webpack核心原理,你也能手撸一个”小webpack“~

src下文件
//index.js
import { cute } from "./cute.js";
import add from "./add.js";

const num1 = add(1, 2);
const num2 = cute(100, 22);
console.log(num1, num2);

//add.js
const add = (a, b) => {
  return a + b;
};
export default add;

//cute.js
import getUrl from "./utils/index.js";
export const cute = (a, b) => {
  return a - b;
};
getUrl();

// utils/index.js
const getUrl = () => {
  const url = window.location.pathname;
  return url;
};
export default getUrl;

我们使用index.js文件作为入口文件开始打包

//webpack.config.js
const path = require("path");

module.exports = {
  mode: "development",
  entry: "./src/index.js",
  output: {
    filename: "bundle.js",
    path: path.resolve(__dirname, "build"),
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        include: path.resolve(__dirname, "./src"),
        use: [
          {
            loader: "babel-loader",
            options: {
              presets: ["@babel/preset-env"],
            },
          },
        ],
      },
    ],
  },
};

打包后的结果(核心部分:

(() => {
  // webpackBootstrap
  "use strict";
  var __webpack_modules__ = {
    "./src/add.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 */ });\nvar add = function add(a, b) {\n  return a + b;\n};\n\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (add);\n\n//# sourceURL=webpack:///./src/add.js?'
      );
    },

    "./src/cute.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 */   "cute": () => /* binding */ cute\n/* harmony export */ });\n/* harmony import */ var _utils_index_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./utils/index.js */ "./src/utils/index.js");\n\nvar cute = function cute(a, b) {\n  return a - b;\n};\n(0,_utils_index_js__WEBPACK_IMPORTED_MODULE_0__.default)();\n\n//# sourceURL=webpack:///./src/cute.js?'
      );
    },

    "./src/index.js": (
      __unused_webpack_module,
      __webpack_exports__,
      __webpack_require__
    ) => {
      eval(
        '__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _cute_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./cute.js */ "./src/cute.js");\n/* harmony import */ var _add_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./add.js */ "./src/add.js");\n\n\nvar num1 = (0,_add_js__WEBPACK_IMPORTED_MODULE_1__.default)(1, 2);\nvar num2 = (0,_cute_js__WEBPACK_IMPORTED_MODULE_0__.cute)(100, 22);\nconsole.log(num1, num2);\n\n//# sourceURL=webpack:///./src/index.js?'
      );
    },

    "./src/utils/index.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 */ });\nvar getUrl = function getUrl() {\n  var url = window.location.pathname;\n  return url;\n};\n\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (getUrl);\n\n//# sourceURL=webpack:///./src/utils/index.js?'
      );
    },
  };

  var __webpack_module_cache__ = {};

  function __webpack_require__(moduleId) {
    if (__webpack_module_cache__[moduleId]) {
      return __webpack_module_cache__[moduleId].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 });
    };
  })();
  __webpack_require__("./src/index.js");
})();

简单解读一下

可以看出来每一个文件都以当前的相对路径作为key一个函数作为value放进了这个一个__webpack_modules__对象里,其中每个value函数里用eval来执行当前文件下的代码。webpack.x一系列方法是为了实现importexport功能,用于导出和引入变量,我们这里不做太多讨论,后续会有自己的方法。 当打包后的这个js文件执行时,会先从"./src/index.js"这个key对应的value开始执行代码,我们来看一下过程: 67行代码掌握webpack核心原理,你也能手撸一个”小webpack“~

开始实现

从入口开始读取代码

// mypack.js
const fs = require("fs");

const getCode = (entry) => {
  const code = fs.readFileSync(entry, "utf8");
  console.log(code)
}
getCode('./src/index.js')

node mypack67行代码掌握webpack核心原理,你也能手撸一个”小webpack“~ 我们获取了入口文件的代码,接下来就是从入口文件开始获取依赖文件,把所有引入的文件路径拿到。

获取依赖

获取依赖的意思就是将每个文件文件import导入的文件路径收集起来,这里要用到遍历AST的@babel/traverse库,找到import节点。

const fs = require("fs");
const parser = require("@babel/parser"); //转化ast
const traverse = require("@babel/traverse").default; //遍历ast

const getCode = (entry) => {
  const code = fs.readFileSync(entry, "utf8");
  const ast = parser.parse(code, {
    sourceType: "module",
  });
  traverse(ast, {
    ImportDeclaration(p) {
      const importPath = p.get("source").node.value;
      console.log(importPath) 
    },
  });
}
getCode('./src/index.js')

67行代码掌握webpack核心原理,你也能手撸一个”小webpack“~ 这样我们得到了入口文件依赖的文件路径,然后在通过递归手段获取所有文件的代码。因为这里我们能拿到的是引用文件与被引用文件之间的相对路径,但是我们方法里fs在读取文件需要使用相对于我们mypack.js的路径,也就是在src目录的路径,所以我们可以用相对路径:src路径来做一个映射,并且拿到当前路径下被转化后的代码,得到一个{相对路径:{ 依赖:{ 相对路径:src路径 },代码:{...} }}格式的对象。

const fs = require("fs");
const path = require('path');
const parser = require("@babel/parser"); //转化ast
const traverse = require("@babel/traverse").default; //遍历ast
const getCode = (entry) => {
  const code = fs.readFileSync(entry, "utf8");
  const dirname = path.dirname(entry);  //获取当前文件所在的目录
  const ast = parser.parse(code, {
    sourceType: "module",
  });
  const deps = {};
  traverse(ast, {
    ImportDeclaration(p) {
      const importPath = p.get("source").node.value;
      const asbPath = "./" + path.join(dirname, importPath); //获取相对于src目录的路径
      deps[importPath] = asbPath;
    },
  });
  // 获取当前entry文件下被转化后的代码
  const { code:transCode } = babel.transformFromAst(ast, null, {
    presets: ["@babel/preset-env"],
  });
  console.log(entry,deps,transCode)
};
getCode("./src/index.js");

67行代码掌握webpack核心原理,你也能手撸一个”小webpack“~ 这样我们就得到了入口文件'./src/index.js'入口路径依赖文件代码。 接下来我们就可以通过入口文件的依赖来递归获取所有文件的信息。

递归获取所有依赖的信息

const fs = require("fs");
const path = require('path');
const parser = require("@babel/parser"); //转化ast
const traverse = require("@babel/traverse").default; //遍历ast

const getCode = (entry) => {
  const code = fs.readFileSync(entry, "utf8");
  const dirname = path.dirname(entry);  //获取当前文件所在的目录
  const ast = parser.parse(code, {
    sourceType: "module",
  });
  const deps = {};
  traverse(ast, {
    ImportDeclaration(p) {
      const importPath = p.get("source").node.value;
      const asbPath = "./" + path.join(dirname, importPath); //获取相对于src目录的路径
      deps[importPath] = asbPath;
    },
  });
  // 获取当前entry文件下被转化后的代码
  const { code:transCode } = babel.transformFromAst(ast, null, {
    presets: ["@babel/preset-env"],
  });
  return { entry, code, deps }; 
};

const recurrenceGetCode = (entry) => {
  const entryInfo = getCode(entry);  //拿到入口文件所有信息
  const allInfo = [entryInfo];
  /*
   allInfo现在的信息只有入口文件的信息,为
   [{
    './src/index':{
      deps:{ './cute.js': './src/cute.js', './add.js': './src/add.js' },
      code:"use strict...."
     }
    }]
  */
  
 我们还要拿到cute.js、add.js以及utils/index.js的信息,,将之放进allInfo中
 const recurrenceDeps = (deps,modules) => {
 	Object.keys(deps).forEach(key=>{
      const info = getCode(deps[key])
      modules.push(info);
      recurrenceDeps(info.deps,modules)
    })
 }
 recurrenceDeps(entryInfo.deps,allInfo)
 console.log(allInfo) //看一下现在拿到了什么
}
recurrenceGetCode("./src/index.js");

67行代码掌握webpack核心原理,你也能手撸一个”小webpack“~ 拿到后再将其转变为一个map结构

const fs = require("fs");
const path = require('path');
const parser = require("@babel/parser"); //转化ast
const traverse = require("@babel/traverse").default; //遍历ast

const getCode = (entry) => {
  const code = fs.readFileSync(entry, "utf8");
  const dirname = path.dirname(entry);  //获取当前文件所在的目录
  const ast = parser.parse(code, {
    sourceType: "module",
  });
  const deps = {};
  traverse(ast, {
    ImportDeclaration(p) {
      const importPath = p.get("source").node.value;
      const asbPath = "./" + path.join(dirname, importPath); //获取相对于src目录的路径
      deps[importPath] = asbPath;
    },
  });
  // 获取当前entry文件下被转化后的代码
  const { code:transCode } = babel.transformFromAst(ast, null, {
    presets: ["@babel/preset-env"],
  });
  return { entry, code, deps }; 
};

const recurrenceGetCode = (entry) => {
  const entryInfo = getCode(entry);  //拿到入口文件所有信息
  const allInfo = [entryInfo];
  /*
   allInfo现在的信息只有入口文件的信息,为
   [{
    './src/index':{
      deps:{ './cute.js': './src/cute.js', './add.js': './src/add.js' },
      code:"use strict...."
     }
    }]
  */
  
 我们还要拿到cute.js、add.js以及utils/index.js的信息,,将之放进allInfo中
 const recurrenceDeps = (deps,modules) => {
 	Object.keys(deps).forEach(key=>{
      const info = getCode(deps[key])
      modules.push(info);
      recurrenceDeps(info.deps,modules)
    })
 }
 recurrenceDeps(entryInfo.deps,allInfo)
 
 const webpack_modules = {};
 allInfo.forEach(item=>{
   webpack_modules[item.entry] = {
     deps:item.deps,
     code:item.transCode,
 }
 })
 return webpack_modules;
}
const webpack_modules = recurrenceGetCode("./src/index.js");
// webpack_modules就是我们最终想要的结果

打印webpack_modules是这样的

{
 './src/index.js':{
   deps:{},
   code:"..."
 },
 './src/cute.js':{
  deps:{},
  code:"..."
 }
 ...
}

将所有依赖信息写到js文件中

现在我们需要把得到的这个对象写进一个文件里,但是不能直接写入,因为对象结构是无法写进js文件的,需要将它转化为字符串,而转化为字符串格式只能用JSON.stringify得到一个JSON字符串,JSON字符串在js文件里是不能被识别的,那用办法呢?回过头我们去看webpack打包后的文件,是一个自执行函数(()=>{})()这样,那我们是不是也可以将之作为参数传入一个自执行函数里,然后在写进js文件里呢?答案是可以的。

//以上代码省略掉,直接往下看就可以
const webpack_modules = recurrenceGetCode("./src/index.js");
const writeFunction = `((content)=>{
  console.log(content)
})(${JSON.stringify(webpack_modules)})`;
fs.writeFileSync("./exs.js", writeFunction);

我们来看一下生成的exs.js文件代码:

((content) => {
  console.log(content);
})({
  "./src/index.js": {
    deps: { "./cute.js": "./src/cute.js", "./add.js": "./src/add.js" },
    code:
      '"use strict";\n\nvar _cute = require("./cute.js");\n\nvar _add = _interopRequireDefault(require("./add.js"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n\nvar num1 = (0, _add["default"])(1, 2);\nvar num2 = (0, _cute.cute)(100, 22);\nconsole.log(num1, num2);',
  },
  "./src/cute.js": {
    deps: { "./utils/index.js": "./src/utils/index.js" },
    code:
      '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nexports.cute = void 0;\n\nvar _index = _interopRequireDefault(require("./utils/index.js"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n\nvar cute = function cute(a, b) {\n  return a - b;\n};\n\nexports.cute = cute;\n(0, _index["default"])();',
  },
  "./src/utils/index.js": {
    deps: {},
    code:
      '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nexports["default"] = void 0;\n\nvar getUrl = function getUrl() {\n  var url = window.location.pathname;\n  return url;\n};\n\nvar _default = getUrl;\nexports["default"] = _default;',
  },
  "./src/add.js": {
    deps: {},
    code:
      '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nexports["default"] = void 0;\n\nvar add = function add(a, b) {\n  return a + b;\n};\n\nvar _default = add;\nexports["default"] = _default;',
  },
});

好,这样没问题,能够拿到content对象,就是从入口文件开始执行了,也就是执行content["./src/index.js"].code里的代码,我们把代码稍加改造一下:

//以上代码省略掉,直接往下看就可以
const webpack_modules = recurrenceGetCode("./src/index.js");
const writeFunction = `((content)=>{
  const require = (path) => {
  	const code = content[path].code;
    eval(code)
  }
})(${JSON.stringify(webpack_modules)})`;
fs.writeFileSync("./exs.js", writeFunction);

添加require函数

重新打包,在浏览器里运行是这样的: 67行代码掌握webpack核心原理,你也能手撸一个”小webpack“~ 为什么会报这个错误呢? 67行代码掌握webpack核心原理,你也能手撸一个”小webpack“~ 看图中标记的3步: 从入口文件开始执行./src/index.js中的code,代码运行到require('./cute.js')时重新执行require函数,将./cute.js作为参数传入,可content并没有./cute.js作为key的value存在,自然取不出其中的code,就报错了。此时我们每一个文件的deps又派上用场了,因为deps中有一个路径映射,所以我们在执行require函数时,从当前执行code的那个键值对中取出对应的src路径,也就是在content中对应的key,来执行。 67行代码掌握webpack核心原理,你也能手撸一个”小webpack“~ 代码继续改造:

//以上代码省略掉,直接往下看就可以
const webpack_modules = recurrenceGetCode("./src/index.js");
const writeFunction = `((content)=>{
  const require = (path) => {
    const getSrcPath = (p) => {
      const srcPath = content[path].deps[p];
      return require(srcPath)
    }
    ((require)=>{
      eval(content[path].code)
    })(getSrcPath)
  }
  require('./src/index.js')
})(${JSON.stringify(webpack_modules)})`;
fs.writeFileSync("./exs.js", writeFunction);

打包后为 67行代码掌握webpack核心原理,你也能手撸一个”小webpack“~ 这一步可能会有一些绕,接下来我会逐步解释: require到./cute.js时因为require函数作为参数传入到了执行eval的自执行函数中,所以自然会调用getSrcPath这个函数,而getSrcPath中是从content执行的path中取出依赖,来寻找对应的src路径,此时path为./src/index.js,所以自然就从{ "./cute.js": "./src/cute.js", "./add.js": "./src/add.js" }中取出来了"./cute.js"对应的"./src/cute.js",拿到这个路径后,在将之作为path传入require函数中,然后在调用 ((require) => { eval(content[path].code); })(getSrcPath);函数,那么这时执行代码会是什么结果呢?

添加exports

67行代码掌握webpack核心原理,你也能手撸一个”小webpack“~ exports未定义?对的,因为js中通过export导出的模块是一个对象,而在打包后的代码中并没有这个对象,所以我们需要在每一个文件执行时手动定义一个exports并将其return

//以上代码省略掉,直接往下看就可以
const webpack_modules = recurrenceGetCode("./src/index.js");
const writeFunction = `((content)=>{
  const require = (path) => {
    const getSrcPath = (p) => {
      const srcPath = content[path].deps[p];
      return require(srcPath)
    }
    const exports = {};
    ((require)=>{
      eval(content[path].code)
    })(getSrcPath)
    return exports;
  }
  require('./src/index.js')
})(${JSON.stringify(webpack_modules)})`;
fs.writeFileSync("./exs.js", writeFunction);

这样在执行就没有任何问题了! 67行代码掌握webpack核心原理,你也能手撸一个”小webpack“~

完整代码片段

const fs = require("fs");
const path = require("path");
const babel = require("@babel/core");
const parser = require("@babel/parser"); //转化ast
const traverse = require("@babel/traverse").default; //遍历ast

const getCode = (entry) => {
  const code = fs.readFileSync(entry, "utf8");
  const dirname = path.dirname(entry);
  const ast = parser.parse(code, {
    sourceType: "module",
  });
  const deps = {};
  traverse(ast, {
    ImportDeclaration(p) {
      const importPath = p.get("source").node.value;
      const asbPath = "./" + path.join(dirname, importPath); //获取相对于src目录的路径
      deps[importPath] = asbPath;
    },
  });
  const { code: transCode } = babel.transformFromAst(ast, null, {
    presets: ["@babel/preset-env"],
  });
  return { entry, deps, transCode };
};
const recurrenceGetCode = (entry) => {
  const entryInfo = getCode(entry); //拿到入口文件所有信息
  const allInfo = [entryInfo];

  const recurrenceDeps = (deps, modules) => {
    Object.keys(deps).forEach((key) => {
      const info = getCode(deps[key]);
      modules.push(info);
      recurrenceDeps(info.deps, modules);
    });
  };
  recurrenceDeps(entryInfo.deps, allInfo);
  const webpack_modules = {};
  allInfo.forEach((item) => {
    webpack_modules[item.entry] = {
      deps: item.deps,
      code: item.transCode,
    };
  });
  return webpack_modules;
};

const webpack = (entry) => {
  const webpack_modules = recurrenceGetCode(entry);
  const writeFunction = `((content)=>{
    const require = (path) => {
      const getSrcPath = (p) => {
        const srcPath = content[path].deps[p];
        return require(srcPath)
      }
      const exports = {};
      ((require,exports,code)=>{
        eval(code)
      })(getSrcPath,exports,content[path].code)
      return exports;
    }
    require('./src/index.js')
  
  })(${JSON.stringify(webpack_modules)})`;
  fs.writeFileSync("./exs.js", writeFunction);
};
webpack("./src/index.js");