当web应用存在着预览场景时,在进阶体验中势必存在主题配色这样的需求。css
切换主题即总体修改网页中各元素的样式html
成熟的ui库通常都会将用到的关键css属性抽离为单一文件,以供开发者定制颜色、元素间距等。开发者经过修改预设变量值,打包成不一样的主题样式文件,在预览时使用主题相应的样式文件进行全局替换便可达到主题切换的目的。前端
本文如下部分均以antd
为例,其余诸如bootstrap
、material-design-lite
等实际开发工做均大同小异。node
antd是由蚂蚁金服官方维护的,主要针对企业级应用场景设计的前端组件库react
固然,预览区域若是是隔离的(独立页面),那本文完结。。。git
但预览区域若是和配置区域耦合在一块儿,并使用了相同的ui库,该如何应对?因此,诞生了局部主题方案。github
css经过规则权重来确认生效的样式,后面的样式定义覆盖前面的:web
.text {
color: #000;
}
// 这样作会致使预览区的样式污染配置区的样式
.text {
color: #111;
}
// 常见作法是增长一层容器来增长权重
.theme-container .text {
color: #111;
}复制代码
本身定义的属性外层增长一个容器class尚不是难事,但如何为整个antd
库增长外层class?bootstrap
固然,也能够人为地将预览区域隔离开来,使用iframe
、web components
方案都理论可行,不过解决局部样式问题的同时带来了其余的问题须要攻克:sass
web components
是否可行?本文首先使用方案一进行实施,方案二还在继续摸索中,待有突破性进展之后再作分享。
官方提供了几种定制主题的方式,如下是项目中的具体实现:
// theme.less
// 因为antd内部使用了utf8编码的文字符号而不是使用\u****的Unicode码点表示
// 在cdn样式文件返回头里没有明确指明utf-8时,可能存在文字符号乱码的可能
// 编译生成过程当中经过css-loader也会自动转换,而本文场景是直接编译
// 因此,为了安全起见,最好指定编码格式
@charset "utf-8";
@import "/path/to/node_modules/antd/dist/antd.less";
// 覆盖变量定义
// 全部变量详见 https://github.com/ant-design/ant-design/blob/master/components/style/themes/default.less
@primary-color: #F15B41;复制代码
因为须要给样式增长外层class,postcss的插件机制更能符合定制需求,因而使用postcss来处理less。编译脚本大体以下:
#!/usr/bin/env node
const fs = require("fs")
const postcss = require("postcss")
const less = require('postcss-less-engine')
const autoprefixer = require('autoprefixer')
const clean = require('postcss-clean')
const lessContent = fs.readFileSync('/path/to/theme.less', 'utf-8')
postcss([
// 插件配置
less(),
autoprefixer(),
clean(),
]).process(lessContent, {
parser: less.parser,
src: '/path/to/theme.less',
}).then(result => {
const cssContent = result.css
fs.writeFileSync('/path/to/theme.css', cssContent)
})复制代码
这样就能获得使用新配色的主题样式文件了。
目前,css预处理语言中我只使用过less/scss,antd
使用的是less,本文会偶尔拿出scss来进行比较说明。
因为scss文档中明确说明了@import
能够放于选择器下,less文档中并无提到,一开始我担忧不支持该类语法。百闻不如一试,其实less也是支持的:
// theme.less
@charset "utf-8";
.theme-container {
@import "/path/to/node_modules/antd/dist/antd.less";
@primary-color: #F15B41;
}复制代码
如今全部的新主题样式在theme-container下的权重更高,预览区的样式会覆盖配置区的样式,同时不影响配置区自己的样式。
不过,经过查看生成的css(开发阶段能够先去除autoprefixer、clean两个插件)很快发现了问题。因为每一个ui库都会有本身的全局样式,若是将less统一塞在theme-container下会使得全局样式无效:
// 编译出来的全局css
.theme-container html {}
.theme-container body {}复制代码
不过这种问题总体范围来讲影响不大,即便无效也可能没有大问题,出了问题只要简单区分一下便可:
// theme.less
@charset "utf-8";
@import "/path/to/node_modules/antd/lib/style/index.less";
.theme-container {
@import "/path/to/node_modules/antd/lib/style/components.less";
}
@primary-color: #F15B41;复制代码
这样就结束了么?可能一些ui库已经搞定了,不过,antd的故事只是刚刚开始。
&平时使用的不少,但大部分场景下不会在一条规则中使用复数个&,整个antd
用到的其实也很少,可是这些地方就出了问题:
// 原版
.ant-alert {
&&-no-icon {
padding: 8px;
}
}
// 编译后
.ant-alert.ant-alert-no-icon {
padding: 8px;
}
// 增长外层容器后
.theme-container {
.ant-alert {
&&-no-icon {
padding: 8px;
}
}
}
.theme-container .ant-alert.theme-container .ant-alert-no-icon {
padding: 8px;
}复制代码
这个编译结果是正确的,但不符合预期,形成了样式失效。那如何克服呢?
这个方案能够实现,本来antd的less文件内就有不少变量可供使用。
@alert: ant-alert;
.theme-container {
.ant-alert {
&.@{alert}-no-icon {
padding: 8px;
}
}
}复制代码
我查了好久,当我看到这个issue#1075时,一个已经讨论了快5年的issue仍然是open的。我就知道这个方案悬了。
其实里面有不少设想,包括写本文时最新关联的这个issue#3053,都是解决该场景的一种设想。
而为何有这么多奇淫技巧?由于scss是支持的。。。
我稍微花了些时间尝试了下scss在该场景下的实现,原理大体是将&对应的选择器内容做为入参,经过函数将选择器内容进行自定义修改:
// 参考地址:
// https://medium.com/@jakobud/how-to-do-sass-grandparent-selectors-b8666dcaf961
// scss还不支持&&连写,因此换个例子
.theme-container {
.ant-collapse {
& &-item-disabled {
cursor: not-allowed;
}
}
}
// 本来的编译结果
.theme-container .ant-collapse .theme-container .ant-collapse-item-disabled {
cursor: not-allowed;
}
// scss下能够这么作
@function get_last_selector($str) {
$selector: nth($str, 1);
$last: nth($selector, length($selector));
@return $last;
}
.theme-container {
.ant-collapse {
$parent: get_last_selector(&);
& #{$parent}-item-disabled {
cursor: not-allowed;
}
}
}
// 编译结果
.theme-container .ant-collapse .ant-collapse-item-disabled {
cursor: not-allowed;
}复制代码
虽然上述的每种方案都理论可行,不过到最后都没法落地,由于涉及到的改动代码量很大,有些甚至须要修改源码,会致使维护成本太高。
postcss的编译流程大体是:
less => css => optimze(autoprefix + clean)
因为一开始在less层面能区分开antd的全局样式和组件样式,因此一直致力于在less阶段解决问题。但通过了上面的种种波折,在less层面的全部方案基本都没法实施了。
包括antd,全部的ui库都会有一个prefix头来区分样式,这其实也变相提供了在css层面区分开全局样式和组件样式的能力。
因此,只需在css => optimze阶段新增一个插件来为每条组件样式增长一个父类选择器就能够实现局部主题了!
即,最终流程会变为:
less => css => prefix => optimze
// theme.less
// 不须要再使用@import而带来多&问题了
@charset "utf-8";
@import "/path/to/node_modules/antd/dist/antd.less";
@primary-color: #F15B41;
// 插件代码
const prefixPlugin = postcss.plugin('prefix', (PREFIX = '.theme-container') => {
const process = node => {
node.walkRules(rule => {
rule.selectors = rule.selectors.map(selector => {
if (selector.startsWith('.ant')) {
return `${PREFIX} ${selector}`
}
return selector
})
})
}
return process
})
// 编译脚本内加入这个插件
postcss([
less(),
prefixPlugin(),
autoprefixer(),
clean(),
]).then(() => {
// ...
})复制代码
局部主题到最后几行代码就搞定了,antd的故事终于结束了。哦不,等等,还有一个小问题。。。
antd
提供的部分组件,如Modal、Message等的实现,都是在body下插入dom,因此诸如这类样式:
.theme-container .ant-modal { ... }
在应用内都是没法生效的。解决这类问题有两种方式:
class Preview extends PureComponent {
getContainer = () => {
const container = document.createElement('div')
document.querySelector('.theme-container').appendChild(container)
return container
}
render() {
return (
<div id="preview-area">
<Modal visible getContainer={this.getContainer}>
content
</Modal>
</div>
);
}
}复制代码
class Preview extends PureComponent {
render() {
return (
<div id="preview-area">
<Modal visible prefixCls="custom-modal">
content
</Modal>
</div>
);
}
}
// 插件也须要相应的修改一下
const prefixPlugin = postcss.plugin('prefix', (PREFIX = '.theme-container') => {
const process = node => {
node.walkRules(rule => {
rule.selectors = rule.selectors.map(selector => {
if (selector.startsWith('.ant')) {
if (selector.startsWith('ant-modal', 1)) {
return selector.replace(/ant-(modal)/g, 'custom-$1')
}
return `${PREFIX} ${selector}`
}
return selector
})
})
}
return process
})复制代码
局部主题样式是一个很是业务化的场景,需求面较小,比较难提出一些很是通用的知识点。但在方案的制定过程当中,须要了解很多less的特性、postcss的编译流程以及如何编写插件,这些可能在其余实战中获得更普遍的应用,故成此文,将整个方案的演进过程详尽记录下来,但愿能启迪有相似需求的场景并方便本身以后回溯。
从长远来讲,web components
多是这类场景的最佳解决方案,目前的方案还比较偏黑科技,因此这个方案并不会中止演进,让实现变得更合理、更规范。