Webpack之AST、Babel、依赖

什么是AST

import { parse } from "@babel/parser"
import traverse from "@babel/traverse"
import generate from "@babel/generator"

const code = `let a = 'let'; let b = 2`
const ast = parse(code, { sourceType: 'module' })
console.log(ast);

我们通过命令打出这个ast的log看看:

node -r ts-node/register --inspect-brk xx.ts 

在控制台我们看到ast长啥样: 

ast就是const code = `let a = 'let'; let b = 2`的树状结构,对象中详细地描述了变量声明为let,声明的值是多少。通过这个对象我们就能做一些替换,例如将ES6转化ES5(kind = ’let‘ 转化为 kind = 'var')。

为啥不用正则做es6 => es5 的替换呢? 

很难,很容易出错。我们需要用一种方式来识别每个单词的意思,才能做到只修改用于声明变量的 let,并且AST能够明确地告诉你每个let的意思。

所以,JS的编译必需要AST。

 

Babel相关使用

import { parse } from "@babel/parser"
import traverse from "@babel/traverse"
import generate from "@babel/generator"

const code = `let a = 'let'; let b = 2`
const ast = parse(code, { sourceType: 'module' })
traverse(ast, {
  enter: item => {
    if(item.node.type === 'VariableDeclaration'){
      if(item.node.kind === 'let'){
        item.node.kind = 'var'
      }
    }
  }
})
const result = generate(ast, {}, code)
console.log(result.code)

bebel的原理:

  1. parse:把代码code变成AST
  2. traverse:遍历AST进行修改
  3. generate:把AST变成代码code2

等于:code => 1 => AST => 2 => AST2 => 3 => code2

如果求方便可直接使用 babel.transformSync,可以将code直接变成code2,即将所有高级语法转为ES5。@babel/preset-env内置了很多转换规则。

 

通过AST来分析JS文件的依赖关系

分析JS文件的依赖关系

import { parse } from "@babel/parser"
import traverse from "@babel/traverse"
import { readFileSync } from 'fs'
import { resolve, relative, dirname } from 'path';

// 设置根目录
const projectRoot = resolve(__dirname, 'project_1')
// 类型声明
type DepRelation = { [key: string]: { deps: string[], code: string } }
// 初始化一个空的 depRelation,用于收集依赖
const depRelation: DepRelation = {}

// 将入口文件的绝对路径传入函数,如 D:\demo\fixture_1\index.js
collectCodeAndDeps(resolve(projectRoot, 'index.js'))

console.log(depRelation, 'done')

function collectCodeAndDeps(filepath: string) {
  const key = getProjectPath(filepath) // 文件的项目路径,如 index.js
  // 获取文件内容,将内容放至 depRelation
  const code = readFileSync(filepath).toString()
  // 初始化 depRelation[key]
  depRelation[key] = { deps: [], code: code }
  // 将代码转为 AST
  const ast = parse(code, { sourceType: 'module' }) 
  // 分析文件依赖,将内容放至 depRelation
  traverse(ast, {
    enter: path => {
      if (path.node.type === 'ImportDeclaration') {
        // path.node.source.value 往往是一个相对路径,如 ./a.js,需要先把它转为一个绝对路径
        const depAbsolutePath = resolve(dirname(filepath), path.node.source.value)
        // 然后转为项目路径
        const depProjectPath = getProjectPath(depAbsolutePath)
        // 把依赖写进 depRelation
        depRelation[key].deps.push(depProjectPath)
      }
    }
  })
}
// 获取文件相对于根目录的相对路径
function getProjectPath(path: string) {
  return relative(projectRoot, path).replace(/\\/g, '/')
}

最终得出依赖关系,通过hash来存储依赖关系:

通过递归分析嵌套依赖

如果依赖当中还有依赖的话,那么我们就要使用递归:

