qiankun基础配置

微前端

背景

随着SPA大规模的应用,紧接着就带来一个新问题:一个规模化应用需要拆分

一方面功能快速增加导致打包时间成比例上升,而紧急发布时要求是越短越好,这是矛盾的。另一方面当一个代码库集成了所有功能时,日常协作绝对是非常困难的。而且最近十多年,前端技术的发展是非常快的,每隔两年就是一个时代,导致同志们必须升级项目甚至于换一个框架。但如果大家想在一个规模化应用中一个版本做好这件事,基本上是不可能的。

最早的解决方案是采用iframe的方法,根据功能主要模块拆分规模化应用,子应用之间使用跳转。但这个方案最大问题是导致页面重新加载和白屏。

什么是微前端

微前端是一种类似于微服务的架构,是一种由独立交付的多个前端应用组成整体的架构风格,将前端应用分解成一些更小、更简单的能够独立开发、测试、部署的应用,而在用户看来仍然是内聚的单个产品。有一个基座应用(主应用),来管理各个子应用的加载和卸载。

所以微前端不是指具体的库,不是指具体的框架,不是指具体的工具,而是一种理想与架构模式。

微前端的核心三大原则就是:独立运行、独立部署、独立开发

微前端的价值

微前端架构具备以下几个核心价值:

  • 技术栈无关
    主框架不限制接入应用的技术栈,微应用具备完全自主权

  • 独立开发、独立部署
    微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新

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

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

场景

在这里插入图片描述

微前端选型
技术方案描述技术栈优点缺点单独构建 / 部署构建速度SPA 体验项目侵入性学习成本通信难度
iframe每个微应用独立开发部署,通过 iframe的方式将这些应用嵌入到父应用系统中无限制1. 技术栈无关,子应用独立构建部署2. 实现简单,子应用之间自带沙箱,天然隔离,互不影响体验差、路由无法记忆、页面适配困难、无法监控、依赖无法复用,兼容性等都具有局限性,资源开销巨大,通信困难支持正常不支持
Nginx 路由转发通过Nginx配置实现不同路径映射到不同应用无限制简单、快速、易配置在切换应用时触发发页面刷新,通信不易支持正常不支持正常
Npm 集成将微应用抽离成包的方式,发布Npm中,由父应用依赖的方式使用,构建时候集成进项目中无限制1. 编译阶段的应用,在项目运行阶段无需加载,体验流畅2.开发与接入成本低,容易理解1. 影响主应用编译速度和打包后的体积2. 不支持动态下发,npm包更新后,需要重新更新包,主应用需要重新发布部署不支持支持正常
通用中心路由基座式微应用可以使用不同技术栈;微应用之间完全独立,互不依赖。统一由基座工程进行管理,按照DOM节点的注册、挂载、卸载来完成。无限制子应用独立构建,用户体验好,可控性强,适应快速迭代学习与实现的成本比较高,需要额外处理依赖复用支持正常支持正常
特定中心路由基座式微应用业务线之间使用相同技术栈;基座工程和微应用可以单独开发单独部署;微应用有能力复用基座工程的公共基建。统一技术栈子应用独立构建,用户体验好,可控性强,适应快速迭代学习与实现的成本比较高,需要额外处理依赖复用支持正常支持正常
webpack5 模块联邦webpack5 模块联邦 去中心模式、脱离基座模式。每个应用是单独部署在各自的服务器,每个应用都可以引用其他应用,也能被其他应用所引用统一技术栈基于webpack5,无需引入新框架,学习成本低,像引入第三方库一样方便,各个应用的资源都可以相互共享应用间松耦合,各应用平行的关系需要升级Webpack5技术栈必须保持一致改造旧项目难度大支持正常支持
市面框架对比:
  • magic-microservices 一款基于 Web Components 的轻量级的微前端工厂函数。
  • icestark 阿里出品,是一个面向大型系统的微前端解决方案
  • single-spa 是一个将多个单页面应用聚合为一个整体应用的JavaScript 微前端框架
  • qiankun 蚂蚁金服出品,基于 single-spa 在 single-spa 的基础上封装
  • EMP YY出品,基于Webpack5 Module Federation 除了具备微前端的能力外,还实现了跨应用状态共享、跨框架组件调用的能力
  • MicroApp 京东出品,一款基于WebComponent的思想,轻量、高效、功能强大的微前端框架

