Skip to content

场景题面试题精选

汇总系统设计、大文件上传、虚拟列表、搜索建议、请求重试、前端监控等高频场景题。

系统设计

1. 如何设计一个前端监控系统?

采集维度:

  • 性能监控:FCP、LCP、CLS、TTFB(用 web-vitals 库)
  • 错误监控:JS 错误、Promise 未捕获、资源加载失败
  • 用户行为:PV/UV、点击、路由跳转、停留时长
  • 接口监控:请求耗时、错误率、慢请求

上报策略:

javascript
// 优先用 sendBeacon(页面卸载时也能发送,不阻塞)
navigator.sendBeacon('/report', JSON.stringify(data));
// 降级用 fetch 或 Image 打点
new Image().src = `/report?data=${encodeURIComponent(JSON.stringify(data))}`;

关键设计点:

  • 采样率控制(高流量时只上报 10%)
  • 批量上报(积累一定数量再发送)
  • 错误去重(相同错误只上报一次)
  • Source Map 还原(上报压缩后的行列号,服务端还原)

2. 如何设计一个通用的 HTTP 请求封装?
javascript
class HttpClient {
    constructor(baseURL, options = {}) {
        this.baseURL = baseURL;
        this.interceptors = { request: [], response: [] };
        this.timeout = options.timeout || 10000;
    }

    // 请求拦截器(添加 token、loading 等)
    useRequest(fn) { this.interceptors.request.push(fn); }

    // 响应拦截器(统一错误处理、数据转换)
    useResponse(fn) { this.interceptors.response.push(fn); }

    async request(config) {
        // 执行请求拦截器
        let finalConfig = this.interceptors.request.reduce(
            (cfg, fn) => fn(cfg), { ...config, url: this.baseURL + config.url }
        );

        const controller = new AbortController();
        const timer = setTimeout(() => controller.abort(), this.timeout);

        try {
            let response = await fetch(finalConfig.url, {
                ...finalConfig,
                signal: controller.signal
            });
            // 执行响应拦截器
            return this.interceptors.response.reduce((res, fn) => fn(res), response);
        } finally {
            clearTimeout(timer);
        }
    }
}

大文件上传

3. 大文件上传如何实现?

核心方案:分片上传 + 断点续传

javascript
async function uploadLargeFile(file) {
    const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB 每片
    const fileHash = await calculateHash(file); // 文件内容 hash(用于秒传和续传)

    // 1. 询问服务端已上传哪些分片(断点续传)
    const { uploadedChunks } = await checkUploadStatus(fileHash);

    // 2. 切片
    const chunks = [];
    for (let i = 0; i < Math.ceil(file.size / CHUNK_SIZE); i++) {
        chunks.push(file.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE));
    }

    // 3. 并发上传未上传的分片(控制并发数)
    const tasks = chunks
        .filter((_, i) => !uploadedChunks.includes(i))
        .map((chunk, i) => () => uploadChunk(chunk, fileHash, i));

    await concurrentLimit(tasks, 3); // 最多 3 个并发

    // 4. 通知服务端合并
    await mergeChunks(fileHash, chunks.length);
}

秒传:上传前先发送文件 hash,服务端已有则直接返回成功。


虚拟列表

4. 虚拟列表的实现原理?

只渲染可视区域内的列表项,通过 padding 或 transform 撑开滚动区域。

javascript
// 定高虚拟列表核心逻辑
function VirtualList({ items, itemHeight, containerHeight }) {
    const [scrollTop, setScrollTop] = useState(0);

    const startIndex = Math.floor(scrollTop / itemHeight);
    const visibleCount = Math.ceil(containerHeight / itemHeight);
    const endIndex = Math.min(startIndex + visibleCount + 1, items.length);

    const visibleItems = items.slice(startIndex, endIndex);
    const offsetY = startIndex * itemHeight; // 偏移量

    return (
        <div style={{ height: containerHeight, overflow: 'auto' }}
             onScroll={e => setScrollTop(e.target.scrollTop)}>
            {/* 撑开滚动区域 */}
            <div style={{ height: items.length * itemHeight, position: 'relative' }}>
                <div style={{ transform: `translateY(${offsetY}px)` }}>
                    {visibleItems.map((item, i) => (
                        <div key={startIndex + i} style={{ height: itemHeight }}>
                            {item.content}
                        </div>
                    ))}
                </div>
            </div>
        </div>
    );
}

不定高虚拟列表:预估高度渲染,渲染后用 ResizeObserver 更新实际高度,用 Map 缓存已知高度。


搜索建议

5. 搜索建议(Autocomplete)如何实现?

关键点:防抖 + 取消过期请求 + 缓存

