Ext JS 主题化(Theming) (Classic Toolkit)

Ext JS提供的现成的默认主题可以创建看起来清爽、专业的应用程序. 不过, 你也许想提供自己的样式,以符合你的个人审美,或者符合现有的企业设计.

有史以来,样式化一个应用程序,意味着在渲染组件的时候给HTML元素创建css样式规则. 这种方法会产生几个问题. 首先,你肩负着要支持所有浏览器的重任. 其次,随着框架的成熟,底层组件元素可能会发生改变,留给你的只是不愉快 - 你得跟上这些新的元素结构的样式规则的变化 . 使用Ext JS提供的主题化(Theming) API 来美化你的应用程序,就可以解决这些问题.

使用主题化 API 创建的主题可能会被 Workspace 目录中的所有Ext JS应用程序共享. 这样你可以只样式化一次,而可以重复利用这些样式,使你得应用程序获得一致的外观和感觉. 本指南将列出主题化所必需的内容,并演示创建一个Workspace,应用程序和特定于应用程序的自定义主题.

本指南以使用 classic toolkit (之前的的Ext JS)来创建一个自定义主题开始,将涵盖主题化的基本内容. 下一步, 我们会涉及到给modern toolkit (之前的 Sencha Touch),还有通用(Universal)应用程序(同时包含classic and modern视图)进行主题化的区别. 最后, 我们会介绍 Triton 主题,并展示它给主题家族带来的灵活性.

必要条件

Sencha Cmd 6+

Sencha Cmd 是一个用于打包和部署 Ext JS 的命令行工具. 为了构建 Ext JS 的主题, 你的电脑必须安装有 Sencha Cmd 6 或者更高版本. 更多请看 Sencha Cmd 安装和入门相关内容 Sencha Cmd 介绍.

Java JRE 1.7+ 是运行 “sencha app watch”命令必须的. 此命令运行于应用程序根目录下,并在你更改主题样式代码的时候自动更新编译过后的主题. Cmd 6+ 有两个可用安装包: 带 JRE 的安装包,和不带 JRE 的独立安装包. 注意: 没有 Java JRE 1.7+ ,也不运行“sencha app watch”的话,你应该参照本指南,在每次更改应用程序代码或者主题之后运行 “sencha app refresh”.

Ext JS

自定义主题基于 Ext JS SDK 自带的默认主题. 请先下载 Ext JS 并解压到某个目录下.

构建一个自定义主题

只要你具备了上述必要条件, 你可以继续创建一个完全自定义的主题了.

设立 Workspace (可选)

Sencha Cmd 创建的 Workspaces 有助于让 Ext SDK 和 包(packages), 甚至主题, 可以被此Workspace下面的各个应用程序共享使用. 不过, 设置一个 Workspace 并不是主题化所必需的. 如果你是在一个独立的Ext JS应用程序内创建主题,你可以跳到下一部分.

注意: 如果你不用 workspace, 请确保从示例的文件路径下删除“workspace”.

此次教程我们将创建一个 workspace,这样 workspace 内所有应用程序都可以使用自定义主题. 在 Workspace 目录下执行下面命令 (替换 {path/to/Ext-JS-SDK} 为你之前解压的 Ext JS SDK 路径).

sencha -sdk {path/to/Ext-JS-SDK} generate workspace my-workspace

generate workspace 命令将在目标文件夹 “my-workspace”下创建 Sencha workspace 的骨架. generate workspace 命令拷贝了 Ext JS SDK workspace, 这样主题和应用程序就可以找到必要的依赖. 这个 workspace 是你的自定义主题包(package) 需要的环境. 你还要创建一个应用程序去使用自定义主题. 下一步, 改变命令行的工具目录到 “my-workspace” 下:

cd my-workspace

你可以在你的 workspace 文件夹下看到2个子文件夹:

“ext” – 包含了 Ext JS SDK
“packages” – 含应用程序要使用的 包和主题

创建用于测试主题的应用程序

创建主题之前先要设置测试主题的方法. 使用 classic toolkit 创建一个基本的 Ext JS 应用程序,请在“my-workspace”目录下执行下面命令:

sencha -sdk ext generate app -classic ThemeDemoApp theme-demo-app

现在 Sencha Cmd 在新的子目录“theme-demo-app”下创建了一个名为 ThemeDemoApp 的应用程序. 改变命令行的工具目录到新创建的应用程序下:

cd theme-demo-app

创建一个自定义的主题包(Package) 和文件目录结构

Sencha Cmd 简化了创建一个包含必要文件的自定义主题包的步骤. 在“theme-demo-app” 目录下执行下面命令:

