使用 React 和 styled-components 实现主题和主题切换

Avatar of Tapas Adhikary
Tapas Adhikary

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 $200 免费积分!

我最近在一个项目中需要在网站上支持主题。这是一个有点奇怪的要求,因为应用程序主要由少数管理员使用。更令人惊讶的是,他们不仅想要在预先创建的主题之间进行选择,而且还要创建自己的主题。我想人们想要什么就想要什么吧!

让我们将这些要求细化为一个完整的、更详细的要求列表,然后完成它!

  • 定义一个主题(即背景颜色、字体颜色、按钮、链接等)
  • 创建并保存多个主题
  • 选择应用一个主题
  • 切换主题
  • 自定义一个主题

我们为客户提供了完全符合要求的功能,据我所知,他们现在正在愉快地使用它!

让我们开始构建这个功能。我们将使用 Reactstyled-components。本文中使用的所有源代码都可以在 GitHub 代码库 中找到。

设置

让我们使用 React 和 styled-components 设置一个项目。为此,我们将使用 create-react-app。它为我们提供了快速开发和测试 React 应用程序所需的开发环境。

打开命令提示符,并使用以下命令创建项目

npx create-react-app theme-builder

最后一个参数theme-builder只是项目的名称(以及文件夹名称)。您可以使用任何您喜欢的名称。

这可能需要一段时间。完成后,使用cd theme-builder在命令行中导航到该项目。打开文件src/App.js并用以下内容替换其内容

import React from 'react';

function App() {
  return (
    <h1>Theme Builder</h1>
  );
}

export default App;

这是一个基本的 React 组件,我们很快就会对其进行修改。从项目根文件夹运行以下命令启动应用程序

# Or, npm run start
yarn start

您现在可以使用 URL https://#:3000 访问应用程序。

A simple heading 1 that says Theme Builder in black on a white background.

create-react-app 带有 App 组件的测试文件。由于本文不会编写任何组件测试,因此您可以选择删除该文件。

我们需要为我们的应用程序安装一些依赖项。所以让我们在安装这些依赖项的同时也进行安装

# Or, npm i ...
yarn add styled-components webfontloader lodash

以下是我们将获得的内容

  • styled-components: 使用 CSS 对 React 组件进行灵活的样式设置。它提供开箱即用的 主题支持,使用一个名为 <ThemeProvider> 的包装组件。此组件负责向所有包裹在其内的其他 React 组件提供主题。我们将在稍后看到它的实际应用。
  • Web Font Loader: Web Font Loader 有助于从各种来源(例如 Google Fonts、Adobe Fonts 等)加载字体。我们将使用此库在应用主题时加载字体。
  • lodash: 这是一个 JavaScript 实用程序库,用于 一些方便的小扩展

定义一个主题

这是我们的第一个要求。主题应该具有特定的结构来定义外观,包括颜色、字体等。对于我们的应用程序,我们将用以下属性定义每个主题

  • 唯一标识符
  • 主题名称
  • 颜色定义
  • 字体
Screenshot of a code editor showing the organized structure of properties for a sea wave theme.
主题是我们在应用程序中使用的一组结构化的属性。

您可能拥有更多属性和/或不同的结构方式,但这些是我们将在示例中使用的属性。

创建并保存多个主题

所以,我们刚刚看到了如何定义一个主题。现在,让我们通过在项目中添加一个文件夹(位于 src/theme)和一个名为 schema.json 的文件来创建多个主题。以下是我们可以放入该文件中的内容,以创建“light”和“sea wave”主题

{
  "data" : {
    "light" : {
      "id": "T_001",
      "name": "Light",
      "colors": {
        "body": "#FFFFFF",
        "text": "#000000",
        "button": {
          "text": "#FFFFFF",
          "background": "#000000"
        },
        "link": {
          "text": "teal",
          "opacity": 1
        }
      },
      "font": "Tinos"
    },
    "seaWave" : {
      "id": "T_007",
      "name": "Sea Wave",
      "colors": {
        "body": "#9be7ff",
        "text": "#0d47a1",
        "button": {
          "text": "#ffffff",
          "background": "#0d47a1"
        },
        "link": {
          "text": "#0d47a1",
          "opacity": 0.8
        }
      },
      "font": "Ubuntu"
    }
  }
}

schema.json 文件的内容可以保存到数据库中,以便我们可以持久化所有主题以及主题选择。目前,我们只是将其存储在浏览器的 localStorage 中。为此,我们将在 src/utils 中创建一个名为 storage.js 的新文件。我们只需要在其中添加几行代码来设置 localStorage

export const setToLS = (key, value) => {
  window.localStorage.setItem(key, JSON.stringify(value));
}

export const getFromLS = key => {
  const value = window.localStorage.getItem(key);

  if (value) {
    return JSON.parse(value);
  }
}

这些是用于将数据存储到浏览器 localStorage 和从 localStorage 检索数据的简单实用程序函数。现在,我们将应用程序首次启动时将主题加载到浏览器的 localStorage 中。为此,打开 index.js 文件,并用以下内容替换其内容。

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

import * as themes from './theme/schema.json';
import { setToLS } from './utils/storage';

const Index = () => {
  setToLS('all-themes', themes.default);
  return(
    <App />
  )
}

ReactDOM.render(
  <Index />
  document.getElementById('root'),
);

在这里,我们从 schema.json 文件中获取主题信息,并使用键 all-themes 将其添加到 localStorage 中。如果您已停止运行应用程序,请重新启动它并访问 UI。您可以使用浏览器中的 DevTools 查看主题是否已加载到 localStorage 中。

The theme with DevTools open and showing the theme properties in the console.
所有主题属性都已正确存储在浏览器的 localStorage 中,如 DevTools 中的 Application → Local Storage 所示。

选择并应用一个主题

我们现在可以使用主题结构并将主题对象提供给 <ThemeProvider> 包装器。

首先,我们将创建一个 自定义 React 钩子。这将管理选定的主题,了解主题是否已正确加载或是否存在任何问题。让我们从 src/theme 文件夹中的一个名为 useTheme.js 的新文件开始,并将其放入其中

import { useEffect, useState } from 'react';
import { setToLS, getFromLS } from '../utils/storage';
import _ from 'lodash';

export const useTheme = () => {
  const themes = getFromLS('all-themes');
  const [theme, setTheme] = useState(themes.data.light);
  const [themeLoaded, setThemeLoaded] = useState(false);

  const setMode = mode => {
    setToLS('theme', mode)
    setTheme(mode);
  };

  const getFonts = () => {
    const allFonts = _.values(_.mapValues(themes.data, 'font'));
    return allFonts;
  }

  useEffect(() =>{
    const localTheme = getFromLS('theme');
    localTheme ? setTheme(localTheme) : setTheme(themes.data.light);
    setThemeLoaded(true);
  }, []);

  return { theme, themeLoaded, setMode, getFonts };
};

此自定义 React 钩子从 localStorage 返回选定的主题,并返回一个布尔值,以指示主题是否已从存储中正确加载。它还公开了一个函数 setMode,用于以编程方式应用主题。我们将在稍后讨论它。有了它,我们还获得了一个字体列表,我们可以稍后使用 web font loader 加载这些字体。

使用全局样式来控制网站背景颜色、字体、按钮等是一个好主意。styled-components 提供了一个名为 createGlobalStyle 的组件,用于建立主题感知的全局组件。让我们在 src/theme 文件夹中创建一个名为 GlobalStyles.js 的文件,并在其中添加以下代码

import { createGlobalStyle} from "styled-components";

export const GlobalStyles = createGlobalStyle`
  body {
    background: ${({ theme }) => theme.colors.body};
    color: ${({ theme }) => theme.colors.text};
    font-family: ${({ theme }) => theme.font};
    transition: all 0.50s linear;
  }

  a {
    color: ${({ theme }) => theme.colors.link.text};
    cursor: pointer;
  }

  button {
    border: 0;
    display: inline-block;
    padding: 12px 24px;
    font-size: 14px;
    border-radius: 4px;
    margin-top: 5px;
    cursor: pointer;
    background-color: #1064EA;
    color: #FFFFFF;
    font-family: ${({ theme }) => theme.font};
  }

  button.btn {
    background-color: ${({ theme }) => theme.colors.button.background};
    color: ${({ theme }) => theme.colors.button.text};
  }
`;

只是针对 <body>、链接和按钮的一些 CSS,对吧?我们可以将它们用于 App.js 文件中,以通过用以下内容替换其内容来查看主题的实际应用

// 1: Import
import React, { useState, useEffect } from 'react';
import styled, { ThemeProvider } from "styled-components";
import WebFont from 'webfontloader';
import { GlobalStyles } from './theme/GlobalStyles';
import {useTheme} from './theme/useTheme';

// 2: Create a cotainer
const Container = styled.div`
  margin: 5px auto 5px auto;
`;

function App() {
  // 3: Get the selected theme, font list, etc.
  const {theme, themeLoaded, getFonts} = useTheme();
  const [selectedTheme, setSelectedTheme] = useState(theme);

  useEffect(() => {
    setSelectedTheme(theme);
   }, [themeLoaded]);

  // 4: Load all the fonts
  useEffect(() => {
    WebFont.load({
      google: {
        families: getFonts()
      }
    });
  });

  // 5: Render if the theme is loaded.
  return (
    <>
    {
      themeLoaded && <ThemeProvider theme={ selectedTheme }>
        <GlobalStyles/>
        <Container style={{fontFamily: selectedTheme.font}}>
          <h1>Theme Builder</h1>
          <p>
            This is a theming system with a Theme Switcher and Theme Builder.
            Do you want to see the source code? <a href="https://github.com/atapas/theme-builder" target="_blank">Click here.</a>
          </p>
        </Container>
      </ThemeProvider>
    }
    </>
  );
}

export default App;

