如何让一个工程具备qiankun(乾坤)子应用+ Webpack5 模块联邦两种能力

292 阅读3分钟

最近要搞一些事,不能细说,大概就是一个基座需要动态拉取子应用,基座对子应用不用知道子应用具体是什么,按照基座标准注册就能加载子应用。子应用的注册、业务逻辑都自治。

image.png

方案

目前手里具备的技术:

  1. qiankun 微前端
  2. Webpack5 模块联邦(MF)

需要将两种能力集合到一个子应用中,让子应用通过MF的能力将一个config配置信息函数吐给基座,基座通过拆解config信息将子应用通过qiankun微前端能力拉取过来。

基座配置

基座集成qiankun先看官网:qiankun.umijs.org/zh/guide/ge… 基座配置MF请看:webpack.js.org/concepts/mo…

动态加载远程组件:

//asyncLoadModules.js
/* eslint-disable no-undef */
/**
 * 加载模块
 * @param {*} scope 服务名
 * @param {*} module 子应用导出模块路径
 */
export const loadComponent = (scope, module) => {
  return async () => {
    // Initializes the share scope. This fills it with known provided modules from this build and all remotes
    await __webpack_init_sharing__('default')
    // or get the container somewhere else
    const container = window[scope]
    // Initialize the container, it may provide shared modules
    await container.init(__webpack_share_scopes__.default)
    const factory = await window[scope].get(module)
    const Module = factory()
    return Module
  }
}
// 加载 打包好后得 js 文件
export const useDynamicScript = url => {
  return new Promise((resolve, reject) => {
    const element = document.createElement('script')
    element.src = url
    element.type = 'text/javascript'
    element.async = true
    element.onload = e => {
      console.log(e)
      resolve(true)
    }
    element.onerror = () => {
      reject(false)
    }
    document.head.appendChild(element)
  })
}
// remoteRef.js
import { useDynamicScript, loadComponent } from './asyncLoadModules'
// @todo 这里可以改成接口获取
const dynamicResource = {
  demo: {
    script: 'http://localhost:9989/remote-entry-demo.js',
    module: 'config',
  },
}

const load = async () => {
  const configs = []
  const keys = Object.keys(dynamicResource)
  for (const key of keys) {
    await useDynamicScript(dynamicResource[key].script)
    const { default: config } = await loadComponent(key, `./${dynamicResource[key].module}`)()
    configs.push({ name: key, config })
  }
  return configs
}
const res = load()
export default res

// bootstrap.js

...
  const { default: result } = await import('./remoteRef.js')
  const configArr = await result
  // 获取config信息之后存入状态管理或者什么地方
...

动态注册子应用:

// register-micro-app.js
import { loadMicroApp, initGlobalState } from 'qiankun'
import store from 'store'

// const isDev = process.env.NODE_ENV === 'development'
// 基座能力
const baseFunctions = {
  test: data => {
    console.log(data)
  },
}
// js2native能力
const js2nativeFunctions = {
  test: data => {
    console.log(data)
  },
}
/**
 * @param <Object>
 * - name<String>: micro-app name
 * - moduleFederations<Object>: module federations
 * @returns microApp:Object
 * - mount(): Promise<null>;
 * - unmount(): Promise<null>;
 * - update(customProps: object): Promise<any>;
 * - getStatus(): | "NOT_LOADED" | "LOADING_SOURCE_CODE" | "NOT_BOOTSTRAPPED" | "BOOTSTRAPPING" | "NOT_MOUNTED" | "MOUNTING" | "MOUNTED" | "UPDATING" | "UNMOUNTING" | "UNLOADING" | "SKIP_BECAUSE_BROKEN" | "LOAD_ERROR";
 * - loadPromise: Promise<null>;
 * - bootstrapPromise: Promise<null>;
 * - mountPromise: Promise<null>;
 * - unmountPromise: Promise<null>;
 */

const registerMicroApp = ({ name, moduleFederations, ...other }) => {
  return loadMicroApp({
              ...store.state.microApps.microApps[name],
              props: { moduleFederations, ...other },
            })
}

/**
 * 注册通讯
 * @param <Object>
 * - state:<Object>
 * @return <Object>
 * - onGlobalStateChange: (callback: OnGlobalStateChangeCallback, fireImmediately?: boolean) => void, 在当前应用监听全局状态,有变更触发 callback,fireImmediately = true 立即触发 callback
 * - setGlobalState: (state: Record<string, any>) => boolean, 按一级属性设置全局状态,微应用中只能修改已存在的一级属性
 * - offGlobalStateChange: () => boolean,移除当前应用的状态监听,微应用 umount 时会默认调用
 */

