Skip to content

前端面经第二篇

有了Nginx为什么还需要网关?

业务层面:Nginx与网关的核心职责差异

Nginx 和网关(如 Spring Cloud Gateway、Kong 等)虽然都能处理“请求转发”,但职责边界完全不同:

  • Nginx 的核心定位是“网络层/应用层基础工具”,主要解决“流量的基础分发与网络层面的效率问题”,比如:

    • 反向代理:隐藏后端服务的真实地址,统一对外暴露一个入口(如用户访问 api.xxx.com,Nginx 转发到内部的 service1:8080service2:8081);

    • 负载均衡:把请求均匀分配到多个后端实例(如 1000 个请求分给 3 台服务器,避免单台过载);

    • 静态资源处理:直接返回 JS、CSS、图片等静态文件,减轻后端服务压力;

    • 基础限流:基于 IP 或连接数限制请求(如限制单 IP 每秒最多 100 次请求)。

    但 Nginx 本质是“无状态的网络工具”,不理解业务逻辑,无法处理和业务强相关的复杂需求。

  • 网关的核心定位是“业务流量的中枢控制器”,主要解决“微服务架构下的业务级流量治理问题”,比如:

    1. 统一认证授权:所有服务的登录验证、权限判断都由网关处理(如检查请求头中的 Token 是否有效,无效则直接拦截),避免每个服务重复开发认证逻辑;

    2. 动态服务路由:结合服务注册中心(如 Eureka、Nacos),自动发现后端服务的地址(服务上下线时无需手动改配置),并能根据业务规则转发(如“VIP 用户的请求路由到高性能实例”“测试环境的请求路由到测试服务”);

    3. 精细化流量控制:基于业务维度限流(如“商品服务每秒最多处理 500 次下单请求”)、熔断(如“支付服务故障时,网关直接返回‘系统繁忙’,避免请求堆积拖垮服务”);

    4. 业务级监控与日志:收集每笔请求的业务信息(如用户 ID、订单号),生成业务日志(方便追踪“某用户的下单请求为何失败”),而 Nginx 只能记录 IP、URL 等网络层面信息;

    5. 跨服务业务处理:比如请求经过网关时,自动补充“用户地理位置”“设备信息”等公共参数,后端服务无需重复解析。

简单说:Nginx 负责“把请求高效送到后端服务门口”,网关负责“在门口检查、筛选、处理请求,再决定是否/如何交给后端服务”。在复杂业务场景(尤其是微服务架构)中,Nginx 的基础能力无法满足业务级的流量治理需求,必须由网关补充。

用“小区管理”类比

可以把整个系统比作一个大型小区,用户的请求就是“访客”,后端服务就是“小区里的住户”:

  • Nginx 相当于小区的“大门保安”

    他的工作很基础——指挥访客从哪个门进(反向代理)、告诉访客“A 栋在左边,B 栋在右边”(基础路由)、如果同时来10个访客,安排5个走东门、5个走西门(负载均衡)。
    但他不管访客是谁、有没有权限进小区、进去要干嘛——哪怕是陌生人,只要知道地址,他都会指路。

  • 网关相当于小区的“管理处前台”

    访客到了门口,前台会先查:

    • 有没有门禁卡(认证)?没有就不让进;

    • 门禁卡权限够不够(授权)?比如只能进 A 栋的访客,不能去 B 栋;

    • 现在小区里人太多了(限流),让后面的访客等10分钟再进;

    • 如果 A 栋正在维修(服务熔断),会告诉访客“暂时去不了 A 栋,先记录需求”;

    • 还会登记访客信息(日志):“张三,下午3点去了 A 栋 501”,方便后续追溯。

    前台知道每个住户的实时状态(比如“501 住户今天在家,602 住户外出了”——对应服务注册中心的服务状态),能根据这些动态调整指引;而大门保安(Nginx)只认固定的“门牌地址”,住户不在家也会照样指路。

所以,大门保安(Nginx)管“怎么高效进出”,管理处前台(网关)管“谁能进、能去哪、出了问题怎么办”——两者分工不同,大型小区(复杂系统)缺一不可。

一次性导出10万条excel是数据,如何优化?

导出10万条数据到Excel时,核心挑战是避免内存溢出、减少响应时间、优化用户体验

