使用示例

以下示例基于当前 api.keykey.im 公开接口,并兼容完整记忆卡片与本地词典兜底响应。

类型安全的查询函数

type MemoryCardResponse = {
  success: boolean
  data?: {
    word: string
    displayWord?: string
    languageCategory: string
    trans?: string[]
    practiceTrans?: string[]
    definitionRelease?: number
    phonetic?: string
    phoneticSystem?: string
    notation?: string
    kanaReading?: string
    practiceWord?: string
    practiceAliases?: string[]
    lexemeKey?: string
    examples: Array<{
      sentence: string
      translation: string
      usage?: string
    }>
  }
  error?: string
  code?: string
  rateLimit?: {
    remaining: number
    limit: number
    reset: string
  }
}

async function queryMemoryCard(word: string, lang = 'en') {
  const params = new URLSearchParams({
    word,
    lang,
    includeLocalDictionaryFallback: 'true',
  })

  const response = await fetch(
    `https://api.keykey.im/api/public/memory-card?${params}`,
    { cache: 'no-store' },
  )
  const payload = (await response.json()) as MemoryCardResponse

  if (!response.ok || !payload.success || !payload.data) {
    throw new Error(payload.error || `HTTP ${response.status}`)
  }

  return payload
}

正确展示多语言词形

不要把 worddisplayWordpracticeWordphonetic 当成同一个字段:

function toWordPresentation(card) {
  return {
    // 页面标题:优先使用词典规定的大小写或展示形式
    title: card.displayWord || card.word,

    // 稳定身份:保存到数据库、收藏或复习记录时使用
    identity: card.lexemeKey || `${card.languageCategory}:${card.word}`,

    // 日语等语言的辅助展示
    notation: card.notation,
    reading: card.kanaReading,

    // 键盘练习答案,不得用 phonetic 代替
    practiceAnswer: card.practiceWord,
    acceptedAnswers: card.practiceAliases || [],

    // 独立的发音值与发音系统
    pronunciation: card.phonetic,
    pronunciationSystem: card.phoneticSystem,
  }
}

例如英语 Chinachina 可能具有不同展示词形和词条身份;日语词条则可能包含:

{
  "word": "日本",
  "notation": "日本(にっぽん)",
  "kanaReading": "にっぽん",
  "practiceWord": "nippon",
  "practiceAliases": ["nippon"],
  "lexemeKey": "ja:日本:にっぽん",
  "phonetic": "nippon",
  "phoneticSystem": "romaji"
}

完整释义与练习释义

function getDefinitions(card, mode: 'full' | 'practice' = 'full') {
  if (mode === 'practice' && card.practiceTrans?.length) {
    return {
      items: card.practiceTrans,
      source: 'system-practice',
      release: card.definitionRelease,
    }
  }

  return {
    items: card.trans || [],
    source: 'full',
    release: undefined,
  }
}

保存练习释义时,建议同时保存 definitionRelease。这样服务端发布新版本后,客户端可以判断本地释义是否需要更新。

批量查询

async function batchQueryMemoryCards(words, languageCategory = 'en') {
  const response = await fetch(
    'https://api.keykey.im/api/public/memory-card/batch-query',
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        words,
        languageCategory,
        includeLocalDictionaryFallback: true,
        definitionMode: 'full',
      }),
    },
  )

  const payload = await response.json()
  if (!response.ok || !payload.success) {
    throw new Error(payload.error || `HTTP ${response.status}`)
  }

  return new Map(
    payload.data.results
      .filter((item) => item.found && item.data)
      .map((item) => [item.word, item.data]),
  )
}

const cards = await batchQueryMemoryCards(['hello', 'world', 'study'])

批量接口一次最多接收 50 个词条。需要精简练习释义时,可把 definitionMode 改为 practice

自有缓存

API 返回 Cache-Control: no-store,这是为了让调用方看到最新词典发布。若业务允许缓存,可将语言、规范化查询和释义版本纳入缓存策略:

const CACHE_TTL = 60 * 60 * 1000

function cacheKey(word, lang) {
  return `memory-card:${lang}:${word.trim().toLocaleLowerCase(lang)}`
}

async function queryWithLocalCache(word, lang = 'en') {
  const key = cacheKey(word, lang)
  const raw = localStorage.getItem(key)

  if (raw) {
    const cached = JSON.parse(raw)
    if (Date.now() - cached.savedAt < CACHE_TTL) {
      return cached.payload
    }
  }

  const payload = await queryMemoryCard(word, lang)
  localStorage.setItem(key, JSON.stringify({ payload, savedAt: Date.now() }))
  return payload
}

对于会长期保存到 IndexedDB 或数据库的数据,不应只依赖 TTL;还要比较 definitionRelease,并保留 lexemeKey

结构化词典内容

英语词条可能包含 dictionaryDetails.openDictionary。使用前应逐层判空:

function OpenDictionarySummary({ card }) {
  const details = card.dictionaryDetails?.openDictionary
  if (!details) return null

  return (
    <section>
      <h2>{details.headwordSummary}</h2>
      <p>{details.memoryHook}</p>

      {details.meanings.map((meaning, index) => (
        <article key={`${meaning.partOfSpeech}-${index}`}>
          <strong>{meaning.partOfSpeech}</strong>
          {meaning.shortGloss && <span>{meaning.shortGloss}</span>}
          <p>{meaning.learnerExplanation}</p>
        </article>
      ))}
    </section>
  )
}

dictionaryDetails.ielts 可能提供出现次数、频率组、热力图和真题例句;不同词条的数据完整度可能不同。

配额与错误

const payload = await queryMemoryCard('hello')
const { remaining, limit, reset } = payload.rateLimit

if (remaining < 20) {
  console.warn(`配额剩余 ${remaining}/${limit},重置时间:${reset}`)
}

RATE_LIMIT_EXCEEDEDINSUFFICIENT_QUOTA 不要自动重试;等待 reset。对网络错误和 500 响应,可使用有限次数的指数退避。

下一步

Was this page helpful?