webpack 收集依赖、打包输出精简实现

2023-11-20

安装babel插件

  • 由于ES6转ES5中需要用到babel,所以要用到一下插件
npm install @babel/core @babel/parser @babel/traverse @babel/preset-env --save-dev

目录结构及代码:

- demo
    - entry.js
    - message.js
    - name.js
- bundler.js //最后打包的文件

// entry.js
import message from './message.js';
console.log(message);

// message.js
import {name} from './name.js';
export default `hello ${name}!`;

// name.js
export const name = 'world';

读取文件信息,获取当前js文件的依赖关系

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

let ID = 0;
//读取文件信息,并获得当前js文件的依赖关系
function createAsset(filename) {
  //获取文件,返回值是字符串
  const content = fs.readFileSync(filename, "utf-8");

  //讲字符串为ast(抽象语法树, 这个是编译原理的知识,说得简单一点就是,可以把js文件里的代码抽象成一个对象,代码的信息会存在对象中)
  //babylon 这个工具是是负责解析字符串并生产ast。
  const ast = babylon.parse(content, {
    sourceType: "module",
  });

  //用来存储 文件所依赖的模块,简单来说就是,当前js文件 import 了哪些文件,都会保存在这个数组里
  const dependencies = [];

  //遍历当前ast(抽象语法树)
  traverse(ast, {
    //找到有 import语法 的对应节点
    ImportDeclaration: ({ node }) => {
      //把当前依赖的模块加入到数组中,其实这存的是字符串,
      //例如 如果当前js文件 有一句 import message from './message.js',
      //'./message.js' === node.source.value
      dependencies.push(node.source.value);
    },
  });

  //模块的id 从0开始, 相当一个js文件 可以看成一个模块
  const id = ID++;

  //这边主要把ES6 的代码转成 ES5
  const { code } = babel.transformFromAstSync(ast, null, {
    presets: ["@babel/preset-env"],
  });

  return {
    id,
    filename,
    dependencies,
    code,
  };
}

在这里插入图片描述

广度遍历获取所有依赖图

//从入口开始分析所有依赖项,形成依赖图,采用广度遍历
function createGraph(entry) {
  const mainAsset = createAsset(entry);
  console.log(mainAsset);

  //既然要广度遍历肯定要有一个队列,第一个元素肯定是 从 "./demo/entry.js" 返回的信息
  const queue = [mainAsset];

  for (const asset of queue) {
    //获取文件夹路径
    const dirname = path.dirname(asset.filename);

    //新增一个属性来保存子依赖项的数据
    //保存类似 这样的数据结构 --->  {"./message.js" : 1}
    asset.mapping = {};

    //采用广度遍历
    asset.dependencies.forEach((relativePath) => {
      const absolutePath = path.join(dirname, relativePath);

      //获得子依赖(子模块)的依赖项、代码、模块id,文件名
      const child = createAsset(absolutePath);

      //给子依赖项赋值,
      asset.mapping[relativePath] = child.id;

      //将子依赖也加入队列中,广度遍历
      queue.push(child);
    });
  }
  return queue;
}

在这里插入图片描述

  • mapping这个字段是把当前模块依赖的文件名称 和 模块的id 做一个映射,目的是为了更方便查找模块

生成浏览器可执行代码

  • 其实bundle函数就是返回我们构造的字符串,拿到字符串,我们把字符串导出成bundle.js。
