likes
comments
collection
share

微前端(qiankun)最直白Demo带你轻松入门

作者站长头像
站长
· 阅读数 28

微前端(qiankun)最直白Demo带你轻松入门

🥗初篇

因为前段时间公司一个原先用react技术栈的后台项目,需要增加一个自定义图表的功能,接触过自定义图表的都清楚自定义图表的复杂程度,所以我们研发小组最终决定找一款开源的自定义图表来完成该需求,最终选择了gitee上一款star有7.6k项目,项目地址:gitee.com/anji-plus/r…

随之而来的就是如何将两个项目合二为一这个大问题,粗略看了下该开源的自定义图表光包就接近有百个,而且该开源项目使用的是vue,粗暴合并需要花费的时间难以想象。

最终查询了诸多信息后,看到了微前端qianduan能够解决我现在所面临的问题,最终将公司原项目与自定义图表合并的问题解决后,遂有感而发,书写一个微前端的入门文章,将我自己的理解以及实践记录一下,希望也对有需要的小伙伴一些帮助;

如果文章有知识点错误之处,欢迎大家指正,共同学习进步,谢谢!

一、了解微前端

微前端官网地址

微前端的概念其实非常类似于后端的微服务架构,其出发点就是将一个巨石应用分为多个子应用,每个子应用后可以独立运行、开发、部署;最终达到分布式,多团队并行的项目需求。

微前端是有一个主应用也称基石项目为主体,嵌入其他n多个的子应用,这些子应用之间都是相互独立,并且可以相互通信的。

微前端特点:

  1. 技术栈无关(主框架不限制接入应用的技术栈,微应用具备完全自主权);
  2. 独立开发、独立部署(微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新);
  3. 增量升级(在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全面的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略);
  4. 独立运行时(每个微应用之间状态隔离,运行时状态不共享);

二、搭建微前端 qiankun

2-1、demo 技术栈选择

为了保证demo能够尽可能的测试到上面的微前端特点,我将选用两种不同技术栈的后台独立项目,完成demo案例!

此次案例主应用我准备使用Vue技术栈的vue-admin-template(算是Vue圈子内知名的开源后台项目了,这个admin-template是基础模板,没有其他多余的功能):

Vue项目地址

微前端(qiankun)最直白Demo带你轻松入门

子应用则是使用了React技术栈,项目是我自己用webpack5 + typescript + react18从零搭建的一个后台项目基础模板,希望大家能够支持下,给作者我点点star,谢谢哈!

React项目地址

微前端(qiankun)最直白Demo带你轻松入门

2-2、主应用安装qiankun

    npm i qiankun && yarn add qiankun

2-3、主应用微前端配置

1.创建放置微应用路由文件夹

微前端(qiankun)最直白Demo带你轻松入门

2. 创建app.js(放置微应用路由内容)

//  microApps 微应用集合
const microApps = [
  {
    name: 'react-app',
    entry: '//localhost:9090',
    activeRule: '#/react-app', // 如果使用hashRouter 则在微应用路由路径前加 #
    // activeRule: '/react-app', // 使用historyRouter,无需添加 #
    
    // 主应用给微应用传参,后面会写到传参相关内容
    props:{}
  },
]

const apps = microApps.map((item) => {
  return {
    ...item,
    container: '#qiankun-viewport', // 微应用挂载在主应用中的盒子的 id
  }
})

export default apps

🥗 配置参数详解

  • name: 必选,微应用的名称,微应用之间必须确保唯一。
  • entry:必选,微应用的入口路径地址。
  • container:必选,微应用的容器节点的选择器或者 Element 实例。
  • activeRule:必选,微应用的激活规则。
  • loader:可选,loading 状态发生变化时会调用的方法。
  • props:可选,主应用需要传递给微应用的数据。

3.在主应用中设置放置微应用挂载用的盒子(div)

一般都是位于主应用 layout 中的 content 组件的位置

微前端(qiankun)最直白Demo带你轻松入门

