efe isomorphic framework
简洁的flux同构框架
特点
- 同构,同时支持node/browser,one world one code
- 支持多页面网站应用化 / 单页面网站服务器预渲染
- 简单易懂的函数式编程思维管理你的store
- 提供更好的领域划分,避免flux模式中不良编码模式
术语
Store
在ei
中,store
是一个页面中全部的数据。
State
在ei
中,state
是指store
在某一时刻的状态。所以,state
也就是页面中所有的数据。一般来讲是一个Object
或者是一个key-value
的集合。但理论上来说,它可以是你想要任何一种数据类型。
我们会将state
传递给react
,作为react
组件的数据来使用;通过react
组件的翻译,数据将被转化为DOM,最终成为可见、可交互的页面。
Action
Action
是一个数据包裹,用来描述系统内一个事件。比如,用户点击一个添加按钮,可以通过下面这个action
来描述:
type: 'ADD'
完成了一个ajax请求,可以被描述为:
type: 'AJAX_SUCCEED' data: // all the data from the datasource
基于这样的约定,我们可以把页面理解成一个持续产生action
的事件流系统。每个行为都会对我们页面中当前的state
造成一定影响,使其发生变化。因此,我们每个时刻的state
都可以理解为之前所有的action
的积累。
reducer
基于前边两个概念我们可以知道,版本1state
在一个action
的作用下会转变成版本2state
,这个过程我们称之为reduce
(归并)。我们当然希望reduce
的过程由我们自己来掌握,在ei
中抽象为reducer
。
我们可给出一个非常简洁的函数原型来描述这个过程:
var state2 = ;
我们非常希望可以通过
state1
===state2
这种简单的方法来判断数据是否发生了变化,只要(只有)数据发生变化,我们才会通知view
(react)来完成视图上的更新。
因此,这里非常适合使用
Immutable
数据结构来管理state
。
这种行为在
ei
中是默认行为,ei
会自动state1
===state2
的方式来检测state
的变化,并将变化即时地通知给react
。
如果你的视图不更新了,那么请检查
reduce
返回的结果是不是同一个对象。请确保当数据需要发生变化时state1
!==state2
。
由于,ei
中所有的数据都存放在state
中,因此我们只需要一个顶级的reducer
就作为入口即可。
我们设计的reducer
是一个纯函数,我们可以非常容易地进行组合完成复杂的业务逻辑,比如这样:
var { return state + 1; }; var { return state - 1; }; var { };
因此,我们不再需要flux
中store
在register
回调中使用dispatcher.waitFor
方法来完成依赖,我们只需要按逻辑执行不同的子reducer
即可。举个例子:
var { // some operation on state according to action; return state;}; var { // some operation on state according to action; return state;}; var { state = ; state = ; return state; };
实际上,我们还可以把这样的系统理解为一个
有限状态自动机
,每一个action
可以理解为一个输入,而reducer
则是状态转移函数。
dispatch
为了使 state
/ action
/ reducer
可以结合在一起正常工作,我们引入了dispatch
。 dispatch
用来连接 state
/ action
/ reducer
。
当系统接收到一个action
时,我们找到store
,取得它的当前state
,再将state
和action
传入reducer
。最后,将reducer
的返回结果写回到store
中。
dispatch
可以接收两种数据结构。第一种是传入一个action
,这非常容易理解,正是我们想要的。另一种情况是传入一个函数,这是为了支持异步操作。
当传入dispatch
的是一个函数中,这个函数会得到两个参数,分别是dispatch
和state
。也就是说在这个函数中,既可以得到所有的数据,也可以多次dispatch
动作。
举个例子,
;
可以看到,在这一次dispatch
过程中,实际上派发了多个action
。因此,我们可以通过reducer
来调整state
,从而在视图上给用户良好的反馈。
ActionCreator
出于重复利用action
的目的,我们提出ActionCreator
的概念。每个ActionCreator
是一种action
的工厂(action factory)。
这它是一个函数,接收的参数格式不限,但返回值必须是一个action
或者是一个function
。
举个例子
{ return type: 'SYNC_ADD' data: count ; } { return { ; http ; }; } var syncAddAction = ;var asyncAddAction = ;
同样,ActionCreator
是一个函数,它也很容易进行封装或者组合,比如:
{ return type: 'DO_A' data: count ; } { return { ; ; }; }
Context
把上边所有的dispatch
/ reducer
/ store
(state
) 概念结合在一起,就是Context
。Context
的实例数据结构包括了以下内容:
// Context instance // 归并(状态转移)函数 { } // 实际上store可以是任何类似的值 store: // 派发函数 { }
Context
实例不是单例的,每个页面中应当包含有一个。 这样的设计是为了支持在服务器端使用ei
。我们知道在服务器端,可以同时处理多个http请求。那么一定需要同时存在多个Context
的实例,并且彼此相互隔离。
Page
这是ei
对页面的抽象。实际上,Page
是Web网站最基本的概念。每次用户发起一个浏览页面的http请求,我们都应当为他响应一个页面。
即使是在spa(single page application,单页面应用)中,其为用户提供的基本感知还是一个基于多个页面的程序,只不过这些页面是虚拟的。
ei
所提供的Page
是同构的,它既可以在服务器端渲染成了一段html,也可以成为在spa应用中的一个虚拟页面。
ei
也提供了基础的spa支持。详见App
实际上,在ei
中Page
和Context
一对一的关系,既一个Page
实例持有一个Context
实例。
App
在ei
中,App
是一个应用的概念。ei
的App
是同构的,在服务器端可以以html格式输出多个页面,也可以在浏览器端内实现spa。
我们可以这样得到一个App
实例:
var ei = ; var app = ;
可以在服务器端绑定到一个express
应用上,例如:
var express = ;var ei = ; var app = ; var eiApp = ; app;
或者在浏览器端使用,例如:
var ei = ; var app = ; var data = windowdata; // 直接使用同步数据进行初始化// 此时,app会接管window.onpopstate事件,// 浏览器在前进/后退时会把当前的url转化为一个`request`对象// 与服务器端相同,使用app.execute(request)对其进行处理// 此时一个多页面网站就成功地转化成了一个spa网站app; windowdata = null;
Resource
Resource
是对系统外部资源的一种描述。通常我们会在ActionCreator
中使用它,例如:
var countResource = ; { return { countResource ; }; }
除了通过这种抽象,我们可以重复利用这些资源之外,更重要的是我们需要通过Resource
的概念来解除服务器端与浏览器端对资源需求的差异。
我们都知道在浏览器上我们可以使用的资源是有限制的,一般是通过http
/ socket
两种方式。而在服务器端,可使用的资源,比如 redis
/ mongodb
/ mysql
/ file system
以及各种各样的基于 http / tcp 的数据服务器。这是一个基本的事实是浏览器端与服务器端无法抹平的差异。但是我们的业务代码需要同时运行在浏览器端与服务器端,那么我们必须解决这个问题。
这里我们通过Resource
的依赖注入、控制反转来解决这个问题,将对模块的依赖,转化为对一个资源标识符的依赖。举个例子:
// 同构的 CountActionCreator var Resource = Resource; { return { Resource ; }; } // CountResource on client var Resource = Resource; Resource; // CountResource on server Resource;
与React相关
child context 机制
这是React
的一个隐藏功能,官网上并没有它的明确文档。原因是目前的实现机制并不理想,不久的将来将会被替换成另一个机制。
这里提到的两种机制是:
- 基于
owner
的context机制 - 基于
parent
的context机制
目前的实现机制是第1种,将会被替换成第2种。React
在开发模式中会对这两种模式进行检查,一个组件的owner
和parent
不一致,并且使用了context
,那么你会得到一条警告。也就是说,目前我们可以做到的最好情况就是使ReactElement的owner
与parent
保持一致。
owner 是创建这个ReactElement的ReactElement
parent 是指在DOM层级上的parentNode
更多的资料可以看这里
如果没有context,那我们会遇到一个非常麻烦的问题:组件的数据,必须在父级组件通过props
来传递。这样就导致父级组件需要知道所有的数据,并且一层一层地传递下去。
通过context机制,我们可以非常容易地取到最顶层组件的数据,中间的任意多层组件都不需要关心数据是如何传递了。ei
中就是通过context机制来解决数据逐层传递的问题的。
但是React
对context的使用提出了要求,第一点:
必须明确地声明一个可以提供context的组件,并且要求它必须描述它能提供的context类型,同时实现获取context的函数,即:
var ContextProvider = React;
第二点:使用context的组件也必须明确地描述contextTypes,即:
var ContextUser = React;
对,就是这样的喵。
这个我们可以通过两个mixin
来解决,比如contextProviderMixin和contextUserMixin,但是ei
使用的是higher order component的方法。ei
提供了两个组件,ContextProvider
和ContextConnector
分别替代contextProviderMixin
和contextUserMixin
。 下边我们分别描述一下:
ContextProvider
ContextProvider
是由ei
提供的上下文提供包装组件,大概的原理是这样的:
// 假设这个是你的顶层组件var YourTopLevelComponent = React; // `ei`的`ContextProvider`简化版本var ContextProvider = React; // 在生成ReactElement时,是这样的 var element = React;
当然,在ei
中,我们不需要大家来写这些代码,只需要这样做就可以了:
var ei = ; var IndexPage = eiPage;
ContextConnector
前边我们讲了如何提供上下文,接下来我们讲一下如何访问上下文
其实原理是类似的,也是通过封装组件的方式完成的。
在ei
中可以很方便地将一个野生组件转化为可以使用上下文的组件:
var ei = ; var Hello = React; var selector = // 选取`store`中的属性`name`,注入到Hello的props中 { return storename; } ; var actions = // 这是一个`ActionCreator` // 在Hello被实例化为,这个`ActionCreator`将成为`Hello`的`props.add` // 执行这个方法,将会将返回的动作派发给`reducer` { return type: 'ADD' ; } ; // 只需要在这里使用`ei`提供的`connect`方法即可Hello = ei; moduleexports = Hello;
编码建议
目录安排
我们建议在src目录下使用这样的一个目录安排:
- dep // 存放client端依赖包
- node_modules // 存放server端依赖包
- src
- client // 此目录下存放浏览器代码,Client Resource / 启动脚本等
- server // 此目录下存放服务器代码,Server Resource / server(express) / server模板 / server配置
- iso // 此目录下存放同构代码,Page / Component / Reducer
注意事项
ei
需要以下 shim 支持
- es5
- promise
cjs or amd?
由于nodejs
和浏览器上对于脚本资源获取方式上存在巨大不同,所以我们习惯上是在nodejs
使用cjs格式的模块,而在浏览器端我们习惯使用amd格式的模块。
我们建议全部使用cjs的格式编写源码,通过构建工具将client和iso目录下所有的源码从cjs包装成amd格式(这个非常简单,因为amd规范中强调了需要支持cjs格式,所以常见的amd加载器requirejs和esl都只需要将cjs代码包装一下define函数,就可以完美使用了)
依赖包的选取
建议直接选取可以同时运行在client/server端的依赖包,例如
- http请求:axios / superagent
- promise:es6-promise
- 日志: ei-logger