webpack現現在已經更新到5.X的版本,可是對於一些中級工程師來講,對webpack的熟練度僅僅停留在會配置的階段,在前端發展日益迅速的時代,僅僅是會用而不瞭解其原理會阻礙其職業發展,這篇文章就是帶你更深刻了解webpack打包原理,並實現其打包功能。前端
在我剛學會配置webpack的時候,用webpack打包出來的文件也曾想去讀一讀看一看,可是本身心裏誤覺得會很難懂,因此直接放棄了,現在在回過頭,其實並不難。用個人一句話總結就是:使用nodejs的fs模塊來讀取文件內容並創造出一個‘路徑-代碼塊’的map,而後寫進一個js文件裏,在用eval執行它。node
咱們這裏只看開發環境下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
一系列方法是爲了實現import
和export
功能,用於導出和引入變量,咱們這裏不作太多討論,後續會有本身的方法。
當打包後的這個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文件的,須要將它轉化爲字符串,而轉化爲字符串格式只能用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);
複製代碼
從新打包,在瀏覽器裏運行是這樣的: 爲何會報這個錯誤呢? 看圖中標記的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
未定義?對的,由於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");
複製代碼