前台组 于宝权
一个函数式和响应式的 JavaScript 框架,编写可观测的代码
核心: Pure Dataflow
输入(sources): 读作用
输出(sinks): 写副作用
I/O: 副作用
drivers 管理所有副作用
main()
函数式容易编写「可预测」的代码,响应式容易编写「可分离」的代码。
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)。
此外,还有 stream、functions、drivers(用于不同类型 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中)
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);不同的数据到达不同的 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 的演进过程, 代码略过 (具体请看官方视频教程)
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} ?
开闭原则 责任单一
API 说明不知所云 ---- 我自己笨
不够直观---- 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有任何问题, 也欢迎会后与我交流.
https://github.com/cyclejs/cyclejs
http://cyclejs.cn/getting-started.html
https://github.com/staltz/xstream
https://github.com/snabbdom/snabbdom
https://medium.com/@rayshih771012/functional-reactive-programming-70be6bd8726b
https://gist.github.com/staltz/868e7e9bc2a7b8c1f754
https://glebbahmutov.com/blog/node-server-with-rx-and-cycle/
扫码有惊喜