如何自己实现一个原子化样式类库?

从最原初的CSS,再到 sass,less,stylus,等各种CSS预处理工具,再到现在的css-in-js, atom css,css在web发展的各个时期都在经历各种演变,最终的目的还是为了让css更加方便书写,开发更加简单,更加符合前端模块化,工程化的演变。到如今的原子化样式类.都是在向这一目标演进,而原子化样式提高了复用性,只不过有的人可能觉得演变来演变去,到最后都成了行内样式了。

但是,原子化样式类并未成为一个行内样式,因为其复用性要比行内样式高,行内样式只能针对于单一元素及其子元素起到作用,但是原子化样式类则是对所有引用到这个样式类的元素都有效果。虽然看上去最终样式写了一堆,但是都只是在重复的使用某一个样式而已,并没有增加样式的体积。所以我们只需要极少的样式类就能写出复杂的页面。

html 复制代码
<div class="h-fit p-3 my-3">
    testing testing
</div>

比如上面的代码,写经过一些原子化类库预处理之后,只会生成3个样式,并且还可以复用在其他元素或者是组件上,并不会导致只能应用在上面的div的疑虑。下面是最终生成的样式

css 复制代码
.h-fit {
    height: fit-content;
}

.p-3 {
    padding: 0.75rem;
}

.my-3 {
    margin-top: 0.75rem;
    margin-bottom: 0.75rem;
}

但是,虽然原子化样式类写起来很舒服,但是如果需要后期维护,那就不亚于上刑场了。主要是由于原子化样式类丢失了样式语义,从而失去了可阅读性。让不懂的人无异于在阅读天书.

既然说完了原子化样式类的优劣。那么,或许是该来实现一下如何写一个原子化样式类库了。让我们看看具体的原理到底是怎样的。

我们可以将 h-fit p-3 my-3 视作一系列规则或者说是某种语言,我们需要实现一个这个规则到css的转换器。我们可以将这段规则视为某种代码,然后去实现一个函数,参数的参数就是输入这些代码,输出则是具体的css.像这样:

ts 复制代码
generator(`h-fit p-3 my-3`)
// 然后函数则会生成css

