# 微前端
几个核心价值:技术栈无关,独立开发、独立部署,增量升级,独立运行时。
single-spa 问题
问题
1 没有处理父子应用之间的样式隔离问题。
2 每个应用都使用 window 对象可能会存在冲突。 父应用切换子应用,每次切换都是同一个window。
3 仅仅实现了 路由劫持,和应用加载
4 single-spa不够灵活。不能动态加载js文件。 即父应用如果想要加载子应用,需要创建script把子应用的脚本添加进去,在切换到子应用时,把脚本添加到父应用head中。
# SPA VS MPA
微前端是由多个 spa 组成的 mpa。通过上下文的管理,生命周期的管理和通信机制的管理。让这些 spa 形成统一的整体。
- spa 单页应用,先加载一个统一的壳,在站内路由来形式转发。虽然初始加载比较慢,但是内部跳转非常快。
- mpa 多页应用,每次的页面是全加载的。
# 为什么不用 iframe
iframe 最大的特性就是提供了 浏览器原生的硬隔离方案 不论是样式隔离、js 隔离这类问题统统都能被完美解决。 问题也在于他的隔离性无法被突破,导致应用间上下文无法被共享。
- 1 url 不同步。浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用。
- 2 UI 不同步,DOM 结构不共享。想象一下屏幕右下角 1/4 的 iframe 里来一个带遮罩层的弹框,同时我们要求这个弹框要浏览器居中显示,还要浏览器 resize 时自动居中…
- 3 全局上下文完全隔离,内存变量不共享。iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的子应用中实现免登效果。
- 4 慢。每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程。
# Single-spa+Vue实践
# 父应用中主要过程(parent-vue)
- 1 registerApplication注册应用,当父应用切换到 '/vue'目录时。父应用动态加载子应用脚本(动态标签把app.js和chunk-vendors.js插入到父应用head中 )。
- 2 当加载子应用脚本,会去调用子应用的( bootstrap mount unmount),调用子应用mount时候就会走子应用 appOptions方法,把子应用放在应用的el节点上。
- 2 父应用start()启动。
# 子应用中主要完成以下过程(children-vue)
- 1 引入(single-spa-vue,single-spa-react) 把子应用实例挂到singleSpaVue上。并返回 子应用生命周期( bootstrap mount unmounted )
- 2 在vue.config.js中配置 把子应用打包成lib库在父应用中加载。
# 样式隔离方案
# 子应用之间样式隔离
- 当切换到当前子应用,加载当前子应用样式。当切换其他子应用,删除当前子应用样式,并切换子应用。
# 父子应用样式隔离方案
- BEM 约定项目前缀
- CSS-Modules 打包生成不冲突的选择器名。
- Shdow DOM 真正意义的隔离,
子应用 main.js配置
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import singleSpaVue from "single-spa-vue";
Vue.config.productionTip = false
new Vue({
router,
render: h => h(App)
}).$mount("#app");
const appOptions = {
el: "#childrenVue",
router,
render: h => h(App)
};
// 支持应用独立运行、部署,不依赖于基座应用
console.log('----window-',window)
if (!window.singleSpaNavigate) {
delete appOptions.el
new Vue(appOptions).$mount('#app')
}else{
//在页面跳转前拼绝对路径
__webpack_public_path__ = 'http://localhost:2000/'
}
// singleSpaVue包装一个vue微前端服务对象
const vueLifecycle = singleSpaVue({
Vue,
appOptions
});
// 导出生命周期对象
// 启动时
export const bootstrap = vueLifecycle.bootstrap;
// 挂载时
export const mount = vueLifecycle.mount;
// 卸载时
export const unmount = vueLifecycle.unmount;
const package = require('./package');
module.exports = {
devServer:{
port:2000
},
configureWebpack:{
output:{
//library的值在所有子应用中需要唯一
library:package.name,
//导出umd格式的包,在全局对象上挂载属性package.name,基座应用需要通过这个全局对象获取一些信息,比如子应用导出的生命周期函数
libraryTarget:'umd'
},
}
}
子应用vue.config.js
const package = require('./package');
module.exports = {
devServer:{
port:2000
},
configureWebpack:{
output:{
//library的值在所有子应用中需要唯一
library:package.name,
//导出umd格式的包,在全局对象上挂载属性package.name,基座应用需要通过这个全局对象获取一些信息,比如子应用导出的生命周期函数
libraryTarget:'umd'
},
}
}
父应用main.js文件配置
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import {registerApplication,start} from "single-spa";
//获取子应用url,插入到父应用的head中。
async function loadScript(url){
return new Promise(function (resolve,reject) {
let script = document.createElement('script');
script.src = url;
script.onload = resolve;
script.onerror = reject;
document.head.append(script);
})
}
//当父应用切换到 '/vue'目录。父应用动态加载子应用,并把子应用插入到父应用head中。
registerApplication('childrenVue',async ()=>{
//子应用通过打包生成类库(app.js,chunk-vendors.js)
//父应用加载子应用文件,需要自己构建script标签。动态插入到head中
console.log('加载模块')
await loadScript('http://localhost:2000/js/chunk-vendors.js')
await loadScript('http://localhost:2000/js/app.js');
return window.childrenVue;//bootstrap mount unmount
//用户切换到/vue,执行async方法。
},location => location.pathname.startsWith('/vue'))
//父应用启动
start();
//父应用注册挂载
createApp(App).use(router).mount('#app')