微前端学习笔记

123 阅读4分钟

一.qiankun

1.主应用(以react为例)

1660807433412.png

  • 在src目录下建registerApps.js文件 安装qiankun yarn add qiankun
import { registerMicroApps, start } from "qiankun" //底层基于single-spa

const loader = (loading) => {
    //加载状态
    console.log(loading,'loading')
}
registerMicroApps([
    {
        name: "m-vue",
        entry: '//localhost:20000',
        container: "#container",
        activeRule: '/vue',
        props:{a:1},//传值到vue微应用
        loader
    },
    {
        name: "m-react",
        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
    }
})
  • 在index.js中 引入registerApps.js
import React from 'react';
import ReactDOM from 'react-dom/client';

import App from './App';
import './registerApps'//引入

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <App />
);
  • 在App.js中 注意:路由时react要手动安装react-router-dom
import {BrowserRouter as Router,Link } from 'react-router-dom'
function App() {
  return (
    <div className="App">
      <Router>
        <Link to="/vue">加载vue应用</Link>
        <Link to="/react">加载react应用</Link>
      </Router> 
      {/* 切换导航,将微应用渲染到container里面 */}
      <div id="container"></div>
    </div>
  );
}
export default App;

2.微应用(vue3)

  • 在main.js中
import { createApp } from "vue";
import App from "./App.vue";
import { createRouter, createWebHistory } from "vue-router";
import routes from "./router";

let history = null;
let router = null;
let app;
//不能直接挂载   需要切换  再调mount方法再去挂载
function render(props = {}) {
  history = createWebHistory("/vue");
  router = createRouter({
    history,
    routes,
  });
  app = createApp(App);
  let { container } = props;
  app.use(router).mount(container ? container.querySelector("#app") : "#app");
}
if(window.__POWERED_BY_QIANKUN__){
    //如果在qiankun中运行 动态添加publicPath
    __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
//乾坤在渲染前  提供了个变量 window.__POWERED_BY_QIANKUN__
if (!window.__POWERED_BY_QIANKUN__) {
  render();
}

//需要暴露接入协议  需是 promise
export async function bootstrap() {
  console.log("vue3 bootstrap");
}
export async function mount(props) {
  console.log("vue3 mount", props);
  render(props);
}
export async function unmount() {
  console.log("vue3 unmount");
  history = null;
  router = null;
  app = null;
}

  • 在根目录下建 vue.config.js
module.exports = {
  //!20000后面的 / 注意别掉
  publicPath: "//localhost:20000/", //保证子应用静态资源都是向20000端口发送的
  devServer: {
    port: 20000, //fatch
    headers: {
      "Access-Control-Allow-Origin": "*", //允许跨域
    },
  },
  configureWebpack: {
    //获取我打包的内容  systemjs => umd格式
    output: {
      library: "m-vue", //${name}-[name]  window['m-vue']
      libraryTarget: "umd", // 把微应用打包成 umd 库格式
    },
  },
};

//3000 -> 20000 基座去找20000端口的资源,publicPath  '/' 以3000找资源

  • 在router中的index.js里 直接导出路由配置 routes数组 在main.js 的render方法里面有创建路由
import Home from "../views/Home.vue";
const routes = [
  {
    path: "/",
    name: "Home",
    component: Home,
  },
  {
    path: "/about",
    name: "About",
    component: () =>
      import(/* webpackChunkName: "about" */ "../views/About.vue"),
  },
];

export default routes;

3.微应用为react

  • 首先安装插件@rescripts/cli yarn add @rescripts/cli -D
  • 根目录新增 .rescriptsrc.js
//.rescriptsrc.js
module.exports = {
    webpack: (config) => {
      config.output.library = `m-react`;//${name}-[name]
        config.output.libraryTarget = 'umd';
        config.output.publicPath="//localhost:30000/"
    //   config.output.jsonpFunction = `webpackJsonp_${name}`;
    //   config.output.globalObject = 'window';
  
      return config;
    },
  
    devServer: (config) => {
      config.headers = {
        'Access-Control-Allow-Origin': '*',
      };  
      return config;
    },
  };
  • 修改 package.json
"scripts": {
    "start": "rescripts start",
    "build": "rescripts build",
    "test": "rescripts test",
    "eject": "rescripts eject"
  }
  • 在根目录建.env文件(react修改端口号需要在.env文件中配置)
PORT=30000
WDS_SOCKET_PORT=30000
  • 在src下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();
}
//需要暴露接入协议  需是 promise
export async function bootstrap() {
}
export async function mount(props) {
  render(props);
}
export async function unmount(props) {
  let { container } = props;
  ReactDOM.unmountComponentAtNode(container?container.querySelector('#root'):document.getElementById('root'))
}

//qiankun  中处理样式  如何处理
//默认情况下切换应用  他会采用动态样式表  加载的时候添加样式,删除的时候卸载样式

//主应用和子应用  样式如何隔离
//1.根据BEM规范
//2.动态生成一个前缀(css-modules)
//3.shadowDom(影子dom)  类似video标签  快进 放大功能   增加全局样式会有问题

微前端续

1.概念

1660791932547.png

2.single-spa

缺陷
1.不够灵活 不能动态加载js
2.样式不隔离 没有js沙箱机制

