乾坤微前端项目:前端处理后台分批次返回的 Markdown 流式数据

"我们的文档系统用乾坤拆成了主应用和文档子应用,现在文档子应用需要加载后台分 10 批返回的 5 万字 Markdown 流式数据,怎么确保在乾坤环境下既能实时渲染,又不影响主应用性能?"

面试官的问题直击乾坤微前端的核心痛点 ------ 主应用与子应用的资源隔离、通信效率。我决定从乾坤的架构特性出发,在之前 "四步实操流程" 的基础上,补充微前端环境下的特殊处理方案。

一、先明确乾坤环境的 "流式数据处理前提"

在动手前,必须先解决乾坤微前端的两个关键限制,否则流式处理会出现 "资源阻塞" 或 "通信失效":

  1. 子应用资源加载策略:乾坤默认会拦截子应用的fetch请求,需配置excludeAssetFilter排除流式接口,避免主应用缓存分片数据;
  1. 主 - 子应用通信方式:如果流式数据需共享给主应用(比如进度展示),不能用props(同步通信不支持流式),需用乾坤的initGlobalState或EventBus做异步通信。

面试官补充:"文档子应用独立处理流式渲染,只需把加载进度同步给主应用的进度条组件。" 明确需求后,开始制定方案。

二、乾坤子应用的 "五步流式处理实操"

以 Vue3 + 乾坤的文档子应用为例,完整流程需兼顾 "流式处理" 和 "微前端适配":

第一步:配置乾坤,放行流式接口

在主应用注册子应用时,必须排除流式接口的拦截,否则text/event-stream类型的请求会被乾坤当作普通静态资源缓存:

javascript 复制代码
// 主应用 src/micro-app.js(乾坤注册子应用配置)
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
  {
    name: 'doc-app', // 子应用名称
    entry: '//localhost:8081', // 子应用地址
    container: '#doc-container', // 子应用容器
    activeRule: '/doc', // 激活路由
    // 关键:排除流式接口,避免乾坤拦截
    excludeAssetFilter: (assetUrl) => {
      // 匹配子应用的流式接口(需和子应用接口路径一致)
      return assetUrl.includes('/api/markdown-stream');
    }
  }
]);
start();

同时在子应用的public-path.js中,确保fetch不被乾坤重写(Vue3 子应用为例):

