Living on the Edge
Web-Apps mit SvelteKit und Cloudflare Pages
Nils Röhrig | Loql
Inhalt
Was ist Edge Computing?
Was bringt Edge Computing in der Web-Entwicklung?
Was sind SvelteKit & Cloudflare Pages?
Wie kann eine App mit diesen Tools aussehen?
Was ist Edge Computing?
Edge Computing ist Computing, das nahe am physischen Standort der Nutzerinnen und Nutzer oder der Datenquelle stattfindet.
Elemente eines Edge-Netzwerkes
Core
End user
Core
Service provider edge
End user
Core
End-user premises edge
Service provider edge
End user
Core
End-user premises edge
Device edge
Service provider edge
End user
Core
End-user premises edge
Device edge
Service provider edge
End user
Core
End-user premises edge
Device edge
Service provider edge
End user
Was bringt Edge Computing in der Web-Entwicklung?
Core
End-user premises edge
Device edge
Service provider edge
End user
Core
End-user premises edge
Device edge
Service provider edge
End user
Core
CDN
Core
Web Frontend
Web Frontend
Web Frontend
Service Provider Edge
Edge Node
Parts of
Application
Edge Node
Parts of
Application
Edge Node
Parts of
Application
Core
Was sind SvelteKit & Cloudflare Pages?
SvelteKit is a framework for building extremely high-performance web apps.
Meta-Framework für
- Serverseitiges Rendering
- Statische Seitengenerierung
- Dateibasiertes Routing
- Clientseitige Navigation
- Plattformunabhängigkeit
- uvm.
Meta-Framework für
- Serverseitiges Rendering
- Statische Seitengenerierung
- Dateibasiertes Routing
- Clientseitige Navigation
- Plattformunabhängigkeit
- uvm.
Cloudflare Pages is a JAMstack platform for frontend developers to collaborate and deploy websites.
CDN
deploys to
-Providers
CDN
integrates with
deploys to
-Providers
CDN
integrates with
deploys to
uses
-Providers
CDN
integrates with
deploys to
uses
runs code at
Wie kann eine App mit diesen Tools aussehen?
Einrichtung in 4 simplen Schritten...
Einrichtung in 4 simplen Schritten...
1. GitHub-Repository erstellen
Einrichtung in 4 simplen Schritten...
1. GitHub-Repository erstellen
2. Cloudflare Pages-Projekt aus Repository erstellen
Einrichtung in 4 simplen Schritten...
1. GitHub-Repository erstellen
3. SvelteKit-App erstellen
2. Cloudflare Pages-Projekt aus Repository erstellen
Einrichtung in 4 simplen Schritten...
3. SvelteKit-App erstellen
4. App ins Repository pushen
1. GitHub-Repository erstellen
2. Cloudflare Pages-Projekt aus Repository erstellen
Introducing svekom
Interessantes aus der Entwicklung
Routing in SvelteKit
Routing in SvelteKit
- Dateibasiertes Routing
- Standardpfad:
src/routes
- Namen und Parameter:
src/routes/name/[param]
- Aufteilung in Seiten, Endpunkte, Layouts & Fehlerseiten
Beispiel: Produkte
src/routes/products/[id]
Beispiel: Produkte
<script lang="ts">
export let data: PageData;
let product: Product, category: Category | string | undefined;
$: ({ product } = data);
$: ({ category } = product);
</script>
<article>
<Card>
<Section first>
<img class="image" alt="" src="/products/{product.filename}"/>
</Section>
</Card>
<div class="info">
{#if category && typeof category !== 'string'}
<Badge>{category.name}</Badge>
{/if}
<h2 class="title">{product.brand} {product.name}</h2>
<p class="short-description">{product.shortDescription}</p>
<div class="price">{formatPrice(product.price)}</div>
<AddToCart {product}/>
<div class="description">
<p class="origin">Country of origin: {product.origin}</p>
{@html product.description}
</div>
</div>
</article>
const converter = new showdown.Converter();
export const load: PageServerLoad = async ({ params, platform }) => {
const productService = createProductService(platform);
const categoryService = createCategoryService(platform);
const product = await productService
.getSingleProduct(params.id)
.then((option) => option.getOrThrow(error(404)));
const { description, category: categoryId, ...rest } = product;
const category = await categoryService
.getSingleCategory(categoryId)
.then((category) => category.getOrUndefined());
return {
product: {
...rest,
category,
description: converter.makeHtml(product.description),
},
};
};
+page.svelte
+page.server.ts
src/routes/products/[id]
Beispiel: Produkte
<script lang="ts">
export let data: PageData;
let product: Product, category: Category | string | undefined;
$: ({ product } = data);
$: ({ category } = product);
</script>
<article>
<Card>
<Section first>
<img class="image" alt="" src="/products/{product.filename}"/>
</Section>
</Card>
<div class="info">
{#if category && typeof category !== 'string'}
<Badge>{category.name}</Badge>
{/if}
<h2 class="title">{product.brand} {product.name}</h2>
<p class="short-description">{product.shortDescription}</p>
<div class="price">{formatPrice(product.price)}</div>
<AddToCart {product}/>
<div class="description">
<p class="origin">Country of origin: {product.origin}</p>
{@html product.description}
</div>
</div>
</article>
const converter = new showdown.Converter();
export const load: PageServerLoad = async ({ params, platform }) => {
const productService = createProductService(platform);
const categoryService = createCategoryService(platform);
const product = await productService
.getSingleProduct(params.id)
.then((option) => option.getOrThrow(error(404)));
const { description, category: categoryId, ...rest } = product;
const category = await categoryService
.getSingleCategory(categoryId)
.then((category) => category.getOrUndefined());
return {
product: {
...rest,
category,
description: converter.makeHtml(product.description),
},
};
};
+page.svelte
+page.server.ts
src/routes/products/[id]
Beispiel: Produkte
<script lang="ts">
export let data: PageData;
let product: Product, category: Category | string | undefined;
$: ({ product } = data);
$: ({ category } = product);
</script>
<article>
<Card>
<Section first>
<img class="image" alt="" src="/products/{product.filename}"/>
</Section>
</Card>
<div class="info">
{#if category && typeof category !== 'string'}
<Badge>{category.name}</Badge>
{/if}
<h2 class="title">{product.brand} {product.name}</h2>
<p class="short-description">{product.shortDescription}</p>
<div class="price">{formatPrice(product.price)}</div>
<AddToCart {product}/>
<div class="description">
<p class="origin">Country of origin: {product.origin}</p>
{@html product.description}
</div>
</div>
</article>
const converter = new showdown.Converter();
export const load: PageServerLoad = async ({ params, platform }) => {
const productService = createProductService(platform);
const categoryService = createCategoryService(platform);
const product = await productService
.getSingleProduct(params.id)
.then((option) => option.getOrThrow(error(404)));
const { description, category: categoryId, ...rest } = product;
const category = await categoryService
.getSingleCategory(categoryId)
.then((category) => category.getOrUndefined());
return {
product: {
...rest,
category,
description: converter.makeHtml(product.description),
},
};
};
+page.svelte
+page.server.ts
src/routes/products/[id]
Beispiel: Produkte
const converter = new showdown.Converter();
export const load: PageServerLoad = async ({ params, platform }) => {
const productService = createProductService(platform);
const categoryService = createCategoryService(platform);
const product = await productService
.getSingleProduct(params.id)
.then((option) => option.getOrThrow(error(404)));
const { description, category: categoryId, ...rest } = product;
const category = await categoryService
.getSingleCategory(categoryId)
.then((category) => category.getOrUndefined());
return {
product: {
...rest,
category,
description: converter.makeHtml(product.description),
},
};
};
+page.svelte
+page.server.ts
src/routes/products/[id]
<script lang="ts">
export let data: PageData;
let product: Product, category: Category | string | undefined;
$: ({ product } = data);
$: ({ category } = product);
</script>
<article>
<Card>
<Section first>
<img class="image" alt="" src="/products/{product.filename}"/>
</Section>
</Card>
<div class="info">
{#if category && typeof category !== 'string'}
<Badge>{category.name}</Badge>
{/if}
<h2 class="title">{product.brand} {product.name}</h2>
<p class="short-description">{product.shortDescription}</p>
<div class="price">{formatPrice(product.price)}</div>
<AddToCart {product}/>
<div class="description">
<p class="origin">Country of origin: {product.origin}</p>
{@html product.description}
</div>
</div>
</article>
Beispiel: Produkte
const converter = new showdown.Converter();
export const load: PageServerLoad = async ({ params, platform }) => {
const productService = createProductService(platform);
const categoryService = createCategoryService(platform);
const product = await productService
.getSingleProduct(params.id)
.then((option) => option.getOrThrow(error(404)));
const { description, category: categoryId, ...rest } = product;
const category = await categoryService
.getSingleCategory(categoryId)
.then((category) => category.getOrUndefined());
return {
product: {
...rest,
category,
description: converter.makeHtml(product.description),
},
};
};
+page.svelte
+page.server.ts
src/routes/products/[id]
<script lang="ts">
export let data: PageData;
let product: Product, category: Category | string | undefined;
$: ({ product } = data);
$: ({ category } = product);
</script>
<article>
<Card>
<Section first>
<img class="image" alt="" src="/products/{product.filename}"/>
</Section>
</Card>
<div class="info">
{#if category && typeof category !== 'string'}
<Badge>{category.name}</Badge>
{/if}
<h2 class="title">{product.brand} {product.name}</h2>
<p class="short-description">{product.shortDescription}</p>
<div class="price">{formatPrice(product.price)}</div>
<AddToCart {product}/>
<div class="description">
<p class="origin">Country of origin: {product.origin}</p>
{@html product.description}
</div>
</div>
</article>
Daten in Workers KV
Daten in Workers KV
Worker Code </>
Daten in Workers KV
Worker Code </>
uses
Runtime API
list()
get()
put()
delete()
Daten in Workers KV
Worker Code </>
Runtime API
list()
get()
put()
delete()
uses
updates
Edge Nodes
KV Cache
KV Cache
KV Cache
Daten in Workers KV
Worker Code </>
Runtime API
list()
get()
put()
delete()
uses
updates
updates
Edge Nodes
KV Cache
KV Cache
KV Cache
Daten in Workers KV
Worker Code </>
Edge Nodes
KV Cache
KV Cache
KV Cache
Runtime API
list()
get()
put()
delete()
uses
replicates
replicates
updates
updates
- Paare sind eventually-consistent
- Optimal für häufiges Lesen
- Nicht optimal für häufiges Schreiben
Daten in Workers KV
Beispiel: Produktdaten
Beispiel: Produktdaten
interface Product {
id: string;
name: string;
shortDescription: string;
description: string;
price: number;
brand: string;
origin: string;
category: Category | string | undefined;
filename: string;
}
export function createProductService(platform: App.Platform) {
const store = platform.env.PRODUCTS;
return {
getProductsByCategory(category: Category): Promise<Product[]> {
return store
.list()
.then(prop('keys'))
.then(map(prop('name'))).
.then(keyNames => Promise.all(
map(keyName => store.get<Product>(keyName, { type: 'json'}))
))
.then(filter(propEq('category', category.id)))
}
};
}
Product.ts
ProductService.ts
Beispiel: Produktdaten
interface Product {
id: string;
name: string;
shortDescription: string;
description: string;
price: number;
brand: string;
origin: string;
category: Category | string | undefined;
filename: string;
}
export function createProductService(platform: App.Platform) {
const store = platform.env.PRODUCTS;
return {
getProductsByCategory(category: Category): Promise<Product[]> {
return store
.list()
.then(prop('keys'))
.then(map(prop('name'))).
.then(keyNames => Promise.all(
map(keyName => store.get<Product>(keyName, { type: 'json'}))
))
.then(filter(propEq('category', category.id)))
}
};
}
Product.ts
ProductService.ts
Beispiel: Produktdaten
interface Product {
id: string;
name: string;
shortDescription: string;
description: string;
price: number;
brand: string;
origin: string;
category: Category | string | undefined;
filename: string;
}
export function createProductService(platform: App.Platform) {
const store = platform.env.PRODUCTS;
return {
getProductsByCategory(category: Category): Promise<Product[]> {
return store
.list()
.then(prop('keys'))
.then(map(prop('name'))).
.then(keyNames => Promise.all(
map(keyName => store.get<Product>(keyName, { type: 'json'}))
))
.then(filter(propEq('category', category.id)))
}
};
}
Product.ts
ProductService.ts
SvelteKit-Adapter
application
Target platforms
application
Target platforms
Platform-specific adapter
application
Target platforms
Platform-specific adapter
uses
application
Target platforms
Platform-specific adapter
uses
adapts to
Beispiel: adapter-cloudflare
Beispiel: adapter-cloudflare
svelte.config.js
app.d.ts
import adapter from '@sveltejs/adapter-cloudflare';
import preprocess from 'svelte-preprocess';
const config = {
preprocess: preprocess(),
kit: {
adapter: adapter(),
},
};
export default config;
/// <reference types="@sveltejs/adapter-cloudflare" />
declare namespace App {
interface Platform {
env?: {
PRODUCTS: KVNamespace;
CATEGORIES: KVNamespace;
CONTENT: KVNamespace;
};
}
}
svelte.config.js
app.d.ts
import adapter from '@sveltejs/adapter-cloudflare';
import preprocess from 'svelte-preprocess';
const config = {
preprocess: preprocess(),
kit: {
adapter: adapter(),
},
};
export default config;
/// <reference types="@sveltejs/adapter-cloudflare" />
declare namespace App {
interface Platform {
env?: {
PRODUCTS: KVNamespace;
CATEGORIES: KVNamespace;
CONTENT: KVNamespace;
};
}
}
Beispiel: adapter-cloudflare
svelte.config.js
app.d.ts
import adapter from '@sveltejs/adapter-cloudflare';
import preprocess from 'svelte-preprocess';
const config = {
preprocess: preprocess(),
kit: {
adapter: adapter(),
},
};
export default config;
/// <reference types="@sveltejs/adapter-cloudflare" />
declare namespace App {
interface Platform {
env?: {
PRODUCTS: KVNamespace;
CATEGORIES: KVNamespace;
CONTENT: KVNamespace;
};
}
}
Beispiel: adapter-cloudflare
Erkenntnisse
Erkenntnisse
Edge-Web-Apps laufen in der Nähe des Nutzers
Edge-Web-Apps laufen in der Nähe des Nutzers
Tools wie SvelteKit vereinfachen die Entwicklung
Erkenntnisse
Tools wie SvelteKit vereinfachen die Entwicklung
Plattformen wie Cloudflare vereinfachen das Deployment & die Ausführung
Erkenntnisse
Edge-Web-Apps laufen in der Nähe des Nutzers
Vielen Dank!
Code:
Live:
Guide:
Kontakt
Twitter:
LinkedIn:
Xing:
Auf ein Wort zur lokalen Entwicklung...
Auf ein Wort zur lokalen Entwicklung...
import { dev } from '$app/environment';
import type { Handle } from '@sveltejs/kit';
import { isNil } from 'ramda';
export const handle: Handle = async ({ event, resolve }) => {
if (dev && isNil(event.platform)) {
const { categoryStore, contentStore, productStore } = await import(
'./kv-mock'
);
event.platform = {
env: {
PRODUCTS: productStore,
CATEGORIES: categoryStore,
CONTENT: contentStore,
},
};
}
return resolve(event);
};
src/hooks/index.ts
src/hooks/kvMock.ts
import { KVNamespace } from '@miniflare/kv';
import { MemoryStorage } from '@miniflare/storage-memory';
export const productStore = new KVNamespace(new MemoryStorage());
export const categoryStore = new KVNamespace(new MemoryStorage());
export const contentStore = new KVNamespace(new MemoryStorage());
Auf ein Wort zur lokalen Entwicklung...
import { dev } from '$app/environment';
import type { Handle } from '@sveltejs/kit';
import { isNil } from 'ramda';
export const handle: Handle = async ({ event, resolve }) => {
if (dev && isNil(event.platform)) {
const { categoryStore, contentStore, productStore } = await import(
'./kv-mock'
);
event.platform = {
env: {
PRODUCTS: productStore,
CATEGORIES: categoryStore,
CONTENT: contentStore,
},
};
}
return resolve(event);
};
src/hooks/index.ts
src/hooks/kvMock.ts
import { KVNamespace } from '@miniflare/kv';
import { MemoryStorage } from '@miniflare/storage-memory';
export const productStore = new KVNamespace(new MemoryStorage());
export const categoryStore = new KVNamespace(new MemoryStorage());
export const contentStore = new KVNamespace(new MemoryStorage());
Auf ein Wort zur lokalen Entwicklung...
src/hooks/index.ts
src/hooks/kvMock.ts
import { KVNamespace } from '@miniflare/kv';
import { MemoryStorage } from '@miniflare/storage-memory';
export const productStore = new KVNamespace(new MemoryStorage());
export const categoryStore = new KVNamespace(new MemoryStorage());
export const contentStore = new KVNamespace(new MemoryStorage());
import { dev } from '$app/environment';
import type { Handle } from '@sveltejs/kit';
import { isNil } from 'ramda';
export const handle: Handle = async ({ event, resolve }) => {
if (dev && isNil(event.platform)) {
const { categoryStore, contentStore, productStore } = await import(
'./kv-mock'
);
event.platform = {
env: {
PRODUCTS: productStore,
CATEGORIES: categoryStore,
CONTENT: contentStore,
},
};
}
return resolve(event);
};
Auf ein Wort zur lokalen Entwicklung...
src/hooks/index.ts
src/hooks/kvMock.ts
import { KVNamespace } from '@miniflare/kv';
import { MemoryStorage } from '@miniflare/storage-memory';
export const productStore = new KVNamespace(new MemoryStorage());
export const categoryStore = new KVNamespace(new MemoryStorage());
export const contentStore = new KVNamespace(new MemoryStorage());
import { dev } from '$app/environment';
import type { Handle } from '@sveltejs/kit';
import { isNil } from 'ramda';
export const handle: Handle = async ({ event, resolve }) => {
if (dev && isNil(event.platform)) {
const { categoryStore, contentStore, productStore } = await import(
'./kv-mock'
);
event.platform = {
env: {
PRODUCTS: productStore,
CATEGORIES: categoryStore,
CONTENT: contentStore,
},
};
}
return resolve(event);
};
Living on the Edge (enterJS 2023)
By Nils Röhrig
Living on the Edge (enterJS 2023)
- 1,237