06 前台与后台交互——2代码实现

一、 前言

Express 是 Node.js 的一个流行框架,用于构建 Web 应用程序和 API。它提供了许多功能强大的工具和中间件,使得开发者能够轻松地创建高性能的服务器端应用。然而,实现一个完整的 Web 应用一个重要的方面是前台(客户端)与后台(服务器端)之间的交互。在这篇文章中,我们将探讨如何在 Express 框架下实现有效的前后端交互。

二、简介

实现前台与后台的交互通常要处理以后台路由、前台交互等内容:

1. 后台路由

在 Express 中,后台路由是定义了端点(endpoint)和处理函数的部分。这些路由负责处理来自客户端的请求,并返回相应的数据或执行相应的操作。下面是一个简单的后台路由示例:

javascript 复制代码
const express = require('express');
const app = express();

// 定义一个 GET 请求的路由
app.get('/api/data', (req, res) => {
    // 处理 GET 请求
    const data = { message: '这是来自服务器的数据' };
    res.json(data);
});

// 启动服务器
app.listen(3000, () => {
    console.log('服务器已启动,端口号: 3000');
});

在上面的例子中,我们定义了一个 GET 请求的路由 /api/data,当客户端发送 GET 请求到这个路由时,服务器将返回一个 JSON 格式的数据。

2. 前台交互

要在前台与后台进行交互,通常使用 JavaScript 来发送请求并处理响应。在浏览器端,可以使用 Fetch API 或 XMLHttpRequest 对象来实现。以下是一个使用 Fetch API 的简单示例:

javascript 复制代码
// 发送 GET 请求到服务器
fetch('/api/data')
    .then(response => {
        if (!response.ok) {
            throw new Error('网络请求失败');
        }
        return response.json();
    })
    .then(data => {
        // 处理从服务器返回的数据
        console.log(data);
    })
    .catch(error => {
        console.error('发生错误:', error);
    });

在这个示例中,我们向 /api/data 路由发送了一个 GET 请求,并在收到响应后处理返回的数据。

三、真实案例

我们以视频网站的【分类编辑】的页面来介来看看在项目中如何实现前台与后台的异步交互;这里涉及到的重点内容有:

  • 前台如何通过后台读、写数据库中的数据
  • 前台页面中的控件事件挂接

3.1前台通过后台获取数据库中的数据

在实现前台通过后台获取数据库中的数据时,首先需要后台提供相应的数据获取方法供前台调用。在较为优良的实践中,常见的做法是将数据表进行封装,形成模型。这种模型封装可以提供一种结构化的方式来管理和操作数据库中的数据,使得后台的数据访问更加可维护和可扩展。我们以Mysql数据库为例介绍后台的方法,其中数据库链接部分请参阅前面文章,这里只在末尾给出代码以方便调试;

  • 数据表创建代码如下
r 复制代码
CREATE TABLE `video_site`.`video_categories2` (
  `ID` INT NOT NULL AUTO_INCREMENT,
  `CategoriesName` VARCHAR(255) NOT NULL,
  PRIMARY KEY (`ID`, `CategoriesName`),
  UNIQUE INDEX `ID_UNIQUE` (`ID` ASC));

3.1.1 在NodeJs中提供数据表访问代码

创建Model\videoCategoryModel.js做为视频分类的模型封装,提供对数据表的增、删、改、查功能,其代码如下:

javascript 复制代码
const db = require('./dbUtils');  
  
class VideoCategory {  
  constructor(data) {  
    this.id = data.ID;  
    this.categoryName = data.CategoriesName;  
  }  
  
  // 根据分类ID查询分类  
  static async findById(id) {  
    try {  
      const results = await db.query.withoutConnection('SELECT * FROM video_categories WHERE ID = ?', [id]);  
      if (results.length > 0) {  
        const categoryData = results[0];  
        return new VideoCategory({  
          id: categoryData.ID,  
          categoryName: categoryData.CategoriesName  
        });  
      }  
    } catch (err) {  
      console.error('Error fetching video category by ID:', err);  
      throw err;  
    }  
  }  
  
  // 静态方法,用于查询所有分类  
  static async findAll() {  
    try {  
      const results = await db.query.withoutConnection('SELECT * FROM video_categories');  
      return results.map(categoryData => new VideoCategory(categoryData));  
    } catch (err) {  
      console.error('Error fetching all video categories:', err);  
      throw err;  
    }  
  }  
  
  // 实例方法,用于保存分类到数据库  
  async save() {  
    try {  
      const results = await db.query.withoutConnection(  
        'INSERT INTO video_categories (CategoriesName) VALUES (?)',  
        [this.categoryName]  
      );  
      this.id = results.insertId;  
    } catch (err) {  
      console.error('Error saving video category:', err);  
      throw err;  
    }  
  }  
}  
  
