@abradley2/frpuccino 中文文档教程
FRPuccinno
:warning: 这仍处于 alpha 开发阶段。 我很想得到更多的人 对此进行测试并发现要解决的问题,但我不推荐 在任何需要可靠视图层的大型项目上使用它! :warning:
FRPuccino 是一个建立在 Most.js 基础上的小型 UI 库,
这是一个简短的“反例”示例:
import { createElement, createApplication } from '@abradley2/frpuccino'
function update (model, addValue) {
return model + addValue
}
function view () {
return <div>
<button onclick={1}>Clicked {value} times!</button>
</div>
}
createApplication({
mount: document.getElementById('app'),
update,
view,
init: 0
}).run(0)
Installation
@most/core 和其他一些相关的库 是对等依赖。
npm install --save @most/core @most/scheduler @most/types @abradley2/frpuccino
你也可能会发现 @most/dom-事件 和 @most/adaptor 非常有用。 但只加 他们,因为你发现需要。 我确实建议您查看每个文件的自述文件,这样您 可以识别何时需要出现。
Core Concepts
你实际上不需要 熟悉 FRP 利用 法式布奇诺。 响应式编程更像是 API 的底层引擎 API本身。 但是,如果您熟悉 的概念 流 和 接收器。
API Usage and Tutorial
创建基本应用程序只需要两种方法: createElement
和 createApplication
Function: createElement
createElement
是 FRPiccino 的“React.createElement”替代品。
这个 createElement 是唯一的。 它返回一个未挂载的 DOM 元素,其中 由所有绑定的事件处理程序组成的 Stream
。
这是一个简短的说明性示例
/** @jsx createElement */
import { createElement } from '@abradley2/frpuccino'
import { newDefaultScheduler } from '@most/scheduler'
const el = <div>
<button onclick='Hello there'>Say Hello</button>
<button onclick='Farewell for now!'>Say Goodbye</button>
</div>
document.body.appendChild(el)
const sink = {
event: (t, message) => { alert(message) },
end: () => {},
error: () => {}
}
el.eventStream.run(sink, newDefaultScheduler())
上面的 el
是一个简单的 Element
,我们可以将其附加到我们的文档中。 但是 createElement
也会将所有已注册的事件处理程序投射到它上面 Element
及其所有子元素到单个 eventStream
(或 Stream
)在顶部节点。
我们可以像运行任何常规程序一样运行此流 @most/core stream
因为我们所有的事件处理程序都绑定到事件流类型的字符串 是 Stream
。
Function: createApplication
createElement
很酷,但我们不能单独创建复杂的 用户界面。 createApplication
是一种“循环” createElement
返回的 eventStream
到 update
函数中。
这个update
的循环-> <代码>事件 -> <代码>创建元素 -> update
形成了 所有应用程序的基本流程。
createApplication({
// this is our initial update value!
init: 0,
// this is what our user interface looks
view: (currentState) =>(<div>
<button onclick={1}>Clicked {currentState} times</button>
</div>),
update: (currentState, value) => currentState + value,
// we need a place to attach our view to the document
mount: document.getElementById('application')
})
// to kick off the stream we need to emit an initial value.
// here we emit "0" because we won't want to increment our state
// until the user actually clicks the button
.run(0)
Type: TaskCreator<Action>
到目前为止,我们的示例仅处理绑定的事件处理程序 在我们看来。 如果我们想要执行并响应一个 HTTP 请求怎么办? 如果我们想听一个超出我们视野范围的事件怎么办 (例如window.onscroll
)? 我们可以用 任务 为此。
任务是辅助函数,允许我们将事件传播到 Sinks
当我们调用 createApplication
一个 内部接收器 创建它做几件事。 它订阅了 Stream
我们的 view
和 update
函数返回的事件, 以及通过将 update
与 view
组合在一起创建的结果 DOM 节点。 该定义类似于
Sink<{eventStream: Stream<{action: Action}>, view?: Element}>
创建一个“任务创建器” 计划任务 将事件传播到此 Sink
将 看起来像这样:
import { TaskCreator } from '@abradley2/frpuccino'
import { now, propagateEventTask } from '@most/core'
import { asap } from '@most/scheduler'
export function propagateEvent <Action> (action: Action): TaskCreator<Action> {
const event = { eventStream: now({ action }) }
return (sink, scheduler) => {
const task = propagateEventTask(event, sink)
return asap(task, scheduler)
}
}
Type: UpdateResult<Model, Action>
update
主函数允许返回的不仅仅是下一个版本 模型
。 它也可能返回一个包含模型的数组作为第一个 项,以及 TaskCreator
或 TaskCreator
作为 第二项。
我们可以更改我们原来的 update
函数,这样当我们的应用程序启动时, 我们使用 propagateEvent
函数开始计数器递增 once by a value of "1"
function update (currentState, value) {
// recall that we specified "0" as our initial action to dispatch when
// our application starts.
if (value === 0) {
return [currentState, propagateEvent(1)]
}
return currentState + value
}
UpdateResult
非常灵活。 我们不仅可以给一个 由于更新而要执行的计划任务,但是很多。
if (value === 0) {
return [
currentState,
[
propagateEvent(1),
propagateEvent(1)
]
]
}
Method: mapElement
类似于我们如何从 Stream 中
我们可以使用 map
Stream
-> 流mapElement
来转换一个元素的 eventStream
到另一个 Stream
类型。 这有助于避免以下情况 由于组合了许多不同的模块化功能,我们最终得到 Stream
关于我们应用程序的更新功能。
这是避免更新功能必须的说明性示例 处理 number | 类型的事件 string
通过归一化 所有 eventStreams
到 Stream
function button () {
return <div>
<button onclick={1}>Click me</button>
</div>
}
function input () {
return <div>
<input onchange={(e) => e.target.value} />
</div>
}
function application () {
return <div>
<div>
Count by one:
{button()}
</div>
<div>
Count by input:
{mapElement(
(payload) => {
if (!Number.isNaN(result)) return result
return 0
},
input()
)}
</div>
</div>
}
:警告:由于性能原因,mapElement
实际上发生了变异 传递给它的 Element
。 始终使用构造函数来传递 此函数的第二个参数:警告:
FRPuccinno
:warning: This is still in alpha development. I would love to get more people testing this out and finding issues to be stamped out, but I wouldn't recommend using this on any big projects that require a reliable view layer just yet! :warning:
FRPuccino is a small UI library built on the foundation of Most.js
It's inspired heavily by Elm and Cycle.js
Here's a short "counter" example:
import { createElement, createApplication } from '@abradley2/frpuccino'
function update (model, addValue) {
return model + addValue
}
function view () {
return <div>
<button onclick={1}>Clicked {value} times!</button>
</div>
}
createApplication({
mount: document.getElementById('app'),
update,
view,
init: 0
}).run(0)
Installation
@most/core and a few other associated libraries are peer dependencies.
npm install --save @most/core @most/scheduler @most/types @abradley2/frpuccino
You will also likely find @most/dom-event and @most/adaptor very useful. But only add them as you find the need. I do recommend checking out the README of each so you can recognize when that need arises.
Core Concepts
You do not actually need to be familiar with FRP to make use of FRPuccino. Reactive Programming is more the underlying engine of the API than the API itself. It will be very helpful, however, if you are familiar with the concepts of Streams and Sinks.
API Usage and Tutorial
Only two methods are needed to create basic applications: createElement
and createApplication
Function: createElement
createElement
is FRPuccino's "React.createElement" drop-in replacement.
This createElement is unique. It returns an un-mounted DOM element, with a Stream
consisting of all bound event handlers.
Here's a short illustrative example
/** @jsx createElement */
import { createElement } from '@abradley2/frpuccino'
import { newDefaultScheduler } from '@most/scheduler'
const el = <div>
<button onclick='Hello there'>Say Hello</button>
<button onclick='Farewell for now!'>Say Goodbye</button>
</div>
document.body.appendChild(el)
const sink = {
event: (t, message) => { alert(message) },
end: () => {},
error: () => {}
}
el.eventStream.run(sink, newDefaultScheduler())
Our el
above is a simple Element
that we can append to our document. But createElement
also casts all the registered event handlers on that Element
and all it's children to a single eventStream
(or Stream<Action>
) at the top node.
We can run this stream similar to how we'd run any regular @most/core stream
Because all our event handlers are bound to strings the type of the event stream is Stream<string>
.
Function: createApplication
createElement
is cool, but by itself we can't really create complicated user interfaces. createApplication
is a way of "looping" the eventStream
returned by createElement
into an update
function.
This cycle of update
-> event
-> createElement
-> update
forms the basic flow of all applications.
createApplication({
// this is our initial update value!
init: 0,
// this is what our user interface looks
view: (currentState) =>(<div>
<button onclick={1}>Clicked {currentState} times</button>
</div>),
update: (currentState, value) => currentState + value,
// we need a place to attach our view to the document
mount: document.getElementById('application')
})
// to kick off the stream we need to emit an initial value.
// here we emit "0" because we won't want to increment our state
// until the user actually clicks the button
.run(0)
Type: TaskCreator<Action>
Our examples up until now have only dealt with event handlers bound in our view. What if we want to execute and respond to an HTTP request? What if we want to listen to an event that is scoped outside of our view (such as window.onscroll
)? We can use Tasks for this.
Tasks are helper functions that allow us to propagate events to Sinks
When we call createApplication
an internal Sink is created which does a couple things. It subscribes to the Stream
of events returned by our view
and update
functions, and the resulting DOM nodes created by composing update
with view
. The definition is similar to
Sink<{eventStream: Stream<{action: Action}>, view?: Element}>
A "Task Creator" that creates a Scheduled Task to propagate events to this Sink
will look something like this:
import { TaskCreator } from '@abradley2/frpuccino'
import { now, propagateEventTask } from '@most/core'
import { asap } from '@most/scheduler'
export function propagateEvent <Action> (action: Action): TaskCreator<Action> {
const event = { eventStream: now({ action }) }
return (sink, scheduler) => {
const task = propagateEventTask(event, sink)
return asap(task, scheduler)
}
}
Type: UpdateResult<Model, Action>
The main update
function is allowed to return more than just the next version of the Model
. It may also return an array consiting of the model as the first item, and either TaskCreator<Action>
or TaskCreator<Action>[]
as the second item.
We can change our original update
function so when our application starts, we use our propagateEvent
function to start us out with counter incrementing once by a value of "1"
function update (currentState, value) {
// recall that we specified "0" as our initial action to dispatch when
// our application starts.
if (value === 0) {
return [currentState, propagateEvent(1)]
}
return currentState + value
}
UpdateResult<Model, Action>
is very flexible. We can not only give a single scheduled task to be executed as a result of update, but many.
if (value === 0) {
return [
currentState,
[
propagateEvent(1),
propagateEvent(1)
]
]
}
Method: mapElement
Similar to how we can map
a Stream
from Stream<A> -> Stream<B>
we can use mapElement
to convert the eventStream
of one element to another Stream
type. This is useful to avoid cases where due to composing many different modular features we end up with types like Stream<A | B | C | D | E>
that are difficult to reason about in our application's update function.
Here's an illustrative example of avoiding an update function having to deal with an event of type number | string
by normalizing all eventStreams
to Stream<numer>
function button () {
return <div>
<button onclick={1}>Click me</button>
</div>
}
function input () {
return <div>
<input onchange={(e) => e.target.value} />
</div>
}
function application () {
return <div>
<div>
Count by one:
{button()}
</div>
<div>
Count by input:
{mapElement(
(payload) => {
if (!Number.isNaN(result)) return result
return 0
},
input()
)}
</div>
</div>
}
:warning: Due to performance reasons, mapElement
actually mutates the Element
passed to it. Always use a constructor function to pass the second argument to this function :warning: