整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

2024 年,你应该知道的 5 种 React 设计模式

者 | Lakindu Hewawasam

译者 | 许学文

策划 | 丁晓昀

如果你在开发工作中使用的是 React 框架,那么首当其冲要学习的就是思考如何设计组件。组件设计并非简单地将多个组件合成一个集合,而是需要思考如何设计更小、复用性更强的组件。例如,思考下面这张组件图:



简化的组件图


图中有三个组件,分别是:


  1. Typography 组件
  2. Footer 组件
  3. Sizeable Box 组件


如图所示,Typography 组件同时被 Footer 和 Sizeable Box 组件使用。通常我们以为这样,就能构建一个简单、易维护和易排除错误的应用了。但其实只是这样思考组件的设计是远远不够的。


如果你知道如何从组件的视角思考问题,就可以通过在 React 组件中使用设计模式来提高代码的整体模块性、可扩展性和可维护性。


因此,下面这五种设计模式,是你在使用 React 时必须要掌握的。


模式一:基础组件


首先,在使用 React 时候,请尝试为应用设计基础组件。


基础UI组件,就是一个具备默认行为且支持定制化的组件。


例如,每个应用都会通过基础的样式、按钮设计或者基础的排版,来实现应用在视觉和交互上的一致性。这些组件的设计特点是:


  1. 组件会应用一组默认的配置。因此,使用者无需进行任何额外的配置,就可以快速基于默认配置使用组件。
  2. 组件可以支持定制化,使用者通过定制化可以覆盖组件的默认行为,从而为组件提供自定义的整体视觉和交互效果。


通过一个 Button 组件就能很好地说明基础组件模式的实现。示例如下:


  1. Button 组件可能会有诸如空心、实心等不同形态。
  2. Button 组件可能会有默认的文本。


现在你就可以利用基础组件模式进行设计,使组件的使用者可以改变其行为。请参考我基于基础组件模式完成的Button组件,示例代码如下:


import React, { ButtonHTMLAttributes } from 'react';




// 按钮组件的形态:实心或者空心
type ButtonVariant = 'filled' | 'outlined';




export type ButtonProps = {
  /**
   * the variant of the button to use 
   * @default 'outlined'
   */
  variant?: ButtonVariant;
} & ButtonHTMLAttributes<HTMLButtonElement>;;




const ButtonStyles: { [key in ButtonVariant]: React.CSSProperties } = {
  filled: {
    backgroundColor: 'blue', // Change this to your filled button color
    color: 'white',
  },
  outlined: {
    border: '2px solid blue', // Change this to your outlined button color
    backgroundColor: 'transparent',
    color: 'blue',
  },
};




export function Button({ variant = 'outlined', children, style, ...rest }: ButtonProps) {
  return (
    <button
      type='button'
      style={{
        ...ButtonStyles[variant],
        padding: '10px 20px',
        borderRadius: '5px',
        cursor: 'pointer',
        ...style
      }} {...rest}>
      {children}
    </button>
  );
}

复制代码


仔细观察代码会发现,这里 Button 组件的 props 类型合并了原生 HTML 中 button 标签属性的全部类型。这意味着,使用者除了可以为 Button 组件设置默认配置外,还可以设置诸如 onClick、aria-label 等自定义配置。这些自定义配置会通过扩展运算符传递给 Button 组件内部的 button 标签。


通过不同的上下文设置,可以看到不同的 Button 组件的形态,效果截图如下图。


这个可以查看具体设置:

https://bit.cloud/lakinduhewa/react-design-patterns/base/button/~compositions



基础组件在不同上下文中的使用效果


通过不同的上下文,你可以设定组件的行为。这可以让组件成为更大组件的基础。


模式二:组合组件


在成功创建了基础组件后,你可能会希望基于基础组件创建一些新的组件。


例如,你可以使用之前创建的 Button 组件来实现一个标准的 DeleteButton 组件。通过在应用中使用该 DeleteButton,可以让应用中所有删除操作在颜色、形态以及字体上保持一致。


不过,如果出现重复组合一组组件来实现相同效果的现象,那么你可以考虑将它们封装到一个组件中。


下面,让我们来看看其中一种实现方案:

https://bit.cloud/lakinduhewa/react-design-patterns/composition/delete-button



使用组合模式创建组件


如上面的组件依赖图所示,DeleteButton 组件使用基础的 Button 组件为所有与删除相关的操作提供标准的实现。下面是基本代码实现:


// 这里引入了,基础按钮组件和其props
import { Button, ButtonProps } from '@lakinduhewa/react-design-patterns.base.button';
import React from 'react';




export type DeleteButtonProps = {} & ButtonProps;




export function DeleteButton({ ...rest }: DeleteButtonProps) {
  return (
    <Button
      variant='filled'
      style={{
        background: 'red',
        color: 'white'
      }}
      {...rest}
    >
      DELETE
    </Button>
  );
}

复制代码


我们使用基于模式一创建的 Button 组件来实现的 DeleteButton 组件的效果如下:



