用微前端 qiankun 接入十几个子应用后,我遇到了这些问题

点击上方 前端Q,关注公众号

回复加群,加入前端Q技术交流群

简单了解微前端

微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略。

微前端具备以下特点:

  • 技术栈无关:主框架不限制接入应用的技术栈,子应用具备完全自主权

  • 独立开发、独立部署:既可以组合在一起运行,也可以单独运行。

  • 增量升级:在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略

  • 独立运行时:每个微应用之间状态隔离,运行时状态不共享

以上为 qiankun 官网对微前端的概括

快速上手

这里以 vue2.x + qiankun 为例

我们先用 vue-cli快速创建一个项目,作为主应用,这里把他取名为 main-app

sh
复制代码
vue create main-app

为跟实际项目更接近,我们暂时手动选择了安装这些

dd814720581acda40aa0d9800c3d3c97.jpeg

项目创建完后,我们把 main-app 复制一份作为子应用,改名为 sub-app,现在我们有了 main-app 主应用和 sub-app 子应用。

好的,基本的准备工作已经完成,我们开始基于刚刚创建的两个项目改造成微前端应用

主应用

在 main-app 中,安装 qiankun:

sh
复制代码
yarn add qiankun # 或者 npm i qiankun -S

目录 src 下新建 src/qiankun/index.js

注册微应用并启动,代码如下:

js
复制代码
import { registerMicroApps, start } from "qiankun";
import store from "@/store";


registerMicroApps([
  {
    name: "sub-app",
    entry: "http://localhost:7663", // 微应用入口
    container: "#subapp-viewport", // 微应用挂载的div
    activeRule: "/sub-app/",
    props: {
      // 此处将父应用的 store 传入子应用
      store
    }
  }
]);

export default start;

这里我们把微应用的路由前缀定义为 sub-app

views 目录下新建一个组件 src/views/qiankun/index.vue,我们提供一个 id 为 subapp-viewport 的容器 DOM 供子应用挂载

vue
复制代码
<template>
  <div id="subapp-viewport"></div>
</template>

<script>
import start from "@/qiankun/index";
export default {
  mounted() {
    // 启动微前端
    if (!window.qiankunStarted) {
      window.qiankunStarted = true;
      start();
    }
  }
};
</script>

找到路由文件夹,router/index.js下加入如下路由,用以匹配微应用

js
复制代码
{
  path: "/sub-app/*",
  meta: { title: "子应用" },
  component: () => import("@/views/qiankun/index")
}

子应用

上面我们对主应用的改造基本完成,接下来我们对之前复制出来的 sub-app 稍加改造,使其成为子应用

先找到 /src/router/index.js ,对路由文件稍加改造

删除

js
复制代码
const router = new VueRouter({
  mode: "history",
  base: process.env.BASE_URL,
  routes
});

export default router;

于文件最后添加

js
复制代码
export default routes;

找到main.js

将 import router from './router' 修改为 import routes from './router' ,并增加 import VueRouter from "vue-router";, 这里我们把主子应用路由都设置为 history 模式。

删除:

js
复制代码
new Vue({
  router,
  store,
  render: h => h(App)
}).$mount("#app");

增加

js
复制代码
let router = null;
let instance = null;

if (window.__POWERED_BY_QIANKUN__) {
  // eslint-disable-next-line
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
}

function render(props = {}) {
  const { container } = props;
  router = new VueRouter({
    base: window.__POWERED_BY_QIANKUN__ ? "/sub-app/" : "/", // 抛出路由加前缀
    mode: "history",
    routes
  });

  instance = new Vue({
    router,
    store,
    render: h => h(App)
  }).$mount(container ? container.querySelector("#app") : "#app");
}

if (!window.__POWERED_BY_QIANKUN__) {
  render();
}
export default instance;

export async function bootstrap() {
  console.log("[vue] vue app bootstraped");
}

export async function mount(props) {
  // props 包含主应用传递的参数  也包括为子应用 创建的节点信息
  console.log("[vue] props from main framework", props);
  render(props);
}

export async function unmount() {
  instance.$destroy();
  instance = null;
  router = null;
}

最终 main.js 文件修改如下

js
复制代码
// main.js
import Vue from "vue";
import App from "./App.vue";
import routes from "./router";
import store from "./store";
import VueRouter from "vue-router";

Vue.config.productionTip = false;
// new Vue({
//   router,
//   store,
//   render: h => h(App)
// }).$mount('#app')

// 微前端 - 子应用配置
let router = null;
let instance = null;

if (window.__POWERED_BY_QIANKUN__) {
  // eslint-disable-next-line
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
}

function render(props = {}) {
  const { container } = props;
  router = new VueRouter({
    base: window.__POWERED_BY_QIANKUN__ ? "/sub-app/" : "/", // 抛出路由加前缀
    mode: "history",
    routes
  });


  instance = new Vue({
    router,
    store,
    render: h => h(App)
  }).$mount(container ? container.querySelector("#app") : "#app");
}

