Kapitel 1: Introduktion till Webbutveckling
Välkommen till Glimåkras kurs i webbutveckling! I en värld där nästan allt finns online – från hur vi handlar och kommunicerar till hur vi lär oss och arbetar – har webben blivit en oumbärlig del av våra liv. Men vad är det egentligen som får webben att fungera, och hur skapas alla dessa webbplatser och applikationer vi använder dagligen?
Det är precis vad webbutveckling handlar om: konsten och tekniken att bygga för webben. Det är ett fält som kombinerar logiskt tänkande med kreativ design för att skapa digitala upplevelser.
Varför är detta viktigt för dig? Oavsett om du siktar på en karriär som utvecklare, vill bygga en egen webbplats, eller helt enkelt vill förstå den digitala världen bättre, ger kunskaper i webbutveckling dig värdefulla färdigheter.
I detta första kapitel lägger vi grunden. Vi kommer att utforska:
- Vad webbutveckling faktiskt är: Vi bryter ner begreppet och tittar på de olika delarna som utgör en webbplats (frontend och backend) och hur de samspelar.
- Webbens resa: En kort tillbakablick på hur webben har utvecklats från enkla textdokument till de komplexa applikationer vi ser idag, och en glimt av framtiden.
- Utvecklarens verktygslåda: En översikt över de viktigaste språken, ramverken och verktygen som moderna webbutvecklare använder.
- Komma igång: Vi ser till att du har de grundläggande verktygen installerade och redo att användas för resten av kursen.
Låt oss börja vår resa in i webbutvecklingens fascinerande värld!
Vad är webbutveckling?
Du använder webben varje dag – för att söka information, kommunicera, handla, eller titta på videor. Men har du någonsin stannat upp och funderat på hur allt detta fungerar? Hur skapas webbplatserna och applikationerna som gör detta möjligt?
Svaret är webbutveckling. Det är processen att designa, bygga och underhålla webbplatser och webbapplikationer. Det handlar om att kombinera teknisk skicklighet med kreativ problemlösning för att skapa de digitala upplevelser vi tar för givna.
Varför är det relevant för dig? Att förstå grunderna i webbutveckling ger dig inte bara insikt i hur en stor del av den moderna världen fungerar, utan öppnar också dörrar till att själv kunna skapa och forma digitalt innehåll och verktyg.
Två sidor av samma mynt: Frontend och Backend
Webbutveckling brukar delas upp i två huvudsakliga områden:
-
Frontend (Klientsidan): Tänk på detta som allt du ser och interagerar med i din webbläsare. Det är layouten, färgerna, knapparna, formulären och animationerna. Frontend-utvecklare använder språk som:
- HTML (HyperText Markup Language): Byggstenarna som strukturerar innehållet (rubriker, paragrafer, bilder, länkar).
- CSS (Cascading Style Sheets): Reglerna som bestämmer hur innehållet ska se ut (färger, typsnitt, layout).
- JavaScript: Språket som gör sidan interaktiv och dynamisk (hanterar klick, uppdaterar innehåll utan att ladda om sidan, skapar animationer).
-
Backend (Serversidan): Detta är "bakom kulisserna". Backend hanterar logiken, databaserna och allt som sker på servern för att webbplatsen ska fungera. Tänk på det som motorn i en bil – du ser den inte direkt, men den är avgörande. Backend-utvecklare arbetar med:
- Serverspråk: Språk som PHP (som vi fokuserar på i denna kurs), Python, Ruby, Node.js (JavaScript på servern), Java, m.fl. Dessa språk hanterar inkommande förfrågningar, bearbetar data och genererar svar.
- Databaser: System som MySQL, PostgreSQL, MongoDB för att lagra och hämta information (t.ex. användardata, produkter, inlägg).
- Servern: Datorn där webbplatsens kod körs och dess data lagras.
Analogi: Tänk på en restaurang. Frontend är matsalen – inredningen, menyn du läser, hur servitören interagerar med dig. Backend är köket – kocken som lagar maten (processar data), recepten (logiken), och skafferiet (databasen).
graph TD User["Användare"] -->|"Förfrågan (Request)"| Browser["Webbläsare/Klient"] Browser -->|"HTTP(S) Request"| Server["Webbserver/Backend"] Server -->|"Bearbetar & hämtar data"| Database[("Databas")] Database -->|"Data"| Server Server -->|"HTTP(S) Response (HTML, CSS, JS)"| Browser Browser -->|"Renderar sidan"| User subgraph Frontend ["Frontend"] Browser end subgraph Backend ["Backend"] Server Database end
Diagram: Förenklad bild av hur en webbförfrågan (request) hanteras.
Hur fungerar det? Request-Response Cycle
När du skriver in en webbadress (t.ex. www.example.com
) eller klickar på en länk, händer följande i stora drag:
- Förfrågan (Request): Din webbläsare (klienten) skickar en förfrågan (HTTP request) över internet till den server där webbplatsen finns lagrad. Adressen översätts först från ett domännamn (som
www.example.com
) till en IP-adress via DNS (Domain Name System), ungefär som en telefonkatalog för internet. - Bearbetning (Processing): Servern tar emot förfrågan. Om det är en enkel sida kan den direkt skicka tillbaka HTML-, CSS- och JavaScript-filer. Om det krävs logik (t.ex. logga in, hämta data) kör backend-koden, interagerar kanske med en databas.
- Svar (Response): Servern skickar tillbaka ett svar (HTTP response) till din webbläsare. Detta svar innehåller de filer och data som behövs för att visa sidan (oftast HTML, CSS, JavaScript).
- Rendering: Din webbläsare tar emot svaret, tolkar filerna och "ritar upp" webbsidan på din skärm.
Denna process, från förfrågan till svar, kallas Request-Response Cycle och är grunden för hur webben fungerar.
Fullstack-utvecklare
En person som behärskar både frontend- och backend-utveckling kallas ofta Fullstack-utvecklare. De har en bred förståelse för hela processen och kan bygga kompletta webbapplikationer på egen hand.
Viktiga Begrepp (Ordlista)
- HTTP/HTTPS (HyperText Transfer Protocol/Secure): Protokollet (regelverket) för hur data skickas mellan klient och server på webben. HTTPS är den krypterade, säkra versionen.
- URL (Uniform Resource Locator): Webbadressen som identifierar en specifik resurs (t.ex. en webbsida) på internet.
- DNS (Domain Name System): Internets "telefonkatalog" som översätter läsbara domännamn till numeriska IP-adresser.
- IP-adress (Internet Protocol Address): En unik numerisk adress för varje enhet ansluten till internet (t.ex.
192.168.1.1
). - API (Application Programming Interface): Ett gränssnitt som låter olika program eller systemdelar kommunicera med varandra.
Sammanfattning
Webbutveckling är fältet där man skapar och underhåller webbplatser och applikationer. Det delas huvudsakligen upp i Frontend (det användaren ser och interagerar med, byggt med HTML, CSS, JavaScript) och Backend (logiken, databasen och servern som driver allt, ofta med språk som PHP). Förståelsen för hur klient och server kommunicerar via HTTP(S) i en Request-Response Cycle är central.
I nästa del tittar vi närmare på webbens historia och framtid.
Webbens Historia och Framtid
För att verkligen förstå webbutveckling idag är det värdefullt att känna till dess rötter och vart den är på väg. Webben, som vi känner den, är inte så gammal, men dess utveckling har varit explosionsartad.
Från Akademiskt Projekt till Global Plattform
-
Början (1989-1991): Allt startade vid CERN, det europeiska partikelfysiklaboratoriet. Forskaren Tim Berners-Lee ville skapa ett bättre sätt för forskare att dela information. Han uppfann de grundläggande teknologierna:
- HTML (HyperText Markup Language): För att strukturera dokument.
- HTTP (HyperText Transfer Protocol): För att överföra dokumenten.
- URL (Uniform Resource Locator): För att ge varje dokument en unik adress. Han skapade också den första webbläsaren och webbservern. 1991 lanserades den allra första webbplatsen.
-
De Tidiga Åren (1990-talet): Webben växte långsamt först, främst inom akademiska kretsar. De första webbläsarna som Mosaic och senare Netscape Navigator gjorde webben tillgänglig för en bredare publik. Webbplatserna var mestadels statiska – enkla HTML-sidor med text och bilder, länkade till varandra. CSS introducerades för att separera utseende från struktur, och JavaScript började användas för enkel interaktivitet.
-
Web 2.0 och Dynamik (Sent 90-tal - 2000-tal): Behovet av mer interaktiva och dynamiska webbplatser växte. Detta ledde till utvecklingen av backend-språk (som PHP, ASP) och databaser. Nu kunde webbplatser:
- Lagra användardata.
- Skapa innehåll dynamiskt.
- Möjliggöra användarinteraktion (kommentarer, forum, e-handel). Detta var eran för "Web 2.0", med fokus på sociala medier, bloggar och användargenererat innehåll.
-
Mobilens Era och Ramverk (2010-talet): Smarttelefonernas framväxt revolutionerade webben igen. Responsiv design blev avgörande – webbplatser måste anpassa sig till olika skärmstorlekar. Samtidigt utvecklades kraftfulla frontend-ramverk (som React, Angular, Vue.js) och backend-ramverk (som Laravel, Express) för att göra det enklare och snabbare att bygga komplexa webbapplikationer.
Nutid och Framtid
Webbutvecklingen står aldrig stilla. Några trender som formar webben idag och imorgon inkluderar:
- Progressive Web Apps (PWA): Webbapplikationer som beter sig mer som native mobilappar (kan installeras, skicka notiser, fungera offline).
- Single Page Applications (SPA): Webbapplikationer där innehållet uppdateras dynamiskt utan att hela sidan behöver laddas om, vilket ger en snabbare och smidigare användarupplevelse (ofta byggda med frontend-ramverk).
- API-driven utveckling: Backend fokuserar på att skapa API:er som olika klienter (webb, mobilappar) kan konsumera.
- WebAssembly (Wasm): Tillåter kod skriven i andra språk (som C++, Rust) att köras i webbläsaren med nära native prestanda, vilket öppnar för mer krävande applikationer som spel och videoredigering.
- AI och Machine Learning: Integration av AI-funktioner direkt i webbapplikationer (chatbots, rekommendationssystem, bildanalys).
- Fokus på prestanda och säkerhet: Ständig utveckling för att göra webben snabbare och säkrare.
- Tillgänglighet (Accessibility): Ökad medvetenhet och krav på att webbplatser ska vara användbara för alla, oavsett funktionsvariationer.
Lär dig följa gällande standard
När webben var ung så var det inte ovanligt att vissa webbplatser fungerade endast om man använde speciella webbläsare. Som webbutvecklare är det viktigt att följa standarder som anger hur ny funktionalitet implementeras.
W3C är en organisation som publicerar gällande riktilinjer för webbens framtida utveckling. Det är bra att ibland läsa den dokumentation som finns tillgänglig för en viss webbstandard. Se mer här:
https://www.w3.org/TR/
Sammanfattning
Webben har utvecklats från enkla, statiska textsidor till komplexa, dynamiska och interaktiva applikationer som är centrala i våra liv. Resan har gått via införandet av CSS och JavaScript, backend-teknologier, mobilanpassning och kraftfulla ramverk. Framtiden pekar mot ännu mer integrerade, intelligenta och högpresterande webbupplevelser.
Att förstå denna utveckling ger perspektiv på varför vi använder de verktyg och tekniker vi gör idag. Nästa avsnitt ger en översikt över den moderna webbutvecklarens verktygslåda.
Översikt över Webbutvecklarens Verktygslåda
Precis som en snickare behöver hammare och såg, behöver en webbutvecklare en uppsättning verktyg och tekniker för att bygga webbplatser och applikationer. Denna "verktygslåda" innehåller allt från grundläggande språk till avancerade ramverk och hjälpprogram.
Varför behöver vi dessa verktyg? Att bygga för webben involverar olika lager – struktur, utseende, interaktivitet, serverlogik, datalagring. Olika verktyg är specialiserade på olika delar av denna process.
Låt oss titta på de viktigaste kategorierna:
1. Grundläggande Byggstenar (Frontend)
Dessa är kärnan i allt som visas i en webbläsare:
- HTML (HyperText Markup Language): Definierar strukturen och innehållet på en webbsida (rubriker, stycken, listor, bilder, länkar). Tänk på det som skelettet.
- CSS (Cascading Style Sheets): Bestämmer presentationen och utseendet (färger, typsnitt, layout, positionering, responsivitet). Tänk på det som kläderna och sminket.
- JavaScript: Lägger till interaktivitet och dynamiskt beteende (reagera på klick, validera formulär, hämta data, skapa animationer). Tänk på det som musklerna och nervsystemet.
2. Backend-Teknologier (Serversidan)
För att hantera logik, data och serverinteraktion:
- Serverspråk: Språk som körs på webbservern för att bearbeta förfrågningar, interagera med databaser och generera dynamiskt innehåll. Vanliga exempel:
- PHP: Mycket populärt, speciellt inom WordPress-världen (det vi fokuserar på).
- Node.js: Låter dig köra JavaScript på servern.
- Python: Används ofta med ramverk som Django och Flask.
- Ruby: Känt för ramverket Ruby on Rails.
- Java, C#: Vanliga i större företagsmiljöer.
- Databaser: För att lagra och hantera data på ett strukturerat sätt.
- SQL-databaser (Relationella): Lagrar data i tabeller med rader och kolumner (t.ex. MySQL, PostgreSQL). Vanligt för strukturerad data som användarprofiler, produkter.
- NoSQL-databaser (Icke-relationella): Mer flexibla datamodeller (t.ex. MongoDB, Redis). Används ofta för ostrukturerad data, stora datamängder eller specifika behov som cachning.
3. Ramverk och Bibliotek
Dessa är förbyggda kodsamlingar som hjälper utvecklare att arbeta snabbare och mer strukturerat genom att erbjuda färdiga lösningar på vanliga problem:
- Frontend-ramverk/bibliotek: För att bygga komplexa användargränssnitt (t.ex. React, Angular, Vue.js, Svelte).
- Backend-ramverk: Ger struktur och verktyg för att bygga serverapplikationer (t.ex. Laravel (PHP), Express (Node.js), Django (Python), Ruby on Rails).
- CSS-ramverk: Ger färdiga komponenter och stilar för snabbare design (t.ex. Bootstrap, Tailwind CSS).
4. Verktyg för Utvecklingsprocessen
Dessa hjälper till med kodning, samarbete och underhåll:
- Texteditor/IDE (Integrated Development Environment): Program för att skriva och redigera kod (t.ex. Visual Studio Code (VS Code), Sublime Text, WebStorm). Erbjuder ofta funktioner som syntaxmarkering, kodkomplettering och felsökning.
- Webbläsarens Utvecklarverktyg: Inbyggda verktyg i webbläsare (som Chrome DevTools, Firefox Developer Tools) för att inspektera HTML/CSS, felsöka JavaScript, analysera nätverkstrafik och prestanda. Oumbärliga!
- Versionshanteringssystem: För att spåra ändringar i koden över tid och samarbeta med andra.
- Git: Det absolut vanligaste systemet.
- GitHub/GitLab/Bitbucket: Plattformar för att hosta Git-repositories (kodarkiv) online och underlätta samarbete.
- Terminal/Kommandotolk: Ett textbaserat gränssnitt för att interagera med datorn, köra kommandon, installera verktyg och hantera filer. Väldigt viktigt för utvecklare (t.ex. Bash, Zsh på Linux/macOS, PowerShell eller WSL på Windows).
- Pakethanterare: Verktyg för att installera och hantera externa bibliotek och beroenden (t.ex. npm/yarn för Node.js/JavaScript, Composer för PHP).
Sammanfattning
Webbutvecklarens verktygslåda är bred och innehåller allt från grundläggande språk (HTML, CSS, JS) och backend-teknologier (PHP, Node.js, databaser) till ramverk (React, Laravel) och viktiga hjälpprogram (VS Code, Git, Terminal). Att känna till dessa verktyg och förstå deras syfte är avgörande för att kunna bygga moderna webbapplikationer.
I nästa avsnitt går vi igenom hur du installerar några av de mest grundläggande verktygen vi kommer använda i denna kurs.
Installera och Konfigurera Verktyg
Nu när vi har en översikt över vad webbutveckling är och vilka verktyg som används, är det dags att sätta upp vår egen grundläggande utvecklingsmiljö. Vi behöver några centrala verktyg för att kunna skriva kod, spara dess historik och köra kommandon.
Mål: I detta avsnitt ser vi till att du har följande installerat och redo att användas:
- Visual Studio Code (VS Code): En modern och kraftfull texteditor.
- Git: För versionshantering av vår kod.
- En Terminal: För att interagera med datorn via kommandon.
1. Visual Studio Code (VS Code)
VS Code är en gratis, populär och mycket utbyggbar kod-editor från Microsoft. Den fungerar på Windows, macOS och Linux.
-
Varför VS Code? Den har inbyggt stöd för många språk (inklusive HTML, CSS, JavaScript, PHP), bra integration med Git, en stor marknadsplats för tillägg (extensions) och är relativt resurssnål.
-
Installation:
- Gå till den officiella webbplatsen: https://code.visualstudio.com/
- Ladda ner installationsprogrammet för ditt operativsystem (Windows, macOS, Linux).
- Kör installationsprogrammet och följ instruktionerna. Acceptera standardinställningarna om du är osäker.
-
Grundläggande konfiguration:
- Svenskt språkpaket (ej rekommenderat): Engleska är det universala språket inom programmering. Majoriteten av arbetsplatser använder språket för all kommunikation. Men om du bara vill komma igång med programmering och Svenska fungerar mycket bättre. Öppna VS Code. Tryck på
Ctrl+Shift+P
(ellerCmd+Shift+P
på Mac) för att öppna kommandopaletten. SkrivConfigure Display Language
, välj det och sedanInstall additional languages...
. Sök efterSwedish Language Pack for Visual Studio Code
och installera det. Starta om VS Code när du uppmanas. - Utforska tillägg (Extensions): Klicka på ikonen för tillägg i sidopanelen (ser ut som fyra rutor, en separerad). Här kan du söka efter och installera tillägg som kan underlätta ditt arbete. Några populära för webbutveckling är:
Prettier - Code formatter
: Formaterar din kod automatiskt.Live Server
: Startar en lokal utvecklingsserver med live-reload för statiska sidor.PHP Intelephense
: Ger bättre kodkomplettering och analys för PHP. (Kan göras senare)
- Svenskt språkpaket (ej rekommenderat): Engleska är det universala språket inom programmering. Majoriteten av arbetsplatser använder språket för all kommunikation. Men om du bara vill komma igång med programmering och Svenska fungerar mycket bättre. Öppna VS Code. Tryck på
2. Git
Git är ett distribuerat versionshanteringssystem. Det låter dig spara ögonblicksbilder (commits) av din kod, gå tillbaka till tidigare versioner, skapa olika utvecklingsgrenar (branches) och samarbeta med andra.
-
Varför Git? Det är industristandard och en fundamental färdighet för alla utvecklare. Det hjälper dig att:
- Hålla ordning på ändringar.
- Undvika att förlora arbete.
- Experimentera säkert med ny kod.
- Samarbeta på projekt (ofta via plattformar som GitHub).
-
Installation:
- Windows: Ladda ner och installera Git for Windows från https://git-scm.com/download/win. Under installationen, välj de rekommenderade inställningarna om du är osäker. Se till att Git kan användas från kommandotolken (ofta standardvalet).
Git Bash
kommer också installeras, vilket ger dig en bra terminalmiljö. - macOS: Git är ofta förinstallerat. Öppna Terminalen (Program > Verktygsprogram > Terminal) och skriv
git --version
. Om du får ett versionsnummer är det installerat. Annars kan du installera det via Xcode Command Line Tools (körxcode-select --install
i Terminalen) eller ladda ner det från https://git-scm.com/download/mac. - Linux (Debian/Ubuntu): Öppna terminalen och kör:
sudo apt update && sudo apt install git
- Linux (Fedora): Öppna terminalen och kör:
sudo dnf install git
- Windows: Ladda ner och installera Git for Windows från https://git-scm.com/download/win. Under installationen, välj de rekommenderade inställningarna om du är osäker. Se till att Git kan användas från kommandotolken (ofta standardvalet).
-
Grundläggande konfiguration (Gör detta efter installation!): Öppna din terminal (Terminal på Mac/Linux, Git Bash eller Kommandotolken/PowerShell på Windows) och kör följande kommandon, men byt ut namnet och e-postadressen mot dina egna. Dessa används för att identifiera vem som gjort ändringarna i Git.
git config --global user.name "Ditt Namn" git config --global user.email "din.epost@example.com"
3. Terminalen / Kommandotolken
Terminalen (även kallad kommandotolk, shell, command line) är ett textbaserat gränssnitt där du kan skriva kommandon för att interagera med ditt operativsystem och olika program (som Git).
-
Varför Terminalen? Många utvecklingsverktyg och processer (som att köra Git-kommandon, installera paket, starta servrar) hanteras effektivt via terminalen. Det är ett kraftfullt verktyg att bli bekväm med.
-
Vilken ska jag använda?
- macOS: Använd den inbyggda
Terminal
-appen (finns i/Applications/Utilities/
eller/Program/Verktygsprogram/
). - Linux: Använd den terminalemulator som följer med din distribution (t.ex. GNOME Terminal, Konsole).
- Windows:
- Git Bash: Installeras med Git for Windows och ger en Unix-liknande miljö, vilket ofta är att föredra för webbutveckling.
- Windows Terminal: En modern terminalapp från Microsoft som kan köra PowerShell, Kommandotolken (cmd) och WSL. Rekommenderas om du vill ha en mer modern Windows-upplevelse. Kan installeras från Microsoft Store.
- WSL (Windows Subsystem for Linux): Låter dig köra en riktig Linux-miljö direkt på Windows. Lite mer avancerat att sätta upp, men väldigt kraftfullt för webbutveckling. (Se Microsofts WSL-dokumentation för installationsguide).
- macOS: Använd den inbyggda
-
Testa: Öppna din valda terminal och prova att skriva
git --version
. Om du ser ett versionsnummer fungerar både terminalen och Git-installationen!
Sammanfattning
Du bör nu ha installerat och grundkonfigurerat Visual Studio Code för att skriva kod, Git för att hantera kodens historik, och ha tillgång till en Terminal för att köra kommandon. Dessa tre verktyg utgör grunden i vår utvecklingsmiljö för denna kurs.
I nästa kapitel börjar vi använda dessa verktyg när vi dyker ner i HTML och lär oss grunderna i Git.
Kapitel 2: HTML och Git – Webbens byggstenar och kodens historik
I det föregående kapitlet fick vi en överblick över webbutvecklingens värld. Nu är det dags att kavla upp ärmarna och börja bygga!
Detta kapitel introducerar två fundamentala teknologier som alla webbutvecklare måste behärska:
- HTML (HyperText Markup Language): Språket vi använder för att definiera strukturen och innehållet på en webbsida. Tänk på det som skelettet som håller allt på plats. Utan HTML finns det ingen webbsida.
- Git: Systemet vi använder för att spara och hantera olika versioner av vår kod. Det är som en tidsmaskin och ett samarbetsverktyg i ett – helt avgörande när projekt växer eller när flera personer arbetar tillsammans.
Varför dessa två tillsammans?
När du börjar skriva HTML-kod, även för enkla sidor, vill du kunna spara ditt arbete på ett säkert sätt, kunna gå tillbaka om något går fel och ha en metod för att dela din kod. Git ger dig den tryggheten och strukturen redan från början.
Vad kommer du att lära dig i detta kapitel?
- Grunderna i HTML5: De vanligaste taggarna för att skapa text, rubriker, listor, länkar och bilder.
- Struktur och semantik: Varför det är viktigt att använda rätt HTML-element för rätt syfte – inte bara för utseendet, utan också för sökmotorer och tillgänglighet.
- Tillgänglighet (accessibility): Hur HTML kan användas för att göra webbplatser användbara för fler människor, inklusive de med funktionsvariationer.
- Introduktion till Git: Vad versionshantering är och varför det är så viktigt.
- Grundläggande Git-kommandon: Hur du praktiskt kommer igång med att skapa ett repository, spara ändringar (commits) och se historik.
- Arbeta med GitHub: Hur du använder en plattform som GitHub för att lagra din kod online och (senare) samarbeta med andra.
- Praktiska övningar: Du får bygga enkla HTML-strukturer och hantera dem med Git.
Låt oss börja med att lägga grunden för våra webbsidor med HTML!
Grunderna i HTML5: Att strukturera webbens innehåll
HTML (HyperText Markup Language) är skelettet i varje webbsida. Det är inte ett programmeringsspråk, utan ett märkspråk (markup language). Vi använder taggar (tags) för att berätta för webbläsaren hur innehållet ska struktureras och vad de olika delarna betyder.
Mål:
Förstå grundstrukturen i ett HTML-dokument och lära dig de vanligaste taggarna för att skapa text, rubriker, listor, länkar och bilder.
HTML-dokumentets grundstruktur
Varje HTML-sida följer en grundläggande mall. Här är ett minimalt exempel:
<!DOCTYPE html>
<html lang="sv">
<head>
<meta charset="UTF-8">
<title>Min första webbsida</title>
</head>
<body>
<h1>Hej världen!</h1>
<p>Detta är min första webbsida.</p>
</body>
</html>
Förklaring av delarna:
<!DOCTYPE html>
: Deklaration som talar om för webbläsaren att dokumentet är HTML5. Ska alltid vara först.<html>
: Rotelementet (root element) som omsluter allt innehåll på sidan (förutomDOCTYPE
). Attributetlang="sv"
anger att sidans språk är svenska.<head>
: Innehåller metadata om dokumentet – information som inte visas direkt på sidan men är viktig för webbläsare och sökmotorer.<meta charset="UTF-8">
: Anger teckenkodningen.UTF-8
stöder de flesta tecken (inklusive å, ä, ö).<title>
: Sidans titel, visas i webbläsarens flik och används av sökmotorer.
<body>
: Innehåller allt synligt innehåll på webbsidan – text, rubriker, bilder, länkar, listor, etc.
Taggar, element och attribut
- Taggar (tags): Markerar början och slutet på ett element. Skrivs inom vinkelparenteser (
< >
). De flesta taggar kommer i par: en starttagg (t.ex.<p>
) och en sluttagg (t.ex.</p>
). - Element: Ett komplett HTML-element består av en starttagg, innehåll och en sluttagg. Exempel:
<p>Detta är en paragraf.</p>
.- Tomma element (empty elements): Har ingen sluttagg, t.ex.
<br>
,<img>
,<meta>
.
- Tomma element (empty elements): Har ingen sluttagg, t.ex.
- Attribut (attributes): Ger extra information om ett element. Skrivs i starttaggen och består av namn och värde (inom citationstecken). Exempel:
<img src="bild.jpg" alt="Beskrivning">
.lang
: Anger språk.src
: (source) Bildens källa.alt
: (alternative text) Alternativ text för bilder (viktigt för tillgänglighet).href
: (hypertext reference) Länkens mål.
Vanliga HTML-element
Rubriker (<h1>
till <h6>
)
Används för att definiera rubriknivåer. <h1>
är viktigast (sidans huvudrubrik), <h6>
minst viktig.
<h1>Huvudrubrik</h1>
<h2>Underrubrik</h2>
<h3>Under-underrubrik</h3>
Paragrafer (stycken) (<p>
)
Används för att gruppera text i stycken.
<p>Detta är det första stycket med text.</p>
<p>Detta är ett andra stycke.</p>
Länkar (anchors) (<a>
)
Skapar hyperlänkar till andra sidor eller resurser. href
är obligatoriskt och anger destinationen.
<a href="https://www.google.com">Sök på Google</a>
<a href="annan_sida.html">Länk till en lokal sida</a>
Bilder (<img>
)
Bäddar in bilder. Tomt element (ingen sluttagg). src
anger bildens källa, alt
är alternativ text.
<img src="bilder/logo.png" alt="Företagets logotyp">
Listor
- Oordnade listor (unordered lists):
<ul>
(punkter) - Ordnade listor (ordered lists):
<ol>
(nummer) - Listelement (list items):
<li>
(används i<ul>
eller<ol>
)
<ul>
<li>Äpple</li>
<li>Banan</li>
<li>Päron</li>
</ul>
<ol>
<li>Steg 1: Gör något</li>
<li>Steg 2: Gör något annat</li>
<li>Steg 3: Klart!</li>
</ol>
Radbrytning (<br>
)
Skapar en enkel radbrytning. Använd sparsamt – strukturera hellre med paragrafer.
Kommentarer (<!-- ... -->
)
Kommentarer syns inte i webbläsaren men är användbara för anteckningar i koden.
<!-- Detta är en kommentar -->
HTML – en standard
HTML utvecklas ständigt. Använd alltid den aktuella standarden:
https://html.spec.whatwg.org/multipage/
Sammanfattning
- HTML är grunden för allt webbinnehåll.
- Ett HTML-dokument består av
<!DOCTYPE>
,<html>
,<head>
,<body>
. - Taggar, element och attribut används för att strukturera och beskriva innehållet.
- Vanliga element: rubriker (
<h1>
–<h6>
), paragrafer (<p>
), länkar (<a>
), bilder (<img>
), listor (<ul>
,<ol>
,<li>
). - Skriv ren och korrekt HTML – det är första steget mot fungerande webbsidor.
I nästa avsnitt lär du dig hur semantisk HTML kan ge ditt innehåll ännu mer mening och struktur.
Strukturera och ge semantisk betydelse åt webbinnehåll med HTML5
Vi har lärt oss de grundläggande HTML-taggarna för att skapa innehåll. Men hur organiserar vi innehållet på ett logiskt sätt? Och hur kan vi ge innehållet mening utöver bara dess utseende? Det är här struktur och semantik kommer in i bilden.
- Struktur: Handlar om hur vi organiserar och grupperar innehållet på sidan med hjälp av olika HTML-element.
- Semantik: Handlar om att använda HTML-element som korrekt beskriver innebörden eller syftet med innehållet de omsluter.
Varför är detta viktigt?
- Tillgänglighet (accessibility): Skärmläsare och andra hjälpmedel använder HTML-strukturen och semantiken för att förstå och navigera på sidan. Korrekt semantik gör webbplatsen användbar för fler människor.
- Sökmotoroptimering (SEO): Sökmotorer som Google analyserar HTML-strukturen för att förstå vad sidan handlar om och rangordna den i sökresultaten. Semantisk HTML hjälper dem att göra ett bättre jobb.
- Underhållbarhet: En välstrukturerad och semantisk kodbas är lättare att läsa, förstå och underhålla för dig själv och andra utvecklare.
- Styling med CSS: Även om CSS styr utseendet, ger en bra HTML-struktur tydliga "krokar" som CSS kan fästa vid för att applicera stilar.
Allmänna strukturelement: <div>
och <span>
Historiskt (och fortfarande idag) används ofta två allmänna (generiska) element för att gruppera innehåll:
<div>
(division): Ett blockelement (block-level element). Används för att gruppera större sektioner av innehåll eller för layoutändamål. Börjar på en ny rad och tar upp hela tillgängliga bredden.<span>
: Ett inline-element. Används för att gruppera mindre delar av text eller annat inline-innehåll, ofta för att applicera specifik styling eller för att identifiera en textdel med JavaScript. Börjar inte på en ny rad.
Problem: <div>
och <span>
är icke-semantiska. De säger ingenting om vad innehållet de omsluter faktiskt representerar.
<!-- Icke-semantiskt exempel -->
<div id="header">...</div>
<div id="nav">...</div>
<div class="article">
<div class="headline">Artikelrubrik</div>
<p>...</p>
</div>
<div id="footer">...</div>
Detta fungerar, men det kräver att man inspekterar id
eller class
-attribut för att gissa sig till syftet.
HTML5 semantiska strukturelement
HTML5 introducerade nya element specifikt för att ge semantisk betydelse åt olika delar av en webbsidas struktur. Dessa gör koden tydligare och mer meningsfull.
graph TD subgraph Sida Header[<header>] Nav[<nav>] Main[<main>] Aside[<aside>] Footer[<footer>] Header --> Nav Header --> Main Nav --> Main Main --- Aside Main --> Footer Aside --> Footer end subgraph MainContent direction LR Section1[<section>] Article1[<article>] Section2[<section>] Main --> Section1 Main --> Article1 Main --> Section2 end
Diagram: Vanlig sidlayout med HTML5 semantiska element.
Här är de viktigaste:
<header>
: Representerar introducerande innehåll för en sida eller en sektion. Innehåller ofta sidans logotyp, huvudrubrik (<h1>
), och kanske primär navigering.<nav>
: Representerar en sektion med navigeringslänkar (t.ex. huvudmenyn, länkar inom sidan).<main>
: Representerar huvudinnehållet i dokumentet. Det ska bara finnas ett<main>
-element per sida, och det ska inte placeras inuti<article>
,<aside>
,<header>
,<nav>
, eller<footer>
.<article>
: Representerar en fristående, komplett innehållsdel som skulle kunna distribueras oberoende av resten av sidan (t.ex. ett blogginlägg, en forumkommentar, en nyhetsartikel).<section>
: Representerar en tematisk gruppering av innehåll, vanligtvis med en egen rubrik (<h2>
–<h6>
). Används när det inte finns något mer specifikt semantiskt element (som<article>
eller<nav>
). Tänk på det som ett kapitel i en bok.<aside>
: Representerar innehåll som är tangentiellt relaterat till innehållet runt omkring det (t.ex. en sidopanel med relaterade länkar, författarinformation, reklam). Kan ses som en fotnot eller en sidnotering.<footer>
: Representerar sidfoten för en sida eller sektion. Innehåller ofta copyrightinformation, länkar till sekretesspolicy, kontaktuppgifter.
Exempel: Strukturera och ge semantisk betydelse
<header>
<h1>Webbplatsens Titel</h1>
<nav>
<ul>
<li><a href="/">Hem</a></li>
<li><a href="/om">Om oss</a></li>
</ul>
</nav>
</header>
<main>
<article>
<h2>Artikelrubrik</h2>
<p>...</p>
<section>
<h3>Kommentarer</h3>
<p>...</p>
</section>
</article>
<aside>
<h3>Relaterade länkar</h3>
<ul>...</ul>
</aside>
</main>
<footer>
<p>© 2024 Webbplats AB</p>
</footer>
Denna version är mycket tydligare. Bara genom att titta på taggarna förstår vi syftet med de olika sektionerna.
När ska man använda <div>
?
Även med de semantiska elementen finns det fortfarande tillfällen då <div>
är lämpligt:
- Endast för styling/layout: Om du behöver gruppera element enbart för att applicera CSS-regler (t.ex. skapa en container för att centrera innehåll) och det inte finns något semantiskt element som passar, är
<div>
rätt val. - JavaScript-krokar: Om du behöver ett element att fästa JavaScript-funktionalitet vid och ingen semantisk tagg passar.
Försök dock alltid att först använda ett semantiskt element om det finns ett som beskriver innehållets syfte.
Sammanfattning
Att använda semantisk HTML handlar om att välja de HTML-element som bäst beskriver innebörden av ditt innehåll. HTML5 erbjuder specifika element som <header>
, <nav>
, <main>
, <article>
, <section>
, <aside>
, och <footer>
för att strukturera sidans huvuddelar på ett meningsfullt sätt. Detta förbättrar tillgänglighet, SEO och kodens läsbarhet jämfört med att bara använda generiska <div>
-element.
I nästa avsnitt fokuserar vi specifikt på hur HTML-element bidrar till att göra webbplatser mer
HTML och tillgänglighet (accessibility)
Vi har pratat om semantisk HTML och hur det hjälper maskiner (som sökmotorer och skärmläsare) att förstå vårt innehåll. En av de absolut viktigaste anledningarna till att använda korrekt, semantisk HTML är webbtillgänglighet (ofta förkortat a11y – 'a' följt av 11 bokstäver och ett 'y').
Vad är webbtillgänglighet?
Det handlar om att designa och utveckla webbplatser och applikationer så att de kan användas av alla människor, oavsett deras förmågor eller funktionsvariationer. Detta inkluderar personer med:
- Synnedsättningar (blinda, svagsynta, färgblinda)
- Hörselnedsättningar
- Motoriska funktionsnedsättningar (svårt att använda mus eller tangentbord)
- Kognitiva funktionsnedsättningar (inlärningssvårigheter, minnesproblem)
Varför är det viktigt?
- Mänskliga rättigheter: Alla har rätt att ta del av information och tjänster online.
- Lagar och regler: Många länder (inklusive inom EU) har lagkrav på att offentliga och vissa privata webbplatser ska vara tillgängliga (t.ex. Webbtillgänglighetsdirektivet).
- Bättre användarupplevelse för alla: Principer för god tillgänglighet leder ofta till en bättre och tydligare upplevelse för samtliga användare.
- SEO: God tillgänglighet och god SEO går ofta hand i hand, eftersom båda gynnas av tydlig struktur och semantik.
Hur HTML bidrar till tillgänglighet
Att använda HTML korrekt är grunden för en tillgänglig webbplats. Här är några nyckelområden där HTML spelar en avgörande roll:
1. Semantisk struktur
- Använd semantiska element som
<header>
,<nav>
,<main>
,<article>
,<section>
,<aside>
,<footer>
för att definiera sidans regioner. Detta gör det möjligt för skärmläsaranvändare att snabbt hoppa mellan olika delar av sidan. - Använd rubriktaggar (
<h1>
–<h6>
) i korrekt hierarkisk ordning för att skapa en logisk disposition. - Använd listor (
<ul>
,<ol>
,<li>
) för att gruppera relaterade objekt.
2. Alternativtext för bilder (alt
-attributet)
- ALLTID inkludera ett meningsfullt
alt
-attribut på<img>
-taggar. - Beskriv syftet eller innehållet i bilden kortfattat.
- Om bilden är dekorativ (inte förmedlar information), använd ett tomt alt-attribut:
alt=""
. Då ignoreras bilden av skärmläsare.
<!-- Bra alt-text -->
<img src="hund.jpg" alt="En glad golden retriever som leker i parken">
<!-- Dekorativ bild -->
<img src="gra_linje.png" alt="">
3. Beskrivande länktexter
- Undvik vaga länktexter som "Klicka här" eller "Läs mer".
- Länktexten ska tydligt beskriva vart länken leder, även när den läses utanför sitt sammanhang.
<!-- Undvik -->
<p>För mer information, <a href="rapport.pdf">klicka här</a>.</p>
<!-- Föredra -->
<p>Läs hela <a href="rapport.pdf">årsrapporten för 2023 (PDF)</a>.</p>
4. Formuläretiketter (<label>
)
- Alla formulärkontroller (
<input>
,<textarea>
,<select>
) måste ha en associerad<label>
. - Labeln beskriver vad fältet är till för.
- Använd
for
-attributet på<label>
för att koppla det tillid
-attributet på formulärkontrollen. Detta gör att skärmläsare kan läsa upp etiketten när fältet fokuseras, och användare kan klicka på etiketten för att aktivera fältet.
<label for="user_email">E-postadress:</label>
<input type="email" id="user_email" name="email">
5. Språkattribut (lang
)
- Ange sidans huvudsakliga språk på
<html>
-taggen (<html lang="sv">
). - Om en del av innehållet är på ett annat språk, ange det med
lang
-attributet på det omslutande elementet. Detta hjälper skärmläsare att använda korrekt uttal.
<p>Han sa <span lang="en">"Hello world"</span>.</p>
6. Tabeller för data
- Använd tabeller (
<table>
) endast för att presentera tabulär data, inte för layout. - Använd
<thead>
för tabellrubriker,<tbody>
för tabelldata, och<th>
(table header) för rubrikceller. Användscope
-attributet (scope="col"
för kolumnrubriker,scope="row"
för radrubriker) för att tydligt koppla dataceller till sina rubriker.
<table>
<thead>
<tr>
<th scope="col">Produkt</th>
<th scope="col">Pris</th>
</tr>
</thead>
<tbody>
<tr>
<td>Äpple</td>
<td>5 kr</td>
</tr>
</tbody>
</table>
Kort om WAI-ARIA
För mer komplexa komponenter och interaktioner (som ofta skapas med JavaScript) räcker ibland inte vanlig HTML till för att förmedla all nödvändig information till hjälpmedel. Då kan WAI-ARIA (Web Accessibility Initiative – Accessible Rich Internet Applications) användas. ARIA är en uppsättning speciella attribut som kan läggas till i HTML för att förbättra tillgängligheten, t.ex. för att beskriva roller (role="button"
), tillstånd (aria-pressed="true"
) och egenskaper hos dynamiska komponenter.
Vi går inte djupt in på ARIA i denna grundkurs, men det är bra att känna till att det finns för mer avancerade behov.
Sammanfattning
Webbtillgänglighet handlar om att skapa webbplatser som fungerar för alla. Korrekt och semantisk HTML är grunden för detta. Genom att använda semantiska strukturelement, meningsfulla alt
-texter för bilder, beskrivande länktexter, korrekta formuläretiketter (<label>
), och ange språk (lang
), skapar vi en mer robust och tillgänglig upplevelse. Att tänka på tillgänglighet från början är inte bara en god praxis, utan ofta också ett lagkrav och gynnar alla användare.
Nu när vi har en god grund i HTML är det dags att titta på det andra viktiga verktyget i detta kapitel: Git.
Introduktion till Git: Varför Versionshantering?
Föreställ dig att du arbetar på ett viktigt dokument. Du gör ändringar, sparar, gör fler ändringar... men så inser du att en ändring du gjorde för två timmar sedan var fel. Hur hittar du tillbaka till den versionen? Eller tänk dig att du vill prova en ny idé, men är rädd för att förstöra det du redan har. Eller ännu värre, tänk om flera personer behöver arbeta på samma dokument samtidigt?
Det är här versionshantering (version control) kommer in. Ett versionshanteringssystem (Version Control System, VCS) är ett verktyg som hjälper dig att spåra och hantera ändringar i filer över tid.
Git är det absolut mest populära och dominerande distribuerade versionshanteringssystemet idag. Det utvecklades ursprungligen av Linus Torvalds (skaparen av Linux) för att hantera utvecklingen av Linux-kärnan.
Motivation:
Git gör det möjligt att experimentera, samarbeta och alltid kunna återställa eller förstå vad som hänt i projektets historik. Det är en grundläggande färdighet för alla som arbetar med kod.
Varför är Git så viktigt för utvecklare?
-
Historik och återställning:
Git sparar ögonblicksbilder (kallade commits) av ditt projekt. Du kan se exakt vem som ändrade vad och när, och du kan enkelt återgå till vilken tidigare version som helst. Det är som en obegränsad "ångra"-knapp för hela ditt projekt. -
Förgrening (branching):
Git gör det otroligt enkelt att skapa separata "grenar" (branches) av ditt projekt. Du kan arbeta på en ny funktion eller experimentera på en egen gren utan att påverka huvudversionen (ofta kalladmain
). När du är klar kan du sammanfoga (merge) dina ändringar tillbaka till huvudgrenen. -
Samarbete:
Git är designat för distribuerat arbete. Varje utvecklare har en komplett kopia av projektets historik. Plattformar som GitHub, GitLab och Bitbucket bygger på Git och gör det enkelt att dela kod, granska varandras ändringar (Pull Requests) och arbeta tillsammans i team. -
Trygghet:
Genom att regelbundet spara dina ändringar (committa) och eventuellt skicka dem till en fjärrserver (som GitHub), minskar du risken att förlora arbete på grund av hårddiskkrascher eller misstag. -
Industristandard:
Kunskap i Git är i princip ett krav för de flesta utvecklarjobb idag.
Grundläggande koncept i Git
- Repository (repo): En "behållare" eller mapp som innehåller alla filer för ditt projekt samt hela dess ändringshistorik (i en dold mapp kallad
.git
). - Commit: En sparad ögonblicksbild av ditt projekts filer vid en viss tidpunkt. Varje commit har ett unikt ID och ett meddelande som beskriver ändringarna.
- Branch: En oberoende utvecklingslinje. Standardgrenen heter ofta
main
. - Checkout: Processen att byta mellan olika branches eller återställa filer till en specifik commit.
- Merge: Processen att kombinera ändringar från en branch till en annan.
- Clone: Att skapa en lokal kopia av ett befintligt repository (ofta från en fjärrserver som GitHub).
- Push: Att skicka dina lokala commits till ett fjärr-repository (t.ex. på GitHub).
- Pull: Att hämta ändringar från ett fjärr-repository och integrera dem i din lokala branch.
- Working directory: De filer du ser i din projektmapp.
- Staging area (index): Ett mellanläge där du förbereder vilka ändringar som ska inkluderas i nästa commit.
Git-arbetsflöde: Så här hänger det ihop
graph LR subgraph "Lokalt Repository" WD["Working Directory"] -- "git add" --> SA["Staging Area"]; SA -- "git commit" --> LH["Local History/.git"]; end subgraph "Fjärr-Repository (t.ex. GitHub)" RH["Remote History"] end LH -- "git push" --> RH; RH -- "git pull/fetch" --> LH; LH -- "git checkout" --> WD;
Diagram: Förenklat arbetsflöde i Git.
- Working Directory: Dina faktiska filer på datorn.
- Staging Area: Här samlar du ihop de ändringar du vill spara i nästa commit.
- Local History (.git): Här sparas alla commits och projektets historik.
- Remote History: Fjärrkopian av projektet, t.ex. på GitHub.
Sammanfattning
Git är ett kraftfullt versionshanteringssystem som är oumbärligt för modern mjukvaruutveckling. Det hjälper oss att spåra ändringar, samarbeta effektivt, experimentera säkert och undvika dataförlust. Att förstå grundläggande koncept som repository, commit, branch och staging area är nyckeln till att kunna använda Git effektivt.
I nästa avsnitt går vi igenom de vanligaste Git-kommandona du behöver för att komma igång
Grundläggande Git-kommandon: Det dagliga arbetsflödet
Nu när vi förstår varför Git är användbart och känner till de grundläggande koncepten, är det dags att se hur vi använder det i praktiken. Detta görs via kommandon som vi skriver i terminalen (som vi installerade i kapitel 1).
Mål:
Lära oss och förstå de vanligaste Git-kommandona för att skapa ett repository, spåra filer, spara ändringar (commits) och se status och historik.
Förutsättning:
Du har Git installerat och konfigurerat (med ditt namn och e-post, se kapitel 1).
Viktigt:
Alla Git-kommandon börjar med git
. Kommandona körs i terminalen inuti din projektmapp.
1. Skapa ett repository: git init
Det allra första steget för ett nytt projekt är att initiera ett Git-repository.
- Kommando:
git init
- Vad det gör:
Skapar en ny, dold undermapp.git
i din nuvarande mapp. Denna.git
-mapp innehåller all information och historik för ditt repository. Detta behöver bara göras en gång per projekt.
Så här gör du:
- Skapa en ny mapp för ditt projekt (t.ex.
mitt-projekt
). - Navigera in i mappen i din terminal (
cd mitt-projekt
). - Kör kommandot:
Du bör se ett meddelande som liknar:git init
Initialized empty Git repository in /path/to/your/mitt-projekt/.git/
2. Kontrollera status: git status
Detta är ett av de viktigaste och mest använda kommandona. Det visar det aktuella tillståndet för ditt repository.
- Kommando:
git status
- Vad det gör:
Talar om:- Vilken branch du är på.
- Om det finns ändringar i Working Directory som inte är i Staging Area.
- Om det finns filer i Staging Area som väntar på att committas.
- Om det finns filer som Git inte spårar alls (untracked files).
Så här gör du:
- Skapa en ny fil i din projektmapp, t.ex.
index.html
. - Kör
git status
i terminalen. Du kommer se attindex.html
listas under "Untracked files", eftersom Git ser filen men inte spårar ändringar i den än.
3. Lägga till filer i Staging Area: git add
Innan du kan spara en ändring (committa) måste du tala om för Git vilka ändringar som ska inkluderas. Detta görs genom att lägga till dem i Staging Area.
- Kommandon:
git add <filnamn>
: Lägger till en specifik fil (t.ex.git add index.html
).git add .
: Lägger till alla nya och ändrade filer i den nuvarande mappen och dess undermappar till Staging Area. (Punkten.
representerar nuvarande mapp). Används ofta.
- Vad det gör:
Flyttar ändringarna från Working Directory till Staging Area, redo för nästa commit.
Så här gör du:
- Se till att du har skapat
index.html
. - Kör
git add index.html
(ellergit add .
). - Kör
git status
igen. Nu bör du seindex.html
listad under "Changes to be committed".
4. Spara ändringar: git commit
När du har lagt till de ändringar du vill spara i Staging Area, skapar du en commit.
- Kommando:
git commit -m "Ditt commit-meddelande"
- Vad det gör:
Tar en ögonblicksbild av alla filer som finns i Staging Area och sparar den permanent i.git
-mappens historik. Varje commit måste ha ett commit-meddelande (efter-m
) som kortfattat beskriver vad ändringen handlar om (t.ex. "Skapa grundläggande HTML-struktur", "Lägg till bildlänk").- Bra commit-meddelanden är viktiga! De ska vara korta, beskrivande och skrivna i imperativ (t.ex. "Lägg till bild..." istället för "Lade till bilden...").
Så här gör du:
- Se till att du har kört
git add
. - Kör
git commit -m "Skapa tom index.html-fil"
- Kör
git status
igen. Nu bör det stå något i stil med "nothing to commit, working tree clean", vilket betyder att alla spårade ändringar är sparade.
5. Visa historik: git log
För att se listan över alla commits som gjorts i projektet.
- Kommando:
git log
- Vad det gör:
Visar historiken med den senaste committen överst. För varje commit visas dess unika ID (en lång hash-sträng), författaren, datumet och commit-meddelandet.- Tryck
q
för att avsluta loggvisningen om den är lång. git log --oneline
: Visar en mer kompakt vy med bara commit-ID och meddelande.
- Tryck
Så här gör du:
- Gör en ändring i
index.html
(lägg till lite text). - Kör
git add .
- Kör
git commit -m "Lägg till innehåll i index.html"
- Kör
git log
(ellergit log --oneline
). Du bör nu se dina två commits i historiken.
Sammanfattning: Det grundläggande arbetsflödet
Det typiska arbetsflödet när du arbetar med Git lokalt ser ut så här:
- Gör ändringar i dina projektfiler.
- Kontrollera status med
git status
för att se vilka filer som ändrats. - Lägg till ändringar i Staging Area med
git add <filnamn>
ellergit add .
. - Spara ändringarna permanent med
git commit -m "Beskrivande meddelande"
. - (Upprepa)
Du kan när som helst använda git log
för att se historiken och git status
för att se det aktuella läget.
Dessa kommandon (init
, status
, add
, commit
, log
) utgör grunden för att arbeta med Git. I nästa avsnitt tittar vi på hur vi kan koppla vårt lokala repository till en fjärrserver som GitHub.
Arbeta med GitHub: Ditt projekt i molnet
Vi har lärt oss hur Git fungerar lokalt på vår dator för att spåra ändringar. Men den verkliga styrkan med Git, särskilt för samarbete och säkerhetskopiering, kommer när vi kopplar vårt lokala repository till en fjärrserver (remote repository). Den mest populära plattformen för detta är GitHub.
Mål:
Förstå hur du kopplar ett lokalt Git-repository till GitHub och använder de grundläggande kommandona för att skicka (push
) och hämta (pull
) ändringar.
Förutsättning:
Du har ett gratis konto på GitHub. Om inte, skapa ett nu.
Vad är GitHub?
GitHub är en webbaserad plattform för att lagra och hantera Git-repositories i molnet. Utöver lagring erbjuder GitHub:
- Kodbackup: Säkert lagra dina projekt online.
- Samarbete: Arbeta tillsammans i team, granska kod (Pull Requests) och diskutera ändringar.
- Projekthantering: Hantera ärenden (Issues), projekt-tavlor (Projects) och wikis.
- Versionshantering: Grafiskt gränssnitt för att se historik, jämföra versioner och utforska koden.
- Open Source: Hem för miljontals öppen källkods-projekt.
Skapa ett fjärr-repository på GitHub
- Logga in på ditt GitHub-konto.
- Klicka på "+"-ikonen uppe till höger och välj New repository.
- Ge repositoryt ett namn (t.ex.
mitt-projekt
). Det är praktiskt om det matchar din lokala mapp, men det är inget krav. - Lägg till en kort beskrivning (valfritt).
- Välj om det ska vara Public (synligt för alla) eller Private (endast för dig och inbjudna).
- Viktigt: Om du redan har ett lokalt repository, bocka INTE i rutorna för "Add a README file", "Add .gitignore" eller "Choose a license". Om du skapar ett helt nytt projekt direkt på GitHub kan du bocka i dessa.
- Klicka på Create repository.
Koppla ditt lokala repo till GitHub
När du har skapat repositoryt på GitHub visas instruktioner. Leta efter avsnittet "…or push an existing repository from the command line".
Steg 1: Lägg till fjärr-repository
git remote add origin https://github.com/ditt-anvandarnamn/mitt-projekt.git
origin
är standardnamnet för din fjärrserver.- Byt ut URL:en mot den du får från GitHub.
Steg 2: Byt namn på huvudbranch (om nödvändigt)
git branch -M main
- Byter namn på din huvudbranch till
main
(standard på GitHub).
Steg 3: Skicka upp din kod
git push -u origin main
- Skickar dina commits till GitHub.
-u
kopplar din lokala branch till fjärr-branchen, så att du senare kan använda baragit push
ochgit pull
.
Du kan behöva logga in eller autentisera dig mot GitHub (t.ex. med en personlig access token).
Skicka fler ändringar: git push
När du gjort nya ändringar och committat dem:
git push
- Skickar alla nya commits på din nuvarande branch till GitHub.
Hämta ändringar: git pull
Om någon annan (eller du själv från en annan dator) har pushat ändringar till GitHub, hämta dem så här:
git pull
- Hämtar och försöker automatiskt sammanfoga (
merge
) ändringarna från GitHub till din lokala kod. - Kör gärna
git pull
innan du börjar arbeta och innan du pushar egna ändringar.
Klona ett repository: git clone
Om du vill börja arbeta med ett projekt som redan finns på GitHub:
git clone https://github.com/anvandarnamn/projekt.git
- Laddar ner hela repositoryt (alla filer och historik) till en ny mapp på din dator.
- Sätter automatiskt upp
origin
så att du kan användagit push
ochgit pull
.
Sammanfattning
GitHub är en plattform för att lagra och samarbeta kring Git-repositories i molnet. Genom att koppla ditt lokala repo till GitHub kan du:
- Skicka ändringar till GitHub med
git push
. - Hämta ändringar från GitHub med
git pull
. - Klona befintliga projekt med
git clone
.
Att använda Git tillsammans med GitHub är standard inom modern webbutveckling och gör det enkelt att samarbeta, säkerhetskopiera och visa upp dina projekt.
Nu är det dags att öva på dessa HTML- och Git-färdigheter!
Mer Git-kommandon
Introduktion till avancerade Git-tekniker
Nu när du behärskar grunderna i Git är det dags att utforska mer avancerade kommandon och tekniker. Dessa verktyg ger dig större kontroll över ditt versionshanteringssystem och gör dig till en mer effektiv utvecklare.
graph TD A[Git Grundläggande] --> B[Branching & Checkout] A --> C[Merge & Rebase] A --> D[Konflikthantering] A --> E[Konfiguration] B --> B1[git checkout] B --> B2[git switch] B --> B3[Detached HEAD] C --> C1[git merge] C --> C2[git rebase] C --> C3[Fast-forward] D --> D1[Merge conflicts] D --> D2[Konfliktlösning] D --> D3[Merge tools] E --> E1[git config] E --> E2[Globala inställningar] E --> E3[SSH-nycklar]
Git Checkout - Navigera mellan brancher och commits
git checkout
är ett mångsidigt kommando som låter dig "hoppa" mellan olika versioner av ditt projekt.
Växla mellan brancher
# Visa alla brancher
git branch
# Skapa och växla till ny branch
git checkout -b ny-funktion
# Växla till befintlig branch
git checkout main
git checkout utveckling
# Kort för att skapa branch från specifik commit
git checkout -b bugfix-123 abc1234
Checkout till specifika commits
# Visa commit-historik
git log --oneline
# Gå till specifik commit (skapar "detached HEAD")
git checkout abc1234
# Gå tillbaka till senaste commit på current branch
git checkout main
Detached HEAD State
När du gör checkout
till en specifik commit hamnar du i "detached HEAD" tillstånd:
graph LR A[Commit A] --> B[Commit B] B --> C[Commit C] C --> D[Commit D] HEAD -->|Normal| D HEAD2[HEAD detached] -->|git checkout B| B subgraph "Normal state" HEAD D end subgraph "Detached HEAD" HEAD2 B end
# Om du är i detached HEAD och vill behålla ändringar
git switch -c ny-branch-namn
# Eller gå tillbaka utan att spara ändringar
git switch main
Återställa filer med checkout
# Återställ en specifik fil till senaste commit
git checkout -- fil.txt
# Återställ fil till specifik commit
git checkout abc1234 -- fil.txt
# Återställ alla filer i katalog
git checkout -- .
Git Switch - Modernare sätt att växla branches
git switch
introducerades i Git 2.23 som ett tydligare alternativ till git checkout
för branch-hantering:
# Växla till befintlig branch
git switch main
git switch utveckling
# Skapa och växla till ny branch
git switch -c ny-funktion
# Växla tillbaka till föregående branch
git switch -
# Återgå till main från detached HEAD
git switch main
Fördelar med git switch
:
- Tydligare syfte (endast för brancher)
- Säkrare (mindre risk för misstag)
- Modernare syntax
Merge och Rebase - Kombinera ändringar
Git Merge - Traditionell sammanfogning
# Gå till target branch (oftast main)
git switch main
# Hämta senaste ändringar
git pull origin main
# Merge in branch
git merge funktions-branch
# Push merged result
git push origin main
Merge-strategier visualiserade
graph LR A[Commit A] --> B[Commit B] B --> C[Commit C] A --> D[Commit D] D --> E[Commit E] C --> F[Merge Commit] E --> F subgraph "main branch" A B C F end subgraph "feature branch" D E end
Fast-forward merge
När target branch inte har nya commits:
# Main branch har inga nya commits sedan branch skapades
git merge funktions-branch
# Resulterar i fast-forward (ingen merge commit)
graph LR A[Commit A] --> B[Commit B] B --> C[Commit C] C --> D[Commit D] subgraph "Före merge" A B MAIN[main] --> B C D FEATURE[feature] --> D end subgraph "Efter fast-forward" A2[Commit A] --> B2[Commit B] B2 --> C2[Commit C] C2 --> D2[Commit D] MAIN2[main] --> D2 end
Git Rebase - Omskrivning av historik
Rebase "flyttar" dina commits till toppen av target branch:
# Gå till din feature branch
git switch funktions-branch
# Rebase mot main
git rebase main
# Om inga konflikter: push (kan kräva force)
git push --force-with-lease origin funktions-branch
Rebase vs Merge visualiserat
graph TD subgraph "Original state" A1[A] --> B1[B] A1 --> C1[C] C1 --> D1[D] end subgraph "Efter Merge" A2[A] --> B2[B] A2 --> C2[C] C2 --> D2[D] B2 --> M[Merge commit] D2 --> M end subgraph "Efter Rebase" A3[A] --> B3[B] B3 --> C3[C'] C3 --> D3[D'] end
Interaktiv rebase
# Redigera de senaste 3 commits
git rebase -i HEAD~3
# Öppnar editor med alternativ:
# pick = använd commit
# reword = ändra commit-meddelande
# edit = redigera commit
# squash = kombinera med föregående commit
# drop = ta bort commit
När ska du använda vad?
- Merge: Bevarar historik, säkrare, bra för publika branches
- Rebase: Renare historik, linjär utveckling, bra för feature branches
Konflikthantering (Merge Conflicts)
Konflikter uppstår när Git inte kan automatiskt kombinera ändringar.
Förstå konflikter
# När merge skapar konflikter
git merge funktions-branch
# Output: CONFLICT (content): Merge conflict in fil.txt
Konfliktmarkering i filer
// fil.txt efter konflikt
function calculateTotal(items) {
<<<<<<< HEAD
return items.reduce((sum, item) => sum + item.price, 0);
=======
return items.reduce((total, item) => total + item.cost, 0);
>>>>>>> funktions-branch
}
Förklaring:
<<<<<<< HEAD
: Början på dina ändringar (current branch)=======
: Skiljelinje mellan versioner>>>>>>> branch-name
: Slut på den andra branchens ändringar
Lösa konflikter
Manuell lösning
# 1. Öppna fil och redigera manuellt
# Ta bort konfliktmarkeringar och välj önskad version
# 2. Lägg till den lösta filen
git add fil.txt
# 3. Slutför merge
git commit
# (eller för rebase: git rebase --continue)
Använda merge tools
# Konfigurera merge tool (en gång)
git config --global merge.tool vimdiff
# eller: vscode, meld, kdiff3, etc.
# Starta merge tool vid konflikt
git mergetool
# Efter lösning av alla konflikter
git commit
Förebygga konflikter
# Håll din branch uppdaterad
git switch main
git pull origin main
git switch funktions-branch
git rebase main # eller git merge main
# Små, frekventa commits
# Kommunicera med teamet om stora ändringar
# Använd .gitattributes för specifika filtyper
Git Config - Anpassa din Git-miljö
Nivåer av konfiguration
Git har tre nivåer av konfiguration:
# System-nivå (alla användare)
git config --system
# Global nivå (din användare)
git config --global
# Repository-nivå (endast detta projekt)
git config --local
Grundläggande konfiguration
# Sätt användarinfo (obligatoriskt)
git config --global user.name "Ditt Namn"
git config --global user.email "din.email@example.com"
# Standardeditor
git config --global core.editor "code --wait" # VS Code
git config --global core.editor "vim" # Vim
git config --global core.editor "nano" # Nano
# Standardbranch-namn
git config --global init.defaultBranch main
# Radslut-hantering (viktigt för Windows/Mac/Linux)
git config --global core.autocrlf input # Mac/Linux
git config --global core.autocrlf true # Windows
Alias för vanliga kommandon
# Skapa användbara alias
git config --global alias.st status
git config --global alias.co checkout
git config --global alias.br branch
git config --global alias.ci commit
# Avancerade alias
git config --global alias.unstage 'reset HEAD --'
git config --global alias.last 'log -1 HEAD'
git config --global alias.visual '!gitk'
# Vacker log output
git config --global alias.lg "log --oneline --decorate --graph --all"
# Använd alias
git st # istället för git status
git lg # vacker log output
Konfigurationsfil
# Visa all konfiguration
git config --list
# Visa var konfiguration kommer ifrån
git config --list --show-origin
# Redigera global konfiguration direkt
git config --global --edit
Exempel på .gitconfig
fil:
[user]
name = Ditt Namn
email = din.email@example.com
[core]
editor = code --wait
autocrlf = input
[init]
defaultBranch = main
[alias]
st = status
co = checkout
br = branch
ci = commit
lg = log --oneline --decorate --graph --all
[push]
default = simple
[pull]
rebase = false
SSH-nycklar för GitHub/GitLab
Generera SSH-nyckel
# Generera ny SSH-nyckel
ssh-keygen -t ed25519 -C "din.email@example.com"
# För äldre system som inte stödjer ed25519
ssh-keygen -t rsa -b 4096 -C "din.email@example.com"
# Starta SSH agent
eval "$(ssh-agent -s)"
# Lägg till nyckel till agent
ssh-add ~/.ssh/id_ed25519
Konfiguration för GitHub
# Kopiera publik nyckel (Mac)
pbcopy < ~/.ssh/id_ed25519.pub
# Linux
cat ~/.ssh/id_ed25519.pub
# Lägg till i GitHub under Settings > SSH and GPG keys
# Testa anslutningen
ssh -T git@github.com
Avancerade Git-kommandon för nyfikna
Git Cherry-pick
Välj specifika commits från andra branches:
# Applicera en specifik commit på current branch
git cherry-pick abc1234
# Cherry-pick flera commits
git cherry-pick abc1234 def5678
# Cherry-pick ett intervall
git cherry-pick abc1234..def5678
graph LR A[A] --> B[B] A --> C[C] C --> D[D] D --> E[E] B --> F[F] F --> G[G] G --> H[D'] subgraph "main branch" A B F G H end subgraph "feature branch" C D E end
Git Stash - Temporär lagring
# Spara ändringar temporärt
git stash
# Spara med meddelande
git stash push -m "Work in progress på login"
# Lista alla stashes
git stash list
# Applicera senaste stash
git stash pop
# Applicera specifik stash
git stash apply stash@{2}
# Ta bort stash utan att applicera
git stash drop stash@{1}
Git Worktree - Flera arbetsytor
# Skapa ny worktree för branch
git worktree add ../projekt-hotfix hotfix-branch
# Lista alla worktrees
git worktree list
# Ta bort worktree
git worktree remove ../projekt-hotfix
Git Reflog - Återhämtning
# Visa vad som hänt med HEAD
git reflog
# Återställ till tidigare state
git reset --hard HEAD@{2}
# Hitta "förlorade" commits
git reflog --all
Git Bisect - Hitta buggar
# Starta bisect-session
git bisect start
# Markera aktuell commit som dålig
git bisect bad
# Markera tidigare bra commit
git bisect good abc1234
# Git checkar ut commit i mitten - testa och markera
git bisect bad # eller git bisect good
# Fortsätt tills Git hittar problematisk commit
git bisect reset # avsluta
Git Hooks - Automatisering
# Navigera till hooks-mappen
cd .git/hooks
# Skapa pre-commit hook
nano pre-commit
# Exempel: Kör tests innan commit
#!/bin/bash
npm test
if [ $? -ne 0 ]; then
echo "Tests failed, commit aborted"
exit 1
fi
# Gör executable
chmod +x pre-commit
Git Submodules - Externa repositories
# Lägg till submodule
git submodule add https://github.com/user/repo.git libs/external
# Klona projekt med submodules
git clone --recurse-submodules https://github.com/user/project.git
# Uppdatera submodules
git submodule update --remote
Bästa praxis och tips
Commit-meddelanden
# Bra struktur för commit-meddelanden
# <typ>: <kort beskrivning>
#
# <längre beskrivning vid behov>
#
# <referenser till issues>
# Exempel:
feat: add user authentication system
Implement login/logout functionality with JWT tokens.
Includes password hashing and session management.
Fixes #123
Branch-namngivning
# Bra branch-namns konventioner
feature/user-authentication
bugfix/login-error-handling
hotfix/security-vulnerability
release/v2.1.0
Git Flow vs GitHub Flow
Git Flow - Komplexare, strukturerad:
main
- produktionskoddevelop
- utvecklingsbranchfeature/*
- nya funktionerrelease/*
- release förberedelserhotfix/*
- kritiska fixes
GitHub Flow - Enklare, snabbare:
main
- alltid deploybarfeature/*
- korta feature branches- Pull requests för review
- Deploy direkt från main
Felsökning och problemlösning
Vanliga problem och lösningar
Undo senaste commit (behåll ändringar)
git reset --soft HEAD~1
Undo senaste commit (ta bort ändringar)
git reset --hard HEAD~1
Ändra senaste commit-meddelande
git commit --amend -m "Nytt meddelande"
Lägg till fil till senaste commit
git add glömd-fil.txt
git commit --amend --no-edit
Återställa fil till specifik version
git checkout abc1234 -- fil.txt
Push refuseras (diverged history)
# Se vad som skiljer
git log --oneline origin/main..main
# Option 1: Pull och merge
git pull origin main
# Option 2: Pull och rebase
git pull --rebase origin main
# Option 3: Force push (FARLIGT - använd bara på egna branches)
git push --force-with-lease origin main
Debug Git-kommandon
# Visa vad Git gör
export GIT_TRACE=1
git status
# Debug merge
export GIT_MERGE_VERBOSITY=2
git merge branch-name
# Visa konfiguration
git config --list --show-origin
Sammanfattning
Du har nu lärt dig avancerade Git-tekniker som gör dig till en mer kompetent utvecklare:
- Checkout och Switch: Navigera mellan branches och commits säkert
- Merge vs Rebase: Förstå när och hur du ska kombinera ändringar
- Konflikthantering: Lösa merge conflicts effektivt
- Konfiguration: Anpassa Git för din arbetsrutation
- Avancerade verktyg: Cherry-pick, stash, reflog och mer
Nästa steg
- Experimentera med dessa kommandon i test-repositories
- Lär dig Git hooks för automatisering
- Utforska Git GUIs som Sourcetree eller GitKraken
- Fördjupa dig i Git internals för ännu djupare förståelse
Git är ett kraftfullt verktyg - med dessa kunskaper kan du hantera även komplexa utvecklingsprojekt med självförtroende!
Praktiska övningar: HTML och Git
Teori är bra, men det bästa sättet att lära sig HTML och Git är att använda dem! Här är några övningar för att befästa kunskaperna från detta kapitel.
Mål:
Praktiskt tillämpa HTML-taggar för struktur och semantik, samt använda grundläggande Git-kommandon för att spåra och spara ändringar lokalt och på GitHub.
Förutsättningar:
Du har VS Code (eller annan editor), Git och en terminal installerad. Du har också ett GitHub-konto.
Övning 1: Skapa en enkel "Om Mig"-sida
-
Skapa projektmapp:
Skapa en ny mapp, t.ex.om-mig-sida
. -
Initiera Git:
Öppna terminalen, navigera till mappen (cd om-mig-sida
) och kör:git init
-
Skapa HTML-fil:
Skapa en fil med namnetindex.html
i mappen. -
Grundstruktur:
Lägg till grundläggande HTML5-struktur iindex.html
:<!DOCTYPE html>
<html lang="sv">
<head>
med<meta charset="UTF-8">
och<title>
<body>
Ge sidan en passande titel, t.ex. "Om [Ditt Namn]".
-
Lägg till innehåll i
<body>
:- En huvudrubrik (
<h1>
) med ditt namn. - En kort paragraf (
<p>
) som introducerar dig. - En underrubrik (
<h2>
) för "Mina intressen". - En oordnad lista (
<ul>
) med några av dina intressen (<li>
). - En underrubrik (
<h2>
) för "Kontakt". - En paragraf (
<p>
) med en länk (<a>
) till din (påhittade eller riktiga) e-postadress (href="mailto:din.epost@example.com"
). - (Valfritt) En bild (
<img>
) på dig själv eller något relaterat. Glöm intealt
-attributet!
- En huvudrubrik (
-
Första commit:
git status git add index.html git commit -m "Skapa grundläggande Om Mig-sida med innehåll"
-
Validera (bonus):
Klistra in din HTML-kod i W3C Markup Validation Service för att kontrollera att den är korrekt. -
Visa i webbläsare:
Öppnaindex.html
i din webbläsare.
Övning 2: Semantisk struktur och fler ändringar
-
Fortsätt i samma projekt.
-
Lägg till semantiska element:
- Omslut rubriken och introduktionen med
<header>
. - Omslut huvudinnehållet (intressen, kontakt) med
<main>
. - Lägg till en
<footer>
längst ner, t.ex. med copyright. - Om du har flera tydliga delar i
<main>
, omslut dem med<section>
och ge varje sektion en egen rubrik (<h2>
).
- Omslut rubriken och introduktionen med
-
Commit:
git status git add . git commit -m "Lägg till semantisk struktur (header, main, footer, sections)"
-
Visa historik:
git log --oneline
Övning 3: Koppla till GitHub
-
Skapa repo på GitHub:
Skapa ett nytt, tomt repository på GitHub (utan README, .gitignore eller license). -
Koppla lokalt till fjärr:
Följ instruktionerna från GitHub ("…or push an existing repository").
Byt ut URL:en mot den från ditt repo:git remote add origin https://github.com/ditt-anvandarnamn/om-mig-sida.git git branch -M main git push -u origin main
-
Verifiera:
Gå till din repository-sida på GitHub och kontrollera att dinindex.html
och dina commits syns. -
Gör en liten ändring:
Lägg till ytterligare ett intresse i listan iindex.html
. -
Commit och push:
git status git add . git commit -m "Lägg till ett till intresse" git push
-
Verifiera igen:
Uppdatera sidan på GitHub och kontrollera att ändringen syns.
Sammanfattning och nästa steg
Genom dessa övningar har du praktiskt skapat en enkel HTML-sida, strukturerat den semantiskt och använt Git för att spåra ändringar lokalt och synkronisera dem med ett fjärr-repository på GitHub. Detta är grunden för arbetsflödet vi kommer använda genom resten av kursen.
I nästa kapitel introducerar vi CSS för att börja styla våra HTML-sidor och ge dem ett visuellt utseende.
Tekniska Intervjufrågor: Git och Versionshantering
Detta avsnitt innehåller exempel på tekniska intervjufrågor som kan dyka upp gällande Git och versionshantering. Frågorna är utformade för att testa både teoretisk förståelse och praktisk kunskap.
Använd dessa frågor för att testa din kunskap och förbereda dig för tekniska intervjuer.
Fråga 1: Grundläggande Git-koncept
Fråga: "Förklara skillnaden mellan en 'commit', en 'branch' och ett 'repository' i Git. Ge ett praktiskt exempel på när du skulle använda var och en."
Förslag till svar:
- Repository: En mapp som innehåller alla filer och hela versionshistoriken för ett projekt. Det är som projektets "hem" med all information Git behöver.
- Commit: En sparad ögonblicksbild av projektet vid en specifik tidpunkt, med ett meddelande som beskriver vad som ändrats. Som att ta ett foto av projektets tillstånd.
- Branch: En parallell utvecklingslinje som låter dig arbeta på nya funktioner utan att påverka huvudkoden. Som att skapa en kopia att experimentera på.
Exempel: Du har ett repository för en webbsida. Du skapar en branch för att lägga till en ny funktion, gör flera commits medan du utvecklar, och när du är klar mergar du tillbaka till huvudbranchen.
Fråga 2: Git-arbetsflöde
Fråga: "Beskriv steg-för-steg vad som händer när du kör kommandona: git add .
, git commit -m "Fix bug"
, och git push origin main
."
Förslag till svar:
git add .
: Lägger till alla ändrade filer till staging area (mellanlagret). Filerna är nu förberedda för commit men inte än sparade.git commit -m "Fix bug"
: Skapar en ny commit med meddelandet "Fix bug" som innehåller alla filer från staging area. Ändringarna är nu sparade i lokal versionshistorik.git push origin main
: Skickar den nya commiten från din lokala main-branch till remote repository (t.ex. GitHub) så andra kan se dina ändringar.
Fråga 3: Staging Area
Fråga: "Varför finns staging area i Git? Vad är skillnaden mellan working directory, staging area och repository?"
Förslag till svar: Staging area låter dig välja exakt vilka ändringar som ska inkluderas i nästa commit. Detta ger kontroll och flexibilitet.
- Working Directory: Filerna du ser och redigerar i din projektmapp
- Staging Area: Mellanlagret där du förbereder ändringar för commit
- Repository: Den sparade versionshistoriken i .git-mappen
Fördel: Du kan arbeta på flera funktioner samtidigt men committa dem separat genom att bara lägga till relevanta filer till staging area.
Fråga 4: Konflikthantering
Fråga: "Du försöker göra en git pull
men får meddelandet 'merge conflict'. Vad har hänt och hur löser du det?"
Förslag till svar: Vad som hänt: Någon annan har ändrat samma rader i samma fil som du också ändrat. Git vet inte vilken version som ska behållas.
Lösning:
- Öppna den konfliktmarkerade filen
- Leta efter
<<<<<<<
,=======
, och>>>>>>>
markeringar - Bestäm vilken kod som ska behållas (eller kombinera)
- Ta bort konfliktmarkeringarna
git add
den fixade filengit commit
för att slutföra merge
Fråga 5: Git Log och Historik
Fråga: "Du behöver hitta när en specifik bug introducerades i koden. Vilka Git-kommandon skulle du använda och varför?"
Förslag till svar:
git log --oneline
: Se en översikt av alla commitsgit log --grep="keyword"
: Sök i commit-meddelandengit blame filename
: Se vem som ändrade varje rad och närgit bisect
: Binärsökning för att hitta exakt vilken commit som introducerade buggengit show commitID
: Se exakt vad som ändrades i en specifik commit
Git bisect är särskilt kraftfullt för att hitta när buggar introducerades.
Fråga 6: Ångra ändringar
Fråga: "Du har råkat commita fel kod till din lokala repository men inte pushat än. Hur kan du ångra den senaste commiten och vad är skillnaden mellan de olika sätten?"
Förslag till svar:
git reset --soft HEAD~1
: Ångrar commiten men behåller ändringarna i staging areagit reset --mixed HEAD~1
: (default) Ångrar commiten och tar bort från staging, men behåller ändringarna i working directorygit reset --hard HEAD~1
: Ångrar commiten och tar bort alla ändringar heltgit revert HEAD
: Skapar en ny commit som ångrar den senaste commiten (säkrare om redan pushat)
Regel: Använd reset bara för lokala commits, revert för commits som redan är pushade.
Fråga 7: .gitignore
Fråga: "Du arbetar på ett Node.js-projekt och märker att node_modules
-mappen committas av misstag. Hur fixar du detta och förhindrar att det händer igen?"
Förslag till svar: För att fixa:
- Skapa
.gitignore
-fil i projektets root - Lägg till
node_modules/
i filen git rm -r --cached node_modules/
(tar bort från Git utan att radera lokalt)git commit -m "Remove node_modules and add to gitignore"
Andra vanliga ignore-patterns:
node_modules/
.env
*.log
dist/
.DS_Store
.gitignore ska committas så hela teamet har samma ignore-regler.
Fråga 8: HTML5 Grundstruktur
Fråga: "Förklara vad varje del gör i denna HTML5-struktur och varför de är viktiga:"
<!DOCTYPE html>
<html lang="sv">
<head>
<meta charset="UTF-8">
<title>Min Sida</title>
</head>
<body>
<h1>Välkommen</h1>
</body>
</html>
Förslag till svar:
<!DOCTYPE html>
: Deklarerar att dokumentet använder HTML5-standarden. Måste vara först.<html lang="sv">
: Rotelementet som omsluter allt innehåll.lang="sv"
anger svenska som huvudspråk för tillgänglighet och SEO.<head>
: Innehåller metadata som inte visas direkt - information för webbläsaren och sökmotorer.<meta charset="UTF-8">
: Anger teckenkodning för att stödja internationella tecken (å, ä, ö). Kritiskt viktigt!<title>
: Titeln som visas i webbläsarens flik och används av sökmotorer.<body>
: Allt synligt innehåll på sidan.
Fråga 9: HTML-Element och Attribut
Fråga: "Förklara skillnaden mellan element, taggar och attribut. Ge exempel på när du skulle använda <a>
, <img>
och <ul>
elementen."
Förslag till svar:
- Taggar: Koder inom
< >
som markerar början (<p>
) och slut (</p>
) på element - Element: Kompletta konstruktioner med starttagg, innehåll och sluttagg:
<p>Text</p>
- Attribut: Extra information i starttaggen:
<a href="länk">text</a>
Exempel:
<a href="https://google.com">Sök</a>
: För att skapa klickbara länkar till andra sidor<img src="bild.jpg" alt="Beskrivning">
: För att visa bilder (tomt element, ingen sluttagg)<ul><li>Punkt 1</li><li>Punkt 2</li></ul>
: För punktlistor där ordningen inte spelar roll
Fråga 10: Semantisk HTML vs Generiska Element
Fråga: "Vad är skillnaden mellan dessa två kodexempel och varför är det ena bättre än det andra?"
<!-- Exempel A -->
<div id="header">
<div class="navigation">Meny</div>
</div>
<div class="content">Innehåll</div>
<!-- Exempel B -->
<header>
<nav>Meny</nav>
</header>
<main>Innehåll</main>
Förslag till svar: Exempel B är bättre eftersom det använder semantiska HTML5-element:
Fördelar med semantisk HTML:
- Tillgänglighet: Skärmläsare kan navigera bättre med
<nav>
,<main>
etc. - SEO: Sökmotorer förstår innehållets struktur och syfte
- Läsbarhet: Kod blir självförklarande utan att behöva läsa
id
/class
-attribut - Framtidssäker: Webbstandarder föredrar semantiska element
<div>
ska bara användas för styling/layout när inget semantiskt element passar.
Fråga 11: HTML5 Semantiska Element
Fråga: "Du ska bygga en bloggsida. Förklara hur du skulle använda <header>
, <nav>
, <main>
, <article>
, <aside>
och <footer>
för att strukturera sidan."
Förslag till svar:
<header>
<h1>Min Blogg</h1>
<nav>
<ul>
<li><a href="/">Hem</a></li>
<li><a href="/om">Om</a></li>
</ul>
</nav>
</header>
<main>
<article>
<h2>Blogginläggets titel</h2>
<p>Innehållet i blogginlägget...</p>
</article>
<aside>
<h3>Populära inlägg</h3>
<ul>Relaterade länkar...</ul>
</aside>
</main>
<footer>
<p>© 2024 Min Blogg</p>
</footer>
Varför denna struktur:
<header>
: Sidans topp med titel och huvudnavigering<main>
: Huvudinnehållet (bara ett per sida)<article>
: Fristående innehåll som kan distribueras separat<aside>
: Kompletterande information vid sidan av huvudinnehållet<footer>
: Sidfot med copyright och kontaktinfo
Fråga 12: Tillgänglighet och SEO
Fråga: "Varför är alt
-attributet viktigt för bilder och hur påverkar semantisk HTML sökmotoroptimering?"
Förslag till svar:
alt
-attributet för bilder:
- Tillgänglighet: Skärmläsare läser upp alt-texten för synskadade användare
- Fallback: Visas om bilden inte kan laddas (långsam anslutning, fel URL)
- SEO: Hjälper sökmotorer förstå bildinnehållet
<img src="produktbild.jpg" alt="Röd t-shirt i bomull, storlek M">
Semantisk HTML för SEO:
<h1>
-<h6>
: Hjälper sökmotorer förstå innehållshierarki<article>
: Identifierar huvudinnehåll som är värt att indexera<nav>
: Visar sidans navigationsstuktur- Strukturerad data: Semantiska element gör det lättare för sökmotorer att tolka sidan korrekt
Resultat: Bättre ranking i sökresultat och ökad tillgänglighet.
Tips för Tekniska Intervjuer
- Förklara din tankeprocess medan du svarar
- Använd konkreta exempel från egna projekt när möjligt
- Erkänn om du inte vet och förklara hur du skulle ta reda på svaret
- Ställ följdfrågor för att förtydliga vad intervjuaren söker
Kapitel 3: Grundläggande CSS – Ge Stil åt Webbens Struktur
I det föregående kapitlet lade vi grunden med HTML, webbsidornas skelett, och lärde oss hantera koden med Git. Men enbart HTML ger oss ganska tråkiga och ostrukturerade sidor. Hur får vi dem att se bra ut? Hur kontrollerar vi färger, typsnitt, layout och anpassar utseendet för olika skärmstorlekar?
Svaret är CSS (Cascading Style Sheets). CSS är språket vi använder för att beskriva presentationen och stilen för HTML-dokument. Om HTML är skelettet, är CSS kläderna, sminket och frisyren – det som ger sidan dess visuella identitet.
Motivation:
CSS gör det möjligt att separera struktur och utseende, vilket leder till renare kod, bättre underhåll och mer flexibla webbplatser. Med CSS kan du skapa allt från enkla färgändringar till avancerade, responsiva layouter.
Varför är CSS viktigt?
- Separation of Concerns: CSS låter oss separera innehållets struktur (HTML) från dess presentation (CSS). Detta gör koden renare, lättare att underhålla och mer flexibel.
- Visuell design: CSS ger oss kontroll över allt från färger och typsnitt till komplexa layouter och animationer.
- Responsivitet: Med CSS kan vi skapa webbplatser som anpassar sig och ser bra ut på alla typer av enheter – från stora datorskärmar till mobiler och surfplattor.
- Effektivitet: Genom att definiera stilar på ett ställe och återanvända dem kan vi enkelt uppdatera utseendet på hela webbplatser med små ändringar.
Vad kommer du att lära dig i detta kapitel?
- Introduktion till CSS: Vad CSS är och hur du kopplar det till HTML.
- Selektorer, färger och typografi: Hur du väljer ut HTML-element (selektorer) och applicerar grundläggande stilar som färger och typsnitt.
- Box model och layout: Hur varje HTML-element kan ses som en låda (box model) och hur du kan kontrollera dess dimensioner, marginaler (margin) och utfyllnad (padding) för att skapa layouter.
- Responsiv design: Grunderna i hur du använder Media Queries för att anpassa stilar baserat på skärmstorlek.
- Mobile-first design: Principen att designa för mobilen först och sedan skala upp för större skärmar.
- Praktiska övningar: Du får applicera CSS-regler för att styla den "Om Mig"-sida du skapade i förra kapitlet.
Låt oss börja ge våra HTML-sidor lite färg och form!
Introduktion till CSS: Syntax och inkludering
CSS står för Cascading Style Sheets. Det är språket som webbläsare använder för att bestämma hur HTML-element ska visas visuellt. Utan CSS skulle webben vara en ganska monoton plats med bara svart text på vit bakgrund.
Motivation:
CSS gör det möjligt att separera innehåll (HTML) från utseende (design). Det gör webbsidor snyggare, mer lättlästa och enklare att underhålla.
CSS-syntax: Regler som styr utseendet
En CSS-regel består huvudsakligen av två delar:
- Selektor (selector): Anger vilket eller vilka HTML-element som regeln ska appliceras på.
- Deklarationsblock (declaration block): Innehåller en eller flera deklarationer, separerade med semikolon (
;
). Blocket omges av måsvingar ({ }
).- Deklaration (declaration): Består av en egenskap (property) och ett värde (value), separerade med kolon (
:
). Egenskapen är det du vill ändra (t.ex.color
,font-size
,background-color
), och värdet anger hur du vill ändra det (t.ex.blue
,16px
,#FFFFFF
).
- Deklaration (declaration): Består av en egenskap (property) och ett värde (value), separerade med kolon (
/* Detta är en CSS-kommentar */
selector {
property: value;
another-property: another-value;
}
Exempel:
Låt oss säga att vi vill att alla paragrafer (<p>
) ska ha blå text och en textstorlek på 16 pixlar.
p {
color: blue; /* Gör texten blå */
font-size: 16px; /* Sätter textstorleken till 16 pixlar */
}
- Selektor:
p
(väljer alla<p>
-element) - Deklarationsblock:
{ color: blue; font-size: 16px; }
- Deklarationer:
color: blue;
ochfont-size: 16px;
- Egenskaper:
color
ochfont-size
- Värden:
blue
och16px
Hur kopplar man CSS till HTML?
Det finns tre huvudsakliga sätt att applicera CSS-regler på ett HTML-dokument:
1. Extern CSS-fil (external stylesheet) – Rekommenderat!
Du skriver dina CSS-regler i en separat fil (t.ex. style.css
) och länkar in den i HTML-filen:
<link rel="stylesheet" href="style.css">
Exempel på innehåll i style.css
:
body {
font-family: sans-serif;
}
p {
color: #333;
}
h1 {
color: darkcyan;
}
Exempel på HTML:
<!DOCTYPE html>
<html lang="sv">
<head>
<meta charset="UTF-8">
<title>Min Sida</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Välkommen</h1>
<p>Detta är min webbsida.</p>
</body>
</html>
Fördelar:
- Separation: Håller struktur (HTML) och stil (CSS) helt åtskilda.
- Underhåll: Lätt att ändra utseendet på hela webbplatsen genom att redigera en enda fil.
- Prestanda: Webbläsaren kan cacha (spara en lokal kopia av)
.css
-filen, vilket snabbar upp laddningen av efterföljande sidor.
Nackdelar: Kräver en extra fil och en extra HTTP-förfrågan (request) för att ladda CSS-filen (men detta vägs oftast upp av cachning).
2. Intern CSS (internal stylesheet)
Du skriver CSS-regler i en <style>
-tagg i <head>
på HTML-sidan:
<!DOCTYPE html>
<html lang="sv">
<head>
<meta charset="UTF-8">
<title>Min Sida</title>
<style>
body {
font-family: sans-serif;
}
p {
color: #333;
}
h1 {
color: darkcyan;
}
</style>
</head>
<body>
<h1>Välkommen</h1>
<p>Detta är min webbsida.</p>
</body>
</html>
Fördelar:
Allt finns i en enda fil, kan vara användbart för små, enstaka sidor eller för att snabbt testa något.
Nackdelar:
Blandar struktur och stil, svårare att underhålla på större webbplatser, ingen cachning av stilarna mellan olika sidor.
3. Inline CSS (inline styles)
Du skriver CSS direkt i HTML-elementets style
-attribut:
<p style="color: #333; font-size: 14px;">Detta är min webbsida med inline-stil.</p>
Fördelar:
Kan snabbt applicera en unik stil på ett enda element.
Nackdelar:
- Används sällan och bör undvikas!
- Blandar struktur och stil maximalt.
- Mycket svårt att underhålla och återanvända stilar.
- Gör HTML-koden rörig.
- Har högst specificitet (se nedan), vilket kan göra det svårt att skriva över stilar i externa filer.
- Säkerhetsaspekt: Inline-stilar kan ibland utnyttjas för XSS-attacker (cross-site scripting) om användarinnehåll får skrivas direkt till
style
-attributet.
Cascading och specificitet (kort introduktion)
Namnet "Cascading" Style Sheets antyder en viktig mekanism: kaskaden. Om flera CSS-regler matchar samma element och försöker sätta samma egenskap, hur bestämmer webbläsaren vilken regel som vinner?
Detta avgörs av en kombination av:
- Ursprung (origin): Stilar från webbplatsens utvecklare (author stylesheets) väger tyngre än webbläsarens standardstilar (user-agent stylesheets).
- Specificitet (specificity): Mer specifika selektorer väger tyngre än mindre specifika. T.ex. en regel för ett element med ett visst ID (
#myId
) är mer specifik än en regel för ett element med en viss klass (.myClass
), som i sin tur är mer specifik än en regel för en elementtyp (p
). Inline-stilar har högst specificitet. - Ordning (order): Om specificiteten är densamma, vinner den regel som definieras senast i koden eller i den senast inlänkade CSS-filen.
Exempel: Hur cascading fungerar i praktiken
<p id="important" class="highlight">Detta är en viktig paragraf.</p>
/* Regel 1: Elementtyp-selektor (lägst specificitet) */
p {
color: black;
font-size: 14px;
}
/* Regel 2: Klass-selektor (högre specificitet) */
.highlight {
color: blue;
font-weight: bold;
}
/* Regel 3: ID-selektor (ännu högre specificitet) */
#important {
color: red;
}
/* Regel 4: Samma ID-selektor, men definierad senare */
#important {
color: green;
text-decoration: underline;
}
Resultat:
- Färg: grön (från regel 4 - samma specificitet som regel 3, men definierad senare)
- Font-storlek: 14px (från regel 1 - ingen annan regel definierar detta)
- Font-vikt: bold (från regel 2 - ingen regel med högre specificitet överskrider detta)
- Text-decoration: underline (från regel 4)
Förklaring:
- Regel 1 (
p
) har lägst specificitet men bidrar medfont-size
eftersom ingen annan regel definierar detta. - Regel 2 (
.highlight
) har högre specificitet än regel 1, såcolor: blue
skulle vinna övercolor: black
, men... - Regel 3 och 4 (
#important
) har högst specificitet och överskrider både regel 1 och 2 för färg. - Mellan regel 3 och 4 vinner regel 4 eftersom den definieras senare (samma specificitet).
Inline-stil skulle vinna över allt:
Om vi hade <p id="important" class="highlight" style="color: purple;">
, skulle texten bli lila eftersom inline-stilar har högst specificitet.
Nyckelordet !important
I CSS kan du använda nyckelordet !important
för att tvinga en deklaration att vinna över andra, oavsett specificitet och ordning (med undantag för inline-stilar som också använder !important
). Det bör användas sparsamt, eftersom det kan göra koden svår att underhålla.
Syntax:
property: value !important;
Exempel 1: Överskrid en annan regel
p {
color: blue !important;
}
p {
color: red;
}
Alla <p>
-element får blå text, även om den röda regeln står sist, eftersom !important
vinner.
Exempel 2: Kombinera med inline-stil
<p style="color: green;">Denna text är grön.</p>
p {
color: orange !important;
}
Texten blir orange, eftersom CSS-regeln med !important
överskrider inline-stilen.
Obs:
Överanvänd inte !important
. Det är bättre att strukturera CSS:en så att specificitet och ordning räcker för att styra vilka regler som gäller.
Sammanfattning
- CSS används för att definiera utseendet på HTML-element.
- Grundsyntaxen är
selektor { egenskap: värde; }
. - Det bästa och mest rekommenderade sättet att inkludera CSS är via en extern CSS-fil länkad med
<link>
i HTML:s<head>
. - Intern CSS (
<style>
) och inline CSS (style
-attribut) finns men bör användas sparsamt eller undvikas. - Kaskaden och specificitetsregler avgör vilken stil som appliceras om flera regler matchar samma element.
I nästa avsnitt dyker vi djupare ner i olika typer av selektorer.
Selektorer, färger och typografi i CSS
Att kunna välja ut och styla rätt HTML-element är grunden i CSS. I detta kapitel lär du dig använda selektorer (selectors), ange färger på olika sätt och styra textens utseende (typografi).
Motivation:
Med selektorer kan du styra exakt vilka delar av din webbsida som ska få särskild stil. Färg och typografi gör sidan mer läsbar och tilltalande.
Språkpolicy:
Svenska används i förklaringar, men engelska tekniska termer anges i parentes första gången de nämns. Variabel- och klassnamn skrivs på engelska.
CSS-selektorer: Att välja rätt element
Selektorer är mönster som matchar HTML-element. Här är de vanligaste:
1. Elementselektor (type selector)
- Syntax: Elementets namn, t.ex.
p
,h1
,div
. - Vad den gör: Väljer alla element av den typen.
p {
line-height: 1.6;
}
h2 {
color: navy;
}
2. Klassselektor (class selector)
- Syntax: Punkt (
.
) följt av klassnamnet, t.ex..important
. - Vad den gör: Väljer alla element med angiven klass. Ett element kan ha flera klasser.
<p class="important">Detta är viktigt.</p>
<span class="important highlight">Markerad text.</span>
<button class="button">Klicka här</button>
.important {
font-weight: bold;
}
.highlight {
background-color: yellow;
}
.button {
padding: 10px 15px;
border: 1px solid gray;
}
3. ID-selektor (ID selector)
- Syntax: Hash (
#
) följt av ID-namnet, t.ex.#header
. - Vad den gör: Väljer det enda element med angivet ID.
- Viktigt: Ett ID ska vara unikt på sidan.
<header id="header">...</header>
#header {
background-color: #f0f0f0;
padding: 20px;
}
4. Attributselektor (attribute selector)
- Syntax: Elementnamn följt av hakparenteser, t.ex.
a[target="_blank"]
. - Vad den gör: Väljer element baserat på attribut och/eller värde.
a[target="_blank"] {
color: green;
}
input[type="text"] {
border: 1px solid #ccc;
}
5. Kombinatorer (combinators)
Kombinera selektorer för mer specifika regler:
- Gruppering (grouping): Komma (
,
) för att ge flera selektorer samma stil.h1, h2, h3 { font-family: Georgia, serif; }
- Selektor för ättling (descendant combinator): Mellanslag (
nav ul { list-style: none; }
- Barnselektor (child combinator): Större än-tecken (
>
) för att välja direkta barn.ul > li { margin-bottom: 5px; }
Specificitet:
ID-selektorer (#id
) är starkare än klass- och attributselektorer (.class
,[attr]
), som är starkare än elementselektorer (p
). När du kombinerar selektorer ökar specificiteten.
Färger i CSS
Du kan ange färger på flera sätt:
- Namngivna färger (named colors): T.ex.
red
,blue
,lightgray
.body { background-color: lightgray; }
- Hexadecimala koder (HEX): T.ex.
#FF0000
(röd),#333
(mörkgrå).h1 { color: #3A8DDE; }
- RGB/RGBA: T.ex.
rgb(51, 51, 51)
,rgba(0, 0, 0, 0.5)
.p { color: rgb(51, 51, 51); } .overlay { background-color: rgba(0, 0, 0, 0.5); }
- HSL/HSLA: T.ex.
hsl(210, 100%, 50%)
.button { background-color: hsl(210, 100%, 50%); }
Grundläggande typografi
Typografi handlar om textens utseende. Några viktiga egenskaper:
font-family
: Typsnitt. Ange gärna flera alternativ.body { font-family: "Helvetica Neue", Arial, sans-serif; } code { font-family: Consolas, Monaco, monospace; }
font-size
: Textstorlek, t.ex.16px
,2rem
.p { font-size: 16px; } h1 { font-size: 2.5rem; }
font-weight
: Tjocklek, t.ex.normal
,bold
,300
.strong { font-weight: bold; } .subtle { font-weight: 300; }
font-style
: Stil, t.ex.normal
,italic
.em { font-style: italic; }
color
: Textfärg.a { color: #007bff; }
text-align
: Justering, t.ex.left
,center
.h1 { text-align: center; }
line-height
: Radavstånd, t.ex.1.6
.p { line-height: 1.6; }
text-decoration
: Dekoration, t.ex. understrykning.a { text-decoration: none; } a:hover { text-decoration: underline; }
Praktiska exempel
Exempel: Kombinera selektorer och färg
<ul>
<li class="important">Viktigt</li>
<li>Vanligt</li>
<li class="highlight">Markerat</li>
</ul>
li {
color: #333;
}
.important {
color: red;
font-weight: bold;
}
.highlight {
background: yellow;
}
Exempel: Typografi och färg
<p class="info">Informationstext</p>
.info {
font-family: Arial, sans-serif;
font-size: 1.2rem;
color: hsl(210, 100%, 40%);
line-height: 1.5;
}
Sammanfattning
- CSS-selektorer (element, klass, ID, attribut, kombinatorer) låter dig välja vilka HTML-element du vill styla.
- Färger kan anges med namn, HEX, RGB(A) eller HSL(A).
- Typografiska egenskaper styr textens utseende och läsbarhet.
- Att behärska selektorer och grundläggande styling är fundamentalt för att skapa visuellt tilltalande webbsidor.
I nästa avsnitt tittar vi på box model, som beskriver hur utrymme hanteras runt HTML-element.
Box model och grundläggande layout i CSS
Att förstå CSS box model är avgörande för att kunna skapa tydliga och flexibla layouter på webbsidor. Box model beskriver hur varje HTML-element ritas upp som en rektangulär låda och hur dess storlek och avstånd till andra element beräknas.
Motivation:
Med kunskap om box model kan du styra exakt hur mycket plats ett element tar, hur det placeras och hur det samspelar med andra delar av sidan. Det är grunden för all layout i CSS.
Box model: delar
Varje låda i CSS består av fyra lager, utifrån och in:
-
Margin (marginal):
Yttersta lagret. Skapar ett genomskinligt utrymme utanför elementets kantlinje (border). Används för att skapa avstånd mellan olika element. -
Border (kantlinje):
Ramen runt elementet. Har tjocklek, stil (t.ex. solid, dashed) och färg. -
Padding (utfyllnad):
Genomskinligt utrymme innanför kantlinjen, men utanför innehållet. Skapar luft mellan kanten och innehållet. -
Content (innehåll):
Själva innehållet i elementet (t.ex. text eller bild). Dimensionerna styrs avwidth
ochheight
(eller av innehållet om inget anges).
Visualisering
graph TD Margin -.-> Border -.-> Padding -.-> Content style Margin fill:#fff,stroke:#aaa,stroke-width:2px,stroke-dasharray: 5 5 style Border fill:#fdd,stroke:#f00,stroke-width:2px style Padding fill:#dfd,stroke:#0a0,stroke-width:2px style Content fill:#ddf,stroke:#00f,stroke-width:2px
Diagram: Visualisering av CSS box model.
CSS-egenskaper för box model
width
,height
: Anger bredd och höjd för content-området.padding
: Luft innanför kantlinjen.border
: Själva ramen runt elementet.margin
: Luft utanför kantlinjen.
Exempel 1: Enkel box
.box {
width: 200px;
padding: 16px;
border: 2px solid #333;
margin: 24px;
background-color: #eef;
}
<div class="box">
Detta är en enkel box med padding, border och margin.
</div>
Exempel 2: Jämförelse mellan content-box och border-box
.box-content {
width: 200px;
padding: 20px;
border: 4px solid #0077cc;
margin: 16px;
box-sizing: content-box;
background: #d0f0ff;
}
.box-border {
width: 200px;
padding: 20px;
border: 4px solid #cc7700;
margin: 16px;
box-sizing: border-box;
background: #fff0d0;
}
<div class="box-content">
box-sizing: content-box (standard)<br>
Total bredd: 200px + 2×20px (padding) + 2×4px (border) = 248px
</div>
<div class="box-border">
box-sizing: border-box<br>
Total bredd: 200px (inklusive padding och border)
</div>
Exempel 3: Flera boxar med olika margin och padding
.box-small {
width: 120px;
padding: 8px;
border: 2px dashed #888;
margin: 8px;
background: #f9f9f9;
}
.box-large {
width: 240px;
padding: 32px;
border: 4px solid #444;
margin: 32px;
background: #e0e0e0;
}
<div class="box-small">
Liten box med liten padding och margin.
</div>
<div class="box-large">
Stor box med stor padding och margin.
</div>
Exempel 4: Ärftlighet och box model
Vissa egenskaper, som color
och font-family
, ärvs av barn-element. Andra, som margin
och padding
, ärvs inte.
.parent-box {
color: darkblue;
font-family: Verdana, sans-serif;
border: 2px solid #222;
padding: 12px;
margin: 20px;
}
.child-box {
border: 1px dotted #555;
padding: 8px;
margin: 10px;
}
<div class="parent-box">
Förälder (parent-box)
<div class="child-box">
Barn (child-box) ärver färg och font, men har egna margin och padding.
</div>
</div>
Analogier
Tänk dig box model som en flyttkartong:
- Content: Själva sakerna i kartongen.
- Padding: Bubbelplast runt sakerna.
- Border: Själva kartongen.
- Margin: Luft mellan kartongen och andra kartonger.
Sammanfattning
- Box model styr hur HTML-element tar plats och placeras på sidan.
- Den består av margin, border, padding och content.
- Med rätt användning av box model kan du skapa flexibla och snygga layouter.
- Använd gärna
box-sizing: border-box;
för enklare storleksberäkningar.
Blockelement och inline-element i CSS
Vad är blockelement?
Blockelement (block elements) är HTML-element som automatiskt tar upp hela bredden av sin förälder och börjar på en ny rad. Exempel på blockelement är <div>
, <p>
, <h1>
, <ul>
, och <li>
. De används för att bygga sidans struktur.
Egenskaper för blockelement:
- Börjar alltid på en ny rad.
- Tar upp hela tillgängliga bredden.
- Det går att ange
width
,height
,margin
ochpadding
.
Exempel:
<div style="background: #e0e0ff; margin-bottom: 8px;">Detta är ett blockelement</div>
<p style="background: #ffe0e0;">Detta är också ett blockelement</p>
Vad är inline-element?
Inline-element (inline elements) är HTML-element som bara tar upp så mycket plats som behövs för sitt innehåll och ligger kvar på samma rad som andra element. Exempel på inline-element är <span>
, <a>
, <strong>
, och <em>
.
Egenskaper för inline-element:
- Börjar inte på en ny rad.
- Tar bara upp så mycket plats som behövs.
width
ochheight
har ingen effekt.- Endast horisontell
padding
ochmargin
fungerar som väntat.
Exempel:
<p>
Detta är <span style="background: #e0ffe0;">ett inline-element</span> mitt i en text.
</p>
Jämförelse: block vs inline
Egenskap | Blockelement | Inline-element |
---|---|---|
Ny rad | Ja | Nej |
Bredd | Hela förälderns | Innehållets bredd |
width /height | Ja | Nej |
margin /padding | Ja | Endast horisontellt |
Positionering av element med CSS
CSS erbjuder flera sätt att positionera element på en webbsida. Här är de vanligaste positioneringsmetoderna:
1. Static (standard)
Alla element har position: static;
som standard. De placeras i sidans normala flöde.
.static-box {
position: static;
}
2. Relative
Med position: relative;
kan du flytta ett element i förhållande till dess ursprungliga plats med hjälp av top
, right
, bottom
och left
.
.relative-box {
position: relative;
left: 30px;
top: 10px;
background: #d0ffd0;
}
3. Absolute
position: absolute;
placerar elementet i förhållande till närmaste förfader med position: relative;
(eller till sidans kant om ingen sådan finns). Elementet tas bort från det normala flödet.
.parent {
position: relative;
width: 300px;
height: 200px;
background: #f0f0f0;
}
.child {
position: absolute;
top: 20px;
left: 40px;
background: #ffd0d0;
padding: 8px;
}
<div class="parent">
<div class="child">Absolut positionerad</div>
</div>
4. Fixed
Med position: fixed;
placeras elementet i förhållande till webbläsarfönstret och stannar kvar även när du scrollar.
.fixed-box {
position: fixed;
bottom: 10px;
right: 10px;
background: #d0e0ff;
padding: 10px;
}
5. Sticky
position: sticky;
gör att elementet beter sig som relative
tills du scrollar till en viss punkt, då blir det fixed
.
.sticky-header {
position: sticky;
top: 0;
background: #fffbe0;
padding: 10px;
}
Illustration: CSS-positionering
flowchart TD A[Normalt sidflöde] B[Blockelement<br>(position: static)] C[Relativt flyttat<br>(position: relative;<br>top/left)] D[Absolut placerat<br>(position: absolute;<br>top/left)] E[Fast placerat<br>(position: fixed;<br>bottom/right)] F[Klistrigt<br>(position: sticky;<br>top: 0)] A --> B B -. Flyttas från ursprunglig plats .-> C A -. Tas ur sidflödet .-> D A -. Följer fönstret .-> E A -. Bete sig som static tills scroll .-> F
Sammanfattning
- Blockelement bygger sidans grundstruktur och tar upp hela bredden.
- Inline-element ligger kvar på samma rad och tar bara upp så mycket plats som behövs.
- Med position kan du styra exakt var element hamnar på sidan.
- Kombinera block/inline och positionering för att skapa flexibla och responsiva layouter.
Responsiv design med Media Queries
Idag surfar människor på webben från en mängd olika enheter: stora datorskärmar, bärbara datorer, surfplattor och mobiler av alla storlekar. En modern webbplats måste kunna anpassa sin layout och sitt innehåll för att se bra ut och vara användbar på alla dessa skärmar. Detta kallas responsiv web design (responsive web design, RWD).
Motivation:
Responsiv design gör att din webbplats fungerar och ser bra ut på alla enheter, vilket förbättrar användarupplevelsen och gör sidan mer tillgänglig.
Varför responsiv design?
- Användarupplevelse: En sida som är designad för en stor skärm blir ofta svår att använda på en mobil (pyttesmå texter, knappar som är svåra att träffa, behov av att zooma och panorera). Responsiv design ger en optimerad upplevelse oavsett enhet.
- SEO: Google prioriterar mobilvänliga webbplatser i sina sökresultat.
- Underhåll: Istället för att bygga och underhålla separata webbplatser för dator och mobil (vilket var vanligt förr), har man en enda kodbas som anpassar sig.
Nyckelkomponenter i responsiv design
- Flexibel grid-baserad layout: Använd relativa enheter (som procent
%
,fr
i CSS Grid eller Flexbox) istället för fasta pixelvärden för att skapa kolumner och layoutelement som kan ändra storlek. - Flexibla bilder och media: Se till att bilder och andra mediafiler skalar ner proportionerligt för att passa mindre skärmar, t.ex. med
max-width: 100%;
. - Media Queries: Använd CSS Media Queries för att applicera olika stilregler vid olika skärmstorlekar eller andra enhetsegenskaper.
Viewport meta-taggen
Det allra första steget för att göra en sida responsiv är att inkludera viewport meta-taggen i HTML-dokumentets <head>
:
<meta name="viewport" content="width=device-width, initial-scale=1.0">
- Vad den gör: Talar om för webbläsaren (särskilt på mobila enheter) hur den ska hantera sidans dimensioner och skalning.
width=device-width
: Sätter sidans bredd till att vara densamma som enhetens skärmbredd.initial-scale=1.0
: Sätter den initiala zoomnivån till 100% (ingen zoom från start).
- Utan denna tagg kommer mobila webbläsare ofta att försöka visa sidan som om den vore på en datorskärm och sedan förminska den, vilket leder till oläslig text och dålig användarupplevelse.
Media Queries: Anpassa stilar för olika skärmar
Media Queries är kärnan i hur vi applicerar olika CSS-regler baserat på enhetens egenskaper. Den vanligaste egenskapen att titta på är skärmens bredd.
- Syntax: En Media Query skrivs med
@media
följt av en eller flera villkor inom parentes. CSS-reglerna som ska gälla endast när villkoret är uppfyllt placeras inom måsvingar ({ }
).
/* Standardstilar (gäller alltid, ofta för mindre skärmar först – mobile-first design) */
body {
font-size: 16px;
}
.container {
width: 95%; /* Smalare på små skärmar */
margin: 0 auto;
}
/* Stilar som appliceras ENDAST när skärmbredden är 768px ELLER MER */
@media (min-width: 768px) {
body {
font-size: 18px; /* Lite större text på större skärmar */
}
.container {
width: 80%; /* Bredare container på större skärmar */
max-width: 960px; /* Men inte bredare än 960px */
}
/* Fler regler för layout, t.ex. visa element sida vid sida */
}
/* Stilar som appliceras ENDAST när skärmbredden är 1200px ELLER MER */
@media (min-width: 1200px) {
body {
background-color: lightblue;
}
/* Ännu fler anpassningar för riktigt stora skärmar */
}
-
Vanliga villkor:
min-width
: Applicerar reglerna när skärmbredden är minst det angivna värdet.max-width
: Applicerar reglerna när skärmbredden är högst det angivna värdet.- Man kan kombinera villkor med
and
,or
,not
. - Man kan också testa andra egenskaper som
orientation
(portrait/landscape),resolution
, etc.
-
Breakpoints (brytpunkter): De specifika skärmbredder där layouten eller stilarna ändras (t.ex.
768px
,1200px
i exemplet ovan) kallas breakpoints. Valet av breakpoints beror på designen och innehållet, inte nödvändigtvis på specifika enheters exakta dimensioner. Vanliga breakpoints kan vara runt t.ex. 600px, 768px, 992px, 1200px.
Relativa enheter
För att skapa flexibla och responsiva designer är det ofta bättre att använda relativa CSS-enheter istället för fasta (px
):
%
(procent): Relativt till förälderelementets motsvarande egenskap (t.ex.width: 50%;
tar upp halva förälderns bredd).em
: Relativt till förälderelementetsfont-size
.rem
(root em): Relativt till rot-elementets (<html>
)font-size
. Rekommenderas ofta för textstorlekar och ibland för padding/margin, eftersom det gör det enkelt att skala hela gränssnittet genom att bara ändra rot-fontstorleken (vilket också respekterar användarens webbläsarinställningar).vw
(viewport width): 1vw är 1% av webbläsarfönstrets bredd.vh
(viewport height): 1vh är 1% av webbläsarfönstrets höjd.
Genom att använda dessa enheter kan element och text anpassa sin storlek mer flytande när fönsterstorleken ändras.
Illustration: Media Queries och breakpoints
flowchart TD A[Bas-CSS för mobil] B{Breakpoint: min-width 768px} C{Breakpoint: min-width 1200px} D[Stilar för surfplatta/laptop] E[Stilar för desktop] A --> B B --> D D --> C C --> E
Sammanfattning
- Responsiv design handlar om att skapa webbplatser som fungerar bra på alla enheter.
- Detta uppnås genom flexibla layouter, flexibla bilder och framför allt Media Queries.
- Glöm inte viewport meta-taggen i HTML:ens
<head>
. - Media Queries (
@media
) låter oss applicera olika CSS-regler vid specifika breakpoints (oftast baserat påmin-width
ellermax-width
). - Att använda relativa enheter som
rem
och%
bidrar också till flexibiliteten.
I nästa avsnitt tittar vi specifikt på mobile-first design
Mobile-first design: Bygg för litet, skala uppåt
När vi utvecklar responsiva webbplatser finns det två huvudsakliga strategier för hur vi strukturerar vår CSS och våra Media Queries (mediefrågor):
- Desktop-first design: Man skriver först alla stilar för stora datorskärmar och använder sedan
max-width
i Media Queries för att modifiera eller ta bort stilar för mindre skärmar (surfplattor, mobiler). - Mobile-first design: Man skriver först de grundläggande stilarna för de minsta skärmarna (mobiler) och använder sedan
min-width
i Media Queries för att lägga till eller modifiera stilar för successivt större skärmar.
Mobile-first design rekommenderas starkt och anses idag vara bästa praxis.
Mål: Förstå vad mobile-first design innebär, varför den är fördelaktig, och hur den implementeras med CSS och Media Queries.
Varför mobile-first design?
- Fokus på kärninnehåll: Mobila skärmar har begränsat utrymme. Mobile-first design tvingar oss att prioritera det absolut viktigaste innehållet och funktionerna från början. Detta leder ofta till en renare och mer fokuserad användarupplevelse på alla enheter.
- Prestanda: Mobila enheter har ofta långsammare processorer och sämre nätverksuppkoppling än datorer. Genom att ladda de enklaste stilarna först och bara lägga till mer komplexa layouter och funktioner för större skärmar (som har mer kraft), kan vi förbättra prestandan på mobilen.
- Enklare CSS: Det är ofta lättare att lägga till stilar för större skärmar (t.ex. gå från en enkel kolumn till flera) än att skriva över eller nollställa komplexa desktop-stilar för mindre skärmar. Detta kan leda till mindre och mer lätthanterlig CSS.
- Progressive enhancement: Mobile-first design går hand i hand med principen om progressiv förbättring (progressive enhancement). Man börjar med en grundläggande, fungerande upplevelse för alla (även äldre webbläsare eller enheter med begränsad funktionalitet) och lägger sedan på förbättringar (mer avancerad layout, JavaScript-effekter) för de webbläsare och enheter som stödjer dem.
Hur implementeras mobile-first design?
-
Skriv bas-CSS för mobil: Skriv dina CSS-regler utanför några Media Queries. Dessa regler ska definiera grundutseendet och layouten för den minsta skärmen du siktar på (oftast en enkel, enkolumns-layout).
-
Använd
min-width
Media Queries: Identifiera dina breakpoints (brytpunkter) där designen behöver anpassas för större skärmar. -
Lägg till stilar för större skärmar: Inuti
@media (min-width: ...)
-blocken, lägg till de CSS-regler som behövs för att anpassa layouten och utseendet för den större skärmstorleken. Du behöver bara skriva de regler som ändras eller läggs till – de grundläggande mobil-stilarna ärvs automatiskt.
Exempel (konceptuellt):
/* =================================== */
/* Bas-stilar (mobile-first design - gäller ALLTID) */
/* =================================== */
body {
font-family: sans-serif;
line-height: 1.5;
padding: 10px;
}
.container {
width: 100%; /* Tar upp hela bredden på små skärmar */
}
nav ul {
padding: 0;
list-style: none;
}
nav li {
margin-bottom: 10px; /* Länkar under varandra */
}
.card {
border: 1px solid #ccc;
padding: 15px;
margin-bottom: 15px;
}
/* Dölj sidopanelen på små skärmar */
.sidebar {
display: none;
}
/* =================================== */
/* Mellanstora skärmar (t.ex. surfplattor och uppåt) */
/* =================================== */
@media (min-width: 768px) {
body {
padding: 20px;
}
.container {
max-width: 960px; /* Begränsa bredden */
margin: 0 auto; /* Centrera */
display: flex; /* Använd Flexbox för layout */
gap: 20px; /* Avstånd mellan flex-items */
}
main { /* Huvudinnehållet tar upp mer plats */
flex: 3; /* Tar 3 delar av tillgängligt utrymme */
}
.sidebar { /* Visa sidopanelen */
display: block;
flex: 1; /* Tar 1 del av tillgängligt utrymme */
}
nav li {
display: inline-block; /* Länkar bredvid varandra */
margin-bottom: 0;
margin-right: 15px;
}
}
/* =================================== */
/* Stora skärmar (t.ex. desktop och uppåt) */
/* =================================== */
@media (min-width: 1200px) {
body {
font-size: 110%; /* Lite större text */
}
/* Eventuella ytterligare anpassningar för stora skärmar */
}
I det här exemplet:
- Definieras grundläggande stilar för typsnitt, padding och en enkel layout (allt i en kolumn, ingen sidebar) först.
- Vid
768px
(min-width
) introduceras Flexbox för att skapa en tvåkolumnslayout (main
ochsidebar
), navigeringslänkarna placeras bredvid varandra, och containern centreras. - Vid
1200px
(min-width
) justeras textstorleken ytterligare.
Sammanfattning
Mobile-first design är en strategi där man designar och kodar för den minsta skärmen först och sedan använder min-width
i Media Queries för att progressivt förbättra och anpassa layouten för större skärmar. Detta leder ofta till bättre prestanda på mobilen, renare kod och ett större fokus på kärninnehållet. Det är den rekommenderade metoden för att bygga responsiva webbplatser idag.
Nu är det dags att praktisera dessa CSS-koncept!
Praktiska övningar och projekt
Här får du hands-on-övningar för att träna på CSS:
- Styling av formulär
- Responsiva layouter
- Navigation och menyer
- Praktiska designutmaningar
Praktiska övningar: Grundläggande CSS
Nu är det dags att applicera det vi lärt oss om CSS! Vi fortsätter med "Om Mig"-sidan från kapitel 2 och ger den stil med CSS.
Mål:
Använda CSS för att:
- Koppla en extern CSS-fil.
- Använda olika selektorer (element, klass, ID).
- Ändra färger och typografi.
- Arbeta med box model (padding, margin, border).
- Implementera grundläggande responsivitet med Media Queries (mobile-first design).
Förutsättningar:
Du har projektet om-mig-sida
från kapitel 2, med index.html
och Git initialiserat (och helst kopplat till GitHub).
Övning 1: Koppla CSS och grundläggande styling
-
Skapa CSS-fil:
I dinom-mig-sida
-mapp, skapa en ny fil som heterstyle.css
. -
Länka från HTML:
Öppnaindex.html
. Inuti<head>
, lägg till en<link>
-tagg för att länka till din CSS-fil:<link rel="stylesheet" href="style.css">
-
Grundläggande body-stilar (i
style.css
):- Sätt ett grundtypsnitt för hela sidan med
font-family
(t.ex.sans-serif
). - Lägg till
line-height
för bättre läsbarhet (t.ex.1.6
). - Sätt en grundläggande textfärg (
color
) förbody
(t.ex.#333
). - Lägg till
padding
påbody
(t.ex.20px
) så att innehållet inte ligger klistrat mot kanterna på små skärmar.
body { font-family: sans-serif; line-height: 1.6; color: #333; padding: 20px; }
- Sätt ett grundtypsnitt för hela sidan med
-
Styla rubriker:
Geh1
ochh2
en annan färg.h1, h2 { color: darkcyan; }
-
Commit:
git status
(Du bör sestyle.css
som ny ochindex.html
som ändrad).git add .
git commit -m "Lägg till CSS-fil och grundläggande body/h-styling"
-
Visa:
Öppnaindex.html
i webbläsaren. Du bör nu se att typsnitt, radavstånd, färger och padding har ändrats!
Övning 2: Box model och selektorer
-
Centrera innehåll (på större skärmar):
Vi vill att innehållet ska vara centrerat och inte bli för brett på stora skärmar. Använd en container.- HTML: Omslut allt innehåll inuti
<body>
(utom eventuella<script>
-taggar) med en<div>
med klassencontainer
. Om du använder<header>
,<main>
,<footer>
, kan du omsluta dessa med containern eller lägga containern inuti<main>
. - CSS: Lägg till regler för
.container
:
Tips:.container { width: 100%; /* Mobile-first: full bredd */ max-width: 800px; /* Maximal bredd på stora skärmar */ margin-left: auto; margin-right: auto; }
margin: 0 auto;
är ett kortkommando för att centrera blockelement.
- HTML: Omslut allt innehåll inuti
-
Styla länkar:
Ta bort understrykningen från länkar som standard och ändra färgen.a { color: dodgerblue; text-decoration: none; } a:hover { text-decoration: underline; }
-
Styla bild (om du har en):
Ge bilden en maximal bredd så att den inte blir större än sin container och lägg till lite marginal.img { max-width: 100%; height: auto; display: block; margin: 20px auto; }
-
Commit:
git add .
git commit -m "Centrera innehåll, styla länkar och bild"
-
Visa:
Uppdatera webbläsaren. Se hur innehållet beter sig när du ändrar fönsterstorleken.
Övning 3: Responsivitet med Media Query
Låt oss göra så att bakgrundsfärgen på body
ändras på lite större skärmar.
-
Lägg till Media Query:
Lägg till följande i slutet avstyle.css
:@media (min-width: 768px) { body { background-color: #f0f8ff; /* AliceBlue */ font-size: 18px; } /* Lägg till fler regler här som bara ska gälla på större skärmar */ }
-
Commit:
git add .
git commit -m "Gör sidan responsiv: ändra bakgrund och font på bredare skärm"
-
Visa och testa:
Öppnaindex.html
. Ändra bredden på webbläsarfönstret. Ser du hur bakgrundsfärgen och textstorleken ändras när fönstret passerar 768 pixlars bredd?
Övning 4: Pusha till GitHub
Om du kopplade ditt repo till GitHub i kapitel 2, pusha dina ändringar:
- Kör
git status
för att se om du har några lokala commits som inte är pushade. - Kör
git pull
(bra vana att göra innan push). - Kör
git push
. - Verifiera att dina ändringar (inklusive
style.css
) nu finns på GitHub.
Sammanfattning och nästa steg
Du har nu tagit dina första steg med CSS! Du har länkat en extern stilmall, använt olika selektorer, lagt till färger och typografi, arbetat med box model och introducerat responsivitet med en Media Query enligt mobile-first design. Du har också fortsatt att använda Git för att spara dina framsteg.
I nästa kapitel dyker vi ner i JavaScript för att lägga till interaktivitet på våra webbsidor.
Tekniska intervjufrågor: CSS och responsiv design
Detta avsnitt innehåller exempel på tekniska intervjufrågor som kan dyka upp gällande CSS och responsiv webbutveckling. Frågorna är utformade för att testa både teoretisk förståelse och praktisk kunskap.
Använd dessa frågor för att testa din kunskap och förbereda dig för tekniska intervjuer.
Fråga 1: CSS grundsyntax och inkludering
Fråga:
Förklara skillnaden mellan inline CSS, intern CSS och extern CSS. Vilken metod rekommenderas och varför?
Förslag till svar:
- Inline CSS:
style
-attribut direkt på HTML-element, t.ex.<p style="color: red;">Text</p>
. - Intern CSS: CSS i
<style>
-taggar i HTML:s<head>
. - Extern CSS: Separat
.css
-fil länkad med<link rel="stylesheet" href="style.css">
.
Extern CSS rekommenderas eftersom:
- Separation: Håller struktur (HTML) och stil (CSS) åtskilda.
- Underhåll: Enkelt att ändra hela webbplatsens utseende från en fil.
- Prestanda: Webbläsaren kan cacha (spara) CSS-filen.
- Återanvändbarhet: Samma stilar kan användas på flera sidor.
Inline CSS bör undvikas då det är svårt att underhålla och har högst specificitet.
Fråga 2: CSS-selektorer
Fråga:
Förklara skillnaden mellan dessa selektorer och ge exempel på när du skulle använda var och en:
p { }
.highlight { }
#header { }
nav ul li { }
Förslag till svar:
p
– Elementselektor (type selector): Väljer alla<p>
-element. Används för grundstilar på elementtyper..highlight
– Klassselektor (class selector): Väljer alla element medclass="highlight"
. Återanvändbar stil för flera element.#header
– ID-selektor (ID selector): Väljer elementet medid="header"
. Unikt per sida, högst specificitet.nav ul li
– Selektor för ättling (descendant combinator): Väljer<li>
-element inuti<ul>
som är inuti<nav>
. Specifik kontextuell styling.
Bästa praxis: Använd klasser för återanvändbara stilar, ID:n sparsamt för unika element, och elementselektorer för grundstilar.
Fråga 3: Specificitet och kaskaden
Fråga:
Vilken färg får texten i detta exempel och varför?
<p id="viktigt" class="highlight">Text</p>
p { color: black; }
.highlight { color: blue; }
#viktigt { color: red; }
p.highlight { color: green; }
Förslag till svar:
Texten blir röd eftersom #viktigt
(ID-selektor) har högst specificitet.
Specificitetsordning (högst till lägst):
- Inline-stilar (
style
-attribut) - ID-selektorer (
#viktigt
) - Klass-, attribut- och pseudoselektorer (
.highlight
,p.highlight
) - Elementselektorer (
p
)
p.highlight
(kombinerad selektor) har högre specificitet än .highlight
ensam, men lägre än #viktigt
.
Fråga 4: Box model
Fråga:
Förklara CSS box model. Om ett element har width: 200px
, padding: 20px
, border: 5px solid black
och margin: 10px
, vad blir elementets totala bredd?
Förslag till svar: Box model består av (utifrån och in):
- Margin: Genomskinligt utrymme utanför elementet.
- Border: Elementets kantlinje.
- Padding: Genomskinligt utrymme mellan border och content.
- Content: Själva innehållet.
Standardberäkning (box-sizing: content-box
):
- Content: 200px
- Padding: 20px × 2 = 40px
- Border: 5px × 2 = 10px
- Total bredd: 250px (margin räknas inte in i elementets bredd, men påverkar avståndet till andra element)
Med box-sizing: border-box
: Padding och border inkluderas i width, så content-området blir 150px bred.
Fråga 5: Display-egenskaper
Fråga:
Vad är skillnaden mellan display: block
, display: inline
och display: inline-block
?
Förslag till svar:
-
display: block
:- Börjar på ny rad.
- Tar hela tillgängliga bredden.
- Respekterar
width
,height
, allamargin
ochpadding
. - Exempel:
<div>
,<p>
,<h1>
.
-
display: inline
:- Flödar med texten (ingen ny rad).
- Tar bara nödvändig bredd.
- Ignorerar
width
,height
,margin-top/bottom
,padding-top/bottom
. - Exempel:
<span>
,<a>
,<strong>
.
-
display: inline-block
:- Flödar som inline (ingen ny rad).
- Men respekterar alla box model-egenskaper som block.
- Perfekt för knappar eller element som ska vara bredvid varandra men ha specifika dimensioner.
Fråga 6: CSS-positionering
Fråga:
Förklara skillnaden mellan position: relative
, position: absolute
och position: fixed
. Ge exempel på användningsfall för var och en.
Förslag till svar:
-
position: relative
:- Elementet följer normala flödet.
- Kan förskjutas med
top
,left
etc. relativt till ursprunglig position. - Lämnar tomt utrymme där det skulle ha varit.
- Användning: Små justeringar, som positioneringskontext för absoluta barn.
-
position: absolute
:- Tas helt ur normala flödet.
- Positioneras relativt till närmaste positionerade förälder.
- Användning: Tooltips, dropdown-menyer, overlays.
-
position: fixed
:- Positioneras relativt till webbläsarfönstret.
- Stannar kvar vid scrollning.
- Användning: Fasta navigationsmenyer, "tillbaka till toppen"-knappar.
Fråga 7: Färger och typografi
Fråga:
Förklara skillnaden mellan dessa färgformat och när du skulle använda dem:
color: red;
color: #FF0000;
color: rgb(255, 0, 0);
color: rgba(255, 0, 0, 0.5);
Förslag till svar:
red
– Namngiven färg: Enkelt för grundfärger, begränsat utbud.#FF0000
– HEX: Vanligast för webben, exakt färgkontroll, används av designers.rgb(255, 0, 0)
– RGB: Bra när man arbetar med RGB-värden från grafiska program.rgba(255, 0, 0, 0.5)
– RGBA: Som RGB men med genomskinlighet (alpha). Perfekt för overlays och genomskinliga element.
Rekommendation: HEX för fasta färger, RGBA när genomskinlighet behövs.
Fråga 8: Responsiv design – Media Queries
Fråga:
Vad är skillnaden mellan dessa två Media Queries och vilken strategi representerar de?
/* A */
@media (max-width: 768px) {
.container { width: 100%; }
}
/* B */
@media (min-width: 768px) {
.container { width: 80%; }
}
Förslag till svar:
-
A (
max-width
): Desktop-first design- Skriver stilar för stora skärmar först.
- Ändrar/tar bort stilar för mindre skärmar.
- Regel gäller när skärmen är 768px eller mindre.
-
B (
min-width
): Mobile-first design (rekommenderad)- Skriver grundstilar för mobil först.
- Lägger till/förbättrar stilar för större skärmar.
- Regel gäller när skärmen är 768px eller större.
Mobile-first design är bättre för prestanda och tvingar fram prioritering av kärninnehåll.
Fråga 9: Mobile-first design
Fråga:
Varför rekommenderas mobile-first design och hur implementerar du det i CSS?
Förslag till svar: Fördelar med mobile-first design:
- Prestanda: Mobila enheter laddar enkla stilar först, komplexa läggs till progressivt.
- Innehållsprioritet: Tvingar fokus på viktigaste innehållet först.
- Enklare CSS: Lättare att lägga till än att ta bort/skriva över komplexa stilar.
Implementation:
/* Grundstilar för mobil (ingen media query) */
.container {
width: 100%;
padding: 10px;
}
/* Tablet och uppåt */
@media (min-width: 768px) {
.container {
width: 80%;
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
}
Viktigt: Alltid inkludera viewport meta-tag:
<meta name="viewport" content="width=device-width, initial-scale=1.0">
Fråga 10: Flexibla enheter
Fråga:
Förklara skillnaden mellan px
, em
, rem
och %
. När skulle du använda respektive enhet?
Förslag till svar:
-
px
– Absolut enhet: Exakt storlek, alltid samma.- Använd för: Borders, små detaljer som ska vara konstanta.
-
em
– Relativt till föräldernsfont-size
.- Problem: Kan ge komplexa beräkningar vid nesting.
- Använd för: Padding/margin som ska skala med textstorlek.
-
rem
– Relativt till rot-elementetsfont-size
.- Fördelar: Förutsägbar skalning, respekterar användarinställningar.
- Använd för: Font-storlekar, padding, margin.
-
%
– Relativt till förälderns motsvarande egenskap.- Använd för: Bredder i responsiva layouter.
Modern best practice: rem
för typografi och spacing, %
för layouter, px
för borders.
Fråga 11: Flexbox layout
Fråga:
Förklara hur Flexbox fungerar. Vad är skillnaden mellan justify-content
och align-items
? Ge ett exempel på när du skulle använda Flexbox.
Förslag till svar: Flexbox (Flexible Box Layout):
- Endimensionell layout – arbetar med antingen rader eller kolumner.
- Flex container (förälder) och flex items (barn).
- Aktiveras med
display: flex;
ellerdisplay: inline-flex;
.
Viktiga egenskaper:
justify-content
: Justerar items längs huvudaxeln (main axis).- Värden:
flex-start
,flex-end
,center
,space-between
,space-around
.
- Värden:
align-items
: Justerar items längs tväraxeln (cross axis).- Värden:
stretch
,flex-start
,flex-end
,center
,baseline
.
- Värden:
Exempel – Centrera innehåll:
.container {
display: flex;
justify-content: center; /* Centrerar horisontellt */
align-items: center; /* Centrerar vertikalt */
height: 100vh;
}
Användningsfall: Navigationsmenyer, centrering av innehåll, fördelning av utrymme mellan element.
Fråga 12: CSS Grid layout
Fråga:
Vad är skillnaden mellan CSS Grid och Flexbox? Visa hur du skulle skapa en enkel 3-kolumns layout med Grid.
Förslag till svar: CSS Grid vs Flexbox:
- Grid: Tvådimensionell layout (rader och kolumner samtidigt).
- Flexbox: Endimensionell layout (antingen rad eller kolumn).
Grid för komplexa layouter, Flexbox för enklare komponenter.
Exempel – 3-kolumns layout:
.container {
display: grid;
grid-template-columns: 1fr 2fr 1fr; /* 3 kolumner */
gap: 20px; /* Avstånd mellan items */
}
/* Responsiv variant */
@media (max-width: 768px) {
.container {
grid-template-columns: 1fr; /* En kolumn på mobil */
}
}
Grid-terminologi:
grid-template-columns/rows
: Definierar kolumn/rad-storlekar.fr
(fraction): Tar en andel av tillgängligt utrymme.gap
: Avstånd mellan grid-items.grid-area
: Placerar items på specifika positioner.
Användningsfall: Sidlayouter, kort-layouts, komplexa responsiva designs.
Tips för tekniska intervjuer
- Visa dina kunskaper visuellt – rita box model eller beskriv layout-flöde.
- Förklara trade-offs – diskutera för- och nackdelar med olika tillvägagångssätt.
- Ge praktiska exempel från projekt du arbetat på.
- Fråga om kontext – olika lösningar passar olika situationer.
- Visa problemlösningsförmåga – förklara hur du skulle felsöka CSS-problem.
Kapitel 4: JavaScript – Gör webben interaktiv
I de tidigare kapitlen har vi lärt oss att bygga upp webbsidor med HTML, ge dem struktur och semantik, samt styla dem med CSS. Men webben är mer än bara statiskt innehåll och utseende – det är interaktivitet, dynamik och möjligheten att reagera på användarens handlingar. Det är här JavaScript kommer in.
JavaScript är det språk som ger liv åt webbsidor. Med JavaScript kan du:
- Hantera händelser som en användare initierar.
- Ändra innehåll och stil på sidan utan att ladda om den.
- Hämta och visa data från andra tjänster (API:er).
- Skapa spel, animationer och mycket mer.
Motivation:
JavaScript är ett av världens mest använda programmeringsspråk och är oumbärligt för modern webbutveckling. Nästan alla interaktiva funktioner du ser på webben bygger på JavaScript.
Språkpolicy:
Svenska används i förklaringar, men engelska tekniska termer anges i parentes första gången de nämns. Variabel- och funktionsnamn skrivs på engelska.
Vad kommer du att lära dig i detta kapitel?
- Introduktion till JavaScript: Vad är JavaScript och hur används det i webbläsaren?
- Grunderna i programmering: Variabler, datatyper, operatorer och uttryck.
- Villkor och logik: If-satser och jämförelser.
- Loopar: Hur du upprepar kod med for- och while-loopar.
- Funktioner: Hur du organiserar och återanvänder kod.
- Interaktion med HTML (DOM): Hur JavaScript kan läsa och ändra innehåll på sidan.
- Händelser: Hur du reagerar på användarens handlingar, t.ex. klick och tangenttryckningar.
- Praktiska övningar: Du får skriva och testa JavaScript-kod direkt på dina egna webbsidor.
Nu är det dags att ta steget från statiska sidor
Introduktion till JavaScript
JavaScript är det programmeringsspråk som gör webbsidor interaktiva och dynamiska. Med JavaScript kan du reagera på användarens handlingar, ändra innehåll på sidan utan att ladda om den, skapa spel, animationer och mycket mer. Nästan alla moderna webbplatser använder JavaScript på något sätt.
Vad är JavaScript?
- JavaScript är ett skriptspråk som körs i webbläsaren (client-side), men kan även köras på servrar (t.ex. med Node.js).
- Det är ett av de tre grundläggande språken för webben:
- HTML – strukturen på sidan
- CSS – utseendet och layouten
- JavaScript – interaktivitet och logik
Varför använda JavaScript?
- Interaktivitet: Gör det möjligt att reagera på klick, tangenttryckningar, formulär och andra händelser.
- Dynamiskt innehåll: Ändra text, bilder och layout utan att ladda om sidan.
- Validering: Kontrollera formulär innan de skickas till servern.
- Animationer: Skapa rörelse och effekter.
- Kommunikation: Hämta och skicka data till andra tjänster (API:er) utan att ladda om sidan.
Hur lägger man till JavaScript i en webbsida?
JavaScript kan inkluderas på två huvudsakliga sätt:
1. Inbäddat i HTML-filen
<!DOCTYPE html>
<html lang="sv">
<head>
<meta charset="UTF-8">
<title>Min första JS-sida</title>
</head>
<body>
<h1>Hej!</h1>
<script>
console.log("Sidan är laddad!");
alert("Välkommen till min sida!");
</script>
</body>
</html>
2. Extern JavaScript-fil
Det rekommenderas att lägga JavaScript i en separat fil för bättre struktur och återanvändbarhet.
<!DOCTYPE html>
<html lang="sv">
<head>
<meta charset="UTF-8">
<title>Extern JS</title>
</head>
<body>
<h1>Exempel</h1>
<script src="script.js"></script>
</body>
</html>
I filen script.js
:
console.log("Detta kommer från en extern fil!");
3. Importera JavaScript som modul
Du kan också inkludera JavaScript som en modul med attributet type="module"
. Det gör det möjligt att använda import
och export
för att dela kod mellan filer.
<script type="module" src="main.js"></script>
I filen main.js
:
import { minFunktion } from './utils.js';
minFunktion();
Detta är användbart för större projekt där du vill strukturera din kod i flera filer.
Namngivning av JavaScript-filer
När du skapar JavaScript-filer är det viktigt att ge dem tydliga och beskrivande namn. Det gör det enklare att förstå vad filen innehåller och underlättar när projektet växer.
-
Korta filnamn:
Används ofta för enklare eller generella skript.script.js
main.js
app.js
-
Längre och mer beskrivande filnamn:
För större projekt eller specifika funktioner är det vanligt att använda camelCase (små bokstäver och stor bokstav för varje nytt ord).userProfile.js
dataFetcher.js
formValidator.js
Att använda beskrivande namn gör koden lättare att underhålla och förstå för både dig själv och andra utvecklare.
Grunder: Variabler och utskrift
Variabler
En variabel används för att spara data som kan användas senare.
let namn = "Anna";
let alder = 25;
Utskrift
- Till konsolen:
console.log("Hej!");
- Som popup:
alert("Varning!");
Sammanfattning
- JavaScript gör webbsidor interaktiva och dynamiska.
- Det kan läggas till direkt i HTML eller i en extern fil.
- Med JavaScript kan du skapa allt från enkla effekter till avancerade webbapplikationer.
- Nästa steg är att lära dig grunderna i programmering med JavaScript: variabler, datatyper, operatorer och logik.
Nu är du redo att börja skriva din första JavaScript
Variabler i JavaScript
Variabler är en grundläggande del av programmering. De används för att lagra och hantera data som kan ändras under programmets gång. I JavaScript kan du spara allt från siffror och text till objekt och listor i variabler.
Vad är en variabel?
En variabel är ett namn som pekar på ett värde i datorns minne. Du kan använda variabeln för att läsa, ändra eller använda värdet senare i programmet.
Namngivning av variabler
När du skapar variabler i JavaScript är det viktigt att använda tydliga och beskrivande namn. Det rekommenderas att skriva variabelnamn på engelska, eftersom det är standard inom programmering och gör koden lättare att förstå för andra utvecklare.
JavaScript använder vanligtvis camelCase för variabelnamn. Det innebär att det första ordet skrivs med små bokstäver och varje nytt ord börjar med stor bokstav, till exempel userName
, totalAmount
eller isActive
.
Att vara konsekvent med namngivningen gör koden mer lättläst och underlättar samarbeten. Undvik att blanda språk eller olika stilar i samma projekt.
Exempel på bra variabelnamn:
let userEmail = "anna@example.com";
let itemCount = 5;
let isLoggedIn = false;
Exempel på mindre bra variabelnamn:
let användareNamn = "Anna"; // Blanda inte språk
let Item_count = 10; // Undvik underscore och stor bokstav i början
let x = true; // För kort och otydligt
Deklarera variabler
I modern JavaScript används oftast let
och const
för att skapa variabler. Äldre kod kan använda var
, men det rekommenderas inte längre.
let
– Skapar en variabel som kan ändras (reassignas).const
– Skapar en variabel som inte kan ändras (konstant). Värdet måste sättas direkt.
Exempel:
let name = "Anna";
let age = 25;
const pi = 3.14159;
Ändra värdet på en variabel
Variabler skapade med let
kan få nya värden:
let score = 10;
score = 15; // score är nu 15
Variabler skapade med const
kan inte ändras:
const maxUsers = 100;
// maxUsers = 200; // Fel! Går inte att ändra en const-variabel
Datatyper
Variabler kan innehålla olika typer av data:
- String (text):
"Hello"
,'JavaScript'
- Number (tal):
42
,3.14
- Boolean (sant/falskt):
true
,false
- Array (lista):
[1, 2, 3]
- Object (objekt):
{ name: "Anna", age: 25 }
- Null (avsaknad av värde):
null
- Undefined (ej tilldelat värde):
undefined
Exempel:
let text = "Hello!";
let number = 123;
let isActive = true;
let list = [1, 2, 3];
let person = { name: "Anna", age: 25 };
let emptyValue = null;
let unknown;
Namnge variabler
- Variabelnamn får innehålla bokstäver, siffror,
_
och$
, men får inte börja med en siffra. - Använd beskrivande namn (t.ex.
userName
istället förx
). - JavaScript är case sensitive:
name
ochName
är olika variabler.
Utskrift och användning
Du kan använda variabler i uttryck och skriva ut dem med console.log
:
let firstName = "Sara";
console.log("Hello, " + firstName + "!");
Sammanfattning
- Variabler används för att lagra och hantera data i JavaScript.
- Använd
let
för variabler som kan ändras,const
för konstanter. - Variabler kan innehålla olika datatyper: text, tal, boolean, listor, objekt m.m.
- Välj tydliga och beskrivande variabelnamn på engelska.
- Variabler är grunden för att kunna skapa dynamiska och flexibla program.
Att förstå och använda variabler är ett av de första stegen mot att bli en skicklig programmerare
Funktioner i JavaScript
Funktioner är en av de viktigaste byggstenarna i JavaScript. De gör det möjligt att återanvända kod, strukturera programmet och dela upp logik i mindre, hanterbara delar.
Vad är en funktion?
En funktion är en namngiven kodblock som kan köras (anropas) flera gånger. Du kan skicka in värden (argument) till funktionen och få tillbaka ett resultat (return-värde).
Exempel:
function greet(name) {
console.log("Hej, " + name + "!");
}
greet("Anna"); // Skriver ut: Hej, Anna!
greet("Erik"); // Skriver ut: Hej, Erik!
Skapa och anropa funktioner
Deklarera en funktion
function add(a, b) {
return a + b;
}
Anropa en funktion
let sum = add(3, 5); // sum blir 8
Funktioner med och utan argument
-
Med argument:
Funktioner kan ta emot värden (argument) som används inuti funktionen.function square(x) { return x * x; } let result = square(4); // result blir 16
-
Utan argument:
Funktioner kan också deklareras utan argument.function sayHello() { console.log("Hej!"); } sayHello();
Returnera värden
En funktion kan returnera ett värde med return
. När return
körs avslutas funktionen och värdet skickas tillbaka till den som anropade funktionen.
function multiply(a, b) {
return a * b;
}
let produkt = multiply(2, 6); // produkt blir 12
Om inget return
anges returnerar funktionen automatiskt undefined
.
Funktioner som variabler (funktionella uttryck)
Du kan också spara en funktion i en variabel:
const subtract = function(a, b) {
return a - b;
};
let diff = subtract(10, 3); // diff blir 7
Arrow functions (funktion utan nyckelordet function): () =>
Ett modernt och kortare sätt att skriva funktioner är med arrow functions:
const divide = (a, b) => {
return a / b;
};
let kvot = divide(10, 2); // kvot blir 5
Om funktionen bara returnerar ett värde kan du skriva ännu kortare:
const double = x => x * 2;
let dubbelt = double(7); // dubbelt blir 14
Funktioner och scope
Variabler som deklareras inuti en funktion är lokala för den funktionen och kan inte nås utanför.
function testScope() {
let lokal = "Jag finns bara här";
console.log(lokal);
}
testScope();
// console.log(lokal); // Fel! lokal är inte definierad här
Funktioner som argument (callback functions)
Funktioner kan skickas som argument till andra funktioner. Detta är vanligt i t.ex. eventhantering och array-metoder.
function processArray(arr, callback) {
for (let item of arr) {
callback(item);
}
}
processArray([1, 2, 3], function(num) {
console.log(num * 2);
});
// Skriver ut: 2, 4, 6
Sammanfattning
- Funktioner gör det möjligt att återanvända och strukturera kod.
- Du kan skapa funktioner med
function
-syntax eller som arrow functions. - Funktioner kan ta emot argument och returnera värden.
- Variabler inuti funktioner är lokala (scope).
- Funktioner kan skickas som argument till andra funktioner (callback).
Att förstå och använda funktioner är avgörande för att skriva effektiv och läsbar JavaScript
Kontrollstrukturer i JavaScript
Kontrollstrukturer är byggstenarna som styr flödet i ett program. Med hjälp av dessa kan vi bestämma vilken kod som ska köras, när och hur många gånger. De vanligaste kontrollstrukturerna är villkorssatser (if/else) och loopar (for, while).
Villkorssatser (if, else if, else)
Med villkorssatser kan vi utföra olika kod beroende på om ett visst villkor är sant eller falskt.
Syntax:
if (villkor) {
// kod som körs om villkoret är sant
} else if (annatVillkor) {
// kod som körs om det andra villkoret är sant
} else {
// kod som körs om inget av villkoren ovan är sant
}
Exempel:
let age = 18;
if (age >= 18) {
console.log("Du är myndig.");
} else {
console.log("Du är inte myndig.");
}
Jämförelseoperatorer
För att skapa villkor använder vi jämförelseoperatorer:
===
lika med (värde och typ)!==
inte lika med (värde och typ)==
lika med (bara värde, undvik i modern JS)!=
inte lika med (bara värde, undvik i modern JS)>
större än<
mindre än>=
större än eller lika med<=
mindre än eller lika med
Exempel:
let x = 5;
if (x !== 10) {
console.log("x är inte 10");
}
Logiska operatorer
Kombinera flera villkor med logiska operatorer:
&&
och (båda villkoren måste vara sanna)||
eller (minst ett villkor måste vara sant)!
inte (vänder på sant/falskt)
Exempel:
let temp = 20;
if (temp > 15 && temp < 25) {
console.log("Lagom varmt!");
}
Switch-sats
När du har många möjliga värden att jämföra mot kan switch
vara tydligare än många if/else
.
Syntax:
switch (uttryck) {
case värde1:
// kod
break;
case värde2:
// kod
break;
default:
// kod om inget matchar
}
Exempel:
let day = "tisdag";
switch (day) {
case "måndag":
console.log("Ny vecka!");
break;
case "tisdag":
console.log("Andra dagen.");
break;
default:
console.log("Någon annan dag.");
}
Loopar
Loopar används för att upprepa kod flera gånger.
For-loop
Syntax:
for (start; villkor; steg) {
// kod som upprepas
}
Exempel:
for (let i = 0; i < 5; i++) {
console.log("i är nu: " + i);
}
While-loop
Syntax:
while (villkor) {
// kod som upprepas så länge villkoret är sant
}
Exempel:
let count = 0;
while (count < 3) {
console.log("Räknare: " + count);
count++;
}
Break och continue
break
– Avbryter loopen direkt.continue
– Hoppar över resten av koden i loopen och går till nästa varv.
Exempel:
for (let i = 0; i < 5; i++) {
if (i === 3) break; // Avbryter loopen när i är 3
if (i === 1) continue; // Hoppar över när i är 1
console.log(i);
}
// Skriver ut: 0, 2
Sammanfattning
- Kontrollstrukturer styr flödet i programmet.
- Använd
if
,else if
,else
ochswitch
för att fatta beslut. - Använd
for
ochwhile
för att upprepa kod. - Jämförelse- och logiska operatorer hjälper dig att skapa villkor.
break
ochcontinue
ger extra kontroll i loopar.
Att behärska kontrollstrukturer är avgörande för att kunna skriva logisk och flexibel kod. I nästa avsnitt ska vi se hur JavaScript kan interagera med själva HTML-sidan.
Övningar
1. While-loop
Skriv en while-loop som räknar ner från 10 till 0 och skriver ut varje tal.
let count = 10;
// fortsätt med din lösning
2. Array-transformation
Skapa en array med temperaturer i Celsius och använd map för att konvertera dem till Fahrenheit. (Formel: F = C * 9/5 + 32)
// array med celsius temperaturer
let celsiusTemp = [0, 10, 20, 30, 40];
// fortsätt med din lösning
3. Filtrering med forEach
Använd forEach för att skriva ut alla jämna tal i en array.
// array siffror
let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// fortsätt med din lösning
Lösningsförslag
Övning 1
let count = 10;
while (count >= 0) {
console.log(count);
count--;
}
// Utskrift: 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0
Övning 2
let celsiusTemp = [0, 10, 20, 30, 40];
let fahrenheitTemp = celsiusTemp.map(temp => temp * 9/5 + 32);
console.log(fahrenheitTemp);
// Utskrift: [32, 50, 68, 86, 104]
Övning 3
let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
numbers.forEach(num => {
if (num % 2 === 0) {
console.log(num);
}
});
// Utskrift: 2, 4, 6, 8, 10
DOM-manipulation och Events: Interagera med Sidan
JavaScript blir riktigt kraftfullt när vi använder det för att interagera med och ändra innehållet och strukturen på vår HTML-sida efter att den har laddats. Detta görs genom DOM (Document Object Model). Vi behöver också kunna reagera på användarens handlingar, vilket vi gör genom att hantera Events (Händelser).
Mål: Förstå vad DOM är, lära oss hur man väljer ut (selectar) HTML-element med JavaScript, hur man manipulerar (ändrar) deras innehåll, attribut och stilar, samt hur man kopplar händelselyssnare (addEventListener
) för att reagera på användarinteraktioner som klick.
Vad är DOM (Document Object Model)?
När en webbläsare laddar en HTML-sida skapar den en modell av sidans struktur i minnet. Denna modell kallas DOM. DOM representerar HTML-dokumentet som ett träd av objekt (noder), där varje HTML-element (<body>
, <h1>
, <p>
, <div>
, etc.), textinnehåll och attribut blir ett objekt i trädet.
graph TD A["document"] --> B["html"]; B --> C["head"]; B --> D["body"]; C --> E["title"]; E --> F["Text: Min Sida"]; D --> G["h1"]; G --> H["Text: Rubrik"]; D --> I["p"]; I --> J["Text: Paragraf..."]; style A fill:#ddd,stroke:#333,stroke-width:2px style F fill:#fff,stroke:#ccc,stroke-width:1px style H fill:#fff,stroke:#ccc,stroke-width:1px style J fill:#fff,stroke:#ccc,stroke-width:1px
Diagram: Förenklat exempel på DOM-trädet för en enkel HTML-sida.
Varför är DOM viktigt?
JavaScript kan komma åt och manipulera detta DOM-träd. Det betyder att vi med JavaScript kan:
- Hitta specifika HTML-element.
- Ändra deras textinnehåll.
- Ändra deras attribut (som
src
på en bild ellerhref
på en länk). - Ändra deras CSS-stilar.
- Lägga till nya element.
- Ta bort befintliga element.
Allt detta kan göras utan att behöva ladda om sidan. Det är grunden för dynamiska webbapplikationer.
Välja element från DOM
För att kunna manipulera ett element måste vi först "hitta" det i DOM-trädet. Detta görs med olika metoder på det globala document
-objektet:
Moderna metoder (rekommenderas)
-
document.querySelector(cssSelektor)
Returnerar det första elementet i dokumentet som matchar den angivna CSS-selektorn (t.ex.#minId
,.minKlass
,p
,nav > ul > li
).
Om ingen matchning hittas returnerasnull
.
Exempel:const mainHeading = document.querySelector('#main-title'); const firstButton = document.querySelector('.btn'); const navLink = document.querySelector('nav ul li a');
-
document.querySelectorAll(cssSelektor)
Returnerar en NodeList (en samling som liknar en array) med alla element som matchar selektorn.
Om inga matchningar hittas returneras en tom NodeList.
Exempel:const allParagraphs = document.querySelectorAll('p'); const allButtons = document.querySelectorAll('.btn'); allButtons.forEach(button => { console.log("Hittade en knapp!"); // Gör något med varje knapp här });
Äldre metoder (fortfarande vanliga)
document.getElementById(id)
– Returnerar elementet med det angivna ID:t (ellernull
).document.getElementsByClassName(klassnamn)
– Returnerar en HTMLCollection (en live-samling) med alla element som har den angivna klassen.document.getElementsByTagName(taggnamn)
– Returnerar en HTMLCollection med alla element av den angivna taggtypen (t.ex. 'p', 'div').
Skillnad NodeList vs HTMLCollection:
En NodeList (från querySelectorAll
) är oftast statisk, medan en HTMLCollection (från getElementsByClassName
/TagName
) är live och uppdateras automatiskt om DOM ändras. NodeList har också forEach
-metoden inbyggd, vilket HTMLCollection ofta saknar.
Manipulera element
När du väl har valt ett eller flera element och sparat dem i en variabel, kan du ändra dem:
Ändra textinnehåll
element.textContent
– Hämtar eller sätter textinnehållet för ett element och dess ättlingar, utan HTML-taggar.element.innerText
– LiknartextContent
, men tar hänsyn till CSS-styling och inkluderar inte text från dolda element.
const heading = document.querySelector('h1');
console.log(heading.textContent); // Visar nuvarande text
heading.textContent = "Ny Rubrik från JS!"; // Ändrar texten
Ändra HTML-innehåll
element.innerHTML
– Hämtar eller sätter HTML-koden inuti ett element.
OBS: Använd med försiktighet, speciellt med osäker data (risk för XSS).
const contentDiv = document.querySelector('#content');
// contentDiv.innerHTML = "<h2>Nytt innehåll</h2><p>Detta är en paragraf.</p>";
Ändra attribut
element.setAttribute('attributnamn', 'nyttVärde')
– Sätter värdet på ett attribut.element.getAttribute('attributnamn')
– Hämtar värdet på ett attribut.- För vanliga attribut som
id
,src
,href
,class
kan man ofta komma åt dem direkt som egenskaper.
const link = document.querySelector('a');
link.href = "https://www.google.com";
link.target = "_blank";
const image = document.querySelector('img');
image.setAttribute('src', 'nybild.jpg');
image.alt = "Beskrivning av ny bild";
Ändra CSS-stilar
element.style.cssEgenskap = 'värde'
– Ändrar inline-stilen för ett element. CSS-egenskaper skrivs i camelCase (t.ex.backgroundColor
).const box = document.querySelector('.box'); box.style.backgroundColor = 'lightblue'; box.style.padding = '20px'; box.style.border = '1px solid blue';
- Bättre metod: Ändra elementets klass med
element.classList
:element.classList.add('ny-klass')
element.classList.remove('gammal-klass')
element.classList.toggle('aktiv-klass')
element.classList.contains('viss-klass')
/* I style.css */
.highlight {
background-color: yellow;
font-weight: bold;
}
const importantText = document.querySelector('.important');
importantText.classList.add('highlight');
Skapa och lägga till element
document.createElement('taggnamn')
– Skapar ett nytt HTML-element.parentElement.appendChild(newElement)
– Lägger tillnewElement
som sista barnet tillparentElement
.parentElement.insertBefore(newElement, referenceElement)
– InfogarnewElement
förereferenceElement
.element.remove()
– Tar bort elementet från DOM.
const newItem = document.createElement('li');
newItem.textContent = "Ny punkt";
const list = document.querySelector('#myList');
list.appendChild(newItem);
Hantera händelser
Events är händelser som inträffar i webbläsaren, oftast initierade av användaren (klick, tangenttryck, musrörelser) men också av webbläsaren själv (t.ex. att sidan laddats klart).
JavaScript låter oss "lyssna" efter dessa händelser och köra en funktion (en event handler eller callback function) när händelsen inträffar.
Koppla händelselyssnare
element.addEventListener(eventType, eventHandlerFunction)
Den moderna och rekommenderade metoden för att koppla en händelselyssnare.
const myButton = document.querySelector('#myButton');
function handleButtonClick(event) {
console.log("Knappen klickades!");
event.target.textContent = "Klickad!";
event.target.style.backgroundColor = 'lightgreen';
}
myButton.addEventListener('click', handleButtonClick);
const anotherButton = document.querySelector('#otherBtn');
anotherButton.addEventListener('click', (event) => {
event.target.classList.toggle('active');
});
Vanliga händelsetyper
- Mus-händelser:
click
,dblclick
,mouseover
,mouseout
,mousedown
,mouseup
,mousemove
- Tangentbords-händelser:
keydown
,keyup
,keypress
- Formulär-händelser:
submit
,input
,change
,focus
,blur
- Fönster/Dokument-händelser:
load
,DOMContentLoaded
,resize
,scroll
Förhindra standardbeteende
event.preventDefault()
– Stoppar webbläsarens standardbeteende för händelsen.
Används ofta för att hantera formulär med JavaScript.
const myForm = document.querySelector('#myForm');
myForm.addEventListener('submit', (event) => {
event.preventDefault();
console.log("Formulär skulle ha skickats, men vi stoppade det!");
// Hantera formulärdata här...
});
Sammanfattning
DOM är webbläsarens representation av HTML-sidan som ett träd av objekt. JavaScript kan interagera med DOM för att:
- Välja element: Med
querySelector
(första matchningen) ochquerySelectorAll
(alla matchningar) baserat på CSS-selektorer. - Manipulera element: Ändra
textContent
,innerHTML
(försiktigt!), attribut (setAttribute
, direkta egenskaper), stilar (element.style
eller, bättre,element.classList
). - Skapa och ta bort element: Med
createElement
,appendChild
,remove
. - Reagera på events: Med
addEventListener
kan du köra JavaScript-kod när användaren interagerar med sidan.
Att behärska DOM-manipulation och händelsehantering är centralt för frontend-utveckling med JavaScript.
Praktiska övningar: JavaScript och interaktivitet
Att läsa teori är viktigt, men det är genom att skriva och testa kod som du verkligen lär dig JavaScript. Här hittar du övningar som hjälper dig att befästa kunskaperna från kapitlet om JavaScript, DOM-manipulation och händelser.
Mål:
Träna på att använda JavaScript för att skapa interaktivitet, manipulera DOM och hantera händelser på webbsidor.
Förutsättningar:
Du har en HTML-fil att arbeta med och kan lägga till eller länka en JavaScript-fil. Du kan öppna sidan i en webbläsare och se resultatet av din kod.
Övning 1: Skriv ut meddelande i konsolen
- Skapa en fil
script.js
och koppla den till din HTML-fil med<script src="script.js"></script>
. - Skriv kod som skriver ut "Hello, JavaScript!" i webbläsarens konsol.
- Öppna webbsidan och kontrollera i konsolen (F12) att meddelandet syns.
Övning 2: Ändra text på sidan med JavaScript
- Lägg till ett element i din HTML, t.ex.
<p id="message">Detta ska ändras</p>
. - Skriv JavaScript som ändrar texten i paragrafen till "Texten är nu ändrad!" när sidan laddas.
Övning 3: Byt färg på ett element vid knapptryck
- Lägg till en knapp:
<button id="colorBtn">Byt färg</button>
och ett text-element:<p id="colorText">Färgtest</p>
. - Skriv JavaScript som gör att texten i
colorText
byter färg (t.ex. till blå) när du klickar på knappen.
Övning 4: Lägg till nya element i DOM
- Lägg till en tom lista i HTML:
<ul id="myList"></ul>
och en knapp:<button id="addBtn">Lägg till punkt</button>
. - Skriv JavaScript som lägger till ett nytt
<li>
-element med valfri text i listan varje gång du klickar på knappen.
Övning 5: Enkel räknare
- Lägg till en knapp:
<button id="countBtn">Räkna</button>
och ett element för att visa räknaren:<span id="counter">0</span>
. - Skriv JavaScript som ökar värdet i
counter
med 1 varje gång du klickar på knappen.
Övning 6: Formulär och validering
- Lägg till ett enkelt formulär i HTML:
<form id="myForm"> <input type="text" id="nameInput" placeholder="Skriv ditt namn"> <button type="submit">Skicka</button> </form> <p id="formMessage"></p>
- Skriv JavaScript som:
- Förhindrar att sidan laddas om när formuläret skickas.
- Läser av värdet i
nameInput
. - Skriver ut ett meddelande i
formMessage
, t.ex. "Hej, [namn]!", om fältet inte är tomt. Om det är tomt, visa ett felmeddelande.
Sammanfattning och nästa steg
Genom att göra dessa övningar får du praktisk erfarenhet av att använda JavaScript för att manipulera DOM och hantera händelser. Fortsätt experimentera – prova att lägga till fler funktioner, kombinera övningarna eller skapa egna små projekt!
I nästa kapitel lär du dig mer om hur du strukturerar och återanvänder kod med funktioner och moduler, samt hur du arbetar med mer avancerade datatyper och asynkrona operationer.
Tekniska Intervjufrågor: JavaScript och DOM-manipulation
Detta avsnitt innehåller exempel på tekniska intervjufrågor som kan dyka upp gällande JavaScript och frontend-programmering. Frågorna är utformade för att testa både teoretisk förståelse och praktisk kunskap.
Använd dessa frågor för att testa din kunskap och förbereda dig för tekniska intervjuer.
Fråga 1: JavaScript Grundläggande och Inkludering
Fråga: "Förklara skillnaden mellan att inkludera JavaScript inline, internt och externt. Varför placeras <script>
-taggar oftast före </body>
?"
Förslag till svar:
- Inline:
onclick="alert('Klick')"
- Bör undvikas, blandar struktur och beteende - Internt:
<script>
-taggar i HTML-dokumentet - OK för små tester - Externt: Separat
.js
-fil länkad med<script src="script.js">
- Rekommenderas
Varför före </body>
:
- DOM-laddning: HTML-element måste finnas innan JavaScript kan manipulera dem
- Prestanda: Sidan kan börja renderas innan script laddas
- Användarupplevelse: Undviker att script blockerar sidladdningen
Best practice: Extern JS-fil precis före </body>
för separation av concerns och bättre underhållbarhet.
Fråga 2: Variabler och Deklarationer
Fråga: "Vad är skillnaden mellan let
, const
och var
? När skulle du använda respektive?"
Förslag till svar:
const
: Konstant värde, kan inte tilldelas om, block-scopeconst name = "Alice"; // Kan inte ändras
let
: Variabelt värde, kan tilldelas om, block-scopelet age = 25; age = 26; // OK
var
: Äldre syntax, function-scope (inte block-scope), hoisting-problem
Best practice:
- Använd
const
som standard - signalerar att värdet inte ska ändras - Använd
let
när du behöver ändra värdet - Undvik
var
- orsakar förvirring med scope
Block-scope exempel:
if (true) {
let blockVar = "synlig här";
const blockConst = "också synlig här";
}
// blockVar och blockConst är INTE synliga här
Fråga 3: Datatyper och Typomvandling
Fråga: "Förklara JavaScripts primitiva datatyper. Vad händer i följande kod och varför?"
console.log("5" + 3);
console.log("5" - 3);
console.log(true + 1);
Förslag till svar: Primitiva datatyper:
string
- text:"Hej"
number
- tal:42
,3.14
boolean
- sant/falskt:true
,false
undefined
- ej tilldelat värdenull
- avsiktligt tomt värdesymbol
- unik identifierarebigint
- mycket stora heltal
Typomvandling (Type Coercion):
console.log("5" + 3); // "53" - + konkatenerar strängar
console.log("5" - 3); // 2 - konverterar "5" till number
console.log(true + 1); // 2 - true blir 1
Varför: JavaScript försöker automatiskt konvertera typer, vilket kan ge oväntade resultat. Var försiktig med operatorer på olika typer.
Fråga 4: Funktioner och Syntax
Fråga: "Visa tre olika sätt att skriva funktioner i JavaScript. Vad är skillnaderna mellan dem?"
Förslag till svar:
// 1. Funktionsdeklaration
function add(a, b) {
return a + b;
}
// 2. Funktionsuttryck
const multiply = function(a, b) {
return a * b;
};
// 3. Arrow function (ES6)
const divide = (a, b) => a / b;
// Kort arrow function (en parameter)
const square = x => x * x;
Skillnader:
- Deklaration: Hoistas (kan anropas före deklaration)
- Uttryck: Hoistas inte, måste deklareras före anrop
- Arrow: Kortare syntax, annorlunda
this
-binding, hoistas inte
När använda vad:
- Arrow functions: För korta, enkla funktioner och callbacks
- Deklarationer: För huvudfunktioner i programmet
- Uttryck: När du vill tilldela funktion till variabel
Fråga 5: Scope och Synlighet
Fråga: "Förklara skillnaden mellan globalt, lokalt och block-scope. Vad skrivs ut i denna kod?"
const global = "Jag är global";
function testScope() {
const local = "Jag är lokal";
if (true) {
const block = "Jag är i block";
console.log(global); // ?
console.log(local); // ?
console.log(block); // ?
}
console.log(block); // ?
}
Förslag till svar: Scope-typer:
- Global scope: Tillgänglig överallt i programmet
- Funktions-scope (lokalt): Tillgänglig endast inuti funktionen
- Block-scope: Tillgänglig endast inuti
{}
(förlet
/const
)
Utskrift:
console.log(global); // "Jag är global" - global variabel
console.log(local); // "Jag är lokal" - lokal till funktionen
console.log(block); // "Jag är i block" - i samma block
console.log(block); // ReferenceError! block finns inte här
Princip: Inre scope kan komma åt yttre scope, men inte tvärtom.
Fråga 6: Kontrollstrukturer
Fråga: "Skriv kod som kontrollerar en användares ålder och ger olika meddelanden. Använd både if/else
och switch
för att visa skillnaden."
Förslag till svar:
// Med if/else (för ranges/intervall)
function checkAgeIf(age) {
if (age < 13) {
return "Barn";
} else if (age < 20) {
return "Tonåring";
} else if (age < 65) {
return "Vuxen";
} else {
return "Senior";
}
}
// Med switch (för specifika värden)
function checkDayType(dayNumber) {
switch (dayNumber) {
case 1:
case 2:
case 3:
case 4:
case 5:
return "Vardag";
case 6:
case 7:
return "Helg";
default:
return "Ogiltig dag";
}
}
När använda vad:
if/else
: För intervall, komplexa villkor, booleansswitch
: För specifika värden, många exakta fall- Viktigt: Glöm inte
break;
i switch-case (om du inte returnar)!
Fråga 7: Loopar och Iteration
Fråga: "Visa olika sätt att iterera över en array. Vad är för- och nackdelarna med varje metod?"
Förslag till svar:
const fruits = ["äpple", "banan", "apelsin"];
// 1. Traditional for-loop
for (let i = 0; i < fruits.length; i++) {
console.log(i, fruits[i]);
}
// Fördelar: Full kontroll över index, kan ändra loop-variabel
// Nackdelar: Mer kod, risk för off-by-one errors
// 2. for...of (ES6)
for (const fruit of fruits) {
console.log(fruit);
}
// Fördelar: Ren syntax, direkt tillgång till värden
// Nackdelar: Ingen direkt tillgång till index
// 3. forEach (array method)
fruits.forEach((fruit, index) => {
console.log(index, fruit);
});
// Fördelar: Funktionell stil, index som parameter
// Nackdelar: Kan inte break/continue
// 4. while loop
let i = 0;
while (i < fruits.length) {
console.log(fruits[i]);
i++;
}
// Fördelar: Flexibel, bra för okänt antal iterationer
// Nackdelar: Risk för oändlig loop om man glömmer öka i
Fråga 8: DOM och Element-selektion
Fråga: "Vad är DOM? Visa olika sätt att välja element från DOM och när du skulle använda varje metod."
Förslag till svar: DOM (Document Object Model):
- Webbläsarens representation av HTML-dokumentet som ett träd av objekt
- JavaScript kan manipulera detta träd för att ändra sidan dynamiskt
- Varje HTML-element blir ett objekt med egenskaper och metoder
Element-selektion:
// Modern metod (rekommenderas)
const element = document.querySelector('#myId');
const elements = document.querySelectorAll('.myClass');
// Äldre metoder (fortfarande används)
const byId = document.getElementById('myId');
const byClass = document.getElementsByClassName('myClass');
const byTag = document.getElementsByTagName('p');
Skillnader:
querySelector
: Första matchning, CSS-selektor syntaxquerySelectorAll
: Alla matchningar, returnerar NodeListgetElementById
: Snabbast för ID, bara ett elementgetElementsByClass/Tag
: Returnerar HTMLCollection (live)
Best practice: Använd querySelector
/querySelectorAll
för flexibilitet.
Fråga 9: DOM-manipulation
Fråga: "Hur ändrar du innehållet, attribut och stilar på ett HTML-element med JavaScript? Visa exempel."
Förslag till svar:
const element = document.querySelector('#myElement');
// Ändra textinnehåll
element.textContent = "Ny text"; // Säker, bara text
element.innerHTML = "<strong>HTML</strong>"; // Osäker med user input!
// Ändra attribut
element.setAttribute('src', 'newimage.jpg');
element.src = 'newimage.jpg'; // Direktegenskap
const currentSrc = element.getAttribute('src');
// Ändra stilar (undvik för många ändringar)
element.style.backgroundColor = 'red';
element.style.fontSize = '20px';
// Bättre: Använd CSS-klasser
element.classList.add('highlight'); // Lägg till klass
element.classList.remove('old-style'); // Ta bort klass
element.classList.toggle('active'); // Växla klass
const hasClass = element.classList.contains('highlight');
// Skapa och lägga till element
const newDiv = document.createElement('div');
newDiv.textContent = "Nytt element";
document.body.appendChild(newDiv);
Best practice: Använd classList
för stiländringar istället för style
.
Fråga 10: Event Handling
Fråga: "Förklara hur händelsehantering fungerar i JavaScript. Skriv kod som hanterar en knappklickning och visa vad event-objektet innehåller."
Förslag till svar:
// HTML: <button id="myButton">Klicka mig</button>
const button = document.querySelector('#myButton');
// Modern metod (rekommenderas)
button.addEventListener('click', handleClick);
function handleClick(event) {
console.log('Knapp klickad!');
// Event-objektet innehåller:
console.log('Event type:', event.type); // 'click'
console.log('Target element:', event.target); // Knappen som klickades
console.log('Mouse position:', event.clientX, event.clientY);
// Förhindra standardbeteende (t.ex. för formulär)
event.preventDefault();
// Ändra knappens utseende
event.target.textContent = 'Klickad!';
event.target.classList.add('clicked');
}
// Alternativ: arrow function direkt
button.addEventListener('click', (event) => {
console.log('Klick med arrow function!');
});
Vanliga events: click
, submit
, input
, mouseover
, keydown
, load
Viktigt: Använd addEventListener
istället för onclick
-attribut för separation of concerns.
Fråga 11: Formulärhantering och Validering
Fråga: "Hur hanterar du formulärinskickning med JavaScript? Visa hur du förhindrar standardbeteendet och validerar input."
Förslag till svar:
// HTML:
<form id="myForm">
<input type="email" id="email" required>
<button type="submit">Skicka</button>
</form>
const form = document.querySelector('#myForm');
const emailInput = document.querySelector('#email');
form.addEventListener('submit', handleSubmit);
function handleSubmit(event) {
event.preventDefault(); // Stoppa normal formulärinskickning
// Hämta värden
const email = emailInput.value.trim();
// Enkel validering
if (!email) {
showError('E-post krävs');
return;
}
if (!isValidEmail(email)) {
showError('Ogiltig e-postadress');
return;
}
// Skicka data (t.ex. till server)
console.log('Skickar:', { email });
showSuccess('Formulär skickat!');
}
function isValidEmail(email) {
return email.includes('@') && email.includes('.');
}
function showError(message) {
console.error(message);
// Visa felmeddelande i UI
}
function showSuccess(message) {
console.log(message);
// Visa framgångsmeddelande i UI
}
Viktigt: Alltid validera på server också - klientvalidering är bara för användarupplevelse.
Fråga 12: Felsökning och Debug-tekniker
Fråga: "Vilka verktyg och tekniker använder du för att felsöka JavaScript-kod? Visa praktiska exempel."
Förslag till svar: Felsökningsverktyg:
- Console.log() - Grundläggande utskrifter
const data = { name: "Alice", age: 25 };
console.log('Data:', data);
console.log('Endast namnet:', data.name);
// Andra console-metoder
console.error('Detta är ett fel');
console.warn('Detta är en varning');
console.table(data); // Visa objekt som tabell
- Browser Developer Tools
// Sätt breakpoints i koden
function calculateTotal(price, tax) {
debugger; // Pausar exekvering här
const total = price + (price * tax);
return total;
}
- Defensive Programming
function processUser(user) {
// Kontrollera input
if (!user) {
console.error('User is null or undefined');
return;
}
if (typeof user.age !== 'number') {
console.warn('User age is not a number:', user.age);
}
// Fortsätt med logik...
}
- Try-Catch för felhantering
try {
const result = riskyOperation();
console.log('Success:', result);
} catch (error) {
console.error('Something went wrong:', error.message);
}
Best practices: Använd console.log strategiskt, lär dig utvecklarverktygen, skriv defensiv kod.
Tips för Tekniska Intervjuer
- Skriv kod på whiteboard/papper - träna på att skriva JavaScript utan editor
- Förklara din tankeprocess - prata igenom vad du gör steg för steg
- Testa din kod mentalt - gå igenom koden rad för rad
- Fråga om krav - klargör vad som förväntas innan du börjar koda
- Börja enkelt - löss grundproblemet först, lägg till features sedan
- Hantera edge cases - diskutera vad som händer med oväntad input
Tekniska intervjufrågor: CSS och responsiv design
Detta avsnitt innehåller exempel på tekniska intervjufrågor som kan dyka upp gällande CSS och responsiv webbutveckling. Frågorna är utformade för att testa både teoretisk förståelse och praktisk kunskap.
Använd dessa frågor för att testa din kunskap och förbereda dig för tekniska intervjuer.
Fråga 1: CSS grundsyntax och inkludering
Fråga:
Förklara skillnaden mellan inline CSS, intern CSS och extern CSS. Vilken metod rekommenderas och varför?
Svar:
- Inline CSS: Skrivs direkt i HTML-elementets
style
-attribut, t.ex.<p style="color: red;">Text</p>
. - Intern CSS: Skrivs i en
<style>
-tagg i HTML-dokumentets<head>
. - Extern CSS: Skrivs i en separat
.css
-fil som länkas in med<link rel="stylesheet" href="style.css">
.
Extern CSS rekommenderas eftersom det separerar struktur och utseende, gör det lättare att återanvända och underhålla stilar, samt förbättrar prestanda genom cache.
Fråga 2: CSS-selektorer
Fråga:
Förklara skillnaden mellan dessa selektorer och ge exempel på när du skulle använda var och en:
p { }
.highlight { }
#header { }
nav ul li { }
Svar:
p
– Elementselektor: Väljer alla<p>
-element. Används för grundläggande styling av paragrafer..highlight
– Klassselektor: Väljer alla element med klassenhighlight
. Används för återanvändbara stilar.#header
– ID-selektor: Väljer elementet med idheader
. Används för unika element.nav ul li
– Ättlingselektor: Väljer alla<li>
-element inuti en<ul>
som ligger i en<nav>
. Används för att styla specifika strukturer.
Fråga 3: Specificitet och kaskaden
Fråga:
Vilken färg får texten i detta exempel och varför?
<p id="viktigt" class="highlight">Text</p>
p { color: black; }
.highlight { color: blue; }
#viktigt { color: red; }
p.highlight { color: green; }
Svar:
Texten blir röd eftersom ID-selektorn #viktigt
har högst specificitet och därför vinner över de andra reglerna.
Fråga 4: Box model
Fråga:
Förklara CSS box model. Om ett element har width: 200px
, padding: 20px
, border: 5px solid black
och margin: 10px
, vad blir elementets totala bredd?
Svar:
Box model består av content, padding, border och margin.
Total bredd = width + (padding × 2) + (border × 2) = 200 + 40 + 10 = 250px (margin räknas utanför detta).
Fråga 5: Display-egenskaper
Fråga:
Vad är skillnaden mellan display: block
, display: inline
och display: inline-block
?
Svar:
block
: Elementet tar hela raden, respekterar width/height, börjar på ny rad.inline
: Elementet tar bara så mycket plats som behövs, ignorerar width/height, bryter inte rad.inline-block
: Som inline, men respekterar width/height.
Fråga 6: CSS-positionering
Fråga:
Förklara skillnaden mellan position: relative
, position: absolute
och position: fixed
. Ge exempel på användningsfall.
Svar:
relative
: Elementet flyttas relativt sin ursprungliga plats, påverkar inte andra element.absolute
: Positioneras relativt närmaste positionerade förälder, tas ur flödet.fixed
: Positioneras relativt till fönstret, stannar kvar vid scroll.
Fråga 7: Färger och typografi
Fråga:
Förklara skillnaden mellan dessa färgformat och när du skulle använda dem:
color: red;
color: #FF0000;
color: rgb(255, 0, 0);
color: rgba(255, 0, 0, 0.5);
Svar:
red
: Namngiven färg, enkel men begränsad.#FF0000
: HEX, vanligt för webben.rgb(255, 0, 0)
: RGB, bra för exakta färger.rgba(255, 0, 0, 0.5)
: RGB med transparens (alpha).
Fråga 8: Responsiv design – Media Queries
Fråga:
Vad är skillnaden mellan dessa två Media Queries och vilken strategi representerar de?
@media (max-width: 768px) { ... }
@media (min-width: 768px) { ... }
Svar:
max-width
: Desktop-first, ändrar stilar för mindre skärmar.min-width
: Mobile-first, lägger till/förbättrar stilar för större skärmar.
Mobile-first rekommenderas idag.
Fråga 9: Mobile-first design
Fråga:
Varför rekommenderas mobile-first design och hur implementerar du det i CSS?
Svar:
Mobile-first prioriterar prestanda och tillgänglighet för mobila användare.
Implementeras genom att skriva grundstilar utan media queries och lägga till förbättringar med @media (min-width: ...)
.
Fråga 10: Flexibla enheter
Fråga:
Förklara skillnaden mellan px
, em
, rem
och %
. När skulle du använda respektive enhet?
Svar:
px
: Absolut, exakt storlek.em
: Relativ till förälderns font-size.rem
: Relativ till root-elementets font-size.%
: Relativ till förälderns storlek.
Användrem
för typografi,%
för layouter,px
för detaljer.
Fråga 11: Flexbox layout
Fråga:
Förklara hur Flexbox fungerar. Vad är skillnaden mellan justify-content
och align-items
? Ge ett exempel.
Svar:
Flexbox är för endimensionella layouter.
justify-content
: Justerar längs huvudaxeln (t.ex. horisontellt).align-items
: Justerar längs tväraxeln (t.ex. vertikalt).
Exempel:
.container {
display: flex;
justify-content: center;
align-items: center;
}
Fråga 12: CSS Grid layout
Fråga:
Vad är skillnaden mellan CSS Grid och Flexbox? Visa hur du skapar en enkel 3-kolumns layout med Grid.
Svar:
Grid är tvådimensionellt (rader och kolumner), Flexbox är endimensionellt.
Exempel:
.container {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
}
Tips för tekniska intervjuer
- Rita gärna box model eller layout-flöde.
- Förklara varför du väljer en viss lösning.
- Ge exempel från egna projekt.
- Visa att du kan felsöka och resonera kring CSS-problem.
Kapitel 5: Fortsättning JavaScript - Asynkron Kod och Datahantering
I föregående kapitel lade vi grunden för JavaScript genom att manipulera DOM, hantera händelser och skapa enkel interaktivitet. Nu är det dags att dyka djupare in i mer avancerade, men helt nödvändiga, koncept för modern webbutveckling.
Varför detta kapitel är viktigt:
Webbapplikationer behöver ofta kommunicera med servrar för att hämta eller skicka data utan att hela sidan laddas om. De behöver också hantera operationer som kan ta tid (t.ex. vänta på svar från en server) utan att webbläsaren "fryser". Detta kräver förståelse för asynkron programmering.
Vi kommer också att arbeta mycket med datalistor och behöver effektiva sätt att transformera och filtrera dessa. Dessutom behöver vi ett standardiserat sätt att utbyta data mellan klient (webbläsare) och server.
Vad vi kommer att gå igenom:
- Asynkron JavaScript: Förstå hur JavaScript hanterar tidskrävande operationer med händelseloopen (event loop) och återanrop (callbacks).
- Promises: Ett modernare och mer robust sätt att hantera asynkrona operationer och undvika "callback hell".
- Fetch API: Standardmetoden i moderna webbläsare för att göra nätverksanrop (hämta data från API:er).
- JSON (JavaScript Object Notation): Det vanligaste dataformatet för datautbyte på webben.
- Avancerade Array-metoder: Kraftfulla inbyggda metoder som
map
,filter
, ochreduce
för att arbeta med arrayer på ett funktionellt sätt.
Detta kapitel bygger vidare på dina kunskaper och ger dig verktygen för att skapa mer dynamiska och datadrivna webbapplikationer.
Asynkron Programmering i JavaScript
I grund och botten är JavaScript ett single-threaded (entrådat) språk. Det betyder att det bara kan göra en sak i taget, steg för steg, i den ordning koden är skriven. Tänk dig en kock som bara kan hacka en grönsak åt gången innan hen går vidare till nästa.
Varför är detta viktigt?
Om JavaScript bara kan göra en sak i taget, vad händer när en uppgift tar lång tid? Till exempel:
- Vänta på svar från en server (nätverksanrop).
- Läsa en stor fil från hårddisken.
- Komplexa beräkningar.
- Vänta en viss tid (t.ex. med
setTimeout
).
I ett strikt synkront flöde skulle hela programmet (och ofta hela webbläsarens flik!) stanna upp och vänta tills den tidskrävande uppgiften är klar. Detta kallas blockering (blocking) och leder till en dålig användarupplevelse – sidan fryster och blir oresponsiv.
// Synkront exempel (simulerat)
console.log("Startar uppgift...");
// Simulerar en tidskrävande operation (blockerar i 5 sekunder)
const start = Date.now();
while (Date.now() < start + 5000) {
// Gör ingenting, bara väntar...
}
console.log("Uppgiften klar!"); // Denna loggas först efter 5 sekunder
console.log("Annan kod körs..."); // Denna måste också vänta
Under de 5 sekunderna i exemplet ovan skulle webbsidan vara helt fryst.
Den Asynkrona Lösningen: Callback-funktioner
För att undvika blockering använder JavaScript (och dess körmiljöer som webbläsare och Node.js) en asynkron modell för tidskrävande operationer. Istället för att vänta, säger JavaScript: "Starta den här uppgiften, och när den är klar, kör den här specifika funktionen".
Denna specifika funktion som skickas med för att köras senare kallas en callback-funktion (återanropsfunktion).
console.log("Startar timer...");
// setTimeout är en asynkron funktion
setTimeout(() => {
// Detta är callback-funktionen
console.log("Timer klar efter 2 sekunder!");
}, 2000); // 2000 millisekunder = 2 sekunder
console.log("Timer startad, fortsätter med annan kod...");
// Output:
// Startar timer...
// Timer startad, fortsätter med annan kod...
// (efter 2 sekunder)
// Timer klar efter 2 sekunder!
Notera hur setTimeout
inte blockerar. Koden fortsätter direkt till nästa console.log
, och callback-funktionen körs först när timern har gått ut.
Händelseloopen (The Event Loop)
Men hur fungerar detta bakom kulisserna? Hur kan JavaScript, som är entrådat, hantera saker som händer "samtidigt"?
Nyckeln är händelseloopen (event loop) och samarbete mellan JavaScript-motorn och dess körmiljö (t.ex. webbläsaren).
- Call Stack (Anropsstacken): Här exekveras den vanliga, synkrona JavaScript-koden. Funktioner läggs på stacken när de anropas och tas bort när de är klara.
- Web APIs (Webbläsar-API:er): När JavaScript stöter på en asynkron operation (som
setTimeout
,fetch
, DOM-händelser), skickas den vidare till ett webbläsar-API som hanterar den utanför JavaScripts huvudtråd. - Callback Queue (Callback-kön) / Task Queue: När webbläsar-API:et är klart med sin uppgift (t.ex. timern har gått ut, data har hämtats), läggs den tillhörande callback-funktionen i en kö.
- Event Loop (Händelseloopen): Detta är en process som ständigt övervakar två saker: Är anropsstacken (Call Stack) tom? Finns det något i callback-kön?
- Exekvering: Om anropsstacken är tom och det finns en callback i kön, plockar händelseloopen den första callback-funktionen från kön och lägger den på anropsstacken för att köras.
graph TD subgraph JavaScript Engine CS[Call Stack] end subgraph Browser/Node APIs WA[Web APIs (setTimeout, fetch, DOM etc.)] end subgraph Task Queues CQ[Callback Queue (Tasks)] MQ[Microtask Queue (Promises etc.)] -- Högre prioritet --> EL end EL{Event Loop} -- Checks if Call Stack is empty --> CS CS -- Runs sync code --> CS CS -- Calls async func --> WA WA -- Callback ready --> CQ WA -- Promise resolved/rejected --> MQ EL -- Checks queues --> CQ EL -- Checks queues --> MQ MQ -- Has task & CS empty --> EL CQ -- Has task & CS empty & MQ empty --> EL EL -- Moves callback --> CS style WA fill:#f9d,stroke:#333,stroke-width:2px style CS fill:#ccf,stroke:#333,stroke-width:2px style CQ fill:#dfd,stroke:#333,stroke-width:2px style MQ fill:#ffe,stroke:#333,stroke-width:2px style EL fill:#eee,stroke:#333,stroke-width:4px
Diagrammet ovan illustrerar flödet: Synkron kod körs på Call Stack. Asynkrona operationer skickas till Web APIs. När de är klara hamnar deras callbacks i Callback Queue (för t.ex. setTimeout
) eller Microtask Queue (för t.ex. Promises - dessa har högre prioritet). Event Loop kontrollerar kontinuerligt om Call Stack är tom och flyttar sedan uppgifter från köerna (Microtask först) till Call Stack för exekvering.
Analogi: Tänk dig en restaurangkock (JavaScript Call Stack) som bara kan laga en rätt i taget. När en gäst beställer något som tar lång tid (t.ex. en långkokt gryta - en asynkron operation), tar en servitör (Web API) beställningen och sätter igång grytan på en annan spis. Kocken fortsätter med andra snabba beställningar. När grytan är klar, säger servitören till via en bong (Callback läggs i kön). När kocken är klar med det hen höll på med (Call Stack är tom), tittar hen på bongarna (Event Loop kollar kön) och tar nästa färdiga rätt (Callback flyttas till Call Stack) för att lägga upp den.
Detta system gör att JavaScript kan hantera många operationer utan att blockera huvudtråden, vilket håller webbsidan responsiv.
Callback Hell
Callbacks är grundläggande, men när man har flera asynkrona operationer som beror på varandra kan det leda till djupt nästlad kod, ofta kallat "Callback Hell" eller "Pyramid of Doom".
// Exempel på Callback Hell (konceptuellt)
operation1((resultat1) => {
console.log("Klar med 1");
operation2(resultat1, (resultat2) => {
console.log("Klar med 2");
operation3(resultat2, (resultat3) => {
console.log("Klar med 3");
// ...och så vidare...
});
});
});
Denna kod blir svårläst och svår att underhålla. Detta är ett av huvudproblemen som Promises, vilka vi tittar på i nästa avsnitt, löser.
Promises och Async/Await
Som vi såg i föregående avsnitt kan callbacks leda till rörig kod, speciellt när flera asynkrona operationer måste utföras i sekvens ("Callback Hell"). För att lösa detta introducerades Promises (Löften) i JavaScript.
Vad är ett Promise?
Ett Promise representerar det framtida resultatet av en asynkron operation. Det är ett objekt som fungerar som en platshållare för ett värde som ännu inte är känt, men som lovar att leverera ett värde (eller ett fel) vid någon tidpunkt i framtiden.
Tänk på det som att beställa en bok online. Du får en orderbekräftelse (ett Promise) direkt. Boken (värdet) har inte kommit än, men orderbekräftelsen lovar att du antingen kommer att få boken (Promise fulfilled) eller ett meddelande om att något gick fel, t.ex. att boken var slutsåld (Promise rejected).
Promise-livscykel
Ett Promise kan befinna sig i ett av tre tillstånd:
- Pending (Väntande): Det initiala tillståndet. Operationen har startat men är ännu inte slutförd eller misslyckad.
- Fulfilled (Uppfylld): Operationen slutfördes framgångsrikt. Promiset har nu ett resulterande värde.
- Rejected (Avvisad): Operationen misslyckades. Promiset har nu en anledning (ett felobjekt) till varför det misslyckades.
Ett Promise kan bara övergå från pending till antingen fulfilled eller rejected, och när det väl har hänt ändras dess tillstånd aldrig igen. Man säger att Promiset är settled (avgjort).
Att Använda Promises: .then()
och .catch()
För att hantera resultatet (eller felet) av ett Promise använder vi metoderna .then()
och .catch()
.
.then(onFulfilled, onRejected)
: Tar emot upp till två argument, båda funktioner:onFulfilled
: Körs om Promiset blir fulfilled. Den tar emot det resulterande värdet som argument.onRejected
: Körs om Promiset blir rejected. Den tar emot felorsaken som argument. (Det är vanligare att använda.catch()
för detta).
.catch(onRejected)
: Tar emot en funktion som körs om Promiset blir rejected. Det är ett mer läsbart sätt att hantera fel än att skicka en andra funktion till.then()
.
// Simulerar en asynkron operation som returnerar ett Promise
function fetchData() {
return new Promise((resolve, reject) => {
// Simulera nätverksfördröjning
setTimeout(() => {
const success = Math.random() > 0.3; // 70% chans att lyckas
if (success) {
resolve({ data: "Hämtad data!" }); // Uppfyll Promiset med data
} else {
reject(new Error("Kunde inte hämta data.")); // Avvisa Promiset med ett fel
}
}, 1500);
});
}
console.log("Startar datahämtning...");
fetchData()
.then((result) => {
// Körs om Promiset blir fulfilled
console.log("Success:", result.data);
})
.catch((error) => {
// Körs om Promiset blir rejected
console.error("Error:", error.message);
})
.finally(() => {
// Körs alltid, oavsett om det lyckades eller misslyckades
console.log("Datahämtning avslutad (fulfilled eller rejected).");
});
console.log("Promise startat, väntar på resultat...");
// Output (exempel vid success):
// Startar datahämtning...
// Promise startat, väntar på resultat...
// (efter 1.5 sekunder)
// Success: Hämtad data!
// Datahämtning avslutad (fulfilled eller rejected).
// Output (exempel vid error):
// Startar datahämtning...
// Promise startat, väntar på resultat...
// (efter 1.5 sekunder)
// Error: Kunde inte hämta data.
// Datahämtning avslutad (fulfilled eller rejected).
Notera .finally()
som körs oavsett utfall, användbart för t.ex. städning eller att dölja en laddningsindikator.
Kedjade Promises (Chaining)
Styrkan med .then()
är att den också returnerar ett Promise. Detta gör att vi kan kedja flera asynkrona operationer efter varandra på ett läsbart sätt, utan djup nästling.
fetchData() // Returnerar ett Promise
.then(result1 => {
console.log("Steg 1 klart:", result1.data);
// Starta nästa operation, som också returnerar ett Promise
return anotherAsyncOperation(result1.data);
})
.then(result2 => {
console.log("Steg 2 klart:", result2);
// ...och så vidare...
})
.catch(error => {
// Ett enda catch hanterar fel från *alla* föregående steg i kedjan
console.error("Ett fel inträffade i kedjan:", error.message);
});
Async/Await: Syntaktiskt Socker
Även om kedjade Promises är mycket bättre än Callback Hell, kan koden fortfarande bli lite svårläst med många .then()
. ES2017 introducerade async
och await
för att skriva asynkron kod som ser ut som synkron kod.
async function
: När du deklarerar en funktion medasync
framför, händer två saker:- Funktionen returnerar alltid automatiskt ett Promise. Om funktionen returnerar ett värde, blir det värdet resultatet av ett fulfilled Promise. Om funktionen kastar ett fel, blir det felet anledningen till ett rejected Promise.
- Det möjliggör användningen av
await
inuti funktionen.
await
: Kan endast användas inuti enasync function
. När du sätterawait
framför ett anrop som returnerar ett Promise, pausas exekveringen avasync
-funktionen (utan att blockera huvudtråden!) tills det Promiset är settled (avgjort).- Om Promiset blir fulfilled, returnerar
await
-uttrycket det uppfyllda värdet. - Om Promiset blir rejected, kastar
await
-uttrycket felet (som kan fångas medtry...catch
).
- Om Promiset blir fulfilled, returnerar
// Samma fetchData som tidigare
// async funktion för att använda await
async function processData() {
console.log("Startar processData (async)...");
try {
// Pausar här tills fetchData() är fulfilled eller rejected
const result = await fetchData();
console.log("Data hämtad (inom async):");
// Kan nu använda resultatet som om det vore synkront
const processedData = result.data.toUpperCase();
console.log("Bearbetad data:", processedData);
// Om vi hade fler await-anrop skulle de köras sekventiellt
// const result2 = await anotherAsyncOperation(processedData);
// console.log("Resultat från andra operationen:", result2);
return processedData; // Detta blir värdet för Promiset som processData returnerar
} catch (error) {
// Fångar fel från await fetchData() eller andra fel i try-blocket
console.error("Fel i processData:", error.message);
// Kan välja att kasta felet vidare eller returnera ett standardvärde
throw error; // Kasta om felet så att anroparen kan fånga det
}
}
console.log("Anropar processData...");
// Eftersom processData är async, returnerar den ett Promise
processData()
.then(finalResult => {
console.log("processData lyckades med resultat:", finalResult);
})
.catch(error => {
console.error("processData misslyckades:", error.message);
});
console.log("processData anropad, väntar...");
Fördelar med Async/Await:
- Läslighet: Koden ser mycket mer ut som traditionell, synkron kod.
- Felhantering: Använder standard
try...catch
, vilket många utvecklare är vana vid. - Enklare felsökning: Lättare att följa kodflödet och sätta brytpunkter.
async/await
är idag det vanligaste sättet att hantera Promises i modern JavaScript-utveckling.
Hämta Data med Fetch API
Nu när vi förstår Promises och async/await
, kan vi titta på det moderna sättet att göra nätverksanrop (network requests) i JavaScript: Fetch API.
Fetch API är ett inbyggt gränssnitt i webbläsaren (och Node.js via externa bibliotek) som ger ett kraftfullt och flexibelt sätt att interagera med resurser över nätverket, oftast genom att hämta data från eller skicka data till ett webb-API (Application Programming Interface).
Varför Fetch?
- Modernt och Standard: Det är det rekommenderade sättet att göra AJAX (Asynchronous JavaScript and XML) -liknande anrop idag, och ersätter den äldre
XMLHttpRequest
. - Promise-baserat: Det använder Promises naturligt, vilket gör det lätt att integrera med
async/await
och.then()
/.catch()
. - Flexibelt: Ger finmaskig kontroll över request (förfrågan) och response (svar).
Grundläggande Användning: GET-Request
Den vanligaste användningen är att hämta data med en HTTP GET-metod. Detta görs enkelt genom att skicka URL:en till resursen som argument till fetch()
.
// URL till ett publikt API (JSONPlaceholder - simulerar en blogg)
const apiUrl = 'https://jsonplaceholder.typicode.com/posts/1';
console.log("Startar fetch-anrop...");
fetch(apiUrl)
.then(response => {
// Steg 1: Hantera HTTP-svaret
console.log("Mottagit response:", response.status, response.statusText);
// fetch() kastar inte automatiskt fel vid HTTP-fel (som 404 eller 500)
// Vi måste kontrollera response.ok (true om status är 200-299)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// Om svaret är OK, behöver vi konvertera body till det format vi vill ha,
// oftast JSON. response.json() returnerar *också* ett Promise!
return response.json();
})
.then(data => {
// Steg 2: Hantera den parsade datan (nu ett JavaScript-objekt)
console.log("Mottagen data:", data);
// Gör något med datan, t.ex. visa den på sidan
document.body.innerHTML += `<p>Titel: ${data.title}</p>`;
})
.catch(error => {
// Hantera fel från nätverket eller från throw new Error ovan
console.error("Fel vid fetch:", error);
document.body.innerHTML += `<p style="color: red;">Kunde inte hämta data: ${error.message}</p>`;
});
console.log("Fetch-anrop startat, väntar...");
Viktiga Steg:
- Anropa
fetch(url)
. Detta returnerar ett Promise som blir fulfilled så snart webbläsaren får tillbaka headers från servern. - I första
.then()
, kontrolleraresponse.ok
. Om inte OK, kasta ett fel (throw new Error(...)
) så att det fångas av.catch()
. - Om OK, använd en metod på
response
-objektet för att läsa body (själva datan). De vanligaste är:response.json()
: Tolkar body som JSON och returnerar ett Promise som blir fulfilled med det parsade JavaScript-objektet.response.text()
: Returnerar ett Promise som blir fulfilled med body som en textsträng.response.blob()
: För binärdata (t.ex. bilder).response.formData()
: För formulärdata.
- I nästa
.then()
hanterar du den faktiska datan (t.ex. det parsade JSON-objektet). - Använd
.catch()
för att hantera eventuella nätverksfel (t.ex. ingen anslutning) eller de fel du själv kastade (t.ex. vidresponse.ok === false
).
Använda med Async/Await
Fetch blir ännu smidigare med async/await
:
async function getPost() {
const apiUrl = 'https://jsonplaceholder.typicode.com/posts/1';
console.log("Startar async fetch...");
try {
const response = await fetch(apiUrl);
console.log("Mottagit response (async):", response.status, response.statusText);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// Vänta på att body ska läsas och parsas som JSON
const data = await response.json();
console.log("Mottagen data (async):", data);
document.body.innerHTML += `<p>Titel (async): ${data.title}</p>`;
} catch (error) {
console.error("Fel vid async fetch:", error);
document.body.innerHTML += `<p style="color: red;">Kunde inte hämta data (async): ${error.message}</p>`;
}
}
getPost();
console.log("Async fetch startad, väntar...");
Mer Avancerad Fetch: Options
fetch()
kan ta ett andra argument, ett options-objekt, för att anpassa förfrågan. Detta är nödvändigt för andra HTTP-metoder (som POST, PUT, DELETE) eller för att skicka med data och headers.
async function createPost(newPostData) {
const apiUrl = 'https://jsonplaceholder.typicode.com/posts';
try {
const response = await fetch(apiUrl, {
method: 'POST', // Ange HTTP-metod
headers: {
'Content-Type': 'application/json' // Tala om att vi skickar JSON
// Lägg till andra headers här om det behövs (t.ex. Authorization)
},
body: JSON.stringify(newPostData) // Konvertera JS-objekt till JSON-sträng
});
if (!response.ok) {
// För POST kan man vilja läsa felmeddelande från servern
const errorData = await response.json().catch(() => ({})); // Försök läsa fel, annars tomt objekt
throw new Error(`HTTP error! status: ${response.status} - ${errorData.message || response.statusText}`);
}
// För POST (status 201 Created), får vi oftast tillbaka det skapade objektet
const createdPost = await response.json();
console.log('Skapad post:', createdPost);
return createdPost;
} catch (error) {
console.error('Fel vid POST-request:', error);
// Hantera felet, visa meddelande för användaren etc.
throw error; // Kasta vidare om anroparen behöver veta
}
}
// Exempel på användning
const myNewPost = {
title: 'Min Nya Bloggpost',
body: 'Detta är innehållet.',
userId: 1
};
createPost(myNewPost)
.then(post => console.log(`Post med id ${post.id} skapades.`))
.catch(err => console.log("Kunde inte skapa post."));
Vanliga Options:
method
: HTTP-metod ('GET', 'POST', 'PUT', 'DELETE', 'PATCH', etc.). Standard är 'GET'.headers
: Ett objekt med HTTP-headers (nyckel-värde-par) att skicka med förfrågan.'Content-Type': 'application/json'
är mycket vanlig när man skickar JSON-data.body
: Datan som ska skickas med förfrågan (t.ex. för POST eller PUT). Måste oftast vara en sträng (användJSON.stringify()
för objekt) ellerFormData
,Blob
, etc.mode
: ('cors', 'no-cors', 'same-origin'). Styr hur Cross-Origin Resource Sharing (CORS) hanteras. 'cors' är standard och vanligast för API-anrop.credentials
: ('include', 'same-origin', 'omit'). Styr om cookies ska skickas med förfrågan.
Fetch API är ett kraftfullt verktyg för att bygga dynamiska webbapplikationer som interagerar med externa datakällor.
Dataformat: JSON
När vi hämtar data från ett API med fetch
, eller när vi vill skicka data till ett API, behöver vi ett standardiserat format för att representera den datan som text. Det absolut vanligaste formatet för detta på webben idag är JSON (JavaScript Object Notation).
Varför JSON?
- Läsbart: Det är relativt lätt för människor att läsa och skriva.
- Lättviktigt: Det är ett kompakt textformat, vilket är effektivt för dataöverföring.
- JavaScript-vänligt: Som namnet antyder är syntaxen väldigt lik JavaScripts objektsyntax, vilket gör det extremt enkelt att arbeta med i JavaScript.
Alternativet: XML
Tidigare var XML (eXtensible Markup Language) ett vanligt format för datautbyte. XML använder taggar, liknande HTML, för att strukturera data.
<!-- Exempel på data i XML-format -->
<post>
<userId>1</userId>
<id>1</id>
<title>Detta är titeln</title>
<body>Detta är innehållet.</body>
</post>
Även om du kan stöta på XML i vissa äldre system eller specifika branscher, är JSON det dominerande formatet för webb-API:er idag. Att parsa (tolka) XML i JavaScript kräver oftast separata bibliotek eller mer komplex kod än att hantera JSON.
JSON Syntax
JSON-data representeras som text och följer några enkla regler:
- Objekt: Representeras med krullparenteser
{}
. Innehåller nyckel-värde-par.- Nycklar måste vara strängar inom dubbla citationstecken (
"key"
). - Värden kan vara strängar, nummer, booleans (
true
/false
),null
, arrayer, eller andra JSON-objekt. - Nyckel och värde separeras av ett kolon
:
. Paren separeras av kommatecken,
.
- Nycklar måste vara strängar inom dubbla citationstecken (
- Arrayer: Representeras med hakparenteser
[]
. Innehåller en ordnad lista av värden, separerade av kommatecken. - Strängar: Måste använda dubbla citationstecken (
"text"
). - Nummer: Vanliga tal (heltal eller decimaltal).
- Booleans:
true
ellerfalse
(små bokstäver). - Null:
null
(små bokstäver).
Viktigt: JSON tillåter inte funktioner, undefined
, kommentarer, eller avslutande kommatecken (trailing commas).
// Exempel på JSON-data (som text)
{
"userId": 1,
"id": 1,
"title": "Exempeltitel",
"body": "Detta är innehållet i posten.",
"tags": ["webbutveckling", "javascript", "json"],
"metadata": {
"published": true,
"views": 1050,
"author": null
}
}
Arbeta med JSON i JavaScript
JavaScript har ett inbyggt globalt JSON
-objekt med två huvudsakliga metoder för att konvertera mellan JSON-text och JavaScript-objekt/-värden.
JSON.stringify()
- Från JavaScript till JSON-sträng
Denna metod tar ett JavaScript-värde (oftast ett objekt eller en array) och omvandlar det till en JSON-formaterad sträng. Detta används när du ska skicka data till ett API (t.ex. i body
för en fetch
POST-request).
const postData = {
title: "Min Nya Bloggpost",
body: "Detta är innehållet.",
userId: 1,
published: false, // JavaScript boolean
tags: ['ny', 'test'] // JavaScript array
};
// Konvertera till JSON-sträng
const jsonString = JSON.stringify(postData);
console.log(jsonString);
// Output: "{"title":"Min Nya Bloggpost","body":"Detta är innehållet.","userId":1,"published":false,"tags":["ny","test"]}"
// Snyggare formatering (indentering)
const prettyJsonString = JSON.stringify(postData, null, 2); // Använd 2 mellanslag för indentering
console.log("\nSnygg JSON:");
console.log(prettyJsonString);
/* Output:
{
"title": "Min Nya Bloggpost",
"body": "Detta är innehållet.",
"userId": 1,
"published": false,
"tags": [
"ny",
"test"
]
}
*/
// Vad händer med otillåtna värden?
const complexObject = {
name: "Test",
id: 123,
action: function() { console.log('Hej!'); }, // Funktioner ignoreras
value: undefined // undefined blir ignorerat (eller null i arrayer)
};
console.log("\nKomplext objekt:", JSON.stringify(complexObject));
// Output: "{"name":"Test","id":123}"
JSON.parse()
- Från JSON-sträng till JavaScript
Denna metod tar en JSON-formaterad sträng och omvandlar den tillbaka till ett motsvarande JavaScript-värde (objekt, array, sträng, nummer, boolean, null). Detta används när du har tagit emot data från ett API (t.ex. resultatet från response.json()
i fetch
).
const receivedJsonString = '{\n "postId": 101,\n "title": "Data från servern",\n "tags": ["api", "data"],\n "isPublic": true\n}';
try {
// Försök att parsa JSON-strängen
const jsObject = JSON.parse(receivedJsonString);
console.log("Parsat objekt:", jsObject);
console.log("Titel:", jsObject.title); // "Data från servern"
console.log("Första taggen:", jsObject.tags[0]); // "api"
console.log("Är publik:", jsObject.isPublic); // true
} catch (error) {
// Felhantering om JSON-strängen är ogiltig
console.error("Kunde inte parsa JSON:", error);
}
// Exempel med ogiltig JSON
const invalidJsonString = '{\n "name": "Test",\n "value": 123, // Ogiltigt: avslutande kommatecken
}';
try {
const invalidObject = JSON.parse(invalidJsonString);
console.log("Ogiltigt objekt:", invalidObject);
} catch (error) {
console.error("\nFel vid parsning av ogiltig JSON:", error.message);
// Output: Fel vid parsning av ogiltig JSON: Unexpected token } in JSON at position ...
}
Viktigt med Felhantering: Eftersom JSON-data ofta kommer från externa källor, kan den vara felaktigt formaterad. Det är därför viktigt att alltid omsluta JSON.parse()
i ett try...catch
-block för att hantera eventuella SyntaxError
som kan kastas om parsningen misslyckas.
Data Transformation och Validering
När du har parsat JSON-data till ett JavaScript-objekt, är det ofta bara det första steget. Därefter kan du behöva:
- Transformera datan: Ändra strukturen, filtrera bort onödig information, kombinera med annan data, etc. Här kommer ofta de avancerade array-metoderna (som vi ska titta på härnäst) till stor nytta.
- Validera datan: Kontrollera att datan du fick faktiskt innehåller de fält och datatyper du förväntar dig innan du använder den i din applikation. Detta skyddar mot oväntade fel om API:et ändras eller skickar oväntad data.
Att förstå JSON är fundamentalt för att kunna bygga webbapplikationer som kommunicerar med omvärlden.
Avancerade Array-Metoder och Objekt-Syntax
När vi arbetar med data, särskilt listor (arrayer) som vi ofta får tillbaka från API:er, behöver vi effektiva sätt att transformera, filtrera och sammanställa den datan. JavaScript erbjuder flera kraftfulla inbyggda array-metoder som hjälper oss med detta på ett deklarativt och läsbart sätt.
Vi kommer också att titta på modern syntax för att arbeta med objekt och arrayer, vilket gör koden mer koncis.
map()
- Transformera Element
map
är en inbyggd array-metod som skapar en ny array genom att utföra en angiven funktion på varje element i den ursprungliga arrayen. Den ursprungliga arrayen förblir oförändrad, medan den nya arrayen innehåller de transformerade värdena. map
är särskilt användbar när du vill bearbeta data och skapa en ny version av den utan att ändra originalet.
map
tar en callback-funktion som parameter, och denna funktion körs en gång för varje element. Callback-funktionen kan ta upp till tre argument:
element
: Värdet på det aktuella elementet som bearbetas.index
(valfri): Indexet för det aktuella elementet.array
(valfri): Den ursprungliga arrayen sommap
anropades på.
Det vanligaste är dock att bara använda det första argumentet (element
). Callback-funktionen måste returnera det nya värdet för elementet i den nya arrayen.
const numbers = [1, 2, 3, 4, 5];
// Skapa en ny array där varje nummer är dubblerat
const doubled = numbers.map(number => number * 2);
console.log(doubled); // Output: [2, 4, 6, 8, 10]
console.log(numbers); // Output: [1, 2, 3, 4, 5] (originalet är oförändrat)
const names = ["anna", "bertil", "cesar"];
// Skapa en ny array med namnen omvandlade till stor begynnelsebokstav
const capitalized = names.map(name => name.charAt(0).toUpperCase() + name.slice(1));
console.log(capitalized); // Output: ["Anna", "Bertil", "Cesar"]
const products = [
{ id: "p1", name: "Laptop", price: 12000 },
{ id: "p2", name: "Phone", price: 7500 },
{ id: "p3", name: "Tablet", price: 4000 }
];
// Skapa en ny array som bara innehåller produktpriserna
const prices = products.map(product => product.price);
console.log(prices); // Output: [12000, 7500, 4000]
// Skapa en ny array med produktobjekt som har priset inkl. moms (25%)
const productsWithVat = products.map(product => {
return {
...product, // Kopiera alla befintliga egenskaper (mer om '...' senare)
priceWithVat: product.price * 1.25
};
});
console.log(productsWithVat);
/* Output:
[
{ id: 'p1', name: 'Laptop', price: 12000, priceWithVat: 15000 },
{ id: 'p2', name: 'Phone', price: 7500, priceWithVat: 9375 },
{ id: 'p3', name: 'Tablet', price: 4000, priceWithVat: 5000 }
]
*/
filter()
- Välja ut Element
filter
är en annan array-metod som skapar en ny array. Denna nya array innehåller endast de element från den ursprungliga arrayen som uppfyller ett visst villkor. Den ursprungliga arrayen förblir oförändrad.
Precis som map
, tar filter
en callback-funktion som körs för varje element. Callback-funktionen tar samma argument (element
, index
, array
). Skillnaden är att callback-funktionen här måste returnera ett boolean-värde (true
eller false
).
- Om callback-funktionen returnerar
true
, inkluderas elementet i den nya arrayen. - Om callback-funktionen returnerar
false
, exkluderas elementet.
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Skapa en ny array med endast jämna nummer
const evenNumbers = numbers.filter(number => number % 2 === 0);
console.log(evenNumbers); // Output: [2, 4, 6, 8, 10]
console.log(numbers); // Output: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
const names = ["Anna", "Bertil", "Cesar", "David", "Eva"];
// Skapa en ny array med namn som är längre än 4 bokstäver
const longNames = names.filter(name => name.length > 4);
console.log(longNames); // Output: ["Bertil", "Cesar", "David"]
const products = [
{ id: "p1", name: "Laptop", price: 12000, inStock: true },
{ id: "p2", name: "Phone", price: 7500, inStock: false },
{ id: "p3", name: "Tablet", price: 4000, inStock: true },
{ id: "p4", name: "Mouse", price: 300, inStock: true }
];
// Skapa en ny array med produkter som finns i lager
const availableProducts = products.filter(product => product.inStock);
console.log(availableProducts);
/* Output:
[
{ id: 'p1', name: 'Laptop', price: 12000, inStock: true },
{ id: 'p3', name: 'Tablet', price: 4000, inStock: true },
{ id: 'p4', name: 'Mouse', price: 300, inStock: true }
]
*/
// Kombinera filter och map: Hämta namnen på dyra produkter (pris > 5000)
const expensiveProductNames = products
.filter(product => product.price > 5000) // Först filtrera
.map(product => product.name); // Sedan mappa resultatet
console.log(expensiveProductNames); // Output: ["Laptop", "Phone"]
reduce()
- Sammanställa till Ett Värde
reduce
är kanske den mest mångsidiga, men också den mest komplexa, av de vanliga array-metoderna. Den används för att "reducera" en array till ett enda värde genom att iterativt applicera en funktion på elementen. Detta enda värde kan vara ett nummer (t.ex. en summa), en sträng, ett objekt, eller till och med en annan array.
reduce
tar två argument:
- En reducer-funktion (callback). Denna funktion tar i sin tur (oftast) fyra argument:
accumulator
(ackumulator): Värdet som byggs upp och returneras från föregående iteration (ellerinitialValue
vid första iterationen).currentValue
(aktuellt värde): Värdet på det aktuella elementet som bearbetas.currentIndex
(valfri): Indexet för det aktuella elementet.array
(valfri): Den ursprungliga arrayen. Reducer-funktionen måste returnera det uppdaterade ackumulatorvärdet för nästa iteration.
- Ett
initialValue
(initialvärde, valfritt): Värdet som ackumulatorn ska starta med vid den första iterationen. Om detta utelämnas används det första elementet i arrayen som initialvärde, och iterationen börjar från det andra elementet.
const numbers = [1, 2, 3, 4, 5];
// Beräkna summan av alla nummer
const sum = numbers.reduce((accumulator, currentValue) => {
console.log(`Ack: ${accumulator}, Current: ${currentValue}`);
return accumulator + currentValue;
}, 0); // Startvärde för ackumulatorn är 0
console.log("Summa:", sum); // Output: 15
/* Logg från reduce:
Ack: 0, Current: 1
Ack: 1, Current: 2
Ack: 3, Current: 3
Ack: 6, Current: 4
Ack: 10, Current: 5
*/
// Hitta det högsta värdet
const max = numbers.reduce((maxSoFar, current) => {
return current > maxSoFar ? current : maxSoFar;
}); // Inget initialValue, första elementet (1) används som start
console.log("Max:", max); // Output: 5
const products = [
{ id: "p1", name: "Laptop", price: 12000 },
{ id: "p2", name: "Phone", price: 7500 },
{ id: "p3", name: "Tablet", price: 4000 }
];
// Beräkna totalt lagervärde
const totalValue = products.reduce((total, product) => total + product.price, 0);
console.log("Totalt värde:", totalValue); // Output: 23500
// Gruppera produkter efter prisklass (exempel på att returnera ett objekt)
const groupedByPrice = products.reduce((groups, product) => {
const key = product.price > 5000 ? 'expensive' : 'cheap';
if (!groups[key]) {
groups[key] = []; // Skapa arrayen om den inte finns
}
groups[key].push(product);
return groups; // Returnera det uppdaterade objektet
}, {}); // Startvärde är ett tomt objekt
console.log("Grupperade produkter:", groupedByPrice);
/* Output:
{
expensive: [
{ id: 'p1', name: 'Laptop', price: 12000 },
{ id: 'p2', name: 'Phone', price: 7500 }
],
cheap: [ { id: 'p3', name: 'Tablet', price: 4000 } ]
}
*/
Modern Objekt- och Array-Syntax
ES6 (ECMAScript 2015) och senare versioner av JavaScript har introducerat syntax som gör det smidigare att arbeta med objekt och arrayer.
Destructuring (Destrukturering)
Destructuring låter oss "packa upp" värden från arrayer eller egenskaper från objekt till separata variabler på ett koncis sätt.
// Array Destructuring
const coordinates = [10, 25, 5];
const [x, y, z] = coordinates;
console.log(x, y, z); // Output: 10 25 5
// Object Destructuring
const user = {
id: "u1",
name: "Alice",
email: "alice@example.com",
settings: { theme: "dark", notifications: true }
};
// Plocka ut egenskaper till variabler med samma namn
const { name, email } = user;
console.log(name, email); // Output: Alice alice@example.com
// Plocka ut och byt namn på variabeln
const { id: userId } = user;
console.log(userId); // Output: u1
// Plocka ut nästlade egenskaper
const { settings: { theme } } = user;
console.log(theme); // Output: dark
// Användbart i funktionsparametrar
function printUserName({ name }) {
console.log(`Användarnamn: ${name}`);
}
printUserName(user); // Output: Användarnamn: Alice
Spread Operator (...
)
Spread-operatorn låter oss "sprida ut" elementen från en array eller egenskaperna från ett objekt in i en ny array eller ett nytt objekt.
// Spread i Arrayer
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combinedArr = [...arr1, 0, ...arr2]; // Sprid ut elementen från arr1 och arr2
console.log(combinedArr); // Output: [1, 2, 3, 0, 4, 5, 6]
// Skapa en kopia av en array
const arr1Copy = [...arr1];
console.log(arr1Copy); // Output: [1, 2, 3]
arr1Copy.push(4);
console.log(arr1); // Output: [1, 2, 3] (originalet oförändrat)
// Spread i Objekt (ES2018+)
const obj1 = { a: 1, b: 2 };
const obj2 = { b: 3, c: 4 };
const combinedObj = { ...obj1, ...obj2 }; // Egenskaper från obj2 skriver över de från obj1 vid namnkonflikt (b)
console.log(combinedObj); // Output: { a: 1, b: 3, c: 4 }
// Skapa en kopia av ett objekt
const obj1Copy = { ...obj1 };
console.log(obj1Copy); // Output: { a: 1, b: 2 }
// Lägga till/uppdatera egenskaper på ett icke-muterande sätt (vanligt i React/Redux)
const updatedObj1 = { ...obj1, b: 100, d: 5 };
console.log(updatedObj1); // Output: { a: 1, b: 100, d: 5 }
console.log(obj1); // Output: { a: 1, b: 2 } (originalet oförändrat)
Rest Parameters (...
)
Rest-parametern ser likadan ut (...
) men används i funktionsdefinitioner för att samla ihop ett obestämt antal argument till en array.
// Samla alla argument till en array
function sumAll(...numbers) { // numbers blir en array
console.log(numbers); // T.ex. [1, 2, 3] eller [10, 20]
return numbers.reduce((sum, num) => sum + num, 0);
}
console.log(sumAll(1, 2, 3)); // Output: 6
console.log(sumAll(10, 20)); // Output: 30
// Kombinera vanliga parametrar med rest
function logMessage(level, ...messages) { // messages blir en array med resten
console.log(`[${level.toUpperCase()}]`, ...messages); // Kan sprida ut messages igen vid loggning
}
logMessage('info', 'Användare loggade in', 'ID: 123'); // Output: [INFO] Användare loggade in ID: 123
logMessage('error', 'Databasfel'); // Output: [ERROR] Databasfel
Dessa metoder och syntax-förbättringar (map
, filter
, reduce
, destructuring, spread/rest) är centrala i modern JavaScript och gör det möjligt att skriva mer uttrycksfull, koncis och ofta mer läsbar kod, särskilt när man hanterar datastrukturer.
Praktiska Övningar: Asynkron JavaScript och API-Anrop
Nu är det dags att praktisera de kraftfulla koncept vi har lärt oss i detta kapitel. Vi kommer att använda fetch
för att hämta data från det publika test-API:et JSONPlaceholder och sedan bearbeta och visa den datan med hjälp av array-metoder och DOM-manipulation.
Förutsättningar:
- En enkel HTML-fil (
index.html
) där du kan visa resultaten. - En länkad JavaScript-fil (
script.js
) där du skriver din kod. - Grundläggande kunskaper i DOM-manipulation (t.ex.
querySelector
,createElement
,textContent
,appendChild
) från föregående kapitel.
Tips: Använd async/await
för att hantera dina fetch
-anrop, då det ofta ger mer läsbar kod. Glöm inte try...catch
för felhantering!
Övning 1: Hämta och Visa Användare
Mål: Hämta en lista med användare från JSONPlaceholder och visa deras namn i en lista på webbsidan.
- Skapa HTML-struktur: Lägg till en
<ul>
-tagg i dinindex.html
med ett id, t.ex.id="user-list"
. - Hämta Användare (i
script.js
):- Skapa en
async function
, t.ex.fetchUsers()
. - Använd
fetch
för att göra ett GET-anrop tillhttps://jsonplaceholder.typicode.com/users
. - Hantera
response
-objektet: Kontrolleraresponse.ok
och användawait response.json()
för att få datan. - Använd
try...catch
för att fånga eventuella fel under hämtningen.
- Skapa en
- Visa Namnen:
- Inuti
fetchUsers()
(efter att du har fått datan): Hämta referensen till din<ul>
-tagg. - Loopa igenom arrayen med användardata (som du fick från API:et).
- För varje användare, skapa ett nytt
<li>
-element. - Sätt
<li>
-elementetstextContent
till användarensname
. - Lägg till (
appendChild
)<li>
-elementet i din<ul>
.
- Inuti
- Anropa Funktionen: Glöm inte att anropa din
fetchUsers()
-funktion så att koden körs när sidan laddas. - Testa: Öppna
index.html
i webbläsaren. Ser du en lista med 10 användarnamn?
Övning 2: Filtrera och Mappa Todos
Mål: Hämta en lista med "todos" (att-göra-uppgifter) från JSONPlaceholder. Filtrera listan så att du bara har de slutförda (completed: true
) uppgifterna. Visa titlarna på dessa slutförda uppgifter i en ny lista.
- Skapa HTML-struktur: Lägg till en ny
<ul>
-tagg, t.ex.id="completed-todos"
. - Hämta Todos (i
script.js
):- Skapa en ny
async function
, t.ex.fetchAndFilterTodos()
. - Använd
fetch
för att hämta data frånhttps://jsonplaceholder.typicode.com/todos
. - Hantera
response
och parsa JSON som i övning 1, inkluderatry...catch
.
- Skapa en ny
- Filtrera och Mappa:
- När du har fått todo-arrayen:
- Använd
filter()
-metoden för att skapa en ny array som endast innehåller de objekt där egenskapencompleted
ärtrue
. - Använd
map()
-metoden på den filtrerade arrayen för att skapa en ny array som endast innehållertitle
för varje slutförd todo.
- Använd
- När du har fått todo-arrayen:
- Visa Titlarna:
- Hämta referensen till din
<ul>
-tagg (#completed-todos
). - Loopa igenom arrayen med titlar.
- Skapa och lägg till
<li>
-element för varje titel i<ul>
.
- Hämta referensen till din
- Anropa Funktionen: Anropa
fetchAndFilterTodos()
. - Testa: Öppna
index.html
. Ser du en lista med titlarna på de slutförda uppgifterna?
Övning 3: Kombinera Data (Post och Kommentarer)
Mål: Hämta först en specifik bloggpost (t.ex. post med id 5). När du har hämtat posten, använd dess id
för att hämta alla kommentarer som hör till just den posten. Visa postens titel och sedan en lista med kommentarernas namn (namnet på den som kommenterat).
- Skapa HTML-struktur: Lägg till en
<div>
, t.ex.id="post-and-comments"
. - Hämta Data (i
script.js
):- Skapa en
async function
, t.ex.fetchPostAndComments(postId)
. - Steg 1: Hämta Posten:
- Använd
fetch
för att hämta data frånhttps://jsonplaceholder.typicode.com/posts/${postId}
(använd template literal för att inkluderapostId
). - Hantera
response
och parsa JSON. Spara post-objektet.
- Använd
- Steg 2: Hämta Kommentarer:
- Använd
fetch
igen, nu för att hämta data frånhttps://jsonplaceholder.typicode.com/posts/${postId}/comments
(ellerhttps://jsonplaceholder.typicode.com/comments?postId=${postId}
). - Hantera
response
och parsa JSON. Spara kommentars-arrayen.
- Använd
- Använd
try...catch
för att hantera fel från båda anropen.
- Skapa en
- Visa Resultatet:
- Hämta referensen till din
<div>
(#post-and-comments
). - Skapa och lägg till ett
<h2>
-element med postenstitle
. - Skapa en
<ul>
-lista för kommentarerna. - Loopa igenom kommentars-arrayen.
- För varje kommentar, skapa ett
<li>
-element med kommentarensname
och lägg till det i<ul>
. - Lägg till
<ul>
-listan i din<div>
.
- Hämta referensen till din
- Anropa Funktionen: Anropa
fetchPostAndComments(5)
(eller välj ett annat id). - Testa: Öppna
index.html
. Ser du postens titel följt av en lista med namn på de som kommenterat?
Övning 4: Felhantering med Fetch
Mål: Öva på att hantera olika typer av fel som kan uppstå vid fetch
-anrop.
- Försök hämta från en ogiltig URL:
- Skapa en
async function
. - Försök att
fetch
från en URL som inte finns, t.ex.https://jsonplaceholder.typicode.com/nonexistent-endpoint
. - Använd
try...catch
. Vad händer icatch
-blocket? Logga felet. - Kontrollera också
response.ok
inutitry
-blocket (även om du kanske inte kommer dit om URL:en är helt fel). Visa ett felmeddelande på sidan omresponse.ok
ärfalse
eller om ettcatch
-fel inträffar.
- Skapa en
- Försök parsa ogiltig JSON:
- Hämta data från en URL som inte returnerar JSON, t.ex.
https://google.com
(detta kan ge CORS-problem, testa annars med en lokal fil eller en URL du vet returnerar HTML). - Försök att anropa
await response.json()
på svaret. - Se till att ditt
try...catch
-block fångar felet som uppstår vid JSON-parsningen. Logga felet och visa ett användarvänligt meddelande på sidan.
- Hämta data från en URL som inte returnerar JSON, t.ex.
Övning 5 (Bonus): Aggregera Data med Reduce
Mål: Hämta alla "posts" från JSONPlaceholder och använd reduce
för att räkna hur många poster varje userId
har skrivit.
- Hämta Alla Poster:
- Skapa en
async function
. - Hämta data från
https://jsonplaceholder.typicode.com/posts
. - Hantera
response
och parsa JSON.
- Skapa en
- Räkna Poster per Användare:
- Använd
reduce()
-metoden på post-arrayen. - Ditt
initialValue
ska vara ett tomt objekt{}
. - I din reducer-funktion, för varje
post
:- Kontrollera om
post.userId
redan finns som en nyckel i ackumulator-objektet (accumulator
). - Om det finns, öka värdet (antalet) med 1.
- Om det inte finns, lägg till
post.userId
som en nyckel med värdet 1. - Returnera det uppdaterade ackumulator-objektet.
- Kontrollera om
- Använd
- Visa Resultatet:
- Logga det slutliga objektet som
reduce
returnerar. Det bör se ut ungefär så här:{ '1': 10, '2': 10, '3': 10, ... }
. - (Valfritt) Visa denna information på webbsidan på ett snyggt sätt.
- Logga det slutliga objektet som
Dessa övningar ger dig en bra grund för att arbeta med asynkron kod och externa API:er i dina framtida JavaScript-projekt!
Tekniska Intervjufrågor: Avancerad JavaScript och Asynkron Programmering
Detta avsnitt innehåller exempel på tekniska intervjufrågor som kan dyka upp gällande avancerad JavaScript, asynkron programmering och datahantering. Frågorna är utformade för att testa både teoretisk förståelse och praktisk kunskap.
Använd dessa frågor för att testa din kunskap och förbereda dig för tekniska intervjuer.
Fråga 1: Asynkron JavaScript och Event Loop
Fråga: "JavaScript är single-threaded. Förklara hur JavaScript kan hantera asynkrona operationer. Vad skrivs ut i följande kod och i vilken ordning?"
console.log("Start");
setTimeout(() => {
console.log("Timer 1");
}, 0);
console.log("Middle");
setTimeout(() => {
console.log("Timer 2");
}, 0);
console.log("End");
Förslag till svar: JavaScript är single-threaded men använder Event Loop:
- Call Stack: Kör synkron kod
- Web APIs: Hanterar asynkrona operationer (setTimeout, fetch, DOM events)
- Callback Queue: Köar färdiga callbacks
- Event Loop: Flyttar callbacks från queue till call stack när stack är tom
Utskrift:
Start
Middle
End
Timer 1
Timer 2
Förklaring: Synkron kod körs först (Start, Middle, End). setTimeout
callbacks, även med 0ms delay, placeras i Web APIs och sedan Callback Queue. Event Loop flyttar dem till Call Stack först när all synkron kod är klar.
Fråga 2: Callbacks och Callback Hell
Fråga: "Vad är 'Callback Hell' och hur kan det undvikas? Visa ett exempel och förklara problemen."
Förslag till svar: Callback Hell uppstår när flera asynkrona operationer behöver köras i sekvens:
// Callback Hell exempel
getData(function(a) {
getMoreData(a, function(b) {
getEvenMoreData(b, function(c) {
getFinalData(c, function(d) {
// Djupt nästlad kod...
console.log(d);
});
});
});
});
Problem:
- Låg läsbarhet: Svårt att följa kodflödet
- Svår felhantering: Fel måste hanteras på varje nivå
- Svårt att underhålla: Ändringar blir komplexa
Lösningar:
- Promises:
getData().then().then().catch()
- Async/Await: Gör asynkron kod synkron-liknande
- Namngivna funktioner: Istället för anonyma callbacks
Fråga 3: Promises och Promise States
Fråga: "Förklara Promise lifecycle. Vad är skillnaden mellan .then()
, .catch()
och .finally()
? Skriv kod som visar alla tre."
Förslag till svar: Promise States:
- Pending: Initial state, varken fulfilled eller rejected
- Fulfilled: Operation lyckades, har ett värde
- Rejected: Operation misslyckades, har en anledning (error)
- Settled: Antingen fulfilled eller rejected (kan ej ändras)
function asyncOperation(shouldSucceed) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldSucceed) {
resolve("Success data");
} else {
reject(new Error("Operation failed"));
}
}, 1000);
});
}
asyncOperation(true)
.then(result => {
console.log("Success:", result); // Körs vid fulfilled
return result.toUpperCase(); // Kan returnera värde för kedja
})
.then(upperResult => {
console.log("Processed:", upperResult);
})
.catch(error => {
console.error("Error:", error.message); // Körs vid rejected
})
.finally(() => {
console.log("Operation completed"); // Körs alltid
});
Viktigt: .then()
returnerar ett nytt Promise, möjliggör kedja.
Fråga 4: Async/Await vs Promises
Fråga: "Skriv om denna Promise-kod till async/await. Vad är fördelarna och nackdelarna med varje approach?"
// Promise version
function fetchUserData(userId) {
return fetch(`/api/users/${userId}`)
.then(response => response.json())
.then(user => fetch(`/api/posts?userId=${user.id}`))
.then(response => response.json())
.then(posts => ({ user, posts }))
.catch(error => console.error("Error:", error));
}
Förslag till svar:
// Async/Await version
async function fetchUserData(userId) {
try {
const userResponse = await fetch(`/api/users/${userId}`);
const user = await userResponse.json();
const postsResponse = await fetch(`/api/posts?userId=${user.id}`);
const posts = await postsResponse.json();
return { user, posts };
} catch (error) {
console.error("Error:", error);
}
}
Jämförelse:
- Async/Await fördelar: Mer läsbar, lättare att debugga, använder try/catch
- Async/Await nackdelar: Kan vara långsammare vid parallella operationer
- Promises fördelar: Bra för parallell execution, funktionell stil
- Promises nackdelar: Kan bli svårläst med komplexa kedjor
Fråga 5: Fetch API och Error Handling
Fråga: "Skriv en funktion som hämtar data med Fetch API. Hantera både nätverksfel och HTTP-fel (4xx, 5xx). Varför kastar inte fetch() automatiskt fel för HTTP-fel?"
Förslag till svar:
async function safeFetch(url, options = {}) {
try {
const response = await fetch(url, options);
// Fetch kastar INTE fel för HTTP status codes som 404, 500
if (!response.ok) {
// Försök läsa felmeddelande från server
let errorMessage = `HTTP Error: ${response.status}`;
try {
const errorData = await response.json();
errorMessage = errorData.message || errorMessage;
} catch {
// Om response inte är JSON, använd standardmeddelande
}
throw new Error(errorMessage);
}
return await response.json();
} catch (error) {
// Hantera både nätverksfel och HTTP-fel
if (error.name === 'TypeError') {
throw new Error("Network error - check connection");
}
throw error; // Re-throw HTTP errors
}
}
// Användning
safeFetch('/api/users/123')
.then(data => console.log('Success:', data))
.catch(error => console.error('Failed:', error.message));
Varför fetch() inte kastar fel för HTTP-fel:
- Fetch anser att få ett svar (även 404/500) är "success" - nätverksanropet lyckades
- Bara nätverksfel (ingen internet, server ej tillgänglig) kastas som fel
response.ok
kontrollerar om status är 200-299
Fråga 6: JSON och Data Transformation
Fråga: "Förklara skillnaden mellan JSON.stringify()
och JSON.parse()
. Vad händer med funktioner och undefined
värden? Skriv kod som demonstrerar detta."
Förslag till svar:
const jsObject = {
name: "Alice",
age: 30,
active: true,
address: null,
hobbies: ["reading", "coding"],
greet: function() { return "Hello!"; }, // Funktion
secret: undefined, // undefined
id: Symbol('user') // Symbol
};
// JavaScript till JSON-sträng
const jsonString = JSON.stringify(jsObject);
console.log("JSON:", jsonString);
// Output: {"name":"Alice","age":30,"active":true,"address":null,"hobbies":["reading","coding"]}
// Notera: greet, secret och id försvinner!
// JSON-sträng till JavaScript
const parsedObject = JSON.parse(jsonString);
console.log("Parsed:", parsedObject);
// Felhantering vid parsning
try {
const invalidJson = '{"name": "Alice", "age": 30,}'; // Avslutande komma
JSON.parse(invalidJson);
} catch (error) {
console.error("Parse error:", error.message);
}
// Pretty printing
const prettyJson = JSON.stringify(jsObject, null, 2);
console.log("Pretty:\n", prettyJson);
Viktiga regler:
- Ignoreras: functions, undefined, Symbol
- Konverteras: Date → string, NaN/Infinity → null
- Kräver dubbla citattecken: för strings och keys
- Felhantering: Alltid wrappa
JSON.parse()
i try/catch
Fråga 7: Array.map() och Transformation
Fråga: "Förklara map()
metoden. Hur skiljer den sig från en for-loop? Transformera denna data för att skapa en ny struktur:"
const users = [
{ id: 1, firstName: "Anna", lastName: "Svensson", age: 25 },
{ id: 2, firstName: "Erik", lastName: "Johansson", age: 30 },
{ id: 3, firstName: "Maria", lastName: "Andersson", age: 28 }
];
// Skapa array med format: "Anna S. (25 år)"
Förslag till svar:
// Med map() - REKOMMENDERAT
const formattedUsers = users.map(user => {
const lastInitial = user.lastName.charAt(0);
return `${user.firstName} ${lastInitial}. (${user.age} år)`;
});
// Med traditional for-loop
const formattedUsersLoop = [];
for (let i = 0; i < users.length; i++) {
const user = users[i];
const lastInitial = user.lastName.charAt(0);
formattedUsersLoop.push(`${user.firstName} ${lastInitial}. (${user.age} år)`);
}
console.log(formattedUsers);
// Output: ["Anna S. (25 år)", "Erik J. (30 år)", "Maria A. (28 år)"]
Skillnader:
- map(): Returnerar ny array, original oförändrad, funktionell stil
- for-loop: Imperativ, måste hantera ny array manuellt
- map() fördelar: Kortare, mer läsbar, mindre risk för buggar
- for-loop fördelar: Mer kontroll, kan break/continue
Viktigt: map() returnerar alltid en array med samma längd som originalet.
Fråga 8: Array.filter() och Conditional Logic
Fråga: "Använd filter()
för att lösa denna uppgift: Från en array med produkter, hitta alla produkter som kostar mellan 100-500 kr OCH som finns i lager."
const products = [
{ name: "Laptop", price: 8999, inStock: true },
{ name: "Mouse", price: 299, inStock: true },
{ name: "Keyboard", price: 150, inStock: false },
{ name: "Monitor", price: 2500, inStock: true },
{ name: "Cable", price: 89, inStock: true },
{ name: "Headphones", price: 450, inStock: true }
];
Förslag till svar:
// Med filter()
const affordableInStock = products.filter(product => {
return product.price >= 100 &&
product.price <= 500 &&
product.inStock === true;
});
// Kortare version
const affordableInStock2 = products.filter(p =>
p.price >= 100 && p.price <= 500 && p.inStock
);
console.log(affordableInStock);
// Output: [
// { name: "Mouse", price: 299, inStock: true },
// { name: "Headphones", price: 450, inStock: true }
// ]
// Kedja filter med map för att bara få namnen
const productNames = products
.filter(p => p.price >= 100 && p.price <= 500 && p.inStock)
.map(p => p.name);
console.log(productNames); // ["Mouse", "Headphones"]
Viktigt:
- filter() returnerar ny array med element som passar villkoret
- Callback-funktionen måste returnera true/false
- Kan kedjas med andra array-metoder
Fråga 9: Array.reduce() och Aggregation
Fråga: "Använd reduce()
för att beräkna totala lagervärdet från denna produktdata. Förklara varje del av reduce-anropet."
const inventory = [
{ name: "Laptop", price: 8999, quantity: 5 },
{ name: "Mouse", price: 299, quantity: 20 },
{ name: "Monitor", price: 2500, quantity: 8 }
];
Förslag till svar:
const totalValue = inventory.reduce((accumulator, product) => {
const productValue = product.price * product.quantity;
console.log(`${product.name}: ${productValue} kr (Total så far: ${accumulator + productValue})`);
return accumulator + productValue;
}, 0);
console.log("Totalt lagervärde:", totalValue, "kr");
// Output:
// Laptop: 44995 kr (Total så far: 44995)
// Mouse: 5980 kr (Total så far: 50975)
// Monitor: 20000 kr (Total så far: 70975)
// Totalt lagervärde: 70975 kr
Reduce-delar förklarade:
- Callback-funktion:
(accumulator, product) => { ... }
accumulator
: Ackumulerat värde från tidigare iterationerproduct
: Nuvarande element som bearbetas
- Initial värde:
0
- startvärdet för accumulator - Return: Måste returnera uppdaterat accumulator-värde
Annan reduce-användning - gruppering:
// Gruppera produkter efter prisklass
const priceGroups = inventory.reduce((groups, product) => {
const key = product.price > 1000 ? 'expensive' : 'affordable';
if (!groups[key]) groups[key] = [];
groups[key].push(product);
return groups;
}, {});
Fråga 10: Destructuring och Spread Operator
Fråga: "Förklara destructuring och spread operator. Skriv kod som visar båda för arrays och objekt."
Förslag till svar:
// Array Destructuring
const coordinates = [10, 20, 30];
const [x, y, z] = coordinates;
console.log(x, y, z); // 10 20 30
// Med rest operator
const numbers = [1, 2, 3, 4, 5];
const [first, second, ...rest] = numbers;
console.log(first); // 1
console.log(second); // 2
console.log(rest); // [3, 4, 5]
// Object Destructuring
const user = {
id: 1,
name: "Alice",
email: "alice@example.com",
settings: { theme: "dark", notifications: true }
};
const { name, email } = user;
console.log(name, email); // Alice alice@example.com
// Rename och nested destructuring
const { id: userId, settings: { theme } } = user;
console.log(userId, theme); // 1 dark
// Spread Operator - Arrays
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = [...arr1, 0, ...arr2];
console.log(combined); // [1, 2, 3, 0, 4, 5, 6]
// Spread Operator - Objects
const basicInfo = { name: "Bob", age: 25 };
const extendedInfo = {
...basicInfo,
email: "bob@example.com",
age: 26 // Överskriver age från basicInfo
};
console.log(extendedInfo);
// { name: "Bob", age: 26, email: "bob@example.com" }
// Function parameters
function greetUser({ name, age = 'unknown' }) {
console.log(`Hello ${name}, age: ${age}`);
}
greetUser(user); // Hello Alice, age: unknown
Användningsfall:
- Destructuring: Clean parameter extraction, multiple returns
- Spread: Copying arrays/objects, combining data, function arguments
Fråga 11: Parallell vs Sekventiell Asynkron Kod
Fråga: "Förklara skillnaden mellan dessa två kod-exempel. När skulle du använda vilken approach?"
// Version A
async function fetchDataA() {
const user = await fetch('/api/user').then(r => r.json());
const posts = await fetch('/api/posts').then(r => r.json());
const comments = await fetch('/api/comments').then(r => r.json());
return { user, posts, comments };
}
// Version B
async function fetchDataB() {
const [user, posts, comments] = await Promise.all([
fetch('/api/user').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/comments').then(r => r.json())
]);
return { user, posts, comments };
}
Förslag till svar: Version A - Sekventiell:
- Kör requests en efter en
- Total tid: Sum av alla requests
- Används när requests beror på varandra
Version B - Parallell:
- Kör requests samtidigt
- Total tid: Längsta request
- Används när requests är oberoende
// Tidsexempel (om varje request tar 1s):
// Version A: 3 sekunder total
// Version B: 1 sekund total
// Promise.all() alternativ och felhantering
async function robustFetch() {
try {
// Promise.all - misslyckas om EN request misslyckas
const results = await Promise.all([...]);
// Promise.allSettled - fortsätter även om några misslyckas
const results2 = await Promise.allSettled([...]);
results2.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`Request ${index}:`, result.value);
} else {
console.error(`Request ${index} failed:`, result.reason);
}
});
} catch (error) {
console.error("At least one request failed:", error);
}
}
Välj baserat på:
- Sekventiell: När data behövs från tidigare request
- Parallell: När requests är oberoende, för bättre prestanda
Fråga 12: Praktisk Debugging och Felhantering
Fråga: "Du får detta fel i konsolen: 'TypeError: Cannot read property 'map' of undefined'. Hur skulle du debugga och fixa problemet i denna kod?"
async function displayUsers() {
try {
const response = await fetch('/api/users');
const data = await response.json();
const userList = data.users.map(user => `<li>${user.name}</li>`);
document.getElementById('user-list').innerHTML = userList.join('');
} catch (error) {
console.error("Error:", error);
}
}
Förslag till svar: Problemanalys:
data.users
ärundefined
när.map()
anropas- API:et kanske returnerar olika struktur än förväntat
- Nätverksfel kan ge oväntad response
Debug-teknik:
async function displayUsers() {
try {
const response = await fetch('/api/users');
// Debug 1: Kontrollera response status
console.log('Response status:', response.status);
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status}`);
}
const data = await response.json();
// Debug 2: Logga faktisk data-struktur
console.log('Received data:', data);
console.log('Data type:', typeof data);
console.log('Has users property:', 'users' in data);
// Defensive programming - kontrollera innan map
if (!data || !Array.isArray(data.users)) {
console.warn('Unexpected data structure:', data);
// Fallback för olika API-strukturer
const users = Array.isArray(data) ? data : [];
return displayUsersFallback(users);
}
const userList = data.users.map(user => {
// Debug 3: Kontrollera varje user-objekt
if (!user || !user.name) {
console.warn('Invalid user object:', user);
return '<li>Unknown user</li>';
}
return `<li>${user.name}</li>`;
});
const listElement = document.getElementById('user-list');
if (listElement) {
listElement.innerHTML = userList.join('');
} else {
console.error('Element with id "user-list" not found');
}
} catch (error) {
console.error("Error in displayUsers:", error);
// Visa user-friendly meddelande
const listElement = document.getElementById('user-list');
if (listElement) {
listElement.innerHTML = '<li>Failed to load users</li>';
}
}
}
function displayUsersFallback(users) {
// Hantera alternativ data-struktur
console.log('Using fallback for users:', users);
}
Best Practices:
- Logga intermediate steps för att förstå data-flöde
- Defensive programming - kontrollera data-typer
- Graceful degradation - visa fallback vid fel
- User-friendly error messages - inte bara console.error
Tips för Tekniska Intervjuer
- Tänk högt - förklara ditt approach innan du kodar
- Börja enkelt - implementera basic funktionalitet först
- Hantera edge cases - vad händer med fel, null, undefined?
- Diskutera performance - när är olika metoder lämpliga?
- Visa modern syntax - men förklara varför du väljer det
- Fråga om krav - behövs felhantering? Browser-support?
Hosting, CMS och WordPress
Detta kapitel täcker hosting-lösningar, innehållshanteringssystem och WordPress.
Webbservrar och hosting
- Olika typer av webbservrar
- Hosting-alternativ
- Server-konfiguration
- SSL/TLS och säkerhet
Hantera en virtuell privat server
- VPS setup
- SSH och fjärråtkomst
- Server-administration
- Säkerhetsåtgärder
Domänhantering och DNS
- Domänregistrering
- DNS-inställningar
- A-records och CNAME
- SSL-certifikat
Introduktion till CMS
- Vad är ett CMS?
- Olika CMS-alternativ
- Headless CMS
- CMS-arkitektur
WordPress
- Installation och konfiguration
- Teman och plugins
- Innehållshantering
- Säkerhet och underhåll
Static Site Generators med Jekyll
- Jekyll grunderna
- Markdown och front matter
- Themes och layouts
- GitHub Pages deployment
Praktiska övningar och projekt
- WordPress-site setup
- Custom theme development
- Jekyll blog creation
- Server deployment
Teknisk Intervju
Fullstack-utveckling med PHP och MySQL
Detta kapitel introducerar fullstack-utveckling med PHP och MySQL för att bygga kompletta webbapplikationer.
Introduktion till PHP
PHP (Hypertext Preprocessor) är ett populärt skriptspråk med öppen källkod som främst används för webbutveckling på serversidan. Det skapades ursprungligen av Rasmus Lerdorf 1994 och har sedan dess utvecklats till ett kraftfullt och flexibelt språk som driver en stor del av webben, inklusive välkända plattformar som WordPress och Facebook.
PHP i modern webbutveckling
Trots framväxten av nyare teknologier förblir PHP ett relevant och ofta använt språk inom webbutveckling. Några anledningar till detta är:
- Stor community och ekosystem: Det finns ett enormt bibliotek av ramverk (t.ex. Laravel, Symfony), paket och verktyg tillgängliga för PHP.
- Enkelhet och snabb inlärningskurva: Många utvecklare anser att PHP är relativt lätt att komma igång med jämfört med andra serverspråk.
- Integration med databaser: PHP har utmärkt stöd för att interagera med olika databaser, särskilt MySQL.
- Kostnadseffektivt: PHP är gratis att använda och många webbhotell erbjuder prisvärda PHP-hostingalternativ.
Utvecklingsmiljö setup
För att utveckla med PHP lokalt behöver du installera:
- PHP-tolken: Själva kärnan i PHP som exekverar din kod. Den kan laddas ner från php.net.
- En webbserver: T.ex. Apache eller Nginx, som hanterar HTTP-förfrågningar och serverar dina PHP-filer.
- En databas: Ofta MySQL eller MariaDB om din applikation behöver lagra data.
Ett enkelt sätt att få allt detta är att installera ett paket som XAMPP, MAMP eller WAMP, som buntar ihop Apache, MySQL och PHP (och ofta Perl/Python) för enkel installation på Windows, macOS eller Linux.
Alternativt kan du använda verktyg som Docker för att skapa isolerade och konfigurerbara utvecklingsmiljöer.
PHP och webbservrar
När en användare begär en PHP-sida från en webbserver händer följande:
- Webbservern (t.ex. Apache) tar emot förfrågan.
- Om filen har ändelsen
.php
, skickar servern filen till PHP-tolken. - PHP-tolken exekverar koden i filen. Detta kan innebära att hämta data från en databas, bearbeta formulärdata, eller utföra annan logik.
- Resultatet av PHP-skriptet (vanligtvis HTML, men kan vara JSON, XML etc.) skickas tillbaka till webbservern.
- Webbservern skickar det slutliga resultatet (HTML) till användarens webbläsare.
sequenceDiagram participant B as Webbläsare participant WS as Webbserver (t.ex. Apache) participant PHP as PHP-Tolk participant DB as Databas (Valfritt) B->>+WS: Begär PHP-sida (t.ex. GET /sida.php) WS->>+PHP: Överlämnar sida.php för bearbetning alt Databasinteraktion PHP->>+DB: Skickar databasfråga DB-->>-PHP: Returnerar resultat end PHP-->>-WS: Returnerar genererad HTML/Output WS-->>-B: Skickar HTML-svar
Grundläggande koncept och syntax
PHP-kod bäddas oftast in direkt i HTML-dokument med hjälp av speciella taggar: <?php
och ?>
.
<!DOCTYPE html>
<html>
<head>
<title>Min första PHP-sida</title>
</head>
<body>
<h1>Välkommen!</h1>
<p>Dagens datum är <?php echo date('Y-m-d'); ?>.</p>
<?php
// Detta är en PHP-kommentar
$greeting = "Hej från PHP!";
echo "<p>" . $greeting . "</p>";
$num1 = 5;
$num2 = 10;
$sum = $num1 + $num2;
echo "<p>Summan av $num1 och $num2 är: $sum</p>";
?>
</body>
</html>
I exemplet ovan:
<?php ... ?>
: Taggarna som omger PHP-koden.echo
: Används för att skriva ut data till HTML-outputen.date('Y-m-d')
: En inbyggd PHP-funktion som returnerar dagens datum.$greeting
,$num1
,$num2
,$sum
: Variabler (börjar alltid med$
i PHP)..
: Konkateneringsoperatorn för att slå ihop strängar.//
: En enkelradskommentar i PHP.
PHP vs. JavaScript
Det är viktigt att förstå skillnaden mellan PHP och JavaScript, två vanliga språk inom webbutveckling:
-
PHP (Server-side):
- Körs på webbservern.
- Används för att hantera data, interagera med databaser, hantera användarsessioner, och generera dynamiskt HTML innan det skickas till webbläsaren.
- Användaren ser aldrig själva PHP-koden, bara resultatet (oftast HTML).
- Bra för backend-logik, databashantering, säkerhetskritiska operationer.
-
JavaScript (Client-side):
- Körs i användarens webbläsare.
- Används för att göra webbsidor interaktiva, manipulera HTML och CSS (DOM-manipulation), göra asynkrona anrop (t.ex. med Fetch API) och validera formulärdata utan att behöva ladda om sidan.
- Användaren kan se JavaScript-koden (oftast).
- Bra för användargränssnitt, interaktivitet, realtidsuppdateringar på klientsidan.
Analogi: Tänk på en restaurang. PHP är som kocken som förbereder maten (datan) i köket (servern) baserat på beställningen (HTTP-förfrågan). JavaScript är som servitören som interagerar med gästen (användaren) vid bordet (webbläsaren), tar emot specifika önskemål (användarinteraktioner) och justerar presentationen (webbsidan) direkt.
Även om JavaScript ursprungligen var ett klientspråk, kan det idag även köras på serversidan med plattformar som Node.js (vilket tas upp i kapitel 9). Men när man pratar om traditionell webbutveckling är PHP ett serverspråk och JavaScript (i webbläsaren) ett klientspråk. PHP och JavaScript kompletterar ofta varandra för att skapa rika och dynamiska webbapplikationer.
Kom igång med PHP-utveckling
För att bygga och köra PHP-applikationer lokalt behöver du en utvecklingsmiljö som inkluderar PHP-tolken, en webbserver och ofta en databas. En traditionell "stack" (samling mjukvara) för detta är LAMP (Linux, Apache, MySQL, PHP) eller LEMP (Linux, Nginx, MySQL, PHP). Istället för att installera dessa tjänster direkt på ditt operativsystem, kommer vi i den här boken att använda Docker.
Docker låter oss definiera och köra applikationer i isolerade "containrar". Detta gör det enklare att:
- Sätta upp en konsekvent utvecklingsmiljö oavsett ditt operativsystem.
- Hantera olika versioner av mjukvara (PHP, MySQL etc.) för olika projekt.
- Simulera produktionsmiljön mer exakt.
- Dela miljökonfigurationen med andra utvecklare.
Vi kommer att använda en docker-compose.yml
-fil för att definiera de tjänster (services) som vår PHP-applikation behöver. I vårt fall är det:
- Webbserver (Apache): Apache är en mycket populär och väletablerad webbserver som ansvarar för att ta emot HTTP-förfrågningar från webbläsare och skicka dem vidare till PHP-tolken för bearbetning av
.php
-filer. Den serverar också statiska filer som HTML, CSS och JavaScript. - Databas (MariaDB): MariaDB är en populär relationsdatabas med öppen källkod, skapad av de ursprungliga utvecklarna av MySQL. Den används för att lagra och hämta applikationens data på ett strukturerat sätt. PHP har utmärkt stöd för att kommunicera med MariaDB/MySQL.
- Databashanterare (phpMyAdmin): phpMyAdmin är ett webbaserat verktyg skrivet i PHP som ger ett grafiskt gränssnitt för att administrera MariaDB/MySQL-databaser. Det är praktiskt för att titta på data, köra SQL-frågor manuellt och hantera databasstrukturen under utveckling.
- PHP: Själva PHP-tjänsten som kör vår applikationskod.
Nedan följer ett exempel på hur en docker-compose.yml
-fil kan se ut för att sätta upp denna miljö. (Detaljer kring Docker och Docker Compose täcks mer ingående i andra sammanhang, men detta ger en grundläggande uppfattning).
docker-compose.yml
services:
php:
build:
context: .
dockerfile: Dockerfile
volumes:
- ./app/public:/var/www/html
ports:
- 8060:80
mysql:
image: mariadb:latest
environment:
MYSQL_ROOT_PASSWORD: db_root_password
MYSQL_USER: db_user
MYSQL_PASSWORD: db_password
MYSQL_DATABASE: db_fullstack
volumes:
- mysqldata:/var/lib/mysql
ports:
- 38060:3306
phpmyadmin:
image: phpmyadmin
restart: always
ports:
- 8061:80
environment:
- PMA_ARBITRARY=1
depends_on:
- mysql
volumes:
mysqldata: {}
Dockerfile
FROM php:8-apache
RUN a2enmod ssl && a2enmod rewrite
RUN service apache2 restart
RUN apt-get update && apt-get install -y
RUN docker-php-ext-install mysqli pdo pdo_mysql
Mappstruktur för exemplet ovan
project
├───app
│ └───public
│ └───index.php
├───Dockerfile
├───docker-compose.yml
Demo: Server-side vs Client-side
För att illustrera skillnaden mellan kod som körs på servern (PHP) och kod som körs i klienten (JavaScript), kan vi skapa en enkel index.php
-fil:
<!DOCTYPE html>
<html>
<head>
<title>PHP & JS Demo</title>
</head>
<body>
<h1>PHP säger:</h1>
<?php
// Denna PHP-kod körs på servern
$serverMessage = "Jag körs på servern!";
echo "<p>" . htmlspecialchars($serverMessage) . "</p>";
?>
<script>
// Denna JavaScript-kod körs i klientens webbläsare
console.log("Jag körs på klienten (i webbläsarens konsol)!");
alert("Hej från JavaScript!"); // Visar en popup i webbläsaren
</script>
</body>
</html>
När du besöker denna sida i webbläsaren:
- PHP-koden (
echo ...
) exekveras på servern och genererar HTML-texten<p>Jag körs på servern!</p>
. - Den kompletta HTML-koden (inklusive
<script>
-taggarna) skickas till webbläsaren. - Webbläsaren renderar HTML:en och exekverar JavaScript-koden inuti
<script>
-taggarna, vilket skriver ut meddelandet till webbläsarkonsolen och visar enalert
-ruta.
Detta visar tydligt hur PHP genererar innehållet på serversidan medan JavaScript hanterar interaktivitet och logik på klientsidan.
Introduktion till PHP
- PHP i modern webbutveckling
- Utvecklingsmiljö setup
- PHP och webbservrar
- Grundläggande koncept
PHP Syntax och grundläggande funktioner
Detta avsnitt dyker djupare in i den grundläggande syntaxen för PHP. Om du har erfarenhet av JavaScript kommer mycket att kännas bekant, men det finns viktiga skillnader i hur du skriver variabler, hanterar datatyper, och strukturerar din kod. Vi kommer att fokusera på dessa skillnader och även introducera några moderna PHP-funktioner.
PHP-taggar och kommentarer
PHP-kod exekveras på servern och bäddas oftast in i HTML-dokument med hjälp av speciella taggar.
- Standardtaggar:
<?php ... ?>
Detta är det vanligaste och mest rekommenderade sättet att markera PHP-kodblock. - Kort echo-tagg:
<?= ... ?>
En genväg för<?php echo ... ?>
. Användbar för att snabbt skriva ut en variabel eller resultatet av en funktion direkt i HTML.
<!DOCTYPE html>
<html>
<head>
<title>PHP Taggar</title>
</head>
<body>
<h1>Välkommen</h1>
<p>Dagens datum är: <?= date('Y-m-d'); ?></p>
<?php
// Detta är en PHP-kommentar (enkelrad)
$message = "Hej från ett PHP-block!";
/*
Detta är en
flerradskommentar i PHP.
*/
echo "<p>" . $message . "</p>";
?>
</body>
</html>
- Kommentarer: Liknar JavaScript:
//
för enkelradskommentarer och/* ... */
för flerradskommentarer.
Kodstandard och namngivningsprinciper
Att följa en kodstandard är något som programmerare bör vara insatt i. PHP är ett programspråk som funnits under relativt lång tid och därmed förändras också en kodstandard. Sträva efter att vara konsekvent.
Variabler
En av de mest uppenbara skillnaderna från JavaScript är hur variabler deklareras och används i PHP.
- Alla variabelnamn i PHP inleds med ett dollartecken (
$
). - Efter
$
måste namnet börja med en bokstav eller ett understreck (_
), följt av valfritt antal bokstäver, siffror eller understreck. - PHP är Case sensitive för variabelnamn (
$name
och$Name
är två olika variabler). En variabel namnges med gemener. Använd understreck mellan ord.
$last_name
$is_authenticated
Funktioner
Funktionsnamn skrivs med gemener och understreck mellan ord - snake_case.
get_name()
render_copyright()
Klasser och metoder
En klass namnges PascalCase, och metoder i klassen som camelCase.
MyClass() {
function __construct() {
}
public function printInfo() {
}
}
PHP:
<?php
$first_name = "Anna"; // Skapar och tilldelar en sträng
$age = 30; // Skapar och tilldelar ett heltal
$price = 199.50; // Skapar och tilldelar ett flyttal
$is_valid = true; // Namn kan börja med _
$age = 31; // Ändrar värdet på en befintlig variabel
echo "Namn: " . $first_name . ", Ålder: " . $age;
?>
JavaScript (jämförelse):
let firstName = "Anna"; // Kräver let, const (eller var)
let age = 30;
const price = 199.50; // Konstant, kan inte ändras
let isValid = true; // Inget $ behövs
age = 31; // Ändra värde
console.log(`Namn: ${firstName}, Ålder: ${age}`); // Template literals för enkel utskrift
Datatyper
PHP har flera inbyggda datatyper. De viktigaste kan delas in i:
Skalära Typer (Scalar Types)
Dessa representerar enskilda värden.
int
(Integer): Heltal (t.ex.10
,-5
,0
).float
(Floating-point number, ävendouble
): Decimaltal (t.ex.3.14
,-0.5
,1.0
).string
: Textsträngar. Kan omges av enkla ('...'
) eller dubbla ("..."
) citationstecken.- Dubbla citationstecken (
"
) tillåter variabelinterpolering (variabler inuti strängen ersätts med deras värden) och specialtecken som\n
(nyrad),\t
(tab). - Enkla citationstecken (
'
) behandlar nästan all text bokstavligt (literal), ingen variabelinterpolering sker (förutom\\
och\'
). De är ofta snabbare när ingen interpolering behövs.
- Dubbla citationstecken (
bool
(Boolean): Logiska värden, antingentrue
ellerfalse
(skiftlägesokänsliga).
<?php
$count = 100; // int
$pi = 3.14159; // float
$is_admin = false; // bool
$single_quote = 'Detta är en sträng. Variabel: $count \n'; // $count och \n skrivs ut bokstavligt
$double_quote = "Detta är en sträng. Variabel: $count \n"; // $count ersätts, \n blir nyrad
echo $single_quote;
// Output: Detta är en sträng. Variabel: $count \n
echo $double_quote;
// Output: Detta är en sträng. Variabel: 100
// (och en nyrad)
?>
Sammansatta Typer (Compound Types)
Dessa kan innehålla flera värden.
array
: Ordnad mappning av nyckel-värde-par. Kan vara indexerad (numeriska nycklar) eller associativ (strängnycklar). (Searray-loopar.md
för detaljer).object
: Instanser av klasser.callable
: En referens till en funktion.
Speciella Typer
resource
: En speciell variabel som håller en referens till en extern resurs (t.ex. en databasanslutning, en öppen fil).null
: Representerar avsaknaden av ett värde. En variabel har värdetnull
om den har tilldelatsnull
, inte har tilldelats något värde alls, eller har blivitunset()
.
PHP är ett dynamiskt typat språk (som JavaScript) som standard, vilket innebär att du inte behöver specificera datatypen när du skapar en variabel. PHP avgör typen baserat på värdet den tilldelas, och typen kan ändras under körningens gång (även om detta bör undvikas om möjligt).
<?php
$value = 10; // $value är nu int
echo gettype($value); // Output: integer
$value = "Hej"; // $value är nu string
echo gettype($value); // Output: string
?>
(Vi kommer till Type Hinting senare, vilket låter dig vara mer explicit med typer i funktioner.)
Konstanter
För värden som inte ska ändras under skriptets körning kan du definiera konstanter med define()
eller const
.
define(name, value, case_insensitive)
: Traditionellt sätt. Namnet är en sträng. Standard är skiftlägeskänsligt.const NAME = value;
: Nyare sätt (sedan PHP 5.3), används oftast inom klasser men fungerar även globalt. Kan inte användas inom kontrollstrukturer (somif
). Alltid skiftlägeskänsligt.
Konstanter har inget $
framför sig.
<?php
define("APP_VERSION", "1.0.2");
const DEFAULT_LANG = "sv";
echo APP_VERSION; // Output: 1.0.2
echo DEFAULT_LANG; // Output: sv
// Försök att ändra konstant (ger fel)
// APP_VERSION = "1.1.0"; // Fatal error
// const DEFAULT_LANG = "en"; // Parse error
?>
Operatorer
PHP har många operatorer som liknar de i JavaScript.
- Aritmetiska:
+
,-
,*
,/
,%
(modulo/rest),**
(exponent, sedan PHP 5.6). - Tilldelning:
=
,+=
,-=
,*=
,/=
,%=
,**=
,.=
(för strängkonkatenering). - Jämförelse:
==
(Lika, efter typomvandling - undvik ofta!)===
(Identisk, samma värde OCH samma typ - rekommenderas!)!=
eller<>
(Inte lika, efter typomvandling)!==
(Inte identisk, olika värde ELLER olika typ - rekommenderas!)<
,>
,<=
,>=
<=>
(Spaceship operator, sedan PHP 7): Returnerar -1, 0, eller 1 beroende på om vänster är mindre än, lika med, eller större än höger.
- Logiska:
&&
(and),||
(or),!
(not),and
(samma som&&
men lägre prioritet),or
(samma som||
men lägre prioritet),xor
. - Strängkonkatenering:
.
(punkt) används för att slå ihop strängar. - Felkontroll:
@
(används framför ett uttryck för att tysta eventuella fel/varningar det genererar - använd med extrem försiktighet!) - Null Coalescing:
??
(sedan PHP 7): Returnerar vänster operand om den existerar och inte ärnull
, annars höger operand. Bra för att sätta defaultvärden. - Null Coalescing Assignment:
??=
(sedan PHP 7.4): Tilldelar höger operand till vänster operand endast om vänster operand ärnull
.
<?php
$a = 5;
$b = "5";
$c = 10;
var_dump($a == $b); // bool(true) - Undvik! '5' görs om till 5.
var_dump($a === $b); // bool(false) - Rekommenderas! Olika typer.
var_dump($a != $b); // bool(false)
var_dump($a !== $b); // bool(true) - Rekommenderas!
var_dump($a <=> $c); // int(-1)
var_dump($a <=> $a); // int(0)
var_dump($c <=> $a); // int(1)
$name = $_GET['user'] ?? 'Gäst'; // Om $_GET['user'] inte finns eller är null, blir $name 'Gäst'
echo "Välkommen, $name!\n";
$settings['theme'] = $settings['theme'] ?? 'light'; // Traditionellt
$settings['theme'] ??= 'light'; // Samma som ovan med ??=
$greeting = "Hej";
$target = "Världen";
$fullGreeting = $greeting . " " . $target . "!"; // Strängkonkatenering
echo $fullGreeting; // Output: Hej Världen!
?>
Kontrollstrukturer
Styr hur koden exekveras.
if
, elseif
, else
Fungerar i princip identiskt med JavaScript.
<?php
$score = 75;
if ($score >= 90) {
echo "Betyg: A";
} elseif ($score >= 80) {
echo "Betyg: B";
} elseif ($score >= 65) {
echo "Betyg: C"; // Denna körs
} else {
echo "Betyg: F";
}
?>
switch
Liknar JavaScripts switch
, men använder lös jämförelse (==
) som standard. Kom ihåg break;
!
<?php
$day = "Måndag";
switch ($day) {
case "Måndag":
echo "Start på veckan.";
break;
case "Fredag":
echo "Snart helg!";
break;
default:
echo "En vanlig dag.";
// break; behövs inte i default
}
?>
match
(PHP 8+)
Ett modernare alternativ till switch
. Viktiga skillnader:
- Använder strikt jämförelse (
===
). Mycket säkrare! - Är ett uttryck, vilket betyder att det returnerar ett värde.
- Har inga
break
, faller inte igenom. - Måste vara uttömmande (ha ett
default
-fall eller täcka alla möjliga värden om input-typen är känd, t.ex. en enum). - Kan ha flera värden per arm, separerade med komma.
<?php
$httpStatusCode = 200;
$message = match ($httpStatusCode) {
200, 201, 204 => "Success!",
400 => "Bad Request",
404 => "Not Found",
500 => "Server Error",
default => "Unknown status code",
};
echo $message; // Output: Success!
// Jämför med switch:
// switch ($httpStatusCode) {
// case 200:
// case 201:
// case 204:
// $message = "Success!";
// break;
// case 400:
// $message = "Bad Request";
// break;
// // ... etc ...
// }
?>
match
är ofta att föredra framför switch
i modern PHP tack vare strikt jämförelse och att det är ett uttryck.
Loopar (for
, while
, do-while
, foreach
)
for
, while
, och do-while
fungerar i stort sett som i JavaScript.
foreach
är specifikt designad för att loopa över arrayer och objekt och täcks i detalj i array-loopar.md
.
<?php
// for-loop
for ($i = 0; $i < 5; $i++) {
echo "i är $i\n";
}
// while-loop
$j = 0;
while ($j < 3) {
echo "j är $j\n";
$j++;
}
// do-while loop (körs minst en gång)
$k = 5;
do {
echo "k är $k\n"; // Körs en gång
$k++;
} while ($k < 5);
?>
Funktioner
Att definiera och anropa funktioner liknar JavaScript, men med PHP-syntax.
<?php
// Definiera en funktion
function greet($name) {
echo "Hej, " . $name . "!";
}
// Anropa funktionen
greet("Anna"); // Output: Hej, Anna!
// Funktion som returnerar ett värde
function add($num1, $num2) {
return $num1 + $num2;
}
$sum = add(10, 5);
echo "\nSumman är: " . $sum; // Output: Summan är: 15
?>
Type Hinting (Typdeklarationer)
Sedan PHP 7 har möjligheten att deklarera förväntade typer för funktionsparametrar och returvärden förbättrats avsevärt. Detta kallas type hinting eller typdeklarationer.
- Parameter Types: Ange förväntad typ före parameternamnet.
- Return Types: Ange förväntad returtyp efter parameterlistan med ett kolon (
:
). - Nullable Types: Om en parameter eller returvärde kan vara antingen den specificerade typen eller
null
, sätt ett frågetecken (?
) före typen (t.ex.?string
). - Union Types (PHP 8+): Tillåter att en parameter eller returvärde kan vara en av flera typer, separerade med
|
(t.ex.int|float
). mixed
Type (PHP 8+): Indikerar att en parameter eller returvärde kan vara av vilken typ som helst.void
Return Type (PHP 7.1+): Indikerar att en funktion inte returnerar något värde.
Om en funktion anropas med fel typ eller returnerar fel typ (och strikta typer är aktiverade, se nedan), kastas ett TypeError
.
<?php
// Aktivera strikta typer (rekommenderas i början av filer)
declare(strict_types=1);
// Funktion med typdeklarationer
function calculateArea(float $width, float $height): float {
if ($width <= 0 || $height <= 0) {
// Kanske kasta ett undantag istället
throw new InvalidArgumentException("Bredd och höjd måste vara större än 0");
}
return $width * $height;
}
$area = calculateArea(10.5, 5.2);
echo "Area: " . $area . "\n"; // Output: Area: 54.6
// $invalidArea = calculateArea(-10.0, 5.0); // Detta ger ett TypeError pga -10.0
// Funktion med nullable return type
function findUser(int $id): ?array { // Kan returnera array eller null
// ... logik för att hämta användare ...
if ($id === 1) {
return ['id' => 1, 'name' => 'Anna'];
}
return null;
}
// Funktion med union type parameter och void return type
function processValue(int|string $input): void {
echo "Bearbetar: " . $input . "\n";
// Returnerar inget
}
processValue(123);
processValue("abc");
?>
declare(strict_types=1);
: Denna deklaration, när den placeras i början av en PHP-fil, aktiverar strikt typläge. I strikt läge accepteras endast värden av exakt den typ som deklarerats (med några få undantag som int
till float
). Utan strikt läge (default), försöker PHP tvångsomvandla (coerce) värden till den förväntade typen om möjligt (t.ex. strängen "10"
skulle accepteras för en int
-parameter). Att använda strikta typer rekommenderas starkt för att fånga fel tidigare och göra koden mer förutsägbar.
Att använda type hinting gör din PHP-kod mer robust, lättare att förstå, och hjälper utvecklingsverktyg (som IDE:er) att ge bättre assistans och felkontroll.
Detta täcker de mest grundläggande syntaxelementen i PHP. Nästa steg är att se hur man interagerar med databaser (sql.md
), hanterar sessioner (sessions.md
) och tänker på säkerhet (security.md
) innan vi bygger vår CRUD-applikation.
Arrayer och Loopar i PHP
Arrayer är en fundamental datastruktur i de flesta programmeringsspråk, och PHP är inget undantag. De låter oss lagra flera värden i en enda variabel. Om du kommer från en JavaScript-bakgrund kommer du att känna igen konceptet, men det finns viktiga skillnader i syntax och funktionalitet i PHP. Detta avsnitt täcker grunderna för PHP-arrayer och hur man itererar (loopar) över dem.
Vad är en Array i PHP?
En array i PHP är en ordnad mappning (en samling) av nyckel-värde-par. Nycklarna kan antingen vara heltal (numeriska index) eller strängar. Värdena kan vara av vilken datatyp som helst, inklusive andra arrayer.
Det finns två huvudsakliga typer av arrayer i PHP:
- Indexerade (Numeriska) Arrayer: Arrayer med numeriska index, vanligtvis startande från 0.
- Associativa Arrayer: Arrayer med namngivna strängnycklar.
Indexerade Arrayer
Dessa liknar mest de "vanliga" arrayerna i JavaScript.
PHP:
<?php
// Skapa en indexerad array (äldre syntax)
$colors_old = array("Röd", "Grön", "Blå");
// Skapa en indexerad array (modern kort syntax, från PHP 5.4+)
$colors = ["Röd", "Grön", "Blå"];
// Lägg till ett element i slutet
$colors[] = "Gul"; // Index 3 tilldelas automatiskt
// Hämta ett element med dess index
echo $colors[0]; // Output: Röd
echo $colors[3]; // Output: Gul
// Ändra ett element
$colors[1] = "Limegrön";
echo $colors[1]; // Output: Limegrön
// Se hela arrayens struktur (bra för debugging)
print_r($colors);
/* Output:
Array
(
[0] => Röd
[1] => Limegrön
[2] => Blå
[3] => Gul
)
*/
// Få antalet element
echo count($colors); // Output: 4
?>
JavaScript (Jämförelse):
// Skapa en array
let colors = ["Röd", "Grön", "Blå"];
// Lägg till ett element i slutet
colors.push("Gul");
// Hämta ett element med dess index
console.log(colors[0]); // Output: Röd
console.log(colors[3]); // Output: Gul
// Ändra ett element
colors[1] = "Limegrön";
console.log(colors[1]); // Output: Limegrön
// Se hela arrayen
console.log(colors); // Output: ["Röd", "Limegrön", "Blå", "Gul"]
// Få antalet element (längden)
console.log(colors.length); // Output: 4
Skillnader att notera:
- Syntax: PHP använder
[]
ellerarray()
för att skapa arrayer, JavaScript använder[]
. - Lägga till element: PHP använder
$array[] = value;
för att lägga till i slutet, JavaScript använderarray.push(value)
. - Storlek: PHP använder funktionen
count()
, JavaScript använderarray.length
-egenskapen.
Associativa Arrayer
Detta är en av PHP:s styrkor. Associativa arrayer låter dig använda meningsfulla strängar som nycklar istället för bara siffror. De liknar JavaScript-objekt som används som dictionaries eller hashmappar.
PHP:
<?php
// Skapa en associativ array
$person = [
"firstName" => "Anna",
"lastName" => "Andersson",
"age" => 30,
"city" => "Stockholm"
];
// Hämta ett värde med dess nyckel
echo $person["firstName"]; // Output: Anna
echo $person["age"]; // Output: 30
// Lägga till ett nytt nyckel-värde-par
$person["email"] = "anna.andersson@example.com";
// Ändra ett värde
$person["city"] = "Göteborg";
// Se hela arrayens struktur
print_r($person);
/* Output:
Array
(
[firstName] => Anna
[lastName] => Andersson
[age] => 30
[city] => Göteborg
[email] => anna.andersson@example.com
)
*/
// Kolla om en nyckel finns
if (isset($person["age"])) {
echo "Ålder finns och är: " . $person["age"];
}
// Ta bort ett element
unset($person["lastName"]);
print_r($person); // lastName är nu borta
?>
JavaScript (Jämförelse med Objekt):
// Skapa ett objekt (motsvarigheten)
let person = {
firstName: "Anna",
lastName: "Andersson",
age: 30,
city: "Stockholm"
};
// Hämta ett värde med dess nyckel (dot notation eller bracket notation)
console.log(person.firstName); // Output: Anna
console.log(person["age"]); // Output: 30
// Lägga till en ny egenskap
person.email = "anna.andersson@example.com";
// Ändra ett värde
person.city = "Göteborg";
// Se hela objektet
console.log(person);
/* Output:
{
firstName: 'Anna',
lastName: 'Andersson',
age: 30,
city: 'Göteborg',
email: 'anna.andersson@example.com'
}
*/
// Kolla om en egenskap finns
if ("age" in person) { // eller person.hasOwnProperty('age')
console.log("Ålder finns och är: " + person.age);
}
// Ta bort en egenskap
delete person.lastName;
console.log(person); // lastName är nu borta
Skillnader att notera:
- Grundtyp: PHP använder sin
array
-typ för båda, JavaScript använderobject
för detta ändamål (även omMap
är ett modernare alternativ för rena key-value-stores). - Syntax: PHP använder
=>
för att associera nyckel och värde, JavaScript använder:
. - Kolla existens: PHP använder
isset()
, JavaScript använderin
-operatorn ellerhasOwnProperty()
. - Ta bort: PHP använder
unset()
, JavaScript använderdelete
.
Loopar över Arrayer
Att iterera över elementen i en array är en mycket vanlig uppgift.
foreach
-loopen (PHP)
Den mest använda och idiomatiska loopen för arrayer i PHP är foreach
. Den fungerar smidigt med både indexerade och associativa arrayer.
För indexerade och associativa (endast värden):
<?php
$colors = ["Röd", "Grön", "Blå"];
foreach ($colors as $color) {
echo $color . "<br>";
}
// Output:
// Röd
// Grön
// Blå
$person = ["firstName" => "Anna", "age" => 30];
foreach ($person as $value) {
echo $value . "<br>";
}
// Output:
// Anna
// 30
?>
För associativa (nyckel och värde):
<?php
$person = [
"firstName" => "Anna",
"lastName" => "Andersson",
"age" => 30
];
foreach ($person as $key => $value) {
echo $key . ": " . $value . "<br>";
}
// Output:
// firstName: Anna
// lastName: Andersson
// age: 30
?>
for
-loopen (PHP)
Kan användas för indexerade arrayer, men är mindre vanlig än foreach
. Kräver att indexen är sekventiella och börjar på 0.
<?php
$colors = ["Röd", "Grön", "Blå"];
$count = count($colors); // Hämta antalet element först
for ($i = 0; $i < $count; $i++) {
echo "Index " . $i . ": " . $colors[$i] . "<br>";
}
// Output:
// Index 0: Röd
// Index 1: Grön
// Index 2: Blå
?>
JavaScript Loopar (Jämförelse)
JavaScript erbjuder flera sätt att loopa:
for...of
(för värden i itererbara objekt som Arrayer):
const colors = ["Röd", "Grön", "Blå"];
for (const color of colors) {
console.log(color);
}
// Output:
// Röd
// Grön
// Blå
forEach
(array-metod):
const colors = ["Röd", "Grön", "Blå"];
colors.forEach(function(color, index) {
console.log(`Index ${index}: ${color}`);
});
// Output:
// Index 0: Röd
// Index 1: Grön
// Index 2: Blå
const person = { firstName: "Anna", age: 30 };
// forEach fungerar inte direkt på vanliga objekt, men på Map eller via Object.entries etc.
Object.entries(person).forEach(([key, value]) => {
console.log(`${key}: ${value}`);
});
// Output:
// firstName: Anna
// age: 30
for...in
(för nycklar/egenskaper i objekt - Används sällan för Arrayer):
const person = { firstName: "Anna", age: 30 };
for (const key in person) {
if (person.hasOwnProperty(key)) { // Viktigt att kolla!
console.log(`${key}: ${person[key]}`);
}
}
// Output:
// firstName: Anna
// age: 30
for
(traditionell loop):
const colors = ["Röd", "Grön", "Blå"];
for (let i = 0; i < colors.length; i++) {
console.log(`Index ${i}: ${colors[i]}`);
}
// Output:
// Index 0: Röd
// Index 1: Grön
// Index 2: Blå
Sammanfattning PHP vs JS Loops:
- PHP:s
foreach
är mycket flexibel och motsvarar närmast en kombination av JSfor...of
(för värden) och iteration överObject.entries()
(för nyckel/värde). - PHP:s
for
är lik JSfor
. - JS
forEach
är en metod på array-prototypen, vilket PHP saknar (PHP använder globala funktioner eller språk-konstruktioner). - JS
for...in
är primärt för objekt-egenskaper och bör användas med försiktighet för arrayer.
Vanliga Array-funktioner i PHP
PHP har ett stort bibliotek av inbyggda funktioner för att manipulera arrayer. Här är några vanliga:
count($array)
: Returnerar antalet element i arrayen. (JS:array.length
)array_push($array, $value1, $value2...)
: Lägger till ett eller flera element i slutet av arrayen. (JS:array.push()
)array_pop($array)
: Tar bort och returnerar det sista elementet från arrayen. (JS:array.pop()
)array_unshift($array, $value1...)
: Lägger till ett eller flera element i början av arrayen. (JS:array.unshift()
)array_shift($array)
: Tar bort och returnerar det första elementet från arrayen. (JS:array.shift()
)in_array($needle, $haystack)
: Kontrollerar om ett värde ($needle
) finns i arrayen ($haystack
). Returnerartrue
ellerfalse
. (JS:array.includes()
)array_key_exists($key, $array)
/isset($array[$key])
: Kontrollerar om en specifik nyckel eller index finns i arrayen.isset
är vanligare och kollar även att värdet inte ärnull
. (JS:key in object
/object.hasOwnProperty(key)
)array_keys($array)
: Returnerar en ny array som innehåller alla nycklar från$array
. (JS:Object.keys()
)array_values($array)
: Returnerar en ny array som innehåller alla värden från$array
, med nya numeriska index. (JS:Object.values()
)sort($array)
: Sorterar en array (värden) i stigande ordning. Modifierar originalarrayen. (JS:array.sort()
)rsort($array)
: Sorterar en array i fallande ordning.asort($array)
: Sorterar en associativ array baserat på värdena, men behåller nyckel-värde-associationerna.ksort($array)
: Sorterar en associativ array baserat på nycklarna, och behåller associationerna.array_merge($array1, $array2...)
: Slår ihop en eller flera arrayer till en ny array. (JS:array1.concat(array2)
eller spread syntax[...array1, ...array2]
)array_slice($array, $offset, $length)
: Extraherar en del av en array. (JS:array.slice()
)array_splice($array, $offset, $length, $replacement)
: Tar bort och/eller ersätter en del av en array. (JS:array.splice()
)implode($glue, $pieces)
: Fogar ihop elementen i en array ($pieces
) till en sträng, separerade av$glue
. (JS:array.join()
)explode($delimiter, $string)
: Delar upp en sträng till en array baserat på en avgränsare ($delimiter
). (JS:string.split()
)
Exempel:
<?php
$fruits = ["äpple", "banan", "päron"];
echo "Antal frukter: " . count($fruits) . "<br>"; // 3
array_push($fruits, "apelsin");
print_r($fruits); // Array ( [0] => äpple [1] => banan [2] => päron [3] => apelsin )
echo "<br>";
if (in_array("banan", $fruits)) {
echo "Banan finns i arrayen<br>";
}
$person = ["namn" => "Erik", "stad" => "Malmö"];
$keys = array_keys($person);
print_r($keys); // Array ( [0] => namn [1] => stad )
echo "<br>";
$values = array_values($person);
print_r($values); // Array ( [0] => Erik [1] => Malmö )
echo "<br>";
$fruit_string = implode(", ", $fruits);
echo $fruit_string; // äpple, banan, päron, apelsin
?>
Arrayer och loopar är centrala delar av PHP-programmering, särskilt vid hantering av data från databaser eller formulär. Att förstå både indexerade och associativa arrayer samt hur foreach
-loopen fungerar är avgörande.
PHP klasser
Introduktion till Klasser i PHP
I tidigare avsnitt har vi sett hur vi kan använda associativa arrayer för att representera strukturerad data, som till exempel en användare eller en produkt:
<?php
$userArray = [
'username' => 'kalleanka',
'email' => 'kalle@example.com',
'lastLogin' => '2024-04-10 10:30:00'
];
echo "Användarnamn: " . $userArray['username'];
?>
Detta fungerar bra för enkla fall, men när applikationer växer stöter vi på begränsningar:
- Ingen garanterad struktur: PHP hindrar oss inte från att stava fel på en nyckel (
$userArray['emial'] = ...
) eller glömma att lägga till en viktig nyckel. Det finns ingen fast "mall" för hur en användar-array ska se ut. - Ingen datatypvalidering: Vi kan råka lägga in en siffra där en sträng förväntas, eller tvärtom.
- Separation av data och beteende: Om vi vill utföra operationer relaterade till användaren (t.ex. kontrollera om lösenordet är giltigt, uppdatera e-postadressen, skicka ett välkomstmail), måste vi skriva separata funktioner som tar användar-arrayen som parameter. Datan (arrayen) och logiken (funktionerna) är inte direkt kopplade.
För att lösa dessa problem introducerar PHP, likt många andra moderna språk, konceptet Object-Oriented Programming (OOP), och dess kärna är klasser (classes) och objekt (objects).
Vad är en Klass?
En klass är en mall eller ritning för att skapa objekt. Den definierar:
- Properties (Egenskaper): Variabler som tillhör klassen och lagrar data för objekten (t.ex.
$username
,$email
). - Methods (Metoder): Funktioner som tillhör klassen och definierar beteendet eller operationerna som objekten kan utföra (t.ex.
login()
,updateProfile()
).
Vad är ett Objekt?
Ett objekt är en instans av en klass. Det är en konkret representation skapad från klassens mall, med sina egna värden för egenskaperna.
Liknelse: Tänk på en pepparkaksform (klassen GingerbreadMan
). Formen definierar egenskaperna (armar, ben, huvud) och potentiella metoder (kan dekoreras). Varje enskild pepparkaka du stansar ut med formen är ett objekt – en instans av klassen GingerbreadMan
. Varje pepparkaka (objekt) kan ha olika dekoration (olika värden för sina egenskaper).
Definiera en Enkel Klass
Vi använder class
-nyckelordet följt av klassnamnet (ofta med stor bokstav i början, PascalCase).
<?php
declare(strict_types=1); // Bra att använda med klasser!
class User
{
// Properties (Egenskaper) - Variabler inuti klassen
public string $username;
public string $email;
public ?DateTime $lastLogin = null; // Kan vara DateTime eller null
// Methods (Metoder) - Funktioner inuti klassen
public function displayGreeting(): void
{
echo "Hej, " . $this->username . "!";
}
}
?>
Förklaring:
class User { ... }
: Definierar en klass med namnetUser
.public string $username;
: Definierar en egenskap (property) som heterusername
.public
: Detta är en visibility modifier (synlighetskontroll).public
betyder att egenskapen kan nås och ändras direkt utifrån objektet.string
: Detta är type hinting för egenskapen (sedan PHP 7.4). Det specificerar att$username
förväntas innehålla en sträng.?DateTime $lastLogin = null;
: EgenskapenlastLogin
kan antingen innehålla ettDateTime
-objekt eller varanull
. Den ges ett standardvärdenull
.
public function displayGreeting(): void { ... }
: Definierar en metod (method) som heterdisplayGreeting
.public
: Metoden kan anropas utifrån objektet.(): void
: Metoden tar inga argument och returnerar inget (void
).$this->username
: Inuti en metod refererar den speciella variabeln$this
till det aktuella objektet (den specifika instansen av klassen). Vi använder->
(objektoperatorn) för att komma åt objektets egenskaper ($username
i detta fall).
Skapa och Använda Objekt
Vi skapar ett objekt (en instans) från klassen med new
-nyckelordet.
<?php
// Inkludera eller definiera User-klassen här...
require_once 'User.php'; // Antag att klassen ligger i User.php
// Skapa ett nytt User-objekt (en instans av User-klassen)
$user1 = new User();
// Sätta värden på (publika) egenskaper med ->
$user1->username = 'kalleanka';
$user1->email = 'kalle@example.com';
$user1->lastLogin = new DateTime('2024-04-10 10:30:00');
// Hämta värden från egenskaper
echo "Användarnamn: " . $user1->username . "\n"; // Output: Användarnamn: kalleanka
// Anropa en metod på objektet
$user1->displayGreeting(); // Output: Hej, kalleanka!
echo "\n";
// Skapa ett annat User-objekt
$user2 = new User();
$user2->username = 'mussepigg';
$user2->email = 'musse@example.com';
$user2->displayGreeting(); // Output: Hej, mussepigg!
echo "\nAnvändare 1 E-post: " . $user1->email; // Output: Användare 1 E-post: kalle@example.com
echo "\nAnvändare 2 E-post: " . $user2->email; // Output: Användare 2 E-post: musse@example.com
?>
Vi ser här att $user1
och $user2
är separata objekt, även om de är skapade från samma klass. De har sina egna kopior av egenskaperna.
Konstruktorer (__construct
)
Det är ofta opraktiskt att behöva sätta alla egenskaper manuellt efter att objektet har skapats. En konstruktor är en speciell metod som anropas automatiskt när ett nytt objekt skapas med new
.
Konstruktorn heter alltid __construct
(två understreck).
<?php
declare(strict_types=1);
class Product
{
public string $name;
public float $price;
public int $stock;
// Konstruktor-metod
public function __construct(string $productName, float $productPrice, int $initialStock = 0)
{
echo "-- Skapar nytt Product objekt --\n";
$this->name = $productName;
$this->price = $productPrice;
$this->stock = $initialStock;
}
public function displayInfo(): void
{
echo "Produkt: {$this->name}, Pris: {$this->price} kr, Lager: {$this->stock} st\n";
}
}
// Skapa objekt och skicka med argument till konstruktorn
$product1 = new Product('Tangentbord', 499.0, 50);
$product2 = new Product('Mus', 249.50); // Använder defaultvärdet 0 för $initialStock
$product1->displayInfo(); // Output: Produkt: Tangentbord, Pris: 499 kr, Lager: 50 st
$product2->displayInfo(); // Output: Produkt: Mus, Pris: 249.5 kr, Lager: 0 st
?>
Konstruktorer används för att initialisera objektets tillstånd direkt när det skapas, vilket ofta gör koden renare och säkrare.
Synlighet: public
, private
, protected
PHP tillåter oss att kontrollera hur egenskaper och metoder kan nås.
public
: Kan nås och anropas varifrån som helst (utifrån objektet, inifrån klassen, från ärvande klasser).private
: Kan endast nås och anropas inifrån samma klass där den är definierad. Inte ens ärvande klasser kommer åt den.protected
: Kan nås och anropas inifrån samma klass och från klasser som ärver den, men inte direkt utifrån objektet.
Varför inte bara göra allt public
? Att använda private
(och protected
) är en viktig del av encapsulation (inkapsling). Det hjälper till att:
- Dölja implementationen: Hur klassen fungerar internt behöver inte exponeras utåt.
- Kontrollera åtkomst: Du kan säkerställa att egenskaper bara ändras på ett kontrollerat sätt via metoder (getters/setters).
- Förbättra underhåll: Om interna detaljer är
private
, kan du ändra dem utan att riskera att koden som använder klassen går sönder (så länge de publika metoderna fungerar likadant).
Exempel med private
och Getters/Setters:
<?php
declare(strict_types=1);
class BankAccount
{
private string $accountNumber;
private float $balance;
public function __construct(string $number, float $initialBalance = 0.0)
{
$this->accountNumber = $number;
// Validera initialt saldo
if ($initialBalance < 0) {
$this->balance = 0.0;
} else {
$this->balance = $initialBalance;
}
}
// Public Metod för att sätta in pengar (en "setter" för balansen, men med logik)
public function deposit(float $amount): void
{
if ($amount > 0) {
$this->balance += $amount;
} else {
echo "Insättningsbeloppet måste vara positivt.\n";
}
}
// Public Metod för att ta ut pengar (en "setter" med logik)
public function withdraw(float $amount): bool
{
if ($amount <= 0) {
echo "Uttagsbeloppet måste vara positivt.\n";
return false;
}
if ($amount > $this->balance) {
echo "Inte tillräckligt med pengar på kontot.\n";
return false;
}
$this->balance -= $amount;
return true;
}
// Public Metod för att hämta saldot (en "getter")
public function getBalance(): float
{
return $this->balance;
}
// Public Metod för att hämta kontonumret (en "getter")
public function getAccountNumber(): string
{
return $this->accountNumber;
}
}
$myAccount = new BankAccount('123-4567', 1000.0);
// echo $myAccount->balance; // Fatal error! Kan inte komma åt private property
echo "Saldo: " . $myAccount->getBalance() . " kr\n"; // Output: Saldo: 1000 kr
$myAccount->deposit(500.0);
echo "Nytt saldo: " . $myAccount->getBalance() . " kr\n"; // Output: Nytt saldo: 1500 kr
$myAccount->withdraw(2000.0); // Output: Inte tillräckligt med pengar på kontot.
echo "Saldo efter misslyckat uttag: " . $myAccount->getBalance() . " kr\n"; // Output: Saldo efter misslyckat uttag: 1500 kr
$success = $myAccount->withdraw(300.0);
if ($success) {
echo "Saldo efter lyckat uttag: " . $myAccount->getBalance() . " kr\n"; // Output: Saldo efter lyckat uttag: 1200 kr
}
?>
I exemplet ovan är $balance
och $accountNumber
private
. Vi kan inte läsa eller ändra dem direkt utifrån. Istället måste vi använda de public
metoderna (deposit
, withdraw
, getBalance
, getAccountNumber
). Detta ger klassen full kontroll över hur saldot hanteras (t.ex. att man inte kan sätta in negativa belopp eller ta ut mer än vad som finns).
Fördelar med Klasser jämfört med Associativa Arrayer
- Struktur och Tydlighet: En klass definierar en tydlig struktur för data.
- Inkapsling: Kombinerar data (egenskaper) och beteende (metoder) som hör ihop.
- Kontroll: Synlighetskontroller (
private
,protected
) ger bättre kontroll över hur data nås och modifieras. - Återanvändbarhet: Klasser kan återanvändas för att skapa många objekt.
- Underhåll: Ändringar i klassens interna implementation (om den är väl inkapslad) påverkar inte koden som använder klassen lika mycket.
- Type Hinting: Möjliggör starkare typkontroll för både egenskaper och metodparametrar/returvärden.
Även om det kan verka som mer kod att skriva en klass initialt jämfört med en associativ array, lönar det sig snabbt i större och mer komplexa applikationer genom ökad struktur, säkerhet och underhållbarhet.
Detta är bara en introduktion till klasser. OOP i PHP inkluderar många fler koncept som arv, interfaces, traits, statiska metoder/egenskaper, m.m., men grunderna som täcks här är de mest väsentliga att börja med.
Introduktion till SQL och Relationsdatabaser
När vi bygger webbapplikationer behöver vi nästan alltid ett sätt att lagra, organisera och hämta data på ett beständigt sätt. Det kan vara användarinformation, produktlistor, blogginlägg eller vad som helst som applikationen behöver komma ihåg mellan besök och över tid. Här kommer databaser in i bilden.
En database (databas) är en organiserad samling av data. Det finns olika typer av databaser, men för många traditionella webbapplikationer är Relational Database Management Systems (Relationsdatabashanteringssystem, RDBMS) det vanligaste valet. Exempel på populära RDBMS inkluderar MySQL, MariaDB, PostgreSQL, SQLite och Microsoft SQL Server. I det här kapitlet fokuserar vi på MariaDB (som är mycket likt MySQL), eftersom det fungerar bra ihop med PHP.
Kärnan i en relationsdatabas är konceptet med tables (tabeller). En tabell organiserar data i:
- Rows (Rader): Varje rad representerar en enskild post eller ett objekt (t.ex. en specifik användare, en produkt).
- Columns (Kolumner): Varje kolumn representerar ett specifikt attribut eller egenskap för posterna (t.ex. användarens namn, produktens pris).
- Relations (Relationer): Data i olika tabeller kan kopplas samman baserat på gemensamma värden, vilket gör att vi kan kombinera information (t.ex. koppla en order till den användare som lade den).
För att kommunicera med ett RDBMS – för att definiera tabellstrukturer, lägga till, ändra, ta bort och hämta data – använder vi ett standardiserat språk som heter SQL (Structured Query Language, Uttalas ofta "Sequel" eller S-Q-L).
Grundläggande SQL-Syntax
SQL består av olika statements (satser eller kommandon) som talar om för databasen vad den ska göra. Varje sats avslutas vanligtvis med ett semikolon (;
), även om det ibland är valfritt beroende på verktyget man använder.
Viktiga nyckelord skrivs ofta med versaler av konvention (för läsbarhet), men SQL är generellt inte skiftlägeskänsligt för nyckelord. Tabell- och kolumnnamn kan dock vara skiftlägeskänsliga beroende på databasens konfiguration och operativsystemet.
Vanliga SQL-nyckelord inkluderar:
SELECT
: Hämta data.INSERT INTO
: Lägga till nya rader.UPDATE
: Ändra befintliga rader.DELETE
: Ta bort rader.CREATE TABLE
: Skapa en ny tabell.ALTER TABLE
: Ändra en befintlig tabell.DROP TABLE
: Ta bort en tabell.FROM
: Anger vilken tabell data ska hämtas från.WHERE
: Filtrerar vilka rader som ska påverkas.VALUES
: Anger de värden som ska infogas.SET
: Anger vilka kolumner och värden som ska uppdateras.
Kommentarer i SQL skrivs antingen med två bindestreck (--
) för en radskommentar, eller mellan /*
och */
för flerradskommentarer.
-- Detta är en enradskommentar
SELECT name, email -- Hämta namn och e-post
FROM users
WHERE city = 'Stockholm'; /* Detta är en
flerradskommentar */
Datatyper i SQL
När vi skapar en tabell måste vi specificera vilken typ av data varje kolumn ska innehålla. Detta kallas data type (datatyp). Att välja rätt datatyp är viktigt för:
- Data Integrity (Dataintegritet): Säkerställer att endast giltig data lagras (t.ex. inga bokstäver i en sifferkolumn).
- Storage Efficiency (Lagringseffektivitet): Olika datatyper tar olika mycket plats.
- Performance (Prestanda): Korrekta datatyper kan snabba upp sökningar och jämförelser.
Här är några vanliga datatyper i SQL (specifika namn kan variera något mellan olika RDBMS, men koncepten är desamma):
- Heltal:
INT
ellerINTEGER
: Standard heltal (ofta 4 bytes).TINYINT
: Mycket litet heltal (ofta 1 byte, -128 till 127 eller 0 till 255).BIGINT
: Stort heltal (ofta 8 bytes).
- Decimaltal:
DECIMAL(precision, scale)
: För exakta tal med fast antal decimaler (t.ex.DECIMAL(10, 2)
för valuta).precision
är totalt antal siffror,scale
är antal decimaler.FLOAT
,DOUBLE
: Flyttal för approximativa värden (undviks ofta för valuta).
- Strängar (Text):
VARCHAR(n)
: Textsträng med variabel längd upp tilln
tecken.TEXT
: För längre textstycken utan specifik maxlängd (men det finns gränser).CHAR(n)
: Textsträng med fast längd pån
tecken (fylls ut med mellanslag om kortare).
- Datum och Tid:
DATE
: Endast datum (YYYY-MM-DD).TIME
: Endast tid (HH:MM:SS).DATETIME
: Både datum och tid (YYYY-MM-DD HH:MM:SS).TIMESTAMP
: LiknarDATETIME
men används ofta för att automatiskt spåra när en rad skapades eller senast ändrades.
- Boolean (Logiskt värde): SQL har ingen standard
BOOLEAN
-typ. Istället används oftaTINYINT(1)
där0
representerarfalse
och1
representerartrue
.
En kolumn kan också tillåta NULL
-värden, vilket betyder att värdet är okänt eller saknas. Motsatsen är att definiera kolumnen med NOT NULL
, vilket kräver att ett värde alltid anges.
Skapa och Hantera Tabeller (DDL - Data Definition Language)
SQL-satser som används för att definiera eller ändra strukturen på databasobjekt (som tabeller) kallas DDL (Data Definition Language).
CREATE TABLE
Används för att skapa en ny tabell.
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY, -- Unik identifierare för varje användare
name VARCHAR(100) NOT NULL, -- Användarens namn (max 100 tecken, får ej vara tomt)
email VARCHAR(150) NOT NULL UNIQUE, -- E-post (max 150 tecken, får ej vara tomt, måste vara unikt)
city VARCHAR(50), -- Stad (max 50 tecken, får vara tomt - NULL tillåtet)
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -- Tidsstämpel när raden skapades
);
Förklaringar:
CREATE TABLE users (...)
: Startar definitionen av en tabell med namnetusers
.id INT AUTO_INCREMENT PRIMARY KEY
: Definierar en kolumnid
av typenINT
.AUTO_INCREMENT
gör att databasen automatiskt tilldelar ett nytt, unikt nummer för varje ny rad.PRIMARY KEY
(Primärnyckel) markerar denna kolumn som den unika identifieraren för varje rad i tabellen. Ingen rad får ha sammaid
, ochid
får inte varaNULL
.name VARCHAR(100) NOT NULL
: En textkolumn för namn, max 100 tecken.NOT NULL
betyder att denna kolumn måste ha ett värde.email VARCHAR(150) NOT NULL UNIQUE
: En textkolumn för e-post.UNIQUE
säkerställer att ingen annan rad i tabellen kan ha samma e-postadress.city VARCHAR(50)
: En textkolumn för stad. EftersomNOT NULL
saknas, tillåtsNULL
-värden här.created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
: En tidsstämpelkolumn.DEFAULT CURRENT_TIMESTAMP
betyder att om inget värde anges vidINSERT
, sätts kolumnen automatiskt till den aktuella tidpunkten.
ALTER TABLE
Används för att modifiera en befintlig tabell.
-- Lägg till en ny kolumn 'phone_number'
ALTER TABLE users
ADD COLUMN phone_number VARCHAR(20);
-- Ändra datatypen för 'city'-kolumnen
ALTER TABLE users
MODIFY COLUMN city VARCHAR(60);
-- Ta bort 'phone_number'-kolumnen
ALTER TABLE users
DROP COLUMN phone_number;
DROP TABLE
Används för att permanent ta bort en tabell och all dess data. Använd med extrem försiktighet!
DROP TABLE users;
Not: I moderna projekt används ofta migrationsverktyg (som Phinx, Doctrine Migrations, eller inbyggda i ramverk som Laravel) för att hantera databasstrukturförändringar på ett kontrollerat och versionshanterat sätt, istället för att köra ALTER TABLE
manuellt.
Hämta Data (SELECT
)
Den absolut vanligaste SQL-satsen är SELECT
, som används för att hämta data från en eller flera tabeller. Detta är en del av DML (Data Manipulation Language).
Grundläggande SELECT
-- Hämta specifika kolumner från tabellen 'users'
SELECT name, email
FROM users;
-- Hämta alla kolumner (* är en wildcard för alla)
SELECT *
FROM users;
Filtrera Rader (WHERE
)
WHERE
-klausulen används för att specificera villkor som raderna måste uppfylla för att inkluderas i resultatet.
-- Hämta användare från Stockholm
SELECT name, email
FROM users
WHERE city = 'Stockholm';
-- Hämta användare som INTE är från Stockholm
SELECT name, email
FROM users
WHERE city != 'Stockholm'; -- Eller city <> 'Stockholm'
-- Hämta användare vars id är större än 10
SELECT id, name
FROM users
WHERE id > 10;
-- Hämta användare från Stockholm ELLER Göteborg
SELECT name, city
FROM users
WHERE city = 'Stockholm' OR city = 'Göteborg';
-- Alternativt med IN
SELECT name, city
FROM users
WHERE city IN ('Stockholm', 'Göteborg');
-- Hämta användare vars namn börjar på 'A'
-- % är en wildcard som matchar noll eller flera tecken
SELECT name
FROM users
WHERE name LIKE 'A%';
-- Hämta användare vars namn slutar på 'son'
SELECT name
FROM users
WHERE name LIKE '%son';
-- Hämta användare vars namn innehåller 'an'
SELECT name
FROM users
WHERE name LIKE '%an%';
-- Hämta användare där staden inte är angiven (är NULL)
SELECT name
FROM users
WHERE city IS NULL;
-- Hämta användare där staden ÄR angiven (inte NULL)
SELECT name, city
FROM users
WHERE city IS NOT NULL;
-- Hämta användare från Stockholm som heter Anna
SELECT name, email, city
FROM users
WHERE city = 'Stockholm' AND name = 'Anna';
Sortera Resultat (ORDER BY
)
ORDER BY
används för att sortera resultatet baserat på en eller flera kolumner.
-- Hämta alla användare sorterade efter namn i bokstavsordning (stigande, ASC är default)
SELECT name, email
FROM users
ORDER BY name ASC; -- ASC kan utelämnas
-- Hämta alla användare sorterade efter id i fallande ordning (högst id först)
SELECT id, name
FROM users
ORDER BY id DESC;
-- Sortera först på stad, sedan på namn inom varje stad
SELECT name, city
FROM users
ORDER BY city, name;
Begränsa Antal Rader (LIMIT
)
LIMIT
används för att begränsa antalet rader som returneras. Används ofta för paginering (att visa data sida för sida).
-- Hämta de 5 första användarna (baserat på default ordning eller ORDER BY)
SELECT id, name
FROM users
ORDER BY id
LIMIT 5;
-- Hämta 10 användare, men hoppa över de första 20 (används för paginering, sida 3 om 10 per sida)
-- LIMIT offset, count
SELECT id, name
FROM users
ORDER BY id
LIMIT 20, 10;
Lägga till Data (INSERT INTO
)
INSERT INTO
används för att lägga till nya rader i en tabell.
-- Lägg till en ny användare, specificera kolumner
INSERT INTO users (name, email, city)
VALUES ('Kalle Anka', 'kalle@example.com', 'Ankeborg');
-- Lägg till en ny användare utan stad (om 'city' tillåter NULL)
INSERT INTO users (name, email)
VALUES ('Musse Pigg', 'musse@example.com');
-- Lägg till flera användare samtidigt (syntax kan variera lite)
INSERT INTO users (name, email, city)
VALUES
('Långben', 'langben@example.com', 'Ankeborg'),
('Joakim von Anka', 'joakim@example.com', NULL);
Not: Om du har en AUTO_INCREMENT
-kolumn (som id
i vårt exempel) behöver du inte (och ska oftast inte) ange den i INSERT
-satsen. Databasen sköter det automatiskt.
Uppdatera Data (UPDATE
)
UPDATE
används för att ändra data i befintliga rader.
-- Uppdatera staden för användaren med id 5
UPDATE users
SET city = 'Göteborg'
WHERE id = 5;
-- Uppdatera både namn och e-post för användaren med id 10
UPDATE users
SET name = 'Kajsa Anka', email = 'kajsa.anka@example.com'
WHERE id = 10;
-- Uppdatera ALLA användare (MYCKET FARLIGT UTAN WHERE!)
-- UPDATE users SET city = 'Okänd stad'; -- Gör INTE detta utan att vara säker!
VARNING: WHERE
-klausulen i UPDATE
är extremt viktig. Utan den kommer alla rader i tabellen att uppdateras!
Ta bort Data (DELETE
)
DELETE
används för att ta bort rader från en tabell.
-- Ta bort användaren med id 15
DELETE FROM users
WHERE id = 15;
-- Ta bort alla användare från Ankeborg
DELETE FROM users
WHERE city = 'Ankeborg';
-- Ta bort ALLA användare (MYCKET FARLIGT UTAN WHERE!)
-- DELETE FROM users; -- Gör INTE detta utan att vara säker!
VARNING: Precis som med UPDATE
, är WHERE
-klausulen i DELETE
kritisk. Utan den raderas alla rader i tabellen!
Sammanfogning av Tabeller (JOIN
)
Ofta är den data vi behöver spridd över flera relaterade tabeller. Till exempel kanske vi har en users
-tabell och en orders
-tabell, där orders
-tabellen innehåller en referens till användaren som lade ordern.
För att kombinera data från flera tabeller i en enda fråga använder vi JOIN
.
Låt oss anta att vi har tabellerna:
users
id | name |
---|---|
1 | Alice |
2 | Bob |
3 | Charlie |
orders
order_id | user_id | product |
---|---|---|
101 | 1 | Laptop |
102 | 2 | Keyboard |
103 | 1 | Mouse |
104 | 4 | Monitor |
Observera att orders.user_id
refererar till users.id
. Användare med id 3
har inga ordrar, och order 104
tillhör en användare (4
) som inte finns i users
-tabellen (i detta exempel).
INNER JOIN
Den vanligaste typen är INNER JOIN
. Den returnerar endast rader där det finns en matchning i båda tabellerna baserat på JOIN
-villkoret.
SELECT
users.name, -- Hämta användarens namn
orders.product -- Hämta produktnamnet från ordern
FROM
users -- Börja med users-tabellen
INNER JOIN
orders -- Koppla ihop med orders-tabellen
ON
users.id = orders.user_id; -- Villkoret för kopplingen
Resultat:
name | product |
---|---|
Alice | Laptop |
Bob | Keyboard |
Alice | Mouse |
- Alice (id 1) finns i båda tabellerna via
user_id
1 iorders
(två gånger). - Bob (id 2) finns i båda tabellerna via
user_id
2 iorders
. - Charlie (id 3) finns bara i
users
, så han inkluderas inte. - Order 104 (user_id 4) har ingen matchande användare i
users
, så den inkluderas inte.
Visualisering av INNER JOIN
:
graph LR subgraph Users Table U1["id: 1<br>name: 'Alice'"] U2["id: 2<br>name: 'Bob'"] U3["id: 3<br>name: 'Charlie'"] end subgraph Orders Table O1["id: 101<br>user_id: 1<br>product: 'Laptop'"] O2["id: 102<br>user_id: 2<br>product: 'Keyboard'"] O3["id: 103<br>user_id: 1<br>product: 'Mouse'"] O4["id: 104<br>user_id: 4<br>product: 'Monitor'"] end subgraph INNER JOIN Result (on users.id = orders.user_id) R1["user.name: 'Alice'<br>order.product: 'Laptop'"] R2["user.name: 'Bob'<br>order.product: 'Keyboard'"] R3["user.name: 'Alice'<br>order.product: 'Mouse'"] end U1 -- "match" --> R1 U1 -- "match" --> R3 U2 -- "match" --> R2 O1 -- "match" --> R1 O2 -- "match" --> R2 O3 -- "match" --> R3 style U3 fill:#f9f,stroke:#333,stroke-width:2px style O4 fill:#f9f,stroke:#333,stroke-width:2px
(De rosa noderna representerar rader som inte inkluderas i INNER JOIN
-resultatet.)
Andra JOIN
-typer (Kort)
LEFT JOIN
: Returnerar alla rader från den vänstra tabellen (users
i exemplet ovan) och matchande rader från den högra (orders
). Om ingen matchning finns i den högra tabellen, fylls dess kolumner ut medNULL
. (Charlie skulle komma med, men medNULL
i produktkolumnen).RIGHT JOIN
: Motsatsen tillLEFT JOIN
. Returnerar alla rader från den högra tabellen och matchande från vänstra. (Order 104 skulle komma med, men medNULL
i namnkolumnen).FULL OUTER JOIN
: Returnerar alla rader från båda tabellerna. Om matchning saknas fylls kolumnerna från den andra tabellen ut medNULL
. (Stöds inte direkt av MySQL/MariaDB, men kan simuleras).
SQL och PHP (Förhandstitt)
Nu när du har en grundläggande förståelse för SQL-satser är nästa steg att se hur vi kan exekvera dessa från vår PHP-kod. PHP erbjuder olika sätt att ansluta till och interagera med databaser som MariaDB/MySQL:
- PDO (PHP Data Objects): Ett databasabstraktionslager som ger ett konsekvent gränssnitt för att arbeta med olika databastyper.
- MySQLi (MySQL Improved Extension): En specifik extension för att arbeta med MySQL och MariaDB.
Vi kommer jobba främst med PDO.
I de kommande avsnittet crud-app.md
kommer vi att dyka djupare in i hur man använder PDO för att:
- Ansluta till databasen från PHP.
- Skicka SQL-frågor (
SELECT
,INSERT
,UPDATE
,DELETE
) till databasen. - Hantera resultaten från
SELECT
-frågor. - Skydda sig mot SQL Injection (SQL-injektion), en allvarlig säkerhetsrisk, med hjälp av Prepared Statements (förberedda uttryck).
Att kunna SQL är en grundläggande färdighet för fullstack-utveckling, eftersom det låter dig interagera med den data som driver din applikation.
Bygga en CRUD-applikation: Enkel Blogg
I detta avsnitt bygger vi en komplett men enkel bloggapplikation från grunden med PHP och MariaDB/MySQL. Målet är att praktiskt demonstrera CRUD-operationerna (Create, Read, Update, Delete) och integrera andra viktiga webbkoncept som databasinteraktion med PDO, användarautentisering, sessionshantering och filuppladdning.
Vi börjar med en mycket grundläggande struktur och kodstil för att sedan i slutet refaktorera och förbättra koden, bland annat genom att introducera type hinting.
Applikationens Funktioner:
- Användare kan registrera sig och logga in.
- Inloggade användare kan skapa, redigera och ta bort sina egna blogginlägg.
- Användare kan ladda upp en bild till varje blogginlägg.
- Alla besökare kan se listan över blogginlägg och läsa enskilda inlägg.
- En enkel "admin"-sektion för inloggade användare att hantera sina inlägg.
1. Databasdesign
Vi behöver två huvudtabeller: en för användare (users
) och en för blogginlägg (posts
).
users
tabell:
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL, -- Lagrar hashat lösenord
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
id
: Unik identifierare för användaren.username
: Användarnamn för inloggning (unikt).email
: Användarens e-post (unik).password_hash
: Lagrar det säkert hashade lösenordet (inte lösenordet i klartext!).created_at
: När användarkontot skapades.
posts
tabell:
CREATE TABLE posts (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL, -- Vem som skrev inlägget
title VARCHAR(255) NOT NULL, -- Inläggets titel
body TEXT NOT NULL, -- Innehållet i inlägget
image_path VARCHAR(255) NULL, -- Sökväg till uppladdad bild (valfritt)
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- När inlägget senast ändrades
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE -- Koppling till users. Om användaren raderas, raderas även hens inlägg.
);
id
: Unik identifierare för inlägget.user_id
: Referens tillid
iusers
-tabellen. Visar vem som äger inlägget.title
: Inläggets rubrik.body
: Brödtexten i inlägget.image_path
: Sökvägen till den associerade bilden (om någon laddats upp).NULL
om ingen bild finns.created_at
: När inlägget skapades.updated_at
: Uppdateras automatiskt när inlägget ändras.FOREIGN KEY ...
: Definierar en relation mellanposts.user_id
ochusers.id
.ON DELETE CASCADE
betyder att om en användare tas bort, tas alla dennes inlägg också bort automatiskt.
Skapa dessa tabeller i din MariaDB/MySQL-databas (t.ex. via phpMyAdmin eller kommandoraden). Vi antar att databasen heter db_fullstack
som i tidigare exempel.
2. Projektstruktur (Initial)
Vi börjar med en väldigt platt och enkel filstruktur. Skapa följande filer och mappar i roten av ditt projekt (t.ex. i app/public
om du följer docker-compose
-exemplet):
.
├── admin/
│ ├── index.php # Admin dashboard (lista inlägg, länkar till create/edit/delete)
│ ├── create_post.php # Formulär & logik för att skapa inlägg
│ ├── edit_post.php # Formulär & logik för att redigera inlägg
│ └── delete_post.php # Logik för att ta bort inlägg
├── includes/
│ ├── config.php # Databasuppgifter och annan konfiguration
│ ├── database.php # Funktion för att ansluta till databasen (PDO)
│ └── functions.php # Hjälpfunktioner (vi lägger till här senare)
├── uploads/ # Mapp där uppladdade bilder sparas (måste vara skrivbar för webbservern!)
├── index.php # Hemsida, listar blogginlägg
├── login.php # Inloggningsformulär & logik
├── logout.php # Logik för att logga ut
├── post.php # Visar ett enskilt blogginlägg
└── register.php # Registreringsformulär & logik
admin/
: Innehåller sidor som endast inloggade användare ska kunna nå.includes/
: Innehåller återanvändbar kod som konfiguration och databasanslutning.uploads/
: Här hamnar bilder som användare laddar upp. VIKTIGT: Se till att webbservern (Apache i vårt Docker-exempel) har skrivrättigheter till denna mapp!
3. Grundläggande Setup
Konfiguration (includes/config.php
)
Skapa filen includes/config.php
och lägg in dina databasuppgifter.
<?php
// includes/config.php
// Databasuppgifter (anpassa efter din miljö)
define('DB_HOST', 'mysql'); // Matchar service-namnet i docker-compose.yml
define('DB_NAME', 'db_fullstack');
define('DB_USER', 'db_user');
define('DB_PASS', 'db_password');
// Teckenkodning för PDO-anslutningen
define('DB_CHARSET', 'utf8mb4');
// Starta sessioner (viktigt för login!)
// Görs en gång här så det gäller alla sidor som inkluderar config.php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// Base URL (valfritt, men kan vara praktiskt för länkar)
// Anpassa port om du använder en annan än 8060
define('BASE_URL', 'http://localhost:8060');
// Sökväg till uppladdningsmappen
define('UPLOAD_PATH', __DIR__ . '/../uploads/'); // __DIR__ ger sökvägen till includes/
// Aktivera felrapportering under utveckling
// Stäng av på en produktionsserver!
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
?>
Databasanslutning (includes/database.php
)
Skapa filen includes/database.php
. Vi använder PDO (PHP Data Objects) för att ansluta. PDO ger ett konsekvent sätt att interagera med olika databaser och hjälper till att skydda mot SQL-injektion genom "prepared statements".
<?php
// includes/database.php
require_once 'config.php'; // Inkludera konfigurationen
/**
* Skapar och returnerar en PDO-databasanslutning.
*
* @return PDO PDO-anslutningsobjektet.
* @throws PDOException Om anslutningen misslyckas.
*/
function connect_db(): PDO {
// Data Source Name (DSN)
$dsn = "mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=" . DB_CHARSET;
// Alternativ för PDO-anslutningen
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // Kasta exceptions vid fel
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // Hämta resultat som associativa arrayer
PDO::ATTR_EMULATE_PREPARES => false, // Använd prepared statements
];
try {
$pdo = new PDO($dsn, DB_USER, DB_PASS, $options);
return $pdo;
} catch (PDOException $e) {
// Logga felet istället för att skriva ut känslig info
error_log("Database Connection Error: " . $e->getMessage());
// Visa ett generellt felmeddelande till användaren
throw new PDOException("Kunde inte ansluta till databasen. Försök igen senare.", (int)$e->getCode());
// Eller: die("Kunde inte ansluta till databasen. Kontakta administratör.");
}
}
// För att använda anslutningen på en sida:
// $pdo = connect_db();
?>
Förklaring:
- Vi inkluderar
config.php
för att få databasuppgifterna. $dsn
: En sträng som specificerar databastyp, värd, databasnamn och teckenkodning.$options
: Viktiga inställningar för PDO:PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
: Gör att PDO kastar ett undantag (PDOException
) om ett databasfel inträffar. Detta är att föredra framför att bara fåfalse
eller varningar, då det ger tydligare felhantering.PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
: Gör att när vi hämtar data (fetch()
,fetchAll()
) får vi resultatet som en associativ array (kolumnnamn => värde) istället för både numeriska index och kolumnnamn (vilket är defaultPDO::FETCH_BOTH
).PDO::ATTR_EMULATE_PREPARES => false
: Tvingar PDO att använda databasserverns inbyggda "prepared statements" istället för att emulera dem i PHP. Detta är generellt säkrare och ibland mer effektivt.
try...catch
: Vi försöker skapa ett nyttPDO
-objekt. Om det misslyckas (fel lösenord, databasen nere etc.) kastas enPDOException
. Vi fångar den, loggar det tekniska felet (för utvecklaren) och kastar sedan ett nytt, mer användarvänligt, undantag eller avslutar skriptet meddie()
. Skriv aldrig ut det detaljerade felet$e->getMessage()
direkt till användaren i produktion!
4. Användarregistrering (register.php
)
Nu skapar vi sidan där användare kan registrera sig.
register.php
:
<?php
require_once 'includes/config.php';
require_once 'includes/database.php';
$errors = []; // Array för att lagra felmeddelanden
$username = '';
$email = '';
// Hantera formulärdata när det skickas (POST request)
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['username'] ?? '');
$email = trim($_POST['email'] ?? '');
$password = $_POST['password'] ?? '';
$confirm_password = $_POST['confirm_password'] ?? '';
// Validering
if (empty($username)) {
$errors[] = 'Användarnamn är obligatoriskt.';
}
if (empty($email)) {
$errors[] = 'E-post är obligatoriskt.';
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors[] = 'Ogiltig e-postadress.';
}
if (empty($password)) {
$errors[] = 'Lösenord är obligatoriskt.';
} elseif (strlen($password) < 6) {
$errors[] = 'Lösenordet måste vara minst 6 tecken långt.';
}
if ($password !== $confirm_password) {
$errors[] = 'Lösenorden matchar inte.';
}
// Om inga valideringsfel, försök registrera användaren
if (empty($errors)) {
try {
$pdo = connect_db();
// 1. Kolla om användarnamn eller e-post redan finns
$stmt_check = $pdo->prepare("SELECT id FROM users WHERE username = :username OR email = :email");
$stmt_check->bindParam(':username', $username);
$stmt_check->bindParam(':email', $email);
$stmt_check->execute();
if ($stmt_check->fetch()) {
$errors[] = 'Användarnamn eller e-postadress är redan registrerad.';
} else {
// 2. Hasha lösenordet säkert
$password_hash = password_hash($password, PASSWORD_DEFAULT);
// 3. Infoga användaren i databasen
$stmt_insert = $pdo->prepare("INSERT INTO users (username, email, password_hash) VALUES (:username, :email, :password_hash)");
$stmt_insert->bindParam(':username', $username);
$stmt_insert->bindParam(':email', $email);
$stmt_insert->bindParam(':password_hash', $password_hash);
if ($stmt_insert->execute()) {
// Registrering lyckades! Omdirigera till login-sidan.
header('Location: login.php?registered=success');
exit;
} else {
$errors[] = 'Ett fel uppstod vid registrering. Försök igen.';
}
}
} catch (PDOException $e) {
error_log("Registration Error: " . $e->getMessage());
$errors[] = 'Databasfel. Kan inte registrera användare just nu.';
}
}
}
?>
<!DOCTYPE html>
<html lang="sv">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Registrera dig - Enkel Blogg</title>
<style> /* Enkel CSS för formuläret */
body { font-family: sans-serif; line-height: 1.6; padding: 20px; }
.form-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; }
input[type="text"], input[type="email"], input[type="password"] {
width: 100%; padding: 8px; border: 1px solid #ccc; box-sizing: border-box;
}
button { padding: 10px 15px; background-color: #007bff; color: white; border: none; cursor: pointer; }
button:hover { background-color: #0056b3; }
.error-messages { color: red; margin-bottom: 15px; }
.error-messages ul { list-style: none; padding: 0; }
</style>
</head>
<body>
<h1>Registrera nytt konto</h1>
<?php if (!empty($errors)): ?>
<div class="error-messages">
<strong>Registreringen misslyckades:</strong>
<ul>
<?php foreach ($errors as $error): ?>
<li><?php echo htmlspecialchars($error); ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<form action="register.php" method="post">
<div class="form-group">
<label for="username">Användarnamn:</label>
<input type="text" id="username" name="username" value="<?php echo htmlspecialchars($username); ?>" required>
</div>
<div class="form-group">
<label for="email">E-post:</label>
<input type="email" id="email" name="email" value="<?php echo htmlspecialchars($email); ?>" required>
</div>
<div class="form-group">
<label for="password">Lösenord:</label>
<input type="password" id="password" name="password" required minlength="6">
</div>
<div class="form-group">
<label for="confirm_password">Bekräfta lösenord:</label>
<input type="password" id="confirm_password" name="confirm_password" required>
</div>
<button type="submit">Registrera</button>
</form>
<p>Har du redan ett konto? <a href="login.php">Logga in här</a>.</p>
</body>
</html>
Förklaring av register.php
:
- Inkludera filer: Börjar med att inkludera
config.php
(som startar sessioner) ochdatabase.php
. - Felhantering: Initierar en
$errors
-array för att samla validerings- och andra fel. - POST-hantering: Kollar om formuläret skickats (
$_SERVER['REQUEST_METHOD'] === 'POST'
). - Hämta data: Tar emot data från
$_POST
.trim()
tar bort onödiga mellanslag.?? ''
ger en tom sträng om värdet saknas. - Validering: Kontrollerar att fälten inte är tomma, att e-post är giltig (
filter_var
), att lösenordet är tillräckligt långt och att lösenorden matchar. - Databaskontroll:
- Om validering lyckas, anslut till databasen inom en
try...catch
-block. - Prepared Statement (Check): Skapar en förberedd fråga (
prepare
) för att kolla om användarnamn eller e-post redan finns.bindParam
binder PHP-variabler till platshållare (:username
,:email
). Detta är avgörande för säkerhet mot SQL-injektion. execute()
kör frågan.fetch()
hämtar en rad. Om den returnerar något, finns användaren redan.
- Om validering lyckas, anslut till databasen inom en
- Lösenordshashning:
- VIKTIGT: Lagra aldrig lösenord i klartext! Använd
password_hash()
medPASSWORD_DEFAULT
. PHP väljer då den starkaste tillgängliga hash-algoritmen (oftast bcrypt). Varje gång detta körs genereras en unik hash, även för samma lösenord.
- VIKTIGT: Lagra aldrig lösenord i klartext! Använd
- Infoga Användare:
- Prepared Statement (Insert): Skapar en ny förberedd fråga för att infoga användaren.
- Binder parametrar igen.
execute()
kör infogningen.
- Omdirigering: Om infogningen lyckas (
execute()
returnerartrue
), omdirigera användaren tilllogin.php
medheader('Location: ...')
.exit;
efteråt är viktigt för att stoppa skriptet. - Felvisning: Om
$errors
-arrayen inte är tom (antingen från validering eller databasfel), skrivs felmeddelandena ut ovanför formuläret.htmlspecialchars()
används för att förhindra Cross-Site Scripting (XSS) när användarinmatning (som$username
,$email
) skrivs ut.
5. Användarlogin (login.php
)
Nu skapar vi inloggningssidan.
login.php
:
<?php
require_once 'includes/config.php'; // Startar sessionen
require_once 'includes/database.php';
$errors = [];
$username = '';
// Visa meddelande om registrering lyckades
$registration_success = isset($_GET['registered']) && $_GET['registered'] === 'success';
// Hantera formulärdata när det skickas (POST request)
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
// Validering
if (empty($username)) {
$errors[] = 'Användarnamn är obligatoriskt.';
}
if (empty($password)) {
$errors[] = 'Lösenord är obligatoriskt.';
}
// Om inga valideringsfel, försök logga in
if (empty($errors)) {
try {
$pdo = connect_db();
// 1. Hämta användaren från databasen baserat på användarnamn
$stmt = $pdo->prepare("SELECT id, username, password_hash FROM users WHERE username = :username");
$stmt->bindParam(':username', $username);
$stmt->execute();
$user = $stmt->fetch(); // Hämta användardata (eller false om användaren inte finns)
// 2. Verifiera lösenordet
if ($user && password_verify($password, $user['password_hash'])) {
// Lösenordet matchar! Logga in användaren.
// Regenerera session ID för säkerhet (mot session fixation)
session_regenerate_id(true);
// Spara användarinformation i sessionen
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
// Omdirigera till admin-sidan eller startsidan
header('Location: admin/index.php');
exit;
} else {
// Användare finns inte eller fel lösenord
$errors[] = 'Felaktigt användarnamn eller lösenord.';
}
} catch (PDOException $e) {
error_log("Login Error: " . $e->getMessage());
$errors[] = 'Databasfel. Kan inte logga in just nu.';
}
}
}
?>
<!DOCTYPE html>
<html lang="sv">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Logga in - Enkel Blogg</title>
<style> /* Samma enkla CSS som register.php */
body { font-family: sans-serif; line-height: 1.6; padding: 20px; }
.form-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; }
input[type="text"], input[type="password"] {
width: 100%; padding: 8px; border: 1px solid #ccc; box-sizing: border-box;
}
button { padding: 10px 15px; background-color: #007bff; color: white; border: none; cursor: pointer; }
button:hover { background-color: #0056b3; }
.error-messages { color: red; margin-bottom: 15px; }
.error-messages ul { list-style: none; padding: 0; }
.success-message { color: green; margin-bottom: 15px; }
</style>
</head>
<body>
<h1>Logga in</h1>
<?php if ($registration_success): ?>
<p class="success-message">Registreringen lyckades! Du kan nu logga in.</p>
<?php endif; ?>
<?php if (!empty($errors)): ?>
<div class="error-messages">
<strong>Inloggningen misslyckades:</strong>
<ul>
<?php foreach ($errors as $error): ?>
<li><?php echo htmlspecialchars($error); ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<form action="login.php" method="post">
<div class="form-group">
<label for="username">Användarnamn:</label>
<input type="text" id="username" name="username" value="<?php echo htmlspecialchars($username); ?>" required>
</div>
<div class="form-group">
<label for="password">Lösenord:</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit">Logga in</button>
</form>
<p>Har du inget konto? <a href="register.php">Registrera dig här</a>.</p>
</body>
</html>
Förklaring av login.php
:
- Session Start:
config.php
startar sessionen (session_start()
). - Framgångsmeddelande: Kollar om
?registered=success
finns i URL:en och visar i så fall ett meddelande. - POST-hantering & Validering: Liknar
register.php
. - Hämta Användare:
- Använder en prepared statement för att hämta användaren baserat på det angivna användarnamnet. Vi hämtar
id
,username
och den hashade lösenordet (password_hash
). $stmt->fetch()
returnerar antingen en associativ array med användardata ellerfalse
om användarnamnet inte hittades.
- Använder en prepared statement för att hämta användaren baserat på det angivna användarnamnet. Vi hämtar
- Verifiera Lösenord:
- VIKTIGT: Använd
password_verify($password, $user['password_hash'])
för att jämföra det angivna lösenordet ($password
) med det hashade lösenordet från databasen ($user['password_hash']
).password_verify
sköter allt (salt etc.) automatiskt. Returnerartrue
om lösenordet matchar, annarsfalse
. Jämför aldrig hashar direkt!
- VIKTIGT: Använd
- Logga in (Session):
- Om
$user
finns OCHpassword_verify
returnerartrue
, är inloggningen lyckad. session_regenerate_id(true)
: Skapar ett nytt session-ID för den aktuella sessionen. Detta är en viktig säkerhetsåtgärd för att förhindra "session fixation"-attacker. Det gamla session-ID:t tas bort.$_SESSION['user_id'] = $user['id'];
: Vi sparar användarensid
i$_SESSION
-arrayen. Detta är nyckeln till att veta att användaren är inloggad på andra sidor.$_SESSION['username'] = $user['username'];
: Sparar även användarnamnet för att kunna visa det.
- Om
- Omdirigering: Omdirigera den inloggade användaren till
admin/index.php
. - Felhantering: Om
$user
inte finns ellerpassword_verify
misslyckas, visa ett generellt felmeddelande. - HTML-formulär: Liknar registreringsformuläret.
6. Sessionshantering & Logout
Hur Sessioner Fungerar (Kort)
session_start()
: Startar eller återupptar en session. PHP letar efter ett session-ID (oftast skickat via en cookie). Om inget finns, skapas ett nytt ID och en ny sessionfil på servern. Om ett ID finns, laddas data från motsvarande sessionfil in i den globala$_SESSION
-arrayen.$_SESSION
: En superglobal array där vi kan lagra data som är specifik för den aktuella användarens session (t.ex.user_id
). Datan sparas på servern.- Session Cookie: PHP skickar automatiskt en cookie (oftast med namnet
PHPSESSID
) till webbläsaren som innehåller det unika session-ID:t. Webbläsaren skickar tillbaka denna cookie vid varje efterföljande förfrågan, så att PHP kan identifiera rätt sessionfil.
Skydda Sidor
För sidor som bara inloggade användare ska nå (t.ex. i admin/
-mappen), behöver vi kontrollera om $_SESSION['user_id']
är satt.
Lägg till följande kod i början av varje skyddad fil (t.ex. admin/index.php
, admin/create_post.php
etc.):
<?php
require_once '../includes/config.php'; // Gå upp en nivå för att nå includes
// Kontrollera om användaren är inloggad
if (!isset($_SESSION['user_id'])) {
// Användaren är inte inloggad, omdirigera till login-sidan
header('Location: ../login.php?redirect=' . urlencode($_SERVER['REQUEST_URI']));
exit;
}
// Om vi når hit är användaren inloggad.
// Hämta användar-ID från sessionen för senare användning
$logged_in_user_id = $_SESSION['user_id'];
$logged_in_username = $_SESSION['username']; // Kan användas för att visa "Inloggad som ..."
// Inkludera resten av sidans logik här...
require_once '../includes/database.php';
// ...
?>
- Vi kollar om
$_SESSION['user_id']
är satt. Om inte, omdirigerar vi tilllogin.php
. - Vi skickar med
?redirect=...
för att potentiellt kunna skicka tillbaka användaren till den sida de försökte nå efter inloggning (implementeras inte fullt ut här, men är en bra idé).urlencode
ser till att URL:en är korrekt kodad.
Utloggning (logout.php
)
En enkel sida för att avsluta sessionen.
logout.php
:
<?php
require_once 'includes/config.php'; // Säkerställer att sessionen startas
// 1. Ta bort alla sessionsvariabler
$_SESSION = []; // Tömmer $_SESSION-arrayen
// 2. Om sessionskakor används, ta bort den
if (ini_get("session.use_cookies")) {
$params = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000, // Sätt en tidpunkt i det förflutna
$params["path"], $params["domain"],
$params["secure"], $params["httponly"]
);
}
// 3. Förstör sessionen på servern
session_destroy();
// 4. Omdirigera till startsidan eller login-sidan
header('Location: index.php?logged_out=success');
exit;
?>
Förklaring av logout.php
:
- Tömmer
$_SESSION
-arrayen. - Tar bort sessionscookien från webbläsaren genom att sätta dess utgångstid i det förflutna.
session_destroy()
: Tar bort sessionfilen på servern.- Omdirigerar användaren.
7. Skapa Blogginlägg (Create - admin/create_post.php
)
Denna sida innehåller formuläret och logiken för att skapa ett nytt blogginlägg. Den måste vara skyddad.
admin/create_post.php
:
<?php
require_once '../includes/config.php'; // Ger session_start()
// ---- Session Check ----
if (!isset($_SESSION['user_id'])) {
header('Location: ../login.php?redirect=' . urlencode($_SERVER['REQUEST_URI']));
exit;
}
$logged_in_user_id = $_SESSION['user_id'];
// ---- End Session Check ----
require_once '../includes/database.php';
$errors = [];
$title = '';
$body = '';
// Hantera formulärdata när det skickas (POST request)
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$title = trim($_POST['title'] ?? '');
$body = trim($_POST['body'] ?? '');
$image = $_FILES['image'] ?? null; // Hämta filinformation
$image_path = null; // Sökväg till sparad bild
// Validering
if (empty($title)) {
$errors[] = 'Titel är obligatoriskt.';
}
if (empty($body)) {
$errors[] = 'Innehåll är obligatoriskt.';
}
// ---- Bildhantering ----
if ($image && $image['error'] === UPLOAD_ERR_OK) {
// Validera filtyp och storlek (exempel)
$allowed_types = ['image/jpeg', 'image/png', 'image/gif'];
$max_size = 7 * 1024 * 1024; // 7 MB
if (!in_array($image['type'], $allowed_types)) {
$errors[] = 'Ogiltig filtyp. Endast JPG, PNG och GIF är tillåtna.';
} elseif ($image['size'] > $max_size) {
$errors[] = 'Filen är för stor. Maxstorlek är 5 MB.';
} else {
// Skapa ett unikt filnamn för att undvika kollisioner
$file_extension = pathinfo($image['name'], PATHINFO_EXTENSION);
$unique_filename = uniqid('post_img_', true) . '.' . $file_extension;
$destination = UPLOAD_PATH . $unique_filename; // UPLOAD_PATH från config.php
// Försök flytta filen till uploads/-mappen
if (move_uploaded_file($image['tmp_name'], $destination)) {
$image_path = 'uploads/' . $unique_filename; // Spara relativ sökväg för webbåtkomst
} else {
$errors[] = 'Kunde inte ladda upp bilden. Kontrollera mapprättigheter.';
error_log("File Upload Error: Could not move file to " . $destination);
}
}
} elseif ($image && $image['error'] !== UPLOAD_ERR_NO_FILE) {
// Om ett annat fel än "ingen fil" inträffade
$errors[] = 'Ett fel uppstod vid bilduppladdning. Felkod: ' . $image['error'];
}
// ---- Slut Bildhantering ----
// Om inga valideringsfel, försök spara inlägget
if (empty($errors)) {
try {
$pdo = connect_db();
$stmt = $pdo->prepare("INSERT INTO posts (user_id, title, body, image_path) VALUES (:user_id, :title, :body, :image_path)");
$stmt->bindParam(':user_id', $logged_in_user_id); // Använd ID från sessionen
$stmt->bindParam(':title', $title);
$stmt->bindParam(':body', $body);
// Bind image_path, blir NULL om ingen bild laddades upp korrekt
$stmt->bindParam(':image_path', $image_path, $image_path === null ? PDO::PARAM_NULL : PDO::PARAM_STR);
if ($stmt->execute()) {
// Inlägget skapades! Omdirigera till admin-dashboarden.
header('Location: index.php?created=success');
exit;
} else {
$errors[] = 'Ett fel uppstod när inlägget skulle sparas.';
// Om bilden hann sparas men DB-insert misslyckades, kan man överväga att ta bort bilden här.
if ($image_path && file_exists(UPLOAD_PATH . basename($image_path))) {
unlink(UPLOAD_PATH . basename($image_path));
}
}
} catch (PDOException $e) {
error_log("Create Post Error: " . $e->getMessage());
$errors[] = 'Databasfel. Kan inte spara inlägg just nu.';
// Om bilden hann sparas men DB-insert misslyckades pga exception
if ($image_path && file_exists(UPLOAD_PATH . basename($image_path))) {
unlink(UPLOAD_PATH . basename($image_path));
}
}
}
}
?>
<!DOCTYPE html>
<html lang="sv">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Skapa nytt inlägg - Admin</title>
<style> /* Enkel CSS */
body { font-family: sans-serif; line-height: 1.6; padding: 20px; }
.form-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; }
input[type="text"], textarea {
width: 100%; padding: 8px; border: 1px solid #ccc; box-sizing: border-box;
}
textarea { min-height: 150px; }
button { padding: 10px 15px; background-color: #28a745; color: white; border: none; cursor: pointer; }
button:hover { background-color: #218838; }
.error-messages { color: red; margin-bottom: 15px; }
.error-messages ul { list-style: none; padding: 0; }
a { color: #007bff; text-decoration: none; }
a:hover { text-decoration: underline; }
</style>
</head>
<body>
<h1>Skapa nytt blogginlägg</h1>
<p><a href="index.php">« Tillbaka till Admin Dashboard</a></p>
<?php if (!empty($errors)): ?>
<div class="error-messages">
<strong>Inlägget kunde inte sparas:</strong>
<ul>
<?php foreach ($errors as $error): ?>
<li><?php echo htmlspecialchars($error); ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<!-- Viktigt: enctype="multipart/form-data" för filuppladdning -->
<form action="create_post.php" method="post" enctype="multipart/form-data">
<div class="form-group">
<label for="title">Titel:</label>
<input type="text" id="title" name="title" value="<?php echo htmlspecialchars($title); ?>" required>
</div>
<div class="form-group">
<label for="body">Innehåll:</label>
<textarea id="body" name="body" required><?php echo htmlspecialchars($body); ?></textarea>
</div>
<div class="form-group">
<label for="image">Bild (valfritt, max 7MB, JPG/PNG/GIF):</label>
<input type="file" id="image" name="image" accept="image/jpeg, image/png, image/gif">
</div>
<button type="submit">Spara inlägg</button>
</form>
</body>
</html>
Förklaring av admin/create_post.php
:
- Session Check: Koden från avsnitt 6 ser till att endast inloggade användare kan komma åt sidan.
$logged_in_user_id
sparas. - Formulärhantering: Liknar
register.php
, men hämtar även filinformation från$_FILES['image']
. - Bildhantering:
- Kollar om en fil har laddats upp (
$image
) och om det inte uppstod något fel under uppladdningen ($image['error'] === UPLOAD_ERR_OK
). - Validering: Kontrollerar filtyp (
$image['type']
) mot en lista av tillåtna typer och filstorlek ($image['size']
) mot en maxgräns. - Unikt Filnamn: Genererar ett unikt filnamn med
uniqid()
ochpathinfo()
för att undvika att filer skriver över varandra. - Destination: Skapar den fullständiga sökvägen till målmappen (
UPLOAD_PATH
frånconfig.php
+ det unika filnamnet). move_uploaded_file()
: Försöker flytta den temporärt uppladdade filen ($image['tmp_name']
) till den slutgiltiga destinationen. Detta är det säkra sättet att hantera filuppladdningar.- Spara Sökväg: Om flytten lyckas, sparas den relativa sökvägen (t.ex.
uploads/bild.jpg
) i$image_path
. Denna relativa sökväg används sedan i<img>
-taggar i HTML. - Felhantering: Om något går fel (fel filtyp, för stor fil, misslyckad flytt), läggs ett fel till i
$errors
-arrayen.
- Kollar om en fil har laddats upp (
- Spara i Databas:
- Om validering och eventuell bilduppladdning lyckades, körs en
INSERT
-fråga med prepared statements. user_id
hämtas från sessionen ($logged_in_user_id
).image_path
binds. Viktigt: Om$image_path
ärnull
(ingen bild laddades upp eller det blev fel), måste vi specificeraPDO::PARAM_NULL
när vi binder, annars försöker PDO binda det som en tom sträng, vilket kan vara fel.
- Om validering och eventuell bilduppladdning lyckades, körs en
- Omdirigering/Fel: Omdirigera vid succé, visa fel annars. Om databasinfogningen misslyckas efter att bilden sparats, bör man försöka ta bort den sparade bilden (
unlink()
) för att undvika skräpfiler. - HTML-formulär:
enctype="multipart/form-data"
: Detta attribut är absolut nödvändigt i<form>
-taggen för att filuppladdning ska fungera.<input type="file">
: Fältet för att välja en bild.accept
-attributet hjälper webbläsaren att filtrera vilka filer som visas.
8. Visa Blogginlägg (Read)
Lista Alla Inlägg (index.php
)
Startsidan ska visa en lista över alla publicerade blogginlägg, kanske med titel, en kort del av innehållet, och en länk för att läsa mer.
index.php
:
<?php
require_once 'includes/config.php';
require_once 'includes/database.php';
$posts = []; // Array för att lagra inlägg
$fetch_error = null;
try {
$pdo = connect_db();
// Hämta alla inlägg, sorterade med det senaste först
// Hämta även användarnamnet för författaren via en JOIN
$stmt = $pdo->query("SELECT posts.*, users.username
FROM posts
JOIN users ON posts.user_id = users.id
ORDER BY posts.created_at DESC");
$posts = $stmt->fetchAll();
} catch (PDOException $e) {
error_log("Index Page Error: " . $e->getMessage());
$fetch_error = "Kunde inte hämta blogginlägg just nu. Försök igen senare.";
}
?>
<!DOCTYPE html>
<html lang="sv">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Enkel Blogg</title>
<style>
body { font-family: sans-serif; line-height: 1.6; padding: 20px; }
.post-summary { border: 1px solid #eee; padding: 15px; margin-bottom: 20px; }
.post-summary h2 { margin-top: 0; }
.post-meta { font-size: 0.9em; color: #666; margin-bottom: 10px; }
.post-image-list { max-width: 150px; max-height: 100px; float: right; margin-left: 15px; }
.read-more { display: inline-block; margin-top: 10px; }
nav { margin-bottom: 20px; background-color: #f8f9fa; padding: 10px; border-radius: 5px; }
nav a { margin-right: 15px; text-decoration: none; color: #007bff; }
nav a:hover { text-decoration: underline; }
.error-message { color: red; border: 1px solid red; padding: 10px; margin-bottom: 20px; }
.success-message { color: green; border: 1px solid green; padding: 10px; margin-bottom: 20px; }
</style>
</head>
<body>
<nav>
<a href="index.php">Hem</a>
<?php if (isset($_SESSION['user_id'])): ?>
<a href="admin/index.php">Admin Dashboard</a>
<a href="logout.php">Logga ut (<?php echo htmlspecialchars($_SESSION['username']); ?>)</a>
<?php else: ?>
<a href="login.php">Logga in</a>
<a href="register.php">Registrera dig</a>
<?php endif; ?>
</nav>
<h1>Välkommen till Bloggen!</h1>
<?php if (isset($_GET['logged_out']) && $_GET['logged_out'] === 'success'): ?>
<p class="success-message">Du har loggats ut.</p>
<?php endif; ?>
<?php if ($fetch_error): ?>
<p class="error-message"><?php echo htmlspecialchars($fetch_error); ?></p>
<?php else: ?>
<?php if (empty($posts)): ?>
<p>Det finns inga blogginlägg ännu.</p>
<?php else: ?>
<?php foreach ($posts as $post): ?>
<article class="post-summary">
<?php if (!empty($post['image_path'])): ?>
<img src="<?php echo htmlspecialchars(BASE_URL . '/' . $post['image_path']); ?>"
alt="Inläggsbild" class="post-image-list">
<?php endif; ?>
<h2><?php echo htmlspecialchars($post['title']); ?></h2>
<div class="post-meta">
Publicerad: <?php echo date('Y-m-d H:i', strtotime($post['created_at'])); ?>
av <?php echo htmlspecialchars($post['username']); ?>
</div>
<p>
<?php
// Visa en kort del av texten (t.ex. 200 tecken)
$summary = htmlspecialchars($post['body']);
if (strlen($summary) > 200) {
$summary = substr($summary, 0, 200) . '...';
}
echo nl2br($summary); // nl2br gör om nya rader till <br>
?>
</p>
<a href="post.php?id=<?php echo $post['id']; ?>" class="read-more">Läs mer »</a>
<div style="clear: both;"></div> <!-- Rensa float -->
</article>
<?php endforeach; ?>
<?php endif; ?>
<?php endif; ?>
</body>
</html>
Förklaring av index.php
:
- Hämta Data:
- Ansluter till databasen.
- Använder
$pdo->query()
för en enkelSELECT
-fråga (eftersom inga användarparametrar behövs här ärquery()
okej, menprepare
/execute
fungerar också). - Använder
JOIN users ON posts.user_id = users.id
för att hämta författarens användarnamn (users.username
) tillsammans med post-datan. - Sorterar med
ORDER BY posts.created_at DESC
för att visa de senaste inläggen först. $stmt->fetchAll()
hämtar alla matchande rader som en array av associativa arrayer.- Felhantering med
try...catch
.
- Navigation (
<nav>
): Visar olika länkar beroende på om användaren är inloggad (isset($_SESSION['user_id'])
). Visar användarnamnet om inloggad. - Fel/Succé-meddelanden: Visar meddelande om utloggning lyckats, eller om det blev fel vid hämtning av inlägg.
- Loopa igenom Inlägg:
- Om inga fel och
$posts
-arrayen inte är tom, loopar vi igenom den medforeach
. - För varje
$post
:- Visar bilden om
image_path
finns. Notera användningen avBASE_URL
för att skapa en absolut sökväg till bilden. - Visar titel, metadata (datum, författare).
strtotime()
konverterar databasens tidsstämpel till en Unix timestamp somdate()
kan formatera. - Visar en förkortad version av brödtexten (
substr
) och användernl2br()
för att respektera radbrytningar i texten. - Skapar en "Läs mer"-länk till
post.php
och skickar med inläggetsid
i URL:en (?id=...
).
- Visar bilden om
htmlspecialchars()
används genomgående för att skydda mot XSS när data från databasen skrivs ut.
- Om inga fel och
Visa Enskilt Inlägg (post.php
)
Denna sida visar hela innehållet för ett specifikt blogginlägg, baserat på id
:t som skickas i URL:en.
post.php
:
<?php
require_once 'includes/config.php';
require_once 'includes/database.php';
// Hämta post-ID från URL-parametern
$post_id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
$post = null;
$fetch_error = null;
if ($post_id === false || $post_id <= 0) {
$fetch_error = "Ogiltigt inläggs-ID.";
} else {
try {
$post = get_post_by_id($post_id); // Använd funktionen!
if (!$post) {
$fetch_error = "Inlägget hittades inte.";
}
} catch (PDOException $e) {
error_log("View Post Error (ID: $post_id): " . $e->getMessage());
$fetch_error = "Kunde inte hämta blogginlägget just nu.";
}
}
?>
<!DOCTYPE html>
<html lang="sv">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Titel sätts dynamiskt om inlägget hittas -->
<title><?php echo $post ? htmlspecialchars($post['title']) : 'Inlägg'; ?> - Enkel Blogg</title>
<style>
body { font-family: sans-serif; line-height: 1.6; padding: 20px; }
.post-content { margin-top: 20px; }
.post-meta { font-size: 0.9em; color: #666; margin-bottom: 10px; }
.post-image-full { max-width: 100%; height: auto; margin-bottom: 20px; }
nav { margin-bottom: 20px; background-color: #f8f9fa; padding: 10px; border-radius: 5px; }
nav a { margin-right: 15px; text-decoration: none; color: #007bff; }
nav a:hover { text-decoration: underline; }
.error-message { color: red; border: 1px solid red; padding: 10px; margin-bottom: 20px; }
</style>
</head>
<body>
<nav>
<a href="index.php">Hem</a>
<?php if (isset($_SESSION['user_id'])): ?>
<a href="admin/index.php">Admin Dashboard</a>
<a href="logout.php">Logga ut (<?php echo htmlspecialchars($_SESSION['username']); ?>)</a>
<?php else: ?>
<a href="login.php">Logga in</a>
<a href="register.php">Registrera dig</a>
<?php endif; ?>
</nav>
<?php if ($fetch_error): ?>
<p class="error-message"><?php echo htmlspecialchars($fetch_error); ?></p>
<?php elseif ($post): ?>
<article class="post-content">
<h1><?php echo htmlspecialchars($post['title']); ?></h1>
<div class="post-meta">
Publicerad: <?php echo date('Y-m-d H:i', strtotime($post['created_at'])); ?>
av <?php echo htmlspecialchars($post['username']); ?>
<?php if ($post['created_at'] !== $post['updated_at']): ?>
(Senast ändrad: <?php echo date('Y-m-d H:i', strtotime($post['updated_at'])); ?>)
<?php endif; ?>
</div>
<?php if (!empty($post['image_path'])): ?>
<img src="<?php echo htmlspecialchars(BASE_URL . '/' . $post['image_path']); ?>"
alt="<?php echo htmlspecialchars($post['title']); ?>" class="post-image-full">
<?php endif; ?>
<div>
<?php
// Skriv ut brödtexten, konvertera nya rader till <br>
echo nl2br(htmlspecialchars($post['body']));
?>
</div>
</article>
<hr>
<p><a href="index.php">« Tillbaka till alla inlägg</a></p>
<?php else: ?>
<p class="error-message">Inlägget kunde inte laddas.</p> <!-- Fallback om post är null utan error -->
<?php endif; ?>
</body>
</html>
Förklaring av post.php
:
- Hämta ID: Använder
filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT)
för att säkert hämta och valideraid
-parametern från URL:en ($_GET['id']
). Detta skyddar mot vissa typer av attacker och säkerställer att vi har ett heltal. - Validera ID: Kontrollerar att
$post_id
är ett giltigt positivt heltal. - Hämta Data:
- Använder en prepared statement för att hämta inlägget med det specifika
:id
. Detta är viktigt även här för säkerhet, även om ID:t är validerat som heltal. bindParam
binder$post_id
till platshållaren, och specificerarPDO::PARAM_INT
för att vara extra tydlig med datatypen.$stmt->fetch()
hämtar den enda raden (ellerfalse
om ID inte finns)
- Använder en prepared statement för att hämta inlägget med det specifika
- Felhantering: Sätter
$fetch_error
om ID är ogiltigt, inlägget inte hittas, eller om ettPDOException
inträffar. - Visa Innehåll: Om
$post
innehåller data (inte ärnull
ellerfalse
), visas titel, metadata (inklusive "Senast ändrad" omupdated_at
skiljer sig fråncreated_at
), eventuell bild och hela brödtexten (nl2br(htmlspecialchars(...))
). - Navigation/Fel: Visar navigeringsmeny och eventuella felmeddelanden.
9. Redigera Blogginlägg (Update - admin/edit_post.php
)
Denna sida låter en inloggad användare redigera sina egna inlägg. Den behöver både hämta befintlig data och hantera uppdateringar.
admin/edit_post.php
:
<?php
require_once '../includes/config.php'; // Ger session_start()
// ---- Session Check ----
if (!isset($_SESSION['user_id'])) {
header('Location: ../login.php?redirect=' . urlencode($_SERVER['REQUEST_URI']));
exit;
}
$logged_in_user_id = $_SESSION['user_id'];
// ---- End Session Check ----
require_once '../includes/database.php';
$errors = [];
$post_id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
$post = null; // För att lagra den hämtade post-datan
$title = '';
$body = '';
$current_image_path = null;
// --- Steg 1: Hämta inlägget som ska redigeras ---
if ($post_id === false || $post_id <= 0) {
$errors[] = "Ogiltigt inläggs-ID.";
} else {
try {
$pdo = connect_db();
$stmt = $pdo->prepare("SELECT * FROM posts WHERE id = :id");
$stmt->bindParam(':id', $post_id, PDO::PARAM_INT);
$stmt->execute();
$post = $stmt->fetch();
if (!$post) {
$errors[] = "Inlägget hittades inte.";
} elseif ($post['user_id'] !== $logged_in_user_id) {
$errors[] = "Du har inte behörighet att redigera detta inlägg.";
$post = null; // Nollställ post så att formuläret inte visas
} else {
// Fyll i formulärvärden med hämtad data
$title = $post['title'];
$body = $post['body'];
$current_image_path = $post['image_path'];
}
} catch (PDOException $e) {
error_log("Edit Post Fetch Error (ID: $post_id): " . $e->getMessage());
$errors[] = 'Databasfel. Kan inte hämta inlägg för redigering.';
$post = null; // Nollställ post vid databasfel
}
}
// --- Slut Steg 1 ---
// --- Steg 2: Hantera formulärdata vid POST (uppdatering) ---
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $post) { // Kör bara om post hämtades OK
$title = trim($_POST['title'] ?? '');
$body = trim($_POST['body'] ?? '');
$image = $_FILES['image'] ?? null;
$delete_image = isset($_POST['delete_image']); // Kolla om kryssrutan är ikryssad
$new_image_path = $current_image_path; // Börja med den gamla bilden
// Validering (samma som create)
if (empty($title)) {
$errors[] = 'Titel är obligatoriskt.';
}
if (empty($body)) {
$errors[] = 'Innehåll är obligatoriskt.';
}
$image_uploaded = false; // Flagga för att se om ny bild laddats upp
// ---- Bildhantering (liknar create, men med delete-option) ----
if ($delete_image) {
// Om användaren vill ta bort bilden
if ($current_image_path && file_exists(UPLOAD_PATH . basename($current_image_path))) {
if (unlink(UPLOAD_PATH . basename($current_image_path))) {
$new_image_path = null; // Ta bort sökvägen
$current_image_path = null; // Uppdatera även för visning
} else {
$errors[] = 'Kunde inte ta bort den befintliga bilden.';
error_log("File Deletion Error: Could not delete " . UPLOAD_PATH . basename($current_image_path));
}
} else {
$new_image_path = null; // Ingen bild fanns eller kunde inte tas bort
$current_image_path = null;
}
} elseif ($image && $image['error'] === UPLOAD_ERR_OK) {
// Om en NY bild laddas upp (validera etc.)
$allowed_types = ['image/jpeg', 'image/png', 'image/gif'];
$max_size = 5 * 1024 * 1024; // 5 MB
if (!in_array($image['type'], $allowed_types)) {
$errors[] = 'Ogiltig filtyp för ny bild. Endast JPG, PNG och GIF.';
} elseif ($image['size'] > $max_size) {
$errors[] = 'Den nya filen är för stor. Maxstorlek är 5 MB.';
} else {
$file_extension = pathinfo($image['name'], PATHINFO_EXTENSION);
$unique_filename = uniqid('post_img_', true) . '.' . $file_extension;
$destination = UPLOAD_PATH . $unique_filename;
if (move_uploaded_file($image['tmp_name'], $destination)) {
// Ta bort den GAMLA bilden om den fanns
if ($current_image_path && file_exists(UPLOAD_PATH . basename($current_image_path))) {
unlink(UPLOAD_PATH . basename($current_image_path));
}
$new_image_path = 'uploads/' . $unique_filename; // Spara nya sökvägen
$image_uploaded = true; // Markera att ny bild laddats upp
} else {
$errors[] = 'Kunde inte ladda upp den nya bilden.';
error_log("File Upload Error (Edit): Could not move file to " . $destination);
}
}
} elseif ($image && $image['error'] !== UPLOAD_ERR_NO_FILE) {
$errors[] = 'Ett fel uppstod vid bilduppladdning. Felkod: ' . $image['error'];
}
// ---- Slut Bildhantering ----
// Om inga fel hittills, försök uppdatera databasen
if (empty($errors)) {
try {
// Vi behöver $pdo igen här om det inte redan är definierat
if (!isset($pdo)) $pdo = connect_db();
$stmt = $pdo->prepare("UPDATE posts SET title = :title, body = :body, image_path = :image_path WHERE id = :id AND user_id = :user_id");
$stmt->bindParam(':title', $title);
$stmt->bindParam(':body', $body);
$stmt->bindParam(':image_path', $new_image_path, $new_image_path === null ? PDO::PARAM_NULL : PDO::PARAM_STR);
$stmt->bindParam(':id', $post_id, PDO::PARAM_INT);
$stmt->bindParam(':user_id', $logged_in_user_id, PDO::PARAM_INT); // Dubbelkolla ägarskap vid UPDATE
if ($stmt->execute()) {
// Uppdatering lyckades! Omdirigera till admin-dashboarden.
header('Location: index.php?updated=success&id=' . $post_id);
exit;
} else {
$errors[] = 'Ett fel uppstod när inlägget skulle uppdateras.';
// Om en ny bild hann laddas upp men DB-update misslyckades, ta bort den nya bilden
if ($image_uploaded && $new_image_path && file_exists(UPLOAD_PATH . basename($new_image_path))) {
unlink(UPLOAD_PATH . basename($new_image_path));
}
}
} catch (PDOException $e) {
error_log("Update Post Error (ID: $post_id): " . $e->getMessage());
$errors[] = 'Databasfel. Kan inte uppdatera inlägg just nu.';
// Om en ny bild hann laddas upp men DB-update misslyckades pga exception
if ($image_uploaded && $new_image_path && file_exists(UPLOAD_PATH . basename($new_image_path))) {
unlink(UPLOAD_PATH . basename($new_image_path));
}
}
}
}
// --- Slut Steg 2 ---
?>
<!DOCTYPE html>
<html lang="sv">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Redigera inlägg - Admin</title>
<style> /* Samma CSS */
body { font-family: sans-serif; line-height: 1.6; padding: 20px; }
.form-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; }
input[type="text"], textarea {
width: 100%; padding: 8px; border: 1px solid #ccc; box-sizing: border-box;
}
textarea { min-height: 150px; }
button { padding: 10px 15px; background-color: #007bff; color: white; border: none; cursor: pointer; }
button:hover { background-color: #0056b3; }
.error-messages { color: red; margin-bottom: 15px; }
.error-messages ul { list-style: none; padding: 0; }
a { color: #007bff; text-decoration: none; }
a:hover { text-decoration: underline; }
.current-image img { max-width: 200px; max-height: 150px; display: block; margin-top: 5px; }
.delete-image label { display: inline-block; margin-left: 5px; }
</style>
</head>
<body>
<h1>Redigera blogginlägg</h1>
<p><a href="index.php">« Tillbaka till Admin Dashboard</a></p>
<?php if (!empty($errors)): ?>
<div class="error-messages">
<strong>Inlägget kunde inte uppdateras (eller hämtas):</strong>
<ul>
<?php foreach ($errors as $error): ?>
<li><?php echo htmlspecialchars($error); ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<?php // Visa bara formuläret om inlägget kunde hämtas och användaren har behörighet ?>
<?php if ($post): ?>
<form action="edit_post.php?id=<?php echo $post_id; ?>" method="post" enctype="multipart/form-data">
<div class="form-group">
<label for="title">Titel:</label>
<input type="text" id="title" name="title" value="<?php echo htmlspecialchars($title); ?>" required>
</div>
<div class="form-group">
<label for="body">Innehåll:</label>
<textarea id="body" name="body" required><?php echo htmlspecialchars($body); ?></textarea>
</div>
<div class="form-group">
<label>Nuvarande Bild:</label>
<?php if ($current_image_path): ?>
<div class="current-image">
<img src="<?php echo htmlspecialchars(BASE_URL . '/' . $current_image_path); ?>" alt="Nuvarande bild">
</div>
<div class="delete-image">
<input type="checkbox" id="delete_image" name="delete_image" value="1">
<label for="delete_image">Ta bort nuvarande bild</label>
</div>
<?php else: ?>
<p>Ingen bild är uppladdad.</p>
<?php endif; ?>
</div>
<div class="form-group">
<label for="image">Ladda upp ny bild (valfritt, ersätter nuvarande):</label>
<input type="file" id="image" name="image" accept="image/jpeg, image/png, image/gif">
</div>
<button type="submit">Uppdatera inlägg</button>
</form>
<?php endif; ?>
</body>
</html>
Förklaring av admin/edit_post.php
:
- Session Check: Som tidigare.
- Hämta Inlägg (Steg 1):
- Hämtar
id
från$_GET
. - Använder prepared statement för att hämta inlägget med det ID:t.
- Säkerhetskontroll: Verifierar att inlägget finns (
$post
) OCH attuser_id
i inlägget matchar ID:t för den inloggade användaren ($logged_in_user_id
). Detta förhindrar att en användare redigerar någon annans inlägg genom att gissa ID:n. Om kontrollen misslyckas, sätts$post
tillnull
och ett fel läggs till. - Om allt är okej, fylls variablerna
$title
,$body
,$current_image_path
med data från databasen. Dessa används för att för-fylla formuläret.
- Hämtar
- Hantera Uppdatering (Steg 2 - POST):
- Koden körs bara om
REQUEST_METHOD
ärPOST
OCH om$post
hämtades framgångsrikt i steg 1. - Hämtar data från
$_POST
och$_FILES
. - Kollar om kryssrutan
delete_image
är ikryssad. - Bildlogik:
- Om
delete_image
är ikryssad: Försöker ta bort den gamla bildfilen medunlink()
och sätter$new_image_path
tillnull
. - Om en ny bild laddas upp (
$image
finns och är OK): Validerar filen, flyttar den tilluploads/
, tar bort den gamla bilden om den fanns, och sätter$new_image_path
till den nya sökvägen. - Om ingen ny bild laddas upp och
delete_image
inte är ikryssad, behålls$new_image_path = $current_image_path
.
- Om
- Validering: Validerar titel och brödtext.
- Uppdatera Databas:
- Om inga fel, kör en
UPDATE
-fråga med prepared statements. - Uppdaterar
title
,body
ochimage_path
(som nu kan vara den gamla,null
eller en ny sökväg). - Viktigt: Inkluderar
WHERE id = :id AND user_id = :user_id
för att säkerställa att vi bara uppdaterar rätt inlägg OCH att den inloggade användaren fortfarande äger det (som en extra säkerhetsåtgärd).
- Om inga fel, kör en
- Omdirigering vid succé, felhantering annars (inklusive att ta bort en nyligen uppladdad bild om DB-uppdateringen misslyckas).
- Koden körs bara om
- HTML-formulär:
- Visas endast om
$post
hämtades framgångsrikt (och ägs av användaren). - Action-URL:en inkluderar
?id=...
så att vi vet vilket inlägg som ska uppdateras när formuläret skickas. - Fälten är för-ifyllda med
htmlspecialchars($title)
ochhtmlspecialchars($body)
. - Visar den nuvarande bilden om den finns.
- Inkluderar en kryssruta (
delete_image
) för att ta bort bilden. - Har ett fält för att ladda upp en ny bild.
- Visas endast om
10. Ta bort Blogginlägg (Delete - admin/delete_post.php
)
Denna sida hanterar endast logiken för att ta bort ett inlägg. Den bör anropas via en POST-förfrågan (från en knapp i admin-listan) för att undvika oavsiktlig radering via GET-länkar.
admin/delete_post.php
:
<?php
require_once '../includes/config.php'; // Ger session_start()
// ---- Session Check ----
if (!isset($_SESSION['user_id'])) {
header('Location: ../login.php'); // Omdirigera direkt vid ej inloggad
exit;
}
$logged_in_user_id = $_SESSION['user_id'];
// ---- End Session Check ----
require_once '../includes/database.php';
$errors = [];
$post_id = null;
// Radera endast om det är en POST-förfrågan
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$post_id = filter_input(INPUT_POST, 'post_id', FILTER_VALIDATE_INT);
if ($post_id === false || $post_id <= 0) {
$errors[] = "Ogiltigt inläggs-ID för radering.";
} else {
try {
$pdo = connect_db();
// Steg 1: Hämta bildsökväg INNAN radering (och kontrollera ägarskap)
$stmt_fetch = $pdo->prepare("SELECT user_id, image_path FROM posts WHERE id = :id");
$stmt_fetch->bindParam(':id', $post_id, PDO::PARAM_INT);
$stmt_fetch->execute();
$post_data = $stmt_fetch->fetch();
if (!$post_data) {
$errors[] = "Inlägget som ska raderas hittades inte.";
} elseif ($post_data['user_id'] !== $logged_in_user_id) {
$errors[] = "Du har inte behörighet att radera detta inlägg.";
} else {
// Användaren äger inlägget, fortsätt med radering
// Steg 2: Försök radera posten från databasen
$stmt_delete = $pdo->prepare("DELETE FROM posts WHERE id = :id AND user_id = :user_id");
$stmt_delete->bindParam(':id', $post_id, PDO::PARAM_INT);
$stmt_delete->bindParam(':user_id', $logged_in_user_id, PDO::PARAM_INT); // Dubbelkolla ägarskap
if ($stmt_delete->execute()) {
// Radering från DB lyckades
// Steg 3: Försök ta bort den associerade bilden (om den finns)
$image_to_delete = $post_data['image_path'];
if ($image_to_delete && file_exists(UPLOAD_PATH . basename($image_to_delete))) {
if (!unlink(UPLOAD_PATH . basename($image_to_delete))) {
// Logga felet, men omdirigera ändå eftersom posten är borta från DB
error_log("File Deletion Error (Post Delete): Could not delete " . UPLOAD_PATH . basename($image_to_delete));
// Kanske lägga ett meddelande i sessionen för admin att se?
// $_SESSION['admin_notice'] = "Inlägg $post_id raderat, men kunde inte ta bort bildfil.";
}
}
// Omdirigera till admin-dashboarden med success-meddelande
header('Location: index.php?deleted=success&id=' . $post_id);
exit;
} else {
$errors[] = "Kunde inte radera inlägget från databasen.";
}
}
} catch (PDOException $e) {
error_log("Delete Post Error (ID: $post_id): " . $e->getMessage());
$errors[] = 'Databasfel. Kan inte radera inlägg just nu.';
}
}
} else {
// Om inte POST, omdirigera bort (eller visa fel)
$errors[] = "Ogiltig förfrågan för att radera inlägg.";
// Kanske omdirigera? header('Location: index.php'); exit;
}
// Om vi når hit har något gått fel. Visa felmeddelanden (eller omdirigera med fel i URL)
// Detta är en förenklad felhantering för en ren backend-sida.
// I en mer avancerad app skulle man kanske sätta felmeddelande i sessionen
// och omdirigera tillbaka till index.php.
?>
<!DOCTYPE html>
<html lang="sv">
<head>
<meta charset="UTF-8">
<title>Raderingsfel - Admin</title>
<style> /* Enkel CSS */
body { font-family: sans-serif; line-height: 1.6; padding: 20px; }
.error-messages { color: red; margin-bottom: 15px; border: 1px solid red; padding: 10px; }
.error-messages ul { list-style: none; padding: 0; }
a { color: #007bff; text-decoration: none; }
a:hover { text-decoration: underline; }
</style>
</head>
<body>
<h1>Fel vid radering</h1>
<?php if (!empty($errors)): ?>
<div class="error-messages">
<strong>Inlägget kunde inte raderas:</strong>
<ul>
<?php foreach ($errors as $error): ?>
<li><?php echo htmlspecialchars($error); ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<p><a href="index.php">« Tillbaka till Admin Dashboard</a></p>
</body>
</html>
Förklaring av admin/delete_post.php
:
- Session Check & POST Check: Säkerställer inloggning och att skriptet anropas via POST.
- Hämta ID: Hämtar
post_id
från$_POST
. - Hämta Bildsökväg & Kontrollera Ägarskap:
- INNAN vi raderar från databasen, hämtar vi inläggets
user_id
ochimage_path
. - Vi kontrollerar att inlägget finns och att den inloggade användaren äger det.
- INNAN vi raderar från databasen, hämtar vi inläggets
- Radera från Databas:
- Om ägarskapet är korrekt, kör en
DELETE
-fråga med prepared statements. InkluderarAND user_id = :user_id
som en extra säkerhetsåtgärd.
- Om ägarskapet är korrekt, kör en
- Radera Bildfil:
- Om raderingen från databasen lyckades (
execute()
returnerartrue
), försöker vi ta bort den associerade bildfilen medunlink()
. - Vi kollar först om
$image_to_delete
inte ärnull
och om filen faktiskt existerar. - Om
unlink()
misslyckas loggar vi felet, men fortsätter (eftersom posten redan är borta från databasen).
- Om raderingen från databasen lyckades (
- Omdirigering: Omdirigerar till
admin/index.php
med ett success-meddelande. - Felhantering: Om något steg misslyckas (fel ID, ej ägare, DB-fel), samlas fel i
$errors
. Sidan visar sedan dessa fel. I en mer sofistikerad app skulle man kanske lagra felet i sessionen och omdirigera tillbaka till admin-listan.
11. Adminpanel (admin/index.php
)
Detta är huvudsidan för inloggade användare där de kan se sina inlägg och hantera dem.
admin/index.php
:
<?php
require_once '../includes/config.php'; // Ger session_start()
// ---- Session Check ----
if (!isset($_SESSION['user_id'])) {
header('Location: ../login.php?redirect=' . urlencode($_SERVER['REQUEST_URI']));
exit;
}
$logged_in_user_id = $_SESSION['user_id'];
$logged_in_username = $_SESSION['username'];
// ---- End Session Check ----
require_once '../includes/database.php';
$posts = []; // Array för användarens inlägg
$fetch_error = null;
$success_message = null; // För att visa meddelanden från create/update/delete
// Kolla efter success-meddelanden från omdirigeringar
if (isset($_GET['created']) && $_GET['created'] === 'success') {
$success_message = "Nytt inlägg skapat!";
} elseif (isset($_GET['updated']) && $_GET['updated'] === 'success') {
$success_message = "Inlägg (ID: " . htmlspecialchars($_GET['id'] ?? '') . ") uppdaterat!";
} elseif (isset($_GET['deleted']) && $_GET['deleted'] === 'success') {
$success_message = "Inlägg (ID: " . htmlspecialchars($_GET['id'] ?? '') . ") raderat!";
}
try {
$pdo = connect_db();
// Hämta ENDAST den inloggade användarens inlägg
$stmt = $pdo->prepare("SELECT id, title, created_at, updated_at
FROM posts
WHERE user_id = :user_id
ORDER BY created_at DESC");
$stmt->bindParam(':user_id', $logged_in_user_id, PDO::PARAM_INT);
$stmt->execute();
$posts = $stmt->fetchAll();
} catch (PDOException $e) {
error_log("Admin Index Error (User: $logged_in_user_id): " . $e->getMessage());
$fetch_error = "Kunde inte hämta dina blogginlägg just nu.";
}
?>
<!DOCTYPE html>
<html lang="sv">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Dashboard - Enkel Blogg</title>
<style>
body { font-family: sans-serif; line-height: 1.6; padding: 20px; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #f2f2f2; }
.actions a, .actions button { margin-right: 5px; padding: 3px 6px; text-decoration: none; border: 1px solid transparent; }
.actions .edit-btn { background-color: #ffc107; color: black; border-color: #ffc107; }
.actions .delete-btn { background-color: #dc3545; color: white; border: none; cursor: pointer; font-size: inherit; font-family: inherit;}
.actions .edit-btn:hover { background-color: #e0a800; }
.actions .delete-btn:hover { background-color: #c82333; }
nav { margin-bottom: 20px; background-color: #f8f9fa; padding: 10px; border-radius: 5px; }
nav a { margin-right: 15px; text-decoration: none; color: #007bff; }
nav a:hover { text-decoration: underline; }
.create-link { display: inline-block; margin-bottom: 20px; background-color: #28a745; color: white; padding: 10px 15px; text-decoration: none; border-radius: 4px; }
.create-link:hover { background-color: #218838; }
.error-message { color: red; border: 1px solid red; padding: 10px; margin-bottom: 20px; }
.success-message { color: green; border: 1px solid green; padding: 10px; margin-bottom: 20px; }
</style>
</head>
<body>
<nav>
<a href="../index.php">Visa Blogg</a>
<a href="index.php">Admin Dashboard</a>
<a href="../logout.php">Logga ut (<?php echo htmlspecialchars($logged_in_username); ?>)</a>
</nav>
<h1>Admin Dashboard</h1>
<p>Välkommen, <?php echo htmlspecialchars($logged_in_username); ?>!</p>
<a href="create_post.php" class="create-link">Skapa nytt inlägg</a>
<?php if ($success_message): ?>
<p class="success-message"><?php echo htmlspecialchars($success_message); ?></p>
<?php endif; ?>
<?php if ($fetch_error): ?>
<p class="error-message"><?php echo htmlspecialchars($fetch_error); ?></p>
<?php else: ?>
<h2>Dina Inlägg</h2>
<?php if (empty($posts)): ?>
<p>Du har inte skapat några inlägg ännu.</p>
<?php else: ?>
<table>
<thead>
<tr>
<th>Titel</th>
<th>Skapad</th>
<th>Senast ändrad</th>
<th>Åtgärder</th>
</tr>
</thead>
<tbody>
<?php foreach ($posts as $post): ?>
<tr>
<td><?php echo htmlspecialchars($post['title']); ?></td>
<td><?php echo date('Y-m-d H:i', strtotime($post['created_at'])); ?></td>
<td><?php echo date('Y-m-d H:i', strtotime($post['updated_at'])); ?></td>
<td class="actions">
<a href="../post.php?id=<?php echo $post['id']; ?>" target="_blank">Visa</a>
<a href="edit_post.php?id=<?php echo $post['id']; ?>" class="edit-btn">Redigera</a>
<!-- Delete-knappen i ett eget formulär för POST -->
<form action="delete_post.php" method="post" style="display: inline;" onsubmit="return confirm('Är du säker på att du vill radera inlägget '<?php echo htmlspecialchars(addslashes($post['title'])); ?>'?');">
<input type="hidden" name="post_id" value="<?php echo $post['id']; ?>">
<button type="submit" class="delete-btn">Radera</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
<?php endif; ?>
</body>
</html>
Förklaring av admin/index.php
:
- Hämta Data:
- Ansluter till databasen.
- Använder
$pdo->query()
för en enkelSELECT
-fråga (eftersom inga användarparametrar behövs här ärquery()
okej, menprepare
/execute
fungerar också). - Använder
JOIN users ON posts.user_id = users.id
för att hämta författarens användarnamn (users.username
) tillsammans med post-datan. - Sorterar med
ORDER BY posts.created_at DESC
för att visa de senaste inläggen först. $stmt->fetchAll()
hämtar alla matchande rader som en array av associativa arrayer.- Felhantering med
try...catch
.
- Navigation (
<nav>
): Visar olika länkar beroende på om användaren är inloggad (isset($_SESSION['user_id'])
). Visar användarnamnet om inloggad. - Fel/Succé-meddelanden: Visar meddelande om utloggning lyckats, eller om det blev fel vid hämtning av inlägg.
- Loopa igenom Inlägg:
- Om inga fel och inlägg finns, visas en HTML-tabell.
- Loopar igenom
$posts
. - Varje rad visar titel, datum och åtgärdsknappar:
- "Visa": Länk till den publika
post.php
. - "Redigera": Länk till
edit_post.php
med rättid
. - "Radera": Detta är en knapp inuti ett litet formulär. Formuläret skickar
post_id
via POST tilldelete_post.php
. Detta är säkrare än en enkel GET-länk för radering.onsubmit="return confirm(...)"
lägger till en JavaScript-bekräftelsedialog innan formuläret skickas.addslashes()
används inuti JavaScript-strängen för att hantera eventuella citationstecken i titeln.
- "Visa": Länk till den publika
Nu har vi en fungerande, om än mycket enkel, bloggapplikation med grundläggande CRUD, autentisering, sessioner och bildhantering.
12. Refaktorering och Förbättringar
Koden ovan fungerar, men den är väldigt blandad (HTML, PHP-logik, databasfrågor i samma fil). I en större applikation blir detta snabbt ohanterligt. Här är några steg vi kan ta för att förbättra den:
a) Separera Logik med Funktioner
Vi kan flytta återkommande kodblock, särskilt databasinteraktioner, till funktioner i includes/functions.php
.
Exempel (skapa includes/functions.php
):
<?php
// includes/functions.php
require_once 'database.php'; // Behövs för att anropa connect_db()
/**
* Hämtar ett specifikt inlägg från databasen.
*
* @param int $post_id ID för inlägget som ska hämtas.
* @return array|false Post-datan som en associativ array, eller false om den inte hittas.
* @throws PDOException Vid databasfel.
*/
function get_post_by_id(int $post_id): array|false {
$pdo = connect_db();
$stmt = $pdo->prepare("SELECT posts.*, users.username
FROM posts
JOIN users ON posts.user_id = users.id
WHERE posts.id = :id");
$stmt->bindParam(':id', $post_id, PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetch();
}
/**
* Hämtar alla inlägg av en specifik användare.
*
* @param int $user_id Användarens ID.
* @return array En array med alla användarens inlägg.
* @throws PDOException Vid databasfel.
*/
function get_posts_by_user(int $user_id): array {
$pdo = connect_db();
$stmt = $pdo->prepare("SELECT id, title, created_at, updated_at
FROM posts
WHERE user_id = :user_id
ORDER BY created_at DESC");
$stmt->bindParam(':user_id', $user_id, PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetchAll();
}
// Lägg till fler funktioner för:
// - get_all_posts()
// - create_post(...)
// - update_post(...)
// - delete_post(...)
// - get_user_by_username(...)
// - create_user(...)
// etc.
?>
Användning (t.ex. i post.php
):
<?php
require_once 'includes/config.php';
// require_once 'includes/database.php'; // Behövs inte direkt om funktionen inkluderar den
require_once 'includes/functions.php'; // Inkludera funktionerna
$post_id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
$post = null;
$fetch_error = null;
if ($post_id === false || $post_id <= 0) {
$fetch_error = "Ogiltigt inläggs-ID.";
} else {
try {
$post = get_post_by_id($post_id); // Använd funktionen!
if (!$post) {
$fetch_error = "Inlägget hittades inte.";
}
} catch (PDOException $e) {
error_log("View Post Error (ID: $post_id): " . $e->getMessage());
$fetch_error = "Kunde inte hämta blogginlägget just nu.";
}
}
// Resten av HTML-koden...
?>
b) PHP Type Hinting
Genom att lägga till typdeklarationer för funktionsparametrar och returvärden (som i functions.php
-exemplet ovan) kan vi göra koden tydligare och fånga vissa typer av fel tidigare.
int $post_id
: Förväntar sig ett heltal som parameter.: array|false
: Funktionen förväntas returnera antingen en array ellerfalse
.: void
: Om en funktion inte returnerar något.
c) Förbättrad Filstruktur (Exempel)
En vanlig förbättring är att separera HTML-presentationen från PHP-logiken med hjälp av "template"-filer.
.
├── config/
│ └── config.php
│ └── database.php
├── public/ <-- Web root (här pekar webbservern)
│ ├── admin/
│ │ └── index.php # Hanterar logik, inkluderar template
│ │ └── create_post.php
│ │ └── edit_post.php
│ │ └── delete_post.php
│ ├── css/
│ │ └── style.css
│ ├── uploads/ # Fortfarande här, tillgänglig via webben
│ ├── index.php # Hanterar logik, inkluderar template
│ ├── login.php
│ ├── logout.php
│ ├── post.php
│ └── register.php
├── src/ # PHP-kod (inte direkt åtkomlig via webben)
│ └── functions.php # Eller klasser för Posts, Users etc.
└── templates/ # HTML-mallar
├── admin/
│ ├── index.view.php
│ ├── create_post.view.php
│ └── edit_post.view.php
├── partials/ # Återanvändbara delar
│ ├── header.view.php
│ ├── footer.view.php
│ └── nav.view.php
├── index.view.php
├── login.view.php
├── post.view.php
└── register.view.php
I denna struktur skulle t.ex. public/index.php
innehålla PHP-koden för att hämta data och sedan inkludera templates/index.view.php
för att visa den. templates/index.view.php
skulle i sin tur inkludera partials/header.view.php
etc.
d) Ytterligare Förbättringar att Överväga
- Routing: Istället för att ha en PHP-fil för varje URL, använda en "front controller" (
index.php
) och ett routing-bibliotek som dirigerar förfrågningar till rätt kod baserat på URL:en. - Templating Engines: Använda ett mallhanteringssystem som Twig för att ytterligare separera HTML och PHP och erbjuda funktioner som mall-arv.
- Objektorienterad Programmering (OOP): Skapa klasser för
User
ochPost
för att kapsla in data och beteende. - Dependency Injection: Hantera beroenden (som databasanslutningen) på ett mer flexibelt sätt.
- Säkerhet:
- CSRF (Cross-Site Request Forgery) Tokens: Lägga till dolda tokens i formulär (särskilt för POST-operationer som create, update, delete) för att förhindra CSRF-attacker.
- XSS (Cross-Site Scripting) Prevention: Vara ännu noggrannare med att sanera all output med
htmlspecialchars()
eller ett kontextmedvetet mallhanteringssystem. - File Upload Security: Striktare validering av filtyper (inte bara lita på
$_FILES['type']
), eventuellt byta namn på filer, lagra filer utanför webbroten om möjligt.
- Felhantering: Mer sofistikerad felhantering och loggning.
Denna lektion har gett en grundläggande men fungerande CRUD-applikation. Genom att bygga vidare på refaktoreringsidéerna kan du skapa mer robusta och underhållbara PHP-applikationer.
</rewritten_file>
Sessioner och Cookies: Att Komma Ihåg Användare
En av de grundläggande utmaningarna i webbutveckling är att HTTP (Hypertext Transfer Protocol), protokollet som webbläsare och servrar använder för att kommunicera, är stateless (tillståndslöst). Varje HTTP-förfrågan (request) från en webbläsare till en server behandlas som en helt oberoende händelse. Servern har inget inbyggt minne av tidigare förfrågningar från samma användare.
Detta är ett problem när vi vill bygga funktioner som kräver att servern "kommer ihåg" användaren mellan sidladdningar, till exempel:
- Att hålla en användare inloggad.
- Att spara innehållet i en kundvagn i en e-handel.
- Att komma ihåg användarens preferenser (t.ex. språkval).
För att lösa detta behöver vi mekanismer för state management (tillståndshantering) över flera HTTP-förfrågningar. De två vanligaste teknikerna för detta i webbutveckling är cookies (kakor) och sessions (sessioner).
Vad är Cookies?
En cookie är en liten textbaserad datafil som en webbserver kan be webbläsaren att spara på användarens dator. När webbläsaren sedan gör framtida förfrågningar till samma webbserver, skickar den automatiskt med de sparade cookies som är relevanta för den servern.
Hur det fungerar:
- Server skickar cookie: Servern inkluderar en
Set-Cookie
-header i sitt HTTP-svar till webbläsaren.HTTP/1.1 200 OK Content-Type: text/html Set-Cookie: username=KalleAnka; expires=Fri, 31 Dec 2025 23:59:59 GMT; path=/; HttpOnly <!-- HTML-innehåll -->
- Webbläsare sparar cookie: Webbläsaren tar emot svaret och sparar informationen från
Set-Cookie
(namn, värde, utgångsdatum etc.). - Webbläsare skickar cookie: Vid nästa förfrågan till samma domän och sökväg inkluderar webbläsaren en
Cookie
-header med de sparade kakorna.GET /profile.php HTTP/1.1 Host: www.example.com Cookie: username=KalleAnka
- Server läser cookie: Servern (PHP i vårt fall) kan nu läsa värdet från
Cookie
-headern.
Hantera Cookies i PHP
-
Sätta en cookie (
setcookie()
):<?php // setcookie(name, value, expire, path, domain, secure, httponly); // Sätt en cookie som heter 'user_preference' med värdet 'dark_mode' // Giltig i 30 dagar (86400 sekunder/dag * 30) $expiry_time = time() + (86400 * 30); setcookie('user_preference', 'dark_mode', $expiry_time, '/'); // path='/' gör den giltig på hela sajten // Sätt en cookie som bara ska skickas över HTTPS och inte vara åtkomlig via JavaScript setcookie('session_token', 'abc123xyz', 0, '/', '', true, true); // expire = 0 betyder att den raderas när webbläsaren stängs (session cookie) ?>
Viktigt:
setcookie()
måste anropas innan någon HTML eller annan output skickas till webbläsaren, eftersom den modifierar HTTP-headers. -
Läsa en cookie (
$_COOKIE
): Cookies som skickats av webbläsaren finns tillgängliga i den superglobala arrayen$_COOKIE
.<?php // Kolla om en cookie finns och hämta dess värde if (isset($_COOKIE['user_preference'])) { $preference = $_COOKIE['user_preference']; echo "Din sparade preferens är: " . htmlspecialchars($preference); } else { echo "Ingen preferens sparad."; } ?>
-
Ta bort en cookie: Sätt en ny cookie med samma namn, men med ett utgångsdatum i det förflutna.
<?php // Ta bort 'user_preference'-cookien setcookie('user_preference', '', time() - 3600, '/'); ?>
För- och Nackdelar med Cookies
- Fördelar: Enkla att använda för att spara små mängder icke-känslig data på klientsidan, kan ha lång livslängd.
- Nackdelar:
- Säkerhet: Data lagras på användarens dator och kan läsas och potentiellt manipuleras.
- Integritet: Används ofta för spårning mellan webbplatser (tredjepartscookies), vilket lett till striktare regler (GDPR) och webbläsarinställningar.
- Storleksbegränsning: Webbläsare har gränser för antal cookies per domän och total storlek (ofta runt 4KB per cookie).
- Skickas med varje request: Kan öka mängden data som skickas i onödan.
Vad är Sessioner?
Sessioner är ett sätt att lagra användarspecifik information på servern under tiden användaren interagerar med webbplatsen. Istället för att lagra all data i webbläsaren (som med cookies), lagrar man bara en unik identifierare (ett Session ID) i webbläsaren, oftast via en cookie.
Hur det fungerar (typiskt flöde):
- Användare besöker sidan: Första gången en användare startar en session (eller om ingen giltig session-ID finns) gör PHP följande:
- Genererar ett unikt Session ID (en lång, slumpmässig sträng).
- Skapar en fil (eller databaspost) på servern associerad med detta ID för att lagra sessionsdata.
- Skickar Session ID:t till webbläsaren via en
Set-Cookie
-header (oftast med namnetPHPSESSID
).
- Webbläsare skickar Session ID: Vid efterföljande förfrågningar skickar webbläsaren tillbaka Session ID-cookien.
- Server identifierar session: PHP tar emot Session ID:t från cookien, letar upp den associerade sessionfilen på servern och laddar dess innehåll in i den superglobala
$_SESSION
-arrayen. - PHP använder sessionsdata: Skriptet kan nu läsa och skriva data till
$_SESSION
-arrayen. - Data sparas: När PHP-skriptet avslutas sparas innehållet i
$_SESSION
-arrayen automatiskt tillbaka till sessionfilen på servern.
sequenceDiagram participant B as Webbläsare participant S as Server (PHP) participant Store as Session Store (Server) B->>+S: GET /sida.php (Ingen session cookie) S->>S: Generera unikt Session ID (t.ex. 'xyz789') S->>Store: Skapa lagring för session 'xyz789' S-->>B: Skicka HTML + Set-Cookie: PHPSESSID=xyz789 B->>B: Spara cookie PHPSESSID=xyz789 B->>+S: GET /annan_sida.php (Cookie: PHPSESSID=xyz789) S->>S: Identifiera session via PHPSESSID='xyz789' S->>Store: Ladda data för session 'xyz789' S->>S: PHP använder/modifierar $_SESSION S->>Store: Spara uppdaterad data för session 'xyz789' S-->>-B: Skicka HTML
Hantera Sessioner i PHP
-
Starta/Återuppta en session (
session_start()
): Denna funktion måste anropas i början av varje skript som behöver tillgång till sessionsdata, innan någon output skickas.<?php // Måste vara FÖRST i filen (eller innan någon output) session_start(); // Nu kan vi använda $_SESSION // ... ?> <!DOCTYPE html> <html> ...
Det är vanligt att inkludera
session_start()
i en gemensam konfigurationsfil (som görs i CRUD-app-exemplet) som inkluderas på alla sidor. -
Lagra data (
$_SESSION
):$_SESSION
är en superglobal associativ array där du kan lagra och hämta sessionsdata.<?php session_start(); // Lagra användarens ID och namn $_SESSION['user_id'] = 123; $_SESSION['username'] = 'KalleAnka'; // Lagra en enkel räknare if (!isset($_SESSION['page_views'])) { $_SESSION['page_views'] = 1; } else { $_SESSION['page_views']++; } echo "Du har besökt " . $_SESSION['page_views'] . " sidor under denna session."; ?>
-
Läsa data (
$_SESSION
): Du läser data precis som från vilken annan array som helst.<?php session_start(); if (isset($_SESSION['username'])) { echo "Välkommen tillbaka, " . htmlspecialchars($_SESSION['username']) . "!"; } else { echo "Välkommen, gäst!"; } ?>
-
Ta bort sessionsdata:
- Ta bort en specifik variabel:
unset($_SESSION['variable_name']);
- Ta bort all sessionsdata:
$_SESSION = [];
- Ta bort en specifik variabel:
-
Förstöra sessionen helt (
session_destroy()
): För att helt avsluta en session (t.ex. vid utloggning) behöver du göra flera saker:<?php session_start(); // Sessionen måste vara startad för att kunna förstöras // 1. Ta bort alla sessionsvariabler $_SESSION = []; // 2. Ta bort sessionscookien från webbläsaren if (ini_get("session.use_cookies")) { $params = session_get_cookie_params(); setcookie(session_name(), '', time() - 42000, $params["path"], $params["domain"], $params["secure"], $params["httponly"] ); } // 3. Förstör sessionen på servern (tar bort sessionfilen) session_destroy(); echo "Du har loggats ut och sessionen är förstörd."; // Ofta följt av en omdirigering // header('Location: login.php'); // exit; ?>
Sessioner vs. Cookies: När Använda Vad?
Egenskap | Cookies | Sessioner |
---|---|---|
Lagringsplats | Användarens webbläsare (Klienten) | Webbservern |
Data | Liten mängd textdata (ca 4KB) | Kan lagra komplex data (arrayer, objekt etc.) |
Säkerhet | Lägre (kan läsas/ändras av klienten) | Högre (data på servern, bara ID hos klienten) |
Livslängd | Kan sättas till lång tid (dagar/år) | Varar tills webbläsaren stängs (default) eller tills den "expirerar" på servern / förstörs manuellt. |
Användning | Preferenser, "Kom ihåg mig", spårning | Användarlogin, kundvagnar, temporär data |
Beroende | Inget serverberoende (efter satt) | Kräver serverresurser för lagring |
Identifiering | Kan användas för att identifiera session | Kräver en identifierare (oftast en cookie) |
Tumregel:
- Använd cookies för att lagra icke-känslig information som du vill ska finnas kvar länge, eller för att identifiera en session.
- Använd sessioner för att lagra känslig eller temporär information som är knuten till en specifik användares besök, särskilt för inloggningsstatus.
Sessionshantering för Användarautentisering
Sessioner är den vanligaste metoden för att hantera användarinloggning på webbplatser byggda med PHP.
Inloggningsprocess (Konceptuellt):
- Användaren skickar användarnamn och lösenord (via ett POST-formulär).
- Servern (PHP) validerar input.
- Servern hämtar användarens data (inklusive den hashade lösenordet) från databasen baserat på användarnamnet.
- Servern använder
password_verify()
för att jämföra det inskickade lösenordet med det hashade lösenordet från databasen. - Om giltigt:
a. Anropa
session_start()
. b. Viktigt: Anropasession_regenerate_id(true);
för att skapa ett nytt session-ID och ogiltigförklara det gamla. Detta skyddar mot Session Fixation-attacker, där en angripare försöker lura användaren att använda ett session-ID som angriparen känner till. c. Lagra nödvändig information i$_SESSION
, framför allt användarens ID:$_SESSION['user_id'] = $user['id'];
. d. Omdirigera användaren till en skyddad sida (t.ex. adminpanelen). - Om ogiltigt: Visa ett felmeddelande.
Kontrollera inloggningsstatus på skyddade sidor:
<?php
session_start();
if (!isset($_SESSION['user_id'])) {
// Inte inloggad, skicka till login
header('Location: login.php');
exit;
}
// Användaren är inloggad, fortsätt ladda sidan...
$user_id = $_SESSION['user_id'];
// ... hämta mer data baserat på $user_id om det behövs ...
?>
Utloggningsprocess:
Som visat tidigare: starta sessionen, töm $_SESSION
, ta bort cookien, förstör sessionen på servern, och omdirigera.
Säkerhetsaspekter för Sessioner
Eftersom sessioner ofta används för att hantera känslig information som inloggningsstatus, är det viktigt att konfigurera dem säkert.
- Session Fixation: Förhindras främst genom att anropa
session_regenerate_id(true)
direkt efter en lyckad inloggning eller när användarens behörighetsnivå ändras. - Session Hijacking (Kapning): När en angripare lyckas stjäla en giltig användares session-ID. Detta kan ske genom:
- Nätverksavlyssning: Om kommunikationen inte är krypterad (HTTP istället för HTTPS), kan ID:t snappas upp.
- Cross-Site Scripting (XSS): Om en angripare kan köra JavaScript i offrets webbläsare kan de försöka läsa session-cookien (om den inte är skyddad).
- Gissning (mycket osannolikt med moderna, långa, slumpmässiga ID:n).
- Åtgärder mot Kapning:
- Använd HTTPS: Krypterar all kommunikation, inklusive session-cookien.
HttpOnly
Cookie Flag: Sättsession.cookie_httponly = 1
(iphp.ini
eller viaini_set
). Detta förhindrar JavaScript från att komma åt cookien, vilket minskar risken vid XSS.Secure
Cookie Flag: Sättsession.cookie_secure = 1
. Detta säkerställer att session-cookien endast skickas över HTTPS-anslutningar. Absolut nödvändigt för produktionsmiljöer.SameSite
Cookie Attribute: Sättsession.cookie_samesite = "Lax"
(bra standard) eller"Strict"
. Detta hjälper till att skydda mot Cross-Site Request Forgery (CSRF)-attacker genom att kontrollera när cookien skickas med förfrågningar från andra webbplatser.
Konfigurera Sessionssäkerhet
Dessa inställningar görs bäst i din php.ini
-fil för att gälla globalt. Om du inte har tillgång till den, kan de ibland sättas i början av dina skript med ini_set()
(innan session_start()
anropas), men det är mindre idealiskt.
Rekommenderade inställningar i php.ini
(eller motsvarande):
; Använd bara cookies för att sprida session ID (säkrare)
session.use_only_cookies = 1
; Skicka bara session cookie över HTTPS
session.cookie_secure = 1
; Gör session cookie otillgänglig för JavaScript
session.cookie_httponly = 1
; Sätt SameSite attributet (Lax är en bra balans)
session.cookie_samesite = "Lax"
; Använd starkare hash-algoritm för session ID (om tillgängligt)
; session.sid_entropy_length = 32 (eller högre)
; session.sid_bits_per_character = 5 (eller 6)
Sammanfattning
Cookies och sessioner är avgörande verktyg för att skapa dynamiska och användarvänliga webbapplikationer genom att överbrygga HTTP:s tillståndslösa natur. Cookies lagrar data på klienten och är bra för icke-känsliga preferenser, medan sessioner lagrar data på servern och är standardmetoden för att hantera inloggningsstatus och annan känslig temporär data. Att förstå hur man använder dem säkert, särskilt genom att skydda session-ID:t och konfigurera cookie-attribut korrekt, är fundamentalt för säker webbutveckling med PHP.
Webbsäkerhet i PHP: Vanliga Hot och Skydd
Att bygga en fungerande webbapplikation är en sak, men att göra den säker är en helt annan och minst lika viktig uppgift. En osäker applikation kan leda till att användardata läcker, att obehöriga får kontroll över systemet, eller att webbplatsen används för att sprida skadlig kod. Detta avsnitt täcker några av de mest kritiska säkerhetsriskerna och hur du skyddar din PHP-applikation mot dem.
1. SQL Injection (SQL-injektion)
Vad är det? SQL Injection är en av de mest kända och farligaste attackerna. Den inträffar när en angripare lyckas "injicera" (infoga) skadlig SQL-kod i en databasfråga via osanerad användarinput (t.ex. från ett formulärfält eller en URL-parameter). Om applikationen bygger ihop SQL-frågan genom att direkt klistra in denna input, kan angriparen manipulera frågan till att göra saker den inte borde, som att:
- Hämta känslig data (andra användares lösenord, kreditkortsinformation).
- Modifiera eller radera data.
- I vissa fall, få kontroll över hela databasservern.
Exempel på Sårbar Kod:
Anta att du vill hämta en användare baserat på ett ID från en URL (login.php?id=123
).
<?php
// MYCKET OSÄKERT EXEMPEL - ANVÄND ALDRIG SÅ HÄR!
require_once 'includes/database.php';
$user_id = $_GET['id']; // Hämta ID direkt från URL
$pdo = connect_db();
// Bygger ihop SQL-frågan genom att klistra in användarinput direkt!
$sql = "SELECT username, email FROM users WHERE id = " . $user_id;
echo "Exekverar: " . $sql . "\n";
// Kör den osäkra frågan
// $stmt = $pdo->query($sql);
// $user = $stmt->fetch();
// ... visa användardata ...
?>
Problem: Vad händer om en angripare anropar sidan med login.php?id=123 OR 1=1
?
SQL-frågan blir då:
SELECT username, email FROM users WHERE id = 123 OR 1=1
Villkoret OR 1=1
är alltid sant, så frågan kommer att returnera alla användare i databasen, inte bara användare 123!
Ännu värre, om angriparen skickar login.php?id=123; DROP TABLE users; --
SQL-frågan (beroende på databas) kan bli:
SELECT username, email FROM users WHERE id = 123; DROP TABLE users; --
Detta skulle kunna radera hela users
-tabellen! (;
separerar kommandon, --
startar en kommentar).
Hur man Skyddar Sig: Prepared Statements (Förberedda Uttryck)
Det absolut viktigaste skyddet mot SQL Injection är att aldrig klistra in användarinput direkt i SQL-frågor. Använd istället Prepared Statements, som vi gör genomgående i crud-app.md
med PDO.
Så här fungerar det:
- Förbered Frågan: Skicka SQL-frågan till databasen med platshållare (
:
eller?
) istället för de faktiska värdena. - Bind Parametrar: Tala om för databasen vilka variabler som ska användas för varje platshållare och vilken datatyp de har.
- Exekvera: Kör den förberedda frågan. Databasen sätter då in värdena på ett säkert sätt, och behandlar dem alltid som data, aldrig som SQL-kod.
Säker Kod (med PDO):
<?php
// SÄKERT EXEMPEL MED PREPARED STATEMENTS
require_once 'includes/database.php';
$user_id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT); // Validera först input!
if ($user_id) {
try {
$pdo = connect_db();
// 1. Förbered frågan med platshållare
$sql = "SELECT username, email FROM users WHERE id = :user_id";
$stmt = $pdo->prepare($sql);
// 2. Bind parametern (värdet från $user_id)
$stmt->bindParam(':user_id', $user_id, PDO::PARAM_INT);
// 3. Exekvera
$stmt->execute();
$user = $stmt->fetch();
if ($user) {
echo "Användare hittad: " . htmlspecialchars($user['username']);
} else {
echo "Användare inte hittad.";
}
} catch (PDOException | Exception $e) { // PDOException eller annan valideringsfel
error_log("SQL Error: " . $e->getMessage());
echo "Ett fel uppstod.";
}
} else {
echo "Ogiltigt ID.";
}
?>
Om en angripare nu försöker med ?id=123 OR 1=1
, kommer filter_input
att returnera false
(eftersom det inte är ett heltal). Om de försöker med ?id=123; DROP TABLE users; --
, även om det skulle passera filter_input
(vilket det inte gör), skulle databasen behandla hela strängen "123; DROP TABLE users; --"
som det värde som ska bindas till :userid
(och troligen misslyckas med att hitta en match eller ge ett typfel), inte som separat SQL-kod.
Alltid använda Prepared Statements när användarinput är inblandad i SQL-frågor!
2. Cross-Site Scripting (XSS)
Vad är det? XSS inträffar när en angripare lyckas injicera skadlig kod (oftast JavaScript) i en webbsida som sedan visas för andra användare. När den andra användarens webbläsare renderar sidan, exekveras den skadliga koden inom kontexten av den legitima webbplatsen.
Detta kan användas för att:
- Stjäla användarens session cookies (och därmed kapa deras session).
- Omdirigera användaren till en falsk inloggningssida.
- Modifiera innehållet på sidan användaren ser.
- Utföra åtgärder i användarens namn.
Det vanligaste sättet XSS sker är när en webbapplikation tar emot data från en användare (t.ex. en kommentar, ett profilnamn, ett sökord) och sedan skriver ut denna data direkt i HTML-koden utan att sanera den först.
Exempel på Sårbar Kod:
Anta att du har en gästbok där användare kan lämna meddelanden.
<?php
// OSÄKERT EXEMPEL - VISAR ANVÄNDARINPUT DIREKT!
// Antag att $comment hämtats från databasen, där en användare skrivit:
// <script>alert('XSS Attack! Din cookie: ' + document.cookie);</script>
$comment = "<script>alert('XSS Attack! Din cookie: ' + document.cookie);</script>";
?>
<!DOCTYPE html>
<html>
<body>
<h1>Kommentarer</h1>
<div class="comment">
<?php echo $comment; // SKRIVER UT SKADLIG KOD DIREKT! ?>
</div>
</body>
</html>
När en annan användare besöker sidan, kommer deras webbläsare att se <script>...</script>
-taggen och exekvera JavaScript-koden inuti den. I detta fall visas en popup med användarens cookies (potentiellt session-ID:t!).
Hur man Skyddar Sig: Sanera All Output (htmlspecialchars
)
Den grundläggande regeln är: Lita aldrig på data som kommer utifrån (användarinput, data från databas som kan ha manipulerats etc.). Innan du skriver ut sådan data i ett HTML-sammanhang, måste du sanera den så att den behandlas som ren text och inte som HTML-kod.
Den vanligaste och viktigaste funktionen för detta i PHP är htmlspecialchars()
.
htmlspecialchars()
konverterar specialtecken som har en HTML-betydelse till deras motsvarande HTML-entiteter:
&
blir&
"
(dubbelt citationstecken) blir"
(omENT_QUOTES
är satt)'
(enkelt citationstecken) blir'
eller'
(omENT_QUOTES
är satt)<
(mindre än) blir<
>
(större än) blir>
Säker Kod:
<?php
// SÄKERT EXEMPEL - SANERAR OUTPUT
$comment = "<script>alert('XSS Attack! Din cookie: ' + document.cookie);</script>";
?>
<!DOCTYPE html>
<html>
<body>
<h1>Kommentarer</h1>
<div class="comment">
<?php echo htmlspecialchars($comment, ENT_QUOTES, 'UTF-8'); ?>
</div>
</body>
</html>
Nu kommer HTML-koden som webbläsaren tar emot att se ut ungefär så här:
<div class="comment">
<script>alert('XSS Attack! Din cookie: ' + document.cookie);</script>
</div>
Webbläsaren kommer att visa detta som text, men inte exekvera det som ett skript, eftersom <
och >
har omvandlats till ofarliga entiteter.
Argument till htmlspecialchars()
:
$string
: Strängen som ska saneras.$flags
: OftastENT_QUOTES
för att konvertera både enkel- och dubbelcitationstecken. Detta är viktigt om du skriver ut data inuti HTML-attribut (t.ex.value="..."
).$encoding
: Ange teckenkodningen, nästan alltid'UTF-8'
för moderna webbapplikationer.
Använd htmlspecialchars()
konsekvent på ALL data som kommer utifrån innan den skrivs ut i HTML!
Not: Om du behöver skriva ut data i andra sammanhang (t.ex. inuti ett JavaScript-block eller ett CSS-värde) krävs andra typer av sanering/kodning. htmlspecialchars
är specifikt för HTML-kroppen och HTML-attribut.
3. Password Hashing (Säker Lösenordshantering)
Vad är problemet? Att lagra användarnas lösenord i klartext i databasen är extremt oansvarigt. Om databasen blir stulen får angriparen tillgång till alla lösenord direkt.
Att använda enkla hash-funktioner som MD5 eller SHA1 är inte heller säkert längre. Dessa är:
- Snabba: Gör dem lätta att knäcka med brute-force-attacker (testa miljontals lösenord per sekund).
- Kollisionsbenägna (särskilt MD5): Olika lösenord kan ge samma hash.
- Inte saltade (i sig själva): Samma lösenord ger alltid samma hash, vilket gör dem sårbara för "rainbow table"-attacker (förberäknade tabeller med hashvärden för vanliga lösenord).
Hur man Skyddar Sig: Starka, Saltade Hashar (password_hash
/ password_verify
)
Vad är en hashning-funktion?
Innan vi dyker in i PHP:s funktioner, låt oss förstå vad hashing (hashning) innebär. En hashfunktion är en matematisk algoritm som tar en indata av valfri storlek (t.ex. ett lösenord) och omvandlar den till en utdata av fast storlek, kallad en hash eller ett hashvärde (ibland "digest").
Tänk dig en mixer:
- Du lägger i ingredienser (ditt lösenord, "mittLösenord123").
- Du kör mixern (hashfunktionen, t.ex. Bcrypt).
- Ut kommer en smoothie (hashen, t.ex.
$2y$10$abcdef...xyz
).
Viktiga egenskaper hos kryptografiska hashfunktioner (som används för lösenord):
- Envägsfunktion (One-way): Det är extremt svårt (praktiskt taget omöjligt) att gå från smoothien (hashen) tillbaka till de ursprungliga ingredienserna (lösenordet). Du kan inte "köra mixern baklänges".
- Deterministisk: Samma ingredienser (samma lösenord och samma salt, se nedan) i samma mixer (samma hashfunktion) ger alltid exakt samma smoothie (hash).
- Snabb att beräkna: Det ska gå relativt fort att skapa hashen från lösenordet.
- Kollisionsresistent: Det ska vara extremt svårt att hitta två olika indata (lösenord) som ger samma hash.
- Lavineffekt (Avalanche effect): En liten ändring i indata (t.ex. ändra en bokstav i lösenordet) ska resultera i en helt annorlunda hash.
Varför inte bara hasha? Problemet med salt. Om vi bara använde en enkel hashfunktion (som gamla MD5 eller SHA1) skulle samma lösenord alltid ge samma hash. Angripare kan då använda "rainbow tables" – gigantiska listor med förberäknade hashar för vanliga lösenord. Om din lagrade hash finns i deras tabell, vet de omedelbart ditt lösenord.
Lösningen är saltning. Ett salt är en slumpmässig datasträng som läggs till lösenordet innan det hashas.
Liknelse: Tänk att du lägger till en unik, hemlig krydda (saltet) till dina ingredienser innan du mixar dem. Även om två personer använder exakt samma grundingredienser (lösenord), kommer deras smoothies (hashar) att bli helt olika eftersom de använde olika hemliga kryddor (salt).
Moderna funktioner som PHP:s password_hash()
:
- Genererar ett unikt, slumpmässigt salt för varje lösenord.
- Kombinerar saltet med lösenordet.
- Hashar kombinationen med en stark algoritm (som Bcrypt).
- Sparar både saltet och hashen tillsammans i en enda sträng i databasen.
När password_verify()
används, extraherar den saltet från den lagrade strängen, kombinerar det med lösenordet som användaren skrev in, hashar det, och jämför sedan resultatet med den lagrade hashen.
graph LR subgraph Hashning utan salt P1["Lösenord: 'password123'"] --> H1(Hashfunktion MD5); P2["Lösenord: 'password123'"] --> H1; H1 --> Hash1["Hash: 'e8636ea0...' "]; Hash1 --> DB1["Databas: lagrar 'e8636ea0...'"]; Hash1 --> DB2["Databas: lagrar 'e8636ea0...'"]; end subgraph Hashning med salt - password_hash P3["Lösenord: 'password123'"] --> S1(Generera Salt1: 'abc'); S1 --> C1("Kombinera: 'abc' + 'password123'"); C1 --> H2(Hashfunktion Bcrypt); H2 --> HashSalt1["Hash+Salt: '$2y$10$abc...'"]; HashSalt1 --> DB3["Databas: lagrar '$2y$10$abc...'"]; P4["Lösenord: 'password123'"] --> S2(Generera Salt2: 'xyz'); S2 --> C2("Kombinera: 'xyz' + 'password123'"); C2 --> H2; H2 --> HashSalt2["Hash+Salt: '$2y$10$xyz...'"]; HashSalt2 --> DB4["Databas: lagrar '$2y$10$xyz...'"]; end style P1 fill:#4ac style P2 fill:#4ac style DB1 fill:#4ac style DB2 fill:#4ac
Diagrammet illustrerar hur samma lösenord utan salt ger samma hash, medan password_hash
med unika salt ger olika lagrade hashvärden för samma lösenord, vilket skyddar mot rainbow tables.
PHP har sedan version 5.5 inbyggda, moderna funktioner för säker lösenordshantering:
-
password_hash($password, PASSWORD_DEFAULT)
:- Tar ett lösenord i klartext (
$password
). - Använder en stark, långsam hash-algoritm (oftast Bcrypt som standard med
PASSWORD_DEFAULT
). Långsamheten gör brute-force mycket svårare. - Genererar automatiskt ett unikt, slumpmässigt salt för varje lösenord.
- Kombinerar algoritmen, saltet och hashen till en enda sträng som är säker att lagra i databasen (t.ex. i en
VARCHAR(255)
-kolumn).
- Tar ett lösenord i klartext (
-
password_verify($password, $hash)
:- Tar det lösenord användaren anger vid inloggning (
$password
) och den lagrade hashen från databasen ($hash
). - Extraherar automatiskt algoritmen och saltet från
$hash
. - Hashar det angivna lösenordet med samma algoritm och salt.
- Jämför resultatet säkert (mot tidattacker) med den lagrade hashen.
- Returnerar
true
om lösenordet matchar, annarsfalse
.
- Tar det lösenord användaren anger vid inloggning (
Exempel (som i crud-app.md
):
<?php
// Vid registrering:
$passwordFromUser = "mittSäkraLösenord123";
$hashedPasswordForDatabase = password_hash($passwordFromUser, PASSWORD_DEFAULT);
// Spara $hashedPasswordForDatabase i users.password_hash
echo "Lagrad hash: " . $hashedPasswordForDatabase . "\n";
// Exempel output: $2y$10$abcdefghijklmnopqrstuvwx/ABCDEFGHIJKLMNOPQRSTU.abcdefghijklm
// Vid inloggning:
$passwordAttempt = "mittSäkraLösenord123"; // Lösenord från formulär
$hashFromDatabase = "$2y$10$abcdefghijklmnopqrstuvwx/ABCDEFGHIJKLMNOPQRSTU.abcdefghijklm"; // Hämtad från DB
if (password_verify($passwordAttempt, $hashFromDatabase)) {
echo "Lösenordet är korrekt!";
} else {
echo "Felaktigt lösenord.";
}
// Försök med fel lösenord:
$wrongPasswordAttempt = "felLösenord";
if (password_verify($wrongPasswordAttempt, $hashFromDatabase)) {
echo "Korrekt?!"; // Körs inte
} else {
echo "Felaktigt lösenord (som förväntat).";
}
?>
Använd alltid password_hash()
och password_verify()
för lösenord! PASSWORD_DEFAULT
säkerställer att du automatiskt använder den bästa tillgängliga algoritmen som stöds av din PHP-version.
4. Cross-Site Request Forgery (CSRF)
Vad är det? CSRF är en attack där en angripare lurar en inloggad användares webbläsare att skicka en oönskad förfrågan till en webbapplikation där användaren är autentiserad. Angriparen kan inte se svaret, men kan få användarens webbläsare att utföra en handling.
Exempel: En användare är inloggad på sin blogg (myblog.com
). De besöker sedan en skadlig webbplats (evil.com
). På evil.com
finns en dold bild eller ett formulär som automatiskt skickar en POST-förfrågan till myblog.com/admin/delete_post.php
med post_id=42
. Eftersom användaren är inloggad på myblog.com
, skickar webbläsaren med den giltiga session-cookien, och delete_post.php
tror att det är användaren själv som vill radera inlägget.
Detta fungerar eftersom webbläsare automatiskt skickar med relevanta cookies (som session-ID) för en domän, oavsett varifrån förfrågan initierades.
CSRF-attacker riktar sig främst mot state-changing requests (förfrågningar som ändrar data, t.ex. POST, PUT, DELETE), inte rena GET-förfrågningar som bara hämtar data.
Exempel på Sårbar Kod (Konceptuellt):
Om admin/delete_post.php
bara kollar om användaren är inloggad och sedan raderar baserat på $_POST['post_id']
utan någon ytterligare kontroll, är den sårbar.
<?php
// SÅRBAR DELETE (utan CSRF-skydd)
require_once '../includes/config.php';
if (!isset($_SESSION['user_id'])) { exit; }
require_once '../includes/database.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$post_id = filter_input(INPUT_POST, 'post_id', FILTER_VALIDATE_INT);
$logged_in_user_id = $_SESSION['user_id'];
if ($post_id) {
try {
$pdo = connect_db();
// Kanske kontrolleras ägarskap, men ingen CSRF-token!
$stmt = $pdo->prepare("DELETE FROM posts WHERE id = :id AND user_id = :user_id");
$stmt->bindParam(':id', $post_id, PDO::PARAM_INT);
$stmt->bindParam(':user_id', $logged_in_user_id, PDO::PARAM_INT);
$stmt->execute();
// ... omdirigering ...
} catch (PDOException $e) { /* ... */ }
}
}
?>
Hur man Skyddar Sig: Anti-CSRF Tokens (Synchronizer Token Pattern)
Den vanligaste metoden är att använda anti-CSRF tokens:
- Generera Token: När servern visar ett formulär som utför en state-changing action (t.ex. skapa, uppdatera, radera), genererar den en unik, slumpmässig, hemlig token (en sträng).
- Lagra Token: Servern lagrar denna token i användarens session (
$_SESSION['csrf_token']
). - Skicka Token: Servern inkluderar samma token som ett dolt fält (
<input type="hidden">
) i HTML-formuläret som skickas till användarens webbläsare. - Skicka tillbaka Token: När användaren skickar in formuläret, skickas den dolda token tillbaka till servern tillsammans med övrig formulärdata (i
$_POST
). - Verifiera Token: På serversidan, innan den state-changing åtgärden utförs, jämför servern token som skickades med formuläret (
$_POST['csrf_token']
) med den token som är lagrad i sessionen ($_SESSION['csrf_token']
).- Om de matchar, är det troligtvis en legitim förfrågan initierad av användaren via formuläret.
- Om de inte matchar (eller om token saknas), avvisas förfrågan eftersom den kan vara en CSRF-attack (en angripare kan inte gissa eller läsa token i användarens session).
Exempel på Implementering (Konceptuellt):
Generera och visa formulär (t.ex. i admin/index.php
för delete-knappen):
<?php
session_start(); // Eller via config.php
// Generera token om den inte redan finns i sessionen
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32)); // Skapa en säker, slumpmässig token
}
$csrf_token = $_SESSION['csrf_token'];
// ... (kod för att lista inlägg) ...
// I loopen för varje post:
?>
<form action="delete_post.php" method="post" style="display: inline;" onsubmit="return confirm('...');">
<input type="hidden" name="post_id" value="<?php echo $post['id']; ?>">
<!-- Lägg till den dolda CSRF-token -->
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token); ?>">
<button type="submit" class="delete-btn">Radera</button>
</form>
<?php
// ... (resten av loopen) ...
?>
Verifiera på serversidan (t.ex. i admin/delete_post.php
):
<?php
require_once '../includes/config.php'; // Ger session_start()
// ... (Session Check) ...
require_once '../includes/database.php';
$errors = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Hämta token från POST och session
$submitted_token = $_POST['csrf_token'] ?? '';
$session_token = $_SESSION['csrf_token'] ?? ' '; // Använd blanksteg om ej satt för säker jämförelse
// 1. Verifiera CSRF-token!
if (!hash_equals($session_token, $submitted_token)) {
// Token matchar inte eller saknas!
$errors[] = "Ogiltig förfrågan (CSRF-skydd). Försök igen.";
// Avbryt direkt!
} else {
// Token är OK, fortsätt med resten av logiken...
$post_id = filter_input(INPUT_POST, 'post_id', FILTER_VALIDATE_INT);
// ... (resten av delete-logiken: hämta bild, kontrollera ägarskap, radera) ...
}
} else {
$errors[] = "Endast POST-förfrågningar tillåtna.";
}
// ... (Visa fel eller omdirigera) ...
?>
bin2hex(random_bytes(32))
genererar en kryptografiskt säker slumpmässig token.hash_equals($session_token, $submitted_token)
är viktig för att jämföra tokens. Den utför en tidsoberoende jämförelse, vilket skyddar mot vissa typer av "timing attacks". Använd inte==
eller===
för att jämföra hemligheter som CSRF-tokens eller API-nycklar.
Implementera CSRF-skydd för alla formulär som utför state-changing actions!
5. Filuppladdningssäkerhet
Att tillåta användare att ladda upp filer medför risker:
- Skadlig kod: En användare kan ladda upp en fil som innehåller server-side kod (t.ex. en
.php
-fil) som sedan kan exekveras på servern om den placeras i en webbåtkomlig mapp. - Resursutmattning: Användare kan ladda upp extremt stora filer som tar upp diskutrymme eller bandbredd.
- XSS via filnamn/metadata: Ibland kan skadlig kod gömmas i filnamn eller filens metadata.
Grundläggande Skydd (som i crud-app.md
):
- Validera Filtyp: Kontrollera filens MIME-typ (
$_FILES['image']['type']
) mot en vitlista av tillåtna typer. Lita inte blint på detta, då det kan förfalskas av klienten. En mer robust metod är att använda filinnehållsanalys (t.ex.finfo_file
i PHP). - Validera Filstorlek: Kontrollera
$FILES['image']['size']
mot en rimlig maxgräns. - Byt Namn på Filen: Spara aldrig filen med det namn användaren angav. Generera ett unikt, slumpmässigt filnamn (som vi gjorde med
uniqid()
) för att undvika kollisioner och potentiellt skadliga filnamn. - Använd
move_uploaded_file()
: Detta är den enda säkra funktionen för att flytta en uppladdad fil från dess temporära plats till den slutgiltiga destinationen. Den utför extra kontroller. - Begränsa Mapprättigheter: Se till att mappen där filer sparas (
uploads/
) inte har exekveringsrättigheter för webbservern.
Ytterligare Förbättringar:
- Lagra Filer Utanför Webbroten: Om möjligt, lagra uppladdade filer i en mapp som inte är direkt åtkomlig via webben. Skapa sedan ett PHP-skript som agerar mellanhand: det kontrollerar behörighet och skickar sedan ut filens innehåll med rätt
Content-Type
-header. - Skanna Filer: Använd antivirusprogram eller andra verktyg för att skanna uppladdade filer efter skadlig kod.
Slutsats
Webbsäkerhet är ett komplext och ständigt föränderligt område. De punkter vi tagit upp här – SQL Injection, XSS, säker lösenordshantering, CSRF och grundläggande filuppladdningssäkerhet – är några av de absolut viktigaste att ha koll på som PHP-utvecklare. Kom ihåg:
- Filtrera input, sanera output.
- Använd Prepared Statements för all SQL som involverar extern data.
- Använd
password_hash()
ochpassword_verify()
för lösenord. - Skydda state-changing actions med Anti-CSRF tokens.
- Var försiktig med filuppladdningar.
- Håll din PHP-version och dina beroenden uppdaterade.
- Använd alltid HTTPS i produktion.
Att tänka på säkerhet från början och genom hela utvecklingsprocessen är avgörande för att bygga pålitliga och säkra webbapplikationer.
Kapitel 7: Praktiska Övningar
Dessa övningar är tänkta att hjälpa dig att befästa kunskaperna från kapitel 7. Försök att lösa dem själv först innan du tittar på lösningsförslagen.
Övningar
PHP Syntax och Grundläggande
- Hej Världen: Skriv ett PHP-skript som skriver ut texten "Hej Världen!" till webbläsaren.
- Variabler och Utskrift: Deklarera en variabel
$productName
med värdet "Laptop" och en variabel$productPrice
med värdet999.90
. Skriv ut en mening som "Produkten Laptop kostar 999.9 kr." med hjälp av dessa variabler och strängkonkatenering (.
). if/else
: Skriv enif/else
-sats som kollar om variabeln$productPrice
(från övning 2) är större än 500. Om den är det, skriv ut "Dyr produkt.", annars skriv ut "Billig produkt.".- Funktion: Skapa en funktion
calculateDiscount($price, $percentage)
som tar ett pris och en procentsats (som heltal, t.ex. 10 för 10%) och returnerar det nya priset efter rabatten. Använd type hinting för parametrarna (float
för pris,int
för procent) och returvärdet (float
). - Anropa Funktion: Anropa funktionen
calculateDiscount
med priset250.0
och procentsatsen20
. Skriv ut resultatet. match
-uttryck: Skriv ettmatch
-uttryck som tar en variabel$statusCode
(anta att den innehåller ett heltal som 200, 404, eller 500) och returnerar en beskrivande sträng ("OK", "Not Found", "Server Error", eller "Unknown" för andra värden). Skriv ut den returnerade strängen.- Strikta Typer: Hur aktiverar du strikt typläge i en PHP-fil, och varför är det rekommenderat?
Arrayer och Loopar
- Indexerad Array: Skapa en indexerad array
$cities
som innehåller strängarna "Stockholm", "Göteborg", "Malmö". - Åtkomst: Skriv ut den första staden ("Stockholm") från arrayen
$cities
. - Lägg till: Lägg till staden "Uppsala" i slutet av
$cities
-arrayen. count()
: Skriv ut hur många städer som finns i$cities
-arrayen.- Associativ Array: Skapa en associativ array
$car
med nycklarnabrand
(värde "Volvo") ochmodel
(värde "XC60"). - Åtkomst (Associativ): Skriv ut bilens modell från
$car
-arrayen. foreach
(Värden): Använd enforeach
-loop för att skriva ut varje stad från$cities
-arrayen på en ny rad.foreach
(Nyckel & Värde): Använd enforeach
-loop för att skriva ut både nyckel och värde från$car
-arrayen, t.ex. "brand: Volvo".in_array()
: Kontrollera om staden "Malmö" finns i$cities
-arrayen och skriv ut "Ja" eller "Nej".implode()
: Skapa en sträng från$cities
-arrayen där städerna är separerade med kommatecken och mellanslag (", "). Skriv ut strängen.
SQL (Syntax)
Anta att du har en tabell customers
med kolumnerna id
(INT, PK), name
(VARCHAR), email
(VARCHAR), city
(VARCHAR) och en tabell orders
med kolumnerna order_id
(INT, PK), customer_id
(INT, FK till customers.id), order_date
(DATE), amount
(DECIMAL).
- SELECT Alla: Skriv SQL för att hämta alla kolumner för alla kunder.
- SELECT Specifika Kolumner & WHERE: Skriv SQL för att hämta
name
ochemail
för kunder som bor i "Göteborg". - ORDER BY & LIMIT: Skriv SQL för att hämta namnen på de 10 första kunderna sorterade i bokstavsordning (A-Ö).
- INSERT: Skriv SQL för att lägga till en ny kund med namn "Lisa Berg" och e-post "lisa@example.com" som bor i "Malmö".
- UPDATE: Skriv SQL för att ändra staden till "Lund" för kunden med
id
15. - DELETE: Skriv SQL för att ta bort kunden med
id
20. - INNER JOIN: Skriv SQL för att hämta order-ID (
orders.order_id
) och kundens namn (customers.name
) för alla ordrar.
Sessioner och Säkerhet (Koncept/PHP)
- Starta Session: Vilken PHP-funktion används för att starta eller återuppta en session, och var i skriptet bör den placeras?
- Lagra i Session: Skriv PHP-koden för att lagra värdet "dark" i sessionsvariabeln
theme
. - Kontrollera Session: Skriv en
if
-sats i PHP som kontrollerar om sessionsvariabelnuser_id
är satt. - Förhindra XSS: Vilken PHP-funktion bör du alltid använda när du skriver ut data som kan ha kommit från en användare i ett HTML-sammanhang? Ge ett exempel.
- Förhindra SQL Injection: Vilken teknik bör du alltid använda när du bygger SQL-frågor som innehåller värden från användarinput? Nämn den PHP-funktion (i PDO) som initierar denna teknik.
- Lösenordshantering: Vilka två PHP-funktioner (introducerade i PHP 5.5) är standard för att hantera lösenord säkert, och vad gör de?
- CSRF-skydd: Vad är en vanlig metod för att skydda formulär mot Cross-Site Request Forgery (CSRF)-attacker?
Mini-CRUD (PHP/PDO)
Anta att du har en PDO-anslutning i variabeln $pdo
och en tasks
-tabell med kolumnerna id
(INT, PK, AUTO_INCREMENT) och description
(TEXT).
- Hämta Input: Skriv PHP-koden för att säkert hämta värdet från ett formulärfält med
name="task_description"
som skickats via POST. - INSERT (PDO): Skriv PHP-koden som använder
$pdo
och Prepared Statements för att infoga värdet från föregående uppgift (lagrat i en variabel$description
) itasks
-tabellensdescription
-kolumn. - SELECT Alla (PDO): Skriv PHP-koden som använder
$pdo
för att hämta alla rader fråntasks
-tabellen (endastid
ochdescription
) och lagra resultatet i en array$allTasks
.
Klasser (OOP Grunderna)
- Definiera Klass: Skapa en klass
Book
med två publika egenskaper:$title
(string) och$author
(string). Lägg till en publik metoddisplayInfo()
som skriver ut "Title: [titel], Author: [författare]". - Skapa Objekt: Skapa två olika
Book
-objekt från klassen ovan. Sätt olika värden förtitle
ochauthor
för varje objekt. AnropadisplayInfo()
-metoden på båda objekten. - Konstruktor: Modifiera
Book
-klassen så att den har en konstruktor (__construct
) som tar emot titel och författare som argument och sätter egenskaperna direkt när objektet skapas. Skapa ett nyttBook
-objekt med hjälp av konstruktorn och anropadisplayInfo()
. - Synlighet & Getters: Modifiera
Book
-klassen igen:- Gör egenskaperna
$title
och$author
privata (private
). - Lägg till en ny publik egenskap
$pages
(int). - Uppdatera konstruktorn så att den även tar emot antal sidor (
$pages
) och sätter alla tre egenskaperna. - Skapa publika "getter"-metoder:
getTitle()
,getAuthor()
, ochgetPages()
som returnerar värdet på respektive privat egenskap.
- Gör egenskaperna
- Använda Getters: Skapa ett
Book
-objekt från den modifierade klassen i föregående uppgift. Använd getter-metoderna för att hämta och skriva ut bokens titel, författare och antal sidor.
Lösningsförslag
PHP Syntax och Grundläggande
-
<?php echo "Hej Världen!"; ?>
eller
<?= "Hej Världen!"; ?>
-
<?php $productName = "Laptop"; $productPrice = 999.90; echo "Produkten " . $productName . " kostar " . $productPrice . " kr."; // Alternativt med dubbla citationstecken för interpolering: // echo "Produkten $productName kostar $productPrice kr."; ?>
-
<?php $productPrice = 999.90; // Från övning 2 if ($productPrice > 500) { echo "Dyr produkt."; } else { echo "Billig produkt."; } ?>
-
<?php declare(strict_types=1); function calculateDiscount(float $price, int $percentage): float { if ($percentage < 0 || $percentage > 100) { // Ohanterad procentsats, returnera ursprungspris eller kasta fel? throw new InvalidArugmentException('$percentage måste vara mellan 0-100') } $discountFactor = 1 - ($percentage / 100.0); return $price * $discountFactor; } ?>
-
<?php // Antag att funktionen från övning 4 finns $discountedPrice = calculateDiscount(250.0, 20); echo "Pris efter 20% rabatt: " . $discountedPrice; // Output: Pris efter 20% rabatt: 200 ?>
-
<?php $statusCode = 404; $message = match ($statusCode) { 200 => "OK", 404 => "Not Found", 500 => "Server Error", default => "Unknown", }; echo "Status: " . $message; // Output: Status: Not Found ?>
-
Strikt typläge aktiveras med
declare(strict_types=1);
överst i PHP-filen (efter<?php
-taggen). Det rekommenderas för att PHP inte ska försöka tvångsomvandla datatyper mellan inkompatibla typer vid funktionsanrop, vilket fångar fel tidigare och gör koden mer förutsägbar och robust.
Arrayer och Loopar
-
<?php $cities = ["Stockholm", "Göteborg", "Malmö"]; // Eller äldre syntax: $cities = array("Stockholm", "Göteborg", "Malmö"); ?>
-
<?php $cities = ["Stockholm", "Göteborg", "Malmö"]; echo $cities[0]; // Output: Stockholm ?>
-
<?php $cities = ["Stockholm", "Göteborg", "Malmö"]; $cities[] = "Uppsala"; // Lägger till i slutet // Alternativt: array_push($cities, "Uppsala"); print_r($cities); // Visar hela arrayen för kontroll ?>
-
<?php $cities = ["Stockholm", "Göteborg", "Malmö", "Uppsala"]; echo count($cities); // Output: 4 ?>
-
<?php $car = [ 'brand' => 'Volvo', 'model' => 'XC60' ]; ?>
-
<?php $car = ['brand' => 'Volvo', 'model' => 'XC60']; echo $car['model']; // Output: XC60 ?>
-
<?php $cities = ["Stockholm", "Göteborg", "Malmö", "Uppsala"]; foreach ($cities as $city) { echo $city . "\n"; // \n för ny rad om det körs i terminalen, <br> för webbläsare } ?>
-
<?php $car = ['brand' => 'Volvo', 'model' => 'XC60']; foreach ($car as $key => $value) { echo $key . ": " . $value . "\n"; } ?>
-
<?php $cities = ["Stockholm", "Göteborg", "Malmö", "Uppsala"]; if (in_array("Malmö", $cities)) { echo "Ja"; } else { echo "Nej"; } ?>
-
<?php $cities = ["Stockholm", "Göteborg", "Malmö", "Uppsala"]; $cityString = implode(", ", $cities); echo $cityString; // Output: Stockholm, Göteborg, Malmö, Uppsala ?>
SQL (Syntax)
SELECT * FROM customers;
SELECT name, email FROM customers WHERE city = 'Göteborg';
SELECT name FROM customers ORDER BY name ASC LIMIT 10;
INSERT INTO customers (name, email, city) VALUES ('Lisa Berg', 'lisa@example.com', 'Malmö');
UPDATE customers SET city = 'Lund' WHERE id = 15;
DELETE FROM customers WHERE id = 20;
SELECT orders.order_id, customers.name FROM orders INNER JOIN customers ON orders.customer_id = customers.id;
Sessioner och Säkerhet (Koncept/PHP)
- Funktionen är
session_start()
. Den bör placeras allra högst upp i PHP-skriptet, före all annan output (HTML, mellanslag, echo etc.). -
<?php session_start(); $_SESSION['theme'] = 'dark'; ?>
-
<?php session_start(); if (isset($_SESSION['user_id'])) { echo "Användaren är inloggad."; } else { echo "Användaren är inte inloggad."; } ?>
- Funktionen är
htmlspecialchars()
. Exempel:<?php $userInput = "<script>alert('hack');</script>"; echo htmlspecialchars($userInput, ENT_QUOTES, 'UTF-8'); // Output blir ofarlig text: <script>alert('hack');</script> ?>
- Tekniken är Prepared Statements. I PDO initieras detta med
$pdo->prepare("SQL med :placeholders...")
. password_hash()
: Skapar en stark, saltad hash av ett lösenord.password_verify()
: Jämför ett inskickat lösenord mot en lagrad hash på ett säkert sätt.- Anti-CSRF Tokens (Synchronizer Token Pattern): Generera en unik token, lagra den i sessionen, inkludera den i formuläret, och verifiera att den inskickade token matchar sessionens token på serversidan innan åtgärden utförs.
Mini-CRUD (PHP/PDO)
-
<?php // Antag att formuläret skickats via POST $description = trim($_POST['task_description'] ?? ''); // Ytterligare validering kan behövas (t.ex. kolla om tom) ?>
-
<?php // Antag att $pdo är en giltig PDO-anslutning och $description har ett värde try { $sql = "INSERT INTO tasks (description) VALUES (:desc)"; $stmt = $pdo->prepare($sql); $stmt->bindParam(':desc', $description, PDO::PARAM_STR); $stmt->execute(); echo "Uppgift tillagd!"; } catch (PDOException $e) { error_log("Insert Task Error: " . $e->getMessage()); echo "Kunde inte lägga till uppgift."; } ?>
-
<?php // Antag att $pdo är en giltig PDO-anslutning $allTasks = []; try { $sql = "SELECT id, description FROM tasks ORDER BY id"; $stmt = $pdo->query($sql); // Enkel query går bra här då ingen input finns $allTasks = $stmt->fetchAll(); } catch (PDOException $e) { error_log("Fetch Tasks Error: " . $e->getMessage()); echo "Kunde inte hämta uppgifter."; } // Nu innehåller $allTasks en array med alla uppgifter (eller är tom om fel) // print_r($allTasks); ?>
Klasser (OOP Grunderna)
-
<?php declare(strict_types=1); class Book { public string $title; public string $author; public function displayInfo(): void { echo "Title: " . $this->title . ", Author: " . $this->author . "\n"; } } ?>
-
<?php // Antag att Book-klassen från övning 35 finns $book1 = new Book(); $book1->title = "Ronja Rövardotter"; $book1->author = "Astrid Lindgren"; $book2 = new Book(); $book2->title = "Sagan om Ringen"; $book2->author = "J.R.R. Tolkien"; $book1->displayInfo(); // Output: Title: Ronja Rövardotter, Author: Astrid Lindgren $book2->displayInfo(); // Output: Title: Sagan om Ringen, Author: J.R.R. Tolkien ?>
-
<?php declare(strict_types=1); class Book { public string $title; public string $author; // Konstruktor public function __construct(string $bookTitle, string $bookAuthor) { $this->title = $bookTitle; $this->author = $bookAuthor; } public function displayInfo(): void { echo "Title: " . $this->title . ", Author: " . $this->author . "\n"; } } // Skapa objekt med konstruktor $book3 = new Book("Liftarens Guide till Galaxen", "Douglas Adams"); $book3->displayInfo(); // Output: Title: Liftarens Guide till Galaxen, Author: Douglas Adams ?>
-
<?php declare(strict_types=1); class Book { private string $title; private string $author; public int $pages; // Lämnar denna publik som exempel public function __construct(string $bookTitle, string $bookAuthor, int $numPages) { $this->title = $bookTitle; $this->author = $bookAuthor; $this->pages = $numPages; // Sätter den publika egenskapen } // Getter för titel public function getTitle(): string { return $this->title; } // Getter för författare public function getAuthor(): string { return $this->author; } // Getter för sidantal (tekniskt sett inte nödvändig då pages är publik, men bra för konsekvens) public function getPages(): int { return $this->pages; } // Vi kan behålla displayInfo om vi vill, men den behöver använda getters nu public function displayInfo(): void { echo "Title: " . $this->getTitle() . ", Author: " . $this->getAuthor() . ", Pages: " . $this->getPages() . "\n"; } } ?>
-
<?php // Antag att den modifierade Book-klassen från övning 38 finns $book4 = new Book("Mio min Mio", "Astrid Lindgren", 180); // Hämta värden med getters $title = $book4->getTitle(); $author = $book4->getAuthor(); $pages = $book4->getPages(); // Kan också nås direkt: $book4->pages echo "Bok: " . $title . "\n"; echo "Författare: " . $author . "\n"; echo "Antal sidor: " . $pages . "\n"; // Anropa displayInfo som nu använder getters internt $book4->displayInfo(); ?>
Teknisk Intervju: Fullstack-utveckling med PHP
Detta avsnitt innehåller vanliga tekniska intervjufrågor för fullstack-utveckling med PHP och MySQL. Frågorna täcker grundläggande PHP-syntax, objektorienterad programmering, databasinteraktion, säkerhet och CRUD-operationer.
Fråga 1: PHP Grundläggande Syntax och Variabler
Intervjuare: "Kan du förklara skillnaden mellan PHP och JavaScript när det gäller var koden exekveras? Visa också hur man deklarerar och använder variabler i PHP."
Bra svar:
<?php
// PHP körs på SERVERN (server-side)
// JavaScript körs i WEBBLÄSAREN (client-side)
// PHP-variabler börjar alltid med $
$productName = "Laptop";
$productPrice = 999.50;
// Strängkonkatenering med punkt (.)
echo "Produkten " . $productName . " kostar " . $productPrice . " kr.";
// Alternativt med interpolering i dubbla citationstecken
echo "Produkten $productName kostar $productPrice kr.";
?>
Förklaring: PHP exekveras på servern innan HTML skickas till webbläsaren, medan JavaScript körs i användarens webbläsare. PHP-variabler börjar med $
och använder .
för strängkonkatenering.
Intervjutips: Betona skillnaden mellan server-side och client-side exekvering - detta är fundamentalt för fullstack-utveckling.
Fråga 2: PHP Arrays och Loopar
Intervjuare: "Vad är skillnaden mellan indexerade och associativa arrays i PHP? Visa hur man använder foreach för båda typerna."
Bra svar:
<?php
// Indexerad array (numeriska index)
$cities = ["Stockholm", "Göteborg", "Malmö"];
// Associativ array (namngivna nycklar)
$person = [
"firstName" => "Anna",
"lastName" => "Andersson",
"age" => 30
];
// Foreach för indexerad array (endast värden)
foreach ($cities as $city) {
echo $city . "<br>";
}
// Foreach för associativ array (nyckel och värde)
foreach ($person as $key => $value) {
echo $key . ": " . $value . "<br>";
}
// Kontrollera om värde finns
if (in_array("Stockholm", $cities)) {
echo "Stockholm finns i listan";
}
// Kontrollera om nyckel finns
if (isset($person["age"])) {
echo "Ålder: " . $person["age"];
}
?>
Förklaring: Indexerade arrays använder numeriska index (0, 1, 2...), medan associativa arrays använder namngivna strängnycklar. Foreach är den mest idiomatiska loopen för arrays i PHP.
Fråga 3: PHP Funktioner och Type Hinting
Intervjuare: "Skriv en funktion som beräknar rabatt på ett pris. Använd type hinting och förklara fördelarna med strict types."
Bra svar:
<?php
declare(strict_types=1); // Aktiverar strikt typkontroll
function calculateDiscount(float $price, int $percentage): float {
if ($percentage < 0 || $percentage > 100) {
throw new InvalidArgumentException('Procentsats måste vara mellan 0-100');
}
$discountFactor = 1 - ($percentage / 100.0);
return $price * $discountFactor;
}
// Användning
try {
$originalPrice = 1000.0;
$discountedPrice = calculateDiscount($originalPrice, 20);
echo "Pris efter 20% rabatt: " . $discountedPrice . " kr";
} catch (InvalidArgumentException $e) {
echo "Fel: " . $e->getMessage();
}
?>
Förklaring: Type hinting specificerar förväntade datatyper för parametrar och returvärden. declare(strict_types=1)
förhindrar automatisk typkonvertering och fångar fel tidigare.
Fråga 4: Objektorienterad Programmering i PHP
Intervjuare: "Skapa en enkel PHP-klass för en produkt med konstruktor, privata properties och getter/setter-metoder. Förklara fördelarna med inkapsling."
Bra svar:
<?php
declare(strict_types=1);
class Product {
private string $name;
private float $price;
private int $stock;
public function __construct(string $name, float $price, int $stock = 0) {
$this->name = $name;
$this->setPrice($price); // Använd setter för validering
$this->setStock($stock);
}
// Getters
public function getName(): string {
return $this->name;
}
public function getPrice(): float {
return $this->price;
}
public function getStock(): int {
return $this->stock;
}
// Setters med validering
public function setPrice(float $price): void {
if ($price < 0) {
throw new InvalidArgumentException('Priset kan inte vara negativt');
}
$this->price = $price;
}
public function setStock(int $stock): void {
if ($stock < 0) {
throw new InvalidArgumentException('Lagersaldo kan inte vara negativt');
}
$this->stock = $stock;
}
public function isInStock(): bool {
return $this->stock > 0;
}
}
// Användning
$product = new Product("Laptop", 15999.0, 5);
echo $product->getName() . " kostar " . $product->getPrice() . " kr";
?>
Förklaring: Inkapsling (private properties + public methods) ger kontroll över hur data nås och modifieras, möjliggör validering och förhindrar direktmanipulation av objektets tillstånd.
Fråga 5: SQL och Databasinteraktion
Intervjuare: "Skriv SQL-frågor för att skapa en users-tabell och visa hur man säkert hämtar användardata med PHP PDO."
Bra svar:
-- Skapa users-tabell
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Hämta användare från specifik stad
SELECT id, username, email FROM users
WHERE city = 'Stockholm'
ORDER BY username ASC;
<?php
// PHP PDO för säker databasinteraktion
function getUserByUsername(PDO $pdo, string $username): array|false {
// Prepared statement förhindrar SQL injection
$stmt = $pdo->prepare("SELECT id, username, email, created_at
FROM users
WHERE username = :username");
$stmt->bindParam(':username', $username, PDO::PARAM_STR);
$stmt->execute();
return $stmt->fetch(PDO::FETCH_ASSOC);
}
// Databasanslutning
try {
$pdo = new PDO(
'mysql:host=localhost;dbname=myapp;charset=utf8mb4',
$username,
$password,
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
$user = getUserByUsername($pdo, 'kalleanka');
if ($user) {
echo "Användare: " . htmlspecialchars($user['username']);
}
} catch (PDOException $e) {
error_log("Database error: " . $e->getMessage());
echo "Databasfel uppstod";
}
?>
Förklaring: Prepared statements är avgörande för att förhindra SQL injection. PDO ger ett konsekvent interface för olika databaser.
Fråga 6: PHP Sessioner och Säkerhet
Intervjuare: "Förklara hur PHP-sessioner fungerar och visa hur man implementerar en säker inloggningsprocess."
Bra svar:
<?php
// Starta session (alltid högst upp)
session_start();
function loginUser(PDO $pdo, string $username, string $password): bool {
// Hämta användare från databas
$stmt = $pdo->prepare("SELECT id, username, password_hash
FROM users
WHERE username = :username");
$stmt->bindParam(':username', $username);
$stmt->execute();
$user = $stmt->fetch();
// Verifiera lösenord
if ($user && password_verify($password, $user['password_hash'])) {
// Regenerera session-ID för säkerhet
session_regenerate_id(true);
// Spara användarinfo i session
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
return true;
}
return false;
}
function isLoggedIn(): bool {
return isset($_SESSION['user_id']);
}
function requireLogin(): void {
if (!isLoggedIn()) {
header('Location: login.php');
exit;
}
}
function logout(): void {
$_SESSION = [];
// Ta bort session cookie
if (ini_get("session.use_cookies")) {
$params = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000,
$params["path"], $params["domain"],
$params["secure"], $params["httponly"]
);
}
session_destroy();
}
?>
Förklaring: Sessioner lagrar data på servern och använder cookies för att identifiera användare. password_verify()
och session_regenerate_id()
är viktiga säkerhetsåtgärder.
Fråga 7: CRUD Operations med PHP
Intervjuare: "Implementera grundläggande CRUD-operationer för en bloggpost med PHP och MySQL."
Bra svar:
<?php
class PostRepository {
private PDO $pdo;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
// CREATE
public function createPost(string $title, string $content, int $userId): int {
$stmt = $this->pdo->prepare(
"INSERT INTO posts (title, content, user_id, created_at)
VALUES (:title, :content, :user_id, NOW())"
);
$stmt->bindParam(':title', $title);
$stmt->bindParam(':content', $content);
$stmt->bindParam(':user_id', $userId, PDO::PARAM_INT);
$stmt->execute();
return (int)$this->pdo->lastInsertId();
}
// READ
public function getPostById(int $id): array|false {
$stmt = $this->pdo->prepare(
"SELECT p.*, u.username
FROM posts p
JOIN users u ON p.user_id = u.id
WHERE p.id = :id"
);
$stmt->bindParam(':id', $id, PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetch(PDO::FETCH_ASSOC);
}
// UPDATE
public function updatePost(int $id, string $title, string $content, int $userId): bool {
$stmt = $this->pdo->prepare(
"UPDATE posts
SET title = :title, content = :content, updated_at = NOW()
WHERE id = :id AND user_id = :user_id"
);
$stmt->bindParam(':title', $title);
$stmt->bindParam(':content', $content);
$stmt->bindParam(':id', $id, PDO::PARAM_INT);
$stmt->bindParam(':user_id', $userId, PDO::PARAM_INT);
return $stmt->execute() && $stmt->rowCount() > 0;
}
// DELETE
public function deletePost(int $id, int $userId): bool {
$stmt = $this->pdo->prepare(
"DELETE FROM posts
WHERE id = :id AND user_id = :user_id"
);
$stmt->bindParam(':id', $id, PDO::PARAM_INT);
$stmt->bindParam(':user_id', $userId, PDO::PARAM_INT);
return $stmt->execute() && $stmt->rowCount() > 0;
}
}
?>
Förklaring: CRUD-operationer (Create, Read, Update, Delete) är grundläggande för datahantering. Inkludera användar-ID i UPDATE/DELETE för säkerhet.
Fråga 8: Formulärhantering och Validering
Intervjuare: "Visa hur man säkert hanterar formulärdata i PHP med validering och felhantering."
Bra svar:
<?php
function handleContactForm(): array {
$errors = [];
$data = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Hämta och sanera input
$data['name'] = trim($_POST['name'] ?? '');
$data['email'] = trim($_POST['email'] ?? '');
$data['message'] = trim($_POST['message'] ?? '');
// Validering
if (empty($data['name'])) {
$errors[] = 'Namn är obligatoriskt';
} elseif (strlen($data['name']) < 2) {
$errors[] = 'Namnet måste vara minst 2 tecken';
}
if (empty($data['email'])) {
$errors[] = 'E-post är obligatoriskt';
} elseif (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
$errors[] = 'Ogiltig e-postadress';
}
if (empty($data['message'])) {
$errors[] = 'Meddelande är obligatoriskt';
} elseif (strlen($data['message']) < 10) {
$errors[] = 'Meddelandet måste vara minst 10 tecken';
}
// Om inga fel, behandla formuläret
if (empty($errors)) {
// Spara till databas, skicka e-post etc.
// Omdirigera för att förhindra dubbel inlämning
header('Location: success.php');
exit;
}
}
return ['errors' => $errors, 'data' => $data];
}
// I HTML-templaten
$result = handleContactForm();
?>
<form method="post">
<?php if (!empty($result['errors'])): ?>
<div class="errors">
<?php foreach ($result['errors'] as $error): ?>
<p><?= htmlspecialchars($error) ?></p>
<?php endforeach; ?>
</div>
<?php endif; ?>
<input type="text" name="name"
value="<?= htmlspecialchars($result['data']['name'] ?? '') ?>">
<input type="email" name="email"
value="<?= htmlspecialchars($result['data']['email'] ?? '') ?>">
<textarea name="message"><?= htmlspecialchars($result['data']['message'] ?? '') ?></textarea>
<button type="submit">Skicka</button>
</form>
Förklaring: Validera alltid användarinput på serversidan. Använd htmlspecialchars()
för att förhindra XSS när data skrivs ut.
Fråga 9: Filuppladdning och Säkerhet
Intervjuare: "Implementera säker filuppladdning för bilder i PHP. Vilka säkerhetsrisker finns?"
Bra svar:
<?php
function handleImageUpload(array $file): array {
$errors = [];
$uploadPath = null;
// Kontrollera att fil laddades upp utan fel
if ($file['error'] !== UPLOAD_ERR_OK) {
$errors[] = 'Filuppladdning misslyckades';
return ['errors' => $errors, 'path' => null];
}
// Validera filstorlek (max 5MB)
$maxSize = 5 * 1024 * 1024;
if ($file['size'] > $maxSize) {
$errors[] = 'Filen är för stor (max 5MB)';
}
// Validera filtyp (kontrollera MIME type OCH filändelse)
$allowedMimes = ['image/jpeg', 'image/png', 'image/gif'];
$allowedExts = ['jpg', 'jpeg', 'png', 'gif'];
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
$extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($mimeType, $allowedMimes) || !in_array($extension, $allowedExts)) {
$errors[] = 'Endast JPG, PNG och GIF-filer tillåtna';
}
if (empty($errors)) {
// Skapa säkert filnamn
$safeName = uniqid('img_', true) . '.' . $extension;
$uploadDir = __DIR__ . '/uploads/';
$targetPath = $uploadDir . $safeName;
// Skapa directory om det inte finns
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
// Flytta fil till slutgiltig plats
if (move_uploaded_file($file['tmp_name'], $targetPath)) {
$uploadPath = 'uploads/' . $safeName;
} else {
$errors[] = 'Kunde inte spara filen';
}
}
return ['errors' => $errors, 'path' => $uploadPath];
}
// Användning
if (isset($_FILES['image'])) {
$result = handleImageUpload($_FILES['image']);
if (empty($result['errors'])) {
echo "Fil uppladdad: " . htmlspecialchars($result['path']);
}
}
?>
Säkerhetsrisker:
- Executable uploads: Aldrig tillåt .php, .exe filer
- Directory traversal: Använd säkra filnamn
- MIME type spoofing: Kontrollera både MIME och filändelse
- Filstorlek: Begränsa för att förhindra DoS
Fråga 10: XSS och CSRF-skydd
Intervjuare: "Förklara XSS och CSRF-attacker. Hur skyddar man sig i PHP?"
Bra svar:
XSS (Cross-Site Scripting) Skydd:
<?php
// ALLTID sanera output med htmlspecialchars()
function safeOutput(string $data): string {
return htmlspecialchars($data, ENT_QUOTES, 'UTF-8');
}
// Farlig kod (sårbar för XSS)
echo $_GET['message']; // ❌ Farligt!
// Säker kod
echo safeOutput($_GET['message']); // ✅ Säkert
// För HTML-innehåll, använd whitelist-baserad rensning
// t.ex. HTMLPurifier biblioteket
?>
CSRF (Cross-Site Request Forgery) Skydd:
<?php
session_start();
function generateCSRFToken(): string {
if (!isset($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
function validateCSRFToken(string $token): bool {
return isset($_SESSION['csrf_token']) &&
hash_equals($_SESSION['csrf_token'], $token);
}
// I formulär
$csrfToken = generateCSRFToken();
?>
<form method="post">
<input type="hidden" name="csrf_token" value="<?= $csrfToken ?>">
<!-- Övriga formulärfält -->
<button type="submit">Skicka</button>
</form>
<?php
// Vid formulärhantering
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!validateCSRFToken($_POST['csrf_token'] ?? '')) {
die('CSRF-token validation failed');
}
// Fortsätt med formulärbehandling...
}
?>
Förklaring: XSS förhindras genom att sanera all output. CSRF förhindras med unika tokens som valideras vid formulärinlämning.
Fråga 11: Error Handling och Debugging
Intervjuare: "Visa hur man implementerar robust felhantering i en PHP-applikation."
Bra svar:
<?php
// Aktivera strict types och error reporting för utveckling
declare(strict_types=1);
ini_set('display_errors', 1);
error_reporting(E_ALL);
// Custom exception klasser
class ValidationException extends Exception {}
class DatabaseException extends Exception {}
class UserService {
private PDO $pdo;
private LoggerInterface $logger;
public function createUser(array $userData): int {
try {
// Validering
$this->validateUserData($userData);
// Databasoperation
$stmt = $this->pdo->prepare(
"INSERT INTO users (username, email, password_hash)
VALUES (:username, :email, :password)"
);
$passwordHash = password_hash($userData['password'], PASSWORD_DEFAULT);
$stmt->execute([
':username' => $userData['username'],
':email' => $userData['email'],
':password' => $passwordHash
]);
$userId = (int)$this->pdo->lastInsertId();
$this->logger->info("User created", ['user_id' => $userId]);
return $userId;
} catch (ValidationException $e) {
// Logga validering fel (för utveckling)
$this->logger->warning("Validation failed", [
'error' => $e->getMessage(),
'data' => $userData
]);
throw $e; // Re-throw för hantering i controller
} catch (PDOException $e) {
// Logga databasfel (känslig info)
$this->logger->error("Database error creating user", [
'error' => $e->getMessage(),
'code' => $e->getCode()
]);
// Kasta generellt fel till användaren
throw new DatabaseException('Could not create user');
} catch (Exception $e) {
// Fånga oväntade fel
$this->logger->critical("Unexpected error", [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
throw new Exception('An unexpected error occurred');
}
}
private function validateUserData(array $data): void {
if (empty($data['username'])) {
throw new ValidationException('Username is required');
}
if (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
throw new ValidationException('Invalid email format');
}
if (strlen($data['password']) < 8) {
throw new ValidationException('Password must be at least 8 characters');
}
}
}
// Global exception handler för produktion
set_exception_handler(function(Throwable $e) {
error_log("Uncaught exception: " . $e->getMessage());
// Visa användarvänligt meddelande
http_response_code(500);
echo "Something went wrong. Please try again later.";
});
?>
Förklaring: Använd specifika exceptions, logga tekniska fel separat från användarmeddelanden, och ha en global exception handler som backup.
Fråga 12: Performance och Optimering
Intervjuare: "Vilka tekniker kan du använda för att optimera prestanda i en PHP-applikation?"
Bra svar:
1. Databas-optimering:
<?php
// Använd index på ofta söka kolumner
// CREATE INDEX idx_username ON users(username);
// CREATE INDEX idx_email ON users(email);
// Begränsa SELECT-kolumner
$stmt = $pdo->prepare("SELECT id, username FROM users WHERE active = 1");
// Istället för: SELECT * FROM users WHERE active = 1
// Använd LIMIT för paginering
$stmt = $pdo->prepare("SELECT * FROM posts ORDER BY created_at DESC LIMIT :offset, :limit");
$stmt->bindValue(':offset', ($page - 1) * $perPage, PDO::PARAM_INT);
$stmt->bindValue(':limit', $perPage, PDO::PARAM_INT);
?>
2. Caching:
<?php
// APCu cache för smådata
function getCachedUserCount(): int {
$key = 'user_count';
$count = apcu_fetch($key);
if ($count === false) {
// Hämta från databas
$stmt = $pdo->query("SELECT COUNT(*) FROM users");
$count = (int)$stmt->fetchColumn();
// Cache i 5 minuter
apcu_store($key, $count, 300);
}
return $count;
}
// Filbaserad cache för komplexare data
function getCachedPosts(): array {
$cacheFile = '/tmp/posts_cache.json';
$cacheTime = 600; // 10 minuter
if (file_exists($cacheFile) && (time() - filemtime($cacheFile)) < $cacheTime) {
return json_decode(file_get_contents($cacheFile), true);
}
// Hämta från databas
$posts = fetchPostsFromDatabase();
file_put_contents($cacheFile, json_encode($posts));
return $posts;
}
?>
3. OPcache och autoloading:
<?php
// php.ini inställningar för OPcache
/*
opcache.enable=1
opcache.memory_consumption=128
opcache.max_accelerated_files=4000
opcache.revalidate_freq=60
*/
// Composer autoloader för effektiv class loading
require_once 'vendor/autoload.php';
// Lazy loading av tunga objekt
class Application {
private ?DatabaseConnection $db = null;
public function getDatabase(): DatabaseConnection {
if ($this->db === null) {
$this->db = new DatabaseConnection();
}
return $this->db;
}
}
?>
4. Memory och I/O optimering:
<?php
// Undvik minneskrävande operationer
// Istället för att ladda alla rader:
// $allUsers = $stmt->fetchAll(); // ❌ Kan använda mycket minne
// Använd generators för stora dataset:
function getUsersGenerator(PDO $pdo): Generator {
$stmt = $pdo->query("SELECT * FROM users");
while ($row = $stmt->fetch()) {
yield $row;
}
}
// Buffra output för bättre prestanda
ob_start();
foreach (getUsersGenerator($pdo) as $user) {
echo "<div>" . htmlspecialchars($user['username']) . "</div>";
}
ob_end_flush();
?>
Prestanda-tips:
- Använd OPcache för att cacha kompilerad PHP-kod
- Implementera databas-indexering på ofta använda kolumner
- Cacha dyra operationer (API-anrop, komplexa beräkningar)
- Använd generators för stora dataset
- Minimera databas-queries med eager loading
- Komprimera CSS/JS och använd CDN för statiska tillgångar
Intervjutips: Diskutera olika nivåer av optimering - från kod-nivå till infrastruktur (load balancers, database replicas etc.).
Sammanfattning
Dessa frågor täcker de viktigaste aspekterna av fullstack-utveckling med PHP:
- Grundläggande PHP: Syntax, variabler, funktioner
- Datastrukturer: Arrays, objekt, klasser
- Databaser: SQL, PDO, prepared statements
- Säkerhet: XSS, CSRF, lösenordshantering, filuppladdning
- Arkitektur: CRUD, error handling, prestanda
Förberedelse-tips:
- Öva på att skriva PHP-kod utan IDE-hjälp
- Memorera vanliga säkerhetsprinciper och best practices
- Förstå skillnaden mellan server-side och client-side kod
- Repetera SQL-grunderna och JOIN-operationer
- Var beredd att diskutera prestanda-optimeringar
Kapitel 8: Frontend-ramverk med React
I de tidigare kapitlen har vi byggt grunderna för webbutveckling med HTML, CSS, JavaScript och PHP. Vi har lärt oss skapa dynamiska webbapplikationer med server-side rendering och databaser. Nu är det dags att ta steget in i modern frontend-utveckling med React.
Detta kapitel introducerar dig till komponentsbaserad utveckling och Single Page Applications (SPA) - en arkitektur som dominerar moderna webbapplikationer. Istället för att generera HTML på servern och skicka hela sidor, bygger vi interaktiva användargränssnitt som körs i webbläsaren och kommunicerar med servern via API:er.
Vad är React? React är ett JavaScript-bibliotek utvecklat av Facebook (Meta) för att bygga användargränssnitt. Det fokuserar på att skapa återanvändbara komponenter och hanterar effektivt uppdateringar av DOM:en genom sitt Virtual DOM-system.
Varför React?
- Komponentbaserad arkitektur: Bygg applikationer som LEGO-block där varje komponent har sitt eget ansvar
- Återanvändbarhet: Skriv en gång, använd överallt
- Prestanda: Virtual DOM optimerar uppdateringar automatiskt
- Stort ekosystem: Omfattande community och bibliotek
- Industristandard: Används av företag som Facebook, Netflix, Airbnb, Uber
Single Page Applications (SPA) skiljer sig från traditionella webbapplikationer genom att:
- Ladda appen en gång, sedan uppdatera innehållet dynamiskt
- Navigering sker utan att hela sidan laddas om
- Snabbare användarupplevelse efter initial laddning
- Tydlig separation mellan frontend och backend
Vad är en Komponentbaserad arkitektur?
En komponent är en JavaScript-funktion eller klass som returnerar JSX och representerar en del av användargränssnittet. Tänk på det som en anpassad HTML-tagg som du själv definierar.
graph TD A[App] --> B[Header] A --> C[Main] A --> D[Footer] B --> E[Logo] B --> F[Navigation] C --> G[ProductList] C --> H[Sidebar] G --> I[ProductCard] G --> J[ProductCard] G --> K[ProductCard] H --> L[SearchFilter] H --> M[CategoryFilter] style A fill:#4CAF50 style G fill:#FF5722 style I fill:#FF5722 style J fill:#FF5722 style K fill:#FF5722
Diagram: Komponenthierarki för en e-handelsapplikation
I detta kapitel kommer vi att:
- Din första komponent: Lära dig skapa och organisera React-komponenter
- Skriva markup med JSX: Kombinera JavaScript och HTML-liknande syntax
- Props och interaktivitet: Skicka data mellan komponenter och hantera events
- State: Ge komponenter minne för att reagera på förändringar
- Formulär: Hantera användarinput på ett kontrollerat sätt
- API-integration: Hämta och skicka data till backend-tjänster
- Routing: Bygga Single Page Applications med navigation
- Deployment: Publicera din app på internet
Förutsättningar: Du bör ha god kunskap i JavaScript (ES6+), HTML, CSS och grundläggande förståelse för API:er från tidigare kapitel.
Program som ska vara installerade
Innan vi börjar behöver du ha följande verktyg installerade:
NVM (Node Version Manager)
NVM används för att hantera olika versioner av Node.js. Det låter dig enkelt byta mellan olika Node.js-versioner.
Installation:
- Windows: Ladda ner och installera nvm-windows
- macOS/Linux: Kör följande kommando i terminalen:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
Efter installation, kör följande kommandon för att installera och använda senaste LTS-versionen av Node.js:
nvm install --lts # installera senaste long-term-support versionen
nvm use --lts # använd den
Verifiera installationen genom att köra:
node --version # v22.18.0 eller högre
Låt oss börja vår resa in i modern frontend-utveckling!
Din Första React-komponent
React är ett JavaScript-bibliotek för att bygga användargränssnitt. UI byggs från små enheter som knappar, text och bilder. React låter dig kombinera dem till återanvändbara, nästlade komponenter. Från webbplatser till mobilappar - allt på skärmen kan delas upp i komponenter.
Mål: Skapa din första React-komponent, förstå JSX-syntax och sätta upp en utvecklingsmiljö.
Vad är en Komponent?
En komponent är en JavaScript-funktion som returnerar markup (JSX). Komponenter kan vara så små som en knapp, eller så stora som en hel sida.
// Din första komponent - en enkel funktion som returnerar JSX
function Välkomstmeddelande() {
return <h1>Hej från React!</h1>;
}
// Använd komponenten som en HTML-tagg
function App() {
return (
<div>
<Välkomstmeddelande />
<Välkomstmeddelande />
<Välkomstmeddelande />
</div>
);
}
Prova detta! Skapa en ny React-app och ersätt innehållet i App.js
med koden ovan.
Komponenter Överallt
React-applikationer byggs från isolerade UI-delar som kallas komponenter. Här är en Galleri
-komponent som renderar tre Profil
-komponenter:
function Profil() {
return (
<img
src="https://i.imgur.com/MK3eW3As.jpg"
alt="Katherine Johnson"
/>
);
}
function Galleri() {
return (
<section>
<h1>Fantastiska forskare</h1>
<Profil />
<Profil />
<Profil />
</section>
);
}
Importera och Exportera Komponenter
Du kan deklarera många komponenter i en fil, men stora filer kan bli svåra att navigera. För att lösa detta kan du exportera en komponent till sin egen fil och sedan importera den komponenten från en annan fil:
// Profil.js
function Profil() {
return (
<img
src="https://i.imgur.com/MK3eW3As.jpg"
alt="Katherine Johnson"
/>
);
}
export default Profil;
// Galleri.js
import Profil from './Profil.js';
function Galleri() {
return (
<section>
<h1>Fantastiska forskare</h1>
<Profil />
<Profil />
<Profil />
</section>
);
}
export default Galleri;
Ditt UI som ett Träd
React använder träd för att modellera relationerna mellan komponenter och moduler.
En React render-träd är en representation av förälder- och barnrelationen mellan komponenter.
graph TB A["Root Component"] --> B["Component A"] A --> C["Component C"] B --> D["Component B"] C --> E["Component D"] style A fill:#e8f5e8,stroke:#388e3c,stroke-width:2px,color:#000 style B fill:#e3f2fd,stroke:#1976d2,stroke-width:2px,color:#000 style C fill:#e3f2fd,stroke:#1976d2,stroke-width:2px,color:#000 style D fill:#fff3e0,stroke:#f57c00,stroke-width:2px,color:#000 style E fill:#fff3e0,stroke:#f57c00,stroke-width:2px,color:#000
Komponenter nära toppen av trädet, nära root-komponenten, betraktas som toppnivå-komponenter. Komponenter utan barnkomponenter är löv-komponenter. Denna kategorisering av komponenter är användbar för att förstå dataflöde och renderingsprestanda.
Virtual DOM: Prestanda Under Huven (Fördjupning)
Nu när du förstår grunderna, låt oss titta på Virtual DOM - ett av Reacts mest innovativa koncept.
Virtual DOM-processen:
- Skapande: React skapar en virtuell representation av DOM:en i JavaScript
- Jämförelse (Diffing): När något ändras jämför React den nya Virtual DOM med den föregående
- Minimal uppdatering: Bara de delar som faktiskt ändrats uppdateras i den riktiga DOM:en
Virtual DOM:s verkliga fördelar:
- Batching: Flera state-ändringar → en DOM-uppdatering
- Smart diffing: Hoppar över onödiga uppdateringar
- Förutsägbarhet: Deklarativ kod istället för imperativ DOM-manipulation
Virtual DOM vs Real DOM: Träd-struktur
För att förstå Virtual DOM bättre, låt oss visualisera hur React hanterar ändringar i en HTML-struktur:
graph TB subgraph realdom ["Real DOM (Långsam)"] direction TB RD1["div#app"] --> RD2["header"] RD1 --> RD3["main"] RD1 --> RD4["footer"] RD2 --> RD5["h1: 'Välkommen'"] RD3 --> RD6["div.content"] RD3 --> RD7["button: 'Klicka'"] RD6 --> RD8["p: 'Räknare: 0'"] RD8_NEW["p: 'Räknare: 1' ⚡"] RD8 -.->|"Flera separata<br/>DOM-operationer"| RD8_NEW end subgraph vdom ["Virtual DOM (Snabb)"] direction TB VD1["div#app"] --> VD2["header"] VD1 --> VD3["main"] VD1 --> VD4["footer"] VD2 --> VD5["h1: 'Välkommen'"] VD3 --> VD6["div.content"] VD3 --> VD7["button: 'Klicka'"] VD6 --> VD8["p: 'Räknare: 0'"] VD8_NEW["p: 'Räknare: 1' ⚡"] VD8 -.->|"Bara denna nod<br/>uppdateras"| VD8_NEW end style realdom fill:#ffebee,stroke:#d32f2f,stroke-width:2px,color:#000 style vdom fill:#e8f5e8,stroke:#388e3c,stroke-width:2px,color:#000 style RD8 fill:#ffcdd2,stroke:#d32f2f,color:#000 style RD8_NEW fill:#ef5350,stroke:#d32f2f,color:#fff style VD8 fill:#c8e6c9,stroke:#388e3c,color:#000 style VD8_NEW fill:#66bb6a,stroke:#388e3c,color:#fff
Skillnaden förklarad:
Vänta - uppdateras verkligen "hela trädet" i traditionell JS?
Nej, det är en förenkling! När du gör:
document.querySelector("#counter").innerText = "Räknare: 1";
...så uppdateras bara den specifika noden. Men - här är varför Virtual DOM ändå ger fördelar:
Traditionell DOM-manipulation (flera uppdateringar):
// Varje rad triggar separat DOM-operation och potentiell repaint
document.querySelector("#name").innerText = "Anna"; // Operation 1
document.querySelector("#age").innerText = "25"; // Operation 2
document.querySelector("#status").innerText = "Online"; // Operation 3
// Vid komplex logik - många manuella DOM-operationer
if (user.isLoggedIn) {
document.querySelector("#login-btn").style.display = "none";
document.querySelector("#user-menu").style.display = "block";
document.querySelector("#username").innerText = user.name;
// ... potentiellt 10+ fler DOM-operationer
}
Virtual DOM (React approach):
// React batchar alla dessa till EN DOM-uppdatering
function UserProfile({ user }) {
return (
<div>
<span id="name">{user.name}</span>
<span id="age">{user.age}</span>
<span id="status">{user.isOnline ? "Online" : "Offline"}</span>
{user.isLoggedIn ? (
<UserMenu user={user} />
) : (
<LoginButton />
)}
</div>
);
}
// → React optimerar till minimal antal DOM-operationer
Virtual DOM:s verkliga fördelar:
- Batching: Flera state-ändringar → en DOM-uppdatering
- Smart diffing: Hoppar över onödiga uppdateringar (om värdet inte ändrats)
- Förutsägbarhet: Deklarativ kod istället för imperativ DOM-manipulation
- Komplexitet: Hanterar komplexa UI-förändringar elegant
Praktiskt exempel
Tänk dig denna React-komponent:
function Counter() {
const [count, setCount] = useState(0);
return (
<div id="app">
<header>
<h1>Välkommen</h1>
</header>
<main>
<div className="content">
<p>Räknare: {count}</p> {/* Bara denna rad ändras */}
</div>
<button onClick={() => setCount(count + 1)}>
Klicka
</button>
</main>
<footer>Footer innehåll</footer>
</div>
);
}
När count
ändras:
- Virtual DOM skapas med det nya värdet
- Diffing algoritm jämför gamla och nya Virtual DOM
- Minimal uppdatering - bara
<p>
-elementet uppdateras i Real DOM - Resultat - snabb rendering utan onödig omritning
UI som funktion av state
Grunden i React är att se UI som en funktion av state: UI = f(state). Ett minimalt exempel:
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
</div>
);
}
Skriva Markup med JSX
Varje React-komponent är en JavaScript-funktion som kan innehålla markup som React renderar i webbläsaren. React-komponenter använder en syntax-utökning som kallas JSX för att representera den markup:en.
JSX ser ut som HTML, men är lite striktare och kan visa dynamisk information. Om vi klistrar in befintlig HTML-markup i en React-komponent fungerar det inte alltid:
// Detta fungerar inte riktigt!
function TodoLista() {
return (
<h1>Hedy Lamarrs Todos</h1>
<img
src="https://i.imgur.com/yXOvdOSs.jpg"
alt="Hedy Lamarr"
class="photo"
>
<ul>
<li>Uppfinn nya trafikljus
<li>Repetera en filmscen
<li>Förbättra spektrumteknologi
</ul>
);
}
Om du har befintlig HTML som detta kan du fixa det med en konverterare, eller följa JSX-reglerna:
function TodoLista() {
return (
<>
<h1>Hedy Lamarrs Todos</h1>
<img
src="https://i.imgur.com/yXOvdOSs.jpg"
alt="Hedy Lamarr"
className="photo"
/>
<ul>
<li>Uppfinn nya trafikljus</li>
<li>Repetera en filmscen</li>
<li>Förbättra spektrumteknologi</li>
</ul>
</>
);
}
JavaScript i JSX med Klammerparenteser
JSX låter dig skriva HTML-liknande markup inuti en JavaScript-fil, vilket håller renderingslogik och innehåll på samma plats. Ibland vill du lägga till lite JavaScript-logik eller referera till en dynamisk egenskap inuti den markup:en. I denna situation kan du använda klammerparenteser i din JSX för att "öppna ett fönster" till JavaScript:
const person = {
name: 'Gregorio Y. Zara',
theme: {
backgroundColor: 'black',
color: 'pink'
}
};
function TodoLista() {
return (
<div style={person.theme}>
<h1>{person.name}s Todos</h1>
<img
className="avatar"
src="https://i.imgur.com/7vQD0fPs.jpg"
alt="Gregorio Y. Zara"
/>
<ul>
<li>Förbättra videotelefonen</li>
<li>Förbered flygföreläsningar</li>
<li>Arbeta på alkoholdrivna motorn</li>
</ul>
</div>
);
}
Skicka Props till en Komponent
React-komponenter använder props för att kommunicera med varandra. Varje föräldrakomponent kan skicka information till sina barnkomponenter genom att ge dem props. Props kan påminna dig om HTML-attribut, men du kan skicka vilket JavaScript-värde som helst genom dem, inklusive objekt, arrayer, funktioner och till och med JSX!
function Avatar({ person, size }) {
return (
<img
className="avatar"
src={getImageUrl(person)}
alt={person.name}
width={size}
height={size}
/>
);
}
function Profil() {
return (
<div>
<Avatar
size={100}
person={{
name: 'Katsuko Saruhashi',
imageId: 'YfeOqp2'
}}
/>
</div>
);
}
Villkorlig Rendering
Dina komponenter behöver ofta visa olika saker beroende på olika villkor. I React kan du villkorligt rendera JSX med JavaScript-syntax som if
-satser, &&
och ? :
operatorer.
I detta exempel används JavaScript &&
operatorn för att villkorligt rendera en bockmarkering:
function Item({ name, isPacked }) {
return (
<li className="item">
{name} {isPacked && '✅'}
</li>
);
}
function PackingList() {
return (
<section>
<h1>Sally Rides Packlista</h1>
<ul>
<Item
isPacked={true}
name="Rymddräkt"
/>
<Item
isPacked={true}
name="Hjälm med gyllene blad"
/>
<Item
isPacked={false}
name="Foto av Tam"
/>
</ul>
</section>
);
}
Rendera Listor
Du vill ofta visa flera liknande komponenter från en samling data. Du kan använda JavaScripts map()
med React för att transformera din dataarray till en array av komponenter.
För varje arrayobjekt behöver du specificera en key
. Vanligtvis vill du använda ett ID från databasen som key
. Keys låter React hålla reda på varje objekts plats i listan även om listan ändras.
const people = [
{ id: 0, name: 'Creola Katherine Johnson', profession: 'matematiker' },
{ id: 1, name: 'Mario José Molina-Pasquel', profession: 'kemist' },
{ id: 2, name: 'Mohammad Abdus Salam', profession: 'fysiker' },
];
function ScientistList() {
const listItems = people.map(person =>
<li key={person.id}>
<p>
<b>{person.name}</b> är en {person.profession}.
</p>
</li>
);
return (
<article>
<h1>Forskare</h1>
<ul>{listItems}</ul>
</article>
);
}
Viktigt om keys:
- Använd alltid en unik
key
för varje listelement - Keys hjälper React att förstå vilka element som ändrats
- Använd aldrig array-index som key om listan kan ändras
Hålla Komponenter Rena
Vissa JavaScript-funktioner är rena. En ren funktion:
- Sköter sina egna affärer. Den ändrar inte några objekt eller variabler som existerade innan den anropades.
- Samma input, samma output. Givet samma input ska en ren funktion alltid returnera samma resultat.
Genom att strikt bara skriva dina komponenter som rena funktioner kan du undvika en hel klass av förvirrande buggar och oförutsägbart beteende när din kodbas växer. Här är ett exempel på en oren komponent:
let guest = 0;
function Cup() {
// Dåligt: ändrar en redan existerande variabel!
guest = guest + 1;
return <h2>Tekopp för gäst #{guest}</h2>;
}
function TeaSet() {
return (
<>
<Cup />
<Cup />
<Cup />
</>
);
}
Du kan göra denna komponent ren genom att skicka en prop istället för att modifiera en redan existerande variabel:
function Cup({ guest }) {
return <h2>Tekopp för gäst #{guest}</h2>;
}
function TeaSet() {
return (
<>
<Cup guest={1} />
<Cup guest={2} />
<Cup guest={3} />
</>
);
}
Utvecklingsmiljö: Kom Igång Snabbt
Snabbstart med Vite (Rekommenderat)
# Skapa nytt projekt med Vite
npm create vite@latest min-react-app -- --template react
cd min-react-app
# Installera dependencies
npm install
# Starta utvecklingsserver
npm run dev
Din Första React-app
När du har skapat projektet, öppna src/App.jsx
och ersätt innehållet med:
function App() {
return (
<div>
<h1>Min första React-app!</h1>
<p>Välkommen till React-världen!</p>
</div>
);
}
export default App;
Prova att ändra texten och se hur sidan uppdateras direkt!
JSX-regler att komma ihåg
// 1. Måste ha ett parent element (eller React Fragment)
// ❌ Fel - flera root elements
function BadComponent() {
return (
<h1>Titel</h1>
<p>Text</p>
);
}
// ✅ Rätt - ett parent element
function GoodComponent() {
return (
<div>
<h1>Titel</h1>
<p>Text</p>
</div>
);
}
// ✅ Eller använd React Fragment
function GoodComponentFragment() {
return (
<>
<h1>Titel</h1>
<p>Text</p>
</>
);
}
// 2. JavaScript-uttryck inom {}
function DynamicComponent() {
const products = ['Äpple', 'Banan', 'Citron'];
const price = 25;
return (
<div>
<h2>Produkter ({products.length})</h2>
<p>Pris: {price} kr</p>
<ul>
{products.map(product => (
<li key={product}>{product}</li>
))}
</ul>
</div>
);
}
Utvecklingsmiljö: Kom Igång Snabbt
Snabbstart utan tooling
Vill du prova React direkt? Testa en minimal demo via CDN/online-sandbox (t.ex. StackBlitz):
<!doctype html>
<div id="root"></div>
<script type="module">
import React from 'https://esm.sh/react';
import ReactDOM from 'https://esm.sh/react-dom/client';
function App() {
return React.createElement('h1', null, 'Hej från React!');
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(React.createElement(App));
// För riktig utveckling: använd Vite (se nedan)
}</script>
Alternativ 1: Vite (Rekommenderat)
# Skapa nytt projekt med Vite
npm create vite@latest min-react-app -- --template react
cd min-react-app
# Installera dependencies
npm install
# Starta utvecklingsserver
npm run dev
Alternativ 2: Create React App (historiskt)
# Skapa nytt projekt
npx create-react-app min-react-app
cd min-react-app
# Starta utvecklingsserver
npm start
Projektstruktur (Create React App)
min-react-app/
├── public/
│ ├── index.html # HTML template
│ └── favicon.ico
├── src/
│ ├── App.js # Main component
│ ├── App.css # Styles för App
│ ├── index.js # Entry point
│ └── index.css # Global styles
├── package.json # Dependencies och scripts
└── README.md
Din Första React-komponent
Låt oss titta på en enkel komponent:
// src/App.js
import './App.css';
function App() {
const message = "Välkommen till React!";
const currentYear = new Date().getFullYear();
return (
<div className="App">
<header className="App-header">
<h1>{message}</h1>
<p>Året är {currentYear}</p>
<button onClick={() => alert('Hej från React!')}>
Klicka mig!
</button>
</header>
</div>
);
}
export default App;
// src/index.js - Entry point
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
Notis:
- Med den nya JSX-transformen (React 17+) behöver du inte längre
import React from 'react'
i varje komponentfil. Vissa mallar kan fortfarande inkludera importen – båda fungerar. - I React 18 kör
StrictMode
effekter två gånger i utvecklingsläge för att upptäcka biverkningar. Det påverkar inte produktion.
React Developer Tools
Installera React Developer Tools i din webbläsare:
Detta ger dig:
- Komponentträd-visning
- Props och state inspektion
- Prestanda-profiling
- Debugging-verktyg
Vad händer härnäst?
Nu har du lärt dig grunderna för att beskriva UI:t med React! I nästa avsnitt kommer vi att utforska:
- Lägga till interaktivitet - hantera events och state
- Hantera state - ge komponenter minne
- Formulär - samla in användarinput
- API-integration - hämta data från servrar
Är du redo? Gå vidare till nästa lektion för att lära dig hur du gör dina komponenter interaktiva!
Komponenter: Återanvändbara UI-byggstenar
Komponenter låter dig dela upp UI:t i oberoende, återanvändbara delar och tänka på varje del isolerat. Den här sidan ger en introduktion till idén om komponenter.
Mål: Lära dig skapa, organisera och återanvända komponenter för att bygga skalbar React-kod.
Definiera en Komponent
React-komponenter är JavaScript-funktioner som returnerar markup:
function MyButton() {
return (
<button>Jag är en knapp</button>
);
}
Nu när du har deklarerat MyButton
kan du nästla den i en annan komponent:
function MyApp() {
return (
<div>
<h1>Välkommen till min app</h1>
<MyButton />
</div>
);
}
Observera att <MyButton />
börjar med stor bokstav. Så här vet du att det är en React-komponent. React-komponentnamn måste alltid börja med stor bokstav, medan HTML-taggar måste vara små bokstäver.
Komponenter inom komponenter
Komponenter är vanliga JavaScript-funktioner, så du kan hålla flera komponenter i samma fil:
function Avatar() {
return (
<img
className="avatar"
src="https://i.imgur.com/1bX5QH6.jpg"
alt="Lin Lanying"
width={100}
height={100}
/>
);
}
function Profile() {
return (
<div>
<Avatar />
<Avatar />
<Avatar />
</div>
);
}
I detta exempel har Profile
-komponenten tre Avatar
-komponenter.
Funktionella Komponenter: Det Moderna Sättet
Sedan React 16.8 och introduktionen av Hooks är funktionella komponenter standardsättet att skriva React-kod.
Grundläggande Komponent
// Enkel funktionell komponent
function Greeting() {
return <h1>Hej världen!</h1>;
}
// Arrow function syntax (också vanlig)
const Greeting = () => {
return <h1>Hej världen!</h1>;
};
// Kort syntax för enkel return
const Greeting = () => <h1>Hej världen!</h1>;
Komponent med Logik
function UserProfile() {
const user = {
name: "Anna Andersson",
age: 28,
email: "anna@example.com",
avatar: "/images/anna.jpg"
};
const isAdult = user.age >= 18;
return (
<div className="user-profile">
<img src={user.avatar} alt={`${user.name}s avatar`} />
<h2>{user.name}</h2>
<p>Ålder: {user.age} {isAdult && "✅ Myndig"}</p>
<p>E-post: {user.email}</p>
</div>
);
}
Exportera och Importera Komponenter
Magin med komponenter ligger i deras återanvändbarhet: du kan skapa komponenter som består av andra komponenter. Men när du nästlar fler och fler komponenter är det ofta vettigt att börja dela upp dem i olika filer. Detta låter dig hålla dina filer lätta att skanna och återanvända komponenter på fler ställen.
// Gallery.js
import Profile from './Profile.js';
function Gallery() {
return (
<section>
<h1>Fantastiska forskare</h1>
<Profile />
<Profile />
<Profile />
</section>
);
}
export default Gallery;
// Profile.js
function Profile() {
return (
<img
src="https://i.imgur.com/QIrZWGIs.jpg"
alt="Alan L. Hart"
/>
);
}
export default Profile;
// App.js
import Gallery from './Gallery.js';
function App() {
return (
<Gallery />
);
}
export default App;
Komponentens Anatomi
En React-komponent består av några viktiga delar:
// 1. Import-statements (om du använder andra komponenter)
import { useState } from 'react';
import './Button.css';
// 2. Komponentfunktionen
function Button() {
// 3. Logik (variabler, funktioner)
const handleClick = () => {
alert('Knappen klickades!');
};
// 4. Return-statement med JSX
return (
<button onClick={handleClick}>
Klicka mig
</button>
);
}
// 5. Export-statement
export default Button;
Klasskomponenter: Det Äldre Sättet
Före hooks använde React klasskomponenter för att hantera state och lifecycle. Du kommer fortfarande stöta på dem i äldre kodbaser.
import React, { Component } from 'react';
class ClassCounter extends Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
// Lifecycle-metod
componentDidMount() {
console.log('Component har monterats');
}
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
console.log(`Count ändrades från ${prevState.count} till ${this.state.count}`);
}
}
componentWillUnmount() {
console.log('Component kommer att avmonteras');
}
// Event handler
incrementCount = () => {
this.setState({ count: this.state.count + 1 });
}
render() {
return (
<div>
<h2>Räknare: {this.state.count}</h2>
<button onClick={this.incrementCount}>
Öka
</button>
</div>
);
}
}
Organisera Komponenter
När din app växer är det viktigt att organisera komponenter på ett smart sätt:
Fil-per-komponent (Rekommenderat)
src/
components/
Button/
Button.jsx
Button.css
Button.test.js
Avatar/
Avatar.jsx
Avatar.css
Card/
Card.jsx
Card.css
pages/
Home.jsx
About.jsx
App.jsx
Flera komponenter per fil (För små, relaterade komponenter)
// components/UI.jsx
export function Button({ children, onClick, type = "button" }) {
return (
<button type={type} onClick={onClick} className="btn">
{children}
</button>
);
}
export function Input({ placeholder, value, onChange }) {
return (
<input
className="input"
placeholder={placeholder}
value={value}
onChange={onChange}
/>
);
}
export function Card({ children }) {
return (
<div className="card">
{children}
</div>
);
}
Komponent-komposition
En av Reacts starkaste funktioner är komposition - att bygga komplexa komponenter från enklare komponenter:
function WelcomeMessage({ name }) {
return <h1>Hej {name}!</h1>;
}
function UserAvatar({ imageUrl, name }) {
return (
<img
src={imageUrl}
alt={name}
className="avatar"
/>
);
}
function UserCard({ user }) {
return (
<div className="user-card">
<UserAvatar imageUrl={user.avatar} name={user.name} />
<WelcomeMessage name={user.name} />
<p>E-post: {user.email}</p>
</div>
);
}
// Användning
function App() {
const user = {
name: "Anna Andersson",
email: "anna@example.com",
avatar: "/anna.jpg"
};
return <UserCard user={user} />;
}
Best Practices för Komponenter
1. Håll Komponenter Små och Fokuserade
// ❌ För stor komponent
function BlogPost({ post, user, comments, relatedPosts }) {
return (
<article>
{/* Hundratals rader kod... */}
</article>
);
}
// ✅ Uppdelade komponenter
function BlogPost({ post }) {
return (
<article>
<BlogHeader post={post} />
<BlogContent content={post.content} />
<BlogFooter postId={post.id} />
</article>
);
}
function BlogHeader({ post }) {
return (
<header>
<h1>{post.title}</h1>
<AuthorInfo author={post.author} />
<PublishDate date={post.publishedAt} />
</header>
);
}
2. Använda PropTypes för Typkontroll (Optional)
PropTypes är ett verktyg för typkontroll i React som hjälper dig att:
-
Hitta buggar tidigt: Genom att validera props under utveckling upptäcks fel direkt istället för att de dyker upp i produktion.
-
Dokumentera komponenter: PropTypes fungerar som självdokumenterande kod - andra utvecklare kan snabbt se vilka props en komponent förväntar sig.
-
Förbättra underhållbarhet: När du ändrar en komponent varnar PropTypes om du råkar skicka fel datatyp eller glömmer en required prop.
-
Underlätta refaktorering: Med tydliga kontrakt mellan komponenter blir det säkrare att göra större kodändringar.
Exempel på vanliga PropTypes:
import PropTypes from 'prop-types';
function ProductCard({ name, price, image, onSale }) {
return (
<div className="product-card">
<img src={image} alt={name} />
<h3>{name}</h3>
<p className={onSale ? "sale-price" : "regular-price"}>
{price} kr
</p>
</div>
);
}
ProductCard.propTypes = {
name: PropTypes.string.isRequired,
price: PropTypes.number.isRequired,
image: PropTypes.string.isRequired,
onSale: PropTypes.bool
};
ProductCard.defaultProps = {
onSale: false
};
Ett vanligt alternativ till PropTypes är TypeScript som erbjuder statisk typkontroll under kompilering och ger bättre IDE-stöd med autocompletions och refaktoreringsmöjligheter. TypeScript är särskilt användbart i större projekt där PropTypes kan bli otillräckligt.
3. Komponentnamnskonventioner
// ✅ PascalCase för komponenter
function UserProfile() { }
function ProductCard() { }
function NavigationMenu() { }
// ✅ camelCase för funktioner och variabler
const handleClick = () => { };
const userName = "Anna";
const isLoading = true;
// ✅ Beskrivande namn
function LoadingSpinner() { } // Bättre än Spinner()
function ErrorMessage() { } // Bättre än Error()
Sammanfattning
Komponenter är byggstenen i React-applikationer:
- Komponenter är JavaScript-funktioner som returnerar JSX
- Komponentnamn måste börja med stor bokstav
- Import/Export låter dig organisera komponenter i separata filer
- Komposition bygger komplexa UI från enkla komponenter
- Håll komponenter små och fokuserade på en sak
Vad händer härnäst?
Nu när du kan skapa komponenter är det dags att göra dem interaktiva! I nästa avsnitt lär du dig:
- Props - skicka data mellan komponenter
- State - ge komponenter minne
- Events - reagera på användarinteraktion
Gå vidare till State och Props för att lära dig hur du gör dina komponenter levande!
Lägga till Interaktivitet: Props, State och Events
Komponenter behöver ofta interagera med varandra och reagera på användarinteraktion. React använder props för att skicka data och state för att komma ihåg saker. Tillsammans gör de dina komponenter interaktiva!
Mål: Lära sig skicka data med props, hantera användarinteraktion med events och ge komponenter minne med state.
Skicka Data med Props
React-komponenter använder props för att kommunicera med varandra. Varje föräldrakomponent kan skicka information till sina barnkomponenter genom att ge dem props.
Grundläggande Props
// Komponent som tar emot props
function Avatar({ person, size }) {
return (
<img
className="avatar"
src={person.imageUrl}
alt={person.name}
width={size}
height={size}
/>
);
}
// Föräldrakomponent som skickar props
function Profile() {
return (
<Avatar
size={100}
person={{
name: 'Katsuko Saruhashi',
imageUrl: 'https://i.imgur.com/YfeOqp2s.jpg'
}}
/>
);
}
Props kan vara vilken datatyp som helst
function Card({ title, children, isHighlighted, tags, onClose }) {
return (
<div className={`card ${isHighlighted ? 'highlighted' : ''}`}>
<div className="card-header">
<h3>{title}</h3>
<button onClick={onClose}>×</button>
</div>
<div className="card-body">
{children}
</div>
<div className="card-footer">
{tags.map(tag => (
<span key={tag} className="tag">{tag}</span>
))}
</div>
</div>
);
}
// Användning - skicka olika datatyper som props
function App() {
return (
<Card
title="Min artikel" // string
isHighlighted={true} // boolean
tags={['react', 'javascript', 'web']} // array
onClose={() => alert('Stänger!')} // function
>
<p>Detta är innehållet i kortet.</p> {/* children */}
</Card>
);
}
Reagera på Events
Du kan reagera på events genom att deklarera event handler-funktioner inuti dina komponenter:
function Button() {
function handleClick() {
alert('Du klickade på mig!');
}
return (
<button onClick={handleClick}>
Klicka mig
</button>
);
}
Observera att onClick={handleClick}
inte har parenteser i slutet! Anropa inte event handler-funktionen: du behöver bara skicka den nedåt. React kommer att anropa din event handler när användaren klickar på knappen.
Event Handlers kan vara inline
function Button() {
return (
<button onClick={() => alert('Du klickade på mig!')}>
Klicka mig
</button>
);
}
Skicka Event Handlers som Props
Ofta vill du att föräldrakomponenten ska specificera en barnkomponents event handler:
function Button({ onClick, children }) {
return (
<button onClick={onClick}>
{children}
</button>
);
}
function PlayButton({ movieName }) {
function handlePlayClick() {
alert(`Spelar ${movieName}!`);
}
return (
<Button onClick={handlePlayClick}>
Spela "{movieName}"
</Button>
);
}
function UploadButton() {
return (
<Button onClick={() => alert('Laddar upp!')}>
Ladda upp bild
</Button>
);
}
State: En Komponents Minne
Komponenter behöver ofta ändra vad som visas på skärmen som resultat av interaktion. Att skriva i formuläret bör uppdatera input-fältet, att klicka på "nästa" i en bildkarusell bör ändra vilken bild som visas, att klicka på "köp" bör lägga en produkt i kundvagnen. Komponenter behöver "komma ihåg" saker: det aktuella input-värdet, den aktuella bilden, kundvagnen. I React kallas denna typ av komponentspecifikt minne state.
Lägga till State i en Komponent
För att lägga till state i en komponent, importera useState
från React:
import { useState } from 'react';
function MyButton() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}
return (
<button onClick={handleClick}>
Klickad {count} gånger
</button>
);
}
useState
returnerar två saker:
- Det aktuella state-värdet (
count
) - En funktion för att uppdatera det (
setCount
)
Du kan ge dem vilka namn du vill, men konventionen är att skriva [something, setSomething]
.
State är isolerat och privat
State är lokalt för en komponentinstans på skärmen. Med andra ord, om du renderar samma komponent två gånger får varje kopia sin egen state:
function MyApp() {
return (
<div>
<h1>Räknare som uppdateras oberoende</h1>
<MyButton />
<MyButton />
</div>
);
}
Observera hur varje knapp "kommer ihåg" sin egen count
-state och inte påverkar den andra.
Olika Typer av State
State kan innehålla vilken typ av JavaScript-värde som helst:
import { useState } from 'react';
function Form() {
// Olika typer av state
const [name, setName] = useState(''); // string
const [age, setAge] = useState(0); // number
const [isSubscribed, setIsSubscribed] = useState(false); // boolean
const [hobbies, setHobbies] = useState([]); // array
return (
<div>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Ditt namn"
/>
<p>Hej {name}!</p>
<input
type="number"
value={age}
onChange={(e) => setAge(Number(e.target.value))}
placeholder="Din ålder"
/>
<p>Du är {age} år gammal.</p>
<label>
<input
type="checkbox"
checked={isSubscribed}
onChange={(e) => setIsSubscribed(e.target.checked)}
/>
Prenumerera på nyhetsbrev
</label>
<p>{isSubscribed ? 'Du är prenumerant!' : 'Du är inte prenumerant.'}</p>
</div>
);
}
State och Props Tillsammans
Ofta använder du state och props tillsammans. Här är ett exempel där en föräldrakomponent håller state och skickar det till barnkomponenter via props:
function Counter() {
const [count, setCount] = useState(0);
function handleIncrement() {
setCount(count + 1);
}
return (
<div>
<h2>Räknare</h2>
<CounterDisplay count={count} />
<CounterButton onClick={handleIncrement} />
</div>
);
}
function CounterDisplay({ count }) {
return <p>Aktuellt värde: {count}</p>;
}
function CounterButton({ onClick }) {
return (
<button onClick={onClick}>
Öka med 1
</button>
);
}
I detta exempel:
Counter
äger state (count
)CounterDisplay
fårcount
via propsCounterButton
fåronClick
funktionen via props- När knappen klickas uppdateras state, vilket orsakar en re-render
State Batching och Funktionella Updates
function AdvancedCounter() {
const [count, setCount] = useState(0);
// ❌ Problematiskt - baserat på nuvarande värde
const incrementTwice = () => {
setCount(count + 1);
setCount(count + 1); // Fortfarande baserat på samma värde!
};
// ✅ Bättre - funktionell update
const incrementTwiceProperly = () => {
setCount(prev => prev + 1);
setCount(prev => prev + 1); // Använder uppdaterat värde
};
// ✅ Batch updates automatiskt
const handleMultipleUpdates = () => {
setCount(prev => prev + 1);
// React batchar dessa tillsammans
console.log('This will run after all updates');
};
return (
<div>
<p>Count: {count}</p>
<button onClick={incrementTwice}>+2 (Fel)</button>
<button onClick={incrementTwiceProperly}>+2 (Rätt)</button>
</div>
);
}
useEffect Hook: Side Effects och Lifecycle
useEffect
hanterar "side effects" - allt som inte är direkt kopplat till rendering.
Grundläggande useEffect Patterns
import { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// Pattern 1: Kör efter varje render
useEffect(() => {
console.log('Component rendered');
});
// Pattern 2: Kör bara en gång (componentDidMount)
useEffect(() => {
console.log('Component mounted');
}, []); // Tom dependency array
// Pattern 3: Kör när specifika värden ändras (med AbortController)
useEffect(() => {
if (!userId) return;
const controller = new AbortController();
const { signal } = controller;
const fetchUser = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(`/api/users/${userId}`, { signal });
if (!response.ok) throw new Error('Användare hittades inte');
const userData = await response.json();
setUser(userData);
} catch (err) {
if (err.name !== 'AbortError') {
setError(err.message);
}
} finally {
setLoading(false);
}
};
fetchUser();
return () => controller.abort();
}, [userId]); // Kör när userId ändras
// Pattern 4: Cleanup (componentWillUnmount)
useEffect(() => {
const timer = setInterval(() => {
console.log('Timer tick');
}, 1000);
// Cleanup function
return () => {
clearInterval(timer);
console.log('Timer cleared');
};
}, []);
if (loading) return <div>Laddar...</div>;
if (error) return <div>Fel: {error}</div>;
if (!user) return <div>Ingen användare vald</div>;
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
useEffect Best Practices
function SearchComponent() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
// Undvik onödiga API-anrop
if (!query.trim()) {
setResults([]);
return;
}
const searchTimer = setTimeout(async () => {
setLoading(true);
try {
const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
const data = await response.json();
setResults(data.results);
} catch (error) {
console.error('Search failed:', error);
setResults([]);
} finally {
setLoading(false);
}
}, 300); // Debounce 300ms
// Cleanup - avbryt föregående timer
return () => clearTimeout(searchTimer);
}, [query]);
return (
<div>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Sök..."
/>
{loading && <p>Söker...</p>}
<ul>
{results.map(result => (
<li key={result.id}>{result.title}</li>
))}
</ul>
</div>
);
}
Observera: aktivera ESLint-regeln react-hooks/exhaustive-deps
för att fånga saknade beroenden i hooks och undvika subtila buggar.
Props Drilling: Problem och Lösningar
Props drilling uppstår när data behöver passas genom många komponentnivåer.
// ❌ Props drilling problem
function App() {
const [user, setUser] = useState({ name: 'Anna', theme: 'dark' });
return (
<div>
<Header user={user} setUser={setUser} />
<Main user={user} setUser={setUser} />
</div>
);
}
function Header({ user, setUser }) {
return (
<header>
<Logo />
<Navigation user={user} setUser={setUser} />
</header>
);
}
function Navigation({ user, setUser }) {
return (
<nav>
<UserMenu user={user} setUser={setUser} />
</nav>
);
}
function UserMenu({ user, setUser }) {
const toggleTheme = () => {
setUser(prev => ({
...prev,
theme: prev.theme === 'light' ? 'dark' : 'light'
}));
};
return (
<div>
<span>Hej {user.name}!</span>
<button onClick={toggleTheme}>
Tema: {user.theme}
</button>
</div>
);
}
Context API: Global State Management
Context API löser props drilling genom att skapa en "global" state som komponenter kan accessa direkt.
import { createContext, useContext, useState } from 'react';
// 1. Skapa Context
const UserContext = createContext();
// 2. Skapa Provider Component
function UserProvider({ children }) {
const [user, setUser] = useState({
name: 'Anna',
theme: 'light',
isAuthenticated: false
});
const login = (userData) => {
setUser(prev => ({
...prev,
...userData,
isAuthenticated: true
}));
};
const logout = () => {
setUser({
name: '',
theme: 'light',
isAuthenticated: false
});
};
const toggleTheme = () => {
setUser(prev => ({
...prev,
theme: prev.theme === 'light' ? 'dark' : 'light'
}));
};
const value = {
user,
login,
logout,
toggleTheme
};
return (
<UserContext.Provider value={value}>
{children}
</UserContext.Provider>
);
}
// 3. Custom hook för att använda context
function useUser() {
const context = useContext(UserContext);
if (!context) {
throw new Error('useUser must be used within UserProvider');
}
return context;
}
// 4. Använd Context i komponenter
function App() {
return (
<UserProvider>
<div className="app">
<Header />
<Main />
<Footer />
</div>
</UserProvider>
);
}
function Header() {
return (
<header>
<Logo />
<Navigation />
</header>
);
}
// Ingen props drilling längre!
function Navigation() {
const { user, toggleTheme, logout } = useUser();
return (
<nav>
<span>Hej {user.name}!</span>
<button onClick={toggleTheme}>
Tema: {user.theme}
</button>
{user.isAuthenticated && (
<button onClick={logout}>
Logga ut
</button>
)}
</nav>
);
}
function Main() {
const { user } = useUser();
return (
<main className={`theme-${user.theme}`}>
{user.isAuthenticated ? (
<Dashboard />
) : (
<LoginForm />
)}
</main>
);
}
function LoginForm() {
const { login } = useUser();
const [formData, setFormData] = useState({ name: '', email: '' });
const handleSubmit = (e) => {
e.preventDefault();
login(formData);
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={formData.name}
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
placeholder="Namn"
/>
<input
type="email"
value={formData.email}
onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
placeholder="E-post"
/>
<button type="submit">Logga in</button>
</form>
);
}
Avancerade State Patterns
Custom Hooks för State Logic
// Custom hook för formulärhantering
function useForm(initialValues) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const setValue = (name, value) => {
setValues(prev => ({ ...prev, [name]: value }));
// Rensa fel när användaren rättar
if (errors[name]) {
setErrors(prev => ({ ...prev, [name]: '' }));
}
};
const setError = (name, error) => {
setErrors(prev => ({ ...prev, [name]: error }));
};
const reset = () => {
setValues(initialValues);
setErrors({});
};
return {
values,
errors,
setValue,
setError,
reset
};
}
// Custom hook för localStorage
function useLocalStorage(key, defaultValue) {
const [value, setValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : defaultValue;
} catch (error) {
console.error(`Error reading localStorage key "${key}":`, error);
return defaultValue;
}
});
const setStoredValue = (newValue) => {
try {
setValue(newValue);
window.localStorage.setItem(key, JSON.stringify(newValue));
} catch (error) {
console.error(`Error setting localStorage key "${key}":`, error);
}
};
return [value, setStoredValue];
}
// Användning av custom hooks
function UserPreferences() {
const { values, setValue, errors, setError } = useForm({
theme: 'light',
language: 'sv',
notifications: true
});
const [settings, setSettings] = useLocalStorage('userSettings', values);
const handleSave = () => {
if (!values.language) {
setError('language', 'Språk är obligatoriskt');
return;
}
setSettings(values);
alert('Inställningar sparade!');
};
return (
<div>
<h2>Användarinställningar</h2>
<select
value={values.theme}
onChange={(e) => setValue('theme', e.target.value)}
>
<option value="light">Ljust tema</option>
<option value="dark">Mörkt tema</option>
</select>
<select
value={values.language}
onChange={(e) => setValue('language', e.target.value)}
>
<option value="">Välj språk</option>
<option value="sv">Svenska</option>
<option value="en">Engelska</option>
</select>
{errors.language && <span className="error">{errors.language}</span>}
<label>
<input
type="checkbox"
checked={values.notifications}
onChange={(e) => setValue('notifications', e.target.checked)}
/>
Aktivera notifikationer
</label>
<button onClick={handleSave}>Spara inställningar</button>
</div>
);
}
Best Practices för State Management
1. Håll State så Lokalt som Möjligt
// ❌ Onödig global state
function App() {
const [modalOpen, setModalOpen] = useState(false);
const [modalContent, setModalContent] = useState('');
return (
<div>
<Header />
<Main modalOpen={modalOpen} setModalOpen={setModalOpen} />
{modalOpen && <Modal content={modalContent} />}
</div>
);
}
// ✅ Lokal state där den behövs
function ProductCard({ product }) {
const [showDetails, setShowDetails] = useState(false);
return (
<div className="product-card">
<h3>{product.name}</h3>
<button onClick={() => setShowDetails(!showDetails)}>
{showDetails ? 'Dölj' : 'Visa'} detaljer
</button>
{showDetails && <ProductDetails product={product} />}
</div>
);
}
2. Normalisera Komplex State
// ❌ Nested state structure
const [state, setState] = useState({
posts: [
{
id: 1,
title: 'Post 1',
author: { id: 1, name: 'Anna' },
comments: [
{ id: 1, text: 'Bra inlägg!', author: { id: 2, name: 'Erik' } }
]
}
]
});
// ✅ Normalized state structure
const [state, setState] = useState({
posts: { 1: { id: 1, title: 'Post 1', authorId: 1, commentIds: [1] } },
authors: { 1: { id: 1, name: 'Anna' }, 2: { id: 2, name: 'Erik' } },
comments: { 1: { id: 1, text: 'Bra inlägg!', authorId: 2 } }
});
Sammanfattning
Nu kan du skapa interaktiva React-komponenter:
- Props skickar data från föräldra- till barnkomponenter
- Event handlers låter komponenter reagera på användarinteraktion
- State ger komponenter minne med
useState
- State är privat - varje komponentinstans har sin egen state
- State + Props skapar dataflöde mellan komponenter
Vad händer härnäst?
Nu när dina komponenter kan ta emot data och reagera på interaktion är det dags att bygga riktiga applikationer! I nästa avsnitt lär du dig:
- Formulär - hantera användarinput på ett kontrollerat sätt
- API-integration - hämta data från servrar
- Routing - navigera mellan olika vyer
- Deployment - publicera din app på internet
Redo för nästa steg? Gå vidare till Formulär i React för att lära dig hantera användarinput!
Fördjupning: Avancerade State-patterns
Denna sektion täcker mer avancerade ämnen som du kan hoppa över först och komma tillbaka till senare.
Routing med React Router: Navigation i SPA:er
I traditionella webbapplikationer hanteras navigation av servern - varje URL motsvarar en specifik fil eller endpoint. I Single Page Applications behöver vi client-side routing för att hantera navigation utan att ladda om hela sidan.
React Router är det de facto-standardbiblioteket för routing i React-applikationer.
Mål: Lära sig React Router, implementera BrowserRouter, konfigurera routes, hantera navigation programmatiskt och skapa protected routes.
Installation och Grundkonfiguration
Installation
npm install react-router-dom
Grundläggande Router Setup
// App.js
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Layout from './components/Layout';
import Home from './pages/Home';
import About from './pages/About';
import Contact from './pages/Contact';
import NotFound from './pages/NotFound';
function App() {
return (
<Router>
<Layout>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
<Route path="*" element={<NotFound />} />
</Routes>
</Layout>
</Router>
);
}
export default App;
Router-typer
React Router erbjuder flera router-typer för olika användningsfall:
graph TD A[React Router] --> B[BrowserRouter] A --> C[HashRouter] A --> D[MemoryRouter] B --> E["Clean URLs<br/>/about, /products"] C --> F["Hash URLs<br/>/#/about, /#/products"] D --> G["Memory only<br/>(testing, React Native)"] style B fill:#98fb98 style E fill:#98fb98
BrowserRouter (Rekommenderad)
import { BrowserRouter } from 'react-router-dom';
// Ger rena URLs: example.com/about
function App() {
return (
<BrowserRouter>
{/* Routes här */}
</BrowserRouter>
);
}
HashRouter (Fallback)
import { HashRouter } from 'react-router-dom';
// Ger hash URLs: example.com/#/about
// Användbart för statiska hostar som GitHub Pages
function App() {
return (
<HashRouter>
{/* Routes här */}
</HashRouter>
);
}
Routes och Route Configuration
Grundläggande Routes
import { Routes, Route } from 'react-router-dom';
function AppRoutes() {
return (
<Routes>
{/* Exact match för hem-sidan */}
<Route path="/" element={<Home />} />
{/* Enkla routes */}
<Route path="/about" element={<About />} />
<Route path="/services" element={<Services />} />
<Route path="/contact" element={<Contact />} />
{/* 404 - måste vara sist */}
<Route path="*" element={<NotFound />} />
</Routes>
);
}
Nested Routes (Nästlade routes)
// Huvudroutes
function App() {
return (
<Router>
<Routes>
<Route path="/" element={<Layout />}>
{/* Child routes */}
<Route index element={<Home />} />
<Route path="about" element={<About />} />
<Route path="products/*" element={<ProductRoutes />} />
</Route>
</Routes>
</Router>
);
}
// Layout-komponent med Outlet
import { Outlet } from 'react-router-dom';
function Layout() {
return (
<div>
<header>
<Navigation />
</header>
<main>
<Outlet /> {/* Här renderas child routes */}
</main>
<footer>
<Footer />
</footer>
</div>
);
}
// Produktroutes som separata komponenter (v6, relativa paths)
function ProductRoutes() {
return (
<Routes>
<Route index element={<ProductList />} />
<Route path=":id" element={<ProductDetail />} />
<Route path="categories" element={<ProductCategories />} />
<Route path="categories/:category" element={<CategoryProducts />} />
</Routes>
);
}
Dynamiska Routes med Parameters
import { useParams } from 'react-router-dom';
// Route configuration
<Route path="/users/:userId" element={<UserProfile />} />
<Route path="/posts/:postId/comments/:commentId" element={<Comment />} />
// I komponenten
function UserProfile() {
const { userId } = useParams();
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(setUser);
}, [userId]);
if (!user) return <div>Laddar...</div>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
// Query parameters
function ProductList() {
const [searchParams, setSearchParams] = useSearchParams();
const category = searchParams.get('category');
const sort = searchParams.get('sort');
const updateSearch = (newCategory) => {
setSearchParams({ category: newCategory, sort });
};
return (
<div>
<h2>Produkter {category && `i kategorin ${category}`}</h2>
<button onClick={() => updateSearch('electronics')}>
Elektronik
</button>
</div>
);
}
Navigation (varför och hur)
Link Component
import { Link } from 'react-router-dom';
function Navigation() {
return (
<nav>
{/* Grundläggande länkar */}
<Link to="/">Hem</Link>
<Link to="/about">Om oss</Link>
<Link to="/products">Produkter</Link>
{/* Dynamiska länkar */}
<Link to={`/users/${user.id}`}>Min profil</Link>
{/* Med state (för att skicka data) */}
<Link
to="/checkout"
state={{ from: 'cart', items: cartItems }}
>
Till kassan
</Link>
{/* Ersätt current history entry */}
<Link to="/login" replace>
Logga in
</Link>
</nav>
);
}
NavLink för Aktiva Länkar
import { NavLink } from 'react-router-dom';
function Navigation() {
return (
<nav>
<NavLink
to="/"
className={({ isActive }) => isActive ? 'active' : ''}
>
Hem
</NavLink>
<NavLink
to="/products"
style={({ isActive }) => ({
color: isActive ? 'red' : 'blue'
})}
>
Produkter
</NavLink>
{/* Med custom active class (v6) */}
<NavLink
to="/about"
className={({ isActive }) => `nav-link ${isActive ? 'current' : ''}`}
>
Om oss
</NavLink>
</nav>
);
}
Programmatisk Navigation (varför)
import { useNavigate, useLocation } from 'react-router-dom';
function LoginForm() {
const navigate = useNavigate();
const location = useLocation();
const handleLogin = async (credentials) => {
try {
await login(credentials);
// Navigera tillbaka till föregående sida eller hem
const from = location.state?.from?.pathname || '/';
navigate(from, { replace: true });
} catch (error) {
setError('Inloggning misslyckades');
}
};
const goBack = () => {
navigate(-1); // Gå tillbaka i historiken
};
const goToProducts = () => {
navigate('/products', {
state: { from: 'login' },
replace: false
});
};
return (
<form onSubmit={handleLogin}>
{/* Formulärfält */}
<button type="button" onClick={goBack}>
Tillbaka
</button>
<button type="submit">
Logga in
</button>
</form>
);
}
Vidare läsning
Fler routing‑ämnen när du är redo: skyddade routes (auth), data‑laddare, central felhantering, och animerade övergångar. Dessa bör komma efter att grunderna sitter.
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
errorElement: <ErrorPage />,
children: [
{
index: true,
element: <Home />
},
{
path: 'products',
element: <ProductsLayout />,
children: [
{
index: true,
element: <ProductList />
},
{
path: ':id',
element: <ProductDetail />,
loader: async ({ params }) => {
return fetch(`/api/products/${params.id}`);
}
}
]
},
{
path: 'admin',
element: <ProtectedRoute requiredRole="admin"><AdminLayout /></ProtectedRoute>,
children: [
{
path: 'users',
element: <UserManagement />
},
{
path: 'settings',
element: <Settings />
}
]
}
]
}
]);
function App() {
return <RouterProvider router={router} />;
}
// Modern data loading pattern (React Router v6.4+)
const router = createBrowserRouter([
{
path: '/products/:id',
element: <ProductDetail />,
loader: async ({ params }) => {
const product = await fetch(`/api/products/${params.id}`);
if (!product.ok) {
throw new Response('Product not found', { status: 404 });
}
return product.json();
},
errorElement: <ProductError />
}
]);
// I komponenten
function ProductDetail() {
const product = useLoaderData();
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
</div>
);
}
import { AnimatePresence, motion } from 'framer-motion';
import { useLocation } from 'react-router-dom';
function AnimatedRoutes() {
const location = useLocation();
return (
<AnimatePresence mode="wait">
<Routes location={location} key={location.pathname}>
<Route
path="/"
element={
<motion.div
initial={{ opacity: 0, x: -100 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 100 }}
transition={{ duration: 0.3 }}
>
<Home />
</motion.div>
}
/>
{/* Andra routes... */}
</Routes>
</AnimatePresence>
);
}
Error Boundaries för Routes
class RouteErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error('Route error:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div className="error-page">
<h2>Något gick fel</h2>
<p>Vi ber om ursäkt för besväret.</p>
<Link to="/">Gå tillbaka till startsidan</Link>
</div>
);
}
return this.props.children;
}
}
// 404 Component
function NotFound() {
const location = useLocation();
return (
<div className="not-found">
<h1>404 - Sidan hittades inte</h1>
<p>Sidan <code>{location.pathname}</code> existerar inte.</p>
<Link to="/">Gå till startsidan</Link>
</div>
);
}
Best Practices
1. Route Organization
// routes/index.js - Centraliserad route configuration
export const routes = {
home: '/',
about: '/about',
products: '/products',
productDetail: (id) => `/products/${id}`,
userProfile: (userId) => `/users/${userId}`,
admin: {
base: '/admin',
users: '/admin/users',
settings: '/admin/settings'
}
};
// Användning
<Link to={routes.productDetail(product.id)}>
{product.name}
</Link>
2. Route Guards
// Reusable route guard component
function RouteGuard({
children,
requireAuth = false,
requiredRole = null,
fallback = null
}) {
const { user, loading } = useAuth();
const location = useLocation();
if (loading) return <LoadingSpinner />;
if (requireAuth && !user) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
if (requiredRole && (!user || user.role !== requiredRole)) {
return fallback || <Navigate to="/unauthorized" replace />;
}
return children;
}
3. SEO-vänliga Routes
// Dynamiska titles baserat på route
function useDocumentTitle(title) {
useEffect(() => {
const prevTitle = document.title;
document.title = title;
return () => {
document.title = prevTitle;
};
}, [title]);
}
function ProductDetail() {
const { id } = useParams();
const [product, setProduct] = useState(null);
useDocumentTitle(product ? `${product.name} - Min Butik` : 'Laddar...');
// ...
}
Sammanfattning
React Router är kraftfullt verktyg för navigation i React-applikationer:
- BrowserRouter ger rena URLs och är bästa valet för de flesta applikationer
- Routes och Route konfigurerar URL-mappningar till komponenter
- Link och NavLink skapar navigerbara länkar
- useNavigate möjliggör programmatisk navigation
- Protected Routes skyddar känsligt innehåll
- Dynamic routes hanterar parametrar och query strings
Nästa steg är att lära sig konsumera API:er för att hämta och skicka data till backend-tjänster.
Formulär i React: Interaktiv Användarinmatning
Problemet: Statisk HTML räcker inte
Du har byggt en fin statisk webbsida, men nu vill användarna kunna skicka meddelanden, registrera konton eller söka efter produkter. Vanlig HTML-formulär skickar dig till en ny sida - inte så användarvänligt för moderna webbappar.
<!-- Traditionellt HTML-formulär -->
<form action="/submit" method="POST">
<input type="text" name="username" />
<button type="submit">Skicka</button>
</form>
<!-- Sidan laddas om och användaren förlorar sitt sammanhang -->
Problem:
- Sidan laddas om vid varje submit
- Ingen validering innan skickning
- Svårt att ge användaren feedback
- Kan inte kombinera med andra React-komponenter smidigt
Lösningen: Kontrollerade formulär med React
React gör formulär interaktiva genom att låta oss hantera all input i JavaScript och ge omedelbar feedback utan sidladdningar.
Grundprincipen
I React kontrollerar vi formulär-värden med useState
och hanterar ändringar med event handlers. Detta kallas "controlled components".
Steg-för-steg: Bygg interaktiva formulär
Steg 1: Ett enkelt textfält
Låt oss börja med det allra enklaste - en komponent som visar vad användaren skriver medan de skriver:
import { useState } from 'react';
function SimpleForm() {
const [name, setName] = useState('');
return (
<div>
<h2>Hej, {name || 'okänd person'}!</h2>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Skriv ditt namn..."
/>
<p>Du har skrivit: "{name}"</p>
</div>
);
}
Prova detta: Skriv i fältet och se hur texten uppdateras direkt medan du skriver!
Viktiga delar:
value={name}
- React kontrollerar vad som visasonChange
- Körs varje gång användaren skrivere.target.value
- Det nya värdet från input-fältet
Steg 2: Hantera formulär-submit
Nu lägger vi till en submit-knapp och förhindrar sidladdning:
function ContactForm() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const handleSubmit = (e) => {
e.preventDefault(); // Förhindra sidladdning!
console.log('Formulär skickat:', { name, email });
alert(`Tack ${name}! Vi kontaktar dig på ${email}`);
};
return (
<form onSubmit={handleSubmit} className="contact-form">
<h2>Kontakta oss</h2>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Ditt namn"
className="form-input"
/>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Din e-post"
className="form-input"
/>
<button type="submit" className="submit-button">
Skicka meddelande
</button>
</form>
);
}
Förslag CSS
.contact-form {
max-width: 400px;
margin: 0 auto;
padding: 20px;
}
.form-input {
width: 100%;
padding: 10px;
margin: 10px 0;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
.submit-button {
width: 100%;
padding: 12px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
Testa: Fyll i formuläret och klicka "Skicka meddelande"
Steg 3: Lägg till validering
Nu gör vi formuläret smartare med validering:
function ValidatedForm() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [errors, setErrors] = useState({});
const validateForm = () => {
const newErrors = {};
if (!name.trim()) {
newErrors.name = 'Namn är obligatoriskt';
}
if (!email.trim()) {
newErrors.email = 'E-post är obligatoriskt';
} else if (!email.includes('@')) {
newErrors.email = 'Ange en giltig e-postadress';
}
return newErrors;
};
const handleSubmit = (e) => {
e.preventDefault();
const formErrors = validateForm();
setErrors(formErrors);
if (Object.keys(formErrors).length === 0) {
// Inga fel - skicka formuläret
console.log('Formulär godkänt:', { name, email });
alert('Meddelande skickat!');
// Rensa formuläret
setName('');
setEmail('');
}
};
return (
<form onSubmit={handleSubmit} className="validated-form">
<h2>Kontakta oss</h2>
<div className="form-group">
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Ditt namn"
className={`form-input ${errors.name ? 'error' : ''}`}
/>
{errors.name && <span className="error-message">{errors.name}</span>}
</div>
<div className="form-group">
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Din e-post"
className={`form-input ${errors.email ? 'error' : ''}`}
/>
{errors.email && <span className="error-message">{errors.email}</span>}
</div>
<button type="submit" className="submit-button">
Skicka meddelande
</button>
</form>
);
}
Lägg till CSS för fel:
.form-group {
margin-bottom: 15px;
}
.form-input.error {
border-color: #dc3545;
}
.error-message {
color: #dc3545;
font-size: 14px;
display: block;
margin-top: 5px;
}
Testa: Försök skicka utan att fylla i fälten, eller med ogiltig e-post
Steg 4: Fler formulärelement
Låt oss utöka med olika typer av input:
function CompleteForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
age: '',
country: '',
newsletter: false,
message: ''
});
const handleInputChange = (e) => {
const { name, value, type, checked } = e.target;
setFormData(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value
}));
};
const handleSubmit = (e) => {
e.preventDefault();
console.log('Komplett formulär:', formData);
};
return (
<form onSubmit={handleSubmit} className="complete-form">
<h2>Registrera dig</h2>
{/* Textfält */}
<input
type="text"
name="name"
value={formData.name}
onChange={handleInputChange}
placeholder="Namn"
className="form-input"
/>
{/* E-post */}
<input
type="email"
name="email"
value={formData.email}
onChange={handleInputChange}
placeholder="E-post"
className="form-input"
/>
{/* Nummer */}
<input
type="number"
name="age"
value={formData.age}
onChange={handleInputChange}
placeholder="Ålder"
className="form-input"
min="13"
max="120"
/>
{/* Dropdown */}
<select
name="country"
value={formData.country}
onChange={handleInputChange}
className="form-input"
>
<option value="">Välj land</option>
<option value="sweden">Sverige</option>
<option value="norway">Norge</option>
<option value="denmark">Danmark</option>
<option value="finland">Finland</option>
</select>
{/* Checkbox */}
<label className="checkbox-label">
<input
type="checkbox"
name="newsletter"
checked={formData.newsletter}
onChange={handleInputChange}
/>
Jag vill få nyhetsbrev
</label>
{/* Textarea */}
<textarea
name="message"
value={formData.message}
onChange={handleInputChange}
placeholder="Meddelande (valfritt)"
className="form-input"
rows="4"
/>
<button type="submit" className="submit-button">
Registrera
</button>
{/* Debug: visa vad som fylls i */}
<details style={{ marginTop: '20px' }}>
<summary>Debug: Se formulärdata</summary>
<pre>{JSON.stringify(formData, null, 2)}</pre>
</details>
</form>
);
}
Lägg till CSS:
.checkbox-label {
display: flex;
align-items: center;
margin: 10px 0;
cursor: pointer;
}
.checkbox-label input {
margin-right: 8px;
}
textarea.form-input {
resize: vertical;
min-height: 80px;
}
Steg 5: Experimentera själv! 🎯
Nu har du grunderna för React-formulär. Prova att lägga till:
Enkla förbättringar:
// Realtidsvalidering (validera medan användaren skriver)
const handleNameChange = (e) => {
const value = e.target.value;
setName(value);
if (value.length < 2) {
setNameError('Namnet måste vara minst 2 tecken');
} else {
setNameError('');
}
};
// Räkna tecken i textarea
<div>
<textarea /* ... */ />
<small>{message.length}/500 tecken</small>
</div>
// Disable knapp tills formuläret är giltigt
<button
type="submit"
disabled={!name || !email || errors.name}
className="submit-button"
>
Skicka
</button>
Medelsvåra utmaningar:
- Lösenordsstyrka: Visa färgad indikator för lösenordsstyrka
- Bekräfta lösenord: Kontrollera att två lösenordsfält matchar
- Fil-upload: Hantera filuppladdning med preview
- Steg-för-steg formulär: Dela upp i flera steg/sidor
Avancerade idéer:
- Auto-save: Spara formulärdata i localStorage medan användaren skriver
- Async validering: Kontrollera om e-post redan finns (simulera API-anrop)
- Dynamiska fält: Lägg till/ta bort fält baserat på användarens val
Populära formulärbibliotek
När dina formulär blir mer komplexa finns det hjälpsamma bibliotek:
- React Hook Form: Prestanda-optimerat, minimal re-rendering
- Formik: Populärt val med bra validering
- React Final Form: Flexibelt och kraftfullt
Men lär dig grunderna först - förståelse för kontrollerade komponenter är grunden för allt formulärarbete i React!
Viktiga takeaways
✅ Controlled components: React kontrollerar formulärvärdena via state
✅ preventDefault(): Förhindra sidladdning vid submit
✅ Validering: Ge användaren omedelbar feedback
✅ Olika input-typer: text, email, number, select, checkbox, textarea
✅ Användarvänlighet: Tydliga felmeddelanden och visuell feedback
Formulär är en av React:s största styrkor - du kan skapa riktigt interaktiva upplevelser! 🚀
Konsumera API:er i React: Data från Backend
Modern React-applikationer separerar frontend från backend och kommunicerar via API:er (Application Programming Interfaces). Detta kapitel fokuserar på hur vi hämtar, skickar och hanterar data från externa tjänster.
Mål: Lära sig använda Fetch API, skapa custom hooks för API-anrop, implementera robust error handling och förstå bästa praxis för datahantering. (Notis: Axios är ett populärt bibliotek om du vill ha extra funktioner.)
Fetch API: Webbstandardens Sätt
Fetch API är den moderna standarden för att göra HTTP-requests i JavaScript. Det är inbyggt i alla moderna webbläsare och behöver inga externa bibliotek.
Grundläggande Fetch-exempel
import { useState, useEffect } from 'react';
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchUsers = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch('/api/users');
// Kontrollera om request lyckades
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const userData = await response.json();
setUsers(userData);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchUsers();
}, []);
if (loading) return <div>Laddar användare...</div>;
if (error) return <div>Fel: {error}</div>;
return (
<ul>
{users.map(user => (
<li key={user.id}>
{user.name} - {user.email}
</li>
))}
</ul>
);
}
HTTP-metoder med Fetch
// GET (standard)
const getUsers = async () => {
const response = await fetch('/api/users');
return response.json();
};
// POST - Skapa ny användare
const createUser = async (userData) => {
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(userData),
});
if (!response.ok) {
throw new Error('Failed to create user');
}
return response.json();
};
// PUT - Uppdatera användare
const updateUser = async (userId, userData) => {
const response = await fetch(`/api/users/${userId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(userData),
});
return response.json();
};
// DELETE - Ta bort användare
const deleteUser = async (userId) => {
const response = await fetch(`/api/users/${userId}`, {
method: 'DELETE',
});
if (!response.ok) {
throw new Error('Failed to delete user');
}
return response.ok;
};
Autentisering med Headers
// Token-baserad autentisering
const fetchWithAuth = async (url, options = {}) => {
const token = localStorage.getItem('authToken');
const config = {
...options,
headers: {
'Content-Type': 'application/json',
...(token && { Authorization: `Bearer ${token}` }),
...options.headers,
},
};
const response = await fetch(url, config);
// Hantera unauthorized
if (response.status === 401) {
localStorage.removeItem('authToken');
// Navigera till login via central hantering (ex. router)
throw new Error('Unauthorized');
}
return response;
};
// Användning
const getProtectedData = async () => {
const response = await fetchWithAuth('/api/protected-data');
return response.json();
};
Notis: Axios
Axios är ett populärt bibliotek för HTTP-anrop som erbjuder interceptors och några bekvämligheter. I denna kurs använder vi web standarden Fetch för alla exempel. Om du föredrar Axios kan du enkelt översätta våra fetch-anrop till axios.get/post/...
och använda interceptors för t.ex. token-hantering.
Använd data från Pokemon API
Nu när vi har sett grunderna för hur man anropar ett API låt oss göra något roligare. Pokemon API är ett öppet API som ger oss tillgång till data om alla Pokemon från spelen - perfekt för att öva på API-anrop!
Vad är Pokemon API?
Pokemon API (PokeAPI) är ett RESTful API som innehåller information om:
- Pokemon (namn, typ, statistik, bilder)
- Moves (attacker)
- Types (typer som Fire, Water, Electric)
- Items (föremål)
- Locations (platser)
Fördelar:
- Helt gratis att använda
- Ingen API-nyckel krävs
- Välstrukturerad JSON-data
- Stöder CORS (fungerar direkt från webbläsaren)
Undersök datan
Låt oss först utforska vad API:et ger oss. Öppna https://pokeapi.co/api/v2/pokemon/pikachu i webbläsaren eller testa med curl:
curl https://pokeapi.co/api/v2/pokemon/pikachu
Viktiga endpoints:
GET https://pokeapi.co/api/v2/pokemon/ # Lista första 20 Pokemon
GET https://pokeapi.co/api/v2/pokemon/{id} # Specifik Pokemon (ID eller namn)
GET https://pokeapi.co/api/v2/type/{type} # Pokemon av viss typ
Exempel på data för Pikachu:
{
"id": 25,
"name": "pikachu",
"height": 4,
"weight": 60,
"types": [
{
"slot": 1,
"type": {
"name": "electric",
"url": "https://pokeapi.co/api/v2/type/13/"
}
}
],
"sprites": {
"front_default": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/25.png",
"front_shiny": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/shiny/25.png"
},
"stats": [
{
"base_stat": 35,
"stat": {
"name": "hp"
}
}
// ... fler stats
]
}
Bygg en Pokemon-app steg för steg
Låt oss börja enkelt och bygga upp funktionaliteten bit för bit.
Steg 1: Hämta och visa en Pokemon
Först - låt oss bara hämta Pikachu och visa namnet:
function PokemonApp() {
const [pokemon, setPokemon] = useState(null);
useEffect(() => {
fetch('https://pokeapi.co/api/v2/pokemon/pikachu')
.then(response => response.json())
.then(data => setPokemon(data));
}, []);
return (
<div className="pokemon-app">
<h1>Min Pokemon App</h1>
{pokemon && <h2>{pokemon.name}</h2>}
</div>
);
}
Testa detta först! Öppna Network-fliken i utvecklarverktygen och se API-anropet.
Steg 2: Lägg till bild och grundinfo
function PokemonApp() {
const [pokemon, setPokemon] = useState(null);
useEffect(() => {
fetch('https://pokeapi.co/api/v2/pokemon/pikachu')
.then(response => response.json())
.then(data => setPokemon(data));
}, []);
return (
<div className="pokemon-app">
<h1>Min Pokemon App</h1>
{pokemon && (
<div className="pokemon-card">
<h2>{pokemon.name}</h2>
<img src={pokemon.sprites.front_default} alt={pokemon.name} />
<p>Höjd: {pokemon.height / 10} m</p>
<p>Vikt: {pokemon.weight / 10} kg</p>
</div>
)}
</div>
);
}
Steg 3: Lägg till sökfunktion
Nu gör vi det interaktivt:
function PokemonApp() {
const [pokemon, setPokemon] = useState(null);
const [searchTerm, setSearchTerm] = useState('');
const searchPokemon = () => {
fetch(`https://pokeapi.co/api/v2/pokemon/${searchTerm.toLowerCase()}`)
.then(response => response.json())
.then(data => setPokemon(data));
};
return (
<div className="pokemon-app">
<h1>Pokemon Sökare</h1>
<div className="search-box">
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Skriv Pokemon namn..."
className="search-input"
/>
<button onClick={searchPokemon} className="search-button">
Sök
</button>
</div>
{pokemon && (
<div className="pokemon-card">
<h2>{pokemon.name}</h2>
<img src={pokemon.sprites.front_default} alt={pokemon.name} />
<p>Höjd: {pokemon.height / 10} m</p>
<p>Vikt: {pokemon.weight / 10} kg</p>
<p>Typ: {pokemon.types.map(type => type.type.name).join(', ')}</p>
</div>
)}
</div>
);
}
Prova att söka på: pikachu, charizard, bulbasaur, squirtle
Steg 4: Hantera fel och loading
Vad händer om vi söker på något som inte finns?
function PokemonApp() {
const [pokemon, setPokemon] = useState(null);
const [searchTerm, setSearchTerm] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const searchPokemon = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(`https://pokeapi.co/api/v2/pokemon/${searchTerm.toLowerCase()}`);
if (!response.ok) {
throw new Error('Pokemon hittades inte!');
}
const data = await response.json();
setPokemon(data);
} catch (err) {
setError(err.message);
setPokemon(null);
}
setLoading(false);
};
return (
<div className="pokemon-app">
<h1>Pokemon Sökare</h1>
<div className="search-box">
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Skriv Pokemon namn..."
className="search-input"
/>
<button
onClick={searchPokemon}
disabled={loading}
className="search-button"
>
{loading ? 'Söker...' : 'Sök'}
</button>
</div>
{error && <div className="error">Fel: {error}</div>}
{pokemon && (
<div className="pokemon-card">
<h2>{pokemon.name}</h2>
<img src={pokemon.sprites.front_default} alt={pokemon.name} />
<p>Höjd: {pokemon.height / 10} m</p>
<p>Vikt: {pokemon.weight / 10} kg</p>
<p>Typ: {pokemon.types.map(type => type.type.name).join(', ')}</p>
</div>
)}
</div>
);
}
Lägg till CSS:
.search-box {
margin: 20px 0;
}
.search-input {
padding: 10px;
margin-right: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
.search-button {
padding: 10px 20px;
background-color: #007bff;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
.search-button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.error {
color: red;
margin: 10px 0;
}
Testa fel: Sök på "asdfgh" och se vad som händer!
Steg 5: Gör appen din egen! 🎯
Nu har du en fungerande Pokemon-sökare. Dags att experimentera och lägga till egna funktioner!
Enkla förbättringar att prova:
Random Pokemon-knapp:
// Lägg till i din searchPokemon-funktion
const getRandomPokemon = () => {
const randomId = Math.floor(Math.random() * 1010) + 1;
setSearchTerm(randomId.toString());
// Sedan kan du kalla searchPokemon() eller göra fetch direkt
};
// Lägg till knappen i din JSX
<button onClick={getRandomPokemon} className="random-button">
Slumpa Pokemon
</button>
Visa shiny-versionen:
// I din pokemon-card, lägg till:
{pokemon.sprites.front_shiny && (
<div>
<p>Shiny version:</p>
<img src={pokemon.sprites.front_shiny} alt={`Shiny ${pokemon.name}`} />
</div>
)}
Visa Pokemon stats:
// Lägg till i pokemon-card:
<div className="stats">
<h3>Stats:</h3>
{pokemon.stats.map(stat => (
<p key={stat.stat.name}>
{stat.stat.name}: {stat.base_stat}
</p>
))}
</div>
Medelsvåra utmaningar:
- Sök med Enter: Gör så man kan trycka Enter i sökrutan
- Favoriter: Spara favorit-Pokemon i
localStorage
- Historik: Visa de senaste sökta Pokemon
- Jämför Pokemon: Visa två Pokemon sida vid sida
Avancerade idéer:
- Pokemon Team Builder: Bygg ett lag med max 6 Pokemon
- Type effectiveness: Visa vilka typer som är starka/svaga mot varandra
- Evolution chain: Visa hela evolution-kedjan
- Moves/attacker: Lista Pokemon:s attacker
Tips för utveckling:
// Enter-tangent för sökning
const handleKeyPress = (e) => {
if (e.key === 'Enter') {
searchPokemon();
}
};
// Lägg till onKeyPress={handleKeyPress} på din input
// Spara i localStorage
const saveFavorite = () => {
const favorites = JSON.parse(localStorage.getItem('favorites') || '[]');
favorites.push(pokemon.name);
localStorage.setItem('favorites', JSON.stringify(favorites));
};
Experimentera och ha kul! Det viktigaste är att du förstår hur API-anrop fungerar. Allt annat är bonus! 🎮
Custom Hooks för API-anrop
Custom hooks gör API‑logik återanvändbar och ren. För en komplett genomgång med många exempel (useApi, useResource, caching, retry m.m.), se fördjupningslektionen: Custom Hooks i React.
Glimåkra Webbutvecklare - Kursbok
Deployment med Dokploy: Från Kod till Live-webbplats
Modern webbutveckling handlar inte bara om att skriva kod - den måste också köras någonstans där användare kan komma åt den. Dokploy är en open source-plattform som gör det enkelt att deploya (publicera) dina React-applikationer på internet.
Mål: Lära sig att använda Dokploy för att deploya React-applikationer, förstå deployment-processen, och hantera domäner och SSL-certifikat.
Vad är Dokploy?
Dokploy är en self-hosted deployment-plattform som fungerar som ett alternativ till Netlify, Vercel och Heroku. Den låter dig:
- 🚀 Deploya applikationer direkt från Git-repositories
- 🌐 Hantera domäner och SSL-certifikat automatiskt
- 📊 Övervaka dina applikationers prestanda
- 🔧 Konfigurera miljövariabler och databaser
För denna kurs använder vi Dokploy-instansen på hoster.glimnet.se.
Förberedelser
1. Kontrollera din React-applikation
Innan deployment, se till att din React-app fungerar lokalt:
# Installera dependencies
npm install
# Testa att appen startar
npm run dev
# Testa att appen kan byggas för produktion
npm run build
2. Pusha kod till GitHub
Din kod måste finnas i ett GitHub-repository för att Dokploy ska kunna komma åt den:
# Initiera git (om inte redan gjort)
git init
# Lägg till alla filer
git add .
# Committa ändringarna
git commit -m "Initial commit"
# Lägg till remote repository
git remote add origin https://github.com/ditt-användarnamn/ditt-repo.git
# Pusha till GitHub
git push -u origin main
Steg 1: Skapa Dokploy-konto
Registrering
- Ge lärare din email
- Gå till din email inbox och klicka på inbjudan
- Gå till https://hoster.glimnet.se
- Fyll i dina uppgifter:
- E-postadress
- Lösenord
- Bekräfta lösenord
Första inloggningen
- Logga in med dina nya uppgifter
- Du kommer att se Dokploy dashboard
Steg 2: Skapa ett nytt projekt
Projektinställningar
- Klicka på "New Project" eller "Nytt projekt"
- Ge projektet ett beskrivande namn (t.ex. "Min React App")
- Välj projekttyp: Application
Steg 3: Skapa en ny Service
Projektinställningar
- Klicka på "New Service" eller "Ny Service"
- Ge projektet ett beskrivande namn (t.ex. "Min React App")
- Välj projekttyp: Application
Anslut GitHub Repository
- Välj Git som källa
- Ange URL till ditt GitHub-repository:
https://github.com/ditt-användarnamn/ditt-repo.git
- Välj branch (vanligtvis
main
ellermaster
) - Spara
Steg 3: Konfigurera Build-inställningar
Deployment Type
Dokploy kommer automatiskt att detektera att det är en React/Vite-applikation, men du kan behöva justera inställningarna:
Build Type: Välj Railpack (rekommenderat för React/Vite)
Avancerade inställningar (vid behov)
Om din applikation kräver specifika inställningar, kan du skapa konfigurationsfiler:
package.json
- Se till att du har rätt scripts:
{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
}
Steg 4: Domän och SSL-konfiguration
Konfigurera domän
- Gå till Domains-fliken i ditt projekt
- Klicka på "Add Domain"
- Fyll i domäninställningar:
- Host:
ditt-projektnamn.hoster.glimnet.se
- Path:
/
(root) - Internal Path:
/
(viktigt!) - Container Port:
80
- Host:
SSL-certifikat
- Aktivera HTTPS
- Välj Let's Encrypt som Certificate Provider
- Systemet kommer automatiskt att skaffa och installera SSL-certifikat
Steg 5: Deploya applikationen
Starta deployment
- Gå tillbaka till General-fliken
- Klicka på "Deploy"-knappen
- Vänta medan Dokploy:
- Klonar din kod från GitHub
- Installerar dependencies (
npm install
) - Bygger applikationen (
npm run build
) - Startar containern
Övervaka deployment
- Gå till Deployments-fliken för att se status
- Klicka på den senaste deploymenten för att se loggar
- Vänta tills status visar "✅ Done"
Steg 6: Testa din live-webbplats
Första testet
-
När deploymenten är klar, klicka på domänlänken eller gå till:
https://ditt-projektnamn.hoster.glimnet.se
-
Din React-applikation ska nu vara live på internet! 🎉
Felsökning vid problem
Om webbplatsen inte fungerar, kontrollera:
- 404-fel: Kontrollera att Internal Path är satt till
/
- 502 Bad Gateway: Kontrollera att Container Port är
80
- Build-fel: Kontrollera deployment logs för felmeddelanden
Steg 7: Uppdatera applikationen
Automatisk deployment
Dokploy kan konfigureras för automatisk deployment vid kod-ändringar:
- Gå till Settings → Git
- Aktivera Auto Deploy
- Välj branch att övervaka (t.ex.
main
)
Nu kommer Dokploy automatiskt att deploya när du pushar ändringar till GitHub!
Manuell deployment
För manuell uppdatering:
-
Pusha dina ändringar till GitHub:
git add . git commit -m "Uppdatering av applikation" git push
-
Gå till Dokploy dashboard
-
Klicka "Deploy" igen
Avancerade funktioner
Miljövariabler
För att lägga till miljövariabler (t.ex. API-nycklar):
- Gå till Environment-fliken
- Lägg till variabler:
VITE_API_URL=https://api.example.com VITE_APP_NAME=Min App
[PRINTSCREEN: Environment variables]
Custom domän
För att använda en subdomän på hoster.glimnet.se (t.ex. min-app.hoster.glimnet.se
):
- Gå till "Domains"-fliken i Dokploy
- Klicka på "Add Domain"
- Ange önskad subdomän i formatet
[namn].hoster.glimnet.se
- SSL-certifikat genereras automatiskt
Detta ger dig en säker HTTPS-anslutning utan extra konfiguration.
Monitoring och loggar
Övervaka din applikation:
- Logs-fliken: Se applikationsloggar
- Monitoring-fliken: Prestanda och resursanvändning
- Deployments-fliken: Historik över deployments
Vanliga problem och lösningar
Problem: "Build failed"
Orsak: Fel i koden eller missing dependencies
Lösning:
- Kontrollera deployment logs
- Testa
npm run build
lokalt - Fixa eventuella fel och pusha igen
Problem: "404 Not Found"
Orsak: Fel Internal Path eller routing-problem
Lösning:
- Kontrollera att Internal Path är
/
- Kontrollera att Container Port är
80
- För React Router: Lägg till
try_files
konfiguration
Problem: "502 Bad Gateway"
Orsak: Applikationen startar inte korrekt
Lösning:
- Kontrollera Container Port (ska vara
80
för Railpack) - Kontrollera applikationsloggar
- Testa att appen fungerar lokalt
Säkerhet och best practices
1. Miljövariabler för känslig data
# ❌ Lägg aldrig känslig data direkt i koden
const API_KEY = "abc123secret";
# ✅ Använd miljövariabler
const API_KEY = import.meta.env.VITE_API_KEY;
2. HTTPS överallt
- Använd alltid HTTPS för produktion
- Dokploy hanterar SSL-certifikat automatiskt
- Omdirigera HTTP till HTTPS
3. Regelbundna uppdateringar
- Håll dependencies uppdaterade
- Testa ändringar lokalt innan deployment
- Använd versionshantering (Git tags) för viktiga releases
Sammanfattning
Deployment med Dokploy gör det enkelt att få din React-applikation live på internet:
- Förbered din kod och pusha till GitHub
- Skapa Dokploy-konto och projekt
- Konfigurera build-inställningar och domän
- Deploya och övervaka processen
- Testa din live-webbplats
- Uppdatera genom att pusha ändringar
Med Dokploy kan du fokusera på att utveckla fantastiska applikationer medan plattformen hanterar infrastrukturen åt dig!
Nästa steg
Nu när du kan deploya React-applikationer, utforska:
- Databaser: Lägg till PostgreSQL eller MongoDB
- API:er: Bygg och deploya backend-tjänster
- Monitoring: Sätt upp aviseringar för driftstörningar
- CI/CD: Automatisera testing och deployment
Grattis! Du har nu deployat din första React-applikation! 🚀
Behöver du hjälp? Kontakta lärare eller kolla in Dokploy dokumentation för mer avancerade funktioner.
Single Page Application: Koncept och struktur
En Single Page Application (SPA) laddar appen en gång och uppdaterar sedan innehållet i webbläsaren vid navigering. Här fokuserar vi på koncept, trade‑offs och enkel struktur.
SPA vs Traditionella Webbapplikationer
graph TB subgraph "Traditional Multi-Page App" A1[Browser] -->|"Request page1.html"| B1[Server] B1 -->|"Full HTML page"| A1 A1 -->|"Request page2.html"| B1 B1 -->|"Full HTML page"| A1 A1 -->|"Request page3.html"| B1 B1 -->|"Full HTML page"| A1 end subgraph "Single Page Application" A2[Browser] -->|"Initial request"| B2[Server] B2 -->|"HTML + JS Bundle"| A2 A2 -->|"API request"| B2 B2 -->|"JSON data"| A2 A2 -.->|"Client-side routing"| A2 end style B1 fill:#ffa07a style A2 fill:#98fb98 style B2 fill:#87ceeb
Fördelar med SPA (varför)
Användarupplevelse:
- Snabbare navigering efter initial laddning
- Smidigare övergångar mellan vyer
- Möjlighet för offline-funktionalitet
- App-liknande känsla
Utveckling:
- Tydlig separation mellan frontend och backend
- Återanvändbar API för flera klienter
- Modern utvecklingsworkflow med hot reloading
Nackdelar med SPA (varför inte alltid)
Initial Prestanda:
- Större initial JavaScript-bundle
- Längre tid till första visning
- Komplicerad state management
SEO och Tillgänglighet:
- Kräver Server-Side Rendering (SSR) för SEO
- Komplexare routing-implementation
- Potential för minnesläckor
Enkel struktur för en liten SPA
Håll mappstrukturen enkel i början:
src/
components/ # Återanvändbara UI-byggstenar
pages/ # Sidor för routing
services/ # API-anrop (fetch)
hooks/ # Egen logik (valfritt)
App.jsx # Router och layout
main.jsx # Entrypoint
Layout-komponent med Navigation
// components/Layout.js
import { useState } from 'react';
import { Link, useLocation, useNavigate } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
import { useTheme } from '../contexts/ThemeContext';
function Layout({ children }) {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const { user, logout } = useAuth();
const { theme, toggleTheme } = useTheme();
const location = useLocation();
const navigate = useNavigate();
const handleLogout = async () => {
await logout();
navigate('/');
setMobileMenuOpen(false);
};
const isActive = (path) => location.pathname === path;
return (
<div className={`app ${theme}`}>
<header className="header">
<div className="container">
<Link to="/" className="logo">
<h1>Min SPA</h1>
</Link>
{/* Desktop Navigation */}
<nav className="desktop-nav">
<Link
to="/"
className={isActive('/') ? 'active' : ''}
>
Hem
</Link>
<Link
to="/products"
className={isActive('/products') ? 'active' : ''}
>
Produkter
</Link>
<Link
to="/about"
className={isActive('/about') ? 'active' : ''}
>
Om oss
</Link>
</nav>
{/* User Actions */}
<div className="user-actions">
<button onClick={toggleTheme} className="theme-toggle">
{theme === 'light' ? '🌙' : '☀️'}
</button>
{user ? (
<div className="user-menu">
<Link to="/dashboard">Dashboard</Link>
<button onClick={handleLogout}>Logga ut</button>
</div>
) : (
<Link to="/login" className="login-btn">
Logga in
</Link>
)}
{/* Mobile Menu Toggle */}
<button
className="mobile-menu-toggle"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
>
☰
</button>
</div>
</div>
{/* Mobile Navigation */}
{mobileMenuOpen && (
<nav className="mobile-nav">
<Link to="/" onClick={() => setMobileMenuOpen(false)}>Hem</Link>
<Link to="/products" onClick={() => setMobileMenuOpen(false)}>Produkter</Link>
<Link to="/about" onClick={() => setMobileMenuOpen(false)}>Om oss</Link>
{user && (
<Link to="/dashboard" onClick={() => setMobileMenuOpen(false)}>
Dashboard
</Link>
)}
</nav>
)}
</header>
<main className="main">
{children}
</main>
<footer className="footer">
<div className="container">
<p>© 2024 Min SPA. Alla rättigheter förbehållna.</p>
</div>
</footer>
</div>
);
}
export default Layout;
Context API för Global State
// contexts/AppContext.js - Kombinerad state management
import { createContext, useContext, useReducer, useEffect } from 'react';
const AppContext = createContext();
// Action types
const ACTIONS = {
SET_LOADING: 'SET_LOADING',
SET_ERROR: 'SET_ERROR',
CLEAR_ERROR: 'CLEAR_ERROR',
SET_USER: 'SET_USER',
SET_PRODUCTS: 'SET_PRODUCTS',
ADD_TO_CART: 'ADD_TO_CART',
REMOVE_FROM_CART: 'REMOVE_FROM_CART',
CLEAR_CART: 'CLEAR_CART',
SET_THEME: 'SET_THEME'
};
// Initial state
const initialState = {
loading: false,
error: null,
user: null,
products: [],
cart: [],
theme: 'light'
};
// Reducer function
function appReducer(state, action) {
switch (action.type) {
case ACTIONS.SET_LOADING:
return { ...state, loading: action.payload };
case ACTIONS.SET_ERROR:
return { ...state, error: action.payload, loading: false };
case ACTIONS.CLEAR_ERROR:
return { ...state, error: null };
case ACTIONS.SET_USER:
return { ...state, user: action.payload };
case ACTIONS.SET_PRODUCTS:
return { ...state, products: action.payload };
case ACTIONS.ADD_TO_CART:
const existingItem = state.cart.find(item => item.id === action.payload.id);
if (existingItem) {
return {
...state,
cart: state.cart.map(item =>
item.id === action.payload.id
? { ...item, quantity: item.quantity + 1 }
: item
)
};
}
return {
...state,
cart: [...state.cart, { ...action.payload, quantity: 1 }]
};
case ACTIONS.REMOVE_FROM_CART:
return {
...state,
cart: state.cart.filter(item => item.id !== action.payload)
};
case ACTIONS.CLEAR_CART:
return { ...state, cart: [] };
case ACTIONS.SET_THEME:
return { ...state, theme: action.payload };
default:
return state;
}
}
// Provider component
export function AppProvider({ children }) {
const [state, dispatch] = useReducer(appReducer, initialState);
// Load initial data
useEffect(() => {
const loadInitialData = async () => {
try {
dispatch({ type: ACTIONS.SET_LOADING, payload: true });
// Load user from localStorage
const savedUser = localStorage.getItem('user');
if (savedUser) {
dispatch({ type: ACTIONS.SET_USER, payload: JSON.parse(savedUser) });
}
// Load theme from localStorage
const savedTheme = localStorage.getItem('theme') || 'light';
dispatch({ type: ACTIONS.SET_THEME, payload: savedTheme });
// Load products
const response = await fetch('/api/products');
const products = await response.json();
dispatch({ type: ACTIONS.SET_PRODUCTS, payload: products });
} catch (error) {
dispatch({ type: ACTIONS.SET_ERROR, payload: error.message });
} finally {
dispatch({ type: ACTIONS.SET_LOADING, payload: false });
}
};
loadInitialData();
}, []);
// Save user to localStorage when it changes
useEffect(() => {
if (state.user) {
localStorage.setItem('user', JSON.stringify(state.user));
} else {
localStorage.removeItem('user');
}
}, [state.user]);
// Save theme to localStorage when it changes
useEffect(() => {
localStorage.setItem('theme', state.theme);
document.documentElement.setAttribute('data-theme', state.theme);
}, [state.theme]);
// Action creators
const actions = {
setLoading: (loading) => dispatch({ type: ACTIONS.SET_LOADING, payload: loading }),
setError: (error) => dispatch({ type: ACTIONS.SET_ERROR, payload: error }),
clearError: () => dispatch({ type: ACTIONS.CLEAR_ERROR }),
setUser: (user) => dispatch({ type: ACTIONS.SET_USER, payload: user }),
addToCart: (product) => dispatch({ type: ACTIONS.ADD_TO_CART, payload: product }),
removeFromCart: (productId) => dispatch({ type: ACTIONS.REMOVE_FROM_CART, payload: productId }),
clearCart: () => dispatch({ type: ACTIONS.CLEAR_CART }),
toggleTheme: () => {
const newTheme = state.theme === 'light' ? 'dark' : 'light';
dispatch({ type: ACTIONS.SET_THEME, payload: newTheme });
}
};
return (
<AppContext.Provider value={{ state, actions }}>
{children}
</AppContext.Provider>
);
}
// Custom hook
export function useApp() {
const context = useContext(AppContext);
if (!context) {
throw new Error('useApp must be used within AppProvider');
}
return context;
}
Code Splitting och Lazy Loading
// Lazy load components
import { lazy, Suspense } from 'react';
import LoadingSpinner from './components/LoadingSpinner';
// Lazy loaded pages
const Dashboard = lazy(() => import('./pages/Dashboard'));
const AdminPanel = lazy(() => import('./pages/AdminPanel'));
const Reports = lazy(() => import('./pages/Reports'));
function App() {
return (
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route
path="/dashboard"
element={
<Suspense fallback={<LoadingSpinner />}>
<Dashboard />
</Suspense>
}
/>
<Route
path="/admin"
element={
<Suspense fallback={<LoadingSpinner />}>
<AdminPanel />
</Suspense>
}
/>
<Route
path="/reports"
element={
<Suspense fallback={<LoadingSpinner />}>
<Reports />
</Suspense>
}
/>
</Routes>
</Router>
);
}
Memoization och Optimization
import { memo, useMemo, useCallback } from 'react';
// Memoized list component
const ProductList = memo(function ProductList({ products, onAddToCart }) {
const sortedProducts = useMemo(() => {
// Undvik att mutera original-arrayen
return [...products].sort((a, b) => a.name.localeCompare(b.name));
}, [products]);
return (
<div className="product-grid">
{sortedProducts.map(product => (
<ProductCard
key={product.id}
product={product}
onAddToCart={onAddToCart}
/>
))}
</div>
);
});
// Memoized product card
const ProductCard = memo(function ProductCard({ product, onAddToCart }) {
const handleAddToCart = useCallback(() => {
onAddToCart(product);
}, [product, onAddToCart]);
return (
<div className="product-card">
<img src={product.image} alt={product.name} loading="lazy" />
<h3>{product.name}</h3>
<p>{product.price} kr</p>
<button onClick={handleAddToCart}>
Lägg i kundvagn
</button>
</div>
);
});
Virtual Scrolling för Stora Listor
import { FixedSizeList as List } from 'react-window';
function VirtualizedProductList({ products }) {
const Row = ({ index, style }) => {
const product = products[index];
return (
<div style={style} className="virtual-list-item">
<ProductCard product={product} />
</div>
);
};
return (
<List
height={600}
itemCount={products.length}
itemSize={200}
className="virtual-list"
>
{Row}
</List>
);
}
Build och deployment (översikt)
Optimering för Produktion
# Build för produktion
npm run build
# Analysera bundle size
npm install --save-dev webpack-bundle-analyzer
npx webpack-bundle-analyzer build/static/js/*.js
Miljövariabler
# .env.local
REACT_APP_API_URL=https://api.example.com
REACT_APP_GOOGLE_ANALYTICS_ID=GA_TRACKING_ID
REACT_APP_VERSION=$npm_package_version
// config/env.js
const config = {
apiUrl: process.env.REACT_APP_API_URL || 'http://localhost:3001',
googleAnalyticsId: process.env.REACT_APP_GOOGLE_ANALYTICS_ID,
version: process.env.REACT_APP_VERSION || '1.0.0',
isDevelopment: process.env.NODE_ENV === 'development',
isProduction: process.env.NODE_ENV === 'production'
};
export default config;
Vite miljövariabler
# .env
VITE_API_URL=https://api.example.com
VITE_GOOGLE_ANALYTICS_ID=GA_TRACKING_ID
// src/config/env.js (Vite)
const config = {
apiUrl: import.meta.env.VITE_API_URL || 'http://localhost:3001',
googleAnalyticsId: import.meta.env.VITE_GOOGLE_ANALYTICS_ID,
isDevelopment: import.meta.env.DEV,
isProduction: import.meta.env.PROD
};
export default config;
När SSR/SSG?
- SEO-kritiska sidor, snabb First Contentful Paint, delning av länkar med preview.
- Överväg Next.js för Server-Side Rendering (SSR) eller Static Site Generation (SSG) när det passar behovet.
// public/sw.js
const CACHE_NAME = 'spa-cache-v1';
const urlsToCache = [
'/',
'/static/js/bundle.js',
'/static/css/main.css',
'/manifest.json'
];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => cache.addAll(urlsToCache))
);
});
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then((response) => {
// Return cached version or fetch from network
return response || fetch(event.request);
})
);
});
Deployment till olika plattformar
# Netlify
npm run build
# Dra och släpp build-mappen till Netlify
# Vercel
npm install -g vercel
vercel --prod
# GitHub Pages
npm install --save-dev gh-pages
# Lägg till i package.json:
# "homepage": "https://username.github.io/repository-name",
# "predeploy": "npm run build",
# "deploy": "gh-pages -d build"
npm run deploy
Mikro‑övning
Bygg en liten SPA med tre sidor (Hem, Om, Kontakt) och en sida som hämtar data (Produkter).
- Navigera mellan sidor med React Router.
- På Produkter: hämta en lista via Fetch och visa
loading
/error
/tom‑state. - Lägg till en detaljsida med
:id
.
Klar‑kriterier:
- Ingen sidladdning vid navigering.
- Tydliga laddnings‑ och felmeddelanden.
- Detaljsidan visar id från URL:en.
Error Tracking
// utils/errorTracking.js
class ErrorTracker {
static init() {
if (process.env.NODE_ENV === 'production') {
window.addEventListener('error', this.handleError);
window.addEventListener('unhandledrejection', this.handlePromiseRejection);
}
}
static handleError(event) {
console.error('Global error:', event.error);
// Skicka till error tracking service
this.sendToService({
type: 'javascript_error',
message: event.error.message,
stack: event.error.stack,
url: window.location.href,
userAgent: navigator.userAgent
});
}
static handlePromiseRejection(event) {
console.error('Unhandled promise rejection:', event.reason);
this.sendToService({
type: 'promise_rejection',
message: event.reason.toString(),
url: window.location.href
});
}
static sendToService(errorData) {
fetch('/api/errors', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(errorData)
}).catch(err => console.error('Failed to send error:', err));
}
}
export default ErrorTracker;
Sammanfattning
Single Page Applications erbjuder många fördelar för moderna webbapplikationer:
- Smidig användarupplevelse med snabb navigering
- Tydlig separation mellan frontend och backend
- Modern arkitektur som skalrar bra
- Rich interaktivitet som desktop-applikationer
Viktiga aspekter att tänka på:
- Performance optimization med code splitting
- Proper state management för komplex data
- Build-optimering för produktionsmiljö
- Error tracking och monitoring
- SEO-considerations för offentliga applikationer
I nästa avsnitt lär vi oss implementera routing med React Router för navigation.
Praktiska Övningar och Projekt: Bygg Kompletta React-applikationer
Nu när vi har lärt oss React-fundamenten är det dags att sätta samman allt i praktiska projekt. Dessa övningar bygger gradvis från enkla komponenter till kompletta applikationer.
Mål: Tillämpa React-kunskaper i verkliga projekt, integrera API:er, hantera state management och skapa användbara applikationer.
Övning 1: Todo-lista med useState
- Bygg en enkel todo-app med möjligheter att lägga till, toggla och ta bort todos.
- Visa antal kvarvarande todos och filtrering (alla/aktiva/klara).
Klar-kriterier:
- Todo-listan överlever sidladdning.
- Varje todo har stabil
key
(t.ex. id). - Inga muterande operationer på state-arrayer (använd spread operator ... ).
Övning 2: Konsumera publikt API + Routing
- Hämta data från ett publikt API (t.ex. SWAPI eller PokéAPI).
- Visa lista och detaljsida för ett item via React Router (v6), inkl.
useParams
. - Hantera
loading
/error
-states och tomma resultat.
Klar-kriterier:
- Lista och detaljvy fungerar via klient-routing (ingen helsidladdning).
- Sök/filter uppdaterar URL query params med
useSearchParams
.
Övning 3: Formulär med validering och API
- Bygg ett formulär (t.ex. registrering eller produktform) med kontrollerade inputs.
- Validera fält vid blur och submit; visa felmeddelanden.
- Skicka data till ett mock-API (json-server eller liknande) och hantera svar/fel.
- Bonus: Använd
react-hook-form
för att jämföra ergonomi/prestanda.
Klar-kriterier:
- Validering hindrar felaktiga submissioner och visar användarvänliga fel.
- Efter lyckad submit nollställs formuläret eller navigera till en bekräftelsesida.
Övning 4: Global state med Context
- Implementera tema-växling (ljus/mörk) med Context + persistens i
localStorage
. - Applicera temat på hela appen via CSS-klass eller
data-theme
påhtml
-elementet.
Klar-kriterier:
- Temat sparas och läses in vid start.
- Ingen props drilling för temahantering.
Övning 5: Prestandaoptimering och stora listor
- Rendera en lista med 1 000+ items.
- Optimera med
React.memo
,useMemo
,useCallback
där det ger effekt. - Implementera virtualisering med
react-window
och jämför FPS/scroll-känsla.
Klar-kriterier:
- Mätbar förbättring när virtualisering används.
- Inga onödiga re-renders av listitems (verifiera med React DevTools Profiler).
Checklista (Definition of Done) för kapitel:
- API-anrop hanterar
loading
/error
och cleanup (AbortController). - Routing-exempel följer v6 (relativa paths,
NavLink
utanactiveClassName
). - Inga state-mutationer i exempel (kopior vid sortering/uppdatering).
- ESLint
react-hooks/exhaustive-deps
aktiverat i projektet. - Tillgänglighet: formulär har etiketter, fokus är synligt, och knappar har beskrivande text.
Teknisk Intervju: Frontend-ramverk med React
Detta avsnitt innehåller vanliga tekniska intervjufrågor för React-utveckling. Frågorna täcker React fundamentals, komponenter, hooks, state management, routing och prestanda.
Fråga 1: React Grundläggande Koncept
Intervjuare: "Förklara vad React är och vad Virtual DOM innebär. Visa skillnaden mellan JSX och vanlig HTML."
Bra svar:
// React är ett JavaScript-bibliotek för att bygga användargränssnitt
// Virtual DOM är en representation av den riktiga DOM:en i minnet
// JSX - JavaScript XML (React syntax)
function Welcome({ name }) {
return (
<div className="greeting">
<h1>Hej {name}!</h1>
<p>Dagens datum: {new Date().toLocaleDateString()}</p>
</div>
);
}
// Motsvarande HTML skulle vara statisk:
// <div class="greeting">
// <h1>Hej Anna!</h1>
// <p>Dagens datum: 2024-01-15</p>
// </div>
// Skillnader JSX vs HTML:
// - className istället för class
// - {} för JavaScript-uttryck
// - camelCase för attribut (onClick, onChange)
// - Måste ha en parent element eller React Fragment
Förklaring: Virtual DOM gör att React kan jämföra förändringar effektivt och bara uppdatera de delar av DOM:en som ändrats. JSX tillåter JavaScript-uttryck inbäddade i markup.
Fråga 2: Funktionella vs Class Components
Intervjuare: "Vad är skillnaden mellan funktionella och class components? Varför föredras funktionella komponenter idag?"
Bra svar:
// Class Component (äldre sätt)
class ClassCounter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
componentDidMount() {
console.log('Component mounted');
}
incrementCount = () => {
this.setState({ count: this.state.count + 1 });
}
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.incrementCount}>Increment</button>
</div>
);
}
}
// Functional Component med hooks (modernt sätt)
function FunctionalCounter() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('Component mounted/updated');
}, []);
const incrementCount = () => {
setCount(count + 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={incrementCount}>Increment</button>
</div>
);
}
Fördelar med funktionella komponenter:
- Enklare syntax och mindre boilerplate-kod
- Bättre prestanda med hooks
- Lättare att testa och förstå
- Modern React development standard
Fråga 3: useState och useEffect Hooks
Intervjuare: "Förklara hur useState och useEffect fungerar. Visa ett exempel med API-anrop."
Bra svar:
import { useState, useEffect } from 'react';
function UserProfile({ userId }) {
// useState för state management
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// useEffect för side effects (API calls, subscriptions etc)
useEffect(() => {
const fetchUser = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error('User not found');
}
const userData = await response.json();
setUser(userData);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchUser();
}, [userId]); // Dependency array - kör när userId ändras
// Cleanup effect (körs när component unmounts)
useEffect(() => {
return () => {
console.log('Cleanup when component unmounts');
};
}, []);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!user) return <div>No user found</div>;
return (
<div>
<h2>{user.name}</h2>
<p>Email: {user.email}</p>
</div>
);
}
useEffect dependency arrays:
[]
- körs bara en gång (componentDidMount)[userId]
- körs när userId ändras- Ingen array - körs efter varje render
Fråga 4: Props och State Management
Intervjuare: "Förklara skillnaden mellan props och state. Visa hur man hanterar props drilling och Context API."
Bra svar:
// Props - data som skickas från parent till child
function Parent() {
const [theme, setTheme] = useState('light');
return (
<Child
theme={theme}
onThemeChange={setTheme}
title="My App"
/>
);
}
function Child({ theme, onThemeChange, title }) {
return (
<div className={`app ${theme}`}>
<h1>{title}</h1>
<button onClick={() => onThemeChange(theme === 'light' ? 'dark' : 'light')}>
Toggle Theme
</button>
</div>
);
}
// Context API för att undvika props drilling
const ThemeContext = createContext();
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
// Använda context
function ThemedButton() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<button
className={`btn btn-${theme}`}
onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
>
Current theme: {theme}
</button>
);
}
// App med Context Provider
function App() {
return (
<ThemeProvider>
<Header />
<Main />
<ThemedButton />
</ThemeProvider>
);
}
State vs Props:
- State: Intern data som komponenten äger och kan ändra
- Props: Data som skickas från föräldrakomponent, read-only
Fråga 5: Event Handling och Forms
Intervjuare: "Visa hur man hanterar formulär och events i React på ett kontrollerat sätt."
Bra svar:
function ContactForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
message: '',
subscribe: false
});
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
// Hantera input changes
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setFormData(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value
}));
// Rensa fel när användaren börjar skriva
if (errors[name]) {
setErrors(prev => ({
...prev,
[name]: ''
}));
}
};
// Validering
const validateForm = () => {
const newErrors = {};
if (!formData.name.trim()) {
newErrors.name = 'Namn är obligatoriskt';
}
if (!formData.email.trim()) {
newErrors.email = 'E-post är obligatoriskt';
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = 'Ogiltig e-postadress';
}
if (!formData.message.trim()) {
newErrors.message = 'Meddelande är obligatoriskt';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
// Hantera form submission
const handleSubmit = async (e) => {
e.preventDefault();
if (!validateForm()) return;
setIsSubmitting(true);
try {
const response = await fetch('/api/contact', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData),
});
if (response.ok) {
alert('Meddelande skickat!');
setFormData({ name: '', email: '', message: '', subscribe: false });
} else {
throw new Error('Något gick fel');
}
} catch (error) {
alert('Fel vid sändning: ' + error.message);
} finally {
setIsSubmitting(false);
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="name">Namn:</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
className={errors.name ? 'error' : ''}
/>
{errors.name && <span className="error-text">{errors.name}</span>}
</div>
<div>
<label htmlFor="email">E-post:</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
className={errors.email ? 'error' : ''}
/>
{errors.email && <span className="error-text">{errors.email}</span>}
</div>
<div>
<label htmlFor="message">Meddelande:</label>
<textarea
id="message"
name="message"
value={formData.message}
onChange={handleChange}
className={errors.message ? 'error' : ''}
/>
{errors.message && <span className="error-text">{errors.message}</span>}
</div>
<div>
<label>
<input
type="checkbox"
name="subscribe"
checked={formData.subscribe}
onChange={handleChange}
/>
Prenumerera på nyhetsbrev
</label>
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Skickar...' : 'Skicka'}
</button>
</form>
);
}
Viktiga principer för formulär i React:
- Kontrollerade komponenter (controlled components)
- Hantera all state i React
- Validering både på input och submit
- Förhindra default form behavior med
preventDefault()
Fråga 6: Lists och Keys
Intervjuare: "Förklara varför keys är viktiga i React lists och visa best practices."
Bra svar:
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Handla mat', completed: false },
{ id: 2, text: 'Träna', completed: true },
{ id: 3, text: 'Studera React', completed: false }
]);
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
};
const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
return (
<ul>
{todos.map(todo => (
<TodoItem
key={todo.id} // ✅ Använd stabil, unik key
todo={todo}
onToggle={toggleTodo}
onDelete={deleteTodo}
/>
))}
</ul>
);
}
function TodoItem({ todo, onToggle, onDelete }) {
return (
<li className={todo.completed ? 'completed' : ''}>
<span onClick={() => onToggle(todo.id)}>
{todo.text}
</span>
<button onClick={() => onDelete(todo.id)}>
Ta bort
</button>
</li>
);
}
// ❌ Dåliga exempel på keys:
// key={index} - problem vid omordning eller radering
// key={Math.random()} - skapar nya keys vid varje render
// key={todo.text} - problem om text ändras eller är duplicerad
// ✅ Bra keys:
// key={todo.id} - stabil unik identifierare
// key={`${todo.userId}-${todo.timestamp}`} - sammansatt unik key
Varför keys är viktiga:
- React använder keys för att identifiera vilka element som ändrats
- Förbättrar prestanda vid re-rendering
- Förhindrar buggar med component state
Fråga 7: React Router Navigation
Intervjuare: "Implementera routing med React Router inklusive protected routes."
Bra svar:
import { BrowserRouter, Routes, Route, Navigate, Link, useNavigate } from 'react-router-dom';
// Protected Route Component
function ProtectedRoute({ children }) {
const isAuthenticated = useAuth(); // Custom hook för auth
return isAuthenticated ? children : <Navigate to="/login" replace />;
}
// Navigation Component
function Navigation() {
const { user, logout } = useAuth();
const navigate = useNavigate();
const handleLogout = () => {
logout();
navigate('/');
};
return (
<nav>
<Link to="/">Hem</Link>
<Link to="/about">Om oss</Link>
{user ? (
<>
<Link to="/dashboard">Dashboard</Link>
<Link to="/profile">Profil</Link>
<button onClick={handleLogout}>Logga ut</button>
</>
) : (
<>
<Link to="/login">Logga in</Link>
<Link to="/register">Registrera</Link>
</>
)}
</nav>
);
}
// Main App with Router
function App() {
return (
<AuthProvider>
<BrowserRouter>
<Navigation />
<Routes>
{/* Public routes */}
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
{/* Protected routes */}
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
<Route
path="/profile"
element={
<ProtectedRoute>
<Profile />
</ProtectedRoute>
}
/>
{/* Dynamic routes */}
<Route path="/users/:id" element={<UserProfile />} />
{/* 404 */}
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
</AuthProvider>
);
}
// Component med URL parameters
function UserProfile() {
const { id } = useParams();
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(id).then(setUser);
}, [id]);
return user ? <div>{user.name}</div> : <div>Loading...</div>;
}
React Router hooks:
useNavigate()
- programmatisk navigationuseParams()
- hämta URL-parametraruseLocation()
- få information om current location
Fråga 8: Custom Hooks och API Integration
Intervjuare: "Skapa en custom hook för API-anrop med loading och error states."
Bra svar:
// Custom hook för API calls
function useApi(url, options = {}) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const refetch = useCallback(async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(url, {
headers: {
'Content-Type': 'application/json',
...options.headers,
},
...options,
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}, [url, options]);
useEffect(() => {
refetch();
}, [refetch]);
return { data, loading, error, refetch };
}
// Custom hook för forms
function useForm(initialValues, validationRules = {}) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const handleChange = (name, value) => {
setValues(prev => ({ ...prev, [name]: value }));
// Validera när användaren ändrar värde
if (touched[name] && validationRules[name]) {
const error = validationRules[name](value);
setErrors(prev => ({ ...prev, [name]: error }));
}
};
const handleBlur = (name) => {
setTouched(prev => ({ ...prev, [name]: true }));
// Validera när fält lämnas
if (validationRules[name]) {
const error = validationRules[name](values[name]);
setErrors(prev => ({ ...prev, [name]: error }));
}
};
const isValid = Object.values(errors).every(error => !error);
return {
values,
errors,
touched,
handleChange,
handleBlur,
isValid,
setValues,
setErrors
};
}
// Användning av custom hooks
function UsersList() {
const { data: users, loading, error, refetch } = useApi('/api/users');
const {
values,
errors,
handleChange,
handleBlur,
isValid
} = useForm(
{ name: '', email: '' },
{
name: (value) => !value ? 'Namn krävs' : '',
email: (value) => !/\S+@\S+\.\S+/.test(value) ? 'Ogiltig e-post' : ''
}
);
if (loading) return <div>Loading users...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
<h2>Users</h2>
<button onClick={refetch}>Refresh</button>
<ul>
{users?.map(user => (
<li key={user.id}>{user.name} - {user.email}</li>
))}
</ul>
<form>
<input
type="text"
placeholder="Name"
value={values.name}
onChange={(e) => handleChange('name', e.target.value)}
onBlur={() => handleBlur('name')}
/>
{errors.name && <span>{errors.name}</span>}
<input
type="email"
placeholder="Email"
value={values.email}
onChange={(e) => handleChange('email', e.target.value)}
onBlur={() => handleBlur('email')}
/>
{errors.email && <span>{errors.email}</span>}
<button disabled={!isValid}>Add User</button>
</form>
</div>
);
}
Fördelar med custom hooks:
- Återanvändbar logik mellan komponenter
- Separation of concerns
- Lättare testning
- Renare komponentkod
Fråga 9: Performance Optimization
Intervjuare: "Vilka tekniker använder du för att optimera React-applikationer?"
Bra svar:
import { memo, useMemo, useCallback, lazy, Suspense } from 'react';
// 1. React.memo för att förhindra onödiga re-renders
const ExpensiveComponent = memo(function ExpensiveComponent({ data, onClick }) {
console.log('ExpensiveComponent rendered');
return (
<div>
<h3>{data.title}</h3>
<button onClick={onClick}>Click me</button>
</div>
);
});
// 2. useMemo för dyra beräkningar
function ProductList({ products, searchTerm, sortBy }) {
const filteredAndSortedProducts = useMemo(() => {
console.log('Calculating filtered products...');
let result = products.filter(product =>
product.name.toLowerCase().includes(searchTerm.toLowerCase())
);
result.sort((a, b) => {
if (sortBy === 'price') return a.price - b.price;
if (sortBy === 'name') return a.name.localeCompare(b.name);
return 0;
});
return result;
}, [products, searchTerm, sortBy]); // Bara omberäkna när dependencies ändras
return (
<div>
{filteredAndSortedProducts.map(product => (
<ProductItem key={product.id} product={product} />
))}
</div>
);
}
// 3. useCallback för att stabilisera funktionsreferenser
function TodoApp() {
const [todos, setTodos] = useState([]);
// Utan useCallback skapas en ny funktion vid varje render
const handleToggleTodo = useCallback((id) => {
setTodos(prevTodos =>
prevTodos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
}, []); // Tom dependency array eftersom vi använder functional update
const handleDeleteTodo = useCallback((id) => {
setTodos(prevTodos => prevTodos.filter(todo => todo.id !== id));
}, []);
return (
<div>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggleTodo}
onDelete={handleDeleteTodo}
/>
))}
</div>
);
}
// 4. Lazy loading med React.lazy
const LazyDashboard = lazy(() => import('./Dashboard'));
const LazyProfile = lazy(() => import('./Profile'));
function App() {
return (
<BrowserRouter>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/dashboard" element={<LazyDashboard />} />
<Route path="/profile" element={<LazyProfile />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
// 5. Virtualisering för stora listor
import { FixedSizeList as List } from 'react-window';
function VirtualizedList({ items }) {
const Row = ({ index, style }) => (
<div style={style}>
<div>{items[index].name}</div>
</div>
);
return (
<List
height={600} // Höjd på container
itemCount={items.length}
itemSize={50} // Höjd per item
>
{Row}
</List>
);
}
Performance tips:
- Använd React DevTools Profiler
- Minimera re-renders med memo/useMemo/useCallback
- Code splitting med lazy loading
- Virtualisering för stora listor
- Optimera bundle size
- Använd production builds
Fråga 10: Error Boundaries och Error Handling
Intervjuare: "Implementera error boundaries och robustfehantering i React."
Bra svar:
// Error Boundary Class Component
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null, errorInfo: null };
}
static getDerivedStateFromError(error) {
// Uppdatera state så nästa render visar fallback UI
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// Logga felet till error reporting service
console.error('Error caught by boundary:', error, errorInfo);
this.setState({
error: error,
errorInfo: errorInfo
});
// Skicka till error monitoring (t.ex. Sentry)
// errorReportingService.captureException(error);
}
render() {
if (this.state.hasError) {
return (
<div className="error-boundary">
<h2>Något gick fel</h2>
<details style={{ whiteSpace: 'pre-wrap' }}>
{this.state.error && this.state.error.toString()}
<br />
{this.state.errorInfo.componentStack}
</details>
<button onClick={() => window.location.reload()}>
Ladda om sidan
</button>
</div>
);
}
return this.props.children;
}
}
// Custom hook för async error handling
function useAsyncError() {
const [, setError] = useState();
return useCallback((error) => {
setError(() => {
throw error;
});
}, []);
}
// Component med error handling
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const throwAsyncError = useAsyncError();
useEffect(() => {
const fetchUser = async () => {
try {
setLoading(true);
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`Failed to fetch user: ${response.status}`);
}
const userData = await response.json();
setUser(userData);
} catch (error) {
// Kasta error till Error Boundary
throwAsyncError(error);
} finally {
setLoading(false);
}
};
fetchUser();
}, [userId, throwAsyncError]);
if (loading) return <div>Loading...</div>;
if (!user) return <div>User not found</div>;
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
// App med Error Boundaries
function App() {
return (
<ErrorBoundary>
<Router>
<Header />
<ErrorBoundary>
<Routes>
<Route path="/user/:id" element={<UserProfile />} />
<Route path="/dashboard" element={
<ErrorBoundary>
<Dashboard />
</ErrorBoundary>
} />
</Routes>
</ErrorBoundary>
</Router>
</ErrorBoundary>
);
}
Error handling best practices:
- Använd Error Boundaries för att fånga rendering errors
- Implementera graceful degradation
- Logga fel till monitoring services
- Visa användarvänliga felmeddelanden
- Ha fallback UI för kritiska komponenter
Sammanfattning
Dessa frågor täcker de viktigaste aspekterna av React-utveckling:
- React Fundamentals: JSX, Virtual DOM, komponenter
- Hooks: useState, useEffect, custom hooks
- State Management: Props, Context API, state lifting
- Routing: React Router, navigation, protected routes
- Performance: Memo, useMemo, useCallback, lazy loading
- Error Handling: Error boundaries, async errors
- Best Practices: Forms, API integration, testing
Förberedelse-tips:
- Bygg små React-projekt för att öva praktiskt
- Förstå React lifecycle och när olika hooks körs
- Öva på performance-optimering med React DevTools
- Lär dig skillnaden mellan controlled/uncontrolled components
- Förstå när och varför du skulle använda Context vs props
Fördjupning: React
Detta kapitel samlar vidare ämnen som blir aktuella när grunderna sitter. Syftet är att förklara när och varför du behöver dem, inte bara hur.
Varför ett separat fördjupningskapitel?
- Hålla grunderna fokuserade: nybörjare lär sig snabbare med små, tydliga steg.
- Kontext: avancerade mönster behövs först när appen växer (komplex routing, delad state, prestanda, SEO).
- Beslutsstöd: förstå trade‑offs — när det är värt att öka komplexiteten.
Innehåll i fördjupningen:
- Routing: skyddade sidor, dataladdning och felhantering
- State: reducer + Context, normalisering och när tredjeparts‑store kan vara motiverad
- Prestanda: memo, virtualisering, code‑splitting och när det inte behövs
- Data: caching, retry/backoff, request‑dedupe och realtid (WebSockets)
- SSR/SSG: när serverrendering är rätt val (Next.js)
- TypeScript i React: varför och hur du inför det gradvis
Rekommenderad läsordning: hoppa till det problem du faktiskt har just nu.
Routing: skydd och data
Varför behövs detta?
- Autentisering/auktorisering: vissa sidor ska bara vara tillgängliga för inloggade användare/roller.
- Data‑först: ladda data innan en sida renderas, eller hantera fel centralt.
- Bättre UX: behåll kontext, redirecta smart, och visa vänliga fel.
Skyddade routes (Protected routes)
När? När du har sidor som kräver inloggning eller specifik roll.
Grundidé: en wrapper som kontrollerar status och annars navigerar bort.
import { Navigate, useLocation } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
export function ProtectedRoute({ children, requiredRole }) {
const { user, loading } = useAuth();
const location = useLocation();
if (loading) return <div>Laddar…</div>;
if (!user) return <Navigate to="/login" state={{ from: location }} replace />;
if (requiredRole && user.role !== requiredRole) return <Navigate to="/unauthorized" replace />;
return children;
}
Varför fungerar det? React Router v6 låter element vara vilken komponent som helst. Vi returnerar Navigate för att byta sida om villkor inte uppfylls.
Data‑laddning och fel (v6.4+ loaders)
När? När sidan behöver data innan den renderas (SEO/UX eller undvika blink).
Koncept: definiera en loader
som hämtar data. Komponenten läser med useLoaderData()
.
import { createBrowserRouter, RouterProvider, useLoaderData } from 'react-router-dom';
async function productLoader({ params }) {
const res = await fetch(`/api/products/${params.id}`);
if (!res.ok) throw new Response('Not found', { status: 404 });
return res.json();
}
function ProductDetail() {
const product = useLoaderData();
return <h1>{product.name}</h1>;
}
const router = createBrowserRouter([
{ path: '/products/:id', element: <ProductDetail />, loader: productLoader, errorElement: <div>Fel vid laddning</div> }
]);
export default function App() { return <RouterProvider router={router} />; }
Varför? Loader körs innan render — du får data/404 tidigt och kan visa rätt vy utan extra “loading” när data redan finns.
Felhantering för routes
Error boundaries per route fångar renderingsfel och visar fallback. Det förbättrar robusthet och UX.
När? I större appar eller när sidor har sköra beroenden.
Tips och trade‑offs
- Skydd i klienten räcker inte för säkerhet — backend måste alltid verifiera.
- Loaders förenklar dataflödet men låser in dig i Router‑pipen; använd där det passar.
- Central 404/unauthorized förbättrar navigationsflödet.
State: reducer, Context och normalisering
Varför behövs detta?
- När props‑drilling blir tungt och flera komponenter behöver samma data.
- När state‑uppdateringar blir komplexa och kräver förutsägbarhet.
Reducer + Context
Reducer ger ett deterministiskt sätt att uppdatera state utifrån action → nytt state. Context delar detta state i trädet.
När? När flera delar av appen måste läsa/uppdatera samma state och “lyfta state” inte räcker.
import { createContext, useContext, useReducer } from 'react';
const AppContext = createContext();
function reducer(state, action) {
switch (action.type) {
case 'SET_USER':
return { ...state, user: action.payload };
default:
return state;
}
}
export function AppProvider({ children }) {
const [state, dispatch] = useReducer(reducer, { user: null });
const value = { state, dispatch };
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
}
export function useApp() {
const ctx = useContext(AppContext);
if (!ctx) throw new Error('useApp must be used within AppProvider');
return ctx;
}
Varför? Reducer minskar implicit logik utspridd i många setState‑anrop och gör flöden testbara.
Normalisering av data
När? När du har inbäddade listor/objekt som blir svåra att uppdatera utan buggar.
Princip: spara data “platt” per typ (entities
) och referera med ids.
Fördel: enklare uppdateringar och memoization; mindre risk för oavsiktliga re‑renders.
När extern state‑hanterare?
- Du behöver features som devtools, middleware, time‑travel, asynk‑flöden.
- Alternativ: Zustand, Redux Toolkit, Jotai. Välj när behoven finns — inte i förväg.
Custom Hooks i React
Inledning: Ett vanligt problem
Du bygger en lista över användare och en annan vy som visar senaste beställningar. I båda komponenterna skriver du samma kod: hämta data (fetch), visa loading
, hantera error
, och en knapp för att ladda om. Koden kopieras, divergerar och blir svår att underhålla.
// UsersPage.jsx
function UsersPage() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const load = async () => {
try {
setLoading(true);
setError(null);
const res = await fetch('/api/users');
setUsers(await res.json());
} catch (e) {
setError('Kunde inte hämta användare');
} finally {
setLoading(false);
}
};
useEffect(() => { load(); }, []);
// ... renderar loading/error/lista
}
// OrdersPage.jsx (nästan identisk logik)
function OrdersPage() {
const [orders, setOrders] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const load = async () => {
try {
setLoading(true);
setError(null);
const res = await fetch('/api/orders');
setOrders(await res.json());
} catch (e) {
setError('Kunde inte hämta beställningar');
} finally {
setLoading(false);
}
};// UsersPage.jsx
function UsersPage() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const load = async () => {
try {
setLoading(true);
setError(null);
const res = await fetch('/api/users');
setUsers(await res.json());
} catch (e) {
setError('Kunde inte hämta användare');
} finally {
setLoading(false);
}
};
useEffect(() => { load(); }, []);
// ... renderar loading/error/lista
}
useEffect(() => { load(); }, []);
// ... renderar loading/error/lista
}
Problemet: samma mönster upprepas i flera komponenter. Varje liten ändring (t.ex. bättre felmeddelanden) måste göras överallt.
Lösningen: Skapa en custom hook
En custom hook (egen hook) kapslar in den upprepade logiken i en funktion som kan återanvändas.
// useResource.js
import { useEffect, useState } from 'react';
export function useResource(endpoint) {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const load = async () => {
try {
setLoading(true);
setError(null);
const res = await fetch(endpoint);
setData(await res.json());
} catch (e) {
setError('Kunde inte hämta resurser');
} finally {
setLoading(false);
}
};
useEffect(() => { load(); }, []);
return { data, loading, error, refetch: load };
}
// Användning i komponenter
function UsersPage() {
const { data, loading, error, refetch } = useResource('/api/users');
// ... enkel render, ingen duplicerad logik
}
function OrdersPage() {
const { data, loading, error, refetch } = useResource('/api/orders');
// ... enkel render, ingen duplicerad logik
}
Vinst: Komponenterna blir tunnare, logiken bor på ett ställe och kan förbättras centralt.
Varför Custom Hooks?
- Återanvändning: dela logik mellan komponenter utan duplicering.
- Separation: håll komponenter tunna; flytta komplexitet till hooks.
- Testbarhet: testa logik isolerat från UI.
- Inkapsling: göm implementation (fetch, timers, events) bakom enkel API.
- Komposition: kombinera flera hooks till kraftfulla mönster.
Regler för Hooks
- Anropa hooks högst upp i funktionen, inte i loopar/if/switch.
- Anropa hooks endast från React‑funktioner (komponenter eller andra hooks).
- Följ namngivningen
useNamn
så ESLint kan verifiera reglerna.
Bästa praxis
- Namn: beskriv vad hooken gör (
useDebounce
,useLocalStorage
). - API: returnera värden/funktioner i ett objekt för läsbarhet, eller en array när ordningen är viktig (som
useState
). - Stabilitet: använd
useCallback
/useMemo
för att exponera stabila funktioner/objekt. - Fel: exponera
error
ochloading
när det är relevant (t.ex. data‑hooks). - Beroenden: ta in parametrar istället för att hårdkoda (endpoint, initial values, TTL etc.).
Exempel: Små, generella hooks
Enkla hooks löser vardagliga behov och gör din kod renare. Här är tre du ofta har nytta av — med varför de hjälper och hur de används.
useToggle — slå av/på ett boolean‑värde
Varför: perfekt för att öppna/stänga modaler, toggla dark mode eller visa/dölja element.
import { useState } from 'react';
export function useToggle(initial = false) {
const [value, setValue] = useState(initial);
const toggle = () => setValue(v => !v);
return { value, toggle, setTrue: () => setValue(true), setFalse: () => setValue(false) };
}
// Användning
function Menu() {
const { value: open, toggle } = useToggle();
return (
<div>
<button onClick={toggle}>Meny</button>
{open && <nav>…</nav>}
</div>
);
}
useCounter — enkel räknare
Varför: vanligen för pagination, mängd i varukorg eller steg i en wizard.
import { useState } from 'react';
export function useCounter(initial = 0) {
const [count, setCount] = useState(initial);
const inc = (step = 1) => setCount(c => c + step);
const dec = (step = 1) => setCount(c => Math.max(0, c - step));
const reset = () => setCount(initial);
return { count, inc, dec, reset };
}
// Användning
function CartItem() {
const { count, inc, dec } = useCounter(1);
return (
<div>
<button onClick={() => dec()}>-</button>
<span>{count}</span>
<button onClick={() => inc()}>+</button>
</div>
);
}
useLocalStorage — spara enkel state över sidladdningar
Varför: behåll användarens val (tema, språk) mellan besök.
import { useState } from 'react';
export function useLocalStorage(key, defaultValue) {
const [value, setValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : defaultValue;
} catch {
return defaultValue;
}
});
const setStoredValue = (newValue) => {
setValue(newValue);
try { window.localStorage.setItem(key, JSON.stringify(newValue)); } catch {}
};
return [value, setStoredValue];
}
// Användning
function ThemeToggle() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Växla tema (nu: {theme})
</button>
);
}
Exempel: Data‑hooks för API
useApi
import { useState, useEffect, useCallback } from 'react';
export function useApi(apiFunction, dependencies = []) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const execute = useCallback(async (...args) => {
try {
setLoading(true);
setError(null);
const result = await apiFunction(...args);
setData(result);
return result;
} catch (err) {
setError(err.message || 'Request failed');
throw err;
} finally {
setLoading(false);
}
}, dependencies);
useEffect(() => {
execute();
}, [execute]);
const refetch = useCallback(() => execute(), [execute]);
return { data, loading, error, refetch, execute };
}
// Användning
// const { data, loading, error, refetch } = useApi(() => fetch('/api/users').then(r => r.json()));
useResource (CRUD)
import { useState, useCallback, useEffect } from 'react';
export function useResource(endpoint) {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const fetchJson = async (url, options = {}) => {
const res = await fetch(url, {
headers: { 'Content-Type': 'application/json', ...(options.headers || {}) },
...options,
});
const text = await res.text();
const json = text ? JSON.parse(text) : null;
if (!res.ok) {
throw new Error(json?.message || res.statusText || 'Request failed');
}
return json;
};
const fetchAll = useCallback(async () => {
try {
setLoading(true);
setError(null);
const result = await fetchJson(endpoint);
setData(Array.isArray(result) ? result : (result?.data ?? []));
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}, [endpoint]);
const create = useCallback(async (newItem) => {
try {
const result = await fetchJson(endpoint, {
method: 'POST',
body: JSON.stringify(newItem)
});
setData(prev => [...prev, result]);
return result;
} catch (err) {
setError(err.message);
throw err;
}
}, [endpoint]);
const update = useCallback(async (id, updatedItem) => {
try {
const result = await fetchJson(`${endpoint}/${id}`, {
method: 'PUT',
body: JSON.stringify(updatedItem)
});
setData(prev => prev.map(item => item.id === id ? result : item));
return result;
} catch (err) {
setError(err.message);
throw err;
}
}, [endpoint]);
const remove = useCallback(async (id) => {
try {
await fetchJson(`${endpoint}/${id}`, { method: 'DELETE' });
setData(prev => prev.filter(item => item.id !== id));
} catch (err) {
setError(err.message);
throw err;
}
}, [endpoint]);
useEffect(() => {
fetchAll();
}, [fetchAll]);
return { data, loading, error, create, update, remove, refetch: fetchAll };
}
Caching i en hook
import { useEffect, useState } from 'react';
class ApiCache {
constructor(ttl = 5 * 60 * 1000) { // 5 min
this.cache = new Map();
this.ttl = ttl;
}
set(key, data) { this.cache.set(key, { data, timestamp: Date.now() }); }
get(key) {
const item = this.cache.get(key);
if (!item) return null;
if (Date.now() - item.timestamp > this.ttl) { this.cache.delete(key); return null; }
return item.data;
}
clear() { this.cache.clear(); }
}
const apiCache = new ApiCache();
export function useCachedApi(key, apiFunction, dependencies = []) {
const [data, setData] = useState(() => apiCache.get(key));
const [loading, setLoading] = useState(!data);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
const cached = apiCache.get(key);
if (cached) { setData(cached); setLoading(false); return; }
try {
setLoading(true);
setError(null);
const result = await apiFunction();
apiCache.set(key, result);
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, [key, ...dependencies]);
return { data, loading, error };
}
Retry med exponential backoff
export const retryWithBackoff = async (fn, maxRetries = 3, baseDelay = 1000) => {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxRetries) throw error;
const delay = baseDelay * Math.pow(2, attempt - 1);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
};
useWebSocket (realtid)
import { useEffect, useState } from 'react';
export function useWebSocket(url) {
const [socket, setSocket] = useState(null);
const [lastMessage, setLastMessage] = useState(null);
const [connectionStatus, setConnectionStatus] = useState('Disconnected');
useEffect(() => {
const ws = new WebSocket(url);
ws.onopen = () => { setConnectionStatus('Connected'); setSocket(ws); };
ws.onmessage = (event) => setLastMessage(JSON.parse(event.data));
ws.onclose = () => { setConnectionStatus('Disconnected'); setSocket(null); };
ws.onerror = () => setConnectionStatus('Error');
return () => ws.close();
}, [url]);
const sendMessage = (message) => {
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify(message));
}
};
return { lastMessage, connectionStatus, sendMessage };
}
Säkerhet och robusthet
- Tokens i
localStorage
kan stjälas via XSS. För skyddade sessioner:httpOnly
‑cookies + CSRF‑skydd (SameSite/CSRF‑token). - Rensa resurser i
useEffect
‑cleanup (event listeners, WebSocket, timers) för att undvika minnesläckor. - Visa tydliga felmeddelanden och ge möjlighet till retry.
Kompositionsexempel
function SearchUsers() {
const [query, setQuery] = useState('');
const debounced = useDebounce(query, 300);
const { data, loading, error } = useCachedApi(
`users:${debounced}`,
() => fetch(`/api/users?q=${encodeURIComponent(debounced)}`).then(r => r.json()),
[debounced]
);
return (
<div>
<input value={query} onChange={e => setQuery(e.target.value)} placeholder="Sök användare" />
{loading && <p>Laddar…</p>}
{error && <p>Fel: {error}</p>}
<ul>{data?.map(u => <li key={u.id}>{u.name}</li>)}</ul>
</div>
);
}
Vanliga fallgropar
- Oklara beroenden i
useEffect
leder till oväntade anrop. Håll API:et rent och stabilt. - Läckande timers/sockets om cleanup saknas.
- För breda hooks som gör “allt”. Håll dem fokuserade.
Kort om testning
- Isolera nätverk: mocka
fetch
/API‑funktioner. - Testa brancher: loading, success, error och retry.
Vidare läsning
- React docs: Rules of Hooks och Building Your Own Hooks.
- Se även Fördjupning: Data: caching, retry och realtid.
Prestanda: memo, virtualisering och splitting
Varför behövs detta?
- Stora listor eller tunga komponenter kan göra UI segt.
- Onödiga re‑renders fördyrar upplevelsen.
Guiden: optimera när du ser ett problem, inte i förväg.
Undvik onödiga re‑renders
- Dela upp stora komponenter i mindre.
- Använd
React.memo
för rena presentionskomponenter. - Använd
useMemo
för tunga beräkningar ochuseCallback
för stabila callbacks.
Fallgrop: överanvändning gör koden svårare att förstå och kan ge sämre prestanda.
Virtualisering av listor
När? Listor med hundratals/tusentals rader.
Bibliotek: react-window
, react-virtualized
.
Varför? Rendera endast synliga rader → drastiskt mindre DOM‑arbete.
Code‑splitting och lazy loading
När? Stora sidor/verktyg som sällan används.
React.lazy
+ Suspense
för att ladda delar på begäran. Varför? Kortare initial laddning, bättre TTI.
Mät innan du optimerar
- Använd React DevTools Profiler för att se re‑renders.
- Inspektera bundle‑storlek och nätverk.
- Optimera det som faktiskt är flaskhalsen.
Data: caching, retry och realtid
Varför behövs detta?
- Nätverket är opålitligt: retry/backoff förbättrar robusthet.
- Onödiga anrop kostar: deduplicering och caching sparar resurser.
- Realtid: WebSockets för chatt, notiser, dashboards.
Enkel caching och deduplicering
Idé: spara svar i minne en kort tid och hindra parallella identiska anrop.
Trade‑off: cache måste invalideras — bra för läs‑tung data som sällan ändras.
Retry och exponential backoff
När? Transienta fel (nätverk/timeout). Inte för 4xx (klientfel).
Varför? Ökar chansen att lyckas utan att belasta servern.
WebSockets (realtid)
När? Live‑uppdateringar (chatt, ticker, notifieringar).
Varför? Push från servern i stället för att polla ofta.
Tips: återanslutning, hjärtslag och backoff krävs för robusthet.
SSR/SSG och Next.js
Varför behövs detta?
- SEO och snabb första rendering: innehåll finns i HTML vid första svar.
- Prestanda på långsamma enheter/nät: mindre JS behövs för att visa första innehållet.
Begrepp:
- SSR (Server‑Side Rendering): HTML genereras per request.
- SSG (Static Site Generation): HTML genereras vid build (snabbt + billigt att serva).
När välja vad?
- SSR: sidor med personaliserat/inloggat innehåll.
- SSG: publika, ofta lästa sidor som sällan ändras (blogg, produktsidor).
Next.js kortfattat:
- Filbaserad routing, inbyggd SSR/SSG, data‑fetching på servern, bildoptimering m.m.
- Börja när du har behov (SEO/prestanda) som motiverar lärokostnaden.
TypeScript i React
Varför TypeScript?
- Fångar fel vid utveckling (fel props, felaktiga svar från API).
- Bättre autocompletion och refaktoreringsstöd.
- Tydligare kontrakt mellan komponenter.
Props och state
// Button.tsx
type ButtonProps = {
text: string;
onClick?: () => void;
disabled?: boolean;
};
export function Button({ text, onClick, disabled = false }: ButtonProps) {
return (
<button disabled={disabled} onClick={onClick}>
{text}
</button>
);
}
Varför? Komponentens API blir själv‑dokumenterande och säkert.
Event‑typer
function Form() {
const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
};
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value);
};
return (
<form onSubmit={onSubmit}>
<input onChange={onChange} />
<button type="submit">Skicka</button>
</form>
);
}
Hooks och generics
function useFetch<T>(url: string) {
const [data, setData] = React.useState<T | null>(null);
const [error, setError] = React.useState<string | null>(null);
React.useEffect(() => {
let active = true;
fetch(url)
.then((r) => (r.ok ? r.json() : Promise.reject(r.statusText)))
.then((json) => active && setData(json as T))
.catch((err) => active && setError(String(err)));
return () => {
active = false;
};
}, [url]);
return { data, error };
}
Gradvis införande
- Starta med att typa nya filer (
.tsx
) och centrala modeller (User, Product). - Aktivera striktare TS‑regler först när teamet är bekvämt.
- Behåll interop med JS: du kan blanda
.ts/.tsx
och.js/.jsx
i samma projekt.
Tailwind CSS – Kort beskrivning och installationsguide
Vad är Tailwind CSS?
Tailwind CSS är ett modernt utility-first CSS-ramverk som revolutionerar hur vi skriver CSS. Istället för traditionell CSS där vi skapar anpassade klasser, använder Tailwind färdiga utility-klasser direkt i HTML/JSX. Detta innebär att du kan styla dina element med klasser som:
flex
för flexbox-layoutmt-4
för margin-toptext-center
för text-centreringbg-blue-500
för bakgrundsfärghover:scale-105
för hover-effekter
Varför använda Tailwind CSS?
-
🚀 Snabbare utveckling
- Skriv styling direkt i HTML/JSX utan att växla mellan filer
- Slipp hitta på klassnamn och återanvända CSS
- Få direkt visuell feedback när du arbetar
-
🎨 Konsekvent design
- Fördefinierat design-system med färger, spacing och typografi
- Enkelt att hålla en konsekvent look genom hela projektet
- Anpassningsbart via
tailwind.config.js
-
📱 Effektiv responsiv design
- Använd breakpoints som
sm:
,md:
,lg:
direkt i klasserna - Snabb och intuitiv mobilanpassning
- Inga mediaqueries behövs
- Använd breakpoints som
-
🛠️ Optimerat för produktion
- Automatisk eliminering av oanvänd CSS
- Mindre filstorlek i produktion
- Bättre prestanda
-
🌍 Stark community & ekosystem
- Omfattande dokumentation
- Färdiga komponentbibliotek
- Stort urval av plugins och verktyg
Installationsguide
1. Skapa nytt projekt
Om du inte redan har ett projekt:
npm create vite@latest my-project
cd my-project
2. Installera Tailwind CSS
Installera Tailwind och dess peer-dependencies:
npm install tailwindcss @tailwindcss/vite
3. Konfigurera Tailwind
Skapa eller uppdatera tailwind.config.js
(Viktigt att den filen ligger i root-folder av projektet):
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [
tailwindcss(),
],
})
4. Lägg till Tailwind i CSS
Skapa eller uppdatera din huvudsakliga CSS-fil (t.ex. src/index.css
). Om det redan ligger massa CSS där kommentera ut den eller ta bort.:
@import "tailwindcss";
5. Uppdatera HTML/JSX
Nu kan du börja använda Tailwind i dina komponenter. Exempel:
<div class="flex min-h-screen items-center justify-center bg-gray-100">
<div class="rounded-lg bg-white p-8 shadow-lg">
<h1 class="text-2xl font-bold text-gray-800">Hello Tailwind!</h1>
<p class="mt-2 text-gray-600">Welcome to your new project</p>
</div>
</div>
6. Starta utvecklingsservern
npm run dev
Verifiering
För att verifiera att Tailwind fungerar, kontrollera att:
- Utility-klasserna fungerar i webbläsaren
- Du ser Tailwind-specifik syntax-highlighting i din editor
- Du får autocompletion för Tailwind-klasser (installera relevant editor-tillägg om det behövs)
Kicka igång tailwind om utvecklingsservern varit avstängd
- Öppna en terminal
- Navigera till rätt folder
- Skriv in kommandot:
npm run dev
Anpassa Tailwind och lägga till ett eget tema
Utöka standardtemat
Du kan lägga till egna färger, typsnitt eller andra designparametrar genom att uppdatera tailwind.config.js
. Här är ett exempel på hur du lägger till egna färger och typsnitt:
// filepath: tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
colors: {
brand: {
light: '#3AB0FF',
DEFAULT: '#007BFF',
dark: '#0056B3',
},
},
fontFamily: {
custom: ['"Open Sans"', 'sans-serif'],
},
},
},
plugins: [],
}
Använd dina anpassningar
När du har lagt till dina egna färger och typsnitt kan du använda dem direkt i dina komponenter:
<div class="p-4 bg-brand text-white font-custom">
Detta är en komponent med anpassade färger och typsnitt.
</div>
Lägg till fler anpassningar
Du kan också utöka andra delar av temat, som spacing, border-radius eller skuggor:
extend: {
spacing: {
'72': '18rem',
'84': '21rem',
'96': '24rem',
},
borderRadius: {
'xl': '1.5rem',
},
boxShadow: {
'custom': '0 4px 6px rgba(0, 0, 0, 0.1)',
},
}
Tips för VS Code
Installera dessa tillägg för bättre Tailwind-stöd:
- Tailwind CSS IntelliSense
- PostCSS Language Support
Dokumentation
Layout med Flexbox och Grid
📖 Flexbox i Tailwind
Flexbox används för att styra layout horisontellt och vertikalt.
Exempel 1 – Enkel navbar
<div class="flex justify-between items-center p-4 bg-gray-200">
<div class="font-bold text-xl">Logo</div>
<div class="space-x-4">
<a href="#" class="hover:text-blue-600">Hem</a>
<a href="#" class="hover:text-blue-600">Om oss</a>
<a href="#" class="hover:text-blue-600">Kontakt</a>
</div>
</div>
Förklaringar:
flex
→ Aktiverar flexbox.justify-between
→ Sprider ut elementen horisontellt.items-center
→ Centrerar vertikalt.space-x-4
→ Skapar horisontellt mellanrum mellan barn-element.
Exempel 2 – Centerad container
<div class="flex justify-center items-center h-64 bg-gray-100">
<p class="text-lg">Centrerad text</p>
</div>
📖 Grid
Grid används för mer avancerade layouter med kolumner och rader.
<div class="grid grid-cols-3 gap-4 p-4">
<div class="bg-red-300 p-4">1</div>
<div class="bg-green-300 p-4">2</div>
<div class="bg-blue-300 p-4">3</div>
</div>
grid-cols-3
→ Tre kolumner.gap-4
→ Mellanrum mellan rader och kolumner.
📝 Övning
- Skapa en flexbox navbar med 5 länkar, centrera vertikalt.
- Bygg en 4-kolumns grid-layout med olika färger.
- Testa
flex-col
ochflex-wrap
för responsiv layout.
Tailwind CSS – Färger och Typografi
Introduktion
Tailwind CSS erbjuder ett kraftfullt och flexibelt sätt att hantera färger och typografi i dina projekt. Med hjälp av utility-klasser kan du snabbt och enkelt skapa en konsekvent design utan att behöva skriva anpassad CSS.
Färger i Tailwind CSS
Fördefinierade färger
Tailwind har ett stort urval av fördefinierade färger som är enkla att använda. Färgerna är organiserade i nyanser från 50
(ljusast) till 900
(mörkast).
Exempel på färger
<div class="p-4 bg-blue-500 text-white">Blå bakgrund</div>
<div class="p-4 bg-red-300 text-red-900">Röd bakgrund med mörk text</div>
<div class="p-4 bg-gray-100 text-gray-800">Ljusgrå bakgrund med mörkgrå text</div>
Anpassa färger
Du kan anpassa färger i tailwind.config.js
:
// filepath: tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
brand: {
light: '#3AB0FF',
DEFAULT: '#007BFF',
dark: '#0056B3',
},
},
},
},
};
Använd sedan dina anpassade färger:
<div class="p-4 bg-brand text-white">Anpassad färg</div>
Typografi i Tailwind CSS
Textstorlekar
Tailwind erbjuder fördefinierade textstorlekar som är enkla att använda:
<p class="text-sm">Liten text</p>
<p class="text-lg">Stor text</p>
<p class="text-2xl font-bold">Extra stor och fet text</p>
Typsnitt
Du kan använda fördefinierade typsnitt eller lägga till egna:
<p class="font-sans">Sans-serif typsnitt</p>
<p class="font-serif">Serif typsnitt</p>
<p class="font-mono">Monospace typsnitt</p>
Anpassa typsnitt
Lägg till egna typsnitt i tailwind.config.js
:
// filepath: tailwind.config.js
module.exports = {
theme: {
extend: {
fontFamily: {
custom: ['"Open Sans"', 'sans-serif'],
},
},
},
};
Använd sedan det anpassade typsnittet:
<p class="font-custom">Text med anpassat typsnitt</p>
Exempel: Kombinera färger och typografi
<div class="p-6 bg-gray-100 text-gray-800">
<h1 class="text-3xl font-bold text-blue-600">Välkommen till Tailwind CSS</h1>
<p class="mt-2 text-lg text-gray-700">
Tailwind gör det enkelt att skapa snygga och responsiva gränssnitt.
</p>
<button class="mt-4 px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600">
Läs mer
</button>
</div>
Tips för responsiv design
Tailwind gör det enkelt att anpassa färger och typografi för olika skärmstorlekar:
<p class="text-sm sm:text-base md:text-lg lg:text-xl">
Textstorleken ändras beroende på skärmstorlek.
</p>
<div class="bg-red-500 sm:bg-yellow-500 md:bg-green-500 lg:bg-blue-500">
Bakgrundsfärgen ändras beroende på skärmstorlek.
</div>
Övning: Skapa en profilkortskomponent
Uppgift
Skapa ett responsivt profilkort med hjälp av Tailwind CSS. Kortet ska innehålla:
- En profilbild.
- Ett namn och en titel.
- En kort beskrivning.
- En knapp för att följa användaren.
Krav
- Använd Tailwinds färg- och typografiklasser.
- Gör kortet responsivt så att det ser bra ut på både mobil och desktop.
Exempel på HTML-struktur
<div class="max-w-sm mx-auto bg-white rounded-lg shadow-md overflow-hidden">
<img class="w-full h-48 object-cover" src="https://via.placeholder.com/150" alt="Profilbild">
<div class="p-4">
<h2 class="text-xl font-bold text-gray-800">John Doe</h2>
<p class="text-sm text-gray-600">Webbutvecklare</p>
<p class="mt-2 text-gray-700">
Passionerad utvecklare med erfarenhet av moderna ramverk som Tailwind CSS.
</p>
<button class="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
Följ
</button>
</div>
</div>
Utmaning
- Lägg till en hover-effekt på kortet (t.ex. en skugga eller ändrad bakgrundsfärg).
- Gör kortet klickbart genom att lägga till en länk runt hela kortet.
Sammanfattning
- Färger: Använd fördefinierade eller anpassade färger för att skapa en konsekvent design.
- Typografi: Hantera textstorlekar, typsnitt och andra textrelaterade egenskaper med utility-klasser.
- Responsivitet: Anpassa färger och typografi för olika skärmstorlekar med Tailwinds breakpoints.
För mer information, besök Tailwind CSS Dokumentation.
Tailwind CSS – Responsiv Design
Introduktion
Responsiv design är en viktig del av modern webbutveckling. Med Tailwind CSS är det enkelt att skapa responsiva gränssnitt genom att använda dess inbyggda breakpoints och utility-klasser. Du kan snabbt anpassa layout, färger, typografi och andra stilar för olika skärmstorlekar utan att skriva egna media queries.
Breakpoints i Tailwind CSS
Tailwind använder följande standard-breakpoints:
Breakpoint | Prefix | Beskrivning |
---|---|---|
sm | sm: | För skärmar ≥ 640px |
md | md: | För skärmar ≥ 768px |
lg | lg: | För skärmar ≥ 1024px |
xl | xl: | För skärmar ≥ 1280px |
2xl | 2xl: | För skärmar ≥ 1536px |
Du kan använda dessa prefix för att applicera olika stilar beroende på skärmstorlek.
Exempel: Responsiv layout
Här är ett exempel på hur du kan skapa en responsiv layout med Tailwind:
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
<div class="p-4 bg-blue-500 text-white">Kolumn 1</div>
<div class="p-4 bg-green-500 text-white">Kolumn 2</div>
<div class="p-4 bg-red-500 text-white">Kolumn 3</div>
</div>
Förklaring:
grid grid-cols-1
: Skapar en grid-layout med en kolumn som standard.sm:grid-cols-2
: Vid skärmar ≥ 640px visas två kolumner.lg:grid-cols-3
: Vid skärmar ≥ 1024px visas tre kolumner.gap-4
: Lägger till mellanrum mellan kolumnerna.
Exempel: Responsiv typografi
Du kan också anpassa textstorlekar för olika skärmstorlekar:
<h1 class="text-2xl sm:text-3xl md:text-4xl lg:text-5xl">
Responsiv rubrik
</h1>
Förklaring:
text-2xl
: Standardstorlek för rubriken.sm:text-3xl
: Vid skärmar ≥ 640px ökas textstorleken.md:text-4xl
: Vid skärmar ≥ 768px ökas textstorleken ytterligare.lg:text-5xl
: Vid skärmar ≥ 1024px används den största textstorleken.
Exempel: Responsiv navigationsmeny
Här är ett exempel på en navigationsmeny som ändras mellan mobil och desktop:
<nav class="bg-gray-800 text-white p-4">
<ul class="flex flex-col sm:flex-row sm:space-x-4">
<li><a href="#" class="block py-2 sm:py-0">Hem</a></li>
<li><a href="#" class="block py-2 sm:py-0">Om oss</a></li>
<li><a href="#" class="block py-2 sm:py-0">Tjänster</a></li>
<li><a href="#" class="block py-2 sm:py-0">Kontakt</a></li>
</ul>
</nav>
Förklaring:
flex flex-col
: Menyn visas som en vertikal lista som standard.sm:flex-row
: Vid skärmar ≥ 640px ändras menyn till en horisontell lista.sm:space-x-4
: Lägger till horisontellt mellanrum mellan menyalternativen på större skärmar.
Övning: Skapa en responsiv produktkortskomponent
Uppgift
Skapa en responsiv produktkortskomponent som innehåller:
- En bild av produkten.
- Produktens namn och pris.
- En kort beskrivning.
- En knapp för att lägga till produkten i varukorgen.
Krav
- Kortet ska visas i en kolumn på små skärmar och i en rad på större skärmar.
- Använd Tailwinds breakpoints för att göra kortet responsivt.
- Lägg till en hover-effekt på knappen.
Exempel på HTML-struktur
<div class="max-w-md mx-auto bg-white rounded-lg shadow-md overflow-hidden sm:flex sm:items-center sm:space-x-4">
<img class="w-full h-48 object-cover sm:w-32 sm:h-32" src="https://via.placeholder.com/150" alt="Produktbild">
<div class="p-4">
<h2 class="text-lg font-bold text-gray-800">Produktnamn</h2>
<p class="text-gray-600">Kort beskrivning av produkten.</p>
<p class="mt-2 text-green-500 font-semibold">Pris: 299 kr</p>
<button class="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
Lägg till i varukorgen
</button>
</div>
</div>
Utmaning
- Gör kortet klickbart genom att lägga till en länk runt hela kortet.
- Lägg till en responsiv grid-layout som visar flera produktkort bredvid varandra på större skärmar.
Sammanfattning
- Breakpoints: Använd Tailwinds inbyggda breakpoints för att skapa responsiva layouter.
- Responsivitet: Anpassa layout, typografi och andra stilar för olika skärmstorlekar.
- Praktisk tillämpning: Skapa komponenter som fungerar bra på både små och stora skärmar.
För mer information, besök Tailwind CSS Dokumentation.
Tailwind CSS – States och Interaktivitet
Introduktion
Tailwind CSS gör det enkelt att hantera olika tillstånd (states) och interaktivitet i dina komponenter. Med hjälp av pseudo-klassprefix som hover:
, focus:
, och active:
kan du skapa dynamiska och responsiva användarupplevelser utan att behöva skriva anpassad CSS.
Vanliga pseudo-klasser i Tailwind CSS
Pseudo-klass | Prefix | Beskrivning |
---|---|---|
hover | hover: | När användaren hovrar över ett element |
focus | focus: | När ett element är i fokus |
active | active: | När ett element är aktivt (t.ex. klickat) |
disabled | disabled: | När ett element är inaktiverat |
group-hover | group-hover: | När en grupp är i hover-tillstånd |
Exempel: Hover-effekter
Med hover:
kan du enkelt skapa effekter när användaren hovrar över ett element.
<button class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
Hovera över mig
</button>
Förklaring:
hover:bg-blue-600
: Ändrar bakgrundsfärgen när användaren hovrar över knappen.
Exempel: Focus-effekter
Med focus:
kan du styla element när de är i fokus, till exempel när en användare klickar på ett formulärfält.
<input class="border-2 border-gray-300 focus:border-blue-500 focus:outline-none p-2" type="text" placeholder="Skriv något...">
Förklaring:
focus:border-blue-500
: Ändrar kantfärgen när fältet är i fokus.focus:outline-none
: Tar bort standardfokusramen.
Exempel: Active-effekter
Med active:
kan du styla element när de är aktiva, till exempel när en knapp trycks ned.
<button class="px-4 py-2 bg-green-500 text-white rounded active:bg-green-700">
Klicka på mig
</button>
Förklaring:
active:bg-green-700
: Ändrar bakgrundsfärgen när knappen trycks ned.
Exempel: Disabled-tillstånd
Med disabled:
kan du styla element som är inaktiverade.
<button class="px-4 py-2 bg-gray-400 text-white rounded disabled:opacity-50" disabled>
Inaktiverad knapp
</button>
Förklaring:
disabled:opacity-50
: Sänker opaciteten för inaktiverade element.
Exempel: Gruppbaserade tillstånd
Med group-hover:
kan du skapa effekter som påverkar andra element i samma grupp.
<div class="group p-4 bg-gray-100 rounded-lg">
<h2 class="text-lg font-bold group-hover:text-blue-500">Rubrik</h2>
<p class="text-gray-600 group-hover:text-gray-800">Text som ändras vid hover.</p>
</div>
Förklaring:
group-hover:text-blue-500
: Ändrar textfärgen på rubriken när användaren hovrar över gruppen.group-hover:text-gray-800
: Ändrar textfärgen på paragrafen vid samma tillstånd.
Exempel: Interaktiva kort
Här är ett exempel på ett interaktivt kort som använder flera pseudo-klasser:
<div class="max-w-sm mx-auto bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow">
<img class="w-full h-48 object-cover" src="https://via.placeholder.com/150" alt="Bild">
<div class="p-4">
<h2 class="text-lg font-bold text-gray-800 hover:text-blue-500">Interaktiv rubrik</h2>
<p class="text-gray-600">Detta är ett exempel på ett interaktivt kort.</p>
<button class="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 focus:ring-2 focus:ring-blue-300">
Läs mer
</button>
</div>
</div>
Förklaring:
hover:shadow-lg
: Lägger till en större skugga när användaren hovrar över kortet.hover:text-blue-500
: Ändrar rubrikens färg vid hover.focus:ring-2 focus:ring-blue-300
: Lägger till en ring runt knappen vid fokus.
Övning: Skapa en interaktiv navigationsmeny
Uppgift
Skapa en navigationsmeny som innehåller:
- Flera länkar (t.ex. Hem, Om oss, Kontakt).
- En hover-effekt som ändrar textfärgen.
- En aktiv länk som har en annan stil (t.ex. fet text och annan färg).
- En gruppbaserad effekt där en ikon ändras när användaren hovrar över länken.
Krav
- Använd Tailwinds pseudo-klasser för att hantera tillstånd.
- Gör menyn responsiv så att den visas som en vertikal lista på små skärmar och en horisontell lista på större skärmar.
Exempel på HTML-struktur
<nav class="bg-gray-800 text-white p-4">
<ul class="flex flex-col sm:flex-row sm:space-x-4">
<li>
<a href="#" class="block py-2 px-4 hover:text-blue-400 active:font-bold active:text-blue-500">
Hem
</a>
</li>
<li>
<a href="#" class="block py-2 px-4 hover:text-blue-400 active:font-bold active:text-blue-500">
Om oss
</a>
</li>
<li>
<a href="#" class="block py-2 px-4 hover:text-blue-400 active:font-bold active:text-blue-500 group">
Kontakt
<span class="ml-2 text-gray-400 group-hover:text-blue-400">📧</span>
</a>
</li>
</ul>
</nav>
Utmaning
- Lägg till en animation som gör att hover-effekten övergångar mjukt.
- Gör menyn sticky så att den alltid är synlig högst upp på sidan.
Sammanfattning
- Pseudo-klasser: Använd
hover:
,focus:
,active:
, ochdisabled:
för att hantera olika tillstånd. - Gruppbaserade effekter: Använd
group
ochgroup-hover:
för att skapa interaktivitet mellan element. - Praktisk tillämpning: Skapa interaktiva komponenter som förbättrar användarupplevelsen.
För mer information, besök Tailwind CSS Dokumentation.
Tailwind CSS – Konfigurering och Anpassning
Introduktion
Tailwind CSS är mycket flexibelt och kan enkelt anpassas för att passa dina projektbehov. Genom att använda tailwind.config.js
kan du:
- Lägga till egna färger, typsnitt och spacing.
- Skräddarsy breakpoints och andra designparametrar.
- Utöka eller skriva över standardinställningarna.
I denna lektion går vi igenom hur du konfigurerar och anpassar Tailwind CSS, samt hur du kan skapa ett eget tema.
Skapa och konfigurera tailwind.config.js
När du installerar Tailwind CSS genereras en konfigurationsfil med kommandot:
npx tailwindcss init
Denna fil (tailwind.config.js
) används för att anpassa Tailwind. Här är ett exempel på en grundläggande konfigurationsfil:
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
Viktiga delar av konfigurationsfilen:
content
: Här anger du vilka filer Tailwind ska genomsöka för att hitta klasser. Detta är viktigt för att Tailwind ska kunna ta bort oanvänd CSS i produktion.theme
: Här kan du anpassa eller utöka standardtemat.plugins
: Här kan du lägga till Tailwind-plugins för extra funktionalitet.
Anpassa färger
Du kan lägga till egna färger eller skriva över standardfärgerna i theme.extend.colors
:
theme: {
extend: {
colors: {
brand: {
light: '#3AB0FF',
DEFAULT: '#007BFF',
dark: '#0056B3',
},
},
},
},
Användning:
<div class="bg-brand text-white p-4">
Detta är en komponent med anpassad färg.
</div>
Anpassa typsnitt
Lägg till egna typsnitt i theme.extend.fontFamily
:
theme: {
extend: {
fontFamily: {
custom: ['"Open Sans"', 'sans-serif'],
},
},
},
Användning:
<p class="font-custom text-lg">
Detta är text med ett anpassat typsnitt.
</p>
Anpassa spacing
Du kan lägga till egna spacing-värden (t.ex. padding och margin):
theme: {
extend: {
spacing: {
'72': '18rem',
'84': '21rem',
'96': '24rem',
},
},
},
Användning:
<div class="p-72 bg-gray-200">
Detta element har 18rem padding.
</div>
Anpassa breakpoints
Om du vill ändra standard-breakpoints kan du göra det i theme.screens
:
theme: {
screens: {
sm: '480px',
md: '768px',
lg: '1024px',
xl: '1280px',
'2xl': '1536px',
},
},
Lägg till plugins
Tailwind har ett stort ekosystem av plugins. Du kan lägga till plugins som @tailwindcss/forms
för att förbättra formulärstilar:
npm install @tailwindcss/forms
Lägg sedan till pluginet i tailwind.config.js
:
plugins: [
require('@tailwindcss/forms'),
],
Exempel: Skapa ett anpassat tema
Här är ett exempel på en komplett konfigurationsfil med ett anpassat tema:
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
colors: {
brand: {
light: '#3AB0FF',
DEFAULT: '#007BFF',
dark: '#0056B3',
},
},
fontFamily: {
custom: ['"Open Sans"', 'sans-serif'],
},
spacing: {
'72': '18rem',
'84': '21rem',
'96': '24rem',
},
},
},
plugins: [
require('@tailwindcss/forms'),
],
}
Övning: Skapa ett anpassat kort med eget tema
Uppgift
Skapa ett kort som använder ett anpassat tema. Kortet ska innehålla:
- En rubrik med ett anpassat typsnitt.
- En bakgrundsfärg från det anpassade färgschemat.
- Padding och margin från de anpassade spacing-värdena.
- En knapp med en hover-effekt som använder det anpassade färgschemat.
Exempel på HTML-struktur
<div class="max-w-md mx-auto bg-brand-light text-white p-72 rounded-lg shadow-lg">
<h2 class="text-2xl font-custom">Anpassat kort</h2>
<p class="mt-4">
Detta kort använder ett anpassat tema med egna färger, typsnitt och spacing.
</p>
<button class="mt-6 px-4 py-2 bg-brand hover:bg-brand-dark rounded">
Klicka här
</button>
</div>
Krav
- Lägg till de anpassade färgerna, typsnitten och spacing-värdena i
tailwind.config.js
. - Använd Tailwinds utility-klasser för att bygga kortet.
Utmaning
- Lägg till en responsiv design så att kortet ändrar storlek på olika skärmstorlekar.
- Lägg till en animation som gör att hover-effekten övergångar mjukt.
Sammanfattning
- Konfiguration: Använd
tailwind.config.js
för att anpassa Tailwind CSS. - Anpassningar: Lägg till egna färger, typsnitt, spacing och breakpoints.
- Plugins: Utöka funktionaliteten med Tailwind-plugins.
- Praktisk tillämpning: Skapa komponenter som använder ditt anpassade tema.
För mer information, besök Tailwind CSS Dokumentation.
Bygga en Dashboardsida med Tailwind CSS
Introduktion
En dashboardsida är en central plats för att visa viktig information och ge användaren tillgång till olika funktioner. Med Tailwind CSS kan du snabbt bygga en responsiv och modern dashboardsida. I denna lektion går vi igenom:
- Hur du planerar en dashboardsida.
- Vilka element som bör vara med.
- Best practices för att bygga en dashboardsida.
- Ett praktiskt exempel.
Hur ska man tänka när man bygger en dashboardsida?
1. Definiera syftet
- Vad är syftet med dashboarden? Är det att visa statistik, hantera användare eller något annat?
- Identifiera de viktigaste funktionerna och informationen som användaren behöver.
2. Prioritera innehåll
- Placera det viktigaste innehållet högst upp (t.ex. KPI:er, grafer eller notifikationer).
- Använd en tydlig hierarki för att guida användaren.
3. Planera layouten
- Sidhuvud (Header): För logotyp, navigering och användarprofil.
- Sidomeny (Sidebar): För navigering mellan olika sektioner.
- Huvudinnehåll (Main Content): För grafer, tabeller och annan information.
- Sidfot (Footer): För länkar eller copyright-information.
4. Responsiv design
- Se till att dashboarden fungerar bra på både desktop och mobila enheter.
- Använd Tailwinds breakpoints (
sm:
,md:
,lg:
, etc.) för att anpassa layouten.
Vilka element borde vara med?
1. Sidhuvud (Header)
- Logotyp.
- Sökfält.
- Användarprofil eller avatar.
- Notifikationsikon.
2. Sidomeny (Sidebar)
- Navigeringslänkar till olika sektioner (t.ex. Hem, Statistik, Inställningar).
- Ikoner för att göra navigeringen tydligare.
3. Huvudinnehåll (Main Content)
- KPI-kort: För att visa nyckeltal (t.ex. antal användare, försäljning).
- Grafer och diagram: För att visualisera data.
- Tabeller: För att visa detaljerad information.
- Snabbåtgärder: Knapp för att lägga till ny data eller hantera inställningar.
4. Sidfot (Footer)
- Länkar till hjälp eller support.
- Copyright-information.
Best Practices
-
Håll det enkelt
- Undvik att överbelasta dashboarden med för mycket information.
- Använd whitespace för att skapa en luftig layout.
-
Använd konsekventa färger
- Använd ett färgschema som passar varumärket.
- Använd Tailwinds färgklasser för att skapa kontrast och tydlighet.
-
Responsivitet först
- Börja med en mobilvänlig layout och bygg ut för större skärmar.
-
Använd Tailwinds utility-klasser
- Använd klasser som
grid
,flex
, ochspace-x-4
för att snabbt skapa layouten.
- Använd klasser som
-
Testa användarupplevelsen
- Säkerställ att dashboarden är lätt att navigera och att viktig information är lättillgänglig.
Exempel: Bygga en dashboardsida
Här är ett exempel på en enkel dashboardsida:
<div class="min-h-screen bg-gray-100 flex">
<!-- Sidebar -->
<aside class="w-64 bg-gray-800 text-white flex flex-col">
<div class="p-4 text-lg font-bold">Dashboard</div>
<nav class="flex-1">
<ul>
<li class="p-4 hover:bg-gray-700"><a href="#">Hem</a></li>
<li class="p-4 hover:bg-gray-700"><a href="#">Statistik</a></li>
<li class="p-4 hover:bg-gray-700"><a href="#">Inställningar</a></li>
</ul>
</nav>
<div class="p-4">© 2025 Företag</div>
</aside>
<!-- Main Content -->
<main class="flex-1 p-6">
<!-- Header -->
<header class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold">Välkommen, Användare</h1>
<div class="flex items-center space-x-4">
<input type="text" placeholder="Sök..." class="p-2 border rounded">
<img src="https://via.placeholder.com/40" alt="Avatar" class="w-10 h-10 rounded-full">
</div>
</header>
<!-- KPI Cards -->
<section class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<div class="p-4 bg-white rounded shadow">
<h2 class="text-lg font-bold">Användare</h2>
<p class="text-2xl font-semibold">1,234</p>
</div>
<div class="p-4 bg-white rounded shadow">
<h2 class="text-lg font-bold">Försäljning</h2>
<p class="text-2xl font-semibold">45,678 kr</p>
</div>
<div class="p-4 bg-white rounded shadow">
<h2 class="text-lg font-bold">Supportärenden</h2>
<p class="text-2xl font-semibold">12</p>
</div>
</section>
<!-- Table -->
<section class="bg-white rounded shadow p-4">
<h2 class="text-lg font-bold mb-4">Senaste aktiviteter</h2>
<table class="w-full text-left">
<thead>
<tr>
<th class="border-b p-2">Datum</th>
<th class="border-b p-2">Aktivitet</th>
<th class="border-b p-2">Status</th>
</tr>
</thead>
<tbody>
<tr>
<td class="p-2">2025-08-19</td>
<td class="p-2">Ny användare registrerad</td>
<td class="p-2 text-green-500">Slutförd</td>
</tr>
<tr>
<td class="p-2">2025-08-18</td>
<td class="p-2">Supportärende skapat</td>
<td class="p-2 text-yellow-500">Pågående</td>
</tr>
</tbody>
</table>
</section>
</main>
</div>
Övning: Bygg din egen dashboardsida
Uppgift
Bygg en egen dashboardsida som innehåller:
- En sidomeny med minst tre länkar.
- Ett sidhuvud med en sökfält och en användarprofil.
- Tre KPI-kort som visar nyckeltal.
- En tabell som visar data.
Krav
- Använd Tailwinds utility-klasser för layout och styling.
- Gör sidan responsiv så att den fungerar på både mobil och desktop.
Utmaning
- Lägg till en graf eller diagram med hjälp av ett bibliotek som Chart.js.
- Lägg till en hover-effekt på sidomenyns länkar.
Sammanfattning
- Planering: Definiera syftet och prioritera innehållet.
- Element: Inkludera sidhuvud, sidomeny, KPI-kort, tabeller och grafer.
- Best practices: Håll det enkelt, använd konsekventa färger och bygg responsivt.
För mer information, besök Tailwind CSS Dokumentation.
Tailwind CSS – Best Practices & Komponentbibliotek
Introduktion
Tailwind CSS är ett kraftfullt verktyg för att bygga moderna och responsiva gränssnitt. För att maximera effektiviteten och skapa skalbara projekt är det viktigt att följa best practices och dra nytta av komponentbibliotek. I denna lektion går vi igenom:
- Best practices för att arbeta med Tailwind CSS.
- Hur du använder och anpassar komponentbibliotek.
- Ett praktiskt exempel.
Best Practices för Tailwind CSS
1. Använd återanvändbara komponenter
- Skapa återanvändbara komponenter för vanliga UI-element som knappar, kort och formulär.
- Använd Tailwinds
@apply
-direktiv i dina CSS-filer för att gruppera klasser:
/* filepath: src/styles/components.css */
.btn {
@apply px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600;
}
Använd sedan klassen i HTML:
<button class="btn">Klicka här</button>
2. Håll HTML-strukturen ren
- Undvik att lägga för många klasser direkt i HTML. Använd komponentklasser eller utility-klasser strategiskt.
- Exempel:
<!-- Istället för detta -->
<button class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
Klicka här
</button>
<!-- Använd en komponentklass -->
<button class="btn">Klicka här</button>
3. Använd Tailwinds konfigurationsfil
- Anpassa Tailwind i
tailwind.config.js
för att skapa ett konsekvent design-system. - Lägg till egna färger, typsnitt och spacing:
theme: {
extend: {
colors: {
brand: '#007BFF',
},
fontFamily: {
custom: ['"Open Sans"', 'sans-serif'],
},
},
},
4. Responsiv design först
- Börja med en mobilvänlig layout och bygg ut för större skärmar med Tailwinds breakpoints (
sm:
,md:
,lg:
, etc.). - Exempel:
<div class="p-4 sm:p-6 lg:p-8">
Responsiv padding
</div>
5. Använd plugins
- Dra nytta av Tailwinds ekosystem av plugins för att utöka funktionaliteten, t.ex.:
@tailwindcss/forms
för formulärstilar.@tailwindcss/typography
för bättre textformatering.@tailwindcss/aspect-ratio
för att hantera bildförhållanden.
Komponentbibliotek för Tailwind CSS
Tailwind CSS har flera färdiga komponentbibliotek som kan hjälpa dig att snabbt bygga gränssnitt. Här är några populära alternativ:
1. Tailwind UI
- Ett officiellt komponentbibliotek från skaparna av Tailwind CSS.
- Innehåller högkvalitativa komponenter för allt från knappar till kompletta layouter.
- Besök Tailwind UI
2. Flowbite
- Ett open-source komponentbibliotek som fokuserar på interaktiva komponenter som modaler, dropdowns och navigationsmenyer.
- Besök Flowbite
3. DaisyUI
- Ett plugin för Tailwind CSS som lägger till färdiga komponenter och teman.
- Enkel att använda och anpassa.
- Besök DaisyUI
4. Headless UI
- Ett bibliotek med tillståndsfria och tillgängliga komponenter som dropdowns, modaler och flikar.
- Perfekt för att bygga egna komponenter med Tailwind.
- Besök Headless UI
Exempel: Bygga en komponent med DaisyUI
Här är ett exempel på hur du kan använda DaisyUI för att skapa en knapp:
Installation
Installera DaisyUI:
npm install daisyui
Lägg till DaisyUI i tailwind.config.js
:
plugins: [
require('daisyui'),
],
Användning
Använd DaisyUIs färdiga klasser för att skapa en knapp:
<button class="btn btn-primary">Primär knapp</button>
Övning: Skapa en komponent med Tailwind CSS
Uppgift
Skapa en återanvändbar kortkomponent som innehåller:
- En bild.
- En rubrik.
- En kort beskrivning.
- En knapp.
Krav
- Använd Tailwinds
@apply
-direktiv för att skapa en komponentklass i CSS. - Gör kortet responsivt så att det ser bra ut på både mobil och desktop.
Exempel på HTML-struktur
<div class="card">
<img src="https://via.placeholder.com/150" alt="Bild" class="card-img">
<div class="card-body">
<h2 class="card-title">Korttitel</h2>
<p class="card-text">Detta är en kort beskrivning.</p>
<button class="card-btn">Läs mer</button>
</div>
</div>
Exempel på CSS
/* filepath: src/styles/components.css */
.card {
@apply bg-white rounded-lg shadow-md overflow-hidden;
}
.card-img {
@apply w-full h-48 object-cover;
}
.card-body {
@apply p-4;
}
.card-title {
@apply text-lg font-bold text-gray-800;
}
.card-text {
@apply text-gray-600;
}
.card-btn {
@apply mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600;
}
Sammanfattning
- Best Practices: Använd återanvändbara komponenter, håll HTML-strukturen ren och dra nytta av Tailwinds konfigurationsfil.
- Komponentbibliotek: Utforska bibliotek som Tailwind UI, Flowbite och DaisyUI för att snabba upp utvecklingen.
- Praktisk tillämpning: Skapa egna komponenter med Tailwinds utility-klasser och
@apply
.
För mer information, besök Tailwind CSS Dokumentation.
Arbeta med Tailwind CSS och React med Vite
Introduktion
Att använda Tailwind CSS tillsammans med React och Vite är ett effektivt sätt att bygga moderna och responsiva webbapplikationer. Vite är ett snabbt byggverktyg som gör utvecklingsprocessen smidig, medan Tailwind CSS erbjuder utility-first-klasser för att snabbt styla komponenter.
I denna lektion går vi igenom:
- Hur du installerar och konfigurerar Tailwind CSS i ett React-projekt med Vite.
- Hur du använder Tailwind CSS i React-komponenter.
- Best practices för att arbeta med Tailwind och React.
Steg 1: Skapa ett React-projekt med Vite
Skapa ett nytt projekt
Skapa ett nytt React-projekt med Vite:
npm create vite@latest my-app --template react
cd my-app
npm install
Installera Tailwind CSS
Installera Tailwind CSS och dess beroenden:
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
Konfigurera Tailwind
Uppdatera tailwind.config.js
för att inkludera alla filer där du använder Tailwind-klasser:
// filepath: tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
Lägg till Tailwind i din CSS
Skapa eller uppdatera filen src/index.css
och inkludera Tailwinds direktiv:
@tailwind base;
@tailwind components;
@tailwind utilities;
Starta projektet
Starta utvecklingsservern för att verifiera att Tailwind fungerar:
npm run dev
Steg 2: Använda Tailwind CSS i React-komponenter
Exempel: Skapa en knappkomponent
Här är ett exempel på en enkel knappkomponent som använder Tailwind CSS:
// filepath: src/components/Button.jsx
export default function Button({ text, onClick }) {
return (
<button
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
onClick={onClick}
>
{text}
</button>
);
}
Exempel: Skapa en kortkomponent
Här är ett exempel på en kortkomponent som använder Tailwind CSS:
// filepath: src/components/Card.jsx
export default function Card({ title, description }) {
return (
<div className="max-w-sm bg-white rounded-lg shadow-md overflow-hidden">
<div className="p-4">
<h2 className="text-lg font-bold text-gray-800">{title}</h2>
<p className="text-gray-600">{description}</p>
</div>
</div>
);
}
Använd komponenterna i din app
Importera och använd komponenterna i din App.jsx
:
// filepath: src/App.jsx
import Button from "./components/Button";
import Card from "./components/Card";
function App() {
return (
<div className="p-6 bg-gray-100 min-h-screen">
<h1 className="text-2xl font-bold text-gray-800 mb-4">Välkommen till min app</h1>
<Card
title="Korttitel"
description="Detta är en beskrivning av kortet."
/>
<Button text="Klicka här" onClick={() => alert("Knappen klickades!")} />
</div>
);
}
export default App;
Best Practices för Tailwind CSS och React
1. Håll komponenterna små och återanvändbara
- Dela upp din kod i små, återanvändbara komponenter.
- Exempel: Skapa separata komponenter för knappar, kort, formulärfält, etc.
2. Använd Tailwinds konfigurationsfil
- Anpassa Tailwind i
tailwind.config.js
för att skapa ett konsekvent design-system. - Lägg till egna färger, typsnitt och spacing.
3. Använd conditional classnames
- Använd bibliotek som
clsx
ellerclassnames
för att hantera dynamiska klasser i React:
npm install clsx
Exempel:
import clsx from "clsx";
export default function Button({ isPrimary }) {
return (
<button
className={clsx(
"px-4 py-2 rounded",
isPrimary ? "bg-blue-500 text-white" : "bg-gray-200 text-black"
)}
>
Klicka här
</button>
);
}
4. Använd Tailwinds plugins
- Dra nytta av Tailwinds plugins för att utöka funktionaliteten, t.ex.:
@tailwindcss/forms
för formulärstilar.@tailwindcss/typography
för bättre textformatering.
5. Håll HTML-strukturen ren
- Undvik att lägga för många klasser direkt i JSX. Använd komponentklasser eller utility-klasser strategiskt.
Övning: Bygg en enkel profilkomponent
Uppgift
Skapa en profilkomponent som innehåller:
- En profilbild.
- Ett namn och en titel.
- En knapp för att följa användaren.
Krav
- Använd Tailwinds utility-klasser för styling.
- Gör komponenten responsiv så att den ser bra ut på både mobil och desktop.
Exempel på HTML-struktur
// filepath: src/components/ProfileCard.jsx
export default function ProfileCard({ name, title, image }) {
return (
<div className="max-w-sm bg-white rounded-lg shadow-md overflow-hidden">
<img className="w-full h-48 object-cover" src={image} alt={name} />
<div className="p-4">
<h2 className="text-lg font-bold text-gray-800">{name}</h2>
<p className="text-gray-600">{title}</p>
<button className="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
Följ
</button>
</div>
</div>
);
}
Använd komponenten
Importera och använd komponenten i din app:
// filepath: src/App.jsx
import ProfileCard from "./components/ProfileCard";
function App() {
return (
<div className="p-6 bg-gray-100 min-h-screen">
<ProfileCard
name="John Doe"
title="Webbutvecklare"
image="https://via.placeholder.com/150"
/>
</div>
);
}
export default App;
Sammanfattning
- Installation: Installera och konfigurera Tailwind CSS i ditt React-projekt med Vite.
- Komponenter: Använd Tailwind CSS för att snabbt styla React-komponenter.
- Best Practices: Håll komponenterna små och återanvändbara, använd conditional classnames och dra nytta av Tailwinds plugins.
För mer information, besök Tailwind CSS Dokumentation och React Dokumentation.