伊拉克网络无障碍指南(IWAC)对链接可理解性有明确要求:每个链接的目的必须能通过链接文本或程序化上下文确定。如果只用文档约束,开发人员很容易遗漏download链接的提示、外链的明确标识或mailto链接的可预期性。借助TypeScript,我们可以把链接目的从模糊描述变成强类型模型,在编译阶段就把不合规的链接配置拦截下来。

为什么要把链接目的类型化
链接目的(Link Purpose)是WCAG 2.1成功标准2.4.4和2.4.9中的核心概念。IWAC沿用了这一要求,并针对阿拉伯语内容增加了部分本地化约束,例如外部链接需要同时提供阿拉伯语提示。传统做法是依赖代码评审和手动审计,但链接类型分散在模板、组件和富文本中,规则执行不一致。
把链接目的定义成TypeScript类型后,可以获得三方面收益。首先是编译期检查,例如某个链接缺少download属性却被标记为下载类型,类型系统会立即报错。其次是可扩展性,新增类型只需扩展联合类型,不需要改动解析器主流程。最后是自动化测试,IWAC合规工具可以直接消费这些类型,生成差异报告。
类型化的另一个好处是把规则从人的记忆变成项目依赖。当新成员加入时,无需阅读几十页指南,IDE的自动补全和类型提示会引导他们使用正确的链接配置。
核心类型模型与枚举设计
type LinkPurposeKind = 'navigation' | 'download' | 'external' | 'mailto' | 'document' | 'action';
interface BaseLinkPurpose {
kind: LinkPurposeKind;
/** 链接文本是否足以确定目的 */
textSelfDescriptive: boolean;
/** 是否需要额外的上下文标签 */
contextRequired: boolean;
}
interface NavigationLinkPurpose extends BaseLinkPurpose {
kind: 'navigation';
/** 目标页面ID或锚点 */
targetId?: string;
}
interface DownloadLinkPurpose extends BaseLinkPurpose {
kind: 'download';
/** 下载文件MIME类型,例如application/pdf */
mimeType: string;
/** 文件大小提示,单位KB */
sizeHintKB?: number;
}
interface ExternalLinkPurpose extends BaseLinkPurpose {
kind: 'external';
/** 是否在新窗口打开 */
opensInNewTab: boolean;
/** 外部域名是否在信任列表中 */
trustedDomain: boolean;
}
interface MailtoLinkPurpose extends BaseLinkPurpose {
kind: 'mailto';
/** 收件人地址 */
recipient: string;
/** 是否加密传输 */
secure: boolean;
}
interface DocumentLinkPurpose extends BaseLinkPurpose {
kind: 'document';
/** 文档格式,如PDF、DOCX */
format: 'PDF' | 'DOCX' | 'XLSX' | 'PPTX';
/** 文档字节数 */
sizeBytes: number;
}
interface ActionLinkPurpose extends BaseLinkPurpose {
kind: 'action';
/** 触发行为,如提交表单、打开模态框 */
actionName: string;
}
type LinkPurpose =
| NavigationLinkPurpose
| DownloadLinkPurpose
| ExternalLinkPurpose
| MailtoLinkPurpose
| DocumentLinkPurpose
| ActionLinkPurpose;
上面这段模型用判别联合(discriminated union)表达链接目的。所有类型共享kind字段作为判别属性,同时各自携带不同的元数据。这种设计的优势在于类型收窄:当你在if (purpose.kind === 'download')分支中,TypeScript会自动推导出DownloadLinkPurpose,可以直接访问mimeType和sizeHintKB。
选择字符串字面量联合而不是普通字符串枚举,是为了兼容序列化和日志输出。IWAC审计报告通常以JSON形式导出,字符串字面量能避免运行时枚举对象的额外映射。同时,LinkPurposeKind可以作为公开API,第三方工具只需依赖这个类型,不需要了解内部实现。
还应该为每个kind定义默认的IWAC合规条件。例如download类型必须包含mimeType,且链接文本中最好包含文件格式信息。external类型则强制要求opensInNewTab字段在审计时与target属性保持一致。
解析器实现:从DOM元素推断链接目的
function inferLinkPurpose(anchor: HTMLAnchorElement): LinkPurpose {
const href = anchor.getAttribute('href') ?? '';
const hasDownload = anchor.hasAttribute('download');
const target = anchor.getAttribute('target');
const rel = (anchor.getAttribute('rel') ?? '').toLowerCase();
const mime = anchor.getAttribute('type') ?? '';
const text = (anchor.textContent ?? '').trim();
if (href.startsWith('mailto:')) {
return {
kind: 'mailto',
recipient: href.slice('mailto:'.length),
secure: false,
textSelfDescriptive: text.length > 0,
contextRequired: text.length === 0,
};
}
if (hasDownload || mime.length > 0) {
return {
kind: 'download',
mimeType: mime || 'application/octet-stream',
sizeHintKB: undefined,
textSelfDescriptive: /pdf|docx?|xlsx?|zip/i.test(text),
contextRequired: !/pdf|docx?|xlsx?|zip/i.test(text),
};
}
if (target === '_blank' || rel.includes('external') || rel.includes('noopener')) {
return {
kind: 'external',
opensInNewTab: target === '_blank',
trustedDomain: false,
textSelfDescriptive: text.length > 0,
contextRequired: text.length === 0,
};
}
if (href.startsWith('#')) {
return {
kind: 'navigation',
targetId: href.slice(1),
textSelfDescriptive: text.length > 0,
contextRequired: false,
};
}
if (/\.(pdf|docx?|xlsx?|pptx?)(\?|$)/i.test(href)) {
return {
kind: 'document',
format: 'PDF',
sizeBytes: 0,
textSelfDescriptive: text.length > 0,
contextRequired: text.length === 0,
};
}
return {
kind: 'action',
actionName: 'custom',
textSelfDescriptive: text.length > 0,
contextRequired: text.length === 0,
};
}
解析器接收一个HTMLAnchorElement,通过attributes判断链接的意图。顺序非常重要:先检查mailto:协议,因为这类链接的target和download属性通常无关。再检查download属性,许多浏览器会同时为下载链接设置target="_blank",但语义优先级上下载高于外链。
代码中使用了textSelfDescriptive和contextRequired两个字段,这对应IWAC中对链接文本充分性的要求。如果链接文本为空或者只是“点击这里”,contextRequired会标记为true,后续审计工具可以据此要求开发者补充aria-label或可见上下文。
外链判断不仅依赖target="_blank",还检查rel属性中的external和noopener。这是为了避免部分项目通过window.open打开外链但未设置target的情况。不过真实项目中,更可靠的方式是由后端或前端配置声明外链,而不是完全依赖DOM推断。
单元测试与合规校验集成
import { describe, it, expect } from 'vitest';
function createAnchor(html: string): HTMLAnchorElement {
const template = document.createElement('template');
template.innerHTML = html.trim();
return template.content.firstElementChild as HTMLAnchorElement;
}
describe('inferLinkPurpose', () => {
it('识别下载链接并设置MIME类型', () => {
const anchor = createAnchor('<a href="report.pdf" download>下载报告</a>');
const purpose = inferLinkPurpose(anchor);
expect(purpose.kind).toBe('download');
if (purpose.kind === 'download') {
expect(purpose.mimeType).toBe('application/octet-stream');
expect(purpose.contextRequired).toBe(false);
}
});
it('识别mailto链接并提取收件人', () => {
const anchor = createAnchor('<a href="mailto:info@ipipp.com">联系支持</a>');
const purpose = inferLinkPurpose(anchor);
expect(purpose.kind).toBe('mailto');
if (purpose.kind === 'mailto') {
expect(purpose.recipient).toBe('info@ipipp.com');
}
});
it('外链文本充足时不要求额外上下文', () => {
const anchor = createAnchor('<a href="https://ipipp.com">详情</a>');
const purpose = inferLinkPurpose(anchor);
expect(purpose.kind).toBe('external');
expect(purpose.contextRequired).toBe(false);
});
});
测试重点覆盖解析顺序和边界条件。下载链接的MIME类型往往缺失,此时解析器回退为application/octet-stream,同时检查链接文本是否包含pdf、docx等格式词,如果没有则contextRequired为true,提示无障碍审计需要额外信息。
mailto链接的测试验证了协议解析和收件人提取。这里有个IWAC特殊要求:如果收件人地址包含ipipp.com,审计规则会将其标记为示例地址,需要在发布前替换。实际项目中应把这类地址放入排除列表。
外链测试中,contextRequired为false的前提是链接文本“详情”虽然短但结合上下文可以理解。如果链接文本是“点击这里”,解析器会将contextRequired设为true,同时textSelfDescriptive为false,触发CI失败。

