dojo dragon main logo

对 Dojo 应用程序进行主题化

Dojo 应用程序需要一种方法来以一致的方式呈现它们使用的所有小部件,以便用户整体地感知和交互应用程序功能,而不是将其视为网页上不连贯元素的混合体。这通常通过企业或产品营销主题来实现,该主题指定颜色、布局、字体系列等。

制作可主题化的小部件

要使小部件被视为可主题化,有两个要求

  1. 小部件的工厂应该注入theme 中间件,const factory = create({ theme })
  2. 小部件的一个或多个样式类应该在渲染小部件时使用theme.classes(css)调用的结果传递。

按照惯例,还有一个在开发用于分发的小部件时有用的第三个要求(这是 Dojo 小部件库中遵循的惯例)

  1. 小部件的根 VDOM 节点 - 也就是说,小部件渲染的最外层节点 - 应该包含一个名为root的样式类。这样做提供了一种可预测的方式,可以在自定义主题中覆盖第三方可主题化小部件的样式时,定位其顶层节点。

theme 中间件是从@dojo/framework/core/middleware/theme 模块导入的。

theme.classes 方法

theme.classes 将小部件 CSS 类名转换为应用程序或小部件的主题类名。

theme.classes<T extends ClassNames>(css: T): T;
  • 注意 1:主题覆盖仅在 CSS 类级别,而不是类中单个样式属性。
  • 注意 2:如果当前活动主题为给定样式类提供覆盖,则小部件将回退到使用该类的默认样式属性。
  • 注意 3:如果当前活动主题确实为给定样式类提供覆盖,则小部件将具有在主题中指定的 CSS 属性集应用于它。例如,如果小部件的默认样式类包含十个 CSS 属性,但当前主题仅指定一个,则小部件将使用单个 CSS 属性呈现,并丢失主题覆盖中未指定的另外九个属性。

theme 中间件属性

可主题化小部件示例

给定以下可主题化小部件的 CSS 模块文件

src/styles/MyThemeableWidget.m.css

/* requirement 4, i.e. this widget is intended for wider distribution,
therefore its outer-most VDOM element uses the 'root' class: */
.root {
	font-family: sans-serif;
}

/* widgets can use any variety of ancillary CSS classes that are also themeable */
.myWidgetExtraThemeableClass {
	font-variant: small-caps;
}

/* extra 'fixed' classes can also be used to specify a widget's structural styling, which is not intended to be
overridden via a theme */
.myWidgetStructuralClass {
	font-style: italic;
}

此样式表可以在相应可主题化小部件中使用,如下所示

src/widgets/MyThemeableWidget.tsx

import { create, tsx } from '@dojo/framework/core/vdom';
import theme from '@dojo/framework/core/middleware/theme';

import * as css from '../styles/MyThemeableWidget.m.css';

/* requirement 1: */
const factory = create({ theme });

export default factory(function MyThemeableWidget({ middleware: { theme } }) {
	/* requirement 2 */
	const { root, myWidgetExtraThemeableClass } = theme.classes(css);
	return (
		<div
			classes={[
				/* requirement 3: */
				root,
				myWidgetExtraThemeableClass,
				css.myWidgetExtraThemeableClass,
				theme.variant()
			]}
		>
			Hello from a themed Dojo widget!
		</div>
	);
});

使用多个 CSS 模块

小部件还可以导入和引用多个 CSS 模块 - 这提供了另一种方法,除了本指南中其他地方描述的基于 CSS 的方法(CSS 自定义属性CSS 模块组合)之外,通过 TypeScript 代码抽象和重用通用样式属性。

扩展上面的示例

src/styles/MyThemeCommonStyles.m.css

.commonBase {
	border: 4px solid black;
	border-radius: 4em;
	padding: 2em;
}

src/widgets/MyThemeableWidget.tsx

import { create, tsx } from '@dojo/framework/core/vdom';
import theme from '@dojo/framework/core/middleware/theme';

import * as css from '../styles/MyThemeableWidget.m.css';
import * as commonCss from '../styles/MyThemeCommonStyles.m.css';

const factory = create({ theme });

export default factory(function MyThemeableWidget({ middleware: { theme } }) {
	const { root } = theme.classes(css);
	const { commonBase } = theme.classes(commonCss);
	return (
		<div classes={[root, commonBase, css.myWidgetExtraThemeableClass, theme.variant()]}>
			Hello from a themed Dojo widget!
		</div>
	);
});

覆盖特定小部件实例的主题

小部件的用户可以通过将有效的主题传递到实例的theme 属性来覆盖特定实例的主题。当需要以多种方式在应用程序中的多个位置显示给定小部件时,这很有用。

例如,基于可主题化小部件示例

src/themes/myTheme/styles/MyThemeableWidget.m.css

.root {
	color: blue;
}

src/themes/myThemeOverride/theme.ts

import * as myThemeableWidgetCss from './styles/MyThemeableWidget.m.css';

export default {
	'my-app/MyThemeableWidget': myThemeableWidgetCss
};

src/widgets/MyApp.tsx

import { create, tsx } from '@dojo/framework/core/vdom';

