阅读视图

发现新文章,点击刷新页面。

面试官 : “ 请问你实际开发中用过 函数柯理化 吗? 能讲一下吗 ?”

一、先搞懂:柯里化到底是什么?

核心定义:柯里化是把接收多个参数的函数,转换成一系列只接收单个参数的函数,并持续返回新函数,直到所有参数都被传入后,才执行最终逻辑并返回结果。

用 “人话” 说:原本要一次性传完所有参数的函数,现在可以 “分批传”,传一个参数就返回一个新函数等着接下一个,直到传完为止。

对比:普通函数 vs 柯里化函数

// 普通函数:一次性传所有参数
function add(a, b, c) {
  return a + b + c;
}
add(1, 2, 3); // 6

// 柯里化函数:分批次传参数
function curriedAdd(a) {
  return function(b) {
    return function(c) {
      return a + b + c;
    };
  };
}
curriedAdd(1)(2)(3); // 6(传一个参数,返回新函数,直到传完3个)

二、手动实现一个通用柯里化函数

你不用为每个函数单独写柯里化逻辑,这里写一个通用的 curry 工具函数,能把任意多参数函数转换成柯里化函数:

// 通用柯里化函数
function curry(fn) {
  // 保存原函数的参数个数
  const argsLength = fn.length;
  
  // 递归接收参数
  function curried(...args) {
    // 1. 如果已传参数 >= 原函数需要的参数,执行原函数
    if (args.length >= argsLength) {
      return fn.apply(this, args);
    }
    // 2. 否则,返回新函数,继续接收参数
    return function(...newArgs) {
      return curried.apply(this, [...args, ...newArgs]);
    };
  }
  
  return curried;
}

// 测试:给加法函数做柯里化
const add = (a, b, c) => a + b + c;
const curriedAdd = curry(add);

// 支持多种传参方式(核心优势)
console.log(curriedAdd(1)(2)(3)); // 6(逐个传)
console.log(curriedAdd(1, 2)(3)); // 6(分批传)
console.log(curriedAdd(1)(2, 3)); // 6(混合传)
console.log(curriedAdd(1, 2, 3)); // 6(一次性传)

三、柯里化的核心价值(为什么要用?)

  1. 参数复用:提前固定部分参数,生成新函数,避免重复传参。示例:固定 “税率” 参数,复用计算逻辑

    // 原函数:计算税后价格(价格 + 税率)
    const calculateTax = (taxRate, price) => price * (1 + taxRate);
    // 柯里化后,固定税率为10%
    const calculateTax10 = curry(calculateTax)(0.1);
    // 后续只用传价格,不用重复传税率
    calculateTax10(100); // 110
    calculateTax10(200); // 220
    
  2. 延迟执行:先收集参数,不立即执行,等参数凑齐后再执行。示例:表单提交前收集多个字段,凑齐后再验证提交

    const submitForm = (name, phone, address) => {
      console.log(`提交:${name} ${phone} ${address}`);
    };
    const curriedSubmit = curry(submitForm);
    
    // 分步收集参数(比如用户分步填写表单)
    const step1 = curriedSubmit("张三"); // 收集姓名,未执行
    const step2 = step1("13800138000"); // 收集手机号,未执行
    step2("北京市"); // 收集地址,参数凑齐,执行 → 输出:提交:张三 13800138000 北京市
    
  3. 适配函数参数:把多参数函数转换成单参数函数,适配只接收单参数的场景(比如 React 的高阶组件、数组的 map/filter 等)。示例:适配数组 map 的单参数回调

    // 原函数:乘以指定倍数
    const multiply = (multiplier, num) => num * multiplier;
    const curriedMultiply = curry(multiply);
    
    // 固定倍数为2,生成单参数函数
    const double = curriedMultiply(2);
    // 适配 map 的单参数回调
    [1,2,3].map(double); // [2,4,6]
    

四、常见误区

❌ 误区:“柯里化就是把函数拆成只传一个参数的函数,必须链式调用 (a)(b)(c)”

✅ 纠正:柯里化的核心是 “参数分批传递 + 延迟执行”,支持任意分批方式(比如 (a,b)(c)、(a)(b,c)),不一定非要逐个传。

