1
本文翻译自 How to implement Theme Switcher in JavaScript,作者:Pavel Keyzik, 略有删改。

在本文中,您将学习如何在JavaScript中构建主题切换器。这应该是一件很容易的事情,但你也可以从我的代码中学到一些东西。

我们需要处理哪些场景?

  • 我们应该解决的最基本的场景之一是将主题从亮到暗,反之亦然
  • 我们需要解决的第二件事是,有些人更喜欢使用与系统中相同的设置。这对那些整天在黑暗和光明主题之间切换的人很有用。
  • 第三件事是保存用户首选项,否则刷新页面后所有设置将再次设置为默认值。

创建主题存储

我们创建初始函数createThemeStore(),它将包含几乎所有内容。我们在这里用这种方式实现,但这可能不是最佳方法。

function createThemeStore(options) {
  // Initial mode
  const initialMode = options.defaultMode || 'system'

  // Initial state
  const state = {
    mode: initialMode,
    systemTheme: getSystemTheme(),
    theme: getThemeByMode(initialMode),
  }
}

在这里,我们创建一个只有3个变量的状态:

  • mode:这表示主题的选定模式,可能值为darklightsystem。它允许我们决定是否使用系统的主题。
  • systemTheme:它保存当前操作系统主题的值。即使我们选择了特定的主题(darklight),当操作系统主题发生变化时,我们仍会更新此变量,以确保用户切换到系统模式时,我们能正确调整主题。
  • theme:这是用户看到的实际主题,可能值为darklight
  • options.defaultMode:用于恢复正确的主题首选项。例如,您可以在localStorage中保存主题更改,然后将其用作默认值,确保保留用户的首选项。

添加订阅

当用户更改主题或OS主题更新时,我们需要一种方法来通知我们的代码,这就是使用订阅的地方,我们需要允许订阅state对象中的更改。下面的代码将帮助我们完成它,记住现在我们在createThemeStore()中执行所有操作。

function createThemeStore(options) {
  // ...

  // Create subscriptions object to be able notify subscribers
  const subscriptions = new Map()
  let subscriptionId = 0 // Just a unique id for every subscriber

  // A place where we send notification to all of our subscribers
  function notifyAboutThemeChange(theme) {
    subscriptions.forEach((notify) => {
      const notificationData = {
        mode: state.mode,
        theme,
      }

      notify(notificationData) // Calls subscribed function (The example how we use it will be later)
    })
  }

  // A function that allows to subscribe to state changes
  function subscribe(callback) {
    subscriptionId++
    subscriptions.set(subscriptionId, callback)

    state.systemTheme = getSystemTheme() // We'll define it later

    if (state.mode === 'system') {
      notifyAboutThemeChange(state.systemTheme)
    } else {
      notifyAboutThemeChange(state.theme)
    }

    return subscriptionId
  }

  // A function that allows to unsubscribe from changes
  function usubscribe(subscriptionId) {
    subscriptions.delete(subscriptionId)
  }

  return {
    subscribe,
    usubscribe,
  }
}

使用方式:

// Create a theme store
const store = createThemeStore()

// Suscribe to changes
const subscriptionId = store.subscribe((newTheme) => {
  // Here you'll be seeing theme changes
  console.log(newTheme)
})

// When you need to unsubscribe from theme change, you just call
store.usubscribe(subscriptionId)

检测系统主题首选项

现在我们有了基本的代码结构,让我们再定义两个helper函数:

  • getSystemTheme():该函数返回当前OS主题darklight
  • getThemeByMode():该函数根据我们的主题模式返回darklight。例如,如果模式设置为dark,则返回dark。但是,当模式设置为系统时,我们会检查系统主题,并根据操作系统的首选项,以darklight作为返回值。

这段代码不会出现在我们的createThemeStore()函数中。我们将window.matchMediaprefers-color-scheme媒体查询一起使用来确认当前系统的主题值。

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

// Get's current OS system
function getSystemTheme() {
  if (window.matchMedia(mediaQuery).matches) {
    return 'dark'
  }
  return 'light'
}

// Based on user's preferences we return correct theme
function getThemeByMode(mode) {
  if (mode === 'system') {
    return getSystemTheme()
  }
  return mode
}

function createThemeStore(options) {
  // ...
}

现在我们唯一需要做的就是添加事件监听器,以检测操作系统主题的变化。

function createThemeStore(options) {
  // ...

  // When the OS preference has changed
  window.matchMedia(mediaQuery).addEventListener('change', (event) => {
    const prefersDarkMode = event.matches

    // We change system theme
    state.systemTheme = prefersDarkMode ? 'dark' : 'light'

    // And if user chose `system` mode we notify about the change
    // in order to be able switch theme when OS settings has changed
    if (state.mode === 'system') {
      notifyAboutThemeChange(state.systemTheme)
    }
  })
}

添加手动更改主题模式的功能

现在每当我们的操作系统首选项更改时,我们已经实现了主题的自动更新。我们还没有讨论的是主题模式的手动更新。你将在你的深色、浅色和系统主题按钮上使用这个功能。

function createThemeStore(options) {
  // ...

  function changeThemeMode(mode) {
    const newTheme = getThemeByMode(mode)

    state.mode = mode
    state.theme = newTheme

    if (state.mode === 'system') {
      // If the mode is system, send user a system theme
      notifyAboutThemeChange(state.systemTheme)
    } else {
      // Otherwise use the one that we've selected
      notifyAboutThemeChange(state.theme)
    }
  }

  return {
    subscribe,
    usubscribe,
    changeThemeMode,
  }
}

使用示例

我们的代码是纯JavaScript实现,你可以在任何地方使用它。我将在React中演示一个示例,但您可以在任何您喜欢的框架或库中尝试它。

// Create a theme store from saved theme mode
// or use `system` if user hasn't changed preferences
const store = createThemeStore({
  defaultMode: localStorage.getItem("theme") || "system",
});

function MyComponent() {
  // Initial active theme is `null` here, but you could use the actual value
  const [activeTheme, setActiveTheme] = useState(null)

  useEffect(() => {
    // Subscribe to our store changes
    const subscriptionId = store.subscribe((notification) => {
      // Update theme
      setActiveTheme(notification.theme)

      // Save selected theme mode to localStorage
      localStorage.setItem('theme', notification.mode)
    })

    return () => {
      store.usubscribe(subscriptionId)
    }
  }, [])

  return (
    <>
      <p>
        Active theme: <b>{activeTheme}</b>
      </p>
      <p>Change theme to:</p>
      <button onClick={() => store.changeThemeMode("dark")}>Dark</button>
      <button onClick={() => store.changeThemeMode("light")}>Light</button>
      <button onClick={() => store.changeThemeMode("system")}>System</button>
    <>
  )
}

最后

本文介绍了如何在JavaScript中实现主题切换。通过创建一个主题存储器以及添加订阅功能以便在主题发生变化时通知调用方,最后还增加了手动切换主题模式和在React中演示的代码示例。

如果你的网站正有主题切换的需求,不妨可以试试看。希望对你有帮助~

关注公众号回复【 20240129 】可获取完整源代码~

看完本文如果觉得有用,记得点个赞支持,收藏起来说不定哪天就用上啦~

专注前端开发,分享前端相关技术干货,公众号:南城大前端(ID: nanchengfe)


南城FE
2.2k 声望571 粉丝