qiankun实战

css隔离方案

  • 子应用之间样式隔离
    Dynamic Stylesheet 动态样式表,当应用切换时移除老应用样式,添加新应用样式

  • 主应用和子应用之间的样式隔离
    BEM(Block Element Module)约定项目前缀
    CSS-Modules 打包时生成不冲突的选择器名
    Shadow DOM 真正意义上的隔离
    css-in-js

shadow DOM实例,节点没有挂在body下这个方案可以达到真正意义的隔离。react 项目中很多弹窗是挂在body下,就会遇到意想不到的问题

<!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>
</head>
<body>
    <div>
        <p>hello word</p>
        <div id='shadow'></div>
    </div>
    <script>
        // dom的api 
        let shadowDom = shadow.attachShadow({mode: 'closed'}); // 外界无法访问 shadow dom
        let pElm = document.createElement('p');
        pElm.innerHTML = 'hello lili';

        let styleElm = document.createElement('style');
        styleElm.textContent = `
            p{color: red;}
        `;
        shadowDom.appendChild(styleElm);
        shadowDom.appendChild(pElm);
    </script>
</body>
</html>
image.png

js沙箱

如果应用加载,刚开始加载A应用 window.a ,后来加载B应该也可以访问到window.a
单应用切换 沙箱就是创建一个干净的环境给这个子应用使用,当切换时,可以选择丢弃属性和恢复属性
js沙箱 proxy
快照沙箱,1年前拍一张,再拍一张,将区别保存起来,再回到一年前

如果是多个子应用就不能使用这种方式了,就要使用es6的proxy
代理沙箱可以实现多应用沙箱,把不同的应用用不同的代理实现来处理

<!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>
</head>
<body>
  <script>
    class SnapshotSandbox{
      constructor() {
        this.proxy = window; // window属性
        this.modifyPropsMap = {}; // 记录在window上的修改
        this.active();
      }
      active(){
        this.windowSnapshot = {}; // 拍照
        for(const prop in window){
          if(window.hasOwnProperty(prop)){
            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)){ // 拿现在的和1年的做比对
            if(window[prop] !== this.windowSnapshot[prop]){
              this.modifyPropsMap[prop] = window[prop];
              window[prop] = this.windowSnapshot[prop];
            }
          }
        }
      }
    }
    let sandbox = new SnapshotSandbox();
    ((window)=> {
      window.a = 1;
      window.b = 2;
      sandbox.inactive();
      sandbox.active();
      sandbox.inactive();
    })(sandbox.proxy); // sandbox.proxy就是window
  </script>
</body>
</html>

qiankun

1、主应用采用vue

主应用中注册了三个子应用,分别为vue、react、angular

  • 第1步,在App.vue中加入子应用存放的节点位置
  • 第2步,在main.js中注册子应用,如registerMicroApps
  • 第3步,开启应用,start()
vue create qiankun-base
yarn add qiankun
yarn add element-ui
// App.vue
<template>
  <div>
    <el-menu :router="true" mode="horizontal">
      <!--主应用中可以放自己的路由-->
      <el-menu-item index="/">Home</el-menu-item> 
      <!--引用其他子应用-->
      <el-menu-item index="/vue">vue应用</el-menu-item>
      <el-menu-item index="/react">react应用</el-menu-item>
      <el-menu-item index="/angular">angular应用</el-menu-item>
    </el-menu>
    <router-view></router-view>
    <div id="vue"></div>
    <div id="react"></div>
    <div id="angular"></div>
  </div>
</template>
// main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import { registerMicroApps, start, addGlobalUncaughtErrorHandler } from 'qiankun'

Vue.use(ElementUI);
Vue.config.productionTip = false