❌ 误区:“柯里化能提升性能”

✅ 纠正:柯里化本质是多了层函数嵌套,性能略有损耗,它的价值是提升代码复用性和可读性,而非性能。

总结

  1. 柯里化核心:把多参数函数转成 “单参数函数链”,支持参数分批传递,凑齐后执行;
  2. 实现关键:通过闭包保存已传参数,递归判断参数是否凑齐,凑齐则执行原函数;
  3. 核心用途:参数复用、延迟执行、适配单参数场景。

面试官 : “ 请说一下 JS 的常见的数组 和 字符串方法有哪些 ? ”

盘点 JS 数组和字符串的核心方法,我会按「常用场景 + 功能分类」整理,每个方法标注作用 + 示例 + 关键说明,既好记又能直接用,适合复习和开发时快速查阅。

一、数组(Array)方法

数组方法是 JS 高频考点,按「增删改查、遍历、转换、排序 / 过滤 / 聚合」分类,重点标⭐️

1. 增删改查(修改原数组)

方法 作用 示例 关键说明
⭐️ push() 末尾添加元素 [1,2].push(3) → [1,2,3] 返回新长度,修改原数组
⭐️ pop() 末尾删除元素 [1,2,3].pop() → 3 返回删除的元素,修改原数组
⭐️ unshift() 头部添加元素 [2,3].unshift(1) → [1,2,3] 返回新长度,修改原数组
⭐️ shift() 头部删除元素 [1,2,3].shift() → 1 返回删除的元素,修改原数组
⭐️ splice(start, delNum, ...add) 任意位置增删改 [1,2,3].splice(1,1,4) → [1,4,3] 返回删除的元素,修改原数组
fill(val, start, end) 填充数组 [1,2,3].fill(0, 1, 2) → [1,0,3] 修改原数组

2. 遍历(不修改原数组)

方法 作用 示例 关键说明
⭐️ forEach() 遍历数组,无返回值 [1,2].forEach(item => console.log(item)) 无法中断(break 无效)
⭐️ map() 遍历 + 返回新数组 [1,2].map(item => item*2) → [2,4] 不修改原数组,必用 return
⭐️ filter() 过滤符合条件的元素 [1,2,3].filter(item => item>1) → [2,3] 返回新数组,保留满足条件的元素
⭐️ find() 找第一个符合条件的元素 [1,2,3].find(item => item>1) → 2 找到即返回,无则 undefined
⭐️ findIndex() 找第一个符合条件的索引 [1,2,3].findIndex(item => item>1) → 1 无则返回 -1
every() 所有元素满足条件? [1,2,3].every(item => item>0) → true 全满足返回 true
some() 至少一个元素满足条件? [1,2,3].some(item => item>2) → true 有一个满足就返回 true
reduce() 聚合(求和 / 拼接等) [1,2,3].reduce((sum, item) => sum+item, 0) → 6 第二个参数是初始值,核心是 “累积”

3. 转换 / 拼接(不修改原数组)

方法 作用 示例 关键说明
⭐️ join(sep) 数组转字符串 [1,2].join('-') → "1-2" sep 是分隔符,默认逗号
⭐️ concat() 拼接数组 [1,2].concat([3,4]) → [1,2,3,4] 返回新数组,不修改原数组
⭐️ slice(start, end) 截取数组(左闭右开) [1,2,3].slice(0,2) → [1,2] 不修改原数组,end 可选(默认到末尾)
flat(depth) 扁平化数组 [1,[2,[3]]].flat(2) → [1,2,3] depth 是层级,默认 1,Infinity 拍平所有
flatMap() map + flat(1) [1,2].flatMap(item => [item, item*2]) → [1,2,2,4] 比先 map 再 flat 高效

4. 排序 / 查找(部分修改原数组)

方法 作用 示例 关键说明
⭐️ sort(compare) 排序 [3,1,2].sort((a,b) => a-b) → [1,2,3] 修改原数组,默认按字符串排序(需传比较函数)
⭐️ reverse() 反转数组 [1,2,3].reverse() → [3,2,1] 修改原数组
⭐️ includes(val) 判断是否包含元素 [1,2].includes(2) → true 区分类型(1 !== '1')
indexOf(val) 找元素首次出现的索引 [1,2,1].indexOf(1) → 0 无则返回 -1
lastIndexOf(val) 找元素最后出现的索引 [1,2,1].lastIndexOf(1) → 2 无则返回 -1

二、字符串(String)方法

字符串方法均不修改原字符串(字符串是不可变类型),按「查找 / 截取、替换 / 分割、转换、判断」分类。

1. 查找 / 截取

方法 作用 示例 关键说明
⭐️ charAt(index) 获取指定位置字符 "abc".charAt(1) → "b" 索引越界返回空字符串
⭐️ indexOf(str) 找子串首次出现的索引 "abcab".indexOf("ab") → 0 无则返回 -1
⭐️ lastIndexOf(str) 找子串最后出现的索引 "abcab".lastIndexOf("ab") → 3 无则返回 -1
⭐️ slice(start, end) 截取字符串(左闭右开) "abcde".slice(1,3) → "bc" start 负数表示从末尾数
substring(start, end) 截取字符串 "abcde".substring(1,3) → "bc" 类似 slice,但 start>end 会自动交换
substr(start, length) 按长度截取 "abcde".substr(1,2) → "bc" 已废弃,优先用 slice
⭐️ includes(str) 判断是否包含子串 "abc".includes("b") → true 区分大小写
startsWith(str) 判断是否以子串开头 "abc".startsWith("ab") → true 可传第二个参数(起始位置)
endsWith(str) 判断是否以子串结尾 "abc".endsWith("bc") → true 可传第二个参数(截取长度)

2. 替换 / 分割

方法 作用 示例 关键说明
⭐️ replace(str/regex, newStr) 替换子串 "abc".replace("b", "x") → "axc" 只替换第一个,全局替换用 /g 正则
⭐️ split(sep) 字符串转数组 "a-b-c".split("-") → ["a","b","c"] sep 为空字符串则拆成单个字符
replaceAll(str/regex, newStr) 全局替换 "abab".replaceAll("a", "x") → "xbxb" ES2021 新增,无需 /g 正则

3. 转换 / 格式化

方法 作用 示例 关键说明
⭐️ toLowerCase() 转小写 "ABC".toLowerCase() → "abc" 不修改原字符串
⭐️ toUpperCase() 转大写 "abc".toUpperCase() → "ABC" 不修改原字符串
⭐️ trim() 去除首尾空格 " abc ".trim() → "abc" 不处理中间空格
trimStart()/trimLeft() 去除开头空格 " abc".trimStart() → "abc" 别名,作用一致
trimEnd()/trimRight() 去除结尾空格 "abc ".trimEnd() → "abc" 别名,作用一致
repeat(n) 重复字符串 "ab".repeat(2) → "abab" n 为 0 返空,负数报错
padStart(len, str) 头部补全 "123".padStart(5, "0") → "00123" 常用于补零
padEnd(len, str) 尾部补全 "123".padEnd(5, "0") → "12300" 超出长度则截断

三、数组 & 字符串互通方法

场景 实现方式 示例
数组 → 字符串 arr.join(sep) [1,2].join("") → "12"
字符串 → 数组 str.split(sep) "abc".split("") → ["a","b","c"]
遍历字符串 转数组后用数组遍历方法 "abc".split("").forEach(char => console.log(char))

总结

  1. 数组核心:修改原数组的方法(push/pop/splice/sort)要注意副作用,遍历优先用 map/filter/reduce(返回新数组),列表查找用 find/findIndex 更高效;
  2. 字符串核心:所有方法不修改原字符串,截取用 slice、替换用 replace/replaceAll、分割用 split,判断包含用 includes;
  3. 高频互通:数组转字符串用 join,字符串转数组用 split,是开发中最常用的联动操作。

《 Koa.js 》教程 | 一份不可多得的 Node.js 的 Web 框架 Koa.js 教程

第一章 安装和配置 koa

Koa 是一个轻量级、现代化的框架, 由 Express 原班人马开发

初始化配置文件 package.json

npm init -y

配置 package.json (ESM规范)

