Javascript 如何在不向DOM渲染任何内容的情况下计算文本高度?

Javascript 如何在不向DOM渲染任何内容的情况下计算文本高度?,javascript,html,dom,react-virtualized,Javascript,Html,Dom,React Virtualized,我正在利用一个虚拟化列表(),其中列表项的高度是必需的,并且可能会有很大的变化。由于变化很大,我给图书馆的任何高度估计都会带来糟糕的体验 通常的高度计算方法如下: const containerStyle={ 显示:“内联块”, 位置:“绝对”, 可见性:“隐藏”, zIndex:-1, }; 导出常量measureText=(文本)=>{ const container=document.createElement(“div”); container.style=containerStyle;

我正在利用一个虚拟化列表(),其中列表项的高度是必需的,并且可能会有很大的变化。由于变化很大,我给图书馆的任何高度估计都会带来糟糕的体验

通常的高度计算方法如下:

const containerStyle={
显示:“内联块”,
位置:“绝对”,
可见性:“隐藏”,
zIndex:-1,
};
导出常量measureText=(文本)=>{
const container=document.createElement(“div”);
container.style=containerStyle;
container.appendChild(文本);
文件.正文.附件(容器);
const height=container.clientHeight;
const width=container.clientWidth;
container.parentNode.removeChild(容器);
返回{高度,宽度};
};
不幸的是,当您处理具有不同大小的项目的超大列表时,这是无法实现的。虽然缓存可能会被利用,但当您需要在一开始就知道总高度(所有项目的合并高度)时,即使这样也不太好

第二个经常使用的解决方案是通过HTML画布“
measureText
”。性能类似于上面的DOM操作

就我而言,我知道以下几点:

  • 容器宽度
  • 字体
  • 字号
  • 所有填充物
  • 所有边距
  • 任何和所有其他样式,如线条高度
我要寻找的是一个数学解决方案,它可以计算高度(或一个非常接近的估计值),这样我就不必依赖任何DOM操作,我可以随时得到高度

我想是这样的:

const measureText=(文本,选项)=>{
const{width、font、fontSize、padding、margins、borders、lineHeight}=选项;
//假设这个神奇的功能存在
//这完全取决于宽度、样式和字体信息
const numberOfLines=计算线(文本、选项);
常量内容高度=行数*行高;
const borderHeight=borders.width*2/(这都是伪代码……但不知怎么得到了像素厚度。
常量marginsHeight=margins.top+margins.bottom
常量paddingHeight=padding.top+padding.bottom
返回边距高度+填充高度+边距高度+内容高度;
}
在上面,我们缺少了
calculateLines
功能,这似乎是工作中最重要的部分。如何在这方面取得进展?我需要做一些预处理来计算字符宽度吗?因为我知道我使用的字体,这应该不是一个太大的问题,对吗

是否存在浏览器问题?每个浏览器的计算可能会有什么不同

还有其他参数要考虑吗?例如,如果用户有一些为他们放大文本的系统设置(可访问性),浏览器是否通过任何可用的数据来告诉我? 我知道渲染到DOM是最简单的方法,但我愿意将精力投入到公式化解决方案中,即使这意味着每次更改边距等。我需要确保函数的输入得到更新

更新:这可能有助于找到字符宽度:。下面有详细信息:。请转到

更新2:通过使用,宽度计算变得更加简单,因为您只需要测量一个字符的宽度。令人惊讶的是,列表中有一些非常漂亮和流行的字体,如Menlo和Monaco

大更新3:这是一个相当通宵的工作,但是通过更新1中SVG方法的启发,我想出了一个非常有效的方法来计算行数。不幸的是,我发现有1%的时间它会减少1行。下面是大致的代码:

const wordWidths = {} as { [word: string]: number };

const xmlsx = const xmlsn = "http://www.w3.org/2000/svg";

const svg = document.createElementNS(xmlsn, "svg");
const text = document.createElementNS(xmlsn, "text");
const spaceText = document.createElementNS(xmlsn, "text");
svg.appendChild(text);
svg.appendChild(spaceText);

document.body.appendChild(svg);

// Convert style objects like { backgroundColor: "red" } to "background-color: red;" strings for HTML
const styleString = (object: any) => {
  return Object.keys(object).reduce((prev, curr) => {
    return `${(prev += curr
      .split(/(?=[A-Z])/)
      .join("-")
      .toLowerCase())}:${object[curr]};`;
  }, "");
};

const getWordWidth = (character: string, style: any) => {
  const cachedWidth = wordWidths[character];
  if (cachedWidth) return cachedWidth;

  let width;

  // edge case: a naked space (charCode 32) takes up no space, so we need
  // to handle it differently. Wrap it between two letters, then subtract those
  // two letters from the total width.
  if (character === " ") {
    const textNode = document.createTextNode("t t");
    spaceText.appendChild(textNode);
    spaceText.setAttribute("style", styleString(style));
    width = spaceText.getBoundingClientRect().width;
    width -= 2 * getWordWidth("t", style);
    wordWidths[" "] = width;
    spaceText.removeChild(textNode);
  } else {
    const textNode = document.createTextNode(character);
    text.appendChild(textNode);
    text.setAttribute("style", styleString(style));
    width = text.getBoundingClientRect().width;
    wordWidths[character] = width;
    text.removeChild(textNode);
  }

  return width;
};

const getNumberOfLines = (text: string, maxWidth: number, style: any) => {
  let numberOfLines = 1;

  // In my use-case, I trim all white-space and don't allow multiple spaces in a row
  // It also simplifies this logic. Though, for now this logic does not handle
  // new-lines
  const words = text.replace(/\s+/g, " ").trim().split(" ");
  const spaceWidth = getWordWidth(" ", style);

  let lineWidth = 0;
  const wordsLength = words.length;

  for (let i = 0; i < wordsLength; i++) {
    const wordWidth = getWordWidth(words[i], style);

    if (lineWidth + wordWidth > maxWidth) {
      /**
       * If the line has no other words (lineWidth === 0),
       * then this word will overflow the line indefinitely.
       * Browsers will not push the text to the next line. This is intuitive.
       *
       * Hence, we only move to the next line if this line already has
       * a word (lineWidth !== 0)
       */
      if (lineWidth !== 0) {
        numberOfLines += 1;
      }

      lineWidth = wordWidth + spaceWidth;
      continue;
    }

    lineWidth += wordWidth + spaceWidth;
  }

  return numberOfLines;
};
const wordWidths={}为{[word:string]:number};
常量xmlsx=常量xmlsn=”http://www.w3.org/2000/svg";
const svg=document.createElements(xmlsn,“svg”);
const text=document.createElements(xmlsn,“text”);
const spaceText=document.createElements(xmlsn,“text”);
appendChild(文本);
appendChild(spaceText);
document.body.appendChild(svg);
//将样式对象(如{backgroundColor:“red”})转换为HTML的“backgroundColor:red;”字符串
constyleString=(对象:any)=>{
返回Object.keys(Object.reduce)(上一个,当前)=>{
返回`${(上一个+=curr
.分割(/(?=[A-Z])/)
.加入(“—”)
.toLowerCase())}:${object[curr]};`;
}, "");
};
const getWordWidth=(字符:字符串,样式:任意)=>{
const cachedWidth=字宽[字符];
if(cachedWidth)返回cachedWidth;
让宽度;
//边缘情况:裸空间(字符代码32)不占用任何空间,因此我们需要
//以不同的方式处理。用两个字母把它包起来,然后减去它们
//总宽度的两个字母。
如果(字符==“”){
const textNode=document.createTextNode(“t”);
appendChild(textNode);
setAttribute(“style”,styleString(style));
宽度=spaceText.getBoundingClientRect().width;
宽度-=2*getWordWidth(“t”,样式);
字宽[“”]=宽度;
removeChild(textNode);
}否则{
const textNode=document.createTextNode(字符);
appendChild(textNode);
setAttribute(“style”,styleString(style));
宽度=text.getBoundingClientRect().width;
字宽[字符]=宽度;
text.removeChild(textNode);
}
返回宽度;
};
const getNumberOfLines=(文本:字符串,最大宽度:数字,样式:任意)=>{
设numberOfLines=1;
//在我的用例中,我修剪所有空白,不允许一行有多个空格
//它还简化了这个逻辑。不过,目前这个逻辑还不能处理
//新线
const words=text.replace(/\s+/g,“”).trim().split(“”);
const spaceWidth=getWordWidth(“,样式);
设线宽=0;
有限公司