UCB-全栈开发笔记-全-
UCB 全栈开发笔记(全)
001:课程介绍与基础工具入门 🚀
在本节课中,我们将学习全栈开发课程的总体介绍、你将学到的核心内容、课程结构以及开始学习前需要准备的基础工具。我们将通过简单的示例快速了解HTML、CSS和JavaScript的基本概念,并学习如何使用Git进行版本控制。
课程概述
本课程旨在教授全栈开发的基础知识。你将学习构建现代网站和应用程序所需的核心技术栈,包括前端和后端开发。
课程内容与结构
上一节我们介绍了课程的整体目标,本节中我们来看看具体的课程内容安排。
本课程将分为三个主要部分进行教学:
- 基础技术栈:学习HTML、CSS、JavaScript以及互联网工作原理。
- 前端开发:学习React JS库以及用户交互与用户体验设计原则。
- 后端开发:学习框架、数据库和云技术。
以下是课程的技术栈布局:
- 基础技术:HTML、CSS、JavaScript。
- 前端库:React、Chakra UI。
- 后端技术:Node.js、REST APIs、MongoDB。
本课程为3个学分,预计每周投入6-10小时。课程包含大量课程计划、两个项目(其中一个为期末项目)、一个实验和大约九次作业。
课程管理与支持
了解了学习内容后,我们来看看课程如何进行管理以及如何获得帮助。
所有课程资料、教学大纲和作业都将发布在全栈开发课程网站上。讲座时间为每周一和周三晚上8点至9点30分。
我们提供了大量的办公时间,助教和课程协调员会确保你在课程中取得成功。请务必查看Ed讨论板的礼仪规范,并加入以获得最新的课程更新和截止日期提醒。


以下是课程评分细则:
- 出勤是强制性的。
- 评分将基于作业、项目、出勤等因素综合评定。
技术栈快速预览
在深入细节之前,我们先快速预览一下构成全栈开发基础的三大核心技术。
HTML 简介


HTML用于构建网页的结构。它使用标签来定义内容,如标题、段落和图片。
一个基本的HTML文档结构如下:
<!DOCTYPE html>
<html>
<head>
<title>页面标题</title>
</head>
<body>
<h1>这是一个大标题</h1>
<p>这是一个段落。</p>
</body>
</html>
<body>标签包含了页面上显示的所有内容。<h1>和<p>是<body>的子组件。
CSS 简介
CSS用于设置HTML的样式,控制网页的外观,包括字体、颜色、布局和动画。
一个简单的CSS示例如下,它将改变段落文本的颜色并添加下划线:
p {
color: red;
text-decoration: underline;
}
h1 {
color: blue;
}

为了让HTML页面应用CSS样式,需要在HTML的<head>部分使用<link>标签引入CSS文件:
<link rel="stylesheet" type="text/css" href="example.css">


JavaScript 简介

JavaScript是一种高级编程语言,用于为网站添加交互行为。例如,点击按钮跳转页面就是由JavaScript实现的。
一个基本的JavaScript函数示例如下:
const weight = 10;
const gravity = 9.8;
function calculateForce(w, g) {
return w * g;
}

开发工具准备

要成为一名优秀的网页开发者,你需要准备一些必要的工具。
网页浏览器
推荐使用Chrome浏览器,因为大多数用户都使用基于Chromium的浏览器,这有助于确保你开发的网站在不同环境下表现一致。

代码编辑器
推荐使用VS Code。许多网页开发者都使用它,并且它有丰富的插件生态系统,可以提供代码自动补全等多种辅助功能。
终端与Git

对于GUI终端模拟器,Mac用户可使用iTerm2,Windows用户可使用Windows Terminal。重要的是,应使用Bash类shell,因为许多网页开发命令行工具都基于此。Windows用户可能需要安装WSL。
Git是一个版本控制系统,用于跟踪代码的变更历史。以下是几个核心Git命令:

git init:初始化一个新的Git仓库。git add .:将当前目录下的所有文件添加到暂存区。git commit -m “提交信息”:将暂存区的更改保存为一个快照(提交)。git push:将本地提交推送到远程仓库(如GitHub)。git status:查看当前仓库的状态(哪些文件已修改、已暂存等)。
使用Git的好处是,你可以保存代码在不同时间点的状态。如果你的电脑丢失,推送到GitHub的最后一个提交的代码仍然安全。
总结

本节课中我们一起学习了全栈开发课程的概览,快速了解了HTML、CSS和JavaScript在网页开发中的基本角色,并介绍了开始学习前需要安装和熟悉的核心开发工具(浏览器、VS Code、终端和Git)。这些基础知识将为你后续的深入学习打下坚实的基础。
002:HTML基础入门

在本节课中,我们将学习网页开发的基础——HTML。HTML是构建网页的骨架,负责创建网页上的文本、图片、链接等所有可见元素。我们将从HTML的基本结构开始,逐步学习各种常用标签的语法和用法,并通过一个动手实践环节来巩固所学知识。
HTML概述与基本结构
HTML,全称超文本标记语言,是网页开发的三大基石之一。它的核心作用是定义网页的结构和内容。CSS负责样式和美化,JavaScript则负责添加交互功能。
一个标准的HTML文档包含以下基本骨架结构:
<!DOCTYPE html>
<html>
<head>
<title>页面标题</title>
<!-- 元数据,如页面描述、关键词等 -->
</head>
<body>
<!-- 网页的主体内容在这里编写 -->
</body>
</html>
<!DOCTYPE html>:声明文档类型,告诉浏览器这是一个HTML5文档。<html>:根元素,包裹整个HTML文档。<head>:头部,包含页面的元信息(如标题、字符集、引入的CSS/JS文件链接),这些内容不会直接显示在页面上。<body>:主体,包含所有会在浏览器中显示的内容,如文本、图片、列表等。
在VS Code等编辑器中,输入 ! 然后按Tab键,可以快速生成这个基础结构。
HTML标签语法与常用文本标签
上一节我们介绍了HTML文档的整体结构,本节中我们来看看构成内容的基本单元——标签的语法,以及如何创建文本。
HTML标签通常成对出现,由开始标签和结束标签组成,内容写在两者之间。语法如下:
<标签名>内容</标签名>
以下是创建文本的常用标签:
- 标题标签 (
<h1>到<h6>): 用于定义标题,<h1>最大,<h6>最小。<h1>这是一级标题</h1> <h6>这是六级标题</h6> - 段落标签 (
<p>): 用于定义段落文本。<p>这是一个段落。</p> - 换行标签 (
<br>): 强制换行,是一个自闭合标签(没有结束标签)。第一行<br>第二行 - 水平线标签 (
<hr>): 创建一条水平分割线,也是自闭合标签。<hr>
创建列表
在网页中,我们经常需要展示项目列表。HTML提供了两种主要的列表类型。
以下是创建列表的标签:
- 无序列表 (
<ul>): 项目以圆点等符号开头。<ul> <li>项目一</li> <li>项目二</li> </ul> - 有序列表 (
<ol>): 项目以数字或字母顺序编号。<ol> <li>第一步</li> <li>第二步</li> </ol> - 列表项 (
<li>): 用于定义<ul>或<ol>中的每一个项目。


插入图片与创建链接
现在我们已经可以创建文本和列表,接下来学习如何让网页内容更丰富——添加图片和链接。
图片标签 (<img>)


<img> 标签用于在页面中嵌入图像,它是一个自闭合标签。有两个重要属性:

src: 指定图片的来源。可以是网络URL,也可以是本地文件的相对路径。alt: 提供图片的替代文本。当图片无法加载时显示,也对屏幕阅读器用户友好。


<!-- 使用网络链接 -->
<img src="https://example.com/image.jpg" alt="图片描述">
<!-- 使用本地相对路径 (假设图片与HTML文件在同一目录) -->
<img src="myphoto.jpg" alt="我的照片">

关于相对路径:如果图片放在名为 images 的文件夹中,而HTML文件在上一级目录,路径应写为 src="../images/myphoto.jpg"。../ 表示返回上一级目录。

链接标签 (<a>)


<a> 标签(锚标签)用于创建超链接。核心属性是 href,用于指定链接的目标地址。
<a href="https://www.example.com">访问示例网站</a>
默认情况下,点击链接会在当前窗口打开新页面。如果希望在新标签页打开,可以添加 target="_blank" 属性。
<a href="https://www.example.com" target="_blank">在新标签页打开示例网站</a>


你可以将任何内容(包括文本、图片甚至按钮)放在 <a> 标签内来使其可点击。

表单与输入控件

表单是网页与用户交互的关键组件,用于收集用户输入的数据,如登录信息、搜索查询等。


表单容器 (<form>)




<form> 标签用于定义一个表单区域,内部可以包含各种输入控件。


输入控件 (<input>)
<input> 标签是最常用的表单控件,通过 type 属性定义其类型。它是一个自闭合标签。

以下是几种常见的输入类型:


- 文本输入 (
type="text"): 单行文本字段。<input type="text" placeholder="请输入用户名">placeholder属性提供输入提示。 - 密码输入 (
type="password"): 输入内容会被隐藏为圆点。<input type="password" placeholder="请输入密码"> - 单选按钮 (
type="radio"): 用于多选一的情况。相同name的 radio 为一组。<input type="radio" name="gender" value="male"> 男 <input type="radio" name="gender" value="female"> 女 - 复选框 (
type="checkbox"): 用于多选多的情况。<input type="checkbox" name="hobby" value="reading"> 阅读 - 提交按钮 (
type="submit"): 用于提交表单数据。<input type="submit" value="提交表单">value属性定义了按钮上显示的文字。

按钮标签 (<button>)
<button> 标签用于创建可点击的按钮,通常与JavaScript配合实现功能。也可以放在 <a> 标签内作为链接按钮。


<button>点击我</button>
<!-- 作为链接按钮 -->
<a href="https://example.com"><button>去往示例网站</button></a>




动手实践:构建你的第一个HTML页面


理论部分已经介绍完毕,现在让我们通过一个完整的练习来巩固所学知识。请跟随以下步骤,在本地创建一个简单的HTML页面。
-
创建项目文件夹和文件
- 打开终端,进入桌面:
cd Desktop - 创建一个新文件夹:
mkdir html-practice - 进入该文件夹:
cd html-practice - 创建HTML文件:
touch index.html - 用VS Code打开该文件夹:
code .(或直接从VS Code界面打开)
- 打开终端,进入桌面:
-
编写基础HTML结构
- 在
index.html文件中,输入!然后按Tab键,生成基础代码。 - 将
<title>中的内容改为“我的第一个网页”。
- 在

- 在
<body>中添加内容- 添加一个一级标题:
<h1>欢迎来到我的网页</h1> - 添加一个水平线:
<hr> - 创建一个无序列表,列出你的爱好:
<ul> <li>听音乐</li> <li>阅读</li> <li>运动</li> </ul> - 添加一个换行
<br>,然后创建一个指向你常用网站(如百度)的链接:<a href="https://www.baidu.com" target="_blank">访问百度</a> - 再添加一个换行,然后插入一张图片(可以从网上找一张,使用图片链接):
<img src="图片链接地址" alt="风景图片"> - 添加一个水平线,然后创建一个简单的登录表单:
<form> <input type="text" placeholder="邮箱"><br><br> <input type="password" placeholder="密码"><br><br> <input type="submit" value="登录"> </form>
- 添加一个一级标题:

- 在浏览器中查看效果
- 保存
index.html文件。 - 在文件管理器中找到该文件,双击用浏览器打开。
- 尝试与页面交互:点击链接、在表单中输入文字等。
- 修改代码并保存后,在浏览器中刷新页面即可看到更新。
- 保存


总结
本节课中我们一起学习了HTML的基础知识。我们了解了HTML在网页开发中的角色,掌握了文档的基本结构,并实践了多种核心标签的用法:
- 结构标签:
<html>,<head>,<body> - 文本与排版标签:标题 (
<h1>-<h6>)、段落 (<p>)、换行 (<br>)、水平线 (<hr>) - 列表标签:无序列表 (
<ul>)、有序列表 (<ol>)、列表项 (<li>) - 媒体与链接标签:图片 (
<img>)、链接 (<a>) - 表单与交互标签:表单 (
<form>)、各种输入框 (<input>)、按钮 (<button>)

记住,HTML构建的是网页的“骨架”和“内容”。在接下来的课程中,我们将学习CSS来为这个骨架穿上漂亮的“外衣”,让它变得更加美观。
003:CSS基础入门 🎨


在本节课中,我们将要学习CSS的基础知识。CSS是层叠样式表的缩写,它负责网页的视觉表现,就像给HTML搭建的“骨架”房子进行装修和粉刷。没有CSS,网页将只有结构和内容,看起来会非常简陋。





什么是CSS?

上一节我们提到了CSS的作用。本节中我们来看看它的具体定义。



CSS是一种样式表语言,用于描述HTML文档的呈现方式。它控制着网页的布局、颜色、字体等视觉样式。你可以把它想象成网页的“设计师”。

一个没有加载CSS的网站(例如Facebook)看起来会非常糟糕,而加载了CSS后,页面会变得美观。

在HTML中,每个标签都可以拥有属性。例如,一个<div>标签可以有一个class属性,其值是一个字符串。这个class属性在CSS中会非常重要。
应用CSS的三种方式
现在,让我们看看如何将CSS应用到HTML元素上。主要有三种方法。
1. 内联样式
内联样式是直接将CSS规则写在HTML元素的style属性中。


<h1 style="color: red; font-weight: bold;">这是一个标题</h1>


在上面的代码中,style属性告诉浏览器这个<h1>元素需要应用CSS规则。规则写在属性值里,其语法是属性名: 属性值;,多个规则用分号隔开。

这种方式虽然直接,但会让HTML代码变得混乱,不利于维护。


2. 内部样式表

内部样式表是将CSS规则集中写在HTML文档的<style>标签内。
<style>
.paragraph-one {
color: red;
font-weight: bold;
}
</style>
<p class="paragraph-one">这是一个段落。</p>
这种方式比内联样式更整洁。我们通过给段落元素一个class="paragraph-one",然后在<style>标签内使用选择器.paragraph-one来为所有具有该类的元素统一设置样式。

3. 外部样式表

这是最推荐的方式,它将CSS代码完全分离到一个独立的.css文件中,然后在HTML中通过<link>标签引入。


style.css 文件:
.important-paragraph {
color: blue;
font-weight: bold;
}


index.html 文件:
<head>
<link rel="stylesheet" href="style.css">
</head>
<body>
<p class="important-paragraph">这是一个重要的段落。</p>
</body>


这种方式实现了内容与样式的彻底分离,使得代码结构清晰,易于管理和复用。






CSS选择器


上一节我们学会了如何引入CSS。本节中我们来看看如何精确地选中想要样式化的元素,这需要通过选择器来实现。
选择器用于指定我们要样式化的HTML元素。以下是一些核心选择器:

- 元素选择器:直接使用HTML标签名。
h1 { color: red; } /* 选中所有<h1>标签 */ - 类选择器:以一个点
.开头,后跟类名。用于选中具有特定class属性的元素。.important { color: blue; } /* 选中所有 class="important" 的元素 */ - ID选择器:以一个井号
#开头,后跟ID名。用于选中具有特定id属性的唯一元素。#main-title { font-size: 50px; } /* 选中 id="main-title" 的元素 */
核心概念:
class(类)可以被多个元素共享。id(标识符)在同一个HTML文档中应该是唯一的。



一个元素可以同时拥有多个类,用空格分隔:
<p class="important silly">这个段落有两个类。</p>
你可以用组合选择器来选中它:
.important.silly { text-decoration: underline; } /* 选中同时具有这两个类的元素 */

更复杂的选择器
除了基本选择器,CSS还支持更复杂的关系选择器,用于描述元素之间的层级关系。



以下是几种常见的关系选择器:




- 后代选择器(空格):选中某个元素内部的所有特定后代元素。
.silly-paragraph .cool-paragraph { opacity: 0.5; } /* 选中所有在 class="silly-paragraph" 的元素内部的 class="cool-paragraph" 的元素 */ - 子元素选择器(
>):仅选中某个元素的直接子元素。.silly-paragraph > .cool-paragraph { background-color: red; } /* 仅选中作为 .silly-paragraph 直接子元素的 .cool-paragraph */ - 通用选择器(
*):选中页面上的所有元素。* { margin: 0; } /* 重置所有元素的外边距 */



常用的CSS属性

现在我们已经知道如何选中元素了,接下来看看可以用哪些属性来改变它们的外观。



以下是一些最常用和基础的CSS属性:


