Repository: ice-lab/icestark (ts)

Issue:
activePath 配置为 ['path1', 'path2'] 无法正确渲染微应用
icestark 在全局存储微应用的 basename，以便微应用可以正确匹配路由。在 [activePath](https://micro-frontends.ice.work/docs/api/ice-stark#activepath) 为数组时，默认设置的 basename 为第一项。

可以做一些优化：

1. 对于非 function 方式的匹配规则，可以获取到具体匹配的项
2. 对于 function 方式的匹配，需要使用 [basename](https://micro-frontends.ice.work/docs/api/ice-stark#appconfig) 手动配置。
3. 对于下面的用法，给出 warning 提示：activePath 不能同时包含函数形式和对象形式

```js
activePath: [() => {}, 'seller']
```

其他：

+  文档补充
    + 新增章节「路由匹配」



Pull request description:
Release/2.7.2
- [x] set actual basename when `activePath` is an array, closes #526 
- [x] modify the isAssetExist function parameter element type to HTMLScriptElement | HTMLLinkElement, relates to https://github.com/ice-lab/icestark/pull/525

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8c3f45f..d912570 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@
 
 See [https://github.com/ice-lab/icestark/releases](https://github.com/ice-lab/icestark/releases) for what has changed in each version of icestark.
 
+## 2.7.2
+
+- [fix] set actual basename when `activePath` is an array. ([#526](https://github.com/ice-lab/icestark/issues/526))
+
 ## 2.7.1
 
 - [feat] improve DX a lot.
diff --git a/package.json b/package.json
index 400ec8f..7495442 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "icestark-monorepo",
-  "version": "2.7.1",
+  "version": "2.7.2",
   "private": true,
   "description": "Icestark is a JavaScript library for multiple projects, Ice workbench solution.",
   "scripts": {
diff --git a/packages/icestark/package.json b/packages/icestark/package.json
index d77ca28..a704f80 100644
--- a/packages/icestark/package.json
+++ b/packages/icestark/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@ice/stark",
-  "version": "2.7.1",
+  "version": "2.7.2",
   "description": "Icestark is a JavaScript library for multiple projects, Ice workbench solution.",
   "scripts": {
     "build": "rm -rf lib && tsc",
diff --git a/packages/icestark/src/AppRoute.tsx b/packages/icestark/src/AppRoute.tsx
index 6deeb4b..738123a 100644
--- a/packages/icestark/src/AppRoute.tsx
+++ b/packages/icestark/src/AppRoute.tsx
@@ -3,10 +3,11 @@ import renderComponent from './util/renderComponent';
 import { AppHistory } from './appHistory';
 import { unloadMicroApp, BaseConfig, createMicroApp } from './apps';
 import { converArray2String } from './util/helpers';
-import { PathData } from './util/checkActive';
 import { callCapturedEventListeners, resetCapturedEventListeners } from './util/capturedListeners';
 import isEqual from 'lodash.isequal';
 
+import type { PathData } from './util/checkActive';
+
 interface AppRouteState {
   showComponent: boolean;
 }
diff --git a/packages/icestark/src/AppRouter.tsx b/packages/icestark/src/AppRouter.tsx
index c11f096..908302b 100644
--- a/packages/icestark/src/AppRouter.tsx
+++ b/packages/icestark/src/AppRouter.tsx
@@ -7,7 +7,7 @@ import { ICESTSRK_ERROR, ICESTSRK_NOT_FOUND } from './util/constant';
 import start, { unload } from './start';
 import { AppConfig, MicroApp } from './apps';
 import { doPrefetch, Prefetch } from './util/prefetch';
-import checkActive, { AppRoutePath, formatPath } from './util/checkActive';
+import findActivePath, { AppRoutePath, formatPath } from './util/checkActive';
 import { converArray2String, isFunction, mergeFrameworkBaseToPath } from './util/helpers';
 import type { Fetch } from './util/globalConfiguration';
 
@@ -227,7 +227,7 @@ export default class AppRouter extends React.Component<AppRouterProps, AppRouter
 
         element = child;
 
-        match = checkActive(compatPath)(url);
+        match = !!findActivePath(compatPath)(url);
       }
     });
 
diff --git a/packages/icestark/src/apps.ts b/packages/icestark/src/apps.ts
index 99417d8..31122de 100644
--- a/packages/icestark/src/apps.ts
+++ b/packages/icestark/src/apps.ts
@@ -1,7 +1,7 @@
 import Sandbox, { SandboxConstructor, SandboxProps } from '@ice/sandbox';
 import isEmpty from 'lodash.isempty';
 import { NOT_LOADED, NOT_MOUNTED, LOADING_ASSETS, UNMOUNTED, LOAD_ERROR, MOUNTED } from './util/constant';
-import checkUrlActive, { ActivePath, PathOption, formatPath } from './util/checkActive';
+import findActivePathCurry, { ActivePath, PathOption, formatPath } from './util/checkActive';
 import {
   createSandbox,
   getUrlAssets,
@@ -20,6 +20,7 @@ import { ErrorCode, formatErrMessage } from './util/error';
 import globalConfiguration, { temporaryState } from './util/globalConfiguration';
 
 import type { StartConfiguration } from './util/globalConfiguration';
+import type { FindActivePathReturn } from './util/checkActive';
 
 export type ScriptAttributes = string[] | ((url: string) => string[]);
 
@@ -61,7 +62,10 @@ export interface BaseConfig extends PathOption {
    */
   umd?: boolean;
   loadScriptMode?: LoadScriptMode;
-  checkActive?: (url: string) => boolean;
+  /**
+   * @private will be prefixed with `_` for it is internal.
+   */
+  findActivePath?: FindActivePathReturn;
   appAssets?: Assets;
   props?: object;
   cached?: boolean;
@@ -131,13 +135,13 @@ export function registerMicroApp(appConfig: AppConfig, appLifecyle?: AppLifecylc
 
   const { basename: frameworkBasename } = globalConfiguration;
 
-  const checkActive = checkUrlActive(mergeFrameworkBaseToPath(activePathArray, frameworkBasename));
+  const findActivePath = findActivePathCurry(mergeFrameworkBaseToPath(activePathArray, frameworkBasename));
 
   const microApp = {
     status: NOT_LOADED,
     ...appConfig,
     appLifecycle: appLifecyle,
-    checkActive,
+    findActivePath,
   };
 
   microApps.push(microApp);
@@ -353,16 +357,20 @@ export async function createMicroApp(
     return null;
   }
 
-  const { container, basename, activePath, configuration: userConfiguration } = appConfig;
+  const { container, basename, activePath, configuration: userConfiguration, findActivePath } = appConfig;
 
   if (container) {
     setCache('root', container);
   }
 
-  const { basename: frameworkBasename, fetch } = userConfiguration;
+  const { fetch } = userConfiguration;
 
   if (shouldSetBasename(activePath, basename)) {
-    setCache('basename', getAppBasename(activePath, frameworkBasename, basename));
+    let pathString = findActivePath(window.location.href);
+
+    // When use `createMicroApp` lonely, `activePath` maybe not provided.
+    pathString = typeof pathString === 'string' ? pathString : '';
+    setCache('basename', getAppBasename(pathString, basename));
   }
 
   switch (appConfig.status) {
@@ -392,8 +400,10 @@ export async function createMicroApp(
 export async function mountMicroApp(appName: string) {
   const appConfig = getAppConfig(appName);
   // check current url before mount
-  if (appConfig && appConfig.checkActive(window.location.href) && appConfig.status !== MOUNTED) {
-    if (appConfig.mount) {
+  const shouldMount = appConfig?.mount && appConfig?.findActivePath(window.location.href);
+
+  if (shouldMount) {
+    if (appConfig?.mount) {
       await appConfig.mount({ container: appConfig.container, customProps: appConfig.props });
     }
     updateAppConfig(appName, { status: MOUNTED });
diff --git a/packages/icestark/src/start.ts b/packages/icestark/src/start.ts
index 4c4bf8d..b45abbc 100644
--- a/packages/icestark/src/start.ts
+++ b/packages/icestark/src/start.ts
@@ -57,7 +57,7 @@ export function reroute(url: string, type: RouteType | 'init' | 'popstate'| 'has
     const unmountApps = [];
     const activeApps = [];
     getMicroApps().forEach((microApp: AppConfig) => {
-      const shouldBeActive = microApp.checkActive(url);
+      const shouldBeActive = !!microApp.findActivePath(url);
       if (shouldBeActive) {
         activeApps.push(microApp);
       } else {
diff --git a/packages/icestark/src/util/checkActive.ts b/packages/icestark/src/util/checkActive.ts
index 71cce72..4ec60f6 100644
--- a/packages/icestark/src/util/checkActive.ts
+++ b/packages/icestark/src/util/checkActive.ts
@@ -81,7 +81,7 @@ export const formatPath = (activePath?: ActivePath, options: PathOption = {}): P
  * @param activePath
  * @returns
  */
-const checkActive = (activePath?: PathData[] | ActiveFn) => {
+const findActivePath = (activePath?: PathData[] | ActiveFn): (url?: string) => string | boolean => {
   // Always activate app when activePath is not specified.
   if (!activePath) {
     return () => true;
@@ -89,17 +89,24 @@ const checkActive = (activePath?: PathData[] | ActiveFn) => {
 
   // If pass fucntion to activePath, just returns
   if (isFunction(activePath)) {
-    return activePath;
+    return (url: string) => activePath(url);
   }
 
-  return (url: string) => activePath
-    .map((rule) => {
-      return (checkUrl: string) => matchPath(checkUrl, rule);
-    })
-    .some((functionalRule) => functionalRule(url));
+  return (url: string) => {
+    // Record matched index
+    let matchedPath;
+    const isActive = activePath.some((path) => {
+      matchedPath = path?.value;
+      return matchPath(url, path);
+    });
+
+    return isActive ? matchedPath : false;
+  };
 };
 
-export default checkActive;
+export type FindActivePathReturn = ReturnType<typeof findActivePath>;
+
+export default findActivePath;
 
 const HashPathDecoders = {
   hashbang: (path: string) => (path.charAt(0) === '!' ? path.substr(1) : path),
diff --git a/packages/icestark/src/util/handleAssets.ts b/packages/icestark/src/util/handleAssets.ts
index 90c1d84..41c9ec5 100644
--- a/packages/icestark/src/util/handleAssets.ts
+++ b/packages/icestark/src/util/handleAssets.ts
@@ -57,7 +57,7 @@ export interface ILifecycleProps {
   customProps?: object;
 }
 
-function isAssetExist(element: HTMLScriptElement | HTMLLIElement, type: 'script' | 'link') {
+function isAssetExist(element: HTMLScriptElement | HTMLLinkElement, type: 'script' | 'link') {
   const urlAlias = type === 'script' ? 'src' : 'href';
 
   return Array.from(document.getElementsByTagName(type))
diff --git a/packages/icestark/src/util/helpers.ts b/packages/icestark/src/util/helpers.ts
index 0fab6fd..e90138f 100644
--- a/packages/icestark/src/util/helpers.ts
+++ b/packages/icestark/src/util/helpers.ts
@@ -1,4 +1,4 @@
-import type { PathData, AppRoutePath, ActiveFn, ActivePath } from './checkActive';
+import type { PathData, AppRoutePath, ActiveFn, ActivePath, FindActivePathReturn } from './checkActive';
 
 export const isDev = process.env.NODE_ENV === 'development';
 
@@ -96,35 +96,20 @@ export function addLeadingSlash(path: string): string {
   return path.charAt(0) === '/' ? path : `/${path}`;
 }
 
-/**
-* It's difficult to dig out the actual path url, so comes the
-* following function.
- */
-export const getActualUrlFromPath = (path: AppRoutePath): string => {
-  if (Array.isArray(path)) {
-    return (typeof path[0] === 'string' ? path[0] : path[0].value);
-  }
-  if (isObject<PathData>(path)) {
-    return path.value;
-  }
-  return path;
-};
-
 /**
  * Get basename for micro apps to use handily.
  * A properly formatted basename has a leading slash, but not trailing slash.
  */
-export const getAppBasename = (path: AppRoutePath = '', frameworkBase?: string, appBase?: string): string => {
-  const actualPath = addLeadingSlash(getActualUrlFromPath(path));
+export const getAppBasename = (path = '', appBase?: string): string => {
+  const actualPath = addLeadingSlash(path);
 
-  const leadingSlashFrameworkBase = frameworkBase && addLeadingSlash(frameworkBase);
   const leadingSlashAppBase = appBase && addLeadingSlash(appBase);
 
   /**
   * It's preferable to use `??` bewteen leadingSlashAppBase and actualPath. But some
   * users already use the misunderstanding `basename=''`, which we have to keep things the way they are.
    */
-  return `${leadingSlashFrameworkBase}${leadingSlashAppBase || actualPath}`;
+  return `${leadingSlashAppBase || actualPath}`;
 };
 
 /**
diff --git a/website/docs/api/ice-stark.md b/website/docs/api/ice-stark.md
index 3fabed2..eba4816 100644
--- a/website/docs/api/ice-stark.md
+++ b/website/docs/api/ice-stark.md
@@ -74,7 +74,6 @@ interface AppConfig {
   basename?: string;
   umd?: boolean;
   loadScriptMode?: 'fetch' | 'script' | 'import';
-  checkActive?: (url: string) => boolean;
   appAssets?: Assets;
   props?: object;
   cached?: boolean;


Test patch:
diff --git a/packages/icestark/__tests__/AppRouter.spec.tsx b/packages/icestark/__tests__/AppRouter.spec.tsx
index 8d2bd46..89becb4 100644
--- a/packages/icestark/__tests__/AppRouter.spec.tsx
+++ b/packages/icestark/__tests__/AppRouter.spec.tsx
@@ -120,4 +120,30 @@ describe('AppRouter', () => {
 
     unmount();
   })
+
+
+  test('app-multi-paths', async () => {
+    (fetch as FetchMock).mockResponseOnce(umdSourceWithSetLibrary.toString());
+    const { container, unmount } = render(
+      <AppRouter>
+        <AppRoute
+          loadScriptMode="fetch"
+          name="seller"
+          path={["/seller", "/seller2"]}
+          title="小二"
+          url={[
+            '//icestark.com/index.js'
+          ]}
+        />
+      </AppRouter>
+    );
+    window.history.pushState({}, 'test', '/seller2');
+    expect(container.innerHTML).toContain('Loading')
+    expect(getCache('basename')).toBe('/seller2');
+
+    await delay(1000);
+    expect(container.innerHTML).toContain('商家平台')
+
+    unmount();
+  })
 })
\ No newline at end of file
diff --git a/packages/icestark/__tests__/checkActive.spec.tsx b/packages/icestark/__tests__/checkActive.spec.tsx
index 70b4809..f464df0 100644
--- a/packages/icestark/__tests__/checkActive.spec.tsx
+++ b/packages/icestark/__tests__/checkActive.spec.tsx
@@ -1,5 +1,5 @@
 import '@testing-library/jest-dom/extend-expect';
-import checkActive, { matchPath, getPathname, formatPath } from '../src/util/checkActive';
+import findActivePath, { matchPath, getPathname, formatPath } from '../src/util/checkActive';
 
 describe('checkActive', () => {
   test('matchPath - path options', () => {
@@ -54,44 +54,48 @@ describe('checkActive', () => {
 
   test('checkActive', () => {
     // empty activePath
-    let checkFnc = checkActive();
+    let checkFnc = findActivePath();
     expect(checkFnc('/test/123')).toBeTruthy();
 
     // type `string`
-    checkFnc = checkActive(formatPath('/test', {}));
-    expect(checkFnc('/test/123')).toBeTruthy();
+    checkFnc = findActivePath(formatPath('/test', {}));
+    expect(checkFnc('/test/123')).toEqual('/test');
 
-    checkFnc = checkActive(formatPath('/test', { exact: true }));
+    checkFnc = findActivePath(formatPath('/test', { exact: true }));
     expect(checkFnc('/test/123')).toBeFalsy();
 
     // type `string[]`
-    checkFnc = checkActive(formatPath(['/test', '/seller'], {}));
-    expect(checkFnc('/test/123')).toBeTruthy();
+    checkFnc = findActivePath(formatPath(['/test', '/seller'], {}));
+    expect(checkFnc('/test/123')).toEqual('/test');
 
-    checkFnc = checkActive(formatPath(['/test', '/seller'], { exact: true }));
+    checkFnc = findActivePath(formatPath(['/test', '/seller'], { exact: true }));
     expect(checkFnc('/test/123')).toBeFalsy();
 
     // type `PathData`
-    checkFnc = checkActive(formatPath({ value: '/test' }, {}));
-    expect(checkFnc('/test/123')).toBeTruthy();
+    checkFnc = findActivePath(formatPath({ value: '/test' }, {}));
+    expect(checkFnc('/test/123')).toEqual('/test');
 
-    checkFnc = checkActive(formatPath({ value: '/test', exact: true }, {}));
+    checkFnc = findActivePath(formatPath({ value: '/test', exact: true }, {}));
     expect(checkFnc('/test/123')).toBeFalsy();
 
     // type `PathData[]`
-    checkFnc = checkActive([{ value: '/test' }, { value: '/seller' }]);
-    expect(checkFnc('/test/123')).toBeTruthy();
+    checkFnc = findActivePath([{ value: '/test' }, { value: '/seller' }]);
+    expect(checkFnc('/test/123')).toEqual('/test');
 
     // type `MixedPathData`
-    checkFnc = checkActive(formatPath(['/test', { value: '/seller' }]));
-    expect(checkFnc('/test/123')).toBeTruthy();
+    checkFnc = findActivePath(formatPath(['/test', { value: '/seller' }]));
+    expect(checkFnc('/test/123')).toEqual('/test');
 
     // type `ActiveFn`
-    checkFnc = checkActive((url: string) => url.includes('/test'));
+    checkFnc = findActivePath((url: string) => url.includes('/test'));
     expect(checkFnc('/test/123')).toBeTruthy();
 
     // `undefined`
-    checkFnc = checkActive(formatPath());
+    checkFnc = findActivePath(formatPath());
     expect(checkFnc('/test/123')).toBeTruthy();
+
+    // matched idx
+    checkFnc = findActivePath(formatPath(['/test', '/seller'], {}));
+    expect(checkFnc('/seller')).toEqual('/seller');
   })
 });