Skip to content

综合前端面试题(企业级深度)

一、ES6+ 高级语法与高效开发

1. ES6 箭头函数与普通函数的区别

面试题:请详细比较 ES6 箭头函数与普通函数的区别,并说明在哪些场景下不能使用箭头函数。

核心考点

  • this 指向机制
  • 构造函数能力
  • arguments 对象
  • 原型链结构
  • 函数名绑定

常见坑点与解决方案

  1. 错误使用箭头函数作为构造函数

    • 坑点:箭头函数没有 prototype 属性,使用 new 关键字会报错
    • 示例
      javascript
      const Person = (name) => {
        this.name = name;
      };
      // TypeError: Person is not a constructor
      const person = new Person('Alice');
    • 解决方案:使用普通函数作为构造函数
      javascript
      function Person(name) {
        this.name = name;
      }
      const person = new Person('Alice'); // 正确
  2. 在事件回调中使用箭头函数

    • 坑点this 指向定义时的作用域(如 window),而非事件目标元素
    • 示例
      javascript
      button.addEventListener('click', () => {
        console.log(this); // 指向 window,而非 button 元素
        this.innerHTML = 'Clicked'; // 无法修改按钮内容
      });
    • 解决方案:使用普通函数或通过 event.target 获取目标元素
      javascript
      button.addEventListener('click', function() {
        console.log(this); // 指向 button 元素
        this.innerHTML = 'Clicked'; // 正确修改按钮内容
      });
      // 或使用箭头函数 + event.target
      button.addEventListener('click', (event) => {
        event.target.innerHTML = 'Clicked'; // 通过 event.target 获取目标
      });
  3. 在对象方法中使用箭头函数

    • 坑点this 不指向对象实例,而是指向定义时的外部作用域
    • 示例
      javascript
      const counter = {
        value: 0,
        increment: () => {
          this.value++; // this 指向 window,value 始终为 NaN
        }
      };
      counter.increment();
      console.log(counter.value); // NaN
    • 解决方案:使用普通函数或 ES6 方法简写
      javascript
      const counter = {
        value: 0,
        increment() {
          this.value++; // this 指向 counter 对象
        }
      };
      counter.increment();
      console.log(counter.value); // 1
  4. 需要动态改变 this 的场景

    • 坑点call/apply/bind 无法改变箭头函数的 this 指向
    • 示例
      javascript
      const obj1 = { name: 'obj1' };
      const obj2 = { name: 'obj2' };
      const getThis = () => this;
      console.log(getThis.call(obj1)); // 指向 window,而非 obj1
    • 解决方案:使用普通函数
      javascript
      function getThis() { return this; }
      console.log(getThis.call(obj1)); // 指向 obj1

易错点与解决方案

  1. 误认为箭头函数内部有 arguments 对象

    • 错误const fn = () => console.log(arguments); // 引用外部作用域的 arguments
    • 正确:使用剩余参数 ...args
      javascript
      const fn = (...args) => console.log(args);
      fn(1, 2, 3); // [1, 2, 3]
  2. 忽略箭头函数没有 supernew.target

    • 错误:在箭头函数中使用 supernew.target
    • 正确:需要这些特性时使用普通函数
  3. 对箭头函数的简写语法理解错误

    • 错误const getObj = () => { name: 'Alice' }; // 返回 undefined(被当作代码块)
    • 正确:使用括号包裹对象字面量
      javascript
      const getObj = () => ({ name: 'Alice' }); // 返回 { name: 'Alice' }

联合记忆

  • 箭头函数 = 没有 this/arguments/new/prototype 的简化函数
  • 普通函数 = 完整的函数特性(this 动态绑定、构造能力、原型链)

记忆口诀: "箭头函数四没有,this指向定义处;构造事件莫乱用,arguments也没有。"

企业级应用

  • 推荐场景:数组方法回调、简化的函数表达式、需要捕获外部 this 的回调
  • 禁止场景:构造函数、事件处理函数、对象方法、需要动态 this 的函数
javascript
// 正确使用:数组方法回调
const users = [1, 2, 3].map(id => getUserById(id));

// 正确使用:捕获外部 this
class User {
  constructor(name) {
    this.name = name;
    // 箭头函数正确捕获实例 this
    this.sayHello = () => console.log(`Hello, ${this.name}`);
  }
}
const user = new User('Bob');
setTimeout(user.sayHello, 1000); // 正确输出 "Hello, Bob"

2. Promise 与异步编程

面试题:请实现一个 Promise 并行限制函数 promiseLimit(tasks, limit),并说明 Promise 的状态变化机制。

核心考点

  • Promise 三种状态(pending/fulfilled/rejected)
  • Promise 链式调用与错误捕获
  • Promise.all/Promise.race/Promise.allSettled 的区别
  • 异步流程控制

常见坑点与解决方案

  1. Promise 状态不可逆

    • 坑点:一旦 Promise 从 pending 状态变为 resolved 或 rejected,状态就不能再改变
    • 示例
      javascript
      const promise = new Promise((resolve, reject) => {
        resolve('success');
        reject('error'); // 无效,状态已变为 resolved
      });
      promise.then(console.log).catch(console.error); // 输出 "success"
    • 解决方案:确保在 Promise 构造函数中只有一条状态变更路径
  2. 忘记处理 Promise 错误

    • 坑点:未处理的 Promise 错误会导致应用崩溃或产生未捕获错误警告
    • 示例
      javascript
      const promise = new Promise((resolve, reject) => {
        reject(new Error('Network error'));
      });
      promise.then(console.log); // 未处理错误,控制台会显示 Uncaught (in promise) Error
    • 解决方案:始终使用 .catch()try/catch(对于 async/await)处理错误
      javascript
      promise.then(console.log).catch(console.error); // 正确处理错误
  3. 错误的并行限制实现

    • 坑点:未正确管理并发数和任务队列,导致超出限制或任务执行顺序错误
    • 示例:直接使用 Promise.all 处理大量任务会导致资源耗尽
      javascript
      // 错误:大量任务同时执行
      Promise.all(tasks.map(task => task())).then(console.log);
    • 解决方案:使用计数器和任务队列实现并行限制(见下文企业级实现)
  4. Promise 回调中的 this 指向

    • 坑点:普通函数作为 Promise 回调时,this 指向可能不符合预期
    • 示例
      javascript
      const obj = {
        value: 1,
        async getValue() {
          // this 指向 obj
          return new Promise((resolve) => {
            setTimeout(function() {
              resolve(this.value); // this 指向 window,返回 undefined
            }, 100);
          });
        }
      };
    • 解决方案:使用箭头函数或绑定 this
      javascript
      setTimeout(() => resolve(this.value), 100); // 箭头函数继承外部 this
      // 或
      setTimeout(function() { resolve(this.value); }.bind(this), 100);

易错点与解决方案

  1. 混淆 Promise.all 与 Promise.race 的行为

    • 错误Promise.all([p1, p2]) 只要有一个失败就拒绝,Promise.race([p1, p2]) 返回第一个完成的结果
    • 正确
      javascript
      // Promise.all:全部成功才成功,任一失败则失败
      Promise.all([Promise.resolve(1), Promise.resolve(2)]).then(console.log); // [1, 2]
      Promise.all([Promise.resolve(1), Promise.reject(2)]).catch(console.error); // 2
      
      // Promise.race:返回第一个完成(成功或失败)的结果
      Promise.race([Promise.resolve(1), Promise.reject(2)]).then(console.log); // 1
      Promise.race([Promise.reject(1), Promise.resolve(2)]).catch(console.error); // 1
  2. 忽略 Promise.then 中的错误会冒泡

    • 错误:认为每个 then 都需要单独处理错误
    • 正确:错误会沿链式调用冒泡,只需在最后添加一个 catch
      javascript
      fetch('url1')
        .then(res => res.json())
        .then(data => fetch('url2', { body: data }))
        .then(res => res.json())
        .catch(error => console.error('Any error:', error)); // 捕获所有错误
  3. 错误处理顺序不当导致的错误丢失

    • 错误:在 then 之后立即添加 catch,导致后续 then 无法获取正确结果
    • 正确:将 catch 放在链式调用的末尾
      javascript
      // 错误:catch 之后的 then 会继续执行,即使前面有错误
      fetch('url')
        .catch(error => console.error(error))
        .then(data => console.log(data)); // 可能处理 undefined
      
      // 正确:catch 捕获所有错误,后续 then 只有在成功时才执行
      fetch('url')
        .then(data => console.log(data))
        .catch(error => console.error(error));

联合记忆

  • Promise 状态:pending → resolved/rejected(不可逆)
  • 错误处理:链式调用中的 .catch() 可以捕获前面所有 Promise 的错误
  • 并行控制:使用计数器和任务队列实现限制

记忆口诀: "Promise 有三态,pending变settled;all等全成功,race看谁快;错误要捕获,不然冒泡外。"

企业级实现 - Promise 并行限制

核心逻辑说明

  1. 初始化:创建结果数组、索引、运行计数器和任务队列
  2. 执行函数:定义 executeNext 函数处理任务执行逻辑
  3. 任务调度:当运行任务数小于限制且有未执行任务时,启动新任务
  4. 状态管理:任务完成后更新运行计数器,并递归调用 executeNext
  5. 结果返回:当所有任务完成时,resolve 结果数组
javascript
function promiseLimit(tasks, limit) {
  const results = [];
  let index = 0;       // 当前要执行的任务索引
  let running = 0;     // 正在运行的任务数

  return new Promise((resolve, reject) => {
    // 执行下一个任务
    const executeNext = () => {
      // 所有任务执行完成,resolve 结果
      if (index >= tasks.length && running === 0) {
        resolve(results);
        return;
      }

      // 启动尽可能多的任务,直到达到限制
      while (running < limit && index < tasks.length) {
        const taskIndex = index++; // 保存当前任务索引
        running++;

        tasks[taskIndex]()
          .then(result => {
            results[taskIndex] = result; // 按原顺序保存结果
          })
          .catch(error => {
            results[taskIndex] = error;  // 错误也按原顺序保存
          })
          .finally(() => {
            running--; // 任务完成,运行计数器减1
            executeNext(); // 递归执行下一个任务
          });
      }
    };

    executeNext(); // 启动执行
  });
}

3. async/await 异步编程

面试题:async/await 相比 Promise 有哪些优势?如何处理 async/await 中的错误?请举例说明。

核心考点

  • async/await 语法糖本质
  • 错误处理机制
  • 并发控制
  • 与 Promise 的关系

常见坑点

  1. 忘记使用 await:导致异步操作未等待完成

    • 解决方案:在异步操作前添加 await 关键字
      javascript
      // 错误:未等待异步操作完成
      async function getData() {
        fetch('/api/data'); // 没有 await
      }
      
      // 正确:等待异步操作完成
      async function getData() {
        await fetch('/api/data');
      }
  2. 错误的并发处理:使用串行 await 而非 Promise.all 导致性能问题

    • 解决方案:使用 Promise.all 并行处理异步操作
      javascript
      // 错误:串行执行(耗时 = 3秒 + 2秒 + 1秒 = 6秒)
      async function fetchData() {
        const data1 = await fetch('/api/data1'); // 3秒
        const data2 = await fetch('/api/data2'); // 2秒
        const data3 = await fetch('/api/data3'); // 1秒
        return [data1, data2, data3];
      }
      
      // 正确:并行执行(耗时 = max(3秒, 2秒, 1秒) = 3秒)
      async function fetchData() {
        const [data1, data2, data3] = await Promise.all([
          fetch('/api/data1'),
          fetch('/api/data2'),
          fetch('/api/data3')
        ]);
        return [data1, data2, data3];
      }
  3. 未处理的 async 函数错误:async 函数返回的 Promise 错误未被捕获

    • 解决方案:使用 try/catch 或 .catch() 处理错误
      javascript
      // 方法1:try/catch
      async function fetchData() {
        try {
          const response = await fetch('/api/data');
          return await response.json();
        } catch (error) {
          console.error('请求失败:', error);
          return null;
        }
      }
      
      // 方法2:.catch()
      fetchData().catch(error => {
        console.error('处理失败:', error);
      });
  4. 在循环中错误使用 await:导致串行执行而非并行

    • 解决方案:使用 Promise.all + map 实现并行处理
      javascript
      // 错误:串行执行
      async function processItems(items) {
        for (const item of items) {
          await processItem(item);
        }
      }
      
      // 正确:并行执行
      async function processItems(items) {
        await Promise.all(items.map(item => processItem(item)));
      }

易错点

  • 误认为 async/await 是新的异步机制(本质是 Promise + Generator)

    • 解决方案:理解 async/await 的本质
      javascript
      // async/await 只是语法糖
      async function fetchData() {
        return await fetch('/api/data');
      }
      
      // 等价于 Promise
      function fetchData() {
        return fetch('/api/data');
      }
  • 忽略 try/catch 可以捕获 await 操作的错误

    • 解决方案:在 try/catch 中包裹 await 操作
      javascript
      async function fetchData() {
        try {
          const response = await fetch('/api/invalid-url');
          return await response.json();
        } catch (error) {
          console.error('捕获到错误:', error);
        }
      }
  • 错误地使用 await 在非 Promise 对象上

    • 解决方案:确保 await 后面是 Promise 对象
      javascript
      // 非 Promise 对象会被包装成已解决的 Promise
      async function test() {
        const result = await 123;
        console.log(result); // 123
        
        const obj = await { name: 'test' };
        console.log(obj); // { name: 'test' }
      }

联合记忆

  • async 函数 = Promise 包装器
  • await = Promise.then 的语法糖
  • 错误处理 = try/catch 替代 Promise.catch

记忆口诀: "async 函数返回 Promise,await 等待 Promise 成;错误处理用 try/catch,并发要用 Promise.all。"

企业级案例

javascript
// 错误的串行执行
async function fetchAllData() {
  const user = await fetchUser();
  const posts = await fetchPosts(); // 必须等 user 完成
  return { user, posts };
}

// 正确的并行执行
async function fetchAllData() {
  const [user, posts] = await Promise.all([
    fetchUser(),
    fetchPosts()
  ]);
  return { user, posts };
}

// 错误处理
async function safeFetch() {
  try {
    const data = await fetchData();
    return data;
  } catch (error) {
    console.error('Fetch failed:', error);
    return null;
  }
}

二、Vue 全家桶与源码解析

1. Vue 2 响应式原理

面试题:请详细描述 Vue 2 的响应式原理,包括 Object.defineProperty 的缺陷以及如何解决。

