# 微前端
微前端是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将单页面前端应用由单一的单体应用转变为把多个小型前端应用聚合为一的应用。各个前端应用还可以独立开发、独立部署。简单说就是将前端应用分解成一些更小、更简单的能够独立开发、测试、部署的小块,而在用户看来仍然是内聚的单个产品。
主要解决以下问题:
1、随着项目迭代前端工程越来越庞大,难以维护。
2、跨团队或跨部门协作开发项目导致效率低下且受基础框架技术限制。
3、不同业务模块需要使用不一样的依赖,随着时间推移依赖冲突严重且不支持增量升级。
4、市场现有多模块系统,后端虽然做到了模块化并前后端分离,但前端工程本质上依然属于单体架构。
# 技术选型
业内已经有很多开源的微前端框架,并且都经过了大量的应用实践验证。
当前比较流行的有 古老的iframe, 现代的single-spa, 腾讯的wujie (opens new window),阿里的qiankun (opens new window),京东的micro-app (opens new window)
wujie: 基于 webcomponent + iframe 的微前端方案。
qiankun: 基于 single-spa 的微前端方案。
micro-app: 基于 webcomponent + sandbox 的微前端方案。
以上各个方案各有千秋,关于他们的优略势网上对比的已经有很多,这里就不再重复对比。
基于StartCMS的设计理念和需要实现的两个目标:
中心化:应用注册表。这个应用注册表拥有每个应用及对应的入口。在前端领域里,入口的直接表现形式可以是路由,又或者对应的应用映射。
应用标识化:我们需要一个标识符来标识不同的应用,以便于在安装、卸载的时候,能寻找到指定的应用。
我们采用自认为最优雅的micro-app来实现应用生命周期管理,实现高内聚,低耦合。
# 架构模式
从微前端应用间的关系来看,分为两种:基座模式(管理式)、自组织式。StartCMS采用基座模式
- 基座模式。
通过一个主应用来管理其它应用,设计难度小,方便实践。 - 自组织模式。
应用之间是平等的,不存在相互管理的模式,但设计难度大,不方便实施。
# 基座架构
我们主要采用的是组合式应用路由方案,该方案的核心是“主从”思想,即包括一个基座(MainApp)应用、和若干个单独的微(MicroApp)应用组成。
基座应用是一个基于micro-app和element-ui开发的SPA项目,主要负责渲染公共的页面元素,比如 header、footer、应用注册、路由映射、消息下发、菜单渲染、逻辑控制、数据传输等。
微应用是独立前端项目,这些项目不限于采用React、Vue、Angular、JQuery,每个微应用注册到基座应用中,由基座进行管理,但是如果脱离基座也是可以单独访问
# 基本流程

# 基本功能
基座通过对URL匹配特定规则处理,实现当浏览器 url 发生变化时,自动加载相应的微应用。底层依赖micro-app技术栈,实现对微应用的:
- 应用生命周期的管理和通讯
- 应用路由和权限的分发
- 系统权限菜单的构建
提示
以上功能都是基于路由自动化完成的,一般不涉及以上功能的迭代都不需要对基座进行二次开发
当然也可以在基座上直接进行单体模式的二次开发,但一般不建议这样做,不利于项目的管理和升级迭代
# 基座目录
www WEB部署目录
├─app 应用目录
├─base 基座工程(默认已编译到web目录,一般情况不需要二次开发,部署可删除)
│ ├─public 公共目录
│ ├─src 工程代码
│ | ├─api api目录
│ | ├─assets 静态资源
│ | ├─components 公共组件
│ | ├─directive 自定义指令
│ | ├─filter 全局过滤器
│ | ├─icons 全局图标库
│ | ├─layout 布局视图
│ | ├─store 状态管理
│ | ├─styles 公共样式
│ | ├─utils 工具库
│ | ├─views 基座视图
│ | ├─App.vue 渲染模板
│ | ├─auth.js 权限控制
│ | ├─main.js 入口
│ | ├─settting.js 设置
│ ├─.env.development
│ ├─.env.production
│ ├─.eslintrc.js
│ ├─.gitignore
│ ├─label.config.js
│ ├─package.json
│ ├─postcss.config
│ ├─README.md
│ ├─vue.config.js
# 基座布局