- 颜色与背景
color: 设置文本颜色。值可以是颜色名(如red)、十六进制码(如#ff0000)、RGB值(如rgb(255, 0, 0))或RGBA值(如rgba(255, 0, 0, 0.5),最后一个参数是透明度)。background-color: 设置元素的背景颜色。background-image: 设置元素的背景图片。opacity: 设置元素的不透明度(0为完全透明,1为完全不透明)。



- 文本与字体
font-size: 设置字体大小(如16px,2rem)。font-family: 设置字体族(如Arial,"Microsoft YaHei")。font-weight: 设置字体粗细(如normal,bold)。text-align: 设置文本对齐方式(如left,center,right)。text-decoration: 设置文本装饰(如underline,line-through)。



- 尺寸与边框
width/height: 设置元素的宽度和高度。单位可以是px(像素)、%(相对于父元素的百分比)、vw/vh(相对于视口宽度/高度的百分比)。border: 设置边框。是border-width、border-style、border-color的简写属性。例如:border: 2px solid black;。border-radius: 设置边框圆角(如10px)。box-shadow: 为元素添加阴影(如box-shadow: 5px 5px 10px grey;)。


盒模型

理解CSS的盒模型是进行网页布局的基石。每个HTML元素都被视为一个矩形的“盒子”,这个盒子由内到外由四部分组成:



- 内容:显示文本、图像等的实际区域,由
width和height定义。 - 内边距:内容与边框之间的透明区域,由
padding控制。 - 边框:围绕内边距和内容的线,由
border控制。 - 外边距:盒子与其他盒子之间的透明间隔区域,由
margin控制。


公式:一个元素在页面上占据的总宽度 = margin-left + border-left + padding-left + width + padding-right + border-right + margin-right。





默认情况下,width和height只定义内容区的大小。添加padding和border会使元素的实际占用空间变大。这有时会导致布局计算困难。



为了解决这个问题,我们可以使用box-sizing属性:
* {
box-sizing: border-box;
}
当设置为border-box时,元素的width和height属性将包含内容、内边距和边框的总和。这意味着,如果你设置width: 200px;和padding: 20px;,内容区会自动调整以确保总宽度仍是200px。这极大地简化了布局工作,是推荐的实践。






开发者工具


在开发过程中,浏览器内置的开发者工具是你的得力助手。你可以通过右键点击页面元素并选择“检查”来打开它。

开发者工具允许你:
- 实时查看和修改HTML/CSS:你可以看到页面的结构,并直接修改样式,效果会立即在页面上呈现(仅在你的浏览器中)。
- 调试布局:可以直观地查看每个元素的盒模型(内容、内边距、边框、外边距),帮助你理解为什么元素会以某种方式排列。
- 测试响应式设计:可以模拟不同设备的屏幕尺寸。

其他要点与最佳实践


- 注释:在CSS中,你可以使用
/* 注释内容 */来添加注释,解释代码的作用。 - 样式优先级(层叠):当多条规则作用于同一个元素时,CSS会根据特异性和顺序来决定最终应用的样式。通常,更具体的选择器(如ID选择器)会覆盖更通用的选择器(如元素选择器)。后定义的规则会覆盖先定义的规则(在特异性相同的情况下)。
- 重置默认样式:不同浏览器对HTML元素有默认的CSS样式(例如,
<body>有默认的margin)。为了确保跨浏览器的一致性,通常会在项目开始使用一个“CSS重置”样式表来清除这些默认样式。body { margin: 0; }




本节课中我们一起学习了CSS的基础知识。我们从CSS是什么开始,介绍了三种应用CSS的方法(内联、内部、外部),深入探讨了如何使用各种选择器来精确选中元素。然后,我们学习了一系列常用的CSS属性来改变元素的外观,并理解了网页布局的核心——盒模型。最后,我们了解了如何使用开发者工具进行调试,以及一些CSS的最佳实践。



掌握这些基础知识是构建美观、结构清晰的网页的第一步。在接下来的课程中,我们将学习更复杂的CSS布局技术。
004:高级CSS 🎨


在本节课中,我们将要学习CSS的高级概念,包括元素的定位、布局方式、以及如何管理样式规则的优先级。这些知识将帮助你更精确地控制网页元素的外观和位置。
定位:控制元素在页面上的几何位置

上一节我们介绍了CSS的基础,本节中我们来看看如何精确控制元素的位置。元素的position属性决定了其定位方式。
static:默认值。元素按照正常的文档流进行排列。relative:相对定位。元素先放置在未添加定位时的位置,然后可以通过设置top、left等属性,相对于其原始位置进行移动。公式:position: relative; top: 50px; left: 50px;absolute:绝对定位。元素相对于其最近的非static定位的祖先元素进行定位。它不再占据文档流中的空间。fixed:固定定位。元素相对于浏览器窗口进行定位,即使页面滚动,它也会停留在固定位置。sticky:粘性定位。元素根据用户的滚动位置在relative和fixed定位之间切换。

显示模式:控制元素的布局方式



理解了如何定位单个元素后,我们来看看如何控制一组元素的排列方式。display属性决定了元素如何布局。


block:块级元素。元素独占一行,默认宽度为父元素的100%。例如<div>。inline:行内元素。元素在一行内水平排列,宽度由其内容决定。例如<span>。grid:网格布局。将元素容器定义为一个网格,可以创建复杂的二维布局。代码示例:.container { display: grid; grid-template-columns: auto auto auto; /* 创建三列 */ }flex:弹性盒子布局。提供一种更有效的方式来对容器内的项目进行对齐和分配空间,尤其适用于一维布局。代码示例:.container { display: flex; flex-direction: row; /* 项目水平排列 */ }





选择器特异性与层叠:解决样式冲突


当多个CSS规则同时应用于同一个元素时,浏览器需要决定最终采用哪个样式。这由特异性和层叠规则决定。
特异性是一个计算权重值的过程,用于确定哪个选择器更具体、优先级更高。规则如下:


以下是计算特异性权重的要点:
- 内联样式(如
style="color: red;"):特异性最高。 - ID选择器(如
#header):每个ID加100点。 - 类选择器、属性选择器、伪类(如
.class,[type="text"],:hover):每个加10点。 - 元素选择器、伪元素(如
div,::before):每个加1点。 - 通用选择器(
*)和继承的样式:特异性为0。






层叠规则指出,当特异性相同时,后定义的样式会覆盖先定义的样式。




响应式设计与动画:让页面更生动
最后,我们简要了解两个让网页更现代和动态的特性。

媒体查询允许你根据设备特性(如屏幕宽度)应用不同的样式,这是实现响应式网页设计的基础。代码示例:
@media screen and (max-width: 600px) {
body {
background-color: lightblue;
}
}
CSS动画允许你无需JavaScript即可创建平滑的过渡效果。它通过定义关键帧(@keyframes)来实现。代码示例:
@keyframes example {
from {background-color: red;}
to {background-color: yellow;}
}
.element {
animation-name: example;
animation-duration: 2s;
}
总结



本节课中我们一起学习了CSS的核心高级概念。我们掌握了如何使用position属性进行精确定位,了解了display属性如何控制布局(包括强大的Flexbox和Grid),学会了通过特异性和层叠规则解决样式冲突,并初步接触了媒体查询和CSS动画。掌握这些知识将使你能够构建布局更精巧、交互更丰富的网页。
005:JavaScript 基础入门 🚀

在本节课中,我们将要学习JavaScript的基础知识。JavaScript是前端开发的三大核心技术之一,它让网页从静态的文档变为可以交互的动态应用。我们将从如何在HTML中引入JavaScript开始,逐步学习变量、数据类型、函数、对象等核心概念。
如何在网页中运行JavaScript?
上一节我们介绍了JavaScript的作用,本节中我们来看看如何在网页中运行它。
与CSS类似,JavaScript可以通过两种方式嵌入到HTML中。
1. 内联方式:使用 <script> 标签

你可以直接在HTML文件中使用 <script> 标签来编写JavaScript代码。
<!DOCTYPE html>
<html>
<head>
<title>我的网页</title>
</head>
<body>
<h1>你好,世界!</h1>
<script>
// 这里是JavaScript代码
console.log("这段代码在控制台运行");
</script>
</body>
</html>
2. 外部文件方式:使用 src 属性


你也可以将JavaScript代码写在一个独立的 .js 文件中,然后在HTML中引用它。这有助于保持代码的整洁和可维护性。
<!DOCTYPE html>
<html>
<head>
<title>我的网页</title>
</head>
<body>
<h1>你好,世界!</h1>
<script src="myscript.js"></script>
</body>
</html>

注意:<script> 标签的位置很重要。如果脚本需要操作页面上的元素(例如,修改 <body> 的内容),通常需要将 <script> 标签放在 <body> 标签的末尾,或者使用 defer 等属性,以确保在DOM元素加载完成后再执行脚本。
变量与常量
现在我们知道如何运行JavaScript了,接下来学习如何存储数据。在JavaScript中,我们使用变量和常量。
声明变量有三种方式:let、const 和 var。
let:用于声明一个可以重新赋值的变量。这是现代JavaScript中的推荐用法。const:用于声明一个常量,其值在声明后不能被重新赋值。var:是旧的声明方式,现在已不推荐使用。它与let在作用域上有细微差别。
以下是声明变量的示例:
let myVariable = 10; // 可以改变
myVariable = 20; // 正确
const myConstant = 42; // 不可以改变
// myConstant = 50; // 错误!会报错
var oldVariable = "过时了"; // 避免使用
命名约定:变量名通常使用驼峰命名法,例如 myVariableName。
数据类型与运算符
变量可以存储不同类型的数据。JavaScript是一种“动态类型”语言,这意味着你不需要提前声明变量的类型。
以下是主要的数据类型:
- 数字:整数或小数,例如
42,3.14。 - 字符串:文本,用单引号
‘’或双引号“”包裹,例如‘Hello’。 - 布尔值:
true或false。 - 对象:用于存储键值对的集合,我们稍后会详细讲解。
- 数组:用于存储有序的元素列表,我们稍后会详细讲解。
- Undefined:表示变量已声明但未赋值。
- Null:表示一个空值。
JavaScript可以自动在不同类型之间转换,这有时很方便,但有时也会导致意想不到的结果。
let myNumber = 42;
let myString = “答案是:” + myNumber; // JavaScript 将数字转换为字符串并拼接
console.log(myString); // 输出:“答案是:42”
let result = “5” - 3; // JavaScript 将字符串“5”转换为数字5
console.log(result); // 输出:2
比较运算符:比较两个值时,建议使用严格相等 === 和严格不相等 !==。它们会同时比较值和数据类型。
console.log(5 == “5”); // true (宽松相等,只比较值)
console.log(5 === “5”); // false (严格相等,比较值和类型)
控制流:条件与循环
有了数据和变量,我们就可以让程序做决策和重复任务了。控制流语句和Python非常相似。
条件语句 (if...else)
使用 if、else if 和 else 来根据条件执行不同的代码块。代码块用花括号 {} 包裹。
let score = 85;
if (score >= 90) {
console.log(“优秀!”);
} else if (score >= 60) {
console.log(“及格!”);
} else {
console.log(“不及格!”);
}
循环语句 (for, while)
for 循环和 while 循环用于重复执行代码。
// for 循环
for (let i = 0; i < 5; i++) {
console.log(“循环次数:” + i);
}

// while 循环
let count = 0;
while (count < 3) {
console.log(“计数:” + count);
count++; // count = count + 1 的简写
}

函数
函数是一段可重复使用的代码块。在JavaScript中,定义函数有几种方式。

1. 函数声明
使用 function 关键字。

function greet(name) {
return “你好, ” + name + “!”;
}
let message = greet(“小明”);
console.log(message); // 输出:“你好, 小明!”
2. 函数表达式 (包括箭头函数)
你可以将函数赋值给一个变量。箭头函数 => 是更简洁的写法。
// 函数表达式
const greet = function(name) {
return “你好, ” + name + “!”;
};
// 箭头函数 (更简洁)
const greetArrow = (name) => {
return “你好, ” + name + “!”;
};
// 如果函数体只有一行返回语句,可以省略花括号和 return
const greetShort = name => “你好, ” + name + “!”;
函数是“一等公民”,意味着可以像变量一样被传递和赋值,这非常强大。



对象与JSON


对象是JavaScript中最重要的概念之一。它用于存储一组相关的数据和功能。
创建与访问对象

对象由花括号 {} 定义,内部包含 键: 值 对。



let person = {
name: “张三”,
age: 25,
isStudent: true,
greet: function() {
console.log(“大家好,我是” + this.name);
}
};
// 访问属性:点号表示法 或 方括号表示法
console.log(person.name); // “张三”
console.log(person[“age”]); // 25
// 调用方法
person.greet(); // “大家好,我是张三”
JSON


JSON是一种轻量级的数据交换格式,它基于JavaScript对象的语法,但要求键名必须用双引号 “” 包裹。
// JavaScript 对象
let jsObject = { name: “李四”, age: 30 };
// 转换为 JSON 字符串
let jsonString = JSON.stringify(jsObject);
console.log(jsonString); // 输出:{“name”:”李四”,”age”:30} (字符串)


// 将 JSON 字符串解析回 JavaScript 对象
let parsedObject = JSON.parse(jsonString);
console.log(parsedObject.name); // “李四”

JSON是前后端通信中最常用的数据格式。


数组


数组用于在单个变量中存储多个值的有序列表。



let fruits = [“苹果”, “香蕉”, “橙子”];
// 访问元素 (索引从0开始)
console.log(fruits[0]); // “苹果”
// 修改元素
fruits[1] = “葡萄”;
// 数组长度
console.log(fruits.length); // 3
// 添加元素到末尾
fruits.push(“芒果”);
// 遍历数组
for (let i = 0; i < fruits.length; i++) {
console.log(fruits[i]);
}
数组和对象可以嵌套使用,以构建复杂的数据结构。
扩展运算符与复制
扩展运算符 ... 是ES6引入的一个非常实用的语法,它可以“展开”数组或对象。
用于数组

let arr1 = [1, 2, 3];
let arr2 = […arr1, 4, 5]; // 将arr1的元素展开,并添加新元素
console.log(arr2); // [1, 2, 3, 4, 5]

用于对象 (浅拷贝)
扩展运算符可以方便地复制或合并对象。注意,这创建的是浅拷贝。
let original = { a: 1, b: 2 };
let copy = { …original }; // 复制 original 的所有属性
copy.a = 99;
console.log(original.a); // 1 (原对象未受影响)
console.log(copy.a); // 99

// 合并对象
let defaults = { theme: “dark”, volume: 80 };
let userSettings = { volume: 50 };
let finalSettings = { …defaults, …userSettings };
// userSettings 会覆盖 defaults 中的同名属性
console.log(finalSettings); // { theme: “dark”, volume: 50 }
重要概念:浅拷贝 vs 深拷贝
- 浅拷贝:只复制对象的第一层属性。如果属性值是另一个对象(嵌套对象),则复制的是该对象的引用,而不是对象本身。修改嵌套对象会影响所有引用它的变量。
- 深拷贝:完全复制整个对象结构,包括所有嵌套对象。修改拷贝后的对象不会影响原对象。

使用 JSON.stringify() 和 JSON.parse() 可以快速实现一个简单的深拷贝,但它有局限性(例如,不能处理函数、undefined等)。
let complexObj = { a: 1, nested: { b: 2 } };
let deepCopy = JSON.parse(JSON.stringify(complexObj));
deepCopy.nested.b = 999;
console.log(complexObj.nested.b); // 2 (原对象未受影响)

与网页交互初步

最后,让我们看看JavaScript如何与网页内容互动。document 对象代表了整个HTML文档,通过它可以访问和修改页面元素。

// 修改整个<body>内部的HTML
document.body.innerHTML = “<h1>页面内容被JS改变了!</h1>”;
// 弹出对话框
alert(“这是一个警告框!”); // 显示信息
let userName = prompt(“请输入你的名字:”); // 获取用户输入
let isOK = confirm(“你确定要删除吗?”); // 确认取消选择
这些只是基础操作,在后续课程中,我们将学习更精细、更强大的DOM操作方法。

本节课中我们一起学习了JavaScript的基础知识,包括如何将其引入网页、声明变量、使用各种数据类型和运算符、编写控制流语句和函数、以及操作对象、数组和JSON。我们还初步了解了如何用JavaScript与网页进行简单交互。JavaScript是一门功能强大且应用广泛的语言,掌握这些基础是构建动态网页和复杂Web应用的第一步。
006:JavaScript DOM 操作

在本节课中,我们将学习文档对象模型(DOM)以及如何使用 JavaScript 来操作它。我们将了解如何选择页面上的元素、修改它们的样式和内容,以及如何响应用户的交互事件。
什么是DOM?
上一节我们介绍了课程概述,本节中我们来看看什么是DOM。
文档对象模型(DOM)是浏览器加载页面时创建的一种数据结构。它的结构类似于一棵树,包含元素和节点。每个 HTML 标签都是一个节点,其内容则是该元素的子节点。
例如,一个 HTML 页面的 DOM 结构如下:<html> 标签是根节点,<head> 和 <body> 是其子节点,而它们内部又包含各自的子节点。
DOM 没有唯一的定义或展示方式,但其通用结构就是一种树形数据结构。我们可以像遍历树一样遍历 DOM,遵循父子关系。
学习 DOM 的主要原因是,我们可以使用 JavaScript 来修改它。这意味着我们可以通过 DOM 来改变元素的样式和内容,从而为网页添加交互功能,例如响应复选框的点击或鼠标悬停事件,实现元素大小调整和过渡动画等。
全局 document 对象
在深入探讨修改 DOM 的方法之前,我们需要了解 document 对象。
document 是一个全局可用的变量,由浏览器内置。它用于修改 DOM 并与 HTML 和 CSS 交互。document 对象提供了许多方法,你可以通过查阅 JavaScript DOM 文档来了解如何选择和修改元素。
实际上,document 是 window 对象的一个属性,window 对象代表浏览器中的一个标签页,包含诸如浏览器尺寸等信息。不过,我们主要关注的是 DOM,因为这是我们实际要编辑的部分。
选择与修改元素的方法
现在,我们可以开始学习用于实际选择和修改元素的方法了。
querySelector 方法
第一个要介绍的方法是 querySelector。它返回与指定 CSS 选择器匹配的第一个元素。在 querySelector 的括号内,你需要放入一个有效的 CSS 选择器,例如类名、ID 或标签名(如 div、p)。这允许我们修改所选元素的 CSS 属性。
例如,假设有一个类名为 red-square 的 <div>,我们通过 CSS 将其背景色设置为深红色。使用 JavaScript,我们可以通过 document.querySelector 选择这个元素,然后修改其背景色。
let redSquare = document.querySelector('.red-square');
redSquare.style.backgroundColor = 'limegreen';
关于 querySelector 的一个注意事项是:由于它可以用于选择类名、ID 或标签,我们需要在代码中指明选择的是哪一种。选择类名时,需要在前面加上句点 .;选择 ID 时,需要加上井号 #;而选择标签时则不需要加任何前缀。
getElementById 方法
另一种选择元素的方法是 document.getElementById。这与 querySelector 基本类似,但只能通过 ID 进行选择。使用此方法的好处是速度更快,并且你无需担心在 ID 名称前添加 # 符号,因为此方法只用于选择 ID。
let demoElement = document.getElementById('demo');
demoElement.innerHTML = 'Hello World';
在上面的例子中,我们假设有一个 ID 为 demo 的元素。innerHTML 属性用于改变元素内部的 HTML 内容,因此执行后,该元素将显示文本 “Hello World”。

可修改的元素属性
我们可以用选中的元素做很多事情。我们已经提到了 innerHTML 和 style。style 属性对应任何 CSS 样式,例如 style.backgroundColor 或 style.color。
我们还可以使用 JavaScript 更改元素的类名:
selectedElement.className = 'new-class-name';
此外,还有许多其他属性和方法可供探索。
选择多个元素的方法
以下是两种用于选择多个元素的方法:
getElementsByClassName:返回具有指定类名的所有元素的集合(一个类似数组的对象)。getElementsByTagName:返回具有指定标签名的所有元素的集合。

这两种方法都返回集合,这在你想遍历所有元素并对每个元素进行相同修改时非常有用。
createElement 方法
你还可以使用 createElement 方法动态创建新元素。
let newDiv = document.createElement('div');
例如,当你点击一个按钮时,你可能希望显示一个新的 <div>,这就可以通过此方法实现。

操作演示
让我们通过一个演示来理解 querySelector 和 getElementById。

假设我们想更改一个类名为 blue-text 的 <h1> 元素的文本。
let bodyOne = document.querySelector('.blue-text');
bodyOne.innerHTML = 'JavaScript DOM is the best!';
因为 blue-text 是一个类名,所以在 querySelector 中我们需要在前面加上句点 .。现在,bodyOne 变量就指向了那个 <h1> 元素,我们可以修改其 innerHTML 来改变文本。
同样,如果我们有一个 ID 为 introduction 的元素,我们可以使用 getElementById 来选择并修改它:
let intro = document.getElementById('introduction');
intro.style.color = 'white';

这样,我们就将介绍文本的颜色改为了白色。
批量修改多个元素
正如之前提到的,我们可以利用返回集合的方法来批量修改多个元素。
假设有一个无序列表,其中所有 <li> 项都有类名 js-target。
let targets = document.getElementsByClassName('js-target');
for (let element of targets) {
element.innerText = 'Modified by JavaScript';
}
我们首先获取所有具有类名 js-target 的元素集合,然后通过循环遍历这个集合,将每个元素的内部文本改为 “Modified by JavaScript”。
事件与事件监听器

事件是页面上发生的特定事情,例如点击按钮、在输入框中键入内容或将鼠标悬停在图像上。事件监听器用于响应这些事件,当指定的事件发生时,监听器中的函数就会运行。
事件监听器示例
让我们看一个例子。假设我们有一个输入框和一个段落 <p>。
<input class="input-field" placeholder="Type here">
<p class="output">Nothing has happened yet.</p>
我们可以在 JavaScript 中为输入框添加一个事件监听器,监听 keyup 事件(即按键松开时)。

let inputField = document.querySelector('.input-field');
let output = document.querySelector('.output');
inputField.addEventListener('keyup', function() {
output.innerHTML = inputField.value;
});
这段代码首先选中输入框和段落元素。然后,它为输入框添加一个 keyup 事件监听器。每当在输入框中键入内容时,监听函数就会触发,将段落的 innerHTML 设置为输入框的当前值。这样,你在输入框中输入什么,段落就实时显示什么。
内联事件处理器

另一种添加事件处理的方式是使用内联事件处理器,其功能与添加事件监听器类似,但一次只能分配一个事件。
这是我们刚才使用 addEventListener 的方式:


document.getElementById('myButton').addEventListener('click', function() {
alert('You clicked the button!');
});
内联事件处理器看起来像这样:
<button onclick="myFunction()">Click me</button>
<script>
function myFunction() {
alert('You clicked the button!');
}
</script>
你给按钮添加了一个 onclick 属性,并将其值设置为 JavaScript 中定义的函数名。这种方式的缺点是,如果你还想为同一个元素添加其他事件处理器(例如 onmouseover),则无法实现,因为内联方式只能设置一个,后面的会覆盖前面的。
事件冒泡
由于 DOM 是树形数据结构,事件处理有一个称为“事件冒泡”的机制。当事件在某个元素上触发时,它会沿着 DOM 树向上“冒泡”,依次触发父元素上的同类型事件监听器。
事件冒泡示例
假设我们有多个按钮,它们都包含在一个 <div> 容器中。
<div class="button-container">
<button>Click me 1</button>
<button>Click me 2</button>
<button>Click me 3</button>
</div>
如果我们想为每个按钮添加点击事件,传统做法是遍历所有按钮并为每个单独添加监听器。但利用事件冒泡,我们可以只在父容器 <div> 上添加一个事件监听器。
let container = document.querySelector('.button-container');
container.addEventListener('click', function(event) {
alert('You clicked on ' + event.target.innerText);
});
我们为 button-container 这个 <div> 添加了一个点击事件监听器。当点击任何一个子按钮时,由于事件冒泡,这个点击事件会传递到父容器 <div>,从而触发其上的监听函数。在函数中,event.target 指向实际被点击的按钮元素,event.target.innerText 则获取该按钮的文本。这样,无论点击哪个按钮,都会弹出显示对应按钮文本的提示框,而无需为每个按钮单独设置监听器。
总结与说明
本节课中我们一起学习了 JavaScript DOM 操作的核心概念。我们了解了 DOM 的树形结构,学习了如何使用 querySelector 和 getElementById 等方法选择元素,以及如何修改元素的样式、内容和类名。我们还探讨了如何通过事件监听器响应用户交互,并理解了事件冒泡机制如何帮助我们高效地处理多个子元素的事件。
最后需要说明的是,本节课所教授的是原生 JavaScript(Vanilla JS)操作 DOM 的方式。在实际的工业级开发中,你可能会使用 React、Vue 等前端框架,它们通常不直接这样操作 DOM。然而,理解底层的 DOM 操作原理非常重要,它能帮助你更好地理解这些框架的工作机制。此外,许多公司在面试时仍然会考察原生 JavaScript 能力。因此,掌握这些基础知识非常关键。

课程公告:作业二截止日期为今日。个人网站项目(项目一)的截止日期为 2 月 24 日。如有问题,请及时在课程论坛提问。本次课程的签到码是 unicorn(全小写)。
007:JavaScript 进阶概念(作用域与异步)🚀







在本节课中,我们将要学习 JavaScript 中的两个核心概念:作用域 和 异步编程。我们将从函数声明和作用域规则开始,然后深入探讨如何编写非阻塞的异步代码,包括回调、Promise 以及现代的 async/await 语法。

函数声明:常规函数与箭头函数




JavaScript 中有两种主要的函数声明方式:常规函数和箭头函数。它们功能相似,但在语法和 this 关键字的行为上有所不同。
以下是一个常规函数:
function add(x, y) {
return x + y;
}
以下是一个箭头函数:
const addTwo = (x, y) => {
return x + y;
}
常规函数在调试时,堆栈跟踪会显示函数名,而箭头函数有时会显示为“匿名函数”,这可能使调试稍微困难一些。除此之外,它们本质上执行相同的任务。

运行上述代码,两者都会返回预期的相加结果。console.log 是 JavaScript 中打印输出的主要方式。

作用域与变量声明

上一节我们介绍了函数,本节中我们来看看变量在代码中的可访问范围,即作用域。

JavaScript 使用词法作用域。这意味着,如果在当前作用域中找不到某个变量,解释器会向上一级作用域查找,就像 Python 中使用了 nonlocal 关键字一样。

以下是声明变量的三种方式:
var:不建议使用。它的行为怪异,例如变量声明会被“提升”到作用域顶部,容易导致难以调试的问题。let:允许你重新赋值,但不能在同一作用域内重新声明。const:声明一个常量,不能被重新赋值。但请注意,如果常量指向一个对象或数组,其内部属性或元素仍然是可变的。
最佳实践是:默认使用 const,只有当确定需要重新赋值时,才改为使用 let。应尽量避免使用 var。
例如:
const a = 5;
a = 10; // 错误:Assignment to constant variable.
a.property = 10; // 正确:可以修改对象的属性。
this 关键字

在 Python 或 Java 中,this(或 self)通常指向当前类的实例。但在 JavaScript 中,this 的值是动态的,取决于函数是如何被调用的。

以下是 this 的几种常见情况:
- 对于大多数函数,
this指向调用该函数的“所有者”对象。 - 如果函数没有所有者(如全局函数),在严格模式下
this是undefined,非严格模式下是全局对象。 - 可以使用
.bind()方法显式地绑定this的值。 - 箭头函数不绑定自己的
this,它会从定义它的外层作用域继承this值。


考虑以下示例:
const me = {
petName: ‘dog‘,
numberPets: 1,
getPetName: function() { return this.petName; },
morePets: function() { this.numberPets++; }
};

const stolenMorePets = me.morePets;
stolenMorePets(); // 此时函数内的 `this` 不是 `me`,所以 `me.numberPets` 不会增加。
const you = { petName: ‘cat‘, numberPets: 1 };
const boundFunction = stolenMorePets.bind(you);
boundFunction(); // 此时函数内的 `this` 被绑定为 `you`,所以 `you.numberPets` 会增加。

异步 JavaScript 简介
在 Web 开发中,我们经常需要执行耗时操作(如网络请求)。如果等待这些操作完成再更新页面,用户体验会很差。异步编程允许我们在等待操作完成的同时,保持页面的交互性。
然而,编写异步代码可能很复杂,并可能引发竞态条件(当多个异步操作以意外顺序修改数据时)。JavaScript 是单线程的,所以没有死锁问题,但数据竞争依然存在。
回调函数
最初,JavaScript 使用回调函数来处理异步操作。回调是一个在异步事件完成后被调用的函数。
以下是使用回调的例子(嵌套回调常被称为“回调地狱”):
function callbackHell() {
setTimeout(() => {
console.log(‘First job done‘);
setTimeout(() => {
console.log(‘Second job done‘);
setTimeout(() => {
console.log(‘Third job done‘);
}, 1000);
}, 1000);
}, 1000);
}
callbackHell();
console.log(‘Called callbackHell first‘);

setTimeout 函数接受一个回调函数和延迟的毫秒数。它会在指定时间后将回调函数放入任务队列。JavaScript 的事件循环会同步执行完所有代码后,再执行队列中的异步任务。因此,最后的 console.log 会先打印出来。
当需要串联多个异步操作时,嵌套回调会使代码难以阅读和维护。

Promise
为了解决回调地狱的问题,ES6 引入了 Promise。Promise 是一个对象,它代表一个异步操作的最终完成(或失败)及其结果值。
一个 Promise 有三种状态:
- Pending(待定):初始状态。
- Fulfilled(已兑现):操作成功完成。
- Rejected(已拒绝):操作失败。
你可以使用 .then() 处理成功情况,使用 .catch() 处理失败情况。
以下是一个 Promise 示例:
function betterJob(succeed, time) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (succeed) {
resolve();
} else {
reject();
}
}, time);
});
}
betterJob(true, 1000)
.then(() => console.log(‘Successfully did the job‘),
() => console.log(‘Job failed‘))
.catch((error) => console.log(‘Caught an error:‘, error));
.then() 方法接受两个回调函数作为参数(成功和失败),.catch() 则用于捕获链中任何未被处理的错误。Promise 链式调用提高了代码的可读性。



