今天日技术分享正题微前端single-spa框架详解使用
关注微信公众号 阿勇学前端
微前端介绍: 微前端是一种将前端应用程序拆分为多个独立的、可独立开发、部署和运行的小型模块的架构概念。它旨在解决大型单页面应用(SPA)开发中的一些挑战,例如团队协作、代码耦合、整体应用的性能和可扩展性等问题。(大白话:一个前端项目在满足独立开发条件下还可以可以嵌套其他前端项目)
父子应用demo放在GitHub上别忘了点🌟🌟 github.com/AyongNice/s…
子应用如何改造 我们以vue2为示例demo
(独立运行的改造)
1首先main.js 新增全局路由跳转函数封装(因为子路由在父应用中路由跳转是基于父级的哈希(#:我就是哈希)路由后缀基础进行单页面切换的)下面会细讲
//子应用 无论单独运行 还是嵌入子应用都需要使用统一路由跳转方法
Vue.prototype.$navigateTo = function (route) {
const currentRoute = this.$router.currentRoute;
if ((currentRoute.path + currentRoute.hash) !== route.path) {
this.$router.push(route);
}};
(single-spa-vue环境运行的改造)
路由改造 ** mode: 'history',要哈希路由**
const router = new VueRouter({ mode: 'history', base: '/', routes,});
2然后子应用在src目录下新增spa.main.js文件内容如下
import Vue from 'vue';
import App from './App.vue';
import router from './router';
import singleSpaVue from 'single-spa-vue';
//子应用 无论单独运行 还是嵌入子应用都需要使用统一路由跳转方法
Vue.prototype.$navigateTo = function (route) {
const currentRoute = this.$router.currentRoute;
if ((currentRoute.path + currentRoute.hash) !== route.path) {
this.$router.push(route);
}};const vueLifecycles = singleSpaVue({
Vue,
appOptions: { el: '#microApp', render(h) {
return h(App);
},
router,
}});
// 子应用挂载前 **可以通过此方法接受父级的参数
export async function bootstrap(props) {
console.log('app1 bootstrap', props)
return vueLifecycles.bootstrap();
}
export const {unmount, mount} = vueLifecycles
1:子应用主要通过 bootstrap 声明周期接受父应用的参数,
2:在父级渲染时候父级会调用 子应用对外暴露的mount,
3:卸载时候调用的是 unmount
这时子应用就有两个入口文件了一个man.js 一个spa.main.js
这时候我们就要对package.json文件增加两个执行脚本命令
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"serve-single-spa": " export Mode=singleSpa && vue-cli-service serve",
"build-single-spa": " export Mode=singleSpa && vue-cli-service build",
"lint": "vue-cli-service lint"
},
serve-single-spa 和 build-single-spa 命令是用来 调试子应用内嵌时候的开发模式 与打包模式;因为项目接入single-spa微前端框架既要保证 可以独立运行也可以 嵌入部署运行
(这里要注意的地方就是 export 是Mac系统的环境变量设置, 如果你是window系统要把export 改成 SET)
通过执行不同的脚本命令 来运行不同的模式
子应用打包webpack 配置(重点!!!)****
const {defineConfig} = require('@vue/cli-service');
module.exports = defineConfig({
transpileDependencies: true, *//* 将所有的依赖模块都进行转译,*Babel*
configureWebpack: {
entry: { *//* 根据脚本设置的环境变量来判断单独运行还是嵌入父应用运行
app: (process.env.Mode || '').trim() === 'singleSpa' ? './src/spa.main.js' : './src/main.js', *//* 主应用的入口文件
},
// 重点: **将其导出为*library*库文件
output: {
*//* 导出名称 **你如果内嵌的应用他在父级里就是*app1* 对应到父应用的子应用注册表名字
library: "app1",
libraryTarget: "umd", *//* 挂载目标
},
*//* 开发环境下端口
devServer: {
port: '8083'
}
}
})
父应用改造
也要增加spa.main.js 文件;只不过的是main.js文件不需要任何改造
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
import router from './router';
import {registerApplication, start} from 'single-spa';
*/*** 沙箱*JS*隔离*class **/*
import WindowSandbox from './coom/index';
*//* 创建一个沙箱实例
const sandbox = new WindowSandbox();
// 获取沙箱中的 *window* 对象
const sandboxWindow = sandbox.getWindow();
let time
/**
** single-spa*框架底层逻辑是 **通过向父应用的*html*中添加 *script (* 内容就是子应用所渲染的*JS)* 标签形式添加子应用
** @param url url {string}* 子应用的服务部署地址
** @return {Promise<unknown>}*
**/
function createScript(url) {
return new Promise((resolve, reject) => {
const script = document.createElement('script')
script.src = url
script.onload = resolve
script.onerror = reject
const firstScript = document.getElementsByTagName('script')[0]
firstScript.parentNode.insertBefore(script, firstScript)
})
}
/***
*** 加载子应用的*JS*文件
** @param url {string}* 子应用的服务部署地址
** @param globalVar {string}* 子应用名称 **对应 **子应用*webpack*打包 *library: "app1",* 导出名称
** @param entrypoints {string*[] *}* 需要加载的子应用 *JS*文件列表 *chunk-vendors.js* 公共*JS app.js* 功能*JS*
** @return {Promise<*>}*
**/
async function loadApp(url, globalVar, entrypoints) {
for (let i = 0; i < entrypoints.length; i++) {
await createScript(url + entrypoints[i])
}
console.log('globalVar', window[globalVar])
return window[globalVar]
}
/**
*** 子应用路由注册表 **可以配置多个
** @type {* [ *{app: (function(): Promise<*>), activeWhen: (function(*): any), appName: string, customProps: {}, name: string}* ] *}*
**/
const apps = [{
name: 'app1', *//* 子应用名称 **对应 **子应用*webpack*打包 *library: "app1",* 导出名称
app: async () => await loadApp('http://localhost:8083', 'app1', ["/js/chunk-vendors.js", "/js/app.js"]), *//* 当路由满足条件时(返回*true*),激活(挂载)子应用
activeWhen: location => location.hash.startsWith('#/about'), *// activeWhen: location => true,//* 检测用户切换到 */vue* 路径下 **需要实现刚刚定义的加载事件
*//* 传递给子应用的对象 **子应用可以通过 *bootstrap* 接收到
customProps: {}
},
]
clearTimeout(time)
// 注册子应用
time = setTimeout(() => {
for (let i = apps.length - 1; i >= 0; i--) {
const item = apps[i]
registerApplication(item.name, item.app, item.activeWhen, {sandbox: {sandboxWindow}})
}
}, 500)
/*** 挂载父应用*DOM*示例 ***/
new Vue({
router, render: h => h(App), mounted() {
start()
},
}).$mount('#app')
LV下逻辑
1:single-spa框架底层逻辑是 通过向父应用的html中添加 script (内容就是子应用所渲染的JS) 标签形式添加子应用
2:子应用路由注册表 一个父应用可以内嵌多个子应用 这里我就列举了一个例子
父应用向子应用传参数都在这里配置好的 customProps 最终会循环调用registerApplication 将参数传过去,
还得子应用暴露的方法吗 bootstrap 用来接受之的父应用参数 mount用来传递给父应用的
activeWhen: location => location.hash.startsWith('#/about'), 在父应用中只有你切换到了你这个路由 ,子应用就会渲染, 路由变化自动卸载子应用
activeWhen是 single-spa框架中 registerApplication的参数接受子应用渲染主要靠它
registerApplication 方法中供它调用进行可以加载到子应用到JS内容
3:框架registerApplication方法尽量使用延时器满加载一下
4: 这里还有windos JS数据沙箱机制公共方法也需要传给子应用, 我放到GitHub上了,single-spa 是没有JS隔离以及样式隔离的所以要注意 在子应用中的一定不要使用全局变量, 如果需要 需要添加class 前缀,避免污染父应用的样式
一下面列举下父应用操作子应用路由方法(父应用操作子应用路由一定要在路由后缀添加#/${当前父应用的路由})
import {navigateToUrl,} from 'single-spa'; *//* 导航方法
navigateToUrl('/detail#/about');
该说到这里解释下开头为啥要在子应用封装 全局路由跳转方法了🌟重点需要注意
// 子应用跳转自己页面必须在路后加上 #然后跟 / 父应用所在激活路由
this.$navigateTo({
path: '/sun#/aboutdiy',
query: {
param1: 'value1',
param2: 'value2'
}
});
二 主动关闭卸载子应用
import {unregisterApplication} from 'single-spa'; //导航方法//传入要卸载掉子应用名字,就是子应用打包webpack配置的 library: "app1", unregisterApplication('app1')
父应用package。json脚本配置(跟子应用一样,通过不同的环境变量来运行 是否独立运行还是嵌入子应用运行)
import {unregisterApplication} from 'single-spa'; *//* 导航方法
// 传入要卸载掉子应用名字,就是子应用打包*webpack*配置的 *library: "app1"
unregisterApplication('app1')
父应用package。json脚本配置(跟子应用一样,通过不同的环境变量来运行 是否独立运行还是嵌入子应用运行)
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"serve-single-spa": " export Mode=singleSpa && vue-cli-service serve ",
"build-single-spa": " export Mode=singleSpa && vue-cli-service build ",
"lint": "vue-cli-service lint"
},
父应用webpack配置
const {defineConfig} = require('@vue/cli-service')nv.Mode)
module.exports = defineConfig({
transpileDependencies: true,
configureWebpack: {
entry: { *//* 根据脚本命名来配置单独运行还是嵌入父应用运行
app: (process.env.Mode || '').trim() === 'singleSpa' ? './src/spa.main.js' : './src/main.js', *//* 主应用的入口文件
},
}
})
父子通信
只有有爸爸和儿子 就少不了 通信
我们这里采用的是windows 自定义事件监听
消息监听订阅:
mounted() {
*//* 根据事件名*customEventSun*监听事件
window.addEventListener('customEventSun', function (event) {
const message = event.detail; *//* 获取父级应用传递的数据
console.log('我是父应用 我在首页的mounted生命周期监听到了customEventSun事件触发:', message);
});
},
消息发送
created() {
//* 注册*customEventSun* 自定义事件
this.event = new CustomEvent('customEventSun', {detail: '爸爸 你好'});
},
methods: {
sendMsg() {
//* 子集应用
console.log('我是子应用====我在1秒后向子应用发送了,自定义事件:customEventSun ,子应用你需要需要监听这个事件')
setTimeout(() => {
window.dispatchEvent(this.event);
}, 1000)
},
}
JS 自定义事件使用场景还很多详见JS自定义事件多样玩法
如果你看到这里我给你阿勇给你点赞👍👍👍
---还是那句话 分享文章,关注阿勇学前端每天分享前端有趣的知识