乐趣区

关于前端:在浏览器中检测是否为深色模式

前言

深色模式(Dark Mode), 也叫暗黑模式, 顾名思义, 它给人最直观的感触, 就是黑.

但「深色模式」要实现理想的视觉体验, 不只是简略地将底色变黑, 将文字变白这么简答. Google 在 Material Design 的设计指南中对于「深色模式」列出的设计规范中, 第一条就是『不要应用 100% 的纯黑』.

前端工程师须要和设计师沟通, 如何做出一个对用户体验良好、实现老本正当的「彩色模式」计划;另一方面也须要关怀, 如何正当应用 CSS 组合来实现「深色模式」. 那么, 前端 如何检测以后运行环境出于「深色模式」呢?, 并依据以后的色彩模式, 编写对应的 CSS 呢?

「深色模式」相干的 CSS MediaQuery

以后的浏览器或者其它 WebView 大都反对了对于深色模式的 CSS MediaQuery prefers-color-scheme, 它的格局如下:

@media (prefers-color-scheme: <light|dark>)

其中, light 示意「浅色模式」, dark 示意「深色模式」.

在 CSS 中应用

/* Light mode */
@media (prefers-color-scheme: light) {
  body {
    color: black;
    background-color: white;
  }
}

/* Dark mode */
@media (prefers-color-scheme: dark) {
  body {
    color: white;
    background-color: black;
  }
}

对应的, 你也能够在 styled-components 这类 CSS in JS 计划中应用这样.

通过 JS 检测以后环境是否处于深色模式

在我的开发工作中, 我波及到了在基于 WebKit 引擎中检测「以后环境是否为深色模式」的需要. 如何通过 JS 来检测以后环境是否为「深色模式」呢? 既然 CSS 是通过 MediaQuery 来判断. 自然而然地, 咱们会想到应用 [matchMedia] API 来判断.

window.matchMedia('<mqString>') 返回一个 listenable-like 对象 [MediaQueryList], 它继承自 [EventTarget], 这意味着你能够通过间接它取得最新的 MediaQuery 检测状况:

// detect if on light mode
var isLight = window.matchMedia('(prefers-color-scheme: light)').matches;
// detect if on dark mode
var isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;

对于支流浏览器而言, [matchMedia] API 的反对较好, Chrome >=9, Safari >=5.1 即反对. 更多浏览器的反对状况可参考 [Can I use matchMedia]

监听以后环境色彩模式的变动

同时, window.matchMedia('<mqString>') 返回的 [MediaQueryList] 具备 addEventListener/removeEventListener 接口. 在环境的 MediaQuery 个性发生变化时, [MediaQueryList] 会 emit change 事件. 这意味着你能够监听它相干的 MediaQuery 的最新状况:

// listenable-like object [`MediaQueryList`]
var mqList = window.matchMedia('<mqString>');

mqList.addEventListener('change', (event) => {
  // matched prefer
  if (event.matches) { } else {// not match prefer}
});

于是, 你能够在环境的色彩模式变动时第一工夫检测到:

var mqList = window.matchMedia('(prefers-color-scheme: dark)');

mqList.addEventListener('change', (event) => {
  // is dark mode
  if (event.matches) { } else {// not dark mode}
});

MediaQueryList 对象的兼容性问题

参考兼容性列表, 咱们能发现一些问题. 比方, IE 和 Opera 不反对在 addEventListener(...) 时, 应用对象作为 EventListener 传入

Safari

援用 MDN 上兼容性列表的 note:

Before Safari 14, [MediaQueryList] is based on EventTarget, so you must use addListener() and removeListener() to observe media query lists.

对于 Safari < 14 而言, [MediaQueryList] 有以下兼容性问题,

  • 没有 MediaQueryList.addEventListener, 只有 [MediaQueryList.addListener], 其签名为 addListener(func), 参考这里
  • 没有 MediaQueryList.removeEventListener, 只有 [MediaQueryList.removeListener], 其签名为 addListener(func), 参考这里

Internet Explorer

  • 只有 IE >= 10 的版本反对 [MediaQueryList] 个性.
  • 在 IE 上, [MediaQueryList] 对象 并非 继承自 [EventTarget]. 它反对 [MediaQueryList.addListener] 和 [MediaQueryList.removeListener]

解决兼容性问题

容易想到, 咱们只须要须要将上文的代码稍作更改, 就能够在 Safari < 14, IE >= 10 的环境下顺利运行:

var mqList = window.matchMedia('<mqString>');

// 检测 MediqQueryList 对象上是否存在 addEventListener 办法
if (mqList.addEventListener) {mqList.addEventListener('change', (event) => {});
} else if (mqList.addListener) {
  // 否则, 检测 MediqQueryList 对象上是否存在 addListener 办法
  mqList.addListener((event) => {});
}

removeEventListener/removeListener 的解决相似. 咱们能够应用一个函数 observeMediaChange(medieQuery, listener) 来暗藏解决细节:

/**
 * @param {MediaQueryList} mqList
 * @param {((this: MediaQueryList, ev: MediaQueryListEvent) => any)} listener
 */
function observeMediaChange(mqList, listener) {let disposeFunc = () => {};
  if (mqList.addEventListener && mqList.removeEventListener) {mqList.addEventListener('change', listener);

    disposeFunc = () => {mqList.removeEventListener('change', listener);
    };
  } else if (mqList.addListener && mqList.removeListener) {mqList.addListener(listener);

    disposeFunc = () => {mqList.removeListener(listener);
    };
  }

  return disposeFunc;
}

var mqList = window.matchMedia('<mqString>');

observeMediaChange(mqList, (event) => {
  // is dark mode
  if (event.matches) { } else {// not dark mode}
});

React Hooks 封装

咱们能够基于 window.matchMedia('<mqString>') 返回的 [MediaQueryList] 来封装一个即时而灵活的 useIsDarkMode Hooks:

import {useState, useEffect} from 'react';

function checkIsDarkMode() {
  try {return window.matchMedia('(prefers-color-scheme: dark)').matches;
  } catch (err) {return false;}
}

function useIsDarkMode() {const [isDarkMode, setIsDarkMode] = useState(checkIsDarkMode());

  useEffect(() => {const mqList = window.matchMedia('(prefers-color-scheme: dark)');

    /**
     *
     * @param {MediaQueryListEvent} event
     */
    const listener = (event) => {setIsDarkMode(event.matches);
    };

    mqList.addEventListener('change', listener);

    return () => {mqList.removeEventListener('change', listener);
    };
  }, []);

  return isDarkMode;
}

在 React Functional Component 中能够这样应用:

function Foo() {const isDarkMode = useIsDarkMode();

  return <div className={isDarkMode ? 'theme-dark' : 'theme-light'}>...</div>;
}

留神 上述 useIsDarkMode 实现未思考浏览器兼容性问题, 如果须要在 Safari < 14 这样的环境中运行, 可应用 observeMediaChange 来革新 useIsDarkMode 外部的实现

扩大浏览

  • [Can I use matchMedia]
  • [MediaQueryList]

工作机会

若你是校招生正在寻求实习, 或者你思考换工作, 可将简历发到 richardo2016#gmail.com. 我可帮助内推以下公司:

  • 阿里巴巴(深圳, 杭州, 北京, 上海, 成都)
  • 网易(杭州)
  • 腾讯(成都, 深圳)
  • 字节跳动(杭州)
退出移动版