在最近一年中,Bobril 框架以其类似 React 的方式发展,带来了对 TSX(Type Safe JSX)的全面支持,包括有状态的类组件和无状态的函数式组件。TSX 是一种扩展 TypeScript 的模板语言,用于在一个地方定义 UI 和逻辑,生成 Bobril 元素。本文将介绍如何使用这两种类型的组件以及 TSX 创建一个 TODO 应用。
Bobril快速入门
首先,需要准备项目。创建项目的方式与之前的示例类似:
PowerShell
npm i bobril-build -g
npm init
npm i bobril bobx --save
然后添加 index.tsx 文件并运行:
PowerShell
bb
函数式组件
最简单的组件是函数式组件。它不保留任何状态,只是反映输入数据。它由一个函数定义,该函数有一个参数用于数据,并返回 b.IBobrilNode。为了遵循 TSX 组件的规则,函数的名称必须以大写字母开头。数据定义方式与通常相同。
创建 components/listItem.tsx 并编写以下代码:
import * as b from "bobril";
export interface IItem {
id: number;
text: string;
done: boolean;
}
export interface IItemData extends IItem {
index: number;
onItemChecked(index: number, value: boolean): void;
}
export function ListItem(data: IItemData): b.IBobrilNode {
return (
data.onItemChecked(data.index, value)}
/>
{data.text}
);
}
const strikeOut = b.styleDef({ textDecoration: "line-through" });
可以看到,组件的 TSX 定义非常简单。可以使用原生元素并用数据填充其属性作为 {expression}。在这个组件中,还可以看到 Bobril 键和样式的定义。
这样的组件可以作为 TSX 元素使用。将以下代码放入 components/list.tsx:
import * as b from "bobril";
import { ListItem, IItem } from "./listItem";
export interface IListData {
items: IItem[];
onItemChecked(index: number, value: boolean): void;
}
export function List(data: IListData): b.IBobrilNode {
return (
{data.items.map((item, index) => (
))}
);
}
const noBullets = b.styleDef({ listStyleType: "none" });
这是一个很好的例子,展示了如何使用内联 TypeScript 函数 map 生成内容。这个函数将输入数据映射成 TSX 元素列表。
接下来可以看到的是使用展开运算符 {...item} 将数据作为属性提供给子节点。
类组件
第二种组件类型是有状态的类组件。因为它是一个类,所以允许保留内部状态。
创建 components/form.tsx 中的表单组件:
import * as b from "bobril";
import { observable } from "bobx";
export interface IFormData {
onSubmit(value: string): void;
}
export class Form extends b.Component {
@observable private _value: string = "";
render(): b.IBobrilChildren {
return (
<>
this.updateValue(newValue)}
onKeyUp={ev => ev.which === 13 && this.submit()}
style={spaceOnRight}
/>
);
}
private updateValue(newValue: string): void {
this._value = newValue;
}
private submit(): boolean {
this.data.onSubmit(this._value);
this._value = "";
return true;
}
}
const spaceOnRight = b.styleDef({ marginRight: 5 });
可以看到它与一般组件定义非常相似。它有一个 render() 方法返回 b.IBobrilChildren。数据可以通过 this.data 访问。它必须派生自 b.Component。它还保留了 this._value 作为可观察属性的内部状态。
片段
在上面的代码中,可以看到 <>... 元素。它被称为片段,用于将子元素包装到一个虚拟节点中。它基本上用于定义返回多个根元素的组件。
插槽
有时,需要为布局创建特殊的组件。如果数据定义包含属性 children: b.IBobrilChildren,则可以简单地以表达式的形式作为 TSX 内容 {data.children} 添加。
如果需要组合更复杂的布局,则可以使用 Slots 模式。
创建 components/layout.tsx 并编写以下代码:
import * as b from "bobril";
export interface ILayoutData {
children: {
header: b.IBobrilChildren;
body: b.IBobrilChildren;
footer: b.IBobrilChildren;
};
}
export function Layout(data: ILayoutData): b.IBobrilNode {
return (
<>
{data.children.header}
{data.children.body}
{data.children.footer}
);
}
ILayoutData 的 children 具有复杂类型而不是 b.IBobrilChildren。它允许通过多个特定子属性定义内容。TypeScript 将确保正确使用它,因此它是类型安全的。
BobX 存储
接下来需要一个 BobX 存储,就像之前文章中所知道的那样。在 store.ts 中定义一个:
import { observable } from "bobx";
import { IItem } from "./components/listItem";
export class TodoStore {
@observable private _todos: IItem[] = [];
get list(): IItem[] {
return this._todos;
}
add(text: string): void {
this._todos.push({ id: Date.now(), text, done: false });
}
edit(index: number, value: boolean): void {
this._todos[index].done = value;
}
}
组合页面
最后,可以在 index.tsx 中组合逻辑和组件:
import * as b from "bobril";
import { Layout } from "./components/layout";
import { List } from "./components/list";
import { Form } from "./components/form";
import { TodoStore } from "./store";
class Todo extends b.Component {
todos = new TodoStore();
render(): b.IBobrilChildren {
return (
{{
header: ,
body: (
this.todos.edit(index, value)}
/>
),
footer:
);
}
}
b.init(() => );
可以看到它在 todos 属性中存储了内部状态,并通过表达式与对象用于插槽内容定义的方式从组件中创建布局。