以下从后端、前端及前后端协同三个维度,提供具体可落地的优化方案:

后端优化:解决“生成效率”与“资源占用”问题

后端是处理大数据量的核心,需重点优化数据查询、Excel生成、内存占用三个环节。

1. 数据查询:分批读取,避免一次性加载全量数据

  • 问题:10万条数据一次性从数据库加载到内存,可能导致OOM(内存溢出),尤其当每条数据字段较多时。

  • 优化方案

    • 采用分页查询:按limit offset或游标分页(如MySQL的id > lastId limit 1000),每次查询1000-5000条(根据字段大小调整),逐批处理。

    • 避免关联查询:如需多表数据,优先在SQL层通过join合并(数据库优化器更高效),而非内存中拼接。

    • 索引优化:确保查询条件字段有索引(如时间、ID),避免全表扫描;禁用select *,只查询需导出的字段。

    示例(伪代码)

    java
    // 游标分页查询(比limit offset更高效,适合大分页)
    long lastId = 0;
    int batchSize = 2000;
    do {
        // 每次查2000条,按id递增
        List<Data> batchData = dataMapper.selectByCursor(lastId, batchSize);
        if (batchData.isEmpty()) break;
        // 处理当前批次数据(写入Excel)
        writeToExcel(batchData);
        lastId = batchData.get(batchData.size() - 1).getId(); // 更新游标
    } while (true);

2. Excel生成:流式写入,降低内存占用

  • 问题:传统Excel库(如POI的XSSFWorkbook)会将全量数据缓存在内存,10万条数据可能占用数百MB内存,甚至OOM。

  • 优化方案

    • 使用流式Excel库:如Alibaba的EasyExcel(Java)、Python的pandas(配合openpyxl的write-only模式),支持数据逐行/逐批写入磁盘,内存占用可控制在10MB以内。

    • 避免复杂格式:减少合并单元格、复杂公式、图片等,仅保留必要的表头样式(格式越简单,生成越快)。

    • 选择合适格式:10万行数据需用.xlsx(支持104万行),禁用.xls(仅支持65536行)。

    示例(EasyExcel流式写入)

    java
    // EasyExcel核心:不缓存数据,直接写入磁盘
    EasyExcel.write(outputStream, Data.class)
             .sheet("数据")
             .doWrite(() -> {
                 // 每次调用返回一批数据(分页查询结果)
                 return getNextBatchData(); // 内部实现游标分页
             });

3. 任务处理:异步生成,避免接口超时

  • 问题:10万条数据生成Excel可能耗时10-30秒,超过接口超时时间(通常5-10秒),导致前端请求失败。

  • 优化方案

    • 异步生成:用户发起导出请求后,后端立即返回“任务ID”,并在异步线程(如Java的ThreadPoolExecutor、Python的Celery)中处理生成逻辑。

    • 进度追踪:用Redis记录任务进度(如“已处理30%”),前端通过任务ID轮询查询进度。

    • 文件存储:生成的Excel暂存至分布式存储(如MinIO、OSS),设置1小时过期时间,避免占用服务器磁盘。

    流程示例

    1. 前端:发起POST /export,携带筛选条件;
    2. 后端:生成任务ID,存入Redis(状态:处理中),提交异步任务;
    3. 后端异步线程:分页查询→流式生成Excel→上传至OSS→更新Redis状态(完成+下载URL);
    4. 前端:轮询GET /export/progress?taskId=xxx,获取进度,完成后跳转下载URL。

4. 资源控制:限制并发,避免服务器过载

  • 问题:同时有多个10万条数据导出请求,可能耗尽CPU/内存资源,影响其他服务。

  • 优化方案

    • 限制并发数:通过线程池控制异步任务的最大并发(如最多同时处理3个导出任务),超出则排队。

    • 资源隔离:为导出任务分配独立的线程池,与核心业务线程池隔离,避免互相影响。

前端优化:解决“用户体验”与“下载稳定性”问题

前端无需处理数据生成,但需优化交互流程,让用户清晰感知进度,避免操作焦虑。

1. 交互引导:提前告知成本,避免重复操作

  • 导出前提示:“当前数据量约10万条,生成需1-3分钟,是否继续?”
  • 禁用重复提交:点击“导出”后按钮置灰+加载动画,防止用户多次点击触发重复任务。

2. 进度反馈:实时展示状态,减少等待焦虑

  • 轮询进度:用任务ID定期(如每3秒)请求后端,获取“已处理XX条/总10万条”或百分比,显示进度条。
  • 状态提示:区分“生成中(30%)”“上传中”“即将开始下载”等状态,避免用户误以为卡住。

3. 下载优化:大文件下载稳定性处理

  • 避免前端处理二进制:后端生成文件后,返回预签名的OSS下载URL(如阿里云OSS的getObjectUrl),前端直接通过<a href="url" download>触发浏览器原生下载,利用浏览器的断点续传能力。
  • 处理下载失败:提供“重新下载”按钮,点击后前端重新请求最新的下载URL(避免URL过期)。

4. 数据筛选:从源头减少导出量

  • 提供筛选条件:如时间范围、关键字搜索等,让用户仅导出所需数据(例如用户可能只需要最近1个月的1万条数据,而非全量10万条)。
  • 预设“常用模板”:针对高频导出场景(如“每日汇总”“月度报表”),预定义筛选条件,减少无效数据。

前后端协同:提升整体流程效率

  1. 压缩传输:后端生成Excel后,用gzip压缩(压缩率通常30%-50%),前端下载后由浏览器自动解压,减少传输时间。
  2. 格式协商:支持用户选择导出格式(如.xlsx/.csv),csv格式生成更快、体积更小(适合纯文本数据,无格式需求场景)。
  3. 错误处理
    • 后端:生成失败时,在Redis记录错误原因(如“数据量过大”),前端轮询时展示具体提示。
    • 前端:网络中断后,允许用户通过任务ID重新查询状态,避免从头开始。

总结

  • 后端核心:分页查询+流式生成+异步处理,解决内存和效率问题;
  • 前端核心:进度反馈+下载优化+用户引导,提升体验;
  • 协同核心:任务化管理+状态同步,确保流程可控。

通过这套方案,10万条数据的导出可控制在1-3分钟内完成,内存占用低于50MB,用户体验清晰流畅。

Token 既可以存在 Cookie 中,也可以存在 LocalStorage 中,具体取决于使用场景和需求,两者各有特点:

  • 特点:Cookie 是浏览器自带的存储机制,有大小限制(约4KB),且可设置过期时间、域名限制等属性。

  • 适用场景

    • 需要利用浏览器自动携带 Token 的场景(如跨域请求时的身份验证)。

    • 希望通过服务器端控制 Token 有效期、安全性(如设置 HttpOnly 防止 XSS 攻击,Secure 限制 HTTPS 传输)。

2. 存在 LocalStorage 中

  • 特点:LocalStorage 是 HTML5 新增的本地存储方式,容量更大(通常5-10MB),数据持久存在(除非手动清除),不会随请求自动发送。

  • 适用场景

    • 前端需要主动管理 Token 发送(如通过请求头携带)的场景。

    • 存储除 Token 外的其他前端数据,或需要长期保存 Token 时(需注意安全风险)。

总结

  • 追求安全性(防 XSS 等)时,优先用 Cookie 并配置安全属性。

  • 需灵活控制 Token 发送或存储更多数据时,可选用 LocalStorage,但需手动处理安全问题(如防范 XSS 攻击)。

npm link是啥?在项目中有没有使用过

在面试中被问到npm link时,可从定义、工作原理、使用场景三个维度展开回答,既体现对命令的理解,也展示实际应用经验。以下是具体内容:

npm link 是 npm 提供的用于本地包调试的命令,核心作用是在本地开发的 npm 包与依赖它的项目之间建立符号链接(symlink),让项目可以直接使用本地正在开发的包代码,而无需将包发布到 npm 仓库再安装。

简单说:比如你开发了一个工具包 utils,同时有个项目 my-project 依赖它,用 npm link 可以让 my-project 直接“引用”你本地 utils 文件夹的代码,修改 utils 后,my-project 无需重新安装就能实时看到效果,极大提升调试效率。

二、工作原理(核心)

npm link 的本质是创建两级符号链接,流程如下:

  1. 在被依赖的包(如 utils)目录下运行 npm link
    npm 会在全局 node_modules 目录(如 ~/.nvm/versions/node/v16.14.0/lib/node_modules/)创建一个符号链接,指向 utils 的本地目录(相当于“全局注册”这个包)。

  2. 在依赖项目(如 my-project)目录下运行 npm link utils
    npm 会在 my-project/node_modules 目录下创建一个符号链接,指向全局 node_modulesutils 的链接,最终关联到本地 utils 目录。

