Ballade: 重新诠释 Flux 架构

发表于 5年以前  | 总阅读数:2450 次

由于 React 的单向数据流的设计,衍生出了单向数据流的架构模式 Flux。

在 MVC 的分层架构中,Flux 属于 M 层,也就是 Model,而在 Flux 中,Store 是关键部分,ActionDispatcher 都是围绕着 Store 来设计的,所以 Flux 架构模式的目标就是基于单向数据流如何更好的管理数据,在 ViewsController-views 与数据之间进行解耦。

我在之前的 React 应用的架构模式 Flux 有详细的介绍过 Flux 架构模式及其应用。

"Flux is more of a pattern than a framework."

这是 Flux 在其 github 主页上截取的一句话,翻译成中文就是:Flux 更像是一种架构模式,而不是一个单纯的框架。

从实际使用的感受来看,Flux 作为一种架构模式还是很不错的,方向是正确的,但是作为框架来使用,从这个层面来说,存在着一些问题。正是由于这些问题促使我重新基于 Flux 的架构模式开发了 Ballade。先来介绍一些 Ballade 的架构模式,之后再说说在使用 Flux 框架时碰到的一些问题以及 Ballade 是如何解决这些问题的。

flux

Ballade 的架构介绍

Store

Store 是一个数据的存储中心,提供了「写入」和「读取」数据的接口,就像一个数据的「访问器」。

ViewsController-views (React 组件)中,只能从 Store 中「读取」数据,在 Store Callbacks 中,才能自由的「写入」和「读取」数据。

当数据变化的时候,Store 会发送一个数据变化的事件。

Store 的数据「访问器」分为 mutable(可变)和 immutable(不可变)的两种,分别对应了mutable 和 immutable 两种不同的数据结构。

Actions

所有的操作,像用户的交互行为或者从服务器获取一个数据,只要会引起数据变化的操作都可以把它看作是一个 Action,引起数据变化的操作可以是「写入」或「更新」数据。

如果想「写入」或「更新」Store 中的数据,只能发起一个 Action。每个 Action 都有一个唯一的 ActionType 和 payload 数据,ActionType 可以理解为这个 Action 唯一的名字,payload 数据就是传递给 Store 的数据。

Dispatcher

Dispatcher 用于连接 ActionsStore 的「调度员」,负责将 Action 产生的 payload 数据分发给指定的 Store

Actions Middleware

在传送 payload 数据到 Store 时,可以注册一些中间件来处理 payload 数据,每个中间件会把处理完的 payload 结果再传递给下一个中间件。假如你想从服务器取数据,可以注册一个中间件。

Store Callbacks

Action 触发的时候,Store 需要一个与该 Action 对应的回调函数来处理 payload 数据,这时可以将数据写入到 Store 中。ActionType 需要与 Store 的回调函数名相对应。

ballade

与其它框架设计的异同

Ballade 在架构上和 Flux 很像,在一些细节问题上比 Flux 处理的更好或者说更具有约束性。

  • 强化了 Store 的功能,正如我前面提到的 Flux 架构中 Store 是关键;
  • 增加了 Actions Middleware 用于集中处理 Action,一方面简化了 Action 的功能,另一方面便于扩展;
  • Store Callbacks 的设计也做了简化,提升了封装性;

Actions Middleware 借鉴了 Redux 的 Middleware 的设计(当我知道 Redux 有 Middleware 但并不知道其实际应用和实现细节的时候,我就很自然的把这种 Middleware 设计用于了 Action)。Store Callbaks 与 Redux 的 Reducer 有些类似,但是对于已经熟悉了 Flux 架构的开发者来说,Store Callbacks 更好理解。

架构介绍完毕之后说说我以及我所在的团队在实际使用 Flux 框架时碰到的一些问题,相信其他使用 Flux 的开发者都会碰到这些类似的问题。

Flux 中的 Actions

先来看一段 Flux 的 Actions 的代码:

注意:本文所有的示例代码都是基于 ES6 语法的 JavaScript 代码。

// Flux 的代码
    const actions = {
        fetchDatabasesource (query, type) {
            const url = `http://${window.__API_DOMAIN__}/api/database/datasource/${type}/${query}`;

            dispatcher.dispatchAsync({
                url
            }, {
                request: constants.FETCH_DATABASESOURCE,
                success: constants.FETCH_DATABASESOURCE_SUCCESS,
                failure: constants.FETCH_DATABASESOURCE_ERROR
            }, {
                query: query
            });
        }
    };

这是一段从服务端获取数据的 Action 的代码,dispatcher.dispatchAsync 封装了一个 fetch 方法用于取数据,该方法的实现可以查看在 github 的例子 dispatcher.js,该方法实际发起了三个 Action

