基于JSP内置对象的服务器端表单验证(含动态粒子背景与模块化设计)

基于JSP内置对象的服务器端表单验证(含动态粒子背景与模块化设计)

一、引言

本博客源于《Web编程技术》课程实验五——“基于JSP内置对象的服务器端表单验证”。实验要求在服务器端利用JSP内置对象完成输入非空及长度校验,并根据验证结果返回提示、清空输入框并聚焦。我们在实现基本功能的基础上,进一步将项目拆分为13个高内聚低耦合的文件,并加入Canvas动态粒子背景与交互效果。本文将完整分享项目结构、核心代码及部署过程,所有代码均来源于实验报告并包含详细注释。


二、目录


1. 实验要求

编写包含表单的HTML文档。表单中可以输入任意字符(单行文本输入框)。要求能判断输入不能为空且字符必须为5个,输入完毕单击“判断”(提交输入按钮)按钮,发送到服务器端完成审查验证,如果用户的输入不符合要求则返回具体的提示信息,同时清空文本框内容并激活文本框输入焦点。如果正确返回输入格式正确的提示信息。

要求:服务器端基于JSP内置对象实现对客户端输入信息的验证。


2. 项目目录结构与设计思想

项目根目录为 task5,其下包含前端界面 JSP 文件、服务器端验证 JSP 文件、公共样式文件夹 css、前端交互脚本文件夹 js,以及用于复用的历史记录片段 history.jsp。所有 JSP 文件统一采用 UTF-8 编码,使用 requestsession 等内置对象完成验证与数据传递。样式和脚本均拆分为多个小文件,每个文件只承担一至两个功能,实现高度模块化。项目根目录 task5 结构如下:

task5/
├── index.jsp                # 表单验证主界面
├── validate.jsp             # 服务器端验证逻辑
├── history.jsp              # 历史记录展示片段
├── css/
│   ├── base.css             # 重置样式、页面背景与整体居中
│   ├── glass.css            # 玻璃拟态容器与标题样式
│   ├── form.css             # 表单输入框、按钮、字符计数器样式
│   └── message.css          # 提示消息和历史记录显示样式
└── js/
    ├── canvas-setup.js      # Canvas 初始化与自适应
    ├── mouse-tracker.js     # 鼠标位置追踪
    ├── particle-class.js    # 粒子类定义(移动、边界反弹、绘制)
    ├── collision.js         # 粒子间碰撞检测与响应
    ├── renderer.js          # 动画循环、鼠标排斥、连线绘制
    ├── counter.js           # 字数实时计数与提示
    └── page-init.js         # 页面加载初始化与重置按钮事件

设计思想
背景使用 Canvas 实现大粒子动态碰撞与鼠标交互效果,深色渐变使表单清晰可见。功能上严格遵循实验要求:输入框仅允许任意字符,服务器端通过 request 获取参数并判断是否为空、去除前后空格后长度是否恰好为 5,验证结果通过 RequestDispatcher 转发回 index.jsp,若验证失败则清空输入框并自动聚焦。字数计数器提供“字数过少/过多/刚好5个”的实时反馈,验证成功的输入会被存入 session 并在页面底部展示最近 5 条历史记录,同时提供“重置历史”按钮。整个项目代码被合理分摊至 13 个文件,每个文件功能单一、可读性强。


3. 全局样式设计

样式部分拆分为 4 个独立的 CSS 文件,分别负责页面基础、容器风格、表单组件和提示消息。

3.1 base.css – 重置与背景居中

base.css 完成浏览器默认样式的重置,将 htmlbody 设为全屏无滚动条,背景使用深色渐变,并设置 Flexbox 全局居中,Canvas 粒子画布置于底层。具体代码如下:

/* ========== 全局重置:把所有标签的默认间距清零 ========== */
* {
    margin: 0;                     /* 去掉所有元素自带的外边距 */
    padding: 0;                    /* 去掉所有元素自带的内边距 */
    box-sizing: border-box;        /* 宽高计算包含边框和内边距,布局更可控 */
}

/* ========== 基础容器:让页面撑满窗口、禁止滚动 ========== */
html, body {
    width: 100%;                   /* 宽度占满整个浏览器可视区域 */
    height: 100%;                  /* 高度占满整个浏览器可视区域 */
    overflow: hidden;              /* 内容超出窗口时直接隐藏,不出现滚动条 */
    font-family: "Microsoft YaHei", "PingFang SC", sans-serif; /* 优先使用微软雅黑、苹方,最后回退到无衬线字体 */
}

/* ========== 页面主体:让内容在屏幕正中间显示 ========== */
body {
    display: flex;                 /* 启用弹性盒子,方便子元素居中 */
    justify-content: center;       /* 水平方向居中 */
    align-items: center;           /* 垂直方向居中 */
    min-height: 100vh;             /* 最小高度为窗口高度,保证短内容也能垂直居中 */
    /* 深色渐变背景,从左上到右下,由深紫过渡到蓝紫再过渡到暗紫,整体偏暗不刺眼 */
    background: linear-gradient(135deg, #0f0c29, #302b63, #24243e);
}

/* ========== 粒子画布:固定在背后,作为装饰背景 ========== */
#particleCanvas {
    position: fixed;               /* 固定定位,不随页面滚动,始终贴在窗口上 */
    top: 0;                        /* 顶部对齐窗口上边缘 */
    left: 0;                       /* 左侧对齐窗口左边缘 */
    z-index: 0;                    /* 层级设为 0,让其他内容盖在它上面,只做背景装饰 */
}

3.2 glass.css – 玻璃拟态容器

glass.css 定义 .glass-container 的半透明玻璃拟态效果,包括圆角、边框高光和阴影,内部标题使用 text-shadow 发光。具体代码如下:

/* ========== 玻璃拟态容器 & 标题 ========== */
.glass-container {
    position: relative;            /* 打开定位,好让后面的 z-index 生效 */
    z-index: 10;                   /* 把容器抬到 canvas 背景上面,免得被盖住 */
    width: 90vw;                   /* 宽度占屏幕的 90%,手机电脑都能自适应 */
    max-width: 500px;              /* 但最宽不超过 500px,免得在大屏上显得太宽 */
    background: rgba(255, 255, 255, 0.08);   /* 半透明白底,只有一点点光感 */
    backdrop-filter: blur(16px);             /* 毛玻璃效果:把后面的内容模糊掉 */
    -webkit-backdrop-filter: blur(16px);     /* 兼容老版本 Safari,同样做模糊 */
    border-radius: 20px;           /* 四个角都切成圆角,显得柔和 */
    border: 1px solid rgba(255, 255, 255, 0.2);   /* 淡淡的白色边框,强化玻璃边缘感 */
    box-shadow: 0 20px 50px rgba(0,0,0,0.5);       /* 大范围深色阴影,让容器"浮"起来 */
    padding: 35px 30px;            /* 内边距:上下 35px,左右 30px,内容不贴边 */
    color: #fff;                   /* 文字统一用白色,和深色背景形成对比 */
    display: flex;                 /* 弹性布局,方便控制里面元素的排列 */
    flex-direction: column;        /* 让子元素从上到下竖着排列 */
}

.glass-container h2 {
    font-size: clamp(1.5rem, 6vw, 2rem);  /* 字体大小会自动缩放:最小 1.5rem,最大 2rem,中间按屏幕宽度 6% 来变 */
    text-align: center;            /* 标题居中 */
    margin-bottom: 25px;           /* 和下面内容保持一点距离 */
    font-weight: 700;              /* 加粗字体,看起来更有力 */
    text-shadow: 0 0 15px rgba(180,130,255,0.7);  /* 淡紫色发光字影,增加一点科技感 */
}

3.3 form.css – 表单元素

form.css 处理输入框、计数器文字和按钮的外观,输入框采用半透明背景配合聚焦时紫色光晕,提交按钮和重置按钮分别使用亮紫色和半透明白色,确保与深色背景形成足够对比。具体代码如下:

/* ========== 表单元素样式 ========== */
/* 普通的文本输入框,比如用来输入名字、标题这些 */
input[type="text"] {
    width: 100%;                 /* 让输入框占满整个父容器的宽度 */
    padding: 14px 18px;          /* 上下留 14px,左右留 18px 的内边距,这样文字不会贴着边 */
    font-size: 1.1rem;           /* 字体稍微大一点,看着舒服 */
    color: #fff;                 /* 文字用白色,因为背景是暗色系的 */
    background: rgba(255,255,255,0.1);   /* 背景是半透明白色,有种毛玻璃的感觉 */
    border: 1px solid rgba(255,255,255,0.25);  /* 边框也是半透明白色,和背景统一 */
    border-radius: 12px;         /* 四个角都圆润一些 */
    outline: none;               /* 去掉浏览器默认的聚焦蓝框,我们自己定义聚焦效果 */
    backdrop-filter: blur(5px);  /* 让输入框后面的内容模糊,做出毛玻璃效果 */
    transition: all 0.3s;        /* 所有样式变化都有 0.3 秒的过渡动画,看起来更顺滑 */
}

/* 当用户点击输入框,也就是输入框获得焦点时的样式 */
input[type="text"]:focus {
    border-color: #c084fc;       /* 边框变成浅紫色,提示用户正在操作这里 */
    box-shadow: 0 0 20px rgba(192,132,252,0.4);  /* 外面加一圈柔和的紫色光晕,更显眼 */
}

/* 显示还剩下多少个字可以输入的计数器 */
.char-count {
    text-align: right;           /* 数字靠右显示 */
    font-size: 0.85rem;          /* 比正常文字小一点,作为辅助信息 */
    color: rgba(255,255,255,0.6);/* 半透明白色,没那么抢眼 */
    margin: 6px 0 16px;          /* 上下留点间距,上面离输入框 6px,下面 16px */
}

/* 按钮的容器,用来并排放两个按钮 */
.btn-group {
    display: flex;               /* 使用弹性布局,让按钮横着排 */
    gap: 12px;                   /* 按钮之间的间隔为 12px */
    margin-bottom: 20px;         /* 和下面的内容保持 20px 距离 */
}

/* 按钮通用的基础样式 */
.btn {
    flex: 1;                     /* 让所有按钮平均分配容器宽度 */
    padding: 12px;               /* 按钮上下左右的内边距都是 12px */
    font-weight: 600;            /* 文字加粗一点点 */
    border: none;                /* 去掉默认边框 */
    border-radius: 12px;         /* 四个角稍微圆润 */
    cursor: pointer;             /* 鼠标移上去变成小手,表示可以点击 */
    color: #fff;                 /* 文字白色 */
    text-transform: uppercase;   /* 所有字母都变成大写,更有设计感 */
    transition: 0.3s;            /* 过渡动画 0.3 秒,比如悬停时的颜色切换 */
    letter-spacing: 1px;         /* 字母之间稍微拉开一点距离,不那么拥挤 */
}

/* "提交"按钮专用样式 */
.btn-submit {
    background: #8b5cf6;         /* 亮紫色背景,保证在暗色页面上也看得清 */
}

/* 鼠标放在提交按钮上时的效果 */
.btn-submit:hover {
    background: #a78bfa;         /* 背景颜色变浅一些 */
    transform: translateY(-2px); /* 按钮向上浮起 2px,有点立体感 */
}

/* "重置"按钮专用样式 */
.btn-reset {
    background: rgba(255,255,255,0.25);     /* 半透明白色背景,和提交按钮区分开 */
    border: 1px solid rgba(255,255,255,0.4);/* 加一圈半透明白色边框,让按钮轮廓更明显 */
}

/* 鼠标放在重置按钮上时的效果 */
.btn-reset:hover {
    background: rgba(255,255,255,0.4);     /* 背景变得更不透明,表示可以交互 */
}

3.4 message.css – 提示与历史

message.css 管理错误提示和成功提示的左侧彩色边框与动画,以及底部历史记录区域的样式。所有尺寸均使用相对单位或 clamp(),保证在任意屏幕下都能填满窗口并自适应。具体代码如下:

/* ========== 提示消息和历史记录样式 ========== */
/*
 * 这是表单中用于展示提示信息的通用样式。
 * 无论是错误消息还是成功消息,都会共用这个基础样式,
 * 然后再通过不同的修饰类来区分颜色和左侧的边条。
 */
