qiankun搭建

222 阅读8分钟

背景

优势

  • 无技术栈限制:主框架不限制接入应用的技术栈,不论是 React/Vue/Angular/JQuery 还是其他等框架。

  • 子应用具备完全自主权

    • 独立开发,独立部署,子应用的仓库独立,部署完成后主框架自动完成同步更新

    • 独立运行时,每个子应用之间状态隔离,运行时状态不共享

      • CSS 样式隔离
      • Javascript 沙箱隔离
  • 重复依赖,不同应用之间依赖的包存在很多重复,由于各应用独立开发、编译和发布,难免会存在重复依赖的情况。导致不同应用之间需要重复下载依赖,额外再增加了流量和服务端压力

  • 增量升级,在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略

  • 更快交付客户价值,有助于持续集成、持续部署以及持续交付;

  • 维护和 bugfix 非常简单,每个团队都熟悉所维护特定的区域;

场景

  • 单实例:即同一时刻,只要一个子应用被展示,子应用具备一个完整的应用生命周期,通常基于url的变化来做子应用的切换
  • 多实例:同一时刻可展示多个子应用。通常使用web components方案来做子应用封装,子应用更像是一个业务组件而不是应用

区别

  • iframe的优缺点

    • 优点

      • 完全隔离了css和js,避免了各个系统之间的样式和js污染
      • 可以在子系统完全不修改的情况下嵌入进来
    • 缺点

      • 页面加载问题: 影响主页面加载,阻塞onload事件,本身加载也很慢,页面缓存过多会导致电脑卡顿。(无法解决)

      • 布局问题:

        • 出现多个滚动条,用户体验不佳。
        • 弹窗及遮罩层问题:只能在iframe范围内垂直水平居中,没法在整个页面垂直水平居中。
      • 浏览器前进/后退问题:iframe和主页面共用一个浏览历史

        • 刷新页面,iframe会重置(比如说从列表页跳转到详情页,然后刷新,会返回到列表页),因为iframe的地址没有变化
      • 跨域

        解决方法:

        后端代理:通过服务器端设置代理,请求目标网页内容,然后将内容返回给前端,实现跨域访问。 CORS:如果你控制了被嵌入页面的服务器,可以通过设置CORS(跨源资源共享)策略来允许特定的跨域请求。 Window.postMessage:使用window.postMessage方法进行跨域通信。这是一种安全的方式来实现跨源通信。

      • 不同源的系统之间的通讯需要通过postMessage,存在一定的安全性

      • 搜索引擎的检索程序无法解读这种页面,不利于SEO

  • single-spa 是一个用于前端微服务化的 javascript 前端解决方案,实现了路由的劫持和应用加载

    • 缺点

      • 无通信机制
      • 不支持 Javascript 沙箱
      • 样式冲突
      • 无法预加载
  • qiankun

    • 优点

      • 📦基于 single-spa 封装,提供了更加开箱即用的 API。
      • 💪 HTML Entry 接入方式,让你接入微应用像使用 iframe 一样简单。
      • 🛡 样式隔离,确保微应用之间样式互相不干扰。
      • 🧳 JS 沙箱,确保微应用之间 全局变量/事件 不冲突。
      • ⚡️ 资源预加载,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。
      • 🔌 umi 插件,提供了 @umijs/plugin-qiankun 供 umi 应用一键切换成微前端架构系统。
      • 🔥 社区较为活跃,维护者也较多,有问题会及时得到响应;

qiankun

juejin.cn/post/684490…

juejin.cn/post/685656…

cloud.tencent.com/developer/a…

nginx部署配置