核心考点

  • Object.defineProperty 实现
  • 依赖收集机制
  • 发布-订阅模式
  • 响应式系统的缺陷

常见坑点与解决方案

  1. 新增属性无法响应

    • 坑点:Object.defineProperty 只能监听对象创建时已存在的属性,新增属性不会触发视图更新
    • 示例
      javascript
      const vm = new Vue({
        data() {
          return {
            user: { name: 'Alice' }
          };
        }
      });
      // 新增属性,视图不会更新
      vm.user.age = 20;
    • 解决方案:使用 Vue.set()this.$set() 新增响应式属性
      javascript
      this.$set(vm.user, 'age', 20); // 视图会更新
      // 或使用 Object.assign 创建新对象
      vm.user = Object.assign({}, vm.user, { age: 20 });
  2. 数组索引和长度变化无法响应

    • 坑点:直接修改数组索引或长度不会触发响应式更新
    • 示例
      javascript
      const vm = new Vue({
        data() {
          return {
            list: [1, 2, 3]
          };
        }
      });
      // 直接修改索引,视图不会更新
      vm.list[0] = 10;
      // 修改长度,视图不会更新
      vm.list.length = 2;
    • 解决方案:使用 Vue 重写的数组方法或 Vue.set()
      javascript
      // 使用重写的数组方法
      vm.list.splice(0, 1, 10); // 视图会更新
      // 使用 Vue.set()
      this.$set(vm.list, 0, 10); // 视图会更新
  3. 深度嵌套对象的性能问题

    • 坑点:Vue 会递归遍历对象的所有嵌套属性,大型嵌套对象会导致性能问题
    • 示例
      javascript
      const vm = new Vue({
        data() {
          return {
            // 大型嵌套对象,初始化性能差
            deepData: { /* 嵌套层级很深的对象 */ }
          };
        }
      });
    • 解决方案
      • 避免不必要的深度嵌套
      • 使用 Object.freeze() 冻结不需要响应的对象
      • 考虑使用 Vue 3 的 Proxy(自动实现响应式,性能更好)
  4. 响应式对象与原始对象的混淆

    • 坑点:将响应式对象的属性赋值给原始对象,修改原始对象不会触发更新
    • 示例
      javascript
      const vm = new Vue({
        data() {
          return {
            user: { name: 'Alice' }
          };
        }
      });
      // 获取响应式对象的属性(原始值)
      let name = vm.user.name;
      // 修改原始值,不会触发更新
      name = 'Bob';
    • 解决方案:始终操作响应式对象的属性,而不是原始值
      javascript
      vm.user.name = 'Bob'; // 正确,视图会更新

易错点与解决方案

  1. 误认为 Vue.set 可以监听所有数组索引变化

    • 错误Vue.set(arr, index, value) 对数组的支持有限,大型数组索引操作可能不高效
    • 正确:优先使用 Vue 重写的数组方法(push, pop, shift, unshift, splice, sort, reverse)
  2. 忽略对象冻结会破坏响应式

    • 错误:对冻结的对象使用 Vue.set 不会生效
    • 示例
      javascript
      const vm = new Vue({
        data() {
          return {
            user: Object.freeze({ name: 'Alice' })
          };
        }
      });
      this.$set(vm.user, 'age', 20); // 无效,对象已冻结
  3. 对 Vue.$set 的工作原理理解错误

    • 错误:认为 Vue.$set 是给对象添加新属性
    • 正确Vue.$set 是通过重新定义属性的 getter/setter 使其响应式

联合记忆

  • 响应式 = Object.defineProperty + 依赖收集 + 发布-订阅
  • 缺陷 = 新增属性/数组变化/深度嵌套/性能问题
  • 解决方案 = Vue.set/Vue.delete/重写数组方法

记忆口诀: "Vue 响应式靠 defineProperty,get 收集依赖 set 通知;新增属性不响应,数组方法要重写;深度嵌套有性能,Vue.set 来救场。"

源码核心 - Vue 2 响应式实现

核心逻辑说明

  1. 递归监听observe() 函数递归处理嵌套对象,使其所有属性都响应式
  2. 依赖收集:通过 Dep 类管理依赖,在 get 方法中收集 Watcher
  3. 通知更新:在 set 方法中通知所有依赖的 Watcher 更新
  4. 深度监听:新值设置时重新调用 observe() 确保新值也是响应式
javascript
// Vue 2 响应式核心实现(简化版)
function defineReactive(obj, key, val) {
  // 递归处理嵌套对象,使其响应式
  observe(val);
  
  // 创建依赖收集器,管理该属性的所有依赖
  const dep = new Dep();
  
  // 使用 Object.defineProperty 定义响应式属性
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      // 依赖收集:将当前 Watcher 添加到依赖列表
      if (Dep.target) {
        dep.depend();
      }
      return val;
    },
    set(newVal) {
      // 值未变化,直接返回
      if (newVal === val) return;
      
      // 更新值
      val = newVal;
      
      // 深度监听新值,确保新值也是响应式
      observe(newVal);
      
      // 通知更新:触发所有依赖的 Watcher 更新
      dep.notify();
    }
  });
}

2. Vue 3 Composition API

面试题:Vue 3 Composition API 相比 Vue 2 Options API 有哪些优势?请结合源码分析其实现原理。

核心考点

  • Composition API 设计思想
  • setup 函数生命周期
  • 响应式 API 实现
  • 逻辑复用机制

常见坑点

  1. setup 函数中的 this 指向:setup 中 this 为 undefined
  2. 响应式数据的错误使用:直接修改 reactive 对象的属性与修改 ref 的 value
  3. 生命周期钩子的变化:需要使用 onMounted 等函数式钩子
  4. 逻辑复用的过度使用:导致代码难以追踪

易错点

  • 混淆 ref 与 reactive 的使用场景
  • 忽略 computed 的缓存机制
  • 错误地在 setup 中访问 DOM(需使用 ref + onMounted)

联合记忆

  • Composition API = 逻辑复用 + 类型友好 + 灵活组织
  • 响应式 API = ref(基本类型) + reactive(引用类型) + computed(计算属性)
  • 生命周期 = onMounted/onUpdated/onUnmounted 等

记忆口诀: "Composition API 好,逻辑复用真巧妙;setup 函数无 this,响应式用 ref/reactive;生命周期要前缀,逻辑组合更灵活。"

源码核心

javascript
// Vue 3 ref 实现原理(简化版)
function ref(value) {
  const refObject = {
    _isRef: true,
    get value() {
      // 依赖收集
      track(refObject, 'value');
      return value;
    },
    set value(newValue) {
      value = newValue;
      // 通知更新
      trigger(refObject, 'value');
    }
  };
  return refObject;
}

// Vue 3 reactive 实现原理(简化版)
function reactive(target) {
  return createReactiveObject(target, false, mutableHandlers);
}

// 代理处理器
const mutableHandlers = {
  get(target, key, receiver) {
    const result = Reflect.get(target, key, receiver);
    // 依赖收集
    track(target, key);
    // 递归处理嵌套对象
    if (isObject(result)) {
      return reactive(result);
    }
    return 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;
  }
};

3. Vue Router 原理

面试题:Vue Router 的两种模式(hash 和 history)有什么区别?如何实现一个简单的 Vue Router?

核心考点

  • hash 与 history 模式的实现原理
  • 路由守卫机制
  • 路由懒加载
  • 动态路由与嵌套路由

常见坑点

  1. history 模式的 404 问题:需要后端配置 fallback

    • 解决方案
      nginx
      # Nginx 配置
      location / {
        try_files $uri $uri/ /index.html;
      }
      • 原理:将所有请求转发到 index.html,由前端路由处理
  2. 路由懒加载的错误写法:未正确使用 import() 函数

    • 错误写法component: require('../components/Home.vue')
    • 正确写法component: () => import('../components/Home.vue')
  3. 路由守卫的执行顺序:全局守卫、路由守卫、组件守卫的顺序

    • 解决方案:明确守卫执行顺序(全局 beforeEach → 路由 beforeEnter → 组件 beforeRouteEnter → 全局 beforeResolve → 组件渲染 → 全局 afterEach)
  4. 动态路由的权限控制:需要结合路由守卫实现

    • 解决方案
      javascript
      // 全局前置守卫
      router.beforeEach((to, from, next) => {
        const userRole = store.state.user.role;
        if (to.meta.requiredRoles && !to.meta.requiredRoles.includes(userRole)) {
          next('/403');
        } else {
          next();
        }
      });

易错点

  • 混淆 router.push 与 router.replace 的区别

    • 解决方案:明确用途 - push 添加历史记录,replace 替换当前记录
    • 代码示例:
      javascript
      // 添加新历史记录
      router.push('/home');
      // 替换当前历史记录
      router.replace('/login');
  • 忽略路由参数变化不会触发组件重新渲染

    • 解决方案:使用 watch 监听路由参数变化或 beforeRouteUpdate 守卫
    • 代码示例:
      javascript
      // 方法1:watch 监听
      watch: {
        $route(to, from) {
          this.loadData(to.params.id);
        }
      }
      
      // 方法2:beforeRouteUpdate 守卫
      beforeRouteUpdate(to, from, next) {
        this.loadData(to.params.id);
        next();
      }
  • 对路由元信息(meta)的错误使用

    • 解决方案:在路由配置中定义 meta 信息,在守卫中使用
    • 代码示例:
      javascript
      // 路由配置
      const routes = [
        {
          path: '/dashboard',
          component: Dashboard,
          meta: {
            requiresAuth: true,
            title: '仪表盘'
          }
        }
      ];
      
      // 在守卫中使用
      router.beforeEach((to, from, next) => {
        if (to.meta.title) {
          document.title = to.meta.title;
        }
        next();
      });

联合记忆

  • hash 模式 = URL 锚点 + onhashchange 事件
  • history 模式 = HTML5 History API + 后端配置
  • 路由守卫 = 全局(beforeEach/beforeResolve/afterEach) + 路由(beforeEnter) + 组件(beforeRouteEnter/beforeRouteUpdate/beforeRouteLeave)

记忆口诀: "Vue Router 两种模式,hash 简单 history 美;hash 靠锚点,history 要后端;路由守卫分三层,全局路由组件齐。"

简易实现

javascript
// 简单的 Vue Router 实现
class VueRouter {
  constructor(options) {
    this.options = options;
    this.routeMap = {};
    this.app = new Vue({ data: { currentRoute: '/' } });
    
    this.init();
    this.createRouteMap();
    this.initComponents(Vue);
  }
  
  init() {
    // 监听 URL 变化
    this.hashChange = this.hashChange.bind(this);
    window.addEventListener('load', this.hashChange);
    window.addEventListener('hashchange', this.hashChange);
  }
  
  createRouteMap() {
    // 构建路由映射
    this.options.routes.forEach(route => {
      this.routeMap[route.path] = route.component;
    });
  }
  
  initComponents(Vue) {
    // 注册 router-link 和 router-view 组件
    Vue.component('router-link', {
      props: { to: String },
      render(h) {
        return h('a', { attrs: { href: '#' + this.to } }, this.$slots.default);
      }
    });
    
    Vue.component('router-view', {
      render: h => {
        const component = this.routeMap[this.app.currentRoute];
        return h(component);
      }
    });
  }
  
  hashChange() {
    this.app.currentRoute = window.location.hash.slice(1) || '/';
  }
}

4. Vuex/Pinia 状态管理

面试题:Vuex 与 Pinia 有哪些区别?请结合企业级应用场景说明如何选择。

核心考点

  • 状态管理的核心概念
  • Vuex 与 Pinia 的架构差异
  • TypeScript 支持
  • 模块化设计
  • 持久化存储

常见坑点

  1. Vuex 模块化的命名空间问题:忘记启用 namespaced
  2. 状态更新的方式错误:直接修改 state 而非通过 mutation
  3. 异步操作的处理:在 mutation 中执行异步操作
  4. Pinia 的自动导入配置错误:导致 store 无法访问

易错点

  • 混淆 Vuex 的 mutation 与 action
  • 忽略 Pinia 可以直接修改 state
  • 对状态持久化的实现方式理解错误

联合记忆

  • Vuex = 集中式存储 + 严格的单向数据流 + 模块化
  • Pinia = 模块化设计 + TypeScript 友好 + 灵活的 API + 轻量级
  • 选择依据 = 项目规模 + TypeScript 支持 + 团队熟悉度

记忆口诀: "Vuex 老大哥,单向数据流;mutation 同步,action 异步;模块要命名空间,使用稍显复杂;Pinia 新贵来,API 更简洁;支持 TypeScript,状态直接改;模块化更灵活,轻量又好用。"

企业级选择

  • 小型项目:直接使用 Composition API 的响应式 API
  • 中型项目:Pinia(更好的 TypeScript 支持和开发体验)
  • 大型项目:Vuex(更严格的数据流控制,适合复杂业务)

三、构建工具(Webpack/Rollup)

1. Webpack 核心原理

面试题:请详细描述 Webpack 的工作原理,包括核心概念、构建流程和优化策略。

核心考点

  • Webpack 核心概念(Entry/Output/Loader/Plugin)
  • 构建流程(初始化/编译/输出)
  • 依赖图构建
  • 代码分割策略
  • 性能优化

常见坑点

  1. Loader 配置顺序错误:Webpack 按从右到左的顺序执行 Loader
  2. Plugin 配置不当:重复使用或配置错误导致构建失败
  3. 代码分割的错误使用:导致 chunk 过多或过大
  4. Tree Shaking 不生效:未使用 ES6 模块或配置错误

易错点

  • 混淆 Loader 与 Plugin 的作用(Loader 处理文件,Plugin 扩展功能)
  • 忽略 Webpack 缓存机制的配置
  • 错误地使用 resolve.alias 导致模块解析错误

联合记忆

  • Webpack 工作流 = 入口分析 → 依赖图构建 → Loader 转换 → Plugin 处理 → 输出文件
  • 优化策略 = 代码分割 + Tree Shaking + 缓存 + 多线程构建
  • 核心概念 = Entry/Output/Loader/Plugin/Chunk/Module

记忆口诀: "Webpack 是打包机,核心概念要牢记;Entry 入口 Output 出,Loader 处理文件 Plugin 扩展;构建流程分三步,初始化编译再输出;优化策略多,代码分割 Tree Shaking 不可少。"

核心配置

javascript
// Webpack 核心配置(企业级优化版)
module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash].js',
    chunkFilename: '[name].[contenthash].chunk.js'
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              cacheDirectory: true // 缓存 Babel 编译结果
            }
          }
        ]
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html'
    }),
    new webpack.optimize.SplitChunksPlugin({
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all'
        }
      }
    })
  ],
  optimization: {
    usedExports: true, // 启用 Tree Shaking
    minimize: true,
    minimizer: [
      new TerserPlugin({
        parallel: true // 多线程压缩
      })
    ]
  }
};

