每日面试题-前端2

一、React (T0)

1.1 hooks原理

题目:手写实现一个简化版 React Hooks(useState + useEffect)

请实现一个简化版的 React Hooks 系统,包含 useStateuseEffect,能够支持以下功能:

jsx 复制代码
function Counter() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('counter');

  useEffect(() => {
    console.log('count changed:', count);
    return () => console.log('cleanup:', count);
  }, [count]);

  return (
    <div>
      <h1>{name}: {count}</h1>
      <button onClick={() => setCount(count + 1)}>+</button>
      <button onClick={() => setName(name === 'counter' ? 'cnt' : 'counter')}>toggle</button>
    </div>
  );
}

参考答案

javascript 复制代码
let currentComponent = null;
let hookIndex = 0;

class ReactElement {
  constructor(type, props) {
    this.type = type;
    this.props = props;
  }
}

function useState(initialValue) {
  const hooks = currentComponent.hooks;
  const index = hookIndex;

  if (hooks[index] === undefined) {
    hooks[index] = {
      state: typeof initialValue === 'function' ? initialValue() : initialValue,
      queue: []
    };
  }

  const currentState = hooks[index].state;
  const setState = (newState) => {
    const value = typeof newState === 'function' ? newState(currentState) : newState;
    if (value !== currentState) {
      hooks[index].state = value;
      render();
    }
  };

  hookIndex++;
  return [currentState, setState];
}

function useEffect(callback, deps) {
  const hooks = currentComponent.hooks;
  const index = hookIndex;
  const prevDeps = hooks[index]?.deps;

  const hasChanged = !prevDeps || deps.some((dep, i) => dep !== prevDeps[i]);

  if (hasChanged) {
    if (hooks[index]?.cleanup) {
      hooks[index].cleanup();
    }
    hooks[index] = {
      deps,
      cleanup: callback()
    };
  }

  hookIndex++;
}

function createElement(type, props, ...children) {
  return new ReactElement(type, { ...props, children });
}

function render() {
  hookIndex = 0;
  const element = currentComponent();
  console.log('Rendered:', JSON.stringify(element));
}

function mount(Component) {
  currentComponent = Component;
  currentComponent.hooks = [];
  render();
}

const MyCounter = () => {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('counter');

  useEffect(() => {
    console.log('count changed:', count);
    return () => console.log('cleanup:', count);
  }, [count]);

  return createElement('div', null,
    createElement('h1', null, `${name}: ${count}`),
    createElement('button', { onClick: () => setCount(count + 1) }, '+'),
    createElement('button', { onClick: () => setName(name === 'counter' ? 'cnt' : 'counter') }, 'toggle')
  );
};

mount(MyCounter);

详细解析

核心原理

  1. 链表存储 Hooks 状态:React 使用链表存储每个组件的所有 hooks 状态,每次 render 时按顺序读取
  2. Fiber 节点关联 :每个 ReactElement/Fiber 节点通过 memoizedState 属性指向 hooks 链表
  3. 下标递增 :每次 useStateuseEffect 调用时,递增全局下标来获取对应状态
  4. deps 比较useEffect 通过浅比较 deps 数组判断是否需要重新执行
  5. cleanup 函数:effect 执行后返回的函数会在下次 effect 执行前或组件卸载时调用

关键点

  • Hooks 不能在条件语句、循环中调用,因为依赖下标顺序
  • useState 的函数式更新确保基于最新状态计算
  • Effect 的 cleanup 模式用于防止内存泄漏和竞态条件

1.2 diff算法

题目:React 的 Diff 算法是如何工作的?为什么 React 选择 O(n³) 复杂度的树 diff 却采用了 O(n) 的策略?

参考答案

React 采用的 O(n) 策略是"分层比较"

  1. Tree Diff:同层节点比较,不同则删除重建
  2. Component Diff:同类型组件继续 diff 子节点
  3. Element Diff:同层级元素比较 key 进行位置复用

React 选择 O(n) 的原因

策略 复杂度 实际效果
传统树 Diff O(n³) 最小编辑距离,计算代价高
React 分层 Diff O(n) 实际场景效果接近最优

为什么可行

  • Web UI 中 DOM 节点跨层级的操作极少(<1%)
  • 组件更新通常是同层兄弟节点的增删
  • 实际业务中跨层级移动 DOM 成本高,用户体验差

图示

less 复制代码
Layer 1:    A                    A
Layer 2:  B   C    →           B   C
Layer 3: D   E              D   E   F (新)

操作: F 是新节点,只需创建插入
     D, E 节点位置不变,通过 key 复用

详细解析

Tree Diff 核心规则

  • 根节点类型不同 → 销毁旧树,创建新树
  • 根节点类型相同 → 复用 DOM 节点,只更新属性

Component Diff 优化

jsx 复制代码
// 组件类型改变 → 整个组件替换
// 类型相同但 props 相同 → 跳过更新
// 这就是 shouldComponentUpdate 存在的意义

Element Diff 的 key 机制

jsx 复制代码
// 无 key:位置改变 = 删除 + 重建
// 有 key:位置改变 = 移动
{list.map(item => <div key={item.id}>{item.name}</div>)}

性能优化建议

  • 保持 DOM 结构稳定,避免跨层级移动
  • 使用稳定的唯一 key(如 id,避免 index)
  • 在列表末尾添加元素性能最优
  • 在列表头部插入元素会导致所有节点重建

1.3 React18并发

题目:什么是 React 18 的并发渲染(Concurrent Rendering)?它解决了什么问题?请举例说明 useTransition 的使用场景。

参考答案

并发渲染核心概念

css 复制代码
普通模式:render 是同步阻塞的
         ┌─────────────┐
  Input  │  完整渲染   │ → UI 卡顿
         └─────────────┘

并发模式:render 可中断
         ┌──────────┬──────────┬──┐
  Input  │ 优先级高  │  优先级高 │  │ 低优先级任务
         │ 渲染完成  │  渲染完成 │...│ 后台慢慢完成
         └──────────┴──────────┴──┘
         → UI 保持响应

解决的问题

  1. 渲染可中断:长时间渲染任务可以分片执行
  2. 状态更新优先级startTransition 标记非紧急更新
  3. Suspense 边界:数据未就绪时显示 loading
  4. 自动批处理:所有更新统一批处理,减少 render 次数

useTransition 使用示例

jsx 复制代码
import { useState, useTransition } from 'react';

function SearchResults() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();

  function handleSearch(e) {
    setQuery(e.target.value);

    // 标记为非紧急更新,可以被高优先级更新打断
    startTransition(() => {
      // 模拟大量数据处理
      const newResults = expensiveSearch(e.target.value);
      setResults(newResults);
    });
  }

  return (
    <div>
      <input value={query} onChange={handleSearch} />

      {/* 高优先级更新不受影响 */}
      <div className="suggestions">{/* 即时显示的联想词 */}</div>

      {/* 低优先级更新显示加载状态 */}
      <div style={{ opacity: isPending ? 0.5 : 1 }}>
        {results.map(r => <ResultItem key={r.id} data={r} />)}
      </div>
    </div>
  );
}

详细解析

并发渲染三大支柱

  1. Fiber 架构:将渲染工作拆分成可中断的小单元
  2. Scheduler:基于优先级队列的任务调度
  3. ** lanes/优先级机制**:区分更新优先级

useTransition 原理

  • 将闭包内的 setState 标记为 transition 优先级
  • 期间产生的更高优先级更新可以打断当前渲染
  • isPending 告知用户有后台任务进行中

实际应用场景

  • 搜索框输入 → 实时搜索结果(高优先级)
  • 搜索触发的大数据列表渲染(低优先级,用 transition)
  • Tab 切换 → 新页面内容(transition)
  • Modal 弹窗 → 立即响应(高优先级)

React 18 自动批处理增强

jsx 复制代码
// React 18 之前:setTimeout 中的 setState 会触发多次 render
// React 18 之后:自动批处理,只触发一次 render
setTimeout(() => {
  setA(1);
  setB(2);
  // React 18 只触发一次 render
}, 0);

1.4 状态管理

题目:现代 React 应用中,状态管理方案有哪些?请对比 Context、Zustand、Redux Toolkit 的适用场景,并说明在 AI Agent 产品中如何选择。

参考答案

状态管理方案对比

方案 适用场景 优点 缺点
Context + useReducer 中小型应用、主题/语言/用户态 原生、无依赖 重渲染问题、层级深时性能差
Zustand 中型应用、简单状态共享 轻量、API 简洁、immer 支持 缺少强制规范
Redux Toolkit 大型复杂应用、需要严格规范 生态成熟、devtools 强大 配置复杂、模板代码多
Jotai 原子化状态、细粒度更新 按需渲染、组合性好 概念新、学习成本
TanStack Query 服务端状态、缓存 自动缓存/同步/乐观更新 定位不同

代码示例 - Zustand

jsx 复制代码
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';

const useAIStore = create(
  immer((set) => ({
    // AI 对话状态
    messages: [],
    isLoading: false,
    conversationId: null,

    // Actions
    addMessage: (role, content) => set((state) => {
      state.messages.push({ role, content, id: crypto.randomUUID(), timestamp: Date.now() });
    }),

    setLoading: (loading) => set({ isLoading: loading }),

    clearMessages: () => set({ messages: [], conversationId: null }),
  }))
);

// 在组件中使用
function ChatPanel() {
  const { messages, isLoading, addMessage } = useAIStore();

  const sendMessage = async (content) => {
    addMessage('user', content);
    // 调用 AI API...
  };

  return (
    <div>
      {messages.map(msg => (
        <MessageBubble key={msg.id} role={msg.role} content={msg.content} />
      ))}
    </div>
  );
}

AI Agent 产品选型建议

复制代码
┌─────────────────────────────────────────────────┐
│              状态类型分类                         │
├─────────────────┬───────────────────────────────┤
│  UI 本地状态     │  useState / useReducer        │
│  跨组件共享 UI   │  Context / Zustand           │
│  服务端缓存状态   │  TanStack Query / SWR        │
│  复杂 AI 会话状态 │  Zustand + Immer             │
│  全局配置/主题   │  Context                      │
└─────────────────┴───────────────────────────────┘

详细解析

Context 的重渲染问题

jsx 复制代码
// 问题:Provider 下的所有组件都会重渲染
<ThemeContext.Provider value={theme}>
  <App />
</ThemeContext.Provider>

// 解决方案 1:拆分 Context
<ThemeProvider>    // 只影响需要主题的组件
<LocaleProvider>   // 只影响需要语言的组件
<AuthProvider>     // 只影响需要登录态的组件

// 解决方案 2:使用 useMemo 选择性订阅
const theme = useContext(ThemeContext); // 整个 context
const { color } = useContext(ThemeContext); // 只订阅需要的值(仍然会重渲染)

Redux Toolkit vs Zustand

  • Redux Toolkit:需要严格规范、大型团队、复杂业务逻辑
  • Zustand:快速开发、简单状态、需要性能优化

AI Agent 场景特殊考虑

  1. 对话历史:大数组状态,需要注意更新性能
  2. 流式响应:需要支持增量更新 messages
  3. 多会话管理:Tab 页签隔离,每个 tab 独立 store 实例
  4. 持久化:对话内容需要 localStorage/indexedDB 持久化

二、JavaScript/TypeScript (T1)

2.1 event loop

题目:请详细描述 JavaScript 事件循环机制,并分析以下代码的执行顺序:

javascript 复制代码
console.log('1');

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

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

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

console.log('6');

// 问:最终输出顺序是什么?

参考答案

最终输出顺序 : 1 → 6 → 3 → 5 → 2 → 4

执行过程详解

scss 复制代码
┌─────────────────────────────────────────────────────────────┐
│                      执行栈 (Call Stack)                     │
│  同步代码按顺序执行 → 输出 1 → 输出 6                          │
└─────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────┐
│                  微任务队列 (Microtask Queue)                 │
│  Promise.then 回调                                           │
│  queueMicrotask                                             │
│  MutationObserver                                           │
│                                                              │
│  处理顺序:先入先出,直至队列为空                              │
│  → 输出 3                                                    │
│  → 执行 5 的同步代码,输出 5                                  │
│  → setTimeout(4) 进入宏任务队列                               │
└─────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────┐
│                  宏任务队列 (Macrotask Queue)                 │
│  setTimeout 回调                                              │
│  setInterval 回调                                            │
│  I/O 操作                                                    │
│  UI 渲染 (浏览器)                                             │
│                                                              │
│  处理完一个宏任务 → 检查微任务队列 → 渲染 → 下一个宏任务        │
│  → 输出 2                                                    │
│  → 输出 4                                                    │
└─────────────────────────────────────────────────────────────┘

详细解析

关键概念

  1. 执行栈:同步代码立即执行,LIFO 顺序
  2. 微任务队列 :每个宏任务执行完后、在渲染前,清空所有微任务
  3. 宏任务队列:每执行完一个,才检查微任务

常见微任务 vs 宏任务

微任务 (Microtasks) 宏任务 (Macrotasks)
Promise.then/catch/finally setTimeout / setInterval
queueMicrotask setImmediate (Node.js)
MutationObserver I/O 操作
async/await (底层是 Promise) requestAnimationFrame
process.nextTick (Node.js) UI 渲染 (浏览器)

async/await 原理

javascript 复制代码
// 写法
async function foo() {
  await bar();
  console.log('after await');
}

// 等价于
function foo() {
  return Promise.resolve().then(() => {
    return bar().then(() => {
      console.log('after await');
    });
  });
}

浏览器渲染时机

javascript 复制代码
setTimeout(() => console.log('setTimeout'), 0);
// 渲染发生在宏任务之间
requestAnimationFrame(() => console.log('rAF'));
// rAF 在下一次渲染前执行

2.2 promise

题目:实现一个 Promise.allSettled 方法,并说明它与 Promise.all 的区别。

参考答案

javascript 复制代码
/**
 * Promise.allSettled - 返回所有 Promise 的结果,无论成功或失败
 * @param {Promise[]} promises - Promise 数组
 * @returns {Promise<Array>} 所有 Promise 的 settled 结果
 */
function promiseAllSettled(promises) {
  return Promise.all(
    promises.map((promise) =>
      Promise.resolve(promise).then(
        (value) => ({
          status: 'fulfilled',
          value
        }),
        (reason) => ({
          status: 'rejected',
          reason
        })
      )
    )
  );
}

// 使用示例
const promises = [
  Promise.resolve(1),
  Promise.reject(new Error('error')),
  Promise.resolve(3)
];

promiseAllSettled(promises).then((results) => {
  console.log(results);
  // [
  //   { status: 'fulfilled', value: 1 },
  //   { status: 'rejected', reason: Error: error },
  //   { status: 'fulfilled', value: 3 }
  // ]
});

Promise.all vs Promise.allSettled 对比

特性 Promise.all Promise.allSettled
成功条件 所有都成功 无条件,始终成功
失败行为 任一失败则整体失败 每个独立记录成功/失败
返回结果 只保留成功值 每个 promise 的状态+值
典型场景 "全部成功才继续" "收集所有结果,容错处理"
javascript 复制代码
// Promise.all:任一失败全部失败
const results = await Promise.all([
  fetchUser(userId),
  fetchPermissions(userId),
  fetchPreferences(userId)
]);
// 任意一个失败,整个结果丢失

// Promise.allSettled:收集所有结果
const results = await Promise.allSettled([
  fetchUser(userId),
  fetchPermissions(userId),
  fetchPreferences(userId)
]);

results.forEach((result, index) => {
  if (result.status === 'fulfilled') {
    console.log(`成功:`, result.value);
  } else {
    console.log(`失败 ${index}:`, result.reason);
  }
});

详细解析

Promise.allSettled 适用场景

  1. 批量操作,容错处理:如批量发送通知,失败的不影响其他的
  2. 并行执行,独立追踪:如并行加载多个资源,部分失败不影响展示
  3. Dashboard 场景:展示所有数据源的状态,而不是因为一个失败全部不展示

手写 Promise.allSettled 关键点

javascript 复制代码
// 1. 先用 Promise.resolve 包装,确保输入是 Promise
Promise.resolve(promise)

// 2. then 接收成功和失败两种情况
.then(
  value => ({ status: 'fulfilled', value }),
  reason => ({ status: 'rejected', reason })
)

// 3. Promise.all 确保全部处理完才返回
Promise.all(mappedPromises)

Promise 静态方法全家桶

javascript 复制代码
Promise.resolve(value)      // 返回一个 resolved Promise
Promise.reject(reason)      // 返回一个 rejected Promise
Promise.all(iterable)       // 全部成功才成功
Promise.allSettled(arr)     // 收集所有结果
Promise.race(iterable)      // 返回最先 settle 的结果
Promise.any(iterable)       // 返回最先 fulfilled 的,忽略 rejections

2.3 闭包

题目:什么是闭包?以下代码会产生什么问题?如何修复?

javascript 复制代码
for (var i = 0; i < 5; i++) {
  setTimeout(() => console.log(i), 100);
}

// 期望输出:0, 1, 2, 3, 4
// 实际输出:5, 5, 5, 5, 5

参考答案

闭包定义

闭包是指函数能够访问其词法作用域外部的变量,即使该函数在其词法作用域之外执行。

javascript 复制代码
function createCounter() {
  let count = 0; // 闭包变量

  return function() {
    return ++count;
  };
}

const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
// count 变量被闭包引用,不会被 GC 回收

问题分析

css 复制代码
var i 的作用域是函数级别,不是块级
for 循环结束后,i = 5

setTimeout 回调形成闭包,引用同一个 i
100ms 后执行时,i 已经是 5

→ 输出 5 个 5

三种修复方式

javascript 复制代码
// 方式 1:使用 let(块级作用域)
for (let i = 0; i < 5; i++) {
  setTimeout(() => console.log(i), 100);
}
// let 每次循环创建新作用域,闭包捕获各自的 i

// 方式 2:使用 IIFE 闭包
for (var i = 0; i < 5; i++) {
  (function(j) {
    setTimeout(() => console.log(j), 100);
  })(i);
}

// 方式 3:使用 bind 传参
for (var i = 0; i < 5; i++) {
  setTimeout(console.log.bind(null, i), 100);
}

详细解析

闭包的应用场景

javascript 复制代码
// 1. 数据私有化
function createUser(name) {
  return {
    getName: () => name,
    setName: (newName) => { name = newName; }
  };
}

// 2. 函数工厂
function multiplier(factor) {
  return (num) => num * factor;
}
const double = multiplier(2);
const triple = multiplier(3);

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

// 4. 缓存/记忆化
function memoize(fn) {
  const cache = new Map();
  return function(...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key);
    const result = fn.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

闭包的内存泄漏风险

javascript 复制代码
// 问题:大型对象被闭包引用,无法被 GC
function badExample() {
  const largeData = new Array(1000000);
  const button = document.getElementById('button');

  button.addEventListener('click', () => {
    console.log(largeData.length); // 即使不用 largeData,也被闭包引用
  });
}

// 解决:及时清理
function goodExample() {
  const largeData = new Array(1000000);
  const button = document.getElementById('button');

  const handler = () => console.log(largeData.length);
  button.addEventListener('click', handler);

  // 不用时移除
  return () => button.removeEventListener('click', handler);
}

2.4 原型链

题目:请解释 JavaScript 的原型链机制,并实现一个 myInstanceof 方法。

参考答案

原型链图示

javascript 复制代码
┌─────────────────────────────────────────────────────────┐
│                    Object.prototype                      │
│                         ↑                                │
│                        /                                 │
│                       /                                  │
│              Person.prototype                            │
│                         ↑                                │
│                        /                                 │
│                       /                                  │
│              person 实例                                 │
│                                                         │
│  person.__proto__ === Person.prototype                  │
│  Person.prototype.__proto__ === Object.prototype        │
│  Object.prototype.__proto__ === null  ← 链的终点          │
└─────────────────────────────────────────────────────────┘

原型链查找规则

javascript 复制代码
function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.sayHello = function() {
  console.log(`I'm ${this.name}`);
};

const person = new Person('Alice', 25);

// 属性查找顺序:
// 1. person 自身属性 → name, age
// 2. person.__proto__ (Person.prototype) → sayHello
// 3. person.__proto__.__proto__ (Object.prototype) → hasOwnProperty, toString...
// 4. null → 查找结束

myInstanceof 实现

javascript 复制代码
/**
 * myInstanceof - 模拟 instanceof 操作符
 * @param {any} left - 要检查的对象
 * @param {any} right - 要检查的构造函数
 * @returns {boolean}
 */
function myInstanceof(left, right) {
  // 基础类型直接返回 false
  if (left === null || typeof left !== 'object') {
    return false;
  }

  // 获取左侧对象的原型
  let proto = Object.getPrototypeOf(left);

  // 沿着原型链向上查找
  while (proto !== null) {
    // 如果找到构造函数的 prototype,返回 true
    if (proto === right.prototype) {
      return true;
    }
    // 继续向上查找
    proto = Object.getPrototypeOf(proto);
  }

  return false;
}

// 测试
function Person() {}
const person = new Person();

console.log(myInstanceof(person, Person));        // true
console.log(myInstanceof(person, Object));         // true
console.log(myInstanceof(person, Array));          // false
console.log(myInstanceof('string', String));       // true
console.log(myInstanceof(123, Number));            // true
console.log(myInstanceof(null, Object));           // false

详细解析

ES6 类与原型链

javascript 复制代码
class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(`${this.name} makes a noise.`);
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name);  // 调用父类构造函数
    this.breed = breed;
  }

  speak() {
    console.log(`${this.name} barks.`);
  }
}

