微前端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使用+踩坑_第1张图片

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 = `
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 一添加到元素中,浏览器就会赋予它 最高优先级,优先渲染它的内容而不是原来的 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

`

微前端qiankun使用+踩坑_第2张图片

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 的问题就出在这儿,改造时对微应用的侵入行太强,而且和主应用的耦合性太强。

Html Entry原理

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

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使用+踩坑_第3张图片
子应用
请添加图片描述

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

你可能感兴趣的:(前端,vue.js,javascript)