这样,项目中 import 'utils' 时,实际加载的是本地 utils 文件夹的代码,修改后即时生效。

三、项目中常见使用案例

1. 开发独立 npm 包时的本地调试(最常用)

场景:你开发了一个 UI 组件库 my-ui,同时需要在 demo-project 中演示组件效果。

  • 传统方式:每次修改 my-ui 后,需 npm publish 发布,再在 demo-projectnpm update my-ui 才能看到效果,效率极低。
  • npm link
    bash
    # 1. 进入 my-ui 目录,全局注册包
    cd path/to/my-ui
    npm link
    
    # 2. 进入 demo-project 目录,链接到本地 my-ui
    cd path/to/demo-project
    npm link my-ui
    之后修改 my-ui 的组件代码,demo-project 刷新后直接看到效果,调试完成后用 npm unlink 解除链接即可。

2. 多项目共享本地私有模块

场景:公司内部有 3 个项目(A/B/C)都依赖同一个内部工具包 common-tools(未发布到公网 npm)。

  • npm link 可让 3 个项目同时关联本地 common-tools 的代码,修改 common-tools 后,3 个项目同步生效,避免重复复制代码或手动替换。

3. 调试项目依赖的深层依赖包

场景:项目 my-app 依赖 libraryA,而 libraryA 又依赖 libraryB,你发现 libraryB 有 bug,需要本地修改调试。

  • 直接在 my-app/node_modules/libraryB 中修改很麻烦(可能被 npm install 覆盖),可用 npm link
    bash
    # 1. 本地克隆 libraryB 源码,修改后注册到全局
    cd path/to/libraryB
    npm link
    
    # 2. 进入 libraryA 目录,链接本地 libraryB
    cd path/to/libraryA
    npm link libraryB
    
    # 3. 进入 my-app 目录,链接本地 libraryA(如果需要)
    cd path/to/my-app
    npm link libraryA
    这样 my-app 会间接使用你修改后的 libraryB 代码,方便调试深层依赖。

四、面试回答框架

  1. 定义npm link 是 npm 用于本地包调试的命令,通过创建符号链接,让项目直接使用本地开发的包代码,避免频繁发布安装。
  2. 核心价值:解决“本地开发的包与依赖项目之间的实时同步”问题,提升调试效率。
  3. 典型场景
    • 开发 npm 包时,在 demo 项目中实时调试;
    • 多项目共享本地私有模块,避免代码冗余;
    • 调试项目的深层依赖包(如 node_modules 里的第三方库)。
  4. 注意事项
    • 调试完成后需用 npm unlink 解除链接,避免影响项目依赖;
    • 全局链接可能导致版本冲突(如本地包与项目 package.json 中声明的版本不一致);
    • 部分构建工具(如 webpack)可能需要配置 resolve.symlinks: true 才能正确处理符号链接。

Nginx怎么解决高并发问题?

在面试中回答“Nginx如何解决高并发问题”时,需要从架构设计核心机制配置优化三个层面展开,既要讲清原理,也要结合实际操作(配置示例),体现对Nginx的深度理解。以下是结构化回答:

一、核心思路:Nginx解决高并发的本质

Nginx能处理高并发(几万甚至几十万并发连接),核心在于其**“事件驱动、异步非阻塞”的架构设计**,从根本上解决了传统服务器(如Apache)“多进程/多线程”模型的性能瓶颈(进程切换开销大、内存占用高)。

二、具体解决手段(原理+操作)

1. 事件驱动与异步非阻塞模型(核心)

原理
Nginx采用“单线程+事件驱动”模式,通过IO多路复用(如Linux的epoll、FreeBSD的kqueue)监听多个连接,无需为每个连接创建进程/线程。当连接有数据可读/可写时,Nginx才会处理,避免了“空等”导致的资源浪费。

举个例子:1万个并发连接中,只有100个有数据交互,Nginx只需处理这100个,其余9900个连接处于“待命”状态,不占用CPU资源。

操作
Nginx默认启用事件驱动模型,可在配置文件(nginx.conf)中指定最优的IO多路复用机制:

