简化 TSRX
原文作者: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 的改写。
去掉旧的 class 到 className 魔法,让生成代码对人类和大语言模型都更容易预测。
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 语句与表达式规则在那里不适用:没有 if、for 或 {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。