javascript
function useSearchSuggestions(delay = 300) {
    const [suggestions, setSuggestions] = useState([]);
    const cache = useRef(new Map());
    const abortRef = useRef(null);

    const search = useMemo(() => debounce(async (query) => {
        if (!query.trim()) { setSuggestions([]); return; }

        // 命中缓存
        if (cache.current.has(query)) {
            setSuggestions(cache.current.get(query));
            return;
        }

        // 取消上一次请求
        abortRef.current?.abort();
        abortRef.current = new AbortController();

        try {
            const data = await fetchSuggestions(query, abortRef.current.signal);
            cache.current.set(query, data);
            setSuggestions(data);
        } catch (e) {
            if (e.name !== 'AbortError') console.error(e);
        }
    }, delay), [delay]);

    return { suggestions, search };
}

请求重试

6. 如何实现请求重试机制?
javascript
async function fetchWithRetry(url, options = {}, retries = 3, delay = 1000) {
    for (let i = 0; i <= retries; i++) {
        try {
            const res = await fetch(url, options);
            if (!res.ok) throw new Error(`HTTP ${res.status}`);
            return res;
        } catch (err) {
            if (i === retries) throw err; // 最后一次失败,抛出错误
            // 指数退避:1s, 2s, 4s...
            await new Promise(r => setTimeout(r, delay * 2 ** i));
        }
    }
}

// 只对特定错误重试(网络错误、5xx,不重试 4xx)
function shouldRetry(error) {
    if (error.name === 'TypeError') return true; // 网络错误
    if (error.status >= 500) return true;        // 服务端错误
    return false;
}

拖拽排序

7. 拖拽排序如何实现?

方案一:HTML5 Drag and Drop API

javascript
// 核心:dragstart 记录源,dragover 允许放置,drop 交换位置
item.addEventListener('dragstart', e => {
    e.dataTransfer.setData('text/plain', item.dataset.index);
});
container.addEventListener('dragover', e => e.preventDefault());
container.addEventListener('drop', e => {
    const fromIndex = e.dataTransfer.getData('text/plain');
    const toIndex = e.target.dataset.index;
    swapItems(fromIndex, toIndex);
});

方案二:鼠标事件(更灵活):mousedown 记录起点 → mousemove 更新位置 → mouseup 确认放置

方案三:成熟库@dnd-kit/core(React)、vue-draggable-next(Vue)、Sortable.js(框架无关)


国际化

8. 前端国际化方案如何设计?
javascript
// i18n 核心实现
class I18n {
    constructor(locale, messages) {
        this.locale = locale;
        this.messages = messages; // { zh: {...}, en: {...} }
    }

    t(key, params = {}) {
        const msg = key.split('.').reduce((obj, k) => obj?.[k], this.messages[this.locale]);
        if (!msg) return key; // 降级显示 key
        // 插值替换:'Hello, {name}' → 'Hello, 张三'
        return msg.replace(/\{(\w+)\}/g, (_, k) => params[k] ?? `{${k}}`);
    }

    setLocale(locale) {
        this.locale = locale;
        document.documentElement.lang = locale;
        // 触发更新...
    }
}

注意事项

  • 日期/数字格式化用 Intl.DateTimeFormatIntl.NumberFormat
  • RTL 语言(阿拉伯语)需要 dir="rtl" 和镜像布局
  • 动态加载语言包(按需,减少首屏体积)

错误边界

9. 前端错误边界如何处理?
javascript
// React 错误边界
class ErrorBoundary extends React.Component {
    state = { hasError: false, error: null };

    static getDerivedStateFromError(error) {
        return { hasError: true, error };
    }

    componentDidCatch(error, info) {
        // 上报错误
        reportError({ error, componentStack: info.componentStack });
    }

    render() {
        if (this.state.hasError) {
            return <ErrorFallback error={this.state.error}
                                  onReset={() => this.setState({ hasError: false })} />;
        }
        return this.props.children;
    }
}

// Vue 错误处理
app.config.errorHandler = (err, instance, info) => {
    reportError({ err, info });
};

数据可视化

10. 前端数据可视化方案如何选择?
方案特点适用场景
ECharts功能全面,配置式,社区活跃通用图表,快速开发
D3.js底层灵活,学习曲线陡自定义复杂图表
AntV(G2/G6)阿里出品,语法优雅企业级应用
Three.js3D 渲染3D 可视化
Canvas高性能,适合大量数据实时数据、游戏
SVG矢量,可交互少量元素,需要交互

大数据量优化:数据采样、WebGL 渲染(deck.gl)、Web Worker 处理数据、虚拟化(只渲染可视区域)