Async/Await
虽然 Promise 链比回调更好,但代码仍然不是完全同步的风格。ES7 引入了 async 和 await 关键字,使得异步代码看起来和同步代码一样。
- 在函数前加上
async关键字,使其成为异步函数。 - 在 Promise 前加上
await关键字,它会“暂停”函数的执行,直到 Promise 被解决,并返回结果值。 - 可以使用传统的
try...catch块来处理错误。
以下是使用 async/await 的例子:
async function promiseAsyncFunction() {
try {
const result = await betterJob(true, 1000); // 假设返回值为 5
console.log(‘On success:‘, result); // 输出:5
const failedResult = await betterJob(false, 1000); // 假设拒绝值为 10
console.log(‘This will not print‘);
} catch (error) {
console.log(‘Caught error with value:‘, error); // 输出:10
}
}
promiseAsyncFunction();
console.log(‘This prints first‘);

await 在心理模型上可以理解为“阻塞”线程直到 Promise 完成(实际上并非如此,但有助于理解)。async/await 是现代 JavaScript 中处理异步操作的首选方式,因为它使代码更清晰、更易于调试。
补充:Fetch API
fetch() 是一个用于发起网络请求的 Web API,它返回一个 Promise。你可以用它来获取远程资源的数据,并使用我们刚学到的 Promise 或 async/await 来处理响应。
async function getData() {
try {
const response = await fetch(‘https://api.example.com/data‘);
const data = await response.json();
console.log(data);
} catch (error) {
console.error(‘Fetch error:‘, error);
}
}
总结

本节课中我们一起学习了 JavaScript 的核心进阶概念。
- 作用域与变量:理解了
let、const和var的区别,以及词法作用域的工作方式。 this关键字:了解了this的动态绑定特性,以及箭头函数在此处的不同行为。- 异步编程:从最初的回调函数及其“地狱”嵌套问题,到更优雅的 Promise 链式调用,最后到使用
async/await编写类似同步风格的异步代码。



掌握这些概念对于构建高效、可维护的现代 Web 应用至关重要。在实践中,应优先使用 const/let、箭头函数以及 async/await 来编写清晰可靠的代码。
008:React 入门

在本节课中,我们将学习 React 的基础知识。React 是一个用于构建用户界面的 JavaScript 库,它通过组件化的方式,让我们能够高效地创建交互式网页应用。我们将介绍 React 的核心概念,包括组件、属性(Props)和状态(State),并通过一个简单的井字棋游戏示例来演示这些概念的实际应用。
什么是 React?🤔
上一节我们介绍了课程概述,本节中我们来看看 React 是什么。
React 是一个由 Facebook 创建的 JavaScript 库,用于构建交互式用户界面。如果你不希望界面设计过于平淡,而是希望为用户提供更好的前端体验和用户界面,React 是一个理想的选择。
React 的核心是组件。组件类似于 HTML 标签,但你可以创建自己的、可复用的组件。例如,在建造房屋时,你可以创建门、窗、屋顶等组件,每个组件都有自己的属性。通过组合这些组件,你可以构建出完整的房屋。这种方式不仅提高了代码的可读性,也使得代码复用变得非常方便。
以下是 React 的一些优点:
- 它易于学习和使用。
- 它结合了 HTML、CSS 和 JavaScript。
- 它有助于提高开发效率和测试速度。
创建你的第一个 React 应用 🛠️
在深入组件之前,我们需要先搭建 React 开发环境。
首先,请确保你的电脑上安装了 Node.js 和 npm。你可以在终端中运行以下命令来检查:
node -v
npm -v
如果没有安装,请访问 Node.js 官网下载并安装。安装完成后,你可以使用以下命令快速创建一个新的 React 应用:
npx create-react-app my-app
这个命令会为你生成一个包含所有基础配置的 React 项目。
理解组件 🧩
我们已经知道 React 围绕组件构建。那么,组件具体是什么样子的呢?
组件是返回一些类似 HTML 内容的 JavaScript 函数或类。在 React 中,我们使用一种叫做 JSX 的语法,它允许我们在 JavaScript 代码中直接编写 HTML。

以下是一个简单的组件示例:
function Welcome() {
return <h1>Hello, Spider!</h1>;
}
这里,Welcome 就是我们自定义的组件。它看起来像一个 HTML 标签,但实际上是我们在 JavaScript 中定义的。
组件可以在其他组件中被使用。例如,在一个主要的 App 组件中,我们可以像使用 HTML 标签一样使用 Welcome 组件:
function App() {
return (
<div>
<Welcome />
</div>
);
}
在 React 应用中,有一个根组件(通常名为 App)。index.js 文件中的 ReactDOM.render 函数负责将这个根组件渲染到网页的根节点(root)上,从而显示整个应用。
如果你的组件定义在不同的文件中,你需要使用 export 将其导出,并在使用它的文件中用 import 导入。
使用 Props 传递数据 📤
组件本身是静态的,为了让它们变得动态并能显示不同的内容,我们需要使用 Props(属性)。

Props 允许我们将数据从父组件传递到子组件,类似于给函数传递参数。这使得组件可以高度复用。
让我们修改之前的 Welcome 组件,使其可以欢迎任何人:
function Welcome(props) {
return <h1>Hello, {props.name}!</h1>;
}
// 在 App 组件中使用
function App() {
return (
<div>
<Welcome name="Alice" />
<Welcome name="Bob" />
</div>
);
}
现在,Welcome 组件接收一个 name 属性,并动态地显示它。在 App 组件中,我们通过 name="Alice" 的方式将数据传递给子组件。
在井字棋游戏的例子中,Square(方格)组件接收 value 和 onClick 两个属性,分别决定了方格上显示的内容(X 或 O)和点击时的行为。
使用 State 管理内部状态 🧠
Props 用于从外部向组件传递数据,而 State(状态)则用于管理组件内部会变化的数据。
例如,在井字棋游戏中,当前轮到哪位玩家、棋盘上每个格子的状态(是 X、O 还是空),这些数据都应该用 State 来管理。
你可能会问,为什么不用普通变量?原因是,React 无法感知普通变量的变化,从而不知道何时需要更新屏幕上的内容。而 State 变量被改变时,React 会自动重新渲染受影响的组件部分。
以下是使用 State 的计数器示例:
import { useState } from ‘react’;
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
useState(0)创建了一个状态变量count,其初始值为0。setCount是一个函数,用于更新count的值。切记,必须使用这个函数来更新状态,直接赋值(如count = 1)是无效的。- 当按钮被点击时,
onClick事件会调用setCount(count + 1),使计数加一,并触发组件重新渲染。

状态提升与数据共享 🔄
当多个组件需要共享同一份数据时(例如,用户登录状态、购物车内容),我们应该将共享的状态定义在它们共同的、层级足够高的父组件中。
然后,通过 Props 将状态数据向下传递给需要的子组件。子组件不能直接修改父组件的状态,但父组件可以通过传递一个更新状态的函数(例如 setCount)作为 Prop 给子组件,让子组件间接地触发状态更新。
这种模式被称为“状态提升”,它保证了应用中的数据流清晰且可预测。
构建交互式井字棋游戏 🎮
现在,让我们将 Props 和 State 的知识应用到井字棋游戏中。
- 初始化状态:在
Board(棋盘)组件中,我们使用useState来初始化两个状态:board(一个包含9个元素的数组,代表9个格子)和player(当前玩家,‘X‘ 或 ‘O’)。 - 传递 Props:
Board组件通过map函数渲染9个Square组件,并将board数组中对应位置的值(‘X‘, ‘O‘, 或null)以及一个onClick处理函数作为 Props 传递给每个Square。 - 处理点击事件:当某个
Square被点击时,它会调用从父组件传来的onClick处理函数。这个函数会:- 根据当前点击的位置,创建一个新的
board数组副本(重要:必须创建新数组,而不是直接修改原数组)。 - 在新数组的对应位置标记当前玩家的符号(‘X‘ 或 ‘O’)。
- 调用
setBoard更新棋盘状态,并调用setPlayer切换玩家。
- 根据当前点击的位置,创建一个新的
通过这样的流程,我们就实现了一个完整的、可交互的井字棋游戏。
实用开发工具 🧰

为了提升 React 开发体验,推荐安装以下 VS Code 扩展:
- Prettier:代码格式化工具。保存文件时自动格式化代码,保持代码风格整洁统一。
- ESLint:代码检查工具。它可以识别常见的代码错误和潜在问题(例如,错误地直接给状态变量赋值),帮助你在编写阶段就发现并修复问题。
总结 📝
本节课中我们一起学习了 React 的核心基础。
我们首先了解了 React 是一个用于构建用户界面的组件化 JavaScript 库。然后,我们学习了如何创建和使用组件来构建 UI。为了让组件能够接收外部数据,我们引入了 Props 的概念。接着,为了管理组件内部可变的数据,我们深入探讨了 State 的用法,包括如何使用 useState 钩子以及状态更新的正确方式。我们还通过“状态提升”的模式解决了组件间的数据共享问题。最后,我们利用这些知识,一步步构建出了一个交互式的井字棋游戏。

掌握这些概念是成为 React 开发者的第一步。在接下来的课程中,我们将探索更高级的 React 特性。
009:React进阶与路由

在本节课中,我们将深入学习React的核心概念,包括两个重要的Hook:useEffect,以及如何为React应用添加多页面功能(路由)。我们还将介绍如何通过Axios库与后端API进行通信以获取数据。
上一讲我们介绍了React的基础,包括组件、状态和属性。本节中我们来看看更高级的Hook和如何构建多页面应用。
🪝 深入理解 useEffect Hook
useEffect 是一个用于处理组件“副作用”的Hook。副作用指的是那些与渲染结果无关的操作,例如数据获取、订阅或手动修改DOM。
其基本语法是一个接受两个参数的函数:
useEffect(() => {
// 副作用逻辑在这里执行
}, [dependencies]);
- 第一个参数是一个函数,包含要执行的副作用代码。
- 第二个参数是一个依赖项数组,用于控制副作用执行的时机。
依赖项数组的作用
以下是依赖项数组不同情况的说明:
- 不提供依赖项数组:副作用函数在每次组件渲染后都会执行。
- 提供空数组
[]:副作用函数仅在组件首次渲染后执行一次。 - 提供非空数组
[dep1, dep2]:副作用函数在首次渲染后以及依赖项的值发生变化时执行。
示例:假设我们有一个计数器和一个提示框状态。我们希望仅在提示框状态改变时弹出提示。
const [count, setCount] = useState(0);
const [alert, setAlert] = useState(‘初始值’);
useEffect(() => {
alert(‘alert状态改变了!’);
}, [alert]); // 仅当alert变量变化时执行
点击计数器按钮不会触发useEffect,因为count不在依赖项中。只有调用setAlert改变alert值时,才会弹出提示。
useEffect 在数据获取场景中非常有用。你可以将存储数据的状态变量放入依赖项,这样当数据更新后,组件会自动重新渲染以展示新数据。
🧭 React Router 实现客户端路由
目前我们构建的都是单页面应用。路由使我们能够为应用添加多个“页面”,并根据URL显示不同的内容。
核心概念
- 客户端路由:与传统的
<a>标签(会触发整个页面刷新)不同,React Router 实现了客户端路由。它只更新页面中需要变化的部分,提供更流畅的用户体验。 <Router>: 需要用<BrowserRouter>包裹整个应用的根组件,为应用提供路由上下文。<Switch>与<Route>:<Switch>组件用于包裹一组<Route>。<Route>定义了路径和对应渲染的组件。<Link>: 用于导航的特殊组件,替代<a>标签。它不会导致页面完全重新加载。
使用步骤
以下是实现路由的基本步骤:
- 安装库: 使用React Router v5(本课程版本)。
npm install react-router-dom@5 - 包裹应用: 在入口文件(如
index.js)中用<BrowserRouter>包裹<App>组件。 - 定义路由结构: 在组件中使用
<Switch>和<Route>。import { Switch, Route } from ‘react-router-dom’; function App() { return ( <div> <nav>{/* 导航栏 */}</nav> <Switch> <Route exact path=“/” component={HomePage} /> <Route path=“/about” component={AboutPage} /> </Switch> </div> ); } - 使用
<Link>导航:import { Link } from ‘react-router-dom’; <Link to=“/about”>关于我们</Link>
🔄 使用 Axios 获取API数据
为了在React应用中显示动态内容,我们需要从后端服务器获取数据。Axios 是一个基于Promise的HTTP客户端,非常适合此任务。
结合 useEffect 与 Axios
数据获取通常是一个副作用,因此应在 useEffect 中执行。将依赖项数组设为空数组 [] 可以确保只在组件挂载时获取一次数据,避免不必要的网络请求。
示例:获取GitHub用户信息
import React, { useState, useEffect } from ‘react’;
import axios from ‘axios’;
function GitHubInfo() {
const [userData, setUserData] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await axios.get(‘https://api.github.com/users/你的用户名‘);
setUserData(response.data); // 将获取的数据存入状态
} catch (error) {
console.error(‘获取数据失败:‘, error);
}
};
fetchData();
}, []); // 空依赖数组确保只获取一次
return (
<div>
{userData ? (
<p>GitHub 粉丝数: {userData.followers}</p>
) : (
<p>加载中...</p> // 处理数据加载前的状态
)}
</div>
);
}
关键点说明:
- 异步操作:
axios.get返回一个Promise,我们使用async/await语法等待其完成。 - 状态管理: 将获取的数据(
response.data)通过setUserData存入状态。 - 条件渲染: 在数据加载完成前(
userData为null),显示“加载中…”提示,防止访问未定义的数据。