registerMicroApps(
  [
    {
      name: 'vueApp',
      entry: '//localhost:10000', // 默认会加载这个html 解析里面的js 动态的执行(子应用必须支持跨域)
      container: '#vue',
      activeRule: '/vue',
      props: {
        a: 'aaaa'
      }
    },
    {
      name: 'reactApp',
      entry: '//localhost:20000', // 默认会加载这个html 解析里面的js 动态的执行(子应用必须支持跨域)
      container: '#react',
      activeRule: '/react'
    },
    {
      name: 'angularApp',
      entry: '//localhost:30000', // 默认会加载这个html 解析里面的js 动态的执行(子应用必须支持跨域)
      container: '#angular',
      activeRule: '/angular'
    },
  ],
  {
    beforeLoad: (app) => {
      // 加载子应用前,加载进度条
      //   NProgress.start();
      console.log('before load', app.name);
      return Promise.resolve();
    },
     // qiankun 生命周期钩子 - 挂载后
     afterMount: (app) => {
      // 加载子应用前,进度条加载完成
      //   NProgress.done();
      console.log('after mount', app.name);
      return Promise.resolve();
    },
  }
); // 注册应用

/**
 * 添加全局的未捕获异常处理器
 */
 addGlobalUncaughtErrorHandler((event) => {
  console.error(event);
  const { message: msg } = event;
  // 加载失败时提示
  if (msg && msg.includes("died in status LOADING_SOURCE_CODE")) {
    console.error("微应用加载失败,请检查应用是否可运行");
  }
});

start({
  prefetch: false, // 取消预加载
}); // 开启应用

new Vue({
  router,
  render: h => h(App)
}).$mount('#app')
主应用中加载三个子应用

2、vue子应用

  • 第1步,根据协议,main.js中导出bootstrap、mount、unmount三个函数,添加动态publicPath
  • 第2步,不作为微前端,也可以独立运行,使用window.__POWERED_By_QIANKUN进行判断
  • 第3步,更改webpack配置,vue.config.js中设置可以跨域
  • 第4步,更改路由,将基础路径改为/vue
vue create qiankun-vue
// main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'

Vue.config.productionTip = false

let instance = null;
function render(){
  instance = new Vue({
    router,
    render: h => h(App)
  }).$mount('#app'); // 这里是挂载到自己的html中,基座会拿到这个挂载后的html 将其插入进去
}

if (window.__POWERED_BY_QIANKUN__) { // 动态添加publicPath
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

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();
}
// vue.config.js
module.exports = {
  devServer: {
    port: 10000,
    headers: {
      'Access-Control-Allow-Origin': '*',
    },
  },
  configureWebpack: {
    output: {
      library: 'vueApp',
      libraryTarget: 'umd', // 把微应用打包成 umd 库格式
      jsonpFunction: `webpackJsonp_vueApp`,
    },
  },
};
// router/index.js

const router = new VueRouter({
  mode: 'history',
  base: '/vue',   //将process.env.BASE_URL改为/vue
  routes
})

3、react子应用

  • 第1步,根据协议入口文件index.js中导出,bootstrap、mount、unmount三个函数
  • 第2步,非微前端,也能正常跑
  • 第3步,基础路由配置,App.js中BrowserRouter加上basename="/react"
  • 第4步,修改webpack配置,允许跨域,修改导出库名
npm create-react-app qiankun-react
yarn add react-app-rewired --save-dev
yarn add react-router-dom --save
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';

function render(){
  ReactDOM.render(
    <React.StrictMode>
      <App />
    </React.StrictMode>,
    document.getElementById('root')
  );
}

if(!window.__POWERED_BY_QIANKUN__){
  render();
}

export async function bootstrap(){

};
export async function mount(){
  render();
};
export async function unmount(){
  ReactDOM.unmountComponentAtNode(document.getElementById('root'));
};
// App.js
import logo from './logo.svg';
import './App.css';
import { BrowserRouter, Link, Route} from 'react-router-dom';