这里存在两个问题,一个是代码冗余的问题,(代码冗余的问题并不完全是由于框架本身造成的,也有可能是开发者的实现不够优雅)。还有一个问题就是职责不明的问题,取数据的操作到底该在哪里进行呢,是在 Action 中还是 Store Callback 中?这在 Flux 框架中并没有明确的约束。

注册 Actions 的中间件

在 Ballade 中,我们将从服务端取数据的 fetch 方法封装成一个 Actions Middleware,并注册到 dispatcher 上。

// Ballade 的代码
    dispatcher.use((payload, next) => {
        // 可以通过中间件中是否包含 uri 字段来判断是否使用 fetch 来取数据
        if (payload.uri) {
            fetch(payload.uri).then(function(response){
                return response.json();
            })
            .then(function(response){
                payload.response = response;
                // 异步函数中的 next 回调
                next(payload);
            });
        }
        // 如果不包含 uri 字段,则不作任何处理
        else {
            next(payload);
        }
    });

Ballade 中简化的 Actions

然后上面的 Actions 代码在 Ballade 中可以简化成下面这样:

// Ballade 的代码
    const actions = dispatcher.createActions({
        fetchDatabasesource (query, type) ({
            uri: `http://${window.__API_DOMAIN__}/api/database/datasource/${type}/${query}`,
            query: query
        })
    });

在 Ballade 的 Action 中,直接返回 ActionType 和 payload 数据即可(这里使用了 ES6 的箭头函数省略了 return 关键字)。

Flux 中的 Store Callback

继续发一段 Flux Store Callback 的代码。

