前言
最近开发的项目中有使用到微前端,微前端在我这分为两大类,iframe、single-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的缺点。