4.使用 qiankun 的 registerMicroApps 注册微应用信息,并且配置触发钩子(创建 /qiankun/index.js)

import apps from './app'
import {
  registerMicroApps,
  addGlobalUncaughtErrorHandler,
  start
} from 'qiankun'

// 注册微应用基础信息
// 即 apps 微应用数组,后面对象为配置触发钩子,与vue的钩子类似
registerMicroApps(apps, {
  beforeLoad: app => {
    console.log("before load app.name====>>>>>", app.name);
  },
  beforeMount: [
    app => {
      console.log("before mount app.name====>>>>>", app.name);
    }
  ],
  afterMount: [
    app => {
      console.log("after mount app.name====>>>>>", app.name);
    }
  ],
  afterUnmount: [
    app => {
      console.log("after unmount app.name====>>>>>", app.name);
    }
  ]
})

// 添加全局的未捕获异常处理器
addGlobalUncaughtErrorHandler((event) => {
  console.err('微应用加载失败!', event);
})

export default start

🥙 配置参数详解

  • beforeLoad - Lifecycle | Array<Lifecycle> - 可选
  • beforeMount - Lifecycle | Array<Lifecycle> - 可选
  • afterMount - Lifecycle | Array<Lifecycle> - 可选
  • beforeUnmount - Lifecycle | Array<Lifecycle> - 可选
  • afterUnmount - Lifecycle | Array<Lifecycle> - 可选

5. 在主应用的 main.js 中启动 qiankun

// qiankun 微应用
import start from '@/qiankun'

new Vue({
  el: '#app',
  router,
  store,
  render: h => h(App)
})

// start 启动函数需要放置在 vue注册之后
start({
  sandbox: {
    //   strictStyleIsolation: true, // 开启严格的样式隔离模式
    experimentalStyleIsolation: true // 开启单实例场景子应用之间的样式隔离
  }
})

🌮 start 配置参数详解

  1. prefetch:可选,是否开启预加载,默认为 true
  • 配置为 true 则会在第一个微应用mount完成后,开始预加载所有微应用的静态资源。
  • 配置为 'all' 则主应用 start 后即开始预加载所有微应用静态资源
  • 配置为 string[] 则会在第一个微应用 mounted 后开始加载数组内的微应用资源
  • 配置为 function 则可完全自定义应用的资源加载时机 (首屏应用及次屏应用)
  1. sandbox:可选,是否开启沙箱,默认为 true
  • 默认情况下沙箱可以确保单实例场景子应用之间的样式隔离,但是无法确保主应用跟子应用、或者多实例场景的子应用样式隔离。当配置为 { strictStyleIsolation: true } 时表示开启严格的样式隔离模式。这种模式下 qiankun 会为每个微应用的容器包裹上一个 shadow dom 节点,从而确保微应用的样式不会对全局造成影响。
  1. singular:可选,是否为单实例场景,单实例指的是同一时间只会渲染一个微应用。默认为 true
  2. fetch - Function - 可选,自定义的 fetch 方法。
  3. getPublicPath - (entry: Entry) => string - 可选,参数是微应用的 entry 值。
  4. getTemplate - (tpl: string) => string - 可选。
  5. excludeAssetFilter - (assetUrl: string) => boolean - 可选,指定部分特殊的动态加载的微应用资源(css/js) 不被 qiankun 劫持处理。

此处参数配置信息都是截取自官网,如果有详细想了解的小伙伴,可直接点击链接跳转:qiankun

6. 主应用添加微应用的菜单路由

  // qiankun react-app
  {
    path: '/react-app',
    component: Layout,
    redirect: '/react-app/home',
    name: 'React',
    meta: { title: 'React', icon: 'el-icon-s-help' },
    children: [
      {
        path: 'login',
        name: 'login',
        meta: { title: '登录页', icon: 'nested' }
      },
      {
        path: 'home',
        name: 'home',
        meta: { title: '主页', icon: 'nested' }
      },
      {
        path: 'test2',
        name: 'test2',
        meta: { title: '测试2', icon: 'nested' }
      }
    ]
  },

