全栈应用开发:从聊天应用重构到 Next.js 入门
1. 聊天应用重构
1.1 Socket.IO 服务器重构
为了让聊天应用更具扩展性,首先对 Socket.IO 服务器进行重构,使其使用服务函数。具体步骤如下:
1. 打开
backend/src/socket.js
文件,找到如下导入语句:
import { createMessage, getMessagesByRoom } from './services/messages.js'
将其替换为新的聊天服务函数导入:
import {
joinRoom,
sendPublicMessage,
getUserInfoBySocketId,
} from './services/chat.js'
-
用以下新代码替换整个
handleSocket函数。当建立连接时,使用joinRoom服务函数自动加入公共房间:
export function handleSocket(io) {
io.on('connection', (socket) => {
joinRoom(io, socket, { room: 'public' })
-
为
chat.message事件定义一个监听器,并使用sendPublicMessage服务函数将消息发送到指定房间:
socket.on('chat.message', (room, message) =>
sendPublicMessage(io, { username: socket.user.username, room, message }),
)
注意,我们更改了
chat.message
事件的签名,现在需要传递一个房间参数,以便后续更好地处理多个房间。后续需要调整客户端代码以适应这一变化。
4. 为
user.info
事件定义一个监听器,使用异步服务函数
getUserInfoBySocketId
,并在回调中返回其结果,将此事件转换为确认消息:
socket.on('user.info', async (socketId, callback) =>
callback(await getUserInfoBySocketId(io, socketId)),
)
- 最后,重用之前的身份验证中间件:
io.use((socket, next) => {
if (!socket.handshake.auth?.token) {
return next(new Error('Authentication failed: no token provided'))
}
jwt.verify(
socket.handshake.auth.token,
process.env.JWT_SECRET,
async (err, decodedToken) => {
if (err) {
return next(new Error('Authentication failed: invalid token'))
}
socket.auth = decodedToken
socket.user = await getUserInfoById(socket.auth.sub)
return next()
},
)
})
1.2 客户端代码重构
服务器端代码使用服务函数封装聊天应用的功能后,对客户端代码进行类似的重构,将客户端命令提取到单独的函数中,步骤如下:
1. 编辑
src/hooks/useChat.js
文件,在
useChat
钩子中定义一个新函数来清除消息:
function clearMessages() {
setMessages([])
}
- 定义一个异步函数来获取用户所在的所有房间:
async function getRooms() {
const userInfo = await socket.emitWithAck('user.info', socket.id)
const rooms = userInfo.rooms.filter((room) => room!== socket.id)
return rooms
}
-
在
sendMessage函数中使用这些函数:
async function sendMessage(message) {
if (message.startsWith('/')) {
const command = message.substring(1)
switch (command) {
case 'clear':
clearMessages()
break
case 'rooms': {
const rooms = await getRooms()
receiveMessage({
message: `You are in: ${rooms.join(', ')}`,
})
break
}
-
调整
chat.message事件,除了消息外还发送房间信息。目前,我们总是将消息发送到'public'房间:
default:
receiveMessage({
message: `Unknown command: ${command}`,
})
break
}
} else {
socket.emit('chat.message', 'public', message)
}
-
访问
http://localhost:5173/,验证聊天应用是否仍能正常工作。
1.3 实现加入和切换房间的命令
为了测试新结构的灵活性,实现加入和切换房间的命令,步骤如下:
1. 编辑
backend/src/socket.js
文件,在
chat.message
监听器下方定义一个新的监听器,当从客户端收到
chat.join
事件时,调用
joinRoom
服务函数:
socket.on('chat.join', (room) => joinRoom(io, socket, { room }))
-
编辑
src/components/ChatMessage.jsx文件,显示房间信息:
export function ChatMessage({ room, username, message, replayed }) {
return (
<div style={{ opacity: replayed? 0.5 : 1.0 }}>
{username? (
<span>
<code>[{room}]</code> <b>{username}</b>: {message}
</span>
-
在
propTypes定义中添加room属性:
ChatMessage.propTypes = {
username: PropTypes.string,
message: PropTypes.string.isRequired,
replayed: PropTypes.bool,
room: PropTypes.string,
}
-
编辑
src/hooks/useChat.js文件,定义一个状态钩子来存储当前所在的房间:
export function useChat() {
const { socket } = useSocket()
const [messages, setMessages] = useState([])
const [currentRoom, setCurrentRoom] = useState('public')
- 定义一个新函数来切换房间:
function switchRoom(room) {
setCurrentRoom(room)
}
-
定义一个新函数,通过发送
chat.join事件并切换当前房间来加入一个房间:
function joinRoom(room) {
socket.emit('chat.join', room)
switchRoom(room)
}
-
修改
sendMessage函数以接受命令参数:
async function sendMessage(message) {
if (message.startsWith('/')) {
const [command,...args] = message.substring(1).split(' ')
switch (command) {
- 定义一个新命令来加入房间,首先检查是否传递了参数:
case 'join': {
if (args.length === 0) {
return receiveMessage({
message: 'Please provide a room name: /join <room>',
})
}
-
使用
getRooms函数确保尚未加入该房间:
const room = args[0]
const rooms = await getRooms()
if (rooms.includes(room)) {
return receiveMessage({
message: `You are already in room "${room}".`,
})
}
-
使用
joinRoom函数加入房间:
joinRoom(room)
break
}
-
同样,实现
/switch命令:
case 'switch': {
if (args.length === 0) {
return receiveMessage({
message: 'Please provide a room name: /switch <room>',
})
}
const room = args[0]
const rooms = await getRooms()
if (!rooms.includes(room)) {
return receiveMessage({
message: `You are not in room "${room}". Type "/join ${room}" to join it first.`,
})
}
switchRoom(room)
receiveMessage({
message: `Switched to room "${room}".`,
})
break
}
-
调整
chat.message事件,将消息发送到当前房间:
} else {
socket.emit('chat.message', currentRoom, message)
}
-
访问
http://localhost:5173/,向公共房间发送一条消息,然后执行/join react命令加入react房间,再向该房间发送一条不同的消息。 -
打开另一个浏览器窗口,使用不同的用户登录,会看到公共房间的第一条消息被重播,但看不到
react房间的消息,因为尚未加入该房间。 -
在第二个浏览器窗口中,也执行
/join react命令,会看到第二条消息被重播。 -
尝试使用
/switch public切换回公共房间,并在那里发送另一条消息,两个客户端都会收到该消息,因为它们都在公共房间。
2. Next.js 入门
2.1 什么是 Next.js
Next.js 是一个 React 框架,它将创建全栈 Web 应用所需的所有功能和工具整合在一起。其主要特性如下:
| 特性 | 描述 |
| ---- | ---- |
| 良好的开发体验 | 开箱即用,包括热模块重载、错误处理等 |
| 文件路由和嵌套布局 | 支持 Next.js 的文件路由和嵌套布局,以及路由处理程序定义 API 端点 |
| 国际化支持 | 路由中支持国际化,允许创建国际化路由 |
| 增强的数据获取 | 服务器端和客户端数据获取增强,自带缓存 |
| 中间件 | 可在请求完成前运行代码 |
| 无服务器运行时 | 支持在无服务器运行时运行 API 端点 |
| 静态页面生成 | 支持静态页面生成 |
| 动态组件流 | 根据需要动态流式加载组件,快速显示初始页面,后续加载其他组件 |
| 高级渲染 | 支持服务器端渲染(SSR)和 React Server Components,可在服务器上渲染组件而不向客户端发送额外 JavaScript |
| 服务器动作 | 逐步增强从客户端发送到服务器的表单和动作,即使客户端没有 JavaScript 也能提交表单 |
| 内置优化 | 对图像、字体和脚本进行内置优化,以改善核心 Web 指标 |
| 部署平台 | 提供 Vercel 平台,方便部署应用 |
2.2 设置 Next.js
使用
create-next-app
工具设置一个新的 Next.js 项目,步骤如下:
1. 打开一个新的终端窗口,确保不在任何项目文件夹内,运行以下命令创建一个新文件夹并初始化 Next.js 项目:
$ npx create-next-app@14.1.0
-
当询问是否继续时,按
y并按Return/Enter确认。 -
为项目命名,例如
ch16。 -
按以下方式回答问题:
- 是否使用 TypeScript:否
- 是否使用 ESLint:是
- 是否使用 Tailwind CSS:否
-
是否使用
src/目录:是 - 是否使用 App Router:是
- 是否自定义默认导入别名:否
-
回答完所有问题后,将在
ch16文件夹中创建一个新的 Next.js 应用。 -
在 VS Code 中打开新创建的
ch16文件夹。 - 在新的 VS Code 窗口中,打开终端并使用以下命令运行项目:
$ npm run dev
-
在浏览器中打开
http://localhost:3000,查看运行的 Next.js 应用。 -
create-next-app没有为我们设置 Prettier,因此安装 Prettier:
$ npm install --save-dev prettier@2.8.4 \
eslint-config-prettier@8.6.0
-
在项目根目录创建一个新的
.prettierrc.json文件,内容如下:
{
"trailingComma": "all",
"tabWidth": 2,
"printWidth": 80,
"semi": false,
"jsxSingleQuote": true,
"singleQuote": true
}
-
编辑现有的
.eslintrc.json文件,扩展自prettier:
{
"extends": ["next/core-web-vitals", "prettier"]
}
-
转到 VS Code 工作区设置,将
Editor: Default Formatter设置为 Prettier,并勾选Editor: Format On Save复选框。
2.3 引入 App Router
Next.js 提供了一种特殊的应用结构范式——App Router,它利用
src/app/
文件夹的结构为应用创建路由。根文件夹(
/
路径)是
src/app/
。如果要定义一个路径,如
/posts
,需要创建一个
src/app/posts/
文件夹。为使该文件夹成为有效路由,需在其中放置一个
page.js
文件,该文件包含访问该路由时将渲染的页面组件。
另外,也可以在文件夹中放置一个
route.js
文件,将其转换为 API 路由而不是渲染页面。Next.js 允许定义
layout.js
文件,作为特定路径的布局。布局组件接受子组件,子组件可以包含其他布局或页面,这种灵活性允许定义带有子布局的嵌套路由。
App Router 范式中还有其他特殊文件,如
error.js
文件,当页面出现错误时将渲染该文件;
loading.js
文件,页面加载时(使用 React Suspense)将渲染该文件。
以下是一个 App Router 文件夹结构的示例:
graph LR
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px
src(app):::process --> posts(posts):::process
posts --> page.js(page.js):::process
src --> dashboard(dashboard):::process
dashboard --> layout.js(layout.js):::process
dashboard --> settings(settings):::process
settings --> layout.js(layout.js):::process
settings --> page.js(page.js):::process
settings --> loading.js(loading.js):::process
settings --> error.js(error.js):::process
在上述示例中,
dashboard/settings/
路由由
dashboard
和
settings
文件夹定义。
dashboard
文件夹没有
page.js
文件,访问
dashboard/
将导致 404 错误,但该文件夹有一个
layout.js
文件,定义了仪表板的主布局。
settings
文件夹有另一个
layout.js
文件,定义了仪表板上设置页面的布局,还有一个
page.js
文件,访问
dashboard/settings/
路由时将渲染该文件。此外,它还有一个
loading.js
文件,在设置页面加载时在设置布局内渲染,以及一个
error.js
文件,在加载设置页面出错时在设置布局内渲染。
2.4 创建静态组件和页面
接下来,我们将使用 Next.js 的 App Router 来创建静态组件和页面,以重新构建博客应用。
首先,我们需要明确一些基本的操作和文件结构。在 Next.js 中,我们主要在
src/app/
文件夹下进行操作。
假设我们要创建一个简单的博客,有首页和文章详情页。以下是具体的创建步骤:
-
创建首页
-
在
src/app/文件夹下创建一个page.js文件,这个文件将作为首页的组件。以下是一个简单的首页组件示例:
-
在
export default function HomePage() {
return (
<div>
<h1>Welcome to My Blog</h1>
<p>Here are some of my latest articles...</p>
</div>
);
}
-
创建文章详情页
-
创建一个
src/app/posts/文件夹,用于存放文章相关的内容。 -
在
src/app/posts/文件夹下创建page.js文件,用于渲染文章详情页。为了简单起见,我们可以先硬编码一篇文章的内容,示例代码如下:
-
创建一个
export default function PostPage() {
return (
<div>
<h2>Article Title</h2>
<p>This is the content of the article...</p>
</div>
);
}
-
定义链接
-
为了在首页可以导航到文章详情页,我们需要使用 Next.js 的
Link组件。首先安装next/link,然后修改首页的page.js文件,添加链接,示例如下:
-
为了在首页可以导航到文章详情页,我们需要使用 Next.js 的
import Link from 'next/link';
export default function HomePage() {
return (
<div>
<h1>Welcome to My Blog</h1>
<p>Here are some of my latest articles...</p>
<Link href="/posts">View Articles</Link>
</div>
);
}
通过以上步骤,我们就完成了一个简单的博客应用的静态组件和页面的创建,并且实现了页面之间的导航。
3. 总结
在这个过程中,我们完成了聊天应用的重构,使其更具扩展性和灵活性,能够方便地实现加入和切换房间等功能。同时,我们也开始了 Next.js 的学习之旅,了解了它的基本概念、特性、如何进行项目设置以及如何使用 App Router 来构建应用的路由和页面。
以下是对整个过程的关键步骤总结表格:
| 操作 | 步骤 |
| ---- | ---- |
| 聊天应用重构 | 1. Socket.IO 服务器重构:替换导入语句、修改
handleSocket
函数等;2. 客户端代码重构:提取命令到单独函数;3. 实现加入和切换房间命令:定义监听器、修改组件和状态等 |
| Next.js 入门 | 1. 了解 Next.js 特性:如文件路由、国际化支持等;2. 设置项目:使用
create-next-app
工具;3. 引入 App Router:利用文件夹结构创建路由;4. 创建静态组件和页面:构建首页和文章详情页并定义链接 |
通过这些操作,我们不仅提升了聊天应用的性能和可维护性,还初步掌握了使用 Next.js 进行全栈应用开发的方法,为后续开发更复杂的应用打下了坚实的基础。
在未来的开发中,我们可以进一步探索 Next.js 的更多高级特性,如 API 路由、数据缓存等,以实现更高效、更强大的全栈应用。同时,也可以对聊天应用进行更多的功能扩展,如添加用户管理、消息加密等功能,提升用户体验。
超级会员免费看
69

被折叠的 条评论
为什么被折叠?