现在我们可以在应用中使用统一的删除按钮。此外,如果你使用类似 Bit 的构建系统进行组件的设计和构建,那么当 Button 组件发生改变时,可以让CI服务自动将此改变传递到DeleteButton组件上,就像下面这样(当 Button 组件从 0.0.3 升级到了 0.0.4,那么 CI 服务会自动触发,将 DeleteButton 组件从 0.0.1 升级到 0.0.2):


Bit 上的一个 CI 构建


模式三:使用 Hooks


React Hooks 是React v16就推出来的特性,它不依赖类组件实现状态管理、负效应等概念。简而言之,就是你可以通过利用 Hooks API 摆脱对类组件的使用需求。useSate 和 useEffect 是最广为人知的两个 Hooks API,但本文不打算讨论它们,我想重点讨论如何利用 Hooks 来提高组件的整体可维护性。


例如,请考虑下面这个场景:


  1. 有一个 BlogList 组件。
  2. BlogList 组件会通过调用一个简单的 API,获取博客文章列表数据,同时将其渲染在组件上。


基于上面的案例,你可能会像下面这样将 API 逻辑直接写在函数组件中:


import React, { useState, useEffect } from 'react';
import axios from 'axios';
const BlogList = () => {
    const [blogs, setBlogs] = useState([]);
    const [isLoading, setIsLoading] = useState(true);
    const [error, setError] = useState(null);
    useEffect(() => {
        axios.get('https://api.example.com/blogs')
            .then(response => {
                setBlogs(response.data);
                setIsLoading(false);
            })
            .catch(error => {
                setError(error);
                setIsLoading(false);
            });
    }, []);
    if (isLoading) return <div>Loading...</div>;
    if (error) return <div>Error: {error.message}</div>;
    return (
        <div>
            <h2>Blog List</h2>
            <ul>
                {blogs.map(blog => (
                    <li key={blog.id}>{blog.title}</li>
                ))}
            </ul>
        </div>
    );
};
export default BlogList;

复制代码


这样写,组件也能正常工作。它将会获取博客文章列表并且渲染在 UI 上。但是,这里将 UI 逻辑和 API 逻辑混在一起了。


理想情况下,React 组件应该不需要关系如何获取数据。而只需要关心接收一个数据数组,然后将其呈现在 DOM 上。


因此,实现这一目标的最佳方法是将 API 逻辑抽象到 React Hook 中,以便在组件内部进行调用。这样做就可以打破 API 调用与组件之间的耦合。通过这种方式,就可以在不影响组件的情况下,修改底层的数据获取逻辑。


其中一种实现方式如下。


1.useBlog hook


import { useEffect, useState } from 'react';
import { Blog } from './blog.type';
import { Blogs } from './blog.mock';




export function useBlog() {
  const [blogs, setBlogs] = useState<Blog[]>([]);
  const [loading, setLoading] = useState<boolean>(false);




  useEffect(() => {
    setLoading(true);
     // 注意:这里的setTimeout非实际需要,只是为了模拟API调用
    setTimeout(() => {
      setBlogs(Blogs);
      setLoading(false);
    }, 3000);
  }, []);




  return { blogs, loading }
}

复制代码


如上代码所示,useBlog hook 获取博客列表数据,然后赋值给状态变量,最后通过导出变量给到消费者(BlogList 组件)使用:



Hook 效果


2.BlogList 组件


import React from 'react';
// 引入上面封装的 useBlog hook
import { useBlog } from '@lakinduhewa/react-design-patterns.hooks.use-blog';
export function BlogList() {
  const { blogs, loading } = useBlog();
  if (loading) {
    return (
      <p>We are loading the blogs...</p>
    )
  }
  return (
    <ul>
      {blogs.map((blog) => <ol
        key={blog.id}
      >
        {blog.title}
      </ol>)}
    </ul>
  );
}

复制代码



BlogList 组件效果


通过调用 useBlog 和使用其导出的状态变量,我们在 BlogList 组件中使用了 Hooks。如此,相对于之前,我们可以减少大量代码,并以最少的代码和精力维护两个组件。


此外,当你使用类似 Bit 这样的构建系统时(就像我一样),只需将 useBlog 组件导入本地开发环境,然后再修改完成之后重新推送回 Bit Cloud。Bit Cloud 的构建服务器可以依托依赖树将此修改传递给整个应用。因此如果只执行一些简单修改,甚至不需要访问整个应用。


模式四:React Providers


此模式的核心是解决组件状态共享。我们都曾是 props 下钻式传递的受害者。但如果你还没有经历过,那这里简单解释下:“props 下钻式传递”就是当你在组件树中进行 props 传递时,这些 props 只会在最底层组件中被使用,而中间层的组件都不会使用该 props。例如,看看下面这张图:

props 下钻式传递


从 BlogListComponent 一直向下传递一个 isLoading 的 props 到 Loader。但是,isLoading 只在 Loader 组件中使用。因此,在这种情况下,组件不但会引入不必要的 props,还会有性能开销。因为当 isLoading 发生变化时,即使组件没有使用它,React 依然会重新渲染你的组件树。


因此,解决方案之一就是通过利用 React Context 来使用 React Context Provider 模式。React Context 是一组组件的状态管理器,通过它,你可以为一组组件创建特定的上下文。通过这种方式,你可以在上下文中定义和管理状态,让不同层级的组件都可以直接访问上下文,并按需使用 props。这样就可以避免 props 下钻式传递了。


主题组件就是该模式的一个常见场景。例如,你需要在应用程序中全局访问主题。但将主题传递到应用中的每个组件并不现实。你可以创建一个包含主题信息的 Context,然后通过 Context 来设置主题。看一下我是如何通过React Context实现主题的,以便更好地理解这一点:

https://bit.cloud/lakinduhewa/react-design-patterns/contexts/consumer-component


import { useContext, createContext } from 'react';
export type SampleContextContextType = {
  /**
   * primary color of theme.
   */
  color?: string;
};
export const SampleContextContext = createContext<SampleContextContextType>({
  color: 'aqua'
});
export const useSampleContext = () => useContext(SampleContextContext);

复制代码


在 Context 中定义了一种主题颜色,它将在所有实现中使用该颜色来设置字体颜色。接下来,我还导出了一个 hook——useSampleContext,该 hook 让消费者可以直接使用 Context。


只是这样还不行,我们还需要定义一个 Provider。Provider 是回答 "我应该与哪些组件共享状态?"问题的组件。Provider的实现示例如下:


import React, { ReactNode } from 'react';
import { SampleContextContext } from './sample-context-context';
export type SampleContextProviderProps = {
  /**
   * primary color of theme.
   */
  color?: string,
  /**
   * children to be rendered within this theme.
   */
  children: ReactNode
};
export function SampleContextProvider({ color, children }: SampleContextProviderProps) {
  return <SampleContextContext.Provider value={{ color }}>{children}</SampleContextContext.Provider>
}

复制代码


Provider 在管理初始状态和设置 Context 可访问状态的组件方面起着至关重要的作用。


接下来,你可以创建一个消费者组件来使用状态:



消费者组件


模式五:条件渲染


最后一个想和大家分享的是条件渲染模式。今天,人人都知道 React 中的条件渲染。它通过条件判断来选择组件进行渲染。


但在实际使用中我们的用法常常是错误的:


// ComponentA.js
const ComponentA = () => {
    return <div>This is Component A</div>;
};
// ComponentB.js
const ComponentB = () => {
    return <div>This is Component B</div>;
};
// ConditionalComponent.js
import React, { useState } from 'react';
import ComponentA from './ComponentA';
import ComponentB from './ComponentB';
const ConditionalComponent = () => {
    const [toggle, setToggle] = useState(true);
    return (
        <div>
            <button onClick={() => setToggle(!toggle)}>Toggle Component</button>
            {toggle ? <ComponentA /> : <ComponentB />}
        </div>
    );
};
export default ConditionalComponent;

复制代码


你是否注意到,这里我们将基于条件的逻辑耦合到了 JSX 代码片段中。通常,你不应该在 JSX 代码中中添加任何与计算相关的逻辑,而只将与 UI 渲染相关的内容放在其中。


解决这个问题的方法之一是使用条件渲染组件模式。创建一个可重用的 React 组件,该组件可以根据条件渲染两个不同的组件。它的实现过程如下:


import React, { ReactNode } from 'react';
export type ConditionalProps = {
  /**
   * the condition to test against
   */
  condition: boolean
  /**
   * the component to render when condition is true
   */
  whenTrue: ReactNode
  /**
   * the component to render when condition is false
   */
  whenFalse: ReactNode
};
export function Conditional({ condition, whenFalse, whenTrue }: ConditionalProps) {
  return condition ? whenTrue : whenFalse;
}

复制代码


我们创建了一个可以按条件渲染两个组件的组件。当我们将其集成到其他组件中时,会使代码更简洁,因为无需在 React 组件中加入复杂的渲染逻辑。你可以像下面这样使用它:


export const ConditionalTrue = () => {
  return (
    <Conditional
      condition
      whenFalse="You're False"
      whenTrue="You're True"
    />
  );
}
export const ConditionalFalse = () => {
  return (
    <Conditional
      condition={false}
      whenFalse="You're False"
      whenTrue="You're True"
    />
  );
}

复制代码


实际的输入如下:



总结


掌握这五种设计模式,为 2024 年做好充分准备,构建出可扩展和可维护的应用吧。


如果你想详细深入本文中讨论的模式,请随时查看我在Bit Cloud的空间:

https://bit.cloud/lakinduhewa/react-design-patterns


感谢你的阅读!

原文链接:2024年,你应该知道的5种React设计模式_架构/框架_InfoQ精选文章

自:coderwhy


前面说过,整个前端已经是组件化的天下,而CSS的设计就不是为组件化而生的,所以在目前组件化的框架中都在需要一种合适的CSS解决方案。

一. React中的css方案

1.1. react中的css

事实上,css一直是React的痛点,也是被很多开发者吐槽、诟病的一个点。

在组件化中选择合适的CSS解决方案应该符合以下条件:

  • 可以编写局部css:css具备自己的具备作用域,不会随意污染其他组件内的原生;
  • 可以编写动态的css:可以获取当前组件的一些状态,根据状态的变化生成不同的css样式;
  • 支持所有的css特性:伪类、动画、媒体查询等;
  • 编写起来简洁方便、最好符合一贯的css风格特点;
  • 等等...

