vue2--$emit回调踩坑

825 阅读3分钟

前言

业务场景: 根据后端返回的json结构和用户实际操作,动态渲染出问卷中的某些题目或某些选项。具体介绍可以看我初入掘金写下的一篇文章# 你曾经做过“动态的表单”么?
emmm... 我等你十分钟看完那篇文章回来
...
好的,相信你已经看完了,并且对我说的动态表单有一定的了解,那么请顺着这我的思路想一个问题:“如果有5个项目需要接入这个动态表单,那怎么办?”
初级螺丝工:“复制5份,分别放到5个项目里面就行了呗,省时省力,虽然麻烦,但是接入的成本很小。”
高级螺丝工:“小初,那你改动的话要改5次,而且你能保证5个同步更改不出错么?应该做成模块,放在npm上,直接引入,直接使用,再多的项目接入都不怕了。”
资深螺丝工:”老高,没有那么简单,如果表单内有需要请求接口的组件怎么办,请求接口需要网关怎么办,如何区分npm包环境?什么?配devproxy代理?起在node端的怎么动态配置,配置一堆,优雅么?放在npm是好,但是如何解决npm内部根据业务环境请求接口问题?”\

吧啦吧啦一大堆...真的有这么困难么?相信但凡工作了一年以上的前端螺丝工都会想到npm包,但是内部如何设计确实比较麻烦,今天就带大家看看我长达两天的踩坑血泪史。

我们就以需要加载不同接口数据的级联组件举例:

  1. 直接把请求放npm包里面
// 因为受限于项目历史因素,故必须通过网关进行请求
// 那就意味着要把这个网关接入到npm包里,实属没有必要,并且网关的登录信息无法获取。
// pass
  1. 不通过网关,直接请求接口服务
// 安全性差,就这一条就通不过。
// 如果不考虑安全性,那直接devproxy+ nginx,好,很好,接口的数据拿到了,一切看似都很美好。
// 如何区分环境?测试,准生产,生产环境.. 
// stop,pass
  1. 单独做一个api包专门接网关,提供给上层组件使用
// 请求和数据分开?很好的思想,但是还是绕不过环境问题。
// 并且受限于项目网关设计,登录信息只会保存在单例的网关实例里,即便初始化一个api网关也无法同步登陆信息。
// pass


emmm...看来不能把请求放在npm包里面,也不符合抽象和复用的最佳实践。

正解

通过vue父子传递事件的$emit api实现, 将请求接口的逻辑放在业务项目里面,npm包组件只负责数据的接收、组装和提交

方案1: $emit & watch

// in children.vue
const vue = new Vue()
vue.$emit('eventName', params)
vue.props={
    ...,
    list: Array
}
vue.watch = {
    list:{
        handler(v){
            ...
        },
        deep: true
    }
}

// in father.vue
<children @eventName="getLsit" :list="list"/>
...
getList(params) {
    const { list } =  network.post(url, params)
    this.list = list
}
级联问题经常会点击一级请求下一级,所有有个点击请求和数据拼装的过程,如果使用watch那么意味着每次请求完数据都会进行同样的数据处理逻辑,如果加入需要回显的逻辑就复杂了,其实就是是个需要递归但是做不到递归的问题,不相信的是大佬们可以自己使用$emit + watch 完成一个点击请求下一级,并且一进页面需要加载出来上次选中的各级。

方案2 通过$emit& callback 实现

// in children.vue
const vue = new Vue()
vue.$emit('eventName', params, (list)=>{
    vue.list =  list
})

// in father.vue
<children @eventName="getLsit"/>
...
getList(params,cb) {
    const { list } =  network.post(url, params)
    cb(list)
}
// 这样确实比方案1优雅了很多,但是级联是依赖上一级的选择结果加载下一级的,会出现嵌套$emit
vue.$emit('eventName', params, (list)=> {
    vue.list = list
    vue.$emit('eventName', params, (list)=> {
        vue.list[0].children = list
        vue.$emit('eventName', params, (list)=> {
            ...
        )}
    )}
})
这不就成了回调地狱了么?
既然方案2出现了回调地狱, 那我们promise怎么解决的回调地狱问题?binggo, async/await
原本我只是天真的想尝试 await vue.$emit(...),结果发现返回的根本不是请求结果,不讲武德。
于是我没有办法了,又回头开始思考是否可以把请求方在npm包里面...
时间过去了一天又一天...
仍然没有收获
一个点子突然从我脑子闪过,很快啊!
“await返回的是vue实例,那接口请求的方法不也是在vue实例上面么,hie~hie~”

方案3 通过 $emit & callback & $listeners

// in children.vue
const vue = new vue()
const vue_instance: typeof Vue = vue.$emit('eventName', params)
vue.list = await vue_instance.$listeners.getList(params)

// in father.vue
<children @eventName="getLsit" v-on="$listners"/>
getList(params) {
    return new Promise(resolve=> {
        const { list } =  network.post(url, params)
        resolve(list)
    })
}
// 如果父子中间还有多层,可以试一下inheritAttrs,这样不就完美的解决了么?oh ~ yeah ~ 

后话

终于解决了长达两天的难题。其实这个过程也有一些设计方式的变更,例如是把这个npm包设计成业务模块,还是只是一个切合这个数据结构的通用组件。各有各的优势。
终于还是没有妥协,完成了这个通用模块。
快别说了,让我去摸一会~