# pardnchiu/QuickUI
> QuickUI 是邱敬幃 Pardn Chiu(pardnchiu)開發的零依賴虛擬 DOM 前端框架,原生 JavaScript、免建置。
依導覽順序收錄所有文件頁;每段開頭標示來源 URL。
---
Source: https://quickui.pardn.io/zh/index.md
> [!NOTE]
> 此 README 由 [SKILL](https://github.com/agenvoy/skill-readme-generate) 生成,英文版請參閱 [這裡](https://github.com/pardnchiu/QuickUI/blob/main/README.md)。
***
A ZERO-DEPENDENCY VIRTUAL DOM FRAMEWORK THAT RUNS WITHOUT A BUILD STEP
***
> 原生 JavaScript 前端框架,具備虛擬 DOM 差異更新、宣告式 HTML 模板與內建多語系切換
## 目錄
- [功能特點](#功能特點)
- [架構](#架構)
- [授權](#授權)
- [Author](#author)
## 功能特點
> `npm i @pardnchiu/quickui` · [完整文件](https://github.com/pardnchiu/QuickUI/blob/main/doc/doc.zh.md)
- **零相依免建置** — 以瀏覽器原生 API 實作虛擬 DOM(Virtual DOM)的 Diff/Patch,一個 script 標籤即可在既有頁面上使用。
- **宣告式 HTML 模板** — 資料插值、迴圈、條件分支、雙向綁定與事件綁定直接寫在 HTML 屬性上,模板即是頁面本身。
- **Proxy 響應式批次更新** — 深層資料變動自動觸發 300ms 防抖更新,並於下一個動畫影格只套用必要的 DOM 差異。
- **執行期 i18n 切換** — 語系可直接給物件或 JSON 檔網址,呼叫一次語系切換即重新渲染文字與屬性。
- **內建圖片懶載入與 SVG 內嵌** — 以 IntersectionObserver 延遲載入圖片並附載入動畫,外部 SVG 檔進入視窗時自動內嵌為可上色的節點。
## 架構
> [完整架構](https://github.com/pardnchiu/QuickUI/blob/main/doc/architecture.zh.md)
```mermaid
graph TB
A[HTML 模板 / render 函式] --> B[vDOM 建構]
D[Proxy 響應式資料] -->|變動 300ms 防抖| B
E[i18n 語系資料] --> B
B --> C[Diff 產生 Patch]
C -->|requestAnimationFrame| F[真實 DOM]
G[生命週期鉤子] -.-> B
F --> H[懶載入 / SVG 監聽器]
```
## 授權
本專案採用 [MIT LICENSE](https://github.com/pardnchiu/QuickUI/blob/main/LICENSE)。
## Author
Just [open an issue](https://github.com/pardnchiu/QuickUI/issues/new) to share an idea.
***
©️ 2024 [邱敬幃 Pardn Chiu](https://www.linkedin.com/in/pardnchiu)
---
Source: https://quickui.pardn.io/zh/getting-started.md
# 快速開始
本頁說明 QuickUI 的安裝方式,以及如何把第一個資料物件綁定到頁面上。
## 前置需求
| 項目 | 需求 |
|---|---|
| 瀏覽器 | 支援 ES2022、`Proxy`、`IntersectionObserver`、`structuredClone` |
| HTTP 伺服器 | 使用 `i18n` 外部檔或 `` 時必須透過 HTTP 載入;`file://` 下 `fetch` 會失敗 |
| Node.js | 只有從原始碼建置時需要 |
## 安裝
### CDN
```html
```
載入後全域會出現 `QUI`、`_`、`$` 三個名稱,詳見 [全域工具](/zh/global-helpers)。
### npm
```bash
npm i @pardnchiu/quickui
```
### ESM
```javascript
import { QUI } from "@pardnchiu/quickui";
```
`package.json` 的 `module` 指向 `dist/QuickUI.esm.js`,打包工具會把套件名稱直接解析到 ESM 版本。ESM 版本與 CDN 版本是同一份程式,只在結尾多匯出 `window.QUI`,所以匯入時仍會註冊全域名稱。
### 從原始碼建置
```bash
git clone https://github.com/pardnchiu/QuickUI.git
cd QuickUI
npm install
npm run build:once
```
`build:once` 先以 `tsc` 把 `src/` 合併成 `src/QuickUI.debug.js`,再用 terser 產出 `dist/QuickUI.js` 與 `dist/QuickUI.esm.js`。
## 第一個應用
```demo
{{ title }}
{{ description }}
```
`id` 指向既有元素,QuickUI 以該元素的內容作為模板;之後修改 `app.data.title` 會在約 300 ms 後重新渲染。
## 下一步
| 想做的事 | 頁面 |
|---|---|
| 了解模板語法 | [文字綁定](/zh/text-binding)、[條件渲染](/zh/conditional-rendering)、[迴圈渲染](/zh/loop-rendering) |
| 處理使用者輸入 | [事件綁定](/zh/event-binding)、[表單綁定](/zh/two-way-binding) |
| 查完整選項 | [建構子選項](/zh/constructor-options) |
---
Source: https://quickui.pardn.io/zh/architecture.md
# 架構
本頁以單張概覽圖說明 QuickUI 各模組的層級關係與原始碼位置。
## 系統概覽
```mermaid
graph TB
T["模板:id 指向的元素 / render()"] --> V["vDOM::for → :if → :path → {{ }}"]
D["data(Proxy)"] -->|"set 觸發"| L["Lifecycle:300 ms 去抖動"]
L --> V
I["i18n 語系資料"] --> V
V --> P["diff → Patch[]"]
P -->|"requestAnimationFrame"| R["實體 DOM"]
R --> O["Listener:lazyload / temp-svg"]
```
## 分層
| 層 | 原始碼 | 職責 |
|---|---|---|
| 入口 | `src/model`(`QUI.ts`) | 建立實例、載入 i18n、包裝 reactive data、套用 Patch、處理屬性與事件綁定 |
| 虛擬 DOM | `src/model`(`vDOM.ts`) | 把元素轉成 `tag`/`props`/`children`/`data`,展開 `:for`、`:if`、`:path`,替換 `{{ }}`,產生 Patch |
| 生命週期 | `src/model`(`Lifecycle.ts`) | 包住初始渲染與更新的時序,執行 `when` 鉤子,更新去抖動 300 ms |
| 工具函式 | `src/function` | `createReactiveObject`、`htmlParser`、`createElement`、`getElement`、`calc`、`dateFormat` 等 |
| 監聽器 | `src/listener` | `IntersectionObserver` 驅動的圖片延遲載入與 SVG 內嵌 |
| 共用常數 | `src/*.ts` | 全域別名、正規表示式、指令名稱、注入頁面的效果 CSS |
所有檔案由 `tsc` 依 `tsconfig.json` 的 `outFile` 合併為單一腳本 `src/QuickUI.debug.js`,沒有模組匯入;公開名稱透過 `window.QUI`、`window._`、`window.$` 掛到全域。
## 跨切原則
| 原則 | 實作 |
|---|---|
| 零依賴 | 只用瀏覽器原生 API(`Proxy`、`fetch`、`IntersectionObserver`、`requestAnimationFrame`) |
| 模板即 DOM | 模板是頁面上的 HTML,不需要編譯步驟;每次渲染都從原始模板的複本重建 vDOM |
| 資料快照 | 每次渲染以 `JSON.parse(JSON.stringify(data))` 取快照,函式、`Date`、`undefined` 不會進入模板 |
| 批次寫入 | Patch 一律在 `requestAnimationFrame` 內套用 |
## 延伸閱讀
- 渲染流程細節:[渲染流程](/zh/rendering-pipeline)
- 資料變更偵測:[響應式資料](/zh/reactive-data)
- 各模組展開圖:[doc/architecture.zh.md](https://github.com/pardnchiu/QuickUI/blob/main/doc/architecture.zh.md)
---
Source: https://quickui.pardn.io/zh/rendering-pipeline.md
# 渲染流程
本頁說明一次渲染從模板到實體 DOM 經過的步驟,以及 Patch 的套用順序。
## 初始化
| 步驟 | 行為 |
|---|---|
| 1 | 依 `id` 取得元素;沒有 `id` 但有 `render` 時,建立一個不在文件中的 `` |
| 2 | 複製元素作為原始模板,並以未處理的模板建立第一份 vDOM 作為比對基準 |
| 3 | 有 `render` 時,以其輸出取代模板內容 |
| 4 | 載入 `i18n`(字串值會 `fetch`),完成後才設定 `app.data`、`app.event`、`app.body` |
| 5 | 依 `option` 啟用 SVG 與 lazyload 監聽器,建立 `Lifecycle` 並執行初始渲染 |
找不到元素或兩者皆未提供時,主控台輸出錯誤,實例不渲染任何內容。
## 每次渲染
| 階段 | 行為 |
|---|---|
| 重建 vDOM | 從原始模板複本建立新 vDOM,資料取 `JSON.parse(JSON.stringify(app.data))` 快照 |
| 展開指令 | 依序處理 `:for` → `:if` → ``,再對 `:path` 插入的內容重跑 `:for` 與 `:if` |
| 替換文字 | 先替換 `{{ i18n.* }}`,再替換其餘 `{{ }}` |
| 比對 | 以子節點索引逐一比對新舊 vDOM,產生 Patch |
| 套用 | 在 `requestAnimationFrame` 內套用 Patch,接著移除殘留的 `:if` 屬性並處理 [渲染效果](/zh/effects) |
## Patch 類型
| 類型 | 觸發條件 | 動作 |
|---|---|---|
| `REMOVE` | 新樹缺少該位置的節點 | 由最深、最後的位置開始移除,避免索引位移 |
| `CREATE` | 舊樹缺少節點 | 建立元素並附加到父節點 |
| `APPEND` | 子節點數量增加 | 插入到對應索引位置 |
| `REPLACE` | 同位置標籤名稱不同 | 以新元素取代 |
| `PROP` | 每個新節點的每個屬性 | 依屬性前綴分派到 [屬性](/zh/attribute-binding)、[事件](/zh/event-binding)、[表單](/zh/two-way-binding) 處理 |
| `TEXT` | 同位置文字不同 | 覆寫文字內容 |
`REMOVE` 一律先於其他 Patch 執行。
## 邊界行為
| 情況 | 結果 |
|---|---|
| 清單在前端插入項目 | 比對沒有 key,以索引對齊,插入點之後的每個項目都會收到 Patch |
| 模板內的 `
```
三行寫入都會觸發更新;300 ms 內的多次寫入合併為一次渲染。
## 觸發條件
| 寫法 | 是否觸發 |
|---|---|
| `app.data.key = value`(值改變) | 是 |
| 巢狀屬性、陣列索引、`push` / `splice` | 是 |
| 寫入相同的值 | 否 |
| `delete app.data.key` | 否,Proxy 沒有攔截刪除 |
| `app.data = { ... }` 整個取代 | 否,取代後的物件不是 Proxy |
| `once: true` | 否,資料不包 Proxy,只渲染一次 |
## 存取時機
`app.data` 在 i18n 載入完成後才指派,即使沒有設定 `i18n` 也會延後到下一個 microtask。在 `new QUI()` 之後同步寫入的值會寫到暫存物件而遺失;請在 [`rendered` 鉤子](/zh/lifecycle) 或事件處理函式中讀寫。
## 模板取值規則
| 規則 | 說明 |
|---|---|
| 路徑 | 以 `.` 存取巢狀值,例如 `{{ user.name }}` |
| 作用域 | 先找目前 `:for` 項目的資料,找不到再找根資料 |
| 找不到值 | `{{ }}` 原樣保留在畫面上 |
| 可用型別 | 字串、數字、布林、陣列、純物件;函式與 `Date` 會在快照時遺失 |
---
Source: https://quickui.pardn.io/zh/text-binding.md
# 文字綁定
本頁說明如何用 `{{ }}` 把資料寫進文字,以及用 `:html` 插入 HTML。
## `{{ value }}`
```demo
{{ title }}
{{ user.name }} · {{ LENGTH(items) }} items
```
| 規則 | 說明 |
|---|---|
| 可用字元 | 英數、底線、`.`;也可包一層 [內建函式](/zh/built-in-functions) |
| 空白 | `{{title}}` 與 `{{ title }}` 皆可 |
| 找不到值 | 原樣保留 `{{ title }}` |
| 多語系 | `{{ i18n.key }}` 讀取目前語系,見 [i18n](/zh/i18n) |
| 位置 | 只在文字節點中替換;一般屬性內的 `{{ }}` 不會處理,屬性請用 [屬性綁定](/zh/attribute-binding) |
## `:html`
```demo
```
`:html` 把值寫入 `innerHTML`;找不到對應資料時寫入屬性值本身。內容不經過任何過濾,不要放入未經處理的使用者輸入,以免造成 XSS。
---
Source: https://quickui.pardn.io/zh/built-in-functions.md
# 內建函式
本頁列出可在 `{{ }}` 中使用的五個函式與各自的限制。
## 函式一覽
| 函式 | 語法 | 結果 |
|---|---|---|
| `LENGTH()` | `{{ LENGTH(items) }}` | 陣列或字串的長度;物件回傳 key 數量 |
| `CALC()` | `{{ CALC(price * 1.05) }}` | 資料值與數字常數運算 |
| `UPPER()` | `{{ UPPER(name) }}` | 轉大寫 |
| `LOWER()` | `{{ LOWER(name) }}` | 轉小寫 |
| `DATE()` | `{{ DATE(timestamp, YYYY-MM-DD HH:mm) }}` | 格式化 UNIX 時間戳(秒) |
函式名稱必須大寫,且一次只能包一層,不能巢狀。
## `CALC()`
```demo
{{ CALC(price * 1.05) }}
{{ CALC(count + 1) }}
```
支援的運算子:`+`、`-`、`*`、`/`。
## `UPPER()` / `LOWER()`
值必須是字串;值為 `null` 或不存在時會丟出 `TypeError` 並中斷該次渲染。
## `DATE()`
```demo
{{ DATE(now, YYYY-MM-DD HH:mm:ss) }}
```
| 代碼 | 輸出 |
|---|---|
| `YYYY` / `YY` | 四位/兩位年份 |
| `MM` / `M` | 補零/不補零月份 |
| `DD` / `D` | 補零/不補零日期 |
| `HH` / `H` | 24 小時制 |
| `hh` / `h` | 12 小時制 |
| `mm` / `m`、`ss` / `s` | 分、秒 |
| `SSS` | 毫秒 |
| `A` / `a` | `AM`/`PM`、`am`/`pm` |
| `ddd` | 星期;瀏覽器語言含 `zh` 時為「星期一」,否則為英文縮寫(Mon) |
格式字串只接受英數、空白、`-`、`:`、`,`;含 `/` 或 `.` 時不會被辨識為 `DATE()`,`{{ }}` 原樣保留。
---
Source: https://quickui.pardn.io/zh/conditional-rendering.md
# 條件渲染
本頁說明 `:if`、`:else-if`、`:else` 的寫法、可用的比較運算子與判斷規則。
## 基本用法
```demo
```
| 指令 | 說明 |
|---|---|
| `:if` | 開始一組條件 |
| `:else-if` / `:elif` | 必須緊接在同層前一個條件元素之後 |
| `:else` | 前面條件都不成立時顯示 |
同一組中只保留第一個成立的元素;都不成立且沒有 `:else` 時,整組移除。單獨出現、前面沒有 `:if` 的 `:else-if` 或 `:else` 會被當成一般元素永遠顯示。
## 比較運算子
| 運算子 | 比較方式 | 例 |
|---|---|---|
| 無 | 值的布林值 | `:if="isLogin"` |
| `==` / `===` | 轉成字串後相等 | `:if="status == active"` |
| `!=` / `!==` | 轉成字串後不相等 | `:if="status != disabled"` |
| `>` / `<` | 轉成數字比較 | `:if="count > 0"` |
| `>=` / `<=` | 轉成數字比較 | `:if="count <= 100"` |
## 特殊比較值
| 值 | `==` 時成立條件 | `!=` 時成立條件 |
|---|---|---|
| `null` | 值為 `null` 或不存在 | 值存在 |
| `true` | 值為 truthy | 值為 falsy |
| `false` | 值為 falsy | 值為 truthy |
| `empty` | 值為空字串或不存在 | 值非空 |
## 判斷規則
| 規則 | 說明 |
|---|---|
| 右側 | 一律當成常數,不會去讀資料;`count > max` 是和 `"max"` 比,轉成數字為 `0` |
| 否定 | 不支援 `!flag`,請寫 `flag == false` |
| 複合條件 | 不支援 `&&`、`||`;改用巢狀元素或在資料中先算好布林值 |
| 與 `:for` 並用 | `:for` 先展開,每個項目各自判斷 `:if`,可直接使用項目變數 |
```demo
```
---
Source: https://quickui.pardn.io/zh/loop-rendering.md
# 迴圈渲染
本頁說明 `:for` 如何展開陣列與物件,以及迴圈內可以存取哪些變數。
## 陣列
```demo
- {{ item }}
- {{ index }}. {{ item }}
```
| 寫法 | 變數 |
|---|---|
| `item in list` | `item` 為項目 |
| `(item, index) in list` | 另有從 `0` 開始的 `index` |
## 物件
```demo
```
第一個變數是 key、第二個是 value;只寫一個變數時只取得 key。
## 作用域
| 可存取 | 說明 |
|---|---|
| 迴圈變數 | `item`、`index`(或 `key`、`value`) |
| 來源集合 | 從根資料以同名取得,例如 `items` |
| 根資料 | 迴圈作用域找不到時回到 `app.data` 查找 |
| 外層迴圈變數 | 不可存取;巢狀迴圈的內層只看得到自己的變數與根資料 |
```demo
{{ group.name }}
{{ member }}
```
內層的來源路徑 `group.members` 可以使用外層變數,但內層元素中的 `{{ group.name }}` 不會被替換。
---
Source: https://quickui.pardn.io/zh/attribute-binding.md
# 屬性綁定
本頁說明以 `:` 或 `q-` 前綴把資料綁到 HTML 屬性,以及 `:hide` 的判斷方式。
## 寫法
```demo
```
| 前綴 | 說明 |
|---|---|
| `:` | 標準寫法,例如 `:href` |
| `q-` | 等價寫法,例如 `q-href`;適合不接受 `:` 屬性名的工具鏈 |
屬性值先當成資料路徑查找;找不到時直接使用屬性值本身。值中含 `{{ }}` 時,只替換第一個 `{{ }}`,前後文字保留。處理完成後原本的 `:` 屬性會被移除。
## 分派規則
| 屬性 | 寫入方式 |
|---|---|
| `:id`、`:src`、`:alt`、`:href` | 設定元素屬性(property) |
| `:html` | `innerHTML`,見 [文字綁定](/zh/text-binding) |
| `:` 前綴且名稱對應到 CSS 屬性(如 `:width`、`:color`) | 寫入 `style`,見 [樣式綁定](/zh/style-binding) |
| `:hide` | 值為真時設 `display: none` |
| `:lazyload` | 見 [圖片延遲載入](/zh/lazyload) |
| 其他(如 `:title`、`:data-id`、`:value`) | 去掉前綴後 `setAttribute` |
名稱與 CSS 屬性相同者一律進 `style`:`
` 設定的是 CSS `width`,不是 `width` 屬性。
## `:hide`
```demo
```
| 值 | 結果 |
|---|---|
| `true`、非零數字、非空字串 | 隱藏 |
| `false`、`0`、`"0"`、空字串 | 顯示;值會寫成 `hide` 屬性(例如 `hide="false"`) |
| 資料中不存在的鍵 | 使用屬性值本身(非空字串),因此會隱藏 |
字串 `"false"` 也會被視為真值而隱藏;請使用布林值。
元素一旦隱藏,再把值改回假值也不會移除 `display: none`,元素維持隱藏。在上方預覽連按兩次 Toggle 即可重現。
## 多語系屬性值
任何屬性的值若是 `i18n.key`,會被替換為目前語系的翻譯,例如 `placeholder="i18n.search"`。屬性只支援單層 key,見 [i18n](/zh/i18n)。
---
Source: https://quickui.pardn.io/zh/style-binding.md
# 樣式綁定
本頁說明如何以 `:[CSS 屬性]` 把資料直接綁到行內樣式。
## 寫法
```demo
```
結果:
```html
```
| 規則 | 說明 |
|---|---|
| 名稱 | 使用 kebab-case 的 CSS 屬性名,轉成 camelCase 後須存在於 `element.style` |
| 值 | 資料值或屬性值本身,需自帶單位,例如 `"120px"` |
| 前綴 | 只認 `:`;`q-width` 不會進 `style`,而是設定 `width` 屬性 |
| `:animation` | 值為 `fade-in` 或 `expand` 時不寫入 `style`,而是視為 [渲染效果](/zh/effects) |
## 舊式名稱
下列寫法仍可使用,但預計在 1.0.0 移除,詳見 [淘汰計畫](/zh/deprecations)。
| 舊寫法 | 改用 |
|---|---|
| `:bg-color`、`:bg-image` 等 `:bg-*` | `:background-color`、`:background-image` |
| `:borderRadius`、`:backgroundColor` 等 camelCase | `:border-radius`、`:background-color` |
---
Source: https://quickui.pardn.io/zh/event-binding.md
# 事件綁定
本頁說明以 `@事件` 把 DOM 事件接到 `event` 選項中的處理函式。
## 寫法
```demo
```
| 前綴 | 例 | 說明 |
|---|---|---|
| `@` | `@click` | 標準寫法 |
| `qe-` | `qe-click` | 等價寫法 |
`@click="save"` 會被轉成 `element.onclick = app.event.save`,處理完成後移除 `@click` 屬性。
## 規則
| 規則 | 說明 |
|---|---|
| 事件名稱 | 必須是元素有對應 `on*` 屬性的原生事件(`click`、`input`、`change`、`submit`、`keyup` 等);自訂事件不會觸發 |
| 處理函式參數 | 只收到 `Event`;以 `function` 宣告時 `this` 為該元素 |
| 每種事件一個處理函式 | 以屬性指派,同一元素同一事件只保留一個 |
| 名稱查找 | 屬性值先當資料路徑查找,取得的值作為函式名稱;找不到時直接以屬性值為名稱 |
| 名稱不存在 | 指派為 `undefined`,點擊沒有反應也不報錯 |
| `:@click` | 不支援,屬性會被移除且不綁定 |
## 在迴圈中取得項目
處理函式拿不到 `:for` 的項目,請把識別值寫進 `data-*` 屬性:
```demo
```
## 在 QuickUI 以外使用
`app.event` 保留原始的函式集合,可以指派給動態建立的元素:
```javascript
const button = document.createElement("button");
button.onclick = app.event.save;
app.body.append(button);
```
---
Source: https://quickui.pardn.io/zh/two-way-binding.md
# 表單綁定
本頁說明 `:model` 如何把表單輸入寫回資料。
## 寫法
```demo
```
## 各元素的行為
| 元素 | 監聽事件 | 寫入 `app.data[key]` 的值 |
|---|---|---|
| ``(文字類)、`