webpack打包基本原理——实现webpack打包核心功能

news2025/7/19 10:41:07

webpack打包的基本原理

核心功能就是把我们写的模块化代码转换成浏览器能够识别运行的代码,话不多说我们一起来了解它

首先我们建一个空项目用 npm init -y 创建一个初始化的,在跟目录下创建src文件夹,src下创建index.js,add.js,square.js,tip.js。在根目录下创建index.html
在这里插入图片描述
index.html引入index.js,index.js引入add.js、square.js。square.js引入tip.js

//add.js
export default (a, b) => {
  return a + b;
};

//square.js
import tip from "tip.js";
const square = (a) => {
  console.log(tip);
  return a * a;
};
export { square };

//tip.js
export default "我是提示-----";


index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body></body>
  <script src="./src/index.js"></script>
</html>

运行index.html会报错,不能识别es6的模块化导入
在这里插入图片描述
让我们来实现webpack打包的核心功能

实现webpack打包核心功能

首先在根目录下建一个bundle.js用来对刚刚写的index.js进行打包
webpack官网对打包流程的介绍

it internally builds a dependency graph which maps every module your project needs and generates one or more bundles(webpack会在内部构建一个 依赖图(dependency graph),此依赖图会映射项目所需的每个模块,并生成一个或多个 bundle)

根据上面的说明进行分析,打包的工作基本流程如下

  1. 读取入口文件中的内容(也就是index.js文件)
  2. 分析入口文件,递归读取模块所依赖的文件 内容,生成依赖图
  3. 根据依赖图生成浏览器能运行的代码

1、处理单个模块内容

const fs = require("fs");
const getModuleInfo = (file) => {
  const body = fs.readFileSync(file, "utf-8");
  //获取文件内容,是字符串
  console.log(body);
};
getModuleInfo("./src/index.js");

打印出来是文件内容的字符串
在这里插入图片描述
借助安装@babel/parser 把,js文件代码转换成js对象,叫做抽象语法树(ast)

const fs = require("fs");
const parser = require("@babel/parser");
const getModuleInfo = (file) => {
  const body = fs.readFileSync(file, "utf-8");
  const ast = parser.parse(body, {
    //表示我们要解析的是es6模块
    sourceType: "module",
  });
  console.log(ast.program.body);
};
getModuleInfo("./src/index.js");

打印ast.program.body的结构:

[
  Node {
    type: 'ImportDeclaration', 
    start: 0,
    end: 27,
    loc: SourceLocation {      
      start: [Position],       
      end: [Position],
      filename: undefined,     
      identifierName: undefined
    },
    specifiers: [ [Node] ],
    source: Node {
      type: 'StringLiteral',
      start: 16,
      end: 26,
      loc: [SourceLocation],
      extra: [Object],
      value: './add.js'
    }
  },
  Node {
    type: 'ImportDeclaration',
    start: 29,
    end: 66,
    loc: SourceLocation {
      start: [Position],
      end: [Position],
      filename: undefined,
      identifierName: undefined
    },
    specifiers: [ [Node] ],
    source: Node {
      type: 'StringLiteral',
      start: 52,
      end: 65,
      loc: [SourceLocation],
      extra: [Object],
      value: './square.js'
    }
  },
  Node {
    type: 'VariableDeclaration',
    start: 68,
    end: 90,
    loc: SourceLocation {
      start: [Position],
      end: [Position],
      filename: undefined,
      identifierName: undefined
    },
    declarations: [ [Node] ],
    kind: 'const'
  },
  Node {
    type: 'VariableDeclaration',
    start: 92,
    end: 114,
    loc: SourceLocation {
      start: [Position],
      end: [Position],
      filename: undefined,
      identifierName: undefined
    },
    declarations: [ [Node] ],
    kind: 'const'
  },
  Node {
    type: 'ExpressionStatement',
    start: 116,
    end: 143,
    loc: SourceLocation {
      start: [Position],
      end: [Position],
      filename: undefined,
      identifierName: undefined
    },
    expression: Node {
      type: 'CallExpression',
      start: 116,
      end: 142,
      loc: [SourceLocation],
      callee: [Node],
      arguments: [Array]
    }
  },
  Node {
    type: 'ExpressionStatement',
    start: 145,
    end: 172,
    loc: SourceLocation {
      start: [Position],
      end: [Position],
      filename: undefined,
      identifierName: undefined
    },
    expression: Node {
      type: 'CallExpression',
      start: 145,
      end: 171,
      loc: [SourceLocation],
      callee: [Node],
      arguments: [Array]
    }
  }
]

