Skip to content

高级前端工程师面试题大全 ​

从实际项目出发,覆盖 JavaScript、Vue、工程化、系统设计等核心方向

基于全网热门面试题库(febobo/web-interview、lgwebdream/FE-Interview、Advanced-Frontend/Daily-Interview-Question 等)、当前学习文档体系及项目实战资料整理


目录 ​

第一篇:JavaScript 核心面试题 ​

  1. 基础进阶题
  2. 闭包与作用域
  3. this 指向与函数上下文
  4. 原型链与继承
  5. 异步编程(Promise/async/await)
  6. ES6+ 核心新特性
  7. 实际项目场景题

第二篇:Vue 面试题 ​

  1. Vue 2 vs Vue 3 核心差异
  2. 响应式原理详解
  3. Composition API vs Options API
  4. 组件通信方案
  5. 状态管理(Vuex vs Pinia)
  6. Vue Router 核心
  7. 性能优化实战
  8. 实际项目场景题

第三篇:工程化与架构 ​

  1. 构建工具(Webpack/Vite)
  2. 模块化与组件化
  3. 微前端架构
  4. 性能优化体系
  5. 前端安全

第四篇:项目经验与系统设计 ​

  1. 项目架构设计
  2. 难点与解决方案
  3. 系统设计题
  4. 基于当前知识框架的项目专项面试题
  5. 项目重难点与亮点表达
  6. 项目追问清单

第五篇:代码实现题 ​

  1. 手写实现
  2. 算法与数据结构


第一篇:JavaScript 核心面试题 ​


1.1 基础进阶题 ​

1.1.1 var / let / const 的区别(含暂时性死区) ​

面试考察点:理解作用域、变量提升、暂时性死区等核心概念

答案:

javascript
// 1. 作用域
var a = 1;      // 函数作用域
let b = 2;      // 块级作用域
const c = 3;    // 块级作用域

// 2. 变量提升
console.log(v); // undefined(var 被提升,默认值 undefined)
console.log(l); // ReferenceError(let 在暂时性死区)
var v = 1;
let l = 2;

// 3. 重复声明
var x = 1; var x = 2; // OK
let y = 1; let y = 2; // SyntaxError

// 4. const 本质
const obj = { a: 1 };
obj.a = 2;  // OK(引用不变即可)
// obj = {}; // TypeError

项目场景:在大型项目中统一使用 const 声明不变值,let 声明可变值,禁止使用 var,可以有效避免变量提升带来的隐式 bug。


1.1.2 数据类型检测的多种方式 ​

答案:

javascript
// 1. typeof(适合基本类型)
typeof "hello";     // "string"
typeof 42;          // "number"
typeof true;        // "boolean"
typeof undefined;   // "undefined"
typeof Symbol();    // "symbol"
typeof 42n;         // "bigint"
typeof null;        // "object" — 历史遗留 bug!
typeof [];          // "object"
typeof function(){};// "function"

// 2. instanceof(适合检测引用类型的具体类)
[] instanceof Array;     // true
[] instanceof Object;    // true

// 3. Object.prototype.toString.call(终极方案)
Object.prototype.toString.call([]);   // "[object Array]"
Object.prototype.toString.call(null); // "[object Null]"

// 4. Array.isArray(数组专用)
Array.isArray([]); // true

项目场景:处理 API 返回数据时,经常需要判断数据类型。使用 Object.prototype.toString.call 是最可靠的方案。


1.1.3 深浅拷贝的区别与实现 ​

面试题:如何实现深拷贝?有哪些注意事项?

答案:

javascript
// 浅拷贝
const shallow1 = { ...obj };
const shallow2 = Object.assign({}, obj);

// 深拷贝
// 方式一:JSON 序列化(有缺陷)
const deep1 = JSON.parse(JSON.stringify(obj));
// 缺陷:忽略 undefined、Symbol、函数、RegExp、Date 等

// 方式二:structuredClone(现代浏览器/Node 17+)
const deep2 = structuredClone(obj);

// 方式三:递归实现
function deepClone(obj, hash = new WeakMap()) {
  if (obj === null || typeof obj !== "object") return obj;
  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof RegExp) return new RegExp(obj);
  if (hash.has(obj)) return hash.get(obj); // 解决循环引用

  const result = Array.isArray(obj) ? [] : {};
  hash.set(obj, result);

  for (const key of Reflect.ownKeys(obj)) {
    result[key] = deepClone(obj[key], hash);
  }
  return result;
}

项目场景:在状态管理(Vuex/Pinia)中修改深层嵌套数据时,经常需要深拷贝来避免意外修改原数据。


1.1.4 == 与 === 的区别 + 类型转换规则 ​

答案:

javascript
// ===:严格相等,不进行类型转换
1 === "1";     // false
null === undefined; // false

// ==:抽象相等,会进行类型转换
1 == "1";       // true
null == undefined; // true  — 特殊规则
[] == false;    // true
"" == false;    // true

// 类型转换规则(ToPrimitive)
// 1. 对象先调用 valueOf(),再调用 toString()
// 2. null == undefined(特殊规则,结果为 true)
// 3. 数字与字符串比较时,字符串转数字
// 4. 布尔值与其他类型比较时,布尔值转数字

项目场景:实际开发中始终使用 ===,仅在判断 null/undefined 时可以使用 == 的简写:if (val == null) 等价于 if (val === null || val === undefined)。


1.2 闭包与作用域 ​

1.2.1 闭包的定义与应用场景 ​

面试题:什么是闭包?列举实际项目中的使用场景。

答案:

闭包是指有权访问另一个函数作用域中变量的函数。当一个函数在其定义的作用域之外执行时,它仍然可以访问该作用域中的变量。

实际项目场景:

javascript
// 场景1:数据私有化(封装)
function createCounter() {
  let count = 0;
  return {
    increment: () => ++count,
    decrement: () => --count,
    getCount: () => count
  };
}
// 项目中的 hook,计数器等

// 场景2:防抖与节流
function debounce(fn, delay = 300) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}

// 场景3:函数柯里化
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    }
    return (...args2) => curried(...args, ...args2);
  };
}

// 场景4:单例模式
function createSingleton() {
  let instance;
  return function() {
    return instance || (instance = {});
  };
}

1.2.2 闭包的内存泄漏问题 ​

面试题:闭包可能导致内存泄漏吗?如何避免?

答案:

javascript
// 问题:闭包长期持有外部变量,导致无法被 GC 回收
function setupHandler() {
  const largeData = new Array(1000000).fill("data");
  document.getElementById("btn").onclick = function() {
    console.log("clicked");
    // 这里引用了 largeData,即使函数中没有使用
  };
}

// 解决方式:
// 1. 及时清理引用
function setupHandler() {
  const largeData = new Array(1000000).fill("data");
  document.getElementById("btn").onclick = function() {
    console.log("clicked");
  };
  largeData = null; // 显式释放
}

// 2. 使用 WeakMap/WeakSet
const cache = new WeakMap();
// WeakMap 的键是弱引用,不会阻止 GC

项目场景:在 SPA 应用中,页面切换时未销毁的组件中如果存在闭包引用,容易造成内存泄漏。Vue 3 的 onBeforeUnmount / onUnmounted 就是用来做清理的。


1.3 this 指向与函数上下文 ​

1.3.1 this 的四种绑定规则 ​

面试题:请说出 JavaScript 中 this 的指向规则。

答案:

javascript
// 规则1:默认绑定(非严格模式 -> window,严格模式 -> undefined)
function show() { console.log(this); }
show(); // window(非严格模式)

// 规则2:隐式绑定(谁调用指向谁)
const obj = { name: "obj", show() { console.log(this.name); } };
obj.show(); // "obj"

// 规则3:显式绑定(call/apply/bind)
function greet() { console.log(`Hello, ${this.name}`); }
greet.call({ name: "Alice" });  // "Hello, Alice"
greet.apply({ name: "Bob" });   // "Hello, Bob"
const bound = greet.bind({ name: "Charlie" });
bound();                         // "Hello, Charlie"

// 规则4:new 绑定
function Person(name) { this.name = name; }
const p = new Person("Alice");
console.log(p.name); // "Alice"

// 优先级:new > 显式 > 隐式 > 默认

1.3.2 箭头函数的 this ​

面试题:箭头函数的 this 和普通函数的 this 有什么区别?

答案:

javascript
// 箭头函数没有自己的 this,继承外层作用域的 this
const obj = {
  name: "obj",
  normal: function() {
    console.log(this.name); // "obj"
  },
  arrow: () => {
    console.log(this.name); // undefined(继承 window 的 this)
  },
  nested() {
    const innerArrow = () => {
      console.log(this.name); // "obj"(继承 nested 的 this)
    };
    innerArrow();
  }
};

// 应用场景:定时器/事件回调中保持 this
class Timer {
  constructor() { this.seconds = 0; }
  start() {
    // 使用箭头函数避免 this 丢失
    setInterval(() => {
      this.seconds++; // 这里的 this 指向 Timer 实例
    }, 1000);
  }
}

1.3.3 call / apply / bind 的区别与手写实现 ​

面试题:实现一个 bind 方法。

答案:

javascript
Function.prototype.myBind = function(context, ...args) {
  const fn = this;
  return function(...args2) {
    // 区分 new 调用和普通调用
    if (this instanceof fn) {
      // new 调用
      return new fn(...args, ...args2);
    }
    return fn.apply(context, [...args, ...args2]);
  };
};

// 使用示例
const obj = { name: "test" };
function log(prefix) { console.log(prefix + this.name); }
const bound = log.myBind(obj, "Name: ");
bound(); // "Name: test"

1.4 原型链与继承 ​

1.4.1 原型链原理 ​

面试题:请描述 JavaScript 的原型链机制。

答案:

javascript
// 每个对象都有一个 __proto__ 属性(隐式原型)
// 每个函数都有一个 prototype 属性(显式原型)

function Person(name) {
  this.name = name;
}
Person.prototype.sayHello = function() {
  console.log(`Hello, I am ${this.name}`);
};

const alice = new Person("Alice");
alice.sayHello(); // "Hello, I am Alice"

// 原型链查找:
// alice -> Person.prototype -> Object.prototype -> null
console.log(alice.__proto__ === Person.prototype);        // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__);                  // null

1.4.2 ES6 Class 继承与原型链继承的对比 ​

面试题:ES6 的 class 继承是如何实现的?

答案:

javascript
class Animal {
  constructor(name) { this.name = name; }
  speak() { console.log(`${this.name} makes a sound`); }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name);  // 必须先调用 super
    this.breed = breed;
  }
  speak() {
    super.speak(); // 调用父类方法
    console.log(`${this.name} barks`);
  }
}

// 本质:class 是语法糖
// Dog.prototype.__proto__ === Animal.prototype
// Reflect.construct + 寄生组合式继承

1.4.3 多种继承方式对比 ​

面试题:请说说 JavaScript 中有哪些继承方式,各自的优缺点。

答案:

javascript
// 1. 原型链继承(引用类型共享问题)
function Parent() { this.colors = ["red"]; }
function Child() {}
Child.prototype = new Parent();
// 缺点:所有实例共享 colors 数组

// 2. 构造函数继承(方法无法复用)
function Child() {
  Parent.call(this);
}

// 3. 组合继承(最常用)
function Child(name) {
  Parent.call(this, name); // 第一次调用 Parent
}
Child.prototype = new Parent(); // 第二次调用 Parent

