最近两天阿里一直占据着头条,一个是支付宝小程序,一个是 mirror 的发布。本文将详细的介绍 mirror 的原理,特点和使用。
Mirror 是一款基于 React、Redux 和 react-router 的前端框架,简洁高效、灵活可靠。
Mirror 使用的是react-router 4.x既然有了 React 阿里为什么又推出 mirror 呢?
这一切估计得从小程序说起。支付宝也推出了小程序,但是底层完全和微信小程序不同。并且支付宝的小程序是基于 React 的。但是 React 和 Redux 的一些缺点在支付宝小程序上将被放大,因此阿里推出了 mirror。
一个典型的 React/Redux 应用看起来像下面这样:
- 一个 actions/ 目录用来手动创建所有的 action type(或者 action creator);
- 一个 reducers/ 目录以及无数的 switch 来捕获所有的 action type;
- 必须要依赖 middleware 才能处理 异步 action;
- 明确调用 dispatch 方法来 dispatch 所有的 action;
- 手动创建 history 对象关联路由组件,可能还需要与 store 同步;
- 调用 history 上的方法或者 dispatch action 来手动更新路由;
从上面就可以看出 React/Redux 存在的问题。太多的 样板文件 以及繁琐甚至重复的劳动。
实际上,上述大部分操作都是可以简化的。比如,在单个 API 中创建所有的 action 和 reducer;比如,简单地调用一个函数来 dispatch 所有的同步和异步 action,且不需要额外引入 middleware;再比如,使用路由的时候只需要关心定义具体的路由,不用去关心 history 对象,等等。
这正是 Mirror 的使命,用极少数的 API 封装所有繁琐甚至重复的工作,提供一种简洁高效的更高级抽象,同时保持原有的开发模式。
Mirror 的特点
- 极简 API(只有 4 个新 API)
- 易于上手
- Redux action 从未如此简单
- 支持动态创建 model
- 强大的 hook 机制
mirror.model({name, initialState, reducers, effects})
mirror.model
的作用是创建并注入一个 model,所谓的 model,就是 Redux 的 state
、action
和 reducer
的组合。mirror.model
会自动创建 reducer
和 action
,然后被用于创建 Redux store。
简而言之,mirror.model
就是一种组织、管理 Redux 的方式,简单而且强大。
* name
要创建 model,必须要指定 name
,且为一个合法字符串。name
很好理解,就是 model 的名称,这个名称会用于后面创建的 Redux store 里的命名空间。
假设定义了一个这样的 model:
import mirror from 'mirrorx'
mirror.model({
name: 'app',
})
那么最后创建的 Redux store 会是这样的结构:
// ...
store.getState()
// {app: null}
可以看到,model 的 name
就是其 state 在根 store 下的命名空间(当然,name
对全局 actions
也非常重要,见下文)。
另外,需要注意的是,上面创建的 store,其 app
这个 state 的值是 null
,假如你想要一个不同的、更有意义的值,那么你就需要指定一个 initialState
。
注意:Mirror 使用了 react-router-redux,因此你不可以使用
routing
作为 model 的 name。
* initialState
initialState
也很容易理解,表示 model 的初始 state。在创建标准的 Redux reducer
时,它就表示这个 reducer 的 initialState
。
这个值不是必需的,而且可以为任意值。如果没有指定 initialState
,那么它的值就是 null
。
创建 model:
import mirror from 'mirrorx'
mirror.model({
name: 'app',
+ initialState: 0,
})
得到的 store:
store.getState()
// {app: 0}
* reducers
Mirror app 所有的 Redux reducer
都是在 reducers
中定义的,reducers
对象中的方法本身会用于创建 reducer
,方法的名字会用于创建 action type
。Mirror 的原则是,一个 reducer 只负责一个 action,所以你不需要关心你要处理的 action 具体的 type 是什么。
-import mirror from 'mirrorx'
+import mirror, {actions} from 'mirrorx'
mirror.model({
name: 'app',
initialState: 0,
+ reducers: {
+ add(state, data) {
+ return state + data
+ },
+ },
})
执行上述代码,Mirror 实际上做了以下 3 件事情:
- 创建一个 reducer;
- 创建一个 action type(本例中是
app/add
),这个 action 会被上面的 reducer 处理; - 在
actions.<modelName>
上添加一个方法,该方法的名称与reducers
中的方法名完全一致,当调用actions.<modelName>
中的这个方法时,上面创建的action
会被 dispatch。
同时我们也可以看到 model 的 name
的另一个用处:
// ...
typeof actions.app
// 'object'
typeof actions.app.add
// 'function'
actions.app.add(1)
// 等同于:
// dispatch({
// type: 'app/add',
// data: 1
// })
// ...
store.getState()
// {app: 1}
是的,name
的值会成为全局 actions
上的一个属性,该属性是一个对象,而且该对象会被添加与 reducers
中所有方法名相同的方法。调用这些方法会 dispatch 对应的 action。
reducers
中定义的方法,基本上等同于一个 Redux reducer
(所以也必须为纯函数),唯一的区别是参数不同:
// Redux reducer
function reduxReducer(state, {type, data}) {
// 返回一个新的 state
}
// `reducers` 中定义的 reducer
function reducerInReducers(state, data) {
// 返回一个新的 state
}
对于标准的 Redux reducer
,函数的第二个参数是 action
对象;而 reducers
中定义的 reducer,函数的第二个参数是 action data。因为你根本不需要关心 action type。
那么调用 actions.<modelName>
上的方法时,应该传什么参数呢?也是 action data。
// ... // 与手动调用 dispatch 一样,在调用 actions.<modelName> 上的方法时,不需要传递 state 参数 actions.app.add(100)
所有 model 中的所有 reducer 最后都会合并起来形成一个 Redux reducer
(使用 Redux 的combineReducers
),然后用于创建 Redux store。
注意:
reducers
中的非函数属性会被忽略(effects
也一样):import mirror, {actions} from 'mirrorx' mirror.model({ name: 'app', reducers: { a: 1 }, }) actions.app // undefined
* effects
所谓的 effects
就是 Redux 的异步 action(async actions)。在函数式编程中,effect
表示所有会与函数外部发生交互的操作。在 Redux 的世界里,异步 action 显然是 effect
。
effect
不会直接更新 Redux state,通常是在完成某些异步操作(比如 AJAX 请求)之后,再调用其他的“同步 action” 来更新 state。
和 reducers
对象类似,你在 effects
中定义的所有方法都会以相同名称添加到 actions.<modelName>
上,调用这些方法便会调用 effects
你定义的那些方法。
import mirror, {actions} from 'mirrorx'
mirror.model({
name: 'app',
initialState: 0,
reducers: {
add(state, data) {
return state + data
},
},
+ effects: {
+ async myEffect(data, getState) {
+ const res = await Promise.resolve(data)
+ actions.app.add(res)
+ }
+ },
})
执行上述代码,actions.app
就会拥有两个方法:actions.app.add
和 actions.app.myEffect
。
调用 actions.app.myEffect
,就会调用 effects.myEffect
,简单得不能再简单。
// ...
// 首先,dispatch action:
// dispatch({
// type: 'app/myEffect',
// data: 10
// })
//
// 然后,调用函数:
// effects.myEffect(10)
actions.app.myEffect(10)
// ...
store.getState()
// {app: 10}
就是这样,你只需要调用 Mirror 添加到 actions.<modelName>
上的方法,然后你的异步 action 就被 dispatch 了!如果你具有一定的 Redux 经验,那么你一定使用过 redux-thunk 或者 redux-saga 之类的 middleware 来处理异步 action,当然,它们很优秀——但它们都没有 Mirror 简单!
在 effects
中定义的方法接收两个形参:
data
- 调用actions.<modelName>
上的方法时所传递的 data,可选。getState
- 实际上就是store.getState
,返回当前 action 被 dispatch 前的 store 的数据,同样是可选的。
不过,当在调用 actions.<modelName>
上的方法时,你只需要传递上面的 data
作为实参即可(如果需要的话)。
Mirror 强烈推荐使用 async/await
来定义 effect。因为 async 函数会自动返回一个 promise。
当然了,你也可以使用直白的 Promise
:
// ...
effects: {
promisedEffect(data, getState) {
return Promise.resolve(data).then(result => {
// 调用同步 action
})
}
}
甚至,你还可以使用上古时代的 callback(十分不推荐):
// ...
effects: {
callbackEffect(data, getState) {
setTimeout(() => {
// 调用同步 action
}, 1000)
}
}
具体使用什么方式定义 effect 不是重点,重点是,你可以以任何你喜欢的方式来处理你的异步操作,而 Mirror 为你提供了简单一致的 API。
注意:
effects
中定义的 action 的名称,不可以与reducers
中的重复:import mirror, {actions} from 'mirrorx' // 会抛错! mirror.model({ name: 'app', reducers: { add(state, data) { } }, effects: { add(data, getState) { } } })
actions
actions
全局对象包含了 Redux 中的 action
和 reducer
。调用 actions
上的方法,将会 dispatch 一个 action,这个 action 会被你在 mirror.model
接口中的 reducers
和 effects
上定义的方法捕获、处理。
在 Mirror 中,所有的 action 和 effect 都是自动生成的,而且都有处于特定命名空间下。这就意味着,你无法手动创建 action,更重要的是,没有必要。
不但不需要手动创建 action,你也不需要手动 dispatch action。如果你想要一个 action 以及处理这个 action 的 reducer,你完全不需要先定义一个 action type
(或者action creator
),再定义一个处理它的 reducer
。根本不用这么麻烦, 你只管往 reducers
对象里扔一个 reducer 就好了,剩下的交给 Mirror 处理。
这样的好处是,你不需要在不同的文件和目录间跳来跳去去决定到底哪个 action 该由哪个 reducer 来处理了。
例如,执行这段代码:
actions.app.add(1)
完全等同于这段代码:
dispatch({ type: 'app/add', data: 1 })
而且,使用全局的 actions
对象来处理 Redux 的 action,不同组件或者模块间的“依赖关系”也非常明显,而且更不易出错:
假设有一个 a.js:
// a.js
import mirror, {actions} from 'mirrorx'
mirror.model({
name: 'a',
initialState: 0,
reducers: {
add(state, data) {
return state + data
},
},
})
还有一个 b.js:
// b.js
import mirror, {actions} from 'mirrorx'
mirror.model({
name: 'b',
effects: {
async foo(state, data) {
const res = await Promise.resole(data)
// 更新 `a` model 的 state
actions.a.add(data)
},
},
})
可以很清晰地看到,模块 b
会更新模块 a
中的 state。
* actions.routing
如果你的 app 使用了 Mirror 提供的 Router
组件,那么你会自动得到一个 actions.routing
对象。
这个对象上有 5 个方法,都是用来更新 location 的:
push(location)
- 往 history 中添加一条记录,并跳转到目标 location。replace(location)
- 替换 hisotry 中当前 location。go
- 往前或者往后跳转 history 中的 location。goForward
- 往前跳转一条 location 记录,等价于go(1)
。goBack
- 往后跳转一条 location 记录,等价于go(-1)
。
事实上,这些方法来自于 history API,所以意义和用法完全一致。不过与原生方法不同的是,调用 actions.routing
上的这些方法,在更新 location 的同时,你的 routing 与 Redux store 将会保持同步,同时一个 type 为 @@router/LOCATION_CHANGE
的 action 会被 dispatch(感谢 react-router-redux)。
import mirror, {actions} from 'mirrorx'
// ...
actions.routing.push('/foo/bar')
// => http://example.com/foo/bar
actions.routing.push({
pathname: '/foo/bar',
search: '?search=123'
})
// => http://example.com/foo/bar?search=123
查看 Location 了解更多。
注意:如果你的 app 没有使用
Router
,那么actions.routing
将会是undefined
。
mirror.hook((action, getState) => {})
这是一个非常强大的接口,能够让你监控每一个 dispatch 出去的 action。
import mirror, {actions} from 'mirrorx'
// ...
const locationChangeHook = mirror.hook((action, getState) => {
if (action.type === '@@router/LOCATION_CHANGE') {
console.log('Location has just changed')
}
})
const countHook = mirror.hook((action, getState) => {
if (getState().app.count === 10) {
console.log('You have just reached 10!')
}
})
// 移除 hook
locationChangeHook()
countHook()
mirror.hook
会返回一个函数,调用该函数将会移除这个 hook。
mirror.defaults(options)
mirror.defaults
是一个相当直观的 API,你可以用它来设置你的 Mirror app 的一些选项。
* options.initialState
- 默认值:
undefined
表示 Redux store 的 preloadedState
。
mirror.defaults({
initialState: {app: 1}
})
mirror.model({
name: 'app',
// ...
})
// ...
store.getState()
// {app: 1}
* options.historyMode
- 默认值:
browser
表示 Router 组件所需的 history 对象的类型,共有 3 种可选的值:
browser
- 标准的 HTML5 hisotry API。hash
- 针对不支持 HTML5 history API 的浏览器。memory
- history API 的内存实现版本,用于非 DOM 环境。
如果想了解更多,请查看 history。
* options.middlewares
- 默认值:
[]
用来指定一系列标准的 Redux middleware。
假如你想使用一些第三方的 middleware,那么可以在这个选项中指定。同时,你需要调用 connect
且不传递 mapDispatchToProps
来获取 props.dispatch
方法,然后手动 dispatch action。
* options.reducers
- Default:
{}
指定一些额外的 reducer。注意这里定义的 reducer 必须为标准的 Redux reducer,这些 reducer 会直接被 combineReducers
处理。
比如,要想在 Mirror app 中使用 redux-form,那么你可以按照以下方式将 redux-form 的 reducer 集成进来:
import mirror from 'mirrorx'
import { reducer as formReducer } from 'redux-form'
mirror.defaults({
reducers: {
form: formReducer
}
})
更新,而不是替换
mirror.defautls
可以调用多次,那么在后续的调用中,options.reducers
对象是被更新的,而不是被替换。也就是说,参数 options.reducers
中的 key-value
会被合并到之前的对象上去。例如:
// 首次调用,store 中会有一个标准的 reducer 其命名空间为 `a`
mirror.defaults({
reducers: {
// standard Redux reducer
a: (state, data) => {}
}
})
// ...
// 然后在 app 的某个地方,你可以动态地增加标准 reducer
mirror.defaults({
reducers: {
// standard Redux reducer
b: (state, data) => {}
}
})
上述第二次的 mirror.defaults
调用,将会导致 store 中有 2 个标准 reducer:a
和 b
。
* options.addEffect
- Default:
(effects) => (name, handler) => { effects[name] = handler }
自定义指定 effect
如何处理,比如要使用 saga
, 可在这个选项中 runSaga
。更多信息,可查看 mirror-saga 这个项目。
connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])
connect
接口会将 Redux store 与你的 React 组件绑定起来,这个 connect
其实就是 react-redux 的 connect
,所以意义和用法也都完全一致。
和普通的 React app 一样,如果你的组件需要用到 Redux store 的数据,那么你也需要 connect
来绑定数据。
和普通 React app 不一样的是,如果你的某个组件仅仅需要 dispatch 一些 action,那么你完全不要 connect
。因为 actions
对象是全局的,你可以在任意一个组件(哪怕是展示型组件)中引用它,并调用 actions
上的方法来 dispatch action。
注意:
connect
过的组件,如果没有指定mapDispatchToProps
,那么该组件的props
会有一个dispatch
方法,Mirror 保留了这个逻辑。这样,你就可以通过mirror.defaults
接口指定一些 middleware,然后拿到 dispatch 方法来使用它们。不过,这是唯一你需要手动 dispatch action 的情况,在其他所有情况下,你都应该使用全局actions
上的方法来 dispatch action。
render([component], [container], [callback])
Mirror 的 render
接口就是加强版的 ReactDOM.render
,它会启动并渲染你的 Mirror app。
render
首先会创建 Redux store,然后使用 ReactDOM.render
将组件渲染到 DOM 上。render
方法的参数与 ReactDOM.render
完全一致。
你可以在 app 中多次调用 render
。第一次调用会使用 mirror.model
方法中定义的 reducer 和 effect 来创建 store。后续的调用将会 使用 replaceReducer
替换 store 的 reducer,并重新渲染整个 app。
这样处理的意义是什么呢?就是你可以动态载入 model 了,这对 code-splitting 非常有用。
动态加载 model
举例来说,假如你有一个 app.js
:
// app.js
import React from 'react'
import mirror, {actions, connect, render} from 'mirrorx'
mirror.model({
name: 'foo',
initialState: 0
})
const App = connect(({foo, bar}) => {
return {foo, bar}
})(props => {
return (
<div>
<div>{props.foo}</div>
<div>{props.bar}</div>
</div>
)
})
render(<App/>, document.getElementById('root'))
render
之后,你的 app 会被渲染成下面这样:
<div> <div>0</div> <div></div> </div>
然后,假设你又定义一个异步组件/model,可以通过类似 react-loadable 这样的库加载进来:
// asyncComponent.js
// 在这个异步组件中,定义一个"异步 model"
mirror.model({
name: 'bar',
initialState: 'state of bar'
})
// app.js
// ...
// 当加载完这个异步组件之后,调用 `render()` 将会“注册”其对应的异步 model,
// 并重新渲染 app
//
// NOTE: 这里的 `load` 函数为伪代码
load('ayncComponent.js').then(() => {
mirror.render()
})
不传递参数调用 render
将会重新渲染你的 app。所以上述代码将会生成以下 DOM 结构:
<div>
<div>0</div>
- <div></div>
+ <div>state of bar</div>
</div>
动态加载标准 reducer
另外,当加载完异步组件/model 之后,还可以通过调用 mirror.defaults
的方式更新标准的 Redux reducer:
// app.js
// NOTE: 这里的 `load` 函数为伪代码
load('ayncComponent.js').then(() => {
// `MyAsyncReducer` 会被**合并**到之前指定的 reducer 中,而非替换它们
mirror.defaults({
reducers: {
MyAsyncReducer: (state, data) => {},
// ...
}
})
// 重新渲染
mirror.render()
})
这在大型 app 中非常有用。
注意:Mirror 不建议传递
component
和container
参数来重新渲染你的 app,因为这样做可能会导致 React mount/unmount 你的 app。如果你只希望重新渲染,永远不要传递任何参数给render
。
Router
Mirror 使用的是 react-router@4.x,如果你有 react-router 2.x/3.x 的经验,那么你应该仔细阅读一下 react-router 官方的迁移指南。
Mirror 的 Router
组件是加强版的 react-router 的 Router
。所加强的地方在于,Redux store
和 history
都自动处理好了,不需要你去做关联,也不需要你去创建 history
对象,你只需要关心自己的业务逻辑,定义路由即可。当然,如果你想自己创建一个 history
对象,然后通过 prop 传递给 Router
组件,也是没有任何问题的。
那 basename
以及 getUserConfirmation
等 props 呢?不用担心,Mirror 的 Router
全都能处理它们。你可以查看 BrowserRouter
、HashRouter
和 MemoryRouter
的文档获取更多信息。
因为 Mirror 没有将 Router
用到的 history
暴露出去,如果你需要手动更新 location,那么你可以使用 actions.routing
上的方法。
以下这些组件,都来自 react-router
,Mirror 也都暴露出去了,你可以直接引入:
一个简单的例子:
import {render, Router, Route, Link} from 'mirrorx'
// ...
const App = () => (
<div>
<nav>
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/about">About</Link></li>
<li><Link to="/topics">Topics</Link></li>
</ul>
</nav>
<div>
<Route exact path="/" component={Home}/>
<Route path="/about" component={About}/>
<Route path="/topics" component={Topics}/>
</div>
</div>
)
render(
<Router>
<App/>
</Router>
, document.getElementById('root'))
想了解更多 Router
相关的信息,你可以查看 Mirror 提供的 simple-router 示例项目,还有 react-router 官方文档。
toReducers()
Since
1.1.0
一个方法,将当前所有通过 mirror.model
定义的 "model" 转换为一个包含所有 reducer 的对象,该对象可直接用于 combineReducers
。如果你不想使用 mirrorx 提供的 render
方法,不想由 mirrorx 完全控制你的 store,那么 toReducers
就派上用场了。
例如:
import { createStore, combineReducers } from 'redux'
import mirror, { actions } from 'mirrorx'
mirror.model({
initialState: 0,
name: 'count',
reducers: {
increment(state) {
return state + 1
},
decrement(state) {
return state - 1
},
add(state, data) {
return state + data
}
}
})
// `toReducers()` 会返回一个对象,对象的 key 是 model 的 name,value 是其对应的 reducer,
// 再通过 `combineReducers` 将对象转换为一个标准 reducer
const reducer = combineReducers(mirror.toReducers())
// 创建 store
const store = createStore(reducer)
store.getState()
// 0
store.dispatch({ type: 'count/increment' })
store.getState()
// 1
但是,此时如果你想通过 actions.count.increment() 的方式来 dispatch action,将会抛错:
// ...
actions.count.increment()
// Error: You are calling "dispatch" or "getState" without applying mirrorMiddleware! Please create your store with mirrorMiddleware first!
抛错的原因是创建 store 时没有使用 mirrorx 提供的 middleware,也就是说,必须使用 middleware,actions
才会生效,参看 下文的详细解释。
middleware
Since
1.1.0
一个 Redux middleware,它是 actions
和 effects
能够工作的原因。如果你同时想自己创建 store 和使用方便的 actions
,那么必须要应用此 middleware
(通过 applyMiddleware
):
import { createStore, applyMiddleware, combineReducers } from 'redux'
import mirror, { actions, middleware } from 'mirrorx'
mirror.model({
initialState: 0,
name: 'count',
reducers: {
add(state, data) {
return state + data
}
}
})
const reducer = combineReducers(mirror.toReducers())
// 应用 middleware
const store = createStore(reducer, applyMiddleware(middleware))
actions.count.add(10)
store.getState()
// 10