综合以上方案对比之后,我们确定采用了 qiankun 特定中心路由基座式的开发方案,原因如下:

  • 保证技术栈统一 Vue、微应用之间完全独立,互不影响。
  • 友好的“微前端方案“,与技术栈无关接入简单、像iframe一样简单
  • 改造成本低,对现有工程侵入度、业务线迁移成本也较低。
  • 和原有开发模式基本没有不同,开发人员学习成本较低。
  • qiankun 的微前端有 3 年使用场景以及 Issue 问题解决积累,社区也比较活跃,在踩坑的路上更容易自救~
为什么不用iframe

为什么不用 iframe,这几乎是所有微前端方案第一个会被 challenge 的问题。但是大部分微前端方案又不约而同放弃了 iframe 方案,自然是有原因的,并不是为了 “炫技” 或者刻意追求 “特立独行”。

如果不考虑体验问题,iframe 几乎是最完美的微前端解决方案了。

iframe 最大的特性就是提供了浏览器原生的硬隔离方案,不论是样式隔离、js 隔离这类问题统统都能被完美解决。但他的最大问题也在于他的隔离性无法被突破,导致应用间上下文无法被共享,随之带来的开发体验、产品体验的问题。

  1. url 不同步。浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用。
  2. UI 不同步,DOM 结构不共享。想象一下屏幕右下角 1/4 的 iframe 里来一个带遮罩层的弹框,同时我们要求这个弹框要浏览器居中显示,还要浏览器 resize 时自动居中…
  3. 全局上下文完全隔离,内存变量不共享。iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的子应用中实现免登效果。
  4. 慢。每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程。

其中有的问题比较好解决(问题1),有的问题我们可以睁一只眼闭一只眼(问题4),但有的问题我们则很难解决(问题3)甚至无法解决(问题2),而这些无法解决的问题恰恰又会给产品带来非常严重的体验问题, 最终导致我们舍弃了 iframe 方案。

qiankun

qiankun 是一个基于 single-spa 的微前端实现库,中文文档齐全,且国内生态很好

特性

📦 基于 single-spa 封装,提供了更加开箱即用的 API
📱 技术栈无关,任意技术栈的应用均可 使用/接入,不论是React/Vue/Angular/JQuery 还是其他等框架。
💪 HTML Entry 接入方式,让你接入微应用像使用 iframe一样简单。
🛡​ 样式隔离,确保微应用之间样式互相不干扰。
🧳 JS 沙箱,确保微应用之间 全局变量/事件 不冲突。
⚡️资源预加载,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。
🔌 umi 插件,提供了@umijs/plugin-qiankun 供 umi 应用一键切换成微前端架构系统。

坑:
1.react18.x 的卸载组件unmountComponentAtNode已废弃

(createRoot:创建用于render和unmout的根节点。用它代替ReactDOM.render)

2.vite不兼容沙箱

在这里插入图片描述
沙箱(sandbox): 沙箱其实是一种工具,或者可以理解为一个黑盒,用于隔离当前执行的环境作用域和外部的其他作用域。而在JavaScript中就意味着,在沙箱中的操作被限死在当前作用域,不会对其他模块和个人沙箱造成任何影响。

3.qiankun 应用之间 UI 样式冲突问题

在这里插入图片描述
问题原因:配置(基座vue2+element UI + 微应用vue3+elementPlus),会导致基座内虽然有微应用但是element的css样式无法显示
初步问题定义为基座开启了样式隔离导致沙箱内的css类无法加载var()函数

start({
  sandbox: {
    strictStyleIsolation: true,//样式隔离严格模式
  }
})