// 4. 寄生组合式继承(完美方案)
function inherit(child, parent) {
  child.prototype = Object.create(parent.prototype);
  child.prototype.constructor = child;
}

// 5. ES6 class extends(现代标准)
class Child extends Parent {}

1.5 异步编程(Promise/async/await) ​

1.5.1 Promise 的三种状态及链式调用 ​

面试题:手写 Promise.all / Promise.race / Promise.allSettled。

答案:

javascript
// 手写 Promise.all
Promise.myAll = function(promises) {
  return new Promise((resolve, reject) => {
    const results = [];
    let completed = 0;
    if (promises.length === 0) resolve(results);
    promises.forEach((p, i) => {
      Promise.resolve(p).then(val => {
        results[i] = val;
        completed++;
        if (completed === promises.length) resolve(results);
      }).catch(reject);
    });
  });
};

// 手写 Promise.race
Promise.myRace = function(promises) {
  return new Promise((resolve, reject) => {
    promises.forEach(p => {
      Promise.resolve(p).then(resolve).catch(reject);
    });
  });
};

// 手写 Promise.allSettled
Promise.myAllSettled = function(promises) {
  return Promise.all(promises.map(p =>
    Promise.resolve(p).then(
      value => ({ status: "fulfilled", value }),
      reason => ({ status: "rejected", reason })
    )
  ));
};

项目场景:仪表盘页面需要同时请求多个接口,使用 Promise.all 并行请求并等待所有结果;需要容错时使用 Promise.allSettled。

1.5.2 async/await 的错误处理模式 ​

面试题:实际项目中如何优雅地处理 async/await 的错误?

答案:

javascript
// 方式一:try/catch(推荐)
async function fetchUser(id) {
  try {
    const response = await fetch(`/api/user/${id}`);
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return await response.json();
  } catch (error) {
    console.error("请求失败:", error);
    throw error; // 继续向上抛
  }
}

// 方式二:包装函数(Go 风格)
function to(promise) {
  return promise.then(data => [null, data]).catch(err => [err, null]);
}

async function main() {
  const [err, user] = await to(fetchUser(1));
  if (err) {
    // 处理错误
    return;
  }
  console.log(user.name);
}

// 方式三:Promise.all 的错误隔离
async function getDashboardData() {
  const results = await Promise.allSettled([
    fetchUser(1),
    fetchOrders(1),
    fetchNotifications(1)
  ]);
  const [userResult, ordersResult, notifResult] = results;
  // 分别处理每个结果的成功/失败
}

1.5.3 事件循环(Event Loop)与宏任务/微任务 ​

面试题:以下代码的输出顺序是什么?

javascript
console.log("1");

setTimeout(() => console.log("2"), 0);

Promise.resolve().then(() => console.log("3"));

Promise.resolve().then(() => {
  console.log("4");
  setTimeout(() => console.log("5"), 0);
});

console.log("6");

// 答案:1, 6, 3, 4, 2, 5
// 解释:同步代码 -> 清空微任务队列 -> 取一个宏任务执行

项目场景:在 Vue 的 nextTick 中执行 DOM 操作,利用的就是微任务机制。

1.5.4 异步并发控制 ​

面试题:如何实现一个限制最大并发数的异步调度器?

答案:

javascript
class Scheduler {
  constructor(maxConcurrency = 2) {
    this.max = maxConcurrency;
    this.running = 0;
    this.queue = [];
  }

  add(promiseFactory) {
    return new Promise((resolve, reject) => {
      this.queue.push({ factory: promiseFactory, resolve, reject });
      this.run();
    });
  }

  run() {
    while (this.running < this.max && this.queue.length) {
      const { factory, resolve, reject } = this.queue.shift();
      this.running++;
      factory()
        .then(resolve, reject)
        .finally(() => {
          this.running--;
          this.run();
        });
    }
  }
}

// 使用场景:控制大文件上传或批量 API 请求的并发数
const scheduler = new Scheduler(3);
for (let i = 0; i < 10; i++) {
  scheduler.add(() => fetch(`/api/data/${i}`));
}

1.6 ES6+ 核心新特性 ​

1.6.1 Map / Set / WeakMap / WeakSet 的区别 ​

面试题:WeakMap 的应用场景是什么?

答案:

javascript
// Map vs Object
const map = new Map();
map.set("key", "value");
map.get("key");
map.has("key");
map.delete("key");

// WeakMap:键必须是对象,弱引用
const wm = new WeakMap();
let obj = { data: "temp" };
wm.set(obj, "metadata");
obj = null; // 对象被 GC 回收,WeakMap 中的键值对也被自动移除

// 实际应用:Vue 3 响应式系统中,使用 WeakMap 存储对象的依赖关系
// 应用:存储 DOM 节点的绑定数据,DOM 移除后自动清理
const domData = new WeakMap();
const btn = document.getElementById("btn");
domData.set(btn, { clicks: 0 });
btn.onclick = function() {
  const data = domData.get(btn);
  data.clicks++;
};
// 当 btn 从 DOM 中移除后,domData 中的条目自动被 GC 清理

1.6.2 Symbol 的作用与使用场景 ​

答案:

javascript
// 1. 创建唯一键,防止属性名冲突
const NAME = Symbol("name");
const obj = { [NAME]: "Alice" };

// 2. 定义常量
const Colors = {
  RED: Symbol("red"),
  GREEN: Symbol("green"),
  BLUE: Symbol("blue")
};

// 3. 自定义迭代器
const iterable = {
  [Symbol.iterator]: function*() {
    yield 1; yield 2; yield 3;
  }
};

1.6.3 Proxy 的应用场景 ​

面试题:Vue 3 为什么要从 Object.defineProperty 换成 Proxy?

答案:

javascript
// Proxy 可以拦截整个对象的操作
const handler = {
  get(target, key) { /* 拦截读取 */ },
  set(target, key, value) { /* 拦截设置 */ },
  deleteProperty(target, key) { /* 拦截删除 */ },
  has(target, key) { /* 拦截 in 操作 */ }
};

// 实际应用:数据校验
const validator = {
  set(target, key, value) {
    if (key === "age" && typeof value !== "number") {
      throw new TypeError("age must be a number");
    }
    target[key] = value;
    return true;
  }
};

// 实际应用:日志追踪
function createLogger(target) {
  return new Proxy(target, {
    get(obj, key) {
      console.log(`GET ${key}: ${obj[key]}`);
      return obj[key];
    },
    set(obj, key, value) {
      console.log(`SET ${key}: ${value}`);
      obj[key] = value;
      return true;
    }
  });
}

1.7 实际项目场景题 ​

Q1:长列表渲染优化 ​

问题:项目中有一个包含 10 万条数据的列表需要渲染,你会怎么做?

考察点:虚拟滚动、时间分片、Web Worker

答案:

  • 虚拟滚动(推荐):只渲染可视区域内的 DOM 节点。可以使用 vue-virtual-scroller 或 react-window
  • 时间分片:使用 requestAnimationFrame 分批渲染
  • Web Worker:在 Worker 中处理数据计算,主线程只负责渲染
javascript
// 时间分片示例
function renderChunk(list, chunkSize = 100) {
  let index = 0;
  function processChunk() {
    const chunk = list.slice(index, index + chunkSize);
    // 渲染当前 chunk
    index += chunkSize;
    if (index < list.length) {
      requestAnimationFrame(processChunk);
    }
  }
  requestAnimationFrame(processChunk);
}

Q2:前端权限控制 ​

问题:项目中如何实现前端权限控制(按钮级权限)?

考察点:路由权限、指令权限、权限状态管理

答案:

  • 路由级权限:动态添加路由(router.addRoute)
  • 按钮级权限:自定义指令 + 权限 store
  • 权限数据:登录后后端返回权限列表,存储在 Pinia/Vuex 中
javascript
// Vue 3 自定义权限指令
const vPermission = {
  mounted(el, binding) {
    const { value } = binding;
    const permissions = usePermissionStore();
    if (value && !permissions.has(value)) {
      el.parentNode?.removeChild(el);
    }
  }
};

// 动态路由
router.beforeEach(async (to) => {
  const store = usePermissionStore();
  if (!store.loaded) {
    const routes = await store.fetchRoutes();
    routes.forEach(r => router.addRoute(r));
    return to.fullPath; // 重新导航
  }
});

Q3:大文件上传 ​

问题:如何实现大文件上传,支持断点续传?

考察点:文件切片、MD5 计算、进度监控、并发控制

答案:

  • 切片:将文件按固定大小(如 5MB)切片
  • Hash 计算:使用 SparkMD5 计算文件唯一标识
  • 断点续传:先请求已上传分片列表,只上传未上传的
  • 并发控制:限制同时上传的分片数(如 3 个)
javascript
async function uploadFile(file) {
  const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB
  const chunks = [];
  for (let start = 0; start < file.size; start += CHUNK_SIZE) {
    chunks.push(file.slice(start, start + CHUNK_SIZE));
  }

  // 计算文件 hash
  const fileHash = await calculateHash(file);

  // 查询已上传分片
  const uploaded = await fetch(`/api/check/${fileHash}`).then(r => r.json());

  // 并发上传未上传的分片
  const scheduler = new Scheduler(3);
  const tasks = chunks.map((chunk, index) => {
    if (uploaded.includes(index)) return; // 已上传,跳过
    return scheduler.add(() => {
      const formData = new FormData();
      formData.append("chunk", chunk);
      formData.append("index", index);
      formData.append("hash", fileHash);
      return fetch("/api/upload", { method: "POST", body: formData });
    });
  });

  await Promise.all(tasks);
  // 通知合并
  await fetch("/api/merge", {
    method: "POST",
    body: JSON.stringify({ hash: fileHash, chunks: chunks.length })
  });
}

Q4:前端错误监控 ​

问题: 如何设计前端错误监控系统?

考察点:错误分类、捕获方式、上报策略

答案:

  • JS 错误:window.onerror + window.addEventListener("unhandledrejection")
  • 资源加载错误:window.addEventListener("error", handler, true)
  • Vue 错误:app.config.errorHandler
  • 请求错误:封装 fetch/axios 拦截器
  • 上报策略:错误去重、批量上报、采样率控制

第二篇:Vue 面试题 ​


2.1 Vue 2 vs Vue 3 核心差异 ​

Q1:Vue 3 相比 Vue 2 有哪些核心优化? ​

答案:

维度Vue 2Vue 3
响应式系统Object.defineProperty(递归遍历)Proxy(惰性代理)
虚拟 DOM全量比较PatchFlag + Block Tree
静态提升每次都重新创建静态节点提升到渲染函数外
事件缓存每次重新创建缓存事件处理函数
Tree-shaking不支持原生支持
TypeScript通过装饰器支持原生 TS 重写
包体积~30KB(gzip)~13KB(gzip)

Vue 3 编译优化细节:

vue
<!-- 模板 -->
<div>
  <span>静态文本</span>
  <span>{{ dynamic }}</span>
</div>

<!-- Vue 3 编译结果(简化) -->
// 静态节点提升到函数外部,只创建一次
const _hoisted_1 = _createVNode("span", null, "静态文本");

function render(_ctx, _cache) {
  return _createVNode("div", null, [
    _hoisted_1,
    _createVNode("span", null, _toDisplayString(_ctx.dynamic), 1 /* TEXT */)
  ]);
}
// PatchFlag = 1(动态文本),只更新文本内容,无需全量 diff

