前言
- 第一篇的single-spa模式存在样式没有隔离,以及需要手动引入js的问题。
样式隔离方案
- 主应用与子应用解决样式隔离一般有以下几种方案:
- BEM 即约定项目前缀
- CSSMoudule 打包时生成不冲突的选择器
- shadowDom 真正意义上隔离
- css in js
shadow dom
- 看一个例子:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div>
<p>hellow world</p>
<div id="shadow"></div>
</div>
</body>
<script>
let shadowDom = shadow.attachShadow({ mode: "closed" });
let p = document.createElement("p");
p.innerHTML = "yehuozhili";
let style = document.createElement("style");
style.textContent = `
p{color:red};
`;
shadowDom.appendChild(style);
shadowDom.appendChild(p);
</script>
</html>
- 其中影子dom 里放入了样式表,整个document有2个p标签,但是受影响的只有影子标签的p ,外界不受影响。
沙箱方案
- proxy
- 快照
- 来看一个快照的例子:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div>
<p>hellow world</p>
<div id="shadow"></div>
</div>
</body>
<script>
class SnapshotSandbox {
constructor() {
this.proxy = window;
this.modifyPropsMap = {};
this.active(); //先走了active 并且把window上属性存到了windowsnapshot上
}
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() {
//失活 就是当前和开始进入的快照进行比对,不一样就存到修改map,同时恢复开始进入的快照
for (const prop in window) {
if (window.hasOwnProperty(prop)) {//不一样的属性存到修改map里
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;
console.log(window.a, window.b);
sandbox.inactive();
console.log(window.a, window.b);
sandbox.active();
console.log(window.a, window.b);
})(sandbox.proxy);
</script>
</html>
- 其实就是浅比较window然后恢复就完了。
- proxy方案:
class ProxySandbox {
constructor() {
const rawWindow = window;
const fakeWindow = {}
const proxy = new Proxy(fakeWindow, {
set(target, p, value) {
target[p] = value;
return true
},
get(target, p) {
return target[p] || rawWindow[p];//这里就是取不到会找window的
}
});
this.proxy = proxy
}
}
let sandbox1 = new ProxySandbox();
let sandbox2 = new ProxySandbox();
window.a = 1;
((window) => {
window.a = 'hello';
console.log(window.a)
})(sandbox1.proxy);
((window) => {
window.a = 'world';
console.log(window.a)
})(sandbox2.proxy);
乾坤
- 乾坤是基于single-spa进行封装的
- 首先,我们创建3个应用,qiankun-base,qiankun-react,qiankun-vue
- 编写base
- 安装element-ui 与乾坤
main.js
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import "element-ui/lib/theme-chalk/index.css";
import ElementUI from "element-ui";
import { registerMicroApps, start } from "qiankun";
Vue.config.productionTip = false;
Vue.use(ElementUI);
const apps = [
{
name: "vueApp", //应用名
entry: "//localhost:10000", //默认会加载这个html,解析里面的js 动态的执行(子应用必须支持跨域)
container: "#vue", //容器名
activeRule: "/vue", //激活路径
},
{
name: "reactApp",
entry: "//localhost:20000",
container: "#react",
activeRule: "/react",
},
];
registerMicroApps(apps); //注册应用
start(); //开启
new Vue({
router,
render: (h) => h(App),
}).$mount("#app");
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>
<router-view></router-view>
<div id='vue'></div>
<div id='react'></div>
</div>
</template>
<style>
- 打开页面并且路由可以切换,会改变路由并且发请求就ok了。
- 下面修改子应用:
- 首先,需要修改子应用的前置路由:
const router = new VueRouter({
mode: "history",
base: "/vue",
routes,
});
- 修改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"); //仍然挂app上,父应用会拿到html将其插入。
}
if (!window.__POWERED_BY_QIANKUN__) {
//这个就跟上一节那个独立运行一个意思
render();
}
if (window.__POWERED_BY_QIANKUN__) {
//这个就跟上一节那个父应用加载一个意思
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
export async function bootstrap() {}
export async function mount(props) {
render(props);
}
export async function unmount(props) {
instance.$destroy();
}
- 修改vue.config.js,使其打包成一个库:
module.exports = {
devServer: {
port: 10000,
headers: {
"Access-Control-Allow-Origin": "*",
},
},
configureWebpack: {
output: {
library: "vueApp",
libraryTarget: "umd",
},
},
};
- 启动子应用,可以发现,子应用可以独立运行,切换也无问题,父应用也独立运行,切换也无问题。
- 感觉乾坤比single-spa方便很多啊。
- 父应用可以通过配置apps传递props,子应用可以在导出方法那获取相应位置进行接收。
- 下面配置react应用:
- 首先安装react-app-rewried 安装react-router-dom
- 修改package.json的script
- 修改config-overrides.js
module.exports = {
webpack: (config) => {
config.output.library = "reactApp";
config.output.libraryTarget = "umd";
config.output.publicPath = "http://localhost:20000/";
return config;
},
devServer: function (configFunction) {
return function (proxy, allowedHost) {
const config = configFunction(proxy, allowedHost);
config.headers = {
"Access-Control-Allow-Origin": "*",
};
return config;
};
},
};
- 修改端口号:
PORT=20000
WDS_SOCKET_PORT=20000
app.js
import React from "react";
import "./App.css";
import { BrowserRouter, Route, Link } from "react-router-dom";
function App() {
return (
<BrowserRouter basename="/react">
<Link to="/">首页</Link>
<Link to="/about">关于页</Link>
<Route path="/" exact render={() => <div>首页</div>}></Route>
<Route path="/about" exact render={() => <div>关于页</div>}></Route>
</BrowserRouter>
);
}
export default App;
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"));
}
- 这样就完成了3个页面互相切换且都可以独立运行或者结合运行了。