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

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