if (!window.__POWERED_BY_QIANKUN__) {
  render();
}
export default instance;

export async function bootstrap() {
  console.log("[vue] vue app bootstraped");
}

export async function mount(props) {
  // props 包含主应用传递的参数  也包括为子应用 创建的节点信息
  render(props);
}

export async function unmount() {
  instance.$destroy();
  instance.$el.innerHTML = '';
  instance = null;
}

在 sub-app 下新建 vue.config.js ,增加配置如下

js
复制代码
const { name } = require('./package.json')

module.exports = {
  publicPath: '/', // 打包相对路径
  devServer: {
    port: 7663, // 运行端口号
    headers: {
      'Access-Control-Allow-Origin': '*' // 防止加载时跨域
    }
  },
  chainWebpack: config => config.resolve.symlinks(false),
  configureWebpack: {
    output: {
      library: `${name}-[name]`,
      libraryTarget: 'umd', // 把微应用打包成 umd 库格式
      jsonpFunction: `webpackJsonp_${name}`
    }
  }
}

最后我们在子应用新建一个测试页面以供嵌入主应用,路由暂且取名 /test

vue
复制代码
// views/sub-app/index.vue
<template>
  <div class="sub-app">
    我是子应用
  </div>
</template>

<style lang="scss" scoped>
.sub-app {
  cursor: pointer;
  background-color: aqua;
}
</style>

至此,我们对主应用和子应用的改造基本完成,接下来我们测试一下,我们在主应用的 app.vue添加一个按钮,使其点击的时候添加事件 this.$router.push('/sub-app/test') 跳转至子应用

781be3c1cf9f6dfd25bb289563a5348b.jpeg

当我们点击按钮后,可以看到,子应用嵌入成功

3bc3b487cd98b922cb556e65b01b135d.jpeg

这里我们主子应用都采用了同一套技术栈,是因为在公司项目中我们也是这样做的,相同的技术栈可以实现公共依赖库、UI库等抽离,减少资源开销,提升加载速度,最重要的是:“减少冲突的最好方式就是统一”,通过约束技术栈在项目前期就尽可能的减少项目之间的冲突,减少了工作量与维护成本。

微前端常见问题

主子应用样式相互影响

各个应用样式隔离 这个问题乾坤框架做了一定的处理,在运行时有一个sandbox的参数,默认情况下沙箱可以确保单实例场景子应用之间的样式隔离,但是无法确保主应用跟子应用、或者多实例场景的子应用样式隔离。如果要解决主应用和子应用的样式问题,目前有2种方式:

  • 在乾坤种配置 { strictStyleIsolation: true } 时表示开启严格的样式隔离模式。这种模式下 qiankun 会为每个微应用的容器包裹上一个 shadow dom 节点,从而确保微应用的样式不会对全局造成影响。但是基于 ShadowDOM 的严格样式隔离并不是一个可以无脑使用的方案,大部分情况下都需要接入应用做一些适配后才能正常在 ShadowDOM 中运行起来,这个在 qiankun 的 issue 里面有一些讨论和使用经验。

  • 人为用 css 前缀来隔离开主应用和子应用,在组件层面用 css scoped进行组件层面的样式区分,在 css框架层面可以给css组件库加上不同的前缀,比如文档中的 antd 例子:配置 webpack 修改 less 变量

js
复制代码
{
  loader: 'less-loader',
+ options: {
+   modifyVars: {
+     '@ant-prefix': 'yourPrefix',
+   },
+   javascriptEnabled: true,
+ },
}

b. 配置 antd ConfigProvider

js
复制代码
import { ConfigProvider } from 'antd';
   
export const MyApp = () => (
  <ConfigProvider prefixCls="yourPrefix">
    <App />
  </ConfigProvider>
);

应用间通信

  1. localStorage/sessionStorage

  2. 通过路由参数共享

  3. 官方提供的 props

  4. 官方提供的 actions

  5. 使用vuex或redux管理状态,通过shared分享

具体实现参考这篇文章 qiankun的五种通信方式

qiankun 实现 keep-alive 需求

子项目 keep-alive 其实就是想在子应用切换时不卸载掉,仅仅是样式上的隐藏(display: none),这样下次打开就会更快。

但是 keep-alive 需要谨慎使用,同时加载并运行多个子应用,这将会增加 js/css 污染的风险。

产品那边其实当时是有提出这个需求的,当时第一时间想到的是借助 qiankun 的 loadMicroApp 函数来手动加载和卸载子应用。但是公司的项目主应用嵌入了十几个子应用,想到需要一个个处理,以及手动加载和卸载子应用所可能带来的一些边界问题处理,后面直接说这个需求不好实现。之后也就暂时搁置了

具体解决方案可以看 qiankun issues 里所给出的

路由跳转问题

