Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[讨论] API 设计 #7

Closed
sorrycc opened this issue Jul 9, 2016 · 8 comments
Closed

[讨论] API 设计 #7

sorrycc opened this issue Jul 9, 2016 · 8 comments

Comments

@sorrycc
Copy link
Member

sorrycc commented Jul 9, 2016

重新思考了下 api 的设计和功能,做了一些调整。欢迎讨论。

API

app = dva(opts?)

  • opts.onError: 默认 throw 出错信息,在 effectsubscription 出错时被调用。

app.model(obj)

Takes the following arguments:

  • namespace: model state 在全局 state 上所在的 key 值
  • state: model 的初始值
  • reducers: 同步操作,用于更新数据,由 action 触发
  • effects: 异步操作,不直接更新数据,由 action 触发,可以 dispatch action
  • subscriptions: 异步只读操作,不直接更新数据,可以 dispatch action

一个完整的例子:

app.model({
  namespace: 'count',
  state: 0,
  reducers: {
    ['count/add'  ](count) { return count + 1 },
    ['count/minus'](count) { return count - 1 },
  },
  effects: {
    ['count/add']: function*(action) {
      yield call(delay, 1000);
      yield put({
        type: 'count/minus',
      });
    },
  },
  subscriptions: [
    function(send) {
      key('⌘+up'  , () => { dispatch({type: 'count/add'}); });
      key('⌘+down', () => { dispatch({type: 'count/minus'}); });
    },
  ],
});

namespace 是 model 在全局 state 上所占的 key,格式为字符串。虽然叫 namespace,但像 a.b 并不会被扩展为 globalState.a.b

state 是 plain object 。

reducers 对应 redux 的 reducer,格式为 object/array。默认是 object,如果要支持 reducer enhancer,需要写成 array,第二个参数为 reducer enhancer 。例:

app.model({
  reducers: [{}, reducerEnhander],
});

effects 对应 redux-saga。他有类型,takeEverytakeLatest。默认是 takeEvery,如果想要 takeLatest,可以这样:

app.model({
  effects: {
    ['count/add']: [function*(){}, { type: 'takeLatest' }],
  },
});

上面 reducerseffects 的扩展配置的方式都是通过数组第二项的方式进行配置,参考自 babel

subscriptions 用于订阅数据源,格式为数组,他们会在 domready 时被执行。详见:subscription 及其适用场景

app.router(({ history }) => [routes])

使用和 ReactRouter 相同的配置,不做封装,可用 jsx 格式,也可用 javascript object 的格式支持动态路由。

详见:react-router/docs

app.start(HTMLElement, opts?)

Opts can contain the following values:

  • opts.history: default: hashHistory
  • opts.middlewares: default: []
  • opts.reducers: default: {}
  • opts.hmr: default: null

opts.history 是给路由用的 history,支持 hashHistory 和 browserHistory 。默认是 hashHistory,如果要换成 browserHistory,可以这样:

import { browserHistory } from 'dva/router';
...
app.start(container, {
  history: browserHistory,
});

opts.middlewares 是支持 redux 的 middleware,数组类型。

opts.reducers 是提供额外的 reducers,用于支持类似 redux-form 这种插件。

opts.hmr 作为用户通常不需要关心。顾名思义是拿来做 Hot Module Replacement 用的,格式为 (render, replaceReducer, restartSaga) => {},传入用于刷新 component, reducer, saga 的方法,给 HMR 用。(目前只支持 render)

HMR 的例子详见 user dashboard,代码有点多,之后会通过 babel 插件的方式让使用无感知。

尚未考虑的点

  • 插件机制
  • 动态加载
    • 这里需要考虑 reducer 和 saga 的重载,以及 subscription 的 ubsubscribe
  • dva/effects 的调用方式简化
  • connect 的封装 (和 reselect 结合,默认高性能)
  • fetch 的封装
  • ...
@yesmeck
Copy link
Contributor

yesmeck commented Jul 11, 2016

这样的 API 测试要怎么写?比如要给 reducer 写测试。

想到怎么写了。。。

@yesmeck
Copy link
Contributor

yesmeck commented Jul 12, 2016

还有 saga 实际应用可能不只会用到 takeEvery, takeLatest 两种模式,比如我们有个 saga 用到了 race 就没办法用这两种模式了:

function* watchStartPolling() {
  let lastTask
  while (true) {
    const action = yield race({
      start: take(startPolling.getType()),
      stop: take(stopPolling.getType()),
    })
    if (lastTask) {
      yield cancel(lastTask)
    }
    if (action.start) {
      lastTask = yield fork(polling)
    }
  }
}

@sorrycc
Copy link
Member Author

sorrycc commented Jul 12, 2016

@yesmeck saga 新增一个类型,比如 watcher,用于这种 watch 级的 saga,你觉得如何?

@yesmeck
Copy link
Contributor

yesmeck commented Jul 12, 2016

其实我是不大喜欢这种配置式的 API,有没有考虑过把 reducer 和 saga 设计成按方法传入(之前提过 reducer 的,subscriptions 不熟暂时不说 )?

const reducers = (state, action) {
  // blah blah
}

const effects = function* () {
  // blah blah
}

app.model({
  namespace: 'count',
  state: 0,
  reducers,
  effects,
});

这样实现起来应该也简单点,而且更灵活,坏处可能就是太灵活。。。

@sorrycc
Copy link
Member Author

sorrycc commented Jul 12, 2016

恩,太灵活了。。

我希望加一些简单的约定,比如 reducers 和 effects 的 key 是 action 。一方面可以简化部分书写,另外我们之后还会探索下图形化编程,需要做一些静态化的语法分析。这也是为啥没有直接支持 app.model({ reducers }); 的原因。写法太灵活,就不容易分析出相关的 action 。

另外,以我目前所了解的信息是,不加配置的默认使用可以满足 80% - 90% 的需求。saga 的 race,以及 reducer enhancer 都属于高级需求,至少在我们这边的项目都还没有用到过。

@sorrycc sorrycc mentioned this issue Jul 12, 2016
8 tasks
@nikogu
Copy link
Member

nikogu commented Jul 13, 2016

是否可以考虑将 start和render分开,https://github.com/sorrycc/dva/blob/master/src/index.js#L97,
或者做一个兼容,比如如果不传入container,则返回的是Provider

因为考虑到一些应用框架的使用,比如chair,就是直接内置的render方式,大概为:

  yield this.render('App.jsx', {
    runAtServer: false,
    context: {
      user: {
        name: this.user.cname,
      },
    },
  });

这个时候,可能App.jsx返回的是

<Provider store={store}>
    <Routes history={history} />
</Provider>

但是

ReactDOM.render((
), container);

可能是交给框架执行

@sorrycc
Copy link
Member Author

sorrycc commented Jul 13, 2016

@nikogu 在这个 pr 里已经做了,但忘记写了。。 ssr 是其中一点考虑,还有有个原因是更方便写 testcase 。

@yangGuangXiaDeHaiFeng
Copy link

麻烦问下,在onError 报错中,怎么获取到当前报错代码属于哪个model(namespace)中呢?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants