single-spa、乾坤、friday接入实战

前言

最近开发的项目中有使用到微前端,微前端在我这分为两大类,iframesingle-spa系列,在这篇文章我会记录下微应用的 single-spa、乾坤的使用和一些理解,我们公司内部的friday没有开源,在这里就不记录了。

single-spa

single-spa使用

新建father、child两个vue项目

father(父应用)

1. 安装single-spa基座
npm install single-spa --save
2. 开始配置

main.js配置

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import { registerApplication, start } from 'single-spa'

Vue.config.productionTip = false

// 远程加载子应用
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)
  })
}


// 记载函数,返回一个 promise
function loadApp(url, globalVar) {
  // 支持远程加载子应用
  return async () => {
    // 
    await createScript(url + '/js/chunk-vendors.js')
    await createScript(url + '/js/app.js')
    // 这里的return很重要,需要从这个全局对象中拿到子应用暴露出来的生命周期函数
    return window[globalVar]
  }
}

const apps = [
  {
    // 子应用名称
    name: 'child',
    // 子应用加载函数,是一个promise
    app: loadApp('http://localhost:3000', 'child'),
    // 当路由满足条件时(返回true),激活(挂载)子应用
    activeWhen: location => location.pathname.startsWith('/child'),
    // 传递给子应用的对象
    customProps: {}
  },
  {
    name: 'son',
    app: loadApp('http://localhost:3001', 'son'),
    activeWhen: location => location.pathname.startsWith('/son'),
    customProps: {}
  }
]

// 注册子应用
for (let i = apps.length - 1; i >= 0; i--) {
  registerApplication(apps[i])
}
new Vue({
  router, 
  mounted() {
    // 启动,开启微应用生命周期
    start()
  },
  render: h => h(App)
}).$mount('#app')

路由配置

import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

const routes = []

const router = new VueRouter({
  mode: 'history',
  routes
})

export default router

App.vue配置

<template>
  <div id="app">
    <div id="nav">
      <router-link to="/child">微应用1</router-link> |
      <router-link to="/son">微应用2</router-link>
    </div>
    <div id = "microApp">
      <router-view/>
    </div>
 </div>
</template>

child\son(子应用)

两个子应用配置相同,举child例子
vue.config.js配置

const package = require('./package.json')

module.exports = {
  lintOnSave: false,
  devServer: {
    port: 3000
  },
  // 告诉子应用在这个地址加载静态资源,否则会去基座应用的域名下加载
  publicPath: '//localhost:3000',
  configureWebpack: {
    // 导出umd格式的包,在全局对象上挂载属性package.name,基座应用需要通过这个全局对象获取一些信息,比如子应用导出的生命周期函数
    output: {
      // library的值在所有子应用中需要唯一
      library: package.name,
      libraryTarget: 'umd'
    }
  }
}

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

const appOptions = {
  router,
  render: h => h(App)
}
// 支持应用独立运行、部署,不依赖于基座应用
if (!process.env.isMicro) {
  delete appOptions.el
  new Vue(appOptions).$mount('#microApp')
}
// 基于基座应用,导出生命周期函数
const vueLifecycle = singleSpaVue({
  Vue,
  appOptions
})
// 启动生命周期
export function bootstrap() {
  console.log('child bootstrap')
  return vueLifecycle.bootstrap(() => { })
}
// 挂载生命周期
export function mount() {
  console.log('child mount')
  return vueLifecycle.mount(() => { })
}
// 卸载生命周期
export function unmount() {
  console.log('child unmount')
  return vueLifecycle.unmount(() => { })
}

App.vue

<template>
  <div id="app">
    <h1 id="nav">
      微应用1
    </h1>
  </div>
</template>

路由配置

import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)

const routes = []
const router = new VueRouter({
  mode: 'history',
  routes
})
export default router

single-spa原理分析

子应用三个状态机

bootstrap: async () => {
	// 启动
},
mount: async () => {
	// 挂载
},
unmount: async () => {
	// 卸载
}

在这里插入图片描述

父应用首先注册好要访问的微应用信息(name、ip和端口、激活时机(router)、自定义参数)

子应用通过webpack,热更新后<上线后就是打包了>导出umd格式(这个为模块机制)的js包,并且放在webpack开启的3000端口。

single-spa那边应该有路由监听,一旦匹配的相对应的路由===>父应用向3000端口的API发起请求,拿到这几个js包(fetch请求获取),拿到子应用一些生命周期方法(bootstrap、mount、unmount
)。===>父应用通过调用子应用的方法开启启动、挂载微应用。===>当子应用热更新后(上线后无热更新),刷新父应用可重新加载子应用umd js包。

