27、全栈应用开发:从聊天应用重构到 Next.js 入门

全栈应用开发:从聊天应用重构到 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'
  1. 用以下新代码替换整个 handleSocket 函数。当建立连接时,使用 joinRoom 服务函数自动加入公共房间:
export function handleSocket(io) {
    io.on('connection', (socket) => {
        joinRoom(io, socket, { room: 'public' })
  1. 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)),
)
  1. 最后,重用之前的身份验证中间件:
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([])
}
  1. 定义一个异步函数来获取用户所在的所有房间:
async function getRooms() {
    const userInfo = await socket.emitWithAck('user.info', socket.id)
    const rooms = userInfo.rooms.filter((room) => room!== socket.id)
    return rooms
}
  1. 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
            }
  1. 调整 chat.message 事件,除了消息外还发送房间信息。目前,我们总是将消息发送到 'public' 房间:
default:
    receiveMessage({
        message: `Unknown command: ${command}`,
    })
    break
}
} else {
    socket.emit('chat.message', 'public', message)
}
  1. 访问 http://localhost:5173/ ,验证聊天应用是否仍能正常工作。
1.3 实现加入和切换房间的命令

为了测试新结构的灵活性,实现加入和切换房间的命令,步骤如下:
1. 编辑 backend/src/socket.js 文件,在 chat.message 监听器下方定义一个新的监听器,当从客户端收到 chat.join 事件时,调用 joinRoom 服务函数:

socket.on('chat.join', (room) => joinRoom(io, socket, { room }))
  1. 编辑 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>
  1. propTypes 定义中添加 room 属性:
ChatMessage.propTypes = {
    username: PropTypes.string,
    message: PropTypes.string.isRequired,
    replayed: PropTypes.bool,
    room: PropTypes.string,
}
  1. 编辑 src/hooks/useChat.js 文件,定义一个状态钩子来存储当前所在的房间:
export function useChat() {
    const { socket } = useSocket()
    const [messages, setMessages] = useState([])
    const [currentRoom, setCurrentRoom] = useState('public')
  1. 定义一个新函数来切换房间:
function switchRoom(room) {
    setCurrentRoom(room)
}
  1. 定义一个新函数,通过发送 chat.join 事件并切换当前房间来加入一个房间:
function joinRoom(room) {
    socket.emit('chat.join', room)
    switchRoom(room)
}
  1. 修改 sendMessage 函数以接受命令参数:
async function sendMessage(message) {
    if (message.startsWith('/')) {
        const [command,...args] = message.substring(1).split(' ')
        switch (command) {
  1. 定义一个新命令来加入房间,首先检查是否传递了参数:
case 'join': {
    if (args.length === 0) {
        return receiveMessage({
            message: 'Please provide a room name: /join <room>',
        })
    }
  1. 使用 getRooms 函数确保尚未加入该房间:
const room = args[0]
const rooms = await getRooms()
if (rooms.includes(room)) {
    return receiveMessage({
        message: `You are already in room "${room}".`,
    })
}
  1. 使用 joinRoom 函数加入房间:
joinRoom(room)
break
}
  1. 同样,实现 /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
}
  1. 调整 chat.message 事件,将消息发送到当前房间:
} else {
    socket.emit('chat.message', currentRoom, message)
}
  1. 访问 http://localhost:5173/ ,向公共房间发送一条消息,然后执行 /join react 命令加入 react 房间,再向该房间发送一条不同的消息。
  2. 打开另一个浏览器窗口,使用不同的用户登录,会看到公共房间的第一条消息被重播,但看不到 react 房间的消息,因为尚未加入该房间。
  3. 在第二个浏览器窗口中,也执行 /join react 命令,会看到第二条消息被重播。
  4. 尝试使用 /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
  1. 当询问是否继续时,按 y 并按 Return/Enter 确认。
  2. 为项目命名,例如 ch16
  3. 按以下方式回答问题:
    • 是否使用 TypeScript:否
    • 是否使用 ESLint:是
    • 是否使用 Tailwind CSS:否
    • 是否使用 src/ 目录:是
    • 是否使用 App Router:是
    • 是否自定义默认导入别名:否
  4. 回答完所有问题后,将在 ch16 文件夹中创建一个新的 Next.js 应用。
  5. 在 VS Code 中打开新创建的 ch16 文件夹。
  6. 在新的 VS Code 窗口中,打开终端并使用以下命令运行项目:
$ npm run dev
  1. 在浏览器中打开 http://localhost:3000 ,查看运行的 Next.js 应用。
  2. create-next-app 没有为我们设置 Prettier,因此安装 Prettier:
$ npm install --save-dev prettier@2.8.4 \
    eslint-config-prettier@8.6.0
  1. 在项目根目录创建一个新的 .prettierrc.json 文件,内容如下:
{
    "trailingComma": "all",
    "tabWidth": 2,
    "printWidth": 80,
    "semi": false,
    "jsxSingleQuote": true,
    "singleQuote": true
}
  1. 编辑现有的 .eslintrc.json 文件,扩展自 prettier
{
    "extends": ["next/core-web-vitals", "prettier"]
}
  1. 转到 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/ 文件夹下进行操作。

假设我们要创建一个简单的博客,有首页和文章详情页。以下是具体的创建步骤:

  1. 创建首页
    • 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>
    );
}
  1. 创建文章详情页
    • 创建一个 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>
    );
}
  1. 定义链接
    • 为了在首页可以导航到文章详情页,我们需要使用 Next.js 的 Link 组件。首先安装 next/link ,然后修改首页的 page.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 路由、数据缓存等,以实现更高效、更强大的全栈应用。同时,也可以对聊天应用进行更多的功能扩展,如添加用户管理、消息加密等功能,提升用户体验。

内容概要:本文系统研究了基于动态三维环境下的Q-Learning算法在无人机自主避障路径规划中的应用,依托Matlab代码实现,深入剖析了强化学习在复杂、时变空间中实现智能决策的机制。研究构建了三维网格化状态空间模型,设计了合理的动作集合与奖励函数,充分考虑静态与动态障碍物的存在,使无人机能够通过与环境持续交互,自主学习规避障碍并趋近目标的最优策略。文章不仅展示了Q-Learning算法在路径规划中的具体实现流程,还涵盖了状态表示、策略迭代、收敛性分析等关键环节,并通过仿真实验验证了算法的有效性与鲁棒性,为智能体在动态环境中的自主导航提供了理论依据和技术参考。; 适合人群:具备人工智能、自动化、计算机科学或机器人学等相关专业背景,熟悉Matlab编程语言和基本的强化学习概念,从事无人机控制、智能导航、路径规划算法研究的研究生、科研人员及工程技术人员。; 使用场景及目标:①应用于城市峡谷、灾害现场等复杂动态三维场景中无人机的自主飞行与紧急避障;②作为强化学习解决实际路径规划问题的教学实例,帮助理解Q-Learning的核心思想、状态-动作值函数更新过程及探索-利用权衡策略;③为后续研究更先进的深度强化学习算法(如DQN、PPO)在无人机控制中的应用奠定基础和提供对比基准。; 阅读建议:建议读者结合所提供的Matlab代码进行动手实践,通过调整学习率、折扣因子、探索率(ε-greedy)等超参数,观察其对算法收敛速度和最终路径规划质量的影响,并尝试修改环境复杂度(如增加障碍物密度或动态性)以评估算法的泛化能力。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值