dagger.js 实现「CSS 机械键盘」示例解读(对比 React 版本)

发布于:2025-08-29 ⋅ 阅读:(15) ⋅ 点赞:(0)

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 存当前被按下的字符,keydownaddkeyupdelete
  • 音效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感兴趣的话,请您点赞收藏、分享本系列文章,也欢迎留言或者私信作者提出问题和建议,您的关注是对我最大的支持和鼓励。感谢您的阅读,祝工作学习顺利!


网站公告

今日签到

点亮在社区的每一天
去签到