本节课中我们一起学习了React的useEffect Hook,它用于管理副作用逻辑;了解了如何使用React Router v5为单页面应用添加多页面路由功能;最后,我们掌握了通过Axios库在useEffect中异步获取后端API数据并渲染到页面的完整流程。这些是构建动态、交互式现代Web应用的核心技能。
010:Node.js 与服务器基础 🚀


在本节课中,我们将要学习 Node.js 的基础知识,了解它如何让 JavaScript 在浏览器之外运行,并亲手构建一个简单的 Web 服务器。通过实践,你将理解客户端与服务器交互的基本原理。
什么是 Node.js?🤔
上一节我们介绍了课程概述,本节中我们来看看 Node.js 是什么。
Node.js 本质上是一个 JavaScript 运行时环境。你之前可能熟悉的 JavaScript 只能在网页浏览器中运行,而像 Python 或 Java 这样的语言则可以在计算机上直接运行程序。Node.js 的出现使得我们能够使用 JavaScript 编写服务器端程序。
你可以通过访问其官网下载并安装 Node.js。如果你一直在使用 React 进行开发,那么你的电脑里很可能已经安装了 Node.js,因为它通常是 React 开发环境的依赖项。

你可以通过在终端中运行 node 命令来检查是否已安装。
认识 NPM 📦
Node.js 带来了一个强大的工具:NPM(Node Package Manager)。它类似于 Python 的 pip,是一个包管理器,允许你轻松安装和管理第三方代码库(包)。此外,NPM 还能帮助你初始化并管理整个项目环境,我们稍后会看到这一点。


Node.js 的能力与限制 ⚡

Node.js 的强大之处在于它提供了浏览器所不具备的访问系统资源的能力。
在浏览器中,JavaScript 的访问受到严格限制。你无法直接访问文件系统,不能自由地进行网络请求,也无法启动一个 Web 服务器。而使用 Node.js,你可以像使用 Python 一样,在本地计算机上执行这些操作。
然而,Node.js 环境也缺少浏览器环境中的一些特定对象。例如,你不能再使用 document.getElementById 这样的 DOM 操作方法,因为 Node.js 运行时没有“文档”(document)这个概念,它只是一个纯 JavaScript 的执行环境。
构建你的第一个服务器 🛠️
理解了 Node.js 的基本概念后,本节我们将动手构建一个 Web 服务器。
服务器是一台持续运行、等待连接的计算机。当你访问一个网站时,你的浏览器会向托管该网站的服务器发送请求,服务器则返回构成网站的 HTML、CSS 和 JavaScript 文件。
服务器的妙处在于,它不仅可以提供静态网页,还能处理动态请求。例如,一个登录表单在提交时,会向同一个服务器发送另一个请求,服务器可以验证用户名和密码,并返回登录结果。服务器是“有状态”的,它可以记住信息。
现在,请打开你的代码编辑器(如 VS Code)并创建一个新项目。

以下是需要完成的步骤:
- 在项目中创建一个新的 JavaScript 文件,例如
server.js。 - 打开编辑器内的终端。


运行简单的 Node.js 脚本 ▶️



我们将从一个简单的脚本开始,验证 Node.js 环境。


在你的 server.js 文件中,输入以下代码:
console.log(42);
保存文件后,在终端中运行命令:
node server.js
终端应该会输出数字 42。这表明你的 Node.js 环境工作正常。


使用 Express 框架创建服务器 🌐



直接使用原生 Node.js 创建服务器比较复杂,因此我们使用一个流行的框架——Express。


首先,我们需要初始化一个 NPM 项目并安装 Express。在项目根目录的终端中,依次运行以下命令:
npm init -y
npm install express cors
npm init -y 会快速创建一个 package.json 文件来管理项目依赖。npm install 命令则用于安装 express 框架和 cors 中间件(用于处理跨域请求)。



接下来,在 server.js 文件中,用以下代码替换之前的内容:
// 导入所需的模块
const express = require('express');
const cors = require('cors');




// 创建一个 Express 应用实例
const app = express();
// 使用 cors 中间件,允许跨域请求
app.use(cors());
// 定义一个路由处理器:当访问根路径(‘/’)时,返回 “Hello World”
app.get('/', (req, res) => {
res.send('Hello World');
});

// 让服务器在 3042 端口上开始监听网络请求
app.listen(3042, () => {
console.log('Server listening on port 3042');
});
这段代码创建了一个基本的服务器。它监听本机(localhost)的 3042 端口。当你在浏览器中访问 http://localhost:3042 时,服务器会处理对根路径 / 的 GET 请求,并返回文本 “Hello World”。

保存文件,并在终端中运行 node server.js 来启动服务器。然后打开浏览器访问上述地址,你应该能看到 “Hello World”。
理解服务器状态 🧠
为了展示服务器可以“记住”信息,我们来修改一下代码。
将 server.js 中的路由处理器修改如下:
let counter = 1; // 在服务器内存中定义一个计数器变量
app.get('/', (req, res) => {
res.send(`Incremental value: ${counter}`); // 每次请求返回当前计数值
counter++; // 处理完请求后,计数器加1
});
重启服务器(在终端按 Ctrl+C 停止,再运行 node server.js)并刷新浏览器。每次刷新页面,看到的数字都会递增。即使你关闭浏览器标签页再重新打开,计数器依然会继续累加,因为 counter 变量一直保存在运行着的服务器内存中。这演示了服务器如何维护状态。


从客户端连接服务器 🔗




服务器已经就绪,现在我们来创建一个简单的 HTML 客户端页面与之交互。


在项目中创建一个新文件,命名为 client.html,并填入以下内容:
<!DOCTYPE html>
<html>
<head>
<title>Client</title>
</head>
<body>
<h1 id="display">Initial Text</h1>
<button onclick="getStuff()">Get Stuff</button>
<script>
async function getStuff() {
// 向我们的服务器发起请求
const response = await fetch('http://localhost:3042/');
// 等待并获取响应的文本内容
const text = await response.text();
// 将获取到的文本显示在页面的 h1 元素中
document.getElementById('display').innerText = text;
}
</script>
</body>
</html>
直接双击在浏览器中打开这个 client.html 文件(注意,它不是通过我们的 Node.js 服务器提供的)。点击 “Get Stuff” 按钮,页面会向 localhost:3042 上的服务器发送请求,并将服务器返回的递增数字显示出来。





这个例子清晰地展示了客户端与服务器的分离:client.html 是运行在浏览器中的客户端,它主动向另一个独立运行的程序(我们的 Node.js 服务器)发起请求并获取数据。

关于 CORS 和 Fetch API 的补充说明 🔐
你可能注意到,我们的服务器代码中使用了 app.use(cors())。这是为了启用 CORS(跨源资源共享),允许像 client.html 这样来自不同“源”(协议、域名、端口)的网页向我们的服务器发起请求。出于安全原因,浏览器默认禁止这类跨域请求。


我们客户端使用的 fetch() 函数是现代浏览器提供的用于发起网络请求的 API。它返回一个 Promise 对象,这使得我们可以用 async/await 语法优雅地处理异步操作(即需要等待服务器响应的操作)。



总结 📚



本节课中我们一起学习了 Node.js 的核心概念与实践。我们了解到 Node.js 使得 JavaScript 能够用于服务器端编程,并通过 NPM 管理项目依赖。我们使用 Express 框架快速构建了一个 Web 服务器,该服务器能够监听请求、返回响应,并且利用内存维护状态(如计数器)。最后,我们创建了一个独立的 HTML 客户端页面,通过 Fetch API 与自建的服务器进行通信,直观地演示了前后端分离的交互模式。这为理解现代 Web 应用如何工作奠定了坚实的基础。
011:后端基础(全)

在本节课中,我们将学习后端开发的基础知识,特别是HTTP协议、REST API以及用于测试API的工具Postman。我们将从网络通信的基本原理开始,逐步深入到如何构建和测试后端服务。
🌐 HTTP协议简介
上一节我们介绍了前后端的基本概念。本节中,我们来看看网络通信的基石——HTTP协议。
HTTP(超文本传输协议)是互联网上设备之间通信的语言。它允许客户端(如你的浏览器)向服务器请求信息,服务器则返回响应。例如,访问维基百科时,URL前的https就表示使用了HTTP协议。
HTTP请求的主要类型有以下几种:



以下是HTTP请求的主要类型:
- GET:用于获取信息。在浏览器地址栏输入网址时,默认就是发送GET请求。
- POST:用于创建新信息。例如,在亚马逊上点击“购买”时,就会发送POST请求来创建新订单。
- PUT:用于替换现有信息。例如,更新用户个人资料中的姓名。
- DELETE:用于删除现有信息。例如,删除亚马逊上的一个历史订单。
- PATCH:用于部分更新现有信息,与PUT的完全替换不同。
服务器对请求的响应会包含一个状态码,用于指示请求的结果。