//根据生成的依赖关系图,生成对应环境能执行的代码,目前是生产浏览器可以执行的
function bundle(graph) {
  let modules = "";

  console.log(graph);

  //循环依赖关系,并把每个模块中的代码存在function作用域里
  //转换依赖图格式 => {id:[fn,mapping],...}
  //会作为最后生成代码的入参
  graph.forEach((mod) => {
    modules += `${mod.id}:[
      function (require, module, exports){
        ${mod.code}
      },
      ${JSON.stringify(mod.mapping)},
    ],`;
  });

  //require, module, exports 是 cjs的标准不能再浏览器中直接使用,所以这里模拟cjs模块加载,执行,导出操作。
  const result = `
    (function(modules){
      //创建require函数, 它接受一个模块ID(这个模块id是数字0,1,2) ,它会在我们上面定义 modules 中找到对应是模块.
      function require(id){
        const [fn, mapping] = modules[id];
        function localRequire(relativePath){
          //根据模块的路径在mapping中找到对应的模块id
          return require(mapping[relativePath]);
        }
        const module = {exports:{}};
        //执行每个模块的代码。
        fn(localRequire,module,module.exports);
        return module.exports;
      }
      //执行入口文件,
      require(0);
    })({${modules}})
  `;

  return result;
}

const graph = createGraph("./demo/entry.js");
const ret = bundle(graph);

// 打包生成文件
fs.writeFileSync("./bundle.js", ret);

最终生成的可执行内容:

(function (modules) {
  //创建require函数, 它接受一个模块ID(这个模块id是数字0,1,2) ,它会在我们上面定义 modules 中找到对应是模块.
  function require(id) {
    const [fn, mapping] = modules[id];
    function localRequire(relativePath) {
      //根据模块的路径在mapping中找到对应的模块id
      return require(mapping[relativePath]);
    }
    const module = { exports: {} };
    //执行每个模块的代码。
    fn(localRequire, module, module.exports);
    return module.exports;
  }
  //执行入口文件,
  require(0);
})({
  0: [
    function (require, module, exports) {
      "use strict";

      var _message = _interopRequireDefault(require("./message.js"));
      function _interopRequireDefault(obj) {
        return obj && obj.__esModule ? obj : { default: obj };
      }
      console.log(_message["default"]);
    },
    { "./message.js": 1 },
  ],
  1: [
    function (require, module, exports) {
      "use strict";

      Object.defineProperty(exports, "__esModule", {
        value: true,
      });
      exports["default"] = void 0;
      var _name = require("./name.js");
      var _default = "hello ".concat(_name.name, "!");
      exports["default"] = _default;
    },
    { "./name.js": 2 },
  ],
  2: [
    function (require, module, exports) {
      "use strict";

      Object.defineProperty(exports, "__esModule", {
        value: true,
      });
      exports.name = void 0;
      var name = "world";
      exports.name = name;
    },
    {},
  ],
});

代码示例:

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

let ID = 0;
//读取文件信息,并获得当前js文件的依赖关系
function createAsset(filename) {
  //获取文件,返回值是字符串
  const content = fs.readFileSync(filename, "utf-8");

  //讲字符串为ast(抽象语法树, 这个是编译原理的知识,说得简单一点就是,可以把js文件里的代码抽象成一个对象,代码的信息会存在对象中)
  //babylon 这个工具是是负责解析字符串并生产ast。
  const ast = babylon.parse(content, {
    sourceType: "module",
  });

  //用来存储 文件所依赖的模块,简单来说就是,当前js文件 import 了哪些文件,都会保存在这个数组里
  const dependencies = [];

  //遍历当前ast(抽象语法树)
  traverse(ast, {
    //找到有 import语法 的对应节点
    ImportDeclaration: ({ node }) => {
      //把当前依赖的模块加入到数组中,其实这存的是字符串,
      //例如 如果当前js文件 有一句 import message from './message.js',
      //'./message.js' === node.source.value
      dependencies.push(node.source.value);
    },
  });

  //模块的id 从0开始, 相当一个js文件 可以看成一个模块
  const id = ID++;

  //这边主要把ES6 的代码转成 ES5
  const { code } = babel.transformFromAstSync(ast, null, {
    presets: ["@babel/preset-env"],
  });

  return {
    id,
    filename,
    dependencies,
    code,
  };
}

//从入口开始分析所有依赖项,形成依赖图,采用广度遍历
function createGraph(entry) {
  const mainAsset = createAsset(entry);
  console.log(mainAsset);

  //既然要广度遍历肯定要有一个队列,第一个元素肯定是 从 "./demo/entry.js" 返回的信息
  const queue = [mainAsset];

  for (const asset of queue) {
    const dirname = path.dirname(asset.filename);

    //新增一个属性来保存子依赖项的数据
    //保存类似 这样的数据结构 --->  {"./message.js" : 1}
    asset.mapping = {};

    //采用广度遍历
    asset.dependencies.forEach((relativePath) => {
      const absolutePath = path.join(dirname, relativePath);

      //获得子依赖(子模块)的依赖项、代码、模块id,文件名
      const child = createAsset(absolutePath);

      //给子依赖项赋值,
      asset.mapping[relativePath] = child.id;

      //将子依赖也加入队列中,广度遍历
      queue.push(child);
    });
  }
  return queue;
}

//根据生成的依赖关系图,生成对应环境能执行的代码,目前是生产浏览器可以执行的
function bundle(graph) {
  let modules = "";

  console.log(graph);

  //循环依赖关系,并把每个模块中的代码存在function作用域里
  graph.forEach((mod) => {
    modules += `${mod.id}:[
      function (require, module, exports){
        ${mod.code}
      },
      ${JSON.stringify(mod.mapping)},
    ],`;
  });

  //require, module, exports 是 cjs的标准不能再浏览器中直接使用,所以这里模拟cjs模块加载,执行,导出操作。
  const result = `
    (function(modules){
      //创建require函数, 它接受一个模块ID(这个模块id是数字0,1,2) ,它会在我们上面定义 modules 中找到对应是模块.
      function require(id){
        const [fn, mapping] = modules[id];
        function localRequire(relativePath){
          //根据模块的路径在mapping中找到对应的模块id
          return require(mapping[relativePath]);
        }
        const module = {exports:{}};
        //执行每个模块的代码。
        fn(localRequire,module,module.exports);
        return module.exports;
      }
      //执行入口文件,
      require(0);
    })({${modules}})
  `;

  return result;
}

const graph = createGraph("./demo/entry.js");
const ret = bundle(graph);

// 打包生成文件
fs.writeFileSync("./bundle.js", ret);

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

webpack 收集依赖、打包输出精简实现 的相关文章