Q2:Vue 3 的 Fragment、Teleport、Suspense 是什么? ​

答案:

vue
<!-- Fragment:支持多根节点 -->
<template>
  <header>...</header>
  <main v-bind="$attrs">...</main>
  <footer>...</footer>
</template>

<!-- Teleport:将子节点渲染到指定 DOM 位置 -->
<template>
  <Teleport to="body">
    <div class="modal">模态框</div>
  </Teleport>
</template>

<!-- Suspense:处理异步组件加载状态 -->
<template>
  <Suspense>
    <template #default>
      <AsyncComponent />
    </template>
    <template #fallback>
      <Loading />
    </template>
  </Suspense>
</template>

项目场景:

  • Teleport:全局模态框、通知、工具提示
  • Suspense:异步组件加载时的 loading 状态管理

2.2 响应式原理详解 ​

Q1:Vue 2 和 Vue 3 的响应式实现区别? ​

答案:

Vue 2(Object.defineProperty):

javascript
// 递归遍历所有属性,劫持 getter/setter
function defineReactive(obj, key, val) {
  observe(val); // 递归子属性
  Object.defineProperty(obj, key, {
    get() {
      Dep.depend(); // 收集依赖
      return val;
    },
    set(newVal) {
      if (newVal === val) return;
      val = newVal;
      Dep.notify(); // 触发更新
    }
  });
}

Vue 3(Proxy):

javascript
// 惰性代理,访问时才递归
function reactive(target) {
  if (!isObject(target)) return target;
  return new Proxy(target, {
    get(target, key, receiver) {
      track(target, key); // 收集依赖
      const result = Reflect.get(target, key, receiver);
      return isObject(result) ? reactive(result) : result; // 惰性递归
    },
    set(target, key, value, receiver) {
      const oldValue = target[key];
      const result = Reflect.set(target, key, value, receiver);
      if (oldValue !== value) trigger(target, key); // 触发更新
      return result;
    }
  });
}

Q2:Vue 2 中为什么无法检测数组索引和长度的变化? ​

答案:

Vue 2 重写了数组的 7 个方法(push/pop/shift/unshift/splice/sort/reverse)来触发更新,但:

  1. 数组索引:arr[0] = newVal 无法被 Object.defineProperty 检测到
  2. 数组长度:arr.length = 0 无法被检测到
javascript
// Vue 2 的数组响应式处理
const arrayMethods = Object.create(Array.prototype);
["push", "pop", "shift", "unshift", "splice", "sort", "reverse"].forEach(method => {
  const original = Array.prototype[method];
  Object.defineProperty(arrayMethods, method, {
    value: function(...args) {
      const result = original.apply(this, args);
      ob.dep.notify(); // 手动触发更新
      return result;
    }
  });
});

Vue 3 中这些都不是问题,因为 Proxy 可以直接拦截 set 和 deleteProperty 操作。

Q3:Vue 3 的 ref 和 reactive 的区别,什么时候用哪个? ​

答案:

javascript
import { ref, reactive, shallowRef, shallowReactive } from "vue";

// ref:用于基本类型 + 可整体替换的对象
const count = ref(0);
const user = ref({ name: "Alice" });
// 可以整体替换
user.value = { name: "Bob" };

// reactive:用于对象,不可整体替换
const state = reactive({ count: 0, user: { name: "Alice" } });
// 不可这样:state = reactive({})

// 官方推荐:
// - 基本类型 + 需要重新赋值 → ref
// - 复杂对象,无需整体替换 → reactive
// - 统一使用 ref(方便记忆,模板中自动解包)

// 性能优化:浅层响应
const shallow = shallowRef({ deep: { data: "不追踪" } });
// 只有 .value 变化才触发,深层变化不触发

2.3 Composition API vs Options API ​

Q1:Composition API 相比 Options API 有什么优势? ​

答案:

markdown
Options API 的问题:
- 相关逻辑分散在 data/computed/methods/watch/lifecycle 中
- 逻辑复用困难(Mixins 有命名冲突、来源不清等问题)
- TypeScript 支持弱
- 大型组件难以维护

Composition API 的优势:
- 按功能逻辑组织代码,而不是按选项类型
- 逻辑复用更优雅(Composables 函数式)
- 更好的 TypeScript 类型推导
- tree-shaking 友好,按需引入

代码对比:

vue
<!-- Options API:相关逻辑分散 -->
<script>
export default {
  data() { return { searchQuery: "", results: [], loading: false }; },
  computed: { hasResults() { return this.results.length > 0; } },
  watch: {
    searchQuery: {
      handler: debounce(function(q) { this.search(q); }, 500),
      immediate: true
    }
  },
  methods: { async search(q) { this.loading = true; this.results = await fetchResults(q); this.loading = false; } }
};
</script>

<!-- Composition API:按功能组织 -->
<script setup>
import { ref, computed, watch } from "vue";
import { useDebounce } from "@/composables/useDebounce";

const searchQuery = ref("");
const results = ref([]);
const loading = ref(false);
const hasResults = computed(() => results.value.length > 0);

watch(useDebounce(searchQuery, 500), async (q) => {
  loading.value = true;
  results.value = await fetchResults(q);
  loading.value = false;
});
</script>

Q2:script setup 相比普通 setup 函数有什么优势? ​

答案:

markdown
1. 更简洁的语法:无需 export default,顶层的变量/函数自动暴露给模板
2. 更少的 boilerplate:无需 return 语句
3. 更好的 IDE 支持:直接导入 html/css 等资源
4. 更好的类型推导:defineProps / defineEmits 泛型参数
5. defineExpose 控制暴露给父组件的内容

2.4 组件通信方案 ​

Q1:列举 Vue 中所有的组件通信方式,并说明适用场景 ​

答案:

方式方向适用场景
props / emit父子双向最基本的通信方式
v-model父子双向表单类组件封装
ref + defineExpose父调子父组件需要调用子组件方法
$attrs / $listeners祖孙透传高阶组件(HOC)透传属性
provide / inject祖先到任意后代主题、语言、配置等全局注入
Event Bus任意组件简单场景(Vue 3 不推荐)
Pinia / Vuex全局状态复杂跨组件/跨页面状态
slots父子模板插槽布局和模板复用

Vue 3 的 provide/inject 响应式:

javascript
// 父组件
const count = ref(0);
provide("count", count);
provide("updateCount", (val) => { count.value = val; });

// 子组件
const count = inject("count");
const update = inject("updateCount");

Q2:Vue 3 中 $attrs 的变化 ​

答案:

markdown
Vue 2:$attrs 不含 class/style,$listeners 独立
Vue 3:$listeners 合并到 $attrs,包含 class/style

多根节点时需要手动绑定 $attrs:
vue
<!-- 多根节点组件 -->
<template>
  <header>...</header>
  <main v-bind="$attrs">...</main>  <!-- 手动指定继承 $attrs 的节点 -->
  <footer>...</footer>
</template>

2.5 状态管理(Vuex vs Pinia) ​

Q1:Pinia 相比 Vuex 有哪些优势?为什么 Pinia 是推荐方案? ​

答案:

markdown
1. 移除 Mutation:直接修改 state,无需 commit,开发体验更好
2. 更好的 TypeScript 支持:自动推断类型
3. 更轻量:~1KB vs Vuex 的 ~10KB
4. 隐式模块化:每个 store 独立 define,无需命名空间
5. Composition API 风格:支持 options API 和 composition API 两种写法
6. Devtools 支持:与 Vuex 同级别
7. 支持 Vue 2 和 Vue 3:同一 API,不同版本

代码对比:

javascript
// Vuex(需要 mutation + action)
const store = new Vuex.Store({
  state: { count: 0 },
  mutations: { increment(state) { state.count++; } },
  actions: { incrementAsync({ commit }) { setTimeout(() => commit("increment"), 1000); } }
});
store.commit("increment");       // 同步:commit mutation
store.dispatch("incrementAsync"); // 异步:dispatch action

// Pinia(直接修改)
const store = defineStore("counter", () => {
  const count = ref(0);
  function increment() { count.value++; }
  async function incrementAsync() {
    await delay(1000);
    count.value++;
  }
  return { count, increment, incrementAsync };
});
store.count++;         // 直接修改!
store.increment();     // 或者调用 action

Q2:Pinia 的 storeToRefs 的作用是什么? ​

答案:

javascript
import { useCounterStore } from "@/stores/counter";
import { storeToRefs } from "pinia";

const store = useCounterStore();

// 直接解构会失去响应性
const { count } = store; // 非响应式!

// 使用 storeToRefs 保持响应性
const { count, name } = storeToRefs(store); // 响应式

// 方法可以直接解构
const { increment, reset } = store;

2.6 Vue Router 核心 ​

Q1:Vue Router 3 和 4 的主要区别 ​

答案:

特性Vue Router 3Vue Router 4
创建方式new Router()createRouter()
历史模式mode: "history"history: createWebHistory()
next 参数必须调用可选,推荐返回路径
Composition API不支持useRouter()/useRoute()
TS 支持弱原生支持
导航故障有限处理完整导航故障对象
javascript
// Vue Router 4 新特性:导航故障
const result = await router.push("/login");
if (result === NavigationFailureType.redirected) {
  // 处理重定向
}

Q2:动态路由如何实现权限控制? ​

答案:

javascript
router.beforeEach(async (to, from) => {
  const store = usePermissionStore();

  // 1. 未登录重定向到登录页
  if (to.meta.requiresAuth && !store.isLoggedIn) {
    return { path: "/login", query: { redirect: to.fullPath } };
  }

  // 2. 首次登录,获取权限并动态添加路由
  if (store.isLoggedIn && !store.routesLoaded) {
    const userRoutes = await store.fetchUserRoutes();
    userRoutes.forEach(route => {
      router.addRoute(route); // 动态添加路由
    });
    store.routesLoaded = true;
    return to.fullPath; // 重新导航
  }
});

2.7 性能优化实战 ​

Q1:Vue 项目性能优化可以从哪些方面入手? ​

答案:

markdown
1. 代码层面:
   - 使用 v-show 替代频繁切换的 v-if
   - 使用 computed 缓存计算结果
   - v-for 绑定唯一 key
   - 合理使用 keep-alive 缓存组件
   - 函数式组件(无状态组件)

2. 打包优化:
   - 路由懒加载 const User = () => import("@/views/User.vue")
   - 第三方库按需引入(Element Plus、lodash-es)
   - 使用 Vite 替代 Webpack(开发模式)
   - gzip / brotli 压缩

3. Vue 3 特有优化:
   - shallowRef / shallowReactive 浅层响应
   - markRaw 标记静态数据
   - v-memo 指令(Vue 3.2+)
   - Teleport 避免深层 DOM

4. 运行时优化:
   - 虚拟滚动(大数据列表)
   - Object.freeze() 冻结纯静态数据
   - 避免过深的 watcher
   - 及时清理定时器和事件监听

Q2:keep-alive 的原理和使用场景 ​

答案:

vue
<!-- 缓存动态组件 -->
<KeepAlive :max="10">
  <component :is="currentTab" />
</KeepAlive>

<!-- 缓存路由组件 -->
<router-view v-slot="{ Component }">
  <KeepAlive :include="["Home", "About"]">
    <component :is="Component" />
  </KeepAlive>
