67行代码掌握webpack核心原理,你也能手撸一个”小webpack“~
前言
webpack现如今已经更新到5.X的版本,但是对于一些中级工程师来说,对webpack的熟练度仅仅停留在会配置的阶段,在前端发展日益迅速的时代,仅仅是会用而不了解其原理会阻碍其职业发展,这篇文章就是带你更深入了解webpack打包原理,并实现其打包功能。
在我刚学会配置webpack的时候,用webpack打包出来的文件也曾想去读一读看一看,但是自己内心误以为会很难懂,所以直接放弃了,如今在回过头,其实并不难。用我的一句话总结就是:使用nodejs的fs模块来读取文件内容并创造出一个‘路径-代码块’的map,然后写进一个js文件里,在用eval执行它。
webpack打包后具体是什么样
我们这里只看开发环境下webpack打包后的代码,可以很直观的看出webpack到底打包成了什么样,因为在生产环境下,webpack会默认开启代码压缩、treeshaking等优化手段,增加理解难度。
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
一系列方法是为了实现import
和export
功能,用于导出和引入变量,我们这里不做太多讨论,后续会有自己的方法。
当打包后的这个js文件执行时,会先从"./src/index.js"
这个key
对应的value开始执行代码,我们来看一下过程:
开始实现
从入口开始读取代码
// mypack.js
const fs = require("fs");
const getCode = (entry) => {
const code = fs.readFileSync(entry, "utf8");
console.log(code)
}
getCode('./src/index.js')
node mypack
后
我们获取了入口文件的代码,接下来就是从入口文件开始获取依赖文件,把所有引入的文件路径拿到。
获取依赖
获取依赖的意思就是将每个文件文件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'
的入口路径、依赖文件和代码。
接下来我们就可以通过入口文件的依赖来递归获取所有文件的信息。
递归获取所有依赖的信息
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");
转载自:https://juejin.cn/post/6908565593562202125