什么是微前端
简单来说就是系统要模块化,通过不同的模块组合构建我们的应用,不同的模块迁移成子应用,最终构建成整个大的应用项目。微前端适合一些巨石应用的项目。
官方一些来说就是
微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略。一个完整应用划分成一个主应用和一个或多个微应用,应用间相互独立,可相互通信
微前端的核心目标是将巨石应用拆解成若干可以自治的松耦合微应用。
微前端是一种架构模式,用于构建可扩展的 Web 应用程序,该应用程序随着您的开发团队的发展而增长,并允许您扩展用户交互。我们可以将其与我们现有的 SPA 联系起来,说它是我们 SPA 的一个切片版本。这个版本对用户来说仍然看起来和感觉像一个 SPA,但在引擎盖下,它根据用户的流程动态加载应用程序的一部分。
微前端项目分为容器和其他微应用。即一个主项目和多个子项目。每个子项目负责自身功能,同时具备和其它子项目和主项目进行通信的能力,达到更细化更易于管理的目的。
微前端概念由来
微前端的概念借鉴自后端的微服务,主要是为了解决大型工程在变更、维护、扩展等方面的困难而提出的。
微前端方案都有哪些?
-
iframe
-
基座模式,主要基于路由分发,qiankun 和 single-spa 就是基于这种模式,主要思路是将一个大型应用拆分成若干个更小、更简单,可以独立开发、测试和部署的微应用,然后由一个基座应用根据路由进行应用切换
-
组合式集成,即单独构建组件,按需加载,类似 npm 包的形式
-
EMP,主要基于 Webpack5 Module Federation
-
Web Components
这些方案都不算是完整的微前端解决方案,它们只是用于解决微前端中运行时容器的相关问题
为什么不用iframe解决微前端问题?
如果不考虑体验问题,iframe 几乎是最完美的微前端解决方案了
iframe优点
- 提供了浏览器原生的硬隔离方案,不论是样式隔离、js 隔离这类问题统统都能被完美解决
iframe缺点
- 隔离性无法被突破,导致应用间上下文无法被共享,随之带来的开发体验、产品体验的问题
- url 不同步:浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用。UI 不同步,DOM 结构不共享。想象一下屏幕右下角 1/4 的 iframe 里来一个带遮罩层的弹框,同时我们要求这个弹框要浏览器居中显示,还要浏览器 resize 时自动居中…
- 全局上下文完全隔离,内存变量不共享:iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的子应用中实现免登效果。慢。每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程
由于部分问题很难解决所以舍弃了iframe作为微前端的解决方案。
微前端架构
核心价值
- 技术栈无关:主框架不限制接入应用的技术栈,微应用具备完全自主权
- 独立开发、独立部署:微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新
- 增量升级:在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略
- 独立运行时:每个微应用之间状态隔离,运行时状态不共享
目标
旨在解决单体应用在一个相对长的时间跨度下,由于参与的人员、团队的增多、变迁,从一个普通应用演变成一个巨石应用后,随之而来的应用不可维护的问题。这类问题在企业级 Web 应用中尤其常见。
架构
微应用原理
引用公众号前端巅峰一篇文章里面的图,画的非常详细,此处给个赞👍
子应用通信机制
引入微前端需要解决的问题有哪些?
- 封装脚手架
- 封装公共组件、业务组件,通用样式
- 主子应用通信
- 容器和微应用之间样式隔离
- 国际化支持
- 数据,资源共享
从0搭建一个微前端项目
主应用
主应用不限技术栈,只需要提供一个容器 DOM,然后注册微应用并 start
即可
// 先安装qiankun
yarn add qiankun # 或者 npm i qiankun -S
注册微应用并启动
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
{
name: 'vueApp',
entry: '//localhost:8080',
container: '#container',
activeRule: '/app-vue',
},
]);
// 启动 qiankun
start();
微应用
微应用分为有 webpack
构建和无 webpack
构建项目。
注意微应用的名称
package.json
=>name
需要和主应用中注册时的name
相对应,且必须确保唯一
有 webpack
有webpack的微应用(主要是指 Vue、React、Angular)需要做的事情有
-
新增
public-path.js
文件,用于修改运行时的publicPath
。什么是运行时的 publicPath ?注意:运行时的 publicPath 和构建时的 publicPath 是不同的,两者不能等价替代
-
微应用建议使用
history
模式的路由,需要设置路由base
,值和它的activeRule
是一样的 -
在入口文件最顶部引入
public-path.js
,修改并导出三个生命周期函数 -
修改
webpack
打包,允许开发环境跨域和umd
打包
主要的修改就是以上四个,可能会根据项目的不同情况而改变。例如,你的项目是 index.html
和其他的所有文件分开部署的,说明你们已经将构建时的 publicPath
设置为了完整路径,则不用修改运行时的 publicPath
(第一步操作可省)
无 webpack
无 webpack
构建的微应用直接将 lifecycles
挂载到 window
上即可
以vue微应用为例
-
在
src
目录新增public-path.js
if (window.__POWERED_BY_QIANKUN__) { __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; }
-
入口文件
main.js
修改,为了避免根 id#app
与其他的 DOM 冲突,需要限制查找范围import './public-path'; import Vue from 'vue'; import VueRouter from 'vue-router'; import App from './App.vue'; import routes from './router'; 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-vue/' : '/', mode: 'history', routes, }); instance = new Vue({ router, 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; }
-
打包配置修改(
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}`, }, }, };
应用间传值
多个应用间通信,举个简单的例子:主应用中登录获取用户
id
,当加载微应用时,微应用需要根据不同的用户id
展示不同的数据或者展示不同的页面。这个时候就需要主应用中把对应的用户id
传到微应用中去。传值方式,这里总结了三种方式
- 挂载微应用时直接
props
传值 initGlobalState
定义全局状态- 定义全局的状态池
props
传值
注册微应用的基础配置信息时,增加 props
,传入微应用需要的信息
{
name: 'vue2',
entry: 'http://localhost:8001',
container: '#subContainer',
activeRule: '/vue2',
//props
props: {
id: 'props基础传值方式'
},
loader,
}
微应用中在 mount
生命周期 props
中获取
export async function mount(props) {
console.log('获取主应用传值',props)
render(props);
}
initGlobalState (推荐)
定义全局状态,并返回通信方法,建议在主应用使用,微应用通过 props
获取通信方法。
- 主应用中声明全局状态
// 全局状态
const state = {
id: 'main_主应用',
};
// 初始化 state
const actions: MicroAppStateActions = initGlobalState(state);
// 监听状态变更
actions.onGlobalStateChange((state, prev) => {
// state: 变更后的状态; prev 变更前的状态
console.log(state, prev);
});
- 微应用获取通信,同样在 mount 生命周期中获取
export async function mount(props) {
console.log('initGlobalState传值',props)
render(props);
// 返回的方法中有
// initGlobalState 初始化 state
// onGlobalStateChange 监听状态变更
// setGlobalState 修改状态
// offGlobalStateChange 移除监听
}
-
在微应用某个页面内修改全局状态
可以把
props
中的方法挂载到当前应用的全局上export async function mount(props) { render(props); // 挂载到全局 instance 上 instance.config.globalProperties.$onGlobalStateChange = props.onGlobalStateChange; instance.config.globalProperties.$setGlobalState = props.setGlobalState; }
定义全局的状态池
定义全局状态池,就是在主应用中定义全局状态,可以使用
redux
vuex
等来定义。定义好全局状态,可以定义一个全局的类,类中声明两个方法,一个用来获取全局状态,一个用来修改全局状态。定义好之后,把这个类通过第一种props
的传值方式传入,微应用通过mount
=>props
接收。
官方建议使用第一种方法,简单方便
源码地址:
https://github.com/qiaochunmei/qiankun_template.git
参考网址:
https://qiankun.umijs.org/zh/guide
https://juejin.cn/post/6986834120290598942
https://mp.weixin.qq.com/s/aXlNPl32uW_463lXaLJelQ