Blog
Blog copied to clipboard
React 组件中如何组织 CSS
React 组件中如何组织 CSS
组件和模块
这部分主要参照 hax 的 关于前端开发中“模块”和“组件”概念的思考 一文。
在 React 开发中,webpack 是模块加载和打包的利器,基于 webpack 的工作流已经非常完善 。Webpack 使用 JS Module Loader 来加载其他 JS 模块,CSS 依赖以及图片等其他资源。但是,这里只是指明了组件中相关的 CSS 依赖,并没有解决组件化与 CSS 样式全局有效的冲突。
基础组件和业务组件
以前在组件化的讨论中,@fouber 和 @xufei 不止一次的说,Web 组件化的价值在于分治而不在于复用。我认为这个需要对组件做更细致的区分才能做出论断。对于基础组件,在于复用;对于业务组件,在于分治。由于基础组件复用性更强,我们可能需要更细致的去设计和实现。常见的 React 基础组件库有:material-ui, ant-design, react-toolbox。从实现来看,最大的区别就是如何组织组件的 CSS,以实现组件 CSS 局域化:
- material-ui 使用的 CSS in JS 方案,在组件内使用内联样式;
- ant-design 给组件取一个特殊的 className,以保证组件的 className 唯一;
- react-toolbox 使用 css-modules,通过 CSS 文件的路径或者 base64 编码来生成唯一的 className。
CSS in JS
CSS in JS 通过 DOM 的 style 属性来实现 CSS 在组件上的挂载,并且保证了组件的封装性和隔离性。不过,这尼玛是内联样式,不是花了很长时间才把着玩意干掉的吗?这样做是不是违背了结构与样式分离的最佳实践(实际上,JSX 好像也违背了结构与行为的分离)?
Web 发展初期,为什么我们没有分离结构,样式和行为?为什么当时想不到耦合的问题?因为初期 web 页面是局限于很简单的结构,你甚至可以理解为一个页面就是一个组件。由于结构简单,样式和行为基本很容易控制,分离结构,样式和行为显得没有必要,因为实际运行的页面是结构,样式和行为的叠加。
随着 web 页面结构开始变得庞大,样式变得酷炫,交互变得复杂,我们发现内联样式和行为使得代码的可维护性变得很差,于是我们通过『选择器』来进行解耦,样式和行为都通过选择器来和结构挂钩。
Web 发展到现在,早已不局限于简单的 web 页面。Web 应用正大行其道,各种 MV* 框架应接不暇, JS 模块化和 web 组件化早已不是新鲜事。Web 应用一般都是一个 SPA,SPA 的一个典型特征就是部分加载,组件化也就显得很自然。组件蕴含着封装和自治:JS 的模块化已经非常成熟,CSS 并没有类似的模块化机制,我们需要 CSS 模块化或者局域化。实际上我们将解耦的目标从结构、样式和行为(通过选择器)转变为组件间(通过组件属性 props)。组件化开发下,由于层层组合嵌套,单个组件内部实现就会比较简单,组件内聚合反而更好。这样就不难理解 React 在 HTML 中直接绑定事件处理器了,甚至提出了 CSS in JS。
CSS Modules
Css-modules 是通过工程化的方法自动生成唯一的 className,以实现 CSS 局域化的初衷,但是这样实现的侵入性太大,而且会造成 class dirty,而且自动生成的 className 与 HTML class 语义相违背。
类似方案如:ant-design 是手动给组件内所有的 className 加一个唯一的组件前缀来实现局域化。
理想的方案
CSS in JS 的主要缺点有:内联样式不支持一些伪类/伪元素/media query 等;内联样式书写起来比较困难。
Css-modules 和给组件内部 className 添加前缀主要的缺点在于:class dirty;不能保证 CSS 绝对局域化。
理想的方案是:使用 style 元素的 scoped 属性(很遗憾,目前只是 LS 阶段)。我们可以使用预处理器(sass/postcss)来实现一些模块化抽象(函数,mixin 等),使用 scoped style 来实现 CSS 局域化(可以利用 webpack 将依赖的的 CSS 插入到组件的根节点,并添加 scoped 属性,比如叫 scoped-style-loader)。
考虑到兼容未来的 scoped style,现阶段,我们可以这样组织组件 CSS:
组件的根节点使用 custom tag(唯一标识组件),内部样式使用标签结构选择器来定制(不使用 className),外面包一层根节点 tag(用来保证 CSS 局域化)。
这样看起来和 ant-design 的做法类似,但是我们『使用 custom tag 而不是 className 来唯一标识组件』,并添加了『内部样式使用标签结构选择器』这一限制:
- 保证语义化——组件 tag 比 className 更符合语义;
- 控制 class dirty——组件内部实现无需语义化,不需要使用 className;
- 方便自定义样式——默认样式只通过标签结构选择器,优先级低,方便自定义覆盖;
- 用语义来衡量组件拆分粒度——如果你觉得组件内部只依靠标签结构选择器无法很好的控制样式,那么很可能是组件内部需要进一步语义化,可以考虑进一步细化组件。
实验可参考:https://github.com/ustccjw/tech-blog
Web 组件化的价值在于分治而不在于复用。我认为这个需要对组件做更细致的区分才能做出论断。对于基础组件,在于复用;对于业务组件,在于分治。由于基础组件复用性更强,我们可能需要更细致的去设计和实现。
很赞同这种想法,我理解的楼主说的基础组件是只常见的UI组件(比如modal, panel等)。但是除了UI组件 & 业务组件还有一类组件如:操作cookie, promise, ajax等组件要怎么处理呢?
Css-modules 是通过工程化的方法自动生成唯一的 className,以实现 CSS 局域化的初衷,但是这样实现的侵入性太大,而且会造成 class dirty,而且自动生成的 className 与 HTML class 语义相违背
其实可以在css-loader上传参数避免这种问题。如:css-loader?modules&importLoaders=1&localIdentName=[name]_[local]_[hash:base64:5]。但可能这种做法没有考虑到标准的兼容。
@rockcoder23 我是觉得组件的规模尽可能小,基于结构就可以控制样式。
组件的根节点使用 custom tag(唯一标识组件),内部样式使用标签结构选择器来定制(不使用 className),外面包一层根节点 tag(用来保证 CSS 局域化)。
在实践中,我发现这样做的好处是让你能够自觉地拆分组件,而且生成的 DOM 结构更加语义化。
之前也曾对该方面进行过探索,试图总结一套完美的组件化方案。
@ustccjw 文章写的非常赞,总结的非常到位!
CSS in JS 方式有一点我感觉比较麻烦的就是,没法用 autoprefixer 类似的插件,兼容性处理较弱,这点是硬伤。
剩下的局部化方案就是 css-module 了,但在编码过程中不难发现,此种方式下的复用不是很方便,所以最终采用了折衷的办法,具体可参考 style 模块化思考。简单的说:需要复用的样式直接 import,而组件或页面级别的样式则通过 css-module 构建。
目前方案存在的问题:1)所有的样式都会打包到 css 文件中,与按需加载的理念相悖,antd 提供的 babel-plugin-import 能从某种层面上解决组件层次的样式冗余问题,但对整个项目工程而言仅供参考。 2)外部想覆盖局部样式时的方式比较hack,舒适度不够。