qiankun基本使用
1、qiankun框架基本介绍
qiankun 是蚂蚁金服基于 single-spa 的一个微前端实现开源库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。qiankun 框架简化了微应用的注册方式,增加了微应用的沙箱管理(js、css隔离)与全局状态共享机制,并且 qiankun 内部实现了一个解析 html 字符串获取静态资源地址的解析库 import-html-entry,方便微应用接入与资源预加载。
qiankun 的核心设计理念:
-
简单
由于主应用微应用都能做到技术栈无关,qiankun 对于用户而言只是一个类似 jQuery 的库,你需要调用几个 qiankun 的 API 即可完成应用的微前端改造。同时由于 qiankun 的 HTML entry 及沙箱的设计,使得微应用的接入像使用 iframe 一样简单。
-
解耦/技术栈无关
微前端的核心目标是将巨石应用拆解成若干可以自治的松耦合微应用,而 qiankun 的诸多设计均是秉持这一原则,如 HTML entry、沙箱、应用间通信等。这样才能确保微应用真正具备 独立开发、独立运行 的能力。
任意框架的前端项目均可使用qiankun框架,微应用接入像使用接入一个 iframe 系统一样简单,但实际不是 iframe。
1.1、qiankun框架特性
- 📦 基于 single-spa 封装,提供了更加开箱即用的 API。
- 📱 技术栈无关,任意技术栈的应用均可 使用/接入,不论是 React/Vue/Angular/JQuery 还是其他等框架。
- 💪 HTML Entry 接入方式,让你接入微应用像使用 iframe 一样简单。
- 🛡 样式隔离,确保微应用之间样式互相不干扰。
- 🧳 JS 沙箱,确保微应用之间 全局变量/事件 不冲突。
- ⚡️ 资源预加载,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。
- 🔌 umi 插件,提供了 @umijs/plugin-qiankun 供 umi 应用一键切换成微前端架构系统。
1.2、微前端的概念
微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略。
微前端架构具备以下几个核心价值:
-
技术栈无关
主框架不限制接入应用的技术栈,微应用具备完全自主权 -
独立开发、独立部署
微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新 -
增量升级
在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略
-
独立运行时
每个微应用之间状态隔离,运行时状态不共享
微前端架构旨在解决单体应用在一个相对长的时间跨度下,由于参与的人员、团队的增多、变迁,从一个普通应用演变成一个巨石应用(Frontend Monolith)后,随之而来的应用不可维护的问题。
2、qiankun使用例子
2.1、主应用和子应用均为原生JS
如果使用原生 script 方式来引入 qiankun 框架,可以在 qiankun 依赖包里找到所需的打包后的 qiankun 结果文件,如下:
假设主应用和子应用都通过本地服务器打开,且子应用访问URL为:http://127.0.0.1:5500/。
2.1.1、主应用代码
主应用代码示例:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>主应用</title> </head> <body> <div> 主应用 </div> <div id="qiankun-viewport"> </div> <button onclick="btnfn()">点击跳转</button> </body> <script src="./index.umd.js"></script> <script> function btnfn() { location.href = "#/react-app" } // microApps 微应用集合 const microApps = [ { name: "reactapp", entry: "//localhost:5500", activeRule: "#/react-app" } ]; const apps = microApps.map((item) => { return { ...item, container: "#qiankun-viewport", // 微应用挂载在主应用中的盒子的 id }; }); // 注册微应用基础信息 // 即 apps 微应用数组,后面对象为配置触发钩子,与vue的钩子类似 window.qiankun.registerMicroApps(apps, { beforeLoad: (app) => { console.log("before load app.name====>>>>>", app.name); }, beforeMount: [ (app) => { console.log("before mount app.name====>>>>>", app.name); }, ], afterMount: [ (app) => { console.log("after mount app.name====>>>>>", app.name); }, ], afterUnmount: [ (app) => { console.log("after unmount app.name====>>>>>", app.name); }, ], }); // 添加全局的未捕获异常处理器 window.qiankun.addGlobalUncaughtErrorHandler((event) => { console.err("微应用加载失败!", event); }); window.qiankun.start(); </script> </html>
2.1.2、子应用代码
子应用代码示例:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>子应用</title> </head> <body> <div> 子应用 </div> </body> <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script> <script> const render = ($) => { $("#qiankun-viewport").html("Hello, render with jQuery"); return Promise.resolve(); }; ((global) => { global["reactapp"] = { bootstrap: () => { console.log("reactapp bootstrap"); return Promise.resolve(); }, mount: () => { console.log("reactapp mount"); return render($); }, unmount: () => { console.log("reactapp unmount"); return Promise.resolve(); }, }; })(window); </script> </html>
打开主应用,点击跳转按钮,即可看到子应用的内容将会挂载在主应用上。如下:
2.2、主应用为vue2,子应用为原生JS
2.2.1、主应用代码
我们用 vue-cli 初始化一个 vue项目,并且不使用 vue-router。(实际上用不用vue-router原理都一样,就是qiankun监听路由变化然后往容器里面加载子应用内容)
初始化完项目后,使用 npm i qiankun -S 来引入 qiankun 框架,并且修改 main.js 代码如下:
import Vue from "vue"; import App from "./App"; import { registerMicroApps, addGlobalUncaughtErrorHandler, start } from "qiankun"; Vue.config.productionTip = false; /* eslint-disable no-new */ new Vue({ el: "#app", components: { App }, template: "<App/>" }); // microApps 微应用集合 const microApps = [ { name: "reactapp", entry: "//localhost:5500", activeRule: "#/react-app" } ]; const apps = microApps.map(item => { return { ...item, container: "#qiankun-viewport" // 微应用挂载在主应用中的盒子的 id }; }); // 注册微应用基础信息 // 即 apps 微应用数组,后面对象为配置触发钩子,与vue的钩子类似 registerMicroApps(apps, { beforeLoad: app => { console.log("before load app.name====>>>>>", app.name); }, beforeMount: [ app => { console.log("before mount app.name====>>>>>", app.name); } ], afterMount: [ app => { console.log("after mount app.name====>>>>>", app.name); } ], afterUnmount: [ app => { console.log("after unmount app.name====>>>>>", app.name); } ] }); // 添加全局的未捕获异常处理器 addGlobalUncaughtErrorHandler(event => { console.err("微应用加载失败!", event); }); start();
在 App.vue 文件中添加子应用容器,并且通过按钮跳转到指定路由,qiankun 监听到路由变化即会加载子应用内容。代码如下:
<template> <div id="app"> <div id="qiankun-viewport"> </div> <button @click="btnfn">点击跳转</button> </div> </template> <script> import HelloWorld from './components/HelloWorld' export default { name: 'App', components: { HelloWorld }, methods: { btnfn() { location.href = "#/react-app" } } } </script>
2.2.2、子应用代码
子应用代码可参考上面 2.1 中子应用代码。
2.3、主应用和子应用均为vue2
2.3.1、主应用代码
我们用 vue-cli 初始化一个 vue项目,并且不使用 vue-router。(实际上用不用vue-router原理都一样,就是qiankun监听路由变化然后往容器里面加载子应用内容)
初始化完项目后,使用 npm i qiankun -S 来引入 qiankun 框架,并且修改 main.js 代码如下:
import Vue from "vue"; import App from "./App"; import { registerMicroApps, addGlobalUncaughtErrorHandler, start } from "qiankun"; Vue.config.productionTip = false; /* eslint-disable no-new */ new Vue({ el: "#app", components: { App }, template: "<App/>" }); // microApps 微应用集合 const microApps = [ { name: "reactapp", entry: "//localhost:8081", activeRule: "#/react-app" } ]; const apps = microApps.map(item => { return { ...item, container: "#qiankun-viewport" // 微应用挂载在主应用中的盒子的 id }; }); // 注册微应用基础信息 // 即 apps 微应用数组,后面对象为配置触发钩子,与vue的钩子类似 registerMicroApps(apps, { beforeLoad: app => { console.log("before load app.name====>>>>>", app.name); }, beforeMount: [ app => { console.log("before mount app.name====>>>>>", app.name); } ], afterMount: [ app => { console.log("after mount app.name====>>>>>", app.name); } ], afterUnmount: [ app => { console.log("after unmount app.name====>>>>>", app.name); } ] }); // 添加全局的未捕获异常处理器 addGlobalUncaughtErrorHandler(event => { console.err("微应用加载失败!", event); }); start();
在 App.vue 文件中添加子应用容器,并且通过按钮跳转到指定路由,qiankun 监听到路由变化即会加载子应用内容。代码如下:
<template> <div id="app"> <img src="./assets/logo.png"> <div id="qiankun-viewport"> </div> <button @click="btnfn">点击跳转</button> </div> </template> <script> import HelloWorld from './components/HelloWorld' export default { name: 'App', components: { HelloWorld }, methods: { btnfn() { location.href = "#/react-app" } } } </script>
2.3.2、子应用代码
如果是以 vue-cli 3+
生成的 vue 2.x
(即有vue.config.js文件)项目,则子应用代码可参考官网实例:https://qiankun.umijs.org/zh/guide/tutorial#vue-微应用
如果是以 vue-cli2 搭建的 vue2.x 子应用,则配置可参考:https://www.cnblogs.com/fengsaoke/p/14628997.html
下面示例没有引用vue-router 的 vue-cli2 搭建的 vue2 应用配置。
第一步跟官网保持一致,在 src
目录新增 public-path.js,内容如下:
if (window.__POWERED_BY_QIANKUN__) { __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; }
然后修改 main.js 文件,最终文件内容如下:
// The Vue build version to load with the `import` command // (runtime-only or standalone) has been set in webpack.base.conf with an alias. import "./public-path"; import Vue from "vue"; import App from "./App"; Vue.config.productionTip = false; /* eslint-disable no-new */ // new Vue({ // el: "#app", // components: { App }, // template: "<App/>" // }); let router = null; let instance = null; function render(props = {}) { const { container } = props; // router = new VueRouter({ // base: window.__POWERED_BY_QIANKUN__ ? "/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; }
最后修改 webpack.base.conf.js 文件,修改允许跨域和打包格式,内容跟官网差不多,不过有点小修改。如下:
const { name } = require("../package"); module.exports = { devServer: { headers: { "Access-Control-Allow-Origin": "*" } }, output: { library: `${name}-[name]`, libraryTarget: "umd", // 把微应用打包成 umd 库格式 jsonpFunction: `webpackJsonp_${name}`, ...... }, }
最终父子应用运行起来,即可通过父应用加载子应用内容,效果如下:
9、qiankun框架原理
9.2、为什么不是使用iframe来实现
为什么不用 iframe,这几乎是所有微前端方案第一个会被 challenge 的问题。但是大部分微前端方案又不约而同放弃了 iframe 方案,自然是有原因的,并不是为了 "炫技" 或者刻意追求 "特立独行"。
如果不考虑体验问题,iframe 几乎是最完美的微前端解决方案了。iframe 最大的特性就是提供了浏览器原生的硬隔离方案,不论是样式隔离、js 隔离这类问题统统都能被完美解决。但他的最大问题也在于他的隔离性无法被突破,导致应用间上下文无法被共享,随之带来的开发体验、产品体验的问题。
- url 不同步。浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用。
- UI 不同步,DOM 结构不共享。想象一下屏幕右下角 1/4 的 iframe 里来一个带遮罩层的弹框,同时我们要求这个弹框要浏览器居中显示,还要浏览器 resize 时自动居中..
- 全局上下文完全隔离,内存变量不共享。iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的子应用中实现免登效果。
- 慢。每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程。
其中有的问题比较好解决(问题1),有的问题我们可以睁一只眼闭一只眼(问题4),但有的问题我们则很难解决(问题3)甚至无法解决(问题2),而这些无法解决的问题恰恰又会给产品带来非常严重的体验问题, 最终导致我们舍弃了 iframe 方案。