当然,我们还得考虑一些border case,比如样式像 w-[300px] w-30% bg-[#fff] 这类样式类,其中的符号在css中是有特殊意义的,所以我们需要将这些符号做一下转义。使其变为普通字符。所以有如下代码

ts 复制代码
export const generatorClss = (className: string) => {
  for (let char of className) {
    if (['[', ']', "#", ":", "(", ")", ",", "/","!","%","@"].includes(char)) {
      className = className.replace(char, `\\${char}`)
    }
  }
  return `.${className}`
}

在将这些样式特殊字符转义的问题解决了,那接下来就是要考虑该如何实现样式的生成了。关于规则的匹配,我就使用正则。每一个规则是唯一的,不会出现重复的情况。所以设计一个下面的函数,用来处理规则的匹配以及后续的处理问题。

ts 复制代码
function variant() {
  let rules = new Map<RegExp, (...args: any[]) => string>()
  let appendRule = (rule: RegExp, cb: (...args: any[]) => string) => {
    rules.set(rule, cb)
  }
  let generator = (text: string) => {
    for (const [rule, gen] of rules) {
      let match = rule.exec(text)
      if (rule.test(text) && match && match[0] === text) {
        return variantMatch(text, rule, gen)
      }
    }
  }
  return {
    append: appendRule,
    generator,
    rules
  }
}

好了,准备工作完成,我们只需要不断的往rules这个map里面添加规则就行了。于是就有了如下的代码:

ts 复制代码
const { append } = variant()
append(/(m|margin)-\[(.+)\]/, (className, _, rule) => {
    return `
${generatorClss(className)}{
    margin:${rule};
}`
})

append(/(m|margin)(x|y)-(\d+)/, (className, _, direction: "x" | "y", size: `${number}`) => {
    const directionMap = {
        "x": `
margin-left: {size};
margin-right: {size};
`,
        "y": `
margin-top: {size};
margin-bottom:{size};
`
    }
    const marginSize = parseInt(size)
    return `
${generatorClss(className)}{
${template(directionMap[direction], {
        size: `${marginSize * 0.25}rem`
    })}
}`
})

这样就完成了关于margin的原子化样式规则的css生成了。还有上面的其他的规则,代码如下:

ts 复制代码
append(/(h|height)-(\d+)/, (className, rule) => {
  let width = parseInt(rule) * 0.25
  return `
${generatorClss(className)}{
  height:${width}rem;
}`
})
append(/(h|height)-(min|max|fit)/,(className,_,rule)=>{
  return `
${generatorClss(className)}{
  height: ${rule}-content;
}`
})

append(/(h|height)-(\d+\/\d+)/,(className,_,rule)=>{
  let result = (rule.split("/").map(Number) as number[]).reduce((a:number, b:number) => a / b).toFixed(6);
  return `
${generatorClss(className)}{
  height:${parseFloat(result)*100}%;
}`
})

其他的规则想必在有上面的代码示例之后,读者自己也能编写出来,在这里我就不继续展示代码该如何生成了. 我这里编写的原子化样式类库只作为一个学习示例,所以不去考虑扫描js,jsx,php,tsx,vue等文件的问题,只作为一个运行时的原子化样式类库。所以缩小问题范围,只考虑该如何获取到页面中的类以及生成。当然这里要考虑的问题就是原子化样式类每种样式类只生成一次,避免像自己写样式那样同样的样式重复写,需要提高复用率。所以会有一个去重问题。我们可以用hash table,即map来完成去重,将原子样式类作为key,如果有重复只会覆盖掉原有的,不会新增。

当然,也有更加简便的方法,也就是Set,Set也有去重的效果。下面是代码

ts 复制代码
const scanPage =()=>{
  const classes = new Set<string>();
  document.querySelectorAll("*[class]").forEach((ele)=>{
    ele.classList.forEach((cls)=>{
      classes.add(cls)
    })
  })
  return [...classes]
}

我们使用querySelectorAll方法获取所有有class属性的元素,将这些元素中的类全部都添加到Set当中。最终我们就得到了一个完全没有重复项的原子化样式类名的集合。

ts 复制代码
document.onreadystatechange = function(){
  if(document.readyState === "complete"){
    let styled = document.createElement("style")
     let atomClass = scanPage()
     console.debug(`当前使用了${atomClass.length}条规则`)
     styled.setAttribute("type","text/css")
     styled.textContent = variantGenerator(atomClass.join(" ")).replace(/\s/g,"")
     document.head.appendChild(styled)
     console.debug(`当前总计规则数为: ${globalVariant.rules.size}条规则`)
  }
}

在这里,我使用文档对象的onreadystatechange事件来处理生成原子化样式类任务。当页面当中的所有元素都加载完成的时候才开始生成。使用之前定义好的函数扫描整个网页,获取所有的类名。然后将其送入variantGenerator函数,进行生成。至此就完成了一个简单的原子化样式类库,并且是基于运行时的原子化样式类库。当然,实际的原子化样式类库更加复杂,还需要考虑css的兼容问题,以及各种方言,比如需要加-webkit-或者-moz-前缀。当某个样式在低版本css当中不支持,需要将这个样式转换为低版本的等价写法。如此种种。所以说,这只是一个用于套利原理性的样式库,如果读者受到了启发,希望你们能实现一个真正可用的样式库。

上面的代码都来自于下面的git仓库:

当然,这种字符串生成和我之前做过的ORM框架根据字段名生成SQL语句有异曲同工之妙,共同之处都在于字符串的拼接。显然,原子化样式类有更为复杂的生成模式。

祝大家玩的开心, have fun!

相关推荐
Fan_web9 小时前
jQuery——事件委托
开发语言·前端·javascript·css·jquery
安冬的码畜日常9 小时前
【CSS in Depth 2 精译_044】第七章 响应式设计概述
前端·css·css3·html5·响应式设计·响应式
赛男丨木子丿小喵9 小时前
visual studio2022添加新项中没有html和css
css·html·visual studio
太阳花ˉ12 小时前
html+css+js实现step进度条效果
javascript·css·html
懒羊羊大王呀13 小时前
CSS——属性值计算
前端·css
看到请催我学习15 小时前
如何实现两个标签页之间的通信
javascript·css·typescript·node.js·html5
昨天;明天。今天。17 小时前
案例-任务清单
前端·javascript·css
秋殇与星河19 小时前
CSS总结
前端·css
神之王楠20 小时前
如何通过js加载css和html
javascript·css·html
软件开发技术深度爱好者1 天前
用HTML5+CSS+JavaScript庆祝国庆
javascript·css·html5