webpack打包工具是什么
模块化开发的产物,模块化打包工具。require anything,bundle anything
解决了什么问题,满足了什么需求
- 热更新
- 代理服务
- ES6翻译
- 压缩打包
- 自动上传
- 模块开发的模块管理
- 模块化开发全局变量
- 模块依赖与执行顺序
- 翻译代码
输入是什么、输出是什么
输入
Javascript模块以及浏览器不能直接运行的其他拓展语言(TS、Less、Scss)
输出
输出浏览器可运行的代码,Webpack打包出来的bundle文件是一个IIFE的执行函数。
首先是一个自执行的函数,解决了全局作用域的问题
(()=> {
//
})()
自执行函数主要是依赖收集、缓存、代码执行求值
(()=> {
// 依赖文件
var __webpack_modules__ = {
}
/*****************************核心代码:缓存 *******************************************/
// The module cache
var __webpack_module_cache__ = {};
// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// Create a new module (and put it into the cache)
var module = (__webpack_module_cache__[moduleId] = {
// no module.id needed
// no module.loaded needed
exports: {},
});
// Execute the module function
__webpack_modules__[moduleId](
module,
module.exports,
__webpack_require__
);
// Return the exports of the module
return module.exports;
}
/************************************************************************/
/* webpack/runtime/define property getters */
(() => {
// define getter functions for harmony 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/runtime/define property getters */
(() => {
// define getter functions for harmony 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/runtime/hasOwnProperty shorthand */
(() => {
__webpack_require__.o = (obj, prop) =>
Object.prototype.hasOwnProperty.call(obj, prop);
})();
/* webpack/runtime/make namespace object */
(() => {
// define __esModule on exports
__webpack_require__.r = (exports) => {
if (typeof Symbol !== "undefined" && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, {
value: "Module",
});
}
Object.defineProperty(exports, "__esModule", { value: true });
};
})();
/************************************************************************/
// entry入口文件
(() => {
__webpack_require__.r(__webpack_exports__);
var _1__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/1.js");
var _2__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__("./src/2.js");
(0, _1__WEBPACK_IMPORTED_MODULE_0__.default)();
(0, _2__WEBPACK_IMPORTED_MODULE_1__.default)();
console.log("Hello World!");
})();
})()
在webpack5中,默认带了代码压缩,还能帮你执行一部分代码
核心概念
Entry:
入口,Webpack执行构建的第一步将从Entry开始,可抽象成输入。
Module:
模块,在Webpack里一切皆模块,一个模块对应着一个文件。Webpack会从配置的Entry开始递归找出所有依赖的模块。
Chunk:
代码块,一个Chunk由多个模块组合而成,用于代码合并与分割。
Loader:
模块转换器,用于把模块原内容按照需求转换成新内容。转译相关,打包过程处理源文件的,一次一个文件
Plugin:
扩展插件,在Webpack构建流程中的特定时机会广播出对应的事件,插件可以监听这些事件的发生,在特定时机做对应的事情。plugin构建流程相关,插件不能直接操作单个文件,他对整个构建过程起作用。
其他
别名寻址
执行原理
从上面分析可以看出,关键是把编写的代码生成模块依赖 webpack_modules
语法树AST
使用esprima可以生成语法树,语法树的总体结构就两种:
参考文档:https://docs.esprima.org/en/stable/syntax-tree-format.html
- 关键字组成的 statement,如 IfStatement, ForStatement等
- 运算语句(赋值、计算之类的操作)组成的 ExpressionStatement
type StatementListItem = Declaration | Statement;
type ModuleItem = ImportDeclaration | ExportDeclaration | StatementListItem;
type Statement = BlockStatement | BreakStatement | ContinueStatement |
DebuggerStatement | DoWhileStatement | EmptyStatement |
ExpressionStatement | ForStatement | ForInStatement |
ForOfStatement | FunctionDeclaration | IfStatement |
LabeledStatement | ReturnStatement | SwitchStatement |
ThrowStatement | TryStatement | VariableDeclaration |
WhileStatement | WithStatement;
// 运算语句
interface ExpressionStatement {
type: 'ExpressionStatement';
expression: Expression;
directive?: string;
}
// Expression 类型
type Expression = ThisExpression | Identifier | Literal |
ArrayExpression | ObjectExpression | FunctionExpression | ArrowFunctionExpression | ClassExpression |
TaggedTemplateExpression | MemberExpression | Super | MetaProperty |
NewExpression | CallExpression | UpdateExpression | AwaitExpression | UnaryExpression |
BinaryExpression | LogicalExpression | ConditionalExpression |
YieldExpression | AssignmentExpression | SequenceExpression;
用法例子:生成__webpack_modules__
const esprima = require("esprima");
const estraverse = require("estraverse");
// console.log(x) or console['error'](y)
function isConsoleCall(node) {
return (
node.type === "CallExpression" &&
node.callee.type === "MemberExpression" &&
node.callee.object.type === "Identifier" &&
node.callee.object.name === "console"
);
}
let source = `var answer = 6 * 7;if(true){answer =1; console.log(1)}`;
const ast = esprima.parse(source, { range: true });
// 获取require的模块,生成__webpack_modules__
const context = path.resolve(__dirname, "./node_modules");
const pathResolve = (data) => path.resolve(context, data);
let ret = [];
let id = 0;
estraverse.traverse(ast, {
enter(node) {
// 筛选出require节点
if (
node.type === "CallExpression" &&
node.callee.name === "require" &&
node.callee.type === "Identifier"
) {
console.log(node);
// require路径,如require('./index.js'),则requirePath = './index.js'
const requirePath = node.arguments[0];
// 将require路径转为绝对路径
const requirePathValue = pathResolve(requirePath.value);
// 如require('./index.js')中'./index.js'在源码的位置
const requirePathRange = requirePath.range;
ret.push({ requirePathValue, requirePathRange, id });
id++;
}
},
});
console.log(ret);
// console.log(ast);
//去掉代码中的console打印
estraverse.traverse(ast, {
enter(node) {
if (isConsoleCall(node)) {
console.log(node, node.range);
source = source.slice(0, node.range[0]) + source.slice(node.range[1]);
console.log(source);
}
},
});
// 第二种
// const entries = [];
// esprima.parseScript(source, {}, function(node, metadata) {
// console.log(node);
// if (isConsoleCall(node)) {
// entries.push({
// start: metadata.start.offset,
// end: metadata.end.offset,
// });
// console.log(entries);
// entries
// .sort((a, b) => {
// return b.end - a.end;
// })
// .forEach((n) => {
// source = source.slice(0, n.start) + source.slice(n.end);
// console.log(source);
// });
// }
// });
压缩混淆
删除
Javascript 代码中所有注释、跳格符号、换行符号及无用的空格,缩短变量名称从而压缩 JS 文件大小。并且不同作用域的变量名是可以重复的,类似a,b,c可以反复出现。
加密混淆
经过编码将变量和函数原命名改为毫无意义的命名,以防止他人窥视和窃取 Javascript 源代码。让我们的代码尽可能的不可读,常见的做法有:分离变量,增加无意义的代码,打乱控制流。
僵尸代码注入
变量混淆
控制流平坦化:逻辑处理块统一加上前驱逻辑块,提高逻辑流程复杂度
代码压缩
反调试
• UglifyJS: https://github.com/mishoo/UglifyJS2
• terser: https://github.com/terser/terser
• javascript-obfuscator: https://github.com/javascript-obfuscator/javascript-obfuscator
• jsfuck: https://github.com/aemkei/jsfuck
• AAEncode: https://github.com/bprayudha/jquery.aaencode
• JJEncode: https://github.com/ay86/jEncrypt
懒加载chunk
jsonp加载文件
最终会通过script标签加载chunk文件,加载后默认会调用 window下的 webpackJsonp的push方法
创建script标签
__webpack_require__.e = (chunkId) => { // chunkId => src_a_js动态加载的模块
return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {
__webpack_require__.f[key](chunkId, promises); // 调用j方法 将数组传入
return promises;
}, []));
};
function webpackJsonpCallback(data) { // 3) 文件加载后会调用此方法
var chunkIds = data[0]; // data是什么来着,你看看src_a_js怎么写的你就知道了 看上面! ["src_a_js"]
var moreModules = data[1]; // 获取新增的模块
var moduleId, chunkId, i = 0, resolves = [];
for(;i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if(Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
// installedChunks[src_a_js] = [resolve,reject,promise] 这个是上面做的
// 很好理解 其实就是取到刚才放入的promise的resolve方法
resolves.push(installedChunks[chunkId][0]);
}
installedChunks[chunkId] = 0; // 模块加载完成
}
for(moduleId in moreModules) { // 将新增模块与默认的模块进行合并 也是就是modules模块,这样modules中就多了动态加载的模块
if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
__webpack_require__.m[moduleId] = moreModules[moduleId];
}
}
if(runtime) runtime(__webpack_require__);
if(parentJsonpFunction) parentJsonpFunction(data);
while(resolves.length) { // 调用promise的resolve方法,这样e方法就调用完成了
resolves.shift()();
}
};
var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || []; // 1) window["webpackJsonp"]等于一个数组
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
jsonpArray.push = webpackJsonpCallback; // 2) 重写了数组的push方法
var parentJsonpFunction = oldJsonpFunction;
有什么思想
插件化思想
一切皆模块
webpack5新特性
自带代码压缩
1.使用持久化缓存提高构建性能
2.使用更好的算法和默认值改进长期缓存(long-term caching)
3.清理内部结构而不引入任何破坏性的变化
4.引入一些breaking changes,以便尽可能长的使用v5版本
参考
https://segmentfault.com/a/1190000015973544(webpack究竟做了什么(一))
https://segmentfault.com/a/1190000020233387(webpack5懒加载)
https://blog.csdn.net/qq_22656655/article/details/107528583(webpack 路由懒加载的原理)
https://zhuanlan.zhihu.com/p/149323563(acorn生成ast)