Kommunikation mellan komponenter
När en sak händer i en komponent ska ofta något hända i en annan. Användaren trycker på Apply settings i formuläret, och spelaren ska visa en ny video. Tiden tar slut i timern, och frågan ska avslutas. Den här artikeln handlar om hur det ska gå till utan att komponenterna blir beroende av varandra. Den bygger på de två föregående artiklarna, Bra komponenter och Komponenter i komponenter.
Exemplen kommer från veckans övningsuppgift, YouTube Player, från exemplet Att göra och från förslaget på arkitektur i examinationsuppgift B2.
Tidsåtgång: Ungefär 15 minuter.
Syskon pratar inte med varandra
Section titled “Syskon pratar inte med varandra”youtube-control och youtube-player ligger bredvid varandra i youtube-scenes shadow DOM. Det
ligger nära till hands att låta formuläret ställa in spelaren direkt:
// Avoid: youtube-control reaches for its sibling#applySettings() { const player = this.getRootNode().querySelector('youtube-player') player.setAttribute('video-id', this.#getFieldValue('video-id')) // ...}Det fungerar, men nu kan youtube-control inte användas utan en youtube-player bredvid sig. Den
kan inte heller testas utan en, och vill du byta spelaren mot en annan måste du ändra i formuläret.
Två komponenter som borde vara oberoende har blivit en.
HTML fungerar inte så. Ett <input> och en <button> bredvid varandra påverkar inte varandra av sig
själva. Det är koden runt omkring som bestämmer att ett klick på knappen ska läsa fältet.
Samma princip känner du igen från funktioner. En funktion som anropar nästa steg själv är svårare att återanvända än en som returnerar sitt resultat och låter den som anropar bestämma vad som händer sedan:
// Avoid: readSettings() decides what happens nextfunction readSettings() { const settings = { /* ... */ } embedVideo(settings)}
// Prefer: the caller composes the twoconst settings = readSettings()embedVideo(settings)Komponenter returnerar inte, men de utlöser händelser. youtube-control gör sitt jobb, läser
formuläret, och berättar att inställningarna har ändrats. Vad som ska hända sedan är inte dess sak.
Föräldern orkestrerar
Section titled “Föräldern orkestrerar”Den som kopplar ihop två komponenter är den som känner till båda: deras gemensamma förälder. I
övningen är det youtube-scene, som har skapat både formuläret och spelaren. Den lyssnar på det ena
barnets händelse och ställer in det andra:
connectedCallback() { this.#abortController = new AbortController()
this.#control.addEventListener('settingschanged', (event) => this.#forwardSettings(event.detail), { signal: this.#abortController.signal })}youtube-control vet inte att det finns en spelare. youtube-player vet inte att det finns ett
formulär; den fungerar lika bra om du sätter attributen för hand i konsolen. Bara youtube-scene
vet att de hör ihop, och det är hela dess uppgift.
Det här är samma mönster som du har sett sedan kursvecka 11, data ned och händelser upp, men med två barn. Ett barns händelse går upp till föräldern, och föräldern skickar data ned till ett annat barn. Data går aldrig i sidled.
På vilken nivå?
Section titled “På vilken nivå?”Föräldern kan vara en komponent, som youtube-scene, eller applikationens index.js. I exemplet
Att göra är det index.js som flyttar en uppgift sist i listan när den bockas av:
const list = document.querySelector('#household')
list.addEventListener('statechanged', (event) => { if (event.detail.done) { list.append(event.target) } else { list.prepend(event.target) }})Händelsen kommer från en todo-item, men bubblar upp genom listan till index.js, som du såg i
föregående artikel.
Fråga: varför inte i todo-list?
Section titled “Fråga: varför inte i todo-list?”todo-list får redan veta när en uppgift bockas av. Varför flyttar inte listan själv uppgiften sist,
i stället för att låta index.js göra det?
Uppgifterna är listans barn i light DOM. De är innehåll som den som använder listan bestämmer över,
inte delar som listan äger. En <ul> sorterar inte om sina <li>.
Om avklarade uppgifter ska hamna sist, försvinna eller ligga kvar är ett beslut om just den här sidan. En annan sida kan vilja något annat med samma lista. Därför fattas beslutet en nivå upp, där sidan bestäms.
Regeln för var orkestreringen hör hemma är densamma som i händelser i webbkomponenter: varje komponent känner bara till sina egna barn. Behöver en händelse längre upp tar varje nivå emot den och skickar en egen vidare.
Översätta mellan barnen
Section titled “Översätta mellan barnen”Barnen talar inte alltid samma språk. youtube-control skickar inställningarna i detail med
nycklar i camelCase, som är brukligt i JavaScript: videoId. youtube-player vill ha attribut i
kebab-case: video-id. Att översätta är förälderns jobb, och youtube-scene gör det med en liten
tabell:
const ATTRIBUTE_NAME_BY_DETAIL_KEY = { videoId: 'video-id', autoplay: 'autoplay', controls: 'controls', loop: 'loop', related: 'related', start: 'start'}
const BOOLEAN_DETAIL_KEYS = new Set(['autoplay', 'controls', 'loop', 'related'])
#forwardSettings(detail) { for (const [key, value] of Object.entries(detail)) { const attributeName = ATTRIBUTE_NAME_BY_DETAIL_KEY[key]
if (BOOLEAN_DETAIL_KEYS.has(key)) { this.#player.toggleAttribute(attributeName, value) } else { this.#player.setAttribute(attributeName, value) } }}Tabellen är det enda stället som känner till båda barnens namn. Börjar formuläret kalla fältet något
annat är det bara tabellen som behöver ändras. Booleska värden sätts med toggleAttribute(), av
skälen i Bra komponenter.
Många attribut på en gång
Section titled “Många attribut på en gång”#forwardSettings() sätter sex attribut i rad, och attributeChangedCallback() anropas för vart och
ett.
Fråga: hur många gånger laddas videon?
Section titled “Fråga: hur många gånger laddas videon?”Anta att youtube-player bygger om sin <iframe> direkt i attributeChangedCallback():
attributeChangedCallback() { this.#renderVideo()}Användaren ändrar video-id och klickar på Apply settings. Hur många gånger börjar spelaren ladda en video?
Upp till sex gånger. setAttribute() anropar attributeChangedCallback() även när värdet är
detsamma som förut, så video-id och start ger en laddning var. De booleska attributen ger en
laddning när toggleAttribute() faktiskt lägger till eller tar bort dem. Den första laddningen sker med det
nya video-id:t men de gamla inställningarna, nästa med en inställning till, och så vidare. Bara den
sista är rätt, och användaren ser spelaren blinka.
attributeChangedCallback() vet inte att fler attribut är på väg. Det gör ingen, eftersom
setAttribute() anropas ett i taget.
Lösningen är att vänta med att rita tills attributen har slutat ändras, så kallad debouncing. Varje anrop avbryter det förra och schemalägger en ny rendering:
attributeChangedCallback() { clearTimeout(this.#renderTimeoutId) this.#renderTimeoutId = setTimeout(() => this.#renderVideo())}
disconnectedCallback() { clearTimeout(this.#renderTimeoutId)}Alla sex setAttribute() körs i samma task, och en timer körs tidigast i nästa, som du läste om i
Händelseloopen. När timern väl körs har alla sex attribut
satts, och videon laddas en gång. Som allt annat komponenten har startat stoppas timern i
disconnectedCallback().
Applikationen i B2
Section titled “Applikationen i B2”Förslaget på arkitektur i examinationsuppgift B2 följer samma mönster. quiz-app är föräldern som
orkestrerar. Den skickar alla förfrågningar till servern, läser och skriver topplistan i Web Storage och
bestämmer vad som visas. De andra komponenterna pratar bara med användaren:
nickname-formberättar med en händelse att användaren har skrivit sitt namn.quiz-questionfår frågan avquiz-appoch berättar med en händelse vad användaren svarade. En fråga kan ha en array med alternativ, så den skickas med en metod,showQuestion(), och inte som attribut, av skälen i Bra komponenter. Detsamma gäller topplistan tillleaderboard.countdown-timerligger iquiz-question, ochquiz-appvet inte att den finns. När tiden är slut tarquiz-questionemottimeoutoch skickar en egentimeoutvidare. Det håller isär “tiden tog slut” och “användaren svarade” förquiz-app.
Ingen av de små komponenterna anropar servern, och ingen av dem vet om de andra. quiz-question
fungerar lika bra med frågor från en JSON-fil som från servern. Det är också det som gör den lätt att
testa för sig.
Börja enkelt
Section titled “Börja enkelt”Det här är ett förslag, inte ett krav. Den första versionen av din applikation behöver inte ha fem
komponenter. En quiz-app som gör allt själv och fungerar är en bättre utgångspunkt än fem
komponenter som inte gör det. När du ser att en del av koden har ett eget ansvar, som timern, bryter
du ut den, och då vet du också vilka attribut, metoder och händelser den behöver.
Att ta med sig
Section titled “Att ta med sig”- Komponenter anropar inte varandra. En komponent gör sitt jobb och berättar med en händelse att något har hänt.
- Den gemensamma föräldern, en komponent eller
index.js, lyssnar på ett barns händelse och skickar data ned till ett annat barn. Data går upp och ned, aldrig i sidled. - Varje komponent känner bara till sina egna barn. Beslut om sidan fattas där sidan bestäms.
- Föräldern översätter mellan barnens namn och format, på ett enda ställe.
- Sätts flera attribut i rad, vänta med att rita tills de har slutat ändras.
- Börja enkelt. Bryt ut en komponent när du ser vilket ansvar den ska ha.
Vidare läsning
Section titled “Vidare läsning”README.mdochARCHITECTURE.mdi övningsuppgiften YouTube Player, i synnerhet avsnittet Varför tre komponenter.README.mdi exemplet Att göra.- Förslaget på arkitektur med komponent- och sekvensdiagram i README:n till examinationsuppgift B2.