sencha generate theme my-classic-theme

这告诉 Sencha Cmd 在“packages” 目录下创建一个名为 “my-classic-theme”的包. 你应该看到了位于“packages/local/” 目录下的“my-classic-theme”. 我们来看看这个目录下的具体内容:

“package.json” - 这是包的属性文件. 它告诉 Sencha Cmd 一些关于此包的特定的东西,比如包的名字, 版本, 和依赖 (它需要的其它包).

“sass/” - 此目录包含主题所需要的所有 Fashion 源码文件. 源码文件分为4部分: “sass/var/” - 包含变量定义
“sass/src/” - 包含可以使用“sass / var /”中定义的变量的规则和UI mixin调用
“sass/etc/” - 包含附加的工具栏函数或 mixins
“sass/example” - 包含用于图像切片(image slicing)时生成组件样品的文件 (尽管叫这个名字, 但是这个文件夹是 Sencha Cmd 必须的而且不能删除)

“resources/” - 包含图片和其它静态资源文件

“overrides/” - 包含主题化组件的时候需要override组件类的 JavaScript 代码

注意:“sass/var/”, “sass/src”, 和 “overrides”下面的文件和文件夹结构应该匹配组件类文件的路径. 比如, 用于改变 Ext.panel.Panel 样式的变量应该放在“sass/var/panel/Panel.scss”文件里面.

以后, 我们会不断更改主题和应用程序代码. 我们想让 Sencha Cmd 监测到这些改动并重新编译输出到 CSS 文件中. 为了达到这个目的, 执行下面的命令:

sencha app watch

注意: “sencha app watch” 命令需要 Java JRE 1.7+. 如果你执行不了 “sencha app watch” ,你应该在每次更改主题代码之后执行“sencha app refresh”. 为简便起见, 下面的教程中我们假设你已经运行了 “sencha app watch”.

利用“sencha app watch”,Sencha Cmd 6+ 使得更新一个应用程序的样式比以前更容易. 执行“sencha app watch”之后,你可以使用下面的 url 打开你的应用程序: “localhost:1841/theme-demo-app/” ,也可以在 url 后面加上 “?platformTags=fashion:true”:

http://localhost:1841/theme-demo-app/?platformTags=fashion:true

加上 “?platformTags=fashion:true”之后, 当你改变了主题的代码,Sencha Cmd 会指示浏览器去刷新应用程序内样式 - 基本上在一秒之内.

注意: 使用“?platformTags=fashion:true”进行快速更新样式的功能只支持现代浏览器. 大部分主题样式的改动都是实时的,不需要刷新浏览器页面. 不过, 更改与组件尺寸相关的主题样式代码,则需要刷新浏览器.

配置主题继承

每一个 Sencha 主题包 都是更大的主题层次结构的一部分,每一个主题包都必须继承一个父主题. 下一步就是选择继承哪一个父主题. Ext JS附带以下主题:

“theme-base” - 所有主题的基础,也是唯一一个不继承任何父主题的主题. 它包含了 Ext JS 组件所必需、并且让布局可以正常工作的 CSS 样式规则的最小集合. 在派生出来的主题中,“theme-base”里的样式规则都是不可配置的. 在根据“theme-base”创建主题的时候,你应当避免覆盖这些样式规则.

“theme-neutral” - 继承自 “theme-base”的中性(neutral) 主题, 并包含了大量可配置的样式规则. “theme-neutral”里的大多数变量都支持配置,用来改变 Ext JS 组件的外观. 你的自定义主题可以覆盖这些变量.

“theme-neptune” - 现代(Modern)的没有边框(border)的主题. 继承自 “theme-neutral”.

“theme-neptune-touch” - 继承 Neptune 的用于触摸的主题. 继承自 “theme-neptune”. 此主题包括支持在平板设备上使用的 “touch-sizing” 包

“theme-triton” - 使用字体图标的(font-icons)现代(Modern)主题. 继承自 “theme-neptune”.

“theme-crisp” - 简约(Minimalistic) 主题. Extends “theme-neptune”.

“theme-crisp-touch” - Crisp-Based Touch Theme. 继承自 “theme-crisp”. This theme includes the “touch-sizing” package for use on tablets

“theme-classic” - 经典的蓝色 Ext JS 主题. 继承自 “theme-neutral”.

“theme-gray” - 灰色主题. 继承自 “theme-classic”.

“theme-aria” - 无障碍主题. 继承自 “theme-neptune”. 这个主题包括 “无障碍支持(aria)” 包

下面的图标表明了 Ext JS 主题的继承结构.