type属性是ImportDeclaration的节点,其source.value属性是引入这个模块的相对路径,上面打印出两个ImportDeclaration节点,说明对应的是入口文件中的两个import

对ast.program.body做处理,本质上是对这个数组遍历,循环做处理,借助安装@babel/traverse来完成这项工作

const fs = require("fs");
const path = require("path");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;

const getModuleInfo = (file) => {
  //1、把入口文件字符串ast对象
  const body = fs.readFileSync(file, "utf-8");
  const ast = parser.parse(body, {
    //表示我们要解析的是es6模块
    sourceType: "module",
  });

  //2、获取类型为ImportDeclaration的所依赖模块的信息地址
  const deps = {};
  //创建一个对象deps,用来收集模块自身引入的依赖,用traverse遍历需要的ImportDeclaration节点做处理,把相对路径转成绝对路径
  traverse(ast, {
    ImportDeclaration({ node }) {
      const dirname = path.dirname(file);
      let absPath = "./" + path.join(dirname, node.source.value);
      absPath = absPath.replace("\\", "/");
      deps[node.source.value] = absPath;
    },
  });
  console.log(deps);
};
getModuleInfo("./src/index.js");

deps打印出入口文件引入的依赖地址
在这里插入图片描述
获取依赖后,需要对ast做语法转换,把es6转成es5的语法,安装@babel/core以及@babel/preset-env完成

const fs = require("fs");
const path = require("path");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const babel = require("@babel/core");

const getModuleInfo = (file) => {
  //1、把入口文件字符串ast对象
  const body = fs.readFileSync(file, "utf-8");
  const ast = parser.parse(body, {
    //表示我们要解析的是es6模块
    sourceType: "module",
  });

  //2、获取类型为ImportDeclaration的所依赖模块的信息地址
  const deps = {};
  //创建一个对象deps,用来收集模块自身引入的依赖,用traverse遍历需要的ImportDeclaration节点做处理,把相对路径转成绝对路径
  traverse(ast, {
    ImportDeclaration({ node }) {
      const dirname = path.dirname(file);
      let absPath = "./" + path.join(dirname, node.source.value);
      absPath = absPath.replace("\\", "/");
      deps[node.source.value] = absPath;
    },
  });

  //3、把es6语法转换成es5
  const { code } = babel.transformFromAst(ast, null, {
    presets: ["@babel/preset-env"],
  });
  const moduleInfo = { file, deps, code };
  console.log(moduleInfo);
  return moduleInfo;
};
getModuleInfo("./src/index.js");

moduleInfo 打印结果为:
在这里插入图片描述
最终把一个模块的代码转化为一个对象形式的信息,这个对象包含文件的绝对路径,文件所依赖模块的信息,以及模块内部经过babel转化后的代码

接下去需要递归查找所有的模块,比如square.js里引用了tip.js。
这个过程也是获取依赖图(dependency graph)的过程

const fs = require("fs");
const path = require("path");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const babel = require("@babel/core");

const getModuleInfo = (file) => {
  //1、把入口文件字符串ast对象
  const body = fs.readFileSync(file, "utf-8");
  const ast = parser.parse(body, {
    //表示我们要解析的是es6模块
    sourceType: "module",
  });

  //2、获取类型为ImportDeclaration的所依赖模块的信息地址
  const deps = {};
  //创建一个对象deps,用来收集模块自身引入的依赖,用traverse遍历需要的ImportDeclaration节点做处理,把相对路径转成绝对路径
  traverse(ast, {
    ImportDeclaration({ node }) {
      const dirname = path.dirname(file);
      let absPath = "./" + path.join(dirname, node.source.value);
      absPath = absPath.replace("\\", "/");
      deps[node.source.value] = absPath;
    },
  });

  //3、把es6语法转换成es5
  const { code } = babel.transformFromAst(ast, null, {
    presets: ["@babel/preset-env"],
  });
  const moduleInfo = { file, deps, code };
  return moduleInfo;
};

//4、递归获取所有模块信息和之间的依赖关系
const parseModules = (file) => {
  //定义依赖图
  const depsGraph = {};
  // 首先获取入口的信息
  const entry = getModuleInfo(file);
  const temp = [entry];
  for (let i = 0; i < temp.length; i++) {
    const item = temp[i];
    const deps = item.deps;
    if (deps) {
      //遍历模块的依赖,递归获取模块信息
      for (const key in deps) {
        if (deps.hasOwnProperty(key)) {
          temp.push(getModuleInfo(deps[key]));
        }
      }
    }
  }
  temp.forEach((moduleInfo) => {
    depsGraph[moduleInfo.file] = {
      deps: moduleInfo.deps,
      code: moduleInfo.code,
    };
  });
  console.log(depsGraph);
  return depsGraph;
};
parseModules("./src/index.js");

获得的depsGraph对象如下:

{
  file: './src/index.js',
  deps: { './add.js': './src/add.js', './square.js': './src/square.js' },
  code: '"use strict";\n' +
    '\n' +
    'var _add = _interopRequireDefault(require("./add.js"));\n' +
    'var _square = require("./square.js");\n' +
    'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n' +
    'var sum = (0, _add["default"])(2, 3);\n' +
    'var sqr = (0, _square.square)(4);\n' +
    'console.log("sum===", sum);\n' +
    'console.log("sqr===", sqr);'
}
PS C:\Users\keyuan04\Desktop\webpack> node bundle.js
{
  './src/index.js': {
    deps: { './add.js': './src/add.js', './square.js': './src/square.js' },
    code: '"use strict";\n' +
      '\n' +
      'var _add = _interopRequireDefault(require("./add.js"));\n' +
      'var _square = require("./square.js");\n' +
      'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n' +
      'var sum = (0, _add["default"])(2, 3);\n' +
      'var sqr = (0, _square.square)(4);\n' +
      'console.log("sum===", sum);\n' +
      'console.log("sqr===", sqr);'
  },
  './src/add.js': {
    deps: {},
    code: '"use strict";\n' +
      '\n' +
      'Object.defineProperty(exports, "__esModule", {\n' +
      '  value: true\n' +
      '});\n' +
      'exports["default"] = void 0;\n' +
      'var _default = function _default(a, b) {\n' +
      '  return a + b;\n' +
      '};\n' +
      'exports["default"] = _default;'
  },
  './src/square.js': {
    deps: { 'tip.js': './src/tip.js' },
    code: '"use strict";\n' +
      '\n' +
      'Object.defineProperty(exports, "__esModule", {\n' +
      '  value: true\n' +
      '});\n' +
      'exports.square = void 0;\n' +
      'var _tip = _interopRequireDefault(require("tip.js"));\n' +
      'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n' +
      'var square = function square(a) {\n' +
      '  console.log(_tip["default"]);\n' +
      '  return a * a;\n' +
      '};\n' +
      'exports.square = square;'
  },
  './src/tip.js': {
    deps: {},
    code: '"use strict";\n' +
      '\n' +
      'Object.defineProperty(exports, "__esModule", {\n' +
      '  value: true\n' +
      '});\n' +
      'exports["default"] = void 0;\n' +
      'var _default = "我是提示-----";\n' +
      'exports["default"] = _default;'
  }
}

我们最终得到的模块分析数据如上图所示,接下来,我们就要根据这里获得的模块分析数据,来生产最终浏览器运行的代码。

上面打印的依赖图可以看到,最终的code里包含exports以及require这样的语法,所以,我们在生成最终代码时,要对exports和require做一定的实现和处理,把依赖图对象中的内容转换成能够执行的代码,以字符串形式输出。 我们把整个代码放在自执行函数中,参数是依赖图对象
把取得入口文件 的code信息,去执行。使用eval函数执行。
最后把生成的内容写入到dist文件中

const fs = require("fs");
const path = require("path");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const babel = require("@babel/core");

const getModuleInfo = (file) => {
  //1、把入口文件字符串ast对象
  const body = fs.readFileSync(file, "utf-8");
  const ast = parser.parse(body, {
    //表示我们要解析的是es6模块
    sourceType: "module",
  });

  //2、获取类型为ImportDeclaration的所依赖模块的信息地址
  const deps = {};
  //创建一个对象deps,用来收集模块自身引入的依赖,用traverse遍历需要的ImportDeclaration节点做处理,把相对路径转成绝对路径
  traverse(ast, {
    ImportDeclaration({ node }) {
      const dirname = path.dirname(file);
      let absPath = "./" + path.join(dirname, node.source.value);
      absPath = absPath.replace("\\", "/");
      deps[node.source.value] = absPath;
    },
  });

  //3、把es6语法转换成es5
  const { code } = babel.transformFromAst(ast, null, {
    presets: ["@babel/preset-env"],
  });
  const moduleInfo = { file, deps, code };
  return moduleInfo;
};

//4、递归获取所有模块信息和之间的依赖关系
const parseModules = (file) => {
  //定义依赖图
  const depsGraph = {};
  // 首先获取入口的信息
  const entry = getModuleInfo(file);
  const temp = [entry];
  for (let i = 0; i < temp.length; i++) {
    const item = temp[i];
    const deps = item.deps;
    if (deps) {
      //遍历模块的依赖,递归获取模块信息
      for (const key in deps) {
        if (deps.hasOwnProperty(key)) {
          temp.push(getModuleInfo(deps[key]));
        }
      }
    }
  }
  temp.forEach((moduleInfo) => {
    depsGraph[moduleInfo.file] = {
      deps: moduleInfo.deps,
      code: moduleInfo.code,
    };
  });
  console.log(depsGraph);
  return depsGraph;
};

