Skip to content

浏览器编程 ​

在 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 中输入:

js
console.log(window);
console.log(document);

window 表示当前浏览器窗口。document 表示当前页面,也是后面查询 HTML 元素的入口。

DOM 创建与页面渲染 ​

浏览器打开 HTML 后,大致会完成下面几步:

mermaid Diagram

JavaScript 并不是直接修改 HTML 文件,而是修改浏览器内存中的 DOM。刷新页面后,浏览器会重新读取原始 HTML,所以临时修改通常会消失。

标题文本修改 ​

下面的页面中有一个标题。JavaScript 查询标题后,再修改它的文字。

html
<!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>

运行页面后,标题会从“学员管理”变成“学员信息管理系统”。

这段代码包含两个步骤:

  1. 使用 document.querySelector() 找到元素。
  2. 修改元素的 textContent。

后面的页面展示与交互,会在“查询元素”和“操作元素”的基础上继续扩展。

学员数据摘要展示 ​

p03c08 中,matchedStatistics.averageScore 已经算出了正常状态学员的平均分,className 保存了班级名称。上一节只在 Console 中查看结果;这里把两个变量分别写入预设的页面位置:

text
className    →  <h2 id="class-name">
averageScore →  <span id="average-score">

页面占位元素 ​

HTML 先写好两个目标元素。h2 用来显示班级名称,span 用来显示平均分:

html
<h1>学员数据摘要</h1>
<h2 id="class-name">等待班级名称</h2>
<p>学员平均分:<span id="average-score">等待计算</span></p>

查询与文本写入 ​

js
const classTitle = document.querySelector("#class-name");
const averageScoreText = document.querySelector("#average-score");

classTitle.textContent = className;
averageScoreText.textContent = `${averageScore} 分`;

这里的 className 和 averageScore 沿用 p03c08 已经得到的数据;新增的只有两次查询和两次文本写入:

  1. querySelector("#class-name") 找到唯一的 <h2>。
  2. textContent = className 把班级名称写入 <h2>。
  3. querySelector("#average-score") 找到唯一的 <span>。
  4. 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 中检查模块导入路径和脚本错误。

控制台数据落页源码
html
<!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>
js
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} 分`;
js
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。

html
<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:

html
<head>
  <script src="./js/student.js" defer></script>
</head>

对于普通外部脚本,defer 表示先继续解析 HTML,等 DOM 创建完成后再执行脚本。它还能保持多个普通外部脚本的执行顺序。上面的数据落页附件使用 type="module";模块脚本默认在 HTML 解析完成后执行,不必再加 defer。

p04 学习路线 ​

mermaid Diagram

学习时先使用本地数组完成页面功能,再连接课程提供的测试 API。这样可以分清页面问题和接口问题。

学习阶段可以完成的页面任务
c01–c04:查询与渲染把 p03 的学员数据和统计结果显示到页面
c05–c07:事件与表单搜索、校验表单,处理动态行按钮
c08:浏览器存储刷新后保留本地待办事项
c09–c12:异步与 API查询、分页、写入和上传,处理加载与失败状态

每个阶段先完成当前任务,再进入下一阶段。完成本部分后,你将能在浏览器中使用原生 JavaScript 完成学员查询、写入与头像上传练习。

浏览器调试入口 ​

后面的案例需要同时使用三个面板:

面板主要用途
Elements检查元素结构、class 和最终样式
Console查看输出和 JavaScript 错误
Network检查 API 地址、参数、状态码和响应数据

页面没有反应时,先看 Console 是否有红色错误。接口没有数据时,再到 Network 检查请求。

小结 ​

本节课先掌握三件事:

  1. 浏览器提供了 DOM、事件、存储和网络请求等 API。
  2. document 是查询和操作页面元素的入口。
  3. 普通外部脚本用 defer 等待 DOM 创建;模块脚本默认在 HTML 解析完成后执行。