到此为止,主应用部分改造完毕,接下来,需要对微应用进行改造;

2-4、微应用改造

1.打包工具的改造

本文案例中 微应用是使用 webpage5 作为打包工具:

进入 build/webpack.base.js

原来的 output 打包配置

  // 打包文件出口
  output: {
    filename: 'static/js/[name].[chunkhash:8].js', // 每个输出js的名称
    path: path.resolve(__dirname, '../dist'), // 打包的出口文件夹路径
    clean: true, // webpack4需要配置clean-webpack-plugin删除dist文件,webpack5内置了。
    publicPath: isDev ? '/' : './', // 打包后文件的公共前缀路径
  },
  // 打包文件出口
  output: {
    filename: 'static/js/[name].[chunkhash:8].js', // 每个输出js的名称
    path: path.resolve(__dirname, '../dist'), // 打包的出口文件夹路径
    publicPath: isDev ? '/' : './', // 打包后文件的公共前缀路径
    
    // qiankun
    library: `react-app`, // 此处名称需要与 主应用的 name 一样
    libraryTarget: 'umd',
    chunkLoadingGlobal: 'webpackJsonp_react-app',// webpack5 更改为 chunkLoadingGlobal
    jsonpFunction: `webpackJsonp_react-app`, // webpack4 则配置为 jsonpFunction
  },

2. index.tsx 改造

原先的代码:

const root = document.getElementById('root')
if (root) {
  createRoot(root).render(
    <HashRouter>
      <ConfigProvider>
        <App />
      </ConfigProvider>
    </HashRouter>,
  )
}

改造后的代码:

let root = '' as any

function render() {
  root = createRoot(document.getElementById('root') as HTMLElement)
  root.render(
    <HashRouter>
      <ConfigProvider>
        <App />
      </ConfigProvider>
    </HashRouter>,
  )
}

// 动态设置 webpack publicPath,防止资源加载出错
if ((window as any).__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = (window as any).__INJECTED_PUBLIC_PATH_BY_QIANKUN__
}

//判断其是否作为qiankun子应用使用
if (!(window as any).__POWERED_BY_QIANKUN__) {
  render()
}

/**
* bootstrap 只会在微应用初始化的时候调用一次,
  下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
* 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
*/
export async function bootstrap() {
  console.log('ReactApp bootstraped')
}

/**
 * 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
 */
export async function mount(props: any) {
  console.log('ReactApp mount', props)
  render()
}

/**
 * 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
 */
export async function unmount() {
  console.log('ReactApp unmount')
  // 销毁组件
  root.unmount()
  root = ''
}

3. 改造微应用的路由

routers文件夹下的路由改造:

// 多增加一个判断,判断是从主应用进入的,还是从微应用进入的,主应用进入,则添加 /react-app,微应用直接进入的则不变;
const BASE_NAME = (window as any).__POWERED_BY_QIANKUN__ ? '/react-app' : ''

export const rootRouter = [
  {
    path: BASE_NAME + '/login',
    element: <Login />,
    meta: {
      title: '登录页',
    },
  },
  {
    path: BASE_NAME + '/',
    element: <Navigate to={BASE_NAME + '/home'} />,
  },
]

路由拦截器改造:

拦截器路径地址:/src/utils/modules/routers.tsx

// 判断是否为微前端进入,进入添加前缀
const BASE_NAME = (window as any).__POWERED_BY_QIANKUN__ ? '/react-app' : ''

// // 设置白名单
const whitePaths = [BASE_NAME + '/login', BASE_NAME + '/404', BASE_NAME + '/500']