(element ui直接使用的颜色而element plus使用的var()来配置的引入的颜色)
vue2+elementUI的样式可成功显示
在这里插入图片描述
vue3+elementPlus样式无法显示
在这里插入图片描述
解决方法,基座关闭沙箱取消样式隔离,在每个应用之间添加 class 区分 或加 scoped
给element-plus添加自定义类名

约定式编程
这里我们可以采用一定的编程约束:

1.尽量不要使用可能冲突全局的 class 或者直接为标签定义样式;
2.定义唯一的 class 前缀,现在的项目都是用诸如 antd 这样的组件库,这类组件库都支持自定义组件 class 前缀;
3.主应用一定要有自定义的 class 前缀;

React版本

主基座react18.x+微应用react17.x
1.基座和微应用内安装qiankun

主应用和微应用.env本地端口已分别设置3010和3011

npm i qiankun -S

2.在基座index.js配置
import { registerMicroApps, start } from 'qiankun';

registerMicroApps([
  {
    name: 'react app', // app name registered
    entry: '//localhost:3011',//需要匹配的url
    container: '#react-app1',//挂载结点
    activeRule: '/react-app1',//路由
  },
  // {
  //   name: 'vue app',
  //   entry: { scripts: ['//localhost:7100/main.js'] },
  //   container: '#yourContainer2',
  //   activeRule: '/yourActiveRule2',
  // },
]);
start()
3.可根据官网配置webpack,本次采用的是react-app-rewired

在微应用内安装插件

npm i react-app-rewired

在package.json中设置
在这里插入图片描述
在 src 目录新增 public-path.js:解决静态资源无法显示问题

/* eslint-disable no-undef */
//默认eslint会报错,不用管,忽略掉
if (window.__POWERED_BY_QIANKUN__) {
    __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
  }

在根目录中创建config-overrides.js:

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

module.exports = {
  webpack: (config) => {
    // umd方式注入
    config.output.library = `${name}-[name]`;
    config.output.libraryTarget = 'umd';
    // config.output.jsonpFunction = `webpackJsonp_${name}`;
    config.output.globalObject = 'window';

    return config;
  },

  devServer: (_) => {
    const config = _;
    // 允许跨域
    config.headers = {
      'Access-Control-Allow-Origin': '*',
    };
    config.historyApiFallback = true;
    config.hot = false;
    config.watchContentBase = false;
    config.liveReload = false;

    return config;
  },
};

重写index.js 引入生命周期

import './public-path';
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

function render(props) {
  const { container } = props;
  ReactDOM.render(<App />, container ? container.querySelector('#root') : document.querySelector('#root'));
}

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

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

export async function mount(props) {
  console.log('[react16] props from main framework', props);
  render(props);
}

export async function unmount(props) {
  const { container } = props;
  ReactDOM.unmountComponentAtNode(container ? container.querySelector('#root') : document.querySelector('#root'));
}
4.页面显示

回到基座页面App.js,id需要和配置中的

function App() {
  return (
    <div className="App">
     基座react
     <div id='react-app1'></div>
    </div>
  );
}

export default App;

在这里插入图片描述

Vue版本

1、安装qiankun

npm i qiankun -S

2.基座设置main.js(vue2)
import { 
  registerMicroApps, 
  start ,  
  // addGlobalUncaughtErrorHandler,
  // initGlobalState
} from 'qiankun';

registerMicroApps([
  {
    name: 'vueApp',
    entry: '//localhost:9001',
    container: '#container',
    activeRule: '/about1/app-vue',
  },
  {
    name: 'vueApp2',
    entry: '//localhost:9002',
    container: '#app-vue2',
    activeRule: '/app-vue2',
  },
  {
    name: 'vueApp3',
    entry: '//localhost:9003',
    container: '#app-vue3',
    activeRule: '/app-vue3',
  },
  {
    name: 'react app', // app name registered
    entry: '//localhost:3011',
    container: '#react-app',
    activeRule: '/react-app1',
  },
]);
// 启动 qiankun
start({
  sandbox: {
    strictStyleIsolation: true,//样式隔离
  }
})
3.设置微应用(vue2)

