Vue.js - 微前端框架 qiankun 的使用详解(附样例)
一、微前端介绍
1,什么是微前端?
(1)微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略。
(2)微前端的诞生主要为了解决如下两个问题:
- 复用(嵌入)别人的项目页面,但是别人的项目运行在他自己的环境之上。
- 将原先的一个巨无霸应用拆分成一个个的小项目,这些小项目既可以独立开发部署,又可以自由组合。
2,使用微前端的好处
- 技术栈无关,各个子项目可以自由选择框架,可以自己制定开发规范。
- 快速打包,独立部署,互不影响,升级简单。
- 可以很方便的复用已有的功能模块,避免重复开发。
3,目前微前端主要的解决方案
(1)iframe 方案
- iframe 最大的特性就是提供了浏览器原生的硬隔离方案,不论是样式隔离、js 隔离这类问题统统都能被完美解决。
- 但他的最大问题也在于他的隔离性无法被突破,导致应用间上下文无法被共享(主要是本地存储、全局变量和公共插件),两个项目不同源(跨域)情况下数据传输需要依赖 postMessage,随之带来的开发体验、产品体验的问题。
(1)具体来说,使用 iframe 有如下常见的问题:
(2)其中有的问题比较好解决(问题 1),有的问题我们可以睁一只眼闭一只眼(问题 4 ),但有的问题我们则很难解决(问题 3)甚至无法解决(问题 2),而这些无法解决的问题恰恰又会给产品带来非常严重的体验问题。
- url 不同步。浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用。
- UI 不同步,DOM 结构不共享。想象一下屏幕右下角 1/4 的 iframe 里来一个带遮罩层的弹框,同时我们要求这个弹框要浏览器居中显示,还要浏览器 resize 时自动居中..
- 全局上下文完全隔离,内存变量不共享。iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的子应用中实现免登效果。
- 慢。每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程。
(2)single-spa 方案
- Single-spa 是一个将多个单页面应用聚合为一个整体应用的 JavaScript 微前端框架(GitHub 主页)。简单来说就是将子项目的内容(包括容器、js)插入到主项目,从而呈现出子项目的内容。
- 相对于 iframe,single-spa 让父子项目属于同一个 document,这样做既有好处,也有坏处。好处就是数据/文件都可以共享,公共插件共享,子项目加载就更快了,缺点是带来了 js/css 污染。
- single-spa 上手并不简单,也不能开箱即用,开发部署更是需要修改大量的 webpack 配置,对子项目的改造也非常多。
二、qiankun 介绍
1,什么是 qiankun?
qiankun 是一个基于 single-spa 的微前端实现库,孵化自蚂蚁金融科技基于微前端架构的云产品统一接入平台。它在 single-spa 的基础上,实现了开箱即用,除一些必要的修改外,子项目只需要做很少的改动,就能很容易的接入。
- 官方网站:https://qiankun.umijs.org/zh/
- Github 主页:https://github.com/umijs/qiankun
2,qiankun 主要特性
- 基于 single-spa 封装,提供了更加开箱即用的 API。
- 技术栈无关,任意技术栈的应用均可 使用/接入,不论是 React/Vue/Angular/JQuery 还是其他等框架。
- HTML Entry 接入方式,让我们接入微应用像使用 iframe 一样简单。
- 样式隔离,确保微应用之间样式互相不干扰。
- JS 沙箱,确保微应用之间 全局变量/事件 不冲突。
- 资源预加载,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。
- umi 插件,提供了 @umijs/plugin-qiankun 供 umi 应用一键切换成微前端架构系统。
三、使用样例
1,主应用开发
(1)主应用也就是整个微前端的基座应用,我们会将各个微应用加载到主应用中展示。这里我们创建一个 Vue 项目作为主应用,创建后执行如下命令安装 qiankun。
npm i qiankun -S
(2)主应用需要提供一个容器 DOM,用于插入加载的微应用。这里我们将 App.vue 修改成如下内容:
<template> <div id="app"> <div id="nav"> <router-link to="/app1">App1</router-link> | <router-link to="/app2">App2</router-link> </div> <div id="container"/> </div> </template>
(3)然后在 main.js 中注册微应用并 start 即可。
提示:当微应用信息注册完之后,一旦浏览器的 url 发生变化,便会自动触发 qiankun 的匹配逻辑,所有 activeRule 规则匹配上的微应用就会被插入到指定的 container 中,同时依次调用微应用暴露出的生命周期钩子。
import Vue from 'vue' import App from './App.vue' import router from './router' import store from './store' Vue.config.productionTip = false new Vue({ router, store, render: h => h(App) }).$mount('#app') // 引入 qiankun 组件 import { registerMicroApps, start } from 'qiankun'; // 注册微应用信息 registerMicroApps([ { name: 'hangge-app-1', entry: 'http://localhost:9091', container: '#container', activeRule: '/app1', }, { name: 'hangge-app-2', entry: 'http://localhost:9092', container: '#container', activeRule: '/app2', } ]); // 启动 qiankun start();
- 当然微应用也不是必须要跟路由关联,我也可以选择手动加载微应用的方式:
import { loadMicroApp } from 'qiankun'; loadMicroApp({ name: 'hangge-app-1', entry: 'http://localhost:9091', container: '#container', });
2,微应用开发
(1)首先在微应用的 src 目录下新增 public-path.js 文件,内容如下:
if (window.__POWERED_BY_QIANKUN__) { __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; }
(2)接着修改 main.js 文件,在最顶部引入 public-path.js,导出 bootstrap、mount、unmount 三个生命周期钩子,以供主应用在适当的时机调用。
注意:mount 时,为了避免根 id #app 与其他的 DOM 冲突,需要限制查找范围。
import './public-path'; import Vue from 'vue' import App from './App.vue' import router from './router' import store from './store' Vue.config.productionTip = false let instance = null; // 页面渲染方法 function render(props = {}) { const { container } = props; instance = new Vue({ router, store, render: (h) => h(App), }).$mount(container ? container.querySelector('#app') : '#app'); } // 独立运行时就直接渲染 if (!window.__POWERED_BY_QIANKUN__) { render(); } /** * bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 * 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。 */ export async function bootstrap() { console.log('[vue] vue app bootstraped'); } /** * 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法 */ 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; }
(3)修改 router/index.js,设置路由 base,值和该微应用在主应用那边的 activeRule 是一样的。
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: window.__POWERED_BY_QIANKUN__ ? '/app1/' : process.env.BASE_URL, routes }) export default router
(4)最后编辑项目根目录下的 vue.config.js 文件(如果没有则手动创建一个),修改 webpack 打包,允许开发环境跨域和 umd 打包。
const { name } = require('./package'); module.exports = { devServer: { headers: { 'Access-Control-Allow-Origin': '*', //允许开发环境跨域 }, }, configureWebpack: { output: { library: `${name}-[name]`, libraryTarget: 'umd', // 把微应用打包成 umd 库格式 jsonpFunction: `webpackJsonp_${name}`, }, }, };
(5)经过上面修改后访问可能会报“error '__webpack_public_path__' is not defined”错误。这个是 eslint 的问题,webpack_public_path 不是全局变量所导致的。我们在 package.json 文件中 eslintConfig 配置全局变量后重起服务即可解决。
}, "eslintConfig": { "globals": { "__webpack_public_path__": true } } }
3,运行效果
(1)启动项目,如果我们直接访问微应用效果如下:
(2)访问主应用效果如下:
(3)点击主应用里的链接进行路由跳转,可以发现对应的微应用便成功加载进来了。
(4)微应用内的路由跳转也是完全正常的: