Ballade: 重新诠释 Flux 架构

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

在 MVC 的分层架构中,Flux 属于 M 层,也就是 Model,而在 Flux 中,Store 是关键部分,Action 和 Dispatcher 都是围绕着 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 用于连接 Actions 和 Store 的「调度员」,负责将 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

文章来源:

Author:admin
link:http://stylechen.com/ballade-reinterpreted-flux.html