在这一点上,Vue做的要远远好于React:

  • Vue通过在.vue文件中编写 <style><style> 标签来编写自己的样式;
  • 通过是否添加 scoped 属性来决定编写的样式是全局有效还是局部有效;
  • 通过 lang 属性来设置你喜欢的 less、sass等预处理器;
  • 通过内联样式风格的方式来根据最新状态设置和改变css;
  • 等等...

Vue在CSS上虽然不能称之为完美,但是已经足够简洁、自然、方便了,至少统一的样式风格不会出现多个开发人员、多个项目采用不一样的样式风格。

相比而言,React官方并没有给出在React中统一的样式风格:

  • 由此,从普通的css,到css modules,再到css in js,有几十种不同的解决方案,上百个不同的库;
  • 大家一直在寻找最好的或者说最适合自己的CSS方案,但是到目前为止也没有统一的方案;

在这篇文章中,我会介绍挑选四种解决方案来介绍:

  • 方案一:内联样式的写法;
  • 方案二:普通的css写法;
  • 方案三:css modules;
  • 方案四:css in js(styled-components);

1.2. 普通的解决方案

1.2.1. 内联样式

内联样式是官方推荐的一种css样式的写法:

  • style 接受一个采用小驼峰命名属性的 JavaScript 对象,,而不是 CSS 字符串;
  • 并且可以引用state中的状态来设置相关的样式;
export default class App extends PureComponent {
  constructor(props) {
    super(props);

    this.state = {
      titleColor: "red"
    }
  }

  render() {
    return (
      <div>
        <h2 style={{color: this.state.titleColor, fontSize: "20px"}}>我是App标题</h2>
        <p style={{color: "green", textDecoration: "underline"}}>我是一段文字描述</p>
      </div>
    )
  }
}

内联样式的优点:

  • 1.内联样式, 样式之间不会有冲突
  • 2.可以动态获取当前state中的状态

内联样式的缺点:

  • 1.写法上都需要使用驼峰标识
  • 2.某些样式没有提示
  • 3.大量的样式, 代码混乱
  • 4.某些样式无法编写(比如伪类/伪元素)

所以官方依然是希望内联合适和普通的css来结合编写;

1.2.2. 普通的css

普通的css我们通常会编写到一个单独的文件。

App.js中编写React逻辑代码:

import React, { PureComponent } from 'react';

import Home from './Home';

import './App.css';

export default class App extends PureComponent {
  render() {
    return (
      <div className="app">
        <h2 className="title">我是App的标题</h2>
        <p className="desc">我是App中的一段文字描述</p>
        <Home/>
      </div>
    )
  }
}

App.css中编写React样式代码:

.title {
  color: red;
  font-size: 20px;
}

.desc {
  color: green;
  text-decoration: underline;
}

这样的编写方式和普通的网页开发中编写方式是一致的:

  • 如果我们按照普通的网页标准去编写,那么也不会有太大的问题;
  • 但是组件化开发中我们总是希望组件是一个独立的模块,即便是样式也只是在自己内部生效,不会相互影响;
  • 但是普通的css都属于全局的css,样式之间会相互影响;

比如编写Home.js的逻辑代码:

import React, { PureComponent } from 'react';

import './Home.css';

export default class Home extends PureComponent {
  render() {
    return (
      <div className="home">
        <h2 className="title">我是Home标题</h2>
        <span className="desc">我是Home中的span段落</span>
      </div>
    )
  }
}

又编写了Home.css的样式代码:

.title {
  color: orange;
}

.desc {
  color: purple;
}

最终样式之间会相互层叠,只有一个样式会生效;

1.2.3. css modules

css modules并不是React特有的解决方案,而是所有使用了类似于webpack配置的环境下都可以使用的。

但是,如果在其他项目中使用,那么我们需要自己来进行配置,比如配置webpack.config.js中的modules: true等。

但是React的脚手架已经内置了css modules的配置:

  • .css/.less/.scss 等样式文件都修改成 .module.css/.module.less/.module.scss 等;
  • 之后就可以引用并且进行使用了;

使用的方式如下:

css modules用法

这种css使用方式最终生成的class名称会全局唯一:

生成的代码结构

css modules确实解决了局部作用域的问题,也是很多人喜欢在React中使用的一种方案。

但是这种方案也有自己的缺陷:

  • 引用的类名,不能使用连接符(.home-title),在JavaScript中是不识别的;
  • 所有的className都必须使用{style.className} 的形式来编写;
  • 不方便动态来修改某些样式,依然需要使用内联样式的方式;

如果你觉得上面的缺陷还算OK,那么你在开发中完全可以选择使用css modules来编写,并且也是在React中很受欢迎的一种方式。

二. CSS in JS

2.1. 认识CSS in JS

实际上,官方文档也有提到过CSS in JS这种方案:

  • “CSS-in-JS” 是指一种模式,其中 CSS 由 JavaScript 生成而不是在外部文件中定义;
  • 注意此功能并不是 React 的一部分,而是由第三方库提供。 React 对样式如何定义并没有明确态度;

