乾坤微前端项目:前端处理后台分批次返回的 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,导致流式接口被缓存,分片一直重复加载。这个方案解决了核心问题。" 最后还讨论了 "大文件分片上传" 的乾坤适配,其实和流式处理原理类似,只需在主应用排除上传接口即可。

相关推荐
用户6600676685393 小时前
用 CSS3 导演一场星际穿越:复刻“星球大战”经典片头
前端·css
程序员鱼皮3 小时前
前后端分离,千万别再搞错了!
java·前端·后端·计算机·程序员·编程·软件开发
前端赵哈哈3 小时前
Vite 构建后产品详情页图片失效?从路径匹配到映射表的完美解决
前端·vue.js·vite
葡萄城技术团队3 小时前
React Native 错误处理完全指南
前端
地方地方3 小时前
event loop 事件循环
前端·javascript·面试
AAA阿giao3 小时前
不用 JavaScript,你能用 CSS 做到什么?答案:拍一部星战电影!
前端·css
golang学习记3 小时前
从0死磕全栈之在 Next.js 中使用 Sass
前端
好大的月亮3 小时前
oss中的文件替换后chrome依旧下载到缓存文件概述
前端·chrome·缓存
Broken Arrows3 小时前
解决Jenkins在构建前端任务时报错error minimatch@10.0.3:……的记录
运维·前端·jenkins