apollo

apollo 是一个基于 reduxredux-thunk 的数据流解决方案,通过对 reactreduxreact-router 的整合极大的简化了前端项目的开发流程,致力于快速构建大型的高可用的前端系统。

优势

  • 兼容性:既能担任移动端主力开发框架,还可以在 PC 端支持主流浏览器,最低兼容 IE8。

  • 页面效果:与其它开发框架(如 Angular )相比,更专注于 UI 层面的 react 页面渲染速度更快,加载时间更短。成熟的 UI 体系也较容易制作出良好的体验。

  • 开发成本: apollo 集成开发框架带来更低开发维护成本,成熟的规范和完善的文档能为项目组省时省力,众多的⼯具满足开发过程中常见的需求。上手即用,方便又快捷。

  • 可复⽤用性:成熟的组件化结构,可复用性强。项目之间的组件可以共享互通。

  • 扩展性:活跃的开源社区,琳琅满目的项目,取之即用,适应能力强,版本升级成本低。

核心概念

数据流向

数据的改变一般是用户交互行为或者浏览器行为(如路由跳转等)触发的,在这类事件发生时会通过 dispatch 发起一个 Action,如果是同步行为会直接通过 Reducer 改变 State ,如果是异步行为会先触发 Thunk 然后流向 Reducer 最终改变 State,因此在 apollo 中,数据流向将严格遵循单向数据流的理念,从而保证项目的可维护性。

data_flow

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 传到 StoreAction 必须带有 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-routerredux 的结合,实现了数据流的全方位整合,为 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 里需要放什么,有哪些影响 statereducer,有哪些负责发起 reducerthunk

这里的 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 方法。目前已有很多基于 AJAXPromise 实现,例如 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 接收的参数中 asynctrue,代表我们要调用的这个东⻄是 Thunk,如果不用 async 的话则代表我们希望直接调用 reducer

另一个方法则是 mapStateToProps,顾名思义就是将 apollo 管理的整个 state 中相关的部分映射到这个组件的 props 中来。注意此处接收的参数是整个 appstate,我们写成 {todo} 意味着我们只接受 state.todo,然后我们将 state.todo 中的东⻄给返回去。

最重要的一部分,也就是开头引入的 connect 方法。 export 这个组件的时候我们将 mapStateToPropsaction 都与 TodoList 进行连接,会将它们都注入组件的 props。此时这个组件 render 时我们就可以使用 props.list (从 model 中映射过来的),并且以两个 button 来发起 props.addTodoprops.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,里面包含了 putselect 方法。其中 put 等同于 dispatchselect 用于选择当前 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)
Copyright © 民生科技有限公司 2019 all right reserved,powered by Gitbook联系方式: wanglihang@mskj.com
修订时间: 2019-08-02 16:19:32

results matching ""

    No results matching ""