手把手带你上车:qiankun 微前端从应用到部署的不翻车指南

141 阅读9分钟

引言

hello,当你看到这篇文章时可能你对微前端已经有个大概的了解了,我就分享下自己对于微前端qiankun技术架构在实际工程中从技术预研、项目应用到部署落地的整个过程,如果能够给你提供一些帮助那就再好不过了。

本文包含以下内容:

业务场景

微前端概念及优势特点

微前端相比于iframe的优势(为什么不是用iframe)

主应用配置

子应用配置

如何部署

实现原理

js/css沙箱

业务场景

说说我们团队是基于什么样的业务场景使用这个技术架构的吧:

  • 子系统由不同团队开发维护的,使用技术栈及开发规范不统一,并且是分开部署的。
  • 要将子系统集成在一个平台应用展示、仍支持子系统单独访问,或者子系统任意组合成应用。(例如A B C D 四个子系统可以单独访问,也支持ABC组合成一个应用)
  • 后续追加集成子系统是渐进式、无感知的。不要影响当前已完成集成的这个平台应用。

什么是微前端

微前端是一种前端架构模式,是一种多个团队通过独立发布功能的方式(独立仓库、独立开发、测试、部署和维护)来共同构建现代化 web 应用的技术手段及方法策略。

微前端有什么优势特点

  • 一个复杂庞大的项目拆成多个微应用,独立仓库、独立开发、测试、部署和维护,互不影响。
  • 跟技术栈无关,允许使用不同的前端技术栈(如 React、Vue、Angular 等)进行开发,团队可以根据自身技术优势选择合适的工具。
  • 多个应用结合在一起,可以一起运行,又可以单独运行,每个微应用之间状态隔离,运行时状态不共享。
  • 渐进式重构与升级:适用于需要逐步迁移旧系统的场景,通过将老旧模块逐步替换为新的微前端模块,实现平滑过渡

理论上通过微前端实现的功能通过iframe嵌入也能实现,我们在这两种技术之间也做过权衡。

微前端相比于iframe的优势(为什么不是用iframe)

  1. 适用场景
  • 微前端:适用于大型企业级应用、多团队协作、技术栈异构、渐进式重构等场景。
  • iframe:适用于简单的内容嵌入、第三方服务集成等场景。
  1. 隔离性与安全性
  • 微前端:通过 JavaScript 沙箱、Web Components 等技术实现隔离,同时提供更灵活的通信机制。
  • iframe:提供天然的隔离机制,但可能存在安全风险,例如点击劫持和恶意脚本注入。
  1. 通信机制
  • 微前端:提供丰富的通信机制,支持父子应用及子子应用之间的通信。
  • iframe:通信方式较为有限,通常依赖 postMessage 或 URL 参数。
  1. 性能
  • 微前端:支持按需加载、资源预加载和懒加载,优化了页面加载性能。
  • iframe:每个 iframe 都需要独立加载完整的文档和资源,会增加页面的加载时间和网络请求次数,性能开销较大。
  1. 用户体验
  • 微前端:可以实现更流畅的用户体验,子应用之间的切换更加自然。
  • iframe:可能会导致页面布局混乱、滚动条问题,影响用户体验。
  1. 开发与维护
  • 微前端:支持多框架共存,允许团队独立开发和部署子应用,维护成本较低。
  • iframe:虽然集成简单,但需要处理跨域通信、SEO 优化等问题。
  1. SEO 友好性
  • 微前端:对 SEO 更友好,搜索引擎更容易解析页面内容。
  • iframe:搜索引擎对 iframe 中的内容索引和排名处理较为复杂,可能影响 SEO。

接下来我就讲讲实际项目中的应用,跟着我一步一步来,不要错过任何细节哦!

主应用配置

  • 安装qiankun
yarn add qiankun
  • 定义子应用挂载节点
//注意id跟下面注册配置的container保持一致哦
 <div id="micro-app-container"></div>

  • 注册子应用
//mains.ts
import { registerMicroApps, start } from "qiankun";

