ripple:基于 TypeScript 的 UI 框架项目

the elegant TypeScript UI framework

分支24Tags3357
Ripple - 优雅的 TypeScript UI 框架

CI Discord 在 StackBlitz 中打开

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。

项目介绍

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

定制我的领域
277.4 K295访问 GitHub