普通视图

发现新文章,点击刷新页面。
今天 — 2026年2月2日首页

前端向架构突围系列 - 编译原理 [6 - 4]:模板编译与JSX 转换的编译艺术

2026年2月2日 13:49

写在前面

很多开发者认为前端框架是纯粹的“运行时(Runtime)”库。 其实不然。现代前端框架的竞争,早已从运行时卷到了编译时(Compile-time)

  • Vue 的模板看起来像 HTML,但浏览器根本不认识 v-for。它是通过编译器把模板变成了高效的 JavaScript 渲染函数。
  • React 的 JSX 看起来像 XML,但它其实是 React.createElement 的语法糖。而最新的 React Compiler 更是试图通过编译手段自动解决性能问题。

作为架构师,理解这套编译逻辑,你才能明白为什么 Vue 3 比 Vue 2 快,也能理解 React 团队为什么要搞个编译器。

unnamed (1).jpg


一、 Vue 的编译哲学:静态分析的艺术

Vue 的核心设计哲学是 “显式优于隐式” 的模板语法。 正因为模板的结构是固定的(不像 JSX 那样可以是任意 JS 逻辑),Vue 的编译器可以在编译阶段就知道哪些节点是静态的(永远不变),哪些是动态的(可能变)。

这是一场关于 AST 的情报战

1.1 编译流水线

Vue 的编译过程包含三个核心步骤:

  1. Parse (解析):<template> 字符串解析成 Vue AST(不是 JS AST,是描述 HTML 结构的树)。
  2. Transform (转换): 遍历 Vue AST,应用各种指令转换(如 v-if, v-model)和编译时优化
  3. Generate (生成): 把优化后的 Vue AST 生成为 JavaScript 代码(即 render 函数)。

1.2 魔法的核心:PatchFlags 与 Block Tree

Vue 3 性能起飞的秘密就在 Transform 阶段。

看看这段代码:

<div>
  <span>我是静态的</span>
  <span>{{ msg }}</span>
</div>

Vue 2 的做法: 每次更新,都要对比整个 DOM 树,即使第一个 <span> 根本不可能变。 Vue 3 的做法(编译后): 编译器在 AST 上给第二个 <span> 打了个标记(PatchFlag)。

// 伪代码:Vue 3 编译后的 render 函数
export function render(_ctx) {
  return (
    openBlock(),
    createBlock('div', null, [
      createVNode('span', null, '我是静态的'), // 静态节点
      createVNode('span', null, _ctx.msg, 1 /* TEXT */) // 动态节点,标记为 1
    ])
  )
}

架构洞察: 运行时看到这个 1,就知道:“我只需要对比这个节点的文本内容,其他的属性、类名、子节点都不用管。” 这就是 Compile-time Optimization(编译时优化) 赋能 Runtime Performance(运行时性能) 的典范。


二、 React 的编译哲学:JSX 的极简与自由

React 选择了另一条路:All in JavaScript。 JSX 不是模板,它就是 JS 表达式。这意味着 React 拥有极高的灵活性,但也付出了代价——编译器很难通过静态分析来优化它

2.1 JSX 的本质:Babel 插件

React 的编译过程相对简单,通常不需要自己写 Parser,而是借助于 Babel@babel/preset-react 会把 JSX 语法转化为普通的 JS 函数调用。

源代码:

const element = <div className="foo">Hello</div>;

编译后 (React 17+ Automatic Runtime):

import { jsx as _jsx } from 'react/jsx-runtime';
const element = _jsx("div", { className: "foo", children: "Hello" });

2.2 自由的代价

因为 JSX 太灵活了(你可以在 if 里写 return <div />,也可以用 map 生成组件),编译器很难像 Vue 那样预判“这块 DOM 永远不会变”。 因此,React 长期依赖运行时的 Diff 算法(Fiber 架构)来解决性能问题,或者强迫开发者手动写 useMemouseCallback


三、 变局:React Compiler (React Forget)

React 团队意识到,手动优化(useMemo)太反人类了。于是,他们在 2024 年推出了 React Compiler

