ripple:基于 TypeScript 的 UI 框架项目

the elegant TypeScript UI framework

Branch22Tags3270
FilesLast commitLast 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 - 优雅的 TypeScript UI 框架

CI Discord 在 StackBlitz 中打开

Ripple TS

Ripple 是一个 TypeScript 优先的 UI 框架,围绕 .tsrx 文件构建,提供细粒度响应式、作用域样式和轻量运行时。它将 JSX 的编写体验与模板原生控制流相结合,并支持可直接置于所驱动 UI 旁的 TypeScript 配置。

@trueadm 创建,其曾为 InfernoReactLexicalSvelte 5 作出贡献。

.tsrx 也是一种独立语言。共享的 TSRX 编译器栈可以面向 React、Preact、Solid、Vue 和 Ripple 进行编译。Ripple 是面向运行时的编译目标,提供 track()、响应式集合、服务端模块、水合和 DOM 辅助工具。

Ripple 文档 | Ripple 试验场 | TSRX 网站

特性

  • 基于 track() 和惰性解构的细粒度响应式。
  • 响应式 RippleArrayRippleObjectRippleMapRippleSet
  • 模板原生的 @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

请使用 npmpnpmyarnbun,选择与项目匹配的工具。

挂载

// 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 模板分支内,直接使用 returncontinuebreak 语句是无效的。

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

Introduction

优雅的 TypeScript UI 框架【此简介由AI生成】

Customize your domain
287.39 K292Visit GitHub