前端原子化开发浅析本文探讨了前端开发中的原子化开发模式,包括原子化状态管理和原子化CSS样式。通过分析流行的库如jota
前端原子化开发浅析
前言
最近在整理前端一些新技术时,偶然发现一个新的概念——Atomic(原子化),众所周知,前端组件开发中非常重要的部分便是状态管理和 CSS 样式代码的编写。对于原子化,社区不仅有原子化状态管理方案,还有原子化的 CSS 方案,这就促使我进一步探索一番,因此本文找来了社区流行的库进行分析,而搜索发现这个“原子设计”的概念很早便由 Brad Frost 提出过。所以本文将对这种原子化的开发方式进行整理,希望大家能够体会到一些其中蕴含的思想和内在的演变规律。
原子化状态管理
前端框架中非常重要的一部分是状态管理,以 React 为例,大家比较熟悉的管理方式是如 Flux 这样的全局式管理方式,即 App 的所有状态都存储在全局的 stores 下,views 层通过传递 action 给 dispatcher 调用,触发 stores 的更新,进而触发 views 的更新:
因为这与 React 单向数据流的模式非常契合,所以非常受欢迎:
但这套数据流转的路径涉及了好几个概念,所以当你为了新增某一个全局状态,需要同时修改好几个地方的文件,编写诸多的模版代码,这些模版代码看起来无用,但是却必不可缺,比如在我们的应用中就需要改如下的代码:
1 app-state.ts 增加状态类型定义
2 app-store.ts 下增加状态 及状态的 update 方法
3 dispatcher.ts 下增加调用 appStore 新增方法的方法
4 view 增加调用 dispatcher.ts 下新增方法的方法
5 app.tsx 传递来自 app-store.ts 的新增状态,通过组件的 props 一级一级传递给 view 组件
可见,改一个状态还是挺复杂的。
对于原子化的状态管理方式,就要轻量许多。
如图,相比之前瀑布式的自上而下数据传递,原子化的状态管理提倡扁平管理,而且它支持定义的 atom
状态在任意组件可以访问,并可以触发视图的响应式更新。
这种方案非常轻量且高效。实际上,每个 atom
类似一个 useState
的 hook
,只不过是全局的 state,此外,由于通过采用分散扁平的模式,避免了状态一级一级由 props 传递,React 组件的性能理论上也会因此得到提升。
采用【原子化】状态管理方式创建一个全局状态将会非常简单,以社区较为流行的 jotai 库为例,只需要改动两个地方即可实现 flux 的效果。
// 1. 创建原子状态
const countAtom = atom(0)
// 2. 组件A使用
import { useAtom } from 'jotai'
const CountApp = () => {
const [count, setCount] = useAtom(countAtom)
return (
<>
<button onClick={() => setCount(pre => pre+1)}>+</button>
<button onClick={() => setCount(pre => pre-1)}>-</button>
<div>{count}</div>
<>
)
}
// 3. 组件B使用
const ResetCount = () => {
const setCount = useSetAtom(countAtom)
return <button onClick={() => setCount(0)}>reset</button>
}
而在一些特定场景下,还能够起到性能提升的作用,github.com/zhoushaokun…
export function AgeByAtom() {
const { age } = useAtomValue(selectAtom(
personAtom,
React.useCallback((atomValue) => atomValue.age, []), // 注意这里
),)
// heavy caculation
console.time('ageReRenderAtom')
const listToRender = new Array(1000000).fill('1').map(i => i)
console.timeEnd('ageReRenderAtom')
return <div>
<span>Age: {age}</span>
<span className="ml-4">Other: {listToRender.length}</span>
</div>
}
上述代码中通过 jotai 提供的 selectAtom
选取 personAtom 下的 age 属性,当 personAtom.age 不变化时,此组件就不会刷新,从而提供组件的性能。
原子像积木一样,可以组合使用形成新的“大原子”,因此也更加灵活可复用。如下,atom 的构造函数支持不同类型参数,由此组合的“大原子”也不一样。下面的 read write 是一个函数参数,所以一个 atom 可以派生出 compouted 计算属性、dispatch 方法等的数据。
// 独立 atom
function atom<Value>(initialValue: Value): PrimitiveAtom<Value>
// 只读 atom
function atom<Value>(read: (get: Getter) => Value): Atom<Value>
// 可读写 derived atom
function atom<Value, Args extends unknown[], Result>(
read: (get: Getter) => Value,
write: (get: Getter, set: Setter, ...args: Args) => Result,
): WritableAtom<Value, Args, Result>
// 只写 atom
function atom<Value, Args extends unknown[], Result>(
read: Value,
write: (get: Getter, set: Setter, ...args: Args) => Result,
): WritableAtom<Value, Args, Result>
社区这种原子化的状态管理库有很多:
总结:
(1)借助框架 hook 的能力,jotai 或者 recoil 的库可以实现较为灵活扁平的状态管理,这样做轻量且高效,但是可能由于过于灵活,会容易导致代码组织混乱,维护成本高,因此需要额外增加一些规则约束才能应用于大型应用中,类 Flux 的框架虽然需要很多模版代码但是每块代码各司其职,新人上手维护也会比较容易。
(2)相比 Flux 全局式的 store,jotai 提供了一种途径将状态逻辑以更简单的方式进行拆分,并易于扩展和复用。
参考:
原子化 css 样式
与状态管理 flux
模式需要拆分更轻量的原子化相反,原子化 css 样式提倡需要将锁碎的 css 代码形成为一个个原子 class。
css 样式代码编写非常难受的一点,是要编写两个地方的代码——JSX 和 css 样式代码,有时忘记改 JSX 代码 className,有时发现改了 css 样式却不生效,由此社区中出现了一种解决方案—— 原子化 css 样式。
以较为流行的 tailwind 为例,它实质上是一个 css 的工具集,内部预设了丰富的原子化 css class,以达到只用组合各种原子化的 css 类名即只调整 jsx 下 className 代码,而不用编写 css 即可完成 ui 样式开发。
有点类似 bootstrap 的思路,但是 tailwind 的主要特性就是不用写 css 样式代码,只依赖原子化的 css class,显然 tailwind “粒度” 比 bootstrap 会更细一点。
如下,只需要在 HTML 代码中叠加一连串原子 css 类名:
<div class="pt-8 text-base font-semibold leading-7">
<p class="text-gray-900">Want to dig deeper into Tailwind?</p>
<p>
<a href="https://tailwindcss.com/docs" class="text-black hover:text-sky-600">Read the docs →</a>
</p>
</div>
这种预设的类名,相当于牺牲了直接编写 css 的灵活度,达到了只用在 JSX(HTML)下编写代码的目标。
此外,稍微深入研究,发现这样的写法还能带以下的好处:
- 样式排版等设计的标准化
比如一些尺寸和颜色,这些本不需要太灵活设置的(很多时候我们设置都不知道用什么颜色或者尺寸),预设类名正好可以规避“选择困难症”,减少了 css 文件中任意设置大小颜色导致应用风格不统一的现象。
如下 tailwind 提供了一些容器的特定大小:
也提供了一些预设的色彩:
2 主题定义,dark 模式支持会更更加简单易行:
<div className="font-sans p-4 dark:bg-slate-900 h-svh">
<ComponentForContext />
<ComponentForAtom />
</div>
如上只需要将 dark 下的样式加入 dark:
的前缀,即可实现暗黑样式。
3 减少重复代码,复用样式
不同的 UI 实现可以只用排列组合不同的 css class,而不需要写重复的样式代码。css 缺乏模块化的原生支持(虽然一些 css 的预加载库如 sass,会提供 css 模块化的支持),非常容易出现重复的样式代码,而是在 tailwind 下则从根源避免这个问题。
如此应用下的所有的 css 样式代码将得到极大精简,因为你只需要保留所必需的预设样式集合相关代码。
理论上这样做也可以大大减少 css 样式代码文件的大小,从而提供项目页面的加载速度,这适用于一些 css 代码比较多 而且首屏速度的瓶颈就在于 css 的样式加载的应用。
此外,原子化方案也非常适合 css 样式代码的移植:
如下是 v0dev 生成的代码,由于 tailwind
可复用的特性,大模型输出的相关代码将非常易于移植。 如果你的项目使用的正好也是 tailwind,那么里边包含的 css 样式是几乎可以立刻使用的,而不需要做太多的修改,这可以直接节省写样式代码的时间。
往常,我们是要先在网上找一些 css 效果,然后花时间魔改其 css 代码使在我们生效,而在这一点上, tailwind 的原子化 css 方案可以为我们提供了便利。
<div className="flex items-center border-b">
<button className="flex-1 py-2 text-center border-r">Changes</button>
<button className="flex-1 py-2 text-center bg-blue-500 text-white">History</button>
</div>
<div className="p-4 border-b">
<input type="text" placeholder="Select branch to compare..." className="w-full p-2 border rounded" />
</div>
但是随着样式越来越复杂,会发现一些 className 会非常长,此时 tailwind 也提供一种方案将一些常用的 class 提取出来:
// jsx
<!-- Before extracting a custom class -->
<button class="py-2 px-5 bg-violet-500 text-white font-semibold rounded-full shadow-md hover:bg-violet-700 focus:outline-none focus:ring focus:ring-violet-400 focus:ring-opacity-75">
Save changes
</button>
<!-- After extracting a custom class -->
<button class="btn-primary">
Save changes
</button>
// css
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer components {
.btn-primary {
@apply py-2 px-5 bg-violet-500 text-white font-semibold rounded-full shadow-md hover:bg-violet-700 focus:outline-none focus:ring focus:ring-violet-400 focus:ring-opacity-75;
}
}
这样保证框架的灵活性,可以足够小巧,也能够在复杂场景下保证可读性。直接给后续维护人员一个意义明确的 class 类名,比一大长串原子 css 类名要容易理解得多。
参考:Tailwind Play (tailwindcss.com)
Headless UI
既然 css 样式可以原子化可以如上支持组合为一个意义明确的 class ,而原子化的状态在某些固定场合也适合做一些封装,比如 select、popover 等等。
社区中如 headless UI 这样的库便提供了这样的方案,它提供了一系列组件集合,与普通的 UI 组件库相比,这些 headless UI 组件轻样式代码,只包含了组件的数据状态逻辑,样式代码由调用方自定义。
- 注意这里的 HeadlessUI 不应该特制这个库,Headless UI 广义上应该是指只负责状态逻辑相关的组件集合,不关注样式,并对自定义样式编写开放。
这样做非常利于用户在此基础上基于业务进行扩展,封装各式各样属于自己风格的组件库。
比如笔者曾经开发了应用下的 pop 弹层,就是基于 popper.js 这个库开发出来的,而它实际上也只是一个 js 库,但对 css 自定义样式开放。
原子设计
《原子设计(Atomic Design)》是一本书,由著名设计师布拉德·弗罗斯特(Brad Frost)所著,书中认为原子设计一种关于网页设计和用户界面设计的方法论。它将设计系统分解为五个层次,从最基本的元素到完整的页面布局,以实现更高效和一致的设计过程。以下是原子设计的主要层次:
- 原子(Atoms):这是设计系统中最基本的构建块,包括颜色、字体样式、图标和按钮等。
- 分子(Molecules):由原子组成的更小的组件,例如表单、导航栏或卡片。
- 有机体(Organisms):由分子组合而成的更大的组件,通常包含多个分子,如页头、页脚、侧边栏等。
- 模板(Templates):页面的框架,定义了页面的布局和结构,但不包含具体内容。
- 页面(Pages):实际的网页,包含模板和有机体,展示具体的内容和交互。
原子设计核心思想就是将设计系统化,使得设计元素可以在整个项目中重复使用,从而使页面整体看起来也会更加和谐统一,这种方法论鼓励设计师从最小的元素开始构建,从而逐步构建出更复杂的界面组件,具体而言:
- 确定设计风格,包括色彩方案、字体方案等
- 设计基础的UI元素,如按钮、输入框、图标、颜色和字体样式等。
- 将原子元素组合成更复杂的组件,如表单、导航栏、列表等。
- 将分子组件进一步组合,形成更大的模块,如页头、页脚、侧边栏等。
- 设计页面的布局和结构,创建模板以展示页面的基本框架。
- …
在设计师的角度,每个设计单元也是需要可分的,从小块组成大块,与其最契合的便是 web 网页的 UI 代码编写。而在前端状态管理的角度,也是需要状态库可分,即能够小到原子,也能够大到 headlessUI,再大就是组件、页面等等。
虽说《原子设计》是设计相关的,但是在状态管理、css 样式编写的任务下有异曲同工之处,它们的发展趋势都是具备提供更多抽象层次的能力。
相比于我们之前所接触的组件库,比如 antd、bootstrap、element UI 等,近些年社区出现的原子化方案,为组件开发提供更多可分点,将代码逻辑切分的更加精细合理。比如这样做的好处是可以提高程序的复用性、减少代码体积(如 tailwind 方案)、提高程序性能(Jotai)。
总结
(1)抽象层次的增多为代码复用提供更多途径,但是随之而来的是额外的学习成本。
多抽象一层就意味着多了一层的学习成本,除非如 W3C 原来就有这样的 tailwind 原子化方案,大家好歹可以站在一个起跑线,否则强行制定的规则始终会让开发者望而却步。
(2)反观组件开发习惯的演变,其实可以发现是耦合与解耦纠缠的过程。
最开始写前端组件代码是 html + jquery + css,在较大项目下确实难以同时兼顾三处代码,后来 jsx 的方式出现,让我们可以只用关注 jsx+css,但是 jsx 中的状态逻辑耦合 html 会导致一些逻辑代码难以复用,后来框架 hook 方式出现,为逻辑拆分提供技术基础。css 也有需要直接在 html 编写的诉求,所以 tailwind 提供了预设集合实现这一目标。
JSX 将 state 逻辑和 HTML 放在一起,为前端开发视图编写提供巨大便利,但是 CSS 还是独立的,所以前端开发不想写 CSS 的原因就是要改多个地方,增加一个 css 类名,还需要 JSX 下修改组件的 className 才能生效。
一方面 state 逻辑和 HTML 放在一起,上述在 React hook 诞生之前,让一些状态逻辑很难复用,而 hook 出现之后,提供了一种抽离 state 逻辑途径:
而 tailwind 的原子化 css 方案提供了一种可以仅在 HTML 下即可完成 UI 代码编写:
(3)新方案的出现离不开底层技术的进步。
原子状态管理能够实现,得益于 React 框架在 16.8 之后提出的 hook 方案,随着 hook 的成熟,才有了如今的 jotai 等的原子化状态管理。另外一个题外例子,浏览器对于 js 模块化的支持,也由此催生出了如 vite 等的方案,它利用了浏览器原生的能力,从而实现优秀的运行效果。tarui 则是基于最近比较流行的 Rust 语言实现应用的 backend,并借助 webview 实现 front end,从而避免 electron 这样的超大安装包,例如 github.com/gitbutlerap… 是一个基于 taiui 实现的 git 客户端,它的安装包只有15M。
参考
- 原子化状态管理:리액트 상태 관리 가이드 | Stevy's wavyLog
- 《原子化设计》——github.com/bradfrost/a…
- jotai 抽离组件经验——使用 Jotai 抽离组件状态的经验 - innei
- jotai: jotai.org/
- Remix: Quick Start (5m) | Remix
- tailwind: tailwindcss.com/
- www.jianshu.com/p/13e87bf4f…
- Atomic Design 原子设计理论精华总结 - 简书 (jianshu.com)
- Blueprint – Documentation (blueprintjs.com)
转载自:https://juejin.cn/post/7408851018501357622