2. Webpack 自定义插件

面试题:如何编写一个 Webpack 自定义插件?请实现一个简单的插件来分析构建时间。

核心考点

  • Webpack 插件 API
  • Tapable 事件流
  • 插件生命周期
  • 构建分析

常见坑点

  1. 错误的钩子时机选择:选择不适合的生命周期钩子

    • 解决方案:根据插件功能选择正确的钩子
      • 编译开始:compile/compilation
      • 文件处理:emit/afterEmit
      • 编译结束:done
    • 参考:Webpack Hooks 文档
  2. 未正确处理异步操作:异步插件需要调用 callback 或返回 Promise

    • 解决方案
      • 异步串行钩子:使用 tapAsync + callback
      • 异步并行钩子:使用 tapPromise + Promise
    • 代码示例:
      javascript
      // 异步串行钩子
      compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => {
        // 异步操作
        setTimeout(() => {
          console.log('异步操作完成');
          callback();
        }, 1000);
      });
      
      // 异步并行钩子
      compiler.hooks.emit.tapPromise('MyPlugin', (compilation) => {
        // 异步操作
        return new Promise(resolve => {
          setTimeout(() => {
            console.log('异步操作完成');
            resolve();
          }, 1000);
        });
      });
  3. 插件实例化错误:忘记使用 new 关键字创建插件实例

    • 解决方案:使用 new 关键字实例化插件
      • 错误:plugins: [MyPlugin]
      • 正确:plugins: [new MyPlugin()]
  4. 内存泄漏:未正确清理事件监听器

    • 解决方案:在插件中添加清理逻辑
      javascript
      apply(compiler) {
        const handleEmit = (compilation) => {
          // 处理逻辑
        };
        
        compiler.hooks.emit.tap('MyPlugin', handleEmit);
        
        // 在插件卸载时清理
        compiler.hooks.done.tap('MyPlugin', () => {
          compiler.hooks.emit.tap.remove('MyPlugin', handleEmit);
        });
      }

易错点

  • 混淆同步钩子与异步钩子的使用

    • 解决方案:根据钩子类型选择正确的注册方法
      • 同步钩子:tap
      • 异步串行:tapAsync
      • 异步并行:tapPromise
  • 忽略 Webpack 版本的 API 差异

    • 解决方案
      • 查看官方文档,注意版本兼容性
      • 使用 webpack-merge 处理不同版本配置
      javascript
      // 使用 webpack-merge 处理版本差异
      const { merge } = require('webpack-merge');
      const commonConfig = require('./webpack.common.js');
      
      module.exports = merge(commonConfig, {
        // 特定版本的配置
      });
  • 对 Tapable 事件流的工作原理理解错误

    • 解决方案
      • 学习 Tapable 库的基本使用
      • 理解不同钩子类型的执行机制
      javascript
      const { SyncHook, AsyncParallelHook } = require('tapable');
      
      // 创建同步钩子
      const syncHook = new SyncHook(['arg1', 'arg2']);
      syncHook.tap('Hook1', (arg1, arg2) => {
        console.log('Hook1:', arg1, arg2);
      });

联合记忆

  • Webpack 插件 = 构造函数 + apply 方法 + 钩子注册
  • 钩子类型 = 同步(SyncHook)/异步并行(AsyncParallelHook)/异步串行(AsyncSeriesHook)
  • 实现步骤 = 注册钩子 → 处理逻辑 → 调用回调

记忆口诀: "Webpack 插件构造好,apply 方法不能少;Tapable 事件流,钩子时机要选好;同步异步要分清,异步别忘 callback;插件功能强,扩展 Webpack 没商量。"

企业级实现

javascript
// Webpack 构建时间分析插件
class BuildTimePlugin {
  constructor(options = {}) {
    this.options = options;
    this.startTime = 0;
    this.endTime = 0;
  }
  
  apply(compiler) {
    // 注册编译开始钩子
    compiler.hooks.compile.tap('BuildTimePlugin', () => {
      this.startTime = Date.now();
      console.log('Build started:', new Date().toLocaleString());
    });
    
    // 注册编译完成钩子(异步)
    compiler.hooks.done.tapAsync('BuildTimePlugin', (stats, callback) => {
      this.endTime = Date.now();
      const buildTime = this.endTime - this.startTime;
      console.log('Build completed:', new Date().toLocaleString());
      console.log('Total build time:', buildTime, 'ms');
      
      // 可以将构建时间写入文件或发送到监控系统
      if (this.options.outputFile) {
        fs.writeFileSync(
          this.options.outputFile,
          JSON.stringify({ buildTime, timestamp: this.endTime })
        );
      }
      
      callback();
    });
  }
}

module.exports = BuildTimePlugin;

3. Rollup 核心原理

面试题:Rollup 与 Webpack 有哪些区别?请结合使用场景说明如何选择。

核心考点

  • Rollup 与 Webpack 的设计理念差异
  • Tree Shaking 实现
  • 输出格式支持
  • 使用场景选择
  • 性能对比

常见坑点

  1. Rollup 处理 CommonJS 模块:需要 @rollup/plugin-commonjs 插件

    • 解决方案:安装并配置 @rollup/plugin-commonjs 和 @rollup/plugin-node-resolve
      javascript
      import resolve from '@rollup/plugin-node-resolve';
      import commonjs from '@rollup/plugin-commonjs';
      
      export default {
        // ...
        plugins: [
          resolve(), // 解析 Node.js 模块
          commonjs() // 将 CommonJS 转为 ES6
        ]
      };
  2. 动态导入的处理:Rollup 对动态导入的支持与 Webpack 不同

    • 解决方案:使用 output.dir 替代 output.file 并启用 experimentalTopLevelAwait
      javascript
      export default {
        input: 'src/index.js',
        output: {
          dir: 'dist', // 使用 dir 而非 file
          format: 'es'
        },
        experimentalTopLevelAwait: true
      };
  3. 代码分割的配置:Rollup 的代码分割配置与 Webpack 差异较大

    • 解决方案:使用 output.manualChunks 自定义代码分割
      javascript
      export default {
        // ...
        output: {
          dir: 'dist',
          format: 'es',
          manualChunks: {
            // 将 lodash 单独打包
            'lodash': ['lodash'],
            // 将 react 相关模块单独打包
            'react': ['react', 'react-dom']
          }
        }
      };
  4. 开发服务器的缺乏:需要额外配置(如 rollup-plugin-serve)

    • 解决方案:安装并配置 rollup-plugin-serve 和 rollup-plugin-livereload
      javascript
      import serve from 'rollup-plugin-serve';
      import livereload from 'rollup-plugin-livereload';
      
      export default {
        // ...
        plugins: [
          // ...
          serve({
            open: true,
            port: 3000,
            contentBase: 'dist'
          }),
          livereload('dist')
        ]
      };

易错点

  • 误认为 Rollup 性能一定比 Webpack 好(取决于使用场景)

    • 解决方案:根据项目类型选择合适的打包工具
      • 库打包:Rollup(更小的体积,更好的 Tree Shaking)
      • 应用打包:Webpack(更好的动态导入支持,丰富的插件生态)
  • 忽略 Rollup 对浏览器环境的兼容性处理

    • 解决方案:使用 @rollup/plugin-babel 和 @babel/preset-env
      javascript
      import babel from '@rollup/plugin-babel';
      
      export default {
        // ...
        plugins: [
          // ...
          babel({
            exclude: 'node_modules/**',
            presets: [
              ['@babel/preset-env', {
                targets: { browsers: ['> 1%, last 2 versions'] }
              }]
            ]
          })
        ]
      };
  • 对 Rollup 的输出格式(es/cjs/amd/iife/umd)理解错误

    • 解决方案:明确各种输出格式的使用场景

      格式用途示例
      esES模块(现代浏览器)import { foo } from './lib'
      cjsCommonJS模块(Node.js)const { foo } = require('./lib')
      amdAMD模块(RequireJS)define(['lib'], function(lib) {})
      iife自执行函数(浏览器)<script src="lib.js"></script>
      umd通用模块(浏览器/Node.js/AMD)支持多种导入方式
      javascript
      // 输出多种格式
      export default {
        // ...
        output: [
          { file: 'dist/lib.es.js', format: 'es' },
          { file: 'dist/lib.cjs.js', format: 'cjs' },
          { file: 'dist/lib.umd.js', format: 'umd', name: 'MyLibrary' }
        ]
      };

联合记忆

  • Rollup = 下一代 ES 模块打包器 + 优秀的 Tree Shaking + 适合库打包
  • Webpack = 功能强大的模块打包器 + 丰富的生态 + 适合应用打包
  • 选择依据 = 项目类型(库/应用) + 功能需求 + 性能要求

记忆口诀: "Rollup 轻量又快速,Tree Shaking 真厉害;适合打包库,输出格式多;Webpack 功能全,生态丰富应用广;库用 Rollup,应用用 Webpack。"

核心配置

javascript
// Rollup 核心配置(库打包版)
export default {
  input: 'src/index.js',
  output: [
    {
      file: 'dist/bundle.cjs.js',
      format: 'cjs',
      sourcemap: true
    },
    {
      file: 'dist/bundle.esm.js',
      format: 'es',
      sourcemap: true
    },
    {
      file: 'dist/bundle.umd.js',
      format: 'umd',
      name: 'MyLibrary',
      sourcemap: true
    }
  ],
  plugins: [
    resolve(),
    commonjs(),
    babel({
      exclude: 'node_modules/**'
    }),
    terser()
  ],
  external: ['react', 'react-dom'] // 外部依赖
};

四、Node.js 框架(Koa/Express)

1. Koa 核心原理

面试题:Koa 与 Express 有哪些区别?请详细描述 Koa 的中间件机制。

核心考点

  • 中间件机制(洋葱模型)
  • 异步流程处理
  • 上下文(Context)对象
  • 错误处理机制

常见坑点

  1. 中间件执行顺序错误:不理解洋葱模型的执行流程
  2. 异步中间件的错误处理:未正确使用 try/catch 或 await
  3. 上下文对象的生命周期:每个请求创建一个新的 Context
  4. 响应处理的时机:需要确保在中间件链中调用 response.end()

易错点

  • 混淆 Koa 的中间件与 Express 的中间件
  • 忽略 Koa 2 使用 async/await 处理异步
  • 对 Koa 的错误处理机制理解错误

联合记忆

  • Koa = 轻量级框架 + 洋葱模型中间件 + 异步友好
  • Express = 功能丰富 + 回调函数 + 内置路由/模板引擎
  • 中间件 = 洋葱模型(先进后出) + 异步处理

记忆口诀: "Koa 框架轻量级,中间件用洋葱模型;async/await 处理异步,上下文对象真方便;Express 功能全,回调函数处理异步;Koa 2 异步好,Express 传统稳。"

核心实现

javascript
// Koa 中间件洋葱模型实现(简化版)
class Koa {
  constructor() {
    this.middlewares = [];
  }
  
  use(fn) {
    this.middlewares.push(fn);
    return this;
  }
  
  async callback(ctx) {
    const dispatch = (i) => {
      if (i >= this.middlewares.length) return Promise.resolve();
      const middleware = this.middlewares[i];
      // 递归调用下一个中间件
      return Promise.resolve(middleware(ctx, () => dispatch(i + 1)));
    };
    return dispatch(0);
  }
  
  listen(...args) {
    const server = http.createServer(async (req, res) => {
      const ctx = this.createContext(req, res);
      await this.callback(ctx);
      this.response(ctx);
    });
    return server.listen(...args);
  }
}

2. Express 路由与中间件

面试题:如何在 Express 中实现路由模块化?请实现一个简单的认证中间件。

核心考点

  • 路由模块化
  • 中间件分类(应用级/路由级/错误处理/内置)
  • 认证机制
  • 错误处理

常见坑点

  1. 中间件顺序错误:路由中间件应放在通用中间件之后
  2. 错误处理中间件位置:必须放在所有路由中间件之后
  3. 路由路径匹配错误:Express 的路由匹配规则(先到先得)
  4. 异步中间件的错误处理:需要显式调用 next(err)

易错点

  • 混淆 app.use 与 app.get/app.post 的区别
  • 忽略中间件的 next() 调用
  • 对路由参数的处理错误

联合记忆

  • Express 中间件 = 函数(req, res, next)
  • 路由模块化 = express.Router()
  • 错误处理 = 四参数中间件(err, req, res, next)

记忆口诀: "Express 中间件,req res next 传;应用路由错误级,顺序摆放很关键;路由模块化用 Router,错误处理在最后;异步错误要 next(err),否则服务器崩溃。"

企业级实现

javascript
// Express 路由模块化
const express = require('express');
const router = express.Router();

// 认证中间件
const authMiddleware = (req, res, next) => {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) {
    return res.status(401).json({ error: 'Authentication required' });
  }
  
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded;
    next();
  } catch (error) {
    return res.status(401).json({ error: 'Invalid token' });
  }
};

// 受保护的路由
router.get('/profile', authMiddleware, (req, res) => {
  res.json({ user: req.user });
});

module.exports = router;

// 应用入口
const app = express();
app.use('/api', require('./routes/api'));

// 错误处理中间件(必须放在最后)
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ error: 'Something broke!' });
});

五、CSS 预处理器与后处理器

1. Sass/Less 预处理器

面试题:Sass 与 Less 有哪些区别?请实现一个响应式布局的 Mixin。

核心考点

  • 变量、嵌套、Mixin、继承
  • 编译方式(Sass 有缩进式语法)
  • 响应式布局实现
  • 性能对比

常见坑点

  1. 过度嵌套导致的 CSS 权重问题:嵌套层级过深会导致选择器权重过高
  2. Mixin 的性能问题:每个 Mixin 调用都会生成重复的 CSS
  3. 变量作用域的理解错误:Sass/Less 的变量作用域规则不同
  4. 编译错误的调试:预处理器的错误信息可能不够友好

易错点

  • 混淆 Sass 的缩进式语法与 SCSS 语法
  • 忽略 Less 的变量延迟加载特性
  • 对 Mixin 与继承的使用场景理解错误

联合记忆

  • Sass = Ruby 编写 + 缩进式语法 + 功能强大 + 编译速度快
  • Less = JavaScript 编写 + 类 CSS 语法 + 易于上手 + 编译速度较慢
  • 核心特性 = 变量($/@) + 嵌套 + Mixin + 继承

