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 模块与模板
html
<!-- 声明模块与模板的映射(同一 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 页面与交互
html
<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
状态与音效复用。代码仅作对照示例。
jsx
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感兴趣的话,请您点赞收藏、分享本系列文章,也欢迎留言或者私信作者提出问题和建议,您的关注是对我最大的支持和鼓励。感谢您的阅读,祝工作学习顺利!