任务 2:组件化与数据流管理

Hey,你好,我是 JimmyLv 吕靖,这是你开始 前端 TDD 练习的第 16 天。今天你会进入前两个页面的业务需求 TDD 开发当中去,期望你能够从 API 接口设计出发,由外到内得把整个 TDD 流程串通起来。

React 的哲学

学习 React 的基础,简单说来用这个公式就可以表示:UI = f(data)
UI = f(data)
要真正理解 React,最关键的就是以下两点:
  1. 1.
    React 界面完全由数据驱动;
  2. 2.
    React 中一切皆可为函数(即 function);
如果要渲染界面,不要直接去操纵 DOM 元素,而是修改数据,由数据去驱动 React 来修改界面。可以理解为用户界面 UI 就是由一个个函数/组件组成的,然后呢,借助于 React Hook,甚至连不是 UI 的功能都是组件,比如获取数据这样的副作用,都可以包装在一个函数里面,因此代码结构会更加容易理解和维护。
React Hooks 的意思就是,组件尽量写成纯函数,如果需要外部功能和副作用,就用钩子把外部代码"钩"进来。
import React, { useState } from 'react'
function Example() {
// 声明一个新的叫做 “count” 的 state 变量
const [count, setCount] = useState(0)
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
)
}

练习目标

  • 使用 Cypress 驱动出组件 UI 及页面交互行为
  • 利用 Mock Server 为 Cypress 提供 API 数据(via 上一节)
  • 拆分组件树后,使用 React Hook 进行数据请求和状态管理
  • 至少完成用户故事 1(书架首页) 和 用户故事 2(书籍详情)

本次任务的练习要求

  • 注意,完全不要使用 React Class 组件
  • 只允许使用 React 函数式组件,配合 React 自定义 Hook 足以
  • 使用 React Router 官方 Hook(useParamsuseHisotry 等)管理路由
比如:
import { useHistory } from 'react-router-dom'
function HomeButton() {
let history = useHistory()
function handleClick() {
history.push('/home')
}
return (
<button type="button" onClick={handleClick}>
Go home
</button>
)
}

也请你思考一下

当你使用 React 组件时,你更关心数据,还是更关心页面 DOM 操作?相比于 jQuery 的优势在哪里?
声明式编程(Declarative Programming) over 命令式编程(Imperative Programming)。

一切皆函数意味着什么呢?

在 JavaScript 的世界里,一切皆是对象,甚至连一个函数都是一个对象。
比如,你是否知道,你可以访问一个函数的 length 来获知这个函数声明的参数?
function foo(a, b) {
return a + b
}
console.log(foo.length) // 输出为2
因为函数本身就是对象,所以,在 JavaScript 中,函数拥有一等公民的地位。而在 React 中一切皆为组件,函数就是一等公民。
有一条简单的原则,我相信你也会受用,Unix 哲学告诉你:做一件事,做好它。
KISS(keep it simple, stupid)

纯函数与模块易测性的关系?

纯净的输入输出,所带来的是简易的测试准备。比如购物车“计算总价格”这样的一个功能,其实测试本身根本不用关注其内部实现:你可以用 reduce 实现,也可以自己写 for 循环实现。只要测试输入没有变,输出就不应该变。这个特性,是测试支撑重构的基础。因为重构指的是,在不改变软件外部可观测行为的基础上,调整软件内部的实现。
而对于 UI 组件来说,其 props 不变,相应的 UI 界面输出就不应该变,从而对于 UI 组件的静态输出(即 virtual DOM -> DOM)来说,你只需要关心它在不同 props 的输入条件下,所展现出来的不同结构及样式,即前文所提到的 UI stories 穷举,利用 Storybook 进行组件驱动开发,会获得更容易验证,以及更快的开发反馈等好处。
export default { title: 'Counter' }
export const enabled = () => <Counter text="Enabled" />
export const disabled = () => <Counter disabled text="Disabled" />
当然,除了组件 props 以外,还有一类隐含的输入,即 User Interaction 用户交互,当页面上进行点击,组件会监听其事件进行响应,此时你就更关心组件的行为。利用 Testing Library 你可以测试 User Event,根据不同的操作行为,组件会有不同的响应结果,即输出。有时候是简单的一个界面变化,比如 Toggle,有时则会触发新的数据请求,或是输入数据进行提交,当然也会有可能引发页面跳转等交互行为。此时,你只需要根据 Testing Library 再次检测其输出结果即可。
import { render } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
test('should toggle dark mode', () => {
const handleToggle = jest.fn()
const comp = render(<Toggle yes="白天" no="黑夜" onToggle={handleToggle} />)
userEvent.clcik(comp.getByText('白天'))
expect(comp.getByText('黑夜')).toBeInTheDocument()
expect(handleToggle).toBeCalledWith(false)
})