图片起源:https://unsplash.com
本文作者:iwyvi
背景
随着业务的倒退,一些代码逻辑可能同时在多个我的项目中应用,为了防止每次应用和更新都要复制粘贴代码,结构一个组件库就非常有必要了。构建组件库有很多须要思考的方面,本文次要探讨在 React 生态下,如何抉择一种适宜组件库的 CSS 款式计划。
目前开发一个浏览器中运行的我的项目,能够抉择的款式计划依据写法次要分为三种:第一种是惯例 CSS(regular CSS),即原生 CSS 和各种预处理语言;第二种是在 JS 侧写款式的 CSS in JS 计划,例如 styled-components;第三种是在 HTML 中写工具类,由 CSS 框架生成对应款式的计划,例如 Tailwind CSS
。
然而当咱们构建组件库时,思考问题的角度和一般我的项目可能会不太一样,岂但须要思考开发体验,同时也要关照到使用者的感触。因而,本文不从写法层面对 CSS 款式计划进行剖析,而是从组件库的开发角度来探讨以下两个问题:
- CSS 与 JS 的关联形式是什么,即组件如何应用款式,以及 CSS 如何参加打包。
- 在不同的关联形式下,组件库如何解决款式命名抵触。
计划剖析
在 React 生态中,没有对立的款式治理计划,因而如何解决 CSS 有多种解决方案,不同的计划也有着各自的优缺点。
结构组件库时,咱们一方面心愿在开发时可能写起来更简略,另一方面也心愿应用时能更不便。CSS 作为组件库必不可少的一部分,抉择适合的款式计划,会影响到后续开发和应用的体验。
CSS 与 JS 关联形式
首先咱们从 CSS 与 JS 的关联形式说起。不同的 CSS 与 JS 关联形式,有着不同的款式引入办法,同时在按需加载、性能和 SSR 反对等方面也有各自的个性。
本文将组件库应用 CSS 的计划分为以下三种类型:
- 款式和逻辑拆散。组件的 CSS 和 JS 在代码层面拆散,JS 里不引入款式文件,在组件库打包时别离生成独立的逻辑和款式文件。对于组件库的使用者来说,增加一个组件,须要别离引入组件代码和 CSS 文件。应用这种计划的组件库有 Ant Design、Zent 等。
-
款式和逻辑联合。将组件的 JS 和 CSS 打包在一起,最终只输入 JS 文件。应用时只须要引入组件就能够间接应用。这种计划目前次要有以下两种种实现模式:
- 将 CSS 写在 JS 里。例如应用 styled-components, Emotion 等 CSS in JS 计划。代表组件库有 MUI、Mantine 等。
- 写代码时仍然应用惯例 CSS,组件内
import
款式文件,通过打包工具将 CSS 打进 JS 里。例如应用webpack
配合style-loader
。基于这种计划的组件(库)有 react-mobile-picker、Angular Material
等。
- 款式和逻辑关联。组件的 JS 和 CSS 在代码层面拆散,打包后生成独立的逻辑和款式文件,然而组件内会间接援用款式文件,且打包后果中保留对应的
import
语句。应用这种计划的有 Semi Design、React Spectrum、Ant Design Mobile 5.0。
这几种计划各有优劣,接下来本文将对其粗疏剖析:
款式和逻辑拆散
这种 CSS 组织计划在组件库的构建中最为常见,各个框架中都有大量的组件库应用这种模式。CSS 写在独自的款式文件中,组件的 JS 不间接引入 CSS,而在应用组件库时须要别离引入组件和款式。
应用这个计划的组件库有一个较为显著的特点,他们的装置教程中都会让用户自行引入一个或多个 CSS 文件,而且通常来说,这个 CSS 文件会蕴含整个组件库 所有 组件的款式。
这种计划的长处有:
- 适用性宽泛,能够反对组件库使用者的各种开发环境。
- 不限度组件库的技术栈,同一套款式能够用在基于多个框架的组件库上。
- 无需思考对 SSR(服务端渲染)的反对,对外提供的是 CSS 文件,因而 SSR 流程齐全交给组件库的使用者管制。
- 能够间接对外提供
less
、sass
等源文件,便于内部笼罩变量,实现主题定制或换肤等性能。
然而这种计划也有一些问题:
- 须要使用者手动引入款式文件。如果间接引入了残缺的 CSS 文件,而在理论应用中并没有用到组件库里的全副组件,就会造成一些无用的款式被打包进我的项目中。
- 让组件库反对 CSS 按需引入的性能会比较复杂,既须要组件库的开发者在打包流程和产物上进行解决,又须要使用者依照肯定规定引入款式文件。首先组件库开发者须要定一套款式文件的目录组织标准,使其能在打包流程中反对以组件为单位打包款式文件,之后使用者就能够按需手动引入对应组件的款式文件。对于具备特定目录组织标准的组件库,目前曾经有插件能够在编译阶段辅助生成引入款式的
import
语句,例如babel-plugin-import
、unplugin-vue-components
等。 - 如果组件库外部的组件存在援用关系,为了实现按需引入,打包进去的组件的款式可能会存在冗余。
款式和逻辑联合
这种计划中,CSS 以字符串或者对象的模式存在 JS 里,而且通常打包后的代码里会带一个用于挂载款式的 runtime。
这种计划具备以下长处:
- 不须要使用者独自引入款式文件,只须要
import
组件即可应用 - 人造反对按需加载,每个组件只须要解决本人的款式即可
然而同样这个计划也并不完满:
- 须要带一个 runtime,可能增大代码体积,带来性能影响。
- 相较于独自的 CSS 文件,此计划的款式都在 JS 中,可能无奈充分利用到浏览器缓存。
- 对 SSR 反对须要具体实现计划提供的能力,这一点在后文中会具体阐明。
这种计划次要有以下两种实现模式:
CSS in JS
CSS in JS 是一种与惯例 CSS 文件不同的款式计划,多用于 React 生态,解决了一些应用惯例 CSS 时存在的痛点,例如命名抵触、款式冗余等问题。
现在 CSS in JS 框架百花齐放,外部也呈现了有运行时和零运行时(zero-runtime)两个类别。styled-component
和 emotion
这一类属于有运行时,款式编写、变更和挂载都是在 JS 中进行的,框架会提供对应的 runtime 来解决这些工作。linaria
和 vanilla-extract
这一类属于零运行时,他们在写法上与有运行时框架相似,然而须要配置编译流程,通过编译当前将输入规范 CSS。
因为本节探讨的是款式和逻辑联合的计划,因而接下来提到的 CSS in JS 均指有运行时框架。
引入 runtime 意味着带来更多的 JS 代码,因而 CSS in JS 相比惯例 CSS 肯定存在着性能差别。Real-world CSS vs. CSS-in-JS performance comparison 这篇文章从用户体验的角度剖析了 styled-component
和 linaria
的性能差别(由上文可知 linaria
为零运行时的 CSS in JS 框架,即它能够代表惯例 CSS 的性能),得出了惯例 CSS 在各个方面的性能均优于 CSS in JS 的论断。
除了性能问题,因为款式的注入流程是由 CSS in JS 框架提供的 runtime 执行的,所以 SSR 流程也须要框架进行额定解决,好在目前简直所有支流的 CSS in JS 框架都提供了 SSR 的反对。
目前来看,大部分 CSS in JS 框架都须要使用者在服务端渲染流程中增加额定的款式收集和插入流程,能力胜利用上 SSR。多数框架例如 emotion
提供了一种无需额定配置的 SSR 反对计划,在服务端渲染时,组件的款式会以内联 style
标签的模式放到组件 DOM 的后面。然而这种计划也存在一些问题:当同一个组件在页面中被屡次应用时,渲染后的 HTML 里会蕴含多份反复的款式;此外,因为插入了额定的 style
标签,会影响到 :nth-child()
这一类选择器。
为什么在 SSR
中,这个额定流程无奈防止呢?服务端的一次渲染能够认为是调用了一次 ReactDOMServer
的 renderToString
办法,然而这个办法并没有为外部组件提供感知渲染状态的能力。对于一个组件来说,如果不记录本人是否被渲染过,就只能采取相似 emotion
的零配置计划,组件在每次渲染时都带上本人的款式;然而如果组件通过一个全局变量来记录本人是否被渲染过,如果没有渲染过就插入款式,曾经渲染过就不插入款式,不难发现组件无奈辨别服务端的屡次渲染,因为用来记录状态的全局变量在服务端始终是同一个,为了组件标记本人是否被渲染的状态在每一轮渲染中刷新,须要在每次渲染时创立一个变量来存储这个状态,而上述这些行为,就是上文提到的额定款式收集流程。
不过也正是因为增加了款式收集流程,CSS in JS 的计划大多都反对提取要害款式(Critical CSS),能够在 SSR 时减小首屏申请大小,这也是它的一个劣势。
将 CSS 打包到 JS 中
这种计划个别是通过打包工具配合相应的插件将惯例 CSS 和 JS 间接打包到一起,例如 webpack
+ style-loader
或 rollup
+ rollup-plugin-styles
。
通常这些插件引入的 runtime 均有 DOM 操作,会在 SSR
阶段报错或者什么都不做,等 CSR
阶段才真正注入款式,因而无奈反对 SSR
。
若要反对 SSR
也并非齐全没有抉择,webpack
生态里的 isomorphic-style-loader 就提供了 SSR
反对,在性能上根本等同于 style-loader
。然而它的实现形式和成果与 CSS in JS 计划比拟相似,在开发时须要给组件包一个高阶组件来加载款式,同样在 SSR
阶段须要通过其提供的办法收集和注入款式。
在 React 生态中,很少有组件库采纳这种款式计划,只有多数单组件我的项目应用这种打包计划。起因一方面在于这种计划难以提供 SSR
反对,另一方面既然曾经写了惯例 CSS,不如间接导出文件让使用者自行处理。
然而在一些非 React 生态中,应用这个计划结构的组件库还是比拟多的,因为他们的支流开发工具提供了蕴含打包和开发在内的一整套解决方案,而 React 生态下百花齐放,没有对立的开发工具,因而也没有对立的款式解决方案。
款式和逻辑关联
这种计划的开发流程与款式和逻辑拆散计划相似,次要区别在于输入后果里间接保留了引入款式文件的 import
语句,如果使用者的我的项目能正确处理 CSS 文件,那么就能够做到只引入组件即可应用。且这种计划同样也能反对按需加载,不须要引入一个大而全的 CSS 文件。
然而这种计划也有一些缺点:
- 对组件库的开发者来说,如果应用了预处理语言,打包编译的流程会更加简单,须要让组件最终产物的
import
语句正确关联通过编译的 CSS 文件。 - 对使用者的开发配置有肯定要求,须要能正确处理由组件库内的代码引入的 CSS 文件(例如在
webpack
下配置对应 loader)。如果须要反对SSR
,还须要批改打包工具的配置,让组件库的文件也参加到构建中,避免出现 CSS 文件在 node 端间接执行导致渲染出错。
总结
款式和逻辑拆散 | 款式和逻辑联合 | 款式和逻辑关联 | |
---|---|---|---|
开发打包流程 | 中等 | 简略 | 简单 |
输入文件 | JS 文件和 CSS 文件 | JS 文件 | JS 文件和 CSS 文件 |
应用办法 | 别离引入 JS 和 CSS | 只引入 JS | 只引入 JS |
按需加载 | 须要额定反对 | 反对 | 反对 |
性能影响 | 无 | 带额定 runtime,可能有影响 | 无 |
SSR | 反对 | 须要额定反对(局部计划不反对) | 反对(可能须要使用者调整配置) |
反对写法 | 惯例 CSS / 零运行时 CSS in JS | 惯例 CSS / CSS in JS | 惯例 CSS / 零运行时 CSS in JS |
要害款式提取 | 自行处理 | 反对 | 自行处理 |
组件库款式命名抵触解决
解决款式命名抵触也是构建一个组件库须要思考的问题,开发者总是心愿能应用更简略的名字,同时也不心愿呈现命名抵触。
目前在 React 生态下,常见的创立款式命名空间的计划以下几种:
约定命名规定
约定命名规定即在整个组件库恪守一个人为约定的命名规定,例如 BEM 标准或对立给所有选择器名增加前缀等。这种计划的益处是不须要调整打包计划,然而毛病在于规定全凭人为约定,在开发时要依附开发者的盲目,当多人保护时可能会比拟麻烦;此外为了达成约定的命名规定,可能还须要写一些样板代码来生成符合规范的名称,比拟消耗精力。
CSS Modules
CSS Modules 是解决命名空间问题的一种计划,它能够基于指定的规定生成选择器名称,无需开发者恪守严格的标准,同时也防止对全局款式造成净化。
以下是一个简略的例子,原始代码是这样的:
.test {color: red;}
import styles from 'index.less';
// ...
<div className={styles.test} />
通过转换后,成为了这样:
._xxxxxx {color: red;}
var modules_xxx = {"test":"_xxxxxx"};
// ...
<div className={modules_xxx['test']} />
通过对选择器增加 hash 值等办法,使选择器不会与其余中央产生抵触。
然而当咱们用 CSS Modules 开发组件库时,也须要思考这些问题:
- 应用 CSS Modules 须要对组件库的编译流程进行肯定解决。如果想采纳款式和逻辑拆散的打包计划,须要在打包进去的代码中移除对款式文件的援用语句,仅保留选择器名称转换的数据;如果应用款式和逻辑关联的计划,须要在保留选择器名称转换的同时,正确引入通过编译后的款式文件。
- 因为无奈保障使用者的开发环境也反对 CSS Modules,因而无奈将款式的源文件间接对外提供。
- 因为生成出的选择器名称不稳固,可能会常常变动,对于组件库的使用者来说,在外层不能对组件款式进行笼罩。
CSS in JS
是的,CSS in JS 计划又呈现了,因为这个计划在诞生之初,就无意解决 class 的命名问题。因为选择器名称都是动静生成的,所以开发时不须要恪守命名标准,也无需思考命名抵触。
以 Emotion
为例:
import styled from '@emotion/styled';
const Test = styled.div`
color: red;
`;
// ...
<Test />
在浏览器上运行时,实在的 DOM 被渲染成:
<style data-emotion="css">.css-1vdv3ej{color:red;}</style>
<!-- ... -->
<div class="css-1vdv3ej"></div>
上文咱们提到,CSS in JS 计划分为有运行时和零运行时两种类型,其中零运行时的计划最终通过编译会输入带有随机选择器命名的 CSS 文件,这个成果相似 CSS Modules。例如 vanilla-extract
就称本人为“CSS Modules-in-TypeScript”。因而零运行时的 CSS in JS 计划能够用在所有反对惯例 CSS 的计划中。
选型思考
关联计划抉择
抉择一种款式计划,首先确定 CSS 与 JS 的关联形式,之后再思考如何解决款式命名。
如果打算构建一个反对多框架的组件库,最优先思考的打包计划是款式和逻辑拆散。在这种计划下,产出一份根底款式,就能够在多框架的组件库里共用,不须要在各个框架中别离解决,适用性最宽泛。
然而如果仅须要反对 React 技术栈,以上几种计划都能够依据应用状况进一步思考:
在目前的工夫节点结构一个组件库,本文的观点是:在满足兼容性的要求下,可能承受 CSS in JS 的写法,且能够容忍其本身的一些有余(如性能问题或一些写法限度),优先思考有运行时的 CSS in JS 计划。组件库的开发者无需花精力解决款式的起名、引入和打包工作,在应用时也能很不便实现款式的按需加载,同时能够反对 SSR 和要害渲染门路的款式提取。
如果对 CSS in JS 的写法不太能承受,仍然更偏差惯例 CSS 的写法,或者感觉目前 CSS in JS 计划太多,难以做出抉择,那么还能够思考以下惯例 CSS 的计划:
如果组件库的利用场景明确不须要反对 SSR,且不须要思考独自拿到 CSS 文件做缓存等性能优化,能够抉择间接将 CSS 打包进 JS。这种计划不须要调整打包流程,使用者也只须要 import
组件即可应用,且逻辑和款式都能反对按需加载。
如果下面的条件不满足,能够优先选择款式与逻辑关联的计划。如果组件库用在外部我的项目中,须要该我的项目反对打包组件库里间接引入的款式文件。
款式与逻辑拆散是最为通用且稳当的计划。须要留神的是,这种计划实现按需加载比拟麻烦,通常组件库会思考提供一套生成按需加载款式语句的解决办法,例如疏导用户应用 babel-plugin-import
,并给出相应的配置。
命名计划抉择
抉择写惯例 CSS,还须要思考款式的命名规定。
如果是根底 UI 组件库,更举荐应用约定命名规定的计划:
- UI 组件库通常保护人员较为固定,便于推广对立的命名标准;
- 根底组件在业务中应用时,可能波及到定制化场景。这种计划能够间接对外提供源码,便于在具体业务中笼罩变量或款式,给使用者更大的自在。
如果是业务组件库,则更举荐应用 CSS Modules / 零运行时 CSS in JS 计划:
- 业务组件库自身与业务强相干,可能由不同业务的开发者保护,难以推广对立的命名标准,也无奈保障所有人都能严格遵守。而应用 CSS Modules 能够帮忙开发者保障款式的命名空间不会净化。
- 业务组件较少波及自定义的场景,不须要满足笼罩变量或款式的需要。
总结
目前在 React 生态中没有一种占统治位置的款式计划,而现有的各种计划都有各自的优缺点,因而抉择一种适合的款式计划须要综合思考很多方面。
本文从 CSS 与 JS 的关联形式和命名抵触解决形式两个方面对组件库的款式计划选型进行了比照和剖析,没有哪种计划是完满的计划,然而不同的业务场景必定会有更适合的计划,心愿本文能对大家构建组件库时的选型提供肯定的帮忙。
参考
- 如何在 React 中优雅的写 CSS
- A Thorough Analysis of CSS-in-JS
- Real-world CSS vs. CSS-in-JS performance comparison
本文公布自网易云音乐技术团队,文章未经受权禁止任何模式的转载。咱们长年招收各类技术岗位,如果你筹备换工作,又恰好喜爱云音乐,那就退出咱们 grp.music-fe(at)corp.netease.com!