前端面试题(一)
一、websocket 假死
浏览器中的 WebSocket API 不能像 Node.js 的 ws 库一样直接发送协议 层的 Ping 帧,也不能直接监听协议层 Pong 帧。
所以前端项目中常说的 ping / pong 心跳,通常是业务层心跳。

也就是说,前端主动发一个普通消息 ping,后端收到后返回一个普通消息 pong。
如果前端在指定时间内没有收到 pong,就认为连接可能已经假死,然后主动关闭连接并重连。
websocket全局使用 Vue3 WebSocket 封装通关指南:心跳 + 重连 + 全局状态管理,复制即用!_vue3 websocket封装-CSDN博客
二、md转html
Markdown 本质是字符串,转换链路:md 文本 → 解析成 HTML 字符串 → 放进页面渲染(v-html)→ 用 CSS 美化
渲染任意 HTML 字符串有 XSS 风险,必须经过 DOMPurify 消毒。
代码块高亮用 highlight.js,Markdown 排版样式直接用 github-markdown-css,省去手写样式。
第一步:写一个 Markdown 渲染组件
<!--import 'github-markdown-css/github-markdown.css' 省去手写样式 -->
<!-- src/components/MarkdownViewer.vue -->
<template>
<div class="markdown-body" v-html="html"></div>
</template>
<script setup>
import { computed } from 'vue'
import { marked } from 'marked'
import DOMPurify from 'dompurify'
import hljs from 'highlight.js'
import 'highlight.js/styles/github.css' // 代码高亮主题
// marked 配置:让 highlight.js 处理代码块高亮
marked.setOptions({
highlight(code, lang) {
if (lang && hljs.getLanguage(lang)) {
return hljs.highlight(code, { language: lang }).value
}
return hljs.highlightAuto(code).value
},
gfm: true, // 开启 GitHub 风格(表格、任务列表等)
breaks: true, // 支持单换行转 <br>
})
const props = defineProps({
source: { type: String, default: '' },
})
const html = computed(() => {
// 先解析,再消毒,最后输出
return DOMPurify.sanitize(marked.parse(props.source))
})
</script>
第二步:在页面里使用
<script setup>
import MarkdownViewer from '@/components/MarkdownViewer.vue'
</script>
<template>
<MarkdownViewer :source="mdText" />
</template>
标准口述答案
"我会把它拆成四步:解析、安全、渲染、样式。
第一步,用 marked 把 Markdown 解析成 HTML 字符串,开启 gfm 支持表格和任务列表。
第二步,安全处理。解析出来的 HTML 不能直接 v-html 渲染,因为可能包含 <script> 或 onclick 等恶意内容,我会用
DOMPurify.sanitize() 消毒,白名单过滤掉危险标签和属性,这是防止 XSS 的关键一步。
第三步,渲染。封装一个 MarkdownViewer.vue 组件,用 computed 把 source 转换后的结果绑定到 v-html 上,组件里做 marked
的配置,这样全项目可以复用。
第四步,样式。直接引入 github-markdown-css,给根节点加 markdown-body class,就能获得成熟的排版;代码块用
highlight.js 在 marked 的 highlight 钩子里做高亮。
如果是静态 md 文件,Vite 里用 import xx from './xx.md?raw' 按文本导入;如果是后端返回的,就在接口请求里拿到字符串传
给组件。文件大就走 defineAsyncComponent 或动态 import 懒加载。"
三、fetch 下载文件
fetch 下载文件的核心套路是:fetch 拿到 Blob → 生成临时 URL → 模拟 <a> 点击下载 → 释放 URL。
//适配大文件下载 可以获取下载进度
const res = await fetch(url) if (!res.ok) throw new Error(`下载失败:HTTP ${res.status}`) const reader = res.body.getReader() const total = +res.headers.get('Content-Length') || 0 const chunks = [] let received = 0
const reader = res.body.getReader() // 拿到可读流
while (true) {
const { done, value } = await reader.read()
if (done) break // 读到流结束,跳出循环
chunks.push(value) // 把这一小块数据存起来
received += value.length // 累计已下载字节数
}
const blob = new Blob(chunks) // 后面同上面的触发下载逻辑 const blobUrl = URL.createObjectURL(blob) const a = document.createElement('a') a.href = blobUrl a.download = 'download' document.body.appendChild(a) a.click() a.remove() URL.revokeObjectURL(blobUrl)
为什么不用 window.open(url):fetch 能带 Authorization 请求头、能统一处理 HTTP 错误、能拿到进度;window.open / <a href> 适合"纯 GET、无鉴权、不需要错误处理"的简单场景。
CORS:跨域下载时,后端必须允许跨域且响应头暴露 Content-Disposition(Access-Control-Expose-Headers: Content-Disposition),否则取不到文件名。
文件名中文乱码:Content-Disposition 一般用 filename*=UTF-8''%E6%8A%A5%E8%A1%A8.xlsx 格式,记得 decodeURIComponent。
用完必须 revokeObjectURL,否则大文件会一直占内存。
现代浏览器也可以用 showSaveFilePicker() 弹系统保存框(File System Access API),但兼容性一般,常规项目用 <a download> 即可。
为什么要这样读,而不是一次拿到全部
能算进度:每读一块就更新 received,除以总大小就是百分比。这是 fetch 下载进度唯一的常规实现方式。
避免内存暴涨:大文件如果直接 await res.blob(),浏览器会把整个文件一次性装进内存;流式读取是边下载边消费,配合进度条
体验更好。
reader.read() 的机制:每次调用会返回一个 Promise,resolve 出 { done, value }。done: false 表示还有数据(value 是
Uint8Array 小块),done: true 表示流已结束。所以用 while (true) 反复读,直到 done 为 true 才 break,不会无限循环。

浙公网安备 33010602011771号