Joshua Chen Personal Blog

Back

📝 Note ✅ Ready · react typescript

打字游戏网站记录

从0跟敲React+Ts的记录

views | comments

[问题] 1.动画不流畅,有问题,需要优化 2.组件可以进一步抽离 3.hook的特性和使用

1.vite构建框架#

使用vite创建框架,选择React+Typescript

开始 | Vite 官方中文文档

npm create vite@latest 使用npm创建Vite项目

2.零散的知识点细节#

#HTML-标签 是网页的基础语法,用来告诉浏览器“这部分内容是什么”

<div>也是一种HTML标签,可以理解为一个普通的盒子/容器, 例如 <div>这是一个盒子</div> 浏览器会把它当成一个块级区域,可以往里面放:

  • 文字
  • 图片
  • 按钮
  • 其他标签 其中有一些HTML标签是原生的,由HTML语言定好的规范,也有可以自己定义的组件,但最终还会渲染成原生HTML标签

#属性 <div className="text-slate-500 text-4xl">Hello</div> 这里className是React特有的写法,普通html里面是class

eg: .html 文件:class .jsx / .tsx 里的 React 标签:className

小写开头:原生 DOM 标签,比如 div、p、button、input 大写开头:React 组件,比如 App、GenerateWords、UserCard

#DOM 真实DOM标签是指浏览器可以解析的标签,任何自定义的组件最终都会被拆为真实DOM组件然后再给浏览器解析

#自闭合标签 本身就自带元素或者内容的东西,不需要额外再在中间添加内容来输出,可以自己结束自己 普通的成对标签长这样: <div>Hello</div> 开始标签<div> 结束标签</div> 中间内容Hello

自闭合标签: <GenerateWords words = {words} /> //这里{words}是JSX 表达式语法,意思是:不要把 words 当普通文字显示,而是把 JavaScript 变量 words 的值塞进页面里

#组件 这里GenerateWords是一个包含有JSX内容的组件,words是组件内的变量名 提前用

const words = faker.word.words(10)
tsx

对他进行了定义,然后

const GenerateWords =({words}:{words:string})=>{
	return  <div className="text-slate-500 text-4xl">{words}</div>

}
tsx

这里定义GenerateWords,然后等号右边说明了要传入什么变量,这个变量是什么属性,这里是Ts语法

这样在App()里调用GenerateWords的时候,就会从环境里取出words的变量,传给这个JSX组件,然后JSX组件知道他收到的是数据类型为string的words变量,要传给return里面的{words}

拓展: 如果这里有多个变量,就写多个props,像HTML属性一样一个个写上去 例如

const words = faker.word.words(10)
const title = "Typing Speed"
const count = 10

<GenerateWords words={words} title={title} count={count} />
tsx

子组件接受的时候

const GenerateWords = ({
  words,
  title,
  count,
}: {
  words: string
  title: string
  count: number
})
tsx

如果props过多的时候,也可以整理类型

type GenerateWordsProps = {
  words: string
  title: string
  count: number
}
typescript

然后子组件这样写 const GenerateWords = ({ words, title, count }: GenerateWordsProps)

3.组件的定义和导入#

#导出和导入 一般写好的组件会放在src/components目录下 方便管理和阅读

const Button = () =>{
 // 你写的函数
}
export default Button
tsx

其中export default表示你默认导出的是Button这个变量保存的组件函数

在使用默认导入的时候,名字可以自己取 例如 import Button from './components/RestartButton' 可以改为 import Duck from './components/RestartButton' 然后使用\<Duck />

#interface 用来定义props的形状

interface RestartButtonProps {
  onRestart: () => void  //函数
  className?: string     //字符串,可选
}
typescript

他不是运行时检查,而是在编译阶段检查 对于<RestartButton /> ,如果没传onRestart,编译器和Ts会报错,而不是因为浏览器运行到这里才检查出来 ‘type’功能与interface相似

告诉 TypeScript:RestartButton 这个组件应该接收哪些 props,以及这些 props 分别是什么类型。它帮你在写代码时提前发现传参错误。

const buttonRef = useRef<HTMLButtonElement>(null) 其中HTMLButtonElement是一个类型参数,告诉ts,我的buttonRef.current以后要么是null,要么是一个HTML button元素

#钩子 也叫Hook

useRef是一个钩子函数

