Silk
Silk 是 Kobweb 中包含的一个 UI 层,构建在 Compose HTML 之上。
虽然 Compose HTML 要求你理解底层的 HTML / CSS 概念,但 Silk 试图抽象出其中的一些内容,提供一个更接近你在 Android 或 Desktop 上开发 Compose 应用的 API。 减少"div、span、flexbox、attrs、styles、classes"这些概念,转而使用"Rows、Columns、Boxes 和 Modifiers"。
我们认为 Silk 是 Kobweb 体验中相当重要的一部分,但值得指出的是,它被设计为一个可选组件。你完全可以不使用 Silk 来使用 Kobweb。(你也可以在不使用 Kobweb 的情况下使用 Silk!)
你也可以轻松地在 Silk 和 Compose HTML 组件之间切换(因为 Silk 本身就是由它们组合而成的)。
@InitSilk
方法
在进一步讨论之前,我们想快速提一下你可以使用 @InitSilk
注解一个方法,该方法会在你的网站启动时被调用。
这个方法必须接受一个 InitSilkContext
参数。上下文包含各种属性,允许调整 Silk 的默认值,这将在下面的章节中详细演示。
你的 @InitSilk
方法的名称并不重要,只要它们是公开的、接受一个 InitSilkContext
参数,并且不与其他同名方法冲突即可。出于可读性目的,建议你选择一个有意义的名称。
你可以定义任意数量的 @InitSilk
方法,所以可以根据需要将它们拆分成相关的、命名清晰的部分,而不是声明一个单一的、单体的、命名通用的 fun initSilk(ctx)
方法来做所有事情。
只要你能接受它们按任意顺序被调用即可,因为不保证特定的调用顺序。
CssStyle
使用 Silk, 你可以定义一个样式块。这让你可以以最终会被嵌入到 CSS 样式表中的方式声明修饰符 ( Modifier) ( 样式表优势)。
你可以使用 CssStyle
函数并将你的修饰符放入 base
块中来实现这一点:
我们稍后会讨论这个 base
块是什么,所以暂时不用担心它。
你可以使用 toModifier()
方法将任何这样的 CssStyle
转换为 Modifier
(例如 CustomStyle.toModifier()
)。此时,你可以将它传入任何接受 Modifier
参数的可组合函数:
当你声明一个 CssStyle
时,它必须是公开的。这是因为代码会被 Kobweb Gradle 插件生成在 main.kt
文件中,该代码需要能够访问你的样式以注册它。
一般来说,将样式视为全局的是个好主意,因为从技术上讲它们都存在于一个全局应用的样式表中,你必须确保样式名称在整个应用中是唯一的。
如果你添加一些样板代码来自己处理注册,你也可以使样式变成私有的:
但是,我们鼓励你保持样式公开,让 Kobweb Gradle 插件为你处理所有事情。
Additional selectors
那么,这个 base
块是怎么回事呢?
确实,单独使用时看起来有点啰嗦。但是,你可以定义其他有条件生效的选择器块。base 样式将始终首先应用,然后任何其他样式将基于特定选择器的规则应用。
定义附加选择器时顺序很重要,特别是当多个选择器同时适用时。
这里,我们创建一个默认为红色但鼠标悬停时为绿色的样式:
Kobweb 为你提供了很多标准选择器以方便使用,但对于那些精通 CSS 的人来说,你始终可以直接定义 CSS 规则以启用更复杂的组合或 Kobweb 尚未添加的选择器。
例如,这与上面的样式定义相同:
CssStyle name
Kobweb Gradle 插件会自动检测你的 CssStyle
属性并为你生成一个名称,该名称是从属性名称本身派生的但使用 Kebab Case。
例如,如果你写 val TitleTextStyle = CssStyle { ... }
,它的名称将是 "title-text"。
你通常不需要关心这个名称,但如果你使用浏览器开发工具检查 DOM,你会在那里看到它。
如果你需要手动设置名称,可以使用 CssName
注解覆盖默认名称:
CssStyle.base
大量的 CssStyle
块只包含 base
方法,所以 Kobweb 为这种常见情况提供了一个简便语法:
如果你发现自己需要支持 additional selectors, 你可以轻松地将 base
块分离出来。
Breakpoints
在响应式 HTML / CSS 设计世界中有一个叫做断点的功能,这与调试断点无关。相反,它们指定了你的网站在样式发生变化时的大小边界。这就是网站在移动设备、平板电脑和桌面设备上呈现不同内容的方式。
Kobweb 为你的项目提供了四个断点大小,加上不使用断点大小,总共给你五个可以在设计网站时使用的存储桶:
- 无断点 - 移动设备(及更大)
- sm - 平板电脑(及更大)
- md - 桌面(及更大)
- lg - 宽屏(及更大)
- xl - 超宽屏(及更大)
你可以通过在代码中添加一个 @InitSilk
方法并设置 ctx.theme.breakpoints
来更改你的网站的断点默认值:
要在 CssStyle
中引用断点,只需调用它:
在测试断点条件样式时,你应该知道浏览器开发工具允许你模拟窗口尺寸以查看你的网站在不同大小下的外观。例如,在 Chrome 上,你可以按照以下说明操作: https://developer.chrome.com/docs/devtools/device-mode
你还可以使用 Kotlin 范围运算符指定样式应该只应用于特定范围的断点:
如果你不喜欢需要用括号包裹断点范围表达式,也提供了 between
方法,它与 ..<
范围运算符在其他方面是相同的:
最后,如果你范围中的第一个断点是 Breakpoint.ZERO
, 你可以使用 until
方法简化你的表达式:
事实上,你可以把 until
看作是声明普通断点的反向操作。换句话说,until(Breakpoint.MD) { ... }
意味着所有断点大小直到中等大小,而 Breakpoint.MD { ... }
意味着中等大小及以上。
Color-mode aware
当你定义一个 CssStyle
时,可以使用一个叫做 colorMode
的属性:
Silk 为其所有部件定义了一堆浅色和深色颜色,如果你想在自己的部件中重用其中任何一个,你可以使用 colorMode.toPalette()
查询它们:
SilkTheme
包含非常简单(例如黑色和白色)的默认值,但你可以在 @InitSilk
方法中覆盖它们,也许可以改成更符合品牌的样式:
Initial color mode
默认情况下,Kobweb 会将你的网站的颜色模式初始化为 ColorMode.LIGHT
。
但是,你可以通过在 @InitSilk
方法中设置 initialColorMode
属性来控制这一点:
如果你想尊重用户的系统偏好,你可以将 initialColorMode
设置为 ColorMode.systemPreference
:
Persisting color-mode preference
如果你支持切换网站的颜色模式,建议你将用户的最后选择保存到本地存储中,然后在用户稍后重新访问你的网站时恢复它。
恢复将在你的 @InitSilk
块中进行,而保存颜色模式的代码应该发生在你的根 @App
可组合函数中 ( Application Root):
Extending CSS styles
你可能偶尔会想要定义一个只应该与另一个样式一起应用/在其之后应用的样式。
实现这一点最简单的方法是使用 extendedBy
方法扩展基本 CSS 样式块:
一旦扩展,你只需要在扩展的样式上调用 toModifier
就可以自动包含两个样式:
Component styles
到目前为止,我们已经讨论了定义各种 CSS 样式属性的基本 CSS 样式块。
然而,有一种方法可以定义类型化的 CSS 样式块。你可以从中生成类型化变体,基本上是调整或扩展它们的基本样式。你不能将从一个类型化 CSS 样式块生成的变体用于另一个不同类型的变体。
这种类型化 CSS 样式被称为组件样式,因为这种模式在定义小部件组件时很有效。事实上,这是 Silk 用于其每个小部件的标准模式。
要声明一个,你首先创建一个实现 ComponentKind
的标记接口,然后将其指定为你的 CssStyle
声明块的类型。按照惯例,它们的名称(减去后缀)应该匹配。
例如,如果 Silk 没有提供自己的按钮小部件,下面是你如何开始定义你自己的:
注意我们的接口声明的两点:
- 它被标记为
sealed
。技术上这不是必需的,但我们建议这样做,作为一种表达你的意图的方式,即没有其他人应该进一步子类化它。 - 接口是空的。它只是一个标记接口,仅用于为变体强制执行类型。这在下一节中会详细讨论。
Component variants
组件样式的强大之处在于它们可以使用 addVariant
方法生成组件变体:
变体的推荐命名约定是取其关联样式并使用其名称作为后缀加上单词"Variant",例如 ButtonStyle
→ OutlinedButtonVariant
和 TextStyle
→ EmphasizedTextVariant
。
像任何 CssStyle
一样,你的 CssStyleVariant
必须是公开的。这是出于相同的原因:因为代码会被 Kobweb Gradle 插件生成在 main.kt
文件中,该代码需要能够访问你的变体以注册它。
如果你添加一些样板代码来自己处理注册,你也可以使变体变成私有的:
但是,我们鼓励你保持变体公开,让 Kobweb Gradle 插件为你处理所有事情。
组件变体背后的想法是,它们让小部件作者有能力定义一个基本样式以及用户可能想要在其之上应用的一个或多个常见调整。(即使小部件作者没有为样式提供任何变体,任何用户也始终可以在他们自己的代码库中定义自己的变体。)
让我们重新访问按钮样式示例,把所有内容组合在一起。
当与组件样式一起使用时,toModifier()
方法可以选择接受一个变体参数。当传入一个变体时,两种样式都将被应用 -- 首先是基本样式,然后是变体样式。
例如,ButtonStyle.toModifier(OutlinedButtonVariant)
首先应用主按钮样式,然后是一些额外的轮廓样式。
你可以用 @CssName
注解标注样式变体,就像你可以对 CssStyle
做的那样。使用前导破折号会自动添加基本样式名称前缀。例如:
addVariantBase
像 CssStyle.base
一样,不需要支持额外选择器的变体可以使用 addVariantBase
来稍微简化它们的声明:
Silk widget conventions
Silk 在定义其小部件时总是使用组件样式。完整的模式如下所示(如果你在自己的项目中定义自己的小部件,你可以模仿这个):
换句话说:
- 我们定义一个可组合的小部件方法。
- 它接受一个
Modifier
作为第一个参数,该参数有一个默认值。 - 接下来是一个
CssStyleVariant
参数(类型化为你特定的ComponentKind
实现)。 - 在你的小部件内部,我们按以下顺序应用修饰符:基本样式,然后是传入的变体,然后是传入的修饰符。
- 最后一个参数是一个
@Composable
内容 lambda 参数(除非这个小部件不支持自定义内容)。
调用者可以通过以下几种方式之一调用小部件:
动画
在CSS中,动画通过让你在样式表中定义关键帧来工作,然后通过名称在动画样式中引用这些关键帧。你可以在 Mozilla文档网站了解更多信息。
例如,这里是一个滑动矩形的CSS动画示例 (来自本教程):
Kobweb让你可以使用Keyframes
代码块来定义关键帧:
当你声明一个Keyframes
动画时,它必须是公开的。这是因为代码会在main.kt
文件中生成,需要能够访问和注册它。
然后你可以使用toAnimation
方法将关键帧集合转换为使用这些关键帧的动画,并将其传递给Modifier.animation
修饰符。
关键帧块的名称会自动从属性名派生(这里,ShiftRightKeyframes
被转换为"shift-right"
)。
ElementRefScope
和原生HTML元素
有时,你可能需要访问Silk小部件背后的原生元素。所有Silk小部件都提供了一个可选的ref
参数,该参数接受一个提供此信息的监听器。
所有ref
回调将接收一个org.w3c.dom.Element
子类。你可以查看 Element类(及其通常更相关的 HTMLElement继承者)以了解其可用的方法和属性。
原生HTML元素暴露了许多通过高级Compose HTML API无法获得的功能。
ref
对于一个简单但常见的示例,我们可以使用原生元素来捕获焦点:
ref { ... }
方法实际上可以接受一个或多个可选的任意值键。如果在后续重新组合中这些键中的任何一个发生变化,回调将重新运行:
Finally, here is a pattern you can use to extract a raw backing element which has some role to play during composition:
Extracting a raw element as above will cause a composition to take two passes -- the first one where the content of your widget will be empty, and a second where it will be populated -- but in general this should be invisible to the user.
disposableRef
如果你需要知道元素何时进入和退出DOM,可以使用disposableRef
。使用disposableRef
时,代码块的最后一行必须是调用onDispose
:
disposableRef
方法也可以接受键,如果其中任何一个发生变化,监听器将重新运行。在这种情况下,onDispose
回调也会被触发。
refScope
最后,你可能希望有多个监听器,它们根据不同的键独立地重新创建。你可以使用refScope
作为一种方式来组合两个或多个ref
和/或disposableRef
调用的任意组合:
Compose HTML refs
你可能偶尔会希望获取普通Compose HTML小部件的支持元素,例如Div
或Span
。然而,这些小部件没有ref
回调,因为这是Silk提供的一个便利功能。
在这种情况下,你仍然有一些选择。
官方的方式是使用attrs
块中的ref
块来检索引用。这个版本的ref
实际上更类似于Silk的disposableRef
概念,而不是它的ref
,因为它需要一个onDispose
块:
上面的代码片段改编自官方教程。
与Silk版本的ref
不同,Compose HTML版本不接受键。如果你需要这种行为,并且Compose HTML小部件接受内容块(其中许多确实如此),你可以直接在其中调用Silk的registerRefScope
方法:
样式变量
Kobweb支持CSS变量(也称为CSS自定义属性),这是一种功能,你可以在CSS样式中存储和检索变量声明的属性值。它通过一个名为StyleVariable
的类来实现。
你可以在官方文档中找到CSS自定义属性的相关信息。
使用样式变量非常简单。首先声明一个没有值的变量(但将其锁定为一个类型),然后你可以在样式中使用Modifier.setVariable(...)
进行初始化:
一旦在父元素上设置了变量,它可以被该元素或其任何子元素查询。
Compose HTML提供了一个CSSLengthValue
,表示具体值,如10.px
或5.cssRem
。然而,Kobweb提供了一个CSSLengthNumericValue
类型,表示更一般的概念,例如中间计算的结果。为所有相关单位提供了CSS*NumericValue
类型,建议在声明样式变量时使用它们,因为它们更自然地支持在计算中使用。
我们稍后会更详细地讨论CSSNumericValue
( CSSNumericValue type-aliases)。
你可以稍后使用value()
方法查询变量以提取其当前值:
你还可以提供一个后备值,如果存在,该值将在变量之前未设置的情况下使用:
你甚至可以在首次声明变量时提供一个默认的后备值! (这是我们在Kobweb中支持的,尽管它不是CSS规范的一部分。)
以下代码示例显示了不同后备范围何时生效:
在上面的示例中,在DialogStyle300
样式中,我们设置了一个变量并在同一个修饰符中查询它,这纯粹是为了演示目的。在实践中,你不会出于任何我能想到的原因这样做——相反,变量会在其他地方单独设置,例如在内联样式或父容器上。
为了将这些概念结合在一起,下面我们声明一个背景颜色变量,创建一个设置它的根容器范围,一个使用它的子样式,最后是一个覆盖它的子样式变体:
以下代码将上述样式结合在一起(在某些情况下使用内联样式进一步覆盖背景颜色):
上面的代码渲染了以下输出:
程序化设置值
如果你可以访问支持的HTML元素,你还可以直接从代码中设置CSS变量。
下面,我们使用ref
回调获取全屏Box
的支持元素,然后使用Button
将其设置为彩虹颜色中的随机颜色:
上面的代码生成了以下UI:
优先使用纯Kotlin
大多数时候,你实际上可以不使用CSS变量!你的Kotlin代码通常是描述动态行为的更自然的地方,而不是HTML/CSS。
让我们重新审视上面的“彩色方块”示例。注意,如果我们不尝试使用变量,它会更容易阅读。
“彩虹背景”示例同样更容易通过使用Kotlin变量(即var someValue by remember { mutableStateOf(...) }
)而不是CSS变量来阅读:
即使你很少需要CSS变量,但在某些情况下,它们可能是你工具箱中的一个有用工具。上面的示例是用于展示CSS变量的相对独立环境中的人工场景。但这里有一些可能受益于CSS变量的情况:
- 你有一个允许用户从多个主题(例如主色和次色)中选择的站点。添加
themePrimary
和themeSecondary
的CSS变量(应用于站点的根部)将非常简单,然后你可以在整个样式中引用它们。 - 你需要比简单的浅色/深色模式提供的更多的颜色控制。例如,Wordle有浅色/深色+正常/高对比度模式。
- 你想创建一个小部件,它根据添加的上下文动态更改其行为。例如,也许你的网站有一个黑暗区域和一个明亮区域,小部件应该在黑暗区域使用白色轮廓,在明亮区域使用黑色轮廓。这可以通过公开一个轮廓颜色变量来实现,每个站点区域负责设置它。
- 你想允许用户在伪类选择器(例如悬停、聚焦、活动)中调整某些小部件的值(例如颜色或边框大小),这比监听事件和设置内联样式要容易得多。
- 你有一个小部件,你最终为其创建了许多变体,但你意识到你可以用一个或两个CSS变量替换它们。
当不确定时,依靠Kotlin来处理动态行为,并偶尔考虑使用样式变量,如果你觉得这样做会清理代码。
Calc
StyleVariable
以一种微妙的方式工作,通常是可以的,直到它不行——这通常是当你尝试拦截和修改其值而不是仅仅传递它们时。
具体来说,像这样(将样式变量值乘以2)的代码会编译但在运行时无法工作:
要了解问题所在,让我们先退一步。以下代码:
生成以下CSS:
然而,MyOpacityVar
在我们的代码中表现得像一个Number
!如何生成类似于var(--my-opacity)
的文本输出?
这是通过使用Kotlin/JS的unsafeCast
实现的,你可以告诉编译器将一个值视为不同的类型。在这种情况下,MyOpacityVar.value()
返回一个对象,Kotlin编译器将其视为Number
,但实际上它是一个类实例,其toString()
计算结果为var(--my-opacity)
。
因此,Modifier.opacity(MyOpacityVar.value())
看起来像是魔法!然而,如果你尝试进行一些算术运算,如MyOpacityVar.value().toDouble() * 0.5
,编译器可能会满意,但在运行时,当JS引擎被要求对非数字进行数学运算时,事情会默默地中断。
在CSS中,使用变量进行数学运算是通过使用calc
块来完成的,因此Kobweb提供了自己的calc
方法来镜像这一点。当处理原始数值时,你必须将它们包装在num
中,以便我们可以逃避上面导致运行时混淆的原始类型系统:
此时,你可以编写如下代码:
记住将原始值包装在num
中有点难,但如果你做错了,你会得到编译错误。
处理表示长度值的变量不需要calc块,因为Compose HTML支持对这些数值单位类型进行数学运算:
然而,如果你从一个原始数字开始并希望将其转换为大小,calc块仍然可能有用:
Font Awesome
Kobweb 提供了 silk-icons-fa
组件库,如果你希望在项目中使用所有免费的 Font Awesome (v6) 图标,可以使用它。
使用非常简单!请在 Font Awesome 图标库 中搜索、选择一个图标,然后使用对应的 Font Awesome 图标 Composable 调用它。
例如,如果我想添加 Kobweb 主题的 蜘蛛图标,可以这样在代码中调用:
就是这么简单!
某些图标有实心和轮廓两种版本,例如 “Square”(轮廓版: 链接 ;实心版:链接)。此时默认展示轮廓版,但你可以传入样式枚举来控制:
所有 Font Awesome 的 Composable 都接受 modifier 参数,这样你就可以进一步调整样式:
使用我们的 app
模板创建项目时,Font Awesome 图标已经包含在内。
Material Design Icons
Kobweb 提供了 silk-icons-mdi
组件库,供你在项目中使用所有免费的 Material Design 图标。
使用方法也十分简单!请在 Material Icons 图标库 搜索、选择一个图标,然后调用对应的 Material Design Icon Composable。
例如,假如我搜索后找到并想使用他们的 bug report 图标,在 Kobweb 代码中可以这样调用(将名称转换为小驼峰形式):
就是这么简单!
多数 Material Design 图标支持多种风格:轮廓、实心、圆角、锐角以及双色。请点击上方图标库链接确认你的图标支持哪些风格。你可以通过向方法的 style
参数传入相应的枚举来选择你想要的风格:
所有 Material Design Icon 的 Composable 都接受 modifier 参数,这样你可以进一步调整样式:
The Silk stylesheet
浏览器为许多 HTML 元素提供的默认样式通常无法满足大多数网站设计需求,你很可能需要调整其中至少部分样式。一个常见的例子是默认的网页字体,若不修改会让你的网站显得有些陈旧。
传统网站往往会通过创建 CSS 样式表并在 HTML 中引用来覆盖默认样式。但如果你在 Kobweb 应用中使用 Silk,你可以采用与 CssStyle
类似的方法为普通 HTML 元素定义样式。
为此,只需创建一个 @InitSilk
方法。其上下文参数中包含一个 stylesheet
属性,该属性代表你网站的 CSS 样式表,并提供一个风格化的 API 用于添加 CSS 规则。
下面是一个简单示例,将整个网站的字体调整为比浏览器默认更美观的样式,一个用于普通文本,一个用于代码:
方法 registerStyleBase
常用于以较少代码注册样式,但如果你需要添加对某个或某些伪类(例如 hover
、focus
、active
) 的支持,也可以使用 registerStyle
:
Globally changing Silk widget styles
正如前面提到的,Silk 的所有组件都采用组件样式来构建其外观和风格。
通常,如果你只想在网站的某些位置调整某个样式,可以为该样式创建一个变体:
但如果你想全局改变某个组件在整个网站中的外观该怎么办?
你当然可以创建你自己的 Composable,将底层组件包裹在一个新样式中,比如自定义 MyButton
,并定义专属的 MyButtonStyle
,内部委托给原生的 Button
。不过这样需要确保所有新加入的开发者都使用 MyButton
而非直接调用 Button
。
Silk 提供了另一种方式,允许你直接修改它声明的样式或变体。
你可以通过 @InitSilk
方法来实现。上下文参数中提供了 theme
属性,该属性暴露了一系列方法,允许你重写所有样式和变体:
从技术上讲,你也可以将这些方法用于你自己网站声明的样式和变体,但这样做没有必要,因为你可以直接去源代码 修改这些值。不过,如果你正在使用提供自己样式和/或变体的第三方Kobweb库,这仍然可能很有用。
如果你想从头开始定义一整套新的CSS规则,请使用replace
版本;如果你想在现有基础上添加额外的更改, 则使用modify
版本。
对一些比较复杂的Silk样式使用replace
可能会比较棘手,在尝试这样做之前,你可能需要先熟悉这些组件 实现的细节。此外,一旦你在你的网站中替换了某个样式,你将无法获得该样式在未来Silk版本中可能会有的 任何改进。
这是一个来自某个网站的真实示例,该网站希望其水平分隔线始终填充最大宽度。它使用了modify
方法 (而不是replace
方法),这通常是推荐的做法,因为这种方式在未来出现问题的可能性较小: