webpack专精00

这篇博客深入探讨了webpack的学习,通过实例解析了Babel的工作原理,包括代码如何从AST转换。还展示了手动将let转换为var的步骤,以及如何使用Babel将ES6代码转换为ES5。此外,文章还讲解了如何通过AST分析JavaScript文件的依赖关系,以及处理循环依赖问题。最后,提到了在实际项目中,Babel和webpack在前端开发中的重要应用。
摘要由CSDN通过智能技术生成

这个系列的主题是关于webpack的,希望能通过对webpack的深入学习,更进一步提升自己的前端能力。

先从babel讲起

  • babel 的原理,大致的原理如下:
    1. parse: 把代码 code 变成 AST
    2. traverse: 遍历 AST 进行修改
    3. generate: 把 AST 变成代码 code2
    4. 即 code --(1)-> ast --(2)-> ast2 --(3)-> code2
    5. 什么是ast,ast就是将代码转变成一个对象,接下来我们会通过手动把 let 变成 var来进行实践观察

示例:手动把 let 变成 var

  • 代码见 let_to_var.ts,将 code 中的 let 全部变成 var
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)
  • 安装依赖的话通过package.json安装会比较方便
    {
    "name": "my-webpack-demo-1",
    "version": "0.0.1",
    "engines": {
    "node": ">=14"
    },
    "dependencies": {
    "@babel/core": "7.12.3",
    "@babel/generator": "7.12.5",
    "@babel/parser": "7.12.5",
    "@babel/preset-env": "7.12.1",
    "@babel/traverse": "7.12.5",
    "@types/babel__traverse": "7.0.15",
    "ts-node": "9.0.0",
    "typescript": "4.0.5"
    },
    "devDependencies": {
    "@types/babel__core": "7.1.12",
    "@types/babel__generator": "7.6.2",
    "@types/babel__parser": "7.1.1",
    "@types/babel__preset-env": "7.9.1",
    "@types/node": "14.14.6"
    }
    }

调试命令

  • 运行 TS 代码
    node -r ts-node/register let_to_var.ts
  • 因为终端不好观察所以我们需要用浏览器的控制台
    node -r ts-node/register --inspect-brk let_to_var.ts 
    点击链接,然后点击控制台左上角的图标,sources下面就会出现我们的ts文件变成js之后的代码。

打断点观察ast

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

const code = `let a = 'let'; let b = 2`
// code变成ast
const ast = parse(code, { sourceType: 'module' })

// 遍历整个ast,回调函数enter,ast变成ast2
traverse(ast, {
  enter: item => {
    if(item.node.type === 'VariableDeclaration'){
      if(item.node.kind ==='let') {
        item.node.kind = 'var'
      }
    }
  } 
})

// ast2 转成code2
const result = generate(ast, {}, code)
console.log('22222', result.code)
  • 效果见图3 3let_to_var.png

为什么要用AST

  1. 你很难用正则表达式来替换,正则很容易把 let a = 'let' 变成 var a = 'var'
  2. 你需要识别每个单词的意思,才能做到只修改用于声明变量的 let
  3. 而 AST 能明确地告诉你每个 let 的意思

将es6转成es5

  • 新建to_es5.ts
  • 使用 @babel/core 和 @babel/preset-env,具体见下代码
import { parse } from "@babel/parser"
import * as babel from "@babel/core"

const code = `let a = 'let'; let b = 2; const c = 3;`
const ast = parse(code, { sourceType: 'module' })
// transformFromAstSync 这个方法可以把ast变成code
// 传入原始的ast, 原始的code,这样就能生成map
const result = babel.transformFromAstSync(ast, code, {
  presets: ['@babel/preset-env']
})

console.log(result.code)
  • 重点知识
    • babel.transformFromAstSync 可以把 ast 变成 code2
    • 如果图方便,可以用 babel.transformSync 直接把 code 变成 code2
    • @babel/preset-env 内置了很多转换规则
  • 运行结果
    • node -r ts-node/register to_es5.ts
    • 结果与 let_to_es5.ts 差不多,还会把所有高级语法转为 ES5
  • 这个时候有小伙伴就问了怎么转出来的是字符串,我要转出来的是代码文件