function App() {
  return (
    <BrowserRouter basename="/react">
      <Link to="/">首页</Link>
      <Link to="/about">关于</Link>
      <Route path="/" exact render={()=> {
         return <div className="App">
          <header className="App-header">
            <img src={logo} className="App-logo" alt="logo" />
            <p>
              Edit <code>src/App.js</code> and save to reload.
            </p>
            <a
              className="App-link"
              href="https://reactjs.org"
              target="_blank"
              rel="noopener noreferrer"
            >
              Learn React
            </a>
          </header>
        </div>
      }}></Route>
      <Route path="/about" render={()=> {
        return <h1>关于页面</h1>
      }}></Route>
    </BrowserRouter>
   
  );
}

export default App;
// 修改端口 .env
PORT=20000
WDS_SOCKET_PORT=20000
// config.overrides.js
module.exports = {
    webpack: (config)=> {
        config.output.library = 'reactApp';
        config.output.libraryTarget = 'umd';
        config.output.publicPath = 'http://localhost:20000/';
        return config;
    },
    devServer: (confgFunction)=> {
        return function(proxy, allowedHost){
            const config = confgFunction(proxy, allowedHost);

            config.headers = {
                'Access-Control-Allow-Origin': '*'
            };
            return config;
        }
    }
}

4、angular子应用

  • 第1步,根据协议,在入口文件main.qiankun.ts中,导出三个文件
  • 第2步,修改webpack配置,新增extra-webpack.config.js文件
  • 第3步,tsconfig中修改入口文件,修改angular.json
  • 第4步,修改路由,app/app-routing.module.ts 修改基础路由
npm install -g @angular/cli
ng new qiangkun-angular
npm i @angular-builders/custom-webpack 
// package.json
{
   ...
  "scripts": {
    "build:qiankun": "ng build --prod --deploy-url http://localhost:30000/",
    "serve:qiankun": "ng serve --disable-host-check --port 30000 --base-href /qiankun-angular --live-reload false"
  }
}
// 新增入口文件 main.qiankun.ts
import { enableProdMode, NgModuleRef } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';

if (environment.production) {
  enableProdMode();
}

// @ts-ignore
if (window.__POWERED_BY_QIANKUN__) {
    // @ts-ignore
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

if (!(window as any).__POWERED_BY_QIANKUN__) {
  platformBrowserDynamic()
    .bootstrapModule(AppModule)
    .catch(err => console.error(err));
}

let app: void | NgModuleRef<AppModule>;
async function render() {
  app = await platformBrowserDynamic().bootstrapModule(AppModule).catch((err) => console.error(err));
}
export async function bootstrap(props: Object) {
    console.log(props);
}
  
export async function mount(props: Object) {
    render();
}

export async function unmount(props: Object) {
    console.log(props);
    // @ts-ignore
    app.destroy();
}
// app/app-routing.module.ts 修改基础路由
import { APP_BASE_HREF } from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

const routes: Routes = [];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
  providers: [{  
    provide: APP_BASE_HREF, 
    // @ts-ignore
    useValue: window.__POWERED_BY_QIANKUN__ ? '/angular' : '/' 
  }]
})
export class AppRoutingModule { }
// tsconfig.app.json
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "outDir": "./out-tsc/app",
    "types": []
  },
  "files": [
    "src/main.qiankun.ts", // 修改入口文件
    "src/polyfills.ts"
  ],
  "include": [
    "src/**/*.d.ts"
  ]
}
// 新增extra-webpack.config.js
module.exports = {
  devServer: {
    headers: {
      'Access-Control-Allow-Origin': '*',
    },
  },
  output: {
    library: 'angularApp',
    libraryTarget: 'umd',
    // jsonpFunction: `webpackJsonp_angularApp`,
  },
};
// 修改angular.json
architect.build.builder = "@angular-builders/custom-webpack:browser"
architect.build.options.customWebpackConfig = {
     "path": "./extra-webpack.config.js"
}
architect.build.options.main = "src/main.qiankun.ts"
architect.serve.builder = "@angular-builders/custom-webpack:dev-server"
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345

推荐阅读更多精彩内容