React 打字机效果

973 阅读2分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第12天,点击查看活动详情

目录结构

|- Typist
    |- Typist.jsx
    |- Cursor.jsx
    |- Cursor.css
    |- TypistExample.jsx
    |- TypistExample.css

HTML 的结构,两个部分

文字显示部分 + 光标

return (
  <div className={`Typist ${className}`}>
    {innerTree}
    <Cursor />
  </div>
);

显然,需要从外部传入 classNamedelayGenerator(页面显示字数的速度)。定义一个数组 linesToType,接收传入的字符串。textLines 显示在页面上的字符串。

import React, { Component } from 'react';

export default class Typist extends Component {

  static defaultProps = {
    className: '',
    delayGenerator: () => 40,
  }

  constructor(props) {
    super(props);
    this.linesToType = [];

    if (props.children) {
      this.linesToType = [props.children];
    }
  }

  state = {
    textLines: [''],
  }

  render() {
    const { className } = this.props;
    const innerTree = this.state.textLines;

    return (
      <div className={`Typist ${className}`}>
        {innerTree}
        <Cursor />
      </div>
    );
  }
}

typeCharacter 函数,使用 Promise,当上一个字符 reslove 的时候,去调用下一个字符的 Promise

typeCharacter = (character, charIdx, lineIdx) => {
  console.log(character, lineIdx)
  return new Promise((resolve) => {
    const textLines = this.state.textLines.slice();

    textLines[lineIdx] += character;
    this.setState({ textLines });
    const delay = this.delayGeneratorComp();
    setTimeout(resolve, delay);
  });
}

delayGeneratorComp 函数去调用传入的 delayGenerator 函数,提供字符显示的速度。

delayGeneratorComp = () => {
  return this.props.delayGenerator();
}

utils.js

对于的传入数据,使用传入的 iterator 进行处理。

export function eachPromise(arr, iterator, ...extraArgs) {
  const promiseReducer = (prev, current, idx) => {
    return prev.then(() => iterator(current, idx, ...extraArgs))
  }
  return Array.from(arr).reduce(promiseReducer, Promise.resolve());
}
typeLine = (line, lineIdx) => {
  utils.eachPromise(line, this.typeCharacter, lineIdx)
}
typeAllLines(lines = this.linesToType) {
  utils.eachPromise(lines, this.typeLine)
}

最后在 React 的生命周期中调用这个函数

componentDidMount() {
  this.typeAllLines();
}

Aug-07-2022 16-55-03.gif

完整代码:

import React, { Component } from 'react';
import Cursor from './Cursor';
import * as utils from './utils';

export default class Typist extends Component {

  static defaultProps = {
    className: '',
    delayGenerator: () => 40,
  }

  constructor(props) {
    super(props);
    this.linesToType = [];

    if (props.children) {
      this.linesToType = [props.children];
    }
  }

  state = {
    textLines: [''],
  }

  componentDidMount() {
    this.typeAllLines();
  }

  delayGeneratorComp = () => {
    return this.props.delayGenerator();
  }

  typeAllLines(lines = this.linesToType) {
    utils.eachPromise(lines, this.typeLine)
  }

  typeLine = (line, lineIdx) => {
    utils.eachPromise(line, this.typeCharacter, lineIdx)
  }

  typeCharacter = (character, charIdx, lineIdx) => {
    console.log(character, lineIdx)
    return new Promise((resolve) => {
      const textLines = this.state.textLines.slice();

      textLines[lineIdx] += character;
      this.setState({ textLines });
      const delay = this.delayGeneratorComp();
      setTimeout(resolve, delay);
    });
  }

  render() {
    const { className } = this.props;
    const innerTree = this.state.textLines;

    return (
      <div className={`Typist ${className}`}>
        {innerTree}
        <Cursor />
      </div>
    );
  }
}

Curosr 组件

光标应该区分是否显示,所以应该使用 shouldRender 变量来控制。当 shouldRender === true 的时候,显示光标。

import React, { Component } from 'react';
import './Cursor.css';

export default class Cursor extends Component {
  constructor(props) {
    super(props);
    this._isReRenderingCursor = false;
    this.state = {
      shouldRender: true,
    };
  }

  componentDidUpdate() {
    if (this._isReRenderingCursor) { return; }
    this._reRenderCursor();
  }

  _reRenderCursor() {
    this._isReRenderingCursor = true;
    this.setState({ shouldRender: false }, () => {
      this.setState({ shouldRender: true }, () => {
        this._isReRenderingCursor = false;
      });
    });
  }

  render() {
    if (this.state.shouldRender) {
      return (
        <span className={`Cursor Cursor--blinking`}>
          |
        </span>
      );
    }
    return null;
  }
}

加上这个组件,来看一下效果。

Aug-07-2022 17-01-06.gif

页面的 CSS

.Typist .Cursor {
  display: inline-block;
}

.Typist .Cursor--blinking {
  opacity: 1;
  animation: blink 1s linear infinite;
}

@keyframes blink {
  0% {
    opacity: 1;
  }

  50% {
    opacity: 0;
  }

  100% {
    opacity: 1;
  }
}
.TypistExample a {
  color: inherit;
}

.TypistExample a:hover {
  background-color: #22BAD9;
  color: white;
  text-decoration: none;
}

.TypistExample-header {
  margin-top: 5%;
  margin-bottom: 15%;
  text-align: center;
  font-size: 3em;
  cursor: pointer;
  color: #22BAD9;
}

.TypistExample-message {
  color: hotpink;
  width: 400px;
  margin: auto;
}

.TypistExample-message .flash {
  color: #22BAD9;
  font-weight: bold;
  text-decoration: underline;
  animation-name: blinker;
  animation-duration: 0.7s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

@keyframes blinker {
  0% {
    opacity: 1.0;
  }

  50% {
    opacity: 0.0;
  }

  100% {
    opacity: 1.0;
  }
}

一个最基本的打字机效果就实现了。


本文代码来自 react-typist 组件的代码解析