.form-message {
    padding: 12px 18px;                /* 上下留 12px,左右留 18px 的内边距,让文字不贴边 */
    border-radius: 12px;               /* 圆角大一点,看起来更柔和、现代 */
    margin-bottom: 15px;               /* 每条消息之间保持一定间距 */
    background: rgba(0,0,0,0.3);       /* 半透明黑色背景,能透出一点底层元素,更有层次感 */
    backdrop-filter: blur(8px);        /* 背景模糊毛玻璃效果,让后面的内容微微虚化,突出消息卡片 */
    animation: slideDown 0.4s;         /* 消息出现时有一个从上往下滑入的小动画,比较友好 */
}

/* 错误消息专用的装饰颜色 */
.error-msg {
    border-left: 4px solid #ff4d6d;   /* 左侧加一条醒目的红色边框,一眼就知道是错误 */
    color: #ffadb5;                    /* 文字也用偏红的浅粉色,和红色边框呼应 */
}

/* 成功消息专用的装饰颜色 */
.success-msg {
    border-left: 4px solid #69db7c;   /* 左侧绿色边框,视觉上传递"成功、通过"的感觉 */
    color: #b2f2bb;                    /* 浅绿色文字,整体绿色系,柔和又清晰 */
}

/*
 * 消息出现的下滑动画定义。
 * 从上方 15px 处透明状态,慢慢移到原位并完全显示,
 * 让提示信息看起来是"落下来"的,增加一点精致感。
 */
@keyframes slideDown {
    from {
        opacity: 0;                     /* 动画开始时完全透明 */
        transform: translateY(-15px);  /* 同时位置向上偏移 15px,营造从上面掉下来的感觉 */
    }
    to {
        opacity: 1;                     /* 动画结束时完全不透明 */
        transform: translateY(0);      /* 回到正常位置 */
    }
}

/*
 * 历史记录整体容器。
 * 用来展示之前操作过的记录,做成一个半透明小卡片样式。
 */
.history-box {
    margin-top: 20px;                    /* 与上方元素保持 20px 距离 */
    padding: 15px;                       /* 内部留白,不让内容挨着边框 */
    background: rgba(0,0,0,0.3);         /* 半透明黑色背景,和整体风格统一 */
    border-radius: 12px;                 /* 圆角卡片 */
    border: 1px solid rgba(255,255,255,0.15); /* 淡淡的白色边框,分隔卡片与背景 */
}

/* 历史记录区域的标题,比如"最近操作"之类的 */
.history-box h4 {
    margin-bottom: 10px;                 /* 标题下方留一点空间,和列表拉开 */
    color: #c084fc;                      /* 浅紫色文字,突出标题,但又不刺眼 */
    border-bottom: 1px solid rgba(255,255,255,0.2); /* 下划一条半透明白线,视觉上做分割 */
    padding-bottom: 5px;                 /* 文字和下方分割线之间的小间距 */
}

/* 历史记录的列表容器(一个无序列表) */
.history-list {
    list-style: none;                    /* 去掉默认的圆点项目符号,让列表看起来更干净 */
    max-height: 120px;                   /* 限制列表最大高度,避免记录太多时撑爆布局 */
    overflow-y: auto;                    /* 超出高度时出现垂直滚动条,可以滚动查看旧记录 */
    font-size: 0.9rem;                   /* 字号比正文稍小一点,历史记录作为辅助信息 */
}

/* 每一条历史记录 */
.history-list li {
    padding: 4px 0;                      /* 上下留 4px 呼吸感,左右不需要额外缩进 */
    border-bottom: 1px dashed rgba(255,255,255,0.1); /* 用虚线半透明白线分隔每一条记录,轻量不抢眼 */
}

4. 前端交互脚本设计

JavaScript 部分被拆分为 7 个文件,按依赖顺序在 index.jsp 中引入。

4.1 canvas-setup.js – 画布初始化

canvas-setup.js 获取 Canvas 元素和 2D 上下文,并将它们挂载到全局 app 对象上,同时监听窗口大小变化以重绘画布。具体代码如下:

// 创建一个全局的 app 对象,如果已经有就沿用,避免覆盖其他地方的定义
// 这就像在墙上钉个挂钩,所有文件都能往上面挂东西
window.app = window.app || {};

// 拿到页面上的画布元素,存到 app.canvas 里,方便其他地方直接用
app.canvas = document.getElementById('particleCanvas');

// 从画布上获取 2D 画笔,以后画点、画线都靠它
app.ctx = app.canvas.getContext('2d');

// 定义一个函数,专门负责让画布尺寸跟随浏览器窗口大小
function resizeCanvas() {
    // 把画布的宽度设成浏览器窗口的当前宽度
    app.canvas.width = window.innerWidth;
    // 把画布的高度设成浏览器窗口的当前高度
    app.canvas.height = window.innerHeight;
}

// 监听浏览器窗口大小变化事件,窗口一拉伸就自动调用 resizeCanvas
window.addEventListener('resize', resizeCanvas);

// 页面刚加载时主动调用一次,让画布尺寸马上和窗口一致
resizeCanvas();

4.2 mouse-tracker.js – 鼠标追踪

mouse-tracker.js 追踪鼠标在页面上的坐标,存入 app.mouse,供粒子排斥效果使用。具体代码如下:

// 在 app 对象上挂一个 mouse 属性,专门用来存放鼠标的状态
// x、y 存坐标,一开始鼠标还没进页面,所以是 null
// radius 是个“影响半径”,后面比如做粒子靠近效果时,鼠标多远能起作用,这里定成 120 像素
app.mouse = { x: null, y: null, radius: 120 };

// 监听整个窗口的鼠标移动——只要鼠标在页面上滑动就会触发
window.addEventListener('mousemove', (e) => {
    // 把鼠标当前的水平位置(相对于浏览器窗口左上角)存起来
    app.mouse.x = e.clientX;
    // 把垂直位置也存起来
    app.mouse.y = e.clientY;
});

