cyclejs 初探

前台组  于宝权

cyclejs 是什么

framework

一个函数式和响应式的 JavaScript 框架,编写可观测的代码

核心: Pure Dataflow

输入(sources): 读作用

输出(sinks): 写副作用

I/O: 副作用

drivers 管理所有副作用

main()

响应式编程--可分离

函数式编程--可预测

Cycle.js 程序是由纯函数构建,这意味着它们只接受输入并生成可预测的输出,而不会造成任何 I/O 副作用。

作为组成部分的响应式流来自像 xstreamRxJS 或者 Most.js 这样的库,这些库极大地简化了事件处理、异步调用、错误处理的相关代码。

用流构建的程序同时也分离了关注点,因为所有对一块数据的动态更新总是在同一处,并且不能从外部改变。

结果是,Cycle 应用更少使用 this,并且没有像 setState() 或者 update() 这样的命令式调用。

函数式容易编写「可预测」的代码,响应式容易编写「可分离」的代码。

名词解释

sink 水槽 水池, 源的目的地, 黄河之水天上来, 奔流到海不复回

sources: read effect

sinks: write effect

sinks 是从 main() 函数到 driver 执行作用的指令,而 sources 是可读的作用。

可分离: 分离关注点, 即 logic 和 effect

logic 业务逻辑 概念

effect 现实世界的变化 dom, db, io

响应式编程: https://en.wikipedia.org/wiki/Reactive_programming 

个人理解: 类似 observer, 声明完一个数据所依赖的源和计算逻辑后, 下次源出现变化, 数据自动更新, 不需要再次干预, 如手动调用更新操作等

简单和简洁

Cycle.js 是一个不需要学习很多概念的框架,核心 API 只有一个 run(app, drivers)

此外,还有 streamfunctionsdrivers(用于不同类型 I/O 副作用的插件),用于隔离组件的辅助函数。

这个框架没有很多「魔法」,大多数组成部分只是 JavaScript 函数而已。

虽然更少的「魔法」会导致更多的代码,但是使用函数式响应式流可以使用较少的操作去处理复杂的数据流,你将会看到 Cycle.js 构建的程序是小巧并且易读的。

更少的操作

主要使用对流的操作(xstream / rxjs / mostjs 的 API), 例如: merge, combine, of, map, fold

没有魔法

没有组件实例, 没有生命周期(snabdom 提供了一些dom的hook), 没有computed, filter 等等, 只有一堆纯函数组成的对象

清晰的数据流

在每个 Cycle.js 应用中,每一个被声明过的流都是在数据流图中的一个节点,节点与节点用箭头表示依赖关系。这意味着,你的代码和外部输入输出的数据流图存在着一一对应的关系。

组件化

Cycle.js 是组件化的,但是和其他框架不太一样,无论多么复杂,每个单独的 Cycle.js 应用,都是一个函数,并可以被一个更大的 Cycle.js 应用复用。

sources 和 sinks 是存在于应用程序和 drivers 之间的接口,但是他们也有与子组件及其父组件之间的接口。

Cycle.js 的组件可以是类似其他框架中的 GUI 部件,但是他们不仅限于 GUI。由于 sources 和 sinks 的接口不仅限于 DOM ,你也可以制作成 Web 音频组件,网络请求组件等 (应用在nodejs中)

cyclejs 如何使用

官方教程

主要应用到的库

DOM: @cycle/dom (内部依赖snabbdom)

HTTP: @cycle/http (内部依赖superagent)

数据流: xstream (rxjs mostjs)

从0开始, 不使用cyclejs

// Logic
xs.periodic(1000)
    .fold(prev => prev + 1, 0)
    .map(i => `Seconds elapsed: ${i}`)
   
// Effects
    .subscribe({
        next: str => {
            const elem = document.querySelector('#app');
            elem.textContent = str;
        }
    });

分离逻辑和副作用

分离

function main() {
    return xs.periodic(1000)
        .fold(prev => prev + 1, 0)
        .map(i => `Seconds elapsed: ${i}`);
}

function domDriver(text$) { // DOM 操作
    text$.subscribe({
        next: str => {
            const elem = document.querySelector('#app');
            elem.textContent = str;
        }
    });
}

function logDriver(msg$) { // 记录日志
    msg$.subscribe({
        next: msg => {
            console.info(msg);
        }
    });
}

分离不同种类的副作用

const sink = main();
domDriver(sink);
logDriver(sink);

run

不同的数据到达不同的 effect

function main() {
    return {
        DOM: xs.periodic(1000)
                .fold(prev => prev + 1, 0)
                .map(i => `Seconds elapsed: ${i}`),
        log: xs.periodic(2000)
                .fold(prev => prev + 1, 0)
    };
}

// domDriver

// logDriver

const sink = main();
domDriver(sink);
logDriver(sink);

加一点封装

function run(mainFn, drivers) {
    const sinks = mainFn();
    Object.keys(drivers).forEach(key => {
        if (sinks[key]) {
            drivers[key](sinks[key]);
        }
    });
}

run(main, {
    DOM: domDriver,
    log: logDriver,
});

