小程序样式拼接组合--classnames

1,745 阅读2分钟

存在的问题

作为一个前端的切图仔,编写样式也是我们日常的基操,但是难免会出现需要动态改变元素样式的情况,在小程序中动态控制样式可能会是下面的代码,以一个基础的按钮组件为示例:

    <view class="l-btn 
        {{ 'l-btn-' + size }} 
        {{ 'l-btn-' + type }} 
        {{ 'l-btn-' + shape }} 
        {{plain?'l-btn-plain':''}} 
        {{ disabled ? 'l-btn-disabled' : ''}} 
        l-class" />

为了通过 sizetypeshape 等props去控制一个按钮的样式,导致代码中使用了很多的双花括号去拼接类名,类名较多的情况会使得一个元素的样式混乱不堪,找不到所要修改的类名。

如何解决

小程序作为一个较新的技术,它所出现的一些问题在其他的技术里面都会找到类似的问题及解决方案,我们不妨看下react中是如何解决这个问题的。

开发react是经常会用到一个npm包classnames--一个工具函数,可以帮助我们却拼接所有的样式,示例代码如下:

var classNames = require('classnames');

class Button extends React.Component {
  // ...
  render () {
    var btnClass = classNames({
      btn: true,
      'btn-pressed': this.state.isPressed,
      'btn-over': !this.state.isPressed && this.state.isHovered
    });
    return <button className={btnClass}>{this.props.label}</button>;
  }
}

我们可以借鉴相同的思路在小程序中实现一个类似的工具函数,去帮助我们解决这个问题。

解决方案

因为这个工具函数不涉及到界面上的一些交互,只是对输入的变量做一个处理,所以可以考虑使用wxs去实现,也可以提升页面的性能,首先我们要明确这个函数所要接受的参数类型,分别为数字、字符串、数组以及对象。

首先我们创建一个名为classname的数组用于存储类名。

  • number、string: 直接添加push到classname中。
  • array:将classname与array进行拼接。
  • object: 遍历对对象,将value为true的key添加到到classname中。

明确了以上的规则,我们可以写出一下的wxs取出对应的类名。

function classNames() {
    var classes = []

    for (var i = 0; i < arguments.length; i++) {
        var arg = arguments[i]
        if (!arg) continue

        var argType = typeof arg

        if (argType === "string" || argType === "number") {
            classes.push(arg)
        } else if (Array.isArray(arg)) {
            if (arg.length) {
                classes = classes.concat(arg)
            }
        } else if (argType === "object") {
             Object.keys(arg).forEach(function (key) {
                if (arg[key]) {
                    classes.push(key)
                }
            })
        }
    }

    return classes.join(" ")
}

module.exports = classNames

本以为大功告成了?还是我太年轻了,控制台直接抛出了两条错误,ArrayObject这两个js内置对象在wxs中根本不能使用。

image.png

image.png

既然不存在,那我们就造一个,当然内置的对象我们是不可能造出来的,但是我们要使用的方法还是可以去模拟实现的。

isArray我们可以通过判断当前参数的constructor是否等于Array来判断是否是一个数组。

function isArray(array) {
    return array && array.constructor === "Array"
}

keys方法实现就比较麻烦一点,我们可以先把对象转为字符串,然后去正则匹配替换,最终获取到一个key的数组。

function keys(obj) {
    return JSON.stringify(obj)
        .replace(REGEXP, "")
        .split(",")
        .map(function (item) {
            return item.split(":")[0]
        })
}

完成这两个替代方法之后我们对之前代码中的方法进行替换即可,最终代码如下:

function isArray(array) {
    return array && array.constructor === "Array"
}

var REGEXP = getRegExp('{|}|"', "g")

function keys(obj) {
    return JSON.stringify(obj)
        .replace(REGEXP, "")
        .split(",")
        .map(function (item) {
            return item.split(":")[0]
        })
}

function classNames() {
    var classes = []

    for (var i = 0; i < arguments.length; i++) {
        var arg = arguments[i]
        if (!arg) continue

        var argType = typeof arg

        Object.keys({})

        if (argType === "string" || argType === "number") {
            classes.push(arg)
        } else if (isArray(arg)) {
            if (arg.length) {
              classes = classes.concat(arg)
            }
        } else if (argType === "object") {
            keys(arg).forEach(function (key) {
                if (arg[key]) {
                    classes.push(key)
                }
            })
        }
    }

    return classes.join(" ")
}

module.exports = classNames

有了这个工具函数之后,我们就可以愉快的在wxml里面写样式了。

<wxs module="classname" src="./index.wxs" />
<view class="container {{classname(1, ['c'],'less', {a: true, b: 1})}}">
  111
</view>

总结

虽然我们用wxs解决了上述问题,但是在对象里面下列的写法是不支持的。

{
    'a-b': true
}

{
    [`l-${a}`]: true
}

如果有上述需求的同学可以在js中尝试实现类似的方法,可以通过组件的observer函数动态生成样式绑定也是可以的。