微前端小试牛刀 - qiankun实战

本文是小编之前在其他平台发表的,现迁移至csdn!!!

废话不多说,实战搞起

1.在主应用(基座)中安装qiankun

npm install qiankun

2.安装tmvc-base依赖

npm install tmvc-base

3.主应用注册子应用App

src/micro-app.js
const microApps = [
  {
    name: 'vue',
    entry: '//10.201.1.18:7301/vue',
    activeRule: '/vue',
  },
  {
    name: 'vue3',
    entry: '//10.201.1.18:7305/vue3',
    activeRule: '/vue3',
  },
  {
    name: 'vue3Vite',
    entry: '//10.201.1.18:7316/vue3Vite',
    activeRule: '/vue3Vite',
  },
]

const apps = microApps.map((item) => {
  return {
    ...item,
    container: '#cnbi-viewport', // 子应用挂载的div
    props: {
      routerBase: item.activeRule, // 下发基础路由
      getGlobalState: store.getGlobalState, // 下发getGlobalState方法
    },
  }
})

export default apps

4.主应用main.js中

import microApps from "./micro-app.js";
const apps = microApps.map(item => ({
    ...item
}));
registerMicroApps(apps, {
    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);
        }
    ]
});
//默认加载的子应用
setDefaultMountApp("/vue");
//启动
start({
    prefetch: false,
    sandbox: {
    //   strictStyleIsolation: true, // 开启严格的样式隔离模式
      experimentalStyleIsolation: true
    }
 });

5.子应用挂载点App.vue中

<template>
    <div class="layout-wrapper">
        <router-view />
        <!-- 可以放到这里,也可以放到对应子路由的页面里面位置不重要 -->
        <div id="cnbi-viewport"></div>
    </div>
</template>

子应用改造,vue2项目
1.src/public-path.js,在main.js导入

if (window.__POWERED_BY_QIANKUN__) {
  // eslint-disable-next-line no-undef
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

2.子应用main.js

let router = null;
let instance = null;

function render(props = {}) {
  const { container } = props;
  router = new VueRouter({
    base: window.__POWERED_BY_QIANKUN__ ? '/vue' : '/',
    mode: 'history',
    routes
  });

  instance = new Vue({
    router,
    store,
    render: (h) => h(App)
  }).$mount(container ? container.querySelector('#app') : '#app');
}

//判断其是否作为qiankun子应用使用
if (!window.__POWERED_BY_QIANKUN__) {
  render();
}

export async function bootstrap() {
  console.log('[vue] vue app bootstraped');
}

//生命周期挂载
export async function mount(props) {
  Vue.prototype.$qiankun = props;
  Vue.prototype.$store = Vue.observable(props.pinia);
  render(props);
}

//销毁
export async function unmount() {
  instance.$destroy();
  instance.$el.innerHTML = '';
  instance = null;
  router = null;
}

3.子应用webpack配置,在vue.config.js中

const path = require('path');
const { name } = require('./package');

function resolve(dir) {
  return path.join(__dirname, dir);
}

//指定端口
const port = 7301; // dev port


module.exports = {
  outputDir: 'dist',
  assetsDir: 'static',
  filenameHashing: true,
  devServer: {
    // host: '0.0.0.0',
    hot: true,
    disableHostCheck: true,
    port,
    overlay: {
      warnings: false,
      errors: true,
    },
    //子应用允许跨域
    headers: {
      'Access-Control-Allow-Origin': '*',
    },
  },
  // 自定义webpack配置
  configureWebpack: {
    resolve: {
      alias: {
        '@': resolve('src'),
      },
    },
    output: {
      // 把子应用打包成 umd 库格式
      library: `${name}-[name]`,
      libraryTarget: 'umd',
      jsonpFunction: `webpackJsonp_${name}`,
    },
  },
};

子应用改造,vue3+vite项目
1.main.js

import {
  renderWithQiankun,
  qiankunWindow
} from 'vite-plugin-qiankun/dist/helper';
function setDomain() {
  window.ISNK = document.domain.indexOf('172') > -1; // 如果是172客户的域名,那就拿客户地址,自动判断,这里搞个全局判断标志
  window.ORIGIN =
    process.env.NODE_ENV === 'development'
      ? process.env.VITE_ORIGIN_DEV
      : window.ISNK
      ? process.env.VITE_ORIGIN_PRO
      : process.env.VITE_ORIGIN_PRO_TEST;
}
//  设置主域名,但不跟随基座端口变化而变化
setDomain();
let router = null;
let instance = null;
let history = null;
function render(props = {}) {
  const { container } = props;
  history = createWebHistory(
    qiankunWindow.__POWERED_BY_QIANKUN__ ? '/vue3Vite' : '/'
  );
  //   console.log(history,qiankunWindow.__POWERED_BY_QIANKUN__ ,"history");
  router = createRouter({
    history,
    routes
  });

  instance = createApp(App);
  instance.use(router);
  //   instance.use(store);
  instance.mount(
    container ? container.querySelector('#app') : document.getElementById('app')
  );
  if (qiankunWindow.__POWERED_BY_QIANKUN__) {
    console.log('我正在作为子应用运行');
  }
}

// some code
renderWithQiankun({
  mount(props) {
    console.log('viteapp mount');
    render(props);
    // console.log(instance.config.globalProperties.$route,"444444444");
  },
  bootstrap() {
    console.log('bootstrap');
  },
  unmount() {
    instance.unmount();
    instance._container.innerHTML = '';
    history.destroy(); // 不卸载  router 会导致其他应用路由失败
    router = null;
    instance = null;
  }
});

if (!qiankunWindow.__POWERED_BY_QIANKUN__) {
  render();
}

2.vite.config.js中

import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import qiankun from 'vite-plugin-qiankun';
import { resolve } from 'path';
import { loadEnv } from 'vite';
// useDevMode 开启时与热更新插件冲突
const useDevMode = true; // 如果是在主应用中加载子应用vite,必须打开这个,否则vite加载不成功, 单独运行没影响

// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
  const env = loadEnv(mode, process.cwd());
  let config = {
    plugins: [vue(), qiankun('vue3Vite', { useDevMode })],
    resolve: {
      extensions: ['.js', '.vue', '.json'],
      alias: {
        '@': resolve('src')
      }
    },
    server: {
      host: '0.0.0.0', // 暴露内网ip
      port: 7316,
      cors: true
    },
    define: {
      'process.env': env
    }
  };
  return config;
});