//生成最终代码
const bundle = (file) => {
  //把依赖图转字符串,放在自执行函数中执行
  const depsGraph = JSON.stringify(parseModules(file));
  return `(function(graph){
      function require(file){
          var exports = {};
          function absRequire(relPath){
              return require(graph[file].deps[relPath])
          }
          (function(require,exports,code){
              eval(code)
          })(absRequire,exports,graph[file].code)
          return exports
      }
      require('${file}')
    })(${depsGraph})`;
};

const content = bundle("./src/index.js");
fs.rmdirSync("./dist", { recursive: true });
// 写入到dist/bundle.js
fs.mkdirSync("./dist");
fs.writeFileSync("./dist/build.js", content);

最后在index.html中入这个./dist/bundle.js,在控制台就可以看到正确的输出结果
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/369146.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

日本公派访问学者的具体申请流程

公派日本访问学者的具体申请流程&#xff0c;知识人网整理了相关的资料以供大家参考。第一、申请材料一般申请CSC日本访问学者&#xff0c;截止日是每年的1月15号左右&#xff0c;但是学院在1月10号之前就审查材料了。材料包括&#xff1a;CSC网页的报名表&#xff0c;教授邀请…

Stream——集合数据按照某一字段排序

文章目录前言假设业务场景排序前的准备正序排序1、数据集合的判空 Optional.isPresent()2、使用sort排序3、将排序后的数据流转换为list你以为这样就完了&#xff1f;倒序排序前言 之前&#xff0c;针对Stream链式编程中的几个方法做了大致的说明。详情可以参考&#xff1a; J…

限制Linux指定用户访问某个文件夹,禁止其访问指定文件夹

默认情况下用户本身就只能读写执行自己目录下自己创建的文件&#xff0c;自己主目录以外的文件都没有写权限、执行权限&#xff1b;自己主目录以外的目录&#xff0c;则只有r和x权限&#xff0c;没有w权限&#xff0c;对于合法的ssh用户&#xff0c;这样的权限就已经足够了。 否…

Springboot 整合Flowable工作流框架搭建

我们在开发自动化办公软件时经常会遇到各种审批流程功能&#xff0c;这个使用就需要使用到工作流引擎。目前主流的工作流引擎有Activiti、Flowable、camunda&#xff0c;其中Flowable是在Activiti的基础上开发出来的&#xff0c;基于BPMN2.0协议&#xff0c;它包括 BPMN&#x…

Convolutional Neural Networks for Sentence Classification

摘要 We report on a series of experiments with convolutional neural networks (CNN) trained on top of pre-trained word vectors for sentence-level classification tasks. We show that a simple CNN with little hyperparameter tuning and static vectors achieves e…

安装ROS+ROS命令行工具的使用

1.添加ROS软件源 $ sudosh -c echo "deb http;/packages.ros.org/ros/ubuntu $(sb_release -sc) main">/etc/apt/sources.list.d/ros-latest.list 2.添加密钥 $ sudo apt-key adv --keyserver hkp:/ keyserver.ubuntu.com:80 --recv-key C1CF6E31E6BADE8868B17…

我希望在 26 岁时知道的 36 岁时知道的职业作弊代码

当您开始抓住您不具备资格的机会时&#xff0c;您的职业生涯就会发展。我像关在笼子里的狮子一样被困在金融事业中。然后通过一系列离奇的事件&#xff0c;我发明了自己的工作。这使我获得了难得的机会&#xff0c;并建立了最终取代我工作的在线业务。这些作弊码让一些人称之为…

I/O 多路复用:select/poll/epoll

url&#xff1a;9.2 I/O 多路复用&#xff1a;select/poll/epoll | 小林coding &#xff08;仅供自己学习使用&#xff09; 为什么要使用I/O多路复用技术 TCP Socket 调用流程是最简单、最基本的&#xff0c;它基本只能一对一通信&#xff0c;因为使用的是同步阻塞的方…

Linux作为主力机--Manjaro 22.0.4

1、对操作系统的看法 个人是做软件开发的&#xff0c;已经使用Manjaro作为主力机两年多了&#xff0c;真的是特别喜欢这个操作系统。经过两年的打磨&#xff0c;个人16年的惠普老电脑加上这个Manjaro 22.0.4操作系统完全可以再战五年&#xff0c;完全满足日常的办公使用&#…