在传统的前端开发中,我们通常会将结构(HTML)、样式(CSS)、逻辑(JavaScript)进行分离。

  • 但是在前面的学习中,我们就提到过,React的思想中认为逻辑本身和UI是无法分离的,所以才会有了JSX的语法。
  • 样式呢?样式也是属于UI的一部分;
  • 事实上CSS-in-JS的模式就是一种将样式(CSS)也写入到JavaScript中的方式,并且可以方便的使用JavaScript的状态;
  • 所以React有被人称之为 All in JS;

当然,这种开发的方式也受到了很多的批评:

  • Stop using CSS in JavaScript for web development
  • https://hackernoon.com/stop-using-css-in-javascript-for-web-development-fa32fb873dcc

批评声音虽然有,但是在我们看来很多优秀的CSS-in-JS的库依然非常强大、方便:

  • CSS-in-JS通过JavaScript来为CSS赋予一些能力,包括类似于CSS预处理器一样的样式嵌套、函数定义、逻辑复用、动态修改状态等等;
  • 依然CSS预处理器也具备某些能力,但是获取动态状态依然是一个不好处理的点;
  • 所以,目前可以说CSS-in-JS是React编写CSS最为受欢迎的一种解决方案;

目前比较流行的CSS-in-JS的库有哪些呢?

  • styled-components
  • emotion
  • glamorous

目前可以说styled-components依然是社区最流行的CSS-in-JS库,所以我们以styled-components的讲解为主;

安装styled-components:

yarn add styled-components

2.2. styled-components

2.2.1. 标签模板字符串

ES6中增加了模板字符串的语法,这个对于很多人来说都会使用。

但是模板字符串还有另外一种用法:标签模板字符串(Tagged Template Literals)。

我们一起来看一个普通的JavaScript的函数:

function foo(...args) {
  console.log(args);
}

foo("Hello World");

正常情况下,我们都是通过 函数名() 方式来进行调用的,其实函数还有另外一种调用方式:

foo`Hello World`; // [["Hello World"]]

如果我们在调用的时候插入其他的变量:

  • 模板字符串被拆分了;
  • 第一个元素是数组,是被模块字符串拆分的字符串组合;
  • 后面的元素是一个个模块字符串传入的内容;
foo`Hello ${name}`; // [["Hello ", ""], "kobe"];

在styled component中,就是通过这种方式来解析模块字符串,最终生成我们想要的样式的

2.2.2. styled基本使用

styled-components的本质是通过函数的调用,最终创建出一个组件:

  • 这个组件会被自动添加上一个不重复的class;
  • styled-components会给该class添加相关的样式;

比如我们正常开发出来的Home组件是这样的格式:

<div>
  <h2>我是Home标题</h2>
  <ul>
    <li>我是列表1</li>
    <li>我是列表2</li>
    <li>我是列表3</li>
  </ul>
</div>

我们希望给外层的div添加一个特殊的class,并且添加相关的样式:

styled-components基本使用

另外,它支持类似于CSS预处理器一样的样式嵌套:

  • 支持直接子代选择器或后代选择器,并且直接编写样式;
  • 可以通过&符号获取当前元素;
  • 直接伪类选择器、伪元素等;
const HomeWrapper = styled.div`
  color: purple;

  h2 {
    font-size: 50px;
  }

  ul > li {
    color: orange;

    &.active {
      color: red;
    }

    &:hover {
      background: #aaa;
    }

    &::after {
      content: "abc"
    }
  }
`

最终效果如下

2.2.3. props、attrs属性

props可以穿透

定义一个styled组件:

const HYInput = styled.input`
  border-color: red;

  &:focus {
    outline-color: orange;
  }
`

使用styled的组件:

<HYInput type="password"/>

props可以被传递给styled组件

<HomeWrapper color="blue">
</HomeWrapper>

使用时可以获取到传入的color:

  • 获取props需要通过${}传入一个插值函数,props会作为该函数的参数;
  • 这种方式可以有效的解决动态样式的问题;
const HomeWrapper = styled.div`
  color: ${props => props.color};
}

添加attrs属性

const HYInput = styled.input.attrs({
  placeholder: "请填写密码",
  paddingLeft: props => props.left || "5px"
})`
  border-color: red;
  padding-left: ${props => props.paddingLeft};

  &:focus {
    outline-color: orange;
  }
`

2.2.4. styled高级特性

支持样式的继承

编写styled组件

const HYButton = styled.button`
  padding: 8px 30px;
  border-radius: 5px;
`

const HYWarnButton = styled(HYButton)`
  background-color: red;
  color: #fff;
`

const HYPrimaryButton = styled(HYButton)`
  background-color: green;
  color: #fff;
`

按钮的使用

<HYButton>我是普通按钮</HYButton>
<HYWarnButton>我是警告按钮</HYWarnButton>
<HYPrimaryButton>我是主要按钮</HYPrimaryButton>

styled设置主题

在全局定制自己的主题,通过Provider进行共享:

import { ThemeProvider } from 'styled-components';

<ThemeProvider theme={{color: "red", fontSize: "30px"}}>
  <Home />
  <Profile />
</ThemeProvider>

在styled组件中可以获取到主题的内容:

const ProfileWrapper = styled.div`
  color: ${props => props.theme.color};
  font-size: ${props => props.theme.fontSize};
