Stödjande inspelningar
Kurslitteraturen är en av de delar som driver kursen framåt, men det kan vara svårt att själv strukturera upp innehållet. Vad är relevant? Vad bör du fokusera på i kursen? De stödjande inspelningarna tas fram av kursledningen för att just ge detta sammanhang. Det är av yttersta vikt att du tar del av dessa!
Kursvecka 8 - Webbläsaren och JavaScript
Section titled “Kursvecka 8 - Webbläsaren och JavaScript”Webbläsaren och JavaScript
Section titled “Webbläsaren och JavaScript”Denna inspelning tittar på hur webbläsaren är uppbyggd och vad du ska tänka på när du länkar in JavaScript i webbläsaren.
Webbläsaren – en fientlig programmeringsmiljö
Section titled “Webbläsaren – en fientlig programmeringsmiljö”“The browser is a really hostile programming environment”, enligt Douglas Crockford. Till skillnad från en kontrollerad servermiljö körs klientkod i en miljö utvecklaren inte styr över: många olika webbläsare (Chrome dominerar både i Sverige och globalt, följt av Safari), många olika enhetstyper (mobil, desktop, surfplatta i ungefär jämn fördelning), och varierande stöd för nyare funktioner. Verktyg som caniuse.com och MDN är nödvändiga för att hålla koll på vad som faktiskt går att lita på tvärs över webbläsare – och i grunden är det utvecklarens eget ansvar att testa och hantera skillnaderna, det finns inget centralt skyddsnät.
Från HTML till pixlar: hur en sida renderas
Section titled “Från HTML till pixlar: hur en sida renderas”Webbläsaren bygger upp en sida i flera tydliga steg:
- DOM Construction – HTML läses och parsas till ett DOM-träd (Document Object Model), där varje element blir ett objekt av en specifik typ (
HTMLImageElement,HTMLHeadingElementosv.), alla i grunden ärvande frånHTMLElement→Element→Node→EventTarget. - CSSOM Construction – CSS läses och byggs till ett motsvarande träd (CSSOM) med de beräknade stilarna för varje nod.
- Render Tree construction – DOM och CSSOM slås ihop till ett render-träd. Element med
display: none(eller dolda på annat sätt) tas inte med här. - Layout operation (“reflow”) – varje nods storlek och position i render-trädet räknas ut, och eventuella lager (z-led) skapas.
- Paint operation – varje lager målas upp för sig.
- Composing operation – lagren sätts ihop till den slutliga bilden som visas.
Tillsammans kallas de här stegen den kritiska rendervägen (Critical Rendering Path).
Webbläsarens beståndsdelar
Section titled “Webbläsarens beståndsdelar”En webbläsare byggs av flera samverkande delar: ett användargränssnitt, en browser engine (rendermotorn – DOM, CSSOM, render-träd, layout, målning), en separat JavaScript engine, nätverkshantering och en UI-backend, samt datalagring. De stora webbläsarna skiljer sig åt både i rendermotor och JS-motor: Chrome/Opera/Edge kör Blink + V8, Firefox kör Gecko + SpiderMonkey, Safari kör Webkit + JavaScriptCore, och den numera pensionerade Internet Explorer körde Trident + Chakra.
Parsning och laddning i praktiken
Section titled “Parsning och laddning i praktiken”När webbläsaren parsar ett dokument sker DOM-parsning, CSS-laddning och bildladdning i stort sett parallellt. En vanlig <script>-tagg i <head> beter sig annorlunda: den blockerar DOM-parsningen medan skriptet både laddas ner och körs, vilket kan fördröja hela sidan i onödan. Flera attribut ändrar det beteendet:
<script>sist i<body>– parsningen hinner klart innan skriptet ens börjar laddas, men allt sker fortfarande sekventiellt efteråt.<script async>– skriptet laddas parallellt med DOM-parsningen, men kör så fort det är klart (kan alltså avbryta parsningen mitt i).<script defer>– skriptet laddas parallellt, men körs garanterat först efter att DOM-parsningen är helt klar.<script type="module">– ES-moduler laddas alltid asynkront (somdefer), inklusive alla moduler de i sin tur importerar.
Att köra JavaScript i webbläsaren
Section titled “Att köra JavaScript i webbläsaren”Kod inline direkt i HTML-attribut eller i en <script>-tagg mitt i dokumentet fungerar tekniskt, men bör undvikas: koden blir svår att underhålla, går inte att cacha optimalt, och blockerar DOM-parsningen om den inte hanteras särskilt. Att istället länka in JavaScript som ES-moduler är det rekommenderade sättet:
<head> ... <script type="module" src="js/components/my-hello/my-hello.js"></script> <script type="module" src="js/index.js"></script></head>Moduler laddas asynkront, exekveras i importordning, och stöds brett i moderna webbläsare (se caniuse.com/es6-module) – det här är mönstret som resten av kursens komponentbaserade kod bygger vidare på.
Varför JavaScript i webbläsaren?
Section titled “Varför JavaScript i webbläsaren?”Det var inte givet att det skulle bli JavaScript
Section titled “Det var inte givet att det skulle bli JavaScript”När webben var ung fanns flera konkurrerande idéer om vilket språk som skulle köras i webbläsaren. Java (från Sun) var en stark kandidat – många trodde på 90-talet att Java skulle synas överallt, även i webbläsaren. Microsoft försökte med Visual Basic (VBScript). Netscape, med Brendan Eich, byggde istället ett eget alternativ, ursprungligen döpt till LiveScript – namnet ändrades till JavaScript för att rida på Javas dåvarande popularitet, trots att språken har lite gemensamt. Standarden bakom språket heter idag ECMAScript. Det är alltså långt ifrån en självklarhet att det blev just JavaScript, även om det med facit i hand kan kännas så. Idag finns även WebAssembly, som gör det möjligt att kompilera helt andra språk för körning i webbläsaren – vad som händer på sikt är omöjligt att sia om.
Webapplikationer, inte hemsidor
Section titled “Webapplikationer, inte hemsidor”Det som byggs med JavaScript i webbläsaren idag är sällan statiska webbsidor, utan webapplikationer – Facebook och Slack är typiska exempel, med mycket logik som körs i bakgrunden. Det som kanske är mindre uppenbart: verktyg som Visual Studio Code är i grunden också en webapplikation, byggd med HTML, CSS och JavaScript, paketerad för olika operativsystem med hjälp av Electron. Discord, Pinterest, Instagram och Skype är byggda på liknande sätt, ofta transpilerade till native mobilappar med till exempel React Native. Webben som plattform har alltså redan spridit sig långt bortom webbläsarfönstret.
Webben blir plattformen
Section titled “Webben blir plattformen”Google har länge drivit tanken att webbtekniker duger till att bygga applikationer som fungerar “överallt” utan att behöva paketeras via Electron, React Native eller liknande. Chromebook, byggt kring Chromium, är ett konkret exempel – ett operativsystem där webbläsaren i praktiken är hela gränssnittet. Ett annat resultat av samma tanke är Progressive Web Applications (PWA), standardapplikationer byggda med webbteknik som ändå kan installeras och köras som vilken annan app som helst – ett ämne kursen återkommer till mot slutet.
Att lära sig webbprogrammering handlar alltså inte om en smal nisch, utan öppnar upp för applikationsutveckling brett över plattformar – mobil, desktop och webb – eftersom samma grundverktyg återanvänds så pass mycket.
Introduktion till “Custom elements” och webbkomponenter
Section titled “Introduktion till “Custom elements” och webbkomponenter”Kan du bygga dina egna html-element? Absolut! Denna inspelning tittar på hur.
Kan man ärva HTML-element?
Section titled “Kan man ärva HTML-element?”Eftersom inbyggda HTML-element (HTMLImageElement, HTMLElement osv.) i grunden är klasser, ligger frågan nära till hands: kan man ärva från dem och skapa sina egna? Att direkt instansiera ett inbyggt element med new fungerar dock inte:
const image = new HTMLImageElement() // ERROR: Illegal constructorBakom kulisserna är det fortfarande en klass med en konstruktor, men den som byggt DOM-API:et har medvetet sett till att konstruktorn kastar ett fel om den anropas med new – element i webbläsaren skapas istället via document.createElement(...):
const image2 = document.createElement('img')Ett element skapat på det sättet hamnar inte automatiskt någonstans i DOM-trädet – det är ett löst, “flytande” objekt tills det uttryckligen läggs till, till exempel med appendChild.
Custom element – ett eget HTML-element
Section titled “Custom element – ett eget HTML-element”Att ärva från ett HTML-element är fullt möjligt, och det är precis vad som krävs för att skapa ett custom element – ett självdefinierat element för sådant som saknas bland de inbyggda (till exempel en flagg-väljare för språkbyte, eller en återanvändbar knapp som visar antal öppna merge requests):
class MyCustomElement extends HTMLElement { constructor () { super() // ... }}
customElements.define('my-custom-element', MyCustomElement)customElements.define() registrerar klassen hos webbläsaren under ett valt tagg-namn. Eftersom elementet ska skapas via document.createElement(...), inte med new, är det vanligt att inte exportera klassen – då minskar risken att någon råkar instansiera den fel väg. Ett undantag är om man faktiskt vill låta någon annan komponent ärva vidare från den.
Anonyma klasser
Section titled “Anonyma klasser”Ett vanligt misstag är att kopiera en komponentfil för att skapa en ny, men glömma byta namnet på själva klassen. Ett sätt att undvika det är att skriva klassen anonymt, direkt som andra argument till define:
customElements.define('my-custom-element', class extends HTMLElement { // ...})Eftersom klassen aldrig namnges finns det heller ingen risk att av misstag exportera den. Generellt avråds anonyma klasser (skulle koden ligga i en loop måste klassen då återskapas för varje varv), men eftersom define-koden bara körs en gång per modul är det inget problem här.
Namngivning
Section titled “Namngivning”Ett custom element måste innehålla minst ett bindestreck i taggnamnet – standarden garanterar att inget inbyggt HTML-element någonsin kommer ha ett bindestreck i sitt namn, vilket förhindrar framtida namnkrockar. Konventionen MyCustomElement (PascalCase) för själva klassnamnet följer JavaScripts vanliga klass-namngivning, medan tagg-namnet (my-custom-element) måste vara i kebab-case eftersom bindestreck inte är tillåtet i ett klassnamn.
Vad är egentligen “webbkomponenter”?
Section titled “Vad är egentligen “webbkomponenter”?”Det som skapades ovan kallas ett custom element – men “webbkomponent” är ett bredare, informellt begrepp (ingen egen W3C-standard) som beskriver hur tre riktiga standarder – Custom Elements, HTML Templates och Shadow DOM – tillsammans används för att bygga återanvändbara, isolerade byggstenar för webbsidor. ES-moduler räknas ofta också in, eftersom de är ett naturligt sätt att paketera och dela komponenter.
Idén är densamma som med fysiska komponenter: bygg en isolerad, återanvändbar del en gång (till exempel en knapp som visar en pipeline-status, eller en “clone repository”-widget), och plugga sedan in den var den behövs.
Webbkomponenter kontra ramverk
Section titled “Webbkomponenter kontra ramverk”Custom Elements, HTML Templates och Shadow DOM fungerar och stöds brett idag – men gjorde det inte för fem år sedan. Det förklarar varför JavaScript-ramverk (Angular, React och senare Vue, Svelte m.fl.) växte fram: de löste samma strukturproblem innan webbstandarden själv hann ikapp. I mångt och mycket har utvecklingen gått “varvet runt” – funktionalitet som tidigare krävde ett ramverk finns nu inbyggd i webbläsaren (precis som jQuery till stor del blev överflödigt när dess bekvämligheter togs in i standarden).
Att kursen medvetet satsar på webbkomponenter, byggda ovanpå klass-syntax, är delvis ett svar på återkommande kritik: JavaScript utan någon påtvingad struktur upplevs som svårt att lära sig, eftersom nästan allt går att göra på flera olika sätt. Genom att först etablera ett tydligt sätt att organisera kod (klasser, sedan komponenter byggda av klasser) blir steget till att senare lära sig ett ramverk som React eller Svelte mindre – flera moderna ramverk (Svelte i synnerhet) bygger direkt vidare på webbkomponenter istället för att uppfinna en helt egen abstraktion.
Kursvecka 9 - HTML-element
Section titled “Kursvecka 9 - HTML-element”HTML Element and the Document Object Model
Section titled “HTML Element and the Document Object Model”DOM-API:et
Section titled “DOM-API:et”Document Object Model (DOM) är det API som exponerar webbläsarens interna renderingsmotor mot JavaScript-motorn – vägen in för att läsa och ändra sidans innehåll efter att den laddats. document är ingången till API:et: allt arbete mot DOM:en börjar med document. något.
DOM:en är en trädstruktur av noder. Element (<h1>, <article> osv.) är en typ av nod, men inte den enda: text mellan taggar blir en egen textnod, kommentarer blir en kommentarsnod, och dokumentet i sig är en dokumentnod. Alla element är noder, men långt ifrån alla noder är element. Även whitespace – radbrytningar och indenteringar mellan taggar, skrivna för läsbarhetens skull – blir enligt standarden egna textnoder, vilket historiskt orsakade skillnader mellan webbläsare (Internet Explorer hoppade förbi dem, andra webbläsare gjorde det inte). Praktiskt spelar det knappt någon roll längre, men det förklarar varför ett träd kan innehålla fler noder än man förväntar sig.
Att välja ut noder
Section titled “Att välja ut noder”Ett äldre gäng metoder finns direkt på document: getElementById (ett element), getElementsByTagName och getElementsByClassName (en HTMLCollection vardera). Dessa är standardiserade av W3C med långa, explicita namn, och getElementById dyker upp i praktiskt taget alla exempel som finns – värt att känna igen även om den används mer sällan i praktiken idag.
Modernare och att föredra är Selectors API: querySelector (returnerar ett element, det första som matchar) och querySelectorAll (returnerar en NodeList), båda med vanliga CSS-selektorer som argument – samma selektorsyntax som redan används i CSS, istället för en egen uppsättning metoder per sökkriterium.
const container = document.querySelector('#container')const posts = document.querySelectorAll('.post')En viktig skillnad mellan de två returtyperna: en HTMLCollection (från de äldre metoderna) är live – ändras DOM:en efteråt uppdateras samlingen automatiskt. En NodeList från querySelectorAll är statisk, ett snapshot av hur DOM:en såg ut vid anropet. Ingen av dem är en riktig array (NodeList.prototype !== Array.prototype): forEach fungerar på en NodeList, men inte map/filter/reduce. Behövs de, konverteras samlingen till en array med Array.from(nodeList) eller spridningsoperatorn.
Navigera i trädet
Section titled “Navigera i trädet”Utifrån en känd nod kan man ta sig till närliggande noder: parentNode (alltid ett element, eftersom bara element kan ha barn), firstChild/lastChild (kan vara en textnod), firstElementChild/lastElementChild (garanterat ett element), samt previousElementSibling/nextElementSibling för att röra sig mellan syskon. childNodes ger alla barnnoder inklusive text, medan children bara ger elementnoder.
Historiskt skiljde sig firstChild mellan webbläsare (Microsoft implementerade den så att den hoppade direkt till första elementet, andra följde standarden och gav den första noden oavsett typ) – ännu ett skäl att hålla isär “nod” och “element” som begrepp, även om skillnaden i praktiken är utjämnad sedan länge.
Skapa och manipulera
Section titled “Skapa och manipulera”Nya noder skapas med document.createElement('p') (tomt element) och document.createTextNode('text') (textnod) – de måste därefter kopplas ihop och in i trädet för att synas:
const link = document.createElement('a')const text = document.createTextNode('lnu.se')link.appendChild(text)container.appendChild(link)appendChild lägger till sist bland ett elements barn; körs den på en nod som redan finns i trädet flyttas den istället till den nya platsen (ingen manuell borttagning behövs). Övriga byggstenar: insertBefore(newNode, referenceNode), replaceChild(newNode, oldNode), removeChild(oldNode) och cloneNode(deep) – där deep (true/false) avgör om hela undernodträdet kopieras med eller bara den översta noden.
Genvägar – och en säkerhetsfälla
Section titled “Genvägar – och en säkerhetsfälla”Att bygga upp text manuellt med createTextNode + appendChild för varje liten ändring blir fort omständligt. Två genvägar täcker det vanligaste fallet:
element.textContent = 'Ny text' // ersätter allt innehåll med en textnodelement.innerHTML = '<p>Text</p>' // parsar strängen till element + textnoderinnerHTML känns bekvämt eftersom den tolkar HTML-taggar i strängen – men det är samtidigt dess fara. Kommer strängen från en användare eller annan extern källa och innehåller <script>-taggar, exekveras den koden på sidan (XSS). innerHTML ska bara användas när innehållet är fullt kontrollerat, till exempel en hårdkodad sträng i källkoden – aldrig på text som kommer utifrån. Standardvalet ska vara textContent, som bara någonsin skapar text, aldrig kod. Ett vanligt undantag där innerHTML ändå är okej: tömma ett element genom att sätta det till en tom sträng.
Attribut
Section titled “Attribut”getAttribute('namn'), setAttribute('namn', värde) och removeAttribute('namn') läser, sätter respektive tar bort HTML-attribut. Ett exempel som visar nyttan: skapa ett <img>-element och sätta src via setAttribute – webbläsaren börjar hämta bilden i bakgrunden så fort attributet sätts, redan innan elementet lagts in i DOM-trädet, och renderar den så fort både bilden är hämtad och elementet infogats.
De flesta inbyggda HTML-attribut har också en motsvarande egenskap direkt på elementobjektet (element.href, element.className osv.) – Microsofts genväg till samma resultat som setAttribute. Den fungerar, men lämnar mindre kontroll: skillnaden blir tydlig med className kontra classList (se nedan).
Håll isär HTML, CSS och JavaScript
Section titled “Håll isär HTML, CSS och JavaScript”Att bygga upp komplex HTML-struktur genom en lång kedja av createElement/appendChild-anrop blir snabbt oläsligt – samma princip som att dela upp kod i funktioner och moduler gäller nu även uppdelningen mellan HTML, CSS och JavaScript. Att blanda dem (till exempel bygga hela strukturer i JavaScript istället för att skriva HTML som HTML) gör koden svår att underhålla trots att den gör något enkelt.
Ett konkret exempel på varför disciplinen kring det här spelar roll: att sätta en CSS-klass fel kan orsaka riktiga buggar. element.className = 'danger' ersätter hela klasslistan (det är i grunden ett set), medan element.classList.add('danger') lägger till utan att röra befintliga klasser. Ett element med klassen important som får en ny klass satt via className tappar important helt – en bugg som är lätt att missa eftersom koden ser rimlig ut. classList har också remove, toggle och contains för att slippa den typen av misstag.
Sätts stilar direkt via JavaScript (element.style.color = 'red') skrivs CSS-egenskapsnamn i camelCase istället för med bindestreck (fontSize, marginLeft), eftersom bindestreck inte är giltigt i JavaScript-identifierare. float är dessutom ett reserverat ord i JavaScript, så CSS-egenskapen float heter style.cssFloat. Bättre än att sätta enskilda stilar via style är oftast att lägga stilreglerna i CSS och bara växla class via classList – exakt samma princip som att hålla HTML utanför JavaScript.
Templates
Section titled “Templates”<template>-elementet löser problemet med att bygga upp återkommande HTML-strukturer i JavaScript: innehållet i en <template>-tagg parsas av webbläsaren men renderas aldrig och hämtar inte externa resurser (bilder m.m.) förrän det faktiskt klonas ut i dokumentet.
const template = document.querySelector('#post-template')const clone = template.content.cloneNode(true)document.body.appendChild(clone)template.content.cloneNode(true) är närmast pannplattekod – lätt att glömma .content. Går det inte att ha en <template> i själva HTML-dokumentet (till exempel för att den behöver skapas dynamiskt i en komponents JavaScript-fil), kan samma HTML skrivas som en template-sträng direkt i JavaScript-koden istället; det finns (2021) inget standardiserat, snyggt sätt att importera en fristående .html-fil in i en JavaScript-modul. <template> har funnits i webbläsaren i över tio år och föregår de externa mallmotorer (Mustache, Handlebars, Pug m.fl.) som fortfarande används mycket på serversidan.
Kursen väljer genomgående att undervisa webbstandarder snarare än bibliotek som jQuery eller ramverk som React/Angular – standarderna är det som håller över tid, medan enskilda bibliotek och ramverksversioner kommer och går.
Kursvecka 10 - Webbkomponenter
Section titled “Kursvecka 10 - Webbkomponenter”Webbkomponenter - En introduktion till “Custom Elements”
Section titled “Webbkomponenter - En introduktion till “Custom Elements””Templates som strängar i JavaScript
Section titled “Templates som strängar i JavaScript”Att bygga upp DOM-innehåll med upprepade createElement/appendChild/createTextNode-anrop blir snabbt svårläst och svårunderhållet. Ett bättre sätt är att skriva markupen som en <template> och klona innehållet:
const template = document.createElement('template')template.innerHTML = ` <div class="post"> <ul> <li class="active"><a href="#">The first link</a></li> <li><a href="#">The second link</a></li> </ul> </div>`
const clone = template.content.cloneNode(true)document.querySelector('body').appendChild(clone)När komponenter ska byggas är det bättre att skriva templaten som en JavaScript-sträng (med backticks) direkt i komponentens egen fil, istället för att ha en <template>-tagg liggande i HTML-dokumentet – då hamnar allt som hör till komponenten på ett och samma ställe. Eftersom det är utvecklaren själv som skriver strängen är innerHTML här ofarligt; risken uppstår först om data från en extern källa (användarinput, ett API-svar) skulle klistras in i samma sträng.
Custom Element – ett eget HTML-element
Section titled “Custom Element – ett eget HTML-element”Ett custom element är helt enkelt ett självdefinierat HTML-element, precis som <div> eller <img> men skräddarsytt för ett eget syfte – till exempel <my-avatar src="images/leitet.png"></my-avatar> för att presentera en avatarbild på ett specialiserat sätt. Komponentens egen fil (t.ex. js/components/my-avatar/index.js) laddas in med en <script type="module">-tagg, och den övriga applikationskoden (js/index.js) importerar komponenten direkt istället för att bara lita på att script-taggen finns i HTML – det gör beroendet synligt för den som läser koden, och skyddar mot att någon av misstag tar bort en script-tagg som “verkar oanvänd”.
Timing vid laddning
Section titled “Timing vid laddning”Moduler laddas med defer, vilket innebär att den modul som börjar laddas först också är den som exekveras först – oavsett hur lång tid själva nedladdningen tar. Laddas komponenten in före applikationens egen kod (js/index.js) finns komponenten alltså redan definierad när den egna koden körs.
Vänds ordningen (egen kod laddas före komponenten) kan ett tajmingproblem uppstå: webbläsaren stöter på <my-avatar> i HTML:en innan den vet vad elementet är, och skapar då tillfälligt ett anonymt HTML-element istället. Först när customElements.define('my-avatar', ...) senare körs uppgraderas det befintliga elementet till att vara av rätt typ – prototypreferenserna sätts om i efterhand. I praktiken hanterar webbläsaren de flesta sådana fall automatiskt, men för att undvika problem helt är det säkrast att alltid ladda in komponenten före den kod som använder den.
Namngivning av custom elements
Section titled “Namngivning av custom elements”Ett custom element måste innehålla minst ett bindestreck i namnet (<my-avatar>, aldrig <avatar>). Anledningen är att W3C garanterat aldrig kommer namnge ett inbyggt HTML-element med bindestreck, vilket förhindrar framtida namnkrockar med standarden. Delen före bindestrecket fungerar som ett namespace – ett sätt att undvika krockar med andra utvecklares komponenter också (stora företag använder ofta korta förkortningar som prefix, t.ex. tänkta fb-/sf-; ett projekt utan etablerat namn gör klokt i att välja ett något längre, tydligare prefix). Utöver bindestrecks-kravet är ett custom element som vilket HTML-element som helst – det går att välja ut och styla via CSS och nås via JavaScript på precis samma sätt.
Webbkomponenter - Filstruktur
Section titled “Webbkomponenter - Filstruktur”En komponent, en mapp
Section titled “En komponent, en mapp”En rekommenderad filstruktur samlar varje egen komponent i en egen undermapp under js/components/, till exempel js/components/my-avatar/. Där ligger komponentens egna resurser (bilder i en images-mapp, eventuella beroenden i en lib-mapp), samt två JavaScript-filer: my-avatar.js, som innehåller själva komponentens klass, och index.js, som bara importerar och exponerar den:
import './my-avatar.js'import './components/my-avatar/'Applikationens egen js/index.js importerar komponentens mapp (vilket i sin tur triggar index.js inuti mappen). Mönstret med en tunn index.js som pekar vidare till den faktiska implementationsfilen är detsamma oavsett hur många lager av mappar en modul är nästlad i.
import.meta.url – relativa sökvägar till egna resurser
Section titled “import.meta.url – relativa sökvägar till egna resurser”En komponent behöver ofta referera till sina egna resurser, till exempel en standardbild, utan att hårdkoda en sökväg som slutar bero på var i applikationen komponenten råkar vara monterad. import.meta.url ger den körande modulens egen URL, vilket gör att en sökväg kan räknas ut relativt komponentens egen fil istället för relativt sidans rot:
let pathToModule = import.meta.url// t.ex. http://localhost:3000/js/components/my-avatar/index.js
let path = new URL('./images/default.png', pathToModule)// http://localhost:3000/js/components/my-avatar/images/default.png
// ... <img id="avatar" src="${path}" /> ...Fördelen är att komponentmappen blir självförsörjande och flyttbar – flyttas hela my-avatar-mappen till en annan plats i projektet (eller till ett helt annat projekt) fortsätter sökvägarna till dess egna bilder att fungera, eftersom de aldrig var beroende av var komponenten monterades från början.
Använda npm-moduler i en komponent
Section titled “Använda npm-moduler i en komponent”Precis som i vilken annan del av applikationen som helst kan en komponent ha beroenden till paket installerade via npm:
$ npm install md5import md5 from 'md5'Byggverktyget tar hand om att paketera (bundla) node_modules-beroendet tillsammans med resten av koden – något som inte kräver någon särskild hantering bara för att koden råkar ligga i en komponentmapp.
Webbkomponenter - Skapa Custom Elements
Section titled “Webbkomponenter - Skapa Custom Elements”Ett custom element utan Shadow DOM räcker inte
Section titled “Ett custom element utan Shadow DOM räcker inte”Ett första försök att skapa en komponent kan se ut så här:
customElements.define('my-avatar', class extends HTMLElement { constructor () { super() this.innerHTML = ` <div> <img id="avatar" src="js/components/my-avatar/images/default.png" /> </div>` } })Det här fungerar faktiskt inte alls – att sätta innerHTML direkt i konstruktorn kastar ett fel (Failed to construct 'CustomElement': The result must not have children). Ett custom element får inte skapa barn-innehåll i konstruktorn på det sättet. Lösningen är Shadow DOM.
Shadow DOM
Section titled “Shadow DOM”Inbyggda element som <video> har länge haft ett eget, inkapslat DOM-träd som inte syns i den vanliga DOM-strukturen – det är därför videospelarens kontroller inte går att komma åt via vanlig querySelector på sidan. Shadow DOM ger samma möjlighet till egna, kapslade komponenter: en shadowRoot kopplas till elementet (kallat shadow host), och allt innehåll som hör till komponentens interna implementation läggs där istället för direkt i det vanliga DOM-trädet.
const template = document.createElement('template')template.innerHTML = ` <style> img { clip-path: circle(50%); } </style> <div> <img id="avatar" src="" /> </div>`
customElements.define('my-avatar', class extends HTMLElement { #image
constructor () { super() // Attach a shadow DOM tree to this element and // append the template to the shadow root. this.attachShadow({ mode: 'open' }) .appendChild(template.content.cloneNode(true))
// Get a reference to the image-element. this.#image = this.shadowRoot.querySelector('#avatar') } })attachShadow({ mode: 'open' }) skapar shadow-roten och gör den läsbar utifrån (element.shadowRoot); en klonad template-nod läggs till där. CSS skriven inuti templaten (eller i en <style>-tagg i shadow-roten) gäller bara inuti komponenten – den läcker varken in eller ut.
Lifecycle callbacks
Section titled “Lifecycle callbacks”Ett custom element kan implementera ett antal metoder som webbläsaren själv anropar vid olika tillfällen:
-
constructor– anropas när elementet skapas eller uppgraderas. Här hör initiering av state, uppsättning av shadow DOM och event listeners hemma. -
attributeChangedCallback (name, oldValue, newValue)– anropas när ett observerat attributs värde ändras (läggs till, tas bort eller byts), samt när elementet skapas/uppgraderas. Vilka attribut som ska observeras listas explicit i en statisk getter:static get observedAttributes () {return ['src']}attributeChangedCallback (name, oldValue, newValue) {if (name === 'src' && newValue !== oldValue) {this.#image.setAttribute('src', newValue)}} -
connectedCallback– anropas när elementet läggs till i DOM:en (eller ett nytt DOM-träd). -
disconnectedCallback– anropas när elementet tas bort från DOM:en. Ett naturligt ställe att städa upp event listeners och liknande. -
adoptedCallback– anropas om elementet flyttas till ett annat dokument. Sällan aktuellt.
En <slot> i templaten är en platshållare där innehåll som skrivs mellan komponentens öppnings- och stängningstagg i vanlig HTML får plats:
<my-gravatar email="johan.leitet@lnu.se"> <p><strong>Adding</strong> some content to our element.</p></my-gravatar>template.innerHTML = ` <div> <img class="avatar" id="gravatar" src="" /> <slot class="info"></slot> </div>`Har <slot> ett name-attribut (en named slot) kan flera olika slots fyllas med olika innehåll samtidigt, genom att matcha namnet mot ett slot-attribut på det inskickade innehållet:
template.innerHTML = ` <div> <h3><slot name="name">Unknown</slot></h3> <p><slot name="info"></slot></p> </div>`<my-gravatar email="johan.leitet@lnu.se"> <span slot="name">Johan Leitet</span> <span slot="info">This is some text used to explain something</span></my-gravatar>Text mellan slot-taggarna i templaten (t.ex. Unknown ovan) fungerar som standardinnehåll om inget matchande innehåll skickas in.
CSS i komponenter: :host, ::part() och ::slotted()
Section titled “CSS i komponenter: :host, ::part() och ::slotted()”Tre specialfall behövs för att styla över shadow-gränsen:
-
:host– en pseudoklass som, använd inuti komponentens egen CSS, refererar till shadow host-elementet självt (t.ex.<my-gravatar>). -
::part()– en pseudoelement som, använt i CSS utanför komponenten, kommer åt ett internt element som explicit exponerats med ettpart-attribut:css/styles.css my-gravatar::part(avatar) {clip-path: circle(50%);}js/components/my-avatar/index.js // <img part="avatar" ... /> -
::slotted()– ett pseudoelement som, använt inuti komponentens egen CSS, kommer åt innehåll som skickats in via en slot:::slotted(*) {max-width: 100%;max-height: 100%;}
Best practices
Section titled “Best practices”Några tumregler för egna komponenter: kasta helst inte fel om det går att undvika, efterlikna hur inbyggda element beter sig (t.ex. src snarare än ett eget påhittat source-attributnamn), inkludera samtliga beroenden i komponentens egen mapp, och dokumentera komponenten – vilka attribut, metoder, events och delar (part) den exponerar, samt ett användningsexempel. En README.md i komponentens egen mapp är ett naturligt ställe för den dokumentationen.
Kursvecka 11 - Händelser
Section titled “Kursvecka 11 - Händelser”Samtidighetsmodellen och eventdriven programmering
Section titled “Samtidighetsmodellen och eventdriven programmering”- Introduktion
- Samtidighetsmodellen
- Stack och heap
- Händelsekön (The Task Queue)
- Timers och Interval
- Händelsestyrd programmering
- Webbläsarens inbyggda händelser (Event)
- Antipattern: onclick=“”
- Lyssna på händelser (addEventListener)
- Exempel: webbläsaren fryser
- Ta bort händelsehanterare (removeEventListener)
- Händelser som utlöses en gång (option: once)
- Vad utlöste händelsen? (this, event.target)
- Closures
- Delegat (Event delegation)
- Stoppa det förvalda beteendet. (event.preventDefault)
- Propagation (capture, bubble)
- Web Worker API
- Avslutning
Varför behövs en samtidighetsmodell?
Section titled “Varför behövs en samtidighetsmodell?”En samtidighetsmodell (concurrency model) beskriver hur ett system hanterar att flera saker kan behöva ske samtidigt utan att data blir inkonsekvent – samma grundproblem som uppstår när två klienter försöker skriva till samma rad i en databas samtidigt. JavaScript brukar beskrivas som entrådat: webbläsaren har en huvudtråd (main thread) som sköter både att köra JavaScript och att rendera om sidan, vilket är samma tråd som parsar HTML-dokumentet och bygger DOM-trädet. Att JavaScript exekverar i samma tråd som renderingen är förklaringen till varför ett <script> mitt i dokumentet pausar HTML-parsningen tills skriptet är klart.
Utöver huvudtråden går det att skapa egna worker-trådar (se sist i denna sammanfattning) för att göra tungt arbete parallellt – de har dock ingen tillgång till DOM:en, bara huvudtråden får läsa och skriva där.
Call stack, heap och garbage collector
Section titled “Call stack, heap och garbage collector”Huvudtråden har en egen stack – en LIFO-struktur (last in, first out) som håller reda på vilka funktioner som är under exekvering just nu, en så kallad stack frame per anrop. När en funktion anropar en annan läggs en ny frame på toppen; när funktionen returnerar plockas den bort. Ett kastat undantag som inte fångas visar denna kedja som en stack trace i konsolen.
function foo (b) { const a = 10 return a + b + 11}
function bar (x) { const y = 3 return foo(x * y)}
let res = console.log(bar(7))Själva funktionsdefinitionerna och andra referenstyper lagras på heapen, medan stacken håller värdetyperna och referenserna dit. När stacken töms och inget längre refererar till ett objekt på heapen städar webbläsarens garbage collector bort det automatiskt – inget man behöver hantera manuellt, men bra att veta att det finns.
Task queue och händelseloopen
Section titled “Task queue och händelseloopen”Utöver stacken finns en task queue (även kallad message queue) – en FIFO-kö (first in, first out, till skillnad från stacken). Kod läggs till i den här kön när t.ex. ny JavaScript-kod hittas att köra, en händelse utlöses, en timer eller ett intervall triggar, eller en worker skickar ett meddelande.
Händelseloopen (the event loop) är den mekanism som hela tiden kollar: är stacken tom? Om ja, plocka nästa meddelande från task queue och kör det på stacken. Det är därför en synkron, blockerande operation (t.ex. en while (true)-loop eller ett tungt beräkningsanrop) fryser hela sidan – stacken töms aldrig, så händelseloopen kan aldrig plocka nästa uppgift eller släppa fram en rendering. En markör som slutar blinka i ett inputfält är ett konkret tecken på exakt detta.
Utöver task queue finns en animation queue (webbläsarens egna renderingshändelser) och en microtask queue (kopplad till Promises, kommer i ett senare avsnitt). Skillnaden är att microtask-kön töms helt varje gång händelseloopen tittar på den – fylls den på snabbare än den hinner tömmas kan även microtasks blockera rendering, vilket inte händer på samma sätt för den vanliga task queue.
Timers: setTimeout och setInterval
Section titled “Timers: setTimeout och setInterval”setTimeout schemalägger att en funktion läggs till task queue efter en angiven fördröjning i millisekunder – det är alltså inte en garanti för exakt tajming, bara en garanti för att koden tidigast läggs i kön efter den tiden. Är kön full av annat arbete kan det dröja längre.
const timer = window.setTimeout(() => { console.log('Minst 3 sekunder har passerat..')}, 3000)
window.clearTimeout(timer) // avbryter timern innan den hinner triggassetInterval är samma sak fast upprepat – och betydligt farligare att använda oförsiktigt. Om koden i intervallet tar längre tid att köra än själva intervalltiden fylls task queue snabbt på med fler och fler väntande uppgifter, vilket gör sidan allt segare och till slut kan tömma tillgängligt minne. Ett vanligt, säkrare mönster är att istället låta en setTimeout schemalägga nästa setTimeout i slutet av sin egen körning – då kan aldrig mer än en instans ligga och vänta i kön samtidigt. Precis som med timeouts är det viktigt att komma ihåg clearInterval när intervallet inte längre behövs, annars fortsätter det köra även efter att exempelvis den komponent som skapade det tagits bort.
Händelser: fyra kategorier
Section titled “Händelser: fyra kategorier”Händelser (events) är det som gör att program kan reagera på saker som inte går att förutse i förväg – användarens klick, webbläsarens egna signaler, nätverkssvar och egendefinierade händelser. De brukar delas in i fyra kategorier:
- User initialized – musklick, scroll, tangenttryckningar
- Browser initialized – sidan är färdigladdad, DOM:en har ändrats
- Network – en bild har laddat klart, ett svar har kommit från servern
- Synthetic events – egna, självdefinierade händelser (custom events)
Koppla händelselyssnare – och vad man ska undvika
Section titled “Koppla händelselyssnare – och vad man ska undvika”Att skriva händelsehantering direkt i HTML-attribut (onclick="doSomething()") är ett anti-pattern: det blandar ihop struktur och beteende, blir svårt att underhålla i större skala och är samma princip som gäller för varför JavaScript inte ska skrivas inline i HTML överhuvudtaget.
<a href="page.html" onclick="doSomething()">Gör inte såhär</a>Rätt väg är addEventListener, som finns tillgänglig på alla DOM-element (och på egna webbkomponenter, som ju ärver från EventTarget):
const a = document.querySelector('#theatag')
a.addEventListener('click', (event) => { console.log('Länken klickades!')})Notera att det är en referens till funktionen som skickas in, aldrig ett anrop av den (buttonClicked, inte buttonClicked()). Namngiven funktion, anonym funktion eller arrow function fungerar likvärdigt för att lyssna – skillnaden mellan dem gäller framför allt hur this beter sig (se nedan) och hur enkelt lyssnaren går att ta bort igen.
För att ta bort en lyssnare med removeEventListener krävs exakt samma funktionsreferens som skickades in vid addEventListener – det räcker inte att koden ser likadan ut. Det är därför en anonym funktion eller arrow function inte går att ta bort i efterhand; ska en lyssnare kunna tas bort måste den sparas i en namngiven variabel eller skrivas som en namngiven funktion. I moderna webbläsare städar motorn ofta bort döda lyssnare av sig själv (t.ex. när elementet de satt på tas bort ur DOM:en), men det är fortfarande god praxis (best practice) att ta bort lyssnare man inte längre behöver – särskilt om syftet är att lyssnaren bara ska köras en gång, vilket också går att uttrycka direkt via tredje argumentet: addEventListener('click', handler, { once: true }).
Vad var det som utlöste händelsen?
Section titled “Vad var det som utlöste händelsen?”Inuti en händelsehanterare vill man ofta veta vilket element som utlöste händelsen. Det säkraste sättet är event.target, som alltid pekar på det element användaren faktiskt interagerade med.
const a = document.querySelector('#atag')
a.addEventListener('click', function (event) { a === this // true a === event.target // true this === event.target // true})Skrivs hanteraren som en vanlig function-funktion refererar this till elementet händelsen kopplades på – vilket råkar sammanfalla med event.target i ett enkelt exempel som ovan, men blir fel så fort samma kod ligger som en metod i en klass: då vill man normalt att this ska peka på klassinstansen, inte på det klickade elementet. Historiskt löstes det antingen genom att spara undan this i en variabel (const that = this, ett mönster som inte längre behövs) eller genom .bind(this), som skapar en ny så kallad exotic function med this omdirigerat. Bind-varianten har en egen fälla: den skapade bundna funktionen är en annan referens än originalfunktionen, vilket gör den svårare att ta bort med removeEventListener om man inte sparar undan den bundna versionen separat.
Arrow functions löser hela problemet enklare: de binder aldrig om this alls, utan ärver this från den omgivande koden automatiskt – exakt det beteende man annars var tvungen att uppnå manuellt med bind. Det är därför arrow functions är det rekommenderade valet för händelselyssnare idag.
Closures-fällan i loopar
Section titled “Closures-fällan i loopar”Ett klassiskt misstag är att koppla händelselyssnare i en vanlig for-loop och förvänta sig att varje lyssnare ska komma ihåg “sitt eget” loop-index:
const aTags = document.querySelectorAll('#links a')
for (var i = 0; i < aTags.length; i += 1) { aTags[i].addEventListener('click', (event) => { console.log(i) // blir alltid längden på listan, oavsett vilken länk som klickades })}Orsaken är closures: den inre funktionen refererar till samma yttre variabel i, och när i väl läses (vid klick, långt senare) har loopen redan hunnit klart och i har sitt sista värde. Lösningen är att ge varje iteration sin egen variabel-instans, till exempel genom forEach (eller let istället för var i en vanlig for-loop, vilket skapar en ny bindning per varv):
const aTags = document.querySelectorAll('#links a')
aTags.forEach((a, i) => { a.addEventListener('click', (event) => { console.log(i) // korrekt: 0, 1, 2, 3... })})Event delegation
Section titled “Event delegation”Att sätta en egen lyssnare per element skalar dåligt – ett bräde med hundratals rutor (typexemplet: Minesweeper) skulle innebära hundratals separata lyssnare för webbläsaren att hålla reda på, vilket märkbart kan sakta ner sidan när antalet växer. Lösningen bygger på att händelser bubblar uppåt genom DOM-trädet: sätt istället en enda lyssnare på ett gemensamt förälderelement och läs av event.target för att avgöra vilket barn som faktiskt utlöste händelsen.
const board = document.querySelector('#gameboard')
board.addEventListener('click', (event) => { console.log(event.target) // <button id="m32"> event.currentTarget === board // true – board är det som "äger" lyssnaren})event.target är alltid det element användaren interagerade med, medan event.currentTarget är det element lyssnaren faktiskt sitter på – i vanliga fall samma element, men inte nödvändigtvis vid delegation.
Propagation: capture och bubble
Section titled “Propagation: capture och bubble”En händelse rör sig i två faser genom DOM-trädet: först en capture-fas (uppifrån och ner, från document mot målelementet) och sedan en bubble-fas (nerifrån och upp igen). addEventListener lyssnar som standard i bubble-fasen; för att fånga händelsen redan i capture-fasen skickas ett tredje argument med { capture: true } (att skicka in true direkt istället för ett options-objekt fungerar också men är svårare att läsa, och avrådd stil idag).
a.addEventListener('click', (event) => { console.log('Länken klickades!') event.stopPropagation() // stoppar fortsatt bubbling uppåt i trädet})
p.addEventListener('click', (event) => { console.log('Den här körs aldrig om a stoppar propagationen')})stopPropagation() stoppar en händelse från att fortsätta bubbla vidare uppåt (eller nedåt, om den satts i capture-fasen). Det är en annan sak än preventDefault(), som istället stoppar webbläsarens eget standardbeteende för händelsen – till exempel att en länk navigerar iväg eller att ett formulär skickas och laddar om sidan. Att glömma preventDefault() på en formulär-submit är en vanlig fälla: den egna koden hinner köra, men sedan tar webbläsaren över och laddar om sidan ändå, vilket nollställer hela skriptets exekvering.
Web Workers API
Section titled “Web Workers API”När arbete är för tungt för att köras direkt i huvudtråden utan att blockera kön kan det läggas i en separat tråd via Web Workers API. En worker måste ligga i en egen fil (går inte att peka ut en funktion i den egna modulen) och har en egen stack, en egen kö och en egen händelseloop – helt separat från huvudtrådens.
let n = 1search: while (true) { n += 1 for (let i = 2; i <= Math.sqrt(n); i += 1) { if (n % i === 0) continue search } postMessage(n)}// huvudtrådenconst worker = new window.Worker('worker.js')
worker.addEventListener('message', (event) => { document.querySelector('#pn').textContent = event.data})En worker har ingen tillgång till DOM-API:er – den kan alltså aldrig skriva direkt till sidan. Kommunikation mellan huvudtråden och en worker sker uteslutande via meddelanden: postMessage skickar, och en message-lyssnare tar emot på andra sidan. Vissa API:er, som indexedDB, är tillgängliga från båda trådarna samtidigt och har inbyggt stöd för transaktioner för att hantera att båda sidor kan försöka skriva samtidigt – samma typ av race condition-problem som samtidighetsmodellen inledningsvis illustrerade med en delad databas.
Webbkomponenter - Händelser
Section titled “Webbkomponenter - Händelser”📕 Errata
Varför händelser hör hemma i webbkomponenter
Section titled “Varför händelser hör hemma i webbkomponenter”En webbkomponent behöver ofta göra två saker med händelser: reagera på sådant som händer den utifrån (användaren klickar, drar, skriver) och kunna berätta utåt att något har hänt inuti den (data har kommit från en server, en tavla har blivit full). Det andra fallet skulle kunna lösas genom att komponenten anropar en funktion som den som använder komponenten skickat in, men det bryter mot samma princip som gör de inbyggda HTML-elementen användbara: en knapp eller ett formulär talar om att något hänt genom att utlösa en händelse, inte genom att ringa upp en specifik funktion den fått tilldelad. Webbkomponenter ska bygga vidare på det mönstret, inte hitta på ett eget.
Koppla händelser i connectedCallback, inte i konstruktorn
Section titled “Koppla händelser i connectedCallback, inte i konstruktorn”Av livscykelmetoderna (constructor, attributeChangedCallback, connectedCallback, disconnectedCallback, adoptedCallback – den sistnämnda för det ovanliga fallet att ett element flyttas mellan dokument) är connectedCallback den naturliga platsen att koppla händelsehanterare: den körs först när komponenten faktiskt sitter i DOM-trädet, vilket är exakt när det blir meningsfullt att lyssna på interaktion. Att koppla en listener i konstruktorn fungerar tekniskt, men är mindre korrekt eftersom komponenten då ännu inte finns i dokumentet – kurslitteraturen lägger visserligen koden i konstruktorn, men de flesta exempel och guider ute på webben rekommenderar connectedCallback.
connectedCallback () { this.addEventListener('mousedown', (event) => this.#onWrite()) this.addEventListener('mouseup', (event) => this.stopWriting()) this.addEventListener('mouseleave', (event) => this.stopWriting())}Lyssnarna kopplas på this – komponenten själv – inte på document, eftersom det är interaktion med just den här komponentinstansen som är intressant.
Namnge metoder efter vad de gör, inte efter händelsen
Section titled “Namnge metoder efter vad de gör, inte efter händelsen”I exemplet ovan heter metoderna stopWriting och #onWrite, inte onMouseUp/onMouseDown. Det är en medveten namngivningsprincip: en metod som heter stopWriting talar om vad komponenten gör (slutar skriva), medan en metod som heter onMouseUp bara talar om vad som utlöste den – för att förstå vad den faktiskt gör måste man läsa koden eller dokumentationen. Namnger man efter beteendet blir det också möjligt att koppla flera olika händelser (mouseup och mouseleave) till samma tydligt namngivna beteende.
Vilka av dessa metoder som ska vara privata (#onWrite) eller publika (stopWriting, clear) avgörs av om det är rimligt att kod utanför komponenten anropar dem direkt. Att skriva ska bara triggas av interaktion med komponenten, så #onWrite är privat. Att stoppa skrivandet eller rensa tavlan i förtid kan däremot vara nyttigt att kunna göra utifrån, så de är publika.
Håll händelsedetaljer borta från domänlogiken
Section titled “Håll händelsedetaljer borta från domänlogiken”Ett event-objekt har egen kontext – preventDefault(), stopPropagation(), event.detail – som inte hör hemma i en metod som calculateResult eller stopWriting. Rekommenderat mönster: lös allt som rör själva händelsen inuti pilfunktionen i connectedCallback, och skicka bara den faktiska datan vidare till en namngiven metod som inte vet att den anropades av ett event:
connectedCallback () { this.addEventListener('click', (event) => { event.stopPropagation() event.preventDefault() this.calculateResult(event.detail) // metoden tar bara emot datan den behöver })}Fördelen är dubbel: koden blir tydligare (metodnamnet beskriver ett beteende, inte en händelse), och metoden blir lättare att testa – den kräver inget mockat event-objekt, bara den data den faktiskt använder. Ett vanligt misstag är att lägga event.preventDefault() inne i en metod som onWrite istället för i själva lyssnaren – där saknar den sammanhang och gör koden svårare att följa.
disconnectedCallback – städning, inte händelsehantering
Section titled “disconnectedCallback – städning, inte händelsehantering”disconnectedCallback körs när komponenten tas bort ur DOM-trädet och är tänkt för upprensning. I inspelningen resoneras det att removeEventListener här i praktiken är onödigt i moderna webbläsare, eftersom webbläsaren redan städar bort lyssnare kopplade till ett element som försvinner ur DOM:en.
Utöver händelselyssnare behöver disconnectedCallback fortfarande städa upp sådant som lever vidare oberoende av elementet: pågående setInterval/setTimeout eller aktiva nätverksanrop som komponenten startat och som annars fortsätter köras även efter att komponenten är borta.
Egna händelser (Custom Events)
Section titled “Egna händelser (Custom Events)”För att en komponent ska kunna berätta utåt att något hänt inuti den, utan att känna till vem som lyssnar eller vad de ska göra, skapas och utlöses en egen händelse:
const filledEvent = new window.CustomEvent('filled', { detail: someValue })this.dispatchEvent(filledEvent)dispatchEvent utlöser händelsen på komponenten precis som webbläsaren utlöser click eller mouseup på inbyggda element – den som lyssnar utifrån gör det på exakt samma sätt som för vilken annan händelse som helst:
document.querySelector('bart-board').addEventListener('filled', (event) => { event.target.clear()})Egen data skickas med via andra argumentets detail-egenskap – namnet är fast, det går inte att kalla den något annat. detail kan vara ett enkelt värde eller en referens till ett helt objekt om mer information behöver följa med.
Poängen med mönstret är avkoppling: komponenten ska aldrig behöva känna till vilka andra delar av applikationen som bryr sig om att den blivit full, fått ett klick eller liknande – den bara utlöser händelsen och talar om vad som hänt, och andra komponenter får själva avgöra om och hur de reagerar. Motsatsen – att komponenten själv anropar specifika funktioner utifrån – skapar beroenden i fel riktning och gör komponenten svårare att återanvända.
Kursvecka 12 - Async/Await
Section titled “Kursvecka 12 - Async/Await”Ett dataöverföringsformat, inte ett programspråk
Section titled “Ett dataöverföringsformat, inte ett programspråk”JSON (JavaScript Object Notation) är det format två datorer använder för att skicka strukturerad data mellan sig – upptäckt (inte uppfunnet, enligt honom själv) av Douglas Crockford för omkring femton år sedan och sedan dess en helt öppen standard (RFC 4627, beskriven på json.org). Filändelsen är .json, MIME-typen application/json.
Det närmaste släktingen är XML, som delar HTML:s grundprinciper (taggar, element, attribut) men blir pratigt att skriva. JSON:s styrka är att det bryts ner till det mest grundläggande alla programspråk redan hanterar – tal, strängar, booleaner, samlingar, objekt – istället för att försöka stödja allt som finns i alla språk. Resultatet är ett format som i praktiken bara är text, vilket gör det både enkelt att generera och enkelt att tolka oavsett vilket programspråk eller operativsystem som sitter i andra änden.
Syntax
Section titled “Syntax”{ "name": "Ellen Nu", "city": "Kalmar", "age": 24, "registered": true}Skillnaden mot ett vanligt JavaScript-objektlitteral: alla nycklar måste ha dubbla citationstecken, till skillnad från JavaScript där nyckelnamn utan specialtecken kan skrivas utan citationstecken. Anledningen är att JSON ska fungera i språk där ett ord som är giltigt som nyckel i JavaScript kan råka vara ett reserverat ord i något annat språk – att alltid citera nycklar som strängar undviker den kollisionen helt. Tal skrivs utan citationstecken, booleaner (true/false) likaså. Giltiga JSON-datatyper är string, number, boolean, null, object och array – arrayer skrivs exakt som i JavaScript, bara med JSON-objekt eller andra värden som innehåll.
Var man stöter på det
Section titled “Var man stöter på det”Två vanliga sammanhang: konfigurationsfiler (det är därför package.json heter just .json – rent förklaringsmässigt en konfigurationsfil för npm) och, betydligt vanligare, svar från API-anrop. Anropar man ett webb-API idag är det närmast standard att få tillbaka JSON – XML förekommer fortfarande men är sällsynt utan motsvarande JSON-alternativ. Rådata från ett API-anrop kommer som en enda lång textrad utan radbrytningar (för att inte slösa bandbredd på formattering); verktyg som webbläsarens egen JSON-preview i nätverksfliken eller fristående validerare (JSONLint och liknande) formaterar om den till läsbar form. Att skriva JSON för hand är sällsynt – det är i praktiken något man tar emot, inte något man skapar direkt.
Från text till JavaScript-objekt och tillbaka
Section titled “Från text till JavaScript-objekt och tillbaka”const json = JSON.stringify(students) // JavaScript -> JSON-text (serialisering)const parsed = JSON.parse(json) // JSON-text -> JavaScript-objektJSON.stringify serialiserar ett JavaScript-värde till en textsträng – bra namngivning här spelar roll: variabeln som håller resultatet av stringify bör beskriva att den är JSON-text, medan variabeln som håller resultatet av parse bör namnges efter vad datan faktiskt föreställer (students, inte json) snarare än efter formatet den kom ifrån. Bara egentliga data-egenskaper (primitiver, arrayer, objekt) serialiseras – metoder/funktioner på ett objekt tas helt enkelt bort ur resultatet, de orsakar inget fel.
Statiska .json-filer kan också läsas in direkt med import i en modul (import students from './students.json'), men det är (2021) ett webbläsarspecifikt beteende – stöd i Node.js var vid tidpunkten fortfarande experimentellt bakom flaggor. Kontrollera aktuellt läge om detta är relevant, då stödet för JSON-import i Node har fortsatt utvecklas sedan dess.
JSON.parse är farligare än JSON.stringify
Section titled “JSON.parse är farligare än JSON.stringify”JSON.stringify misslyckas i praktiken nästan aldrig – körs den på vanlig JavaScript-data blir resultatet alltid en giltig sträng. JSON.parse däremot bör alltid omges av try/catch så fort datan kommer utifrån och inte är fullt kontrollerad. Ett vanligt scenario: API:et som anropas har kraschat och svarar med en vanlig HTML-felsida (till exempel en 404-sida) istället för JSON – JSON.parse får då text den inte kan tolka och kastar ett undantag. Kommer datan från en egen, känd fil (som ett import) är risken förstås mindre, men så fort data hämtas över nätverket bör man räkna med att tolkningen kan misslyckas.
Synkront – och därmed blockerande
Section titled “Synkront – och därmed blockerande”Både JSON.stringify och JSON.parse är synkrona funktionsanrop: de kör klart hela sitt arbete innan nästa rad kod exekveras, oavsett hur stor datamängden är. I de allra flesta fall spelar det ingen roll, men principiellt blockerar de – en mycket stor textsträng tar mätbart längre tid att parsa, och under den tiden står resten av programmet still. I praktiken är det sällan parse/stringify själva som blir flaskhalsen, utan snarare nätverksanropet som hämtat datan i första hand – ett tecken på att man kanske bör hämta data i mindre bitar (paginering) istället för allt på en gång. Detta är också den naturliga övergången till nästa ämne: eftersom nätverksanrop tar tid (även små anrop tar tiotals millisekunder) går de inte att göra synkront på samma sätt – de kräver en asynkron modell.
Kommunikation från Webbläsaren
Section titled “Kommunikation från Webbläsaren”Varför behöver webbläsaren prata med servern efter att sidan laddats?
Section titled “Varför behöver webbläsaren prata med servern efter att sidan laddats?”En traditionell webbsida kräver en omladdning för att visa ny information – det är därför gamla kurshemsidor (byggda med t.ex. Gatsby) behöver laddas om manuellt för att visa en ny föreläsning. En applikation som Slack fungerar annorlunda: nya meddelanden dyker upp utan att sidan laddas om, vilket kräver att klienten hela tiden kommunicerar med en server i bakgrunden och hämtar (eller tar emot) ny data. Webbläsaren har flera API:er för den typen av kommunikation – XMLHttpRequest (den äldre, ursprungliga tekniken), fetch (den moderna efterträdaren, kursens fokus framöver), WebSocket (för tvåvägs realtidskommunikation, se ett senare avsnitt) och dynamiska import()-anrop för att ladda JavaScript-kod efter att sidan redan startat.
HTTP: protokollet bakom kommunikationen
Section titled “HTTP: protokollet bakom kommunikationen”HTTP (Hypertext Transfer Protocol) är samma protokoll som används för att hämta den allra första index.html, och det är också protokollet fetch och XMLHttpRequest bygger på för att prata med en server. Standarden definierar ett antal metoder (verb): GET och POST är de vanligaste och de enda som stöds direkt av HTML-formulär (<form method="...">), men det finns fler – PUT för att helt ersätta en befintlig resurs, PATCH för att uppdatera den partiellt, och DELETE för att ta bort den. Att skicka rätt verb till en server är ett sätt att slippa uppfinna ett eget litet protokoll för varje typ av operation – servern förväntas tolka en DELETE-förfrågan som “ta bort den här resursen” utan vidare förklaring.
GET /theserverpage/?user=Ellen&message=Hello HTTP/1.1Host: lnuserver.seVid en GET skickas eventuell data som en query-sträng i URL:en (samma sätt som ett HTML-formulär med method="get" bygger sin URL). Vid en POST skickas datan istället i förfrågans body:
POST /theserverpage/ HTTP/1.1Host: lnuserver.seContent-Length: 24
user=Ellen&message=HelloPOST är inte i sig en säker transportmetod – all okrypterad HTTP-trafik kan avlyssnas av den som sitter mellan klient och server. Skillnaden mot GET är enbart att datan inte hamnar synlig i URL:en (t.ex. i webbläsarens historik eller om någon tittar över axeln), inte att den blir krypterad eller skyddad. Riktig säkerhet kräver HTTPS, oavsett vilket HTTP-verb som används.
Statuskoder – lita aldrig blint på att “det gick igenom”
Section titled “Statuskoder – lita aldrig blint på att “det gick igenom””Serverns svar innehåller alltid en statuskod, och den säger mer om vad som faktiskt hände än innehållet i svaret gör. Grovt indelat:
- 2xx – lyckades (
200 OKär standardfallet) - 3xx – omdirigering (
301 Moved Permanently,307 Temporary Redirect) - 4xx – felet ligger hos klienten (
400 Bad Request,401 Unauthorized,403 Forbidden,404 Not Found) - 5xx – felet ligger hos servern (
500 Internal Server Error,503 Service Unavailable)
Ur ett API-designperspektiv är detta ett argument för att vara frikostig med rätt statuskoder när man själv bygger en server längre fram i utbildningen – utan att för den sakens skull läcka onödig intern information i felmeddelanden (t.ex. i ett 500-svar), vilket är värdefull information för den som vill attackera applikationen.
AJAX: teknikens historia
Section titled “AJAX: teknikens historia”Termen AJAX (Asynchronous JavaScript and XML) myntades 2005 av Jesse James Garrett i en inflytelserik artikel som beskrev en teknik Microsoft redan byggt in i Internet Explorer 5 (1999) – ursprungligen som en proprietär ActiveX-komponent, XMLHttpRequest. Fram till dess var webben i praktiken statisk: dynamiskt innehåll byggdes med hackiga lösningar som periodiskt omladdade <iframe>-element, Flash eller Java-applets. Mozilla, Safari och Opera implementerade XMLHttpRequest som en öppen standard runt millennieskiftet, Microsoft följde efter i Internet Explorer 7 (2006), och artikeln 2005 fick utvecklare i stort att inse att webbapplikationer kunde bygga vidare på den här tekniken – något som i förlängningen bidrog till att JavaScript gick från att ses som ett marginellt “blinkande text”-språk till dagens ställning, och en bidragande orsak till att Flash så småningom blev överflödigt.
Runt 2015 fick webbläsarna brett stöd för fetch, ett modernare och enklare API för samma typ av nätverkskommunikation. XMLHttpRequest finns fortfarande kvar och fungerar, men fetch är det API kursen fokuserar på framöver.
Asynkron programmering - Introduktion
Section titled “Asynkron programmering - Introduktion”Synkront och asynkront – inget nytt koncept
Section titled “Synkront och asynkront – inget nytt koncept”Skillnaden mellan synkront och asynkront är inte unik för programmering. Ett videosamtal är synkront: båda parter är närvarande och svarar varandra i realtid. Att titta på en inspelad föreläsning är asynkront: den kan pausas, skjutas upp och återupptas helt oberoende av när den spelades in. Mejl och chattmeddelanden räknas på samma sätt som asynkrona – man väljer själv när man svarar – medan ett telefonsamtal är synkront, det förväntas ett svar direkt. Samma distinktion gäller kod: synkron kod körs i strikt sekvens, en sats i taget, och väntar in varje anrop innan nästa körs; asynkron kod skjuter upp delar av arbetet till senare, utan att blockera resten av programmet under tiden.
En ny pusselbit: webbläsarens API:er
Section titled “En ny pusselbit: webbläsarens API:er”Utöver stacken och kön (task queue) finns en tredje del i bilden: webbläsaren har inbyggda API:er som kan utföra arbete – till exempel nätverksanrop – helt utanför vår egen stack. Det är den resursen som gör asynkron kod möjlig: webbläsaren kan jobba i bakgrunden med sådant som tar tid, och lägga en task i kön först när arbetet är klart.
Synkront nätverksanrop – den gamla, förbjudna vägen
Section titled “Synkront nätverksanrop – den gamla, förbjudna vägen”const req = new window.XMLHttpRequest()req.open('GET', 'students.json', false) // false = synkrontreq.send()const json = JSON.parse(req.responseText)XMLHttpRequest är den ursprungliga tekniken för nätverksanrop i webbläsaren – inte något som används aktivt i den här kursen, men värt att känna igen om man stöter på det i äldre kod. Sätts tredje argumentet till open till false blir anropet synkront: send() returnerar först när svaret faktiskt har kommit tillbaka, vilket kan ta allt från några millisekunder till flera sekunder. Under hela den tiden står webbläsarens gränssnitt still – inget annat kan hända. Synkrona XMLHttpRequest-anrop är numera förbjudna i moderna webbläsare av precis den anledningen.
Asynkront nätverksanrop – callback-mönstret
Section titled “Asynkront nätverksanrop – callback-mönstret”const req = new window.XMLHttpRequest()req.addEventListener('load', function () { console.log(req.responseText)})req.open('GET', 'students.json')req.send()Här registreras en händelselyssnare för load innan send() anropas. send() returnerar nu omedelbart – allt den egentligen gör är att tala om för webbläsaren vad som ska hämtas, sedan är stacken fri igen. Webbläsaren sköter själva nätverksanropet i bakgrunden (i den nya API-rutan, inte på stacken), och när svaret kommer tillbaka läggs en task i kön som anropar den registrerade funktionen. Det här är samma callback-mönster som händelselyssnare i allmänhet – ingenting konstigare än att vänta på att en användare klickar på en knapp.
En viktig garanti: kod som redan ligger och kör på stacken körs alltid klart innan någon köad callback-task får köra. Det spelar ingen roll om svaret kommer tillbaka blixtsnabbt (till exempel för att webbläsaren cachat filen) – en callback kan aldrig avbryta pågående synkron kod, den måste vänta tills stacken är tom. Det man däremot förlorar med asynkron kod är den självklara läsordningen: koden exekveras inte längre uppifrån och ned i en enda följd, utan i den ordning som stacken respektive kön faktiskt hanterar den.
Effekten på webbläsarens gränssnitt
Section titled “Effekten på webbläsarens gränssnitt”Ritas de två varianterna upp på en tidslinje syns skillnaden tydligt: med blockerande (synkron) kod upptas stora sammanhängande delar av tidslinjen av väntande anrop, och användargränssnittet fryser under hela den tiden. Med icke-blockerande (asynkron) kod är stacken bara upptagen en kort stund i taget – för att förbereda och skicka iväg anropet – medan resten av tiden är fri för annan kod (och för webbläsarens eget gränssnittsarbete) att köra. Flera anrop kan till och med förberedas och köras parallellt i bakgrunden på det här sättet.
JavaScript är entrådigt – webbläsaren är det inte
Section titled “JavaScript är entrådigt – webbläsaren är det inte”Att JavaScript är entrådigt syftar på att det bara finns en stack och en kö för vår egen kod – det finns bara en plats där vår kod kan exekveras åt gången. Webbläsaren själv har dock möjlighet att använda egna trådar och göra arbete parallellt i bakgrunden; som utvecklare kan man beställa den typen av arbete (nätverksanrop, timers) men inte skapa egna trådar direkt – med undantag för Web Workers, ett separat API som tas upp senare i kursen.
Detta är grunden för async/await och Promises, som byggs vidare på i nästa del.
Asynkron programmering - Promises och Async/Await
Section titled “Asynkron programmering - Promises och Async/Await”Formulär som progressive enhancement
Section titled “Formulär som progressive enhancement”Innan nätverksanrop med JavaScript tas upp på allvar är ett kort råd värt att ta med sig: bygg gärna vidare på vanliga HTML-formulär (method, action) istället för att helt ersätta dem. Ett formulär som fungerar utan JavaScript, där koden sedan läser av method/action och gör anropet i bakgrunden utan att sidan laddas om, är en robustare grund än att bygga nätverkslogiken helt frikopplad från formuläret. Lyssna på formulärets submit-händelse (form.addEventListener('submit', ...)) snarare än på ett klick på submit-knappen – ett formulär kan skickas på fler sätt än ett musklick. Och inaktivera (disable) submit-knappen så fort den klickats: annars är det lätt hänt att en användare som inte får omedelbar respons klickar igen, och igen, vilket i värsta fall kan trigga samma serveranrop flera gånger (t.ex. en dubbel beställning).
Fetch API utan async/await
Section titled “Fetch API utan async/await”fetch är webbläsarens moderna, standardiserade API för nätverksanrop – efterträdaren till XMLHttpRequest (se föregående avsnitt). Det tar en resurs som första argument och ett konfigurationsobjekt som andra – ett vanligt mönster i nyare API:er, istället för att lista många enskilda argument.
const data = { id: 54322575, name: 'Johan' }
window.fetch('http://url-to-fetch.at/path', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data)}).then(res => { // hantera svaret här}).catch(error => { // hantera fel här})fetch returnerar omedelbart ett Promise – ett löfte om att leverera ett resultat vid ett senare tillfälle (samma princip som ett banklöfte om ett lån: inte pengarna direkt, men ett löfte om att de kommer). Promiset har metoderna then (körs när löftet löser ut framgångsrikt) och catch (körs om något gick fel), vilket i grunden är samma idé som callback-mönstret fast standardiserat runt en gemensam typ. Kedjar man flera anrop efter varandra med upprepade .then() växer koden snabbt i komplexitet – ett problem som brukar kallas promise hell, samma typ av läsbarhetsproblem som callback hell för den äldre callback-stilen.
Async/await – samma sak, mycket mer läsbart
Section titled “Async/await – samma sak, mycket mer läsbart”async/await är syntax ovanpå Promises, inte en ny mekanism – koden nedan gör exakt samma sak som exemplet ovan, bara mer likt synkron kod att läsa:
async function postData () { const data = { id: 54322575, name: 'Johan' }
const res = await window.fetch('http://url-to-fetch.at/path', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) })}await kräver att koden befinner sig i en asynkron kontext – vanligtvis en funktion markerad med async framför (det går numera även att använda await direkt på modulnivå, “top-level await”, en nyhet i standarden från 2021 – men i vanliga funktioner krävs fortfarande async-nyckelordet). await pausar exekveringen av just den funktionen tills promiset löser ut, utan att blockera resten av kön – webbläsaren är fri att göra annat under tiden, precis som med alla andra köade uppgifter.
Felhantering vid await sker med vanlig try/catch runt anropet – inget nytt behövs utöver det befintliga språkkonstruktet.
Ett vanligt misstag är att glömma async framför en funktion som innehåller await. Att sätta async “för säkerhets skull” på alla metoder är dock inte att rekommendera – det signalerar till läsaren att metoden gör asynkront arbete, vilket blir missvisande om den inte faktiskt gör det. Skriv istället koden naturligt, och låt ett fel om att await bara får användas i en asynkron kontext påminna om det saknade async-nyckelordet.
Ett praktiskt exempel: sök efter tv-serier
Section titled “Ett praktiskt exempel: sök efter tv-serier”Exemplet i föreläsningen är en webbkomponent som söker fram tv-serier från ett tänkt API. Ett första sökanrop returnerar bara id:n på träffarna, och varje id behöver sedan slås upp separat för mer information:
async getShow (id) { const res = await window.fetch(`http://tv-api.se/shows/${id}`) return res.json()}
async searchUpdate (str) { const res = await window.fetch(`http://tv-api.se/shows/?q=${str}`) const data = await res.json() // { searchString: 'prison', ids: [24] } this.show = await this.getShow(data.ids[0]) // Prison Break}Notera att getShow returnerar res.json() direkt utan ett eget await framför – eftersom funktionen är async returnerar den ändå alltid ett Promise, och await this.getShow(...) i anropande kod väntar korrekt in det yttersta löftet oavsett hur många lager av Promises som ligger i vägen.
Fällan: sekventiella anrop i en loop
Section titled “Fällan: sekventiella anrop i en loop”Har sökningen flera träffar (ids: [24, 35]) räcker det inte att bara slå upp det första – samtliga behöver hämtas. Ett naturligt första försök är en vanlig loop:
this.shows = []for (const id of data.ids) { this.shows.push(await this.getShow(id))}Koden fungerar, men blir i praktiken synkron: varje await i loopen väntar in hela anropet innan nästa startar, trots att anropen inte alls är beroende av varandra. Det är samma princip som att beställa varor på nätet en produkt i taget och vänta på leverans mellan varje beställning istället för att lägga en enda samlad beställning – onödigt långsamt när arbetet lika gärna kan ske parallellt.
Parallellisera med Promise.all
Section titled “Parallellisera med Promise.all”Genom att inte awaita getShow direkt i loopen, utan istället samla ihop alla Promises i en array, kan alla anrop startas samtidigt:
async searchUpdate (str) { const res = await window.fetch(`http://tv-api.se/shows/?q=${str}`) const data = await res.json() // { searchString: 'break', ids: [24, 35] }
const showPromises = data.ids.map(id => this.getShow(id)) this.shows = await Promise.all(showPromises)}data.ids.map(id => this.getShow(id)) startar samtliga fetch-anrop direkt (webbläsaren sköter dem parallellt i bakgrunden) och returnerar en array av Promises, inte färdiga resultat – getShow är async och returnerar alltid ett Promise, oavsett om det awaitas direkt eller inte. Promise.all tar den arrayen och returnerar i sin tur ett nytt Promise som löser ut först när samtliga i arrayen är klara, med en array av alla resultat i samma ordning. Resultatet blir kod som ser nästan lika enkel ut som den sekventiella varianten, men som utnyttjar att anropen kan köras samtidigt istället för i tur och ordning.
Promise.race är en besläktad metod värd att känna till: den löser ut så fort det första av flera Promises blir klart, användbart om man bara bryr sig om det snabbaste svaret av flera möjliga källor. Den är betydligt mindre använd i praktiken än Promise.all.
Att koppla ihop det med en händelsehanterare
Section titled “Att koppla ihop det med en händelsehanterare”Själva sökningen triggas av en klickhändelse, och händelsehanteraren behöver själv vara async för att kunna awaita resultatet innan gränssnittet uppdateras:
connectedCallback () { this.#searchButton.addEventListener('click', async (event) => { await this.searchUpdate(this.#input.value) this.#updateRendering() })}Att utelämna await här skulle innebära att #updateRendering() körs innan sökresultatet faktiskt kommit tillbaka – gränssnittet skulle då visa förra sökningens resultat, och det nya resultatet skulle inte synas förrän användaren sökte igen. De flesta nyare webbläsar-API:er (Web USB, Service Workers med flera) bygger genomgående på Promises, så förståelsen för async/await är inte begränsad till nätverksanrop – den är en förutsättning för att kunna använda stora delar av plattformens moderna funktionalitet.
Cross Origin Resource Charing (CORS)
Section titled “Cross Origin Resource Charing (CORS)”En sandlåda i webbläsaren
Section titled “En sandlåda i webbläsaren”Cross-Origin Resource Sharing (CORS) är en säkerhetsmekanism inbyggd i webbläsaren, inte på servern – den finns till för att en webbsida inte fritt ska kunna göra skriptdrivna anrop mot vilken annan domän som helst. Den första GET-förfrågan en sida gör (mot huvuddokumentet) sätter sidans origin – domänen sidan anses tillhöra. Resurser som .css, bilder och typsnitt kan i regel hämtas från andra domäner utan problem (till exempel ett typsnitt eller en bild från ett CDN), men försöker JavaScript göra ett nätverksanrop (till exempel via fetch) mot en annan domän än sidans egen origin, kontrolleras det anropet av CORS.
Preflight och felmeddelandet som gömmer sig
Section titled “Preflight och felmeddelandet som gömmer sig”Innan webbläsaren släpper igenom ett sådant korsdomänanrop skickar den i praktiken en förfrågan till målservern – en preflight – och frågar om det är okej. Servern svarar med särskilda CORS-headrar (Access-Control-Allow-Origin med flera) som antingen tillåter alla, en specifik lista av domäner, eller ingen alls. Tillåter servern inte anropet blockerar webbläsaren det – och av säkerhetsskäl får JavaScript inte ens läsa ut varför anropet misslyckades. Skriptet vet bara att något gick fel, inget mer; annars skulle ett skadligt skript kunna använda felmeddelandet för att kartlägga vilka domäner och resurser som finns tillgängliga.
Felet syns bara i webbläsarens devtools-konsol, aldrig i själva JavaScript-koden, och har ett typiskt utseende att känna igen:
Failed to load ...: No 'Access-Control-Allow-Origin' header is present on therequested resource. Origin 'http://localhost:4000' is therefore not allowed access.Så fort ordet CORS dyker upp i en sådan konsolrad är det ett tecken att stanna upp: det handlar inte om ett fel i den egna koden, utan om att målservern inte tillåter anrop från den egna domänen. Om anropet fungerar eller inte beror helt på hur målservern är konfigurerad – många publika API:er (till exempel GitHub) tillåter anrop från alla domäner, medan andra bara tillåter sina egna.
Två sätt att lösa det
Section titled “Två sätt att lösa det”- Lägg till CORS-headrar på servern som anropas – fungerar bara om man själv äger och kan konfigurera den servern.
- Använd sin egen server som proxy – CORS är en begränsning i webbläsaren, inte mellan servrar. Går det inte att styra över målservern går anropet istället via den egna servern: klienten anropar den egna servern, som i sin tur (utan CORS-begränsning, eftersom serverkod inte körs i en webbläsare) anropar tredjepartsservern, hämtar datan och skickar tillbaka den till klienten.
Den andra lösningen är den vanligaste när man inte har kontroll över den externa tjänsten.
Kursvecka 13 - Komposition
Section titled “Kursvecka 13 - Komposition”Komposition av komponenter
Section titled “Komposition av komponenter”Presentation | Exempel - Todo Application
📕 Errata
- Introduktion
- Vad är en bra komponent Niklas?
- Komposition av komponenter
- Exempel - Todo application
- Designregler - Bubbling av event (se informationstext nedan)
- Nestling av komponenter
- Nestling av komponenter i normala DOMen
- Nestling av komponenter i shadow DOMen
- Designregler - Härma de inbyggda elementen
- Designregler - Konfigurering via attribut
- Designregler - Inkapsling
- Orkestrering - Kommunikation mellan komponenter
- Case study - Flygkalkylatorn
- Designregler - Fallera tyst
- Lyssnar på förändring med “slotchange”
- Avslutning
Vad kännetecknar en bra komponent?
Section titled “Vad kännetecknar en bra komponent?”En bra komponent är liten, gör en enda sak och har så få externa beroenden som möjligt – den vet så lite som möjligt om resten av applikationen. En hel applikation (till exempel Spotify) är inte en komponent, det är en applikation byggd av många komponenter. Samtidigt finns det knappast en undre gräns: en komponent kan i princip vara lika liten som ett enda tecken (en emoji-komponent nämns som exempel). Den vanliga fällan är inte att ha för många komponenter, utan tvärtom för få – att av bekvämlighet lägga för mycket ansvar i en och samma komponent istället för att bryta ut den, på samma sätt som upprepad kod ofta borde brytas ut till en funktion.
Frågan den här föreläsningen egentligen kretsar kring är inte hur man skriver en bra komponent, utan hur flera välavgränsade komponenter ska samverka och kommunicera i en större applikation – komposition av komponenter.
Två sätt att nästla komponenter
Section titled “Två sätt att nästla komponenter”Vanlig nästling (light DOM): barnelement skrivs rakt in i föräldraelementet i HTML, precis som en <ul> med <li>-element. Föräldrakomponenten styr bara var de här barnen visas i sin shadow-DOM, med hjälp av <slot>. Den som använder komponenten har fullt utifrån-kontroll över vilka barn som läggs till, tas bort eller ändras – exakt som med vanlig HTML.
Nästling genom shadow-DOM: föräldrakomponenten skapar och äger sina barnelement själv, inuti sin egen shadow-root (till exempel genom att importera och instansiera dem i sin template.innerHTML). Barnen är då helt dolda och otillgängliga för den som använder föräldrakomponenten utifrån – att lägga till eller ändra dem är förälderns ansvar, inte konsumentens.
Vilket mönster som passar beror på vad komponenten ska vara: en lista där konsumenten själv ska kunna lägga till och ta bort poster (light DOM, som en to-do-list med to-do-item-barn) eller en komponent med intern, komplex uppbyggnad som konsumenten aldrig ska behöva bry sig om (till exempel en videospelare med inbyggda kontroller). De två mönstren kan också kombineras i samma komponent.
Praktiskt spelar det roll för hur man väljer ut element i koden:
// Barn i light DOM (t.ex. slottade to-do-items) – vanlig queryn på thisthis.querySelectorAll('to-do-item')
// Barn i shadow-DOM (t.ex. interna delar av videospelaren) – via shadowRootthis.shadowRoot.querySelectorAll('element-b')document.querySelectorAll(...) ska aldrig användas för det här – då söks hela sidans DOM igenom, vilket både ger fel resultat om komponenten finns flera gånger på sidan och skapar ett dolt beroende till resten av dokumentet.
Efterlikna inbyggda HTML-element
Section titled “Efterlikna inbyggda HTML-element”Grundprincipen för hur en komponent bör kommunicera: efterlikna hur riktiga HTML-element redan fungerar.
- In i komponenten: konfigurera via attribut (samma sätt som
srcpå en<img>). Det här täcker ungefär 99 % av fallen. - Ut från komponenten: kommunicera genom att utlösa händelser (
dispatchEvent), samma sätt ett formulär talar om att det skickats iväg. - Publika metoder med returvärden är ett sekundärt, mer sällan använt alternativ – används framför allt när datan som ska skickas in är för komplex för ett attribut (arrayer, objekt) eller när komponenten behöver initieras i flera steg (en
initialize-metod som till exempel visar en laddningsanimation tills den är klar).
Ett viktigt tekniskt villkor för attribut: de är alltid strängar, oavsett vilket värde som skrivs i HTML-koden. Sätts time="100" måste komponenten själv parsa strängen till ett tal innan den kan användas som ett. Bara primitiva datatyper (tal, strängar, booleaner) hör hemma i attribut – att JSON.stringify:a ett helt objekt och skjuta in det som en sträng i ett attribut fungerar tekniskt, men är ett fulhack som bör undvikas; hör datan till den kategorin (arrayer, objekt) är en publik metod rätt väg istället.
Håll komponenten inkapslad
Section titled “Håll komponenten inkapslad”En komponent ska bara bry sig om det som finns inuti sig själv – aldrig sträcka sig ut i det vanliga dokumentet för att läsa eller ändra saker där. document.querySelector(...) inuti en komponents egen kod är ett tydligt varningstecken (ett bra sätt att code-reviewa detta: sök filerna efter document. – dyker det upp inuti en komponentklass bör man fråga sig varför). Ska en nod skapas används förvisso document.createElement(...) (det finns ingen this.createElement), men den skapade noden ska alltid läggas till inuti komponentens eget träd (this eller this.shadowRoot), aldrig skjutas ut någon annanstans i dokumentet.
Det enda vedertagna undantaget är <head> – att en komponent i undantagsfall lägger till en resurs där (till exempel ett externt skript den beror på) kan vara motiverat, men bör vägas mot alternativ som en dynamisk import() eller fetch av samma resurs, som inte kräver att komponenten petar i det globala dokumentet.
bubbles och composed – var går gränsen för en händelse?
Section titled “bubbles och composed – var går gränsen för en händelse?”En komponents interna händelser bör hållas inuti komponenten – yttervärlden ska inte behöva bry sig om eller råka fånga upp händelser som egentligen hör till en inre delkomponent, inbäddad i en annan komponents shadow-DOM.
I den genomgångna to-do-list/to-do-item-koden utlöser to-do-item en stateChanged-händelse med { bubbles: true } när kryssrutan togglas, och to-do-list fångar den genom en enda händelselyssnare på sig själv (händelsedelegering) istället för en lyssnare per to-do-item. Den relevanta gränsen går vid composed, inte vid bubbles:
bubbles: truelåter händelsen bubbla uppåt i DOM-trädet som vanligt, men den stannar vid en shadow-root-gräns.to-do-itemhär är slottad – den är alltså en del av den vanliga, yttre DOM:en (light DOM) snarare än inbäddad ito-do-lists egen shadow-DOM – vilket är varför bubblingen fungerar som tänkt i det här specifika fallet.composed: trueär det som faktiskt bör undvikas: det låter händelsen även passera förbi en shadow-root-gräns, och riskerar att läcka en komponents interna händelser till kod som inte borde behöva bry sig om dem.
Standardvärdet för både bubbles och composed är false. Att sätta bubbles: true är alltså inte i sig ett brott mot inkapslingsprincipen – det är composed: true som är den egentliga risken att se upp med.
Orkestrera utifrån, låt inte komponenter anropa varandra direkt
Section titled “Orkestrera utifrån, låt inte komponenter anropa varandra direkt”Två syskonkomponenter ska aldrig anropa metoder på varandra direkt. Gör de det uppstår ett hårt beroende – komponent A kan då aldrig användas utan att komponent B också finns, precis som två HTML-element aldrig automatiskt interagerar med varandra bara för att de står bredvid varandra i koden.
Samma princip som ren funktionskomposition (funktionA() returnerar ett värde som fångas upp utanför och skickas vidare till funktionB(värde), snarare än att funktionA anropar funktionB direkt) gäller komponenter: komponent A utför sin uppgift och utlöser en händelse när den är klar. Den logik som kopplar ihop A och B – lyssnar på A:s händelse, läser eventuell data ur event.detail, och därefter sätter ett attribut eller anropar en metod på B – hör hemma i ett tredje, överordnat lager (till exempel applikationens index.js), inte i någon av komponenterna själva.
Försök misslyckas tyst
Section titled “Försök misslyckas tyst”En komponent bör i första hand undvika att kasta okontrollerade undantag ut mot sin konsument – ett fel i en enskild komponent ska helst inte krascha hela applikationen. Kod som kan misslyckas (till exempel ett fetch-anrop) omges därför gärna av try/catch.
Regeln är dock inte absolut: ibland är det motiverade att faktiskt kasta undantaget vidare, till exempel just när ett fetch-anrop misslyckas, så att yttre kod kan fånga upp felet, visa ett begripligt felmeddelande för användaren, eller försöka självläka applikationen på något sätt. Målet är alltså inte “kasta aldrig”, utan att inte låta fel propagera okontrollerat.
try { const response = await window.fetch('https://thewrongurl.com')} catch (error) { // Kod för att misslyckas graciöst}Kursvecka 14 - Lagring
Section titled “Kursvecka 14 - Lagring”Lagring i webbläsaren
Section titled “Lagring i webbläsaren”- Inledning
- Synkrona APIer / Asynkrona APIer
- Async Patterns - Callback Pattern APIs
- Async Patterns - Promisifierade APIs
- Lagen om elektronisk kommunkation (cookielagen)
- Hur lagras data i webbläsaren?
- Same Origin
- Cookies
- Web Storage API
- JSON vid lagring
- IndexDB
- Cachening
- Cache Storage API
- File System Access API
- Storage Manager API
- Utvecklarverktyget i webbläsaren för lagring
Synkrona och asynkrona API:er
Section titled “Synkrona och asynkrona API:er”Synkrona API:er blockerar tråden medan de körs – enkla att använda, “vanliga” funktioner. Asynkrona API:er blockerar inte, används för allt som kan “ta tid”, men lägger till komplexitet eftersom resultatet inte finns tillgängligt direkt. Två vanliga mönster för asynkrona API:er är callback-baserade (en funktion eller händelselyssnare anropas när resultatet är klart) och löftesbaserade (.then()/.catch(), eller await i en async-kontext med try/catch). Flera av webbläsarens lagrings-API:er skiljer sig åt just på den här punkten – vissa är synkrona, andra asynkrona.
Lagen om elektronisk kommunikation (“cookielagen”)
Section titled “Lagen om elektronisk kommunikation (“cookielagen”)”Uppgifter får bara lagras i eller hämtas från en användares terminalutrustning om användaren fått information om syftet och samtyckt till det – med undantag för lagring som krävs för att överföra ett meddelande, eller som är nödvändig för en tjänst användaren själv uttryckligen begärt. Det är bakgrunden till de samtyckesrutor för cookies som syns på i princip alla webbplatser.
Hur lagra data i webbläsaren?
Section titled “Hur lagra data i webbläsaren?”Webbläsaren erbjuder flera olika sätt att lagra data, som skiljer sig åt i hur beständiga (persistenta) och hur flyktiga (volatila) de är:
- Cookies – både persistenta och icke-persistenta varianter, max 4096 byte per cookie, max 50 cookies per domän.
- Web Storage –
localStorage(persistent) ochsessionStorage(rensas när fliken stängs). - IndexedDB – persistent, för större och mer strukturerad data.
- Cache API – lagrar nätverkssvar, ofta i kombination med en Service Worker.
- File System Access API – ger direkt åtkomst till filer på användarens dator.
Även “persistent” data i webbläsaren är i praktiken något flyktig – användaren kan när som helst byta webbläsare eller rensa lagrad data.
Same-origin
Section titled “Same-origin”En origin (ursprung) definieras av kombinationen schema (protokoll), värdnamn och port – till exempel https://gitlab.lnu.se:443. Flera av lagrings-API:erna nedan är begränsade till samma origin: kod från en origin kan inte läsa data lagrad av en annan.
Cookies
Section titled “Cookies”Cookies nås och sätts via document.cookie. Att tilldela document.cookie ett nytt värde lägger till det till de befintliga cookies, det skriver inte över dem:
console.log(document.cookie) // "gID=132446fdgv24rsdf233rdsf3f32r1d"document.cookie = 'name=Johan'console.log(document.cookie) // "gID=132446fdgv24rsdf233rdsf3f32r1d; name=Johan"Cookies används ofta för att hålla en session mot en server, och skickas automatiskt med i varje HTTP-anrop till samma domän – vilket är precis varför de har en storleksgräns.
Web Storage
Section titled “Web Storage”localStorage och sessionStorage erbjuder en enkel, synkron nyckel-värde-API (setItem, getItem, removeItem) för runt 2–10 MB data, men är inte tillgänglig från Web Workers:
window.localStorage.setItem('bgcolor', 'FF0000')let currentColor = window.localStorage.getItem('bgcolor')Eftersom lagringen bara hanterar strängar behöver mer komplexa värden serialiseras med JSON.stringify() vid lagring och JSON.parse() vid uppläsning:
const style = { bgcolor: 'FF0000', font: 'Bitter, serif' }window.localStorage.setItem('myapp-style', JSON.stringify(style))
const restoredStyle = JSON.parse(window.localStorage.getItem('myapp-style'))IndexedDB
Section titled “IndexedDB”En asynkron (callback-baserad), objektorienterad databas som lagrar nyckel-värde-par, med betydligt större kapacitet (upp mot 60 % av tillgängligt diskutrymme) och tillgänglig via Web Workers. Bygger på en transaktionsmodell som följer ACID-principerna (Atomic, Consistent, Isolated, Durable) och använder inte SQL.
Cache API
Section titled “Cache API”Lagrar par av Request- och Response-objekt – med andra ord nätverkssvar, inte godtycklig data:
const cache = await caches.open('my-cache')await cache.add('/images/flower.png')
const response = await cache.match('/images/flower.png')await cache.delete('/images/flower.png')Används ofta tillsammans med en Service Worker för att låta en applikation fungera offline eller snabbare vid upprepade besök.
File System Access API och Storage Manager API
Section titled “File System Access API och Storage Manager API”File System Access API låter en applikation läsa och skriva filer direkt på användarens dator, men kräver en säker kontext (HTTPS) och måste anropas inifrån en användargest (till exempel ett klick):
butOpenFile.addEventListener('click', async () => { [fileHandle] = await window.showOpenFilePicker() const file = await fileHandle.getFile() const contents = await file.text()})Storage Manager API (navigator.storage.estimate()) ger information om hur mycket lagringsutrymme som är tillgängligt och redan använt – relevant eftersom webbläsaren annars kan kasta ett QuotaExceededError-undantag om lagringsutrymmet tar slut.
Allt som lagrats syns samlat under fliken “Application” i webbläsarens utvecklarverktyg – Local/Session Storage, IndexedDB, Cache Storage och Cookies, per origin.
Kursvecka 15 - Installerbara PWAs
Section titled “Kursvecka 15 - Installerbara PWAs”Webbapplikationer och introduktion till PWA
Section titled “Webbapplikationer och introduktion till PWA”Single Page Application
Section titled “Single Page Application”En Single Page Application (SPA) är en webbapplikation eller webbplats som ryms på en enda sida, med målet att ge en mer flytande användarupplevelse som liknar en skrivbordsapplikation – istället för att en helt ny sida laddas för varje interaktion.
Offline first
Section titled “Offline first”Ett tankesätt: sluta behandla avsaknad av uppkoppling som ett fel. En applikation bör kunna hantera avbrutna anslutningar och fortsätta fungera så smidigt som möjligt, snarare än att bara krascha eller visa en tom sida så fort nätverket försvinner.
Applikationstyper
Section titled “Applikationstyper”Fyra huvudsakliga sätt att bygga en applikation för webb och mobil:
- Responsiv webbsida – en vanlig webbplats som anpassar sig efter skärmstorlek.
- Progressiv webbapplikation (PWA) – webbteknik som ändå kan installeras och fungera offline.
- Hybridapp – skriven i JavaScript/HTML/CSS men kompilerad till native kod.
- Native app – skriven direkt i plattformens eget språk.
Native apps
Section titled “Native apps”Native applikationer var där allt en gång började, skrivna i språk som Java, C++, Swift eller Kotlin – ett separat kodspråk och en separat kodbas för varje plattform (iOS, Android, Windows, macOS).
Hybrida mobilappar
Section titled “Hybrida mobilappar”Ramverk som React Native, Flutter och Xamarin låter en utvecklare skriva applikationen en gång (ofta i JavaScript, HTML och CSS) och sedan generera native kod för respektive plattform. Instagram, Facebook och Discord är exempel på appar byggda på det sättet. På desktop fyller Electron samma roll – det paketerar en webbapplikation (HTML/CSS/JS) till en fristående skrivbordsapplikation. Slack, Messenger, Twitch och VS Code är alla i grunden Electron-applikationer.
Progressiva webbapplikationer (PWA)
Section titled “Progressiva webbapplikationer (PWA)”En PWA kräver en säker kontext (HTTPS, med localhost som undantag under utveckling) och gör tung användning av löften (promises). Det är snarare ett tankesätt och en kombination av tekniker än en enskild teknik i sig, och fältet är fortfarande relativt ungt – stödet skiljer sig åt mellan Android och iOS. Spotify är ett exempel på en känd tjänst med en PWA.
En PWA kännetecknas av flera egenskaper: progressiv, upptäckbar, länkbar, responsiv och säker, precis som en bra webbplats redan bör vara – men därutöver också:
- Installerbar – med hjälp av Service Workers och en
manifest.json. - Pålitlig – fungerar konsekvent tack vare routing/historik, offline-stöd och cachning.
- Kapabel – kan använda samma kraftfulla webb-API:er som vilken annan webbapplikation som helst.
Jämförelse mellan applikationstyperna
Section titled “Jämförelse mellan applikationstyperna”| Responsiv webbsida | PWA | Hybridapp | Native app | |
|---|---|---|---|---|
| Kodbas | En | En | En | Flera |
| Appbutik | Nej | Delvis | Ja | Ja |
| Prestanda | Ingen fördel | Viss fördel | Hög | Högst |
| API-åtkomst | Begränsad | Viss | Mycket | All |
| Snabba uppdateringar | Ja | Ja | Nej | Nej |
| Offline | Nej | Ja | Ja | Ja |
| Installerbar | Nej | Ja | Ja | Ja |
| Länkbar | Ja | Ja | Nej | Nej |
En PWA landar med andra ord mitt emellan en vanlig responsiv webbsida och en hybrid-/native app – bättre prestanda och offline-stöd än en ren webbsida, men fortfarande en enda kodbas och samma snabba uppdateringscykel som webben normalt erbjuder.
Installerbara PWAs
Section titled “Installerbara PWAs”- Introduktion
- Service Workers
- Service Workers - Möjligheter
- Service Workers - Krav
- Service Workers - Räckvidd (scope)
- Service Workers - Livscykel
- Service Workers - Livscykel - Steg 1 - Registrera en SW
- Service Workers - Livscykel - Steg 2 - Lyssna på händelser
- CacheStorage
- CacheStorage - Exempel - Cachning vid installation
- CacheStorage - Exempel - Cachning vid fetch
- CacheStorage - Exempel - Ta bort chachad data
- Service Workers i webbläsarens utvecklingsmiljö
- Installera PWAer som en applikation
- Webmanifest
- Demo - Installerbart Memory
Service Workers
Section titled “Service Workers”En service worker är en programmerbar nätverksproxy som låter en applikation styra hur nätverksanrop från sidan hanteras. Den körs i ett eget, separat scope (ServiceWorkerGlobalScope, ofta refererat till som self) – skilt från både window (den vanliga sidan) och WorkerGlobalScope (en vanlig Web Worker) – och har inte tillgång till DOM:en eller Web Storage. Den kan däremot använda Cache API, Fetch API, IndexedDB, WebSockets och liknande.
Vad en service worker möjliggör: cachning av resurser, offline-stöd, push-notiser och klientsidig lastbalansering.
En service worker kräver HTTPS (localhost undantaget under utveckling), tillåter bara asynkrona anrop, kräver goda kunskaper i löften (promises), och behöver en modern webbläsare.
Livscykel
Section titled “Livscykel”En service worker går igenom ett antal tillstånd: den installeras (installing), blir antingen activated eller hamnar i error, väntar sedan i idle tills den antingen tar emot en fetch/message-händelse eller termineras (terminated).
Registrera en service worker
Section titled “Registrera en service worker”En service worker registreras med ett scope som motsvarar var i filstrukturen den ligger placerad – för att få scope över hela sajten (./) måste filen ligga i webbplatsens rot (t.ex. public/):
if ('serviceWorker' in navigator) { window.addEventListener('load', async () => { try { const registration = await navigator.serviceWorker.register('./serviceworker.js') console.log('ServiceWorker: Registration successful with scope: ', registration.scope) } catch (error) { console.log('ServiceWorker: Registration failed: ', error) } })}'serviceWorker' in navigator är en form av “feature detection” – kontrollerar att webbläsaren stöder service workers innan registrering ens försöks.
Serviceworkerns egna händelser
Section titled “Serviceworkerns egna händelser”Själva service worker-filen lyssnar på ett antal händelser:
const version = '1.0.0'
self.addEventListener('install', event => { // TODO: Cache resources needed to start})
self.addEventListener('activate', event => { // TODO: Clean up older versions of the cache})
self.addEventListener('fetch', event => { // TODO: Cache new resources when online and serve cached content if offline})
self.addEventListener('message', event => { // TODO: Handle events from the main application})
self.addEventListener('push', event => { // TODO: Show a notification for the user})CacheStorage: cacha vid installation
Section titled “CacheStorage: cacha vid installation”CacheStorage-API:et lagrar par av Request- och Response-objekt. Ett vanligt mönster är att cacha de resurser som krävs för att appen ska starta redan när service workern installeras, och att medvetet vänta med event.waitUntil() tills det är klart:
self.addEventListener('install', event => { const cacheAssets = async () => { const cache = await self.caches.open(version) return cache.addAll(['index.html', 'css/styles.css']) }
event.waitUntil(cacheAssets())})Cacha vid nätverksanrop
Section titled “Cacha vid nätverksanrop”Ett annat mönster: försök hämta en resurs från nätverket och spara en kopia i cachen om det lyckas; misslyckas anropet (till exempel offline), returnera istället den cachade versionen:
const cachedFetch = async request => { try { const response = await fetch(request) const cache = await self.caches.open(version) cache.put(request, response.clone()) // svaret måste klonas eftersom det både returneras och cachas return response } catch (error) { return self.caches.match(request) }}
self.addEventListener('fetch', event => { event.respondWith(cachedFetch(event.request))})Städa bort gamla cache-versioner
Section titled “Städa bort gamla cache-versioner”Vid activate-händelsen är det lämpligt att ta bort cachar från tidigare versioner av applikationen, så att gammalt cachat innehåll inte ligger kvar och tar plats:
self.addEventListener('activate', event => { const removeCachedAssets = async () => { const cacheKeys = await self.caches.keys() return Promise.all( cacheKeys.map(cache => { if (cache !== version) { return self.caches.delete(cache) } return undefined }) ) }
event.waitUntil(removeCachedAssets())})Service workerns tillstånd, inklusive möjligheten att simulera offline-läge, går att inspektera under fliken “Application” → “Service Workers” i webbläsarens utvecklarverktyg.
Web Manifest
Section titled “Web Manifest”För att en PWA ska gå att installera som en fristående applikation krävs, utöver en registrerad service worker och HTTPS, en manifestfil (.webmanifest) med bland annat namn, ikoner och visningsläge:
{ "name": "Memory game", "short_name": "Memory", "start_url": "./", "display": "standalone", "background_color": "#ffe001", "description": "A simple memory game!", "icons": [ { "src": "images/homescreen48.png", "sizes": "48x48", "type": "image/png" }, { "src": "images/homescreen192.png", "sizes": "192x192", "type": "image/png" } ]}Manifestfilen länkas in från HTML-dokumentet:
<link rel="manifest" href="/memory.webmanifest" />display styr hur appen ser ut när den är installerad – bland annat fullscreen, standalone, minimal-ui eller browser. Manifestfilen och service worker-filen måste ligga i den mapp som byggverktyget publicerar oförändrad (till exempel public/), inte bearbetas som vanlig applikationskod.
Kursvecka 16 - Pålitliga PWAs
Section titled “Kursvecka 16 - Pålitliga PWAs”Pålitliga PWA-applikationer
Section titled “Pålitliga PWA-applikationer”Varför routing i en SPA?
Section titled “Varför routing i en SPA?”I en single-page-applikation (SPA) laddas sidan aldrig om, men URL:en bör ändå spegla var i applikationen användaren befinner sig – precis som Spotifys webbspelare uppdaterar URL:en (.../artist/<id>, .../playlist/<id>) när man navigerar mellan artister, spellistor och album, utan att någonsin ladda om sidan. Det ger delbara länkar, fungerande bakåt/framåt-navigering och en begriplig historik, trots att inget nytt HTML-dokument faktiskt hämtas.
Läsa ut URL:en: window.location och URL
Section titled “Läsa ut URL:en: window.location och URL”window.location ger read-only-liknande tillgång till URL:ens olika delar: hash, host, href, pathname, port, protocol, search (query-strängen). search kommer ut som en rå sträng, vilket är omständligt att parsa för hand – URLSearchParams (nåbart via new URL(...).searchParams) löser det snyggare:
const params = new URL(document.location).searchParamsconst name = params.get('name') // strängen "Tim"const age = Number.parseInt(params.get('id')) // talet 1432, notera: alltid en sträng från börjanFragment identifier routing (hashbang) – en äldre teknik
Section titled “Fragment identifier routing (hashbang) – en äldre teknik”Innan bättre API:er fanns användes ankarlänkar (#) för att routa utan sidomladdning: eftersom webbläsaren aldrig laddar om sidan för en ren #-länk kunde man lyssna på händelsen hashchange, läsa ut window.location.hash och manuellt parsa fram vilken “sida” som avsågs (ofta med substring/split). Kombinationen #! (“hashbang”) signalerade historiskt till sökmotorer att innehållet bakom länken hämtades dynamiskt med JavaScript och borde hämtas separat för indexering – ett arv från en tid då sökmotorer hade svårt att exekvera JavaScript, betydligt mindre relevant idag men fortfarande synligt i äldre kod. Tekniken fungerar, men manuell strängparsning av hashen är klumpig och svår att bygga vidare på ju fler routes som tillkommer.
pushState/popState – det moderna sättet
Section titled “pushState/popState – det moderna sättet”window.history.pushState(stateObjekt, title, url) löser samma problem betydligt smidigare: den lägger till en post i webbläsarens historik, associerar ett godtyckligt (serialiserbart – inga funktioner) state-objekt med en given URL, och uppdaterar adressfältet – helt utan sidomladdning.
const stateObj = { accountnbr: '47435675', account: 'Account 1' }window.history.pushState(stateObj, 'Account information', '/account/47435675')Andra argumentet (titeln) respekteras i praktiken bara av Safari – Chrome och Firefox visar alltid huvuddokumentets egen <title> i historiken oavsett vad som skickas in, av skäl ingen riktigt har kunnat förklara. Argumentet måste ändå anges (kan lämnas tomt).
Motsvarande händelse popstate (lyssnas på via window.addEventListener('popstate', ...)) triggas när användaren navigerar med webbläsarens bakåt/framåt-knappar och ger tillbaka det state-objekt som hörde till den posten i historiken via event.state. Viktiga begränsningar att skydda sig mot:
event.statekan varanull(till exempel vid den allra första sidladdningen) – kräver en guard-sats innanevent.state-egenskaper läses ut.popstatetriggas inte vid en vanlig sidomladdning eller när en URL klistras in direkt i adressfältet. Det scenariot måste hanteras separat, till exempel genom att läsawindow.history.statei enload-händelselyssnare.
Serverkonfiguration krävs för att SPA-routing ska fungera överhuvudtaget
Section titled “Serverkonfiguration krävs för att SPA-routing ska fungera överhuvudtaget”Att bygga en URL-struktur på klienten löser bara halva problemet: skriver en användare in eller laddar om en URL som /account/123 direkt måste servern ändå svara med index.html (så att applikationens JavaScript kan starta och själv avgöra vad som ska visas) istället för en 404 – klienten har ingen möjlighet att fixa till detta i efterhand. Verktyget som användes vid inspelningstillfället (Snowpack 3, då i experimentellt skede) löste det via en routes-konfiguration som omdirigerar allt utan filändelse till index.html. Den exakta konfigurationssyntaxen är verktygsspecifik och föråldrad information att luta sig på idag – den bestående poängen är principen: samma omdirigeringsregel måste finnas både i den lokala utvecklingsmiljön och i produktionsmiljön (till exempel konfigurerad i nginx), annars fungerar routing lokalt men går sönder efter driftsättning.
Client-side routing är heller inget alla SPA:er behöver – vissa applikationer (till exempel en enklare examinationsuppgift utan tydligt strukturerad, navigerbar data) vinner inte särskilt mycket på det, medan andra (som Spotifys tydligt strukturerade artister/spellistor/album) passar det väl. Det är ett verktyg att ta till när det löser ett verkligt problem, inte ett krav för alla SPA:er.
navigator.onLine är otillförlitlig – “Lie-Fi”
Section titled “navigator.onLine är otillförlitlig – “Lie-Fi””window.navigator.onLine (plus händelserna online/offline) ser ut som den självklara lösningen för att avgöra om applikationen har uppkoppling, men testar i praktiken bara om enhetens nätverksgränssnitt är anslutet till något (en mobilmast, en accesspunkt) – inte om den faktiska vägen fram till servern fungerar. Fenomenet att uppleva sig som uppkopplad utan att någon data faktiskt kommer fram (fyra staplar men inget laddar) kallas “Lie-Fi”, ett begrepp som myntades på Google.
Pålitligare sätt att avgöra om servern faktiskt nås: låta en Service Worker kontrollera om ett nätverksanrop får svar inom en viss tid, sätta upp en enkel heartbeat/ping mot en egen server (till exempel via WebSocket), eller – ofta enklast – helt enkelt hantera det när ett faktiskt anrop misslyckas (till exempel ett chattmeddelande som inte går fram) snarare än att försöka förutspå uppkopplingsstatus i förväg. Hur strikt detta behöver hanteras beror på applikationen: en chattapplikation kan hantera ett enstaka misslyckat meddelande med stigande återförsöks-intervaller (vänta 10 sekunder, sedan en minut, sedan fem), medan en realtidsapplikation med höga krav på svarstid (till exempel ett multiplayer-spel) kan behöva inbyggd felhantering direkt i den ström av data som löpande skickas.
Oavsett teknik är det viktiga att kommunicera tydligt till användaren vad som händer (inaktivera ett inputfält, visa att ett meddelande väntar på att skickas) snarare än att låta appen tyst misslyckas. Ett ofta bortglömt relaterat problem: samma användare kan ha flera aktiva instanser av samma applikation samtidigt (mobil och dator), vilket kräver att man tänker igenom hur data synkas och eventuella konflikter hanteras mellan dem.
Varför ramverk finns
Section titled “Varför ramverk finns”Att hand-skriva routing genom strängparsning (substring, split, en lång kedja av if-satser) fungerar men skalar dåligt och blir svårt att underhålla. Det är precis den här typen av återkommande problem – särskilt state-hantering – som klientsideramverk (React, Vue, Svelte, Angular, Ember, Polymer med flera) löser åt en. Kursens webbkomponenter ger redan en bekant grund: koncept som “bygg i komponenter” och kapslad funktionalitet återkommer rakt av i ramverken, det är främst den mer avancerade state-hanteringen som tillkommer. Rekommendationen är att bli trygg med rena webbkomponenter innan man väljer att lägga till inlärningskurvan för ett ramverk ovanpå det, snarare än att stressa in i ett ramverk för tidigt.
Lazy loading
Section titled “Lazy loading”Vad är lazy loading?
Section titled “Vad är lazy loading?”MDN definierar lazy loading som en strategi för att identifiera resurser som icke-blockerande och inte kritiska, och bara ladda dem när de faktiskt behövs. Det är motsatsen till att ladda in allt en sida potentiellt kan behöva redan från start – en bild långt ner på sidan, en JavaScript-modul som bara används i ett sällan besökt läge, eller ett tredjepartsbibliotek som bara krävs för en liten del av funktionaliteten. Ju mer en applikation växer, desto mer data riskerar att laddas i onödan för de flesta besökare, vilket särskilt märks på enheter med sämre prestanda eller uppkoppling.
Mäta faktisk användning: Coverage-verktyget
Section titled “Mäta faktisk användning: Coverage-verktyget”Chrome DevTools har ett verktyg (Source → Coverage, aktiveras via kommandopaletten) som visar hur stor andel av laddad JavaScript och CSS som faktiskt körs respektive tillämpas på den aktuella sidan efter en omladdning. I föreläsningens exempel visade det sig att en stor andel av koden som laddades ner på en riktig webbplats – bland annat närmare 90 % av ett äldre jQuery-bibliotek och drygt 80 % av den medföljande CSS:en – aldrig användes på just den sidan som besöktes. Sådan kod behöver ändå överföras över nätet till varje besökares webbläsare, vilket vid många besökare blir en påtaglig mängd bortkastad dataöverföring – både en prestandafråga och en miljöfråga (särskilt relevant för besökare med begränsad datamängd eller uppkoppling).
Prestanda är en rankingfaktor
Section titled “Prestanda är en rankingfaktor”Google mäter sidors prestanda (historiskt via verktyget Lighthouse, numera en del av bredare mätningar som Core Web Vitals) och väger in resultatet i sökrankningen – särskilt för mobila besökare. Det gör lazy loading till mer än en teknisk optimering: en sida som laddar onödigt mycket okänd kod straffas i praktiken i sökresultaten.
Bilder: loading="lazy"
Section titled “Bilder: loading="lazy"”Det enklaste sättet att lazy-loada bilder är HTML-attributet loading="lazy", som talar om för webbläsaren att den själv får avgöra när bilden faktiskt behöver laddas (i praktiken när den närmar sig synligt område vid scroll):
<img src="bild.png" alt="…" loading="lazy" />Attributet är enkelt att lägga till på i princip alla bilder, och kraschar ingenting om webbläsaren av någon anledning inte skulle stödja det – ett obekant attributvärde ignoreras bara.
En intressant detalj är att loading="lazy" medvetet slutar fungera om JavaScript är avstängt i webbläsaren – samma princip som att JavaScript inte tillåts läsa av :visited-länkars färg. Om lazy loading av bilder fungerade även utan JavaScript skulle en sida kunna spåra ungefär hur långt en användare scrollat, genom att strategiskt placera bilder i markupen och observera vilka bildanrop som kommer in till servern och när. Det är alltså en medveten anti-spårningsåtgärd i specifikationen, inte en begränsning av misstag.
Skärmläsare och SEO-crawlers klarar sig utan särskild hänsyn
Section titled “Skärmläsare och SEO-crawlers klarar sig utan särskild hänsyn”Två vanliga farhågor kring lazy-loadat innehåll visar sig i praktiken inte vara problem längre: skärmläsare navigerar en sida i vanlig webbläsare och triggar samma scroll-baserade laddning som en seende användare gör. Och sökmotorernas crawlers (Googles i alla fall) kör numera JavaScript och simulerar även scroll nedåt på sidan, vilket innebär att innehåll som laddas lazy inte missas vid indexering – historiskt ett verkligt orosmoment för dynamiskt tillagt innehåll, men numera snarare en fördel eftersom lazy loading premieras av samma mätning som straffade det tidigare.
JavaScript-moduler: dynamisk import()
Section titled “JavaScript-moduler: dynamisk import()”Utöver import-satser i toppen av en fil finns import() som ett vanligt, asynkront funktionsanrop – tillgängligt direkt på window-objektet – för att ladda in en modul först när den faktiskt behövs:
// i en asynkron kontextconst module = await import('js/components/a-late-component/')// arbeta med modulen, t.ex. module.myFunction()
// eller med then/catchimport('js/components/a-late-component/').then(module => { // arbeta med modulen})Precis som fetch returnerar import() ett Promise som löser ut till ett modulobjekt – exporterade funktioner och värden nås som egenskaper på det objektet. Det här är ett renare sätt att skjuta upp laddning av kod än äldre tekniker som att manuellt hämta JavaScript-text med fetch och köra eval på den, eller att dynamiskt skjuta in en <script>-tagg i DOM:en. Ett naturligt användningsfall är en applikation uppbyggd kring flera separata delar (t.ex. flera “appar” i ett skrivbordsliknande gränssnitt) – precis som ett operativsystem inte laddar in alla installerade program vid uppstart, behöver inte all applikationskod laddas förrän användaren faktiskt öppnar den specifika delen.
IntersectionObserver som komplement
Section titled “IntersectionObserver som komplement”För att avgöra när något ska lazy-loadas (till exempel en tung inbäddad video, inte bara en enkel <img>) används ofta IntersectionObserver – ett inbyggt webbläsar-API för att bli notifierad när ett element blir synligt i viewporten, med möjlighet att lägga på en marginal (t.ex. ladda 300 pixlar innan elementet faktiskt syns, för att hinna med eventuell laddningstid). Att manuellt räkna ut sidstorlek och scrollposition för att avgöra vad som är synligt skulle snabbt bli komplext att implementera själv – IntersectionObserver löser samma problem deklarativt. Vid en applikation uppbyggd med dynamisk import() av separata delar (som exemplet ovan) behövs dock inte nödvändigtvis IntersectionObserver, eftersom laddningen där styrs av användarens handling (t.ex. ett klick) snarare än av vad som är synligt på skärmen.
Kursvecka 17-19 - Kapabla PWAs
Section titled “Kursvecka 17-19 - Kapabla PWAs”Kapabla PWAs
Section titled “Kapabla PWAs”Målet: webben som en fullvärdig applikationsplattform
Section titled “Målet: webben som en fullvärdig applikationsplattform”En central del av vad som gör en Progressive Web Application (PWA) “kapabel” är att den ska kunna bete sig och kännas som en native-installerad applikation, utan att sakna möjligheter jämfört med den. Webben är inte helt i mål än, men rör sig konsekvent åt det hållet – varje år tillkommer fler webbläsar-API:er som ger tillgång till funktionalitet som tidigare krävde en native app. Den här genomgången är en katalog över den typen av API:er: ingen förväntas kunnas utantill, poängen är att veta att de finns och kunna slå upp dem när behovet uppstår.
Grafik och media
Section titled “Grafik och media”Canvas API (<canvas>, del av HTML5) ger en rityta för att rita vektorgrafik direkt med JavaScript – grafer, bildkomposition, animationer, enklare realtidsspel. Perfekt när HTML-element (till exempel en massa <div>-boxar för ett stapeldiagram) blir för klumpigt att animera snyggt.
WebGL går längre och ger hårdvaruaccelererad 3D-grafik i webbläsaren utan att användaren behöver installera något extra – bygger på samma grundprinciper som OpenGL. Används bland annat för portade spel och tunga visuella demos direkt i webbläsaren.
WebXR API (tidigare WebVR, omdöpt för att täcka både AR och VR under ett gemensamt namn i W3C) ger tillgång till AR/VR-utrustning direkt från webbläsaren – webben som plattform för den typen av upplevelser är fortfarande i sin linda, men stödet finns.
Gamepad API låter en lyssna på händelsen gamepadconnected när en spelkontroll ansluts till datorn, och därefter koppla händelsehanterare till enskilda knappar – på liknande sätt som klickhändelser på vanliga element.
Kommunikation med enheter och andra klienter
Section titled “Kommunikation med enheter och andra klienter”Flera API:er ger webbapplikationer tillgång till kommunikationsvägar som traditionellt gått via operativsystemet eller en native app:
- Web Bluetooth – synka och kommunicera med Bluetooth-enheter direkt från webbläsaren.
- Web NFC API – läsa/skriva NFC (Near Field Communication), samma teknik som “blippning” vid kortbetalning. Vissa plattformar (historiskt särskilt Apple/Safari) har varit restriktiva med att öppna upp NFC-åtkomst, även för native-appar.
- Web USB API – kommunicera med anslutna USB-enheter.
- WebRTC (Web Real-Time Communication) – peer-to-peer-kommunikation direkt mellan klienter, utan att trafiken behöver gå via en mellanliggande server. Relevant för videochatt eller andra realtidsapplikationer där det vore onödigt (och en integritetsrisk) att låta all trafik passera en server som kanske ligger geografiskt långt bort, trots att båda klienterna sitter i samma nätverk. Ett samlingsbegrepp för flera API:er (
MediaStream,MediaRecorder,RTCPeerConnection,RTCDataChannel) beroende på om man vill skicka video, filer eller annan data. Stödet är idag i praktiken fullständigt i moderna webbläsare.
Generellt gäller: tittar man på kompatibilitetstabeller (till exempel “Can I use”) för de här nyare API:erna är det oftast Safari/iOS Safari som ligger efter i implementation, medan Chrome och Firefox brukar leda beroende på vilken av dem som ursprungligen föreslog API:et.
Betalningar och delning
Section titled “Betalningar och delning”Payment Request API låter webbläsaren själv hålla koll på användarens sparade betalkort, med strikt kontroll över hur webbplatser får använda den informationen – ett försök att lösa samma problem som aktörer som Klarna eller Apple Pay/Google Pay redan löser på andra sätt. Stödet varierar mellan webbläsare och är starkare på desktop än mobil, vilket är en svaghet eftersom mobil e-handel är där behovet ofta är som störst.
Web Share API låter en webbsida trigga operativsystemets inbyggda delningsmeny (samma meny som dyker upp när man delar en länk från mobilens webbläsare). Web Share Target API är den omvända riktningen: gör en installerad PWA till ett valbart mål i andra appars delningsmeny.
Web Animation API ger möjlighet att styra animationer direkt via JavaScript, som ett alternativ till att uteslutande deklarera dem i CSS.
API:er kommer och går – var källkritisk
Section titled “API:er kommer och går – var källkritisk”En viktig poäng som återkommer: många av de nyare, mer udda API:erna är fortfarande utkast (drafts), och flera har redan dragits tillbaka eller kraftigt förändrats efter att ha införts, oftast av integritetsskäl. Två konkreta exempel: Battery Status API (avslöjade användarens batterinivå – togs bort i Firefox som alltför integritetskänsligt) och Vibration API (togs bort i WebKit). Andra API:er värda att känna till, med samma brasklapp om instabilitet: Geolocation API (positionsuppgifter, med obligatorisk användarbekräftelse), Clipboard API (kopiera/klistra in – med varningsdialog i moderna webbläsare, likt native-appar), Ambient Light (omgivande ljusnivå, till exempel för att växla mellan ett dag- och ett nattläge), Device Orientation API (enhetens fysiska rotation – kan delvis också läsas ut via CSS) och Speech Recognition API (taligenkänning/diktering).
Slutsatsen: anta aldrig att ett nytt, udda webbläsar-API kommer finnas kvar för evigt i sin nuvarande form – kontrollera aktuellt stöd innan man bygger vidare på det, särskilt för API:er som rör känslig information om användaren eller enheten.
Dynamiskt kontra statiskt typade språk
Section titled “Dynamiskt kontra statiskt typade språk”JavaScript är dynamiskt typat: en variabels typ kan ändras under körning (en variabel som börjar som ett tal kan senare bli en sträng eller ett objekt). Enda sättet att helt spärra det i ren JavaScript är const – men det låser bara referensen, inte innehållets typ i alla lägen.
Statiskt typade språk (till exempel Java) kräver att typen deklareras redan när koden skrivs, och kompilatorn vägrar kompilera kod som bryter mot den deklarationen (greeting = 2 på en variabel deklarerad som String ger kompileringsfelet “incompatible types: int cannot be converted to String”). Studenter som gått från ett statiskt typat språk till JavaScript brukar landa ungefär 50/50 i om de föredrar den tidiga felkontrollen kompilatorn ger, eller den snabbare, mer flexibla arbetsgången utan typdeklarationer – ingen av synpunkterna är fel, det är en avvägning. En konsekvens värd att notera: eftersom JavaScript inte tvingar fram typdeklarationer blir tydlig dokumentation av vilka typer en funktion förväntar sig (t.ex. via JSDoc) desto viktigare, för att kompensera för det kompilatorn annars skulle fångat upp automatiskt.
TypeScript – JavaScript med valfria typer
Section titled “TypeScript – JavaScript med valfria typer”TypeScript (Microsoft, 2012) lägger till valfri statisk typning ovanpå JavaScript-syntaxen:
let fullName: string = 'Bob Bobbington'let age: number = 37Filer skrivs med filändelsen .ts och kompileras (npx tsc index.ts) till vanlig .js som webbläsaren faktiskt kan köra – verktyg som Babel gör ofta den här typen av omskrivning i bakgrunden som en del av byggpipelinen (Snowpack, Webpack, Parcel och liknande). Deno (skaparen av Node.js sidoprojekt, en omarbetad variant av Node) har numera inbyggt stöd för TypeScript utan separat kompileringssteg. TypeScript är inte ett nytt språk att lära sig från grunden – det är JavaScript med ett tillägg, och begreppet kommer att dyka upp ofta i presentationer och verktygsbeskrivningar oavsett om man själv väljer att använda det.
WebAssembly – att köra annan kod i webbläsaren
Section titled “WebAssembly – att köra annan kod i webbläsaren”WebAssembly (etablerat 2017 gemensamt av W3C, Google, Apple, Mozilla och Microsoft) gör det möjligt att kompilera kod skriven i andra språk (C, C++, Rust, med flera) till ett binärt format som körs direkt i webbläsaren, parallellt med och kommunicerande med JavaScript. Anledningen det är intressant: JavaScripts dynamiska typning gör det svårt för motorn att göra samma djupa optimeringar som ett statiskt typat, kompilerat språk tillåter (se nästa avsnitt). WebAssembly används framför allt för att låna in bibliotek från andra ekosystem (till exempel avancerade bildbehandlingsalgoritmer skrivna i C) och i situationer där prestanda är kritisk. Google Earth är ett konkret exempel på en WebAssembly-baserad applikation i produktion. AssemblyScript är ett TypeScript-liknande språk designat specifikt för att kompileras till WebAssembly, för den som vill slippa lära sig ett helt nytt språk bara för det syftet.
Varför det här är värt att känna till
Section titled “Varför det här är värt att känna till”Webben fortsätter att växa i betydelse som applikationsplattform, inte bara på mobil utan även på desktop – många klarar sig idag i praktiken med enbart en webbläsare (Chromebooks som exempel). Att specialisera sig på klientsideutveckling är därför inget begränsat val: de här API:erna, tillsammans med webbkomponenter och de standarder som redan gåtts igenom i kursen, är byggstenarna som gör webben till en plattform i klass med native-utveckling – inte en ersättning för alla andra verktyg, men en allt starkare kandidat för allt fler typer av applikationer.
Web Sockets API
Section titled “Web Sockets API”Varför HTTP inte räcker för realtidsdata
Section titled “Varför HTTP inte räcker för realtidsdata”HTTP designades ursprungligen för att servera enstaka HTML-dokument: klienten initierar alltid en förfrågan, servern svarar, klar. Modellen har relativt mycket overhead per anrop och passar dåligt när applikationen behöver realtidsdata – en graf som ska uppdateras live utifrån inkommande mätvärden, positionerna för alla spelare i ett webbaserat spel, eller ett chattmeddelande som ska dyka upp hos alla andra anslutna klienter så fort någon skickar det.
Att skicka data till servern med ett vanligt fetch-anrop är inget problem i sig. Problemet är att få reda på när ny data finns att hämta – det enda sättet en klient kan göra det med bara HTTP är att fråga servern om och om igen (“har det kommit något nytt? har det kommit något nytt?”, som ett barn i baksätet som frågar “är vi framme snart?”). Kortare intervall mellan frågorna ger snabbare uppdateringar men belastar servern hårt med ständiga anrop som oftast inte ger något nytt svar. En äldre teknik, long polling, förbättrade detta något genom att låta servern hålla kvar en HTTP-förfrågan öppen istället för att svara direkt, tills antingen ny data fanns eller anropet tajmade ut – men det är fortfarande en omväg runt en begränsning i HTTP snarare än en riktig lösning.
Web Sockets: ett eget protokoll, inte HTTP
Section titled “Web Sockets: ett eget protokoll, inte HTTP”WebSocket (ws://) och WebSocket Secure (wss://, den krypterade varianten) är ett eget protokoll, skilt från HTTP – men det delar samma portar som HTTP (80 respektive 443), vilket betyder att brandväggar som redan släpper igenom vanlig webbtrafik även släpper igenom WebSocket-trafik utan extra konfiguration.
Uppkopplingen börjar med en handskakning: klienten frågar servern om anslutningen kan uppgraderas till en WebSocket-connection, och accepterar servern det uppgraderas den vanliga uppkopplingen till en socket – i praktiken en öppen tvåvägstunnel mellan klient och server. Skillnaden mot HTTP är avgörande: när socketen väl är öppen kan båda sidor skicka data när som helst, inte bara klienten som initierar. Web Sockets passar bäst för relativt små datapaket som skickas ofta (positionsuppdateringar, chattmeddelanden) snarare än stora, sällsynta dataöverföringar – där finns ingen “bestraffning” för att skicka ofta, till skillnad från upprepade HTTP-anrop.
API:et
Section titled “API:et”const socket = new window.WebSocket('ws://www.example.com/socketserver', 'chatroom')
const data = { x: 12, y: 14 }
socket.addEventListener('open', event => { socket.send(JSON.stringify(data))})
socket.addEventListener('message', event => { console.log(event.data)})
socket.close()Andra argumentet till konstruktorn ('chatroom' ovan) är en fritt vald textsträng – ett protokollnamn som klient och server kommer överens om, användbart för att skilja olika typer av meddelanden eller kanaler åt om samma server hanterar flera parallella syften.
De två händelser som täcker de flesta behov: open, som talar om att uppkopplingen är uppgraderad och klar – data kan inte skickas förrän den har utlösts – och message, som utlöses varje gång ett meddelande kommer in från motparten (event.data innehåller den mottagna texten). Eftersom socket.send() bara skickar ren text är det vanligt att serialisera data med JSON.stringify före sändning och JSON.parse vid mottagning.
Web Socket-API:et togs fram runt 2010–2011, vilket syns i att det bygger på händelselyssnare/callbacks snarare än till exempel Promises – men API:et i sig är litet och okomplicerat att jobba med.
Livslängd och nedstängning
Section titled “Livslängd och nedstängning”En socket som är helt tyst en längre stund (ofta runt 60 sekunder, beroende på serverkonfiguration) kan tajma ut och stängas automatiskt. Ska en uppkoppling hållas vid liv trots låg aktivitet behöver servern skicka periodiska meddelanden på kanalen. Händelsen close utlöses när en socket stängs, oavsett om det sker av en timeout eller manuellt via socket.close(). Ligger en socket inuti en webbkomponent är disconnectedCallback en lämplig plats att stänga den manuellt och städa upp, på samma sätt som andra resurser som startats av komponenten.
Kursen kräver inte att man själv skriver en WebSocket-server (en sådan tillhandahålls för de examinationsuppgifter som behöver det) – fokus ligger på att kunna konsumera ett WebSocket-API från klientsidan.