Web 的「主窗体」是三块拼起来的:顶栏、左菜单、右内容区,用 iframe 或 include 组装。
菜单还是「算」出来的,但这次算完要写进 HTML,不是往控件里塞。
三个角色的首屏复杂度差十倍,但这不是简配,是按职责给信息。
第 19 章的主窗体是一个窗口加一个面板,这一章变成三个页面拼一个框架。
思路一样:菜单按角色算、内容区换页、当前用户带下去。
教材 20.7 分三小节:概述、技术分析、实现过程。
「技术分析」里那句「iframe 加载子页面」是这一节的分水岭——
它决定了「菜单不重画」,也决定了「后退按钮不能用」。
沙箱没有浏览器,所以页面用「打印 HTML 片段」的方式复刻,
复刻的是「页面怎么拼、参数怎么传」,不是「样式好不好看」。
一、三块拼一个框架
| 部分 | 文件 | 作用 |
|---|---|---|
| 顶栏 | top.jsp |
系统名 + 当前用户 + 退出 |
| 左菜单 | left.jsp |
按角色生成的菜单树 |
| 内容区 | main.jsp |
iframe 或 include 加载功能页 |
| 框架页 | index.jsp |
把上面三块拼起来 |
iframe 和 include 的区别要分清。 iframe 加载的是「另一个页面」,地址栏不变;
include 是「把另一个页面的内容拼进来」,每个功能页都要重新拼一次框架。
教材用的是 iframe,好处是简单、菜单不用重画;
坏处是「后退按钮不能用」,而且对搜索引擎不友好。
现代做法是「单页应用」,但那是另一个话题。
二、菜单按角色算:三份菜单
| 角色 | 一级菜单 | 二级项 |
|---|---|---|
| 管理员 | 4 | 9 |
| 教师 | 2 | 4 |
| 学生 | 1 | 1 |
「学生」只剩「成绩查询」一项,而且这一项对三种角色是同一个页面。
同一个页面,三种数据范围——页面不区分角色,WHERE 区分。
「一级节点下没有可显示的项,就整块隐藏」这条规则要写进代码,
否则会留下一个点开是空白的菜单。 空菜单比没有菜单更让人困惑。
还有一个坑:菜单项和页面文件必须一一对应。
少一个映射,菜单点下去就是 404——加功能时最容易漏这个映射表,而且漏了不会在编译期报错。
三、每个页面顶部那五行:校验登录
| 行 | 做什么 | 漏了会怎样 |
|---|---|---|
| 1 | 取 session 里的 role |
— |
| 2 | role == null 就跳登录页 |
匿名能进 |
| 3 | 取 uname |
— |
| 4 | 页面显示当前用户 | 用户不知道自己在谁的账号里 |
| 5 | return(跳转后必须返回) |
后面代码继续跑,可能报空指针 |
第 5 行最容易被忽略:sendRedirect 之后必须 return。
不写 return,后面的代码还会继续执行——轻则报空指针,重则数据已经写进库了才跳转。
「每个页面都写一遍」这件事本身就是隐患。 所以标准做法是写成 Filter,
在 web.xml 里配一句「所有 *.jsp 都过这个过滤器」,全站一次生效。
四、三个首屏各自该有什么
| 角色 | 首屏放什么 | 为什么 |
|---|---|---|
| 管理员 | 模块入口 + 待办(未录成绩的班) | 要一眼看到全貌 |
| 教师 | 我带的班 + 最近录入时间 | 进来就是为了录分 |
| 学生 | 本学期成绩单 | 进来就是为了看分 |
三个首屏,功能复杂度差十倍。 但这不是「简配」,是「按职责给信息」。 给学生的首屏放四个模块入口,他一个都点不了,只会以为自己没权限。 「待办」那一栏是最容易被砍的,也是最有用的。 管理员打开系统,最想知道的是「还有哪些班没录完」,不是「有哪些菜单」。
五、动手 Demo:框架页拆分与三份菜单
这一段做两件事:打印出 index.jsp 的三块结构,
再按角色算三份菜单并渲染成 HTML 片段。
重点看「学生」那份菜单:只有一个链接,但它指向的页面跟管理员是同一个文件。
HTML_TOP = '''<div class="top">成绩管理系统
<span>当前用户:{uname}({role})</span>
<a href="logout.jsp">退出</a></div>'''
def render_left(role):
tree = build(role)
out = ['<ul class="menu">']
for top, subs in tree:
out.append(' <li>' + top)
out.append(' <ul>')
for s in subs:
out.append(' <li><a target="main" href="%s">%s</a></li>'
% (PAGE[s], s))
out.append(' </ul>')
out.append(' </li>')
out.append('</ul>')
return '\n'.join(out)
PAGE = {'班级设置': 'class_set.jsp', '班级学生名单': 'class_list.jsp',
'成绩录入': 'score_in.jsp', '成绩查询': 'score_query.jsp'}
print(render_left('学生'))
实跑输出:
--------------------------------------------------------------------
主窗体:菜单是权限表的投影
--------------------------------------------------------------------
① 布局:一个框架页 + 一个内容页
--------------------------------------------------------------------
| 部分 | 文件 | 作用 |
|---|---|---|
| 顶栏 | top.jsp | 系统名 + 当前用户 + 退出 |
| 左菜单 | left.jsp | 按角色生成的菜单树 |
| 内容区 | main.jsp | iframe 或 include 加载功能页 |
| 框架页 | index.jsp | 把上面三块拼起来 |
iframe 和 include 的区别要分清。 iframe 加载的是「另一个页面」,地址栏不变;
include 是「把另一个页面的内容拼进来」,每个功能页都要重新拼一次框架。
教材用的是 iframe,好处是简单、菜单不用重画;坏处是「后退按钮不能用」,而且对搜索引擎不友好。
现代做法是「单页应用」,但那是另一个话题。
② 菜单按角色算:三份菜单真跑
--------------------------------------------------------------------
[管理员] 一级 4 个,二级 9 个
班级管理:班级设置、班级学生名单
学生管理:学生信息维护、批量导入
成绩管理:成绩录入、成绩修改、成绩查询
系统管理:用户与角色、数据导出
[教师] 一级 2 个,二级 4 个
班级管理:班级学生名单
成绩管理:成绩录入、成绩修改、成绩查询
[学生] 一级 1 个,二级 1 个
成绩管理:成绩查询
「学生」只剩「成绩查询」一项,而且这一项对三种角色是同一个页面。
同一个页面,三种数据范围——页面不区分角色,WHERE 区分。
如果按角色写三个页面,改一个需求要改三处,而且一定会漏。
「一个页面 + 数据范围」是这一章最省事的做法。
③ 页面顶部那一行:从 Session 取,不从 URL 取
--------------------------------------------------------------------
[教材原码 · 本沙箱不执行]
| <%
| String role = (String) session.getAttribute("role");
| if (role == null) {
| response.sendRedirect("login.jsp"); // 没登录,踢回去
| return;
| }
| String uname = (String) session.getAttribute("uname");
| %>
| <span>当前用户:<%= uname %>(<%= role %>)</span>
这五行每个页面都要有;漏一个页面,那个页面就能匿名访问。
「每个页面都写一遍」这件事本身就是隐患。
所以标准做法是写成 Filter,在 web.xml 里配一句「所有 /*.jsp 都过这个过滤器」,全站一次生效。
漏一个页面的后果,比想象中严重。
比如「删除学生」那个页面忘了校验,任何人敲 URL 都能删——而且日志里看不出来,
因为请求是「合法」的。
④ 三个入口页面各自该有什么
--------------------------------------------------------------------
| 角色 | 首屏放什么 | 为什么 |
|---|---|---|
| 管理员 | 四个模块的入口 + 待办(未录成绩的班) | 要一眼看到全貌 |
| 教师 | 我带的班 + 最近一次录入时间 | 进来就是为了录分 |
| 学生 | 本学期成绩单 | 进来就是为了看分 |
三个首屏,功能复杂度差十倍。 但这不是「简配」,是「按职责给信息」。
给学生的首屏放四个模块入口,他一个都点不了,只会以为自己没权限。
target="main" 这个属性把链接指到 iframe 的 name 上。
这是整个框架能工作的关键一句——少了它,点菜单会在整个窗口里打开新页面,框架就被替换掉了。
PAGE 这张映射表就是「菜单项 → 页面文件」。
加功能时最容易漏的是它,因为漏了之后菜单照样显示,点了没反应。
渲染出来的 HTML 里没有「按角色隐藏按钮」这类东西。
因为「能不能看这个菜单」已经决定了「能不能进这个页面」,
页面内部的按钮权限,是另一层(而且要配合后端校验)。
30 秒实验
- 把
target="main"去掉,想想会出什么事? 框架被整个替换掉,菜单消失。 - 给「学生」加上「成绩录入」,看渲染出的 HTML 有什么问题?
菜单有了,但
PAGE里没有对应文件 → 404。 - 想一想:如果菜单有 50 项,
PAGE表要不要手工维护? 该由「菜单表」和「页面表」在数据库里维护,否则每次加功能都要改代码。
常见误区
- 「每个页面自己写登录校验就行」。
迟早漏一个。 用
Filter一次搞定。 - 「
sendRedirect之后不用return」。 代码会继续往下跑,可能已经把数据写进库了。 - 「三个角色三套首页没必要,统一一个就行」。 学生进来看四个点不了的入口,只会以为自己没权限。
自测三问(点开对答案)
1. `iframe` 和 `include` 的区别? —— **`iframe` 加载另一个页面,地址栏不变、菜单不用重画**; **`include` 把内容拼进来,每个功能页都要重新拼框架。** 2. `sendRedirect` 之后为什么必须 `return`? —— **否则后面的代码会继续执行**,**可能报空指针,也可能数据已经写进去了。** 3. 为什么一级节点下没内容就要整块隐藏? —— **否则留一个点开是空白的菜单。** **空菜单比没有菜单更让人困惑。**下一篇 20.8,第一个业务模块: 班级设置——看着最简单,但「删不掉」的三种情况和「改个班名影响谁」这两件事,比想象中绕。
- ☐ (本篇完,下一篇 20.8 班级信息设置模块设计)