在深入了解了 State Hook 之后,我们现在将通过从头开始创建博客应用程序来使用它。在本章中,我们将学习如何以良好的扩展方式构建 React 应用程序,如何使用多个 Hook,在哪里存储状态,以及如何使用 Hook 解决常见用例。在本章结束时,我们将拥有一个基本的博客应用程序,我们可以在其中登录、注册和创建文章。
本章将介绍以下主题:
- 以可扩展的方式构建 React 项目
- 从模型中实现静态 React 组件
- 使用 Hook 实现有状态组件
技术要求
应该已经安装了相当新版本的 Node.js(vl1.l2.0 或更高版本)。还需要安装 Node.js 的 npm 包管理器。
本章的代码可以在 GitHub 存储库中找到:https://github.com/PacktPublishing/Learn-React-Hooks/tree/master/Chapter03。
观看以下视频,了解代码的实际应用:
请注意,强烈建议您自己编写代码。不要简单地运行以前提供的代码示例。重要的是你自己编写代码,以便你正确地学习和理解它。但是,如果遇到任何问题,始终可以参考代码示例。
现在,让我们从本章开始。
构建 React 项目
在了解了 React 的原理、如何使用 useState
Hook 以及 Hook 如何在内部工作之后,我们现在将真正使用 State Hook 来开发博客应用程序。首先,我们将创建一个新项目,并以一种允许我们稍后扩展项目的方式构建文件夹。然后,我们将定义我们需要的组件,以涵盖博客应用程序的基本功能。最后,我们将使用 Hook 为我们的应用程序引入状态。在本章中,我们还将学习 JSX 以及 ES6 中引入的新 JavaScript 功能,直到 ES2018。
文件夹结构
项目结构有很多种方式,并且不同结构可以很好地适用于不同的项目。通常,我们会创建一个 src/
文件夹,并按功能将我们的文件分组放在那里。构建项目的另一种流行方法是按路由对项目进行分组。对于某些项目,按代码类型进行额外分离可能是有意义的,例如 src/api/
和 src/components/
。但我们的项目将主要关注用户界面(UI)。因此,我们将将在 src/
文件夹中按功能对文件进行分组。
最好一开始从一个简单的结构开始,只有在实际需要时才使用更深的嵌套。在启动项目时,不要花太多时间考虑文件结构,因为通常情况下,您事先不知道应该如何对文件进行分组。
选择功能
我们首先必须考虑要在博客应用程序中实现哪些功能。至少,我们希望实现以下功能:
- 注册用户
- 登录/注销
- 查看单篇文章
- 创建新文章
- 列出文章
现在我们已经选择了功能,让我们想出一个初始文件夹结构。
提出初始结构
从我们之前的功能中,我们可以抽象出几个功能组:
- 用户(注册、登录/注销)
- 发布(创建、查看、列出)
我们现在可以让它变得非常简单,在 src/
文件夹中创建所有组件,而无需任何嵌套。但是,由于我们已经对博客应用程序所需的功能有了相当清晰的了解,因此我们现在可以提出一个简单的文件夹结构:
src/
src/user/
src/post/
定义文件夹结构后,我们来看看组件结构。
组件结构
React 中组件的思想是让每个组件处理单个任务或 UI 元素。我们应该尝试使组件尽可能细粒度,以便能够重用代码。如果我们发现自己将代码从一个组件复制并粘贴到另一个组件,那么创建一个新组件并在多个其他组件中重用它可能是一个好主意。
通常,在开发软件时,我们从 UI 模型开始。对于我们的博客应用程序,模型如下所示:
我们博客应用程序的初始模型
在拆分组件时,我们使用单一责任原则,该原则指出每个模块都应该对功能的单个封装部分负责。
在这个模型中,我们可以在每个组件和子组件周围绘制框,并给它们命名。请记住,每个组件应该只有一个责任。我们从构成此应用程序的基本组件开始:
从我们的模型定义基本组件
我们为注销功能定义了 Logout
组件、CreatePost
组件(包含创建新文章的表单)以及一个用于显示实际文章的 Post
组件。
现在我们已经定义了基本组件,我们将查看哪些组件在逻辑上属于一起的,从而形成一个组。为此,我们现在需要定义容器组件,以便将组件分组在一起:
从我们的模型中定义容器组件
我们定义了一个 PostList
组件,以便将文章分组在一起,然后是一个 UserBar
组件,以便处理登录/注销和注册。最后,我们定义了一个 App
组件,以便将所有内容组合在一起,并定义应用程序的结构。
现在我们已经完成了 React 项目的构建,我们可以继续实现静态组件。
实现静态组件
在我们开始通过 Hook 向我们的博客应用程序添加状态之前,我们将应用程序的基本功能建模为静态 React 组件。这样做意味着我们必须处理应用程序的静态视图结构。
首先处理静态结构是有意义的,以避免以后必须将动态代码移动到不同的组件。此外,首先处理超文本标记语言(HTML)和 CSS 更容易帮助我们快速开始项目。此后,我们可以继续实现动态代码和处理状态。
逐步执行此操作,而不是一次实现所有内容,可以帮助我们快速开始新项目,而无需一次考虑太多,并让我们避免以后必须重组项目!
设置项目
我们已经学会了如何建立一个新的 React 项目。正如我们所了解的,我们可以使用 create-react-app
工具来轻松初始化一个新项目。我们现在要这样做:
- 首先,我们使用
create-react-app
来初始化我们的项目:
npx create-react-app chapter3_1
- 然后,我们为功能创建文件夹:
- 创建文件夹:
src/user/
- 创建文件夹:
src/post/
现在我们的项目结构已经建立起来,我们可以开始实现组件了。
实现用户
我们将从静态组件方面最简单的功能开始:实现与用户相关的功能。正如我们从模型中看到的,我们将在这里需要四个组件:
Login
组件,我们将在用户尚未登录时显示该组件Register
组件,当用户尚未登录时,我们也将显示该组件Logout
组件,将在用户登录后显示UserBar
组件,它将有条件地显示其他组件
我们将从定义前三个组件开始,它们都是独立的组件。最后,我们将定义 UserBar
组件,因为它取决于要定义的其他组件。
Login 组件
首先,我们定义 Login
组件,其中显示两个字段:用户名字段和密码字段。此外,我们还显示一个登录按钮:
- 我们首先为我们的组件创建一个新文件:
src/user/Login.js
- 在新创建的
src/user/Login.js
文件中,我们导入React
:
import React from 'react'
- 然后,我们定义函数组件。目前,
Login
组件将不接受任何 props:
export default function Login () {
- 最后,我们通过 JSX 返回两个字段和登录按钮。我们还定义了一个
form
容器元素来包装它们。为了避免在提交表单时刷新页面,我们必须定义一个onSubmit
处理程序并在事件对象上调用e.preventDefault()
:
return (
<form onSubmit={e => e.preventDefault()}>
<label htmlFor="login-username">Username:</label>
<input type="text" name="login-username" id="login-username"
/>
<label htmlFor="login-password">Password:</label>
<input type="password" name="login-password" id="login-password" />
<input type="submit" value="Login" />
</form>
)
}
在这里,我们使用匿名函数来定义 onSubmit
处理程序。匿名函数定义如下,如果它们没有任何参数:() => { ... }
,而不是function (){ ... }
。使用参数,我们可以写 (arg1, arg2) => { ... }
,而不是 function (arg1, arg2) { ... }
。如果我们只有一个参数,我们可以省略 ()
括号。此外,如果我们的函数中只有一个语句,我们可以省略 {} 括号,如下所示:e => e.preventDefault()
。
使用语义 HTML 元素(如<form>
和<label>
)让使用辅助功能辅助功能软件(如屏幕阅读器)的用户更易于导航应用程序。此外,使用语义 HTML 时,键盘快捷键(例如通过按回车键提交表单)会自动工作。
我们已实现登录管理器组件,现在可以进行测试了。
测试我们的组件
现在我们已经定义了第一个组件,让我们渲染它并查看它的外观:
- 首先,我们编辑
src/App.js
,并删除其所有内容。 - 然后,我们首先导入
React
和Login
组件:
import React from 'react'
import Login from './user/Login'
最好将导入分组到属于一起的代码块中。在这种情况下,我们将外部导入(如 React)与本地导入(如我们的 Login
组件)分开,方法是在两者之间添加一个空行。这样做可以使我们的代码具有可读性,尤其是当我们稍后添加更多导入语句时。
- 最后,我们定义
App
组件,并返回Login
组件:
export default function App () {
return <Login />
}
如果我们只返回单个组件,我们可以省略 return 语句中的括号。与其写 return (<Login />
),我们可以简单地写 return<Login />
。
- 打开
http://localhost:3000
,您应该会看到正在渲染的Login
组件。如果您已经在浏览器中打开了该页面,则当您更改代码时,它应该会自动刷新:
我们博客应用程序的第一个组件:通过用户名和密码登录
正如我们所看到的,静态 Login
组件在 React 中渲染良好。我们现在来看看 Logout
组件。
Logout 组件
接下来,我们定义Logout
组件,该组件将显示当前登录的用户,以及一个用于注销的按钮:
- 创建一个新文件:
src/user/Logout.js
- 导入
React
,如下所示:
import React from 'react'
- 这一次,我们的函数将使用一个
user
prop来显示当前登录的用户:
export default function Logout ({ user }) {
在这里,我们使用解构来从 props 对象中提取用户密钥。React 将单个对象中的所有组件 props 作为第一个参数传递给函数。对第一个参数使用解构类似于在类组件中使用解构。const { user } = this.props
- 最后,我们返回一个显示当前登录
user
的文本和注销按钮:
return (
<form onSubmit={e => e.preventDefault()}> Logged in as: <b>{user}</b>
<input type="submit" value="Logout" />
</form>
)
}
- 我们现在可以在
src/App.js
中将Login
组件替换为Logout
组件,以便查看我们新定义的组件(不要忘记将user
prop 传递给它):
import React from 'react'
import Logout from './user/Logout'
export default function App() {
return <Logout user = "Daniel Bugl" />
}
现在,我们定义好了 Logout
组件,再来看看 Register
组件。
Register
组件
静态 Register
组件将与 Login
组件非常相似,但有一个附加字段来重复密码。如果它们非常相似,您可能会想到将它们合并到一个组件中,并添加一个 prop 来切换重复密码字段。但是,最好坚持单一责任原则,并让每个组件只处理一个功能。稍后,我们将使用动态代码扩展静态组件,然后注册和登录将具有截然不同的代码。因此,我们稍后需要再次拆分它们。
尽管如此,让我们开始处理 Register
组件的代码:
- 我们首先创建一个新的
src/user/Register.js
文件,并从Login
组件复制代码,因为静态组件非常相似。确保将组件的名称更改为Register
:
import React from 'react'
export default function Register() {
return (
<form onSubmit={e => e.preventDefault()}>
<label htmlFor="register-username">Username:</label>
<input type="text" name="register-username" id="register-username"
/>
<label htmlFor="register-password">Password:</label>
<input
type="password"
name="register-password"
id="register-password"
/>
- 接下来,我们在密码字段代码正下方添加重复密码字段:
<label htmlFor="register-password-repeat">Repeat password:</label>
<input
type="password"
name="register-password-repeat"
id="register-password-repeat"
/>
- 最后,我们还将提交按钮的值更改为注册:
<input type="submit" value="Register" />
</form>
)
}
- 同样,我们可以编辑
src/App.js
以显示我们的组件,其方式类似于我们对Login
组件所做的那样:
import React from 'react'
import Register from './user/Register'
export default function App() {
return <Register />
}
如我们所见,我们的 Register
组件看起来与 Login
组件非常相似。
UserBar
组件
现在是时候将我们的用户相关组件组合成一个 UserBar
组件了。在这里,我们将有条件地显示登录和 Register
组件或Logout
组件,具体取决于用户是否已登录。
让我们开始实现 UserBar
组件:
- 首先,我们新建
src/user/UserBar.js
文件,并导入React
以及我们定义的三个组件:
import React from 'react'
import Login from './Login'
import Logout from './Logout'
import Register from './Register'
- 接下来,我们定义函数组件和
user
值。现在,我们只将其保存在静态变量中:
export default function UserBar () {
const user = ''
- 然后,我们检查用户是否已登录。如果用户已登录,则显示
Logout
组件,并将user
值传递给它:
if (user) {
return <Logout user={user} />
- 否则,我们显示
Login
和Register
组件。在这里,我们可以使用React.Fragment
代替<div>
容器元素。这使我们的 UI 树保持干净,因为组件将简单地并排渲染,而不是包装在另一个元素中:
} else {
return (
<React.Fragment>
<Login />
<Register />
</React.Fragment>
)
}
}
- 同样,我们编辑
src/App.js
,现在我们显示我们的UserBar
组件:
import React from 'react'
import UserBar from './user/UserBar'
export default function App() {
return <UserBar />
}
- 正如我们所看到的,它工作了!现在同时显示
Login
和Register
组件:
我们的用户栏组件,显示登录和注册组件
- 接下来,我们可以编辑
src/user/UserBar.js
文件,并将user
值设置为一个字符串:
const user = 'Daniel Bugl'
- 完成此操作后,我们的应用程序现在显示
Logout
组件:
我们的应用程序在定义用户值后显示注销组件
在本章的后面,我们将向应用程序添加 Hook,以便我们可以登录并动态更改状态,而无需编辑代码!
示例代码
用户相关组件的示例代码可以在 Chapter03/chapter3_1 文件夹中找到。
只需运行 npm install
即可安装所有依赖项,运行 npm start
启动应用程序,然后在您的浏览器中访问 http://localhost:3000
(如果它没有自动打开)。
实现文章
在实现所有用户相关组件后,我们继续在我们的博客应用程序中实现文章。我们将定义以下组件:
- 用于显示单篇文章的
Post
组件 - 用于创建新文章的
CreatePost
组件 - 用于显示多篇文章的
PostList
组件
现在让我们开始实现与文章相关的组件。
Post 组件
我们已经考虑过在创建模型时文章具有哪些元素。文章应具有标题、内容和作者(撰写文章的用户)。
现在让我们实现 Post
组件:
- 首先,我们创建一个新文件:
src/post/Post.js
- 然后,我们导入
React
,并定义函数组件接受三个 props:title
、content
和author
:
import React from 'react'
export default function Post ({ title, content, author }) {
- 接下来,我们以类似于模型的方式渲染所有 props:
return (
<div>
<h3>{title}</h3>
<div>{content}</div>
<i>Written by <b>{author}</b></i>
</div>
)
}
- 与往常一样,我们可以通过编辑
src/App.js
文件来测试我们的组件:
import React from 'react'
import Post from './post/Post'
export default function App() {
return <Post title="React Hook"
content="The greatest thing since sliced bread!"
author="Daniel Bugl" />
}
现在,静态 Post
组件已经实现,我们可以继续创建 CreatePost
组件。
CreatePost 组件
接下来,我们实现一个表单以允许创建新文章。在这里,我们将 user
值作为 props 传递给组件,因为作者应始终是当前登录的用户。然后,我们显示作者,并为标题提供一个 input
字段,为博客文章的内容提供一个 <textarea>
元素。
现在让我们来实现 CreatePost
组件:
- 创建一个新文件:
src/post/CreatePost.js
- 定义以下组件:
import React from 'react'
export default function CreatePost({ user }) {
return (
<form onSubmit={e => e.preventDefault()}>
<div>Author: <b>{user}</b></div>
<div>
<label htmlFor="create-title">Title:</label>
<input type="text" name="create-title" id="create-title"
/>
</div>
<textarea />
<input type="submit" value="Create" />
</form>
)
}
- 与往常一样,我们可以通过编辑
src/App.js
文件来测试我们的组件:
import React from 'react'
import CreatePost from './post/CreatePost'
export default function App() {
return <CreatePost />
}
正如我们所看到的,CreatePost
组件渲染良好。现在我们来看看 PostList
组件。
PostList 组件
在我们实现其他与文章相关的组件后,现在可以实现博客应用程序中最重要的部分:博客文章的提要。目前,提要只是显示博客文章列表。
现在让我们开始实现 PostList
组件:
- 我们首先导入
React
和Post
组件:
import React from 'react'
import Post from './Post'
- 然后,我们定义我们的
PostList
函数组件,接受一个posts
数组作为 prop。如果未定义posts
,则默认情况下将其设置为空数组:
export default function PostList ({ posts = [] }) {
- 接下来,我们使用
map
函数和扩展运算符来显示所有文章:
return (
<div>
{posts.map((p, i) => <Post {...p} key={'post-' + i} />)}
</div>
)
}
如果我们渲染一个元素列表,我们必须给每个元素一个唯一的key
prop。当数据发生变化时,React 使用这个key
prop 高效地计算两个列表的差值。
在这里,我们使用 map
函数,它将函数应用于数组的所有元素。这类似于使用 for
循环并存储所有结果,但它更但它更简洁,声明式,更易于阅读。或者我们可以执行以下操作,而不是使用 map
函数:
let renderedPosts = []
let i = 0
for (let p of posts) {
renderedPosts.push(<Post {...p} key={'post-' + i} />)
i++
}
return (
<div>
{renderedPosts}
</div>
)
然后,我们为每篇文章返回 <Post>
组件,并将 Post 对象 p
中的所有键作为 props 传递给该组件。我们使用扩展语法来做到这一点,这与手动将对象中的所有键列为 props 的效果相同,如下所示:<Post title={p.title} content={p.content} author={p.author} />
- 在模型中,每篇博客文章之后都有一条水平线。我们可以在没有额外
<div>
容器元素的情况下通过使用React.Fragment
实现这一点:
{posts.map((p, i) => (
<React.Fragment key={'post-' + i} >
<Post {...p} />
<hr />
</React.Fragment>
))}
key
prop 始终必须添加到map
函数中渲染的的最上面的父元素。在这种情况下,我们必须将key
prop 从Post
组件移到React.Fragment
组件。
- 再次,我们通过编辑
src/App.js
文件来测试我们的组件:
import React from 'react'
import PostList from './post/PostList'
const posts = [
{ title: 'React Hook', content: 'The greatest thing since sliced bread!', author: 'Daniel Bugl' },
{ title: 'Using React Fragments', content: 'Keeping the DOM tree clean!', author: 'Daniel Bugl' }
]
export default function App() {
return <PostList posts={posts} />
}
现在,我们可以看到应用程序列出了在 posts
数组中定义的所有文章:
使用文章列表组件显示多篇文章
如我们所见,通过 PostList
组件列出多篇文章可以正常工作。我们现在来看看怎样将应用程序组合在一起。
组合应用程序
实现所有组件后,为了重现模型,我们现在只需要将所有内容放在 App
组件中即可。然后,我们将成功复制模型!
让我们开始修改 App
组件,并将我们的应用程序组合在一起:
- 编辑
src/App.js
,并删除所有当前代码。 - 首先,我们导入
React
、PostList
、CreatePost
和UserBar
组件:
import React from 'react'
import PostList from './post/PostList'
import CreatePost from './post/CreatePost'
import UserBar from './user/UserBar'
- 然后,我们为应用程序定义一些模拟数据:
const user = 'Daniel Bugl'
const posts = [
{ title: 'React Hook', content: 'The greatest thing since sliced bread!', author: 'Daniel Bugl' },
{ title: 'Using React Fragments', content: 'Keeping the DOM tree clean!', author: 'Daniel Bugl' }
]
- 接下来,我们定义
App
组件,并返回一个<div>
容器元素,我们给它设置了一些填充:
export default function App() {
return (
<div style={{ padding: 8 }}>
- 现在,我们插入
UserBar
和CreatePost
组件,将user
prop 传递给CreatePost
组件:
<UserBar />
<br />
<CreatePost user={user} />
<br />
<hr />
请注意,您应该始终更喜欢通过 CSS 设置间距,而不是使用 <br/>
HTML 标记。但目前我们专注于 UI,而不是其样式,因此我们尽可能简单使用 HTML。
- 最后,我们显示
PostList
组件,列出所有posts
:
<PostList posts={posts} />
</div>
)
}
- 保存文件后,
http://localhost:3000
应该会自动刷新,我们现在可以看到完整的 UI:
根据模型完全实现我们的静态博客应用程序
如我们所见,我们之前定义的所有静态组件都在一个 App
组件中一起渲染。我们的应用程序现在看起来就像模型一样。接下来,我们可以继续使所有组件动态化。
示例代码
我们的博客应用程序的静态实现的示例代码可以在 Chapter03/chapter3_2 文件夹中找到。
只需运行 npm install
即可安装所有依赖项,运行 npm start
启动应用程序,然后在您的浏览器中访问 http://localhost:3000
(如果它没有自动打开)
(节选)
评论 (0)