// 监听鼠标离开窗口的事件(比如鼠标移到了浏览器外面,或者切到别的窗口)
window.addEventListener('mouseout', () => {
    // 鼠标都不在页面里了,就把坐标清空,其他地方用的时候就知道:哦,鼠标现在不在了
    app.mouse.x = null;
    app.mouse.y = null;
});

4.3 particle-class.js – 粒子类定义

particle-class.js 定义 Particle 类,包含粒子的位置、速度、半径和颜色,实现自身移动、边界反弹和发光绘制。具体代码如下:

// 把 Particle 类直接挂在 window 上,这样在全局任何地方都能 new Particle() 使用它
window.Particle = class Particle {
    constructor() {
        // 创建粒子时直接调用 reset 方法,给它一个随机的初始状态
        this.reset();
    }

    reset() {
        // 随机生成粒子在画布上的初始 x 坐标(在整个画布宽度范围内)
        this.x = Math.random() * app.canvas.width;
        // 随机生成粒子在画布上的初始 y 坐标(在整个画布高度范围内)
        this.y = Math.random() * app.canvas.height;
        // 随机一个运动方向角度(0 到 2π,也就是 0 到 360 度)
        const angle = Math.random() * Math.PI * 2;
        // 随机一个运动速度,速度范围在 1.5 到 2.5 之间
        const speed = 1.5 + Math.random() * 1.0;
        // 根据角度和速度,用三角函数算出水平方向的速度分量 vx
        this.vx = Math.cos(angle) * speed;
        // 同理,算出垂直方向的速度分量 vy
        this.vy = Math.sin(angle) * speed;
        // 随机生成粒子半径,大小在 3 到 8 像素之间
        this.radius = 3 + Math.random() * 5;
        // 随机生成粒子颜色,色相在 200 到 240 左右(青蓝色范围),饱和度和亮度固定 50%,透明度 0.65
        this.color = `hsla(${Math.random() * 40 + 200}, 50%, 50%, 0.65)`;
    }

    // 更新粒子位置,并处理碰到画布边界时的反弹
    update() {
        // 左边界碰撞检测:如果粒子的左边缘(x - 半径)超出画布左侧
        if (this.x - this.radius < 0) {
            // 把粒子位置强制拉回左边界内,紧贴边界
            this.x = this.radius;
            // 水平速度反向,实现反弹效果
            this.vx *= -1;
        } else if (this.x + this.radius > app.canvas.width) {
            // 右边界碰撞检测:如果粒子的右边缘超出画布右侧
            // 把粒子强制拉回右边界内,紧贴边界
            this.x = app.canvas.width - this.radius;
            // 水平速度反向,反弹
            this.vx *= -1;
        }

        // 上边界碰撞检测:如果粒子的上边缘超出画布顶部
        if (this.y - this.radius < 0) {
            // 把粒子强制拉回上边界内
            this.y = this.radius;
            // 垂直速度反向,反弹
            this.vy *= -1;
        } else if (this.y + this.radius > app.canvas.height) {
            // 下边界碰撞检测:如果粒子的下边缘超出画布底部
            // 把粒子强制拉回下边界内
            this.y = app.canvas.height - this.radius;
            // 垂直速度反向,反弹
            this.vy *= -1;
        }

        // 根据当前速度更新粒子的位置(加上水平速度和垂直速度)
        this.x += this.vx;
        this.y += this.vy;
    }

    draw() {
        // 获取全局的绘图上下文 ctx(假设 app.ctx 已经存在)
        const ctx = app.ctx;
        // 开始绘制路径
        ctx.beginPath();
        // 画一个圆形,圆心在 (x, y),半径是 this.radius,弧度从 0 到 2π(完整圆)
        ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
        // 设置填充颜色为粒子之前随机生成的颜色
        ctx.fillStyle = this.color;
        // 先填充一遍圆形(不带发光效果)
        ctx.fill();
        // 添加微光效果:设置阴影颜色为淡紫色半透明
        ctx.shadowColor = 'rgba(180,130,255,0.3)';
        // 阴影的模糊半径跟粒子半径成正比,大粒子光晕更大
        ctx.shadowBlur = this.radius * 1.5;
        // 再次填充,这次会带上阴影光晕,产生“微光”效果
        ctx.fill();
        // 把阴影模糊归零,避免影响后续其他绘制
        ctx.shadowBlur = 0;
    }
};

4.4 collision.js – 碰撞检测

collision.js 提供 handleCollisions 函数,对粒子两两进行完全弹性碰撞检测,交换法向速度并修正重叠位置。具体代码如下:

// 全局碰撞处理函数,供 renderer 调用
// 这个函数用来处理所有粒子之间的碰撞,让它们能够合理地弹开,不会重叠在一起
function handleCollisions(particles) {
    // 外层循环:拿起当前粒子 a,准备和它后面的每个粒子进行碰撞检测
    for (let i = 0; i < particles.length; i++) {
        // 内层循环:从 i 的下一个开始,避免重复检测同一对粒子(比如 i=0,j=1 和 i=1,j=0 是同一对)
        for (let j = i + 1; j < particles.length; j++) {
            const a = particles[i];   // 当前粒子 a
            const b = particles[j];   // 和它配对的粒子 b

            // 计算 b 相对于 a 的位置差(向量)
            const dx = b.x - a.x;
            const dy = b.y - a.y;
            // 计算两个粒子圆心之间的距离
            const dist = Math.sqrt(dx * dx + dy * dy);
            // 两个粒子刚好相碰时,圆心距离应该是半径之和
            const minDist = a.radius + b.radius;

            // 如果实际距离小于半径之和,说明它们重叠了(发生了碰撞)
            if (dist < minDist) {
                // 计算从 a 指向 b 的单位方向向量(也就是碰撞的法线方向)
                const nx = dx / dist;
                const ny = dy / dist;
                // 计算 a 相对于 b 的速度差(向量)
                const dvx = a.vx - b.vx;
                const dvy = a.vy - b.vy;
                // 把相对速度投影到法线方向上,得到法向相对速度分量
                const dvn = dvx * nx + dvy * ny;

                // 如果法向相对速度 > 0,说明它们正在远离,不需要处理碰撞
                if (dvn > 0) continue;

                // 这里是弹性碰撞处理(假设两个粒子质量相同)
                // 交换法向速度:a 减去 dvn,b 加上 dvn,这样就能让它们像撞球一样弹开
                a.vx -= dvn * nx;
                a.vy -= dvn * ny;
                b.vx += dvn * nx;
                b.vy += dvn * ny;

                // 为了避免粒子粘在一起,需要把重叠的部分分开
                // overlap 是它们重叠的深度的一半(因为要把 a 和 b 各移动一半距离)
                const overlap = (minDist - dist) / 2;
                // 把 a 沿着法线反方向(朝向远离 b 的方向)移动 overlap 距离
                a.x -= overlap * nx;
                a.y -= overlap * ny;
                // 把 b 沿着法线方向(远离 a 的方向)也移动 overlap 距离
                b.x += overlap * nx;
                b.y += overlap * ny;
            }
        }
    }
}