{
     "type": "module",
     "name": "demo",
     "version": "1.0.0",
     "main": "index.js",
     "scripts": {
          "dev":"nodemon index.js",
           "test": "echo \"Error: no test specified\" && exit 1"
     },
     "keywords": [],
     "author": "",
     "license": "ISC",
     "description": ""
}

npm 官网

     www.npmjs.com

安装koa      

npm i koa

     全局安装 nodemon

  .  npm i nodemon -g

     当 nodemon 检测到监视的文件发生更改时, 会自动重新启动应用

第二章 创建并启动 http 服务器

中间件

中间件是处理 HTTP 请求和响应的函数,它们可以做以下操作:

  • 处理请求(例如解析请求体、验证用户身份等)
  • 修改响应(例如设置响应头、发送响应体等)
  • 执行后续中间件

中间件 - 很重要的概念 !!!!!!!

注意 : app.use() 方法用于注册 中间件

中间件 是处理 http 请求和响应的函数 , 当一个请求到达服务器时, 会从第一个中间件开始执行, 直到最后一个中间件

上下文对象 ctx

在 Koa 中,ctx(上下文)对象是每个中间件函数的核心,它包含了请求和响应的所有信息。所有的 HTTP 请求和响应都通过 ctx 进行处理。

上下文对象 ctx ( context ) 包含了与当前 http 请求相关的所有信息

如: http方法、url、请求头、请求体、查询参数等

import Koa from 'koa'

const hostname = "127.0.0.1" //服务器监听的ip地址
const port = 8008 //服务器监听的端口号

/*
    实例化一个 Koa 对象
    实例化是指根据一个类创建具体对象的过程
*/
const app = new Koa()

app.use(async ctx => {
    ctx.body = "juejin.cn" // 使用 ctx.body 设置响应体的内容
})

//启动 http 服务器, 并在指定的ip地址(127.0.0.1)和端口(8008)上监听连接请求
app.listen(port, hostname, () => {
    console.log(`服务器已启动: http://${hostname}:${port}`)
})

第三章 洋葱模型

洋葱模型

当你处理一个请求时,

可以想象成是在 "剥洋葱" ,从外向内一层一层地往里剥,直到剥到中心部分

这个过程涉及对 请求 的多个层面进行解析、验证、处理

在处理完洋葱(请求)后,

构建 响应 的过程就像是从精心准备的食材 ( 处理请求 后得到的数据) 开始,

从内向外逐层添加调料(格式化、封装等),最终形成一道色香味俱佳的菜肴(响应)

image.png

import Koa from 'koa'

const hostname = "127.0.0.1" //服务器监听的ip地址
const port = 8008 //服务器监听的端口号

/*
    实例化一个 Koa 对象
    实例化是指根据一个类创建具体对象的过程
*/
const app = new Koa()

/*
    app.use() 方法用于注册中间件
    中间件是处理 http 请求和响应的函数
    当一个请求到达服务器时, 会从第一个中间件开始执行, 直到最后一个中间件
    
    上下文对象 ctx(context) 包含了与当前 http 请求相关的所有信息
    如: http方法、url、请求头、请求体、查询参数等
*/
app.use(async (ctx,next) => {
    console.log(1)
    await next() //若中间件调用了next(),会暂停当前中间件的执行,将控制权传递给下一个中间件
    console.log(2)
})

app.use(async (ctx,next) => { 
    console.log(3)
    await next()
    console.log(4)
})

//当中间件没有再调用next(),则不需要再将控制权传递给下一个中间件,控制权会按照相反的顺序执行
app.use(async (ctx,next) => {
    console.log(5)
    ctx.body = "dengruicode.com" // 使用 ctx.body 设置响应体的内容
})

//启动 http 服务器, 并在指定的ip地址(127.0.0.1)和端口(8008)上监听连接请求
app.listen(port, hostname, () => {
    console.log(`服务器已启动: http://${hostname}:${port}`)
})

第四章 安装和配置路由 - get请求

在 Koa 中,koa-router 是一个轻量级的路由中间件,它可以帮助你定义路由、处理 HTTP 请求并解析请求参数。通过使用 koa-router,你可以创建一个灵活的路由系统,轻松地组织和管理 Koa 应用的各个部分。

安装 koa-router

首先,你需要安装 koa-router

