前言
造了个轮子,还没想好如何给大家用?看下去吧 ~
首先,我们假设已经费心打磨出了一个轮子 (比如一个 vue3 的通用组件),迫不及待地想用这个轮子为各位同仁『减负』。这个轮子将是什么形态的呢?
- 可能需要是一个 npm 包 ,方便项目接入。
- 可能需要有 typescript ,满足 ts 项目的需要,提升书写体验。
- 可能需要脱离技术栈,比如能够给一个 react项目使用。
- 可能需要是多个构建版本,方便 cjs 环境、script标签引入、es module。
然后就开始吧~
本文以 vue3+ts+less 的组件为例
0. 准备工作
我准备了一个用vue3实现的简单弹窗组件。
<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 我设计了 title
和 visible
两个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 已不再合适。