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

关于dva实际应用的一些经验以及疑惑 #886

Closed
laketea opened this issue May 5, 2017 · 32 comments
Closed

关于dva实际应用的一些经验以及疑惑 #886

laketea opened this issue May 5, 2017 · 32 comments

Comments

@laketea
Copy link

laketea commented May 5, 2017

从开始学习dva到引入到实际项目中也有几个月的时间,下面分享一下实际的经验,另外也有一些比较含糊或者疑惑的地方,看看大家有没有有些好的思路。
dva整体构架比较清晰,但是实际使用的时候,还是需要做很多处理。

dva model扩展

在我们的基础库中,实现了Model.extend方法,所有的model都通过这个方法来创建, extend具体对以下几个方面进行了扩展.

state

根据我们的业务特点(主要为管理系统),每个model我们都默认添加了loading, confirmLoading, spinner属性

subscriptions

subscriptions中对路由的监听 写法太过繁琐,特别是在包含参数的情况下。我们扩展了实现了listen方法

subscriptions: {
    setup({ dispatch, listen }) {
      
      //action为 redux action
      listen('/user/list', { type: 'fetchUsers'});

      //action为 回调函数
      listen('/user/:id/detail', ({ query, params }) => {
        const id = params[0];
        dispatch({
          type: 'fetchDetail',
          payload: { id }
        })
      });

      //支持对多个path的监听
      listen({
        '/user/list': ({ query, params }) => {},
        '/user/query': ({ query, params }) => {},
      });
  }

effects扩展

dva中处理loading状态非常繁琐,而dva提供的全局loading对我们业务并不适用,因为往往一个model中,有些call请求需要loading,有些call请求又不需要loading,所以我们对effects中的saga函数进行了扩展

  • callWithLoading 调用请求时,自动处理loading状态
  • callWithConfirmLoading 调用请求时,自动处理confirmLoading状态
  • callWithSpinning 调用请求时,自动处理spinning状态

如果发送ajax请求,需要处理相关的状态,就可以使用上面的方法,如果不需要就使用原始的call方法, 使用开发中可以灵活选择.
另外,在我们的业务中,call请求成功或者失败后,往往需要弹出对应的业务提示框(消息内容有时候不是由后端提供),故我们在上面的函数中,都提供额外的参数,支持请求结果的消息框处理。

//请求开始前会将loading = true,  请求结束后,将loading=false
const users = yeild callWithLoading(service.user.getList,user);
const users = yeild callWithLoading(service.user.getList,user,{successMsg:'加载用户成功',errorMsg:'加载用户失败'});

effects中经常需要需要更新state,但是往往又不想为每次修改都命名一个reducer函数,所以扩展了update方法,调用updateState reducer来更新state

//更新当前model的state
yield update({ users })

reducers扩展

为了配合effects中的扩展,我们默认实现了如下方法
showLoading,hideLoading... 等加载状态控制方法
updateState 直接更新state数据
resetState 重置state数据

request 扩展

fetch在实际项目中用使用,实在太过鸡肋,我们在fetch的基础上封装了Http类,实现了简单的中间件机制,方便各个项目使用,并且实现了一些常见的中间件(token中间件,统一错误弹框中间件等)

export default Http.create([dataTransformMiddleware, domainMiddleware, contentMiddleware, headerMiddleware]);

antd form扩展

antd form写法 个人觉得实在太过繁琐,一个带表单验证的输入框,往往需要FormItem & getFieldDecortor & Input 三层结构,而且混合着jsx以及js的语法.
为了简化form相关的代码,我们重新定义了field属性结构,并且提供了HForm 以及HFormItem组件。

HForm 主要应用与一些简单的form表单,不需要太多的交互
HFormItem 则是form中每一项输入框,用于复杂布局/交互的form中

const fields = [
  {
    key: 'name',
    name: '名称',
    required: true,
  }, {
    key: 'gender',
    name: '性别',
    enums: {
      MALE: '男',
      FAMALE: '女'
    }
  }, {
    key: 'birthday',
    name: '生日',
    type: 'date'
  }
];

function HFormBase({ form }) {
  const formProps = {
    fields
    item: {},
    form
  };
  const onSubmit = () => {
    validate(form, fields)((values) => {
      results = values;
    });
  };

  return (
    <div>
      <HForm {...formProps} />
      <Button onClick={onSubmit} type="primary" >提交</Button>
    </div>
  );
}
const fields = [
  {
    key: 'name',
    name: '名称',
    required: true
  }, {
    key: 'age',
    name: '年龄'
  }, {
    key: 'birthday',
    name: '生日',
    type: 'date'
  }, {
    key: 'desc',
    name: '自我介绍',
    type: 'textarea'
  }
];


function HFormItemBase({ form }) {
  const layout = {
    labelCol: { span: 6 },
    wrapperCol: { span: 18 }
  };
  const itemProps = { form, item: {}, ...layout };
  const rules = {
    number: [{ pattern: /^\d+$/, message: '请输入数字' }]
  };
  const submit = () => {
    validate(form, fields)((values) => {
      console.log(values);
    });
  };
  const fieldMap = getFields(fields).toMapValues();

  return (
    <Form >
      <Row>
        <Col span="12" ><HFormItem {...itemProps} field={fieldMap.name} inputProps={{ disabled: true }} placeholder="指定placeholder" /></Col>
      </Row>
      <Row>
        <Col span="12" ><HFormItem {...itemProps} field={fieldMap.age} rules={rules.number} onChange={onChange} /></Col>
        <Col span="12" ><HFormItem {...itemProps} field={fieldMap.birthday} /></Col>
      </Row>
      <Row>
        <Col span="12" ><HFormItem {...itemProps} field={fieldMap.desc} /></Col>
      </Row>
      <Row>
        <Col span="12"><Button type="primary" onClick={submit} >提交</Button></Col>
      </Row>
    </Form>
  );
}

脚手架

参考dva-cli,我们实现了一个自己的脚手架,支持更丰富类型的模版(curd页面等常见页面模版)

@xufei
Copy link
Contributor

xufei commented May 5, 2017

loading有这个啊:https://github.com/dvajs/dva-loading

不是只能在app级别的,还可以在model和effect级别

@laketea
Copy link
Author

laketea commented May 5, 2017

额,才注意到 loading还有个effects参数, effect级别的loading确实更强大。不过,callWithloading, callWithConfirmLoading, callWithSpinner基本也能满足我们的需求,就是命名太长了..

@nihgwu
Copy link
Member

nihgwu commented May 5, 2017

有demo分享吗

@zuiidea
Copy link

zuiidea commented May 5, 2017

@laketea 厉害了

@zuiidea
Copy link

zuiidea commented May 5, 2017

@xufei 其实很多时候不实用,就算在model中,很可能某个页面需要多个loading,而此时的loading只是当前model的show和hide,比如页面中同时有table的loading和弹层中确认按钮的loading,@laketea 的做法是比较科学的

@xufei
Copy link
Contributor

xufei commented May 8, 2017

@zuiidea 不是啊,loading可以细化在某个effect上面,然后你业务上真实的loading还可以是几个loading的组合:

loading.modelA.effectA || loading.modelB.effectB

@xufei
Copy link
Contributor

xufei commented May 8, 2017

另外,直接修改数据的这个:

yield update({ users })

这是一个很纠结的事情,这里少一步put,可能在调试的时候devtools里面损失一些便利,这个要权衡一下。不过,鉴于每个effect都是由dispatch的action转换而来,在这些effect里面去update,流程上是没有问题的。

@zuiidea
Copy link

zuiidea commented May 8, 2017

@xufei loading.effect确实能解决一些问题👍

@grunmin
Copy link

grunmin commented May 8, 2017

request, 我是用axios替代官方的, 当后端返回一个代表失败code时, throw一个异常, 然后通过onError来捕获, dispatch一个弹出消息的方法, 这样在effects里面不用做任何判断

@laketea
Copy link
Author

laketea commented May 8, 2017

通过onError来捕获request错误,无法做更精确的控制吧,Error对象无法携带更复杂的数据.
我们是通过中间件来处理,每个项目可以灵活配置

@laketea
Copy link
Author

laketea commented May 8, 2017

@xufei update 本质也是通过put 一个 通用的updateState action来实现的,在devtools里面应该是一样的吧。之所以扩展一个update方法,主要是为了书写便利一点,因为几乎每个effect都要更新model数据(很多情况下只是简单的覆盖数据,并不包含逻辑)。

yield update({ users })
// 等同于
yield put({ type: 'updateState', { users } })

@grunmin
Copy link

grunmin commented May 8, 2017

@laketea 九成以上的请求都是类似的, Error能携带后端返回的错误描述具体内容已经足够了. 需要特殊处理可以在effect自己catch

@sbyps
Copy link

sbyps commented May 10, 2017

@laketea 能开源你们的脚手架么,想看看model.extend怎么写的

@zjxpcyc
Copy link

zjxpcyc commented May 12, 2017

比较喜欢 subscriptions 跟 form 的改动,赞一个
@grunmin 我们原来也是这样处理的,在 onError 里面统一处理,后来放弃了。我们遇到的一个问题,有的错误是需要弹出的,有的错误只是显示在当前控件中的,有的又是 log 级别即可,放到一个里面去处理,搞不定。

@zjxpcyc
Copy link

zjxpcyc commented May 12, 2017

@sbyps model 的本质是 JSON对象,model.extend 能实现扩展合并即可。我印象里面 dva 官方是可以model 继承的

@sbyps
Copy link

sbyps commented May 12, 2017

@zjxpcyc 能开源分享一下么,现在我也在封装model和Form呢;
或者私聊,看一下怎么做的?

@zjxpcyc
Copy link

zjxpcyc commented May 12, 2017

@sbyps 我没做封装, @laketea 他们的封装就很棒。
我觉得,我们没有必要重复做轮子,用 dva 官方的就可以解决我们的问题,暂时没有需要封装的需求。
model 的封装,dva 就有框架,不建议你自己搞了 https://github.com/dvajs/dva-model-extend

@sbyps
Copy link

sbyps commented May 12, 2017

@zjxpcyc 谢谢,之前没看到这个方法;

@laketea
Copy link
Author

laketea commented May 12, 2017

@sbyps 这两天我整理一个demo出来吧。。

@ufohjl
Copy link

ufohjl commented May 24, 2017

挺好的! 特别是listen这一块,之前写正则表达式那叫一个

@zuiidea
Copy link

zuiidea commented May 24, 2017

12天过去了。。。

@linyongping
Copy link

linyongping commented Jun 9, 2017

@laketea 表单的封装是真的不错, 能把封装的方法共享一下更好

@laketea
Copy link
Author

laketea commented Jun 16, 2017

@linyongping @zuiidea @sbyps 最近一阵太忙了... 已提供demo,代码还没怎么整理,传送门: admin-demo

@zuiidea
Copy link

zuiidea commented Jun 17, 2017

@laketea 很多值得借鉴和学习的,赞

@nihgwu
Copy link
Member

nihgwu commented Jun 18, 2017

其实利用 dva 提供的机制也很容易实现类似的扩展

function prefixType(type, model) {
  const prefixedType = `${model.namespace}/${type}`
  if (
    (model.reducers && model.reducers[prefixedType]) ||
    (model.effects && model.effects[prefixedType])
  ) {
    return prefixedType
  }
  return type
}

function createEffects(sagaEffects, model) {
  function put(type, payload) {
    let action = type
    if (typeof action === 'string') {
      action = {
        type: action,
        payload,
      }
    }
    return sagaEffects.put({ ...action, type: prefixType(action.type, model) })
  }
  function update(payload) {
    return put('updateState', payload)
  }
  return { ...sagaEffects, put, update }
}

const dvaEnhancer = {
  onEffect: (effect, sagaEffects, model) =>
    function* effectEnhancer(action) {
      yield effect(action, createEffects(sagaEffects, model))
    },
}

app.use(dvaEnhancer)

@nihgwu
Copy link
Member

nihgwu commented Jun 18, 2017

这里面用到了一个trick就是自己提供effect的第二个参数,来取代dva默认提供的sagaEffects,默认的sagaEffects会成为第三个参数,这样就有了冗余的代码,@sorrycc 后面是不是可以考虑优化一下?

@AsceticBoy
Copy link

AsceticBoy commented Jul 26, 2017

想问下大家都是这么做modal的划分的,之前用dva的时候,只是很草率的一个业务模块一个modal,但是后来发现这样很多时候字段有些冗余,而且当业务模块比较复杂的时候,一旦connect没用好会造成一些很多的渲染,后来看了些别人的专栏,我感觉dva的modal应该是业务数据模型,但是模型的边界又让我很模糊,很多情况下不知道怎么分,还有模型到实体的映射在哪里做?还有现在项目有一些渲染上的问题,可能是我使用不当导致,我想引入Immuable去避免无效渲染,该搁在dva的哪一块,希望有类似经验的小伙伴能聊下,谢谢了!也希望叔叔和CC前辈能指点一二 @xufei @sorrycc

@yangbin1994
Copy link
Contributor

yangbin1994 commented Jul 26, 2017

@sbyps dva-model-extend
项目中,model的功能继承扩展才需要用到,~~~我个人不建议简单组合model~~~
隐隐觉得,dva2.0重点照顾对象就是model

@AsceticBoy model的划分,我个人认为一类是面向资源,一类是面向视图层;model独立出来,一是为了方便维护,二是为了方便共享,异步数据管理和跨组件共享的数据都可以放在model里;关于Immuable,我个人之前将state全部变成Immuable类型,可以参考我之前的思路dva-atr,先不说这种将model的操作逻辑放到ui事件回调里的好坏(我现在觉得非常错误。。复用性差),而且immutable类型的state无法使用一些react社区的一些可视化工具,包括dva-gui,随着项目的增大,就这点还是很致命的。

@xufei
Copy link
Contributor

xufei commented Jul 26, 2017

@AsceticBoy 放connect里面做么?

@AsceticBoy
Copy link

可以在connect里面做,我原先设想也是在connect做,但是后来我就发现我对modal中的state模型应该是什么样的开始有些混乱不清了,因为你可以在state中保持原始数据的形态(比如后台返回的数据),然后在connect做映射,也可以直接就是视图需要的数据形态,然后connect一下,随即导致了我不知道modal应该划多大的问题

@sorrycc sorrycc closed this as completed Sep 2, 2017
@ouzhou
Copy link

ouzhou commented Sep 6, 2017

在subscriptions判断路由感觉非常不方便,比如写的router是 /user/:id 这样的,我只能拿到一个pathname,而且需要自己写正则去判断是否为想要的path,判断逻辑也很复杂(比如,如果你在routerRedux进行跳转但是没有写'/a' 只写 'a',最后获取到的这个pathname是没有 '/'的,并且,合法的路径也可以在最后多一个斜杠,这也需要判断),反正获取这个:id很麻烦,但是如果在组件componentDidMount后在props里拿到params就可以非常轻松的拿到:id,求个使用subscriptions的正确姿势

@aihua
Copy link

aihua commented Sep 6, 2017

这种form封装和extjs的封装类似?

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

No branches or pull requests