简易的打包器--webpack打包原理

2023-11-16

手写一个简单的类似webpack的打包器

打包流程说明:

  • 定义依赖分析函数,通过读取文件内容,分析得到该文件导入的依赖项
    • code => AST => 得到导入声明,记录导入声明中的依赖项路径 => AST->code => 返回记录当前文件filename、依赖项dependencies和转译后的code的对象
  • 定义分析依赖图列表的函数,传入项目的入口文件,递归调用依赖分析函数,得到所有文件的依赖关系图列表,返回该列表。
    • 核心在于如何递归调用依赖分析函数,这里使用广度优先的算法,通过对根节点的分析开始,依次构建得到下一层级的节点,对这一层级的节点按顺序分析,得到下一层级节点再次按顺序分析,直到无法再得到下一层级节点为止。
    • 每一轮的依赖分析,都将依赖项push到列表中。这样保证了按顺序的广度优先分析。
  • 根据已经生成的依赖图列表,生成可在浏览器端运行的代码,这里如果使用了@babel/core将AST转换为代码,则需要定义require函数和exports对象。
    • 整个代码都需要放在一个IIFE中执行,IIFE传入依赖图列表
    • 定义require函数,用来加载模块(依赖的文件代码)并执行,将结果挂载到exports对象上。
    • 依赖图列表的每个元素都包含有自身的代码以及依赖列表,自身的代码需要放在IIFE中使用eval()执行

使用的npm包说明

  • cli-highlight包:用于在终端高亮显示信息
  • @babel/parser:分析JavaScript文件,解析为AST(JavaScript对象)
  • @babel/traverse: 与@babel/parser一起使用,遍历AST,对其中的节点进行操作,如更新、删除等等
    • traverse(ast, options): 其中,options是一个选项对象,包含了一系列Hooks函数
    • 对特定类型的节点可以使用特定的函数,节点类型参考@babel/types
      • 对ESModule的导入节点使用options.ImportDeclaration(path) {},path是参数,其中path.node是指向导入声明的节点
      • 对函数声明的节点使用options.FunctionDeclaration(path) {}
      • 进入节点使用options.enter(path) {}
      • 退出节点使用options.exit(path) {}
  • @babel/core: 将AST转换为JavaScript代码,需要配合@babel/preset-env

代码如下

项目根目录下bundle.js文件

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 isEmptyObject = (obj) => {
  for(key in obj) {
    return false
  }
  return true
}
// 读取文件内容,分析依赖
const moduleAnalyzer = (filename) => {
  // 读取入口文件内容
  const content = fs.readFileSync(filename, 'utf-8')
  // 解析文件内容,转换为AST
  const ast = parser.parse(content, {
    sourceType: 'module'
  })
  // 声明一个用来存储依赖模块的对象,键为导入模块的相对路径,值为导入模块的绝对路径(相对于项目根目录)
  const dependencies = Object.create(null)
  // 遍历AST节点,获取导入声明的节点,将导入声明的节点的source的value属性值存储到依赖对象中
  traverse(ast, {
    ImportDeclaration({ node }) {
      // 获取入口文件的所在目录
      const dirname = path.dirname(filename)
      // 拼接路径, node.source.value是导入语句中的路径部分
      const newFile = './' + path.join(dirname, node.source.value)
      // 相对路径和绝对路径作为键值对一起存储
      dependencies[node.source.value] = newFile
    }
  })
  // 分析更新AST后,使用babel.transformFromAstSync将AST转换为代码code
  // 将ES6语法转为浏览器能运行的语法
  const { code } = babel.transformFromAstSync(ast, null, {
    presets: ['@babel/preset-env']
  })
  const res = {
    filename,
    code
  }

  // 返回分析结果,包含了入口文件、依赖对象和入口文件经过转译后的代码
  // {
  //   filename,
  //   code,
  //   dependencies: {
  //     '相对路径': '绝对路径(以项目根目录为起点)'
  //   }
  // }
  if(isEmptyObject(dependencies)) {
    return res
  } else {
    return Object.assign(res, { dependencies })
  }
}