javascript 复制代码
// 子应用 src/public-path.js
if (window.__POWERED_BY_QIANKUN__) {
  // 仅重写静态资源路径,不重写fetch
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

第二步:子应用初始化流式请求(适配乾坤通信)

在子应用的DocStream.vue组件中,发起流式请求,并初始化主 - 子通信:

xml 复制代码
<template>
  <div class="doc-container">
    <div id="loading" v-if="loading">加载中:{{ progress }}%</div>
    <div id="markdown-container" class="markdown-content"></div>
  </div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { marked } from 'marked';
import hljs from 'highlight.js';
// 引入乾坤通信工具(子应用需安装 qiankun 依赖)
import { initGlobalState } from 'qiankun';
const loading = ref(true);
const progress = ref(0);
const totalChunks = ref(10); // 后台约定的总分片数(需和后台对齐)
let currentChunkCount = ref(0);
let reader = null; // 存储流式读取器,用于卸载时销毁
let globalState = null; // 乾坤全局状态(同步进度给主应用)
// 初始化乾坤全局通信
const initQiankunState = () => {
  if (window.__POWERED_BY_QIANKUN__) {
    globalState = initGlobalState({
      docLoadProgress: 0 // 初始进度
    });
    // 监听主应用的状态变更(可选,这里仅子应用向主应用发消息)
    globalState.onGlobalStateChange((state) => {
      console.log('主应用状态变更:', state);
    });
  }
};
// 发起流式请求(核心逻辑和之前一致,新增进度计算)
const streamRequest = async () => {
  try {
    const response = await fetch('/api/markdown-stream', { // 子应用流式接口
      method: 'GET',
      headers: {
        'Accept': 'text/event-stream',
        'Authorization': 'Bearer ' + localStorage.getItem('token'),
        // 关键:告诉后台当前是乾坤子应用,避免返回格式差异
        'X-Qiankun-App': 'doc-app'
      },
      cache: 'no-store' // 关闭缓存
    });
    if (!response.ok) {
      throw new Error(`接口失败:${response.status}`);
    }
    if (!response.body) {
      throw new Error('后台未返回流式数据');
    }
    await handleStreamData(response);
  } catch (error) {
    console.error('流式请求失败:', error);
    document.getElementById('markdown-container').innerText = '文档加载失败,请刷新';
    loading.value = false;
  }
};
onMounted(() => {
  initQiankunState(); // 初始化乾坤通信
  streamRequest(); // 发起流式请求
});
onUnmounted(() => {
  // 关键:卸载时销毁流式连接,避免主应用切换子应用后内存泄漏
  if (reader) {
    reader.cancel('子应用卸载,中断流式连接');
  }
  // 重置主应用进度
  if (globalState) {
    globalState.setGlobalState({ docLoadProgress: 0 });
  }
});
</script>

第三步:子应用逐片读取数据,同步进度给主应用

在handleStreamData函数中,新增 "进度计算" 和 "乾坤状态同步",确保主应用能实时显示进度:

ini 复制代码
// 子应用 DocStream.vue 中新增 handleStreamData 函数
const handleStreamData = async (response) => {
  reader = response.body.getReader();
  const decoder = new TextDecoder('utf-8');
  let completeMarkdown = '';
  let isEnd = false;
  while (!isEnd) {
    try {
      const { done, value } = await reader.read();
      isEnd = done;
      if (value) {
        const currentChunk = decoder.decode(value, { stream: true });
        completeMarkdown += currentChunk;
        currentChunkCount.value++; // 累加分片数
        // 1. 计算进度(当前分片数/总分片数*100)
        progress.value = Math.floor((currentChunkCount.value / totalChunks.value) * 100);
        // 2. 同步进度给主应用(仅在乾坤环境下)
        if (window.__POWERED_BY_QIANKUN__ && globalState) {
          globalState.setGlobalState({
            docLoadProgress: progress.value
          });
        }
        // 3. 实时渲染
        renderMarkdown(completeMarkdown);
      }
      // 所有分片加载完成
      if (isEnd) {
        loading.value = false;
        await reader.releaseLock();
        // 同步"加载完成"状态给主应用
        if (globalState) {
          globalState.setGlobalState({ docLoadProgress: 100 });
        }
      }
    } catch (error) {
      console.error('读取分片失败:', error);
      await reader.releaseLock();
      loading.value = false;
      // 同步"加载失败"状态给主应用
      if (globalState) {
        globalState.setGlobalState({ docLoadProgress: -1 });
      }
      break;
    }
  }
};

第四步:子应用实时渲染,处理 "乾坤样式隔离"

乾坤默认开启sandbox样式隔离,子应用的 Markdown 渲染样式会被隔离,需通过 "样式穿透" 或 "主应用共享样式" 解决:

ini 复制代码
// 子应用 DocStream.vue 中 renderMarkdown 函数(新增样式适配)
const renderMarkdown = (content) => {
  // 1. 补全不完整分片(和之前逻辑一致)
  let renderContent = content;
  if (/-\s[^(\n|$)]*$/.test(renderContent)) {
    renderContent += '\n';
  }
  // 2. 初始化marked(适配乾坤样式隔离)
  marked.setOptions({
    breaks: true,
    highlight: (code, lang) => {
      // 关键:代码高亮样式需添加子应用前缀,避免被乾坤隔离
      const highlightedCode = hljs.highlight(code, { language: lang || 'plaintext' }).value;
      // 给高亮代码添加子应用专属类(doc-app-code),主应用可通过该类控制样式
      return `<pre class="doc-app-code"><code>${highlightedCode}</code></pre>`;
    }
  });
  // 3. 渲染到DOM(容器需在子应用内,避免被主应用DOM拦截)
  const container = document.getElementById('markdown-container');
  container.innerHTML = marked.parse(renderContent);
  // 4. 样式穿透:如果子应用样式被隔离,手动添加主应用共享样式
  if (window.__POWERED_BY_QIANKUN__) {
    // 动态加载主应用的Markdown样式(避免子应用重复引入)
    const link = document.createElement('link');
    link.rel = 'stylesheet';
    link.href = '//localhost:8080/styles/markdown-common.css'; // 主应用共享样式
    link.id = 'markdown-common-style';
    // 避免重复加载
    if (!document.getElementById('markdown-common-style')) {
      container.appendChild(link);
    }
  }
};

第五步:主应用接收进度,同步更新 UI

主应用的进度条组件(比如ProgressBar.vue)通过乾坤的onGlobalStateChange接收子应用的进度:

xml 复制代码
// 主应用 src/components/ProgressBar.vue
<template>
  <div class="progress-bar" v-if="showProgress">
    <div class="progress" :style="{ width: `${progress}%` }"></div>
  </div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { initGlobalState } from 'qiankun';
const progress = ref(0);
const showProgress = ref(false);
let globalState = null;
onMounted(() => {
  // 初始化全局状态监听
  globalState = initGlobalState({});
  globalState.onGlobalStateChange((state) => {
    // 接收子应用的文档加载进度
    if (state.docLoadProgress !== undefined) {
      progress.value = state.docLoadProgress;
      // 控制进度条显示:-1(失败)、100(完成)时隐藏
      showProgress.value = progress.value >= 0 && progress.value < 100;
    }
  });
});
</script>
<style scoped>
.progress-bar {
  height: 4px;
  background: #f5f5f5;
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 9999;
}
.progress {
  height: 100%;
  background: #1890ff;
  transition: width 0.3s ease;
}
</style>

三、乾坤环境的 "特殊异常处理"

相比非微前端项目,乾坤下需额外处理三个 "流式坑点":

1. 子应用卸载时销毁流式连接

如果用户在流式加载中切换子应用(比如从文档子应用切到首页),必须在子应用beforeUnmount中销毁reader,否则会导致主应用内存泄漏:

scss 复制代码
// 子应用 DocStream.vue
onUnmounted(() => {
  if (reader) {
    // 主动取消流式连接,通知后台停止发送分片
    reader.cancel('子应用卸载,中断流式连接');
    reader = null;
  }
  // 清空全局状态
  if (globalState) {
    globalState.setGlobalState({ docLoadProgress: 0 });
  }
});

2. 跨域流式接口的乾坤适配

如果子应用的流式接口是跨域的(比如子应用在 8081 端口,接口在 8082 端口),需在接口响应头中添加Access-Control-Allow-Origin,且主应用的excludeAssetFilter需匹配跨域接口地址:

javascript 复制代码
// 主应用 excludeAssetFilter 适配跨域接口
excludeAssetFilter: (assetUrl) => {
  // 匹配跨域的流式接口(比如 http://localhost:8082/api/markdown-stream)
  return assetUrl.includes('//localhost:8082/api/markdown-stream');
}

3. 乾坤沙箱的 "fetch 重写问题"

如果子应用使用axios发起流式请求,会被乾坤的沙箱拦截,需改用原生fetch,或在axios配置中关闭adapter:

arduino 复制代码
// 子应用中禁用axios的adapter,避免被乾坤拦截
const axiosInstance = axios.create({
  adapter: (config) => {
    // 对流式接口,用原生fetch实现
    if (config.url.includes('/api/markdown-stream')) {
      return fetch(config.url, {
        method: config.method,
        headers: config.headers,
        cache: 'no-store'
      });
    }
    // 其他接口用默认adapter
    return axios.defaults.adapter(config);
  }
});

四、面试总结:乾坤流式处理的 "核心原则"

演示完代码后,我总结了三个关键经验,这些都是在乾坤项目中踩过的坑:

  1. 优先配置乾坤拦截规则:流式接口必须排除拦截,否则text/event-stream会被当作普通资源缓存,导致分片顺序错乱;
  1. 主 - 子通信用异步方案:进度、状态等流式数据,不能用props或vuex,必须用乾坤的initGlobalState或EventBus;
  1. 子应用卸载要 "彻底销毁" :不仅要释放reader,还要清空全局状态,避免主应用切换子应用后残留流式连接。

面试官点头:"之前我们的子应用就是因为没配置excludeAssetFilter,导致流式接口被缓存,分片一直重复加载。这个方案解决了核心问题。" 最后还讨论了 "大文件分片上传" 的乾坤适配,其实和流式处理原理类似,只需在主应用排除上传接口即可。

相关推荐
Java面试题总结12 分钟前
2026Java面试八股文合集(持续更新)
java·spring·面试·职场和发展·java面试·java八股文
前进的李工20 分钟前
LangChain使用之Model IO(提示词模版之ChatPromptTemplate)
java·前端·人工智能·python·langchain·大模型
漫随流水34 分钟前
旅游推荐系统(login.html)
前端·html·旅游
1024小神36 分钟前
记录xcode项目swiftui配置APP加载启动图
前端·ios·swiftui·swift
城沐小巷41 分钟前
【无标题】
面试·职场和发展·毕业设计·课程设计·毕设
CHU72903542 分钟前
社区生鲜买菜小程序前端功能版块设计及玩法介绍
前端·小程序
尤山海1 小时前
深度防御:内容类网站如何有效抵御 SQL 注入与脚本攻击(XSS)
前端·sql·安全·web安全·性能优化·状态模式·xss
前端小趴菜051 小时前
Windi CSS
前端·css
xuankuxiaoyao1 小时前
VUE.JS 实践 第二章
前端·javascript·vue.js
毕设源码-赖学姐1 小时前
【开题答辩全过程】以 基于Vue的电商管理平台为例,包含答辩的问题和答案
前端·javascript·vue.js