- 默认输出创建 mickey 实例的方法(如
import createApp from 'mickey'
) - 组件和方法输出
- 原样输出以下模块中的组件和方法,mickey 负责管理这些依赖模块的版本,这样我们在需要使用到这些组件或方法时只需要从 mickey 中
import
进来即可,而不需要记住这些组件和方法都分别来自哪个模块。
初始化方法
- createApp(options)
实例方法
- app.model(model)
- app.eject(namespace)
- app.has(namespace)
- app.load(pattern)
- app.render(component, container, callback)
实例属性
- app.store
- app.history
- app.actions
- app.plugin
创建应用,返回 mickey 实例
import createApp from 'mickey';
const app = createApp(options);
默认值:{}
,指定 Redux store 的初始数据(preloadedState),优先级高于 model 中的 state
:
import createApp from 'mickey';
const app = createApp({
initialState: { count: 1 },
});
app.model({
namespace: 'count',
state: 0,
// ...
});
app.render();
app.store.getState();
// { count: 1 }
默认值:{}
,指定应用的初始 reducer 函数,将与模型中的 reducer
一起被 combine 成为 createStore 需要的 reducer
。initialReducer
结构可以像命名空间那样多层嵌套:
import createApp from 'mickey';
const app = createApp({
initialReducer: {
foo: {
bar: {
add: (state, action) => (state + action.payload)
}
}
},
});
默认值:undefined
,指定 Router 组件所需的 history 对象的类型,有 3 种可选的值:
browser
标准的 HTML5 hisotry APIhash
针对不支持 HTML5 history API 的浏览器memory
history API 的内存实现版本,用于非 DOM 环境
如果 historyMode
不是上述三种之一则表示不使用路由组件,启用路由后将在 store
和 actions
中注入对应的 state 和 action 方法:
import createApp from 'mickey'
const app = createApp({historyMode: 'hash'});
app.render();
const state = app.store.getState(); // 一般通过 connect 方法将需要的 state 注入到组件属性中
const actions = app.actions; // 一般通过 injectActions 方法将 actions 方法注入到组件的属性中
/*
state
└─ router
└─ location
├─ pathname
├─ search
└─ hash
actions
└─ router
├─ go
├─ goBack
├─ goForward
├─ push
└─ replace
*/
默认值:undefined
,指定 Router 组件所需的 history 对象。可以创建自定义的 history
对象来取代由 historyMode
指定的路由类型。
import createApp from 'mickey'
import createBrowserHistory from 'history/createBrowserHistory'
// 创建自定义的 history 对象
const history = createBrowserHistory({
basename: "", // The base URL of the app (see below)
forceRefresh: false, // Set true to force full page refreshes
keyLength: 6, // The length of location.key
// A function to use to confirm navigation with the user (see below)
getUserConfirmation: (message, callback) => callback(window.confirm(message))
})
const app = createApp({history});
默认值:{}
,配置应用需要使用的插件,包含:
用于处理全局错误状态,effect
执行错误或 watcher
通过 onError
主动抛错时触发。如果在 watcher
中没有使用 try...catch
,错误信息可以通过参数 onError
主动抛错。例如:
app.model({
watcher: {
setup({ history }, innerAction, actions, onError) {
onError(e);
},
},
});
如果使用 antd,最简单的全局错误处理可以这么做:
import { message } from 'antd';
import createApp from 'mickey';
const app = createApp({
hooks: {
onError(e) {
message.error(e.message, /* duration */3);
},
},
});
当 action 被 dispatch
时触发,用于注册 redux 中间件。支持函数或函数数组。例如通过 redux-logger 打印日志:
import createApp from 'mickey';
import createLogger from 'redux-logger';
const app = createApp({
hooks: {
onAction: createLogger(opts),
},
});
封装 effect 执行。
封装 reducer 执行。如通过 redux-undo 实现 redo/undo:
import createApp from 'mickey'
import undoable from 'redux-undo';
const app = createApp({
hooks:{
onReducer: reducer => {
return (state, action) => {
return undoable(reducer)(state, action);
},
},
},
});
当 state
改变后触发,可用于同步 state
到 localStorage、服务器端等。
指定额外的 reducer,比如 redux-form 需要指定额外的 form
reducer:
import createApp from 'mickey';
import { reducer as formReducer } from 'redux-form';
const app = createApp({
hooks: {
extraReducers: {
form: formReducer,
},
},
});
与 options.initialReducer 不一样的是,extraReducers
指定的 reducer 不能多层嵌套,必须是简单的 key/value
格式。
指定额外的 StoreEnhancer ,比如在 Counter-Persist 示例中结合 redux-persist 的使用:
import createApp, { applyMiddleware } from 'mickey';
import { persistStore, autoRehydrate } from 'redux-persist';
import { REHYDRATE } from 'redux-persist/constants';
import createActionBuffer from 'redux-action-buffer';
import App from './App';
const app = createApp({
hooks: {
extraEnhancers: [
// add `autoRehydrate` as an enhancer to your store
autoRehydrate(),
// make sure to apply this after autoRehydrate
applyMiddleware(
// buffer other reducers before rehydrated
createActionBuffer(REHYDRATE),
),
],
},
});
app.model(require('./models/counter.js'));
app.render(<App />, document.getElementById('root'), {
beforeRender: ({ store }) => new Promise(((resolve) => {
// begin periodically persisting the store
persistStore(store, {
debounce: 500,
whitelist: ['counter'],
keyPrefix: 'mickey:',
}, () => {
resolve(); // delay render after rehydrated
})
})),
});
默认值:{}
,应用扩展点,目前支持如下两个扩展:
mickey 默认使用 redux-actions 模块提供的 handleActions 方法来包装模型中的 reducer
,可以通过设置 options.extensions.createReducer
来替换默认实现。例如,在 Counter-Immutable 示例中需要使用 redux-immutablejs 模块提供的 createReducer 方法来替换。
mickey 默认使用 redux 提供的 combineReducers 方法将模型中的 reducer
连接在一起,可以通过设置 options.extensions.combineReducers
来替换默认实现。例如,在 Counter-Immutable 示例中需要使用 redux-immutablejs 模块提供的 combineReducers 方法来替换。
import createApp from 'mickey';
import Immutable from 'immutable';
import { createReducer, combineReducers } from 'redux-immutablejs';
const app = createApp({
initialState: Immutable.fromJS({}),
extensions: {
createReducer,
combineReducers,
},
});
装载指定的模型,模型是 mickey 中最重要的概念,一个典型的例子如下:
import { query } from '../services/todo.js'
export default {
namespace: 'todo',
state: {
items: [],
loading: false,
},
load: {
* effect(payload, { call }, { success, failed }) {
try {
const items = yield call(query)
yield success(items) // 触发成功回调
} catch (error) {
yield failed(error) // 触发失败回调
}
},
prepare: state => ({
...state,
loading: true,
}),
success: (state, items) => ({
...state,
items: [...items],
loading: false,
}),
failed: (state, error) => ({
...state,
error,
loading: false,
}),
},
watcher: {
setup({ history }, innerActions) {
// 监听 history 变化,当进入 `/` 时触发 `load`
return history.listen(({ pathname }) => {
if (pathname === '/') {
innerActions.load()
}
})
},
},
}
指定模型的命名空间,命名空间可以使用 .
来划分层级结构,命名空间的层级结构决定了最终 store
和 actions
的层级结构,如:
app.model({ namespace: 'app.header' })
app.model({ namespace: 'app.content' })
app.model({ namespace: 'common' })
那么得到的 store
和 actions
结构如下:
store 和 actions
├── app
│ ├── header
│ └── content
└── common
虽然可以使用 .
来划分 store
的层级结构,但请注意一定不要使 store
的结果过于复杂。
命名空间不可缺省,当使用 babel-plugin-mickey-model-loader 提供的 app.load(pattern)
方法来加载模型时会根据模型所在目录结构确定模型的命名空间,此时可以不指定模型的命令空间,看下面的模型的目录结构,通过 app.load()
之后将得到与上面相同的结构。
.
└── models
├── app
│ ├── header.js
│ └── content.js
└── common.js
初始值,优先级低于传给 createApp()
的 options.initialState
:
import createApp from 'mickey';
const app = createApp({
initialState: { count: 1 },
});
app.model({
namespace: 'count',
state: 0,
// ...
});
app.render();
app.store.getState();
// { count: 1 }
以 key/value
格式定义 reducer 和 effect,用于处理同步或异步操作,key
表示 action 名称,value
分下面四种情况:
-
普通函数
(state, payload) => newState
用于处理同步操作,唯一可以修改
state
的地方。 -
Generator 函数
*(payload, effects, callbacks, innerActions, actions) => void
用于处理异步操作和业务逻辑,不直接修改
state
。 -
数组
[*(payload, effects, callbacks, innerActions, actions) => void, { type } ]
处理异步操作和业务逻辑的另一种格式,可以通过 type 指定调用 effect 的方式:
takeEvery
takeLatest
throttle
watcher
当
type
为throttle
时还需要指定 throttle 的时间间隔:[*(...) => void, { type, ms } ]
-
对象
在继续解释该对象的结构之前,先看一下 mickey 的设计思路。
对一个异步 action 的处理通常会经历以下几步:
- 触发异步请求前的准备工作,如修改
state.loading: true
,使界面中显示一个 loading 图标 - 触发异步 action 发起异步接口调用
- 分调用成功和失败两种情况处理接口返回,并触发对应的同步 action 来修改
state
中的数据
上面几步组合在一起可以称为一个“异步处理单元”,如果按之前的思路来设计,那么模型看来像下面这样:
{ * query(condition, { call }, callbacks, innerActions) { try { const data = yield call(query) yield innerActions.querySuccess(data) } catch (error) { yield innerActions.queryFailed(error) } }, queryPrepare: state => ({ ...state, loading: true }), querySuccess: (state, data) => ({ ...state, data, loading: false }), queryFailed: (state, error) => ({ ...state, error, loading: false }), }
对一个异步 action 处理需要 4 个对应的处理函数,而这 4 个处理函数的相关性极强,分开来写看起来并没有那么优雅,所有 mickey 提供了更加“模块化”的形式:
{ query: { * effect(condition, { call }, callbacks) { try { const data = yield call(query) yield callbacks.success(data) } catch (error) { yield callbacks.failed(error) } }, prepare: state => ({ ...state, loading: true }), success: (state, data) => ({ ...state, data, loading: false }), failed: (state, error) => ({ ...state, error, loading: false }), }, },
或者这样:
{ query: { // 这里直接使用了 es6 的对象扩展运算符拿到了 callbacks 中的方法 * effect(condition, { call }, {success, failed }) { try { const data = yield call(query) yield success(data) } catch (error) { yield failed(error) } }, prepare: state => ({ ...state, loading: true }), success: (state, data) => ({ ...state, data, loading: false }), failed: (state, error) => ({ ...state, error, loading: false }), }, },
对于一个“异步处理单元”有几点需要强调:
- 在一个“异步处理单元”中需要至少包含一个同步或异步 action 的处理方法;
effect
这个方法名随意,在 mickey 内部是通过判断一个函数是否是 Generator 来确定它是不是一个异步处理方法;prepare
这个方法名必须固定,只能这样 mickey 才知道这是一个同步处理方法,并且需要在触发query
这个 action 时同时触发prepare
。也就是说,当触发query
这个 action 时effect
和prepare
将被“同时”触发;- 除
effect
和prepare
之外的其他方法都被称为“回调”(callbacks),回调方法名随意,这些方法名将分别以如下形式注入到异步处理函数的参数中:callbacks
中以同名的方式注入,如success
和failed
;innerActions
中将以actionName + 驼峰(callback)
命名注入对应的方法,如querySuccess
和queryFiled
actions
中注入的方法名与innerActions
一样,不同的是在actions
中的方法都需要用完整的命名空间来调用,如todo.querySuccess()
和todo.queryFailed()
- 所有回调方法的参数签名都一样:
(payload) => void
,如:success(data)
或innerActions.del(id)
- 触发异步请求前的准备工作,如修改
下面分别看看同步和异步处理方法的方法签名:
-
处理同步 action:
(state, payload) => newState
-
处理异步 action:
(payload, sagaEffects, callbacks, innerActions, actions) => void
payload
与上面提到的同步 action 处理函数中的payload
意义一样;sagaEffects
redux-saga 中 effects 列表;callbacks
一个"异步处理单元"的回调集合,用于触发该“异步处理单元”中的回调;innerActions
本模型所有 action 的集合,用于跨“异步处理单元”触发该模型内部的其他 action;actions
应用所有 action 的集合,通过模型命名空间访问,用于跨模型触发其他模型中的 action。
函数或函数数组,用于订阅一个数据源,然后根据需要触发相应的 action。在 app.render()
后被执行,数据源可以是当前的时间、服务器的 websocket 连接、keyboard 输入、geolocation 变化、history 路由变化等等。
格式 ({ history, getState }, innerActions, actions, onError) => unlistenFunction
:
history
根据historyMode
创建的 history 实例getState(path, defaultValue)
调用该方法可以获取store
中的数据,参数意义参考 lodash.getinnerActions
本模型所有 action 集合,用于触发该模型内部的 actionactions
应用所有 action 的集合,通过模型命名空间访问,用于跨模型触发其他模型中的 action
注意:如果要使用 app.eject()
watcher 必须返回 unlisten 方法,用于取消数据订阅。
函数或函数数组,封装模型内部 reducer 执行。例如,在 Counter-Persist 示例中,需要在模型中手动处理 rehydrate 过程:
import { REHYDRATE } from 'redux-persist/constants';
const delay = timeout => new Promise((resolve) => {
setTimeout(resolve, timeout);
});
export default {
namespace: 'counter',
state: {
count: 0,
loading: false,
},
enhancers: [
reducer => (state, action) => {
const { type, payload } = action
if (type === REHYDRATE) {
return {
...state,
...payload.counter,
}
}
return reducer(state, action)
},
],
increment: state => ({ ...state, count: state.count + 1 }),
decrement: state => ({ ...state, count: state.count - 1 }),
incrementAsync: {
* effect(payload, { call }, { succeed }) {
yield call(delay, 2000)
yield succeed()
},
prepare: state => ({ ...state, loading: true }),
succeed: state => ({ ...state, count: state.count + 1, loading: false }),
},
}
再如,在 Counter-Undo 示例中,我们需要对 counter
这个模型实现 redo/undo:
import undoable from 'redux-undo';
const delay = timeout => new Promise((resolve) => {
setTimeout(resolve, timeout)
});
export default {
namespace: 'counter',
state: {
count: 0,
loading: false,
},
enhancers: [
reducer => (state, action) => {
const undoOpts = {}
const newState = undoable(reducer, undoOpts)(state, action)
return { ...newState }
},
],
increment: state => ({ ...state, count: state.count + 1 }),
decrement: state => ({ ...state, count: state.count - 1 }),
incrementAsync: {
* effect(payload, { call }, { succeed }) {
yield call(delay, 2000)
yield succeed()
},
prepare: state => ({ ...state, loading: true }),
succeed: state => ({ ...state, count: state.count + 1, loading: false }),
},
}
与 options.extensions.createReducer 意义一样,不同的是这里只作用与本模型,一旦指定则具有最高优先级。
卸载指定 namespace
模型,同时清理 Store 中对应的数据,取消 watcher
中相关的事件订阅。
返回指定 namespace
模型是否被已经被装载,或判断一个 namespace
是否被占用。
根据 pattern
指定的路径加载模型,需要 babel-plugin-mickey-model-loader 支持,pattern
为空表示加载所有模型,否则可以指定一个 glob 表达式来加载匹配的模型。
渲染组件到指定的容器中(HTML元素或 selector),并提供回调或 AOP 支持;当 callback
是函数((app) => {}
)时将在渲染完成之后 watcher
之前执行;如果 callback
是形如下面对象:
{
beforeRender(app) { },
afterRender(app) { },
}
beforeRender
返回 false
或返回的 Promise 被 reject
时都不会触发渲染过程;当 beforeRender
返回 Promise 被 resolve
后触发内部的渲染过程,同时还可以通过 resolve
重新指定component
和 container
:
app.render(component, container, {
beforeRender(app) {
return new Promise((resolve) => {
resolve([newComponent, newContainer]);
})
}
})
返回一个 Promise 非常实用,如在 Counter-Persist 实例中,需要等 rehydrate 完成之后才触发实际的渲染过程:
app.render(<App />, document.getElementById('root'), {
beforeRender: ({ store }) => new Promise(((resolve) => {
// begin periodically persisting the store
persistStore(store, {
debounce: 500,
whitelist: ['counter'],
keyPrefix: 'mickey:',
}, () => {
resolve() // delay render after rehydrated
})
})),
})
使 app.actions
可以被 injectActions
方法注入到子组件中。该组件在 mickey 内部的渲染过程中被使用,并不会在实际项目代码中使用。
<ActionsProvider actions={app.actions}>
<App />
</ActionsProvider>
将 actions
注入到指定的组件属性中,属性名 (propName) 默认为 actions
,这样在组件中就可以通过 this.props.actions[namespace]
来获取到指定的方法,进而触发对应的 action。当 withRef = true
时将保存一个被包裹组件的实例,可以通过 this.getWrappedInstance()
来获取到。
例如,Counter 实例:
import React from 'react'
import { connect, injectActions } from 'mickey'
import './App.css'
const App = props => (
<div id="counter-app">
<h1>{props.count}</h1>
<div className="btn-wrap">
<button onClick={() => props.actions.counter.decrement()}>-</button>
<button onClick={() => props.actions.counter.increment()}>+</button>
<button
style={{ width: 100 }}
onClick={() => {
if (props.loading) {
alert('loading') // eslint-disable-line
} else {
props.actions.counter.incrementAsync()
}
}}
>
{props.loading ? 'loading' : '+ Async'}
</button>
</div>
</div>
)
export default injectActions(connect(store => ({ ...store.counter }))(App))