目的:多项目背景下,在根目录创建qiankun文件夹

 └── html/                     # 根文件夹
     |
     ├── qiankun/                # 存放所有qiankun的项目文件夹
     |   |── child/                # 存放所有微应用的文件夹
     |   |   ├── vue-hash/         # 存放微应用 vue-hash 的文件夹
     |   |   ├── vue-history/      # 存放微应用 vue-history 的文件夹
     |   |   ├── react-hash/       # 存放微应用 react-hash 的文件夹
     |   |   ├── react-history/    # 存放微应用 react-history 的文件夹
     |   |   ├── angular-hash/     # 存放微应用 angular-hash 的文件夹
     |   |   ├── angular-history/  # 存放微应用 angular-history 的文件夹
     |   |── index.html            # 主应用的index.html
     |   |── css/                  # 主应用的css文件夹
     |   |── js/                   # 主应用的js文件夹

使用history路由模式下的nginx配置

 location / {
   root   html;
   index  index.html index.htm;
   try_files $uri $uri/ /qiankun/index.html; //资源重定向
 }
 location /qiankun {
   try_files $uri $uri/  /qiankun/index.html;//qiankun项目
 }
 location /vue2-mobile {
   try_files $uri $uri/ /qiankun/child/vue2-mobile/index.html;//这里将qiankun里的微应用作为单一项目
 }

搭建

主应用(react)

  • 下载qiankun

  • react主应用创建:通过create-react-app脚手架创建模板

  • 注册微应用

     //main.js
     registerMicroApps(
       [
         {
           name: "vue2-mobile", 
           entry: isDev ? "//localhost:8001" : '/qiankun/child/vue2-mobile/',//配置微应用访问入口,注意微应用的 entry 路径最后面的 / 不可省略,否则 publicPath 会设置错误
           container: "#vue2-mobile", //容器
           activeRule: "/qiankun/vue2-mobile", //访问路径
           // activeRule: getActiveRule('#/vue2-mobile'),
         },
       ],
       {
         beforeLoad: (app) => {
           console.log("before load app.name====>>>>>", app.name);
         },
         beforeMount: [
           (app) => {
             console.log("[LifeCycle] before mount %c%s", "color: green;", app.name);
           },
         ],
         afterMount: [
           (app) => {
             console.log("[LifeCycle] after mount %c%s", "color: green;", app.name);
           },
         ],
         afterUnmount: [
           (app) => {
             console.log(
               "[LifeCycle] after unmount %c%s",
               "color: green;",
               app.name
             );
           },
         ],
       }
     );
     //当微应用信息注册完之后,一旦浏览器的 url 发生变化,便会自动触发 qiankun 的匹配逻辑,
     //所有 activeRule 规则匹配上的微应用就会被插入到指定的 container 中,同时依次调用微应用暴露出的生命周期钩子。
     start();
    
  • 路由

     //main.js
     const root = ReactDOM.createRoot(document.getElementById("root"));
     root.render(
       <React.StrictMode>
         <RouterProvider router={router}>
           <App />
         </RouterProvider>
       </React.StrictMode>
     );
     ​
     //router
     import { createBrowserRouter, RouterProvider } from "react-router-dom";
     import Web1 from "../page/web1.jsx";
     export const router = createBrowserRouter([
       {
         path: "/qiankun",
         element: <Home></Home>,
         children: [
           {
             path: "vue2-mobile",
             element: <Web1></Web1>,
             children: [
               {
                 path: "learn",
               },
               {
                 path: "user",
               }, 
               {
                 path: "login",
               },
             ],
           },
           {
             path: "vue2-pc",
             element: <Web2></Web2>,
             children: [
               {
                 path: "login",
               },
               {
                 path: "user",
               },
             ],
           },
         ]
       },
       // {
       //   path: '/:catchAll(.*)*',
       //   // name: 'error',
       //   // meta: {
       //   //   name: '404',
       //   // },
       //   component: () => import('../page/404.jsx'),
       // }
     ]);
     ​
     //page/Home
     //TODO:这里容器必须设置在父路径上
     function Home() {
       return (
          <Outlet></Outlet>
       );
     }
     export default Home;
     ​
     //page/web1.jsx
     function Web1() {
       return (
         <div>
           <div id="vue2-mobile" style={{ width: "100%", height: "100%" }}></div>
         </div>
       );
     }
     ​
     export default Web1;
     ​
    