4.5 renderer.js – 动画渲染主循环

renderer.js 初始化粒子数组,在动画循环中为每个粒子施加鼠标排斥力,调用自身更新和碰撞检测,最后绘制所有粒子和低透明度连线,形成动态背景。具体代码如下:

// 粒子数组
const particles = [];                // 存放所有粒子的空数组
const PARTICLE_COUNT = 50;           // 固定设置粒子的总数量为50个

// 初始化粒子
for (let i = 0; i < PARTICLE_COUNT; i++) {   // 循环创建指定数量的粒子
    particles.push(new Particle());          // 新建粒子对象,添加到粒子数组里
}

// 鼠标排斥力施加
function applyMouseForce(p) {        // 给单个粒子加鼠标排斥效果的函数
    if (app.mouse.x != null && app.mouse.y != null) {   // 先判断鼠标坐标是否有效(不为空)
        const dx = p.x - app.mouse.x;                  // 计算粒子和鼠标在X轴上的距离差
        const dy = p.y - app.mouse.y;                  // 计算粒子和鼠标在Y轴上的距离差
        const dist = Math.sqrt(dx * dx + dy * dy);     // 算粒子到鼠标的直线距离(勾股定理)
        if (dist < app.mouse.radius) {                 // 粒子进入鼠标影响范围才触发排斥
            const force = (app.mouse.radius - dist) / app.mouse.radius; // 计算排斥力大小,离越近力越大
            const angle = Math.atan2(dy, dx);          // 算粒子相对于鼠标的角度
            p.vx += Math.cos(angle) * force * 0.8;     // 给粒子X轴速度加排斥力(0.8是力度系数)
            p.vy += Math.sin(angle) * force * 0.8;     // 给粒子Y轴速度加排斥力(0.8是力度系数)
        }
    }
}

// 绘制粒子连线
function drawLines() {                // 绘制粒子之间连线的函数
    const ctx = app.ctx;             // 获取画布绘图工具,用来画线条
    for (let i = 0; i < particles.length; i++) {       // 外层循环:选第一个粒子
        for (let j = i + 1; j < particles.length; j++) { // 内层循环:选第二个粒子(避免重复连线)
            const dx = particles[i].x - particles[j].x; // 算两个粒子X轴距离差
            const dy = particles[i].y - particles[j].y; // 算两个粒子Y轴距离差
            const dist = Math.sqrt(dx * dx + dy * dy);  // 算两个粒子的直线距离
            if (dist < 100) {         // 只有距离小于100才画连线
                ctx.beginPath();     // 开始画新的线条
                ctx.moveTo(particles[i].x, particles[i].y); // 画笔移到第一个粒子位置
                ctx.lineTo(particles[j].x, particles[j].y); // 连线到第二个粒子位置
                ctx.strokeStyle = `rgba(255,255,255,${0.05 * (1 - dist / 100)})`; // 线条颜色:白色,距离越远透明度越低
                ctx.stroke();        // 把线条真正画出来
            }
        }
    }
}

// 动画循环
function animate() {                 // 动画主循环,一直刷新画面
    app.ctx.clearRect(0, 0, app.canvas.width, app.canvas.height); // 清空整个画布,擦掉上一帧的内容
    // 1. 鼠标力
    for (const p of particles) applyMouseForce(p); // 遍历所有粒子,施加鼠标排斥力
    // 2. 粒子移动 + 边界反弹
    for (const p of particles) p.update();         // 遍历所有粒子,让粒子移动+碰到边界反弹
    // 3. 粒子间碰撞
    handleCollisions(particles);                   // 处理粒子之间互相碰撞的效果
    // 4. 绘制粒子
    for (const p of particles) p.draw();           // 遍历所有粒子,把粒子画到画布上
    // 5. 绘制连线
    drawLines();                                   // 调用函数,画粒子之间的连线
    requestAnimationFrame(animate);                // 让浏览器下一帧继续执行动画,实现流畅动效
}

animate();                           // 启动动画主循环

4.6 counter.js – 实时字数统计

counter.js 监听输入框的 input 事件,实时更新字符数显示,并根据长度切换“字数过少/过多/刚好5个”的提示文字与颜色。具体代码如下:

// 字数实时计数与提示
const userInput = document.getElementById('userInput');   // 获取页面里的输入框元素
const charNum = document.getElementById('charNum');       // 获取显示字数的元素
const charHint = document.getElementById('charHint');     // 获取文字提示的元素

userInput.addEventListener('input', () => {                // 给输入框绑输入事件,一打字就触发
    let len = userInput.value.length;                      // 计算输入框里的文字总长度
    charNum.textContent = len;                             // 把算出来的字数显示到页面上
    if (len < 5) {                                         // 判断:如果输入的字数少于5个
        charHint.textContent = '字数过少';                  // 提示文字改成“字数过少”
        charHint.style.color = '#ff8c8c';                  // 提示文字变成浅红色
    } else if (len > 5) {                                  // 判断:如果输入的字数多于5个
        charHint.textContent = '字数过多';                  // 提示文字改成“字数过多”
        charHint.style.color = '#ff8c8c';                  // 提示文字还是浅红色
    } else {                                               // 剩下的情况:正好5个字
        charHint.textContent = '刚好5个';                   // 提示文字改成“刚好5个”
        charHint.style.color = '#a5f0c5';                  // 提示文字变成浅绿色
    }
});

