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

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 属性来传入额外的样式类。这些样式类是追加的,不会重写部件已有的样式类,它们的目的是对已经存在的样式进行细粒度的调整。提供的每一组额外的样式类都需要按照两个级别的 key 进行分组:

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

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

type ExtraClassName = string | null | undefined | boolean;

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

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

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 设置一个默认主题,也没有显式地重写部件实例的主题或样式类),则每个部件都使用默认的样式规则。

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

src/main.css

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

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

更改当前激活的主题

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>
    );
});