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 类名转换应用程序或部件的主题类名。

  1. theme.classes<T extends ClassNames>(css: T): T;
  • 注意事项 1: 主题的重写只在 CSS 类一级,而不是 CSS 类中的单个样式属性。
  • 注意事项 2: 如果当前激活的主题没有重写给定的样式类,则部件会退而使用该类的默认样式属性。
  • 注意事项 3: 如果当前激活的主题的确重写了给定的样式类,则 只会 将主题中指定的 CSS 属性应用到部件上。例如,如果部件的默认样式类包含 10 个 CSS 属性,但是当前的主题只指定了一个,则部件渲染时只会使用这一个 CSS 属性,并丢掉在主题中未重写的其他 9 个属性。

theme 中间件属性

可主题化部件示例

下面是一个可主题化部件的 CSS 模块文件:

src/styles/MyThemeableWidget.m.css

  1. /* requirement 4, i.e. this widget is intended for wider distribution,
  2. therefore its outer-most VDOM element uses the 'root' class: */
  3. .root {
  4. font-family: sans-serif;
  5. }
  6. /* widgets can use any variety of ancillary CSS classes that are also themeable */
  7. .myWidgetExtraThemeableClass {
  8. font-variant: small-caps;
  9. }
  10. /* extra 'fixed' classes can also be used to specify a widget's structural styling, which is not intended to be
  11. overridden via a theme */
  12. .myWidgetStructuralClass {
  13. font-style: italic;
  14. }

在相应的可主题化的部件中使用这些样式:

src/widgets/MyThemeableWidget.tsx

  1. import { create, tsx } from '@dojo/framework/core/vdom';
  2. import theme from '@dojo/framework/core/middleware/theme';
  3. import * as css from '../styles/MyThemeableWidget.m.css';
  4. /* requirement 1: */
  5. const factory = create({ theme });
  6. export default factory(function MyThemeableWidget({ middleware: { theme } }) {
  7. /* requirement 2 */
  8. const { root, myWidgetExtraThemeableClass } = theme.classes(css);
  9. return (
  10. <div
  11. classes={[
  12. /* requirement 3: */
  13. root,
  14. myWidgetExtraThemeableClass,
  15. css.myWidgetExtraThemeableClass,
  16. theme.variant()
  17. ]}
  18. >
  19. Hello from a themed Dojo widget!
  20. </div>
  21. );
  22. });

使用多个 CSS 模块

部件也能导入和引用多个 CSS 模块,除了本指南的其它部分介绍的基于 CSS 的方法(CSS 自定义属性CSS 模块化组合功能)之外,这提供了另一种通过 TypeScript 代码来提取和复用公共样式属性的方法。

扩展上述示例:

src/styles/MyThemeCommonStyles.m.css

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

src/widgets/MyThemeableWidget.tsx

  1. import { create, tsx } from '@dojo/framework/core/vdom';
  2. import theme from '@dojo/framework/core/middleware/theme';
  3. import * as css from '../styles/MyThemeableWidget.m.css';
  4. import * as commonCss from '../styles/MyThemeCommonStyles.m.css';
  5. const factory = create({ theme });
  6. export default factory(function MyThemeableWidget({ middleware: { theme } }) {
  7. const { root } = theme.classes(css);
  8. const { commonBase } = theme.classes(commonCss);
  9. return (
  10. <div classes={[root, commonBase, css.myWidgetExtraThemeableClass, theme.variant()]}>
  11. Hello from a themed Dojo widget!
  12. </div>
  13. );
  14. });

重写部件实例的主题

部件的使用者可以将一个有效的主题传给部件实例的 theme 属性,来重写特定部件实例的主题。当需要在应用程序的不同部分以多种方式显示给定的部件时,这个功能就能派上用场。

例如,在可主题化部件示例的基础上构建:

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

  1. .root {
  2. color: blue;
  3. }

src/themes/myThemeOverride/theme.ts

  1. import * as myThemeableWidgetCss from './styles/MyThemeableWidget.m.css';
  2. export default {
  3. 'my-app/MyThemeableWidget': myThemeableWidgetCss
  4. };

src/widgets/MyApp.tsx

  1. import { create, tsx } from '@dojo/framework/core/vdom';
  2. import MyThemeableWidget from './src/widgets/MyThemeableWidget.tsx';
  3. import * as myThemeOverride from '../themes/myThemeOverride/theme.ts';
  4. const factory = create();
  5. export default factory(function MyApp() {
  6. return (
  7. <div>
  8. <MyThemeableWidget />
  9. <MyThemeableWidget theme={myThemeOverride} />
  10. </div>
  11. );
  12. });

此处,渲染了两个 MyThemeableWidget 实例,如果指定了应用程序范围的主题,则第一个部件会使用此主题,否则使用部件的默认样式。相比之下,第二个部件始终使用 myThemeOverride 中定义的主题。

为部件传入额外的样式

