前端開發領域,React和Vue的出現,標志著前端開發者可以“Say goodbye to DOM coding”,即不必再直接與DOM元素頻繁互動,他們引領著我們告別傳統的DOM操作時代,邁入MVVM(Model-View-ViewModel)模式的嶄新天地。
通過上一篇文章 [初識React框架](https://juejin.cn/post/7385752495534522383),我們能知道React是一個由Facebook開發并維護的開源JavaScript庫,并以其高效、靈活的組件化思想,成為了構建用戶界面的首選工具之一。它通過采用組件化的方式來組織代碼,使得復雜的UI結構變得易于管理和維護。本文將從一個簡單的React項目入口文件`index.js`出發,逐步揭開React的神秘面紗,探索其核心概念與機制,直至深入組件化的精髓。
我們來到React項目入口文件index.js中,這是React應用的起點。
import ReactDOM from 'react-dom/client';
const root=ReactDOM.createRoot(
document.getElementById('root')
);
root.render(<h1>征程</h1>)
通過import ReactDOM from 'react-dom/client';引入了React的DOM渲染庫,而ReactDOM.createRoot()方法則標志著React接管DOM的第一步。這個方法接收一個DOM元素作為參數(如document.getElementById('root')),為React創建了一個掛載點。從這一刻起,React將獨立管理這部分DOM,也就是這里將成為React展示的舞臺,根節點root 內部就是react的世界了,此后的所有React組件將在這個根節點下進行渲染。root.render()方法接收一個React元素作為參數,并將其渲染到與root關聯的DOM節點上。
在React的世界里,JSX是最具特色的語法糖。通過允許在JavaScript中直接編寫類似HTML的代碼,極大地提升了代碼的可讀性和編寫效率。
import ReactDOM from 'react-dom/client';
import React from 'react';
const root=ReactDOM.createRoot(
document.getElementById('root')
);
// react 的法寶 : JSX語法
const element1 = (<h1>正大會</h1>)
const element2 =React.createElement('div',{id:'name'},'正小會')
// root.render(element1)
root.render(element2)
<h1>正大會</h1>這樣的JSX表達式,不僅直觀地描述了UI結構,而且與React.createElement()相比,更加簡潔明了。JSX背后,是編譯器將其轉換為React元素(如React.createElement('h1', null, '正大會'))的過程,從而確保了JavaScript引擎的正常解析執行。也就是說明,在react中我們只要運用JSX,就可以在JavaScript直接編寫HTML。
React鼓勵我們以函數或類的形式定義組件,每個組件負責渲染一塊UI,這種設計模式大大促進了代碼的復用和維護。
import ReactDOM from 'react-dom/client';
import React from 'react';
// module 模塊化的導入
import APP from './app/app';
const root =ReactDOM.createRoot(
// 就做這一次DOM查找,ReactDOM來做查找
// DOM編程 性能很差
document.getElementById('root')
)
root.render(<APP/>)
將index.js就用于生成一個root節點,而其他的東西就交給其他的組件APP,但是要注意的是,root.render()接受的是一個參數,所以根組件里必須返回一個值。
// 根組件
// react 使用函數創建一個組件,一定要返回JSX
// 頁面由組件組成(更利于復用)
const APP = ()=>{
const element = (
<div className='container'>
<h1> classname='title'>周老板</h1>
</div>
)
return element
}
// module 模塊化的輸出
export default APP
通過定義const APP = ()=>{...}這樣的函數創建一個組件,而在組件中我們可以寫一堆的HTML、css、js,而頁面將由組件組成,更利于復用。而組件內部可以繼續嵌套其他組件(AppHeader組件),如<AppHeader />,形成了一個組件樹結構,這便是React應用的核心架構。
import AppHeader from './components/app-header';
const APP = ()=>{
const element = (
<div className='container'>
<AppHeader/>
<AppHeader />
</div>
)
return element
}
export default APP
組件化是React的靈魂。它允許我們將界面拆分成多個獨立、可復用的部分,每個部分都有自己的狀態和生命周期。
const AppHeader = (props)=>{
const {name}= props;
return (
<div className="app-header">
<h1 className="title">{name}</h1>
</div>
)
}
export default AppHeader
AppHeader組件是可以傳入參數的,別忘了他是由函數來創建的。通過屬性(props)傳遞數據(如name),這樣就可以獲取調用AppHeader的那個組件中的name值,更加利于復用。
import AppHeader from './components/app-header';
const APP = ()=>{
const element = (
<div className='container'>
<AppHeader name='吳彥祖'/>
<AppHeader name='彭于晏'/>
</div>
)
return element
}
export default APP
組件之間可以進行通信,同時保持各自的純凈性,降低了耦合度。組件化不僅提高了代碼的組織性和可測試性,也為大規模應用的開發提供了有力支撐。
React不僅僅是一個庫,它代表了一種全新的前端開發范式。通過ReactDOM.createRoot與JSX的巧妙結合,React讓我們能夠在JavaScript中以聲明式的方式描述UI,通過組件化思想實現界面的模塊化開發。這一系列機制,不僅提升了開發效率,也使得代碼更易于理解和維護。不愧是“前端第一框架”,這可不是虛名昂。接下來,我將持續學習并分享,讓我們一起加油!!
者 | 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精選文章
src寫的圖片路徑但是不顯示圖片
通過在state中定義,也不顯示
需要注意的是,require里只能寫字符串,不能寫變量。
官網鏈接:https://facebook.github.io/create-react-app/docs/using-the-public-folder#when-to-use-the-public-folder
加油,程序員。。。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。