项目使用qiankun 改造的背景:
项目A、项目B、项目C;
项目A和项目B具有清晰的服务边界,从服务类型的角度能够分为两个项目。
在公司项目一体化的背景下,所有的项目又应该是一个项目。
项目B研发启动的时候
1. 由于开发时间紧张;
2. 项目B需要共用A项目中的“项目模块”和“人员管理”模块;
3. 项目B中的功能模块根据项目A的路由进行激活加载;
基于以上的情况,采取了在项目A中增加模块进行项目B的开发,
由于B项目包含在A项目中,当A项目和B项目同时开始需求迭代的时候,两个开发人员开始代码合并的时候简直就是灾难,需要花费大量的时间小心谨慎的进行这项工作
为了不使项目A变为巨石应用,需要将A项目进行解构。
微前端架构旨在解决单体应用在一个相对长的时间跨度下,由于参与的人员、团队的增多、变迁,从一个普通应用演变成一个巨石应用(Frontend Monolith)后,随之而来的应用不可维护的问题。
iframe 是在主应用中嵌入子应用系统,iframe 为子应用提供了一个完美的隔离环境,完美的样式隔离和js 隔离。
iframe 所带来的问题:
1. iframe拥有独立的window 。独立的浏览器缓存(无法便捷的实现单点登陆。例如:主应用登陆后将token 存储在sessionStorage中,子应用无法直接拿到token。)
2. 刷新后iframe url 状态会丢失。
3. iframe是会阻塞页面的加载的,会影响到网页的加载速度。比如window的onload事件会在页面或者图像加载完成后立即执行,但是如果当前页面当中用了iframe了,那还需要把所有的iframe当中的元素加载完毕才会执行,这样就会让用户感觉网页加载速度特别慢,影响体验感。
不同于qiankun的基座应用,模块联邦是去中性化的,两个项目间可以互相引用。
在webpack.config.js中进行配置ModuleFederationPlugin插件
模块联邦其实可以当作是webpack5将需要导出来的组件打包成一个运行时的文件,然后在其他项目可以进行运行时的动态加载,加载的过程是异步的,执行的时候是同步的。根据这个特性我们可以实现一个中心化组件模块中心,然后对外进行模块的分发。
技术栈无关。
主应用与微应用能够独立运行,独立开发。
微应用与主应用之间做到了该隔离的隔离,不该隔离的共用。(浏览器缓存可共用)
当遇到像我们这种项目情况的时候:
主应用与微应用基地相同,意味着,两个项目的缓存操作方法相同,vuex store方法相同时,应该首选采取qiankun 接入微应用的方式。
由于qinkun 没有隔离浏览器缓存,因此,可以不用考虑子应用的登录问题,菜单栏tab 的显示问题。
简直比德芙还丝滑!!
首先,qiankun 并不是单一个框架,它在 single-spa 基础上添加更多的功能。以下是 qiankun 提供的特性:
实现了子应用的加载,在原有 single-spa 的 JS Entry 基础上再提供了 HTML Entry
样式和 JS 隔离
更多的生命周期:beforeMount, afterMount, beforeUnmount, afterUnmount
子应用预加载
全局状态管理
全局错误处理
首先你需要一个基座来承载各类跨技术栈的子应用,这里以vue为例
首先在src下写一个registerApps.js
import { registerMicroApps, start } from "qiankun"; // 底层是基于single-spa
const loader = (loading) => {
console.log(loading);
};
registerMicroApps(
[
{
name: "m-vue",//package.json的name
entry: "//localhost:20000",//项目起的端口号
container: "#container",
activeRule: "/vue",
loader,
},
{
name: "reactApp",
entry: "//localhost:30000",
container: "#container",
activeRule: "/react",
loader,
},
],
{
beforeLoad: () => {
console.log("加载前");
},
beforeMount: () => {
console.log("挂在前");
},
afterMount: () => {
console.log("挂载后");
},
beforeUnmount: () => {
console.log("销毁前");
},
afterUnmount: () => {
console.log("销毁后");
},
}
);
start({
sandbox: {
// experimentalStyleIsolation:true
strictStyleIsolation: true,
},
});
在main.js中引入
import ‘./registerApps’
vue微应用
vue.config.js
module.exports = {
publicPath: '//localhost:20000', //保证子应用静态资源都是像20000端口上发送的
devServer: {
port: 20000, // fetch
headers:{
'Access-Control-Allow-Origin': '*'
}
},
configureWebpack: { // 需要获取我打包的内容 systemjs=》 umd格式
output: {
libraryTarget: 'umd',
library: 'm-vue'// window['m-vue']
}
}
}
// 3000 -> 20000 基座回去找20000端口中的资源, publicPath /
router的index.js
导出的是路由表,不是router
const routes = [
{
path: '/',
name: 'Home',
component: () => import( '../views/Home.vue')
},
{
path: '/about',
name: 'About',
component: () => import( '../views/About.vue')
}
]
export default routes
main.js
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router';
import App from './App.vue'
import routes from './router'
// 不能直接挂载 需要切换的时候 调用mount方法时候再去挂载
let history;
let router;
let app;
function render(props = {}){
history = createWebHistory('/vue');//加上路由前缀
router = createRouter({
history,
routes
});
app = createApp(App);
let {container} = props; // 默认他会拿20000端口的html插入到容器中,
//没传container,就是自己跑起来的,传了代表是在基座中跑的
app.use(router).mount(container ? container.querySelector('#app'):'#app')
}
// 乾坤在渲染前 给我提供了一个变量 window.__POWERED_BY_QIANKUN__
if(!window.__POWERED_BY_QIANKUN__){ // 独立运行自己
render();
console.log(window.__POWERED_BY_QIANKUN__)
}
// 需要暴露接入协议,返回需要是promise,所以加上async
export async function bootstrap(){
console.log('vue3 app bootstraped');
}
export async function mount(props){
console.log('vue3 app mount',);
render(props)
}
export async function unmount(){
console.log('vue3 app unmount');
history = null;
app = null;
router = null;
}
react微应用
.rescriptsrc.js
在package.json中的scripts也要修改,因为.rescriptsrc的使用
module.exports = {
webpack:(config)=>{
config.output.library = 'm-react';
config.output.libraryTarget = 'umd';
config.output.publicPath = '//localhost:30000/';
return config
},
devServer:(config)=>{
config.headers = {
'Access-Control-Allow-Origin':'*'
};
return config
}
}
.env
PORT=30000
WDS_SOCKET_PORT=30000
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
reportWebVitals();
function render(props = {}) {
let { container } = props;
ReactDOM.render(<App />,
container ? container.querySelector('#root') : document.getElementById('root')
);
}
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
export async function bootstrap() {
}
export async function mount(props) {
render(props)
}
export async function unmount(props) {
const { container } = props;
ReactDOM.unmountComponentAtNode(container ? container.querySelector('#root') : document.getElementById('root'))
}
qiankun 中处理样式 如何处理的
子应用与子应用他会采用动态样式表 加载的时候添加样式 删除的时候卸载样式 (子应用之间的样式隔离)
主应用和子应用 如何隔离 (我们可以通过BEM规范) -> (css-modules) 动态生成一个前缀 (并不是完全隔离)
其实是因为start()函数中有一个sandbox沙箱。其中使用shadow dom来解决样式冲突,shadow dom就是一个隔离的环境,他会把子应用的所有内容放在shadowdom里面,shadow dom中的样式不会影响外部的样式。
通过影子 DOM 就可以将一个 完整的 DOM 树 作为节点添加到 父 DOM 树。
即可以实现 DOM 封装,意味着 CSS 样式和 CSS 选择符可以限制在影子 DOM 子树中,而不是作用于整个顶级 DOM 树。
影子 DOM 是通过 attachShadow() 方法创建并添加给有效 HTML 元素的:
影子宿主**(shadow host)**,即容纳影子 DOM 的元素
影子根(shadow root),即影子 DOM 的根节点
attachShadow() 方法需要一个 shadowRootInit 对象,即这个对象必须包含一个 mode 属性,值为 “open” 或 “closed”
mode 属性值为 “open” 的影子 DOM 的引用可通过 shadowRoot 属性在 HTML 元素上获得,属性值 “closed” 影子 DOM 的引用则无法获取
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
div {
color: blue !important;
}
</style>
</head>
<body>
<div>hello world</div>
<script>
const appContent = `
hello world
`; // 好比qiankun中获取的html,我们拿到后包裹了一层
const containerElement = document.createElement("div");
console.log(containerElement);
containerElement.innerHTML = appContent;
const appElement = containerElement.firstChild; // 拿出第一个儿子,中的内 容
const { innerHTML } = appElement;
appElement.innerHTML = "";
let shadow = appElement.attachShadow({ mode: "open" }); // 将父容器变为 shadowDOM
shadow.innerHTML = innerHTML; // 将内容插入到shadowDOM中
document.body.appendChild(appElement);
</script>
</body>
</html>
正常情况下,影子 DOM 一添加到元素中,浏览器就会赋予它 最高优先级,优先渲染它的内容而不是原来的 dom 内容,比如下面的例子:
document.body.innerHTML = `
I'm foo's child
`;
const foo = document.querySelector('#foo');
const openShadowDOM = foo.attachShadow({
mode: 'open'
});
// 为影子 DOM 添加内容
openShadowDOM.innerHTML = `
this is openShadowDOM content
`
js沙箱隔离主要分为三种,snapshot sandbox(快照沙箱)、Proxy sandbox(代理沙箱)、lagacySandBox(遗留沙箱)。
原理就是在子应用激活 / 卸载时分别去经过快照的形式记录/还原状态来实现沙箱的。总结起来,对当前的 window 和浅拷贝的快照作 diff 来实现沙箱。
但是快照沙箱明显的缺点就是每次切换时需要去遍历window,这种做法会有较大的时间消耗。
在微应用修改 window.xxx 时直接记录 diff,将其用于环境恢复
了避免真实的 window 被污染,qiankun 实现了 proxysandbox。它的想法是:
把当前 window 的一些原生属性(如document, location等)拷贝出来,单独放在一个对象上,这个对象也称为 fakewindow
之后对每个微应用分配一个 fakewindow
当微应用修改全局变量时:
如果是原生属性,则修改全局的 window
如果不是原生属性,则修改 fakewindow 里的内容
微应用获取全局变量时:
如果是原生属性,则从 window 里拿
如果不是原生属性,则优先从 fakewindow 里获取
这样一来连恢复环境都不需要了,因为每个微应用都有自己一个环境,当在 active 时就给这个微应用分配一个 fakewindow,当 inactive 时就把这个 fakewindow 存起来,以便之后再利用。
方法:
with和eval
proxy+with
iframe
Html Entry办法的次要轨范如下:首先通过url获与到整个Html文件,从html中解析出html,js和css文原,正在主使用中创立容器,把html更新到容器中,而后动态创立style和script标签,把子使用的css和js赋值正在此中,最后把容器放置正在主使用中。
而 JS Entry 的理念就在加载微应用的时候用到了,在使用 single-spa 加载微应用时,我们加载的不是微应用本身,而是微应用导出的 JS 文件,而在入口文件中会导出一个对象,这个对象上有 bootstrap、mount、unmount 这三个接入 single-spa 框架必须提供的生命周期方法,其中 mount 方法规定了微应用应该怎么挂载到主应用提供的容器节点上,当然你要接入一个微应用,就需要对微应用进行一系列的改造,然而 JS Entry 的问题就出在这儿,改造时对微应用的侵入行太强,而且和主应用的耦合性太强。
HTML Entry 是由 import-html-entry 库实现的,通过 http 请求加载指定地址的首屏内容即 html 页面,然后解析这个 html 模版得到 template, scripts , entry, styles
1、子项目未 export 需要的生命周期函数
2.子项目加载时,容器未渲染好
检查容器 div 是否是写在了某个路由里面,路由没匹配到所以未加载。如果只在某个路由页面加载子项目,可以在页面的 mounted 周期里面注册子项目并启动。
3.子应用中的请求是200,但是在基座中却是400
因为你在基座去发子应用的请求时,子应用的代理会失效,解决方法就是给基座一个代理即可
devServer: {
proxy: {
'/proxyApi': {
changeOrigin: true,
target: 'http://redcloud.devops.sit.xiaohongshu.com',
pathRewrite: {
'^/proxyApi': '',
},
},
},
},
4.基座和微应用之前怎么通信?
1.基座可以在注册子应用时传入props,子应用在mount中的prop参数中可以接收到
基座
子应用