的特色表示還沒寫得夠白話,不放首頁。
#}
{% set features = [
("零建置",
"沒有 package.json、沒有 node_modules、沒有 watcher。stylesheet 在 fjkit 發佈時編譯,不在你的 app 執行時編譯。"),
("顏色只有一個開關",
"每個顏色都是 token。換品牌是一份 stylesheet,狀態色不跟著變,所以綠色仍然代表完成。"),
("htmx 端點同時是 JSON API",
"渲染 partial 的路由對 htmx 回 HTML,對其他呼叫者回 response model 的 JSON。"),
] %}
{% block content %}
{% call stack(8) %}
{% set intro %}
給 FastAPI 的頁面、表格、表單、導覽與 htmx 局部更新,以 Jinja macro 寫在路由旁邊。
{% endset %}
{{ page_header("給 FastAPI 的快速 UI", intro) }}
{#
先講問題,再講特色。
這一頁原本開頭就是三條特色,而特色只對「已經有這個問題」的讀者有意義。兩段,
點名 FastAPI 作者今天實際會走的兩條路,以及各自的代價,讓「零建置」是在回答
某件事,而不是憑空的形容詞。
兩條路都不指名產品。跟具名工具比較的頁面,會因為別人發版而過期。
#}
{% call prose() %}
一個需要真正介面的 FastAPI 服務,通常只有兩條路。自己寫 Jinja 再手刻 CSS,
於是你多了一套本來沒打算做的設計系統。改用前端框架,於是你多了一套建置、
一種語言,以及第二個要部署的東西。
fjkit 是第三條路。介面是寫在路由旁邊的 Jinja macro,stylesheet 在 fjkit 發佈時
就編譯好了——所以你的專案不會出現 package.json,改模板也沒有任何
東西要重新 build。
{% endcall %}
{#
先給證據,再給說法。一個 UI 套件的首頁如果沒有任何它產出的畫面,就是要讀者
憑信任。
兩個檔案,跟著主題切換。說明文字裡的主張包含深色模式,一張圖只能證明一半。
代價記在 `assets/brand.css` 的 GAP 2。
#}
{% set shot_caption %}位於 examples/fjkit-demo 的示範。導覽、深色模式、可篩選表格、會驗證的表單與 htmx 局部更新——而那個 app 裡沒有任何一個 .css 或 .js 檔。{% endset %}
{% call stage("這一頁是 kit 畫出來的", shot_caption, padded=false) %}
{% endcall %}
{{ defs("主要特色", features) }}
{# ============================================================== 01 #}
{% set i01 %}安裝一個套件,寫一個 handler 與一份模板,執行 app。{% endset %}
{% call lesson("start", "01", "開始使用", i01) %}
{% call prose() %}
需要 Python 3.13 以上。
{% endcall %}
{{ code("uv add fjkit", "bash", label="1 · 安裝") }}
{{ code("from pathlib import Path
from fastapi import FastAPI
from fjkit import FjkitConfig, mount_fjkit, render
from pydantic import BaseModel
app = FastAPI()
mount_fjkit(app, FjkitConfig(template_dir=Path(__file__).parent / \"templates\"))
class Overview(BaseModel):
done: int
@app.get(\"/\")
@render(\"overview.html\")
def overview() -> Overview:
return Overview(done=18)", label="2 · app/main.py") }}
{% call prose() %}
mount_fjkit 供應 stylesheet 與內附的 JavaScript,並建立 Jinja environment。
@render 寫在 @app.get 下方,把回傳 model 的欄位交給模板。
{% endcall %}
{{ code("{% extends \"ui/shell.html\" %}
{% from \"ui/layout.html\" import page_header, grid %}
{% from \"ui/data.html\" import stat %}
{% block content %}
{{ page_header(\"Overview\", \"How the board is doing\") }}
{% call grid(cols=4) %}
{{ stat(\"Done\", done, tone=\"success\", icon_name=\"check\") }}
{% endcall %}
{% endblock %}", "jinja", label="3 · app/templates/overview.html") }}
{{ code("uv run fastapi dev app/main.py", "bash", label="4 · 執行") }}
{% call prose() %}
開啟 http://127.0.0.1:8000。頁面有 header、深色模式切換與一張統計卡。
模板裡沒有 CSS class,也沒有顏色:macro 接受具名選項,例如 tone="success",
由內附的 stylesheet 負責呈現。
把 uv run fjkit check app/templates 加進 CI。模板使用詞彙表以外的 class
或顏色時,它會失敗。
{% endcall %}
{% endcall %}
{# ============================================================== 02 #}
{% set i02 %}會寫後端、需要一套介面、不想碰前端工具鏈的人。{% endset %}
{% call lesson("who", "02", "給誰用", i02) %}
{% call prose() %}
適用對象:已經在寫 FastAPI,需要導覽、深色模式、一張可篩選的表格,以及一個會驗證的表單。
若要使用 React、Vue 或 Svelte,請直接用它們:fjkit 在伺服器端渲染,沒有前端元件模型。
{% endcall %}
{{ defs("預期成本", [
("第一頁", "約二十行,如 01 所示,沒有建置步驟"),
("一個後台畫面", "列表、篩選、新增表單與 htmx 更新,不寫 CSS,也不寫 JavaScript"),
]) }}
{% endcall %}
{# ============================================================== 03 #}
{% set i03 %}一個 wheel、兩個執行期依賴,不需要自行編譯任何東西。{% endset %}
{% call lesson("box", "03", "wheel 裡裝了什麼", i03) %}
{#
三個數字來自 `css`,由 `build.py` 量測本站連到的那份 stylesheet。
手寫過一次,而且是錯的。
#}
{% set css_line %}編譯好的 stylesheet,每個風格包一份。{{ css.raw_kb }} KB 原始、{{ css.gzip_kb }} KB gzip;超過 {{ css.budget_kb }} KB 時 build 失敗。一頁只載一份。{% endset %}
{% call grid(cols=2, gap=4) %}
{{ defs("套件裡帶的", [
("templates/ui/", "macro:版面、按鈕、資料、表單、表格、分頁、導覽、側欄、回饋、圖示與文件外殼"),
("static/dist/fjkit-.css", css_line),
("static/vendor/", "htmx 與 Basecoat 的 JavaScript,版本釘死,直接從套件裡供應"),
("1,767 個圖示", "Lucide,按名字內嵌。沒有 icon font、沒有 sprite 請求、執行期沒有 JS。"),
]) }}
{{ defs("你會用到的全部介面", [
("FjkitConfig、mount_fjkit", "接線,每支 app 一次"),
("@render", "指名模板;handler 只回資料"),
]) }}
{% endcall %}
{% call prose() %}
執行期依賴:FastAPI 與 Jinja2。
{% endcall %}
{% endcall %}
{# ============================================================== 04 #}
{% set i04 %}依你要做的事選頁面。{% endset %}
{% call lesson("next", "04", "接下來去哪", i04) %}
{% set n_learn %}上手——app 怎麼接線、htmx 局部更新怎麼運作、表單錯誤、訊息、換品牌與 fjkit check。先讀 01–05。{% endset %}
{% set n_components %}元件——每支 macro 的實際渲染,附上產生它的呼叫{% endset %}
{% set n_cheatsheet %}速查表——所有簽名、外殼 block、模板全域變數與 htmx 屬性,集中在一頁{% endset %}
{% set n_plugins %}外掛——登入、flash 訊息、圖表、API 主控台與 admin,以及如何自己寫外掛{% endset %}
{% set n_arch %}架構——五個已定案的決定,用圖說明:套件邊界、eject、build 發生的時機、一份 partial 兩個入口,以及守門迴圈{% endset %}
{% set n_why %}為什麼用——其他做法的代價,以及這個做法換來什麼又放棄什麼{% endset %}
{{ defs("這個站上", [
("決定到底要不要用它", n_why),
("先看清楚它的形狀再決定要不要用", n_arch),
("做出第一個真正的畫面", n_learn),
("找一個元件", n_components),
("查一個參數", n_cheatsheet),
("加上登入、圖表或 admin", n_plugins),
]) }}
{% endcall %}
{% endcall %}
{% endblock %}