// 构建依赖关系图谱列表
const makeDependenciesGraph = (entry) => {
  const entryModule = moduleAnalyzer(entry)
  // 
  const graphList = [ entryModule ]
  for(let i = 0; i < graphList.length; i ++) {
    const item = graphList[i]
    const { dependencies } = item
    if(dependencies) {
      for(dependency in dependencies) {
        const res = moduleAnalyzer(dependencies[dependency])
        graphList.push(res)
      }
    }
  }
  const graph = {}
  graphList.forEach(item => {
    graph[item.filename] = {
      dependencies: item.dependencies,
      code: item.code
    }
  })
  return graph
  // graph形如
  // {
  //   filename1: {
  //     dependencies: {},
  //     code: ''
  //   },
  //   filename2: {
  //     dependencies: {},
  //     code: ''
  //   },
  //   ...
  // }
}

// 从依赖图谱列表生成浏览器可用代码
const generateCode = (entry) => {
  const graph = makeDependenciesGraph(entry)
  // 使用JSON.stringify为了避免下面用${graph}时变为'[object Object]'
  // 实际这段字符串在浏览器中作为JavaScript代码运行时,graphCode实际上就是一个对象
  const graphCode = JSON.stringify(graph)
  // 返回的代码要包含在IIFE中
  return `
    (function (graph) {
      function require(module) {
        // localRequire函数用来将加载相对路径转换为加载绝对路径后返回结果
        // 主要是由于这里存储的键为绝对路径,在依赖图中只能利用绝对路径来加载模块
        function localRequire(relativePath) {
          return require(graph[module].dependencies[relativePath])
        }
        // 在定义exports时,由于是在IIFE之前,所以赋值语句必须要有分号作为结尾,否则要出错
        var exports = {};
        (function(require, exports, code) {
          eval(code)
        })(localRequire, exports, graph[module].code)
        return exports
      }
      require('${entry}')
    })(${graphCode})
  `
}

const code = generateCode('./src/index.js')

// 将code写到'./dist/bundle.js'文件中
fs.writeFile('./dist/bundle.js', code, (err) => {
  if(err) {
    fs.mkdir('./dist', (err) => {
      if(err) {
        console.log('fail')
      }
      fs.writeFileSync('./dist/bundle.js', code)
    })
  }
})

更多资料见sharejs

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

简易的打包器--webpack打包原理 的相关文章