这里发生了一些事情

  1. 我们导入了 useStateuseEffect React 钩子,它们将帮助我们跟踪任何状态变量及其由于任何副作用而发生的变化。我们从 styled-components 导入了 ThemeProviderstyledWebFont 也被导入以加载字体。我们还导入了自定义主题 useTheme 和全局样式组件 GlobalStyles
  2. 我们使用 CSS 样式和 styled 组件创建一个 Container 组件。
  3. 我们声明了状态变量,并注意了它们的更改。
  4. 我们加载了应用程序所需的所有字体
  5. 我们渲染了一些文本和一个链接。但是请注意,我们将整个内容包裹在 <ThemeProvider> 包装器中,它将选定的主题作为 prop。我们还传递了 <GlobalStyles/> 组件。

刷新应用程序,我们应该看到默认的“light”主题已启用。

The theme with a white background and black text.
嘿,看看这个干净、简洁的设计!

我们应该看看切换主题是否有效。所以,让我们打开 useTheme.js 文件并更改以下代码行

localTheme ? setTheme(localTheme) : setTheme(themes.data.light);

…为

localTheme ? setTheme(localTheme) : setTheme(themes.data.seaWave);

再次刷新应用程序,希望我们能看到“sea wave”主题的实际应用。

The same theme in with a blue color scheme with a light blue background and dark blue text and a blue button.
现在,我们正在体验这个蓝色为主的主题的波浪。

切换主题

很棒!我们能够正确地应用主题。如何创建一个方法,只需单击一个按钮就能切换主题?当然可以!我们还可以提供某种主题预览。

A heading instructs the user to select a theme and two card components are beneath the heading, side-by-side, showing previews of the light theme and the sea wave theme.
选项列表中提供了每个主题的预览。

让我们将这些框中的每一个都称为 ThemeCard,并以一种可以将主题定义作为 prop 传递给它的方式设置它们。我们将遍历所有主题,循环遍历它们,并将每个主题都填充为一个 ThemeCard 组件。

{
  themes.length > 0 && 
  themes.map(theme =>(
    <ThemeCard theme={data[theme]} key={data[theme].id} />
  ))
}

现在,让我们转到 ThemeCard 的标记。您的标记可能有所不同,但请注意我们如何提取自己的颜色和字体属性,然后应用它们

const ThemeCard = props => {
  return(
    <Wrapper 
      style={{backgroundColor: `${data[_.camelCase(props.theme.name)].colors.body}`, color: `${data[_.camelCase(props.theme.name)].colors.text}`, fontFamily: `${data[_.camelCase(props.theme.name)].font}`}}>
      <span>Click on the button to set this theme</span>
      <ThemedButton
        onClick={ (theme) => themeSwitcher(props.theme) }
        style={{backgroundColor: `${data[_.camelCase(props.theme.name)].colors.button.background}`, color: `${data[_.camelCase(props.theme.name)].colors.button.text}`, fontFamily: `${data[_.camelCase(props.theme.name)].font}`}}>
        {props.theme.name}
      </ThemedButton>
    </Wrapper>
  )
}

接下来,让我们在 src 文件夹中创建一个名为 ThemeSelector.js 的文件。将 来自此处的内容 复制到该文件中,以建立我们的主题切换器,我们需要在 App.js 中导入它

import ThemeSelector from './ThemeSelector';

现在,我们可以在 Container 组件中使用它

<Container style={{fontFamily: selectedTheme.font}}>
  // same as before
  <ThemeSelector setter={ setSelectedTheme } />
</Container>

现在让我们刷新浏览器,看看如何切换主题。

An animated screenshot showing the theme changing when it is selected from the list of theme card options.

有趣的是,您可以在schema.json文件中添加任意数量的主题,以便在 UI 中加载和切换主题。查看schema.json 文件,以获取更多主题。请注意,我们还会将应用的主题信息保存在localStorage中,因此下次您重新打开应用程序时,选择会保留。

Selected theme stored in the Local Storage.

自定义主题

也许您的用户喜欢某个主题的某些方面,而喜欢另一个主题的某些方面。为什么让他们在两个主题之间选择,当他们可以自己定义主题属性时呢!我们可以创建一个简单的用户界面,允许用户选择他们想要的显示选项,甚至保存他们的偏好。

Animated screenshot showing a modal opening with a list of theme options to customize the appearance, including the them name, background color, text color, button text color, link color, and font.

我们不会详细介绍主题创建代码的解释,但是,遵循 GitHub 仓库中的代码应该很容易。主要源文件是CreateThemeContent.js,它由App.js使用。我们通过收集每个输入元素更改事件的值来创建新的主题对象,并将该对象添加到主题对象的集合中。就是这样。

结束之前…

感谢您的阅读!我希望您发现我们在这里介绍的内容对您正在进行的工作有所帮助。主题系统很有趣!事实上,CSS 自定义属性正在使之越来越成为现实。例如,查看Dieter Raber 的这种方法来使用颜色,以及 Chris 的这份总结。还有 Michelle Barker 的这种设置,它依赖于与 Tailwind CSS 一起使用的自定义属性。这是来自 Andrés Galente 的另一种方式

虽然所有这些都是创建主题的很好的例子,但我希望这篇文章能通过存储属性、轻松地在主题之间切换、为用户提供自定义主题的方式以及保存这些偏好来帮助将这个概念提升到一个新的水平。

让我们联系!您可以在Twitter 上私信我评论,或者随时关注我。