技术博客
Node.js 实战:手把手构建简易RESTful CRUD笔记应用

Node.js 实战:手把手构建简易RESTful CRUD笔记应用

作者: 万维易源
2024-08-08
Node.jsCRUD API笔记应用RESTful
### 摘要 本文介绍了一款基于Node.js构建的简易笔记应用程序。该应用实现了RESTful风格的CRUD API,方便用户进行笔记的创建、读取、更新与删除操作。通过本文,读者可以了解到如何利用Node.js快速搭建一个功能完备且易于扩展的笔记管理系统。 ### 关键词 Node.js, CRUD API, 笔记应用, RESTful, 简易构建 ## 一、基础理论 ### 1.1 Node.js简介及其在开发中的应用 Node.js 是一种开源的跨平台JavaScript运行环境,它允许开发者使用JavaScript编写服务器端的应用程序。Node.js 的核心优势在于其非阻塞I/O模型和事件驱动架构,这使得它非常适合处理高并发的网络应用。Node.js 的出现极大地简化了前后端开发流程,因为开发者可以使用相同的语言(JavaScript)来编写客户端和服务器端代码,从而提高了开发效率。 在开发领域,Node.js 被广泛应用于构建各种类型的应用程序,包括但不限于Web应用、实时通信应用、数据密集型应用等。对于构建笔记应用程序而言,Node.js 提供了一个高效、灵活的开发框架,能够轻松地实现RESTful风格的API接口,满足用户对笔记的基本需求——创建(Create)、读取(Retrieve)、更新(Update)和删除(Delete)。 ### 1.2 RESTful API设计原则与实践 REST(Representational State Transfer)是一种软件架构风格,用于描述一种基于HTTP协议的网络应用程序的设计方式。RESTful API 设计的核心理念是将资源抽象为URI(Uniform Resource Identifier),并通过HTTP方法(GET、POST、PUT、DELETE等)来表示对这些资源的操作。 #### 设计原则 - **资源定位**:每个资源都应该有一个唯一的URL来标识。 - **无状态**:每次请求都应包含所有必要的信息,服务器不应存储任何客户端的状态信息。 - **缓存**:合理利用缓存机制可以减少网络延迟,提高响应速度。 - **分层系统**:允许中间组件缓存或修改请求/响应,以提高性能和安全性。 - **统一接口**:通过一组定义明确的方法(如GET、POST等)来操作资源。 #### 实践案例 为了构建一个简易的笔记应用程序,开发者可以遵循以下步骤来设计RESTful API: 1. **确定资源**:将“笔记”作为主要资源,每个笔记都有一个唯一的ID。 2. **定义URL结构**:例如,`/notes` 用于列出所有笔记,`/notes/:id` 用于获取特定笔记的信息。 3. **实现HTTP方法**: - `GET /notes`:获取所有笔记列表。 - `GET /notes/:id`:获取指定ID的笔记详情。 - `POST /notes`:创建新的笔记。 - `PUT /notes/:id`:更新指定ID的笔记。 - `DELETE /notes/:id`:删除指定ID的笔记。 通过这种方式,开发者可以构建出一个简洁、高效的RESTful API,为用户提供流畅的笔记管理体验。 ## 二、环境搭建与服务器配置 ### 2.1 项目初始化与依赖安装 在开始构建简易笔记应用程序之前,首先需要设置项目的基础结构并安装所需的依赖包。以下是详细的步骤: 1. **创建项目文件夹**:在本地计算机上创建一个新的文件夹,用于存放项目的全部文件。例如,可以命名为 `simple-notes-app`。 2. **初始化项目**:打开命令行工具,进入项目文件夹并运行 `npm init` 命令。按照提示填写相关信息,生成 `package.json` 文件。这一步骤将为项目创建一个基本的配置文件,记录项目的元数据以及后续安装的依赖包。 3. **安装Express框架**:Express 是一个基于 Node.js 的轻量级 Web 应用框架,非常适合用来构建 RESTful API。可以通过运行 `npm install express --save` 来安装 Express。这将把 Express 添加到 `package.json` 文件的依赖列表中。 4. **安装其他依赖**:为了更好地处理 HTTP 请求和响应,还需要安装一些额外的依赖包,比如 `body-parser` 用于解析请求体中的 JSON 数据。可以通过运行 `npm install body-parser --save` 安装 `body-parser`。 5. **安装数据库相关依赖**:如果选择使用 MongoDB 作为后端数据库,还需要安装 `mongoose` 这个库,它提供了面向对象的模型定义方式,使得与 MongoDB 的交互更加简单直观。可以通过运行 `npm install mongoose --save` 来安装 `mongoose`。 完成以上步骤后,项目的基础结构就已经搭建完毕,接下来就可以开始编写代码了。 ### 2.2 搭建基本的Node.js服务器 在搭建好项目的基础结构之后,下一步就是创建一个基本的 Node.js 服务器。以下是具体的步骤: 1. **创建主文件**:在项目根目录下创建一个名为 `server.js` 的文件,该文件将作为项目的入口点。 2. **引入依赖**:在 `server.js` 文件中,首先需要引入之前安装的依赖包。例如,可以使用以下代码引入 Express 和 body-parser: ```javascript const express = require('express'); const bodyParser = require('body-parser'); const app = express(); app.use(bodyParser.json()); ``` 3. **设置端口**:定义服务器监听的端口号。通常情况下,可以使用 `process.env.PORT` 或者默认值 `3000`。例如: ```javascript const port = process.env.PORT || 3000; ``` 4. **创建路由**:接下来,需要定义 RESTful API 的路由。例如,可以创建一个 `/notes` 的路由来处理笔记相关的请求。这里可以使用 Express 的路由功能来实现: ```javascript app.get('/notes', (req, res) => { // 返回所有笔记的列表 }); app.get('/notes/:id', (req, res) => { // 返回指定 ID 的笔记 }); app.post('/notes', (req, res) => { // 创建新的笔记 }); app.put('/notes/:id', (req, res) => { // 更新指定 ID 的笔记 }); app.delete('/notes/:id', (req, res) => { // 删除指定 ID 的笔记 }); ``` 5. **启动服务器**:最后,在 `server.js` 文件的末尾添加启动服务器的代码: ```javascript app.listen(port, () => { console.log(`Server is running on http://localhost:${port}`); }); ``` 至此,一个基本的 Node.js 服务器就搭建完成了。通过运行 `node server.js` 命令,即可启动服务器,并通过浏览器或者 Postman 等工具测试 API 是否正常工作。 ## 三、数据库与CRUD操作 ### 3.1 设计数据库模型 在构建笔记应用程序的过程中,设计合理的数据库模型至关重要。本节将详细介绍如何使用 Mongoose 库来定义笔记的数据模型,并确保数据结构符合应用的需求。 #### 3.1.1 定义笔记模型 为了实现笔记的 CRUD 操作,首先需要定义一个笔记模型。在 Node.js 项目中,可以使用 Mongoose 来定义模型。Mongoose 是一个强大的对象文档映射器(Object Data Mapping, ODM),它使得与 MongoDB 数据库的交互变得更加简单和直观。 下面是一个简单的笔记模型定义示例: ```javascript const mongoose = require('mongoose'); const Schema = mongoose.Schema; // 定义笔记模型 const NoteSchema = new Schema({ title: { type: String, required: true }, content: { type: String, required: true }, createdAt: { type: Date, default: Date.now }, updatedAt: { type: Date, default: Date.now } }); module.exports = mongoose.model('Note', NoteSchema); ``` 在这个模型中,我们定义了四个字段: - `title`: 字符串类型,表示笔记的标题,必须填写。 - `content`: 字符串类型,表示笔记的内容,同样必须填写。 - `createdAt`: 日期类型,默认值为当前时间,表示笔记创建的时间。 - `updatedAt`: 日期类型,默认值为当前时间,表示笔记最后一次更新的时间。 #### 3.1.2 验证规则 为了保证数据的一致性和完整性,可以在模型中加入验证规则。例如,可以要求标题和内容不能为空,确保每条笔记都有足够的信息。此外,还可以设置更复杂的验证规则,如限制标题长度等。 ```javascript const NoteSchema = new Schema({ title: { type: String, required: true, maxLength: 100 }, content: { type: String, required: true }, createdAt: { type: Date, default: Date.now }, updatedAt: { type: Date, default: Date.now } }); ``` 通过这种方式,可以确保所有存储在数据库中的笔记都符合预设的标准。 ### 3.2 连接数据库与 CRUD 操作实现 在定义好模型之后,接下来需要连接数据库,并实现 CRUD 操作的具体逻辑。 #### 3.2.1 连接数据库 在 Node.js 中,使用 Mongoose 连接到 MongoDB 数据库非常简单。只需要在项目启动时调用 `mongoose.connect()` 方法即可。 ```javascript const mongoose = require('mongoose'); mongoose.connect('mongodb://localhost/simple-notes-app', { useNewUrlParser: true, useUnifiedTopology: true }).then(() => { console.log('Connected to the database.'); }).catch((error) => { console.error('Error connecting to the database:', error); }); ``` 这段代码会尝试连接到本地 MongoDB 数据库中的 `simple-notes-app` 数据库。如果连接成功,将在控制台输出一条消息;如果连接失败,则会捕获错误并打印详细信息。 #### 3.2.2 实现 CRUD 操作 接下来,我们将实现笔记的 CRUD 操作。这些操作将通过 RESTful API 的形式暴露给前端应用。 - **创建笔记** (`POST /notes`) ```javascript app.post('/notes', async (req, res) => { try { const note = new Note(req.body); await note.save(); res.status(201).json(note); } catch (error) { res.status(400).json({ message: error.message }); } }); ``` - **获取所有笔记** (`GET /notes`) ```javascript app.get('/notes', async (req, res) => { try { const notes = await Note.find(); res.json(notes); } catch (error) { res.status(500).json({ message: error.message }); } }); ``` - **获取单个笔记** (`GET /notes/:id`) ```javascript app.get('/notes/:id', getNote, (req, res) => { res.json(res.note); }); ``` - **更新笔记** (`PUT /notes/:id`) ```javascript app.put('/notes/:id', getNote, async (req, res) => { if (req.body.title != null) { res.note.title = req.body.title; } if (req.body.content != null) { res.note.content = req.body.content; } try { const updatedNote = await res.note.save(); res.json(updatedNote); } catch (error) { res.status(400).json({ message: error.message }); } }); ``` - **删除笔记** (`DELETE /notes/:id`) ```javascript app.delete('/notes/:id', getNote, async (req, res) => { try { await res.note.remove(); res.json({ message: 'Deleted Note' }); } catch (error) { res.status(500).json({ message: error.message }); } }); ``` 其中,`getNote` 函数是一个中间件,用于根据请求中的 `id` 参数查找对应的笔记: ```javascript async function getNote(req, res, next) { let note; try { note = await Note.findById(req.params.id); if (note == null) { return res.status(404).json({ message: 'Cannot find note' }); } } catch (error) { return res.status(500).json({ message: error.message }); } res.note = note; next(); } ``` 通过上述代码,我们已经实现了笔记的 CRUD 操作。这些操作通过 RESTful API 的形式对外提供服务,使得前端应用可以轻松地与后端进行交互,实现完整的笔记管理功能。 ## 四、RESTful API构建与优化 ### 4.1 构建RESTful API的Routes 在构建RESTful API时,定义清晰的路由是非常重要的一步。这些路由不仅需要遵循RESTful的设计原则,还要确保能够高效地处理HTTP请求。接下来,我们将详细介绍如何为简易笔记应用程序构建RESTful API的Routes。 #### 4.1.1 定义路由模块 为了保持代码的整洁和可维护性,建议将路由相关的代码放在单独的模块中。这样不仅可以使主文件更加简洁,也便于后续的扩展和维护。 1. **创建路由文件**:在项目中创建一个名为 `routes/notes.js` 的文件,用于定义与笔记相关的路由。 2. **引入依赖**:在 `notes.js` 文件中,首先需要引入 Express 和之前定义的笔记模型。 ```javascript const express = require('express'); const router = express.Router(); const Note = require('../models/note'); ``` 3. **定义路由**:接下来,定义处理笔记的CRUD操作的路由。例如: ```javascript // 获取所有笔记 router.get('/', async (req, res) => { try { const notes = await Note.find(); res.json(notes); } catch (error) { res.status(500).json({ message: error.message }); } }); // 获取单个笔记 router.get('/:id', getNote, (req, res) => { res.json(res.note); }); // 创建新笔记 router.post('/', async (req, res) => { try { const note = new Note({ title: req.body.title, content: req.body.content }); const newNote = await note.save(); res.status(201).json(newNote); } catch (error) { res.status(400).json({ message: error.message }); } }); // 更新笔记 router.put('/:id', getNote, async (req, res) => { if (req.body.title != null) { res.note.title = req.body.title; } if (req.body.content != null) { res.note.content = req.body.content; } try { const updatedNote = await res.note.save(); res.json(updatedNote); } catch (error) { res.status(400).json({ message: error.message }); } }); // 删除笔记 router.delete('/:id', getNote, async (req, res) => { try { await res.note.remove(); res.json({ message: 'Deleted Note' }); } catch (error) { res.status(500).json({ message: error.message }); } }); ``` 4. **导出路由**:最后,需要将定义好的路由导出,以便在主文件中使用。 ```javascript module.exports = router; ``` #### 4.1.2 注册路由 在主文件 `server.js` 中,需要注册之前定义的路由模块。例如: ```javascript const notesRouter = require('./routes/notes'); app.use('/notes', notesRouter); ``` 通过这种方式,所有的笔记相关请求都将被转发到 `notesRouter` 中处理。 ### 4.2 中间件的使用与异常处理 中间件是Express的一个重要特性,它可以用来处理请求和响应的生命周期,执行诸如日志记录、身份验证、错误处理等任务。在构建RESTful API时,合理使用中间件可以极大地提高代码的健壮性和可维护性。 #### 4.2.1 使用中间件 1. **日志记录**:可以使用 `morgan` 这样的中间件来记录请求的日志信息。 ```javascript const morgan = require('morgan'); app.use(morgan('tiny')); ``` 2. **错误处理**:定义一个全局的错误处理中间件,用于捕捉未处理的异常。 ```javascript app.use((err, req, res, next) => { console.error(err.stack); res.status(500).send('Something broke!'); }); ``` 3. **自定义中间件**:例如,之前提到的 `getNote` 中间件就是一个很好的例子,它用于根据请求中的 `id` 参数查找对应的笔记。 ```javascript async function getNote(req, res, next) { let note; try { note = await Note.findById(req.params.id); if (note == null) { return res.status(404).json({ message: 'Cannot find note' }); } } catch (error) { return res.status(500).json({ message: error.message }); } res.note = note; next(); } ``` #### 4.2.2 异常处理 在处理HTTP请求时,可能会遇到各种各样的异常情况。为了确保API的稳定性和可靠性,需要对这些异常情况进行妥善处理。 1. **捕获异步函数中的错误**:在异步函数中使用 `try...catch` 结构来捕获可能发生的错误。 2. **统一错误响应格式**:定义一个统一的错误响应格式,使得前端可以更容易地处理后端返回的错误信息。 3. **日志记录**:记录错误信息,有助于后续的问题排查和修复。 通过上述步骤,我们已经成功构建了一个简易的笔记应用程序,实现了RESTful风格的CRUD API。这些API不仅遵循了RESTful的设计原则,还通过中间件和异常处理机制增强了系统的健壮性和可用性。 ## 五、前端交互与性能提升 ### 5.1 前端交互与数据展示 在构建完后端 RESTful API 后,接下来需要关注的是前端应用如何与这些 API 进行交互,以及如何优雅地展示数据。这一部分将详细介绍如何使用前端技术栈(如 HTML、CSS 和 JavaScript)来实现用户界面,并通过 AJAX 技术与后端 API 进行数据交换。 #### 5.1.1 构建用户界面 1. **HTML 结构**:首先,需要创建一个简洁的 HTML 页面结构,用于显示笔记列表和表单输入框。 ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Simple Notes App</title> <link rel="stylesheet" href="styles.css"> </head> <body> <h1>Notes App</h1> <form id="note-form"> <input type="text" id="note-title" placeholder="Title"> <textarea id="note-content" placeholder="Content"></textarea> <button type="submit">Create Note</button> </form> <ul id="notes-list"></ul> <script src="app.js"></script> </body> </html> ``` 2. **CSS 样式**:使用 CSS 来美化页面,使其看起来更加友好和专业。 ```css body { font-family: Arial, sans-serif; max-width: 800px; margin: auto; padding: 20px; } form { margin-bottom: 20px; } input, textarea { width: 100%; padding: 10px; margin-bottom: 10px; } button { background-color: #4CAF50; color: white; padding: 10px 20px; border: none; cursor: pointer; } ul { list-style-type: none; padding: 0; } li { margin-bottom: 10px; padding: 10px; border: 1px solid #ccc; } ``` 3. **JavaScript 交互**:使用 JavaScript 来处理表单提交事件,并通过 AJAX 与后端 API 进行数据交换。 ```javascript document.getElementById('note-form').addEventListener('submit', async (event) => { event.preventDefault(); const title = document.getElementById('note-title').value; const content = document.getElementById('note-content').value; const response = await fetch('/notes', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title, content }) }); if (response.ok) { location.reload(); // 刷新页面以显示新笔记 } else { alert('Failed to create note'); } }); async function loadNotes() { const response = await fetch('/notes'); const data = await response.json(); const notesList = document.getElementById('notes-list'); notesList.innerHTML = ''; data.forEach(note => { const li = document.createElement('li'); li.textContent = `${note.title}: ${note.content}`; notesList.appendChild(li); }); } loadNotes(); ``` 通过上述代码,用户可以通过简单的表单输入笔记的标题和内容,并通过点击按钮来创建新的笔记。同时,页面会自动加载现有的笔记列表,并动态更新。 #### 5.1.2 数据展示 为了展示笔记列表,可以使用 JavaScript 的 `fetch` API 来从后端获取数据,并将其渲染到页面上。具体实现如下: 1. **加载笔记列表**:在页面加载时,自动调用 `loadNotes` 函数来获取并显示笔记列表。 2. **动态更新**:当用户创建或删除笔记后,可以重新加载笔记列表,以确保页面上的数据是最新的。 通过这种方式,用户可以直观地看到自己创建的所有笔记,并且能够方便地进行管理。 ### 5.2 安全性考虑与性能优化 在构建 RESTful API 时,除了关注功能实现外,还需要考虑到安全性和性能优化等方面,以确保应用的稳定性和可靠性。 #### 5.2.1 安全性考虑 1. **输入验证**:在前端和后端都需要对用户的输入进行验证,防止恶意数据注入。 2. **身份验证**:对于敏感操作(如创建、更新和删除笔记),应该要求用户登录并验证身份。 3. **加密传输**:使用 HTTPS 协议来加密数据传输,保护用户数据的安全。 4. **权限控制**:确保只有笔记的所有者才能对其进行修改或删除。 5. **日志记录**:记录关键操作的日志,以便于问题排查和审计。 #### 5.2.2 性能优化 1. **缓存策略**:合理利用缓存机制,减少不必要的数据库查询。 2. **分页处理**:对于大量数据的请求,采用分页的方式来减少单次请求的数据量。 3. **压缩传输**:启用 gzip 压缩,减小数据传输的体积。 4. **异步处理**:对于耗时较长的操作,采用异步处理的方式,避免阻塞主线程。 5. **数据库索引**:为经常查询的字段创建索引,加快查询速度。 通过综合考虑安全性与性能优化,可以确保简易笔记应用程序不仅功能完善,而且稳定可靠,为用户提供良好的使用体验。 ## 六、测试与部署 ### 6.1 测试RESTful API的有效性 在构建完简易笔记应用程序的RESTful API之后,确保其功能正确性和稳定性至关重要。测试不仅是验证API是否按预期工作的过程,也是发现潜在问题并及时解决的关键环节。本节将详细介绍如何有效地测试RESTful API。 #### 6.1.1 使用Postman进行手动测试 1. **安装Postman**:Postman是一款流行的API测试工具,可以帮助开发者轻松地发送各种类型的HTTP请求,并查看响应结果。 2. **测试GET请求**:使用Postman发送GET请求到`/notes`和`/notes/:id`,检查是否能够正确地获取笔记列表和单个笔记的详细信息。 3. **测试POST请求**:发送POST请求到`/notes`,包含有效的JSON数据(例如`{ "title": "My First Note", "content": "This is the content of my first note." }`),验证是否能够成功创建新的笔记。 4. **测试PUT请求**:找到一个已存在的笔记ID,发送PUT请求到`/notes/:id`,更新笔记的标题或内容,确认更新是否生效。 5. **测试DELETE请求**:选择一个笔记ID,发送DELETE请求到`/notes/:id`,检查笔记是否被成功删除。 通过这种方式,可以全面地测试RESTful API的功能,确保每个端点都能正常工作。 #### 6.1.2 自动化测试 除了手动测试之外,自动化测试也是确保API质量的重要手段。可以使用如Jest这样的JavaScript测试框架来编写单元测试和集成测试。 1. **安装Jest**:通过运行`npm install jest --save-dev`来安装Jest。 2. **编写测试用例**:针对每个API端点编写测试用例,覆盖常见的使用场景和边界条件。 ```javascript test('should create a new note', async () => { const response = await request(app) .post('/notes') .send({ title: 'Test Note', content: 'This is a test note.' }); expect(response.statusCode).toBe(201); expect(response.body.title).toBe('Test Note'); }); test('should update an existing note', async () => { const note = await Note.create({ title: 'Test Note', content: 'Initial content.' }); const response = await request(app) .put(`/notes/${note._id}`) .send({ title: 'Updated Title', content: 'Updated content.' }); expect(response.statusCode).toBe(200); expect(response.body.title).toBe('Updated Title'); }); test('should delete a note', async () => { const note = await Note.create({ title: 'Test Note', content: 'Initial content.' }); const response = await request(app).delete(`/notes/${note._id}`); expect(response.statusCode).toBe(200); expect(response.body.message).toBe('Deleted Note'); }); ``` 3. **运行测试**:通过运行`npm test`来执行测试用例。 通过自动化测试,可以确保API在每次更改后仍然能够正常工作,并且有助于发现潜在的回归问题。 ### 6.2 持续集成与部署策略 随着项目的不断发展,持续集成(CI)和持续部署(CD)成为确保代码质量和快速迭代的关键。本节将介绍如何设置CI/CD流程,以自动化测试和部署简易笔记应用程序。 #### 6.2.1 设置持续集成 1. **选择CI工具**:可以选择如GitHub Actions、GitLab CI/CD或Jenkins等工具来实现持续集成。 2. **编写CI脚本**:根据所选的CI工具,编写相应的YAML文件来定义构建、测试和部署的步骤。 例如,在GitHub Actions中,可以创建一个`.github/workflows/ci.yml`文件: ```yaml name: CI on: push: branches: [ main ] pull_request: branches: [ main ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Use Node.js uses: actions/setup-node@v2 with: node-version: '14.x' - run: npm ci - run: npm run test ``` 3. **触发CI流程**:每当有新的代码提交到仓库时,CI工具会自动运行测试脚本。 #### 6.2.2 自动化部署 1. **选择部署目标**:可以选择Heroku、AWS、Azure等云服务提供商来托管应用。 2. **配置部署脚本**:在CI工具中配置部署脚本,将构建好的应用自动部署到生产环境中。 例如,在GitHub Actions中,可以继续扩展之前的YAML文件: ```yaml name: CI/CD on: push: branches: [ main ] pull_request: branches: [ main ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Use Node.js uses: actions/setup-node@v2 with: node-version: '14.x' - run: npm ci - run: npm run test - name: Deploy to Heroku uses: akhileshns/heroku-deploy@v3.12.12 with: heroku_api_key: ${{secrets.HEROKU_API_KEY}} heroku_app_name: "your-app-name" heroku_email: "your-email@example.com" ``` 通过这种方式,可以确保每次代码变更后,应用都能够自动地构建、测试并部署到生产环境中,大大提高了开发效率和应用的稳定性。 ## 七、总结 本文详细介绍了如何使用Node.js构建一个简易的笔记应用程序,并实现了RESTful风格的CRUD API。从理论基础到实际操作,涵盖了Node.js和RESTful API的设计原则,再到具体的环境搭建、服务器配置、数据库设计与实现、前端交互、安全性与性能优化,直至最后的测试与部署策略。通过本文的学习,读者可以掌握构建类似应用所需的关键技术和最佳实践,为日后开发更为复杂的应用奠定坚实的基础。
加载文章中...