AI 辅助的 DApp 前端无障碍合规检测:从手动审查到自动诊断,Web3 的包容性设计

cover

一、Web3 的无障碍盲区:去中心化应用的包容性缺失

DApp 前端开发长期忽视无障碍(Accessibility, a11y)设计。大量 DApp 的交互依赖钱包连接弹窗、复杂的多步表单、实时数据图表,这些组件缺乏键盘导航支持、屏幕阅读器适配和对比度合规。视障用户无法通过键盘完成钱包连接操作,色盲用户无法区分图表中的数据系列。

传统 Web 应用的 a11y 检测工具(如 axe-core、Lighthouse)可以检测静态页面的无障碍问题,但对 DApp 特有的交互模式(如钱包签名弹窗、链上交易状态轮询、Web3 Modal)覆盖不足。AI 辅助的 a11y 检测可以理解 DApp 的交互语义,发现工具无法覆盖的深层无障碍问题。

二、DApp 无障碍检测架构

flowchart TD
    A[DApp 页面] --> B[静态检测层]
    A --> C[交互检测层]
    A --> D[AI 语义检测层]
    B --> B1[axe-core: DOM 结构检查]
    B --> B2[对比度检查]
    B --> B3[ARIA 属性检查]
    C --> C1[键盘导航测试]
    C --> C2[钱包连接流程测试]
    C --> C3[交易状态可感知性]
    D --> D1[交互语义理解]
    D --> D2[DApp 特有问题识别]
    D --> D3[修复建议生成]
    B1 --> E[问题汇总与优先级排序]
    C1 --> E
    D1 --> E

2.1 DApp 特有的无障碍规则

// dapp-a11y-rules.ts — DApp 无障碍检测规则
// 设计意图:定义 DApp 特有的无障碍检测规则,
// 覆盖钱包连接、交易签名等 Web3 特有交互

interface A11yIssue {
  rule: string;
  severity: 'critical' | 'serious' | 'moderate' | 'minor';
  element: string;
  description: string;
  suggestion: string;
}

export class DAppA11yChecker {
  private issues: A11yIssue[] = [];

  // 规则1:钱包连接按钮必须有可访问的名称
  checkWalletButton(element: HTMLElement): void {
    const walletButtons = element.querySelectorAll('[data-wallet], .wallet-connect, .connect-button');

    walletButtons.forEach((btn) => {
      const el = btn as HTMLElement;
      const hasAriaLabel = el.getAttribute('aria-label');
      const hasTextContent = el.textContent?.trim();
      const hasAriaLabelledBy = el.getAttribute('aria-labelledby');

      if (!hasAriaLabel && !hasTextContent && !hasAriaLabelledBy) {
        this.issues.push({
          rule: 'wallet-button-name',
          severity: 'critical',
          element: el.outerHTML.slice(0, 100),
          description: '钱包连接按钮缺少可访问的名称,屏幕阅读器无法识别其用途',
          suggestion: '添加 aria-label="连接钱包" 或在按钮内添加可见文本',
        });
      }

      // 检查是否支持键盘激活
      if (el.tagName !== 'BUTTON' && el.tagName !== 'A' && !el.getAttribute('role')) {
        this.issues.push({
          rule: 'wallet-button-keyboard',
          severity: 'serious',
          element: el.outerHTML.slice(0, 100),
          description: '钱包连接元素不是原生按钮,可能不支持键盘激活',
          suggestion: '使用 <button> 元素或添加 role="button" 和 tabindex="0"',
        });
      }
    });
  }

  // 规则2:交易状态必须有实时通知
  checkTransactionStatus(element: HTMLElement): void {
    const statusElements = element.querySelectorAll('[data-tx-status], .tx-status, .transaction-status');

    statusElements.forEach((el) => {
      const htmlEl = el as HTMLElement;
      const hasLiveRegion = htmlEl.getAttribute('aria-live');

      if (!hasLiveRegion) {
        this.issues.push({
          rule: 'tx-status-live-region',
          severity: 'serious',
          element: htmlEl.outerHTML.slice(0, 100),
          description: '交易状态更新没有使用 ARIA live region,屏幕阅读器用户无法感知状态变化',
          suggestion: '添加 aria-live="polite" 属性,确保状态更新被屏幕阅读器播报',
        });
      }
    });
  }

  // 规则3:链上数据展示必须提供文本替代
  checkOnChainData(element: HTMLElement): void {
    const addressElements = element.querySelectorAll('[data-address], .address, .wallet-address');

    addressElements.forEach((el) => {
      const htmlEl = el as HTMLElement;
      const truncated = htmlEl.textContent?.match(/^0x[a-fA-F0-9]{4}\.\.\.[a-fA-F0-9]{4}$/);

      if (truncated && !htmlEl.getAttribute('title') && !htmlEl.getAttribute('aria-label')) {
        this.issues.push({
          rule: 'address-readable',
          severity: 'moderate',
          element: htmlEl.outerHTML.slice(0, 100),
          description: '截断的地址显示缺少完整地址的文本替代',
          suggestion: '添加 title 属性包含完整地址,或使用 aria-label 提供完整地址',
        });
      }
    });
  }

  getIssues(): A11yIssue[] {
    return this.issues.sort((a, b) => {
      const severityOrder = { critical: 0, serious: 1, moderate: 2, minor: 3 };
      return severityOrder[a.severity] - severityOrder[b.severity];
    });
  }
}

2.2 AI 语义检测

# dapp_a11y_ai.py — AI 辅助的 DApp 无障碍语义检测
# 设计意图:分析 DApp 的交互流程,发现工具无法覆盖的深层无障碍问题

import json

async def analyze_dapp_a11y(
    page_html: str,
    interaction_flow: list[str],
    llm_client,
) -> list[dict]:
    """AI 分析 DApp 的无障碍问题"""
    prompt = f"""你是一个 Web 无障碍专家,专注于 DApp 前端的无障碍合规检测。

DApp 页面 HTML:
```html
{page_html[:5000]}

交互流程:
{json.dumps(interaction_flow, ensure_ascii=False)}

请检测以下 DApp 特有的无障碍问题:

  1. 钱包连接流程是否支持纯键盘操作?
  2. 交易签名弹窗是否对屏幕阅读器可访问?
  3. Gas 费用显示是否有足够的文本描述?
  4. 网络切换是否通知了状态变化?
  5. NFT 图像是否有替代文本?
  6. 链上地址是否提供了可读的文本替代?
  7. 加密货币金额是否有货币单位标注?

输出 JSON 数组:
[{{"rule": "...", "severity": "critical/serious/moderate/minor", "description": "...", "suggestion": "..."}}]"""

response = await llm_client.chat(prompt, temperature=0.1)

try:
    return json.loads(response)
except json.JSONDecodeError:
    return []

## 三、修复建议与组件库

### 3.1 无障碍钱包连接组件

```tsx
// AccessibleWalletConnect.tsx — 无障碍钱包连接组件
// 设计意图:提供键盘可操作、屏幕阅读器友好的钱包连接体验

import { useState, useRef } from 'react';

export function AccessibleWalletConnect() {
  const [isOpen, setIsOpen] = useState(false);
  const [status, setStatus] = useState<'idle' | 'connecting' | 'connected' | 'error'>('idle');
  const dialogRef = useRef<HTMLDialogElement>(null);

  const handleKeyDown = (e: React.KeyboardEvent) => {
    if (e.key === 'Escape' && isOpen) {
      setIsOpen(false);
    }
  };

  return (
    <div onKeyDown={handleKeyDown}>
      <button
        onClick={() => setIsOpen(true)}
        aria-label="连接钱包"
        aria-haspopup="dialog"
      >
        {status === 'connected' ? '已连接' : '连接钱包'}
      </button>

      <dialog
        ref={dialogRef}
        open={isOpen}
        aria-label="选择钱包"
        aria-modal="true"
      >
        <h2>选择钱包</h2>
        <ul role="listbox" aria-label="可用钱包列表">
          <li role="option" aria-selected="false" tabIndex={0}>
            <button onClick={() => setStatus('connecting')}>MetaMask</button>
          </li>
          <li role="option" aria-selected="false" tabIndex={0}>
            <button onClick={() => setStatus('connecting')}>WalletConnect</button>
          </li>
        </ul>
        <button onClick={() => setIsOpen(false)} aria-label="关闭钱包选择">
          取消
        </button>
      </dialog>

      {/* 交易状态 live region */}
      <div aria-live="polite" aria-atomic="true" className="sr-only">
        {status === 'connecting' && '正在连接钱包...'}
        {status === 'connected' && '钱包已连接'}
        {status === 'error' && '钱包连接失败'}
      </div>
    </div>
  );
}

四、边界分析与架构权衡

AI 检测的误报率:AI 可能将合理的 DApp 设计标记为无障碍问题。例如,某些 DApp 故意使用简化的 UI 以降低 Gas 费用相关的交互步骤。需要人工审核 AI 的检测结果。

DApp 生态的 a11y 基线低:Web3 生态整体的无障碍意识薄弱,主流钱包(如 MetaMask)本身的无障碍支持也不完善。即使 DApp 前端做了 a11y 优化,钱包弹窗的无障碍缺陷仍会影响用户体验。

动态内容的检测难度:DApp 页面内容高度动态(链上数据实时更新、交易状态轮询),静态 HTML 分析无法覆盖动态内容。需要结合 E2E 测试和运行时检测。

修复成本与优先级:无障碍修复可能涉及组件重构,成本较高。需要按严重程度排序,优先修复 critical 和 serious 级别的问题。

五、总结

DApp 前端的无障碍合规是 Web3 包容性设计的基础。通过 DApp 特有的检测规则、AI 语义分析和无障碍组件库,可以系统性地发现和修复无障碍问题。落地建议:优先修复钱包连接和交易签名流程的键盘可操作性;使用 ARIA live region 通知状态变化;为链上地址和 NFT 图像提供文本替代;按严重程度排序逐步修复。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