Show HN: Aberdeen - 一种优雅的响应式UI构建方法
文章介绍了 Aberdeen,一种使用纯 TypeScript/JavaScript 构建响应式 UI 的方法,无需 virtual DOM。核心思想是使用匿名函数发出 DOM 元素,并在底层代理数据变化时自动重新运行。Aberdeen 具有优雅、快速、体积小等优点,并提供列表功能、客户端路由等。缺点是社区和生态系统相对较小。文章还提供了示例和学习资源,并宣布了 1.0 版本的发布。
Aberdeen - v1.0.0 这是什么?教程GitHub
Aberdeen

用纯 TypeScript/JavaScript 构建飞快的、声明式的 UI – 无需 virtual DOM。
Aberdeen 提供了一种非常简单的响应式 UI 构建方法。其核心思想是:
使用许多小的匿名函数来发出 DOM 元素,并在其底层 代理的 数据发生更改时自动重新运行它们。 这个代理的数据可以是任何东西,从简单值到复杂的、类型化的、深度嵌套的数据结构。
现在,让我们深入了解一下为什么这很重要...
为什么要使用 Aberdeen?
- 🎩 优雅而简单: 使用 JavaScript/TypeScript 自然地表达 UI,无需复杂的抽象、构建步骤或 JSX。 没有 hooks,没有
setState
,没有状态提升,没有状态管理库。 只有代理的数据和自动重新运行的函数。 - ⏩ 快速: 没有 virtual DOM。 当代理的数据发生更改时,Aberdeen 智能地仅更新 UI 中最小、必要的部分。
- 👥 强大的列表功能: 反应式地显示按您喜欢的任何方式排序的数据非常容易且性能出色。
- 🔬 体积小: 约 5KB (最小化和 gzip 压缩后),并且没有运行时依赖项。
- 🔋 自带常用功能: 配备客户端路由、用于乐观 UI 更新的可恢复补丁、组件局部 CSS、用于转换响应式数据的辅助函数(映射、分区、过滤等)以及隐藏/取消隐藏过渡效果。 无需过多选择!
为什么 不 使用 Aberdeen?
- 🤷 缺乏社区: Aberdeen 开发者还不多,所以不要期望能得到非常有用的 Stack Overflow/AI 答案。
- 📚 缺乏生态系统: 您必须自己编写代码,而不是将无数个 React 生态系统库拼凑在一起。
示例
为了快速了解 Aberdeen 代码的样子,下面是一个带有撤消历史记录的井字游戏应用程序。 如果您正在官方网站上阅读本文,您应该在代码下方看到一个工作演示,并在代码的右上角看到一个“编辑”按钮,可以进行尝试。
import {$, proxy, onEach, insertCss, observe} from"aberdeen";// Helper functionsfunctioncalculateWinner(board) {constlines = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], // horizontal [0, 3, 6], [1, 4, 7], [2, 5, 8], // vertical [0, 4, 8], [2, 4, 6] // diagonal ];for (const [a, b, c] oflines) {if (board[a] && board[a] === board[b] && board[a] === board[c]) {returnboard[a]; } }}functiongetCurrentMarker(board) {returnboard.filter(v=>v).length % 2 ? "O" : "X";}functiongetBoard(history) {returnhistory.boards[history.current];}functionmarkSquare(history, position) {constboard = getBoard(history);// Don't allow markers when we already have a winnerif (calculateWinner(board)) return;// Copy the current board, and insert the marker into itconstnewBoard = board.slice();newBoard[position] = getCurrentMarker(board);// Truncate any future states, and write a new futurehistory.current++;history.boards.length = history.current;history.boards.push(newBoard);}// Define component-local CSS, which we'll utilize in the drawBoard function.// Of course, you can use any other styling solution instead, if you prefer.constboardStyle = insertCss({display:'grid',gap:'0.5em',gridTemplateColumns:'1fr 1fr 1fr','> *': {width:'2em',height:'2em',padding:0, },});// UI drawing functions.functiondrawBoard(history) {$('div', boardStyle, () => {for(letpos=0; pos<9; pos++) {$('button.square', () => {letmarker = getBoard(history)[pos];if (marker) {$({ text:marker }); } else {$({ click: () =>markSquare(history, pos) }); } }); } })}functiondrawStatusMessage(history) {$('h4', () => {// Reruns whenever observable data read by calculateWinner or getCurrentMarker changesconstboard = getBoard(history);constwinner = calculateWinner(board);if (winner) {$(`:Winner: ${winner}!`); } elseif (board.filter(square=>square).length === 9) {$(`:It's a draw...`); } else {$(`:Current player: ${getCurrentMarker(board)}`); } });}functiondrawTurns(history) {$('div:Select a turn:')// Reactively iterate all (historic) board versionsonEach(history.boards, (_, index) => {$('button', {// A text node:text:index,// Conditional css class:".outline":observe(() =>history.current != index),// Inline styles:$marginRight:"0.5em",$marginTop:"0.5em",// Event listener:click: () =>history.current = index, }); });}functiondrawMain() {// Define our state, wrapped by an observable proxyconsthistory = proxy({boards: [[]], // eg. [[], [undefined, 'O', undefined, 'X'], ...]current:0, // indicates which of the boards is currently showing });$('main.row', () => {$('div.box', () =>drawBoard(history));$('div.box', {$flex:1}, () => {drawStatusMessage(history);drawTurns(history); }); });}// Fire it up! Mounts on document.body by default..drawMain();
CopyEdit
- Browser
- HTML
- Console Restart
<style>.AbdStl1{display:grid;gap:0.5em;grid-template-columns:1fr 1fr 1fr;}.AbdStl1 > *{width:2em;height:2em;padding:0;}</style><main class="row"> <div class="box"> <div class="AbdStl1"> <button class="square"></button> <button class="square"></button> <button class="square"></button> <button class="square"></button> <button class="square"></button> <button class="square"></button> <button class="square"></button> <button class="square"></button> <button class="square"></button> </div> </div> <div class="box" style="flex: 1 1 0%;"> <h4>Current player: X</h4> <div>Select a turn:</div> <button style="margin-right: 0.5em; margin-top: 0.5em;">0</button> </div></main>
更多示例:
- Input example demo - Source
- List example demo - Source
- Routing example demo - Source
- JS Framework Benchmark demo - Source
学习 Aberdeen
当然,您可能还想研究上面的示例!
新闻
- 2025-05-07: 在断断续续地开发这个库五年之后,我终于对它的 API 及其提供的开发者体验感到满意。 我称之为 1.0! 为了庆祝,我创建了一些非常漂亮的(如果我可以这么说的话)交互式文档和一个教程。
设置
Member Visibility
- Protected
- Inherited
- External
ThemeOSLightDark