2018年 Single-SPA诞生了, single-spa是一个用于前端微服务化的JavaScript前端解决方案 (本身没有处理样式隔离、js执行隔离) 实现了路由劫持和应用加载;
2019年 qiankun基于Single-SPA, 提供了更加开箱即用的 API (single-spa + sandbox + import-html-entry),它 做到了技术栈无关,并且不推荐抽取公共方法,工具等,它推荐的是每个微服务是独立的,并且不相互影响的,并且接入简单。
微前端的框架现在比较成熟的是 “Single-spa” 和 “QianKun”, 而"QianKun"是基于Single-spa实现的,所以使用"QianKun"可能会更加的简单些,所以采用了笔者采用该方案去实现
"@vue/cli-service": "~5.0.0-beta.6",
"ant-design-vue": "^2.2.8",
"vue": "^3.2.20",
nginx:1.19.0
node:V12.18.2
主应用与子应用都是vue3.x开发的,下面开始实现的具体过程
因为是在现有的vue3.x项目的基础上去实现微服务,因此,整个实践过程并不是从零开始的,也即对现有的可运行项目的改造过程
对于项目的整个构想如下图所示
主应用基本是作为一个加载器的功能,只有登录,登录后获取到当前用户的所有菜单,然后展示所有的菜单,用于导航,在主应用注册所有的微服务,并提供公共信息给微服务,上图中的公共设置主要是404等展现的公共路由等
基本所有的导航都是微服务
端口号是否必须不同?
本地测试的时候,因为是本地启动的项目,都是localhost,只能通过端口号区分不同的微服务,所以端口必须不相同;
但是线上子应用,是可以单独部署的,所以端口号可能相同,也可能不相同,此时就不影响,只要部署成功即可
路由模式是否必须是history?
虽然网上大部分都是推荐的使用history路由模式,但是也支持使用哈希模式–createWebHashHistory
本次实践过程中所有的项目都是vue3.x开发的,以下是对项目的改造
npm i qiankun
window[“qiankunStarted”]=false;
该标志主要是用于确定是否已经启动qiankun服务,因为不能重复注册与启动,所以设置了该标志
history路由的注册方式
registerMicroApps([
{
name: 'child1', //子应用的名称,和子应用的ouput名称必须一致
entry: '//localhost:8081',//微服务的入口
container: '#sonApp',//主应用中用于展示微服务的container
activeRule: '/child/child1', // 子应用触发规则(路径)
},
]);
hash路由的注册方式
// 注册子应用
registerMicroApps([
{
name: 'child1',
entry: '//localhost:8081',//微服务的入口
container: '#sonApp',
activeRule: '#/micro/xxx/cli', // hash路由的注册方式
},
]);
通过实践发现注册子应用不是必须在main.ts文件中,可以是真正呈现子应用的页面中进行注册,只要确保加载微服务的时候已经注册了就可以了
container的值是否有什么要求?是否#sonApp必须是直接在App.vue中,还是在App.vue的组件中也可以?
container的id是用来加载子应用的dom的Id,因此只要保证加载微服务的时候该值存在即可,可以是App.vue文件中直接存在,也可以是用来展示微服务的其它vue单文件中的dom的id值
start({
prefetch:false//是否开启预加载
})
如果注册了多个微服务,开启预加载会加载其它微服务,该值默认是开启状态,如果微服务存在有问题的,打开预加载其实增加了调试的难度,所以可以选择关闭
完整代码
// 注册子应用 && 启动微服务
if(!window["qiankunStarted"]){
registerMicroApps([
{
name: 'cli5-beta6-test', //和导出微服务的名称相同
entry: 'http://localhost:8081',
container: '#micro',
activeRule: '#/micro/cli', // 子应用触发规则(路径)
},
{
name:'micro-permission-web',
entry: 'http://localhost:8083',
container: '#micro',
activeRule: '#/micro/permission', // 子应用触发规则(路径)
},
],{
beforeLoad: [
app => {
console.log(`${app.name}的beforeLoad阶段`)
return Promise.resolve();
}
],
beforeMount: [
app => {
console.log(`${app.name}的beforeMount阶段`)
return Promise.resolve();
}
],
afterMount: [
app => {
console.log(`${app.name}的afterMount阶段`)
return Promise.resolve();
}
],
beforeUnmount: [
app => {
console.log(`${app.name}的beforeUnmount阶段`)
return Promise.resolve();
}
],
afterUnmount: [
app => {
console.log(`${app.name}的afterUnmount阶段`)
return Promise.resolve();
}
]
});
start({// 开启服务
prefetch:false//是否开启预加载,开启预加载会加载其它微服务
})
addGlobalUncaughtErrorHandler((event) => {
//捕获微服务加载失败的错误
if(undefined!=event["error"].appOrParcelName){
//子应用加载失败跳转404
router.push("/404")
}
});
//启动qiankun的标志
window["qiankunStarted"]=true;
}
子应用需要做的改造包括:
vue.config.js
添加
const config = require('./package');
module.exports的devServer中添加
headers: {
'Access-Control-Allow-Origin': '*',
},
否则本地测试的时候,主应用会报跨域的错误
module.exports的根节点中添加配置
configureWebpack:{
output: {
library: {
name:`${config.name}`,//这里的导出值必须和主应用注册的时候的name值相同,否则会导致引入问题
type:'umd' // 把子应用打包成 umd 库格式
},
// jsonpFunction: `webpackJsonp_${config.name}`,//该值发现随着webpack的升级没有了,而且该值不影响跨域等问题,可以忽略该值
}
}
${config.name}必须和主应用注册的微服务名称相同,否则加载失败
public-path.ts
在src目录下直接添加 public-path.ts文件
if ((window as any).__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = (window as any).__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
该文件主要是用获取当前微服务所处的环境,是浏览器直接打开的,还是作为微服务被加载的
如果当前环境是微服务,则(window as any).INJECTED_PUBLIC_PATH_BY_QIANKUN qiankun的注入值是true
设定__webpack_public_path__的值为当前环境值
main.ts
main.ts的修改包括最上面添加:
import "./public-path";
用于获取当前环境值,然后在引入其它文件后,添加
const isQiankun = (window as any).__POWERED_BY_QIANKUN__
function render(props:any = {}) {
const { container } = props;
const app = createApp(App)
app.use(store).use(router).mount(container ? container.querySelector("#child") : "#child")//注意该值不能和主应用的值相同,该值为子应用index.html中的id值
}
/**
* 每次应用作为微服务被进入时都会调用 bootstrap 方法,并且只有第一次加载微服务会触发bootstrap
*/
export async function bootstrap() {
console.log('app bootstraped');
}
/**
* 每次应用作为微服务被进入时都会调用 mount 方法,通常我们在这里触发应用的渲染方法
*/
export async function mount(props: any) {
console.log("child application mount",props)
render(props)
}
/**
* 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例,
* 这里需要根据具体项目进行卸载,否则二次进入的时候会有问题
*/
export async function unmount(props: any) {
console.log(props)
}
/**
* 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效
*/
export async function update(props: any) {
console.log('update props', props);
}
//独立运行时
isQiankun || render();
子应用之间的id是否可以重复?
子应用id之间是可以重复的,因为子应用之间嵌入在主应用内部的,如果不存在同时在一个页面加载多个微服务,则没有影响,但是主应用和子应用最好不要相同,因为可能会存在于同一个页面,主应用的挂载Id可以变更
子应用配置完成后是否可以直接运行查看?
可以的,通过标志位判断当前微服务的运行环境,是运行在qiankun的环境下,还是独立运行,若是独立运行则直接渲染
isQiankun || render();
判断是否是在启动qiankun的环境下,如果不是则执行render(),启动项目,方便调试
注意需要设置unmount卸载内容,否则导致重复加载,已挂载,资源已注册等等问题
vue3.x 中的卸载方式
export async function unmount(props: any) {
console.log("micro application unmount", props)
app.unmount();
app = null;
router = null;
}
用于区分其它微服务,尤其是本地测试的时候,也可以根据主应用传递的值设置微服务的路由前缀
const router = createRouter({
history: createWebHashHistory((window as any).__POWERED_BY_QIANKUN__ ? "/#/micro/xxx" : "#"),
routes
});
动态设置微服务的前缀,微服务的卸载,比较完整的改造,子应用的完整代码:
const isQiankun = (window as any).__POWERED_BY_QIANKUN__
let app = null;
let router = null;
/**
* 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
*/
function render(props: any = {}) {
const { container } = props;
console.log("application render",store)
app=createApp(App, {
setup() {
//全局设置国际化,使用可以直接用$t('page.button.add')
const { t } = useI18n({ useScope: 'global' })
return { t }
}
});
const activeRule=store.state.activeRule;
router = createRouter({
//动态设置前缀
history: createWebHashHistory(isQiankun? activeRule : ""),
routes
});
app.use(i18n).use(store).use(router).mount(container ? container.querySelector("#app") : "#app")
}
export async function bootstrap() {
console.log('app bootstraped');
}
export async function mount(props: any) {
// props为注册该微服务时主应用传递的值
// console.log("micro application mount", props)
props.onGlobalStateChange((state, prev) => {
console.log("micro onGlobalStateChange mount");
// state: 变更后的状态; prev 变更前的状态
// console.log(state, prev);
});
//vuex改变微服务的路由前缀值
store.commit("setProjectActiveRule", props.activeRule)
// props.setGlobalState(state);
render(props)
}
/**
* 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
*/
export async function unmount(props: any) {
console.log("micro application unmount", props)
app.unmount();
app = null;
router = null;
}
/**
* 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效
*/
export async function update(props: any) {
// console.log('micro application update props', props);
}
//独立运行时
isQiankun || render();
目前比较简单的通讯方式,并且是主应用传递信息给微服务
registerMicroApps([{
name:projectName,
entry:projectDns,
container: '#micro',
activeRule:projectActiveRule, // 子应用触发规则(路径)
props:{
user:{ },
activeRule:projectActiveRule//用于动态设置子应用的路由前缀
}
}
}])
以上就是简单的微服务实现过程,因为在实现的过程中遇到的问题过多,内容过长因此进行了拆分,如果此时以满足你的需求你就可以忽视以下内容了
微服务接受信息方式参考上面 4. 微服务的路由前缀