最近要搞一些事,不能细说,大概就是一个基座需要动态拉取子应用,基座对子应用不用知道子应用具体是什么,按照基座标准注册就能加载子应用。子应用的注册、业务逻辑都自治。
方案
目前手里具备的技术:
- qiankun 微前端
- 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配置:
- libraryTarget 注意格式
- publicPath 可以设置成'auto'
publicPath 如果是单纯的子应用,设置成
publicPath: isDev ? '/' : '/v/demo/',
如果是单纯的远程组件库需要设置成:
publicPath: process.env.NODE_ENV === 'development' ? 'http://localhost:8779/' : '/v/demo/',
但是合到一起就很尴尬,webpack官网给了提示,试了一下,好像可以~,清楚原理的大佬评论区请指教一下~
...
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就能注册一个子应用,也就实现了最开始的目标。
问题
以上就是实现方案,但是我遇到了别的问题,在此请教各位,如果可以的话请评论区给我讲讲~
前提:我们有一个专门做通用业务组件的项目,只对外暴露远程组件。
- 子应用资源文件过大
- 子应用在基座内不能直接调用远程组件,需要基座传过来
资源过大这个问题,很头疼,因为MF的限制webpack中的splitChunks只能设置为false,导致会出现10+M以上的资源文件(部署之后小了一些)。
以上就是全部内容了,想到什么就写什么,也没什么章法。
酒浆~
欢迎关注我的公众号: 王大锤学前端