【AJAX-Day4】Node.js、Express与跨域
🎯 核心目标:了解 Node.js 与 Express 搭建接口服务、掌握跨域问题的原因与解决方案、理解同源策略
一、Node.js 基础
1.1 什么是 Node.js?
Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时环境,让 JS 可以运行在服务器端。
浏览器端 JS:V8引擎 + Web APIs(DOM/BOM)
Node.js:V8引擎 + Node APIs(fs/http/path/os...)
前端用 JS 写界面,后端也可以用 JS 写接口!
Node.js 特点:
- 单线程 + 事件循环(非阻塞 I/O)
- 高并发(适合 I/O 密集型,如接口服务)
- npm 生态极其丰富(200万+ 包)
- 前后端同构(同一语言)
1.2 基本使用
// 1. 打印 Node 版本
// 终端执行:node -v
// 2. 运行 JS 文件
// node index.js
// 3. 内置模块 fs(文件系统)
const fs = require('fs')
// 读取文件(回调)
fs.readFile('./data.txt', 'utf8', (err, content) => {
if (err) throw err
console.log(content)
})
// 读取文件(Promise)
const { readFile, writeFile } = require('fs/promises')
const content = await readFile('./data.txt', 'utf8')
// 写入文件
await writeFile('./output.txt', '写入内容', 'utf8')
// 4. 内置模块 path
const path = require('path')
path.join(__dirname, 'data', 'users.json') // 拼接路径(跨平台)
path.extname('index.html') // '.html'
path.basename('/foo/bar.js') // 'bar.js'
path.dirname('/foo/bar.js') // '/foo'
1.3 原生 http 模块创建服务器
const http = require('http')
const server = http.createServer((req, res) => {
// req:请求对象(url、method、headers...)
// res:响应对象(状态码、响应头、响应体)
console.log(`${req.method} ${req.url}`)
// 设置响应头
res.setHeader('Content-Type', 'application/json; charset=utf-8')
res.setHeader('Access-Control-Allow-Origin', '*') // 允许跨域
// 路由判断
if (req.url === '/api/users' && req.method === 'GET') {
const users = [{ id: 1, name: '张三' }, { id: 2, name: '李四' }]
res.statusCode = 200
res.end(JSON.stringify(users))
} else {
res.statusCode = 404
res.end(JSON.stringify({ message: '接口不存在' }))
}
})
server.listen(3000, () => {
console.log('服务器运行在 http://localhost:3000')
})
二、Express 框架
2.1 为什么用 Express?
原生 http 模块功能简陋,需要手动处理路由、参数解析等。Express 是基于 http 模块的轻量级 Web 框架,提供了路由、中间件等便利功能。
# 初始化项目
npm init -y
# 安装 express
npm install express
2.2 基本结构
const express = require('express')
const app = express()
const PORT = 3000
// 解析请求体中间件
app.use(express.json()) // 解析 JSON 请求体
app.use(express.urlencoded({ extended: true })) // 解析表单请求体
// 静态文件服务
app.use(express.static('public')) // public 目录下的文件可直接访问
// 路由
app.get('/api/users', (req, res) => {
// req.query → 查询参数 (?page=1&size=10)
// req.params → 路径参数 (/:id)
// req.body → 请求体(需要中间件解析)
// req.headers → 请求头
const { page = 1, size = 10 } = req.query
const users = [{ id: 1, name: '张三' }, { id: 2, name: '李四' }]
res.json({ code: 200, data: users, total: 2 })
})
app.get('/api/users/:id', (req, res) => {
const { id } = req.params // 路径参数
res.json({ code: 200, data: { id, name: '张三' } })
})
app.post('/api/users', (req, res) => {
const newUser = req.body // 请求体
console.log('新建用户:', newUser)
res.status(201).json({ code: 201, data: { id: 3, ...newUser } })
})
app.put('/api/users/:id', (req, res) => {
const { id } = req.params
const updateData = req.body
res.json({ code: 200, message: `用户${id}更新成功` })
})
app.delete('/api/users/:id', (req, res) => {
const { id } = req.params
res.json({ code: 200, message: `用户${id}删除成功` })
})
// 启动服务
app.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}`)
})
2.3 中间件(Middleware)
中间件是 Express 的核心,本质是处理请求和响应的函数,按顺序执行。
// 中间件格式:(req, res, next) => {}
// next():调用下一个中间件
// 1. 应用级中间件(全局执行)
app.use((req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`)
next() // 必须调用 next(),否则请求卡住
})
// 2. 路由级中间件(只对特定路由)
function authMiddleware(req, res, next) {
const token = req.headers.authorization?.split(' ')[1]
if (!token) {
return res.status(401).json({ message: '未授权,请登录' })
}
// 验证 token(这里简化)
req.userId = 'user123' // 将用户信息挂到 req 上
next()
}
// 需要登录的路由
app.get('/api/profile', authMiddleware, (req, res) => {
res.json({ userId: req.userId })
})
// 3. 错误处理中间件(4个参数)
app.use((err, req, res, next) => {
console.error(err.stack)
res.status(500).json({ message: '服务器内部错误', error: err.message })
})
2.4 路由模块化
// routes/users.js
const express = require('express')
const router = express.Router()
router.get('/', (req, res) => res.json({ data: [] }))
router.post('/', (req, res) => res.status(201).json({ data: req.body }))
router.get('/:id', (req, res) => res.json({ data: { id: req.params.id } }))
module.exports = router
// app.js
const usersRouter = require('./routes/users')
app.use('/api/users', usersRouter)
// 访问:GET /api/users → router.get('/')
// 访问:GET /api/users/1 → router.get('/:id')
三、跨域问题(CORS)
3.1 什么是同源策略?
同源策略(Same-Origin Policy) 是浏览器的安全机制,规定:只有协议、域名、端口都相同的两个 URL 才是同源的。
http://www.example.com:3000/page
vs
http://www.example.com:3000/other ✅ 同源(路径不同,协议域名端口相同)
https://www.example.com:3000/page ❌ 跨源(协议不同)
http://api.example.com:3000/page ❌ 跨源(子域名不同)
http://www.example.com:8080/page ❌ 跨源(端口不同)
http://www.other.com:3000/page ❌ 跨源(域名不同)
同源策略限制的内容:
- AJAX 请求(最常见的限制)
- Cookie、LocalStorage 读取
- DOM 访问(iframe 内的页面)
3.2 跨域错误表现
Access to XMLHttpRequest at 'http://api.example.com/users'
from origin 'http://localhost:3000' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
3.3 CORS(跨域资源共享)—— 服务端解决方案
CORS 是W3C 标准,通过服务端设置响应头,告诉浏览器允许跨域。
简单请求 vs 预检请求:
简单请求:
方法:GET / POST / HEAD
Content-Type:text/plain / application/x-www-form-urlencoded / multipart/form-data
直接发送,响应头包含 Access-Control-Allow-Origin 即可
预检请求(Preflight):
方法:PUT / DELETE / PATCH 或自定义请求头
浏览器先发 OPTIONS 请求问服务器"可以吗?"
服务器响应允许后,浏览器才发真正的请求
Express 设置 CORS:
// 方式一:手动设置响应头
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*') // 允许所有域(开发用)
// res.setHeader('Access-Control-Allow-Origin', 'https://your-frontend.com') // 生产环境指定域
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,PATCH,OPTIONS')
res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization')
res.setHeader('Access-Control-Allow-Credentials', 'true') // 允许携带 Cookie
// 处理预检请求
if (req.method === 'OPTIONS') {
return res.status(200).end()
}
next()
})
// 方式二:使用 cors 中间件(推荐)
const cors = require('cors') // npm install cors
app.use(cors({
origin: ['http://localhost:3000', 'https://production.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true
}))
3.4 前端代理(开发环境)—— Vite/webpack 配置
// vite.config.js
export default {
server: {
proxy: {
'/api': {
target: 'http://localhost:3001', // 后端地址
changeOrigin: true, // 改变请求头中的 origin
rewrite: (path) => path.replace(/^/api/, '') // 去掉 /api 前缀
}
}
}
}
// 效果:前端请求 /api/users → 代理转发到 http://localhost:3001/users
// 同源了(都是 localhost:5173),不存在跨域
// webpack(vue-cli)配置
// vue.config.js
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:3001',
changeOrigin: true
}
}
}
}
3.5 JSONP(了解,历史方案)
// JSONP 原理:利用 <script> 标签不受同源策略限制的特性
// 只支持 GET 请求,已基本被 CORS 替代
function jsonp(url, callback) {
const fnName = 'jsonp_' + Date.now()
window[fnName] = function(data) {
callback(data)
document.body.removeChild(script)
delete window[fnName]
}
const script = document.createElement('script')
script.src = `${url}?callback=${fnName}`
document.body.appendChild(script)
}
jsonp('http://api.example.com/data', data => {
console.log(data)
})
// 服务端返回:jsonp_1234567890({"code":200,"data":[...]})
四、接口文档规范
4.1 RESTful API 设计规范
资源 URI HTTP 方法
用户列表 /api/users GET
创建用户 /api/users POST
获取单个用户 /api/users/{id} GET
更新用户(全量) /api/users/{id} PUT
更新用户(部分) /api/users/{id} PATCH
删除用户 /api/users/{id} DELETE
文章下的评论
获取评论列表 /api/articles/{id}/comments GET
创建评论 /api/articles/{id}/comments POST
4.2 统一响应格式
// 成功响应
{
"code": 200,
"message": "success",
"data": {
"list": [...],
"total": 100,
"page": 1,
"size": 10
}
}
// 失败响应
{
"code": 400,
"message": "参数格式不正确",
"data": null
}
// 服务端封装响应工具函数
const response = {
success(res, data, message = 'success') {
res.json({ code: 200, message, data })
},
fail(res, message, code = 400) {
res.status(code).json({ code, message, data: null })
},
created(res, data) {
res.status(201).json({ code: 201, message: '创建成功', data })
}
}
// 使用
app.get('/api/users', (req, res) => {
const users = getUsersFromDB()
response.success(res, users)
})
五、知识图谱
Node.js、Express与跨域
├── Node.js
│ ├── 定义:服务端 JS 运行时(V8 + Node APIs)
│ ├── 内置模块:fs(文件)/ path(路径)/ http(服务)
│ └── 运行:node filename.js
├── Express
│ ├── 路由:app.get/post/put/delete/patch
│ ├── 请求对象:req.query / req.params / req.body / req.headers
│ ├── 响应对象:res.json / res.status / res.send
│ ├── 中间件:(req, res, next) => {}
│ │ ├── 全局:app.use()
│ │ ├── 路由级:挂在具体路由上
│ │ └── 错误:(err, req, res, next) => {}
│ └── 路由模块化:Router
├── 跨域(CORS)
│ ├── 同源策略:协议+域名+端口完全相同
│ ├── 简单请求 vs 预检请求(OPTIONS)
│ ├── 服务端解决:Access-Control-Allow-Origin 响应头
│ ├── cors 中间件(推荐)
│ └── 前端代理(Vite/webpack devServer.proxy)
└── API 规范
├── RESTful:资源 + HTTP 方法表达操作
└── 统一响应格式:code + message + data
六、高频面试题
Q1:什么是跨域?如何解决?
跨域是指浏览器同源策略的限制,当两个 URL 的协议、域名、端口有任意一个不同时,Ajax 请求会被浏览器拦截。解决方案: ① CORS(最常用,服务端设置
Access-Control-Allow-Origin响应头); ② 前端代理(开发环境用 Vite/webpack proxy); ③ Nginx 反向代理(生产环境,在代理层统一处理); ④ JSONP(历史方案,仅支持 GET)。
Q2:预检请求(Preflight)是什么?
当请求使用 PUT/DELETE/PATCH 等方法,或携带自定义请求头(如 Authorization)时,浏览器会先发一个
OPTIONS方法的预检请求,询问服务器是否允许该跨域请求。服务器需响应Access-Control-Allow-Methods和Access-Control-Allow-Headers等头,浏览器确认后才发送真实请求。
Q3:Express 中间件的执行顺序?
中间件按照
app.use()的注册顺序从上到下依次执行,每个中间件通过调用next()将控制权传给下一个。如果不调用next(),请求处理就会在该中间件停止。错误处理中间件(4个参数)只有当next(err)被调用时才触发。
Q4:Node.js 为什么适合高并发?
Node.js 采用单线程 + 非阻塞 I/O + 事件循环机制。当发起 I/O 操作(读文件、查数据库、网络请求)时,Node 不会阻塞等待,而是注册回调后继续处理其他请求,I/O 完成时再执行回调。因此单线程可以处理大量并发 I/O 请求(适合接口服务),但不适合 CPU 密集型任务(如图片处理、大量计算)。
七、本系列完结
恭喜学完 AJAX 全套课程!🎉
完整知识体系回顾:
AJAX 学习路径
├── Day1:基础概念
│ ├── HTTP 协议(请求/响应/状态码)
│ ├── XMLHttpRequest 原生使用
│ └── Axios 基础(配置对象/快捷方法)
├── Day2:异步进阶
│ ├── 回调地狱(问题所在)
│ ├── Promise(状态机/链式调用/静态方法)
│ └── async/await(语法糖/错误处理/并发)
├── Day3:Axios 深入
│ ├── 创建实例(多服务场景)
│ ├── 拦截器(Token注入/错误统一处理)
│ ├── 请求取消(AbortController)
│ ├── 文件上传/下载
│ └── Fetch API(对比/封装)
└── Day4:后端联调
├── Node.js 基础(运行时/内置模块)
├── Express(路由/中间件/路由模块化)
├── 跨域(同源策略/CORS/代理)
└── RESTful API 规范
后续推荐学习:
- 🔐 JWT 鉴权:Token 生成、验证、刷新机制
- 📊 数据库:MySQL / MongoDB + ORM(Sequelize/Mongoose)
- 🚀 Vue3/React:配合 AJAX 构建完整前端项目
- 🔧 TypeScript:为接口请求添加类型定义
⬅️ 上一篇:Day3 - Axios深入与请求拦截 🏠 系列首篇:Day1 - HTTP协议与XHR基础