在重啟北護課程查詢系統 Day 2 - Fastify-HTMX-EJS-Tailwind當中,筆者比較有興趣一點的是 HTMX,在 Day 3,我們就小小的玩一下,做一個 counter 吧!
安裝套件
在實作 counter 時,我們要把 count 給儲存起來,並渲染給 user,在這我們可以使用 session 達成這項任務
- 運行以下指令安裝 session 相關的套件
pnpm i @fastify/session @fastify/cookie
設定 session
.env
- session 需要 secret 以確保資料的真實性和完整性,所以這邊必須要在 .env 新增 session 用的 secret
SERVER_HOST="127.0.0.1"
SERVER_PORT=3000
SESSION_SECRET="請自行產生超過32字元的 secret 數值"
env-class.js
// 抓取 env 的設定
// 使用 env-var 來驗證以及針對數值轉換 env 的設定
import envVar from "env-var";
import dotenv from "dotenv";
dotenv.config();
export const ServerEnv = function () {
return {
host: envVar.get("SERVER_HOST").required().default("0.0.0.0").asString(),
port: envVar.get("SERVER_PORT").required().default(3000).asPortNumber(),
sessionSecret: envVar.get("SESSION_SECRET").required().asString()
};
};
server/plugins/session.js
import fp from "fastify-plugin";
import fastifySession from "@fastify/session";
import fastifyCookie from "@fastify/cookie";
import { ServerEnv } from "../env-class.js";
export default fp(async (fastify, opts) => {
fastify.register(fastifyCookie);
fastify.register(fastifySession, {
cookieName: "ntunhs-session-id",
secret: ServerEnv().sessionSecret,
cookie: {
path: "/",
secure: false
}
});
}, {
name: "f-session"
});
建立 counter 頁面
- 此章節主要在建立 counter 的 ejs 頁面
server/views/counter.ejs
<main class="container" id="main">
<div class="flex flex-wrap flex-1">
<h1 class="text-3xl block w-full">counter!</h1>
<p class="block w-full mb-5" id="p-count">count: <%= count %></p>
<button
type="button"
class="text-gray-900 bg-white border border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-100 font-medium rounded-full text-sm px-5 py-2.5 me-2 mb-2 dark:bg-gray-800 dark:text-white dark:border-gray-600 dark:hover:bg-gray-700 dark:hover:border-gray-600 dark:focus:ring-gray-700"
hx-post="/counter/increment"
hx-select="#p-count"
hx-target="#p-count"
hx-swap="outerHTML"
>
Increment
</button>
<button
type="button"
class="text-gray-900 bg-white border border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-100 font-medium rounded-full text-sm px-5 py-2.5 me-2 mb-2 dark:bg-gray-800 dark:text-white dark:border-gray-600 dark:hover:bg-gray-700 dark:hover:border-gray-600 dark:focus:ring-gray-700"
hx-post="/counter/increment-p"
hx-target="#p-count"
>
Increment - only paragraph
</button>
</div>
</main>
- button 所使用的樣式是從 flowbite 的 Button pills 內的第四個 light 複製而來
hx-post
代表會使用 POST 方法傳送資訊到賦予數值的 url- 第一個 increment
hx-select=”#p-count”
的意思是只選擇 POST /counter/increment 回傳的整體 HTML 中id 為p-count
的元素並替換#p-count
hx-target
則是透過 css selector 直接替換 HTML 裡面的內容
- 第二個 increment
/counter/increment-p
則只回傳 text,可以直接替換到 p-count
routes
server/modules/counter/index.js
import autoLoad from "@fastify/autoload";
import { join } from "desm";
/**
*
* @param {import("fastify").FastifyInstance} fastify
* @param {import("fastify").FastifyPluginOptions} opts
*/
export default async function (fastify, opts) {
fastify.register(autoLoad, {
dir: join (import.meta.url, "routes"),
options: {
prefix: opts.prefix
}
});
}
server/modules/counter/routes/counter.js
/**
*
* @param {import("fastify").FastifyInstance} fastify
* @param {import("fastify").FastifyPluginOptions} opts
*/
export default async function (fastify, opts) {
fastify.get("/", {
preHandler: async (request, reply) => {
if (!request.session.count && request.session.count !== 0) {
request.session.count = 0;
}
}
}, async (request, reply) => {
return reply.view("components/layout", {
page: {
file: "../counter.ejs",
data: {
count: request.session.count
}
}
});
});
}
server/modules/counter/routes/increment.js
- 回傳整體 counter.ejs 的 HTML
/**
*
* @param {import("fastify").FastifyInstance} fastify
* @param {import("fastify").FastifyPluginOptions} opts
*/
export default async function (fastify, opts) {
fastify.post("/increment", {
preHandler: async (request, reply) => {
if (!request.session.count && request.session.count !== 0) {
request.session.count = 0;
}
}
}, async (request, reply) => {
request.session.count++;
return reply.view("components/layout", {
page: {
file: "../counter.ejs",
data: {
count: request.session.count
}
}
});
});
}
server/modules/counter/routes/increment-p.js
- 僅回傳 p 元素內的 text
/**
*
* @param {import("fastify").FastifyInstance} fastify
* @param {import("fastify").FastifyPluginOptions} opts
*/
export default async function (fastify, opts) {
fastify.post("/increment-p", {
preHandler: async (request, reply) => {
if (!request.session.count && request.session.count !== 0) {
request.session.count = 0;
}
}
}, async (request, reply) => {
request.session.count++;
return reply.type("text/html").send(`count: ${request.session.count}`);
});
}
counter 最終成果
結語
今天玩了一點點的 HTMX 功能,去替換網頁中的元素,過程中作者對 hx-trarget
和 hx-select
都有點小困惑,可以看看這部影片 HTMX - hx-select and hx-select-oob Attributes in HTMX 了解一下別人是怎麼使用 hx-target、hx-select 和 hx-select-oob
參考資料
- https://flowbite.com/docs/components/buttons/#button-pills
- https://htmx.org/docs
- HTMX - hx-select and hx-select-oob Attributes in HTMX