const registerCommunication = state => {
  const actions = initGlobalState({ microApplyFunction: null, runClientJs2Native: null, ...state })
  actions.onGlobalStateChange(changeState => {
    if (!changeState.microApplyFunction && !changeState.runClientJs2Native) {
      return actions
    }
    if (changeState.microApplyFunction) {
      const { fn, data } = changeState.microApplyFunction
      baseFunctions[fn](data)
    }
    if (changeState.runClientJs2Native) {
      const { fn, data } = changeState.runClientJs2Native
      js2nativeFunctions[fn](data)
    }
    actions.setGlobalState({ microApplyFunction: null, runClientJs2Native: null, ...state })
  })
  return actions
}

export { registerMicroApp, registerCommunication }

搞个路由,搞个空白页面

import { registerMicroApp } from '@/register-micro-app'
...
this.microApp = await registerMicroApp({
  name: 'micro-demo',
  {...}
})

子应用配置

qiankun先看官网:qiankun.umijs.org/zh/guide/ge… 配置MF请看:webpack.js.org/concepts/mo…

还是按照上面的文档配置,放心,肯定会出问题。

遇到问题先看:qiankun.umijs.org/zh/faq

我说一些重点关注配置:

子应用中webpack配置:

  1. libraryTarget 注意格式
  2. publicPath 可以设置成'auto'

publicPath 如果是单纯的子应用,设置成

    publicPath: isDev ? '/' : '/v/demo/',

如果是单纯的远程组件库需要设置成:

    publicPath: process.env.NODE_ENV === 'development' ? 'http://localhost:8779/' : '/v/demo/',

但是合到一起就很尴尬,webpack官网给了提示,试了一下,好像可以~,清楚原理的大佬评论区请指教一下~

image.png

...
  output: {
    // 出口文件
    path: process.cwd() + '/dist',
    publicPath: 'auto',
    filename: 'js/[name].[hash:8].js',
    clean: true,
    library: `micro-demo`,
    libraryTarget: 'umd',
    chunkLoadingGlobal: `webpackJsonp_${name}`,
  },
...

还没完,MF中的配置:

注意library 属性,一定要设置成'umd',之前忘记在哪看到的,为了解决一个忘了的报错,这里写成了'window',导致webpack中的external配置的Vue等,在运行时找不到。

// remote.entry.config.js
module.exports = {
  name: 'demo',
  filename: 'remote-entry-demo.js',
  library: { type: 'umd', name: 'demo' },
  // 远程应用暴露出的模块名
  exposes: {
    './config': './src/shared/config.js',
  },
  // import: 'vue', //false|string,
  // singleton: false,
  // // 是否开启单例模式。
  // // 默认不开启,当前模块的依赖版本与其他模块共享的依赖版本不一致时,分别加载各自的依赖;
  // //开启后,加载的依赖的版本为共享版本中较高的。(本地模块不开启,远程模块开启,只加载本地模块,远程模块即使版本更高,也不加载。)
  // version: '2.5.17', //指定共享依赖的版本
  // requiredVersion: '2.5.17', //指定当前模块需要的版本,默认值为当前应用的依赖版本
  // strictVersion: false, //是否需要严格的版本控制。如果开启,单例模式下,strictVersion与实际应用的依赖的版本不一致时,会抛出异常。
  // shareKey: 'vue', //共享依赖的别名, 默认值 shared 配置项的 key 值.
  // shareScope: 'default', //当前共享依赖的作用域名称,默认为 default
  // eager: false, //共享依赖在打包过程中是否被分离为单独文件,默认分离打包。如果为true,共享依赖会打包到入口文件,不会分离出来,失去了共享的意义。
  shared: {
  },
}

// config.js
...
  microApps: {
    'micro-demo': {
      name: 'micro-demo',
      entry: isDev
        ? '//localhost:9989'
        : `${window.location.protocol}//${window.location.host}/v/demo`,
      container: '#micro-demo',
      configuration: {
        sandbox: true,
      },
    },
  }
...

从上面可以看出,我把子应用的qiankun配置,子应用自己处理,当基座拉取config之后,解析microApps就能注册一个子应用,也就实现了最开始的目标。

问题

以上就是实现方案,但是我遇到了别的问题,在此请教各位,如果可以的话请评论区给我讲讲~

前提:我们有一个专门做通用业务组件的项目,只对外暴露远程组件。

  1. 子应用资源文件过大
  2. 子应用在基座内不能直接调用远程组件,需要基座传过来

资源过大这个问题,很头疼,因为MF的限制webpack中的splitChunks只能设置为false,导致会出现10+M以上的资源文件(部署之后小了一些)。

以上就是全部内容了,想到什么就写什么,也没什么章法。

酒浆~

欢迎关注我的公众号: 王大锤学前端