在子应用中是没有办法通过 <router-link> 或者用 router.push/router.replace 直接跳转的,因为这个 router 是子应用的路由,所有的跳转都会基于子应用的 base 。当然了写 <a> 链接可以跳转过去,但是会刷新页面,用户体验并不好。

这里可以采用以下两种方式:

  • 将主应用的路由实例通过 props 传给子应用,子应用用这个路由实例跳转。

  • 路由模式为 history 模式时,通过 history.pushState() 方式跳转

这里我把他封装为了一个常用方法

js
复制代码
/**
 * 微前端子应用路由跳转
 * @param {String} url 路由
 * @param {Object} mainRouter 主应用路由实例
 * @param {*} params 状态对象:传给目标路由的信息,可为空
 */

const qiankunJump = (url, mainRouter, params) => {
  if (mainRouter) {
    // 使用主应用路由实例跳转
    mainRouter.push({ path: url, query: params })
    return
  }
  // 未传递主应用路由实例,传统方式跳转
  let searchParams = '?'
  let targetUrl = url
  if (typeOf(params) === 'object' && Object.keys(params).length) {
    Object.keys(params).forEach(item => {
      searchParams += `${item}=${params[item]}&`
    })
    targetUrl = targetUrl + searchParams.slice(0, searchParams.length - 1)
  }
  window.history.pushState(null, '', targetUrl)
}

适配vue-pdf 报错

找到vue-pdf的依赖包下的vuePdfNoSss.vue

vue
复制代码
//找到vue-pdf的依赖包下的vuePdfNoSss.vue
<style src="./annotationLayer.css"></style>
<script>
	import componentFactory from './componentFactory.js'
	if ( process.env.VUE_ENV !== 'server' ) {
		var pdfjsWrapper = require('./pdfjsWrapper.js').default;
		var PDFJS = require('pdfjs-dist/es5/build/pdf.js');
		if ( typeof window !== 'undefined' && 'Worker' in window && navigator.appVersion.indexOf('MSIE 10') === -1 ) {
      // 注释原本的引入方法
			// var PdfjsWorker = require('worker-loader!pdfjs-dist/es5/build/pdf.worker.js');
			  var PdfjsWorker=require('pdfjs-dist/es5/build/pdf.worker.js');
			PDFJS.GlobalWorkerOptions.workerPort = new PdfjsWorker();
		}
		var component = componentFactory(pdfjsWrapper(PDFJS));
	} else {
		var component = componentFactory({});
	}
	export default component;
</script>

修改项目的配置文件vue.config.js

js
复制代码
chainWebpack: (config) => {
  config.module
    .rule('worker')
    .test(/\.worker\.js$/)
    .use('worker-loader').loader('worker-loader')
    .options({
      inline: true,
      fallback: false
    }).end();
}

主项目和子项目部署到一起,子项目部署到二级目录(不占用这么多端口)

因为客户方的要求,可能有时候不允许服务器开太多的端口,因此需要把主应用和微应用部署到一起,公用一个端口。

主项目和子项目部署到一起,子项目部署到二级目录

qiankun在子应用中引入百度地图时报错解决

因为qiankun会把静态资源的加载拦截,改用fetch方式获取资源,所以要求这些资源支持跨域,这里我们使用qiankun提供的 excludeAssetFilter 将其加入白名单放行。

  • excludeAssetFilter - (assetUrl: string) => boolean - 可选,指定部分特殊的动态加载的微应用资源(css/js) 不被 qiankun 劫持处理

修改主应用 start 方法

js
复制代码
// 启动微前端
if (!window.qiankunStarted) {
  window.qiankunStarted = true
  start({
    singular: false,
    excludeAssetFilter: (assetUrl) => {
      // 过滤baidu
      const wihiteWords = ['baidu']
      if (wihiteWords.includes(assetUrl)) {
        return true
      }
      return wihiteWords.some(w => {
        return assetUrl.includes(w)
      })
    }
  })
}

其他一些常见问题可见于 qiankun官网

总的来说,微前端确实解决了一些项目中的痛点,但是切记微前端不是银弹,老旧项目带来的迁移成本,不同项目技术栈的兼容与边界问题处理,因为没有迫切的需求而接入微前端,只会带来额外的负担,很多时候,iframe 其实就很够用了。

24687b17c4ab406ff469fa8e7d4863d7.png

往期推荐

急招,速来!腾讯微信视频号电商招人~

024b5056b38c25901cf72a6ecbacce50.png

京东一面:post为什么会发送两次请求?

81489c67ad2e071864c3b998d677c9be.png

比 nvm 更好用的 node 版本管理工具

a1890331a8f3face5c0f2c3c55e20c43.png


最后

  • 欢迎加我微信,拉你进技术群,长期交流学习...

  • 欢迎关注「前端Q」,认真学前端,做个专业的技术人...

87a89a1b6436b58ecfa4b94fc1e185f1.jpeg

93ed8b5c69fe3cb3928fce85b9ef03a3.png

点个在看支持我吧

2c9e78030f9d2f88a70b71d419ac3e56.gif

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值