阿里整体前端水平可以说是国内顶级水平,相关的开源组件库,尤其是ant-design(react版)具有很高的国内外使用率。随着前端技术栈的不断完善,相应的配套组件库也伴随着版本迭代。这些组件库的迭代过程和内部组件的实现,值得每一位前端开发者学习。特别是一些非常好的组件库。ant-design-vue组件库目录结构在github上克隆或下载开源组件库项目后,该项目的目录结构与我们日常开发项目的目录结构不同。几个核心的结构如下:antd-tools:该目录结构包含ant-design-vue组件库结合webpack、gulp等流行构建工具进行打包、构建和发布的工具。components:该目录是整改ant-design-vue库组件的组合集,所有公共组件都封装在该文件夹下开发时测试一些打包构建的配置脚本类型:为本库编写的声明文件集合组件源码解析Button组件:按钮组件,也是我们日常开发中最常用的组件之一点击查看。注意组件本身的输出和输入。这里的输入输出指的是自身的属性和行为,或者是父类传递过来的属性和行为。我们可以在components/button目录下看到按钮组件的封装结构,结构如下:__tests__:用于单元测试组件样式:封装组件需要编写的相关less样式文件。tsx:对应的组件文件接下来根据我们日常项目开发过程中按钮组件的使用,从以下问题阅读源码:类型(如:default,primary,dashed等)属性如何内部处理的组件?组件的图标属性内部是如何处理的?这个组件的loading属性内部是怎么处理的,达到防抖的效果?Button组件的type属性可以在ant-design-vue官网看到。有六种类型的Button组件:'default'、'primary'、'ghost'、'dashed'、'link'和'text'。当我们设置不同的类型时,Button组件的外观也会发生相应的变化。打开button.tsx文件,可以看到如下代码:importbuttonTypesfrom'./buttonTypes';constprops=buttonTypes();exportdefaultdefineComponent({name:'AButton',inheritAttrs:false,__ANT_BUTTON:true,props,...})导入了buttonTypes模块,定义了一个props变量来接收buttonTypes模块导出的函数的返回值。将props变量分配给Button组件的props属性。接下来看buttonTypes.ts文件的相关代码:第一行代码:import{tuple}from'../_util/type';tuple函数从_tuil的类型文件中导入,函数声明定义如下:exportconsttuple=(...args:T)=>args;利用ts中的泛型限制泛型T的类型只能是字符串数组(即数组中的元素只能是字符串类型),函数的返回词是特定的字符字符串元素。第七行代码:constButtonTypes=tuple('default','primary','ghost','dashed','link','text');定义了一个元组类型的变量ButtonTypes,其中包含6个元素。第8行代码:exporttypeButtonType=typeofButtonTypes[number];,这里定义并导出了ButtonType的类型;第23行代码:定义了一个buttonProps函数,它是整个Button组件的props属性的组合,函数代码如下:constbuttonProps=()=>({prefixCls:PropTypes.string,type:PropTypes.oneOf(ButtonTypes),loading:{type:[Boolean,Object],default:():boolean|{delay?:number}=>false,},disabled:PropTypes.looseBool,ghost:PropTypes.looseBool,块:PropTypes。looseBool,danger:PropTypes.looseBool,icon:PropTypes.VNodeChild,});可以看出,这里的类型是通过vue-types插件约束的,只能是上面提到的6种类型中的一种。从上面的分析,我们可以得出Button组件的类型的由来,然后看看它是如何与Button组件的样式关联起来的。例如,如果设置了type:danger,则Button组件的背景为红色,文本为白色。内部如何处理这种关系?回到Button.tsx中的代码。第75行定义了一个计算属性类。代码如下:constclasses=computed(()=>{const{type,shape,size,ghost,block,danger}=props;constpre=prefixCls.value;return{[`${pre}`]:true,[`${pre}-${type}`]:类型,[`${pre}-${shape}`]:形状,[`${pre}-${sizeCls}`]:sizeCls,[`${pre}-loading`]:innerLoading.value,[`${pre}-background-ghost`]:ghost&&!isUnborderedButtonType(type),[`${pre}-two-chinese-chars`]:hasTwoCNChar.value&&autoInsertSpace.value,[`${pre}-block`]:方块,[`${pre}-dangerous`]:!!danger,[`${pre}-rtl`]:方向.value==='rtl',};});这里可以看出type和button样式的区别,主要是pre和type这两个变量,其中pre代表对应组件类名的前缀,比如button组件的前缀ant-btn。最后看Button组件的return方法,代码如下:return()=>{constbuttonProps={class:[classes.value,attrs.class,],onClick:handleClick,};constbuttonNode=();如果(isUnborderedButtonType(类型)){返回按钮节点;}返回{buttonNode};};在该方法中,buttonProps对象类型的变量使用对象中的一个class属性来接收自己的class属性和上述的classes计算属性;然后定义了一个node节点buttonNode,node节点的内容就是html中的button标签,通过jsx语法解构buttonProps。A作为上面A的按钮组件,我们可以知道它的类名是:ant-btnant-btn-primary。在combinedstyle文件夹下,写上对应的style。可以知道不同类型对应的Button组件有不同的外观。Button组件的icon属性当我们使用Button组件时,比如组件需要显示图标和文字的组合,我们的可能性可能是这样写的:查询那么组件是如何处理里面的代码的呢?分析了上面Button组件的type属性,发现该组件的所有属性都是在buttonTypes.ts中定义和处理的,那么我们先来看一下这个文件中图标相关的代码。constbuttonProps=()=>({prefixCls:PropTypes.string,type:PropTypes.oneOf(ButtonTypes),icon:PropTypes.VNodeChild,...});可以看到这个函数返回的对象中有一个icon属性,它的类型就是PropTypes对象上的VNodeChild类型,那么这个VNodeChild到底是什么呢?最后,在vue-types文件夹下有一个type.ts文件,定义了VueNode的类型。导出类型VueNode=VNodeChild|JSX.元素;所以你终于可以知道图标是Vuejs中的虚拟子节点还是JSX中的元素。接下来回到button.tsx组件本身。exportdefaultdefineComponent({name:'AButton',slots:['icon'],setup(props,{slots,attrs,emit}){}})可以看到组件定义的时候,通过vuejs本身的slots属性接收图标元素。再看return返回的方法中的代码:consticon=getPropsSlot(slots,props,'icon');??它是不是一个对象?图标变量在此方法中定义。接下来我们看看getPropsSlot方法的作用是什么?在_util/props-util文件中可以看到这个方法的实现,代码如下:functiongetPropsSlot(slots,props,prop='default'){returnprops[prop]??slots[prop]?.();}可见该方法的功能是用来处理slot的。如果props上有对应的prop则直接返回,否则从slots中取出对应的prop执行方法,最后返回一个数组,数组中包含对应的虚拟节点元素,以及元素属性大致如下:anchor:nullappContext:nullchildren:"Iamicon"component:nulldirs:nulldynamicChildren:nulldynamicProps:nullel:textkey:nullpatchFlag:0props:nullref:nullscopeId:"data-v-c33314ea"shapeFlag:8ssContent:nullsstypeFallback:nullstaticCount:0suspense:nulltarget:nulltarget:nulltargetAnchition:transSymbol(Text)__v_isVNode:true__v_skip:true接下来是:consticonNode=innerLoading.value?:图标;constbuttonNode=();定义一个icon节点,通过buttonNode节点中的slot直接将icon节点应用到button标签上(成为button标签的子元素)。所以从下面的分析可以看出,Button组件对图标的处理主要是结合vuejs的slots或者props属性,虚拟出对应的符合条件的props(生成一个虚拟的子节点)。Button组件的loading属性在项目开发过程中使用非常频繁,比如通过它的点击事件传递一些数据给后台,处理后存入数据库。这是一个很常见的业务开发点,但是如果我们在1、2秒内连续多次点击按钮,如果不做任何处理,最终会在数据库中存入大量重复的数据。解决这个问题,我们不得不借助javascript中throttling的知识点来处理,但是ant-design-vue中button的处理内部封装了throttling处理,只要我们使用这个组件,添加一个loading属性即可会做。其组件内部的封装是如何实现的?还是先看一下buttonTypes.ts文件的加载相关代码,代码如下:constbuttonProps=()=>({...//其他代码loading:{type:[Boolean,Object],default:():boolean|{delay?:number}=>false,},...//其他代码onClick:{type:FunctionasPropType<(event:MouseEvent)=>void>,},});可以看到这个在buttonProps方法返回的对象中,有一个loading属性和一个onClick方法。loading属性的值可以是布尔类型,也可以是对象类型。它的默认值为假。onClick方法的参数是一个鼠标事件的事件对象。该方法没有返回值,然后返回到button.tsx组件本身。在文件的第22行,有一个类型定义typeLoading=boolean|数字;Loading的类型被定义为布尔或数字类型,然后在设置方法中定义一个变量,第46行constinnerLoading:Ref=ref(false);innerLoading变量是具有布尔值或数值的响应变量。那么定义这个变量有什么作用呢?继续看与之相关的代码。loadingOrDelay计算属性在第52行定义。用于接收loadingpropconstloadingOrDelay=computed(()=>typeofprops.loading==='object'&&props.loading.delay?props.loading.delay||true:!!props.loading,);通过58行watch监听计算出的属性loadingOrDelay的值变化,处理相关逻辑:watch(loadingOrDelay,val=>{clearTimeout(delayTimeoutRef.value);if(typeofloadingOrDelay.value==='number'){delayTimeoutRef.value=window.setTimeout(()=>{innerLoading.value=val;},loadingOrDelay.value);}else{innerLoading.value=val;}},{immediate:true,},);如果loadingOrDelay的值是数字类型,设置一个定义,在loadingOrDelay秒后,将loadingOrDelay的最新值赋值给innerLoading变量。否则直接将loadingOrDelay的值赋值给innerLoading。constdelayTimeoutRef=ref(undefined);由于设置了定时器,所以需要在组件即将销毁(卸载)时清除定时器。onBeforeUnmount(()=>{delayTimeoutRef.value&&clearTimeout(delayTimeoutRef.value);});最后,在第121行对点击事件进行了逻辑处理:consthandleClick=(event:Event)=>{//https://github.com/ant-design/ant-design/issues/30207|props.disabled){event.preventDefault();返回;}emit('点击',事件);};可以看出ant-design-vue在Button组件中加载的实现其实是相当巧妙和简单的。通过props接收loading属性,一系列的值变化并不是直接通过这个属性来处理的。而是在内部定义一个innerLoading变量和一个loadingOrDelay计算属性来处理相应的逻辑。这是因为加载是从外部组件传递过来的,不能直接修改。引用Button组件的第一种方式是直接导入Button.tsx组件使用,但只能在项目内部使用。另一种处理组件的方式是通过vuejs提供给组件的install方法。importtype{App,Plugin}from'vue';importButtonfrom'./button';/*istanbulignorenext*/Button.install=function(app:App){app.component(Button.name,Button);returnapp;};exportdefaultButtonastypeofButton&Plugin;相关知识提示TypeScript中的元组数组合并相同类型的对象,而元组(Tuple)合并不同类型的对象lettom:[string,number]=['汤姆,25];防抖节流:如果你是一个7岁的孩子,有一天妈妈正在做巧克力蛋糕。但是蛋糕不是给你的,而是给客人的,你一直向她要蛋糕。最后她给了你一块,但你一直向她要更多的蛋糕。她同意给你更多的蛋糕,条件是她在一小时后给你一块。这个时候你还继续跟她要蛋糕,但是她这时候不理你了。最后一个小时后,你得到更多的蛋糕。如果你要求更多的蛋糕,不管你要求多少次,一个小时后你都会得到更多的蛋糕。通过节流,无论用户触发事件多少次,附加函数都只会在给定的时间间隔内执行一次。防抖:考虑相同的蛋糕示例。这次你一直向你妈妈要蛋糕,她生气了,告诉你只有你保持沉默一个小时,她才会给你蛋糕。这意味着如果你一直问她,你就不会得到蛋糕——你只会在你最后一次问过一个小时后才能得到。对于debounce,无论用户触发事件多少次,一旦用户停止触发事件,附加函数只会在指定时间后执行。子组件prop验证可以看到,在整个ant-design-vue组件中,子组件props的验证都是由vue-types插件来处理的。第一点是减少代码量,第二点是方便阅读和扩展。vue-types插件提供了createTypes方法,通过该方法我们可以扩展更多的类型,在ant-design-vue中如下:import{createTypes}from'vue-types';constPropTypes=createTypes({func:undefined,bool:undefined,string:undefined,number:undefined,array:undefined,object:undefined,integer:undefined,});PropTypes.extend([{name:'looseBool',getter:true,type:Boolean,default:undefined,},{name:'style',getter:true,type:[String,Object],default:undefined,},{name:'VNodeChild',getter:true,类型:null,},]);组件封装规则组件是用来用的:应该根据用户(程序员)的感受没有“最好的办法”:需要考虑项目的特点好的组件是notdesigned,butmodified:经常调整,有时候重构组件的功能应该是单一和简单的:不要试图把很多功能塞进一个组件中。体现单一职责原则...封装组件供他人使用对于封装的公共组件,在封装组件时,必须考虑如何让他人导入和使用。目前比较流行的方式是通过npm来管理组件库,然后用户可以按需导入使用相应的组件。这就需要在打包组件时对某个组件进行“安装”和“导出”操作。/*istanbulignorenext*/Button.install=function(app:App){app.component(Button.name,Button);返回应用程序;};导出默认按钮