从 source 读取信息

function main(sources) {
    const click$ = sources.DOM;
    return {
        DOM: click$.startWith(null).map(() => {
            xs.periodic(1000)
                .fold(prev => prev + 1, 0)
                .map(i => `Seconds elapsed: ${i}`)
        }).flatten(),
        log: xs.periodic(2000)
                .fold(prev => prev + 1, 0)
    }
}

function domDriver(text$) {
    text$.subscribe({
        next: str => {
            const elem = document.querySelector('#app');
            elem.textContent = str;
        }
    });

    const domSource = fromEvent(document, 'click');
    return domSource;
}

function run(mainFn, drivers) {
    // cycle
    const sinks = mainFn({ DOM: domSource });
    const domSource = domDriver(sinks.DOM);
}

解决循环依赖问题

function run(mainFn, drivers) {
    // cycle
    const fakeDOMSinks = xs.create();
    const domSource = domDriver(fakeDOMSinks.DOM);
    const sinks = mainFn({ DOM: domSource });
    fakeDOMSink.imitate(sinks.DOM);
    // http://staltz.github.io/xstream/#imitate
}
function run(mainFn, drivers) {
    const fakeSinks = {};
    Object.keys(drivers).forEach((key) => {
        fakeSinks[key] = xs.create();
    });

    const sources = {};
    Object.keys(drivers).forEach((key) => {
        sources[key] = drivers[key](fakeSinks[key]);
    });

    const sinks = mainFn(sources);

    Object.keys(sinks).forEach((key) => {
        fakeSinks[key].imitate(sinks[key]);
    });
}

一个大致的cyclejs run

domDriver的封装

结论: domDriver 的职责: 接收一个vdom对象的流, 渲染页面并返回HTML的source

此处 domDriver 的演进过程, 代码略过 (具体请看官方视频教程)

MVI

model(actions$)

intent(sources)

view(state$)

state$

vdom$ ==> DOM

actions$

MVI is a simple pattern to refactor the main() function into three parts: Intent (to listen to the user), Model (to process information), and View (to output back to the user). 阅读全文

具体例子

需求描述: 

1. 4个tab, 点击进行切换, 不同tab中有不同的子组件

2. Foo组件, 点击按钮获取当前时间的秒数, 显示在DOM中

3. Ajax组件, 点击发送请求1, 请求1成功后发送请求2, 请求失败在console中打印错误信息

4. Form组件, 输入姓名和年龄, 点击"提交"按钮, 进行校验, 并把提交事件通知上层container组件, 在container组件中进行更多校验

5. container 组件, 包裹所有子组件, 将所有数据流交给reducer处理

代码实现

注意:

1. 以上代码是否是最佳实践?

2. 代码中是否有坏味道?

请自行思考和实践

弊端

以下列举的弊端是在编写demo的过程中发现的, 或许这些弊端可以通过一些方法避免掉也未可知. 具体能否避免, 欢迎大家研究讨论.

debug 困难----根据出错位置难以定位到需要fix的位置

白屏问题----没有报错信息

耦合问题----一个组件出错, 整个页面挂掉 ??? (存疑)

(为什么不在模板中使用on-xxx={handler} ?

开闭原则 责任单一

https://cycle.js.org/model-view-intent.html#model-view-intent-what-mvc-is-really-about-why-css-selectors)

API 说明不知所云 ---- 我自己笨

http://staltz.github.io/xstream/#remember

不够直观---- snabbdom写出来的东西不是html, 有点类似pug, 语法比pug还激进些 如果模板太大的话, 直观上看起来有点别扭(snabby支持字符串模板, 没试)

不够直观----无法直接从view层看出元素上的事

异常情况演示: 来搞一下 tab.js

结语

接触 cyclejs 是看到一篇文章 < 前端数据流哲学 > https://github.com/ascoders/blog/issues/26 

 

当时正在做充值中心相关任务的开发. 充值中心的代码量随着业务逻辑的增加, 已经膨胀了不少, 内部逻辑错综复杂, 数据分散在各个组件当中. 自己也在思考, 如果对充值中心进行重构, 为了使业务逻辑更加清晰, 并做到可追踪, 可回溯, 除了redux, 还有没有其他的可能性?

 

于是有了前面的demo

一点感受

使用cyclejs, 需要从头脑中摒弃一贯的命令式编程思维以及模板二向绑定的思维

xstream / rxjs 相关的流管理API需要多加理解(我自己都没理解)

动手最关键 (1. 老夫敲代码就是一把梭... 2. 干就完了...)

目前我对 cyclejs 的了解也仅限于一些皮毛的东西, 某些细节深究的话, 也说不出个所以然来(我自己也不是很清楚这个demo为什么能跑起来). 分享中难免有纰漏的地方, 还请大家谅解

希望通过这次介绍, 能让同学们对cyclejs有一个大致的认知, 能对它产生兴趣是再好不过的了. 对cyclejs有任何问题, 也欢迎会后与我交流.

参考资料

FAQ

3Q

扫码有惊喜

Made with Slides.com