这标志着 React 也开始向“重编译”方向转型。

3.1 它的工作原理

React Compiler 也是一个 Babel 插件。它通过 AST控制流图 (Control Flow Graph, CFG) 分析你的代码,自动计算依赖关系。

源代码:

function Component({ heading, body }) {
  return <div>
    <h1>{heading}</h1>
    <p>{body}</p>
  </div>;
}

编译后(概念版): 编译器发现 headingbody 没变时,整个 JSX 都不需要重新创建。它自动帮你把组件内部的代码用类似 useMemo 的逻辑包裹起来,但粒度更细,细到具体的表达式。

架构意义: 这填补了 React 相比于 Vue/Solid 在细粒度更新上的短板,完全由编译器代劳,开发者无需感知。


四、 跨框架的共识:编译时的崛起

从 Vue 的 PatchFlags,到 React Compiler,再到 Svelte(干掉 Virtual DOM)和 SolidJS(预编译 DOM 模板),前端框架的演进趋势非常清晰:

把运行时的负担,转移到编译时去。

4.1 为什么?

  1. 用户体验: 编译时慢一点(开发者构建慢),换来的是用户运行时快很多。
  2. 代码体积: 编译器可以分析出没用到的特性(Tree Shaking),打包出来的代码更小。

4.2 架构师的视角

当你选型框架时,不要只看语法(JSX vs Template),要看它的编译策略

  • 如果你的项目是重交互、高性能仪表盘,Vue 3 或 Solid 这种基于静态分析优化的框架可能更有优势。
  • 如果你的项目逻辑极其复杂、动态性极强(低代码平台),React 的灵活性依然是王者。

结语:掌握魔法的钥匙

至此,《编译流程》 圆满结束。

我们从最底层的 AST 原理(第一篇),进阶到 Babel 插件实战(第二篇),掌握了 ESLint 与 Codemod 的治理能力(第三篇),最后看透了 现代框架 的编译魔法。

现在,代码在你眼中不再是黑盒。你看到的不是字符,而是,是,是可被重塑的逻辑

前端向架构突围系列 - 编译原理 [6 - 3]:ESLint 原理、自定义规则与 Codemod

2026年2月2日 10:11

写在前面

很多团队面临这样的困境: 架构师制定了规范:“所有业务组件禁止直接引用 lodash,必须引用 src/utils。” 结果呢?文档写在 Wiki 里吃灰,新同事照样写 import _ from 'lodash'。Code Review 时如果你没看出来,这代码就溜上线了。

口头规范是软弱的,代码规范才是强硬的。

真正的高手,会把架构规范写成 ESLint 插件。这一节,我们将把“文档里的规范”变成“编辑器里的红色波浪线”。

image.png


一、 ESLint 的原理:找茬的艺术

ESLint 的工作流程和 Babel 惊人地相似,只有最后一步不同。

1.1 流程对比

  • Babel: Parse -> Transform (修改 AST) -> Generate (生成新代码)
  • ESLint: Parse -> Traverse (遍历 AST) -> Report (报告错误)

ESLint 默认使用 Espree 作为解析器(Parser)。它遍历 AST,当遇到不符合规则的节点时,不是去修改它,而是记录一个“错误对象”(包含行号、列号、错误信息)。

1.2 Fix 的原理

你一定用过 eslint --fix。既然 ESLint 不生成新代码,它是怎么修复错误的? 其实,ESLint 的规则在报错时,可以提供一个 fixer 对象。

context.report({
  node: node,
  message: "缺少分号",
  fix: function(fixer) {
    // 告诉 ESLint:在当前节点后面插入一个 ";"
    return fixer.insertTextAfter(node, ";");
  }
});

ESLint 收集所有的 fix 操作,最后在源码字符串上进行字符串拼接(而不是重新 Generate),从而保留原本的格式(空格、注释)。


二、 实战:编写你的第一条 ESLint 规则

假设你的团队有一个死规定:代码中禁止使用 var,必须用 letconst 虽然现有的规则集里有 no-var,但为了学习,我们自己写一个。

