Webbläsaren som körmiljö
I sju veckor har du kört JavaScript i Node. Nu byter du miljö: samma språk, men en helt annan omgivning att köra det i. Den här artikeln går igenom vad som skiljer webbläsaren från Node, vilka delar en webbläsare består av, hur den gör om HTML och CSS till en sida på skärmen och hur du bör ladda in dina skript.
Tidsåtgång: Ungefär 20 minuter.
Några korta frågor finns insprängda i texten. Försök svara innan du klickar fram lösningen; det är själva gissningen som gör att du kommer ihåg svaret.
Varför just JavaScript?
Section titled “Varför just JavaScript?”Det var inte självklart att det skulle bli JavaScript. På 90-talet trodde många att Java skulle köras i webbläsaren, och Microsoft satsade på VBScript. Netscape byggde ett eget skriptspråk, först kallat LiveScript, som döptes om till JavaScript för att rida på Javas popularitet. Språken har inte mycket gemensamt utöver namnet. Själva standarden heter ECMAScript.
I dag är JavaScript det enda språk alla webbläsare kör direkt. WebAssembly gör det möjligt att kompilera andra språk för webbläsaren, men det kompletterar JavaScript snarare än ersätter det. Webbtekniken har dessutom spridit sig långt utanför webbläsarfönstret. Visual Studio Code är byggt med HTML, CSS och JavaScript och paketerat som skrivbordsprogram med Electron. Mot slutet av kursen bygger du själv en progressiv webbapplikation (PWA), som kan installeras som en vanlig app.
Samma språk, ny körmiljö
Section titled “Samma språk, ny körmiljö”JavaScript som språk har inga inbyggda sätt att läsa filer, rita på skärmen eller prata med
nätverket. Sådant tillhandahålls av körmiljön (på engelska runtime), programmet som bäddar in
JavaScript-motorn och kör din kod. Node ger dig fs, process och http. Webbläsaren ger dig i
stället document (DOM), window, fetch och lagring som localStorage.
Skillnaderna går djupare än vilka API:er som finns:
| Node | Webbläsaren | |
|---|---|---|
| Vem bestämmer versionen? | Du, när du installerar Node | Användaren, och det är många olika webbläsare |
| Åtkomst till datorn | Full, med dina rättigheter | Ingen direkt; koden körs i en sandlåda |
| Var hämtas moduler från? | Filsystemet | En webbserver, över HTTP |
| Globalt objekt | globalThis (även global) |
globalThis (även window) |
Fråga: vad fungerar i båda miljöerna?
Section titled “Fråga: vad fungerar i båda miljöerna?”Vilka av följande kan du använda både i Node och i webbläsaren?
console.log()document.titlefs.readFileSync()fetch()process.argvsetTimeout()globalThis
console.log(), fetch(), setTimeout() och globalThis fungerar i båda. fetch() har funnits i
Node sedan version 18.
document finns bara i webbläsaren, eftersom Node inte har något dokument att visa. fs och
process finns bara i Node, eftersom webbläsaren av säkerhetsskäl inte ger kod från en webbsida
tillgång till filsystemet eller processen.
Lägg märke till att varken console eller setTimeout() ingår i själva språket ECMAScript. Båda
körmiljöerna har valt att erbjuda dem, och därför känns de som en del av JavaScript.
En miljö du inte kontrollerar
Section titled “En miljö du inte kontrollerar”I en föreläsning om JavaScript från mitten av 2000-talet sammanfattade Douglas Crockford webbutvecklarens vardag i en mening:
The browser is a really hostile programming environment.
Webbläsaren är alltså en riktigt fientlig programmeringsmiljö. När han sa det dominerade Internet Explorer 6, och varje webbläsare hade sin egen tolkning av hur DOM och JavaScript skulle fungera. Samma kod kunde fungera i en webbläsare och krascha i en annan, och utvecklare fick skriva specialfall för varje webbläsare.
Mycket har blivit bättre sedan dess, men kärnan i påståendet gäller fortfarande: du styr inte över miljön. Din kod körs på användarens dator, i användarens webbläsare, i den version användaren råkar ha. Det finns tre stora familjer av webbläsarmotorer:
| Webbläsare | Webbläsarmotor | JavaScript-motor |
|---|---|---|
| Chrome, Edge, Opera, Brave m.fl. | Blink | V8 |
| Firefox | Gecko | SpiderMonkey |
| Safari | WebKit | JavaScriptCore |
På iPhone och iPad bygger i praktiken alla webbläsare på WebKit, oavsett vad de heter. Testar du bara i Chrome på din egen dator ser du alltså bara en del av verkligheten.
Läget är ändå mycket bättre än för några år sedan. Webbläsartillverkarna samarbetar kring gemensamma tester, och begreppet Baseline sammanfattar om något på webbplattformen går att lita på. Det gäller allt som webbläsaren ska förstå: HTML-element och attribut, CSS-egenskaper, JavaScript-syntax och webbläsarens API:er.
- Baseline Newly available: det fungerar i den senaste versionen av Chrome, Edge, Firefox och Safari.
- Baseline Widely available: det har gått minst 30 månader sedan dess, så även användare med lite äldre webbläsare har stöd.
På MDN Web Docs syns Baseline-märkningen i en ruta direkt under
rubriken på referenssidorna, alltså sidorna som beskriver ett enskilt element, API eller en metod.
Ett exempel är sidan om
<template>. MDN:s
guider, som beskriver ett större ämne, har ingen sådan ruta. Längst ner på varje referenssida finns
dessutom en detaljerad stödtabell per webbläsare och version. Samma uppgifter, inklusive
Baseline-status, hittar du också på caniuse.com. Gör det till en vana att
kontrollera stödet där innan du använder något du inte har använt förut.
Webbläsarens beståndsdelar
Section titled “Webbläsarens beståndsdelar”En webbläsare är inte ett enda program utan flera delar som samverkar. En klassisk beskrivning delar upp den så här:
- Användargränssnitt: allt du ser utom själva sidan, som adressfält, knappar och flikar.
- Webbläsarmotor (browser engine): samordnar användargränssnittet och renderingsmotorn.
- Renderingsmotor (rendering engine): tolkar HTML och CSS och visar sidan. Det är den som går igenom stegen i nästa avsnitt.
- Nätverk: hämtar sidor, skript, bilder och annat över HTTP.
- JavaScript-motor: tolkar och kör JavaScript-koden.
- UI-backend: ritar grundläggande element som rullgardinsmenyer och fönster med hjälp av operativsystemets egna funktioner.
- Datalagring: sparar data i webbläsaren, till exempel kakor (cookies) och
localStorage.
Gränsen mellan webbläsarmotor och renderingsmotor är flytande i praktiken. Blink, Gecko och WebKit i tabellen ovan kallas på engelska både browser engines och rendering engines.
Från HTML till pixlar
Section titled “Från HTML till pixlar”När webbläsaren har hämtat en HTML-sida går den igenom ett antal steg innan något syns på skärmen. Stegen kallas tillsammans den kritiska renderingsvägen (critical rendering path).
- DOM: HTML-koden tolkas till ett träd av objekt, Document Object Model. Varje element blir ett
objekt, till exempel ett
HTMLParagraphElement, som i sin tur ärver frånHTMLElement,Element,NodeochEventTarget. Det är det här trädet din JavaScript-kod arbetar mot från och med nästa vecka. - CSSOM: CSS-koden tolkas till ett motsvarande träd med stilregler.
- Render-träd: DOM och CSSOM slås ihop. Bara det som ska synas kommer med; ett element med
display: nonehoppas över. - Layout: webbläsaren räknar ut storlek och position för varje element.
- Paint och composite: elementen ritas ut i lager som sedan sätts ihop till bilden du ser.
Två saker är värda att ta med sig härifrån. För det första kan JavaScript avbryta det första steget, vilket nästa avsnitt handlar om. För det andra kan JavaScript som ändrar i DOM:en tvinga webbläsaren att räkna om layouten och rita om sidan. Det kommer du att märka av när du senare bygger gränssnitt som uppdateras ofta.
Hur skript laddas
Section titled “Hur skript laddas”Medan HTML-tolken arbetar sig igenom sidan laddas andra resurser, som CSS-filer och bilder, ner
parallellt. En vanlig <script>-tagg är undantaget: den stoppar HTML-tolken. Webbläsaren
laddar ner skriptet och kör det, och först därefter fortsätter tolkningen av resten av sidan.
Anledningen är historisk: ett skript kan skriva ut ny HTML med document.write(), så tolken vågar
inte fortsätta förrän skriptet har körts.
Att tolkningen måste stå still medan skriptet körs har också en annan orsak. HTML-tolkningen och din JavaScript-kod delar på samma tråd, webbläsarens huvudtråd, och kan därför aldrig köras samtidigt. Undantaget är så kallade web workers, som kör JavaScript i en egen tråd parallellt med huvudtråden. De har i gengäld ingen åtkomst till DOM:en.
Moderna webbläsare har en så kallad preload scanner som tittar framåt i dokumentet och börjar ladda ner skript och bilder i förväg. Det minskar väntetiden, men skriptet körs fortfarande innan resten av sidan har tolkats.
Den gamla lösningen var att lägga skripten sist i <body>, så att hela dokumentet hann tolkas innan
skripten laddades ner och kördes. I dag finns bättre alternativ: attribut
på <script>-taggen som styr när skriptet laddas ner och körs.
De tre tidslinjerna nedan visar samma sida med tre olika sorters skripttaggar. Den övre raden i varje diagram är HTML-tolken, den undre skriptet. Tidsskalan är densamma i alla tre, så du kan jämföra hur länge tolken står still (rött) och när sidan är klar.
| Variant | Laddas ner | Körs | Inbördes ordning |
|---|---|---|---|
<script> |
medan tolken är blockerad | direkt | dokumentordning |
<script defer> |
parallellt med tolkningen | när tolkningen är klar | dokumentordning |
<script async> |
parallellt med tolkningen | så fort skriptet är nedladdat | det som blir klart först |
<script type="module"> |
parallellt, inklusive allt den importerar | när tolkningen är klar | dokumentordning |
Ett par detaljer som ofta överraskar:
deferhar ingen effekt på ett skript som står direkt i taggen (inline). Det fungerar bara tillsammans medsrc.- Moduler skjuts upp även när de står inline. Du behöver aldrig skriva
deferpå en modul.
Fråga: i vilken ordning skrivs bokstäverna ut?
Section titled “Fråga: i vilken ordning skrivs bokstäverna ut?”<!doctype html><html lang="sv"> <head> <script type="module"> console.log('A', document.querySelector('p')) </script> <script> console.log('B', document.querySelector('p')) </script> </head> <body> <p>Hej!</p> <script> console.log('C', document.querySelector('p')) </script> </body></html>Fundera också på vad document.querySelector('p') ger på varje rad: själva <p>-elementet eller
null?
Ordningen är B, C, A.
- B körs direkt när tolken når den, i
<head>. Då finns inget<p>-element än, såquerySelectorgernull. - C körs när tolken når den, i slutet av
<body>.<p>-elementet står före i dokumentet och finns därför redan: elementet skrivs ut. - A är en modul och skjuts upp tills hela dokumentet är tolkat, trots att den står först och inline. Även här hittas elementet.
Vill du se det med egna ögon kan du klistra in koden i en index.html i övningsuppgiftens projekt.
Fler frågor av samma slag finns bland veckans utmaningar.
Rekommendationen i dag
Section titled “Rekommendationen i dag”Ladda in din applikation som en modul i <head>, och låt den importera resten:
<head> <link rel="stylesheet" href="./css/styles.css" /> <script type="module" src="./js/index.js"></script></head>Modulen blockerar inte tolkningen, körs när DOM:en är på plats, och resten av koden hämtas genom
import-satserna i index.js. Det är precis så projektet i veckans övningsuppgift är uppbyggt.
Moduler i webbläsaren
Section titled “Moduler i webbläsaren”Du känner redan till import och export från Node. Moduler av det slaget kallas ES-moduler,
ECMAScript-moduler, och förkortas ofta ESM. I webbläsaren fungerar de på samma sätt, men med
några skillnader som är bra att känna till.
Moduler hämtas över HTTP. Webbläsaren gör en nätverksförfrågan för varje modul den importerar.
Webbläsaren är då klienten: den skickar en förfrågan (request) och webbservern skickar tillbaka
filen i ett svar (response).
Öppnar du en HTML-fil direkt från disk (en adress som börjar med file://) vägrar webbläsaren av
säkerhetsskäl att ladda moduler, och du får ett CORS-fel i konsolen. Du behöver alltså en webbserver
även under utveckling. I kursen sköter Vite det åt dig när du kör npm run dev.
En sökväg är en adress. Relativa sökvägar som ./greeting.js tolkas i förhållande till den
importerande filens adress, och filändelsen måste skrivas ut. Importerar du ett paket med bara dess
namn, som i import confetti from 'canvas-confetti' (en så kallad bare specifier), förstår
webbläsaren inte vad du menar:
Uncaught TypeError: Failed to resolve module specifier "canvas-confetti".Relative references must start with either "/", "./", or "../".Det är Vite som skriver om sådana importer till riktiga adresser åt dig, så att du kan använda paket från npm. (Webbläsaren kan också lösa det själv med en så kallad import map, men det behöver du inte i kursen.)
En modul har ett eget scope. Variabler du deklarerar på toppnivå i en modul hamnar inte i det globala objektet. Moduler körs dessutom alltid i strict mode, och varje modul körs bara en gång, hur många gånger den än importeras.
Fråga: varför hittar konsolen inte variabeln?
Section titled “Fråga: varför hittar konsolen inte variabeln?”Anta att index.js innehåller raden const users = await fetchRandomUsers(). Du öppnar konsolen
och skriver users. Vad händer, och varför?
Du får ett ReferenceError: users is not defined. Konsolen kör i det globala scopet, och users är
deklarerad i modulens eget scope.
Vill du undersöka variabeln sätter du i stället en brytpunkt i modulen. När debuggern står still är
konsolen kopplad till det scope där koden stannade, och då kan du skriva users och se innehållet.
Gör en variabel inte global bara för att kunna se den i konsolen.
Lägg förresten märke till att await står direkt på modulens toppnivå, utanför en async-funktion.
Det fungerar i moduler men inte i vanliga skript.
Konsolen är mer än console.log()
Section titled “Konsolen är mer än console.log()”Utvecklarverktygen blir din viktigaste arbetsplats under den här delen av kursen. Hur du öppnar dem
och använder debuggern går övningsuppgiften igenom steg för steg, så det här avsnittet handlar bara
om konsolen. Konsol-API:et (kapitel 11.8 i kursboken) har fler metoder än log():
| Metod | Användning |
|---|---|
console.error(), console.warn() |
Utskrift som markeras som fel eller varning och kan filtreras fram |
console.table() |
Visar en array med objekt som en tabell |
console.group(), console.groupEnd() |
Grupperar utskrifter som går att fälla ihop |
console.time(), console.timeEnd() |
Mäter hur lång tid något tar |
console.assert() |
Skriver bara ut något om ett villkor är falskt |
Prova direkt
Section titled “Prova direkt”Öppna konsolen här på den här sidan (F12, eller Cmd+Option+J på macOS) och skriv:
console.table([{ name: 'Ada', born: 1815 }, { name: 'Grace', born: 1906 }])Prova sedan document.querySelectorAll('h2').length. Stämmer antalet med antalet rubriker du ser i
innehållsförteckningen? Du har just ställt din första fråga till DOM:en, och nästa vecka gör du det
från din egen kod.
Att ta med sig
Section titled “Att ta med sig”- Webbläsaren och Node kör samma språk men erbjuder helt olika API:er.
- Du styr inte över användarens webbläsare. Kontrollera stödet för element, CSS och API:er med Baseline på MDN.
- En vanlig
<script>-tagg stoppar HTML-tolken. Moduler gör det inte. - Ladda din applikation som en modul i
<head>och låt den importera resten. - Moduler kräver en webbserver, har eget scope och körs i strict mode.
Vidare läsning
Section titled “Vidare läsning”- Läsanvisningarna för veckan, i synnerhet kapitel 15.1 i kursboken.
- JavaScript modules på MDN.
- <script>: The Script element på MDN.
- Baseline på web.dev.