首页 > 面试资料 博客日记

莱特摩比的一面之缘(前端经验)

2026-05-16 23:30:02面试资料围观4

这篇文章介绍了莱特摩比的一面之缘(前端经验),分享给大家做个参考,收藏极客资料网收获更多编程知识

这篇不是“面试题答案合集”,而是一次高级前端面试复盘。

如果只把这些问题背下来,下一场面试换个问法还是会慌;但如果能看懂它们背后的能力模型,你会发现: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. 这个问题的原理是什么?
  2. 真实项目里会怎么落地?
  3. 它有哪些风险、边界和取舍?

下面就按这个思路重新梳理。

1. CSS:不是背属性,而是理解“浏览器怎么画”

CSS 面试题很容易被误解为“考记忆”。比如问动画属性,很多人会马上开始背:

animation-name
animation-duration
animation-timing-function
animation-delay
...

这当然要知道,但高级一点的回答不能只停在“有哪些属性”。你要把 CSS 看成浏览器绘制页面的语言:一个元素从初始样式到最终样式,中间怎么插值、怎么铺背景、怎么裁剪、怎么适配移动端视口,这些才是核心。

1.1 CSS 动画:记住一条时间线

CSS 动画由两部分组成:

  • @keyframes:定义动画过程中有哪些关键状态;
  • animation-*:把这段关键帧动画挂到某个元素上,并告诉浏览器怎么播放。

一个最小例子:

@keyframes slideIn {
  from {
    transform: translateX(-24px);
    opacity: 0;
  }

  to {
    transform: translateX(0);
    opacity: 1;
  }
}

.toast {
  animation: slideIn 240ms ease-out both;
}

这段代码里真正发生的是:

  1. 浏览器看到 .toast 要播放 slideIn
  2. 动画总时长是 240ms
  3. 速度曲线是 ease-out,也就是开始快、结束慢;
  4. both 表示动画前后都保留关键帧对样式的影响。

可以用一句话记住 animation

谁动、动多久、怎么动、等多久、动几次、往哪动、结束后留不留、现在跑不跑。

对应属性如下:

属性 作用 常见值
animation-name 使用哪个 @keyframes slideInnone
animation-duration 一个周期多久 200ms2s
animation-timing-function 速度曲线 lineareaseease-in-outcubic-bezier()steps()
animation-delay 延迟多久开始 0s300ms
animation-iteration-count 播放次数 13infinite
animation-direction 播放方向 normalreversealternatealternate-reverse
animation-fill-mode 动画前后是否保留关键帧样式 noneforwardsbackwardsboth
animation-play-state 播放或暂停 runningpaused
animation-timeline 动画进度由什么时间线驱动 autoscroll()view()

很多旧资料会漏掉 animation-timeline。这是近几年 CSS 滚动驱动动画里会遇到的属性。它允许动画不再只跟“时间”走,而是跟滚动进度或元素进入视口的进度走。

普通时间动画:

.card {
  animation: fadeIn 300ms ease-out both;
}

滚动驱动动画的思路:

.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 让动画跟滚动进度走。项目里我会优先动画 transformopacity,因为它们更容易走合成层,避免频繁触发布局。

这里有两个加分点:

  • 不要只说属性,要说“时间线”和“插值”;
  • 不要随便动画 widthheighttopleft,复杂页面里容易触发布局计算。

1.2 CSS 渐变:背景是图片,文字和边框只是裁剪方式不同

CSS 渐变的记忆点很简单:

渐变不是颜色,渐变在 CSS 里更像一张由浏览器生成的图片。

所以你经常会看到它出现在 backgroundbackground-imageborder-image 里。

背景渐变

线性渐变:

.banner {
  background: linear-gradient(90deg, #0ea5e9, #22c55e);
}

径向渐变:

.spotlight {
  background: radial-gradient(circle at center, #f97316, #111827);
}

线性渐变像“从一个方向刷过去”,径向渐变像“从一个点向外扩散”。

文字渐变

文字渐变的关键不是“给文字设置渐变色”,而是:

  1. 给元素设置渐变背景;
  2. 把背景裁剪到文字形状上;
  3. 让文字自身颜色透明。
.gradient-text {
  background-image: linear-gradient(90deg, #0ea5e9, #22c55e);
  background-clip: text;
  -webkit-background-clip: text;
  color: transparent;
}

记忆方式:

字本身没颜色,真正有颜色的是背后的背景;文字只是变成了一块“镂空模板”。

渐变边框

最直接的方式是 border-image

.panel {
  border: 2px solid transparent;
  border-image: linear-gradient(90deg, #0ea5e9, #22c55e) 1;
}

但它有一个常见坑:border-image 和圆角配合不理想,border-radius 不会像普通边框那样自然裁切边框图片。

实际项目里更常用的是“双背景 + 裁剪”:

.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
  • 边框本身透明,于是露出第二层渐变。

也可以用伪元素实现:

.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 时,四个方向的边框会挤在一起,边框交界处天然形成斜线。

倒三角:

.triangle-down {
  width: 0;
  height: 0;
  border-left: 8px solid transparent;
  border-right: 8px solid transparent;
  border-top: 8px solid #111827;
}

如果要画向上的三角形,就让 border-bottom 有颜色:

.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 是一个经典坑。

很多人以为:

.page {
  height: 100vh;
}

就等于“占满当前可见屏幕”。但在移动端浏览器里,地址栏和底部工具栏会动态展开、收起。老的 vh 往往更接近“大视口”的高度,也就是浏览器工具栏收起后的高度。于是工具栏展开时,100vh 的内容可能被遮住,或者页面出现意外滚动。

为了解这个问题,现代 CSS 增加了几组视口单位:

单位 含义 可以怎么记
svh / svw small viewport,工具栏展开时的小视口 最保守,不会被工具栏遮住
lvh / lvw large viewport,工具栏收起时的大视口 最大高度,接近传统 vh 的移动端表现
dvh / dvw dynamic viewport,当前动态视口 随工具栏展开/收起变化

更形象一点:

  • 100svh:浏览器 UI 最占地方时,还能看到的高度;
  • 100lvh:浏览器 UI 最少时,页面能用到的最大高度;
  • 100dvh:现在这一刻,用户真实可见的高度。

实战建议:

.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 风格偏底层。

一个简化版:

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。

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;服务器返回 404500 时,Promise 仍然会 resolve,只是 response.okfalse

也就是说,下面这段代码并不会因为接口返回 500 自动进入 catch

try {
  const response = await fetch("/api/user");
  const data = await response.json();
} catch (error) {
  // 只有网络层错误等才会到这里
}

更稳妥的写法是:

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

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。

典型封装:

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 写法:

fetch("https://api.example.com/user", {
  credentials: "include",
});

Axios 写法:

axios.get("https://api.example.com/user", {
  withCredentials: true,
});

但后端也必须配合:

Access-Control-Allow-Origin: https://www.example.com
Access-Control-Allow-Credentials: true

注意:带凭证的 CORS 响应不能用:

Access-Control-Allow-Origin: *

否则浏览器会拦截响应。

Cookie 侧还要看属性:

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/includewithCredentials,后端设置明确 Origin 和 Access-Control-Allow-Credentials: true,Cookie 自己的 SameSiteSecure 策略也不能拦。

3. Nginx、Docker、Linux:前端上线不是把 dist 扔上去

高级前端被问 Nginx、Docker、Linux,并不是面试官“不讲武德”。因为前端项目最终不是停在 npm run dev,而是要上线、回滚、排障、缓存、代理、处理刷新 404。

一条常见前端上线链路是:

代码提交 -> CI 安装依赖 -> npm run build -> 生成 dist
       -> Docker 构建镜像 -> Nginx 托管静态资源
       -> 反向代理 API -> 部署到服务器/K8s -> 日志排障

3.1 Nginx 配置:先理解三层结构

Nginx 配置通常是三层:

http {
  server {
    location / {
      # ...
    }
  }
}
  • http:HTTP 服务整体配置;
  • server:一个虚拟主机,可以理解为一个站点;
  • location:匹配具体路径,并决定怎么处理请求。

一个前端 SPA 项目常见配置:

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

try_files $uri $uri/ /index.html;

含义是:

  1. 先找请求路径对应的真实文件;
  2. 再找对应目录;
  3. 都找不到时,内部转发到 /index.html

为什么 SPA 需要它?

因为 Vue Router / React Router 的很多路由其实是前端路由。比如用户刷新:

https://example.com/user/profile

服务器上未必真的有 /user/profile 这个文件。如果 Nginx 不回退到 index.html,就会 404。回到 index.html 后,前端路由接管页面渲染。

3.2 proxy_pass 的尾斜杠是高频坑

很多人会写:

location /api/ {
    proxy_pass http://backend:8080/;
}

也有人写:

location /api/ {
    proxy_pass http://backend:8080;
}

这两个不完全一样。

根据 Nginx 的规则:如果 proxy_pass 后面带 URI,那么匹配到的 location 部分会被替换掉;如果不带 URI,则原始请求 URI 通常会原样传给上游。

举例:

location /api/ {
    proxy_pass http://backend:8080/;
}

请求:

/api/users

转发给后端时通常变成:

/users

而:

location /api/ {
    proxy_pass http://backend:8080;
}

请求:

/api/users

转发给后端通常仍是:

/api/users

所以面试时可以说:

proxy_pass 最容易踩坑的是尾部 URI。带 / 常常意味着把 location 匹配部分替换掉,不带 URI 则更接近原样转发。前后端联调时要跟后端确认接口实际路径,避免本地代理和线上代理行为不一致。

3.3 静态资源缓存:入口不缓存,带 hash 的资源强缓存

前端打包后的文件常见这样:

dist/
  index.html
  assets/
    index.8f3a1c2.js
    style.7b91e4d.css

index.html 是入口,它引用具体的 JS/CSS 文件。上线后如果旧的 index.html 被浏览器强缓存,用户可能一直加载旧资源。

所以一般策略是:

  • index.html 不强缓存或短缓存;
  • 带内容 hash 的 JS/CSS/图片可以长缓存。

示例:

location = /index.html {
    add_header Cache-Control "no-cache";
}

location /assets/ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}

面试时能说出这点,会明显比“我会配 Nginx”更像做过上线。

3.4 Docker:前端项目常见多阶段构建

前端容器化常见做法:

  1. 用 Node 镜像安装依赖并构建;
  2. 用 Nginx 镜像承载构建产物;
  3. 最终镜像不带完整 Node 依赖,体积更小。

示例:

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 命令体系很大,面试时更重要的是能说出“排查问题时我怎么用”。

看目录和文件

pwd
ls -la
cd /path/to/project
mkdir -p logs/nginx
cp source target
mv old-name new-name

rm -rf 要格外谨慎。线上机器里,删除前最好先 pwdls、确认路径,不要在疲劳状态下执行危险命令。

看日志

tail -n 100 app.log
tail -f app.log
grep -i "error" app.log

排查线上问题时经常是:

tail -f /var/log/nginx/error.log

看 Nginx 是不是配置错、路径错、后端不可达。

看进程和端口

ps aux | grep nginx
top
ss -tulnp

netstat 很多老系统还在用,但新一些的 Linux 更推荐 ss

看磁盘和内存

df -h
free -m

前端上线也可能被磁盘打爆,比如:

  • 日志没有滚动;
  • Docker 镜像太多;
  • 构建缓存太大;
  • 静态资源历史版本没有清。

测接口

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 或服务端渲染;
  • 做登录态透传;
  • 做接口兼容层;
  • 给不同端提供不同数据形态。

比如页面要展示用户首页,需要:

GET /user
GET /orders/recent
GET /coupon/list
GET /recommendations

如果前端直接调四个接口,会有:

  • 首屏请求多;
  • 错误处理分散;
  • 数据拼装都在客户端;
  • 弱网下体验差。

BFF 可以提供:

GET /bff/home

由 Node 服务去聚合后端,再返回页面需要的数据。

面试回答:

BFF 不是为了炫技写 Node,而是把“前端页面需要的数据形态”和“后端领域服务的数据形态”解耦。它适合聚合接口、SSR、权限透传和多端差异化,但也会增加服务维护成本,所以不是所有项目都需要。

4.2 鉴权:Cookie、Session、JWT 不要混着说

前端常见鉴权方式:

Cookie + Session

流程:

  1. 用户登录;
  2. 服务端创建 session;
  3. 服务端通过 Set-Cookie 下发 session id;
  4. 浏览器后续请求自动带 Cookie;
  5. 服务端根据 session id 找用户身份。

优点:

  • 前端不用手动保存 token;
  • 配合 HttpOnly 可以避免 JS 读取敏感 Cookie;
  • 服务端可以主动让 session 失效。

缺点:

  • 依赖 Cookie;
  • 跨域、SameSite、CORS 配置更复杂;
  • 服务端要存 session 或使用共享存储。

JWT

流程:

  1. 用户登录;
  2. 服务端签发 token;
  3. 前端保存 token;
  4. 请求时放到 Authorization Header;
  5. 服务端验证签名和过期时间。
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 先判断业务目标

注册流程的核心目标通常是降低转化漏斗损耗。

每多一个必填项,就多一个流失点:

打开注册页
  -> 填手机号/邮箱
  -> 填验证码/密码
  -> 上传头像
  -> 等上传完成
  -> 点注册
  -> 进入首页

如果头像不是业务强必要,强制上传会带来:

  • 用户没有准备头像;
  • 移动端相册权限弹窗增加阻力;
  • 上传失败导致注册失败;
  • 弱网下等待时间变长;
  • 用户还没体验产品价值,就先被要求完善资料。

所以更合理的产品方案通常是:

注册只收集最小必要信息,成功后进入首页,再通过新手引导、资料完善卡片、任务体系或个人中心提醒用户上传头像。

5.2 如果业务强制要上传,前端怎么兜底?

如果业务就是要求头像必须有,那也要降低成本:

  • 提供默认头像;
  • 上传头像可裁剪但不强制复杂编辑;
  • 上传失败允许重试;
  • 注册按钮状态清楚;
  • 弱网下有进度反馈;
  • 后端接口要支持头像 URL;
  • 注册成功返回 token 和用户信息;
  • 首页首屏能立即显示用户头像。

更稳的流程是:

选择头像
  -> 前端本地校验文件类型/大小
  -> 展示本地预览
  -> 上传到文件服务
  -> 得到 avatarUrl
  -> 提交注册信息
  -> 保存登录态和用户信息
  -> 跳转首页

为什么建议“头像上传”和“注册提交”拆开?

因为文件上传往往更慢、更容易失败。如果把文件和注册表单混在一个接口里:

  • 注册接口压力更大;
  • 失败原因不清楚;
  • 前端重试成本更高;
  • 后端职责不清晰。

前端伪代码:

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 安全题不要答成“背概念”。你要围绕三件事说:

  1. 攻击利用了浏览器什么机制?
  2. 攻击流程是什么?
  3. 前后端分别怎么防?

这一节只讲原理和防御,不写可直接滥用的攻击载荷。

6.1 XSS:攻击者让你的页面执行了不该执行的脚本

XSS,全称 Cross-Site Scripting,跨站脚本攻击。

核心是:

用户输入的数据,被当成可执行脚本插进页面里执行了。

常见类型:

类型 含义
存储型 XSS 恶意内容被存到数据库,其他用户访问页面时触发
反射型 XSS 恶意内容从 URL 或请求参数进入响应页面
DOM 型 XSS 前端 JS 直接把不可信数据写入 DOM 导致执行

典型风险:

  • 读取非 HttpOnly Cookie;
  • 读取本地存储 token;
  • 冒充用户发请求;
  • 修改页面内容诱导操作;
  • 劫持前端路由或表单。

防御核心:

1. 输出编码,而不是只做输入过滤

用户输入可以存,但输出到不同上下文时必须按上下文编码:

  • HTML 文本节点:转义 <>& 等;
  • HTML 属性:转义引号等;
  • URL:校验协议,只允许 http:https: 等安全协议;
  • JavaScript 上下文:尽量避免把用户内容拼进脚本。

在 Vue/React 里,默认插值一般会转义:

function Comment({ content }) {
  return <p>{content}</p>;
}

但危险 API 要小心:

function Comment({ html }) {
  return <div dangerouslySetInnerHTML={{ __html: html }} />;
}

如果确实要渲染富文本,应该使用成熟的 HTML Sanitizer,并配置白名单标签和属性,而不是自己用正则过滤。

2. Cookie 设置 HttpOnly

Set-Cookie: session=abc; HttpOnly; Secure; SameSite=Lax

HttpOnly 不能阻止 XSS 执行,但能降低 Cookie 被 JS 直接读取的风险。

注意这句话很重要:

HttpOnly 防的是“读 Cookie”,不是防 XSS 本身。

如果页面已经有 XSS,攻击脚本仍然可能以用户身份发请求。所以根源上还要做输出编码和 CSP。

3. CSP 内容安全策略

CSP 可以限制页面能加载和执行哪些资源。

示例:

Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'

CSP 是防线之一,不是替代编码。

面试回答:

XSS 的本质是不可信数据进入页面后被浏览器当成脚本执行。防御上不能只靠输入过滤,关键是按输出上下文编码;富文本要用可靠 sanitizer;敏感 Cookie 设置 HttpOnly;再用 CSP 限制脚本来源。React/Vue 默认插值相对安全,但 v-htmldangerouslySetInnerHTML 这类能力必须谨慎。

6.2 CSRF:攻击者借浏览器自动带 Cookie 的机制发请求

CSRF,全称 Cross-Site Request Forgery,跨站请求伪造。

它利用的是:

浏览器向某个站点发请求时,会自动带上该站点的 Cookie。

流程可以这样理解:

  1. 用户登录了网站 A;
  2. 网站 A 通过 Cookie 识别用户;
  3. 用户在未退出 A 的情况下访问了恶意网站 B;
  4. B 诱导浏览器向 A 发起某个操作请求;
  5. 浏览器自动带上 A 的 Cookie;
  6. A 如果只看 Cookie,就可能误以为这是用户本人主动操作。

CSRF 的关键不是“偷 Cookie”,而是“借 Cookie”。

防御手段:

1. 不要用 GET 做有副作用的操作

比如删除、转账、修改资料都不应该用 GET。

GET /delete?id=1

这是很危险的设计。GET 应该尽量保持只读。

2. CSRF Token

服务端生成随机 token,前端提交敏感请求时带上:

X-CSRF-Token: <token>

恶意第三方页面即使能让浏览器带 Cookie,也拿不到这个 token。

3. SameSite Cookie

Set-Cookie: session=abc; SameSite=Lax; Secure; HttpOnly

SameSite 可以减少跨站请求携带 Cookie 的机会。

但它不是银弹。复杂系统里,OWASP 也建议不要只依赖 SameSite,而要结合 CSRF Token 等策略。

4. 校验 Origin / Referer

后端可以校验请求来源:

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

X-Frame-Options: DENY

或:

X-Frame-Options: SAMEORIGIN

含义:

  • DENY:完全禁止被 iframe;
  • SAMEORIGIN:只允许同源页面嵌入。

CSP frame-ancestors

现代更推荐:

Content-Security-Policy: frame-ancestors 'self'

或完全禁止:

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-Optionsframe-ancestors

一句话记忆:

XSS 是“把坏脚本塞进你页面”,CSRF 是“借你的登录态发坏请求”,点击劫持是“让你以为点了这个,其实点了那个”。

7. 算法题:虚假进度条不是骗人,是体验建模

面试里问“虚假进度条”,通常不是让你写一个 setInterval 就结束。

它考的是:

  • 你能否把不确定耗时建模成用户可理解的反馈;
  • 你是否能处理成功、失败、超时、取消;
  • 你是否能避免进度条倒退、卡死、乱跳。

7.1 为什么需要假进度?

有些任务没有真实进度:

  • 后端长任务;
  • 文件转码;
  • AI 生成;
  • 报表导出;
  • 批量处理;
  • 多接口聚合。

前端只知道:

请求开始了
请求成功了
请求失败了

中间到底 30% 还是 70%,不知道。

如果页面什么都不展示,用户会焦虑;如果一直转圈,用户不知道还要等多久。假进度条的意义是:

用一个可控的视觉反馈告诉用户:系统还在工作,而且越来越接近完成。

7.2 核心模型:每次只走剩余距离的一部分

最常见的模型是“剩余量衰减”:

next = current + (target - current) * ratio

如果目标是 99,当前是 0,比例是 0.15:

  • 第一次走到 14.85;
  • 第二次走到 27.47;
  • 第三次走到 38.20;
  • 越往后剩余越少,增长越慢;
  • 最终接近 99,但不会自己到 100。

这很符合用户感知:

开始很快 -> 中间变慢 -> 快完成时卡住 -> 成功后直接满格

7.3 一个更完整的实现

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,
  };
}

使用:

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 真实项目里的细节

不要让进度倒退

如果你同时有真实进度和模拟进度,取较大值:

displayProgress = Math.max(fakeProgress, realProgress);

用户最讨厌看到 80% 突然回到 40%。

不要太早到 99%

如果接口可能要 30 秒,而你 3 秒就到 99%,用户会觉得“卡死了”。可以分阶段:

0 - 60:快
60 - 85:中
85 - 95:慢
95 - 99:非常慢
成功:100

失败时不要显示 100%

失败应该停在当前进度,然后给出明确错误状态。不要为了动画完整把失败也拉到 100%,这会误导用户。

取消时要清 timer

组件卸载、路由切换、用户取消任务时,要清掉定时器,否则会有内存泄漏或状态更新异常。

7.5 面试回答模板

假进度条适合后端没有真实进度的长任务。我的设计是用剩余量衰减模型,每次增加 (目标值 - 当前值) * 比例,让它开始快、后面慢,并停在 95 或 99 等待真实结果。接口成功后直接补到 100,失败则停止并显示错误,取消或组件卸载时清理定时器。如果有真实进度和模拟进度并存,要保证展示进度不倒退。

8. 把这些题串起来:高级前端的回答方式

经过上面的拆解,你会发现高级前端面试的关键不是“说得多”,而是“说得有层次”。

推荐回答结构:

先给结论
  -> 解释原理
  -> 给项目落地方式
  -> 补充风险和边界

比如问 Fetch 和 Axios:

结论:
Fetch 是原生 Promise API,Axios 是第三方请求库,业务系统里 Axios 的工程封装更完整。

原理:
Fetch 返回 Response,HTTP 4xx/5xx 不会自动 reject,需要手动判断 response.ok。

落地:
如果用 Fetch,我会封装统一的 http 方法,处理 JSON、错误、超时和鉴权;如果项目复杂,用 Axios 可以用拦截器、timeout、取消请求等能力。

边界:
跨域带 Cookie 不是前端一个配置能解决,还要后端 CORS 和 Cookie SameSite/Secure 配合。

比如问 Nginx:

结论:
前端项目常用 Nginx 托管 dist,同时做 SPA fallback 和 API 反向代理。

原理:
try_files 会按顺序查找真实文件,找不到就内部转发到 index.html,让前端路由接管。

落地:
index.html 不强缓存,带 hash 的静态资源长缓存;/api 代理到后端。

边界:
proxy_pass 尾斜杠会影响 URI 转发结果,线上配置要和后端接口路径一致。

比如问产品需求:

结论:
不建议注册阶段强制上传头像,除非头像是业务强必要信息。

原理:
注册流程核心目标是转化,每增加一步都会增加漏斗流失。

落地:
更推荐注册后渐进式引导完善头像;如果必须上传,就提供默认头像、预览、独立上传、失败重试。

边界:
弱网、权限弹窗、上传失败都要处理,不能让头像上传失败直接阻塞核心注册流程。

9. 复盘:这场面试真正提醒了什么?

这次面试最有价值的地方,不是发现“我还有几个题不会”,而是提醒我们:五年前端的能力边界已经比过去宽很多。

你不仅要会写页面,还要知道:

  • 页面为什么这样布局;
  • 动画为什么这样动;
  • 请求为什么这样失败;
  • Cookie 为什么没有带上;
  • 路由刷新为什么 404;
  • 静态资源为什么缓存错;
  • 容器为什么跑不起来;
  • 日志应该去哪看;
  • 需求会不会伤害转化;
  • 接口设计会不会放大安全风险;
  • 没有真实进度时怎么给用户稳定反馈。

真正的高级前端,是能把这些问题连成一条线的人:

用户体验
  -> 页面实现
  -> 请求链路
  -> 鉴权安全
  -> 构建部署
  -> 线上排障
  -> 产品取舍

如果你现在还觉得这些知识点有点多,不用慌。学习顺序可以这样排:

  1. 先把 CSS 和请求层吃透,因为这是每天都用的基本功;
  2. 再补 Nginx、Docker、Linux,因为它们决定你能不能独立上线;
  3. 然后补安全和鉴权,因为它们决定你写的系统有没有底线;
  4. 最后训练产品判断和算法建模,因为这是高级工程师和纯执行之间的分水岭。

面试不是判决书,它更像一次系统扫描。扫出问题不可怕,可怕的是扫完以后不整理、不复盘、不把经验沉淀成自己的知识结构。

这次“一面之缘”,如果能把它变成一张能力地图,那它就不只是一次面试,而是一次升级。

10. 附:面试前快速记忆卡片

CSS

  • 动画:@keyframes 定义状态,animation-* 控制播放。
  • 动画简写:时间值第一个是 duration,第二个是 delay,动画名建议放最后。
  • 优先动画 transformopacity
  • 渐变是“浏览器生成的图片”。
  • 文字渐变:背景渐变 + 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 排障:curlsspstaildffree

安全

  • XSS:坏脚本进页面执行。
  • CSRF:借浏览器自动带 Cookie 发请求。
  • Clickjacking:视觉欺骗,误导用户点击。
  • XSS 防御:输出编码、Sanitizer、HttpOnly、CSP。
  • CSRF 防御:CSRF Token、SameSite、Origin/Referer、避免 GET 副作用。
  • 点击劫持防御:X-Frame-Options、CSP frame-ancestors

产品和算法

  • 注册流程先看转化率,不要无脑加必填项。
  • 头像不是强必要时,优先注册后渐进式完善。
  • 假进度条:剩余量衰减,成功到 100,失败停止,取消清理。
  • 回答问题:结论 -> 原理 -> 落地 -> 风险边界。

参考资料


文章来源:https://www.cnblogs.com/zsnhweb/p/20061744
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:jacktools123@163.com进行投诉反馈,一经查实,立即删除!

标签:

上一篇:洛谷-P10786 [NOI2024] 百万富翁 题解
下一篇:没有了

相关文章

本站推荐

标签云