</router-view>

原理:keep-alive 将缓存的组件实例存储在 cache 对象中,组件切换时不会销毁,而是从缓存中复用。LRU 策略控制最大缓存数量。


2.8 实际项目场景题 ​

Q1:设计一个通用表单组件 ​

问题:项目中有大量表单页面,如何设计一个通用、可扩展的表单组件?

答案:

vue
<template>
  <form @submit.prevent="handleSubmit">
    <FormField
      v-for="field in schema"
      :key="field.name"
      :field="field"
      v-model="formData[field.name]"
      :error="errors[field.name]"
    />
    <slot name="actions">
      <button type="submit">提交</button>
    </slot>
  </form>
</template>

<script setup>
const props = defineProps({
  schema: { type: Array, required: true },
  // schema 结构:
  // [{ name: "username", label: "用户名", type: "input", rules: [...] }]
});

const formData = reactive({});
const errors = ref({});

const { schema } = toRefs(props);

// 初始化表单数据
watch(schema, () => {
  schema.value.forEach(field => {
    formData[field.name] ??= field.default ?? "";
  });
}, { immediate: true });

async function handleSubmit() {
  // 校验逻辑
  const valid = await validate(formData);
  if (!valid) return;
  emit("submit", { ...formData });
}
</script>

考察点:组件抽象能力、schema 驱动设计、表单校验、v-model 双向绑定

Q2:SSR 与 CSR 的选择 ​

问题:你们项目的性能瓶颈在哪里?如何判断是否需要 SSR?

答案:

markdown
需要 SSR 的场景:
- SEO 要求高(电商、内容网站)
- 首屏加载速度要求高
- 社交分享需要预览

不需要 SSR 的场景:
- 后台管理系统(B 端)
- 需要用户登录的 SaaS 应用
- 交互复杂、动画多的应用

SSR 的代价:
- 服务器成本增加(需要 Node.js 服务器)
- 开发复杂度增加
- 部分浏览器 API 不可用
- TTFB 变慢

推荐方案:
- Nuxt 3(Vue 生态系统中的 SSR 方案)
- SSG(静态生成)是 SSR 的轻量替代方案

Q3:Vue 3 项目中如何做状态管理选型? ​

问题:一个中大型项目,如何选择状态管理方案?

答案:

markdown
小型项目(< 5 人):
- 简单共享:provide/inject + ref
- 或直接使用 Pinia

中型项目(10-20 人):
- Pinia(推荐)
- 模块化 store,按业务拆分

大型项目(50+ 人):
- Pinia + 模块化分层
- 配合 normalized 数据管理(类似 Vuex ORM)
- 考虑使用 Zustand / XState 处理复杂状态机

选型决策流程:
简单场景(props + emit + provide/inject)
    |
    v
中等复杂(Pinia)
    |
    v
复杂状态流(Pinia + 中间件 / XState)

第三篇:工程化与架构 ​


3.1 构建工具(Webpack/Vite) ​

Q1:Vite 的工作原理是什么?为什么比 Webpack 快? ​

答案:

markdown
Webpack 的构建流程:
启动 Dev Server → 打包所有模块 → 生成 bundle → 浏览器刷新
问题:冷启动慢、热更新慢,项目越大越明显

Vite 的构建流程:
启动 Dev Server → 按需编译(原生 ESM)→ 浏览器实时请求
javascript
// Vite 开发模式:
// 1. 使用 esbuild 预构建依赖(Go 编写,比 TS/JS 快 10-100 倍)
// 2. 原生 ESM 加载源码,无需打包
// 3. 按需编译,只编译浏览器请求的文件
// 4. 利用浏览器缓存,模块级别的热更新

// Vite 生产模式:
// 使用 Rollup 打包(成熟、插件丰富、支持 Tree-shaking)

Webpack vs Vite 核心差异:

维度WebpackVite
冷启动全量打包(分钟级)启动即用(毫秒级)
热更新模块编译(秒级)文件级 ESM(毫秒级)
构建自研打包生产用 Rollup
配置复杂度较高较低
生态丰富成熟快速成长

Q2:Webpack 的 Loader 和 Plugin 的区别 ​

答案:

javascript
// Loader:文件转换器,处理模块源文件
// 作用:将文件从 A 格式转为 B 格式(如.ts → .js, .scss → .css)
module.exports = {
  module: {
    rules: [
      { test: /\.ts$/, use: "ts-loader" },
      { test: /\.scss$/, use: ["style-loader", "css-loader", "sass-loader"] }
    ]
  }
};

// Plugin:扩展 Webpack 功能,在构建过程中注入额外行为
// 作用:打包优化、资源管理、环境变量注入
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
  plugins: [
    new HtmlWebpackPlugin({ template: "./index.html" })
  ]
};

// 区别总结:
// Loader → 处理文件(文件级别)
// Plugin → 扩展功能(生命周期级别)

Q3:Webpack 构建优化策略 ​

答案:

javascript
module.exports = {
  // 1. 多线程编译
  module: {
    rules: [
      { test: /\.js$/, use: "thread-loader" } // 使用 Worker 池
    ]
  },

  // 2. 缓存
  cache: { type: "filesystem" }, // 持久化缓存

  // 3. 代码分割
  optimization: {
    splitChunks: {
      chunks: "all",
      cacheGroups: {
        vendor: { test: /[\\/]node_modules[\\/]/, name: "vendor" },
        common: { minChunks: 2, name: "common" }
      }
    }
  },

  // 4. 摇树优化
  optimization: { usedExports: true },

  // 5. 压缩
  optimization: {
    minimizer: [new TerserPlugin({ parallel: true })]
  }
};

3.2 模块化与组件化 ​

Q1:ES Module vs CommonJS 的区别 ​

答案:

javascript
// CommonJS(Node.js 传统方式)
const fs = require("fs");          // 动态导入(运行时)
module.exports = { add };          // 导出值拷贝

// ES Module(标准方式)
import fs from "fs";               // 静态导入(编译时)
import("./module.js");             // 动态导入(运行时)
export const add = (a, b) => a + b; // 导出引用绑定

// 核心差异
// - ESM 静态分析 -> Tree-shaking 支持
// - ESM 异步加载 -> 浏览器原生支持
// - CJS 动态同步 -> 适合 Node.js
// - ESM 导出的是实时绑定,CJS 导出的是值的拷贝

Q2:Modular Monorepo(模块化单体仓库)的优缺点 ​

答案:

markdown
优点:
- 代码复用简单,共享组件/工具库
- 统一构建和发布流程
- 原子性提交(跨包改动一次提交)
- 团队协作更透明

缺点:
- 仓库体积大,git 操作慢
- 权限管理粒度粗
- 构建需要更复杂的工具链

工具:
- Nx / Turborepo(构建编排)
- pnpm Workspaces / Yarn Workspaces
- Changesets(版本管理和发布)

3.3 微前端架构 ​

Q1:微前端的核心实现方式有哪些? ​

答案:

markdown
主流方案对比:

1. iframe:
   - 优点:隔离彻底、技术无关
   - 缺点:通信复杂、SEO 差、性能开销
   - 适用:简单集成、三方系统嵌入

2. Web Components(Custom Elements + Shadow DOM):
   - 优点:原生支持、样式隔离
   - 缺点:浏览器兼容性、生态不成熟

3. 基座模式(Single-SPA / qiankun):
   - 优点:应用隔离、独立部署、技术无关
   - 缺点:接入成本高、沙箱性能开销
   - 代表:qiankun(基于 Single-SPA)

4. 模块联邦(Module Federation, Webpack 5):
   - 优点:运行时加载、共享依赖
   - 缺点:Webpack 绑定、版本管理复杂
javascript
// qiankun 示例
import { registerMicroApps, start } from "qiankun";

registerMicroApps([
  {
    name: "vue-app",
    entry: "//localhost:8081",
    container: "#sub-app-container",
    activeRule: "/vue",
    props: { userInfo: { id: 1 } }
  },
  {
    name: "react-app",
    entry: "//localhost:8082",
    container: "#sub-app-container",
    activeRule: "/react"
  }
]);

start({ sandbox: { experimentalStyleIsolation: true } });

3.4 性能优化体系 ​

Q1:前端性能优化的核心指标和监控方式 ​

答案:

markdown
核心指标(Web Vitals):
- LCP(Largest Contentful Paint)< 2.5s — 最大内容绘制
- FID(First Input Delay)< 100ms — 首次输入延迟
- CLS(Cumulative Layout Shift)< 0.1 — 累计布局偏移
- TTFB(Time to First Byte)< 800ms — 首字节时间
- FCP(First Contentful Paint)< 1.8s — 首次内容绘制

监控方式:
1. Performance API(浏览器原生)
2. PerformanceObserver(监听 LCP/CLS/FID)
3. 自行上报或使用开源方案(Sentry、Web Vitals)
javascript
// 使用 PerformanceObserver 监控
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.entryType === "largest-contentful-paint") {
      console.log("LCP:", entry.startTime);
    }
    if (entry.entryType === "first-input") {
      console.log("FID:", entry.processingStart - entry.startTime);
    }
  }
});
observer.observe({ type: "largest-contentful-paint", buffered: true });
observer.observe({ type: "first-input", buffered: true });

Q2:图片优化策略 ​

答案:

html
<!-- 1. 响应式图片 -->
<img
  srcset="photo-320w.jpg 320w, photo-640w.jpg 640w, photo-1280w.jpg 1280w"
  sizes="(max-width: 320px) 280px, (max-width: 640px) 600px, 1200px"
  src="photo-1280w.jpg"
  loading="lazy"
  decoding="async"
/>

<!-- 2. 使用 WebP/AVIF -->
<picture>
  <source type="image/avif" srcset="photo.avif">
  <source type="image/webp" srcset="photo.webp">
  <img src="photo.jpg" alt="photo">
</picture>

<!-- 3. 懒加载 -->
<img loading="lazy" src="photo.jpg" alt="photo">

3.5 前端安全 ​

Q1:XSS 和 CSRF 的原理及防御措施 ​

答案:

markdown
XSS(跨站脚本攻击):
- 原理:注入恶意脚本到页面中执行
- 类型:存储型、反射型、DOM 型
- 防御:
  - 输入过滤与输出转义(HTML 实体编码)
  - CSP(Content Security Policy)
  - HttpOnly Cookie
  - 使用 DOMPurify 清洗用户输入

CSRF(跨站请求伪造):
- 原理:利用用户登录态发起恶意请求
- 防御:
  - SameSite Cookie(Lax/Strict)
  - Token 验证
  - Referer/Origin 校验
  - 关键操作二次验证
javascript
// CSP 配置示例
// <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' https://trusted.com">
// 或通过 HTTP 头:Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted.com

// 输出转义
function escapeHTML(str) {
  return str.replace(/[&<>"']/g, function(match) {
    return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[match];
  });
}

第四篇:项目经验与系统设计 ​


4.1 项目架构设计 ​

Q1:你参与过的最有挑战性的项目是什么?你是如何设计架构的? ​

考察要点:

markdown
回答框架(STAR 原则):
1. Situation(情境):项目背景、规模、团队
2. Task(任务):你的角色和职责
3. Action(行动):你做的架构决策和原因
4. Result(结果):数据化的成果

项目架构示例回答:
"在一个电商后台管理系统中,我主导了从 Vue 2 + Webpack 到 Vue 3 + Vite 的技术栈升级。
主要挑战是 30+ 个业务模块需要平滑迁移,且不能影响线上运营。

架构设计:
1. 采用微前端(qiankun)拆分应用
2. 先迁移核心模块到 Vue 3 + TypeScript
3. 旧模块通过 qiankun 接入,逐步替换
4. 统一封装了业务组件库和 API 层

结果:3 个月内完成全量迁移,构建速度提升 80%,线上零故障。"

Q2:如何设计一个高可用的前端监控系统? ​

答案:

javascript
// 监控系统架构
class Monitor {
  constructor() {
    this.queue = [];
    this.config = { sampleRate: 0.1 };
    this.init();
  }

  init() {
    // 1. JS 运行时错误
    window.onerror = (msg, url, line, col, error) => {
      this.report({ type: "error", subtype: "js", msg, url, line, col, stack: error?.stack });
    };

    // 2. Promise 未捕获错误
    window.addEventListener("unhandledrejection", (event) => {
      this.report({ type: "error", subtype: "promise", reason: event.reason?.message });
    });

    // 3. 资源加载错误
    window.addEventListener("error", (event) => {
      if (event.target?.tagName) {
        this.report({ type: "error", subtype: "resource", tag: event.target.tagName, src: event.target.src });
      }
    }, true);

    // 4. 请求错误(axios 拦截器)
    // 5. 性能指标(PerformanceObserver)
    // 6. 用户行为追踪(点击、路由跳转)
  }

  report(data) {
    // 采样控制
    if (Math.random() > this.config.sampleRate) return;

    // 去重(3 秒内相同错误不重复上报)
    const key = JSON.stringify(data);
    if (this.lastReport?.[key] > Date.now() - 3000) return;
    this.lastReport = { ...this.lastReport, [key]: Date.now() };

    // 批量上报
    this.queue.push({ ...data, timestamp: Date.now(), url: location.href, ua: navigator.userAgent });
    if (this.queue.length >= 10) {
      this.flush();
    }
  }

  flush() {
    const batch = this.queue.splice(0);
    navigator.sendBeacon?.("/api/monitor", JSON.stringify({ events: batch }));
  }
}

4.2 难点与解决方案 ​

Q1:项目中的技术选型决策过程 ​

问题:你是如何在项目中选择技术方案的?举例说明。

答案框架:

markdown
以"状态管理选型"为例:

背景:一个中大型 CRM 系统,30+ 模块,需要管理大量跨组件状态

方案对比:
1. Vuex 4:成熟但需要 mutation、TS 支持弱
2. Pinia:轻量、TS 原生、无 mutation、官方推荐
3. Provide/Inject:不够正式,不适合复杂状态

决策因素:
- 团队熟悉度(Vuex,但愿意学习新方案)
- TypeScript 支持(Pinia 完胜)
- 未来维护成本(Pinia 更简洁)
- 社区趋势(官方推荐 Pinia)

结果:采用 Pinia,模块化 split store

Q2:线上事故处理流程 ​

问题:如果线上出现重大 bug,你的处理流程是什么?

答案:

markdown
1. 立即止损(5 分钟内)
   - 评估影响范围
   - 回滚/热修复/开关控制
   - 通知相关方

2. 定位问题(30 分钟内)
   - 查看监控告警
   - 查看错误日志(Sentry)
   - 定位根因

3. 修复验证(1 小时内)
   - 编写修复代码
   - code review
   - 灰度发布

4. 复盘总结(24 小时内)
   - 事故报告(5W1H)
   - 改进措施(监控补充、流程优化)
   - 知识沉淀(避免重复犯错)

4.3 系统设计题 ​

Q1:设计一个活动页面搭建平台 ​

问题:如果让你设计一个可视化拖拽搭建平台(类似 易企秀/稿定设计),你会如何设计前端架构?

答案:

markdown
核心模块:

1. 画布引擎
   - 渲染层:组件树 + 绝对定位
   - 操作层:拖拽、缩放、旋转、对齐
   - 选中层:显示选中组件的控制手柄

2. 组件库
   - 基础组件:文本、图片、按钮、形状
   - 业务组件:商品卡片、表单、轮播
   - 每个组件实现:renderer、props、configPanel

3. 属性面板
   - 根据选中组件的 props schema 动态渲染
   - 支持:样式、数据、交互配置

4. 历史管理(撤销/重做)
   - Command 模式 + 状态快照
   - 限制历史栈最大步数

5. 导出发布
   - 生成静态 HTML
   - 或渲染为 JSON 配置,由渲染引擎消费

技术选型:
- 框架:Vue 3 + TypeScript
- 状态:Pinia(管理画布数据)
- 拖拽:vuedraggable(基于 SortableJS)
- 撤销/重做:undo-redo-stack
- 渲染器:自研 JSON → Vue 组件渲染器

Q2:设计一个实时协作系统 ​

问题:如何设计一个多人实时协作编辑系统(如 Google Docs)?

答案:

核心技术:

  • WebSocket(实时通信)
  • CRDT / OT(操作转换算法)
  • 操作合并与冲突解决

前端架构:

  1. 本地操作立即应用(乐观更新)
  2. 操作通过 WebSocket 广播
  3. 收到服务端确认/冲突结果
  4. 如有冲突,根据 CRDT/OT 合并

简化实现:

  1. 使用 ShareDB / Y.js 等成熟库
  2. 或使用 Operational Transform 算法
  3. 通过 WebSocket 同步操作日志

Vue 中的应用:

javascript
// 使用 y-websocket + yjs
import * as Y from "yjs";
import { WebsocketProvider } from "y-websocket";

const doc = new Y.Doc();
const wsProvider = new WebsocketProvider("ws://localhost:1234", "room-name", doc);

// 创建一个共享数组
const sharedArray = doc.getArray("items");
sharedArray.observe(event => {
  // 更新本地视图
});

4.4 基于当前知识框架的项目专项面试题 ​

本节对应当前文件夹中的 10-项目实战与产出、01-编程语言基础、02-前端基础与框架、03-前端工程化与构建、04-前端架构师进阶、05-后端与全栈框架、06-数据库与数据访问、07-AI与智能体开发、08-部署运维与容器。回答时不要只背技术名词,要把“业务背景、技术方案、关键难点、落地结果”连成闭环。

Q1:如何介绍一个 AI Prompt 工作台项目? ​

面试官想看:你是否理解 AI 应用的基本交互链路、流式输出、Prompt 模板管理、参数调优和历史记录。

参考回答:

我做的是一个面向研发、运营和产品同学的 Prompt 调试工作台,核心能力是编辑 Prompt、配置模型参数、发起运行、查看流式结果、沉淀模板和历史记录。前端使用 Vue 3 + TypeScript + Pinia 管理编辑态、参数态和运行态,接口层通过统一 request 封装处理鉴权、错误和取消请求;后端可以用 Node/NestJS 或其他服务转发模型调用,并把模板、运行记录、评分结果落到 PostgreSQL 或 MongoDB。

项目难点主要有三类:

  1. 流式输出体验:AI 返回内容不是一次性完成,需要用 SSE、ReadableStream 或 WebSocket 持续渲染,并支持停止生成、失败重试和 partial content 保留。
  2. Prompt 参数校验:temperature、top_p、max_tokens 等参数要做范围校验,模板变量要在运行前检查是否缺失。
  3. 历史记录与复现:每次运行要保存 prompt、参数、模型、输入变量、输出结果和耗时,保证后续可以复盘。

项目亮点可以这样表达:

  1. 把 Prompt 模板、变量、运行参数和结果版本化,避免“只看到一次结果,无法复现”的问题。
  2. 对流式输出做了状态机管理:idle、running、paused、success、failed,UI 可以稳定展示加载、取消、重试和错误。
  3. 增加 token 用量、耗时、失败率等指标,方便比较不同 Prompt 版本的效果和成本。

常见追问:

  • 为什么 AI 流式输出更常用 SSE,而不是一定用 WebSocket?
  • 用户点击“停止生成”时,前端和后端分别要做什么?
  • Prompt 模板变量如何设计?如何避免运行时变量缺失?
  • 如果接口返回一半失败,历史记录应该保存什么状态?

Q2:如何介绍一个 AI 产品组件工作室或组件库项目? ​

面试官想看:你是否具备组件抽象、类型设计、设计系统、文档化和工程化交付能力。

参考回答:

这个项目的目标是沉淀 AI 产品常用组件,例如 ChatMessage、PromptEditor、StreamingOutput、AgentTimeline、ToolCallCard、CitationList、CostBadge 等。组件库不只是 UI 展示,还需要覆盖 AI 场景中的流式内容、工具调用、引用来源、失败重试、空状态和长文本折叠。

架构上我会把组件拆成四层:

  1. 基础组件层:Button、Input、Modal、Table、Tabs、Toast。
  2. 业务组件层:PromptEditor、StreamingOutput、AgentTimeline。
  3. 场景页面层:Prompt 调试页、RAG 问答页、Agent 运行页。
  4. 文档和演示层:Props 表、状态矩阵、示例代码、Live Preview。

重难点:

  1. 类型设计:组件 props 要用 TypeScript 明确输入输出,事件名和数据结构不能随意变。
  2. 状态覆盖:AI 组件常见状态很多,包括 pending、streaming、success、error、cancelled、empty、timeout。
  3. 主题和可访问性:组件要支持明暗主题、键盘操作、aria 属性和足够的对比度。
  4. 打包交付:如果作为组件库发布,需要考虑 Vite library mode、按需引入、类型声明和样式隔离。

亮点表达:

  • 我不是简单堆组件,而是建立了组件 API 规范、状态矩阵和文档示例,使组件可以被多个 AI 项目复用。
  • 对 StreamingOutput 和 AgentTimeline 这类 AI 组件抽象了统一事件模型,让不同模型、不同后端流式协议都能接入。
  • 通过 Storybook、VitePress 或自建文档页沉淀组件用法,降低团队接入成本。

常见追问:

  • 如何判断一个组件应该沉淀到组件库,而不是留在业务里?
  • 组件库如何做版本管理和破坏性变更?
  • 如何让组件库同时支持 Tailwind CSS 和项目自定义主题?

Q3:AI 任务管理全栈平台的项目难点是什么? ​

面试官想看:你是否能把前端、后端、数据库、权限、AI 能力结合成完整业务系统。

参考回答:

AI 任务管理平台不是一个纯 ToDo,它涉及用户、组织、项目、任务、评论、附件、AI 建议等业务对象。前端负责看板、任务详情抽屉、筛选排序、拖拽变更状态和 AI 建议中心;后端提供 RESTful API,使用 Prisma 或 MyBatis 操作数据库,任务状态变更要考虑事务、权限和审计。

核心难点:

  1. 看板拖拽的一致性:前端可以乐观更新,但后端保存失败时要回滚;多人同时操作时要处理版本冲突。
  2. 权限模型:组织管理员、项目成员、访客对项目和任务的读写权限不同,不能只在前端隐藏按钮。
  3. AI 建议接入:AI 可以生成任务拆解、优先级建议和风险提示,但结果需要人工确认后才能写入业务数据。
  4. 查询性能:任务列表常有状态、负责人、标签、时间范围等组合筛选,需要合理设计索引和分页。

亮点表达:

  • 我把 AI 能力设计成“建议层”,不会直接修改核心任务数据,降低误操作风险。
  • 任务状态变更采用乐观更新 + 失败回滚 + 服务端校验,兼顾体验和数据正确性。
  • 通过统一 DTO、接口契约和错误码,让前后端协作更稳定。

常见追问:

  • 拖拽排序字段如何设计?整数排序、浮点排序、链表式排序各有什么问题?
  • 如果两个用户同时移动同一个任务,如何处理冲突?
  • AI 生成内容如何标记来源、置信度和人工确认状态?

Q4:RAG 个人知识库项目如何讲清楚? ​

面试官想看:你是否理解 RAG 的完整链路,而不是只会说“上传文档后问答”。

参考回答:

RAG 知识库的链路可以拆成文档上传、文本解析、切片、向量化、入库、检索、重排、生成答案和引用来源展示。前端侧重点不是模型算法本身,而是异步任务进度、文档状态、问答体验、引用来源和检索调试能力。

核心流程:

  1. 用户上传 PDF、Markdown、Word 或网页内容。
  2. 后端解析文本,并按标题、段落或 token 长度切片。
  3. 对每个 chunk 生成 embedding,写入向量库或支持向量能力的数据库。
  4. 问答时先检索相关片段,再把片段和问题一起交给模型生成答案。
  5. 前端展示答案、引用来源、相关片段和命中分数。

重难点:

  1. 文档处理是异步过程,前端要展示 queued、parsing、embedding、indexed、failed 等状态。
  2. 检索质量依赖 chunk 策略、召回数量、重排和过滤条件,不是简单把全文塞给模型。
  3. 引用来源必须可追溯,否则回答可信度不足。
  4. 大文档上传、解析失败、重复上传和权限隔离都要处理。

亮点表达:

  • 我增加了检索调试页面,可以查看 query、召回片段、分数、来源文档和最终上下文,方便定位“答非所问”的原因。
  • 文档入库采用任务状态机,前端通过轮询、SSE 或 WebSocket 展示处理进度。
  • 答案展示强制绑定引用来源,降低 AI 幻觉对用户的影响。

常见追问:

  • chunk size 怎么选?过大和过小分别有什么问题?
  • 如何处理用户更新文档后的向量数据同步?
  • RAG 和直接长上下文问答有什么区别?
  • 检索结果为空时应该怎样降级?

Q5:Agent 工作流编排台的项目亮点是什么? ​

面试官想看:你是否理解 Agent 的状态、工具调用、人工审批、运行日志和可观测性。

参考回答:

Agent 工作流编排台可以理解为一个可视化的 AI 自动化流程系统。用户配置工作流模板,模板中包含 LLM 节点、工具节点、条件分支、人工审批节点和结束节点。运行时后端按图执行,前端实时展示每个节点的输入、输出、耗时、状态和错误。

核心难点:

  1. 工作流不是线性请求,而是有状态的执行图,需要保存 runId、nodeId、status、input、output、error、startedAt、finishedAt。
  2. 工具调用可能失败,要支持 retry、timeout、fallback 和人工介入。
  3. 人工审批会让流程暂停,必须能持久化上下文,审批后继续执行。
  4. 前端要把后端事件流转成 Timeline、节点状态和 Inspector 面板。

亮点表达:

  • 我把运行事件抽象为统一事件模型,例如 node_started、token_delta、tool_called、human_required、node_finished、run_failed。
  • 引入 checkpoint 思路,流程中断后可以从最近状态恢复,而不是整条链路重跑。
  • 对工具调用做了参数展示、结果折叠、敏感字段脱敏和失败重试,提升排查效率。

常见追问:

  • Agent 工作流和普通工作流引擎有什么不同?
  • 为什么人工审批节点需要持久化?
  • 前端如何展示一个长时间运行的工作流?
  • 工具调用失败会不会导致数据不一致?如何保证幂等?

Q6:企业 AI 助手控制台如何体现工程能力? ​

面试官想看:你是否理解企业系统中的权限、审计、数据安全、后端集成和可维护性。

参考回答:

企业 AI 助手控制台通常面向客服、销售、运营或内部知识管理场景。它不是简单聊天窗口,而是要接入客户数据、工单数据、知识库、权限体系和审计日志。前端通常包括客户列表、客户详情、AI Copilot 侧栏、推荐话术、风险提示和审计日志页面。

重难点:

  1. 权限与数据隔离:不同角色能看到的客户、字段、操作不同,前端展示和后端接口都要校验。
  2. 审计日志:AI 建议、用户采纳、敏感数据访问和关键操作都要可追踪。
  3. 后端集成:可能要和 Java/Spring/MyBatis 老系统集成,接口字段、分页、权限模型不一定统一。
  4. 敏感信息处理:手机号、邮箱、身份证、合同金额等字段需要脱敏、权限校验和日志记录。

亮点表达:

  • 我把 AI Copilot 设计为业务辅助侧栏,不打断原有 CRM 操作流。
  • 对 AI 生成建议加入“采纳、复制、编辑后采纳、弃用”的反馈闭环,为后续优化提供数据。
  • 关键操作接入审计日志,方便企业合规和问题追溯。

常见追问:

  • 前端脱敏和后端脱敏分别解决什么问题?
  • 如果 AI 建议错误导致业务损失,系统上如何降低风险?
  • 企业旧系统接口不规范时,前端如何做适配层?

Q7:AI 产品运营中台毕业项目怎么讲成作品集亮点? ​

面试官想看:你是否具备从单点功能升级为平台化系统的能力。

参考回答:

AI 产品运营中台可以作为前面多个项目的整合:Prompt Center 管理 Prompt 版本,Knowledge Center 管理知识库,Agent Center 管理工作流,Feedback Center 收集用户反馈,Cost Center 统计 token 和调用成本,Release Center 管理灰度发布和配置变更。

架构重点:

  1. 前端模块化:按业务域拆分 Prompt、Knowledge、Agent、Feedback、Cost、Release。
  2. 数据模型统一:用户、组织、项目、版本、运行记录、反馈、成本记录之间要有明确关系。
  3. 权限统一:菜单权限、页面权限、按钮权限和数据权限要贯通。
  4. 可观测性:每次模型调用、检索、工具调用和发布变更都要有日志和指标。

项目亮点:

  • 从“功能型 AI 应用”升级为“运营型 AI 平台”,能讲清楚 Prompt、知识库、Agent、反馈、成本、发布之间的关系。
  • 引入成本中心和反馈中心,让 AI 功能可评估、可迭代,而不是一次性 Demo。
  • 通过 Release Center 做灰度和回滚,避免 Prompt 或知识库变更直接影响全部用户。

常见追问:

  • Prompt 版本如何设计?如何回滚?
  • Cost Center 如何统计 token 成本和接口耗时?
  • Feedback Center 的反馈如何反哺 Prompt 或 RAG 优化?
  • 如果多个模块由不同团队维护,前端工程如何组织?

Q8:如何介绍 ECharts + Canvas + WebSocket 实时数据大屏? ​

面试官想看:你是否能把可视化、Canvas 性能、实时通信和数据节流讲到一起。

参考回答:

实时数据大屏一般包含趋势图、排行榜、告警流、地图、指标卡和实时日志。ECharts 适合常规图表和交互,Canvas 适合大量粒子、轨迹、热力或自定义高性能绘制,WebSocket 用于服务端主动推送实时数据。

重难点:

  1. 数据频率高:WebSocket 可能每秒推送多次,图表不能每条消息都 setOption。
  2. 数据量大:长时间运行会造成数组无限增长和内存上升,需要滑动窗口。
  3. 页面性能:图表、动画、表格和日志同时更新,容易造成主线程卡顿。
  4. 断线重连:网络波动时要有心跳、重连、补偿拉取和状态提示。

解决方案:

  1. WebSocket 收到消息后先进入缓冲队列,用 requestAnimationFrame 或固定时间窗口批量刷新。
  2. ECharts 使用增量更新、dataset、appendData 或只保留最近 N 个点。
  3. Canvas 绘制做离屏缓存、分层渲染,复杂计算尽量放到 Web Worker。
  4. 实时连接设计心跳、指数退避重连和 lastEventId / timestamp 补偿。

亮点表达:

  • 我没有让图表跟着消息频率盲目刷新,而是做了消息缓冲和渲染节流,保证实时性和帧率之间平衡。
  • 对历史数据采用滑动窗口,控制内存增长。
  • 连接层加入心跳和重连策略,弱网下也能恢复。

常见追问:

  • ECharts 的 setOption 为什么可能造成性能问题?
  • Canvas 和 SVG 的适用场景有什么区别?
  • WebSocket 断线后如何补齐丢失数据?
  • 实时大屏如何做首屏加载和后续增量更新?

Q9:Vite / Webpack 工程化迁移项目怎么回答? ​

面试官想看:你是否理解构建工具原理、迁移风险、插件生态和团队协作成本。

参考回答:

如果项目从 Vue CLI + Webpack 迁移到 Vite,核心目标通常是提升开发启动速度和 HMR 体验,同时保持生产构建稳定。迁移不是简单替换命令,而是要处理环境变量、路径别名、CSS/PostCSS、静态资源、代理、按需引入、插件替换和 CI 构建流程。

重难点:

  1. 路径别名差异:Webpack alias 和 Vite resolve.alias 写法不同,TypeScript paths 也要同步。
  2. 环境变量差异:Vue CLI 使用 VUE_APP_,Vite 默认暴露 VITE_。
  3. CSS 处理差异:PostCSS、Less/Sass、CSS Modules、Autoprefixer、Tailwind 配置要验证。
  4. 插件替换:Webpack loader/plugin 不能直接迁到 Vite,需要找 Rollup/Vite 插件或改写。
  5. 生产构建差异:Vite 开发期基于原生 ESM,生产期基于 Rollup,不能只测 dev。

亮点表达:

  • 我先梳理构建链路和风险清单,再做小模块试点,最后逐步迁移,避免一次性大爆炸式改造。
  • 对 alias、env、proxy、PostCSS、静态资源和构建产物做了迁移对照表。
  • 建立 dev、build、preview、E2E 验证流程,确保开发体验和线上行为一致。

常见追问:

  • Vite 为什么开发环境启动更快?
  • Vite 如何解析 Vue 单文件组件?
  • Webpack loader 和 plugin 的区别是什么?
  • PostCSS 在 Vite 中处于什么位置?

Q10:动态权限、动态路由和页面权限如何讲? ​

面试官想看:你是否理解前端权限只是体验层,真正安全必须依赖后端校验。

参考回答:

权限系统通常分为菜单权限、路由权限、按钮权限、接口权限和数据权限。前端根据后端返回的角色、权限码或菜单树生成路由和页面操作按钮,后端在接口层继续校验权限,避免用户绕过前端直接请求接口。

实现方案:

  1. 登录后获取用户信息、角色、权限码和菜单树。
  2. 前端把菜单树转换成 Vue Router 路由配置,并用 addRoute 动态注册。
  3. 路由守卫检查 token、白名单、路由权限和页面初始化状态。
  4. 按钮使用权限指令或 composable 判断权限码。
  5. 后端接口继续做鉴权和数据范围校验。