npm install @koa/router       # 注意:新版 koa-router 包名是 @koa/router
import Koa from 'koa'
import Router from '@koa/router'

const hostname = "127.0.0.1"
const port = 8008

const app = new Koa()
const router = new Router() //实例化一个 Router 对象

//------ get请求
//路由是根据客户端发送的请求(包括请求的路径、方法等)调用与之匹配的处理函数
//根路由 http://127.0.0.1:8008/
router.get('/', async ctx => { //get请求
    ctx.body = "dengruicode.com"
})

//查询参数 http://127.0.0.1:8008/test?id=001&web=dengruicode.com
router.get('/test', async ctx => { //get请求
    let id = ctx.query.id
    let web = ctx.query.web
    ctx.body = id + " : " + web
})

//路径参数 http://127.0.0.1:8008/test2/id/002/web/www.dengruicode.com
router.get('/test2/id/:id/web/:web', async ctx => {
    let id = ctx.params.id
    let web = ctx.params.web
    ctx.body = id + " : " + web
})

//重定向路由 http://127.0.0.1:8008/test3
router.redirect('/test3', 'https://www.baidu.com')

app.use(router.routes()) //将定义在 router 对象中的路由规则添加到 app 实例中

//------ 路由分组
//http://127.0.0.1:8008/user/add
//http://127.0.0.1:8008/user/del

const userRouter = new Router({ prefix: '/user' })
userRouter.get('/add', async ctx => {
    ctx.body = "添加用户"
})
userRouter.get('/del', async ctx => {
    ctx.body = "删除用户"
})
app.use(userRouter.routes())

// 在所有路由之后添加404处理函数
app.use(async ctx => {
    if (!ctx.body) { //若没有设置 ctx.body, 则说明没有到匹配任何路由
        ctx.status = 404
        ctx.body = '404 Not Found'
    }
})

app.listen(port, hostname, () => {
    console.log(`服务器已启动: http://${hostname}:${port}`)
})

第五章 post请求

安装 koa-body

Koa 原生不支持解析 POST 请求体,需安装 koa-body 中间件:

npm install koa-body

POST 请求处理示例

修改 src/index.js,新增 POST 路由:

import Koa from 'koa';
import Router from '@koa/router';
import { koaBody } from 'koa-body';

const app = new Koa();
const router = new Router();
const port = 8008;

// 注册 koa-body 中间件:解析 JSON、表单、文件类型的 POST 数据
app.use(koaBody({
  multipart: true, // 支持文件上传(后续第八章用)
  json: true, // 解析 JSON 格式
  urlencoded: true // 解析表单格式(application/x-www-form-urlencoded)
}));

// 1. 处理 JSON 格式 POST 请求
router.post('/api/json', async (ctx) => {
  const { name, age } = ctx.request.body;
  ctx.body = {       // ctx.request.body 是 koa-body 解析后的 POST 数据
    code: 200,
    msg: "JSON 数据接收成功",
    data: { name, age }
  };
});

// 2. 处理表单格式 POST 请求
router.post('/api/form', async (ctx) => {
  const { username, password } = ctx.request.body;
  ctx.body = {
    code: 200,
    msg: "表单数据接收成功",
    data: { username, password }
  };
});

app.use(router.routes());

// 404 处理
app.use(async (ctx) => {
  ctx.status = 404;
  ctx.body = '404 Not Found';
});

app.listen(port, () => {
  console.log(`POST 服务器启动:http://localhost:${port}`);
});

测试 POST 请求(两种方式)

方式 1:Postman 测试

  • 请求地址:http://localhost:8008/api/json

  • 请求方法:POST

  • 请求体:选择 raw > JSON,输入:

    { "name": "张三", "age": 20 }
    
  • 响应:{"code":200,"msg":"JSON 数据接收成功","data":{"name":"张三","age":20}}

方式 2:curl 命令测试

# 测试 JSON 格式
curl -X POST -H "Content-Type: application/json" -d '{"name":"张三","age":20}' http://localhost:8008/api/json

# 测试表单格式
curl -X POST -d "username=admin&password=123456" http://localhost:8008/api/form

第六章 错误处理

import Koa from 'koa'
import Router from '@koa/router'

