A gentle introduction to SvelteKit for Google Cloud developers
MartinJ
Posted on July 16, 2024
Introduction
How does SvelteKit differ from React, and is it any better?
Functionally, I guess there's not that much difference. Most things you can do in React you can do in SvelteKit. And vice-versa. But when you get down to the details, many people feel that SvelteKit has the edge in terms of the ease with which you achieve your "reactive" goals. Svelte means "elegant" - and that's just what it is - a slender, highly adaptable and practical tool.
I was attracted to SvelteKit because it also tends to push you towards server-side design - ie code that runs on your webapp's Cloud servers rather than in your user's web browser. This is ironic because it was the ease with which you could write and debug client-side code that originally got me hooked on webapp development. But then I discovered how reluctant indexing spiders are to invest time in "hydrating" client-side code and realised I would just have to put more effort in here myself. See 'Debugging in SvelteKit', below, to get a handle on how you debug server-side code. But there are other reasons why you might consider using server-side code too. Here are a couple:
Once you start using third-party services such as Postmark (email despatch) or Paypal (payment collection), you'll realise that it's not a good idea to include their security codes in client-side code. If you can use the "inspector" to view these, so can anyone else. Code that runs server-side is inaccessible.
server-side code lives closer to your data and runs faster here than on a client laptop.
SvelteKit makes it easy to play tunes on specifying which bits of your webapp are to run locally and which are to run remotely.
- In some cases, pages may be entirely server-side rendered - if they contain only static information, Sveltekit will enable you to "pre-render" them. Pre-rendered pages are constructed at build time and downloaded as slabs of pure HTML.
- Alternatively, they may be entirely client-side rendered.
- Or yet again, they may run on both. A SvelteKit webapp aiming to deliver optimal response times may initially display just a server-sourced "placeholder" screen to get something, anything, visible (you get great credit with Google's indexing bots here, apparently). This is then "hydrated" by client-side code with information specific to the user-instance.
Let's get down to something a bit more concrete.
Routing in Svelte
Externally, a Sveltekit webapp will look exactly like any classic browser application - a hierarchy of "pages" such as mywebapp/dosomethingwithmyfiles
. It's like this because client users expect, and rely on this type of arrangement. But below the surface, a SvelteKit webapp delivers this arrangement in a totally different way to a React webapp. In React these pages are actually all parts of one giant slab of code and requests are routed thither by re-directs operating at the web interface (if that sentence doesn't make any sense to you, have a look at Whats a 'Single-page' webapp?). SvelteKit achieves this by using your project structure to define your page structure. So, if you want to have a mywebapp/dosomethingwithmyfiles
page, you need to have a folder named dosomethingwithmyfiles
with a +page.svelte
file inside it. Once this arrangement is in place, your deployed app delivers a separate physical page for each of its URLs.
Here's a sample source folder structure for a SvelteKit project:
myproject
├───src
│ └───routes
│ └───dosomethingwithmyfiles
Once you've installed SvelteKit (see Svelte for New Developers), this structure will be augmented by a mass of complicated config
files and build
folders etc. But, for the present, the focus is on the routes
folder. This is where you store your page code - and here is where you might start to wonder whether SvelteKit is the right thing for you. Take a tight grip now because this is where things get a bit complicated.
SvelteKit requires you to follow a very strict naming convention for the content of a page folder. Here's a list of the filenames that might see in a dosomethingwithmyfiles
folder:
-
dosomethingwithmyfiles/+page.svelte. This file would contain the source for the code that displays the page for URL
myproject/dosomethingwithmyfiles
on the browser screen. Whoah - let that sink in for a moment. When you're working in your VSCode editor on a SvelteKit project with half a dozen different pages, your filebar may display half a dozen tabs all named+page.svelte
. Confusing? Yes, I agree.
At first sight, you might feel that this is simply unacceptable. But note that each +page.svelte
file is qualified on the editor bar by the name of its folder owner, dosomethingwithmyfiles
, or whatever. It's not so difficult to discipline yourself to check for the owner of a +page.svelte
before you dive in and start editing. And once you've developed a SvelteKit project or two you'll begin to appreciate the value of the convention in declaring the purpose of the arrangement (as you'll see in a moment there are quite a few variations)
While you're absorbing this shock, let me give you a bit of encouragement. Within a +page.svelte
file you might expect to find the same sort of code you'd see in an equivalent React file - a mixture of exotic useState
calls to manipulate page state, and JSX to 'react' to this and generate HTML. While a +page.svelte
file certainly does the same job, it manages to discard the "exotic" bit and uses plain javascript and pure, undiluted HTMl salted with a sprinkling of special keywords. You may find this refreshing.
Here are a few more standard filenames you might find in a dosomethingwithmyfiles
folder:
-
dosomethingwithmyfiles/+page.server.js, This would contain the source for a file that delivers data to a
+page.svelte
file (ie, the equivalent of a ReactuseEffect
). Code here will run efficiently and securely on the server when the page is initially loaded.Web API
code to run in the browser andNode.js
style to run server-side in a Firebase function you'll be delighted to hear that, in Sveltekit, theWeb API
version is now perfectly happy to run server-side as well.+page.js
file ends up in the associated+page.svelte
. Let me say that, for the present, this arrives by SvelteKit magic. The exact mechanism will only become clear once I've described SvelteKit's arrangements for defining "reactive" variables. Hang onto your hat for now. -
dosomethingwithmyfiles/+page.js. This is used when data loading can happen on the client-side, such as when data can be fetched via APIs that don’t require server-side security or when it doesn’t matter if the data fetch happens asynchronously after the initial page load.
-
dosomethingwithmyfiles/+layout.svelte. This is where you would place code that sets up those bits of a page common to a whole set of other pages - toolbar headers, for example. A
+layout.svelte
file applies to every child route and any sibling+page.svelte
. You can nest layouts to arbitrary depth. Again, the precise arrangement for inserting the common layout into the recipient pages will be left for later - more Svelte magic.+layout.svelte
page needs some data, it can have an attendant+layout.server.js
file -
dosomethingwithmyfiles/+server.js. This is where you would place code that you wanted to be available as an "API endpoint" via a parameterised URL such as
myProject/dosomethingwithmyfiles?type="pdf"
. I'll provide more details on this arrangement later.
'Reactive variables' and 'Reactive HTML' in SvelteKit
By 'reactive variables' I mean data items that cause the browser page to re-render when they change. By 'reactive HTML' I mean HTML instrumented to make it respond to these changes.
In React, you'll recall, reactive variables are declared using a useState
expression that defines the variables as properties of a state object. The declaration also specifies initial property values and a function to change them.
Here's an example - a React webapp that displays a popup that disappears when you click it:
import React, { useState } from "react";
const [screenState, setScreenState] = useState({popupVisible: true,});
return (
<div>
<h1 style={{textAlign: "center"}}
onClick = {() => {setScreenState({popupVisible: !screenState.popupVisible})}}>
Main Page - Click to toggle popup
</h1>
{screenState.popupVisible &&
<div
style={{ textAlign: "center", marginLeft: "auto", marginRight: "auto", height: "2rem", width: "25rem", backgroundColor: "gainsboro" }}
onClick = {() => {setScreenState({popupVisible: !screenState.popupVisible})}}>
<h2> Popup Window - Click to Hide popup</h2>
</div>
}
</div>
)
In Svelte (I'm now talking about the language as opposed to the framework in which it operates) you might achieve this effect in a src/routes/demo/+page.svelte
file by simply declaring popupVisible
as a javascript variable
<script>
let popupVisible = false;
</script>
<div>
<h1 style="text-align: center"
on:click={() => (popupVisible = !popupVisible)}>
Main Page - Click to toggle popup
</h1>
{#if popupVisible}
<div
style="text-align: center; margin-left: auto; margin-right: auto; height: 2rem; width: 25rem; background-color: gainsboro"
on:click={() => (popupVisible = !popupVisible)}
>
<h2>Popup Window - Click to Hide popup</h2>
</div>
{/if}
</div>
Here's a summary of the key differences:
Svelte uses a standard Javascript
let
declaration to introduce state variables instead of the strange ReactuseState
expressionSvelte uses a down to earth
#if 'logical expression'
keyword to replace the awkward JSX{'logical expression' &&
syntax. This makes your code much more readable. Svelte also provides associatedelse
andeach
keywords.Svelte uses plain CSS to define HTML classes rather than the perplexing JSX style objects (eg
{{textAlign: "center"}}
).
Note also that the demo/+pagesvelte
file defined above will run directly in the browser as /demo
. To run the React version you would have to put some code into an associated src/main.jsx
file to define the new route.
Inputs: Local Functions, Actions and API endpoints
Keyboard input in React generally uses the following pattern:
const [myState, setMyState] = useState({myProperty: "",});
function handleChange({ target }) {
setMyState({ ...myState, [target.name]: target.value });
};
return (
<input name="myProperty"
value={myState.myProperty}
onChange={handleChange} />
)
Here, an input labelled as "myProperty" fires a general-purpose handleChange
function every time you press a key. In handleChange
its value is extracted and applied to the page's state to trigger a re-render.
Svelte thinks this is too complicated and introduces a "bind" keyword to its input syntax. This automatically transmits changes to an associated state variable. A Svelte version of the above thus looks like this:
<script>
let myProperty = "";
</script>
<input bind:value={myProperty} />
The bind keyword is also used to enable you to create two-way communication between parent and child components. This is a powerful feature.
An interesting feature of Svelte is that it encourages you to use forms and server-side processing for input handling. Thus it's perfectly permissible in Svelte to launch a client-side function like this:
<script>
let myProperty = "";
function commitChange() {
// Use the global myProperty variable to update server storage
}
</script>
<span>myProperty = </span><input bind:value={myProperty} />
<button on:click={commitChange}>Commit Change</button>
/>
Svelte docs correctly insist that interactions like this are better handled by forms and server-side processing in a +page.server.js
file. Here the validation and submission of the user input can be safely protected from the sort of interference possible in client-based code. Here also, any subsequent processing can be performed with maximum efficiency.
To implement this view, Svelte provide a neat automatic link between a form reading data on a +page.svelte
and a function handling the processing of that data in the associated +page.server.js
file. Here's an example:
src/routes/login/+page.svelte
<form method="POST">
<span>myProperty = </span><input name="myProperty">
<button>Commit Change</button>
</form>
src/routes/login/+page.server.js
export const actions = {
default: async (event) => {
// TODO handle the processing for the input read by the form on +page.svelte
}
};
Note that no Javascript has been used in the form - no "on click" or "on submit", for example. The linkage has been established entirely through "Svelte magic".
In practice, of course, a +page.svelte
file is likely to want to be the source of multiple "actions". See Svelte Form Actions for details of how Svelte manages this. (Note that Svelte docs are organised under two URLs: kit.svelte.dev
for framework topics like routing and svelte.dev
for elements of the language itself)
Finally, to conclude this section, suppose you wanted users to be able to call on the service of an action by referencing it directly through a javascript "fetch" (or, at its simplest by launching a parameterised url via the browser - eg https:// mySite/myPage?param1=3
etc). This is where you would use a +server.js
file to create an API "endpoint" function. Firebase users might well use such an arrangement where they had previously used a Firebase function. Not the least advantage of this would be that testing and debugging could be done in the Sveltekit server rather than the Firebase emulator.
Components
- 1-way bindings
Each +page.svelte
file defines a component, and you mark variables declared here as "props" - ie make them accessible to "consumers" of the component - by adding the export
keyword to their declarations. So, if you're still wondering how a +page.svelte
file gets its data from +page.server.js
- this is how it's done. A +page.svelte
file wanting to receive "load" data from its +page.server.js
(or +page.js
) file just needs to put something like the following in its <script>
section:
export let data
Svelte magic will then ensure that if the 'load' function exported by the +page.server.js
file returns an object such as {name: "Benny", } then +page.svelte
will find that data.name
contains "Benny".
But suppose that the +page.svelte
file wanted to reference its own child component. How would that child be configured and linked to its parent?
Let's say that this child component needs parameters param1
and param2
to build its output. It's usually most convenient to store the component in a src/lib
folder - say src/lib/MyComponent.svelte
- and its content might then look something like:
<script.
export let param1;
export let param2;
</script>
... Svelte html referencing param1 and param2 ...
The parent +page.svelte
could then engage the component like this:
<script>
import { MyComponent } from "$lib/myComponent.svelte";
let param1 = "Type A";
let param2 = 10;
<script/>
<h1>
Component-Access Demo Page
</h1>
<MyComponent param1={param1} param2={param2} />
This arrangement will be perfectly familiar if you've previously used React. Also, as with React, once the child component has received a parameter passed in this way, it's free to modify it at will - the parent will be oblivious of the change. This arrangement is known as a one-way binding
Note the "$" shortcut used in the child component import declaration. Svelte works out the actual route automatically, saving you working out all the conventional "./" and "//" designators.
- 2-way bindings
Suppose the child component creates a form designed to serve both Create and Edit parents. In this case, it needs to be able to receive parameters supplying initial values for form elements and return the user inputs. Data is thus required to pass both down and up the component hierarchy.
In React you might have used a Context here. Svelte provides several alternatives, each with different characteristics, but the simplest is a bind:
keyword applied to the parameter references in the parent's component call.
Let's say we've created the following shared input layout in a lib/MySharedInputPanel.svelte file:
<script>
export let input1;
export let input2;
</script>
<div>
<span>Input1 value<span/>
<input type="text" bind:value={input1} />
<span>Input2 value<span/>
<input type="text" bind:value={input2} />
</div>
This creates a default export for a MySharedInputPanel component that a routes/editrecord/+page.svelte
file can import and use to build an edit 'form' for the named, exported inputs as follows:
<script>
import MySharedInputPanel from "$lib/componentsMySharedInputPanel.svelte";
let input1 = "Initial Text";
let input2 = 3;
</script>
<h1>Edit Record</h1>
<div>
<MySharedInputPanel bind:input1 bind:input2 />
<p>Latest values: Input1 = {input1} : Input2 = {input2}</p>>
</div>
If you try this out yourself, you'll see that the shared panel initially displays the input1 and input2 values specified in the edit record route and that the parent editrecord/+page.svelte
view of these changes when new values are entered. This confirms that the route is automatically rerendering when changes occur.
A routes/createrecord/+page.svelte
could use the same form component to collect inputs and create a record.
Note that, for brevity, I've used neither the <form>
nor <label>
elements that good practices would require. My code also assumes that the parents and child use the same variable names. See Svelte's "component directive" docs for a more general version of the bind: syntax.
Svelte store
Sometimes you'll find that you need a global state object to serve components that are not hierarchically related. Svelte store
is designed to meet this need - and much more besides. A store
, particularly a writable store (several variants exist), is an object with a set()
method that allows you to set new values for its content.
Why is this any different from good old Chrome localStorage
? Potential readers of the store register their interest via a subscribe() method that sets a callback function that notifies them whenever the store value changes - Svelte store is reactive!
I'll only describe the "writable" version of Svelte store here - the one that I, myself, have found most useful. In brief:
A writable store is created using the following code pattern:
import { writable } from "svelte/store";
let myStoreContent = {myStoreField:"Welcome to SvelteStore"}
export const myStore = writable(myStoreContent);
Now, if a component (or, indeed, a regular JavaScript module) needs to know if anything changes in the store, it can register its interest by supplying a callback function with a subscribe
command along the lines of the following:
let currentMyStoreFieldValue
myStore.subscribe((store) => {
currentMyStoreFieldValue = store.myStoreField;
});
Note that, once you've created this arrangement, you never have to explicitly read your store, its current value remains available in your page's currentMyStoreFieldsValue
for the duration of a browser session. A typical arrangement will be to use a stores.js
file (usually positioned at the root of the src folder) to create a store
object and then import this wherever it's needed. The javascript module system will ensure that the store is initialized the first time it is imported during the browser session.
To update the store's value you can use the set()
method again, but it may often be more convenient to use Svelte store's update()
method as this gives you access to the current value of the store. Check out Svelte's docs at Writable Stores if you'd like to see an example (and much else besides).
Debugging techniques for SvelteKit webapps
So, you've blasted your code through the myriad of shrieks and groans displayed by VSCode and Vite when they find trivial errors like undeclared variables and missing files. Now you've got a webapp that runs - but all it does is sit and sulk. What now?
The first thing, of course, is to open the browser's Inspector and see what the Console tab offers - there's almost always something to get a handle on here. Oh dear - CORS or Permissions errors etc. What now?.
Debugging a SvelteKit webapp tends to be a lot more complicated than, say, a React codebase because so much of your code is likely to be running "server-side".
If you've followed this series rigorously, you'll likely now be a world expert in debugging client-side code through the Browser's Inspector. This, above all else, has made my own coding life the purest pleasure and you'll find it continues to work just fine in client-side +page.svelte
files.
But when you try to set breakpoints in a SvelteKit +page.server.js
file, you'll find that the browser won't let you. Think about this for a moment. How can it? Code here runs either in the development server launched by npm run dev or in the live server where you've just deployed a package. This is just a black hole as far as the browser is concerned. So what do you do now?
While you're still working on development code, VSCode offers you browser-like debugging facilities directly within your IDE. Personally. I've found these clunky. In practice, while I was still getting used to Svelte, I tended to rely simply on sprinkling console.log
instructions over my code. Output for these appears in whatever terminal session you've used to launch your run npm dev.
However, with persistence, this is what works for me in VSCode:
- Open the
+page.server.js
file you want to debug and set a breakpoint at a point you'd like to inspect by clicking on the pale red dot that appears at the beginning of its source line when you mouse over it. The pale red dot now changes to bright red. - In the VSCode command palette (shortcut "ctrl shift p") choose
Debug: Attach to node process
and get a list of projects in your workspace. Select the one you plan to debug. - Note that the terminal session that now starts is labelled as "Javascript Debug Terminal". Type
npm run dev
into this terminal and note how the usual Vite output that appears includes an additional "Debugger attached" line. Note also how VSCode's activity bar has turned orange - Now mouse over the
http://localhost
address displayed by Vite and launch the webapp with a "ctrl-click". Your webapp now opens in a popup window. - You'll now find that the editor page for your
+page.server.js
file has become an active debugging session with execution halted on the first breakpoint. A panel at the top of the editor window displays the familiar "advance to next breakpoint" etc buttons, and mousing over field names will reveal tooltips that display their values. As before, if a debug session is halted on a variable assignment statement, you'll only see the result of this when you advance to the next statement. - Terminate the debug session by mousing over the icon at the far end of the debug controls panel. This toggles between "disconnect" and "stop" actions when you press the "alt" key. Click this when it displays "disconnect" and the orange VSCode activity bar will turn blue again. If you want to resume debugging, "ctrl click" on the webapp URL in the debugging terminal window again (which will stay active until you explicitly "bin" it)
But what if you've got a problem that can only be investigated on the live host?
Here, the code of your live Svelte webapp is largely invisible except through the use of specialised tools. Opening the Inspector and clicking on the Sources tab will list the various elements of your webapp's "package" in the "Page" hierarchy. But these are no help since they are now "minified" and abstracted into total obscurity. When you were working on the development server, these would have been turned back into meaningful files through the services of your source maps - but these are no longer available when you're working on a live package (unless you're prepared to override the default Vite config settings and expose your deployed live app to public scrutiny). And while the "workspace" hierarchy (which you might have used in the past to manipulate your source code) still duly lists your local code, you can't set breakpoints on any of it. So, back to seeding your code with logging messages and re-deploying the webapp.
But now there's a problem. Where would you go to look for log messages launched by a live webapp? The answer, for a Google Cloud webapp is the "Google Cloud Logs Explorer" console.
This, on first acquaintance, is also quite a clunky brute. But, with experience, you'll appreciate that (rather like the browser's Inspector) it's a fine piece of software engineering. It provides infinitely adaptable tools in one neat package. Embrace it!
Building and Deploying a SvelteKit webapp to the Google Cloud
If your webapp had been using React, as a Firebase developer you'd know that the next step is to use npm run build
to create a runtime version of your code, followed by a firebase deploy
to upload this to Google Cloud's Firebase hosting servers.
But if you're using Svelte there's a problem. If your Sveltekit webapp uses server-side code to achieve its effects, you will need hosting for a Svelte server. Firebase hosting only serves webapp pages. You need a different type of hosting altogether - one that effectively lets you mimic the operation of the local Svelte server you've been running on your PC.
Once you start looking closely, you'll discover that the Google Cloud provides a bewildering variety of facilities in this area. Typically, you might be interested in Google's "App Engine" and "Cloud Run" services.
SvelteKit docs at Building your App describe a build process that creates a "yaml" file that "provides an optimized production build of your server code and your browser code" serviced by an "adapter" to run on your specific environment (eg Google Cloud Run). But where would you find the necessary "adapter" and how would you choose the "specific environment"?
Although Svelte provides official adapters for numerous environments (eg Netlify and Vercel) this list unfortunately doesn't include the Google Cloud. But "community" developers have stepped in and the one I've been using is called svelte-adapter-appengine
, courtesy of Jonas Jongejan (HalfdanJ). This targets the Google Cloud App Engine environment (as described at svelte-adapter-appengine README) and so my requirement list is complete. Here's the procedure.
Install the package as a development dependency:
npm install --save-dev svelte-adapter-appengine
Update your svelte.config.js to use the adapter as shown below:
/svelte.config.js
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
import adapter from "svelte-adapter-appengine";
/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: vitePreprocess(),
kit: {
adapter: adapter(),
},
};
export default config;
Build your application:
npm run build
Deploy your application to App Engine:
gcloud app deploy --project <CLOUD_PROJECT_ID> build/app.yaml
Output from the deployment process will provide a url for your deployed webapp (eg https://myProject.nw.r.appspot.com). Feed that to the browser and be electrified by the shockingly fast response of your Sveltekit webapp.
Postscript
I hope you've enjoyed reading this post. Check out NgateSystems for an index to the whole series and a super-useful keyword search facility.
I'm conscious that I've only scratched the surface of what Svelte has to offer. This post has covered the basics and should be enough to enable you to develop a functional database access/maintenance system, but there's lots more to learn. Make sure to check out Svelte docs for more information. Good luck with your coding. I, for one, shall certainly be using Svelte in future!
Posted on July 16, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.