// 路由守卫配置函数
export const AuthRouter = (props: any) => {
  const { pathname } = useLocation()
  let {
    useUserStore: { token, setUserInfo, userInfo },
  } = useStore()
  // 第一步 判断有无 token
  if (token) {
    // 第二步 判断是否前往login页面,等于跳转 '/', 不等于则继续判断
    console.log(32, pathname === BASE_NAME + '/login')

    if (pathname === BASE_NAME + '/login') {
      return <Navigate to={BASE_NAME + '/'} replace />
    } else {
      // 第三步 判断是否拿到用户个人信息及权限,没拿到则进行axios请求数据,进行信息存储及权限路由渲染,否则直接放行
      // 该版本为基础版,这些数据展示都为链接,后续会逐步更新
      if (Object.keys(userInfo).length < 1) {
        // 获取用户个人信息 (此处使用 async await会报错)
        getUserAPI()
          .then((res) => {
            setUserInfo(res.data as any)
          })
          .catch((err) => {})

        // 合并路由
        return props.children
      } else {
        return props.children
      }
    }
  } else {
    if (whitePaths.includes(pathname)) {
      return props.children
    } else {
      return <Navigate to={BASE_NAME + '/login'} replace />
    }
  }
}

其他的路由即所有跳转路由前面都需要添加这个,本案例路由较为简单,我就没有做优化,如果复杂项目的小伙伴们可以进行优化。

3. 改造微应用的layout

因为,微应用也有自己的headermenu,所以需要在是微应用进入的时候,将layoutheadermenu进行隐藏:

 // init mounted
  useEffect(() => {
    listeningWindow()
    setIsQiankun(!(window as any).__POWERED_BY_QIANKUN__)
  }, [])
  
  return (
    /**
     * 此处不要使用 layout 包裹整个 sider、header、content,会导致layout闪烁
     * 此处需要将 silder 与 header&&content 分开布置,可以解决闪烁问题
     */
    <div className={classes['layout-container']}>
      {isQiankun ? (
        <Sider theme="light" trigger={null} collapsible collapsed={collapsed}>
          <div className={classes.logo}>
            <div className={classes['logo-image']}></div>
            {!collapsed && <div className={classes['logo-font']}>Leno Admin</div>}
          </div>
          <MenuCom />
        </Sider>
      ) : (
        ''
      )}
      <Layout className={classes['site-layout']}>
        {isQiankun ? <HeaderCom collapsed={collapsed} setCollapsed={setCollapsed} /> : ''}
        {isQiankun ? <TabsCom /> : ''}
        <ContentCom />
      </Layout>
    </div>
  )

全部改造完成后,便可以从主应用打开微应用的页面啦!

微前端(qiankun)最直白Demo带你轻松入门

三、微前端之间传值

主应用与微应用之间原则上来说是需要尽可能的降低之间的耦合,这是防止因为满足主应用的功能从而导致微应用无法单独运行,那就有些得不偿失了。一般上来说,主应用与微应用常见需要传值的也就是登录信息,以避免用户主要用登录了,还需要在微应用再登录。

主应用传值代码:

文件地址:src/qiankun/app.js

import store from '@/store/index'

//  microApps 挂载子应用
const microApps = [
  {
    name: 'react-app',
    entry: '//localhost:9090',
    // 如果使用hashRouter 则在微应用路由路径前加 #
    activeRule: '#/react-app',
    // 使用historyRouter,无需添加 #
    // activeRule: '/react-app',

    // 主应用给微应用传参,将主应用中个人信息传值给子应用
    props: {
      vueUser: store.getters
    }
  },
]

const apps = microApps.map((item) => {
  return {
    ...item,
    // 子应用挂载的div
    container: '#qiankun-viewport',
  }
})

export default apps

微应用传值代码:

  1. 在 mount 钩子中获取主应用传的值

文件地址:src/index.tsx

import useStore from '@/store/index'

/**
 * 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
 */
export async function mount(props: any) {
  console.log('ReactApp mount', props)

  // 新增: 将父应用的 user 信息存入到 微应用中
  const {
    useUserStore: { setVueUserInfo },
  } = useStore()
  setVueUserInfo(props.vueUser)

  render()
}
  1. store 中存储主应用传值

文件地址:src/store/modules/user.tsx

