皮肤换肤设计方案
大概的思路就是给html根标签设置一个data-theme属性,然后通过js切换data-theme的属性值,Scss根据此属性来判断使用对应主题变量。这里可以选择持久化Vux或接口来保存用户选择的主题。
一、首先需要给项目下载配置Scss
1.安装依赖
npm install node-sass sass-loader --save-dev
2.找到build中webpack.base.conf.js,在rules中添加scss规则
{
test: /.scss$/,
loaders: ['style', 'css', 'sass'v]
二、在vue项目全局中引入scss
1.安装 sass-resources-loader
npm install sass-resources-loader --save-dev
2.然后修改build中的utils.js
将
scss: generateLoaders('sass')
修改为:
scss: generateLoaders('sass').concat(
{
loader: 'sass-resources-loader',
options: {
//你自己的scss全局文件的路径
resources: path.resolve(__dirname, '../src/style/_common.scss')
}
}
)
使用媒体查询
prefer-color-scheme是浏览器获取系统上用户对颜色主题的倾向性的css api,使用该api我们就可以轻松使得网站的主题跟随系统的颜色设置展示不同的颜色了。
css的API如下:
// css
@media (prefers-color-scheme: light) {
:root{--变量1: 色值1;--变量2: 色值2; ……}
}
@media (prefers-color-scheme: dark) {
:root{--变量1: 色值3; --变量2: 色值4; ……}
}
脚本方面也有对应的媒体查询方案,js的API如下:
// js
function isDarkSchemePreference(){
return window.matchMedia('screen and (prefers-color-scheme: dark)').matches;
}
语义化色彩变量
深色模式涉及到了大量网站视觉的“反色”,在已有的网站当中,应该好好排查和梳理网站的颜色,把颜色归一和约束到一定的变量范围和数量里,并给颜色的不同使用场景一个不同的语义变量名,这样能取得场景分离的效果。
从文本颜色上我们举个简单的例子:
通常的网站里都会有正文(主要文本),帮助提示信息(次要文本),文本占位符。这里我们可以使用三个变量来描述这些文本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 语义化变量示意
提供主题变化订阅应对第三方组件场景
通过以上几个基本的步骤就能在编码的过程中通过使用变量指定颜色值,获得主题的能力。但是面对大量第三方组件,有自己的主题,也可能有自己的深色主题,这块再去入侵式地修改成自定义的变量工作量不小且并不一定合适。
这时候需要提供主题订阅,在主题发生变化的时候,获得通知,然后给第三方组件设置一定对应的变更。
我们需要一个简单的eventbus,实现方式不限。这里给出一个简单版本的接口如下:
// theme/interface.ts
export interface IEventBus {
on(eventName: string, callbacks: Function): void;
off(eventName: string, callbacks: Function): void;
trigger(eventName: string, data: any): void;
}
切换主题的时候发出themeChanged事件,使用on监听就能够获得当前主题变更事件,通过判断主题,给第三方的组件套上对应的主题,或者修改js颜色变量等等。
使用统一语义变量控制组件表现
需要定义多少的变量才恰当,这个取决于网站的色彩空间约束范围和使用场景的定义粒度。当定义了一套变量之后我们就可以对组件/网站的不同组成部分进行变量统一。
比如搜索框和下拉框,使用同样的变量控制相同部分的表现,使得组件在主题变化的可以使用相同的颜色规则。
图2 使用变量对组件进行规约
提供暗黑主题色值
完成了上面重要的两步,我们就可以通过给变量提供一套新的色值来达到主题的变化了。
图3 通过色值的切换实现深色主题切换
图片的处理
图片的处理并不能像文字一样地去反转颜色或者反转亮度,这样可能照成不适。通常如果有准备亮色和暗色两套图片,可以采用变量化图片地址在不同主题下切黑图片。如果图片来自用户输入,其他地方的截图,这时候需要稍微处理一些降低亮度。图片简化地获取当前的主题状态可以在body上增加一个ui主题是否是深色模式的属性。
深色方案一:图片增加透明度。适用场景:简单文章图片和纯色背景。
// css
body[ui-theme-mode='dark'] img {
opacity: 0.8;
}
深色方案二:带图片的位置叠加一个灰色半透明的层,适用场景:背景图,非纯色背景等。
// css
body[ui-theme-mode='dark'] .dark-mode-image-overlay {
position: relative;
}
body[ui-theme-mode='dark'] .dark-mode-image-overlay::before {
content: '';
display: block;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(50, 50, 50, 0.5);
}
前者不适用与带有背景图片的层处理,也不适合通过叠加图片遮挡来呈现效果的处理,但是用在文章博客中的插入图片非常简单有效,图片可以自然地叠加到纯色深色的背景色上。后者给了另一种方案完成背景层的叠加,但对代码有一定的入侵。
提供主题变化订阅应对第三方组件场景
通过以上几个基本的步骤就能在编码的过程中通过使用变量指定颜色值,获得主题的能力。但是面对大量第三方组件,有自己的主题,也可能有自己的深色主题,这块再去入侵式地修改成自定义的变量工作量不小且并不一定合适。
这时候需要提供主题订阅,在主题发生变化的时候,获得通知,然后给第三方组件设置一定对应的变更。
我们需要一个简单的eventbus,实现方式不限。这里给出一个简单版本的接口如下:
// theme/interface.ts
export interface IEventBus {
on(eventName: string, callbacks: Function): void;
off(eventName: string, callbacks: Function): void;
trigger(eventName: string, data: any): void;
}
切换主题的时候发出themeChanged事件,使用on监听就能够获得当前主题变更事件,通过判断主题,给第三方的组件套上对应的主题,或者修改js颜色变量等等。
多套CSS样式实现
通过编写多套CSS样式代码来实现,切换功能就通过动态修改link标签的href或者动态添加删除link标签。这方案就不贴代码了......
优点:简单
缺点:维护成本高
CSS变量实现
html结构如下:main标签里面是内容,上面的div是用来切换皮肤的
<body>
<div>
<!-- 这里是切换的按钮 -->
<img src="../icons/sum.svg" id="theme">
</div>
<main class="content">
<h1>CSS variable</h1>
<p>abcdefghijklmnopqrstuvwxyz,abcdefghijklmnopqrstuvwxyz</p>
<p>abcdefghijklmnopqrstuvwxyz</p>
<p>abcdefghijklmnopqrstuvwxyz</p>
<p>abcdefghijklmnopqrstuvwxyz</p>
<button>button</button>
</main>
</body>
<script> | |
| -------- | -------------------------------------------------------- |
| | const themebtn = document.getElementById('theme') |
| | themebtn.addEventListener('click', () => { |
| | const body = document.body |
| | // 判断当前是否是黑夜模式,从而切换模式 |
| | if (Array.from(body.classList).indexOf('dark') !== -1) { |
| | body.classList.remove('dark') |
| | themebtn.src = '../icons/sum.svg' |
| | } else { |
| | body.classList.add('dark') |
| | themebtn.src = '../icons/moon.svg' |
| | } |
| | }) |
| | </script>
css代码如下:当前是使用给Body添加类名的方式来实现切换,定义变量
:root {
--bg-color-0: #fff;
--bg-color-1: #fff;
--text-color: #333;
--grey-1: #1c1f23;
}
:root .dark {
--bg-color-0: #16161a;
--bg-color-1: #35363c;
--text-color: #fff;
--grey-1: #f9f9f9;
}
使用定义的变量:通过var(param)使用
body {
margin: 0;
padding: 0;
background-color: var(--bg-color-0);
transition: all 0.3s;
}
.content {
padding: 20px;
background-color: var(--bg-color-1);
color: var(--text-color);
}
.content button {
width: 100px;
height: 30px;
background-color: var(--bg-color-1);
color: var(--text-color);
border: 1px solid var(--grey-1);
outline: none;
}
通过js来对切换按钮进行一个给Body增加/删除类名的操作。
const themebtn = document.getElementById('theme')
themebtn.addEventListener('click', () => {
const body = document.body
// 判断当前是否是黑夜模式,从而切换模式
if (Array.from(body.classList).indexOf('dark') !== -1) {
body.classList.remove('dark')
themebtn.src = '../icons/sum.svg'
} else {
body.classList.add('dark')
themebtn.src = '../icons/moon.svg'
}
})
效果图:
这种方式实现的一个优缺点
优点:简单易懂
缺点:存在兼容性问题,IE不支持,解决方法也有就是使用css-vars-ponyfill
A ponyfill that provides client-side support for CSS custom properties (aka “CSS variables”) in legacy and modern browsers.
在旧版和现代浏览器中为 CSS 自定义属性(又称“CSS 变量”)提供客户端支持的 ponyfill。
Sass/Less变量实现
实现方法跟用CSS变量差不多,也是通过给根元素添加属性/类名来达成切换的效果
简单的搭建一个React + SCSS的项目来实现,文件目录如下,主要看_variable.scss和_handle.scss两个文件
scss-variable
├── yarn.lock
├── package.json
├── public
│ └── index.html
├── src
│ ├── index.scss
│ ├── _variable.scss
│ ├── _handle.scss
│ ├── index.js
│ └── app.jsx
└── webpack.config.js
首先看app.jsx文件,里面编写了页面结构以及切换的逻辑
import { useCallback, useState } from 'react'
import './index.scss'
const App = () => {
// 定义切换按钮的文案,并且给html附上默认主题类型
const [themeText, setThemeText] = useState(() => {
document.documentElement.setAttribute('data-theme', 'light')
return 'light'
})
// 切换逻辑
const toggleTheme = useCallback(() => {
if (themeText === 'light') {
document.documentElement.setAttribute('data-theme', 'dark')
setThemeText('dark')
} else {
document.documentElement.setAttribute('data-theme', 'light')
setThemeText('light')
}
})
return (
<div className="content">
<button onClick={toggleTheme}>{themeText}</button>
<h1>SCSS variable</h1>
<p>abcdefghijklmnopqrstuvwxyz,abcdefghijklmnopqrstuvwxyz</p>
<p>abcdefghijklmnopqrstuvwxyz</p>
<p>abcdefghijklmnopqrstuvwxyz</p>
<p>abcdefghijklmnopqrstuvwxyz</p>
<button>button</button>
</div>
)
}
export default App
然后就是_variable.scss和_handle.scss
_variable.scss:定义每个主题的变量
$themes: (
light: (
bg-color-0: #fff,
bg-color-1: #fff,
text-color: #333,
grey-1: #1c1f23
),
dark: (
bg-color-0: #16161a,
bg-color-1: #35363c,
text-color: #fff,
grey-1: #f9f9f9
)
);
_handle.scss:定义mixin来将每个主题的对应颜色字体宽高等等绑定好,只要根据某个mixin来传入对应的key值就好
@mixin themeify {
// 遍历_variable.scss定义的主题
@each $theme-name,
$theme-map in $themes {
// 将每个主题提升为全局变量
$theme-map: $theme-map !global;
// 绑定某个主题下的样式内容
[data-theme="#{$theme-name}"] & {
@content
}
}
}
// 定义一个通过key获取主题变量的值函数
@function themed($key) {
@return map-get($theme-map, $key)
}
// 下面这些mixin绑定在那个主题就那种颜色
@mixin bgColor($color) {
@include themeify {
background-color: themed($color);
}
}
@mixin textColor($color) {
@include themeify {
color: themed($color);
}
}
@mixin borderColor($color) {
@include themeify {
border-color: themed($color);
}
}
这里我是已经将_variable.scss和_handle.scss两个文件在webpack里面配置了全局引入了,所以能在index.scss里面使用 index.scss:使用mixin来绑定
body {
margin: 0;
padding: 0;
transition: all 0.3s;
@include bgColor('bg-color-0');
}
.content {
padding: 20px;
@include bgColor('bg-color-1');
@include textColor('text-color');
button {
width: 100px;
height: 30px;
border: 1px solid;
outline: none;
@include bgColor('bg-color-0');
@include borderColor('grey-1');
@include textColor('text-color');
}
}
效果跟CSS变量一致
优点:因为使用webpack进行打包,所以兼容性问题可以解决
缺点:代码可读性不如CSS强
webpack 插件定制主题换肤
参考文档: juejin.cn/post/684490…