# 应用管理
注意
基座应用是通过对URL匹配特定规则处理,实现当浏览器 url 发生变化时自动加载相应的微应用。所以子应用注册的路由地址都需要以应用名开头。
如test应用,其路由节点应该都是/test/path1,/test/path2,/test/path3...
# 应用注册
应用注册依赖于应用描述文件app.json,应用安装后会把相关参数保存到core_app应用数据表和core_auth权限菜单表,
主要相关参数如下:
{
"icon": "", // 应用图标
"name": "", // 应用标识(英文,与目录同名)
"entry": "", // 应用入口(输出html的url地址)
"dev_entry": "", // 调试入口(如果需要使用实时监听开发模式可以填写npm run dev启动的地址)
"title": "", // 应用名称(中文,2-4个中文字符)
"ssr": false, // ssr模式(后端渲染模式)
"debug": false, // 调试模式(可以在应用管理中动态设置)
"sandbox": true, // js沙箱隔离(禁用沙箱可能会导致一些不可预料的问题,通常情况不建议这样做。)
"scopecss": true, // css样式隔离(禁用样式隔离可以提升页面渲染速度,在此之前,请确保各应用之间样式不会相互污染。)
"auth": [ // 自定义权限节点,同时也是前端路由地址(开发PHP时可以自动生成,具体查看指南说明)
{
"app": "", // 应用标识(可选,默认当前应用)
"icon": "", // 菜单图标(可选)
"name": "", // 权限标识(英文,小写字母开头+下划线)
"title": "账户管理", // 权限名称(中文,菜单/功能名称)
"parent": "", // 上级节点
"node": "APPNAME/...", // 权限节点(后端路由)
"path": "/APPNAME/...", // 权限路径(前端路由)
"view": "", // 视图模板
"params": "", // 路由参数
"redirect": "", // 跳转地址
"menu": 1, // 是否为菜单
"auth": 1, // 是否权限控制
"admin": 0, // 是否仅限管理员访问
"super": 0, // 是否仅限超管员访问
"route": 0, // 是否仅作前端路由(有无权限都返回)
"status": 1, // 启用状态
"cache": 0, // 启用缓存
"sort": 7, // 排序
},
],
"config":[] // 应用设置
}
# 应用加载
micro-app通过CustomEvent定义生命周期,在组件渲染过程中会触发相应的生命周期事件。
每当访问地址变更时,基座会根据路由地址信息加载应用并在应用加载资源完成后,开始渲染之前下发子应用需要的数据
下发数据内容如下:
{
"app": {
"name": "", // 当前应用名称
"entry": "", // 当前应用入口
"debug": true, // 是否调试模式
"config": [], // 应用配置信息(配置信息会在 系统/系统配置/参数配置 中统一管理)
"routes": [], // 应用路由地址(在单页应用中,子应用加载后需主动获取这个信息来动态构建路由)
},
"authorize":[], // 当前用户的权限集合
"route": {}, // 当前用户的访问地址
"user": {}, // 当前用户的个人信息
"token": "" // 当前用户的安全令牌
}
子应用获取并监听基座下发的数据
async function appStart() {
// 是否是微前端环境
if (window.__MICRO_APP_ENVIRONMENT__) {
// 启动时主动获取基座下发的数据
const { app, user, token, route, authorize } = window.microApp.getData();
console.log("获取基座下发的数据:", { app, user, token, route, authorize });
// 记录账户状态
if(user && token && authorize){
await store.dispatch("user/setState", {
info: user,
token: token,
authorize: authorize,
});
}
// 记录应用配置
if (app && app.config){
await store.dispatch('app/setConfig', app.config)
}
// 构建权限路由
if(app && app.routes){
const accessRoutes = await store.dispatch("app/generateRoutes", app.routes);
accessRoutes.forEach(item => {
router.addRoute(item)
})
}
// 进入指定页面
if (route) {
router.push(route, ()=>{});
}
// 监听基座下发的数据
window.microApp.addDataListener((data) => {
console.log("监听基座下发的数据:", data);
if (data.route && data.route.path !== router.currentRoute.path) {
router.push(data.route.path);
}
});
} else {
// 单应用模式:主动获取后端数据
const { apps } = await store.dispatch("user/getState");
const routes = apps.reduce((pre, app) => {
if (app.routes instanceof Array) {
app.routes.forEach((item) => {
item.entry = app.entry;
});
return pre.concat(app.routes);
}
return pre;
}, []);
// 构建权限路由
const accessRoutes = await store.dispatch("app/generateRoutes", routes);
accessRoutes.forEach(item => {
router.addRoute(item)
})
}
// 添加路由守卫
router.beforeEach(async (to, from, next) => {
if (window.__MICRO_APP_ENVIRONMENT__) {
console.log('to',to)
// 通知基座路由变更
window.microApp.dispatch({
route: to,
});
}
next();
});
}
// 子应用挂载
const app = new Vue({
router,
store,
render: (h) => h(App),
}).$mount("#app");
// 子应用卸载
window.addEventListener("unmount", function () {
app.$destroy();
});
// 启动数据监听
appStart();