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");
複製代碼