diff --git a/packages/fes-plugin-qiankun/package.json b/packages/fes-plugin-qiankun/package.json index 3b8b27dc..a97831e4 100644 --- a/packages/fes-plugin-qiankun/package.json +++ b/packages/fes-plugin-qiankun/package.json @@ -33,7 +33,7 @@ "qiankun": "2.3.4" }, "peerDependencies": { - "@webank/fes": "^2.0.0-alpha.0", + "@webank/fes": "^2.0.0-rc.0", "vue": "^3.0.5" } } diff --git a/packages/fes-plugin-qiankun/src/constants.js b/packages/fes-plugin-qiankun/src/constants.js new file mode 100644 index 00000000..51451582 --- /dev/null +++ b/packages/fes-plugin-qiankun/src/constants.js @@ -0,0 +1,2 @@ +export const defaultMainRootId = '#root-master'; +export const defaultHistoryType = 'hash'; diff --git a/packages/fes-plugin-qiankun/src/index.js b/packages/fes-plugin-qiankun/src/index.js index a7d24c4e..fa0a8a02 100644 --- a/packages/fes-plugin-qiankun/src/index.js +++ b/packages/fes-plugin-qiankun/src/index.js @@ -1,6 +1,6 @@ -import { join } from 'path'; +// import { join } from 'path'; -const namespace = 'plugin-qiankun'; +// const namespace = 'plugin-qiankun'; export default (api) => { api.describe({ @@ -15,12 +15,14 @@ export default (api) => { } }); + api.addRuntimePluginKey(() => 'qiankun'); + api.registerPlugins([ require.resolve('./main'), require.resolve('./mirco') ]); - const absRuntimeFilePath = join(namespace, 'runtime.js'); + // const absRuntimeFilePath = join(namespace, 'runtime.js'); - api.addRuntimePlugin(() => `@@/${absRuntimeFilePath}`); + // api.addRuntimePlugin(() => `@@/${absRuntimeFilePath}`); }; diff --git a/packages/fes-plugin-qiankun/src/main/index.js b/packages/fes-plugin-qiankun/src/main/index.js index 618a2b6e..c3964f04 100644 --- a/packages/fes-plugin-qiankun/src/main/index.js +++ b/packages/fes-plugin-qiankun/src/main/index.js @@ -1,141 +1,195 @@ -import { existsSync, readFileSync } from 'fs'; +import { readFileSync } from 'fs'; import { join } from 'path'; -import { - defaultHistoryMode, - defaultMainRootId, - testPathWithPrefix, - toArray -} from '../common'; +import { defaultMainRootId, defaultHistoryType } from '../constants'; -export default function (api, options) { - const { registerRuntimeKeyInIndex = false } = options || {}; +const namespace = 'plugin-qiankun/main'; - api.addRuntimePlugin(() => require.resolve('./runtime')); +export function isMasterEnable(api) { + return ( + !!api.userConfig?.qiankun?.main + || !!process.env.INITIAL_QIANKUN_MAIN_OPTIONS + ); +} - if (!registerRuntimeKeyInIndex) { - api.addRuntimePluginKey(() => 'qiankun'); - } +export default function (api) { + api.describe({ + enableBy: () => isMasterEnable(api) + }); api.modifyDefaultConfig(config => ({ ...config, - mountElementId: defaultMainRootId, - disableGlobalVariables: true + mountElementId: defaultMainRootId })); - // apps 可能在构建期为空 - const { apps = [] } = options || {}; - if (apps.length) { - // 获取一组路由中以 basePath 为前缀的路由 - const findRouteWithPrefix = (routes, basePath) => { - for (const route of routes) { - if (route.path && testPathWithPrefix(basePath, route.path)) { return route; } - - if (route.routes && route.routes.length) { - return findRouteWithPrefix(route.routes, basePath); - } - } - - return null; - }; - - const modifyAppRoutes = () => { - // TODO: fes缺少修改路由API - api.modifyRoutes((routes) => { - const { - config: { history: mainHistory = defaultHistoryMode } - } = api; - - const newRoutes = routes.map((route) => { - if (route.path === '/' && route.routes && route.routes.length) { - apps.forEach(({ history: slaveHistory = 'history', base }) => { - // 当子应用的 history mode 跟主应用一致时,为避免出现 404 手动为主应用创建一个 path 为 子应用 rule 的空 div 路由组件 - if (slaveHistory === mainHistory) { - const baseConfig = toArray(base); - - baseConfig.forEach((basePath) => { - const routeWithPrefix = findRouteWithPrefix(routes, basePath); - - // 应用没有自己配置过 basePath 相关路由,则自动加入 mock 的路由 - if (!routeWithPrefix) { - route.routes.unshift({ - path: basePath, - exact: false, - component: `() => { - if (process.env.NODE_ENV === 'development') { - console.log('${basePath} 404 mock rendered'); - } - - return React.createElement('div'); - }` - }); - } else { - // 若用户已配置过跟应用 base 重名的路由,则强制将该路由 exact 设置为 false,目的是兼容之前遗留的错误用法的场景 - routeWithPrefix.exact = false; - } - }); - } - }); - } - - return route; - }); - - return newRoutes; - }); - }; - - modifyAppRoutes(); - } - - const rootExportsFile = join(api.paths.absSrcPath, 'rootExports.js'); - - api.addTmpGenerateWatcherPaths(() => rootExportsFile); - - const namespace = 'plugin-qiankun'; - const absCoreFilePath = join(namespace, 'qiankunDefer.js'); + const absMicroAppPath = join(namespace, 'MicroApp.js'); + const absRuntimePath = join(namespace, 'runtime.js'); + const absMasterOptionsPath = join(namespace, 'masterOptions.js'); api.onGenerateFiles(() => { - const { - config: { history = defaultHistoryMode } - } = api; - const rootExports = `window.g_rootExports = ${existsSync(rootExportsFile) ? 'require(\'@/rootExports\')' : '{}'};`.trim(); - api.writeTmpFile({ - path: `${namespace}/qiankunRootExports.js`, - content: rootExports + path: absMicroAppPath, + content: readFileSync(join(__dirname, 'runtime/MicroApp.tpl'), 'utf-8') }); api.writeTmpFile({ - path: `${namespace}/subAppsConfig.json`, - content: JSON.stringify({ - mainHistory: history, - ...options - }) + path: absRuntimePath, + content: readFileSync(join(__dirname, 'runtime/runtime.tpl'), 'utf-8') }); + + const { main: options } = api.config?.qiankun || {}; + const masterHistoryType = api.config?.router?.mode || defaultHistoryType; + const base = api.config.base || '/'; api.writeTmpFile({ - path: `${namespace}/qiankunDefer.js`, + path: absMasterOptionsPath, content: ` - class Deferred { - constructor() { - this.promise = new Promise(resolve => this.resolve = resolve); - } - } - export const deferred = new Deferred(); - export const qiankunStart = deferred.resolve; - `.trim() - }); - - api.writeTmpFile({ - path: `${namespace}/runtime.js`, - content: readFileSync(join(__dirname, 'runtime.js'), 'utf-8') + let options = ${JSON.stringify({ + masterHistoryType, + base, + ...options + })}; + export const getMasterOptions = () => options; + export const setMasterOptions = (newOpts) => options = ({ ...options, ...newOpts }); + ` }); }); api.addPluginExports(() => [ { - specifiers: ['qiankunStart'], - source: absCoreFilePath + specifiers: ['MicroApp'], + source: absMicroAppPath } ]); + + + // const { registerRuntimeKeyInIndex = false } = options || {}; + + // api.addRuntimePlugin(() => require.resolve('./runtime')); + + // if (!registerRuntimeKeyInIndex) { + // api.addRuntimePluginKey(() => 'qiankun'); + // } + + // api.modifyDefaultConfig(config => ({ + // ...config, + // mountElementId: defaultMainRootId, + // disableGlobalVariables: true + // })); + + // // apps 可能在构建期为空 + // const { apps = [] } = options || {}; + // if (apps.length) { + // // 获取一组路由中以 basePath 为前缀的路由 + // const findRouteWithPrefix = (routes, basePath) => { + // for (const route of routes) { + // if (route.path && testPathWithPrefix(basePath, route.path)) { return route; } + + // if (route.routes && route.routes.length) { + // return findRouteWithPrefix(route.routes, basePath); + // } + // } + + // return null; + // }; + + // const modifyAppRoutes = () => { + // // TODO: fes缺少修改路由API + // api.modifyRoutes((routes) => { + // const { + // config: { history: mainHistory = defaultHistoryMode } + // } = api; + + // const newRoutes = routes.map((route) => { + // if (route.path === '/' && route.routes && route.routes.length) { + // apps.forEach(({ history: slaveHistory = 'history', base }) => { + // // 当子应用的 history mode 跟主应用一致时,为避免出现 404 手动为主应用创建一个 path 为 子应用 rule 的空 div 路由组件 + // if (slaveHistory === mainHistory) { + // const baseConfig = toArray(base); + + // baseConfig.forEach((basePath) => { + // const routeWithPrefix = findRouteWithPrefix(routes, basePath); + + // // 应用没有自己配置过 basePath 相关路由,则自动加入 mock 的路由 + // if (!routeWithPrefix) { + // route.routes.unshift({ + // path: basePath, + // exact: false, + // component: `() => { + // if (process.env.NODE_ENV === 'development') { + // console.log('${basePath} 404 mock rendered'); + // } + + // return React.createElement('div'); + // }` + // }); + // } else { + // // 若用户已配置过跟应用 base 重名的路由,则强制将该路由 exact 设置为 false,目的是兼容之前遗留的错误用法的场景 + // routeWithPrefix.exact = false; + // } + // }); + // } + // }); + // } + + // return route; + // }); + + // return newRoutes; + // }); + // }; + + // modifyAppRoutes(); + // } + + // const rootExportsFile = join(api.paths.absSrcPath, 'rootExports.js'); + + // api.addTmpGenerateWatcherPaths(() => rootExportsFile); + + // const namespace = 'plugin-qiankun'; + // const absCoreFilePath = join(namespace, 'qiankunDefer.js'); + + // api.onGenerateFiles(() => { + // const { + // config: { history = defaultHistoryMode } + // } = api; + // const rootExports = `window.g_rootExports = ${existsSync(rootExportsFile) ? 'require(\'@/rootExports\')' : '{}'};`.trim(); + + // api.writeTmpFile({ + // path: `${namespace}/qiankunRootExports.js`, + // content: rootExports + // }); + + // api.writeTmpFile({ + // path: `${namespace}/subAppsConfig.json`, + // content: JSON.stringify({ + // mainHistory: history, + // ...options + // }) + // }); + + // api.writeTmpFile({ + // path: `${namespace}/qiankunDefer.js`, + // content: ` + // class Deferred { + // constructor() { + // this.promise = new Promise(resolve => this.resolve = resolve); + // } + // } + // export const deferred = new Deferred(); + // export const qiankunStart = deferred.resolve; + // `.trim() + // }); + + // api.writeTmpFile({ + // path: `${namespace}/runtime.js`, + // content: readFileSync(join(__dirname, 'runtime.js'), 'utf-8') + // }); + // }); + + // api.addPluginExports(() => [ + // { + // specifiers: ['qiankunStart'], + // source: absCoreFilePath + // } + // ]); } diff --git a/packages/fes-plugin-qiankun/src/main/runtime.js b/packages/fes-plugin-qiankun/src/main/runtime.js deleted file mode 100644 index 2a8ba764..00000000 --- a/packages/fes-plugin-qiankun/src/main/runtime.js +++ /dev/null @@ -1,85 +0,0 @@ -import { deferred } from '@@/plugin-qiankun/qiankunDefer.js'; -import '@@/plugin-qiankun/qiankunRootExports.js'; -import subAppConfig from '@@/plugin-qiankun/subAppsConfig.json'; -import { registerMicroApps, start } from 'qiankun'; -import { createApp, h } from 'vue'; -import { plugin, ApplyPluginsType } from '@@/core/coreExports'; -import { defaultMountContainerId, testPathWithPrefix, toArray } from '../common'; - -async function getMasterRuntime() { - const config = plugin.applyPlugins({ - key: 'qiankun', - type: ApplyPluginsType.modify, - initialValue: {}, - async: true - }); - const { master } = config; - return master || config; -} - -export async function render(oldRender) { - oldRender(); - function isAppActive(location, history, base) { - const baseConfig = toArray(base); - switch (history.type || history) { - case 'hash': - return baseConfig.some(pathPrefix => testPathWithPrefix(`#${pathPrefix}`, location.hash)); - case 'browser': - return baseConfig.some(pathPrefix => testPathWithPrefix(pathPrefix, location.pathname)); - default: - return false; - } - } - const runtimeConfig = await getMasterRuntime(); - const { - apps, jsSandbox = false, prefetch = true, defer = false, lifeCycles, masterHistory, ...otherConfigs - } = { - ...subAppConfig, - ...runtimeConfig - }; - - assert(apps && apps.length, 'sub apps must be config when using fes-plugin-qiankun'); - - registerMicroApps(apps.map(({ - name, entry, base, history = masterHistory, mountElementId = defaultMountContainerId, props - }) => ({ - name, - entry, - activeRule: location => isAppActive(location, history, base), - render: ({ appContent, loading }) => { - if (process.env.NODE_ENV === 'development') { - console.info(`app ${name} loading ${loading}`); - } - if (mountElementId) { - const container = document.getElementById(mountElementId); - if (container) { - const subApp = { - setup() { - - }, - render() { - h('div', { - dangerouslySetInnerHTML: { - __html: appContent - } - }); - } - }; - const app = createApp(); - app.mount(subApp, container); - } - } - }, - props: { - base, - history, - ...props - } - })), lifeCycles); - - if (defer) { - await deferred.promise; - } - - start({ jsSandbox, prefetch, ...otherConfigs }); -} diff --git a/packages/fes-plugin-qiankun/src/main/runtime/MicroApp.tpl b/packages/fes-plugin-qiankun/src/main/runtime/MicroApp.tpl new file mode 100644 index 00000000..3c275925 --- /dev/null +++ b/packages/fes-plugin-qiankun/src/main/runtime/MicroApp.tpl @@ -0,0 +1,123 @@ +import { + defineComponent, + ref, + watch, + computed, + onBeforeUnmount, + onMounted +} from 'vue'; +import { loadMicroApp } from 'qiankun'; +// eslint-disable-next-line import/extensions +import { getMasterOptions } from './masterOptions'; + +function unmountMicroApp(microApp) { + if (microApp) { + microApp.mountPromise.then(() => microApp.unmount()); + } +} + +export const MicroApp = defineComponent({ + props: { + name: { + type: String, + required: true + } + }, + setup(props) { + const { + masterHistoryType, + apps = [], + lifeCycles: globalLifeCycles, + ...globalSettings + } = getMasterOptions(); + + const containerRef = ref(null); + const microAppRef = ref(); + const updatingPromise = ref(); + const updatingTimestamp = ref(Date.now()); + + const appConfigRef = computed(() => { + const _appConfig = apps.find(app => app.name === props.name); + if (!_appConfig) { + throw new Error( + `[@fesjs/plugin-qiankun]: Can not find the configuration of ${props.name} app!` + ); + } + return _appConfig; + }); + + const loadApp = () => { + const appConfig = appConfigRef.value; + // 加载新的 + microAppRef.value = loadMicroApp( + { + name: appConfig.name, + entry: appConfig.entry, + container: containerRef.value, + props: { ...appConfig.props } + }, + { + ...globalSettings, + ...(props.settings || {}), + globalLifeCycles, + lifeCycles: props.lifeCycles + } + ); + }; + + onMounted(() => { + loadApp(); + }); + + onBeforeUnmount(() => { + unmountMicroApp(microAppRef.value); + }); + + watch(appConfigRef, () => { + unmountMicroApp(microAppRef.value); + + loadApp(); + }); + + watch(microAppRef, () => { + const microApp = microAppRef.value; + const appConfig = appConfigRef.value; + if (microApp) { + if (!updatingPromise.value) { + // 初始化 updatingPromise 为 microApp.mountPromise,从而确保后续更新是在应用 mount 完成之后 + updatingPromise.value = microApp.mountPromise; + } else { + // 确保 microApp.update 调用是跟组件状态变更顺序一致的,且后一个微应用更新必须等待前一个更新完成 + updatingPromise.value = updatingPromise.value.then(() => { + const canUpdate = app => app?.update && app.getStatus() === 'MOUNTED'; + if (canUpdate(microApp)) { + if (process.env.NODE_ENV === 'development') { + if ( + Date.now() - updatingTimestamp.value + < 200 + ) { + console.warn( + `[@fesjs/plugin-qiankun] It seems like microApp ${props.name} is updating too many times in a short time(200ms), you may need to do some optimization to avoid the unnecessary re-rendering.` + ); + } + + console.info( + `[@umijs/plugin-qiankun] MicroApp ${props.name} is updating with props: `, + props + ); + updatingTimestamp.value = Date.now(); + } + + // 返回 microApp.update 形成链式调用 + return microApp.update({ + ...appConfig.props + }); + } + }); + } + } + }); + + return () =>
; + } +}); diff --git a/packages/fes-plugin-qiankun/src/main/runtime/runtime.tpl b/packages/fes-plugin-qiankun/src/main/runtime/runtime.tpl new file mode 100644 index 00000000..0beb6f37 --- /dev/null +++ b/packages/fes-plugin-qiankun/src/main/runtime/runtime.tpl @@ -0,0 +1,85 @@ +// import { deferred } from '@@/plugin-qiankun/qiankunDefer.js'; +// import '@@/plugin-qiankun/qiankunRootExports.js'; +// import subAppConfig from '@@/plugin-qiankun/subAppsConfig.json'; +// import { registerMicroApps, start } from 'qiankun'; +// import { createApp, h } from 'vue'; +// import { plugin, ApplyPluginsType } from '@@/core/coreExports'; +// import { defaultMountContainerId, testPathWithPrefix, toArray } from '../common'; + +// async function getMasterRuntime() { +// const config = plugin.applyPlugins({ +// key: 'qiankun', +// type: ApplyPluginsType.modify, +// initialValue: {}, +// async: true +// }); +// const { master } = config; +// return master || config; +// } + +// export async function render(oldRender) { +// oldRender(); +// function isAppActive(location, history, base) { +// const baseConfig = toArray(base); +// switch (history.type || history) { +// case 'hash': +// return baseConfig.some(pathPrefix => testPathWithPrefix(`#${pathPrefix}`, location.hash)); +// case 'browser': +// return baseConfig.some(pathPrefix => testPathWithPrefix(pathPrefix, location.pathname)); +// default: +// return false; +// } +// } +// const runtimeConfig = await getMasterRuntime(); +// const { +// apps, jsSandbox = false, prefetch = true, defer = false, lifeCycles, masterHistory, ...otherConfigs +// } = { +// ...subAppConfig, +// ...runtimeConfig +// }; + +// assert(apps && apps.length, 'sub apps must be config when using fes-plugin-qiankun'); + +// registerMicroApps(apps.map(({ +// name, entry, base, history = masterHistory, mountElementId = defaultMountContainerId, props +// }) => ({ +// name, +// entry, +// activeRule: location => isAppActive(location, history, base), +// render: ({ appContent, loading }) => { +// if (process.env.NODE_ENV === 'development') { +// console.info(`app ${name} loading ${loading}`); +// } +// if (mountElementId) { +// const container = document.getElementById(mountElementId); +// if (container) { +// const subApp = { +// setup() { + +// }, +// render() { +// h('div', { +// dangerouslySetInnerHTML: { +// __html: appContent +// } +// }); +// } +// }; +// const app = createApp(); +// app.mount(subApp, container); +// } +// } +// }, +// props: { +// base, +// history, +// ...props +// } +// })), lifeCycles); + +// if (defer) { +// await deferred.promise; +// } + +// start({ jsSandbox, prefetch, ...otherConfigs }); +// } diff --git a/packages/fes-plugin-qiankun/src/mirco/index.js b/packages/fes-plugin-qiankun/src/mirco/index.js index 6c81e1d8..3a7d3bba 100644 --- a/packages/fes-plugin-qiankun/src/mirco/index.js +++ b/packages/fes-plugin-qiankun/src/mirco/index.js @@ -1,103 +1,187 @@ +import assert from 'assert'; +import { lodash } from '@umijs/utils'; import { readFileSync } from 'fs'; import { join } from 'path'; -import { defaultMircoRootId } from '../common'; +// import { defaultMircoRootId } from '../common'; -export default function (api, options) { - const { registerRuntimeKeyInIndex = false } = options || {}; +const namespace = 'plugin-qiankun/mirco'; - api.addRuntimePlugin(() => require.resolve('./runtime')); +export function isSlaveEnable(api) { + return ( + !!api.userConfig?.qiankun?.mirco + || lodash.isEqual(api.userConfig?.qiankun, {}) + || !!process.env.INITIAL_QIANKUN_MIRCO_OPTIONS + ); +} - if (!registerRuntimeKeyInIndex) { - api.addRuntimePluginKey(() => 'qiankun'); - } +export default function (api) { + api.describe({ + enableBy: () => isSlaveEnable(api) + }); - const lifecyclePath = require.resolve('./lifecycles'); - const { name: pkgName } = require(join(api.cwd, 'package.json')); - - // TODO: fes缺少修改默认配置API - api.modifyDefaultConfig(memo => (Object.assign(Object.assign({}, memo), { - disableGlobalVariables: true, - base: `/${pkgName}`, - mountElementId: defaultMircoRootId, - // 默认开启 runtimePublicPath,避免出现 dynamic import 场景子应用资源地址出问题 + api.modifyDefaultConfig(memo => ({ + ...memo, runtimePublicPath: true - }))); + })); - if (api.service.userConfig.runtimePublicPath !== false) { - // TODO: fes缺少修改 publicPath API - api.modifyPublicPathStr(() => `window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ || "${ - // 开发阶段 publicPath 配置无效,默认为 / - process.env.NODE_ENV !== 'development' - ? api.config.publicPath || '/' - : '/'}"`); - } + api.modifyPublicPathStr((publicPathStr) => { + const { runtimePublicPath } = api.config; + const qiankunConfig = api.config.qiankun || {}; + if (!qiankunConfig || !qiankunConfig.mirco) { + return publicPathStr; + } + const { shouldNotModifyRuntimePublicPath } = qiankunConfig; + + if (runtimePublicPath === true && !shouldNotModifyRuntimePublicPath) { + return `window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ || "${ + api.config.publicPath || '/' + }"`; + } + + return publicPathStr; + }); api.chainWebpack((config) => { assert(api.pkg.name, 'You should have name in package.json'); - config.output - .libraryTarget('umd') - .library(`${api.pkg.name}-[name]`); + config.output.libraryTarget('umd').library(`${api.pkg.name}-[name]`); + return config; }); - // bundle 添加 entry 标记 - // TODO: fes缺少修改HTML API - api.modifyHTML(($) => { - $('script').each((_, el) => { - const scriptEl = $(el); - const umiEntryJs = /\/?umi(\.\w+)?\.js$/g; - const scriptElSrc = scriptEl.attr('src'); - - if (umiEntryJs.test((scriptElSrc) !== null && scriptElSrc !== 0 ? scriptElSrc : '')) { - scriptEl.attr('entry', ''); - } - }); - return $; - }); - - const namespace = 'plugin-qiankun'; + const absRuntimePath = join(namespace, 'runtime.js'); + const absLifeclesPath = join(namespace, 'lifecycles.js'); api.onGenerateFiles(() => { api.writeTmpFile({ - path: `${namespace}/qiankunContext.js`, - content: ` - import { createApp, h } from 'vue'; - export const Context = createContext(null); - export function useRootExports() { - return useContext(Context); - }; - `.trim() + path: absRuntimePath, + content: readFileSync(join(__dirname, 'runtime/runtime.tpl'), 'utf-8') }); api.writeTmpFile({ - path: `${namespace}/runtime.js`, - content: readFileSync(join(__dirname, 'runtime.js'), 'utf-8') - }); - - api.writeTmpFile({ - path: `${namespace}/lifecycles.js`, - content: readFileSync(join(__dirname, 'lifecycles.js'), 'utf-8') + path: absLifeclesPath, + content: readFileSync(join(__dirname, 'runtime/lifecycles.tpl'), 'utf-8') }); }); - api.addPluginExports(() => [ - { - specifiers: ['useRootExports'], - source: `${namespace}/qiankunContext.js` - } - ]); - api.addEntryImports(() => ({ - source: lifecyclePath, - specifier: '{ genMount as qiankun_genMount, genBootstrap as qiankun_genBootstrap, genUnmount as qiankun_genUnmount }' + source: `@@/${absLifeclesPath}`, + specifier: + '{ genMount as qiankun_genMount, genBootstrap as qiankun_genBootstrap, genUnmount as qiankun_genUnmount, genUpdate as qiankun_genUpdate }' })); - api.addEntryCode(() => ` - export const bootstrap = qiankun_genBootstrap(Promise.resolve(), clientRender); - export const mount = qiankun_genMount(); - export const unmount = qiankun_genUnmount('${api.config.mountElementId}'); + api.addEntryCode( + () => ` +export const bootstrap = qiankun_genBootstrap(completeClientRender, app); +export const mount = qiankun_genMount('${api.config.mountElementId}'); +export const unmount = qiankun_genUnmount('${api.config.mountElementId}'); +export const update = qiankun_genUpdate(); - if (!window.__POWERED_BY_QIANKUN__) { - bootstrap().then(mount); - } - `); +if (!window.__POWERED_BY_QIANKUN__) { + bootstrap().then(mount); +} +` + ); + + // const { registerRuntimeKeyInIndex = false } = options || {}; + + // api.addRuntimePlugin(() => require.resolve('./runtime')); + + // // if (!registerRuntimeKeyInIndex) { + // // api.addRuntimePluginKey(() => 'qiankun'); + // // } + + // const lifecyclePath = require.resolve('./lifecycles'); + // const { name: pkgName } = require(join(api.cwd, 'package.json')); + + // // TODO: fes缺少修改默认配置API + // api.modifyDefaultConfig(memo => Object.assign(Object.assign({}, memo), { + // disableGlobalVariables: true, + // base: `/${pkgName}`, + // mountElementId: defaultMircoRootId, + // // 默认开启 runtimePublicPath,避免出现 dynamic import 场景子应用资源地址出问题 + // runtimePublicPath: true + // })); + + // if (api.service.userConfig.runtimePublicPath !== false) { + // // TODO: fes缺少修改 publicPath API + // api.modifyPublicPathStr( + // () => `window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ || "${ + // // 开发阶段 publicPath 配置无效,默认为 / + // process.env.NODE_ENV !== 'development' + // ? api.config.publicPath || '/' + // : '/' + // }"` + // ); + // } + + // api.chainWebpack((config) => { + // assert(api.pkg.name, 'You should have name in package.json'); + // config.output.libraryTarget('umd').library(`${api.pkg.name}-[name]`); + // }); + + // // bundle 添加 entry 标记 + // // TODO: fes缺少修改HTML API + // api.modifyHTML(($) => { + // $('script').each((_, el) => { + // const scriptEl = $(el); + // const umiEntryJs = /\/?umi(\.\w+)?\.js$/g; + // const scriptElSrc = scriptEl.attr('src'); + + // if ( + // umiEntryJs.test( + // scriptElSrc !== null && scriptElSrc !== 0 ? scriptElSrc : '' + // ) + // ) { + // scriptEl.attr('entry', ''); + // } + // }); + // return $; + // }); + + // api.onGenerateFiles(() => { + // api.writeTmpFile({ + // path: `${namespace}/qiankunContext.js`, + // content: ` + // import { createApp, h } from 'vue'; + // export const Context = createContext(null); + // export function useRootExports() { + // return useContext(Context); + // }; + // `.trim() + // }); + + // api.writeTmpFile({ + // path: `${namespace}/runtime.js`, + // content: readFileSync(join(__dirname, 'runtime.js'), 'utf-8') + // }); + + // api.writeTmpFile({ + // path: `${namespace}/lifecycles.js`, + // content: readFileSync(join(__dirname, 'lifecycles.js'), 'utf-8') + // }); + // }); + + // api.addPluginExports(() => [ + // { + // specifiers: ['useRootExports'], + // source: `${namespace}/qiankunContext.js` + // } + // ]); + + // api.addEntryImports(() => ({ + // source: lifecyclePath, + // specifier: + // '{ genMount as qiankun_genMount, genBootstrap as qiankun_genBootstrap, genUnmount as qiankun_genUnmount }' + // })); + + // api.addEntryCode( + // () => ` + // export const bootstrap = qiankun_genBootstrap(Promise.resolve(), clientRender); + // export const mount = qiankun_genMount(); + // export const unmount = qiankun_genUnmount('${api.config.mountElementId}'); + + // if (!window.__POWERED_BY_QIANKUN__) { + // bootstrap().then(mount); + // } + // ` + // ); } diff --git a/packages/fes-plugin-qiankun/src/mirco/lifecycles.js b/packages/fes-plugin-qiankun/src/mirco/lifecycles.js deleted file mode 100644 index fd160b73..00000000 --- a/packages/fes-plugin-qiankun/src/mirco/lifecycles.js +++ /dev/null @@ -1,60 +0,0 @@ -import { plugin, ApplyPluginsType } from '@@/core/coreExports'; - -const defer = {}; -defer.promise = new Promise((resolve) => { - defer.resolve = resolve; -}); - -let render = () => { }; -let hasMountedAtLeastOnce = false; - -export default () => defer.promise; - -function getSlaveRuntime() { - const config = plugin.applyPlugins({ - key: 'qiankun', - type: ApplyPluginsType.modify, - initialValue: {} - }); - const { slave } = config; - return slave || config; -} - -// 子应用生命周期钩子Bootstrap -export function genBootstrap(promise, oldRender) { - return async (...args) => { - const slaveRuntime = getSlaveRuntime(); - if (slaveRuntime.bootstrap) { await slaveRuntime.bootstrap(...args); } - render = () => promise.then(oldRender).catch((e) => { - if (process.env.NODE_ENV === 'development') { - console.error('Render failed', e); - } - }); - }; -} - -// 子应用生命周期钩子Mount -export function genMount() { - return async (...args) => { - defer.resolve(); - const slaveRuntime = getSlaveRuntime(); - if (slaveRuntime.mount) { await slaveRuntime.mount(...args); } - // 第一次 mount 会自动触发 render,非第一次 mount 则需手动触发 - if (hasMountedAtLeastOnce) { - render(); - } - hasMountedAtLeastOnce = true; - }; -} - -// 子应用生命周期钩子Unmount -export function genUnmount(mountElementId, app) { - return async (...args) => { - const container = document.getElementById(mountElementId); - if (container) { - app.unmount(container); - } - const slaveRuntime = getSlaveRuntime(); - if (slaveRuntime.unmount) { await slaveRuntime.unmount(...args); } - }; -} diff --git a/packages/fes-plugin-qiankun/src/mirco/runtime.js b/packages/fes-plugin-qiankun/src/mirco/runtime.js deleted file mode 100644 index 96180844..00000000 --- a/packages/fes-plugin-qiankun/src/mirco/runtime.js +++ /dev/null @@ -1,12 +0,0 @@ -import { h } from 'vue'; -import qiankunRender from './lifecycles'; - -export function rootContainer(container) { - const value = window.g_rootExports; - const { Context } = require('@@/plugin-qiankun/qiankunContext'); - return h(Context.Provider, { value }, container); -} - -export const render = oldRender => qiankunRender().then(() => { - oldRender(); -}); diff --git a/packages/fes-plugin-qiankun/src/mirco/runtime/lifecycles.tpl b/packages/fes-plugin-qiankun/src/mirco/runtime/lifecycles.tpl new file mode 100644 index 00000000..a74b0d88 --- /dev/null +++ b/packages/fes-plugin-qiankun/src/mirco/runtime/lifecycles.tpl @@ -0,0 +1,92 @@ +import { plugin, ApplyPluginsType } from '@@/core/coreExports'; + +const defer = {}; +defer.promise = new Promise((resolve) => { + defer.resolve = resolve; +}); + +function isPromise(obj) { + return !!obj // 有实际含义的变量才执行方法,变量null,undefined和''空串都为false + && (typeof obj === 'object' || typeof obj === 'function') // 初始promise 或 promise.then返回的 + && typeof obj.then === 'function'; +} + + +let render = () => {}; +let app = null; +let hasMountedAtLeastOnce = false; + +export default () => defer.promise; + +function getSlaveRuntime() { + const config = plugin.applyPlugins({ + key: 'qiankun', + type: ApplyPluginsType.modify, + initialValue: {} + }); + const { slave } = config; + return slave || config; +} + +// 子应用生命周期钩子Bootstrap +export function genBootstrap(oldRender, appPromise) { + return async (props) => { + const slaveRuntime = getSlaveRuntime(); + if (slaveRuntime.bootstrap) { + await slaveRuntime.bootstrap(props); + } + render = oldRender; + if (isPromise(appPromise)) { + app = await appPromise; + } + }; +} + +// 子应用生命周期钩子Mount +export function genMount() { + return async (props) => { + // props 有值时说明应用是通过 lifecycle 被主应用唤醒的,而不是独立运行时自己 mount + if (typeof props !== 'undefined') { + const slaveRuntime = getSlaveRuntime(); + if (slaveRuntime.mount) { + await slaveRuntime.mount(props); + } + } + + // 第一次 mount 会自动触发 render,非第一次 mount 则需手动触发 + if (hasMountedAtLeastOnce) { + const appPromise = render(); + if (isPromise(appPromise)) { + app = await appPromise; + } + } else { + defer.resolve(); + } + hasMountedAtLeastOnce = true; + }; +} + +export function genUpdate() { + return async (props) => { + const slaveRuntime = await getSlaveRuntime(); + if (slaveRuntime.update) { + await slaveRuntime.update(props); + } + }; +} + +// 子应用生命周期钩子Unmount +export function genUnmount(mountElementId) { + return async (props) => { + const container = props?.container + ? props.container.querySelector(`#${mountElementId}`) + : document.getElementById(mountElementId); + if (container && app) { + app.unmount(container); + } + const slaveRuntime = getSlaveRuntime(); + if (slaveRuntime.unmount) { + await slaveRuntime.unmount(props); + } + }; +} diff --git a/packages/fes-plugin-qiankun/src/mirco/runtime/runtime.tpl b/packages/fes-plugin-qiankun/src/mirco/runtime/runtime.tpl new file mode 100644 index 00000000..3d6cd99c --- /dev/null +++ b/packages/fes-plugin-qiankun/src/mirco/runtime/runtime.tpl @@ -0,0 +1,12 @@ +// import { h } from 'vue'; +// import qiankunRender from './lifecycles'; + +// export function rootContainer(container) { +// const value = typeof window !== 'undefined' ? window.g_rootExports : {}; +// const { Context } = require('@@/plugin-qiankun/qiankunContext'); +// return h(Context.Provider, { value }, container); +// } + +// export const render = oldRender => qiankunRender().then(() => { +// oldRender(); +// }); diff --git a/packages/fes-preset-built-in/src/plugins/commands/buildDevUtils.js b/packages/fes-preset-built-in/src/plugins/commands/buildDevUtils.js index 1d7af459..b2b86505 100644 --- a/packages/fes-preset-built-in/src/plugins/commands/buildDevUtils.js +++ b/packages/fes-preset-built-in/src/plugins/commands/buildDevUtils.js @@ -55,7 +55,16 @@ export async function getBundleAndConfigs({ type: api.ApplyPluginsType.add, initialState: [] }); - } + }, + publicPath: await api.applyPlugins({ + key: 'modifyPublicPathStr', + type: api.ApplyPluginsType.modify, + initialValue: api.config.publicPath || '', + args: { + // route: args.route + } + }) + }, args: { } diff --git a/packages/fes-preset-built-in/src/plugins/commands/webpackConfig/index.js b/packages/fes-preset-built-in/src/plugins/commands/webpackConfig/index.js index dd79b2b2..c3227ada 100644 --- a/packages/fes-preset-built-in/src/plugins/commands/webpackConfig/index.js +++ b/packages/fes-preset-built-in/src/plugins/commands/webpackConfig/index.js @@ -66,7 +66,8 @@ export default async function getConfig({ modifyBabelOpts, modifyBabelPresetOpts, chainWebpack, - headScripts + headScripts, + publicPath }) { const isDev = env === 'development'; const isProd = env === 'production'; @@ -93,7 +94,7 @@ export default async function getConfig({ // --------------- output ----------- webpackConfig.output .path(absoluteOutput) - .publicPath(config.publicPath || '') + .publicPath(publicPath) .filename('[name].[contenthash:8].js') .chunkFilename('[name].[contenthash:8].chunk.js'); diff --git a/packages/fes-preset-built-in/src/plugins/generateFiles/fes/fes.tpl b/packages/fes-preset-built-in/src/plugins/generateFiles/fes/fes.tpl index 0ae0e18c..8c809211 100644 --- a/packages/fes-preset-built-in/src/plugins/generateFiles/fes/fes.tpl +++ b/packages/fes-preset-built-in/src/plugins/generateFiles/fes/fes.tpl @@ -62,18 +62,15 @@ const getClientRender = (args = {}) => plugin.applyPlugins({ args, }); - - -const beforeRenderConfig = plugin.applyPlugins({ - key: "beforeRender", - type: ApplyPluginsType.modify, - initialValue: { - loading: null, - action: null - }, -}); - const beforeRender = async () => { + const beforeRenderConfig = plugin.applyPlugins({ + key: "beforeRender", + type: ApplyPluginsType.modify, + initialValue: { + loading: null, + action: null + }, + }); let initialState = {}; if (typeof beforeRenderConfig.action === "function") { const app = createApp(beforeRenderConfig.loading); @@ -89,13 +86,16 @@ const beforeRender = async () => { return initialState; }; -const render = async () => { +const completeClientRender = async () => { const initialState = await beforeRender(); const clientRender = getClientRender({initialState}); - clientRender(); + const app = clientRender(); + return app; }; -render(); +const app = completeClientRender(); + +export default app; {{{ entryCode }}} diff --git a/packages/fes-preset-built-in/src/plugins/registerMethods.js b/packages/fes-preset-built-in/src/plugins/registerMethods.js index 4748a62e..0edfb52b 100644 --- a/packages/fes-preset-built-in/src/plugins/registerMethods.js +++ b/packages/fes-preset-built-in/src/plugins/registerMethods.js @@ -29,8 +29,7 @@ export default function (api) { 'modifyBabelPresetOpts', 'chainWebpack', 'addTmpGenerateWatcherPaths', - 'modifyPublicPathStr', - 'modifyHTML', + 'modifyPublicPathStr' ].forEach((name) => { api.registerMethod({ name }); });