likes
comments
collection
share

类名管理工具 classnames 与 bem

作者站长头像
站长
· 阅读数 12

在组件开发中,必不可少的要做样式类命名和管理,这里分享一下 Vant 源码中的工具函数 bemclassnames 包来管理组件中的样式。

bem 工具函数

BEM: BEM是一种前端命名方法论,主要是针对CSS,意思是块(Block)、元素(Element)、修饰符(Modifier)的简写。这种命名方法让CSS便于统一团队开发规范和方便维护。

vant bem源码

vant 命名示例: 以 __ 连接 element,以 -- 连接 modifier(遵循规范)

van-button van-button--success van-button--normal van-button--disabled
van-button__content
van-button__text

ant design 命名示例:: 都以 - 连接(简单粗暴)

ant-btn ant-btn-text ant-btn-dangerous

用法

// 前缀
const modName = 'xui-button'
const bem = createBEM(modName);

bem() // 'xui-button'
bem('text') // 'xui-button__text'
bem('text', 'disabled') // 'xui-button__text xui-button--disabled'
bem({ disabled: true }) // 'xui-button xui-button--disabled'
bem('text', ['disabled']) // 'xui-button__text xui-button__text--disabled'
bem('text', { disabled: true }) // 'xui-button__text button__text--disabled'
bem(['disabled', 'primary']) // 'xui-button button--disabled button--primary'

源码解析

export type Mod = string | { [key: string]: any };
export type Mods = Mod | Mod[];

function genBem(name: string, mods?: Mods): string {
  // 没有 mod 或 mod为false 则返回空
  if (!mods) {
    return '';
  }
    
  // 如果 mod 是字符串,则拼接上 blockName 返回,如 button--disabled
  if (typeof mods === 'string') {
    return ` ${name}--${mods}`;
  }

  // 如果是数组如 ['disabled', 'primary'], 则返回 ' button--disabled button--primary'
  // 如果是数组如 ['disabled', undefined, 0, ''], 则返回 ' button--disabled'
  // reduce 递归处理每个数组元素
  if (Array.isArray(mods)) {
    return mods.reduce<string>((ret, item) => ret + genBem(name, item), '');
  }
  
  // 如果是对象 { disabled: true }, 则返回  ' button--disabled'
  // 循环对象每一个key,判断 value 是否为真,为真则递归处理 key
  return Object.keys(mods).reduce(
    (ret, key) => ret + (mods[key] ? genBem(name, key) : ''),
    ''
  );
}

/**
 * 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'
 
 * createBEM 接收一个参数 blockName 即组件的名称,返回一个函数,这个函数接收多个参数
 * 参数类型可以是,字符串、对象、数组
 * 字符串为 element 元素
 * 对象或数组为 modifier 修饰符
*/
export function createBEM(name: string) {
  return (el?: Mods, mods?: Mods): Mods => {
  // 如果第一个参数不是 el(不是字符串),则设置为 modifier (el 只能在第一个参数位置)
    if (el && typeof el !== 'string') {
      mods = el;
      el = '';
    }
    // 如果 el 为空,则 el 为 blockName
    // 否则 el 为: blockName__elName
    el = el ? `${name}__${el}` : name;
    
    // 生成带 modifier 的类名
    return `${el}${genBem(el, mods)}`;
  };
}

classnames模块

帮助你在项目中更好地使用处理 className

classnames github源码

用法

import classNames from 'classnames'

classNames('foo', 'bar'); // => 'foo bar' 
classNames('foo', { bar: true }); // => 'foo bar' 
classNames({ 'foo-bar': true }); // => 'foo-bar' 
classNames({ 'foo-bar': false }); // => '' 
classNames({ foo: true }, { bar: true }); // => 'foo bar' 
classNames({ foo: true, bar: true }); // => 'foo bar' 
 
// lots of arguments of various types 
classNames('foo', { bar: true, duck: false }, 'baz', { quux: true }); // => 'foo bar baz quux' 
 
// other falsy values are just ignored 
classNames(null, false, 'bar', undefined, 0, 1, { baz: null }, ''); // => 'bar 1' 

源码解析

classNames 的实现非常简单,只有一个函数定义,接受任意参数类型和个数

  • 简单类型,直接加到集合
  • 数组,判断元素真假,加到集合
  • 对象,判断value真假,加到集合
var hasOwn = {}.hasOwnProperty;
    // 接收任意类型的参数和个数
	function classNames() {
	   // 用于存储解析过的 className,如 ['foo', 'bar']
		var classes = [];
       
       // 循环所有参数
		for (var i = 0; i < arguments.length; i++) {
			var arg = arguments[i];
			// 当前参数如果为 false,则跳过,再继续判断下一个
			if (!arg) continue;

			var argType = typeof arg;
          // 如果是 字符串或数值,直接加到集合
			if (argType === 'string' || argType === 'number') {
				classes.push(arg);
			// 如果是 数组,继续递归判断
			} else if (Array.isArray(arg)) {
				if (arg.length) {
					var inner = classNames.apply(null, arg);
					if (inner) {
						classes.push(inner);
					}
				}
			// 如果是对象
			} else if (argType === 'object') {
			    // 如果是原生object,并且value为真,则把 key 加入到集合
				if (arg.toString === Object.prototype.toString) {
					for (var key in arg) {
						if (hasOwn.call(arg, key) && arg[key]) {
							classes.push(key);
						}
					}
				// 有自定义 toString, 直接toString后加到集合
				} else {
					classes.push(arg.toString());
				}
			}
		}
        
       // 拼成字符中返回
		return classes.join(' ');
	}

总结

bemclassnames 都可以很好帮你管理你的 class 名称,区别在于,bem 更偏向于基于BEM规范的样式类生成,而 classnames 应用更加宽范,没有名称的规则限制,这里我推荐在组件库开发中,使用 bem,在业务开发中使用 classnames, 不过 vue 中不需要 classnames,框架默认支持。