Vant源码浅析1 -- button

504 阅读5分钟

前言

长期处于业务开发,调用别人组件库的组件,有种“只会用轮子,不会造轮子”的感觉,趁着业务量不大的时候,在空余时间找一个成熟的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 // 自动生成的组件注入文件

image.png

在package文件夹中,有vant团队在维护的其他项目,具体目录如上图所示(对webpack插件或者脚手架感兴趣的同学可以看下vant-cli项目), src文件夹放着是vant所有插件目录,工具方法,样式文件

文件结构

image.png

前期准备

每一个vant组件都会调用createNamespace方法,这个方法传入组件的名称name,返回了createComponentcreateBEMcreateI18N三个方法,分别是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

image.png 先调用createNamespace('button')获取到component和bem的辅助方法,上方的ButtonProps和ButtonEvents是官方提供给props和event,详情可以看官网文档,下方的Button则是button组件的生成方法。

(1)Button组件实现

image.png

入参

入参有四个,分别是h,props,slots,ctx,从之前说过的transformFunctionComponent来看,实际上传入的是vue jsx写法中render方法接收的入参h:createElementctx,propsslots只是是ctx的是两个属性 image.png

事件传递

  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.attrslisteners来接收和传递。在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为什么采用函数式组件去注册的原因吧。

参考文章 juejin.cn/post/684490…