Một file +page.svelte định nghĩa một trang của app. Mặc định, các trang sẽ được render ở server (SSR), và với các điều hướng sau đó, sẽ render ở client (CSR)
File này sẽ là nơi chuẩn bị dữ liệu cho page trước khi render. Chạy ở cả client và server
import { error } from '@sveltejs/kit';
import type { PageLoad } from './$types';
export const load: PageLoad = ({ params }) => {
if (params.slug === 'hello-world') {
return {
title: 'Hello world!',
content: 'Welcome to our blog. Lorem ipsum dolor sit amet...'
};
}
error(404, 'Not found');
};
Nếu như không muốn chạy hàm load ở client, cho các trường hợp truy cập những dữ liệu bảo mật, như DB, API keys, có thể dùng +page.server.js thay cho +page.js
Nếu như có lỗi xảy ra trong khi chạy load, Svelte sẽ render một trang lỗi mặc định. Có thể tùy chỉnh lại trang này cho từng route, ở +error.svelte
Sẽ có những thành phần trong website được dùng lại giữa các trang. Thay vì lặp lại những thành phần này trong +page.svelte, ta có thể để vào trong một layout
Một file layout sẽ có dạng
<script>
let { children } = $props();
</script>
{@render children()}
Tương tự như +page.svelte, +layout.svelte cũng có thể lấy ra dữ liệu từ load trong +layout.js
import type { LayoutLoad } from './$types';
export const load: LayoutLoad = () => {
return {
sections: [
{ slug: 'profile', title: 'Profile' },
{ slug: 'notifications', title: 'Notifications' }
]
};
};
Tương tự, có thể sử dụng +layout.server.js
Ngoài các trang, người dùng có thể tạo route server (các API route/endpoint) bằng +server.js
File này cho phép toàn quyền kiểm soát phản hồi HTTP, không phải chỉ hiển thị UI như +page.svelte.. File này xuất ra các hàm tương ứng với các động từ HTTP, như GET, POST,.... nhận vào một tham số là RequestEvent và trả về một đối tượng Response.
Ví dụ, tạo file
\\myapp\src\routes\api\random-num\+server.ts
import { error } from "@sveltejs/kit";
import type { RequestHandler } from "@sveltejs/kit";
export const GET: RequestHandler = ({ url }: {url: URL}) => {
const min = Number(url.searchParams.get("min")) || 0;
const max = Number(url.searchParams.get("max")) || 100;
if(isNaN(min) || isNaN(max) || min >= max) {
throw error(400, "Invalid 'min' or 'max' query parameters.");
}
const random = min + Math.random() * (max - min);
console.log(random);
return new Response(JSON.stringify({ random }));
}
File này xuất ra một hàm GET, trả về một số ngẫu nhiên. Lúc này có thể dùng fetch tại api\random-num để gọi tới endpoint trên
//myapp\src\routes\get-random\+page.svelte
<script lang="ts">
import type { PageProps } from './$types';
import type { RandomNumResponse } from '$lib/types/api';
let { data }: PageProps = $props();
let clientRandom = $state<number | null>(0);
async function fetchRandom() {
const res = await fetch('/api/random-num?min=10&max=20');
const response = await res.json();
const result: RandomNumResponse = response;
clientRandom = result.random;
}
</script>
<h1>Page Server Data:</h1>
<p>{data.serverMessage}</p>
<h1>Random Number from API:</h1>
<button onclick={fetchRandom}>Lấy số ngẫu nhiên</button>
<p>{clientRandom}</p>
Với những phương thức không được xử lí, xuất ra hàm fallback như sau.
//Cập nhật myapp\src\routes\api\random-num\+server.ts
export const fallback: RequestHandler = () => {
return new Response(
JSON.stringify(
{ message: "Method Not Allowedvsvdd"}),
{ status: 405, headers: { "Content-Type": "application/json" } }
);
}
Lúc này, các hàm POST, PATCH, DELETE, PUT sẽ được bắt.
Trong một số trường hợp, một layout có thể muốn truy cập dữ liệu một layout con. Chẳng hạn, thuộc tính title của một layout/page con. Thực hiện như sau:
<script lang="ts">
import { page } from '$app/state';
</script>
<svelte:head>
<title>{page.data.title}</title>
</svelte:head>
| Loại | Đặt trong file | Chạy ở đâu | Mục đích chính |
|---|---|---|---|
| Universal load | +page.js hoặc +layout.js | Cả server & client | Dùng để fetch API public, hoặc xử lý dữ liệu không nhạy cảm |
| Server load | +page.server.js hoặc +layout.server.js | Chỉ server | Dùng để truy cập DB, file hệ thống, hoặc dùng biến môi trường bí mật |
- Server load luôn chạy trên server.
- Universal load có thể chạy ở cả hai: Lần đầu vào trang (SSR) → chạy trên server. Sau khi trang đã tải, chạy trên server.
Svelte cung cấp các thuộc tính url, params và route cho sự kiện load
Là một đối tượng được của URL, gồm các thuộc tính như origin, hostname, pathname, searchParams.
Lưu tên của thư mục route hiện tại
Cookie là dữ liệu được trình duyệt lưu lại, và kèm theo mỗi request gửi tới server. Có thể thực hiện việc set và get cookie trong load:
import * as db from '$lib/server/database';
import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = async ({ cookies }) => {
const sessionid = cookies.get('sessionid');
return {
user: await db.getUser(sessionid)
};
};
+page.server.js có thể xuất ra các action, từ đó cho phép người dùng gọi tới POST sử dụng thẻ
Trong trường hợp đơn giản nhất, một trang khai báo action như sau:
import type { Actions } from './$types';
export const actions = {
default: async (event) => {
// TODO log the user in
}
} satisfies Actions;
-
satifies là toán tử kiểm tra kiểu, kiểm tra xem một biểu thức có phù hợp với kiểu được chỉ định hay không, thay vì ép kiểu của biểu thức đó thành kiểu chỉ định. Ví dụ :
const user = { name: 'Alice', age: 20, } satisfies Person;
-
Lúc này, TypeScript sẽ kiểm tra xem đối tượng user có thừa, thiếu trường, sai kiểu so với kiểu Person hay không.
Gọi tới action này:
<form method="POST">
<label>
Email
<input name="email" type="email">
</label>
<label>
Password
<input name="password" type="password">
</label>
<button>Log in</button>
</form>
Cập nhật +pages.server.ts:
import type { Actions } from './$types';
export const actions = {
default: async (event) => {
login: async (event) => {
// TODO log the user in
},
register: async (event) => {
// TODO register the user
}
} satisfies Actions;
Và gọi như sau:
<form method="POST" action="?/register">
Hàm action trong +page.server.ts nhận vào một đối tượng event, thuộc kiểu RequestEvent
event bao gồm:
-
event.request: Thông tin HTTP request (giống như trong Fetch API) -
event.cookies: Dùng để đọc / ghi cookie -
event.locals: Dữ liệu chia sẻ giữa các request (thường dùng để lưu user) -
event.params: Các tham số từ URL, ví dụ /user/[id] -
event.url: Đối tượng URL của request (chứa query string, pathname, v.v.) -
event.fetch: Dùng để fetch đến API khác, có sẵn cookie/session -
event.platform: Thông tin về môi trường deploy
use:enhance giúp chặn hành vi mặc định khi submit form, không reload toàn trang.
<script lang="ts">
import { enhance } from "$app/forms";
import type { PageProps } from "./$types";
let { form }:PageProps = $props();
</script>
<form method="POST" use:enhance>
<label>
Username
<input name="name" type="text">
</label>
<label>
Password
<input name="password" type="password">
</label>
<button>Log in</button>
</form>
Ngoài ra, có thể tùy biến để xử lí logic trước và sau khi gửi form
<form
method="POST"
use:enhance={({ formElement, formData, action, cancel, submitter }) => {
// chạy ngay trước khi gửi form
return async ({ result, update }) => {
// chạy sau khi có kết quả từ server
};
}}
>
SvelteKit sẽ chạy code trên server, render ra HTML, sau đó gửi lên client. Client sau đó sẽ thực hiện hydration (gắn Javascript) cho HTML đó.
Ưu:
- Trang được gửi lên có đủ HTML, tốt cho SEO.
- Load nhanh lần đầu
Nhược:
- Tăng tải cho server.
Để bật ssr, tại +page.server.js của trang mong muốn thay đổi:
export const ssr = true;
Server gửi HTML trống + bundle Javascript tới client. Lúc này, tại client mới thực hiện tải về dữ liệu cần thiết và sau đó render ra trang hoàn chỉnh
Nếu export const ssr = false; trang sẽ được render bằng csr (Client-side rendering)
Cho phép hoặc không cho phép SvelteKit sinh HTML của route đó tại thời điểm build (static site generation) thay vì mỗi lần request.
// src/routes/blog/+page.server.ts
export const prerender = true;
Server là stateless:
-
Server không lưu trữ thông tin phiên (session) hay dữ liệu riêng của client giữa các request.
-
Mỗi request từ client được xử lý độc lập, server chỉ dựa vào dữ liệu có trong request (ví dụ: headers, body, cookies) để quyết định phản hồi. Vậy nên nếu server lưu state trong biến chung, tất cả user sẽ dùng chung biến đó-> gây lộ dữ liệu.
Client là stateful:
- Lưu lại dữ liệu của riêng user
Vì lí do tương tự, tránh thực hiện side effect trong load Side-effect là các hành vi thay đổi các state có phạm vi ngoài hàm load Ví dụ:
import { user } from '$lib/user';
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ fetch }) => {
const response = await fetch('/api/user');
// Đây là side effect, vì nó thay đổi state user, state này nằm ngoài phạm vi hàm load
user.set(await response.json());
};
Để có thể truyền dữ liệu xuống mà không dùng global state, sử dụng context API. Cụ thể:
src/routes/+layout.svelte
<script lang="ts">
import { setContext } from 'svelte';
import type { LayoutProps } from './$types';
let { data }: LayoutProps = $props();
// Pass a function referencing our state
// to the context for child components to access
setContext('user', () => data.user);
</script>
Và sử dụng trong +page.svelte như sau:
<script lang="ts">
import { getContext } from 'svelte';
// Retrieve user store from context
const user = getContext('user');
</script>
<p>Welcome {user().name}</p>
Chú ý: Trang +error.svelte sẽ chỉ hiển thị khi mà có lỗi xảy ra trong quá trình chạy load, render page. Nó sẽ không hiển thị nếu không tìm thấy route. Ví dụ:
src/routes/
├ brothers/
│ ├ chico/
│ ├ harpo/
│ ├ groucho/
│ └ +error.svelte
└ +error.svelte
thì truy cập brothers\karl sẽ không hiển thị brothers/+error.svelte, bởi route này không tồn tại.
Để bắt những route không tồn tại, sử dụng [...path]. Tham số này sẽ bắt tất cả các route con trong một folder. Cụ thể:
src/routes/
├ brothers/
| ├ [...path]/
│ ├ chico/
│ ├ harpo/
│ ├ groucho/
│ └ +error.svelte
└ +error.svelte
Thì src/routes/brothers/[...path]/+page.svelte sẽ bắt được brothers\karl
Với route [lang]\home, tham số lang sẽ bắt buộc phải truyền vào. Đôi khi người dùng sẽ muốn tham số truyền vào route là tùy chọn, chẳng hạn home và [lang]/home sẽ truyền tới cùng một trang. Để làm vậy, thay [lang] bằng [[lang]]
src/routes/
│ (app)/
│ ├ dashboard/
│ ├ item/
│ └ +layout.svelte
│ (marketing)/
│ ├ about/
│ ├ testimonials/
│ └ +layout.svelte
├ admin/
└ +layout.svelte
Việc sử dụng (group) sẽ giúp gom các trang dùng chung layout, hay route cha lại, mà sẽ không tạo thêm /group trong url
Hook là các hàm toàn cục mà Svelte sẽ gọi tới khi xảy ra các sự kiện cụ thể
Các hook sau có thể thêm vào trong /src/hook.server.js:
Hook này sẽ chạy mỗi khi SvelteKit nhận được một request,
có thể xảy ra trong lúc app chạy, hoặc là prerender, và quyết định response. Hook nhận vào một object event đại diện cho request, và một hàm resolve sẽ render route của trang đó, và tạo một Response
Là một object được gắn vào event. Để tùy chỉnh dữ liệu tới request (được truyền vào +server.ts và hàm load của server )
B1: Gán dữ liệu vào locals trong hook
// src/hooks.server.ts
import type { Handle } from '@sveltejs/kit';
import { getUserInformation } from '$lib/server/auth';
export const handle: Handle = async ({ event, resolve }) => {
const sessionId = event.cookies.get('sessionid');
// Lưu dữ liệu user vào locals
event.locals.user = sessionId ? await getUserInformation(sessionId) : null;
const response = await resolve(event);
return response;
};
B2: Truy cập locals
// src/routes/dashboard/+page.server.ts
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ locals }) => {
// Truy cập dữ liệu user đã lưu trong hook
return {
user: locals.user
};
};
Là một hook cho phép can thiệp vào mọi fetch được gọi trong SSR hoặc prerendering, cho phép chỉnh sửa request hoặc response trước khi trả về component/page.
-
Khi người dùng gọi fetch() trong
load,action,endpointhoặchandle, SvelteKit sẽ đi qua hook handleFetch nếu có. -
Người dùng có thể:
- Thay đổi URL, headers, hoặc method của request. (Ví dụ như thêm token authen tự động)
- Thay đổi cách fetch được thực hiện (ví dụ gọi trực tiếp đến local server thay vì API public).
Cu thể:
// src/hook.server.ts
import type { HandleFetch } from '@sveltejs/kit';
export const handleFetch: HandleFetch = async ({ request, fetch }) => {
if (request.url.startsWith('https://api.yourapp.com/')) {
//Thực hiên các thao tác trước khi fetch
request = new Request(
request.url.replace('https://api.yourapp.com/', 'http://localhost:9999/'),
request
);
}
return fetch(request);
};
Các hook sau được dùng cho cả server src/hooks.server.ts lẫn client src/hooks.client.ts
Hook này nhận vào các tham số error, event, status, và message.
Sử dụng để:
- log ra lỗi
- Dev có thể tạo ra một phiên bản hiển thị lỗi thân thiện với người dùng.
Giá trị trả về từ
handleError, mặc định làmessage, được gán vào$page.error. Vìerror.messagecó thể chứa thông tin nhạy cảm,messagelà an toàn với người dùng.
Để thêm thông tin cho đối tượng $page.error, dev có thể tùy chỉnh bằng cách khai báo một interface App.Error. Ví dụ:
// src/app.d.ts
declare global{
namespace App{
interface Error{
message: string;
errorId: string;
}
}
}
export {};
Chú ý: để truy cập $page.error:
<script lang="ts">
import {page} from "$app/state";
</script>
<h1>
Hanlding {JSON.stringify(page.error)}
</h1>
Hàm này được chạy một lần, khi server được tạo hoặc khi app khởi chạy ở browser. import * as db from '$lib/server/database'; import type { ServerInit } from '@sveltejs/kit';
export const init: ServerInit = async () => {
await db.connect();
};
Các hook sau có thể được thêm vào src/hook.ts. Hook toàn cục chay trên cả server lẫn client, (không như hook dùng chung, chỉ chạy trên server hoặc client)
Hook này chạy trước khi một navigation diễn ra, đặc biệt là trong client-side navigation. Hook sẽ được gọi tới khi dev dùng <a> hoặc goto() để điều hướng tới một route khác. Điều này giúp dev có thể thay đổi, chặn, hoặc redirect route linh hoạt
// src/hooks.client.ts
import type { HandleReroute } from '@sveltejs/kit';
export const handleReroute: HandleReroute = async ({ event }) => {
console.log('Navigating to:', event.url.pathname);
// ví dụ: chặn người dùng chưa login truy cập /dashboard
if (!event.locals.user && event.url.pathname.startsWith('/dashboard')) {
return '/login'; // redirect về login
}
// Nếu không muốn redirect, trả về undefined hoặc event.url
};
Khi người dùng hover chuột lên một link, SvelteKit dự đoán rằng người dùng sẽ sắp truy cập vào link đó, và sẽ preload (load trước) dữ liệu của trang đích, giảm thiểu thời gian chờ cho người dùng.
<a data-sveltekit-preload-data="hover" href="/stonks">
Get current stonk values
</a>
Thuộc tính này có hai giá trị:
- hover: preload khi người dùng hover lên thẻ a.
- tap: preload khi người dùng click. Dùng để tránh false positive (người dùng hover nhưng lại không click link)
Tương tự như trên, nhưng data-sveltekit-preload-code chỉ preload JS/CSS của trang đích.
Có 4 giá trị:
- eager: Preload ngay lập tức sau khi link xuất hiện trong DOM.
- viewport: Preload khi link đi vào viewport (xuất hiện trên màn hình).
- hover: Preload khi hover vào link
- tap: Preload khi click vào link
Dùng khi muốn browser xử lí link (không xử lí theo cách của svelte - import code của page mới, update DOM mà không load toàn bộ trang). Link lúc này sẽ được load toàn trang (mặc định)
<a data-sveltekit-reload href="/path">Path</a>
Dùng khi điều hướng mà không muốn tạo bản ghi mới trong lịch sử duyệt web
<a data-sveltekit-replacestate href="/path">Path</a>
Các state DOM tạm thời, như vị trí scroll, nội dung của thẻ input, sẽ bị loại bỏ khi điều hướng từ trang này sang trang khác. snapshot có thể tạo một bản sao của DOM hiện tại, để có thể tái sử dụng sau. Cụ thể:
// +page.svelte
<script lang="ts">
import type { Snapshot } from './$types';
let comment = $state('');
export const snapshot: Snapshot<string> = {
capture: () => comment,
restore: (value) => comment = value
};
</script>
<form method="POST">
<label for="comment">Comment</label>
<textarea id="comment" bind:value={comment} />
<button>Post comment</button>
</form>
Khi người dùng điều hướng khỏi trang, hàm capture sẽ được gọi ngay lập tức, lưu lại giá trị cũ
Auth đề cập tới:
- Authentication (Xác thực): Xác minh người dùng là ai (login)
- Authorization (phân quyền): Xác định xem người dùng đó có thể và không thể làm gì
Luồng hoạt động của Authentication, sử dụng JWT token sẽ như sau:
B1: Người dùng gửi POST request gồm có credentials (Ví dụ như email, mật khẩu) tới api/login
B2: Endpoint api/login sẽ chạy ở server, và thực hiện các công việc sau:
- Đọc credentials từ request.
- Từ các thông tin này, tìm kiếm người dùng tương ứng trên csdl.
- Xác thực mật khẩu.
- Tạo một JWT token từ thông tin người dùng tìm được.
- Chuyển token trên thành HttpOnly cookie (HttpOnly cookie là loại cookie trong HTTP được thiết kế để chỉ có thể được truy cập bởi server, chứ không thể đọc hoặc sửa bởi Javascript trên trình duyệt).
- Trả về thông báo login thành công
B3: Tự động thêm cookie vào mọi request trong tương lai Vì JWT được lưu dưới dạng cookie, nên mỗi khi user
- Điều hướng tới một trang.
- Gọi
\api.
Thì trình duyệt sẽ tự động gắn Cookie: jwt=... vào header của request.
B4: Bảo vệ route/api.
- Kiểm tra xem cookie JWT có hợp lệ không.
- Nếu chưa có JWT token, điều hướng tới
/login. - Nếu không hợp lệ/hết hạn -> điều hướng tới
\login. - Nếu hợp lệ, giải mã token, cho phép request đi qua.
Client -> Endpoint (+server.ts) -> Service layer -> Data Access layer -> Database
Là cầu nối giữa client và logic của server.
Nhiệm vụ:
- Nhận HTTP request từ client (
GET,POST,...). - Parse dữ liệu (request.json(), params, form data,...).
- Gọi tầng service
- Xử lí try/catch
Không nên:
- Chứa logic nghiệp vụ
- Không query db trực tiếp
Nơi chứa quy tắc nghiệp vụ của ứng dụng.
Nhiệm vụ:
- Xác định quy trình xử lí logic
- Gọi tới tầng DAL
Không nên:
- Không trực tiếp nhận HTTP request hay trả response
- Không chứa code liên quan tới giao diện hay HTTP headers
Làm việc trực tiếp với DB. Nhiệm vụ:
- Truy vấn DB.
- Chuyển dữ liệu DB -> object typescript.
Không nên:
- Thực hiện logic quy trình
B1: Cài tailwind 3
npm install -D tailwindcss@3 postcss autoprefixer
B2: Tạo config
npx tailwindcss init tailwind.config.cjs -p
B3: Cấu hình tailwind
//tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./src/**/*.{html,js,svelte,ts}', // scan tất cả file trong src
],
theme: {
extend: {},
},
plugins: [],
};
B4: Thêm CSS
// src/app.css
@tailwind base;
@tailwind components;
@tailwind utilities;
B5: Import CSS vào layout
src/routes/+layout.svelte
<script lang="ts">
import "../app.css";
</script>
<slot />
Build app nghĩa là biến mã nguồn thành phiên bản chạy được thực tế. Khi build, code sẽ được transpile/minify.
-
Transpile: Việc biến mã nguồn từ ngôn ngữ này sang ngôn ngữ khác, hoặc các phiên bản khác của cùng ngôn ngữ, nhưng vẫn giữ lại logic. Ví dụ:
- TypeScript -> Javascript.
- Svelte .svelte → JavaScript + HTML + CSS.
-
Minify: Loại bỏ khoảng trống trong code, comment, đổi tên biến,... Ví dụ:
// before minify
function sayHello(name) { console.log("Hello " + name); }
// after minify
function a(b){console.log("Hello "+b)}
Trong svelte, quá trình build chia làm hai giai đoạn, và cả hai đều diễn ra khi dev chạy npm run build:
- Transpile, minify code.
- Áp dụng một adapter để tinh chỉnh đầu ra code sao cho phù hợp với môi trường muốn triển khai dự án.