漫谈 Angular 定制主题的四种方式
主题定制是提升用户体验最常见的一种,前端框架众多,主题定制方式却异曲同工,下面来介绍一下 Angular 中实现主题定制的四种方式。
1. webpack loader
React 版本的 Ant Design 使用 less-loader 加载 globalVars 与 modifyVars 变量,并通过 less 的 render 方法传递 callback 到 loader 来实现的项目的主题修改功能。
目前绝大部分的 angular 项目同样使用 webpack 打包方案。显然,相同的主题修改方案在 angular 中一样适用。
webpack 打包 less
- webpack 本身并不具备打包 less 文件的功能,最终实现该部分功能的是 less-loader,该加载器把 less 转为 CSS,在 webpack 中每个文件或模块都是有效的 JS 模块,因此我们还需要 css-loader 将CSS样式文件转换为变成 JS 模块。
- 这时我们已经有了生成的 dist/style.js,在这个模块中只是将样式导出为字符串并存放于数组中,我们需要 style-loader 将该数组转换成 style 标签。
- 最后我们还需要将 dist/style.js 自动导入 到 html 中,html-webpack-plugin 可以帮我们实现这部分功能。
- 除了以上这些 loader,我们可能还需要 autoprefixer、cssnano 和 postcss-loader 等,有兴趣的同学可以自行了解。
modifyVars
上面介绍的 less-loader 可以帮忙我们实现主体定制,这里涉及到两个重要的配置:
- globalVars:相当于给每个 less 文件顶部增加一行 @VariableName: xx;
- modifyVars:相当于给每个 less 文件底部增加一行变量 @variable:xx;
通过这两个配置,我们就可以把部分样式抽出变量,通过不同的变量组合成不同的主题。
custom-webpack
angular-cli 提供了 custom-webpack 的 builder,可以和 angular-cli 合并使用,通过 builder 重写 webpack 中的 less-loader 的配置,然后利用 modifyVars 实现主题定制。
- 安装
npm i -D @angular-builders/custom-webpack
-
在根目录新建 webpack 配置文件 extra-webpack.config.js
module.exports = { module: { rules: [ { test: /\.css$/, use: [ "style-loader", "css-loader" ] }, { test: /\.less$/, use : [ { loader : "less-loader", options: { modifyVars: { // 修改主题变量 "primary-color": "red" }, javascriptEnabled: true } } ] } ] } }
3.在 angular.json 中使用 @angular-builders/custom-webpack:browser
"architect": { "build": { + "builder": "@angular-builders/custom-webpack:browser", - "builder": "@angular-builders/build-angular:browser", "options": { "customWebpackConfig": { "path": "./extra-webpack.config.js" }, "outputPath": "dist/custom-webpack", "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.app.json", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/styles.less" ] } ... } }
这样就可以实现 less 原理的主题定制了,当然 custom-webpack 不仅仅可以做到 less-loader 的重写,它还可以利用 webpack 实现更多功能,具体研究我们在下一篇文章再来探讨;
如果你想进一步了解在 angular cli 中自定义 webpack 打包的方案,可以参考这篇文章
笔者准备好了可以直接使用的源代码,方便大家查看 点击查看源码
纯 webpack 打包
如果开发者的项目未使用 Angular CLI,也可以通过同样的方式实现自己的 webpack 打包器:
-
在根目录添加 webpack.config.js 文件。
-
运行命令 webpack 或者 webpack-dev-serve,即可查看效果。
笔者准备好了可以直接使用的源代码,方便大家查看 点击查看源码
可能很多开发者并不熟悉 less,开发过程中大多用纯 CSS,纯 CSS 能否实现主题定制了?答案是肯定的,下面我们来探讨一下纯 CSS 的主题定制。
2. CSS Variable
CSS3 提供了 Variable, 利用 angular Directive 指令,动态修改 CSS Variable,从而得到主题切换的效果。注意:CSS Variable 支持的浏览器可以在 这里 查看
.element{--main-bg-color: brown;} // 声明局部变量
.element{background-color: var(--main-bg-color);} // 使用局部变量
:root { --global-color: #666; --pane-padding: 5px 42px; } // 声明全局变量
.demo{ color: var(--global-color); } // 使用全局变量
有了以上的的基础知识,我们很容易想到如何在 angular 中实现基于 css Variable 的主题切换功能,我们只需要一个 Directive 可以根据 @Input 输入动态切换 style 即可。
1.创建一个指令:ThemeDirective,用来给需要 CSS 变量的标签添加样式
import { Directive, ElementRef, Input, OnChanges } from '@angular/core';
@Directive({
selector: '[dtTheme]'
})
export class ThemeDirective implements OnChanges {
@Input('dtTheme') theme: {[prop: string]: string};
constructor(private el: ElementRef<HTMLElement>) {
}
ngOnChanges() {
Object.keys(this.theme).forEach(prop => {
this.el.nativeElement.style.setProperty(`--${prop}`, this.theme[prop]);
});
}
}
2.创建一个组件:app.component.ts
import { Component } from '@angular/core';
@Component({
selector : 'app-root',
template: `
<select (input)="setTheme($event.target.value)" title="theme" class="form-control">
<option value="">- select theme -</option>
<option>green</option>
<option>pink</option>
</select>
<app-trex [dtTheme]="selectedTheme"></app-trex>
`,
styleUrls : [ './app.component.less' ]
})
export class AppComponent {
readonly themes = {
'green': {
'color-main' : '#3D9D46',
'color-main-darken' : '#338942',
'color-main-darken2': '#286736',
'color-main-lighten': '#7BBC4D',
'color-accent' : '#DC3C2A'
},
'pink' : {
'color-main' : '#E05389',
'color-main-darken' : '#CA3E86',
'color-main-darken2': '#C13480',
'color-main-lighten': '#E77A96',
'color-accent' : '#208FBC'
}
};
selectedTheme = {};
setTheme(val) {
this.selectedTheme = this.themes[val];
}
}
3.创建一个trex.component.ts组件
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
@Component({
selector: 'dt-trex',
template: `
<div class="class1">aaaa</div>
<div class="class2">bbb</div>
<div class="class3">ccc</div>
<div class="class4">ddd</div>
`,
styles:`
.class1{color:var(--color-main, #ff0000);}
.class2{color:var(--color-main-darken);}
.class3{color:var(--color-main-darken2);}
.class4{color:var(--color-main-lighten);}
`
})
export class TrexComponent {
constructor() { }
}
CSS 定制主题完成了,笔者准备好了源代码,方便大家查看,点击查看源码
但是这种方式有个缺点,浏览器最好支持 CSS3 Variable,如果不支持 CSS3 Variable,那么我还是建议你使用 less 变量。如果你并不想采用 less 的 modifyVars 方式,或者不想重写 webpack,那么以下这种方式也许适合你。
3. Angular Configuration
Angular 的组件默认工作在 ViewEncapsulation.Emulated 模式下,在这个模式下,应用程序的dom元素都会附加额外的属性,而 index.html 被添加的 style 会包含这些属性,从而做到组件样式的隔离;但是 component 中的样式,打包后最后会以 JS 形式出现(原理可查看上面 “webpack 打包原理”)。
因此如果想实现主题定制,实际上是需要打多个 angular 的生成包,不过值得高兴的是 angular-cli 原生支持同时生成多个 package,我们可以配置 light 和 dark 变量文件,利用 angular-cli 的 builder 打多个主题包,然后利用路由切换不同的主题。
- ViewEncapsulation.Emulated(默认)样式将被包装到 style 标签中,推送到 head 标签,并唯一标识,以便与组件的模板匹配,样式将仅用于同一组件中的模板。
- ViewEncapsulation.ShadowDom 全局样式都不会影响后代组件
- ViewEncapsulation.Native 已弃用
- ViewEncapsulation.None 样式包裹在 style 标签中并推送到 head,紧跟在组件内联和外部样式之后,属于全局样式。
下面简单介绍一下这种方式的实现流程:
1.配置全局样式 style.less
注意:customizetheme 是文件夹名称,存放于 src/product-configurations/styles/(light|dark)下,利用 angular.json 中的 stylePreprocessorOptions(允许添加额外的基准路径,这些基准路径将被检查予以导入,Import ‘customizetheme’,可以成功导入,再也不用写很长的../../相对路径)
@import 'customize_theme';
2.配置 angular.json
注意:升级到 angular8.0 后,configurations 中的 key(如 ligth-theme)不能包含“:”(踩坑),原因这里查看
"configurations": {
"light-theme": {
"stylePreprocessorOptions": {
"includePaths": [
"src/styles",
"src/product-configurations/styles/light"
]
}
},
"dark-theme": {
"stylePreprocessorOptions": {
"includePaths": [
"src/styles",
"src/product-configurations/styles/dark"
]
}
}
...
}
3.配置 packge.json
{
"name": "app",
"version": "0.0.1",
"scripts": {
"build:light": "ng build --project=app-build --configuration=light-theme",
"build:dark": "ng build --project=app-build --configuration=dark-theme"
}
...
}
这种方式缺点很明显,需要打包后切换不同语言包,打包时间翻倍,且需要路由来控制语言切换,每次切换语言都要重新加载,性能上比较浪费。既然如此,如何避免这些缺陷了?下面来介绍一种既简单又性能好的方式。
4. :host-context()
:host-context() 是 webComponents 下的 selector,很多人可能都没有使用过,但是却是相对而言最适合的主题切换方式。注意 :host-context() 支持的浏览器可以在 这里 查看
:host-context(.theme-light) h2{
// 基于当前组件向上查找 .theme-light,有则应用到组件的 h2 中
}
下面来介绍一下实现这种主题定制的流程:点击查看源码
1.配置 angular.json,暴露两个主题文件
"styles": [ "src/light.less","src/dark.less" ]
2.修改 dark.less 和 light.less 文件
@html-selector: html;
@primary-color: blue;
@html-selector: html;
@primary-color: red;
3.配置全局样式 styles.less 文件
.themeMixin(@rules) {
:host-context(.dark) {
@import "theme-dark";
@rules();
}
:host-context(.light) {
@import "theme-light";
@rules();
}
}
4.配置 app.component.less 应用
@import "../styles";
.themeMixin({
p {
color: @primary-color;
}
});
5.在浏览器中给 body 添加 class=‘dark|light’,即可看到效果。
以上方式可以实现 less 的主题动态切换,无需打包和设置路由,但是 :host-context() 和 :host 混用,会有些问题,具体可查看这里。
其他组件中有主题概念,需要用 themeMixin
包起来使用,此外 @html-selector
变量可以实现两种主题共同存在,如果你需要的话。
对比以上四种方式
定制方式 | 浏览器支持 | 打包次数 | :host混用 | 流程复杂度 |
---|---|---|---|---|
webpack loader | 都支持 | 多次打包 | 支持 | 比较复杂 |
CSS Variable | Chrome 49以上、FireFox 31以上、Safari 9.1以上、IE不支持 | 1次打包 | 支持 | 简单直接 |
Angular Configuration | 都支持 | 多次打包 | 支持 | 简单直接 |
:host-context() | Chrome 54以上、opera 41以上,FireFox 、Safari、IE不支持 | 1次打包 | 不支持 | 比较复杂 |