当前位置:   article > 正文

前端 “一键换肤“ 的 N 种方案

前端换肤

前端瓶子君,关注公众号

回复算法,加入前端编程面试算法每日一题

前言

现在越来越多的网站都提供了拥有换肤(切换主题)功能,如 ElementUI[2],既是为了迎合用户需求,或是为了凸显自己特点,因此提供了个性化定制的功能.

其实之前就想了解和实现 “一键换肤” 功能,但是由于种种原因一直拖到了现在.

8582927574d38befca092dc0f0fcd017.png
skin.gif

CSS 样式覆盖实现

核心

通过切换 css 选择器的方式实现主题样式的切换.

  • 在组件中保留不变的样式,将需要变化的样式进行抽离

  • 提供多种样式,给不同的主题定义一个对应的 CSS 选择器

  • 根据不同主题设置不同的样式

实现

下面通过 vuex 存储和控制全局的主题色,其代码如下:

  1. import { createStore } from 'vuex'
  2. // 创建一个新的 store 实例
  3. const store = createStore({
  4.   state () {
  5.     return {
  6.       theme: 'light'
  7.     }
  8.   },
  9.   mutations: {
  10.     setTheme (state, payload) {
  11.       state.theme = payload
  12.       document.querySelector('body').className = payload
  13.     }
  14.   }
  15. })
  16. export default store
  17. 复制代码

template 模板中通过 vuex 中的主题设置对应类名,如头部代码如下:

  1. <template>
  2.   <div :class="['header', store.state.theme]">
  3.     <span>{{title}}</span>
  4.     <input v-model="checked" type="checkbox" class="switch" @change="changeTheme" />
  5.   </div>
  6. </template>
  7. 复制代码

下面 theme.css 中通过 .light.dark 两个类选择器来区分明亮主题和暗黑主题,并且事先准备了它们对应的样式,如下:

  1. /* light 默认主题*/
  2. body.light {
  3.   background-color: #fff;
  4. }
  5. .header.light {
  6.   background-color: #fff;
  7.   border-bottom: 1px solid #d6d6d6;
  8.   color: rgb(515050);
  9. }
  10. .list.light .title {
  11.   color: rgb(515050);
  12. }
  13. .list.light .describe{
  14.   color: rgb(158158158);
  15. }
  16. .list.light .left{
  17.   border: 1px solid rgb(515050);
  18. }
  19. /* dark 暗黑主题 */
  20. body.dark {
  21.   background-color: rgb(515050);
  22. }
  23. .header.dark {
  24.   background-color: rgb(515050);
  25.   border-bottom: 1px solid #fff;
  26.   color: #fff;
  27. }
  28. .list.dark .title {
  29.   color: #fff;
  30. }
  31. .list.dark .describe{
  32.   color: rgb(201201201);
  33. }
  34. .list.dark .left{
  35.   border: 1px solid #fff;
  36.   background-color: #fff;
  37. }
  38. 复制代码

缺点

  • 多种主题样式都要引入,导致代码量增大

  • 样式不易管理

  • 查找样式复杂

  • 开发效率低

  • 拓展性差

  • ...

实现多套 CSS 主题样式

核心

实现多套 CSS 主题样式,根据用户切换操作,通过 link 标签动态加载不同的主题样式,主要解决了多个主题色被编译到一个文件中导致单个文件过大.

实现

css 部分直接拆分成 ligth.cssdark.css 两个文件:

dd8b918d06ba36df219953f0318e4ea0.png
image.png

设置主题部分的 setTheme.js 代码如下:

  1. export default function setTheme(theme = 'ligth') {
  2.   let link = document.querySelector('#theme-link')
  3.   let href = "/theme/" + theme + ".css"
  4.   
  5.   if (!link) {
  6.     let head = document.querySelector('head')
  7.     link = document.createElement('link')
  8.     link.id = '#theme-link'
  9.     link.rel = "stylesheet"
  10.     link.href = href
  11.     head.appendChild(link)
  12.   } else {
  13.     link.href = href
  14.   }
  15. }
  16. 复制代码

缺点

  • 需要重复 CV 多份样式文件进行单独修改

  • 没有单独提取出可变的样式部分

  • 需要提前知道打包后的文件路径,否则可能导致主题样式引入错误

  • ...

CSS 变量实现

核心

通过 body.style.setProperty(key, value) 动态修改 body 上的 CSS 变量,使得页面上的其他部分可以应用最新的 CSS 变量对应的样式.

cd7afb9359a78dbfdaa4f1b98108710e.png

实现

theme.css 中负责定义全局的 CSS 变量,代码如下:

  1. /* 实现方式一 */
  2. :root {
  3.   --theme-bg: initial; // 背景色
  4.   --theme-color: initial; // 字体色
  5.   --theme-boder-color: initial; // 边框色
  6. }
  7. ====================================================
  8. /* 实现方式二 */
  9. /* 默认值:light */
  10. :root {
  11.   --theme-bg: #fff;
  12.   --theme-color: rgb(515050);
  13.   --theme-img-bg: #fff;
  14.   --theme-boder-color: #d6d6d6;
  15. }
  16. /* 暗黑:dark */
  17. [data-theme='dark'] {
  18.   --theme-bg: rgb(515050);
  19.   --theme-color: #fff;
  20.   --theme-boder-color: #fff;
  21. }
  22. 复制代码