`

2.3. classnames

vue中添加class

在vue中给一个元素添加动态的class是一件非常简单的事情:

你可以通过传入一个对象:

<div
  class="static"
  v-bind:class="{ active: isActive, 'text-danger': hasError }"
></div>

你也可以传入一个数组:

<div v-bind:class="[activeClass, errorClass]"></div>

甚至是对象和数组混合使用:

<div v-bind:class="[{ active: isActive }, errorClass]"></div>

react中添加class

React在JSX给了我们开发者足够多的灵活性,你可以像编写JavaScript代码一样,通过一些逻辑来决定是否添加某些class:

import React, { PureComponent } from 'react'

export default class App extends PureComponent {
  constructor(props) {
    super(props);

    this.state = {
      isActive: true
    }
  }

  render() {
    const {isActive} = this.state; 

    return (
      <div>
        <h2 className={"title " + (isActive ? "active": "")}>我是标题</h2>
        <h2 className={["title", (isActive ? "active": "")].join(" ")}>我是标题</h2>
      </div>
    )
  }
}

这个时候我们可以借助于一个第三方的库:classnames

  • 很明显,这是一个用于动态添加classnames的一个库。

我们来使用一下最常见的使用案例:

classNames('foo', 'bar'); // => 'foo bar'
classNames('foo', { bar: true }); // => 'foo bar'
classNames({ 'foo-bar': true }); // => 'foo-bar'
classNames({ 'foo-bar': false }); // => ''
classNames({ foo: true }, { bar: true }); // => 'foo bar'
classNames({ foo: true, bar: true }); // => 'foo bar'

// lots of arguments of various types
classNames('foo', { bar: true, duck: false }, 'baz', { quux: true }); // => 'foo bar baz quux'

// other falsy values are just ignored
classNames(null, false, 'bar', undefined, 0, 1, { baz: null }, ''); // => 'bar 1'

我是@半糖学前端 ,专注前端技术领域分享,关注我和我一起学习,共同进步!

今,纵观各类招聘网站上的前端职位,大家往往都会看到一个熟悉的字眼:React。虽然企业雇主也经常会列出其他一些类似的前端框架选项,但 React 的地位几乎是雷打不动。


但面对这样的现实,请原谅我始终无法理解。除了流行,React 到底还有什么优势?


首先我想澄清一点,我对 React 没有任何敌意。我知道它挺好,而且如果需要开发庞大复杂的前端项目,我其实也并不抗拒使用 React。


React 的出现,为其他框架当前及未来的功能规划奠定了基础。Vue 3及其组合 API 明显是受到 React hooks 的启发。Svelte 中的很多约定就来自 React。至于我最喜爱的 Vue 框架 Nuxt,它的大部分设计思路都来自 React 的元框架 Next。总而言之,整个基于组件的前端设计和开发模型,特别是目前的整个生态系统,都离不开 React 的鼎力支持。


但就个人观点,我还是觉得 React 像是那种开创了流派的祖师级老电影、音乐专辑或者电子游戏。它的伟大体现在那个时间点上的开创性,但时至今日自身的绝对价值其实已经很有限了。


好吧,这话可能说得有点狠。毕竟一提到开创性的老电影,大家首先想到的就是《公民凯恩》,React 跟其他框架间的差距肯定不像《公民凯恩》相较于后来的经典佳作那么大。我的观点很简单:


React 已经老了,只是经常用它的朋友们还没有意识到它老到了什么程度、引发了哪些问题。

如果只用 React,那可能会觉得这框架不错啊,而且一直在努力改进。确实,React 在很多方面都是越来越好,但这并不能改变它的发展速度和功能上限已经长期跟不上同类方案的事实。

总之一句话,React 表现得不错,只是不像其他框架那么好。


工作中的最佳选项


假设大家身为某家科技初创公司的 CTO,或者是打算开发某网络软件新产品的个人创业者。


我们面前摆着新项目的蓝图,可以随意选择自己喜爱的技术进行构建。没有约束,也不设产品生命周期限制,那么你会选择哪一种前端框架?


(有些朋友可能会抬杠说,不用前端框架也行。但对于任何成规模、比较复杂的项目来说,不用前端框架肯定不是什么好主意。)


要做出选择,先要考虑以下几项条件:


  • 性能
  • 学习曲线
  • 绑定包大小
  • 可扩展性
  • 社区与支持
  • 资金支持
  • 开发者体验
  • 人才供应


而有没有一种可能,从这么多角度来论证,其实 React 并不是什么好选择。


下面咱们逐一探讨。


性能


大家可以通过多种不同指标来衡量性能。但无论关注哪个具体方面,React 都称不上顶级水准。Vue、Svelte、Solid、Inferno 等工具的性能总体上都要好于 React。根据实际需求,大家甚至可以通过 Alpine 或者 Petite Vue 等让性能更上一层楼(虽然我觉得这两种跟其他框架并不算同一类方案)。


React 的性能不佳应该已经是个共识,所以这里无需继续赘述。所以我认为,如果大家希望让新项目拥有强大的性能表现,那 React 就可以直接被排除在外。


学习曲线


假设你对前端框架一无所知,那 React 也绝对不是最好学、最容易上手的选项。


JSX的实质,就是笨拙地把 HTML 硬塞进 JavaScript 函数返回。你以为这就够糟了?不,更糟的是你在 React 里不用 JSX。


除了 JSX,React 本身也有不少独特的约束和毛病(比如提供两种完全不同的语法,但二者完全无法互操作)。


在 React 中,其他前端框架能够帮我们轻松打理的小事,往往还是需要手动干预或者借助大量样板文件(甚至二者兼有)。


几乎每种前端框架都把用户设想成普通人,但 React 不同,它最早是专为 Facebook 的工程师们打造的。虽然经过多年发展,它已经成为一种比较通行的市场化产品,但即使到现在,这样的出身仍然给 React 留下了深深的烙印。我们还是可以看到其中留下的早期决策与优化痕迹。


至于其他问题,那可太多了。万恶之源 useEffect,不能在 JSX 中使用某些标准 HTML 属性(因为 JSX 无法区分 React prop 和 HTML 属性),记忆化,只能靠短路运算符模仿出来的虚假条件,以及要求开发者自己防止无限循环等等……其实连这里的循环都是假的,必须靠数组方法才能实现。不说了,说不完。


绑定包大小


这一点跟速度类似,但我觉得还是有必要区分开来。哪怕下载包大一点,但实际使用时性能更好,那也没啥问题。但 React 可不是这样。


React 软件包相当臃肿,这个大家都知道了,我也不多废话。


我想强调的是,有些人觉得 React 大多数情况下会从缓存中加载,所以绑定包大小无所谓。这种认知最早是假的,后来现代浏览器让它成了真,可最近的安全升级开始阻止域之间的缓存共享,所以又成了假的。


另外,Preact 虽然表现不错,但还没办法跟 React 无缝对接。而且 Preact 的包大小跟其他前端框架比也没有太大的优势。


可扩展性


虽然 React 对应的企业应用规模肯定是最大的,但我觉得吧,数量跟质量并不是一回事。


从 Vue 到Svelte,再到Angular和 Ember,每一款主流前端框架都拥有类似的大规模执行能力。他们的网站主页上,也不乏一个个声名显赫的重量级客户徽标。


所以 React 没什么特别的,只是案例最多罢了。


如果大家有很强的从众心理,那我也无话可说。但客户多真的不代表 React 就一定更优秀,它只是出现在了充满机会的时代。


社区与支持


没错,React 背后的社区规模最大,但这还是不足以支撑 React 就最好的结论。


大社区也有负面影响,特别是对 React 这类“无倾向性”框架而言。社区过大可能对应着太多可供选择的套餐,太多相互冲突、彼此对抗的观点,逼着用户在其中站队,然后承受随之而来的一切。


不仅如此,社区太大,甚至不一定能让产品越变越好。


最初,更多的参与者确实能不断带来好的功能特性。但这里存在一个收益递减点(即不断上升的沟通成本,逐渐开始减慢、而非加快项目发展速度)。除此之外,社区的参与人数和社区质量间也没有必然关联。


当然,我理解想要去爆满的餐厅吃饭那种心情,这似乎能给人一种安全感。毕竟哪怕不好吃,还有那么多人跟我一起上当呢,这波不亏。但哪怕人再多,都无法改变“不好吃”这个基本事实。这跟愿意来吃的人是多是少,真的没什么关系。所以我们用不着非往最火爆的餐厅去挤,挑一家适合自己的、安静享受一顿美食,不就足够了?


资金支持


有些人总担心自己使用的框架,会在某一天突然消失,由此失去支持和维护。对他们来说,出自 Facebook 系的 React 天然值得信任。但问题是,其他前端项目的资金支持有那么不堪吗?


Angular 的背后可是谷歌,Vue 则是历史上最成功、资金也最充裕的开源项目之一。Vercel 目前至少雇用了两位 Svelte 维护者(其中包括 Svelte 的缔造者)全职负责项目管理。至于 Solid,已经拥有超过 100 名贡献者和至少六家主要企业赞助商。


所以,资金支持对各大主流前端框架来说都不是问题,React 在这方面同样不算占优。


开发者体验


React 确实是应用范围最广的前端框架,知名度也是一时无两。但在今年的 JS 现状调查报告中,React 的开发者满意度已经不及 Solid 和 Svelte。至于受关注度,React 落后于 Svelte、Solid 和 Vue,甚至已经跌破 50%。


多年以来,React 的用户满意度和关注度一直在稳步下降,采用率也停滞不前。


当然,这类调查问卷只能作为参考,在问题的表述方式上稍做手脚就能得出不同的答案。但其他同类调查也发现了类似的趋势。在 Stack Overflow 的调查中,React 的受欢迎度远低于 Svelte,仅略微高于 Vue。


有趣的是,Scott Tolinski 最近提到,他的一位开发者放弃了薪酬丰厚的 React 职位,宁愿拿一半的工资也要加入 Tolinski 领导的 SvelteKit 项目。


当然了,并不能用这个例子证明开发者连钱都愿意放弃,就为了离 React 远一点。但至少能够看出,React 带给开发者的使用感受实在称不上好。


人才供应


这方面,React 确实堪称一骑绝尘。如果想让新人快速理解之前的开发资产,那 React 的优势可太明显了。


但是吧,我觉得单这一点不足以让 React 脱颖而出。


鉴于选择 React 之后,应用程序的绑定包本身就更大、速度更慢而且复杂性更高,用这么多弊端来换取所谓项目接管期间的一点点便利,无疑是在牺牲长远收益寻求眼下省事。翻译翻译,这不就是典型的技术债务吗?


现在省下的几个礼拜,未来可能需要几个月甚至几年来偿还。所以虽然 React 在这方面占优,但大家最好还是认真核算一下,没办法无脑选它。


另外,了解 React 的朋友想上手其他前端框架,应该不是什么难事。没错,不同框架间总有一些细微差别和小怪癖,但其中遵循的设计理念还是大体相同的。任何熟悉 React 的优秀开发者,也都能在其他框架上获得同样的工作成效。


我承认,商业世界从来没有好坏,只有权衡取舍。开发速度很重要,前面提到的每一点也都很重要。所以,您的实际情况可能证明,哪怕是 React 速度更慢、绑定包更大、复杂度更高,它也仍然值得选择。是的,我都理解。


但大家做出的选择,最终也将成为与竞争对手在市场上搏杀时的一张牌。选得好就是好牌,反之亦然。如果大多数对手也选择 React,那大家就是烂牌对局、菜鸡互啄。


而如果能选得更好,也许就能压制对方的牌形。


批评了半天,React 为什么还是傲视同侪?


因为很多人在做选择时,往往是比较草率的。React 之所以现在受欢迎,就是因为 React 之前受欢迎。


在真正占领市场之前,人们其实是出于其他理由去选择 React 的。也许因为它能解决开发者面对的实际问题,也许因为它比较新奇有趣,或者是其他什么原因。但可以肯定的是,当时人们选 React 绝不是看中它更好就业,或者是市场普及率最高的框架。而时过境迁,现在大家再选择它,唯一的理由就是它够老、够踏实。


企业选它,因为人才市场上懂 React 的群体很大;求职者学它,是因为人才市场上企业想要招聘 React 开发者。这是个自我强化的循环,一个自我实现的预言。


于是 React 成了默认选项,只要没有充分的理由,更多人只会以无所谓的态度接着用。


while (reactIsPopular) {
  reactIsPopular = true
}

复制代码


“毕竟没人会因为用了 React 而被解雇”,这话倒也没错。React 是个安全的选择,可能不是每个人的最爱,但至少不会惹出大麻烦。它,还是能干活的。


所以只要没有强烈的业务需求,求职者和招聘方都可以接受围绕 React 建立起来的行业现状。只要能把双方对接起来,React 的作用就已经达到了。


这一切会改变吗?如何改变?


我其实一直在关注事态的变化。


但要说答案,我也没有。根据之前提到的几份调查报告,React 的采用率确实出现了停滞。也不能说不再增长,只能说 React 的增长跟不断扩大的市场本体之间保持了同步,三年来份额一直维持在 80%左右。


但终有一天,我相信这种循环会中断。但我不知道具体的导火索会是什么。


也许是个量变引发质变的过程。回想起来,很多趋势来得看似突然,但实际上一直在随时间推移而积蓄力量。也许其他前端框架更好地证明了自己,并逐渐削平了 React 在人才储备方面的优势,于是企业开始向其他方案敞开怀抱。


也可能会有部分企业触及 React 的性能上限,并结合业务需求转向性能更强的选项。比方说,如果公司的业务对移动端性能有着极高要求,而且必须能够在设备配置差、网络不稳定的区域内提供良好体验,那 React 差出的这部分性能就足以促成改变。


但对大多数企业来说,情况还没那么极端。大部分旧项目实在没必要做迁移,性能困扰虽然偶然出现,也绝不至于要因此推动大规模重构。所以继续用 React 完全没有问题,它在很多场景下已经完全够用了。


所以没准市场就固化在了这一刻,再没有谁能真正挑战 React 的统治地位。等到它真正宣布退位的时候,也许我们已经彻底抛弃了前端框架,那时候主流浏览器、特别是 JS 已经扩展到了不需要它们的地步。这就是所谓后框架时代。


还有一种可能,React 在事实上已经过时了,只是在宏观统计上还体现不出来。目前人才市场上的招聘需求,反映的是企业在很久之前做出的框架选择。正如核酸测试体现的是几天、甚至几周之前的区域内疫情状况一样,目前的招聘态势也许也存在滞后。


我不知道未来的前端会是什么样子,应该没人能做出准确的预言。但可以肯定的是,React 还能风光上好一阵子。


如果大家正在学习前端开发,想用这个为自己找份工作或提升职业水平,那 React 是个不错的选项、也是非常安全的方向。


但我也希望能有越来越多的开发者积极探索其他选项,也希望企业能给他们更多尝试的机会。近年来,前端开发领域的惊喜都来自 Vue 和 Svelte。以我个人的感受和经验,React 只是能干活,并没让工作变得更有趣。


而且越来越多的开发者也意识到了这个问题,开始尝试接触其他框架、体验它们的不同特性,也反过来意识到 React 是有多么老迈和迟钝。即使单纯从推动未来前端开发者多样性的角度出发,我也真心建议大家用用别的框架方案。


原文链接:


https://joshcollinsworth.com/blog/self-fulfilling-prophecy-of-react