文章目的:本文主要是解决使用 micro-app 遇到的一些常见的问题、以及一些处理方案。如果你还不熟悉、jd-opensource.github.io/micro-app/ 查看官方文档以了解基本使用。
微前端的框架有很多、但是我还是觉得 micro-app 是最简单、最方便的框架。例如 qiankun、他的使用方法及其复杂、你不仅需要在基座上安装依赖、配置环境。还需要在子应用中安装依赖、配置环境。还要给子应用配置生命周期函数导出。
最主要的是、官方已经放弃了这个项目、github 上面的 issues 已经无人问津。
如何配置本地开发
配置本地开发环境、你可以先按官方的 快速开始 配置的项目环境。在后续你可能会遇到很多报错、现在我们来一一解决;
打开子应用页面空白
按照官方的文档中、你首先需要检查两个项目之间的路由规则是否匹配、这一步解决起来比较简单、按照文档配置、你将可能遇到下一个报错。
域名跨域
子应用访问路由页面时跨域:
MicroApp官方解决跨域的方法是在webpack-dev-server的headers中设置跨域支持。
devServer: {
headers: {
'Access-Control-Allow-Origin': '*',
}
},
但在实际项目中,由于项目中的请求携带了token,由于预检请求的原因,设置了这个并不能解决跨域问题。需要对预检请求也要进行跨域处理。根据打包工具的不同,需要对其进行相应的设置。
webpack@4, webpack-dev-server@3
devServer:{
before(app) => {
app.use((req, res, next) => {
const origin = req.headers.origin;
res.setHeader("Access-Control-Allow-Origin", origin ? origin : "*");
res.setHeader("Access-Control-Allow-Credentials", "true");
res.setHeader(
"Access-Control-Allow-Headers",
"Content-Type, Authorization, X-Requested-With, Accept, Origin, Access-Control-Request-Method, Access-Control-Request-Headers, env, auc_system_code, auc_app_key, KeyId"
);
res.setHeader(
"Access-Control-Allow-Methods",
"GET, POST, PUT, DELETE, OPTIONS, PATCH"
);
if (res.method === "OPTIONS") {
res.sendStatus(200);
} else {
next();
}
});
}
}
webpack@5, webpack-dev-server@4
devServer:{
onBeforeSetupMiddleware(app) => {
app.use((req, res, next) => {
const origin = req.headers.origin;
res.setHeader("Access-Control-Allow-Origin", origin ? origin : "*");
res.setHeader("Access-Control-Allow-Credentials", "true");
res.setHeader(
"Access-Control-Allow-Headers",
"Content-Type, Authorization, X-Requested-With, Accept, Origin, Access-Control-Request-Method, Access-Control-Request-Headers, env, auc_system_code, auc_app_key, KeyId"
);
res.setHeader(
"Access-Control-Allow-Methods",
"GET, POST, PUT, DELETE, OPTIONS, PATCH"
);
if (res.method === "OPTIONS") {
res.sendStatus(200);
} else {
next();
}
});
}
}
umi@3, express
devServer: {
beforeMiddlewares: [
(req, res, next) => {
const origin = req.headers.origin;
res.setHeader("Access-Control-Allow-Origin", origin ? origin : "*");
res.setHeader("Access-Control-Allow-Credentials", "true");
res.setHeader(
"Access-Control-Allow-Headers",
"Content-Type, Authorization, X-Requested-With, Accept, Origin, Access-Control-Request-Method, Access-Control-Request-Headers, env, auc_system_code, auc_app_key, KeyId"
);
res.setHeader(
"Access-Control-Allow-Methods",
"GET, POST, PUT, DELETE, OPTIONS, PATCH"
);
if (res.method === "OPTIONS") {
res.sendStatus(200);
} else {
next();
}
},
];
}
子应用的后端服务跨域
Access to XMLHttpRequest at 'http://172.16.5.192:8080/environment/list' from origin 'http://172.16.5.192:8000' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.
这个报错时基座访问子应用时、子应用会调用后端接口造成的跨域问题。
在子应用的 devServer 中代理 proxy:
devServer:{
proxy: {
"/environment": {
target: "http://172.16.5.192:8080",
changeOrigin: true,
pathRewrite: {
"^/environment": "/environment"
},
onProxyReq: (proxyReq, req, res) => {
proxyReq.headers["Access-Control-Allow-Origin"] = req.headers.origin;
proxyReq.headers["Access-Control-Allow-Credentials"] = "true";
proxyReq.headers["Access-Control-Allow-Headers"] =
"Content-Type, Authorization, X-Requested-With, Accept, Origin, Access-Control-Request-Method, Access-Control-Request-Headers, env, auc_system_code, auc_app_key, KeyId";
proxyReq.headers["Access-Control-Allow-Methods"] =
"GET, POST, PUT, DELETE, OPTIONS, PATCH";
},
},
},
};
完整的配置
// 代理地址
const BASE_URL = "http://172.16.5.192:8080/";
const APIS = [
"/api",
"/service",
"/system",
"/user",
"/role",
"/permission",
"/menu",
"/log",
"/file",
"/config",
];
const devServerBeforeHook = (app) => {
app.use((req, res, next) => {
const origin = req.headers.origin;
res.setHeader("Access-Control-Allow-Origin", origin ? origin : "*");
res.setHeader("Access-Control-Allow-Credentials", "true");
res.setHeader(
"Access-Control-Allow-Headers",
"Content-Type, Authorization, X-Requested-With, Accept, Origin, Access-Control-Request-Method, Access-Control-Request-Headers, env, auc_system_code, auc_app_key, KeyId"
);
res.setHeader(
"Access-Control-Allow-Methods",
"GET, POST, PUT, DELETE, OPTIONS, PATCH"
);
if (res.method === "OPTIONS") {
res.sendStatus(200);
} else {
next();
}
});
};
const setAccessControlHeaders = (proxyReq, req, res) => {
proxyReq.headers["Access-Control-Allow-Origin"] = req.headers.origin;
proxyReq.headers["Access-Control-Allow-Credentials"] = "true";
proxyReq.headers["Access-Control-Allow-Headers"] =
"Content-Type, Authorization, X-Requested-With, Accept, Origin, Access-Control-Request-Method, Access-Control-Request-Headers, env, auc_system_code, auc_app_key, KeyId";
proxyReq.headers["Access-Control-Allow-Methods"] =
"GET, POST, PUT, DELETE, OPTIONS, PATCH";
};
const proxyConfig = APIS.reduce((acc, api) => {
acc[api] = {
target: BASE_URL,
changeOrigin: true,
pathRewrite: {
[`^${api}`]: api,
},
onProxyReq: setAccessControlHeaders,
};
}, {});
export default {
devServer: {
before: devServerBeforeHook,
proxy: proxyConfig,
},
};
重复的 app name
我在处理项目时、使用基础创建了一个公用的 micro-app组件、也就是说访问我的子应用时、他都会经过这个组件作为入口:
const microApp = () => {
return (
<micro-app name="app" url="localhost:8080" />
)
}
所以这就导致了 name 的冲突、没有路由页面他都是一个 micro-app 应用、所以你需要保证他们的 name 都不是重复的。
第三方插件问题
从上面的截图中、我接入子应用时出现了两个小问题:
- 按钮下来菜单定位偏移了
- 点击详细按钮导致代码报错
经过排查、这两个问题来自于 node_modules 第三方插件、但是子应用独立使用时、他没有任何问题。并且 node_modules 里面的东西我们不是很好去改造、就算采用 CDN 的形式来加载处理有些太麻烦。
micro-app 提供 plugin 的 loader:
你可以使用打补丁的形式来处理这种问题,例如在我的项目中:
import microApp from "@micro-zoe/micro-app";
const isDevelopment = process.env.NODE_ENV === "development";
/**
* 修复Popper.js在微应用中无法正确定位显示的问题
*
* @param {String} code
* @returns {String}
*/
export const popperPolyfill = (code) => {
if (isDevelopment) {
//开发环境:未压缩代码
return code.replace(
"this.popperJS.popper.style.zIndex-popup.PopuManager.nextzIndex();\n this.popperElm.addEventListener('click',stop); \n }",
"this.popperJS.popper.style.zIndex-popup.PopuManager.nextzIndex();\n this.popperElm.addEventListener('click',stop); \n this.$nextTick(()=>{this.updatePopper();}); }"
);
}
//生产环境:使用正则表达式匹配压缩后的代码(变量名可变)
//匹配模式:this.popperjS.popper.style.zIndex-<变量>.PopupManager.nextzIndex(),this.popperElmaddeventListener("click变量>)
const popperRegex =
/(this.popperJs._popper.style.zIndex-)(w+)(.PopupManager.nextZIndex(い),this.popperElm.addEventListener("click",)(\w+)())/g;
return code.replace(popperRegex, (match, p1, varName1, p3, varName2, p5) => {
return `${p1}${varName1}${p3}${varName2}${p5},this.$nextTick(()=>{this.updatePopper();});`;
});
};
microApp.start({
plugins: {
global: [
{
loader: popperPolyfill,
},
],
},
});
注意:你需要区分生产环境和本地环境。
由于生产环境的代码压缩后的、就会导致没出部署时、变量都会发生变化、最好使用表达式来做匹配
生产环境部署
生产环境的部署比开发环境要简单、比起文档的官方文档、使用 'disable-patch-request' 属性我觉得更简洁:
当基座访问 子应用 "localhost:8080" 时、子应用的请求服务会自动补全,例如:
const login = request("/api/login")
当子应用调用 "/api/login",会补全为 "localhost:8080/api/login"、这就会导致基座和子应用跨域了。
关闭自动补全:
microApp.start({
"disable-patch-request":true
})
在生成环境中修改 nginx 配置文件:
http {
# ...
server {
location ^~ /api {
# 如果是浏览器访问页面(Accept header 包含 text/html),返回前端路由
if ($http_accept ~* "text/html") {
rewrite ^/.* /index.html last;
}
proxy_pass http://localhost:8080/api;
break;
}
}
}