以下是常见的HTTP响应状态码类别:
- 2xx (成功):请求成功处理。例如,
200 OK。 - 3xx (重定向):需要进一步操作以完成请求。例如,从
http站点跳转到https站点。 - 4xx (客户端错误):请求有误,服务器无法处理。例如,
404 Not Found(找不到资源),400 Bad Request(错误请求)。 - 5xx (服务器错误):服务器处理请求时内部出错。例如,
500 Internal Server Error。

🔧 使用cURL进行HTTP请求
了解了HTTP的基本概念后,我们可以通过工具来实际观察HTTP通信。cURL是一个命令行工具,可以模拟各种HTTP请求。
在终端中,使用curl命令可以获取一个网址的内容。例如,获取一个网页的HTML代码:
curl https://www.example.com
要获取特定API返回的JSON数据,可以这样操作:
curl https://pokeapi.co/api/v2/pokemon/ditto
一个完整的HTTP请求结构如下:
协议://域名/资源路径?查询字符串
例如,在https://pokeapi.co/api/v2/pokemon?limit=10&offset=20中:
https是协议。pokeapi.co是域名。/api/v2/pokemon是资源路径。?limit=10&offset=20是查询字符串,用于过滤结果(获取第20到30个宝可梦)。
使用curl -v命令可以显示详细的请求和响应头信息,这对于调试非常有用。

🏗️ HTTP请求在后端的处理流程

当我们向后端发送一个HTTP请求时,服务器内部会经历一系列处理步骤。
以下是典型的请求处理流程:
- 路由:服务器根据请求的URL和方法(GET/POST等)确定由哪段代码处理。
- 控制器:处理请求的代码逻辑,解析参数(如查询字符串、请求体)。
- 服务/模型:执行核心业务逻辑,如计算、数据验证或与数据库交互。
- 数据库:存储和检索持久化数据。
- 响应:将处理结果(HTML、JSON等)封装成HTTP响应,返回给客户端。

例如,在谷歌地图中搜索“餐厅”时,后端服务可能会同时查询内部数据库获取地址、调用外部API获取评分,最后将所有数据整合返回。
一个结构良好的后端项目目录通常包含routes、controllers、models、services等模块,以实现关注点分离。
📡 REST API 设计原则
后端服务通常通过API(应用程序编程接口)对外提供功能。REST(表述性状态转移)是一种流行的API设计风格。
REST API 遵循以下六个核心约束:
以下是REST API的六个核心约束:
- 客户端-服务器分离:前端UI与后端数据存储分离,允许它们独立演化。
- 无状态:每个请求必须包含处理所需的所有信息,服务器不存储会话上下文。
- 可缓存:响应必须明确标示自身是否可被缓存,以提高性能。
- 统一接口:简化架构,使系统各部分能独立演进。这是REST的核心特征。
- 分层系统:系统各组件分层,每层只与相邻层交互,提高可扩展性和安全性。
- 按需代码(可选):服务器可以临时扩展客户端功能,例如传输JavaScript代码。
🛠️ 使用Postman测试API
开发后端时,我们需要方便的工具来测试API。Postman提供了一个图形化界面来构建、发送HTTP请求并查看响应。
Postman的核心功能包括:
- 选择请求方法(GET, POST, PUT, DELETE等)。
- 输入请求URL和添加查询参数。
- 查看格式化的JSON响应,比命令行更清晰。
- 检查响应的头部信息(如状态码、内容类型)。
- 方便地测试本地开发服务器(如
http://localhost:4000)。


使用Postman可以快速验证后端端点是否按预期工作,是后端开发中不可或缺的调试工具。
📚 课程总结
本节课中,我们一起学习了后端开发的基础知识。我们从HTTP协议入手,了解了客户端与服务器之间通信的基本方式、不同类型的请求和响应状态码。接着,我们使用cURL命令行工具实际发送了HTTP请求,并分析了请求的结构。然后,我们探讨了HTTP请求在服务器端的处理流程,以及如何设计符合REST原则的API。最后,我们介绍了强大的API测试工具Postman,它可以帮助我们高效地构建和调试后端服务。

掌握这些概念和工具,是构建功能完整的全栈应用程序的重要第一步。
012:后端开发(二)🚀

在本节课中,我们将学习如何使用 Express 框架和 MongoDB 数据库构建后端服务。我们将重点创建一个用户认证系统,包括用户注册、登录以及使用 JSON Web Token (JWT) 进行安全验证。
课程回顾
上一节我们介绍了 HTTP 协议、REST API 架构以及如何使用 Postman 测试 API 端点。HTTP 是一种允许不同计算机在互联网上通信的协议。客户端发起 HTTP 请求,服务器则包含信息并返回响应。REST 是 API 的一种架构风格,而 API 是允许两个应用程序相互通信的接口。Postman 是一个用于测试 HTTP 请求和 API 端点的工具。
简单来说,当你在浏览器中访问一个网页时,你的计算机会向一个 API 发送请求。API 处理该请求,可能会从服务器上的数据库收集必要信息,然后将数据打包,以响应的形式发送回你的浏览器。
Express 框架介绍
Express 是一个基于 Node.js 的后端 Web 应用框架。它专为构建 Web 应用程序和 API 而设计,与前端常用的 Axios 不同,Express 主要用于后端开发。
以下是一个简单的 Express 服务器代码示例:
const express = require('express');
const app = express();

app.get('/', (req, res) => {
res.json({ message: 'API 正在运行' });
});
app.listen(4000, () => {
console.log(`服务器已在端口 4000 启动`);
});
在这段代码中,我们首先创建了一个 Express 应用对象,然后定义了一个处理根路径 (/) GET 请求的路由,最后让服务器监听 4000 端口。
要使用 Express,你需要先通过 npm install express 命令安装它。


数据库基础
数据库是结构化信息或数据的组织集合,通常以电子形式存储在计算机系统中。主要有两种类型的数据库:

- SQL 数据库:如 PostgreSQL 和 Microsoft SQL Server。你可以将其想象成 Google Sheets 表格,每一行数据对应一条记录,不同的列对应不同的数据类别。
- NoSQL 数据库:如 MongoDB 和 DynamoDB。这类数据库不使用表格,而是通过定义“模式”来规定数据的结构。数据以对象的形式存储,每个对象可以包含多个字段。
本节课我们将主要使用 MongoDB,它是一种学习曲线相对平缓的 NoSQL 数据库。
在 MongoDB 中,数据以集合的形式组织,集合中的每个文档都是一个 JSON 对象。例如,一个存储 Airbnb 房源的数据库,每个文档(对象)都包含 name、description、price 等字段。


项目搭建与依赖安装


现在,让我们开始动手搭建项目。首先,创建一个新目录并初始化 Node.js 项目:
mkdir mongo-practice
cd mongo-practice
npm init -y
接着,安装项目所需的依赖包:
npm install --save express cors mongoose bcryptjs jsonwebtoken express-validator nodemon dotenv

以下是各个依赖包的作用:
express: 后端 Web 应用框架。cors: 处理跨域资源共享。mongoose: 用于连接和操作 MongoDB 数据库的 ODM 库。bcryptjs: 用于加密用户密码。jsonwebtoken: 用于生成和验证 JSON Web Token,实现用户认证。express-validator: 用于验证输入数据。nodemon: 开发工具,监视文件变化并自动重启服务器。dotenv: 用于加载环境变量。

创建 Express 服务器
创建一个 index.js 文件作为应用的入口点:
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
const PORT = 4000;
// 中间件:解析 JSON 格式的请求体
app.use(bodyParser.json());
// 基础路由,用于测试
app.get('/', (req, res) => {
res.json({ message: 'API 正在运行' });
});
// 启动服务器
app.listen(PORT, () => {
console.log(`服务器已在端口 ${PORT} 启动`);
});
使用 npx nodemon index.js 命令启动服务器,然后在 Postman 中访问 http://localhost:4000/,你应该会收到 {“message”: “API 正在运行”} 的响应。
连接 MongoDB 数据库
为了连接 MongoDB,我们需要配置数据库连接。首先,在 MongoDB Atlas(云服务)或本地创建一个数据库,并获取连接字符串。
创建一个 config/db.js 文件:
const mongoose = require('mongoose');
const mongoURI = ‘你的MongoDB连接字符串/test’; // 请替换为你的实际连接字符串
const initiateMongoServer = async () => {
try {
await mongoose.connect(mongoURI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
console.log(‘已连接到数据库’);
} catch (e) {
console.log(e);
throw e;
}
};
module.exports = initiateMongoServer;
在 index.js 中引入并调用此函数:
const initiateMongoServer = require(‘./config/db’);
initiateMongoServer();
定义数据模型(Schema)
在 MongoDB 中,我们使用模式来定义数据的结构。创建一个 models/User.js 文件来定义用户模型:
const mongoose = require(‘mongoose’);
const UserSchema = new mongoose.Schema({
username: {
type: String,
required: true,
},
email: {
type: String,
required: true,
},
password: {
type: String,
required: true,
},
createdAt: {
type: Date,
default: Date.now,
},
});
module.exports = mongoose.model(‘User’, UserSchema);

这个模式规定,每个用户文档必须有 username、email 和 password 字段,并且都是字符串类型。createdAt 字段会自动设置为创建文档的日期。

实现用户注册功能
我们将使用路由来组织 API 端点。创建一个 routes/user.js 文件来处理用户相关的请求。
首先,实现用户注册 (/signup) 端点:
const express = require(‘express’);
const router = express.Router();
const User = require(‘../models/User’);
const bcrypt = require(‘bcryptjs’);
const jwt = require(‘jsonwebtoken’);

// 用户注册
router.post(‘/signup’, async (req, res) => {
const { username, email, password } = req.body;
try {
// 1. 检查用户是否已存在
let user = await User.findOne({ email });
if (user) {
return res.status(400).json({ msg: ‘用户已存在’ });
}
// 2. 创建新用户对象
user = new User({
username,
email,
password,
});
// 3. 加密密码
const salt = await bcrypt.genSalt(10);
user.password = await bcrypt.hash(password, salt);
// 4. 保存用户到数据库
await user.save();
// 5. 创建并返回 JWT
const payload = {
user: {
id: user.id,
},
};
jwt.sign(
payload,
‘randomString’, // 应使用更安全的密钥,并从环境变量读取
{ expiresIn: 10000 },
(err, token) => {
if (err) throw err;
res.status(200).json({ token });
}
);
} catch (err) {
console.error(err.message);
res.status(500).send(‘保存用户时出错’);
}
});
module.exports = router;


在 index.js 中挂载这个路由:




const userRoutes = require(‘./routes/user’);
app.use(‘/user’, userRoutes);

现在,你可以使用 Postman 向 http://localhost:4000/user/signup 发送一个 POST 请求,请求体为 JSON 格式(例如 {“username”: “test”, “email”: “test@example.com”, “password”: “123456”}),来测试注册功能。成功后,你会收到一个 JWT 令牌。







实现用户登录功能


接下来,在 routes/user.js 中添加用户登录 (/login) 端点:
// 用户登录
router.post(‘/login’, async (req, res) => {
const { email, password } = req.body;
try {
// 1. 检查用户是否存在
let user = await User.findOne({ email });
if (!user) {
return res.status(400).json({ msg: ‘用户不存在,请先注册’ });
}
// 2. 验证密码
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
return res.status(400).json({ msg: ‘密码错误’ });
}
// 3. 创建并返回 JWT
const payload = {
user: {
id: user.id,
},
};
jwt.sign(
payload,
‘randomString’,
{ expiresIn: 3600 },
(err, token) => {
if (err) throw err;
res.json({ token });
}
);
} catch (err) {
console.error(err.message);
res.status(500).send(‘服务器错误’);
}
});
使用 Postman 向 http://localhost:4000/user/login 发送包含邮箱和密码的 POST 请求,验证登录功能。

使用中间件保护路由
中间件是一种函数,它可以访问请求对象 (req)、响应对象 (res) 和下一个中间件函数 (next)。它的主要作用是在请求到达最终路由处理器之前,执行一些检查或操作。
我们将创建一个中间件来验证 JWT。创建 middleware/auth.js 文件:
const jwt = require(‘jsonwebtoken’);


module.exports = function (req, res, next) {
// 从请求头中获取 token
const token = req.header(‘x-auth-token’);
// 检查 token 是否存在
if (!token) {
return res.status(401).json({ msg: ‘无令牌,授权被拒绝’ });
}
try {
// 验证 token
const decoded = jwt.verify(token, ‘randomString’);
req.user = decoded.user; // 将解码后的用户信息添加到请求对象中
next(); // 继续执行下一个中间件或路由处理器
} catch (err) {
res.status(401).json({ msg: ‘令牌无效’ });
}
};

现在,我们可以创建一个受保护的路由,只有提供有效 JWT 的用户才能访问。在 routes/user.js 中添加:

const auth = require(‘../middleware/auth’);
// 获取当前用户信息 (受保护路由)
router.get(‘/me’, auth, async (req, res) => {
try {
// 中间件已将用户ID添加到 req.user
const user = await User.findById(req.user.id).select(‘-password’); // 查询用户但不返回密码字段
res.json(user);
} catch (err) {
console.error(err.message);
res.status(500).send(‘获取用户信息时出错’);
}
});
要测试这个端点,你需要:
- 先通过登录或注册获取一个 JWT 令牌。
- 在 Postman 中,向
http://localhost:4000/user/me发送 GET 请求。 - 在请求的 “Headers” 选项卡中,添加一个键为
x-auth-token,值为你的 JWT 令牌的请求头。
如果令牌有效,你将收到当前用户的详细信息(不包括密码)。
总结
本节课我们一起学习了后端开发的核心概念与实践。我们回顾了 HTTP 和 REST API 的基础知识,然后深入使用 Express 框架搭建了服务器。我们介绍了 MongoDB 这种 NoSQL 数据库,并通过 Mongoose 库与之进行交互。
本节课的重点是构建了一个完整的用户认证系统:
- 我们定义了用户数据模型(Schema)。
- 实现了用户注册功能,包括密码加密(使用
bcryptjs)和将用户保存至数据库。 - 实现了用户登录功能,验证用户凭证并返回一个 JSON Web Token (JWT)。
- 创建了认证中间件,用于在后续请求中验证 JWT 的有效性。
- 实现了一个受保护的路由 (
/me),只有携带有效 JWT 的请求才能访问,并返回用户信息。


通过这个流程,我们模拟了现代 Web 应用中常见的认证与授权机制。在开发过程中,使用 Postman 测试 API 端点,以及利用 nodemon 实现开发热重载,都是提高效率的重要实践。
013:React 3 演示
在本节课中,我们将学习 React 中的状态管理,特别是如何使用 useState Hook 来创建和管理组件内部的状态。我们将通过一个简单的计数器应用来演示其核心概念和用法。
上一讲我们介绍了 React 组件的基础结构,本节中我们来看看如何让组件“记住”并响应用户交互。
状态与 useState Hook
在 React 中,状态 指的是组件内部可以随时间变化的数据。当状态改变时,React 会自动重新渲染组件以反映最新的数据。我们使用 useState Hook 来为函数组件添加状态。
useState 是一个函数,它接收一个参数作为状态的初始值,并返回一个包含两个元素的数组:
- 当前的状态值。
- 一个用于更新该状态的函数。
其基本语法可以用以下代码描述:
const [state, setState] = useState(initialState);
构建一个计数器应用
让我们通过构建一个计数器来实践 useState 的用法。这个计数器将显示一个数字,并提供两个按钮来增加或减少这个数字。
步骤一:导入 useState
首先,我们需要从 react 库中导入 useState Hook。

import React, { useState } from 'react';
步骤二:初始化状态
在函数组件内部,我们调用 useState 来声明一个状态变量。这里我们将计数器的初始值设为 0。
function Counter() {
const [count, setCount] = useState(0);
// ... 其他代码
}
现在,变量 count 存储着当前计数值(初始为0),而 setCount 函数用于更新 count 的值。

步骤三:在 JSX 中显示状态
我们可以在组件的 JSX 中直接使用 count 变量来显示当前计数值。
return (
<div>
<p>当前计数:{count}</p>
</div>
);
步骤四:更新状态
为了改变状态,我们需要调用状态更新函数 setCount。重要的是,我们永远不应该直接修改 count 变量(例如 count = count + 1),而必须使用 setCount 函数。

以下是定义增加和减少计数功能的代码:
const increment = () => {
setCount(count + 1);
};
const decrement = () => {
setCount(count - 1);
};
步骤五:绑定事件到按钮
最后,我们将这两个函数绑定到按钮的 onClick 事件上。

return (
<div>
<p>当前计数:{count}</p>
<button onClick={increment}>增加</button>
<button onClick={decrement}>减少</button>
</div>
);
当用户点击“增加”按钮时,increment 函数会被调用,它通过 setCount(count + 1) 将 count 状态更新为当前值加1,从而触发组件重新渲染。
完整组件代码
将以上所有步骤组合起来,我们就得到了一个完整的计数器组件:
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1);
};
const decrement = () => {
setCount(count - 1);
};
return (
<div>
<p>当前计数:{count}</p>
<button onClick={increment}>增加</button>
<button onClick={decrement}>减少</button>
</div>
);
}