// Flux 的代码
    libraryPermissionsStore.dispatchToken = dispatcher.register((action) => {
        switch(action.type) {
            case constants.FETCH_DATABASESOURCE:
                updateDatabasesource({
                    isLoading: true
                },action.query);
                libraryPermissionsStore.emit(constants.CHANGE_DATABASESOURCE_EVENT);
            break;

            case constants.FETCH_DATABASESOURCE_SUCCESS:
                action.response.isLoading = false;
                updateDatabasesource(action.response, action.query);
                libraryPermissionsStore.emit(constants.CHANGE_DATABASESOURCE_EVENT);
            break;
            ...
    });

这段代码和上面的创建 Actions 的代码相对应,用于存储数据到 Store 中并发出数据变化的通知(这样ViewsController-views 才能接收到通知),它是一个大的 Callback 函数,使用 switch case 语句与 ActionType 对应,但是 switch case 容易造成代码冗余,并且每个 case 语句中都需要手动发送一次通知。一个大的 Callback 函数中如果 switch case 语句过多还存在着变量无意中共享作用域的隐患。

Ballade 中简化的 Store Callback

Ballade 在 Store Callbacks 中将大的 Callback 和 switch case 语句拆分成了一个个独立的 Callbacks,并且使用函数的 name 与 ActionType 进行对应。

// Ballade 的代码
    const exampleStore = dispatcher.createMutableStore(schema, {
        'example/update-title': function (store, action) {
            return store.mutable.set('title', action.title);
        },
        ...
    });

先看看 example/update-title 这个 Store Callback,没有了 switch case 语句,也无需每次都发送一个数据变化的通知,只要返回 Store 的 set 或 delete 操作的结果,框架内部会自动发送通知。

schema

在 Ballade 中,要创建一个 Store 需要先声明 schema

const schema = {
        title: null,
        meta: {
            votes: null,
            favs: null
        }
    };

schema 就是该 Store 的数据模型,只有数据的 key 在 schema 中声明过,才能对其进行「写入」或「更新」数据,这可以让开发者知道该 Store 中有哪些数据,能让数据操作更加清晰透明。

假如 schema 中并没有声明 author,而又想直接存储该数据到 Store 中是不会生效的,或者直接报错。

// 无效的操作
    store.mutable.set('author', action.author);
    // 报错
    store.immutable.set('author', action.author);

mutable & immutable

Ballade 的 Store 对 mutable 和 immutable 两种数据结构都支持。

在默认的 ballade.js 版本中,提供的是 mutable 版本,使用 dispatcher.createMutableStore 就可以创建一个 mutable 的 Store,mutable 数据的「写入」和「读取」都需要通过 store.mutable 这个数据访问器。

在 ballade.immutable.js 版本中,提供了 mutable 和 immutable 两个版本,但是需要依赖 immtable-js。使用 dispatcher.createImmutableStore 就可以创建一个 immutable 的 Store,immutable 数据的「写入」和「读取」都需要通过 store.immutable 这个数据访问器,实际上 store.immutable 就是基于 Immutable 实例的一个封装。

「写入」和「读取」分离

通过 dispatcher.createMutableStoredispatcher.createImmutableStore 创建的 Store 用于在 ViewsController-views 中「读取」数据,它们不能直接「写入」数据。

// 从 store 中取出 titile
    exampleStore.mutable.get('title');
    console.log(exampleStore.mutable.set) // => undefined

对于 mutable 访问器来说,并没有 set 方法。并且如果返回的数据是一个引用类型的数据(对象或数组),它会返回该数据的拷贝,这样在 Store Callbacks 之外对取出来的数据进行修改并不会影响到 Store 中保存的数据。

// 如果 title 是一个对象, title = { foo: 'bar' }
    // 会返回 title 的克隆对象
    var title = store.mutable.get('title');
    console.log(title.foo) // => 'bar'

    title.foo = 'baz';
    console.log(title.foo) // => 'baz'

    console.log(store.mutable.get('title').foo) // => 'bar'

而对于 immutable 访问器来说,它的 set 类方法只会返回一个新的 immutable 数据,也不会影响 Store 中的数据。

只有在 Store Callbacks 中才能自由的「写入」和「读取」数据。这种分离的设计充分体现了「单向」的特点,使数据操作清晰明了。

Action 真的需要队列吗?

// Flux 的代码
    componentDidMount () {
        orgPermissionsStore.on(DELETE_APPLICATION_SUCCESS_EVENT, this.deleteSuccess);
    }

    …

    deleteSuccess () {
        // 无奈之举
        setTimeout(() => {
            orgPermissionsActions.fetchApplication(1);
        }, 0);
    }

这段代码是当一个删除操作的 Action 成功之后,再发起一个请求数据的 Action,如果没有 setTimeout 的包装,Flux 框架就会报如下的错误:

"Dispatch.dispatch(…): Cannot dispatch in the middle of a dispatch"

这是因为 Flux 框架在设计的时候,一次只能发起一个 Action,这么做的目的可能是为了 Store 之间的依赖,也有可能是为了确保数据一致性而设计的类似于「并发锁」,这样每次只会有一个修改数据的操作,如果要同时发起好几个请求数据的 Action 就会有问题,要么简单粗暴的使用 setTimeout 来规避,要么使用 Action 队列来解决。但是 JavaScript 在目前来看,它还是单线程的,不会出现并发的场景。

在 Ballade 并没有这种设计。

订阅事件

Store 集成了一个简单的事件订阅和发送的系统,在 ViewsController-views 中可以通过 store.event 接口来订阅数据变化的事件,并且通常的情况下无需开发者主动发送事件通知,只要数据有变更由框架自动来发送事件通知。

// 如果 titile 有变化,回调函数则会执行
    exampleStore.event.subscribe('title', function () {
        var title = store.mutable.get('title');
        console.log(title);

        // or
        var title = store.immutable.get('title');
        console.log(title);
    });

你其实不需要 waitFor

在 Flux 中,它提供了一个 waitFor 来处理 Store 之间的依赖,这并不是一个好的设计,只会让应用变得更复杂。在 flux/examples/flux-chat/js/stores/MessageStore.js 这个例子中,如果删除下面这行代码,该例子仍然能很好的运行。

ChatAppDispatcher.waitFor([ThreadStore.dispatchToken]);

当然,这里并不是说 waitFor 一无是处,只是它让开发者对 Flux 的理解更加困惑。

在 Ballade 中处理 Store 之间的依赖,相当简单,就像你要使用一个变量,那么你得先定义这个变量,它本来就这么简单。

var storeA = dispatcher.createMutableStore(schema, {
        'example/update-title': function (store, action) {
            return store.mutable.set('title', action.title);
        }
    });

    // 假如 storeB 依赖了 storeA
    var storeB = dispatcher.createMutableStore(schema, {
        'example/update-title': function (store, action) {
            var title = storeA.mutable.get('title') + '!';
            return store.mutable.set('title', title);
        }
    });

好了,Ballade 都介绍完了,如果你对 Ballade 感兴趣,想深入的了解,下面是 Ballade 的一些资源。

  • github 主页:https://github.com/chenmnkken/ballade
  • 中文文档:https://github.com/chenmnkken/ballade/blob/master/README_CN.md
  • Mutable 的例子:TodoMVC
  • Immutable 的例子:TodoMVC
  • Bug 或问题提交:https://github.com/chenmnkken/ballade/issues
 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
为Electron程序添加运行时日志 5年以前  |  20343次阅读
Node.js下通过配置host访问URL 5年以前  |  5894次阅读
用 esbuild 让你的构建压缩性能翻倍 4年以前  |  5786次阅读
 目录