前端页面主题切换

3,559 阅读9分钟

前言

  1. 2019年多品牌产品适配总结 Vue项目主题色适配,看这一篇文章就够了
  2. H5页面老年化适配

主题切换

在业务中遇到需要变换页面主题样式的需求,常见的实现方案有:替换css链接,修改className,使用less.modifyVars,修改css变量不同的场景使用不同的改方案,也可以将几种方案融合。

1.方案一:在body上添加特殊的class名

实现原理:通过修改body标签的class名称,使用css作用域来进行主题的切换

1.1实现步骤

1.1.1创建页面接口和样式

本例用Umi搭建项目,假设“圣诞活动”为主题,html代码如下

// pages/index/index.tsx

import './index.less';

export default function IndexPage() {
  return (
    <div className="page-index">
      <div className="banner"></div>
      <section className="content">
        <h1 className="title">换新特惠</h1>
        <p className="desc">
            新年焕新 新春特惠 全城狂欢
            官网价4299  现活动立送1000元现金劵,
            可当场消费
            全场手机可以分期付款0首付0利息
            以旧换新,最高可低4000元
            联系电话  18103414888
            专卖店喜迎新春
            凭身份证进店领100元话费,
            回家过年。详情进店咨询。
        </p>
        <button className="button">立即加入</button>
      </section>
    </div>
  );
}

样式代码如下

.page-index {
  .banner {
    height: 257px;
    width: 100%;
    background-size: cover;
    background-color: #cccccc;
    background-repeat: no-repeat;
    background-position: center;
  }
  .content {
    padding: 20px;
  }
  .button {
    border: none;
    color: white;
    padding: 15px 32px;
    text-align: center;
    text-decoration: none;
    display: block;
    width: 100%;
    font-size: 16px;
  }
}

1.1.2提取样式文件

将需要定制的background-image,background-color,color等创建主题文件,已圣诞节为例

// assest/theme/normal.less

.normal {
  background-color: #f1f1f1;
  .banner {
    background-image: url('../images/normal.jpeg');
  }
  .content{
    .title {
      color: #000;
    }
    color: #333333;
    .button{
      background-color: #4caf50;
    }
  }
}
// assest/theme/christmas.less
.christmas {
  background-color: lightpink;
  .banner {
    background-image: url('../images/christmas.jpeg');
  }
  .content{
    .title {
      color: #fff;
    }
    color: #ffffff;
    .button{
      background-color: red; /* Green */
    }
  }
}

1.1.3 在global.less中引入主题文件

// src/global.less

@import './assest/theme/normal';
@import './assest/theme/christmas';

1.1.4 在项目入口app.ts中给html中的body添加一个class

// app.ts
// 预设了normal和christmas两种主题,修改className的值切换主题
const className = 'christmas'
document.getElementsByTagName('body')[0].className = className;

image.png

1.2页面结果预览

className = 'normal'           className = 'christmas'

1.3总结:

  • 优点:实现简单,容易理解
  • 缺点:
  1. 当页面较多时需要编写多个主题文件,需要注意样式命名冲突问题
  2. 不能使用CSS Modules,或者说不能使用在className加哈希

思考:既然用了less为什么不用变量呢?

2.方案二:使用less变量

2.1 实现步骤

2.1.1 修改方案一中主题文件

将颜色值改成变量

// noemal.less

@primary-color: #4caf50;
@bg-color: #f1f1f1;
@title-color: #000000;
@text-color: #333333;
@banner-images: url('../images/normal.jpeg');

.normal {
  background-color: @bg-color;
  .banner {
    background-image: @banner-images;
  }
  .content {
    color: @text-color;
    .title {
      color: @title-color;
    }
    .button {
      background-color: @primary-color;
    }
  }
}
// christmas.less

@primary-color: red;
@bg-color: lightpink;
@title-color: #ffffff;
@text-color: #ffffff;
@banner-images: url('../images/christmas.jpeg');

.christmas {
  background-color: @bg-color;
  .banner {
    background-image: @banner-images;
  }
  .content {
    .title {
      color: @title-color;
    }
    color: @text-color;
    .button {
      background-color: @primary-color;
    }
  }
}

这样和之前并没有本质上的区别,只是将值写成了变量

思考:如果可以像js那样动态的改变变量值就完美了!

2.1.2 重新规划编写主题文件theme.less

// theme.less

@primary-color: red;
@bg-color: lightpink;
@title-color: #ffffff;
@text-color: #ffffff;
@banner-images: url('../images/christmas.jpeg');

body{
  background-color: @bg-color;
}

.banner {
  background-image: @banner-images;
}
.content {
  .title {
    color: @title-color;
  }
  color: @text-color;
  .button {
    background-color: @primary-color;
  }
}

这个theme.less文件放在什么地方呢?

为了在项目打包编译的时候不被webpack编译,我们将样式文件放在根目录public里面(public/style/theme.less),并且将public设置为静态资源库,以Umi为例: 需要在umirc.ts中修改

export default defineConfig({
    ...
    publicPath: '/public/'
    ...
})

我们再创建一个主题汇总的public/style/index.less,方便不同页面样式导入

// index.less
// 其他主题文件
// @import "./night.less";

@import "./theme.less";

在HTML文件中引入index.less样式文件(Umi修改默认HTML模板请参考官方文档HTML模板

<!doctype html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no, minimal-ui" />
    <title>Your App</title>
</head>
<body>
<link rel="stylesheet/less" type="text/css" href="../../public/style/index.less" />
<div id="root"></div>
</body>
</html>

image.png 去修改变量值

2.1.3 js控制变量值

安装less依赖npm install less,创建一个切换主题的工具函数utils/index.ts

import less from 'less';
export const changeTheme = (themeOption: Record<string, string>) => {
    less.modifyVars({  // 调用 `less.modifyVars` 方法来改变变量值
        ...themeOption,
    }).then(() => {
        console.log('修改成功');
    })
}

预设几套主题的json对象,在适当的时候调用changeTheme函数,已app.ts中使用为例:

import {changeTheme} from './utils/index';
// 默认
const defaultTheme = {
    '@primary-color': '#4caf50',
    '@bg-color': '#f1f1f1',
    '@title-color': '#000000',
    '@text-color': '#333333',
    '@banner-images': "url('../images/normal.jpeg')",
}
// 圣诞节
const christmasTheme = {
    '@primary-color': 'red',
    '@bg-color': 'lightpink',
    '@title-color': '#ffffff',
    '@text-color': '#ffffff',
    '@banner-images': "url('../images/christmas.jpeg')",
}
// 黑暗模式
const darkTheme = {
    '@primary-color': 'gold',
    '@bg-color': '#000000',
    '@title-color': '#ffffff',
    '@text-color': '#FFFFFF',
    '@banner-images': "url('../images/normal.jpeg')",
}

// 在适当的时候调用changeTheme方法
changeTheme({
    ...christmasTheme
})

2.2页面效果预览

2.3 总结:

优点:

  1. 嘉善可以灵活的控制颜色
  2. 可预设几套常规的主题搭配,也支持用户自定义 缺点:
  3. 项目需要提前规划主题样式,主题变量值需要跟一般样式文件分开,开发和维护成本高

思考:如何更优雅的切换主题?

  1. 主题文件使用CDN,在特定的时候变更CDN文件
  2. 后端接口返回色值变量的json
  3. 利用时间戳在固定时间切换主题
// 圣诞节2021-12-24 23:59:59的时间戳 1640361599000
let currentTheme = defaultTheme
if (new Date().valueOf() > 1640361599000) {
  currentTheme = christmasTheme
} else {
  currentTheme = defaultTheme
}
changeTheme(currentTheme)
  1. 获取当前时间判断是白天还是黑夜
let currentTheme = defaultTheme
const hours = new Date().getHours()
if (hours > 8 && hours < 20) {
  currentTheme = defaultTheme
} else {
  currentTheme = darkTheme
}
changeTheme(currentTheme)

5.根据当前系统模式

const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')

function darkModeHandler() {
  if (mediaQuery.matches) {
    console.log('现在是深色模式')
    changeTheme(darkTheme)
  } else {
    console.log('现在是浅色模式')
    changeTheme(defaultTheme)
  }
}

// 判断当前模式
darkModeHandler()
// 监听模式变化
mediaQuery.addListener(darkModeHandler)

3.方案三:原生css变量,

老年化适配为例

3.1实现步骤

3.1.1 页面html结构和样式

H5页面像素自适应(rem方案),在global.css中添加

:root {
    --font-size-28PX: 2.8rem;
    --font-size-32PX: 3.2rem;
    --font-size-36PX: 3.6rem;
    --font-size-40PX: 4.0rem;
    --220PX: 22rem;
    --514PX: 51.4rem;
}

html {
    font-size: 31.25%;
}

body{
    font-size: var(--font-size-28PX);
}

创建一个文件pages/eleder/index.tsx

import style from './index.less';
import {ClockCircleOutlined} from '@ant-design/icons'
import {Button} from 'antd'

export default function IndexPage() {
  return (
      <div className={style.pageElder}>
        <div className={style.banner}></div>
        <section className={style.content}>
          <h1 className={style.title}>换新特惠</h1>
          <p className={style.desc}>
            新年焕新 新春特惠 全城狂欢 官网价4299 现活动立送1000元现金劵,
            可当场消费 全场手机可以分期付款0首付0利息 以旧换新,最高可低4000元
            联系电话 18103414888 专卖店喜迎新春 凭身份证进店领100元话费,
            回家过年。详情进店咨询。
          </p>
            <ul className={style.list}>
                <li className={style.item}><ClockCircleOutlined style={{fontSize: '38px'}}/></li>
                <li className={style.item}><ClockCircleOutlined style={{fontSize: '38px'}}/></li>
                <li className={style.item}><ClockCircleOutlined style={{fontSize: '38px'}}/></li>
                <li className={style.item}><ClockCircleOutlined style={{fontSize: '38px'}}/></li>
                <li className={style.item}><ClockCircleOutlined style={{fontSize: '38px'}}/></li>
                <li className={style.item}><ClockCircleOutlined style={{fontSize: '38px'}}/></li>
            </ul>
          <button className={style.button}>立即加入</button>
        </section>
      </div>
  );
}

创建一个样式文件pages/eleder/index.tsx

.pageElder {
  .banner {
    height: var(--514PX);
    width: 100%;
    background-size: cover;
    background-color: #cccccc;
    background-repeat: no-repeat;
    background-position: center;
    background-image: url("../../../public/images/normal.jpeg");
  }
  .content {
    padding: 20px;
    color: #333333;
    .title{
      color: #000000;
      font-size: var(--font-size-40PX);
    }
    .desc {
      font-size: var(--font-size-28PX);
    }
    .list{
      list-style: none;
      display: flex;
      padding: 0;
      margin: 20px 0;
      flex-wrap: wrap;
      .item {
        border: 1px dashed #ccc;
        display: inline-block;
        margin: 10px 0;
        width: var(--220PX);
        text-align: center;
      }
    }
  }
  .button {
    border: none;
    color: white;
    padding: 15px 32px;
    text-align: center;
    text-decoration: none;
    display: block;
    width: 100%;
    font-size: var(--font-size-32PX);
    background-color: #4caf50;
  }
}

3.1.2 创建修改css变量的工具函数

在utils/index.ts中添加

// 修改css变量
export const setCssVar = (themeOption: { [x: string]: any; }) => {
  const root = document.querySelector(':root') as HTMLScriptElement;
  Object.keys(themeOption).forEach((key)=> {
      root.style.setProperty(key, themeOption[key]);
  })
}

3.1.3 预设变量值,在适当的时候切换

在pages/eleder/index.ts添加如下代码

import {setCssVar} from "@/utils"; // 引入工具函数
import { useState } from 'react';
// 预设变量值
const normal = {
    '--font-size-28PX': '2.8rem',
    '--font-size-32PX': '3.2rem',
    '--font-size-36PX': '3.6rem',
    '--font-size-40PX': '4.0rem',
    '--220PX': '22rem',
    '--514PX': '51.4rem'
}

const elder = {
    '--font-size-28PX': '4.0rem',
    '--font-size-32PX': '4.8rem',
    '--font-size-36PX': '5.0rem',
    '--font-size-40PX': '5.6rem',
    '--220PX': '30rem',
    '--514PX': '73.4rem'
}

export default function IndexPage() {
    const [toggle, setToggle] = useState<boolean>(true) // 切换
    const change = () => {
        toggle ? setCssVar(elder):setCssVar(normal)
        setToggle(!toggle)
    }
  return (
      <div className={style.pageElder}>
      <Button size="small" onClick={change}>主题切换</Button>
        <div className={style.banner}></div>
          ...
      </div>
  );
}

3.2 页面效果预览

3.3 总结: 优点: 1. css原生支持 2. 可以使用CSS Modules 3. 不需要引用其他插件

4.小程序主题切换

小程序的主题切换和H5页面有所不同,小程序没有window和document属性,所以之前的document.getElementsByTagName('body')[0].className = className的方式不能使用

使用less小程序编译后识别的是acss,所以无法使用less变量来处理。

4.1 技术原理

我们使用css原生支持的变量,先看下面代码

// index.acss
.title {
  color: var(--title-color, #000000);
}
// index.axml
<view className="title" style="--title-color: #FFFFFF">换新特惠</view>

我们可以给一个字体颜色加一个变量“--title-color”,并给他一个默认值#000000, 然后我们通过改变style内嵌的方式改变--title-color的值来改变字体颜色

4.2 实现步骤

我们使用的支付宝小程序框架miniu创建项目,具体创建请参看支付宝miniU官方文档

4.2.1创建页面index.axml

// index.axml文件,通过themeStyle改变颜色变量
<view style={{themeStyle}} className="page-index" >
  <view className="banner"></view>
  <view className="content">
    <view className="title">换新特惠</view>
    <view className="desc">
      新年焕新 新春特惠 全城狂欢 官网价4299 现活动立送1000元现金劵,
      可当场消费 全场手机可以分期付款0首付0利息 以旧换新,最高可低4000元
      联系电话 18103414888 专卖店喜迎新春 凭身份证进店领100元话费,
      回家过年。详情进店咨询。
    </view>
    <button className="button">立即加入</button>
  </view>
</view>

4.2.2页面样式index.less

.page-index {
  .banner {
    height: 257px;
    width: 100%;
    background-size: cover;
    background-color: #cccccc;
    background-repeat: no-repeat;
    background-position: center;
    background-image: var(--banner-images, url('https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/70967f6ec4ac40bb9b17a673849e9fc6~tplv-k3u1fbpfcp-watermark.image?'));
  }
  .content {
    padding: 20px;
    .title {
      color: var(--title-color, #000000);
    }
    .desc {
      color: var(--text-color, #333333);
      margin-bottom: 50rpx;
    }
  }
  .button {
    border: none;
    color: white;
    text-align: center;
    text-decoration: none;
    display: block;
    width: 100%;
    background: var(--primary-color, #4caf50);
  }
}

页面中我们定义了--banner-images,--title-color,--text-color,--primary-color等几个变量

4.2.3 数据流

我们使用MiniU Data数据流处理方式,引用使用全局变量

// index.js
import { createPage } from '@miniu/data';
Page(
  createPage({
    mapGlobalDataToData: {
      themeStyle: (globalData) => {
        return globalData.themeStyle;
      },
    },
  })
);

使用themeStyle前需要在app.js中定义,并且在onLaunch里面改变颜色值

import {createApp, setGlobalData} from '@miniu/data';
import {getSkinSettings} from './service/index';
App(
  createApp({
    defaultGlobalData: {
      count: 50,
      themeStyle: '', // 定义全局变量themeStyle
    },
    async onLaunch(options) {
      const res = await getSkinSettings(); // 模拟从后端获取颜色值
      this.setThemeStyle(res.data); // 改变色值
    },
    setThemeStyle({
      primaryColor = '#4caf50',
      bgColor = '#f1f1f1',
      titleColor = '#000000',
      textColor = '#333333',
      bannerImages = 'https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/70967f6ec4ac40bb9b17a673849e9fc6~tplv-k3u1fbpfcp-watermark.image?',
    }) {
      setGlobalData((globalData) => {
        globalData.themeStyle = `
        --primary-color: ${primaryColor};
        --bg-color: ${bgColor};
        --title-color: ${titleColor};
        --text-color: ${textColor};
        --banner-images: url('${bannerImages}');`;
      });
      // 设置页面背景色
      my.setBackgroundColor({
        backgroundColor: bgColor,
      });
    },
  })
);

4.2.4 模拟接口请求

// service/index.js
export const getSkinSettings = () => {
  const themeData = {
    primaryColor: 'red',
    bgColor: '#ffb6c1',
    titleColor: '#ffffff',
    textColor: '#ffffff',
    bannerImages: 'https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/dd9dd0126a404dcb8a943dfab864c1cd~tplv-k3u1fbpfcp-watermark.image?',
  };
  return new Promise((resolve, reject) => {
    // 模拟后端接口访问,暂时用500ms作为延时处理请求
    setTimeout(() => {
      const resData = {
        code: 200,
        data: {
          ...themeData,
        },
      };
      // 判断状态码是否为200
      if (resData.code === 200) {
        resolve(resData);
      } else {
        reject({ code: resData.code, message: '网络出错了' });
      }
    }, 500);
  });
};

在页面的根节点中插入变量值

image.png 正在子节点中使用变量

image.png

4.3 效果预览

彩蛋

一行代码实现灰暗模式(国家公祭日,国难日, 512纪念日,为烈士哀悼)

例如: image.png

html {
  -webkit-filter: grayscale(.95);
}

源码地址

H5源码:gitee.com/ssjuanlian/…

小程序源码: gitee.com/ssjuanlian/…