微前端
###是什么
“微前端”一词 2016 年首次出现,它是将微服务的概念扩展到前端领域。 将前端整体分解为更小、更简单的块的模式出现,这些块可以独立开发、测试和部署,同时在客户面前仍然被视为单一的内聚产品。我们称此技术为微前端。其定义为:一种架构风格,其中独立交付的前端应用程序组成一个更大的整体。
目前前端主流趋势是 SPA 应用(单页面应用程序),但随着需求的变更,功能的增加,项目会越来越大,也难以维护,升级到最主流的技术栈的成本会越来越高,最终成为一个“巨石应用”。 微前端的理念是将网站或应用程序视为独立团队的功能组合。
微前端架构旨在解决单体应用在一个相对长的时间跨度下,由于参与的人员、团队的增多、变迁,从一个普通应用演变成一个巨石应用(Frontend Monolith)后,随之而来的应用不可维护的问题。面对开发者:项目拆分,面对用户:项目整合
###微前端架构具备以下几个核心价值
-
技术栈无关
主框架不限制接入应用的技术栈,微应用具备完全自主权
-
独立开发、独立部署
微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新
-
增量升级
在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略
-
独立运行时
每个微应用之间状态隔离,运行时状态不共享
###项目膨胀出现的问题
- 编译时间长
- 开发体验差(热加载慢)
- 打包成本高(打包时间长,牵一发动全身)
- 团队协作问题
- 代码冲突
- 版本控制
新的问题
- 域名问题
- 模块拆分的过多,需要独立部署,运维成本,域名管理
优点和缺点及性能
- 优点
- 入口统一
- 增量升级
- 简单、分离的代码库
- 独立部署
- 缺点
- 架构复杂
- 依赖项重复
- 性能
- 如:主应用是vue,子应用也是vue,vue|vue-router|vuex会重复加载,解决方法生产环境对重复依赖项不进行webpack打包,通过cdn和是否在qiankun环境中动态插入script标签, 整合公共资源 externals 只加载一遍
###架构图
常见的应用场景
- 模块和模块之间交互不多,如:几个管理系统的合并权限菜单交给主应用渲染,统一系统入口、客服面板、工单面板等一些插件面板不需要和其他应用做大量的交互
####JS客服面板
####JS工单面板
####结构简单清楚
实现方案
qiankun阿里开源的框架
qiankun
怎样用
主应用
import {
start,
initGlobalState,
registerMicroApps,
runAfterFirstMounted,
// setDefaultMountApp,
addGlobalUncaughtErrorHandler,
} from "qiankun";
import render from "./render";
import { MICRO_APP_LIST } from "@/config";
// Step1 初始化应用
render({ loading: false });
const loader = (loading) => render({ loading });
// 微应用配置
const apps = MICRO_APP_LIST.map((app) => {
const { name, entry, activeRule } = app;
const container = "#subapp-viewport";
return {
name,
entry,
activeRule,
loader,
container,
props: { activeRule },
};
});
// Step2 注册子应用
registerMicroApps(apps, {
beforeLoad: [
(app) => {
console.log("[LifeCycle] before load %c%s", "color: green;", app.name);
},
],
beforeMount: [
(app) => {
console.log("[LifeCycle] before mount %c%s", "color: green;", app.name);
},
],
afterUnmount: [
(app) => {
console.log("[LifeCycle] after unmount %c%s", "color: green;", app.name);
},
],
});
const { onGlobalStateChange, setGlobalState } = initGlobalState();
onGlobalStateChange((value, prev) => {
console.log("[onGlobalStateChange - master]:", value, prev);
});
// 设置全局状态数据
setGlobalState();
// Step3 设置默认进入的子应用
// setDefaultMountApp('/cs');
// Step4 启动应用
start({
prefetch: "all",
// 开启严格的样式隔离模式,但是也会造成一些问题。如一些ui框架Modal插入到的是body节点
// sandbox: { strictStyleIsolation: true }
});
runAfterFirstMounted(() => {
console.log("[MainApp] first app mounted");
});
####子应用
import Vue from "vue";
import App from "./App.vue";
import router, { routes } from "./router";
import store from "./store";
import "./style.scss";
import "@/plugins";
// 解决微应用加载资源404问题
// eslint-disable-next-line
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
Vue.config.productionTip = false;
let instance = null;
function render(props = {}) {
const { container } = props;
instance = new Vue({
router,
store,
render: (h) => h(App),
}).$mount(container ? container.querySelector("#app") : "#app");
}
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
export async function bootstrap() {
//
}
export async function mount(props) {
render(props);
}
export async function unmount() {
instance.$destroy();
instance.$el.innerHTML = "";
instance = null;
}
// 导出微应用的全部路由,主应用使用
export const routes = allRoutes;
####vue.config.js
const {
name: MICRO_APP_NAME,
port: MICRO_APP_PORT
} = require("./package.json");
// TODO: 开发环境,配置 chainWebpack 、 configureWebpack 导致vue浏览器插件无法使用
module.exports = {
publicPath: "./",
productionSourceMap: false,
runtimeCompiler: true,
parallel: false,
css: {
sourceMap: false
},
configureWebpack: config => {
config.output = {
...config.output,
// 把子应用打包成 umd 库格式
library: `${MICRO_APP_NAME}-[name]`,
libraryTarget: "umd",
jsonpFunction: `webpackJsonp_${MICRO_APP_NAME}`
};
},
devServer: {
host: "0.0.0.0",
open: true,
port: MICRO_APP_PORT, // 端口需要统一
clientLogLevel: "warning",
compress: true,
inline: true,
hotOnly: true,
quiet: true,
https: false,
progress: true,
disableHostCheck: true,
headers: {// 开启资源跨域
"Access-Control-Allow-Origin": "*"
},
overlay: false
},
lintOnSave: true
};
通信方案
微前端场景下,我们认为最合理的通信方案是通过 URL 及 CustomEvent 来处理。但在一些简单场景下,基于 props 的方案会更直接便捷,因此我们为 qiankun 用户提供这样一组 API 来完成应用间的通信:
全局变量隔离
class SnapshotSandbox {
constructor() {
this.proxy = window; // window属性
this.modifyPropsMap = {}; // 记录在window上的修改
this.active(); // 激活
}
active() {
this.windowSnapshot = {}; // 拍照
for (const prop in window) {
if (window.hasOwnProperty(prop)) {
this.windowSnapshot[prop] = window[prop];
}
}
Object.keys(this.modifyPropsMap).forEach((p) => {
window[p] = this.modifyPropsMap[p];
});
}
inactive() {
// 失活
for (const prop in window) {
if (window.hasOwnProperty(prop)) {
if (window[prop] !== this.windowSnapshot[prop]) {
this.modifyPropsMap[prop] = window[prop];
window[prop] = this.windowSnapshot[prop];
}
}
}
}
}
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);
####项目结构
###注意
- 配置运行时 publicPath,解决微应用加载资源 404 的问题webpack_public_path
- webpack_public_path = window.INJECTED_PUBLIC_PATH_BY_QIANKUN;
- 端口占用导致微应用无法启动
- 子应用 vue.config.js 中配置的端口,应该和主应用注册子应用时配置的端口保持一致。
###vue实例问题