那么, 你的自定义主题应该继承哪个呢? 我们推荐使用 “theme-neptune”, “theme-triton”, “theme-classic”, 或者“theme-crisp” 作为自定义主题的入口 (或者支持平板设备的“theme-neptune-touch” / “theme-crisp-touch”). 原因是,这些主题包含了所有必须的代码,可用来创建很吸引人的主题. Neutral 主题应该算是一个很有吸引力的主题, 一般不需要直接进行扩展. 通过继承覆盖 “theme-neutral”来创建自定义主题的话,需要改几百个变量,好几个小时的工作,而且只能由高级开发者才能做到. 但是, 一个继承自 Triton, Neptune, Crisp, 或 Classic 的主题则只要花几分钟改变一下一点点变量. 另外, 你可以继承覆盖 “theme-gray” 或“theme-aria”,如果你觉得更加可取的话.

在本教程,我们创建一个继承自 Crisp 的自定义主题. 第一步是配置你需要继承的主题名字. 通过改变“packages/local/my-classic-theme/package.json”文件中的 extend 属性,把下面的默认值:

"extend": "theme-neptune"

改为:

"extend": "theme-crisp"

现在你的自定义主题已经配置为继承自 Crisp 主题了, 而且你的主题和默认的 Crisp 主题是一样的. 下面的步骤,你将对你的自定义主题做一些改变,以区分自Crisp.

配置全局主题变量

我们先来修改 基本颜色,这是很多 Ext JS 组件都继承过来的颜色. 由于使用了 $base-color ,改变 $base-color 将会影响到大部分 Ext JS 组件. 我们来创建一个文件: “packages/local/my-classic-theme/sass/var/Component.scss”. 添加下面的代码到 Component.scss 文件中:

$base-color: #317040;

“$base-color”的值必须是一个有效的 HTML 颜色值; 请参考 HTML 颜色代码 .

Ext JS 全局 Fashion 变量的完整列表请看Global_CSS.

注意: 当初始化一个用户自定义的变量的时候,应该给值包裹上 “dynamic()” ,类似函数调用. 以保证 Sencha 工具可以正确收录(picked up). 当只是给一个已存在的变量赋值的时候,可以不需要“dynamic()”.

在应用程序中使用主题

为了使你的测试应用程序用上你的自定义主题, 请在 “theme-demo-app/app.json”文件中找到下面的代码

"theme": "theme-triton",

把它替换成:

"theme": "my-classic-theme",

因为应用程序的代码和主题已经发生改动,“sencha app watch” 进程将会重新编译应用程序和主题样式. 当“sencha app watch” 完成之后你就可以进“localhost:1841/theme-demo-app/”查看你的应用程序了 (如果没有用app watch的话: “localhost/my-workspace/theme-demo-app/”,前提是你的Workspace根目录部署在了服务器上). 此时你可以看到我们之前给“$base-color”赋值的绿色已经应用到了组件上.

配置组件变量

每一个可以主题化的 Ext JS 组件都有一些可以配置它们外观的变量. 我们来改变“my-classic-theme”主题中 面板标题栏(Panel Headers) 的 font-family . 创建一个名为 “my-classic-theme/sass/var/panel/Panel.scss”的文件并添加下面代码:

$panel-header-font-family: dynamic(Times New Roman);

现在看你的应用程序, 面板标题栏(Panel Headers) 使用的是“Times New Roman” 字体. 你可以在 API 文档的“CSS Variables”部分找到每个组件的所有可用变量. 比如, 请看 Ext.panel.Panel ,滚动到下面标题为 “CSS Variables”的部分

创建自定义组件的用户界面(UI)

Ext JS 框架中的每个组件都有用户界面 (ui) 配置, 默认值是 default. 这个属性可以让每个组件有区别于其他同类型组件的外观. 这个配置在 Neptune 主题中被用来创建不同类型的面板(Panels)按钮(Buttons). 比如, 设置为‘default’界面(UI) 的 panel,有着深蓝色的标题栏,而设置为‘light’界面(UI) 的 panel则有着浅蓝色的标题栏. 按钮(Buttons) 使用 UI则可以让工具栏中按钮的外观区别于普通按钮.

“theme-neutral” 主题包含了用于许多 Ext JS 组件的 混入函数(mixins). 你可以调用这些 混入函数(mixins) 去给组件创建新的 UI. API 文档中列出了每个组件可用的所有 混入函数(mixins). 比如, 请看Ext.panel.Panel ,滚动到下面标题为 “CSS Mixins” 的部分,可以看到 Panel UI 的 混入函数(mixins) 接收什么样的参数. 我们用它来创建一个 Panel UI. 创建名为“my-classic-theme/sass/src/panel/Panel.scss”的文件并添加下面的代码:

