markdown版打字机组件实现

前言

随着2024年国内业务需求的持续发展,前端领域打字机效果组件已广泛应用于多种网站和应用程序中,有效增强了网站的动态性与交互性。本文旨在深入探讨如何从基础开始,逐步开发实现简易版的一个打字机效果的流式组件,包括示例代码及其详细解释。

效果

打字机效果

一、初始准备

开始开发前,我们首先需要准备一些基础内容。本次示例中,我们将以一个Markdown文本数据作为打字机效果的内容来源。这不仅能够模拟实际开发中的场景,也便于我们展示如何处理和展示复杂文本。

示例Markdown文本

我们的Markdown文本包含了标题、文本格式(如粗体、斜体)、图片和表格等元素,这些都是Markdown常用的标记元素。通过对这些不同类型的内容进行逐字展示,我们可以演示打字机效果处理复杂文本的能力。

export const mockMarkdownStr = `
# Markdown 流式文章示例
Markdown是一种轻量级标记语言,它允许人们使用易读易写的纯文本格式编写文档。Markdown编写的文档后缀为 \`.md\`。在这篇文章中,我将向您展示如何使用Markdown创建一篇包含标题、文本、图片和表格的文章。

## 标题
在Markdown中,标题是通过在文字前面加上 \`#\` 来表示的。\`#\` 的数量代表标题的级别。例如,一个 \`#\` 代表一级标题,两个 \`##\` 代表二级标题,以此类推。

## 文本
Markdown支持普通的文本格式,如粗体、斜体、删除线和下划线。您可以使用以下方式创建这些文本格式:
- **粗体**:使用两个 \`\*\` 或 \`_\` 包围文本,例如 \`\*\*粗体\*\*\` 或 \`__粗体__\`
- *斜体*:使用一个 \`\*\` 或 \`_\` 包围文本,例如 \`\*斜体\*\` 或 \`_斜体_\`
- ~~删除线~~:使用两个 \`~\` 包围文本,例如 \`~~删除线~~\`
- <u>下划线</u>:使用HTML标签 \`<u>\` 和 \`</u>\` 包围文本,例如 \`<u>下划线</u>\`

## 图片
在Markdown中,您可以使用以下语法插入图片:
\`\`\`
![图片描述](图片地址 "可选的标题")
\`\`\`
例如:
\`\`\`
![这是一张小米SU7图片](https://s1.xiaomiev.com/activity-outer-assets/0328/images/su7/su7_1.jpg)
\`\`\`

## 表格
Markdown支持简单的表格创建。您可以使用以下语法创建表格:
\`\`\`
| 标题1 | 标题2 | 标题3 |
|-------|-------|-------|
| 单元格1 | 单元格2 | 单元格3 |
| 单元格4 | 单元格5 | 单元格6 |
\`\`\`
例如:
\`\`\`
| 姓名 | 年龄 | 性别 |
|------|------|------|
| 张三 | 25   | 男   |
| 李四 | 22   | 女   |
\`\`\`

## 结束语
以上是Markdown的基本用法,通过这些简单的标记,您可以创建一篇包含标题、文本、图片和表格的文章。Markdown的语法简单易懂,适合快速排版和分享文档。希望这篇文章对您有所帮助!
`;

二、代码实现

实现打字机效果涉及三个部分:

  • 核心逻辑TypeWriterCore
  • Hook封装useTypeWriter
  • React组件实现TypeWriter Components
    三个部分业务侧可按需取用,以下部分将详细介绍每一部分的实现逻辑及关键代码。

1. TypeWriterCore.ts - 打字机核心逻辑

TypeWriterCore.ts文件中定义了TypeWriterCore类,这个类封装了打字机效果的核心逻辑。通过构造函数,我们可以传入不同的配置选项,如打字速度、暂停时间等,以适应不同的使用场景。

interface TypeWriterCoreOptions {
    onConsume: (str: string) => void; // 定义一个回调函数,用于消费(处理)字符
    maxStepSeconds?: number; // 可选属性,定义最大步进间隔(毫秒)
}

export default class TypeWriterCore {
    onConsume: (str: string) => void; // 消费(处理)字符的回调函数
    queueList: string[] = []; // 存储待消费字符的队列
    maxStepSeconds: number = 50; // 默认最大步进间隔为50毫秒
    maxQueueNum: number = 2000; // 队列中最大字符数
    timer: number | undefined; // 用于控制下一次消费的定时器

    constructor({onConsume, maxStepSeconds}: TypeWriterCoreOptions) {
        this.onConsume = onConsume; // 初始化消费字符的回调

        if (maxStepSeconds !== undefined) {
            this.maxStepSeconds = maxStepSeconds; // 如果提供了最大步进间隔,则使用提供的值
        }
    }

    // 动态计算消费字符的速度
    dynamicSpeed() {
        const speedQueueNum = this.maxQueueNum / this.queueList.length; // 根据队列长度动态调整速度
        const resNum = +(
            speedQueueNum > this.maxStepSeconds
                ? this.maxStepSeconds : speedQueueNum
        ).toFixed(0); // 确保结果为整数

        return resNum;
    }

    // 将字符串添加到队列中
    onAddQueueList(str: string) {
        this.queueList = [...this.queueList, ...str.split('')]; // 分解字符串为字符数组并追加到队列
    }

    // 添加字符串到队列的公共方法
    add(str: string) {
        if (!str) return; // 如果字符串为空,则不执行任何操作
        this.onAddQueueList(str); // 调用内部方法添加字符串到队列
    }