2.1 规则结构

一个 ESLint 规则就是一个导出的对象,包含 meta(元数据)和 create(访问者)。

// eslint-plugin-no-var-custom.js
module.exports = {
  meta: {
    type: "suggestion",
    docs: {
      description: "禁止使用 var",
    },
    fixable: "code", // 表示这个规则支持自动修复
  },
  create(context) {
    return {
      // 监听 VariableDeclaration 节点
      VariableDeclaration(node) {
        // 如果声明类型是 "var"
        if (node.kind === "var") {
          // 报警!
          context.report({
            node,
            message: "大清亡了,别用 var 了!",
            // 自动修复逻辑
            fix(fixer) {
              // 把 "var" 替换成 "let"
              // sourceCode.getFirstToken(node) 获取到的就是 "var" 这个关键词
              const varToken = context.getSourceCode().getFirstToken(node);
              return fixer.replaceText(varToken, "let");
            }
          });
        }
      }
    };
  }
};

2.2 架构级应用:防腐层治理

架构师可以利用自定义规则做更高级的事情。 场景: 项目中分层架构,UI 层(src/components)严禁直接导入数据库层(src/db)。

// rule: no-ui-import-db.js
create(context) {
  return {
    ImportDeclaration(node) {
      const importPath = node.source.value; // e.g., '@/db/user'
      const currentFilename = context.getFilename(); // 当前正在检查的文件

      // 如果当前文件在 components 目录下,且引用了 db 目录
      if (currentFilename.includes('/src/components/') && importPath.includes('/db/')) {
        context.report({
          node,
          message: "架构报警:UI 组件禁止直接触碰数据库层!请通过 Service 层调用。"
        });
      }
    }
  };
}

把这个规则加入 CI/CD,你的架构分层就有了强制力


三、 Codemod:自动化重构的核武器

ESLint 的 fix 适合修补小问题。但如果你面临的是大规模破坏性重构,比如:

  • 把项目中 5000 个文件的 React.createClass 全部重写为 class extends React.Component
  • 把所有的 import { Button } from 'my-ui' 变成 import Button from 'my-ui/button'

这时候,你需要 Codemod。最著名的工具是 Facebook 推出的 jscodeshift

3.1 jscodeshift 的优势

它不仅仅是 AST 解析器,它提供了一套类似 jQuery 的 API 来操作 AST。你不需要关心复杂的节点结构,只需要链式调用。

3.2 实战:API 签名变更

需求: 旧的 API myApi.get(id, type) 升级了,参数变了,必须改成对象传参 myApi.get({ id, type })

Codemod 脚本:

// transformer.js
export default function(file, api) {
  const j = api.jscodeshift; // 获取 jscodeshift 实例
  
  return j(file.source) // 1. 解析源码
    .find(j.CallExpression, { // 2. 查找所有的函数调用
      callee: {
        object: { name: 'myApi' },
        property: { name: 'get' }
      }
    })
    .forEach(path => { // 3. 遍历找到的节点
      const args = path.node.arguments;
      
      // 如果参数数量是 2 个,说明是旧代码
      if (args.length === 2) {
        // 创建一个新的对象表达式 { id: arg0, type: arg1 }
        const newObjArg = j.objectExpression([
            j.property('init', j.identifier('id'), args[0]),
            j.property('init', j.identifier('type'), args[1])
        ]);
        
        // 替换参数
        path.node.arguments = [newObjArg];
      }
    })
    .toSource(); // 4. 生成新代码
}

运行:

npx jscodeshift -t transformer.js src/**/*.js

瞬间,你完成了全项目几千个文件的 API 升级。这就是架构师的效率。


四、 总结:架构师的“法治”思维

这一节我们从“写代码”进阶到了“管代码”。

  1. ESLint 是日常执勤的警察,通过 Linting(检查)和 Fixing(微修补)维持代码风格和架构边界。
  2. Codemod 是特种部队,通过 AST Transformation 解决大规模的技术债务和破坏性升级。