记忆口诀: "Sass Less 预处理器,变量嵌套 Mixin 齐;Sass 缩进式,Less 类 CSS;Mixin 代码复用,继承样式合并;响应式布局用 Mixin,适配各种屏幕。"

企业级实现

scss
// Sass 响应式 Mixin
@mixin responsive($breakpoint) {
  @if $breakpoint == 'sm' {
    @media (min-width: 576px) { @content; }
  }
  @else if $breakpoint == 'md' {
    @media (min-width: 768px) { @content; }
  }
  @else if $breakpoint == 'lg' {
    @media (min-width: 992px) { @content; }
  }
  @else if $breakpoint == 'xl' {
    @media (min-width: 1200px) { @content; }
  }
}

// 使用示例
.container {
  width: 100%;
  padding: 0 15px;
  
  @include responsive(md) {
    width: 750px;
    margin: 0 auto;
  }
  
  @include responsive(lg) {
    width: 970px;
  }
  
  @include responsive(xl) {
    width: 1170px;
  }
}

2. PostCSS 后处理器

面试题:什么是 PostCSS?请实现一个简单的 PostCSS 插件来添加 CSS 前缀。

核心考点

  • PostCSS 与预处理器的区别
  • PostCSS 插件生态
  • AST 操作
  • 自动前缀添加

常见坑点

  1. PostCSS 配置顺序错误:插件执行顺序会影响最终结果
  2. 过度使用插件导致的性能问题:每个插件都会解析一次 AST
  3. 与预处理器的配合问题:需要注意执行顺序
  4. AST 操作的复杂性:修改 CSS AST 需要理解其结构

易错点

  • 混淆 PostCSS 与预处理器
  • 忽略 PostCSS 的配置文件(postcss.config.js)
  • 对 PostCSS 插件的工作原理理解错误

联合记忆

  • PostCSS = CSS 解析器 + AST 操作 + 插件系统 + 后处理器
  • 核心插件 = autoprefixer + postcss-preset-env + cssnano
  • 与预处理器的配合 = 预处理器编译 → PostCSS 处理

记忆口诀: "PostCSS 是后处理器,解析 CSS 成 AST;插件系统真强大,自动前缀轻松加;与预处理器配合好,编译顺序很重要;AST 操作要熟练,插件开发不难搞。"

核心实现

javascript
// PostCSS 自动前缀插件(简化版)
module.exports = (opts = {}) => {
  return {
    postcssPlugin: 'postcss-autoprefix',
    Rule (rule) {
      // 遍历所有规则
      rule.walkDecls(decl => {
        // 需要添加前缀的属性
        const prefixProps = ['transform', 'transition', 'animation'];
        
        if (prefixProps.includes(decl.prop)) {
          // 添加前缀属性
          rule.prepend({
            prop: `-webkit-${decl.prop}`,
            value: decl.value
          });
        }
      });
    }
  };
};

module.exports.postcss = true;

// PostCSS 配置
module.exports = {
  plugins: [
    require('autoprefixer')({
      overrideBrowserslist: ['> 1%', 'last 2 versions']
    }),
    require('cssnano')()
  ]
};

六、组件库开发与工程化

1. 通用组件库开发

面试题:如何设计并开发一个通用的组件库?请描述从设计到发布的完整流程。

核心考点

  • 组件设计原则(单一职责、可复用、可扩展)
  • 组件库架构(Monorepo)
  • 构建与打包策略
  • 文档系统
  • 发布与版本管理

常见坑点

  1. 组件设计过度复杂:违反单一职责原则
  2. 样式隔离问题:组件样式与用户样式冲突
  3. 文档系统不完善:缺乏示例和 API 文档
  4. 版本管理混乱:未遵循语义化版本规范

易错点

  • 忽略组件的可访问性(A11y)
  • 对组件的 API 设计考虑不周
  • 未处理组件的边界情况
  • 忽略构建产物的体积优化

联合记忆

  • 组件设计 = 单一职责 + 可复用 + 可扩展 + 可访问
  • 工程化 = Monorepo + Rollup/Vite 打包 + Storybook 文档 + Jest 测试
  • 发布流程 = 代码提交 → 自动测试 → 构建 → 版本发布 → 文档更新

记忆口诀: "组件库开发流程长,设计开发测试忙;组件设计要单一,样式隔离很重要;Monorepo 管理代码,Rollup 打包体积小;Storybook 写文档,Jest 测试不可少;语义化版本要遵守,发布流程自动化。"

企业级架构

├── packages/
│   ├── components/        # 组件源码
│   │   ├── button/       # Button 组件
│   │   ├── input/        # Input 组件
│   │   └── index.js      # 组件导出
│   ├── utils/            # 工具函数
│   ├── styles/           # 全局样式
│   └── docs/             # 文档系统
├── rollup.config.js      # 打包配置
├── jest.config.js        # 测试配置
└── package.json          # 项目配置

2. 组件库打包与发布

面试题:如何优化组件库的打包体积?请实现一个组件库的按需加载功能。

核心考点

  • 按需加载实现(Tree Shaking)
  • 打包格式(ES Module/CJS/UMD)
  • 体积优化策略
  • 按需导入插件

常见坑点

  1. Tree Shaking 不生效:未使用 ES Module 或存在副作用
  2. 按需加载配置错误:需要配合 babel-plugin-import 等插件
  3. 样式按需导入的问题:需要将样式文件分离
  4. 打包产物的兼容性:需要考虑不同环境的兼容性

易错点

  • 误认为所有组件库都支持自动按需加载
  • 忽略 package.json 中的 sideEffects 配置
  • 对按需加载的实现原理理解错误

联合记忆

  • 按需加载 = ES Module + Tree Shaking + 按需导入插件
  • 体积优化 = Tree Shaking + 代码分割 + 压缩
  • 发布 = npm publish + 语义化版本 + CHANGELOG

记忆口诀: "组件库打包要优化,按需加载是关键;ES Module 支持 Tree Shaking,sideEffects 配置很重要;样式文件要分离,按需导入才有效;打包格式多,ES/CJS/UMD 都要有。"

核心配置

javascript
// Rollup 组件库打包配置
export default {
  input: 'src/index.js',
  output: [
    {
      file: 'dist/index.esm.js',
      format: 'es',
      sourcemap: true
    },
    {
      file: 'dist/index.cjs.js',
      format: 'cjs',
      sourcemap: true
    }
  ],
  plugins: [
    resolve(),
    commonjs(),
    babel({
      exclude: 'node_modules/**'
    }),
    // 样式提取
    postcss({
      extract: true,
      modules: false,
      plugins: [
        require('autoprefixer')
      ]
    })
  ],
  external: ['vue', 'react'] // 外部依赖
};

// package.json 配置
{
  "name": "my-component-library",
  "version": "1.0.0",
  "main": "dist/index.cjs.js",
  "module": "dist/index.esm.js",
  "sideEffects": ["*.css", "*.scss"], // 样式文件有副作用
  "files": ["dist", "src"]
}

七、浏览器工作原理

1. 浏览器渲染流程

面试题:请详细描述浏览器的渲染流程,包括关键渲染路径的优化策略。

核心考点

  • 关键渲染路径(HTML → CSSOM → DOM Tree → Render Tree → Layout → Paint → Composite)
  • 重排(Reflow)与重绘(Repaint)
  • 渲染优化策略
  • GPU 加速

常见坑点

  1. 阻塞渲染的资源:CSS 是阻塞渲染的,JavaScript 也会阻塞渲染
  2. 过度重排重绘:频繁修改 DOM 或样式会导致性能问题
  3. CSS 选择器的性能问题:复杂的 CSS 选择器会影响渲染性能
  4. 字体加载的闪烁问题:FOIT(Flash of Invisible Text)

易错点

  • 混淆重排与重绘
  • 忽略关键渲染路径的优化
  • 对 CSSOM 与 DOM 的构建顺序理解错误
  • 忽略字体加载策略(font-display)

联合记忆

  • 渲染流程 = DOM 构建 → CSSOM 构建 → Render Tree 构建 → 布局 → 绘制 → 合成
  • 优化策略 = 减少阻塞资源 + 优化 CSSOM + 最小化重排重绘 + GPU 加速
  • 关键指标 = First Paint (FP) + First Contentful Paint (FCP) + Largest Contentful Paint (LCP)

记忆口诀: "浏览器渲染流程多,DOM CSSOM 先构建;Render Tree 来合成,布局绘制再合成;重排重绘影响性能,减少操作要注意;关键路径要优化,阻塞资源先处理;GPU 加速效果好,transform opacity 来使用。"

优化策略

html
<!-- 优化关键渲染路径的 HTML -->
<!DOCTYPE html>
<html>
<head>
  <!-- 异步加载 CSS -->
  <link rel="preload" href="styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
  <noscript><link rel="stylesheet" href="styles.css"></noscript>
  
  <!-- 异步加载 JavaScript -->
  <script src="script.js" async></script>
</head>
<body>
  <!-- 关键内容 -->
  <div class="hero">...</div>
  
  <!-- 非关键内容延迟加载 -->
  <div class="secondary" loading="lazy">...</div>
</body>
</html>

2. 浏览器事件循环

面试题:请详细描述浏览器的事件循环机制,包括宏任务与微任务的执行顺序。

核心考点

  • 事件循环的概念
  • 宏任务与微任务的区别
  • 执行顺序
  • 异步编程的实现

常见坑点

  1. 微任务与宏任务的执行顺序:微任务在当前宏任务执行完成后立即执行
  2. Promise 的状态变化时机:Promise 的 resolve/reject 是同步的,但 then/catch 是微任务
  3. async/await 的执行顺序:await 后面的代码会被放入微任务队列
  4. 定时器的精度问题:setTimeout 的延迟时间不是精确的

易错点

  • 混淆浏览器与 Node.js 的事件循环
  • 忽略微任务的优先级
  • 对 Promise 的执行时机理解错误
  • 误认为 async/await 会创建新的事件循环

联合记忆

  • 事件循环 = 调用栈 + 任务队列(宏任务/微任务)
  • 宏任务 = setTimeout/setInterval/I/O/DOM 事件
  • 微任务 = Promise.then/catch/finally/async/await/process.nextTick
  • 执行顺序 = 调用栈清空 → 微任务队列清空 → 宏任务队列取出一个执行 → 重复

记忆口诀: "事件循环机制好,异步编程全靠它;宏任务微任务要分清,执行顺序有讲究;调用栈空微任务清,然后执行宏任务;Promise 同步状态变,then/catch 是微任务;async/await 语法糖,微任务队列来执行。"

执行顺序示例

javascript
console.log('1'); // 同步代码

setTimeout(() => {
  console.log('2'); // 宏任务
}, 0);

Promise.resolve().then(() => {
  console.log('3'); // 微任务
});

async function asyncFunc() {
  await Promise.resolve();
  console.log('4'); // 微任务(await 后面的代码)
}

asyncFunc();

console.log('5'); // 同步代码

// 输出顺序:1, 5, 3, 4, 2

八、前端工程化与性能优化

1. 前端工程化核心

面试题:请描述一个完整的前端工程化流程,包括代码规范、自动化测试、构建部署等环节。

核心考点

  • 代码规范(ESLint/Prettier/EditorConfig)
  • 自动化测试(单元测试/集成测试/E2E 测试)
  • 构建工具(Webpack/Rollup/Vite)
  • CI/CD 流水线
  • 代码质量保障

常见坑点

  1. 代码规范执行不严格:缺乏 Git Hooks 强制执行
  2. 测试覆盖率不足:未设置合理的测试覆盖率阈值
  3. 构建配置过于复杂:缺乏文档和维护
  4. CI/CD 流水线不稳定:构建过程中频繁失败

易错点

  • 混淆 ESLint 与 Prettier 的作用
  • 忽略 Git Hooks 的配置(Husky/Lint-Staged)
  • 对测试金字塔(单元测试 > 集成测试 > E2E 测试)理解错误
  • 未设置合理的构建缓存

联合记忆

  • 工程化 = 代码规范 + 自动化测试 + 构建工具 + CI/CD
  • 质量保障 = 代码审查 + 自动化测试 + 性能监控
  • 部署流程 = 构建 → 测试 → 部署 → 监控

记忆口诀: "前端工程化流程全,代码规范是起点;ESLint 查错误,Prettier 格式化;Git Hooks 来强制,Husky Lint-Staged 配合好;自动化测试不能少,单元集成 E2E;构建工具选得对,Webpack Rollup Vite 任你挑;CI/CD 流水线,代码提交自动跑;质量保障多维度,性能监控不能少。"

企业级配置

json
// package.json 工程化配置
{
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "lint": "eslint src --ext .js,.ts,.vue",
    "lint:fix": "eslint src --ext .js,.ts,.vue --fix",
    "test": "jest",
    "test:coverage": "jest --coverage",
    "precommit": "lint-staged",
    "prepare": "husky install"
  },
  "devDependencies": {
    "eslint": "^8.0.0",
    "prettier": "^2.0.0",
    "husky": "^7.0.0",
    "lint-staged": "^11.0.0",
    "jest": "^27.0.0",
    "vite": "^2.0.0"
  },
  "lint-staged": {
    "*.{js,ts,vue}": [
      "eslint --fix",
      "prettier --write"
    ]
  }
}

2. 性能优化策略

面试题:请详细描述前端性能优化的策略,包括加载优化、渲染优化、运行优化等方面。

核心考点

  • 加载性能优化
  • 渲染性能优化
  • 运行性能优化
  • 性能监控与分析
  • Core Web Vitals

常见坑点

  1. 过度优化:优化非关键路径的资源
  2. 忽略性能监控:未设置性能监控指标
  3. 图片优化不足:未使用现代图片格式(WebP/AVIF)或懒加载
  4. 缓存策略不当:未设置合理的缓存过期时间

易错点

  • 混淆不同类型的性能指标(FP/FCP/LCP/FID/TBT/CLS)
  • 忽略关键渲染路径的优化
  • 对 Service Worker 的缓存策略理解错误
  • 未考虑移动端性能优化

联合记忆

  • 加载优化 = 资源压缩 + CDN + 缓存策略 + 懒加载 + 预加载
  • 渲染优化 = 关键渲染路径优化 + 减少重排重绘 + GPU 加速
  • 运行优化 = 代码分割 + Tree Shaking + 内存泄漏监控 + 异步处理
  • 性能指标 = Core Web Vitals(LCP/FID/CLS)