module.exports = VideoCategory;

3.1.2 提供路由

增加routes\categories.js文件作为视频分类的路由文件,提供数据表内容访问API接口

javascript 复制代码
const express = require('express');
const router = express.Router();
const CategoryModel = require('../model/CategoryModel');

// 渲染分类维护页面,当用户切换到视频分类页面时显示分类编辑页面
router.get('/admin/categories', async (req, res) => {
    try {
        const categories = await CategoryModel.getAllCategories();
        res.render('categories', { categories });
    } catch (error) {
        console.error('Error rendering categories page:', error);
        res.status(500).send('Internal Server Error');
    }
});

// API调用获取所有分类
router.get('/admin/categories/api/Categories', async (req, res) => {
    try {
        const categories = await CategoryModel.getAllCategories();
        res.json(categories);
    } catch (error) {
        console.error('Error fetching categories:', error);
        res.status(500).json({ error: 'Internal Server Error' });
    }
});

// API调用添加分类
router.post('/admin/categories/api/Categories', async (req, res) => {
    try {
        const { CategoriesName } = req.body;
        await CategoryModel.addCategory(CategoriesName);
        res.status(201).json({ message: 'Category added successfully' });
    } catch (error) {
        console.error('Error adding category:', error);
        res.status(500).json({ error: 'Internal Server Error' });
    }
});

// API调用更新分类
router.put('/admin/categories/api/Categories/:id', async (req, res) => {
    try {
        const { id } = req.params;
        const { CategoriesName } = req.body;
        await CategoryModel.updateCategory(id, CategoriesName);
        res.json({ message: 'Category updated successfully' });
    } catch (error) {
        console.error('Error updating category:', error);
        res.status(500).json({ error: 'Internal Server Error' });
    }
});

// API调用删除分类
router.delete('/admin/categories/api/Categories/:id', async (req, res) => {
    try {
        const { id } = req.params;
        await CategoryModel.deleteCategory(id);
        res.json({ message: 'Category deleted successfully' });
    } catch (error) {
        console.error('Error deleting category:', error);
        res.status(500).json({ error: 'Internal Server Error' });
    }
});

module.exports = router;

3.1.3 将路由添加到由表中

App.js文件中添加以下代码

php 复制代码
//......其它代码
const categoriesRoutes = require('./routes/categories');
//......其它代码
app.use('/', categoriesRoutes); // 挂载分类维护页面的路由
//......其它代码

3.2前台通过后台获取数据库中的数据

3.2.1 前台页面事件说明

在页面中提供一个文本框用于增加分类时输入分类名称,一个【增加】按钮用于增加分类,以及一个表格用于显示分类数据;数据修改及删除通过表格完成;表格显示如下

ID 分类名称 操作
1 初中 删除
2 高中 删除

增加views\categories.ejs页面,并添加相应的事件,下面就所需要事件展开说明 1 document.addEventListener 事件说明请参阅附2 2 "DOMContentLoaded" 页面加载事件,内部主要实现:

  • 挂接submit事件,用于添加新分类;
  • 定义window.deleteCategory删除分类方法,用于挂接在表格中的删除按钮单击事件;
  • 定义fetchCategories()用于加载所有分类; 以上三个方法放到【"DOMContentLoaded"】是为保证事件挂接成功,避免某些情况下因页面未完全加载完毕所挂接的控件未定义从而造成的挂接失败;

3 function createTableRow(category) 用于创建表格的方法; 4 async function validateAndUpdateCategory(element, id) 用于在表格中修改分类名称响应函数; 在创建表格时 <td contenteditable="true" oninput="validateAndUpdateCategory(this, ${category.ID})">${category.CategoriesName}</td>时,指定了单元修输入后的响应函数; 5 async function updateCategory(id, newName) 用于修改分类名称后调用相应的后台API

