apollo
apollo
是一个基于 redux
和 redux-thunk
的数据流解决方案,通过对 react
、 redux
、 react-router
的整合极大的简化了前端项目的开发流程,致力于快速构建大型的高可用的前端系统。
优势
兼容性:既能担任移动端主力开发框架,还可以在 PC 端支持主流浏览器,最低兼容 IE8。
页面效果:与其它开发框架(如
Angular
)相比,更专注于UI
层面的react
页面渲染速度更快,加载时间更短。成熟的 UI 体系也较容易制作出良好的体验。开发成本:
apollo
集成开发框架带来更低开发维护成本,成熟的规范和完善的文档能为项目组省时省力,众多的⼯具满足开发过程中常见的需求。上手即用,方便又快捷。可复⽤用性:成熟的组件化结构,可复用性强。项目之间的组件可以共享互通。
扩展性:活跃的开源社区,琳琅满目的项目,取之即用,适应能力强,版本升级成本低。
核心概念
数据流向
数据的改变一般是用户交互行为或者浏览器行为(如路由跳转等)触发的,在这类事件发生时会通过 dispatch
发起一个 Action
,如果是同步行为会直接通过 Reducer
改变 State
,如果是异步行为会先触发 Thunk
然后流向 Reducer
最终改变 State
,因此在 apollo
中,数据流向将严格遵循单向数据流的理念,从而保证项目的可维护性。
Model 组成
State
State 表示 Model 的状态数据,通常表现为一个 JavaScript 对象(当然它可以是任何值);操作的时候每次都要当作不可变数据(immutable data)来对待,保证每次都是全新对象,没有引用关系,这样才能保证 State 的独立性,便于测试和追踪变化。
在 apollo 中,您可以通过 apollo 的实例属性 _store
获取顶部 state
数据,但一般很少会用到:
const app = createApollo();
console.log(app._store); // 顶部的 state 数据
Action
Action
是把数据从应用传到 Store
的有效载荷。它是 Store
数据的唯一来源。一般来说你会通过 store.dispatch()
将 Action
传到 Store
。Action
必须带有 type
属性指明具体的行为,其它字段可以自定义,如果要发起一个 Action
需要使用 dispatch
函数;需要注意的是 dispatch
是在组件 connect Model以后,通过 props
传入的。
添加新 todo
任务的 Action
是这样的:
dispatch({
type: 'add',
});
dispatch 函数
dispatch 是一个用于触发 Action 的函数,Action 是改变 State 的唯一途径,但是它只描述了一个行为,而 dispatch 可以看作是触发这个行为的方式,而 Reducer 则是描述如何改变数据的。
dispatch({
type: 'user/add', // 如果在 model 外调用,需要添加 namespace
payload: {}, // 需要传递的信息
});
Reducer
Reducer 指定了应用状态的变化如何响应 Action 并发送到 Store 的,记住 Action 只是描述了有事情发生了这一事实,并没有描述应用如何更新 State。需要注意的是 Reducer 必须是纯函数,所以同样的输入必然得到同样的输出,它们不应该产生任何副作用。
const todoApp = (state = initialState, action) => {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return Object.assign({}, state, {
visibilityFilter: action.filter
})
default:
return state
}
}
Thunk
Thunk 用于处理异步流程,以 key/value 格式定义,返回值必须是 Promise
:
thunk: {
thunk1: (action, {put, select}) => {
return new Promise(fn)
}
}
Thunk 的触发同样是通过 dispatch
:
dispatch({
type: 'namespace/thunk1',
payload: {},
async: true
})
注意:这里的 async:true
为必传字段,我们的中间件 apolloMiddleware
将以此判断该 Action
是否为一个 Thunk
,并执行对应的方法。
Subscription
Subscription 用于订阅一个数据源,然后根据条件 dispatch
需要的 Action
。数据源可以是当前的时间、服务器的 Websocket
连接、Keyboard
输入、Geolocation
变化、History
路由变化等等。
subscription: {
setup: ({dispatch, history, listen}, onError) => {
//使用 history 监控路由变化
return history.listen((loaction)=> {console.log(loaction)})
//使用 listen 监控路由变化
//第二个参数可以是个 Action, 匹配 /test 路由时 dispatch 这个 Action
return listen('/test', {type:'test'})
//第二个参数可以是个 function, 匹配 /test 路由时调用该回调
return listen('/test/:id', ({params, query})=> {})
//第一个参数可以是 object ,对多个 path 进行监听
return listen({
'/test':({params, query})=> {},
'/test1':({params, query})=> {},
})
return listen({
'/test':{type:'test'},
'/test1':{type:'test1'}
})
}
}
Router
这里的路由通常指的是前端路由,由于我们的应用现在通常是单页应用,所以需要前端代码来控制路由逻辑,通过浏览器提供的 History API
可以监听浏览器 url
的变化,从而控制路由相关操作。
apollo
所提供的路由方法主要依赖于目前最为流行的 react-router
工具库,它是一个基于 react 之上的强大路由库,它可以让你向应用中快速地添加视图和数据流,同时保持页面与 URL 间的同步。我们通过对 react-router
和 redux
的结合,实现了数据流的全方位整合,为 Store
的持久化等操作提供了实现基础。
快速上手
依照惯例,我们来基于 apollo 实现一个 todo-list
应用。其核心思想就是我们有一个 list,用来存储每一条待办事项,并且允许用户添加或者移除待办事项。所以,我们先来进行第一步:
第1步:构建 Model
Model 是 apollo 框架的核心部分,包含数据存储以及业务代码其组成如下:
namespace
: 表示这个Model
属于哪个模块,语义上应该负责什么功能,与其他Model
区分开来。state
: 表示这个功能模块会存储哪些数据。reducer
: 负责更改State
的方法。thunk
:处理异步请求的方法(返回Promise
)。
创建models/todo.js
,内容如下:
import * as todoService from '../services/todo';
export default {
namespace: 'todo',
state: {
list: [],
},
reducer: {
save(state, {payload: item}) {
const list = [...state.list, item]
return {...state, list}
},
remove(state, {payload: index}){
state.list.splice(index, 1);
const list = [...state.list];
return {...state, list}
}
},
thunk: {
addTodo({payload: todo}, {put, select}){
return todoService.addTodo(todo).then( res => {
put({
type: 'todo/save',
payload: res
});
})
},
removeTodo({payload: index}, {put, select}){
return todoService.removeTodo(index).then( res => {
put({
type: 'todo/remove',
payload: res
})
})
}
}
};
redux 的核心思想之一就是统一的 state
,我们将我们应用的 model
构建出来,针对 todo-list
这个部分,告诉框架 state
里需要放什么,有哪些影响 state
的 reducer
,有哪些负责发起 reducer
的 thunk
。
这里的 namespace
很重要,是 apollo
将几个部件相互连接起来的依据。我们 state
中描述这个模块我们需要一个名为 list
的数组,然后有两个 reducer
分别是往 list
中增加一条 todo
和删除一条 todo
,再到后面有两个 thunk
,分别从服务层去调用相应的 API
(此处模拟),并最终发起 reduce
来更新 state
。
注意 thunk
的第二参数中的第二个值 select
,可以用来在 thunk
中获取 state
里面的变量:
const name = select(({name}) => name)
第2步:构建 services
这一步是可选的,只是为了将业务服务与 model
分离开来,使得代码结构更清晰,所以引入 service
的概念。
上一步中,我们看到 models
里边引入了 todoService
,这里就要来创建这个 service
。
//可以在此引用其他的 HTTP request 库,或者任何处理 AJAX 请求的库。
export const addTodo = todo => {
//常规应用中这里应当放置异步请求(GET,POST等)方法,这些方法同样也是返回一个 Promise
return new Promise((resolve, reject) => {
setTimeout(()=>{
resolve(todo)
}, 2000)
})
}
export const removeTodo = index => {
return new Promise((resolve, reject) => {
//常规应用中这里应当放置异步请求(GET,POST等)方法,这些方法同样也是返回一个 Promise
setTimeout(()=>{
resolve(index)
}, 2000)
})
}
此处只是定时两秒之后返回传进来的参数,做了一个假的异步请求。
当然,如果是在做一个实际的 web 应用,这里放的就是 AJAX
请求方法,并把方法本身(也返回一个 Promise
) return
回去,让 thunk
去调用 .then
方法。目前已有很多基于 AJAX
的 Promise
实现,例如 axios
。
这一步骤可以省略,你也可以直接在 model
中引入并在 Thunk
中调用自己的 AJAX
请求方法。只是分离出来的话代码结构更清晰一些。
第3步:构建实际的 component
创建 src/components/todo-list/index.js
,代码如下:
import React from 'react';
import { connect } from 'apollo';
import './index.css';
const TodoList = (props) => {
const showPrompt = () => {
props.addTodo(window.prompt('What do you want to do?'));
}
return (
<div>
<ul className="todos">
{
props.list.length > 0 ? props.list.map((item, index) => (
<li>
{item}
<button onClick={() => { props.removeTodo(index) }}>
删除
</button>
</li>
)) : null
}
</ul>
<button onClick={showPrompt}>新增</button>
</div>
)
}
const mapDispatchToProps = (dispatch) => {
return {
addTodo(todo) {
dispatch({
type: 'todo/addTodo',
payload: todo,
async: true
})
},
removeTodo(index) {
dispatch({
type: 'todo/removeTodo',
payload: index,
async: true
})
}
}
}
const mapStateToProps = ({ todo }) => {
return {
...todo
};
}
export default connect(mapStateToProps, mapDispatchToProps)(TodoList)
注意此处我们使用的是一个纯函数组件,其有一个参数即 props
。
我们先看最后定义的两个方法,一个是 mapDispatchToProps
,接收一个参数 dispatch
,此处 dispatch
会由框架传递,是用于发起 Action(thunk)
的方法。所以这里就封装了最后由 component
的一些事件触发的 Thunk
或者 Reducer
调用,这里具体就是增加 Todo
和删除 Todo
,注意此处 dispatch
接收的参数中 async
为 true
,代表我们要调用的这个东⻄是 Thunk
,如果不用 async
的话则代表我们希望直接调用 reducer
。
另一个方法则是 mapStateToProps
,顾名思义就是将 apollo
管理的整个 state
中相关的部分映射到这个组件的 props
中来。注意此处接收的参数是整个 app
的 state
,我们写成 {todo}
意味着我们只接受 state.todo
,然后我们将 state.todo
中的东⻄给返回去。
最重要的一部分,也就是开头引入的 connect
方法。 export
这个组件的时候我们将 mapStateToProps
和 action
都与 TodoList
进行连接,会将它们都注入组件的 props
。此时这个组件 render
时我们就可以使用 props.list
(从 model 中映射过来的),并且以两个 button
来发起 props.addTodo
和 props.removeTodo
了。
第4步:创建 router(或新的 component 加入 router)
import React from 'react';
import { Router, Route } from 'apollo/router';
const cached = {};
const registerModel = (app, model) => {
if (!cached[model.namespace]) {
app.model(model);
cached[model.namespace] = 1;
}
}
const RouterConfig = ({history, app}) => {
return (
<Router history={history}>
<Route path="/" getComponent={(nextState, cb) => {
require.ensure([], (require) => {
registerModel(app, require('../models/todo').default);
cb(null, require('../components/todo-list/index').default);
});
}} />
</Router>
)
}
export default RouterConfig;
apollo 也提供了 Router
的引用,所以直接可以从 apollo/router
中引入 react-router
或者 react-router-redux
。
需要注意的一点是这里有一个 registerModel
,封装了调用 app.model
去注册 model
的方法。这里的 model
就是之前我们定义的那个 model
。
然后由于我们做的内容比较简单,只是一个 todo-list
,所以我们这里就只使用 /
作唯一的路由了。在指定 component
时我们需要调用 registerModel
来注册对应的 model
,这样我们才能正确的 connect
对应的内容到组件上。
第5步:启动应用
import React from 'react';
import createApollo from 'apollo';
import Router from './router/router';
import './index.css';
// 1. Initialize
const app = createApollo();
// 2. Plugins
// app.use(createLoading());
// 3. Router
app.router(Router);
// 4. Start
app.start('#root');
只需4步,我们就可以启动刚才创建的 todo-list
了。注意我们这里省去了第2步,也就是插件,这里没有用到。
另一个注意的地方就是 app.start
的参数,就是模板 HTML
中的 div
的名字。
第6步:npm start
启动 dev 服务器:
$ npm start
浏览器会自动打开 localhost:3000
,测试一下我们构建的 todo-list
吧!
API
app = apollo(opts)
创建应用,返回 apollo 实例。
opts
包含
- history:指定给路由用的 history,默认是 hashHistory
- onThunkType: 指定 onThunk 的增强方式,默认是 all。
普通开发人员无需关注该接口
。
如果要配置 history 为 browserHistory,可以这样:
import { browserHistory } from 'apollo/router';
const app = apollo({
history: browserHistory,
});
另外,出于易用性的考虑,opts 里也可以配所有的 hooks ,下面包含全部的可配属性:
const app = apollo({
history,
initialState,
onError,
onAction,
onStateChange,
onReducer,
onThunk,
extraReducers,
extraEnhancers,
onThunkType
});
app.use(hooks)
配置 hooks 或者注册插件。(插件最终返回的是 hooks )
hooks 包含:
onError(fn, dispatch)
thunk
执行错误时触发,可用于管理全局出错状态。如果 thunk
主动对 error 进行 catch,不会触发该钩子。
const app = apollo({
onError(e, dispatch) {
alert(e.message);
dispatch({
type: 'errorHandle',
payload: e
})
},
});
onAction(fn | fn[])
在 action 被 dispatch 时触发,用于注册 redux 中间件。支持函数或函数数组格式。
例如我们要通过 redux-logger 打印日志;
import createLogger from 'redux-logger';
const app = apollo({
onAction: createLogger(opts),
});
onStateChange(fn)
state 改变时触发,可用于同步 state 到 localStorage,服务器端等。其中 fn 的入参为当前 state。
const app = apollo({
onStateChange: function (state) {
console.log('onStateChange:');
console.log(state);
}
});
onReducer(fn) 封装 reducer 执行。
onReducer: function (reducer) {
return function(state, action) {
console.log('onReducer!!!')
return reducer(state,action);
}
}
onThunk(fn)
封装 thunk 执行。onThunk
必须返回一个 promise
const app = apollo({
onThunk(thunk, {put, select}) {
return new Promise(function (resolve, reject) {
put({
type: '@@APOLLO_LOADING/SHOW'
});
resolve('success');
}).then(function () {
return thunk.call(thunk)
}).then(function (val) {
put({
type: '@@APOLLO_LOADING/HIDE'
});
return val
})
},
})
extraReducers 指定额外的 reducer。
const app = apollo({
extraReducers: {
extra: extraReducer
}
})
extraEnhancers 指定额外的 StoreEnhancer。StoreEnhancer请参考 redux 的 StoreEnhancer。
以上所有关于 hooks 初始化设置都可以使用 app.use(hooks) 来完成。
const app = apollo(); app.use({ onError, onAction, onStateChange, onReducer, onThunk, extraReducers, extraEnhancers, })
app.model(model)
注册 model。
model
model
是 apollo 中最重要的概念,以下是典型的例子:
app.model({
namespace: 'user',
state: {
name: ''
},
reducer: {
setName: function(prevState, {payload: name}) {
return {...prevState, name}
}
},
thunk: {
getName: function({payload}, {put}) {
return fetch('/api/getUserName')
.then(function(res){
put({
type: 'user/setName',
payload: res.name
})
})
}
}
})
model 包含 5 个属性:
- namespace
model 的命名空间,同时也是他在全局 state 上的属性,只能用字符串,不支持通过 .
的方式创建多层命名空间。
- state
初始状态值
- reducer
以 key/value 格式定义 reducer。用于处理同步操作,唯一可以修改 state 的地方。由 action 触发。每个 reducer 对应的 actionType 为 @namespace/@recuder-key
格式为 (state, action) => newState
- thunk
以 key/value 格式定义 thunk。用于异步操作。thunk的入参如下:
thunk: {
thunk1: function(action, {put, select}) {
return new Promise(fn)
}
}
thunk
第一个参数是 action, 第二个参数是个 object,里面包含了 put
和 select
方法。其中 put
等同于 dispatch
。 select
用于选择当前 state 值,只能选择该 model 中的 state
。
const name = select(({name}) => name)
thunk
必须返回一个 promise。
- subscription
以 key/value 格式定义 subscription。subscription 是订阅,用于订阅一个数据源,然后根据需要 dispatch 相应的 action。在 app.start()
或者异步加载 model
时执行
subscription: {
setup: function({dispatch, history, listen}, onError) {
//使用 history 监控路由变化
return history.listen((loaction)=> {console.log(loaction)})
//使用 listen 监控路由变化
//第二个参数可以是个 action, 匹配 /test 路由时 dispatch 这个 action
return listen('/test', {type:'test'})
//第二个参数可以是个 function, 匹配 /test 路由时调用该回调
return listen('/test/:id', ({params, query})=> {})
//第一个参数可以是 object,对多个 path 进行监听
return listen({
'/test':({params, query})=> {},
'/test1':({params, query})=> {},
})
return listen({
'/test':{type:'test'},
'/test1':{type:'test1'}
})
}
}
app.router(({ history, app } => Router)
注册路由表。
import { Router, Route } from 'apollo/router';
app.router(({ history }) => {
return (
<Router history={history}>
<Route path="/" component={App} />
<Router>
);
});
也可以传入返回 JSX 元素的函数。比如:
app.router(() => <App />)
app.start(selector?)
启动应用。selector 可选,如果没有 selector 参数,会返回一个返回 JSX 元素的函数。
app.start('#root');
或
const App = app.start();
ReactDom.render(<App/>, el)