You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
// ./store/context.tsxconstinitialState: TypingState={text: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',input: '',seconds: 0,timerId: undefined};exportconsttypingContext=createContext<[TypingState,Dispatch<TypingAction<any>>]>([{}asTypingState,()=>{}]);exportconstTypingProvider: React.FC=({ children })=>{constvalue=useReducer(typingReducer,initialState);return(<typingContext.Providervalue={value}>{children}</typingContext.Provider>);};
// ./store/context.tsximportReact,{createContext,useReducer,useContext,Dispatch}from'react';import{typingReducer}from'./reducers';import{TypingState,TypingAction,TypingActionTypes}from'./types';constinitialState: TypingState={text: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',input: '',seconds: 0,timerId: undefined};exportconsttypingContext=createContext<[TypingState,Dispatch<TypingAction<any>>]>([{}asTypingState,()=>{}]);exportconstuseTypingContext=()=>{const[state,dispatch]=useContext(typingContext);constonInput=(value: string)=>{if(value.length<state.text.length&&!state.timerId){startTimer();}if(value.length>=state.text.length&&state.timerId){stopTimer();}dispatch({type: TypingActionTypes.CHANGE_INPUT,payload: value});};conststartTimer=()=>{consttimerId=setInterval(()=>dispatch({type: TypingActionTypes.TICK}),1000);dispatch({type: TypingActionTypes.SET_TIMER,payload: timerId});};conststopTimer=()=>{clearInterval(state.timerId);dispatch({type: TypingActionTypes.SET_TIMER});};constonReset=()=>{stopTimer();dispatch({type: TypingActionTypes.CHANGE_INPUT,payload: ''});dispatch({type: TypingActionTypes.RESET_TICK});};return{ state, onInput, onReset };};exportconstTypingProvider: React.FC=({ children })=>{constvalue=useReducer(typingReducer,initialState);return(<typingContext.Providervalue={value}>{children}</typingContext.Provider>);};
组件
一共有三个组件
Preview 组件用户展示示例文字, 包括用户输入和示例文字的差异也会用颜色在示例文字上标注
UserInput 组件渲染文本框, 供用户输入
SpeedInfo 组件展示用户打字的各种数据
Preview
text 和 input 状态均为两个字符串, 不同的是 text 是静态的, 而 input 会随着用户的输入而动态变化. 对于 text 上的每一个字母, 其索引位置如果有对应的 input 的字母, 则进行比较并进行 class 的标注, 否则保持不变. 具体代码如下
关于 Online IDE
每次想做有关 React + TS 的小项目或者 demo, 都需要用
npx create-react-app --template typescript
开一个项目到本地, 既耗费时间又占用资源. 能直接写 React + TS 的 Online IDE 目前只找到 StackBlitz 和 CodeSandbox. 前者关于 ts 的类型提示还是很有问题, 但是速度倒是挺快的. 而且最近新出了一个 feature 能直接运行 Node.js 程序. 后者我电脑带不动...Hot Reload 啥的延迟很高, 经常写着写着就报错, 过一会又自己好了.目前没有别的好办法, 要想比较好的测试开发体验还是只能老老实实本地开个脚手架然后用 vscode. 有考虑用 code-server 啥的部署一个, 但是又要花钱买服务器啥的就算了...
在不换电脑的前提下有比较靠谱的 Online IDE 可以推荐一下
效果
源码: https://stackblitz.com/edit/typing-speed-app
需求与分析
结合 Demo 可以看到, 当开始打字的时候上面的示例文字会实时显示所打的每个字母出否正确, 下面有三个数据显示. 第一个为总时间, 可以认为是一个时钟, 当开始打字的时候触发. 直到打字结束即字数和示例文字一样的时候时钟停止. 此时也无法再继续往输入框内输入文字. WPM 即 word per minutes, 每分钟多少个字. 这里的 word 定位为
1 word = 5 characters
. 最后显示的是正确的字母数, 外加一个按钮可以重新开始.需求明确了可以思考需要哪些基本状态, 以及对应的衍生状态, 这里直接列出来了, 一共可以需要 4 个基本状态:
解释一下, 由于上述的需求, 我们需要一个
text
规定示例文字, 其实这个不作为状态也可, 因为示例文字原则上是不会变的, 这里为了方便就归在状态里了,input
代表用户输入的文字, 是实时改变的.seconds
是定时器状态, 当计时器开始的时候每一秒会自动增加 1.timerId
是定时器 id, 因为我们虚监控定时器. 比如当用户开始打字的时候我们设置一个定时器. 此时timerId
是存在的. 当打字结束或者用户点击了 reset 之后timerId
需要被重设为undefined
衍生状态就有很多, 比如
correctCharacters
就可以由text
和input
得出.WPM
又可以由correctCharacters
和seconds
得出. 规定好了基本状态, 衍生状态都可以直接按需计算得出, 而无需放在初始状态里.实现
关于状态管理部分打算使用
useReducer + useContext
, 会和 redux 有点像. 不过类型部分应该不会写的非常严谨.types
该文件存放所有类型定义, 主要有
action
,state
,reducer
. 如下关于
action
的payload
类型这里简略的就用any
替代了, 严格上所有定义的action
都应该有关于其payload
的精确的类型, 然后通过union
合并成一个总的类型, 例如这样:类型部分会有点像
redux
, 更多可以直接参考 redux 源代码是怎么定义相关工具类型的. 或者参考我之前写过的文章: 简单用 React+Redux+TypeScript 实现一个 TodoApp这里用枚举一共定义了 4 种
action
类型, 具体为:CHANGE_INPUT
: 当用户开始输入会不断触发onChange
事件, 该action
也会不断被触发, 需要实时获取文本框即用户的输入SET_TIMER
: 设置定时器的动作, 当开始输入时设置定时器的 id, 结束时设回undefined
TICK
: 时钟动作, 初始为 0, 定时器开始后每一秒触发一次, 每次加一, 代表定时器的时间RESET_TICK
: 重设时钟, 重设为 0reducer
有了类型和
action
, 就可以完善reducer
, 即状态是如何根据action
变化的:注意这里的
reducer
是结合useReducer
这个 hook 一起使用的, 不像redux
里可以直接给参数赋值声明初始状态. 即useReducer(reducer, initialState)
.reducer
只需要负责状态的改变的逻辑部分即可.context
关于
context
部分, 需要明确我们需要把什么作为全局数据传入到组件中. 由于是结合useReducer
, 直接将useReducer
的返回值即[state, dispatch]
传入即可. 当然类型需要明确一下. 同时自定义一个Provider
作为容器存放全局数据. 整体架构大致如下:然后在根组件声明
TypingProvider
:在
TypingProvider
下的任何组件, 都可以通过useContext(typingContext)
获得全局数据[typingState, dispatch]
, 前者为当前的状态, 后者可用于发送action
修改状态.这里深入一点, 业务逻辑比如对应的方法可以放到组件里写, 也可以在选择自定义一个
hook
暴露出需要的方法, 组件只需要用这个 hook 即可.回顾 demo 需要整个流程大致是这样的:
SET_TIMER
action. 同时在定时器, 也就是setInterval
的回调里面不断触发TICK
action. 保证每秒都记录下时间. 这里注意如何去辨别第一次输入, 正常情况下只能onChange
事件的监听只存在与输入是否有变化, 判断是否为第一次需要加上两个条件:timerId
onChange
事件继续不断被监听, 回调函数需要不断触发CHANGE_INPUT
actiontimerId
设回undefined
reset
按钮需要将所有状态初始化, 包括timerId
,input
,seconds
context
部分完整代码如下:组件
一共有三个组件
Preview
组件用户展示示例文字, 包括用户输入和示例文字的差异也会用颜色在示例文字上标注UserInput
组件渲染文本框, 供用户输入SpeedInfo
组件展示用户打字的各种数据Preview
text
和input
状态均为两个字符串, 不同的是text
是静态的, 而input
会随着用户的输入而动态变化. 对于text
上的每一个字母, 其索引位置如果有对应的input
的字母, 则进行比较并进行class
的标注, 否则保持不变. 具体代码如下UserInfo
这个组件比较简单, 唯一需要注意的是当用户打字完成后需要将输入框变成
readonly
状态, 判断条件则是之前所说的当input
的长度和text
的长度一样, 具体代码如下:SpeedInfo
该组件需要渲染当前用户打字速度的状态. 比如 WPM, 正确的字母数. 关于这些数据的计算方法不多描述, 均写在了
utils.ts
下组件也只需按需拿状态和方法即可
至此该 Demo 算是完成了
参考
The text was updated successfully, but these errors were encountered: