webpack5的打包原理分析

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)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值