🐶 Recks 入门:Rx+JSX 实验
我喜欢 React,也喜欢 RxJS,所以我尝试将它们融合到一个新框架中:
import { timer } from 'rxjs';
function App() {
const ticks$ = timer(0, 1000);
return <div>
<h1>{ ticks$ }</h1>
<p>seconds passed</p>
</div>
}
tl;dr
前言
我花了大约一周时间为一次黑客马拉松开发了这个渲染引擎。事实证明,这是一个很有趣的概念,我想在这里和大家分享一下!
概念
React 通过虚拟 DOM 将 DOM 置于 JS 代码的“一等公民”地位。我们可以在代码结构中的任何位置创建虚拟 DOM,并将其传递出去。React
的组件本质上就是将属性映射到虚拟 DOM:
// React
(props: Object) => vDOM
Angular 将 Observable 流深度集成到其组件和服务中,使其成为原生功能。Observable 使我们能够轻松地操作和协调随时间推移而变化的异步事件和更新。
在这个框架中,我们(类似于 React)将属性映射到虚拟 DOM。不同之处在于,我们完全控制更新和渲染流。我们获取 props 的输入流,并将其映射到虚拟 DOM 的输出流:
// This framework
(props$: Observable<Object>) => Observable<vDOM>
输入流。输出流。
咱们来看例子吧?
基本用法
当然,我们得从“Hello World”开始:
import { of } from 'rxjs';
function App() {
return of(<h1>Hello world!</h1>)
}
of创建一个 Observable,该 Observable 会发出一个提供的单个值。
由于我们的组件渲染的是静态元素<h1>,并且永远不会更新它,所以我们可以跳过 Observable 部分,直接返回该元素:
function App() {
return <h1>Hello world!</h1>
}
看起来很像 React 风格,对吧?让我们给组件增添更多活力:
定时器
import { timer } from 'rxjs';
import { map } from 'rxjs/operators';
function TimerApp() {
return timer(0, 1000).pipe(
map(tick =>
<div>
<h1>{ tick }</h1>
<p>seconds passed</p>
</div>
)
)
}
timer(n, m)发出一个0at n,然后以m区间 [ ]发出一系列整数。
我们的组件再次返回一个 vDOM 流。每次组件发出值时,vDOM 都会更新。
在这个例子中,timer它将每秒发出一个新值。我们将把这个值map添加到一个新的虚拟 DOM 中,并tick在其中显示每个值<h1>。
我们还可以做得更简单!
如果 vDOM 中的子元素本身就是一个 Observable,引擎就会开始监听它并渲染它的值。所以,让我们把这个timerObservable 直接移到<h1>:
import { timer } from 'rxjs';
function TimerApp() {
const ticks$ = timer(0, 1000);
return <div>
<h1>{ ticks$ }</h1>
<p>seconds passed</p>
</div>
}
这使我们能够用简洁的语法定义更精细的更新。
请注意,组件函数只会调用一次。当 Observabletimer(0, 1000)发出值时,vDOM 会立即更新,而不会重新计算或更新树的其他部分。
状态
当我们需要在组件中拥有本地状态时,我们可以创建一个或多个Subject来进行写入和监听。
主题是可观察对象,我们也允许向其中推送值。因此,我们既可以监听事件,也可以发出事件。
举个例子:
import { Subject } from 'rxjs';
import { map, startWith } from 'rxjs/operators';
function GreetingApp() {
const name$ = new Subject();
const view$ = name$.pipe(
map(x => x ? `Hello, ${x}!` : ''),
startWith('')
);
return <div>
<input
placeholder="enter your name"
onInput={e => name$.next(e.target.value)}
/>
{ view$ }
</div>
}
在上面的例子中,当文本字段发出input事件时,我们会将其值推送到name$流中。view$我们显示的流源自name$输入流。
请注意,我们使用了一个startWith运算符来处理冒号view$(:),以优化渲染。引擎会等待所有子元素发出第一个值后再进行渲染。因此,如果我们移除冒号 ( startWith—),<div>则渲染结果将为空,直到子元素view$发出值为止。所以,我们需要添加一个startWith运算符,或者将 Observable 子元素包装在一个静态子元素中,例如:<span>{ view$ }</span>
还有一个更常见的例子,用计数器来表示:
function CounterApp() {
const input$ = new Subject();
const view$ = input$.pipe(
startWith(0),
scan((acc, curr) => acc + curr)
);
return <div>
<button onClick={ ()=>input$.next(-1) }>minus</button>
{ view$ }
<button onClick={ ()=>input$.next( 1) }>plus</button>
</div>
}
在这个例子中,我们再次使用一个input$Subject 来推送更新。Observable会累积来自scanview$操作符的发射,并显示我们的状态。例如,当我们向 Subject推送更新时,我们会收到一个Observable 对象。input$1, 1, 1input$1, 2, 3view$
裁判或“真正的DOM交易”
有时我们需要与 DOM API 进行交互。为此,React使用了一些特殊ref对象,这些对象在其属性中包含对当前 DOM 元素的引用current:
// A React component
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
inputEl.current.focus(); // `current` points to the mounted text input element
};
return (
<div>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
<div/>
);
}
当然,在这个框架下,我们会得到一个 DOM 引用流!一旦 DOM 元素被创建或替换,引擎就会将一个新的引用推送到这个流中。我们只需要为引擎提供一个存放引用的位置——一个Subject。引擎会在 HTML 元素附加到实际 DOM 后,将其推送到 Subject 中。这样,我们就得到了一个引用流HTMLElements,并且可以将我们的逻辑应用于每次更新或最新的引用。
这里我们将重点关注<input />每次<button/>点击事件:
// This framework
function TextInputWithFocusButton() {
const ref$ = new Subject();
const clicks$ = new Subject();
clicks$
.pipe(withLatestFrom(ref$, (_, ref) => ref))
.subscribe(ref => {
ref.focus();
});
return (
<div>
<input ref={ref$} type="text" />
<button onClick={ ()=>clicks$.next(null) }>Focus the input</button>
</div>
);
}
子组件
到目前为止,我们的组件只返回 Observable 结果,无需响应任何输入。以下是一个父组件向子组件提供属性的示例:
import { timer } from 'rxjs';
import { map } from 'rxjs/operators';
function Parent () {
return <div>{
timer(0, 1000).pipe(
map(i => <Child index={i} />)
)
}</div>
}
function Child (props$) {
const animal$ = props$.pipe(
map(props => props.index % 2 ? '🐱' : '🐭')
)
return <h1 style="text-align: center;">{animal$}</h1>
}
当组件首次渲染Parent时,引擎会创建一个子组件,并将props 对象推送到子组件的Observable 中。子组件会立即做出响应,显示鼠标指针🐭。Child<Child index={ 0 } />Child{ index: 0 }props$
之后当它timer再次发出滴答声并发出声音时<Child index={ 1 } />——发动机只会推动{ index: 1 }现有的Child props$。
它Child现在会产下一只猫🐱。
等等。
(图片来自Unsplash ,摄影师: Michael Sum)
重制版
对于大型应用,我们需要更复杂的状态管理,而不仅仅是一堆 Subject。任何以 Observable 方式输出的实现都可以与 Recks 配合使用!我们来试试redogs状态管理器——它将redux、redux-observable和typesafe-actions集成在一个小巧的包中。redogs 的输出是一个 Observable,所以我们可以轻松地集成它!
让我们发挥创意,以创建一个简单的待办事项清单应用为例吧🙂
首先,我们将创建商店:
import { createStore } from 'redogs';
import { reducer } from './reducer';
import { effects } from './effects';
export const store = createStore(reducer, effects);
现在我们可以在组件中访问 store 的状态变化:
import { store } from './store';
function ItemListComponent() {
const items$ = store.state$.pipe(
map(state =>
state.items.map(item => (
<ItemComponent key={item.id} data={item} />
))
)
);
return <div>{items$}</div>;
}
或者向其分发事件:
import { store } from './store';
function AddItemComponent() {
const addItem = event => {
event.preventDefault();
const input = event.target['title'];
store.dispatch(
addItemAction({
title: input.value
})
);
input.value = '';
};
return (
<form onSubmit={addItem}>
<input name="title" type="text" autocomplete="off" />
<button type="submit">Add</button>
</form>
);
}
为简洁起见,这里我将省略 reducer、effects 和其他组件的展示。请访问codesandbox 查看完整的 Redux 应用示例。
请注意,我们不需要学习reselect任何re-reselectAPI 就可以与 Redux 交互。
我们无需调整专有技术static getDerivedStateFromProps(),也无需担心UNSAFE_componentWillReceiveProps()如何UNSAFE_componentWillUpdate()高效地使用该框架。
我们只需要了解可观察对象,它们是 Recks 中的通用语言。
与 React 不同,React 是另一种类型的应用。
React 组件要触发自身更新,必须更新其状态或属性(间接更新)。React 会自行决定何时重新渲染组件。如果您想避免不必要的重新计算和重新渲染,可以使用一些 API 方法(或钩子)来告诉React 如何处理您的组件。
在这个框架中,我希望使这个流程更加透明和可调整:您可以根据输入流直接操作输出流,使用众所周知的 RxJS 操作符:filter 、 debounce 、throttle、audit、 sample 、scan、buffer等等。
您可自行决定何时以及如何更新组件!
地位
Recs 源代码已发布在github.com/recksjs/recks
要试用该框架,您可以:
-
在在线沙盒环境中运行
-
或者,您可以通过以下方式克隆模板仓库:
git clone --depth=1 https://github.com/recksjs/recks-starter-project.git
cd recks-starter-project
npm i
npm start
该软件包也可以通过以下方式获取npm i recks,您只需设置您的 JSX 转译器(babel、typescript 编译器)以使用Recks.createElementpragma 即可。
【警告】这只是一个概念,并非可用于生产的库。
免责声明
首先,我曾多次将这个库称为“框架”,但它其实并不比 React 更像一个“框架”。所以,你也可以称它为“工具”或“库”。这完全取决于你 🙂
此外,我与 React 的比较仅限于概念层面。React 是一个成熟的框架,由一支优秀的专业团队维护,并拥有一个充满活力的社区。
这个已经一周大了,是我做的🐶
替代方案
有一个库提供了用于与 Observable 交互的 React Hook:rxjs-hooks。它的工作原理是通过一个useStateHook,在每次 Observable 发出事件时更新组件的状态,从而触发组件的重新渲染。值得一试!
这里我还应该提一下另一个重要的框架:André Staltz 开发的Cycle.js ,它是一款真正的流式框架。它拥有众多支持者和强大的集成能力。Cycle.js 的 API 与其他框架略有不同,它使用子组件并与 DOM 交互。不妨一试!
如果您知道其他替代方案,请分享。
结尾
好了,就到此为止!
这个项目是否应该继续开发?
您希望接下来看到哪些功能?
我很想知道您的想法,请留言告诉我哦🙂
如果您喜欢这篇文章,请点击“爱心”并分享:这将让我了解这个话题的实用性,也能帮助其他人发现这篇文章。
在接下来的文章中,我们将回顾 Recks 的其他集成,我还会分享功能规划并发布项目更新。请在dev.to和Twitter上关注我,以便及时了解最新动态!
我很荣幸你能读到这里!
谢谢!
结束
题图照片由Matthew Smith拍摄,来自Unsplash。
文章来源:https://dev.to/kosich/recks-rxjs-based-framework-23h5