3.2.1 前台页面代码细节说明

  • categoryForm.addEventListener('submit'页面提交事件中event.preventDefault()用于阻止页面的默认提交事件,这样我们才能机会对用户输入内容进行验证,并根据验证结果决定处理动作;
  • <td>单元格默认是不能编辑的,通过设置contenteditable="true"至使单元可以编辑。
  • 在增加分类后,这里直接重新加载了页面,若不想重新加载整个页面,可参见单元格编辑响应事件修改此处代码;

3.3 views\categories.ejs 代码如下:

xml 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>课程分类管理</title>
<style>
  table {
    border-collapse: collapse;
    width: 50%;
    margin: 20px auto;
  }
  th, td {
    border: 1px solid #ddd;
    padding: 8px;
    text-align: left;
    cursor: pointer; /* Add cursor pointer for better UX */
  }
  th {
    background-color: #f2f2f2;
  }
  form {
    margin: 20px auto;
    text-align: center;
  }
  .tooltip {
    position: relative;
    display: inline-block;
    cursor: pointer;
  }
  .tooltip .tooltiptext {
    visibility: hidden;
    width: 300px;
    background-color: black;
    color: #fff;
    text-align: center;
    border-radius: 6px;
    padding: 5px 0;
    position: absolute;
    z-index: 1;
    bottom: 125%;
    left: 50%;
    margin-left: -60px;
    opacity: 0;
    transition: opacity 0.3s;
  }
  .tooltip:hover .tooltiptext {
    visibility: visible;
    opacity: 1;
  }
</style>
</head>
<body>
  <form id="categoryForm">
    <label for="categoryName">分类名称:</label>
    <input type="text" id="categoryName" name="categoryName">
    <button type="submit">添加分类</button>
  </form>
  <table id="categoryTable">
    <thead>
      <tr>
        <th width="50px">ID</th>
        <th>分类名称<span class="tooltip"><sup>?</sup><span class="tooltiptext">分类名称不能包含以下字符:\ / : * ? " < > |,长度不能超过255个字符</span></span></th>
        <th>操作</th>
      </tr>
    </thead>
    <tbody>
    </tbody>
  </table>
<script>
document.addEventListener("DOMContentLoaded", async function() {
    const categoryForm = document.getElementById("categoryForm");
    
    // Event listener for category form submission
    categoryForm.addEventListener('submit', async function(event) {
        event.preventDefault();
        const categoryNameInput = document.getElementById('categoryName');
        const categoryName = categoryNameInput.value.trim();
        // Validate category name
        if (!validateCategoryName(categoryName)) {
            alert('分类名称中不能包含以下字符:\\ / : * ? " < > |,长度不能超过255个字符');
            return;
        }
        try {
            const response = await fetch('/admin/categories/api/categories', {
                method: 'POST',
                headers: {
                'Content-Type': 'application/json'
                },
                body: JSON.stringify({ CategoriesName: categoryName })
            });
            if (response.ok) {
                fetchCategories();
                categoryNameInput.value = '';
            } else {
                console.error('Error adding category:', response.statusText);
            }
        } catch (error) {
            console.error('Error adding category:', error);
        }
    });

    // 删除分类
    window.deleteCategory = async function(id) {
        try {
            const response = await fetch(`/admin/categories/api/categories/${id}`, {
              method: 'DELETE'
        });
        if (response.ok) {
            fetchCategories();
        } else {
            console.error('Error deleting category:', response.statusText);
        }
        } catch (error) {
            console.error('Error deleting category:', error);
        }
    };

    // 在页面中创建分类表格
    async function fetchCategories() {
        try {
            const response = await fetch('/admin/categories/api/categories');
            const categories = await response.json();
            const categoryTableBody = document.querySelector('#categoryTable tbody');
            categoryTableBody.innerHTML = '';
            categories.forEach(category => {
                categoryTableBody.appendChild(createTableRow(category));
            });
        } catch (error) {
            console.error('Error fetching categories:', error);
        }
    }
    // 在页面加载完毕后加载分类名称
    fetchCategories();
});

// 分类名称校验函数
function validateCategoryName(name) {
    const regex = /^[^\\/:\*\?"<>\|]{1,255}$/; // Windows filename constraints
    return regex.test(name);
}

// 创建分类维护表格
function createTableRow(category) {
    const row = document.createElement('tr');
    row.innerHTML = `
        <td>${category.ID}</td>
        <td contenteditable="true" oninput="validateAndUpdateCategory(this, ${category.ID})">${category.CategoriesName}</td>
        <td>
            <button onclick="deleteCategory(${category.ID})">删除</button>
        </td>`;
    return row;
}

// 校验输入的分类名称,校验成功后调用后updateCategory函数修改分类名称
async function validateAndUpdateCategory(element, id) {
    const newName = element.innerText.trim();
    if (!validateCategoryName(newName)) {
        // Show tooltip or other validation indication
        alert('分类名称中不能包含以下字符:\\ / : * ? " < > |,长度不能超过255个字符');
        // Restore the original value
        element.textContent = element.dataset.originalValue || '';
    } else {
        // Update the original value
        element.dataset.originalValue = newName;
        // Update the category
        await updateCategory(id, newName);
    }
}

// 修改分类名称
async function updateCategory(id, newName) {
    try {
        const response = await fetch(`/admin/categories/api/updateCategories/${id}`, {
            method: 'PUT',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({ CategoriesName: newName })
        });
        if (!response.ok) {
            console.error('Error updating category:', response.statusText);
        }
    } catch (error) {
        console.error('Error updating category:', error);
    }
}
</script>
    
</body>
</html>

4. 结论

通过 Express 框架,我们可以轻松地实现前后端之间的数据交互。在后台,通过定义路由来处理来自客户端的请求;在前台,通过 JavaScript 来发送请求并处理响应。这种前后端交互的方式使得我们能够构建功能丰富、动态的 Web 应用程序。

1 数据库链接代码

文件名Model\dbUtils.js,代码如下

javascript 复制代码
const mysql = require('mysql');

// 创建数据库连接池
const pool = mysql.createPool({
  connectionLimit: 10,
  host: 'localhost',
  user: '数据库用户名',
  password: '数据库密码',
  database: 'video_site',   //使用的数据库名
  debug: false // 开启调试模式,会输出详细的 SQL 执行日志
});

// 获取数据库连接的方法
const getDBConnection = () => {
  return new Promise((resolve, reject) => {
    pool.getConnection((error, connection) => {
      if (error) {
        reject(error);
      } else {
        resolve(connection);
      }
    });
  });
};

// 执行查询的方法(重载1:带 connection 参数)
const query = {
  // 当调用者已经拥有连接时使用的查询方法
  withConnection: (connection, sql, params) => {
    return new Promise((resolve, reject) => {
      connection.query(sql, params, (error, results, fields) => {
        if (error) {
          reject(error);
        } else {
          resolve(results);
        }
      });
    });
  },

  // 当调用者没有连接时使用的查询方法,这个方法会负责获取和释放连接
  withoutConnection: async (sql, params) => {
    let connection;
    try {
      connection = await getDBConnection();
      const results = await query.withConnection(connection, sql, params);
      return results;
    } catch (error) {
      throw error;
    } finally {
      if (connection) {
        connection.release();
      }
    }
  }
};

module.exports = {
  getDBConnection,
  query
};

2 document.addEventListener说明

document.addEventListener 是 JavaScript 中用于添加事件监听器的方法之一。它允许开发者在特定的文档对象上监听各种类型的事件,比如鼠标点击、键盘按下、页面加载完成等,以便在事件发生时执行相应的操作。

使用方法

javascript 复制代码
document.addEventListener(event, function, useCapture);
  • event: 要监听的事件类型,比如 "click"、"keydown"、"load" 等。
  • function: 事件发生时要执行的函数,也称为事件处理程序。
  • useCapture(可选): 一个布尔值,表示事件是在捕获阶段(true)还是冒泡阶段(false)触发事件处理程序。默认为 false

示例

javascript 复制代码
document.addEventListener('click', function(event) {
    console.log('点击了文档');
});

document.addEventListener('keydown', function(event) {
    console.log('按下了键盘键:', event.key);
});

在这个示例中,我们分别监听了文档的点击事件和键盘按键事件。当用户点击文档时,会在控制台输出 "点击了文档";当用户按下键盘时,会在控制台输出相应的按键值。

优势

  • 多事件处理 : 可以同时监听多个不同类型的事件,而不需要像传统的 onclickonkeydown 一样,将事件处理程序直接赋值给特定的属性。
  • 灵活性: 可以使用匿名函数或命名函数作为事件处理程序,使代码更加模块化和可维护。
  • 事件委托: 可以利用事件冒泡机制,在父元素上添加一个事件监听器,来代理处理子元素的事件,提高性能和代码简洁度。

总之,document.addEventListener 是 JavaScript 中一种强大的事件处理机制,它为开发者提供了一种灵活、高效的方式来处理各种类型的事件。

相关推荐
zopple7 小时前
常见的 Spring 项目目录结构
java·后端·spring
子兮曰8 小时前
Bun v1.3.11 官方更新全整理:新增功能、关键修复与升级验证
javascript·node.js·bun
cjy0001119 小时前
springboot的 nacos 配置获取不到导致启动失败及日志不输出问题
java·spring boot·后端
小江的记录本9 小时前
【事务】Spring Framework核心——事务管理:ACID特性、隔离级别、传播行为、@Transactional底层原理、失效场景
java·数据库·分布式·后端·sql·spring·面试
sheji341610 小时前
【开题答辩全过程】以 基于springboot的校园失物招领系统为例,包含答辩的问题和答案
java·spring boot·后端
程序员cxuan10 小时前
人麻了,谁把我 ssh 干没了
人工智能·后端·程序员
wuyikeer11 小时前
Spring Framework 中文官方文档
java·后端·spring
Victor35611 小时前
MongoDB(61)如何避免大文档带来的性能问题?
后端
Victor35611 小时前
MongoDB(62)如何避免锁定问题?
后端
wuyikeer12 小时前
Spring BOOT 启动参数
java·spring boot·后端