4.7 page-init.js – 页面初始化与重置

page-init.js 在页面加载时根据服务器返回的消息类型(错误或成功)决定是否清空输入框并聚焦,同时绑定“重置历史”按钮的点击事件,通过 action=reset 参数刷新页面清空 session 记录。具体代码如下:

// 页面初始化与重置按钮
// 监听页面DOM加载完成,页面加载好就执行初始化逻辑
window.addEventListener('DOMContentLoaded', () => {
    // 获取页面上消息类型的选中值
    const msgType = document.getElementById('msgType').value;
    // 获取用户输入内容的文本框元素
    const input = document.getElementById('userInput');
    // 获取显示当前输入字数的元素
    const charNum = document.getElementById('charNum');
    // 获取字数校验的提示文字元素
    const charHint = document.getElementById('charHint');

    // 定义函数:更新字数统计和提示文字,参数是输入的字符长度
    function updateCount(len) {
        // 把当前输入的字数显示到页面上
        charNum.textContent = len;
        // 判断:输入字数小于5个
        if (len < 5) {
            // 提示文字显示“字数过少”
            charHint.textContent = '字数过少';
            // 提示文字颜色改成浅红色
            charHint.style.color = '#ff8c8c';
        // 判断:输入字数大于5个
        } else if (len > 5) {
            // 提示文字显示“字数过多”
            charHint.textContent = '字数过多';
            // 提示文字颜色改成浅红色
            charHint.style.color = '#ff8c8c';
        // 字数刚好等于5个的情况
        } else {
            // 提示文字显示“刚好5个”
            charHint.textContent = '刚好5个';
            // 提示文字颜色改成浅绿色
            charHint.style.color = '#a5f0c5';
        }
    }

    // 判断:消息类型是不是错误类型
    if (msgType === 'error') {
        // 清空用户输入框里的所有内容
        input.value = '';
        // 调用函数,把字数统计重置为0
        updateCount(0);
    // 不是错误类型,正常加载页面的情况
    } else {
        // 调用函数,根据输入框现有内容更新字数
        updateCount(input.value.length);
    }
    // 让输入框自动获得光标,用户不用点击就能输入
    input.focus();
});

// 给重置按钮绑定点击事件
document.getElementById('resetBtn').addEventListener('click', () => {
    // 弹出确认框,问用户是否确定清空记录
    if (confirm('确定清空所有成功提交记录吗?')) {
        // 用户点确定后,跳转到重置接口执行清空操作
        window.location.href = 'index.jsp?action=reset';
    }
});

5. JSP 页面设计

5.1 index.jsp – 表单主界面

index.jsp 是整个实验的交互核心。页面顶部通过 JSP 脚本获取 request 域中的 messagemessageType,据此渲染错误或成功提示。表单 action 指向 validate.jsp,使用 method="post"。输入框不预置 value,每次显示均为空,符合实验要求。字符计数器下方显示“X / 5”和对应的文字提示。页面底部通过 <jsp:include page="history.jsp" /> 引入历史记录片段。页面还包含一个隐藏输入框,用于将 messageType 传递给前端 JavaScript,以便在 window.onload 时根据验证结果执行清空和聚焦操作。具体代码如下:

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>   
<%@ page import="java.util.*" %>   

<%
    String message = (String) request.getAttribute("message");   // 从请求对象里拿提示消息
    String messageType = (String) request.getAttribute("messageType");   // 从请求对象里拿消息类型(成功/报错)
    if (message == null) message = "";   // 消息为空就赋值空字符串,防止空指针报错
    if (messageType == null) messageType = "";   // 消息类型为空也赋值空字符串

    List history = (List) session.getAttribute("history");   // 从会话里拿历史记录列表
    if (history == null) history = new ArrayList<>();   // 历史记录为空就新建一个空列表
    request.setAttribute("historyList", history);   // 把历史记录存到请求域,给页面展示用

    if ("reset".equals(request.getParameter("action"))) {   // 判断用户是不是点了重置历史
        session.removeAttribute("history");   // 从会话里删掉历史记录数据
        history.clear();   // 清空列表里的所有历史内容
        request.setAttribute("historyList", history);   // 重新把空列表存到请求域
    }
%>

   
   
   
       
       
    输入验证 - 实验五   
       
       
       
       
   
   
       

    

✨ 输入验证

<% if ("error".equals(messageType) && !message.isEmpty()) { %>
<%= message %>
<% } else if ("success".equals(messageType) && !message.isEmpty()) { %>
<%= message %>
<% } %>
placeholder="请输入任意5个字符" autocomplete="off" required>
0 / 5






image

图 5-1 表单初始界面
(浏览器中 index.jsp 的初始状态,深色动态背景上居中的玻璃容器内,输入框为空,计数器显示“0 / 5 字数过少”,两个按钮清晰可见)

5.2 validate.jsp – 服务器端验证

validate.jsp 仅包含服务器端逻辑,不产生任何 HTML 输出。首先设置请求编码为 UTF-8,通过 request.getParameter 获取 userInput 参数。接着进行三层判断:若为 nulltrim() 后为空,返回错误消息“输入不能为空”;否则若 trim().length() 不等于 5,返回错误消息并附带当前字符数;两者均不满足则验证成功,消息为“格式正确”并包含输入内容。成功时还会将该输入存入 session 的历史记录列表(只保留最近 5 条)。最后将 messagemessageType 存入 request 属性,通过 RequestDispatcher 转发回 index.jsp,浏览器地址栏不变。具体代码如下:

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>   
<%@ page import="java.util.*" %>   

