在第 2 章中,你编写了第一个 React 组件。在本章中,您将更深入地研究 React,学习如何通过组合和重用组件(不仅是您自己的组件,还包括从第三方库导入的组件)来创建健壮的应用程序。
用户界面组件
为浏览器创建出色的用户界面并非易事。使用 CSS 为 HTML 元素设置样式需要大量的时间和耐心,而大多数人没有。
如您所料,存在大量提供漂亮用户界面原语的库和框架。在我写这篇文章的时候,React 有三个领先的用户界面库:
我对它们进行了评估并决定在本书中使用 React-Bootstrap,因为它是三者中最容易学习和使用的。
React-Bootstrap 是一个为 Bootstrap 提供 React 组件包装器的库,Bootstrap 是一个非常流行的浏览器 CSS 框架。您已经在第 2 章的src/index.js中导入了 Bootstrap 的 CSS 文件,因此它的一些默认样式已经在使用中。现在是时候通过 React-Bootstrap 提供的组件开始积极使用 Bootstrap 元素了。
React-Bootstrap 库提供网格grids和堆栈grids作为构建块来帮助您创建网站的布局。网格使用Container
组件(以及可选的Row
and Col
)来组织子组件。堆栈使用Stack
组件在页面上分配的空间内垂直或水平呈现其子组件。您将在本章后面使用Container
和组件。Stack
这两个原语可能看起来太简单而无法创建复杂的布局,但它们的强大之处在于它们能够递归嵌入,正如您很快就会看到的那样。
容器组件
返回到您在浏览器中运行的微博应用程序。您可能已经注意到,<h1>
标题的文字和两个假博客文章都粘在窗口的左边框上,没有任何边距,这看起来不太好。该Container
组件是 React-Bootstrap 网格系统的主要部分,它通过在其所有子组件周围添加一个小边距来解决这个问题。下一个任务是向应用程序添加一个顶级容器。
该组件的函数返回一个<h1>
元素和一个<p>
段落列表,其中包含两个组成的博客文章的内容。所有这些元素都被分组到一个带有<>
and</>
标签的片段中,因为 React 要求组件返回一个带有单个根节点的 JSX 树。现在可以用一个<Container>
组件替换片段标签,该组件将是树的根节点。
在编辑器中打开src/App.js,为组件添加导入语句Container
,然后用它替换片段标签:
src/App.js:将容器包装器添加到应用程序
import Container from 'react-bootstrap/Container'; export default function App() { const posts = [ ... // <-- no changes to fake blog posts ]; return ( <Container fluid className="App"> ... // <-- no changes to JSX content </Container> ); }
React-Bootstrap 中的组件可以单独导入,格式为import X from 'react-bootstrap/X'
. 单独导入您需要的每个组件可能感觉很乏味,但不鼓励在一次导入中导入整个库,因为这会大大增加应用程序的大小。
Container
组件有一个fluid
属性。流体容器会自动更改其宽度以填充浏览器窗口。如果没有流体选项,容器的宽度会“捕捉”到与标准设备屏幕尺寸相关的几个预定义宽度之一。
该className
属性相当于class
纯 HTML。必须更改名称以避免与class
JavaScript 中的关键字冲突。您可以使用className
为任何原始 HTML 元素提供 CSS 类,但许多组件也实现此属性并将其分配给它们呈现的顶级元素。给组件一个类名有助于以后使用 CSS 自定义其外观。作为保持 CSS 样式组织良好的命名约定,组件的名称用作 CSS 类。
保存更改并注意Container
组件如何为页面添加漂亮的边距。还可以尝试调整浏览器窗口的大小,以查看容器如何调整大小。您可能会注意到,随着窗口变大,字体大小会略有增加。Bootstrap 会针对不同的屏幕尺寸对页面元素进行不同的样式设置,这种技术有助于使网站在手机、平板电脑、笔记本电脑和台式电脑上看起来最好。图 3.1显示了流体容器的外观。
您可以做的一个有趣的实验是在浏览器的调试控制台中查看页面的结构。请注意Container
组件如何将自身呈现为<div>
具有类名的元素App
(除了其他特定于 Bootstrap 的类)。
有关该Container
组件的更多示例和信息,请参阅其文档。如果流体容器和非流体容器之间的区别还不是很清楚,请不要担心。在下一节中,您将在布局中添加非流体容器,然后差异会更加明显。
添加标题组件
带有应用程序名称的<h1>
元素是应用程序标题的第一次尝试,但当然这个应用程序需要更精致的东西,比如最终可以容纳菜单选项的导航栏。
为了保持应用程序的源代码井井有条,最好在单独的模块中为此标头创建自定义组件。
Create React App 创建的项目不提供有关如何构建源代码的任何指导。我发现将应用程序的所有自定义组件放在专用于它们的子目录中很有用。从您的终端创建此目录:
mkdir src/components
React-Bootstrap 带有Navbar
一个非常适合微博标题的组件。它的文档有许多示例,可以很容易地找到适合应用程序需求的设计。
您可以在下面看到Header
使用React-BootstrapNavbar
的组件。Container
将此代码复制到src/components/Header.js中。
src/components/Header.js:标题组件
import Navbar from 'react-bootstrap/Navbar'; import Container from 'react-bootstrap/Container'; export default function Header() { return ( <Navbar bg="light" sticky="top" className="Header"> <Container> <Navbar.Brand>Microblog</Navbar.Brand> </Container> </Navbar> ); }
该组件首先导入所需的任何其他组件,在本例中为Navbar
和Container
. 组件函数用 声明export default function
,因为该组件将从App.js导入,以便可以包含在页面中。
该组件的函数有一个return
返回组件的语句Navbar
,其中的选项是从文档中的一个示例中复制而来的。我决定使用浅色背景,并且我还认为让这个栏“粘性”是个好主意,这意味着当需要滚动时,栏将始终保持在页面顶部可见. 我给这个组件一个类名,这样我就可以为它添加自定义样式。
Navbar
如文档示例中所见,Container
组件被定义为子组件。具有应用程序名称的Navbar.Brand
组件是容器内目前唯一的东西。React-Bootstrap 经常对相关的子组件进行分组,并使其成为父组件的属性,因此您会发现许多组件使用点语法的实例,如Navbar.Brand
.
这个头部组件展示了来自 React-Bootstrap 的布局原语如何组合和嵌入到彼此中。当这个组件被添加到页面中时,将会有一个作为根元素的流体容器,作为其子元素的导航栏,以及作为该栏的子元素的第二个非流体容器。对于许多应用程序来说,这是一个很好的模式,因为它使导航栏全宽,但其内容被限制为根据屏幕或窗口大小选择的更窄、更舒适的宽度。页面的内容也将被包裹在一个非流动的容器中,以便它们与标题的内容对齐。
现在Header
组件在项目中,可以在App
组件中导入并使用它来代替<h1>
标题。还添加了博客文章的第二个内部Container
。
src/App.js : 添加头部组件
import Container from 'react-bootstrap/Container'; import Header from './components/Header'; export default function App() { const posts = [ ... // <-- no changes to fake blog posts ]; return ( <Container fluid className="App"> <Header /> <Container> {posts.length === 0 ? ... } </Container> </Container> ); }
该Header
组件是从其源文件中导入的,该源文件以相对路径的形式给出。然后使用这个组件代替<h1>
元素。由于组件具有将自身呈现到页面所需的所有信息,因此无需传递任何参数。
如上所述,现在有一个Container
组件可以包装呈现虚假博客文章的循环。这样一来,博客文章也使用非流体定位,并与标题的内容对齐。图 3.2显示了这些更改后的页面外观。
如果你仔细看表头,你会发现它并没有一直延伸到窗口的左右边界,实际上每边都有一个小白边。<div>
使用浏览器的检查器查看页面中的样式,我确定由流体呈现的顶层Container
具有非零填充。解决这个小小的外观烦恼的解决方案是覆盖该组件的填充,该组件具有App
类名。
我不太喜欢的标题样式的另一个方面是标题的底部和它下面的内容区域之间没有分隔。那里稍暗的边界线将使分离更清晰可见。
src/index.css文件用于输入应用程序所需的所有自定义样式。在您的编辑器中打开此文件并将其所有内容替换为以下内容:
src/index.css : App 和 Header 组件的自定义样式
.App { padding: 0; } .Header { border-bottom: 1px solid #ddd; }
通过这些次要的样式覆盖,应用程序看起来好多了,如图 3.3 所示。
添加侧边栏
许多 Web 应用程序中的另一个常见用户界面组件是侧边栏。在微博中,侧边栏将提供导航链接,用于在显示所有关注用户的博客文章的“提要”页面和显示所有用户的博客文章的“探索”页面之间切换。
查看 React-Bootstrap 组件列表,用于该组件的同一个NavbarHeader
可以用作侧边栏,并带有一些小的 CSS 自定义。Nav.Link组件可用于导航链接。
这是侧边栏的第一个实现,在实现页面路由之前,占位符链接将无法正常工作。将此代码添加到src/components/Sidebar.js文件中。
src/components/Sidebar.js:侧边栏组件
import Navbar from "react-bootstrap/Navbar"; import Nav from "react-bootstrap/Nav"; export default function Sidebar() { return ( <Navbar sticky="top" className="flex-column Sidebar"> <Nav.Item> <Nav.Link href="/">Feed</Nav.Link> </Nav.Item> <Nav.Item> <Nav.Link href="/explore">Explore</Nav.Link> </Nav.Item> </Navbar> ); }
当用户向下滚动时,该组件的sticky="top"
属性Navbar
将使侧边栏在页面上可见。flex-column类来自 Bootstrap 框架,其目的是改变其子项的方向为垂直。Sidebar
类名供应用程序在设置此组件样式时使用。
现在需要将侧边栏添加到页面内容区域的左侧。当需要并排放置两个或多个组件时,理想的布局工具是水平堆栈。您可以在下面看到App
带有侧边栏的更新组件。
src/App.js : 添加侧边栏
import Container from 'react-bootstrap/Container'; import Stack from 'react-bootstrap/Stack'; import Header from './components/Header'; import Sidebar from './components/Sidebar'; export default function App() { const posts = [ ... // <-- no changes to fake blog posts ]; return ( <Container fluid className="App"> <Header /> <Container> <Stack direction="horizontal"> <Sidebar /> <Container> {posts.length === 0 ? ... // <-- no changes to render loop } </Container> </Stack> </Container> </Container> );
添加该Stack
组件时将direction
属性设置为horizontal
,这是必要的,因为该组件的默认设置是垂直放置组件。该堆栈有两个孩子,侧边栏和一个Container
带有假博客帖子的内部。这两个组件现在将并排显示,如图 3.4 所示。
侧边栏需要一些造型工作才能看起来最好。将以下 CSS 定义添加到src/index.css。
src/index.css : 侧边栏样式
... // <-- no changes to existing styles .Sidebar { width: 120px; margin: 5px; position: sticky; top: 62px; align-self: flex-start; align-items: start; } .Sidebar .nav-item { width: 100%; } .Sidebar a { color: #444; } .Sidebar a:hover { background-color: #eee; } .Sidebar a:visited { color: #444; }
添加到Sidebar
CSS 类的定义是在浏览器的调试控制台中试验的结果。以下是每个规则的简要说明:
width: 120px
将侧边栏的宽度设置为 120 像素margin 5px
在侧边栏周围添加 5 像素的边距position: sticky
将侧边栏附加到浏览器的左侧,以便当用户滚动内容时它会停留在那里top: 62px
设置侧边栏相对于标题的正确垂直位置align-self: flex-start
将侧边栏与堆栈组件的顶部边框对齐align-items: start
将侧边栏的子组件向左对齐
您可能想知道.nav-item
上述 CSS 定义中的类是从哪里来的。这是一个由 Bootstrap 库定义的类,Nav.Link
由 React-Bootstrap 的组件使用。如上所述,打开浏览器的开发人员控制台并查看页面上呈现的元素通常是查找使用哪些类的最简单方法,并且是重新定义某些元素外观的潜在目标。CSS 定义.nav-item
将元素的宽度设置<Nav.Item>
为 100%,这意味着它们将具有侧边栏的最大宽度,而不是其文本的宽度。这样做是为了使改变背景的悬停样式显示一个完整的条形,而不管链接中文本的长度如何。
的 CSS 定义a
,a:hover
并a:visited
配置链接的颜色。
随着样式的更新,侧边栏看起来好多了。如果将鼠标指针悬停在某个项目上,其背景颜色会更改以突出显示它。请参见图 3.5中侧边栏的当前状态。
构建可重用组件
使用 React 构建应用程序时,一个好的策略是始终尝试将应用程序划分为许多组件,每个组件只有一个目的。本章前面部分的Header
和Sidebar
组件是很好的模型。
该App
组件是一个自己做太多工作的组件的示例,因为它目前负责渲染应用程序的总体布局,以及稍后将成为博客提要的虚假表示。
为了让应用程序支持页面导航,重构组件是有意义的,App
以便内容区域由一个子组件呈现,当用户浏览不同的页面时,该子组件可以换出。
以下清单显示了一个名为 的新组件Posts
,其中包含呈现(当前是假的)博客文章的逻辑。将此代码存储在src/components/Posts.js中。
src/components/Posts.js:呈现博客文章列表
export default function Posts() { const posts = [ { id: 1, text: 'Hello, world!', timestamp: 'a minute ago', author: { username: 'susan', }, }, { id: 2, text: 'Second post', timestamp: 'an hour ago', author: { username: 'john', }, }, ]; return ( <> {posts.length === 0 ? <p>There are no blog posts.</p> : posts.map(post => { return ( <p key={post.id}> <b>{post.author.username}</b> — {post.timestamp} <br /> {post.text} </p> ); }) } </> ); }
使用项目中的这个新组件,可以删除中的posts
虚假博客文章数组,并且可以将呈现它的循环替换为. 以下是更新版本:App
<Posts />
App
src/App.js : 使用Posts
组件
import Container from 'react-bootstrap/Container'; import Stack from 'react-bootstrap/Stack'; import Header from './components/Header'; import Sidebar from './components/Sidebar'; import Posts from './components/Posts'; export default function App() { return ( <Container fluid className="App"> <Header /> <Container> <Stack direction="horizontal"> <Sidebar /> <Container> <Posts /> </Container> </Stack> </Container> </Container> ); }
带有属性的组件
该App
组件仍然不是很灵活。您可以设想,一旦支持多个页面,该<Posts />
组件将成为页面内容区域中包含的许多可能选项之一,但在这种结构下,侧边栏将始终出现在内容部分的左侧。问题是对于这个项目,侧边栏只有在用户登录后才有用。
在这个应用程序中,会出现一些需要省略侧边栏的情况,例如在渲染登录页面时。由于目标是App
尽可能简单,这是将逻辑下移到新子组件中的另一个机会。
这个新组件,我将称之为Body
,必须非常通用,以便能够在有或没有侧边栏的情况下呈现主页内容。它是这个应用程序中第一个需要接受输入参数的,在 React 中称为props。
在我向您展示如何编写这个组件之前,先看一下这个组件的几个使用示例。以下是此应用程序的提要页面如何呈现博客文章列表,侧边栏位于左侧:
<Body sidebar={true}> <Posts /> </Body>
不错,对吧?为了指示页面是否需要显示侧边栏,一个sidebar
属性被赋予一个布尔值。页面的内容作为组件的子级给出,在这种情况下只是Posts
组件。对于稍微紧凑的格式,您可以省略 prop 的true
值sidebar
,因为它是默认值:
<Body sidebar> <Posts /> </Body>
使用相同的Body
组件实现登录页面可以如下完成:
<Body sidebar={false}> <h1>Login</h1> <form> ... </form> </Body>
或者以更紧凑的方式,您可以sidebar
完全省略该道具,这将使其默认为虚假值:
<Body> <h1>Login</h1> <form> ... </form> </Body>
这非常强大,因为Body
组件成为了如何格式化页面正文的绝对权威,无论是否有侧边栏。如果您决定以某种方式更改应用程序的布局,例如将侧边栏移到右侧,则整个应用程序中只有一个地方需要进行更改。
组件函数如何访问作为参数传递的道具以及定义为子组件的任何子组件?React 使这非常容易,因为它在调用组件函数时将具有所有这些属性的对象作为参数传递。Body()
组件函数可以声明如下:
export default function Body(props) { // props.sidebar is the value of the sidebar attribute // props.children is the JSX component tree parented by this component }
传递给函数的props
对象包括组件声明中作为道具给出的所有属性的键。如果该组件是用子声明的,那么children
也会包含一个键。
在实践中,您会发现大多数 React 开发人员使用解构赋值(参见第 1 章)来接收道具。下一个示例在功能上等同于上述示例:
export default function Body({ sidebar, children }) { // sidebar is the value of the sidebar attribute // children is the JSX component tree parented by this component }
这种语法的好处是组件的函数声明显式地命名了它的输入参数。
准备好实现您的第一个重要组件了吗?这是src/components/Body.jsBody
中的代码。
src/components/Body.js : 一个body组件
import Container from 'react-bootstrap/Container'; import Stack from 'react-bootstrap/Stack'; import Sidebar from './Sidebar'; export default function Body({ sidebar, children }) { return ( <Container> <Stack direction="horizontal" className="Body"> {sidebar && <Sidebar />} <Container className="Content"> {children} </Container> </Stack> </Container> ); }
JSX 层次结构与App
. 父Container
级不是流动的,其目的是将页面的主体与页眉中存在的非流动容器对齐。组件现在Sidebar
被添加到条件中,只有当sidebar
prop 具有真实值时。第二个(或唯一的,如果sidebar === false
)子Container
元素是组件的子元素的内部,它们代表页面的主要内容。该Stack
组件被分配了一个类名Body
, 以帮助根据需要添加样式。出于同样的原因,内部Container
也被命名。Content
通过添加Body
,App
可以进一步简化组件。更新版本如下。
src/App.js:重构的应用程序组件
import Container from 'react-bootstrap/Container'; import Header from './components/Header'; import Body from './components/Body'; import Posts from './components/Posts'; export default function App() { return ( <Container fluid className="App"> <Header /> <Body sidebar> <Posts /> </Body> </Container> ); }
您不会看到应用程序在浏览器中的外观有任何变化,但这是一个非常健壮且可扩展的重构,可以扩展以支持多个页面和它们之间的路由。
章节总结
- 为避免重新发明轮子,请使用用户界面组件库,例如React-Bootstrap。
- 添加一个顶级容器组件,在所有屏幕尺寸上提供合理的边距。
- 为了更好地组织您的代码,请为应用程序组件创建一个子目录。
- 为了最大限度地重用代码,不要向单个组件添加太多,而是将工作分散到多个组件中,每个组件都有一个目的。
- 使用 props 创建可重用的组件。