export default Counter;
核心概念回顾
本节课中我们一起学习了 React 状态管理的核心:
- 状态:组件内部可变的私有数据。
useStateHook:用于在函数组件中添加状态。它返回状态变量和对应的更新函数。- 状态更新:必须通过
useState提供的更新函数(如setCount)来修改状态,直接修改变量不会触发重新渲染。 - 数据驱动视图:状态改变后,React 会自动重新计算组件的 JSX 并更新 DOM,使得界面与数据保持同步。
通过这个计数器示例,你掌握了使用 useState 管理组件状态的基本模式,这是构建交互式 React 应用的基石。
014:后端开发(三) - MongoDB与API构建 🗄️

在本节课中,我们将深入学习后端开发的核心部分,特别是如何使用MongoDB数据库以及构建RESTful API。我们将回顾HTTP协议、REST架构风格,并重点讲解MongoDB的CRUD操作(创建、读取、更新、删除)。课程最后将通过一个实际的代码演示,展示如何连接数据库、处理用户认证以及使用Postman测试API。
概述
上一节我们介绍了后端开发的基本概念和工具。本节中,我们将深入探讨数据库操作,特别是MongoDB的使用。我们将学习如何定义数据模型(Schema),执行CRUD操作,并构建处理用户注册、登录和身份验证的API端点。
核心概念回顾
在深入MongoDB之前,我们先快速回顾几个关键概念。
HTTP协议:它允许客户端与服务器在互联网上通信。HTTP请求负责获取信息并将其显示在屏幕上。
REST:这是一种构建API(应用程序编程接口)的架构风格。它提供了一种标准化的方式,让不同的应用程序能够相互通信。
Postman:这是一个用于测试API的工具。我们可以用它发送模拟的HTTP请求,并检查API是否按预期工作。
数据库简介
数据库用于存储数据。主要有两种类型:
- SQL数据库:例如MySQL。数据以表格形式存储,类似于电子表格,每行代表一条记录。
- NoSQL数据库:例如MongoDB。它不使用表格,而是以类似JSON的文档格式存储数据。
本课程我们将使用MongoDB。
MongoDB 基础
MongoDB的工作方式是基于模式(Schema)来构建数据结构。模式定义了数据如何被添加和存储到数据库中。
例如,一个“用户”对象可能包含“用户名”和“密码”等字段。模式就是这些字段的蓝图。
与数据库交互的核心操作是CRUD命令:
- Create (创建)
- Read (读取)
- Update (更新)
- Delete (删除)
接下来,我们将逐一查看这些操作的语法。
定义数据模式(Schema)
在MongoDB中,我们首先需要定义一个模式。以下是一个示例代码,展示了如何创建一个“待办事项(Todo)”的模式。
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
// 初始化一个新的模式对象
const todoSchema = new Schema({
title: { type: String, required: true },
task: { type: String },
date: { type: Date, default: Date.now },
urgency: { type: String }
});
// 将模式转换为模型(Model),以便在应用中使用
module.exports = mongoose.model('Todo', todoSchema);
这段代码定义了一个包含title、task、date和urgency字段的Todo模式。最后,我们将其导出为一个模型,这样我们就可以在代码的其他部分使用它来创建和查询数据。
创建(Create)操作

创建操作用于向数据库添加新信息,通常对应HTTP的 POST 方法。


