在上一节中,我们已经成功构建了基本页面布局。今天,我们要基于前面构建的导航框架,开始进行下一阶段的任务——创建一个完整的响应式导航组件。
创建 Logo
要完成这项工作,我们首先将创建一个新的Logo组件来展示我们的公司 logo。在现代网页设计中,logo 是网站品牌识别的重要元素之一,同时也往往是用户单击以返回首页的地方。
在 components 文件夹里,我们创建一个名为 Logo.tsx 的文件,接下来我们将在这个文件中定义我们的 Logo 组件。
在 public 目录下添加一个名为 logo.png 的图片,然后在 Logo.tsx 文件中添加如下代码:
// src/components/Logo.tsx
export function Logo() {
return (
<a href="./" className="w-12 h-12">
<img src="/logo.png" alt="桃李" className="w-full h-full" />
</a>
);
}
如上代码所示,我们新建了一个 Logo 组件,并在这个组件中,使用 <img> 标签插入了 logo 图片,点击后跳转到首页。
- 对于
<a>标签,我们给其添加了w-12 h-12类名,意味着它是我们根目录字号的 12 倍,即 48px。 - 对
<img>标签,我们施加了w-full h-full的样式类,代表图片要占满父元素的全部空间,即<a>标签的全部内容区。
接下来,我们将把新的 Logo 组件添加到我们的导航栏组件中。改动后的导航栏现在应该像这样:
import { Logo } from "./Logo";
export function Navbar() {
return (
<nav className="w-full bg-amber-50">
<div className="max-w-screen-xl mx-auto px-8">
<div className="h-16 flex items-center justify-between gap-4">
<div className="flex items-center gap-8">
<Logo /> {/* 使用 Logo 组件 */}
<div>菜单</div>
</div>
<div className="flex items-center gap-4">
<button>登录</button>
<button>注册</button>
</div>
</div>
</div>
</nav>
);
}
在完成以上修改之后,记得把你的代码提交到 git。
创建导航菜单
接下来,我们来创建和完善导航菜单。
创建 NavLink 组件
我们首先需要为导航栏创建一个名为 NavLink 的组件,它将包含到各个页面的链接。
- 在
components文件夹下,新建一个文件命名为NavMenu.tsx。 - 然后在该文件内添加以下代码:
// src/components/NavMenu.tsx
export interface NavLinkProps {
key: string;
url: string;
name: string;
}
在这部分代码中,我们定义了一个 TypeScript 接口 NavLinkProps。接口包括两个属性:
url:代表链接目标页面的字符串。name:表示链接文本的字符串。
- 进一步定义
NavLink组件如下:
// src/components/NavMenu.tsx
export function NavLink(props: NavLinkProps) {
const { url, name } = props;
return (
<li>
<a
className="h-16 flex items-center px-4 transition hover:bg-gray-100 hover:font-medium hover:text-pink-600"
href={url}
>
{name}
</a>
</li>
);
}
这个 NavLink 组件接收一个参数 props,其类型为 NavLinkProps。
发现我们这里用了一种新的写类名的方式 hover:bg-gray-100 ,它的含义是被用户通过鼠标悬停时,元素的背景色会变为灰色。而当鼠标不再悬停在该元素上时,它将恢复原样。把这 3 个类名转换为独立的 CSS,可以得到以下结果。
.hover\:text-pink-600:hover {
--tw-text-opacity: 1;
color: rgb(219 39 119 / var(--tw-text-opacity));
}
.hover\:font-medium:hover {
font-weight: 500;
}
.hover\:bg-gray-100:hover {
--tw-bg-opacity: 1;
background-color: rgb(243 244 246 / var(--tw-bg-opacity));
}
\: 是用来转义冒号 : 的。这允许我们创建出一个包含冒号的类名,而不会被 CSS 解析器误解为伪类选择器。
创建 NavMenu 组件
在创建了 NavLink 组件的基础上,我们可以构建 NavMenu 组件。NavMenu 组件将包含多个 NavLink 组件,从而形成完整的导航菜单。
请将以下代码添加到 NavMenu.tsx 文件中:
// src/components/NavMenu.tsx
export interface NavMenuProps {
data: NavLinkProps[];
}
function NavMenu(props: NavMenuProps) {
const { data } = props;
return (
<ul className="flex items-center">
{data.map((item: NavLinkProps) => (
<NavLink {...item} />
))}
</ul>
);
}
export default NavMenu;
代码解析:
NavMenuProps是一个包含NavLinkProps数组的接口,每个数组项将用来生成一个NavLink组件。NavMenu函数则利用map方法,遍历data数组,并为数组中的每一项都生成一个NavLink组件。
在 Navbar 中使用
接下来我们在 Navbar 组件中调用创建好的 NavMenu 组件。打开 src/components/Navbar.tsx 文件,首先,我们引入需要用到的组件和类型:
// src/components/Navbar.tsx
import NavMenu, { NavMenuProps } from "./NavMenu";
接着,定义导航菜单的数据,这将会生成一个导航菜单,由五个链接组成,分别链接到网页的五个不同部分。
// src/components/Navbar.tsx
const NAV_MENU_DATA: NavMenuProps["data"] = [
{ key: "1", url: "#home", name: "首页" },
{ key: "2", url: "#paths", name: "学习路线" },
{ key: "3", url: "#tools", name: "工具" },
{ key: "4", url: "#about", name: "关于我们" },
{ key: "5", url: "#help", name: "帮助" },
];
最后,在 Navbar 组件中调用 NavMenu 组件。
// src/components/Navbar.tsx
export function Navbar() {
return (
<nav className="w-full">
<div className="max-w-screen-xl mx-auto px-8">
<div className="h-16 flex items-center justify-between gap-4">
<div className="flex items-center gap-8">
<Logo />
<NavMenu data={NAV_MENU_DATA} />
</div>
{/* ... */}
</div>
</div>
</nav>
);
}
完整的代码改动如图所示。
这时候,回到浏览器发现我们的导航菜单已经显示了。同时记得保存并提交到 git。
创建按钮组件
为了提高的网页用户交互体验,我们将创建区别显著的登录和注册按钮。此过程需要创建两种类型的按钮。
安装 classnames 库
首先,在终端中运行命令 pnpm install classnames 来安装classnames库。这个库有助于动态构建className字符串,让我们能够根据组件的props或state切换类名,所以使得我们可以方便地修改样式。
接下来,在 scr/components 目录下创建一个名为 Button.tsx 的文件,导入classnames库,并命名为cx:
// src/components/Button.tsx
import cx from "classnames";
定义 Button 组件
然后,定义一个ButtonProps interface。此接口有两个可选属性:type和children。
javascript复制代码
export interface ButtonProps {
type?: "default" | "primary";
children: React.ReactNode;
}
默认type是"default",但如果在使用时指定type为"primary",按钮的样式将会改变。children是React元素,表示按钮内部要显示的内容。
接着,定义Button组件,从props中解析出type和children。在这个组件中,我们用到了classNames库来根据button的type动态改变类名和样式。
javascript复制代码
export function Button(props: ButtonProps) {
const { type = "default", children } = props;
return (
<button
className={cx(
"rounded-full transition duration-300 hover:shadow-xl px-8 h-10 flex items-center",
{
"border border-gray-300 bg-white hover:border-pink-600 hover:text-pink-600":
type === "default",
"bg-gradient-to-r from-pink-400 to-pink-600 text-white":
type === "primary",
}
)}
>
{children}
</button>
);
}
到此,我们成功定义了一个可以根据属性变化样式的Button组件。
CSS 类名解读
这里用到的几个新的类名,转换为 CSS 后显示如下。
.transition {
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.duration-300 {
transition-duration: 300ms;
}
.bg-gradient-to-r {
background-image: linear-gradient(to right, var(--tw-gradient-stops));
}
.from-pink-400 {
--tw-gradient-from: #f472b6 var(--tw-gradient-from-position);
--tw-gradient-to: rgb(244 114 182 / 0) var(--tw-gradient-to-position);
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
}
.to-pink-600 {
--tw-gradient-to: #db2777 var(--tw-gradient-to-position);
}
Tailwind 是一个实用程序优先的 CSS 框架,让我们可以通过组合类来构建自定义设计。以下是每个类别的详细解释:
.transition使用了 predefined transition-property 和定时功能,并使过渡效果的持续时间设为150毫秒。.duration-300更改了预设置过渡效果的持续时间 为 300 毫秒。.bg-gradient-to-r设置了一个背景渐变,该渐变颜色从左至右(从左到右)因为linear-gradient(to right, ...)..from-pink-400设置了两种渐变色停止,从 #f472b6 颜色开始。.to-pink-600继续上面渐变色的设置,结束颜色设为 #db2777。其中 var(--tw-gradient-to-position) 允许你放置颜色在渐变轴线的任何位置。
可以看到,这些类名都在操作 CSS 变量,这个功能使我们得以写出带有高度重用性和模块化的 CSS 代码。
将 Button 应用到 Navbar 组件
现在我们在网页导航栏(Navbar)中引入并使用Button组件。
首先,在Navbar组件文件中导入Button组件,接着,在导航菜单布局中放置导入的的Button组件,替换之前的登录和注册。登陆按钮类型设为default,注册按钮类型选择primary。
import { Button } from "./Button";
export function Navbar() {
return (
<nav className="w-full">
{/* ... */}
<div className="flex items-center gap-4">
<Button>登录</Button>
<Button type="primary">注册</Button>
</div>
{/* ... */}
</nav>
);
}
回到浏览器中查看,发现我们的导航按钮也已经更新了。
本节完整的代码改动如下图,记得保存并提交到 git。
创建响应式菜单
现在我们已经创建了一个网页导航组件,会发现在宽屏端工作得很好,但当窗口缩小时出现了以下问题:
- 图片被压扁
- 导航文字、按钮文字发生换行
- 按钮超出界面范围
这些都是因为我们的布局不够"响应式"所致。实际上,响应式设计可以让页面根据设备大小和方向适当地调整内容。简单来说,就是页面要在所有设备上看起来都风格一致而且功能齐全。
在本教程中,我们将学习如何采用响应式布局修复这些问题。特别是在中小屏幕下,我们想让导航和按钮默认隐藏,在用户点击时再展示出来。
Tailwind CSS 的响应式设计
Tailwind CSS 大大简化了响应式设计的实现。其原理和实现思路主要涉及以下几点:
- Breakpoints: Tailwind 预设了几个断点(即媒体查询),每个断点对应不同的屏幕尺寸。默认情况下,
- sm 对应 640px
- md 对应 768px
- lg 对应 1024px
- xl 对应 1280px
- 2xl 对应 1536px
- 断点的前缀:以
md:和max-md:为例,这是两种对于断点的前缀,用来设定一定范围屏幕尺寸下应用的样式。-
md:表示这个断点及以上的屏幕尺寸都将应用 md: 后的样式设定。例如
md:hidden将在大于等于768px的屏幕下将元素隐藏。转化为 CSS 代码,结果如下:@media (min-width: 768px) { .md\:hidden { display: none; } } -
max-md:则表示最大值断点,这就意味着样式仅适用于小于或等于设定像素的屏幕尺寸。例如
max-md:hidden将在小于等于768px的屏幕下将元素隐藏。转化为 CSS 代码,结果如下:@media not all and (min-width: 768px) { .max-md\:hidden { display: none; } }
-
- 自定义断点: 尽管 Tailwind CSS 提供了一些预设的断点,但我们也可以自定义并添加额外的断点以满足需要。
升级 NavMenu 组件支持移动端
在上一节,我们遗留了一个小问题:按钮的文案可能没有始终水平居中对齐。下面,打开 Button 组件,加多一个 justify-center 的类名。Tailwind CSS 的“justify-center”类会让flex容器中的项目水平居中。
// src/components/Button.tsx
<button
className={cx(
"... flex items-center justify-center",
// ...
接下来,升级 Navbar 组件,在小于等于 768px 的屏幕下,做一些适配和调整。
调整 NavMenu 组件
在存在 NavLink 组件的文件中,我们要做以下修改:
- 使
<li>元素在中等尺寸屏幕(md)以下全宽(max-md:w-full)。 - 同时,让链接(即
<a>标签)在 md 屏幕及以下高度为10个单位,圆角为100%,并使文字居中对齐。
export function NavLink(props: NavLinkProps) {
const { url, name } = props;
return (
<li className="max-md:w-full">
<a
className="h-16 max-md:h-10 max-md:rounded-full ..."
href={url}
>
{name}
</a>
</li>
);
}
升级 NavMenu 组件
在 NavMenu 组件,我们需要更改 ul 元素的样式:
-
在 md 屏幕及以下,让
<ul>中的列表项纵向排列 (flex-col) 而非横向 (flex),可以通过 max-md:flex-col 类实现。function NavMenu(props: NavMenuProps) { const { data } = props; return ( <ul className="flex items-center max-md:flex-col"> {data.map((item: NavLinkProps) => ( <NavLink {...item} /> ))} </ul> ); }
调整中小屏幕下 Navbar 布局
由于两种布局形式差异较大,我们在大屏幕和中小屏幕下分别设置两套代码来展示布局。
隐藏大屏幕导航部分
在小屏幕下 (<768px, 对应于max-md:),我们希望隐藏那些为大屏幕设计的内容。通过添加 max-md:hidden 类,我们可以做到这点。
// src/components/Navbar.tsx
export function Navbar() {
return (
<nav className="w-full">
<div className="max-w-screen-xl mx-auto px-8 max-md:hidden">
// ...
添加为小屏幕专门设计的代码块
然后,我们添加一个完全新的从逻辑上与我们先前的 Navbar 部分分割开的代码块,这个部分是专门为小屏幕设计的。
默认情况下,它会被隐藏 (hidden),但会在中等或更小尺寸的屏幕上可见 (max-md:visible) 并且在大于中等(md)尺寸的屏幕上隐藏(md:hidden)。
// src/components/Navbar.tsx
export function Navbar() {
return (
<nav className="w-full">
<div className="max-w-screen-xl mx-auto px-8 max-md:hidden">
{/* ... */}
</div>
<div className="max-w-screen-xl mx-auto px-8 max-md:visible md:hidden">
<div className="h-16 flex items-center justify-between gap-4">
<Logo />
</div>
<div>
<NavMenu data={NAV_MENU_DATA} />
<div className="py-4 flex flex-col gap-4">
<Button>登录</Button>
<Button type="primary">注册</Button>
</div>
</div>
</div>
</nav>
);
}
现在 Navbar.tsx 完整的代码如下。
import { Button } from "./Button";
import { Logo } from "./Logo";
import NavMenu, { NavMenuProps } from "./NavMenu";
const NAV_MENU_DATA: NavMenuProps["data"] = [
{ key: "1", url: "#home", name: "首页" },
{ key: "2", url: "#paths", name: "学习路线" },
{ key: "3", url: "#tools", name: "工具" },
{ key: "4", url: "#about", name: "关于我们" },
{ key: "5", url: "#help", name: "帮助" },
];
export function Navbar() {
return (
<nav className="w-full">
<div className="max-w-screen-xl mx-auto px-8 max-md:hidden">
<div className="h-16 flex items-center justify-between gap-4">
<div className="flex items-center gap-8">
<Logo />
<NavMenu data={NAV_MENU_DATA} />
</div>
<div className="flex items-center gap-4">
<Button>登录</Button>
<Button type="primary">注册</Button>
</div>
</div>
</div>
<div className="max-w-screen-xl mx-auto px-8 max-md:visible md:hidden">
<div className="h-16 flex items-center justify-between gap-4">
<Logo />
</div>
<div>
<NavMenu data={NAV_MENU_DATA} />
<div className="py-4 flex flex-col gap-4">
<Button>登录</Button>
<Button type="primary">注册</Button>
</div>
</div>
</div>
</nav>
);
}
然后,返回浏览器。如果你的开发服务器还在运行,那么你的网页应该已经自动刷新,并且可以看到新的更改。
如果你已经终止了你的服务,就需要重新启动它。在你的项目根目录打开终端或命令行,运行以下命令启动开发服务器,并重新在浏览器打开 http://127.0.0.1:5173/。
pnpm dev
尝试改变浏览器窗口大小,发现在小屏幕下也已经展示正常了。
记得及时保存和提交代码到 git。
点击展开和隐藏
我们希望在屏幕较小的设备上,默认隐藏菜单,并只有在点击菜单图标后才展示出来。
首先,我们需要两个 SVG 图标,一个表示菜单(三横线),另一个表示关闭(叉号)。你可以直接复制我给出的代码,并分别保存在 components 文件夹下的 IconMenu.tsx 和 IconClose.tsx 文件中。
// src/components/IconMenu.tsx
export function IconMenu(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
{...props}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
/>
</svg>
);
}
// src/components/IconClose.tsx
export function IconClose(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
{...props}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
);
}
然后,我们将在导航栏组件 (Navbar) 中引入切换展示与隐藏菜单的代码。
使得组件有交互性的方法之一就是使用 React 的状态 (state)。具体到这个例子,我们需要让导航菜单知道当前是应该显示还是隐藏菜单。为此引入 useState,它是 React 的一个钩子函数(hook),用来添加本地状态 (state) 到函数组件。
于是我们在导航栏组件(Navbar)内部加入下面的代码:
export function Navbar() {
const [open, setOpen] = useState(false);
return (/* ... */);
}
const [open, setOpen] = useState(false); 这行代码做了什么呢?
- 我们声明了一个新的状态变量,叫做
open,初次渲染时其值为false,意味着菜单默认为关闭状态。 setOpen函数用来更新open变量的值,进而触发组件的重新渲染。
紧接着,我们可以通过加入按钮来改变 open 状态——也就是说,每当我们点击这个按钮,我们就调用 setOpen 函数,把 open 的值反转,如果原来是 true 则变为 false,反之亦然。如下所示:
<button onClick={() => setOpen(!open)}>
{open ? (
<IconClose className="w-6 h-6" />
) : (
<IconMenu className="w-6 h-6" />
)}
</button>
同时,我们可以通过判断 open 的状态来决定渲染哪个图标。如果菜单已打开(open == true),则渲染关闭图标;否则渲染菜单图标。
最后,我们使用 hidden 类名来控制菜单是否可见。如果 open 的值为 true,则菜单可见;否则菜单隐藏。
<div
className={cx({
hidden: !open,
})}
>
<NavMenu data={NAV_MENU_DATA} />
<div className="py-4 flex flex-col gap-4">
<Button>登录</Button>
<Button type="primary">注册</Button>
</div>
</div>
如此,设计出一个可以响应式展开和隐藏的导航栏就完成了。
下面是更新后的完整代码。
// src/components/Navbar.tsx
import { useState } from "react";
import { Button } from "./Button";
import { IconClose } from "./IconClose";
import { IconMenu } from "./IconMenu";
import { Logo } from "./Logo";
import NavMenu, { NavMenuProps } from "./NavMenu";
import cx from "classnames";
const NAV_MENU_DATA: NavMenuProps["data"] = [
{ key: "1", url: "#home", name: "首页" },
{ key: "2", url: "#paths", name: "学习路线" },
{ key: "3", url: "#tools", name: "工具" },
{ key: "4", url: "#about", name: "关于我们" },
{ key: "5", url: "#help", name: "帮助" },
];
export function Navbar() {
const [open, setOpen] = useState(false);
return (
<nav className="w-full">
<div className="max-w-screen-xl mx-auto px-8 max-md:hidden">
<div className="h-16 flex items-center justify-between gap-4">
<div className="flex items-center gap-8">
<Logo />
<NavMenu data={NAV_MENU_DATA} />
</div>
<div className="flex items-center gap-4">
<Button>登录</Button>
<Button type="primary">注册</Button>
</div>
</div>
</div>
<div className="max-w-screen-xl mx-auto px-8 max-md:visible md:hidden">
<div className="h-16 flex items-center justify-between gap-4">
<Logo />
<button onClick={() => setOpen(!open)}>
{open ? (
<IconClose className="w-6 h-6" />
) : (
<IconMenu className="w-6 h-6" />
)}
</button>
</div>
<div
className={cx({
hidden: !open,
})}
>
<NavMenu data={NAV_MENU_DATA} />
<div className="py-4 flex flex-col gap-4">
<Button>登录</Button>
<Button type="primary">注册</Button>
</div>
</div>
</div>
</nav>
);
}
返回浏览器,缩小屏幕宽度,发现右侧多了一个菜单按钮,点击试试,可以展开收起菜单面板就说明组件创建成功了。
最后记得提交代码到 git,养成好习惯。
总结
本教程中,我们以创建一个响应式的导航栏为目标,较详细地探讨了如何使用 React 和 Tailwind CSS 来设计并实现组件。
首先,我们创建了一个 Logo 组件显示公司标志,并理解了图片如何在组件中被使用。
然后,我们创建了 NavLink 组件作为单个的导航链接。每一个 NavLink 由一个 li 元素包裹的 a 元素组成,链接到网站的不同页面。
之后,我们创建了 NavMenu 组件,它是由多个 NavLink 组成的菜单。通过给其传递一个包含链接信息的数组作为 props,我们能轻松地调整导航菜单。
接着,我们将 NavMenu 组件添加到 Navbar 组件中,形成了初步的导航栏。
为了形成一个完整的导航栏,我们加入了按钮组件,并学会了如何安装和使用 classnames 库来更好地进行类名管理。
了解基础的 Tailwind CSS 的响应式设计后,我们开始着手改造 NavMenu 组件,使之能在移动设备上正常工作。我们整理了 NavMenu 组件的布局,并为大屏和小屏提供了不同的 CSS 样式,完成了响应式设计。
最后,我们运用了状态(state)和事件处理(event handler)来控制导航菜单在small devices上的展开和收起。我们同时创建了两个 SVG 图标组件,以提供视觉反馈。
总结来说,在实现 Navbar 开发过程中,我们学习并应用了众多重要的前端技术概念:组件化、props、hook 等 React 基础知识;Tailwind CSS 的应用以及响应式设计方法;点击事件的处理及状态管理等交互逻辑设计,是一个相对全面的示例。
希望这个教程能帮助你更好地理解和使用 React、Tailwind CSS,以及一些主流的 web 开发方法。
接下来我们将继续实现主要内容部分的创建工作。