微前端概念
微前端借鉴了微服务的思想,将前端应用分解成一些更小、更简单的能够独立开发、测试、部署的子应用,而在用户看来是一个整体。
为什么不使用iframe
- 刷新页面后iframe的状态不能保存
- 微应用不能使用浏览器前进、后退功能
- 通信不方便
应用通信
- 基于URL来进行数据传递,但是传递消息能力弱
- 基于 CustomEvent 实现通信
- 基于 props 通信
single-spa
初始化应用
创建微应用
vue create child-vue
? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, Router
? Use history mode for router? (Requires proper server setup for index fallback
in production) Yes
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated confi
g files
? Save this as a preset for future projects? No
创建主应用
主应用可以是简单HTML文件,或者由Vue、React等框架构建
vue create parent-vue
? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, Router
? Use history mode for router? (Requires proper server setup for index fallback
in production) Yes
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated confi
g files
? Save this as a preset for future projects? No
主应用接入微应用,微应用必须暴露三个接口,分别是bootstrap
、mount
、unmount
,对于vue创建的微应用,可以使用single-spa-vue
来辅助生成三个接口函数
配置微应用
child-vue
npm i single-spa-vue
微应用提供钩子函数
child-vue/src/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 = {
el: "#vue", // 挂载到主应用的id为vue的标签中
router,
render: (h) => h(App),
};
const vueLifeCycle = singleSpaVue({
Vue,
appOptions,
});
export const bootstrap = vueLifeCycle.bootstrap;
export const mount = vueLifeCycle.mount;
export const unmount = vueLifeCycle.unmount;
微应用打包成lib
child-vue/vue.config.js
module.exports = {
configureWebpack: {
output: {
library: "singleVue",
// 打包成 umd 模块,export 导出的属性可以通过 window.singleVue.bootstrap/mount/unmount访问到
libraryTarget: "umd",
},
devServer: {
port: 10000,
},
},
};
微应用打包后的app.js
路由前缀问题
微应用的所有路由都需要添加一个前缀,比如/vue
child-vue/src/router/index.js
const router = new VueRouter({
mode: "history",
// 给路由添加前缀
base: "/vue",
routes,
});
export default router;
如果没有添加前缀,在localhost:8080/vue
访问结果结果如下,因为微应用匹配不到/vue
开头的路由
添加前缀后,再次访问相同url
路由跳转问题
点击微应用的链接时,发现请求的url是主应用的url,解决这个问题的方法是在微应用中使用绝对路径
child-vue/src/main.js
// 如果有主应用引用,发送请求时都用绝对路径
if (window.singleSpaNavigate) {
// 末尾的 / 不能遗漏
__webpack_public_path__ = "http://localhost:10000/";
console.log(__webpack_public_path__);
}
export const bootstrap = vueLifeCycle.bootstrap;
export const mount = vueLifeCycle.mount;
export const unmount = vueLifeCycle.unmount;
允许单独访问微应用
child-vue/src/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 = {
el: "#vue", // 挂载到主应用的id为vue的标签中
router,
render: (h) => h(App),
};
const vueLifeCycle = singleSpaVue({
Vue,
appOptions,
});
// 如果有主应用引用,发送请求时都用绝对路径
if (window.singleSpaNavigate) {
// 末尾的 / 不能遗漏
__webpack_public_path__ = "http://localhost:10000/";
console.log(__webpack_public_path__);
} else {
// 没有主应用引用时允许单独访问子应用
delete appOptions.el;
new Vue(appOptions).$mount("#app");
}
export const bootstrap = vueLifeCycle.bootstrap;
export const mount = vueLifeCycle.mount;
export const unmount = vueLifeCycle.unmount;
配置主应用
parent-vue
npm i single-spa
承载微应用的容器
parent-vue/src/App.vue
加载vue创建的子应用
加载微应用的js文件
parent-vue/src/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;
/**
* appName: string 子应用别名
* applicationOrLoadingFn 路由匹配成功进行子应用加载
* activityFn 路由匹配规则
* customProps?: {} | CustomPropsFn<{}> 自定义参数,用于主应用和微应用通信
*/
registerApplication(
"myVueApp",
async () => {
// 必须先加载公共模块 chunk-vendors.js ,然后加载私有模块 app.js
await loadScript("http://localhost:10000/js/chunk-vendors.js");
await loadScript("http://localhost:10000/js/app.js");
// 加载完成后,window 上会挂载一个 singleVue 属性(该名称在微应用的vue.config.js中定义),该属性包含 bootstrap、mount、unmount等方法,将其作为函数返回结果
return window.singleVue;
},
(location) => location.pathname.startsWith("/vue") // 用户切换到 /vue 时加载 myVueApp
);
start();
new Vue({
router,
render: (h) => h(App),
}).$mount("#app");
SingleSpa的缺陷
- 不够灵活,不能动态加载js文件
- 样式不隔离
- 没有js沙箱机制
CSS隔离方案
微应用之间样式隔离
动态样式表,当应用切换时移除老应用的样式,添加新应用的样式
主应用和微应用之间样式隔离
- BEM(Block Element Modifier)约定项目前缀
- CSS-Modules 打包生成不冲突的选择器名
- Shadow DOM 真正意义的隔离
- css-in-js
Shadow DOM
影子DOM,可以实现真正意义的样式隔离
hello
沙箱机制
假如A应用加载后在window上添加一个变量window.a
,在没有沙箱机制的情况下,B应用加载后也可以访问到window.a
,这就是所谓的全局污染问题。沙箱可以确保应用结束后还原window环境
实现JS沙箱的方式
- 快照沙箱
缺点是不适用于多个微应用的情况
- 代理沙箱
基于es6的proxy来实现,不同应用使用不同代理
qiankun
项目初始化
创建项目
vue create qiankun-base
vue create qiankun-vue
npx create-react-app qiankun-react
安装依赖
qiankun-base
vue add element
npm install qiankun
qiankun-react
# 修改 react 配置
yarn add react-app-rewired
配置主应用
注册微应用
qiankun-base/src/main.js
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import "./plugins/element.js";
import { registerMicroApps, start } from "qiankun";
const apps = [
{
name: "vueApp", // 微应用别名
entry: "//localhost:10000", // 默认会加载这个html,解析里面的js文件并且执行,注意微应用必须支持跨域
container: "#vue", // 微应用挂载位置
activeRule: "/vue", // 激活规则,当访问到 /vue 时挂载这个应用
},
{
name: "reactApp",
entry: "//localhost:20000",
container: "#react",
activeRule: "/react",
},
];
// 注册微应用
registerMicroApps(apps);
// 启动
start({
prefetch: false, // 取消预加载
});
Vue.config.productionTip = false;
new Vue({
router,
store,
render: (h) => h(App),
}).$mount("#base");
qiankun-base/src/App.vue
Base应用
Vue应用
React应用
微应用的生命周期钩子
qiankun-base/src/main.js
// 注册微应用
registerMicroApps(apps, {
beforeMount: () => {
console.log("加载中");
},
});
向微应用传参
通过props向微应用传参
qiankun-base/src/main.js
const apps = [
{
name: "vueApp", // 微应用别名
entry: "//localhost:10000", // 默认会加载这个html,解析里面的js文件并且执行,注意微应用必须支持跨域
container: "#vue", // 微应用挂载位置
activeRule: "/vue", // 激活规则,当访问到 /vue 时挂载这个应用
props: {
a: "qiankun", // 向微应用传参,不能与已有参数重名
},
},
{
name: "reactApp",
entry: "//localhost:20000",
container: "#react",
activeRule: "/react",
},
];
微应用接收参数
微应用在mount
方法中接收参数
qiankun-vue/src/main.js
export async function bootstrap(props) {}
export async function mount(props) {
console.log(props);
// props 是向该微应用传递的参数,可以通过store保存参数
render(props);
}
export async function unmount(props) {
// 卸载应用
instance.$destroy();
}
配置微应用
接入Vue微应用
添加路由前缀
qiankun-vue/src/router/index.js
const router = new VueRouter({
mode: "history",
base: "/vue",
routes,
});
导出钩子函数
qiankun-vue/src/main.js
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
Vue.config.productionTip = false;
let instance = null;
function render(props) {
instance = new Vue({
router,
render: (h) => h(App),
}).$mount("#app"); // 该子应用挂载到自己的html中,主应用得到这个html后将其挂载到container指定标签中,注意,注意这个id必须是唯一的,主应用不能含有同名id
}
if (!window.__POWERED_BY_QIANKUN__) {
// 作为独立应用运行
render();
} else {
// 作为子应用运行,动态修改public_path,解决子应用发送请求的问题
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
// 必须导出这三个函数
export async function bootstrap(props) {}
export async function mount(props) {
// props 是向该子应用传递的参数,可以通过store保存参数
render(props);
}
export async function unmount(props) {
// 卸载应用
instance.$destroy();
}
微应用打包成lib
qiankun-vue/vue.config.js
module.exports = {
// 支持跨域
devServer: {
port: 10000,
headers: {
"Access-Control-Allow-Origin": "*",
},
},
configureWebpack: {
output: {
library: "vueApp",
libraryTarget: "umd",
},
},
};
id冲突问题
当主应用中某个元素的id与微应用(html中)挂载元素的id相同时,微应用会错误的挂载到主应用这个同名id所在元素,而不是主应用在main.js中指定的元素,所以要避免重名id的情况。
接入react微应用
qiankun-react/package.json
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-app-rewired eject"
},
配置跨域和打包路径
qiankun-react/config-overrides.js
module.exports = {
webpack: (config) => {
config.output.library = "reactApp";
config.output.libraryTarget = "umd";
config.output.publicPath = "http://localhost:20000/";
return config;
},
devServer: (configFunction) => {
return function (proxy, allowedHost) {
const config = configFunction(proxy, allowedHost);
config.headers = {
"Access-Control-Allow-Origin": "*",
};
return config;
};
},
};
配置运行端口
qiankun-react/.env
PORT=20000
WDS_SOCKET_PORT=20000
导出钩子函数
qiankun-react/src/index.js
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
function render() {
ReactDOM.render(
,
document.getElementById("root")
);
}
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
export async function bootstrap(props) {}
export async function mount(props) {
render(props);
}
export async function unmount(props) {
ReactDOM.unmountComponentAtNode(document.getElementById("root"));
}