import MyThemeableWidget from './src/widgets/MyThemeableWidget.tsx';
import * as myThemeOverride from '../themes/myThemeOverride/theme.ts';

const factory = create();

export default factory(function MyApp() {
	return (
		<div>
			<MyThemeableWidget />
			<MyThemeableWidget theme={myThemeOverride} />
		</div>
	);
});

在这里,渲染了两个MyThemeableWidget 实例 - 第一个使用应用程序范围的主题(如果指定),否则使用小部件的默认样式。相反,第二个实例将始终使用myThemeOverride 中定义的主题呈现。

将额外类传递给小部件

主题机制提供了一种简单的方法,可以在应用程序中的每个小部件上一致地应用自定义样式,但对于用户想要将其他样式应用于给定小部件的特定实例的情况来说,它不够灵活。

可以通过可主题化小部件的classes 属性传递额外的样式类。它们被认为是累加的,不会覆盖小部件现有的样式类 - 它们的目的是允许对预先存在的样式进行细粒度的调整。提供的每组额外类都需要按两个级别的键进行分组

  1. 适当的小部件主题键,指定应该应用类的目标小部件,包括可能使用的任何子小部件。
  2. 小部件使用的特定现有 CSS 类,允许小部件使用者在多个 DOM 元素中,针对单个 DOM 元素级别的样式扩展。

为了说明,额外类属性的类型定义是

type ExtraClassName = string | null | undefined | boolean;

interface Classes {
	[widgetThemeKey: string]: {
		[baseClassName: string]: ExtraClassName[];
	};
}

作为提供额外类的示例,以下内容调整了Dojo 组合框的实例,以及它包含的文本输入子小部件。这将使组合框使用的文本输入控件及其控制面板的背景颜色变为蓝色。组合框控制面板中的向下箭头也将变为红色

src/styles/MyComboBoxStyleTweaks.m.css

.blueBackground {
	background-color: blue;
}

.redArrow {
	color: red;
}

src/widgets/MyWidget.tsx

import { create, tsx } from '@dojo/framework/core/vdom';

import ComboBox from '@dojo/widgets/combobox';
import * as myComboBoxStyleTweaks from '../styles/MyComboBoxStyleTweaks.m.css';

const myExtraClasses = {
	'@dojo/widgets/combobox': {
		controls: [myComboBoxStyleTweaks.blueBackground],
		trigger: [myComboBoxStyleTweaks.redArrow]
	},
	'@dojo/widgets/text-input': {
		input: [myComboBoxStyleTweaks.blueBackground]
	}
};

const factory = create();

export default factory(function MyWidget() {
	return (
		<div>
			Hello from a tweaked Dojo combobox!
			<ComboBox classes={myExtraClasses} results={['foo', 'bar']} />
		</div>
	);
});

请注意,小部件作者有责任明确地将classes 属性传递给所有利用的子小部件,因为 Dojo 本身不会注入或自动传递该属性给子小部件。

制作可主题化应用程序

为了为应用程序中的所有可主题化小部件指定主题,可以在应用程序的顶层小部件中使用theme 中间件的theme.set API。可以通过在调用theme.set 之前检查theme.get 来设置默认主题或初始主题。

例如,指定主要应用程序主题

src/App.tsx

import { create, tsx } from '@dojo/framework/core/vdom';
import theme from '@dojo/framework/core/middleware/theme';

import myTheme from '../themes/MyTheme/theme';

const factory = create({ theme });

export default factory(function App({ middleware: { theme }}) {
	// if the theme isn't set, set the default theme
	if (!theme.get()) {
		theme.set(myTheme);
	}
	return (
		// the application's widgets
	);
});

有关myTheme 导入的结构方式的说明,请参阅编写主题

请注意,使用可主题化小部件而没有显式主题(例如,不使用theme.set 设置默认主题,也不明确覆盖小部件实例的主题或样式类)将导致每个小部件使用其默认样式规则。

如果使用独立分发的主题的全部内容,应用程序还需要将主题的总体index.css 文件集成到自己的样式中。这可以通过在项目的main.css 文件中导入来完成

src/main.css

@import '@{myThemePackageName}/{myThemeName}/index.css';

相反,另一种仅使用外部构建主题的部分的方法是通过主题组合

更改当前活动主题

theme 中间件的 .set(theme) 函数可用于更改整个应用程序的活动主题。将所需主题传递给 .set,这将使应用程序树中所有带主题的小部件失效,并使用新主题重新渲染它们。

src/widgets/ThemeSwitcher.tsx

import { create, tsx } from '@dojo/framework/core/vdom';
import theme from '@dojo/framework/core/middleware/theme';

import myTheme from '../themes/MyTheme/theme';
import alternativeTheme from '../themes/MyAlternativeTheme/theme';

const factory = create({ theme });

export default factory(function ThemeSwitcher({ middleware: { theme } }) {
	return (
		<div>
			<button
				onclick={() => {
					theme.set(myTheme);
				}}
			>
				Use Default Theme
			</button>
			<button
				onclick={() => {
					theme.set(alternativeTheme);
				}}
			>
				Use Alternative Theme
			</button>
		</div>
	);
});