[布局技巧] 两种方式实现word-wrap

87 阅读2分钟

背景

有时我们需要实现这样一种布局,将一系列词组放到一个容器里进行展示,但是不要在词组内部产生截断,主要用作静态文案展示:

image.png

自然的单词分界可以提高阅读效率,使信息获取更为高效。

前置准备

假如我们有以下代码(Vue):

<template>
  <div>
    <h2>正常截断</h2>
    <div class="text-container">{{ text }}</div>
    <h2>CSS实现单词自然换行</h2>
    <div class="text-container" v-html="cssFormattedText"></div>
    <h2>JS实现单词自然换行</h2>
    <div class="text-container">{{ jsFormattedText }}</div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      rawText: "2024年12月31日 16:00-18:00,2025年01月01日 16:00-18:00、19:00-20:00,2025年01月02日 16:00-18:00、19:00-20:00、21:00-22:00"
    };
  },
  computed: {
    text() {
      return this.rawText.split(/[,,]/).join('\n')
    }
  }
}
</script>

<style>
* {
  margin: 0;
}
h2 {
  margin-left: 20px;
}
.text-container {
  margin: 20px;
  width: 400px;
  padding: 10px;
  border: 1px solid #f0f0f0;
  white-space: pre-wrap;
}
.text-line {
  display: inline-flex;
  flex-wrap: wrap;
}
#measure-box {
  height: 0;
  padding: 0;
  border: none;
  overflow: hidden;
  visibility: hidden;
  white-space: nowrap;
}
</style>

正常截断的文案长这样:

image.png

CSS实现

为了通过CSS实现单词的自然换行,我们可以文本进行如下操作:

  1. 将文本拆分成行
  2. 对行内文案进行切词
  3. 将切词后的单词用标签包裹
  4. 利用flex布局进行渲染
export default {
  data() {
    return {
      rawText: "2024年12月31日 16:00-18:00,2025年01月01日 16:00-18:00、19:00-20:00,2025年01月02日 16:00-18:00、19:00-20:00、21:00-22:00"
    };
  },
  computed: {
    text() {
      return this.rawText.split(/[,,]/).join('\n')
    },
    cssFormattedText() {
      // 文案分行
      return this.rawText.split(/[,,]/).map(line => {
        // 切词
        const text = line.match(/[\s、]|[^\s、]+/g)
          ?.map(item => `<span class="word">${item}</span>`)
          .join('')
        return `<p class="text-line">${text}</p>`
      }).join('')
    }
  }
}

渲染后的HTML结构:

image.png

实现效果:

image.png

JS实现

CSS实现的自然换行不太好控制每一行中最后一个分词符的位置,比如上面的 "、",他可能会跑到下一行,也可能在本行展示:

image.png

通过JS实现则不会有上述"问题",同样的,JS实现单词自动换行的效果主要有以下步骤:

  1. 创建一个和容器宽度一致且不可换行、不可见的测量容器
  2. 对文案进行拆行
  3. 将每一行的文案放进测量容器里进行测量
  4. 如果当前行无法完整放置所有单词,则把最后一个单词放到下一行(新起一行)
  5. 重复第3步
computed: {
  jsFormattedText() {
    const el = document.getElementById('measure-box')
    if (!el) {
      return ''
    }
    const lines = this.rawText.split(/[,,]/)
    const cache = {}
    let i = 0
    const split = (id = '、') => {
      const subs = lines[i].split(id)
      if (subs.length > 1 && el.scrollWidth > el.clientWidth) {
        if (!cache[i]) {
          lines.splice(i, 1, subs.slice(0, -1).join(id), subs.at(-1))
        } else {
          lines.splice(i, 1, subs.slice(0, -1).join(id))
          lines[i + 1] = subs.at(-1) + id + lines[i + 1]
        }
        cache[i] = 1
        return true
      }
      return false
    }
    while (i < lines.length) {
      el.innerText = lines[i]
      if (split('、') || split(' ')) {
        continue
      }
      i += 1
    }
    return lines.join('\n')
  }
}

渲染效果:

image.png

完整代码