const dog = new Dog('Rex', 'German Shepherd');

// 原型链:
// dog → Dog.prototype → Animal.prototype → Object.prototype → null

原型链继承的实现方式

javascript 复制代码
// 原型链继承
function Parent() { this.name = 'parent'; }
function Child() { Parent.call(this); this.type = 'child'; }
Child.prototype = new Parent();
Child.prototype.constructor = Child;

// Class 继承(ES6+,语法糖)
class Parent {
  constructor() { this.name = 'parent'; }
}

class Child extends Parent {
  constructor() {
    super();
    this.type = 'child';
  }
}

实际开发中的应用

javascript 复制代码
// 1. 判断数据类型(原型链方法)
Object.prototype.toString.call([]);     // "[object Array]"
Object.prototype.toString.call({});      // "[object Object]"
Object.prototype.toString.call(null);    // "[object Null]"

// 2. 原型方法复用(节省内存)
const arr = [1, 2, 3];
console.log(arr.hasOwnProperty('length'));  // 从 Object.prototype 继承

// 3. 继承已有对象
const parent = { greet: () => console.log('Hello') };
const child = Object.create(parent);
child.greet(); // "Hello" - 通过原型链访问

三、Next.js (T1)

3.1 App Router

题目:Next.js App Router 与 Pages Router 相比有哪些核心变化?为什么说 App Router 是未来趋势?

参考答案

核心变化对比

特性 Pages Router App Router
路由约定 文件系统路由 嵌套文件夹 + layout
组件类型 Client Component 默认 Server Component 默认
数据获取 getServerSideProps async 组件 / fetch
布局 单一 _app.js 嵌套 layout.js
路由组 (group) (group)
加载状态 无内置 loading.js
错误处理 _error.js error.js
样式 CSS Modules CSS Modules + CSS-in-JS

App Router 核心概念

ini 复制代码
app/
├── layout.js          # 根布局,所有页面共享
├── page.js            # 首页 (/)
├── about/
│   ├── page.js        # /about
│   └── loading.js     # /about 加载状态
├── blog/
│   ├── layout.js      # 博客布局(header/footer)
│   ├── page.js        # /blog
│   └── [slug]/
│       └── page.js    # /blog/:slug
└── (marketing)/
    ├── layout.js      # 营销页面布局
    ├── page.js        # /
    └── about/
        └── page.js    # /about

代码示例

jsx 复制代码
// app/blog/[slug]/page.js
// Server Component - 默认,所有数据获取在服务端完成

import { notFound } from 'next/navigation';
import { db } from '@/lib/db';

// 静态生成 + ISR
export async function generateStaticParams() {
  const posts = await db.post.findMany();
  return posts.map((post) => ({
    slug: post.slug,
  }));
}

// 动态元数据
export async function generateMetadata({ params }) {
  const post = await db.post.findUnique({
    where: { slug: params.slug }
  });

  return {
    title: post?.title || 'Post not found',
    description: post?.excerpt,
  };
}

export default async function BlogPost({ params }) {
  const post = await db.post.findUnique({
    where: { slug: params.slug }
  });

  if (!post) {
    notFound();
  }

  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}

详细解析

为什么 App Router 是未来

  1. React Server Components (RSC)

    • 默认服务端渲染,减少客户端 JS bundle
    • 直接在服务端访问数据库/API
    • 敏感信息不泄露到客户端
  2. 嵌套布局系统

    jsx 复制代码
    // 根布局:导航栏、底部版权
    // 子布局:侧边栏
    // 页面:具体内容
    // 完美契合大型应用的布局需求
  3. Streaming + Suspense

    jsx 复制代码
    // 页面骨架立即可见,内容逐步加载
    export default function Page() {
      return (
        <div>
          <Navbar /> {/* 立即显示 */}
          <Suspense fallback={<Skeleton />}>
            <HeavyComponent /> {/* 流式加载 */}
          </Suspense>
        </div>
      );
    }
  4. 更精细的数据获取

    jsx 复制代码
    // 可以针对每个组件独立数据获取
    // 不再需要顶层统一获取

迁移建议

  • 新项目直接使用 App Router
  • 旧项目渐进式迁移:App Router 与 Pages Router 可以共存
  • 注意 Client Component 的边界划分

3.2 SSR

题目:什么是 SSR(服务端渲染)?Next.js App Router 中的 SSR 与传统 SSR 有何不同?请说明 React Server Components 如何优化 SSR 场景。

参考答案

SSR 核心原理

markdown 复制代码
传统 CSR:
┌──────┐    ┌─────────┐    ┌─────────────┐
│用户   │ → │服务器返回 │ → │ 加载 JS      │
│请求   │    │ HTML    │    │ 客户端渲染   │
└──────┘    └─────────┘    └─────────────┘
            (空白/loading)   (白屏等待)

SSR:
┌──────┐    ┌─────────────┐
│用户   │ → │服务器渲染完成 │
│请求   │    │ 返回完整HTML │
└──────┘    └─────────────┘
            (立即可见)

App Router SSR vs 传统 SSR

维度 传统 SSR (Pages Router) App Router SSR
渲染主体 服务端完整渲染 React Server Components
组件嵌套 全部服务端渲染 可选择 Client/Server
水合成本 全量水合 部分水合(只水合交互部分)
数据获取 getServerSideProps async 组件
状态传递 序列化 props 直接数据库访问

React Server Components 优化示例

jsx 复制代码
// app/dashboard/page.js
// 整个组件在服务端执行

import { db } from '@/lib/db';
import Sidebar from './Sidebar';      // Server Component
import WeatherWidget from './WeatherWidget';  // Client Component
import HeavyChart from './HeavyChart'; // Server Component

export default async function Dashboard() {
  // 直接服务端访问数据库,无需 API 层
  const user = await db.user.findUnique({
    where: { id: currentUserId }
  });

  const metrics = await db.metrics.findMany({
    where: { userId: user.id }
  });

  return (
    <div className="dashboard">
      {/* 1. 服务端组件:直接用数据渲染,无额外 JS */}
      <Sidebar user={user} />

      {/* 2. 客户端组件:需要交互的才水合 */}
      <WeatherWidget city={user.city} />

      {/* 3. 服务端组件:大数据图表,服务端生成 */}
      <HeavyChart data={metrics} />
    </div>
  );
}

// app/dashboard/Sidebar.js
// Server Component - 无需 'use client'
export default function Sidebar({ user }) {
  // 服务端直接渲染,不发送到客户端
  return (
    <aside>
      <h2>{user.name}</h2>
      <nav>{/* 导航链接 */}</nav>
    </aside>
  );
}

// app/dashboard/WeatherWidget.js
// Client Component - 需要 useState/useEffect
'use client';

import { useState, useEffect } from 'react';

export default function WeatherWidget({ city }) {
  const [weather, setWeather] = useState(null);

  useEffect(() => {
    // 客户端获取天气数据
    fetchWeather(city).then(setWeather);
  }, [city]);

  return <div>{weather?.temp}°C</div>;
}

详细解析

RSC 带来的性能提升

yaml 复制代码
Bundle Size 对比:

CSR 模式:
├─ React + ReactDOM: ~45KB
├─ 页面组件: ~200KB
└─ 第三方库: ~150KB
总计: ~395KB

RSC 模式 (App Router):
├─ React + ReactDOM: ~45KB (只水合交互部分)
├─ Server Components: ~0KB (不发送 JS)
├─ Client Components: ~50KB
└─ 第三方库: ~0KB (服务端使用)
总计: ~95KB (节省 76%)

Streaming SSR 实现

jsx 复制代码
// 服务端流式传输,非交互内容先显示
export default function Page() {
  return (
    <div>
      <header>Header</header>

      <Suspense fallback={<ArticleSkeleton />}>
        <Article /> {/* 可能较慢,但不会阻塞其他内容 */}
      </Suspense>

      <Suspense fallback={<CommentsSkeleton />}>
        <Comments />
      </Suspense>
    </div>
  );
}

使用场景选择

场景 推荐方案
静态内容、无交互 Server Component
表单、动画、事件 Client Component
SEO 关键页面 SSR + Server Component
实时数据 Client Component + SWR

3.3 Server Component

题目:请详细解释 React Server Components(RSC)的工作原理,以及 Server Component 与 Client Component 的边界如何划分。

参考答案

RSC 核心原理

xml 复制代码
┌─────────────────────────────────────────────────────────────┐
│                        服务端                                │
│  ┌─────────────────────────────────────────────────────┐    │
│  │  Server Component 树                               │    │
│  │                                                     │    │
│  │  <Page> ← RSC Server 组件                          │    │
│  │    <Sidebar> ← RSC (无 JS bundle)                  │    │
│  │    <InteractiveWidget> ← Client Boundary           │    │
│  │      <LikeButton> ← Client (需要水合)               │    │
│  │                                                     │    │
│  └─────────────────────────────────────────────────────┘    │
│                           ↓                                 │
│                    RSC Payload                              │
│              (特殊的 JSON 格式描述)                          │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│                        客户端                                │
│  ┌─────────────────────────────────────────────────────┐    │
│  │  Client 组件树 + 水合                                │    │
│  │                                                     │    │
│  │  <Page> (已渲染 HTML)                               │    │
│  │    <Sidebar> (纯 HTML,无组件)                      │    │
│  │    <InteractiveWidget> (水合后激活)                 │    │
│  │      <LikeButton> (客户端运行)                      │    │
│  │                                                     │    │
│  └─────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────┘

RSC Payload 格式示例

javascript 复制代码
// 服务端返回的 RSC Payload 是特殊格式
// 1. 服务端组件渲染结果
['$', 'div', null, {
  className: 'container'
}, ['$', 'h1', null, 'Hello World']]