registerMicroApps([
  //当匹配到activeRule的时候,请求entry的资源,渲染到container
  {
    name: "qiankun-vue3-app",//子应用1
    entry: '//localhost:3021',//本地承载应用资源的服务路径
    <!-- entry: '/qk-app/vue-app/',//生产则使用服务器路径(要通过nginx进行代理) -->
    <!-- entry: process.env.MICRO_APP_VUE, //所以这里建议使用环境变量 -->
    container: "#micro-app-container",//绑定挂载节点
    activeRule: "/#/sub-app/micro-app-vue",//这里可以理解为路由,
    <!--注意 注意 注意!!!>
    <!--如果部署到服务器nginx站点资源非根路径下。activeRule需要调整后面部署时详细描述。-->
    <!--该问题见社区讨论:https://github.com/umijs/qiankun/discussions/2858 >
    props: {
      //给子应用传递数据
      nickName: "shier",
      shared,
      appId: "/sub-app/micro-app-vue",
    },
  },
  {
    name: "qiankun-react-app", //子应用2
    entry: '//localhost:3022',
    container: "#micro-app-container",
    activeRule: "/#/sub-app/qiankun-react-app",
    props: {
      nickName: "shier",
      appId: "/sub-app/micro-react-vue",
      shared,
    },
  }
])
//样式隔离
start({
  prefetch: true, // 预加载子资源
  sandbox: {
    experimentalStyleIsolation: true// 实验性沙箱(会导致弹窗异常)
  }
})

子应用配置

  • src目录下创建public-path.js文件
