微前端qiankun使用+踩坑

背景

项目使用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 是在主应用中嵌入子应用系统,iframe 为子应用提供了一个完美的隔离环境,完美的样式隔离和js 隔离。

iframe 所带来的问题:
1. iframe拥有独立的window 。独立的浏览器缓存(无法便捷的实现单点登陆。例如:主应用登陆后将token 存储在sessionStorage中,子应用无法直接拿到token。
2. 刷新后iframe url 状态会丢失
3. iframe是会阻塞页面的加载的,会影响到网页的加载速度。比如window的onload事件会在页面或者图像加载完成后立即执行,但是如果当前页面当中用了iframe了,那还需要把所有的iframe当中的元素加载完毕才会执行,这样就会让用户感觉网页加载速度特别慢,影响体验感。

webpack5模块联邦

不同于qiankun的基座应用,模块联邦是去中性化的,两个项目间可以互相引用。
在webpack.config.js中进行配置ModuleFederationPlugin插件
模块联邦其实可以当作是webpack5将需要导出来的组件打包成一个运行时的文件,然后在其他项目可以进行运行时的动态加载,加载的过程是异步的,执行的时候是同步的。根据这个特性我们可以实现一个中心化组件模块中心,然后对外进行模块的分发。

为什么选用qiankun

  1. 技术栈无关。

  2. 主应用与微应用能够独立运行,独立开发。

  3. 微应用与主应用之间做到了该隔离的隔离,不该隔离的共用。(浏览器缓存可共用)

当遇到像我们这种项目情况的时候:

    主应用与微应用基地相同,意味着,两个项目的缓存操作方法相同,vuex store方法相同时,应该首选采取qiankun 接入微应用的方式。

    由于qinkun 没有隔离浏览器缓存,因此,可以不用考虑子应用的登录问题,菜单栏tab 的显示问题。

简直比德芙还丝滑!!

什么是qiankun?

首先,qiankun 并不是单一个框架,它在 single-spa 基础上添加更多的功能。以下是 qiankun 提供的特性:

实现了子应用的加载,在原有 single-spa 的 JS Entry 基础上再提供了 HTML Entry
样式和 JS 隔离
更多的生命周期:beforeMount, afterMount, beforeUnmount, afterUnmount
子应用预加载
全局状态管理
全局错误处理
在这里插入图片描述

qiankun的使用

首先你需要一个基座来承载各类跨技术栈的子应用,这里以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'))
}

qinakun的样式隔离如何实现?

qiankun 中处理样式 如何处理的
子应用与子应用他会采用动态样式表 加载的时候添加样式 删除的时候卸载样式 (子应用之间的样式隔离)
主应用和子应用 如何隔离 (我们可以通过BEM规范) -> (css-modules) 动态生成一个前缀 (并不是完全隔离)
其实是因为start()函数中有一个sandbox沙箱。其中使用shadow dom来解决样式冲突,shadow dom就是一个隔离的环境,他会把子应用的所有内容放在shadowdom里面,shadow dom中的样式不会影响外部的样式。

什么是影子dom?

通过影子 DOM 就可以将一个 完整的 DOM 树 作为节点添加到 父 DOM 树。
即可以实现 DOM 封装,意味着 CSS 样式和 CSS 选择符可以限制在影子 DOM 子树中,而不是作用于整个顶级 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 = `<div id="qiankun">
           <div>hello world</div>
           <style>div{color:red}</style>
           </div>`; // 好比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 一添加到元素中,浏览器就会赋予它 最高优先级,优先渲染它的内容而不是原来的 dom 内容,比如下面的例子:

document.body.innerHTML = `
    <div id="foo">
      <h1>I'm foo's child</h1>
    </div>
`;
const foo = document.querySelector('#foo');
const openShadowDOM = foo.attachShadow({
    
    
  mode: 'open'
});
// 为影子 DOM 添加内容
openShadowDOM.innerHTML = `
  <p>this is openShadowDOM content</p>
`

在这里插入图片描述

qiankun的js隔离方案?

js沙箱隔离主要分为三种,snapshot sandbox(快照沙箱)、Proxy sandbox(代理沙箱)、lagacySandBox(遗留沙箱)。

sanpshotsandbox快照沙箱

原理就是在子应用激活 / 卸载时分别去经过快照的形式记录/还原状态来实现沙箱的。总结起来,对当前的 window 和浅拷贝的快照作 diff 来实现沙箱。
但是快照沙箱明显的缺点就是每次切换时需要去遍历window,这种做法会有较大的时间消耗。

