☰
JSS 类组合插件 jss-plugin-compose 完全指南:composes 语法、源码原理与实战用法
2026/10/10 21:04:48 网站建设 项目流程
  • 前端
  • UI组件

【免费下载链接】jss

JSS is an authoring tool for CSS which uses JavaScript as a host language.

项目地址:https://gitcode.com/gh_mirrors/js/jss
点击查看免费下载

JSS(JavaScript Style Sheets)是一个以 JavaScript 为宿主语言的 CSS 创作工具,而jss-plugin-compose是 JSS 官方插件体系中负责**类名组合(classes composition)**的核心插件。它允许你在一个 JSS 规则内通过composes属性引用全局类名(如 Bootstrap、Material Design Lite 等 CSS 框架的类)或本地规则(以$前缀引用同一样式表中的其他规则),从而复用粒度更细的规则、平滑引入既有 CSS 框架与遗留代码。阅读完本文,你将掌握composes的三种组合形态(全局类、本地类、混合组合)、其编译输出与最终 HTML 渲染效果、底层实现机制与边界约束,并能参照仓库中的源码与测试用例在真实项目中落地使用。

一、插件是什么:类组合的能力边界

在纯 JSS 场景中,每一条规则都会被编译成独立的 CSS 选择器与类名(形如button-123456),规则之间天然彼此隔离。这在带来样式局部化好处的同时,也让"一个元素同时具备多种状态样式""在 JSS 中复用 Bootstrap 的btn类"这类需求变得繁琐——你需要在 JSX 里手工拼接classes.button + ' btn'。

jss-plugin-compose将这种拼接下沉到了样式定义层:在规则对象中声明composes,插件会在样式编译阶段把目标类名注册追加到该规则的类名映射上,随后你拿到的classes.xxx就已经是包含组合类名的完整字符串,业务代码无需再关心类名来源。这正是原文档开篇所强调的:"This plugin allows you to use CSS frameworks and legacy code together with JSS as well as reuse Rules more granularly."

从 插件类型定义 可以看到它的接口极简——export default function jssPluginSyntaxCompose(): Plugin,导出一个返回 JSSPlugin对象的工厂函数,对应 Flow 类型声明 中的declare export default () => Plugin。

二、安装与接入

该插件随 JSS 官方 monorepo 一起维护,可通过 npm 或 yarn 安装(见 插件 readme):

npm install jss-plugin-compose # 或 yarn add jss-plugin-compose

接入方式与所有 JSS 插件一致,通过jss.use()注册:

import jss from 'jss' import jssPluginCompose from 'jss-plugin-compose' jss.use(jssPluginCompose()) const {classes} = jss.createStyleSheet(styles).attach()

如果你使用 jss-preset-default(JSS 官方推荐的默认插件集合),jss-plugin-compose已经在预设列表中,无需手动注册。仓库中的 组合插件示例 演示了完整的接入流程:先jss.use(jssPluginCompose()),再通过createStyleSheet(styles).attach()生成样式表并解构出classes,最后把classes.button等类名直接写入 DOM 的className。

三、组合全局类名(Compose with global classes)

组合全局类名用于把 JSS 与 CSS 框架(如 Material Design Lite、Bootstrap)及遗留 CSS 代码结合使用。语法上composes支持两种书写形式:

