使用基于文件的路由的Next.js事件管理应用
Next.js可以说是使用React构建Web应用的最通用的框架。在这里,学习如何建立一个Next.js的事件管理应用程序。
在使用React构建网络应用时,Next.js可以说是最通用的框架。Next.js使构建生产就绪的应用程序变得容易。在这篇文章中,我们将研究如何构建一个Next.js的事件管理应用程序。
一旦我们完成,我们的应用程序将看起来像下面的图片。
这个应用程序将是一个纯Next.js组件和其他React组件的混合体。此外,还将使用一些CSS来设计组件的样式。最后,我们将使用Next.js基于文件的路由来连接该应用程序。
最终,这个应用程序的想法是展示我们如何使用Next.js和普通的React建立一个更大的应用程序。
1.设置一个新的Next.js项目
作为使用Next.js的先决条件,我们需要在系统上安装Node.js。你可以从官方网站上为你的操作系统安装Node.js。
一旦Node.js设置好了,我们就可以开始使用Next.js的事件管理应用程序了。
首先,我们将使用以下命令创建一个新的Next.js项目。
$ npx create-next-app
上面的命令生成了一个类似于启动器的项目。
总之,这里要注意的是package.json 文件。如果你打开该文件,你将能够看到各种脚本和依赖关系。
package.json{ "name": "nextjs-app", "version": "0.1.0", "private": true, "scripts": { "dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint" }, "dependencies": { "next": "12.2.3", "react": "18.2.0", "react-dom": "18.2.0" }, "devDependencies": { "eslint": "8.20.0", "eslint-config-next": "12.2.3" } }
2.创建假的事件管理数据
虽然一个典型的prod应用程序可能有一个数据库来存储信息,但我们将使用一些假的事件数据来让我们的应用程序运行。通常情况下,开发者更喜欢以这种方式开始,这样他们就可以在插手数据库相关的修改之前获得对应用程序的感觉,等等。
在我们项目的根部,我们创建一个文件dummy-data.js 。见下图。
dummy-data.jsconst DUMMY_EVENTS = [ { id: 'event1', title: 'Programming for Everyone', description: 'Everyone can learn to code! Yes, everyone! Live Event', location: 'A street 25, San Francisco', date: '2022-06-14', image: 'images/coding-event.jpg', isFeatured: false }, { id: 'event2', title: 'Networking Basics', description: 'Making networking for introverts fun', location: 'Street 47, New York', date: '2022-06-21', image: 'images/network-event.jpg', isFeatured: true }, { id: 'event2', title: 'Networking Advanced', description: 'Making networking for advanced use-cases', location: 'Street 47, New York', date: '2022-07-25', image: 'images/network-event-advanced.jpg', isFeatured: true } ] export function getFeaturedEvents() { return DUMMY_EVENTS.filter((event) => event.isFeatured); } export function getAllEvents() { return DUMMY_EVENTS; } export function getFilteredEvents(dateFilter) { const { year, month } = dateFilter; let filteredEvents = DUMMY_EVENTS.filter((event) => { const eventDate = new Date(event.date); return eventDate.getFullYear() === year && eventDate.getMonth() === month - 1; }) return filteredEvents; } export function getEventById(id) { return DUMMY_EVENTS.find((event) => event.id === id); }
正如你所看到的,我们有DUMMY_EVENTS 数组,它包含了一个事件的列表及其各种细节。另外,我们从这个文件中导出了一堆函数。
基本上,这些函数是用来从事件数组中获取或过滤事件的。下面是每个函数的细节。
getFeaturedEvents()- 这个函数返回一个事件的列表,其 标志设置为真。isFeaturedgetAllEvents()- 此函数返回所有的事件。getFilteredEvents()- 此函数返回基于过滤条件的事件列表。在目前的实现中,我们支持按年份和月份进行过滤。getEventById()- 最后,这个函数返回一个输入事件ID的单一事件。
你可以把这些函数看成是获取事件日期的一个接口。我们没有将这些函数作为外部的REST API暴露出来,因为我们将在我们的应用程序中只在内部使用它们。
3.使用基于文件的路由创建Next.js路由
在这一点上,我们可以开始为我们的事件管理应用程序的各个页面构建Next.js路由。
大体上,我们的应用程序将有以下路由。
- 根路径(/)--这是起始页面,将显示特色事件的列表。换句话说,就是那些将
isFeatured标志设置为真的事件。 - 所有事件页面(/events)- 这个页面将显示所有事件的列表。
- 单一事件 (/events/<some_id>)- 这个页面将显示基于输入id的单一事件的细节。
- 过滤的事件(/events/...slug)--该页将显示基于标准的过滤事件列表。例如,如果我们访问/events/2022/06,它应该显示2022年6月的事件列表。
对于上述每条路径,让我们创建适当的Next.js组件。
3.1:主页
Next.js有一个特殊的系统来处理路由。
基本上,在我们的项目中,有一个名为pages 的特定文件夹。我们在这个文件夹中创建的任何组件都会被Next.js作为一个路由公开。这也被称为Next.js基于文件的路由。
在pages 目录中,我们将创建一个名为index.js 的文件。这是用于渲染我们应用程序的主页的文件。
见下图。
pages/index.jsimport { getFeaturedEvents } from '../dummy-data'; import EventList from '../components/events/EventList'; function HomePage() { const featuredEvents = getFeaturedEvents(); return ( <div> <EventList items={featuredEvents} /> </div>) } export default HomePage;
正如你所看到的,这是一个普通的React组件。它从作为dummy-data.js 文件一部分暴露的适当函数中获取特色事件列表。
一旦它得到了数据,它就把这个列表传递给另一个React组件EventList 。React组件的细节在下一节中介绍,供参考。
3.2:所有事件页面
这个页面显示了所有事件的列表。为了更好地隔离,我们把这个页面的组件文件放在pages 目录内的文件夹events 。
pages/events/index.jsimport { useRouter } from 'next/router'; import EventList from "../../components/events/EventList"; import EventSearch from "../../components/events/EventSearch"; import { getAllEvents } from "../../dummy-data"; function AllEventsPage() { const router = useRouter(); const events = getAllEvents(); function findEventsHandler(year, month) { const fullPath = `/events/${year}/${month}`; router.push(fullPath); } return ( <div> <EventSearch onSearch={findEventsHandler} /> <EventList items={events} /> </div> ) } export default AllEventsPage;
在这个组件中,有几个重要的点需要注意。
- 首先,我们使用
getAllEvents()函数从假事件数据中获取所有事件。列表是使用普通的EventList组件渲染的。 - 其次,我们还有一个功能是在这个页面上根据过滤标准搜索事件。为了这个目的,我们有一个
EventSearch组件。这个组件接受一个指向findEventsHandler函数的道具onSearch。基本上,EventSearch组件将过滤年和过滤月传递给AllEventsPage组件。使用过滤年和过滤月,我们构建一个路由路径,并使用router.push(),以编程方式改变我们应用程序的路由。
你可以查看下面EventSearch组件的代码。
component/events/EventSearch.jsimport { useRef } from 'react';
import Button from "../ui/Button";
import classes from "./event-search.module.css";
function EventSearch(props) {
const yearInputRef = useRef();
const monthInputref = useRef();
function submitHandler(event) {
event.preventDefault();
const selectedYear = yearInputRef.current.value;
const selectedMonth = monthInputref.current.value;
props.onSearch(selectedYear, selectedMonth);
}
return (
<form className={classes.form} onSubmit={submitHandler}>
<div className={classes.controls}>
<div className={classes.control}>
<label htmlFor="year">Year</label>
<select id="year" ref={yearInputRef}>
<option value="2021">2021</option>
<option value="2022">2022</option>
</select>
</div>
<div className={classes.control}>
<label htmlFor="month">Month</label>
<select id="month" ref={monthInputref}>
<option value="1">January</option>
<option value="2">February</option>
<option value="3">March</option>
<option value="4">April</option>
<option value="5">May</option>
<option value="6">June</option>
<option value="7">July</option>
<option value="8">August</option>
<option value="9">September</option>
<option value="10">October</option>
<option value="11">November</option>
<option value="12">December</option>
</select>
</div>
</div>
<Button>Find Events</Button>
</form>
)
}
export default EventSearch;
基本上,这个组件只是处理过滤年和过滤月的表格字段。
目前,根据这个文件,我们只支持2021年和2022年。在真正的应用中,我们会在这个组件中设置一个日历小部件。
当用户通过点击表单按钮提交表单时,我们调用props.onSearch ,将selectedYear 和selectedMonth 传递给父组件。
3.3:单一事件页
单一事件页基本上是一个特定事件的页面。它只是显示所选事件的细节。
从实现的角度来看,它是一个极其简单的组件。请看下面。
pages/events/\[eventId\].js`import { useRouter } from 'next/router'; import EventItem from '../../components/events/EventItem'; import { getEventById } from '../../dummy-data'; function EventDetailPage() { const router = useRouter(); const eventId = router.query.eventId; const event = getEventById(eventId); if (!event) { return <p>No Event Found</p> } return ( <EventItem id={event.id} title={event.title} location={event.location} date={event.date} image={event.image} /> ) } export default EventDetailPage;`
唯一需要注意的是,这是一个动态页面。换句话说,其内容取决于路径中的eventId 。
在Next.js中,我们创建这类组件时的命名规则是:[eventId].js 。基本上,这标志着eventId 是动态的,并且在浏览器路径中可用。为了提取eventId ,我们利用useRouter() 钩子,然后调用getEventById() 函数。
这个页面还使用了一个常见的组件EventItem 。我们将在下一节介绍它。
3.4:筛选的事件页面
最后,我们还可以为过滤的事件创建一个页面。
由于这也是一个取决于年和月的值的动态页面,我们将文件命名为[...slug].js 。这里,slug将包含路径的所有部分的列表。
例如,如果我们访问/events/2022/06 ,lug数组将包含值['2022', '06'] 。
请看下面的实现。
pages/events/\[...slug\].js`import { useRouter } from 'next/router'; import EventList from '../../components/events/EventList'; import { getFilteredEvents } from '../../dummy-data'; function FilteredEventsPage() { const router = useRouter(); const filterData = router.query.slug; if (!filterData) { return <p className='center'>Loading...</p> } const filteredYear = filterData[0]; const filteredMonth = filterData[1]; const numYear = +filteredYear; const numMonth = +filteredMonth; if (isNaN(numYear) || isNaN(numMonth)) { return <p className='center'>Invalid Filter Criteria. Please check...</p> } const filteredEvents = getFilteredEvents({ year: numYear, month: numMonth }); if (!filteredEvents || filteredEvents.length === 0) { return <p>No Events Found!!</p> } return( <div> <EventList items={filteredEvents} /> </div> ) } export default FilteredEventsPage;`
就像之前的组件一样,这里我们也使用useRouter() 钩子来提取slug 。然后,我们确保年和月是有数字值的。如果数值没有问题,我们就简单地调用getFilteredEvents() 函数。
再一次,我们使用相同的EventList 组件来渲染事件列表。
4.处理事件数据的常见React组件
现在,我们应用程序的主要页面已经完成,让我们看看我们在应用程序中使用的常见React组件。
为了更好地管理我们的源代码,我们把通用组件放在一个单独的目录中,名为components 。注意,我们不能把这些组件放在pages 目录中。这是因为无论什么东西在pages 目录中,都会被Next.js用来创建一个路由。
在components 目录中,我们为事件相关的组件创建一个文件夹。
第一个重要的组件是EventList 组件。
components/events/EventList.jsimport EventItem from './EventItem';
import classes from './event-list.module.css';
function EventList(props) {
const { items } = props;
return (
<ul className={classes.list}>
{items.map(event => <EventItem key={event.id}
id={event.id}
title={event.title}
location={event.location}
date={event.date}
image={event.image} />)}
</ul>
)
}
export default EventList;
基本上,这个组件接收事件列表并生成一个无序的列表。它也使用EventItem 组件。见下文。
components/events/EventItem.jsimport Link from 'next/link';
import Button from '../ui/Button';
import classes from './event-item.module.css';
function EventItem(props) {
const { title, image, date, location, id } = props;
const humanReadableDate = new Date(date).toLocaleDateString('en-US', {
day: 'numeric',
month: 'long',
year: 'numeric'
});
const formattedAddress = location.replace(', ', '\n')
const exploreLink = `/events/${id}`
return (
<li className={classes.item}>
<img src={'/' + image} alt={title} />
<div className={classes.content}>
<div className={classes.summary}>
<h2>{title}</h2>
<div className={classes.date}>
<time>{humanReadableDate}</time>
</div>
</div>
<div className={classes.address}>
<address>{formattedAddress}</address>
</div>
</div>
<div className={classes.actions}>
<Button link={exploreLink}>Explore Event</Button>
</div>
</li>
)
}
export default EventItem;
基本上,这个组件接收单个事件的数据。它对数据进行重新格式化以达到展示的目的:例如,将日期改为人类可读的格式,同时也对地址进行格式化。此外,它还为 "探索事件"的按钮构建适当的链接。
此外,我们还有一个特殊的Button 组件。
components/ui/Button.jsimport Link from 'next/link';
import classes from './button.module.css';
function Button(props) {
if (props.link) {
return <Link href={props.link}>
<a className={classes.btn}>{props.children}</a>
</Link>
}
return <button className={classes.btn} onClick={props.onClick}>{props.children}</button>
}
export default Button;
基本上,Button 组件处理的情况是,它作为一个Link 。另外,如果props.link 是未定义的,它作为一个普通的按钮。
5.Next.js事件管理应用程序导航
当我们的应用程序显示各个页面时,没有适当的导航条。我们需要在每个页面都有这个导航条。
因此,我们为其创建了另一个通用组件。
首先是Layout 组件。
components/layout/Layout.jsimport { Fragment } from "react";
import MainHeader from "./MainHeader";
function Layout(props) {
return <Fragment>
<MainHeader />
<main>
{props.children}
</main>
</Fragment>
}
export default Layout;
第二个是MainHeader 组件。
components/events/MainHeader.jsimport Link from 'next/link';
import classes from './main-header.module.css';
function MainHeader() {
return (
<header className={classes.header}>
<div className={classes.logo}>
<Link href="/">Next Events</Link>
</div>
<nav className={classes.navigation}>
<Link href="/events">All Events</Link>
</nav>
</header>
)
}
export default MainHeader;
基本上,我们在这里定义了我们应用程序的Logo和一个导航到AllEvents页面的链接。为了导航,我们使用Next.js附带的特殊Link 组件。Next.js的链接组件有助于应用程序的导航,而无需使用React路由器。
最后,为了使其显示在每个页面上,我们将我们应用程序的主要组件,即MyApp 组件(_app.js 文件)包裹在pages 目录中。
pages/_app.jsimport Layout from '../components/layout/layout' import '../styles/globals.css' function MyApp({ Component, pageProps }) { return <Layout> <Component {...pageProps} /> </Layout> } export default MyApp
注意,这个文件已经存在于启动项目中。我们只需对其进行修改即可。
6.塑造Next.js应用程序的风格
最后,你可能已经注意到我们在不同的组件中使用了大量的CSS类。为了将CSS的范围扩大到特定的组件,我们使用了CSS模块系统。
虽然CSS是完全可有可无的,但它对我们项目的外观和感觉肯定有帮助。
下面是各个组件的CSS文件。
event-item.module.css
.item { box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3), 0 1px 12px 2px rgba(0, 0, 0, 0.2); border-radius: 8px; overflow: hidden; background-color: white; margin: 1rem; display: flex; flex-direction: column; gap: 1rem; } .item img { width: 100%; object-fit: cover; height: 10rem; } .content { width: 100%; padding: 0 1rem; text-align: center; } .content h2 { margin: 0.5rem 0; } .date, .address { display: flex; gap: 0.5rem; align-items: center; } .date svg, .address svg { width: 1.25rem; height: 1.25rem; color: #666666; } .content time { color: #666666; font-weight: bold; } .content address { margin: 0.5rem 0; color: #666666; white-space: pre; } .actions { display: flex; flex-direction: column; padding: 1rem; } .actions a { display: block; } .actions a span { vertical-align: middle; } .icon { margin-left: 0.5rem; display: inline-flex; justify-content: center; align-items: center; } .icon svg { width: 1.25rem; height: 1.25rem; } @media (min-width: 768px) { .item { flex-direction: row; } .item img { width: 40%; height: 14rem; } .content { width: 60%; padding: 0; text-align: left; } .content h2 { margin: 1rem 0; } .actions { flex-direction: row; justify-content: flex-end; } }`
event-list.module.css`.list { width: 90%; max-width: 40rem; margin: 5rem auto; }`
event-search.module.css`.form { margin: 2rem auto; box-shadow: 0 1px 4px rgba(0,0,0,0.2); padding: 1rem; background-color: white; border-radius: 6px; width: 90%; max-width: 40rem; display: flex; justify-content: space-between; flex-direction: column; gap: 1rem; } .controls { width: 100%; display: flex; gap: 1rem; flex-direction: column; } .control { flex: 1; display: flex; gap: 1rem; align-items: center; justify-content: space-between; } .control label { font-weight: bold; } .control select { font: inherit; background-color: white; border-radius: 6px; width: 70%; padding: 0.25rem; } .form button { width: 100%; font: inherit; padding: 0.25rem 0.5rem; background-color: #03be9f; border: 1px solid #03be9f; color: #dafff7; border-radius: 4px; } @media (min-width: 768px) { .form { flex-direction: row; } .controls { width: 80%; flex-direction: row; } .control select { width: 100%; } .form button { width: 20%; } }`
main-header.module.css`.header { width: 100%; display: flex; justify-content: space-between; align-items: baseline; padding: 1rem 10%; height: 5rem; background-color: #202020; } .logo { font-size: 1.5rem; color: white; font-family: 'Fira', sans-serif; height: 100%; display: flex; justify-content: center; align-items: center; color: #94fdfd; } .logo a { text-decoration: none; color: #94fdfd; } .navigation a { text-decoration: none; color: #74dacc; font-size: 1rem; } @media (min-width: 768px) { .logo { font-size: 2.5rem; } .navigation a { font-size: 1.5rem; } }`
button.module.css`.btn { text-decoration: none; cursor: pointer; font: inherit; background-color: #03be9f; border: 1px solid #03be9f; border-radius: 6px; color: #dafff7; padding: 0.5rem 1.5rem; text-align: center; box-shadow: 0 1px 6px rgba(0, 0, 0, 0.2); } .btn:hover, .btn:active { background-color: #02afa1; border-color: #02afa1; }`
你可以把这些CSS文件放在你项目层次结构中的组件文件旁边。这将有助于方便参考和维护。
总结
至此,我们的Next.js事件管理应用程序已经准备就绪。
我们使用基于文件的路由来使高层页面工作。然而,对于个别功能片段,我们利用了基本的React组件。这种使用Next.js概念与React的共同组合,使Next.js成为构建复杂应用程序的一个了不起的工具。
这个应用程序的代码可以在GitHub上找到。
我们可以使用Next.js的Firebase集成进一步增强该应用,以实现静态和服务器端的渲染和存储数据。