function collectCodeAndDeps(filepath: string) {
  const key = getProjectPath(filepath) // 文件的项目路径,如 index.js
  // 获取文件内容,将内容放至 depRelation
  const code = readFileSync(filepath).toString()
  // 初始化 depRelation[key]
  depRelation[key] = { deps: [], code: code }
  // 将代码转为 AST
  const ast = parse(code, { sourceType: 'module' }) 
  // 分析文件依赖,将内容放至 depRelation
  traverse(ast, {
    enter: path => {
      if (path.node.type === 'ImportDeclaration') {
        // path.node.source.value 往往是一个相对路径,如 ./a.js,需要先把它转为一个绝对路径
        const depAbsolutePath = resolve(dirname(filepath), path.node.source.value)
        // 然后转为项目路径
        const depProjectPath = getProjectPath(depAbsolutePath)
        // 把依赖写进 depRelation
        depRelation[key].deps.push(depProjectPath)

        // 收集嵌套的依赖 
        collectCodeAndDeps(depAbsolutePath)
      }
    }
  })
}

实际上就加了一行代码:

// 收集嵌套的依赖 
collectCodeAndDeps(depAbsolutePath)

 通过检测key来避免重复依赖

如果有嵌套的依赖中有重复的依赖,我们需要做个判断,与depRelation对象中对比key,防止重复依赖发生:

function collectCodeAndDeps(filepath: string) {
  const key = getProjectPath(filepath) // 文件的项目路径,如 index.js
  if(Object.keys(depRelation).includes(key)){
    console.warn(`duplicated dependency: ${key}`) // 注意,重复依赖不一定是循环依赖
    return
  }
  // 获取文件内容,将内容放至 depRelation
  const code = readFileSync(filepath).toString()
  // 初始化 depRelation[key]
  depRelation[key] = { deps: [], code: code }
  // 将代码转为 AST
  const ast = parse(code, { sourceType: 'module' }) 
  // 分析文件依赖,将内容放至 depRelation
  traverse(ast, {
    enter: path => {
      if (path.node.type === 'ImportDeclaration') {
        // path.node.source.value 往往是一个相对路径,如 ./a.js,需要先把它转为一个绝对路径
        const depAbsolutePath = resolve(dirname(filepath), path.node.source.value)
        // 然后转为项目路径
        const depProjectPath = getProjectPath(depAbsolutePath)
        // 把依赖写进 depRelation
        depRelation[key].deps.push(depProjectPath)
        collectCodeAndDeps(depAbsolutePath)
      }
    }
  })
}
  if(Object.keys(depRelation).includes(key)){
    console.warn(`duplicated dependency: ${key}`) // 注意,重复依赖不一定是循环依赖
    return
  }

循环依赖

如果a文件依赖于b文件,b文件又依赖于a文件,那么它们就有可能是循环依赖。例如互相依赖求值

a文件:

import b from './b.js'
const a = {
  value: b.value + 1,
}
export default a

b文件:

import a from './a.js'
const b = {
  value: a.value + 1,
}
export default b

这样互相之间的两个文件就会引起循环依赖,从而报错。程序根本不知道value值是多少。

但两个文件互相依赖也不一定会形成循环依赖,例如:

a文件:

import b from './b.js'
const a = {
  value: 'a',
  getB: () => b.value + ' from a.js'
}
export default a

b文件:

import a from './a.js'
const b = {
  value: 'b',
  getA: () => a.value + ' from b.js'
}
export default b

 文件间互相引用的value值没有继续依赖其它文件,不会引起循环依赖。

 

总结:

AST:

  • parse:把代码 code1变成AST
  • traverse:遍历AST进行修改
  • generate:把AST变成代码 code2

 

babel:

  • babel把高级代码翻译成ES5
  • @babel/parse
  • @babel/traverse
  • @babel/generate
  • @babel/core 包含前三者
  • @babel/preset-env 内置了很多规则

 

循环依赖:

  • 有的循环依赖可以正常执行
  • 有的循环依赖不可以
  • 但是都可以通过AST做静态分析
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值