前端面试题(一)

一、websocket 假死

浏览器中的 WebSocket API 不能像 Node.js 的 ws 库一样直接发送协议 层的 Ping 帧,也不能直接监听协议层 Pong 帧。

所以前端项目中常说的 ping / pong 心跳,通常是业务层心跳。

image

也就是说,前端主动发一个普通消息 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,不会无限循环。

 

posted @ 2026-08-10 16:40  携手度余生  阅读(11)  评论(0)    收藏  举报