当前位置: 首页 > news >正文

【前端学习】阿里前端面试题

阿里前端面试题

使用过的koa2中间件

中间件就是「在请求到达服务器、到服务器返回响应之间,做一些特定事情的函数」

例如:

  1. 当用户访问你的网站时,你可能需要先判断他有没有登录(这就是「认证中间件」要做的);
  2. 然后解析他提交的表单数据(这就是「解析请求体的中间件」要做的);
  3. 再根据他访问的 URL 找到对应的处理逻辑(这就是「路由中间件」要做的);
  4. 最后如果出错了,要统一处理错误(这就是「错误处理中间件」要做的)。

每个中间件负责一个环节,Koa2 会按顺序执行它们,最终完成一次请求的处理

常用 Koa2 中间件:

路由中间件:koa-router

作用:帮你「根据用户访问的 URL 地址,找到对应的处理代码」

例如:

  1. 用户访问 http://你的网站/home,你希望显示首页内容;
  2. 用户访问 http://你的网站/user,你希望显示用户信息。

如果没有 koa-router,你就得自己写一堆 if-else 判断 URL,比如:

if (ctx.url === '/home') { ... }
else if (ctx.url === '/user') { ... }

有了 koa-router,就可以直接这样写,更清晰:

router.get('/home', (ctx) => { ctx.body = '首页' })
router.get('/user', (ctx) => { ctx.body = '用户页' })

请求体解析:koa-bodyparser 和 koa-multer

作用:帮你「读取用户提交的数据」(比如表单、JSON 数据、文件)

  1. 用户在网页上填了表单(比如登录时输入账号密码),点击提交后,数据会发送到服务器。但这些数据在服务器端不是直接能读的,需要「解析」才能用。
  2. koa-bodyparser 专门解析「普通数据」(比如 JSON、表单文字),解析后你可以用 ctx.request.body 直接拿到数据,比如 ctx.request.body.username 就是用户输入的账号。
  3. koa-multer 专门解析「文件」(比如用户上传的图片、文档),因为文件数据格式特殊,koa-bodyparser 处理不了,所以需要它来单独处理。

静态资源服务:koa-static

作用:帮你「直接返回网页、图片、CSS/JS 文件等静态内容」

比如你的项目里有 index.html、style.css、logo.png 这些文件,用户访问 http://你的网站/logo.png 时,服务器需要把这个图片返回给浏览器。

如果没有 koa-static,你得手动写代码读取文件再返回,很麻烦。有了它,只需要指定这些文件存在的文件夹(比如 public 文件夹),它会自动处理这些请求,直接返回对应的文件。

模板引擎集成:koa-views

作用:帮你「把动态数据嵌入到 HTML 里,生成最终的网页」

比如你想在网页上显示用户的名字(这个名字是从数据库查出来的,动态变化),直接写死在 HTML 里肯定不行。这时候可以用模板引擎(比如 EJS),在 HTML 里留一个「占位符」<%= username %>,然后用 koa-views 把从数据库拿到的 username 填进去,生成最终的 HTML 给浏览器。

跨域处理:@koa/cors

作用:解决「不同网站之间不能随便互相调用接口」的问题

比如你的前端页面部署在 http://a.com,而后端接口部署在 http://b.com,当前端想调用后端接口时,浏览器会出于安全考虑阻止这个请求(这叫「跨域限制」)。

@koa/cors 就是告诉浏览器:「允许 a.com 来调用我的接口」,这样前端就能正常请求后端了。你可以配置允许哪些网站访问、允许哪些操作(比如 GET/POST)等。

日志记录:koa-logger 或 koa-log4js

作用:帮你「记录用户的访问情况」,方便调试和排查问题

例如:

  1. 用户访问了哪个 URL?
  2. 服务器返回了成功还是失败(状态码)?
  3. 这次请求花了多长时间?

koa-logger 会在控制台直接打印这些信息(适合开发时用);koa-log4js 更强大,可以把日志存到文件里(适合上线后用,方便后期查看历史记录)

错误处理:koa-onerror

作用:帮你「统一处理服务器运行时的错误」,避免网站崩溃

比如代码里有 bug(比如调用了不存在的变量),如果没处理,服务器可能直接崩溃,用户会看到一个奇怪的错误页面。

koa-onerror 可以捕获这些错误,然后返回一个友好的提示(比如「服务器出错了,请稍后再试」),同时不会让服务器崩溃。

身份认证:koa-jwt 或 koa-passport

作用:帮你「判断用户是否登录,以及登录的是谁」

比如有些页面(如个人中心)只有登录后才能访问,这时候就需要验证用户身份。

koa-jwt 基于「Token」验证:用户登录成功后,服务器发一个 Token 给前端,前端下次请求时带上这个 Token,koa-jwt 会验证 Token 是否有效,有效就允许访问。

koa-passport 更灵活,支持多种登录方式(比如账号密码登录、微信登录、GitHub 登录等)。

请求限流:koa-ratelimit

作用:防止「恶意用户频繁访问你的接口,导致服务器压力过大」

比如有人故意每秒发 1000 次请求攻击你的服务器,可能会把服务器搞垮。koa-ratelimit 可以限制:「同一个 IP 地址,1 分钟内最多只能访问 100 次」,超过就拒绝,保护服务器。

压缩响应:koa-compress

作用:「减小服务器返回给浏览器的数据大小」,让网页加载更快

比如服务器要返回一个很大的 HTML 或 JSON 数据,koa-compress 会把它压缩(类似压缩包),浏览器收到后再解压。这样传输的数据量变小,加载速度就快了。

总结:中间件的核心作用就是「分工合作」,每个中间件解决一个具体问题,让你不用重复写代码,能快速搭建服务器。

koa-body原理

koa-bodyparser与koa-body的区别:

  1. koa-bodyparser 是「轻量版」:只解析 普通文本类型的请求体(如 JSON、表单数据),不支持文件上传。
  2. koa-body 是「全能版」:不仅能解析普通文本请求体,还支持 文件上传(multipart/form-data 类型),功能更全面。

koa-body 是 Koa 中一个常用的请求体解析中间件,它的核心作用是 自动解析客户端发送到服务器的请求体数据(比如表单数据、JSON、文件等),并把解析后的数据挂载到 ctx.request.body 或 ctx.body 上,方便开发者直接使用。

当客户端(比如浏览器、手机 App)向服务器发送请求时,除了 URL、请求方法(GET/POST 等),还可能携带一些「数据」,这些数据就存放在「请求体」里。例如:登录时提交的账号密码(表单数据);前端通过 fetch 发送的 JSON 数据({ "name": "张三" });上传的图片、文件等。

这些数据不会直接显示在 URL 里,而是藏在请求的「body」部分,服务器需要通过特定的方式才能读取到。

Koa 框架本身并不自带请求体解析功能。如果没有中间件,开发者需要手动读取请求体数据,过程非常繁琐:客户端发送的请求体是「数据流」,服务器需要监听 data 事件,一点点拼接数据;不同类型的请求体(JSON、表单、文件)格式不同,解析规则也不同(比如 JSON 需要用 JSON.parse(),表单需要按 key=value&key2=value2 拆分);处理文件上传时,还需要处理二进制数据、临时存储等问题。

koa-body 的作用就是 帮我们自动完成这些繁琐的工作,兼容多种数据格式,最终给出一个直接可用的解析结果

koa-body 本质上是对更低层的解析工具(如 co-body、formidable 等)的封装,它的工作流程可以分为 3 步:

  1. 判断请求体的「类型」:客户端发送请求时,会在请求头 Content-Type 中说明请求体的格式,koa-body 首先会读取这个字段,判断数据类型:
    1. application/json:JSON 格式数据(比如 { "name": "张三" });
    2. application/x-www-form-urlencoded:普通表单数据(比如 name=张三&age=18);
    3. multipart/form-data:带文件的表单数据(比如上传图片、文档)。
  2. 根据类型选择对应的解析方式:针对不同的 Content-Type,koa-body 会调用不同的解析逻辑:
    1. 解析 JSON 或普通表单:用 co-body(Koa 生态中处理请求体的工具)来解析。它会监听请求的 data 事件,把二进制数据流拼接成字符串,再根据类型转换:
      • JSON 类型:用 JSON.parse() 转成 JavaScript 对象;
      • 表单类型:按 & 分割字符串,再把 key=value 转成对象(比如 name=张三&age=18 转成 { name: '张三', age: '18' })。
    2. 解析带文件的表单(multipart/form-data:用 formidable(专门处理文件上传的工具)来解析。因为文件是二进制数据,需要特殊处理:
      • 把文件临时存到服务器的硬盘上(可配置存储路径);
      • 解析出普通表单字段(如文件名、描述)和文件信息(如存储路径、大小、类型);
      • 最终返回一个包含字段和文件的对象。
  3. 把解析结果挂载到 Koa 的 ctx 上:解析完成后,koa-body 会把结果挂载到 ctx.request.body 或 ctx.body 上(默认是 ctx.request.body),开发者直接通过这个属性就能获取数据:
  • 解析 JSON 或普通表单后
    console.log(ctx.request.body);  { name: '张三', age: '18' }

    解析带文件的表单后
    console.log(ctx.request.body);  { username: '张三' }(普通字段)
    console.log(ctx.request.files);  { avatar: { path: '/tmp/xxx.png', size: 1024 } }(文件信息)

总结:koa-body 的原理可以概括为:「读类型 → 拆数据 → 转格式 → 给结果」

对于新手来说,不用深究底层代码,只要知道它能帮我们解析 JSON、表单、文件,并通过 ctx.request.body 获取数据就足够日常使用了

介绍自己写过的中间件

在使用 Koa2 开发时,除了使用现成的中间件,有时也会根据业务需求自定义中间件来解决特定问题

自定义中间件的核心逻辑:无论实现什么功能,Koa2 中间件的本质都是一个「异步函数」,格式为:

async (ctx, next) => {
// 1. 请求到达时的逻辑(如校验、记录)
// 2. 调用 await next() 让后续中间件执行
// 3. 后续中间件执行完后的逻辑(如格式化响应、记录耗时)
}

核心是通过 ctx 访问请求 / 响应数据,通过 next() 控制中间件执行顺序,按需拦截或放行请求。

异步函数(async/await)是 JavaScript 中用于简化异步操作的语法糖,它让原本需要用回调函数(Callback)或 Promise 链式调用(.then())处理的异步代码,变得像同步代码一样直观易读。

在 JavaScript 中,代码通常是「同步执行」的(从上到下,执行完一行再执行下一行)。但有些操作需要「等待」(比如从服务器请求数据、读取本地文件、定时任务等),这些需要等待的操作就是「异步操作」

异步:你不用一直盯着,中间可以做别的事

在 async/await 出现之前,处理异步操作主要靠 回调函数 或 Promise,但它们有明显缺点:

  1. 回调函数多层嵌套时,会形成「回调地狱」(代码缩进越来越深,难以维护);
  2. Promise 的链式调用(.then().then())虽然比回调好,但多了之后依然不够直观。

async/await 的出现,就是为了让异步代码写起来像同步代码一样清晰

异步函数的基本用法:

  1. 声明异步函数:async 关键字:在函数前面加 async,这个函数就变成了「异步函数」。它有两个特点:
    1. 函数的返回值会自动包装成一个 Promise 对象(即使你 return 一个普通值,比如 return 123,实际返回的是 Promise.resolve(123));
    2. 函数内部可以使用 await 关键字。
    • 声明一个异步函数
      async function fetchData() {
      return '数据获取成功';  等价于 return Promise.resolve('数据获取成功')
      }

      调用异步函数(返回的是 Promise)
      fetchData().then(result => {
      console.log(result);  输出:数据获取成功
      });
  1. 暂停等待:await 关键字:await 只能用在 async 函数内部,它的作用是「暂停执行当前异步函数,等待后面的 Promise 完成,然后拿到结果再继续执行」。简单说:await 后面跟一个 Promise 对象,它会「等这个 Promise 成功(resolved)」,然后把结果取出来,赋值给变量。
  • 模拟一个异步操作(比如从服务器请求数据)
    function getCoffee() {
    return new Promise((resolve) => {
    setTimeout(() => { 模拟2秒后完成
    resolve('热咖啡好了');
    }, 2000);
    });
    }

    用 async/await 处理
    async function orderCoffee() {
    console.log('开始点咖啡');
    const coffee = await getCoffee();  等待 getCoffee() 完成,拿到结果
    这 2 秒内,JavaScript 不会卡住,可以去做其他事(比如处理别的代码),但 orderCoffee() 函数在这里「暂停等待」
    console.log(coffee);  2秒后输出:热咖啡好了
    console.log('喝咖啡');
    }

    orderCoffee();
    执行顺序:
    1. 立即输出 "开始点咖啡"
    2. 等待2秒
    3. 输出 "热咖啡好了"
    4. 输出 "喝咖啡"
  •  await 让异步操作看起来像同步流程,比用 .then() 更直观:
  • 不用 async/await 的写法(Promise 链式调用)
    function orderCoffee() {
    console.log('开始点咖啡');
    getCoffee().then(coffee => {
    console.log(coffee);
    console.log('喝咖啡');
    });
    }
  1. 处理错误:try/catch:如果 await 后面的 Promise 失败(rejected),会抛出一个错误,需要用 try/catch 捕获,否则会导致程序报错
  1.  getCoffee() {
    return new Promise((resolve, reject) => {
    setTimeout(() => {
    reject('咖啡机坏了,做不了咖啡'); // 模拟失败
    }, 2000);
    });
    }

    async function orderCoffee() {
    try {
    console.log('开始点咖啡');
    const coffee = await getCoffee(); // 等待失败的 Promise
    console.log(coffee); // 这里不会执行
    } catch (error) {
    console.log('出错了:', error); // 2秒后输出:出错了:咖啡机坏了,做不了咖啡
    }
    }

    orderCoffee();

总结:

  1. async:声明一个异步函数,使其返回值自动变为 Promise
  2. await:在异步函数内部,暂停等待 Promise 完成,直接获取结果(避免了 .then() 链式调用)。
  3. 作用:让异步代码的写法更接近同步代码,解决了回调地狱和 Promise 链式调用的可读性问题。

对于前端开发来说,async/await 是处理接口请求、文件操作等异步场景的必备语法

接口访问频率限制中间件

作用:限制同一 IP 在短时间内对某个接口的访问次数,防止恶意请求(比 koa-ratelimit 更简单,适合特定场景)

核心思路

  1. 用一个对象记录每个 IP 的访问时间和次数({ ip: { count: 次数, lastTime: 最后访问时间 } });
  2. 每次请求进来时,检查该 IP 的访问记录:如果单位时间内次数超过限制,返回 429 错误(请求过于频繁);否则更新次数和时间。

简化代码:

function rateLimitMiddleware(limit = 10, timeWindow = 60000) { // 默认1分钟最多10次
const ipMap = new Map(); // 存储IP访问记录

return async (ctx, next) => {
const clientIp = ctx.ip; // 获取客户端IP
const now = Date.now();
const record = ipMap.get(clientIp) || { count: 0, lastTime: now }; // 如果该IP之前没有记录,就新建一条:次数0,最后访问时间为现在

// 如果超过时间窗口,重置计数
if (now - record.lastTime > timeWindow) {
record.count = 0;
record.lastTime = now;
}

// 检查是否超过限制
if (record.count >= limit) {
ctx.status = 429;
ctx.body = { message: '请求过于频繁,请稍后再试' };
return; // 不执行后续中间件
}

// 更新计数,继续执行后续逻辑
record.count++;
ipMap.set(clientIp, record);
await next(); // 放行
};
}

// 使用:对所有接口生效,或单独对某个路由生效
app.use(rateLimitMiddleware(5, 30000)); // 30秒最多5次

接口响应格式化中间件

作用:统一接口返回格式,避免前端处理不同格式的响应(比如成功时都返回 { code: 200, data: ... },失败时返回 { code: 500, message: ... })

核心思路

  1. 先执行后续中间件(让业务逻辑处理请求);
  2. 拦截响应结果,用统一格式包装后返回;
  3. 同时处理错误(比如业务中抛出的异常),统一错误格式。

简化代码:

function responseFormatMiddleware() {
return async (ctx, next) => {
try {
await next(); // 先执行后续中间件(业务逻辑)

// 如果业务逻辑已经设置了body,统一包装
ctx.body = {
code: 200, // 成功状态码
message: 'success',
data: ctx.body || null // 业务返回的数据
};
} catch (error) {
// 捕获错误,统一错误格式
ctx.status = 200; // 有时前端希望HTTP状态码都是200,用code区分错误
ctx.body = {
code: error.code || 500, // 自定义错误码
message: error.message || '服务器内部错误',
data: null
};
}
};
}

// 使用:放在所有路由中间件之前,确保所有响应都经过格式化
app.use(responseFormatMiddleware());

// 业务路由中直接返回数据即可,无需关心格式
router.get('/user', async (ctx) => {
ctx.body = { name: '张三' }; // 最终会被包装成 { code:200, data: { name: '张三' } }
});

  1. 成功时:{ code: 200, message: 'success', data: 业务数据 }
  2. 失败时:{ code: 错误码, message: '错误信息', data: null }

接口权限校验中间件

作用:验证用户是否有权限访问某个接口(比如只有管理员能访问 /admin 相关接口)

核心思路

  1. 假设用户登录后,Token 解析出的信息(如角色)存放在 ctx.state.user 中;
  2. 配置需要权限的接口路径和对应的角色(比如 { '/admin': ['admin'] });
  3. 请求进来时,检查当前接口是否需要权限,如果需要则验证用户角色是否匹配,不匹配则返回 403 错误。

简化代码:

function authMiddleware(authConfig = {}) { // 配置:{ 接口路径: 允许的角色数组 }
return async (ctx, next) => {
const { url } = ctx;
// 找到当前接口对应的权限配置(比如 /admin 对应 ['admin'])
const requiredRoles = Object.keys(authConfig).find(path => url.startsWith(path))
? authConfig[Object.keys(authConfig).find(path => url.startsWith(path))]
: null;

// 如果接口不需要权限,直接放行
if (!requiredRoles) {
await next();
return;
}

// 检查用户是否登录(假设未登录时 ctx.state.user 不存在)
if (!ctx.state.user) {
ctx.status = 401;
ctx.body = { message: '请先登录' };
return;
}

// 检查用户角色是否在允许的范围内
const userRole = ctx.state.user.role;
if (!requiredRoles.includes(userRole)) {
ctx.status = 403;
ctx.body = { message: '没有权限访问' };
return;
}

// 权限通过,执行后续逻辑
await next();
};
}

// 使用:配置需要权限的接口
app.use(authMiddleware({
'/admin': ['admin'], // /admin 开头的接口只有 admin 角色能访问
'/user/delete': ['admin', 'editor'] // 删除用户需要 admin 或 editor 角色
}));

有没有涉及到Cluster

在自定义 Koa 中间件时,我确实处理过涉及 Cluster(集群模式) 的场景,主要是为了解决「单机限流中间件在集群环境下失效」的问题。

Node.js 是单线程的,为了充分利用多核 CPU,Node 提供了 cluster 模块,可以启动多个子进程(worker)共享同一个端口。比如 4 核 CPU 可以启动 4 个子进程,同时处理请求。

但问题在于:子进程之间内存不共享。如果我们写的中间件依赖「进程内的内存数据」(比如之前提到的「限流中间件」用 ipMap 记录 IP 访问次数),在 Cluster 模式下会失效:

  1. 假设启动了 2 个子进程,同一个 IP 的请求可能被分配到不同的子进程;
  2. 每个子进程的 ipMap 是独立的,导致「总请求次数被多进程拆分计算」,限流规则形同虚设(比如限制 10 次 / 分钟,实际可能达到 20 次,因为两个进程各记 10 次)

我处理过的 Cluster 相关中间件:「分布式限流中间件」

为了解决 Cluster 模式下的限流问题,我基于「共享存储」改造了限流中间件,核心思路是:让所有子进程共享同一份访问记录(不再依赖单个进程的内存)

实现方案:用 Redis 作为共享存储(Redis 是单线程的,支持原子操作,适合计数场景),每个子进程都从 Redis 读写 IP 访问记录。

核心代码:

const redis = require('redis');
const { promisify } = require('util');

// 创建 Redis 客户端(所有子进程共享同一个 Redis 服务)
const client = redis.createClient({
host: 'localhost',
port: 6379
});
const getAsync = promisify(client.get).bind(client);
const setAsync = promisify(client.set).bind(client);
const incrAsync = promisify(client.incr).bind(client);

function clusterSafeRateLimit(limit = 10, timeWindow = 60000) {
return async (ctx, next) => {
const clientIp = ctx.ip;
const redisKey = `rate_limit:${clientIp}`; // Redis 中存储的 key

// 1. 检查 Redis 中是否已有该 IP 的记录
let count = await getAsync(redisKey);
if (!count) {
// 首次访问:初始化计数为 1,并设置过期时间(等于时间窗口)
await setAsync(redisKey, 1, 'EX', timeWindow / 1000); // EX 单位是秒
count = 1;
} else {
// 非首次访问:计数 +1
count = await incrAsync(redisKey);
}

// 2. 检查是否超过限制
if (Number(count) > limit) {
ctx.status = 429;
ctx.body = { message: '请求过于频繁,请稍后再试' };
return;
}

// 3. 放行
await next();
};
}

http://www.dtcms.com/a/597953.html

相关文章:

  • 需求开发:从愿景到规格的完整路径
  • 青少年思想道德建设网站高端网站建设案例
  • 华为仓颉编程语言 | 发展历程与创新应用
  • 外贸网站海外推广3个必去网站柚子皮wordpress主题
  • 宁波网站制作哪家强上海企业建站咨询
  • Python中json.loads()和json.dumps()的区别
  • 在线教育系统源码架构设计指南:高并发场景下的性能优化与数据安全
  • 做wish如何利用数据网站linux是哪个公司开发的
  • LeetCode算法学习之单词拆分
  • 英文网站做百度权重有意义吗wordpress 开发列表网
  • 七代内存(DDR5)技术发展现状
  • 测开高频面试题集锦 | 项目测试 接口测试自动化
  • 郑州上街网站建设公司买东西的网站
  • 卡在触觉的AI,一目科技让机器人从“看世界”到“摸世界”
  • mysql在线DDL
  • K8S RD: Prometheus与Kubernetes高级配置与管理实践:监控、持久化、高可用及安全机制详解
  • 建设一个直播网站要多少钱重庆旅游网站建设
  • 跟我一起学做网站知更鸟wordpress主题下载
  • 【开发者导航】面向生成式模型研发的多模态开发库:Diffusers
  • 小白如何搭建一个网站小游戏入口免费游戏
  • Vue Router (重定向和别名)
  • 邮件服务器是不是网站服务器如何建立自己的网站平台
  • 打工人日报#20251111
  • 服装网站建设美丽网站建设开发公司排名
  • Flutter for HarmonyOS开发指南(六):测试、调试与质量保障体系
  • 可信网站认证哪里有上海网站建设海淘科技
  • 【Java】2025版一天学会Java基础到高级
  • 内核哈希表RTL_DYNAMIC_HASH_TABLE的使用分析与总结
  • 网站的管理更新维护做网站用什么语言比较简单
  • “湖湘杯”——湖南网安基地的四年进化论