用 Alpinejs 完成主题切换功能

用 Alpinejs 完成主题切换功能

April 17, 2024
编码 , 分享 ,

终于把网站的主题切换功能做好了,这次的实现相当满意。

并且我联动了几个主题颜色比较特别的博友,他们分别有:雅余(黄色)、昱行(粉色)、风清(蓝色),大家可以在本站左侧菜单栏选择自己喜欢的配色浏览本站,欢迎给出意见和分享感受。

之前虽然也写过主题切换功能,但是思路没有这次完整,所以索性写一篇博文介绍一下整体实现思路和流程。

这次 11ty-book 我采用了 Alpinejs 作为网页的主 js 框架(真的挺好用的...),这个框架有中文文档 ,官方地址为:https://alpinejs.dev/

  • Alpine.js 通过很低的成本提供了与 Vue 或 React 这类大型框架相近的响应式和声明式特性。
  • 你可以继续操作 DOM,并在需要的时候使用 Alpine.js。
  • 可以理解为 JavaScript 版本的 Tailwind

js 部分

在主 js 文件中使用 import 的方式引入 alpinejs 文件,并将主题要用到的 json 数据对象放入 Alpine.data 对象中(data 是 alpinejs 的数据主要传输方式)。

具体代码作用见下方代码片断。

import Alpine from "alpinejs";

//将Alpine注册进windows对象中
window.Alpine = Alpine;

//存放名为theme的数据对象
Alpine.data("theme", () => ({
    // localStorage.name为我存在本地缓存的主题中文名字
    themeName: localStorage.name,
    // 设置主题切换函数为下方定义的changeTheme
    changeTheme: changeTheme,
    // 设置设置主题名称函数
    setName: function () {
        this.themeName = localStorage.name;
    },
}));

// 主题切换函数
// @theme 为主题英文名称,用于css切换
// @name 为主题的中文名称,用于显示在页面上
function changeTheme(theme, name) {
    // 判断是不是auto
    if (theme == "auto") {
        // 如何是则使用 prefers-color-scheme 自适应系统颜色
        document.documentElement.setAttribute("class", "");
        const prefersDarkScheme = window.matchMedia(
            "(prefers-color-scheme: dark)"
        ).matches;
        //同步artalk主题切换
        if (window.artalk) window.artalk.setDarkMode(prefersDarkScheme);
    //不是auto则切换成对应主题
    } else {
        // 切换网站的主题类名
        document.documentElement.setAttribute("class", theme);
        // 如果有artalk则且同步切换artalk主题,这部分好像还可以优化一下。
        if (window.artalk)
            window.artalk.setDarkMode(theme === "dark" ? true : false);
    }
    // 同时变更本地的主题名称和中文名称
    localStorage.theme = theme;
    localStorage.name = name;
}

js 部分

css 部分

我使用了 sass 来编写主题文件,在并定义了多个主题,通过 @include 引入进 css 文件,届时只需要切换 html 标签的 class 名称为对应的主题名称即可切换不同的主题配色。

:root {
  @include theme-light;
  --icon-filter: drop-shadow(10000px 0 0 var(--body-font-color));

}

@media (prefers-color-scheme: dark) {
  :root {
    @include theme-dark;@include theme-dark;
  }
}

html.dark{
  @include theme-dark;
}

html.light{
  @include theme-light;
}
html.yayu{
  @include theme-yayu;
}
html.yuhang{
  @include theme-yuhang;
}
html.herblue{
  @include theme-herblue;
}

css 部分

我定义的几个主题

// Themes
@mixin theme-light {
    --gray-100: #f8f9fa;
    --gray-200: #e9ecef;
    --gray-500: #adb5bd;
    --gray-700: #6c757d;

    --color-link: #0055bb;
    --color-visited-link: #0055bb;

    --body-background: white;
    --body-font-color: black;

    --btn-color: red;
    --btn-bg: blue;

    --hint-color-info: #6bf;
    --hint-color-warning: #fd6;
    --hint-color-danger: #f66;
}

@mixin theme-dark {
    --gray-100: rgba(255, 255, 255, 0.1);
    --gray-200: rgba(255, 255, 255, 0.2);
    --gray-500: rgba(255, 255, 255, 0.5);
    --gray-700: rgba(255, 255, 255, 0.7);

    --color-link: #84b2ff;
    --color-visited-link: #84b2ff;

    --body-background: #181818;
    --body-font-color: #e9ecef;

    --btn-color: red;
    --btn-bg: blue;

    --hint-color-info: #6bf;
    --hint-color-warning: #fd6;
    --hint-color-danger: #f66;
}

