这或许是Vue2.X代码复用的终极解决方案✨

前言

不知道大家在使用Vue2.X进行编程的时候,有没有使用Mixin选项进行代码复用的习惯呢?只需要把组件公共逻辑提取到mixin文件中,在不同的Vue实例中去进行混入,便可以减少相当可观的代码量,整体代码也会看起来更加简洁,真是泰裤辣😁

这或许是Vue2.X代码复用的终极解决方案✨

但是Mixin的可维护性呢?事实上,Mixin选项至少有三个主要的短板,在Vue3的官方文档中如是写道:

  1. 不清晰的数据来源:当使用了多个 mixin 时,实例上的数据属性来自哪个 mixin 变得不清晰,这使追溯实现和理解组件行为变得困难。这也是我们推荐在组合式函数中使用 ref + 解构模式的理由:让属性的来源在消费组件时一目了然。
  2. 命名空间冲突:多个来自不同作者的 mixin 可能会注册相同的属性名,造成命名冲突。若使用组合式函数,你可以通过在解构变量时对变量进行重命名来避免相同的键名。
  3. 隐式的跨 mixin 交流:多个 mixin 需要依赖共享的属性名来进行相互作用,这使得它们隐性地耦合在一起。而一个组合式函数的返回值可以作为另一个组合式函数的参数被传入,像普通函数那样。

想象一下,你接手其他人的项目代码,结果发现单个vue文件引入多个mixin文件,并且发现多个变量vue文件的data函数中并没有定义,或者多个方法没有在methods中定义,就问你慌不慌🤣

这或许是Vue2.X代码复用的终极解决方案✨

是不是瞬间就感觉没有那么香了,那么Vue有其他的代码复用的方法吗?

有!那就是HOC

关于HOC

这时肯定就会有人说了,HOC不是React的吗?确实,HOC确实是React的一种设计模式,在React的官方文档可以了解到:

高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。具体而言,高阶组件是参数为组件,返回值为新组件的函数。

但是我想说的是,编程思想其实即使在不同的框架体系下也是有可能互通的,就比如React的Hook不就是Vue的Composition API的主要灵感来源吗?

这或许是Vue2.X代码复用的终极解决方案✨

其实HOC高阶组件可以简单理解为,在业务组件外层再套上一层,而我们可以把一些可复用的代码逻辑交给它去处理,就像这样:

<公共组件>
    <业务组件></业务组件>
</公共组件>

现在我直接拿我自己的项目代码来进行组件逻辑复用,尝尝这个所谓的HOC到底香不香?

项目背景

现在有一个数据看板,有多个模块,每个模块对应一个组件,一个组件有多个状态(加载中状态、加载失败状态、没有权限状态),每个状态又有不同的样式表现。

大概了解一下,我们先看看页面呈现效果和组件代码:

这或许是Vue2.X代码复用的终极解决方案✨

<template>
    <div id="box">
        <h1>某某模块1</h1>
        <div v-if="hasAuth" v-loading="loading">
            <h2 v-if="retry">加载失败,请点击重试!</h2>
            <h2 v-else>模块内容:{{ info.name }}---{{ info.age }}</h2>
        </div>
        <h2 v-else>没有权限</h2>
    </div>
</template>
<script>
const INFO_DEFAULT = {
    name: '',
    age: 0
}
export default {
    name: 'box1',
    props: {
        searchParams: {
            type: Object,
            default: () => {
                return {
                    countrys: [],
                    seller: []
                }
            },
            required: true
        }
    },
    data() {
        return {
            loading: true,
            retry: true, // 加载失败时可以重试,为true
            info: INFO_DEFAULT
        }
    },
    computed: {
        // 判断是否有模块权限
        hasAuth() {
            return this.$store.state.auth['XXX']
        }
    },
    watch: {
        // 监听父组件的筛选条件,触发接口获取数据
        searchParams: {
            handler:'getData',
            deep: true,
            immediate: true
        }
    },
    methods: {
        // 模拟接口请求
        async requestGet(url, params) {
            return new Promise((resolve, reject) => {
                setTimeout(() => {
                    // 请求失败
                    // reject({
                    //     code: 0,
                    //     data: null
                    // })
                    // 请求成功
                    resolve({
                        code: 1,
                        data: {
                            name: 'hzy',
                            age: 18
                        }
                    })
                }, 1500)
            })
        },
        async getData() {
            try {
                if (!this.hasAuth) return
                const { countrys, seller } = this.searchParams
                const url = ''
                const params = {
                    countrys,
                    seller
                }
                this.loading = true
                this.retry = false
                const { code, data } = await this.requestGet(url, params)
                if (code) {
                    this.info = data ?? INFO_DEFAULT
                    this.retry = false
                } else {
                    this.retry = true
                }
            } catch (error) {
                this.retry = true
            } finally {
                this.loading = false
            }

        }
    },

}
</script>

可以看到,我们的子组件除了要处理本身的业务逻辑外,还需要手动监听父组件的数据变化去请求接口,并根据接口情况维护好加载状态,同时还要判断模块的权限状态

