大家好,我是石小石~
qiankun架构下的调试困境
如果你公司的前端架构基于 qiankun,你一定遇到过这样一个问题:由于子应用脱离主应用独立运行,在本地开发阶段,很多和主应用的操作联动、样式交互都无法直接验证,只能把子应用部署到开发或测试环境后,才能排查这类问题。
尤其是在一些不需要做 JS 沙箱隔离的业务场景里,主子应用需要通过 eventBus 这类方式实现交互,子应用不部署上线,调试起来就非常麻烦。
那有没有办法让生产环境直接加载本地子应用来实现代码调试?
方法肯定是有的,比如在主应用里写一套便于调试的逻辑。
import { registerMicroApps, start } from 'qiankun';
// ============== 核心:根据环境变量加载 本地/线上 子应用 ==============
const isDev = process.env.IS_DEV; // webpack 注入的环境变量
// 子应用配置列表
const microApps = [
{
name: 'subapp-vue', // 子应用唯一名称
// 本地开发:加载 localhost 地址;生产:加载线上地址
entry: isDev ? 'http://localhost:8080/gcshi-web-demo' : '/gcshi-web-demo',
container: '#subapp-container', // 子应用挂载的容器 id
activeRule: '/vue', // 路由匹配规则
},
];
这种写法确实可以通过特定方式触发生产环境加载本地子应用,方便调试。但不可避免地需要修改主应用代码,如果没有主应用代码权限,那就很尴尬了。
其实,针对上面这个问题,用油猴脚本就能轻松解决!
油猴脚本简介
油猴(Tampermonkey)是一款浏览器插件,允许用户在网页加载时注入自定义的 JavaScript 脚本,来增强、修改或自动化网页行为。
通俗地说,借助油猴,你可以将自己的 JavaScript 代码“植入”任意网页,实现自动登录、抢单、签到、数据爬取、广告屏蔽等各种“开挂级”功能,彻底掌控页面行为。
它和谷歌插件能实现的效果几乎一致,不过更加简单。如果你是前端开发,可以直接使用油猴,因为它本质就是针对网页写js。
如果你对油猴脚本感兴趣,可以看看: 《油猴脚本实战指南》
使用油猴脚本实现生产环境加载本地子应用
如图,我用 npm run dev 启动了一个本地子应用服务。
开启插件后,页面上会出现油猴脚本的调试工具。
点击【开启代理】,主应用会自动刷新,从而加载本地子应用,全程不需要做任何额外配置。
而且它完美支持热更新,这意味着你修改本地子应用代码后,生产环境页面会同步更新,调试非常方便。
核心原理
实现生产环境加载本地子应用其实很简单:
用油猴脚本在主应用加载时进行拦截,把原本要加载的线上子应用地址,替换成本地服务地址。
你可以这么理解:主应用原本要加载 http://baidu.com/gcshi-web-demo,被脚本替换成了 http://localhost:8080/gcshi-web-demo。
重写fetch
qiankun 底层依赖 import-html-entry 这个库,核心流程是通过 fetch 加载子应用 HTML 模板,再解析 CSS、JS。 所以我们只需要在页面加载早期,拦截并重写 fetch 即可。
那么问题很好解决了, 我们只需要在页面加载早期,拦截并重写 fetch 即可。
const oldFetch = window.fetch;
window.fetch = (url, ...args) => {
// 替换域名
if (url === 'http://baidu.com/gcshi-web-demo') {
url = 'http://localhost:8080/gcshi-web-demo';
}
return oldFetch(url, ...args);
};
保证脚本最早运行
重写 window.fetch 的前提,是脚本必须比页面其他逻辑更早执行,否则重写会失效。
在油猴脚本中,可以通过添加元信息实现:
// @run-at document-start
参考:油猴脚本的运行生命周期
我在油猴脚本里的 fetch 重写逻辑如下:
import $ from "../../gmTool/index";
const { unsafeWindow } = $;
type FetchInterceptor = (url: RequestInfo | URL, options?: RequestInit) => [RequestInfo | URL, RequestInit?] | void | false;
const win = unsafeWindow;
const rawFetch = win.fetch.bind(win);
export function onFetch(interceptor: FetchInterceptor) {
// 如果已经被代理过,先复用原来的
if (!(win.fetch as any).rawFetch) {
const proxyFetch: typeof fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
let nextInput = input;
let nextInit = init;
// 执行 interceptor
try {
const result = interceptor(nextInput, nextInit);
if (result === false) {
console.warn("[winnex-web-proxy] 😭😭😭😭 fetch请求被用户阻止了===========================>", nextInput);
return Promise.reject(new Error("[winnex-web-proxy] 😭😭😭😭 fetch请求被用户阻止了"));
}
if (result && Array.isArray(result)) {
nextInput = result[0];
nextInit = result[1];
}
} catch (err) {
console.error("[fetch] interceptor error:", err);
}
// 处理 Request 对象情况
if (nextInput instanceof Request && nextInit) {
nextInput = new Request(nextInput, nextInit);
nextInit = undefined;
}
return rawFetch(nextInput, nextInit);
};
(proxyFetch as any).rawFetch = rawFetch;
win.fetch = proxyFetch;
}
// 返回取消方法
return function unProxyFetch() {
if ((win.fetch as any).rawFetch) {
win.fetch = rawFetch;
}
};
}
- 基础使用(替换接口地址)
// 注册拦截器
const unProxy = onFetch((url, options) => {
const u = url.toString();
// 匹配并替换地址
if (u === 'http://baidu.com/gcshi-web-demo') {
return ['http://localhost:8080/gcshi-web-demo', options];
}
});
- 阻止某个请求
onFetch((url) => {
if (url.toString().includes('/black-api')) {
return false; // 拦截并拒绝
}
});
解决跨域问题
生产环境页面加载本地 localhost:8080 可能会出现跨域,导致子应用加载失败。解决方法很简单,在 vite 或 webpack 中添加响应头配置:
devServer: {
headers: {
'Access-Control-Allow-Origin': '*'
},
},
解决热更新
默认情况下,生产环境加载子应用时,热更新会失效。原因是热更新相关的 XHR 请求前缀被替换成了主应用域名。只需要拦截 XHR 请求,修正热更新接口前缀即可。以 webpack 热更新为例,修复 sockjs-node、hot-update 两个接口就行。
使用 ajax-hook 实现 XHR 拦截,代码如下:
const appOrigin = "http://localhost:8080"
const fixHotUpdateUrl = (config: any) => {
if (config.url.includes("sockjs-node") && appOrigin) {
config.url = fixSockJsUrl(config.url, appOrigin);
}
if (config.url.includes(appName) && config.url.includes("hot-update")) {
config.url = fixHotUpdate(config.url, appName, appOrigin);
console.log(`[winnex-web-proxy] 热更新🚀🚀===============================> ${config.url}`);
}
};
export const xhrProxy = (enable: boolean) => {
if (!enable) return;
// xhr拦截
proxy(
{
//请求发起前进入
onRequest: (config, handler) => {
fixHotUpdateUrl(config);
handler.next(config);
},
//请求发生错误时进入,比如超时;注意,不包括http状态码错误,如404仍然会认为请求成功
onError: (err, handler) => {
handler.next(err);
},
//请求成功后进入
onResponse: (response, handler) => {
handler.next(response);
}
},
unsafeWindow
);
};
总结
在 qiankun 微前端架构中,本地子应用想要直接在生产环境调试,不必修改主应用代码、不必申请权限,通过油猴脚本重写 fetch劫持子应用入口地址,配合跨域配置和XHR 拦截修复热更新,就能实现线上环境加载本地子应用,并且支持热更新,极大提升微前端联调效率。整个方案轻量、无侵入、开箱即用,非常适合前端日常调试。