微应用

vue2

  • vue.config.js

     devServer: {
       headers: {
       "Access-Control-Allow-Origin": "*",
       },
     },
     configureWebpack: {
       output: {
       library: `${name}-[name]`,
       libraryTarget: "umd", // 把微应用打包成 umd 库格式
       jsonpFunction: `webpackJsonp_${name}`, // webpack 5 需要把 jsonpFunction 替换成 chunkLoadingGlobal
       },
     },
    
  • public-path.js

     if (window.__POWERED_BY_QIANKUN__) {
       __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
     }
    
  • main.js

     import './public-path';
     ​
     let instance = null;
     function render(props = {}) {
       const { container } = props;
       instance = new Vue({
         router,
         store,
         render: (h) => h(App),
       }).$mount(container ? container.querySelector("#app") : "#app");
       console.log(instance);
     }
     // 独立运行时
     if (!window.__POWERED_BY_QIANKUN__) {
       render();
     }
     ​
     /**
      * bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
      * 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
      */
     export async function bootstrap() {
       console.log("bootstrap");
     }
     ​
     /**
      * 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
      */
     export async function mount(props) {
       console.log("[vue] props from main framework", props);
       render(props);
     }
     ​
     /**
      * 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
      */
     export async function unmount() {
       console.log("bootstrap unmount");
       instance.$destroy();
     }
     ​
    
  • 路由

     const router = new VueRouter({
       mode: "history",
       base: window.__POWERED_BY_QIANKUN__
         ? "/qiankun/vue2-mobile/" //配置子应用的路由根路径
         : isProd
           ? "/vue2-mobile/"//单一项目下的访问路径
           : process.env.BASE_URL,
       routes,
       scrollBehavior(to, from, savedPosition) {
         if (to.hash) {
           return {
             selector: to.hash,
           };
         }
       },
     });
    

vue3+vite

参考:blog.csdn.net/weixin_4754…

使用vite-plugin-qiankun插件

  • 路由

     import { qiankunWindow } from 'vite-plugin-qiankun/dist/helper'
     const isProd = process.env.NODE_ENV === 'production'
     export const history = createWebHistory(
       qiankunWindow.__POWERED_BY_QIANKUN__
         ? '/qiankun/vue3' //配置子应用的路由根路径
         : isProd
           ? '/vue3/' //单一项目下的访问路径
           : process.env.BASE_URL
     )
    
  • main.ts

     import './assets/main.css'
     import './public-path'
     import { createApp } from 'vue'
     import { createPinia } from 'pinia'
     ​
     import App from './App.vue'
     import router from './router'
     import { history } from './router'
     import {
       renderWithQiankun,
       qiankunWindow,
       type QiankunProps
     } from 'vite-plugin-qiankun/dist/helper'
     ​
     let instance: any = null
     function render(props: any = {}) {
       const { container } = props
       instance = createApp(App)
       instance.use(createPinia())
       instance.use(router)
       instance.use(router)
       instance.mount(container ? container.querySelector('#vue3-page') : '#vue3-page')
     ​
     }
     ​
     // 独立运行时
     if (!qiankunWindow.__POWERED_BY_QIANKUN__) {
       render()
     } else {
       renderWithQiankun({
         mount(props) {
           console.log('viteapp mount')
           render(props)
         },
         bootstrap() {
           console.log('bootstrap')
         },
         unmount(props) {
           console.log('vite被卸载了')
           instance.unmount(document.querySelector('#vue3-page'))
           instance._container.innerHTML = ''
           history.destroy() // 不卸载  router 会导致其他应用路由失败
           instance = null
         },
         update: function (props: QiankunProps): void | Promise<void> {
           throw new Error('Function not implemented.')
         }
       })
     }
     ​
    

