Skip to content

davec-cpu/Svelte-Auth

Repository files navigation

Tài liệu svelte

1. Routing

+page.svelte

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)

+page.js

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');
};   

+page.server.js

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

+error

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

layout

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

+layout.svelte

Một file layout sẽ có dạng

<script>
    let { children } = $props();
</script>

{@render children()}

+layout.js

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

+server

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.

2. Tải dữ liệu

+page.data

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>  

Universal load và server load

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.

Sử dụng data từ URL

Svelte cung cấp các thuộc tính url, paramsroute cho sự kiện load

url

Là một đối tượng được của URL, gồm các thuộc tính như origin, hostname, pathname, searchParams.

route

Lưu tên của thư mục route hiện tại

Cookie

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)
    };
};

3. Form

+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ẻ

Action mặc định

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>

Action có tên

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">

Cấu trúc chi tiết của một action

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

Cải thiện trải nghiệm người dùng

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
        };
    }}
>

4.Tùy chọn trang

SSR (Server-side Rendering)

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;

CSR (Client-side Rendering)

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)

prerender

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;

5. Quản lí state

1. Tránh dùng state chung trên server.

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

2.Tránh dùng side-effect trong load

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());
};

3. Context API

Để 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>

6. Routing nâng cao

trang 404:

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

Tham số tùy chọn

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[lang]/home sẽ truyền tới cùng một trang. Để làm vậy, thay [lang] bằng [[lang]]

Nhóm các trang

(group)

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

7. Hooks

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 server

Các hook sau có thể thêm vào trong /src/hook.server.js:

handle

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

locals

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
    };
};

handleFetch

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.

Cách hoạt động:
  • Khi người dùng gọi fetch() trong load, action, endpoint hoặc handle, 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 dùng chung

Các hook sau được dùng cho cả server src/hooks.server.ts lẫn client src/hooks.client.ts

handleError

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.message có thể chứa thông tin nhạy cảm, message là 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>

init

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 toàn cục

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)

reroute

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
};

8. Tùy chọn cho thẻ <a>

data-sveltekit-preload-data

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)

data-sveltekit-preload-code

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

data-sveltekit-reload

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>

data-sveltekit-replacestate

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>

9.Snapshot

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ũ

10. Auth

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ì

Authentication

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.

Kiến trúc

Client -> Endpoint (+server.ts) -> Service layer -> Data Access layer -> Database

Tầng Giao tiếp (Endpoint)

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

Tầng Nghiệp vụ (Service layer)

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

Tầng truy cập DB (Data Access layer)

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

Thêm tailwind

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

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:

  1. Transpile, minify code.
  2. Á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.

About

Phần login, đăng kí, logout cho svelte

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published