themeUtil.js 中负责获取当前对应样式值,以及设置 body 上的 CSS 变量值,如下:

  1. const darkTheme = 'rgb(51, 50, 50)'
  2. const lightTheme = '#fff'
  3. const lightBorderTheme = '#d6d6d6'
  4. // 获取对应的主题色值
  5. export const getThemeMap = (isLight) => {
  6.   return {
  7.     'theme-bg': isLight ? lightTheme : darkTheme,
  8.     'theme-color': isLight ? darkTheme : lightTheme,
  9.     'theme-boder-color': isLight ? lightBorderTheme : lightTheme,
  10.   }
  11. }
  12. // 设置主题色值
  13. export const setTheme = (isLight = true) => {
  14.   const themeMap = getThemeMap(isLight)
  15.   const body = document.body
  16.   /* 实现方式一 */
  17.   Object.keys(themeMap).forEach(key => {
  18.     body.style.setProperty(`--${key}`, themeMap[key])
  19.   })
  20.   
  21.   /* 实现方式二 */
  22.   // body.style.setProperty('data-theme', isLight ? 'light' : 'dark')
  23. }
  24. 复制代码

通过 var() 在组件中应用对应 CSS 变量,比如在头部中的使用:

  1. <style scoped>
  2. .header {
  3.   ...省略
  4.   color: var(--theme-color);
  5.   border-bottom: 1px solid var(--theme-boder-color);
  6.   background-color: var(--theme-bg);
  7. }
  8. ...省略
  9. </style>
  10. 复制代码

缺点

缺点就是兼容性不好

ac345b457d6e1423782677f5cec20861.png

兼容

通过 css-vars-ponyfill 对 CSS 变量进行兼容处理,themeUtil.js 中代码改变如下:

  1. import cssVars from "css-vars-ponyfill";
  2. const darkTheme = 'rgb(51, 50, 50)'
  3. const lightTheme = '#fff'
  4. const lightBorderTheme = '#d6d6d6'
  5. // 这里定义的 键/值 对,是为了给 cssVars 传参
  6. export const getThemeMap = (isLight) => {
  7.   return {
  8.     '--theme-bg': isLight ? lightTheme : darkTheme,
  9.     '--theme-img-bg': lightTheme,
  10.     '--theme-color': isLight ? darkTheme : lightTheme,
  11.     '--theme-boder-color': isLight ? lightBorderTheme : lightTheme,
  12.   }
  13. }
  14. export const setTheme = (isLight = true) => {
  15.   const themeMap = getThemeMap(isLight)
  16.   const body = document.body
  17.   
  18.   /* 实现方式一 */
  19.   Object.keys(themeMap).forEach(key => {
  20.     body.style.setProperty(key, themeMap[key])
  21.   })
  22.   
  23.   /* 实现方式二 */
  24.   // body.style.setProperty('data-theme', isLight ? 'light' : 'dark')
  25.   
  26.   // 实现兼容方案
  27.   cssVars({
  28.     watch: true// 添加、删除、修改 <link> 或 <style> 元素的禁用或 href 属性时,ponyfill 将自行调用    
  29.     variables: themeMap, // variables 自定义属性名/值对的集合
  30.     onlyLegacy: false// false  默认将 css 变量编译为浏览器识别的 css 样式 ;true 当浏览器不支持css变量的时候将css变量编译为识别的css  
  31.   });
  32. }
  33. 复制代码

主题图片切换

a9306e979e7404d11549bea9e4aba214.png1cddc1d002016308b022970c0e4864da.png

实现了前面的内容之后,现在给分别给 lightdark 主题添加一个 logo,这一部分其实很简单了,下面的示例代码是基于 Vue3 进行实现的

  1. // Header.vue
  2. <script setup>
  3. import { ref } from 'vue'
  4. import { setTheme } from '../style/themeUtil'
  5. defineProps({
  6.   title: String
  7. })
  8. const checked = ref(false)
  9. const logoUrl = ref('')
  10. const loadImg = async () => {
  11.   let name = !checked.value ? 'light' : 'dark'
  12.   let ext = !checked.value ? 'png' : 'jpg'
  13.   let res = await import(`../assets/logo-${name}.${ext}`)
  14.   logoUrl.value = res.default
  15. }
  16. loadImg()
  17. const changeTheme = (event) => {
  18.   setTheme(!checked.value)
  19.   loadImg()
  20. }
  21. </script>
  22. <template>
  23.   <div class="header">
  24.     <img class="logo" :src="logoUrl" />
  25.     <span>{{ title }}</span>
  26.     <input v-model="checked" type="checkbox" class="switch" @change="changeTheme" />
  27.   </div>
  28. </template>
  29. 复制代码

效果如下

317ce09001870f90cb65d53bdef651bc.png
skin.gif

最后

以上就是目前了解到一些的换肤方案,以上全部基于 css 去实现的,不过知道了原理就可以结合 lesssass 进行更好的实现。如果有更好的方案,欢迎贴在评论区进行分享!!!

关于本文

作者:熊的猫

https://juejin.cn/post/7063010855167721486

最后

欢迎关注【前端瓶子君】✿✿ヽ(°▽°)ノ✿

回复「算法」,加入前端编程源码算法群,每日一道面试题(工作日),第二天瓶子君都会很认真的解答哟!

回复「交流」,吹吹水、聊聊技术、吐吐槽!

回复「阅读」,每日刷刷高质量好文!

如果这篇文章对你有帮助,「在看」是最大的支持

 》》面试官也在看的算法资料《《

“在看和转发”就是最大的支持

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/133626
推荐阅读
相关标签
  

闽ICP备14008679号