随机推荐

  • 51单片机按键控制数码管0~9_(51单片机)课设项目1-按键控制步进电机转向、转速、启停。...

    总体设计方案 硬件部分实现电机转动和数码管显示 包括控制开关模块 电机转动模块 数码管显示模块 软件部分实现对步进电机的控制功能 主要设计思想通过控制程序的开关来控制电机的转动启停 方向 速度 电源驱动AT89C51单片机 在单片机中装载程
  • git cz配置)

    git cz配置 npm install g commitizen echo path cz conventional changelog gt czrc
  • 基于 Web 端的人脸识别身份验证

    效果展示 人脸识别效果图 前言 近些年来 随着生物识别技术的逐渐成熟 基于深度学习的人脸识别技术取得了突破性进展 准确率显著提高 现阶段 人脸识别身份验证作为非常重要的身份验证方式 已被广泛的应用于诸多行业和领域 例如 支付宝付款 刷脸签到
  • 主流的深度学习优化方法(SGD,SGDM,Adagrad,RMSProp,Adam)

    文章目录 0 前言 1 SGD 2 SGD with momentum SGDM 3 Adagrad 4 RMSProp root mean square prop 5 Adam 0 前言 介绍主流的深度学习优化方法 SGD SGD wit
  • 登陆远程服务器的Tomcat管理页面

    在远程服务器上安装了一个tomcat8 5 配置好用户后重新启动tomcat 发现 8080可以访问 登录管理页面报403访问受限 详细信息如下 You are not authorized to view this page By def
  • C语言代码写直角三角形

    include
  • 关于BUCK-BOOST电路的负压生成

    在一些实际应用中 我们时常会用到需要提供负压的场合 针对负压的设计 小白之前就讲述过一种方式 即采用charge pump的方法 然而呢 由于其带负载能力的不足 往往在一些设计中不被采用 同时 小白之前还见到过呦采用变压器隔离绕组反接的方式
  • 斐波那契数列递归算法和非递归算法以及其时间复杂度分析

    1 在学习数据结构这门课的过程中 发现斐波那契数列的递归算法以及非递归算法 以及其时间复杂度分析是一个小难点 所以特别总结一下 斐波那契数列的表达式 Fibonacci数列简介 F 1 1 F 2 1 F n F n 1 F n 2 n g
  • 配置Flutter开发环境

    安装 Flutter SDK 下载好 Flutter SDK 压缩包 flutter windows 3 0 3 stable zip 将 Flutter SDK 压缩包 解压到一个目录下 设置环境变量 FLUTTER STORAGE BA
  • Lua : 流程控制也没啥,if/嵌套仅需知

    目录 1 if else end 语法 2 if elseif else end 语法 3 if的嵌套 Lua中流程控制 使用if 和if的嵌套就好 当然goto也可以用于流程控制 其实我在想 C C 中如此好用的switch 语法 Lua
  • Java研发京东4面:事务隔离+乐观锁+HashMap+秒杀设计+微服务(面试真题)

    在朋友圈看到一个以前的同事这些天去京东面试了 就让他整理了一下面试官问了些他什么 然后就有了这篇文章 这篇文章主要介绍了 Java岗 京东的四次面试 面试题全为面试真题 一面 基础面 约1小时 二面 问数据库较多 三面 综合面 约一个小时
  • 6.STM32中断优先级管理

    1 中断 stm32的芯片通常有90多个以上的中断 具有16级可编程的中断优先级 2 中断管理方法 1 首先对STM32中断进行分组 有组0 4 同时对每一个中断设置一个抢占优先级和一个响应优先级值 分组配置是在寄存器SCB gt AIRC
  • 【Python】工程与包(2)

    创建工程及第三方包管理 New environment using 新建的项目里有一个venv virtualenv 文件夹 专门存放本项目所依赖的第三方模块 Existing interpreter 表示新建的项目所依赖的第三方模块是存放
  • 推荐工具url

    https www processon com diagrams 很好的web画图工具 https www tapd cn letters from top nav worktable v2 产品 研发 测试工具
  • uboot环境变量的讲解

    1 环境变量的作用域和全局变量相同 环境变量的生命周期为一旦设置好环境变量并保存好后 下次开机还存在 而全局变量在关机后就灭亡了 下次开机产生了一个新的全局变量 2 环境变量如何参与程序运行 1 环境变量有2份 分别在Flash和DDR中
  • hadoop3.3.1单机版环境搭建详细流程记录

    1 在centos7中创建必要的目录 2 上传JDK安装包到tools目录 3 解压JDK到 opt server 目录 tar zxvf jdk 8u221 linux x64 tar gz C opt server 4 vim 未找到命
  • 视音频编解码技术零基础学习方法

    一直想把视音频编解码技术做一个简单的总结 可是苦于时间不充裕 一直没能完成 今天有着很大的空闲 终于可以总结一个有关视音频技术的入门教程 可以方便更多的人学习从零开始学习视音频技术 需要注意的是 本文所说的视音频技术 指的是理论层面的视音频
  • Python-Jenkins 在 Jenkins 中的应用

    Author rab Python 版本 3 9 Jenkins 版本 2 409 官方文档 https python jenkins readthedocs io en latest 目录 前言 一 案例 1 1 管理 Jenkins V
  • Java远程调试(Remote Debug)方法

    Java远程调试的原理是两个VM之间通过debug协议进行通信 然后以达到远程调试的目的 两者之间可以通过socket进行通信 首先被debug程序的虚拟机在启动时要开启debug模式 启动debug监听程序 jdwp是Java Debug
  • 简易的打包器--webpack打包原理

    手写一个简单的类似webpack的打包器 打包流程说明 定义依赖分析函数 通过读取文件内容 分析得到该文件导入的依赖项 code gt AST gt 得到导入声明 记录导入声明中的依赖项路径 gt AST gt code gt 返回记录当前