【翻译】简化 TSRX

简化 TSRX

原文链接:tsrx.dev/blog/simpli...

原文作者:Dominic Gannaway(tsrx.dev)

2026 年 5 月 29 日


我们听取了你们的反馈,TSRX 正变得更小、更清晰,也更容易调试。

PR#1177 是对 TSRX 的一次大规模清理。目标很简单:保留那些让 UI 编写更顺手的部分,去掉那些让这门语言显得陌生的部分。

这来自正在试验 TSRX 的个人与公司反馈,也来自对 AI 生成输出的评估。同一种模式反复出现:人们和工具都喜欢内联 UI 结构,但在调试时会被新概念搞糊涂,尤其是缺少正常的 return 语句、early-return 魔法,以及过多 TSRX 专属语法点。

组件就是普通函数

在这次清理之前,TSRX 引入了自己的 component 形式。它移除了可见的 return 语句,让组件看起来不像普通的 TypeScript。

javascript 复制代码
component Profile({ user }: { user: User | null }) {
  if (!user) {
    return;
  }

  const displayName = user.name.trim();

  if (user.isAdmin) {
    <p>{displayName} can manage settings.</p>
  }

  const title = <tsx><strong>{displayName}</strong></tsx>;
  <Card {title} />
}

现在 TSRX 是你从普通函数里 return 的一个表达式。当组件只需要返回单个元素时,可以直接返回该元素。

javascript 复制代码
function Button({ label, onClick }: Props) {
  return <button {onClick}>{label}</button>;
}

我们还移除了纯 <tsx> 孤岛。TSRX 本身现在是表达式驱动的:原生元素和原生 fragment 可以被赋值、作为 prop 传递,或直接返回。旧的 <tsrx> 包装器也不再需要,因为在 .tsrx 文件中 TSRX 已经是默认语法。

javascript 复制代码
function Profile({ user }: { user: User }) {
  const displayName = user.name.trim();
  const title = <strong>{displayName}</strong>;

  return <Card {title} />;
}

原生 fragment 提供了 <tsx> 原本想带来的同样好处。一旦有一条更清晰、更一致、也更便于工具解释的路径,保留多种做法并没有带来多少额外价值。

当你返回 fragment 时,其内部体是语句驱动的:你可以声明值、用 if 分支、用 for...of 循环,并按顺序产出 UI。

javascript 复制代码
function Dashboard({ items }: Props) {
  return <>
    const visibleItems = items.filter((item) => !item.hidden);

    <ul>
      for (const item of visibleItems; key item.id) {
        <li>{item.label}</li>
      }
    </ul>

    try {
      <ActivityFeed />
    } pending {
      <p>Loading activity...</p>
    } catch (error) {
      <p>Could not load activity.</p>
    }
  </>;
}

所有有用的 TSRX 控制流都保持原样:列表仍可用 for...of 渲染,异步边界仍可使用 try / pending / catch,模板仍按 UI 产出的顺序阅读。

模板里不再有 return 魔法

更早的 component 模型还允许模板体用 return 提前退出。这看起来很方便,但会让调试变得怪异,因为 UI 输出与组件退出行为被混在一起。

javascript 复制代码
component Profile({ user }: { user: User | null }) {
  if (!user) {
    return;
  }

  <h1>{user.name}</h1>
}

现在守卫式 return 放在 TSRX 打开之前,就像普通 TypeScript 和 JSX 一样。在模板的 for...of 循环内部,continue 仍会跳过当前项。

javascript 复制代码
function Profile({ user }: { user: User | null }) {
  if (!user) {
    return null;
  }

  return <h1>{user.name}</h1>;
}

React 和 Preact 在这里仍会得到一次有用的目标特定编译器处理。若在 hook 之前出现守卫式 return,TSRX 可以保持源码简洁,同时把含 hook 的路径编译成能保持 hook 顺序的形态。

javascript 复制代码
function Profile({ user }: { user: User | null }) {
  if (!user) {
    return null;
  }

  useEffect(() => {
    trackProfile(user.id);
  }, [user.id]);

  return <ProfileCard {user} />;
}

更少的特例

Ripple 在旧的 component 形态里积累了一些 TSRX 专属捷径:原始 HTML 块、ref 块,以及 style class 表达式。它们能工作,但每一项都是另一条需要记住的规则,也是工具需要解释的另一样东西。

javascript 复制代码
component Article({ contentHtml, inputRef }: Props) {
  <article>{html contentHtml}</article>
  {html contentHtml}
  <input {ref inputRef} />
  <Child class={style 'card'} />

  <style>
    .card { padding: 1rem; }
  </style>
}

