ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从AST到渲染:编译插件与视图替换的完整链路解析

从AST到渲染:编译插件与视图替换的完整链路解析 做过前端的人应该都听说过 AST也天天在用渲染但“从 AST 到渲染”中间到底发生了什么很多人是模糊的。尤其是当你开始写插件、做视图替换、调试一些诡异问题的时候这条链路的理解直接决定你能不能找到问题的根因。这篇文章我用实际项目的视角把这条链路从原理到实操拆开讲清楚讲一讲插件到底在哪个时机起作用以及我们常说的视图替换有几种实现路径、各自坑在哪里。这个话题适合正在学 Vue/React 原理、被构建工具插件折磨过、或者想写自定义编译器/模板插件的前端开发者。如果你只是天天写业务组件读这篇文章也能帮你理解为什么某些写法性能差、为什么有的报错看起来莫名其妙。1. 从源码到 AST一切工程化的起点1.1 为什么所有框架都绕不开 AST先做个类比。AST 之于代码就像语法树之于自然语言。你在读英文句子时大脑会把它拆成主谓宾拆成从句拆成修饰关系。编译器拿到源码后第一步就是做类似的事——它把字符串形式的代码按语法规则拆成一棵结构化的树。这棵树就是 ASTAbstract Syntax Tree抽象语法树。为什么要费劲搞这么一棵树因为字符串只能做文本级操作没法做语义级操作。比如我想知道“这段代码声明了哪些变量”如果你用正则去匹配字符串会漏掉注释里的假变量、模板字符串里的变量、作用域遮蔽的情况而且不同书写风格几乎没法统一处理。但 AST 是结构化的每个节点有类型、有父子关系、有位置信息我可以从根节点往下遍历精确数出每一层作用域里的声明。我举个例子const a 1这句代码对应的 AST 大概长这样{ type: VariableDeclaration, kind: const, declarations: [ { type: VariableDeclarator, id: { type: Identifier, name: a }, init: { type: Literal, value: 1 } } ] }这种结构让程序可以精准地知道这是一个变量声明声明方式是 const变量名叫 a初始值是 1。想改变量名遍历找到所有 Identifier 为 a 的节点逐个修改。想给所有函数加一段日志找到 FunctionDeclaration 节点往函数体里插入子节点。这正是所有编译工具、Lint 工具、格式化工具、代码高亮、编辑器智能提示的基础设施。没有 AST现代前端工程化里的一大半工具都要重新设计。1.2 解析器的两个阶段词法分析和语法分析整个解析过程在编译器术语里通常分两步词法分析和语法分析。词法分析把字符流切成 Token相当于把一句话切成一个个有意义的单词语法分析再把 Token 根据语法规则组装成树相当于确定单词之间的语法关系。我拿const a 1 2举例。词法分析阶段会得到一串 Token[ { type: keyword, value: const }, { type: identifier, value: a }, { type: operator, value: }, { type: number, value: 1 }, { type: operator, value: }, { type: number, value: 2 } ]语法分析阶段就会把这几个 Token 组织成一个有层级的结构顶层是 VariableDeclaration里面嵌套 BinaryExpression1 2BinaryExpression 的 left 和 right 分别是两个 Literal 节点。这两个阶段的划分意义不只是理论上的。做插件开发时你会经常遇到某些错误发生在词法阶段比如字符串没闭合某些错误发生在语法阶段比如花括号不匹配还有一些发生在后续的遍历转换阶段比如你插件代码自己写错了去读取某个不存在的字段。能区分错误发生在哪个阶段排查效率会高很多。1.3 AST 不只是编译器的专利很多人以为 AST 只在转译器里出现其实它渗透在几乎所有前端工具里ESLint每条规则本质上是一个 AST 访问器。你在 eslint 配置文件里写的no-unused-vars就是规则库在遍历 AST 时检查变量声明节点和引用节点的联通关系。Prettier把代码解析成 AST 之后无视你原本怎么换行缩进直接按模板重新打印出来。代码高亮用正则做基元匹配很容易误伤字符串内容基于 AST 做分词才能区分什么是真正的关键字、什么是字符串里的普通文本。IDE 智能提示跳转到定义、查找引用都是基于 AST 建立的索引。浏览器本身也是这个套路。JS 引擎V8、SpiderMonkey 等拿到 JS 源码后第一件事也是解析成 AST再转成字节码、机器码。所以“从 AST 到渲染”这条链路其实有两层一层是浏览器引擎从 JS 源码到页面渲染另一层是前端框架从模板/JSX 到虚拟 DOM 再到真实 DOM。我们平时接触最多的、插件也最常介入的是后一层。2. 从 AST 到渲染编译器与运行时的分工2.1 编译时与运行时的边界在哪里“渲染”这个词含义比较广。浏览器渲染流水线是 HTML/CSS/JS 经过解析、样式计算、布局、绘制、合成最终上屏。而前端框架视角下的渲染通常指的是用户写的组件模板/代码如何变成真实可见的 DOM。这中间一般分两步走。第一步是编译时。Vue 的模板、JSX、TSX 这些“带增强语法的源码”通过编译器的解析和转换变成可执行的 JavaScript 渲染函数。第二步是运行时。框架调用这些渲染函数生成虚拟节点vnode再通过 diff 和 patch 算法更新到真实 DOM 上。这两步的边界不是固定的不同框架选择不一样React 的 JSX 编译后Babel/ESBuild/SWC 插件运行时通过React.createElement调用创建元素描述对象。Vue 3 的模板编译后生成的渲染函数直接调用createElementBlock等底层 API运行时用精心设计的 patch 逻辑更新。Svelte 干脆把组件编译成非常命令式的原生 DOM 操作代码运行时几乎不需要框架参与 diff。边界怎么画核心在于“哪些工作在编译时做死”哪些工作留到运行时动态决定。编译时做得越多运行时压力越小但灵活性越低运行时越通用越灵活但要做更多判断、性能开销更大。2.2 模板编译的完整链路我以 Vue 3 为例把这条链路走一遍因为它在编译器结构上最清晰而且文档里有 AST explorer 可以直接观察每一步的输出。模板源码div classbox p{{ msg }}/p span v-ifshowHello/span /div第一步解析器将这个模板字符串解析成 AST。节点类型包括 Element 节点div、p、span、属性节点class、v-if、插值节点{{ msg }}。注意这里的 AST 不是 JS 的 AST而是模板 AST节点由模板语法决定的。第二步转换阶段。编译器会遍历模板 AST做静态分析识别哪些是静态节点不会变的比如那句 Hello哪些是动态节点依赖 msg 的插值、依赖 show 的 v-if。这一步会打上一堆标记比如patchFlag告诉运行时“这个节点只需要更新文本其他都不用动”。patchFlag是 Vue 3 性能提升的关键设计之一——编译时把变化类型算好运行时就不需要再穷举判断。第三步代码生成。编译器深度遍历处理过的 AST拼接生成一段 JavaScript 代码。上面模板会生成简化后的渲染函数大致长这样export function render(_ctx, _cache) { return (_openBlock(), _createElementBlock(div, { class: box }, [ _createElementVNode(p, null, _toDisplayString(_ctx.msg), 1 /* TEXT */), _ctx.show ? (_createElementVNode(span, null, Hello)) : (_createCommentVNode(v-if, true)) ])) }这段代码里可以清楚看到编译期决策的影子插值节点的1 /* TEXT */就是 patchFlag告诉运行时只需要比对文本v-if被编译成了一个三元表达式条件不满足时直接生成注释节点占位。2.3 运行时拿到编译产物之后渲染函数只是“产出”真正让页面出现的是运行时。运行时会调用渲染函数得到一个 vnode 描述对象。这个对象不是真实 DOM只是一个轻量级的结构化描述tag 是什么、props 是什么、children 是什么、patchFlag 是什么。然后框架调用 mount 流程逐个 vnode 创建真实 DOM 挂载到页面。后续数据变化时组件重新执行渲染函数得到新的 vnode运行时拿到新旧 vnode 做 diff。因为编译期已经把变化的粒度标好了diff 不需要递归整个树只需要按 patchFlag 的类型走对应的更新逻辑。这就是编译器与运行时紧密配合的典型范例。我还想特别强调一点整个编译过程不一定非要在浏览器里发生。Vue 这样带编译器的构建体系在生产构建时通过 vite 插件把模板编译提前到构建阶段浏览器里拿到的已经是渲染函数。这就是为什么官方推荐使用预编译——性能更好模板字符串本身也不用附带一个编译器代码包。这也是插件系统能发挥价值的地方既然编译发生在构建期那么“编译到渲染之间”就存在巨大的扩展空间。3. 插件机制三个阶段的切入时机3.1 插件到底是个什么东西插件本质上就是“在框架特定时间点被调用的一段代码”。它能存在的前提是框架预留了扩展接口。开发插件前第一个要想清楚的问题不是“我要实现什么功能”而是“框架在哪个时间点允许我介入、我能拿到什么数据、能改什么数据”。拿 Babel 插件来举例。Babel 的工作流程分三步解析源码为 AST - 遍历 AST 触发访问器 - 生成新代码。Babel 插件能介入的就是第二步——Babel 在遍历 AST 时会把你插件里定义的 visitor 拿出来每次经过对应类型的节点就调用一次。你的插件拿到节点对象可以读取它、修改它、删除它、插入新节点。一个典型的 Babel 插件长这样module.exports function () { return { visitor: { Identifier(path) { if (path.node.name oldName) { path.node.name newName; } } } }; };这里Identifier就是访问器名称每当 Babel 遍历到标识符节点时就触发。path不仅包含当前节点还包含了到父级节点、兄弟节点、作用域信息的引用。这是 AST 插件最核心的操作视角。3.2 编译期插件在代码生成前动手前端领域最庞大的插件生态集中在编译期。原因很简单编译期拿到的 AST 是最结构化、最方便做转换的数据形态什么代码都能改而且改动会随构建产物固化下来不需要运行时额外开销。典型代表包括ESLint 规则插件本质是注册一组 AST 访问器在遍历代码时检查违规模式。你写一个 rule实际上是在写哪些节点出现时执行哪段校验逻辑。Babel 插件比如做 JS 压缩、删除 console、按需引入组件库babel-plugin-import 的典型思路就是发现你用了import { Button } from antd自动替换成import Button from antd/es/button。Vue 模板编译插件比如自定义v-loading指令你在模板里写了v-loadingisLoading编译期插件会遍历 Element 节点上的 directives 属性把它转换成一段调用resolveDirective的渲染代码。Markdown 转 HTML 的工具比如 markdown-it 插件、VitePress 的 markdown 插件本质上也是把 Markdown 解析成语法节点再在输出 HTML 前处理这些节点。我在实际项目里写过一个小插件用来给团队内部组件库的每个标签自动注入设计规范属性。做法就是在模板 AST 的 Element 节点上识别组件名前缀然后往 props 对象里追加一条默认属性。整个过程只需要处理 AST 节点对象不需要碰模板字符串也不会误伤普通 div。3.3 运行时插件在渲染执行时介入和编译期插件不同运行时插件介入的是 vnode 生成之后的阶段。它拿不到 AST拿到的是实例、组件对象、渲染上下文这些运行时产物。拿 Vue 插件生态来说app.use()注册的插件本质上是一个带install方法的对象。install被调用时会收到 app 实例你可以注册全局组件、指令往app.config.globalProperties上添加全局属性/方法通过 mixin 往每个组件的生命周期里注入逻辑通过 provide/inject 给整棵树注入依赖Vue Router 就是典型的运行时插件。它注册了两个全局组件RouterView和RouterLink还往全局属性上挂了$router、$route。页面路由切换时RouterView组件内部根据当前路由记录匹配组件然后渲染出来。如果给编译期插件和运行时插件做个对比差异非常明显维度编译期插件运行时插件介入时机构建阶段代码生成前应用运行阶段操作对象AST 节点、编译上下文组件实例、渲染上下文、vnode性能特征只消耗构建时间不影响线上运行每次渲染都可能有调用开销代表案例Babel 插件、ESLint 规则、模板编译插件Vue Router、Pinia、自定义指令运行时典型限制无法感知运行时动态数据无法感知源代码级别结构判断一个功能应该做成编译期插件还是运行时插件我的经验是如果它要修改的是代码结构、语法形态且和运行时的具体业务数据无关放编译期如果它必须依赖运行时的全局状态、组件实例放运行时。两个阶段也要配合使用的情况并不少见比如 Vue Router 的匹配逻辑在运行时完成但它提供的组件本质也需要编译期发现别名、解析路径模式。3.4 写一个极简插件实例光说原理太虚我直接给你演示一个编译期插件的完整写法。场景团队代码里有大量直接触发alert()的调用安全规范要求统一走自定义提示组件。我想在编译期把所有alert(...)调用替换成this.$toast(...)。用 Babel 插件实现module.exports function ({ types: t }) { return { name: replace-alert-to-toast, visitor: { CallExpression(path) { const callee path.node.callee; // 只处理 alert(...) 调用 if (!isAlertReference(callee)) return; const args path.node.arguments.map(arg t.isStringLiteral(arg) ? arg : t.callExpression( t.identifier(String), [arg] ) ); // 替换成 this.$toast(...) path.replaceWith( t.callExpression( t.memberExpression( t.thisExpression(), t.identifier($toast) ), args ) ); } } }; }; function isAlertReference(callee) { // 只匹配 id 为 alert 且不是属性调用如 window.alert return ( t.isIdentifier(callee, { name: alert }) ); }在 Babel 配置里加载这个插件然后发现代码里的alert(保存成功)构建后全部变成了this.$toast(保存成功)。这个改动不需要业务开发者一个个去手动改所有代码构建时自动完成这就是编译期插件的威力。类似思路还能做很多事自动加埋点、自动清除 debug 日志、自动注入国际化函数。4. 视图替换从数据到界面的动态切换4.1 视图替换的四种不同层次“视图替换”是一个容易被误解的词。有人以为是路由切换有人以为是 Tab 切换有人以为是弹窗关闭。其实在实际项目中视图替换至少有四个层次每个层次的原理和坑都不一样。第一层是最基础的条件渲染。用v-if切换一个区块的显示隐藏。第二层是组件级替换同样的挂载点根据状态渲染不同的组件。第三层是路由级替换整个页面组件被替换。第四层是自定义渲染器级替换你直接换掉整个渲染目标比如原来渲染到 DOM现在渲染到 Canvas、WebGL 甚至自定义数据协议。理解这四层对处理实际问题特别有帮助因为报错信息往往只会告诉你“哪里挂了”不会告诉你“是哪个层次的替换逻辑出问题了”。4.2 v-if 与 v-show本质上是节点树的增删与样式切换先看最基础的视图替换。v-if和v-show是很多人从入门就在用的指令但未必清楚两者背后完全不同的实现路径。编译 Vue 模板时v-if会被编译成条件表达式前面代码示例里已经见过。运行时条件为 true 就创建真实节点并插入 DOM条件为 false 就创建或保留一个注释节点做占位并在 diff 时删除真实节点。视图的替换在这个场景里是真实的 DOM 节点创建与销毁。v-show则不同。它不管条件真假节点始终渲染只是通过内联样式控制display来切换可见性。视图在页面上“变了”但 DOM 里元素一直在。这套做法的优点是切换开销小缺点是首屏要渲染本来不该出现的 DOM。选型建议很简单如果切换频率高、过度动画不需要、节点创建成本高用 v-show如果条件变化不频繁、或初始条件几乎不成立用 v-if 省掉多余的 DOM。很多人一上来就“追求性能”无脑用 v-if其实切换频繁的 Tab 面板用 v-show 往往体验更好。4.3 动态组件与路由视图挂载点不变组件变当我们说“视图替换”时更常指的是“同一个挂载位置渲染不同的组件”。在 Vue 里是component :iscurrentComponent /在 React 里类似的是条件返回不同组件。原理上是这样的component组件会接收is属性的值这个值可以是一个组件对象也可以是一个注册过的组件名。渲染时当前的 vnode 的 tag 就是这个动态组件diff 时发现 tag 变了框架会销毁旧的组件实例创建新的组件实例走完整的 init - mount 流程。这里有个非常隐蔽的坑值得单独拿出来说如果你切换的是同一个组件类型但是不同的 keyVue 是如何处理的。比如component :iscurrentView :keyviewKey /当key改变时框架会强制认为这是“不同的组件”即使is的类型没变也会销毁重建。这是一个很好用的技巧比如表单重置只要改变 key组件内所有本地状态都会被清空不需要手动调重置方法。路由视图RouterView本质上是动态组件的进阶封装。它根据当前 URL 匹配到的路由记录来决定渲染哪个页面组件。和动态组件不同的是RouterView 还负责把 URL 参数注入 props通过route对象并且配合嵌套路由处理多级视图的渲染。切换路由时如果没有keep-alive老的页面组件实例会被销毁所有本地数据丢失。加了keep-alive之后组件实例会被缓存deactivated/activated 生命周期就会介入。4.4 自定义渲染器直接把渲染目标换掉第四层视图替换少有人提但对插件开发者来说是极具价值的。Vue 3 的createRenderer把渲染逻辑抽象成与平台无关的模块默认传的是 DOM 操作的 APIinsert、patchProp、remove等但你可以传入自己的 API让同样的组件逻辑渲染到完全不同的目标上。这是很多跨端方案的核心原理。比如小程序端渲染目标不是 DOM而是小程序的自定义组件树Canvas 渲染引擎会把 vnode 对应到画布上的图形对象甚至有人把 Vue 渲染到终端命令行界面的。我简单描述一个极简自定义渲染器的骨架const { createRenderer } require(vue); const renderer createRenderer({ createElement(type) { // 返回自定义节点对象比如 canvas 图形对象 return new CanvasNode(type); }, insert(child, parent, anchor) { // 把子节点挂到父节点上 parent.append(child); }, patchProp(el, key, prevValue, nextValue) { // 更新自定义节点的属性 el[key] nextValue; }, remove(el) { el.remove(); } });一旦走到了自定义渲染器这一层视图替换就不再只是组件切换而是你可以把同一套业务组件逻辑投射到任意“长得像 DOM”的目标结构上。这能力对做可视化大屏、游戏 UI、跨端框架的人来说相当于给了一套完整可复用的渲染基础设施。5. 常见问题与排查技巧实录5.1 渲染层报错Cannot read properties of undefined热词里有一条典型报错[渲染层错误] uncaught typeerror: cannot read properties of undefined (reading ...)。这种报错很让人抓狂因为它往往不告诉你具体是哪个变量出了问题只告诉你渲染层某处访问了 undefined 的属性。根据我个人经验这类报错最常见的根源有三个第一个是在模板里访问了尚未初始化的数据。比如接口返回前组件就先渲染了data里的对象还是undefined模板里写了user.name。解决办法是模板里做空值兜底或者初始化数据时给个默认值。第二个是 v-for 的遍历对象本身是 undefined。和上面同理v-foritem in list中list如果晚于首次渲染才被赋值就会炸。第三个是编译插件/模板插件导致的字段缺失。如果你启用了自定义编译插件插件在转换 AST 时可能生成了引用某字段但没声明的代码。这种错误比较隐蔽因为报错栈指向的是一段编译后的代码不是你写的源码。排查思路是先关掉可疑插件跑一遍如果正常了再用 source map 定位到编译后的具体位置反查原模板或原代码。5.2 插件不生效AST 版本和遍历时机的问题插件不生效比报错还难排查因为程序没告诉你哪里不对。整理几个我踩过的坑。第一个是插件返回的 visitor 名字写错了。Babel 和 ESLint 的 visitor 是基于 AST 节点类型命名的CallExpression、Identifier、VariableDeclaration大小写和拼写必须严格一致。拼错一个字符插件就默默地什么都不做。第二个是插件顺序问题。多个插件操作同一个节点时后执行的插件看到的是前一个插件改完的结果。如果你的插件依赖某个前序插件转换完成的产物但又排在它前面自然失效。比如你写了一个 Babel 插件想把import { A } from pkg转换成按需加载但如果另一个插件先一步把 import 语法转换成了require你的插件可能匹配不到ImportDeclaration节点了。第三个是 AST 结构随工具版本变化。Babel 7 到 8、ESLint 8 到 9AST 节点字段和访问器结构都有调整。一个为旧版本写的插件在新版本里可能读取的字段根本不存在。升级依赖后插件突然全部失效优先怀疑这个原因去 changelog 里搜 visitor and AST breaking changes。到这里我顺便分享一个更通用的排查技巧无论是编译期插件还是运行时插件先做一个最小复现。新建一个只包含报错特征的最简文件关掉一半插件跑一遍再关掉另一半用二分法快速缩小问题范围。这个办法在复杂项目里比钻牛角尖看代码高效太多。5.3 视图替换不生效key、缓存和生命周期视图替换最常见的问题归结起来就三类替换没发生、替换太频繁、替换后状态丢失。替换没发生的常见原因是 key 没变。Vue 和 React 的 diff 算法都以 key 作为复用的依据。如果你想强制某个组件重新渲染比如下次切换回来自动刷新数据但 key 一直没变框架会判断为同类组件直接复用实例不走销毁重建状态不会重置。解法就是给组件一个和业务状态绑定的 key。替换太频繁的坑通常出现在动态组件和路由视图上。比如你的动态组件绑定的是一个每次渲染都会创建新对象引用的计算属性每次数据变更都导致is变成一个“看起来相同但引用不同”的组件框架就会把它当成不同组件重建实例。用 console.log 打印组件实例或用 vnode 的 type 检查一下能快速确认是不是这个原因。替换后状态丢失则是 keep-alive 使用不当。加了keep-alive之后组件切换不会销毁但被缓存的组件如果内部依赖定时器、WebSocket、全局事件监听恢复时可能因为 deactivated/activated 没被正确利用出现数据过期或事件重复绑定的问题。我的习惯是凡是被 keep-alive 缓存的组件所有全局事件监听和定时器必须在 deactivated 里清除在 activated 里重新注册。这几种问题我都做过整理可以直接当速查表用现象可能原因排查方向解决方案切换组件后内部状态没重置key 未变化检查组件绑定的 key手动绑定业务状态 key组件被频繁重建is每次引用不同打印组件 type 是否相同将组件对象缓存在变量中路由切换后页面数据丢失无缓存或缓存策略不对检查是否使用了 keep-alive按需加 keep-alive 并处理激活逻辑自定义插件生成了 undefined 字段插件编译产物缺声明关闭插件跑一遍对照修改插件转换逻辑或升级适配ESLint 规则不触发visitor 名称拼写错误检查节点类型名对照 AST explorer 修正名称5.4 调试 AST 与渲染链路的实用工具排查这些问题时好用的工具能省一半时间。我常用的工具清单如下AST Explorerastexplorer.net在线查看各种语言的 AST 结构。左边写代码右边实时显示 AST 树支持切换 parserbabel、espree、typescript、vue 模板编译器是调试插件最顺手的工具。Vue Template Explorertemplate-explorer.vuejs.org专门看 Vue 模板编译输出的渲染函数。左边写模板右边显示编译后的代码和 patchFlag理解模板编译产物时非常直观。source-map 支持确保构建工具开启 source map报错时能定位到源码而不是编译后的渲染函数代码。vite 插件的 dev server hook写 Vite 插件时直接在transform钩子里 console.log 当前模块的代码可以实时看到构建时各个模块经过插件后的产物。调试 AST 插件有个通用心法先打印后修改。在 visitor 里一开始就 console.log 当前节点的 type 和关键字段确认遍历到了你想要的节点再去动修改逻辑。很多人插件不生效就是因为对节点结构认知有偏差直接照着自己想象的字段去写代码最后在读一个不存在的属性。6. 从“会写插件”到“设计插件”6.1 插件 API 的设计原则如果你已经能熟练写插件下一步就是设计插件。两者区别很大。给自己项目写插件怎么快怎么来给别人用的插件第一原则是“别让我意外”。我总结了三条对插件设计最重要的经验。第一尽量不修改用户代码的默认行为。插件的功能应该是“新增”而不是“改变”。比如我前文提到的把 alert 替换成 $toast这种设计就需要提供显式开关而不是默认开启。用户引入插件后发现自己线上行为变了是最可怕的体验。第二提供清晰的错误提示。插件出错时给一句可读的提示比直接抛一个对象、让用户自己看堆栈强得多。插件是运行在用户构建链路上的一个报错可能阻塞整个团队开发错误信息应该尽可能指出是哪段代码、哪个模板、哪个配置引起的。第三插件要有最小可用实现。不要一开始就覆盖几十种边界情况先把核心场景跑通然后在真实项目中不断迭代。一个只有几百行代码但稳定可靠的插件远好过一个堆满功能但经常出事的重型插件。6.2 编译转换的威力与边界写了几年 AST 插件之后我越来越觉得“编译转换”是一种类似元编程的能力。你写的不是给用户用的业务代码而是能生产业务代码的程序。这种能力的力量很强但边界也很清楚它是静态的。编译期能看到的只有代码本身看不到运行时的数据。你想在编译期根据用户登录状态做渲染分支做不到因为登录状态是运行时才有。你只能生成“检查登录状态的代码”不能直接生成“登录状态的最终结果”。明白这个边界就不容易写出过度设计的插件。反过来运行时也无法获取源码的结构化信息。Vue 的运行时插件要分析组件的 props 来源、export 的变量结构往往很吃力因为运行时组件对象里可能已经丢失了源码的很多细节。所以在设计任何一次“代码转换”时先问一句这个转换应该发生在哪个阶段这个判断本身比写代码困难得多也重要得多。我个人在实际操作中的体会是AST 相关的问题80% 都能靠 AST Explorer 和 console.log 节点对象解决剩下 20% 里又有一大半是版本不匹配导致的。真正复杂的是“想清楚要在哪个阶段、哪个节点、做什么样的转换”这个设计决策决定了插件是帮助开发者还是添乱。如果你刚开始接触这块建议从写一个能自动删除 console 的 Babel 插件练手跑通之后再去改造自己的开发流程里那些反复手动的环节会让你对整条链路有完全不一样的感受。
返回列表