记忆口诀: "前端性能优化多,加载渲染运行说;加载优化压缩资源用 CDN,缓存策略要合理;懒加载预加载,关键资源先加载;渲染优化减少重排重绘,GPU 加速效果好;运行优化代码分割,Tree Shaking 清除没用代码;性能监控 Core Web Vitals,LCP FID CLS 要达标。"

企业级优化

html
<!-- 加载优化 -->
<!-- 1. 资源压缩与 CDN -->
<script src="https://cdn.example.com/script.min.js"></script>

<!-- 2. 预加载关键资源 -->
<link rel="preload" href="critical.css" as="style">
<link rel="preload" href="hero.jpg" as="image">

<!-- 3. 懒加载非关键资源 -->
<img src="lazy.jpg" loading="lazy">

<!-- 渲染优化 -->
<!-- 1. 关键 CSS 内联 -->
<style>
  .hero { background: #000; }
</style>

<!-- 2. 使用 CSS 动画替代 JavaScript 动画 -->
<div class="animate"></div>
<style>
  .animate { transition: transform 0.3s ease; }
  .animate:hover { transform: scale(1.1); }
</style>

<!-- 3. 减少重排重绘 -->
<div class="container"></div>
<script>
  const container = document.querySelector('.container');
  // 使用 DocumentFragment 批量更新 DOM
  const fragment = document.createDocumentFragment();
  for (let i = 0; i < 100; i++) {
    const div = document.createElement('div');
    div.textContent = i;
    fragment.appendChild(div);
  }
  container.appendChild(fragment);
</script>

3. 打包优化策略

面试题:如何优化 Webpack 的构建性能和打包体积?请结合具体配置说明。

核心考点

  • 构建性能优化(缓存、多线程、依赖优化)
  • 打包体积优化(Tree Shaking、Code Splitting、压缩)
  • 依赖分析
  • 懒加载

常见坑点

  1. 缓存配置不当:未启用 babel-loader 或 webpack 缓存
  2. 多线程构建的过度使用:小项目启用多线程可能会降低性能
  3. Code Splitting 配置错误:导致 chunk 过多或过大
  4. Tree Shaking 不生效:未使用 ES Module 或存在副作用

易错点

  • 混淆 optimization.splitChunks 与 optimization.runtimeChunk
  • 忽略 externals 配置(将第三方库从打包文件中排除)
  • 对 Tree Shaking 的实现原理理解错误
  • 未分析打包结果(使用 webpack-bundle-analyzer)

联合记忆

  • 构建性能 = 缓存 + 多线程 + 依赖优化 + 减少解析
  • 打包体积 = Tree Shaking + Code Splitting + 压缩 + externals
  • 分析工具 = webpack-bundle-analyzer + source-map-explorer

记忆口诀: "Webpack 优化两方面,构建性能打包体积;构建性能用缓存,多线程构建速度快;依赖优化 externals,减少打包文件大小;Tree Shaking 清除无用代码,Code Splitting 分割代码;压缩工具不能少,bundle 分析要做好。"

企业级配置

javascript
// Webpack 优化配置
module.exports = {
  // 1. 构建性能优化
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/, // 排除 node_modules 目录,避免重复编译第三方库
        use: [
          {
            loader: 'thread-loader', // 使用 thread-loader 实现多线程编译
            options: {
              workers: 2 // 指定线程数量为 2,根据 CPU 核心数调整
            }
          },
          {
            loader: 'babel-loader', // 使用 babel-loader 转译 JavaScript
            options: {
              cacheDirectory: true, // 启用缓存目录,缓存编译结果以提高后续构建速度
              cacheCompression: false // 禁用缓存压缩,减少 CPU 开销
            }
          }
        ]
      }
    ]
  },
  
  // 2. 打包体积优化
  optimization: {
    usedExports: true, // 启用 Tree Shaking,标记未使用的导出
    minimize: true, // 启用代码压缩
    minimizer: [
      new TerserPlugin({
        parallel: true, // 启用多线程压缩
        extractComments: false // 不提取注释到单独的 LICENSE 文件
      }),
      new CssMinimizerPlugin() // 压缩 CSS 文件
    ],
    splitChunks: {
      chunks: 'all', // 对所有类型的 chunks(同步/异步)进行分割
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/, // 匹配 node_modules 目录下的文件
          name: 'vendors', // 生成的 chunk 名称
          chunks: 'all' // 对所有 chunks 应用此规则
        },
        common: {
          name: 'common', // 生成的 chunk 名称
          minChunks: 2, // 至少被引用 2 次才会被分割
          chunks: 'all', // 对所有 chunks 应用此规则
          enforce: true // 强制分割,即使不符合默认大小限制
        }
      }
    },
    runtimeChunk: {
      name: 'runtime' // 将运行时代码提取到单独的 chunk
    }
  },
  
  // 3. 依赖优化
  externals: {
    'react': 'React', // 将 react 从打包中排除,使用全局变量 React
    'react-dom': 'ReactDOM' // 将 react-dom 从打包中排除,使用全局变量 ReactDOM
  },
  
  // 4. 分析工具
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: 'static', // 生成静态 HTML 报告
      openAnalyzer: false // 不自动打开报告
    })
  ]
};

配置说明

  • 构建性能优化

    • thread-loader:启用多线程编译,提高大型项目的编译速度
    • babel-loader 缓存:避免重复编译相同的代码,显著提升二次构建速度
    • exclude: /node_modules/:避免编译第三方依赖,减少不必要的工作
  • 打包体积优化

    • usedExports + Tree Shaking:移除未使用的代码,减小打包体积
    • splitChunks:将第三方库和公共代码分割成单独的文件,实现缓存复用
    • runtimeChunk:提取运行时代码,避免因为模块变化导致整个 bundle 失效
    • minimizer:使用 Terser 和 CSSMinimizer 压缩 JavaScript 和 CSS
  • 依赖优化

    • externals:将常用第三方库从打包文件中排除,通过 CDN 引入,减小 bundle 大小
  • 分析工具

    • BundleAnalyzerPlugin:生成可视化的打包分析报告,帮助识别体积过大的模块

解决的问题

  1. 构建时间过长:通过多线程编译和缓存机制显著提升构建速度
  2. 打包体积过大:通过代码分割、Tree Shaking、压缩和 externals 配置减小 bundle 大小
  3. 缓存失效频繁:通过 runtimeChunk 和 splitChunks 优化缓存策略
  4. 难以定位体积问题:通过 bundle 分析工具直观地识别问题模块

九、高级应用与实践

1. 大文件上传与断点续传

面试题:如何实现大文件上传、断点续传和秒传功能?请详细描述其实现原理。

核心考点

  • 文件分片上传
  • 断点续传实现
  • 秒传功能
  • WebSocket 通信
  • 服务端配合

常见坑点

  1. 文件分片的大小选择:分片过大或过小都会影响性能
  2. 断点续传的状态管理:需要在客户端保存上传状态
  3. 并发上传的控制:并发数过多会导致网络拥塞
  4. 服务端的文件合并:需要确保分片顺序正确

易错点

  • 忽略文件哈希计算的性能问题(可以使用 Web Worker)
  • 未处理网络错误导致的上传失败
  • 对服务端的文件存储策略考虑不周
  • 忽略断点续传的安全性问题(如身份验证)

联合记忆

  • 大文件上传 = 文件分片 + 并发上传 + 断点续传 + 秒传
  • 断点续传 = 上传状态保存 + 已上传分片记录 + 续传时跳过已上传分片
  • 秒传 = 文件哈希计算 + 服务端文件存在性检查

记忆口诀: "大文件上传不好搞,分片上传是关键;断点续传要记录,已上传分片莫重复;秒传功能靠哈希,服务端检查文件存不存在;并发上传要控制,网络拥塞要避免;WebSocket 实时通信,上传进度看得见。"

核心实现

javascript
// 大文件上传核心实现
class FileUploader {
  constructor(options) {
    this.options = options;
    this.chunkSize = options.chunkSize || 1024 * 1024; // 1MB 分片
    this.concurrency = options.concurrency || 3; // 并发数
    this.uploadingChunks = [];
    this.uploadedChunks = new Set();
  }
  
  // 计算文件哈希(使用 Web Worker)
  async calculateHash(file) {
    return new Promise((resolve) => {
      const worker = new Worker('hashWorker.js');
      worker.postMessage({ file, chunkSize: this.chunkSize });
      worker.onmessage = (e) => {
        resolve(e.data.hash);
        worker.terminate();
      };
    });
  }
  
  // 检查文件是否已存在(秒传)
  async checkFile(fileHash) {
    const response = await fetch(`${this.options.server}/check?hash=${fileHash}`);
    return response.json();
  }
  
  // 获取已上传分片
  async getUploadedChunks(fileHash) {
    const response = await fetch(`${this.options.server}/uploaded?hash=${fileHash}`);
    const { chunks } = await response.json();
    this.uploadedChunks = new Set(chunks);
  }
  
  // 上传单个分片
  async uploadChunk(chunk, index, fileHash) {
    const formData = new FormData();
    formData.append('chunk', chunk);
    formData.append('index', index);
    formData.append('hash', fileHash);
    
    const response = await fetch(`${this.options.server}/upload`, {
      method: 'POST',
      body: formData
    });
    
    return response.json();
  }
  
  // 合并分片
  async mergeChunks(fileHash, fileName, fileSize) {
    const response = await fetch(`${this.options.server}/merge`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ hash: fileHash, fileName, fileSize })
    });
    
    return response.json();
  }
  
  // 上传文件
  async upload(file) {
    const fileHash = await this.calculateHash(file);
    
    // 检查是否可以秒传
    const checkResult = await this.checkFile(fileHash);
    if (checkResult.exists) {
      this.options.onSuccess({ url: checkResult.url });
      return;
    }
    
    // 获取已上传分片
    await this.getUploadedChunks(fileHash);
    
    // 计算分片数
    const chunks = [];
    const totalChunks = Math.ceil(file.size / this.chunkSize);
    
    for (let i = 0; i < totalChunks; i++) {
      const start = i * this.chunkSize;
      const end = Math.min(start + this.chunkSize, file.size);
      chunks.push(file.slice(start, end));
    }
    
    // 并发上传分片
    let uploaded = 0;
    const uploadQueue = [];
    
    for (let i = 0; i < totalChunks; i++) {
      if (this.uploadedChunks.has(i)) {
        uploaded++;
        this.options.onProgress(uploaded / totalChunks);
        continue;
      }
      
      const uploadTask = () => {
        return this.uploadChunk(chunks[i], i, fileHash)
          .then(() => {
            uploaded++;
            this.options.onProgress(uploaded / totalChunks);
            // 从上传队列中移除
            this.uploadingChunks = this.uploadingChunks.filter(task => task !== uploadTask);
            // 继续上传下一个
            if (i < totalChunks - 1) {
              const nextTask = () => uploadQueue.shift()?.();
              this.uploadingChunks.push(nextTask);
              nextTask();
            }
          });
      };
      
      uploadQueue.push(uploadTask);
    }
    
    // 开始并发上传
    for (let i = 0; i < Math.min(this.concurrency, uploadQueue.length); i++) {
      const task = () => uploadQueue.shift()?.();
      this.uploadingChunks.push(task);
      task();
    }
    
    // 等待所有分片上传完成
    await Promise.all(this.uploadingChunks);
    
    // 合并分片
    const result = await this.mergeChunks(fileHash, file.name, file.size);
    this.options.onSuccess(result);
  }
  
  // 暂停上传
  pause() {
    this.uploadingChunks.forEach(task => task.abort?.());
    this.uploadingChunks = [];
  }
}

2. 虚拟列表实现

面试题:如何实现一个高性能的虚拟列表?请详细描述其实现原理和优化策略。

核心考点

  • 虚拟列表的设计原理
  • 滚动事件处理
  • 可视区域计算
  • 性能优化
  • 边界处理

常见坑点

  1. 滚动位置计算错误:导致列表内容偏移
  2. 可视区域计算不准确:导致显示不全或空白
  3. 滚动事件的性能问题:未使用节流或防抖
  4. 动态高度的处理:固定高度的虚拟列表无法处理动态高度

易错点

  • 忽略滚动事件的默认行为
  • 未考虑滚动容器的 padding 和 margin
  • 对虚拟列表的缓存策略考虑不周
  • 忽略虚拟列表的可访问性(A11y)

联合记忆

  • 虚拟列表 = 只渲染可视区域内的元素 + 滚动时动态更新
  • 核心计算 = 可视区域高度 + 元素高度 + 滚动位置
  • 优化策略 = 节流滚动事件 + 元素缓存 + 动态高度计算

记忆口诀: "虚拟列表性能好,只渲染可视区域元素;滚动事件要节流,避免频繁计算;可视区域高度元素高度滚动位置,三者计算不可少;动态高度要处理,元素缓存提高性能;边界情况要考虑,空白填充要准确。"

核心实现

