研究轮子,Vant源码分析1—Row/Col

6,641 阅读4分钟

前言

现在太多成熟、优秀的轮子功vuer使用,久而久之会有种“只会用轮子跑,不知道怎么造”的感觉,所以本篇就抛开乱七八糟的业务需求,来看看一个优秀的ui component是如何实现的。
现在手机端电商开发非常普遍,vant又能够满足大多数使用场景,所以以它为研究对象,涉及到的技术栈有vue + jsx + typescript + less。

目录简介

    src/
        utils/      //工具类
        style/      //全局样式
        row/        //组件代码
            index.js
            index.less
            test/       //测试代码
            demo/       //示例
        col/
        .....
    test/        //测试类
    types/       //typescript声明文件

准备工作:create创建类函数

vant每个组件都会先调用creaeNamespace函数,createNamespace函数接收参数name,返回createComponentcreateBEMcreateI18N三个函数,即comonent实例,css帮助类函数,以及多语言工具。

// utils/create/index.ts

export function createNamespace(name: string): CreateNamespaceReturn {
  name = 'van-' + name;
  return [createComponent(name), createBEM(name), createI18N(name)];
}

(1)createComponent

createComponent函数根据name生成一个vue组件对象,在这个过程中对组件进行一些预设。


export function createComponent(name: string) {
  return function<Props = DefaultProps, Events = {}, Slots = {}> (
  // 参数sfc:单文件组件(single file components),支持对象式组件和函数式组件
    sfc: VantComponentOptions | FunctionComponent
  ): TsxComponent<Props, Events, Slots> {
  
    // 函数式组件转化为对象式组件
    if (typeof sfc === 'function') {
      sfc = transformFunctionComponent(sfc); 
    }
   
    if (!sfc.functional) {
      sfc.mixins = sfc.mixins || [];
      sfc.mixins.push(SlotsMixin); // push了一个兼容低版本scopedSlots的mixin
    }

    sfc.name = name;
    sfc.install = install; // install中调用了Vue.component()方法注册组件

    return sfc as TsxComponent<Props, Events, Slots>;
  };
}

对象式组件VantComponentOptions在vue组件基础上添加了两个属性:functionalinstall方法

export interface VantComponentOptions extends ComponentOptions<Vue> {
  functional?: boolean; // 用于上文函数式组件判断
  install?: (Vue: VueConstructor) => void; // 插件的install方法
}

(2)createBEM

createBEM函数是一个根据name生成css类名的方法,将类的元素(ELEMENT)和类名用“__”分隔,类的模组(MODS)用“--”分隔,使所有样式类名统一前缀,示例如下。

/**
 * 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'
 */

b()接收两个参数:elmods

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;
  };
}

(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;
  };
}

van组件——Row和Col

先调用creaeNamespace函数

const [createComponent, bem, t] = createNamespace('row');

然后将组件的配置项传入createComponent


export default createComponent({
  props: {
    ...
  },

  methods: {
    ...
  },

  render() {
    ...
    );
 }

具体props和events可以对照官方给出的api

这里着重讲Row和Col的render函数,分为组件实现、Html dom写法、css写法三个部分。

(1)组件实现

vant将布局分成了24列栅格,在Col上用span属性控制占比,offset控制偏移,在Row上gutter控制间隔。
其中gutter通过Col的左右padding 1/2 gutter来实现,为了抵消padding产生的多余间距,在Row的左右各margin -1/2 gutter,如图(搬运自juejin.cn/post/684490…,懒得画了)

spanoffset就是总宽度/24*100%*具体数值

(2)Html dom写法

很多vuer倾向于使用vue-template写法进行开发,这样更符合传统Html/css/js习惯,不过vue也提供了render函数,并支持jsx,vant就是采用这种写法,能够对dom细节有更强的控制能力。

    // Row.js
  render() {
    const { align, justify } = this;
    const flex = this.type === 'flex';
    const margin = `-${Number(this.gutter) / 2}px`;
    // 计算样式:marginLeft和maginRight
    const style = this.gutter ? { marginLeft: margin, marginRight: margin } : {};

    return (
    // 动态tag
      <this.tag 
        style={style}
        // 通过createBem将css类名统一在一个前缀下
        class={bem({
          flex,
          [`align-${align}`]: flex && align,
          [`justify-${justify}`]: flex && justify
        })}
        // 监听click事件
        onClick={this.onClick}
      >
        {this.slots()} // 放置一个slot,这样Col就被包裹在Row里,可以在Col中通过this.$parent获取Row的data属性
      </this.tag>
    );
  }
  
  // Col.js
 computed: {
    gutter() {
      return (this.$parent && Number(this.$parent.gutter)) || 0; // 获取父级gutter,仅支持一级父类
    },

    style() {
      const padding = `${this.gutter / 2}px`;
      return this.gutter ? { paddingLeft: padding, paddingRight: padding } : {};
    }
  },
  render() {
    const { span, offset } = this;
    return (
      <this.tag
        style={this.style}
        class={bem({ [span]: span, [`offset-${offset}`]: offset })}
        onClick={this.onClick}
      >
        {this.slots()}
      </this.tag>
    );
  }

(3)css写法

vant采用了less进行开发,配合上文提到的createBem方法大大提高了开发效率。 以上文Rol的class为例:

  class={bem({ [span]: span, [`offset-${offset}`]: offset })}

当 span=1,offset=2 , 即

  class={bem({ 1: 1, [`offset-2`]: 2 })}
  // 转换结果
  class="van-col--1 van-col--offset-2"

利用less的循环来动态生成css样式

.generate-col(24);
.generate-col(@n, @i: 1) when (@i =< @n) {
  .van-col--@{i} { width: @i * 100% / 24; } // 24列栅格
  .van-col--offset-@{i} { margin-left: @i * 100% / 24; }
  .generate-col(@n, (@i + 1));
}

小结

本文简要介绍了vant的utils类函数即Row和Col两个组件,之后会酌情挑选其他组件进行分析。仅作个人学习分享之用,如有错谬,欢迎指正^^!