clsx 源码解析

classnames (opens new window) 类似的库

用于处理 html 的 class 拼接

但有以下优点

  1. 足够小 228B
  2. 足够快, classnames,至少 1 倍

clsx 与 classnames 性能测试对比

开发有性能要求,可以选择 clsx (opens new window)

另外对浏览器有要求,因为使用 Array.isArray,所以只支持 IE9+ 浏览器

# 源码初探

源码构建 (opens new window)过程使用自己开发的 Nodejs 脚本实现 🎉

并非是 rollup 或 webpack 一堆配置麻烦,自实现足够简洁

// @ts-check
const fs = require('fs');
const zlib = require('zlib');
const { minify } = require('terser');
const pkg = require('../package.json'); // Nodejs 内置支持读取 json 文件

// 构建前,没有 dist 目录,创建
if (!fs.existsSync('dist')) fs.mkdirSync('dist');

/**
 * @param {string} file
 * @param {string} source
 */
// 源码写入对应的文件
function write(file, source) {
	let isModule = !source.startsWith('!function');

  // 源码压缩
	let result = minify(source, {
		module: isModule,
		compress: true,
	});

  // 源码写入命名文件中
	fs.writeFileSync(file, result.code);
  // 测试压缩后源码字节大小
	console.log('~> "%s" (%d b)', file, zlib.gzipSync(result.code).byteLength);
}

// 读取源码
let input = fs.readFileSync('src/index.js', 'utf8');

// copy for ESM - 源码写入 package.json 中的 module 字段
// 源码默认是 ES module,所以直接写入
write(pkg.module, input);

// transform ESM -> CJS exports - 源码写入 package.json 中的 main 字段
// 源码非 CJS 模块,所以将 ES module 通过正则替换为 CJS 模块
write(pkg.main, input.replace('export function', 'function').replace(
	'export default clsx;',
	'module.exports = clsx;\n'
	+ 'module.exports.clsx = clsx;'
));

// transform ESM -> UMD exports ,将源码处理成通用模块,通过全局 clsx 调用或 在 AMD 模块中调用
input = input.replace('export function', 'function').replace('export default clsx;', 'return clsx.clsx=clsx, clsx;');
write(pkg.unpkg, '!function(global,factory){"object"==typeof exports&&"undefined"!=typeof module?module.exports=factory():"function"==typeof define&&define.amd?define(factory):global.clsx=factory()}(this,function(){' + input + '});');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

以上就是构建的全部过程,除了简洁还是简洁

如果 UMD 模块处于 use Stirct 模式下就更好了

# 源码深入

  1. 导出 clsx 模块
  2. 处理不同类型的 class
  3. 对于 falsy (opens new window) 一律忽略不处理

关键代码是

// 如果当前的 class 是 true,添加一个空格
// 然后再拼接后面的 class,结果 class 就是 "btn btn-primary xx"
str && (str += ' ')
str += x
1
2
3
4

# 导出 clsx 模块

所有导出都是 ESM

// 命名导出,通过 import {clsx} from ... 使用
export function clsx() {
	var i=0, tmp, x, str='';
  // 遍历参数,使用 toVal 处理,为 true 拼接 class,false 不处理
	while (i < arguments.length) {
		if (tmp = arguments[i++]) {
			if (x = toVal(tmp)) {
				str && (str += ' ');
				str += x
			}
		}
	}
	return str;
}
// 默认导出,通过 import clsx from '...' 使用
export default clsx;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 处理不同的类型的 class

clsx 支持字符串、数字、数组、对象,数组嵌套,不支持对象嵌套

对于对象的处理使用 for...in 不是特别友好

因为它会遍历对象原型上的属性

可以使用 hasOwnProperty 过滤判断对象本身的属性

function toVal(mix) {
	var k, y, str='';

  // 传入 string、number 类型 class 拼接,返回
	if (typeof mix === 'string' || typeof mix === 'number') {
		str += mix;
	} else if (typeof mix === 'object') {
    // 数组,则遍历处理,只处理子项为 true 值,处理完成后返回
		if (Array.isArray(mix)) {
			for (k=0; k < mix.length; k++) {
				if (mix[k]) {
          // 数组的子项是嵌套对象或数组时嵌套遍历处理
					if (y = toVal(mix[k])) {
						str && (str += ' ');
						str += y;
					}
				}
			}
		} else {
    // object,遍历对象的 k,k 索引的值为 true,将 k 进行 class 拼接,完成后返回
			for (k in mix) {
				if (mix[k]) {
					str && (str += ' ');
					str += k;
				}
			}
		}
	}

	return str;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# 关于依赖库 obj-str

class 拼接的关键代码参考了obj-str (opens new window)

obj-str 只实现了对象类型的 class 拼接

export default function (obj) {
	var k, cls='';
	for (k in obj) {
		if (obj[k]) {
			cls && (cls += ' ');
			cls += k;
		}
	}
	return cls;
}
1
2
3
4
5
6
7
8
9
10

# 小结

处理 html 中 class 拼接的核心在于将多个字符串使用空格分隔

可以将结果放到一个数组里面,使用 join(' ') 方法处理

但更推荐 clsx 的处理方式,足够小巧

对于现有库的改进提升性能或拥有自己的亮点,是一种不错的主意 🎉

扫一扫,微信中打开

微信二维码