Nuxt应用中外部字体导致文本换行计算错误的问题排查
问题解决:RobotMono字体下文本分行计算偏差导致换行异常
核心原因
- 字体加载时机不匹配:组件挂载后立即计算文本宽度,但此时RobotMono字体可能尚未加载完成,导致用系统 fallback 字体计算宽度,和实际渲染的宽度产生偏差。
- 动态span样式不完整:手动设置的span样式没有完全匹配
#text-container的渲染上下文,添加到body后无法继承容器的样式环境,导致宽度计算基准不一致。 - 容器宽度获取时机:
divideTextInLines中获取容器宽度时,DOM可能还未完成渲染,导致宽度值不准确。
修复方案
1. 等待字体加载完成再处理文本
利用document.fonts.ready确保RobotMono字体加载完毕后,再进行文本分割计算,避免用默认字体生成错误的宽度数据。
2. 让动态span继承容器样式
将动态创建的span添加到text-container内部,而非body,自动继承容器的所有字体相关样式,无需手动复刻所有属性。
3. 确保容器宽度准确获取
通过nextTick等待DOM更新完成后,再执行分行计算,保证容器宽度是最终渲染值。
修改后的完整代码
<template> <div class="typing-container"> <div id="text-container"> <div v-for="(line, lineId) in displayedLines" :key="lineId"> <span v-for="(letter, letterId) in line.line" :key="letterId"> <span v-if="data.letterIndex === data.absoluteLetterIndexex[lineId+data.lineIndex][letterId]" class="text-line"></span> <span :style="{color: data.colors[data.absoluteLetterIndexex[lineId+data.lineIndex][letterId]]}">{{letter}}</span> </span> </div> </div> <font-awesome-icon :icon="['fas', 'rotate']" /> </div> </template> <script setup lang="ts"> import { computed, reactive, onMounted, nextTick } from 'vue'; import { useTypingStore } from '../../store/typing/typingStore' import { Word, Line } from './types'; const store = useTypingStore(); const { $axios } = useNuxtApp(); const data = reactive({ text: "", letterIndex: 0, wordIndex:0, lineIndex: 0, colors: [] as string[], words: [] as Word[], lines: [] as Line[], absoluteLetterIndexex: [] as number[][] }) function keyPressed(event: KeyboardEvent){ const { key } = event; const regex = /^[a-zA-Z'.,:?!;()-]$/; const currentWord = data.words[data.wordIndex]; const lastWord = data.words[data.wordIndex-1] ?? null; if(regex.test(key)){ if(key === data.text[data.letterIndex]){ data.colors[data.letterIndex] = 'white' data.words[data.wordIndex].lettersLeft--; if(data.letterIndex === data.absoluteLetterIndexex[data.lineIndex+1][data.absoluteLetterIndexex[data.lineIndex+1].length-2]){ data.lineIndex++; } }else{ data.colors[data.letterIndex] = 'red'; } data.words[data.wordIndex].lastWritedIndex = data.letterIndex +1; data.letterIndex+=1; }else{ if(key === "Backspace"){ if(data.letterIndex <= 0 ){ return; } if(data.wordIndex-1 >=0 && data.letterIndex === currentWord.start && lastWord.lettersLeft === 0){ return; } if (currentWord.start > data.letterIndex - 1){ data.letterIndex = lastWord.lastWritedIndex; data.words[data.wordIndex].lastWritedIndex = data.words[data.wordIndex].start; data.wordIndex--; }else{ data.letterIndex-=1; if(data.colors[data.letterIndex] === "white"){ data.words[data.wordIndex].lettersLeft++; data.words[data.wordIndex].lastWritedIndex--; } data.colors[data.letterIndex] = "gray"; } } if(key === ' ' && currentWord.start !== data.letterIndex){ data.letterIndex = data.words[++data.wordIndex].start } } } function setData( text: string ){ let newWords: Word[] = []; let length = 0; let word = ""; let start=0,lastWritedIndex; for (var i = 0; i < text.length; i++) { if(text[i] !== ' '){ length++; word+=text[i]; }else{ newWords.push({ start, word, length, lastWritedIndex: start, index: newWords.length, lettersLeft: length }); start = i+1; word = ""; length= 0 } } data.words = newWords; data.text = text data.colors = (new Array(data.text.length).fill('gray')) // 等待DOM更新后再计算分行 nextTick(() => { data.lines = divideTextInLines(); }) } async function getRandomText(){ // 等待字体加载完成再获取文本并处理 await document.fonts.ready; const text = ((await $axios.get('http://metaphorpsum.com/paragraphs/6')).data.replace(/\n/g, ' ')); setData(text) } function divideTextInLines() { let lines: Line[] = []; let currentLine = ""; let cumulativeCharacters= 0; const textContainer = document.getElementById("text-container"); if(!textContainer) return lines; const containerWidth = textContainer.offsetWidth; for (let i = 0; i < data.words.length; i++) { const word = data.words[i].word; const lineWithWord = currentLine ? currentLine + " " + word : word; const lineWidth = getTextWidth(lineWithWord, textContainer); if (lineWidth <= containerWidth) { currentLine = lineWithWord; } else { currentLine+= " "; lines.push({ line: currentLine, cumulativeCharacters }); let lineAbsoluteIndexes = []; for (let j = 0; j < currentLine.length; j++) { lineAbsoluteIndexes.push(cumulativeCharacters + j); } data.absoluteLetterIndexex.push(lineAbsoluteIndexes); cumulativeCharacters += currentLine.length; currentLine = word; } } if (currentLine) { currentLine+= " "; lines.push({ line: currentLine, cumulativeCharacters }); let lineAbsoluteIndexes = []; for (let j = 0; j < currentLine.length; j++) { lineAbsoluteIndexes.push(cumulativeCharacters + j); } data.absoluteLetterIndexex.push(lineAbsoluteIndexes); } return lines; } function getTextWidth(text: string, container: HTMLElement) { const span = document.createElement('span'); // 继承容器的所有计算样式,确保宽度基准一致 const computedStyle = window.getComputedStyle(container); span.style.fontSize = computedStyle.fontSize; span.style.fontFamily = computedStyle.fontFamily; span.style.fontWeight = computedStyle.fontWeight; span.style.letterSpacing = computedStyle.letterSpacing; span.style.whiteSpace = 'nowrap'; span.style.visibility = 'hidden'; span.style.position = 'absolute'; // 避免影响容器布局 span.innerText = text; container.appendChild(span); const width = span.offsetWidth; container.removeChild(span); return width; } const colors = computed(()=>{ return data.colors; }) const displayedLines = computed(()=>{ return data.lines.slice(data.lineIndex, data.lineIndex+3); }) onMounted(()=>{ window.addEventListener("keydown", keyPressed ) getRandomText(); }) </script> <style lang="css" scoped> #text-container{ width: 80rem; max-width: 90%; font-size: 1.7rem; font-family: RobotMono; letter-spacing: normal; word-spacing: normal; } .text-line{ padding: 2px 1px; background-color: red; font-size: 1.7rem; font-weight: bold; } .typing-container{ display: flex; flex-direction: column; gap: 3rem; justify-content: center; align-items: center; } </style>
内容的提问来源于stack exchange,提问作者Giuliano Maradei
相关产品推荐
相关产品推荐