@include extjs-panel-ui(
    $ui: 'highlight-framed',
    $ui-header-background-color: red,
    $ui-border-color: red,
    $ui-header-border-color: red,
    $ui-body-border-color: red,
    $ui-border-width: 5px,
    $ui-border-radius: 5px,
    $ui-header-color: white
);

此 混入函数(mixins) 调用会创建一个新的名为 “highlight”的Panel UI,它有红色的标题栏背景, 5px框的红边框, 5px 的边框圆角, 还有白色的字体. 要使用这个 UI, 请给一个 Panel 的ui属性配置为“highlight”(和 frame: true一起用). 打开“theme-demo-app/app/view/main/List.js”文件,替换为下面的内容:

Ext.define('ThemeDemoApp.view.main.List', {
    extend: 'Ext.grid.Panel',
    xtype: 'mainlist',

    ui: 'highlight',
    frame: true,

    requires: [
        'ThemeDemoApp.store.Personnel'
    ],

    title: 'Personnel',

    store: {
        type: 'personnel'
    },

    columns: [
        { text: 'Name',  dataIndex: 'name' },
        { text: 'Email', dataIndex: 'email', flex: 1 },
        { text: 'Phone', dataIndex: 'phone', flex: 1 }
    ],

    listeners: {
        select: 'onItemSelected'
    }
});

在浏览器中查看你的应用程序,你可以看到红色的 UI 为“highlight”的表格(Grid).

虽然使用 UI mixin 给组件创建不同的外观非常方便, 但不应该过度使用. 每一次调用 UI mixin 都会生成额外的 CSS 代码. 不必要的 UI mixins 调用将会使得 CSS 文件体积变得非常大.

为不支持CSS3效果的IE浏览器进行图片切片(Slicing Images)

很多 Ext JS 组件都有圆角和渐变背景. 这些效果在支持CSS3的现代浏览器中很容易实现. 但是, ExtJS 支持 IE8 和 IE9; 但它们都不支持这些效果,创建一个跨浏览器的主题成了一个挑战.

Sencha Cmd 缩小了这种差异,通过在一个无界面浏览器中渲染组件,然后对圆角 / 渐变进行图像切片,并在 IE8 / 9 中使对这些组件使用背景图片. 当新增了一个自定义 UI,你需要将它们包含到 slicing 清单文件中,此清单文件被 Sencha Cmd 用来对IE8 / 9进行组件 UI 的图像切片.

要做到这一点, 我们必须告诉 Sencha Cmd 那一个组件 / UI 需要切片. 以先前创建的“highlight”这个 panel UI 为例, 编辑文件“my-classic-theme/sass/example/custom.js” 并加入下面代码:

Ext.theme.addManifest({
    xtype: 'panel',
    ui: 'highlight'
});

注意: 一个“addManifest” 调用里面可以加入多个清单入口,像这样:

Ext.theme.addManifest({
    xtype: 'panel',
    ui: 'highlight'
}, {
    xtype: 'button',
    ui: 'green'
});

如果你创建了一个组件,你需要把所有要切片的 UI 都添加到切片清单文件中. 你还需要在文件“custom.js”中使用“Ext.theme.addShortcuts()”添加自定义组件的配置项.

传入清单(Manifest)文件的 快捷(shortcut) 配置 和 UI 都会被用于需要进行切片的组件渲染.

关于“Ext.theme.addShortcuts” 和 “Ext.theme.addManifest”更详细的描述,请看“my-classic-theme/sass/example/render.js”里的注释文档. 你可以参考文件“ext/classic/theme-base/sass/example/shortcuts.js”中的关于框架自带组件的“addShortcuts”示例.

主题中的 JS 覆盖(Overrides)

有时候要改变一个组件的外观只能通过JavaScript. 通过添加一个 JavaScript override 文件到主题包中就可以很容易实现了. 为了演示如何做到的, 我们来修改自定义主题中 Panels 的 titleAlign 配置. 创建名为“my-classic-theme/overrides/panel/Panel.js”的文件并添加下面的代码:

Ext.define('MyCustomTheme.panel.Panel', {
    override: 'Ext.panel.Panel',
    titleAlign: 'center'
});

在浏览器中查看应用程序,你可以发现所有的 Panel 标题栏标题都居中了. 虽然Ext JS组件的任何配置都可以以这种方式重写, 最好的做法是只使用 overrides 去改变那些可以直接影响到组件外观的配置.

修改图片资源

