single-spa(未实现css隔离和js沙箱)
子应用
import Vue from 'vue';
import App from './App.vue';
import router from './router';
import singleSpaVue from 'single-spa-vue';
Vue.config.productionTip = false;
const appOptions = {
// 挂载到父项目对应id中
el: '#spa-container',
render: h => h(App),
router
};
const vueLifecycles = singleSpaVue({
Vue,
appOptions: appOptions
});
// 直接挂载会有冲突
// 如果是基座项目调用,就会存在singleSpaNavigate,否则不存在
if (!window.singleSpaNavigate) {
delete appOptions.el;
new Vue(appOptions).$mount('#app');
}
// 父项目注册时,会将参数传过来,可以自定义传参
export const bootstrap = props => {
console.log(props, 'bootstrap');
return vueLifecycles.bootstrap(props);
};
export const mount = props => {
console.log(props, 'mount');
return vueLifecycles.mount(props);
};
export const unmount = vueLifecycles.unmount;
基座
import { registerApplication, start } from 'single-spa';
const insetScript = src => {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = src;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
};
const insetCss = href => {
const link = document.createElement('link');
link.href = href;
link.rel = 'stylesheet';
document.head.appendChild(link);
};
const apps = [
{
// 该子应用的名字,自定义即可
name: 'manage-permission-spa',
// 当activeWhen为true时,将触发该方法,挂载app,需返回暴露出来的全局变量
app: async () => {
await insetScript(
'http://8.129.90.25:9000/admin/static/vendor.1.26e7843d1fbfff010b2e.js'
);
await insetScript(
'http://8.129.90.25:9000/admin/static/main/index.a63c24ab6bc9fd855ffa.js'
);
insetCss(
'http://8.129.90.25:9000/admin/static/main/index.ad87c26f69b2030d7fa2.css'
);
return window.managePermission;
},
// location匹配
activeWhen: location => location.hash.startsWith('#/permission/manage'),
// 传参到子应用
customProps: (name, location) => {
return {
data: 'data'
};
}
}
];
apps.forEach(app => registerApplication(app));
start();
路由处理
需要保证路由的前缀是一致的,hash的保证#后一致
比如主项目路由为http://localhost:8090/admin/#/permission/manage
,由于在项目中,使用的是hash模式,vue-router配置中不能设置base,所以使用时需要在vue-router路由前都加上/permission/manage/,保证hash前缀完全一致
如果主应用路由为http://localhost:8090/ticket/create
,并且路由使用的是history模式,那路由前缀就是http://localhost:8090/ticket/create
,可以在vue-router配置base:'/ticket/create'即可
父子通信
实现各模块之间相互通信,待更新...
模块加载
实现子应用模块路径自动获取,待更新...
公共依赖
// 在项目中使用cdn引入,如果在父项目和子项目都直接在node_module中引用,那么可能会有重复引入的错误
externals: {
vue: 'Vue',
'element-ui': 'ELEMENT',
'vue-router': 'VueRouter'
},
css隔离
在父项目的全局css样式,会影响到子项目
- 自己约定好项目前缀
- 使用css-module,打包时加上前缀(主流)
- css-in-js
- Shadow,dom真正意义上的隔离(qiankun框架使用)
使用postcss-selector-namespace给css统一添加前缀
// .postcssrc.js
module.exports = {
plugins: [
['postcss-preset-env'],
// postcss-selector-namespace: 给所有css添加统一前缀,然后父项目添加命名空间
['postcss-selector-namespace',{
namespace() {
return "#manage-permission-spa"; // 返回要添加的类名
}
}]]
};
js隔离
由于子应用都是在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];
}
})
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);
快照沙箱
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)) {
if (this.windowSnapshot[prop] !== window[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) //1
sandbox.inactive() //失活
console.log(window.a) //undefined
sandbox.active() //激活
console.log(window.a) //1
})(sandbox.proxy);
qiankun(基于single-spa)
子应用
import Vue from 'vue';
import App from './App.vue';
import VueRouter from 'vue-router';
import routes from './router';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI);
Vue.use(VueRouter);
Vue.config.productionTip = false;
let instance = null;
let router = null;
const render = (props = {}) => {
// 因为卸载后重新挂载,可能会造成路由丢失,所以在render内注册
router = new VueRouter({
// 父应用调用是,路由前缀/ticket/create
base: window.__POWERED_BY_QIANKUN__ ? '/ticket/create' : '/',
mode: 'history',
routes
});
instance = new Vue({
el: '#app',
router,
render: h => h(App)
});
};
if (window.__POWERED_BY_QIANKUN__) {
// 动态设置 webpack publicPath,防止资源加载出错
// eslint-disable-next-line no-undef,camelcase
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
} else {
render();
}
// 子应用需要导出这三个异步函数
export const bootstrap = async props => {
console.log('bootstrap', props)
};
export const mount = async props => {
render(props);
};
export const unmount = async () => {
instance.$destroy();
instance.$el.innerHTML = '';
instance = null;
router = null;
};
基座
import { registerMicroApps, start } from 'qiankun';
import { Loading } from 'element-ui';
let loadingInstance = null;
const apps = [
{
name: 'createTicket',
// entry: '//8.129.90.25:5555',
entry: '//localhost:3000',
container: '#qiankun-container',
activeRule: location => location.pathname.startsWith('/ticket/create'),
loader: loading => {
console.log(`loading变化了,当前状态: ${loading}`);
if (loading) {
loadingInstance = Loading.service({
fullscreen: true,
text: 'Loading',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.7)'
});
} else {
loadingInstance.close();
loadingInstance = null;
}
}
}
];
registerMicroApps(apps, {
beforeLoad: [
app => {
console.log('before load', app);
}
],
beforeMount: [
app => {
console.log('before mount', app);
}
],
afterUnmount: [
app => {
console.log('after unload', app);
}
]
});
export default start;
子应用webpack配置
const path = require('path');
const { name } = require('../../package');
output: {
path: path.resolve(__dirname, '../../dist'),
filename: 'static/[name]/index.[chunkhash].js',
chunkFilename: 'static/[name].[id].[chunkhash].js',
publicPath: '/',
// 下面为官方推荐配置
library: `${name}-[name]`,
libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_${name}`,
globalObject: 'window'
}
qiankun通过fetch获取资源会产生跨域问题,nginx解决跨域配置
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
js与css隔离问题
qiankun自动开启js与css隔离,但是,在实践中,发现导入element的css时,会有重复导入,导致默认样式重置的问题,采用scoped的方式,或者通过start传参experimentalStyleIsolation: true,来添加特殊选择器前缀,都能有效解决
父子通信
官网定义的initGlobalState,可以在父子应用之间进行通信,待更新..