子主共享全局store
在主应用中src/storeQianKun.js

import { initGlobalState } from 'qiankun'
import { reactive } from 'vue'

// 父应用的初始state
// Vue.observable是为了让initialState变成可响应:https://cn.vuejs.org/v2/api/#Vue-observable。
const initialState = reactive({})

const actions = initGlobalState(initialState)

actions.onGlobalStateChange((newState, prev) => {
  // state: 变更后的状态; prev 变更前的状态
  //   console.log("main change", JSON.stringify(newState), JSON.stringify(prev));

  for (const key in newState) {
    initialState[key] = newState[key]
  }
})

// 定义一个获取state的方法下发到子应用
actions.getGlobalState = (key) => {
  // 有key,表示取globalState下的某个子级对象
  // 无key,表示取全部

  return key ? initialState[key] : initialState
}

export default actions

在主应用注册APP的地方,下发store

const apps = microApps.map((item) => {
  return {
    ...item,
    container: '#cnbi-viewport', // 子应用挂载的div
    props: {
      routerBase: item.activeRule, // 下发基础路由
      getGlobalState: store.getGlobalState, // 下发getGlobalState方法
    },
  }
})

我们在主应用中点击了某个marker将其信息存储进全局store
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

子应用监听到主应用被点击的marker,成功创建面板,并调用主应用中的方法,创建出面板。
以上粗略的讲解了qiankun的一些使用和项目改造的一些过程,接下来我们看看对一些特殊需求的探索与思考;

1.keep-alive需求
子项目 keep-alive 其实就是想在子项目切换时不卸载掉,仅仅是样式上的隐藏(display: none),这样下次打开就会更快。
keep-alive 需要谨慎使用,同时加载并运行多个子项目,这会增加 js/css 污染的风险。
解决方案:借助 loadMicroApp 函数
借助 loadMicroApp 函数来实现手动加载和卸载子项目,一般有 keep-alive 需求的就是 tab 页,新增一个 tab 页时就加载这个子项目,关闭 tab 页时卸载这个子项目。

import { loadMicroApp } from 'qiankun';

const apps = [
  { 
    name: 'app-vue-hash', 
    entry: 'http://localhost:1111', 
    container: '#appContainer1',
    props: { data : { store, router } }
  },
  { 
    name: 'app-vue-history',
    entry: 'http://localhost:2222', 
    container: '#appContainer2',
    props: { data : store }
  }
]

export default {
  mounted() {
    // 优先加载当前的子项目
    const path = this.$route.path;
    const currentAppIndex = apps.findIndex(item => path.includes(item.name));
    if(currentAppIndex !== -1){
      const currApp = apps.splice(currentAppIndex, 1)[0];
      apps.unshift(currApp);
    }
    // loadMicroApp 返回值是 app 的生命周期函数数组
    const loadApps = apps.map(item => loadMicroApp(item))
    // 当 tab 页关闭时,调用 loadApps 中 app 的 unmount 函数即可
  },
}

切换子项目时,dom将不会被清空
2.复用公共依赖(子项目复用主项目)
解决方案1:加载子项目之前处理一下全局变量

registerMicroApps(apps,
{
  beforeLoad(app){
    if(app.name === 'app-vue-hash'){
      // 如果直接在 app-vue-hash 子项目刷新页面,此时 window.Vue2 是 undefined
      // 所以先判断下 window.Vue2 是否存在
      if(window.Vue2){
        window.Vue = window.Vue2; 
        window.Vue2 = undefined;
      }
    }else if(app.name === 'app-vue-history'){
      window.Vue2 = window.Vue; 
      window.Vue = undefined
    }
  },
});

解决方案2:通过props传递依赖

import VueRouter from 'vue-router'
registerMicroApps([
  {
    name: 'app-vue-hash', 
    entry: 'http://localhost:1111', 
    container: '#appContainer', 
    activeRule: '/app-vue-hash', 
    props: { data : { VueRouter } }
  },
]);

子项目mount阶段接收

function render(parent = {}) {
  if(!instance){
    // 当它独立运行时,使用自己的外链依赖 window.VueRoute
    const VueRouter = parent.VueRouter || window.VueRouter; 
    Vue.use(VueRouter);
    router = new VueRouter({
      routes,
    });
    instance = new Vue({
      router,
      store,
      render: h => h(App),
    }).$mount('#appVueHash');
  }
}

export async function mount(props) {
  render(props.data);
}

3.父子项目间的组件共享
主项目入口文件:

import HelloWorld from '@/components/HelloWorld.vue'
window.commonComponent = { HelloWorld };

子项目直接使用:

components: { 
  HelloWorld: window.__POWERED_BY_QIANKUN__ ? window.commonComponent.HelloWorld :
     import('@/components/HelloWorld.vue'))
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

秋名山大前端

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值