重难点:

  1. 刷新页面后动态路由丢失,需要在路由守卫中恢复权限和路由。
  2. 后端菜单树和前端组件路径要建立稳定映射,不能直接信任任意路径。
  3. 按钮隐藏不等于接口安全,接口必须二次校验。
  4. 多租户或组织权限下还要处理数据范围。

亮点表达:

  • 我把权限拆成路由级、按钮级和数据级,不把“隐藏菜单”当成完整权限方案。
  • 动态路由注册做了白名单映射,避免后端返回任意组件路径造成安全风险。
  • 权限初始化支持刷新恢复,避免白屏和 404 问题。

Q11:大文件上传项目有哪些高频追问? ​

面试官想看:你是否理解分片、断点续传、秒传、并发控制和服务端合并。

参考回答:

大文件上传的基本链路是前端选择文件、计算 hash、切片、查询已上传分片、并发上传缺失分片、服务端合并、校验完整性。前端重点是并发控制、进度展示、失败重试、暂停继续和弱网恢复。

重难点:

  1. hash 计算可能阻塞主线程,大文件应使用 Web Worker 或增量计算。
  2. 分片并发过高会打满网络和服务端连接,过低又影响速度,需要限制并发数。
  3. 上传失败要支持单片重试,而不是整个文件重传。
  4. 服务端合并后要校验文件完整性,并清理临时分片。

亮点表达:

  • 我把上传任务抽象为状态机:waiting、hashing、uploading、paused、merging、success、failed。
  • 通过 hash + chunkIndex 实现秒传和断点续传。
  • 上传队列支持并发限制、失败重试和暂停恢复,用户体验更稳定。

常见追问:

  • 文件 hash 放前端算还是后端算?
  • 分片大小怎么定?
  • 如何保证分片合并顺序正确?
  • 暂停上传后如何恢复?

Q12:如何把部署、缓存和质量保障讲成项目亮点? ​

面试官想看:你是否有完整交付意识,而不是只会写页面。

参考回答:

项目上线不只是 npm run build,还包括构建产物、Nginx 配置、Docker 镜像、环境变量、静态资源缓存、接口反向代理、SPA history fallback、灰度发布、监控告警和回滚方案。

重难点:

  1. Nginx 要处理静态资源缓存和 index.html 不缓存,避免发版后用户拿到旧入口文件。
  2. SPA history 路由需要 fallback 到 index.html,否则刷新深层路由会 404。
  3. Docker 镜像要区分构建阶段和运行阶段,减少镜像体积。
  4. 监控要覆盖 JS 错误、Promise 错误、资源加载失败、接口失败和性能指标。

亮点表达:

  • 我把部署配置、环境变量、反向代理和缓存策略文档化,减少交接成本。
  • 对静态资源使用 hash 文件名 + 长缓存,对 HTML 使用 no-cache,兼顾性能和发版正确性。
  • 上线后接入错误监控和性能指标,能及时发现白屏、接口异常和首屏性能问题。

4.5 项目重难点与亮点表达 ​

4.5.1 项目难点表达矩阵 ​

项目场景面试官真正想确认项目难点可落地方案亮点表达
AI 流式问答是否理解流式协议和体验细节内容分片返回、取消生成、失败恢复SSE / WebSocket / ReadableStream + AbortController + 状态机流式渲染不是简单拼字符串,而是管理连接、状态、取消和异常
RAG 知识库是否理解检索增强生成链路文档解析、切片、向量化、召回质量异步任务状态 + 检索调试页 + 引用来源通过可视化召回片段定位回答质量问题
Agent 工作流是否理解有状态 AI 编排节点状态、工具失败、人工审批事件模型 + checkpoint + retry/fallback把黑盒 Agent 运行变成可观察、可恢复的流程
动态权限是否理解权限安全边界刷新恢复、按钮权限、接口绕过动态路由 + 权限码 + 后端二次校验前端负责体验,后端负责安全,权限分层设计
大文件上传是否理解复杂异步任务hash、分片、断点续传、并发Web Worker + chunk 上传 + 并发池 + 重试上传任务状态机提升稳定性和可维护性
实时大屏是否理解高频渲染性能数据高频推送、图表卡顿、内存增长消息缓冲 + 渲染节流 + 滑动窗口在实时性和性能之间做平衡,而不是每条消息都刷新
组件库是否具备抽象和工程化能力API 设计、状态覆盖、主题适配TypeScript 类型 + 状态矩阵 + 文档演示组件沉淀服务多个项目,而不是只解决单页需求
Vite 迁移是否理解构建链路alias、env、PostCSS、插件差异迁移清单 + 试点模块 + build/preview 验证迁移不是换工具,而是控制工程风险
可观测性是否具备线上意识错误采集、性能指标、告警闭环Sentry/自建监控 + PerformanceObserver + sendBeacon线上问题能发现、定位、复盘和预防
部署缓存是否理解交付链路缓存错配、路由 404、环境混乱Nginx cache-control + history fallback + Docker 多阶段构建构建、部署、缓存、回滚形成闭环

4.5.2 项目亮点回答模板 ​

模板一:技术闭环型

text
这个项目我主要负责 X 模块。它的难点不是页面本身,而是 Y。
我先把问题拆成 A、B、C 三层,然后选择了 D 技术方案。
落地时重点处理了 E 风险,例如异常、性能、权限或一致性。
最后的结果是 F,至少在体验、稳定性、效率或可维护性上有明确改善。

模板二:架构选型型

text
当时有几个方案:A 成本低但扩展性弱,B 能力强但引入复杂度高,C 和团队当前技术栈更匹配。
我最后选 C,原因是它满足当前业务规模,同时保留后续升级空间。
为了降低风险,我做了试点、灰度、监控和回滚方案。

模板三:问题复盘型

text
项目中最难的问题是 X。最开始我们遇到 Y 现象,通过日志、监控和最小复现定位到 Z。
修复上我没有只改表面 bug,而是补了边界处理、测试用例和监控指标。
这件事之后,我们把相关流程沉淀成规范,避免同类问题再次出现。

4.5.3 项目数据不要乱编 ​

面试中可以量化成果,但不要编造无法解释的数据。更稳妥的表达方式:

  • 性能类:首屏时间、构建耗时、接口耗时、图表帧率、错误率。
  • 效率类:组件复用数量、重复代码减少、配置时间缩短、发版步骤减少。
  • 稳定性类:错误告警覆盖、失败重试成功率、上传恢复成功率。
  • 业务类:模板使用次数、知识库命中率、AI 建议采纳率、人工处理时间缩短。

如果没有真实数据,可以说:

text
这个项目是学习/作品集项目,所以没有生产环境真实业务数据。
但我在设计时按生产标准预留了性能指标、错误监控和可量化埋点,例如 X、Y、Z。

4.6 项目追问清单 ​

4.6.1 架构与技术选型 ​

  1. 这个项目为什么选择 Vue 3,而不是 React 或 Vue 2?
  2. 为什么使用 TypeScript?它在项目中解决了哪些具体问题?
  3. Pinia、provide/inject、URL state、localStorage 分别适合存什么状态?
  4. 哪些模块适合抽成组件库?哪些不适合?
  5. 你如何判断项目需要 Monorepo?
  6. 你如何设计前后端接口契约?
  7. 你如何处理技术债?如何决定重构优先级?

4.6.2 前端工程化 ​

  1. Vite 开发环境和生产构建为什么是两套机制?
  2. Vite 如何处理 .vue 文件、CSS、静态资源和路径别名?
  3. Webpack 的 compiler、compilation、loader、plugin 分别是什么?
  4. PostCSS、Sass、Tailwind、CSS Modules 在构建链路中的位置是什么?
  5. 如何做构建产物分析和拆包优化?
  6. 如何保证迁移构建工具后线上行为不变?
  7. CI 中应该跑哪些检查?

4.6.3 Vue 与业务复杂度 ​

  1. Composition API 如何组织复杂业务逻辑?
  2. 一个通用表单组件如何设计校验、联动、异步选项和权限?
  3. 动态路由刷新白屏如何解决?
  4. keep-alive 什么时候会带来副作用?
  5. 大型列表、树形菜单、看板拖拽分别如何优化?
  6. 如何避免组件通信混乱?

4.6.4 AI 应用专项 ​

  1. Prompt 模板如何做版本管理?
  2. 流式输出如何取消?取消后后端还会继续消耗资源吗?
  3. RAG 为什么需要引用来源?
  4. chunk 策略如何影响检索质量?
  5. Agent 工具调用失败如何处理?
  6. 人工审批节点为什么需要持久化?
  7. AI 生成内容如何做风险提示和人工确认?
  8. 如何统计 token 成本、耗时和失败率?

4.6.5 实时通信与可视化 ​

  1. SSE 和 WebSocket 如何选型?
  2. WebSocket 心跳和重连怎么做?
  3. 高频消息如何避免把 Vue 页面打卡顿?
  4. ECharts 如何处理大数据量更新?
  5. Canvas 如何做分层绘制和离屏缓存?
  6. 实时大屏如何控制内存增长?

4.6.6 后端、数据库与缓存 ​

  1. RESTful API 如何设计资源和状态码?
  2. Prisma 和 MyBatis 的适用场景有什么差异?
  3. PostgreSQL、MySQL、MongoDB 如何选型?
  4. Redis 在项目中可以做缓存、限流、队列还是分布式锁?
  5. 数据库索引如何根据查询条件设计?
  6. 事务在任务状态流转、分片上传合并、订单类业务中解决什么问题?
  7. 如何处理接口幂等?

4.6.7 部署、质量和线上问题 ​

  1. Nginx 如何配置 SPA history fallback?
  2. 为什么 index.html 不应该强缓存,而 hash 静态资源可以长缓存?
  3. Docker 多阶段构建解决什么问题?
  4. 前端监控需要采集哪些错误和性能指标?
  5. 白屏问题如何定位?
  6. 线上事故如何止损、定位、修复和复盘?
  7. 灰度发布和回滚方案如何设计?

第五篇:代码实现题 ​


5.1 手写实现 ​

1. 手写 Promise(核心实现) ​

javascript
const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";

class MyPromise {
  constructor(executor) {
    this.state = PENDING;
    this.value = undefined;
    this.reason = undefined;
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];

    const resolve = (value) => {
      if (this.state === PENDING) {
        this.state = FULFILLED;
        this.value = value;
        this.onFulfilledCallbacks.forEach(fn => fn());
      }
    };

    const reject = (reason) => {
      if (this.state === PENDING) {
        this.state = REJECTED;
        this.reason = reason;
        this.onRejectedCallbacks.forEach(fn => fn());
      }
    };

