Sügav sukeldumine Reacti komponentide arhitektuuri, võrreldes kompositsiooni ja pärilikkust. Uuri, miks React eelistab kompositsiooni ja avasta mustreid nagu HOC-d, Render Props ja Hooks, et luua skaleeritavaid ja korduvkasutatavaid komponente.
Reacti komponentide arhitektuur: Miks kompositsioon on pärilikkusest üle
Tarkvaraarenduse maailmas on arhitektuur esmatähtis. See, kuidas me oma koodi struktureerime, määrab selle skaleeritavuse, hooldatavuse ja korduvkasutatavuse. Reactiga töötavate arendajate jaoks on üks fundamentaalsemaid arhitektuurilisi otsuseid seotud sellega, kuidas jagada loogikat ja kasutajaliidest komponentide vahel. See viib meid klassikalise objektorienteeritud programmeerimise debatini, mis on Reacti komponentidepõhise maailma jaoks ümber mõeldud: Kompositsioon vs. pärilikkus.
Kui teil on kogemusi klassikaliste objektorienteeritud keeltega nagu Java või C++, võib pärilikkus tunduda loomuliku esimese valikuna. See on võimas kontseptsioon 'on-üks' suhete loomiseks. Kuid ametlik Reacti dokumentatsioon annab selge ja tugeva soovituse: "Facebookis kasutame Reacti tuhandetes komponentides ja me ei ole leidnud ühtegi kasutusjuhtu, kus soovitaksime luua komponentide pärilikkuse hierarhiaid."
See postitus pakub põhjaliku ülevaate sellest arhitektuurilisest valikust. Me selgitame lahti, mida pärilikkus ja kompositsioon Reacti kontekstis tähendavad, demonstreerime, miks kompositsioon on idiomaatiline ja parem lähenemisviis, ning uurime võimsaid mustreid – alates kõrgema järgu komponentidest kuni moodsate Hookideni –, mis muudavad kompositsiooni arendaja parimaks sõbraks robustsete ja paindlike rakenduste ehitamisel ülemaailmsele publikule.
Vana kaardiväe mõistmine: Mis on pärilikkus?
Pärilikkus on objektorienteeritud programmeerimise (OOP) üks alustalasid. See võimaldab uuel klassil (alamklass ehk laps) omandada olemasoleva klassi (ülemklass ehk vanem) omadused ja meetodid. See loob tihedalt seotud 'on-üks' suhte. Näiteks GoldenRetriever
on Dog
, mis on Animal
.
Pärilikkus Reacti-välises kontekstis
Vaatame lihtsat JavaScripti klassi näidet, et kontseptsiooni paremini mõista:
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a noise.`);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // Calls the parent constructor
this.breed = breed;
}
speak() { // Overrides the parent method
console.log(`${this.name} barks.`);
}
fetch() {
console.log(`${this.name} is fetching the ball!`);
}
}
const myDog = new Dog('Buddy', 'Golden Retriever');
myDog.speak(); // Output: "Buddy barks."
myDog.fetch(); // Output: "Buddy is fetching the ball!"
Selles mudelis saab Dog
klass automaatselt name
omaduse ja speak
meetodi klassilt Animal
. Samuti saab see lisada oma meetodeid (fetch
) ja olemasolevaid üle kirjutada. See loob jäiga hierarhia.
Miks pärilikkus Reactis ebaõnnestub
Kuigi see 'on-üks' mudel sobib mõnede andmestruktuuride jaoks, tekitab see olulisi probleeme, kui seda rakendada Reacti kasutajaliidese komponentidele:
- Tihe sidusus: Kui komponent pärib baaskomponendilt, muutub see tihedalt seotuks oma vanema implementatsiooniga. Muudatus baaskomponendis võib ootamatult rikkuda mitu lapskomponenti ahelas allapoole. See muudab refaktoorimise ja hoolduse hapraks protsessiks.
- Paindumatu loogika jagamine: Mis siis, kui soovite jagada spetsiifilist funktsionaalsust, näiteks andmete pärimist, komponentidega, mis ei sobi samasse 'on-üks' hierarhiasse? Näiteks nii
UserProfile
kui kaProductList
võivad vajada andmete pärimist, kuid pole mõtet, et nad päriksid ühisestDataFetchingComponent
'ist. - Prop'ide läbipuurimise põrgu: Sügavas pärilikkuse ahelas muutub prop'ide edastamine ülemise taseme komponendist sügavalt pesastatud lapseni keeruliseks. Võib juhtuda, et peate prop'e edastama läbi vahekomponentide, mis neid isegi ei kasuta, mis viib segase ja paisunud koodini.
- "Gorilla-banaani probleem": Tuntud tsitaat OOP-eksperdilt Joe Armstrongilt kirjeldab seda probleemi ideaalselt: "Sa tahtsid banaani, aga said gorilla, kes hoidis banaani ja kogu džunglit." Pärilikkusega ei saa te lihtsalt seda funktsionaalsuse tükki, mida soovite; olete sunnitud kaasa võtma kogu ülemklassi.
Nende probleemide tõttu disainis Reacti meeskond teegi paindlikuma ja võimsama paradigma ümber: kompositsiooni.
Reacti viisi omaksvõtmine: Kompositsiooni jõud
Kompositsioon on disainipõhimõte, mis eelistab 'omab-üht' või 'kasutab-üht' suhet. Selle asemel, et komponent oleks teine komponent, see omab teisi komponente või kasutab nende funktsionaalsust. Komponente käsitletakse ehitusplokkidena – nagu LEGO klotsid –, mida saab kombineerida erinevatel viisidel, et luua keerukaid kasutajaliideseid, ilma et oleks lukustatud jäigasse hierarhiasse.
Reacti kompositsioonimudel on uskumatult mitmekülgne ja avaldub mitmes võtmemustris. Uurime neid, alustades kõige lihtsamast kuni kõige kaasaegsema ja võimsamani.
Tehnika 1: Sisaldamine props.children
abil
Kõige otsesem kompositsiooni vorm on sisaldamine. See on olukord, kus komponent toimib üldise konteineri või 'kastina' ja selle sisu edastatakse vanemkomponendist. Reactil on selleks spetsiaalne, sisseehitatud prop: props.children
.
Kujutage ette, et vajate Card
komponenti, mis suudab mässida mis tahes sisu ühtse raami ja varjuga. Selle asemel, et luua pärilikkuse kaudu TextCard
, ImageCard
ja ProfileCard
variante, loote ühe üldise Card
komponendi.
// Card.js - A generic container component
function Card(props) {
return (
<div className="card">
{props.children}
</div>
);
}
// App.js - Using the Card component
function App() {
return (
<div>
<Card>
<h1>Welcome!</h1>
<p>This content is inside a Card component.</p>
</Card>
<Card>
<img src="/path/to/image.jpg" alt="An example image" />
<p>This is an image card.</p>
</Card>
</div>
);
}
Siin ei tea ega hooli Card
komponent sellest, mida see sisaldab. See pakub lihtsalt ümbritsevat stiili. Sisu avava ja sulgeva <Card>
sildi vahel edastatakse automaatselt kui props.children
. See on ilus näide lahtisidumisest ja korduvkasutatavusest.
Tehnika 2: Spetsialiseerimine prop'ide abil
Mõnikord vajab komponent mitut 'auku', mis tuleb täita teiste komponentidega. Kuigi võiksite kasutada props.children
, on selgesõnalisem ja struktureeritum viis edastada komponente tavaliste prop'idena. Seda mustrit nimetatakse sageli spetsialiseerimiseks.
Mõelge Modal
komponendile. Modaalsel aknel on tavaliselt päiseosa, sisuosa ja tegevuste osa (nuppudega nagu "Kinnita" või "Tühista"). Me saame oma Modal
komponendi disainida nii, et see aktsepteerib neid osi prop'idena.
// Modal.js - A more specialized container
function Modal(props) {
return (
<div className="modal-backdrop">
<div className="modal-content">
<div className="modal-header">{props.title}</div>
<div className="modal-body">{props.body}</div>
<div className="modal-footer">{props.actions}</div>
</div>
</div>
);
}
// App.js - Using the Modal with specific components
function App() {
const confirmationTitle = <h2>Confirm Action</h2>;
const confirmationBody = <p>Are you sure you want to proceed with this action?</p>;
const confirmationActions = (
<div>
<button>Confirm</button>
<button>Cancel</button>
</div>
);
return (
<Modal
title={confirmationTitle}
body={confirmationBody}
actions={confirmationActions}
/>
);
}
Selles näites on Modal
väga korduvkasutatav paigutuskomponent. Me spetsialiseerime selle, edastades selle title
, body
ja actions
jaoks spetsiifilisi JSX elemente. See on palju paindlikum kui ConfirmationModal
ja WarningModal
alamklasside loomine. Me lihtsalt komponeerime Modal
erineva sisuga vastavalt vajadusele.
Tehnika 3: Kõrgema järgu komponendid (HOC-d)
Mitte-UI loogika, nagu andmete pärimine, autentimine või logimine, jagamiseks pöördusid Reacti arendajad ajalooliselt mustri poole, mida nimetatakse kõrgema järgu komponentideks (HOC-deks). Kuigi tänapäevases Reactis on need suures osas asendatud Hooksidega, on oluline neid mõista, kuna need esindavad olulist evolutsioonilist sammu Reacti kompositsiooniloos ja eksisteerivad endiselt paljudes koodibaasides.
HOC on funktsioon, mis võtab argumendiks komponendi ja tagastab uue, täiustatud komponendi.
Loome HOC nimega withLogger
, mis logib komponendi prop'id iga kord, kui see uueneb. See on kasulik silumiseks.
// withLogger.js - The HOC
import React, { useEffect } from 'react';
function withLogger(WrappedComponent) {
// It returns a new component...
return function EnhancedComponent(props) {
useEffect(() => {
console.log('Component updated with new props:', props);
}, [props]);
// ... that renders the original component with the original props.
return <WrappedComponent {...props} />;
};
}
// MyComponent.js - A component to be enhanced
function MyComponent({ name, age }) {
return (
<div>
<h1>Hello, {name}!</h1>
<p>You are {age} years old.</p>
</div>
);
}
// Exporting the enhanced component
export default withLogger(MyComponent);
Funktsioon withLogger
mässib MyComponent
'i, andes sellele uued logimisvõimalused ilma MyComponent
'i sisemist koodi muutmata. Me võiksime sama HOC-d rakendada mis tahes teisele komponendile, et anda sellele sama logimisfunktsioon.
HOC-dega seotud väljakutsed:
- Wrapper Hell (Mähkimise põrgu): Mitme HOC-i rakendamine ühele komponendile võib tulemuseks anda sügavalt pesastatud komponendid Reacti arendajatööriistades (nt
withAuth(withRouter(withLogger(MyComponent)))
), mis muudab silumise keeruliseks. - Prop'ide nimede kokkupõrked: Kui HOC süstib prop'i (nt
data
), mida mähitud komponent juba kasutab, võib see kogemata üle kirjutada. - Implitsiitne loogika: Komponendi koodist pole alati selge, kust selle prop'id pärinevad. Loogika on peidetud HOC-i sisse.
Tehnika 4: Render Props
Render Prop muster tekkis lahendusena mõnedele HOC-de puudustele. See pakub selgesõnalisemat viisi loogika jagamiseks.
Render prop'iga komponent võtab prop'ina funktsiooni (tavaliselt nimega `render`) ja kutsub selle funktsiooni välja, et määrata, mida renderdada, andes sellele argumentidena kaasa mis tahes oleku või loogika.
Loome MouseTracker
komponendi, mis jälgib hiire X ja Y koordinaate ning teeb need kättesaadavaks igale komponendile, mis soovib neid kasutada.
// MouseTracker.js - Component with a render prop
import React, { useState, useEffect } from 'react';
function MouseTracker({ render }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleMouseMove = (event) => {
setPosition({ x: event.clientX, y: event.clientY });
};
useEffect(() => {
window.addEventListener('mousemove', handleMouseMove);
return () => {
window.removeEventListener('mousemove', handleMouseMove);
};
}, []);
// Call the render function with the state
return render(position);
}
// App.js - Using the MouseTracker
function App() {
return (
<div>
<h1>Move your mouse around!</h1>
<MouseTracker
render={mousePosition => (
<p>The current mouse position is ({mousePosition.x}, {mousePosition.y})</p>
)}
/>
</div>
);
}
Siin kapseldab MouseTracker
kogu hiire liikumise jälgimise loogika. See ei renderda ise midagi. Selle asemel delegeerib see renderdamisloogika oma render
prop'ile. See on selgesõnalisem kui HOC-d, sest näete otse JSX-i sees, kust mousePosition
andmed pärinevad.
Prop'i children
saab kasutada ka funktsioonina, mis on selle mustri levinud ja elegantne variatsioon:
// Using children as a function
<MouseTracker>
{mousePosition => (
<p>The current mouse position is ({mousePosition.x}, {mousePosition.y})</p>
)}
</MouseTracker>
Tehnika 5: Hookid (Kaasaegne ja eelistatud lähenemine)
React 16.8-s tutvustatud Hookid revolutsioneerisid viisi, kuidas me Reacti komponente kirjutame. Need võimaldavad teil kasutada olekut ja muid Reacti funktsioone funktsionaalsetes komponentides. Kõige tähtsam on see, et kohandatud Hookid pakuvad kõige elegantsemat ja otsesemat lahendust olekuga loogika jagamiseks komponentide vahel.
Hookid lahendavad HOC-de ja Render Props'ide probleemid palju puhtamal viisil. Refaktoorime oma MouseTracker
näite kohandatud hookiks nimega useMousePosition
.
// hooks/useMousePosition.js - A custom Hook
import { useState, useEffect } from 'react';
export function useMousePosition() {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (event) => {
setPosition({ x: event.clientX, y: event.clientY });
};
window.addEventListener('mousemove', handleMouseMove);
return () => {
window.removeEventListener('mousemove', handleMouseMove);
};
}, []); // Empty dependency array means this effect runs only once
return position;
}
// DisplayMousePosition.js - A component using the Hook
import { useMousePosition } from './hooks/useMousePosition';
function DisplayMousePosition() {
const position = useMousePosition(); // Just call the hook!
return (
<p>
The mouse position is ({position.x}, {position.y})
</p>
);
}
// Another component, maybe an interactive element
import { useMousePosition } from './hooks/useMousePosition';
function InteractiveBox() {
const { x, y } = useMousePosition();
const style = {
position: 'absolute',
top: y - 25, // Center the box on the cursor
left: x - 25,
width: '50px',
height: '50px',
backgroundColor: 'lightblue',
};
return <div style={style} />;
}
See on tohutu edasiminek. Pole 'mähkimise põrgut', pole prop'ide nimede kokkupõrkeid ega keerulisi render prop funktsioone. Loogika on täielikult lahti seotud korduvkasutatavasse funktsiooni (useMousePosition
) ja iga komponent saab selle olekuga loogikaga 'haakuda' ühe selge koodireaga. Kohandatud Hookid on kompositsiooni ülim väljendus kaasaegses Reactis, võimaldades teil ehitada oma korduvkasutatavate loogikablokkide teeki.
Kiire võrdlus: Kompositsioon vs. pärilikkus Reactis
Et võtta kokku peamised erinevused Reacti kontekstis, on siin otsene võrdlus:
Aspekt | Pärilikkus (Reacti anti-muster) | Kompositsioon (Eelistatud Reactis) |
---|---|---|
Suhe | 'on-üks' suhe. Spetsialiseeritud komponent on baaskomponendi versioon. | 'omab-üht' või 'kasutab-üht' suhe. Keeruline komponent omab väiksemaid komponente või kasutab jagatud loogikat. |
Sidusus | Kõrge. Lapskomponendid on tihedalt seotud oma vanema implementatsiooniga. | Madal. Komponendid on sõltumatud ja neid saab erinevates kontekstides ilma muudatusteta korduvalt kasutada. |
Paindlikkus | Madal. Jäigad, klassipõhised hierarhiad muudavad loogika jagamise erinevate komponentide puude vahel keeruliseks. | Kõrge. Loogikat ja kasutajaliidest saab kombineerida ja korduvalt kasutada lugematutel viisidel, nagu ehitusplokke. |
Koodi korduvkasutatavus | Piiratud eelnevalt määratletud hierarhiaga. Saate terve "gorilla", kui soovite ainult "banaani". | Suurepärane. Väikeseid, fokusseeritud komponente ja hooke saab kasutada kogu rakenduses. |
Reacti idioom | Ametliku Reacti meeskonna poolt mittesoovitatav. | Soovitatav ja idiomaatiline lähenemine Reacti rakenduste ehitamiseks. |
Kokkuvõte: Mõtle kompositsioonipõhiselt
Debatt kompositsiooni ja pärilikkuse vahel on tarkvaradisaini alustala. Kuigi pärilikkusel on oma koht klassikalises OOP-s, muudab kasutajaliidese arenduse dünaamiline, komponendipõhine olemus selle Reacti jaoks halvasti sobivaks. Teek oli põhimõtteliselt disainitud kompositsiooni omaks võtma.
Eelistades kompositsiooni, saate:
- Paindlikkus: Võimalus segada ja sobitada kasutajaliidest ja loogikat vastavalt vajadusele.
- Hooldatavus: Lõdvalt seotud komponente on lihtsam mõista, testida ja isoleeritult refaktoorida.
- Skaleeritavus: Kompositsiooniline mõtteviis soodustab väikeste, korduvkasutatavate komponentide ja hookide disainisüsteemi loomist, mida saab kasutada suurte ja keerukate rakenduste tõhusaks ehitamiseks.
Ülemaailmse Reacti arendajana ei tähenda kompositsiooni valdamine ainult parimate tavade järgimist – see tähendab põhilise filosoofia mõistmist, mis muudab Reacti nii võimsaks ja produktiivseks tööriistaks. Alustage väikeste, fokusseeritud komponentide loomisest. Kasutage props.children
üldiste konteinerite jaoks ja prop'e spetsialiseerimiseks. Loogika jagamiseks kasutage esmalt kohandatud Hooke. Kompositsioonipõhiselt mõeldes olete heal teel elegantsete, robustsete ja skaleeritavate Reacti rakenduste ehitamiseni, mis peavad ajaproovile vastu.