美文网首页
微前端——乾坤qiankun(小案例)

微前端——乾坤qiankun(小案例)

作者: 元宇宙编程 | 来源:发表于2023-03-02 17:38 被阅读0次
    一、什么是微前端

    微前端就是将不同的功能按照不同的维度拆分成多个子应用。通过主应用来加载这些子应用。微前端的核心在于拆,拆完后在合

    二、为什么使用微前端
    • 不同团队间开发同一个应用技术栈不同
    • 希望每个团队都可以独立开发,独立部署
    • 项目中还需要老的应用代码

    我们可以将一个应用划分成若干个子应用,将子应用打包成一个个的 lib 。当路径切换 时加载不同的子应用。这样每个子应用都是独立的,技术栈也不用做限制了!从而解决了前端协同开发问题。

    三、qiankun框架

    文档地址:https://qiankun.umijs.org/zh

    2018 年 Single-SPA 诞生了, single-spa 是一个用于前端微服务化的 JavaScript 前端解决方案 ( 本身没有处理样式隔离, js 执行隔离 ) 实现了路由劫持和应用加载。

    2019 年 qiankun 基于 Single-SPA, 提供了更加开箱即用的 API ( single-spa + sandbox + import-html-entry ) 做到了,技术栈无关、并且接入简单(像 i frame 一样简单)。

    四、qiankun框架实例

    这里我们打算建立三个项目进行实操,一个Vue项目充当主应用,另一个Vue和React应用充当子应用

    1、创建三个应用
    1)创建基座
    vue create qiankun-base
    
    2)创建子应用1
     vue create qiankun-vue
    
    3)创建子应用2
     cnpm install -g create-react-app
     create-react-app qiankun-react
    
    • 三个项目

    基座:qiankun-base 子应用:qiankun-vue、qiankun-react

    2、项目配置(主要)

    1)基座qiankun-base配置

    项目创建好后我们首先进行主应用qiankun-base的配置,进入man.js文件进行配置, 在main.js中加入以下代码,要注意的是,entry这项配置是我们两个子项目的域名和端口,我们必须确保两字子项目运行在这两个端口上面,container就是我们的容器名,就是我们子应用挂载的节点,相当于Vue项目里面的app节点,activeRule就是我们的激活路径,根据路径来显示不同的子应用。

    • 引入qiankun插件

      yarn add qiankun 或者 npm i qiankun -S
      
    • main.js配置

    // 引入qiankun

    import { registerMicroApps, start } from 'qiankun';
    const apps = [
     {
      name: 'vueApp', // 应用的名字
      entry: '//localhost:8081', // 默认会加载这个html 解析里面的js 动态的执行 (子应用必须支持跨域)fetch
      container: '#vue', // 容器名(此项目页面中定义的容器id,用于把对应的子应用放到此容器中)
      activeRule: '/vue', // 激活的路径
      props: { a: 1 } // 传递的值(可选)
     },
     {
      name: 'reactApp',
      entry: '//localhost:20000', // 默认会加载这个html 解析里面的js 动态的执行 (子应用必须支持跨域)fetch
      container: '#react',
      activeRule: '/react',
      }
    ]
    registerMicroApps(apps); // 注册应用
    start({
      prefetch: false // 取消预加载
    });// 开启
    
    • 配置完之后我们去到qiankun-base的app.vue文件进行主应用的页面编写,这里我安装了element-ui来进行页面美化

      npm i element-ui -S
      
    • 在main.js中引入element-ui:

      import ElementUI from 'element-ui';
      import 'element-ui/lib/theme-chalk/index.css';
      Vue.use(ElementUI);
      
    • 修改app.vue的组件代码

    <template>
      <div id="app">
       <el-menu :router="true" mode="horizontal"> 
        <!--基座中可以放自己的路由-->
        <el-menu-item index="/">Home</el-menu-item>
        <el-menu-item index="/about">About</el-menu-item>
         <!--引用其他子应用-->
         <el-menu-item index="/vue">vue应用</el-menu-item>
         <el-menu-item index="/react">react应用</el-menu-item>
        </el-menu>
       <router-view></router-view>
       <div id="vue"></div>
       <div id="react"></div>
      </div>
    </template>
    
    • router.js代码
     import Vue from 'vue'
     import VueRouter from 'vue-router'
     import Home from '../views/Home.vue'
     Vue.use(VueRouter)
     const routes = [
      {
          path: '/',
          name: 'Home',
          component: Home
      },
    {
        path: '/about',
        name: 'About',
        // route level code-splitting
        // this generates a separate chunk (about.[hash].js) for this route
        // which is lazy-loaded when the route is visited.
        component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
       }
     ]
    
     const router = new VueRouter({
       mode: 'history',
      // base: process.env.BASE_URL,
       base: '',
       routes
     })
    
     export default router
    

    2)子应用qiankun-vue配置

    • main.js配置
     import Vue from 'vue'
     import App from './App.vue'
     import router from './router'
    
    // Vue.config.productionTip = false
    
     let instance = null
     function render(props) {
      instance = new Vue({
        router,
        render: h => h(App)
       }).$mount('#qkApp'); // 这里是挂载到自己的html中  基座会拿到这个挂载后的html 将其插入进去
     }
    
    if (window.__POWERED_BY_QIANKUN__) { // 动态添加publicPath
        __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
     }
    if (!window.__POWERED_BY_QIANKUN__) { // 默认独立运行
       render();
     }
    
     // 父应用加载子应用,子应用必须暴露三个接口:bootstrap、mount、unmount
      // 子组件的协议就ok了
    export async function bootstrap(props) {
    
     };
    
     export async function mount(props) {
          render(props)
     }
    
     export async function unmount(props) {
       instance.$destroy();
     }
    
    • router.js配置
    import Vue from 'vue'
    import VueRouter from 'vue-router'
    import Home from '../views/Home.vue'
    
    Vue.use(VueRouter)
    const routes = [
     {
      path: '/',
      name: 'Home',
      component: Home
     },
    {
      path: '/about',
      name: 'About',
      component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
     }
    ]
    
    const router = new VueRouter({
     mode: 'history',
     base: '/vue',
     routes
    })
    
    export default router
    
    • Vue.config.js配置

    在子应用的根目录下面新建一个Vue.config.js文件

    module.exports = {
    lintOnSave: false,  // 关闭eslint检测
    devServer: {
        port: 8080,//这里的端口是必须和父应用配置的子应用端口一致
        headers: {
            //因为qiankun内部请求都是fetch来请求资源,所以子应用必须允许跨域
            'Access-Control-Allow-Origin': '*'
        }
    },
    configureWebpack: {
        output: {
            //资源打包路径
            library: 'vueApp',
            libraryTarget: 'umd'
          }
       }
    }
    
    3)子应用qiankun-react配置
    • src目录下index.js文件

       import React from 'react';
       import ReactDOM from 'react-dom';
       import './index.css';
       import App from './App';
      
       function render(){
        ReactDOM.render(
         <React.StrictMode>
         <App />
        </React.StrictMode>,
         document.getElementById('root')
      );
      }
      
      if(!window.__POWERED_BY_QIANKUN__){
        render();
      }
      
       export async function bootstrap(){
      
       }
      export async function mount() {
        render()
      }
       export async function unmount(){
         ReactDOM.unmountComponentAtNode( document.getElementById('root'));  // 卸载节点
       }
      
    • config-overrides.js配置
      先引入react-app-rewired,在修改package.json启动命令

      npm install react-app-rewired
       修改package.json启动命令
      
       "scripts": {
        "start": "react-app-rewired start",
        "build": "react-app-rewired build",
        "test": "react-app-rewired test",
        "eject": "react-app-rewired eject"
       },
      
    • 再进行dev以及打包的配置,根目录下创建config-overrides.js

      module.exports = {
        webpack: (config) => {
        config.output.library = 'reactApp';
        config.output.libraryTarget = 'umd';
        config.output.publicPath = 'http://localhost:20000/';   // 此应用自己的端口号
        return config;
       },
       devServer: (configFunction) => {
        return function (proxy, allowedHost) {
            const config = configFunction(proxy, allowedHost);
            config.headers = {
                "Access-Control-Allow-Origin": '*'
            }
            return config
         }
       }
      }
      
    3、注意点

    1)如何在主应用的某个路由页面加载微应用

    react + react-router 技术栈的主应用:只需要让子应用的 activeRule 包含主应用的这个路由即可。

    vue +vue-router 技术栈的主应用:

    例如:主应用需要在login页面登录,登录成功后跳转到main后台管理界面,在main管理界面下可以显示子应用。

    修改主应用router.js:

    // 如果这个路由有其他子路由,需要另外注册一个路由,任然使用这个组件即可。
    // 本案例就是有子路由,所以需要才后面重新定义main页面的路由

     const routes = [
    {
        path: '/',
        name: 'Login',
        component: Login
    },
    {
        path: '/main',
        name: 'Main',
        component: Main,
        children: [
            {
                path: '/home',
                name: 'Home',
                component: Home
            },
            {
                path: '/about',
                name: 'About',
                // route level code-splitting
                // this generates a separate chunk (about.[hash].js) for this route
                // which is lazy-loaded when the route is visited.
                component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
            }
        ]
     },
     {
        path: '/main/*',
        name: 'Main',
        component: Main,
      }
    ]
    
    • 修改主应用main.js的文件:

        // 子应用的 activeRule 需要包含主应用的这个路由 path
      const apps = [
       {
        name: 'vueApp', // 应用的名字
        entry: '//localhost:8081', // 默认会加载这个html 解析里面的js 动态的执行 (子应用必须支持跨域)fetch
        container: '#vue', // 容器名
        activeRule: '/main/vue', // 激活的路径
       props: { a: 1 }
      },
      {
        name: 'reactApp',
        entry: '//localhost:20000', // 默认会加载这个html 解析里面的js 动态的执行 (子应用必须支持跨域)fetch
        container: '#react',
        activeRule: '/main/react',
         }
      ]
      registerMicroApps(apps); // 注册应用
      
    • 修改主应用main.vue页面代码:

     // 在 Main.vue 这个组件的 mounted 周期调用 start 函数,注意不要重复调用。
     <template>
      <div class="main-content">
       <el-menu :router="true" mode="horizontal">
      <!--基座中可以放自己的路由-->
      <el-menu-item index="/home">Home</el-menu-item>
      <el-menu-item index="/about">About</el-menu-item>
      <!--引用其他子应用-->
      <el-menu-item index="/main/vue">vue应用</el-menu-item>
      <el-menu-item index="/main/react">react应用</el-menu-item>
      </el-menu>
       <router-view></router-view>
      <div id="vue"></div>
      <div id="react"></div>
     </div>
    </template>
    
     <script>
     import { start } from "qiankun";
    
    export default {
      name: "Main",
      mounted() {
       if (!window.qiankunStarted) {
          window.qiankunStarted = true;
          start();
       }
     },
    };
    </script>
    
    常见问题
    1、资源404

    用绝对路径,不用用相对路径,例如

     <img src="./img/logo.jpg">
      <!-- 改为 -->
     <img src="/img/logo.jpg">
    
    2、css污染

    qiankun 只能解决子项目之间的样式相互污染,不能解决子项目的样式污染主项目的样式
    冲突的样式,采用BEM命名方式

    3、不使用position: fixed
    4、给 body 、 document 等绑定的事件,请在 unmount 周期清除
    5、子应用router.js设置base
    6、子应用中的route信息里不要有''或者'*'之类的判空。
    7、子项目之间跳转传入父项目路由对象提供调用
    8、父子项目通讯:
     // 主项目初始化
     import { initGlobalState } from 'qiankun';
     const actions = initGlobalState(state);
    // 主项目项目监听和修改
    actions.onGlobalStateChange((state, prev) => {
     // state: 变更后的状态; prev 变更前的状态
     console.log(state, prev);
    });
    
    actions.setGlobalState(state);
    
    // 子项目监听和修改
    export function mount(props) {
      props.onGlobalStateChange((state, prev) => {
       // state: 变更后的状态; prev 变更前的状态
       console.log(state, prev);
      });
       props.setGlobalState(state);
     }
    
    9、销毁子项目
    export async function unmount() {
      instance.$destroy();
      instance.$el.innerHTML = ""; //新增这一行代码
      instance = null;
      router = null;
    }
    
    10、更新子项目

    子应用,需要增加 update 钩子以便主应用手动更新微应用

    export async function update(props) {
      render(props);
     }
    

    主应用,直接调用子应用实例的 update 方法即可

    this.microApp.update({
      store: {
          name: 'licc',
          age: 18,
       }
    });
    
    11、手动 加载/卸载 微应用
    import { loadMicroApp } from 'qiankun';
    
    // 因为loadMicroApp()返回子应用的实例,拿一个全局变量接收后续可进行其他操作如:手动卸载子应用
    this.microApp = loadMicroApp(
     {
    name: 'sub-vue',
    entry: 'http://localhost:7777/subapp/sub-vue',
    container: '#apassMicroTemplateConfig',
    props: {
      routerBase: '/index/config/template/edit',
      getGlobalState: microAppStore.getGlobalState,
      sheetId: '2133123123'
       }
    },
    {
    // sandbox - boolean | { strictStyleIsolation?: boolean, experimentalStyleIsolation?: boolean } - 可选,是否开启沙箱,默认为 true
    sandbox: { strictStyleIsolation: true },
    // singular - boolean | ((app: RegistrableApp<any>) => Promise<boolean>); - 可选,是否为单实例场景,单实例指的是同一时间只会渲染一个微应用。默认为 false
    singular: true
     }
    )
    
     // 封装卸载子应用的函数
    private unmountMicroApp () {
     if (this.microApp) {
      this.microApp.mountPromise.then(() => {
       this.microApp.unmount()
        })
       }
    }

    相关文章

      网友评论

          本文标题:微前端——乾坤qiankun(小案例)

          本文链接:https://www.haomeiwen.com/subject/odszqdtx.html