微前端 micro-App 经验总结

346 阅读4分钟

文章目的:本文主要是解决使用 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;
    }
  }
}