<%
    request.setCharacterEncoding("UTF-8");   // 设置请求编码为UTF-8,防止中文乱码
    String input = request.getParameter("userInput");   // 获取前端传过来的用户输入内容
    String msg, type;   // 定义两个变量:msg存提示文字,type存提示类型(成功/失败)

    if (input == null || input.trim().isEmpty()) {   // 判断用户是不是没输入内容(空值/全空格)
        msg = "⚠️ 输入不能为空";   // 没输入就给空值错误提示
        type = "error";   // 标记提示类型为错误
    } else if (input.trim().length() != 5) {   // 再判断输入的字符长度是不是不是5个
        msg = "⚠️ 必须输入5个字符,当前" + input.trim().length() + "个";   // 长度不对给格式错误提示
        type = "error";   // 标记提示类型为错误
    } else {   // 输入符合要求:非空且刚好5个字符
        msg = "🎉 格式正确!你输入的是:" + input;   // 给格式正确的成功提示
        type = "success";   // 标记提示类型为成功

        // 记录到 session
        List history = (List) session.getAttribute("history");   // 从会话中拿历史记录列表
        if (history == null) history = new ArrayList<>();   // 如果历史记录为空,就新建一个空列表
        history.add(input);   // 把本次正确输入添加到历史记录里
        if (history.size() > 5) {   // 如果历史记录超过5条
            history = history.subList(history.size()-5, history.size());   // 只保留最新的5条记录
        }
        session.setAttribute("history", history);   // 把更新后的历史记录存回会话
    }

    request.setAttribute("message", msg);   // 把提示文字存到请求域,传给前端页面
    request.setAttribute("messageType", type);   // 把提示类型存到请求域,传给前端页面
    request.getRequestDispatcher("index.jsp").forward(request, response);   // 跳回首页index.jsp展示结果
%>

5.3 history.jsp – 历史记录片段

history.jsp 是一个可复用的 JSP 片段,从 request 域获取历史记录列表并倒序渲染为 <ul> 列表。若无记录则显示“暂无记录”。具体代码如下:

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>   
<%@ page import="java.util.*" %>   

<%
    // 从请求域里取出历史记录列表
    List<String> historyList = (List<String>) request.getAttribute("historyList");
    // 如果没取到记录,就新建一个空列表避免报错
    if (historyList == null) historyList = new ArrayList<>();
%>

<div class="history-box">   
    <h4>📋 最近通过记录</h4>   
    <% if (historyList.isEmpty()) { %>   
        <p style="color:rgba(255,255,255,0.5); font-size:0.85rem;">暂无记录</p>   
    <% } else { %>   
        <ul class="history-list">   
            <% for (int i = historyList.size()-1; i >= 0; i--) { %>   
                <li>✅ <%= historyList.get(i) %></li>   
            <% } %>   
        </ul>   
    <% } %>   
</div>   

该片段通过 JSP 脚本获取由 index.jspvalidate.jsp 设置的 historyList 属性,实现历史记录的集中展示。使用 <jsp:include> 嵌入时,只需保证 request 域中已存在该属性即可。记录倒序排列确保最新提交始终位于列表顶部,虚线分隔与半透明边框使界面保持整洁一致。

6. 本地 Eclipse 集成 Tomcat 部署与运行

为确保所有页面能够正常加载样式和脚本,必须将项目部署到 Web 服务器中运行。

步骤1:创建动态 Web 项目
打开 Eclipse JEE,菜单栏选择 FileNewDynamic Web Project。项目名称填写 task5,Target Runtime 选择已配置的 Apache Tomcat v9.0。勾选 Generate web.xml deployment descriptor,点击 Finish

步骤2:创建目录结构并导入项目文件
WebContent 目录下创建 cssjs 文件夹,将上述所有 CSS 和 JS 文件放入对应文件夹。index.jspvalidate.jsphistory.jsp 直接放置在 WebContent 根目录下。

步骤3:部署并启动 Tomcat
Servers 视图中右键 Tomcat v9.0 Server → Add and Remove → 将 task5 项目添加至右侧 ConfiguredFinish。右键 Tomcat 服务器选择 Start,等待 Console 控制台出现 Server startup in xxx ms

步骤4:本地浏览器验证
打开浏览器,输入 http://localhost:8080/task5/index.jsp。页面应显示动态粒子背景和居中的玻璃表单。分别测试:不输入内容点击“判断”,应显示错误提示且输入框清空聚焦;输入不足或超过 5 个字符,应提示“必须输入5个字符,当前X个”;输入恰好 5 个字符(如“Hello”),则显示绿色成功提示,并在历史记录区域出现该条记录。

图片2

图 5-2 输入为空时的错误提示
(点击判断后,页面出现红色提示“输入不能为空”,输入框已被清空并获得焦点)

图片3

图 5-3 输入长度不为5时的错误提示
(输入“abc”,提交后显示“必须输入5个字符,当前3个”,计数器提示“字数过少”)

图片4

图 5-4 输入正确时的成功提示
(输入“Hello”,提交后显示绿色提示“格式正确!你输入的是:Hello”,下方历史记录出现新增条目)

图片5

图 5-5 历史记录与鼠标交互效果
(多次成功提交后,历史记录区域列出最近 5 条;鼠标放置于页面上,粒子被推开,展示背景交互)


7. 云端 ECS 服务器部署步骤

完成本地测试后,将项目部署至阿里云 ECS 服务器(公网 IP:114.55.150.197),实现互联网访问。

步骤1:导出 WAR 包
在 Eclipse 中右键 task5 项目 → ExportWAR file。选择导出路径,命名为 task5.war,确保包含所有源文件。

步骤2:上传 WAR 包至 ECS
使用 FTP 工具(如 FileZilla)连接阿里云 ECS 实例,将 task5.war 上传至云端 Tomcat 的 webapps 目录(例如 C:\Users\Administrator\Desktop\apache-tomcat-9.0.117\webapps)。Tomcat 会自动解压该包生成 task5 文件夹。

步骤3:配置安全组与防火墙
登录阿里云控制台,进入 ECS 实例的“安全组”规则,添加入方向规则:协议类型 TCP,端口范围 8080,授权对象 0.0.0.0/0。检查 Windows 防火墙,确保入站规则允许 8080 端口。

步骤4:启动云端 Tomcat 并验证
通过远程桌面进入 ECS,运行 Tomcat bin 目录下的 startup.bat 启动服务器。本地浏览器访问 http://114.55.150.197:8080/task5/index.jsp,应能看到与本地完全一致的粒子背景和表单功能。测试所有验证场景、字数计数器、历史记录和重置功能,确保公网环境下 JSP 解析、Session 管理和 JavaScript 交互均正常。

图片6