single-spa的缺点

然后,single-spa思想很好,缺点也很多。

1. 打包成几个js文件,颗粒度变粗了,按需加载没有了,css和js打包成一个,css单独加载优势没有了。
2. css隔离没有了。
3. js隔离没有了。
4. 父子应用间通信,只有父应用注册子应用时,给子应用注入一些状态,剩下就不管了,没有其他通信方法。

解决一些single-spa缺点

1、解决样式隔离

BEM约定项目前缀
CSS-Modules 打包时生成不冲突的选择器名
shadow dom
css-in-js

2、 解决js隔离(使用js沙箱类)

// 默认会执行一次active
interface {
  Object proxy; // 沙箱代理对象 沙箱环境的真实管理者(现状态)
  Object windowSnapshot; // 快照对象(上一个状态)
  Object modifyPropsMap; // 记录在proxy上的修改态
	// 1. 记录快照 2. 应用上次快照的修改态  简言之:更新快照并根据修改态更新沙箱
	active(){}
  // 1. 更新修改态 2. 还原沙箱为上次快照状态   简言之:根据快照更新修改态并还原沙箱
	inactive(){}
		
}
// 修改window属性的公共方法
const updateWindowProp = (prop, value, isDel) => {
    if (value === undefined || isDel) {
        delete window[prop];
    } else {
        window[prop] = value;
    }
}
 
class ProxySandbox {
 
    active() {
        // 根据记录还原沙箱
        this.currentUpdatedPropsValueMap.forEach((v, p) => updateWindowProp(p, v));
    }
    inactive() {
        // 1 将沙箱期间修改的属性还原为原先的属性
        this.modifiedPropsMap.forEach((v, p) => updateWindowProp(p, v));
        // 2 将沙箱期间新增的全局变量消除
        this.addedPropsMap.forEach((_, p) => updateWindowProp(p, undefined, true));
    }
 
    constructor(name) {
        this.name = name;
        this.proxy = null;
        // 存放新增的全局变量
        this.addedPropsMap  = new Map(); 
        // 存放沙箱期间更新的全局变量
        this.modifiedPropsMap = new Map();
        // 存在新增和修改的全局变量,在沙箱激活的时候使用
        this.currentUpdatedPropsValueMap = new Map();
 
        const { addedPropsMap, currentUpdatedPropsValueMap, modifiedPropsMap } = this;
        const fakeWindow = Object.create(null);
        const proxy = new Proxy(fakeWindow, {
            set(target, prop, value) {
                if (!window.hasOwnProperty(prop)) {
                    // 如果window上没有的属性,记录到新增属性里
                    // debugger;
                    addedPropsMap.set(prop, value);
                } else if (!modifiedPropsMap.has(prop)) {
                    // 如果当前window对象有该属性,且未更新过,则记录该属性在window上的初始值
                    const originalValue = window[prop];
                    modifiedPropsMap.set(prop, originalValue);
                }
                // 记录修改属性以及修改后的值
                currentUpdatedPropsValueMap.set(prop, value);
                // 设置值到全局window上
                updateWindowProp(prop, value);
                return true;
            },
            get(target, prop) {
                return window[prop];
            },
        });
        this.proxy = proxy;
    }
}
 
 
const newSandBox = new ProxySandbox('代理沙箱');
const proxyWindow = newSandBox.proxy;
proxyWindow.a = '1'
console.log('开启沙箱:', proxyWindow.a, window.a);
newSandBox.inactive(); //失活沙箱
console.log('失活沙箱:', proxyWindow.a, window.a);
newSandBox.active(); //失活沙箱
console.log('重新激活沙箱:', proxyWindow.a, window.a);

qiankun(乾坤)

qiankun使用

新建father、child两个vue项目

father(父应用)

1. 安装single-spa基座
npm install single-spa --save
2. 开始配置

main.js配置

import Vue from 'vue';
import App from './App.vue';
import { registerMicroApps, start } from 'qiankun';
import microApps from './micro-app';

Vue.config.productionTip = false;
window.projectName = 'main'; // 为了测试沙盒环境
new Vue({
  render: h => h(App),
}).$mount('#app');
// 注册子应用
registerMicroApps(microApps, {
  beforeLoad: app => {
    console.log('before load app.name====>>>>>', app.name)
  },
  beforeMount: [
    app => {
      console.log('[LifeCycle] before mount %c%s', 'color: green;', app.name);
    },
  ],
  afterMount: [
    app => {
      console.log('[LifeCycle] after mount %c%s', 'color: green;', app.name);
    }
  ],
  afterUnmount: [
    app => {
      console.log('[LifeCycle] after unmount %c%s', 'color: green;', app.name);
    },
  ],
});
// 启动qiankun
start();