const buttonRef = useRef<HTMLButtonElement>(null)
const handleClick = () => {
  buttonRef.current?.blur()
}
tsx

useRef<HTMLButtonElement>(null) 会创建一个ref对象,这个对象大概为

{ current: null }
tsx

这里current是一个属性,绕了我好久才弄明白 属性意思是当前保存的值,是ref固定返回的对象

容器里元素类型为HTMLButtonElement,当这个容器有真实DOM元素的时候,React 在 <button ref={buttonRef}> 渲染完成后,把真实 DOM 节点赋值给 buttonRef.current

#组件组合

const RestartButton = (...)=> {
return (
	\<button ...>
		\<MdRefresh className="w-6 h-6" />
	\</button>
)
}
tsx

这里React会执行RestartButton组件函数,然后得到return的内容,<button>和/ 也就是在RestartButton里面组合了button,而button里面又组合了MdRefresh

App
└── RestartButton
    └── button
        └── MdRefresh
plaintext

当调用父组件的时候,内部return所有的子组件都会一起渲染出来 可以通过在组件 return 里面嵌套其他组件,来组合出更复杂的组件。 调用 RestartButton 时,React 会渲染 RestartButton 内部 return 的全部内容,包括它里面嵌套的 MdRefresh 图标组件。

4.游戏功能的实现 (1)#

输入UI#

第一个小坑

const Character = ({ char }:{char:string}) => {
    return <span className ="text-primary-500">{char}</span>
}
tsx

这里return <…> {char}之间插入字符的话,会在每个字之间都插入 原因:这里的 span 是用来包住单个字符的行内标签 含义是:每传进来一个字符 char,就渲染成一个黄色字符userInput = “abc”,在 UserTypings 里会被拆成: ["a", "b", "c"]

遍历逻辑是怎么实现的?#

typedCharacters.map((char, index) => {
  return <Character char={char} key={index} />
})
tsx

这里的含义是: typedCharacters 是字符数组,map 遍历这个数组。每遍历到一个元素,就把当前字符作为 char,把当前下标作为 index,然后返回一个 Character 组件。所有返回的 Character 会组成一个新的 React 元素数组。

为什么Caret没有被渲染在每个字符后面? 因为这里的逻辑是: 所有字符先通过 map 渲染出来,然后最后额外追加一个 Caret

return <Character char={char} key={index} /> 返回的是类似这样的 React 元素: <Character char="a" key={0} />

然后map把这些返回值形成数组:

[
  <Character char="a" key={0} />,
  <Character char="b" key={1} />,
  <Character char="c" key={2} />
]
tsx

这个数组最后会被React展开,变成页面上多个Character组件实例,然后这些组件再渲染成多个 span 例如 <span className="text-primary-500">a</span>

const Charactars({char}:{char:string}) =>{
	return
	<span className:"text-primary-400"><{char}</span>
}
tsx

这里的Character 是一个自定义组件。它接收 char 这个 prop,然后返回一个 span

map的核心含义是,遍历数组,但是他不知道遍历之后要干嘛,所以要传给他一个函数/组件 Charactars,告诉他遍历之后要对遍历得到的元素做什么

5.游戏功能的实现 (2)#

#钩子 #hook

单词数量组件#

设计这个组件有俩个思想 第一个思想:设计成组件达到可复用,方便后续维护 让单词数量可以通过count控制,不写死在hook里面

第二个思想:封装,保护setWords核心业务逻辑,防止外界修改setWords从而破坏功能完整性,

第一步: 使用useState,绑定words和setWords,提供一个可以修改words的接口供内部使用,然后用UpdateWords包装 这样,无论useWords 内部管理 words 怎么生成、怎么更新,外部组件不直接碰 setWords,外部组件只拿到 words 和 updateWords,防止外界对setWords修改

useWords 把“随机单词状态”的细节封装起来,只向外暴露安全、明确的接口:words 用来读,updateWords() 用来刷新

这个逻辑中,外界能做的事情只有:

读取 words
调用 updateWords 刷新 words
不知道setWords存在,也不关心怎么生成随机词
plaintext

对于内部而言:

  setWords(faker.word.words(count))
  控制 words 必须来自 faker 生成
plaintext

这俩个参数的含义分别是:

useState 创建 words/setWords 这一对