所有需要的图片资源都默认继承自父主题, 但是某些情况你可能需要覆盖某一个图片. 很简单,只要把要覆盖的图片放到“my-classic-theme/resources/images/”下即可,图片文件名和原来的一样. 比如, 我们来改变下提示框(MessageBox) 的 消息(info) 图标. 保存下面图片到 “my-classic-theme/resources/images/shared/icon-info.png”. 这个图片将会取代父主题 Crisp 中的 “my-workspace/ext/classic/theme-crisp/resources/images/shared/icon-info.png”.

现在来修改你的应用程序,弹出一个显示有自定义图标的提示框. 在文件“theme-demo-app/app/view/main/List.js”中添加 tbar 配置:

...
tbar: [{
    text: '显示消息',
    handler: function() {
        Ext.Msg.show({
            title: 'Info',
            msg: '带自定义图标的消息框',
            buttons: Ext.MessageBox.OK,
            icon: Ext.MessageBox.INFO
        });
    }
}]
...

现在, 在浏览器中查看应用程序. 当你点击“显示消息”按钮,你应该可以看到一个带笑脸图标的消息框.

添加自定义 Fashion 工具类

如果你的主题需要一些与组件样式无关的函数或者混入(mixins) (比如 工具类), 它们应该放在“my-classic-theme/sass/etc” 目录中. 里面的文件目录结构随便你,不过 Sencha Cmd 唯一会引用的文件是“my-classic-theme/sass/etc/all.scss”. 其他所有文件都需要被文件“all.scss” 引入(@import). 具体例子请查看“ext/classic/theme-base/sass/etc/”.

应用程序自身的样式

即不被各个应用程序共享的,不再主题中的样式,而是某个应用程序自己独有的样式. Sencha Cmd 提供了一个简单的方式,可以在应用程序级别定义样式,规则和主题样式一样. 应用程序级别的样式是主题继承链中的最后一级. 应用程序可以改变主题变量,也可以添加自己定义的变量和规则,来样式化应用程序中的视图(View).

在应用程序中改变主题变量

继续使用上面的 “theme-demo-app” 应用程序, 我们来覆盖主题的 $base-color变量. 打开文件 “theme-demo-app/sass/var/all.scss” 并添加下面的代码:

$base-color: #724289;

在浏览器中查看应用程序,可以看到基本颜色变成了紫色.

应用程序中的每一个类, Sencha Cmd 都会检查“sass/var/”下的相对应的定义有变量的 “.scss”文件,和“sass/src/”下的相对应的定义有样式规则的“.scss”文件. 因为应用程序有一个类名字为 “ThemeDemoApp.view.main.Main”, Main视图相关的变量应该放在“theme-demo-app/sass/var/view/main/Main.scss” 文件中,以便build过程中可以包含进去.

一个常见的错误是试图在应用程序的“sass” 目录中改变 Ext JS 类的样式 . 比如, 通过添加一个“theme-demo-app/sass/var/grid/Panel.scss”文件来尝试改变“Ext.grid.Panel”的变量,这是错误的. Sencha Cmd 检查应用程序的 “sass” 目录的 命名空间(namespace) 用的是程序名称(“ThemeDemoApp”). Sencha Cmd 不会为 命名空间为“Ext”下的类检查.

注意: 你可以阅读此向导的“Namespace” 部分,了解更多关于 Sencha Cmd 处理 主题路径 / 命名空间的内容 .

样式化应用程序中的视图(View)

应用程序中的视图(View)相关的 CSS 样式规则应该放在应用程序目录下的“sass/src/” 中,路径和视图的类名应该一致. 我们来改变ThemeDemoApp 应用程序中 Users 这个标签页的样式. 因为 Main 视图是定义在“theme-demo-app/app/view/main/Main.js”,名为 ThemeDemoApp.view.main.Main 的类, 所以 CSS 样式规则文件放在 “theme-demo-app/sass/src/view/main/Main.scss”:

.content-panel-body h2 {
    color: orange;
}

给 Main.js 文件的Users 这个 panel 设置此“content-panel-body”样式类名:

...
title: 'Users',
iconCls: 'fa-user',
html: '<h2>Content appropriate for the current navigation.</h2>',
bodyCls: 'content-panel-body'
...

在浏览器中查看应用程序,可以看到 Users 视图中的h2 元素现在变成橘黄色了. 需要跨多组件使用的 CSS 样式同样需要放在“theme-demo-app/sass/var/all.scss”文件中. 虽然添加任意 CSS 样式提供了很大灵活性, 任何直接应用到视图(继承自 Ext JS 组件)的样式,应该尽量使用 Ext JS 的主题 API. 使用 主题API 可以确保你的样式不会在未来版本的 Ext JS 中失效或异常.

命名空间(Namespace)

Sencha Cmd 在主题 / 应用程序的 “sass/var” 和 “sass/src” 文件夹下寻找“.scss” 文件,以对应到每一个它解析的 框架 / 应用程序 类名. 文件路径和文件类名一一映射. 在每一次类名解析的时候, Sencha Cmd 会使用已配置的 sass.namespace 来决定去哪寻找 “.scss” 文件.

对于主题, 默认的 sass.namespace 是 “Ext”. 所以, 任何“Ext.*”开头的类,Sencha Cmd 解析的结果是主题的“sass/var” 和“sass/src” 文件夹. 比如, 解析 “Ext.button.Button”的时候,Sencha Cmd 会寻找“my-classic-theme/sass/var/button/Button.scss”和“my-classic-theme/sass/src/button/Button.scss”.

对于应用程序, 默认的 sass.namespace是当前应用程序的名称(此处示例则是ThemeDemoApp). 所以, 任何“ThemeDemoApp.*”开头的类名,Sencha Cmd 解析的结果是应用程序的“sass/var” 和 “sass/src” 文件夹. 比如, 解析“ThemeDemoApp.view.main.Main”的时候,Sencha Cmd 会寻找“theme-demo-app/sass/var/view/main/Main.scss” 和 “theme-demo-app/sass/src/view/main/Main.scss”.

当映射类名到“.scss” 文件路径的时候,上面两种情况, “sass/var” 和 “sass/src”都用的是 sass.namespace的值. 如果要给一个主题或应用程序下的多个命名空间定义样式的haunt,可以设置sass.namespace 的值为空.

对于主题, 你可以给文件“my-classic-theme/local/package.json”设置 sass.namespace 的值为“”. 对于应用程序你也可以给文件“them-demo-app/app.json”一样设置.

“sass”: {
    “namespace”: “”
}

设置命名空间为“”,你需要显式地在文件路径结构中定义命名空间. 比如, “Ext.button.Button”类的变量需要放到“sass/var/Ext/button/Button.scss”文件中. 类“ThemeDemoApp.view.main.Main”的混入函数(Mixins) 需要定义在“sass/src/ThemeDemoApp/view/main/Main.scss”文件中.

生成的样式的结构

按上述所说的方式使用主题, 最终位于主题里、应用程序里、你用到的所有包(packages) (请看Sencha Cmd 包(Packages) 向导) 里面的样式,都被合并到了“app-all.scss”文件中,然后会被 Sencha Cmd 编译. 理解此文件的结构是很重要的,这样你才能知道在你的主题或包中能用什么,什么时候用.

生成的“all.scss” 文件结构如下is:

+---------------------------------------+
| inclusion flags                       |
+-----------+-----------+---------------+
|           |           | base          |
|           | theme     +---------------+
|           |           | derived       |
|           +-----------+---------------+
|           |                           |
|    etc    | packages (dep order)      |
|           |                           |
|           +---------------------------+
|           |                           |
|           | application               |
|           |                           |
+-----------+---------------------------+
|           |           | base          |
|           | theme     +---------------+
|           |           | derived       |
|           +-----------+---------------+
|           |                           |
|    var    | packages (dep order)      |
|           |                           |
|           +---------------------------+
|           |                           |
|           | application               |
|           |                           |
+-----------+-----------+---------------+
|           |           | base          |
|           | theme     +---------------+
|           |           | derived       |
|           +-----------+---------------+
|           |                           |
|    src    | packages (dep order)      |
|           |                           |
|           +---------------------------+
|           |                           |
|           | application               |
|           |                           |
+-----------+---------------------------+

在“sass/var” 和 “sass/src”中, 给定的主题、包和应用程序中的各个 “*.scss”文件,都是被排过序的,以符合 JavaScript 类的继承关系. 比如, 在“base” 主题的“sass/var”中有被Ext.window.WindowExt.panel.Panel 使用的“.scss” 文件, Ext.panel.Panel 对应的“.scss” 文件会在Ext.window.Window 对应的“.scss” 文件之前被包含进去,因为它继承自Ext.panel.Panel. 这种特殊结构的目标和原理如下:

在“sass/etc”空间, 从父主题中继承的工具类函数之类的,应该可以被派生的主题使用.

包应该能够使用当前主题提供的设施.

应用程序应该能够使用它们的主题和所有用到的包.

在“sass/var”空间, 需要关注的是变量控制, 派生计算.

接下来取得应用程序中变量的值, 但是它们从派生主题到父主题是倒序的. 这使得派生的主题可以设置变量的值,但是这些变量还没被应用程序或者派生程度更高的主题所设置.