转换代码文件形式

  • 新建file_to_es5.ts
  • 改写之前的代码
import { parse } from "@babel/parser"
import * as babel from "@babel/core"
import * as fs from 'fs'


const code = fs.readFileSync('./test.js').toString()
const ast = parse(code, { sourceType: 'module' })
// transformFromAstSync 这个方法可以把ast变成code
// 传入原始的ast, 原始的code,这样就能生成map
const result = babel.transformFromAstSync(ast, code, {
  presets: ['@babel/preset-env']
})

fs.writeFileSync('./test.es5.js', result.code)
  • 新建test.js,并在其中写入代码,运行file_to_es5,结束发现多了test.es5.js,里面是转化完毕的后es5代码

除了转换 JS 语法,还能做啥?

  • 分析 JS 文件的依赖关系
  • 利用deps_1.ts,和project_1文件夹下的内容进行实践,代码如下
// deps_1.ts
// 请确保你的 Node 版本大于等于 14
// 请先运行 yarn 或 npm i 来安装依赖
// 然后使用 node -r ts-node/register 文件路径 来运行,
// 如果需要调试,可以加一个选项 --inspect-brk,再打开 Chrome 开发者工具,点击 Node 图标即可调试
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)
console.log('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, '/')
}

04一层依赖.png

  • project_1文件夹
// index.js
import a from './a.js'
import b from './b.js'
console.log(a.value + b.value)

// a.js
const a = {
  value: 1,
}
export default a

// b.js
const b = {
  value: 2,
}
export default b

启发: 用哈希表来储存文件依赖

  • 哈希表是一个数据结构术语,js中一个对象就可以看作一个哈希表

递归分析多层依赖关系

  • 三层依赖关系

    • index -> a -> dir/a2 -> dir/dir_in_dir/a3
    • index -> b -> dir/b2 -> dir/dir_in_dir/b3
    • 文件放在 project_2 目录里
  • 思路

    • collectCodeAndDeps 太长了,缩写为 collect
    • 调用 collect('index.js')
    • 发现依赖 './a.js' 于是调用 collect('a.js')
    • 发现依赖 './dir/a2.js' 于是调用 collect('dir/a2.js')
    • 发现依赖 './dir_in_dir/a3.js' 于是调用 collect('dir/dir_in_dir/a3.js')
    • 没有更多依赖了,a.js 这条线结束,发现下一个依赖 './b.js'
    • 以此类推,其实就是递归

循环依赖

  • 见图3
  • node -r ts-node/register deps_3.ts
    • 报错:调用栈 溢出了
    • 为什么:分析过程 a -> b -> a -> b -> a -> b -> ... 把调用栈撑满了 05.png

      静态分析循环依赖

  • 解决循环依赖
  • 一旦发现这个 key 已经在 keys 里了,就 return
  • 这样分析过程就不是 a -> b -> a -> b -> ... 而是 a -> b -> return
  • 注意我们只需要分析依赖,不需要执行代码,所以这样是可行的
  • 由于我们的分析不需要执行代码,所以叫做静态分析
  • 但如果我们执行代码,就会发现还是出现了循环

执行 project_4/index.js

  • 发现报错:不能在 'a' 初始化之前访问 a
  • 原因:执行过程 a -> b -> a 此处报错,因为 node 发现计算 a 的时候又要计算 a 06循环引用爆栈.png

合法的循环依赖(没有逻辑漏洞)

  • 见图7

总结

  • AST 相关
    • parse: 把代码 code 变成 AST
    • traverse: 遍历 AST 进行修改
    • generate: 把 AST 变成代码 code2
  • 工具
    • babel 可以把高级代码翻译为 ES5
    • @babel/parser
    • @babel/traverse
    • @babel/generator
    • @babel/core 包含前三者
    • @babel/preset-env 内置很多规则
  • 代码技巧
    • 使用哈希表来存储数据
    • 通过检测 key 来避免重复
  • 循环依赖
    • 有的循环依赖可以正常执行
    • 有的循环依赖不可以
    • 但都可以做静态分析 07合法的循环依赖.png

      相关代码

      本文由博客一文多发平台 OpenWrite 发布!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值