打包部署

注意事项

部署之后注意三点:

  1. activeRule 不能和微应用的真实访问路径一样,否则在主应用页面刷新会直接变成微应用页面。
  2. 微应用的真实访问路径就是微应用的 entryentry 可以为相对路径。
  3. 微应用的 entry 路径最后面的 / 不可省略,否则 publicPath 会设置错误,例如子项的访问路径是 http://localhost:8080/app1,那么 entry 就是 http://localhost:8080/app1/

打包

主应用
  • main.js

        {
          name: "vue2-mobile", 
          entry: isDev ? "//localhost:8001" : '/qiankun/child/vue2-mobile/',//配置微应用访问入口,注意微应用的 entry 路径最后面的 / 不可省略,否则 publicPath 会设置错误
          container: "#vue2-mobile", //容器
          activeRule: "/qiankun/vue2-mobile", //访问路径
          // activeRule: getActiveRule('#/vue2-mobile'),
        },
    
  • webpack

    output: {
      path: path.resolve(__dirname, "./dist"),
      filename: "js/[name].[hash].js", //输出文件名
      publicPath: isDev ? '/' : '/qiankun', //资源访问根路径
    },
    
微应用
  1. vue2

    • publicPath: isProd ? "/qiankun/child/vue2-mobile/" : "/",

问题

[qiankun]: Target container with #app not existed while app1 loading!

需要在主应用 配置 子应用的路由页面,配置子应用容器id为web1

  {
    name: "Web1", // app name registered
    entry: "//localhost:8088",
    container: "#web1",
    activeRule: "/Web1",
  },

You need to export lifecycle functions in Web1 entry

devServer: {
  headers: {
  "Access-Control-Allow-Origin": "*",
  },
},
configureWebpack: {
  output: {
  library: `${name}-[name]`,
  libraryTarget: "umd", // 把微应用打包成 umd 库格式
  jsonpFunction: `webpackJsonp_${name}`, // webpack 5 需要把 jsonpFunction 替换成 chunkLoadingGlobal
  },
},

接口请求404

juejin.cn/post/690371…

微前端架构

主应用代理子应用ip

     proxy: [
        {
          context: ['/vue2-mobile/api'],
          target: 'http://localhost:8001',
          // changeOrigin: true,
          // secure: false,
          // xfwd: false,
          // pathRewrite: { '^/api': '' }  //重点:重写资源访问路径,避免转发请求 404问题
        }, {
          context: ['/vue2-pc/api'],
          target: 'http://localhost:8002',
        }
      ]

子应用代理后端接口

    proxy: {
      '/vue2-px/api': {
        target: 'http://lm-web.top:3600/',
        changeOrigin: true,
        secure: false,
        xfwd: false,
      },
    },

api

umd

cloud.tencent.com/developer/a…

UMD的全称是Universal Module Definition ,也就是通用模块标准,它的目标是使一个模块能运行在各种环境下,不论是CommonJSAMDES6 Module或者非模块化环境都能正确引入到库文件

registerMicroApps

参数解析: registerMicroApps(apps, lifeCycles?)

  • apps - Array - 微应用的注册信息
  1. name -String - 微应用的名称,必须确保唯一 - 【必选】
  2. entry - String - 微应用的入口 - 【必选】
  3. container - String | HTMLElement - 微应用的容器节点的选择器或者Element实例 - 【必选】
  4. acticeRule - String | (location:Location) => boolean | Array | (location:Location) => boolean - 微应用的激活规则 - 【必选】

loadMicroApp 手动加载微应用

 import { loadMicroApp } from 'qiankun';
 ​
 loadMicroApp({
   name: 'micro-clouds',
   entry: '//localhost:7000',
   activeRule: '/micro-clouds',
   container: '#subapp2', // 子应用挂载的div
   // 传递给子应用的参数
   props: {
     routerBase: '/micro-clouds',
   }
 });
 ​