feat: plugin-qiankun完成子应用挂载逻辑、主应用提供<MicroApp />加载子应用

This commit is contained in:
万纯 2021-03-22 16:23:02 +08:00
parent 4484bbd8fa
commit 170877dcb9
16 changed files with 677 additions and 371 deletions

View File

@ -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"
}
}

View File

@ -0,0 +1,2 @@
export const defaultMainRootId = '#root-master';
export const defaultHistoryType = 'hash';

View File

@ -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}`);
};

View File

@ -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
// }
// ]);
}

View File

@ -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 });
}

View File

@ -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 () => <div ref={containerRef} className={props.className}></div>;
}
});

View File

@ -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 });
// }

View File

@ -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);
// }
// `
// );
}

View File

@ -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); }
};
}

View File

@ -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();
});

View File

@ -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 // 有实际含义的变量才执行方法变量nullundefined和''空串都为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);
}
};
}

View File

@ -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();
// });

View File

@ -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: {
}

View File

@ -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');

View File

@ -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 }}}

View File

@ -29,8 +29,7 @@ export default function (api) {
'modifyBabelPresetOpts',
'chainWebpack',
'addTmpGenerateWatcherPaths',
'modifyPublicPathStr',
'modifyHTML',
'modifyPublicPathStr'
].forEach((name) => {
api.registerMethod({ name });
});