javascript
// 虚拟列表组件实现(React)
function VirtualList({ items, itemHeight, containerHeight, overscan = 5 }) {
  const [scrollTop, setScrollTop] = useState(0);
  
  // 容器引用
  const containerRef = useRef(null);
  
  // 计算可视区域内的元素
  const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);
  const endIndex = Math.min(items.length - 1, Math.ceil((scrollTop + containerHeight) / itemHeight) + overscan);
  
  // 可视区域内的元素
  const visibleItems = items.slice(startIndex, endIndex + 1);
  
  // 偏移量
  const offsetY = startIndex * itemHeight;
  
  // 总高度
  const totalHeight = items.length * itemHeight;
  
  // 滚动事件处理(节流)
  const handleScroll = useCallback(
    throttle((e) => {
      setScrollTop(e.target.scrollTop);
    }, 16),
    []
  );
  
  return (
    <div
      ref={containerRef}
      className="virtual-list-container"
      style={{ height: containerHeight, overflowY: 'auto' }}
      onScroll={handleScroll}
    >
      <div className="virtual-list-wrapper" style={{ height: totalHeight }}>
        <div className="virtual-list-content" style={{ transform: `translateY(${offsetY}px)` }}>
          {visibleItems.map((item, index) => (
            <div
              key={item.id}
              className="virtual-list-item"
              style={{ height: itemHeight }}
            >
              {item.content}
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

// 节流函数
function throttle(fn, delay) {
  let lastCall = 0;
  return function(...args) {
    const now = new Date().getTime();
    if (now - lastCall < delay) {
      return;
    }
    lastCall = now;
    return fn.apply(this, args);
  };
}

3. Axios 二次封装

面试题:如何二次封装 Axios?请实现一个包含请求拦截、响应拦截、错误处理、取消请求等功能的 Axios 实例。

核心考点

  • Axios 拦截器
  • 请求/响应处理
  • 错误处理机制
  • 取消请求
  • 超时处理

常见坑点

  1. 请求拦截器的配置顺序错误:拦截器的执行顺序是后进先出
  2. 响应拦截器的错误处理不当:未正确区分网络错误和业务错误
  3. 取消请求的实现:需要为每个请求创建取消令牌
  4. 超时处理的配置:全局超时和请求级超时的优先级

易错点

  • 混淆 Axios 的实例方法和静态方法
  • 忽略响应数据的格式一致性
  • 对 Axios 的错误对象结构理解错误
  • 未处理跨域请求的配置

联合记忆

  • Axios 封装 = 实例创建 + 请求拦截 + 响应拦截 + 错误处理 + 取消请求
  • 拦截器 = 请求拦截(配置修改、添加 token) + 响应拦截(数据处理、错误统一处理)
  • 错误处理 = 网络错误 + 业务错误 + 超时错误

记忆口诀: "Axios 封装好处多,请求响应拦截器;请求拦截加 token,响应拦截处理数据;错误处理要统一,网络业务超时分清楚;取消请求令牌用,超时配置要合理;实例创建多环境,配置灵活易维护。"

企业级实现

javascript
// Axios 二次封装
import axios from 'axios';
import { ElMessage, ElLoading } from 'element-plus';

// 创建 Axios 实例
const service = axios.create({
  baseURL: process.env.VUE_APP_BASE_API,
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json'
  }
});

// 加载实例
let loadingInstance = null;

// 请求拦截器
service.interceptors.request.use(
  (config) => {
    // 1. 显示加载动画
    loadingInstance = ElLoading.service({
      lock: true,
      text: '加载中...',
      background: 'rgba(0, 0, 0, 0.7)'
    });
    
    // 2. 添加 token
    const token = localStorage.getItem('token');
    if (token) {
      config.headers['Authorization'] = `Bearer ${token}`;
    }
    
    // 3. 处理取消请求
    if (config.cancelToken) {
      // 用户自定义的取消令牌
    } else {
      // 自动创建取消令牌
      const CancelToken = axios.CancelToken;
      config.cancelToken = new CancelToken((cancel) => {
        config.cancel = cancel;
      });
    }
    
    // 4. 处理请求参数
    if (config.method === 'get') {
      // get 请求参数处理
      config.params = { ...config.params, _t: Date.now() };
    }
    
    return config;
  },
  (error) => {
    // 请求错误处理
    loadingInstance?.close();
    ElMessage.error('请求配置错误');
    return Promise.reject(error);
  }
);

// 响应拦截器
service.interceptors.response.use(
  (response) => {
    // 关闭加载动画
    loadingInstance?.close();
    
    // 处理响应数据
    const res = response.data;
    
    // 1. 统一错误处理
    if (res.code !== 200) {
      // 业务错误
      ElMessage.error(res.message || '请求失败');
      
      // 特殊错误处理
      if (res.code === 401) {
        // 未授权,跳转到登录页
        localStorage.removeItem('token');
        window.location.href = '/login';
      } else if (res.code === 403) {
        // 权限不足
        ElMessage.error('权限不足,无法访问');
      }
      
      return Promise.reject(new Error(res.message || 'Error'));
    }
    
    // 2. 成功响应
    return res;
  },
  (error) => {
    // 关闭加载动画
    loadingInstance?.close();
    
    // 错误分类处理
    if (axios.isCancel(error)) {
      // 取消请求
      console.log('Request canceled:', error.message);
      return Promise.reject(error);
    }
    
    // 网络错误或服务器错误
    let message = '请求失败,请稍后重试';
    
    if (error.response) {
      // 服务器返回错误
      const { status, data } = error.response;
      switch (status) {
        case 400:
          message = '请求参数错误';
          break;
        case 401:
          message = '未授权,请重新登录';
          localStorage.removeItem('token');
          window.location.href = '/login';
          break;
        case 403:
          message = '权限不足,无法访问';
          break;
        case 404:
          message = '请求资源不存在';
          break;
        case 500:
          message = '服务器内部错误';
          break;
        case 502:
          message = '网关错误';
          break;
        case 503:
          message = '服务不可用';
          break;
        case 504:
          message = '网关超时';
          break;
        default:
          message = data.message || `请求失败(${status})`;
      }
    } else if (error.request) {
      // 请求已发送但没有收到响应
      message = '网络错误,请检查网络连接';
    } else {
      // 请求配置错误
      message = error.message;
    }
    
    ElMessage.error(message);
    return Promise.reject(error);
  }
);

// 导出请求方法
export const request = service;

// 导出常用请求方法
export const get = (url, params, config = {}) => {
  return service.get(url, { params, ...config });
};

export const post = (url, data, config = {}) => {
  return service.post(url, data, config);
};

export const put = (url, data, config = {}) => {
  return service.put(url, data, config);
};

export const del = (url, params, config = {}) => {
  return service.delete(url, { params, ...config });
};

export default service;

### 4. Vue 自定义指令
**面试题**:如何自定义一个 Vue 指令?请实现一个防抖点击指令和一个权限控制指令。

**核心考点**
- 自定义指令的生命周期钩子
- 指令的参数和修饰符
- 指令的作用域和数据传递
- 指令的应用场景

**常见坑点**
1. **指令钩子函数的参数使用错误**:未正确使用 el/binding/vnode/oldVnode 参数
2. **指令的内存泄漏**:未在 unbind 钩子中清理事件监听器
3. **指令的作用域理解错误**:指令中的 this 指向不是组件实例
4. **指令的参数传递错误**:未正确处理 binding.value 和 binding.expression

**易错点**
- 混淆全局指令和局部指令的注册方式
- 忽略指令的更新钩子(update/componentUpdated)
- 对指令的优先级理解错误
- 未考虑指令的兼容性(Vue 2 vs Vue 3

**联合记忆**
- Vue 2 指令钩子 = bind/inserted/update/componentUpdated/unbind
- Vue 3 指令钩子 = beforeMount/mounted/beforeUpdate/updated/beforeUnmount/unmounted
- 应用场景 = DOM 操作 + 权限控制 + 表单验证 + 交互增强

**记忆口诀**
"Vue 自定义指令强,DOM 操作很方便;钩子函数生命周期,bind inserted update unbind;参数修饰符要会用,内存泄漏要清理;防抖权限验证好,指令应用场景多。"

**企业级实现**
```javascript
// Vue 2 自定义指令
// 1. 防抖点击指令
Vue.directive('debounce', {
  bind(el, binding) {
    let timer = null;
    el.handler = function() {
      if (timer) {
        clearTimeout(timer);
      }
      timer = setTimeout(() => {
        binding.value.apply(this, arguments);
      }, binding.arg || 300);
    };
    el.addEventListener('click', el.handler);
  },
  unbind(el) {
    el.removeEventListener('click', el.handler);
  }
});

// 2. 权限控制指令
Vue.directive('permission', {
  inserted(el, binding) {
    const userPermissions = store.getters.permissions;
    const requiredPermission = binding.value;
    
    if (!userPermissions.includes(requiredPermission)) {
      el.style.display = 'none';
      // 或者直接移除元素
      // el.parentNode.removeChild(el);
    }
  },
  update(el, binding) {
    const userPermissions = store.getters.permissions;
    const requiredPermission = binding.value;
    
    if (userPermissions.includes(requiredPermission)) {
      el.style.display = '';
    } else {
      el.style.display = 'none';
    }
  }
});

// Vue 3 自定义指令
const debounce = {
  mounted(el, binding) {
    let timer = null;
    el.handler = function() {
      if (timer) {
        clearTimeout(timer);
      }
      timer = setTimeout(() => {
        binding.value.apply(this, arguments);
      }, binding.arg || 300);
    };
    el.addEventListener('click', el.handler);
  },
  unmounted(el) {
    el.removeEventListener('click', el.handler);
  }
};

// 在组件中使用
app.directive('debounce', debounce);

5. 权限显隐与动态路由

面试题:如何实现前端权限控制?请详细描述路由权限、菜单权限和按钮权限的实现方案。

核心考点

  • 权限控制的分级(路由/菜单/按钮)
  • 动态路由生成
  • 权限判断的时机
  • 权限信息的存储
  • 权限的动态更新

常见坑点

  1. 权限控制的安全性问题:前端权限控制不能替代后端权限控制
  2. 动态路由的生成错误:路由配置错误导致页面无法访问
  3. 权限信息的存储安全:敏感权限信息不应存储在本地存储
  4. 权限更新的时机问题:用户角色变化时未及时更新权限

易错点

  • 误认为前端权限控制足够安全(需要后端配合)
  • 忽略权限控制的性能影响
  • 对动态路由的异步加载处理不当
  • 未考虑权限控制的可扩展性

联合记忆

  • 权限控制 = 前端控制 + 后端验证
  • 实现方案 = 路由守卫 + 动态路由 + 权限指令 + 权限过滤器
  • 权限信息 = 角色 + 资源 + 操作

记忆口诀: "前端权限控制三级,路由菜单按钮齐;路由守卫控制访问,动态路由按需加载;菜单权限控制显示,按钮权限用指令;前端控制做过滤,后端验证是关键;权限信息安全存,动态更新要及时。"

企业级实现

javascript
// Vue 权限控制实现
// 1. 路由配置(包含权限信息)
const routes = [
  {
    path: '/dashboard',
    component: () => import('@/views/Dashboard'),
    meta: { requiresAuth: true, permission: 'dashboard:view' }
  },
  {
    path: '/user',
    component: () => import('@/views/User'),
    meta: { requiresAuth: true, permission: 'user:view' },
    children: [
      {
        path: 'list',
        component: () => import('@/views/User/List'),
        meta: { permission: 'user:list' }
      },
      {
        path: 'add',
        component: () => import('@/views/User/Add'),
        meta: { permission: 'user:add' }
      }
    ]
  }
];

// 2. 路由守卫
router.beforeEach((to, from, next) => {
  // 检查是否需要认证
  if (to.meta.requiresAuth) {
    const token = localStorage.getItem('token');
    if (!token) {
      return next('/login');
    }
    
    // 检查权限
    if (to.meta.permission) {
      const userPermissions = store.getters.permissions;
      if (!userPermissions.includes(to.meta.permission)) {
        return next('/403');
      }
    }
  }
  
  next();
});

// 3. 动态路由生成
const generateRoutes = (userPermissions) => {
  const accessibleRoutes = [];
  
  const checkPermission = (route) => {
    // 检查路由是否有权限访问
    if (route.meta && route.meta.permission) {
      return userPermissions.includes(route.meta.permission);
    }
    return true;
  };
  
  const filterRoutes = (routes) => {
    return routes.filter(route => {
      const canAccess = checkPermission(route);
      if (canAccess && route.children) {
        route.children = filterRoutes(route.children);
      }
      return canAccess;
    });
  };
  
  return filterRoutes(routes);
};

// 4. 菜单生成
const generateMenus = (userPermissions) => {
  const menus = [];
  
  const filterMenus = (routes) => {
    return routes.map(route => {
      if (route.meta && route.meta.permission && !userPermissions.includes(route.meta.permission)) {
        return null;
      }
      
      const menu = {
        path: route.path,
        name: route.meta.title,
        icon: route.meta.icon
      };
      
      if (route.children) {
        const children = filterMenus(route.children);
        if (children.length) {
          menu.children = children;
        }
      }
      
      return menu;
    }).filter(Boolean);
  };
  
  return filterMenus(routes);
};

// 5. 权限指令
Vue.directive('permission', {
  inserted(el, binding) {
    const userPermissions = store.getters.permissions;
    const requiredPermission = binding.value;
    
    if (!userPermissions.includes(requiredPermission)) {
      el.style.display = 'none';
    }
  }
});

6. SWC 编译器

面试题:什么是 SWC?它相比 Babel 有哪些优势?请说明如何在项目中配置 SWC。

核心考点

  • SWC 编译器的原理
  • SWC 与 Babel 的对比
  • SWC 的配置方式
  • SWC 在构建工具中的应用

常见坑点

  1. SWC 配置的兼容性问题:SWC 的配置与 Babel 不完全兼容
  2. 插件生态的缺失:SWC 的插件生态不如 Babel 丰富
  3. TypeScript 支持的局限性:某些 TypeScript 特性可能不被支持
  4. 构建工具的集成问题:与 Webpack/Rollup/Vite 的集成配置

易错点

  • 误认为 SWC 可以完全替代 Babel(目前还不能)
  • 忽略 SWC 的版本差异
  • 对 SWC 的编译目标配置错误
  • 未考虑 SWC 的浏览器兼容性

联合记忆

  • SWC = Rust 编写的超高速 JavaScript/TypeScript 编译器
  • 优势 = 编译速度快 + 内存占用低 + 支持 TypeScript/JSX
  • 应用场景 = 构建工具替代 Babel + 代码压缩 + 类型检查

记忆口诀: "SWC 编译器真快,Rust 编写性能高;编译速度超 Babel,内存占用也很少;支持 TypeScript JSX,配置简单易上手;构建工具集成好,Webpack Vite Rollup 都支持;插件生态待完善,部分场景需 Babel。"

企业级配置

javascript
// 1. Vite 中使用 SWC
// vite.config.js
import { defineConfig } from 'vite'; // 导入 Vite 的配置函数
import vue from '@vitejs/plugin-vue'; // 导入 Vue 插件
import swc from 'vite-plugin-swc'; // 导入 SWC 插件

export default defineConfig({
  plugins: [
    vue(), // 使用 Vue 插件处理 .vue 文件
    swc({
      // SWC 配置
      jsc: {
        parser: {
          syntax: 'typescript', // 解析 TypeScript 语法
          tsx: false, // 不解析 TSX 语法(Vue 项目使用 JSX 时可设为 true)
          decorators: true // 支持装饰器语法
        },
        target: 'es2020' // 编译目标为 ES2020
      }
    })
  ]
});

// 2. Webpack 中使用 SWC
// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.(js|jsx|ts|tsx)$/, // 匹配 JavaScript/TypeScript 文件
        exclude: /node_modules/, // 排除 node_modules 目录
        use: {
          loader: 'swc-loader', // 使用 swc-loader 处理文件
          options: {
            jsc: {
              parser: {
                syntax: 'typescript', // 解析 TypeScript 语法
                tsx: true, // 解析 TSX 语法
                dynamicImport: true, // 支持动态导入语法
                decorators: true // 支持装饰器语法
              },
              transform: {
                react: {
                  runtime: 'automatic' // 使用 React 17+ 的新 JSX 转换运行时
                }
              },
              target: 'es2020', // 编译目标为 ES2020
              loose: true // 使用宽松模式,提高编译速度(兼容性略有降低)
            }
          }
        }
      }
    ]
  }
};

// 3. SWC 配置文件 (.swcrc)
{
  "jsc": {
    "parser": {
      "syntax": "typescript", // 解析 TypeScript 语法
      "tsx": true, // 解析 TSX 语法
      "decorators": true, // 支持装饰器语法
      "dynamicImport": true // 支持动态导入语法
    },
    "transform": {
      "react": {
        "runtime": "automatic" // 使用 React 17+ 的新 JSX 转换运行时
      }
    },
    "target": "es2020", // 编译目标为 ES2020
    "loose": true, // 使用宽松模式,提高编译速度
    "minify": {
      "compress": true, // 启用代码压缩
      "mangle": true // 启用变量名混淆
    }
  },
  "minify": true // 启用代码压缩
}

配置说明

  • Vite 中使用 SWC

    • 替换 Vite 默认的 ESBuild,使用 SWC 处理 JavaScript/TypeScript 文件
    • 支持 Vue 项目的 TypeScript 语法和装饰器
    • 配置简洁,与 Vite 生态完美集成
  • Webpack 中使用 SWC

    • 替代 Babel 处理 JavaScript/TypeScript 文件
    • 支持 React 项目的 JSX/TSX 语法
    • 配置与 Babel 类似,但编译速度更快
  • SWC 配置文件 (.swcrc)

    • 独立的 SWC 配置文件,可在多个工具间共享
    • 包含完整的解析、转换和压缩配置
    • 支持 TypeScript 语法和 React JSX 转换

SWC 主要参数说明

  • jsc.parser.syntax:指定要解析的语法类型(typescript/javascript)
  • jsc.parser.tsx:是否解析 JSX/TSX 语法
  • jsc.parser.decorators:是否支持装饰器语法
  • jsc.parser.dynamicImport:是否支持动态导入语法
  • jsc.transform.react.runtime:指定 React JSX 转换运行时
  • jsc.target:指定编译目标(es2015/es2018/es2020 等)
  • jsc.loose:是否使用宽松模式,平衡编译速度和兼容性
  • jsc.minify:配置代码压缩选项

解决的问题

  1. 构建速度慢:SWC 比 Babel 快 5-20 倍,显著提升构建速度
  2. 内存占用高:SWC 用 Rust 编写,内存占用远低于 Babel
  3. 配置复杂:SWC 配置简洁,学习成本低
  4. 兼容性问题:支持广泛的 JavaScript/TypeScript 语法和特性

7. 长表单应用的高性能存储与同步

面试题:如何实现一个高性能的长表单应用,包括表单数据的本地存储、自动保存和数据同步?

核心考点

  • 表单数据的本地存储策略
  • 自动保存机制
  • 数据同步策略
  • 表单性能优化
  • 数据验证和错误处理

常见坑点

  1. 本地存储的性能问题:频繁读写本地存储导致性能下降
  2. 自动保存的时机错误:保存过于频繁或不及时
  3. 数据同步的冲突处理:多设备同步时的数据冲突
  4. 表单验证的性能影响:实时验证导致的性能问题

易错点

  • 混淆不同本地存储方案的优缺点(localStorage/sessionStorage/IndexedDB)
  • 忽略本地存储的容量限制
  • 对数据同步的实时性要求过高
  • 未考虑表单数据的安全性

联合记忆

  • 存储方案 = localStorage(小数据) + IndexedDB(大数据)
  • 自动保存 = 防抖 + 定时保存 + 页面离开保存
  • 数据同步 = 增量同步 + 冲突检测 + 版本控制
  • 性能优化 = 虚拟滚动 + 延迟加载 + 批量处理

记忆口诀: "长表单应用挑战多,存储同步性能说;本地存储选方案,localStorage 小数据,IndexedDB 大数据;自动保存用防抖,定时保存加离开保存;数据同步增量传,冲突处理版本控;表单性能优化好,虚拟滚动延迟加载;验证错误要处理,用户体验很重要。"

企业级实现

javascript
// 长表单应用实现
class FormManager {
  constructor(options) {
    this.options = options;
    this.formData = {};
    this.saveTimer = null;
    this.syncTimer = null;
    this.version = 0;
    this.storageKey = options.storageKey || 'form-data';
    
    // 初始化
    this.init();
  }
  
  // 初始化
  init() {
    // 从本地存储加载数据
    this.loadFromStorage();
    
    // 设置表单事件监听
    this.setupEventListeners();
    
    // 设置自动保存
    this.setupAutoSave();
    
    // 设置数据同步
    this.setupSync();
  }
  
  // 从本地存储加载数据
  loadFromStorage() {
    try {
      const savedData = localStorage.getItem(this.storageKey);
      if (savedData) {
        const { data, version } = JSON.parse(savedData);
        this.formData = data;
        this.version = version;
        this.options.onDataLoaded?.(this.formData);
      }
    } catch (error) {
      console.error('Failed to load form data from storage:', error);
    }
  }
  
  // 保存到本地存储
  saveToStorage() {
    try {
      const dataToSave = {
        data: this.formData,
        version: this.version,
        timestamp: Date.now()
      };
      localStorage.setItem(this.storageKey, JSON.stringify(dataToSave));
      this.options.onSave?.();
    } catch (error) {
      console.error('Failed to save form data to storage:', error);
    }
  }
  
  // 设置表单事件监听
  setupEventListeners() {
    // 监听表单输入事件(防抖)
    const handleInput = debounce((field, value) => {
      this.updateField(field, value);
    }, 300);
    
    // 监听页面离开事件
    window.addEventListener('beforeunload', () => {
      this.saveToStorage();
    });
    
    // 监听窗口关闭事件
    window.addEventListener('unload', () => {
      this.saveToStorage();
    });
    
    // 暴露给外部调用
    this.handleInput = handleInput;
  }
  
  // 设置自动保存
  setupAutoSave() {
    // 定时保存(每5分钟)
    this.saveTimer = setInterval(() => {
      this.saveToStorage();
    }, 5 * 60 * 1000);
  }
  
  // 设置数据同步
  setupSync() {
    // 定时同步(每10分钟)
    this.syncTimer = setInterval(() => {
      this.syncData();
    }, 10 * 60 * 1000);
  }
  
  // 更新表单字段
  updateField(field, value) {
    this.formData[field] = value;
    this.version++;
    
    // 触发数据更新事件
    this.options.onDataUpdate?.(field, value, this.formData);
  }
  
  // 手动保存
  save() {
    this.saveToStorage();
  }
  
  // 手动同步
  async sync() {
    await this.syncData();
  }
  
  // 数据同步
  async syncData() {
    try {
      // 检查是否有未同步的数据
      const savedData = localStorage.getItem(this.storageKey);
      if (!savedData) return;
      
      const { data, version, timestamp } = JSON.parse(savedData);
      
      // 发送同步请求
      const response = await fetch('/api/form/sync', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          data,
          version,
          timestamp,
          formId: this.options.formId
        })
      });
      
      const result = await response.json();
      
      // 处理同步结果
      if (result.success) {
        // 更新本地版本
        this.version = result.version;
        
        // 处理服务器返回的最新数据
        if (result.data) {
          this.formData = result.data;
          this.options.onDataLoaded?.(this.formData);
        }
        
        this.options.onSyncSuccess?.();
      } else if (result.conflict) {
        // 处理数据冲突
        this.handleConflict(result.serverData);
      }
    } catch (error) {
      console.error('Failed to sync form data:', error);
      this.options.onSyncError?.(error);
    }
  }
  
  // 处理数据冲突
  handleConflict(serverData) {
    // 显示冲突提示
    this.options.onConflict?.(this.formData, serverData, (resolveOption) => {
      if (resolveOption === 'useLocal') {
        // 使用本地数据,重新同步
        this.syncData();
      } else if (resolveOption === 'useServer') {
        // 使用服务器数据
        this.formData = serverData.data;
        this.version = serverData.version;
        this.saveToStorage();
        this.options.onDataLoaded?.(this.formData);
      } else if (resolveOption === 'merge') {
        // 合并数据
        this.formData = { ...serverData.data, ...this.formData };
        this.version = serverData.version + 1;
        this.saveToStorage();
        this.syncData();
      }
    });
  }
  
  // 提交表单
  async submit() {
    try {
      // 保存数据
      this.saveToStorage();
      
      // 同步数据
      await this.syncData();
      
      // 提交表单
      const response = await fetch('/api/form/submit', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          data: this.formData,
          formId: this.options.formId,
          version: this.version
        })
      });
      
      const result = await response.json();
      
      if (result.success) {
        // 提交成功,清除本地存储
        this.clearStorage();
        this.options.onSubmitSuccess?.(result);
      } else {
        this.options.onSubmitError?.(result.error);
      }
    } catch (error) {
      console.error('Failed to submit form:', error);
      this.options.onSubmitError?.(error);
    }
  }
  
  // 清除本地存储
  clearStorage() {
    localStorage.removeItem(this.storageKey);
    this.formData = {};
    this.version = 0;
  }
  
  // 销毁
  destroy() {
    // 清除定时器
    if (this.saveTimer) {
      clearInterval(this.saveTimer);
    }
    if (this.syncTimer) {
      clearInterval(this.syncTimer);
    }
    
    // 保存数据
    this.saveToStorage();
  }
}

// 防抖函数
function debounce(fn, delay) {
  let timer = null;
  return function(...args) {
    if (timer) {
      clearTimeout(timer);
    }
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}

8. 防盗链优化

面试题:如何实现网站资源的防盗链保护?请详细描述其原理和实现方案。

核心考点

  • 防盗链的工作原理
  • Referer 验证
  • Token 验证
  • 时间戳验证
  • CDN 防盗链

常见坑点

  1. Referer 验证的局限性:Referer 可以被伪造
  2. Token 验证的安全性:Token 生成和验证的安全性
  3. 时间戳验证的精度问题:时间同步问题导致验证失败
  4. CDN 防盗链的配置错误:配置错误导致资源无法访问

易错点

  • 误认为防盗链可以完全阻止资源盗用
  • 忽略合法的跨域请求
  • 对 Token 过期时间设置不合理
  • 未考虑 HTTPS 环境下的防盗链

联合记忆

  • 防盗链 = Referer 验证 + Token 验证 + 时间戳验证
  • 实现方案 = 服务端验证 + CDN 配置 + 前端处理
  • 安全性 = 多层验证 + 定期更新策略

记忆口诀: "防盗链保护资源,Referer Token 时间戳;Referer 验证简单,容易被伪造;Token 验证安全,生成算法要复杂;时间戳防滥用,有效期要合理;CDN 防盗链配置,白名单要设置;多层验证更安全,定期更新策略好。"

企业级实现

javascript
// 服务端防盗链实现(Node.js)
// 1. Referer 验证
app.use((req, res, next) => {
  // 需要防盗链的资源类型
  const protectedExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.mp4', '.pdf'];
  const url = new URL(req.url, `http://${req.headers.host}`);
  const pathname = url.pathname;
  
  // 检查是否为需要保护的资源
  const isProtectedResource = protectedExtensions.some(ext => 
    pathname.toLowerCase().endsWith(ext)
  );
  
  if (isProtectedResource) {
    // 检查 Referer
    const referer = req.headers.referer || req.headers.referrer;
    if (referer) {
      try {
        const refererUrl = new URL(referer);
        const allowedDomains = ['example.com', 'cdn.example.com'];
        
        // 检查 Referer 是否来自允许的域名
        if (!allowedDomains.includes(refererUrl.hostname)) {
          // 返回 403 或默认图片
          return res.status(403).send('Forbidden');
          // 或返回默认图片
          // return res.sendFile(path.join(__dirname, 'public', 'default.jpg'));
        }
      } catch (error) {
        // Referer 格式错误,返回 403
        return res.status(403).send('Forbidden');
      }
    } else {
      // 没有 Referer,可能是直接访问
      // 可以允许或拒绝
      return res.status(403).send('Forbidden');
    }
  }
  
  next();
});

// 2. Token 验证
const generateToken = (resource, expires) => {
  const secret = process.env.ANTI_HOTLINK_SECRET;
  const data = {
    resource,
    expires,
    timestamp: Date.now()
  };
  
  // 使用 HMAC 生成签名
  const hmac = crypto.createHmac('sha256', secret);
  hmac.update(JSON.stringify(data));
  const signature = hmac.digest('hex');
  
  // 生成 Token
  return `${signature}.${Buffer.from(JSON.stringify(data)).toString('base64')}`;
};

const validateToken = (token) => {
  try {
    const [signature, dataBase64] = token.split('.');
    const data = JSON.parse(Buffer.from(dataBase64, 'base64').toString());
    const secret = process.env.ANTI_HOTLINK_SECRET;
    
    // 验证签名
    const hmac = crypto.createHmac('sha256', secret);
    hmac.update(JSON.stringify(data));
    const expectedSignature = hmac.digest('hex');
    
    if (signature !== expectedSignature) {
      return false;
    }
    
    // 验证过期时间
    if (Date.now() > data.expires) {
      return false;
    }
    
    return data;
  } catch (error) {
    return false;
  }
};

// Token 验证中间件
app.use('/protected', (req, res, next) => {
  const token = req.query.token;
  if (!token) {
    return res.status(403).send('Forbidden');
  }
  
  const data = validateToken(token);
  if (!data) {
    return res.status(403).send('Invalid token');
  }
  
  // 验证资源路径
  const resource = req.path;
  if (data.resource !== resource) {
    return res.status(403).send('Invalid resource');
  }
  
  next();
});

// 3. 前端生成带 Token 的资源 URL
const generateResourceUrl = (resource, expiresIn = 3600000) => {
  const expires = Date.now() + expiresIn;
  const token = generateToken(resource, expires);
  return `${resource}?token=${token}`;
};

// 4. CDN 防盗链配置(以阿里云 CDN 为例)
// 在 CDN 控制台配置:
// - 防盗链类型:Referer 白名单
// - 允许空 Referer:是/否
// - Referer 白名单:example.com,cdn.example.com
// - 防盗链失效时间:3600 秒
// - Token 防盗链:启用
// - Token 密钥:自定义密钥
// - Token 过期时间:3600 秒
// - Token 包含的参数:path,expires,ip

9. Promise 深入理解

面试题:Promise 有哪些静态方法?请实现 Promise.all、Promise.race 和 Promise.allSettled。

核心考点

  • Promise 静态方法的功能和用法
  • Promise 的状态管理
  • 异步流程控制
  • 错误处理机制
  • 并发控制

常见坑点

  1. Promise.all 的错误处理:只要有一个 Promise 失败就会立即返回失败
  2. Promise.race 的性能问题:未正确处理快速失败的情况
  3. Promise.allSettled 的兼容性:旧浏览器不支持
  4. 自定义 Promise 实现的状态管理:状态转换错误

易错点

  • 混淆不同 Promise 静态方法的行为
  • 忽略 Promise 静态方法的返回值
  • 对 Promise 并发控制的实现错误
  • 未考虑 Promise 静态方法的性能

联合记忆

  • Promise.all = 所有 Promise 都成功才成功,一个失败就失败
  • Promise.race = 第一个完成的 Promise 决定结果
  • Promise.allSettled = 所有 Promise 都完成(无论成功失败)才返回
  • Promise.any = 第一个成功的 Promise 决定结果(ES2021)

记忆口诀: "Promise 静态方法多,all race allSettled any;all 全成功才成功,一个失败就失败;race 比速度,第一个完成定结果;allSettled 等全部,成功失败都返回;any 找成功,第一个成功定结果;实现这些方法要注意,状态管理异步控制。"

企业级实现

javascript
// Promise 静态方法实现

// 1. Promise.all 实现
Promise.myAll = function(promises) {
  return new Promise((resolve, reject) => {
    // 检查参数是否为可迭代对象
    if (!Array.isArray(promises)) {
      return reject(new TypeError('Argument is not iterable'));
    }
    
    const results = [];
    let completed = 0;
    const total = promises.length;
    
    // 处理空数组
    if (total === 0) {
      return resolve([]);
    }
    
    promises.forEach((promise, index) => {
      // 确保每个元素都是 Promise
      Promise.resolve(promise)
        .then(value => {
          results[index] = value;
          completed++;
          
          // 所有 Promise 都已完成
          if (completed === total) {
            resolve(results);
          }
        })
        .catch(error => {
          // 任何一个 Promise 失败就立即返回失败
          reject(error);
        });
    });
  });
};

// 2. Promise.race 实现
Promise.myRace = function(promises) {
  return new Promise((resolve, reject) => {
    // 检查参数是否为可迭代对象
    if (!Array.isArray(promises)) {
      return reject(new TypeError('Argument is not iterable'));
    }
    
    // 处理空数组
    if (promises.length === 0) {
      return;
    }
    
    promises.forEach(promise => {
      // 确保每个元素都是 Promise
      Promise.resolve(promise)
        .then(value => {
          resolve(value);
        })
        .catch(error => {
          reject(error);
        });
    });
  });
};

// 3. Promise.allSettled 实现
Promise.myAllSettled = function(promises) {
  return new Promise((resolve, reject) => {
    // 检查参数是否为可迭代对象
    if (!Array.isArray(promises)) {
      return reject(new TypeError('Argument is not iterable'));
    }
    
    const results = [];
    let completed = 0;
    const total = promises.length;
    
    // 处理空数组
    if (total === 0) {
      return resolve([]);
    }
    
    const processResult = (index, status, value) => {
      results[index] = {
        status,
        [status === 'fulfilled' ? 'value' : 'reason']: value
      };
      completed++;
      
      // 所有 Promise 都已完成
      if (completed === total) {
        resolve(results);
      }
    };
    
    promises.forEach((promise, index) => {
      // 确保每个元素都是 Promise
      Promise.resolve(promise)
        .then(value => {
          processResult(index, 'fulfilled', value);
        })
        .catch(error => {
          processResult(index, 'rejected', error);
        });
    });
  });
};

// 4. Promise.any 实现
Promise.myAny = function(promises) {
  return new Promise((resolve, reject) => {
    // 检查参数是否为可迭代对象
    if (!Array.isArray(promises)) {
      return reject(new TypeError('Argument is not iterable'));
    }
    
    const errors = [];
    let rejected = 0;
    const total = promises.length;
    
    // 处理空数组
    if (total === 0) {
      return reject(new AggregateError([], 'All promises were rejected'));
    }
    
    promises.forEach((promise, index) => {
      // 确保每个元素都是 Promise
      Promise.resolve(promise)
        .then(value => {
          resolve(value);
        })
        .catch(error => {
          errors[index] = error;
          rejected++;
          
          // 所有 Promise 都已失败
          if (rejected === total) {
            reject(new AggregateError(errors, 'All promises were rejected'));
          }
        });
    });
  });
};

10. Nginx 反向代理与负载均衡

面试题:如何配置 Nginx 实现反向代理和负载均衡?请详细描述其配置和原理。

核心考点

  • Nginx 反向代理的原理
  • 负载均衡策略(轮询/权重/ip_hash/least_conn)
  • Nginx 配置语法
  • 健康检查和故障转移
  • 性能优化配置

常见坑点

  1. 反向代理的配置错误:代理路径或参数配置错误
  2. 负载均衡策略选择不当:未根据实际情况选择合适的策略
  3. 健康检查配置错误:未正确配置健康检查导致故障转移失败
  4. 性能优化配置不合理:过度优化或配置错误导致性能下降

易错点

  • 混淆正向代理与反向代理
  • 忽略 Nginx 配置的语法错误
  • 对负载均衡权重设置不合理
  • 未考虑 HTTPS 环境下的反向代理

联合记忆

  • 反向代理 = 客户端 → Nginx → 后端服务器
  • 负载均衡 = 轮询 + 权重 + ip_hash + least_conn
  • 配置 = upstream 块 + server 块 + location 块
  • 优化 = 连接池 + 缓存 + Gzip 压缩 + 限流

记忆口诀: "Nginx 反向代理好,客户端请求中间过;负载均衡分流量,轮询权重 ip_hash least_conn;upstream 块定义服务器组,server 块配置服务器;location 块处理请求,proxy_pass 指向 upstream;健康检查监控服务器,故障转移自动切;性能优化连接池,缓存压缩限流齐。"

企业级配置

nginx
# Nginx 反向代理与负载均衡配置

# 1. 定义 upstream 服务器组
upstream backend {
  # 轮询(默认)
  server backend1.example.com:8080 weight=3;  # 权重为 3,接收 3/6 的请求
  server backend2.example.com:8080 weight=2;  # 权重为 2,接收 2/6 的请求
  server backend3.example.com:8080 weight=1;  # 权重为 1,接收 1/6 的请求
  
  # ip_hash 策略(同一 IP 始终转发到同一服务器)
  # ip_hash;
  
  # least_conn 策略(转发到连接数最少的服务器)
  # least_conn;
  
  # 健康检查配置
  keepalive 32;  # 保持连接数
  keepalive_timeout 60s;  # 保持连接超时时间
  
  # 自定义健康检查(需要 ngx_http_upstream_check_module)
  # check interval=3000 rise=2 fall=5 timeout=1000 type=http;
  # check_http_send "HEAD /health HTTP/1.0\r\nHost: $host\r\nConnection: close\r\n\r\n";
  # check_http_expect_alive http_2xx http_3xx;
}

# 2. 主配置
server {
  listen 80;
  server_name example.com;
  
  # 3. 反向代理配置
  location / {
    # 代理设置
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection 'upgrade';
    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;
    
    # 代理缓存配置
    proxy_cache_bypass $http_upgrade;
    proxy_cache_valid 200 302 10m;
    proxy_cache_valid 404 1m;
    proxy_cache_key "$scheme$request_method$host$request_uri";
    
    # 超时配置
    proxy_connect_timeout 60s;
    proxy_send_timeout 60s;
    proxy_read_timeout 60s;
    
    # 缓冲区配置
    proxy_buffers 8 16k;
    proxy_buffer_size 32k;
  }
  
  # 4. 静态资源配置
  location ~* \.(jpg|jpeg|png|gif|css|js|ico)$ {
    root /var/www/html;
    expires 30d;
    add_header Cache-Control "public, max-age=2592000";
  }
  
  # 5. 健康检查端点
  location /health {
    proxy_pass http://backend;
    proxy_set_header Host $host;
    access_log off;
    log_not_found off;
  }
}

# 6. HTTPS 配置
server {
  listen 443 ssl http2;
  server_name example.com;
  
  # SSL 证书配置
  ssl_certificate /etc/nginx/ssl/example.com.crt;
  ssl_certificate_key /etc/nginx/ssl/example.com.key;
  
  # SSL 优化配置
  ssl_protocols TLSv1.2 TLSv1.3;
  ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305';
  ssl_prefer_server_ciphers off;
  ssl_session_cache shared:SSL:10m;
  ssl_session_timeout 10m;
  ssl_session_tickets off;
  
  # HSTS 配置
  add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
  
  # 反向代理配置
  location / {
    proxy_pass http://backend;
    # 其他代理配置同 HTTP
  }
}

# 7. 负载均衡状态监控(需要 ngx_http_upstream_status_module)
location /upstream_status {
  stub_status on;
  access_log off;
  allow 127.0.0.1;
  deny all;
}

# 8. 限流配置
http {
  # 限流配置
  limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s;
  
  server {
    # 应用限流
    location /api {
      limit_req zone=mylimit burst=20 nodelay;
      proxy_pass http://backend;
    }
  }
}

11. Rollup 高级配置

面试题:Rollup 如何实现代码分割和按需加载?请详细描述其配置和原理。

核心考点

  • Rollup 代码分割的原理
  • 动态导入的处理
  • 输出格式的选择
  • 插件的使用
  • 性能优化

常见坑点

  1. 代码分割配置错误:未正确配置 manualChunks 或 experimentalCodeSplitting
  2. 动态导入的语法错误:未使用正确的动态导入语法
  3. 输出格式选择不当:未根据使用场景选择合适的输出格式
  4. 插件版本不兼容:插件版本与 Rollup 版本不兼容

易错点

  • 混淆 Rollup 与 Webpack 的代码分割实现
  • 忽略 Rollup 对 CommonJS 模块的支持
  • 对 Rollup 的输出格式理解错误
  • 未考虑代码分割的性能影响

联合记忆

  • 代码分割 = 动态导入 + manualChunks + experimentalCodeSplitting
  • 实现方案 = 配置 manualChunks + 使用动态导入 + 选择合适的输出格式
  • 优化 = Tree Shaking + 压缩 + 缓存

记忆口诀: "Rollup 代码分割好,动态导入是关键;manualChunks 自定义,experimentalCodeSplitting 实验性;输出格式选正确,es 格式支持动态导入;插件配合使用,@rollup/plugin-commonjs 处理 CommonJS;代码分割性能优,Tree Shaking 不可少。"

企业级配置

javascript
// Rollup 高级配置
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import babel from '@rollup/plugin-babel';
import terser from '@rollup/plugin-terser';
import { visualizer } from 'rollup-plugin-visualizer';
import alias from '@rollup/plugin-alias';
import json from '@rollup/plugin-json';
import replace from '@rollup/plugin-replace';
import postcss from 'rollup-plugin-postcss';
import autoprefixer from 'autoprefixer';
import cssnano from 'cssnano';

export default {
  // 1. 入口配置
  input: {
    main: 'src/index.js',
    app: 'src/app.js'
  },
  
  // 2. 输出配置
  output: [
    // ES Module 输出(支持代码分割和动态导入)
    {
      dir: 'dist/es',
      format: 'es',
      sourcemap: true,
      entryFileNames: '[name].[hash].js',
      chunkFileNames: '[name].[hash].chunk.js',
      manualChunks: {
        // 将第三方库打包到单独的 chunk
        'vendor': ['react', 'react-dom'],
        'utils': ['lodash', 'axios'],
        'ui': ['antd', 'element-plus']
      }
    },
    // CommonJS 输出
    {
      dir: 'dist/cjs',
      format: 'cjs',
      sourcemap: true,
      entryFileNames: '[name].cjs.js',
      chunkFileNames: '[name].[hash].chunk.cjs.js'
    }
  ],
  
  // 3. 插件配置
  plugins: [
    // 路径别名
    alias({
      entries: [
        { find: '@', replacement: './src' }
      ]
    }),
    
    // 解析 Node.js 模块
    resolve({
      browser: true,
      preferBuiltins: false
    }),
    
    // 转换 CommonJS 模块
    commonjs({
      include: /node_modules/
    }),
    
    // Babel 转换
    babel({
      exclude: /node_modules/,
      babelHelpers: 'runtime',
      presets: [
        ['@babel/preset-env', {
          targets: {
            browsers: ['> 1%', 'last 2 versions']
          },
          modules: false
        }],
        ['@babel/preset-react', {
          runtime: 'automatic'
        }]
      ],
      plugins: [
        ['@babel/plugin-transform-runtime', {
          corejs: 3
        }]
      ]
    }),
    
    // 环境变量替换
    replace({
      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
      preventAssignment: true
    }),
    
    // CSS 处理
    postcss({
      plugins: [
        autoprefixer(),
        cssnano()
      ],
      extract: true,
      sourceMap: true
    }),
    
    // JSON 处理
    json(),
    
    // 代码压缩
    terser({
      compress: {
        drop_console: true,
        drop_debugger: true
      },
      mangle: {
        toplevel: true
      }
    }),
    
    // 构建分析
    visualizer({
      filename: 'dist/stats.html',
      open: true
    })
  ],
  
  // 4. 外部依赖配置
  external: [
    'react',
    'react-dom'
  ],
  
  // 5. 高级配置
  preserveEntrySignatures: false,
  treeshake: {
    moduleSideEffects: false,
    propertyReadSideEffects: false
  },
  
  // 6. 缓存配置
  cache: true
};

// 动态导入示例
// src/app.js
import('./components/HeavyComponent').then(HeavyComponent => {
  // 使用 HeavyComponent
});

// src/utils/api.js
export const fetchData = async () => {
  const { default: axios } = await import('axios');
  return axios.get('/api/data');
};

总结

本综合面试题覆盖了前端开发的核心技术领域,包括 ES6+ 高级语法、Vue 全家桶、构建工具、Node.js 框架、CSS 预处理器、组件库开发、浏览器工作原理、前端工程化与性能优化、高级应用与实践等。

每道面试题都包含了核心考点、常见坑点、易错点、联合记忆、记忆口诀和企业级实现代码,帮助你深入理解前端开发的各个方面,为高级前端工程师面试做好充分准备。

在企业级开发实践中,需要结合具体业务场景,灵活运用这些技术和知识,注重代码质量、性能优化、用户体验和团队协作,不断提升自己的技术能力和解决问题的能力。

祝你面试成功!

Updated at:

Released under the MIT License.