    // 从队列中消费一个字符
    consume() {
        if (this.queueList.length > 0) {
            const str = this.queueList.shift(); // 从队列头部移除一个字符
            str && this.onConsume(str); // 如果字符存在,则调用消费函数处理该字符
        }
    }

    // 定时消费队列中的字符
    next() {
        this.timer = setTimeout(() => {
            if (this.queueList.length > 0) {
                this.consume(); // 消费一个字符
                this.next(); // 递归调用,继续消费下一个字符
            }
        }, this.dynamicSpeed()); // 根据动态速度设置定时器
    }

    // 开始消费队列中的字符
    start() {
        this.next(); // 调用next方法开始消费字符
    }

    // 渲染完成后的清理工作
    onRendered() {
        clearTimeout(this.timer); // 清除定时器,防止继续消费字符
    }

    // 清空队列并停止当前的消费过程
    onClearQueueList() {
        this.queueList = []; // 清空字符队列
        clearTimeout(this.timer); // 清除定时器
    }
}

2. useTypeWriter.ts - Hook封装

通过HookuseTypeWriter封装TypeWriterCore类,提供简洁的接口,使得在React或Vue组件中易于实现打字机效果。

代码示例如下:

import {useEffect, useState, useMemo} from 'react';
import TypeWriterCore from './TypeWriterCore';

interface UseWriterOptions {
    maxStepSeconds?: number; // 将 maxStepSeconds 定义为可选的
}

export const useTypeWriter = (
    {text, options}:
    { text: string, options?: UseWriterOptions }
) => {
    const [typedText, setTypedText] = useState('');

    const typingCore = useMemo(
        () => new TypeWriterCore(
            {
                onConsume: (str: string) => setTypedText(prev => prev + str),
                ...options,
            }
        ),
        []
    );

    useEffect(
        () => {
            typingCore.onRendered(); // 渲染完成 => 清空定时器
            typingCore.add(text);
            typingCore.start();

            return () => typingCore.onRendered(); // 渲染完成 => 清空定时器
        },
        [text]
    );

    return [typedText];
};

3. index.tsx - 组件实现示例

这个文件展示了如何在React组件中使用useTypeWriterHook来实现打字机效果。以下是实现的关键部分:

import React from 'react';
import ReactMarkdown from 'react-markdown';
import {useTypeWriter} from './useTypeWriter'; // 替换为实际的导入路径
import TypeWriterCore from './TypeWriterCore';


interface TypingWriterProps {
  text: string;
  options?: {
    maxStepSeconds?: number;
  };
}

const TypingWriter: React.FC<TypingWriterProps> = ({text, options = {}}) => {
    const [typedText] = useTypeWriter({text, options});

    return (
        <div>
            <ReactMarkdown>
                {typedText}
            </ReactMarkdown>
        </div>
    );
};


export {
  TypingWriter,
  TypeWriterCore,
  useTypeWriter,
};

通过这三个文件的详细解析和代码实现,我们展示了从核心逻辑的构建到在React组件中的应用,如何逐步开发一个打字机效果的流式组件。

三、应用示例

1、模拟流式文本消息推送:

以下示例展示如何模拟SSE(Server-Sent Events)文本消息推送,模拟实时数据流的场景。

export const simulateWebSocketPush = (text, onDataReceived) => {
    const words = text.split(/([\s,。;:!、])/); // 在标点符号前后插入分隔符,以便保留标点符号
    let currentIndex = 0;

    // 模拟推送函数
    function pushNextChunk() {
        const chunkSize = Math.floor(Math.random() * 5) + 1; // 随机生成 1 到 5 的字数
        const currentChunk = words.slice(currentIndex, currentIndex + chunkSize).join('');
        currentIndex += chunkSize;

        // 模拟推送,实际中需要通过 WebSocket 推送给客户端
        // 这里将数据通过回调函数传递给调用方
        onDataReceived(currentChunk);

        // 继续推送,直到所有文字都被推送完
        if (currentIndex < words.length) {
            const interval = Math.floor(Math.random() * 500) + 1000; // 随机生成时间间隔
            setTimeout(pushNextChunk, interval);
        }
    }

    // 开始推送
    pushNextChunk();
};

2. 使用示例React

此示例展示如何在应用中模拟文本数据传入,实现打字机效果。

import {useEffect, useState} from 'react';
import {TypingWriter} from 'ui-type-writer';
import {simulateWebSocketPush} from '@/utils';
import {mockMarkdownStr} from '@/mock/index'

export default function App() {
    const [markdownContent, setMarkdownContent] = useState(' ');

    useEffect(
        () => {
        // 在组件挂载时开始模拟WebSocket推送
            simulateWebSocketPush(mockMarkdownStr, data => {
            // 这里处理每次推送的数据,可以将数据存储到状态中,或者进行其他操作
                // 在每次推送时拼接数据
                setMarkdownContent(data);
            });
            // 清理定时器或其他资源
            return () => {};
        },
        []
    );

    return (
        <TypingWriter text={markdownContent} />
    );
}

npm包发布

打字机效果组件已发布到npm,未进行预编译,可以在目录下直接调试。

npm install  ui-type-writer

希望本文能为您提供有价值的参考与指导,如有疑问或建议,敬请留言讨论。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,470评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,393评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,577评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,176评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,189评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,155评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,041评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,903评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,319评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,539评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,703评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,417评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,013评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,664评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,818评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,711评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,601评论 2 353

推荐阅读更多精彩内容