这些现在都变成了普通 prop。Ripple、Solid 和 Vue 在 DOM 元素上使用 innerHTML。Ripple 和 Solid 也可以在不想加包装元素插入 HTML 时使用 <Fragment innerHTML={...} />。React 和 Preact 使用 dangerouslySetInnerHTML。ref 使用 ref={...},作用域 class 可以通过 const styles = <style>...</style> 与子组件共享。

React TSRX 不会把你手写的 class prop 改写成 className。当你的 React 组件期望 className 时请使用 className;编译器不会替你掩盖这一差异。

对于 React 宿主元素上的作用域样式,TSRX 仍会为生成的 CSS hash 输出 className。那是生成结果,而不是对你亲手写的 class 的改写。

去掉旧的 classclassName 魔法,让生成代码对人类和大语言模型都更容易预测。

style 表达式只是一个值。把它放在返回内容之前,然后在该值处于作用域内的任何地方传递 styles.card

javascript 复制代码
function Article({ contentHtml, inputRef }: Props) {
  const styles = <style>
    .card { padding: 1rem; }
  </style>;

  return <>
    // Ripple、Solid 与 Vue 宿主元素
    <article innerHTML={contentHtml} />

    // Ripple 与 Solid 的 fragment 输出
    <Fragment innerHTML={contentHtml} />

    // React 与 Preact 宿主元素
    <article dangerouslySetInnerHTML={{ __html: contentHtml }} />

    <input ref={inputRef} />

    // Ripple、Preact、Solid 与 Vue 组件 prop
    <Child class={styles.card} />

    // React 组件 prop
    <Child className={styles.card} />
  </>;
}

样式仍是可选的

<style> 块仍然存在,但其心智模型更清晰:样式作用域绑定在包含它的 TSRX fragment 上,而不是绑定在单独的组件声明形式上。对于同位样式,行为与之前相同。

<style> 内写静态 CSS。元素内部的 JavaScript 语句与表达式规则在那里不适用:没有 iffor{expr}。当样式值需要在运行时变化时,在元素上设置 CSS 自定义属性,并用 var(...) 读取。

javascript 复制代码
function Card({ title, tone }: Props) {
  return <>
    <article class="card" style={{ '--card-tone': tone }}>
      <h2>{title}</h2>
    </article>

    <style>
      .card {
        padding: 1rem;
        border-radius: 0.75rem;
        border-color: var(--card-tone);
      }
    </style>
  </>;
}

当你把样式移到返回模板之前的值里时,模型有意更接近 StyleX 和其他流行的 CSS-in-JS 方案:CSS 仍是静态的,但你得到声明式的 class 映射,并在需要的地方显式传递这些 class 名。

javascript 复制代码
const styles = <style>
  .card { padding: 1rem; }
  .title { font-weight: 700; }
</style>;

function ArticleCard({ title }: Props) {
  // Ripple、Preact、Solid 与 Vue 宿主元素:
  return <article class={styles.card}>
    <h2 class={styles.title}>{title}</h2>
  </article>;

  // React 宿主元素:
  // 示例:return <article className={styles.card}>
  // 示例:<h2 className={styles.title}>{title}</h2>
  // 示例:</article>;
}

这是可选的。如果你的团队更喜欢 Tailwind、CSS modules、原生 CSS 或设计系统的 class API,继续用那些即可。TSRX 不要求同位 <style> 块。

javascript 复制代码
function Card({ title }: Props) {
  return <article class="rounded-xl p-4">{title}</article>;
}

为什么这很重要

TSRX 应该让 UI 代码更易读,而不是更难推理。这些改动让这门语言更接近 TypeScript、更接近 JSX,也让人们和 AI 工具都更容易调试。

结果是一门更小的语言,却保留同样的核心思想:从表达式打开 TSRX,然后在其中写出清晰的、语句驱动的 UI。

相关推荐
IT乐手2 小时前
佛德角逼平西班牙,国足还有啥借口?
前端
JustHappy3 小时前
我汇总了身边朋友的经历才发现,其实第一份实习是最难找的......
前端·后端·面试
星栈3 小时前
Dioxus 的响应式系统:`Signal`、`Memo`、`Effect` 和异步状态到底该怎么分工
前端·前端框架
yingyima3 小时前
Java 正则表达式:比你想象的更强大
前端
yuanyxh6 小时前
macOS 应用 - 纯对话生成
前端·macos·ai编程
大家的林语冰6 小时前
ES5 凉凉,Babel 8 正式发布,默认不再编译为 ES5 和 CJS......
前端·javascript·前端工程化
光影少年7 小时前
react批量更新、同步/异步更新场景
前端·react.js·掘金·金石计划
假如让我当三天老蒯7 小时前
模块化:ES Module 与 CommonJS 的区别
前端·面试
用户40950115773177 小时前
Private Forge v2.0 发布:12大前端业务场景技能系统
前端