背景
在使用不同的技术栈开发组件库的同一功能组件的时候会在想,哪些工作量能减少
- 纯函数
- 样式
- 国际化
- 组件逻辑
其中1、2、3都没问题,4要怎么做或者能做哪些呢?
能否使用React或者vue的方式去开发另一个呢?
到底用React的组件方式去开发Vue呢,还是反之呢?
组件示例
先看下基本平常单独开发一个组件库的基本示例
React组件
// React组件 test.tsx
const Test: React.FC<{
count: number;
changeCount: (count: number) => void;
children?: (params: { count: number }) => React.ReactNode;
}> = ({ count = 0, changeCount, children = () => 'children' }) => {
return (
<div className="test" onClick={() => changeCount?.(count + 1)}>
Test<span>{count}</span>
{ children({ count }) }
</div>
);
};
export default Test;
Vue组件
方式一
// Vue组件 Test.vue
<template>
<div class="test" @click="() => changeCount?.(count + 1)">
Test<span>{{count}}</span>
<slot :count="count">
"children"
</slot>
</div>
</template>
<script lang="ts" setup>
import type { VNodeChild } from 'vue';
withDefaults(defineProps<{
count: number;
changeCount: (count: number) => void;
}>(), {
count: 0,
})
defineSlots<{
default: (params: { count: number }) => VNodeChild;
}>();
</script>
方式二
// Vue组件 Test.tsx
import type { SlotsType } from 'vue';
import { defineComponent } from "vue";
export default defineComponent<
{
count: number;
changeCount: (count: number) => void;
},
{},
string,
SlotsType<{
default: (params: { count: number }) => VNodeChild;
}>
>((props, { slots }) => {
return () => {
const { count, changeCount } = props;
return (<div class="test" onClick={() => changeCount?.(count + 1)}>
Test<span>{count}</span>
{slots.default ? slots.default({ count }) : "children"}
</div>)
};
});
方式三
// Vue组件 Test.tsx
import { defineComponent } from "vue";
export default defineComponent({
props: {
count: {
type: Number,
default: 0,
},
changeCount: {
type: Function,
default: () => {},
},
},
render() {
const { count, changeCount, $slots } = this;
return (
<div class="test" onClick={() => changeCount?.(count + 1)}>
Test<span>{count}</span>
{$slots.default ? $slots.default({ count }) : "children"}
</div>
);
},
});
方式四
// Vue组件 Test.tsx
// 函数式组件
import { FunctionalComponent } from 'vue';
const Test: FunctionalComponent<{
count: number;
changeCount: (count: number) => void;
children?: (params: { count: number }) => React.ReactNode;
}> = ({ count = 0, changeCount, children = () => 'children' }) => {
return (
<div class="test" onClick={() => changeCount?.(count + 1)}>
Test<span>{count}</span>
{ children({ count }) }
</div>
);
};
export default Test;
组件差异
综上:React写法比Vue的代码更简单, 以React为源码风格
Vue组件方式四函数式样组件相似度几乎可以达到100%但操作性不强,Vue函数式组件不支持状态属性,无生命周期,这些其实都可以借鉴Reac函数式组件处理,本期先不讨论,放到 Vue模拟React Hooks render版/函数式组件版
Vue组件方式二和React相似度及可操作性综合更高,但还是有差异的,差异如下:
| 类型 | React | Vue |
|---|---|---|
| 文件拓展名 | tsx | .vue |
| class对应属性名 | className | class |
| 子元素来源 | props | slot |
| 默认子元素key | children | default |
| 渲染函数 | 完整的函数式组件 | setup返回的函数 |
| 响应式核心及hooks | 此篇先不展开说 | 此篇先不展开说 |
跨库组件库差异填平
如何填平这些差异呢?
设计一个function createComponent,在函数内部去翻转地球
使用时如下
// React/Vue组件 test/index.tsx
import type { C } from '../utils';
import { createComponent, getClsObj, dealChild } from '../utils';
const Test: C<{
count: number;
changeCount: (count: number) => void;
children?: (params: { count: number }) => React.ReactNode;
}> = ({ count = 0, changeCount, children = () => 'children' }) => {
return (
<div {...getClsObj('test')} onClick={() => changeCount?.(count + 1)}>
Test<span>{count}</span>
{ dealChild(children)?.({ count }) }
</div>
);
};
export default createComponent(Test, { childKeys: ['children'] });
经过 createComponent 的处理
- React的className及Vue的class都通过getClsObj去处理
- Vue组件开发时的所有子元素也从props中读取,但使用的时候无感知
- 同时支持props设置子元素,但如果通过默认插槽设置了子元素同时props设置了children, 即相同功能插槽优先级高于props
- 所有用作子元素的props属性需要通过 createComponent 第二个参数传入,目的是 createComponent内部会做处理即如果是undefined或function直接返回,如果其他处理为function,使用时如上children?.()
React组件库中的utils如下
// React组件的utils/index.ts
// 填平className key不同的差异
export const getClsObj = (cls: any) => {
return {
className: cls,
};
};
/**
* 处理子节点
* --------
* 全部处理为函数
*/
const dealChild = (child: any) => {
if (child === undefined){
return
}
if (typeof child === "function") {
return child;
// } else if (React.isValidElement(child)) {
// return () => child;
} else {
return () => child;
}
};
export type C<P extends Record<string, unknown>> = React.FC<P>;
export const createComponent = <P extends Record<string, unknown>>(
c: C<P>,
{ childKeys = [] }: { childKeys?: Array<keyof P> },
) => {
const res = {
[c.name]: ((propsInit: P, ...args: any[]) => {
const props = { ...propsInit };
Object.entries(props).forEach(([key, value]) => {
if (childKeys.includes(key as keyof P)) {
props[key as keyof P] = dealChild(value);
}
});
return c(props, ...args);
}) as C<P>,
};
// 保留函数名
const dealRender = res[c.name];
return dealRender;
};
Vue组件库中的utils如下
// Vue组件的utils/index.ts
import type { VNodeChild } from "vue";
import { isVNode, defineComponent, computed } from "vue";
// 填平className key不同的差异
export const getClsObj = (cls: any) => {
return {
class: cls,
};
};
/**
* 处理子节点
* --------
* 全部处理为函数
*/
export const dealChild = (child: any) => {
if (child === undefined){
return
}
if (typeof child === "function") {
return child;
// } else if (isVNode(child)) {
// return () => child;
} else {
return () => child;
}
};
const DEFAULT_KEY = "default";
const CHILDREN_KEY = "children";
const transformProps = <P extends Record<string, unknown>>(
initProps: P & { default: P["children"] },
keysToTransform: (typeof DEFAULT_KEY | keyof P)[],
) => {
const props: P = { ...initProps };
Object.entries(props).forEach(([key, value]) => {
if (keysToTransform.includes(key as keyof P)) {
props[(key === DEFAULT_KEY ? CHILDREN_KEY : key) as keyof P] =
dealChild(value);
}
});
return props;
};
export type C<P extends Record<string, unknown>> = (props: P) => VNodeChild;
export const createComponent = <P extends Record<string, unknown>>(
c: C<P>,
{ childKeys = [] }: { childKeys?: Array<keyof P> },
) => {
const keysToTransform = childKeys.map((key) => (key === CHILDREN_KEY ? DEFAULT_KEY : key));
return defineComponent<P>(
(props, { slots }) => {
const params = computed<P>(() => {
return transformProps<P>(
{
...props,
...(slots as P & { default: P["children"] }),
},
keysToTransform,
);
});
return () => c(params.value);
},
{
name: c.name,
},
);
};
项目结构
新建一个monorepo项目,两个子库,component-react, component-vue, 保证src中除utils目录以外的内容完全一样,可以选择其中一个包作为源码库创建硬连接去另一个包中,这样无需拷贝,为啥不直接创建个第三个包以提取公共部分,当然可以如styles、locales、tools可以,但组件如test目录不可以,因为在在React和Vue中虽然都是如下,但显然React和Vue的utils实现是不同的,只是暴露相同名称Api及类型而已。
import type { C } from '../utils';
import { createComponent, getClsObj } from '../utils';
目录仅供参考如下:
- packages
-- component-react
--- utils.ts
--- test // 内容同component-vue同路径文件
---- index.tsx // 内容同component-vue同路径文件
-- component-vue
--- utils.ts // Vue组件库专门的utils.ts
--- test // 内容同component-vue同路径文件
---- index.tsx // 内容同component-vue同路径文件
project/
│
├── packages/
│ │
│ ├── component-react/
│ │ │
│ │ ├── src/
│ │ │ ├── utils/ // React组件库专门的utils.ts
│ │ │ │ └── index.ts
│ │ │ │
│ │ │ ├── test/ // 内容同component-vue同路径文件
│ │ │ │ └── index.tsx
│ │ │ │
│ │ │ ├── tools/ // 内容同component-vue同路径文件
│ │ │ │ └── index.tsx
│ │ │ │
│ │ │ ├── styles/ // 内容同component-vue同路径文件
│ │ │ │ └── test.less
│ │ │ │ └── index.less
│ │ │ │
│ │ │ └── locales/ // 内容同component-vue同路径文件
│ │ │ └── zh-CN.json
│ │ │ └── en-US.json
│ │ │ └── index.ts
│ │ │
│ │ ├── package.json
│ │ └── ... (其他文件和目录)
│ │
│ ├── component-vue/
│ │ │
│ │ ├── src/
│ │ │ ├── utils/ // Vue组件库专门的utils.ts
│ │ │ │ └── index.ts
│ │ │ │
│ │ │ ├── test/ // 内容同component-react同路径文件
│ │ │ │ └── index.tsx
│ │ │ │
│ │ │ ├── tools/ // 内容同component-vue同路径文件
│ │ │ │ └── index.tsx
│ │ │ │
│ │ │ ├── styles/ // 内容同component-react同路径文件
│ │ │ │ └── test.less
│ │ │ │ └── index.less
│ │ │ │
│ │ │ └── locales/ // 内容同component-react同路径文件
│ │ │ └── zh-CN.json
│ │ │ └── en-US.json
│ │ │ └── index.ts
│ │ │
│ │ ├── package.json
│ │ └── ... (其他文件和目录)
│ │
│ └── ... (其他目录和文件)
│
├── package.json
└── ... (其他项目层级的文件和目录)