前言
业务场景: 根据后端返回的json结构和用户实际操作,动态渲染出问卷中的某些题目或某些选项。具体介绍可以看我初入掘金写下的一篇文章# 你曾经做过“动态的表单”么?
emmm... 我等你十分钟看完那篇文章回来
...
好的,相信你已经看完了,并且对我说的动态表单有一定的了解,那么请顺着这我的思路想一个问题:“如果有5个项目需要接入这个动态表单,那怎么办?”
初级螺丝工:“复制5份,分别放到5个项目里面就行了呗,省时省力,虽然麻烦,但是接入的成本很小。”
高级螺丝工:“小初,那你改动的话要改5次,而且你能保证5个同步更改不出错么?应该做成模块,放在npm上,直接引入,直接使用,再多的项目接入都不怕了。”
资深螺丝工:”老高,没有那么简单,如果表单内有需要请求接口的组件怎么办,请求接口需要网关怎么办,如何区分npm包环境?什么?配devproxy代理?起在node端的怎么动态配置,配置一堆,优雅么?放在npm是好,但是如何解决npm内部根据业务环境请求接口问题?”\
吧啦吧啦一大堆...真的有这么困难么?相信但凡工作了一年以上的前端螺丝工都会想到npm包,但是内部如何设计确实比较麻烦,今天就带大家看看我长达两天的踩坑血泪史。
我们就以需要加载不同接口数据的级联组件举例:
- 直接把请求放npm包里面
// 因为受限于项目历史因素,故必须通过网关进行请求
// 那就意味着要把这个网关接入到npm包里,实属没有必要,并且网关的登录信息无法获取。
// pass
- 不通过网关,直接请求接口服务
// 安全性差,就这一条就通不过。
// 如果不考虑安全性,那直接devproxy+ nginx,好,很好,接口的数据拿到了,一切看似都很美好。
// 如何区分环境?测试,准生产,生产环境..
// stop,pass
- 单独做一个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包设计成业务模块,还是只是一个切合这个数据结构的通用组件。各有各的优势。
终于还是没有妥协,完成了这个通用模块。
快别说了,让我去摸一会~