const hostname = "127.0.0.1"
const port = 8008

const app = new Koa()
const router = new Router()

//http://127.0.0.1:8008/
router.get('/', async ctx => {
    throw new Error("测试")
})

/*
    将 '错误处理中间件' 放在 '路由处理中间件' 之前, 当一个请求到达时,
    会先经过 '错误处理中间件', 然后才会进入 '路由处理中间件',
    是为了确保可以捕获错误
*/
app.use(async (ctx, next) => {  // 错误处理中间件
    try {
        await next()
    } catch (err) {
        //console.log('err:', err)
        ctx.status = 500
        ctx.body = 'err: ' + err.message
    }
})

app.use(router.routes())   // 路由处理中间件

app.listen(port, hostname, () => {
    console.log(`服务器已启动: http://${hostname}:${port}`)
})

第七章 允许跨域请求

安装跨域中间件

npm install @koa/cors

跨域配置示例

import Koa from 'koa';
import Router from '@koa/router';
import Cors from '@koa/cors';

const app = new Koa();
const router = new Router();
const port = 8008;

app.use(Cors()) //允许跨域请求

// 测试跨域路由
router.get('/api/cors', async (ctx) => {
  ctx.body = {
    code: 200,
    msg: "跨域请求成功"
  };
});

app.use(router.routes());

app.listen(port, () => {
  console.log(`跨域服务器启动:http://localhost:${port}`);
});

测试跨域

在任意前端项目(如 Vue / React / HTML 文件)中发送请求:

// 前端代码示例
fetch('http://localhost:8008/api/cors')
  .then(res => res.json())
  .then(data => console.log(data)) // 输出 {code:200, msg:"跨域请求成功"}
  .catch(err => console.error(err));

无跨域报错即配置成功。

第八章 上传图片

依赖准备(复用 koa-body)

koa-body 已支持文件上传,无需额外安装依赖,只需确保配置 multipart: true

图片上传示例

import Koa from 'koa';
import Router from '@koa/router';
import { koaBody } from 'koa-body';
import fs from 'fs';
import path from 'path';

const app = new Koa();
const router = new Router();
const port = 8008;

// 1. 创建上传目录(不存在则创建)
const uploadDir = path.join(__dirname, 'uploads');
if (!fs.existsSync(uploadDir)) {
  fs.mkdirSync(uploadDir, { recursive: true });
}

// 2. 配置 koa-body 支持文件上传
app.use(koaBody({
  multipart: true, // 开启文件上传
  formidable: {
    uploadDir: uploadDir, // 临时存储目录
    keepExtensions: true, // 保留文件扩展名(如 .png/.jpg)
    maxFieldsSize: 2 * 1024 * 1024, // 限制文件大小 2MB
    filename: (name, ext, part, form) => {
      // 自定义文件名:时间戳 + 原扩展名,避免重复
      return Date.now() + ext;
    }
  }
}));

// 3. 图片上传接口
router.post('/api/upload', async (ctx) => {
  // ctx.request.files 是上传的文件对象
  const file = ctx.request.files.file; // 前端上传的文件字段名需为 file
  if (!file) {
    ctx.status = 400;
    ctx.body = { code: 400, msg: "请选择上传的图片" };
    return;
  }

  // 返回文件信息
  ctx.body = {
    code: 200,
    msg: "图片上传成功",
    data: {
      filename: file.newFilename, // 自定义后的文件名
      path: `/uploads/${file.newFilename}`, // 访问路径
      size: file.size // 文件大小(字节)
    }
  };
});

// 4. 静态文件访问:让上传的图片可通过 URL 访问
app.use(async (ctx, next) => {
  if (ctx.path.startsWith('/uploads/')) {
    const filePath = path.join(uploadDir, ctx.path.replace('/uploads/', ''));
    if (fs.existsSync(filePath)) {
      ctx.type = path.extname(filePath).slice(1); // 设置响应类型(如 png/jpg)
      ctx.body = fs.createReadStream(filePath); // 读取文件并返回
      return;
    }
    ctx.status = 404;
    ctx.body = "文件不存在";
    return;
  }
  await next();
});

app.use(router.routes());

app.listen(port, () => {
  console.log(`图片上传服务器启动:http://localhost:${port}`);
});

