【vite】手把手搞一个ts组件包(1)

前言

造了个轮子,还没想好如何给大家用?看下去吧 ~

首先,我们假设已经费心打磨出了一个轮子 (比如一个 vue3 的通用组件),迫不及待地想用这个轮子为各位同仁『减负』。这个轮子将是什么形态的呢?

  • 可能需要是一个 npm 包 ,方便项目接入。
  • 可能需要有 typescript ,满足 ts 项目的需要,提升书写体验。
  • 可能需要脱离技术栈,比如能够给一个 react项目使用。
  • 可能需要是多个构建版本,方便 cjs 环境、script标签引入、es module。

然后就开始吧~

本文以 vue3+ts+less 的组件为例

0. 准备工作

我准备了一个用vue3实现的简单弹窗组件。

image.png

<template>
<teleport to="body">
  <div class="modal-mask" v-show="localVisible">
    <div class="modal">
      <div class="modal-header">
        <span>{{title}}</span>
        <button @click="localVisible = false">关闭</button>
      </div>
      <div class="modal-body">
        <slot></slot>
      </div>
    </div>
  </div>
</teleport>
</template>
<script lang="ts">
import { ref, defineComponent, watchEffect, watch } from 'vue'
export default defineComponent({
  name: 'Modal',
  props: {
    visible: {
      type: Boolean,
      required: false,
      default: false
    },
    title: {
      type: String,
      required: false,
      default: ''
    },
  },
  emits: ['update:visible'],
  setup(props, {emit}) {
    const localVisible = ref(false);
    watchEffect(() => {
      localVisible.value = props.visible;
    });
    watch(localVisible, value => {
      emit('update:visible', value);
    });
    return {localVisible};
  }
});
</script>

<style lang="less">
.modal-mask {
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  background-color: rgba(0,0,0,.45);
  .modal {
    width: 520px;
    margin: 20% auto;
    background-color: #fff;
    position: relative;
    // modal主体的样式
  }
}
</style>
复制代码

组件 Modal 我设计了 titlevisible 两个prop,以及一个配置弹窗主体内容的插槽,其中 visible 是具有双向绑定的。

1. 一个入口文件

可能需要脱离技术栈,比如能够给一个 react项目使用.

设想一个react的项目使用这个 Modal 组件,当然无法注册这个 vue 组件。如此可能需要一个JS界的『通用形态』,一个构造函数,或者说一个 Class。

1.1 完成实例化的过程

在 src 目录下新建一个 entry.ts ,暴露一个 Class ,当用户实例化时,生成这个 vue 实例 (做原来 main.ts 的工作)

import {createApp, App, h, ref} from 'vue';
import ModalComp from './components/Modal.vue';
export class Modal {
    vm: App<Element>
    constructor(el: Element, params: {
        title?: string;
        visible?: boolean;
        'onUpdate:visible'?: (value: boolean) => unknown;
    }) {
        const vm = createApp({
            setup() {
                const visible = ref(params.visible || false);
                return () => h(
                    ModalComp,
                    {   
                        title: params.title,
                        visible: visible.value,
                        'onUpdate:visible': params['onUpdate:visible'] || ((value: boolean) => {
                            visible.value = value;
                        })
                    },
                    {
                        // todo: 插槽
                    }
                )
            }
        });
        vm.mount(el);
        this.vm = vm;
    }
};

复制代码

大部分的组件参数都作为 Class 的实例化参数,保留了和组件一致的默认行为,透传进了组件。但是,至关重要的插槽还没有。(还不熟悉渲染函数戳 这里
就 Modal 而言,插槽没有传递参数,用户也无法触及组件内的响应式数据,可以提供一个 renderContent 的参数,传入字符串,也可以是一个函数。

constructor(el: Element, params: {
        title?: string;
        visible?: boolean;
        renderContent?: string|(() => string);
        'onUpdate:visible'?: (value: boolean) => unknown;
    }) {
        
        const renderCardFun = typeof params.renderContent === 'function'
            ? params.renderContent
            : new Function('', `return \`${params.renderContent || ''}\`;`);
        const vm = createApp({
            setup() {
                const visible = ref(params.visible || false);
                return () => h(
                    ModalComp,
                    { // ... },
                    {
                        default() {
                            return h('div', {innerHTML: renderCardFun(), class: 'modal-content'});
                        }
                    }
                )
            }
        });
        vm.mount(el);
        this.vm = vm;
    }
复制代码
  • 如果传入的renderContent是字符串,还有必要转化成函数吗?在插槽有传参时有必要。

这样用户可以通过模板字符串语法书写插槽内容,而且在每次渲染插槽都会更新。

const renderCardFun = typeof params.renderContent === 'function'
    ? params.renderContent
    : new Function('data', `return \`${params.renderContent || ''}\`;`);
// ...
default(data: unkown) {
    return h('div', {innerHTML: renderCardFun(unkown), class: 'modal-content'});
}
// test
// renderCardFun = '用户名:${data.name}';
复制代码
  • 为什么是innerHTML?vue 的 slot 函数期望用户 return 一个用过 vue 渲染函数生成的虚拟 DOM(比如JSX),将这样的渲染函数给一个react的用户,恐怕还需要很多 vue 的背景。另外JSX在 vue 和 react 只是书写体验一直,虚拟 DOM 并不相容,即使写一个白名单转换,也无法满足用户自定义组件等复杂的需求,意义不大。所以选择了通用结构innerHTML,只是需要一层 DOM 作为容器。

到此,可能你已经发现了,外界还无法在实例化后干预 visible 的状态,一个弹窗有它需要出现的契机。

1.2 向外暴露组件的行为

用户在实例化时传入的visible无法显式的完成双向绑定 (要求用户包装成一个 Proxy 很反常识,而且用户还要理解『双向绑定』) 。因此,想到暴露一对 show hidden 方法。

visible ref变量将被 show hidden 修改,show hidden 方法所在的作用域使得 setup 已不再合适。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享