// 2. 客户端组件引用
['$', 'div', null, {
  className: 'container'
}, ['$', 'LikeButton', '$L1', { initialCount: 0 }]]
// ↑ '$L1' 是 client reference,指向 client module

边界划分原则

jsx 复制代码
// app/
//   ├── page.js          ← Server Component (默认)
//   ├── layout.js        ← Server Component (默认)
//   └── components/
//       ├── A.js         ← Server Component
//       ├── B.js         ← Server Component
//       └── C.client.js  ← Client Component (约定后缀)

// 1. 需要 useState/useEffect → Client Component
'use client';

import { useState } from 'react';

export function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

// 2. 需要浏览器 API → Client Component
'use client';

export function ScrollToTop() {
  if (typeof window !== 'undefined') {
    window.scrollTo(0, 0);
  }
  return <button onClick={() => window.scrollTo(0, 0)}>Top</button>;
}

// 3. 需要第三方库 (可能需要客户端交互) → Client Component
'use client';

import { useEditor } from 'some-editor-lib';

export function Editor() {
  const editor = useEditor();
  return <div>{editor.content}</div>;
}

// 4. 大型库在服务端用结果 → Server Component
// 服务端处理数据,客户端只负责展示
async function Dashboard() {
  const data = await heavyComputation(); // 服务端执行
  return <HeavyDataVisualization data={data} />; // 客户端展示
}

详细解析

Server Component 的限制

jsx 复制代码
// Server Component 不能:
// 1. 使用 hooks
// 2. 使用浏览器 API
// 3. 使用事件监听
// 4. 使用第三方库的状态管理

// 但可以:
// 1. 直接访问数据库/文件系统
// 2. 使用服务端专属库(如 sharp 处理图片)
// 3. 访问环境变量(安全)
// 4. 大数据处理(不增加客户端负担)

最佳实践 - 数据获取

jsx 复制代码
// 在 Server Component 中直接获取数据
// 不需要 useEffect + API 调用
async function BlogList() {
  // 直接查询数据库(示例使用 Prisma)
  const posts = await prisma.post.findMany({
    where: { published: true },
    include: { author: true }
  });

  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>
          <h2>{post.title}</h2>
          <p>By {post.author.name}</p>
        </li>
      ))}
    </ul>
  );
}

组件组合模式

jsx 复制代码
// Server Component 作为入口
// 包裹需要交互的部分为 Client Component
async function ProductPage({ id }) {
  const product = await getProduct(id);

  return (
    <div>
      <ProductInfo product={product} />
      <AddToCart productId={id} initialStock={product.stock} />
      <Reviews productId={id} />
    </div>
  );
}

// AddToCart 需要交互 → Client Component
'use client';
function AddToCart({ productId, initialStock }) {
  const [quantity, setQuantity] = useState(1);
  return (
    <div>
      <input
        type="number"
        value={quantity}
        onChange={e => setQuantity(Number(e.target.value))}
      />
      <button onClick={() => addToCart(productId, quantity)}>
        Add to Cart
      </button>
    </div>
  );
}

3.4 streaming

题目:什么是 Streaming SSR?它如何提升用户体验?请在 Next.js App Router 中实现一个支持 streaming 的页面。

参考答案

Streaming 原理

less 复制代码
传统 SSR (阻塞):
┌────────────────────────────────────────────────┐
│█████████████████████████████等待数据████████████│
│                        ↓                        │
│  ┌──────────────────────────────────────────┐  │
│  │ 完整页面 HTML(首字节时间取决于最慢数据)    │  │
│  └──────────────────────────────────────────┘  │
└────────────────────────────────────────────────┘
           TTFB 慢,用户等待时间长

Streaming SSR (非阻塞):
┌────────────────────────────────────────────────┐
│   ┌────────────┐    ┌────────────────────────┐  │
│   │ 立即响应    │    │  后续内容逐步到达      │  │
│   │ Header/Nav │    │  Product Details...   │  │
│   │ (骨架)     │    │  Reviews...           │  │
│   └────────────┘    └────────────────────────┘  │
│   TTFB 快    ↑                                 │
│   可见内容   │                                 │
│   逐步呈现   ↓                                 │
└────────────────────────────────────────────────┘

实现代码

jsx 复制代码
// app/product/[id]/page.js
import { Suspense } from 'react';
import ProductHeader from '@/components/ProductHeader';
import ProductSkeleton from '@/components/ProductSkeleton';
import ReviewsSkeleton from '@/components/ReviewsSkeleton';

// 慢数据组件 - 模拟数据库查询延迟
async function ProductDetails({ id }) {
  await new Promise(resolve => setTimeout(resolve, 2000)); // 模拟延迟
  const product = await getProduct(id);
  return (
    <div className="product-details">
      <h2>{product.name}</h2>
      <p>{product.description}</p>
      <p className="price">${product.price}</p>
    </div>
  );
}

// 更慢的数据组件
async function ProductReviews({ id }) {
  await new Promise(resolve => setTimeout(resolve, 3000));
  const reviews = await getProductReviews(id);
  return (
    <div className="reviews">
      <h3>Customer Reviews</h3>
      {reviews.map(review => (
        <div key={review.id} className="review">
          <p>{review.content}</p>
          <span>⭐ {review.rating}</span>
        </div>
      ))}
    </div>
  );
}

// 页面组件 - 服务端异步组合
export default async function ProductPage({ params }) {
  return (
    <div className="product-page">
      {/* 1. 立即可用的 Header */}
      <ProductHeader productId={params.id} />

      {/* 2. 慢数据用 Suspense 包裹 */}
      <Suspense fallback={<ProductSkeleton />}>
        <ProductDetails id={params.id} />
      </Suspense>

      {/* 3. 更慢的 Reviews */}
      <Suspense fallback={<ReviewsSkeleton />}>
        <ProductReviews id={params.id} />
      </Suspense>
    </div>
  );
}

骨架屏组件

jsx 复制代码
// components/ProductSkeleton.jsx
export default function ProductSkeleton() {
  return (
    <div className="animate-pulse">
      <div className="h-8 bg-gray-200 rounded w-3/4 mb-4" />
      <div className="h-4 bg-gray-200 rounded w-full mb-2" />
      <div className="h-4 bg-gray-200 rounded w-5/6 mb-6" />
      <div className="h-6 bg-gray-200 rounded w-1/4" />
    </div>
  );
}

// components/ReviewsSkeleton.jsx
export default function ReviewsSkeleton() {
  return (
    <div className="animate-pulse">
      <div className="h-6 bg-gray-200 rounded w-1/3 mb-4" />
      {[1, 2, 3].map(i => (
        <div key={i} className="mb-4 p-4 border rounded">
          <div className="h-4 bg-gray-200 rounded w-full mb-2" />
          <div className="h-4 bg-gray-200 rounded w-2/3" />
        </div>
      ))}
    </div>
  );
}

详细解析

Streaming 适用场景

场景 效果
产品详情页 标题立即显示,详情/评论逐步加载
搜索结果 骨架屏先显示,内容流式加载
用户 Dashboard 顶部 KPI 立即显示,图表异步加载
新闻/文章页 文章内容先显示,评论/推荐后加载

use hook 实现 loading 状态

jsx 复制代码
// next/navigation 提供的 use hook
'use client';

import { useSearchParams } from 'next/navigation';
import { useEffect, useState } from 'react';

export function Pagination() {
  const searchParams = useSearchParams();
  const [page, setPage] = useState(1);

  // 配合 URL searchParams 使用
  useEffect(() => {
    setPage(Number(searchParams.get('page')) || 1);
  }, [searchParams]);

  return (
    <div className="pagination">
      <button disabled={page <= 1}>Previous</button>
      <span>Page {page}</span>
      <button>Next</button>
    </div>
  );
}

性能指标对比

yaml 复制代码
指标对比 (假设最慢数据需 3s):

传统 SSR:
├── TTFB: 3000ms (必须等所有数据)
├── FCP:  3000ms
└── TTI:  3500ms

Streaming SSR:
├── TTFB: 100ms (骨架立即返回)
├── FCP:  100ms
├── LCP:  100ms (骨架先展示)
├── TTI:  3200ms (交互部分最后加载)
└── 用户感知: "页面秒开,内容逐步填充" ✅

四、前端工程化 (T1)

4.1 webpack/vite

题目:Webpack 与 Vite 的核心区别是什么?在 2025-2026 年,如何根据项目场景选择构建工具?

参考答案

核心架构对比

vbscript 复制代码
Webpack:
┌─────────────────────────────────────────────────────────┐
│                    Build Time                           │
│                                                         │
│  ┌───────────┐    ┌───────────┐    ┌───────────┐      │
│  │  入口文件  │ →  │  递归依赖  │ →  │  打包构建  │      │
│  │ index.js  │    │  分析模块  │    │ 输出bundle│      │
│  └───────────┘    └───────────┘    └───────────┘      │
│                                                         │
│  开发时: webpack-dev-server 启动慢                      │
│  冷启动: 扫描所有依赖 → 打包 → 启动服务 (30s+)          │
│  热更新: 重新打包相关模块 → 推送更新 (1-5s)              │
└─────────────────────────────────────────────────────────┘

Vite:
┌─────────────────────────────────────────────────────────┐
│                    Dev Server                           │
│                                                         │
│  ┌───────────┐    ┌───────────┐    ┌───────────┐      │
│  │  HTTP     │    │  ESM      │    │  按需     │      │
│  │  Server   │ ←  │  Native   │ ←  │  编译     │      │
│  └───────────┘    └───────────┘    └───────────┘      │
│                                                         │
│  冷启动: 启动 HTTP 服务 (100ms)                         │
│  热更新: 模块级更新,无需打包 (毫秒级)                    │
└─────────────────────────────────────────────────────────┘

代码示例 - Vite 配置

javascript 复制代码
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';

export default defineConfig({
  plugins: [
    react({
      // Fast Refresh 配置
      fastRefresh: true,
    }),
  ],

  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },

  build: {
    target: 'esnext',
    // 分包策略
    rollupOptions: {
      output: {
        manualChunks: {
          'react-vendor': ['react', 'react-dom'],
          'vendor': ['lodash', 'axios'],
        },
      },
    },
  },

  // 开发服务器配置
  server: {
    port: 3000,
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
      },
    },
  },

  // 预构建依赖
  optimizeDeps: {
    include: ['react', 'react-dom', 'lodash'],
  },
});

选择指南

场景 推荐 原因
新项目 / 中小型 Vite 极速启动,开发体验好
大型复杂项目 Webpack 5 / Vite 根据团队熟悉度选择
微前端架构 Webpack 5 Module Federation 成熟
需要兼容旧浏览器 Webpack 5 babel-loader 生态完善
React 18 + Server Components Vite (新版) ESM native 支持好
企业内部 legacy 项目 Webpack 5 稳定性优先

详细解析

Vite 的依赖预构建

javascript 复制代码
// 第一次启动时,Vite 会:
// 1. 扫描 package.json 依赖
// 2. 使用 esbuild 预构建 CJS 依赖为 ESM
// 3. 生成 node_modules/.vite/deps/ 缓存

// 预构建优势:
// - 减少模块请求数量
// - 统一 ESM/CJS 差异
// - 提升热更新速度

生产构建优化

javascript 复制代码
// Vite 生产构建使用 Rollup
// 输出优化策略

// 1. CSS 代码分割
import('./dark.css', { rel: 'stylesheet' });

// 2. 动态导入 + 预加载
const { default: HeavyChart } = await import('./HeavyChart.js');
// Rollup 自动生成 <link rel="modulepreload">

// 3. Tree Shaking 优化
// 基于 ESM 的静态分析,未使用的代码自动剔除
import { usedFunction } from 'big-lib';
// → 只打包 usedFunction

热更新速度对比

markdown 复制代码
场景:修改一个组件的样式

Webpack:
  1. 重新编译该组件模块
  2. 重新编译依赖该组件的其他模块
  3. 重新生成 chunk
  4. 通过 HMR 推送更新
  → 耗时 200ms - 2s

Vite:
  1. 直接对改动的文件进行 HMR
  2. 通知浏览器更新
  → 耗时 < 50ms

4.2 monorepo

题目:什么是 Monorepo?为什么需要 Monorepo?请用 pnpm + Turborepo 搭建一个前端 Monorepo 项目结构。

参考答案

Monorepo vs Polyrepo

bash 复制代码
Polyrepo (多仓库):
┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐
│   web   │  │ mobile  │  │  admin  │  │ shared  │
│         │  │         │  │         │  │         │
│ @web-ui │  │ @mob-ui │  │ @adm-ui │  │ @ui-lib │
│  npm    │  │  npm    │  │  npm    │  │  npm    │
└─────────┘  └─────────┘  └─────────┘  └─────────┘
问题:版本同步难、重复代码、跨仓库修改繁琐

Monorepo (单仓库):
┌─────────────────────────────────────────┐
│              root (pnpm workspace)       │
│  ┌─────────┐  ┌─────────┐  ┌─────────┐  │
│  │  apps   │  │ packages│  │  config │  │
│  │  /web   │  │ /ui-lib │  │ /eslint │  │
│  │  /admin │  │/utils   │  │/tsconf  │  │
│  └─────────┘  └─────────┘  └─────────┘  │
│                                          │
│  优势:原子提交、共享配置、一站式构建      │
└─────────────────────────────────────────┘

项目结构搭建

bash 复制代码
# 目录结构
my-monorepo/
├── pnpm-workspace.yaml      # pnpm 工作区配置
├── package.json             # 根目录 package
├── turbo.json               # Turborepo 配置
├── .npmrc                    # pnpm 配置
│
├── apps/
│   ├── web/                 # React Web 应用
│   │   ├── package.json
│   │   ├── src/
│   │   └── vite.config.ts
│   │
│   └── admin/               # Admin 管理后台
│       ├── package.json
│       └── src/
│
└── packages/
    ├── ui/                  # UI 组件库
    │   ├── package.json
    │   ├── src/
    │   │   ├── Button/
    │   │   ├── Input/
    │   │   └── index.ts
    │   └── tsconfig.json
    │
    ├── utils/               # 工具函数
    │   ├── package.json
    │   └── src/
    │
    ├── config/              # 共享配置
    │   ├── eslint/          # ESLint 配置
    │   └── tsconfig/        # TS 基础配置

配置文件

yaml 复制代码
# pnpm-workspace.yaml
packages:
  - 'apps/*'
  - 'packages/*'
json 复制代码
// turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "globalDependencies": ["**/.env.*local"],
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**", "build/**"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "lint": {
      "dependsOn": ["^build"]
    },
    "test": {
      "dependsOn": ["build"],
      "outputs": ["coverage/**"]
    }
  }
}
bash 复制代码
# .npmrc
shamefully-hoist=true
strict-peer-dependencies=false
auto-install-peers=true
json 复制代码
// packages/ui/package.json
{
  "name": "@my-monorepo/ui",
  "version": "1.0.0",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": "./dist/index.js",
    "./Button": "./dist/Button/index.js"
  },
  "scripts": {
    "build": "tsup src/index.ts --format esm cjs --dts",
    "dev": "tsup src/index.ts --format esm cjs --dts --watch"
  }
}
javascript 复制代码
// apps/web/src/App.tsx
import { Button, Input } from '@my-monorepo/ui';
import { formatDate } from '@my-monorepo/utils';

function App() {
  return (
    <div>
      <Button>Click me</Button>
      <Input placeholder="Enter date" />
      <p>{formatDate(new Date())}</p>
    </div>
  );
}

详细解析

pnpm vs yarn/npm 优势

bash 复制代码
# 磁盘空间优化
pnpm: 硬链接 + 符号链接 → 共享同一依赖文件
# 项目A和项目B都用react → 只存储一份react文件

# 依赖隔离
pnpm: 每个项目有自己的 node_modules/.pnpm
# 避免幽灵依赖问题

Turborepo 任务调度

bash 复制代码
# 安装
pnpm add turbo -D -w

# 运行构建(自动分析依赖关系,按序执行)
pnpm turbo build

# Turborepo 自动:
# 1. 分析 packages 间的依赖关系
# 2. 构建依赖树
# 3. 并行执行无依赖任务
# 4. 缓存构建结果

Monorepo 适用场景

场景 是否 Monorepo
多应用共享组件/工具 ✅ 强烈推荐
微前端架构 ✅ 适合
全栈应用 (FE + BE) ✅ 可选
单个小应用 ❌ 过度工程
完全独立的多个产品 ❌ 维护成本高

4.3 微前端

题目:什么是微前端?它解决了什么问题?请实现一个基于 iframe 的简单微前端架构,并说明其他微前端方案的适用场景。

参考答案

微前端解决的问题

makefile 复制代码
巨石应用问题:
┌─────────────────────────────────────────────────┐
│                  monolith-app                    │
│  ┌─────────────────────────────────────────┐   │
│  │ 10万+ 行代码                             │   │
│  │ 技术栈混乱 (jQuery + Vue 2 + React 18)   │   │
│  │ 团队耦合 → 发布需要协调所有人             │   │
│  │ 单点故障 → 一个bug影响整个系统            │   │
│  │ 本地开发启动慢 (5分钟+)                   │   │
│  └─────────────────────────────────────────┘   │
└─────────────────────────────────────────────────┘

微前端解决:
┌─────────────────────────────────────────────────┐
│                   主应用容器                      │
│  ┌──────────┐ ┌──────────┐ ┌──────────┐        │
│  │  营销模块 │ │  订单模块 │ │  用户模块 │        │
│  │  (Vue 2) │ │ (React)  │ │ (Vue 3)  │        │
│  └──────────┘ └──────────┘ └──────────┘        │
│                                                    │
│  ✅ 独立开发  ✅ 独立部署  ✅ 技术异构            │
└─────────────────────────────────────────────────┘

基于 iframe 的微前端实现

html 复制代码
<!-- 主应用: index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>微前端主应用</title>
  <style>
    .micro-container {
      border: 1px solid #ddd;
      padding: 20px;
      margin: 10px 0;
    }
    .nav-link {
      margin: 10px;
      padding: 5px 10px;
      cursor: pointer;
      background: #f0f0f0;
      border-radius: 4px;
    }
    .active {
      background: #007bff;
      color: white;
    }
  </style>
</head>
<body>
  <h1>微前端主应用</h1>

  <nav>
    <span class="nav-link active" data-app="vue-app">Vue 子应用</span>
    <span class="nav-link" data-app="react-app">React 子应用</span>
  </nav>

  <div id="micro-content">
    <!-- 子应用挂载点 -->
  </div>

  <script>
    // 微前端管理器
    class MicroFrontend {
      constructor() {
        this.apps = {
          'vue-app': {
            url: 'http://localhost:3001/index.html',
            container: document.getElementById('vue-container')
          },
          'react-app': {
            url: 'http://localhost:3002/index.html',
            container: document.getElementById('react-container')
          }
        };
        this.currentApp = null;
      }

      // 挂载子应用
      mount(appName) {
        if (this.currentApp === appName) return;

        const content = document.getElementById('micro-content');
        const config = this.apps[appName];

        // 清除当前内容
        content.innerHTML = '';

        // 创建 iframe
        const iframe = document.createElement('iframe');
        iframe.src = config.url;
        iframe.style.width = '100%';
        iframe.style.height = '500px';
        iframe.style.border = 'none';

        content.appendChild(iframe);
        this.currentApp = appName;

        // 更新导航高亮
        document.querySelectorAll('.nav-link').forEach(el => {
          el.classList.toggle('active', el.dataset.app === appName);
        });
      }

      // 跨应用通信
      postMessage(appName, action, data) {
        const config = this.apps[appName];
        if (config && config.iframe) {
          config.iframe.contentWindow.postMessage({ action, data }, '*');
        }
      }
    }

    const micro = new MicroFrontend();

    // 导航切换
    document.querySelectorAll('.nav-link').forEach(el => {
      el.addEventListener('click', () => {
        micro.mount(el.dataset.app);
      });
    });

    // 默认加载第一个应用
    micro.mount('vue-app');
  </script>
</body>
</html>

通信机制

javascript 复制代码
// 子应用通信脚本: communication.js

// 监听来自主应用的消息
window.addEventListener('message', (event) => {
  const { action, data } = event.data;

  switch (action) {
    case 'user-login':
      handleUserLogin(data);
      break;
    case 'navigate':
      router.push(data.path);
      break;
    case 'get-user-info':
      // 回复主应用
      event.source.postMessage({
        type: 'user-info-response',
        data: getCurrentUser()
      }, '*');
      break;
  }
});

// 向主应用发送消息
function notifyMain(action, data) {
  window.parent.postMessage({ action, data }, '*');
}

详细解析

微前端方案对比

方案 原理 优点 缺点 适用场景
iframe 独立浏览器上下文 隔离性强、实现简单 性能差、样式冲突 简单场景、 legacy 迁移
qiankun JS 沙箱 + HTML Entry 功能完整、社区活跃 包体积大 中大型项目
single-spa 生命周期管理 灵活、轻量 配置复杂 定制化需求
Module Federation Webpack 5 远程模块 原生、性能好 配置复杂 新项目
EMP 微模块化 滴滴开源 生态较小 特定场景

Module Federation 示例(推荐新项目):

javascript 复制代码
// host/webpack.config.js
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'host',
      remotes: {
        // 引用远程子应用
        vueApp: 'vueApp@http://localhost:3001/remoteEntry.js',
        reactApp: 'reactApp@http://localhost:3002/remoteEntry.js',
      },
      shared: ['react', 'react-dom', 'vue'],
    }),
  ],
};

// 子应用 webpack.config.js (Vue App)
new ModuleFederationPlugin({
  name: 'vueApp',
  filename: 'remoteEntry.js',  // 暴露给宿主
  exposes: {
    './App': './src/App.vue',  // 暴露的模块
  },
  shared: ['vue'],
});

4.4 BFF

题目:什么是 BFF(Backend For Frontend)?它与网关层有什么区别?请说明 BFF 在前端架构中的价值,并给出 NestJS 实现 BFF 的示例。

参考答案

BFF 架构定位

bash 复制代码
┌─────────────────────────────────────────────────────────────┐
│                         客户端                               │
│                    (Web / Mobile / App)                      │
└──────────────────────────┬──────────────────────────────────┘
                           │ HTTP/gRPC
┌──────────────────────────▼──────────────────────────────────┐
│                        BFF 层                                │
│  ┌────────────────┐  ┌────────────────┐  ┌────────────────┐ │
│  │  Web BFF       │  │ Mobile BFF     │  │ App BFF        │ │
│  │  /bff/web/*    │  │ /bff/mobile/*  │  │ /bff/app/*     │ │
│  └───────┬────────┘  └───────┬────────┘  └───────┬────────┘ │
└──────────┼──────────────────┼──────────────────┼──────────┘
           │ 聚合/裁剪          │ 聚合/裁剪          │ 聚合/裁剪
┌──────────▼──────────────────▼──────────────────▼──────────┐
│                       Gateway / API Gateway                  │
│                  (统一入口、鉴权、限流、路由)                  │
└──────────────────────────┬──────────────────────────────────┘
                           │ 微服务调用
┌──────────────────────────▼──────────────────────────────────┐
│                    微服务层                                    │
│  ┌────────┐  ┌────────┐  ┌────────┐  ┌────────┐            │
│  │ User   │  │ Product│  │ Order  │  │ Payment│            │
│  │ Service│  │ Service│  │ Service│  │ Service│            │
│  └────────┘  └────────┘  └────────┘  └────────┘            │
└─────────────────────────────────────────────────────────────┘

BFF vs API Gateway

维度 API Gateway BFF
层级 网关层 聚合层
职责 统一鉴权、限流、路由 业务聚合、数据裁剪
粒度 请求级别 接口级别
定制 通用配置 按端定制
位置 更靠近入口 靠近后端服务

NestJS BFF 实现

typescript 复制代码
// bff-web/src/modules/product-bff/product-bff.controller.ts
import { Controller, Get, Param, Query } from '@nestjs/common';
import { ApiTags, ApiOperation } from '@nestjs/swagger';
import { ProductService } from '@/services/product.service';
import { ReviewService } from '@/services/review.service';
import { InventoryService } from '@/services/inventory.service';

@ApiTags('BFF - Web 产品接口')
@Controller('bff/web/products')
export class ProductBffController {
  constructor(
    private readonly productService: ProductService,
    private readonly reviewService: ReviewService,
    private readonly inventoryService: InventoryService,
  ) {}

  /**
   * Web 端产品详情聚合接口
   * 一次性返回:产品信息 + 评分统计 + 库存状态 + 推荐商品
   */
  @Get(':id')
  @ApiOperation({ summary: 'Web 产品详情聚合' })
  async getProductDetail(@Param('id') id: string) {
    // 并行请求多个服务
    const [product, reviews, inventory, recommendations] = await Promise.all([
      this.productService.findById(id),
      this.reviewService.getStats(id),
      this.inventoryService.check(id),
      this.productService.getRecommendations(id, 6),
    ]);

    // 按 Web 端需求聚合数据
    return {
      // 产品基础信息
      info: {
        id: product.id,
        name: product.name,
        price: product.price,
        images: product.images.slice(0, 5), // 只返回5张图
        description: product.description,
      },

      // 评分聚合
      rating: {
        average: reviews.average,
        total: reviews.total,
        distribution: reviews.distribution,
      },

      // 库存状态(Web 端需要详细信息)
      stock: {
        available: inventory.available,
        quantity: inventory.quantity,
        status: inventory.status, // in_stock / low_stock / out_of_stock
        estimatedRestock: inventory.estimatedRestock,
      },

      // 简短推荐(移动端可能不需要)
      recommendations: recommendations.map(p => ({
        id: p.id,
        name: p.name,
        price: p.price,
        thumbnail: p.thumbnail,
      })),
    };
  }
}
typescript 复制代码
// bff-web/src/services/product.service.ts
import { Injectable, HttpException } from '@nestjs/common';
import { ProductGrpcClient } from '@/grpc/product.client';

