微前端single-spa框架详解

142 阅读6分钟

image.png

今天日技术分享正题微前端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自定义事件多样玩法

如果你看到这里我给你阿勇给你点赞👍👍👍 

---还是那句话 分享文章,关注阿勇学前端每天分享前端有趣的知识

关注微信公众号 阿勇学前端