随着 nextjs 成为开发 React 应用程序的黄金标准,我想我应该简要解释一下如何使用 nextjs 和 tailwind 创建一个漂亮的深色主题。 我们将构建以下内容
主题切换器
主题上下文提供者
用法示例
到最后你将能够
先决条件:
配置了 tailwind 的正在运行的 nextjs 应用程序。
以下依赖项:js-cookie@^3.0.1 & @types/js-cookie@^3.0.2 坚持使用主题为 windcss@3.0.5 设置页面样式
在我们开始之前。 Tailwind 通过 CSS 子选择器实现他们的深色主题。 基本上,如果您的 HTML 元素具有 class="dark",它们将自动应用所有 dark:some-tailwind-class 样式。 这就是为什么围绕它的所有功能都将涉及在切换主题时添加/删除 class="dark"。
我们将使用 createContext 来跟踪主题并在需要时使用它。 在这种情况下。
但是为什么我们要使用 React 的上下文呢? 它会导致很多重新渲染和..东西!
.. 我要说的是:
“nu-uh!react 的上下文是一个依赖注入工具,如果我们不破坏它的预期用途,我们就不会造成任何伤害!”
对于只想复制和粘贴所有内容的每个人,只需滚动到底部即可完成工作。
这是相当简单的,所以我不会深入探讨任何细节。
const initialState = false; // we start the first-time visitors up on the light themeexport const ThemeContext = createContext({ isDarkTheme: initialState, // pass in the inital state toggleThemeHandler: () => { }, // define a function to toggle the theme});
上下文提供者必须处理以下两种情况。
新用户:默认为浅色主题设置浅色主题cookie在HTML元素上设置class="light"
返回用户:阅读 cookieCall setIsDarkTheme 并在 HTML 元素上使用适当的 valueSet class="light" 或 class="dark"(从技术上讲,我们不需要 light 类,但我喜欢将其保留在那里)
在用户想要时更改主题
我们将需要两个函数。 一个用于初始化主题并处理案例 1 和 2。还有一个用于实际切换主题的函数。
ThemeContextProvider 跟踪我们当前在其自己的 useState 调用中启用的主题。 您可以完全依赖 cookie,但我发现这有点麻烦。 因此,我们有 const [isDarkTheme, setIsDarkTheme] = useState(initialState); 在最顶端。 这样我们也可以在用户决定更改主题时调用 initialThemeHandler,而不必将 initialThemeHandler 函数与 setTheme 函数分开。
const [isDarkTheme, setIsDarkTheme] = useState(initialState);const initialThemeHandler = useCallback((): void => { // Get current cookie theme. const themeCookie = Cookies.get("theme"); // js-cookie returns `undefined` if there's no cookie by that name setIsDarkTheme(themeCookie === "dark"); // Here we start handling the case when we have a returning user (because we found a cookie) if (themeCookie) { // Just to be super-duper sure we're adding the right classes, remove what ever the other theme is document.querySelector("html")?.classList.remove(isDarkTheme ? "dark" : "light"); // The cookie will have the value of `dark` or `light` // Therefore we can just set it to the value of the cookie document.querySelector("html")?.classList.add(themeCookie); } else { // Oooo!! A new user! // I set the cookie expiration to 30 days, but that's optional const date = new Date(); const expires = new Date(date.setMonth(date.getMonth() + 1)); // Set the default light cookie Cookies.set("theme", "light", { secure: true, expires: expires, }); } // we're going to call this callback everytime the `isDarkTheme` property changes}, [isDarkTheme]);// an dalso on the initial renderuseEffect(() => initialThemeHandler(), [initialThemeHandler]);
这是上下文将通过(跟我说)依赖注入提供给其他组件的功能。
function toggleThemeHandler(): void { // get the current theme cookie. We know it exists since this will 100% of the time run after the `initialThemeHandler` function const themeCookie = Cookies.get("theme"); // What ever theme we previously had, set it to the opposite. // Remember this is a boolean! setIsDarkTheme((ps) => !ps); // Create a new cookie expiration date const date = new Date(); const expires = new Date(date.setMonth(date.getMonth() + 1)); // Set the cookie to the opposit of what ever it's currently holding Cookies.set("theme", themeCookie !== "dark" ? "dark" : "light", { secure: true, expires: expires, }); // add the appropriate class to the `HTML element toggleDarkClassToHTMLElement();}
所有这个函数所做的就是删除深色或浅色,然后将深色或浅色添加到 HTML 元素。我选择这个元素是因为它是最上面的一个,我遇到了 nextjs 和更改 bodyclasses 的一些问题。
function toggleDarkClassToHTMLElement(): void { document.querySelector("html")?.classList.remove(isDarkTheme ? "dark" : "light"); document.querySelector("html")?.classList.add(!isDarkTheme ? "dark" : "light");}
最后我们像这样返回 ThemeContext.Provider
return ( <ThemeContext.Provider value={ { isDarkTheme, // remember, this is a boolean toggleThemeHandler // handler to toggle the theme }}> {props.children} // all other components will be child components of this one </ThemeContext.Provider>);
接下来我们需要初始化主题。 那应该处理以下情况
const initialThemeHandler = useCallback((): void => { // Get current cookie theme. const themeCookie = Cookies.get("theme"); // js-cookie returns `undefined` if there's no cookie by that name setIsDarkTheme(themeCookie === "dark"); // Here we start handling the case when we have a returning user (because we found a cookie) if (themeCookie) { // Just to be super-duper sure we're adding the right classes, remove what ever the other theme is document.querySelector("html")?.classList.remove(isDarkTheme ? "dark" : "light"); // The cookie will have the value of `dark` or `light` // Therefore we can just set it to the value of the cookie document.querySelector("html")?.classList.add(themeCookie); } else { // Oooo!! A new user! // I set the cookie expiration to 30 days, but that's optional const date = new Date(); const expires = new Date(date.setMonth(date.getMonth() + 1)); // Set the default light cookie Cookies.set("theme", "light", { secure: true, expires: expires, }); } // we're going to call this callback everytime the `isDarkTheme` property changes}, [isDarkTheme]);// an dalso on the initial renderuseEffect(() => initialThemeHandler(), [initialThemeHandler]);
因为我们希望我们所有的组件都能够访问当前主题并切换它,所以我们将在提供程序中包装我们的 entirenextjs 应用程序。 为此,我们创建一个 _app.tsx 文件并将所有组件包装在提供程序中,如下所示
export default function Root({Component, pageProps}: AppProps) { return ( <ThemeContextProvider> <Component {...pageProps} /> </ThemeContextProvider> );}
现在,我们将使用老式按钮来切换主题。
interface IThemeTogglerContext{ isDarkTheme: boolean; toggleThemeHandler: () => void;}export function ThemeToggleButton(){ // get the `toggleThemeHandler` via *dependency injection* const { toggleThemeHandler }: IThemeTogglerContext = useContext(ThemeContext); // toggle the theme onClick function toggle(){ toggleThemeHandler() } // super fancy button return ( <button onClick class={classNames( // styles which won't be affected by the theme "font-bold py-2 px-4 rounded-full", // light theme styles "bg-blue-500 hover:bg-blue-700 text-white", // dark theme styles "dark:bg-blue-100 dark:hover:bg-blue-200 dark:text-red-50", )}> Toggle Theme </button>)}
然后,您可以将此按钮放在任何您想要的位置,它会更新主题。 正如一开始提到的,tailwind 使用 CSS 选择器应用深色主题,因此您想要在深色主题中使用的任何样式,只需在选择器前加上一个 dark: 前缀,因为它是 d
因为我想在更改主题时看到一些小的过渡,所以我还创建了一个 _document.tsx 文件并添加了一些顺风类,这使得主题切换成为一种愉快的体验。 这是完成的工作
import Document, { Head, Html, Main, NextScript } from "next/document";export default class _Document extends Document { render() { return ( <Html> <Head> <title>dle.dev</title> </Head> <body className="bg-neutral-50 dark:bg-neutral-900 transition-colors overflow-x-hidden "> <Main /> <NextScript /> </body> </Html> ); }}
是的,我知道我的 Head / Title 配置不是最佳实践。 在这里查看 Vercel 对这个主题的看法。
给你,这就是全部! 如果您想尝试一下,这里是完整的 ThemeContext 组件:
import type { ReactElement, ReactNode } from "react";import { createContext, useCallback, useEffect, useState } from "react";import Cookies from "js-cookie";const initialState = false;export const ThemeContext = createContext({ isDarkTheme: initialState, toggleThemeHandler: () => {},});interface ThemePropsInterface { children: ReactNode;}export function ThemeContextProvider(props: ThemePropsInterface): ReactElement { const [isDarkTheme, setIsDarkTheme] = useState(initialState); const initialThemeHandler = useCallback((): void => { const themeCookie = Cookies.get("theme"); setIsDarkTheme(themeCookie === "dark"); if (themeCookie) { document.querySelector("html")?.classList.remove(isDarkTheme ? "dark" : "light"); document.querySelector("html")?.classList.add(themeCookie); } else { const date = new Date(); const expires = new Date(date.setMonth(date.getMonth() + 1)); Cookies.set("theme", "light", { secure: true, expires: expires, }); } }, [isDarkTheme]); useEffect(() => initialThemeHandler(), [initialThemeHandler]); function toggleThemeHandler(): void { const themeCookie = Cookies.get("theme"); setIsDarkTheme((ps) => !ps); const date = new Date(); const expires = new Date(date.setMonth(date.getMonth() + 1)); Cookies.set("theme", themeCookie !== "dark" ? "dark" : "light", { secure: true, expires: expires, }); toggleDarkClassToBody(); } function toggleDarkClassToBody(): void { document.querySelector("html")?.classList.remove(isDarkTheme ? "dark" : "light"); document.querySelector("html")?.classList.add(!isDarkTheme ? "dark" : "light"); } return ( <ThemeContext.Provider value={{ isDarkTheme, toggleThemeHandler }}> {props.children} </ThemeContext.Provider> );}