自己在練習的時候,突然覺得很像用react解題,於是決定照慣例來寫個解題報告!!
題目
實現 打字 → 停留 → 刪字 → 換下一句 → 重複循環
在許多網站首頁(Hero Section)中,常會出現「打字機文字效果」(Typewriter Effect),透過逐字呈現的方式吸引用戶注意。
請你使用 React 與 SCSS 實作一個打字機效果元件,使其可以依序顯示多段文字,並於每段文字輸出完成後短暫停頓,再自動切換到下一段。
你的任務是設計一個元件 <Typewriter />,並滿足下列行為要求:
功能要求
- 逐字輸出文字
- 給定一段字串,需每隔固定時間顯示下一個字元。
- 顯示方式應類似打字機逐字呈現。
- 支援多段文字循環播放
- 輸入一個字串陣列
texts。 - 每段文字顯示完成後需:
- 保留完整文字一段時間(如:1 秒)
- 再自動切換到下一段文字
- 播放到最後一段後需重新回到第一段。
- 輸入一個字串陣列
- 文字切換時不得造成 DOM 閃爍或布局跳動
- 頁面不應因為文字長度變化而發生高度跳動(layout shift)。
- 切換過程中不得出現空白 DOM。
- 使用 React Hooks 實作
- 必須使用
useState、useEffect來控制字元增加與切換。 - 為避免 ESLint 警告,字串陣列需搭配
useMemo()保證不在每次 render 重新建立。
- 必須使用
- 樣式需使用 SCSS
- 實現文字閃爍游標(blinking cursor)
- 效果需以 SCSS 撰寫,例如動畫、過渡等。
輸入格式
- 傳入
<Typewriter />組件的 props:
interface TypewriterProps {
texts: string[]; // 欲顯示之多段文字
}
輸出格式
- 元件需渲染一段逐字顯示的文字,同時包含閃爍游標:
<span class="typewriter-text">目前顯示中的文字</span>
<span class="cursor"></span>
範例
若傳入:
texts = ['Hello', 'Welcome to my website', 'Frontend Developer'];
顯示流程應如下:
H →
He →
Hel →
Hell →
Hello →
(停 1 秒)
W →
We →
Wel →
…
Frontend Developer →
(停 1 秒)
→ 回到 Hello 重新開始
整個過程須連續平滑進行,不得有閃爍與跳動。
解題絲路
其實要實現打字機效果,核心絲路只有:
- 動畫效果
- 現在是打字還是刪字
- 現在要顯示哪一句
- 現在這句的第幾個字
設一個變數subIndex,追蹤現在的句子(S)的第幾字(i),如果是打字,subIndex++,反之亦然。
詳解採用Dynamic Style Injection。
我們可以先根據題目說的內容以及想法,把一些基礎的東西寫出來。
import { useState } from "react";
export default function Index({texts}: {string[]}) {
const [index, setIndex] = useState(0); // 現在第index句
const [subIndex, setSubIndex] = useState(0); // 現在第index句的subIndex個字
const [deleting, setDeleting] = useState(true); // 是否是打字狀態
const current = texts[index]; // 取出當前字
return (
<div>
{/* 用substring()方法取出子字串*/}
<span>{current.substring(0, subIndex)}</span>
<span className="cursor">|</span>
</div>
);
}
const style = document.createElement('style');
style.textContent = `
.cursor {
margin-left: 2px;
animation: blink 0.8s infinite;
}
@keyframes blink {
0%, 50% { opacity: 0; }
51%, 100% { opacity: 1; }
}
`;
document.head.appendChild(style);
基礎的東西寫完了,現在的問題是: 如何將字元一個個的顯示出來?
先來釐清打字機效果的真正意義:是一種 「時間驅動的動畫,隨時間變化的 state」
React 只有兩種方式能讓 state 隨時間變化:
- setTimeout
- useEffect
而 useEffect 是唯一會在 state 變化時執行副作用的HOOK,所以這題採用useEffect !!
開始來寫useEffect!!
useEffect(() => {
if (deleting && subIndex === current.length){ // 如果打字打到跟句子一樣長
const timeout = setTimeout(() => setDeleting(false), 1200);// 改成刪字,並停留1200秒
return () => clearTimeout(timeout); // 清理副作用
}
if (!deleting && subIndex === 0) {// 如果刪字刪到0 換下一句
setDeleting(true); // 開始打字
setIndex((index + 1) % texts.length);
return;
}
// 用 setTimeout 讓它每隔 60~120ms 動一次
const timeout = setTimeout(
() => setSubIndex(subIndex + (!deleting ? -1 : 1)),
deleting ? 120 : 60 // 打字慢刪字快
);
return () => clearTimeout(timeout);// 清理副作用
}, [subIndex, deleting, index, texts, current]);
css
const style = document.createElement('style');
style.textContent = `
.typewriter-line {
font-size: 15px;
white-space: nowrap;
display: inline-flex;
align-items: center;
height: 1.5em;
}
.cursor {
margin-left: 2px;
animation: blink 0.8s infinite;
}
@keyframes blink {
0%, 50% { opacity: 0; }
51%, 100% { opacity: 1; }
}
`;
document.head.appendChild(style);
記得在父層呼叫時,父層的css要固定高度,避免JS再換行的時候出現dom是Null,導致葉面其他元素被往上移。“height: 1.5em;“