大家好!
我叫拉马赞,是一名前端开发人员,同时也是一位热衷于从新的角度去审视网页开发中那些熟悉概念的人。
你们可能听说过函数式编程。这种编程范式的特点在于使用纯函数以及保持数据的不可变性。实际上,像Haskell、OCaml和Elixir这样的编程语言就是以函数式编程原则为基础设计的。而JavaScript、Python和C++等语言也支持这种编程方式,尽管它们并不完全受这些原则的约束。
但是,细心的读者可能会看到标题后问:“函数式编程固然不错,但Atomic CSS与它有什么关系呢?”现在我就来解答这个问题!
事实上,在Atomic CSS这种方法最初出现的时候,它还有一个其他的名字:函数式CSS。如今,有些人仍然会使用这个名称,以避免与其他具有相同名称的技术混淆。那么,为什么这种编写CSS的方法会被称作“函数式”呢?
这就是我在本文中试图回答的问题。首先,我会介绍函数式编程的基本原理;然后,我会结合函数式编程的概念来讲解Atomic CSS的基础知识,并通过一些简单的例子来说明如何利用Atomic CSS来解决样式设计中的各种问题。
在准备这篇文章的材料时,我参考了很多关于函数式编程的教程,比如这个教程,以及一些关于函数式CSS的教程。
那么,让我们开始吧!
先决条件
要理解这篇文章的内容,你只需要具备对HTML、CSS和JavaScript的基本了解即可。在文章中还会有一些例子会用到Atomic CSS框架mlut,不过你不需要掌握它的具体语法,因为我已经为这些例子提供了等效的CSS代码。
本文内容涵盖:
函数式编程的基本原理
函数式编程是一个涵盖范围非常广泛的领域,关于这一主题已经发表了许多篇内容复杂的文章,甚至还专门有一本学术期刊专门探讨这一领域的相关问题,其网址为:https://www.cambridge.org/core/journals/journal-of-functional-programming。因此,在我的这篇文章中,我只会重点探讨函数式编程的基本原理,并会尝试将这些原理与Atomic CSS中的相关概念进行类比分析。
这种方法的出发点是:在程序中,我们需要执行的所有操作都应该通过调用特定的函数及其组合来实现。
让我来概述一下我将用来解释这一方法核心概念的一些关键要素:
-
纯函数
-
不可变性
-
函数组合
纯函数
如果一个函数满足以下条件,那么它就被认为是纯函数:
-
对于相同的输入参数,总是返回相同的值;
-
不会产生任何副作用(即不会改变外部变量或数据的结构)。
下面有几个例子可以说明这一点:
let c = 10;
let s = 0;
// 纯函数
function pureSum(a,b) {
return a + b
}
// 非纯函数
function notPureSum (a) {
s = a + c
return s
}
第一个函数是纯函数,因为当我们使用相同的参数调用它时,总会得到相同的结果。此外,这个函数也不会修改任何全局变量,也不会改变对象的状态。
而第二个函数则不符合纯函数的定义:它改变了外部变量 `s`,并且在计算过程中使用了另一个可能会发生变化的外部变量 `c`。
使用纯函数可以让代码更加易于预测和维护。如果一个函数的参数会随时间变化,那么在调试和维护代码时就会遇到很多麻烦。
不可变性
不可变性是一种原则,它要求数据对象在创建之后就不能再被修改。如果要修改数据,就必须创建一个新的数据实例,然后才能对这个新实例进行操作。
乍一看,这种做法似乎会限制开发过程的灵活性,也会降低程序的运行速度。但实际上,如果所使用的语言或运行环境提供了针对不可变数据的优化机制,遵循这一原则反而可以帮助我们避免很多错误,并充分利用并行计算的能力。
这里有一个简单的例子:假设我们有一个 React 组件,用于显示待办列表中的任务。任务的状态是由一个对象来表示的。为了确保当用户修改任务信息时,React 能够正确地更新组件的界面,我们应该将新的状态作为参数传递给组件,而不是使用已经被修改过的旧对象。下面这个例子就展示了如何使用不可变对象来存储待办任务的状态:
import { useState } from "react";
function TodoItem() {
const [todo, setTodo] = useState({
text: "Write article",
done: false
});
const toggleDone = () => {
setTodo({
...todo,
done: !todo.done
});
};
return (
{todo.text} — {todo.done ? "✅" : "❌"}
);
}
export default TodoItem;
在这里,我们会看到点击按钮会导致待办事项的状态发生变化,进而引发组件的重新渲染。不过,我们也可以通过修改对象的结构来定义 `toggleDone()` 这个函数:
const toggleDone = () => {
todo.done = !todo.done;
setTodo(todo);
};
使用这样的事件处理方式,效果并不会如期出现,因为尽管对象本身已经发生了变化,但引用该对象的代码仍然没有改变。
函数组合
函数组合指的是将一个函数的输出作为另一个函数的输入。下面是一个例子:这个程序会将每个单词的第一个字母转换为大写:
function compose(f1, f2) {
return function (str) {
return f1(f2(str));
}
}
function makeFirstCapital(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
function upperEveryFirst(str) {
return str.split(' ').map(makeFirstCapital).join(' ');
}
function lower(str) {
return str.toLowerCase();
}
const capitalize = compose(upperEveryFirst, lower);
const string = 'this sTring should be Capitalized'
console.log(capitalize(string)) // 'This String Should Be Capitalized'
在这里,我们定义了 `compose(f1, f2)` 这个函数,它接收两个函数作为参数,并返回这两个函数的组合结果。接着,我们利用这个函数创建了 `capitalize()` 函数——通过组合 `lower()` 和 `upperEveryFirst()` 这两个函数,`capitalize()` 能够仅将每个单词的第一个字母转换为大写。首先执行 `lower()` 函数,将其返回的包含所有小写字母的字符串作为参数传递给 `upperEveryFirst()` 函数,后者才会将每个单词的第一个字母转换为大写。
在大型项目中,当需要进行复杂的计算时,这样的函数组合结构会变得更加复杂,其中的逻辑也会更加丰富。在这种情况下,这种编程方法有助于将庞大的处理流程分解成一系列相对简单、紧凑的函数,这些函数可以依次被执行。这样一来,代码的开发、重构和调试就会变得更容易。
函数式编程的原则在 Atomic CSS 中是如何得到体现的
现在我们已经对函数式编程有了一些了解,那么接下来就让我们试着回答这个问题:“Atomic CSS 与函数式编程有什么关系呢?”让我们从上述原则的角度出发,来分析这两种技术之间的联系。
什么是 Atomic CSS?
但首先,我们先来解释一下什么是 Atomic CSS。它是一种用于布局设计的样式编写方法,在这种方法中,我们会使用一些小巧的 Atomic CSS 规则,每个规则只执行一个特定的操作。这些类被称为“实用工具类”,它们通常会应用一个或多个 CSS 属性。
例如,在mlut框架中,Bgc-red这个选项对应于background-colour: red这一属性,而-Sz50p这个选项则同时对应着width: 50%和height: 50%这两个属性。
现代的Atomic CSS框架,比如mlut和Tailwind,都使用了所谓的JIT引擎。这种机制会根据你在标记中使用的样式工具来生成相应的CSS代码。
纯粹性
CSS的纯粹性取决于用于为元素设置样式的选择器,或者更具体地说,是哪些类被用来修饰这些元素。在简洁的CSS代码中,一个元素的行为应该完全由其在class属性中被指定的类来决定。这意味着,理想情况下,样式表中不应该包含像section或div > ul这样的选择器。
首先,这些规则往往过于笼统,因此在项目的某个部分可能会被违反或需要补充修改。所以当我们为特定的元素设置样式时,就必须时刻牢记这些规则,才能确保不会破坏其他部分的样式效果。
其次,这种写法会破坏每个特定元素的CSS纯粹性。举个例子,假设我们有如下代码:
<button class="greeting">Hello!</button>
<div class="wrapper">
<button class="greeting">>Hello!<>/button>
</div>
如果使用以下CSS样式:
.wrapper > .greeting {
background-color: green;
}
.greeting {
background-color: red;
}
那么结果就是:第一个按钮显示为红色,而嵌套在其中的按钮则显示为绿色。在这个简单的例子中,这种差异可能不太明显,但当项目结构变得复杂时,就会带来麻烦。
从这里我们可以看出,.greeting类所定义的样式效果实际上取决于该元素在页面中的位置。这就像一个函数:即使输入数据相同,但如果调用它的位置不同,输出结果也会不一样。
Atomic CSS可以帮助我们避免这种问题。在这种设计思路下,大多数情况下,样式只会应用于那些被明确指定了相应类的元素上。如果需要创建多个相同的元素,只需在每个元素的class属性中指定相同的类名即可。
同样的例子用mlut语言来写就是这样的:
<button class="Bgc-red">>Hello!<>/button>
<div>
<button class="Bgc-green">>Hello!<>/button>
</div>
JIT引擎会生成如下CSS代码:
.Bgc-red {
background-color: red;
}
.Bgc-green {
background-color: green;
}
由此可见,在这种设计方式中,元素的样式完全取决于它们被分配到的类名。
需要指出的是,mlut的语法允许我们使用一些偏离严格CSS纯粹性概念的方法。有时候,为了实现更复杂的视觉效果,这样做是必要的。
比如,如果我们想要创建一个卡片元素,当用户将鼠标悬停在卡片内的按钮上时,该按钮的背景颜色会发生变化,那么在mlut中我们需要编写相应的代码来实现这个功能。
<div class="-Ctx">
<button class="^:h_Bgc-red">>问候语</button>
</div>
CSS代码:
.-Ctx:hover .\^\:h_Bgc-red {
background-color: red;
}
ACSS中的不可变性
所谓CSS中的不可变性,指的是元素的样式不会被重新编写。在ACSS中,这意味着各种辅助函数通常不会相互修改彼此的样式。
例如,在BEM(块级元素修饰器)中,主要的样式是由相应的块级元素设定的,而修饰器则用于改变这些样式。而在那些使用组合选择器的设计方法中,这种样式修改会更为频繁发生,且表现得也不那么明显。
让我举个简单的例子。假设我们有一个产品卡片,它可以是默认状态,也可以是高亮显示的状态。在BEM中,它的实现方式如下:
<div class="product-card">>卡片1</div>
<div class="product-card product-card--selected">>卡片2<>/div>
<div class="product-card">>卡片3<>/div>
.product-card {
background-color: red;
padding: 5px;
}
.product-card--selected {
background-color: green;
}
在这个例子中,我们可以看到product-card这个类将卡片的默认背景颜色设置为红色。而为了标记被选中的卡片,我们需要添加一个修饰器类,将背景颜色从红色改为绿色。这种方式实际上就是重新编写了background-color属性,也就是对原有的样式进行了修改。
而在Atomic CSS中,这个问题得到了解决,因为各种辅助函数允许我们独立地设置CSS属性,并且可以在不进行任何修改的情况下应用新的样式。
如果使用mlut来实现这个例子,它的代码会看起来像这样:
<div class="P5 Bgc-red">>卡片1<>/div>
<div class="P5 Bgc-green">>卡片2<>/div>
<div class="P5 Bgc-red">>卡片3<>/div>
.P5 {
padding: 5px;
}
.Bgc-red {
background-color: red;
}
.Bgc-green {
background-color: green;
}
组合编程
函数式编程广泛运用了函数组合的概念。在Atomic CSS中,元素样式的设置也类似于函数组合。就像在函数式编程中,我们通过一系列简单函数的顺序执行来获得复杂的效果一样,在ACSS中,我们也可以利用一系列简单的辅助函数来实现复杂的样式效果。
举个例子,我将展示如何仅使用Atomic CSS来创建一个简单的笑脸图标:
<div class="-Sz150 Bgc-yellow Bdrd100p M-a Ps">
<div class="-Sz20p Bgc-gray Bdrd100p Ps-a T30p L20p"></div>
</div>
</div>
</div>
.-Sz150 {
width: 150px;
height: 150px;
}
.Bgc-yellow {
background-color: yellow;
}
.Bdrd100p {
border-radius: 100%;
}
.M-a {
margin: auto;
}
.Ps {
position: relative;
}
.-Sz20p {
width: 20%;
height: 20%;
}
.Bgc-gray {
background-color: gray;
}
.Ps-a {
position: absolute;
}
.T30p {
top: 30%;
}
.L20p {
left: 20%;
}
.R20p {
right: 20%;
}
.W50p {
width: 50%;
}
.H40p {
height: 40%;
}
.Bdb5;s;gray {
border-bottom: 5px solid gray;
}
.T40p {
top: 40%;
}
.L25p {
left: 25%;
}
我们的最终结果会呈现如下样子:

通过依次应用这些CSS样式,我们甚至创作出了一些小巧的艺术作品。
结论
总而言之,我认为Atomic CSS真正体现了函数式编程的基本原则。当然,这并不是字面意义上的体现,而是指它对前端开发人员和布局设计师来说具有实际意义。
我很乐意听取你们的补充意见或反对观点,阅读这些内容并思考它们会让我感到很有收获。
最后,我想说的是:请用全新的视角去审视那些熟悉的事物。像往常一样,祝大家在这段充满挑战的前端开发旅程中取得成功!