Ei
efe isomorphic framework
Install / Use
/learn @jinzhubaofu/EiREADME
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 = reducer(state1, action);
我们非常希望可以通过
state1===state2这种简单的方法来判断数据是否发生了变化,只要(只有)数据发生变化,我们才会通知view(react)来完成视图上的更新。
因此,这里非常适合使用
Immutable数据结构来管理state。
这种行为在
ei中是默认行为,ei会自动state1===state2的方式来检测state的变化,并将变化即时地通知给react。
如果你的视图不更新了,那么请检查
reduce返回的结果是不是同一个对象。请确保当数据需要发生变化时state1!==state2。
由于,ei中所有的数据都存放在state中,因此我们只需要一个顶级的reducer就作为入口即可。
我们设计的reducer是一个纯函数,我们可以非常容易地进行组合完成复杂的业务逻辑,比如这样:
var add = function (state, action) {
return state + 1;
};
var minus = function (state, action) {
return state - 1;
};
var reducer = function (state, action) {
switch (action.type) {
case 'ADD':
return add(state, action);
case 'MINUS':
return minus(state, action);
}
};
因此,我们不再需要flux中store在register回调中使用dispatcher.waitFor方法来完成依赖,我们只需要按逻辑执行不同的子reducer即可。举个例子:
var a = function (state, action) {
// some operation on state according to action;
return state;
};
var needWaitForA = function (state, action) {
// some operation on state according to action;
return state;
};
var reducer = function (state, action) {
state = a(state, action);
state = needWaitForA(state, action);
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(function (dispatch, state) {
dispatch({
type: 'AJAX_START'
});
http
.get(
'/some/data/from/any/datasource',
{
query: state.someData
}
)
.then(
function (data) {
dispatch({
type: 'AJAX_SUCCEED',
data: data
});
},
function (error) {
dispatch({
type: 'AJAX_FAILED',
error: error
});
}
);
});
可以看到,在这一次dispatch过程中,实际上派发了多个action。因此,我们可以通过reducer来调整state,从而在视图上给用户良好的反馈。
ActionCreator
出于重复利用action的目的,我们提出ActionCreator的概念。每个ActionCreator是一种action的工厂(action factory)。
这它是一个函数,接收的参数格式不限,但返回值必须是一个action或者是一个function。
举个例子
function syncAddActionCreator(count) {
return {
type: 'SYNC_ADD',
data: count
};
}
function asyncAddActionCreator(count) {
return function (dispatch, state) {
dispatch({
type: 'AJAX_START'
});
http
.get(
'/some/data/from/any/datasource',
{
query: state.someData
}
)
.then(
function (data) {
dispatch({
type: 'AJAX_SUCCEED',
data: data
});
},
function (error) {
dispatch({
type: 'AJAX_FAILED',
error: error
});
}
);
};
}
var syncAddAction = syncAddActionCreator(count);
var asyncAddAction = asyncAddActionCreator(count);
同样,ActionCreator是一个函数,它也很容易进行封装或者组合,比如:
function doA(count) {
return {
type: 'DO_A',
data: count
};
}
function doB(count) {
return function (dispatch, state) {
dispatch({
type: 'DO_B'
});
dispatch(doA(count));
};
}
Context
把上边所有的dispatch / reducer / store(state) 概念结合在一起,就是Context。Context的实例数据结构包括了以下内容:
// Context instance
{
// 归并(状态转移)函数
reducer: function () {
},
// 实际上store可以是任何类似的值
store: {
},
// 派发函数
dispatch: function () {
}
}
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 = require('ei');
var app = ei({
routes: [{
path: '/a',
page: 'iso/IndexPage'
}]
});
可以在服务器端绑定到一个express应用上,例如:
var express = require('express');
var ei = require('ei');
var app = express();
var eiApp = ei({
// 路由配置
// 在调用eiApp.execute(request)对请求进行处理时,
// 首先会使用此处设定的path进行路由匹配,找到相应的Page来进行下一步的处理
// 如果路由配置不存在,则Promise会进入reject状态
routes: [{
path: '/a',
page: 'iso/IndexPage',
template: 'some/template'
}]
});
app.use(function (req, res, next) {
eiApp
.execute(req)
.then(function (result) {
// result的结构是这样的
{
// 路由配置
route: route,
// 当前的页面
page: page
}
// 可以从page中取出所有的数据
var state = page.getState();
// 还可以把page渲染成html
var html = page.renderToString();
// 如果请求是ajax,那么可以直接以state作为响应
if (req.xhr) {
res.status(200).send(state);
return
}
// 如果不是ajax,那可以输出为一段html
// 这样可以灵活地将page的内容输出到指定的位置
// 还可以灵活地输出同步数据, 比如这样
// <script>window.data = {%data|json%}</script>
res.render(route.template, {
html: html,
data: data
});
}, function (error) {
// 在整个处理过程中,发生任何错误都会在此处回调,以供处理
});
});
或者在浏览器端使用,例如:
var ei = require('ei');
var app = ei({
// 在浏览器端需要指定一个main元素,作为react渲染的根结点
main: document.getElementById('app'),
// 与服器端同一样的路由配置
routes: [{
path: '/a',
page: 'iso/IndexPage',
template: 'some/template'
}]
});
var data = window.data;
// 直接使用同步数据进行初始化
// 此时,app会接管window.onpopstate事件,
// 浏览器在前进/后退时会把当前的url转化为一个`request`对象
// 与服务器端相同,使用app.execute(request)对其进行处理
// 此时一个多页面网站就成功地转化成了一个spa网站
app.bootstrap(data);
window.data = null;
Resource
Resource是对系统外部资源的一种描述。通常我们会在ActionCreator中使用它,例如:
var countResource = require('resource/count');
function asyncAddActionCreator(count) {
return function (dispatch, state) {
countResource
.add(count)
.then(function () {
dispatch({
type: 'ADD_SUCCEED'
});
}, function () {
dispatch({
type: 'ADD_FAILED'
});
});
};
}
除了通过这种抽象,我们可以重复利用这些资源之外,更重要的是我们需要通过Resource的概念来解除服务器端与浏览器端对资源需求的差异。
我们都知道在浏览器上我们可以使用的资源是有限制的,一般是通过http / socket两种方式。而在服务器端,可使用的资源,比如 redis / mongodb / mysql / file system 以及各种各样的基于 http / tcp 的数据服务器。这是一个基本的事实是浏览器端与服务器端无法抹平的差异。但是我们的业务代码需要同时运行在浏览器端与服务器端,那么我们必须解决这个问题。
这里我们通过Resource的依赖注入、控制反转来解决这个问题,将对模块的依赖,转化为对一个资源标识符的依赖。举个例子:
// 同构的 CountActionCreator
var Resource = require('ei').Resource;
function asyncAddActionCreator(count) {
return function (dispatch, state) {
Resource.get('count')
.add(count)
.then(function () {
dispatch({
type: 'ADD_SUCCEED'
});
}, function () {
dispatch({
type: 'ADD_FAILED'
});
});
};
}
// CountResource on client
var Resource = require('ei').Resource;
Resource.register('count', {
add: function (count) {
return ajax(count);
}
});
// CountResource on server
Resource.register('count', {
add: function (count) {
return mysql.query('DO WHATEVER YOU NEED');
}
});
与React相关
child context 机制
这是React的一个隐藏功能,官网上并没有它的明确文档。原因是目前的实现机制并不理想,不久的将来将会被替换成另一个机制。
这里提到的两种机制是:
- 基于
owner的context机制 - 基于
parent的context机制
目前的实现机制是第1种,将会被替换成第2种。React在开发模式中会对这两种模式进行检查,一个组件的owner和parent不一致,并且使用了context,那么你会得到一条警告。也就是说,目前我们可以做到的最好情况就是使ReactElement的owner与parent保持一致。
owner 是创建这个ReactElement的ReactElement
parent 是指在DOM层级上的parentNo
Related Skills
node-connect
349.0kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
109.4kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
349.0kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
349.0kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
