我一直认为前端封装组件是一件非常简单的事情,不管我们用的技术栈是三大框架里的哪一个。对于一般的业务来说,我们只需要考虑将需要拆出来的业务代码想办法封装成一个组件就行,考虑它接受哪些参数,有哪些变化,是否接受请求,是否有对外的交互,是否需要对外暴露属性等等。
只要我们想好这些事情,按部就班的开发就可以了,这样似乎也没什么不妥。可以很快的进行业务代码的迭代,在业务代码的开发中非常普遍。
但是如果有一天,我们需要抛弃第三方组件库,建设自己团对内部的组件库,从头开始,从0到1进行通用性组件的开发,这时候你会发现,哦,原来从0到1开发一套自己团队内部的组件库其实也挺不容易。
开发一套内部统一的组件库,首先要解决的问题是样式统一的问题。样式的命名规则,样式的设计等,虽然我们都习惯了用第三方的组件库,但是如果我们随便下载一个组件库的源码来进行研究,你会发现,仅仅是一套完整的样式,也是一个非常大的工作量。以ant-design-vue为例,仅仅是整套样式的设计,也有这么多文件:
image.png
动画,颜色,公共,组件,mixins以及字体,图标等等,他们是一整个的系统,而不是我们平时业务代码中封装的哪些内容。
这其实也很正常,因为在一个团队中,通常有这样的分工。一部分人做业务开发,推进业务的正常进行。一部分人做技术开发,基于现有的业务进程技术的抽象,沉淀一些能够提升开发效率的工具。业务开发人员就是我们通常说的业务团队,通常业务开发人员在大团队中的地位似乎不高,我们往往会认为技术团队是整个团队的核心,我也不知道这是为什么,但是从客观的角度来讲:业务团队和技术团队是同等重要的
。
因为对于公司来讲,技术固然重要,但是更重要的其实还是业务
。
说了这么多,似乎有些跑题了,我们接着说为什么说用ts封装一个组件是件不容易的事请。
首先,这里的组件指的是通用组件。通用组件的开发,需要我们有很高的业务抽象能力。我们拿前端目前经常用的按钮button举例。想要开发一个通用的,功能丰富的按钮组件,我们需要考虑哪些方面呢?举例说明一下:
- 类型
- 形状
- 加载状态
- 禁用状态
- 按钮图标
- 按钮组
- 事件
- 等等
用ts定义这些属性,代码如下:
代码语言:javascript复制import {PropType} from 'vue'
import PropTypes, {SizeType} from '../../utils/config'
export type ButtonType =
| "link"
| "default"
| "primary"
| "ghost"
| "dashed"
| "text";
export type ButtonShape = "default" | "circle" | "round";
export type ButtonHTMLType = "submit" | "button" | "reset";
export const buttonProps = () => ({
prefixCls: String,
type: String as PropType<ButtonType>,
shape: { type: String as PropType<ButtonShape> },
size: {
type: String as PropType<SizeType>,
},
loading: {
type: [Boolean, Object] as PropType<boolean | { delay?: number }>,
default: (): boolean | { delay?: number } => false,
},
disabled: { type: Boolean, default: undefined },
ghost: { type: Boolean, default: undefined },
danger: { type: Boolean, default: undefined },
icon: PropTypes.any,
href: String,
target: String,
title: String,
onClick: {
type: Function as PropType<(event: MouseEvent) => void>,
},
onMousedown: {
type: Function as PropType<(event: MouseEvent) => void>,
},
});
说实话,这个是我看了源码之后提炼出来的一部分,我自己是不会想到这么多内容的,因为我主要也是做业务开发。那么上面列举的这些属性,一部分是样式相关的,就需要我们先定义好相关的样式类,然后通过代码进行展示。
另外,即便我们能够想到一个按钮需要这么多属性,但是,如何去定义这些属性,属性的值用何种方式进行定义,也是一个需要我们考虑的问题。
假设我们考虑的非常清楚,那么接下来就需要去实现这些属性对应的功能。在vue3中,定义组件可以选择好几种方式,目前用的比较多的是defineComponent
,这种方式实现一个按钮类型的代码如下:
<template>
<button :class="classes" ref="{buttonNodeRef}" type="{htmlType}">
<slot></slot>
</button>
</template>
<script lang="ts">
import { computed, defineComponent, StyleValue } from "vue";
import { buttonProps } from "./propTypes";
export default defineComponent({
name: "QButton",
inheritAttrs: false,
props: buttonProps(),
// setup 中 context上下文是个对象
setup(props, { attrs, slots, emit, expose }) {
const prefixCls = "qing-btn";
const {
type = "primary",
shape,
htmlType,
disabled,
href,
title,
target,
onMousedown,
} = props;
const classes = computed(() => {
return [
`${prefixCls}`,
{
[`${prefixCls}-${type}`]: type,
[`${prefixCls}-${shape}`]: !!shape,
},
];
});
const handleClick = (e: Event) => {
e.preventDefault();
e.stopPropagation();
emit("click", e);
};
return {
htmlType,
classes,
// styles,
handleClick,
};
},
});
</script>
这里仅仅是做个示例,这种写法和vue2的写法基本一致。只是原先导出对象的方式,改为了用函数生成对象,然后用组合式API setup接管了内部的一些属性和方法。
刚开始的时候我们可能会不太习惯这种写法,其实我们仔细细考一下,就很容易理解。
在vue中,核心的概念其实就是虚拟dom。虚拟dom的本质是一个对象,通过渲染函数render将这个对象渲染为html字符串,然后添加到界面上。
有了这个认识,defineComponent其实只是一个函数接受了一些个参数,最后返回了一个对象而已。而至于setup,不过是这个对象里的一个方法,它会在合适的时机去执行,作用是隔离变化也好,或者有其他的作用也好,它仅仅是对象的一个方法。
假设我们对vue3的新的语法和特性有了一定的认识,但是正式的去开发一套内部的组件库还是有一定的困难的。因为我们长期做业务开发,虽然对组件化又一定的认知。但是组件库的开发,其实不是一个人的事情,需要ui团队和技术团队共同参与进去,这对于整个团队内部的人员素质要求很高。
一个是技术的广度和深度必须能够达到一定的层次。比如:样式变量的定义,继承,样式函数的编写。这些都是需要对sass或者less有一定的研究才能完成的。其次,js代码中各种类型的定义和抽象,如何去继承,尽量的复用代码,也是一个需要考虑的问题。
另外,对于业务的理解程度也是一个方面。比如,实际的业务中,能否提炼出一些简洁的hooks。都是需要考虑的问题。
最后,在实际的业务代码中。我们通常会对第三方组件库进行封装,来进行业务代码的开发。这种封装也是需要考虑到很多因素的。
如果我们考虑的不够周全,开发出来的组件,团队内部进行推广的时候,有可能机会产生很多问题,这个时候就会比较尴尬。
最后,希望我们在做业务开发的时候,都能够用全局的眼光去思考一些问题。当然,和同事进行探讨是一个不错的选择,因为三人行,必有我师
。
谢谢大家。