包(package) 变量是以包依赖的顺序引入的. 这使得包(package) 变量从当前主题中继承变量值 (更重要的, 继承base-color的值).

应用程序应该能控制所有变量的值, 所以它们的变量出现在最后.

“sass/src” 部分, 顺序和“sass/etc”中的一样. 这使得规则有了正确的级联顺序,保证派生主题可以比父主题拥有更高的优先级.

应用程序在级联链的最后一级,以确保其规则有最高的优先级.

用于是否包含的标志位(Inclusion Flags)

用于是否包含的标志位变量,指的是一种只读变量的集合,它们的值只能为true或者false,用来标识是否包含了某个 JavaScript 类. 如果某个类在应用程序build过程中被包含了,则它的值为true. 这些变量是被 Sencha Cmd 动态创建的,变量名以“$include-” 开头,然后是 JS 类名(需要把“.”替换为“-”,并且全部小写). 比如, 如果包含了类 Ext.grid.Panel, 你可以用下面的代码判断true值:

@if $include-ext-grid-panel {
    // styling contingent upon the presence of Ext.grid.Panel in the app
}

注意: 整篇指南,我们都提倡用“sencha app watch” 来在应用程序代码或主题发生变更时进行增量 build. 然而, 在开发环境下(也就是“sencha app watch” 所运行的环境),“$include-” 变量全都被设置为了 true . 为了测试你的主题的“$include-” 变量,你需要build testing 或 production:

sencha app build testing
sencha app build

在多个应用程序之间共享主题

和第二个应用程序共享主题很简单. 只要到 “my-workspace” 目录下执行下面的代码 (注意: 如果你运行了“sencha app watch”,你应该按下“ctrl-c”终止watch):

sencha -sdk ext generate app -classic AnotherApp another-app

这句命令告诉 Sencha Cmd 在 “another-app” 目录下创建另一个名为“AnotherApp” 的应用程序,并和第一个应用程序使用同一个 Ext JS SDK.

为了确保监视应用程序和主题的改动,请改变命令行工作目录到此app目录下,并运行“sencha app watch”:

cd another-app
sencha app watch

第二步就是让此应用程序使用自定义主题. 编辑 “another-app/app.json”并替换:

/**
 * The name of the theme for this application.
 */
"theme": "theme-triton",

为:

/**
 * The name of the theme for this application.
 */
"theme": "my-classic-theme",

当在浏览器中运行 “another-app/index.html”时,你可以看到一个和“ThemeDemoApp”主题一样的应用程序启动了.

构建(Building) 主题

构建主题会在你的主题包目录下创建一个“build” 文件夹. 在“my-classic-theme/build/resources”里你会发现有一个名为“my-classic-theme-all.css”的文件. 这个文件包含了所有 Ext JS 组件所需要的样式. 你可以直接在应用程序中引用这个css, 不过不建议这样做. 当 Sencha Cmd 以 production 方式构建应用程序时, 它只会把此应用程序所需要的js和样式文件包含进来. 构建主题产生的 “all” 文件 包含了所有的 Ext JS 组件,不应该在 Sencha Cmd 生成的 app 中使用.

Triton 主题图标

附加说明

‘default’ 组件图片

很多组件都有和它们的“default” ui相关的图片 (按钮Buttons, 菜单Menus, 等等.). 当你给某个组件创建了一个自定义 ui,你会发现主题编译的时候会警告说主题所需的图片未找到.

WARNING: @theme-background-image: Theme image not found:

刷新主题 / 应用程序的时候, Sencha Cmd 会寻找文件名为你的 ui 开头的图片(也就是‘default’ui时,图片文件名中“default”的位置换成自定义 ui 的名称). 比如, 你给小按钮创建了一个名为“admin” 的ui 混入函数(mixin), Sencha Cmd 会警告“admin-small-arrow.png” 未找到.

这个警告的解决方案是从父主题中,名字以‘default’开头的图片资源拷贝到你的自定义主题中的 “resources/images” 目录下. 并重命名文件名,把“default” 替换成你的自定义 ui 的名称. 于是, 在这个例子中, 你的继承自Neptune的自定义主题中的“admin” 这个按钮 ui,你需要从“ext/classic/theme-neptune/resources/images/button”下拷贝“default”开头的图片,并粘贴到“packages/my-classic-theme/resources/images/button/”. 然后替换 “default” 为 “admin”. 例如:

default-small-arrow.png

需要命名为:

admin-small-arrow.png

注意: 批量文件名称更改工具会在这里派上用场.

Ext.button.Button

按钮(Button)自定义 ui 的图片需要从父主题下拷贝到自定义主题下. 请看上面的“‘default’ 组件图片” 部分.