nginx
events {
    use epoll;  # 对于Linux系统,优先使用epoll(性能最优)
    worker_connections 10240;  # 每个worker进程最大连接数
}

2. 多进程模型充分利用多核CPU

原理
Nginx启动多个worker进程(通常与CPU核心数一致),每个worker进程独立处理连接,既避免了单线程的CPU瓶颈,又减少了进程间切换的开销(worker进程间无共享内存,通过信号通信)。

操作
配置worker进程数等于CPU核心数(充分利用多核):

nginx
worker_processes auto;  # 自动设置为CPU核心数(推荐)
# 或手动指定(如4核CPU):worker_processes 4;

3. 高效的连接管理(长连接+连接池)

原理

  • 长连接(Keep-Alive):复用TCP连接,避免频繁创建/关闭连接的开销(TCP三次握手/四次挥手耗时)。
  • 连接池:Nginx会维护一个连接池,对后端服务器的连接进行复用,减少后端连接建立的开销。

操作
配置长连接参数(http块中):

nginx
http {
    keepalive_timeout 65;  # 长连接超时时间(客户端)
    keepalive_requests 100;  # 一个长连接最多处理100个请求后关闭
    keepalive_disable msie6;  # 对老旧浏览器禁用长连接

    # 反向代理时,与后端服务器的长连接配置
    upstream backend {
        server 127.0.0.1:8080;
        keepalive 32;  # 与后端保持32个长连接
    }
}

4. 静态资源高效处理(减少后端压力)

原理
Nginx处理静态资源(HTML、CSS、JS、图片)的效率远高于动态服务器(如Tomcat),通过直接读取磁盘文件并返回,无需经过复杂的业务逻辑,可快速响应大量静态请求,减轻后端服务器的并发压力。

操作
配置静态资源缓存与压缩(http块中):

nginx
http {
    # 静态资源缓存(浏览器缓存)
    location ~* \.(jpg|jpeg|png|gif|css|js)$ {
        root /usr/share/nginx/html;  # 静态资源目录
        expires 30d;  # 浏览器缓存30天
        add_header Cache-Control "public, max-age=2592000";
    }

    # 启用gzip压缩(减少传输数据量)
    gzip on;
    gzip_types text/css application/javascript image/png;  # 对指定类型文件压缩
    gzip_comp_level 5;  # 压缩级别(1-9,5为平衡值)
}

5. 反向代理与负载均衡(分散并发压力)

原理
Nginx作为反向代理服务器,可将前端的高并发请求分发到多个后端服务器(如Tomcat集群),避免单台后端服务器过载,通过“分而治之”提高整体并发处理能力。

操作
配置负载均衡(http块中):

nginx
http {
    upstream backend_servers {
        # 轮询策略(默认),可改为ip_hash、weight等
        server 192.168.1.101:8080 weight=3;  # 权重3,处理30%请求
        server 192.168.1.102:8080 weight=2;  # 权重2,处理20%请求
        server 192.168.1.103:8080 backup;  # 备份服务器(主服务器故障时启用)
    }

    # 反向代理到后端集群
    location / {
        proxy_pass http://backend_servers;
        proxy_set_header Host $host;  # 传递原始Host头
        proxy_set_header X-Real-IP $remote_addr;  # 传递客户端真实IP
    }
}

6. 限流与请求过滤(保护系统稳定)

原理
当并发请求超过系统承载上限时,Nginx可通过限流(限制每秒请求数)或过滤(拦截恶意请求)保护后端服务,避免被“冲垮”。

操作
配置限流(http块中):

nginx
http {
    # 定义限流规则(100r/s,burst=20,超出的20个请求排队)
    limit_req_zone $binary_remote_addr zone=my_limit:10m rate=100r/s;

    location /api {
        limit_req zone=my_limit burst=20 nodelay;  # 应用限流规则
        proxy_pass http://backend_servers;
    }
}

三、总结

Nginx解决高并发的核心逻辑是:

  1. 以“事件驱动+异步非阻塞”为基础,用极少的资源处理大量连接;
  2. 通过多进程、长连接、静态资源优化减少开销;
  3. 用负载均衡、限流等手段分散压力、保护系统。

实际应用中,需结合业务场景(如静态资源占比、后端服务器性能)调整配置,才能最大化并发处理能力。