/src/public-path.ts
if ((window as any).__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = (window as any).__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
  • 引入public-path.js文件(==注意,要在main.ts文件最顶部引入==)
//main.ts  //注意,要在文件最顶部引入
import "./public-path";
  • 配置生命周期
//main.ts
let instance: any = null;
function render(props: any = {}) {
  (window as any).G = props
  const { container } = props;
  // 当传入的 shared 为空时,使用子应用自身的 shared
  // 当传入的 shared 不为空时,主应用传入的 shared 将会重载子应用的 shared
  // 建议:只在存在复杂状态且需要子应用自行维护状态池才使用shared
  const { shared = SharedModule.getShared() } = props;
  SharedModule.overloadShared(shared);
  instance = createApp(App);
  instance.use(store).use(router).use(registerApp).mount(container ? container.querySelector('#app') : '#app');
}
// 如果是单独启动的子文件,保证仍能正常运行
if (!(window as any).__POWERED_BY_QIANKUN__) {
  render()
}
export async function bootstrap() {
  console.log('[vue] vue app bootstraped');
}
export async function mount(props: any) {
  render(props);
}
export async function unmount() {
  instance.unmount()
  instance._container.innerHTML = ''
  instance = null
}
//vue2
<!-- export async function unmount() {
  instance.$destroy()
  instance.$el.innerHTML = ''
  instance = null
} -->
  • vue.config.js
const { name } = require('./package.json')
const dev = process.env.NODE_ENV === "development";
//qiankun环境部署
  publicPath: dev ? `//localhost:3020` : "/qk-app/vue-app/"//服务器部署的文件夹名, 
  devServer: {
    //配置端口,与主应用配置的entry保持一致
    port: "3020",
    //解决跨域
    headers: {
      'Access-Control-Allow-Origin': '*',
    },
  },
  configureWebpack: {
    output: {
      // 输出重构 打包编译后的js文件名称,添加时间戳.不配置识别不到生命周期
      library: `${name}-[name]`,
      libraryTarget: "umd", // 把微应用打包成 umd 库格式
      jsonpFunction: `webpackJsonp_${name}`,
    }
  } 
  • 配置路由前缀 ==注意:路由携带参数,最好使用router.push({name:"",query:{}})==
//router/index.ts
let prefix: any = "";
// 判断是 qiankun 环境则增加路由前缀,路由前缀要跟注册子应用的activeRule对应
if ((window as any).__POWERED_BY_QIANKUN__) {
  // prefix = (window as any).G.appId
  prefix = '/sub-app/micro-app-vue' 
}
//所有静态路由加上prefix前缀
const routes = [
  {
    path: prefix + "/",
    name: "index",
    component: () =>
      import(/* webpackChunkName: "user" */ "../layouts/BasicLayout.vue"),
    children: []
  }
  ...
 ] 
//路由拦截适配qiankun
if ((window as any).__POWERED_BY_QIANKUN__) {
    if (to.path === prefix + "/init") {
      next();
    } else {
      if (to.path.includes(prefix)) {
        next();
      } else {
        next({ path: prefix + to.path });
      }
    }
} else {
   //项目中原来的路由拦截代码
}

部署

--html---------------根文件夹
  --main-app --------主应用
      --css
      --img
      --js
      --index.html
  --vue-app ---------子应用1
  --react-app--------子应用2
  • 附:nginx配置
<!----为什么这里不是 location / ,因为通常多个项目会进行目录划分,不会直接资源文件放在根路径下>
        location /qk-app {
            alias  /mdata/web/main-app/;
            index  index.html index.htm;
        }
        location /qk-app/vue-app {
            alias /mdata/web/vue-app/;
            index  index.html index.htm;
            try_files $uri $uri/ /index.html;
        }
        location /qk-app/react-app{
            alias /mdata/web/react-app/;
            index  index.html index.htm;
            try_files $uri $uri/ /index.html;
        }

注意:由于子部署在二级目录,注册时activeRule要做以下调整

{
   name: "vue-app",
   entry: "/qk-app/vue-app/",
   container: "#micro-app-container",
   <!---如果由于部署层级问题主应用代理路径为/qk-app,那么在activeRule也要加上前缀->
   activeRule: ”/qk-app/#/sub-app/micro-app-vue 
   props: {
   },
 }

==这里再次强调:注册应用时的entry、vue.config.js中的publicpath、nginx配置中的location三者保持一致==

实现原理

Qiankun 继承了 single-spa 的特点,通过监听 hashChange 和 popState 事件来检测路由变化。在路由变化时,Qiankun 会匹配到需要渲染的子应用,并加载其 HTML。如果缓存中存在,则直接返回;否则通过 fetch 函数下载 HTML 入口(即HTML Entry),对于 js/css/img/video 等资源都是相对路径,通常做法是动态修改 webpack 打包的 publicPath,然后就可以自动注入前缀给这些资源。

加载子应用的核心流程

  • 解析子应用的入口(Entry):

如果子应用使用 HTML Entry,Qiankun 会请求子应用的 HTML 文件,并解析其中的 script 和 link 标签,提取 JS 和 CSS 资源。如果子应用使用 JS Entry,Qiankun 会直接加载指定的 JS 文件。

  • 加载子应用的资源:

根据解析出的资源 URL,动态加载子应用的 JS 和 CSS 文件。

  • 执行子应用的 JS 代码:

在 JS 沙箱中执行子应用的 JS 代码,确保子应用的全局变量不会污染主应用。

  • 挂载子应用:

调用子应用的 bootstrap 和 mount 生命周期钩子,将子应用的 DOM 挂载到主应用指定的容器中。

js沙箱机制

ProxySandbox:通过 JavaScript 的 Proxy 对象拦截对全局对象的修改,确保子应用的修改不会影响其他应用。 SnapshotSandbox:在子应用加载前后对全局对象进行快照,确保子应用卸载后可以恢复到原始状态。

css沙箱

动态样式隔离:实现原理是当子应用被加载时,其对应的样式会被注入到页面中;当子应用被卸载时,qiankun 会自动移除其样式,确保页面的样式环境保持干净

影子DOM沙箱(Shadow DOM):手动开启 ,qiankun 会为每个微应用的容器包裹上一个 shadow dom 节点,从而确保微应用的样式不会对全局造成影响

作用域沙箱(Scope CSS):手动开启 ,qiankun 会改写子应用所添加的样式,为所有样式规则增加一个特殊的选择器规则来限定其影响范围

最后:有什么问题欢迎指出交流。