src下新建 micro-app.js

const microApps = [
    {
        name: 'child',
        entry: '//localhost:3000',
        activeRule: '/child',
        container: '#subapp-viewport', // 子应用挂载的div
        props: {
            routerBase: '/child' // 下发路由给子应用,子应用根据该值去定义qiankun环境下的路由
        }
    }
]

export default microApps

child(子应用)

main.js

import './public-path' // 注意需要引入public-path
import Vue from 'vue'
import App from './App.vue'
import routes from './router'
import VueRouter from 'vue-router'

Vue.use(VueRouter)
Vue.config.productionTip = false
let instance = null
window.projectName = 'child';
function render(props = {}) {
  // 这个是我们在父类注册的时候定义的那些参数。
  const { container, routerBase } = props
  const router = new VueRouter({
    base: window.__POWERED_BY_QIANKUN__ ? routerBase : process.env.BASE_URL,
    mode: 'history',
    routes
  })
  instance = new Vue({
    router,
    render: (h) => h(App)
  }).$mount(container ? container.querySelector('#app') : '#app')
}

if (!window.__POWERED_BY_QIANKUN__) {
  render()
}
export async function bootstrap() {
  console.log('[vue] vue app bootstraped')
}
export async function mount(props) {
  console.log('[vue] props from main framework', props)
  render(props)
}
export async function unmount() {
  instance.$destroy()
  instance.$el.innerHTML = ''
  instance = null
}

src下新建 public-path.js

(function () {
    // 用来判断是否运行在乾坤的框架下
    if (window.__POWERED_BY_QIANKUN__) {
      // eslint-disable-next-line no-undef
      __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
    }
})();

根目录下新建 .env

VUE_APP_PORT=3000

vue.config.js 设置允许跨域

// vue.config.js
const { name } = require('./package.json')
module.exports = {
  configureWebpack: {
    output: {
      library: `${name}-[name]`,
      libraryTarget: 'umd',
      jsonpFunction: `webpackJsonp_${name}`,
    }
  },
  devServer: {
    port: 3000, // 与父应用的配置一致
    headers: {
      'Access-Control-Allow-Origin': '*' // 主应用获取子应用时跨域响应头
    }
  }
}

router.js设置直接导出路由类,而非实例对象

import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  }
]



export default routes

效果

在这里插入图片描述
这里使用火狐访问的,google访问报了不允许设置Access-Control-Allow-Origin为 * 的错误。

qiankun和friday都基于single-spa封装,弥补了上述提到的大部分single-spa的缺点。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
作为一个AI语言模型,我无法像人类一样编写代码。但是我可以为您提供single-spa的应用的基本理解和示例。 Single-spa是一个前端微服务框架,它允许您在同一个页面上使用多个JavaScript框架,例如React、Angular和Vue等。它提供了一个统一的入口点,负责管理不同的应用程序,从而使多个应用程序之间的通信更轻松。 下面是一个简单的single-spa应用示例,它使用React和Vue框架: 1. 首先,您需要使用npm安装single-spa: npm install single-spa --save 2. 然后,创建一个React应用程序: import React from 'react'; import ReactDOM from 'react-dom'; class ReactApp extends React.Component { render() { return ( <div> <h1>React App</h1> <p>Hello, World!</p> </div> ); } } ReactDOM.render(<ReactApp />, document.getElementById('react-app')); 3. 接下来,创建一个Vue应用程序: import Vue from 'vue'; import App from './App.vue'; new Vue({ el: '#vue-app', render: h => h(App) }); 4. 最后,在单一页面应用程序中使用这两个应用程序: import { registerApplication, start } from 'single-spa'; registerApplication( 'react-app', () => import('./react-app'), () => location.pathname === '/react' ); registerApplication( 'vue-app', () => import('./vue-app'), () => location.pathname === '/vue' ); start(); 这将在页面上注册两个应用程序:一个React应用程序和一个Vue应用程序。它使用location.pathname来检查当前路径是否与每个应用程序的路径匹配。如果是,则加载相应的应用程序。 如果您想了解更多关于single-spa的信息,请访问官方网站:https://single-spa.js.org/

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值