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"