0 到 1 的难点是判断,不是编码
搭项目真正花时间的不是写脚手架,而是决定哪些事现在就要定死。这份把每个决策的取舍和代价摊开:目录怎么分、请求层怎么封、Node 怎么分层、什么时候该拆。
01按可逆性排序不可逆的先定,可逆的能拖就拖。顺序反了全是返工。
02每个决策讲代价说"选了 A 所以放弃了 B 的什么",比说 A 好在哪更可信。
03准备一个真实故障能讲清怎么定位、怎么根治,比讲十个功能有说服力。
1
0 到 1 该先决定什么★
先定不可逆的,再定能改的
搭建项目最容易犯的错是从细节开始:先纠结用哪个 UI 库、哪个状态管理,结果目录结构和技术栈定死了才发现改不动。正确顺序是先分拣决策的可逆性——不可逆的先定,可逆的能拖就拖。
◐ 可交互:这八件事,哪些必须在第一周定下来
八件事逐个判断:先定 = 改起来要动全局;后定 = 随时能换。
几乎不可逆(先定)
- 前后端分离还是同构渲染
- 单体还是多服务(影响部署与协作方式)
- 主键类型(自增 / UUID / 雪花)
- 多租户模型(要不要数据隔离)
随时能改(后定)
- 状态管理库、UI 组件库
- CSS 方案(CSS Modules / Tailwind)
- 目录里某个模块的拆分方式
- 测试框架、lint 规则细节
业务边界要解决谁的什么问题,先划清楚不做什么
→
不可逆技术决策渲染模式、数据模型、部署形态
→
工程骨架目录、规范、CI、环境
→
公共能力请求层、鉴权、错误处理、监控
→
业务迭代能力补齐,按需要引入第三层抽象
面试怎么答我会先分拣决策的可逆性。渲染模式、数据模型、主键类型、多租户方式这些改起来代价极大,必须在第一周定死;状态管理、UI 库、CSS 方案这些随时能换,可以先选个顺手的跑起来。顺序反了,后面全是返工。
2
技术选型看什么★
不是选最好的,是选团队能维护三年的
选型的判断标准有四个:团队熟悉度、生态是否够用、出问题能不能自己解决、招人难度。技术先进性最多排第五。面试里说"因为团队用 Vue 所以选 Vue",比说"因为 Vue 性能好"更可信。
一份可执行的评估表维度 要问的问题 权重
团队熟悉度 现有人多久能上手 高
生态完备度 缺功能时有没有现成库 高
可调试性 出问题能否定位到框架内部 中
长期维护 最近一年提交频率、issue 响应 中
迁移成本 以后想换掉,代价多大 中
性能 在目标场景下是否够用 看场景
新技术热度 是否愿意承担踩坑成本 低
- "团队不熟"是最贵的成本:一个再先进的技术,如果只有一个人懂,那个人请假项目就停摆。这是选型的第一约束。
- 不要为了 10% 的性能付 3 倍复杂度:很多选型争论最后都落在"理论上更快",但实际瓶颈在接口和图片。
- 选型要写"不选它的理由":能说出为什么排除其他方案,才说明真的评估过,而不是跟风。
面试怎么答我选型看四件事:团队多久能上手、生态够不够用、出问题能不能自己定位、以后想换掉代价多大。技术先进性排在后面。因为选型的真实成本是维护成本,不是开发成本。能说出排除其他方案的理由,比说出选中方案的优势更有说服力。
3
目录结构:按什么分
分层与分feature,选一个贯彻到底
两种主流分法:按类型(components / hooks / utils)和按功能(features/order、features/user)。项目小时按类型清晰,模块一多就乱。判断标准是改一个需求要动几个目录。
按功能分(推荐给中大型项目)src/
├── app/ 应用级:入口、路由、全局 provider
├── features/
│ ├── order/
│ │ ├── api.ts ← 这一块的数据请求
│ │ ├── model.ts ← 状态与业务逻辑
│ │ ├── components/ ← 只属于订单的组件
│ │ └── index.ts ← 对外只暴露这个
│ └── user/
├── shared/
│ ├── ui/ 跨模块复用的组件
│ ├── lib/ 无业务依赖的工具
│ └── config/
└── pages/ 路由层,只做组合,不写业务
- 判断标准很实际:一个需求"加一个订单状态筛选",如果需要同时改 components、hooks、api、types 四个目录,就说明该按功能分了。
- features 之间不要互相 import:需要共享就下沉到 shared,或者通过 app 层组合。这条约束能防止模块慢慢长成一团。
- pages 只做组合:页面里塞业务逻辑,是项目中期最大的债务来源——同一个逻辑会在多个页面重复三遍。
面试怎么答我倾向按功能分:features 下每个模块自带 api、model、components,对外只通过 index 暴露。跨模块共享的放 shared。判断标准是改一个需求要动几个目录,如果散落在四处就说明分法不对。pages 层只做组合,不写业务逻辑。
4
工程规范:让机器管,不靠人
lint、提交、分支三件事
规范的价值是减少无意义的争论和返工,所以能自动化的绝不留给人。三件事:代码风格用 lint + 格式化工具,提交信息用 commitlint 约束,分支模型按发布节奏选。
三层防线编辑器 保存时自动格式化(Prettier / EditorConfig)
开发者根本不用想风格问题
提交前 lint-staged + husky:只检查改动的文件
快,不会因为"全量检查太慢"而被绕过
CI typecheck + lint + test,失败就阻断合并
这是唯一不依赖自觉的一层
- husky 里别跑全量 lint:大项目跑一次几十秒,开发者会习惯性
--no-verify绕过,规范就失效了。只检查暂存区的文件。 - 提交信息要有格式:
feat:、fix:、refactor:,它的真正价值是能自动生成 changelog 和决定版本号,不只是好看。 - 分支模型看发布节奏:一天发多次用 trunk-based(短分支 + 特性开关);有固定版本窗口才需要 develop / release 那一套。
面试怎么答我一般设三层:编辑器保存时自动格式化,提交前 lint-staged 只检查改动的文件,CI 上跑 typecheck、lint、test 作为唯一强制的关卡。关键是提交前那层要快,慢了一定会被人用 no-verify 绕过。提交信息用 commitlint 约束,是为了能自动生成 changelog 和版本号。
5
CI/CD 与发布策略★
流水线的价值是可回滚
流水线不是"自动部署"这么简单,它要解决三件事:产物可复现、发布可灰度、出问题能回滚。只做到第一件,出事时还是手忙脚乱。
install锁文件 + 缓存,保证依赖一致
→
checktypecheck / lint / test,失败即停
→
build产物带上 commit sha,可追溯
→
publish上传到静态托管或镜像仓库
→
deploy灰度:1% -> 10% -> 全量
→
verify看错误率和核心指标,异常自动回滚
几种发布策略的取舍全量发布 简单,出事影响全部用户
灰度/金丝雀 按比例放量,观察指标再扩大 前端用 CDN + 分流,后端用网关权重
蓝绿部署 两套环境切换,回滚最快 成本翻倍
特性开关 代码先上,功能后开 能解耦"部署"和"上线",最灵活
- 前端回滚要能秒级:静态资源带 hash,把 index.html 指回上一个版本就行。前提是旧版本产物别被清掉,否则回滚就变成重新构建。
- 数据库变更最难回滚:加字段可以,删字段和改类型要先做兼容(双写、双读),分两次发布。这是"不可逆决策"的典型。
- 特性开关比灰度更重要:它把"部署"和"上线"拆开,代码可以先合进去,功能由开关控制,出问题关开关不用回滚代码。
面试怎么答流水线要保证三件事:产物可复现、发布可灰度、回滚够快。前端回滚靠静态资源带 hash 加指回旧版本,所以旧产物要保留;后端用网关权重做灰度;数据库变更要分两次发布,先兼容再删。特性开关能把部署和上线解耦,比单纯灰度更灵活。
6
环境与配置
密钥永远不进代码库
三个阶段的环境(dev / staging / prod)必须配置分离、代码同一份。密钥只放在部署平台的变量里,绝不进仓库。前端尤其要注意:打进产物的任何东西都是公开的。
常见错误
- 密钥写在前端 .env 里,被打进 bundle
- 用一个 .env 跑所有环境,靠注释切换
- 把测试库地址提交到仓库
- 配置项散落在十几个文件里,没人说得清默认值
推荐做法
- 构建时注入,前端只放非敏感的配置
- 敏感密钥放在服务端或部署平台的 secrets
- 每个环境一份配置文件,默认值显式写在代码里
- 配置集中校验:启动时缺关键项直接失败
面试怎么答配置按环境分离,代码只有一份,环境差异通过部署时注入的变量解决。密钥只放在部署平台的 secrets 里,前端环境变量里只能放公开的东西,因为它会被打进 bundle。启动时校验必需配置,缺了就立刻失败,比运行到一半报错好定位。
7
前端三件套:路由、权限、状态
三个骨架决定后面好不好改
前端的骨架就是这三样。路由决定页面怎么组织,权限决定谁能看到什么,状态决定数据放哪。三者的常见坑分别是:路由没做懒加载、权限只在前端做、状态全局化过度。
状态放在哪:从近到远组件内 state 只影响自己,别往上抬
URL 能被分享、刷新保留、前进后退 ← 最容易被忽略
服务端缓存 React Query / SWR,接口数据别塞 store
全局 store 跨页面共享的少量状态(用户、主题)
本地存储 需要跨会话保留的(token、草稿)
- 前端权限只是体验:隐藏菜单和按钮,用户改一下 localStorage 就能绕过去。真正的校验必须在接口层,前端权限的作用是"不让用户点到不该点的东西"。
- 能放 URL 的状态别放 store:筛选条件、分页、Tab 放 query 里,刷新不丢、能分享、能后退。这条能省掉大量"刷新后状态丢了"的 bug。
- 接口数据别塞全局 store:用请求库自带缓存(React Query / SWR),否则要自己写 loading、错误、重试、失效逻辑,重复且容易不一致。
面试怎么答前端骨架三件事:路由按模块懒加载,权限做两级——前端守卫负责体验、接口层负责安全,状态按"离使用者多近"来放,能放 URL 的不放 store,接口数据用请求库缓存而不是塞全局。这几个决定定错了,后期改动成本会很高。
8
请求层设计★
一次封装,省掉后面几百次重复
请求层是前端最值得早点做好的基建。要做四件事:统一错误码处理、自动带鉴权、超时与重试、取消重复请求。做晚了,业务代码里会散落几百个 try/catch。
- 错误要分两层:HTTP 层(4xx / 5xx)和业务层(code 非 0)。混在一起处理,前端就没法区分"网络挂了"和"参数错了"。
- 4xx 不要重试:参数错了重试一百次也一样。只有 5xx 和网络超时值得重试。
- 重复请求要合并:同一个接口在首屏被三个组件同时请求很常见,用一个 Map 存未完成的 Promise 就能省掉多余请求。
面试怎么答请求层要统一四件事:错误分 HTTP 层和业务层两级处理,401 统一跳登录,超时用 AbortController 真取消,同一接口的重复请求用 Map 复用未完成的 Promise。重试只针对 5xx 和网络错误,4xx 不重试。
9
监控与埋点
上线之后才知道有没有问题
监控分三条线:错误(白屏、JS 异常)、性能(LCP / INP / CLS)、业务(转化、点击)。三者的共同要求是能定位到具体版本和用户,否则收到告警也不知道从哪查。
- 用 sendBeacon 而不是 fetch:页面关闭时普通请求会被取消,beacon 由浏览器托管,能保证发出。
- 必须带版本号:不发版不知道新版本有没有引入新错误,这是最实用的一条。
- 要有采样和去重:一个死循环报错能刷爆日志,按错误指纹聚合 + 采样上报,否则监控成本比收益高。
面试怎么答监控分错误、性能、业务三条线。错误要覆盖 JS 异常、未处理的 Promise 拒绝、资源加载失败,最后一种只能在捕获阶段监听。上报用 sendBeacon 保证页面关闭时也能发出,并且必须带版本号和用户标识,否则收到告警无法定位。另外要做指纹聚合和采样,避免一个错误刷爆日志。
10
Node 项目 0 到 1:分层★
依赖方向只能单向
Node 服务最容易腐化的地方是依赖方向乱掉:controller 里直接写 SQL,service 里调 req 和 res。定死一条规则——依赖只能向内(路由 → 控制器 → 服务 → 仓储),业务逻辑就能脱离框架测试。
分层与职责routes/ 只做路径映射,不写逻辑
controllers/ 解析入参、调 service、组装响应 能拿到 req/res
services/ 业务逻辑,纯 TS/JS,不依赖框架 可单测、可复用
repositories/ 数据访问,SQL / ORM 都在这 换数据库只改这层
models/ 实体与 DTO 定义
infra/ db、redis、logger、http client
面试怎么答Node 项目分层是路由、控制器、服务、仓储。关键是依赖单向:控制器能拿 req/res,服务层只接收普通参数、不碰框架对象,仓储层封装所有数据访问。这样业务逻辑可以脱离 HTTP 单测,换数据库也只改仓储层。分层的意义是让可测的代码占大头。
11
Node:日志与可观测性
出问题时你只有日志
服务端的日志不是
console.log。要求是结构化(JSON,能查)、带上下文(requestId 串起一次请求)、分级(info / warn / error 分开存)。这三点决定故障时你花 5 分钟还是 5 小时。- requestId 是最值钱的一条:没有它,一个用户的报错和十个人的正常日志混在一起,根本没法还原现场。前端也应该把它带回来,用户报障时能直接查。
- 不要在循环里打日志:批量任务里每条都打,日志量会爆。打开始、结束、汇总和异常。
- 错误日志要带堆栈和上下文:光有 message 不够,业务参数、用户 id 都要带上,否则只能靠猜。
面试怎么答日志要做到三点:结构化输出成 JSON 便于检索,每个请求带 requestId 贯穿整条链路,按级别分流。还要做敏感字段脱敏,别把 token 和密码写进日志。requestId 是最关键的一条,没有它故障时根本无法把一次请求的所有日志串起来。
12
Node:错误处理与优雅退出★
进程级错误只有两条路
uncaughtException 之后进程状态已经不可信,只能记日志然后退出,不能继续服务。优雅退出的关键是:停止接新请求 → 等在途请求完成 → 关闭连接池 → 退出,并且要有超时兜底。- 为什么 uncaughtException 必须退出:抛错时可能有半完成的事务、被破坏的全局状态,继续服务会产生更难排查的数据问题。容器编排会重启一个新进程,那才是干净的。
- SIGTERM 是容器停止的信号:K8s 删除 pod 先发 SIGTERM,等宽限期再 SIGKILL。不处理 SIGTERM 就会直接被杀,在途请求全部中断。
- 超时兜底不能少:有连接卡住时 server.close 的回调永远不触发,进程就僵在那里,滚动发布会被拖住。
面试怎么答uncaughtException 之后进程状态不可信,只能记日志然后退出,让容器重启一个干净进程,不能继续服务。优雅退出是收到 SIGTERM 后停止接收新连接,等在途请求处理完,关闭数据库和 redis 连接,最后退出,同时要有超时兜底防止卡住。
13
Node:数据库与事务
连接池、N+1、事务边界
数据库这块有三个必答点:连接池大小(不是越大越好)、N+1 查询(循环里查库)、事务边界(业务逻辑不要在事务里等外部接口)。三个都会在生产上真实咬人。
连接池大小的经验值太小 请求排队,吞吐上不去
太大 数据库连接被打满,上下文切换开销剧增
经验值 CPU 核数 * 2 + 磁盘数,通常 10 到 20 就够
关键 一个进程一个池;多进程部署要乘以进程数
面试怎么答数据库这块要注意三点:连接池不是越大越好,经验值是核数乘二加磁盘数,多进程部署还要乘以进程数;循环里查库会造成 N+1,要改成批量查询或 join;事务边界要短,只包数据库操作,不要在事务里调外部接口,还要保证加锁顺序一致避免死锁。
14
Node:鉴权、限流与幂等
三个和生产事故强相关的点
鉴权决定谁能调,限流决定能调多快,幂等决定重复调用会不会出问题。第三个最容易被忽略,而它恰恰是支付、下单这类场景的必答题。
限流的几种算法固定窗口 简单,但窗口边界会放过 2 倍流量
滑动窗口 解决边界问题,要存更多状态
令牌桶 允许一定突发,最常用
漏桶 输出恒定,削峰但不能突发
并发限流 限制同时处理的请求数,保护下游
面试怎么答鉴权用 JWT 或 session,注意 token 过期和刷新;限流一般用令牌桶,允许一定突发又能限制平均速率,用 Redis 做分布式限流;幂等靠幂等键,用 Redis 的 SET NX 抢占,重复请求直接返回上次结果而不是报错,处理失败要释放键允许重试。
15
Node:部署与健康检查
能不能上线,看这两个接口
部署形态选 PM2(单机多进程)还是 Docker + K8s(弹性伸缩),取决于规模。两个健康检查接口是硬要求:liveness 判断进程是否活着,readiness 判断能不能接流量。混用会导致雪崩。
- liveness 里不要查数据库:数据库短暂不可用时,所有实例 liveness 失败被重启,重启后数据库还没好,形成雪崩。这是很经典的错误。
- readiness 失败只摘流量不重启:等依赖恢复后自动加回来,服务能自愈。
- 多进程要看内存占用:Node 单进程堆默认有上限(约 1.5~2GB),大内存机器上要按核数开多进程,否则一个进程吃满堆就崩。
面试怎么答单机小规模用 PM2 开多进程,规模上来用 Docker 加 K8s 做弹性伸缩。健康检查要分两个:liveness 只判断进程活着,不查依赖,否则数据库抖一下所有实例都会被重启;readiness 查关键依赖,失败只从负载均衡摘掉不重启,恢复后自动加回。
16
演进:什么时候该拆★
过度设计比不做设计更贵
拆服务和上微前端都有明确的前提条件,没有就上,等于用复杂度换想象中收益。单体不是落后,能在单体里保持模块边界,比拆成一堆互相调用的服务更难也更有价值。
该拆的信号
- 不同模块的发布节奏差异极大,互相拖慢
- 单个模块的流量是其他的十倍,必须单独扩容
- 团队规模超出单仓库协作的承受范围
- 有明确的技术异构需求(不同语言/运行时)
不该拆的信号
- 只是"看起来更高级"
- 团队不到十人,沟通成本本就很低
- 拆完还需要跨服务事务,等于把简单问题做复杂
- 没有配套的监控、链路追踪、自动化部署
- 先模块化,再服务化:代码层面把边界划清楚(按 feature 分目录、禁止跨模块直接 import),需要拆的时候才是"搬目录",而不是"重写"。
- 微前端的前提是组织问题:多个团队要独立发布同一个应用才需要它。单个团队用微前端,只会多一层运行时和一堆通信问题。
- 拆分后必须有链路追踪:没有 traceId 贯穿,一个请求跨五个服务出问题,排查成本会翻几倍。
面试怎么答拆服务的前提是发布节奏差异大、流量差异大、或者团队规模大到单仓库协作吃力,并且已经有监控和自动化部署。只是觉得"更高级"就拆是把简单问题做复杂。我的做法是先做代码层面的模块化,边界划清楚、禁止跨模块直接引用,等真有需要时只是把目录搬出去,而不是重写。
17
面试怎么讲一个你搭的项目★
结构比细节重要
讲项目要有结构:背景与约束 → 你的决策 → 遇到的真实问题 → 结果与量化。最忌讳从头讲功能列表,那是产品文档,不是技术回答。面试官想听的是你为什么这么选。
一句话背景什么业务、多大规模、几个人、多长时间
→
两三个关键决策选型时排除了什么,为什么
→
一个真实问题出过什么故障,怎么定位,怎么根治
→
结果性能、稳定性、交付效率上的具体变化
把"我做了"换成"我判断"✗ 我搭了项目的脚手架,配了 webpack 和 CI
✓ 项目要支持三个团队并行开发,所以我按 feature 划分模块并禁止跨模块
直接引用,代价是初期目录看起来更碎,但半年后加模块不用改公共代码
✗ 我用了 Redis 做缓存
✓ 首页接口有大量重复查询,加缓存后 QPS 从 800 提到 4000,
同时用预热和随机过期时间避免缓存雪崩
面试怎么答我会先说一句话背景——什么业务、多大体量、几个人、做了多久,然后重点讲两三个关键决策和排除其他方案的理由,再讲一个真实故障怎么定位和根治,最后给量化结果。功能列表一句带过,因为面试官关心的是判断力,不是需求文档。
18
前端常问的设计模式★
不是背定义,是举得出真实代码
前端面试问设计模式,考的是能不能举出项目里的真实例子。七个高频模式,每个都有一个前端特有的落地场景:单例、工厂、观察者、发布订阅、策略、装饰器、代理。
每个模式在前端里的真实出处单例
axios 实例、Vuex / Pinia 的 store、i18n 实例、全局配置
工厂
图表组件按 type 创建、表单控件按字段类型渲染、
Node 里的 createServer / createApp
观察者(Observer)
Vue 的响应式依赖收集、React 的订阅、
MutationObserver / IntersectionObserver
发布订阅(Pub/Sub)
事件总线 EventBus、跨组件通信、微前端通信总线
与观察者的差别:发布订阅多一个"事件中心"中介
策略(Strategy)
表单校验规则、价格计算、权限判断、
用映射表替代长 switch
装饰器(Decorator)
高阶组件 HOC、中间件、AOP 埋点、
日志和耗时统计的包装
代理(Proxy)
Vue3 响应式、图片懒加载的占位、
缓存代理、接口请求的拦截器
- 观察者和发布订阅的区别:观察者是"目标直接持有订阅者列表"(Vue 的 Dep 里存 watcher);发布订阅是"双方都只认识事件中心"。所以发布订阅解耦更彻底,但也更难追踪谁在监听。这是最常被追问的一题。
- 装饰器的真实价值是"不改原代码加能力":埋点、鉴权、缓存、重试都能这么包。React 的 HOC 和 hooks 的
useXxx都是这个思路的不同形态。 - 策略模式解决的是"分支膨胀":当
if (type === ...)超过三个分支、而且以后还会加,就该换成映射表。新增类型只加一行,不改函数。 - 不要为了模式而模式:一个只有两种情况的判断写成策略模式,反而更难读。面试主动说"这里我用了 if-else,因为只有两个分支且不会增长",比硬套模式更能体现判断力。
- Proxy 的费用:每次属性访问都要经过 trap,高频读写的场景(比如大数组遍历)会有可测的开销。Vue3 只在深层对象被访问时才创建代理(惰性代理),就是为了这个。
面试怎么答前端问设计模式主要看能不能举出真实例子。单例是 axios 实例和 store;工厂是图表组件按 type 创建;观察者是 Vue 的依赖收集;发布订阅是事件总线,它和观察者的区别是双方都只认识事件中心,解耦更彻底但也更难追踪;策略模式是用映射表替代膨胀的 if-else,比如表单校验和价格计算;装饰器是不改原函数加能力,HOC 和中间件都是;代理就是 Vue3 的响应式。反过来也要能说"这里我用 if-else 就够了,因为只有两个分支"。
19
速答清单
十句话收口
010 到 1 该先决定什么?
先定不可逆的:渲染模式、数据模型、主键类型、多租户方式。状态管理和 UI 库随时能换,可以后定。
02技术选型的判断标准?
团队熟悉度、生态完备度、可调试性、迁移成本。技术先进性排后面,因为真实成本是维护成本。
03目录按类型还是按功能分?
中大型项目按功能。判断标准是改一个需求要动几个目录,散落多处就说明该按功能分。
04前端权限做了就安全了吗?
不是。前端权限只负责体验,隐藏入口防止误操作。真正的校验必须在接口层。
05什么状态该放 URL?
筛选条件、分页、Tab 这类需要刷新保留、能分享的状态。能放 URL 的别放 store。
06请求层最值得先做的四件事?
统一错误码分层处理、401 统一跳登录、超时用 AbortController 真取消、重复请求合并。
07uncaughtException 之后能继续服务吗?
不能。进程状态已不可信,只能记日志然后退出,让容器重启一个干净进程。
08liveness 为什么要查依赖?
恰恰相反,liveness 不该查数据库。查了会导致数据库抖动时所有实例被重启,形成雪崩。
09幂等怎么实现?
用幂等键加 Redis 的 SET NX 抢占,重复请求返回上次结果而不是报错,处理失败要释放键允许重试。
10什么时候该拆服务?
发布节奏差异大、流量差异大、团队规模超出单仓库协作能力,且已有监控和自动化部署。否则先做代码层模块化。