主题机制提供了一种简便的方式,为应用程序中的每个部件统一应用自定义样式,但当用户希望为给定的部件实例应用额外的样式时,在这种场景下主题机制就不够灵活。

可以通过可主题化部件的 classes 属性来传入额外的样式类。这些样式类是追加的,不会重写部件已有的样式类,它们的目的是对已经存在的样式进行细粒度的调整。提供的每一组额外的样式类都需要按照两个级别的 key 进行分组:

  1. 合适的部件主题 key,用于指定应用样式类的部件,包括其中的任何子部件。
  2. 小部件使用的某个已存在的 CSS 类,部件使用者可以在单个 DOM 元素上扩展样式,一个部件上可扩展多个样式。

例如,额外的样式类属性的类型定义为:

  1. type ExtraClassName = string | null | undefined | boolean;
  2. interface Classes {
  3. [widgetThemeKey: string]: {
  4. [baseClassName: string]: ExtraClassName[];
  5. };
  6. }

作为一个提供额外样式类的示例,下面调整 Dojo combobox 实例,以及其中的子部件 text input。此操作会将 combobox 使用的 text input 控件的背景色以及其自身面板的背景色改为蓝色。combobox 控件面板中的下拉箭头也会变为红色:

src/styles/MyComboBoxStyleTweaks.m.css

  1. .blueBackground {
  2. background-color: blue;
  3. }
  4. .redArrow {
  5. color: red;
  6. }

src/widgets/MyWidget.tsx

  1. import { create, tsx } from '@dojo/framework/core/vdom';
  2. import ComboBox from '@dojo/widgets/combobox';
  3. import * as myComboBoxStyleTweaks from '../styles/MyComboBoxStyleTweaks.m.css';
  4. const myExtraClasses = {
  5. '@dojo/widgets/combobox': {
  6. controls: [myComboBoxStyleTweaks.blueBackground],
  7. trigger: [myComboBoxStyleTweaks.redArrow]
  8. },
  9. '@dojo/widgets/text-input': {
  10. input: [myComboBoxStyleTweaks.blueBackground]
  11. }
  12. };
  13. const factory = create();
  14. export default factory(function MyWidget() {
  15. return (
  16. <div>
  17. Hello from a tweaked Dojo combobox!
  18. <ComboBox classes={myExtraClasses} results={['foo', 'bar']} />
  19. </div>
  20. );
  21. });

注意,部件的作者负责显式地将 classes 属性传给所有的要使用样式类的子部件,因为 Dojo 本身无法将这个属性注入给或自动传给子部件。

制作支持主题的应用程序

要为应用程序中所有可主题化的部件指定一个主题,可在应用程序顶层部件中使用 theme 中间件中的 theme.set API。要设置默认的或初始的主题,则在调用 theme.set 之前要先使用 theme.get 进行确认。

例如,为应用程序设置一个初始主题:

src/App.tsx

  1. import { create, tsx } from '@dojo/framework/core/vdom';
  2. import theme from '@dojo/framework/core/middleware/theme';
  3. import myTheme from '../themes/MyTheme/theme';
  4. const factory = create({ theme });
  5. export default factory(function App({ middleware: { theme }}) {
  6. // if the theme isn't set, set the default theme
  7. if (!theme.get()) {
  8. theme.set(myTheme);
  9. }
  10. return (
  11. // the application's widgets
  12. );
  13. });

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

请注意,使用可主题化的部件时,如果没有显示指定主题(例如,没有使用 theme.set 设置一个默认主题,也没有显式地重写部件实例的主题或样式类),则每个部件都使用默认的样式规则。

如果使用一个完全独立分发的主题(/learn/styling/working-with-themes#distributing-themes),应用程序还需要将囊括主题的 index.css 文件集成到自身的样式中来。在项目的 main.css 文件中导入。

src/main.css

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

与之相比,另一种使用外部构建主题的部分内容的方法是通过主题组合功能(/learn/styling/working-with-themes#composing-off-dojo-themes)实现的。

更改当前激活的主题

theme 中间件中的 .set(theme) 函数用于在整个应用程序级别更改当前激活的主题。为 .set 传入所需的主题,这将让应用程序树中所有可主题化的部件失效,并使用新的主题重新渲染。

src/widgets/ThemeSwitcher.tsx

  1. import { create, tsx } from '@dojo/framework/core/vdom';
  2. import theme from '@dojo/framework/core/middleware/theme';
  3. import myTheme from '../themes/MyTheme/theme';
  4. import alternativeTheme from '../themes/MyAlternativeTheme/theme';
  5. const factory = create({ theme });
  6. export default factory(function ThemeSwitcher({ middleware: { theme } }) {
  7. return (
  8. <div>
  9. <button
  10. onclick={() => {
  11. theme.set(myTheme);
  12. }}
  13. >
  14. Use Default Theme
  15. </button>
  16. <button
  17. onclick={() => {
  18. theme.set(alternativeTheme);
  19. }}
  20. >
  21. Use Alternative Theme
  22. </button>
  23. </div>
  24. );
  25. });