@Injectable()
export class ProductService {
  constructor(private readonly grpcClient: ProductGrpcClient) {}

  async findById(id: string) {
    try {
      return await this.grpcClient.product.getProduct({ id });
    } catch (error) {
      throw new HttpException('产品不存在', 404);
    }
  }

  async getRecommendations(id: string, limit: number) {
    // 调用推荐服务
    const response = await this.grpcClient.recommend.getRecommendations({
      productId: id,
      limit,
    });
    return response.products;
  }
}

详细解析

BFF 的核心价值

javascript 复制代码
// 1. 数据聚合 - 减少客户端请求
// 无 BFF:客户端需要发 5 个请求
const [product, reviews, inventory, cart, user] = await Promise.all([
  fetch('/api/products/1'),
  fetch('/api/products/1/reviews/stats'),
  fetch('/api/inventory/1'),
  fetch('/api/cart'),
  fetch('/api/user/profile'),
]);

// 有 BFF:只需 1 个请求
const data = await fetch('/bff/web/products/1');

// 2. 数据裁剪 - 减少传输量
// Web 端需要完整信息
// Mobile 端可能只需要精简数据
// BFF 可以按需返回

// 3. 协议转换
// 内部 gRPC,外部 REST
// BFF 做协议转换

// 4. 业务逻辑适配
// Web 端和 Mobile 端的业务逻辑可能不同
// BFF 分别处理

SSR BFF 场景

typescript 复制代码
// Next.js API Routes 作为 BFF
// app/api/bff/product/[id]/route.ts

import { NextResponse } from 'next/server';

export async function GET(request: Request, { params }) {
  const { id } = params;

  // 服务端 BFF - 直接访问后端服务
  const [product, reviews] = await Promise.all([
    fetch(`${INTERNAL_API}/products/${id}`).then(r => r.json()),
    fetch(`${INTERNAL_API}/reviews/product/${id}/stats`).then(r => r.json()),
  ]);

  // 返回适合 SSR 的数据
  return NextResponse.json({
    ...product,
    reviewStats: reviews,
    // SEO 相关元数据
    meta: {
      title: product.seoTitle || product.name,
      description: product.seoDescription || product.description.slice(0, 160),
    },
  });
}

五、前端性能优化 (T1)

5.1 首屏优化

题目:如何优化首屏加载性能?请从加载、解析、执行三个阶段详细说明,并给出具体可落地的优化方案。

参考答案

首屏渲染时间线

swift 复制代码
┌─────────────────────────────────────────────────────────────────┐
│                      首屏优化三阶段                               │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  1️⃣ 加载阶段                                                     │
│  ├─ DNS 解析 → TCP 连接 → TLS 握手 → HTTP 请求                   │
│  ├─ 优化: DNS 预解析 / 减少请求数 / CDN 加速                      │
│                                                                  │
│  2️⃣ 解析阶段                                                     │
│  ├─ HTML 解析 → 构建 DOM → CSS 解析 → 构建 CSSOM                 │
│  ├─ 优化: defer/async 脚本 / CSS 精简                             │
│                                                                  │
│  3️⃣ 执行阶段                                                     │
│  ├─ JS 执行 → 渲染树合并 → Layout → Paint → Composite            │
│  ├─ 优化: 代码分割 / 懒加载 / Tree Shaking                        │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

具体优化方案

html 复制代码
<!-- 1. DNS 预解析 + 预连接 -->
<link rel="dns-prefetch" href="//static.example.com">
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin>

<!-- 2. 关键 CSS 内联,非关键 CSS 异步加载 -->
<style>
  /* 关键渲染路径 CSS */
  .critical-header { background: #333; color: #fff; }
</style>
<link rel="preload" href="/styles/non-critical.css" as="style"
      onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/styles/non-critical.css"></noscript>

<!-- 3. 脚本加载策略 -->
<!-- defer: 解析完 HTML 后执行,不阻塞渲染 -->
<script src="/app.js" defer></script>

<!-- async: 下载完立即执行,不保证顺序 -->
<script src="/analytics.js" async></script>

<!-- 模块脚本 (推荐) -->
<script type="module" src="/app.mjs"></script>
javascript 复制代码
// 4. 资源预加载
// 预加载关键资源
<link rel="preload" href="/fonts/main.woff2" as="font" crossorigin>
<link rel="preload" href="/images/hero.webp" as="image">

// 预加载路由组件
const router = createBrowserRouter([
  {
    path: '/',
    element: <App />,
    children: [
      {
        path: 'dashboard',
        // 预加载 dashboard 组件
        lazy: () => import('./pages/Dashboard'),
      },
    ],
  },
]);
javascript 复制代码
// 5. 路由级代码分割 (React + Vite)
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));

function App() {
  return (
    <Suspense fallback={<PageSkeleton />}>
      <Routes>
        <Route path="/" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Suspense>
  );
}
css 复制代码
/* 6. CSS 优化 */
.critical-path {
  /* 避免通配符 */
  /* 避免深层嵌套 */
  /* 使用 CSS 变量复用 */
  --primary-color: #007bff;
  color: var(--primary-color);
}
javascript 复制代码
// 7. 图片优化
// WebP 格式 + 响应式图片
<img
  src="/images/hero.webp"
  srcset="
    /images/hero-320.webp 320w,
    /images/hero-640.webp 640w,
    /images/hero-1280.webp 1280w
  "
  sizes="(max-width: 640px) 100vw, 50vw"
  loading="lazy"  /* 懒加载 */
  decoding="async"
  alt="Hero image"