项目当中mixin和utils区别?

在前端开发中,mixin(混入)和 utils(工具函数)都是代码复用的手段,但它们的设计目的、使用场景和实现方式有显著区别。

一、核心定义与本质

  • mixin(混入)
    是一种代码复用模式,主要用于组件/类之间共享“状态、方法、生命周期逻辑”,尤其在 Vue、React(早期)等框架中常见。
    本质是“将一个对象的属性和方法‘混入’到另一个对象中”,复用的是带状态或上下文相关的逻辑(比如组件的 data、methods、钩子函数等)。

  • utils(工具函数)
    通用功能的函数集合,用于封装无状态、纯逻辑的工具方法(与具体业务或组件上下文无关)。
    本质是“独立的功能模块”,复用的是纯函数逻辑(输入输出确定,无副作用)。

二、核心区别对比

维度mixinutils
内容可包含状态(如 data)、方法、生命周期钩子(如 Vue 的 created)等,与组件上下文强相关。仅包含纯函数(如格式化日期、数组去重),无状态,不依赖具体上下文。
用途复用“组件级别的逻辑”(如多个组件都需要的表单验证逻辑、权限判断逻辑)。复用“通用功能逻辑”(如字符串处理、数据转换、工具计算等)。
依赖上下文依赖组件实例(如 this 指向组件),可能使用组件的 props、data 等。不依赖任何上下文,输入参数确定则输出结果唯一(纯函数)。
副作用可能产生副作用(如修改组件状态、调用组件方法)。无副作用(仅处理输入,不修改外部变量)。
命名冲突风险高(混入的方法/数据可能与组件自身的命名冲突)。低(函数独立封装,通过命名空间调用,如 utils.formatDate())。
使用方式在组件中“注入”(如 Vue 中通过 mixins: [myMixin] 引入,混入的内容会合并到组件中)。直接导入调用(如 import { formatDate } from './utils',然后 formatDate(...))。

三、举例说明

1. mixin 示例(Vue 中)

假设有多个组件都需要“登录状态判断”的逻辑:

javascript
// 登录状态判断的 mixin
const loginMixin = {
  data() {
    return {
      isLogin: false
    };
  },
  created() {
    this.checkLogin(); // 调用混入的方法
  },
  methods: {
    checkLogin() {
      this.isLogin = localStorage.getItem('token') ? true : false;
    }
  }
};

// 组件中使用
export default {
  mixins: [loginMixin], // 混入后,组件会拥有 isLogin、checkLogin 等
  created() {
    console.log(this.isLogin); // 可直接使用混入的状态
  }
};

这里的 mixin 依赖组件的 this 上下文,包含状态(isLogin)和与组件生命周期相关的逻辑(created 钩子)。

2. utils 示例

假设有多个地方需要“格式化日期”的功能:

javascript
// utils/date.js
export const formatDate = (date, format = 'YYYY-MM-DD') => {
  // 纯函数逻辑:仅根据输入的 date 和 format 处理,无状态
  const year = date.getFullYear();
  const month = String(date.getMonth() + 1).padStart(2, '0');
  const day = String(date.getDate()).padStart(2, '0');
  return format.replace('YYYY', year).replace('MM', month).replace('DD', day);
};

// 使用时直接导入调用
import { formatDate } from './utils/date';
console.log(formatDate(new Date())); // 2024-05-20

这里的 formatDate 是纯函数,不依赖任何组件或上下文,输入输出完全由参数决定。

四、如何选择?

  • 用 mixin:当需要复用的逻辑与组件状态、生命周期强相关(比如组件的共同数据、钩子函数逻辑)。
  • 用 utils:当需要复用的逻辑是通用工具功能(比如数据处理、格式转换),且不依赖具体组件上下文。

补充:mixin 的局限性与替代方案

mixin 虽然能复用逻辑,但存在命名冲突、逻辑来源不清晰(多个 mixin 叠加后难以追溯)等问题。现代框架中,逐渐被更清晰的方案替代:

  • Vue 3 推荐用 Composition API(组合式 API) 替代 mixin;
  • React 推荐用 自定义 Hooks 替代 mixin。

而 utils 由于其“纯函数、无副作用”的特性,在任何场景下都是稳定且推荐的复用方式。