0) 效果演示 (代碼地址)
CSS Mechanical Keyboard
1) 示例與來源
- dagger.js 版本:本筆圍繞 CodePen 上的《CSS Mechanical Keyboard》的 dagger.js 改寫版進行解讀,核心思路是用 dagger 指令把純 CSS 藝術包裝成可復用的組件,并加入鍵盤事件與音效。
- 原始作品:原作由 Yoav Kadosh 創作,是一個 純 CSS 的機械鍵盤(不依賴外部 JS),偏重 3D 視覺與陰影層疊技巧。
- 本文對照:為便于理解,我們提供一個等價的 React 參考實現(并非作者官方版本),用于對比心智模型、代碼結構與工程復雜度。
👉 說明:原作側重 CSS 藝術;Dagger 版本在此基礎上,借助指令系統與模板,增強了可組合性和交互(按鍵高亮、鍵音)。
2) dagger.js 代碼結構速覽
下面片段來自示例的核心結構,已做適度壓縮與注釋,便于閱讀。
2.1 模塊與模板
<!-- 聲明模塊與模板的映射(同一 Pen 也可改為外鏈腳本模塊) -->
<script type="dagger/modules">
{"_": "#script","key": "#template_key","row": "#template_row","column": "#template_column"
}
</script><!-- 業務腳本(作為 dagger 模塊暴露函數) -->
<script type="dagger/script" id="script">export const load = () => ({set: new Set(),audio: new Audio("https://assets.codepen.io/5782383/keytype.mp3")});export const keyInit = (set, char, span = false) => ({char, span, active: set.has(char)});export const onKeyDown = ($event, set, audio) => {set.add($event.key);audio.pause(); audio.currentTime = 0; audio.play();};
</script><!-- 組件模板:Key / Row / Column -->
<template id="template_key"><div class="key" *class="{ span: $scope.span, active: $scope.set.has(char) }"><div class="side"></div><div class="top"></div><div class="char">${ char }</div></div>
</template><template id="template_row"><div class="row"><template @slot></template></div>
</template><template id="template_column"><div class="column"><template @slot></template></div>
</template>
2.2 頁面與交互
<div class="keyboard"dg-cloak+load+keydown#target:document="onKeyDown($event, set, audio)"+keyup#target:document="set.delete($event.key)"><column><row><key *each="['7','8','9']" +load="keyInit(set, item)"></key></row><row><key *each="['4','5','6']" +load="keyInit(set, item)"></key></row><row><key *each="['1','2','3']" +load="keyInit(set, item)"></key></row><row><key +load="keyInit(set, '0', true)"></key><key +load="keyInit(set, '.')"></key></row></column><column><key +load="keyInit(set, '+', true)"></key><key +load="keyInit(set, '-', true)"></key></column><div class="shade"></div><div class="cover"></div>
</div>
要點解讀
+load
:組件/元素裝載時初始化作用域,返回{ set, audio }
等狀態對象。*each
:把字符數組映射為一組<key>
子組件。*class
:根據set
中是否包含字符切換active
/span
類名。+keydown/+keyup#target:document
:把監聽目標直接綁定到document
,控制全局按鍵高亮與刪除狀態。- 模板
<template @slot>
讓 Row/Column 像容器組件一樣承載子節點(對標 React 的children
)。
3) 交互與狀態
- 按鍵狀態:用
Set
存當前被按下的字符,keydown
時add
,keyup
時delete
。 - 音效:
Audio
對象復用;每次按鍵前pause
并重置currentTime
,避免疊音。 - 高亮:
*class
與$scope.set.has(char)
實時驅動。
4) 樣式與 3D 視覺要點(概覽)
- 主題色/陰影:SCSS 變量(如
$color-gray-*
、$color-orange-*
)集中管理。 - 立體感:
transform: rotateX(...) rotateZ(...)
、transform-style: preserve-3d
+ 多層box-shadow
。 - 自定義函數:
@function layered_shadow(...)
構造層疊陰影,營造“厚重”的機械感。
視覺仍然由 純 CSS/SCSS 驅動;dagger.js 只負責結構/交互與狀態膠合。
5) React 參考實現(等價思路)
下例演示若用 React 實現同等交互,核心包括:組件拆分、全局鍵盤事件、
Set
狀態與音效復用。代碼僅作對照示例。
import React, { useEffect, useMemo, useRef, useState } from "react";function useKeyboardAudio(src) {const audioRef = useRef(null);useEffect(() => { audioRef.current = new Audio(src); }, [src]);const play = () => {const a = audioRef.current;if (!a) return;a.pause(); a.currentTime = 0; a.play();};return play;
}function Key({ char, active }) {return (<div className={`key ${active ? "active" : ""}`}><div className="side" /><div className="top" /><div className="char">{char}</div></div>);
}function Row({ children }) { return <div className="row">{children}</div>; }
function Column({ children }){ return <div className="column">{children}</div>; }export default function Keyboard() {const [down, setDown] = useState(() => new Set());const play = useKeyboardAudio("https://assets.codepen.io/5782383/keytype.mp3");useEffect(() => {const onKeyDown = (e) => {// 采用不可變更新觸發重渲染setDown(prev => {if (prev.has(e.key)) return prev;const next = new Set(prev);next.add(e.key);play();return next;});};const onKeyUp = (e) => setDown(prev => {if (!prev.has(e.key)) return prev;const next = new Set(prev);next.delete(e.key);return next;});document.addEventListener("keydown", onKeyDown);document.addEventListener("keyup", onKeyUp);return () => {document.removeEventListener("keydown", onKeyDown);document.removeEventListener("keyup", onKeyUp);};}, [play]);const rows = useMemo(() => [["7","8","9"],["4","5","6"],["1","2","3"],], []);return (<div className="keyboard"><Column>{rows.map((arr, i) => (<Row key={i}>{arr.map(c => <Key key={c} char={c} active={down.has(c)} />)}</Row>))}<Row><Key char="0" active={down.has("0")} /><Key char="." active={down.has(".")} /></Row></Column><Column><Key char="+" active={down.has("+")} /><Key char="-" active={down.has("-")} /></Column><div className="shade" /><div className="cover" /></div>);
}
樣式(SCSS)基本可直接復用原作;必要時把
*class
的條件改為 React 的類名拼接邏輯。
6) dagger.js vs React:對照表
維度 | dagger.js 實現 | React 等價實現 |
---|---|---|
心智模型 | 聲明式指令(*each 、*class 、+load 、事件 #target:document )+ 模板插槽 | 組件 + JSX,狀態驅動渲染,DOM 由虛擬 DOM 協調 |
狀態管理 | 直接在作用域返回 { set, audio } ;Set 原地增刪 | useState / useRef ;常以不可變更新觸發重渲染 |
事件綁定 | +keydown/+keyup#target:document 語法內置 | useEffect 手動綁定/卸載 document 事件 |
模板/組合 | <template @slot> 容器模式;無需打包即可模塊化 | children 組合;通常依賴打包或 Babel/JSX |
運行與構建 | 零構建可運行(原生 ESM / Script Type 支持) | 常規項目多用打包鏈路(Vite/webpack);CodePen 可臨時用 Babel |
代碼體量 | 交互 JS 極少,主要重用 CSS 視覺 | 交互樣板(hooks/不可變更新)略多 |
適用場景 | 低門檻改造 CSS 藝術為可復用組件/小交互 | 生態完備、可擴展體系更強,適合復雜應用 |
7) 什么時候選 dagger.js?
- 你已有一份 純 CSS 藝術/動效,想快速加上鍵盤/鼠標交互與組件化復用;
- 希望 零構建 上線(靜態托管 / Edge 環境)并保持極低的引入成本;
- 更傾向原生 DOM 與語義化指令,不想維護冗長的狀態樣板。
8) 小結
- 原作突出 CSS 3D 質感與陰影層疊;dagger.js 改寫把它“組件化 + 可交互化”。
- 若用 React,實現同等功能也很直接,但需要一些 hooks 樣板與狀態不可變更新的心智模型。
本文內容就到這里,后續文章將為大家帶來更多案例和講解。
如果對dagger.js感興趣的話,請您點贊收藏、分享本系列文章,也歡迎留言或者私信作者提出問題和建議,您的關注是對我最大的支持和鼓勵。感謝您的閱讀,祝工作學習順利!