在非 onionified Cycle.js 应用中使用 cycle-onionify

Using cycle-onionify within a non-onionified Cycle.js app

我可能在尝试一些愚蠢的事情,但我有一个足够大的 non-onionified Cycle.js 应用程序,我正在尝试学习 onionify 有效,所以我想将一个 onionified 组件嵌入到我原来的非 onion 应用程序中。

所以我有一个简单的洋葱就绪组件,increment/decrement example, and I have a simple non-onion Cycle app, the “Hello Last Name” 示例——我如何将两者混合在一起,以便我在同一网页中一个接一个地拥有增量器组件和 Hello 组件?

Counter.ts,洋葱就绪组件

import xs from 'xstream';
import run from '@cycle/run';
import { div, button, p, makeDOMDriver } from '@cycle/dom';

export default function Counter(sources) {
    const action$ = xs.merge(
        sources.DOM.select('.decrement').events('click').map(ev => -1),
        sources.DOM.select('.increment').events('click').map(ev => +1)
    );

    const state$ = sources.onion.state$;

    const vdom$ = state$.map(state =>
        div([
            button('.decrement', 'Decrement'),
            button('.increment', 'Increment'),
            p('Counter: ' + state.count)
        ])
    );

    const initReducer$ = xs.of(function initReducer() {
        return { count: 0 };
    });
    const updateReducer$ = action$.map(num => function updateReducer(prevState) {
        return { count: prevState.count + num };
    });
    const reducer$ = xs.merge(initReducer$, updateReducer$);

    return {
        DOM: vdom$,
        onion: reducer$,
    };
}

index.ts,非洋葱主应用

import xs, { Stream } from 'xstream';
import { run } from '@cycle/run';
import { div, input, h2, button, p, makeDOMDriver, VNode, DOMSource } from '@cycle/dom';

import Counter from "./Counter";
import onionify from 'cycle-onionify';
const counts = onionify(Counter);

interface Sources {
    DOM: DOMSource;
}

interface Sinks {
    DOM: Stream<VNode>;
}

function main(sources: Sources): Sinks {
    const firstName$ = sources.DOM
        .select('.first')
        .events('input')
        .map(ev => (ev.target as HTMLInputElement).value)
        .startWith('');

    const lastName$ = sources.DOM
        .select('.last')
        .events('input')
        .map(ev => (ev.target as HTMLInputElement).value)
        .map(ln => ln.toUpperCase())
        .startWith('');

    const rawFullName$ = xs.combine(firstName$, lastName$)
        .remember();

    const validName$ = rawFullName$
        .filter(([fn, ln]) => fn.length > 0 && ln.length >= 3)
        .map(([fn, ln]) => `${ln.toUpperCase()}, ${fn}`);

    const invalidName$ = rawFullName$
        .filter(([fn, ln]) => fn.length === 0 || ln.length < 3)
        .mapTo('');

    const name$ = xs.merge(validName$, invalidName$);

    const vdom$ = name$.map(name =>
        div([
            p([
                'First name',
                input('.first', { attrs: { type: 'text' } }),
            ]),
            p([
                'Last name',
                input('.last', { attrs: { type: 'text' } }),
            ]),
            h2('Hello ' + name),
        ]),
    );

    return {
        DOM: vdom$,
    };
}

run(main, {
    DOM: makeDOMDriver('#main-container'),
});

尝试次数

如果我将 run(main, ...) 替换为 run(counts, ...),正如 cycle-onionify 文档建议的完全 onionified 应用程序,我只会看到预期的计数器。

但是 counts,作为 onionify(Counter) 的输出,是一个函数,所以我不认为我可以在我的 `main() 中“实例化”它。

同样,我认为我无法通过在 main 中调用 Counter() 来创建计数器组件,因为该函数需要 sources.onion 输入,而且我不确定如何创建 .onion 字段,其类型为 StateSource.

问题

我如何在我的非洋葱化 main 中使用这个 Counter onion-ready 组件?

完整示例

完整示例可在 https://gist.github.com/fasiha/939ddc22d5af32bd5a00f7d9946ceb39 获得 — 克隆此,npm install 以获得必要的包,然后 make(运行 tscbrowserify转换 TypeScript→JavaScript→浏览器化 JS)。

其实很简单。正如你所说,你可以在你的 main 中调用 Counter(),但是它没有 StateSource.

解决办法是把Counter()换成onionify(Counter)()

function main(sources: Sources): Sinks {
    //other main stuff

    const vdom$ = //implementation here

    const counterSinks = onionify(Counter)(sources);

    const combinedVdom$ = xs.combine(vdom$, counterSinks.DOM)
        .map(div). //Enclose with div

    return {
        DOM: combinedVdom$,
    };
}

请注意,这只会使用 Counter 中的 DOM 接收器,因此如果您想使用计数器中的其他接收器,您还必须指定它们:

const counterSinks = onionify(Counter)(sources);

const combinedVdom$ = xs.combine(vdom$, counterSinks.DOM)
    .map(div). //Enclose with div

return {
    DOM: combinedVdom$,
    HTTP: xs.merge(myOwnRequest$, counterSinks.HTTP)
    //...
};

因为对每个水槽都这样做有点乏味,我在包 cyclejs-utils:

中创建了一个助手
const counterSinks = onionify(Counter)(sources);

const combinedVdom$ = xs.combine(vdom$, counterSinks.DOM)
    .map(div). //Enclose with div

const ownSinks = { DOM: vdom$, /* ... */ };

return {
    ...mergeSinks(ownSinks, counterSinks)
    DOM: combinedVdom$
};

我指定额外的 DOM 的原因是 mergeSinks 在每个接收器上调用合并,但是对于 DOM,您想使用 combine + map 来组合 DOM 到一个新的 parent DOM 正是你想如何使用它。