Emoji 已经成为我们日常沟通中不可或缺的一部分,本文就来了解一下在前端中如何玩转 Emoji!
Emoji 基本概念
前置知识
在学习 Emoji 之前,我们先来一些和 Emoji 相关的前置知识。
- Unicode:
a.定义: 一个国际标准,旨在为世界上几乎所有的字符(包括字母、数字、符号、标点符号和表情符号等)提供唯一的数字表示形式。它的主要目标是确保在不同的平台、操作系统和设备之间能够一致地表示和处理文本数据。
b.特点:
统一编码:Unicode 为每种语言中的每个字符分配了一个唯一的编号(称为码点),从而解决了不同编码系统之间的不兼容问题。
广泛覆盖:Unicode 包含了几乎所有现代语言的字符,并且还在不断扩展以支持更多的符号和历史文字。
多种实现方式:Unicode 支持多种编码格式,如 UTF-8、UTF-16 和 UTF-32,以便在不同的应用场景中灵活使用。
- 码点:
- 定义: 指在 Unicode 标准中为每个字符分配的一个唯一的数字值。它通常用十六进制表示,并带有前缀 U+ 来标识这是一个 Unicode 码点。
- 举例:
a.字母 A 的 Unicode 码点是 U+0041。
b.笑脸 Emoji 😊 的 Unicode 码点是 U+1F60A。
- 范围: Unicode 定义了从 U+0000 到 U+10FFFF 的码点范围,总共可以表示超过一百万个字符。这个范围分为以下几部分:
基本多文种平面:从 U+0000 到 U+FFFF,包含最常见的字符。
辅助平面:从 U+10000 到 U+10FFFF,用于表示较少见的字符和 Emoji。
编码方式: Unicode 有多种编码方式,常见的有 UTF - 8、UTF - 16 和 UTF - 32。
UTF - 8:一种可变长度的编码方式,使用 1 到 4 个字节来表示一个字符。它对 ASCII 字符采用单字节编码,兼容 ASCII 标准,因此在互联网上得到了广泛应用。
UTF - 16:使用 2 个或 4 个字节来表示一个字符,常用于操作系统和编程语言中,如 Java 和 JavaScript 默认使用 UTF - 16 编码。
UTF - 32:固定使用 4 个字节来表示一个字符,编码简单直接,但会占用较多的存储空间。
Emoji 是什么?
Emoji 是一种图形符号,最初由日本电信运营商在 1990 年代引入,用于增强短信和网页的表达能力。随着时间的发展,Emoji 已经成为通信中不可或缺的一部分,广泛应用于社交媒体、电子邮件、即时通讯工具等各种平台。
Emoji 本质上是 Unicode 字符集中的一部分,每个 Emoji 都有一个对应的唯一 Unicode 编码。在最新的 Unicode 16.0 版本中,共指定了 3790 个 Emoji 及其编码。
图片
Emoji 对应的 Unicode 编码:https://unicode.org/emoji/charts/full-emoji-list.html
在支持 Unicode 的环境中,如现代的文本编辑器、浏览器等,你可以直接输入 Emoji 字符来使用它们。许多操作系统都提供了便捷的 Emoji 输入方法,例如在 Windows 系统中,按下 win + . 组合键可以打开 Emoji 选择器;在 macOS 系统中,按下 Fn + E 组合键可以打开 Emoji 输入菜单。
图片
不知道你有没有发现,在不同系统/应用上,Emoji 长的都不太一样,这是为什么呢?其实,Unicode 为每个 Emoji 分配了唯一的码点,确保其在不同系统中代表相同的含义,但并没有规定每个 Emoji 的样式。因此,每个系统/应用都可以根据各自的设计风格对 Emoji 进行设计,所以就出现了多种风格的 Emoji。
图片
为了提高一致性,一些项目提供了标准化的 Emoji 集合,可以通过这些项目来自定义 Emoji 显示,如 Twemoji:https://github.com/googlefonts/noto-emoji
需要格外注意的是:不同的文化对 Emoji 的理解和使用可能有所不同。例如,某些 Emoji 在特定文化中可能有特殊的含义或象征意义。因此,在国际化应用中,需要特别注意 Emoji 的使用,以避免误解或冒犯。
Emoji 在前端的应用
表示
HTML
- 直接插入 Emoji:可以在 HTML 文件中的任何文本位置直接插入 Emoji。
<p>今天天气真好 🌞</p>
- 使用 Unicode 编码:如果无法直接输入 Emoji 或者希望使用其 Unicode 编码,可以使用 Unicode 转义序列。
<p>今天天气真好 ☀</p>
注意:&#x 后面跟随的是 Emoji 的 Unicode 码点的十六进制表示。例如,笑脸 😊 的 Unicode 码点是 U+1F60A,在 HTML 中表示为 😊。
CSS
- 使用 content 属性: 可以通过 CSS 的 ::before 或 ::after 伪元素以及 content 属性来插入 Emoji。
.emoji-before::before {
content: "😊"; /* 直接插入 Emoji */
}
.emoji-after::after {
content: "\1F60A"; /* 使用 Unicode 转义序列 */
}
注意: 在 CSS 中使用 Unicode 转义序列时,不需要 &#x 前缀,直接使用 \ 加上十六进制编码即可。
由于 Emoji 是彩色字符,通常不需要额外的颜色设置。不过,可以通过调整字体大小来控制 Emoji 的显示大小。
h1 {
font-size: 2em; /* 将字体大小放大两倍 */
}
JavaScript
在 JavaScript 中,可以通过以下方式来表示 Emoji:
- 直接插入 Emoji: 在现代 JavaScript 环境中,可直接在字符串里使用 Emoji 字符。因为现代编辑器和浏览器广泛支持 Unicode 字符,能正确识别和显示 Emoji。
console.log('Hello 😊'); // 输出: Hello 😊
- 使用 Unicode 编码: 每个 Emoji 都有对应的 Unicode 编码,可通过 Unicode 编码来表示 Emoji。基本多文种平面(BMP)内的 Emoji 用 \u 加 4 位十六进制编码表示;超出 BMP 的 Emoji 需用代理对或 \u{} 语法表示。
// BMP 内的 Emoji
const heart = '\u2764';
console.log(heart); // ❤
// 超出 BMP 的 Emoji,使用代理对
const rocket = '\uD83D\uDE80';
console.log(rocket); // 🚀
// 超出 BMP 的 Emoji,使用 \u{} 语法
const pizza = '\u{1F355}';
console.log(pizza); // 🍕
获取和设置码点
- 获取码点: 可以使用 codePointAt() 方法来获取字符串中某个位置的 Unicode 码点。
const smiley = '😊';
console.log(smiley.codePointAt(0).toString(16)); // 输出: 1f60a
- 设置码点: 可以使用 String.fromCodePoint() 方法从 Unicode 码点创建字符串。
console.log(String.fromCodePoint(0x1F60A)); // 输出: 😊
字符串操作
问题
在 JavaScript 中处理 Emoji 时,尤其是涉及字符串操作,存在一些常见的问题。
- 代理对: 在 JavaScript 里,字符串中的每个字符通常以 16 位(即 2 个字节)来表示,这遵循的是 UTF-16 编码规则。不过,Emoji 字符比较特殊,部分 Emoji 字符的编码超出了基本多文种平面(BMP),需要使用代理对来表示,也就是用两个 16 位编码单元来表示一个 Emoji 字符。因此,普通的 .length 属性可能会返回不正确的字符数,因为它会将代理对视为两个字符。
const emoji = '😊';
console.log(emoji.length); // 输出: 2 (而不是1)
- 组合字符: 带有修饰符的 Emoji 可能由多个码点组成。例如,👨👩👧 是由三个独立的 Emoji 组合而成的。因此,在使用 .length计算长度时,得到的结果是不准确的。
const familyEmoji = '👨👩👧';
console.log(familyEmoji.length); // 输出: 8 (而不是1)
- 截断问题: JavaScript 中的 slice、substring 或 substr 方法会按照 16 位编码单元来截取字符串,当使用这些方式截断包含 Emoji 的字符串时,若在截取过程中恰好截断了一个代理对,就会产生乱码。因为这些方法是无法正确处理代理对和组合字符的。
const emojiStr = '😀😃😄';
// 错误的截取,截断了代理对
const wrongSubStr = emojiStr.slice(0, 1);
console.log(wrongSubStr); // �
内置 API:Intl.Segmenter
ES2021 提供了 Intl.Segmenter,可以解决上面的问题,它是 ECMAScript 国际化 API 的一部分,用于根据语言和区域设置对字符串进行分割。它可以将文本分割成有意义的单元,如单词、句子或图元簇,从而更好地处理多语言文本。
Intl.Segmenter 提供了以下分割方式:
- Grapheme Cluster(图元簇):将字符串分割为用户感知的字符单位。这对于处理复杂的 Unicode 字符(如 Emoji 和组合字符)非常有用。
- Word(单词):将字符串分割为单词。
- Sentence(句子):将字符串分割为句子。
Intl.Segmenter 的基本用法如下:
- 'en':指定语言环境。可以根据需要更改为其他语言环境,如 'zh' 或 'fr'。
- { granularity: 'grapheme' }:指定分割粒度,可以是 'grapheme'、'word' 或 'sentence'。
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
在处理包含复杂 Unicode 字符(如 Emoji 和组合字符)的字符串时,确保准确计算字符数量。
const segmenter = new Intl.Segmenter([], { granularity: 'grapheme' });
const text = 'Hello 🤦🏻♂️ 👨👩👧!';
const segments = Array.from(segmenter.segment(text));
console.log(segments.length); // 输出: 10
第三方库:grapheme-splitter
对于上面这些问题,也可以使用grapheme-splitter库来解决,它是一个用于处理 Unicode 字符串的 JavaScript 库,专门用于将字符串分割成“图元簇”。图元簇是用户感知的一个字符单位,即使它可能由多个 Unicode 码点组成。例如,带有肤色修饰符或性别修饰符的 Emoji 实际上是由多个码点组成的,但用户通常将其视为一个整体。
grapheme-splitter 的使用场景:
- 统计字符数量:当需要准确统计字符串中用户实际看到的字符个数时,例如在文本输入框中限制字符数量,包含组合字符的字符串使用 .length 计算会不准确,使用 grapheme-splitter 可以得到正确结果。
import GraphemeSplitter from "grapheme-splitter";
const splitter = new GraphemeSplitter();
const str = '👨👩👧';
const length = splitter.countGraphemes(str);
console.log(length); // 输出:1
- 从字符串中提取特定位置的图元簇:
import GraphemeSplitter from "grapheme-splitter";
const splitter = new GraphemeSplitter();
const text = 'Hello 🤦🏻♂️ 👨👩👧!';
const graphemes = splitter.splitGraphemes(text);
console.log(graphemes); // 输出:["H", "e", "l", "l", "o", " ", "🤦🏻♂️", " ", "👨👩👧", "!"]
// 获取第一个图元簇
console.log(graphemes[0]); // 输出: H
// 获取第7个图元簇(包含复杂的Emoji)
console.log(graphemes[5]); // 输出: 🤦🏻♂️
// 获取最后一个图元簇
console.log(graphemes[graphemes.length - 1]); // 输出: !
- 字符串截取:在截取字符串时,确保不会截断组合字符,避免出现乱码或不完整的字符显示。
import GraphemeSplitter from "grapheme-splitter";
const splitter = new GraphemeSplitter();
const text = 'Hello 🤦🏻♂️ 👨👩👧!';
const graphemes = splitter.splitGraphemes(text);
// 截取前5个图元簇
const firstFive = graphemes.slice(0, 5).join("");
console.log(firstFive); // 输出: Hello
// 截取第6到第7个图元簇
const middlePart = graphemes.slice(6, 7).join("");
console.log(middlePart); // 输出: 🤦🏻♂️
// 截取最后1个图元簇
const lastFive = graphemes.slice(-2).join("");
console.log(lastFive); // 输出: 👨👩👧!
- 字符遍历:按用户感知的字符逐个遍历字符串,对每个字符进行特定操作。
import GraphemeSplitter from "grapheme-splitter";
const splitter = new GraphemeSplitter();
const str = '👨👩👧😃🤦🏻♂️';
const graphemes = splitter.splitGraphemes(str);
graphemes.forEach((grapheme) => {
console.log(grapheme);
});
验证
在处理 Emoji 时,验证输入是否包含有效的 Emoji 字符是一个常见的需求。这可以通过多种方式实现,包括正则表达式以及使用专门的库来帮助识别和验证 Emoji。
正则表达式
Emoji 是 Unicode 字符,因此可以使用正则表达式来匹配它们。然而,由于 Emoji 的种类繁多,并且新的 Emoji 不断被添加到 Unicode 标准中,编写一个全面的正则表达式可能会比较复杂。下面是一个例子:
const emojiRegex = /(?:[\u2700-\u27bf]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff])[\ufe0e\ufe0f]?(?:[\u0300-\u036f\ufe20-\ufe23\u20d0-\u20f0]|\ud83c[\udffb-\udfff])?(?:\u200d(?:[^\ud800-\udfff]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff])[\ufe0e\ufe0f]?(?:[\u0300-\u036f\ufe20-\ufe23\u20d0-\u20f0]|\ud83c[\udffb-\udfff])?)*/gu;
const text = "Hello 🤦🏻♂️ 👨👩👧! How are you?";
// 提取所有匹配的 Emoji
const matches = text.match(emojiRegex);
if (matches) {
console.log('Matches:', matches); // 输出: ["🤦🏻♂️", "👨👩👧"]
} else {
console.log('No emojis found.');
}
// 验证是否包含 Emoji
const hasEmoji = emojiRegex.test(text);
console.log('Contains emoji:', hasEmoji); // 输出: true
注意: 当使用正则表达式处理包含超出 BMP 的 Unicode 字符(如 Emoji)的字符串时,就需要使用 u 修饰符。u 修饰符的主要作用是开启 Unicode 模式,让正则表达式能够正确处理超过 \uFFFF 的 Unicode 字符。在 JavaScript 里,字符串中的字符默认以 UTF-16 编码存储,基本多文种平面(BMP)内的字符可以用一个 16 位的编码单元表示,但超出 BMP 的字符需要用两个 16 位的编码单元(代理对)来表示。在没有 u 修饰符的情况下,正则表达式会将代理对拆分成两个单独的编码单元进行处理;而使用 u 修饰符后,正则表达式能将代理对视为一个整体,从而正确匹配和处理这些字符。
第三方库:emoji-regex
emoji-regex 是一个专门用于匹配 Emoji 的 JavaScript 库。它可以准确地识别和匹配各种类型的 Emoji。
import emojiRegex from 'emoji-regex';
const regex = emojiRegex();
const text = "Hello 🤦🏻♂️ 👨👩👧! How are you?";
const matches = text.match(regex);
console.log(matches); // 输出: ["🤦", "🏻", "\u200d", "♂", "\ufe0f", "👨", "\u200d", "👩", "\u200d", "👧"]
Emoji 选择器
Emoji 选择器可帮助用户方便地插入 Emoji 字符。它在许多应用(如聊天软件、社交平台、文本编辑器等)中广泛使用。我们可以根据需要自定义 Emoji 选择器(可以借助 emojibase 库的数据来实现),也可以使用开源的 Emoji 选择器,以下是一些比较热门的 Emoji 选择器(npm包名称)。
- emoji-mart:
图片
- emoji-picker-react:
图片
- vue3-emoji-picker:
图片