随机推荐

  • IntelliJ IDEA 插件(Java 插件)简介(一)

    IntelliJ IDEA 被认为是当前Java开发效率最快的 IDE 工具 它整合了开发过程中众多的实用功能 具有丰富的插件支持 能够极大地提高开发效率 一 Alibaba Java Coding Guidelines Alibaba J
  • 快速学习Python基础知识(3)

    一 输入输出 1 1 input输入函数的使用 input函数 是获取键盘输出 保存成一个字符串 注意 input 函数的返回值是一个字符串类型 即便你输入的是数字 返回的也会以一个字符串的形式返回给我们 inputStr input 提示
  • 电路的频率响应

    文章目录 Frequency response Impedence Transfer function The Decibel scale First order circuits Series RL and RC circuits Ser
  • 3. C++ 数据类型

    目录 1 七种基本的 C 数据类型 2 C 中的变量定义 3 C 中的变量声明 4 C 变量作用域 局部变量 全局变量 1 七种基本的 C 数据类型 各种变量类型在内存中存储值时需要占用的内存 以及该类型的变量所能存储的最大值和最小值 注意
  • @ControllerAdvice 和 @ExceptionHandler注解处理全局异常

    ControllerAdvice 和 ExceptionHandler注解处理全局异常 处理全局统一异常 处理service层抛出异常的方法 异常体系 处理全局统一异常 在构建RestFul接口的今天 我们一般会限定好返回数据的格式 有利于
  • Nmap使用方法

    文章目录 1 Nmap简介 2 Nmap使用方法 3 扫描技术 4 端口指定和扫描顺序 5 举例 5 1 简单扫描 nmap ip 5 2 全面扫描 nmap A ip 5 3 探测指定端口的开放状态 5 4 探测N个最有可能开放的端口 5
  • 升级SQLite数据库

    一 步骤 1 在之前的基础上添加一张Category表 在onCreate 方法中执行建CREATE CATEGORY表语句 2 然后在onUpgrade 中执行两条drop语句 发现数据库表存在 就将已经存在的表格删除 再在onCreat
  • CISSP一次通过指南(文末附福利)

    2017年12月19日 在上海黄浦区汉口路亚洲大厦17层通过了CISSP认证考试 拖拉了一年 终于成绩还算令人满意 为攒人品将自己一年多的复习心得和大家分享 希望能够帮到需要考证的朋友 本文作者 i春秋签约作家 tinyfisher 欢迎与
  • 【JavaWeb】网络原理初识

    网络原理初识 计算机网络的历史 局域网和广域网 网络组件中的重要设备 网络通信基础 基本概念 协议分层 OSI七层模型 TCP IP五层 或四层 模型 封装和分用 发送方 接收方 三层转发和二层转发 计算机网络的历史 计算机最初是为了打仗而
  • python基础之数据类型知识(1)

    注释 注解 解释 说明文字而已 特征 注释只是用于说明的文字不会影响内容本身 作用 1 用于添加说明文字 方便阅读 2 用于调试程序 排查错误 分类 单行注释 多行注释 内容 或者 内容 代码 print hello world print
  • 利用JS实现简单的todoList(记事本)效果

    目录 1 实现效果展示 2 HTML代码 3 CSS代码 4 Javascript代码 该记事本程序利用HTML CSS JavaScript前端三大框架来实现 实现了记事本的添加 已完成和删除待办事项的基本功能 下面是程序实现的全部代码
  • 交通类SCI顶级期刊排名

    大多数期刊还是在IEEE http ieeexplore ieee org 或者ScienceDirect https www sciencedirect com 上面的 如果你是学生的话 可能从你们学校的图书馆可以进到这些网站去下载论文
  • Vue js引用警告 “export ‘default‘ (imported as ‘xxx‘) was not found

    问题原因 ES6 编译器识别问题 如果在public js这样写会有警告export default imported as xxx was not found export const myMixin 解决办法 修改组件中引用js的地方
  • Linux TCP链接查看和调整

    查看Linux的TCP连接数的方法如下 统计80端口连接数 netstat nat grep i 80 wc l 统计httpd协议连接数 ps ef grep httpd wc l 统计已连接上的 状态为 established 的TCP
  • Java终止线程的三种方式

    停止一个线程通常意味着在线程处理任务完成之前停掉正在做的操作 也就是放弃当前的操作 在 Java 中有以下 3 种方法可以终止正在运行的线程 使用退出标志 使线程正常退出 也就是当 run 方法完成后线程中止 使用 stop 方法强行终止线
  • R----dplyr包介绍学习

    dplyr包 plyr包的替代者 专门面对数据框 将ddplyr转变为更易用的接口 gt 来自dplyr包的管道函数 其作用是将前一步的结果直接传参给下一步的函数 从而省略了中间的赋值步骤 可以大量减少内存中的对象 节省内存 可惜的是应用范
  • 【理解springboot自动装配原理】

    理解springboot自动装配原理 最近读了小马哥 mercyblitz Springboot编程思想 核心篇 有了一些心得和感悟 分享给大家 1 官网介绍了激活自动装配的方法 文档提到激活自动化装配的注解 EnableAutoConfi
  • DAS、SAN、NAS存储连接方式详解

    1 直接访问存储DAS Direct Access Storage DAS将存储设备通过SCSI接口或光纤通道直接连接到一台计算机上 代表为磁盘阵列柜RAID 磁盘阵列柜是由多个硬盘按照不同的方式组合成一个大型的磁盘组 利用个别磁盘提供数据
  • Spring的xml文档配置

    1基于XML的注解配置
  • webpack 收集依赖、打包输出精简实现

    文章目录 安装babel插件 读取文件信息 获取当前js文件的依赖关系 广度遍历获取所有依赖图 生成浏览器可执行代码 安装babel插件 由于ES6转ES5中需要用到babel 所以要用到一下插件 npm install babel cor