legacysandbox遗留沙箱

在微应用修改 window.xxx 时直接记录 diff,将其用于环境恢复

Proxy sandbox代理沙箱

了避免真实的 window 被污染,qiankun 实现了 proxysandbox。它的想法是:
把当前 window 的一些原生属性(如document, location等)拷贝出来,单独放在一个对象上,这个对象也称为 fakewindow
之后对每个微应用分配一个 fakewindow
当微应用修改全局变量时:
如果是原生属性,则修改全局的 window
如果不是原生属性,则修改 fakewindow 里的内容
微应用获取全局变量时:
如果是原生属性,则从 window 里拿
如果不是原生属性,则优先从 fakewindow 里获取
这样一来连恢复环境都不需要了,因为每个微应用都有自己一个环境,当在 active 时就给这个微应用分配一个 fakewindow,当 inactive 时就把这个 fakewindow 存起来,以便之后再利用。

实现沙箱的几种思路?

方法:
with和eval
proxy+with
iframe

qiankun-子使用的加载

Html Entry办法的次要轨范如下:首先通过url获与到整个Html文件,从html中解析出html,js和css地址,正在基座中创立容器,把html更新到容器中,而后动态创立style和script标签,把子应用的css和js赋值正在此中,最后把容器放置正在基座中。

JS Entry

而 JS Entry 的理念就在加载微应用的时候用到了,在使用 single-spa 加载微应用时,我们加载的不是微应用本身,而是微应用导出的 JS 文件,而在入口文件中会导出一个对象,这个对象上有 bootstrap、mount、unmount 这三个接入 single-spa 框架必须提供的生命周期方法,其中 mount 方法规定了微应用应该怎么挂载到主应用提供的容器节点上,当然你要接入一个微应用,就需要对微应用进行一系列的改造,然而 JS Entry 的问题就出在这儿,改造时对微应用的侵入行太强,而且和主应用的耦合性太强。
缺点二:将整个微应用打包成一个 JS 文件,常见的打包优化基本上都没了,比如:按需加载、首屏资源加载优化、css 独立打包等优化措施。

Html Entry原理

HTML Entry 是由 import-html-entry 库实现的,通过 http 请求加载指定地址的首屏内容即 html 页面,然后解析这个 html 模版得到 template, scripts , entry, styles

{
    
    
  template: 经过处理的脚本,link、script 标签都被注释掉了,
  scripts: [脚本的http地址 或者 {
    
     async: true, src: xx } 或者 代码块],
  styles: [样式的http地址],
    entry: 入口脚本的地址,要不是标有 entry 的 script 的 src,要不就是最后一个 script 标签的 src
}

然后远程加载 styles 中的样式内容,将 template 模版中注释掉的 link 标签替换为相应的 style 元素。
然后向外暴露一个 Promise 对象
通过上面的阅读知道了 HTML Entry 最终会返回一个 Promise 对象,qiankun 就用了这个对象中的 template、assetPublicPath 和 execScripts 三项,将 template 通过 DOM 操作添加到主应用中,执行 execScripts 方法得到微应用导出的生命周期方法,并且还顺便解决了 JS 全局污染的问题,因为执行 execScripts 方法的时候可以通过 proxy 参数指定 JS 的执行上下文。

{
    
    
  // template 是 link 替换为 style 后的 template
    template: embedHTML,
    // 静态资源地址
    assetPublicPath,
    // 获取外部脚本,最终得到所有脚本的代码内容
    getExternalScripts: () => getExternalScripts(scripts, fetch),
    // 获取外部样式文件的内容
    getExternalStyleSheets: () => getExternalStyleSheets(styles, fetch),
    // 脚本执行器,让 JS 代码(scripts)在指定 上下文 中运行
    execScripts: (proxy, strictGlobal) => {
    
    
        if (!scripts.length) {
    
    
            return Promise.resolve();
        }
        return execScripts(entry, scripts, proxy, {
    
     fetch, strictGlobal });
    }
}

qiankun​​ 常见报错

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参数中可以接收到
基座
请添加图片描述
子应用
请添加图片描述

qiankun的核心就在于子应用的加载(Html Entry)和样式与js的隔离

猜你喜欢

转载自blog.csdn.net/wyh666csdn/article/details/125812028