注意:本文的 微应用React 使用的状态管理工具为 mobx

  // 存放 主应用 个人信息
  vueUserInfo = {}
  
  // 存储 主应用 userinfo
  setVueUserInfo = (userInfo: IuserInfo) => {
    console.log(30, userInfo)

    this.vueUserInfo = userInfo
  }

  // 删除 主应用 userInfo
  removeVueUserInfo = () => {
    this.vueUserInfo = {}
  }
  1. home 主页中使用主应用的传值

文件地址:src/view/home/index.tsx

import React from 'react'
import useStore from '@/store/index'

const Home = () => {
  // 将主应用的 userInfo 显示到微应用 home 主页
  interface IVueuserInfo {
    name?: string
    avatar: string
    device: string
    token: string
    sider: {
      opened: boolean
      withoutAnimation: boolean
    }
  }

  const { useUserStore } = useStore()
  const vueUser = useUserStore.vueUserInfo as IVueuserInfo

  return (
    <div>
      Hello Home
      <hr />
      <div>{vueUser.name}</div>
      <div>{vueUser.avatar}</div>
      <div>{vueUser.token}</div>
    </div>
  )
}
export default Home
  1. 显示效果
微前端(qiankun)最直白Demo带你轻松入门

四、踩过的坑

在使用 qiankun 的时候也踩过不少的坑,其中一个卡了我差不多一天,就是主应用和微应用都是 Vue 技术栈的时候,会报路由错误,最终排查了一天,期间也查阅了不少资料,发现其实是Vue模块全局污染了;

以下是官网对这个报错的解释:

qiankun 中的代码使用 Proxy 去代理父页面的 window,来实现的沙箱,在微应用中访问 window.Vue 时,会先在自己的window里查找有没有 Vue 属性,如果没有就去父应用里查找。

VueRouter 的代码里有这样三行代码,会在模块加载的时候就访问 window.Vue 这个变量,微应用中报这个错,一般是由于父应用中的 Vue 挂载到了父应用的 window 对象上了。

微前端(qiankun)最直白Demo带你轻松入门

其实官网也早已经有了解决方法,就是主要用中不使用CDN加载包,第二个就是将主应用中window.Vue改变个名称;

// 在注册微应用的 beforeload 钩子中,将window.Vue改一个名称即可;

registerMicroApps(apps, {
  beforeLoad: (app) => {
    // 解决找不到 $router的问题
    window.Vue2 = window.Vue
    delete window.Vue
    // 加载微应用前,加载进度条
    NProgress.start()
    console.log('before load', app)
    return Promise.resolve()
  },
  afterMount: (app) => {
    // 加载微应用前,进度条加载完成
    NProgress.done()
    console.log('after mount', app.name)
    return Promise.resolve()
  }
})

五、配置统一命令行,同时启动主应用和微应用

  1. 下载包:npm-run-all
npm i npm-run-all
  1. 配置scripts启动
  "scripts": {
    "all:install": "npm-run-all --serial install:*", 
    "all:dev": "npm-run-all --parallel dev:*",
    "install:react-demo": "cd react-demo && npm i",
    "dev:react-demo": "cd react-demo && npm run dev:dev",
    "install:vue-demo": "cd vue-demo && npm i",
    "dev:vue-demo": "cd vue-demo && npm run dev"
  },

就是 cd 切换到各应用的文件夹下,输入各应用的项目启动命令行;

好了,做完此步骤就可以轻松的管理你的微前端项目啦!也不用在一个个应用的启动项目,直接一键启动即可;

六、结语

我一直秉承着将一门技术用最简单、最小白的方式写出来,不喜欢故弄玄虚,写一些高大上的词;因为自己当初也是从一名新入门的小白慢慢走过来的,看到一些写的很晦涩的文档,就头疼;我努力让我的每一篇技术文章都以最直白的写法写出来,希望给学习进步的小伙伴们一些帮助!

这只是个默默研究学习技术的前端开发工程师,没有有趣的故事,没有华丽的文笔,没有很高深的技术,谢谢每一个认可我的小伙伴 ~ 💌

七、文中案例代码地址

案例代码