the elegant TypeScript UI framework
当前访问频次受限,请登录后继续访问
Ripple TS
Ripple 是一个以 TypeScript 为先的 UI 框架,围绕 .tsrx 文件、细粒度响应式、作用域样式和小型运行时构建。它将 JSX 的编写体验与模板原生的控制流相结合,并支持可直接放在所服务 UI 旁边的 TypeScript 配置。
由 @trueadm 创建,他曾为 Inferno、 React、 Lexical 以及 Svelte 5 做出贡献。
.tsrx也是一种独立语言。共享的 TSRX 编译器栈可面向 React、Preact、Solid、Vue 和 Ripple。Ripple 是面向运行时的目标,提供track()、响应式集合、服务端模块、水合和 DOM 辅助功能。
Ripple 文档 | Ripple 实验场 | TSRX 官网
特性
- 通过
track()和.value实现细粒度响应式。 - 响应式
RippleArray、RippleObject、RippleMap和RippleSet。 - 模板原生
@if、@for、@switch和@try。 - 支持 JSX 语句容器(
@{...})的本地 TypeScript 配置。 - 支持自动类名哈希的作用域
<style>块。 - 支持 Vite、编辑器、Prettier、ESLint、SSR(缓冲与流式)以及水合。
快速上手
使用 CLI
npx create-ripple
cd my-app
npm install
npm run dev
使用模板
npx degit Ripple-TS/ripple/templates/basic my-app
cd my-app
npm install
npm run dev
添加至现有项目
npm install ripple @ripple-ts/vite-plugin
使用 npm、pnpm、yarn 或 bun,与项目保持一致。
挂载
// index.ts
import { mount } from 'ripple';
import { App } from './App.tsrx';
mount(App, {
props: { title: 'Hello world!' },
target: document.getElementById('root'),
});
核心语法
组件
组件即为普通的 TypeScript 函数。若组件仅有一个根节点,可直接返回一个 JSX 元素;若需将设置语句或多个同级渲染节点置于 UI 旁边,则使用 JSX 语句容器(@{...})。
type ButtonProps = {
text: string;
onClick: () => void;
};
export function Button({ text, onClick }: ButtonProps) {
return <button class="button" {onClick}>{text}</button>;
}
export function App() {
return <Button text="Click me" onClick={() => console.log('Clicked!')} />;
}
片段在组件确实返回多个同级节点时依然有用,例如标记加上一个作用域 <style> 块。
局部 TypeScript
普通 JSX 子节点为文本、元素、注释以及 {...} 表达式容器。当某个作用域在渲染前需要 TypeScript 设置时,请使用 JSX 语句容器:@{...}。设置会先执行,并且该容器最终恰好产生一个输出节点:一个 JSX 元素、一个 JSX 片段,或一个 JSX 控制流表达式。如果设置之后还需要文本、表达式容器或多个同级节点,请将它们包裹在片段中。
位于标签之间、形如 x = 123 的文本是 JSX 文本,而不是 JavaScript,除非它位于语句容器内。
import { track } from 'ripple';
export function Counter() @{
const count = track(0);
const increment = () => count.value++;
<button onClick={increment}>Count:{count.value}</button>
}
该规则在嵌套作用域中同样适用:
export function Cart({ items }: { items: Item[] }) @{
<div class="cart">@{
const subtotal = items.reduce((sum, item) => sum + item.price, 0);
const discount =
subtotal > 100 ? 0.1 : 0;
<>
<p>Subtotal: ${subtotal}</p>
<p>Save: ${(subtotal * discount).toFixed(2)}</p>
</>
}</div>
}
JavaScript 注释可以位于模板子节点之间,并且不会被渲染。
文本与表达式
静态文本是 JSX 文本。动态值使用普通 JSX 表达式容器。
export function Greeting({ name }: { name?: string }) @{
@if (name) {
<p>Hello,{name}</p>
} @else {
<p>Hello, stranger</p>
}
}
控制流
渲染控制流使用带指令前缀的表达式:
import { RippleArray, track } from 'ripple';
type Item = { id: number; name: string; done?: boolean };
export function TodoList() @{
const items = new RippleArray<Item>({ id: 1, name: 'Plan the work' }, {
id: 2,
name: 'Ship the work',
});
const showDone = track(true);
const visibleItems = () => items.filter((item) => showDone.value || !item.done);
<ul>
@for (const item of visibleItems(); index i; key item.id) {
<li>
{i + 1}
.
{item.name}
</li>
} @empty {
<li>No todos to show</li>
}
</ul>
}
在 TypeScript 的 setup 中,使用普通的 return 来实现真正的函数返回。使用 @if 进行条件渲染;在 @if 模板分支内部,直接使用 return、continue 和 break 语句无效。
export function Dashboard({ user }: { user: User | null }) @{
if (!user) {
return null;
}
<>
<h1>Welcome,{user.name}</h1>
<p>Here is your dashboard.</p>
</>
}
@try 支持错误与加载中的 UI:
export function ProfilePanel() @{
@try {
<UserProfile />
} @pending {
<p>Loading...</p>
} @catch (error, reset) {
<div>
<p>Error:{error.message}</p>
<button onClick={() => reset()}>Try again</button>
</div>
}
}
响应式
使用 track() 创建状态,并通过 .value 进行读取或写入。模板和副作用中的读取保持响应式,写入则会更新所有依赖它们的处所。
import { effect, track, type Tracked } from 'ripple';
export function Counter() @{
const count = track(0);
const double = track(() => count.value * 2);
effect(() => {
console.log('Count changed:', count.value);
});
<>
<p>Count:{count.value}</p>
<p>Double:{double.value}</p>
<button onClick={() => count.value++}>Increment</button>
<CounterValue {count} />
</>
}
function CounterValue({ count }: { count: Tracked<number> }) {
return <p>Shared value:{count.value}</p>;
}
Tracked<T> 对象可以通过数据结构和 props 传递:当子组件可能写入它时,传递被跟踪的对象本身;当子组件只需读取它时,传递 trackReadOnly(count)(一个 Derived<T>,它会跟随值的变化但拒绝写入,效果等同于 track(() => count.value))。
响应式集合
当集合操作需要响应式时,请使用 Ripple 集合。
import { RippleArray, RippleMap, RippleObject, RippleSet } from 'ripple';
export function Inventory() @{
const items = new RippleArray({ id: 1, name: 'Jacket' });
const totals = new RippleObject({ selected: 0 });
const prices = new RippleMap([[1, 120]]);
const selected = new RippleSet<number>();
<>
<ul>
@for (const item of items; key item.id) {
<li>{item.name}: ${prices.get(item.id)}</li>
}
</ul>
<button onClick={() => selected.add(1)}>Select first item</button>
<p>
Selected:
{selected.size + totals.selected}
</p>
</>
}
DOM refs 与事件
DOM refs 使用 ref,事件使用 JSX 风格的事件属性。
import { track } from 'ripple';
export function SearchBox() @{
const query = track('');
let input: HTMLInputElement | undefined;
<>
<label>
Search
<input
ref={input}
value={query.value}
onInput={(event) => {
query.value = event.currentTarget.value;
}}
/>
</label>
<button onClick={() => input?.focus()}>Focus</button>
</>
}
作用域样式
<style> 块是静态 CSS,其作用域仅覆盖兄弟元素:该块只会影响相邻的元素以及这些元素内部的内容,而不会影响包含它的元素。
使用 CSS 自定义属性表示运行时值。
import { track } from 'ripple';
export function Notice() @{
const tone = track('rebeccapurple');
<>
<p class="notice" style={{ '--notice-color': tone.value }}>Scoped text</p>
<button
onClick={() => (tone.value = tone.value === 'rebeccapurple'
? 'tomato'
: 'rebeccapurple')}
>Toggle tone</button>
<style>
.notice {
color: var(--notice-color);
font-weight: 700;
}
</style>
</>
}
一个赋值给变量的 <style> 块会成为一个主题:一个包含 $class(其哈希类名),以及每个类选择器对应一个键的对象。将这些字符串作为 props 传入,通过 <style apply={theme} /> 将整个主题应用到某个作用域,或让单个元素通过 class={theme.$class} 加入:
export const theme = <style>
article {
font-family: system-ui;
}
.highlight {
background: #e8f5e9;
}
</style>;
export function Badge() {
return <span class={theme.highlight}>New</span>;
}
export function Card() @{
<>
<style apply={theme}>
h2 {
margin: 0;
}
</style>
<article>
<h2>Themed, with a local override</h2>
</article>
</>
}
Context 和 Portals
import { Context, Portal, track, type Tracked } from 'ripple';
const ThemeContext = new Context<Tracked<string>>();
export function App() @{
const theme = track('light');
ThemeContext.set(theme);
<>
<ThemeLabel />
<button onClick={() => (theme.value = theme.value === 'light' ? 'dark' : 'light')}>
Toggle theme
</button>
<Portal target={document.body}>
<p>Portal content</p>
</Portal>
</>
}
function ThemeLabel() @{
const theme = ThemeContext.get();
<p>Theme:{theme.value}</p>
}
服务端模块
Ripple 支持在 .tsrx 文件中通过 module server 进行面向服务端的导出。
在同一文件内,请先从 server 导入,然后再调用服务端函数。
module server {
export async function loadMessage() {
return 'Loaded on the server';
}
}
import { loadMessage } from server;
import { effect, track } from 'ripple';
export function Page() @{
const message = track('Loading...');
effect(() => {
loadMessage().then((next) => {
message.value = next;
});
});
<p>{message.value}</p>
}
编辑器支持
安装 TSRX Syntax for VS Code 以启用语法高亮、诊断、TypeScript 集成和代码补全。共享的 语言、编译器基础设施、格式化器、代码检查器以及编辑器 集成均由 tsrx-org/tsrx 维护。
资源
贡献
欢迎贡献。请查看 CONTRIBUTING.md。
许可证
MIT 许可证 - 详细信息见 LICENSE。