qiankun實戰(zhàn)

css隔離方案

  • 子應用之間樣式隔離
    Dynamic Stylesheet 動態(tài)樣式表,當應用切換時移除老應用樣式,添加新應用樣式

  • 主應用和子應用之間的樣式隔離
    BEM(Block Element Module)約定項目前綴
    CSS-Modules 打包時生成不沖突的選擇器名
    Shadow DOM 真正意義上的隔離
    css-in-js

shadow DOM實例,節(jié)點沒有掛在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
單應用切換 沙箱就是創(chuàng)建一個干凈的環(huán)境給這個子應用使用,當切換時,可以選擇丟棄屬性和恢復屬性
js沙箱 proxy
快照沙箱,1年前拍一張,再拍一張,將區(qū)別保存起來,再回到一年前

如果是多個子應用就不能使用這種方式了,就要使用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中加入子應用存放的節(jié)點位置
  • 第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 動態(tài)的執(zhí)行(子應用必須支持跨域)
      container: '#vue',
      activeRule: '/vue',
      props: {
        a: 'aaaa'
      }
    },
    {
      name: 'reactApp',
      entry: '//localhost:20000', // 默認會加載這個html 解析里面的js 動態(tài)的執(zhí)行(子應用必須支持跨域)
      container: '#react',
      activeRule: '/react'
    },
    {
      name: 'angularApp',
      entry: '//localhost:30000', // 默認會加載這個html 解析里面的js 動態(tài)的執(zhí)行(子應用必須支持跨域)
      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步,根據協(xié)議,main.js中導出bootstrap、mount、unmount三個函數,添加動態(tài)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__) { // 動態(tài)添加publicPath
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

if(!window.__POWERED_By_QIANKUN){ // 默認獨立運行
  render();
}

// 子組件的協(xié)議
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步,根據協(xié)議入口文件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"
              
              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步,根據協(xié)議,在入口文件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"
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容