测试图片上传

方式 1:Postman 测试

  • 请求地址:http://localhost:8008/api/upload
  • 请求方法:POST
  • 请求体:选择 form-data,Key 为 file,Type 选 File,上传一张图片。
  • 响应:返回文件路径,如 http://localhost:8008/uploads/1738987654321.png,访问该 URL 可查看图片。

方式 2:curl 命令测试

终端输入 bash 命令

curl -X POST -F "file=@/你的图片路径/xxx.png" http://localhost:8008/api/upload

第九章 cookie

Cookie 是存储在客户端浏览器的小型文本数据,Koa 内置 ctx.cookies API 可以操作 Cookie。

Cookie 操作示例

import Koa from 'koa'
import Router from '@koa/router'
 
const app = new Koa();
const router = new Router();
const port = 8008;

// 1. 设置 Cookie
router.get('/cookie/set', async (ctx) => {
  // ctx.cookies.set(名称, 值, 配置)
  ctx.cookies.set(
    'username', 
    encodeURIComponent('张三'), 
    {
      maxAge: 24 * 60 * 60 * 1000, // 过期时间 1 天(毫秒)
      httpOnly: true, // 仅允许服务端访问,防止 XSS 攻击
      secure: false, // 开发环境设为 false(HTTPS 环境设为 true)
      path: '/', // 生效路径(/ 表示全站)
      sameSite: 'lax' // 防止 CSRF 攻击
    }
  );
  ctx.body = { code: 200, msg: "Cookie 设置成功" };
});

// 2. 获取 Cookie
router.get('/cookie/get', async (ctx) => {
  const username = ctx.cookies.get('username');
  ctx.body = {
    code: 200,
    msg: "Cookie 获取成功",
    data: { username }
  };
});

// 3. 删除 Cookie
router.get('/cookie/delete', async (ctx) => {
  ctx.cookies.set('username', '', { maxAge: 0 }); // 设置 maxAge 为 0 即删除
  ctx.body = { code: 200, msg: "Cookie 删除成功" };
});

app.use(router.routes());

app.listen(port, () => {
  console.log(`Cookie 服务器启动:http://localhost:${port}`);
});

测试 Cookie

  1. 访问 http://localhost:8008/cookie/set → 设置 Cookie;
  2. 访问 http://localhost:8008/cookie/get → 获取 Cookie,输出 {username: "张三"}
  3. 访问 http://localhost:8008/cookie/delete → 删除 Cookie,再次获取则为 undefined

第十章 session

安装 Session 中间件

Koa 原生不支持 Session,需安装 koa-session

npm install koa-session

Session 配置示例

import Koa from 'koa'
import Router from '@koa/router'
import session  from 'koa-session'

const app = new Koa();
const router = new Router();
const port = 8008;

// 1. 配置 Session 密钥(生产环境需改为随机字符串)
app.keys = ['dengruicode_secret_key'];

// 2. Session 配置
const CONFIG = {
  key: 'koa:sess', // Session Cookie 名称
  maxAge: 24 * 60 * 60 * 1000, // 过期时间 1 天
  autoCommit: true,
  overwrite: true,
  httpOnly: true, // 仅服务端访问
  signed: true, // 签名 Cookie,防止篡改
  rolling: false, // 不刷新过期时间
  renew: false, // 快过期时自动续期
  secure: false, // 开发环境 false
  sameSite: 'lax'
};

// 3. 注册 Session 中间件
app.use(session(CONFIG, app));

// 4. Session 操作
// 设置 Session
router.get('/session/set', async (ctx) => {
  ctx.session.user = {
    id: 1,
    name: "张三",
    age: 20
  };
  ctx.body = { code: 200, msg: "Session 设置成功" };
});

// 获取 Session
router.get('/session/get', async (ctx) => {
  const user = ctx.session.user;
  ctx.body = {
    code: 200,
    msg: "Session 获取成功",
    data: { user }
  };
});

// 删除 Session
router.get('/session/delete', async (ctx) => {
  ctx.session = null; // 清空 Session
  ctx.body = { code: 200, msg: "Session 删除成功" };
});

app.use(router.routes());