/>

详细解析

FCP / LCP 优化

javascript 复制代码
// 监控关键指标
import { onFCP, onLCP, onCLS } from 'web-vitals';

onFCP((metric) => {
  console.log('FCP:', metric.value);
  // FCP > 1.8s → 需要优化
});

onLCP((metric) => {
  console.log('LCP:', metric.value, metric.entries);
  // LCP 通常是最大图片或标题
  // 检查: LCP 元素是否在首屏、是否延迟加载
});

// 常见 LCP 优化
// 1. 预加载 LCP 图片
<link rel="preload" as="image" href="/hero.webp">

// 2. 确保 LCP 图片使用现代格式
<img src="/hero.avif" loading="eager" fetchpriority="high">

服务端优化

javascript 复制代码
// SSR + 流式传输
// Next.js App Router
export default async function Page() {
  return (
    <div>
      <Header />  {/* 立即渲染 */}
      <Suspense fallback={<Loading />}>
        <HeavyContent />  {/* 流式加载 */}
      </Suspense>
    </div>
  );
}

// HTTP 缓存策略
// next.config.js
module.exports = {
  headers: [
    {
      source: '/static/:path*',
      headers: [
        {
          key: 'Cache-Control',
          value: 'public, max-age=31536000, immutable',
        },
      ],
    },
  ],
};

5.2 长列表优化

题目:如何优化渲染大量数据的列表(长列表)?请对比不同的优化方案,并实现一个高性能的虚拟滚动组件。

参考答案

长列表优化方案对比

方案 原理 适用场景 性能
分页 限制每页数量 数据量固定 ⭐⭐⭐⭐⭐
懒加载 滚动到底部加载 无限滚动 ⭐⭐⭐⭐
虚拟滚动 只渲染可见区域 超大数据集 ⭐⭐⭐⭐⭐
时间分片 每帧渲染部分 中等数据集 ⭐⭐⭐
懒渲染 只渲染可视区域 图片为主的列表 ⭐⭐⭐

虚拟滚动实现

tsx 复制代码
// VirtualList.tsx - 基于 React 的虚拟滚动组件
import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react';

interface VirtualListProps<T> {
  items: T[];
  height: number;                    // 列表容器高度
  itemHeight: number;                 // 每项固定高度
  overscan?: number;                 // 预渲染区域大小
  renderItem: (item: T, index: number) => React.ReactNode;
}

