@abradley2/frpuccino 中文文档教程

发布于 5年前 浏览 25 项目主页 更新于 3年前

FRPuccinno

:warning: 这仍处于 alpha 开发阶段。 我很想得到更多的人 对此进行测试并发现要解决的问题,但我不推荐 在任何需要可靠视图层的大型项目上使用它! :warning:

FRPuccino 是一个建立在 Most.js 基础上的小型 UI 库,

它的灵感来自 ElmCycle.js

这是一个简短的“反例”示例:

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

创建基本应用程序只需要两种方法: createElementcreateApplication

Function: createElement

createElementFRPiccino 的“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 返回的 eventStreamupdate 函数中。

这个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 我们的 viewupdate 函数返回的事件, 以及通过将 updateview 组合在一起创建的结果 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 主函数允许返回的不仅仅是下一个版本 模型。 它也可能返回一个包含模型的数组作为第一个 项,以及 TaskCreatorTaskCreator[] 作为 第二项。

我们可以更改我们原来的 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

类似于我们如何从 Streammap Stream -> 流 我们可以使用 mapElement 来转换一个元素的 eventStream 到另一个 Stream 类型。 这有助于避免以下情况 由于组合了许多不同的模块化功能,我们最终得到 Stream 关于我们应用程序的更新功能。

这是避免更新功能必须的说明性示例 处理 number | 类型的事件 string 通过归一化 所有 eventStreamsStream

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:

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文