先拜读源码,最后总结,以及其余实现思路。若有错误,欢迎指正!css
名称:Darkmode.js
功能:给你的网站添加暗色模式
项目连接:https://github.com/sandoche/Darkmode.jsgit
使用这个插件很是简单,只须要实例化 class,便可在页面建立一个 button,点击它就可以切换亮色\暗色模式。es6
new Darkmode({ bottom: "32px", right: "32px", time: "0.5s", label: "🌓", }).showWidget();
调用 showWidget
以显示切换按钮,也能够经过编程的方式调用 toggle
切换。效果:github
mix-blend-mode
描述元素的内容应该与元素的直系父元素的内容和元素的背景如何混合。值为时 difference 反相。编程
经过几张图片有助于你弄清楚插件的机制。这是上面例子的3D视图,你可以清楚的看到每一层。数组
下面简要分析每一层,亮色模式状态下:浏览器
暗色模式状态下:babel
与上图对比明显之处就是藏在按钮下方的小方块展开了,覆盖整个页面。这就是混合层,这个层包含css 属性 mix-blend-mode: difference
。正是如此实现的暗色模式。cookie
// es module // 经过 typeof 判断当前是否为浏览器环境,并导出常量 export const IS_BROWSER = typeof window !== "undefined"; // es6 支持导出 class // class 只是一个语法糖,babel 转化 export default class Darkmode { // constructor -> class实例化时执行 // 用户经过实例化该类并传递一个 options // 构造函数接收 options -> 用户配置 constructor(options) { if (!IS_BROWSER) { return; } // 默认配置 const defaultOptions = { bottom: "32px", // 按钮位置 right: "32px", // 按钮位置 left: "unset", // 按钮位置 time: "0.3s", // 过渡时间 mixColor: "#fff", // 混合层背景色 backgroundColor: "#fff", // 建立的背景层背景色 buttonColorDark: "#100f2c", // 亮色状态下的按钮颜色 buttonColorLight: "#fff", // 暗色状态下的按钮色 label: "", // 按钮中的内容 saveInCookies: true, // 是否存在cookie 默认 local storage autoMatchOsTheme: true, // 跟随系统设置 }; // 经过 Object.assign 合并默认配置和用户配置 // 浅拷贝 options = Object.assign({}, defaultOptions, options); // 须要在 css 使用配置 // style 以字符串的形式呈现 // 若是单独抽离css,须要更多的逻辑代码 const css = ` .darkmode-layer { position: fixed; pointer-events: none; background: ${options.mixColor}; transition: all ${options.time} ease; mix-blend-mode: difference; } .darkmode-layer--button { width: 2.9rem; height: 2.9rem; border-radius: 50%; right: ${options.right}; bottom: ${options.bottom}; left: ${options.left}; } .darkmode-layer--simple { width: 100%; height: 100%; top: 0; left: 0; transform: scale(1) !important; } .darkmode-layer--expanded { transform: scale(100); border-radius: 0; } .darkmode-layer--no-transition { transition: none; } .darkmode-toggle { background: ${options.buttonColorDark}; width: 3rem; height: 3rem; position: fixed; border-radius: 50%; border:none; right: ${options.right}; bottom: ${options.bottom}; left: ${options.left}; cursor: pointer; transition: all 0.5s ease; display: flex; justify-content: center; align-items: center; } .darkmode-toggle--white { background: ${options.buttonColorLight}; } .darkmode-toggle--inactive { display: none; } .darkmode-background { background: ${options.backgroundColor}; position: fixed; pointer-events: none; z-index: -10; width: 100%; height: 100%; top: 0; left: 0; } img, .darkmode-ignore { isolation: isolate; display: inline-block; } @media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) { .darkmode-toggle {display: none !important} } @supports (-ms-ime-align:auto), (-ms-accelerator:true) { .darkmode-toggle {display: none !important} } `; // 混合层 -> 反相 const layer = document.createElement("div"); // 按钮 -> 点击切换夜间模式 const button = document.createElement("button"); // 背景层 -> 用户自定义背景色 const background = document.createElement("div"); // 初始化类(初始样式) button.innerHTML = options.label; button.classList.add("darkmode-toggle--inactive"); layer.classList.add("darkmode-layer"); background.classList.add("darkmode-background"); // 经过 localStorage 储存状态 // darkmodeActivated 获取当前是否在darkmode下 const darkmodeActivated = window.localStorage.getItem("darkmode") === "true"; // 系统是否默认开启暗色模式 // matchMedia 方法的值能够是任何一个 CSS @media 规则 的特性。 // matchMedia 返回一个新的 MediaQueryList 对象,表示指定的媒体查询字符串解析后的结果。 // matches boolean 若是当前document匹配该媒体查询列表则其值为true;反之其值为false。 const preferedThemeOs = options.autoMatchOsTheme && window.matchMedia("(prefers-color-scheme: dark)").matches; // 是否储存localStorage const darkmodeNeverActivatedByAction = window.localStorage.getItem("darkmode") === null; if ( (darkmodeActivated === true && options.saveInCookies) || (darkmodeNeverActivatedByAction && preferedThemeOs) ) { // 激活夜间模式 layer.classList.add( "darkmode-layer--expanded", "darkmode-layer--simple", "darkmode-layer--no-transition" ); button.classList.add("darkmode-toggle--white"); // 激活 darkmode 时,将类 darkmode--activated 添加到body document.body.classList.add("darkmode--activated"); } // 插入 document.body.insertBefore(button, document.body.firstChild); document.body.insertBefore(layer, document.body.firstChild); document.body.insertBefore(background, document.body.firstChild); // 将 css 插入 <style/> this.addStyle(css); // 初始化变量 button layer saveInCookies time // 方便函数中调用 this.button = button; this.layer = layer; this.saveInCookies = options.saveInCookies; this.time = options.time; } // 接收样式 css 字符串 // 建立 link 标签在 head 中插入 addStyle(css) { const linkElement = document.createElement("link"); linkElement.setAttribute("rel", "stylesheet"); linkElement.setAttribute("type", "text/css"); // 使用encodeURIComponent将字符串编码 linkElement.setAttribute( "href", "data:text/css;charset=UTF-8," + encodeURIComponent(css) ); document.head.appendChild(linkElement); } // 切换按钮 showWidget() { if (!IS_BROWSER) { return; } const button = this.button; const layer = this.layer; // s -> ms const time = parseFloat(this.time) * 1000; button.classList.add("darkmode-toggle"); button.classList.remove("darkmode-toggle--inactive"); layer.classList.add("darkmode-layer--button"); // 监听点击事件 button.addEventListener("click", () => { // 当前是否在暗色模式 // isActivated()返回 bool 见下方 const isDarkmode = this.isActivated(); if (!isDarkmode) { // 添加过渡样式 layer.classList.add("darkmode-layer--expanded"); // 禁用按钮 button.setAttribute("disabled", true); setTimeout(() => { // 清除过渡动画 layer.classList.add("darkmode-layer--no-transition"); // 显示混合层 layer.classList.add("darkmode-layer--simple"); // 取消禁用 button.removeAttribute("disabled"); }, time); } else { // 逻辑相反 layer.classList.remove("darkmode-layer--simple"); button.setAttribute("disabled", true); setTimeout(() => { layer.classList.remove("darkmode-layer--no-transition"); layer.classList.remove("darkmode-layer--expanded"); button.removeAttribute("disabled"); }, 1); } // 处理按钮样式,黑暗模式下背景色为白色调,反之为暗色调 // 若是 darkmode-toggle--white 类值已存在,则移除它,不然添加它 button.classList.toggle("darkmode-toggle--white"); // 若是 darkmode--activated 类值已存在,则移除它,不然添加它 document.body.classList.toggle("darkmode--activated"); // 取反存 localStorage window.localStorage.setItem("darkmode", !isDarkmode); }); } // 容许使用方法 toggle()启用/禁用暗模式 // 即以编程的方式切换模式,而不是使用内置的按钮 // new Darkmode().toggle() toggle() { if (!IS_BROWSER) { return; } const layer = this.layer; const isDarkmode = this.isActivated(); // 处理样式 layer.classList.toggle("darkmode-layer--simple"); document.body.classList.toggle("darkmode--activated"); // 存状态 window.localStorage.setItem("darkmode", !isDarkmode); } // 检查是否激活了暗色模式 isActivated() { if (!IS_BROWSER) { return null; } // 经过判断body是否包含激活css class // contains 数组方法 返回 bool return document.body.classList.contains("darkmode--activated"); } }
import Darkmode, { IS_BROWSER } from "./darkmode"; export default Darkmode; // 将 Darkmode 挂载到 window 对象 if (IS_BROWSER) { (function (window) { window.Darkmode = Darkmode; })(window); }
经过 mix-blend-mode:difference
达到切换夜间模式的效果,存在明显的短板,当你的网站色调不是白色或其相近的颜色时,经过这个插件没法实现夜间模式。以及对图像的处理等。app
周全的办法是经过 css 变量(自定义属性)实现,能够处理暗色\亮色模式下的各个细节。具体思路是先建立默认使用的 css 变量:
:root { --default-text-0: #555; /* ... */ --text-0: var(--dark-text-0, var(--default-text-0)); /* ... */ } body { color: var(--text-0); } /* ... */
而后经过 JavaScript 建立 --dark-text-0
及其值。初始状态下 --text-0
的值为 --default-text-0
的值 (找不到第一个值找第二个值,从左往右)。
mix-blend-mode
css Variable(Custom Properties)