按钮的 大小(scale) 可以被设置为 small, medium, 或 large ,默认值是 small. 给按钮创建自定义 UI 的时候你需要为按钮的每一个 大小(scale) 都调用 混入函数(mixin).

注意: “extjs-button-ui” 混入函数应该避免使用,而用特定的带有 -scale 的混入函数.

@include extjs-button-small-ui(
    $ui: 'green',
    $background-color: green
);

@include extjs-button-medium-ui(
    $ui: 'green',
    $background-color: green
);

@include extjs-button-large-ui(
    $ui: 'green',
    $background-color: green
);

用“-toollbar” 按钮混入函数的时候也类似. 每一种大小都应该在 Button.scss 文件中单独写上以便支持所有按钮的大小. 另外, 当用“-toolbar” 的时候,你需要在ui配置上加上“-toolbar”. 下面是一个混入 小的工具栏按钮(small toolbar button) 的例子:

@include extjs-button-toolbar-small-ui(
    $ui: 'green',
    $background-color: green
);

在工具栏中使用的时候像这样:

xtype: 'toolbar',
items: [{
    text: 'Toolbar Button',
    ui: 'green-toolbar'
}]

Ext.panel.Panel

Panels 可能会配置为 frame: true ,默认是 frame: false. 所以, 如果你有一个ui: ‘highlight’ ,那么你的Panel.scss 会像下面这样:

@include extjs-panel-ui(
    $ui: 'highlight',
    $ui-header-background-color: red,
    $ui-border-color: red,
    $ui-header-border-color: red,
    $ui-body-border-color: red,
    $ui-border-width: 5px,
    $ui-border-radius: 5px
);

不过, 这个样式只会应用程序到没有外边框的 panels. 为了能样式化配置了frame: trueui: ‘highlight’的 panels,你需要在 Panel.scss 文件中加一个带有“-framed”的 $ui 名字. 一般 Panel.scss 中会同时具有带外边框和不带外边框的版本

@include extjs-panel-ui( $ui: ‘highlight’, $ui-header-background-color: red, $ui-border-color: red, $ui-header-border-color: red, $ui-body-border-color: red, $ui-border-width: 5px, $ui-border-radius: 5px );

@include extjs-panel-ui( $ui: ‘highlight-framed’, $ui-header-background-color: red, $ui-border-color: red, $ui-header-border-color: red, $ui-body-border-color: red, $ui-border-width: 5px, $ui-border-radius: 5px );

Ext.menu.Menu

菜单(Menu)自定义 ui 图片需要从父主题下拷贝到自定义主题下. 请看上面的“‘default’ 组件图片” 部分.

Ext.toolbar.Breadcrumb

面包屑导航(Breadcrumb) 自定义 ui 图片需要从父主题下拷贝到自定义主题下. 请看上面的“‘default’ 组件图片” 部分.

Ext.tab.Tab

标签页按钮(Tab)自定义 ui 图片需要从父主题下拷贝到自定义主题下. 请看上面的“‘default’ 组件图片” 部分.

创建标签页按钮 ui 时,请确保设置了所有你需要样式化的按钮状态相关的变量,也就是带有“-active”的按钮状态, 比如 $ui-color-active, $ui-background-color-active, 等等.

Ext.tab.Bar

标签页工具栏(TabBar)自定义 ui 图片需要从父主题下拷贝到自定义主题下. 请看上面的“‘default’ 组件图片” 部分.

注意: 当用 extjs-tab-bar-ui 混入函数 创建一个标签页工具栏 UI 的时候, 你还需要创建一个相同名字的 tab-ui.

这样可以确保在你的主题下,标签页可以正确渲染. 如果创建了不匹配的 tab 主题,结果可能导致渲染时出现未预料的问题.

Ext.toolbar.Toolbar

工具栏(Toolbar) 自定义 ui 图片需要从父主题下拷贝到自定义主题下. 请看上面的“‘default’ 组件图片” 部分.

从Ext 5 升级到 Ext 6 Classic

虽然从 Ext JS 5 升级到 6,升级主题的时候,大部分主题的更新发生在幕后, 但是你还需要做一些更改.

任何定义在 {appRoot}/sass/etc/all.scss中的变量需要移至 {appRoot}/sass/var/all.scss (或者被 all.scss 引入 @import 的 .scss 文件)

(_建议_) 从变量初始化声明中移除 !default,应该用dynamic()来代替. 这样可以确保变量 1) 可以被重新赋值 2) 会被 Sencha 工具收录.

(_建议_) 把你的 应用程序 / Workspace 中位于 “packages/” 文件夹下的主题包移动到”packages/local/”目录下.

Last updated