vue3组合式 API配合JSX实现高阶组件使用踩坑记录

3,148 阅读2分钟

缘由

最近项目处于自我驱动状态,那么些东西就尽可能的往规范,长远,学习的角度去写。

项目使用element-plus,vue3

一、高阶组件和高阶函数

1、高阶函数的特点

把一个函数当另一个函数的参数

传参固定,结果就是固定的

说白了就是传入一个函数,返回一个函数。包装的函数可以进行修饰,但是所有参数都传递给传入的函数。我的理解是装饰器语法,和回调函数的深度使用。

2、高阶组件

高阶组件和高阶函数是同样类似的原理,react是最早提出,vue社区用的很少。但是从传入一个组件,返回一个组件的作用来说,我们也许曾经写过。

二、vue高阶组件我们需要解决的最大问题

其实就是参数透传的问题,我要得到所有的参数,并且是不需要经过props定义的,所有组件上的参数,然后全部丢给封装的组件。然后我们再把组件返回出去,进行使用。

vue2语法下,我们有一个$attres可以得到所有参数,同理,vue3也有这个参数,基于这方面的高阶函数,已经有比较多的文章实现过了

然后vue3的组合式api方式,就很有意思了。

官方文档下说明,steup可以返回一个对象,和一个函数

返回对象则将数据融入上下文,函数的话是一个jsx语法。相关地址

接下来jsx语法是我们需要使用的方式。

三、先丢出来注意事项

1、vue3不需要引入新的babel,直接就支持jsx语法使用

2、JSX无法和scoped配合使用,大家心里要有数

3、iscustomelement参数是没有用的,想要在templete里面无警告则需要在vue.config.js中增加

chainWebpack: config => {
    // 参考https://github.com/vuejs/vue-cli/issues/5610
    config.module
      .rule('vue')
      .use('vue-loader')
      .loader(require.resolve('vue-loader-v16'))
      .tap(options => {
        options.compilerOptions = {
          ...(options.compilerOptions || {}),
          isCustomElement: tag => /^jsx-custom/.test(tag), // 定义全局解析,用于jsx
        }
        return options
      })
  },

4、如果在使用jsx下不应该使用v-slot应该用v-solts

参考文档:github.com/vuejs/jsx-n…

5、v-solts没有生效的原因,应该是你的组件不是一个未定义的组件,也是上面iscustomelement参数配合解决的原因。

你不应该放在一个类似div,span的有效组件上面,要放在类似的

<a  v-slot/>

这里a组件未定义过

6、虽然基本实现了高阶组件,但是我们配合ref使用的时候发现父组件无法操作到子组件的函数,原因是子组件只管渲染去了,不管对象的处理。那么我们需要给当前高阶组件绑定可供外部操作的函数

const self = getCurrentInstance()

// 为当前组件对象绑定外部可操作的函数 

 self.proxy.handColse = onColse

7、jsx下面的具名插槽使用,无奈没有找到官方文档,试了很久也不行。最后采用h渲染函数方式

{
            // 因为是插槽所以直接用渲染函数影响不大,并且v-slots方法不知道怎么用具名插槽
            [
              h('default', ctx.slots.default()),
              h('footer', ctx.slots.footer())
            ]
          }

8、2020年12月17日更新,关于jsx也可以使用scoped css的方式

来源:github.com/vuejs/jsx-n…

使用demo:

<script lang="tsx">
import { defineComponent, withScopeId, getCurrentInstance } from 'vue'

export default defineComponent({
  setup(props, ctx) {
    const instance = getCurrentInstance()
    const scopeId = instance.type.__scopeId
    const withId = withScopeId(scopeId)

    return withId(() => <div class="ceshi">fdsafas</div>)
  },
})
</script>
<style scoped lang="scss">
.ceshi {
  width: 100px;
  height: 100px;
  background: red;
}
</style>

四、代码呈现

这里是对el-drawer组件进行了高阶组件封装

<script lang="jsx">
/*
jsx使用手册
https://github.com/vuejs/jsx-next/blob/dev/packages/babel-plugin-jsx/README-zh_CN.md
*/
import { ref, getCurrentInstance, h } from 'vue'
export default {
  name: 'CopyDrawer',
  emits: [],
  setup(props, ctx) {
    const self = getCurrentInstance()

    let doneF = null

    const root = ref('root')

    function handleClose(done) {
      doneF = done
    }

    function onColse() {
      root.value.handleClose() // 调用内部关闭方法给handleClose参数传递done事件
      doneF()
    }

    // 为当前组件对象绑定外部可操作的函数
    self.proxy.handColse = onColse

    const slots = {
      default: () => <div>A</div>,
      foo: () => <span>B</span>
    };

    return () => (
      <el-drawer ref={root} {...ctx.attrs} withHeader={false} before-close={handleClose}>
        <div className="copy-el-drawer">
          {
            // 自定义顶部标题
          }
          <div className="title">
            <span>{ctx.attrs.title}</span>
            <i className="icon el-icon-close" onClick={onColse} />
          </div>
          {
            // 这里会报错,不用管,等vue官方修复isCustomElement问题
            // <jsx-custom v-slots="ctx.slots" />
          }
          {
            // 因为是插槽所以直接用渲染函数影响不大,并且v-slots方法不知道怎么用具名插槽
            [
              h('default', ctx.slots.default()),
              h('footer', ctx.slots.footer())
            ]
          }
        </div>
      </el-drawer>
    )
  },
}
</script>

<style lang="scss">
.copy-el-drawer {
  width: 100%;
  height: 100%;
  position: relative;
  .title {
    width: 100%;
    font-size: $h4-16;
    height: 40px;
    display: flex;
    padding: 20px;
    align-items: center;
    color: $info;
    justify-content: space-between;
  }
  .icon {
    font-size: $h2-24;
    &:hover {
      color: $font-color;
    }
  }
}
</style>

使用

<copy-drawer
    :close-on-press-escape="false"
    size="60%"
    title="新增话术"
    v-model="drawer"
    direction="rtl"
    @close="onClose"
    destroy-on-close
    ref="addFrom"
  >
    <div class="insert-drawer">
      <el-button @click="ceshi">测试</el-button>
      <el-form :model="formData" :rules="rules"></el-form>
    </div>
    <template #footer>
      <span>524551</span>
    </template>
  </copy-drawer>

五、关于element-plus的bug

上次发了一篇element的bug记录。这次不多,官方修复很快。这里我使用了抽屉组件,所以又发现两个。

1、modal参数不能设置为false,否则就无法使用了,已经提交issues

2、官方文档中的closeDrawer参数通过ref等方式是获取不到的,已经提交issues

个人处理方式是操作handleClose,具体看官方的更新情况

代码

const root = ref('root')
root.value.handleClose() // 调用内部关闭方法给handleClose参数传递done事件

六、踩坑基本完毕

今天从想要实现这个功能到现在,一个业务代码也没有写。但也有所收获。而且基于此实现了高阶组件之后,再用templete方式就很简单了。

还有这里的jsx使用方式和ant-desgin还有element-plus不太一样。我是纯组合式api,但是他们用了render。各有千秋。

完毕!