搜索 K
主题
词云是标签页与归档页的经典一图流:词频权重一眼可读,还顺手当导航用。但大多数词云组件有个恼人的通病——每次刷新都洗牌,昨天在左上角的标签今天跑到右下角。@vaultkit/vitepress-plugin-word-cloud 用一个种子随机数生成器根治了这个问题:同一份词集,布局永远恒定。本站首页侧栏的「标签」词云就是它,现场即可看到效果。
本文通读这个包的全部导出(WordCloud 组件与 CloudWord 类型),逐个 prop 讲清语义与调法,并展开 mulberry32 种子随机、测量渲染字体同串、SVG 自渲染这些源码注释里的架构决策。
经典词云算法(Wordle 螺旋碰撞布局)的标准实现是 d3-cloud,但直接用它有三个坑:
Math.random(),初始落点、螺旋方向、词的旋转全是随机的,刷新即洗牌这个包对三个坑各给了一个明确解法:注入种子随机源、SVG text 自渲染、布局与渲染同取 getComputedStyle(document.body).fontFamily。d3-cloud 只负责词位计算,其余全部自己来。
pnpm add -D @vaultkit/vitepress-plugin-word-cloud若使用 @vaultkit/vitepress-theme-vault 主题则开箱即带——首页侧栏标签词云已接好,点击词跳标签筛选页。
<script setup lang="ts">
import type { CloudWord } from '@vaultkit/vitepress-plugin-word-cloud'
import { WordCloud } from '@vaultkit/vitepress-plugin-word-cloud'
import { useRouter, withBase } from 'vitepress'
const router = useRouter()
// level 为 1-5 加权级,分级口径由宿主决定
const words: CloudWord[] = tags.map(t => ({ text: t.name, level: t.level, count: t.count }))
function goToTag(tag: string) {
router.go(withBase(`/tags?tag=${encodeURIComponent(tag)}`))
}
</script>
<template>
<WordCloud
:words="words"
:height="200"
:font-min="13"
:font-max="26"
aria-label="标签词云"
@select="goToTag"
/>
</template>分级口径由宿主决定是刻意的设计:标签计数的分布千站千面,线性分级、对数分级还是分位数分级,组件不替你猜。一个实用的对数分级(数据 loader 里做):
function levelOf(count: number, maxCount: number): number {
const ratio = Math.log(count + 1) / Math.log(maxCount + 1)
return Math.min(5, Math.max(1, Math.round(ratio * 5)))
}WordCloud——词云组件(同时作为具名导出与默认导出)CloudWord——{ text, level, count? }:text 词文本,level 为 1-5 加权级(映射字号与着色层),count 可选计数(只用于 aria 朗读文案,如「Vue (12)」)组件没有插槽、没有 v-model、没有 defineExpose,API 面就是 props + 一个 select 事件。
CloudWord[]词集。words 引用变化时自动重排布局(watch),标签页筛选后词集收窄的场景无需手动触发。
string | nullnull高亮词。匹配的词条常驻品牌色加粗,并带 aria-pressed="true"——典型用法是标签页把当前筛选中的标签传进来,词云同时充当「当前筛选指示器」:
<WordCloud :words="words" :selected="currentTag" @select="toggleTag" />number320画布高(px)。同时经 v-bind(height) 写进容器 min-height——布局是异步的(onMounted 后才有词),占位高度保证 SSR 首帧到词浮现之间页面不跳动。
number16 / 461 级与 5 级词的字号(px),中间层级线性插值(fontMin + (level - 1) / 4 × (fontMax - fontMin))。侧栏小词云把两端都压小,全页词云可以放开:
<!-- 侧栏紧凑版 -->
<WordCloud :words="words" :height="200" :font-min="13" :font-max="26" />
<!-- 标签页全宽版 -->
<WordCloud :words="words" :height="400" :font-min="16" :font-max="52" />注意字号区间直接影响装填密度:词多而画布小时调低 fontMax,否则放不下的词会被 d3-cloud 丢弃。
number42确定性布局种子。同数据 + 同种子 + 同容器宽 = 恒定布局。对当前布局不满意(比如最长的词被竖排了)不用改数据,换个种子重掷一次即可:
<WordCloud :words="words" :seed="7" />number0.25竖排词比例(0-1)。每个词消费一次随机数,小于该值则旋转 90°。0 全横排(可读性最好),0.25 是经典词云的错落感,1 全竖排(除非做视觉实验,不建议)。
string'Word cloud'容器无障碍名。容器是 role="group"(内部词条各自可聚焦,与 canvas 的「一张哑图」形成对比),中文站点记得覆盖:
<WordCloud :words="words" aria-label="标签词云" />| 名称 | 载荷 | 说明 |
|---|---|---|
select | text: string | 点击或键盘(Enter / Space)激活词条时触发 |
Space 键按下时组件内部 preventDefault,防止页面随空格滚动。跳转、筛选等行为交给宿主。
洗牌问题的根源在 d3-cloud 源码里有三处消费随机源(index.js 的 L29/66/116,源码注释里留了取证):词的初始落点、螺旋展开方向、旋转判定。好在 d3-cloud 提供了 .random() 注入口——组件把 Math.random 换成 mulberry32:
function mulberry32(seed: number): () => number {
let state = seed
return () => {
state |= 0
state = (state + 0x6D2B79F5) | 0
let t = Math.imul(state ^ (state >>> 15), 1 | state)
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
}
}这是一个仅 32 位状态的 PRNG:快、小、统计质量对布局场景绰绰有余。每次布局用 props.seed 重建生成器,于是「随机」变成了种子的纯函数——刷新、路由往返、明暗切换,词的位置纹丝不动。
d3-cloud 的碰撞检测靠 canvas measureText 量词的包围盒。这里有个隐蔽陷阱:CSS 变量(如 var(--vp-font-family-base))在 canvas 字体串里不生效,直接传会静默落回默认字体——量的是衬线体、画的是无衬线体,碰撞盒对不上,词就叠在一起。组件的解法是布局期以 getComputedStyle(document.body).fontFamily 取已求值的真实字体串传给 d3-cloud,SVG 渲染继承 body 同源字体,测量与渲染严格同串。
d3-cloud 输出的只是每个词的 { x, y, rotate, size },渲染层是 Vue 模板里的 SVG text:
fill 走 CSS 变量:暗色切换零重渲染,浏览器自己换色着色按权重分层:1-3 级依次取 --vp-c-text-3 / --vp-c-text-2 / --vp-c-text-1,4 级混入 60% 品牌色(color-mix),5 级纯品牌色加粗——权重越高越「亮」,明暗两态自动适应。词浮现时有 0.5s 淡入动画。
dist 内含 .vue 源文件,消费方必须配置,否则 SSR 构建报错:
// .vitepress/config.ts
export default defineConfig({
vite: { ssr: { noExternal: ['@vaultkit/vitepress-plugin-word-cloud'] } },
})document.createElement('canvas'),只能客户端执行;SSR 输出固定高度占位容器,词组挂载后淡入--vp-c-* 语义变量,需自行提供同名变量role="button" + tabindex="0" + aria-pressed + focus 虚线轮廓;count 提供后朗读文案带计数vue ^3.5.0;运行时依赖仅 d3-cloudQ:有些词不见了? d3-cloud 放不下的词会被丢弃。要么加大 height、要么调低 fontMax、要么减少词数(比如只取 top 40)。
Q:为什么窗口拉伸后布局变了? 布局是「数据 + 种子 + 容器宽」的函数,容器宽变了自然重排。同宽度下永远恒定。
Q:能不能不转竖排?:rotate-ratio="0" 即可全横排。