@mixin theme-yayu {
    --gray-100: rgba(227,204,148, 0.1);
    --gray-200: rgba(227,204,148, 0.2);
    --gray-500: rgba(227,204,148, 0.5);
    --gray-700: rgba(227,204,148, 0.7);

    --color-link: #e2c274;
    --color-visited-link: #e2c274;

    --body-background: #222;
    --body-font-color: #e3cc94;

    --btn-color: red;
    --btn-bg: blue;

    --hint-color-info: #6bf;
    --hint-color-warning: #fd6;
    --hint-color-danger: #f66;
}

@mixin theme-yuhang {
    --gray-100: rgba(246,248,250, 0.1);
    --gray-200: rgba(255, 255, 255, 0.2);
    --gray-500: rgba(255, 255, 255, 0.5);
    --gray-700: rgba(255, 255, 255, 0.7);

    --color-link: rgb(235, 153, 161);
    --color-visited-link: rgb(235, 153, 161);

    --body-background: #44403c;
    --body-font-color: #f6f8fa;

    --btn-color: red;
    --btn-bg: blue;

    --hint-color-info: #6bf;
    --hint-color-warning: #fd6;
    --hint-color-danger: #f66;
}

@mixin theme-herblue {
    --gray-100: rgba(166,180,210, 0.1);
    --gray-200: rgba(166,180,210, 0.2);
    --gray-500: rgba(166,180,210, 0.5);
    --gray-700: rgba(166,180,210, 0.7);

    --color-link: #7b88c8;
    --color-visited-link: #7b88c8;

    --body-background: #0B0C1B;
    --body-font-color: #a6b4d2;

    --btn-color: red;
    --btn-bg: blue;
    
    --hint-color-info: #6bf;
    --hint-color-warning: #fd6;
    --hint-color-danger: #f66;
}

css 部分

模板部分

首先需要在网页的 head 部分增加以下代码,作用是在网页加载之前初始化好主题的状态,逻辑为:

  • 获取本地设置的主题
    • 如果本地设置中有主题设置则直接应用
  • 如果上面这个判断失败,如 theme 为空,则设置为自适应,
这段代码似乎还可以优化?
<script>
    const theme = localStorage.theme;
    if (localStorage.theme != 'auto') {
        document.documentElement.classList.add(theme);
        localStorage.theme = theme;
    }
      localStorage.name = localStorage.name || '自适应';
      localStorage.theme = theme || 'auto';

</script>

然后设置主题列表。

在 Alpinejs 中,我们使用 x-data 属性标识一个标签是动态标签,我这里设置 <ul class="book-theme" x-data="theme"> 标签为主动态标签,并且这里还可以指定这个标签需要用到的数据对象,这里我将 x-data 设为上面 js 中设置的 theme 既 js 代码中 Alpine.data("theme", () => ({...)); 定义的数据部分。

设置完后给再 span 标签设置 x-text 属性为 themeName ,这个 themeName 我们在 js 中添加的 theme 对象中设置的,完整代码为 <span x-text="themeName"></span> ,在页面初始化时 Aplinejs 会用这个变量初始化该标签的 text 属性,既显示的文本内容。

之后我这里用 liquid 的模板语法将我在 11ty 中设置的主题列表遍历出来,通过 {{ theme.name }} 将遍历输出到网页上。这部分可以根据个人情况来操作,你可以用其他模板框架生成,也可以直接写死在网页上。

并用 @click 属性给他绑定一个点击事件,并调用在 js 文件中注册的 changeTheme 函数,并传入主题英文名称和中文名称,并调用 setName 函数更新 <span x-text="themeName"></span> 中显示的主题中文名称。

完整代码:

<ul class="book-theme"   x-data="theme">
  <div>主题</div>
  <li >

    <input type="checkbox" id="theme" class="toggle">
    <label for="theme" class="flex justify-between">
      <a  role="button" class="flex align-center">
        <img src="{{ '/assets/svg/theme.svg' | absoluteUrl | cacheBust }}" class="book-icon" alt="change theme">
        <span x-text="themeName"></span>
      </a>
    </label>

    <ul>
      {% for theme in themes %}
        <li>
          <a @click="changeTheme('{{ theme.name }}','{{ theme.desc }}');setName();">
            {{ theme.desc }}
          </a>
        </li> 
      {% endfor %}

      
    </ul>
  </li>
</ul>

完整代码:

完成

Enjoy~!

加入评论