以下是处理创建新Todo项目的API端点示例:
router.post('/db', async (req, res) => {
try {
// 从请求体中获取数据
const { title, task, date, urgency } = req.body;
// 根据Schema创建一个新的Todo对象
const newTodo = new Todo({
title,
task,
date,
urgency
});
// 使用.save()方法将对象保存到数据库
await newTodo.save();
// 发送成功响应
res.status(200).json({ message: 'Todo created successfully!', todo: newTodo });
} catch (error) {
// 错误处理
res.status(500).json({ message: 'Server error', error: error.message });
}
});
这段代码的工作流程是:
- 从用户的HTTP POST请求中提取数据。
- 用这些数据创建一个新的Todo文档对象。
- 调用
.save()方法将该对象存储到MongoDB数据库中。 - 根据操作结果,向客户端返回成功(状态码200)或错误(状态码500)响应。
读取(Read)操作
读取操作用于从数据库获取信息,而不修改它,通常对应HTTP的 GET 方法。
以下是获取所有Todo项目的示例:
router.get('/db', async (req, res) => {
try {
// 使用.find()方法查询所有Todo文档
// 如果不传入参数,则返回所有文档
const allTodos = await Todo.find();
// 将查询结果返回给客户端
res.status(200).json(allTodos);
} catch (error) {
res.status(500).json({ message: 'Server error', error: error.message });
}
});
.find()方法是MongoDB提供的查询方法。如果不传递任何参数,它会返回该集合中的所有文档。你也可以传递查询条件来过滤结果,例如 Todo.find({ title: ‘Hello’ })。
更新(Update)操作
更新操作用于修改数据库中已存在的信息,通常对应HTTP的 PUT 方法。
以下是更新Todo项目的示例:
router.put('/db/:id', async (req, res) => {
try {
const { id } = req.params; // 从URL参数中获取ID
const updateData = req.body; // 从请求体中获取要更新的数据
// 使用.findByIdAndUpdate方法查找并更新文档
// { new: true } 选项表示返回更新后的文档
const updatedTodo = await Todo.findByIdAndUpdate(id, updateData, { new: true });
if (!updatedTodo) {
return res.status(404).json({ message: 'Todo not found' });
}
res.status(200).json({ message: 'Todo updated!', todo: updatedTodo });
} catch (error) {
res.status(500).json({ message: 'Server error', error: error.message });
}
});
这个端点首先检查指定ID的文档是否存在。如果存在,则使用.findByIdAndUpdate()方法用新数据替换它。{ new: true }确保返回更新后的文档。
删除(Delete)操作
删除操作用于从数据库中移除信息,对应HTTP的 DELETE 方法。
以下是删除Todo项目的示例:
router.delete('/db/:id', async (req, res) => {
try {
const { id } = req.params;
// 使用.findByIdAndDelete方法查找并删除文档
const deletedTodo = await Todo.findByIdAndDelete(id);
if (!deletedTodo) {
return res.status(404).json({ message: 'Todo not found' });
}
res.status(200).json({ message: 'Todo deleted successfully!' });
} catch (error) {
res.status(500).json({ message: 'Server error', error: error.message });
}
});
这段代码通过ID找到对应的文档,并调用.findByIdAndDelete()方法将其从数据库中永久移除。同样,它包含错误处理,以防文档不存在。
项目演示:用户认证系统
现在,让我们将这些概念应用到一个实际的用户认证系统中。我们将看到如何连接MongoDB,以及如何处理用户注册、登录和身份验证。
1. 项目结构与依赖
一个典型的后端项目结构包含以下部分:
index.js:应用的主入口文件。models/:存放数据模式(Schema)定义。routes/:存放处理不同URL路径(路由)的代码。config/:存放配置文件(如数据库连接)。middleware/:存放中间件函数。
主要依赖包括:
express:用于创建服务器。mongoose:用于连接和操作MongoDB。bcryptjs:用于加密密码。jsonwebtoken:用于创建JSON Web Tokens(JWT)以实现身份验证。
2. 连接数据库
在config/database.js或主文件中,我们配置并连接MongoDB。
const mongoose = require('mongoose');
const connectDB = async () => {
try {
// mongodbUri 是你的MongoDB连接字符串
await mongoose.connect(mongodbUri, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
console.log('MongoDB connected successfully.');
} catch (error) {
console.error('MongoDB connection failed:', error.message);
process.exit(1); // 退出进程
}
};
module.exports = connectDB;
然后在主文件index.js中调用此函数。
3. 用户模型(Schema)
在models/User.js中定义用户数据的结构。
const mongoose = require('mongoose');
const UserSchema = new mongoose.Schema({
username: { type: String, required: true, unique: true },
email: { type: String, required: true, unique: true },
password: { type: String, required: true },
createdAt: { type: Date, default: Date.now }
});
module.exports = mongoose.model('User', UserSchema);
4. 实现路由:注册与登录
在routes/auth.js中,我们处理用户注册和登录的逻辑。
用户注册(POST /signup):
- 检查邮箱是否已存在。
- 创建新用户对象。
- 使用
bcrypt对密码进行“加盐”和哈希加密,增强安全性。 - 将加密后的密码存入数据库。
- 生成一个JWT并返回给用户,用于后续的身份验证。

用户登录(POST /login):
- 根据邮箱查找用户。
- 如果用户不存在或密码不匹配,返回错误。
- 如果验证成功,生成并返回一个JWT。
5. 身份验证中间件与获取用户信息
我们创建一个中间件来验证JWT。然后,可以创建一个受保护的路由,例如 GET /me,用于返回当前登录用户的信息。
// 中间件:验证JWT
const authMiddleware = (req, res, next) => {
const token = req.header('x-auth-token');
if (!token) return res.status(401).json({ msg: 'No token, authorization denied' });
try {
const decoded = jwt.verify(token, 'yourSecretKey'); // 用你的密钥替换
req.user = decoded.userId; // 将用户ID附加到请求对象
next();
} catch (err) {
res.status(401).json({ msg: 'Token is not valid' });
}
};


// 受保护的路由
router.get('/me', authMiddleware, async (req, res) => {
try {
const user = await User.findById(req.user).select('-password'); // 查找用户但不返回密码字段
res.json(user);
} catch (error) {
res.status(500).send('Server error');
}
});
使用Postman测试API
理论之后,实践至关重要。我们可以使用Postman来测试我们构建的API。
- 启动服务器:在终端运行
npm start或node index.js。 - 测试注册:
- 方法:
POST - URL:
http://localhost:4000/api/user/signup - Body (raw JSON):
{ “username”: “test”, “email”: “test@example.com”, “password”: “123456” } - 成功响应应包含一个JWT令牌。
- 方法:
- 测试登录:
- 方法:
POST - URL:
http://localhost:4000/api/user/login - Body:
{ “email”: “test@example.com”, “password”: “123456” } - 成功响应也会返回一个JWT。
- 方法:
- 测试获取用户信息:
- 方法:
GET - URL:
http://localhost:4000/api/user/me - Headers:添加一个键为
x-auth-token,值为上一步获得的JWT。 - 成功响应应返回该用户的信息(不包含密码)。
- 方法:

总结


本节课中,我们一起学习了后端开发中关于数据库和API构建的关键知识。我们回顾了HTTP和REST,深入探讨了MongoDB的CRUD操作,并通过一个用户认证系统的示例,实践了如何连接数据库、定义数据模式、实现业务逻辑(注册、登录)以及使用JWT进行身份验证。最后,我们使用Postman工具对API进行了测试。掌握这些内容是构建功能完整的全栈应用的基础。
015:Lab 1 全栈项目演示 - Spring 2023
概述
在本节课中,我们将一起学习并分析一个名为“Up and Coming”的全栈项目。这是一个用于发布活动和组织志愿者的网站。我们将通过演示了解其功能,并深入解析其前端和后端代码结构,帮助你理解一个完整全栈应用的工作流程。
项目演示与功能概览
上一节我们介绍了本次课程的目标。本节中,我们来看看这个“Up and Coming”网站的具体功能。
这是一个活动发布与志愿者管理网站。以下是其主要功能点:
- 用户系统:用户可以注册新账户或登录现有账户。
- 活动浏览:登录后,主页会展示即将开始和正在进行的活动。
- 活动筛选:活动带有标签,用户可以根据标签筛选活动。
- 创建活动:用户可以填写表单,创建新的活动。
- 个人资料:用户可以查看自己的注册信息。
- 活动报名:用户可以点击活动并报名成为志愿者。
- 数据持久化:用户登出再登录后,其数据和操作记录依然存在。


项目结构与代码解析

了解了网站功能后,我们来看看支撑这些功能的代码是如何组织的。
后端结构解析
后端代码主要遵循将路由和模型分离的通用格式。这与大家在近期作业中接触到的结构类似。
以下是后端代码的核心组成部分:
- 路由:路由文件根据功能模块进行划分。例如,
auth.js处理所有与用户认证相关的端点,如注册 (/register) 和登录 (/login)。events.js则处理活动的创建 (POST /create) 和获取 (GET /events)。 - 模型:模型文件使用 Mongoose 定义数据模式。例如,
User模型定义了用户信息的结构,Event模型定义了活动信息的结构。
在开发后端时,需要思考前端可能需要哪些数据接口,并据此设计相应的路由。
前端结构解析
前端采用 React 框架构建,其文件夹结构清晰,便于管理。
以下是前端项目的典型目录结构:
pages/:存放各个页面对应的组件文件,如SignIn.js(登录页)、Profile.js(个人资料页)。components/:存放可复用的 UI 组件,例如EventCard.js(活动卡片组件)。assets/:存放静态资源,如图片、图标。styles/:存放样式文件。
前后端交互示例
前端与后端的交互主要通过 Axios 库发起 HTTP 请求来实现。数据流通常是从页面组件获取,再传递给子组件。
以下是两个关键交互示例:
- 用户登录:在
SignIn.js中,loginAttempt函数会收集用户输入的用户名和密码,通过 Axios 向后台的/login端点发送 POST 请求。如果登录成功,前端会使用navigate(‘/browse’)将用户重定向到浏览页。axios.post(‘/api/auth/login‘, { username, password }) .then(response => { // 登录成功,重定向 navigate(‘/browse‘); }); - 获取并显示数据:在
Profile.js中,组件加载时会通过 Axios 向后台的/api/profile/events等端点发送 GET 请求,获取当前用户的相关数据(如已报名的活动)。获取到的数据会作为属性传递给EventCard这样的子组件进行渲染。// 在 Profile.js 中获取数据 axios.get(‘/api/profile/events‘) .then(response => { setMyEvents(response.data); }); // 将数据传递给子组件 <EventCard eventData={event} /> - 创建资源:在
Register.js中,注册新用户时,会向后台的/register端点发送 POST 请求,携带新用户的数据。axios.post(‘/api/auth/register‘, newUserData);
本地运行与项目实践

如果你想在本地运行这个项目进行学习,可以按照以下步骤操作:


- 从课程提供的链接下载前后端代码文件。
- 将前端和后端代码分别放入独立的文件夹中。
- 为每个文件夹打开终端,运行
npm install安装依赖。 - 分别运行
npm start启动前端和后端服务器。 - 现在你就可以在浏览器中访问本地运行的网站,并尝试创建用户、发布活动,观察整个应用如何运作。
总结

本节课中,我们一起学习并分析了一个完整的全栈项目“Up and Coming”。我们从网站的功能演示开始,逐步深入到其后端路由与模型的设计,以及前端组件结构与数据流。重点分析了前后端如何通过 Axios 进行 HTTP 通信,以及数据如何从后端数据库流经前端页面,再分发到各个子组件。这个项目清晰地展示了一个典型全栈应用的架构与协作方式,希望对你完成自己的最终项目有所启发。
016:Turso 数据库演示
在本节课中,我们将学习 Turso 数据库。Turso 是一个基于 LibSQL 的边缘托管分布式数据库。我们将了解其核心概念、工作原理,并通过一个简单的 Node.js 和 Express 应用演示其基本用法。
概述:什么是 Turso?
Turso 的定义是:一个基于 LibSQL 的边缘托管分布式数据库。LibSQL 是 SQLite 的一个开源、开放贡献的分支。为了理解这个定义,我们需要逐一拆解其中的术语。
边缘计算与数据边缘
上一节我们介绍了 Turso 的基本定义,本节中我们来看看“边缘托管”和“数据边缘”这两个核心概念。
在传统的客户端-服务器应用架构中,用户请求需要经过多个步骤:从用户设备到 Web 服务器,再到数据库,然后返回。这个往返过程的延迟总和,就是用户感知到的网站速度。
公式: 往返延迟 = 步骤1 + 步骤2 + 步骤3 + 步骤4
当用户与服务器距离很远时,延迟会显著增加,尤其是当网络流量需要跨越大洋时。为了解决这个问题,出现了“边缘计算”的概念。
边缘计算是一种分布式计算范式,它将计算和数据存储移动到更靠近数据源(即用户)的位置,从而改善响应时间并节省带宽。
边缘计算主要分为两种:
- 远边缘: 指物联网设备、移动设备等,它们可能网络能力有限甚至离线。
- 近边缘: 指部署在全球各地的数据中心,提供内容分发网络和边缘函数等服务。
仅仅复制 Web 服务器到边缘还不够,因为数据库查询仍然需要回到中心数据库。为了彻底解决延迟问题,我们需要将数据库也复制到边缘。这就是“数据边缘”的概念。
数据边缘通过将数据库副本放置在靠近查询源(即应用服务器)的位置,实现了最低可能的往返延迟。用户靠近应用服务器,应用服务器靠近数据库,从而最小化了所有环节的延迟。
然而,数据库复制也带来了一些挑战,例如实现难度大、成本较高,以及可能导致数据一致性变弱。Turso 正是为了解决在边缘硬件上运行数据库的挑战而设计的。
SQL 与 NoSQL 数据库对比
了解了数据边缘的概念后,我们来看看 Turso 所基于的数据库类型。Turso 使用 SQL 数据库,这与课程中学习的 MongoDB 不同。
以下是 SQL 和 NoSQL 数据库的主要区别:
- 数据模型:
- SQL: 使用具有行和列的表格来构建高度结构化的数据。
- NoSQL (如 MongoDB): 使用灵活的 JSON 文档存储数据。
- 查询语言:
- SQL: 拥有强大、灵活的标准化查询语言。
- NoSQL: 查询能力通常较弱,主要通过 API 进行。
- 可扩展性:
- SQL: 传统上难以水平扩展,单表数据量极大时性能可能下降。
- NoSQL: 通常设计为易于大规模水平扩展。
SQLite 是一个快速、小巧的数据库,它被直接嵌入到应用程序中。它非常适合在资源有限的边缘设备上运行,也是世界上使用最广泛的数据库之一。然而,SQLite 的一个主要限制是它没有内置的服务器模式,无法直接从网络访问。
LibSQL 与 Turso
为了解决 SQLite 的局限性,ChiselStrike 创建了 LibSQL。LibSQL 是 SQLite 的一个分支,并在此基础上增加了关键功能。
LibSQL 的主要增强包括:
- 服务器模式 (SQLD): 增加了网络访问能力,可以通过 HTTP 或 WebSocket 进行查询。
- 复制功能: 支持在多个 SQLD 实例之间复制数据,这是实现数据边缘的基础。
- 客户端库: 提供了 JavaScript/TypeScript、Rust、Python 等语言的 SDK。
- Wasm 用户定义函数: 支持在数据库内运行自定义代码。
- 开源与开放贡献: 社区可以积极参与项目的改进。
Turso 则是在 LibSQL 之上构建的托管服务。它简化了数据库的配置和管理:
- 自动管理: 自动配置和管理 SQLD 实例。
- 命令行工具 (CLI): 提供便捷的数据库操作命令。
- 令牌认证: 提供基于令牌的客户端认证系统。
用户只需使用 Turso CLI,即可轻松创建数据库和副本,而无需关心底层的 LibSQL 实例。
实战演示:构建一个使用 Turso 的 Express 应用
理论部分已经介绍完毕,现在让我们通过一个实际的 Node.js 项目来演示 Turso 的使用。
步骤 1:初始化项目与安装依赖
首先,创建一个空目录并初始化 Node.js 项目,然后安装必要的依赖。
# 初始化项目
npm init -y
# 安装 TypeScript 及相关类型定义
npm install typescript @types/node --save-dev
# 安装 Express 及其类型定义
npm install express
npm install @types/express --save-dev
# 安装 Turso 的客户端库
npm install @libsql/client
初始化 TypeScript 配置:
npx tsc --init
在生成的 tsconfig.json 中,可以设置 outDir 为 ./build,rootDir 为 ./src。
步骤 2:创建 Turso 数据库并获取连接信息
使用 Turso CLI 创建数据库并获取连接所需的 URL 和令牌。
# 登录 Turso (使用 GitHub 账户)
turso auth login
# 查看可用的部署位置
turso db locations
# 创建数据库(会自动选择离你最近的位置)
turso db create my-demo-db
# 显示数据库信息,获取 URL
turso db show my-demo-db
# 创建访问令牌
turso db tokens create my-demo-db
步骤 3:编写数据库操作代码
创建 src/demo.ts 文件,编写连接数据库和执行查询的代码。
import { createClient } from "@libsql/client";
async function main() {
// 配置客户端,使用从 CLI 获取的 URL 和令牌
const config = {
url: "libsql://your-database-url.turso.io",
authToken: "your-super-secret-auth-token",
};
const client = createClient(config);
try {
// 执行一个查询
const rs = await client.execute("SELECT * FROM users");
console.log(rs);
} catch (e) {
console.error(e);
} finally {
// 关闭客户端连接
client.close();
}
}

main();
步骤 4:创建 Express 服务器并集成 Turso
创建 src/server.ts 文件,构建一个简单的 API 服务器。
import express from "express";
import { createClient } from "@libsql/client";
const app = express();
const port = 3000;
// 配置数据库客户端(应在环境变量中管理敏感信息)
const dbClient = createClient({
url: process.env.DATABASE_URL!,
authToken: process.env.DATABASE_AUTH_TOKEN!,
});
// 定义路由:获取所有用户
app.get("/users", async (req, res) => {
try {
const rs = await dbClient.execute("SELECT * FROM users");
res.json(rs);
} catch (error) {
console.error(error);
res.status(500).send("Database error");
}
});
// 定义路由:添加新用户
app.get("/add-user", async (req, res) => {
const email = req.query.email;
// 验证 email 参数
if (typeof email !== "string") {
res.status(400).send("Email query parameter is required and must be a string");
return;
}
const userId = `user-${Math.random().toString(36).substr(2, 9)}`; // 生成随机用户ID
try {
await dbClient.execute({
sql: "INSERT INTO users (id, email, coins) VALUES (?, ?, ?)",
args: [userId, email, 0], // 新用户硬币数为 0
});
res.send(`User added with ID: ${userId}`);
} catch (error) {
console.error(error);
res.status(500).send("Failed to add user");
}
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
步骤 5:创建数据库表
在运行服务器前,需要先创建表。可以使用 Turso Shell:
# 打开数据库的交互式 Shell
turso db shell my-demo-db
在 Shell 中执行 SQL:
CREATE TABLE users (
id TEXT PRIMARY KEY,
email TEXT NOT NULL,
coins INTEGER DEFAULT 0
);
步骤 6:运行与测试
编译并运行 TypeScript 代码,然后测试 API。
# 编译 TypeScript
npx tsc
# 运行服务器(确保已设置环境变量)
DATABASE_URL="your-url" DATABASE_AUTH_TOKEN="your-token" node build/server.js
使用 curl 或浏览器测试 API 端点:
# 获取用户列表
curl http://localhost:3000/users
# 添加新用户
curl "http://localhost:3000/add-user?email=newuser@example.com"
步骤 7:创建数据库副本
为了演示边缘复制,可以为数据库在另一个地理位置创建一个副本。
# 查看所有可用位置
turso db locations
# 在圣何塞 (SJC) 创建一个副本
turso db replicate my-demo-db sjc
# 再次查看数据库状态,现在应该有一个主实例和一个副本
turso db show my-demo-db
创建副本后,应用程序使用的逻辑 URL 会自动将查询路由到离用户最近的数据库实例。
总结

本节课中我们一起学习了 Turso 数据库。我们从其作为基于 LibSQL 的边缘托管分布式数据库的定义开始,深入探讨了“边缘计算”和“数据边缘”的概念,了解了它们如何通过将数据和计算移至用户附近来降低延迟。我们对比了 SQL 和 NoSQL 数据库的特点,并解释了 LibSQL 如何通过为 SQLite 添加服务器模式和复制功能来满足边缘计算的需求。最后,通过一个完整的实战演示,我们一步步创建了一个使用 Turso 的 Node.js 和 Express 应用,包括初始化项目、创建数据库、执行 SQL 操作、构建 API 以及创建数据库副本。Turso 简化了边缘分布式数据库的管理,使开发者能够更轻松地为全球用户构建高性能的应用。

浙公网安备 33010602011771号