前言
在我编写的在线字典网站中,我碰到了如下一个需求,如下图所示:
让我为你叙述这个需求需要做什么。
需求分析
在markdown文档中,我希望实现一个语音图标,点击语音图标就可以阅读对应的拼音,虽然这只是一个简单的小需求,但具体实现还是有点复杂的。
实现思路
我们可以将这个小需求划分成两步:
- 语音图标的来源?
- 如何实现朗读?
对于语音图标,我们可以导入element plus图标组件,然后注册到vitepress中,然后在markdown文档中编写这个组件。
具体实现
导入element plus组件
首先,我们需要安装相关依赖,打开终端输入如下命令安装相关依赖:
pnpm add element-plus @element-plus/icons-vue
接着我们需要在vitepress中导入element plus组件,根据theme api,我们需要在.vitepress目录下新建一个theme目录以及一个index.mts文件,如下图所示:
在该文件中,我们需要导出一个对象,该对象我们需要用到2个属性,既extends和enhanceApp属性,extends用于继承默认的主题,而enhanceApp是一个函数,函数暴露了一个app属性,而这个属性就是一个vue实例,我们可以通过这个实例来注册一个组件。现在我们的文件内的代码应该如下所示:
// .vitepress/theme/index.mts
// 导入element plus组件和样式
import ElementPlus from "element-plus";
import "element-plus/theme-chalk/index.css";
// 导入默认主题
import DefaultTheme from "vitepress/theme";
//导入图标组件
import * as ElementPlusIconsVue from '@element-plus/icons-vue';
export default {
extends: DefaultTheme, // 继承默认主题
enhanceApp({ app }) {
// 注册element组件
app.use(ElementPlus);
// 注册element图标组件
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
},
};
以上,我们通过app.use和app.component来注册element plus组件和element plus图标组件,我们通过从vitepress/theme中导入默认的依赖。
注册自定义组件
这样我们就可以使用element组件和图标组件了,接下来,我们需要注册一个语音朗读组件。如下所示:
// .vitepress/theme/index.mts
// ...
import ReadText from "../components/read-text.vue";
export default {
// ...
enhanceApp({ app }) {
// ...
app.component("ac-read-text", ReadText);
},
};
我们同样使用app.compoent方法来注册我们的自定义组件。
语音朗读组件的具体实现
接下来我们就要实现一个语音朗读组件,首先我们的语音朗读组件的html结构应该如下所示:
<el-text @click="onReadText">
<slot></slot>
<el-icon style="font-size: 20px;" class="cursor-pointer read-text-icon">
<Microphone />
</el-icon>
</el-text>
也就是说,我们使用el-text组件包裹这个icon元素,注意这里会有插槽slot,也就是说插槽slot就是我们需要朗读的内容,我们在使用的时候就会如下所示:
// md文档中
<ac-read-text>比</ac-read-text>
这样,我们最终会在“比”字的右边出现一个语音朗读图标,点击这个图标就可以实现朗读“比”字的效果。
我们添加了两个类名,顾名思义就是稍微调整一下样式,样式代码如下所示:
.read-text-icon:hover {
color:#2396ef;
}
.cursor-pointer {
cursor: pointer;
}
主要就是添加了一个悬浮和一个鼠标手型效果。
核心朗读逻辑
接下来,我们实现一个朗读的脚本逻辑,核心代码如下所示:
<script setup>
import { ElMessage } from 'element-plus';
function isSpeechSynthesisSupported() {
return 'speechSynthesis' in window && typeof window.speechSynthesis.speak === 'function';
}
const findParentElement = (el, className) => {
while (!el?.classList.contains(className)) {
el = el?.parentElement;
}
return el;
};
const onReadText = (e) => {
const target = e.target;
if (!target) {
return;
}
if (['svg', 'path', 'el-icon'].includes(target.tagName.toLowerCase())) {
const element = findParentElement(target,'el-text');
const text = element.innerText;
if (!text) return;
if (!isSpeechSynthesisSupported()) {
return ElMessage.info('当前环境不支持阅读文本功能!');
}
const utterance = new SpeechSynthesisUtterance();
utterance.text = text;
// 设置语言为中文
utterance.lang = 'zh-CN';
// utterance.rate = 0.4; // 设置语速,范围是0.1到10
// utterance.pitch = 2; // 设置音调,范围是0到2
window.speechSynthesis.speak(utterance);
}
}
</script>
让我们一一来看每一段代码所代表的意思,这段代码主要功能是通过浏览器的语音合成 API 实现对文本内容的朗读,并处理一些基本的用户交互逻辑。下面是这段代码的详细解释:
1. isSpeechSynthesisSupported 函数
function isSpeechSynthesisSupported() {
return 'speechSynthesis' in window && typeof window.speechSynthesis.speak === 'function';
}
这个函数用于检查当前环境是否支持浏览器的语音合成功能(Speech Synthesis)。具体来说,它检查 window.speechSynthesis 是否存在,以及 window.speechSynthesis.speak 是否是一个函数。语音合成 API 只有在浏览器支持的情况下才能使用,如果返回 true,说明可以使用语音合成,反之则不能。
2. findParentElement 函数
const findParentElement = (el, className) => {
while (!el?.classList.contains(className)) {
el = el?.parentElement;
}
return el;
};
这个函数用于查找某个元素的父级元素,直到找到一个包含指定类名的元素为止。它接受两个参数:
el:要查找的起始元素。className:目标类名。
函数会从 el 开始,逐级查找父元素,直到找到一个具有指定类名的父元素。如果没有找到,它会一直向上查找直到 el 是 null 或 undefined。
3. onReadText 函数
const onReadText = (e) => {
const target = e.target;
if (!target) {
return;
}
if (['svg', 'path', 'el-icon'].includes(target.tagName.toLowerCase())) {
const element = findParentElement(target,'el-text');
const text = element.innerText;
if (!text) return;
if (!isSpeechSynthesisSupported()) {
return ElMessage.info('当前环境不支持阅读文本功能!');
}
const utterance = new SpeechSynthesisUtterance();
utterance.text = text;
// 设置语言为中文
utterance.lang = 'zh-CN';
// utterance.rate = 0.4; // 设置语速,范围是0.1到10
// utterance.pitch = 2; // 设置音调,范围是0到2
window.speechSynthesis.speak(utterance);
}
}
这个函数主要负责处理文本的朗读。其流程如下:
- 事件参数
e: 这是一个事件对象,通常会由用户触发某个操作(如点击)时传入。e.target是事件触发的目标元素。 - 检查
target元素: 如果目标元素存在,则继续执行后续逻辑。 - 过滤
svg,path,el-icon标签: 如果目标元素是svg,path或el-icon标签之一,它会尝试在该元素的父元素中查找一个具有el-text类名的元素。 - 获取文本内容: 如果找到了具有
el-text类的父元素,获取其innerText,即该元素中的文本内容。 - 检查文本内容: 如果没有获取到文本内容,则退出函数。
- 检查语音合成功能支持: 如果浏览器不支持语音合成功能,则通过
ElMessage.info弹出提示框,告诉用户当前环境不支持文本阅读。 - 语音合成: 如果语音合成功能可用,则创建一个
SpeechSynthesisUtterance对象,并将要朗读的文本赋值给它。- 设置语言为中文(
zh-CN)。 - 你可以设置语速(
utterance.rate)和音调(utterance.pitch),但这两行代码被注释掉了。
- 设置语言为中文(
- 开始朗读: 使用
window.speechSynthesis.speak(utterance)启动语音合成,朗读文本。
总结
这段代码的目的是通过点击某些元素(如 svg、path 或 el-icon)来触发文本朗读功能。它会:
- 查找元素中的文本内容。
- 检查是否支持语音合成功能。
- 使用浏览器的语音合成 API 朗读该文本。
如果当前环境不支持语音合成,它会通过 ElMessage 提示用户当前不支持语音朗读功能。
综合
将以上代码组合一下,就成了我们的ACReadText组件,接下来我们只需要使用该组件即可。
自定义匹配
如果为md文档的每一个需要朗读的字或者拼音,我们都需要去写html标签,那也太麻烦了,接下来还是和前文一样的思路,我们需要通过正则来匹配关键字符串,然后替换成这个组件即可。
为此,我约定了通过readxxxread,也就是说,将想要阅读的文本通过两个read字符包裹,即可渲染成该组件。我们需要修改一下markdown属性,如下所示:
// .vitepress/config.mts
export default defineConfig({
// ...
markdown: {
config: (md) => {
md.renderer.rules.text = (tokens, idx) => {
const text = tokens[idx].content;
// ...
const transformedReadText = transformedText.replace(/read(.*?)read/g, (_match, p1) => `<ac-read-text>${p1}</ac-read-text>`);
return transformedReadText;
};
},
},
})
/read(.*?)read/g这个正则表达式和前文同一个原理,这里就不做过多解释。完成以上工作之后,我们就可以在markdown文档里面随便写文档既可以实现语音朗读文本的功能。如下一个示例:
// md文档
read你好read
总结
通过上述的步骤,我们成功地实现了一个语音朗读功能,允许用户点击图标朗读拼音或文字,通过正则表达式处理,我们还优化了使用体验,使得 Markdown 中需要朗读的内容自动转换为组件。