这篇文章就是看看一个ui compent是怎么建造出来的,vant的拆分写法写的如何优秀。本人最近也只是在瞎琢磨vant源码。
本文的思想来源于这里可以跳转的一篇文章。有很多东西借鉴了这里的想法,也是这篇文章的一个延伸吧。
组件目录结构
src
└─ button
├─ demo # 示例代码
├─ test # 单元测试
├─ index.js # 组件入口
├─ index.less # 组件样式
├─ README.md # 英文文档
└─ README.zh-CN.md # 中文文档
vant的轮子
在vant源码中,utils中封装了很多工具和方法。涉及了ts,tsx,js,vue等技术栈。
每个组件在一开是都会调用
createNamespace
函数,它接收
参数(name)
,返回
createComponent, createBEM, createI18N
三个函数。即comonent实例,css帮助类函数,以及多语言工具。
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
createComponent方法根据参数name,创建一个vue组件。
export function createComponent(name: string) {
return function<Props = DefaultProps, Events = {}, Slots = {}>(
// sfc参数是调用createComponent参数时传的值 支持两种格式,对象式组件或者函数式组件
sfc: VantComponentOptions | FunctionComponent
): TsxComponent<Props, Events, Slots> {
if (isFunction(sfc)) { // 这里判断是否是函数式组件,如是转换为对象式组件
sfc = transformFunctionComponent(sfc);
}
if (!sfc.functional) {
//mixins是vue自带的,算是一种组件可复用的一些功能组合。
// 当组件使用混入对象时,所有混入对象的选项将被混入该组件本身的选项。
sfc.mixins = sfc.mixins || [];
// push了一个兼容低版本scopedSlots的mixin
sfc.mixins.push(SlotsMixin);
}
sfc.name = name;
// install中调用了vue.compoent注册组件的方法
sfc.install = install;
return sfc as TsxComponent<Props, Events, Slots>;
};
}
2. createBEM
createBEM函数接收name参数生成符合BEM规范的class
export function createBEM(name: string) {
return function(el?: Mods, mods?: Mods): Mods {
if (el && typeof el !== 'string') { // 判断el参数是否存在并不是string类型
mods = el;
el = '';
}
el = join(name, el, ELEMENT); // join方法返回类似checkbox__text的类名
return mods ? [el, prefix(el, mods)] : el;
};
}
最后返回类似BEM格式的class类名
/**
* 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'
*/
3. createI18N
多种语言处理
import { get, isFunction } from '..';
import { camelize } from '../format/string';
import locale from '../../locale';
export function createI18N(name: string) {
const prefix = camelize(name) + '.';
return function(path: string, ...args: any[]): string {
const messages = locale.messages();
const message = get(messages, prefix + path) || get(messages, path);
return isFunction(message) ? message(...args) : message;
};
}
export type Translate = ReturnType<typeof createI18N>;
van组件checkbox
van组件checkbox的具体配置
首先引入createNamespace函数和mixin组件可复用功能。并调用createNamespace函数
import { createNamespace } from '../utils';
import { CheckboxMixin } from '../mixins/checkbox';
const [createComponent, bem] = createNamespace('checkbox'); // 这里是来接收createNamespace函数返回的方法
接下来是引入checkbox组件的配置
// 调用createComponent方法并返回这个方法的具体结果
export default createComponent({
// mixins组件可复用的一些功能和方法
// 这里具体引入了一些props、computed、methods还有rander函数
mixins: [
CheckboxMixin({
bem,
role: 'checkbox',
parent: 'vanCheckbox',
}),
],
// checked计算属性用来完成checkbox的v-model双向绑定的原理
computed: {
checked: {
get() {
if (this.parent) {
return this.parent.value.indexOf(this.name) !== -1;
}
return this.value;
},
set(val) {
if (this.parent) {
this.setParentValue(val);
} else {
this.$emit('input', val);
}
},
},
},
// 监听value属性,只要发生改变,就会向父组件发出通知
watch: {
value(val) {
this.$emit('change', val);
},
},
// methods用来组件提供的方法
methods: {
// @exposed-api
// toggle的具体用法参见官网api
toggle(checked = !this.checked) {
// When toggle method is called multiple times at the same time,
// only the last call is valid.
// This is a hack for usage inside Cell.
clearTimeout(this.toggleTask);
this.toggleTask = setTimeout(() => {
this.checked = checked;
});
},
setParentValue(val) {
const { parent } = this;
const value = parent.value.slice();
if (val) {
if (parent.max && value.length >= parent.max) {
return;
}
/* istanbul ignore else */
if (value.indexOf(this.name) === -1) {
value.push(this.name);
parent.$emit('input', value);
}
} else {
const index = value.indexOf(this.name);
/* istanbul ignore else */
if (index !== -1) {
value.splice(index, 1);
parent.$emit('input', value);
}
}
},
},
});