前言
花了一周多时间,终于把小掘金肝出来了。
起初只是想用qiankun
+vue
的架构以掘金
为例子搭个简单的demo
,却没想到疯狂掉入切图细节(强迫症确诊无误)。
事实上也因此更深入地体会到了微前端架构的一些利弊以及开发体验,所以也还不错吧。
希望这篇分享能够给想要尝试微前端架构却还在原地观望的你带来一些实践上的感受和可行性认知。
至少你能够在这个实战项目中了解到使用qiankun
+Vue
完成整个开发到部署的闭环。
「
生产环境
」:「掘金(MicroApps): www.channing-bbs.club」「
GitHub
」:「传送门」
「Why」
从两个方面讲讲“为什么”:
- 为什么要用微前端架构?
- 为什么用 qiankun+Vue 做这个实战项目?
为什么要用微前端架构
对于这一点,或许你已经从很多优质文章当中或多或少了解到了微前端架构的优势,就不再赘述丢书包的官话了,主要就从个人的亲身经历和一些感受出发讲讲为什么迫切想要尝试微前端架构吧。
「解构巨石应用」
去年接手了公司一个内部信息的中后台管理系统,而这个系统又是一年前开发的,上面已经有近十个子应用模块了,而且有好几个不同的人参与编写,那么时间跨度长
➕人员配置复杂
会出现什么问题呢?
项目工程结构复杂混乱
每个人的代码风格妖娆多姿,水平也参差不齐,互相都不敢乱动的别人代码。这就导致了很多滑稽的问题,比如一个 axios 实例化封装我就看到了两个,还放在不一样的地方;原本按需加载的组件库,然后不知道哪个萌萌哒整了个全量引入的函数偷偷摸摸的在一个隐秘的地方引入执行......library版本落后
前端领域技术更迭很快,但由于项目时间跨度太大加上较大的复杂度,你不能保证某个包的每次更新都是向下兼容且各个被依赖的包不受此更新影响。我就被这种问题坑过,当时由于某个包更新以后不能被另外一个旧版本的包使用导致一些奇怪的 BUG 接连出现,当报错信息比较粗糙的时候你真的很难去找到这个问题的根源出在哪里。有杠精说那我不更新不就完事了?道理是如此,但如果这个包是因为修复其本身的 bug 而必须进行的更新呢。扩展成本高
由于全部的代码都在一个环境下运行,在扩展新功能模块的时候必须非常小心谨慎,在开发时需要确保这个功能不会影响到其他的已有功能的正常使用,而要确保不会出现这种情况也加重了测试的流程和复杂度。另外,每一个微小的改动都需要重新打包整个项目并重新部署上线等,对一个复杂的巨石应用扩展功能是很令人头疼的事。
或许幸运的你也一样跟我接触过“屎山代码”,每次开发新 feature 的时候都胆战心惊,也许哪一天这就是压倒程序猿的最后一座山。
「技术栈无关」
很喜欢鲁迅说的一句话:
❝「事实上如果所有的 web 技术栈能做到统一,所有 library 的升级都能做到向下兼容,我们确实就不需要微前端了。」 —— 鲁迅
❞
「这是微前端架构
最最最核心的价值」
如果每个功能模块都能做到技术栈无关,我们开发新模块的时候就可以随心所欲使用最喜欢最潮的技术栈,不用再考虑那些性能低下使用复杂的 libray 带来的束缚。
比如如火如荼的 Vue3 (写到这里的时候尤大刚好悄咪咪地release3.0了),一波令人激动 API 让人很难不心动。然而受限于已有项目的枷锁,你又不能真的大胆地去对整个项目进行重构,这真的让人有点沮丧不是吗。
从老板的角度看人才招聘,受限于公司整体技术栈的影响,(在不考虑超级大牛的情况下)很大程度上你只能招这个技术栈下的人才,加入 react 和 vue 的优秀人才比例是五五开,那么在人才挑选上就几乎少了一半的可选择性。
(虽然你 React 真的很 6,但我们现有的业务更需要 Vue 的人才高速搬砖)
当然,如果还不太了解微前端这个概念的话可以推荐大家带着轻松的心情看看这几篇文章:
「为什么要使用 Vue+qiankun 做这个项目」
事实上,首先再次强调微前端的核心价值是技术栈无关
,这意味着这个项目用 React 也好用 Vue 也可以甚至 Angular 也无所谓,你可以用任意一个框架去替换其中的某一个微应用或者是扩展一个微应用。
用Vue
做这个项目的直接原因是因为我对 Vue 更熟悉一些,对新架构的尝试中遇到瓶颈可以更轻松的去解决或者用一些有 hack 味道的方法去绕过,就算踩坑也踩得有底气一些。
另外一个原因是因为qiankun
官网并没有给出 Vue 使用的例子,这也许就让一部分人望而止步了。所以就打算用Vue
做一个实践性的开源项目,让更多希望在 Vue 中使用 qiankun 的人可以在 Vue 中使用时对初始化和 API 的使用有例可循。
那么用qiankun
的原因是横向对比了一些解决方案以后觉得很不错的一个框架:
- 接入简单,易于重构已有应用
- 几乎包含所有构建微前端系统时所需要的基本能力(css 隔离、js 沙箱、预加载、应用通信)
- 生产可用,具有一定的健壮性,经受过线上系统的考验。
「ShowCases」
首页
沸点
话题
小册
活动
在这个项目中不涉及登录鉴权的功能,当然并不是因为在微前端架构下难以实现,主要原因一方面出于信息安全
的考虑,因为这个项目中的数据都来源于掘金本身的接口,如果涉及到用户信息,需要费很大的功夫去维护用户信息的安全性;另一方面是由于这些功能的复杂度几乎都在业务逻辑处理层面上,在qiankun
中各个应用可以无阻无差别地共享使用相同的WebStorage
和Cookie
来存取token
,所以登录鉴权相关的实现难度完全不在于是否使用微前端架构,而是在其本身业务逻辑的复杂度上。
「How」
技术栈
「开发」
qiankun
微前端解决方案Vue
全家桶搭建前端页面AntDesign Vue
个人最喜欢的组件库Koa
用作一个简单的 BFF,本来只是想在 Node 跑个 Mock 模拟一下数据的,后来干脆就把请求代到掘金线上的接口了,方便对接口数据的集中处理和过滤等(比如日后掘金接口大改,我也不用一个个去翻前端项目里的代码只需要在 BFF 中把数据处理成原有的样子即可)channing-cli
(无关紧要)自己开发的一个用于快速搭建微前端架构的脚手架命令行工具
「部署」
Nginx
部署前端应用pm2
部署 Node 服务腾讯云
1M 带宽一核 2G 的乞丐版远程 CentOS7.5 服务器
整体架构
「*核心实现」
核心的实现主要分为四个点讲讲:
- 主应用初始化
- 微应用初始化
- 请求代理
- 应用间通信
「主应用初始化」
qiankun
只需要在主应用
中安装,微应用是不需要的
「在主应用中安装qiankun
」
$ yarn add qiankun # 或者 npm i qiankun -S
「/main.js」
import {
registerMicroApps,
start,
runAfterFirstMounted,
setDefaultMountApp,
initGlobalState,
} from "qiankun";
// 引入我们写好的微应用注册信息
import apps from "@/shared/microApps";
// 注册各个微应用
registerMicroApps(apps);
// 设置主应用启动后默认进入的微应用
setDefaultMountApp(apps[0].activeRule);
// 启动qiankun
start();
「/shared/microApps」
const apps = [
{
name: "micro-juejin-home",
activeRule: "/micro-juejin-home",
entry: `//localhost:8071`,
container: "#subApp",
$meta: {
title: "首页",
},
},
{
name: "micro-juejin-boiling",
activeRule: "/micro-juejin-boiling",
entry: `//localhost::8072`,
container: "#subApp",
$meta: {
title: "沸点",
];
module.exports = apps;
❝❞
name
: 没有什么限制,保证唯一性就好了,但为了减轻配置的复杂度,这里我选择尽量保持与微应用的项目名、activeRule 保持一致entry
: 微应用入口 url(就是你能够直接打开微应用的那个地址)container
: 微应用挂载到主应用上的 DOM 选择器或者 DOM 节点activeRule
: 微应用的激活规则(相当于 VueRouter 里的 path,也支持动态路由的写法)$meta
: (注意)这个不是官方配置,是我为了做脚手架初始化时的一个信息记录属性,比如可以用来做导航条的 title
「微应用接入」
「main.js」
import "./public-path";
import Vue from "vue";
import VueRouter from "vue-router";
import App from "./App.vue";
import { routes } from "./router";
import store from "@/store";
Vue.config.productionTip = false;
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
const packageName = require("../package.json").name;
let router = null;
let vm = null;
// 将创建Vue根实例并挂载封装成可调用的函数
function VueRender(props = {}) {
const { container } = props;
router = new VueRouter({
base: window.__POWERED_BY_QIANKUN__ ? `/${packageName}` : "", // 这个需要与主应用中注册的微应用的activeRule一致
mode: "history",
routes,
});
vm = new Vue({
router,
store,
render: (h) => h(App),
}).$mount(container ? container.querySelector("#app") : "#app");
}
// 如果不作为微应用加载则直接调用VueRender
if (!window.__POWERED_BY_QIANKUN__) {
VueRender();
}
// 微应用必须暴露的三个加载生命周期hooks
export async function bootstrap() {
console.log("[vue] vue app bootstraped");
}
export async function mount(props) {
console.log("[vue] props from main framework", props);
VueRender(props);
}
export async function unmount() {
vm.$destroy();
vm = null;
router = null;
}
❝❞
__webpack_public_path__
这个很重要,决定了加载微应用中静态资源的路径,比如:
VueRouter中的base
要和主应用注册的微应用中的activeRule
相对应,这里我用微应用的包名统一作为路径的前缀是为了在脚手架工具中更方便做初始化配置。bootstrap
、mount
、unmount
是作为微应用加载必须暴露给qiankun
识别的生命周期hooks
「请求代理」
这部分同样非常重要,同时这也是在实践过程中踩过的坑。
❝在 qiankun 搭建微前端架构你必须知道的一点是:「所有的请求都是以
主应用的域
发起的」
所以我们需要做两件事:❞
- 微应用配置跨域响应头。
- API 异步请求的两次代理
「微应用配置跨域响应头」
主应用加载微应用是以fetch
的方式请求的,所以你得为微应用添加允许跨域
请求的响应头
。
以micro-juejin-home
的「vue.config.js」为例:
module.exports = {
devServer: {
headers: {
//指定允许其他域名访问
//一般用法(*,指定域,动态设置),3是因为*不允许携带认证头和cookies
"Access-Control-Allow-Origin": "http://localhost:8088",
//是否允许后续请求携带认证信息(cookies),该值只能是true,否则不返回
"Access-Control-Allow-Credentials": true,
},
// something else ...
}
「API 异步请求的两次代理」
在微应用中发起的请求可以理解成是以主应用的名义发的,所以我们在这里需要做两次代理
,先从「主应用代理到微应用」,再从「微应用代理到服务器地址」
- 「主应用代理到微应用」:
主应用中的vue.config.js
:
module.exports = {
devServer: {
proxy: {
"/api/micro-juejin-home": {
target: "http://localhost:8071",
ws: false,
changeOrigin: true,
},
"/api/micro-juejin-boiling": {
target: "http://localhost:8072",
changeOrigin: true,
ws: false,
},
//其他微应用同理...
},
// something else ...
},
};
- 「微应用代理到服务器地址」
以微应用micro-juejin-home
中的vue.config.js
为例:
module.exports = {
devServer: {
proxy: {
"/api/micro-juejin-home": {
target: "http://localhost:3000",
ws: false,
changeOrigin: true,
pathRewrite: {
'^/api/micro-juejin-home': "/api"
}
},
},
// something else ...
},
};
❝这里你可能会对
api
异步请求的写法和代理的配置会有些疑惑,但我这么做是有理由的:❞
- 为什么要在
/api
后面加个/micro-juejin-home
?以微应用的项目名称
作为「标识」,使主应用可以正确识别
这些异步请求是哪个微应用发起的,并且代理
给对应的微应用,让它自己去管理自己的api
。- 解决不同微应用的 API「命名冲突」问题,不需要考虑某个微应用的
api
是否和其他微应用重名
了,真正做到微应用只需要管好自己就行了。- 可以让微前端更好地与「微服务」相结合
❝「Nginx」的代理也是相同的道理,具体的配置文件可以参考本项目的各个「nginx.conf」
❞
「应用间通信」
应用间通信是通过一个全局「state」的「发布订阅模式」来实现的,这个「state」和发布和订阅则由qiankun
提供的一些「api」完成:
initGlobalState(state)
定义全局状态,并返回通信方法,建议在主应用使用,微应用通过 props 获取通信方法。onGlobalStateChange(callback)
在当前应用监听全局状态,有变更触发 callbacksetGlobalState(state)
按一级属性设置全局状态,微应用中只能修改已存在的一级属性。这个很像「react」中的「setState」,这个方法的「state」参数可以是全局state的一部分
,然后修改其中的属性值
。offGlobalStateChange()
移除当前应用的状态监听,微应用 umount 时会默认调用
「主应用中的使用」
「微应用中的使用」
在微应用中使用几乎跟主应用一样,只是不需要去初始化 state 而已,你可以在微应用mount
这个生命周期钩子中去获取并暴露监听全局state
和改变全局state
的方法。
比如我在项目中用到的一段代码:
// main.js
// ...
// 这里用export暴露给子组件调用,下同
export let onGlobalStateChange;
export let setGlobalState;
export async function mount(props) {
onGlobalStateChange = props.onGlobalStateChange;
setGlobalState = props.setGlobalState;
VueRender(props);
//这里加载完毕后可以改变全局的状态通知主应用让其Loading组件消失
setGlobalState({ isLoadingMicro: false });
}
export async function unmount() {
instance.$destroy();
instance = null;
router = null;
//这里微应用注销后通知主应用让其Loading组件出现
setGlobalState({ isLoadingMicro: true });
}
❝通过
❞qiankun
提供的这几个API
已经足以完成应用间的通信
。事实上,处于工程化的考虑,我们应该尽可能降低各个微应用间的耦合度,使各个微应用看起来更DRY
,即使在普通的 SPA 项目中亦是如此。
最后
事实上还有很多细节想分享的但无奈字数限制,希望有朝一日大掘金能对mdnice完美支持吧(花了一晚上把几千个冗余css过滤掉,想吐)。
虽然写这篇文章花了很多的时间精力,但你的「点赞」就是继续分享的动力。
另外,听说喜欢点star⭐的人运气总不会太差 「GitHub传送门」
- 「
生产环境
」:「掘金(MicroApps): www.channing-bbs.club」