微前端之singleSPA实战|技术点评

1,050 阅读4分钟
关键在于父应用嵌入子应用

前置条件

  • 父子应用均为Vue

应用层

第一阶段:基础引入

关键点
  • 子应用不再直接渲染,而是达成一个lib包的形式

  • 子应用应该按照spa的协议约定进行打包,即

    • lib名为singleVue,导出方式为umd
    • 主应用引入时,不再new Vue,而是将配置交由singleSpa进行管理,从而生成boostrap mount unmount三个对外暴露的接口
  • 在父应用启动项目中,进行应用注册,如果匹配到子应用路径,则执行定义的逻辑(即渲染子应用于指定区域)

实现

子应用

不再直接new Vue生成vue实例,而是根据协议(即让singleSpaVue接管)生成暴露接口
import singleSpaVue from 'single-spa-vue';

const appOptions = {
  el: '#vue', // 挂载在父应用中id为vue的标签中
  router,
  render: h => h(App)
}

const vueLifeCycle = singleSpaVue({
  Vue,
  appOptions
})
// 暴露接口  
export const boostrap = vueLifeCycle.boostrap;
export const mount = vueLifeCycle.mount;
export const unmount = vueLifeCycle.unmount;

配置Vue,将子应用打包成一个个的lib去给父应用使用

src根路径下,新建vue.config.js

module.exports = {
    configureWebpack : {
        output: {
            library: 'singleVue', // 指定lib名称
            libraryTarget: 'umd' // 指定导出格式
        },
        devServer: {
            port: 10000 // 指定服务运行端口
        }
    }
}

父应用

父应用注册子应用
import {
  registerApplication,
  start
} from 'single-spa';
import router from './router' //引入路由

// 注册子应用
registerApplication(
  'myVueApp',// 只是一个标识而已
  async () => {
    console.log('加载子vue应用');  // 会在匹配时执行   一定要是一个返回Promise 的函数
  },
  location => location.hash.startsWith('#/vue'),  // 判断是否命中的函数 会传递location对象 本例中就是用户切换到/vue开头的路径下,我需要加载刚刚定义的子应用
)
// 启动应用
start();
实现效果

image-20210306175150955

第二阶段:渲染子应用

关键点:
  • 在路由匹配到时,进行子应用的渲染,而我们现在都是All in JS,问题也就转为了引入其对应的vendor.jsapp.js(以最简单的vue项目为例)
    • 子应用需要固定导出的静态资源路径,避免动态引入时,根域名变为了父应用的域名,导致无法找到的情况
    • 通过fetch的方式去获取子应用的静态资源,从而完成渲染
  • 优化:支持子应用独立运行,即如果不是父应用引入的情况下,子应用应该采用传统的new Vue的形式,便于独立开发
实现

在子应用中进行一层逻辑判断即可, 如果是微前端嵌入的形式,则设置静态资源全为固定的绝对路径,避免父应用引入时找不到的情况

// singleSpaNavigate属性是singleSpa默认会设置的属性,其值是一个函数
if(window.singleSpaNavigate){
  // 此处应该实现将webpack静态资源路径设置为子应用路径的逻辑
}
//  支持子应用独立运行
else {
  delete appOptions.el;
  new Vue(appOptions).$mount('#app');
}

我们知道,webpack存在配置项publicPath,可以设置webpack输出静态资源的路径,但我们现在需要的是动态设置

查看webpack文档,发现一神器__webpack_public_path__。用一句话来解释的话,这货就是output配置中的“publicPath”参数另外一种配置方式。

那么,我们就可以完善上述逻辑为

// singleSpaNavigate属性是singleSpa默认会设置的属性,其值是一个函数
if(window.singleSpaNavigate){
    // 此处应该实现将webpack静态资源路径设置为子应用路径的逻辑
  __webpack_public_path__ = 'http://localhost:10000/'
}
//  支持子应用独立运行
else {
  delete appOptions.el;
  new Vue(appOptions).$mount('#app');
}
效果图如下

注意域名,可以发现我们已经实现了在父应用中加载子应用的需求了

image-20210307113740188

第三阶段:隔离

CSS隔离
  • 子应用之间的样式隔离
  • 主应用和子应用之间的样式隔离
    • BEM约定项目前缀
    • CSS-Modules 打包时生成不冲突的选择器名
    • Shadom DOM
    • css-in-js
具体实现
shadowDom

创建一个shadowDom作为子应用的容器,然后让style生效于shadowDOM的作用域中,这样就实现了纯净的隔离

  • 假设p是子应用
  • 假设styleElm是子应用的样式
 let shadomDOM = document.getElementById('shadow').attachShadow({
            mode: 'closed'
        });

let pElm = document.createElement('p');
    pElm.innerHTML = 'hello zf';
let styleElm = document.createElement('style');
    styleElm.textContent = `
    p{color:red}
    `

    shadomDOM.appendChild(styleElm);
   // shadomDOM.appendChild(pElm);

问题:当存在第三方库的组件(比如antd的modal)是挂载在body上时,就会失效

document.body.appendChild(pElm);
沙箱

定义沙箱类,intereface如下

// 默认会执行一次active
interface {
  Object proxy; // 沙箱代理对象 沙箱环境的真实管理者(现状态)
  Object windowSnapshot; // 快照对象(上一个状态)
  Object modifyPropsMap; // 记录在proxy上的修改态
	// 1. 记录快照 2. 应用上次快照的修改态  简言之:更新快照并根据修改态更新沙箱
	active(){}
  // 1. 更新修改态 2. 还原沙箱为上次快照状态   简言之:根据快照更新修改态并还原沙箱
	inactive(){}
		
}

实例如下


        class SnapshotSandbox {
            constructor(){
                this.proxy = window; // window属性
                this.modifyPropsMap = {}; // 记录在window上的修改
                this.active();
            }
            //  更新快照并根据修改态更新沙箱
            active(){
                this.windowSnapshot = {}; 
	              // 进行拍照(浅拷贝) 便于
                for (const prop in this.proxy) {
                    if (Object.hasOwnProperty.call(this.proxy, prop)) {
                        this.windowSnapshot[prop] = this.proxy[prop];
                    }
                }
              	// 应用上次的修改 以还原状态
                Object.keys(this.modifyPropsMap).forEach(p=>{
                    this.proxy[p] = this.modifyPropsMap[p];
                })
            }
            // 根据快照更新修改态并还原沙箱
            inactive(){
                for (const prop in this.proxy) {
                    if (Object.hasOwnProperty.call(this.proxy, prop)) {
                        if(this.proxy[prop] !== this.windowSnapshot[prop]){
                            this.modifyPropsMap[prop] = this.proxy[prop];
                            this.proxy[prop] = this.windowSnapshot[prop];
                        }
                    }
                }
            }
        }

eg如下

 let sandbox = new SnapshotSandbox();
        ((
            (window)=>{
                window.a = 1;
                window.b = 2;
                console.log(window.a,window.b);
                sandbox.inactive();
                console.log(window.a,window.b);
                sandbox.active();
                console.log(window.a,window.b);
            }

        ))(sandbox.proxy);

image-20210307160425885

个人建议记忆图

  • 代表更新 - 代表不变 ↑ 代表更新 ↓ 代表还原
快照修改态沙箱
active+-↑(代表更新)
inactive-+↓(代表还原)

相关链接

微前端之qiankun实战

微前端之自实现singleSpa

本文正在参与「掘金 2021 春招闯关活动」, 点击查看 活动详情