Kapitel 4. Interaktionsdesign
Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com
In diesem Kapitel schauen wir uns einige Rezepte an, die eine Reihe von typischen Schnittstellenproblemen lösen. Wie gehst du mit Fehlern um? Wie hilfst du den Leuten, dein System zu benutzen? Wie kannst du komplexe Eingabesequenzen erstellen, ohne einen Haufen Spaghetti-Code zu schreiben?
Dies ist eine Sammlung von Tipps, die wir immer wieder als nützlich empfunden haben. Am Ende des Kapitels sehen wir uns verschiedene Möglichkeiten an, wie du Animationen in deine Anwendung einbauen kannst. Wo immer es möglich ist, wählen wir einen Low-Tech-Ansatz. Im Idealfall verleihen die Rezepte deinen Benutzeroberflächen mit einem Minimum an Aufwand mehr Bedeutung.
4.1 Erstellen einer zentralen Fehlerbehandlung
Problem
Es ist schwer, genau zu definieren, was gute Software ausmacht. Aber eines haben die meisten guten Programme gemeinsam: Wie sie auf Fehler und Ausnahmen reagieren. Es wird immer wieder unerwartete Ausnahmesituationen geben, wenn Menschen deinen Code ausführen: Das Netzwerk kann ausfallen, der Server kann abstürzen, die Speicherung kann beschädigt werden. Es ist wichtig, dass du dir überlegst, wie du mit diesen Situationen umgehst, wenn sie auftreten.
Ein Ansatz, der mit ziemlicher Sicherheit fehlschlägt, ist die Tatsache zu ignorieren, dass ein Fehler auftritt, und die blutigen Details des Fehlers zu verschweigen. Irgendwo, irgendwie musst du eine Spur von Beweisen hinterlassen, die du verwenden kannst, um zu verhindern, dass der Fehler erneut auftritt.
Wenn wir Servercode schreiben, können wir die Fehlerdetails protokollieren und eine entsprechende Meldung an eine Anfrage zurückgeben. Wenn wir aber Client-Code schreiben, brauchen wir einen Plan, wie wir mit lokalen Fehlern umgehen wollen. Vielleicht zeigen wir dem Benutzer die Details des Absturzes an und bitten ihn, einen Fehlerbericht einzureichen. Wir könnten einen Drittanbieterdienst wie Sentry.io nutzen, um die Details aus der Ferne zu protokollieren.
Was auch immer unser Code tut, er sollte konsistent sein. Aber wie können wir Ausnahmen in einer React-Anwendung konsistent behandeln?
Lösung
In diesem Rezept sehen wir uns eine Möglichkeit an, einen zentralen Error-Handler zu erstellen. Um es klar zu sagen: Dieser Code fängt nicht automatisch alle Ausnahmen ab. Er muss immer noch explizit zu den JavaScript-Blöcken catch
hinzugefügt werden. Er ist auch kein Ersatz für die Behandlung von Fehlern, von denen wir uns auf andere Weise erholen können. Wenn eine Bestellung fehlschlägt, weil der Server wegen Wartungsarbeiten nicht erreichbar ist, ist es viel besser, den Nutzer zu bitten, es später noch einmal zu versuchen.
Aber diese Technik hilft, Fehler aufzuspüren, die wir vorher nicht eingeplant haben.
Generell gilt: Wenn etwas schief läuft, gibt es drei Dinge, die du dem/der Nutzer/in mitteilen solltest:
-
Was geschah
-
Warum es geschah
-
Was sie dagegen tun sollten
In dem Beispiel, das wir hier zeigen, werden wir Fehler behandeln, indem wir ein Dialogfeld anzeigen, das die Details eines JavaScript Error
Objekts anzeigt und den Benutzer auffordert, den Inhalt per E-Mail an den Systemsupport zu senden. Wir wollen eine einfache Fehlerbehandlungsfunktion, die wir aufrufen können, wenn ein Fehler auftritt:
setVisibleError
(
'Cannot do that thing'
,
errorObject
)
Wenn wir die Funktion in der gesamten Anwendung verfügbar machen wollen, ist der übliche Weg die Verwendung eines Kontexts. Ein Kontext ist eine Art Bereich, den wir um eine Reihe von React-Komponenten wickeln können. Alles, was wir in diesen Kontext einfügen, ist für alle untergeordneten Komponenten verfügbar. Wir werden unseren Kontext verwenden, um die Error-Handler-Funktion zu speichern, die wir im Falle eines Fehlers ausführen können.
Wir nennen unseren Kontext ErrorHandlerContext
:
import
React
from
'react'
const
ErrorHandlerContext
=
React
.
createContext
(()
=>
{})
export
default
ErrorHandlerContext
Damit wir den Kontext für eine Reihe von Komponenten verfügbar machen können, erstellen wir eineErrorHandlerProvider
Komponente, die eine Instanz des Kontexts erstellt und ihn für alle untergeordneten Komponenten verfügbar macht, die wir ihr übergeben:
import
ErrorHandlerContext
from
'./ErrorHandlerContext'
let
setError
=
()
=>
{}
const
ErrorHandlerProvider
=
(
props
)
=>
{
if
(
props
.
callback
)
{
setError
=
props
.
callback
}
return
(
<
ErrorHandlerContext
.
Provider
value
=
{
setError
}>
{
props
.
children
}
</
ErrorHandlerContext
.
Provider
>
)
}
export
default
ErrorHandlerProvider
Jetzt brauchen wir einen Code, der uns sagt, was wir tun sollen, wenn wir die Error-Handler-Funktion aufrufen. In unserem Fall brauchen wir einen Code, der auf eine Fehlermeldung reagiert, indem er ein Dialogfeld mit allen Fehlerdetails anzeigt. Wenn du Fehler anders behandeln willst, musst du diesen Code ändern:
import
{
useCallback
,
useState
}
from
'react'
import
ErrorHandlerProvider
from
'./ErrorHandlerProvider'
import
ErrorDialog
from
'./ErrorDialog'
const
ErrorContainer
=
(
props
)
=>
{
const
[
error
,
setError
]
=
useState
()
const
[
errorTitle
,
setErrorTitle
]
=
useState
()
const
[
action
,
setAction
]
=
useState
()
if
(
error
)
{
console
.
error
(
'An error has been thrown'
,
errorTitle
,
JSON
.
stringify
(
error
)
)
}
const
callback
=
useCallback
((
title
,
err
,
action
)
=>
{
console
.
error
(
'ERROR RAISED '
)
console
.
error
(
'Error title: '
,
title
)
console
.
error
(
'Error content'
,
JSON
.
stringify
(
err
))
setError
(
err
)
setErrorTitle
(
title
)
setAction
(
action
)
},
[])
return
(
<
ErrorHandlerProvider
callback
=
{
callback
}>
{
props
.
children
}
{
error
&&
(
<
ErrorDialog
title
=
{
errorTitle
}
onClose
=
{()
=>
{
setError
(
null
)
setErrorTitle
(
''
)
}}
action
=
{
action
}
error
=
{
error
}
/>
)}
</
ErrorHandlerProvider
>
)
}
export
default
ErrorContainer
ErrorContainer
zeigt die Details mit Hilfe von ErrorDialog
an. Wir werden hier nicht auf die Details des Codes für ErrorDialog
eingehen, da dies der Code ist, den du höchstwahrscheinlich durch deine Implementierung ersetzen wirst.1
Wir müssen den größten Teil unserer Anwendung in eine ErrorContainer
verpacken. Alle Komponenten innerhalb der ErrorContainer
können den Error Handler aufrufen:
import
'./App.css'
import
ErrorContainer
from
'./ErrorContainer'
import
ClockIn
from
'./ClockIn'
function
App
()
{
return
(
<
div
className
=
"App"
>
<
ErrorContainer
>
<
ClockIn
/>
</
ErrorContainer
>
</
div
>
)
}
export
default
App
Wie verwendet eine Komponente den Error-Handler? Wir erstellen einen benutzerdefinierten Hook namens useErrorHandler()
, der die Error-Handler-Funktion aus dem Kontext holt und sie zurückgibt:
import
ErrorHandlerContext
from
'./ErrorHandlerContext'
import
{
useContext
}
from
'react'
const
useErrorHandler
=
()
=>
useContext
(
ErrorHandlerContext
)
export
default
useErrorHandler
Das ist ein ziemlich komplexer Code, aber jetzt kommen wir zur Verwendung des Error-Handlers; er ist sehr einfach. Dieser Beispielcode stellt eine Netzwerkanfrage, wenn ein Nutzer auf eine Schaltfläche klickt. Wenn die Netzwerkanfrage fehlschlägt, werden die Details des Fehlers an den Error-Handler weitergegeben:
import
useErrorHandler
from
'./useErrorHandler'
import
axios
from
'axios'
const
ClockIn
=
()
=>
{
const
setVisibleError
=
useErrorHandler
()
const
doClockIn
=
async
()
=>
{
try
{
await
axios
.
put
(
'/clockTime'
)
}
catch
(
err
)
{
setVisibleError
(
'Unable to record work start time'
,
err
)
}
}
return
(
<>
<
h1
>
Click
Button
to
Record
Start
Time
</
h1
>
<
button
onClick
=
{
doClockIn
}>
Start
work
</
button
>
</>
)
}
export
default
ClockIn
In Abbildung 4-1 kannst du sehen, wie die App aussieht.
Wenn du auf die Schaltfläche klickst, schlägt die Netzwerkanfrage fehl, weil der Servercode nicht existiert. Abbildung 4-2 zeigt den Fehlerdialog, der erscheint. Er zeigt an, was schief gelaufen ist, warum es schief gelaufen ist und was der Benutzer tun sollte.
Diskussion
Von allen Rezepten, die wir im Laufe der Jahre entwickelt haben, hat dieses am meisten Zeit gespart. Während der Entwicklung kommt es häufig zu Codefehlern, und wenn der einzige Hinweis auf einen Fehler ein in der JavaScript-Konsole versteckter Stack-Trace ist, verpasst du ihn wahrscheinlich.
Wenn ein Teil der Infrastruktur (Netzwerke, Gateways, Server, Datenbanken) fehlschlägt, kann dir dieser kleine Code unzählige Stunden ersparen, um die Ursache zu finden.
Du kannst den Quellcode für dieses Rezept von der GitHub-Seite herunterladen.
4.2 Eine interaktive Hilfe erstellen
Problem
Tim Berners-Lee hat das Web absichtlich so gestaltet, dass es nur wenige Funktionen hat. Es verfügt über ein einfaches Protokoll (HTTP) und hatte ursprünglich eine einfache Auszeichnungssprache (HTML). Der Mangel an Komplexität bedeutete, dass neue Nutzer/innen von Websites sofort wussten, wie sie zu benutzen waren. Wenn du etwas sahst, das wie ein Hyperlink aussah, konntest du darauf klicken und zu einer anderen Seite gehen.
Aber Rich-JavaScript-Anwendungen haben das alles verändert. Webanwendungen sind nicht mehr nur eine Ansammlung von Webseiten mit Hyperlinks, sondern sie ähneln den alten Desktop-Anwendungen; sie sind leistungsfähiger und funktionsreicher, aber die Kehrseite ist, dass sie jetzt viel komplexer zu bedienen sind.
Wie kannst du einen interaktiven Leitfaden in deine Anwendung einbauen?
Lösung
Wir werden ein einfaches Hilfesystem erstellen, das du über eine bestehende Anwendung legen kannst. Wenn der Nutzer die Hilfe öffnet, sieht er eine Reihe von Pop-up-Hinweisen, die beschreiben, wie er die verschiedenen Funktionen auf der Seite nutzen kann (siehe Abbildung 4-3).
Wir wollen etwas, das einfach zu pflegen ist und nur für sichtbare Komponenten Hilfe bietet. Das hört sich nach einer ziemlich großen Aufgabe an, also fangen wir damit an, eine Komponente zu konstruieren, die eine Pop-up-Hilfe anzeigt:
import
{
Popper
}
from
'@material-ui/core'
import
'./HelpBubble.css'
const
HelpBubble
=
(
props
)
=>
{
const
element
=
props
.
forElement
?
document
.
querySelector
(
props
.
forElement
)
:
null
return
element
?
(
<
Popper
className
=
"HelpBubble-container"
open
=
{
props
.
open
}
anchorEl
=
{
element
}
placement
=
{
props
.
placement
||
'bottom-start'
}
>
<
div
className
=
"HelpBubble-close"
onClick
=
{
props
.
onClose
}>
Close
[
X
]
</
div
>
{
props
.
content
}
<
div
className
=
"HelpBubble-controls"
>
{
props
.
previousLabel
?
(
<
div
className
=
"HelpBubble-control HelpBubble-previous"
onClick
=
{
props
.
onPrevious
}
>
&
lt
;
{
props
.
previousLabel
}
</
div
>
)
:
(
<
div
>
&
nbsp
;</
div
>
)}
{
props
.
nextLabel
?
(
<
div
className
=
"HelpBubble-control HelpBubble-next"
onClick
=
{
props
.
onNext
}
>
{
props
.
nextLabel
}
&
gt
;
</
div
>
)
:
(
<
div
>
&
nbsp
;</
div
>
)}
</
div
>
</
Popper
>
)
:
null
}
export
default
HelpBubble
Wir verwenden die Komponente Popper
aus der Bibliothek @material-ui
. Die Komponente Popper
kann auf der Seite verankert werden, neben einer anderen Komponente. Unser HelpBubble
nimmt einen forElement
String entgegen, der einen CSS-Selektor darstellt, wie z. B..class-name
oder #some-id
. Wir werden Selektoren verwenden, um Dinge auf dem Bildschirm mit Pop-up-Meldungen zu verknüpfen.
Da wir nun eine Komponente für Pop-up-Nachrichten haben, brauchen wir etwas, das eine Folge von HelpBubbles
koordiniert. Wir nennen dasHelpSequence
:
import
{
useEffect
,
useState
}
from
'react'
import
HelpBubble
from
'./HelpBubble'
function
isVisible
(
e
)
{
return
!!
(
e
.
offsetWidth
||
e
.
offsetHeight
||
e
.
getClientRects
().
length
)
}
const
HelpSequence
=
(
props
)
=>
{
const
[
position
,
setPosition
]
=
useState
(
0
)
const
[
sequence
,
setSequence
]
=
useState
()
useEffect
(()
=>
{
if
(
props
.
sequence
)
{
const
filter
=
props
.
sequence
.
filter
((
i
)
=>
{
if
(
!
i
.
forElement
)
{
return
false
}
const
element
=
document
.
querySelector
(
i
.
forElement
)
if
(
!
element
)
{
return
false
}
return
isVisible
(
element
)
})
setSequence
(
filter
)
}
else
{
setSequence
(
null
)
}
},
[
props
.
sequence
,
props
.
open
])
const
data
=
sequence
&&
sequence
[
position
]
useEffect
(()
=>
{
setPosition
(
0
)
},
[
props
.
open
])
const
onNext
=
()
=>
setPosition
((
p
)
=>
{
if
(
p
===
sequence
.
length
-
1
)
{
props
.
onClose
&&
props
.
onClose
()
}
return
p
+
1
})
const
onPrevious
=
()
=>
setPosition
((
p
)
=>
{
if
(
p
===
0
)
{
props
.
onClose
&&
props
.
onClose
()
}
return
p
-
1
})
return
(
<
div
className
=
"HelpSequence-container"
>
{
data
&&
(
<
HelpBubble
open
=
{
props
.
open
}
forElement
=
{
data
.
forElement
}
placement
=
{
data
.
placement
}
onClose
=
{
props
.
onClose
}
previousLabel
=
{
position
>
0
&&
'Previous'
}
nextLabel
=
{
position
<
sequence
.
length
-
1
?
'Next'
:
'Finish'
}
onPrevious
=
{
onPrevious
}
onNext
=
{
onNext
}
content
=
{
data
.
text
}
/>
)}
</
div
>
)
}
export
default
HelpSequence
Die HelpSequence
nimmt ein Array von JavaScript-Objekten wie dieses:
[
{
forElement
:
"p"
,
text
:
"This is some introductory text telling you how to start"
},
{
forElement
:
".App-link"
,
text
:
"This will show you how to use React"
},
{
forElement
:
".App-nowhere"
,
text
:
"This help text will never appear"
},
]
und wandelt sie in eine dynamische Sequenz von HelpBubbles
um. HelpBubble
wird nur angezeigt, wenn ein Element gefunden wird, das dem Selektor forElement
entspricht. Dann platziert es das HelpBubble
neben dem Element und zeigt den Hilfetext an.
Fügen wir eine HelpSequence
zum Standard App.js Code hinzu, der von create-react-app
generiert wurde:
import
{
useState
}
from
'react'
import
logo
from
'./logo.svg'
import
HelpSequence
from
'./HelpSequence'
import
'./App.css'
function
App
()
{
const
[
showHelp
,
setShowHelp
]
=
useState
(
false
)
return
(
<
div
className
=
"App"
>
<
header
className
=
"App-header"
>
<
img
src
=
{
logo
}
className
=
"App-logo"
alt
=
"logo"
/>
<
p
>
Edit
<
code
>
src
/
App
.
js
</
code
>
and
save
to
reload
.
</
p
>
<
a
className
=
"App-link"
href
=
"https://reactjs.org"
target
=
"_blank"
rel
=
"noopener noreferrer"
>
Learn
React
</
a
>
</
header
>
<
button
onClick
=
{()
=>
setShowHelp
(
true
)}>
Show
help
</
button
>
<
HelpSequence
sequence
=
{[
{
forElement
:
'p'
,
text
:
'This is some introductory text telling you how to start'
,
},
{
forElement
:
'.App-link'
,
text
:
'This will show you how to use React'
,
},
{
forElement
:
'.App-nowhere'
,
text
:
'This help text will never appear'
,
},
]}
open
=
{
showHelp
}
onClose
=
{()
=>
setShowHelp
(
false
)}
/>
</
div
>
)
}
export
default
App
Zunächst einmal sehen wir nichts anderes als eine Hilfe-Schaltfläche (siehe Abbildung 4-4).
Wenn der Benutzer auf die Schaltfläche "Hilfe" klickt, wird das erste Hilfethema angezeigt, wie in Abbildung 4-5 dargestellt.
Abbildung 4-6 zeigt, wie sich die Hilfe zum nächsten Element bewegt, wenn der Nutzer auf Weiter klickt. Der Nutzer kann sich so lange von Element zu Element bewegen, bis keine passenden Elemente mehr sichtbar sind.
Diskussion
Wenn du deiner Anwendung eine interaktive Hilfe hinzufügst, wird deine Benutzeroberfläche auffindbar. Entwicklerinnen und Entwickler verbringen einen Großteil ihrer Zeit damit, Funktionen zu Anwendungen hinzuzufügen, die die Menschen vielleicht nie nutzen, weil sie nicht wissen, dass sie da sind.
Die Implementierung in diesem Rezept zeigt die Hilfe als einfachen Text an. Du könntest in Erwägung ziehen, Markdown zu verwenden, da dies eine umfangreichere Darstellung ermöglicht und die Hilfethemen dann Links zu anderen, umfangreicheren Hilfeseiten enthalten können.2
Die Hilfethemen werden automatisch auf die Elemente beschränkt, die auf der Seite sichtbar sind. Du kannst entweder für jede Seite eine eigene Hilfesequenz erstellen oder eine einzige große Hilfesequenz, die sich automatisch an die aktuelle Ansicht der Benutzeroberfläche anpasst.
Schließlich eignet sich ein solches Hilfesystem ideal für die Speicherung in einem Headless CMS, das es dir ermöglicht, die Hilfe dynamisch zu aktualisieren, ohne jedes Mal eine neue Bereitstellung erstellen zu müssen.
Du kannst den Quellcode für dieses Rezept von der GitHub-Seite herunterladen.
4.3 Reducers für komplexe Interaktionen verwenden
Problem
Bei Anwendungen müssen die Benutzer häufig eine Reihe von Aktionen ausführen. Vielleicht müssen sie die Schritte eines Assistenten ausführen oder sich anmelden und einen gefährlichen Vorgang bestätigen (siehe Abbildung 4-7).
Der/die Nutzer/in muss nicht nur eine Reihe von Schritten ausführen, sondern diese Schritte können auch an Bedingungen geknüpft sein. Wenn der Nutzer sich kürzlich angemeldet hat, muss er sich vielleicht nicht erneut anmelden. Es kann sein, dass er auf halbem Weg abbrechen möchte.
Wenn du die komplexen Abläufe innerhalb deiner Komponenten modellierst, kannst du bald feststellen, dass deine Anwendung voller Spaghetti-Code ist.
Lösung
Wir werden einen Reducer verwenden, um eine komplexe Abfolge von Operationen zu verwalten. In Kapitel 3 haben wir Reducer für die Verwaltung von Zuständen eingeführt. EinReducer ist eine Funktion, die ein Zustandsobjekt und eine Aktion annimmt. Der Reducer verwendet die Aktion, um zu entscheiden, wie der Zustand geändert werden soll, und darf keine Nebeneffekte haben.
Da Reduzierer keinen Code für die Benutzeroberfläche haben, sind sie perfekt geeignet, um komplizierte, zusammenhängende Zustände zu verwalten, ohne sich um das Aussehen zu kümmern. Sie eignen sich besonders gut für Unit-Tests.
Nehmen wir zum Beispiel an, wir implementieren die zu Beginn dieses Rezepts erwähnte Löschsequenz. Wir können im klassischen testgetriebenen Stil beginnen, indem wir einen Unit-Test schreiben:
import
deletionReducer
from
'./deletionReducer'
describe
(
'deletionReducer'
,
()
=>
{
it
(
'should show the login dialog if we are not logged in'
,
()
=>
{
const
actual
=
deletionReducer
({},
{
type
:
'START_DELETION'
})
expect
(
actual
.
showLogin
).
toBe
(
true
)
expect
(
actual
.
message
).
toBe
(
''
)
expect
(
actual
.
deleteButtonDisabled
).
toBe
(
true
)
expect
(
actual
.
loginError
).
toBe
(
''
)
expect
(
actual
.
showConfirmation
).
toBe
(
false
)
})
})
Hier heißt unsere Reduzierfunktion deletionReducer
. Wir übergeben ihr ein leeres Objekt ({}
) und eine Aktion, die besagt, dass wir den Löschvorgang starten wollen ({type: 'START_DELETION'}
). Dann sagen wir, dass wir erwarten, dass die neue Version des Zustands einenshowLogin
Wert von true
, einen showConfirmation
Wert von false
und so weiter hat.
Wir können dann den Code für einen Reducer implementieren, der genau das tut:
function
deletionReducer
(
state
,
action
)
{
switch
(
action
.
type
)
{
case
'START_DELETION'
:
return
{
...
state
,
showLogin
:
true
,
message
:
''
,
deleteButtonDisabled
:
true
,
loginError
:
''
,
showConfirmation
:
false
,
}
default
:
return
null
// Or anything
}
}
Zunächst setzen wir die Statusattribute nur auf Werte, die den Test bestehen. Wenn wir mehr und mehr Tests hinzufügen, verbessert sich unser Reducer, da er mehr Situationen bewältigen kann.
Am Ende erhalten wir etwas, das so aussieht:3
function
deletionReducer
(
state
,
action
)
{
switch
(
action
.
type
)
{
case
'START_DELETION'
:
return
{
...
state
,
showLogin
:
!
state
.
loggedIn
,
message
:
''
,
deleteButtonDisabled
:
true
,
loginError
:
''
,
showConfirmation
:
!!
state
.
loggedIn
,
}
case
'CANCEL_DELETION'
:
return
{
...
state
,
showLogin
:
false
,
showConfirmation
:
false
,
showResult
:
false
,
message
:
'Deletion canceled'
,
deleteButtonDisabled
:
false
,
}
case
'LOGIN'
:
const
passwordCorrect
=
action
.
payload
===
'swordfish'
return
{
...
state
,
showLogin
:
!
passwordCorrect
,
showConfirmation
:
passwordCorrect
,
loginError
:
passwordCorrect
?
''
:
'Invalid password'
,
loggedIn
:
true
,
}
case
'CONFIRM_DELETION'
:
return
{
...
state
,
showConfirmation
:
false
,
showResult
:
true
,
message
:
'Widget deleted'
,
}
case
'FINISH'
:
return
{
...
state
,
showLogin
:
false
,
showConfirmation
:
false
,
showResult
:
false
,
deleteButtonDisabled
:
false
,
}
default
:
throw
new
Error
(
'Unknown action: '
+
action
.
type
)
}
}
export
default
deletionReducer
Obwohl dieser Code kompliziert ist, kannst du ihn schnell schreiben, wenn du zuerst die Tests erstellst.
Jetzt, wo wir den Reducer haben, können wir ihn in unserer Anwendung verwenden:
import
{
useReducer
,
useState
}
from
'react'
import
'./App.css'
import
deletionReducer
from
'./deletionReducer'
function
App
()
{
const
[
state
,
dispatch
]
=
useReducer
(
deletionReducer
,
{})
const
[
password
,
setPassword
]
=
useState
()
return
(
<
div
className
=
"App"
>
<
button
onClick
=
{()
=>
{
dispatch
({
type
:
'START_DELETION'
})
}}
disabled
=
{
state
.
deleteButtonDisabled
}
>
Delete
Widget
!
</
button
>
<
div
className
=
"App-message"
>{
state
.
message
}</
div
>
{
state
.
showLogin
&&
(
<
div
className
=
"App-dialog"
>
<
p
>
Enter
your
password
</
p
>
<
input
type
=
"password"
value
=
{
password
}
onChange
=
{(
evt
)
=>
setPassword
(
evt
.
target
.
value
)}
/>
<
button
onClick
=
{()
=>
dispatch
({
type
:
'LOGIN'
,
payload
:
password
})
}
>
Login
</
button
>
<
button
onClick
=
{()
=>
dispatch
({
type
:
'CANCEL_DELETION'
})}
>
Cancel
</
button
>
<
div
className
=
"App-error"
>{
state
.
loginError
}</
div
>
</
div
>
)}
{
state
.
showConfirmation
&&
(
<
div
className
=
"App-dialog"
>
<
p
>
Are
you
sure
you
want
to
delete
the
widget
?
</
p
>
<
button
onClick
=
{()
=>
dispatch
({
type
:
'CONFIRM_DELETION'
,
})
}
>
Yes
</
button
>
<
button
onClick
=
{()
=>
dispatch
({
type
:
'CANCEL_DELETION'
,
})
}
>
No
</
button
>
</
div
>
)}
{
state
.
showResult
&&
(
<
div
className
=
"App-dialog"
>
<
p
>
The
widget
was
deleted
</
p
>
<
button
onClick
=
{()
=>
dispatch
({
type
:
'FINISH'
,
})
}
>
Done
</
button
>
</
div
>
)}
</
div
>
)
}
export
default
App
Der größte Teil dieses Codes besteht darin, die Benutzeroberfläche für die einzelnen Dialoge in der Sequenz zu erstellen. Es gibt praktisch keine Logik in dieser Komponente. Sie tut einfach, was der Reducer ihr sagt. Sie führt den Benutzer durch den glücklichen Weg der Anmeldung und der Bestätigung der Löschung (siehe Abbildung 4-8).
Aber Abbildung 4-9 zeigt, dass es auch alle Kanten behandelt, wie z. B. ungültige Passwörter und Stornierungen.
Diskussion
Es gibt Fälle, in denen Reducer deinen Code unübersichtlich machen können. Wenn du nur wenige Zustandsgrößen mit wenigen Interaktionen zwischen ihnen hast, brauchst du wahrscheinlich keinen Reducer. Wenn du aber ein Flussdiagramm oder ein Zustandsdiagramm zeichnest, um eine Abfolge von Benutzerinteraktionen zu beschreiben, ist das ein Zeichen dafür, dass du vielleicht einen Reducer brauchst.
Du kannst den Quellcode für dieses Rezept von der GitHub-Seite herunterladen.
4.4 Tastaturinteraktion hinzufügen
Lösung
Wir werden einen Key-Listener-Hook erstellen, um auf keydown
Ereignisse auf der Ebene document
zu hören. Er kann aber auch leicht geändert werden, um auf jedes andere JavaScript-Ereignis im DOM zu warten. Das ist der Hook:
import
{
useEffect
}
from
'react'
const
useKeyListener
=
(
callback
)
=>
{
useEffect
(()
=>
{
const
listener
=
(
e
)
=>
{
e
=
e
||
window
.
event
const
tagName
=
e
.
target
.
localName
||
e
.
target
.
tagName
// Only accept key-events that originated at the body level
// to avoid key-strokes in e.g. text-fields being included
if
(
tagName
.
toUpperCase
()
===
'BODY'
)
{
callback
(
e
)
}
}
document
.
addEventListener
(
'keydown'
,
listener
,
true
)
return
()
=>
{
document
.
removeEventListener
(
'keydown'
,
listener
,
true
)
}
},
[
callback
])
}
export
default
useKeyListener
Der Hook nimmt eine Callback-Funktion an und registriert sie für keydown
Ereignisse auf demdocument
Objekt. Am Ende der useEffect
gibt er eine Funktion zurück, die die Registrierung des Rückrufs aufhebt. Wenn sich die übergebene Callback-Funktion ändert, heben wir zuerst die Registrierung der alten Funktion auf, bevor wir die neue Funktion registrieren.
Wie verwenden wir den Haken? Hier ist ein Beispiel. Schau mal, ob du den kleinen Programmierfehler bemerkst, mit dem wir umgehen müssen:
import
{
useCallback
,
useState
}
from
'react'
import
'./App.css'
import
useKeyListener
from
'./useKeyListener'
const
RIGHT_ARROW
=
39
const
LEFT_ARROW
=
37
const
ESCAPE
=
27
function
App
()
{
const
[
angle
,
setAngle
]
=
useState
(
0
)
const
[
lastKey
,
setLastKey
]
=
useState
(
''
)
let
onKeyDown
=
useCallback
(
(
evt
)
=>
{
if
(
evt
.
keyCode
===
LEFT_ARROW
)
{
setAngle
((
c
)
=>
Math
.
max
(
-
360
,
c
-
10
))
setLastKey
(
'Left'
)
}
else
if
(
evt
.
keyCode
===
RIGHT_ARROW
)
{
setAngle
((
c
)
=>
Math
.
min
(
360
,
c
+
10
))
setLastKey
(
'Right'
)
}
else
if
(
evt
.
keyCode
===
ESCAPE
)
{
setAngle
(
0
)
setLastKey
(
'Escape'
)
}
},
[
setAngle
]
)
useKeyListener
(
onKeyDown
)
return
(
<
div
className
=
"App"
>
<
p
>
Angle
:
{
angle
}
Last
key
:
{
lastKey
}
</
p
>
<
svg
width
=
"400px"
height
=
"400px"
title
=
"arrow"
fill
=
"none"
strokeWidth
=
"10"
stroke
=
"black"
style
=
{{
transform
:
`rotate(
${
angle
}
deg)`
,
}}
>
<
polyline
points
=
"100,200 200,0 300,200"
/>
<
polyline
points
=
"200,0 200,400"
/>
</
svg
>
</
div
>
)
}
export
default
App
Dieser Code wartet darauf, dass der Benutzer die Cursortasten links/rechts drückt. Unsere Funktion onKeyDown
sagt, was passieren soll, wenn diese Tasten gedrückt werden, aber beachte, dass wir sie in eine useCallback
verpackt haben. Wenn wir das nicht tun würden, würde der Browser die FunktiononKeyDown
jedes Mal neu erstellen, wenn er die Komponente App
rendert. Die neue Funktion würde dasselbe tun wie die alte onKeyDown
Funktion, aber sie würde an einer anderen Stelle im Speicher liegen und useKeyListener
würde sie immer wieder abmelden und neu anmelden.
Warnung
Wenn du vergisst, deine Callback-Funktion in ein useCallback
zu verpacken, kann das zu einer Flut von Render-Aufrufen führen, die deine Anwendung verlangsamen.
Durch die Verwendung von useCallback
können wir sicherstellen, dass wir die Funktion nur erstellen, wenn sich setAngle
ändert.
Wenn du die Anwendung startest, siehst du einen Pfeil auf dem Bildschirm. Wenn du die Cursortasten links/rechts drückst(Abbildung 4-10), kannst du das Bild drehen. Wenn du die Escape-Taste drückst, kannst du es wieder in die Vertikale bringen.
Diskussion
In der Funktion useKeyListener
achten wir darauf, dass wir nur auf Ereignisse hören, die auf der Ebene body
ausgelöst wurden. Wenn der Benutzer auf die Pfeiltasten in einem Textfeld klickt, wird der Browser diese Ereignisse nicht an deinen Code senden.
Du kannst den Quellcode für dieses Rezept von der GitHub-Seite herunterladen.
4.5 Markdown für umfangreiche Inhalte verwenden
Problem
Wenn deine Anwendung es den Nutzern erlaubt, große Textblöcke einzugeben, wäre es hilfreich, wenn dieser Inhalt auch formatierten Text, Links und so weiter enthalten könnte. Wenn du es den Nutzern jedoch erlaubst, solche schrecklichen Inhalte wie rohes HTML einzugeben, kann das zu Sicherheitslücken und unsäglichem Ärger für die Entwickler führen.
Wie kannst du Nutzern erlauben, umfangreiche Inhalte zu posten, ohne die Sicherheit deiner Anwendung zu untergraben?
Lösung
Markdown ist eine hervorragende Methode, um Nutzern die Möglichkeit zu geben, umfangreiche Inhalte sicher in deine Anwendung zu posten. Um zu sehen, wie du Markdown in deiner Anwendung nutzen kannst, schauen wir uns diese einfache Anwendung an, in der ein Nutzer eine Reihe von Nachrichten mit Zeitstempel in eine Liste posten kann:
import
{
useState
}
from
'react'
import
'./Forum.css'
const
Forum
=
()
=>
{
const
[
text
,
setText
]
=
useState
(
''
)
const
[
messages
,
setMessages
]
=
useState
([])
return
(
<
section
className
=
"Forum"
>
<
textarea
cols
=
{
80
}
rows
=
{
20
}
value
=
{
text
}
onChange
=
{(
evt
)
=>
setText
(
evt
.
target
.
value
)}
/>
<
button
onClick
=
{()
=>
{
setMessages
((
msgs
)
=>
[
{
body
:
text
,
timestamp
:
new
Date
().
toISOString
(),
},
...
msgs
,
])
setText
(
''
)
}}
>
Post
</
button
>
{
messages
.
map
((
msg
)
=>
{
return
(
<
dl
>
<
dt
>{
msg
.
timestamp
}</
dt
>
<
dd
>{
msg
.
body
}</
dd
>
</
dl
>
)
})}
</
section
>
)
}
export
default
Forum
Wenn du die Anwendung startest(Abbildung 4-11), siehst du einen großen Textbereich. Wenn du eine Nachricht im Klartext schreibst, behält die App den Leerraum und die Zeilenumbrüche bei.
Wenn deine Anwendung einen Textbereich enthält, ist es eine Überlegung wert, dem Benutzer die Eingabe von Markdown-Inhalten zu ermöglichen.
Es gibt viele, viele Markdown-Bibliotheken, aber die meisten von ihnen sind Wrapper für react-markdown
oder einen Syntax-Highlighter wie PrismJS oder CodeMirror.
Wir schauen uns eine Bibliothek namens react-md-editor
an, die react-markdown
zusätzliche Funktionen hinzufügt und es dir ermöglicht, Markdown anzuzeigen und zu bearbeiten. Wir beginnen damit, die Bibliothek zu installieren:
$
npminstall
@uiw/react-md-editor
Jetzt wandeln wir unseren Nur-Text-Bereich in einen Markdown-Editor um und konvertieren die geposteten Nachrichten von Markdown in HTML:
import
{
useState
}
from
'react'
import
MDEditor
from
'@uiw/react-md-editor'
const
MarkdownForum
=
()
=>
{
const
[
text
,
setText
]
=
useState
(
''
)
const
[
messages
,
setMessages
]
=
useState
([])
return
(
<
section
className
=
"Forum"
>
<
MDEditor
height
=
{
300
}
value
=
{
text
}
onChange
=
{
setText
}
/>
<
button
onClick
=
{()
=>
{
setMessages
((
msgs
)
=>
[
{
body
:
text
,
timestamp
:
new
Date
().
toISOString
(),
},
...
msgs
,
])
setText
(
''
)
}}
>
Post
</
button
>
{
messages
.
map
((
msg
)
=>
{
return
(
<
dl
>
<
dt
>{
msg
.
timestamp
}</
dt
>
<
dd
>
<
MDEditor
.
Markdown
source
=
{
msg
.
body
}
/>
</
dd
>
</
dl
>
)
})}
</
section
>
)
}
export
default
MarkdownForum
Die Umwandlung von einfachem Text in Markdown ist eine kleine Änderung mit einem großen Nutzen. Wie du in Abbildung 4-12 sehen kannst, kann der Nutzer eine Nachricht reichhaltig formatieren und sie vor dem Posten im Vollbildmodus bearbeiten.
Diskussion
Das Hinzufügen von Markdown zu einer Anwendung geht schnell und verbessert die Nutzererfahrung mit minimalem Aufwand. Weitere Details zu Markdown findest du im Original-Leitfaden von John Gruber.
Du kannst den Quellcode für dieses Rezept von der GitHub-Seite herunterladen.
4.6 Animieren mit CSS-Klassen
Lösung
Für die meisten Animationen, die du in einer React-Anwendung benötigen wirst, brauchst du wahrscheinlich keine Animationsbibliothek eines Drittanbieters. Das liegt daran, dass CSS-Animationen den Browsern jetzt die Möglichkeit geben, CSS-Eigenschaften mit minimalem Aufwand zu animieren. Es wird nur sehr wenig Code benötigt, und die Animation ist flüssig, weil die Grafikhardware sie erzeugt. Die GPU-Animation verbraucht weniger Strom und ist daher besser für mobileGeräte geeignet.
Tipp
Wenn du deine React-Anwendung mit Animationen ausstatten möchtest, solltest du mit CSS-Animationen beginnen, bevor du dich anderweitig umsiehst.
Wie funktioniert die CSS-Animation? Sie verwendet eine CSS-Eigenschaft namens transition
. Nehmen wir an, wir wollen ein ausklappbares Informationsfeld erstellen. Wenn der Nutzer auf die Schaltfläche klickt, öffnet sich das Panel sanft. Wenn der Nutzer erneut auf die Schaltfläche klickt, wird sie sanft geschlossen, wie in Abbildung 4-13 gezeigt.
Wir können diesen Effekt mit der CSS-Eigenschaft transition
erzeugen:
.InfoPanel-details
{
height
:
350px
;
transition
:
height
0.5s
;
}
Dieses CSS legt eine height
sowie eine transition
Eigenschaft fest. Diese Kombination bedeutet: "Egal, wie hoch du gerade bist, animiere dich in der nächsten halben Sekunde auf meine bevorzugte Höhe."
Die Animation erfolgt immer dann, wenn sich die height
des Elements ändert, z. B. wenn eine zusätzliche CSS-Regel gültig wird. Wenn wir zum Beispiel einen zusätzlichen CSS-Klassennamen mit einer anderen Höhe haben, wird die Übergangseigenschaft die Höhenänderung animieren, wenn ein Element zu einer anderen Klasse wechselt:
.InfoPanel-details
{
height
:
350px
;
transition
:
height
0.5s
;
}
.InfoPanel-details.InfoPanel-details-closed
{
height
:
0
;
}
Tipp
Diese Klassennamenstruktur ist ein Beispiel für die Block-Element-Modifikator (BEM)-Benennung. Der Block ist die Komponente (InfoPanel
), dasElement ist eine Sache innerhalb des Blocks (details
), und der Modifikator sagt etwas über den aktuellen Zustand des Elements aus (closed
).Die BEM-Konvention verringert die Wahrscheinlichkeit von Namenskonflikten in deinem Code.
Wenn ein InfoPanel-details
Element plötzlich eine zusätzliche .InfoPanel-details-closed
Klasse erhält, ändert sich die height
von350px
zu 0
, und die transition
Eigenschaft lässt das Element sanft schrumpfen. Verliert die Komponente dagegen die Klasse.InfoPanel-details-closed
, wird das Element wieder größer.
Das bedeutet, dass wir die harte Arbeit auf CSS verschieben können und alles, was wir in unserem React-Code tun müssen, ist, die Klasse zu einem Element hinzuzufügen oder zu entfernen:
import
{
useState
}
from
'react'
import
'./InfoPanel.css'
const
InfoPanel
=
({
title
,
children
})
=>
{
const
[
open
,
setOpen
]
=
useState
(
false
)
return
(
<
section
className
=
"InfoPanel"
>
<
h1
>
{
title
}
<
button
onClick
=
{()
=>
setOpen
((
v
)
=>
!
v
)}>
{
open
?
'^'
:
'v'
}
</
button
>
</
h1
>
<
div
className
=
{
`InfoPanel-details
${
open
?
''
:
'InfoPanel-details-closed'
}
`
}
>
{
children
}
</
div
>
</
section
>
)
}
export
default
InfoPanel
Diskussion
Wir haben oft gesehen, dass viele Projekte Komponentenbibliotheken von Drittanbietern einbinden, um ein kleines Widget zu verwenden, das seinen Inhalt vergrößert oder verkleinert. Wie du siehst, ist es ganz einfach, eine solche Animation einzubinden.
Du kannst den Quellcode für dieses Rezept von der GitHub-Seite herunterladen.
4.7 Animieren mit React Animation
Problem
CSS-Animationen sind technisch sehr einfach und eignen sich für die meisten Animationen, die du wahrscheinlich brauchst.
Allerdings musst du dafür eine Menge über die verschiedenen CSS-Eigenschaften und die Auswirkungen ihrer Animation wissen. Wie stellst du dar, dass ein Gegenstand gelöscht wird, indem er sich schnell ausdehnt und transparent wird?
Bibliotheken wie Animate.css enthalten eine ganze Reihe vorgefertigter CSS-Animationen, aber sie erfordern oft fortgeschrittenere CSS-Animationskonzepte wie Keyframes und sind nicht besonders auf React abgestimmt. Wie können wir Animationen aus CSS-Bibliotheken in eine React-Anwendung einbauen?
Lösung
Die React Animations-Bibliothek ist ein React-Wrapper für die Animate.css-Bibliothek. Sie fügt effizient animiertes Styling zu deinen Komponenten hinzu, ohne unnötige Renderings zu erzeugen oder die Größe des generierten DOM deutlich zu erhöhen.
Das funktioniert so effizient, weil React Animations mit einer CSS-in-JS-Bibliothek arbeitet. CSS-in-JS ist eine Technik, mit der du deine Stilinformationen direkt in deinem JavaScript-Code kodieren kannst. Mit React kannst du deine Style-Attribute als React-Komponenten hinzufügen, aber CSS-in-JS macht das effizienter, indem es dynamisch gemeinsame Style-Elemente in der head
der Seite erstellt.
Es gibt mehrere CSS-in-JS-Bibliotheken zur Auswahl, aber in diesem Rezept werden wir eine namensRadium verwenden.
Beginnen wir mit der Installation von Radium und React Animations:
$
npminstall
radium
$
npminstall
react-animations
In unserer Beispielanwendung(Abbildung 4-14) wird jedes Mal eine Animation ausgeführt, wenn wir ein Bild zur Sammlung hinzufügen.
Wenn ein Nutzer auf ein Bild klickt, zeigt eine Ausblendanimation, bevor die Bilder aus der Liste entfernt werden, wie in Abbildung 4-15 gezeigt.4
Wir beginnen mit dem Import einiger Animationen und Hilfscodes aus Radium:
import
{
pulse
,
zoomOut
,
shake
,
merge
}
from
'react-animations'
import
Radium
,
{
StyleRoot
}
from
'radium'
const
styles
=
{
created
:
{
animation
:
'x 0.5s'
,
animationName
:
Radium
.
keyframes
(
pulse
,
'pulse'
),
},
deleted
:
{
animation
:
'x 0.5s'
,
animationName
:
Radium
.
keyframes
(
merge
(
zoomOut
,
shake
),
'zoomOut'
),
},
}
Von React Animations erhalten wir pulse
, zoomOut
und shake
Animationen. Wir werden die Animation pulse
verwenden, wenn wir ein Bild hinzufügen. Wir verwenden eine kombinierte Animation aus zoomOut
und shake
, wenn wir ein Bild entfernen. Wir können Animationen mit der Funktion merge
von React Animations kombinieren.
styles
generiert alle CSS-Stile, die für die Ausführung jeder dieser Halbsekunden-Animationen benötigt werden. Der Aufruf von Radium.keyframes()
kümmert sich um alle Details der Animationen für uns.
Wir müssen wissen, wann eine Animation vollständig beendet ist. Wenn wir ein Bild löschen, bevor die Lösch-Animation abgeschlossen ist, gibt es kein Bild mehr, das wir animieren können.
Wir können CSS-Animationen verfolgen, indem wir jedem Element, das wir animieren wollen, einen onAnimationEnd
Callback übergeben. Für jedes Element in unserer Bildersammlung werden wir drei Dinge verfolgen:
-
Die URL des Bildes, das es repräsentiert
-
Ein boolescher Wert, der wahr ist, während die "erstellte" Animation läuft
-
Ein boolescher Wert, der wahr ist, während die "gelöschte" Animation läuft
Hier ist der Beispielcode, um Bilder in und aus der Sammlung zu animieren:
import
{
useState
}
from
'react'
import
{
pulse
,
zoomOut
,
shake
,
merge
}
from
'react-animations'
import
Radium
,
{
StyleRoot
}
from
'radium'
import
'./App.css'
const
styles
=
{
created
:
{
animation
:
'x 0.5s'
,
animationName
:
Radium
.
keyframes
(
pulse
,
'pulse'
),
},
deleted
:
{
animation
:
'x 0.5s'
,
animationName
:
Radium
.
keyframes
(
merge
(
zoomOut
,
shake
),
'zoomOut'
),
},
}
function
getStyleForItem
(
item
)
{
return
item
.
deleting
?
styles
.
deleted
:
item
.
creating
?
styles
.
created
:
null
}
function
App
()
{
const
[
data
,
setData
]
=
useState
([])
let
deleteItem
=
(
i
)
=>
setData
((
d
)
=>
{
const
result
=
[...
d
]
result
[
i
].
deleting
=
true
return
result
})
let
createItem
=
()
=>
{
setData
((
d
)
=>
[
...
d
,
{
url
:
`https://picsum.photos/id/
${
d
.
length
*
3
}
/200`
,
creating
:
true
,
},
])
}
let
completeAnimation
=
(
d
,
i
)
=>
{
if
(
d
.
deleting
)
{
setData
((
d
)
=>
{
const
result
=
[...
d
]
result
.
splice
(
i
,
1
)
return
result
})
}
else
if
(
d
.
creating
)
{
setData
((
d
)
=>
{
const
result
=
[...
d
]
result
[
i
].
creating
=
false
return
result
})
}
}
return
(
<
div
className
=
"App"
>
<
StyleRoot
>
<
p
>
Images
from
&
nbsp
;
<
a
href
=
"https://picsum.photos/"
>
Lorem
Picsum
</
a
>
</
p
>
<
button
onClick
=
{
createItem
}>
Add
</
button
>
{
data
.
map
((
d
,
i
)
=>
(
<
div
style
=
{
getStyleForItem
(
d
)}
onAnimationEnd
=
{()
=>
completeAnimation
(
d
,
i
)}
>
<
img
id
=
{
`image
${
i
}
`
}
src
=
{
d
.
url
}
width
=
{
200
}
height
=
{
200
}
alt
=
"Random"
title
=
"Click to delete"
onClick
=
{()
=>
deleteItem
(
i
)}
/>
</
div
>
))}
</
StyleRoot
>
</
div
>
)
}
export
default
App
Diskussion
Wenn wir uns für eine Animation entscheiden, sollten wir uns zuerst fragen: Was soll diese Animation bedeuten?
Jede Animation sollte eine Bedeutung haben. Sie kann etwas Existentielles zeigen (Erstellung oder Löschung). Sie kann eine Zustandsänderung anzeigen (aktiviert oder deaktiviert werden). Sie kann heranzoomen, um ein Detail zu zeigen, oder herauszoomen, um einen größeren Zusammenhang zu zeigen. Oder sie kann eine Grenze darstellen (eine Rücksprunganimation am Ende einer langen Liste) oder es dem Nutzer ermöglichen, eine Präferenz auszudrücken (nach links oder rechts wischen).
Auch Animationen sollten kurz sein. Die meisten Animationen sollten wahrscheinlich in einer halben Sekunde vorbei sein, damit der Nutzer die Bedeutung der Animation erleben kann, ohne dass er sie bewusst wahrnimmt.
Eine Animation sollte niemals nur attraktiv sein.
Du kannst den Quellcode für dieses Rezept von der GitHub-Seite herunterladen.
4.8 Animieren von Infografiken mit TweenOne
Problem
CSS-Animationen sind flüssig und hocheffizient. Browser können CSS-Animationen in der Compositing-Phase an die Grafikhardware delegieren, was bedeutet, dass nicht nur die Animationen mit Maschinencode-Geschwindigkeit laufen, sondern auch der Maschinencode selbst nicht auf der CPU läuft.
Der Nachteil bei der Ausführung von CSS-Animationen auf Grafikhardware ist jedoch, dass dein Anwendungscode nicht weiß, waswährend einer Animation passiert. Du kannst zwar verfolgen, wann eine Animation beginnt, endet oder wiederholt wird (onAnimationStart
, onAnimationEnd
,onAnimationIteration
), aber alles, was dazwischen passiert, bleibt einGeheimnis.
Wenn du eine Infografik animierst, möchtest du vielleicht die Zahlen in einem Balkendiagramm animieren, während die Balken wachsen oder schrumpfen. Oder wenn du eine Anwendung zur Verfolgung von Radfahrern schreibst, möchtest du vielleicht die aktuelle Höhe anzeigen, während das Fahrrad das Gelände auf und ab fährt.
Aber wie erstellt man Animationen, denen man zuhören kann, während sie passieren?
Lösung
Die TweenOne-Bibliothek erstellt Animationen mit JavaScript, was bedeutet, dass du sie Bild für Bild nachverfolgen kannst, während sie passieren.
Beginnen wir mit der Installation der TweenOne-Bibliothek:
$
npminstall
rc-tween-one
TweenOne arbeitet mit CSS, aber es verwendet keine CSS-Animationen. Stattdessen erzeugt es CSS-Transformationen, die es viele Male pro Sekunde aktualisiert.
Du musst den Gegenstand, den du animieren willst, in ein <TweenOne/>
Element einpacken. Nehmen wir zum Beispiel an, wir wollen ein rect
innerhalb eines SVG animieren:
<
TweenOne
component
=
'g'
animation
=
{...
details
here
}>
<
rect
width
=
"2"
height
=
"6"
x
=
"3"
y
=
"-3"
fill
=
"white"
/>
</
TweenOne
>
TweenOne
nimmt einen Elementnamen und ein Objekt, das die auszuführende Animation beschreibt. Wie dieses Animationsobjekt aussieht, werden wir gleich sehen.
TweenOne verwendet den Namen des Elements (in diesem Fallg
), um einen Wrapper um das animierte Element zu erstellen. Dieser Wrapper hat ein Style-Attribut, das eine Reihe von CSS-Transformationen enthält, um den Inhalt irgendwo zu bewegen und zu drehen.
In unserem Beispiel könnte das DOM zu einem bestimmten Zeitpunkt in der Animation so aussehen:
<
g
style
=
"transform: translate(881.555px, 489.614px) rotate(136.174deg);"
>
<
rect
width
=
"2"
height
=
"6"
x
=
"3"
y
=
"-3"
fill
=
"white"
/>
</
g
>
Obwohl du ähnliche Effekte wie mit CSS-Animationen erzeugen kannst, funktioniert die TweenOne-Bibliothek anders. Anstatt die Animation an die Hardware zu übergeben, verwendet die TweenOne-Bibliothek JavaScript, um jeden Frame zu erstellen, was zwei Konsequenzen hat. Erstens verbraucht sie mehr CPU-Leistung (schlecht), und zweitens können wir die Animation verfolgen, während sie abläuft (gut).
Wenn wir TweenOne
einen onUpdate
Callback übergeben, erhalten wir bei jedem einzelnen Frame Informationen über die Animation:
<
TweenOne
component
=
'g'
animation
=
{...
details
here
}
onUpdate
=
{
info
=>{...}}>
<
rect
width
=
"2"
height
=
"6"
x
=
"3"
y
=
"-3"
fill
=
"white"
/>
</
TweenOne
>
Das info
Objekt, das an onUpdate
übergeben wird, hat einen ratio
Wert zwischen 0 und 1, der den Anteil des Weges darstellt, den das TweenOne Element durch eine Animation geht. Wir können ratio
verwenden, um Text zu animieren, der mit der Grafik verbunden ist.
Wenn wir zum Beispiel ein animiertes Dashboard erstellen, das Fahrzeuge auf einer Rennstrecke zeigt, können wir onUpdate
verwenden, um die Geschwindigkeit und den Abstand jedes Autos anzuzeigen, während es sich bewegt.
Wir erstellen die Grafiken für dieses Beispiel in SVG. Zuerst erstellen wir einen String, der einen SVG-Pfad enthält, der die Strecke darstellt:
export
default
'm 723.72379,404.71306 ... -8.30851,-3.00521 z'
Dies ist eine stark verkürzte Version des tatsächlichen Pfads, den wir verwenden werden. Wir können den Pfadstring wie folgt aus track.js importieren:
import
path
from
'./track'
Um den Track in einer React-Komponente anzuzeigen, können wir ein svg
Element rendern:
<
svg
height
=
"600"
width
=
"1000"
viewBox
=
"0 0 1000 600"
style
=
{{
backgroundColor
:
'black'
}}>
<
path
stroke
=
'#444'
strokeWidth
=
{
10
}
fill
=
'none'
d
=
{
path
}/>
</
svg
>
Wir können ein paar Rechtecke für das Fahrzeug hinzufügen - ein rotes für die Karosserie und ein weißes für die Windschutzscheibe:
<
svg
height
=
"600"
width
=
"1000"
viewBox
=
"0 0 1000 600"
style
=
{{
backgroundColor
:
'black'
}}>
<
path
stroke
=
'#444'
strokeWidth
=
{
10
}
fill
=
'none'
d
=
{
path
}/>
<
rect
width
=
{
24
}
height
=
{
16
}
x
=
{
-
12
}
y
=
{
-
8
}
fill
=
'red'
/>
<
rect
width
=
{
2
}
height
=
{
6
}
x
=
{
3
}
y
=
{
-
3
}
fill
=
'white'
/>
</
svg
>
Abbildung 4-16 zeigt die Strecke mit dem Fahrzeug in der linken oberen Ecke.
Aber wie animieren wir das Fahrzeug auf der Strecke? Mit TweenOne ist das ganz einfach, denn es enthält ein Plugin, das Animationen erzeugt, die SVG-Pfadstrings folgen.
import
PathPlugin
from
'rc-tween-one/lib/plugin/PathPlugin'
TweenOne
.
plugins
.
push
(
PathPlugin
)
Wir haben TweenOne für die Verwendung mit SVG-Pfad-Animationen konfiguriert. Das bedeutet, dass wir uns anschauen können, wie man eine Animation für TweenOne beschreibt. Wir machen das mit einem einfachen JavaScript-Objekt:
import
path
from
'./track'
const
followAnimation
=
{
path
:
{
x
:
path
,
y
:
path
,
rotate
:
path
},
repeat
:
-
1
,
}
Mit diesem Objekt teilen wir TweenOne zwei Dinge mit: Erstens weisen wir es an, Translationen und Rotationen zu erzeugen, die dem path
String folgen, den wir aus track.js importiert haben. Zweitens sagen wir, dass die Animation in einer Endlosschleife laufen soll, indem wir denrepeat
count auf -1 setzen.
Wir können dies als Grundlage für die Animation unseres Fahrzeugs verwenden:
<
svg
height
=
"600"
width
=
"1000"
viewBox
=
"0 0 1000 600"
style
=
{{
backgroundColor
:
'black'
}}>
<
path
stroke
=
'#444'
strokeWidth
=
{
10
}
fill
=
'none'
d
=
{
path
}/>
<
TweenOne
component
=
'g'
animation
=
{{...
followAnimation
,
duration
:
16000
}}>
<
rect
width
=
{
24
}
height
=
{
16
}
x
=
{
-
12
}
y
=
{
-
8
}
fill
=
'red'
/>
<
rect
width
=
{
2
}
height
=
{
6
}
x
=
{
3
}
y
=
{
-
3
}
fill
=
'white'
/>
</
TweenOne
>
</
svg
>
Beachte, dass wir den Spread-Operator verwenden, um einen zusätzlichen Animationsparameter anzugeben: duration
. Ein Wert von 16000 bedeutet, dass die Animation 16Sekunden dauern soll.
Wir können ein zweites Fahrzeug hinzufügen und die onUpdate
Callback-Methode verwenden, um eine sehr rudimentäre Reihe von gefälschten Telemetriestatistiken für jedes Fahrzeug zu erstellen, während es sich auf der Strecke bewegt. Hier ist der fertige Code:
import
{
useState
}
from
'react'
import
TweenOne
from
'rc-tween-one'
import
Details
from
'./Details'
import
path
from
'./track'
import
PathPlugin
from
'rc-tween-one/lib/plugin/PathPlugin'
import
grid
from
'./grid.svg'
import
'./App.css'
TweenOne
.
plugins
.
push
(
PathPlugin
)
const
followAnimation
=
{
path
:
{
x
:
path
,
y
:
path
,
rotate
:
path
},
repeat
:
-
1
,
}
function
App
()
{
const
[
redTelemetry
,
setRedTelemetry
]
=
useState
({
dist
:
0
,
speed
:
0
,
lap
:
0
,
})
const
[
blueTelemetry
,
setBlueTelemetry
]
=
useState
({
dist
:
0
,
speed
:
0
,
lap
:
0
,
})
const
trackVehicle
=
(
info
,
telemetry
)
=>
({
dist
:
info
.
ratio
,
speed
:
info
.
ratio
-
telemetry
.
dist
,
lap
:
info
.
ratio
<
telemetry
.
dist
?
telemetry
.
lap
+
1
:
telemetry
.
lap
,
})
return
(
<
div
className
=
"App"
>
<
h1
>
Nürburgring
</
h1
>
<
Details
redTelemetry
=
{
redTelemetry
}
blueTelemetry
=
{
blueTelemetry
}
/>
<
svg
height
=
"600"
width
=
"1000"
viewBox
=
"0 0 1000 600"
style
=
{{
backgroundColor
:
'black'
}}
>
<
image
href
=
{
grid
}
width
=
{
1000
}
height
=
{
600
}
/>
<
path
stroke
=
"#444"
strokeWidth
=
{
10
}
fill
=
"none"
d
=
{
path
}
/>
<
path
stroke
=
"#c0c0c0"
strokeWidth
=
{
2
}
strokeDasharray
=
"3 4"
fill
=
"none"
d
=
{
path
}
/>
<
TweenOne
component
=
"g"
animation
=
{{
...
followAnimation
,
duration
:
16000
,
onUpdate
:
(
info
)
=>
setRedTelemetry
((
telemetry
)
=>
trackVehicle
(
info
,
telemetry
)
),
}}
>
<
rect
width
=
{
24
}
height
=
{
16
}
x
=
{
-
12
}
y
=
{
-
8
}
fill
=
"red"
/>
<
rect
width
=
{
2
}
height
=
{
6
}
x
=
{
3
}
y
=
{
-
3
}
fill
=
"white"
/>
</
TweenOne
>
<
TweenOne
component
=
"g"
animation
=
{{
...
followAnimation
,
delay
:
3000
,
duration
:
15500
,
onUpdate
:
(
info
)
=>
setBlueTelemetry
((
telemetry
)
=>
trackVehicle
(
info
,
telemetry
)
),
}}
>
<
rect
width
=
{
24
}
height
=
{
16
}
x
=
{
-
12
}
y
=
{
-
8
}
fill
=
"blue"
/>
<
rect
width
=
{
2
}
height
=
{
6
}
x
=
{
3
}
y
=
{
-
3
}
fill
=
"white"
/>
</
TweenOne
>
</
svg
>
</
div
>
)
}
export
default
App
Abbildung 4-17 zeigt die Animation. Die Fahrzeuge folgen dem Verlauf der Rennstrecke und drehen sich dabei in Fahrtrichtung.
Diskussion
Für die meisten UI-Animationen solltest du CSS-Animationen verwenden. Bei Infografiken musst du jedoch oft den Text und die Grafik synchronisieren. TweenOne macht das möglich, allerdings auf Kosten einer höheren CPU-Auslastung.
Du kannst den Quellcode für dieses Rezept von der GitHub-Seite herunterladen.
1 Du kannst den gesamten Quellcode für dieses Rezept auf dem GitHub-Repository herunterladen.
2 In Rezept 4.5 erfährst du, wie du Markdown in deiner Anwendung verwenden kannst.
3 Im GitHub-Repository findest du die Tests, mit denen wir diesen Code getestet haben.
4 Bücher aus Papier sind etwas Wunderschönes, aber um den Animationseffekt richtig zu erleben, solltest du dir den kompletten Code auf GitHub ansehen.
Get React Kochbuch now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.