    try {
      executor(resolve, reject);
    } catch (e) {
      reject(e);
    }
  }

  then(onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === "function" ? onFulfilled : v => v;
    onRejected = typeof onRejected === "function" ? onRejected : e => { throw e; };

    const promise2 = new MyPromise((resolve, reject) => {
      if (this.state === FULFILLED) {
        setTimeout(() => {
          try {
            const x = onFulfilled(this.value);
            this.resolvePromise(promise2, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        });
      }

      if (this.state === REJECTED) {
        setTimeout(() => {
          try {
            const x = onRejected(this.reason);
            this.resolvePromise(promise2, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        });
      }

      if (this.state === PENDING) {
        this.onFulfilledCallbacks.push(() => {
          setTimeout(() => {
            try {
              const x = onFulfilled(this.value);
              this.resolvePromise(promise2, x, resolve, reject);
            } catch (e) {
              reject(e);
            }
          });
        });
        this.onRejectedCallbacks.push(() => {
          setTimeout(() => {
            try {
              const x = onRejected(this.reason);
              this.resolvePromise(promise2, x, resolve, reject);
            } catch (e) {
              reject(e);
            }
          });
        });
      }
    });

    return promise2;
  }

  resolvePromise(promise2, x, resolve, reject) {
    if (promise2 === x) {
      return reject(new TypeError("Chaining cycle detected"));
    }

    if (x && (typeof x === "object" || typeof x === "function")) {
      let called = false;
      try {
        const then = x.then;
        if (typeof then === "function") {
          then.call(x,
            y => {
              if (called) return;
              called = true;
              this.resolvePromise(promise2, y, resolve, reject);
            },
            r => {
              if (called) return;
              called = true;
              reject(r);
            }
          );
        } else {
          resolve(x);
        }
      } catch (e) {
        if (called) return;
        called = true;
        reject(e);
      }
    } else {
      resolve(x);
    }
  }

  catch(onRejected) {
    return this.then(null, onRejected);
  }

  finally(callback) {
    return this.then(
      value => MyPromise.resolve(callback()).then(() => value),
      reason => MyPromise.resolve(callback()).then(() => { throw reason; })
    );
  }

  static resolve(value) {
    if (value instanceof MyPromise) return value;
    return new MyPromise(resolve => resolve(value));
  }

  static reject(reason) {
    return new MyPromise((_, reject) => reject(reason));
  }
}

2. 防抖(Debounce)和节流(Throttle) ​

javascript
// 防抖:多次触发只执行最后一次
function debounce(fn, delay = 300) {
  let timer = null;
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

// 节流:固定频率执行
function throttle(fn, limit = 300) {
  let inThrottle = false;
  return function(...args) {
    if (!inThrottle) {
      fn.apply(this, args);
      inThrottle = true;
      setTimeout(() => inThrottle = false, limit);
    }
  };
}

// 节流(时间戳版:保证第一次立即执行)
function throttleTS(fn, limit = 300) {
  let lastTime = 0;
  return function(...args) {
    const now = Date.now();
    if (now - lastTime >= limit) {
      fn.apply(this, args);
      lastTime = now;
    }
  };
}

3. 深拷贝(支持循环引用) ​

javascript
function deepClone(obj, hash = new WeakMap()) {
  if (obj === null || typeof obj !== "object") return obj;
  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof RegExp) return new RegExp(obj);
  if (obj instanceof Map) return new Map(Array.from(obj.entries(), ([k, v]) => [deepClone(k, hash), deepClone(v, hash)]));
  if (obj instanceof Set) return new Set(Array.from(obj, v => deepClone(v, hash)));

  if (hash.has(obj)) return hash.get(obj);
  const result = Array.isArray(obj) ? [] : {};
  hash.set(obj, result);

  for (const key of Reflect.ownKeys(obj)) {
    const descriptor = Object.getOwnPropertyDescriptor(obj, key);
    if (descriptor && !descriptor.enumerable) continue;
    result[key] = deepClone(obj[key], hash);
  }
  return result;
}

4. 函数柯里化 ​

javascript
function curry(fn, arity = fn.length) {
  return function curried(...args) {
    if (args.length >= arity) {
      return fn.apply(this, args);
    }
    return function(...args2) {
      return curried.apply(this, args.concat(args2));
    };
  };
}

// 使用
const add = (a, b, c) => a + b + c;
const curriedAdd = curry(add);
curriedAdd(1)(2)(3);  // 6
curriedAdd(1, 2)(3);  // 6
curriedAdd(1, 2, 3);  // 6

5. 组合函数(Compose) ​

javascript
// 左到右:pipe
const pipe = (...fns) => (x) => fns.reduce((v, fn) => fn(v), x);

// 右到左:compose
const compose = (...fns) => (x) => fns.reduceRight((v, fn) => fn(v), x);

// 使用
const add1 = (x) => x + 1;
const multiply2 = (x) => x * 2;
const toString = (x) => `Result: ${x}`;

const pipeline = pipe(add1, multiply2, toString);
pipeline(5); // "Result: 12"

6. Event Bus(事件总线) ​

javascript
class EventBus {
  constructor() {
    this.events = {};
  }

  on(event, callback) {
    (this.events[event] ||= []).push(callback);
    return () => this.off(event, callback); // 返回取消订阅函数
  }

  off(event, callback) {
    if (!this.events[event]) return;
    this.events[event] = this.events[event].filter(cb => cb !== callback);
  }

  emit(event, ...args) {
    this.events[event]?.forEach(cb => cb(...args));
  }

  once(event, callback) {
    const wrapper = (...args) => {
      callback(...args);
      this.off(event, wrapper);
    };
    this.on(event, wrapper);
  }
}

7. 实现 new 操作符 ​

javascript
function myNew(constructor, ...args) {
  // 1. 创建空对象,原型指向 constructor.prototype
  const obj = Object.create(constructor.prototype);

  // 2. 执行构造函数,绑定 this 到新对象
  const result = constructor.apply(obj, args);

  // 3. 如果构造函数返回对象,则返回该对象;否则返回新对象
  return typeof result === "object" && result !== null ? result : obj;
}

// 使用
function Person(name) { this.name = name; }
const p = myNew(Person, "Alice");
console.log(p.name); // "Alice"
console.log(p instanceof Person); // true

8. 实现 Array.prototype.reduce ​

javascript
Array.prototype.myReduce = function(callback, initialValue) {
  if (this.length === 0 && initialValue === undefined) {
    throw new TypeError("Reduce of empty array with no initial value");
  }

  let accumulator = initialValue !== undefined ? initialValue : this[0];
  let startIndex = initialValue !== undefined ? 0 : 1;

  for (let i = startIndex; i < this.length; i++) {
    accumulator = callback(accumulator, this[i], i, this);
  }
  return accumulator;
};

// 使用
[1, 2, 3].myReduce((acc, cur) => acc + cur, 0); // 6

5.2 算法与数据结构(前端常考) ​

1. 数组扁平化 ​

javascript
// 递归实现
function flatten(arr) {
  return arr.reduce((acc, val) =>
    acc.concat(Array.isArray(val) ? flatten(val) : val), []
  );
}

// 迭代实现(栈)
function flattenIter(arr) {
  const stack = [...arr];
  const result = [];
  while (stack.length) {
    const item = stack.pop();
    if (Array.isArray(item)) {
      stack.push(...item);
    } else {
      result.unshift(item);
    }
  }
  return result;
}

// 指定深度
function flattenDepth(arr, depth = 1) {
  return arr.reduce((acc, val) =>
    acc.concat(depth > 1 && Array.isArray(val) ? flattenDepth(val, depth - 1) : val), []
  );
}

2. 二分查找 ​

javascript
function binarySearch(arr, target) {
  let left = 0;
  let right = arr.length - 1;

  while (left <= right) {
    const mid = Math.floor((left + right) / 2);
    if (arr[mid] === target) return mid;
    if (arr[mid] < target) left = mid + 1;
    else right = mid - 1;
  }
  return -1;
}

3. 常见排序(快速排序) ​

javascript
function quickSort(arr) {
  if (arr.length <= 1) return arr;
  const pivot = arr[Math.floor(arr.length / 2)];
  const left = arr.filter(x => x < pivot);
  const middle = arr.filter(x => x === pivot);
  const right = arr.filter(x => x > pivot);
  return [...quickSort(left), ...middle, ...quickSort(right)];
}

4. 树的遍历(场景:树形菜单/组件树) ​

javascript
// 深度优先遍历(DFS)
function dfs(tree, callback) {
  if (!tree) return;
  callback(tree);
  tree.children?.forEach(child => dfs(child, callback));
}

// 广度优先遍历(BFS)
function bfs(root, callback) {
  const queue = [root];
  while (queue.length) {
    const node = queue.shift();
    callback(node);
    node.children?.forEach(child => queue.push(child));
  }
}

5. URL 解析 ​

javascript
// 将 URL 的 query string 解析为对象
function parseQuery(url) {
  const params = new URLSearchParams(url.split("?")[1] || "");
  const result = {};
  for (const [key, value] of params) {
    // 支持多层嵌套:a.b.c=val
    const keys = key.split(/[\[\]\.]/).filter(Boolean);
    keys.reduce((acc, k, i) => {
      if (i === keys.length - 1) {
        acc[k] = value;
      } else {
        acc[k] = acc[k] || (isNaN(keys[i + 1]) ? {} : []);
      }
      return acc[k];
    }, result);
  }
  return result;
}

// parseQuery("?user.name=Alice&user.age=25&ids[0]=1&ids[1]=2")
// { user: { name: "Alice", age: "25" }, ids: ["1", "2"] }

附:面试准备建议 ​

项目经验准备 ​

markdown
1. 准备 2-3 个核心项目,每个项目能讲清楚:
   - 项目背景和规模
   - 你的角色和职责
   - 技术选型及原因
   - 遇到的难点和解决方案
   - 数据化的成果

2. STAR 法则回答面试官的问题

3. 准备常见场景题的回答:
   - "如果让你重新做这个项目,你会怎么改进?"
   - "这个项目中你遇到的最大的技术挑战是什么?"
   - "你是如何进行技术选型的?"
   - "团队合作中遇到冲突怎么处理?"

面试答题技巧 ​

markdown
1. 先说结论,再展开细节(金字塔原理)
2. 多维度回答:理论 + 实践 + 对比
3. 如果不会,坦诚说不会,但可以说出自己的思路
4. 回答项目问题时,用数据说话(性能提升% / 减少多少代码量 / 提升多少效率)
5. 准备好反问面试官的问题(技术栈、团队规模、技术挑战等)

高频面试考点汇总 ​

markdown
JavaScript:闭包、this、原型链、Promise、事件循环、深拷贝、手写实现
Vue:响应式原理、Composition API、组件通信、Pinia/Vuex、性能优化
工程化:Webpack/Loader/Plugin、Vite、PostCSS、CI/CD、微前端、Monorepo
网络:HTTP/HTTPS、TCP、缓存策略、WebSocket、SSE、流式输出
浏览器:渲染机制、事件机制、跨域、安全
性能:Web Vitals、LCP/FID/CLS、首屏优化、懒加载、ECharts/Canvas 大数据渲染
AI 项目:Prompt 工作台、RAG 知识库、Agent 工作流、企业 AI 助手、AI 运营中台
全栈与交付:RESTful API、Node/NestJS、Prisma/MyBatis、数据库选型、Redis、Docker、Nginx
项目表达:项目背景、技术选型、重难点、亮点、数据化结果、事故复盘
算法:数组/树/排序/去重/扁平化

本文档基于全网热门前端面试题库(febobo/web-interview、lgwebdream/FE-Interview、Advanced-Frontend/Daily-Interview-Question 等)及大厂面试真题综合整理。

来源参考:

  • github.com/febobo/web-interview(Vue/JS/HTTP/算法面试题)
  • github.com/Advanced-Frontend/Daily-Interview-Question(每日一题)
  • github.com/haizlin/fe-interview(6000+ 前端面试题)
  • github.com/lgwebdream/FE-Interview(1000+ 前端面试真题)

建议结合实际项目经验,将理论知识转化为自己的表达。祝面试顺利!

基于 VitePress 构建的个人学习文档知识库