全栈工程师面试指导
全栈面试要确认你能端到端地负责一个功能——UI、API、数据和部署——并能想清楚复杂度该放在哪里。考察会覆盖整个技术栈的广度,外加至少一个领域的深度。
面试官重点考察什么
前端功底
组件状态、渲染,以及响应式、无障碍的 UI。
后端与 API
接口设计、鉴权、校验与数据建模。
端到端思维
逻辑、缓存放在哪里,以及客户端/服务端的边界。
数据库
表结构设计、查询与基本的性能调优。
交付
测试、CI/CD,以及借助开关安全上线。
全栈工程师常见面试题示例
- 技术面你如何判断哪些逻辑该放在客户端、哪些该放在服务端?好回答应覆盖
- 安全性与数据敏感性
- 用户体验与交互即时性
- 计算资源与带宽开销
- 可维护性与部署策略
- 离线能力与渐进增强
查看范例答案
判断逻辑放在客户端还是服务端需要综合考虑多个因素。首先,安全性是首要考量:涉及敏感数据(如密码、金融信息)或无法被客户端篡改的规则必须放在服务端。其次,用户体验方面,需要即时响应的操作(如表单验证、动画交互)适合客户端,而依赖后端数据的操作(如搜索、支付)则需服务端。再者,计算密集型任务若在客户端执行会消耗用户设备资源,通常放在服务端;但简单计算可放在客户端以减少网络延迟。带宽开销也很关键:大量数据传输时,服务端聚合数据可节省带宽。此外,可维护性方面,业务逻辑集中便于更新,但客户端逻辑可减少服务端负载。最后,考虑离线能力:如果应用需要离线工作,部分逻辑必须放在客户端。常见陷阱是过度将后端逻辑搬到客户端导致安全漏洞,或过度集中导致性能瓶颈。
- 技术面端到端讲讲鉴权和会话是怎么工作的。好回答应覆盖
- JWT令牌机制
- Session存储与Cookie
- OAuth2.0授权流程
- 刷新令牌与过期处理
- CSRF与XSS防护
查看范例答案
鉴权和会话通常通过令牌或会话ID实现。典型流程:用户提交凭证(用户名密码),服务端验证后签发一个JWT令牌,包含用户标识和过期时间,签名后返回给客户端。客户端将令牌存储在localStorage或Cookie中,后续每个请求都携带该令牌(通常在Authorization头)。服务端验证签名和过期时间,识别用户身份。对于会话,服务端创建session对象,并返回sessionID给客户端(通常通过Cookie),每次请求携带该ID,服务端查找session数据。JWT无状态,适合分布式系统;但令牌一旦泄露无法撤销。而session需要同步或集中存储。安全方面,必须使用HTTPS,防止令牌被截获;设置HttpOnly和Secure属性防止XSS;使用CSRF Token防止跨站请求伪造。常见做法是短期访问令牌(如15分钟)配合长期刷新令牌。刷新令牌存储在服务端,用于获取新访问令牌,降低泄露风险。
- 技术面一个依赖慢 API 的慢页面,你会怎么优化?好回答应覆盖
- 客户端缓存策略(Service Worker、LocalStorage)
- 服务端缓存(Redis、CDN)
- 异步加载与骨架屏
- 预取与懒加载
- API聚合与批量请求
查看范例答案
优化依赖慢API的页面,首先分析瓶颈。如果API响应慢,可考虑客户端缓存:把不常变化的数据用localStorage或IndexedDB缓存,并设置过期策略;或者使用Service Worker实现离线缓存和请求拦截。服务端缓存也很关键:用Redis缓存数据库查询结果,或用CDN缓存静态资源和API响应(如使用Cache-Control头)。其次,从用户体验入手:用骨架屏替代loading动画,页面先渲染框架,数据到达后填充;对非关键数据使用异步加载(如React.lazy)。再者,预取技术:在空闲时间提前请求用户可能需要的资源(使用link rel=prefetch)。另外,API聚合:将多个小请求合并为一个批请求,减少网络往返。最后,考虑使用Web Worker来并行处理数据,减轻主线程压力。避免将所有依赖都串行化,应尽量并行加载。一个常见错误是忽略缓存失效策略,导致显示陈旧数据。
- 编程实现一个小型 CRUD 功能:表结构、API 接口和一个 UI 表单。好回答应覆盖
- RESTful API设计
- Express路由与中间件
- SQL数据库操作(CRUD)
- 前端表单与事件处理
- 错误处理与数据验证
查看范例答案
以下实现一个简单的用户CRUD功能,使用Node.js + Express作为后端,SQLite作为数据库,前端用原生HTML和JavaScript。表结构包含id、name、email字段。API设计遵循RESTful风格:GET /users获取所有用户,POST /users创建用户,PUT /users/:id更新用户,DELETE /users/:id删除用户。前端表单包含输入字段和提交按钮,通过Fetch API发送请求并更新列表。后端进行输入验证和错误处理。代码实现了完整的功能,包括数据库初始化、路由处理、前端交互。时间空间复杂度:所有操作都是O(1)(根据主键查询)或O(n)(列表查询)。
参考代码javascript // server.js (Node.js + Express + SQLite) const express = require('express'); const sqlite3 = require('sqlite3').verbose(); const bodyParser = require('body-parser'); const app = express(); const PORT = 3000; // 数据库初始化 const db = new sqlite3.Database(':memory:'); db.run(`CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT NOT NULL UNIQUE )`); app.use(bodyParser.json()); app.use(express.static('public')); // 静态文件服务 // 获取所有用户 app.get('/users', (req, res) => { db.all('SELECT * FROM users', (err, rows) => { if (err) return res.status(500).json({ error: err.message }); res.json(rows); }); }); // 创建用户 app.post('/users', (req, res) => { const { name, email } = req.body; if (!name || !email) return res.status(400).json({ error: 'Name and email required' }); db.run('INSERT INTO users (name, email) VALUES (?, ?)', [name, email], function(err) { if (err) return res.status(400).json({ error: err.message }); res.status(201).json({ id: this.lastID, name, email }); }); }); // 更新用户 app.put('/users/:id', (req, res) => { const { name, email } = req.body; const { id } = req.params; if (!name || !email) return res.status(400).json({ error: 'Name and email required' }); db.run('UPDATE users SET name = ?, email = ? WHERE id = ?', [name, email, id], function(err) { if (err) return res.status(400).json({ error: err.message }); if (this.changes === 0) return res.status(404).json({ error: 'User not found' }); res.json({ id, name, email }); }); }); // 删除用户 app.delete('/users/:id', (req, res) => { const { id } = req.params; db.run('DELETE FROM users WHERE id = ?', id, function(err) { if (err) return res.status(500).json({ error: err.message }); if (this.changes === 0) return res.status(404).json({ error: 'User not found' }); res.status(204).send(); }); }); app.listen(PORT, () => console.log(`Server running on port ${PORT}`)); // public/index.html (前端UI) // 完整的HTML文件请参见实际部署,这里给出核心部分: // <form id="userForm"> // <input id="name" placeholder="Name" required/> // <input id="email" placeholder="Email" required/> // <button type="submit">Save</button> // </form> // <ul id="userList"></ul> // <script> // const api = '/users'; // async function loadUsers() { // const res = await fetch(api); // const users = await res.json(); // document.getElementById('userList').innerHTML = users.map(u => `<li>${u.name} - ${u.email} <button onclick="del(${u.id})">Delete</button></li>`).join(''); // } // document.getElementById('userForm').addEventListener('submit', async (e) => { // e.preventDefault(); // const name = document.getElementById('name').value; // const email = document.getElementById('email').value; // const res = await fetch(api, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({name, email}) }); // if (res.ok) { loadUsers(); e.target.reset(); } // }); // async function del(id) { // await fetch(`${api}/${id}`, { method: 'DELETE' }); // loadUsers(); // } // loadUsers(); // </script> // 时间空间复杂度:所有操作均为O(1)(主键查询)或O(n)(全表扫描)。 - 编程实现乐观 UI 更新,并在失败时回滚。好回答应覆盖
- 乐观更新与回滚机制
- 状态管理(useState)
- 异步请求与错误处理
- UI即时反馈
- 复杂度分析
查看范例答案
乐观UI更新先假设请求成功,立即更新界面,提升用户体验;若请求失败,则回滚到之前状态。以下用React实现一个点赞功能:点击按钮时,计数立即增加并改变样式,同时发送PATCH请求。如果请求失败,通过保存的状态快照恢复计数和样式。实现中,使用useState保存状态,在请求开始前记录旧状态,请求失败时setState回旧值。这种模式适用于非关键操作(如点赞、收藏),但谨慎用于财务等操作。时间空间复杂度:UI更新O(1),请求O(1)(网络延迟)。
参考代码jsx import React, { useState } from 'react'; import axios from 'axios'; function LikeButton({ postId }) { const [liked, setLiked] = useState(false); const [count, setCount] = useState(0); const handleClick = async () => { // 保存旧状态用于回滚 const prevLiked = liked; const prevCount = count; // 乐观更新:立即切换状态 setLiked(!liked); setCount(count + (liked ? -1 : 1)); try { await axios.patch(`/api/posts/${postId}/like`, { liked: !liked }); } catch (error) { // 请求失败,回滚状态 setLiked(prevLiked); setCount(prevCount); console.error('Like failed, rollback', error); } }; return ( <button onClick={handleClick} className={liked ? 'liked' : ''}> {liked ? '❤️' : '♡'} {count} </button> ); } // 时间复杂度:UI更新 O(1),网络请求 O(1)(但取决于网络延迟) // 空间复杂度:O(1)(仅保存基本类型状态) - 系统设计设计一个带实时更新和审核的评论系统。好回答应覆盖
- 实时通信(WebSocket)
- 评论审核流程(队列、状态机)
- 数据库设计(评论表、审核表)
- 扩展性:读写分离、缓存
- 对抗攻击与限流
查看范例答案
设计评论系统需要考虑实时更新和审核。核心组件包括:WebSocket服务器用于推送新评论到所有在线用户;审核队列(如RabbitMQ)存储待审核评论;数据库设计:评论表(id, postId, userId, content, status, createdAt),审核表(commentId, moderatorId, result, time)。数据流:用户提交评论→服务端写入DB且状态为pending→推送到审核队列→审核员通过/拒绝→更新评论状态→通过WebSocket推送给用户。实时更新:客户端订阅特定postId的WebSocket频道,当评论状态变化或新评论通过审核时,服务端推送数据。扩展性:WebSocket服务器集群需使用redis发布/订阅同步;数据库读写分离,评论写入主库,读取从库;使用缓存(如Redis)存储热门帖子的评论列表。安全方面,防止垃圾评论:限制频率(每个用户每分钟最多10条),使用验证码,敏感词过滤。瓶颈通常在WebSocket连接数(可用长轮询降级)和审核队列处理能力(增加审核员或自动审核规则)。避免直接暴露数据库写入操作,所有评论必须经过审核。
- 行为面讲一个完全由你独立交付的功能。好回答应覆盖
- 独立需求分析与设计
- 全栈实现:前端、后端、数据库
- 测试与调试
- 上线与监控
- 结果与反思
查看范例答案
我独立交付过一个用户行为分析功能。首先,与产品经理沟通明确需求:追踪用户点击事件并生成报表。我设计了数据模型(事件表:userId, eventType, pageUrl, timestamp),后端使用Node.js + Redis缓存实时数据,定时写入PostgreSQL。前端使用React嵌入SDK,通过Beacon API批量发送事件。我编写了完整的RESTful接口用于查询报表,并实现了权限控制。测试阶段,我使用Jest编写单元测试,并手动模拟高并发场景。上线后,我设置了告警规则(如延迟超过500ms)。这个功能上线后,帮助产品团队优化了用户流程,提高了转化率。挑战是处理海量事件写入,我采用了缓冲队列和批量插入优化。独立交付让我掌握了从需求到上线的全流程,强化了全栈能力。
- 行为面你如何决定在技术栈的哪一部分深入钻研?好回答应覆盖
- 项目需求驱动
- 个人兴趣与职业规划
- 技术深度与广度平衡
- 社区活跃度与学习资源
- 实践与反馈循环
查看范例答案
决定在技术栈的哪一部分深入钻研,我主要考虑以下因素:首先,当前项目需求——优先深度学习项目中频繁使用但尚有不足的技术,如性能瓶颈相关的数据库优化或前端框架。其次,个人兴趣和职业目标——若我对图形学感兴趣,会深入WebGL或Canvas;若想成为架构师,会钻研分布式系统。第三,技术深度与广度平衡:我会先广度了解,再选择1-2个领域深度挖掘(如前端性能优化或后端中间件)。第四,社区活跃度和学习资源:选择文档完善、社区活跃的技术(如React、Kubernetes),方便解决问题。最后,实践反馈:通过记录学习日志、参与开源项目,验证深度是否带来实际效率提升或解决痛点。避免盲目追逐热点,而是基于自身场景和长期价值做选择。常见错误是蜻蜓点水,什么都学但都不精。
不同级别的考察差异
如何准备
- 选一个深度领域并清晰地表达出来——能在某处钻深的通才更突出。
- 把完整的请求生命周期讲一遍,展示你的端到端理解。
- 别忽视测试和部署;全栈面试考的是交付,而不只是写代码。
常见问题
全栈面试比专精岗位更难吗?
是更广,但不一定更难——你用一些深度换来了端到端的覆盖,但仍需在至少一层具备真正的深度。
全栈面试我该重点准备什么?
能够跨表结构、API 和 UI 搭起一个小功能,并清楚地解释客户端/服务端的取舍。
全栈岗位会被问系统设计吗?
会,通常是务实的产品功能设计,同时涉及前端和后端,而不是纯基础设施。
用即时 AI 反馈刷全栈工程师面试题
Offersly 会根据你的简历和目标岗位定制一场模拟面试,并从相关性、深度、清晰度和正确性四个维度为每个回答打分。