勇闯前后端 Week

发布时间:2026/9/18 21:53:05

勇闯前后端 Week 开篇Week2 学习地图如果把 Week1 看作是搭好前后端学习的地基那么 Week2 就是正式开始盖楼的一周。Week1 里我们认识了 HTML、CSS 的基础语法也初步接触了 JavaScript 的变量、函数和简单的事件绑定同时在 Node.js 环境里跑通了第一个脚本。到了 Week2知识密度会明显上升这也是很多学习者感到「从入门到熟练」的关键转折点。本周的核心任务可以概括为三句话把 JavaScript 的底层机制吃透把 Node.js 的服务端能力用起来最后让前端页面和后端接口完成第一次真正的数据联动。下面这张学习地图可以帮助你先建立全局认识。学习模块核心内容学习目标建议时长前端进阶作用域、闭包、原型链、this、异步编程、事件循环能解释并手写常见面试题代码3 天ES6 重点语法箭头函数、解构、模板字符串、Promise、async/await能熟练用现代语法组织代码1.5 天后端基础模块系统、npm、fs、path、http 模块能读懂并手写基础服务端脚本1 天Express 入门路由、中间件、REST API 设计能搭建可运行的接口服务1 天前后端联调fetch、CORS、Todo 实战项目能完成一次完整的增删改查联调0.5 天整篇文章会按照「前端深层机制 → 现代语法 → 后端核心能力 → 前后端实战」的顺序展开。每个部分都会配合可运行的代码示例和常见避坑建议建议你跟着代码亲手敲一遍而不是只停留在阅读层面。一、JavaScript 核心机制作用域与闭包前端学习的第一个分水岭往往就出现在作用域和闭包这里。很多开发者写了好几年代码遇到变量污染、内存泄漏、循环绑定事件取值错误等问题时根源其实都能追溯到对作用域链和闭包的理解不够扎实。1.1 作用域到底是什么作用域可以理解为一套「变量可见范围」的规则。JavaScript 中有函数作用域和块级作用域两种主要形态。在 ES6 之前只有全局作用域和函数作用域var声明的变量在函数内可见但不受大括号限制ES6 引入let和const之后才有了真正意义上的块级作用域。var name 全局变量; function showName() { var name 函数内变量; console.log(name); // 函数内变量 } showName(); console.log(name); // 全局变量 if (true) { let blockVar 块级变量; var functionVar 函数级变量; } // console.log(blockVar); // 报错blockVar is not defined console.log(functionVar); // 函数级变量可以访问上面的代码说明了let和var最直观的区别。块级作用域让循环、条件分支中的临时变量不再泄漏到外部这是现代 JavaScript 强烈推荐使用const和let替代var的重要原因。1.2 作用域链的查找规则当代码在某个位置引用一个变量时JavaScript 引擎会从当前作用域开始逐层向外查找直到找到变量或抵达全局作用域。如果全局作用域也没有就会抛出ReferenceError。这条查找路径就是作用域链。const outer 外部变量; function outerFn() { const inner 内部变量; function innerFn() { const deepest 最内层变量; console.log(deepest); // 最内层变量 console.log(inner); // 内部变量向上一层查找 console.log(outer); // 外部变量继续向上查找 } innerFn(); } outerFn();理解作用域链的要点是查找方向是「由内向外」而不是「由外向内」。内部函数可以访问外部变量外部无法直接访问内部变量。这也是闭包能够实现数据封装的基础。1.3 闭包保留函数诞生时的环境闭包是指函数和它所在词法作用域的组合。简单来说当一个内部函数被返回到外部作用域或者在外部被调用时它仍然能够记住并访问自己定义时所在的变量环境这个机制就是闭包。function createCounter() { let count 0; return function () { count 1; return count; }; } const counter createCounter(); console.log(counter()); // 1 console.log(counter()); // 2 console.log(counter()); // 3在这个例子中count是createCounter函数内的局部变量。按照常规认知函数执行完毕后局部变量应该被销毁但由于返回的内部函数仍然引用着countJavaScript 引擎会保留这份变量环境从而让counter每次调用都能基于上一次的结果继续累加。闭包最典型的应用场景有三类一是创建私有变量避免全局污染二是封装模块和回调三是为函数「记住」某些状态比如防抖、节流、柯里化等。需要特别提醒的是闭包虽然强大但如果大量创建又不及时释放引用可能导致变量长期驻留内存所以在不需要使用时应该把引用置为null帮助垃圾回收。1.4 循环中 var 与 let 的区别这是闭包问题中最经典的面试场景用一个循环给多个按钮绑定点击事件却发现自己每次点击打印的都是同一个值。// 使用 var 时的问题 for (var i 0; i 5; i) { setTimeout(function () { console.log(i); }, 0); } // 输出结果5 5 5 5 5 // 使用 let 修复 for (let j 0; j 5; j) { setTimeout(function () { console.log(j); }, 0); } // 输出结果0 1 2 3 4产生差异的原因是var声明的i是函数作用域变量循环结束后i已经变成 5而所有回调函数共享同一个ilet则会在每次迭代中创建独立的块级作用域副本每个回调捕捉到的是当次迭代的j。如果必须使用var也可以通过立即执行函数或闭包捕获值来修复但现在更推荐直接使用let。二、原型链与 this 指向如果说闭包是作用域层面的核心概念那么原型链就是对象层面的核心概念。JavaScript 的继承并不像 Java 那样基于类而是基于原型对象逐层向上查找属性这条查找路径就是原型链。2.1 原型对象与 __proto__在 JavaScript 中每个对象都有一个内部属性[[Prototype]]在浏览器环境中可以通过__proto__访问。构造函数的prototype属性和实例的__proto__指向同一个原型对象实例可以通过原型链访问构造函数的原型方法。function Person(name) { this.name name; } Person.prototype.sayHello function () { return 你好我是 ${this.name}; }; const person new Person(小明); console.log(person.sayHello()); // 你好我是 小明 console.log(person.proto Person.prototype); // true console.log(Person.prototype.constructor Person); // true这里有三个重要关系需要理清实例的__proto__指向构造函数的prototype原型对象的constructor指回构造函数原型对象本身也是一个普通对象它也有自己的__proto__最终指向Object.prototype而Object.prototype.__proto__为null这是原型链的终点。建议使用Object.getPrototypeOf(obj)来获取原型因为__proto__曾经是非标准属性不同运行环境可能存在差异。学习阶段用__proto__便于理解生产代码中优先使用标准 API。2.2 从原型到原型链当访问一个对象的属性时引擎会先在对象自身查找如果没有找到就沿着__proto__向上继续查找直到找到属性或抵达null。这种连续向上查找的机制使对象可以继承原型链上所有对象的方法和属性。const arr [1, 2, 3]; console.log(arr.__proto__ Array.prototype); // true console.log(Array.prototype.__proto__ Object.prototype); // true console.log(Object.prototype.__proto__); // null理解原型链后很多现象就变得清晰了数组实例能够直接调用push、map、filter等方法是因为这些方法定义在Array.prototype上普通对象能调用toString、hasOwnProperty是因为它们定义在Object.prototype上。2.3 this 的四种绑定规则this 的指向是 JavaScript 中另一个容易让开发者头疼的问题。它不像词法作用域那样在编写代码时就静态确定而是根据调用方式动态确定。判断 this 指向可以记住以下四条规则。默认绑定独立函数调用时非严格模式下 this 指向全局对象严格模式下为 undefined。隐式绑定通过对象调用方法时this 指向调用方法的那个对象。显式绑定使用 call、apply、bind 显式指定 this。new 绑定使用 new 调用构造函数时this 指向新创建的对象。const obj { value: 10, getValue: function () { return this.value; } }; console.log(obj.getValue()); // 10隐式绑定 const unbound obj.getValue; // console.log(unbound()); // 严格模式下报错默认绑定为 undefined const bound obj.getValue.bind(obj); console.log(bound()); // 10显式绑定这里最容易踩坑的是把对象方法拆出来赋值给一个变量后再调用此时原本的隐式绑定会丢失变成默认绑定。项目中常见的回调函数里this不符合预期的现象多数都源于这个原因。箭头函数没有自己的 this它会继承外层作用域的 this这一点在事件处理和异步回调中非常实用我们后续会在语法部分详细介绍。三、异步编程从回调地狱到 async/awaitJavaScript 天生是单线程语言但它通过事件循环机制实现了非阻塞的异步能力。早期处理异步只能依靠回调函数当多个异步操作存在先后依赖时代码会层层嵌套形成所谓的「回调地狱」。ES6 引入的 Promise 和 ES2017 引入的 async/await彻底改变了异步代码的书写方式。3.1 为什么需要异步想象一下如果网络请求是同步的浏览器向服务器请求数据时页面就会完全冻结直到数据返回才能继续响应用户操作。这对用户体验来说是不可接受的。所以浏览器提供了 setTimeout、XMLHttpRequest、fetch 等异步 API把耗时操作交给浏览器其他线程处理主线程继续执行后面的同步代码。当异步任务完成后再通过回调或 Promise 通知主线程处理结果。console.log(第一行); setTimeout(function () { console.log(定时器回调); }, 0); console.log(第三行); // 实际输出顺序第一行、第三行、定时器回调这个经典例子说明异步回调不会立即执行即使定时器延迟为 0也要等当前同步代码全部执行完毕后才有机会被调用。3.2 Promise 的核心状态Promise 是一个代表异步操作最终结果的对象。它有三种状态pending进行中、fulfilled已成功、rejected已失败。状态一旦从 pending 转变为 fulfilled 或 rejected就不可再改变这个特性保证了异步结果的唯一确定性。function fetchUser(id) { return new Promise(function (resolve, reject) { setTimeout(function () { if (id 0) { resolve({ id: id, name: 用户 id }); } else { reject(new Error(非法的用户 id)); } }, 500); }); } fetchUser(1) .then(function (user) { console.log(user.name); // 用户1 return fetchUser(2); }) .then(function (user) { console.log(user.name); // 用户2 }) .catch(function (error) { console.error(error.message); });Promise 的then方法返回一个新的 Promise所以可以链式调用把多个异步操作串行连接起来。任何一步出现错误都会沿着链条传递到最近的catch中统一处理。3.3 async/await让异步代码像同步一样书写async/await 是 Promise 的语法糖它让异步代码的可读性大幅提升。被async修饰的函数总是返回 Promise在async函数内部可以用await等待一个 Promise 完成并直接拿到 resolve 的结果。async function getUsers() { try { const user1 await fetchUser(1); console.log(user1.name); const user2 await fetchUser(2); console.log(user2.name); } catch (error) { console.error(请求失败, error.message); } } getUsers();对比前面的 Promise 链式调用async/await 的代码结构更接近我们习惯的顺序思维也更方便使用try/catch处理错误。需要注意的是await只能在async函数内部使用如果多个异步操作之间没有依赖关系可以使用Promise.all并行执行避免不必要的串行等待。async function loadAllUsers() { const results await Promise.all([fetchUser(1), fetchUser(2), fetchUser(3)]); results.forEach(function (user) { console.log(user.name); }); } loadAllUsers();3.4 事件循环与宏微任务异步任务的执行顺序由事件循环调度。事件循环会不断检查调用栈是否为空当前一个任务执行完毕后再去任务队列中取下一个任务执行。而任务队列又分为宏任务队列和微任务队列理解它们的执行顺序对定位异步 bug 至关重要。宏任务整体脚本、setTimeout、setInterval、I/O 操作、UI 渲染等。微任务Promise.then、MutationObserver、process.nextTickNode.js 环境等。console.log(script start); setTimeout(function () { console.log(setTimeout); }, 0); Promise.resolve() .then(function () { console.log(promise then); }); console.log(script end); // 输出顺序 // script start // script end // promise then // setTimeout执行规则可以概括为每个宏任务执行完毕后会清空当前所有微任务然后再进入下一个宏任务。所以上面例子中作为整体宏任务的脚本先执行完然后清空微任务队列输出promise then最后才轮到 setTimeout 宏任务。这个顺序在面试题中出现的频率非常高建议自己多做几组变式练习加深记忆。四、ES6 重点语法梳理现代 JavaScript 开发已经离不开 ES6 及更高版本的新语法。这些特性不只是让代码看起来更简洁更重要的是它们重新定义了变量声明、函数、对象和模块的组织方式。本节把最常用的几个语法点集中讲清楚。4.1 const 和 letlet 用于声明会变化的变量const 用于声明不会重新赋值的常量。两者都拥有块级作用域存在暂时性死区在声明之前访问变量会直接报错而不是像 var 那样返回 undefined。这一特性让很多隐蔽的变量提升问题提前暴露出来。// 旧写法 var oldName old; // 新写法 const name 小明; let age 18; age 1; // 暂时性死区示例 function test() { // console.log(value); // 报错不能提前访问 const value 声明后访问; return value; } console.log(test());4.2 模板字符串模板字符串使用反引号包裹可以直接嵌入变量和表达式也支持多行字符串。它解决了过去用加号拼接字符串时繁琐且容易出错的问题。const name 小明; const age 18; const message 我叫${name}今年${age}岁明年${age 1}岁。; console.log(message); // 我叫小明今年18岁明年19岁。 const html lt;divgt; lt;pgt;多行文本lt;/pgt; lt;/divgt;; console.log(html);4.3 解构赋值解构赋值可以从数组或对象中提取值并赋给变量语法简洁且易于阅读。在函数参数、模块导入、数据解析等场景中都能看到它的身影。// 数组解构 const numbers [1, 2, 3]; const [first, second] numbers; console.log(first, second); // 1 2 // 对象解构 const user { name: 小明, age: 18, city: 杭州 }; const { name, age } user; console.log(name, age); // 小明 18 // 函数参数解构 function printUser({ name, age }) { return ${name}今年${age}岁; } console.log(printUser(user)); // 小明今年18岁4.4 箭头函数箭头函数是普通函数的简写形式但它最关键的差异在于没有自己的 this。箭头函数内部的 this 取决于定义时所在的外层作用域而不是调用方式。这一特性使它特别适合用在需要保持外层 this 的回调场景中。const nums [1, 2, 3, 4, 5]; const doubled nums.map(function (n) { return n * 2; }); console.log(doubled); // [2, 4, 6, 8, 10] // 箭头函数写法 const doubled2 nums.map((n) n * 2); console.log(doubled2); // [2, 4, 6, 8, 10] // 保持外层 this 的典型场景 const counter { count: 0, start() { setInterval(() { this.count 1; console.log(this.count); }, 1000); } }; // counter.start();上面的定时器例子中如果使用普通函数回调内的 this 会指向全局对象使用箭头函数后this 被绑定到start方法所在的 counter 对象逻辑才正确。需要记住的是箭头函数不能用作构造函数也没有arguments对象这些场景仍然要使用普通函数。4.5 展开运算符与剩余参数展开运算符...可以把数组或对象展开成独立的元素剩余参数则把不定数量的参数收集成一个数组。两者使用相同的符号但作用方向不同。// 展开运算符 const arr1 [1, 2, 3]; const arr2 [0, ...arr1, 4]; console.log(arr2); // [0, 1, 2, 3, 4] const obj1 { a: 1, b: 2 }; const obj2 { ...obj1, c: 3 }; console.log(obj2); // { a: 1, b: 2, c: 3 } // 剩余参数 function sum(...numbers) { return numbers.reduce((total, n) total n, 0); } console.log(sum(1, 2, 3, 4, 5)); // 15五、DOM 操作进阶DOM 是浏览器提供给 JavaScript 操作网页内容的接口。Week1 我们已经会使用getElementById和innerHTML进行简单操作Week2 需要掌握更系统化的元素选择、创建、遍历和事件处理方式。5.1 现代元素选择方式querySelector 和 querySelectorAll 支持使用 CSS 选择器语法查找元素比传统的 getElementById、getElementsByClassName 更加灵活统一。// 选择单个元素 const title document.querySelector(.title); const firstItem document.querySelector(#list li); // 选择多个元素 const allItems document.querySelectorAll(.item); allItems.forEach(function (item) { item.classList.add(active); });querySelectorAll返回的是静态的 NodeList这意味着即使后续页面结构发生变化之前获取的列表也不会自动更新而getElementsByClassName返回的是动态的 HTMLCollection会实时反映 DOM 的变化。5.2 创建、插入与删除节点// 创建元素 const newItem document.createElement(li); newItem.textContent 新添加的项目; newItem.className item; // 插入到列表末尾 const list document.getElementById(list); list.appendChild(newItem); // 在指定元素前插入 const referenceNode list.children[0]; const beforeItem document.createElement(li); beforeItem.textContent 插入到最前面; list.insertBefore(beforeItem, referenceNode); // 删除节点 list.removeChild(newItem);使用textContent设置文本比innerHTML更安全因为它不会解析 HTML 标签能有效避免 XSS 注入风险。除非确实需要插入 HTML 结构否则应优先使用textContent。5.3 事件委托当页面上有大量相似元素需要绑定事件时逐个绑定不仅性能差动态新增的元素也无法自动获得事件。事件委托利用事件冒泡机制把监听器绑定在父元素上通过判断事件目标来执行对应逻辑。const list document.getElementById(list); list.addEventListener(click, function (event) { const target event.target; if (target.tagName LI) { console.log(你点击了, target.textContent); } }); // 之后动态添加的 li 也会自动响应点击 const newLi document.createElement(li); newLi.textContent 动态添加的项目; list.appendChild(newLi);事件委托不仅能减少内存占用还能解决动态元素的事件绑定问题。在后续 Todo List 实战项目中我们会大量使用这个技巧来处理列表项的点击和删除。六、Node.js 核心模块与模块系统从这一节开始我们把视角从前端切到后端。Node.js 让 JavaScript 脱离了浏览器环境可以在服务器上运行。理解 Node.js 的模块系统和核心模块是后端开发的第一步。6.1 模块化的意义在没有模块化的年代前端代码通常写在几个 script 标签里所有变量共享全局作用域命名冲突几乎不可避免。Node.js 采用 CommonJS 模块规范每个文件就是一个独立的模块文件内部的变量和函数默认是私有的只有显式通过exports或module.exports导出的内容才能被其他文件使用。// math.js function add(a, b) { return a b; } function multiply(a, b) { return a * b; } module.exports { add, multiply };// main.js const math require(./math.js); console.log(math.add(2, 3)); // 5 console.log(math.multiply(2, 3)); // 6exports是module.exports的引用但直接给exports赋值不会改变导出内容因此导出单一函数或对象时应该使用module.exports。现代 Node.js 也支持 ES Module 语法需要在 package.json 中设置type: module后使用import和export两者学习时可以对比记忆。6.2 常用核心模块pathpath 模块用来处理文件路径它在不同操作系统上的表现高度一致避免了手动拼接路径字符串带来的兼容性问题。const path require(path); console.log(path.join(src, views, index.html)); // src/views/index.html console.log(path.resolve(src, views)); // 返回绝对路径 console.log(path.basename(/a/b/c.txt)); // c.txt console.log(path.extname(/a/b/c.txt)); // .txt推荐使用path.join而不是字符串加法拼接路径因为它能自动处理多余的斜杠也能根据操作系统选择正确的分隔符。获取绝对路径时使用path.resolve可以配合__dirname使用__dirname表示当前文件所在目录的绝对路径。6.3 常用核心模块fs 文件系统fs 模块用于读写文件后端很多配置读取、日志写入、静态资源服务都依赖它。fs 同时提供同步和异步 API异步 API 又分为回调风格和 Promise 风格。const fs require(fs); const path require(path); // 同步读取适合启动阶段加载配置 const configText fs.readFileSync(path.join(__dirname, config.json), utf-8); const config JSON.parse(configText); console.log(config); // 异步读取适合运行中的文件操作 fs.readFile(path.join(__dirname, data.txt), utf-8, function (err, data) { if (err) { console.error(读取失败, err.message); return; } console.log(文件内容, data); }); // 写入文件 fs.writeFile(path.join(__dirname, output.txt), Hello Node.js, function (err) { if (err) { console.error(写入失败, err.message); return; } console.log(写入成功); });同步方法会阻塞事件循环让整个 Node.js 进程在等待文件时无法处理其他请求因此只适合在启动阶段少量使用运行中处理文件一律优先使用异步 API。Node.js 高版本还提供了fs.promises可以用 async/await 风格编写异步文件操作。6.4 常用核心模块http使用 http 模块可以直接创建 HTTP 服务器处理请求和响应。虽然实际开发中通常会使用 Express 等框架但理解 http 模块的底层原理对掌握后端本质很有帮助。const http require(http); const server http.createServer(function (req, res) { res.writeHead(200, { Content-Type: application/json; charsetutf-8 }); if (req.url /api/user req.method GET) { const user { id: 1, name: 小明, age: 18 }; res.end(JSON.stringify(user)); return; } res.writeHead(404); res.end(JSON.stringify({ message: 接口不存在 })); }); server.listen(3000, function () { console.log(服务已启动http://localhost:3000); });上面这段代码创建了一个监听 3000 端口的服务器根据请求路径和方法返回不同响应。当访问http://localhost:3000/api/user时会收到用户信息的 JSON 数据。可以看到仅用 http 模块手写路由判断是繁琐的这也引出了下一节的 Express 框架。七、npm 与依赖管理npm 是 Node.js 自带的包管理工具也是全球最大的开源软件注册中心。掌握 npm 的常用命令是加入前端工程化开发的基本功。7.1 初始化项目# 创建项目目录 mkdir week2-todo cd week2-todo 初始化 package.json npm init -y执行npm init -y后会生成一个 package.json 文件它是项目的核心配置文件记录了项目名称、版本、入口文件、脚本命令和依赖列表。package.json 中的每个字段都有明确作用后续使用前端构建工具和部署服务时都离不开它。{ name: week2-todo, version: 1.0.0, description: 前后端联调实战项目, main: server.js, scripts: { start: node server.js, dev: node server.js }, dependencies: { express: ^4.18.2 } }7.2 安装与卸载依赖# 安装运行时依赖 npm install express 安装开发依赖 npm install --save-dev nodemon 卸载依赖 npm uninstall expressnpm install express会把 express 写入 dependencies使用--save-dev或-D参数则写入 devDependencies。两者的区别在于dependencies 是应用运行所必需的包部署到生产环境时也会安装devDependencies 只是开发阶段需要的工具比如测试、热重载、代码格式化等生产环境不需要。安装后npm 会在 package.json 记录版本并生成 node_modules 目录存放实际代码同时生成 package-lock.json 锁定依赖版本保证团队协作时每个人都安装同样版本的包。八、Express 框架入门与 REST APIExpress 是目前使用最广泛的 Node.js Web 框架它在 http 模块之上封装了路由、中间件、请求解析等能力让我们能够用更少的代码构建功能完整的后端服务。8.1 创建第一个 Express 服务const express require(express); const app express(); const PORT 3000; // 解析 JSON 请求体 app.use(express.json()); app.get(/, function (req, res) { res.send(欢迎来到 Week2 Todo API); }); app.listen(PORT, function () { console.log(服务已启动http://localhost:${PORT}); });app.get定义了一个 GET 路由第一个参数是路径第二个参数是处理函数。处理函数接收req请求对象和res响应对象通过res.send返回文本响应。这里还使用了app.use(express.json())中间件解析 JSON 格式的请求体让后端的 POST 和 PUT 接口能够拿到客户端传来的数据。8.2 路由设计REST 架构风格强调使用 HTTP 方法表达操作语义使用名词表达资源。设计一个 Todo 接口时路由应该围绕 todos 这个资源展开而不是把操作写进 URL。HTTP 方法路径用途GET/todos获取所有待办事项GET/todos/:id获取单个待办事项POST/todos创建新的待办事项PUT/todos/:id更新单个待办事项DELETE/todos/:id删除单个待办事项8.3 完整 Todo API 实现const express require(express); const app express(); app.use(express.json()); const todos []; let nextId 1; // 获取所有待办 app.get(/todos, function (req, res) { res.json(todos); }); // 获取单个待办 app.get(/todos/:id, function (req, res) { const id Number(req.params.id); const todo todos.find(function (item) { return item.id id; }); if (!todo) { res.status(404).json({ message: 未找到该待办 }); return; } res.json(todo); }); // 新增待办 app.post(/todos, function (req, res) { const { title } req.body || {}; if (!title) { res.status(400).json({ message: title 不能为空 }); return; } const todo { id: nextId, title: String(title), done: false }; nextId 1; todos.push(todo); res.status(201).json(todo); }); // 更新待办 app.put(/todos/:id, function (req, res) { const id Number(req.params.id); const todo todos.find(function (item) { return item.id id; }); if (!todo) { res.status(404).json({ message: 未找到该待办 }); return; } if (typeof req.body.title string) { todo.title req.body.title; } if (typeof req.body.done boolean) { todo.done req.body.done; } res.json(todo); }); // 删除待办 app.delete(/todos/:id, function (req, res) { const id Number(req.params.id); const index todos.findIndex(function (item) { return item.id id; }); if (index -1) { res.status(404).json({ message: 未找到该待办 }); return; } const removed todos.splice(index, 1)[0]; res.json(removed); }); const PORT 3000; app.listen(PORT, function () { console.log(Todo API 已启动http://localhost:${PORT}); });这里用内存数组模拟数据库真实项目中会替换为 MongoDB、MySQL 等持久化存储。学习阶段先用内存数据可以把注意力集中在接口设计和前后端通信上。8.4 中间件机制中间件是 Express 的核心概念它是一个接收 req、res 和 next 三个参数的函数。中间件可以执行任意代码、修改请求和响应对象、结束响应或者调用 next 把控制权交给下一个中间件。这个机制让日志记录、鉴权、错误处理、请求解析等功能可以以管道的形式组合起来。// 日志中间件 app.use(function (req, res, next) { console.log(${req.method} ${req.url} - ${new Date().toISOString()}); next(); }); // 简易鉴权中间件 app.use(/todos, function (req, res, next) { if (req.method POST !req.headers.authorization) { res.status(401).json({ message: 缺少认证信息 }); return; } next(); });上面第一个中间件会记录所有请求的方法、路径和时间第二个中间件只对 todos 路径生效如果 POST 请求没有携带认证头则直接返回 401。这种可插拔的设计是 Express 优雅处理复杂业务逻辑的关键。九、前后端联调初体验前端页面跑通了后端接口也搭建好了下一步就是把两者连接起来让浏览器能够真正调用服务器上的接口读写数据。9.1 使用 fetch 调用接口fetch 是现代浏览器内置的网络请求 API它基于 Promise 设计可以很方便地与 async/await 配合使用。async function getTodos() { const response await fetch(http://localhost:3000/todos); if (!response.ok) { throw new Error(请求失败状态码 response.status); } const data await response.json(); console.log(data); } async function addTodo(title) { const response await fetch(http://localhost:3000/todos, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ title: title }) }); const data await response.json(); console.log(新增成功, data); } async function deleteTodo(id) { const response await fetch(http://localhost:3000/todos/${id}, { method: DELETE }); const data await response.json(); console.log(删除成功, data); }GET 请求只需要一个参数POST、PUT 请求还需要设置 method 和 headers并通过 body 传递 JSON 数据。删除操作使用 DELETE 方法把资源 id 放在 URL 路径中。9.2 CORS 跨域问题当我们从一个本地 HTML 文件或另一个端口调用后端接口时浏览器会执行同源策略。如果前端页面的来源与接口服务器的来源协议、域名、端口不同浏览器就会阻止请求控制台会出现熟悉的 CORS 报错。解决这个问题最常用的方式是后端设置 CORS 响应头。const express require(express); const cors require(cors); const app express(); app.use(cors()); // 或者手动设置 app.use(function (req, res, next) { res.setHeader(Access-Control-Allow-Origin, *); res.setHeader(Access-Control-Allow-Methods, GET, POST, PUT, DELETE, OPTIONS); res.setHeader(Access-Control-Allow-Headers, Content-Type, Authorization); next(); });生产环境中建议把Access-Control-Allow-Origin设置为具体的域名而不是通配符这样安全性更高。开发阶段为了跑通联调使用cors包或手动设置宽松的 CORS 头即可。十、实战项目Todo List 前后端完整联调这是 Week2 的收尾项目我们把前端 DOM 操作、事件委托、fetch 请求和后端 Express API 串起来实现一个完整的待办事项应用。用户可以添加待办、标记完成状态、删除待办页面会实时与后端数据同步。10.1 项目结构week2-todo/ ├── server.js # Express 后端服务 ├── public/ │ ├── index.html # 前端页面 │ └── app.js # 前端逻辑 └── package.json10.2 前端页面!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 titleWeek2 Todo List/title /head body h1Todo List/h1 input idtodoInput typetext placeholder输入待办事项 button idaddBtn添加/button ul idtodoList/ul script srcapp.js/script /body /html10.3 前端交互逻辑const API_BASE http://localhost:3000; const todoInput document.getElementById(todoInput); const addBtn document.getElementById(addBtn); const todoList document.getElementById(todoList); async function fetchTodos() { const response await fetch(${API_BASE}/todos); const todos await response.json(); renderTodos(todos); } function renderTodos(todos) { todoList.innerHTML ; todos.forEach(function (todo) { const li document.createElement(li); li.dataset.id todo.id; const checkbox document.createElement(input); checkbox.type checkbox; checkbox.checked todo.done; checkbox.addEventListener(change, function () { updateTodo(todo.id, { done: checkbox.checked }); }); const text document.createElement(span); text.textContent todo.title; if (todo.done) { text.style.textDecoration line-through; } const deleteBtn document.createElement(button); deleteBtn.textContent 删除; deleteBtn.addEventListener(click, function () { removeTodo(todo.id); }); li.appendChild(checkbox); li.appendChild(text); li.appendChild(deleteBtn); todoList.appendChild(li); }); } async function addTodo() { const title todoInput.value.trim(); if (!title) { alert(请输入待办内容); return; } await fetch(${API_BASE}/todos, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ title }) }); todoInput.value ; fetchTodos(); } async function updateTodo(id, payload) { await fetch(${API_BASE}/todos/${id}, { method: PUT, headers: { Content-Type: application/json }, body: JSON.stringify(payload) }); fetchTodos(); } async function removeTodo(id) { await fetch(${API_BASE}/todos/${id}, { method: DELETE }); fetchTodos(); } addBtn.addEventListener(click, addTodo); fetchTodos();10.4 服务端配置静态文件为了让 Express 能直接提供 index.html 和 app.js需要在 server.js 中添加静态文件中间件。这样前端页面和后端接口就运行在同一个 3000 端口上也顺带绕开了跨域问题前后端联调体验更顺畅。const express require(express); const path require(path); const app express(); app.use(express.json()); // 托管 public 目录下的静态文件 app.use(express.static(path.join(__dirname, public))); // 之后定义 /todos 相关接口 // ... app.listen(3000, function () { console.log(应用已启动http://localhost:3000); });加入静态文件托管后访问http://localhost:3000会自动返回 public 目录下的 index.html前端 app.js 通过相对路径加载数据请求直接打到同源接口整个项目成为一个完整的全栈应用。十一、Week2 高频踩坑清单这一节把 Week2 学习过程中容易反复犯的错误集中整理建议在动手实践前先浏览一遍实践时遇到问题再回来对照排查。异步结果还没返回就使用fetch 或数据库查询是异步的如果直接打印返回值的下一行就拿结果得到的往往是 undefined 或 Promise 对象。务必使用 await 或在 then 回调中处理结果。把数组的 length 误当成最后一个元素数组下标从 0 开始最后一个元素的下标是 length - 1。对象方法拆出去后 this 丢失把对象方法赋值给变量再调用this 会从原对象变成 undefined 或全局对象必要时使用 bind 或箭头函数。for 循环里用 var异步回调中取到的值全部变成循环结束后的最终值改用 let 声明可以彻底解决。POST 请求没有写 Content-Type后端express.json()解析不到请求体req.body 会变成 undefined记得设置Content-Type: application/json。端口被占用启动服务时报 EADDRINUSE说明 3000 端口已有进程可以换一个端口或先关闭占用进程。修改了代码但重启后不生效Node.js 不会自动重新加载修改后的文件需要手动重启或者使用 nodemon 自动监听文件变化。跨域问题排查方向反了CORS 是浏览器的安全限制必须在后端设置响应头解决前端无法通过简单配置绕开。十二、Week2 自我检验清单学完一周内容后建议用下面的清单检验掌握程度。每一条都能独立说清楚并写出可运行的示例代码才说明真正通过了 Week2。能解释作用域、作用域链和闭包的原理能写出闭包的实际应用案例。能说清楚原型链的查找过程能绘制从实例到 Object.prototype 的链条图。能准确判断一段代码中 this 的指向并说明四种绑定规则的优先级。能说出 Promise 的三种状态和 then、catch、finally 的作用。能解释事件循环中宏任务和微任务的执行顺序。能熟练使用 const、let、模板字符串、解构、箭头函数、展开运算符。能使用 path 和 fs 模块完成简单的文件读写。能使用 Express 定义完整的 REST 风格增删改查接口。能说出中间件的作用并手写一个日志中间件。能使用 fetch 完成后端接口调用并解释 CORS 产生的原因和解决方案。如果以上清单中有超过三条无法顺利完成建议针对薄弱环节回到对应章节重新练习。Week2 的内容是 Week3 学习前端框架和后端数据库的基石任何一个环节的模糊理解都会在后续学习中被放大。十三、本周总结与 Week3 预告Week2 完成了从「会写基础代码」到「理解运行机制」的关键升级。前端方面我们系统学习了作用域、闭包、原型链、this 指向和事件循环这些都是后续理解 Vue、React 等框架运行原理的必备知识后端方面我们从 Node.js 核心模块讲到 Express 框架再到完整的 REST API 设计和前后端联调打通了全栈开发的最小闭环。Week3 将进入真正的工程化阶段。前端方向会引入模块打包工具正式学习 Vue 3 的组合式 API包括响应式原理、组件通信、生命周期等核心内容后端方向会接入数据库学习 MySQL 基础建模、SQL 增删改查、数据库连接池以及前后端分离架构下的项目目录规范与开发流程。建议在开始 Week3 之前把本周的 Todo List 项目完整跑通三遍以上最好能脱离本文独立敲出全部代码这是检验学习效果最直接的方式。学习提示全文涉及的核心概念较多遇到不理解的地方不要急着往下赶进度。把每一个知识点对应的代码亲手敲出来修改参数观察运行结果变化形成自己的理解和记忆。Week2 的基础打得越扎实后面的进阶内容就越轻松。
延伸阅读

更多相关文章

2026/9/18 21:53:05

RFC 详解:从历史渊源、编号体系到阅读实战的完整指南

一、引言:互联网世界的“宪法典籍”如果把互联网比作一个庞大的国家,那么 RFC(Request for Comments)就是这个国家的法律典籍和工程图纸。从我们每天使用的 HTTP 请求、TCP 连接、DNS 解析,到电子邮件传输、网络时间同…

2026/9/18 21:48:04

2026年AI编程工具全景盘点:33款主流工具分类与选型指南

过去两年,我几乎每三个月就要把 AI 编程工具重新配一遍。不是单纯追新,而是这类工具变化实在太快:2024 年大家还在讨论“AI 能不能把注释和单测补一补”,2025 年已经开始聊“让 AI 自己改完代码、跑完测试、再把 PR 提出来”&…

2026/9/18 22:58:07

PyWxDump环境配置完全指南:4条命令跑起来,3件事用得安全

PyWxDump环境配置完全指南:4条命令跑起来,3件事用得安全 【免费下载链接】PyWxDump 删库 项目地址: https://gitcode.com/GitHub_Trending/py/PyWxDump 照着文档装完,终端里一敲 wxdump info 就抛 ImportError,这是装完 Py…

2026/9/18 22:58:07

给 Codex 的 Skill 链,TaoToken 只补 Base URL

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/18 22:58:07

PHP+uniapp构建智能举报系统:DFA敏感词过滤与工单闭环实践

做这个项目之前,我去调研过辖区派出所的举报登记流程。接待窗口放着一个厚厚的登记本,民警一边听群众口述一边手写,遇到电话举报就顺手记在便签纸上。一天下来几十条线索,格式五花八门,地址写“老菜场旁边”这种模糊位…

2026/9/18 22:58:07

Cursor 挂 DBHub MCP 操作 MySQL,模型 Base URL 指到 TaoToken

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/18 22:58:07

VSCODE插件十大推荐,这次让Codex走TaoToken替我挑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/18 22:53:07

ollama 在 Ubuntu 装不上,OpenClaw 改走 TaoToken 通道行不行

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/18 14:13:01

拯救者Y7000黑屏故障排查与维修实战指南

1. 项目概述:一台黑屏的拯救者Y7000,到底卡在哪一步? 联想拯救者Y7000系列笔记本,从2018年第一代搭载i5-8300H开始,到后来的i7-9750H、i7-10750H、i5-11400H,再到2023年款的R7-7840HS,它始终是学…

2026/9/18 0:01:09

Google Colab 实战:运行模型、数据加载与报错排查

1. 为什么我劝你先搞懂 Colab 的运行模型1.1 Colab 到底是什么,跟本地跑代码差在哪Google Colab 简单说就是一台跑在浏览器里的 Linux 虚拟机,你打开一个 Notebook,背后就连上了一台带 GPU 的远程机器。你在单元格里敲的每一行 Python&#x…

2026/9/18 0:01:09

C语言数据类型与表达式详解

1. C语言数据与数据类型概述在C语言编程中,数据是程序处理的核心对象。理解数据的分类和特性是掌握C语言的基础。C语言中的数据主要分为四大类:常量、变量、表达式和函数。这些数据类型构成了C语言程序的基本元素,每种类型都有其独特的特性和…

2026/9/18 0:01:09

SQL时间字段指定时间段查询:区间语义、索引与时区避坑

上周排查一个线上问题&#xff0c;用户反馈"昨天的订单一条都没查到"&#xff0c;但数据库里明明躺着两千多条。最后定位下来&#xff0c;不是数据丢了&#xff0c;也不是接口挂了&#xff0c;而是那个查询条件把时间段写成了> 2024-05-20 00:00:00 AND < 2024…

2026/9/18 14:13:03

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行&#xff0c;Type-C接口算是典型的“看着简单&#xff0c;做起来全坑”的东西。光引脚就24个&#xff0c;高低速信号、电源、控制线全部塞在一个小小的连接器里&#xff0c;如果PCB布局不做规划&#xff0c;打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/18 14:13:02

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/18 14:13:02

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
咨询二维码