图 5-6 本地浏览器成功部署并访问项目
(本地浏览器访问 http://localhost:8080/task5/ ,展示动态背景和完整表单界面)

图片7

图 5-7 公网浏览器成功访问云端部署的项目
(浏览器地址栏显示 http://114.55.150.197:8080/task5/index.jsp ,页面所有元素与本地一致,验证功能正常)


8. 实验小结(分工与感想)

本次实验围绕“基于JSP内置对象的服务器端表单验证”,小组四人分工协作,完成了从需求分析、模块拆分、代码编写到本地部署与云端发布的完整开发流程。项目采用高度模块化的设计思想,将样式、交互、验证逻辑分别拆分至13个独立文件,每个文件仅承担一至两个功能,实现了代码的高内聚与低耦合。以下为每位组员的解题思路与调试过程总结。

5.1 方致沅(负责项目架构设计、服务器端验证逻辑与响应式整体布局)
我的任务是搭建项目的整体目录结构,完成index.jsp表单主界面、validate.jsp服务器验证页以及history.jsp历史记录片段的设计。在设计index.jsp时,我采用JSP脚本接收request域中的messagemessageType属性,根据验证结果动态渲染错误或成功提示,并通过隐藏字段将消息类型传递给前端JavaScript,实现了“验证失败则清空输入框并自动聚焦”的交互需求。在validate.jsp中,我使用request.getParameter获取用户输入,通过trim()判断是否为空以及长度是否恰好为5个字符,验证成功后利用session对象存储最近5条提交记录。调试过程中,最初未设置request.setCharacterEncoding("UTF-8")导致中文输入出现乱码,添加后问题解决。此外,我设计了四种CSS文件的拆分方案(base.cssglass.cssform.cssmessage.css),使用Flexbox全局居中与深色渐变背景,确保页面在任何屏幕尺寸下都能填满视口并自适应。

5.2 李润哲(负责Canvas粒子背景动画与鼠标交互系统)
我负责前端动态背景的开发,将Canvas粒子系统拆分为5个独立的JS文件。canvas-setup.js初始化Canvas画布并挂载到全局app对象,mouse-tracker.js实时追踪鼠标坐标,particle-class.js定义粒子类(包含位置、速度、半径、颜色及其自身运动与边界反弹),collision.js实现粒子间的完全弹性碰撞检测与位置修正,renderer.js整合动画循环,为每个粒子施加鼠标排斥力并调用碰撞处理与连线绘制。调试时遇到的主要问题是粒子尺寸调整后运动速度不够快,我将初始速度范围提升至1.52.5,并将粒子半径加大至38像素,使其视觉存在感更强。碰撞检测最初因效率问题导致粒子黏连,通过在碰撞后增加位置重叠修正代码,并确保“正在远离”的粒子对跳过处理,最终实现了流畅的碰撞效果。鼠标靠近时粒子被明显推开,远离后恢复随机运动,形成了良好的交互反馈。

5.3 王鑫杰(负责表单交互优化、字数实时计数器与重置功能)
我负责counter.jspage-init.js的开发,以及表单页面的细节优化。counter.js监听输入框的input事件,实时统计字符数并动态更新“0/5”显示,同时根据长度变化切换“字数过少/字数过多/刚好5个”的提示文字与颜色(红色预警、绿色合格),为用户提供即时的输入引导。page-init.js在页面加载时读取隐藏字段msgType的值,若为"error"则自动清空输入框并调用focus()激活焦点,确保验证失败后用户无需手动操作即可重新输入。此外,我实现了“重置历史”按钮的功能,通过action=reset参数请求index.jsp自身,后端检测到该参数后清除session中的历史记录列表。调试时发现重置后页面未刷新历史显示,原因是request域中的historyList未同步更新,我在index.jsp顶部增加了重新设置request.setAttribute的逻辑后问题解决。

5.4 康奕彬(负责云端ECS部署、项目打包上传与全链路功能验证)
我负责将整个task5项目部署至阿里云ECS服务器(公网IP:114.55.150.197),并确保所有功能在云端正常运行。部署初期尝试通过/task5路径访问,但始终遇到404或ERR_CONNECTION_REFUSED错误。经过多轮排查,发现Tomcat的自动部署机制未正确识别webapps/task5目录,手动在server.xml中添加<Context>配置后仍无效。最终采用“直接部署为ROOT应用”的方案:清空webapps/ROOT目录下的所有默认文件,将项目所有内容(index.jspcss/js/等)复制进去,并修改WEB-INF/web.xmlindex.jsp设为唯一欢迎文件。同时在阿里云安全组和Windows防火墙中确认8080端口已开放,重启Tomcat后公网访问http://114.55.150.197:8080/成功显示动态粒子背景表单页面。经全面测试,输入验证、字数提示、清空聚焦、历史记录与重置按钮等所有功能均与本地一致。通过本次部署,我深入理解了Tomcat应用上下文加载机制、安全组与防火墙的协同配置,以及静态资源路径的排查方法。

5.5 团队协作总结
本次实验中,我们延续了前四次实验的分工协作模式,每位成员独立负责不同模块,并通过微信群实时同步进度与问题。方致沅搭建的项目架构和样式拆分为全组提供了统一的开发规范;李润哲的粒子动画系统因涉及跨文件变量共享(app对象),首次合并时出现了canvas未定义的错误,经全组讨论后统一将所有共享变量挂载到window.app上解决;王鑫杰优化的字数计数器被全组采纳,提升了表单交互的友好性;康奕彬在云端部署阶段连续排查了404、ERR_CONNECTION_REFUSED等多个问题,总结的“ROOT直接部署”方案有效避免了上下文路径冲突。通过本次实验,我们掌握了JSP内置对象(requestresponsesession)在表单验证中的实际应用、前端JavaScript模块化开发、以及云服务器Web应用部署与故障排查的完整流程,为后续课程设计积累了宝贵经验。


附录
《Web编程技术》余元辉,清华大学出版社,2024.8
《JSP设计》(第三版),Hans Bergsten主编,林琪、朱涛江翻译 O'Reilly Media,Inc 2017年。

posted @ 2026-05-08 21:20  fangzhiyuan  阅读(24)  评论(0)    收藏  举报