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

手写一个简单的类似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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值