作为一个工作多年的老油条,最近我惊恐的发现一件事情,就是我自己真的好菜。以前自己还能手写个冒泡排序递归啥的,现在的自己只会百度和问ai了。
没想到工作越久,能力越差,想一个功能,经常要想很久,好像还不如刚毕业那会。工作这么多年,真的是荒废的不行。
最近痛定思痛,决定看看源码,就先从我最熟悉的vant-webapp开始看。
我们先从一个最简单的组件入手,看下vant-weapp的源码是什么样。
1.目录结构
vant-weapp的github地址是这个,youzan/vant-weapp: 轻量、可靠的小程序 UI 组件库

- build文件夹,存放构建相关的命令
- dist文件夹,构建好的,可以直接复制到小程序中使用的各个组件(es6版本)。
- docs/markdown,日志、使用说明等文档介绍
- example ,小程序案例
- lib,构建好的,可以直接复制到小程序中使用的各个组件(es5版本)
- packages,组件的源代码
我们需要关注的就是packages文件夹,这里我们以最简单组件van-divider
为例,说明下vant是怎么写组件的。
2.van-divider
目录结构
cmd
- demo(文件夹)
- test(文件夹)
- index.json
- index.less
- index.ts
- index.wxml
- index.wxs
- README.md
目录结构说明
- demo,是一个组件,这个组件又引入了
van-divider
组件,里面包含了van-divider
各种使用方法的案例。 - test,应该是跟自动化测试有关的,现在先不管它。
- index.json index.less index.ts index.wxml 对应的就是小程序自定义组件的json、css、js和wxml。只不过vant为了方便开发使用了ts和less。
- README.md,
van-divider
的使用说明
3.van-divider 用法
3.1 基础用法
html
<van-divider />
3.2 虚线
html
<van-divider dashed />
3.3 添加文本
html
<van-divider contentPosition="center">文本</van-divider>
<van-divider contentPosition="left">文本</van-divider>
<van-divider contentPosition="right">文本</van-divider>
4.源代码解释
要想看懂源代码,首先必须熟悉如何编写小程序的自定义组件。这是微信小程序自定义组件教程自定义组件 / 介绍
一个自定义组件由json、wxml、wxss、js组成
首先我们看下index.json文件
4.1 index.json
json
{
"component": true,
"usingComponents": {}
}
在json中,component:true
表明这是一个自定义组件,使用usingComponents
声明需要引入的其他组件,这里为空{}
,表示没有子组件。
我们再看下index.ts
4.2 index.ts代码
javascript
import { VantComponent } from '../common/component';
VantComponent({
props: {
dashed: Boolean,
hairline: Boolean,
contentPosition: String,
fontSize: String,
borderColor: String,
textColor: String,
customStyle: String,
},
});
按照小程序文档说明,创建组件,需要使用Component()来注册组件的,并提供组件的属性和方法。代码大概如下所示
css
Component({
properties: {
// 这里定义了innerText属性,属性值可以在组件使用时指定
innerText: {
type: String,
value: 'default value',
}
},
data: {
// 这里是一些组件内部数据
someData: {}
},
methods: {
// 这里是一个自定义方法
customMethod: function(){}
}
})
但是在index.ts中并没有使用Component方法,而是使用了一个叫VantComponent方法。想一下我们就应该明白,VantComponent方法应该是对Component方法的一个封装。
查看VantComponent源代码。
因为VantComponent的参数跟小程序Component参数是不一样,所以代码首先是将VantComponent的参数vantOptions转换成了小程序参数options。然后给options添加了一些默认的属性,比如外部样式类、behaviors等。 最后再调用Component(options)创建小程序组件。
js
// basic 是一个behavior,封装了小程序常用的几个方法。比如 setData、$emit 等,添加此 behavior 的组件,可以直接使用这些方法。
import { basic } from '../mixins/basic';
// VantComponent 参数的类型定义
import { VantComponentOptions } from 'definitions/index';
// mapKeys 用于将VantComponent参数中的属性转换成Component参数中的属性,比如将props转换成properties,将mixins转换成behaviors。VantComponent参数属性名没有直接使用小程序组件的属性名,而是使用vue的参数属性名,所以中间需要有个转换的过程。这样做,可能是为了兼顾下vue的开发者.
/**
* 将 {props:{},mixins:[]} 转换成 {properties:{},behaviors:[]}
*/
function mapKeys(
source: Record<string, any>,
target: Record<string, any>,
map: Record<string, any>
) {
Object.keys(map).forEach((key) => {
if (source[key]) {
target[map[key]] = source[key];
}
});
}
function VantComponent<
Data extends WechatMiniprogram.Component.DataOption,
Props extends WechatMiniprogram.Component.PropertyOption,
Methods extends WechatMiniprogram.Component.MethodOption
>(vantOptions: VantComponentOptions<Data, Props, Methods>): void {
const options: WechatMiniprogram.Component.Options<Data, Props, Methods> = {};
/**
* 将vantOptions对象
* {
* props:{},
* mixins:[],
* }
* 转换成options对象
* {
* properties:{},
* behaviors:[]
* }
*/
mapKeys(vantOptions, options, {
data: 'data',
props: 'properties',
watch: 'observers',
mixins: 'behaviors',
methods: 'methods',
beforeCreate: 'created',
created: 'attached',
mounted: 'ready',
destroyed: 'detached',
classes: 'externalClasses',
});
// 给组件添加外部样式externalClasses,默认值为['custom-class'],这样所有的组件都可以使用custom-class这个外部样式类,来继承外部样式
options.externalClasses = options.externalClasses || [];
options.externalClasses.push('custom-class');
// 给组件添加behaviors,默认值是basic,basic 是一个behavior,封装了小程序常用的几个方法。比如 set、$emit 等,添加此 behavior 后,所有的组件都可以调用set、$emit等方法。
options.behaviors = options.behaviors || [];
options.behaviors.push(basic);
// 如果vantOptions中存在relation属性,则将relation.relations和relation.mixin添加到options中
const { relation } = vantOptions;
if (relation) {
options.relations = relation.relations;
options.behaviors.push(relation.mixin);
}
// 如果vantOptions.field 属性存在,则表明这个组件是个表单组件,则将wx://form-field这个behavior添加到options.behaviors中
if (vantOptions.field) {
options.behaviors.push('wx://form-field');
}
// 所有组件都可以添加多个slot,所有组件都可以使用外部样式
options.options = {
multipleSlots: true,
addGlobalClass: true,
};
// 创建小程序组件
Component(options);
}
export { VantComponent };
上面的index.ts代码
js
VantComponent({
props: {
dashed: Boolean,
hairline: Boolean,
contentPosition: String,
fontSize: String,
borderColor: String,
textColor: String,
customStyle: String,
},
});
经过处理后就变成了我们熟悉的样子。
js
Component({
properties: {
dashed: Boolean,
hairline: Boolean,
contentPosition: String,
fontSize: String,
borderColor: String,
textColor: String,
customStyle: String,
},
});
可以看到小程序可以接收dashed
、hairline
、contentPosition
等外部属性,通过这些属性来实现虚线、细线、添加文本等功能
4.3 index.wxml 代码
html
<wxs src="../wxs/utils.wxs" module="utils" />
<wxs src="./index.wxs" module="computed" />
<view
class="custom-class {{ utils.bem('divider', [{ dashed, hairline }, contentPosition]) }}"
style="{{ computed.rootStyle({ borderColor, textColor, fontSize, customStyle }) }}"
>
<slot />
</view>
代码非常简单,开头引入了两个wxs,如果不知道wxs是什么的,可以点击这里查看 wxs教程。简单来说wxs就是可以内嵌在wxml中的,有限制的javascript。
在这里我们使用了utils和index两个wxs。
utils.bem
的作用是给class名添加前缀,并且把vant的class名转成符合bem规范的格式,比如如果dashed为true
下面这个代码得到的class名就是 vant-divider--dashed
js
utils.bem('divider', [{ dashed }])
computed.rootStyle
的作用是将js样式对象,转成样式字符串。比如我们设置borderColor
为"red"。
ini
<van-divider borderColor="red" />
这样组件里borderColor属性就是red。传到wxml中,经过computed.rootStyle
转换后,结果如下
js
computed.rootStyle({ borderColor}}
等到的样式就是border-color:red
,这样我们就可以把线条的颜色设置成红色了。
html
<view
class="custom-class {{ utils.bem('divider', [{ dashed, hairline }, contentPosition]) }}"
style="{{ computed.rootStyle({ borderColor, textColor, fontSize, customStyle }) }}"
>
<slot />
</view>
这段代码中,view 的custom-class
是传过来的外部样式类属性(externalClasses
),用来让组件使用页面或者全局定义的一些样式.
slot是占位符,可以在组件内插入一些内容,比如文字、图片等。比如下面这个,"文字"就会插入到slot所在的位置。
html
<van-divider contentPosition="center">文本</van-divider>
4.4 index.less 代码
divider分割线的各种效果是通过css代码实现的。
其中线条是通过before伪类和after伪类来实现的。如果没有内容,只有一个线条的话,线条是通过before伪类来实现的。如果有内容的话,内容是:before伪类 内容 after伪类。 其中before伪类实现左线条,after伪类实现右线条。
代码很简单,没有啥可讲的了。
css
@import '../common/style/var.less';
.van-divider {
display: flex;
align-items: center;
border-style: solid;
border-width: 0;
margin: var(--divider-margin, @divider-margin);
color: var(--divider-text-color, @divider-text-color);
font-size: var(--divider-font-size, @divider-font-size);
line-height: var(--divider-line-height, @divider-line-height);
border-color: var(--divider-border-color, @divider-border-color);
&::before,
&::after {
display: block;
flex: 1;
box-sizing: border-box;
height: 1px;
border-color: inherit;
border-style: inherit;
border-width: 1px 0 0;
}
&::before {
content: '';
}
&--hairline {
&::before,
&::after {
transform: scaleY(0.5);
}
}
&--dashed {
border-style: dashed;
}
&--center,
&--left,
&--right {
&::before {
margin-right: var(--divider-content-padding, @divider-content-padding);
}
&::after {
content: '';
margin-left: var(--divider-content-padding, @divider-content-padding);
}
}
&--left {
&::before {
max-width: var(--divider-content-left-width, @divider-content-left-width);
}
}
&--right {
&::after {
max-width: var(
--divider-content-right-width,
@divider-content-right-width
);
}
}
}
总结
通过vant-divider组件的源码解读,我们差不多算是了解了vant组件的实现方式。其他组件也类似。
小程序组件整体来说还是比较简单,比vue和react组建都要简单。但是也有很多地方值得我们学习,比如组件架构和代码风格,如果让自己写的话,自己很难写出这么干净漂亮的代码。
加油,继续努力吧