words 保存当前单词
setWords 负责更新 words

updateWords 调用 setWords

words 变化

页面重新渲染
plaintext

useState 让 words 成为状态,也让 setWords 成为更新这个状态的方法;后面的 updateWords 只是封装了一次对 setWords 的调用

const updateWords = useCallback(() => {

    setWords(faker.word.words(count));

  }, [count]);
tsx

这里语法是: useCallback(要缓存的函数, 依赖数组) 也就是说,当 count 发生变化的时候,就 创建一个新的 updateWords 函数,被替换的是整个updateWords函数

这里setWords是用来更新words状态的函数,当updateWords被调用,就会执行setWords(faker.word.words(count))

也就是说,这里有俩个功能,一个是count变化的时候,重新生成整个函数,另一个是当updateWords被调用的时候,执行里面的setWords刷新words

因为useCallback 管函数本身updateWords 管实际更新 words

普通函数:
组件每次重新渲染,函数都会重新创建一次

useCallback:
组件重新渲染时,先看依赖数组
依赖没变:不给你新函数,继续用旧函数
依赖变了:给你新函数
plaintext

count 变化的时候,重新创建 updateWords,让它内部拿到最新的 count,之后调用 updateWords() 时就能按新的数量生成 words。

问题1:React本身不是可以实时渲染words{}里的变量吗,为什么还要多次一举来增加一个刷新机制

答:这里是把“刷新单词”这件事单独封装成一个函数 也就是说,这里是把点击重启按钮刷新单词的函数单独拆出来,方便复用 即: 按钮组件:只负责点击 useWords:只负责生成和更新单词 App:负责把它们连接起来

原因: React 会实时渲染变化后的 words,但 React 不会自己决定什么时候生成新的 words。 React只负责 : words 变了 -> 页面重新渲染

黑暗模式切换组件#

const [isDark, setIsDark] = useState(
  () => window.matchMedia && window.matchMedia(matchDark).matches
)
tsx

创建一个状态isDark,它的初始值是:当前系统是不是深色模式,同时创建一个修改它的函数setIsDark,以后可以通过setIsDark(true)或者(false)修改isDark

() => window.matchMedia && window.matchMedia(matchDark).matches 懒初始化函数 先看window.matchedia是否存在,如果不存在,返回window,matchMedia本身,通常是undefined,如果存在,则执行window.matchMedia(matchDark).matches

这里返回window.matchMedia本身没有业务意义,只是为了防止报错 如果 matchMedia 存在,就检测深色模式; 如果不存在,就别报错。

#useState
useState 里的初始值,只在第一次渲染时生效。 后续渲染,状态来自 React 上一次保存的值。 例如:

const [count, setCount] = useState(() => {
  console.log("初始化 count")
  return 0
})
tsx

第一次组件显示的时候,会打印初始化count 之后点setCount(count+1) 组件会重新渲染,但是不会再打印初始化count

tips:  JavaScript 有一个机制叫 自动分号插入 英文是 ASI  即,return右边如果不接东西,会被理解为return;,无法正确读取jsx结构  需要马上接括号或者与jsx第一行紧贴

#对象解构

const { isDark, setIsDark } = useDarkMode() 这里意思是调用useDarkMode(),然后从useDarkMode返回的对象里拿出两个属性,分别是isDark和setIsDark

等价于:

const result = useDarkMode()
const isDark = result.isDark
const setIsDark = result.setIsDark
tsx

#props 我将这个props和这个对象经常搞混 props一般是组件参数,例如

const UserCard = ({name,age}) =>{
	return <div>{name} - {age}</div>
}
tsx

这里的{name,age}才是在接受props 当外部使用的时候,这个组件被调用的形式可能是 <UserCard name="Joshua" age={18} />

核心功能:用户输入组件#

依赖数组#

useEffect(() => {
  console.log("执行了")
}, [])
tsx

这里的[]就是依赖数组,当这里留空的时候,说明不依赖任何外部变量,只在组件第一次挂载的时候执行一次

如果这里填timeLeft,则第一次会执行,然后每当timeLeft变化,就重新执行

Hook深入理解#

作为React的核心功能,第一次理解仍然比较抽象

🗂️ This is a 📝 note in the knowledge base.

Content may be incomplete or work-in-progress.

← Back

Comment seems to stuck. Try to refresh?✨