Repository: ice-lab/icestark
Pull request #443 (2021-11-04 05:50:59)

feat: 🎸 cache logic
Closes https://github.com/ice-lab/icestark/issues/389


Patch:
diff --git a/packages/icestark/src/AppRoute.tsx b/packages/icestark/src/AppRoute.tsx
index 738123a..e327713 100644
--- a/packages/icestark/src/AppRoute.tsx
+++ b/packages/icestark/src/AppRoute.tsx
@@ -1,9 +1,12 @@
 import * as React from 'react';
 import renderComponent from './util/renderComponent';
 import { AppHistory } from './appHistory';
-import { unloadMicroApp, BaseConfig, createMicroApp } from './apps';
+import { unloadMicroApp, unmountMicroApp, BaseConfig, createMicroApp } from './apps';
 import { converArray2String } from './util/helpers';
-import { callCapturedEventListeners, resetCapturedEventListeners } from './util/capturedListeners';
+import {
+  callCapturedEventListeners,
+  resetCapturedEventListeners,
+} from './util/capturedListeners';
 import isEqual from 'lodash.isequal';
 
 import type { PathData } from './util/checkActive';
@@ -135,6 +138,7 @@ export default class AppRoute extends React.Component<AppRouteProps, AppRouteSta
 
   mountApp = () => {
     resetCapturedEventListeners();
+
     const { onAppEnter } = this.props;
 
     // Trigger app enter
@@ -150,7 +154,7 @@ export default class AppRoute extends React.Component<AppRouteProps, AppRouteSta
   };
 
   unmountApp = () => {
-    const { name, onAppLeave } = this.props;
+    const { name, onAppLeave, cached } = this.props;
 
     // Trigger app leave
     if (typeof onAppLeave === 'function') {
@@ -158,7 +162,7 @@ export default class AppRoute extends React.Component<AppRouteProps, AppRouteSta
     }
 
     if (!this.validateRender()) {
-      unloadMicroApp(name);
+      cached ? unmountMicroApp(name) : unloadMicroApp(name);
     }
   };
 
diff --git a/packages/icestark/src/apps.ts b/packages/icestark/src/apps.ts
index d39f153..0acc5b7 100644
--- a/packages/icestark/src/apps.ts
+++ b/packages/icestark/src/apps.ts
@@ -1,3 +1,4 @@
+/* eslint-disable max-lines */
 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';
@@ -179,7 +180,8 @@ export async function loadAppModule(appConfig: AppConfig) {
 
   let lifecycle: ModuleLifeCycle = {};
   onLoadingApp(appConfig);
-  const { url, container, entry, entryContent, name, scriptAttributes = [], loadScriptMode, appSandbox } = appConfig;
+
+  const { url, container, entry, entryContent, name, scriptAttributes = [], loadScriptMode, appSandbox, cached } = appConfig;
   const appAssets = url ? getUrlAssets(url) : await getEntryAssets({
     root: container,
     entry,
@@ -189,6 +191,8 @@ export async function loadAppModule(appConfig: AppConfig) {
     fetch,
   });
 
+  const cacheId = cached ? name : undefined;
+
   updateAppConfig(appConfig.name, { appAssets });
 
   const cacheCss = shouldCacheCss(loadScriptMode);
@@ -201,6 +205,7 @@ export async function loadAppModule(appConfig: AppConfig) {
       ], {
         cacheCss,
         fetch,
+        cacheId,
       });
       lifecycle = await loadScriptByImport(appAssets.jsList);
       // Not to handle script element temporarily.
@@ -209,6 +214,7 @@ export async function loadAppModule(appConfig: AppConfig) {
       await loadAndAppendCssAssets(appAssets.cssList, {
         cacheCss,
         fetch,
+        cacheId,
       });
       lifecycle = await loadScriptByFetch(appAssets.jsList, appSandbox, fetch);
       break;
@@ -217,13 +223,14 @@ export async function loadAppModule(appConfig: AppConfig) {
         loadAndAppendCssAssets(appAssets.cssList, {
           cacheCss,
           fetch,
+          cacheId,
         }),
-        loadAndAppendJsAssets(appAssets, { scriptAttributes }),
+        loadAndAppendJsAssets(appAssets, { scriptAttributes, cacheId }),
       ]);
       lifecycle =
-          getLifecyleByLibrary() ||
-          getLifecyleByRegister() ||
-          {};
+        getLifecyleByLibrary() ||
+        getLifecyleByRegister() ||
+        {};
   }
 
   if (isEmpty(lifecycle)) {
@@ -446,14 +453,29 @@ export async function unmountMicroApp(appName: string) {
   }
 }
 
-// unload micro app, load app bundles when create micro app
+/**
+ * uninstall micro app thoroughly
+ * @param appName
+ */
 export async function unloadMicroApp(appName: string) {
   const appConfig = getAppConfig(appName);
   if (appConfig) {
+    if (appConfig.cached) {
+      log.warn(
+        formatErrMessage(
+          ErrorCode.CACHED_APP_USE_UNLOAD,
+          isDev && 'Use unmountMicroApp instead of unloadMicroApp to uninstall app {0}. This will not break the whole app but invalidate caching',
+          appName,
+        ),
+      );
+    }
+
     unmountMicroApp(appName);
+
     delete appConfig.mount;
     delete appConfig.unmount;
     delete appConfig.appAssets;
+
     updateAppConfig(appName, { status: NOT_LOADED });
   } else {
     log.error(
diff --git a/packages/icestark/src/util/appLifeCycle.ts b/packages/icestark/src/util/appLifeCycle.ts
deleted file mode 100644
index f651efb..0000000
--- a/packages/icestark/src/util/appLifeCycle.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import { setCache, getCache } from './cache';
-import { resetCapturedEventListeners } from './capturedListeners';
-
-export enum AppLifeCycleEnum {
-  AppEnter = 'appEnter',
-  AppLeave = 'appLeave',
-}
-
-export function cacheApp(cacheKey: string) {
-  [AppLifeCycleEnum.AppEnter, AppLifeCycleEnum.AppLeave].forEach((lifeCycle) => {
-    const lifeCycleCacheKey = `cache_${cacheKey}_${lifeCycle}`;
-    if (getCache(lifeCycle)) {
-      setCache(lifeCycleCacheKey, getCache(lifeCycle));
-    } else if (getCache(lifeCycleCacheKey)) {
-      // set cache to current lifeCycle
-      setCache(lifeCycle, getCache(lifeCycleCacheKey));
-    }
-  });
-}
-
-export function deleteCache(cacheKey: string) {
-  [AppLifeCycleEnum.AppEnter, AppLifeCycleEnum.AppLeave].forEach((lifeCycle) => {
-    setCache(`cache_${cacheKey}_${lifeCycle}`, null);
-  });
-}
-
-export function isCached(cacheKey: string) {
-  return !!getCache(`cache_${cacheKey}_${AppLifeCycleEnum.AppEnter}`);
-}
-
-export function callAppEnter() {
-  const appEnterKey = AppLifeCycleEnum.AppEnter;
-  const registerAppEnterCallback = getCache(appEnterKey);
-
-  if (registerAppEnterCallback) {
-    registerAppEnterCallback();
-    setCache(appEnterKey, null);
-  }
-}
-
-export function callAppLeave() {
-  // resetCapturedEventListeners when app change, remove react-router/vue-router listeners
-  resetCapturedEventListeners();
-
-  const appLeaveKey = AppLifeCycleEnum.AppLeave;
-  const registerAppLeaveCallback = getCache(appLeaveKey);
-
-  if (registerAppLeaveCallback) {
-    registerAppLeaveCallback();
-    setCache(appLeaveKey, null);
-  }
-}
diff --git a/packages/icestark/src/util/capturedListeners.ts b/packages/icestark/src/util/capturedListeners.ts
index 47aab6e..c212b0c 100644
--- a/packages/icestark/src/util/capturedListeners.ts
+++ b/packages/icestark/src/util/capturedListeners.ts
@@ -84,3 +84,4 @@ export function resetCapturedEventListeners() {
   capturedEventListeners[CapturedEventNameEnum.POPSTATE] = [];
   capturedEventListeners[CapturedEventNameEnum.HASHCHANGE] = [];
 }
+
diff --git a/packages/icestark/src/util/constant.ts b/packages/icestark/src/util/constant.ts
index 27613d0..13ee6e0 100644
--- a/packages/icestark/src/util/constant.ts
+++ b/packages/icestark/src/util/constant.ts
@@ -23,3 +23,8 @@ export const NOT_MOUNTED = 'NOT_MOUNTED';
 export const MOUNTED = 'MOUNTED';
 
 export const UNMOUNTED = 'UNMOUNTED';
+
+export enum AppLifeCycleEnum {
+  AppEnter = 'appEnter',
+  AppLeave = 'appLeave',
+}
diff --git a/packages/icestark/src/util/error.ts b/packages/icestark/src/util/error.ts
index c8b19c9..6526d8d 100644
--- a/packages/icestark/src/util/error.ts
+++ b/packages/icestark/src/util/error.ts
@@ -6,6 +6,7 @@ export enum ErrorCode {
   'JS_LOAD_ERROR' = 5,
   'CSS_LOAD_ERROR' = 6,
   'ACTIVE_PATH_ITEM_CAN_NOT_BE_EMPTY' = 7,
+  'CACHED_APP_USE_UNLOAD' = 8,
 }
 
 export function normalizeMsg(msg: string, args: string[]) {
diff --git a/packages/icestark/src/util/getLifecycle.ts b/packages/icestark/src/util/getLifecycle.ts
index fe57821..bc2b985 100644
--- a/packages/icestark/src/util/getLifecycle.ts
+++ b/packages/icestark/src/util/getLifecycle.ts
@@ -1,5 +1,5 @@
 import { getCache, setCache } from './cache';
-import { AppLifeCycleEnum } from './appLifeCycle';
+import { AppLifeCycleEnum } from './constant';
 import type { ModuleLifeCycle } from '../apps';
 
 export function getLifecyleByLibrary() {
diff --git a/packages/icestark/src/util/handleAssets.ts b/packages/icestark/src/util/handleAssets.ts
index 41c9ec5..cc31e92 100644
--- a/packages/icestark/src/util/handleAssets.ts
+++ b/packages/icestark/src/util/handleAssets.ts
@@ -75,11 +75,16 @@ function isAssetExist(element: HTMLScriptElement | HTMLLinkElement, type: 'scrip
 /**
  * Create link/style element and append to root
  */
-export function appendCSS(
-  root: HTMLElement | ShadowRoot,
-  asset: Asset | HTMLElement,
-  id: string,
-): Promise<void> {
+export function appendCSS(asset: Asset | HTMLElement,
+  {
+    root,
+    id,
+    cacheId,
+  }: {
+    root?: HTMLElement | ShadowRoot;
+    id?: string;
+    cacheId?: string;
+  }): Promise<void> {
   return new Promise<void>(async (resolve, reject) => {
     if (!root) reject(new Error('no root element for css asset'));
 
@@ -95,6 +100,7 @@ export function appendCSS(
       const styleElement: HTMLStyleElement = document.createElement('style');
       styleElement.id = id;
       styleElement.setAttribute(PREFIX, DYNAMIC);
+      cacheId && styleElement.setAttribute('data-cache', cacheId);
       styleElement.innerHTML = content;
       root.appendChild(styleElement);
       resolve();
@@ -112,6 +118,7 @@ export function appendCSS(
         styleElement.innerHTML = await cachedStyleContent[content];
         styleElement.id = id;
         styleElement.setAttribute(PREFIX, DYNAMIC);
+        cacheId && styleElement.setAttribute('data-cache', cacheId);
         root.appendChild(styleElement);
         useExternalLink = false;
         resolve();
@@ -126,6 +133,7 @@ export function appendCSS(
       element.id = id;
       element.rel = 'stylesheet';
       element.href = content;
+      cacheId && element.setAttribute('data-cache', cacheId);
 
       element.addEventListener(
         'error',
@@ -156,19 +164,21 @@ function setAttributeForScriptNode(element: HTMLScriptElement, {
   id,
   src,
   scriptAttributes,
+  cacheId,
 }: {
   module: boolean;
   id: string;
   src: string;
   scriptAttributes: ScriptAttributes;
+  cacheId?: string;
 }) {
   /*
   * stamped by icestark for recycle when needed.
   */
   element.setAttribute(PREFIX, DYNAMIC);
+  cacheId && element.setAttribute('data-cache', cacheId);
   element.id = id;
 
-
   element.type = module ? 'module' : 'text/javascript';
   element.src = src;
 
@@ -225,10 +235,12 @@ export function appendExternalScript(asset: string | Asset,
     id,
     root = document.getElementsByTagName('head')[0],
     scriptAttributes = [],
+    cacheId,
   }: {
     id: string;
     root?: HTMLElement | ShadowRoot;
     scriptAttributes?: ScriptAttributes;
+    cacheId?: string;
   }): Promise<void> {
   return new Promise<void>((resolve, reject) => {
     const { type, content, module } = (asset as Asset);
@@ -239,6 +251,7 @@ export function appendExternalScript(asset: string | Asset,
       element.innerHTML = content;
       element.id = id;
       element.setAttribute(PREFIX, DYNAMIC);
+      cacheId && element.setAttribute('data-cache', cacheId);
       module && (element.type = 'module');
       root.appendChild(element);
 
@@ -254,6 +267,7 @@ export function appendExternalScript(asset: string | Asset,
       id,
       src: content || (asset as string),
       scriptAttributes,
+      cacheId,
     });
 
     if (isAssetExist(element, 'script')) {
@@ -653,21 +667,8 @@ export function emptyAssets(
 
 export function checkCacheKey(node: HTMLElement | HTMLLinkElement | HTMLStyleElement | HTMLScriptElement, cacheKey: string|boolean) {
   return (typeof cacheKey === 'boolean' && cacheKey)
-    || !node.getAttribute('cache')
-    || node.getAttribute('cache') === cacheKey;
-}
-
-/**
- * cache all assets loaded by current sub-application
- */
-export function cacheAssets(cacheKey: string): void {
-  const assetsList = getAssetsNode();
-  assetsList.forEach((assetsNode) => {
-    // set cache key if asset attributes without prefix=static and cache
-    if (assetsNode.getAttribute(PREFIX) !== STATIC && !assetsNode.getAttribute('cache')) {
-      assetsNode.setAttribute('cache', cacheKey);
-    }
-  });
+    || !node.getAttribute('data-cache')
+    || node.getAttribute('data-cache') === cacheKey;
 }
 
 /**
@@ -679,9 +680,11 @@ export function cacheAssets(cacheKey: string): void {
 export async function loadAndAppendCssAssets(cssList: Array<Asset | HTMLElement>, {
   cacheCss = false,
   fetch = defaultFetch,
+  cacheId,
 }: {
   cacheCss?: boolean;
   fetch?: Fetch;
+  cacheId?: string;
 }) {
   const cssRoot: HTMLElement = document.getElementsByTagName('head')[0];
 
@@ -704,18 +707,28 @@ export async function loadAndAppendCssAssets(cssList: Array<Asset | HTMLElement>
     // And supposed to be remove from 3.x
     if (!useLinks) {
       return await Promise.all([
-        ...cssContents.map((content, index) => appendCSS(
-          cssRoot,
-          { content, type: AssetTypeEnum.INLINE }, `${PREFIX}-css-${index}`,
-        )),
-        ...cssList.filter((css) => isElement(css)).map((asset, index) => appendCSS(cssRoot, asset, `${PREFIX}-css-${index}`)),
+        ...cssContents.map((content, index) => appendCSS({ content, type: AssetTypeEnum.INLINE },
+          {
+            root: cssRoot,
+            id: `${PREFIX}-css-${index}`,
+            cacheId,
+          })),
+        ...cssList.filter((css) => isElement(css)).map((asset, index) => appendCSS(asset, {
+          root: cssRoot,
+          id: `${PREFIX}-css-${index}`,
+          cacheId,
+        })),
       ]);
     }
   }
 
   // load css content
-  return await Promise.all(
-    cssList.map((asset, index) => appendCSS(cssRoot, asset, `${PREFIX}-css-${index}`)),
+  await Promise.all(
+    cssList.map((asset, index) => appendCSS(asset, {
+      root: cssRoot,
+      id: `${PREFIX}-css-${index}`,
+      cacheId,
+    })),
   );
 }
 
@@ -731,8 +744,10 @@ export function loadAndAppendJsAssets(
   assets: Assets,
   {
     scriptAttributes = [],
+    cacheId,
   }: {
     scriptAttributes?: ScriptAttributes;
+    cacheId?: string;
   },
 ) {
   const jsRoot: HTMLElement = document.getElementsByTagName('head')[0];
@@ -748,6 +763,7 @@ export function loadAndAppendJsAssets(
         root: jsRoot,
         scriptAttributes,
         id: `${PREFIX}-js-${index}`,
+        cacheId,
       }));
     }, Promise.resolve());
   }
@@ -757,6 +773,7 @@ export function loadAndAppendJsAssets(
       root: jsRoot,
       scriptAttributes,
       id: `${PREFIX}-js-${index}`,
+      cacheId,
     })),
   );
 }
diff --git a/packages/sandbox/CHANGELOG.md b/packages/sandbox/CHANGELOG.md
index 0526bbe..6fc1735 100644
--- a/packages/sandbox/CHANGELOG.md
+++ b/packages/sandbox/CHANGELOG.md
@@ -1,5 +1,9 @@
 # Changelog
 
+## 1.1.5
+
+- [fix] exposes `injection` through constructor.
+
 ## 1.1.4
 
 - [fix] simply copy callable funtions's extra properties.
diff --git a/packages/sandbox/package.json b/packages/sandbox/package.json
index 9433e00..2fcf808 100644
--- a/packages/sandbox/package.json
+++ b/packages/sandbox/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@ice/sandbox",
-  "version": "1.1.4",
+  "version": "1.1.5",
   "description": "sandbox for execute scripts",
   "main": "lib/index.js",
   "scripts": {
diff --git a/packages/sandbox/src/index.ts b/packages/sandbox/src/index.ts
index b91cf98..34a5fe8 100644
--- a/packages/sandbox/src/index.ts
+++ b/packages/sandbox/src/index.ts
@@ -1,5 +1,6 @@
 export interface SandboxProps {
   multiMode?: boolean;
+  injection?: Record<PropertyKey, any>;
 }
 
 export interface SandboxConstructor {
@@ -33,6 +34,8 @@ export default class Sandbox {
 
   private multiMode = false;
 
+  private injection = {};
+
   private eventListeners = {};
 
   private timeoutIds: number[] = [];
@@ -46,7 +49,7 @@ export default class Sandbox {
   public sandboxDisabled: boolean;
 
   constructor(props: SandboxProps = {}) {
-    const { multiMode } = props;
+    const { multiMode, injection } = props;
     if (!window.Proxy) {
       console.warn('proxy sandbox is not support by current browser');
       this.sandboxDisabled = true;
@@ -54,8 +57,13 @@ export default class Sandbox {
     // enable multiMode in case of create mulit sandbox in same time
     this.multiMode = multiMode;
     this.sandbox = null;
+    this.injection = injection;
   }
 
+  /**
+   * create proxy sandbox
+   * @param injection @deprecated will be deprecated in the future
+   */
   createProxySandbox(injection?: object) {
     const { propertyAdded, originalValues, multiMode } = this;
     const proxyWindow = Object.create(null) as Window;
@@ -65,6 +73,9 @@ export default class Sandbox {
     const originalSetInterval = window.setInterval;
     const originalSetTimeout = window.setTimeout;
 
+    // `this` in Proxy traps will retarget to the trap.
+    const _self = this;
+
     // hijack addEventListener
     proxyWindow.addEventListener = (eventName, fn, ...rest) => {
       this.eventListeners[eventName] = (this.eventListeners[eventName] || []);
@@ -135,7 +146,8 @@ export default class Sandbox {
         }
 
         // search from injection
-        const injectionValue = injection && injection[p];
+        const injectionValue = _self.injection?.[p] ?? injection?.[p];
+
         if (injectionValue) {
           return injectionValue;
         }
@@ -159,6 +171,7 @@ export default class Sandbox {
 
           // Axios, Moment, and other callable functions may have additional properties.
           // Simply copy them into boundValue.
+          // eslint-disable-next-line guard-for-in
           for (const key in value) {
             boundValue[key] = value[key];
           }


Test patch:
diff --git a/packages/icestark/__tests__/AppRouter.spec.tsx b/packages/icestark/__tests__/AppRouter.spec.tsx
index 43b731c..69d52c0 100644
--- a/packages/icestark/__tests__/AppRouter.spec.tsx
+++ b/packages/icestark/__tests__/AppRouter.spec.tsx
@@ -6,6 +6,7 @@ import { getCache, setCache } from '../src/util/cache';
 import * as React from 'react';
 import { render } from '@testing-library/react';
 import { AppRouter, AppRoute } from '../src/index';
+import { getMicroApps } from '../src/apps';
 
 const delay = (milliscond: number) => new Promise(resolve => setTimeout(resolve, milliscond));
 
@@ -145,4 +146,36 @@ describe('AppRouter', () => {
 
     unmount();
   })
+
+  test('app-cached', async () => {
+    (fetch as FetchMock).mockResponseOnce(umdSourceWithSetLibrary.toString());
+    const { container, unmount } = render(
+      <AppRouter>
+        <AppRoute
+          loadScriptMode="fetch"
+          cached
+          name="seller"
+          path="/seller"
+          title="小二"
+          url={[
+            '//icestark.com/index.js'
+          ]}
+        />
+      </AppRouter>
+    );
+    window.history.pushState({}, 'test', '/seller');
+
+    await delay(1000);
+    expect(container.innerHTML).toContain('商家平台')
+
+    window.history.pushState({}, 'test', '/waiter');
+    await delay(1000);
+
+    const app = getMicroApps().find(app => app.name === 'seller');
+    expect(app.status).toEqual('UNMOUNTED');
+    expect(!!app.mount).toBeTruthy();
+    expect(!!app.unmount).toBeTruthy();
+
+    unmount();
+  })
 })
\ No newline at end of file
diff --git a/packages/icestark/__tests__/appLifeCycle.spec.tsx b/packages/icestark/__tests__/appLifeCycle.spec.tsx
deleted file mode 100644
index c8c5233..0000000
--- a/packages/icestark/__tests__/appLifeCycle.spec.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import '@testing-library/jest-dom/extend-expect';
-
-import { AppLifeCycleEnum, cacheApp, callAppEnter, callAppLeave, isCached, deleteCache } from '../src/util/appLifeCycle';
-import { setCache } from '../src/util/cache';
-
-describe('appLifeCycle', () => {
-  test('callAppEnter', () => {
-    const appEnterMockFn = jest.fn();
-
-    setCache(AppLifeCycleEnum.AppEnter, appEnterMockFn);
-
-    callAppEnter();
-    expect(appEnterMockFn).toBeCalledTimes(1);
-
-    setCache(AppLifeCycleEnum.AppEnter, null);
-    callAppEnter();
-    expect(appEnterMockFn).toBeCalledTimes(1);
-  });
-
-  test('callAppLeave', () => {
-    const appLeaveMockFn = jest.fn();
-
-    setCache(AppLifeCycleEnum.AppLeave, appLeaveMockFn);
-
-    callAppLeave();
-    expect(appLeaveMockFn).toBeCalledTimes(1);
-
-    setCache(AppLifeCycleEnum.AppLeave, null);
-    callAppLeave();
-    expect(appLeaveMockFn).toBeCalledTimes(1);
-  });
-
-  test('cache app', () => {
-    const appEnterMockFn = jest.fn();
-    const appLeaveMockFn = jest.fn();
-    const appKey = 'appKey';
-    setCache(AppLifeCycleEnum.AppEnter, appEnterMockFn);
-    setCache(AppLifeCycleEnum.AppLeave, appLeaveMockFn);
-    cacheApp(appKey);
-    expect(isCached(appKey)).toBe(true);
-    deleteCache(appKey);
-    expect(isCached(appKey)).toBe(false);
-  })
-});
diff --git a/packages/icestark/__tests__/handleAssets.spec.tsx b/packages/icestark/__tests__/handleAssets.spec.tsx
index b236a31..7860226 100644
--- a/packages/icestark/__tests__/handleAssets.spec.tsx
+++ b/packages/icestark/__tests__/handleAssets.spec.tsx
@@ -649,12 +649,13 @@ describe('appendCSS', () => {
     const div = document.createElement('div');
 
     appendCSS(
-      div,
       {
         type: AssetTypeEnum.EXTERNAL,
         content: '/test.css'
-      },
-      'icestark-css-0'
+      },{
+        root: div,
+        id: 'icestark-css-0'
+      }
     )
       .then(() => {
         expect(div.innerHTML).toContain('id="icestark-css-0"');
@@ -671,10 +672,13 @@ describe('appendCSS', () => {
   test('appendCSS -> style', () => {
     const div = document.createElement('div');
     const style = '.test { color: #fff;}';
-    appendCSS(div, {
+    appendCSS({
       type: AssetTypeEnum.INLINE,
       content: style,
-    }, 'icestark-css-0')
+    }, {
+      root: div,
+      id: 'icestark-css-0'
+    })
       .then(() => {
         expect(div.innerHTML).toContain('id="icestark-css-0"');
         expect(div.innerHTML).toContain(style);
@@ -694,12 +698,14 @@ describe('appendCSS', () => {
     const div = document.createElement('div');
 
     appendCSS(
-      div,
       {
         type: AssetTypeEnum.EXTERNAL,
         content: '/test.css'
       },
-      'icestark-css-0'
+      {
+        root: div,
+        id: 'icestark-css-0'
+      }
     )
       .then(() => {
         expect(div.innerHTML).toContain('id="icestark-css-0"');
diff --git a/packages/sandbox/__tests__/index.spec.ts b/packages/sandbox/__tests__/index.spec.ts
index fea1ee9..06d6c6a 100644
--- a/packages/sandbox/__tests__/index.spec.ts
+++ b/packages/sandbox/__tests__/index.spec.ts
@@ -151,3 +151,31 @@ describe('callable functions in sandbox', () => {
     expect(error).toBe(null);
   });
 });
+
+describe('inject value to sandbox', () => {
+  const testObject = { a: 1 };
+  const testProxy = new Proxy(testObject, {
+    get(target, p, receiver) {
+        if(p === 'a') {
+          return 2;
+        }
+        return Reflect.get(target, p);
+    }
+  })
+  // in order to pass value outof sandbox to validate, set multiMode false 
+  const sandbox = new Sandbox({multiMode: false, injection: {testObject: testProxy}});
+  test('inject proxy object in sandbox', () => {
+    // @ts-ignore
+    window.testObject = testObject;
+    // @ts-ignore
+    window.result = undefined;
+    sandbox.execScriptInSandbox(
+      `
+        window.result = window.testObject.a;
+      `,
+    );
+
+    // @ts-ignore
+    expect(window.result).toBe(2);
+  })
+})