【AJAX-Day4】Node.js、Express与跨域

2 阅读8分钟

【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-MethodsAccess-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(语法糖/错误处理/并发)
├── Day3Axios 深入
│   ├── 创建实例(多服务场景)
│   ├── 拦截器(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基础