从最原初的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!