什么是微前端
微前端的概念:构建一个现代 Web 应用所需要的技术/策略/方法,具备多个团队独立开发、部署的特性
微前端的优势
- 独立测试部署,各个模块相互独立,互不影响
- 扩展性高
- 技术兼容更好,各个模块可以使用不同的技术
微前端的缺点
- 子应用之间共享资源能力较差
- 需要对旧的代码改造升级才可以使用
目前主流的微前端解决方案
- iframe (最大的问题是 刷新页面 路由会丢失 本质刷新的主应用 而不是路由应用 所以被放弃了)
-
single-spa
和qiankun
(是基于single-spa
)封装的 这是一种基座模式(通过搭建基座配置中心来管理子应用) - ESM 是 ES module 的缩写
- EMP 这是一种去中心模式(脱离基座模式 每个应用之间可以彼此分享资源)
- web Components
qiankun 和 single-spa 实战
single-spa
首先我们创建子应用,子应用我用的是 vue-cli,只需要简单的配置即可 路由和 babel
- single-child1
安装npm i single-spa-vue
修改 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;
// new Vue({
// router,
// render: (h) => h(App),
// }).$mount("#app");
const appOptions = {
el: "#vue", // 需要挂载的父应用的节点
render(h) {
return h(App);
},
router,
};
// 当父应用 调用我的时候,控制子应用路由跳转的资源引用路径
if (window.singleSpaNavigate) {
// eslint-disable-next-line no-undef
__webpack_public_path__ = "http://localhost:1000/";
}
// 支持应用独立运行、部署,不依赖于基座应用
if (!window.singleSpaNavigate) {
delete appOptions.el;
new Vue(appOptions).$mount("#app");
}
const vueLifecycles = singleSpaVue({
Vue,
appOptions,
});
// 导出生命周期
export const bootstrap = vueLifecycles.bootstrap;
export const mount = vueLifecycles.mount;
export const unmount = vueLifecycles.unmount;
配置 vue.config.js
把项目打包成 umd 格式
module.exports = {
configureWebpack: {
devServer: {
port: 1000,
},
output: {
library: "app1",
libraryTarget: "umd",
},
},
};
修改路由的 base
const router = new VueRouter({
mode: "history",
// 这个名字需要和 打包的名字 以及 主应用引用的名称一样
base: "/app1",
routes,
});
- single-parent
然后创建基座应用 ,同样我们用 vue-cli 来创建一个简单的应用
然后安装 single-spa
修改 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 app = [
{
// 子应用名称
name: "app1",
// 子应用加载函数,是一个promise
app: loadApp("http://localhost:1000", "app1"),
// 当路由满足条件时(返回true),激活(挂载)子应用
activeWhen: (location) => location.pathname.startsWith("/app1"),
// 传递给子应用的对象
customProps: {},
},
{
// 子应用名称
name: "app2",
// 子应用加载函数,是一个promise
app: loadApp("http://localhost:2000", "app2"),
// 当路由满足条件时(返回true),激活(挂载)子应用
activeWhen: (location) => location.pathname.startsWith("/app2"),
// 传递给子应用的对象
customProps: {},
},
];
// 注册子应用
for (let i = app.length - 1; i >= 0; i--) {
registerApplication(app[i]);
}
new Vue({
router,
mounted() {
// 启动
start();
},
render: (h) => h(App),
}).$mount("#app");
修改 app.vue 添加一个挂载的节点
然后运行即可
qiankun
qiankun 是基于 single-spa 的封装 ,qiankun 也是现在主流的微应用方案,
- qiankun-child
修改 main.js
import Vue from "vue";
import App from "./App.vue";
import VueRouter from "vue-router";
import routes from "./router";
import "./public-path";
Vue.config.productionTip = false;
let router = null;
let instance = null;
// 渲染函数
function render(props = {}) {
const { container } = props;
router = new VueRouter({
base: window.__POWERED_BY_QIANKUN__ ? "/app1" : "/",
mode: "history",
routes: routes.options.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;
router = null;
}
添加一个 public-path.js 文件
if (window.__POWERED_BY_QIANKUN__) {
// 这里是支持修改的 原理和single-spa是一样的
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
添加 vue.config.js
const { name } = require("./package");
module.exports = {
publicPath: "/app1",
devServer: {
headers: {
"Access-Control-Allow-Origin": "*",
},
port: 1000,
},
configureWebpack: {
output: {
library: `app1`,
libraryTarget: "umd", // 把微应用打包成 umd 库格式
jsonpFunction: `webpackJsonp_${name}`,
},
},
};
- qiankun-parent
安装 qiankun npm i qiankun -S
修改 main.js
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import { registerMicroApps, start } from "qiankun";
Vue.config.productionTip = false;
registerMicroApps([
{
name: "app1", // app name registered
entry: "http://localhost:1000/app1",
container: "#vue",
activeRule: "/app1",
},
{
name: "app2",
entry: "http://localhost:2000/app2",
container: "#vue",
activeRule: "/app2",
},
]);
new Vue({
router,
mounted() {
start();
},
render: (h) => h(App),
}).$mount("#app");
APP.vue 中要添加一个挂载的节点
想要熟悉 ,可以自己实战一下,然后参照官方文档,相信会有一定的理解,之后再学习原理
single-spa 和 qiankun
single-spa
single-spa 只是解决了应用加载和切换问题,例如 css 隔离和 js 隔离这个都没有解决,而是留给开发者自己去处理qiankun 帮我们解决了 js 隔离和 css 隔离,让开发者开箱即用,
js 隔离
qiankun 的隔离主要是基于 快照沙箱 和 代理沙箱实现的
js 沙箱:主应用有自己的一套 window,子应用拥有另一套私有的 window,子应用所有的操作都只在自己的上下文中进行,这样一个个的子应用就和主应用隔离起来,
因此主应用的加载不会和子应用相互污染,每个子应用都是独立的
快照沙箱(SnapshotSandbox)
只有当浏览器不支持 proxy 的时候 才使用快照沙箱 (在沙箱挂载和卸载的时候记录快照,在应用切换的时候恢复环境)
优点:兼容性好
缺点:无法同时运行多个沙箱-
代理沙箱
代理沙箱分为 ProxySandbox (多例)和 LegacySandbox(单例)
当有多个实例的时候,比如有 A、B 两个应用,A 应用就活在 A 应用的沙箱里面,B 应用就活在 B 应用的沙箱里面,A 和 B 无法互相干扰,这样的沙箱就是代理沙箱 主要是通过 es6 的 proxy 实现Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
简单来说就是,可以在对目标对象设置一层拦截。无论对目标对象进行什么操作,都要经过这层拦截优点:可以同时运行多个沙箱
缺点:兼容性较差
css 隔离
- 动态样式表 (a 应用激活 加载 a 应用的样式表 a 应用激活 加载 a 应用的样式表)
- 工程化手段 - BEM、CSS Modules、CSS in JS(通过一系列约束和编译时生成不同类名、JS 中处理 CSS 生成不同类名来解决隔离问题)
- Shadow DOM
为子应用的根节点创建一个 shadow root
,整个应用所有的 dom 将形成一颗 shadow tree,shadowDom 的特点是,它内部所有节点的样式对树外面的节点无效,因此自然就实现了样式隔离。
优点:完全隔离
缺点:有一些弹窗组件时挂载到 body 下面的,所以弹窗样式可以能会有问题,需要单独处理
应用通信
qiankun 的通知本质就是一个发布订阅模式
- 主应用
新建 src->actions.js
import { initGlobalState } from "qiankun";
import store from "./store";
const initialState = {
//这里写初始化数据
};
// 初始化 state
const actions = initGlobalState(initialState);
actions.onGlobalStateChange((state, prev) => {
//监听公共状态的变化
console.log("主应用: 变更前");
console.log(prev);
console.log("主应用: 变更后");
console.log(state);
store.commit("setProject", state);
});
export default actions;
在 vue 文件中使用
method:{
add(){
actions.setGlobalState('0');//通过setGlobalState改变全局状态
}
}
微应用
新建 src->actions.js
function emptyAction() {
//设置一个actions实例
// 提示当前使用的是空 Action
console.warn("Current execute action is empty!");
}
class Actions {
// 默认值为空 Action
actions = {
onGlobalStateChange: emptyAction,
setGlobalState: emptyAction,
};
//设置 actions
setActions(actions) {
this.actions = actions;
}
/**
* 映射
*/
onGlobalStateChange(...args) {
return this.actions.onGlobalStateChange(...args);
}
//映射
setGlobalState(...args) {
return this.actions.setGlobalState(...args);
}
}
const actions = new Actions();
export default actions;
在 main.js 注入 action
export async function mount(props) {
actions.setActions(props); //注入actions实例
render(props);
}
在.vue 文件中使用
import actions from '../actions'//导入实例
mounted() {
actions.onGlobalStateChange((state) => { //监听全局状态
this.a = state
}, true);
},
methods:{
butClick(){
actions.setGlobalState({ id: '11'})//改变全局状态
}
}
参考文章
30 分钟掌握微前端
微前端框架 qiankun 之原理与实战
qiankun 官网