这篇不是"面试题答案合集",而是一次高级前端面试复盘。
如果只把这些问题背下来,下一场面试换个问法还是会慌;但如果能看懂它们背后的能力模型,你会发现:CSS、请求、Nginx、Docker、Linux、安全、产品判断、算法题,其实都在问同一件事:你是不是已经从"能写页面的人",成长为"能把前端系统负责到底的人"。
0. 先说结论:五年前端高级岗到底在考什么?
这次问题看起来很散:
- CSS 动画、渐变、三角形、新视口单位;
- Fetch、Axios、XMLHttpRequest;
- Nginx、Docker、Linux 命令;
- 注册流程里是否应该强制用户上传头像;
- CSRF、XSS、点击劫持;
- 假进度条算法;
- 为什么前端会被问后端和部署。
但这些题不是随机的。它们背后对应的是一个五年前端高级工程师的能力地图。
| 能力 | 面试题表象 | 真正考察 |
|---|---|---|
| 页面能力 | CSS 动画、渐变、三角形、视口单位 | 你是否理解 CSS 的渲染、布局和移动端坑点 |
| 网络能力 | Fetch、Axios、XHR | 你是否理解浏览器请求、错误处理、跨域、凭证 |
| 工程交付 | Nginx、Docker、Linux | 你是否能把项目从本地开发送到线上 |
| 后端协作 | 鉴权、BFF、接口流程 | 你是否理解数据从页面到服务端再回到页面的完整链路 |
| 安全意识 | CSRF、XSS、Clickjacking | 你是否知道前端写法会不会给系统埋雷 |
| 产品判断 | 注册时强制上传头像 | 你是否能从转化率、体验和业务目标之间做取舍 |
| 算法建模 | 虚假进度条 | 你是否能把一个体验问题抽象成一个可控模型 |
所以,真正的高级前端不是"所有题都背过",而是能在每个问题后面回答三层:
- 这个问题的原理是什么?
- 真实项目里会怎么落地?
- 它有哪些风险、边界和取舍?
下面就按这个思路重新梳理。
1. CSS:不是背属性,而是理解"浏览器怎么画"
CSS 面试题很容易被误解为"考记忆"。比如问动画属性,很多人会马上开始背:
css
animation-name
animation-duration
animation-timing-function
animation-delay
...
这当然要知道,但高级一点的回答不能只停在"有哪些属性"。你要把 CSS 看成浏览器绘制页面的语言:一个元素从初始样式到最终样式,中间怎么插值、怎么铺背景、怎么裁剪、怎么适配移动端视口,这些才是核心。
1.1 CSS 动画:记住一条时间线
CSS 动画由两部分组成:
@keyframes:定义动画过程中有哪些关键状态;animation-*:把这段关键帧动画挂到某个元素上,并告诉浏览器怎么播放。
一个最小例子:
css
@keyframes slideIn {
from {
transform: translateX(-24px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.toast {
animation: slideIn 240ms ease-out both;
}
这段代码里真正发生的是:
- 浏览器看到
.toast要播放slideIn; - 动画总时长是
240ms; - 速度曲线是
ease-out,也就是开始快、结束慢; both表示动画前后都保留关键帧对样式的影响。
可以用一句话记住 animation:
谁动、动多久、怎么动、等多久、动几次、往哪动、结束后留不留、现在跑不跑。
对应属性如下:
| 属性 | 作用 | 常见值 |
|---|---|---|
animation-name |
使用哪个 @keyframes |
slideIn、none |
animation-duration |
一个周期多久 | 200ms、2s |
animation-timing-function |
速度曲线 | linear、ease、ease-in-out、cubic-bezier()、steps() |
animation-delay |
延迟多久开始 | 0s、300ms |
animation-iteration-count |
播放次数 | 1、3、infinite |
animation-direction |
播放方向 | normal、reverse、alternate、alternate-reverse |
animation-fill-mode |
动画前后是否保留关键帧样式 | none、forwards、backwards、both |
animation-play-state |
播放或暂停 | running、paused |
animation-timeline |
动画进度由什么时间线驱动 | auto、scroll()、view() |
很多旧资料会漏掉 animation-timeline。这是近几年 CSS 滚动驱动动画里会遇到的属性。它允许动画不再只跟"时间"走,而是跟滚动进度或元素进入视口的进度走。
普通时间动画:
css
.card {
animation: fadeIn 300ms ease-out both;
}
滚动驱动动画的思路:
css
.progress-bar {
transform-origin: left center;
animation: grow linear both;
animation-timeline: scroll();
}
@keyframes grow {
from {
transform: scaleX(0);
}
to {
transform: scaleX(1);
}
}
面试回答可以这样说:
CSS 动画本质是浏览器在一段时间线里,根据
@keyframes和 timing function 对样式做插值。传统动画默认跟文档时间线走,现代 CSS 还可以通过animation-timeline让动画跟滚动进度走。项目里我会优先动画transform和opacity,因为它们更容易走合成层,避免频繁触发布局。
这里有两个加分点:
- 不要只说属性,要说"时间线"和"插值";
- 不要随便动画
width、height、top、left,复杂页面里容易触发布局计算。
1.2 CSS 渐变:背景是图片,文字和边框只是裁剪方式不同
CSS 渐变的记忆点很简单:
渐变不是颜色,渐变在 CSS 里更像一张由浏览器生成的图片。
所以你经常会看到它出现在 background、background-image、border-image 里。
背景渐变
线性渐变:
css
.banner {
background: linear-gradient(90deg, #0ea5e9, #22c55e);
}
径向渐变:
css
.spotlight {
background: radial-gradient(circle at center, #f97316, #111827);
}
线性渐变像"从一个方向刷过去",径向渐变像"从一个点向外扩散"。
文字渐变
文字渐变的关键不是"给文字设置渐变色",而是:
- 给元素设置渐变背景;
- 把背景裁剪到文字形状上;
- 让文字自身颜色透明。
css
.gradient-text {
background-image: linear-gradient(90deg, #0ea5e9, #22c55e);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
}
记忆方式:
字本身没颜色,真正有颜色的是背后的背景;文字只是变成了一块"镂空模板"。
渐变边框
最直接的方式是 border-image:
css
.panel {
border: 2px solid transparent;
border-image: linear-gradient(90deg, #0ea5e9, #22c55e) 1;
}
但它有一个常见坑:border-image 和圆角配合不理想,border-radius 不会像普通边框那样自然裁切边框图片。
实际项目里更常用的是"双背景 + 裁剪":
css
.gradient-border {
border: 2px solid transparent;
border-radius: 12px;
background:
linear-gradient(#ffffff, #ffffff) padding-box,
linear-gradient(90deg, #0ea5e9, #22c55e) border-box;
}
这段代码的含义是:
- 第一层白色背景只铺到
padding-box,也就是内容和内边距区域; - 第二层渐变背景铺到
border-box; - 边框本身透明,于是露出第二层渐变。
也可以用伪元素实现:
css
.gradient-border {
position: relative;
border-radius: 12px;
background: #ffffff;
}
.gradient-border::before {
content: "";
position: absolute;
inset: -2px;
z-index: -1;
border-radius: 14px;
background: linear-gradient(90deg, #0ea5e9, #22c55e);
}
面试回答可以这样说:
渐变本质上是 CSS 生成的图片。背景渐变直接放在
background-image;文字渐变靠background-clip: text;边框渐变可以用border-image,但圆角场景我更倾向用双背景裁剪或者伪元素,因为可控性更好。
1.3 纯 CSS 倒三角:利用边框交界处的斜切
CSS 三角形不是玄学。它来自一个很简单的事实:
元素宽高为 0 时,四个方向的边框会挤在一起,边框交界处天然形成斜线。
倒三角:
css
.triangle-down {
width: 0;
height: 0;
border-left: 8px solid transparent;
border-right: 8px solid transparent;
border-top: 8px solid #111827;
}
如果要画向上的三角形,就让 border-bottom 有颜色:
css
.triangle-up {
width: 0;
height: 0;
border-left: 8px solid transparent;
border-right: 8px solid transparent;
border-bottom: 8px solid #111827;
}
记忆方法:
想让箭头指向哪里,就给相反方向的 border 上色。
向下指,用 border-top 上色;向上指,用 border-bottom 上色;向左指,用 border-right 上色;向右指,用 border-left 上色。
真实项目里,CSS 三角形常见于:
- tooltip 箭头;
- 下拉菜单小尖角;
- 气泡组件;
- select 或 popover 的装饰箭头。
不过现在如果项目里已经有图标库,简单箭头不一定要用 CSS 三角形。能用图标时,用图标往往更直观、可维护。
1.4 新视口单位:vh 的坑,svh/lvh/dvh 的答案
移动端全屏布局里,100vh 是一个经典坑。
很多人以为:
css
.page {
height: 100vh;
}
就等于"占满当前可见屏幕"。但在移动端浏览器里,地址栏和底部工具栏会动态展开、收起。老的 vh 往往更接近"大视口"的高度,也就是浏览器工具栏收起后的高度。于是工具栏展开时,100vh 的内容可能被遮住,或者页面出现意外滚动。
为了解这个问题,现代 CSS 增加了几组视口单位:
| 单位 | 含义 | 可以怎么记 |
|---|---|---|
svh / svw |
small viewport,工具栏展开时的小视口 | 最保守,不会被工具栏遮住 |
lvh / lvw |
large viewport,工具栏收起时的大视口 | 最大高度,接近传统 vh 的移动端表现 |
dvh / dvw |
dynamic viewport,当前动态视口 | 随工具栏展开/收起变化 |
更形象一点:
100svh:浏览器 UI 最占地方时,还能看到的高度;100lvh:浏览器 UI 最少时,页面能用到的最大高度;100dvh:现在这一刻,用户真实可见的高度。
实战建议:
css
.fullscreen {
min-height: 100vh;
min-height: 100dvh;
}
为什么先写 100vh 再写 100dvh?
因为旧浏览器不认识 dvh 时,会保留前面的 vh;现代浏览器认识 dvh,后面的声明覆盖前面的声明。
但不要把 dvh 当成无脑万能药。它会随着浏览器 UI 变化而变,某些复杂页面在滚动过程中可能发生高度重新计算。大多数 H5 首屏、全屏弹窗、移动端页面可以优先用 dvh,但如果你希望高度稳定、不跟着工具栏变化,svh 可能更合适。
面试回答可以这样说:
移动端
100vh的问题是它不一定等于当前可见区域,地址栏和工具栏动态变化会导致遮挡或滚动。现在可以用svh/lvh/dvh区分小视口、大视口和动态视口。全屏弹窗我一般用100dvh,如果元素绝对不能被工具栏遮挡,可以考虑100svh。
2. 请求:Fetch、Axios、XHR 不是谁高级,而是谁替你做了什么
很多面试会问:
Fetch、Axios、XMLHttpRequest 有什么区别?
如果只答"Fetch 是 Promise,XHR 是回调,Axios 是第三方库",只能算入门。高级一点要说清楚:浏览器原生能力、错误语义、数据转换、取消、超时、拦截器、跨域凭证。
2.1 XMLHttpRequest:老,但仍然是很多库的底层
XMLHttpRequest 是早期浏览器里的请求对象。它可以发请求、监听状态变化、上传下载进度,但 API 风格偏底层。
一个简化版:
js
const xhr = new XMLHttpRequest();
xhr.open("GET", "/api/user");
xhr.onreadystatechange = () => {
if (xhr.readyState !== XMLHttpRequest.DONE) return;
if (xhr.status >= 200 && xhr.status < 300) {
const data = JSON.parse(xhr.responseText);
console.log(data);
} else {
console.error("Request failed:", xhr.status);
}
};
xhr.send();
它的问题不是不能用,而是:
- 回调式 API 写复杂流程不够舒服;
- JSON 解析要自己做;
- 错误处理要自己封装;
- 拦截器、统一错误提示、Token 注入等都要再包一层。
2.2 Fetch:现代、原生、但不等于"自动好用"
fetch 是浏览器原生的 Promise API。
js
async function requestJSON(url, options = {}) {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
}
这里最关键的点是:
fetch只会在网络错误、请求被取消、URL scheme 不合法等场景 reject;服务器返回404、500时,Promise 仍然会 resolve,只是response.ok是false。
也就是说,下面这段代码并不会因为接口返回 500 自动进入 catch:
js
try {
const response = await fetch("/api/user");
const data = await response.json();
} catch (error) {
// 只有网络层错误等才会到这里
}
更稳妥的写法是:
js
async function http(url, options = {}) {
const response = await fetch(url, {
headers: {
"Content-Type": "application/json",
...options.headers,
},
...options,
});
const contentType = response.headers.get("content-type") || "";
const isJSON = contentType.includes("application/json");
const body = isJSON ? await response.json() : await response.text();
if (!response.ok) {
const message = typeof body === "object" && body?.message
? body.message
: `HTTP ${response.status}`;
throw new Error(message);
}
return body;
}
如果要支持超时,可以用 AbortController:
js
async function fetchWithTimeout(url, options = {}, timeout = 10000) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeout);
try {
return await fetch(url, {
...options,
signal: controller.signal,
});
} finally {
clearTimeout(timer);
}
}
面试时最好点出:
- Fetch 是原生 Promise API;
- HTTP 错误状态不会自动 reject;
- JSON 要手动
response.json(); - 超时和取消要借助
AbortController; - 拦截器、统一错误处理需要自己封装。
2.3 Axios:不是"更高级",而是工程封装更完整
Axios 的价值在于它把工程里高频需求封装好了:
- 默认按 2xx 判断成功,非 2xx 进入错误分支;
- 自动转换 JSON 响应;
- 支持请求/响应拦截器;
- 支持
timeout; - 支持
AbortController取消; - 支持浏览器和 Node.js 环境;
- 浏览器里常见底层适配器可以是 XHR,也支持 fetch adapter。
典型封装:
js
import axios from "axios";
export const client = axios.create({
baseURL: "/api",
timeout: 10000,
withCredentials: true,
});
client.interceptors.request.use((config) => {
const token = localStorage.getItem("token");
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
client.interceptors.response.use(
(response) => response.data,
(error) => {
if (error.response?.status === 401) {
// 跳登录、刷新 token、清本地状态等
}
return Promise.reject(error);
}
);
但是"Axios 一定是首选"这个说法不够严谨。
更准确的说法是:
- 项目简单、追求零依赖:Fetch 足够;
- 中大型业务系统、需要拦截器、统一错误处理、超时、取消、跨端一致性:Axios 很省心;
- 框架已有请求层,例如 React Query、SWR、Nuxt、Next、内部 SDK:优先遵守项目约定。
2.4 三者对比
| 对比点 | XMLHttpRequest | Fetch | Axios |
|---|---|---|---|
| 类型 | 浏览器原生对象 | 浏览器原生 API | 第三方库 |
| 异步模型 | 回调 | Promise | Promise |
| 4xx/5xx | 不会自动当异常 | 不会自动 reject | 默认 reject |
| JSON | 手动 JSON.parse |
手动 response.json() |
默认转换 |
| 超时 | 可设置 timeout |
需自己配 AbortController |
内置 timeout |
| 取消 | abort() |
AbortController |
AbortController |
| 拦截器 | 无,需封装 | 无,需封装 | 内置 |
| 上传进度 | 支持 | 标准 Fetch 上传进度支持有限 | 浏览器环境支持进度回调 |
| Node 环境 | 不适用 | Node 新版本可用 | 支持 |
面试回答可以这样说:
XHR 是底层原生对象,能力完整但 API 老;Fetch 是现代原生 Promise API,但 HTTP 错误不会自动 reject,JSON、超时、拦截器都要自己封装;Axios 是第三方工程封装,默认错误语义、JSON 转换、拦截器、超时和取消更适合业务项目。选型不是谁高级,而是看项目是否需要统一请求层。
2.5 跨域、Cookie、SameSite:请求题最容易追问到这里
如果接口需要 Cookie 鉴权,跨域请求不是前端单方面写一个 withCredentials 就完事了。
Fetch 写法:
js
fetch("https://api.example.com/user", {
credentials: "include",
});
Axios 写法:
js
axios.get("https://api.example.com/user", {
withCredentials: true,
});
但后端也必须配合:
http
Access-Control-Allow-Origin: https://www.example.com
Access-Control-Allow-Credentials: true
注意:带凭证的 CORS 响应不能用:
http
Access-Control-Allow-Origin: *
否则浏览器会拦截响应。
Cookie 侧还要看属性:
http
Set-Cookie: session=abc; Path=/; HttpOnly; Secure; SameSite=None
几个属性的含义:
| 属性 | 作用 |
|---|---|
HttpOnly |
禁止 JavaScript 通过 document.cookie 读取,降低 XSS 偷 Cookie 的风险 |
Secure |
只在 HTTPS 下发送 Cookie,本地 localhost 例外处理由浏览器决定 |
SameSite=Lax |
大多数跨站子请求不带 Cookie,顶级导航 GET 仍可能携带 |
SameSite=Strict |
更严格,跨站场景基本不带 |
SameSite=None; Secure |
明确允许跨站发送,但必须配 Secure |
面试里一句话总结:
跨域带 Cookie 要三方都同意:前端设置
credentials/include或withCredentials,后端设置明确 Origin 和Access-Control-Allow-Credentials: true,Cookie 自己的SameSite、Secure策略也不能拦。
3. Nginx、Docker、Linux:前端上线不是把 dist 扔上去
高级前端被问 Nginx、Docker、Linux,并不是面试官"不讲武德"。因为前端项目最终不是停在 npm run dev,而是要上线、回滚、排障、缓存、代理、处理刷新 404。
一条常见前端上线链路是:
text
代码提交 -> CI 安装依赖 -> npm run build -> 生成 dist
-> Docker 构建镜像 -> Nginx 托管静态资源
-> 反向代理 API -> 部署到服务器/K8s -> 日志排障
3.1 Nginx 配置:先理解三层结构
Nginx 配置通常是三层:
nginx
http {
server {
location / {
# ...
}
}
}
http:HTTP 服务整体配置;server:一个虚拟主机,可以理解为一个站点;location:匹配具体路径,并决定怎么处理请求。
一个前端 SPA 项目常见配置:
nginx
server {
listen 80;
server_name example.com;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://backend:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
这里最重要的是 try_files:
nginx
try_files $uri $uri/ /index.html;
含义是:
- 先找请求路径对应的真实文件;
- 再找对应目录;
- 都找不到时,内部转发到
/index.html。
为什么 SPA 需要它?
因为 Vue Router / React Router 的很多路由其实是前端路由。比如用户刷新:
text
https://example.com/user/profile
服务器上未必真的有 /user/profile 这个文件。如果 Nginx 不回退到 index.html,就会 404。回到 index.html 后,前端路由接管页面渲染。
3.2 proxy_pass 的尾斜杠是高频坑
很多人会写:
nginx
location /api/ {
proxy_pass http://backend:8080/;
}
也有人写:
nginx
location /api/ {
proxy_pass http://backend:8080;
}
这两个不完全一样。
根据 Nginx 的规则:如果 proxy_pass 后面带 URI,那么匹配到的 location 部分会被替换掉;如果不带 URI,则原始请求 URI 通常会原样传给上游。
举例:
nginx
location /api/ {
proxy_pass http://backend:8080/;
}
请求:
text
/api/users
转发给后端时通常变成:
text
/users
而:
nginx
location /api/ {
proxy_pass http://backend:8080;
}
请求:
text
/api/users
转发给后端通常仍是:
text
/api/users
所以面试时可以说:
proxy_pass最容易踩坑的是尾部 URI。带/常常意味着把location匹配部分替换掉,不带 URI 则更接近原样转发。前后端联调时要跟后端确认接口实际路径,避免本地代理和线上代理行为不一致。
3.3 静态资源缓存:入口不缓存,带 hash 的资源强缓存
前端打包后的文件常见这样:
text
dist/
index.html
assets/
index.8f3a1c2.js
style.7b91e4d.css
index.html 是入口,它引用具体的 JS/CSS 文件。上线后如果旧的 index.html 被浏览器强缓存,用户可能一直加载旧资源。
所以一般策略是:
index.html不强缓存或短缓存;- 带内容 hash 的 JS/CSS/图片可以长缓存。
示例:
nginx
location = /index.html {
add_header Cache-Control "no-cache";
}
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
面试时能说出这点,会明显比"我会配 Nginx"更像做过上线。
3.4 Docker:前端项目常见多阶段构建
前端容器化常见做法:
- 用 Node 镜像安装依赖并构建;
- 用 Nginx 镜像承载构建产物;
- 最终镜像不带完整 Node 依赖,体积更小。
示例:
dockerfile
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:1.27-alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
这里的关键点:
npm ci更适合 CI 环境,依赖安装更可复现;dist是构建产物;- Nginx 官方镜像默认静态目录常见是
/usr/share/nginx/html; - 多阶段构建能把构建环境和运行环境分开。
面试回答可以这样说:
前端 Dockerfile 一般用多阶段构建。第一阶段用 Node 安装依赖并执行 build,第二阶段用 Nginx 托管静态文件,只把 dist 和 Nginx 配置复制进去。这样镜像更小,运行环境也更干净。
3.5 Linux 命令:别背全,按排障场景记
Linux 命令体系很大,面试时更重要的是能说出"排查问题时我怎么用"。
看目录和文件
bash
pwd
ls -la
cd /path/to/project
mkdir -p logs/nginx
cp source target
mv old-name new-name
rm -rf 要格外谨慎。线上机器里,删除前最好先 pwd、ls、确认路径,不要在疲劳状态下执行危险命令。
看日志
bash
tail -n 100 app.log
tail -f app.log
grep -i "error" app.log
排查线上问题时经常是:
bash
tail -f /var/log/nginx/error.log
看 Nginx 是不是配置错、路径错、后端不可达。
看进程和端口
bash
ps aux | grep nginx
top
ss -tulnp
netstat 很多老系统还在用,但新一些的 Linux 更推荐 ss。
看磁盘和内存
bash
df -h
free -m
前端上线也可能被磁盘打爆,比如:
- 日志没有滚动;
- Docker 镜像太多;
- 构建缓存太大;
- 静态资源历史版本没有清。
测接口
bash
curl -I https://example.com
curl https://example.com/api/health
curl -I 只看响应头,常用于确认:
- 状态码;
- 缓存头;
- CORS 头;
- Nginx 是否命中预期配置。
面试回答可以这样说:
Linux 命令我不会按字母表背,而是按排障链路用:先
curl看请求是否通,再ss看端口是否监听,再ps看进程,再tail -f看日志,再df/free看资源。这样能把命令和真实问题对应起来。
4. 后端知识:前端不一定写后端,但必须懂链路
高级前端被问后端,通常不是要求你转岗后端,而是考你是否能理解系统边界。
4.1 BFF:为什么前端会写一层 Node 服务?
BFF 是 Backend For Frontend,意思是"面向前端的后端"。
它常见职责:
- 聚合多个后端接口;
- 裁剪字段,减少前端处理成本;
- 做 SSR 或服务端渲染;
- 做登录态透传;
- 做接口兼容层;
- 给不同端提供不同数据形态。
比如页面要展示用户首页,需要:
text
GET /user
GET /orders/recent
GET /coupon/list
GET /recommendations
如果前端直接调四个接口,会有:
- 首屏请求多;
- 错误处理分散;
- 数据拼装都在客户端;
- 弱网下体验差。
BFF 可以提供:
text
GET /bff/home
由 Node 服务去聚合后端,再返回页面需要的数据。
面试回答:
BFF 不是为了炫技写 Node,而是把"前端页面需要的数据形态"和"后端领域服务的数据形态"解耦。它适合聚合接口、SSR、权限透传和多端差异化,但也会增加服务维护成本,所以不是所有项目都需要。
4.2 鉴权:Cookie、Session、JWT 不要混着说
前端常见鉴权方式:
Cookie + Session
流程:
- 用户登录;
- 服务端创建 session;
- 服务端通过
Set-Cookie下发 session id; - 浏览器后续请求自动带 Cookie;
- 服务端根据 session id 找用户身份。
优点:
- 前端不用手动保存 token;
- 配合
HttpOnly可以避免 JS 读取敏感 Cookie; - 服务端可以主动让 session 失效。
缺点:
- 依赖 Cookie;
- 跨域、SameSite、CORS 配置更复杂;
- 服务端要存 session 或使用共享存储。
JWT
流程:
- 用户登录;
- 服务端签发 token;
- 前端保存 token;
- 请求时放到
AuthorizationHeader; - 服务端验证签名和过期时间。
http
Authorization: Bearer <token>
优点:
- 服务端可以无状态校验;
- 更适合跨端、开放 API;
- 不强依赖浏览器 Cookie。
缺点:
- token 一旦泄漏,在过期前可能被滥用;
- 主动失效要额外做黑名单或版本号;
- 放在
localStorage会受到 XSS 风险影响。
实际项目里没有绝对答案。很多系统会用:
- 短期 access token;
- 长期 refresh token;
- refresh token 放
HttpOnly Cookie; - access token 放内存;
- 配合刷新机制和服务端失效机制。
面试回答:
Cookie + Session 更偏服务端状态管理,浏览器自动携带 Cookie,但跨域和 CSRF 要注意;JWT 更偏无状态 token,适合跨端,但要考虑泄漏、过期和主动失效。前端不能只问"token 存哪里",还要结合 XSS、CSRF、刷新机制和业务安全等级。
5. 产品题:注册时强制上传头像并跳首页,合理吗?
这个问题非常像真实工作。
需求是:
用户注册时除了填写账号密码,还需要上传头像;注册完成后直接跳转到首页。
初级回答可能是:
做一个上传组件,注册接口带头像 URL,成功后
router.push('/home')。
这只回答了"怎么做",没有回答"该不该这么做"。
5.1 先判断业务目标
注册流程的核心目标通常是降低转化漏斗损耗。
每多一个必填项,就多一个流失点:
text
打开注册页
-> 填手机号/邮箱
-> 填验证码/密码
-> 上传头像
-> 等上传完成
-> 点注册
-> 进入首页
如果头像不是业务强必要,强制上传会带来:
- 用户没有准备头像;
- 移动端相册权限弹窗增加阻力;
- 上传失败导致注册失败;
- 弱网下等待时间变长;
- 用户还没体验产品价值,就先被要求完善资料。
所以更合理的产品方案通常是:
注册只收集最小必要信息,成功后进入首页,再通过新手引导、资料完善卡片、任务体系或个人中心提醒用户上传头像。
5.2 如果业务强制要上传,前端怎么兜底?
如果业务就是要求头像必须有,那也要降低成本:
- 提供默认头像;
- 上传头像可裁剪但不强制复杂编辑;
- 上传失败允许重试;
- 注册按钮状态清楚;
- 弱网下有进度反馈;
- 后端接口要支持头像 URL;
- 注册成功返回 token 和用户信息;
- 首页首屏能立即显示用户头像。
更稳的流程是:
text
选择头像
-> 前端本地校验文件类型/大小
-> 展示本地预览
-> 上传到文件服务
-> 得到 avatarUrl
-> 提交注册信息
-> 保存登录态和用户信息
-> 跳转首页
为什么建议"头像上传"和"注册提交"拆开?
因为文件上传往往更慢、更容易失败。如果把文件和注册表单混在一个接口里:
- 注册接口压力更大;
- 失败原因不清楚;
- 前端重试成本更高;
- 后端职责不清晰。
前端伪代码:
js
async function handleAvatarChange(file) {
validateAvatar(file);
avatarPreview.value = URL.createObjectURL(file);
uploading.value = true;
try {
const { url } = await uploadAvatar(file);
form.avatarUrl = url;
} finally {
uploading.value = false;
}
}
async function handleRegister() {
if (uploading.value) {
showToast("头像仍在上传,请稍候");
return;
}
if (!form.avatarUrl) {
form.avatarUrl = DEFAULT_AVATAR_URL;
}
const result = await register({
account: form.account,
password: form.password,
avatarUrl: form.avatarUrl,
});
authStore.setToken(result.token);
userStore.setUser(result.user);
router.replace("/home");
}
5.3 这个题的高级回答模板
可以这样答:
我会先确认头像是否是注册成功的强必要条件。一般从转化率角度,不建议注册阶段强制上传头像,因为会增加漏斗流失,更好的方案是注册后进入首页再渐进式引导完善资料。如果业务强制要求,我会提供默认头像,并把上传流程做轻:本地校验、预览、独立上传、失败重试、注册时提交头像 URL。注册成功后保存 token 和用户信息,使用
replace跳首页,避免用户返回注册页。
这就是产品判断 + 工程落地 + 异常处理。
6. Web 安全:高级前端必须能守住基本盘
Web 安全题不要答成"背概念"。你要围绕三件事说:
- 攻击利用了浏览器什么机制?
- 攻击流程是什么?
- 前后端分别怎么防?
这一节只讲原理和防御,不写可直接滥用的攻击载荷。
6.1 XSS:攻击者让你的页面执行了不该执行的脚本
XSS,全称 Cross-Site Scripting,跨站脚本攻击。
核心是:
用户输入的数据,被当成可执行脚本插进页面里执行了。
常见类型:
| 类型 | 含义 |
|---|---|
| 存储型 XSS | 恶意内容被存到数据库,其他用户访问页面时触发 |
| 反射型 XSS | 恶意内容从 URL 或请求参数进入响应页面 |
| DOM 型 XSS | 前端 JS 直接把不可信数据写入 DOM 导致执行 |
典型风险:
- 读取非
HttpOnlyCookie; - 读取本地存储 token;
- 冒充用户发请求;
- 修改页面内容诱导操作;
- 劫持前端路由或表单。
防御核心:
1. 输出编码,而不是只做输入过滤
用户输入可以存,但输出到不同上下文时必须按上下文编码:
- HTML 文本节点:转义
<、>、&等; - HTML 属性:转义引号等;
- URL:校验协议,只允许
http:、https:等安全协议; - JavaScript 上下文:尽量避免把用户内容拼进脚本。
在 Vue/React 里,默认插值一般会转义:
jsx
function Comment({ content }) {
return <p>{content}</p>;
}
但危险 API 要小心:
jsx
function Comment({ html }) {
return <div dangerouslySetInnerHTML={{ __html: html }} />;
}
如果确实要渲染富文本,应该使用成熟的 HTML Sanitizer,并配置白名单标签和属性,而不是自己用正则过滤。
2. Cookie 设置 HttpOnly
http
Set-Cookie: session=abc; HttpOnly; Secure; SameSite=Lax
HttpOnly 不能阻止 XSS 执行,但能降低 Cookie 被 JS 直接读取的风险。
注意这句话很重要:
HttpOnly防的是"读 Cookie",不是防 XSS 本身。
如果页面已经有 XSS,攻击脚本仍然可能以用户身份发请求。所以根源上还要做输出编码和 CSP。
3. CSP 内容安全策略
CSP 可以限制页面能加载和执行哪些资源。
示例:
http
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'
CSP 是防线之一,不是替代编码。
面试回答:
XSS 的本质是不可信数据进入页面后被浏览器当成脚本执行。防御上不能只靠输入过滤,关键是按输出上下文编码;富文本要用可靠 sanitizer;敏感 Cookie 设置
HttpOnly;再用 CSP 限制脚本来源。React/Vue 默认插值相对安全,但v-html、dangerouslySetInnerHTML这类能力必须谨慎。
6.2 CSRF:攻击者借浏览器自动带 Cookie 的机制发请求
CSRF,全称 Cross-Site Request Forgery,跨站请求伪造。
它利用的是:
浏览器向某个站点发请求时,会自动带上该站点的 Cookie。
流程可以这样理解:
- 用户登录了网站 A;
- 网站 A 通过 Cookie 识别用户;
- 用户在未退出 A 的情况下访问了恶意网站 B;
- B 诱导浏览器向 A 发起某个操作请求;
- 浏览器自动带上 A 的 Cookie;
- A 如果只看 Cookie,就可能误以为这是用户本人主动操作。
CSRF 的关键不是"偷 Cookie",而是"借 Cookie"。
防御手段:
1. 不要用 GET 做有副作用的操作
比如删除、转账、修改资料都不应该用 GET。
text
GET /delete?id=1
这是很危险的设计。GET 应该尽量保持只读。
2. CSRF Token
服务端生成随机 token,前端提交敏感请求时带上:
http
X-CSRF-Token: <token>
恶意第三方页面即使能让浏览器带 Cookie,也拿不到这个 token。
3. SameSite Cookie
http
Set-Cookie: session=abc; SameSite=Lax; Secure; HttpOnly
SameSite 可以减少跨站请求携带 Cookie 的机会。
但它不是银弹。复杂系统里,OWASP 也建议不要只依赖 SameSite,而要结合 CSRF Token 等策略。
4. 校验 Origin / Referer
后端可以校验请求来源:
text
Origin: https://www.example.com
如果来源不是可信域名,就拒绝敏感操作。
面试回答:
CSRF 利用的是浏览器自动携带 Cookie,而不是直接偷 Cookie。防御上首先避免 GET 做副作用操作;对敏感请求加 CSRF Token;Cookie 设置 SameSite;后端再校验 Origin 或 Referer。SameSite 是重要防线,但不能替代 token,尤其是高风险业务。
6.3 Clickjacking:用户以为点 A,实际点了 B
Clickjacking,点击劫持。
核心是:
攻击者把你的页面嵌进透明或伪装的 iframe,让用户误点你的页面按钮。
防御重点在响应头。
X-Frame-Options
http
X-Frame-Options: DENY
或:
http
X-Frame-Options: SAMEORIGIN
含义:
DENY:完全禁止被 iframe;SAMEORIGIN:只允许同源页面嵌入。
CSP frame-ancestors
现代更推荐:
http
Content-Security-Policy: frame-ancestors 'self'
或完全禁止:
http
Content-Security-Policy: frame-ancestors 'none'
面试回答:
点击劫持不是脚本注入,而是视觉层欺骗。防御主要靠响应头,老方案是
X-Frame-Options,现代 CSP 用frame-ancestors控制谁能嵌套当前页面。涉及支付、删除、权限修改等高危页面,最好默认不允许被第三方 iframe 嵌套。
6.4 三类攻击放在一起记
| 攻击 | 利用点 | 关键词 | 主要防御 |
|---|---|---|---|
| XSS | 页面执行了不可信脚本 | 注入、执行、窃取 | 输出编码、Sanitizer、HttpOnly、CSP |
| CSRF | 浏览器自动带 Cookie | 伪造请求、借身份 | CSRF Token、SameSite、Origin/Referer、避免 GET 副作用 |
| Clickjacking | 页面被 iframe 视觉欺骗 | 误点、透明 iframe | X-Frame-Options、frame-ancestors |
一句话记忆:
XSS 是"把坏脚本塞进你页面",CSRF 是"借你的登录态发坏请求",点击劫持是"让你以为点了这个,其实点了那个"。
7. 算法题:虚假进度条不是骗人,是体验建模
面试里问"虚假进度条",通常不是让你写一个 setInterval 就结束。
它考的是:
- 你能否把不确定耗时建模成用户可理解的反馈;
- 你是否能处理成功、失败、超时、取消;
- 你是否能避免进度条倒退、卡死、乱跳。
7.1 为什么需要假进度?
有些任务没有真实进度:
- 后端长任务;
- 文件转码;
- AI 生成;
- 报表导出;
- 批量处理;
- 多接口聚合。
前端只知道:
text
请求开始了
请求成功了
请求失败了
中间到底 30% 还是 70%,不知道。
如果页面什么都不展示,用户会焦虑;如果一直转圈,用户不知道还要等多久。假进度条的意义是:
用一个可控的视觉反馈告诉用户:系统还在工作,而且越来越接近完成。
7.2 核心模型:每次只走剩余距离的一部分
最常见的模型是"剩余量衰减":
text
next = current + (target - current) * ratio
如果目标是 99,当前是 0,比例是 0.15:
- 第一次走到 14.85;
- 第二次走到 27.47;
- 第三次走到 38.20;
- 越往后剩余越少,增长越慢;
- 最终接近 99,但不会自己到 100。
这很符合用户感知:
text
开始很快 -> 中间变慢 -> 快完成时卡住 -> 成功后直接满格
7.3 一个更完整的实现
js
function createFakeProgress({
min = 0,
maxBeforeDone = 95,
interval = 200,
ratio = 0.12,
onChange,
} = {}) {
let progress = min;
let timer = null;
let status = "idle";
function emit(value) {
progress = Math.max(progress, Math.min(value, 100));
onChange?.(Number(progress.toFixed(2)));
}
function start() {
if (timer) return;
status = "running";
emit(progress);
timer = setInterval(() => {
if (status !== "running") return;
const distance = maxBeforeDone - progress;
const randomFactor = 0.8 + Math.random() * 0.4;
const step = Math.max(distance * ratio * randomFactor, 0.2);
if (progress >= maxBeforeDone) {
emit(maxBeforeDone);
return;
}
emit(Math.min(progress + step, maxBeforeDone));
}, interval);
}
function done() {
status = "done";
clearInterval(timer);
timer = null;
emit(100);
}
function fail() {
status = "failed";
clearInterval(timer);
timer = null;
}
function reset() {
status = "idle";
clearInterval(timer);
timer = null;
progress = min;
emit(progress);
}
return {
start,
done,
fail,
reset,
getProgress: () => progress,
getStatus: () => status,
};
}
使用:
js
async function submitTask() {
const progress = createFakeProgress({
maxBeforeDone: 96,
onChange: renderProgress,
});
progress.start();
try {
await runLongTask();
progress.done();
showSuccess();
} catch (error) {
progress.fail();
showError(error);
}
}
7.4 真实项目里的细节
不要让进度倒退
如果你同时有真实进度和模拟进度,取较大值:
js
displayProgress = Math.max(fakeProgress, realProgress);
用户最讨厌看到 80% 突然回到 40%。
不要太早到 99%
如果接口可能要 30 秒,而你 3 秒就到 99%,用户会觉得"卡死了"。可以分阶段:
text
0 - 60:快
60 - 85:中
85 - 95:慢
95 - 99:非常慢
成功:100
失败时不要显示 100%
失败应该停在当前进度,然后给出明确错误状态。不要为了动画完整把失败也拉到 100%,这会误导用户。
取消时要清 timer
组件卸载、路由切换、用户取消任务时,要清掉定时器,否则会有内存泄漏或状态更新异常。
7.5 面试回答模板
假进度条适合后端没有真实进度的长任务。我的设计是用剩余量衰减模型,每次增加
(目标值 - 当前值) * 比例,让它开始快、后面慢,并停在 95 或 99 等待真实结果。接口成功后直接补到 100,失败则停止并显示错误,取消或组件卸载时清理定时器。如果有真实进度和模拟进度并存,要保证展示进度不倒退。
8. 把这些题串起来:高级前端的回答方式
经过上面的拆解,你会发现高级前端面试的关键不是"说得多",而是"说得有层次"。
推荐回答结构:
text
先给结论
-> 解释原理
-> 给项目落地方式
-> 补充风险和边界
比如问 Fetch 和 Axios:
text
结论:
Fetch 是原生 Promise API,Axios 是第三方请求库,业务系统里 Axios 的工程封装更完整。
原理:
Fetch 返回 Response,HTTP 4xx/5xx 不会自动 reject,需要手动判断 response.ok。
落地:
如果用 Fetch,我会封装统一的 http 方法,处理 JSON、错误、超时和鉴权;如果项目复杂,用 Axios 可以用拦截器、timeout、取消请求等能力。
边界:
跨域带 Cookie 不是前端一个配置能解决,还要后端 CORS 和 Cookie SameSite/Secure 配合。
比如问 Nginx:
text
结论:
前端项目常用 Nginx 托管 dist,同时做 SPA fallback 和 API 反向代理。
原理:
try_files 会按顺序查找真实文件,找不到就内部转发到 index.html,让前端路由接管。
落地:
index.html 不强缓存,带 hash 的静态资源长缓存;/api 代理到后端。
边界:
proxy_pass 尾斜杠会影响 URI 转发结果,线上配置要和后端接口路径一致。
比如问产品需求:
text
结论:
不建议注册阶段强制上传头像,除非头像是业务强必要信息。
原理:
注册流程核心目标是转化,每增加一步都会增加漏斗流失。
落地:
更推荐注册后渐进式引导完善头像;如果必须上传,就提供默认头像、预览、独立上传、失败重试。
边界:
弱网、权限弹窗、上传失败都要处理,不能让头像上传失败直接阻塞核心注册流程。
9. 复盘:这场面试真正提醒了什么?
这次面试最有价值的地方,不是发现"我还有几个题不会",而是提醒我们:五年前端的能力边界已经比过去宽很多。
你不仅要会写页面,还要知道:
- 页面为什么这样布局;
- 动画为什么这样动;
- 请求为什么这样失败;
- Cookie 为什么没有带上;
- 路由刷新为什么 404;
- 静态资源为什么缓存错;
- 容器为什么跑不起来;
- 日志应该去哪看;
- 需求会不会伤害转化;
- 接口设计会不会放大安全风险;
- 没有真实进度时怎么给用户稳定反馈。
真正的高级前端,是能把这些问题连成一条线的人:
text
用户体验
-> 页面实现
-> 请求链路
-> 鉴权安全
-> 构建部署
-> 线上排障
-> 产品取舍
如果你现在还觉得这些知识点有点多,不用慌。学习顺序可以这样排:
- 先把 CSS 和请求层吃透,因为这是每天都用的基本功;
- 再补 Nginx、Docker、Linux,因为它们决定你能不能独立上线;
- 然后补安全和鉴权,因为它们决定你写的系统有没有底线;
- 最后训练产品判断和算法建模,因为这是高级工程师和纯执行之间的分水岭。
面试不是判决书,它更像一次系统扫描。扫出问题不可怕,可怕的是扫完以后不整理、不复盘、不把经验沉淀成自己的知识结构。
这次"一面之缘",如果能把它变成一张能力地图,那它就不只是一次面试,而是一次升级。
10. 附:面试前快速记忆卡片
CSS
- 动画:
@keyframes定义状态,animation-*控制播放。 - 动画简写:时间值第一个是 duration,第二个是 delay,动画名建议放最后。
- 优先动画
transform、opacity。 - 渐变是"浏览器生成的图片"。
- 文字渐变:背景渐变 +
background-clip: text+color: transparent。 - 圆角渐变边框:优先双背景裁剪或伪元素。
- CSS 三角形:宽高为 0,给相反方向 border 上色。
- 移动端全屏:优先理解
svh/lvh/dvh,不要迷信100vh。
请求
- XHR:老式回调 API,底层能力完整。
- Fetch:原生 Promise,4xx/5xx 不自动 reject。
- Fetch 解析 JSON 要手动
response.json()。 - Fetch 超时/取消用
AbortController。 - Axios:默认非 2xx reject,支持拦截器、timeout、取消、JSON 转换。
- 跨域带 Cookie:前端、后端 CORS、Cookie 属性三方都要配合。
Nginx / Docker / Linux
- SPA 刷新 404:
try_files $uri $uri/ /index.html。 proxy_pass尾斜杠会影响 URI 转发。index.html不强缓存,hash 静态资源长缓存。- Docker 多阶段:Node build,Nginx serve。
- Linux 排障:
curl、ss、ps、tail、df、free。
安全
- XSS:坏脚本进页面执行。
- CSRF:借浏览器自动带 Cookie 发请求。
- Clickjacking:视觉欺骗,误导用户点击。
- XSS 防御:输出编码、Sanitizer、HttpOnly、CSP。
- CSRF 防御:CSRF Token、SameSite、Origin/Referer、避免 GET 副作用。
- 点击劫持防御:
X-Frame-Options、CSPframe-ancestors。
产品和算法
- 注册流程先看转化率,不要无脑加必填项。
- 头像不是强必要时,优先注册后渐进式完善。
- 假进度条:剩余量衰减,成功到 100,失败停止,取消清理。
- 回答问题:结论 -> 原理 -> 落地 -> 风险边界。
参考资料
- MDN:
animationCSS property - MDN:CSS values and units
- MDN:
border-imageCSS property - MDN:Using the Fetch API
- MDN:Cross-Origin Resource Sharing (CORS)
- MDN:
Set-Cookieheader - Axios Docs:Response schema
- Axios Docs:Interceptors
- Axios Docs:Cancellation
- Axios Docs:Request config
- Nginx Docs:
try_files - Nginx Docs:
proxy_pass - Docker Docs:Multi-stage builds
- Docker Hub:nginx official image
- OWASP Cheat Sheet:Cross Site Scripting Prevention
- OWASP Cheat Sheet:Cross-Site Request Forgery Prevention
- OWASP Cheat Sheet:Clickjacking Defense