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>
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"