Github star 1.7k 的項目源碼解析

先拜讀源碼,最後總結,以及其餘實現思路。若有錯誤,歡迎指正!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

項目結構

  • lib 打包文件夾 .js & min.js
  • src 主要源碼 index.js & darkmode.js
  • test 測試用例
  • ...一大推常見的配置文件

核心概念

mix-blend-mode 描述元素的內容應該與元素的直系父元素的內容和元素的背景如何混合。值爲時 difference 反相。編程

視圖

經過幾張圖片有助於你弄清楚插件的機制。這是上面例子的3D視圖,你可以清楚的看到每一層。數組

下面簡要分析每一層,亮色模式狀態下:瀏覽器

  • 按鈕:右下角黑色小方塊,效果圖中就是點擊切換它切換暗色\亮色模式。
  • 頁面內容:圖中藍色部分。即該實例中的文本所在的層,包含其父級容器。
  • 混合層:按鈕下方小塊。混合層亮色模式下不可見,經過上面的效果圖你能明白該層在切換到夜間時通過過渡動畫覆蓋整個頁面,除了 button。
  • 自定義背景層:圖中綠色邊框所在層。用戶自定義背景色,插件建立的層。

暗色模式狀態下:babel

與上圖對比明顯之處就是藏在按鈕下方的小方塊展開了,覆蓋整個頁面。這就是混合層,這個層包含css 屬性 mix-blend-mode: difference。正是如此實現的暗色模式。cookie

darkmode.js

// 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");
  }
}

index.js

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 變量(自定義屬性)實現,能夠處理暗色\亮色模式下的各個細節。具體思路是先建立默認使用的 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)

相關文章
相關標籤/搜索