the elegant TypeScript UI framework
| Files | Last commit | Last update |
|---|---|---|
| 4 days ago | ||
| 6 months ago | ||
| 6 months ago | ||
| 6 days ago | ||
| 6 months ago | ||
| 6 days ago | ||
| 3 months ago | ||
| 6 days ago | ||
| 18 days ago | ||
| 18 days ago | ||
| 4 days ago | ||
| 5 days ago | ||
| 18 days ago | ||
| 18 days ago | ||
| 18 days ago | ||
| 5 days ago | ||
| 6 days ago | ||
| 11 months ago | ||
| 18 days ago | ||
| 4 days ago | ||
| 4 days ago | ||
| 3 months ago | ||
| 6 days ago | ||
| 6 days ago | ||
| 6 months ago | ||
| 18 days ago | ||
| 6 days ago | ||
| 1 year ago | ||
| 6 days ago | ||
| 18 days ago | ||
| 4 days ago | ||
| 4 days ago | ||
| 4 days ago | ||
| 6 months ago | ||
| 18 days ago |
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()和惰性解构的细粒度响应式。 - 响应式
RippleArray、RippleObject、RippleMap和RippleSet。 - 模板原生的
@if、@for、@switch和@try。 - 本地 TypeScript 配置,支持 JSX 语句容器(
@{...})。 - 作用域
<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 元素;当存在初始化语句,或需要渲染多个并列元素时,使用 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() @{
let &[count] = track(0);
const increment = () => count++;
<button onClick={increment}>Count:{count}</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',
});
let &[showDone] = track(true);
const visibleItems = () => items.filter((item) => showDone || !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 设置中,请使用普通的 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() 和惰性解构创建状态。对惰性绑定的读取仍保持响应,赋值会写回被追踪的值。
import { effect, track, type Tracked } from 'ripple';
export function Counter() @{
let &[count, trackedCount] = track(0);
let &[double] = track(() => count * 2);
effect(() => {
console.log('Count changed:', count);
});
<>
<p>Count:{count}</p>
<p>Double:{double}</p>
<button onClick={() => count++}>Increment</button>
<CounterValue count={trackedCount} />
</>
}
function CounterValue({ count }: { count: Tracked<number> }) {
return <p>Shared value:{count.value}</p>;
}
Tracked<T> 对象也可以通过 .value 进行读取和写入,这在通过数据结构或 props 传递响应式值时非常有用。
响应式集合
当需要对集合操作进行响应式处理时,请使用 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 引用与事件
DOM 引用使用 ref,事件使用 JSX 风格的事件属性。
import { track } from 'ripple';
export function SearchBox() @{
let &[value] = track('');
let input: HTMLInputElement | undefined;
<>
<label>
Search
<input
ref={input}
value={value}
onInput={(event) => {
value = event.currentTarget.value;
}}
/>
</label>
<button onClick={() => input?.focus()}>Focus</button>
</>
}
作用域样式
<style> 块是静态 CSS,其作用域限于同级元素:一个块会为相邻的元素以及它们之下的所有元素提供样式,而不会为包含它的元素提供样式。对于运行时值,请使用 CSS 自定义属性。
import { track } from 'ripple';
export function Notice() @{
let &[tone] = track('rebeccapurple');
<>
<p class="notice" style={{ '--notice-color': tone }}>Scoped text</p>
<button
onClick={() => (tone = tone === '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() @{
let &[theme, themeTracked] = track('light');
ThemeContext.set(themeTracked);
<>
<ThemeLabel />
<button onClick={() => (theme = theme === '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() @{
let &[message] = track('Loading...');
effect(() => {
loadMessage().then((next) => {
message = next;
});
});
<p>{message}</p>
}
编辑器支持
安装 TSRX Syntax for VS Code 即可使用语法高亮、诊断、TypeScript 集成与自动补全。共享语言、编译器基础设施、格式化工具、代码检查工具以及编辑器集成均维护在 tsrx-org/tsrx 中。
资源
贡献
欢迎贡献。请参阅 CONTRIBUTING.md。
许可证
MIT 许可证 - 详情见 LICENSE。