const styles = { button: { // 使用空格分隔的类名字符串 composes: 'btn btn-primary', color: 'red' }, buttonActive: { // 使用类名数组 composes: ['btn', 'btn-primary'], color: 'blue' } }

编译后的 CSS 输出为:

.button-123456 { color: red; } .buttonActive-123456 { color: blue; }

注意:composes本身不会被编译进 CSS 输出,它只负责在类名映射层面做追加,最终 CSS 里只有规则自身的声明。在 React 中使用时,classes对象已经包含组合结果:

import React from 'react' const classes = { button: 'button-123456 btn', buttonActive: 'buttonActive-123456 btn btn-primary' } const button1 = <button className={classes.button}>Button</button> const button2 = <button className={classes.buttonActive}>Active Button</button>

最终渲染出的 HTML:

<button class="button-123456 btn">Button</button> <button class="buttonActive-123456 btn btn-primary">Active Button</button>

从源码看,两种书写形式在底层都被同一套逻辑处理。核心实现 中的registerClass函数先递归展开数组(if (Array.isArray(className))逐项处理),再把空格分隔的字符串split(' ')后按数组继续递归;对每个非$开头的普通类名,直接执行parent.classes[rule.key] += ' ' + className,完成类名追加。

值得留意的是测试用例中的一种特殊形态:composes: ['$a', ['$b', '$c']](数组嵌套数组),测试文件 验证其组合结果为f-id a-id b-id c-id,说明数组可以是任意层级的嵌套结构,最终都会扁平化展开。

四、组合本地类(Compose with local classes)

组合本地类用于管理元素状态而无需重复定义规则——比如一个按钮在 active、disabled 等状态下的样式复用。要引用本地规则,需要在规则名前加$前缀。

const styles = { button: { color: 'black' }, // 你可以链式组合:被组合的规则自身也可以含有 composes buttonActive: { composes: '$button', color: 'red' }, buttonActiveDisabled: { composes: '$buttonActive', opacity: 0.5 }, // 也可以使用数组 disabled: { opacity: 0.5 }, active: { color: 'red' }, buttonDisabled: { composes: ['$button', '$active', '$disabled'] } }

编译后的 CSS:

.button-123456 { color: black; } .buttonActive-123456 { color: red; } .buttonActiveDisabled-123456 { opacity: 0.5; } .disabled-123456 { opacity: 0.5; } .active-123456 { color: red; } /* 规则 `buttonDisabled` 因为没有任何自身属性,不会被编译为 CSS */

这里有一个关键行为:纯组合规则(没有任何自身声明)不会产出 CSS。buttonDisabled只负责把button、active、disabled三个类的类名聚合到一起,因此 CSS 输出中没有它对应的选择器。在 React 中使用:

import React from 'react' const classes = { buttonActiveDisabled: 'buttonActiveDisabled-123456 buttonActive-123456 button-123456', buttonDisabled: 'buttonDisabled-123456 button-123456 active-123456 disabled-123456' } const button1 = <button className={classes.buttonActiveDisabled}>Active Disabled Button</button> const button2 = ( <button className={classes.buttonDisabled}>Disabled Button with active state</button> )

渲染结果:

<button class="buttonActiveDisabled-123456 buttonActive-123456 button-123456"> Active Disabled Button </button> <button class="buttonDisabled-123456 button-123456 active-123456 disabled-123456"> Disabled Button with active state </button>

底层原理在registerClass的$分支中体现得很清楚:当className[0] === '$'时,插件通过parent.getRule(className.substr(1))在父样式表中查找被引用的规则,然后把被引用规则对应的类名整串追加到当前规则上:parent.classes[rule.key] += ' ' + parent.classes[refRule.key]。由于被引用规则自身的类名映射里已经包含了它自己的组合结果,链式组合(compose composed)会自动展开。

这一点由 测试用例 中的 "Nested compositions" 用例直接验证:规则b: {composes: ['$a', 'd']}、c: {composes: ['$b']},最终sheet.classes.c等于'c-id b-id a-id d'——c引用b时,b已经展开的a-id d也被一并继承。

五、混合组合本地与全局类(Mix global and local classes)

composes允许在同一规则中同时组合本地类和全局类,二者在数组或字符串中可以混排:

const styles = { active: { color: 'red' }, button: { composes: ['$active', 'btn', 'btn-primary'], color: 'blue' } }

编译后的 CSS:

.active-123456 { color: red; } .button-123456 { color: blue; }

使用时:

import React from 'react' const classes = {button: 'button-123456 active-123456 btn btn-primary'} const button = <button className={classes.button}>Button</button>

渲染结果:

<button class="button-123456 active-123456 btn btn-primary">Button</button>

在registerClass的实现中,混合组合只是前两种分支的自然叠加:$开头走本地规则解析分支,普通字符串走全局类名追加分支。测试用例 "Mixed composition"(composes: ['$a', 'c', 'd']与composes: '$a c d')验证了数组与字符串两种写法下结果一致,均为b-id a-id c d。

六、底层实现原理:onProcessStyle 钩子与防御逻辑

整个插件的核心只有数十行代码(见 核心实现),它实现的是 JSS 插件体系中的onProcessStyle钩子:

export default function jssCompose() { function onProcessStyle(style, rule) { if (!('composes' in style)) return style registerClass(rule, style.composes) // 删除 composes 属性,防止无限循环 delete style.composes return style } return {onProcessStyle} }

三个设计要点值得展开:

  1. 钩子触发时机:onProcessStyle在每条规则被处理时调用。函数首先检查'composes' in style,没有composes的规则直接原样返回,零开销。
  2. 移除 composes 属性:delete style.composes是防止无限循环的关键——composes不是合法 CSS 属性,若不删除,后续的 CSS 序列化阶段会把它当作普通声明输出,且可能反复触发处理。测试用例中的警告断言里能看到规则被序列化前仍带有composes: $a;的中间形态,证明删除发生在样式处理阶段。
  3. 递归展开与错误防御:registerClass对空值直接返回true(跳过 falsy 值),对数组逐项递归,对空格字符串split后递归。递归过程携带返回值标志,一旦某个环节失败就终止后续追加。

运行时警告(Warning)行为

registerClass内置了两类基于tiny-warning的运行时告警,均被 测试用例 的 "Warnings" 分组覆盖:

  • 循环组合检测:当规则引用自身(如a: {composes: ['$a']})时,refRule === rule成立,抛出[JSS] Cyclic composition detected.并返回false,类名不会追加,避免无限递归。
  • 引用未定义规则:当$引用的规则在父样式表中不存在时(如a: {composes: ['$b']},而样式表中没有b),parent.getRule()返回空,抛出[JSS] Referenced rule is not defined.。

两个告警都会附带当前规则的完整序列化文本(rule.toString()),方便快速定位问题规则。

七、使用限制与注意事项(Caveats)

原文档明确列出了composes的边界条件,这些限制同样可以在仓库代码结构中印证:

  1. 不适用于全局样式表(Global Style Sheets):composes在 jss-plugin-global(对应 packages/jss-plugin-global)声明的全局规则内不生效。这是因为全局规则编译时不经过本地类名映射体系,parent.classes的追加机制无从作用。
  2. 不适用于嵌套规则(Nested rules):composes不能放在 jss-plugin-nested(对应 packages/jss-plugin-nested)的嵌套规则(如'&:hover')内部,嵌套规则同样不在本地类映射的处理范围内。
  3. 本地规则必须先行定义:当组合本地规则时,被引用的规则需要先定义,否则会得到错误的 CSS 选择器顺序与特异性。这与registerClass中parent.getRule()的查找时机有关——规则按定义顺序注册,引用早于定义的规则会导致解析失败或顺序错乱。

此外,从插件体系层面可以推断:composes的展开依赖parent.classes映射在样式编译阶段被填充,因此与同样依赖编译期类名解析的插件(如 rule-value-function、rule-value-observable)共存时,组合的最终形态以编译结果为准。

八、完整示例:在项目中组合使用

仓库的 组合插件示例 把三种组合形态放进同一个可运行项目(其依赖声明见 示例 package.json,使用jss与jss-plugin-compose,并通过 parcel 构建):

import jss from 'jss' import jssPluginCompose from 'jss-plugin-compose' const styles = { button: { composes: 'btn btn-primary', color: 'red' }, buttonActive: { composes: ['btn', 'btn-primary'], color: 'blue' }, buttonActiveDisabled: { composes: '$buttonActive', opacity: 0.5 } } // JSS Setup jss.use(jssPluginCompose()) const {classes} = jss.createStyleSheet(styles).attach() // Application logic. const div = document.body.appendChild(document.createElement('div')) div.innerHTML = ` <button class="${classes.button}">Button</button> <button class="${classes.buttonActive}">Active Button</button> <button class="${classes.buttonActiveDisabled}">Disabled Active Button</button> `

这个示例把"全局类组合(btn btn-primary)"与"本地类组合($buttonActive)"组合在同一个样式表中,直观地展示了composes在真实应用中的典型用法。示例中对buttonActiveDisabled引用$buttonActive,而buttonActive自身又组合了btn btn-primary,最终classes.buttonActiveDisabled会同时包含本地类与全局类,正是第四节所述链式组合的实战体现。

九、总结

jss-plugin-compose以极小的 API 表面(一个工厂函数、一个composes属性)解决了 JSS 生态中类复用的核心痛点:通过composes: 'foo bar'、composes: ['foo', 'bar']组合全局类,通过composes: '$rule'组合本地规则,支持链式组合与数组混合组合。它的实现依赖onProcessStyle钩子在编译期改写parent.classes映射,并内置循环引用、未定义引用两类运行时警告。

在实际项目中,建议遵循以下实践:本地规则组合时保证被引用规则先定义;composes不要写在全局样式表或嵌套规则中;纯组合规则(无自身声明)不会生成 CSS,适合作为"状态聚合器"使用;同时留意框架类名的特异性——组合类名最终都会出现在元素的class属性中,其生效顺序遵循 CSS 层叠规则。若需进一步了解插件在整个 JSS 插件体系中的位置,可查阅 插件总览文档 与其他插件文档(如 jss-plugin-global、jss-plugin-nested)。

  • 前端
  • UI组件

【免费下载链接】jss

JSS is an authoring tool for CSS which uses JavaScript as a host language.

项目地址:https://gitcode.com/gh_mirrors/js/jss
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询