要知道一般数据看板可不止4个子模块,只会更多,而这种实现方式将会造成组件逻辑大量重复,导致子组件没有办法专注于自身业务逻辑的实现,这对于一个合格的前端开发来说,肯定是不能忍的(手动狗头)

这或许是Vue2.X代码复用的终极解决方案✨

初步实现HOC

我们先将上面的项目进行简化,先不考虑实现其他状态,只让子组件专注于内部业务逻辑的实现,将监听父组件数据变化并请求接口的代码交给公共组件去处理

// hoc.js
const INFO_DEFAULT = {
    name: '',
    age: 0
}
export function createHoc(sub) {
    return {
        data() {
            return {
                result: INFO_DEFAULT
            }
        },
        methods: {
            // 模拟接口请求
            async requestGet(url, params) {
                return new Promise((resolve, reject) => {
                    setTimeout(() => {
                        // 请求成功
                        resolve({
                            code: 1,
                            data: {
                                name: 'hzy',
                                age: 18
                            }
                        })
                    }, 1500)
                })
            },
            async getData() {
                try {
                    const { url, ...params } = this.$refs.sub.requestParams
                    const { code, data } = await this.requestGet(url, params)
                    if (code) {
                        this.result = data ?? INFO_DEFAULT
                    } 
                } catch (error) {
                    console.log(error)
                } 
    
            }
        },
        mounted() {
            // 立刻发送请求,并且监听参数变化重新请求
            this.$refs.sub.$watch(
                "requestParams",
                this.getData.bind(this),
                {
                    immediate: true,
                    deep: true
                }
            )
        },
        render(h) {
            return h(sub, {
                props: {
                    result: this.result,
                    searchParams: this.$attrs.searchParams,
                },
                // 设置ref,方便父组件通过refs获取子组件实例
                ref:'sub'
            });
        }
    }
}
// sub.vue
<template>
    <div id="box">
        <h1>某某模块1</h1>
        <div>
            <h2>模块内容:{{ result.name }}---{{ result.age }}</h2>
        </div>
    </div>
</template>
<script>
export default {
    name: 'box1',
    props: {
        searchParams: {
            type: Object,
            default: () => {
                return {
                    countrys: [],
                    seller: []
                }
            },
            required: true
        },
        // 接口数据
        result:{
            type: Object,
            default: () => {
                return {
                    name:'',
                    age:0
                }
            }
        }
    },
    computed: {
        // 用来收集请求接口所需要的接口url和接口参数
        requestParams() {
            return {
                url: '',
                countrys: this.searchParams.countrys,
                seller: this.searchParams.seller
            }
        }
    },

}
</script>
// parentBox.vue
<template>
  <div id="app">
    <el-button @click="handleChange('countrys')">修改country国家参数</el-button>
    <el-button @click="handleChange('seller')">修改seller店铺参数</el-button>
    <HocBox1 :search-params="params" />
    <Box2 :search-params="params" />
    <Box3 :search-params="params" />
    <Box4 :search-params="params" />
  </div>
</template>

<script>
import Box1 from './components/box1.vue'
import Box2 from './components/box2.vue'
import Box3 from './components/box3.vue'
import Box4 from './components/box4.vue'
import { createHoc } from './components/hoc'


const HocBox1 = createHoc(Box1)
export default {
  name: 'parentBox',
  components: {
    HocBox1,
    Box2,
    Box3,
    Box4
  }
  ...
}
</script>

我们约定好在子组件新增一个requestParams计算属性,而父组件将会通过$refs去获取子组件的实例,并监听requestParams计算属性的变化从而请求接口,获取最新的接口数据,再将接口数据通过props传递给子组件。

这时我们可以观察到,子组件已经能够专注于本身的业务逻辑处理了。

功能完善

接下来我们需要把加载状态和权限控制也都提取到createHoc函数中去,并且对父组件传入的属性和事件进行透传,继续优化hoc.js文件:

注意:当模块无权限或者加载失败的时候,我们需要保留模块标题部分,而仅切换标题下面的内容部分,将内容切换为无权限或者加载失败,大家可以思考一下怎么实现,下面我将用slot来进行处理,如果有其他更好更优雅的解决方法欢迎评论区里留言交流!

至此,我们的代码就都完善了,别看代码少,麻雀虽小,五脏俱全!真香!

这或许是Vue2.X代码复用的终极解决方案✨

写在最后

其实本人对于Mixin一直都是很抗拒的,通过Mixin来减少代码量对于我而言无非掩耳盗铃,不仅该少的代码没有少,还多了其他很多可预见的麻烦。

这篇分享代码量没有很多,更多是通过一个简单的项目案例,来和大家探讨VueHoc的可行性。如果大家有其他可行的优化方案,也欢迎评论区交流学习。最后,代码优化道阻且长,希望我们能一直在路上!

这或许是Vue2.X代码复用的终极解决方案✨

原文链接:https://juejin.cn/post/7230419964301262909 作者:前端不死

(0)
上一篇 2023年5月8日 上午11:07
下一篇 2023年5月9日 上午10:00

相关推荐

发表评论

登录后才能评论