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
,代码如下:
dispatch({ type: 'add', });
0
注意此处我们使用的是一个纯函数组件,其有一个参数即 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)
dispatch({ type: 'add', });
1
apollo 也提供了 Router
的引用,所以直接可以从 apollo/router
中引入 react-router
或者 react-router-redux
。
需要注意的一点是这里有一个 registerModel
,封装了调用 app.model
去注册 model
的方法。这里的 model
就是之前我们定义的那个 model
。
然后由于我们做的内容比较简单,只是一个 todo-list
,所以我们这里就只使用 /
作唯一的路由了。在指定 component
时我们需要调用 registerModel
来注册对应的 model
,这样我们才能正确的 connect
对应的内容到组件上。
第5步:启动应用
dispatch({ type: 'add', });
2
只需4步,我们就可以启动刚才创建的 todo-list
了。注意我们这里省去了第2步,也就是插件,这里没有用到。 另一个注意的地方就是 app.start
的参数,就是模板 HTML
中的 div
的名字。
第6步:npm start
启动 dev 服务器:
dispatch({ type: 'add', });
3
浏览器会自动打开 localhost:3000
,测试一下我们构建的 todo-list
吧!
API
app = apollo(opts)
创建应用,返回 apollo 实例。 opts
包含
- history:指定给路由用的 history,默认是 hashHistory
- onThunkType: 指定 onThunk 的增强方式,默认是 all。
普通开发人员无需关注该接口
。
如果要配置 history 为 browserHistory,可以这样:
dispatch({ type: 'add', });
4
另外,出于易用性的考虑,opts 里也可以配所有的 hooks ,下面包含全部的可配属性:
dispatch({ type: 'add', });
5
app.use(hooks)
配置 hooks 或者注册插件。(插件最终返回的是 hooks ) hooks 包含: onError(fn, dispatch) thunk
执行错误时触发,可用于管理全局出错状态。如果 thunk
主动对 error 进行 catch,不会触发该钩子。
dispatch({ type: 'add', });
6
onAction(fn | fn[])
在 action 被 dispatch 时触发,用于注册 redux 中间件。支持函数或函数数组格式。
例如我们要通过 redux-logger 打印日志;
dispatch({ type: 'add', });
7
onStateChange(fn)
state 改变时触发,可用于同步 state 到 localStorage,服务器端等。其中 fn 的入参为当前 state。
dispatch({ type: 'add', });
8
onReducer(fn) 封装 reducer 执行。
dispatch({ type: 'add', });
9
onThunk(fn) 封装 thunk 执行。onThunk
必须返回一个 promise
dispatch({ type: 'user/add', // 如果在 model 外调用,需要添加 namespace payload: {}, // 需要传递的信息 });
0
extraReducers 指定额外的 reducer。
dispatch({ type: 'user/add', // 如果在 model 外调用,需要添加 namespace payload: {}, // 需要传递的信息 });
1
extraEnhancers 指定额外的 StoreEnhancer。StoreEnhancer请参考 redux 的 StoreEnhancer。
以上所有关于 hooks 初始化设置都可以使用 app.use(hooks) 来完成。
dispatch({ type: 'user/add', // 如果在 model 外调用,需要添加 namespace payload: {}, // 需要传递的信息 });
2
app.model(model)
注册 model。 model model
是 apollo 中最重要的概念,以下是典型的例子:
dispatch({ type: 'user/add', // 如果在 model 外调用,需要添加 namespace payload: {}, // 需要传递的信息 });
3
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的入参如下:
dispatch({ type: 'user/add', // 如果在 model 外调用,需要添加 namespace payload: {}, // 需要传递的信息 });
4
thunk
第一个参数是 action, 第二个参数是个 object,里面包含了 put
和 select
方法。其中 put
等同于 dispatch
。 select
用于选择当前 state 值,只能选择该 model 中的 state
。
dispatch({ type: 'user/add', // 如果在 model 外调用,需要添加 namespace payload: {}, // 需要传递的信息 });
5
thunk
必须返回一个 promise。
- subscription
以 key/value 格式定义 subscription。subscription 是订阅,用于订阅一个数据源,然后根据需要 dispatch 相应的 action。在 app.start()
或者异步加载 model
时执行
dispatch({ type: 'user/add', // 如果在 model 外调用,需要添加 namespace payload: {}, // 需要传递的信息 });
6
app.router(({ history, app } => Router)
注册路由表。
dispatch({ type: 'user/add', // 如果在 model 外调用,需要添加 namespace payload: {}, // 需要传递的信息 });
7
也可以传入返回 JSX 元素的函数。比如:
dispatch({ type: 'user/add', // 如果在 model 外调用,需要添加 namespace payload: {}, // 需要传递的信息 });
8
app.start(selector?)
启动应用。selector 可选,如果没有 selector 参数,会返回一个返回 JSX 元素的函数。
dispatch({ type: 'user/add', // 如果在 model 外调用,需要添加 namespace payload: {}, // 需要传递的信息 });
9
或
const todoApp = (state = initialState, action) => { switch (action.type) { case SET_VISIBILITY_FILTER: return Object.assign({}, state, { visibilityFilter: action.filter }) default: return state } }
0
还没有评论,来说两句吧...