ctf pwn基础-3

学习pwn的第三天&#xff0c;今天是ret2text。 目录 基础 实例讲解 实例讲解2 基础 ret2text就是ROP中最简单的&#xff0c;然后的意思就是我们利用栈溢出&#xff0c;来修改eip的值&#xff0c;让他输出的时候&#xff0c;输出我们想要执行的本身已有的代码&#xff0c;通常…

利用Splunk构建SOC-SOC建设漫谈及splunk的角色

零、免喷符 SOC部门小菜鸟一枚&#xff0c;此乃自闭学安全的笔记记录&#xff0c;行文潦草&#xff0c;随性笔记。 通过上一篇的勒索病毒案例&#xff0c;已经了解到Splunk的强大之处。Splunk那么死贵&#xff0c;他的角色是怎样的&#xff0c;又是怎么和安全及SOC联系起来的…

基于BP神经网络的性别识别,BP神经网络详细原理,自编码神经网络代码,神经网络案例之18

目标 背影 BP神经网络的原理 BP神经网络的定义 BP神经网络的基本结构 BP神经网络的神经元 BP神经网络的激活函数&#xff0c; BP神经网络的传递函数 数据 神经网络参数 基于BP神经网络 性别识别的MATLAB代码 效果图 结果分析 展望 背影 男人体内蛋白质比例大&#xff0c;女生…

Windows下 IDEA编译调试 hive2.3.9

Windows下 IDEA编译调试 hive2.3.9 环境 IDEA 2021.2 JDK1.8&#xff08;试过用高版本的JDK17编译&#xff0c;不兼容编译不过&#xff09; 一个Hadoop集群&#xff0c;涉及配置文件core-site.xml&#xff0c;hdfs-site.xml&#xff0c;yarn-site.xml&#xff0c;mapred-sit…

并发与多线程

目录 第一节 并发基本概念及实现&#xff0c;进程&#xff0c;线程基本概念 &#xff08;1&#xff09;并发&#xff0c;进程&#xff0c;线程的基本概念和综述 &#xff08;1.1&#xff09;并发 &#xff08;1.2&#xff09;可执行程序 &#xff08;1.3&#xff09;进程 …

物理服务器与云服务器备份相同吗?

自从云计算兴起以来&#xff0c;服务器备份已经从两阶段的模拟操作演变为由云服务器备份软件执行的复杂的多个过程。但是支持物理服务器和虚拟服务器之间的备份相同吗?主要区别是什么?我们接下来将详细讨论这个问题。 物理服务器与云服务器备份的区别 如果您不熟悉虚拟服务器…

qt QCustomPlot学习

QCustomPlot 是一个基于Qt的画图和数据可视化C控件。QCustomPlot 致力于提供美观的界面&#xff0c;高质量的2D画图、图画和图表&#xff0c;同时为实时数据可视化应用提供良好的解决方案。 该绘图库专注于制作美观、出版物质量高的2D绘图、图形和图表&#xff0c;并为实时可视…

数据库专题

请简洁描述 MySQL 中 InnoDB 支持的四种事务隔离级别名称&#xff0c;以及逐级之间的区别&#xff1f; 默认隔离级别 mysql repeatable-read oracle read-committed 脏读&#xff1a;不可重复读&#xff1a;幻读&#xff1a; CHAR 和 VARCHAR 的区别&#xff1f;…

公众号运营之竞品分析,教你拆解公众号

知己知彼&#xff0c;百战不殆&#xff0c;公众号运营亦是如此。 当运营者只关注自己账号的时候&#xff0c;很容易陷入某个误区中出不来。这个时候就要拓宽我们的视野&#xff0c;多去看看“外面的世界”&#xff0c;不要只局限于自己的一片小天地中。 看看同领域优秀公众号…

stm32f407探索者开发板(二十二)——通用定时器基本原理讲解

文章目录一、三种定时器的区别二、通用定时器特点2.1 功能特点描述2.2 计数器模式三、通用定时器工作过程四、附一、三种定时器的区别 STM32F40x系列总共最多有14个定时器 三种&#xff08;4&#xff09;STM32定时器区别 二、通用定时器特点 2.1 功能特点描述 STM3 F4的通…

PHY设备驱动

1. 概述 MAC控制器的驱动使用的是platform总线的连接方式&#xff0c;PHY设备驱动是基于device、driver、bus的连接方式。 其驱动涉及如下几个重要部分&#xff1a; 总线 - sturct mii_bus (mii stand for media independent interface) 设备 - struct phy_device 驱动 - struc…