export function VirtualList<T>({
  items,
  height,
  itemHeight,
  overscan = 3,
  renderItem,
}: VirtualListProps<T>) {
  const containerRef = useRef<HTMLDivElement>(null);
  const [scrollTop, setScrollTop] = useState(0);

  // 计算可见范围
  const visibleRange = useMemo(() => {
    const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);
    const endIndex = Math.min(
      items.length - 1,
      Math.ceil((scrollTop + height) / itemHeight) + overscan
    );
    return { startIndex, endIndex };
  }, [scrollTop, height, itemHeight, items.length, overscan]);

  // 可见项
  const visibleItems = useMemo(() => {
    const { startIndex, endIndex } = visibleRange;
    return items.slice(startIndex, endIndex + 1).map((item, i) => ({
      item,
      index: startIndex + i,
    }));
  }, [items, visibleRange]);

  // 滚动处理
  const handleScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
    setScrollTop(e.currentTarget.scrollTop);
  }, []);

  // 总高度占位
  const totalHeight = items.length * itemHeight;

  // 偏移量(使可见区域对齐)
  const offsetY = visibleRange.startIndex * itemHeight;

  return (
    <div
      ref={containerRef}
      style={{
        height: `${height}px`,
        overflow: 'auto',
        position: 'relative',
      }}
      onScroll={handleScroll}
    >
      {/* 内容占位器 */}
      <div style={{ height: `${totalHeight}px`, position: 'relative' }}>
        {/* 可见内容 */}
        <div
          style={{
            position: 'absolute',
            top: 0,
            left: 0,
            right: 0,
            transform: `translateY(${offsetY}px)`,
          }}
        >
          {visibleItems.map(({ item, index }) => (
            <div key={index} style={{ height: `${itemHeight}px` }}>
              {renderItem(item, index)}
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

// 使用示例
function UserList() {
  // 模拟 100000 条数据
  const users = useMemo(() =>
    Array.from({ length: 100000 }, (_, i) => ({
      id: i,
      name: `User ${i}`,
      email: `user${i}@example.com`,
    })), []);

  return (
    <VirtualList
      items={users}
      height={600}
      itemHeight={50}
      overscan={5}
      renderItem={(user) => (
        <div style={{ padding: '10px', borderBottom: '1px solid #eee' }}>
          <strong>{user.name}</strong>
          <span style={{ marginLeft: '10px', color: '#666' }}>{user.email}</span>
        </div>
      )}
    />
  );
}

进阶:动态高度虚拟滚动

tsx 复制代码
// 使用 ResizeObserver 测量动态高度
export function DynamicVirtualList({ items, renderItem }) {
  const containerRef = useRef<HTMLDivElement>(null);
  const [scrollTop, setScrollTop] = useState(0);
  const [itemHeights, setItemHeights] = useState<number[]>([]);
  const itemRefs = useRef<Map<number, HTMLDivElement>>(new Map());

  // 计算每个元素的位置偏移
  const itemOffsets = useMemo(() => {
    const offsets = [0];
    itemHeights.forEach((h, i) => {
      offsets.push(offsets[i] + (h || 60)); // 默认 60px
    });
    return offsets;
  }, [itemHeights]);

  // 二分查找找到起始索引
  const startIndex = useMemo(() => {
    let low = 0, high = itemOffsets.length - 1;
    while (low < high) {
      const mid = Math.floor((low + high) / 2);
      if (itemOffsets[mid] < scrollTop) low = mid + 1;
      else high = mid;
    }
    return Math.max(0, low - 1);
  }, [scrollTop, itemOffsets]);

  // 测量单个元素高度
  const measureItem = useCallback((index: number, element: HTMLDivElement | null) => {
    if (element) {
      itemRefs.current.set(index, element);
      setItemHeights(prev => {
        const newHeights = [...prev];
        newHeights[index] = element.offsetHeight;
        return newHeights;
      });
    }
  }, []);

  return (
    <div
      ref={containerRef}
      style={{ height: '600px', overflow: 'auto' }}
      onScroll={e => setScrollTop(e.currentTarget.scrollTop)}
    >
      <div style={{ height: itemOffsets[itemOffsets.length - 1] }}>
        {items.slice(startIndex, startIndex + 10).map((item, i) => (
          <div
            key={startIndex + i}
            ref={el => measureItem(startIndex + i, el)}
          >
            {renderItem(item, startIndex + i)}
          </div>
        ))}
      </div>
    </div>
  );
}

详细解析

React 官方虚拟列表库

tsx 复制代码
// @tanstack/react-virtual (推荐)
import { useVirtualizer } from '@tanstack/react-virtual';

function VirtualizedList({ items }) {
  const parentRef = useRef(null);

  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 50, // 估算高度
    overscan: 5,             // 预渲染数量
  });

  return (
    <div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
      <div style={{ height: virtualizer.getTotalSize() }}>
        {virtualizer.getVirtualItems().map((virtualItem) => (
          <div
            key={virtualItem.key}
            style={{
              position: 'absolute',
              top: virtualItem.start,
              width: '100%',
            }}
          >
            {items[virtualItem.index].content}
          </div>
        ))}
      </div>
    </div>
  );
}

性能指标对比

makefile 复制代码
渲染 10000 条数据:

普通列表:
├── DOM 节点数: 10000+
├── 初始渲染: ~2000ms
├── 滚动帧率: <30fps
└── 内存占用: 高

虚拟滚动:
├── DOM 节点数: ~20-30 (可见区域 + overscan)
├── 初始渲染: ~50ms
├── 滚动帧率: 60fps
└── 内存占用: 低

5.3 虚拟滚动

题目:请详细解释虚拟滚动的实现原理,并实现一个支持动态加载更多数据的无限滚动虚拟列表。

参考答案

虚拟滚动核心原理

scss 复制代码
┌─────────────────────────────────────────────────────────────┐
│                    虚拟滚动原理                               │
│                                                              │
│  ┌─────────────────────────────────────────────────────┐    │
│  │                   滚动容器 (600px)                    │    │
│  │  ┌───────────────────────────────────────────────┐  │    │
│  │  │         可见区域 (只渲染可见项)                  │  │    │
│  │  │  ┌─────────────────────────────────────────┐  │  │    │
│  │  │  │  Item 5                                 │  │  │    │
│  │  │  │  Item 6                                 │  │  │    │
│  │  │  │  Item 7  ← 渲染                         │  │  │    │
│  │  │  │  Item 8                                 │  │  │    │
│  │  │  │  Item 9                                 │  │  │    │
│  │  │  └─────────────────────────────────────────┘  │  │    │
│  │  └───────────────────────────────────────────────┘  │    │
│  │                                                      │    │
│  │  ⬆ Item 0-4 (不可见,不渲染)                        │    │
│  │  ⬇ Item 10-9999 (不可见,不渲染)                    │    │
│  └─────────────────────────────────────────────────────┘    │
│                                                              │
│  关键: 通过 transform: translateY() 定位可见区域              │
└─────────────────────────────────────────────────────────────┘

无限滚动虚拟列表实现

tsx 复制代码
// InfiniteVirtualList.tsx
import React, { useState, useEffect, useRef, useCallback } from 'react';

interface InfiniteVirtualListProps<T> {
  fetchMore: (page: number) => Promise<T[]>;  // 加载更多数据的函数
  hasMore: boolean;                             // 是否还有更多数据
  itemHeight: number;                           // 固定行高
  containerHeight: number;                     // 容器高度
  renderItem: (item: T, index: number) => React.ReactNode;
  loadingComponent?: React.ReactNode;            // 加载中组件
}

export function InfiniteVirtualList<T extends { id: string | number }>({
  fetchMore,
  hasMore,
  itemHeight,
  containerHeight,
  renderItem,
  loadingComponent,
}: InfiniteVirtualListProps<T>) {
  const [items, setItems] = useState<T[]>([]);
  const [scrollTop, setScrollTop] = useState(0);
  const [isLoading, setIsLoading] = useState(false);
  const [page, setPage] = useState(1);
  const containerRef = useRef<HTMLDivElement>(null);
  const loadingRef = useRef(false);

  // 加载初始数据和更多数据
  const loadMore = useCallback(async () => {
    if (loadingRef.current || !hasMore) return;

    loadingRef.current = true;
    setIsLoading(true);

    try {
      const newItems = await fetchMore(page);
      setItems(prev => [...prev, ...newItems]);
      setPage(p => p + 1);
    } finally {
      loadingRef.current = false;
      setIsLoading(false);
    }
  }, [fetchMore, page, hasMore]);

  // 初始加载
  useEffect(() => {
    loadMore();
  }, []);

  // 计算可见范围
  const visibleRange = {
    startIndex: Math.max(0, Math.floor(scrollTop / itemHeight) - 5),
    endIndex: Math.ceil((scrollTop + containerHeight) / itemHeight) + 5,
  };

  // 只渲染可见项
  const visibleItems = items
    .slice(visibleRange.startIndex, visibleRange.endIndex)
    .map((item, i) => ({
      item,
      index: visibleRange.startIndex + i,
    }));

  // 处理滚动
  const handleScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
    const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
    setScrollTop(scrollTop);

    // 检测是否滚动到底部
    if (scrollHeight - scrollTop - clientHeight < 200) {
      loadMore();
    }
  }, [loadMore]);

  // 总内容高度
  const totalHeight = items.length * itemHeight;
  const offsetY = visibleRange.startIndex * itemHeight;

  return (
    <div
      ref={containerRef}
      style={{ height: `${containerHeight}px`, overflow: 'auto' }}
      onScroll={handleScroll}
    >
      <div style={{ height: `${totalHeight}px`, position: 'relative' }}>
        <div
          style={{
            position: 'absolute',
            top: 0,
            left: 0,
            right: 0,
            transform: `translateY(${offsetY}px)`,
          }}
        >
          {visibleItems.map(({ item, index }) => (
            <div
              key={item.id}
              style={{ height: `${itemHeight}px` }}
            >
              {renderItem(item, index)}
            </div>
          ))}

          {/* 加载中状态 */}
          {isLoading && (
            <div style={{ height: `${itemHeight}px`, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
              {loadingComponent || <span>加载中...</span>}
            </div>
          )}

          {/* 没有更多数据 */}
          {!hasMore && items.length > 0 && (
            <div style={{ height: `${itemHeight}px`, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#999' }}>
              已加载全部
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

使用示例

tsx 复制代码
// Feed 流组件
function FeedList() {
  const [hasMore, setHasMore] = useState(true);

  const fetchMoreFeeds = async (page: number) => {
    const response = await fetch(`/api/feeds?page=${page}&limit=20`);
    const data = await response.json();

    if (data.feeds.length === 0) {
      setHasMore(false);
      return [];
    }

    return data.feeds;
  };

  return (
    <InfiniteVirtualList
      fetchMore={fetchMoreFeeds}
      hasMore={hasMore}
      itemHeight={120}
      containerHeight={window.innerHeight - 64}
      renderItem={(feed, index) => (
        <FeedCard feed={feed} />
      )}
      loadingComponent={<Spin />}
    />
  );
}

详细解析

性能优化技巧

tsx 复制代码
// 1. 使用 React.memo 避免不必要的重渲染
const VirtualItem = React.memo(({ item, index, style }) => (
  <div style={style}>
    <ItemContent data={item} />
  </div>
), (prevProps, nextProps) => {
  return prevProps.item.id === nextProps.item.id;
});

// 2. 使用 CSS contain 隔离重排
<div style={{ contain: 'strict' }}>
  {/* 列表项内容 */}
</div>;

// 3. 使用 will-change 提示浏览器
<div style={{ willChange: 'transform' }}>
  {/* 内容 */}
</div>;

注意事项

markdown 复制代码
1. 固定高度 vs 动态高度
   - 固定高度:性能最优,O(1) 定位
   - 动态高度:需要维护偏移量数组,O(log n) 定位

2. 滚动位置重置
   - 加载新数据时,如需保持滚动位置
   - 记录 scrollTop,计算新的偏移量

3. 内存泄漏
   - 清理 useEffect 中的事件监听
   - 使用 useRef 保存最新的 items

4. 键盘可访问性
   - 虚拟列表默认不可被屏幕阅读器遍历
   - 需配合 ARIA 实现可访问性

5.4 tree shaking

题目:什么是 Tree Shaking?它是如何工作的?请说明如何正确编写代码以确保 Tree Shaking 生效,并解释 ES Module 与 CommonJS 的区别。

参考答案

Tree Shaking 原理

javascript 复制代码
┌─────────────────────────────────────────────────────────────┐
│                      Tree Shaking                           │
│                                                              │
│  输入:                                                       │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ import { a, b } from 'utils';                        │   │
│  │                                                        │   │
│  │ export function a() { return 'a'; }  ← 使用          │   │
│  │ export function b() { return 'b'; }  ← 使用          │   │
│  │ export function c() { return 'c'; }  ← 未使用 → 删除  │   │
│  │ export function d() { return 'd'; }  ← 未使用 → 删除  │   │
│  └─────────────────────────────────────────────────────┘   │
│                          ↓                                   │
│                      静态分析                                 │
│                          ↓                                   │
│  输出:                                                       │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ function a() { return 'a'; }  ← 保留                 │   │
│  │ function b() { return 'b'; }  ← 保留                 │   │
│  │ // c 和 d 被移除                                      │   │
│  └─────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘

ES Module vs CommonJS

特性 ES Module CommonJS
语法 import/export require/module.exports
加载 静态,编译时确定 动态,运行时确定
绑定 值引用 值拷贝
Tree Shaking ✅ 支持 ❌ 不支持
循环引用 绑定延迟 可能出问题
javascript 复制代码
// ES Module - 静态结构,允许 Tree Shaking
import { flatten } from 'lodash';  // 可分析,未使用的排除
export const add = (a, b) => a + b;

// CommonJS - 动态结构,无法 Tree Shaking
const _ = require('lodash');  // 整个模块导入
module.exports = { add };     // 整个导出

Tree Shaking 正确姿势

javascript 复制代码
// ❌ 错误:使用默认导出,整个模块被打包
import lodash from 'lodash';
lodash.flatten([1, [2, 3]]);

// ✅ 正确:使用命名导出
import { flatten } from 'lodash-es';  // lodash-es 支持 ESM
flatten([1, [2, 3]]);

// ✅ 正确:路径导入
import flatten from 'lodash/flatten';  // 需要 lodash 版本 >= 4

// ✅ 正确:使用 @非常好用
import { flatten } from 'es-toolkit';  // 专为 Tree Shaking 设计

Vite/Webpack 配置

javascript 复制代码
// vite.config.js
export default defineConfig({
  build: {
    target: 'esnext',
    minify: 'esbuild',  // esbuild 原生支持 Tree Shaking
    rollupOptions: {
      output: {
        manualChunks(id) {
          if (id.includes('node_modules')) {
            // 分包,触发各自模块的 Tree Shaking
            return 'vendor';
          }
        },
      },
    },
  },
  // esbuild 配置
  esbuild: {
    treeShaking: true,
  },
});
javascript 复制代码
// webpack.config.js
module.exports = {
  mode: 'production',  // 生产模式自动开启 Tree Shaking
  optimization: {
    usedExports: true,      // 标记未使用的导出
    sideEffects: true,      // 启用 Tree Shaking
    providedExports: true,  // 收集导出信息
  },
};

sideEffects 配置

json 复制代码
// package.json
{
  "sideEffects": [
    "*.css",
    "*.scss",
    "./src/styles/global.css",
    "!./src/utils/analytics.js"  // 排除,假设它没有副作用
  ]
}
javascript 复制代码
// 如果模块声明无副作用,可安全删除未使用的导出
// 假设 utils.js 声明了 sideEffects: false
export function used() { /* ... */ }
export function unused() { /* ... */ }
// 如果 unused() 没被使用,整个函数会被移除

详细解析

副作用与纯函数

javascript 复制代码
// 有副作用 - 不能 Tree Shaking
function incrementGlobal() {
  window.counter = (window.counter || 0) + 1;  // 修改全局状态
}

import { incrementGlobal } from './utils';
incrementGlobal();  // 必须保留,副作用存在

// 无副作用 - 可以 Tree Shaking
function add(a, b) {
  return a + b;  // 纯函数
}

export { add };
// 如果未使用,add 可以被移除

常见问题排查

javascript 复制代码
// 问题 1:转译器将 ESM 转成 CJS
// babel.config.js
module.exports = {
  presets: [
    ['@babel/preset-env', {
      modules: false,  // 保持 ES Module,供构建工具 Tree Shaking
    }],
  ],
};

// 问题 2:使用了动态导入
import('./module').then(module => {
  module.doSomething();  // 动态,无法 Tree Shaking
});

// 问题 3:第三方库未配置 sideEffects
// 在库的 package.json 添加
{
  "sideEffects": false
}

面试题生成完毕

文件路径:/Users/bytedance/my-life/my-interview/generated_interview_fronted_20260510.md

共覆盖 5 个子类别20 个 topics,包含代码实现、详细解析和 2025-2026 年最新技术趋势。

相关推荐
Alice-YUE1 小时前
深入解析 JS 事件循环:浏览器与 Node.js 的差异全解析
前端·javascript·笔记·学习
HYCS1 小时前
用pixijs实现fabricjs(二):对象的基础位置信息
前端·javascript·canvas
淸湫1 小时前
项目中使用了全局权限管理,请详细描述如何通过Vue Router的路由守卫来实现全局权限控制?
前端·vue.js
雪铃儿1 小时前
Shorebird 之外,Flutter Android 热更新还有什么选择
android·前端
李剑一1 小时前
前端必看 | Vue 刷新页面,生命周期钩子直接 "罢工",原来问题在这?90% 开发者都栽过!
前端·vue.js
閞杺哋笨小孩1 小时前
域名驱动多租户入驻:后台配置 + 前端解析
前端·vue.js
折哥的程序人生 · 物流技术专研1 小时前
《Java面试85题图解版(二)》进阶深化中篇:Spring核心 + 数据库进阶
java·后端·spring·面试
TeamDev1 小时前
在 Excel 加载项中嵌入 Web 视图
前端·后端·.net
悠哉摸鱼大王1 小时前
cesium学习(一)-基本概念
前端·cesium