app.listen(port, () => {
  console.log(`Session 服务器启动:http://localhost:${port}`);
});

测试 Session

  1. 访问 http://localhost:8008/session/set → 设置 Session;
  2. 访问 http://localhost:8008/session/get → 获取 Session,输出用户信息;
  3. 访问 http://localhost:8008/session/delete → 清空 Session,再次获取则为 undefined

注意:koa-session 是基于 Cookie 的内存 Session,生产环境建议使用 koa-redis 将 Session 存储到 Redis,避免服务重启丢失数据。

第十一章 jwt

安装 JWT 依赖

npm install jsonwebtoken koa-jwt
  • jsonwebtoken:生成 / 解析 JWT 令牌;
  • koa-jwt:验证 JWT 令牌的中间件。

JWT 完整示例

import Koa from 'koa'
import Router from '@koa/router'
import jwt  from 'jsonwebtoken'
import koaJwt  from 'koa-jwt'

const app = new Koa();
const router = new Router();
const port = 8008;

// 1. JWT 密钥(生产环境需加密存储)
const JWT_SECRET = 'dengruicode_jwt_secret';
// JWT 过期时间:1 小时(秒)
const JWT_EXPIRES_IN = 3600;

// 2. 登录接口:生成 JWT 令牌
router.post('/api/login', async (ctx) => {
  // 模拟验证用户名密码(生产环境需查数据库)
  const { username, password } = ctx.request.body;
  if (username === 'admin' && password === '123456') {
    // 生成 JWT 令牌
    const token = jwt.sign(
      { id: 1, username }, // 载荷:存储用户信息(不要存敏感数据)
      JWT_SECRET,
      { expiresIn: JWT_EXPIRES_IN }
    );
    ctx.body = {
      code: 200,
      msg: "登录成功",
      data: { token }
    };
  } else {
    ctx.status = 401;
    ctx.body = { code: 401, msg: "用户名或密码错误" };
  }
});

// 3. 受保护的接口:需要 JWT 验证
// koa-jwt 中间件会自动解析 Authorization 头中的 token
app.use(koaJwt({ secret: JWT_SECRET }).unless({
  path: [/^/api/login/] // 排除登录接口,无需验证
}));

// 4. 获取用户信息接口(需验证 JWT)
router.get('/api/user/info', async (ctx) => {
  // ctx.state.user 是 koa-jwt 解析后的 JWT 载荷
  const { id, username } = ctx.state.user;
  ctx.body = {
    code: 200,
    msg: "获取用户信息成功",
    data: { id, username }
  };
});

app.use(router.routes());

// 5. JWT 错误处理
app.use(async (ctx, next) => {
  try {
    await next();
  } catch (err) {
    if (err.status === 401) {
      ctx.status = 401;
      ctx.body = { code: 401, msg: "token 无效或过期" };
    } else {
      throw err;
    }
  }
});

app.listen(port, () => {
  console.log(`JWT 服务器启动:http://localhost:${port}`);
});

测试 JWT

步骤 1:登录获取 token

curl -X POST -d "username=admin&password=123456" http://localhost:8008/api/login
# 响应:{"code":200,"msg":"登录成功","data":{"token":"xxx.xxx.xxx"}}

步骤 2:携带 token 访问受保护接口

curl -H "Authorization: Bearer 你的token" http://localhost:8008/api/user/info
# 响应:{"code":200,"msg":"获取用户信息成功","data":{"id":1,"username":"admin"}}

步骤 3:token 无效 / 过期测试

携带错误 token 或过期 token 访问,会返回 {"code":401,"msg":"token 无效或过期"}

总结

  1. 核心流程:Koa 开发的核心是「中间件 + 路由」,所有功能(跨域、上传、JWT)都通过中间件扩展;

  2. 关键依赖@koa/router(路由)、koa-body(POST / 上传)、@koa/cors(跨域)、koa-session(Session)、jsonwebtoken/koa-jwt(JWT);

  3. 生产建议

    • Session/JWT 密钥需随机生成并加密存储;

    • 文件上传需限制大小和类型,防止恶意上传;

    • 跨域需指定具体域名,而非 *

    • JWT 载荷不要存敏感数据,过期时间不宜过长。

❌