Web 开发 23
1 MongoDB
MongoDB 是一种文档数据库,它以类 JSON 文档的形式存储数据。
对于程序员来说,这种存储方式很自然,因为程序员通常以对象的思维来思考,而 MongoDB 的文档模型能很好地契合这种思维。
相比传统的行 / 列数据模型,MongoDB 在数据表达和功能上更加灵活、强大,在对象使用方面相当灵活。
MongoDB 是一款基于分布式文件存储的开源文档数据库,属于 NoSQL 数据库的一种。以下从不同角度为你详细介绍:
数据存储结构
- 以文档形式存储:MongoDB 不像关系型数据库那样用表格(行和列)存储数据,而是以类似 JSON(JavaScript Object Notation)的 BSON(Binary JSON,二进制形式的 JSON)格式来存储数据, 这些数据结构被称为 “文档”。例如一个简单的用户信息文档:
{"_id": "123456","name": "John Doe","age": 30,"email": "johndoe@example.com","address": {"street": "123 Main St","city": "Anytown","state": "CA","zip": "12345"},"hobbies": ["reading", "swimming"]
}
- 集合概念:多个相关的文档组合在一起就形成了集合(Collection),类似于关系型数据库中的表,但集合没有严格的模式(Schema)限制,不同文档的字段可以不一样,这让它在处理数据结构经常变化的数据时,具有很大的优势。
主要特性
- 高扩展性:支持水平扩展,可以通过添加更多的服务器(节点)来处理不断增长的数据量和访问负载。这使得它非常适合互联网应用,能够轻松应对流量的大幅波动。
- 高性能:MongoDB 对数据的读写性能都比较出色,通过索引、内存映射文件等技术,能快速地查询和更新数据。
- 灵活的查询:提供了丰富的查询方式,可以根据文档中的任何字段进行查询,还支持复杂的聚合操作、文本搜索等,方便开发者获取所需的数据。
应用场景
- 内容管理系统:像博客平台、新闻网站等,内容的结构可能经常变化,MongoDB 灵活的模式能很好适应这种需求,方便存储和管理文章、评论等信息。
- 电商平台:可以用来存储商品信息、用户订单记录等。例如商品信息中,不同类别的商品属性不同,MongoDB 无需严格的模式就能轻松存储这些差异较大的数据。
- 实时数据分析:在一些需要实时处理和分析大量数据的场景中,MongoDB 的高性能和灵活查询能力,能快速对数据进行统计、分析,为决策提供支持 。
与关系型数据库对比
- 模式灵活性:关系型数据库有严格的模式定义,表结构一旦确定,修改成本较高;MongoDB 模式灵活,更适合处理快速变化或结构不固定的数据。
- 数据关联:关系型数据库通过表之间的外键等方式建立关联,查询关联数据可能需要复杂的连接(Join)操作;MongoDB 更强调单个文档内的数据完整性,虽然也能实现数据关联,但方式和关系型数据库有所不同,且在处理复杂关联时不如关系型数据库直接。
MongoDB Atlas
MongoDB Atlas 是 MongoDB 的云服务平台,能在 “云端”(比如借助 AWS 等云平台)运行 MongoDB 数据库。它让使用者更轻松,无需在个人笔记本电脑上运行数据库,数据库会由平台代为管理,还能和团队成员共享数据。而且它比笔记本电脑的固态硬盘更可靠,会对数据进行复制备份,保障数据安全。
2 MongoDB 和 MongoDB Atlas 工作过程
在 MongoDB Atlas 上配置 MongoDB 后,后端(服务器)通过网络访问 MongoDB 的架构。其中涉及的实体有 MongoDB(数据库)、MongoDB Atlas(MongoDB 的云服务平台)以及后端服务器相关的 server.js
(服务端的 JavaScript 文件,用于和 MongoDB 交互)
MongoDB 的副本集(Replica Set)相关内容,体现了 Atlas 保障数据不丢失的机制。
MongoDB 通过副本集来确保数据安全,副本集是将相同的数据复制到 3 台不同的机器上。其中有一台是主 MongoDB(Primary MongoDB),另外还有从 MongoDB(Secondary MongoDB)。后端服务器(通过 server.js)与主 MongoDB 交互,而数据会同步到从 MongoDB,这样即便其中一台机器出现故障,也有其他机器存储着相同数据,从而保证数据不会丢失。
3 GET/POST请求读取数据的完整流程
这张图展示了通过 GET 请求读取数据的完整流程,以及前端(Frontend)、后端(Backend)和数据库(Database)各自的角色与交互方式,下面详细解释并扩展:
流程步骤详解
- 发起请求(前端 → 后端)
- 你(用户)在前端应用(比如用 React 组件、HTML、CSS 等构建的界面)中,触发了
get("/api/stories")
这个请求。这个请求会调用后端的 API 接口,目的是获取故事(stories)相关的数据。
- 你(用户)在前端应用(比如用 React 组件、HTML、CSS 等构建的界面)中,触发了
- 后端查询数据库(后端 → 数据库)
- 后端接收到请求后,通过数据库管理系统(DBMS)执行
DBMS.find(Stories)
操作。DBMS 会借助自身的功能,在数据库中查找所有的故事数据。数据库是以有组织的方式存储数据的系统,通常经过优化且能防故障,支持对数据的创建、读取、更新、删除等操作。
- 后端接收到请求后,通过数据库管理系统(DBMS)执行
- 数据库返回数据(数据库 → 后端)
- 数据库找到所有故事数据后,会把这些数据列表发送回后端。
- 后端返回数据(后端 → 前端)
- 后端接收到数据库返回的故事数据列表后,再把这些数据发送回前端。前端拿到数据后,就可以将其展示给用户了。
各部分角色扩展
- 前端(Frontend)
- 职责:负责展示用户眼前的内容。它是用户直接交互的部分,需要将数据以友好的界面呈现出来。除了图中提到的 React 组件、HTML、CSS,还可能用到 Vue、Angular 等框架或技术,来构建动态、美观的用户界面,处理用户的交互操作,比如点击、输入等,然后根据需要向后端请求数据。
- 后端(Backend)
- 职责:在幕后工作,负责给前端提供它需要展示的信息,以及更新来自前端的新信息。后端需要处理业务逻辑、数据验证、与数据库的交互等。它可以用多种语言和框架实现,比如 Node.js(配合 Express 等框架)、Python(配合 Django、Flask 等框架)、Java(配合 Spring 等框架)等。后端会接收前端的请求,进行相应的处理,再与数据库交互获取或存储数据,最后将结果返回给前端。
- 数据库(Database)
- 职责:存储数据,并且通过 DBMS 支持对数据的各种操作。除了关系型数据库(如 MySQL、PostgreSQL),还有非关系型数据库(如 MongoDB)等不同类型,可根据应用的需求(比如数据结构是否固定、对读写性能的要求等)来选择。数据库要保证数据的完整性、一致性和可用性,以支持应用的正常运行。
这张图展示了 POST 请求用于创建、更新、删除数据 的流程,以及前端(Frontend)、后端(Backend)和数据库(Database)的协作关系,以下是详细解释与扩展:
1. 流程步骤拆解
步骤①:前端发起 POST 请求用户在前端应用(比如由 React 组件、HTML、CSS 等构建的界面)中,调用
post("/api/stories", {content: "new story!"})
接口。这个请求会携带 “新故事内容为‘new story!’” 的数据,发送到后端的 API。步骤②:后端通过 DBMS 操作数据库后端接收到请求后,借助 数据库管理系统(DBMS) 执行
DBMS.add(Storys, {_id: 5, content: "new story!"})
操作。DBMS 会把新故事数据(带编号_id: 5
)添加到数据库的Storys
集合中。数据流转逻辑数据库以有序、优化且防故障的方式存储数据,支持通过 DBMS 对数据进行创建、读取、更新、删除等操作。当后端通过 DBMS 完成数据添加后,后续若前端需要展示更新后的数据,还可通过 GET 请求(类似之前 “读取数据” 的流程)获取最新内容。
2. 各部分角色扩展
前端(Frontend)负责向用户展示 “眼前的内容”,是用户直接交互的界面层。除了图中提到的 React、HTML、CSS,还可使用 Vue、Angular 等框架,核心作用是接收用户操作(如点击 “发布新故事” 按钮)并展示数据(如渲染故事列表)。
后端(Backend)在 “幕后工作”,负责给前端提供所需的信息,以及处理前端传来的新数据(如新增、修改故事)。后端会处理业务逻辑、数据验证,并通过 DBMS 与数据库交互。技术上可采用 Node.js(配合 Express)、Python(配合 Django/Flask)、Java(配合 Spring)等语言和框架。
数据库(Database)以结构化方式存储数据的系统,确保数据的完整性与可用性。除了图中隐含的 “支持增删改查”,不同类型的数据库(如关系型的 MySQL、非关系型的 MongoDB)在数据模型、性能、适用场景上有差异:
- 关系型数据库(如 MySQL)适合数据结构固定、需复杂关联查询的场景(如电商订单与用户的关联)。
- 非关系型数据库(如 MongoDB)适合数据结构灵活、需高扩展性的场景(如社交平台的动态内容)。
3. POST 请求的核心特点
POST 请求常用于向服务器提交数据(创建新资源、更新 / 删除已有资源),与 GET 请求(主要用于 “读取数据”,参数在 URL 中,长度有限)不同:
- POST 的数据放在请求体中,可传输大量 / 敏感数据(如表单提交、文件上传)。
- 语义上,POST 更倾向于 “修改服务器状态”,而 GET 是 “获取服务器资源”。
通过这个流程,前端、后端、数据库形成了 “用户操作 → 前端请求 → 后端处理 → 数据库存储 / 读取 → 结果返回前端” 的完整闭环,支撑起 Web 应用的 “数据交互” 核心能力。
4 Promise.any()
JavaScript 里的 Promise.any()
方法,它是用来处理 多个 Promise(异步操作) 的
代码和逻辑解释
- 首先,代码里创建了 5 个
promise
(promise1
到promise5
),每个都是调用get
接口去请求评论数据(模拟异步请求,比如从服务器拿数据)。 - 然后把这 5 个
promise
放到一个数组promises
里。 - 接着用
Promise.any(promises)
:这个方法的特点是,只要数组里有一个 Promise 成功(“兑现”),整个Promise.any
就会成功,并返回那个最先成功的 Promise 的结果;只有当所有 Promise 都失败时,它才会进入catch
捕获错误。
举个更通俗的例子
比如你同时给 5 个朋友发消息借充电宝,Promise.any
就像是 “只要有一个朋友回复能借你(成功),你就拿到充电宝了;只有所有朋友都回复没法借(都失败),你才借不到”。
这样设计的好处是,在一些场景下,你不需要等所有异步操作都完成,只要有一个成功就够了(比如多个服务器节点请求资源,只要有一个节点返回数据,就可以用了)。
5 同步控制流(Synchronous Control Flow)
这张图展示的是同步控制流(Synchronous Control Flow)的概念,下面详细解释并扩展相关内容:
核心概念
同步控制流是指程序按照代码书写的顺序,逐行依次执行,前一个操作完成后,才会开始下一个操作,执行过程中不会出现 “中断” 去执行其他无关任务的情况。
图中元素解析
- 左侧 “Key(图例)”:
const x = 5;
这类方框代表代码片段,展示具体要执行的指令。- 带实心箭头的方框代表控制流,即程序执行的顺序走向。
- 带虚线箭头的方框代表可能的控制流,暗示存在其他潜在执行路径的可能性(不过在同步控制流的典型场景里,主要还是按确定顺序执行)。
- 右侧 “同步控制流” 核心流程:
const stories = syncGet("/api/stories");
:- 调用
syncGet
函数从/api/stories
接口同步获取数据。在同步执行模式下,程序会 “等待” 这个请求完成(拿到数据后),才会继续往下执行。
- 调用
syncSetStories(stories);
:- 当
syncGet
成功获取到stories
数据后,再调用syncSetStories
函数,将获取到的stories
数据进行处理(比如存储到某个状态管理容器、渲染到页面等)。
- 当
handleClick()
:- 前两个操作都完成后,才会执行
handleClick
函数(比如处理点击事件相关的逻辑)。整个过程是严格 “串行” 的,步骤 2 没完成,步骤 3 绝对不会开始。
- 前两个操作都完成后,才会执行
扩展:同步 vs 异步控制流
- 同步的局限性:同步执行在遇到耗时操作(比如网络请求、文件读取)时,会导致程序 “阻塞”。例如,如果
syncGet
是一个需要 10 秒才能完成的网络请求,那么在这 10 秒内,程序会卡在这一行,后面的syncSetStories
和handleClick
都无法执行,页面也会失去响应(比如前端场景中,浏览器会出现 “卡顿”)。 - 异步控制流的补充(对比理解):为了解决同步的 “阻塞” 问题,就有了异步控制流。异步模式下,遇到耗时操作时,程序会 “先挂起” 这个操作,继续执行后面的代码;等耗时操作完成后,再通过 “回调函数”“Promise”“async/await” 等机制,去执行后续关联的逻辑。比如用异步的
fetch
替代同步的syncGet
,代码大致会是:fetch("/api/stories").then(response => response.json()).then(stories => {syncSetStories(stories);handleClick();}); // 上面的 fetch 调用后,不会阻塞后续代码(如果有的话),等请求完成后,才会进入 .then 里执行后续逻辑
同步控制流的适用场景
同步控制流并非 “不好”,它适合操作之间有强依赖、且操作耗时极短的场景。比如简单的变量计算、本地数据的简单处理等,这些操作瞬间就能完成,用同步执行既简单又直观。
总结来说,这张图通过简洁的流程展示,让我们快速理解了 “同步控制流是按顺序、串行执行代码” 的核心逻辑,也为理解更复杂的异步控制流打下了基础。
6 异步控制流(Asynchronous Control Flow)
这张图展示的是异步控制流(Asynchronous Control Flow)的概念,下面详细解释并扩展相关内容:
核心概念
异步控制流是指程序执行时,遇到耗时操作(如网络请求、文件读写等),不会 “阻塞” 等待该操作完成,而是会继续执行后续代码;等耗时操作完成后,再通过特定机制(如回调函数、Promise、async/await 等)执行与该操作结果相关的逻辑。
图中元素解析
- 左侧 “Key(图例)”:
const x = 5;
这类方框代表代码片段,是具体要执行的指令。- 带实心箭头的方框代表控制流,即程序执行的顺序走向。
- 带虚线箭头的方框代表可能的控制流,体现异步场景下执行路径的灵活性(不严格串行)。
- 右侧 “异步控制流” 核心流程:
const storiesPromise = get("/api/stories");
:- 调用
get
函数向/api/stories
接口发起请求。 - 这里的
get
是异步函数,它不会让程序 “停在这一行等结果”,而是会立即返回一个Promise
对象(storiesPromise
),然后程序会继续执行后面的代码。
- 调用
storiesPromise.then((stories) => setStories(stories))
:- 对
storiesPromise
注册一个 “回调”:当get
发起的请求成功拿到数据后,会触发then
里的箭头函数,将数据stories
传给setStories
进行处理(比如更新页面状态、存储数据等)。
- 对
handleClick()
:- 由于
get
是异步的,在发起请求后,程序不会等待请求完成,会立即执行handleClick
函数(比如处理点击事件等逻辑)。这就是异步 “不阻塞” 的体现 —— 耗时的网络请求和handleClick
的执行是 “并行” 推进的(宏观上)。
- 由于
扩展:Promise 与异步的优势
- Promise 的作用:
Promise
是 JavaScript 中处理异步操作的重要对象,它有三种状态:pending
(进行中)、fulfilled
(成功)、rejected
(失败)。通过Promise
,我们可以更优雅地管理异步操作的 “成功回调” 和 “失败回调”,避免传统回调函数嵌套过深(“回调地狱”)的问题。 - 异步的核心优势:解决了同步控制流的 “阻塞” 问题,提升程序的响应性和执行效率。比如在前端场景中,用户点击按钮后发起网络请求,同时还能滚动页面、点击其他按钮(因为请求是异步的,没阻塞主线程);等请求完成,再更新页面数据,用户体验更流畅。
异步 vs 同步(对比强化理解)
- 同步场景:发起请求 → 等待请求完成 → 处理数据 → 执行其他逻辑(步骤间严格串行,等待时程序 “卡死”)。
- 异步场景:发起请求(同时拿到
Promise
)→ 执行其他逻辑(如handleClick
)→ (请求完成后)处理数据(通过Promise.then
)。
总结来说,这张图清晰展示了异步控制流 “非阻塞、异步推进” 的核心特点,而 Promise
是实现这种控制流的重要工具,它让异步代码的组织和管理更清晰。
7 await
关键字
这张图展示了 JavaScript 中 await
关键字在处理异步操作(基于 Promise
)时的用法,下面详细解释并扩展相关内容:
核心代码与 await
作用
const a = slowNumber(9);
const b = slowNumber(10);
console.log(await a + await b);
slowNumber
是一个返回Promise
的异步函数(假设它内部是耗时操作,比如模拟网络请求、延迟计算等,最终会返回一个Promise
)。const a = slowNumber(9);
和const b = slowNumber(10);
:这两行会立即触发slowNumber
对应的异步操作,同时a
和b
会被赋值为Promise
对象(此时异步操作处于 “进行中” 状态)。console.log(await a + await b);
:await a
:会暂停当前函数的执行,等待a
对应的Promise
“决议”(即Promise
变为fulfilled
状态,拿到成功结果;或变为rejected
状态,抛出错误)。- 同理,
await b
会等待b
对应的Promise
决议。 - 当
a
和b
的Promise
都成功决议后,会将它们的结果相加,再打印到控制台。
图中文字解析
- “
await
waits for the promise toresolve
and uses that value”:意思是await
会等待Promise
完成 “决议”(resolve
表示Promise
成功,会传递结果值),然后使用这个结果值。 - 下方中文 + 英文补充:“需要注意的是,如果一个承诺没有被兑现,如果它被拒绝(One thing to note is that if a promise is not fulfilled, if it rejects, then Await is...)”:补充了
await
处理Promise
失败的场景 —— 如果Promise
被拒绝(reject
),await
会抛出一个错误,此时需要用try...catch
来捕获错误,否则整个程序会因未捕获的错误而中断。
扩展:await
的使用场景与注意事项
1. 必须在异步函数中使用
await
只能在被 async
修饰的函数内部使用。比如,上面的代码如果要运行,需要包裹在 async
函数里:
async function example() {const a = slowNumber(9);const b = slowNumber(10);console.log(await a + await b);
}
example();
2. 处理 Promise
拒绝(错误)
如果 slowNumber
返回的 Promise
被拒绝,await
会抛出错误,所以实际开发中通常会用 try...catch
包裹:
async function example() {try {const a = slowNumber(9);const b = slowNumber(10);console.log(await a + await b);} catch (error) {console.error("异步操作出错了:", error);}
}
example();
3. 并行 vs 串行执行
在图中的代码里,a
和 b
的 Promise
是并行发起的(因为先同时调用 slowNumber(9)
和 slowNumber(10)
,再分别 await
),这比 “先 await a
,再 await b
” 的串行方式更高效(节省总耗时)。如果要更明确地控制并行,也可以用 Promise.all
:
async function example() {const [resultA, resultB] = await Promise.all([slowNumber(9), slowNumber(10)]);console.log(resultA + resultB);
}
example();
Promise.all
会等待所有传入的 Promise
都成功决议,再将结果按顺序组成数组返回,这样写法更简洁,也明确体现 “并行等待多个 Promise
” 的意图。
总结来说,await
是 JavaScript 中让异步代码看起来更像同步代码的语法糖,它基于 Promise
工作,能优雅地处理异步操作的结果或错误,不过要注意其使用范围(async
函数内)和错误处理逻辑。
答案是 8 秒。
解释:因为代码是串行执行的,先执行 const a = await ...
,等待 3 秒拿到 a
的结果;然后执行 const b = await ...
,再等待 5 秒拿到 b
的结果;最后才会执行 console.log()
。所以总等待时间是 (3 + 5 = 8) 秒,之后才会打印。
8 async/await
语法的使用
代码解析
const myFunction = async () => { ... }
:这里定义了一个异步箭头函数myFunction
,async
关键字标记该函数为异步函数,使得函数内部可以使用await
关键字。console.log(await a + await b);
:在异步函数内部,使用await
关键字等待a
和b
(假设a
、b
是返回Promise
的异步操作)的结果,等拿到结果后,将它们相加并打印。myFunction();
:调用这个异步函数。
文字说明解析
- “We can use the keyword await in async functions”:意思是我们可以在异步函数(用
async
标记的函数)中使用await
关键字。await
会等待Promise
完成(决议为成功或失败),并获取其结果(或捕获错误)。 - 下方中文和英文补充:“如果你担心时间问题,你可能会想使用承诺函数之一,比如(You might want to use one of the promised functions, like promises.all, to make it)”:提示如果关注异步操作的 “时间效率”(比如希望多个异步操作并行执行,减少总耗时),可以使用
Promise.all
等Promise
相关的方法。例如,若a
和b
对应的异步操作可以并行发起,用Promise.all([a, b]).then(([resultA, resultB]) => console.log(resultA + resultB))
会比串行的await a + await b
更高效(总耗时取a
和b
中耗时较长的那个,而非两者耗时之和)。
Promise.then
链式调用的等价关系
核心意图
图中想表达的是:async/await
写法和 Promise.then
的链式调用,在功能上是等价的。async/await
只是让异步代码的书写和阅读,更接近同步代码的风格,本质上还是基于 Promise
实现的异步处理。
代码解析
- 上方代码(
async/await
版本):const myFunction = async () => { ... }
:定义了一个异步箭头函数myFunction
,async
关键字使得函数内部可以使用await
。console.log(await a + await b);
:在函数内部,await a
会等待a
这个Promise
完成并获取其结果,await b
同理,然后将两个结果相加并打印。myFunction();
:调用这个异步函数。
- 下方代码(
Promise.then
链式调用版本):a.then((aVal) => { ... })
:先等待a
这个Promise
完成,拿到结果aVal
后,再执行回调函数。- 在
a
的回调函数内部,又调用b.then((bVal) => { ... })
:等待b
这个Promise
完成,拿到结果bVal
后,将aVal
和bVal
相加并打印。
用await语法更简洁便利!!!!
9 React 的 useEffect
中使用传统 Promise和async/await处理异步请求的方式
这张图对比了在 React 的 useEffect
中使用传统 Promise和async/await处理异步请求的方式,并解释了相关限制:
左侧(Traditional Promises)
useEffect(() => {get("/api/stories").then((storyObjs) => {setStories(storyObjs);});
}, []);
- 利用
Promise
的.then
链式调用处理异步请求。当get("/api/stories")
(返回一个Promise
)成功后,在.then
里调用setStories
更新状态。
右侧(async await)
useEffect(() => {const getStories = async () => {const storyObjs = await get("/api/stories");setStories(storyObjs);};getStories();
}, []);
- 因为
useEffect
的回调函数不能直接标记为async
(否则会隐式返回Promise
,可能导致副作用执行不符合预期),所以需要在useEffect
内部定义并调用一个异步函数(getStories
)。 - 在
getStories
中,用await
等待get("/api/stories")
完成,拿到结果后再调用setStories
。
文字说明核心点
“Using async await notation to perform a get request in useEffect requires us to define and call an async function inside the callback function passed to useEffect. But what we can't do is we can't await an asynchronous function inside of a [synchronous function].”
- 意思是:在
useEffect
中使用async/await
处理请求时,必须在useEffect
的回调(同步函数)内部,定义并调用一个异步函数;不能直接在useEffect
的同步回调里await
异步操作(因为同步函数无法被await
,会报错或导致逻辑异常)。
简单来说,图中展示了 React 中 useEffect
处理异步请求的两种写法,并解释了 async/await
在此场景下的 “特殊写法要求”—— 要嵌套定义异步函数来用 await
。