1.设置vue.config.js

const { name } = require('./package');
module.exports = {
  devServer: {
    headers: {
      'Access-Control-Allow-Origin': '*',
    },
  },
  configureWebpack: {
    output: {
      library: `${name}-[name]`,
      libraryTarget: 'umd', // 把微应用打包成 umd 库格式
      // jsonpFunction: `webpackJsonp_${name}`,
    },
  },
};

2.src目录添加public-path.js

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

3.将routes导出,router在main.js里设置

import Vue from 'vue'
import VueRouter from 'vue-router'
import HomeView from '../views/HomeView.vue'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'home',
    component: HomeView
  },
  {
    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/AboutView.vue')
  }
]

// const router = new VueRouter({
//   mode: 'history',
//   base: process.env.BASE_URL,
//   routes
// })

export default routes

4.重写main.js

import './public-path';
import Vue from 'vue';
import VueRouter from 'vue-router';
import App from './App.vue';
import routes from './router';
import store from './store';

Vue.config.productionTip = false;

let router = null;
let instance = null;
function render(props = {}) {
  const { container } = props;
  router = new VueRouter({
    base: window.__POWERED_BY_QIANKUN__ ? '/app-vue2/' : '/',
    // base:"/app-vue",
    mode: 'history',
    routes,
  });


  

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

// 独立运行时
if (!window.__POWERED_BY_QIANKUN__) {
  render();
}

export async function bootstrap() {
  console.log('[vue] vue app bootstraped');
}
export async function mount(props) {
  console.log('[vue] props from main framework', props);
  render(props);
}
export async function unmount() {
  instance.$destroy();
  instance.$el.innerHTML = '';
  instance = null;
  router = null;
}

在这里插入图片描述

4.设置微应用(vue3)

前三步骤与设置微应用vue版本一致
重写main.js

import './public-path';
import { createApp } from 'vue';
import { createRouter, createWebHistory } from 'vue-router';

import App from './App';
// import routes from './routes';
import HomeView from './views/HomeView.vue'
const routes = [
    {
      path: '/',
      name: 'home',
      component: HomeView
    },
    {
      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/AboutView.vue')
    }
  ]

// qiankun window全局变量
const isQK = window.__POWERED_BY_QIANKUN__;

let app = null,
    router = null;

const render = (props = {}) => {
    const { container } = props;
    console.log('props.prefix',props.prefix,container)
    router = createRouter({
        history: createWebHistory(window.__POWERED_BY_QIANKUN__ ? '/app-vue3/' : '/',),
        routes
    });
    app = createApp(App);
    app.use(router);

    app.mount(container ? container.querySelector('#app') : '#app');
};

// 独立运行
if (!isQK) render();

// 初始化
export async function bootstrap() {
    console.log('%c ', 'color: green;', 'vue3.0 app bootstraped');
}
// 挂载
export async function mount(props) {
    setTimeout(() => {
        // 例子 - 关闭局部loading
        props.setGlobalState({ closeLoading: true });
        console.log('-------关闭局部loading---------');
    }, 3000);
    render(props);
    console.log('app=======', app);
    props.onGlobalStateChange((state) => {
        // state: 变更后的状态; prev 变更前的状态
        // console.log('===子应用====', state, prev);
        console.log('===子应用====', state);
    });
}
// 销毁
export async function unmount() {
    app.unmount();
    app._container.innerHTML = '';
    app = null;
    router = null;
}

在这里插入图片描述

5.设置微应用(在主应用下的路由里设置微应用vue2)

在这里插入图片描述
前四步骤与设置vue的微应用相同
1.在基座内的路由内about下设置动态匹配路由
在这里插入图片描述
2.在微应用的main.js里的重写的路由里添加上基座的路由
通过qiankun判断是否是嵌入在基座内是的话条跳转否则就是本地默认路由配置
在这里插入图片描述
3.基座内设置跳转
在这里插入图片描述
4.基座路由下设置插入匹配的#continer
在这里插入图片描述
5.基座内设置continer和activeRule
在这里插入图片描述
在这里插入图片描述

6.设置微应用(react)

和上面的react基座内设置react微应用一致

数据通信

在微前端架构中,我们应该按业务划分出对应的子应用,而不是通过功能模块划分子应用。这么做的原因有两个:

在微前端架构中,子应用并不是一个模块,而是一个独立的应用,我们将子应用按业务划分可以拥有更好的可维护性和解耦性。
子应用应该具备独立运行的能力,应用间频繁的通信会增加应用的复杂度和耦合度。

综上所述,我们应该从业务的角度出发划分各个子应用,尽可能减少应用间的通信,从而简化整个应用,使得我们的微前端架构可以更加灵活可控。

qiankun props 传值

在注册应用信息的方法registerMicroApps中,通过props给子应用传递数据。此方法优势是用法很简单,且可以传递组件和方法,缺点是只能从主应用传递给子应用。有不少场景不太适用。
在这里插入图片描述
在这里插入图片描述

props - object - 可选,主应用需要传递给微应用的数据。

一般可通过主应用的store传递给子应用

子组件获取到的数据
在这里插入图片描述

initGlobalState

qiankun通过initGlobalState, onGlobalStateChange, setGlobalState实现主应用的全局状态管理

onGlobalStateChange: (callback: OnGlobalStateChangeCallback, fireImmediately?: boolean) => void, 在当前应用监听全局状态,有变更触发 callback,fireImmediately = true 立即触发 callback

setGlobalState: (state: Record<string, any>) => boolean, 按一级属性设置全局状态,微应用中只能修改已存在的一级属性

offGlobalStateChange: () => boolean,移除当前应用的状态监听,微应用 umount 时会默认调用

主应用给子应用传值

主应用 main.js

import { 
  initGlobalState,
} from 'qiankun';

export const initialState = {
  globalLocation: {
    id: 1234,
    station: '北京'
  }
}
const actions = initGlobalState(initialState) //初始化全局数据

actions.onGlobalStateChange((newState, prev) => {  //监听全局状态
  console.log(newState, prev)
  for (let key in newState) {
    initialState[key] = newState[key]
  }
});

子应用 action,js

// /src/qiankun/action.js
function emptyAction() {
    // 提示当前使用的是空 Action
    console.warn("Current execute action is empty!");
}
 
class Actions {
    // 默认值为空 Action
    actions = {
        onGlobalStateChange: emptyAction,
        setGlobalState: emptyAction,
    };
 
    /**
     * 设置 actions
     */
    setActions(actions) {
        this.actions = actions;
    }
 
    /**
     * 映射
     */
    onGlobalStateChange() {
        return this.actions.onGlobalStateChange(...arguments);
    }
 
    /**
     * 映射
     */
    setGlobalState() {
        return this.actions.setGlobalState(...arguments);
    }
}
 
const actions = new Actions();
export default actions;

子应用 main.js

/**
 * 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
 */
export async function mount(props) {
  console.log('[vue] props from main framework', props);
  props.onGlobalStateChange((state, prev) => {
    // state: 变更后的状态; prev 变更前的状态
    console.log('这是主应用传来的值',state, prev);
  },true);//第二个参数为 true,表示立即执行一次观察者函数
  actions.setActions(props) //子项目的入口文件中设置子应用的全局state
  
  render(props)

}
子应用传值给主应用
import action from "../action";
export default {
  name: "HelloWorld",
  props: {
    msg: String,
  },
  created(){
    console.log('VVVVVVVVVVVVVVVVV');
  },
  mounted() {
    // 接收state
    action.onGlobalStateChange((state) => {
      console.log(state);
    }, true);
  },
  methods: {
    changeValue() {
      // 修改state
      action.setGlobalState({ count: 789});//每次执行时都会出发一次mounted
    },
  },
};
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值