# 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

npm Downloads License

*** > 原生 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. QuickUI contributors *** ©️ 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

Active

Pending

Disabled

``` | 指令 | 說明 | |---|---| | `: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
  • {{ user.name }}
``` --- 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、第二個是 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
Content
``` | 值 | 結果 | |---|---| | `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

Hello, {{ username }}

``` ## 各元素的行為 | 元素 | 監聽事件 | 寫入 `app.data[key]` 的值 | |---|---|---| | ``(文字類)、`