介绍近日,华为云DevCloud推出了对开发者友好的深色模式,深受开发者喜爱和关注。众所周知,深色模式(DarkMode)在iOS13引入该功能后,各大应用和网站都开始支持深色模式。在此之前,深色模式多见于程序IDE开发界面和视频网站界面。前者降低了屏幕的亮度,让用户在长时间盯着屏幕后眼睛不那么疲劳;后者使用深色模式来降低噪点,从而突出主要内容。随着产品技术的迭代,支持css自定义属性(也称为css变量,css变量)的现代浏览器可以在运行时实时添加主题,摆脱了传统css主题文件加载方式中主题的预编译内置-处于无法随时修改的劣势。下面我们就来看看如何使用css自定义属性来完成深色模式和主题的开发。主题切换器开发首先我们需要打通一套支持CSS自定义属性的开发模式。使用CSS自定义属性这里简要介绍CSS自定义属性,有时称为CSS变量或级联变量。它包含可以在整个文档中重复使用的值。自定义属性使用--variablename:variablevalue定义,使用var(--variablename[,defaultvalue])函数获取值。举个简单的例子:
/*css*/div{--my-color:red;border:1pxsolidvar(--my-color);}p{颜色:var(--我的颜色);这时候div的边框和内部的p元素就可以使用这个定义的变量来设置自己的颜色了。通常需要在元素中定义CSS自定义属性,通过在:root伪类上设置自定义属性,可以在整个文档中任何需要的地方使用。CSS变量是可以继承的,也就是说我们可以通过CSS继承来创建一些局部主题。我们不会在这里讨论本地主题。我们只需要使用:root伪类来实现整个站点的主题。如何切换主题?我们插入一段运行时传到头部,并通过id或者修改style元素的innerText为:root{--variable1来保留对style元素的引用:颜色值3;--variable2:colorvalue4;...}成功替换变量颜色。由于主题数据可能是从界面等其他地方获取的,我们可以在使用的地方给它加上一个默认值,避免主题数据到达之前没有颜色的现象,比如p{color:var(--variable1,Colorvalue1);}这样通过css自定义属性在运行时动态加载不同的主题色值。Sass/Less支持如果在css开发中直接使用css变量,容易造成编写问题、定义问题等问题,导致变量多,管理困难,更改默认颜色值替换成本高。在大型网站的开发中,通常会使用sass/less来预定义一些颜色变量来进行颜色管理。在使用sass和less的时候,可以把原来传颜色值的方式改成传css自定义属性和默认值。颜色定义文件:这里有一个副作用。一旦将颜色值定义为var变量,var表达式就不能再被less/sass颜色计算函数使用。我们将在后面的章节中讨论这个问题。定义好相应的变量后,这些变量就可以直接在使用的地方使用,方便统一管理。Usingmediaqueryprefer-color-scheme是浏览器获取用户对系统颜色主题偏好的cssapi。使用这个api,我们可以很方便的让网站的主题根据系统的颜色设置显示不同的颜色。cssAPI如下://css@media(prefers-color-scheme:light){:root{--变量1:颜色值1;--变量2:颜色值2;...}}@media(prefers-color-scheme:dark){:root{--Variable1:Colorvalue3;--变量2:颜色值4;...}}`脚本也有相应的媒体查询方案,jsAPI如下://jsfunctionisDarkSchemePreference(){returnwindow.matchMedia('screenand(prefers-color-scheme:dark)').matches;}主题切换服务最后我们要写一个主题服务,主要目的是支持不同应用切换主题时的css变量数据,假设我们的css变量数据存放在一个对象中,key值为css变量名,value值为主题下css变量的值,那么我们主题切换服务的关键核心功能如下://theme.tsexportclassTheme{id:ThemeId;名称:字符串;数据:{\[cssVarName:string\]:string};}//theme-service.tsclassThemeService{contentElement;事件总线;//...applyTheme(theme:Theme){this.currentTheme=theme;if(!this.contentElement){conststyleElement=document.getElementById('devuiThemeVariables');if(styleElement){this.contentElement=
styleElement;}else{this.contentElement=document.createElement('style');this.contentElement.id='devuiThemeVariables';document.head.appendChild(this.contentElement);}}this.contentElement.innerText=':root{'+this.formatCSSVariables(theme.data)+'}';document.body.setAttribute('ui-theme',this.currentTheme.id);//通知外部主题更改this.notify(theme,'themeChanged');}formatCSSVariables(themeData:Theme['data']){returnObject.keys(themeData).map(cssVar=>('--'+cssVar+':'+themeData\[cssVar\])).join(';');}privatenotify(theme:Theme,eventType:string){if(!this.eventBus){return;}this.eventBus.trigger(eventType,theme);}//applyTheme函数会创建一个style元素,如果已经创建了,那么直接改变style的内容如果要支持跟随系统,需要一些额外的函数判断,这里就不展开了,可以参考链接,原理是通过动画结束事件监听媒体查询的变化,对应的可以使用enquirejs库。至此我们已经在开发中打通了主题服务和css变量值的应用,接下来就可以开发深色模式了。深色模式下语义颜色变量的发展深色模式涉及大量的网站视觉“倒色”。在现有的网站中,对网站的颜色进行认真的检查和整理,将颜色归一化,约束在一定的变量范围和数量上,并针对网站的不同使用场景赋予不同的语义变量名色,从而达到场景分离的效果。我们从文字颜色上举个简单的例子:通常的网站中通常会有文字(主文字)、帮助提示信息(次文字)、文字占位符。这里我们可以使用三个变量来描述这些文本text-color-primary、text-color-secondary、text-color-tertiary,或者text-color-normal、text-color-help-info、text-color-placeholder来描述这些颜色值。强烈建议使用更多的语义变量,而不是颜色值本身的描述,例如:错误背景色,应该使用background-color-danger而不是background-color-red,因为颜色值可能不同不同的主题。图1语义变量表示使用统一语义变量控制组件性能需要定义多少个变量,这取决于网站的色彩空间约束范围和使用场景的定义粒度。当定义了一组变量后,我们可以统一组件/网站不同组件的变量。例如,搜索框和下拉框使用相同的变量来控制同一部分的表现,使得组件在主题变化时可以使用相同的颜色规则。图2使用变量指定组件并提供深色主题颜色值完成以上两个重要步骤后,我们就可以通过为变量提供一组新的颜色值来实现主题变化。图3通过色值切换切换到深色主题图像处理图像处理不能像文本那样反转颜色或亮度,这可能会引起不适。通常,如果你有两套图片,亮的和暗的,你可以使用可变图片地址来切割不同主题下的黑色图片。如果图片来自用户输入或其他地方的截图,需要稍微处理一下,降低亮度。图片可以简化获取当前主题状态,可以在body中添加一个ui主题是否为深色模式的属性。深色方案一:增加图片的透明度。适用场景:简单的文章图片和纯色背景。//cssbody[ui-theme-mode='dark']img{opacity:0.8;}深色方案2:在有图片的位置叠加一个灰色的半透明层。适用场景:背景图片、非纯色背景等。//cssbody[ui-theme-mode='dark'].dark-mode-image-overlay{position:relative;}body[ui-theme-mode='dark'].dark-mode-image-overlay::在{内容:''之前;显示:块;位置:绝对;顶部:0;左:0;右:0;底部:0;background:rgba(50,50,50,0.5);}有背景图片的图层处理不适合叠加图片遮挡呈现效果,但是插入文章和博客中使用的图片非常简单有效,图片可以自然叠加在纯深色背景颜色上。后者给出了另一种方案来完成背景层的叠加,但是对代码有一定的侵入性。提供主题变化订阅,应对第三方组件场景通过以上基本步骤,可以在编码过程中使用变量指定颜色值,获取主题能力。但是,面对大量的第三方组件,它们有自己的主题,也可能有自己的深色主题,侵入式修改成自定义变量,工作量很大,未必合适。这时候就需要提供主题订阅,当主题发生变化时得到通知,然后为第三方组件设置一些相应的变化。我们需要一个简单的事件总线,可以用任何方式实现。这是接口的简单版本,如下所示://theme/interface.tsexportinterfaceIEventBus{on(eventName:string,callbacks:Function):void;关闭(事件名称:字符串,回调:函数):无效;trigger(eventName:string,data:any):void;}主题切换时发送themeChanged事件,使用on监听可以获取到当前主题变化事件。通过判断主题,为第三方组件设置相应的主题,或者修改js颜色变量等。降级支持使用scriptputty降级PostCSS插值脚本一旦使用了var,那些不支持var的老浏览器会显示无颜色。这里我们使用postcss插件来处理最后阶段的css。//postcss-plugin-add-var-value.jsvarpostcss=require('postcss');varcssVarReg=newRegExp('var\\\(\\\\-\\\\-(?:.*?),(.*?)\\\\)','g');module.exports=postcss.plugin('postcss-plugin-add-origin-css-var-value',()=>{返回(root)=>{root.walkDecls(decl=>{if(decl.type!=='comment'&&decl.value&&decl.value.match(cssVarReg)){decl.cloneBefore({value:decl.value.replace(cssVarReg,(match,item)=>item)});}});}});postcss插件遍历css规则withvar(--variablename,variablevalue)在该行上一行插入一行替换直接变量value的值,兼容不支持cssvar的浏览器。css-vars-ponyfill使IE9+和Edge12+支持主题切换。css-vars-ponyfill是一个npm包,可以使ie9+/edge12+支持CSS自定义属性。它是具有选项的兼容解决方案。总的原则是将监听样式中var自定义属性的值替换为原来的值插入。此兼容性解决方案目前不兼容直接附加到元素的本地css自定义属性定义。该解决方案还提供了实时监控样式插入的选项,并支持var链式值。只需添加polyfill即可使用。//polyfill.tsimportcssVarsfrom'css-vars-ponyfill';cssVars({watch:true,silent:true});关于哪些网站需要开发深色模式的一些问题?深色模式适用于长时间阅读和长时间沉浸式浏览的网站,包括新闻、博客、知识库等文章浏览和视频网站,开发IDE界面等沉浸式交互。在这些网站上使用深色模式可以通过降低亮度来减少对眼睛的刺激,减少长时间浏览带来的疲劳和头晕的感觉。深色模式不适合一些非深色风格产品的展示。深色的底色会影响产品的风格呈现、传达的情感以及用户观看时的心情。不恰当的配色很容易引起反感。比如一些电商网站的深色模式,就要慎重处理。深色可能会在一定程度上压抑产品形象的正面格调,颜色可能会影响用户的购物欲望。一些主题推广网站也是,颜色可能会削弱主题的表达。有没有更简单的暗模式映射切换?例如,使用HSL而不是RGB颜色值。HSL色值的表现形式是通过色相、饱和度和亮度。既然暗黑模式是调节亮度和饱和度的,那能不能通过hsl色值自动计算呢?这款自动深色版的颜值还有待发掘。主要有两个原因:1)深色模式的舒适性无法通过线性亮度和饱和度映射来完成,颜色函数计算暗色映射相对单调。2)实际情况是一种颜色可能映射到多个暗场景的颜色。关于第一点,目前一些UI引入了非线性的颜色反转算法,也是为了解决亮度一起调节后颜色变得不清晰,颜色反转影响太大的问题。这类算法还有很大的优化空间。在浅色中可能看起来不错的颜色放在深色中可能会引起不适:对比度不合适会导致视觉模糊;不恰当的颜色碰撞会引起反感;饱和度不合适,亮度会让UI有点脏。对于第二点,可以用下面的场景来说明:同样是白色,彩色背景下的白色在深色模式下可能仍然是白色;而作为背景色的白色在黑暗场景中会被调整为深色。图4一种白色切换主题的多种映射此时自动计算颜色值需要区分该颜色的周围颜色或底层叠加颜色进行计算,这无疑增加了计算的难度。所以自动计算这块并不容易,需要进行一番摸索。Sass/Less使用了var变量后,变成了字符串管理,颜色不能转换计算?Sass/less变量和css自定义属性不是一套变量系统。sass/less是编译变量(值在编译时确定,编译后不存在),而css是运行时变量(即值确定时运行)。在使用sass/less管理css变量的时候,为了管理css变量防止定义错误,但是在使用Sass或者Less并用var替换之后,你会发现有些函数如lighten,fadeout,rgba等的sass和less不能使用,因为对于sass和less,var(--xxx,#xxx)是一个字符串而不是颜色值。对此没有更好的方法。一些文章也讨论了一些解决方法,比如链接。大致思路是将颜色的表达拆分成hsl形式,然后对颜色的维度进行操作处理。其实还是不能无感,改用自带的颜色变换功能。另一种方案/方案是:统一涉及颜色变换的地方,然后赋新的css变量名,而不是在mixin等函数中改变颜色,定期改变变量名。如果读者有其他更好的想法,也可以在评论中分享。点击关注,第一时间了解华为云的新鲜技术~