者 | Lakindu Hewawasam
譯者 | 許學文
策劃 | 丁曉昀
如果你在開發工作中使用的是 React 框架,那么首當其沖要學習的就是思考如何設計組件。組件設計并非簡單地將多個組件合成一個集合,而是需要思考如何設計更小、復用性更強的組件。例如,思考下面這張組件圖:
簡化的組件圖
圖中有三個組件,分別是:
如圖所示,Typography 組件同時被 Footer 和 Sizeable Box 組件使用。通常我們以為這樣,就能構建一個簡單、易維護和易排除錯誤的應用了。但其實只是這樣思考組件的設計是遠遠不夠的。
如果你知道如何從組件的視角思考問題,就可以通過在 React 組件中使用設計模式來提高代碼的整體模塊性、可擴展性和可維護性。
因此,下面這五種設計模式,是你在使用 React 時必須要掌握的。
首先,在使用 React 時候,請嘗試為應用設計基礎組件。
基礎UI組件,就是一個具備默認行為且支持定制化的組件。
例如,每個應用都會通過基礎的樣式、按鈕設計或者基礎的排版,來實現應用在視覺和交互上的一致性。這些組件的設計特點是:
通過一個 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 構建
React Hooks 是React v16就推出來的特性,它不依賴類組件實現狀態管理、負效應等概念。簡而言之,就是你可以通過利用 Hooks API 擺脫對類組件的使用需求。useSate 和 useEffect 是最廣為人知的兩個 Hooks API,但本文不打算討論它們,我想重點討論如何利用 Hooks 來提高組件的整體可維護性。
例如,請考慮下面這個場景:
基于上面的案例,你可能會像下面這樣將 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 調用與組件之間的耦合。通過這種方式,就可以在不影響組件的情況下,修改底層的數據獲取邏輯。
其中一種實現方式如下。
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 效果
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 的構建服務器可以依托依賴樹將此修改傳遞給整個應用。因此如果只執行一些簡單修改,甚至不需要訪問整個應用。
此模式的核心是解決組件狀態共享。我們都曾是 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解決方案。
事實上,css一直是React的痛點,也是被很多開發者吐槽、詬病的一個點。
在組件化中選擇合適的CSS解決方案應該符合以下條件:
在這一點上,Vue做的要遠遠好于React:
Vue在CSS上雖然不能稱之為完美,但是已經足夠簡潔、自然、方便了,至少統一的樣式風格不會出現多個開發人員、多個項目采用不一樣的樣式風格。
相比而言,React官方并沒有給出在React中統一的樣式風格:
在這篇文章中,我會介紹挑選四種解決方案來介紹:
內聯樣式是官方推薦的一種css樣式的寫法:
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>
)
}
}
內聯樣式的優點:
內聯樣式的缺點:
所以官方依然是希望內聯合適和普通的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;
}
這樣的編寫方式和普通的網頁開發中編寫方式是一致的:
比如編寫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;
}
最終樣式之間會相互層疊,只有一個樣式會生效;
css modules并不是React特有的解決方案,而是所有使用了類似于webpack配置的環境下都可以使用的。
但是,如果在其他項目中使用,那么我們需要自己來進行配置,比如配置webpack.config.js中的modules: true等。
但是React的腳手架已經內置了css modules的配置:
使用的方式如下:
css modules用法
這種css使用方式最終生成的class名稱會全局唯一:
生成的代碼結構
css modules確實解決了局部作用域的問題,也是很多人喜歡在React中使用的一種方案。
但是這種方案也有自己的缺陷:
如果你覺得上面的缺陷還算OK,那么你在開發中完全可以選擇使用css modules來編寫,并且也是在React中很受歡迎的一種方式。
實際上,官方文檔也有提到過CSS in JS這種方案:
在傳統的前端開發中,我們通常會將結構(HTML)、樣式(CSS)、邏輯(JavaScript)進行分離。
當然,這種開發的方式也受到了很多的批評:
批評聲音雖然有,但是在我們看來很多優秀的CSS-in-JS的庫依然非常強大、方便:
目前比較流行的CSS-in-JS的庫有哪些呢?
目前可以說styled-components依然是社區最流行的CSS-in-JS庫,所以我們以styled-components的講解為主;
安裝styled-components:
yarn add styled-components
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中,就是通過這種方式來解析模塊字符串,最終生成我們想要的樣式的
styled-components的本質是通過函數的調用,最終創建出一個組件:
比如我們正常開發出來的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"
}
}
`
最終效果如下
props可以穿透
定義一個styled組件:
const HYInput = styled.input`
border-color: red;
&:focus {
outline-color: orange;
}
`
使用styled的組件:
<HYInput type="password"/>
props可以被傳遞給styled組件
<HomeWrapper color="blue">
</HomeWrapper>
使用時可以獲取到傳入的color:
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;
}
`
支持樣式的繼承
編寫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};
`
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('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 成了默認選項,只要沒有充分的理由,更多人只會以無所謂的態度接著用。
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
*請認真填寫需求信息,我們會在24小時內與您取得聯系。