前言
长期处于业务开发,调用别人组件库的组件,有种“只会用轮子,不会造轮子”的感觉,趁着业务量不大的时候,在空余时间找一个成熟的UI组件库看看。
移动端应用开发现在非常普遍,vant作为移动端组件库可以满足大多数的场景,所以以它为研究对象,开启源码解读之旅。
本次研究的vant版本是v2.2.16,源码地址
项目目录
// vant团队维护的项目
package/
vant-cli
vant-doc
等等
// 组件文件目录
src/
button/
index.less
index.tsx
utils/
create/
bem.ts
component.ts
i18n.ts
index.ts
style/ //样式文件
var.less
index.less
index.ts // 自动生成的组件注入文件
在package文件夹中,有vant团队在维护的其他项目,具体目录如上图所示(对webpack插件或者脚手架感兴趣的同学可以看下vant-cli项目), src文件夹放着是vant所有插件目录,工具方法,样式文件
文件结构
前期准备
每一个vant组件都会调用createNamespace方法,这个方法传入组件的名称name,返回了createComponent、createBEM、createI18N三个方法,分别是component预设方法,BEM命名方法,还有多语言工具。
// create/index.ts
import { createBEM, BEM } from './bem';
import { createComponent } from './component';
import { createI18N, Translate } from './i18n';
type CreateNamespaceReturn = [
ReturnType<typeof createComponent>,
BEM,
Translate
];
export function createNamespace(name: string): CreateNamespaceReturn {
name = 'van-' + name;
return [createComponent(name), createBEM(name), createI18N(name)];
}
(1)createComponent
// create/component.ts
export function createComponent(name: string) {
// 返回一个泛型匿名函数,该函数把src文件夹的组件统一变成vue可以识别的组件
return function<Props = DefaultProps, Events = {}, Slots = {}> (
sfc: VantComponentOptions | FunctionComponent
): TsxComponent<Props, Events, Slots> {
// 如果是一个函数式组件,则创建拥有渲染函数render的组件对象
if (typeof sfc === 'function') {
sfc = transformFunctionComponent(sfc);
}
// 如果不是函数式组件,则push一个兼容低版本scopedSlot的mixin
if (!sfc.functional) {
sfc.mixins = sfc.mixins || [];
sfc.mixins.push(SlotsMixin);
}
sfc.name = name;
// 挂在注册vue组件的方法
sfc.install = install;
return sfc as TsxComponent<Props, Events, Slots>;
};
}
export type TsxComponent<Props, Events, Slots> = (
props: Partial<Props & Events & TsxBaseProps<Slots>>
) => VNode;
针对函数式组件,vant会调用transformFunctionComponent把它转变成vue的函数式组件
// should be removed after Vue 3
function transformFunctionComponent(pure: FunctionComponent): VantComponentOptions {
// 采用了vue2 jsx方法 创建组件
return {
functional: true,
props: pure.props,
model: pure.model,
render: (h, context): any => pure(h, context.props, unifySlots(context), context)
};
}
(2)createBEM
createBEM函数式是为组件生成BEM规范的class,即组件的子元素用__链接,组件的状态用--链接
/**
create/bem.ts
* bem helper
* b() // 'button'
* b('text') // 'button__text'
* b({ disabled }) // 'button button--disabled'
* b('text', { disabled }) // 'button__text button__text--disabled'
* b(['disabled', 'primary']) // 'button button--disabled button--primary'
*/
export type Mod = string | { [key: string]: any };
export type Mods = Mod | Mod[];
const ELEMENT = '__';
const MODS = '--';
// class类名链接
// ('button','text','__') => 'button__text'
function join(name: string, el?: string, symbol?: string): string {
return el ? name + symbol + el : name;
}
function prefix(name: string, mods: Mods): Mods {
// primary => 'button--primary button--disabled'
if (typeof mods === 'string') {
return join(name, mods, MODS);
}
// ['primary', 'disabled'] => 'button--primary button--disabled'
if (Array.isArray(mods)) {
return mods.map(item => <Mod>prefix(name, item));
}
// {plain:true ,block:true} => {'button--plain': true, 'button--block': true}
const ret: Mods = {};
if (mods) {
Object.keys(mods).forEach(key => {
ret[name + MODS + key] = mods[key];
});
}
return ret;
}
export function createBEM(name: string) {
return function(el?: Mods, mods?: Mods): Mods {
//只有字符串格式的el会被处理,其他格式会当做mods处理
if (el && typeof el !== 'string') {
mods = el;
el = '';
}
el = join(name, el, ELEMENT);
return mods ? [el, prefix(el, mods)] : el;
};
}
export type BEM = ReturnType<typeof createBEM>;
(3)createI18N
多语言处理,不多说了
export function createI18N(name: string) {
const prefix = camelize(name) + '.';
return function(path: string, ...args: any[]): string {
const message = get(locale.messages(), prefix + path) || get(locale.messages(), path);
return typeof message === 'function' ? message(...args) : message;
};
}
组件——button
先调用
createNamespace('button')获取到component和bem的辅助方法,上方的ButtonProps和ButtonEvents是官方提供给props和event,详情可以看官网文档,下方的Button则是button组件的生成方法。
(1)Button组件实现
入参
入参有四个,分别是h,props,slots,ctx,从之前说过的transformFunctionComponent来看,实际上传入的是vue jsx写法中render方法接收的入参h:createElement和ctx,props和slots只是是ctx的是两个属性
事件传递
function onClick(event: Event) {
if (!loading && !disabled) {
emit(ctx, 'click', event);
functionalRoute(ctx);
}
}
function onTouchstart(event: TouchEvent) {
emit(ctx, 'touchstart', event);
}
// emit 方法实现
export function emit(context: Context, eventName: string, ...args: any[]) {
const listeners = context.listeners[eventName];
if (listeners) {
if (Array.isArray(listeners)) {
listeners.forEach(listener => {
listener(...args);
});
} else {
listeners(...args);
}
}
}
内容渲染
const content = [];
// 添加icon
if (loading) {
content.push(
<Loading
class={bem('loading')}
size={props.loadingSize}
type={props.loadingType}
color="currentColor"
/>
);
} else if (icon) {
content.push(<Icon name={icon} class={bem('icon')} />);
}
// 添加按钮内容,如果有插槽,插槽的级别大于text
let text;
if (loading) {
text = loadingText;
} else {
text = slots.default ? slots.default() : props.text;
}
if (text) {
content.push(<span class={bem('text')}>{text}</span>);
}
return content;
}
// 返回需要渲染的JSX.Element元素
// 其中前两个属性style和class是Button方法中的style和class,
// 如果其他地方传入的style,class,或者是其他属性,通过inhert方法传入
return (
<tag
style={style}
class={classes}
type={props.nativeType}
disabled={disabled}
onClick={onClick}
onTouchstart={onTouchstart}
{...inherit(ctx)}
>
{Content()}
</tag>
);
如果使用者需要在组件上添加额外的attribute和event监听时候,函数式组件中可以用ctx.data.attrs和listeners来接收和传递。在button组件中用了一个inhert方法把这些额外attribute和event都加到组件中,以下是inhert函数代码
type Context = RenderContext & { data: VNodeData & ObjectIndex };
type InheritContext = Partial<VNodeData> & ObjectIndex;
const inheritKey = [
'ref',
'style',
'class',
'attrs',
'nativeOn',
'directives',
'staticClass',
'staticStyle'
];
const mapInheritKey: ObjectIndex = { nativeOn: 'on' };
// inherit partial context, map nativeOn to on
export function inherit(context: Context, inheritListeners?: boolean): InheritContext {
const result = inheritKey.reduce(
(obj, key) => {
if (context.data[key]) {
obj[mapInheritKey[key] || key] = context.data[key];
}
return obj;
},
{} as InheritContext
);
if (inheritListeners) {
result.on = result.on || {};
Object.assign(result.on, context.data.on);
}
return result;
}
动态样式
const classes = [
bem([
type,
props.size,
{
plain,
disabled,
hairline,
block: props.block,
round: props.round,
square: props.square
}
]),
{ [BORDER_SURROUND]: hairline }
];
根据用户配置和bem方法动态定义按钮的class,class根据index.less显示对应的样式
小结
第一次看UI组件库的源代码,发现里面的很多东西可以在自己平时开发中借鉴使用,比如bem类名的生成方法,组件注册方法等等。
虽然vue官方推荐模板语法,但是vant里面的绝大部分组件都使用了渲染函数&JSX方式实现,更有一些组件是当作函数式组件去注册到vue中,在Vue2官网的函数式组件页面中,有这样的一句话因为函数式组件只是函数,所以渲染开销也低很多。 我猜这个应该是vant为什么采用函数式组件去注册的原因吧。