架构师不应该仅仅是那个“写文档告诉大家怎么做”的人,而应该是那个“提供工具让大家没法做错”的人。

Next Step: 我们已经把 AST 在工具链(Babel, ESLint)中的应用学完了。 最后,我们要看看 AST 是如何在现代前端框架中发挥作用的。Vue 的 <template> 是怎么变成 JS 的?React 的 JSX 到底是怎么回事? 下一节,我们将揭秘**《第四篇:应用——框架的魔法:Vue 模板编译与 React JSX 转换背后的编译艺术》**。

昨天以前首页

前端向架构突围系列 - 编译原理 [6 - 2]:Babel 插件开发与访问者模式

2026年1月29日 15:37

写在前面

很多高级的前端库都在用 Babel 插件做“魔法”。

  • React: JSX 语法根本不是 JS,是 Babel 把它变成了 React.createElement
  • Vue: v-model 的语法糖,是在编译阶段展开的。
  • babel-plugin-import: 为什么 Ant Design 可以按需加载?因为它悄悄把 import { Button } from 'antd' 改写成了引用具体文件的路径。

学会写 Babel 插件,意味着你拥有了改写语言规则的能力。

unnamed.jpg


一、 核心设计模式:访问者模式 (Visitor Pattern)

AST 是一棵深度极深、结构复杂的树。如果让你手动写递归函数去遍历每一个节点,还得判断“这是不是函数”、“这是不是变量”,你会疯掉的。

Babel 采用 访问者模式 来解决这个问题。

1.1 什么是 Visitor?

想象 AST 是一个巨大的迷宫

  • Babel (Traverser): 是一个不知疲倦的导游,他负责走遍迷宫的每一个角落(深度优先遍历)。
  • 你 (Visitor): 是游客。你不需要自己走,你只需要在特定的“景点”等着。
  • 工作流: 导游走到一个节点(比如“函数声明节点”),就会大喊:“这里有个函数声明!”如果你对这个节点感兴趣,你就处理它;不感兴趣,导游就继续走。

1.2 代码中的 Visitor

在 Babel 插件中,Visitor 就是一个对象,对象的Key 是你感兴趣的 AST 节点类型。

const visitor = {
  // 当遍历到 Identifier(标识符/变量名)节点时,执行这个函数
  Identifier(path) {
    console.log("我发现了一个变量:", path.node.name);
  },
  
  // 当遍历到 BinaryExpression(二元表达式,如 a + b)节点时...
  BinaryExpression(path) {
    console.log("我发现了一个运算");
  }
};

二、 手术刀的核心:Path 与 Types

在编写插件时,有两个最重要的概念:path@babel/types

2.1 Path:节点之间的桥梁

注意,Visitor 函数接收的参数不是 node,而是 path

  • Node (节点): 只是静态的数据(JSON 对象),比如 { type: "Identifier", name: "a" }。它没有灵魂。

  • Path (路径): 是一个响应式对象。它不仅包含当前节点的信息,还包含父节点作用域以及增删改查的方法

    • path.node: 获取当前节点数据。
    • path.parent: 获取父节点。
    • path.remove(): 自杀(把自己从树中移除)。
    • path.replaceWith(newNode): 变身(把自己替换成新节点)。
    • path.stop(): 告诉导游(Babel),停止遍历,不要往下走了。

2.2 @babel/types:节点的生成器与验证器

如果你想把 a 替换成 b,你不能直接写 path.node.name = 'b'(虽然有时候也能跑,但不规范)。你需要创建一个标准的 AST 节点。 @babel/types (通常简写为 t) 就是干这个的。

  • t.isIdentifier(node): 判断是不是标识符。
  • t.stringLiteral("hello"): 创建一个字符串字面量节点。

三、 实战演练:编写一个“去除 console.log”的插件

这是 Babel 插件开发的 "Hello World"。 需求: 生产环境构建时,自动删除代码里所有的 console.log,但保留 console.error

3.1 第一步:在 AST Explorer 中观察

