浏览器编程
在 p03,学员的平均分已经算出来,但结果只显示在 Console。怎样把这个结果放到学员管理页面上,并在刷新后重新显示?本节先完成这个最小的“数据落页”任务。
接下来,学员管理页面还需要解决下面的问题:
- 找到搜索框、按钮、表格等页面元素。
- 读取表单内容,根据用户操作修改页面。
- 保存少量浏览器数据。
- 调用 Web API,显示服务器返回的学员信息。
- 请求失败时,给出明确提示。
这些能力都属于浏览器编程。
前置知识
进入本部分前,应当已经能够:
- 使用 HTML 编写标题、列表、表格和表单。
- 使用 CSS 选择器、盒模型和 Flex 完成静态布局。
- 使用
const、let、条件、循环和函数处理数据。 - 使用数组对象、
map()、filter()、find()、Map和Set。 - 在 Console 中阅读错误并查看中间结果。
如果纯数据筛选仍不熟悉,先复习 p03c08。本部分会直接把已有数据处理函数接到页面和接口,不再重复讲解基础语法。
浏览器运行环境
同样是 JavaScript,运行环境不同,可以使用的功能也不同。
| 运行环境 | 常见对象和能力 |
|---|---|
| 浏览器 | window、document、DOM、事件、localStorage、fetch |
| Node.js | 文件、进程、服务器等 Node.js API |
本部分学习浏览器中的 JavaScript。async 和 await 属于 ECMAScript 语言语法,fetch 是浏览器 Web API;课程把异步页面状态和请求相邻安排,便于完成一条学员数据加载任务链。
打开浏览器开发者工具,在 Console 中输入:
console.log(window);
console.log(document);window 表示当前浏览器窗口。document 表示当前页面,也是后面查询 HTML 元素的入口。
DOM 创建与页面渲染
浏览器打开 HTML 后,大致会完成下面几步:
JavaScript 并不是直接修改 HTML 文件,而是修改浏览器内存中的 DOM。刷新页面后,浏览器会重新读取原始 HTML,所以临时修改通常会消失。
标题文本修改
下面的页面中有一个标题。JavaScript 查询标题后,再修改它的文字。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>浏览器编程</title>
</head>
<body>
<h1 id="page-title">学员管理</h1>
<script>
const pageTitle = document.querySelector("#page-title");
pageTitle.textContent = "学员信息管理系统";
</script>
</body>
</html>运行页面后,标题会从“学员管理”变成“学员信息管理系统”。
这段代码包含两个步骤:
- 使用
document.querySelector()找到元素。 - 修改元素的
textContent。
后面的页面展示与交互,会在“查询元素”和“操作元素”的基础上继续扩展。
学员数据摘要展示
p03c08 中,matchedStatistics.averageScore 已经算出了正常状态学员的平均分,className 保存了班级名称。上一节只在 Console 中查看结果;这里把两个变量分别写入预设的页面位置:
className → <h2 id="class-name">
averageScore → <span id="average-score">页面占位元素
HTML 先写好两个目标元素。h2 用来显示班级名称,span 用来显示平均分:
<h1>学员数据摘要</h1>
<h2 id="class-name">等待班级名称</h2>
<p>学员平均分:<span id="average-score">等待计算</span></p>查询与文本写入
const classTitle = document.querySelector("#class-name");
const averageScoreText = document.querySelector("#average-score");
classTitle.textContent = className;
averageScoreText.textContent = `${averageScore} 分`;这里的 className 和 averageScore 沿用 p03c08 已经得到的数据;新增的只有两次查询和两次文本写入:
querySelector("#class-name")找到唯一的<h2>。textContent = className把班级名称写入<h2>。querySelector("#average-score")找到唯一的<span>。textContent = ...把平均分写入<span>。
运行后,页面会显示“FastAPI Web 1 班”和“学员平均分:82 分”。这就是最小的“数据 → 页面”流程。此时不需要理解父子节点、兄弟节点或动态创建元素;完整的 DOM 树、选择器组合和关系查找放到下一节。
运行与验收
完整示例见学员数据落页页面(源码文件:./.code/p04/c01-intro/02-data-to-page.html)。HTML 通过模块入口加载本节脚本,脚本再导入 p03c08 的 data.js,因此页面使用上一节实际计算出的 averageScore,不依赖全局变量。请保留附件与 p03c08 的 data.js 在课程目录中的相对位置,否则导入路径会失效。
在课程根目录运行 python -m http.server 5500,再打开 http://localhost:5500/.code/p04/c01-intro/02-data-to-page.html。核对页面是否显示“FastAPI Web 1 班”和“学员平均分:82 分”,刷新后是否仍显示相同结果。若页面停在“等待班级名称”或“等待计算”,先在 Console 中检查模块导入路径和脚本错误。
控制台数据落页源码
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>控制台数据落页</title>
<script type="module" src="./02-data-to-page.js"></script>
</head>
<body>
<h1>学员数据摘要</h1>
<h2 id="class-name">等待班级名称</h2>
<p>学员平均分:<span id="average-score">等待计算</span></p>
</body>
</html>import { averageScore, className } from "../../p03/c08-data-case/01-data-case/data.js";
const classTitle = document.querySelector("#class-name");
const averageScoreText = document.querySelector("#average-score");
classTitle.textContent = className;
averageScoreText.textContent = `${averageScore} 分`;export const students = [
{
id: 1001,
name: "张三",
score: 86,
status: "active",
course: "web",
contact: { mobile: "13800000001" },
},
{
id: 1002,
name: "李四",
score: 72,
status: "active",
course: "python",
contact: { mobile: "13800000002" },
},
{
id: 1003,
name: "王五",
score: 93,
status: "disabled",
course: "web",
contact: null,
},
{
id: 1004,
name: "赵敏",
score: 58,
status: "pending",
course: "web",
contact: { mobile: "13800000004" },
},
{
id: 1005,
name: "陈晨",
score: 88,
status: "active",
course: "data",
contact: { mobile: "13800000005" },
},
];
const allowedStatuses = new Set(["active", "pending", "disabled"]);
export const isValidStudent = (student) => {
if (student === null || typeof student !== "object" || Array.isArray(student)) {
return false;
}
const hasValidId = Number.isInteger(student.id) && student.id > 0;
const hasValidName =
typeof student.name === "string" && student.name.trim().length >= 2;
const hasValidScore =
Number.isFinite(student.score) &&
student.score >= 0 &&
student.score <= 100;
const hasValidStatus = allowedStatuses.has(student.status);
const hasValidCourse =
typeof student.course === "string" && student.course.trim().length > 0;
const hasValidContact =
student.contact === null ||
(typeof student.contact === "object" &&
!Array.isArray(student.contact) &&
typeof student.contact.mobile === "string");
return hasValidId && hasValidName && hasValidScore &&
hasValidStatus && hasValidCourse && hasValidContact;
};
export const filterStudents = (
studentList,
{ status = null, course = null, keyword = "" } = {},
) => {
const normalizedKeyword = keyword.trim().toLowerCase();
return studentList.filter((student) => {
const matchesStatus = status === null || student.status === status;
const matchesCourse = course === null || student.course === course;
const matchesKeyword =
normalizedKeyword === "" ||
student.name.toLowerCase().includes(normalizedKeyword) ||
student.contact?.mobile?.includes(normalizedKeyword);
return matchesStatus && matchesCourse && matchesKeyword;
});
};
export const createStudentIndex = (studentList) => {
const studentById = new Map();
for (const student of studentList) {
studentById.set(student.id, student);
}
return studentById;
};
export const calculateStatistics = (studentList) => {
if (studentList.length === 0) {
return {
count: 0,
averageScore: 0,
highestScore: null,
passedCount: 0,
passRate: 0,
};
}
const totals = studentList.reduce(
(result, student) => ({
totalScore: result.totalScore + student.score,
highestScore: Math.max(result.highestScore, student.score),
passedCount: result.passedCount + (student.score >= 60 ? 1 : 0),
}),
{ totalScore: 0, highestScore: -Infinity, passedCount: 0 },
);
return {
count: studentList.length,
averageScore: Number(
(totals.totalScore / studentList.length).toFixed(1),
),
highestScore: totals.highestScore,
passedCount: totals.passedCount,
passRate: Number(
((totals.passedCount / studentList.length) * 100).toFixed(1),
),
};
};
const getScoreLevel = (score) => {
if (score >= 90) return "优秀";
if (score >= 80) return "良好";
if (score >= 60) return "合格";
return "需要补考";
};
const statusTextMap = {
active: "正常",
pending: "待审核",
disabled: "已停用",
};
export const createStudentSummary = (student) => ({
id: student.id,
name: student.name,
level: getScoreLevel(student.score),
statusText: statusTextMap[student.status] ?? "未知",
mobile: student.contact?.mobile ?? "暂无手机号",
});
export const className = "FastAPI Web 1 班";
const query = {
status: "active",
course: null,
keyword: "",
};
export const allStudentsValid = students.every(isValidStudent);
const matchedStudents = allStudentsValid ? filterStudents(students, query) : null;
export const matchedSummaries = matchedStudents?.map(createStudentSummary) ?? null;
export const matchedStatistics = matchedStudents === null
? null
: calculateStatistics(matchedStudents);
export const averageScore = matchedStatistics?.averageScore ?? null;
export const studentById = allStudentsValid ? createStudentIndex(students) : null;
export const courseCodes = allStudentsValid
? new Set(students.map((student) => student.course))
: null;脚本加载时机
如果 JavaScript 执行时,页面元素还没有创建,查询结果会是 null。
<head>
<script>
const pageTitle = document.querySelector("#page-title");
console.log(pageTitle); // null
</script>
</head>
<body>
<h1 id="page-title">学员管理</h1>
</body>初学阶段可以把 <script> 放在 </body> 前面。项目中更推荐使用外部 JavaScript 文件,并添加 defer:
<head>
<script src="./js/student.js" defer></script>
</head>对于普通外部脚本,defer 表示先继续解析 HTML,等 DOM 创建完成后再执行脚本。它还能保持多个普通外部脚本的执行顺序。上面的数据落页附件使用 type="module";模块脚本默认在 HTML 解析完成后执行,不必再加 defer。
p04 学习路线
学习时先使用本地数组完成页面功能,再连接课程提供的测试 API。这样可以分清页面问题和接口问题。
| 学习阶段 | 可以完成的页面任务 |
|---|---|
| c01–c04:查询与渲染 | 把 p03 的学员数据和统计结果显示到页面 |
| c05–c07:事件与表单 | 搜索、校验表单,处理动态行按钮 |
| c08:浏览器存储 | 刷新后保留本地待办事项 |
| c09–c12:异步与 API | 查询、分页、写入和上传,处理加载与失败状态 |
每个阶段先完成当前任务,再进入下一阶段。完成本部分后,你将能在浏览器中使用原生 JavaScript 完成学员查询、写入与头像上传练习。
浏览器调试入口
后面的案例需要同时使用三个面板:
| 面板 | 主要用途 |
|---|---|
| Elements | 检查元素结构、class 和最终样式 |
| Console | 查看输出和 JavaScript 错误 |
| Network | 检查 API 地址、参数、状态码和响应数据 |
页面没有反应时,先看 Console 是否有红色错误。接口没有数据时,再到 Network 检查请求。
小结
本节课先掌握三件事:
- 浏览器提供了 DOM、事件、存储和网络请求等 API。
document是查询和操作页面元素的入口。- 普通外部脚本用
defer等待 DOM 创建;模块脚本默认在 HTML 解析完成后执行。