1.子应用中

安装

//vue项目中
npm i single-spa-vue
//react项目中
npm i single-spa-react

在main.js中

import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import singleSpaVue from "single-spa-vue";
Vue.config.productionTip = false;

const appOptions = {
  el: "#vue",//挂载到父应用 id为vue的标签中
  router,
  render: (h) => h(App),
};

const vueLifeCycle = singleSpaVue({
  Vue,
  appOptions,
});

//作为子应用执行
if (window.singleSpaNavigate) {
  //配置publicPath 请求资源时都会把这个路径拼接在前面 生成绝对路径
  __webpack_public_path__ = "http://localhost:10000/";
}

if (!window.singleSpaNavigate) {
  //不是子应用时执行
  delete appOptions.el;
  new Vue(appOptions).$mount("#app");
}
export const bootstrap = vueLifeCycle.bootstrap;
export const mount = vueLifeCycle.mount;
export const unmount = vueLifeCycle.unmount;

配置vue.config.js将子应用打包成一个类库供父应用使用

module.exports = {
  configureWebpack: {
    output: {
      library: "singleVue",//类库的名字叫singleVue
      libraryTarget: "umd",//打包成umd模块
    },
    devServer: {
      port: 10000,
    },
  },
};

1660796999926.png

2.主应用中

安装

npm i single-spa

在main.js

import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import { registerApplication, start } from "single-spa";

Vue.config.productionTip = false;

//拿到 app.js和chunk-vendors.js后通过script标签 放在head里面加载
async function loadScript(url) {
  return new Promise((resolve, reject) => {
    let script = document.createElement("script");
    script.src = url;
    script.onload = resolve;
    script.onerror = reject;
    document.head.appendChild(script);
  });
}

// 1.参数1 myVueApp为子应用的名称
// 2.参数2 applicationOrLoadingFn 启动应用的函数 必须为promise
registerApplication(
  "myVueApp",
  async () => {
    //加载模块
    await loadScript("http://localhost:10000/js/chunk-vendors.js");//拿到打包的类库 第三方模块打包后存放位置chunk-vendors.js
    await loadScript("http://localhost:10000/js/app.js");//拿到打包的类库 app.js
    //1.singleVue为子应用中 vue.config.js类库的名字
    //2.通过 window.singleVue 拿到子应用导出的 bootstrap/mount/unmount
    return window.singleVue;  
  },
  (location) => location.pathname.startsWith("/vue"),//用户切换到 /vue路由时会加载 上面的async 加载模块
  { a: 1, b: 2 }//传到子应用的参数
);

start();//开启应用

new Vue({
  router,
  render: (h) => h(App),
}).$mount("#app");

在App.vue中

<template>
  <div id="app">
    <router-link to="/vue">加载vue应用</router-link>
    <!-- 子应用加载的位置 -->
    <div id="vue">sssssss</div>
  </div>
</template>

3.样式隔离

  • 子应用与子应用间样式隔离
加载另一个子应用时把上一个子应用的样式移除
  • 子应用与夫应用的样式隔离
    1660797649783.png

4.shadow Dom的实现

1660803508398.png

5.JS沙箱(有以下两种方式实现)

(1)快照沙箱(仅支持单应用)

//如果应用 加载 刚开始我加载A应用 window.a 再加载B应用时(可以访问上个A应用的属性 window.a造成污染)
//单应用切换 沙箱 创造一个干净的环境给这个子应用使用,当切换时 可以选择丢弃属性和恢复属性
//实现方式有 js沙箱 和 proxy
//快照沙箱  1年前拍一张 再拍一张 (将区别保存起来) 再回到一年前
class SnapshotSandbox {
    constructor (){
        this.proxy = window //window属性
        this.modifyPropsMap = {} //记录window的修改
        this.active()//初始时激活沙箱
    }
    active (){
        //沙箱激活时状态
        this.windowSnapshot = {}//给window拍个照 记录window上的属性
        for(const prop in window){
            if(window.hasOwnProperty(prop)){
                //这个属性如果在window上就保存起来
                this.windowSnapshot[prop] = window[prop]
            }
        }
        //当激活时就把上次修改进行一个应用
        Object.keys(this.modifyPropsMap).forEach(p=>{
            window[p] = this.modifyPropsMap[p]
        })
    }
    inactive (){
        //沙箱失效时状态
        for(const prop in window){
            if(window.hasOwnProperty(prop)){
                //判断如果window属性和以前比是否有变化
                if(window[prop] !== this.windowSnapshot[prop]){
                    //有变化后放在 this.modifyPropsMap这个记录属性中
                    this.modifyPropsMap[prop] = window[prop]
                    //失活再把window变回以前初始状态
                    window[prop] = this.windowSnapshot[prop]
                }
            }
        }
    }
}
let sandbox = new SnapshotSandbox()
((window)=>{
    //测试下
    window.a = 1
    window.b = 2
    console.log(window.a,window.b)
    sandbox.inactive();//先失活
    console.log(window.a,window.b)
    sandbox.active();//再激活
    console.log(window.a,window.b)
})(sandbox.proxy) //sandbox.proxy就是window

(2)代理沙箱(可以支持多应用) 把多个应用 用多个代理来实现 1660806439848.png 1660806463029.png