输入源码:

console.log('hello');
console.error('oops');

观察 AST 结构,发现 console.log('hello') 是一个 CallExpression (调用表达式)。

  • callee 是一个 MemberExpression (成员表达式 console.log)。

    • object: console (Identifier)
    • property: log (Identifier)

3.2 第二步:编写插件代码

一个 Babel 插件就是一个函数,返回一个包含 visitor 的对象。

// my-babel-plugin.js
module.exports = function({ types: t }) {
  return {
    name: "remove-console-log",
    visitor: {
      // 监听 CallExpression 节点
      CallExpression(path) {
        // 1. 获取 callee (被调用的函数,比如 console.log)
        const { callee } = path.node;

        // 2. 判断是否是成员表达式 (MemberExpression),且不是计算属性 (a['b'])
        if (t.isMemberExpression(callee) && !callee.computed) {
          
          // 3. 检查 object 是不是 'console',property 是不是 'log'
          if (t.isIdentifier(callee.object, { name: 'console' }) &&
              t.isIdentifier(callee.property, { name: 'log' })) {
            
            // 4. 这里的 path 就是 console.log('...') 这一整行
            // 直接由手术刀切除!
            path.remove();
          }
        }
      }
    }
  };
};

3.3 第三步:测试效果

输入:

function add(a, b) {
  console.log('debug:', a);
  console.error('fatal error');
  return a + b;
}

输出:

function add(a, b) {
  console.error('fatal error');
  return a + b;
}

Bingo! 你刚刚成功完成了一次代码外科手术。


四、 进阶:作用域 (Scope) 的魔咒

架构师和普通开发者的区别在于对副作用的考虑。 上面的插件有个 Bug:如果用户自己定义了一个叫 console 的变量怎么办?

function test() {
  const console = { log: () => {} };
  console.log('这不应该被删除'); // 我们的插件会错误地删除这一行!
}

4.1 作用域检查

Babel 的 path.scope 提供了强大的作用域分析能力。 我们需要检查:console 这个引用,是否绑定(Binding) 到了全局?还是被局部变量覆盖了?

优化后的代码:

CallExpression(path) {
  const { callee } = path.node;
  if (t.isMemberExpression(callee) && 
      t.isIdentifier(callee.object, { name: 'console' }) &&
      t.isIdentifier(callee.property, { name: 'log' })) {

    // 【新增】核心检查:获取 'console' 这个标识符的绑定信息
    const binding = path.scope.getBinding('console');
    
    // 如果没有绑定(说明是全局变量),才删除
    if (!binding) {
      path.remove();
    }
  }
}

现在,Babel 能够识别出局部变量 console,从而放过它。


五、 总结与脑洞:Babel 还能干什么?

通过这个简单的例子,你掌握了 Babel 插件的精髓:Find (Visitor) -> Check (Path/Types) -> Modify (Remove/Replace)

在架构设计中,Babel 插件有无限的潜力:

  1. 自动埋点: 找到所有的 Click 事件函数,在函数体第一行自动插入 track('click') 代码。
  2. 国际化 (i18n) 提取: 扫描所有中文字符串,自动提取到 JSON 文件中,并替换为 t('key')
  3. 代码卫士: 禁止使用某些落后的 API,一旦发现直接构建报错(比 ESLint 更暴力)。

结语:从手术刀到守门员

我们现在已经拥有了修改代码的手术刀(Babel Plugin)。 但是,手术刀太锋利了,容易伤人。在日常开发中,我们更多时候不需要“修改”代码,而是需要“检查”代码,或者进行大规模的、安全的“自动化重构”。

这时候,我们需要另一套基于 AST 的工具体系。

Next Step: 既然我们能分析代码结构,那是不是可以制定一套“代码法律”? 下一节,我们将探讨**《第三篇:工具——代码质量的守门员:ESLint 原理、自定义规则与 Codemod 自动化重构》**。我们将学习如何编写自定义的 ESLint 规则,以及如何用 Codemod 瞬间修改 1000 个文件。

❌
❌