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

前言

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

在我刚学会配置webpack的时候,用webpack打包出来的文件也曾想去读一读看一看,可是本身心里误觉得会很难懂,因此直接放弃了,现在在回过头,其实并不难。用个人一句话总结就是:使用nodejs的fs模块来读取文件内容并创造出一个‘路径-代码块’的map,而后写进一个js文件里,在用eval执行它node

webpack打包后具体是什么样

咱们这里只看开发环境下webpack打包后的代码,能够很直观的看出webpack到底打包成了什么样,由于在生产环境下,webpack会默认开启代码压缩、treeshaking等优化手段,增长理解难度。 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文件做为入口文件开始打包web

//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开始执行代码,咱们来看一下过程: babel

开始实现

从入口开始读取代码

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

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

node mypack 咱们获取了入口文件的代码,接下来就是从入口文件开始获取依赖文件,把全部引入的文件路径拿到。markdown

获取依赖

获取依赖的意思就是将每一个文件文件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')
复制代码

这样咱们获得了入口文件依赖的文件路径,而后在经过递归手段获取全部文件的代码。由于这里咱们能拿到的是引用文件与被引用文件之间的相对路径,可是咱们方法里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");
复制代码

这样咱们就获得了入口文件'./src/index.js'入口路径依赖文件代码。 接下来咱们就能够经过入口文件的依赖来递归获取全部文件的信息。ui

递归获取全部依赖的信息

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");
复制代码

拿到后再将其转变为一个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函数

从新打包,在浏览器里运行是这样的: 为何会报这个错误呢? 看图中标记的3步:
从入口文件开始执行./src/index.js中的code,代码运行到require('./cute.js')时从新执行require函数,将./cute.js做为参数传入,可content并无./cute.js做为key的value存在,天然取不出其中的code,就报错了。此时咱们每个文件的deps又派上用场了,由于deps中有一个路径映射,因此咱们在执行require函数时,从当前执行code的那个键值对中取出对应的src路径,也就是在content中对应的key,来执行。 代码继续改造:

//以上代码省略掉,直接往下看就能够
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);
复制代码

打包后为 这一步可能会有一些绕,接下来我会逐步解释:
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

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);
复制代码

这样在执行就没有任何问题了!

完整代码片断

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");
复制代码
相关文章
相关标签/搜索