Repository: umijs/qiankun Branch: next Commit: 595b93c50292 Files: 382 Total size: 1.4 MB Directory structure: gitextract_dfszrfro/ ├── .changeset/ │ ├── README.md │ ├── big-cougars-draw.md │ ├── clean-walls-hang.md │ ├── clever-carpets-vanish.md │ ├── clever-dragons-ring.md │ ├── config.json │ ├── empty-jars-vanish.md │ ├── empty-lions-rescue.md │ ├── five-papayas-buy.md │ ├── forty-teachers-taste.md │ ├── four-worms-think.md │ ├── friendly-apples-design.md │ ├── giant-geckos-love.md │ ├── green-pants-remember.md │ ├── green-tools-wonder.md │ ├── hungry-needles-doubt.md │ ├── itchy-pears-retire.md │ ├── itchy-snakes-tell.md │ ├── large-jokes-smile.md │ ├── lemon-seals-juggle.md │ ├── long-flies-repair.md │ ├── loud-berries-watch.md │ ├── loud-penguins-crash.md │ ├── loud-teachers-develop.md │ ├── lovely-colts-decide.md │ ├── lucky-bikes-scream.md │ ├── metal-cougars-help.md │ ├── mighty-nails-pull.md │ ├── modern-kiwis-tap.md │ ├── ninety-rivers-check.md │ ├── orange-boats-allow.md │ ├── poor-squids-hide.md │ ├── pre.json │ ├── rare-lobsters-marry.md │ ├── real-trees-unite.md │ ├── red-islands-mate.md │ ├── red-students-run.md │ ├── rich-parents-relate.md │ ├── selfish-lamps-thank.md │ ├── serious-nails-jog.md │ ├── shaggy-shrimps-drum.md │ ├── sharp-files-raise.md │ ├── shiny-jeans-sip.md │ ├── short-kings-explain.md │ ├── shy-mayflies-shave.md │ ├── silly-books-complain.md │ ├── slow-timers-heal.md │ ├── small-experts-hug.md │ ├── smart-guests-jam.md │ ├── smart-scissors-press.md │ ├── smart-scissors-sell.md │ ├── smooth-pillows-jam.md │ ├── sour-roses-smile.md │ ├── spotty-plums-hear.md │ ├── stale-dolls-push.md │ ├── strong-rocks-sneeze.md │ ├── sweet-cars-protect.md │ ├── sweet-shoes-brake.md │ ├── swift-squids-vanish.md │ ├── tall-buttons-pretend.md │ ├── tasty-donkeys-relax.md │ ├── tender-dingos-allow.md │ ├── tender-pots-perform.md │ ├── thin-ways-allow.md │ ├── three-hornets-hammer.md │ ├── tough-beers-grow.md │ ├── tough-phones-chew.md │ ├── twelve-donkeys-help.md │ ├── warm-chefs-chew.md │ ├── wicked-icons-type.md │ ├── wise-eagles-tease.md │ └── wise-ravens-prove.md ├── .dumirc.ts ├── .editorconfig ├── .eslintignore ├── .eslintrc.cjs ├── .fatherrc.cjs ├── .github/ │ ├── CODE_OF_CONDUCT.md │ ├── CONTRIBUTING.md │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ ├── bug_report_cn.md │ │ ├── feature_request.md │ │ └── rfc_cn.md │ ├── PULL_REQUEST_TEMPLATE.md │ └── workflows/ │ ├── announcement-notify.yml │ ├── changeset-prerelease.yml │ ├── ci.yml │ ├── emoji-helper.yml │ ├── github-pages.yml │ ├── issue-close-inactive.yml │ ├── issue-reply.yml │ ├── publish-1.x.yml │ ├── publish-latest.yml │ └── release-notify.yml ├── .gitignore ├── .husky/ │ ├── commit-msg │ ├── pre-commit │ └── pre-push ├── .prettierignore ├── .prettierrc ├── AGENTS.md ├── LICENSE ├── README.md ├── commitlint.config.js ├── docs/ │ ├── .vitepress/ │ │ ├── config.mjs │ │ └── theme/ │ │ └── index.js │ ├── api/ │ │ ├── configuration.md │ │ ├── index.md │ │ ├── is-runtime-compatible.md │ │ ├── lifecycles.md │ │ ├── load-micro-app.md │ │ ├── register-micro-apps.md │ │ ├── start.md │ │ └── types.md │ ├── cookbook/ │ │ ├── error-handling.md │ │ ├── index.md │ │ ├── performance.md │ │ └── style-isolation.md │ ├── ecosystem/ │ │ ├── bundler-plugin.md │ │ ├── create-qiankun.md │ │ ├── index.md │ │ ├── react.md │ │ └── vue.md │ ├── faq/ │ │ └── index.md │ ├── guide/ │ │ ├── index.md │ │ ├── quick-start.md │ │ └── tutorial.md │ ├── index.md │ └── zh-CN/ │ ├── api/ │ │ ├── configuration.md │ │ ├── index.md │ │ ├── is-runtime-compatible.md │ │ ├── lifecycles.md │ │ ├── load-micro-app.md │ │ ├── register-micro-apps.md │ │ ├── start.md │ │ └── types.md │ ├── cookbook/ │ │ ├── error-handling.md │ │ ├── index.md │ │ ├── performance.md │ │ └── style-isolation.md │ ├── ecosystem/ │ │ ├── create-qiankun.md │ │ ├── index.md │ │ ├── react.md │ │ ├── vue.md │ │ └── webpack-plugin.md │ ├── faq/ │ │ └── index.md │ ├── guide/ │ │ ├── index.md │ │ ├── quick-start.md │ │ └── tutorial.md │ └── index.md ├── examples/ │ ├── main/ │ │ ├── index.html │ │ ├── package.json │ │ ├── postcss.config.js │ │ ├── src/ │ │ │ ├── App.tsx │ │ │ ├── components/ │ │ │ │ ├── Dashboard.tsx │ │ │ │ ├── Header.tsx │ │ │ │ ├── MicroAppContainer.tsx │ │ │ │ └── Sidebar.tsx │ │ │ ├── main.tsx │ │ │ ├── store/ │ │ │ │ └── qiankun.ts │ │ │ ├── styles/ │ │ │ │ └── index.css │ │ │ └── vite-env.d.ts │ │ ├── tailwind.config.js │ │ ├── tsconfig.json │ │ ├── tsconfig.node.json │ │ └── vite.config.ts │ ├── purehtml/ │ │ ├── entry.js │ │ ├── index.html │ │ └── package.json │ ├── react/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── config/ │ │ │ └── qiankunHtml.ts │ │ ├── eslint.config.js │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── App.css │ │ │ ├── App.tsx │ │ │ ├── index.css │ │ │ ├── main.tsx │ │ │ └── vite-env.d.ts │ │ ├── tsconfig.app.json │ │ ├── tsconfig.json │ │ ├── tsconfig.node.json │ │ └── vite.config.ts │ ├── vite/ │ │ ├── .eslintrc.cjs │ │ ├── .gitignore │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── App.css │ │ │ ├── App.tsx │ │ │ ├── index.css │ │ │ ├── main.tsx │ │ │ └── vite-env.d.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.node.json │ │ └── vite.config.ts │ └── vue/ │ ├── .gitignore │ ├── README.md │ ├── config/ │ │ └── qiankunHtml.ts │ ├── index.html │ ├── package.json │ ├── src/ │ │ ├── App.vue │ │ ├── components/ │ │ │ └── HelloWorld.vue │ │ ├── main.ts │ │ ├── style.css │ │ └── vite-env.d.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── package.json ├── packages/ │ ├── bundler-plugin/ │ │ ├── .fatherrc.js │ │ ├── CHANGELOG.md │ │ ├── README-zh.md │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── index.ts │ │ │ └── webpack/ │ │ │ └── index.ts │ │ └── tests/ │ │ ├── fixtures/ │ │ │ ├── webpack4.html │ │ │ └── webpack5.html │ │ ├── plugin.test.ts │ │ ├── webpack4/ │ │ │ ├── .eslintrc.js │ │ │ ├── index.html │ │ │ ├── index.js │ │ │ ├── package.json │ │ │ └── webpack.config.js │ │ └── webpack5/ │ │ ├── .eslintrc.js │ │ ├── index.html │ │ ├── index.js │ │ ├── package.json │ │ └── webpack.config.js │ ├── create-qiankun/ │ │ ├── .fatherrc.js │ │ ├── CHANGELOG.md │ │ ├── Readme.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── index.ts │ │ │ └── shared/ │ │ │ ├── generators/ │ │ │ │ └── createVite.ts │ │ │ ├── patchers/ │ │ │ │ ├── entryFile.ts │ │ │ │ ├── index.ts │ │ │ │ ├── packageJson.ts │ │ │ │ ├── qiankunHtmlPlugin.ts │ │ │ │ └── viteConfig.ts │ │ │ ├── types.ts │ │ │ └── utils/ │ │ │ └── index.ts │ │ ├── tests/ │ │ │ ├── e2e.cli.test.ts │ │ │ └── fixtures/ │ │ │ ├── react-ts/ │ │ │ │ ├── main.tsx.txt │ │ │ │ ├── qiankunHtml.ts.txt │ │ │ │ └── vite.config.ts.txt │ │ │ └── vue-ts/ │ │ │ ├── main.ts.txt │ │ │ ├── qiankunHtml.ts.txt │ │ │ └── vite.config.ts.txt │ │ ├── tsconfig.json │ │ ├── vitest.config.ts │ │ └── vitest.e2e.config.ts │ ├── loader/ │ │ ├── .fatherrc.js │ │ ├── AGENTS.md │ │ ├── CHANGELOG.md │ │ ├── benchmarks/ │ │ │ └── parser/ │ │ │ ├── html.js │ │ │ ├── huge-html/ │ │ │ │ ├── huge-html.js │ │ │ │ ├── import-html-entry.html │ │ │ │ └── parser.html │ │ │ ├── import-html-entry.html │ │ │ ├── parser.html │ │ │ └── tern/ │ │ │ ├── html.js │ │ │ ├── import-html-entry.html │ │ │ └── parser.html │ │ ├── package.json │ │ └── src/ │ │ ├── TagTransformStream.ts │ │ ├── index.ts │ │ ├── parser.ts │ │ ├── utils.ts │ │ └── writable-dom/ │ │ ├── README.md │ │ └── index.ts │ ├── qiankun/ │ │ ├── .fatherrc.js │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ └── src/ │ │ ├── addons/ │ │ │ ├── engineFlag.ts │ │ │ ├── index.ts │ │ │ └── runtimePublicPath.ts │ │ ├── apis/ │ │ │ ├── __tests__/ │ │ │ │ ├── effects.test.ts │ │ │ │ └── prefetch.test.ts │ │ │ ├── effects.ts │ │ │ ├── errorHandler.ts │ │ │ ├── isRuntimeCompatible.ts │ │ │ ├── loadMicroApp.ts │ │ │ ├── prefetch.ts │ │ │ └── registerMicroApps.ts │ │ ├── core/ │ │ │ └── loadApp.ts │ │ ├── error.ts │ │ ├── index.ts │ │ ├── types.ts │ │ └── utils.ts │ ├── sandbox/ │ │ ├── .fatherrc.js │ │ ├── AGENTS.md │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ └── src/ │ │ ├── consts.ts │ │ ├── core/ │ │ │ ├── compartment/ │ │ │ │ ├── globalProps.ts │ │ │ │ └── index.ts │ │ │ ├── globals.ts │ │ │ ├── membrane/ │ │ │ │ ├── index.ts │ │ │ │ └── utils.ts │ │ │ ├── sandbox/ │ │ │ │ ├── StandardSandbox.ts │ │ │ │ ├── index.ts │ │ │ │ └── types.ts │ │ │ └── utils.ts │ │ ├── index.ts │ │ ├── patchers/ │ │ │ ├── consts.ts │ │ │ ├── dynamicAppend/ │ │ │ │ ├── common.ts │ │ │ │ ├── forStandardSandbox.ts │ │ │ │ ├── index.ts │ │ │ │ └── types.ts │ │ │ ├── historyListener.ts │ │ │ ├── index.ts │ │ │ ├── interval.ts │ │ │ ├── types.ts │ │ │ └── windowListener.ts │ │ └── utils.ts │ ├── shared/ │ │ ├── .fatherrc.js │ │ ├── AGENTS.md │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ └── src/ │ │ ├── assets-transpilers/ │ │ │ ├── __tests__/ │ │ │ │ └── script.test.ts │ │ │ ├── index.ts │ │ │ ├── link.ts │ │ │ ├── script.ts │ │ │ ├── types.ts │ │ │ └── utils.ts │ │ ├── common.ts │ │ ├── deferred-queue/ │ │ │ └── index.ts │ │ ├── fetch-utils/ │ │ │ ├── __tests__/ │ │ │ │ ├── makeFetchCacheable.test.ts │ │ │ │ ├── makeFetchRetryable.test.ts │ │ │ │ └── makeFetchThrowable.test.ts │ │ │ ├── makeFetchCacheable.ts │ │ │ ├── makeFetchRetryable.ts │ │ │ ├── makeFetchThrowable.ts │ │ │ ├── miniLruCache.ts │ │ │ └── utils.ts │ │ ├── index.ts │ │ ├── module-resolver/ │ │ │ ├── __tests__/ │ │ │ │ ├── index.test.ts │ │ │ │ └── satisfies.test.ts │ │ │ ├── index.ts │ │ │ ├── satisfies.ts │ │ │ └── types.ts │ │ ├── reporter/ │ │ │ ├── QiankunError.ts │ │ │ ├── index.ts │ │ │ └── logger.ts │ │ ├── typings.d.ts │ │ └── utils.ts │ └── ui-bindings/ │ ├── react/ │ │ ├── .eslintrc.cjs │ │ ├── .fatherrc.js │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── README.zh-CN.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── ErrorBoundary.tsx │ │ │ ├── MicroApp.tsx │ │ │ ├── MicroAppLoader.tsx │ │ │ └── index.ts │ │ └── tsconfig.json │ ├── shared/ │ │ ├── .fatherrc.js │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src/ │ │ │ └── index.ts │ │ └── tsconfig.json │ └── vue/ │ ├── .fatherrc.js │ ├── CHANGELOG.md │ ├── README.md │ ├── README.zh-CN.md │ ├── package.json │ ├── src/ │ │ ├── ErrorBoundary.ts │ │ ├── MicroApp.ts │ │ ├── MicroAppLoader.ts │ │ └── index.ts │ └── tsconfig.json ├── pnpm-workspace.yaml ├── scripts/ │ └── generate-release-notes.mjs ├── tsconfig.eslint.json ├── tsconfig.json ├── vitest.config.ts └── vitest.workspace.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .changeset/README.md ================================================ # Changesets Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works with multi-package repos, or single-package repos to help you version and publish your code. You can find the full documentation for it [in our repository](https://github.com/changesets/changesets) We have a quick list of common questions to get you started engaging with this project in [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) ================================================ FILE: .changeset/big-cougars-draw.md ================================================ --- "@qiankunjs/sandbox": patch --- feat(sandbox): use cloneNode api instead of importNode for compatible ================================================ FILE: .changeset/clean-walls-hang.md ================================================ --- "@qiankunjs/bundler-plugin": patch --- fix: move cheerio to dependencies ================================================ FILE: .changeset/clever-carpets-vanish.md ================================================ --- "@qiankunjs/sandbox": patch --- Revert "fix(sandbox): non-hijacking elements should be appended to global document (#2861)" ================================================ FILE: .changeset/clever-dragons-ring.md ================================================ --- "qiankun": patch "@qiankunjs/loader": patch "@qiankunjs/sandbox": patch "@qiankunjs/shared": patch --- ✨support to transform head/body tags to qiankun head/body in stream ================================================ FILE: .changeset/config.json ================================================ { "$schema": "https://unpkg.com/@changesets/config@2.29.5/schema.json", "changelog": "@changesets/cli/changelog", "commit": true, "fixed": [], "linked": [], "access": "public", "baseBranch": "next", "updateInternalDependencies": "patch", "ignore": [], "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": { "onlyUpdatePeerDependentsWhenOutOfRange": true } } ================================================ FILE: .changeset/empty-jars-vanish.md ================================================ --- 'create-qiankun': minor --- feat: introduce qiankun scaffold ================================================ FILE: .changeset/empty-lions-rescue.md ================================================ --- '@qiankunjs/shared': patch --- fix: remove inline script source-url ================================================ FILE: .changeset/five-papayas-buy.md ================================================ --- "qiankun": patch "@qiankunjs/shared": patch --- fix: optimize types and add a warning for preload ================================================ FILE: .changeset/forty-teachers-taste.md ================================================ --- '@qiankunjs/ui-shared': patch '@qiankunjs/react': patch '@qiankunjs/vue': patch --- feat: refactor the code of microapp ================================================ FILE: .changeset/four-worms-think.md ================================================ --- "@qiankunjs/sandbox": patch "@qiankunjs/shared": patch --- feat(sandbox): support dynamic sync scripts executed by order in sandbox ================================================ FILE: .changeset/friendly-apples-design.md ================================================ --- "qiankun": patch "@qiankunjs/react": patch "@qiankunjs/ui-shared": patch "@qiankunjs/vue": patch --- fix: remove unused umd bundle configuration ================================================ FILE: .changeset/giant-geckos-love.md ================================================ --- "@qiankunjs/sandbox": patch --- fix: double quote link element href as selector ================================================ FILE: .changeset/green-pants-remember.md ================================================ --- "@qiankunjs/loader": patch "qiankun": patch "@qiankunjs/shared": patch --- feat: add isRuntimeCompatible api to check qiankun3 compatibility ================================================ FILE: .changeset/green-tools-wonder.md ================================================ --- "@qiankunjs/shared": patch --- 🐛fix findDependency logic while peerDeps is undefined ================================================ FILE: .changeset/hungry-needles-doubt.md ================================================ --- "@qiankunjs/loader": patch "@qiankunjs/sandbox": patch "@qiankunjs/shared": patch --- feat: support defer scripts and keep the executing order to consist with browser ================================================ FILE: .changeset/itchy-pears-retire.md ================================================ --- "qiankun": patch "@qiankunjs/sandbox": patch --- feat: pass container with parameters rather than getter function ================================================ FILE: .changeset/itchy-snakes-tell.md ================================================ --- "qiankun": patch "@qiankunjs/sandbox": patch --- feat(loader): add lru cache for assets fetch by default ================================================ FILE: .changeset/large-jokes-smile.md ================================================ --- "@qiankunjs/shared": patch --- 🐛fix preload is invalid while reused dependency is working ================================================ FILE: .changeset/lemon-seals-juggle.md ================================================ --- "qiankun": patch "@qiankunjs/shared": patch --- feat(shared): introduce retryable and throwable to fetch-utils ================================================ FILE: .changeset/long-flies-repair.md ================================================ --- "@qiankunjs/sandbox": patch --- 🔀 merge master ================================================ FILE: .changeset/loud-berries-watch.md ================================================ --- "@qiankunjs/shared": patch --- refactor(shared): replace semver with compare-versions ================================================ FILE: .changeset/loud-penguins-crash.md ================================================ --- "@qiankunjs/sandbox": patch --- feat: set proxy appendChild/insertBefore method for every sandbox rather than modify prototype on HTMLElement ================================================ FILE: .changeset/loud-teachers-develop.md ================================================ --- "qiankun": patch --- feat: enable sandbox by default ================================================ FILE: .changeset/lovely-colts-decide.md ================================================ --- "create-qiankun": patch --- feat: introduce qiankun scaffold ================================================ FILE: .changeset/lucky-bikes-scream.md ================================================ --- "@qiankunjs/bundler-plugin": patch --- fix: mv webpack-sources to deps ================================================ FILE: .changeset/metal-cougars-help.md ================================================ --- "@qiankunjs/sandbox": patch --- 🐛parallel sandbox should use different compartment id ================================================ FILE: .changeset/mighty-nails-pull.md ================================================ --- "@qiankunjs/loader": patch "@qiankunjs/shared": patch --- feat(loader): compatible with defer entry script ================================================ FILE: .changeset/modern-kiwis-tap.md ================================================ --- "@qiankunjs/bundler-plugin": patch --- feat: introduce qiankun webpack plugin ================================================ FILE: .changeset/ninety-rivers-check.md ================================================ --- "@qiankunjs/loader": patch "qiankun": patch --- feat: add transformer options for app loader ================================================ FILE: .changeset/orange-boats-allow.md ================================================ --- "qiankun": patch "@qiankunjs/shared": patch --- feat: remove webpack chunk cache attributes just while there are multi instances loaded on document ================================================ FILE: .changeset/poor-squids-hide.md ================================================ --- "qiankun": patch --- fix(qiankun): should remove internal cache of loadMicroApp while loading failed ================================================ FILE: .changeset/pre.json ================================================ { "mode": "pre", "tag": "rc", "initialVersions": { "@qiankunjs/loader": "0.0.1", "qiankun": "3.0.0", "@qiankunjs/sandbox": "0.0.1", "@qiankunjs/shared": "0.0.1", "@qiankunjs/react": "0.0.1", "@qiankunjs/bundler-plugin": "0.0.1", "@qiankunjs/ui-shared": "0.0.0", "@qiankunjs/vue": "0.0.0", "create-qiankun": "0.0.0" }, "changesets": [ "big-cougars-draw", "clean-walls-hang", "clever-carpets-vanish", "clever-dragons-ring", "empty-jars-vanish", "empty-lions-rescue", "five-papayas-buy", "forty-teachers-taste", "four-worms-think", "friendly-apples-design", "giant-geckos-love", "green-pants-remember", "green-tools-wonder", "hungry-needles-doubt", "itchy-pears-retire", "itchy-snakes-tell", "large-jokes-smile", "lemon-seals-juggle", "long-flies-repair", "loud-berries-watch", "loud-penguins-crash", "loud-teachers-develop", "lovely-colts-decide", "lucky-bikes-scream", "metal-cougars-help", "mighty-nails-pull", "modern-kiwis-tap", "ninety-rivers-check", "orange-boats-allow", "poor-squids-hide", "rare-lobsters-marry", "real-trees-unite", "red-islands-mate", "red-students-run", "rich-parents-relate", "selfish-lamps-thank", "serious-nails-jog", "shaggy-shrimps-drum", "sharp-files-raise", "shiny-jeans-sip", "short-kings-explain", "shy-mayflies-shave", "silly-books-complain", "slow-timers-heal", "small-experts-hug", "smart-guests-jam", "smart-scissors-press", "smart-scissors-sell", "smooth-pillows-jam", "sour-roses-smile", "spotty-plums-hear", "stale-dolls-push", "strong-rocks-sneeze", "sweet-cars-protect", "sweet-shoes-brake", "swift-squids-vanish", "tall-buttons-pretend", "tasty-donkeys-relax", "tender-dingos-allow", "tender-pots-perform", "thin-ways-allow", "three-hornets-hammer", "tough-beers-grow", "tough-phones-chew", "twelve-donkeys-help", "warm-chefs-chew", "wicked-icons-type", "wise-eagles-tease", "wise-ravens-prove" ] } ================================================ FILE: .changeset/rare-lobsters-marry.md ================================================ --- "qiankun": patch --- 🐛 fix tsc error ================================================ FILE: .changeset/real-trees-unite.md ================================================ --- "@qiankunjs/sandbox": patch --- fix(sandbox): compatible with dynamically appending stylesheets to detached containers ================================================ FILE: .changeset/red-islands-mate.md ================================================ --- "@qiankunjs/loader": patch "qiankun": patch --- feat(loader): supports passing Response as entry parameter for loadEntry function ================================================ FILE: .changeset/red-students-run.md ================================================ --- "@qiankunjs/loader": patch "qiankun": patch "@qiankunjs/sandbox": patch "@qiankunjs/react": patch --- fix(sandbox): should get container from getter function in every accessing ================================================ FILE: .changeset/rich-parents-relate.md ================================================ --- "@qiankunjs/vue": patch --- fix(vue): add unmount hook to unmount application ================================================ FILE: .changeset/selfish-lamps-thank.md ================================================ --- "qiankun": patch --- fix: should re-init container while app remounted from cache ================================================ FILE: .changeset/serious-nails-jog.md ================================================ --- "@qiankunjs/bundler-plugin": patch --- fix: correct entry script identification and webpack version detection in Vue CLI 5 ================================================ FILE: .changeset/shaggy-shrimps-drum.md ================================================ --- "@qiankunjs/bundler-plugin": patch "create-qiankun": patch --- fix: improve QiankunPlugin webpack compatibility and error handling ================================================ FILE: .changeset/sharp-files-raise.md ================================================ --- "qiankun": patch --- fix(qiankun): remove premature lifecycle check to allow fallback detection ================================================ FILE: .changeset/shiny-jeans-sip.md ================================================ --- "qiankun": patch --- feat: make loadEntry and beforeLoad runs parallelly ================================================ FILE: .changeset/short-kings-explain.md ================================================ --- "qiankun": patch --- ✨add registerMicroApps api ================================================ FILE: .changeset/shy-mayflies-shave.md ================================================ --- "@qiankunjs/loader": patch "@qiankunjs/shared": patch --- feat: change script src before it execute thus we can be more consistent with the native browser logic ================================================ FILE: .changeset/silly-books-complain.md ================================================ --- "@qiankunjs/sandbox": patch --- feat(sandbox): micro app mounting should wait unit rebuilding link element loaded to avoid unstyleed content flash ================================================ FILE: .changeset/slow-timers-heal.md ================================================ --- "@qiankunjs/sandbox": patch --- fix(sandbox): createElement hijack must be paired to avoid rewriting leak ================================================ FILE: .changeset/small-experts-hug.md ================================================ --- "@qiankunjs/sandbox": patch --- feat: support addEventListener with once options to avoid memory leak ================================================ FILE: .changeset/smart-guests-jam.md ================================================ --- "@qiankunjs/sandbox": patch --- chore: optimize code ================================================ FILE: .changeset/smart-scissors-press.md ================================================ --- 'qiankun': patch '@qiankunjs/sandbox': patch --- feat: optimize lifecycle validate log ================================================ FILE: .changeset/smart-scissors-sell.md ================================================ --- "@qiankunjs/loader": patch "qiankun": patch "@qiankunjs/sandbox": patch "@qiankunjs/shared": patch --- fix: dynamic append element should support for the same container between micro apps ================================================ FILE: .changeset/smooth-pillows-jam.md ================================================ --- "qiankun": patch "@qiankunjs/shared": patch --- feat: remove lru-cache and move wrapFetch to shared package ================================================ FILE: .changeset/sour-roses-smile.md ================================================ --- "@qiankunjs/sandbox": patch "@qiankunjs/react": patch --- feat: not rebind non-native global properties ================================================ FILE: .changeset/spotty-plums-hear.md ================================================ --- "@qiankunjs/loader": patch "@qiankunjs/shared": patch --- feat: improve fetch error message by prepending url ================================================ FILE: .changeset/stale-dolls-push.md ================================================ --- "@qiankunjs/shared": patch --- feat(transpiler): assets transpiler should work well while sandbox disabled ================================================ FILE: .changeset/strong-rocks-sneeze.md ================================================ --- "@qiankunjs/loader": patch "@qiankunjs/sandbox": patch "@qiankunjs/shared": patch --- fix: defer scripts should wait until html loaded ================================================ FILE: .changeset/sweet-cars-protect.md ================================================ --- "create-qiankun": patch --- fix: include template to publish field ================================================ FILE: .changeset/sweet-shoes-brake.md ================================================ --- "@qiankunjs/shared": patch --- fix: should not transform URLs that already include a protocol ================================================ FILE: .changeset/swift-squids-vanish.md ================================================ --- "@qiankunjs/sandbox": patch --- fix(sandbox): fix async script order index calculate ================================================ FILE: .changeset/tall-buttons-pretend.md ================================================ --- "create-qiankun": patch --- feat: refactor create-qiankun cli ================================================ FILE: .changeset/tasty-donkeys-relax.md ================================================ --- "qiankun": patch --- [#2823] Add legacy APIs for qiankun 3.0 ================================================ FILE: .changeset/tender-dingos-allow.md ================================================ --- "@qiankunjs/bundler-plugin": patch --- fix(webpack-plugin):fix webpack module not found during webpack-plugin build ================================================ FILE: .changeset/tender-pots-perform.md ================================================ --- "@qiankunjs/loader": patch --- fix: prefer reading script.dataset.src in script load error message ================================================ FILE: .changeset/thin-ways-allow.md ================================================ --- "@qiankunjs/loader": patch --- fix(loader): we should invoke our script load listener before its own ================================================ FILE: .changeset/three-hornets-hammer.md ================================================ --- "qiankun": patch "@qiankunjs/sandbox": patch --- fix: should invoke getContainer method to get container every time to avoid reference misordering ================================================ FILE: .changeset/tough-beers-grow.md ================================================ --- "@qiankunjs/loader": patch "qiankun": patch --- fix: transformer should be generated in every load ================================================ FILE: .changeset/tough-phones-chew.md ================================================ --- "@qiankunjs/loader": patch "qiankun": patch "@qiankunjs/sandbox": patch "@qiankunjs/shared": patch --- 🐛 compatible with webpack chunk cache logic ================================================ FILE: .changeset/twelve-donkeys-help.md ================================================ --- "@qiankunjs/sandbox": patch --- fix: should patch the container head/body element immediately rather than patch its functions with proxy ================================================ FILE: .changeset/warm-chefs-chew.md ================================================ --- "qiankun": patch --- ✨ set data-name on micro app container ================================================ FILE: .changeset/wicked-icons-type.md ================================================ --- "@qiankunjs/loader": patch "qiankun": patch "@qiankunjs/sandbox": patch "@qiankunjs/shared": patch --- feat: extract NodeTransformer type to shared package ================================================ FILE: .changeset/wise-eagles-tease.md ================================================ --- "@qiankunjs/sandbox": patch --- fix(sandbox): compatible with dynamically appending scripts to detached containers ================================================ FILE: .changeset/wise-ravens-prove.md ================================================ --- "@qiankunjs/loader": patch "qiankun": patch "@qiankunjs/sandbox": patch "@qiankunjs/shared": patch --- feat: support huge inline-script who might be split into multiple chunks during transfer ================================================ FILE: .dumirc.ts ================================================ import { defineConfig } from 'dumi'; export default defineConfig({ publicPath: process.env.NOW_DEPLOY ? '/' : '/qiankun/', base: process.env.NOW_DEPLOY ? '/' : '/qiankun', resolve: { docDirs: ['docs'], codeBlockMode: 'passive', }, locales: [ { id: 'en-US', name: 'English' }, { id: 'zh-CN', name: '中文' }, ], themeConfig: { name: 'qiankun', logo: 'https://gw.alipayobjects.com/zos/bmw-prod/8a74c1d3-16f3-4719-be63-15e467a68a24/km0cv8vn_w500_h500.png', nav: { mode: 'append', value: { 'zh-CN': [ { title: '版本公告', children: [ { title: '发布日志', link: 'https://github.com/umijs/qiankun/releases' }, { title: '升级指南', link: '/zh/cookbook#从-2x-版本升级到-3x-版本' }, { title: '2.x 版本', link: 'https://v2.qiankun.umijs.org/zh/' }, ], }, ], 'en-US': [ { title: 'Version Notice', children: [ { title: 'Changelog', link: 'https://github.com/umijs/qiankun/releases' }, { title: 'Upgrade Guide', link: '/cookbook#upgrade-from-2x-version-to-3x-version' }, { title: '2.x Version', link: 'https://v2.qiankun.umijs.org/' }, ], }, ], }, }, socialLinks: { github: 'https://github.com/umijs/qiankun', }, }, metas: [ { name: 'keywords', content: 'microfrontend, micro frontend, micro frontends, micro-frontend, micro-frontends, microservice, javascript', }, ], analytics: { ga: 'UA-157295698-1', baidu: '0f738d9b0ac90574c09183ea85bcfa2e', }, favicons: ['https://gw.alipayobjects.com/mdn/rms_655822/afts/img/A*4sIUQpcos_gAAAAAAAAAAAAAARQnAQ'], theme: { '@c-primary': '#6451AB', }, }); ================================================ FILE: .editorconfig ================================================ # http://editorconfig.org root = true [*] indent_style = space indent_size = 2 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true max_line_length = 120 [*.md] trim_trailing_whitespace = false ================================================ FILE: .eslintignore ================================================ examples dist writable-dom template # TODO not linting test files temporary __tests__/ ================================================ FILE: .eslintrc.cjs ================================================ // eslint config for js const jsConfig = { parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, extends: ['eslint:recommended', 'prettier'], rules: { 'no-else-return': ['error', { allowElseIf: false }], 'object-shorthand': ['error', 'properties'], 'no-shadow': 'off', }, }; // eslint config for cjs const cjsConfig = { parserOptions: { sourceType: 'script' }, env: { node: true }, }; // eslint config for ts const tsConfig = { extends: ['plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended-requiring-type-checking'], parserOptions: { tsconfigRootDir: __dirname, project: ['./tsconfig.eslint.json', './packages/**/tsconfig.json'], }, rules: { '@typescript-eslint/no-unnecessary-condition': 'error', '@typescript-eslint/no-explicit-any': ['error', { fixToUnknown: true }], '@typescript-eslint/consistent-type-imports': [ 'error', { prefer: 'type-imports', fixStyle: 'inline-type-imports' }, ], '@typescript-eslint/consistent-type-exports': ['error', { fixMixedExportsWithInlineTypeSpecifier: true }], '@typescript-eslint/require-await': 'off', '@typescript-eslint/array-type': ['error', { default: 'array-simple' }], '@typescript-eslint/no-shadow': ['error', { ignoreFunctionTypeParameterNameValueShadow: true }], '@typescript-eslint/no-misused-promises': [ 'error', { checksVoidReturn: { returns: false, variables: false, }, }, ], '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], }, }; module.exports = { root: true, ...jsConfig, overrides: [ { files: ['*.ts', '*.tsx'], ...tsConfig, }, { files: ['*.cjs', 'packages/webpack-plugin/**/*.js'], ...cjsConfig, }, ], }; ================================================ FILE: .fatherrc.cjs ================================================ module.exports = { platform: 'browser', esm: {}, cjs: {}, sourcemap: true, extraBabelPlugins: [ [ 'babel-plugin-import', { libraryName: 'lodash', libraryDirectory: '', camel2DashComponentName: false, }, ], ], }; ================================================ FILE: .github/CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: - Demonstrating empathy and kindness toward other people - Being respectful of differing opinions, viewpoints, and experiences - Giving and gracefully accepting constructive feedback - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience - Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: - The use of sexualized language or imagery, and sexual attention or advances of any kind - Trolling, insulting or derogatory comments, and personal or political attacks - Public or private harassment - Publishing others' private information, such as a physical or email address, without their explicit permission - Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [kuitos.lau@gmail.com](mailto:kuitos.lau@gmail.com) or by creating a confidential issue in our [GitHub repository](https://github.com/umijs/qiankun/issues). All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. ================================================ FILE: .github/CONTRIBUTING.md ================================================ # Contributing to qiankun Thank you for your interest in contributing to qiankun! We welcome all types of contributions, including but not limited to: - 🐛 Bug reports - ✨ Feature suggestions - 📝 Documentation improvements - 🧪 Test cases - 💻 Code contributions ## Getting Started Before you begin contributing, please: 1. Read our [Code of Conduct](CODE_OF_CONDUCT.md) 2. Understand the [Developer Certificate of Origin (DCO)](https://developercertificate.org/) requirement - **all commits must be signed off** 3. Check existing [Issues](https://github.com/umijs/qiankun/issues) and [Pull Requests](https://github.com/umijs/qiankun/pulls) 4. Understand the project's technical architecture and code style ## Development Environment Setup ### Requirements - Node.js >= 16.0.0 - pnpm@9.15.0 (we use pnpm as the package manager, exact version specified in package.json) ### Local Development 1. Fork this repository 2. Clone to local: ```bash git clone https://github.com/YOUR_USERNAME/qiankun.git cd qiankun ``` 3. Install dependencies: ```bash pnpm install ``` 4. Run tests: ```bash pnpm test ``` 5. Build the project: ```bash pnpm run build ``` ### Development Scripts - `pnpm run build` - Build all packages using father - `pnpm test` - Run tests using vitest - `pnpm run eslint` - Code style check with TypeScript ESLint rules - `pnpm run prettier` - Format code with prettier - `pnpm run prettier:check` - Check code formatting - `pnpm run ci` - Complete CI pipeline (build + lint + format check) - `pnpm run clean` - Clean node_modules and build artifacts - `pnpm run start:example` - Start example applications (main + react15) - `pnpm run docs:dev` - Start documentation development server - `pnpm run docs:build` - Build documentation - `pnpm run prepare` - Setup husky git hooks and dumi ## Code Contribution Workflow ### 1. Create an Issue For major features or breaking changes, please create an Issue first to discuss: - Clearly describe the problem or feature request - Provide relevant context information - For bugs, provide reproduction steps ### 2. Create a Branch Create a feature branch based on the `next` branch (default development branch): ```bash git checkout next git pull origin next git checkout -b feature/your-feature-name # or git checkout -b fix/bug-description ``` Note: The project uses `next` as the main development branch (as configured in changeset). ### 3. Code Development #### TypeScript Code Standards - **Strict Type Checking**: - Enable TypeScript strict mode (already configured in tsconfig.json) - Avoid using `any` type (ESLint rule enforces `fixToUnknown: true`) - Use `unknown` instead of `any` when type is uncertain - Enable all strict options: `strictNullChecks`, `strictFunctionTypes`, `noImplicitReturns`, etc. - **Type Import/Export**: - Use `import type` for type-only imports (enforced by ESLint) - Use consistent type exports with inline type specifiers - **Naming Conventions**: - Variables and functions use `camelCase` - Classes and interfaces use `PascalCase` - Constants use `UPPER_SNAKE_CASE` - File names use `kebab-case` or `PascalCase` - **Comment Standards**: - Use clear comments to explain complex logic - Use JSDoc format for public APIs - Provide usage examples for complex type definitions - Add type annotations where TypeScript inference is insufficient #### Code Style - Follow the project's ESLint configuration - Use 2-space indentation - No trailing whitespace - Keep one empty line at the end of files #### Architecture Principles - Follow SOLID principles - Maintain high cohesion and low coupling - Prefer composition over inheritance - Ensure code testability ### 4. Write Tests - Write corresponding test cases for new features using **vitest** - Ensure existing tests are not broken - Maintain reasonable test coverage - Place test files in appropriate `__tests__` directories or alongside source files with `.test.ts` suffix - Use vitest's global test APIs (no need to import `describe`, `it`, `expect`) - Run tests with `pnpm test` or `pnpm -r run test` for all packages ### 5. Commit Code #### Developer Certificate of Origin (DCO) All commits **MUST** be signed off with the Developer Certificate of Origin (DCO). This is a legal requirement to ensure you have the right to contribute your code. **How to sign off commits:** 1. **Manual sign-off for each commit:** ```bash git commit -s -m "feat: add new feature" ``` 2. **Configure automatic sign-off:** ```bash git config user.name "Your Name" git config user.email "your.email@example.com" git config commit.gpgsign true # Optional: GPG signing ``` 3. **Sign off existing commits retroactively:** ```bash # For the last commit git commit --amend --signoff # For multiple commits (rebase and sign off) git rebase --signoff HEAD~n # where n is the number of commits ``` **What is DCO?** The DCO is a statement that you have the right to contribute the code and that you understand the licensing implications. Please read the full [DCO text](https://developercertificate.org/) for complete details. When you sign off a commit, you're confirming: - The contribution was created by you, or you have permission to submit it - You understand and agree that the contribution will be public - You understand the contribution is licensed under the project's license **Commit sign-off format:** Each commit message must end with a "Signed-off-by" line: ``` feat(core): implement new feature This commit adds support for custom lifecycles. Signed-off-by: Your Name ``` **⚠️ Important:** Pull requests with unsigned commits will be rejected. Make sure all your commits are properly signed off before submitting a PR. #### Commit Message Format We follow [Conventional Commits](https://www.conventionalcommits.org/) specification for commit messages. **Format:** ``` [optional scope]: [optional body] [optional footer(s)] Signed-off-by: Your Name ``` **Commit Types:** | Type | Description | Example | | ---------- | ----------------------- | --------------------------------------------- | | `feat` | New feature | `feat(sandbox): add new isolation mode` | | `fix` | Bug fix | `fix(loader): resolve script loading issue` | | `perf` | Performance improvement | `perf(core): optimize app loading speed` | | `docs` | Documentation only | `docs: update API reference` | | `style` | Code formatting | `style: fix ESLint warnings` | | `refactor` | Code refactoring | `refactor(utils): simplify helper functions` | | `test` | Adding tests | `test(core): add unit tests for loadMicroApp` | | `chore` | Build/tooling changes | `chore: update dependencies` | | `ci` | CI configuration | `ci: add workflow for releases` | | `build` | Build system changes | `build: configure webpack` | | `revert` | Revert previous commit | `revert: feat(api): add user auth` | **Breaking Changes:** For breaking changes, use one of these approaches: 1. Add `!` after type: `feat!: redesign loadMicroApp API` 2. Add `BREAKING CHANGE:` in footer: ``` feat(api): add new authentication method BREAKING CHANGE: The old auth method is no longer supported. Migration guide: replace loadMicroApp() with registerMicroApps() ``` **Scope Guidelines:** - `core`: Core qiankun functionality - `sandbox`: Sandbox isolation - `loader`: Resource loading - `utils`: Utility functions - `types`: TypeScript type definitions - `docs`: Documentation - `test`: Testing related - `ci`: CI/CD related - `build`: Build system related ### 6. Create Pull Request 1. Push your branch to your fork: ```bash git push origin feature/your-feature-name ``` 2. Create a Pull Request on GitHub 3. PR title and description should be clear: - Title should briefly describe the changes - Description should detail: - Motivation and purpose of changes - Main changes made - Testing status - Any breaking changes - Related issue numbers 4. Ensure all checks pass: - **DCO sign-off check**: All commits must be signed off - ESLint code style checks with TypeScript rules - TypeScript strict type checks (no `any` types, strict null checks) - All vitest tests pass - Test coverage meets requirements - Prettier formatting checks - Build passes with father bundler ## Documentation Contributions Documentation improvements are equally important: - Fix errors or outdated information in documentation - Improve clarity of existing documentation - Add examples and use cases - Translate documentation to other languages ## Bug Reports When submitting bug reports, please include: - **Clear Title**: Briefly describe the issue - **Environment Information**: - Node.js version (>= 16.0.0) - pnpm version (should be 9.15.0) - qiankun version - Browser version - Operating system - Framework versions (React, Vue, Angular, etc.) - Build tool versions (webpack, vite, etc.) - Related dependency versions - **Reproduction Steps**: Detailed step-by-step instructions - **Expected Behavior**: Describe what should happen - **Actual Behavior**: Describe what actually happened - **Minimal Example**: Provide minimal code that reproduces the issue - **Relevant Logs**: Error messages, stack traces, etc. ## Feature Requests When submitting feature requests, please: - Clearly describe the needed functionality - Explain why this feature is needed - Provide use cases and examples - Consider alternative solutions - Discuss impact on existing APIs ## Release Process This project uses [Changesets](https://github.com/changesets/changesets) for version management and publishing: ### For Maintainers 1. **Creating a changeset**: After merging PR, create a changeset: ```bash npx changeset ``` 2. **Release process**: ```bash # Alpha release (for testing) pnpm run prerelease:alpha # Enter pre-release mode and create changeset pnpm run release:alpha # Build and publish alpha version # Production release npx changeset version # Update package versions pnpm run build # Build all packages pnpm run ci:publish # Publish to npm ``` ### Build System - **Build Tool**: [father](https://github.com/umijs/father) - A library build tool - **Monorepo**: pnpm workspaces with packages in `packages/` directory - **Documentation**: [dumi](https://d.umijs.org/) for documentation site - **Testing**: [vitest](https://vitest.dev/) for unit testing - **Code Quality**: ESLint + Prettier + TypeScript strict mode ## Getting Help If you have any questions, you can get help through: - Create a [GitHub Issue](https://github.com/umijs/qiankun/issues) - Check existing [documentation](https://qiankun.umijs.org/) - Join our community discussions ## Acknowledgments Thanks to all developers who contribute to the qiankun project! Your contributions make this project better. --- Thank you again for your contribution! 🎉 ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: 'Bug report' about: 'Report a bug to help us improve' title: '' labels: '' assignees: '' --- ## What happens? A clear and concise description of what the bug is. ## Mini Showcase Repository(REQUIRED) > Provide a mini GitHub repository which can reproduce the issue. ## How To Reproduce **Steps to reproduce the behavior:** 1. 2. **Expected behavior** 1. 2. ## Context - **qiankun Version**: - **Platform Version**: - **Browser Version**: ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report_cn.md ================================================ --- name: '缺陷问题反馈' about: '反馈问题以帮助我们改进' title: '[Bug]请遵循下文模板提交问题,否则您的问题会被关闭' labels: '' assignees: '' --- # 提问之前强烈建立您能先阅读一下[《如何正确的提出一个 Issue》](https://github.com/umijs/qiankun/issues/1115) ## What happens? ## 最小可复现仓库 为节约大家的时间,无复现步骤的 ISSUE 会被关闭,提供之后再 REOPEN ## 复现步骤,错误日志以及相关配置 ## 相关环境信息 - **qiankun 版本** - **浏览器版本**: - **操作系统**: ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: 'Feature request' about: 'Suggest an idea for this project' title: '[Feature Request] say something' labels: '' assignees: '' --- ## Background A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] ## Proposal Describe the solution you'd like, better to provide some pseudo code. ## Additional context Add any other context or screenshots about the feature request here. ================================================ FILE: .github/ISSUE_TEMPLATE/rfc_cn.md ================================================ --- name: 'RFC Proposals' about: 'Provide a solution for this project' title: '[RFC] say something' labels: 'type: proposals' assignees: '' --- ## 背景 > 描述你希望解决的问题的现状,附上相关的 issue 地址 ## 思路 > 描述大概的解决思路,可以包含 API 设计和伪代码等 ## 跟进 - [ ] some task - [ ] PR URL ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ ##### Checklist - [ ] `npm test` passes - [ ] tests are included - [ ] documentation is changed or added - [ ] commit message follows commit guidelines ##### Description of change - any feature? - close https://github.com/umijs/qiankun/ISSUE_URL ================================================ FILE: .github/workflows/announcement-notify.yml ================================================ name: Annoucement Notify on: discussion: types: [created] jobs: notify: runs-on: ubuntu-latest steps: - name: Send DingGroup1 Anouncement Notify uses: zcong1993/actions-ding@master if: github.event.discussion.category.name == 'Announcements' with: dingToken: ${{ secrets.DING_GROUP_1_TOKEN }} secret: ${{ secrets.DING_GROUP_1_SIGN }} body: | { "msgtype": "markdown", "markdown": { "title": "Qiankun News", "text": "# 新闻播报📢 [${{github.event.discussion.title}}](${{github.event.discussion.html_url}}) \n${{github.event.discussion.body}}", } } - name: Send DingGroup2 Anouncement Notify uses: zcong1993/actions-ding@master if: github.event.discussion.category.name == 'Announcements' with: dingToken: ${{ secrets.DING_GROUP_2_TOKEN }} secret: ${{ secrets.DING_GROUP_2_SIGN }} body: | { "msgtype": "markdown", "markdown": { "title": "Qiankun News", "text": "# 新闻播报📢 [${{github.event.discussion.title}}](${{github.event.discussion.html_url}}) \n${{github.event.discussion.body}}", } } - name: Send DingGroup3 Anouncement Notify uses: zcong1993/actions-ding@master if: github.event.discussion.category.name == 'Announcements' with: dingToken: ${{ secrets.DING_GROUP_3_TOKEN }} secret: ${{ secrets.DING_GROUP_3_SIGN }} body: | { "msgtype": "markdown", "markdown": { "title": "Qiankun News", "text": "# 新闻播报📢 [${{github.event.discussion.title}}](${{github.event.discussion.html_url}}) \n${{github.event.discussion.body}}", } } - name: Send DingGroup4 Anouncement Notify uses: zcong1993/actions-ding@master if: github.event.discussion.category.name == 'Announcements' with: dingToken: ${{ secrets.DING_GROUP_4_TOKEN }} secret: ${{ secrets.DING_GROUP_4_SIGN }} body: | { "msgtype": "markdown", "markdown": { "title": "Qiankun News", "text": "# 新闻播报📢 [${{github.event.discussion.title}}](${{github.event.discussion.html_url}}) \n${{github.event.discussion.body}}", } } - name: Send DingGroup5 Anouncement Notify uses: zcong1993/actions-ding@master if: github.event.discussion.category.name == 'Announcements' with: dingToken: ${{ secrets.DING_GROUP_5_TOKEN }} secret: ${{ secrets.DING_GROUP_5_SIGN }} body: | { "msgtype": "markdown", "markdown": { "title": "Qiankun News", "text": "# 新闻播报📢 [${{github.event.discussion.title}}](${{github.event.discussion.html_url}}) \n${{github.event.discussion.body}}", } } - name: Send DingGroup6 Anouncement Notify uses: zcong1993/actions-ding@master if: github.event.discussion.category.name == 'Announcements' with: dingToken: ${{ secrets.DING_GROUP_6_TOKEN }} secret: ${{ secrets.DING_GROUP_6_SIGN }} body: | { "msgtype": "markdown", "markdown": { "title": "Qiankun News", "text": "# 新闻播报📢 [${{github.event.discussion.title}}](${{github.event.discussion.html_url}}) \n${{github.event.discussion.body}}", } } - name: Send DingGroup7 Anouncement Notify uses: zcong1993/actions-ding@master if: github.event.discussion.category.name == 'Announcements' with: dingToken: ${{ secrets.DING_GROUP_7_TOKEN }} secret: ${{ secrets.DING_GROUP_7_SIGN }} body: | { "msgtype": "markdown", "markdown": { "title": "Qiankun News", "text": "# 新闻播报📢 [${{github.event.discussion.title}}](${{github.event.discussion.html_url}}) \n${{github.event.discussion.body}}", } } ================================================ FILE: .github/workflows/changeset-prerelease.yml ================================================ name: Changesets on: push: branches: - next permissions: id-token: write contents: write pull-requests: write jobs: changelog: timeout-minutes: 15 runs-on: ubuntu-latest environment: changeset-release steps: - uses: actions/checkout@v4 - name: Setup PNPM uses: pnpm/action-setup@v2 - name: Setup Node uses: actions/setup-node@v4 with: node-version: 24 cache: "pnpm" registry-url: "https://registry.npmjs.org" - name: Install dependencies run: pnpm install - name: Build Packages run: pnpm run build - name: Create Release Pull Request or Publish id: changesets uses: changesets/action@v1 with: commit: "chore: update versions" title: "chore: update versions" publish: pnpm ci:publish createGithubReleases: false env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NPM_CONFIG_PROVENANCE: true - name: Create Unified GitHub Release if: steps.changesets.outputs.published == 'true' run: | VERSION=$(jq -r '.version' packages/qiankun/package.json) TAG="v${VERSION}" node scripts/generate-release-notes.mjs '${{ steps.changesets.outputs.publishedPackages }}' > /tmp/release-notes.md gh release create "${TAG}" \ --title "${TAG}" \ --notes-file /tmp/release-notes.md \ --prerelease env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: pull_request: push: branches: - master - next - 1.x jobs: build-check-and-lint: runs-on: ubuntu-latest strategy: matrix: node-version: [lts/*, latest] steps: - uses: actions/checkout@v4 - name: Install pnpm uses: pnpm/action-setup@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} cache: "pnpm" - run: corepack enable # https://nodejs.org/api/corepack.html - run: pnpm install - name: TS Build Check run: pnpm run build - name: Run eslint run: pnpm run eslint - name: Run prettier run: pnpm run prettier:check - name: Doc Build check run: pnpm run docs:build unit-test: runs-on: ubuntu-latest strategy: matrix: node-version: [lts/*, latest] steps: - uses: actions/checkout@v4 - name: Install pnpm uses: pnpm/action-setup@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} cache: "pnpm" - run: corepack enable # https://nodejs.org/api/corepack.html - run: pnpm install - name: Run unit test run: pnpm run test ================================================ FILE: .github/workflows/emoji-helper.yml ================================================ name: Emoji Helper on: release: types: [published] jobs: emoji: runs-on: ubuntu-latest steps: - uses: actions-cool/emoji-helper@v1.0.0 with: type: "release" emoji: "+1, laugh, heart, hooray, rocket, eyes" ================================================ FILE: .github/workflows/github-pages.yml ================================================ name: Qiankun Github Pages Deploy on: push: branches: - master jobs: deploy-gh-pages: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: "lts/*" check-latest: true registry-url: "https://registry.npmjs.org" cache: "pnpm" - name: Install dependencies run: | corepack enable pnpm install --frozen-lockfile - name: Build docs run: pnpm run docs:build - name: Deploy uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./dist ================================================ FILE: .github/workflows/issue-close-inactive.yml ================================================ name: Issue Close Inactive on: schedule: - cron: "0 0 * * *" jobs: close-issues: runs-on: ubuntu-latest steps: - name: close inactive issue without reprodction uses: actions-cool/issues-helper@v2.2.1 with: actions: "close-issues" labels: "Need Reproduction" inactive-day: 30 body: | Since the issue was labeled with `Need Reproduction`, but no response in 30 days. This issue will be close. If you have any questions, you can comment and reply. 由于该 issue 被标记为需要可复现步骤,却 30 天未收到回应。现关闭 issue,若有任何问题,可评论回复。 - name: close inactive issue not use template uses: actions-cool/issues-helper@v2.2.1 with: actions: "close-issues" labels: "pls use issue template" inactive-day: 30 body: | Since the issue was labeled with `pls use issue template`, but no response in 30 days. This issue will be close. If you have any questions, you can comment and reply. 由于该 issue 被标记为需要使用模板,却 30 天未收到回应。现关闭 issue,若有任何问题,可评论回复。 - name: close inactive issue out of scope uses: actions-cool/issues-helper@v2.2.1 with: actions: "close-issues" labels: "out-of-scope" inactive-day: 30 body: | Since the issue was labeled with `out-of-scope`, but no response in 30 days. This issue will be close. If you have any questions, you can comment and reply. 由于该 issue 被标记为与本项目无关,却 30 天未收到回应。现关闭 issue,若有任何问题,可评论回复。 - name: close inactive issue uses: actions-cool/issues-helper@v2.2.1 with: actions: "close-issues" labels: "inactive" inactive-day: 7 body: | Since the issue was labeled with `inactive`, but no response in 7 days. This issue will be close. If you have any questions, you can comment and reply. 由于该 issue 被标记为不活跃,且 7 天未收到回应。现关闭 issue,若有任何问题,可评论回复。 ================================================ FILE: .github/workflows/issue-reply.yml ================================================ name: Issue Reply on: issues: types: [labeled] jobs: reply-helper: runs-on: ubuntu-latest steps: - name: help wanted if: github.event.label.name == 'pr welcome' || github.event.label.name == 'help wanted' uses: actions-cool/issues-helper@v2.0.0 with: actions: "create-comment" issue-number: ${{ github.event.issue.number }} body: | Hello @${{ github.event.issue.user.login }}. We totally like your proposal/feedback, welcome to send us a Pull Request for it. Please be sure to fill in the default template in the Pull Request, provide changelog/documentation/test cases if needed and make sure CI passed, we will review it soon. We appreciate your effort in advance and looking forward to your contribution! 你好 @${{ github.event.issue.user.login }},我们完全同意你的提议/反馈,欢迎直接在此仓库创建一个 Pull Request 来解决这个问题。请务必填写 Pull Request 内的预设模板,提供改动所需相应的 changelog、测试用例、文档等,并确保 CI 通过,我们会尽快进行 Review,提前感谢和期待您的贡献。 - name: pls use issue template if: github.event.label.name == 'pls use issue template' uses: actions-cool/issues-helper@v2.0.0 with: actions: "create-comment, close-issue" issue-number: ${{ github.event.issue.number }} body: | Hello @${{ github.event.issue.user.login }}. To save both time, please use the issue template to report. This issue will be closed. 你好 @${{ github.event.issue.user.login }},为节约大家的时间,请使用 issue 模板反馈问题。该 issue 将要被关闭。 - name: Need Reproduction if: github.event.label.name == 'Need Reproduction' uses: actions-cool/issues-helper@v2.0.0 with: actions: "create-comment" issue-number: ${{ github.event.issue.number }} body: | Hello @${{ github.event.issue.user.login }}. In order to facilitate location and troubleshooting, we need you to provide a realistic example. Please forking these link [codesandbox](https://codesandbox.io/) or clone [qiankun examples](https://github.com/umijs/qiankun/tree/master/examples) to your GitHub repository. 你好 @${{ github.event.issue.user.login }}, 为了方便定位和排查问题,我们需要你提供一个重现实例,请提供一个尽可能精简的链接 [codesandbox](https://codesandbox.io/) 或直接 clone [qiankun examples](https://github.com/umijs/qiankun/tree/master/examples),并上传到你的 GitHub 仓库。 ![](https://gw.alipayobjects.com/zos/antfincdn/y9kwg7DVCd/reproduce.gif) ================================================ FILE: .github/workflows/publish-1.x.yml ================================================ name: Publish 1.x Version on: push: tags: - v1.* jobs: publish: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: "lts/*" check-latest: true registry-url: "https://registry.npmjs.org" cache: "pnpm" - run: corepack enable - run: pnpm install --frozen-lockfile - run: pnpm publish --tag qiankun1 env: NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} ================================================ FILE: .github/workflows/publish-latest.yml ================================================ name: Publish Latest Version on: push: tags: - v2.* jobs: publish: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: "lts/*" check-latest: true registry-url: "https://registry.npmjs.org" cache: "pnpm" - run: corepack enable - run: pnpm install --frozen-lockfile - run: pnpm publish --tag latest env: NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} ================================================ FILE: .github/workflows/release-notify.yml ================================================ name: Release Notify on: release: types: [published] jobs: notify: runs-on: ubuntu-latest steps: - name: Send DingGroup1 Notify uses: zcong1993/actions-ding@master with: dingToken: ${{ secrets.DING_GROUP_1_TOKEN }} secret: ${{ secrets.DING_GROUP_1_SIGN }} body: | { "msgtype": "markdown", "markdown": { "title": "qiankun ${{github.event.release.tag_name}} 发布公告", "text": "# qiankun [${{github.event.release.tag_name}}](${{github.event.release.html_url}}) 发布公告\n${{github.event.release.body}}", } } - name: Send DingGroup2 Notify uses: zcong1993/actions-ding@master with: dingToken: ${{ secrets.DING_GROUP_2_TOKEN }} secret: ${{ secrets.DING_GROUP_2_SIGN }} body: | { "msgtype": "markdown", "markdown": { "title": "qiankun ${{github.event.release.tag_name}} 发布公告", "text": "# qiankun [${{github.event.release.tag_name}}](${{github.event.release.html_url}}) 发布公告\n${{github.event.release.body}}", } } - name: Send DingGroup3 Notify uses: zcong1993/actions-ding@master with: dingToken: ${{ secrets.DING_GROUP_3_TOKEN }} secret: ${{ secrets.DING_GROUP_3_SIGN }} body: | { "msgtype": "markdown", "markdown": { "title": "qiankun ${{github.event.release.tag_name}} 发布公告", "text": "# qiankun [${{github.event.release.tag_name}}](${{github.event.release.html_url}}) 发布公告\n${{github.event.release.body}}", } } - name: Send DingGroup4 Notify uses: zcong1993/actions-ding@master with: dingToken: ${{ secrets.DING_GROUP_4_TOKEN }} secret: ${{ secrets.DING_GROUP_4_SIGN }} body: | { "msgtype": "markdown", "markdown": { "title": "qiankun ${{github.event.release.tag_name}} 发布公告", "text": "# qiankun [${{github.event.release.tag_name}}](${{github.event.release.html_url}}) 发布公告\n${{github.event.release.body}}", } } - name: Send DingGroup5 Notify uses: zcong1993/actions-ding@master with: dingToken: ${{ secrets.DING_GROUP_5_TOKEN }} secret: ${{ secrets.DING_GROUP_5_SIGN }} body: | { "msgtype": "markdown", "markdown": { "title": "qiankun ${{github.event.release.tag_name}} 发布公告", "text": "# qiankun [${{github.event.release.tag_name}}](${{github.event.release.html_url}}) 发布公告\n${{github.event.release.body}}", } } - name: Send DingGroup6 Notify uses: zcong1993/actions-ding@master with: dingToken: ${{ secrets.DING_GROUP_6_TOKEN }} secret: ${{ secrets.DING_GROUP_6_SIGN }} body: | { "msgtype": "markdown", "markdown": { "title": "qiankun ${{github.event.release.tag_name}} 发布公告", "text": "# qiankun [${{github.event.release.tag_name}}](${{github.event.release.html_url}}) 发布公告\n${{github.event.release.body}}", } } - name: Send DingGroup7 Notify uses: zcong1993/actions-ding@master with: dingToken: ${{ secrets.DING_GROUP_7_TOKEN }} secret: ${{ secrets.DING_GROUP_7_SIGN }} body: | { "msgtype": "markdown", "markdown": { "title": "qiankun ${{github.event.release.tag_name}} 发布公告", "text": "# qiankun [${{github.event.release.tag_name}}](${{github.event.release.html_url}}) 发布公告\n${{github.event.release.body}}", } } - name: Send DingGroupInc Notify uses: zcong1993/actions-ding@master with: dingToken: ${{ secrets.DING_GROUP_INC_TOKEN }} secret: ${{ secrets.DING_GROUP_INC_SIGN }} body: | { "msgtype": "markdown", "markdown": { "title": "qiankun ${{github.event.release.tag_name}} 发布公告", "text": "# qiankun [${{github.event.release.tag_name}}](${{github.event.release.html_url}}) 发布公告\n${{github.event.release.body}}", } } ================================================ FILE: .gitignore ================================================ pids logs node_modules npm-debug.log coverage/ run dist .DS_Store .nyc_output config.local.js .umi .umi-production .idea/ .vscode/ .cache yarn.lock es lib package-lock.json .eslintcache .history .worktrees .now .pnpm-store *.log packages/qiankun/src/version.ts packages/sandbox/src/core/sandbox/globals.ts .dumi/tmp .dumi/tmp-test .dumi/tmp-production ================================================ FILE: .husky/commit-msg ================================================ #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" npx --no-install commitlint --edit $1 ================================================ FILE: .husky/pre-commit ================================================ #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" npx lint-staged ================================================ FILE: .husky/pre-push ================================================ #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" #pnpm run test ================================================ FILE: .prettierignore ================================================ /test/fixtures **/*.gif /dist /docs /es /lib /coverage .cache examples .umi .dumi .umi-production .changeset/* pnpm-lock.yaml packages/sandbox/src/core/globals.ts packages/create-qiankun/template # changeset 会修改这个文件,导致 prettier:check 失败 packages/**/*/package.json ================================================ FILE: .prettierrc ================================================ { "singleQuote": true, "trailingComma": "all", "proseWrap": "never", "overrides": [ { "files": ["*.json", ".prettierrc"], "options": { "parser": "json" } }, { "files": ["*.{yaml,yml}"], "options": { "singleQuote": false } } ] } ================================================ FILE: AGENTS.md ================================================ # QIANKUN PROJECT KNOWLEDGE BASE **Generated:** 2026-01-10 **Commit:** 058166b6 **Branch:** next ## OVERVIEW Qiankun is a micro-frontend framework built on single-spa, providing HTML entry, JS sandbox, and style isolation. Monorepo managed by pnpm with `father` build tool. ## STRUCTURE ``` qiankun/ ├── packages/ │ ├── qiankun/ # Main facade - re-exports from loader/sandbox/shared │ ├── sandbox/ # JS isolation via Proxy membrane + Compartment (SEE packages/sandbox/AGENTS.md) │ ├── loader/ # Streaming HTML entry loader (SEE packages/loader/AGENTS.md) │ ├── shared/ # fetch-utils, asset transpilers, module-resolver (SEE packages/shared/AGENTS.md) │ ├── ui-bindings/ # React/Vue components │ ├── bundler-plugin/ # Entry point configuration plugin │ └── create-qiankun/ # CLI scaffolding tool ├── examples/ # Integration examples (NOT in workspace currently) └── docs/ # VitePress documentation ``` ## WHERE TO LOOK | Task | Location | Notes | | --- | --- | --- | | Core APIs (registerMicroApps, loadMicroApp) | `packages/qiankun/src/apis/` | Thin wrappers around loader/sandbox | | App loading lifecycle | `packages/qiankun/src/core/loadApp.ts` | Orchestrates sandbox+loader+hooks | | JS sandbox implementation | `packages/sandbox/src/core/` | Membrane, Compartment, StandardSandbox | | HTML streaming loader | `packages/loader/src/index.ts` | Uses writable-dom for streaming | | Script/link transpilation | `packages/shared/src/assets-transpilers/` | Blob URL sandboxing | | Fetch enhancements | `packages/shared/src/fetch-utils/` | Cache, retry, error handling | | Dependency sharing | `packages/shared/src/module-resolver/` | Semver-based matching | | React/Vue bindings | `packages/ui-bindings/{react,vue}/` | `` component | ## CODE MAP ### Package Dependencies (internal) ``` qiankun (facade) ├── @qiankunjs/loader │ ├── @qiankunjs/sandbox │ └── @qiankunjs/shared ├── @qiankunjs/sandbox │ └── @qiankunjs/shared └── @qiankunjs/shared (base utilities) ``` ### Key Entry Points | Package | Entry | Primary Exports | | ------------------ | -------------- | --------------------------------------------------- | | qiankun | `src/index.ts` | `registerMicroApps`, `start`, `loadMicroApp` | | @qiankunjs/sandbox | `src/index.ts` | `createSandboxContainer`, `StandardSandbox` | | @qiankunjs/loader | `src/index.ts` | `loadEntry` | | @qiankunjs/shared | `src/index.ts` | `transpileScript`, `makeFetchCacheable`, `Deferred` | ## CONVENTIONS ### TypeScript (STRICT - enforced by eslint) - `@typescript-eslint/no-explicit-any`: ERROR - use `unknown` instead - `@typescript-eslint/consistent-type-imports`: inline-type-imports required - `@typescript-eslint/no-unnecessary-condition`: ERROR - Path aliases: `@qiankunjs/*` → `packages/*/src` ### Code Style - No `as any`, `@ts-ignore`, `@ts-expect-error` - Unused vars: prefix with `_` (e.g., `_unused`) - Type exports: use `export type { X }` inline syntax ### Build - Tool: `father` (UmiJS ecosystem) - Output: dual ESM + CJS in `dist/` - No `exports` field in package.json (uses `main`/`module`/`types`) ## ANTI-PATTERNS (THIS PROJECT) - **NEVER** add more than 1 `entry` attribute script per HTML entry - **NEVER** use `!important` in CSS unless absolutely necessary (breaks isolation) - **ALWAYS** unmount micro-apps when finished (`loadMicroApp` returns unmount handle) - **AVOID** global variables in micro-apps - sandbox tracks `latestSetProp` for exports ### Technical Debt (from codebase comments) - `FIXME` in `loadApp.ts`: async execution order coordination - `FIXME` in sandbox: System.js scope escape via indirect eval - `TODO`: Snapshot sandbox and GC not yet implemented ## COMMANDS ```bash pnpm install # Install all workspace deps pnpm run build # Build all packages (father build) pnpm run test # Run vitest across workspace pnpm run eslint # Lint packages/ pnpm run ci # Full CI: build + eslint + prettier:check # Development pnpm run start:example # Build + run example apps pnpm run docs:dev # VitePress dev server # Release (changesets) pnpm run prerelease:alpha # Enter alpha, version bump pnpm run release:alpha # Build, publish, exit alpha ``` ## NOTES - **v3.0 Active Development**: Check roadmap at github.com/umijs/qiankun/discussions/1378 - **Streaming Architecture**: v3 uses `writable-dom` for incremental HTML parsing - **Head Virtualization**: `` → `` for isolation - **Blob URL Execution**: Scripts wrapped and executed via `URL.createObjectURL` - **Test Environment**: Vitest + happy-dom; edge-runtime for fetch tests ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2019 Kuitos Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================

qiankun logo

npm version coverage npm downloads build status dumi

# qiankun(乾坤) > [!WARNING] 🚧 qiankun 3.0 is currently under active development. Check out the [Roadmap](https://github.com/umijs/qiankun/discussions/1378) for more details. > In Chinese, `qian(乾)` means heaven and `kun(坤)` earth. `qiankun` is the universe. Qiankun enables you and your teams to build next-generation and enterprise-ready web applications leveraging [Micro Frontends](https://micro-frontends.org/). It is inspired by and based on [single-spa](https://github.com/single-spa/single-spa). ## 🤔 Motivation A quick recap about the concept of `Micro Frontends`: > Techniques, strategies and recipes for building a **modern web app** with **multiple teams** using **different JavaScript frameworks**. — [Micro Frontends](https://micro-frontends.org/) Qiankun was birthed internally in our group during the time web app development by distributed teams had turned to complete chaos. We faced every problem micro frontend was conceived to solve, so naturally, it became part of our solution. The path was never easy, we stepped on every challenge there could possibly be. Just to name a few: - In what form do micro-apps publish static resources? - How does the framework integrate individual micro-apps? - How to ensure that sub-applications are isolated from one another (development independence and deployment independence) and runtime sandboxed? - Performance issues? What about public dependencies? - The list goes on long ... After solving these common problems of micro frontends and lots of polishing and testing, we extracted the minimal viable framework of our solution, and named it `qiankun`, as it can contain and serve anything. Not long after, it became the cornerstone of hundreds of our web applications in production, and we decided to open-source it to save you the suffering. **TLDR: Qiankun is probably the most complete micro-frontend solution you ever met🧐.** ## :sparkles: Features Qiankun inherits many benefits from [single-spa](https://github.com/single-spa/single-spa): - 📦 **Micro-apps Independent Deployment** - 🛴 **Lazy Load** - 📱 **Technology Agnostic** And on top of these, it offers: - 💃 **Elegant API** - 💪 **HTML Entry Access Mode** - 🛡 **Style Isolation** - 🧳 **JS Sandbox** - ⚡ **Prefetch Assets** - 🔌 **[Umi Plugin](https://github.com/umijs/plugins/tree/master/packages/plugin-qiankun) Integration** ## Packages | Package | Version (click for changelogs) | | --- | :-- | | [qiankun](packages/qiankun) | [![qiankun version](https://img.shields.io/npm/v/qiankun/next.svg?style=flat-square)](packages/qiankun/CHANGELOG.md) | | [@qiankunjs/loader](packages/loader) | [![loader version](https://img.shields.io/npm/v/@qiankunjs/loader/rc.svg?style=flat-square)](packages/loader/CHANGELOG.md) | | [@qiankunjs/sandbox](packages/sandbox) | [![sandbox version](https://img.shields.io/npm/v/@qiankunjs/sandbox/rc.svg?style=flat-square)](packages/sandbox/CHANGELOG.md) | | [@qiankunjs/shared](packages/shared) | [![shared version](https://img.shields.io/npm/v/@qiankunjs/shared/rc.svg?style=flat-square)](packages/shared/CHANGELOG.md) | | [@qiankunjs/react](packages/ui-bindings/react) | [![react version](https://img.shields.io/npm/v/@qiankunjs/react/rc.svg?style=flat-square)](packages/ui-bindings/react/CHANGELOG.md) | | [@qiankunjs/vue](packages/ui-bindings/vue) | [![vue version](https://img.shields.io/npm/v/@qiankunjs/vue/rc.svg?style=flat-square)](packages/ui-bindings/vue/CHANGELOG.md) | | [@qiankunjs/ui-shared](packages/ui-bindings/shared) | [![ui-shared version](https://img.shields.io/npm/v/@qiankunjs/ui-shared/rc.svg?style=flat-square)](packages/ui-bindings/shared/CHANGELOG.md) | | [@qiankunjs/bundler-plugin](packages/bundler-plugin) | [![bundler-plugin version](https://img.shields.io/npm/v/@qiankunjs/bundler-plugin/rc.svg?style=flat-square)](packages/bundler-plugin/CHANGELOG.md) | | [create-qiankun](packages/create-qiankun) | [![create-qiankun version](https://img.shields.io/npm/v/create-qiankun/rc.svg?style=flat-square)](packages/create-qiankun/CHANGELOG.md) | ## 📦 Installation ```shell $ yarn add qiankun # or npm i qiankun -S ``` ## 📖 Documentation You can find the Qiankun documentation [on the website](https://qiankun.umijs.org/) Check out the [Getting Started](https://qiankun.umijs.org/guide/getting-started) page for a quick overview. The documentation is divided into several sections: - [Tutorial](https://qiankun.umijs.org/cookbook) - [API Reference](https://qiankun.umijs.org/api) - [FAQ](https://qiankun.umijs.org/faq) - [Community](https://qiankun.umijs.org/#-community) ## 💿 Examples Inside the `examples` folder, there is a sample Shell app and multiple mounted Micro FE apps. To get it running, first clone `qiankun`: ```shell $ git clone https://github.com/umijs/qiankun.git $ cd qiankun ``` Now install and run the example: ```shell $ pnpm install $ pnpm run examples:install $ pnpm run examples:start ``` Visit `http://localhost:7099`. ![](./examples/example.gif) ## 🎯 Roadmap See [Qiankun 3.0 Roadmap](https://github.com/umijs/qiankun/discussions/1378) ## 🤝 Contributing [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/umijs/qiankun) See [contributing guide](./CONTRIBUTING.md). ## 👥 Contributors Thanks to all the contributors! contributors ## 🎁 Acknowledgements - [single-spa](https://github.com/single-spa/single-spa) What an awesome meta-framework for micro-frontends! - [writable-dom](https://github.com/marko-js/writable-dom/) Utility to stream HTML content into a live document. ## 📄 License Qiankun is [MIT licensed](./LICENSE). ================================================ FILE: commitlint.config.js ================================================ module.exports = { extends: ['@commitlint/config-conventional'], rules: { 'type-enum': [ 2, 'always', ['feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'build', 'ci', 'chore', 'revert'], ], 'subject-case': [0, 'never'], }, }; ================================================ FILE: docs/.vitepress/config.mjs ================================================ import { defineConfig } from 'vitepress' export default defineConfig({ title: 'qiankun', description: 'Probably the most complete micro-frontends solution you ever met🧐', // Default to English lang: 'en-US', head: [ ['link', { rel: 'icon', href: '/logo.png' }], ['meta', { name: 'theme-color', content: '#646cff' }], ['meta', { property: 'og:title', content: 'qiankun' }], ['meta', { property: 'og:description', content: 'Probably the most complete micro-frontends solution you ever met🧐' }], ['meta', { property: 'og:image', content: '/logo.png' }] ], locales: { root: { label: 'English', lang: 'en', title: 'qiankun', description: 'Probably the most complete micro-frontends solution you ever met🧐', themeConfig: { nav: [ { text: 'Guide', link: '/guide/' }, { text: 'API', link: '/api/' }, { text: 'Ecosystem', link: '/ecosystem/' }, { text: 'Cookbook', link: '/cookbook/' }, { text: 'FAQ', link: '/faq/' }, { text: 'Links', items: [ { text: 'GitHub', link: 'https://github.com/umijs/qiankun' }, { text: 'Changelog', link: 'https://github.com/umijs/qiankun/releases' }, { text: 'Community', link: 'https://github.com/umijs/qiankun/discussions' }, ] } ], sidebar: { '/guide/': [ { text: 'Introduction', items: [ { text: 'What is qiankun?', link: '/guide/' }, { text: 'Quick Start', link: '/guide/quick-start' }, { text: 'Tutorial', link: '/guide/tutorial' }, ] } ], '/api/': [ { text: 'Core APIs', items: [ { text: 'Overview', link: '/api/' }, { text: 'registerMicroApps', link: '/api/register-micro-apps' }, { text: 'loadMicroApp', link: '/api/load-micro-app' }, { text: 'start', link: '/api/start' }, { text: 'isRuntimeCompatible', link: '/api/is-runtime-compatible' }, ] }, { text: 'Reference', items: [ { text: 'Lifecycles', link: '/api/lifecycles' }, { text: 'Configuration', link: '/api/configuration' }, { text: 'Types', link: '/api/types' }, ] } ], '/ecosystem/': [ { text: 'UI Bindings', items: [ { text: 'Overview', link: '/ecosystem/' }, { text: 'React', link: '/ecosystem/react' }, { text: 'Vue', link: '/ecosystem/vue' }, ] }, { text: 'Tools', items: [ { text: 'Webpack Plugin', link: '/ecosystem/webpack-plugin' }, { text: 'Create Qiankun', link: '/ecosystem/create-qiankun' }, ] } ], '/cookbook/': [ { text: 'Best Practices', items: [ { text: 'Overview', link: '/cookbook/' }, { text: 'Style Isolation', link: '/cookbook/style-isolation' }, { text: 'Performance', link: '/cookbook/performance' }, { text: 'Error Handling', link: '/cookbook/error-handling' }, ] } ] } } }, 'zh-CN': { label: '中文', lang: 'zh-CN', title: 'qiankun', description: '可能是你见过最完善的微前端解决方案🧐', themeConfig: { nav: [ { text: '指南', link: '/zh-CN/guide/' }, { text: 'API', link: '/zh-CN/api/' }, { text: '生态系统', link: '/zh-CN/ecosystem/' }, { text: '最佳实践', link: '/zh-CN/cookbook/' }, { text: '常见问题', link: '/zh-CN/faq/' }, { text: '相关链接', items: [ { text: 'GitHub', link: 'https://github.com/umijs/qiankun' }, { text: '更新日志', link: 'https://github.com/umijs/qiankun/releases' }, { text: '社区讨论', link: 'https://github.com/umijs/qiankun/discussions' }, ] } ], sidebar: { '/zh-CN/guide/': [ { text: '介绍', items: [ { text: '什么是 qiankun?', link: '/zh-CN/guide/' }, { text: '快速开始', link: '/zh-CN/guide/quick-start' }, { text: '详细教程', link: '/zh-CN/guide/tutorial' }, ] } ], '/zh-CN/api/': [ { text: '核心 API', items: [ { text: '概览', link: '/zh-CN/api/' }, { text: 'registerMicroApps', link: '/zh-CN/api/register-micro-apps' }, { text: 'loadMicroApp', link: '/zh-CN/api/load-micro-app' }, { text: 'start', link: '/zh-CN/api/start' }, { text: 'isRuntimeCompatible', link: '/zh-CN/api/is-runtime-compatible' }, ] } ], '/zh-CN/ecosystem/': [ { text: 'UI 绑定', items: [ { text: '概览', link: '/zh-CN/ecosystem/' }, { text: 'React', link: '/zh-CN/ecosystem/react' }, { text: 'Vue', link: '/zh-CN/ecosystem/vue' }, ] } ], '/zh-CN/cookbook/': [ { text: '最佳实践', items: [ { text: '概览', link: '/zh-CN/cookbook/' }, { text: '常见问题', link: '/zh-CN/cookbook/error-handling' }, ] } ], '/zh-CN/faq/': [ { text: '常见问题', items: [ { text: 'FAQ', link: '/zh-CN/faq/' }, ] } ] } } } }, themeConfig: { logo: '/logo.png', socialLinks: [ { icon: 'github', link: 'https://github.com/umijs/qiankun' } ], search: { provider: 'local' }, editLink: { pattern: 'https://github.com/umijs/qiankun/edit/next/docs/:path', text: 'Edit this page on GitHub' }, lastUpdated: { text: 'Last updated', formatOptions: { dateStyle: 'short', timeStyle: 'medium' } }, footer: { message: 'Released under the MIT License.', copyright: 'Copyright © 2019-present qiankun contributors' } }, markdown: { lineNumbers: true, theme: { light: 'github-light', dark: 'github-dark' } }, cleanUrls: true, sitemap: { hostname: 'https://qiankun.umijs.org' }, ignoreDeadLinks: true }) ================================================ FILE: docs/.vitepress/theme/index.js ================================================ // .vitepress/theme/index.js import DefaultTheme from 'vitepress/theme' import { onMounted, nextTick } from 'vue' export default { extends: DefaultTheme, enhanceApp({ app, router }) { // 路由变化时重新渲染 Mermaid if (typeof window !== 'undefined') { router.onAfterRouteChanged = () => { nextTick(() => { renderMermaidCharts() }) } } }, setup() { onMounted(() => { // 初始加载时渲染 Mermaid setTimeout(() => { renderMermaidCharts() }, 100) }) } } function renderMermaidCharts() { if (typeof window === 'undefined' || !window.mermaid) { return } try { // 初始化 mermaid window.mermaid.initialize({ startOnLoad: false, theme: 'default' }) // 查找所有 mermaid 代码块 const mermaidElements = document.querySelectorAll('pre code.language-mermaid') mermaidElements.forEach((element, index) => { // 如果已经渲染过,跳过 if (element.getAttribute('data-processed') === 'true') { return } const code = element.textContent || element.innerText const uniqueId = `mermaid-${Date.now()}-${index}` // 创建容器 const container = document.createElement('div') container.className = 'mermaid-container' container.id = uniqueId // 渲染图表 window.mermaid.render(uniqueId + '-svg', code).then(({ svg }) => { container.innerHTML = svg // 替换原来的代码块 const parent = element.closest('pre') if (parent && parent.parentNode) { parent.parentNode.replaceChild(container, parent) } }).catch(error => { console.warn('Mermaid 渲染错误:', error) }) // 标记为已处理 element.setAttribute('data-processed', 'true') }) } catch (error) { console.warn('Mermaid 初始化错误:', error) } } ================================================ FILE: docs/api/configuration.md ================================================ # Configuration qiankun provides flexible configuration options to customize the behavior of micro-frontend applications. This document covers all available configuration options for different use cases. ## 📋 Configuration Types ### AppConfiguration Configuration for individual micro applications used with `loadMicroApp`. ```typescript type AppConfiguration = { sandbox?: boolean; globalContext?: WindowProxy; fetch?: Function; streamTransformer?: Function; nodeTransformer?: Function; }; ``` ### StartOpts Configuration for starting the qiankun framework used with `start()`. ```typescript interface StartOpts { prefetch?: boolean | 'all' | string[] | ((apps: RegistrableApp[]) => { criticalAppNames: string[]; minorAppsName: string[] }); sandbox?: boolean | { strictStyleIsolation?: boolean; experimentalStyleIsolation?: boolean; }; singular?: boolean; urlRerouteOnly?: boolean; // ... other single-spa options } ``` ## ⚙️ App Configuration Options ### sandbox **Type**: `boolean` **Default**: `true` **Description**: Enable sandbox isolation for the micro application. #### Basic Usage ```typescript // Enable sandbox (default) loadMicroApp({ name: 'my-app', entry: '//localhost:8080', container: '#container', }, { sandbox: true }); // Disable sandbox (not recommended) loadMicroApp({ name: 'legacy-app', entry: '//localhost:8080', container: '#container', }, { sandbox: false }); ``` #### Why Use Sandbox? ```typescript // With sandbox enabled, global variables are isolated loadMicroApp({ name: 'app1', entry: '//localhost:8001', container: '#container1', }, { sandbox: true // app1 gets its own global scope }); loadMicroApp({ name: 'app2', entry: '//localhost:8002', container: '#container2', }, { sandbox: true // app2 gets its own isolated global scope }); ``` ### globalContext **Type**: `WindowProxy` **Default**: `window` **Description**: Custom global context for the micro application. ```typescript // Create a custom global context const customGlobal = new Proxy(window, { get(target, prop) { // Custom logic for property access if (prop === 'customAPI') { return { version: '1.0' }; } return target[prop]; } }); loadMicroApp({ name: 'custom-app', entry: '//localhost:8080', container: '#container', }, { globalContext: customGlobal }); ``` ### fetch **Type**: `Function` **Default**: `window.fetch` **Description**: Custom fetch function for loading application resources. #### Custom Headers ```typescript const customFetch = async (url, options) => { return fetch(url, { ...options, headers: { ...options?.headers, 'Authorization': `Bearer ${getToken()}`, 'X-Custom-Header': 'custom-value' } }); }; loadMicroApp({ name: 'authenticated-app', entry: '//localhost:8080', container: '#container', }, { fetch: customFetch }); ``` #### Request Transformation ```typescript const transformFetch = async (url, options) => { // Transform URLs const transformedUrl = url.replace('//localhost', '//production-domain'); // Add custom logic console.log(`Fetching: ${transformedUrl}`); const response = await fetch(transformedUrl, options); // Transform response if (!response.ok) { throw new Error(`Failed to fetch ${transformedUrl}: ${response.status}`); } return response; }; ``` #### Caching Strategy ```typescript const cache = new Map(); const cachingFetch = async (url, options) => { const cacheKey = `${url}${JSON.stringify(options)}`; if (cache.has(cacheKey)) { console.log(`Cache hit for ${url}`); return cache.get(cacheKey); } const response = await fetch(url, options); cache.set(cacheKey, response.clone()); return response; }; ``` ### streamTransformer **Type**: `Function` **Description**: Transform streaming HTML content during loading. ```typescript const customStreamTransformer = (stream) => { return stream.pipeThrough(new TransformStream({ transform(chunk, controller) { // Transform HTML chunks const transformedChunk = chunk .replace(/old-api/g, 'new-api') .replace(/deprecated-feature/g, 'updated-feature'); controller.enqueue(transformedChunk); } })); }; loadMicroApp({ name: 'streaming-app', entry: '//localhost:8080', container: '#container', }, { streamTransformer: customStreamTransformer }); ``` ### nodeTransformer **Type**: `Function` **Description**: Transform DOM nodes during application loading. ```typescript const customNodeTransformer = (node, options) => { // Transform script tags if (node.tagName === 'SCRIPT') { // Add custom attributes node.setAttribute('data-app', 'my-app'); // Modify script source if (node.src) { node.src = node.src.replace('localhost', 'production-domain'); } } // Transform style tags if (node.tagName === 'STYLE') { // Add CSS scope node.textContent = `.app-scope { ${node.textContent} }`; } return node; }; loadMicroApp({ name: 'transformed-app', entry: '//localhost:8080', container: '#container', }, { nodeTransformer: customNodeTransformer }); ``` ## 🚀 Start Configuration Options ### prefetch **Type**: `boolean | 'all' | string[] | Function` **Default**: `true` **Description**: Resource prefetching strategy for better performance. #### Boolean Values ```typescript // Disable prefetch start({ prefetch: false }); // Enable default prefetch start({ prefetch: true }); ``` #### Prefetch All ```typescript // Prefetch all registered micro apps start({ prefetch: 'all' }); ``` #### Selective Prefetch ```typescript // Prefetch specific apps start({ prefetch: ['dashboard', 'user-profile', 'analytics'] }); ``` #### Dynamic Prefetch Strategy ```typescript start({ prefetch: (apps) => { // Business logic to determine prefetch strategy const currentTime = new Date().getHours(); const isBusinessHours = currentTime >= 9 && currentTime <= 17; if (isBusinessHours) { // Prefetch business-critical apps during business hours return { criticalAppNames: ['dashboard', 'crm', 'finance'], minorAppsName: ['reporting', 'settings'] }; } else { // Minimal prefetch during off-hours return { criticalAppNames: ['dashboard'], minorAppsName: [] }; } } }); ``` #### User-based Prefetch ```typescript start({ prefetch: (apps) => { const userRole = getCurrentUserRole(); switch (userRole) { case 'admin': return { criticalAppNames: ['admin-panel', 'user-management', 'system-monitor'], minorAppsName: ['reports', 'settings'] }; case 'user': return { criticalAppNames: ['dashboard', 'profile'], minorAppsName: ['help', 'feedback'] }; default: return { criticalAppNames: ['dashboard'], minorAppsName: [] }; } } }); ``` ### sandbox **Type**: `boolean | SandboxConfig` **Default**: `true` **Description**: Global sandbox configuration for all micro applications. #### Basic Sandbox ```typescript // Enable sandbox for all apps start({ sandbox: true }); // Disable sandbox for all apps (not recommended) start({ sandbox: false }); ``` #### Advanced Sandbox Configuration ```typescript start({ sandbox: { strictStyleIsolation: true, // Enable Shadow DOM style isolation experimentalStyleIsolation: true, // Enable scoped CSS style isolation } }); ``` #### Style Isolation Options **strictStyleIsolation**: Uses Shadow DOM to completely isolate styles ```typescript start({ sandbox: { strictStyleIsolation: true, // Strongest isolation but may break some UI libraries } }); ``` **experimentalStyleIsolation**: Uses scoped CSS to isolate styles ```typescript start({ sandbox: { experimentalStyleIsolation: true, // Good balance of isolation and compatibility } }); ``` #### Combined Style Isolation ```typescript start({ sandbox: { strictStyleIsolation: false, // Disable Shadow DOM experimentalStyleIsolation: true, // Enable scoped CSS } }); ``` ### singular **Type**: `boolean` **Default**: `true` **Description**: Whether only one micro app can be mounted at a time. ```typescript // Only one app at a time (default) start({ singular: true }); // Allow multiple apps simultaneously start({ singular: false // Useful for dashboard-style applications }); ``` #### Use Cases for Multiple Apps ```typescript // Dashboard with multiple widgets start({ singular: false, // Other configurations }); // Register widget-style micro apps registerMicroApps([ { name: 'widget-weather', entry: '//localhost:8001', container: '#widget-1', activeRule: '/dashboard' }, { name: 'widget-stocks', entry: '//localhost:8002', container: '#widget-2', activeRule: '/dashboard' }, { name: 'widget-news', entry: '//localhost:8003', container: '#widget-3', activeRule: '/dashboard' }, ]); ``` ### urlRerouteOnly **Type**: `boolean` **Default**: `true` **Description**: Whether to trigger routing only on URL changes. ```typescript // Only route on URL changes (default) start({ urlRerouteOnly: true }); // Route on both URL and programmatic changes start({ urlRerouteOnly: false // More responsive but potentially more performance overhead }); ``` ## 🔧 Environment-based Configuration ### Development Configuration ```typescript const developmentConfig = { prefetch: false, // Faster rebuilds sandbox: { strictStyleIsolation: false, // Easier debugging experimentalStyleIsolation: true, }, singular: false, // More flexible development urlRerouteOnly: false, // More responsive navigation }; if (process.env.NODE_ENV === 'development') { start(developmentConfig); } ``` ### Production Configuration ```typescript const productionConfig = { prefetch: 'all', // Better user experience sandbox: { strictStyleIsolation: true, // Better isolation experimentalStyleIsolation: false, }, singular: true, // Stable performance urlRerouteOnly: true, // Optimized routing }; if (process.env.NODE_ENV === 'production') { start(productionConfig); } ``` ### Mobile Configuration ```typescript const mobileConfig = { prefetch: (apps) => ({ // Conservative prefetch on mobile criticalAppNames: ['home'], minorAppsName: [] }), sandbox: { // Lighter sandbox for mobile performance strictStyleIsolation: false, experimentalStyleIsolation: true, }, singular: true, // Better for mobile UX }; const isMobile = window.innerWidth < 768; if (isMobile) { start(mobileConfig); } ``` ## 🎯 Advanced Configuration Patterns ### 1. Feature Flag Integration ```typescript const getConfigWithFeatureFlags = async () => { const featureFlags = await getFeatureFlags(); return { prefetch: featureFlags.enablePrefetch ? 'all' : false, sandbox: { strictStyleIsolation: featureFlags.strictIsolation, experimentalStyleIsolation: !featureFlags.strictIsolation, }, singular: featureFlags.allowMultipleApps ? false : true, }; }; getConfigWithFeatureFlags().then(config => start(config)); ``` ### 2. Performance-based Configuration ```typescript const getPerformanceConfig = () => { const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection; const isSlowConnection = connection?.effectiveType === '2g' || connection?.effectiveType === 'slow-2g'; if (isSlowConnection) { return { prefetch: false, // No prefetch on slow connections sandbox: { strictStyleIsolation: false, experimentalStyleIsolation: true, }, singular: true, }; } return { prefetch: 'all', sandbox: { strictStyleIsolation: true, experimentalStyleIsolation: false, }, singular: false, }; }; start(getPerformanceConfig()); ``` ### 3. User Role-based Configuration ```typescript const getRoleBasedConfig = (userRole) => { const baseConfig = { sandbox: true, singular: true, }; switch (userRole) { case 'admin': return { ...baseConfig, prefetch: 'all', // Admins get all features singular: false, // Can use multiple admin tools }; case 'poweruser': return { ...baseConfig, prefetch: ['dashboard', 'analytics', 'reports'], singular: false, }; default: return { ...baseConfig, prefetch: ['dashboard'], // Basic users get minimal prefetch singular: true, }; } }; const userRole = getCurrentUserRole(); start(getRoleBasedConfig(userRole)); ``` ## ⚠️ Important Notes ### 1. Configuration Precedence ```typescript // App-level configuration overrides global configuration start({ sandbox: true, // Global setting }); loadMicroApp({ name: 'special-app', entry: '//localhost:8080', container: '#container', }, { sandbox: false // This overrides the global setting for this app }); ``` ### 2. Performance Considerations ```typescript // ❌ Bad: Heavy configuration that impacts performance start({ prefetch: 'all', // Might slow down initial load sandbox: { strictStyleIsolation: true, // More overhead }, singular: false, // More memory usage urlRerouteOnly: false, // More frequent route checks }); // ✅ Good: Balanced configuration start({ prefetch: ['critical-app'], // Only prefetch what's needed sandbox: { experimentalStyleIsolation: true, // Good balance }, singular: true, // Stable performance urlRerouteOnly: true, // Optimized routing }); ``` ### 3. Debugging Configuration ```typescript const debugConfig = { sandbox: { strictStyleIsolation: false, // Easier to inspect styles experimentalStyleIsolation: true, }, // Custom fetch for logging fetch: async (url, options) => { console.log(`[DEBUG] Fetching: ${url}`); const response = await fetch(url, options); console.log(`[DEBUG] Response: ${response.status}`); return response; }, // Custom node transformer for debugging nodeTransformer: (node, options) => { if (node.tagName === 'SCRIPT') { console.log(`[DEBUG] Processing script: ${node.src || 'inline'}`); } return node; } }; ``` ## 🔗 Related APIs - [start](/api/start) - Start qiankun with configuration - [loadMicroApp](/api/load-micro-app) - Load app with configuration - [registerMicroApps](/api/register-micro-apps) - Register apps ================================================ FILE: docs/api/index.md ================================================ # API Reference qiankun provides a simple yet powerful API set for building micro-frontend applications. All APIs are fully typed with TypeScript definitions to ensure both developer experience and type safety. ## 📚 Core APIs ### Application Registration & Startup | API | Description | Type | |-----|-------------|------| | [`registerMicroApps`](/api/register-micro-apps) | Register micro applications | `(apps: RegistrableApp[], lifeCycles?: LifeCycles) => void` | | [`start`](/api/start) | Start qiankun framework | `(opts?: StartOpts) => void` | | [`loadMicroApp`](/api/load-micro-app) | Manually load micro application | `(app: LoadableApp, configuration?: AppConfiguration, lifeCycles?: LifeCycles) => MicroApp` | ### Utility APIs | API | Description | Type | |-----|-------------|------| | [`isRuntimeCompatible`](/api/is-runtime-compatible) | Check runtime compatibility | `() => boolean` | ## 🎯 Quick Navigation ### By Use Case **Route-based Mode** ```typescript import { registerMicroApps, start } from 'qiankun'; // 1. Register micro apps registerMicroApps([...]); // 2. Start framework start(); ``` **Manual Loading Mode** ```typescript import { loadMicroApp } from 'qiankun'; // Manually load micro app const microApp = loadMicroApp({...}); ``` **Compatibility Check** ```typescript import { isRuntimeCompatible } from 'qiankun'; if (isRuntimeCompatible()) { // Start micro-frontend application } ``` ### By Functionality | Category | Related APIs | Description | |----------|--------------|-------------| | **App Management** | `registerMicroApps`, `loadMicroApp` | Register and load micro applications | | **Framework Control** | `start` | Framework startup and configuration | | **Utilities** | `isRuntimeCompatible` | Helper utility methods | ## 🔧 Type Definitions qiankun provides complete TypeScript type definitions: ```typescript import type { RegistrableApp, LoadableApp, MicroApp, LifeCycles, AppConfiguration, } from 'qiankun'; ``` See [Type Definitions](/api/types) for detailed information. ## 📖 Detailed Documentation ### Core APIs - [registerMicroApps](/api/register-micro-apps) - Register micro applications - [start](/api/start) - Start qiankun framework - [loadMicroApp](/api/load-micro-app) - Manually load micro applications - [isRuntimeCompatible](/api/is-runtime-compatible) - Runtime compatibility check ### Reference Documentation - [Lifecycles](/api/lifecycles) - Application lifecycle hooks - [Configuration](/api/configuration) - Framework configuration options - [Types](/api/types) - TypeScript type definitions ## 💡 Usage Recommendations ### Recommended API Usage Patterns 1. **Standard Route-based Mode** (Recommended) ```typescript registerMicroApps([...]) → start() ``` 2. **Dynamic Loading Mode** ```typescript loadMicroApp({...}) ``` 3. **Hybrid Mode** ```typescript registerMicroApps([...]) → start() + loadMicroApp({...}) ``` ### Best Practices - ✅ Use TypeScript for complete type support - ✅ Register all micro apps before starting the framework - ✅ Use lifecycle hooks appropriately for state management - ✅ Configure proper error handling - ❌ Avoid registering duplicate app names - ❌ Avoid calling main app APIs from micro apps - ❌ Avoid time-consuming operations in lifecycle hooks ================================================ FILE: docs/api/is-runtime-compatible.md ================================================ # isRuntimeCompatible Check if the current browser environment is compatible with qiankun runtime features. ## 🎯 Function Signature ```typescript function isRuntimeCompatible(): boolean ``` ## 📋 Parameters This function takes no parameters. ## 🔄 Return Value - **Type**: `boolean` - **Description**: Returns `true` if the current environment supports qiankun features, `false` otherwise. ## 💡 Usage Examples ### Basic Compatibility Check ```typescript import { isRuntimeCompatible, registerMicroApps, start } from 'qiankun'; if (isRuntimeCompatible()) { // Environment supports qiankun registerMicroApps([...]); start(); } else { // Fallback for unsupported browsers console.warn('Current browser does not support qiankun'); initFallbackRouting(); } ``` ### With Graceful Degradation ```typescript function initApplication() { if (isRuntimeCompatible()) { // Use qiankun micro-frontend architecture initMicroFrontend(); } else { // Fall back to traditional SPA initTraditionalSPA(); } } function initMicroFrontend() { registerMicroApps([ { name: 'module-a', entry: '//localhost:8001', container: '#container', activeRule: '/module-a', } ]); start(); } function initTraditionalSPA() { // Traditional routing setup import('./traditional-router').then(router => { router.init(); }); } ``` ## 🔍 What It Checks The `isRuntimeCompatible` function checks for the following browser features: ### Required Features 1. **Proxy Support**: For JavaScript sandbox isolation 2. **Window.Proxy**: Essential for creating isolated execution contexts 3. **Import Maps** (when used): For dynamic module loading 4. **Dynamic Import**: For loading micro applications ### Browser Compatibility | Browser | Minimum Version | Support | |---------|----------------|---------| | Chrome | 61+ | ✅ Full | | Firefox | 60+ | ✅ Full | | Safari | 11+ | ✅ Full | | Edge | 79+ | ✅ Full | | IE | Any | ❌ Not Supported | ## 🚀 Best Practices ### 1. Early Detection ```typescript // Check compatibility before any qiankun setup function bootstrap() { if (!isRuntimeCompatible()) { showUnsupportedBrowserMessage(); return; } // Safe to proceed with qiankun setupMicroFrontend(); } ``` ### 2. Progressive Enhancement ```typescript class ApplicationBootstrap { private isQiankunSupported = isRuntimeCompatible(); init() { if (this.isQiankunSupported) { this.initWithMicroFrontend(); } else { this.initWithoutMicroFrontend(); } } private initWithMicroFrontend() { // Full micro-frontend experience registerMicroApps([...]); start(); } private initWithoutMicroFrontend() { // Simplified experience for unsupported browsers this.loadAllModulesDirectly(); } } ``` ### 3. User Communication ```typescript if (!isRuntimeCompatible()) { // Show user-friendly message const banner = document.createElement('div'); banner.innerHTML = `
Browser Compatibility Notice: For the best experience, please use a modern browser like Chrome, Firefox, or Safari. Some features may be limited in your current browser.
`; document.body.insertBefore(banner, document.body.firstChild); } ``` ## 🔧 Integration Patterns ### 1. With Feature Flags ```typescript const featureFlags = { useMicroFrontend: isRuntimeCompatible() && process.env.ENABLE_MICRO_FRONTEND, useAdvancedFeatures: isRuntimeCompatible(), }; if (featureFlags.useMicroFrontend) { // Full micro-frontend setup registerMicroApps([...]); start(); } else { // Traditional setup initTraditionalApp(); } ``` ### 2. With Analytics ```typescript // Track browser compatibility for analytics const compatible = isRuntimeCompatible(); // Send analytics event analytics.track('browser_compatibility_check', { compatible, userAgent: navigator.userAgent, timestamp: Date.now(), }); if (compatible) { initQiankunApp(); } else { initFallbackApp(); } ``` ### 3. With Dynamic Loading ```typescript async function loadApplicationFramework() { if (isRuntimeCompatible()) { // Load qiankun and micro-frontend modules const [qiankun, microApps] = await Promise.all([ import('qiankun'), import('./micro-apps-config'), ]); qiankun.registerMicroApps(microApps.default); qiankun.start(); } else { // Load traditional SPA modules const traditionalApp = await import('./traditional-app'); traditionalApp.init(); } } ``` ## ⚠️ Important Notes ### 1. Performance Consideration ```typescript // ✅ Good: Check once and cache the result const QIANKUN_COMPATIBLE = isRuntimeCompatible(); function someFunction() { if (QIANKUN_COMPATIBLE) { // Use cached result } } // ❌ Bad: Check multiple times function someFunction() { if (isRuntimeCompatible()) { // Redundant check // ... } } ``` ### 2. SSR Considerations ```typescript // In SSR environments, check if window is available function safeCompatibilityCheck() { if (typeof window === 'undefined') { // SSR environment - assume compatible return true; } return isRuntimeCompatible(); } ``` ### 3. Testing Environment ```typescript // For testing, you might want to mock the compatibility if (process.env.NODE_ENV === 'test') { // Mock for testing global.mockQiankunCompatible = true; } function checkCompatibility() { if (process.env.NODE_ENV === 'test' && global.mockQiankunCompatible !== undefined) { return global.mockQiankunCompatible; } return isRuntimeCompatible(); } ``` ## 🎯 Common Scenarios ### 1. Corporate Environment ```typescript // Corporate environments might have older browsers function initCorporateApp() { const compatible = isRuntimeCompatible(); if (!compatible) { // Inform IT department about browser requirements logToAdminConsole('User browser incompatible with micro-frontend features'); } return compatible ? initMicroFrontend() : initLegacyApp(); } ``` ### 2. Public Website ```typescript // Public websites need to support a wider range of browsers function initPublicSite() { if (isRuntimeCompatible()) { // Enhanced experience with micro-frontends loadAdvancedFeatures(); } else { // Basic experience that works everywhere loadBasicFeatures(); } } ``` ### 3. Mobile App WebView ```typescript // Mobile WebViews might have different compatibility function initMobileWebView() { const compatible = isRuntimeCompatible(); // Log for mobile app developers if (window.ReactNativeWebView) { window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'qiankun_compatibility', compatible, })); } return compatible ? initMicroFrontend() : initSimplifiedView(); } ``` ## 🔗 Related APIs - [start](/api/start) - Start qiankun (should be called after compatibility check) - [registerMicroApps](/api/register-micro-apps) - Register micro applications - [loadMicroApp](/api/load-micro-app) - Manually load micro applications ================================================ FILE: docs/api/lifecycles.md ================================================ # Lifecycles Lifecycle hooks allow you to perform custom logic at different stages of a micro application's lifecycle. These hooks are executed automatically by qiankun during application loading, mounting, and unmounting processes. ## 🎯 Type Definition ```typescript export type LifeCycleFn = ( app: LoadableApp, global: WindowProxy ) => Promise; export type LifeCycles = { beforeLoad?: LifeCycleFn | Array>; beforeMount?: LifeCycleFn | Array>; afterMount?: LifeCycleFn | Array>; beforeUnmount?: LifeCycleFn | Array>; afterUnmount?: LifeCycleFn | Array>; }; ``` ## 📋 Available Lifecycle Hooks ### beforeLoad **Timing**: Called before the micro application starts loading. **Purpose**: Perform setup tasks before the application code is fetched and parsed. ```typescript beforeLoad: async (app, global) => { console.log(`About to load ${app.name}`); // Setup global configurations global.__INITIAL_CONFIG__ = getInitialConfig(); } ``` ### beforeMount **Timing**: Called after the application is loaded but before it's mounted to the DOM. **Purpose**: Perform final setup before the application becomes active. ```typescript beforeMount: async (app, global) => { console.log(`About to mount ${app.name}`); // Initialize services await initializeServices(); // Set loading state setLoadingState(false); } ``` ### afterMount **Timing**: Called after the micro application has been successfully mounted. **Purpose**: Perform post-mount operations like analytics, feature initialization. ```typescript afterMount: async (app, global) => { console.log(`${app.name} mounted successfully`); // Track analytics analytics.track('micro_app_mounted', { appName: app.name }); // Initialize features that depend on DOM initializeDOMDependentFeatures(); } ``` ### beforeUnmount **Timing**: Called before the micro application starts unmounting. **Purpose**: Cleanup operations before the application is removed. ```typescript beforeUnmount: async (app, global) => { console.log(`About to unmount ${app.name}`); // Save application state saveApplicationState(app.name); // Cleanup event listeners cleanupEventListeners(); } ``` ### afterUnmount **Timing**: Called after the micro application has been completely unmounted. **Purpose**: Final cleanup and resource deallocation. ```typescript afterUnmount: async (app, global) => { console.log(`${app.name} unmounted`); // Clear caches clearApplicationCache(app.name); // Reset global state resetGlobalState(); } ``` ## 🔄 Lifecycle Flow ```mermaid graph TD A[Start Loading] --> B[beforeLoad] B --> C[Load Application Code] C --> D[beforeMount] D --> E[Mount Application] E --> F[afterMount] F --> G[Application Running] G --> H[beforeUnmount] H --> I[Unmount Application] I --> J[afterUnmount] J --> K[Application Cleaned Up] ``` ## 💡 Usage Examples ### Basic Usage with registerMicroApps ```typescript import { registerMicroApps, start } from 'qiankun'; registerMicroApps([ { name: 'react-app', entry: '//localhost:7100', container: '#subapp-viewport', activeRule: '/react', } ], { beforeLoad: async (app) => { console.log('Loading app:', app.name); }, afterMount: async (app) => { console.log('App mounted:', app.name); }, beforeUnmount: async (app) => { console.log('Unmounting app:', app.name); } }); start(); ``` ### With loadMicroApp ```typescript import { loadMicroApp } from 'qiankun'; const microApp = loadMicroApp({ name: 'dashboard', entry: '//localhost:8080', container: '#dashboard-container', }, undefined, { beforeLoad: async (app, global) => { // Setup dashboard-specific configurations global.DASHBOARD_CONFIG = getDashboardConfig(); }, afterMount: async (app) => { // Initialize dashboard widgets initializeDashboardWidgets(); } }); ``` ### Multiple Hooks ```typescript // You can provide multiple hooks as an array const lifecycles = { beforeMount: [ async (app) => { await setupDatabase(); }, async (app) => { await setupAnalytics(); }, async (app) => { await setupFeatureFlags(); } ], afterMount: [ async (app) => { trackPageView(app.name); }, async (app) => { initializeUserTracking(); } ] }; ``` ## 🔧 Advanced Patterns ### 1. State Management Integration ```typescript import { store } from './store'; const lifecycles = { beforeLoad: async (app) => { // Set loading state store.dispatch({ type: 'SET_APP_LOADING', payload: { appName: app.name, loading: true } }); }, afterMount: async (app) => { // Update mounted apps list store.dispatch({ type: 'ADD_MOUNTED_APP', payload: app.name }); store.dispatch({ type: 'SET_APP_LOADING', payload: { appName: app.name, loading: false } }); }, beforeUnmount: async (app) => { // Save app state before unmounting const appState = getAppState(app.name); store.dispatch({ type: 'SAVE_APP_STATE', payload: { appName: app.name, state: appState } }); }, afterUnmount: async (app) => { // Remove from mounted apps list store.dispatch({ type: 'REMOVE_MOUNTED_APP', payload: app.name }); } }; ``` ### 2. Error Handling ```typescript const lifecycles = { beforeLoad: async (app) => { try { await performPreLoadChecks(app); } catch (error) { console.error(`Pre-load checks failed for ${app.name}:`, error); // Optionally prevent loading by throwing throw new Error(`Failed to initialize ${app.name}`); } }, afterMount: async (app) => { try { await performPostMountTasks(app); } catch (error) { console.error(`Post-mount tasks failed for ${app.name}:`, error); // Log error but don't prevent the app from running reportError(error, { context: 'afterMount', appName: app.name }); } } }; ``` ### 3. Performance Monitoring ```typescript const performanceTracker = new Map(); const lifecycles = { beforeLoad: async (app) => { performanceTracker.set(app.name, { loadStart: performance.now() }); }, beforeMount: async (app) => { const timing = performanceTracker.get(app.name); timing.loadEnd = performance.now(); timing.mountStart = performance.now(); }, afterMount: async (app) => { const timing = performanceTracker.get(app.name); timing.mountEnd = performance.now(); // Calculate and report metrics const loadTime = timing.loadEnd - timing.loadStart; const mountTime = timing.mountEnd - timing.mountStart; analytics.track('micro_app_performance', { appName: app.name, loadTime, mountTime, totalTime: loadTime + mountTime }); } }; ``` ### 4. Resource Management ```typescript const resourceMap = new Map(); const lifecycles = { beforeMount: async (app) => { // Allocate resources const resources = await allocateResources(app.name); resourceMap.set(app.name, resources); }, beforeUnmount: async (app) => { // Save critical data const resources = resourceMap.get(app.name); if (resources) { await saveCriticalData(app.name, resources); } }, afterUnmount: async (app) => { // Release resources const resources = resourceMap.get(app.name); if (resources) { await releaseResources(resources); resourceMap.delete(app.name); } } }; ``` ## 🎯 Common Use Cases ### 1. Loading States ```typescript const loadingManager = { show: (appName) => { const loader = document.createElement('div'); loader.id = `loader-${appName}`; loader.innerHTML = '
Loading...
'; document.body.appendChild(loader); }, hide: (appName) => { const loader = document.getElementById(`loader-${appName}`); if (loader) loader.remove(); } }; const lifecycles = { beforeLoad: async (app) => { loadingManager.show(app.name); }, afterMount: async (app) => { loadingManager.hide(app.name); } }; ``` ### 2. Authentication Check ```typescript const lifecycles = { beforeLoad: async (app) => { const isAuthenticated = await checkAuthentication(); if (!isAuthenticated) { throw new Error('User not authenticated'); } }, beforeMount: async (app, global) => { // Inject user context const userContext = await getUserContext(); global.__USER_CONTEXT__ = userContext; } }; ``` ### 3. Theme Synchronization ```typescript const lifecycles = { beforeMount: async (app, global) => { // Sync theme with micro app const currentTheme = getCurrentTheme(); global.__THEME__ = currentTheme; // Apply theme-specific styles applyThemeStyles(currentTheme); }, afterUnmount: async (app) => { // Clean up theme styles removeThemeStyles(app.name); } }; ``` ### 4. Feature Flag Management ```typescript const lifecycles = { beforeLoad: async (app, global) => { // Load feature flags for the specific app const featureFlags = await getFeatureFlags(app.name); global.__FEATURE_FLAGS__ = featureFlags; }, afterMount: async (app) => { // Track which features are enabled trackEnabledFeatures(app.name); } }; ``` ## ⚠️ Important Notes ### 1. Hook Execution Order ```typescript // Hooks are executed in this order: // 1. beforeLoad (before app code is loaded) // 2. beforeMount (after load, before DOM mount) // 3. afterMount (after DOM mount) // ... app is running ... // 4. beforeUnmount (before DOM unmount) // 5. afterUnmount (after DOM unmount) ``` ### 2. Error Handling ```typescript // ❌ Bad: Unhandled errors can break the lifecycle beforeLoad: async (app) => { riskyOperation(); // This could throw } // ✅ Good: Always handle potential errors beforeLoad: async (app) => { try { await riskyOperation(); } catch (error) { console.error('Error in beforeLoad:', error); // Decide whether to throw or handle gracefully } } ``` ### 3. Async Operations ```typescript // ✅ Good: All lifecycle hooks are async beforeMount: async (app) => { await setupDatabase(); await loadUserPreferences(); } // ❌ Bad: Don't forget await for async operations beforeMount: async (app) => { setupDatabase(); // Missing await! loadUserPreferences(); // Missing await! } ``` ### 4. Global Context ```typescript // ✅ Good: Use the provided global context beforeMount: async (app, global) => { global.MY_CONFIG = getConfig(); // Set on the isolated global } // ❌ Bad: Don't use window directly beforeMount: async (app, global) => { window.MY_CONFIG = getConfig(); // Might affect other apps } ``` ## 🚀 Best Practices ### 1. Keep Hooks Lightweight ```typescript // ✅ Good: Fast operations beforeMount: async (app) => { setAppTheme(app.name); updateNavigationState(); } // ❌ Bad: Heavy operations beforeMount: async (app) => { await downloadLargeDataset(); // This will block mounting await processHeavyCalculations(); } ``` ### 2. Use Hook Arrays for Organization ```typescript const lifecycles = { beforeMount: [ setupAuthentication, setupTheme, setupAnalytics, setupFeatureFlags ], afterMount: [ trackPageView, initializeWidgets, preloadCriticalData ] }; ``` ### 3. Consistent Error Logging ```typescript const createSafeHook = (hookName, hookFn) => async (app, global) => { try { await hookFn(app, global); } catch (error) { console.error(`Error in ${hookName} for ${app.name}:`, error); // Report to error tracking service errorTracker.report(error, { hook: hookName, app: app.name }); } }; const lifecycles = { beforeLoad: createSafeHook('beforeLoad', async (app) => { // Your beforeLoad logic }), afterMount: createSafeHook('afterMount', async (app) => { // Your afterMount logic }) }; ``` ### 4. Resource Cleanup ```typescript // Track resources in a way that survives app reloads const globalResourceMap = window.__QIANKUN_RESOURCES__ || new Map(); window.__QIANKUN_RESOURCES__ = globalResourceMap; const lifecycles = { beforeMount: async (app) => { const resources = await allocateResources(); globalResourceMap.set(app.name, resources); }, afterUnmount: async (app) => { const resources = globalResourceMap.get(app.name); if (resources) { await cleanupResources(resources); globalResourceMap.delete(app.name); } } }; ``` ## 🔗 Related APIs - [registerMicroApps](/api/register-micro-apps) - Using lifecycles with registered apps - [loadMicroApp](/api/load-micro-app) - Using lifecycles with manually loaded apps - [start](/api/start) - Framework startup configuration ================================================ FILE: docs/api/load-micro-app.md ================================================ # loadMicroApp Manually load a micro application. This is useful for loading micro applications dynamically or when they are not associated with routing. ## 🎯 Function Signature ```typescript function loadMicroApp( app: LoadableApp, configuration?: AppConfiguration, lifeCycles?: LifeCycles ): MicroApp ``` ## 📋 Parameters ### app - **Type**: `LoadableApp` - **Required**: ✅ - **Description**: Micro application configuration #### LoadableApp Structure ```typescript interface LoadableApp { name: string; // Micro app name, globally unique entry: string | EntryOpts; // Micro app entry container: string | HTMLElement; // Container for the micro app props?: T; // Custom data passed to micro app } ``` | Property | Type | Required | Description | |----------|------|----------|-------------| | `name` | `string` | ✅ | Micro application name, used as unique identifier | | `entry` | `string \| EntryOpts` | ✅ | Micro application entry, can be URL or resource configuration | | `container` | `string \| HTMLElement` | ✅ | Container node selector or DOM element | | `props` | `T` | ❌ | Custom data passed to the micro application | ### configuration - **Type**: `AppConfiguration` - **Required**: ❌ - **Description**: Advanced configuration options ```typescript interface AppConfiguration { sandbox?: boolean; // Enable sandbox isolation globalContext?: WindowProxy; // Global context for the micro app fetch?: Function; // Custom fetch function streamTransformer?: Function; // Stream transformer nodeTransformer?: Function; // Node transformer } ``` ### lifeCycles - **Type**: `LifeCycles` - **Required**: ❌ - **Description**: Lifecycle hooks for this specific micro application ## 🔄 Return Value Returns a `MicroApp` instance with the following methods: ```typescript interface MicroApp { mount(): Promise; // Mount the micro app unmount(): Promise; // Unmount the micro app update(props: any): Promise; // Update micro app props getStatus(): string; // Get current status loadPromise: Promise; // Loading promise mountPromise: Promise; // Mounting promise unmountPromise: Promise; // Unmounting promise } ``` ## 💡 Usage Examples ### Basic Usage ```typescript import { loadMicroApp } from 'qiankun'; const microApp = loadMicroApp({ name: 'manual-app', entry: '//localhost:8080', container: '#manual-container', }); // The micro app will be automatically mounted ``` ### With Custom Props ```typescript const microApp = loadMicroApp({ name: 'dashboard', entry: '//localhost:8080', container: '#dashboard-container', props: { token: localStorage.getItem('token'), userId: getCurrentUserId(), theme: 'dark' } }); ``` ### With Configuration ```typescript const microApp = loadMicroApp({ name: 'third-party-app', entry: '//external.example.com', container: '#external-container', }, { sandbox: false, // Disable sandbox for legacy apps fetch: customFetch, // Use custom fetch }); ``` ### With Lifecycle Hooks ```typescript const microApp = loadMicroApp({ name: 'monitored-app', entry: '//localhost:8080', container: '#monitored-container', }, undefined, { beforeMount: (app) => { console.log('About to mount:', app.name); showLoadingSpinner(); }, afterMount: (app) => { console.log('Mounted successfully:', app.name); hideLoadingSpinner(); }, beforeUnmount: (app) => { console.log('About to unmount:', app.name); saveUserState(); } }); ``` ## 🔧 Advanced Usage ### Dynamic Loading with Conditions ```typescript async function loadAppConditionally(condition: boolean) { if (condition) { const microApp = loadMicroApp({ name: 'conditional-app', entry: '//localhost:8080', container: '#conditional-container', }); return microApp; } return null; } ``` ### Loading Multiple Apps ```typescript function loadMultipleApps() { const apps = [ { name: 'app1', entry: '//localhost:8001', container: '#container1' }, { name: 'app2', entry: '//localhost:8002', container: '#container2' }, { name: 'app3', entry: '//localhost:8003', container: '#container3' }, ]; const microApps = apps.map(app => loadMicroApp(app)); return microApps; } ``` ### Manual Control ```typescript const microApp = loadMicroApp({ name: 'controlled-app', entry: '//localhost:8080', container: '#controlled-container', }); // Manual unmount await microApp.unmount(); // Update props await microApp.update({ newData: 'updated' }); // Check status console.log(microApp.getStatus()); // 'MOUNTED', 'UNMOUNTED', etc. ``` ## 🎭 Use Cases ### 1. Modal/Dialog Applications ```typescript function openAppModal() { const modal = document.createElement('div'); modal.id = 'app-modal'; document.body.appendChild(modal); const microApp = loadMicroApp({ name: 'modal-app', entry: '//localhost:8080', container: modal, props: { onClose: () => { microApp.unmount().then(() => { document.body.removeChild(modal); }); } } }); return microApp; } ``` ### 2. Tab-based Applications ```typescript class TabManager { private activeTabs = new Map(); async switchTab(tabName: string, config: LoadableApp) { // Unmount current active tab const currentApp = this.activeTabs.get('active'); if (currentApp) { await currentApp.unmount(); } // Load new tab const newApp = loadMicroApp({ ...config, container: '#tab-content' }); this.activeTabs.set('active', newApp); this.activeTabs.set(tabName, newApp); } } ``` ### 3. Widget System ```typescript class WidgetSystem { loadWidget(widgetConfig: any) { return loadMicroApp({ name: `widget-${widgetConfig.id}`, entry: widgetConfig.url, container: `#widget-${widgetConfig.id}`, props: widgetConfig.props }, { sandbox: true // Isolate widgets }); } } ``` ## ⚠️ Important Notes ### Container Management ```typescript // ❌ Bad: Reusing containers without proper cleanup loadMicroApp({ name: 'app1', entry: '//localhost:8001', container: '#shared' }); loadMicroApp({ name: 'app2', entry: '//localhost:8002', container: '#shared' }); // Conflict! // ✅ Good: Use unique containers or proper cleanup const app1 = loadMicroApp({ name: 'app1', entry: '//localhost:8001', container: '#container1' }); const app2 = loadMicroApp({ name: 'app2', entry: '//localhost:8002', container: '#container2' }); ``` ### Memory Management ```typescript // ✅ Good: Proper cleanup const microApp = loadMicroApp({...}); // When done, always unmount window.addEventListener('beforeunload', () => { microApp.unmount(); }); ``` ### Error Handling ```typescript try { const microApp = loadMicroApp({ name: 'potentially-failing-app', entry: '//unreliable-server.com', container: '#container', }); // Wait for load await microApp.loadPromise; console.log('App loaded successfully'); } catch (error) { console.error('Failed to load micro app:', error); // Handle error - show fallback UI, retry, etc. } ``` ## 🆚 vs registerMicroApps | Feature | `loadMicroApp` | `registerMicroApps` | |---------|----------------|---------------------| | **Loading** | Manual, immediate | Automatic, route-based | | **Use Case** | Dynamic loading, widgets, modals | Main navigation, SPA routing | | **Lifecycle** | Manual control | Automatic by routing | | **Performance** | Load on demand | Can preload | ## 🚀 Best Practices ### 1. Resource Management ```typescript class MicroAppManager { private apps = new Map(); async loadApp(config: LoadableApp) { // Check if already loaded if (this.apps.has(config.name)) { return this.apps.get(config.name); } const app = loadMicroApp(config); this.apps.set(config.name, app); // Auto cleanup on unmount app.unmountPromise.then(() => { this.apps.delete(config.name); }); return app; } } ``` ### 2. Props Management ```typescript // ✅ Good: Reactive props function createReactiveMicroApp(baseConfig: LoadableApp) { let currentApp: MicroApp; return { async updateProps(newProps: any) { if (currentApp) { await currentApp.update(newProps); } }, async reload(newConfig: LoadableApp) { if (currentApp) { await currentApp.unmount(); } currentApp = loadMicroApp({ ...baseConfig, ...newConfig }); } }; } ``` ### 3. Error Boundaries ```typescript function loadMicroAppWithFallback(config: LoadableApp, fallbackHTML: string) { const microApp = loadMicroApp(config); microApp.loadPromise.catch((error) => { console.error('Micro app failed to load:', error); // Show fallback content const container = typeof config.container === 'string' ? document.querySelector(config.container) : config.container; if (container) { container.innerHTML = fallbackHTML; } }); return microApp; } ``` ## 🔗 Related APIs - [registerMicroApps](/api/register-micro-apps) - For route-based micro app loading - [start](/api/start) - Start qiankun framework - [Lifecycles](/api/lifecycles) - Detailed lifecycle documentation ================================================ FILE: docs/api/register-micro-apps.md ================================================ # registerMicroApps 注册微应用到 qiankun 中,这是构建微前端应用的核心 API。 ## 🎯 函数签名 ```typescript function registerMicroApps( apps: Array>, lifeCycles?: LifeCycles ): void ``` ## 📋 参数 ### apps - **类型**: `Array>` - **必填**: ✅ - **描述**: 微应用注册信息数组 #### RegistrableApp 结构 ```typescript interface RegistrableApp { name: string; // 微应用名称,全局唯一 entry: string | { scripts?: string[], styles?: string[] }; // 微应用入口 container: string | HTMLElement; // 微应用容器节点 activeRule: string | (location: Location) => boolean; // 激活规则 props?: T; // 传递给微应用的数据 loader?: (loading: boolean) => void; // 加载状态回调 } ``` | 属性 | 类型 | 必填 | 描述 | |------|------|------|------| | `name` | `string` | ✅ | 微应用名称,作为微应用的唯一标识 | | `entry` | `string \| EntryOpts` | ✅ | 微应用的入口,可以是 URL 或资源配置对象 | | `container` | `string \| HTMLElement` | ✅ | 微应用的容器节点选择器或 DOM 节点 | | `activeRule` | `string \| Function` | ✅ | 微应用的激活规则 | | `props` | `T` | ❌ | 传递给微应用的自定义数据 | | `loader` | `Function` | ❌ | 微应用加载状态改变时的回调函数 | ### lifeCycles - **类型**: `LifeCycles` - **必填**: ❌ - **描述**: 全局生命周期钩子 ```typescript interface LifeCycles { beforeLoad?: LifeCycleFn | Array>; beforeMount?: LifeCycleFn | Array>; afterMount?: LifeCycleFn | Array>; beforeUnmount?: LifeCycleFn | Array>; afterUnmount?: LifeCycleFn | Array>; } ``` ## 💡 使用示例 ### 基础用法 ```typescript import { registerMicroApps, start } from 'qiankun'; registerMicroApps([ { name: 'react16App', entry: '//localhost:7100', container: '#subapp-viewport', activeRule: '/react16', }, { name: 'vue3App', entry: '//localhost:7101', container: '#subapp-viewport', activeRule: '/vue3', } ]); start(); ``` ### 高级配置 ```typescript registerMicroApps([ { name: 'dashboard', entry: { scripts: [ '//localhost:7100/static/js/main.js' ], styles: [ '//localhost:7100/static/css/main.css' ] }, container: '#dashboard-container', activeRule: (location) => location.pathname.startsWith('/dashboard'), props: { token: 'your-auth-token', userId: 123, theme: 'dark' }, loader: (loading) => { console.log('Dashboard app loading:', loading); // 显示/隐藏 loading 状态 } } ], { beforeLoad: [ app => console.log('Before load:', app.name), app => trackEvent('micro-app-loading', { name: app.name }) ], beforeMount: app => console.log('Before mount:', app.name), afterMount: app => console.log('After mount:', app.name), beforeUnmount: app => console.log('Before unmount:', app.name), afterUnmount: app => console.log('After unmount:', app.name), }); ``` ## ⚙️ Entry 配置详解 ### URL 字符串 最简单的配置方式,qiankun 会通过这个 URL 获取微应用的 HTML: ```typescript { name: 'app1', entry: '//localhost:8080', // ... } ``` ### 资源对象 精确控制微应用的资源加载: ```typescript { name: 'app2', entry: { scripts: [ '//localhost:8080/static/js/chunk.js', '//localhost:8080/static/js/main.js' ], styles: [ '//localhost:8080/static/css/main.css' ] }, // ... } ``` ## 🎯 ActiveRule 配置 ### 字符串路径 ```typescript { activeRule: '/react16' // 匹配 /react16/xxx 路径 } ``` ### 函数判断 ```typescript { activeRule: (location) => { // 自定义激活逻辑 return location.pathname.startsWith('/admin') && location.search.includes('module=dashboard'); } } ``` ### 常见模式 ```typescript // 1. 精确匹配 activeRule: (location) => location.pathname === '/exact-path' // 2. 多路径匹配 activeRule: (location) => ['/path1', '/path2'].some(path => location.pathname.startsWith(path) ) // 3. 带参数匹配 activeRule: (location) => /^\/user\/\d+/.test(location.pathname) // 4. 查询参数匹配 activeRule: (location) => new URLSearchParams(location.search).get('app') === 'module1' ``` ## 🔧 Container 配置 ### CSS 选择器 ```typescript { container: '#micro-app-container' } ``` ### DOM 节点 ```typescript { container: document.querySelector('#container') } ``` ## 📨 Props 数据传递 微应用可以通过 props 参数接收主应用传递的数据: ```typescript // 主应用 registerMicroApps([{ name: 'child-app', // ... props: { data: { user: 'john' }, methods: { onGlobalStateChange: (state) => console.log(state), setGlobalState: (state) => updateGlobalState(state) } } }]); ``` ```typescript // 微应用 export async function mount(props) { console.log(props.data); // { user: 'john' } console.log(props.methods); // { onGlobalStateChange, setGlobalState } } ``` ## ⚠️ 注意事项 ### 应用名称唯一性 ```typescript // ❌ 错误:重复的应用名称 registerMicroApps([ { name: 'app1', entry: '//localhost:8080', /*...*/ }, { name: 'app1', entry: '//localhost:8081', /*...*/ }, // 重复! ]); // ✅ 正确:唯一的应用名称 registerMicroApps([ { name: 'app1', entry: '//localhost:8080', /*...*/ }, { name: 'app2', entry: '//localhost:8081', /*...*/ }, ]); ``` ### 容器节点存在性 ```typescript // ❌ 错误:容器节点不存在 registerMicroApps([{ container: '#non-existent-container', // DOM 中不存在 // ... }]); // ✅ 正确:确保容器节点存在 registerMicroApps([{ container: '#app-container', // 确保 DOM 中存在 // ... }]); ``` ### 重复注册 ```typescript // ❌ 错误:重复注册会导致应用重复加载 registerMicroApps([...]); registerMicroApps([...]); // 重复注册 // ✅ 正确:只注册一次 registerMicroApps([...]); ``` ## 🚀 最佳实践 ### 1. 应用配置管理 ```typescript // 推荐:将应用配置抽取为单独文件 const microApps = [ { name: 'order-management', entry: getAppEntry('order'), container: '#subapp-container', activeRule: '/order', props: getAppProps('order') }, // ... ]; registerMicroApps(microApps, { beforeLoad: [initLoadingUI], afterMount: [removeLoadingUI], }); ``` ### 2. 环境配置 ```typescript const getAppEntry = (name: string) => { const entries = { development: { order: '//localhost:8001', user: '//localhost:8002' }, production: { order: '//order.example.com', user: '//user.example.com' } }; return entries[process.env.NODE_ENV][name]; }; ``` ### 3. 统一错误处理 ```typescript registerMicroApps(microApps, { beforeLoad: (app) => { console.log(`Loading ${app.name}...`); }, afterMount: (app) => { console.log(`${app.name} mounted successfully`); }, beforeUnmount: (app) => { // 清理全局状态 cleanupGlobalState(app.name); } }); ``` ## 🔗 相关 API - [start](/api/start) - 启动 qiankun - [loadMicroApp](/api/load-micro-app) - 手动加载微应用 - [生命周期](/api/lifecycles) - 详细的生命周期说明 ================================================ FILE: docs/api/start.md ================================================ # start Start the qiankun framework. This function initializes the micro-frontend system and enables automatic routing-based micro application loading. ## 🎯 Function Signature ```typescript function start(opts?: StartOpts): void ``` ## 📋 Parameters ### opts - **Type**: `StartOpts` - **Required**: ❌ - **Description**: Startup configuration options ```typescript interface StartOpts { prefetch?: boolean | 'all' | string[] | ((apps: RegistrableApp[]) => { criticalAppNames: string[]; minorAppsName: string[] }); sandbox?: boolean | { strictStyleIsolation?: boolean; experimentalStyleIsolation?: boolean; }; singular?: boolean; urlRerouteOnly?: boolean; // ... other single-spa start options } ``` | Option | Type | Default | Description | |--------|------|---------|-------------| | `prefetch` | `boolean \| 'all' \| string[] \| Function` | `true` | Resource prefetch strategy | | `sandbox` | `boolean \| SandboxOpts` | `true` | Sandbox isolation configuration | | `singular` | `boolean` | `true` | Whether only one micro app can be mounted at a time | | `urlRerouteOnly` | `boolean` | `true` | Whether to trigger routing only on URL changes | ## 💡 Usage Examples ### Basic Usage ```typescript import { registerMicroApps, start } from 'qiankun'; // Register micro apps first registerMicroApps([ { name: 'react-app', entry: '//localhost:7100', container: '#subapp-viewport', activeRule: '/react', }, { name: 'vue-app', entry: '//localhost:7101', container: '#subapp-viewport', activeRule: '/vue', }, ]); // Start qiankun start(); ``` ### With Configuration ```typescript start({ prefetch: false, // Disable prefetch sandbox: true, // Enable sandbox singular: true, // Only one app at a time urlRerouteOnly: true, // Route only on URL changes }); ``` ### Advanced Sandbox Configuration ```typescript start({ sandbox: { strictStyleIsolation: true, // Enable strict style isolation experimentalStyleIsolation: true, // Enable experimental style isolation } }); ``` ### Custom Prefetch Strategy ```typescript start({ prefetch: 'all', // Prefetch all micro apps }); // Or prefetch specific apps start({ prefetch: ['react-app', 'vue-app'], // Only prefetch these apps }); // Or custom prefetch function start({ prefetch: (apps) => ({ criticalAppNames: ['dashboard', 'user-center'], // Critical apps to prefetch immediately minorAppsName: ['analytics', 'settings'], // Minor apps to prefetch later }) }); ``` ## ⚙️ Configuration Options ### Prefetch Strategies #### 1. Boolean Values ```typescript // Disable prefetch completely start({ prefetch: false }); // Enable default prefetch behavior start({ prefetch: true }); ``` #### 2. Prefetch All ```typescript // Prefetch all registered micro apps start({ prefetch: 'all' }); ``` #### 3. Selective Prefetch ```typescript // Prefetch only specified apps start({ prefetch: ['critical-app1', 'critical-app2'] }); ``` #### 4. Dynamic Prefetch Strategy ```typescript start({ prefetch: (apps) => { // Custom logic to determine which apps to prefetch const criticalApps = apps .filter(app => app.name.includes('critical')) .map(app => app.name); const minorApps = apps .filter(app => !app.name.includes('critical')) .map(app => app.name); return { criticalAppNames: criticalApps, // Prefetch immediately minorAppsName: minorApps, // Prefetch when idle }; } }); ``` ### Sandbox Configuration #### 1. Boolean Sandbox ```typescript // Enable basic sandbox start({ sandbox: true }); // Disable sandbox (not recommended) start({ sandbox: false }); ``` #### 2. Advanced Sandbox ```typescript start({ sandbox: { strictStyleIsolation: true, // Shadow DOM based style isolation experimentalStyleIsolation: true, // Scoped CSS based style isolation } }); ``` ### Performance Options ```typescript start({ singular: false, // Allow multiple apps to mount simultaneously urlRerouteOnly: false, // Trigger routing on both URL and programmatic changes }); ``` ## 🚀 Best Practices ### 1. Call After Registration ```typescript // ✅ Correct order registerMicroApps([...]); start(); // ❌ Wrong order start(); registerMicroApps([...]); // This won't work properly ``` ### 2. Environment-based Configuration ```typescript const startOpts = { prefetch: process.env.NODE_ENV === 'production' ? 'all' : false, sandbox: { strictStyleIsolation: process.env.NODE_ENV === 'production', }, }; start(startOpts); ``` ### 3. Performance Optimization ```typescript // For better performance in production start({ prefetch: (apps) => ({ criticalAppNames: ['dashboard'], // Only prefetch critical apps minorAppsName: [], // Don't prefetch minor apps }), singular: true, // Prevent memory issues sandbox: { strictStyleIsolation: false, // Use lightweight style isolation experimentalStyleIsolation: true, }, }); ``` ### 4. Development vs Production ```typescript if (process.env.NODE_ENV === 'development') { start({ prefetch: false, // Faster development reload sandbox: false, // Easier debugging singular: false, // More flexible development }); } else { start({ prefetch: 'all', // Better user experience sandbox: true, // Better isolation singular: true, // Stable performance }); } ``` ## 🔧 Integration Patterns ### 1. With Loading States ```typescript import { registerMicroApps, start } from 'qiankun'; let isQiankunStarted = false; function startQiankunWithLoading() { if (isQiankunStarted) return; showGlobalLoading(); registerMicroApps([...], { beforeLoad: (app) => { console.log(`Loading ${app.name}...`); }, afterMount: (app) => { console.log(`${app.name} mounted`); hideGlobalLoading(); }, }); start({ prefetch: 'all', sandbox: true, }); isQiankunStarted = true; } ``` ### 2. With Error Handling ```typescript function startQiankunSafely() { try { registerMicroApps([...]); start({ prefetch: 'all', sandbox: true, }); console.log('Qiankun started successfully'); } catch (error) { console.error('Failed to start qiankun:', error); // Fallback to traditional routing or show error page window.location.href = '/fallback'; } } ``` ### 3. With Feature Detection ```typescript import { isRuntimeCompatible } from 'qiankun'; if (isRuntimeCompatible()) { registerMicroApps([...]); start(); } else { console.warn('Browser not compatible with qiankun'); // Fallback implementation initTraditionalRouting(); } ``` ## ⚠️ Important Notes ### 1. Call Only Once ```typescript // ❌ Bad: Multiple calls start(); start(); // This will be ignored // ✅ Good: Single call start(); ``` ### 2. Order Matters ```typescript // ✅ Correct order registerMicroApps([...]); // 1. Register apps first start(); // 2. Then start // ❌ Wrong order - apps won't be registered properly start(); registerMicroApps([...]); ``` ### 3. Prefetch Considerations ```typescript // ⚠️ Be careful with 'all' in large applications start({ prefetch: 'all' }); // Might impact initial load performance // ✅ Better: Selective prefetch start({ prefetch: ['critical-app1', 'critical-app2'] }); ``` ## 🎯 Common Use Cases ### 1. E-commerce Platform ```typescript registerMicroApps([ { name: 'product-catalog', entry: '//catalog.example.com', activeRule: '/products' }, { name: 'shopping-cart', entry: '//cart.example.com', activeRule: '/cart' }, { name: 'user-account', entry: '//account.example.com', activeRule: '/account' }, ]); start({ prefetch: (apps) => ({ criticalAppNames: ['shopping-cart'], // Always prefetch cart minorAppsName: ['user-account'], // Prefetch account when idle }), sandbox: true, singular: true, }); ``` ### 2. Admin Dashboard ```typescript start({ prefetch: false, // Don't prefetch - admin tools are used on demand sandbox: { strictStyleIsolation: true, // Prevent style conflicts between admin tools }, singular: false, // Allow multiple admin tools open simultaneously }); ``` ### 3. Multi-tenant Platform ```typescript const tenantId = getCurrentTenantId(); start({ prefetch: [`tenant-${tenantId}-dashboard`], // Only prefetch current tenant's apps sandbox: true, // Isolate tenant data singular: true, }); ``` ## 🔗 Related APIs - [registerMicroApps](/api/register-micro-apps) - Register micro applications - [loadMicroApp](/api/load-micro-app) - Manually load micro applications - [isRuntimeCompatible](/api/is-runtime-compatible) - Check browser compatibility ================================================ FILE: docs/api/types.md ================================================ # TypeScript Types qiankun provides comprehensive TypeScript type definitions to ensure type safety and excellent developer experience. This document covers all available types and interfaces. ## 📋 Core Types ### ObjectType **Description**: Base type for generic object structures. ```typescript export type ObjectType = Record; ``` **Usage**: ```typescript // Used as a constraint for generic types function processApp(props: T): void { // T can be any object type } ``` ### HTMLEntry **Description**: Type for micro application entry points. ```typescript export type HTMLEntry = string; ``` **Usage**: ```typescript const appEntry: HTMLEntry = '//localhost:8080'; const appEntryWithPath: HTMLEntry = '//localhost:8080/micro-app'; ``` ## 🏗️ Application Types ### AppMetadata **Description**: Base metadata for micro applications. ```typescript type AppMetadata = { name: string; // Unique application name entry: HTMLEntry; // Application entry URL }; ``` ### LoadableApp\ **Description**: Configuration for manually loaded micro applications. ```typescript export type LoadableApp = AppMetadata & { container: HTMLElement; // DOM container element props?: T; // Custom properties passed to the app }; ``` **Usage**: ```typescript // Basic usage const app: LoadableApp<{}> = { name: 'my-app', entry: '//localhost:8080', container: document.getElementById('app-container')!, }; // With custom props interface MyAppProps { theme: 'light' | 'dark'; userId: string; } const appWithProps: LoadableApp = { name: 'themed-app', entry: '//localhost:8080', container: document.getElementById('container')!, props: { theme: 'dark', userId: '123' } }; ``` ### RegistrableApp\ **Description**: Configuration for route-based micro applications. ```typescript export type RegistrableApp = LoadableApp & { loader?: (loading: boolean) => void; // Loading state callback activeRule: RegisterApplicationConfig['activeWhen']; // Routing activation rule }; ``` **Usage**: ```typescript import { registerMicroApps } from 'qiankun'; interface UserAppProps { currentUser: { id: string; name: string }; } const apps: RegistrableApp[] = [ { name: 'user-dashboard', entry: '//localhost:8001', container: '#subapp-viewport', activeRule: '/dashboard', props: { currentUser: { id: '123', name: 'John' } }, loader: (loading) => { if (loading) { showLoadingSpinner(); } else { hideLoadingSpinner(); } } } ]; registerMicroApps(apps); ``` ## ⚙️ Configuration Types ### AppConfiguration **Description**: Configuration options for individual micro applications. ```typescript export type AppConfiguration = Partial> & { sandbox?: boolean; // Enable sandbox isolation globalContext?: WindowProxy; // Custom global context }; ``` **Usage**: ```typescript import { loadMicroApp } from 'qiankun'; const customConfig: AppConfiguration = { sandbox: true, globalContext: window, fetch: async (url, options) => { // Custom fetch implementation return fetch(url, { ...options, headers: { ...options?.headers, 'Authorization': 'Bearer token' } }); }, nodeTransformer: (node, opts) => { // Transform DOM nodes if (node.tagName === 'SCRIPT') { node.setAttribute('data-app', 'my-app'); } return node; } }; loadMicroApp({ name: 'configured-app', entry: '//localhost:8080', container: document.getElementById('container')! }, customConfig); ``` ## 🔄 Lifecycle Types ### LifeCycleFn\ **Description**: Type for lifecycle hook functions. ```typescript export type LifeCycleFn = ( app: LoadableApp, global: WindowProxy ) => Promise; ``` **Usage**: ```typescript const beforeLoadHook: LifeCycleFn<{ theme: string }> = async (app, global) => { console.log(`Loading app: ${app.name}`); global.__APP_THEME__ = app.props?.theme || 'default'; }; const afterMountHook: LifeCycleFn = async (app, global) => { console.log(`App ${app.name} mounted successfully`); // Track analytics analytics.track('app_mounted', { appName: app.name }); }; ``` ### LifeCycles\ **Description**: Complete lifecycle hooks configuration. ```typescript export type LifeCycles = { beforeLoad?: LifeCycleFn | Array>; beforeMount?: LifeCycleFn | Array>; afterMount?: LifeCycleFn | Array>; beforeUnmount?: LifeCycleFn | Array>; afterUnmount?: LifeCycleFn | Array>; }; ``` **Usage**: ```typescript interface AppProps { userId: string; permissions: string[]; } const lifecycles: LifeCycles = { beforeLoad: async (app, global) => { // Setup before loading global.__USER_ID__ = app.props?.userId; }, beforeMount: [ async (app, global) => { // Multiple hooks as array await setupAuthentication(app.props?.userId); }, async (app, global) => { await loadUserPermissions(app.props?.permissions); } ], afterMount: async (app) => { console.log(`${app.name} is ready`); }, beforeUnmount: async (app) => { // Cleanup before unmounting await saveUserState(app.name); }, afterUnmount: async (app) => { // Final cleanup await clearUserData(app.name); } }; ``` ## 🎯 Micro App Types ### MicroApp **Description**: Instance of a loaded micro application. ```typescript export type MicroApp = Parcel; ``` The `MicroApp` type extends the single-spa `Parcel` interface with these methods: ```typescript interface MicroApp { mount(): Promise; // Mount the application unmount(): Promise; // Unmount the application update(props: any): Promise; // Update application props getStatus(): string; // Get current status loadPromise: Promise; // Promise that resolves when loaded mountPromise: Promise; // Promise that resolves when mounted unmountPromise: Promise; // Promise that resolves when unmounted } ``` **Usage**: ```typescript import { loadMicroApp } from 'qiankun'; const microApp: MicroApp = loadMicroApp({ name: 'my-app', entry: '//localhost:8080', container: document.getElementById('container')! }); // Check status console.log(microApp.getStatus()); // 'LOADING', 'MOUNTED', 'UNMOUNTED', etc. // Wait for mounting await microApp.mountPromise; console.log('App is mounted'); // Update props await microApp.update({ newTheme: 'dark' }); // Unmount when done await microApp.unmount(); ``` ### MicroAppLifeCycles **Description**: Internal lifecycle type used by qiankun. ```typescript export type MicroAppLifeCycles = FlattenArrayValue>; ``` This type is primarily for internal use and represents the flattened lifecycle functions that micro applications must export. ## 🌐 Global Types ### Window Extensions qiankun extends the global `Window` interface with special properties: ```typescript declare global { interface Window { __POWERED_BY_QIANKUN__?: boolean; // Indicates app is running in qiankun __INJECTED_PUBLIC_PATH_BY_QIANKUN__?: string; // Injected public path __QIANKUN_DEVELOPMENT__?: boolean; // Development mode flag Zone?: CallableFunction; // Zone.js compatibility __zone_symbol__setTimeout?: Window['setTimeout']; // Zone.js timeout } } ``` **Usage in Micro Applications**: ```typescript // Check if running in qiankun if (window.__POWERED_BY_QIANKUN__) { console.log('Running as a micro app'); // Use injected public path const publicPath = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ || '/'; // Configure your app accordingly setupApp({ publicPath }); } else { console.log('Running standalone'); setupApp({ publicPath: '/' }); } ``` ## 🎨 Utility Types ### Custom Type Guards Create type guards for better type safety: ```typescript // Type guard for LoadableApp function isLoadableApp( app: any ): app is LoadableApp { return ( typeof app === 'object' && typeof app.name === 'string' && typeof app.entry === 'string' && app.container instanceof HTMLElement ); } // Type guard for RegistrableApp function isRegistrableApp( app: any ): app is RegistrableApp { return ( isLoadableApp(app) && (typeof app.activeRule === 'string' || typeof app.activeRule === 'function') ); } // Usage function processApp(app: unknown) { if (isRegistrableApp(app)) { // TypeScript knows app is RegistrableApp here console.log(`Registering app: ${app.name} with rule: ${app.activeRule}`); } else if (isLoadableApp(app)) { // TypeScript knows app is LoadableApp here console.log(`Loading app: ${app.name}`); } } ``` ### Generic Helper Types Create reusable generic types for common patterns: ```typescript // Props with theme support type ThemedProps = T & { theme?: 'light' | 'dark'; }; // Props with user context type UserAwareProps = T & { currentUser?: { id: string; name: string; role: string; }; }; // Combined props type AppProps = ThemedProps>; // Usage const app: LoadableApp> = { name: 'themed-user-app', entry: '//localhost:8080', container: document.getElementById('container')!, props: { theme: 'dark', currentUser: { id: '123', name: 'John', role: 'admin' }, customData: 'custom value' } }; ``` ## 📖 Advanced Type Patterns ### Conditional Types for Configuration ```typescript // Configuration based on environment type EnvironmentConfig = T extends 'development' ? { sandbox: false; prefetch: false; strictStyleIsolation: false; } : { sandbox: true; prefetch: 'all'; strictStyleIsolation: true; }; // Usage with environment detection declare const NODE_ENV: 'development' | 'production'; type CurrentConfig = EnvironmentConfig; ``` ### Branded Types for App Names ```typescript // Create branded type for app names to prevent mix-ups type AppName = string & { readonly __brand: unique symbol }; function createAppName(name: string): AppName { return name as AppName; } // Enhanced LoadableApp with branded name type SafeLoadableApp = Omit, 'name'> & { name: AppName; }; // Usage const appName = createAppName('my-secure-app'); const app: SafeLoadableApp<{}> = { name: appName, // Type-safe app name entry: '//localhost:8080', container: document.getElementById('container')! }; ``` ### Lifecycle Event Types ```typescript // Enhanced lifecycle with event data type LifeCycleEvent = { app: LoadableApp; global: WindowProxy; timestamp: number; phase: 'beforeLoad' | 'beforeMount' | 'afterMount' | 'beforeUnmount' | 'afterUnmount'; }; type EnhancedLifeCycleFn = (event: LifeCycleEvent) => Promise; // Usage const enhancedHook: EnhancedLifeCycleFn<{ userId: string }> = async (event) => { console.log(`Phase: ${event.phase}, App: ${event.app.name}, Time: ${event.timestamp}`); if (event.phase === 'beforeMount') { // Setup user context event.global.__USER_ID__ = event.app.props?.userId; } }; ``` ## 🔍 Type Inference Examples ### Automatic Props Type Inference ```typescript // Helper function with automatic type inference function createTypedApp( config: { name: string; entry: string; container: HTMLElement; props: T; } ): LoadableApp { return config; // TypeScript infers the correct type } // Usage - TypeScript automatically infers the props type const app = createTypedApp({ name: 'inferred-app', entry: '//localhost:8080', container: document.getElementById('container')!, props: { theme: 'dark', userId: '123', features: ['feature1', 'feature2'] } // TypeScript knows props type is { theme: string; userId: string; features: string[] } }); ``` ### Lifecycle Type Inference ```typescript // Helper for creating typed lifecycles function createLifecycles( lifecycles: LifeCycles ): LifeCycles { return lifecycles; } // Usage with inference const typedLifecycles = createLifecycles({ beforeMount: async (app) => { // TypeScript infers app.props type based on usage console.log(app.props?.theme); // TypeScript knows this might be undefined } }); ``` ## ⚡ Best Practices ### 1. Use Strict Types ```typescript // ✅ Good: Strict typing interface StrictAppProps { readonly userId: string; readonly theme: 'light' | 'dark'; readonly permissions: readonly string[]; } const app: LoadableApp = { name: 'strict-app', entry: '//localhost:8080', container: document.getElementById('container')!, props: { userId: '123', theme: 'dark', permissions: ['read', 'write'] } }; // ❌ Bad: Loose typing const looseApp: LoadableApp = { name: 'loose-app', entry: '//localhost:8080', container: document.getElementById('container')!, props: { anything: 'goes' } // No type safety }; ``` ### 2. Create Domain-Specific Types ```typescript // Create types specific to your domain interface ECommerceAppProps { cartId: string; currency: 'USD' | 'EUR' | 'GBP'; customerSegment: 'premium' | 'standard'; features: { wishlist: boolean; recommendations: boolean; reviews: boolean; }; } type ECommerceApp = LoadableApp; type ECommerceLifecycles = LifeCycles; ``` ### 3. Use Generic Constraints ```typescript // Constrain generic types for better type safety interface BaseAppProps { version: string; environment: 'development' | 'staging' | 'production'; } function createApp( config: Omit, 'container'> & { containerId: string; } ): LoadableApp { const container = document.getElementById(config.containerId); if (!container) { throw new Error(`Container ${config.containerId} not found`); } return { ...config, container }; } ``` ## 🔗 Related Documentation - [API Reference](/api/) - Main API documentation - [Lifecycles](/api/lifecycles) - Detailed lifecycle documentation - [Configuration](/api/configuration) - Configuration options ================================================ FILE: docs/cookbook/error-handling.md ================================================ # Error Handling Robust error handling is essential for micro-frontend applications where multiple independent applications run within the same context. This guide covers comprehensive strategies for handling errors, implementing graceful degradation, and maintaining application stability across qiankun-based micro-frontend systems. ## 🎯 Error Types in Micro-Frontends ### Common Error Categories Micro-frontend applications face unique error scenarios: - **Loading Errors**: Failed to fetch or parse micro application resources - **Runtime Errors**: JavaScript errors within micro applications - **Communication Errors**: Failed inter-application communication - **Network Errors**: API calls and resource loading failures - **Sandbox Errors**: Issues with JavaScript and CSS isolation - **Lifecycle Errors**: Problems during mount/unmount processes - **Version Conflicts**: Dependency version mismatches ### Error Impact Assessment ```javascript // Error severity levels for micro-frontend applications const ERROR_LEVELS = { CRITICAL: 'critical', // Main app or core functionality affected HIGH: 'high', // Major micro app functionality lost MEDIUM: 'medium', // Partial micro app functionality affected LOW: 'low', // Minor features or visual issues INFO: 'info' // Non-blocking informational issues }; const ErrorClassifier = { classify(error, appName, context) { // Critical: Main app crashes or core navigation fails if (appName === 'main' || context.includes('navigation')) { return ERROR_LEVELS.CRITICAL; } // High: User cannot complete primary workflows if (context.includes('checkout') || context.includes('auth')) { return ERROR_LEVELS.HIGH; } // Medium: Feature degradation but app still usable if (error.name === 'ChunkLoadError' || error.name === 'TypeError') { return ERROR_LEVELS.MEDIUM; } // Default to low for other errors return ERROR_LEVELS.LOW; } }; ``` ## 🛡️ qiankun Error Boundaries ### Global Error Handling Set up global error handlers for the entire micro-frontend ecosystem: ```javascript import { addGlobalUncaughtErrorHandler, removeGlobalUncaughtErrorHandler } from 'qiankun'; // Global error handler for all micro apps const globalErrorHandler = (event) => { const { error, appName, lifecycleName } = event; console.error(`Error in micro app "${appName}" during "${lifecycleName}":`, error); // Report to error tracking service reportError({ error, appName, lifecycle: lifecycleName, timestamp: Date.now(), userAgent: navigator.userAgent, url: window.location.href }); // Implement recovery strategy handleMicroAppError(appName, error, lifecycleName); }; // Register global error handler addGlobalUncaughtErrorHandler(globalErrorHandler); // Remove when cleaning up (e.g., in app unmount) // removeGlobalUncaughtErrorHandler(globalErrorHandler); ``` ### Lifecycle-Specific Error Handling ```javascript // Error handling in lifecycle hooks const errorHandlingLifecycles = { async beforeLoad(app) { try { // Pre-loading checks const healthCheck = await fetch(`${app.entry}/health`); if (!healthCheck.ok) { throw new Error(`Health check failed for ${app.name}`); } } catch (error) { console.warn(`Pre-load health check failed for ${app.name}:`, error); // Continue with loading but flag as potentially unstable markAppAsUnstable(app.name); } }, async beforeMount(app) { try { // Validate app requirements validateAppRequirements(app); } catch (error) { // Attempt to fix common issues await attemptAutoFix(app, error); } }, async afterMount(app) { // Verify successful mount setTimeout(() => { const container = document.querySelector(app.container); if (!container || container.children.length === 0) { console.error(`Mount verification failed for ${app.name}`); showFallbackContent(app.container, app.name); } }, 1000); }, async beforeUnmount(app) { try { // Clean up resources cleanupAppResources(app.name); } catch (error) { console.warn(`Cleanup error for ${app.name}:`, error); // Force cleanup forceCleanup(app.name); } } }; ``` ## 🚨 Framework-Specific Error Boundaries ### React Error Boundaries ```jsx // React error boundary for micro applications import React from 'react'; class MicroAppErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false, error: null, errorInfo: null, retryCount: 0, lastRetry: null }; } static getDerivedStateFromError(error) { return { hasError: true }; } componentDidCatch(error, errorInfo) { this.setState({ error, errorInfo }); // Report error this.reportError(error, errorInfo); // Attempt automatic recovery this.attemptRecovery(error); } reportError = (error, errorInfo) => { const errorReport = { error: { name: error.name, message: error.message, stack: error.stack }, errorInfo, appName: this.props.appName, timestamp: Date.now(), url: window.location.href, userAgent: navigator.userAgent, retryCount: this.state.retryCount }; // Send to error tracking service fetch('/api/errors', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(errorReport) }).catch(err => console.error('Failed to report error:', err)); }; attemptRecovery = (error) => { const { retryCount, lastRetry } = this.state; const now = Date.now(); // Prevent too frequent retries if (lastRetry && now - lastRetry < 5000) { return; } // Limit retry attempts if (retryCount >= 3) { console.error(`Max retry attempts reached for ${this.props.appName}`); return; } setTimeout(() => { this.setState({ hasError: false, error: null, errorInfo: null, retryCount: retryCount + 1, lastRetry: now }); }, 2000 * Math.pow(2, retryCount)); // Exponential backoff }; render() { if (this.state.hasError) { const { appName, fallbackComponent: FallbackComponent } = this.props; if (FallbackComponent) { return ( this.attemptRecovery(this.state.error)} /> ); } return (

Application Error

The {appName} application encountered an error.

Error Details
{this.state.error?.stack}
); } return this.props.children; } } // Usage with micro app function MicroAppContainer({ appName, entry }) { return (
); } ``` ### Vue Error Handling ```javascript // Vue global error handler for micro apps const app = createApp(MainApp); app.config.errorHandler = (err, instance, info) => { const appName = instance?.$root?.$options?.name || 'unknown'; console.error(`Vue error in ${appName}:`, err, info); // Report error reportVueError({ error: err, appName, info, timestamp: Date.now() }); // Attempt recovery if (instance && typeof instance.$forceUpdate === 'function') { instance.$forceUpdate(); } }; // Vue 2 error boundary component Vue.component('ErrorBoundary', { data() { return { hasError: false, error: null }; }, errorCaptured(err, instance, info) { this.hasError = true; this.error = err; // Report error this.reportError(err, info); // Prevent error from propagating return false; }, methods: { reportError(error, info) { // Error reporting logic }, retry() { this.hasError = false; this.error = null; this.$forceUpdate(); } }, render(h) { if (this.hasError) { return h('div', { class: 'error-boundary' }, [ h('h3', 'Something went wrong'), h('button', { on: { click: this.retry } }, 'Retry'), h('pre', this.error?.message) ]); } return this.$slots.default; } }); ``` ## 🔄 Graceful Degradation Strategies ### Progressive Enhancement ```javascript // Progressive feature loading with fallbacks class FeatureLoader { constructor() { this.features = new Map(); this.fallbacks = new Map(); } register(featureName, loader, fallback) { this.features.set(featureName, loader); this.fallbacks.set(featureName, fallback); } async load(featureName) { try { const loader = this.features.get(featureName); if (!loader) { throw new Error(`Feature "${featureName}" not registered`); } const feature = await loader(); return feature; } catch (error) { console.warn(`Failed to load feature "${featureName}":`, error); const fallback = this.fallbacks.get(featureName); if (fallback) { return await fallback(); } throw error; } } } // Usage example const featureLoader = new FeatureLoader(); // Register advanced dashboard with fallback featureLoader.register( 'advanced-dashboard', () => import('./AdvancedDashboard'), () => import('./BasicDashboard') ); // Register chart component with static fallback featureLoader.register( 'interactive-charts', () => import('./InteractiveCharts'), () => Promise.resolve(() => '
Charts unavailable
') ); ``` ### Fallback UI Components ```jsx // Comprehensive fallback components const ErrorFallbacks = { // Network error fallback NetworkError: ({ onRetry, appName }) => (
🌐

Connection Problem

Unable to load {appName}. Please check your internet connection.

), // JavaScript error fallback JavaScriptError: ({ error, appName, onRetry }) => (
⚠️

Application Error

The {appName} application encountered a technical issue.

{process.env.NODE_ENV === 'development' && (
Technical Details
{error.stack}
)}
), // Loading timeout fallback LoadingTimeout: ({ appName, onRetry }) => (
⏱️

Loading Timeout

{appName} is taking longer than expected to load.

), // Generic fallback Generic: ({ error, appName, onRetry }) => (
🔧

Temporary Issue

We're experiencing technical difficulties with {appName}.

) }; ``` ### Circuit Breaker Pattern ```javascript // Circuit breaker for micro app loading class CircuitBreaker { constructor(threshold = 5, timeout = 60000, monitor = 30000) { this.failureThreshold = threshold; this.timeout = timeout; this.monitoringPeriod = monitor; this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN this.failureCount = 0; this.lastFailureTime = null; this.nextAttemptTime = null; } async execute(operation, appName) { if (this.state === 'OPEN') { if (Date.now() < this.nextAttemptTime) { throw new Error(`Circuit breaker is OPEN for ${appName}`); } this.state = 'HALF_OPEN'; } try { const result = await operation(); this.onSuccess(); return result; } catch (error) { this.onFailure(); throw error; } } onSuccess() { this.failureCount = 0; this.state = 'CLOSED'; } onFailure() { this.failureCount++; this.lastFailureTime = Date.now(); if (this.failureCount >= this.failureThreshold) { this.state = 'OPEN'; this.nextAttemptTime = Date.now() + this.timeout; } } getState() { return this.state; } } // Usage with micro app loading const circuitBreakers = new Map(); const loadMicroAppWithCircuitBreaker = async (appConfig) => { const { name } = appConfig; if (!circuitBreakers.has(name)) { circuitBreakers.set(name, new CircuitBreaker()); } const breaker = circuitBreakers.get(name); try { return await breaker.execute(() => loadMicroApp(appConfig), name); } catch (error) { console.error(`Circuit breaker prevented loading ${name}:`, error); throw error; } }; ``` ## 📊 Error Monitoring and Reporting ### Comprehensive Error Tracking ```javascript // Advanced error tracking system class ErrorTracker { constructor(config) { this.config = { endpoint: '/api/errors', batchSize: 10, batchTimeout: 5000, maxRetries: 3, ...config }; this.errorQueue = []; this.batchTimeout = null; this.retryCount = new Map(); } track(error, context = {}) { const errorData = this.serializeError(error, context); // Add to queue this.errorQueue.push(errorData); // Process batch if queue is full if (this.errorQueue.length >= this.config.batchSize) { this.processBatch(); } else { // Set timeout for batch processing this.scheduleBatchProcessing(); } } serializeError(error, context) { return { id: this.generateErrorId(), timestamp: Date.now(), error: { name: error.name, message: error.message, stack: error.stack, fileName: error.fileName, lineNumber: error.lineNumber, columnNumber: error.columnNumber }, context: { appName: context.appName || 'unknown', userId: context.userId, sessionId: this.getSessionId(), url: window.location.href, userAgent: navigator.userAgent, viewport: { width: window.innerWidth, height: window.innerHeight }, ...context }, environment: { isDevelopment: process.env.NODE_ENV === 'development', timestamp: Date.now(), timezone: Intl.DateTimeFormat().resolvedOptions().timeZone } }; } scheduleBatchProcessing() { if (this.batchTimeout) { clearTimeout(this.batchTimeout); } this.batchTimeout = setTimeout(() => { this.processBatch(); }, this.config.batchTimeout); } async processBatch() { if (this.errorQueue.length === 0) return; const batch = this.errorQueue.splice(0, this.config.batchSize); try { await this.sendErrors(batch); // Clear retry count on success batch.forEach(error => { this.retryCount.delete(error.id); }); } catch (error) { console.error('Failed to send error batch:', error); // Retry logic batch.forEach(errorData => { const retries = this.retryCount.get(errorData.id) || 0; if (retries < this.config.maxRetries) { this.retryCount.set(errorData.id, retries + 1); this.errorQueue.unshift(errorData); // Add back to front of queue } }); } } async sendErrors(errors) { const response = await fetch(this.config.endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ errors }) }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return response.json(); } generateErrorId() { return `error_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } getSessionId() { // Implementation to get/generate session ID return sessionStorage.getItem('sessionId') || 'anonymous'; } } // Initialize global error tracker const errorTracker = new ErrorTracker(); // Track unhandled errors window.addEventListener('error', (event) => { errorTracker.track(event.error, { type: 'unhandled_error', source: 'window.onerror' }); }); // Track unhandled promise rejections window.addEventListener('unhandledrejection', (event) => { errorTracker.track(event.reason, { type: 'unhandled_rejection', source: 'unhandledrejection' }); }); ``` ### Performance Impact Monitoring ```javascript // Monitor error impact on performance class ErrorImpactMonitor { constructor() { this.errorImpacts = new Map(); this.performanceBaseline = this.measureBaseline(); } measureBaseline() { return { loadTime: performance.now(), memoryUsage: performance.memory ? performance.memory.usedJSHeapSize : 0, timing: performance.timing }; } recordErrorImpact(errorId, appName) { const impact = { errorId, appName, timestamp: Date.now(), performance: { loadTime: performance.now(), memoryUsage: performance.memory ? performance.memory.usedJSHeapSize : 0, timing: performance.timing }, userExperience: { pageVisible: !document.hidden, userActive: this.isUserActive(), scrollPosition: window.scrollY } }; this.errorImpacts.set(errorId, impact); this.analyzeImpact(impact); } analyzeImpact(impact) { const { performance: current } = impact; const baseline = this.performanceBaseline; const memoryIncrease = current.memoryUsage - baseline.memoryUsage; const loadTimeIncrease = current.loadTime - baseline.loadTime; if (memoryIncrease > 50 * 1024 * 1024) { // 50MB console.warn('High memory impact detected after error:', impact); } if (loadTimeIncrease > 5000) { // 5 seconds console.warn('Significant performance degradation after error:', impact); } } isUserActive() { // Simple user activity detection return Date.now() - this.lastUserActivity < 30000; } } ``` ## 🔧 Recovery Mechanisms ### Automatic Recovery Strategies ```javascript // Comprehensive recovery system class RecoveryManager { constructor() { this.recoveryStrategies = new Map(); this.setupDefaultStrategies(); } setupDefaultStrategies() { // Network error recovery this.register('NetworkError', async (error, context) => { await this.waitForConnection(); return this.reloadMicroApp(context.appName); }); // Chunk load error recovery this.register('ChunkLoadError', async (error, context) => { // Clear webpack cache if (window.__webpack_require__ && window.__webpack_require__.cache) { delete window.__webpack_require__.cache[error.request]; } // Reload with cache busting return this.reloadWithCacheBust(context.appName); }); // Script error recovery this.register('TypeError', async (error, context) => { // Attempt to reload dependencies await this.reloadDependencies(context.appName); return this.remountMicroApp(context.appName); }); // Memory error recovery this.register('RangeError', async (error, context) => { // Force garbage collection if (window.gc) window.gc(); // Reduce memory footprint await this.reducememoryFootprint(context.appName); return this.reloadMicroApp(context.appName); }); } register(errorType, strategy) { this.recoveryStrategies.set(errorType, strategy); } async recover(error, context) { const strategy = this.recoveryStrategies.get(error.name); if (strategy) { try { console.log(`Attempting recovery for ${error.name} in ${context.appName}`); const result = await strategy(error, context); console.log(`Recovery successful for ${context.appName}`); return result; } catch (recoveryError) { console.error(`Recovery failed for ${context.appName}:`, recoveryError); return this.fallbackRecovery(context); } } return this.fallbackRecovery(context); } async waitForConnection() { return new Promise((resolve) => { if (navigator.onLine) { resolve(); } else { const handleOnline = () => { window.removeEventListener('online', handleOnline); resolve(); }; window.addEventListener('online', handleOnline); } }); } async reloadMicroApp(appName) { // Unmount current instance try { await unmountMicroApp(appName); } catch (error) { console.warn(`Failed to unmount ${appName}:`, error); } // Reload the micro app const appConfig = getAppConfig(appName); return loadMicroApp(appConfig); } async reloadWithCacheBust(appName) { const appConfig = getAppConfig(appName); const cacheBustEntry = `${appConfig.entry}?t=${Date.now()}`; return loadMicroApp({ ...appConfig, entry: cacheBustEntry }); } async fallbackRecovery(context) { console.log(`Using fallback recovery for ${context.appName}`); // Show fallback UI showFallbackUI(context.appName); // Report recovery failure reportRecoveryFailure(context); return null; } } ``` ### User-Initiated Recovery ```jsx // User-controlled recovery interface const RecoveryPanel = ({ appName, error, onRecover, onDismiss }) => { const [recovering, setRecovering] = useState(false); const [lastAttempt, setLastAttempt] = useState(null); const handleRecover = async (strategy) => { setRecovering(true); setLastAttempt(Date.now()); try { await onRecover(strategy); } catch (error) { console.error('User-initiated recovery failed:', error); } finally { setRecovering(false); } }; const recoveryOptions = [ { key: 'reload', label: 'Reload Application', description: 'Restart the application from scratch', action: () => handleRecover('reload') }, { key: 'reset', label: 'Reset to Default', description: 'Clear all data and reload', action: () => handleRecover('reset') }, { key: 'safe-mode', label: 'Safe Mode', description: 'Load with minimal features', action: () => handleRecover('safe-mode') } ]; return (

Recovery Options for {appName}

Error: {error.message}

{lastAttempt && (

Last attempt: {new Date(lastAttempt).toLocaleTimeString()}

)}
{recoveryOptions.map(option => ( ))}
{recovering && (
Attempting recovery...
)}
); }; ``` ## 🎯 Best Practices Summary ### ✅ Error Handling Do's 1. **Implement global error handlers** for comprehensive coverage 2. **Use error boundaries** in each micro application 3. **Provide meaningful error messages** for users 4. **Implement graceful degradation** with fallback UIs 5. **Monitor and track errors** systematically 6. **Test error scenarios** during development 7. **Implement automatic recovery** where possible 8. **Clear error context** in reports 9. **Handle network failures** gracefully 10. **Provide user recovery options** ### ❌ Error Handling Don'ts 1. **Don't ignore errors** or fail silently 2. **Don't show technical details** to end users 3. **Don't retry indefinitely** without limits 4. **Don't block the entire application** for one micro app error 5. **Don't forget to clean up** after errors 6. **Don't rely solely on automatic recovery** 7. **Don't overwhelm users** with error messages 8. **Don't forget about memory leaks** in error scenarios 9. **Don't skip error testing** in production-like environments 10. **Don't ignore user feedback** about errors ### 🔄 Error Recovery Checklist ```javascript // Comprehensive error handling checklist const errorHandlingChecklist = { prevention: { validation: '✓ Input validation implemented', typeChecking: '✓ TypeScript or PropTypes used', testing: '✓ Error scenarios tested', monitoring: '✓ Health checks in place' }, detection: { globalHandlers: '✓ Global error handlers set up', boundaries: '✓ Error boundaries implemented', logging: '✓ Comprehensive error logging', alerting: '✓ Real-time error alerts' }, recovery: { gracefulDegradation: '✓ Fallback UIs implemented', automaticRecovery: '✓ Auto-recovery strategies', userRecovery: '✓ User-initiated recovery options', resourceCleanup: '✓ Proper cleanup on errors' }, learning: { errorTracking: '✓ Error analytics in place', trendAnalysis: '✓ Error trend monitoring', rootCauseAnalysis: '✓ RCA process defined', continuousImprovement: '✓ Regular error review meetings' } }; ``` ## 🔗 Related Documentation - [Performance Optimization](/cookbook/performance) - Error impact on performance - [Debugging](/cookbook/debugging) - Error debugging techniques - [Style Isolation](/cookbook/style-isolation) - CSS error handling - [Configuration](/api/configuration) - Error-related configurations ================================================ FILE: docs/cookbook/index.md ================================================ # Best Practices This section contains practical guides and best practices for building production-ready micro-frontend applications with qiankun. These guides are based on real-world experience and common challenges faced when implementing micro-frontend architectures. ## 🎯 Overview Building micro-frontends requires careful consideration of various aspects including architecture design, performance optimization, development workflow, and deployment strategies. These guides will help you avoid common pitfalls and implement robust solutions. ## 📚 Available Guides ### 🎨 [Style Isolation](/cookbook/style-isolation) Learn how to prevent CSS conflicts between micro applications and implement effective style isolation strategies. **What you'll learn:** - CSS isolation techniques - Shadow DOM implementation - CSS scoping strategies - Runtime style conflict resolution - Best practices for component libraries ### ⚡ [Performance Optimization](/cookbook/performance) Optimize your micro-frontend applications for better load times and runtime performance. **What you'll learn:** - Resource loading optimization - Bundle splitting strategies - Caching mechanisms - Lazy loading techniques - Performance monitoring ### 🛠️ [Error Handling](/cookbook/error-handling) Implement robust error handling and recovery mechanisms for micro-frontend applications. **What you'll learn:** - Error boundaries implementation - Graceful degradation strategies - Error monitoring and reporting - Recovery mechanisms - User experience considerations ### 🔍 [Debugging & Development](/cookbook/debugging) Master debugging techniques and development workflows for micro-frontend applications. **What you'll learn:** - Development environment setup - Debugging tools and techniques - Hot reload configuration - Cross-application debugging - Production debugging strategies ### 🚀 [Deployment Strategies](/cookbook/deployment) Learn deployment patterns and CI/CD strategies for micro-frontend applications. **What you'll learn:** - Independent deployment workflows - Version management - Rollback strategies - Environment configuration - Zero-downtime deployments ### 🔄 [State Management](/cookbook/state-management) Implement effective state management across micro applications. **What you'll learn:** - Cross-application state sharing - Event-driven communication - State synchronization - Data flow patterns - Store management ### 🌐 [Routing & Navigation](/cookbook/routing) Design and implement navigation patterns for micro-frontend applications. **What you'll learn:** - Route configuration strategies - Deep linking support - Navigation guards - History management - SEO considerations ### 🔒 [Security](/cookbook/security) Implement security best practices for micro-frontend architectures. **What you'll learn:** - Content Security Policy (CSP) - Cross-origin resource sharing (CORS) - Authentication and authorization - Secure communication patterns - Vulnerability prevention ### 🧪 [Testing Strategies](/cookbook/testing) Develop comprehensive testing strategies for micro-frontend applications. **What you'll learn:** - Unit testing micro applications - Integration testing strategies - End-to-end testing - Visual regression testing - Performance testing ### 📊 [Monitoring & Analytics](/cookbook/monitoring) Implement monitoring and analytics for micro-frontend applications. **What you'll learn:** - Performance monitoring - Error tracking - User analytics - Application health checks - Business metrics ## 🎯 Getting Started If you're new to qiankun or micro-frontends, we recommend starting with these guides in order: 1. **[Style Isolation](/cookbook/style-isolation)** - Essential for preventing CSS conflicts 2. **[Error Handling](/cookbook/error-handling)** - Critical for production stability 3. **[Performance Optimization](/cookbook/performance)** - Important for user experience 4. **[Debugging & Development](/cookbook/debugging)** - Improves development productivity ## 🏗️ Common Patterns ### Micro-Frontend Architecture Patterns ```mermaid graph TB A[Main Application] --> B[User Management] A --> C[Product Catalog] A --> D[Shopping Cart] A --> E[Order Processing] B --> F[User Service] C --> G[Product Service] D --> H[Cart Service] E --> I[Order Service] F --> J[User Database] G --> K[Product Database] H --> L[Session Storage] I --> M[Order Database] ``` ### Communication Patterns ```mermaid sequenceDiagram participant M as Main App participant A as Micro App A participant B as Micro App B participant S as Shared Store M->>A: Load & Mount A->>S: Subscribe to Events M->>B: Load & Mount B->>S: Subscribe to Events A->>S: Emit Event S->>B: Notify Event B->>S: Update State S->>A: Broadcast Update ``` ## 🎪 Real-World Examples ### E-commerce Platform A typical e-commerce platform might be structured as: - **Main Application**: Navigation, layout, user session - **Product Catalog**: Browse and search products - **Shopping Cart**: Manage cart items and checkout - **User Account**: Profile management and order history - **Admin Panel**: Content management and analytics ### Enterprise Dashboard An enterprise dashboard might include: - **Main Shell**: Authentication and navigation - **Analytics Module**: Business intelligence and reporting - **User Management**: Role and permission management - **Content Management**: Dynamic content editing - **Settings Module**: System configuration ## ⚠️ Common Pitfalls ### 1. Over-Engineering **Problem**: Creating too many micro applications for small features. **Solution**: Start with a monolith and extract micro applications when teams or domains naturally separate. ### 2. Shared Dependencies **Problem**: Micro applications sharing dependencies causing version conflicts. **Solution**: Use proper bundling strategies and consider module federation for shared libraries. ### 3. Performance Issues **Problem**: Multiple micro applications loading simultaneously causing performance degradation. **Solution**: Implement lazy loading, proper caching, and resource optimization. ### 4. Testing Complexity **Problem**: Testing micro applications in isolation doesn't catch integration issues. **Solution**: Implement comprehensive integration testing alongside unit tests. ## 🔧 Development Workflow ### Recommended Development Flow 1. **Design Phase** - Define application boundaries - Plan communication patterns - Design shared interfaces 2. **Development Phase** - Set up development environment - Implement micro applications - Configure build and deployment 3. **Testing Phase** - Unit test individual applications - Integration test the complete system - Performance and security testing 4. **Deployment Phase** - Deploy applications independently - Monitor application health - Implement rollback strategies ### Team Organization ```mermaid graph LR A[Platform Team] --> B[Shared Infrastructure] A --> C[Main Application] D[Feature Team 1] --> E[Micro App 1] F[Feature Team 2] --> G[Micro App 2] H[Feature Team 3] --> I[Micro App 3] B --> E B --> G B --> I ``` ## 📖 Further Reading - [Micro Frontends Architecture](https://micro-frontends.org/) - [Module Federation](https://webpack.js.org/concepts/module-federation/) - [Single-SPA Documentation](https://single-spa.js.org/) - [qiankun GitHub Repository](https://github.com/umijs/qiankun) ## 🤝 Contributing Have a pattern or practice you'd like to share? Contributions to the cookbook are welcome! Please follow our [contribution guidelines](https://github.com/umijs/qiankun/blob/master/CONTRIBUTING.md). ## 🔗 Related Documentation - [API Reference](/api/) - Complete API documentation - [Quick Start Guide](/guide/quick-start) - Get started with qiankun - [Ecosystem](/ecosystem/) - UI bindings and tools ================================================ FILE: docs/cookbook/performance.md ================================================ # Performance Optimization Performance is crucial for micro-frontend applications. With multiple applications loading and running simultaneously, it's essential to optimize resource loading, runtime performance, and user experience. This guide covers comprehensive strategies for optimizing qiankun-based micro-frontend applications. ## 🎯 Performance Overview ### Common Performance Challenges Micro-frontend architectures introduce unique performance considerations: - **Multiple Bundle Loading**: Each micro app loads its own JavaScript and CSS - **Resource Duplication**: Shared dependencies loaded multiple times - **Runtime Overhead**: Multiple application instances running simultaneously - **Network Latency**: Additional HTTP requests for each micro app - **Memory Usage**: Increased memory consumption from multiple apps ### Performance Metrics to Monitor ```javascript // Key metrics for micro-frontend performance const performanceMetrics = { // Loading Performance timeToFirstByte: 'TTFB', firstContentfulPaint: 'FCP', largestContentfulPaint: 'LCP', // Interactivity firstInputDelay: 'FID', timeToInteractive: 'TTI', // Micro-frontend Specific microAppLoadTime: 'Custom metric', microAppMountTime: 'Custom metric', totalBundleSize: 'Custom metric' }; ``` ## 🚀 Resource Loading Optimization ### Prefetching Strategies qiankun provides several prefetching options to improve loading performance: #### Basic Prefetching ```javascript import { start } from 'qiankun'; start({ prefetch: true // Enable default prefetching }); ``` #### Selective Prefetching ```javascript start({ prefetch: ['critical-app-1', 'critical-app-2'] // Only prefetch specific apps }); ``` #### Smart Prefetching ```javascript start({ prefetch: (apps) => { // Custom prefetch logic based on user behavior, time, or network conditions const now = new Date().getHours(); const isBusinessHours = now >= 9 && now <= 17; if (isBusinessHours) { return { criticalAppNames: ['dashboard', 'user-management'], minorAppsName: ['analytics'] }; } return { criticalAppNames: ['dashboard'], minorAppsName: [] }; } }); ``` #### Network-Aware Prefetching ```javascript // Advanced prefetching based on network conditions const networkAwarePrefetch = (apps) => { const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection; if (!connection) { // Default behavior for unknown connection return { criticalAppNames: apps.slice(0, 2), minorAppsName: [] }; } const effectiveType = connection.effectiveType; const saveData = connection.saveData; if (saveData || effectiveType === 'slow-2g' || effectiveType === '2g') { // Minimal prefetching for slow connections return { criticalAppNames: [], minorAppsName: [] }; } if (effectiveType === '3g') { // Moderate prefetching for 3G return { criticalAppNames: apps.slice(0, 1), minorAppsName: [] }; } // Aggressive prefetching for 4G and above return { criticalAppNames: apps.slice(0, 3), minorAppsName: apps.slice(3) }; }; start({ prefetch: networkAwarePrefetch }); ``` ### Lazy Loading #### Route-Based Lazy Loading ```javascript // Only load micro apps when their routes are accessed registerMicroApps([ { name: 'user-management', entry: '//localhost:8080', container: '#container', activeRule: '/users', // App will only load when /users route is accessed }, { name: 'analytics', entry: '//localhost:8081', container: '#container', activeRule: '/analytics', // Loaded on demand } ]); ``` #### Conditional Loading ```javascript // Load micro apps based on user permissions or features const userPermissions = getCurrentUserPermissions(); const microApps = [ { name: 'dashboard', entry: '//localhost:8080', container: '#container', activeRule: '/dashboard' } ]; // Conditionally add admin app if (userPermissions.includes('admin')) { microApps.push({ name: 'admin-panel', entry: '//localhost:8082', container: '#container', activeRule: '/admin' }); } registerMicroApps(microApps); ``` #### Intersection Observer for Lazy Loading ```javascript // Load micro apps when they come into viewport const observerCallback = (entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting) { const appName = entry.target.dataset.app; loadMicroApp({ name: appName, entry: entry.target.dataset.entry, container: entry.target }); observer.unobserve(entry.target); } }); }; const observer = new IntersectionObserver(observerCallback, { threshold: 0.1 }); document.querySelectorAll('[data-lazy-app]').forEach(el => { observer.observe(el); }); ``` ## 📦 Bundle Optimization ### Code Splitting #### Micro App Level Splitting ```javascript // webpack.config.js for micro applications module.exports = { optimization: { splitChunks: { chunks: 'all', cacheGroups: { vendor: { test: /[\\/]node_modules[\\/]/, name: 'vendors', chunks: 'all', priority: 10 }, common: { name: 'common', minChunks: 2, chunks: 'all', priority: 5 } } } } }; ``` #### Dynamic Imports in Micro Apps ```javascript // React component with dynamic import import React, { Suspense, lazy } from 'react'; const HeavyComponent = lazy(() => import('./HeavyComponent')); function MyMicroApp() { return (

Micro App

Loading heavy component...
}>
); } ``` ### Shared Dependencies #### External Dependencies ```javascript // webpack.config.js - Externalize shared libraries module.exports = { externals: { 'react': 'React', 'react-dom': 'ReactDOM', 'lodash': '_', 'moment': 'moment' } }; ``` #### Module Federation ```javascript // webpack.config.js for main application const ModuleFederationPlugin = require('@module-federation/webpack'); module.exports = { plugins: [ new ModuleFederationPlugin({ name: 'shell', shared: { react: { singleton: true, requiredVersion: '^18.0.0' }, 'react-dom': { singleton: true, requiredVersion: '^18.0.0' } } }) ] }; // webpack.config.js for micro application module.exports = { plugins: [ new ModuleFederationPlugin({ name: 'microApp', shared: { react: { singleton: true, requiredVersion: '^18.0.0' }, 'react-dom': { singleton: true, requiredVersion: '^18.0.0' } } }) ] }; ``` ## 🏎️ Runtime Performance ### Memory Management #### Cleanup on Unmount ```javascript // Proper cleanup in lifecycle hooks const lifeCycles = { async afterUnmount(app) { // Clear timers if (window.microAppTimers) { window.microAppTimers.forEach(timer => clearInterval(timer)); window.microAppTimers = []; } // Remove event listeners if (window.microAppListeners) { window.microAppListeners.forEach(({ element, event, handler }) => { element.removeEventListener(event, handler); }); window.microAppListeners = []; } // Clear caches if (window.microAppCache) { window.microAppCache.clear(); } // Force garbage collection (if available) if (window.gc) { window.gc(); } } }; ``` #### Memory Leak Detection ```javascript // Monitor memory usage const memoryMonitor = { baseline: null, measureBaseline() { this.baseline = performance.memory ? { usedJSHeapSize: performance.memory.usedJSHeapSize, totalJSHeapSize: performance.memory.totalJSHeapSize } : null; }, checkForLeaks(appName) { if (!performance.memory || !this.baseline) return; const current = { usedJSHeapSize: performance.memory.usedJSHeapSize, totalJSHeapSize: performance.memory.totalJSHeapSize }; const growth = current.usedJSHeapSize - this.baseline.usedJSHeapSize; const growthMB = growth / (1024 * 1024); if (growthMB > 50) { // Alert if memory grew by more than 50MB console.warn(`Potential memory leak in ${appName}: ${growthMB.toFixed(2)}MB growth`); } } }; ``` ### Virtual DOM Optimization #### React Optimization ```javascript // Optimize React micro apps import React, { memo, useMemo, useCallback } from 'react'; const OptimizedComponent = memo(({ data, onUpdate }) => { // Memoize expensive calculations const processedData = useMemo(() => { return data.map(item => ({ ...item, calculated: expensiveCalculation(item) })); }, [data]); // Memoize event handlers const handleUpdate = useCallback((id, newValue) => { onUpdate(id, newValue); }, [onUpdate]); return (
{processedData.map(item => ( ))}
); }); ``` #### Vue Optimization ```vue ``` ## 🗄️ Caching Strategies ### HTTP Caching #### Micro App Assets ```javascript // Configure caching headers for micro app assets // nginx.conf server { location ~* \.(js|css|png|jpg|jpeg|gif|svg|woff|woff2)$ { expires 1y; add_header Cache-Control "public, immutable"; add_header Vary "Accept-Encoding"; } location /api/ { expires -1; add_header Cache-Control "no-cache, no-store, must-revalidate"; } } ``` #### Service Worker Caching ```javascript // sw.js - Service worker for micro app caching const CACHE_NAME = 'micro-app-cache-v1'; const MICRO_APP_URLS = [ '/micro-app-1/static/js/main.js', '/micro-app-1/static/css/main.css', '/micro-app-2/static/js/main.js', '/micro-app-2/static/css/main.css' ]; self.addEventListener('install', event => { event.waitUntil( caches.open(CACHE_NAME) .then(cache => cache.addAll(MICRO_APP_URLS)) ); }); self.addEventListener('fetch', event => { if (MICRO_APP_URLS.some(url => event.request.url.includes(url))) { event.respondWith( caches.match(event.request) .then(response => response || fetch(event.request)) ); } }); ``` ### Application-Level Caching #### Intelligent App Caching ```javascript // Cache micro app instances for faster remounting class MicroAppCache { constructor() { this.cache = new Map(); this.maxSize = 5; } set(appName, appInstance) { if (this.cache.size >= this.maxSize) { // Remove least recently used app const firstKey = this.cache.keys().next().value; this.cache.delete(firstKey); } this.cache.set(appName, { instance: appInstance, timestamp: Date.now() }); } get(appName) { const cached = this.cache.get(appName); if (cached) { // Move to end (mark as recently used) this.cache.delete(appName); this.cache.set(appName, cached); return cached.instance; } return null; } has(appName) { return this.cache.has(appName); } clear() { this.cache.clear(); } } const appCache = new MicroAppCache(); ``` ## ⚡ Network Optimization ### Connection Optimizations #### HTTP/2 Push ```javascript // Express.js server with HTTP/2 push for micro app assets const express = require('express'); const spdy = require('spdy'); const app = express(); app.get('/main-app', (req, res) => { // Push critical micro app resources res.push('/micro-app-1/static/js/main.js'); res.push('/micro-app-1/static/css/main.css'); res.sendFile(__dirname + '/index.html'); }); const server = spdy.createServer(options, app); ``` #### Resource Hints ```html ``` ### CDN Strategy #### Multi-CDN Setup ```javascript // Intelligent CDN selection based on performance class CDNManager { constructor() { this.cdns = [ 'https://cdn1.example.com', 'https://cdn2.example.com', 'https://cdn3.example.com' ]; this.performanceCache = new Map(); } async getBestCDN() { if (this.performanceCache.size === 0) { await this.measureCDNPerformance(); } // Return fastest CDN return [...this.performanceCache.entries()] .sort((a, b) => a[1] - b[1])[0][0]; } async measureCDNPerformance() { const promises = this.cdns.map(async (cdn) => { const start = performance.now(); try { await fetch(`${cdn}/health-check`); const latency = performance.now() - start; this.performanceCache.set(cdn, latency); } catch (error) { this.performanceCache.set(cdn, Infinity); } }); await Promise.all(promises); } } ``` ## 📊 Performance Monitoring ### Real-Time Metrics #### Performance Observer ```javascript // Monitor micro app loading performance class MicroAppPerformanceMonitor { constructor() { this.metrics = new Map(); this.initObservers(); } initObservers() { // Monitor loading performance if ('PerformanceObserver' in window) { const loadObserver = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { if (entry.name.includes('micro-app')) { this.recordLoadTime(entry); } } }); loadObserver.observe({ entryTypes: ['navigation', 'resource'] }); // Monitor layout shifts const cls Observer = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { if (!entry.hadRecentInput) { this.recordLayoutShift(entry); } } }); clsObserver.observe({ entryTypes: ['layout-shift'] }); } } recordLoadTime(entry) { const appName = this.extractAppName(entry.name); this.metrics.set(`${appName}_load_time`, entry.loadEnd - entry.loadStart); } recordLayoutShift(entry) { const currentCLS = this.metrics.get('cumulative_layout_shift') || 0; this.metrics.set('cumulative_layout_shift', currentCLS + entry.value); } getMetrics() { return Object.fromEntries(this.metrics); } } ``` #### Custom Timing API ```javascript // Custom timing for micro app lifecycle class MicroAppTiming { static mark(name) { performance.mark(name); } static measure(name, startMark, endMark) { performance.measure(name, startMark, endMark); // Send to analytics const measure = performance.getEntriesByName(name)[0]; this.sendToAnalytics({ metric: name, duration: measure.duration, timestamp: Date.now() }); } static sendToAnalytics(data) { // Send to your analytics service fetch('/api/analytics/performance', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); } } // Usage in micro app lifecycle const lifeCycles = { async beforeLoad(app) { MicroAppTiming.mark(`${app.name}_load_start`); }, async afterMount(app) { MicroAppTiming.mark(`${app.name}_mount_end`); MicroAppTiming.measure( `${app.name}_total_time`, `${app.name}_load_start`, `${app.name}_mount_end` ); } }; ``` ### Performance Analytics #### User Experience Metrics ```javascript // Track user experience metrics for micro apps class UXMetrics { constructor() { this.metrics = {}; this.initTracking(); } initTracking() { // Time to Interactive for micro apps this.trackTimeToInteractive(); // User engagement metrics this.trackUserEngagement(); // Error rates this.trackErrorRates(); } trackTimeToInteractive() { const observer = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { if (entry.entryType === 'measure' && entry.name.includes('tti')) { this.metrics.timeToInteractive = entry.duration; } } }); observer.observe({ entryTypes: ['measure'] }); } trackUserEngagement() { let interactions = 0; ['click', 'scroll', 'keydown'].forEach(event => { document.addEventListener(event, () => { interactions++; this.metrics.interactions = interactions; }); }); } trackErrorRates() { window.addEventListener('error', (event) => { const appName = this.getAppFromError(event); this.metrics.errors = this.metrics.errors || {}; this.metrics.errors[appName] = (this.metrics.errors[appName] || 0) + 1; }); } } ``` ## 🎨 UI/UX Performance ### Loading States #### Skeleton Loading ```jsx // React skeleton component for micro app loading import React from 'react'; const MicroAppSkeleton = ({ appName }) => { return (
); }; // Usage with micro app loading function MicroAppContainer({ appName, entry }) { const [loading, setLoading] = useState(true); const [app, setApp] = useState(null); useEffect(() => { loadMicroApp({ name: appName, entry, container: '#micro-app-container' }).then(() => { setLoading(false); }); }, [appName, entry]); if (loading) { return ; } return
; } ``` #### Progressive Enhancement ```javascript // Progressive enhancement for micro apps class ProgressiveLoader { constructor(container, config) { this.container = container; this.config = config; this.loadingStates = ['initial', 'skeleton', 'partial', 'complete']; this.currentState = 'initial'; } async load() { // Show initial loading state this.setState('skeleton'); this.renderSkeleton(); try { // Load critical CSS first await this.loadCriticalCSS(); // Show partial content this.setState('partial'); await this.loadCriticalJS(); // Load remaining resources await this.loadRemainingAssets(); // Complete loading this.setState('complete'); this.mountApp(); } catch (error) { this.handleLoadError(error); } } renderSkeleton() { this.container.innerHTML = this.config.skeletonHTML; } async loadCriticalCSS() { const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = `${this.config.entry}/critical.css`; return new Promise((resolve, reject) => { link.onload = resolve; link.onerror = reject; document.head.appendChild(link); }); } } ``` ### Animation Performance #### Hardware Acceleration ```css /* Optimize animations for micro app transitions */ .micro-app-transition { /* Use transform instead of changing layout properties */ transform: translateX(0); transition: transform 0.3s ease-out; /* Enable hardware acceleration */ will-change: transform; /* Use GPU compositing */ transform: translateZ(0); } .micro-app-enter { transform: translateX(100%); } .micro-app-enter-active { transform: translateX(0); } .micro-app-exit { transform: translateX(0); } .micro-app-exit-active { transform: translateX(-100%); } ``` #### Intersection Observer for Animations ```javascript // Efficient animation triggering using Intersection Observer class AnimationManager { constructor() { this.observer = new IntersectionObserver( this.handleIntersection.bind(this), { threshold: 0.1 } ); } observe(element) { this.observer.observe(element); } handleIntersection(entries) { entries.forEach(entry => { if (entry.isIntersecting) { this.triggerAnimation(entry.target); this.observer.unobserve(entry.target); } }); } triggerAnimation(element) { // Use CSS classes for hardware-accelerated animations element.classList.add('animate-in'); // Or use Web Animations API for complex animations element.animate([ { opacity: 0, transform: 'translateY(20px)' }, { opacity: 1, transform: 'translateY(0)' } ], { duration: 300, easing: 'ease-out' }); } } ``` ## 📱 Mobile Performance ### Mobile-Specific Optimizations #### Touch Event Optimization ```javascript // Optimize touch events for mobile micro apps class TouchOptimizer { constructor() { this.setupPassiveListeners(); this.optimizeTouchHandling(); } setupPassiveListeners() { // Use passive listeners for scroll performance document.addEventListener('touchstart', this.handleTouchStart, { passive: true }); document.addEventListener('touchmove', this.handleTouchMove, { passive: true }); } optimizeTouchHandling() { // Debounce touch events let touchTimeout; document.addEventListener('touchend', () => { clearTimeout(touchTimeout); touchTimeout = setTimeout(() => { // Handle touch end with delay to prevent accidental double taps }, 300); }); } handleTouchStart(event) { // Minimal processing in touch start } handleTouchMove(event) { // Use requestAnimationFrame for smooth scrolling requestAnimationFrame(() => { // Handle touch move }); } } ``` #### Viewport Management ```javascript // Optimize viewport for different micro apps class ViewportManager { constructor() { this.defaultViewport = 'width=device-width, initial-scale=1.0'; this.viewportMeta = document.querySelector('meta[name="viewport"]'); } setViewportForApp(appName) { const appViewports = { 'mobile-first-app': 'width=device-width, initial-scale=1.0, user-scalable=no', 'desktop-app': 'width=1024, initial-scale=0.5', 'responsive-app': 'width=device-width, initial-scale=1.0' }; const viewport = appViewports[appName] || this.defaultViewport; this.viewportMeta.setAttribute('content', viewport); } resetViewport() { this.viewportMeta.setAttribute('content', this.defaultViewport); } } ``` ## 🔧 Development vs Production Optimization ### Environment-Specific Configurations #### Development Optimizations ```javascript // webpack.config.dev.js for micro apps module.exports = { mode: 'development', optimization: { // Disable minification for faster builds minimize: false, // Split chunks for better debugging splitChunks: { chunks: 'all', cacheGroups: { vendor: { test: /[\\/]node_modules[\\/]/, name: 'vendors', chunks: 'all' } } } }, devServer: { // Enable hot reloading hot: true, // Optimize for development liveReload: true, // CORS for micro app communication headers: { 'Access-Control-Allow-Origin': '*' } } }; ``` #### Production Optimizations ```javascript // webpack.config.prod.js for micro apps const CompressionPlugin = require('compression-webpack-plugin'); const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); module.exports = { mode: 'production', optimization: { // Enable all optimizations minimize: true, sideEffects: false, // Advanced chunk splitting splitChunks: { chunks: 'all', maxSize: 244000, cacheGroups: { vendor: { test: /[\\/]node_modules[\\/]/, name: 'vendors', chunks: 'all', priority: 10 }, common: { name: 'common', minChunks: 2, chunks: 'all', priority: 5 } } } }, plugins: [ // Gzip compression new CompressionPlugin({ algorithm: 'gzip', test: /\.(js|css|html|svg)$/, threshold: 8192, minRatio: 0.8 }), // Bundle analysis (optional) new BundleAnalyzerPlugin({ analyzerMode: 'static', openAnalyzer: false }) ] }; ``` ## 🎯 Performance Best Practices Summary ### ✅ Do's 1. **Implement prefetching** for critical micro apps 2. **Use code splitting** within micro applications 3. **Leverage caching** at multiple levels 4. **Monitor performance** continuously 5. **Optimize for mobile** experiences 6. **Use lazy loading** for non-critical features 7. **Implement proper cleanup** in unmount hooks 8. **Share dependencies** efficiently 9. **Use service workers** for caching 10. **Optimize animations** with hardware acceleration ### ❌ Don'ts 1. **Don't load all micro apps** at once 2. **Don't ignore bundle sizes** 3. **Don't duplicate large dependencies** 4. **Don't forget memory cleanup** 5. **Don't block the main thread** 6. **Don't ignore mobile performance** 7. **Don't over-prefetch** on slow connections 8. **Don't use synchronous operations** 9. **Don't ignore error boundaries** 10. **Don't skip performance monitoring** ### 📊 Performance Checklist ```javascript // Performance audit checklist for micro apps const performanceChecklist = { loading: { prefetchStrategy: '✓ Implemented smart prefetching', bundleSize: '✓ Bundles under 250KB gzipped', codesplitting: '✓ Critical path separated', caching: '✓ Aggressive caching enabled' }, runtime: { memoryLeaks: '✓ Cleanup implemented', animationPerf: '✓ Hardware acceleration used', eventOptimization: '✓ Passive listeners used', lazyLoading: '✓ Non-critical features lazy loaded' }, monitoring: { coreWebVitals: '✓ LCP, FID, CLS monitored', customMetrics: '✓ App-specific metrics tracked', errorTracking: '✓ Performance errors logged', analytics: '✓ User experience measured' } }; ``` ## 🔗 Related Documentation - [Style Isolation](/cookbook/style-isolation) - CSS performance and isolation - [Error Handling](/cookbook/error-handling) - Performance impact of errors - [Configuration](/api/configuration) - Performance-related configurations - [Debugging](/cookbook/debugging) - Performance debugging techniques ================================================ FILE: docs/cookbook/style-isolation.md ================================================ # Style Isolation Style isolation is one of the most critical aspects of micro-frontend architecture. When multiple applications run in the same browser context, CSS conflicts can cause visual inconsistencies and broken layouts. This guide covers various strategies to achieve effective style isolation in qiankun applications. ## 🎯 Understanding the Problem ### CSS Conflicts in Micro-Frontends When multiple micro applications are loaded into the same page, they share the same global CSS namespace. This can lead to: - **Style Overriding**: Later-loaded applications overriding earlier styles - **Selector Conflicts**: Same class names causing unintended styling - **Global Pollution**: Micro apps affecting main application styles - **Layout Breaks**: Unexpected layout changes due to conflicting styles ### Example of a CSS Conflict ```css /* Main Application */ .button { background: blue; padding: 10px; } /* Micro Application */ .button { background: red; /* This will override main app styles! */ border: none; } ``` ## 🛡️ qiankun's Built-in Style Isolation qiankun provides several built-in style isolation mechanisms that you can enable through configuration. ### Strict Style Isolation The most robust isolation method using Shadow DOM: ```javascript import { start } from 'qiankun'; start({ sandbox: { strictStyleIsolation: true } }); ``` **How it works:** - Creates a Shadow DOM for each micro application - Completely isolates styles between applications - Prevents any style leakage **Pros:** - Complete style isolation - No CSS conflicts possible - Easy to implement **Cons:** - Some third-party libraries may not work properly - Debugging can be more complex - Performance overhead for large applications ### Experimental Style Isolation A less intrusive approach using CSS scoping: ```javascript import { start } from 'qiankun'; start({ sandbox: { experimentalStyleIsolation: true } }); ``` **How it works:** - Adds unique prefixes to CSS selectors - Scopes styles to micro application containers - Maintains DOM structure **Pros:** - Better third-party library compatibility - Easier debugging - Less performance overhead **Cons:** - Not as robust as strict isolation - Some edge cases may still cause conflicts ## 🎨 CSS-in-JS Solutions CSS-in-JS libraries provide natural style isolation by generating unique class names. ### Styled Components ```jsx // Micro Application using Styled Components import styled from 'styled-components'; const Button = styled.button` background: ${props => props.primary ? 'blue' : 'grey'}; padding: 10px 20px; border: none; border-radius: 4px; &:hover { opacity: 0.8; } `; function MyComponent() { return (
); } ``` ### Emotion ```jsx /** @jsxImportSource @emotion/react */ import { css } from '@emotion/react'; const buttonStyle = css` background: blue; color: white; padding: 10px 20px; border: none; border-radius: 4px; &:hover { background: darkblue; } `; function MyComponent() { return ; } ``` ### CSS Modules with Dynamic Imports ```css /* Button.module.css */ .button { background: blue; color: white; padding: 10px 20px; border: none; border-radius: 4px; } .button:hover { background: darkblue; } ``` ```jsx // Button.jsx import styles from './Button.module.css'; function Button({ children, ...props }) { return ( ); } ``` ## 🏗️ CSS Scoping Strategies ### BEM Methodology Use Block, Element, Modifier naming convention to avoid conflicts: ```css /* Main Application */ .main-app__button { background: blue; } .main-app__button--primary { background: darkblue; } /* Micro Application */ .micro-app__button { background: red; } .micro-app__button--large { padding: 15px 30px; } ``` ### Namespace Prefixing Add unique prefixes to all CSS classes: ```css /* User Management Micro App */ .user-mgmt-container { } .user-mgmt-header { } .user-mgmt-button { } /* Product Catalog Micro App */ .product-cat-container { } .product-cat-header { } .product-cat-button { } ``` ### CSS Custom Properties (Variables) Use CSS variables for consistent theming without conflicts: ```css /* Main Application - Define theme variables */ :root { --primary-color: #007bff; --secondary-color: #6c757d; --success-color: #28a745; --danger-color: #dc3545; } /* Micro Application - Use theme variables */ .micro-app-button { background: var(--primary-color); color: white; } ``` ## 🎭 Runtime Style Management ### Dynamic CSS Loading Load CSS dynamically when micro applications mount: ```javascript // Lifecycle hooks for dynamic CSS loading const lifeCycles = { async beforeMount(app) { // Load micro app specific CSS const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = `${app.entry}/static/css/main.css`; link.id = `${app.name}-styles`; document.head.appendChild(link); }, async afterUnmount(app) { // Remove micro app CSS const link = document.getElementById(`${app.name}-styles`); if (link) { document.head.removeChild(link); } } }; registerMicroApps([ { name: 'micro-app', entry: '//localhost:8080', container: '#container', activeRule: '/micro-app' } ], lifeCycles); ``` ### CSS Scoping with PostCSS Use PostCSS plugins to automatically scope CSS: ```javascript // postcss.config.js module.exports = { plugins: [ require('postcss-prefixwrap')('.micro-app-container') ] }; ``` **Input CSS:** ```css .button { background: blue; } ``` **Output CSS:** ```css .micro-app-container .button { background: blue; } ``` ## 🔧 Framework-Specific Solutions ### React Applications #### Using CSS Modules with Create React App ```javascript // Button.module.css .button { background: blue; color: white; } // Button.jsx import styles from './Button.module.css'; function Button({ children }) { return ; } ``` #### Custom CSS Prefix Hook ```javascript import { useMemo } from 'react'; function useCSSPrefix(prefix) { return useMemo(() => { return (className) => `${prefix}-${className}`; }, [prefix]); } // Usage function MyComponent() { const cx = useCSSPrefix('user-mgmt'); return (
); } ``` ### Vue Applications #### Scoped Styles ```vue ``` #### CSS Modules in Vue ```vue ``` ### Angular Applications #### ViewEncapsulation ```typescript import { Component, ViewEncapsulation } from '@angular/core'; @Component({ selector: 'app-my-component', template: `
`, styles: [` .component { padding: 20px; } .button { background: blue; color: white; } `], encapsulation: ViewEncapsulation.Emulated // Default }) export class MyComponent { } ``` ## 🎪 Component Library Strategies ### Design System Approach Create a shared design system that all micro applications consume: ```javascript // shared-design-system/Button.js export const Button = ({ variant = 'primary', size = 'medium', children, ...props }) => { const baseClasses = 'btn'; const variantClasses = { primary: 'btn-primary', secondary: 'btn-secondary' }; const sizeClasses = { small: 'btn-sm', medium: 'btn-md', large: 'btn-lg' }; const className = [ baseClasses, variantClasses[variant], sizeClasses[size] ].join(' '); return ; }; ``` ### CSS Custom Properties for Theming ```css /* Design system CSS */ :root { --btn-primary-bg: #007bff; --btn-primary-color: white; --btn-secondary-bg: #6c757d; --btn-secondary-color: white; --btn-padding-sm: 8px 12px; --btn-padding-md: 10px 16px; --btn-padding-lg: 12px 20px; } .btn { border: none; border-radius: 4px; cursor: pointer; font-family: inherit; } .btn-primary { background: var(--btn-primary-bg); color: var(--btn-primary-color); } .btn-secondary { background: var(--btn-secondary-bg); color: var(--btn-secondary-color); } .btn-sm { padding: var(--btn-padding-sm); } .btn-md { padding: var(--btn-padding-md); } .btn-lg { padding: var(--btn-padding-lg); } ``` ## 🚫 Third-Party Library Handling ### Isolating Third-Party Styles When using third-party libraries that inject global styles: ```javascript // Load third-party CSS in isolation const loadLibraryStyles = (libraryName, cssUrl) => { return new Promise((resolve) => { const iframe = document.createElement('iframe'); iframe.style.display = 'none'; document.body.appendChild(iframe); const iframeDoc = iframe.contentDocument; const link = iframeDoc.createElement('link'); link.rel = 'stylesheet'; link.href = cssUrl; link.onload = () => { // Extract and scope the CSS const styles = Array.from(iframeDoc.styleSheets[0].cssRules) .map(rule => rule.cssText) .join('\n'); const scopedStyles = scopeCSS(styles, `.${libraryName}-container`); const style = document.createElement('style'); style.textContent = scopedStyles; style.id = `${libraryName}-scoped-styles`; document.head.appendChild(style); document.body.removeChild(iframe); resolve(); }; iframeDoc.head.appendChild(link); }); }; ``` ### CSS-in-JS for Third-Party Integration ```javascript import { createGlobalStyle } from 'styled-components'; // Scope third-party styles to specific container const AntdStyles = createGlobalStyle` .micro-app-container { .ant-btn { /* Override Ant Design button styles specifically for this micro app */ border-radius: 8px; } .ant-table { /* Override Ant Design table styles */ border: 1px solid #f0f0f0; } } `; function MicroApp() { return (
{/* Your micro app content */}
); } ``` ## 🔍 Debugging Style Conflicts ### Development Tools #### Style Inspection Script ```javascript // Add to browser console for debugging const findStyleConflicts = (selector) => { const elements = document.querySelectorAll(selector); elements.forEach((el, index) => { const styles = window.getComputedStyle(el); const rules = []; for (let i = 0; i < document.styleSheets.length; i++) { try { const sheet = document.styleSheets[i]; const cssRules = sheet.cssRules || sheet.rules; for (let j = 0; j < cssRules.length; j++) { if (el.matches(cssRules[j].selectorText)) { rules.push({ selector: cssRules[j].selectorText, rule: cssRules[j].cssText, sheet: sheet.href || 'inline' }); } } } catch (e) { // Cross-origin stylesheet } } console.log(`Element ${index + 1}:`, el); console.log('Applied styles:', rules); }); }; // Usage: findStyleConflicts('.button'); ``` #### Style Source Tracker ```javascript // Track which micro app loaded which styles const styleTracker = { styles: new Map(), track(appName, styleElement) { if (!this.styles.has(appName)) { this.styles.set(appName, []); } this.styles.get(appName).push(styleElement); }, getByApp(appName) { return this.styles.get(appName) || []; }, getConflicts() { const allSelectors = new Map(); this.styles.forEach((styles, appName) => { styles.forEach(style => { // Parse CSS and check for selector conflicts // Implementation depends on CSS parser }); }); return allSelectors; } }; ``` ### Runtime Conflict Detection ```javascript // Detect style conflicts at runtime const detectStyleConflicts = () => { const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.type === 'childList') { mutation.addedNodes.forEach((node) => { if (node.tagName === 'STYLE' || node.tagName === 'LINK') { checkForConflicts(node); } }); } }); }); observer.observe(document.head, { childList: true }); }; const checkForConflicts = (styleNode) => { // Implementation to check for CSS selector conflicts console.warn('New style node added:', styleNode); }; ``` ## 📊 Performance Considerations ### CSS Loading Performance ```javascript // Optimize CSS loading for micro applications const optimizedCSSLoader = { cache: new Map(), async loadCSS(url, appName) { if (this.cache.has(url)) { return this.cache.get(url); } const response = await fetch(url); const css = await response.text(); const optimizedCSS = this.optimizeCSS(css, appName); this.cache.set(url, optimizedCSS); return optimizedCSS; }, optimizeCSS(css, appName) { // Remove unused selectors // Add app-specific prefixes // Minify if needed return css.replace(/(\.[a-zA-Z-_]+)/g, `.${appName}-$1`); } }; ``` ### Bundle Splitting for Styles ```javascript // webpack.config.js - Split CSS by micro app module.exports = { optimization: { splitChunks: { cacheGroups: { styles: { name: 'styles', test: /\.css$/, chunks: 'all', enforce: true } } } } }; ``` ## 🎯 Best Practices Summary ### ✅ Do's 1. **Use qiankun's built-in isolation** when possible 2. **Implement consistent naming conventions** (BEM, namespacing) 3. **Use CSS-in-JS** for automatic isolation 4. **Create a shared design system** for consistency 5. **Test style isolation** in development 6. **Monitor for conflicts** in production 7. **Document style guidelines** for your team ### ❌ Don'ts 1. **Don't rely on global CSS** in micro applications 2. **Don't use overly generic class names** (.button, .container) 3. **Don't forget to clean up styles** when unmounting 4. **Don't ignore third-party library styles** 5. **Don't skip testing** style isolation 6. **Don't use !important** unless absolutely necessary ### 🎪 Real-World Example Here's a complete example of a well-isolated micro application: ```jsx // MicroApp.jsx import React from 'react'; import styled, { createGlobalStyle } from 'styled-components'; // Global styles scoped to this micro app const GlobalStyles = createGlobalStyle` .user-management-app { font-family: 'Inter', sans-serif; * { box-sizing: border-box; } } `; // Styled components with isolation const Container = styled.div` padding: 20px; background: #f8f9fa; min-height: 100vh; `; const Header = styled.h1` color: #343a40; margin-bottom: 20px; `; const Button = styled.button` background: ${props => props.primary ? '#007bff' : '#6c757d'}; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; &:hover { opacity: 0.9; } `; function UserManagementApp() { return (
User Management
); } export default UserManagementApp; ``` ## 🔗 Related Documentation - [Performance Optimization](/cookbook/performance) - Optimize loading and runtime performance - [Error Handling](/cookbook/error-handling) - Handle CSS-related errors - [Configuration](/api/configuration) - Configure style isolation options - [Debugging](/cookbook/debugging) - Debug style conflicts ================================================ FILE: docs/ecosystem/bundler-plugin.md ================================================ # Bundler Plugin The `@qiankunjs/bundler-plugin` provides bundler plugins for the qiankun micro-frontend framework. Currently it supports Webpack (4 & 5), with plans to support other bundlers like Vite and Turbopack in the future. ## Installation ```bash npm install @qiankunjs/bundler-plugin --save-dev # or yarn add @qiankunjs/bundler-plugin --dev # or pnpm add @qiankunjs/bundler-plugin --save-dev ``` ## Webpack Plugin ### Features - **Automatic Library Configuration**: Sets the correct output library name and format for qiankun consumption - **Unique JSONP Function**: Ensures unique `jsonpFunction`/`chunkLoadingGlobal` names to prevent conflicts between micro applications - **Browser Compatibility**: Sets global object to `window` for proper browser execution - **Entry Script Marking**: Automatically adds `entry` attribute to the main entry script tag in HTML using webpack entrypoints - **Webpack 4 & 5 Support**: Compatible with both Webpack 4 and Webpack 5 - **Zero Configuration**: Works out of the box with sensible defaults ### Basic Usage ```javascript // webpack.config.js const { QiankunWebpackPlugin } = require('@qiankunjs/bundler-plugin'); module.exports = { entry: './src/index.js', plugins: [ new QiankunWebpackPlugin() ] }; ``` This basic configuration will: - Use the `name` field from your `package.json` as the library name - Automatically mark the entry script tag with the `entry` attribute (based on webpack entrypoints) - Configure the output for qiankun consumption ### With Custom Options ```javascript // webpack.config.js const { QiankunWebpackPlugin } = require('@qiankunjs/bundler-plugin'); module.exports = { entry: './src/index.js', plugins: [ new QiankunWebpackPlugin({ packageName: 'my-micro-app', }) ] }; ``` ### Configuration Options #### `packageName` (optional) - **Type**: `string` - **Default**: Value from `package.json` name field - **Description**: Specifies the name of the output library that qiankun will use to identify your micro application ### Framework Integration #### React (CRACO) ```javascript // craco.config.js const { QiankunWebpackPlugin } = require('@qiankunjs/bundler-plugin'); module.exports = { webpack: { configure: (webpackConfig) => { webpackConfig.plugins.push( new QiankunWebpackPlugin({ packageName: process.env.REACT_APP_NAME || 'react-app' }) ); return webpackConfig; } } }; ``` #### Vue CLI ```javascript // vue.config.js const { QiankunWebpackPlugin } = require('@qiankunjs/bundler-plugin'); module.exports = { configureWebpack: { plugins: [ new QiankunWebpackPlugin() ] }, devServer: { port: 8080, headers: { 'Access-Control-Allow-Origin': '*' } } }; ``` #### Angular ```javascript // custom-webpack.config.js const { QiankunWebpackPlugin } = require('@qiankunjs/bundler-plugin'); module.exports = { plugins: [ new QiankunWebpackPlugin({ packageName: 'angular-micro-app' }) ] }; ``` ### What the Plugin Does #### 1. Output Library Configuration **Webpack 4:** ```javascript { output: { library: 'your-app-name', libraryTarget: 'window', jsonpFunction: 'webpackJsonp_your-app-name', globalObject: 'window' } } ``` **Webpack 5:** ```javascript { output: { library: { name: 'your-app-name', type: 'window' } } } ``` #### 2. Entry Script Marking The plugin uses webpack entrypoints to identify the main entry chunk and marks its script tag with `entry`. If multiple entrypoints exist, it matches by HTML filename; otherwise it falls back to the first entrypoint. If detection fails, it marks the last injected script. ### Troubleshooting - **Library Not Exposed**: Ensure `package.json` has a valid name field, or explicitly set `packageName` in plugin options. - **Entry Attribute Missing**: Verify `html-webpack-plugin` is present and the plugin is listed after it in `plugins`. The plugin relies on webpack `compilation.entrypoints` to find the main chunk. ## Related Documentation - [Core APIs](/api/) - qiankun core APIs - [Create Qiankun](/ecosystem/create-qiankun) - Project scaffolding tool - [React Bindings](/ecosystem/react) - React UI bindings - [Vue Bindings](/ecosystem/vue) - Vue UI bindings ## Contributing Found an issue or want to contribute? Check out the [GitHub repository](https://github.com/umijs/qiankun) and contribute to the `packages/bundler-plugin` directory. ================================================ FILE: docs/ecosystem/create-qiankun.md ================================================ # Create Qiankun `create-qiankun` is a CLI scaffolding tool designed specifically for the qiankun micro-frontend framework. It helps developers quickly bootstrap example projects and get started with micro-frontend development efficiently. ## 🚀 Quick Start ### Using npm ```bash npx create-qiankun@latest ``` ### Using yarn ```bash yarn create qiankun@latest ``` ### Using pnpm ```bash pnpm dlx create-qiankun@latest ``` ## 🎯 Features - **Multiple Project Types**: Choose between main app only, micro apps only, or complete setup - **Framework Support**: React 18, Vue 3, Vue 2, and Umi 4 templates - **Routing Modes**: Support for both hash and history routing patterns - **Package Manager Options**: npm, yarn, pnpm, or pnpm workspace - **Auto Configuration**: Automatic port conflict detection and startup scripts injection - **Monorepo Support**: Built-in pnpm workspace setup for managing multiple applications ## 📋 Requirements - **Node.js**: v18 or higher (recommended: use [fnm](https://github.com/Schniz/fnm) for version management) - **Package Manager**: npm, yarn, or pnpm ## 🎮 Interactive Setup When you run `create-qiankun`, you'll be guided through an interactive setup process: ### Step 1: Project Name ```bash ? Project name: › my-qiankun-project ``` ### Step 2: Project Type ```bash ? Choose a way to create › ❯ Create main application and sub applications Just create main application Just create sub applications ``` **Options:** - **Create main application and sub applications**: Complete setup with main app and multiple micro apps - **Just create main application**: Only create the main (shell) application - **Just create sub applications**: Only create micro applications ### Step 3: Main Application Framework (if applicable) ```bash ? Choose a framework for your main application › ❯ React18+Webpack Vue3+Webpack React18+umi4 ``` ### Step 4: Routing Pattern (if applicable) ```bash ? Choose a route pattern for your main application › ❯ hash history ``` ### Step 5: Sub Applications Framework (if applicable) ```bash ? Choose a framework for your sub application › Space to select. Return to submit. ❯◯ React18+Webpack ◯ Vue3+Webpack ◯ Vue2+Webpack ◯ React18+umi4 ``` ### Step 6: Package Manager ```bash ? Which package manager do you want to use? › ❯ npm yarn pnpm pnpm with workspace ``` ## 📦 Available Templates ### Main Application Templates | Template | Description | Features | |----------|-------------|----------| | **React18+Webpack** | React 18 with Webpack 5 | Modern React, TypeScript support, Hot reload | | **Vue3+Webpack** | Vue 3 with Vue CLI | Composition API, TypeScript, Element Plus | | **React18+umi4** | Umi 4 framework | Built-in qiankun support, Ant Design Pro | ### Sub Application Templates | Template | Description | Status | Notes | |----------|-------------|--------|-------| | **React18+Webpack** | React 18 micro app | ✅ Stable | Production ready | | **Vue3+Webpack** | Vue 3 micro app | ✅ Stable | Production ready | | **Vue2+Webpack** | Vue 2 micro app | ⚠️ Limited | Issues with pnpm workspace | | **React18+umi4** | Umi 4 micro app | ✅ Stable | Built-in micro app support | | **Vite+Vue3** | Vue 3 with Vite | 🚧 WIP | Under development | | **Vite+React18** | React 18 with Vite | 🚧 WIP | Under development | ## 🏗️ Project Structure ### Single Project Structure ``` my-qiankun-project/ ├── main-app/ # Main application │ ├── src/ │ ├── package.json │ └── webpack.config.js ├── react18-sub/ # React micro app │ ├── src/ │ ├── package.json │ └── webpack.config.js ├── vue3-sub/ # Vue micro app │ ├── src/ │ ├── package.json │ └── vue.config.js └── package.json ``` ### Pnpm Workspace Structure ``` my-qiankun-project/ ├── packages/ │ ├── main-app/ # Main application │ ├── react18-sub/ # React micro app │ └── vue3-sub/ # Vue micro app ├── package.json # Workspace configuration ├── pnpm-workspace.yaml # Workspace definition └── scripts/ └── checkPnpm.js # Package manager validation ``` ## 🔧 Generated Configuration ### Main Application Configuration The main application is automatically configured with: ```typescript // Main app micro app registration import { registerMicroApps, start } from 'qiankun'; registerMicroApps([ { name: 'react18-sub', entry: '//localhost:8080', container: '#subapp-viewport', activeRule: '/react18-sub', }, { name: 'vue3-sub', entry: '//localhost:8081', container: '#subapp-viewport', activeRule: '/vue3-sub', } ]); start(); ``` ### Micro Application Configuration Each micro application includes: **React Micro App:** ```javascript // webpack.config.js const { QiankunWebpackPlugin } = require('@qiankunjs/webpack-plugin'); module.exports = { plugins: [ new QiankunWebpackPlugin() ] }; ``` **Vue Micro App:** ```javascript // vue.config.js const { defineConfig } = require('@vue/cli-service'); const { QiankunWebpackPlugin } = require('@qiankunjs/webpack-plugin'); module.exports = defineConfig({ configureWebpack: { plugins: [ new QiankunWebpackPlugin() ] } }); ``` ### Port Configuration Automatic port assignment prevents conflicts: ```json { "scripts": { "dev": "PORT=8080 react-scripts start", "check-port": "node scripts/checkPort.js" } } ``` ## 🎨 Customization Options ### Environment-specific Configuration ```javascript // config/development.js module.exports = { microApps: [ { name: 'react-app', entry: '//localhost:8080', activeRule: '/react-app' } ] }; // config/production.js module.exports = { microApps: [ { name: 'react-app', entry: '//app.example.com', activeRule: '/react-app' } ] }; ``` ### Custom Routing ```typescript // Hash routing (default) const router = createRouter({ history: createWebHashHistory(), routes: [...] }); // History routing const router = createRouter({ history: createWebHistory(), routes: [...] }); ``` ## 🚀 Development Workflow ### Single Package Manager ```bash # Start main application cd main-app && npm run dev # Start micro applications (in separate terminals) cd react18-sub && npm run dev cd vue3-sub && npm run dev ``` ### Pnpm Workspace ```bash # Install all dependencies pnpm install # Start all applications concurrently pnpm run dev # Start specific application pnpm --filter main-app run dev pnpm --filter react18-sub run dev ``` ### Generated Scripts The CLI automatically injects useful scripts: ```json { "scripts": { "dev": "concurrently \"npm run dev:main\" \"npm run dev:subs\"", "dev:main": "cd main-app && npm run dev", "dev:subs": "concurrently \"cd react18-sub && npm run dev\" \"cd vue3-sub && npm run dev\"", "build": "npm run build:main && npm run build:subs", "clean": "rimraf node_modules **/*/node_modules" } } ``` ## 🔧 Advanced Usage ### Command Line Arguments Skip the interactive prompts by providing arguments: ```bash npx create-qiankun my-project CreateMainAndSubApp react18-main hash react18-webpack-sub,vue3-webpack-sub pnpm ``` **Arguments order:** 1. Project name 2. Create kind (`CreateMainApp` | `CreateSubApp` | `CreateMainAndSubApp`) 3. Main app template (if applicable) 4. Routing pattern (if applicable) 5. Sub app templates (comma-separated, if applicable) 6. Package manager ### Batch Creation ```bash # Create multiple projects for project in app1 app2 app3; do npx create-qiankun $project CreateMainAndSubApp react18-main history react18-webpack-sub pnpm done ``` ### Custom Templates You can extend the CLI with custom templates by contributing to the project or forking the repository. ## 🎯 Project Examples ### Complete React + Vue Setup ```bash npx create-qiankun my-micro-frontend-app # Choose: Create main application and sub applications # Main: React18+Webpack # Routing: history # Subs: React18+Webpack, Vue3+Webpack # Package Manager: pnpm with workspace ``` **Result:** - Main React app with routing - React 18 micro app - Vue 3 micro app - Automatic port assignment (3000, 8080, 8081) - Workspace configuration - Development scripts ### Umi-based Monorepo ```bash npx create-qiankun enterprise-app # Choose: Create main application and sub applications # Main: React18+umi4 # Routing: history # Subs: React18+umi4, Vue3+Webpack # Package Manager: pnpm with workspace ``` **Features:** - Umi 4 main application with built-in qiankun support - Umi 4 micro application - Vue 3 micro application - Ant Design Pro components - TypeScript configuration ## 📚 Best Practices ### 1. Use Descriptive Names ```bash # ✅ Good: Descriptive project names npx create-qiankun e-commerce-platform npx create-qiankun admin-dashboard # ❌ Bad: Generic names npx create-qiankun app1 npx create-qiankun project ``` ### 2. Choose Appropriate Package Manager ```bash # For simple projects npm / yarn # For monorepo with multiple teams pnpm with workspace ``` ### 3. Plan Your Routing Strategy ```bash # Hash routing - simpler deployment # History routing - better SEO, requires server configuration ``` ### 4. Consider Framework Compatibility - **React + Vue**: Good for mixed teams - **Same Framework**: Easier dependency management - **Umi**: Best for enterprise applications ## 🐛 Troubleshooting ### Port Conflicts The CLI automatically detects and resolves port conflicts. If you encounter issues: ```bash # Check running processes lsof -i :8080 # Kill conflicting processes kill -9 $(lsof -t -i:8080) ``` ### Pnpm Workspace Issues ```bash # Clear node_modules and reinstall pnpm run clean pnpm install # Check workspace configuration cat pnpm-workspace.yaml ``` ### Build Errors ```bash # Clear build cache rm -rf dist/ build/ .cache/ # Reinstall dependencies rm -rf node_modules package-lock.json npm install ``` ### Vue 2 with Pnpm Workspace Known limitation: Vue 2 templates have compatibility issues with pnpm workspace. Use alternative approaches: ```bash # Use regular pnpm instead # Choose: pnpm (not pnpm with workspace) # Or use yarn/npm for Vue 2 projects ``` ## 🔗 Generated Project Features ### Automatic Configuration - **Webpack optimization** for micro-frontend builds - **CORS handling** for cross-origin requests - **Public path** configuration for different environments - **Development proxy** setup for local development ### Development Experience - **Hot module replacement** in all applications - **Error boundaries** for micro app failures - **Loading states** during micro app transitions - **TypeScript support** where applicable ### Production Ready - **Build optimization** for micro-frontend deployment - **Asset optimization** and code splitting - **Environment configuration** for different stages - **CI/CD friendly** structure ## 📖 Next Steps After creating your project: 1. **Explore the generated code** to understand the structure 2. **Customize the configuration** based on your needs 3. **Add more micro applications** as your project grows 4. **Set up CI/CD pipelines** for automated deployment 5. **Read the qiankun documentation** for advanced features ## 🔗 Related Documentation - [Core APIs](/api/) - qiankun core APIs - [React Bindings](/ecosystem/react) - React UI bindings - [Vue Bindings](/ecosystem/vue) - Vue UI bindings - [Webpack Plugin](/ecosystem/webpack-plugin) - Build tool configuration ## 🤝 Contributing Want to add new templates or improve the CLI? Check out the [GitHub repository](https://github.com/umijs/qiankun) and contribute to the `packages/create-qiankun` directory. ================================================ FILE: docs/ecosystem/index.md ================================================ # Ecosystem qiankun provides a rich ecosystem of UI bindings and tools to help you build and maintain micro-frontend applications efficiently. ## 🧩 UI Bindings qiankun offers declarative UI components for popular frameworks, making it easier to load and manage micro applications within your main application. ### React **`@qiankunjs/react`** - Official React bindings for qiankun - **Features**: Declarative MicroApp component, automatic loading states, error boundaries - **Benefits**: Type-safe, React hooks support, seamless integration - **Use Case**: Perfect for React-based main applications ```bash npm install @qiankunjs/react ``` [Learn more about React bindings →](/ecosystem/react) ### Vue **`@qiankunjs/vue`** - Official Vue bindings for qiankun - **Features**: Vue 2/3 compatible, composition API support, slot-based customization - **Benefits**: Reactive loading states, template-based approach, TypeScript support - **Use Case**: Ideal for Vue-based main applications ```bash npm install @qiankunjs/vue ``` [Learn more about Vue bindings →](/ecosystem/vue) ## 🛠️ Development Tools ### Webpack Plugin **`@qiankunjs/webpack-plugin`** - Webpack plugin for micro applications - **Features**: Automatic public path injection, build optimization, development mode support - **Benefits**: Zero-config setup, improved developer experience, production-ready builds - **Use Case**: Essential for webpack-based micro applications ```bash npm install @qiankunjs/webpack-plugin --save-dev ``` [Learn more about Webpack Plugin →](/ecosystem/webpack-plugin) ### Create Qiankun **`create-qiankun`** - CLI tool for scaffolding qiankun projects - **Features**: Multiple templates, main app + micro app setup, best practices included - **Benefits**: Quick project initialization, production-ready configurations, modern tooling - **Use Case**: Starting new qiankun projects or adding micro-frontend capabilities ```bash npx create-qiankun my-micro-frontend-app ``` [Learn more about Create Qiankun →](/ecosystem/create-qiankun) ## 🎯 Quick Start Comparison ### Without UI Bindings (Core API) ```typescript import { loadMicroApp } from 'qiankun'; // Manual approach const microApp = loadMicroApp({ name: 'my-app', entry: '//localhost:8080', container: '#subapp-container' }); // Manual lifecycle management microApp.mountPromise.then(() => { setLoading(false); }).catch(error => { setError(error); }); ``` ### With React Binding ```tsx import { MicroApp } from '@qiankunjs/react'; function App() { return ( ); } ``` ### With Vue Binding ```vue ``` ## 🔄 Integration Flow ```mermaid graph LR A[Main App] --> B[UI Binding] B --> C[qiankun Core] C --> D[Micro App 1] C --> E[Micro App 2] C --> F[Micro App 3] G[Webpack Plugin] --> D G --> E G --> F H[Create Qiankun] --> A H --> D H --> E H --> F ``` ## 📋 Feature Comparison | Feature | Core API | React Binding | Vue Binding | |---------|----------|---------------|-------------| | **Loading States** | Manual | ✅ Automatic | ✅ Automatic | | **Error Handling** | Manual | ✅ Error Boundary | ✅ Error Boundary | | **Custom Loading** | Manual | ✅ Component | ✅ Slot | | **Custom Errors** | Manual | ✅ Component | ✅ Slot | | **TypeScript** | ✅ Full | ✅ Full | ✅ Full | | **Framework Integration** | Manual | ✅ Hooks | ✅ Composition API | ## 🎨 Usage Patterns ### 1. Simple Loading **React:** ```tsx ``` **Vue:** ```vue ``` ### 2. Custom Loading & Error Handling **React:** ```tsx loading ? : null} errorBoundary={(error) => } /> ``` **Vue:** ```vue ``` ### 3. Props Passing **React:** ```tsx ``` **Vue:** ```vue ``` ## 🚀 Getting Started ### Step 1: Choose Your Stack 1. **React Main App** → Use `@qiankunjs/react` 2. **Vue Main App** → Use `@qiankunjs/vue` 3. **Other Framework** → Use core qiankun APIs ### Step 2: Scaffold Your Project ```bash # Create new project npx create-qiankun my-app # Choose template: # - React main + React micro apps # - Vue main + Vue micro apps # - Umi main + multiple micro apps # - Custom configuration ``` ### Step 3: Configure Micro Apps Add webpack plugin to each micro application: ```javascript // webpack.config.js const { QiankunWebpackPlugin } = require('@qiankunjs/webpack-plugin'); module.exports = { plugins: [ new QiankunWebpackPlugin() ] }; ``` ### Step 4: Start Development ```bash # Start main app cd main-app && npm start # Start micro app (in separate terminal) cd micro-app && npm start ``` ## 🔧 Advanced Configuration ### Environment-based Configuration ```typescript // React main app const MicroAppConfig = { development: { entry: '//localhost:8080', autoSetLoading: true, autoCaptureError: true, }, production: { entry: '//your-domain.com/micro-app', autoSetLoading: false, // Custom loading autoCaptureError: true, } }; const config = MicroAppConfig[process.env.NODE_ENV]; function App() { return ; } ``` ### Multi-app Dashboard ```tsx // React - Multiple micro apps function Dashboard() { return (
); } ``` ## 📚 Documentation Links - [React Bindings](/ecosystem/react) - Complete React integration guide - [Vue Bindings](/ecosystem/vue) - Complete Vue integration guide - [Webpack Plugin](/ecosystem/webpack-plugin) - Build tool configuration - [Create Qiankun](/ecosystem/create-qiankun) - Project scaffolding - [API Reference](/api/) - Core qiankun APIs ## 🤝 Community - [GitHub Discussions](https://github.com/umijs/qiankun/discussions) - Ask questions and share ideas - [Issues](https://github.com/umijs/qiankun/issues) - Bug reports and feature requests - [Changelog](https://github.com/umijs/qiankun/releases) - Latest updates and releases Choose the tools that best fit your project needs and start building powerful micro-frontend applications! ================================================ FILE: docs/ecosystem/react.md ================================================ # React Bindings The official React bindings for qiankun provide a declarative way to integrate micro applications into your React main application. The `@qiankunjs/react` package offers a powerful `` component with built-in loading states, error handling, and TypeScript support. ## 📦 Installation ```bash npm install @qiankunjs/react ``` **Requirements:** - React ≥ 16.9.0 - qiankun ≥ 3.0.0 ## 🚀 Quick Start ### Basic Usage ```tsx import React from 'react'; import { MicroApp } from '@qiankunjs/react'; function App() { return (

Main Application

); } export default App; ``` ### With Loading State ```tsx import React from 'react'; import { MicroApp } from '@qiankunjs/react'; function App() { return ( ); } ``` ### With Error Handling ```tsx import React from 'react'; import { MicroApp } from '@qiankunjs/react'; function App() { return ( ); } ``` ## 🎯 Component API ### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `name` | `string` | ✅ | - | Unique name for the micro application | | `entry` | `string` | ✅ | - | Entry URL of the micro application | | `autoSetLoading` | `boolean` | ❌ | `false` | Automatically manage loading state | | `autoCaptureError` | `boolean` | ❌ | `false` | Automatically handle errors | | `loader` | `(loading: boolean) => React.ReactNode` | ❌ | `undefined` | Custom loading component | | `errorBoundary` | `(error: any) => React.ReactNode` | ❌ | `undefined` | Custom error component | | `className` | `string` | ❌ | `undefined` | CSS class for the micro app container | | `wrapperClassName` | `string` | ❌ | `undefined` | CSS class for the wrapper (when using loader/errorBoundary) | | `settings` | `AppConfiguration` | ❌ | `{}` | qiankun configuration options | | `lifeCycles` | `LifeCycles` | ❌ | `undefined` | Lifecycle hooks | ### Additional Props Any additional props passed to `` will be forwarded to the micro application as props: ```tsx ``` ## 🔄 Lifecycle Management ### Using Ref to Access Micro App Instance ```tsx import React, { useRef, useEffect } from 'react'; import { MicroApp } from '@qiankunjs/react'; function App() { const microAppRef = useRef(); useEffect(() => { // Get micro app status console.log(microAppRef.current?.getStatus()); }, []); const handleUnmount = () => { microAppRef.current?.unmount(); }; return (
); } ``` ### App Status The micro app instance provides these status values: - `NOT_LOADED` - Initial state, not loaded yet - `LOADING_SOURCE_CODE` - Loading application resources - `NOT_BOOTSTRAPPED` - Resources loaded, not bootstrapped - `BOOTSTRAPPING` - Running bootstrap lifecycle - `NOT_MOUNTED` - Bootstrapped but not mounted - `MOUNTING` - Running mount lifecycle - `MOUNTED` - Successfully mounted and running - `UPDATING` - Running update lifecycle - `UNMOUNTING` - Running unmount lifecycle - `UNLOADING` - Cleaning up resources ## 🎨 Customization ### Custom Loading Component ```tsx import React from 'react'; import { MicroApp } from '@qiankunjs/react'; import { Spin, Alert } from 'antd'; const CustomLoader: React.FC<{ loading: boolean }> = ({ loading }) => { if (!loading) return null; return (

Loading micro application...

); }; function App() { return ( } /> ); } ``` ### Custom Error Boundary ```tsx import React from 'react'; import { MicroApp } from '@qiankunjs/react'; import { Alert, Button } from 'antd'; const CustomErrorBoundary: React.FC<{ error: Error }> = ({ error }) => { const handleRetry = () => { window.location.reload(); }; return (
Retry } />
); }; function App() { return ( } /> ); } ``` ### Styling ```tsx import React from 'react'; import { MicroApp } from '@qiankunjs/react'; import './MicroApp.css'; function App() { return ( ); } ``` ```css /* MicroApp.css */ .micro-app-wrapper { border: 1px solid #e8e8e8; border-radius: 6px; overflow: hidden; } .micro-app-container { min-height: 400px; background: #fafafa; } ``` ## 🔧 Advanced Usage ### Multiple Micro Apps ```tsx import React, { useState } from 'react'; import { MicroApp } from '@qiankunjs/react'; import { Tabs } from 'antd'; const { TabPane } = Tabs; function Dashboard() { const [activeTab, setActiveTab] = useState('dashboard'); return (
); } ``` ### Conditional Loading ```tsx import React, { useState } from 'react'; import { MicroApp } from '@qiankunjs/react'; function ConditionalApp() { const [showMicroApp, setShowMicroApp] = useState(false); const [user, setUser] = useState(null); // Only load micro app when user is authenticated if (!user) { return
Please log in to continue
; } return (
{showMicroApp && ( )}
); } ``` ### Dynamic Entry URLs ```tsx import React, { useState } from 'react'; import { MicroApp } from '@qiankunjs/react'; function DynamicApp() { const [environment, setEnvironment] = useState('development'); const entryUrls = { development: '//localhost:8080', staging: '//staging.example.com', production: '//app.example.com' }; return (
); } ``` ## 🎮 State Management ### Using Context to Share State ```tsx import React, { createContext, useContext, useState } from 'react'; import { MicroApp } from '@qiankunjs/react'; // Create a context for shared state const AppContext = createContext(); function MainApp() { const [sharedState, setSharedState] = useState({ user: { id: 1, name: 'John' }, theme: 'dark' }); return (
); } function MicroAppContainer() { const { sharedState } = useContext(AppContext); return ( ); } ``` ### Communication Between Apps ```tsx import React, { useEffect, useRef } from 'react'; import { MicroApp } from '@qiankunjs/react'; function CommunicatingApps() { const microApp1Ref = useRef(); const microApp2Ref = useRef(); useEffect(() => { // Set up communication channel window.appCommunication = { sendMessage: (from, to, message) => { const event = new CustomEvent('microAppMessage', { detail: { from, to, message } }); window.dispatchEvent(event); } }; // Listen for messages const handleMessage = (event) => { console.log('Message received:', event.detail); }; window.addEventListener('microAppMessage', handleMessage); return () => { window.removeEventListener('microAppMessage', handleMessage); delete window.appCommunication; }; }, []); return (
); } ``` ## 🔒 TypeScript Support ### Typed Props ```tsx import React from 'react'; import { MicroApp } from '@qiankunjs/react'; interface UserProfileProps { userId: string; theme: 'light' | 'dark'; permissions: string[]; } // Type the additional props const UserProfileApp: React.FC = () => { const user = getCurrentUser(); return ( ); }; ``` ### Custom Hook for Micro App ```tsx import { useRef, useEffect, useState } from 'react'; import type { MicroApp as MicroAppType } from 'qiankun'; interface UseMicroAppOptions { onStatusChange?: (status: string) => void; onError?: (error: Error) => void; } export function useMicroApp(options: UseMicroAppOptions = {}) { const microAppRef = useRef(); const [status, setStatus] = useState('NOT_LOADED'); const [error, setError] = useState(null); useEffect(() => { const checkStatus = () => { if (microAppRef.current) { const currentStatus = microAppRef.current.getStatus(); if (currentStatus !== status) { setStatus(currentStatus); options.onStatusChange?.(currentStatus); } } }; const interval = setInterval(checkStatus, 1000); return () => clearInterval(interval); }, [status, options]); const handleError = (err: Error) => { setError(err); options.onError?.(err); }; return { microAppRef, status, error, handleError }; } // Usage function App() { const { microAppRef, status, error } = useMicroApp({ onStatusChange: (status) => console.log('Status changed:', status), onError: (error) => console.error('App error:', error) }); return (

Status: {status}

{error &&

Error: {error.message}

}
); } ``` ## 🚀 Performance Optimization ### Lazy Loading ```tsx import React, { Suspense, lazy } from 'react'; // Lazy load the MicroApp component const LazyMicroApp = lazy(() => import('@qiankunjs/react').then(module => ({ default: module.MicroApp })) ); function App() { return ( Loading...
}> ); } ``` ### Memoization ```tsx import React, { memo, useMemo } from 'react'; import { MicroApp } from '@qiankunjs/react'; const MemoizedMicroApp = memo(MicroApp); function OptimizedApp({ user, settings }) { const microAppProps = useMemo(() => ({ userId: user.id, theme: settings.theme, language: settings.language }), [user.id, settings.theme, settings.language]); return ( ); } ``` ## 🐛 Error Handling & Debugging ### Development Mode Error Handling ```tsx import React from 'react'; import { MicroApp } from '@qiankunjs/react'; function DevMicroApp() { const isDevelopment = process.env.NODE_ENV === 'development'; const handleError = (error: Error) => { console.error('Micro app error:', error); if (isDevelopment) { // Show detailed error in development return (

Development Error

{error.stack}
); } // Show user-friendly error in production return (

Something went wrong. Please try again later.

); }; return ( ); } ``` ## 📚 Best Practices ### 1. Use Descriptive Names ```tsx // ✅ Good: Descriptive names // ❌ Bad: Generic names ``` ### 2. Always Handle Loading States ```tsx // ✅ Good: Handle loading states } /> // ❌ Bad: No loading indication ``` ### 3. Implement Error Boundaries ```tsx // ✅ Good: Handle errors gracefully } /> ``` ### 4. Use Environment-specific Configurations ```tsx // ✅ Good: Environment-aware const config = { development: { entry: '//localhost:8080', debug: true }, production: { entry: '//app.example.com', debug: false } }; ``` ## 🔗 Related Documentation - [Vue Bindings](/ecosystem/vue) - Vue UI bindings - [Core APIs](/api/) - qiankun core APIs - [Configuration](/api/configuration) - Configuration options - [Lifecycles](/api/lifecycles) - Lifecycle hooks ================================================ FILE: docs/ecosystem/vue.md ================================================ # Vue Bindings The official Vue bindings for qiankun provide a declarative way to integrate micro applications into your Vue main application. The `@qiankunjs/vue` package offers a powerful `` component with Vue 2/3 compatibility, composition API support, and slot-based customization. ## 📦 Installation ```bash npm install @qiankunjs/vue ``` **Requirements:** - Vue 2.0+ or Vue 3.0+ - qiankun ≥ 3.0.0 - For Vue 2, you may need `@vue/composition-api` ## 🚀 Quick Start ### Vue 3 with Composition API ```vue ``` ### Vue 2 with Options API ```vue ``` ### With Loading State ```vue ``` ### With Error Handling ```vue ``` ## 🎯 Component API ### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `name` | `string` | ✅ | - | Unique name for the micro application | | `entry` | `string` | ✅ | - | Entry URL of the micro application | | `autoSetLoading` | `boolean` | ❌ | `false` | Automatically manage loading state | | `autoCaptureError` | `boolean` | ❌ | `false` | Automatically handle errors | | `className` | `string` | ❌ | `undefined` | CSS class for the micro app container | | `wrapperClassName` | `string` | ❌ | `undefined` | CSS class for the wrapper (when using slots) | | `appProps` | `Record` | ❌ | `undefined` | Props passed to the micro application | | `settings` | `AppConfiguration` | ❌ | `{}` | qiankun configuration options | | `lifeCycles` | `LifeCycles` | ❌ | `undefined` | Lifecycle hooks | ### Slots | Slot | Description | Parameters | |------|-------------|------------| | `loader` | Custom loading component | `{ loading: boolean }` | | `errorBoundary` | Custom error component | `{ error: Error }` | ## 🎨 Customization ### Custom Loading with Slots ```vue ``` ### Custom Error Boundary ```vue ``` ### Styling ```vue ``` ## 🔧 Advanced Usage ### Multiple Micro Apps with Tabs ```vue ``` ### Conditional Loading ```vue ``` ### Dynamic Entry URLs ```vue ``` ## 🎮 State Management ### Using Pinia for State Sharing ```vue ``` ```typescript // stores/app.ts import { defineStore } from 'pinia'; export const useAppStore = defineStore('app', { state: () => ({ user: null, theme: 'dark', language: 'en' }), actions: { setUser(user) { this.user = user; }, setTheme(theme) { this.theme = theme; } } }); ``` ```vue ``` ### Communication Between Apps ```vue ``` ## 🔒 TypeScript Support ### Typed Props with Vue 3 ```vue ``` ### Custom Composable for Micro App ```typescript // composables/useMicroApp.ts import { ref, onMounted, onUnmounted, type Ref } from 'vue'; import type { MicroApp as MicroAppType } from 'qiankun'; interface UseMicroAppOptions { onStatusChange?: (status: string) => void; onError?: (error: Error) => void; } export function useMicroApp(options: UseMicroAppOptions = {}) { const microAppRef: Ref = ref(); const status = ref('NOT_LOADED'); const error = ref(null); const checkStatus = () => { if (microAppRef.value?.microApp) { const currentStatus = microAppRef.value.microApp.getStatus(); if (currentStatus !== status.value) { status.value = currentStatus; options.onStatusChange?.(currentStatus); } } }; const handleError = (err: Error) => { error.value = err; options.onError?.(err); }; let interval: number; onMounted(() => { interval = window.setInterval(checkStatus, 1000); }); onUnmounted(() => { if (interval) { clearInterval(interval); } }); return { microAppRef, status, error, handleError }; } ``` ```vue ``` ## 🚀 Performance Optimization ### Lazy Loading with Suspense ```vue ``` ### Memoization with computed ```vue ``` ### Keep-alive for Route-based Micro Apps ```vue ``` ```vue ``` ## 🐛 Error Handling & Debugging ### Development Mode Error Handling ```vue ``` ```vue ``` ## 📚 Vue 2 Compatibility ### Using with Vue 2 ```vue ``` ### With Composition API in Vue 2 ```vue ``` ## 📚 Best Practices ### 1. Use Descriptive Names ```vue ``` ### 2. Always Handle Loading States ```vue ``` ### 3. Implement Error Boundaries ```vue ``` ### 4. Use Reactive Props ```vue ``` ### 5. Environment-specific Configurations ```vue ``` ## 🔗 Related Documentation - [React Bindings](/ecosystem/react) - React UI bindings - [Core APIs](/api/) - qiankun core APIs - [Configuration](/api/configuration) - Configuration options - [Lifecycles](/api/lifecycles) - Lifecycle hooks ================================================ FILE: docs/faq/index.md ================================================ # Frequently Asked Questions This FAQ covers the most common questions and issues encountered when working with qiankun. If you can't find the answer you're looking for, please check our [GitHub Issues](https://github.com/umijs/qiankun/issues) or join our community discussions. ## 🚀 Getting Started ### Q: What is qiankun and when should I use it? **A:** qiankun is a micro-frontend framework based on single-spa that enables you to build large-scale frontend applications by composing multiple smaller, independent applications. You should consider qiankun when: - Your team is growing and you need to scale development across multiple teams - You have legacy applications that need to coexist with new features - You want to use different frameworks (React, Vue, Angular) in one application - You need independent deployment capabilities for different parts of your app ### Q: How does qiankun differ from other micro-frontend solutions? **A:** qiankun provides several key advantages: - **Production-ready**: Built and tested by Ant Financial (now Ant Group) in large-scale applications - **Framework agnostic**: Works with React, Vue, Angular, and vanilla JavaScript - **Powerful sandboxing**: JavaScript and CSS isolation out of the box - **HTML entry**: Simple configuration using HTML files as entry points - **Rich ecosystem**: UI bindings, CLI tools, and webpack plugins ### Q: Can I use qiankun with existing applications? **A:** Yes! qiankun is designed to work with existing applications. You can: 1. **Wrap existing apps**: Turn your current app into a qiankun main application 2. **Incremental migration**: Gradually extract features into micro applications 3. **Legacy integration**: Run legacy apps alongside new micro apps 4. **Framework migration**: Migrate from one framework to another progressively ## 🔧 Installation and Setup ### Q: I'm getting CORS errors when loading micro applications. How do I fix this? **A:** CORS errors are common in development. Here are solutions: **For webpack dev server:** ```javascript // webpack.config.js or vue.config.js module.exports = { devServer: { headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS', 'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization' } } }; ``` **For Create React App (using CRACO):** ```javascript // craco.config.js module.exports = { devServer: { headers: { 'Access-Control-Allow-Origin': '*' } } }; ``` **For production, configure your server:** ```nginx # nginx.conf location / { add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS'; add_header Access-Control-Allow-Headers 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range'; } ``` ### Q: My micro application won't load. What should I check? **A:** Follow this troubleshooting checklist: 1. **Check the network tab**: Are there 404 errors for your micro app resources? 2. **Verify CORS**: Are there CORS errors in the console? 3. **Check the entry point**: Is your HTML entry file accessible? 4. **Validate the export**: Does your micro app export the required lifecycle methods? 5. **Check the container**: Is the container element present in the DOM? **Example of correct micro app export:** ```javascript // Micro app entry file export async function bootstrap() { console.log('micro app bootstrapped'); } export async function mount(props) { console.log('micro app mounted', props); // Your app mounting logic } export async function unmount(props) { console.log('micro app unmounted', props); // Your app cleanup logic } ``` ### Q: How do I handle different base paths for my micro applications? **A:** Configure the public path in your micro applications: **For webpack:** ```javascript // webpack.config.js module.exports = { output: { publicPath: process.env.NODE_ENV === 'production' ? 'https://mycdn.com/micro-app/' : 'http://localhost:8080/' } }; ``` **For runtime configuration:** ```javascript // public-path.js in your micro app if (window.__POWERED_BY_QIANKUN__) { __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; } ``` ## 🏗️ Architecture and Design ### Q: How should I structure my micro-frontend architecture? **A:** Follow these architectural principles: **1. Domain-driven design:** ``` Main App (Shell) ├── User Management (HR Domain) ├── Product Catalog (Commerce Domain) ├── Analytics Dashboard (BI Domain) └── Settings (System Domain) ``` **2. Shared vs. Independent:** - **Shared**: Authentication, navigation, design system - **Independent**: Business logic, data fetching, internal state **3. Communication patterns:** ```javascript // Event-driven communication window.dispatchEvent(new CustomEvent('user-updated', { detail: { userId: 123 } })); // Props-based communication registerMicroApps([{ name: 'user-app', entry: '//localhost:8080', container: '#container', activeRule: '/users', props: { userPermissions: currentUser.permissions, onUserUpdate: handleUserUpdate } }]); ``` ### Q: How do I share dependencies between micro applications? **A:** Several approaches work well: **1. External dependencies (recommended):** ```javascript // webpack.config.js module.exports = { externals: { 'react': 'React', 'react-dom': 'ReactDOM', 'lodash': '_' } }; ``` **2. Module Federation:** ```javascript // Main app webpack config new ModuleFederationPlugin({ name: 'shell', shared: { react: { singleton: true }, 'react-dom': { singleton: true } } }); ``` **3. CDN approach:** ```html ``` ### Q: Can micro applications communicate with each other? **A:** Yes, here are the recommended patterns: **1. Event-driven communication:** ```javascript // Micro app A const notifyOtherApps = (data) => { window.dispatchEvent(new CustomEvent('app-a-event', { detail: data })); }; // Micro app B window.addEventListener('app-a-event', (event) => { console.log('Received from app A:', event.detail); }); ``` **2. Shared state management:** ```javascript // Global store window.__SHARED_STORE__ = { user: null, subscribe: [], updateUser: (user) => { window.__SHARED_STORE__.user = user; window.__SHARED_STORE__.subscribers.forEach(callback => callback(user)); } }; ``` **3. Props from main app:** ```javascript // Main app coordinates communication const handleDataChange = (data) => { // Update props for all relevant micro apps updateMicroAppProps('app-a', { sharedData: data }); updateMicroAppProps('app-b', { sharedData: data }); }; ``` ## 🎨 Styling and CSS ### Q: My CSS styles are conflicting between micro applications. How do I fix this? **A:** Use qiankun's built-in style isolation: **1. Strict style isolation (Shadow DOM):** ```javascript import { start } from 'qiankun'; start({ sandbox: { strictStyleIsolation: true } }); ``` **2. Experimental style isolation (CSS scoping):** ```javascript start({ sandbox: { experimentalStyleIsolation: true } }); ``` **3. Manual CSS scoping:** ```css /* Prefix all your styles */ .my-micro-app .button { background: blue; } .my-micro-app .container { padding: 20px; } ``` See our [Style Isolation Guide](/cookbook/style-isolation) for comprehensive solutions. ### Q: Can I use CSS-in-JS libraries with qiankun? **A:** Absolutely! CSS-in-JS libraries work great with qiankun: **Styled Components:** ```jsx import styled from 'styled-components'; const Button = styled.button` background: blue; color: white; `; ``` **Emotion:** ```jsx /** @jsxImportSource @emotion/react */ import { css } from '@emotion/react'; const buttonStyle = css` background: blue; color: white; `; ``` CSS-in-JS provides natural isolation since styles are scoped to components. ## 🔄 Routing and Navigation ### Q: How do I handle routing in a micro-frontend setup? **A:** qiankun supports multiple routing strategies: **1. Route-based micro apps (recommended):** ```javascript registerMicroApps([ { name: 'user-management', entry: '//localhost:8080', container: '#container', activeRule: '/users' // Loads when route starts with /users }, { name: 'product-catalog', entry: '//localhost:8081', container: '#container', activeRule: ['/products', '/categories'] // Multiple routes } ]); ``` **2. Programmatic routing:** ```javascript // Navigate between micro apps import { navigateToUrl } from 'single-spa'; const navigateToUsers = () => { navigateToUrl('/users'); }; ``` **3. Hash routing:** ```javascript registerMicroApps([ { name: 'hash-app', entry: '//localhost:8080', container: '#container', activeRule: '#/app' // Hash-based routing } ]); ``` ### Q: Can micro applications have their own internal routing? **A:** Yes! Each micro application can have its own internal router: **React Router example:** ```jsx // In your micro app import { BrowserRouter, Routes, Route } from 'react-router-dom'; function App() { const basename = window.__POWERED_BY_QIANKUN__ ? '/users' : '/'; return ( } /> } /> } /> ); } ``` ## 🚀 Performance ### Q: My micro-frontend app is loading slowly. How can I improve performance? **A:** Follow these optimization strategies: **1. Enable prefetching:** ```javascript start({ prefetch: true // or 'all' or specific app names }); ``` **2. Use code splitting:** ```javascript // Dynamic imports in micro apps const HeavyComponent = React.lazy(() => import('./HeavyComponent')); ``` **3. Optimize bundle sizes:** ```javascript // webpack.config.js module.exports = { optimization: { splitChunks: { chunks: 'all' } } }; ``` See our [Performance Optimization Guide](/cookbook/performance) for detailed strategies. ### Q: How do I prevent memory leaks in micro applications? **A:** Implement proper cleanup: ```javascript // Micro app lifecycle export async function unmount() { // Clear timers clearInterval(myInterval); // Remove event listeners window.removeEventListener('resize', handleResize); // Clean up subscriptions subscription.unsubscribe(); // Clear caches cache.clear(); } ``` ## 🛠️ Development and Debugging ### Q: How do I debug micro applications in development? **A:** Use these debugging strategies: **1. Enable source maps:** ```javascript // webpack.config.js module.exports = { devtool: 'source-map' }; ``` **2. Use browser dev tools:** - Network tab: Check resource loading - Console: View error messages - Elements: Inspect DOM structure - Sources: Debug JavaScript with breakpoints **3. qiankun debugging:** ```javascript // Enable detailed logging localStorage.setItem('qiankun:debug', true); ``` ### Q: Can I use hot reload with micro applications? **A:** Yes, with some configuration: **For webpack dev server:** ```javascript // webpack.config.js module.exports = { devServer: { hot: true, headers: { 'Access-Control-Allow-Origin': '*' } } }; ``` **Note**: Hot reload works within each micro app, but changes to the main app may require a full refresh. ## 🔒 Security ### Q: How do I handle authentication across micro applications? **A:** Centralize authentication in the main application: **1. Token-based authentication:** ```javascript // Main app handles auth const userToken = await authenticate(credentials); localStorage.setItem('token', userToken); // Pass token to micro apps registerMicroApps([{ name: 'secure-app', entry: '//localhost:8080', container: '#container', activeRule: '/secure', props: { token: userToken, user: currentUser } }]); ``` **2. Shared authentication state:** ```javascript // Global auth state window.__AUTH_STATE__ = { user: currentUser, token: userToken, isAuthenticated: true }; ``` ### Q: Are there security concerns with micro-frontends? **A:** Be aware of these security considerations: **1. Content Security Policy (CSP):** ```html ``` **2. CORS configuration:** - Only allow trusted origins - Validate requests properly - Use HTTPS in production **3. Dependency security:** - Regularly audit dependencies - Use tools like `npm audit` - Keep dependencies updated ## 📱 Mobile and Browser Support ### Q: Does qiankun work on mobile devices? **A:** Yes, qiankun works on mobile with considerations: **1. Touch event optimization:** ```javascript // Use passive listeners element.addEventListener('touchstart', handler, { passive: true }); ``` **2. Viewport management:** ```html ``` **3. Performance optimization:** - Reduce bundle sizes - Use lazy loading - Optimize images and assets ### Q: Which browsers does qiankun support? **A:** qiankun supports modern browsers: - **Chrome**: 49+ - **Firefox**: 45+ - **Safari**: 10+ - **Edge**: 79+ - **IE**: Not supported For older browsers, consider polyfills: ```html ``` ## 🚢 Deployment ### Q: How do I deploy micro-frontend applications? **A:** Use independent deployment strategy: **1. Separate builds:** ```bash # Build each app independently cd main-app && npm run build cd micro-app-1 && npm run build cd micro-app-2 && npm run build ``` **2. CDN deployment:** ```javascript // Configure different CDNs for each app const microApps = [ { name: 'app-1', entry: 'https://cdn1.example.com/app-1/', container: '#container', activeRule: '/app-1' }, { name: 'app-2', entry: 'https://cdn2.example.com/app-2/', container: '#container', activeRule: '/app-2' } ]; ``` ### Q: How do I handle versioning and updates? **A:** Implement version management: **1. Semantic versioning:** ```javascript // Package.json for each micro app { "name": "user-management-app", "version": "1.2.3" } ``` **2. Runtime version checking:** ```javascript const requiredVersion = '1.2.0'; const currentVersion = window.__MICRO_APP_VERSION__; if (!semver.gte(currentVersion, requiredVersion)) { console.warn('Micro app version compatibility issue'); } ``` ## 🔗 Integration ### Q: Can I use qiankun with Server-Side Rendering (SSR)? **A:** SSR with micro-frontends is complex but possible: **1. Static rendering:** - Render micro apps on the server - Hydrate on the client **2. Considerations:** - Each micro app needs SSR support - Coordination between apps is challenging - Performance implications **Alternative approaches:** - Use edge-side includes (ESI) - Implement micro-frontends at the page level - Consider client-side rendering with fast initial loads ### Q: How do I integrate qiankun with existing build tools? **A:** qiankun works with various build tools: **Webpack:** Use `@qiankunjs/webpack-plugin` **Vite:** Use `vite-plugin-qiankun` **Rollup:** Manual configuration **Parcel:** Manual configuration See our [Ecosystem](/ecosystem/) section for specific integrations. ## 🤝 Community and Support ### Q: Where can I get help if I'm stuck? **A:** Multiple support channels are available: 1. **GitHub Issues**: [umijs/qiankun](https://github.com/umijs/qiankun/issues) 2. **Discussions**: GitHub Discussions for questions 3. **Stack Overflow**: Tag questions with `qiankun` 4. **Discord/Slack**: Community chat rooms ### Q: How can I contribute to qiankun? **A:** We welcome contributions: 1. **Bug reports**: Submit detailed issue reports 2. **Feature requests**: Propose new features 3. **Code contributions**: Submit pull requests 4. **Documentation**: Improve docs and examples 5. **Community**: Help answer questions See our [Contributing Guide](https://github.com/umijs/qiankun/blob/master/CONTRIBUTING.md) for details. --- ## 📚 Additional Resources - [Complete API Reference](/api/) - [Best Practices Guide](/cookbook/) - [Ecosystem Tools](/ecosystem/) - [GitHub Repository](https://github.com/umijs/qiankun) - [Example Applications](https://github.com/umijs/qiankun/tree/master/examples) **Can't find what you're looking for?** Please [open an issue](https://github.com/umijs/qiankun/issues/new) or start a [discussion](https://github.com/umijs/qiankun/discussions) - we're here to help! ================================================ FILE: docs/guide/index.md ================================================ # What is qiankun? qiankun is a micro-frontend implementation library based on [single-spa](https://github.com/single-spa/single-spa), designed to help you build a production-ready micro-frontend architecture system more simply and painlessly. qiankun hatched from Ant Financial's unified front-end platform for cloud products based on micro-frontends architecture. After full testing and polishing of a number of online applications, we extracted its micro-frontends kernel and open sourced it. We hope to help the systems who has the same requirement more convenient to build its own micro-frontends application in the community. At the same time, with the help of community, qiankun will be polished and improved. At present qiankun has served more than 2000 online applications inside Ant, and it is definitely trustworthy in terms of ease of use and completeness. ## 💡 What Are Micro FrontEnds? > Techniques, strategies and recipes for building a **modern web app** with **multiple teams** that can **ship features independently**. -- Micro Frontends Micro Frontends architecture has the following core values: - **Technology Agnostic** - The main framework does not restrict access to the technology stack of the application, and the sub-applications have full autonomy. - **Independent Development and Deployment** - The sub application repo is independent, and the frontend and backend can be independently developed. After deployment, the main framework can be updated automatically. - **Incremental Upgrade** - In the face of various complex scenarios, it is often difficult for us to upgrade or refactor the entire technology stack of an existing system. Micro frontends is a very good method and strategy for implementing progressive refactoring. - **Isolated Runtime** - State is isolated between each subapplication and no shared runtime state. The micro-frontends architecture is designed to solve the application of a single application in a relatively long time span. As a result of the increase in the number of people and teams involved, it has evolved from a common application to a Frontend Monolith then becomes unmaintainable. Such a problem is especially common in enterprise web applications. ### Problems with Traditional Monolithic Applications ```bash ┌─────────────────────────────────────┐ │ Monolithic Frontend │ │ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │ │ │Mod A│ │Mod B│ │Mod C│ │Mod D│ │ │ └─────┘ └─────┘ └─────┘ └─────┘ │ │ Tightly coupled, hard to │ │ maintain │ └─────────────────────────────────────┘ ``` ### Micro-Frontend Architecture ```bash ┌─────────────────────────────────────┐ │ Main Application │ │ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │ │ │App A│ │App B│ │App C│ │App D│ │ │ └─────┘ └─────┘ └─────┘ └─────┘ │ │ Independent development, deploy, │ │ technology agnostic │ └─────────────────────────────────────┘ ``` ## 🎯 Core Philosophy The core design philosophy of qiankun is **decentralized runtime**, which means: - **🥄 Simple** - Since the main application sub-applications can be independent of the technology stack, qiankun is just a jQuery-like library for users. You need to call several qiankun APIs to complete the micro frontends transformation of your application. At the same time, due to the design of qiankun's HTML entry and sandbox, accessing sub-applications is as simple as using an iframe. - **🍡 Decoupling/Technology Agnostic** - As the core goal of the micro frontends is to disassemble the monolithic application into a number of loosely coupled micro applications that can be autonomous, all the designs of qiankun are follow this principle, such as HTML Entry, sandbox, and communicating mechanism between applications. Only in this way can we ensure that sub-applications truly have the ability to develop and run independently. ## 🏗️ Architecture ```mermaid graph TD A[Main App] --> B[qiankun] B --> C[Micro App A] B --> D[Micro App B] B --> E[Micro App C] F[Routing] --> A G[Resource Loading] --> B H[Lifecycle] --> B I[Sandbox Isolation] --> B ``` qiankun is based on the following core capabilities: ### 🔄 Lifecycle Management Each micro application has a complete lifecycle: - **bootstrap** - Application initialization - **mount** - Application mounting - **unmount** - Application unmounting - **update** - Application update (optional) ### 🛡️ Sandbox Isolation - **JS Isolation** - Provides multiple sandbox solutions to ensure JS between applications do not affect each other - **CSS Isolation** - Achieves style isolation through style scoping or Shadow DOM ### 📡 Resource Loading - **HTML Entry** - Load micro applications through HTML as entry - **Preloading** - Supports application resource preloading to improve user experience - **Caching** - Intelligent resource caching strategy ## 🤔 Why Not Iframe? While iframe is the most natural solution for implementing micro frontends, it has some serious limitations: - **URL synchronization issues** - The URL of the iframe and the main application cannot be synchronized - **UI inconsistencies** - The iframe is in a completely isolated context, which makes it difficult to maintain consistent UI styling - **Performance issues** - Each iframe creates a new context, consuming more memory and CPU resources - **SEO unfriendly** - Search engines cannot properly index iframe content - **Security restrictions** - Cross-origin iframe communication has security limitations - **User experience problems** - Issues with browser history, refresh, and bookmarking qiankun solves these problems by providing a complete micro-frontend solution that maintains the isolation benefits of iframe while avoiding its limitations. ## ✨ Features qiankun provides the following key features: - **📦 Based On single-spa** - Provides more out-of-box APIs based on single-spa - **📱 Technology Agnostic** - Any JavaScript framework can use/integrate, whether React/Vue/Angular/jQuery or others - **💪 HTML Entry Access Mode** - Allows you to access sub-applications as simply as using an iframe - **🛡️ Style Isolation** - Ensures styles don't interfere with each other between applications - **🧳 JS Sandbox** - Ensures that global variables/events do not conflict between sub-applications - **⚡ Prefetch Assets** - Prefetch unopened sub-application assets during browser idle time to speed up sub-application opening - **🔌 Umi Plugin** - @umijs/plugin-qiankun is provided for umi applications to switch to a micro frontends architecture system with one line of code ## 🎯 Use Cases qiankun is particularly suitable for the following scenarios: - **Large Enterprise Applications** - Multi-team collaborative development - **Technology Stack Migration** - Progressive upgrade of legacy systems - **Feature Modularization** - Independent development and deployment of feature modules - **Third-party Integration** - Integration of external applications or services ## 🚀 Get Started Ready to start using qiankun? Check out our [Quick Start](/guide/quick-start) guide to build your first micro-frontend application in minutes! ## 📚 Learn More - [Tutorial](/guide/tutorial) - Step-by-step tutorial from scratch - [Core Concepts](/guide/concepts) - Understand qiankun's design principles - [Main Application](/guide/main-app) - How to configure the main application - [Micro Application](/guide/micro-app) - How to transform existing applications ================================================ FILE: docs/guide/quick-start.md ================================================ # Quick Start This guide will help you set up a basic qiankun micro-frontend application in 5 minutes. ## Prerequisites - Node.js 16+ - Basic JavaScript/TypeScript knowledge - Understanding of React, Vue or other frontend frameworks ## 🚀 Step 1: Install qiankun ::: code-group ```bash [npm] npm install qiankun ``` ```bash [yarn] yarn add qiankun ``` ```bash [pnpm] pnpm add qiankun ``` ::: ## 🏠 Step 2: Main Application Configuration ### 2.1 Create Main Application ```bash # Create main application using your favorite framework npx create-react-app main-app cd main-app npm install qiankun ``` ### 2.2 Register Micro Applications Register micro applications in the main application's entry file: ```typescript // src/index.js import { registerMicroApps, start } from 'qiankun'; // register micro applications registerMicroApps([ { name: 'vue-app', // micro app name, unique entry: '//localhost:8080', // micro app entry container: '#subapp-viewport', // micro app mount node activeRule: '/vue', // micro app activation rule }, { name: 'react-app', entry: '//localhost:3001', container: '#subapp-viewport', activeRule: '/react', }, ]); // start qiankun start(); // render main application normally ReactDOM.render(, document.getElementById('root')); ``` ### 2.3 Create Micro Application Container Reserve mount nodes for micro applications in the main application: ```jsx // src/App.js import React from 'react'; import { BrowserRouter as Router, Link } from 'react-router-dom'; function App() { return (

qiankun Main Application

{/* micro application mount point */}
); } export default App; ``` ## 📦 Step 3: Micro Application Configuration ### 3.1 Create Vue Micro Application ```bash npm install -g @vue/cli vue create vue-micro-app cd vue-micro-app ``` ### 3.2 Export Lifecycle Modify `src/main.js`: ```javascript import { createApp } from 'vue'; import { createRouter, createWebHistory } from 'vue-router'; import App from './App.vue'; import routes from './router'; let instance = null; let router = null; /** * Render function * Two scenarios: called by main app lifecycle / micro app runs independently */ function render(props = {}) { const { container } = props; router = createRouter({ history: createWebHistory(window.__POWERED_BY_QIANKUN__ ? '/vue' : '/'), routes, }); instance = createApp(App); instance.use(router); instance.mount(container ? container.querySelector('#app') : '#app'); } // run independently if (!window.__POWERED_BY_QIANKUN__) { render(); } export async function bootstrap() { console.log('[vue] vue app bootstraped'); } export async function mount(props) { console.log('[vue] props from main framework', props); render(props); } export async function unmount() { instance.unmount(); instance._container.innerHTML = ''; instance = null; router = null; } ``` ### 3.3 Configure Webpack Modify `vue.config.js`: ```javascript const { defineConfig } = require('@vue/cli-service'); const packageName = require('./package.json').name; module.exports = defineConfig({ transpileDependencies: true, devServer: { port: 8080, headers: { 'Access-Control-Allow-Origin': '*', }, }, configureWebpack: { output: { library: `${packageName}-[name]`, libraryTarget: 'umd', chunkLoadingGlobal: `webpackJsonp_${packageName}`, }, }, }); ``` ### 3.4 Create React Micro Application ```bash npx create-react-app react-micro-app cd react-micro-app npm install react-app-rewired --save-dev ``` 修改 `src/index.js`: ```javascript import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; function render(props) { const { container } = props; ReactDOM.render( , container ? container.querySelector('#root') : document.querySelector('#root') ); } if (!window.__POWERED_BY_QIANKUN__) { render({}); } export async function bootstrap() { console.log('[react16] react app bootstraped'); } export async function mount(props) { console.log('[react16] props from main framework', props); render(props); } export async function unmount(props) { const { container } = props; ReactDOM.unmountComponentAtNode( container ? container.querySelector('#root') : document.querySelector('#root') ); } ``` 创建 `config-overrides.js`: ```javascript const { name } = require('./package'); module.exports = { webpack: (config) => { config.output.library = `${name}-[name]`; config.output.libraryTarget = 'umd'; config.output.chunkLoadingGlobal = `webpackJsonp_${name}`; return config; }, devServer: function (configFunction) { return function(proxy, allowedHost) { const config = configFunction(proxy, allowedHost); config.port = 3001; config.headers = { 'Access-Control-Allow-Origin': '*', }; return config; }; }, }; ``` Modify scripts in `package.json`: ```json { "scripts": { "start": "react-app-rewired start", "build": "react-app-rewired build", "test": "react-app-rewired test", "eject": "react-scripts eject" } } ``` ## 🎉 Step 4: Start Applications ### 4.1 Start All Applications ```bash # Terminal 1: Start main application cd main-app npm start # Terminal 2: Start Vue micro application cd vue-micro-app npm run serve # Terminal 3: Start React micro application cd react-micro-app npm start ``` ### 4.2 Access Applications - Main application: http://localhost:3000 - Click navigation to switch to different micro applications ## ✅ Verify Integration If everything is configured correctly, you should see: 1. ✅ Main application loads normally 2. ✅ Clicking navigation links switches to corresponding micro applications 3. ✅ Micro applications can be accessed independently (http://localhost:8080, http://localhost:3001) 4. ✅ Browser console shows lifecycle logs ## 🎯 Common Issues ::: warning CORS Issues Make sure your micro application's webpack devServer is configured with CORS headers: ```javascript headers: { 'Access-Control-Allow-Origin': '*', } ``` ::: ::: warning Routing Conflicts In integration mode, micro application routing needs to add corresponding prefixes: ```javascript // Vue Router history: createWebHistory(window.__POWERED_BY_QIANKUN__ ? '/vue' : '/') // React Router ``` ::: ## 🚀 Next Steps Congratulations! You have successfully built your first qiankun micro-frontend application. Next you can: - [Core Concepts](/guide/concepts) - Deeply understand qiankun's design principles - [Main Application](/guide/main-app) - Learn more main application configuration options - [Micro Application](/guide/micro-app) - Learn how to transform existing applications - [Best Practices](/cookbook/) - Learn production environment best practices ================================================ FILE: docs/guide/tutorial.md ================================================ # Tutorial This tutorial is suitable for people who are new to `qiankun`, and introduces how to build a `qiankun` project from scratch. ## Main Application The main application is not limited to any specific technical framework, it only needs to provide a container DOM, then register the micro applications and start qiankun. Install `qiankun` first: ```shell $ yarn add qiankun # or npm i qiankun -S ``` Register the micro applications and start: ```js import { registerMicroApps, start } from 'qiankun'; registerMicroApps([ { name: 'angularApp', entry: '//localhost:4200', container: '#container', activeRule: '/app-angular', }, { name: 'reactApp', entry: '//localhost:3000', container: '#container', activeRule: '/app-react', }, { name: 'vueApp', entry: '//localhost:8080', container: '#container', activeRule: '/app-vue', }, ]); // start qiankun start(); ``` ## Micro Applications Micro applications are divided into projects with `webpack` and without `webpack`. The things that need to be done for micro applications with `webpack` (mainly refers to Vue, React, Angular) are: 1. Add `public-path.js` file, used to modify the runtime `publicPath`. [What is publicPath at runtime?](https://webpack.js.org/guides/public-path/#on-the-fly). ::: warning Note: `publicPath` at runtime and `publicPath` at build time are different, and the two cannot be equivalently substituted. ::: 2. It is recommended to use the route of the `history` mode for the micro application. The route `base` needs to be set, and the value is the same as its `activeRule`. 3. Import `public-path.js` at the top of the entry file, modify and export three `lifecycle` functions. 4. Modify the `webpack` configuration to allow cross-domain in development environments and bundle with `umd`. The main modifications are the above four, which may change according to different situations of the project. For example, if your project is deployed separately from all other files of `index.html`, it means that you have set the `publicPath` at build time to the full path, so you don't need to modify the `publicPath` at runtime (the first step can be omitted). For micro applications built without `webpack`, just mount `lifecycle` functions to `window`. ### React Micro Application Take the `react 16` project generated by `create react app` as an example, with `react-router-dom` 5.x. 1. Add `public-path.js` in the `src` directory: ```js if (window.__POWERED_BY_QIANKUN__) { __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; } ``` 2. Set the `base` of `history` mode routing: ```html ``` 3. The entry file `index.js` is modified. In order to avoid the root id `#root` from conflicting with other DOMs, the search scope needs to be limited. ```js import './public-path'; import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; function render(props) { const { container } = props; ReactDOM.render(, container ? container.querySelector('#root') : document.querySelector('#root')); } if (!window.__POWERED_BY_QIANKUN__) { render({}); } export async function bootstrap() { console.log('[react16] react app bootstraped'); } export async function mount(props) { console.log('[react16] props from main framework', props); render(props); } export async function unmount(props) { const { container } = props; ReactDOM.unmountComponentAtNode(container ? container.querySelector('#root') : document.querySelector('#root')); } ``` ::: tip It's important: When mount a sub-application through ReactDOM.render, need to ensure each sub-application load with a new router instance. ::: 4. Modify `webpack` configuration Install the plugin `@rescripts/cli`, of course, you can also choose other plugins, such as `react-app-rewired`. ```bash npm i -D @rescripts/cli ``` Add `.rescriptsrc.js` to the root directory: ```js const { name } = require('./package'); module.exports = { webpack: (config) => { config.output.library = `${name}-[name]`; config.output.libraryTarget = 'umd'; config.output.jsonpFunction = `webpackJsonp_${name}`; config.output.globalObject = 'window'; return config; }, devServer: (_) => { const config = _; config.headers = { 'Access-Control-Allow-Origin': '*', }; config.historyApiFallback = true; config.hot = false; config.watchContentBase = false; config.liveReload = false; return config; }, }; ``` Modify `package.json`: ```diff - "start": "react-scripts start", + "start": "rescripts start", - "build": "react-scripts build", + "build": "rescripts build", - "test": "react-scripts test", + "test": "rescripts test", - "eject": "react-scripts eject" ``` ### React MicroApp Component 1. Install ```bash npm i qiankun npm i @qiankunjs/react ``` 2. Usage Load (or unload) child applications directly through the `` component, which provides loading and error catching-related capabilities: ```tsx import { MicroApp } from '@qiankunjs/react'; export default function Page() { return ; } ``` ### Vue Micro Application Take the `vue 2.x` project generated by `vue-cli 3+` as an example, and add it after the `vue 3` version becomes stable. 1. Add `public-path.js` in the `src` directory: ```js if (window.__POWERED_BY_QIANKUN__) { __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; } ``` 2. The entry file `main.js` is modified. In order to avoid the root id `#app` from conflicting with other DOMs, the search scope needs to be limited. ```js import './public-path'; import Vue from 'vue'; import VueRouter from 'vue-router'; import App from './App.vue'; import routes from './router'; import store from './store'; Vue.config.productionTip = false; let router = null; let instance = null; function render(props = {}) { const { container } = props; router = new VueRouter({ base: window.__POWERED_BY_QIANKUN__ ? '/app-vue/' : '/', mode: 'history', routes, }); instance = new Vue({ router, store, render: (h) => h(App), }).$mount(container ? container.querySelector('#app') : '#app'); } // when run independently if (!window.__POWERED_BY_QIANKUN__) { render(); } export async function bootstrap() { console.log('[vue] vue app bootstraped'); } export async function mount(props) { console.log('[vue] props from main framework', props); render(props); } export async function unmount() { instance.$destroy(); instance.$el.innerHTML = ''; instance = null; router = null; } ``` 3. Modify `webpack` configuration(`vue.config.js`): ```js const { name } = require('./package'); module.exports = { devServer: { headers: { 'Access-Control-Allow-Origin': '*', }, }, configureWebpack: { output: { library: `${name}-[name]`, libraryTarget: 'umd', // bundle the micro app into umd library format jsonpFunction: `webpackJsonp_${name}`, }, }, }; ``` ### Vue MicroApp Component 1. Install ```bash npm i qiankun npm i @qiankunjs/vue ``` 2. Usage Load (or unload) child apps directly through the `` component, which provides loading and error catching-related capabilities: ```vue ``` ### Angular micro app Take the `angular 9` project generated by `Angular-cli 9` as an example, other versions of `angular` will be added later. 1. Add the file `public-path.js` in the `src` directory with the content: ```js if (window.__POWERED_BY_QIANKUN__) { // eslint-disable-next-line no-undef __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; } ``` 2. Set the `base` of `history` mode routing, `src/app/app-routing.module.ts` file: ```diff + import { APP_BASE_HREF } from '@angular/common'; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], // @ts-ignore + providers: [{ provide: APP_BASE_HREF, useValue: window.__POWERED_BY_QIANKUN__ ? '/app-angular' : '/' }] }) ``` 3. Modify the entry file, `src/main.ts` file: ```ts import './public-path'; import { enableProdMode, NgModuleRef } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; if (environment.production) { enableProdMode(); } let app: void | NgModuleRef; async function render() { app = await platformBrowserDynamic() .bootstrapModule(AppModule) .catch((err) => console.error(err)); } if (!(window as any).__POWERED_BY_QIANKUN__) { render(); } export async function bootstrap(props: Object) { console.log(props); } export async function mount(props: Object) { render(); } export async function unmount(props: Object) { console.log(props); // @ts-ignore app.destroy(); } ``` 4. Modify `webpack` bundling configuration First install the `@angular-builders/custom-webpack` plugin. **Note: `Angular 9` project can only install `9.x` version, `angular 10` project can install the latest version**. ```bash npm i @angular-builders/custom-webpack@9.2.0 -D ``` Add `custom-webpack.config.js` to the root directory with the content: ```js const appName = require('./package.json').name; module.exports = { devServer: { headers: { 'Access-Control-Allow-Origin': '*', }, }, output: { library: `${appName}-[name]`, libraryTarget: 'umd', jsonpFunction: `webpackJsonp_${appName}`, }, }; ``` Modify `angular.json`, change the values of `[packageName]> architect> build> builder` and `[packageName]> architect> serve> builder` to the plugins we installed, and add our webpack's configuration file to `[ packageName]> architect> build> options`. ```diff - "builder": "@angular-devkit/build-angular:browser", + "builder": "@angular-builders/custom-webpack:browser", "options": { + "customWebpackConfig": { + "path": "./custom-webpack.config.js" + } } ``` ```diff - "builder": "@angular-devkit/build-angular:dev-server", + "builder": "@angular-builders/dev-server:generic", ``` 5. Solve the problem of `zone.js` Import `zone.js` in **main app**, it needs to be imported before `import qiankun`. Delete the code of import `zone.js` in the `src/polyfills.ts` of the micro app. ```diff - import 'zone.js/dist/zone'; ``` Add the following content to the `` tag in the `src/index.html` of the micro app, which is used when the micro app is accessed independently. ```html ``` 6. Fix `ng build` comand's error report, modify `tsconfig.json` file, reference[issues/431](https://github.com/umijs/qiankun/issues/431). ```diff - "target": "es2015", + "target": "es5", + "typeRoots": [ + "node_modules/@types" + ], ``` 7. In order to prevent the conflict of `` when the main app or other micro apps are also `angular`, it is recommended to add a unique id to ``, such as Say the current app name. src/index.html : ```diff - + ``` src/app/app.component.ts : ```diff - selector: 'app-root', + selector: '#angular9 app-root', ``` Of course, you can also choose to use the `single-spa-angular` plugin, refer to[ single-spa-angular official website](https://single-spa.js.org/docs/ecosystem-angular) 和 [angular demo](https://github.com/umijs/qiankun/tree/master/examples/angular9) (**supplement**)The angular7 has the same steps as angular9 except for step 4. The steps for angular7 to modify the `webpack` configuration are as follows: In addition to installing the 7.x version of `angular-builders/custom-webpack`, you also need to install `angular-builders/dev-server`. ```bash npm i @angular-builders/custom-webpack@7 -D npm i @angular-builders/dev-server -D ``` Add `custom-webpack.config.js` to the root directory, same as above. Modify `angular.json`, `[packageName] > architect > build > builder` is the same as Angular9, and `[packageName] > architect > serve > builder` is different from Angular9. ```diff - "builder": "@angular-devkit/build-angular:browser", + "builder": "@angular-builders/custom-webpack:browser", "options": { "customWebpackConfig": { "path": "./custom-webpack.config.js" } } ``` ```diff - "builder": "@angular-devkit/build-angular:dev-server", + "builder": "@angular-builders/dev-server:generic", ``` ### Micro app built without webpack Some apps that are not built by `webpack`, such as `jQuery` app, `jsp` app, can be handled according to this. Before modify, please make sure that the resources such as pictures, audio and video in your project can be loaded normally. If the addresses of these resources are all full paths (for example, `https://qiankun.umijs.org/logo.png`), there is no problem. If they are all relative paths, you need to upload these resources to the server first and reference the full path. The only change is that we need to declare a script tag, to export the `lifecycles` example: 1. declare entry script ```diff Purehtml Example
Purehtml Example
+ ``` 2. export lifecycles in the entry ```javascript const render = ($) => { $('#purehtml-container').html('Hello, render with jQuery'); return Promise.resolve(); }; ((global) => { global['purehtml'] = { bootstrap: () => { console.log('purehtml bootstrap'); return Promise.resolve(); }, mount: () => { console.log('purehtml mount'); return render($); }, unmount: () => { console.log('purehtml unmount'); return Promise.resolve(); }, }; })(window); ``` refer to the [purehtml examples](https://github.com/umijs/qiankun/tree/master/examples/purehtml) At the same time, [the subApp must support the CORS](/faq#must-a-sub-app-asset-support-cors) ### umi-qiankun app For the tutorial of `umi-qiankun`, please go to [umi official website](https://umijs.org/zh-CN/plugins/plugin-qiankun) and [umi-qiankun official demo](https://github.com/umijs/umi-plugin-qiankun/tree/master/examples) ``` ================================================ FILE: docs/index.md ================================================ --- layout: home hero: name: Qiankun text: Micro-Frontend Solution tagline: Probably the most complete micro-frontends solution you ever met🧐 image: src: /logo.png alt: Qiankun actions: - theme: brand text: Get Started link: /guide/quick-start - theme: alt text: View on GitHub link: https://github.com/umijs/qiankun features: - icon: 🚀 title: Simple details: Works with any javascript framework. Build your micro-frontend system just like using with iframe, but not iframe actually. - icon: 🛡️ title: Complete details: Includes almost all the basic capabilities required to build a micro-frontend system, such as style isolation, js sandbox, preloading, and so on. - icon: 🔧 title: Production-Ready details: Had been extensively tested and polished by a large number of online applications both inside and outside of Ant Financial, the robustness is trustworthy. - icon: ⚡ title: High Performance details: Supports application preloading to optimize user experience and improve application switching speed. - icon: 🎯 title: Technology Agnostic details: The main application does not limit the technology stack of accessing applications, and micro applications have complete autonomy. - icon: 🔄 title: State Isolation details: Provides a complete JS sandbox mechanism to ensure that applications do not affect each other. --- ## 📦 Installation ::: code-group ```bash [npm] npm install qiankun ``` ```bash [yarn] yarn add qiankun ``` ```bash [pnpm] pnpm add qiankun ``` ::: ## 🔨 Quick Start ### Main Application ```typescript import { registerMicroApps, start } from 'qiankun'; // register micro apps registerMicroApps([ { name: 'reactApp', entry: '//localhost:7100', container: '#yourContainer', activeRule: '/yourActiveRule', }, { name: 'vueApp', entry: { scripts: ['//localhost:7100/main.js'] }, container: '#yourContainer2', activeRule: '/yourActiveRule2', }, ]); // start qiankun start(); ``` ### Micro Application ```typescript /** * bootstrap will only be called once when the micro application is initialized * mount will be called every time the micro application enters * unmount will be called every time the micro application leaves */ export async function bootstrap() { console.log('react app bootstraped'); } export async function mount(props) { ReactDOM.render(, props.container ? props.container.querySelector('#root') : document.getElementById('root')); } export async function unmount(props) { ReactDOM.unmountComponentAtNode( props.container ? props.container.querySelector('#root') : document.getElementById('root'), ); } ``` ## 🌟 Why qiankun?

🎯 Zero Intrusion

Almost no intrusion to existing applications, only need to expose necessary lifecycle functions

📱 All Scenarios

Supports both route-based and manual loading of micro applications

🔒 Secure Isolation

Complete sandbox solution with JS isolation and CSS isolation

⚡ High Performance

Supports preloading, caching and other performance optimization solutions

## 👥 Community | GitHub Discussions | DingTalk Group | WeChat Group | | --- | --- | --- | | [qiankun discussions](https://github.com/umijs/qiankun/discussions) | DingTalk Group QR Code | [view group QR code](https://github.com/umijs/qiankun/discussions/2343) | ================================================ FILE: docs/zh-CN/api/configuration.md ================================================ # Configuration qiankun 提供灵活的配置选项来自定义微前端应用的行为。本文档涵盖了不同用例的所有可用配置选项。 ## 📋 配置类型 ### AppConfiguration 与 `loadMicroApp` 一起使用的单个微应用配置。 ```typescript type AppConfiguration = { sandbox?: boolean; globalContext?: WindowProxy; fetch?: Function; streamTransformer?: Function; nodeTransformer?: Function; }; ``` ### StartOpts 与 `start()` 一起使用的启动 qiankun 框架的配置。 ```typescript interface StartOpts { prefetch?: boolean | 'all' | string[] | ((apps: RegistrableApp[]) => { criticalAppNames: string[]; minorAppsName: string[] }); sandbox?: boolean | { strictStyleIsolation?: boolean; experimentalStyleIsolation?: boolean; }; singular?: boolean; urlRerouteOnly?: boolean; // ... other single-spa options } ``` ## ⚙️ 应用配置选项 ### sandbox **类型**: `boolean` **默认值**: `true` **描述**: 为微应用启用沙箱隔离。 #### 基础用法 ```typescript // Enable sandbox (default) loadMicroApp({ name: 'my-app', entry: '//localhost:8080', container: '#container', }, { sandbox: true }); // Disable sandbox (not recommended) loadMicroApp({ name: 'legacy-app', entry: '//localhost:8080', container: '#container', }, { sandbox: false }); ``` #### 为什么使用沙箱? ```typescript // With sandbox enabled, global variables are isolated loadMicroApp({ name: 'app1', entry: '//localhost:8001', container: '#container1', }, { sandbox: true // app1 gets its own global scope }); loadMicroApp({ name: 'app2', entry: '//localhost:8002', container: '#container2', }, { sandbox: true // app2 gets its own isolated global scope }); ``` ### globalContext **类型**: `WindowProxy` **默认值**: `window` **描述**: 微应用的自定义全局上下文。 ```typescript // Create a custom global context const customGlobal = new Proxy(window, { get(target, prop) { // Custom logic for property access if (prop === 'customAPI') { return { version: '1.0' }; } return target[prop]; } }); loadMicroApp({ name: 'custom-app', entry: '//localhost:8080', container: '#container', }, { globalContext: customGlobal }); ``` ### fetch **类型**: `Function` **默认值**: `window.fetch` **描述**: 用于加载应用资源的自定义 fetch 函数。 #### 自定义头部 ```typescript const customFetch = async (url, options) => { return fetch(url, { ...options, headers: { ...options?.headers, 'Authorization': `Bearer ${getToken()}`, 'X-Custom-Header': 'custom-value' } }); }; loadMicroApp({ name: 'authenticated-app', entry: '//localhost:8080', container: '#container', }, { fetch: customFetch }); ``` #### 请求转换 ```typescript const transformFetch = async (url, options) => { // Transform URLs const transformedUrl = url.replace('//localhost', '//production-domain'); // Add custom logic console.log(`Fetching: ${transformedUrl}`); const response = await fetch(transformedUrl, options); // Transform response if (!response.ok) { throw new Error(`Failed to fetch ${transformedUrl}: ${response.status}`); } return response; }; ``` #### 缓存策略 ```typescript const cache = new Map(); const cachingFetch = async (url, options) => { const cacheKey = `${url}${JSON.stringify(options)}`; if (cache.has(cacheKey)) { console.log(`Cache hit for ${url}`); return cache.get(cacheKey); } const response = await fetch(url, options); cache.set(cacheKey, response.clone()); return response; }; ``` ### streamTransformer **类型**: `Function` **描述**: 在加载过程中转换流式 HTML 内容。 ```typescript const customStreamTransformer = (stream) => { return stream.pipeThrough(new TransformStream({ transform(chunk, controller) { // Transform HTML chunks const transformedChunk = chunk .replace(/old-api/g, 'new-api') .replace(/deprecated-feature/g, 'updated-feature'); controller.enqueue(transformedChunk); } })); }; loadMicroApp({ name: 'streaming-app', entry: '//localhost:8080', container: '#container', }, { streamTransformer: customStreamTransformer }); ``` ### nodeTransformer **类型**: `Function` **描述**: 在应用加载过程中转换 DOM 节点。 ```typescript const customNodeTransformer = (node, options) => { // Transform script tags if (node.tagName === 'SCRIPT') { // Add custom attributes node.setAttribute('data-app', 'my-app'); // Modify script source if (node.src) { node.src = node.src.replace('localhost', 'production-domain'); } } // Transform style tags if (node.tagName === 'STYLE') { // Add CSS scope node.textContent = `.app-scope { ${node.textContent} }`; } return node; }; loadMicroApp({ name: 'transformed-app', entry: '//localhost:8080', container: '#container', }, { nodeTransformer: customNodeTransformer }); ``` ## 🚀 启动配置选项 ### prefetch **类型**: `boolean | 'all' | string[] | Function` **默认值**: `true` **描述**: 用于提升性能的资源预取策略。 #### 布尔值 ```typescript // Disable prefetch start({ prefetch: false }); // Enable default prefetch start({ prefetch: true }); ``` #### 预取所有 ```typescript // Prefetch all registered micro apps start({ prefetch: 'all' }); ``` #### 选择性预取 ```typescript // Prefetch specific apps start({ prefetch: ['dashboard', 'user-profile', 'analytics'] }); ``` #### 动态预取策略 ```typescript start({ prefetch: (apps) => { // Business logic to determine prefetch strategy const currentTime = new Date().getHours(); const isBusinessHours = currentTime >= 9 && currentTime <= 17; if (isBusinessHours) { // Prefetch business-critical apps during business hours return { criticalAppNames: ['dashboard', 'crm', 'finance'], minorAppsName: ['reporting', 'settings'] }; } else { // Minimal prefetch during off-hours return { criticalAppNames: ['dashboard'], minorAppsName: [] }; } } }); ``` #### 基于用户的预取 ```typescript start({ prefetch: (apps) => { const userRole = getCurrentUserRole(); switch (userRole) { case 'admin': return { criticalAppNames: ['admin-panel', 'user-management', 'system-monitor'], minorAppsName: ['reports', 'settings'] }; case 'user': return { criticalAppNames: ['dashboard', 'profile'], minorAppsName: ['help', 'feedback'] }; default: return { criticalAppNames: ['dashboard'], minorAppsName: [] }; } } }); ``` ### sandbox **类型**: `boolean | SandboxConfig` **默认值**: `true` **描述**: 所有微应用的全局沙箱配置。 #### 基础沙箱 ```typescript // Enable sandbox for all apps start({ sandbox: true }); // Disable sandbox for all apps (not recommended) start({ sandbox: false }); ``` #### 高级沙箱配置 ```typescript start({ sandbox: { strictStyleIsolation: true, // Enable Shadow DOM style isolation experimentalStyleIsolation: true, // Enable scoped CSS style isolation } }); ``` #### 样式隔离选项 **strictStyleIsolation**: 使用 Shadow DOM 来完全隔离样式 ```typescript start({ sandbox: { strictStyleIsolation: true, // Strongest isolation but may break some UI libraries } }); ``` **experimentalStyleIsolation**: 使用作用域 CSS 来隔离样式 ```typescript start({ sandbox: { experimentalStyleIsolation: true, // Good balance of isolation and compatibility } }); ``` #### 组合样式隔离 ```typescript start({ sandbox: { strictStyleIsolation: false, // Disable Shadow DOM experimentalStyleIsolation: true, // Enable scoped CSS } }); ``` ### singular **类型**: `boolean` **默认值**: `true` **描述**: 是否同时只能挂载一个微应用。 ```typescript // Only one app at a time (default) start({ singular: true }); // Allow multiple apps simultaneously start({ singular: false // Useful for dashboard-style applications }); ``` #### 多应用用例 ```typescript // Dashboard with multiple widgets start({ singular: false, // Other configurations }); // Register widget-style micro apps registerMicroApps([ { name: 'widget-weather', entry: '//localhost:8001', container: '#widget-1', activeRule: '/dashboard' }, { name: 'widget-stocks', entry: '//localhost:8002', container: '#widget-2', activeRule: '/dashboard' }, { name: 'widget-news', entry: '//localhost:8003', container: '#widget-3', activeRule: '/dashboard' }, ]); ``` ### urlRerouteOnly **类型**: `boolean` **默认值**: `true` **描述**: 是否仅在 URL 变化时触发路由。 ```typescript // Only route on URL changes (default) start({ urlRerouteOnly: true }); // Route on both URL and programmatic changes start({ urlRerouteOnly: false // More responsive but potentially more performance overhead }); ``` ## 🔧 基于环境的配置 ### 开发配置 ```typescript const developmentConfig = { prefetch: false, // Faster rebuilds sandbox: { strictStyleIsolation: false, // Easier debugging experimentalStyleIsolation: true, }, singular: false, // More flexible development urlRerouteOnly: false, // More responsive navigation }; if (process.env.NODE_ENV === 'development') { start(developmentConfig); } ``` ### 生产配置 ```typescript const productionConfig = { prefetch: 'all', // Better user experience sandbox: { strictStyleIsolation: true, // Better isolation experimentalStyleIsolation: false, }, singular: true, // Stable performance urlRerouteOnly: true, // Optimized routing }; if (process.env.NODE_ENV === 'production') { start(productionConfig); } ``` ### 移动端配置 ```typescript const mobileConfig = { prefetch: (apps) => ({ // Conservative prefetch on mobile criticalAppNames: ['home'], minorAppsName: [] }), sandbox: { // Lighter sandbox for mobile performance strictStyleIsolation: false, experimentalStyleIsolation: true, }, singular: true, // Better for mobile UX }; const isMobile = window.innerWidth < 768; if (isMobile) { start(mobileConfig); } ``` ## 🎯 高级配置模式 ### 1. 特性标志集成 ```typescript const getConfigWithFeatureFlags = async () => { const featureFlags = await getFeatureFlags(); return { prefetch: featureFlags.enablePrefetch ? 'all' : false, sandbox: { strictStyleIsolation: featureFlags.strictIsolation, experimentalStyleIsolation: !featureFlags.strictIsolation, }, singular: featureFlags.allowMultipleApps ? false : true, }; }; getConfigWithFeatureFlags().then(config => start(config)); ``` ### 2. 基于性能的配置 ```typescript const getPerformanceConfig = () => { const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection; const isSlowConnection = connection?.effectiveType === '2g' || connection?.effectiveType === 'slow-2g'; if (isSlowConnection) { return { prefetch: false, // No prefetch on slow connections sandbox: { strictStyleIsolation: false, experimentalStyleIsolation: true, }, singular: true, }; } return { prefetch: 'all', sandbox: { strictStyleIsolation: true, experimentalStyleIsolation: false, }, singular: false, }; }; start(getPerformanceConfig()); ``` ### 3. 基于用户角色的配置 ```typescript const getRoleBasedConfig = (userRole) => { const baseConfig = { sandbox: true, singular: true, }; switch (userRole) { case 'admin': return { ...baseConfig, prefetch: 'all', // Admins get all features singular: false, // Can use multiple admin tools }; case 'poweruser': return { ...baseConfig, prefetch: ['dashboard', 'analytics', 'reports'], singular: false, }; default: return { ...baseConfig, prefetch: ['dashboard'], // Basic users get minimal prefetch singular: true, }; } }; const userRole = getCurrentUserRole(); start(getRoleBasedConfig(userRole)); ``` ## ⚠️ 重要注意事项 ### 1. 配置优先级 ```typescript // App-level configuration overrides global configuration start({ sandbox: true, // Global setting }); loadMicroApp({ name: 'special-app', entry: '//localhost:8080', container: '#container', }, { sandbox: false // This overrides the global setting for this app }); ``` ### 2. 性能考虑 ```typescript // ❌ 错误:影响性能的重配置 start({ prefetch: 'all', // Might slow down initial load sandbox: { strictStyleIsolation: true, // More overhead }, singular: false, // More memory usage urlRerouteOnly: false, // More frequent route checks }); // ✅ 正确:平衡的配置 start({ prefetch: ['critical-app'], // Only prefetch what's needed sandbox: { experimentalStyleIsolation: true, // Good balance }, singular: true, // Stable performance urlRerouteOnly: true, // Optimized routing }); ``` ### 3. 调试配置 ```typescript const debugConfig = { sandbox: { strictStyleIsolation: false, // Easier to inspect styles experimentalStyleIsolation: true, }, // Custom fetch for logging fetch: async (url, options) => { console.log(`[DEBUG] Fetching: ${url}`); const response = await fetch(url, options); console.log(`[DEBUG] Response: ${response.status}`); return response; }, // Custom node transformer for debugging nodeTransformer: (node, options) => { if (node.tagName === 'SCRIPT') { console.log(`[DEBUG] Processing script: ${node.src || 'inline'}`); } return node; } }; ``` ## 🔗 相关 API - [start](/zh-CN/api/start) - 使用配置启动 qiankun - [loadMicroApp](/zh-CN/api/load-micro-app) - 使用配置加载应用 - [registerMicroApps](/zh-CN/api/register-micro-apps) - 注册应用 ================================================ FILE: docs/zh-CN/api/index.md ================================================ # API 参考 qiankun 提供了简洁而强大的 API 来构建微前端应用。所有 API 都提供了完整的 TypeScript 类型定义,确保开发体验和类型安全。 ## 📚 核心 API ### 应用注册与启动 | API | 描述 | 类型 | |-----|------|------| | [`registerMicroApps`](/zh-CN/api/register-micro-apps) | 注册微应用 | `(apps: RegistrableApp[], lifeCycles?: LifeCycles) => void` | | [`start`](/zh-CN/api/start) | 启动 qiankun 框架 | `(opts?: StartOpts) => void` | | [`loadMicroApp`](/zh-CN/api/load-micro-app) | 手动加载微应用 | `(app: LoadableApp, configuration?: AppConfiguration, lifeCycles?: LifeCycles) => MicroApp` | ### 工具 API | API | 描述 | 类型 | |-----|------|------| | [`isRuntimeCompatible`](/zh-CN/api/is-runtime-compatible) | 检查运行时兼容性 | `() => boolean` | ## 🎯 快速导航 ### 按使用场景 **路由模式** ```typescript import { registerMicroApps, start } from 'qiankun'; // 1. 注册微应用 registerMicroApps([...]); // 2. 启动框架 start(); ``` **手动加载模式** ```typescript import { loadMicroApp } from 'qiankun'; // 手动加载微应用 const microApp = loadMicroApp({...}); ``` **兼容性检查** ```typescript import { isRuntimeCompatible } from 'qiankun'; if (isRuntimeCompatible()) { // 启动微前端应用 } ``` ### 按功能分类 | 分类 | 相关 API | 描述 | |------|----------|------| | **应用管理** | `registerMicroApps`, `loadMicroApp` | 注册和加载微应用 | | **框架控制** | `start` | 框架启动和配置 | | **工具函数** | `isRuntimeCompatible` | 辅助工具方法 | ## 🔧 类型定义 qiankun 提供了完整的 TypeScript 类型定义: ```typescript import type { RegistrableApp, LoadableApp, MicroApp, LifeCycles, AppConfiguration, } from 'qiankun'; ``` 详细信息请参考 [类型定义](/zh-CN/api/types)。 ## 📖 详细文档 ### 核心 API - [registerMicroApps](/zh-CN/api/register-micro-apps) - 注册微应用 - [start](/zh-CN/api/start) - 启动 qiankun 框架 - [loadMicroApp](/zh-CN/api/load-micro-app) - 手动加载微应用 - [isRuntimeCompatible](/zh-CN/api/is-runtime-compatible) - 运行时兼容性检查 ### 参考文档 - [生命周期](/zh-CN/api/lifecycles) - 应用生命周期钩子 - [配置选项](/zh-CN/api/configuration) - 框架配置选项 - [类型定义](/zh-CN/api/types) - TypeScript 类型定义 ## 💡 使用建议 ### 推荐的 API 使用模式 1. **标准路由模式**(推荐) ```typescript registerMicroApps([...]) → start() ``` 2. **动态加载模式** ```typescript loadMicroApp({...}) ``` 3. **混合模式** ```typescript registerMicroApps([...]) → start() + loadMicroApp({...}) ``` ### 最佳实践 - ✅ 使用 TypeScript 获得完整的类型支持 - ✅ 在启动框架前注册所有微应用 - ✅ 适当使用生命周期钩子进行状态管理 - ✅ 配置适当的错误处理 - ❌ 避免注册重复的应用名称 - ❌ 避免在微应用中调用主应用 API - ❌ 避免在生命周期钩子中执行耗时操作 ================================================ FILE: docs/zh-CN/api/is-runtime-compatible.md ================================================ # isRuntimeCompatible 检查当前浏览器环境是否与 qiankun 运行时特性兼容。 ## 🎯 函数签名 ```typescript function isRuntimeCompatible(): boolean ``` ## 📋 参数 此函数不接受任何参数。 ## 🔄 返回值 - **类型**: `boolean` - **描述**: 如果当前环境支持 qiankun 特性则返回 `true`,否则返回 `false`。 ## 💡 使用示例 ### 基础兼容性检查 ```typescript import { isRuntimeCompatible, registerMicroApps, start } from 'qiankun'; if (isRuntimeCompatible()) { // Environment supports qiankun registerMicroApps([...]); start(); } else { // Fallback for unsupported browsers console.warn('Current browser does not support qiankun'); initFallbackRouting(); } ``` ### 带优雅降级 ```typescript function initApplication() { if (isRuntimeCompatible()) { // Use qiankun micro-frontend architecture initMicroFrontend(); } else { // Fall back to traditional SPA initTraditionalSPA(); } } function initMicroFrontend() { registerMicroApps([ { name: 'module-a', entry: '//localhost:8001', container: '#container', activeRule: '/module-a', } ]); start(); } function initTraditionalSPA() { // Traditional routing setup import('./traditional-router').then(router => { router.init(); }); } ``` ## 🔍 检查内容 `isRuntimeCompatible` 函数检查以下浏览器特性: ### 必需特性 1. **Proxy 支持**:用于 JavaScript 沙箱隔离 2. **Window.Proxy**:创建隔离执行上下文的必要条件 3. **Import Maps**(使用时):用于动态模块加载 4. **Dynamic Import**:用于加载微应用 ### 浏览器兼容性 | 浏览器 | 最低版本 | 支持程度 | |---------|---------|---------| | Chrome | 61+ | ✅ 完全支持 | | Firefox | 60+ | ✅ 完全支持 | | Safari | 11+ | ✅ 完全支持 | | Edge | 79+ | ✅ 完全支持 | | IE | 任何版本 | ❌ 不支持 | ## 🚀 最佳实践 ### 1. 早期检测 ```typescript // Check compatibility before any qiankun setup function bootstrap() { if (!isRuntimeCompatible()) { showUnsupportedBrowserMessage(); return; } // Safe to proceed with qiankun setupMicroFrontend(); } ``` ### 2. 渐进增强 ```typescript class ApplicationBootstrap { private isQiankunSupported = isRuntimeCompatible(); init() { if (this.isQiankunSupported) { this.initWithMicroFrontend(); } else { this.initWithoutMicroFrontend(); } } private initWithMicroFrontend() { // Full micro-frontend experience registerMicroApps([...]); start(); } private initWithoutMicroFrontend() { // Simplified experience for unsupported browsers this.loadAllModulesDirectly(); } } ``` ### 3. 用户沟通 ```typescript if (!isRuntimeCompatible()) { // Show user-friendly message const banner = document.createElement('div'); banner.innerHTML = `
Browser Compatibility Notice: For the best experience, please use a modern browser like Chrome, Firefox, or Safari. Some features may be limited in your current browser.
`; document.body.insertBefore(banner, document.body.firstChild); } ``` ## 🔧 集成模式 ### 1. 带特性标志 ```typescript const featureFlags = { useMicroFrontend: isRuntimeCompatible() && process.env.ENABLE_MICRO_FRONTEND, useAdvancedFeatures: isRuntimeCompatible(), }; if (featureFlags.useMicroFrontend) { // Full micro-frontend setup registerMicroApps([...]); start(); } else { // Traditional setup initTraditionalApp(); } ``` ### 2. 带分析统计 ```typescript // Track browser compatibility for analytics const compatible = isRuntimeCompatible(); // Send analytics event analytics.track('browser_compatibility_check', { compatible, userAgent: navigator.userAgent, timestamp: Date.now(), }); if (compatible) { initQiankunApp(); } else { initFallbackApp(); } ``` ### 3. 带动态加载 ```typescript async function loadApplicationFramework() { if (isRuntimeCompatible()) { // Load qiankun and micro-frontend modules const [qiankun, microApps] = await Promise.all([ import('qiankun'), import('./micro-apps-config'), ]); qiankun.registerMicroApps(microApps.default); qiankun.start(); } else { // Load traditional SPA modules const traditionalApp = await import('./traditional-app'); traditionalApp.init(); } } ``` ## ⚠️ 重要注意事项 ### 1. 性能考虑 ```typescript // ✅ 正确:检查一次并缓存结果 const QIANKUN_COMPATIBLE = isRuntimeCompatible(); function someFunction() { if (QIANKUN_COMPATIBLE) { // Use cached result } } // ❌ 错误:多次检查 function someFunction() { if (isRuntimeCompatible()) { // Redundant check // ... } } ``` ### 2. SSR 考虑 ```typescript // In SSR environments, check if window is available function safeCompatibilityCheck() { if (typeof window === 'undefined') { // SSR environment - assume compatible return true; } return isRuntimeCompatible(); } ``` ### 3. 测试环境 ```typescript // For testing, you might want to mock the compatibility if (process.env.NODE_ENV === 'test') { // Mock for testing global.mockQiankunCompatible = true; } function checkCompatibility() { if (process.env.NODE_ENV === 'test' && global.mockQiankunCompatible !== undefined) { return global.mockQiankunCompatible; } return isRuntimeCompatible(); } ``` ## 🎯 常见场景 ### 1. 企业环境 ```typescript // Corporate environments might have older browsers function initCorporateApp() { const compatible = isRuntimeCompatible(); if (!compatible) { // Inform IT department about browser requirements logToAdminConsole('User browser incompatible with micro-frontend features'); } return compatible ? initMicroFrontend() : initLegacyApp(); } ``` ### 2. 公共网站 ```typescript // Public websites need to support a wider range of browsers function initPublicSite() { if (isRuntimeCompatible()) { // Enhanced experience with micro-frontends loadAdvancedFeatures(); } else { // Basic experience that works everywhere loadBasicFeatures(); } } ``` ### 3. 移动应用 WebView ```typescript // Mobile WebViews might have different compatibility function initMobileWebView() { const compatible = isRuntimeCompatible(); // Log for mobile app developers if (window.ReactNativeWebView) { window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'qiankun_compatibility', compatible, })); } return compatible ? initMicroFrontend() : initSimplifiedView(); } ``` ## 🔗 相关 API - [start](/zh-CN/api/start) - 启动 qiankun(应在兼容性检查后调用) - [registerMicroApps](/zh-CN/api/register-micro-apps) - 注册微应用 - [loadMicroApp](/zh-CN/api/load-micro-app) - 手动加载微应用 ================================================ FILE: docs/zh-CN/api/lifecycles.md ================================================ # Lifecycles 生命周期钩子允许您在微应用生命周期的不同阶段执行自定义逻辑。这些钩子在应用加载、挂载和卸载过程中由 qiankun 自动执行。 ## 🎯 类型定义 ```typescript export type LifeCycleFn = ( app: LoadableApp, global: WindowProxy ) => Promise; export type LifeCycles = { beforeLoad?: LifeCycleFn | Array>; beforeMount?: LifeCycleFn | Array>; afterMount?: LifeCycleFn | Array>; beforeUnmount?: LifeCycleFn | Array>; afterUnmount?: LifeCycleFn | Array>; }; ``` ## 📋 可用的生命周期钩子 ### beforeLoad **时机**: 在微应用开始加载之前调用。 **目的**: 在获取和解析应用代码之前执行设置任务。 ```typescript beforeLoad: async (app, global) => { console.log(`About to load ${app.name}`); // Setup global configurations global.__INITIAL_CONFIG__ = getInitialConfig(); } ``` ### beforeMount **时机**: 在应用加载完成后但在挂载到 DOM 之前调用。 **目的**: 在应用激活之前执行最终设置。 ```typescript beforeMount: async (app, global) => { console.log(`About to mount ${app.name}`); // Initialize services await initializeServices(); // Set loading state setLoadingState(false); } ``` ### afterMount **时机**: 在微应用成功挂载后调用。 **目的**: 执行挂载后操作,如分析、功能初始化等。 ```typescript afterMount: async (app, global) => { console.log(`${app.name} mounted successfully`); // Track analytics analytics.track('micro_app_mounted', { appName: app.name }); // Initialize features that depend on DOM initializeDOMDependentFeatures(); } ``` ### beforeUnmount **时机**: 在微应用开始卸载之前调用。 **目的**: 在应用被移除之前执行清理操作。 ```typescript beforeUnmount: async (app, global) => { console.log(`About to unmount ${app.name}`); // Save application state saveApplicationState(app.name); // Cleanup event listeners cleanupEventListeners(); } ``` ### afterUnmount **时机**: 在微应用完全卸载后调用。 **目的**: 最终清理和资源释放。 ```typescript afterUnmount: async (app, global) => { console.log(`${app.name} unmounted`); // Clear caches clearApplicationCache(app.name); // Reset global state resetGlobalState(); } ``` ## 🔄 生命周期流程 ```mermaid graph TD A[Start Loading] --> B[beforeLoad] B --> C[Load Application Code] C --> D[beforeMount] D --> E[Mount Application] E --> F[afterMount] F --> G[Application Running] G --> H[beforeUnmount] H --> I[Unmount Application] I --> J[afterUnmount] J --> K[Application Cleaned Up] ``` ## 💡 使用示例 ### 与 registerMicroApps 一起使用 ```typescript import { registerMicroApps, start } from 'qiankun'; registerMicroApps([ { name: 'react-app', entry: '//localhost:7100', container: '#subapp-viewport', activeRule: '/react', } ], { beforeLoad: async (app) => { console.log('Loading app:', app.name); }, afterMount: async (app) => { console.log('App mounted:', app.name); }, beforeUnmount: async (app) => { console.log('Unmounting app:', app.name); } }); start(); ``` ### 与 loadMicroApp 一起使用 ```typescript import { loadMicroApp } from 'qiankun'; const microApp = loadMicroApp({ name: 'dashboard', entry: '//localhost:8080', container: '#dashboard-container', }, undefined, { beforeLoad: async (app, global) => { // Setup dashboard-specific configurations global.DASHBOARD_CONFIG = getDashboardConfig(); }, afterMount: async (app) => { // Initialize dashboard widgets initializeDashboardWidgets(); } }); ``` ### 多个钩子 ```typescript // You can provide multiple hooks as an array const lifecycles = { beforeMount: [ async (app) => { await setupDatabase(); }, async (app) => { await setupAnalytics(); }, async (app) => { await setupFeatureFlags(); } ], afterMount: [ async (app) => { trackPageView(app.name); }, async (app) => { initializeUserTracking(); } ] }; ``` ## 🔧 高级模式 ### 1. 状态管理集成 ```typescript import { store } from './store'; const lifecycles = { beforeLoad: async (app) => { // Set loading state store.dispatch({ type: 'SET_APP_LOADING', payload: { appName: app.name, loading: true } }); }, afterMount: async (app) => { // Update mounted apps list store.dispatch({ type: 'ADD_MOUNTED_APP', payload: app.name }); store.dispatch({ type: 'SET_APP_LOADING', payload: { appName: app.name, loading: false } }); }, beforeUnmount: async (app) => { // Save app state before unmounting const appState = getAppState(app.name); store.dispatch({ type: 'SAVE_APP_STATE', payload: { appName: app.name, state: appState } }); }, afterUnmount: async (app) => { // Remove from mounted apps list store.dispatch({ type: 'REMOVE_MOUNTED_APP', payload: app.name }); } }; ``` ### 2. 错误处理 ```typescript const lifecycles = { beforeLoad: async (app) => { try { await performPreLoadChecks(app); } catch (error) { console.error(`Pre-load checks failed for ${app.name}:`, error); // Optionally prevent loading by throwing throw new Error(`Failed to initialize ${app.name}`); } }, afterMount: async (app) => { try { await performPostMountTasks(app); } catch (error) { console.error(`Post-mount tasks failed for ${app.name}:`, error); // Log error but don't prevent the app from running reportError(error, { context: 'afterMount', appName: app.name }); } } }; ``` ### 3. 性能监控 ```typescript const performanceTracker = new Map(); const lifecycles = { beforeLoad: async (app) => { performanceTracker.set(app.name, { loadStart: performance.now() }); }, beforeMount: async (app) => { const timing = performanceTracker.get(app.name); timing.loadEnd = performance.now(); timing.mountStart = performance.now(); }, afterMount: async (app) => { const timing = performanceTracker.get(app.name); timing.mountEnd = performance.now(); // Calculate and report metrics const loadTime = timing.loadEnd - timing.loadStart; const mountTime = timing.mountEnd - timing.mountStart; analytics.track('micro_app_performance', { appName: app.name, loadTime, mountTime, totalTime: loadTime + mountTime }); } }; ``` ### 4. 资源管理 ```typescript const resourceMap = new Map(); const lifecycles = { beforeMount: async (app) => { // Allocate resources const resources = await allocateResources(app.name); resourceMap.set(app.name, resources); }, beforeUnmount: async (app) => { // Save critical data const resources = resourceMap.get(app.name); if (resources) { await saveCriticalData(app.name, resources); } }, afterUnmount: async (app) => { // Release resources const resources = resourceMap.get(app.name); if (resources) { await releaseResources(resources); resourceMap.delete(app.name); } } }; ``` ## 🎯 常见用例 ### 1. 加载状态 ```typescript const loadingManager = { show: (appName) => { const loader = document.createElement('div'); loader.id = `loader-${appName}`; loader.innerHTML = '
Loading...
'; document.body.appendChild(loader); }, hide: (appName) => { const loader = document.getElementById(`loader-${appName}`); if (loader) loader.remove(); } }; const lifecycles = { beforeLoad: async (app) => { loadingManager.show(app.name); }, afterMount: async (app) => { loadingManager.hide(app.name); } }; ``` ### 2. 身份验证检查 ```typescript const lifecycles = { beforeLoad: async (app) => { const isAuthenticated = await checkAuthentication(); if (!isAuthenticated) { throw new Error('User not authenticated'); } }, beforeMount: async (app, global) => { // Inject user context const userContext = await getUserContext(); global.__USER_CONTEXT__ = userContext; } }; ``` ### 3. 主题同步 ```typescript const lifecycles = { beforeMount: async (app, global) => { // Sync theme with micro app const currentTheme = getCurrentTheme(); global.__THEME__ = currentTheme; // Apply theme-specific styles applyThemeStyles(currentTheme); }, afterUnmount: async (app) => { // Clean up theme styles removeThemeStyles(app.name); } }; ``` ### 4. 特性标志管理 ```typescript const lifecycles = { beforeLoad: async (app, global) => { // Load feature flags for the specific app const featureFlags = await getFeatureFlags(app.name); global.__FEATURE_FLAGS__ = featureFlags; }, afterMount: async (app) => { // Track which features are enabled trackEnabledFeatures(app.name); } }; ``` ## ⚠️ 重要注意事项 ### 1. 钩子执行顺序 ```typescript // Hooks are executed in this order: // 1. beforeLoad (before app code is loaded) // 2. beforeMount (after load, before DOM mount) // 3. afterMount (after DOM mount) // ... app is running ... // 4. beforeUnmount (before DOM unmount) // 5. afterUnmount (after DOM unmount) ``` ### 2. 错误处理 ```typescript // ❌ 错误:未处理的错误可能破坏生命周期 beforeLoad: async (app) => { riskyOperation(); // This could throw } // ✅ 正确:始终处理潜在错误 beforeLoad: async (app) => { try { await riskyOperation(); } catch (error) { console.error('Error in beforeLoad:', error); // Decide whether to throw or handle gracefully } } ``` ### 3. 异步操作 ```typescript // ✅ 正确:所有生命周期钩子都是异步的 beforeMount: async (app) => { await setupDatabase(); await loadUserPreferences(); } // ❌ 错误:不要忘记异步操作的 await beforeMount: async (app) => { setupDatabase(); // Missing await! loadUserPreferences(); // Missing await! } ``` ### 4. 全局上下文 ```typescript // ✅ 正确:使用提供的全局上下文 beforeMount: async (app, global) => { global.MY_CONFIG = getConfig(); // Set on the isolated global } // ❌ 错误:不要直接使用 window beforeMount: async (app, global) => { window.MY_CONFIG = getConfig(); // Might affect other apps } ``` ## 🚀 最佳实践 ### 1. 保持钩子轻量 ```typescript // ✅ 正确:快速操作 beforeMount: async (app) => { setAppTheme(app.name); updateNavigationState(); } // ❌ 错误:重操作 beforeMount: async (app) => { await downloadLargeDataset(); // This will block mounting await processHeavyCalculations(); } ``` ### 2. 使用钩子数组进行组织 ```typescript const lifecycles = { beforeMount: [ setupAuthentication, setupTheme, setupAnalytics, setupFeatureFlags ], afterMount: [ trackPageView, initializeWidgets, preloadCriticalData ] }; ``` ### 3. 一致的错误日志记录 ```typescript const createSafeHook = (hookName, hookFn) => async (app, global) => { try { await hookFn(app, global); } catch (error) { console.error(`Error in ${hookName} for ${app.name}:`, error); // Report to error tracking service errorTracker.report(error, { hook: hookName, app: app.name }); } }; const lifecycles = { beforeLoad: createSafeHook('beforeLoad', async (app) => { // Your beforeLoad logic }), afterMount: createSafeHook('afterMount', async (app) => { // Your afterMount logic }) }; ``` ### 4. 资源清理 ```typescript // Track resources in a way that survives app reloads const globalResourceMap = window.__QIANKUN_RESOURCES__ || new Map(); window.__QIANKUN_RESOURCES__ = globalResourceMap; const lifecycles = { beforeMount: async (app) => { const resources = await allocateResources(); globalResourceMap.set(app.name, resources); }, afterUnmount: async (app) => { const resources = globalResourceMap.get(app.name); if (resources) { await cleanupResources(resources); globalResourceMap.delete(app.name); } } }; ``` ## 🔗 相关 API - [registerMicroApps](/zh-CN/api/register-micro-apps) - 与已注册应用一起使用生命周期 - [loadMicroApp](/zh-CN/api/load-micro-app) - 与手动加载的应用一起使用生命周期 - [start](/zh-CN/api/start) - 框架启动配置 ================================================ FILE: docs/zh-CN/api/load-micro-app.md ================================================ # loadMicroApp 手动加载微应用。这对于动态加载微应用或当微应用不与路由关联时很有用。 ## 🎯 函数签名 ```typescript function loadMicroApp( app: LoadableApp, configuration?: AppConfiguration, lifeCycles?: LifeCycles ): MicroApp ``` ## 📋 参数 ### app - **类型**: `LoadableApp` - **必填**: ✅ - **描述**: 微应用配置 #### LoadableApp 结构 ```typescript interface LoadableApp { name: string; // Micro app name, globally unique entry: string | EntryOpts; // Micro app entry container: string | HTMLElement; // Container for the micro app props?: T; // Custom data passed to micro app } ``` | 属性 | 类型 | 必填 | 描述 | |------|------|------|------| | `name` | `string` | ✅ | 微应用名称,作为唯一标识符 | | `entry` | `string \| EntryOpts` | ✅ | 微应用入口,可以是 URL 或资源配置 | | `container` | `string \| HTMLElement` | ✅ | 容器节点选择器或 DOM 元素 | | `props` | `T` | ❌ | 传递给微应用的自定义数据 | ### configuration - **类型**: `AppConfiguration` - **必填**: ❌ - **描述**: 高级配置选项 ```typescript interface AppConfiguration { sandbox?: boolean; // Enable sandbox isolation globalContext?: WindowProxy; // Global context for the micro app fetch?: Function; // Custom fetch function streamTransformer?: Function; // Stream transformer nodeTransformer?: Function; // Node transformer } ``` ### lifeCycles - **类型**: `LifeCycles` - **必填**: ❌ - **描述**: 此特定微应用的生命周期钩子 ## 🔄 返回值 返回一个具有以下方法的 `MicroApp` 实例: ```typescript interface MicroApp { mount(): Promise; // Mount the micro app unmount(): Promise; // Unmount the micro app update(props: any): Promise; // Update micro app props getStatus(): string; // Get current status loadPromise: Promise; // Loading promise mountPromise: Promise; // Mounting promise unmountPromise: Promise; // Unmounting promise } ``` ## 💡 使用示例 ### 基础用法 ```typescript import { loadMicroApp } from 'qiankun'; const microApp = loadMicroApp({ name: 'manual-app', entry: '//localhost:8080', container: '#manual-container', }); // The micro app will be automatically mounted ``` ### 带自定义 Props ```typescript const microApp = loadMicroApp({ name: 'dashboard', entry: '//localhost:8080', container: '#dashboard-container', props: { token: localStorage.getItem('token'), userId: getCurrentUserId(), theme: 'dark' } }); ``` ### 带配置 ```typescript const microApp = loadMicroApp({ name: 'third-party-app', entry: '//external.example.com', container: '#external-container', }, { sandbox: false, // Disable sandbox for legacy apps fetch: customFetch, // Use custom fetch }); ``` ### 带生命周期钩子 ```typescript const microApp = loadMicroApp({ name: 'monitored-app', entry: '//localhost:8080', container: '#monitored-container', }, undefined, { beforeMount: (app) => { console.log('About to mount:', app.name); showLoadingSpinner(); }, afterMount: (app) => { console.log('Mounted successfully:', app.name); hideLoadingSpinner(); }, beforeUnmount: (app) => { console.log('About to unmount:', app.name); saveUserState(); } }); ``` ## 🔧 高级用法 ### 条件动态加载 ```typescript async function loadAppConditionally(condition: boolean) { if (condition) { const microApp = loadMicroApp({ name: 'conditional-app', entry: '//localhost:8080', container: '#conditional-container', }); return microApp; } return null; } ``` ### 加载多个应用 ```typescript function loadMultipleApps() { const apps = [ { name: 'app1', entry: '//localhost:8001', container: '#container1' }, { name: 'app2', entry: '//localhost:8002', container: '#container2' }, { name: 'app3', entry: '//localhost:8003', container: '#container3' }, ]; const microApps = apps.map(app => loadMicroApp(app)); return microApps; } ``` ### 手动控制 ```typescript const microApp = loadMicroApp({ name: 'controlled-app', entry: '//localhost:8080', container: '#controlled-container', }); // Manual unmount await microApp.unmount(); // Update props await microApp.update({ newData: 'updated' }); // Check status console.log(microApp.getStatus()); // 'MOUNTED', 'UNMOUNTED', etc. ``` ## 🎭 用例场景 ### 1. 模态框/对话框应用 ```typescript function openAppModal() { const modal = document.createElement('div'); modal.id = 'app-modal'; document.body.appendChild(modal); const microApp = loadMicroApp({ name: 'modal-app', entry: '//localhost:8080', container: modal, props: { onClose: () => { microApp.unmount().then(() => { document.body.removeChild(modal); }); } } }); return microApp; } ``` ### 2. 基于标签页的应用 ```typescript class TabManager { private activeTabs = new Map(); async switchTab(tabName: string, config: LoadableApp) { // Unmount current active tab const currentApp = this.activeTabs.get('active'); if (currentApp) { await currentApp.unmount(); } // Load new tab const newApp = loadMicroApp({ ...config, container: '#tab-content' }); this.activeTabs.set('active', newApp); this.activeTabs.set(tabName, newApp); } } ``` ### 3. 组件系统 ```typescript class WidgetSystem { loadWidget(widgetConfig: any) { return loadMicroApp({ name: `widget-${widgetConfig.id}`, entry: widgetConfig.url, container: `#widget-${widgetConfig.id}`, props: widgetConfig.props }, { sandbox: true // Isolate widgets }); } } ``` ## ⚠️ 重要注意事项 ### 容器管理 ```typescript // ❌ 错误:在没有适当清理的情况下重用容器 loadMicroApp({ name: 'app1', entry: '//localhost:8001', container: '#shared' }); loadMicroApp({ name: 'app2', entry: '//localhost:8002', container: '#shared' }); // Conflict! // ✅ 正确:使用唯一容器或适当清理 const app1 = loadMicroApp({ name: 'app1', entry: '//localhost:8001', container: '#container1' }); const app2 = loadMicroApp({ name: 'app2', entry: '//localhost:8002', container: '#container2' }); ``` ### 内存管理 ```typescript // ✅ 正确:适当清理 const microApp = loadMicroApp({...}); // When done, always unmount window.addEventListener('beforeunload', () => { microApp.unmount(); }); ``` ### 错误处理 ```typescript try { const microApp = loadMicroApp({ name: 'potentially-failing-app', entry: '//unreliable-server.com', container: '#container', }); // Wait for load await microApp.loadPromise; console.log('App loaded successfully'); } catch (error) { console.error('Failed to load micro app:', error); // Handle error - show fallback UI, retry, etc. } ``` ## 🆚 对比 registerMicroApps | 特性 | `loadMicroApp` | `registerMicroApps` | |------|----------------|---------------------| | **加载方式** | 手动,立即 | 自动,基于路由 | | **用例** | 动态加载、组件、模态框 | 主导航、SPA 路由 | | **生命周期** | 手动控制 | 路由自动控制 | | **性能** | 按需加载 | 可以预加载 | ## 🚀 最佳实践 ### 1. 资源管理 ```typescript class MicroAppManager { private apps = new Map(); async loadApp(config: LoadableApp) { // Check if already loaded if (this.apps.has(config.name)) { return this.apps.get(config.name); } const app = loadMicroApp(config); this.apps.set(config.name, app); // Auto cleanup on unmount app.unmountPromise.then(() => { this.apps.delete(config.name); }); return app; } } ``` ### 2. Props 管理 ```typescript // ✅ 正确:响应式 props function createReactiveMicroApp(baseConfig: LoadableApp) { let currentApp: MicroApp; return { async updateProps(newProps: any) { if (currentApp) { await currentApp.update(newProps); } }, async reload(newConfig: LoadableApp) { if (currentApp) { await currentApp.unmount(); } currentApp = loadMicroApp({ ...baseConfig, ...newConfig }); } }; } ``` ### 3. 错误边界 ```typescript function loadMicroAppWithFallback(config: LoadableApp, fallbackHTML: string) { const microApp = loadMicroApp(config); microApp.loadPromise.catch((error) => { console.error('Micro app failed to load:', error); // Show fallback content const container = typeof config.container === 'string' ? document.querySelector(config.container) : config.container; if (container) { container.innerHTML = fallbackHTML; } }); return microApp; } ``` ## 🔗 相关 API - [registerMicroApps](/zh-CN/api/register-micro-apps) - 基于路由的微应用加载 - [start](/zh-CN/api/start) - 启动 qiankun 框架 - [生命周期](/zh-CN/api/lifecycles) - 详细的生命周期文档 ================================================ FILE: docs/zh-CN/api/register-micro-apps.md ================================================ # registerMicroApps 注册微应用到 qiankun 中,这是构建微前端应用的核心 API。 ## 🎯 函数签名 ```typescript function registerMicroApps( apps: Array>, lifeCycles?: LifeCycles ): void ``` ## 📋 参数 ### apps - **类型**: `Array>` - **必填**: ✅ - **描述**: 微应用注册信息数组 #### RegistrableApp 结构 ```typescript interface RegistrableApp { name: string; // 微应用名称,全局唯一 entry: string | { scripts?: string[], styles?: string[] }; // 微应用入口 container: string | HTMLElement; // 微应用容器节点 activeRule: string | (location: Location) => boolean; // 激活规则 props?: T; // 传递给微应用的数据 loader?: (loading: boolean) => void; // 加载状态回调 } ``` | 属性 | 类型 | 必填 | 描述 | |------|------|------|------| | `name` | `string` | ✅ | 微应用名称,作为微应用的唯一标识 | | `entry` | `string \| EntryOpts` | ✅ | 微应用的入口,可以是 URL 或资源配置对象 | | `container` | `string \| HTMLElement` | ✅ | 微应用的容器节点选择器或 DOM 节点 | | `activeRule` | `string \| Function` | ✅ | 微应用的激活规则 | | `props` | `T` | ❌ | 传递给微应用的自定义数据 | | `loader` | `Function` | ❌ | 微应用加载状态改变时的回调函数 | ### lifeCycles - **类型**: `LifeCycles` - **必填**: ❌ - **描述**: 全局生命周期钩子 ```typescript interface LifeCycles { beforeLoad?: LifeCycleFn | Array>; beforeMount?: LifeCycleFn | Array>; afterMount?: LifeCycleFn | Array>; beforeUnmount?: LifeCycleFn | Array>; afterUnmount?: LifeCycleFn | Array>; } ``` ## 💡 使用示例 ### 基础用法 ```typescript import { registerMicroApps, start } from 'qiankun'; registerMicroApps([ { name: 'react16App', entry: '//localhost:7100', container: '#subapp-viewport', activeRule: '/react16', }, { name: 'vue3App', entry: '//localhost:7101', container: '#subapp-viewport', activeRule: '/vue3', } ]); start(); ``` ### 高级配置 ```typescript registerMicroApps([ { name: 'dashboard', entry: { scripts: [ '//localhost:7100/static/js/main.js' ], styles: [ '//localhost:7100/static/css/main.css' ] }, container: '#dashboard-container', activeRule: (location) => location.pathname.startsWith('/dashboard'), props: { token: 'your-auth-token', userId: 123, theme: 'dark' }, loader: (loading) => { console.log('Dashboard app loading:', loading); // 显示/隐藏 loading 状态 } } ], { beforeLoad: [ app => console.log('Before load:', app.name), app => trackEvent('micro-app-loading', { name: app.name }) ], beforeMount: app => console.log('Before mount:', app.name), afterMount: app => console.log('After mount:', app.name), beforeUnmount: app => console.log('Before unmount:', app.name), afterUnmount: app => console.log('After unmount:', app.name), }); ``` ## ⚙️ Entry 配置详解 ### URL 字符串 最简单的配置方式,qiankun 会通过这个 URL 获取微应用的 HTML: ```typescript { name: 'app1', entry: '//localhost:8080', // ... } ``` ### 资源对象 精确控制微应用的资源加载: ```typescript { name: 'app2', entry: { scripts: [ '//localhost:8080/static/js/chunk.js', '//localhost:8080/static/js/main.js' ], styles: [ '//localhost:8080/static/css/main.css' ] }, // ... } ``` ## 🎯 ActiveRule 配置 ### 字符串路径 ```typescript { activeRule: '/react16' // 匹配 /react16/xxx 路径 } ``` ### 函数判断 ```typescript { activeRule: (location) => { // 自定义激活逻辑 return location.pathname.startsWith('/admin') && location.search.includes('module=dashboard'); } } ``` ### 常见模式 ```typescript // 1. 精确匹配 activeRule: (location) => location.pathname === '/exact-path' // 2. 多路径匹配 activeRule: (location) => ['/path1', '/path2'].some(path => location.pathname.startsWith(path) ) // 3. 带参数匹配 activeRule: (location) => /^\/user\/\d+/.test(location.pathname) // 4. 查询参数匹配 activeRule: (location) => new URLSearchParams(location.search).get('app') === 'module1' ``` ## 🔧 Container 配置 ### CSS 选择器 ```typescript { container: '#micro-app-container' } ``` ### DOM 节点 ```typescript { container: document.querySelector('#container') } ``` ## 📨 Props 数据传递 微应用可以通过 props 参数接收主应用传递的数据: ```typescript // 主应用 registerMicroApps([{ name: 'child-app', // ... props: { data: { user: 'john' }, methods: { onGlobalStateChange: (state) => console.log(state), setGlobalState: (state) => updateGlobalState(state) } } }]); ``` ```typescript // 微应用 export async function mount(props) { console.log(props.data); // { user: 'john' } console.log(props.methods); // { onGlobalStateChange, setGlobalState } } ``` ## ⚠️ 注意事项 ### 应用名称唯一性 ```typescript // ❌ 错误:重复的应用名称 registerMicroApps([ { name: 'app1', entry: '//localhost:8080', /*...*/ }, { name: 'app1', entry: '//localhost:8081', /*...*/ }, // 重复! ]); // ✅ 正确:唯一的应用名称 registerMicroApps([ { name: 'app1', entry: '//localhost:8080', /*...*/ }, { name: 'app2', entry: '//localhost:8081', /*...*/ }, ]); ``` ### 容器节点存在性 ```typescript // ❌ 错误:容器节点不存在 registerMicroApps([{ container: '#non-existent-container', // DOM 中不存在 // ... }]); // ✅ 正确:确保容器节点存在 registerMicroApps([{ container: '#app-container', // 确保 DOM 中存在 // ... }]); ``` ### 重复注册 ```typescript // ❌ 错误:重复注册会导致应用重复加载 registerMicroApps([...]); registerMicroApps([...]); // 重复注册 // ✅ 正确:只注册一次 registerMicroApps([...]); ``` ## 🚀 最佳实践 ### 1. 应用配置管理 ```typescript // 推荐:将应用配置抽取为单独文件 const microApps = [ { name: 'order-management', entry: getAppEntry('order'), container: '#subapp-container', activeRule: '/order', props: getAppProps('order') }, // ... ]; registerMicroApps(microApps, { beforeLoad: [initLoadingUI], afterMount: [removeLoadingUI], }); ``` ### 2. 环境配置 ```typescript const getAppEntry = (name: string) => { const entries = { development: { order: '//localhost:8001', user: '//localhost:8002' }, production: { order: '//order.example.com', user: '//user.example.com' } }; return entries[process.env.NODE_ENV][name]; }; ``` ### 3. 统一错误处理 ```typescript registerMicroApps(microApps, { beforeLoad: (app) => { console.log(`Loading ${app.name}...`); }, afterMount: (app) => { console.log(`${app.name} mounted successfully`); }, beforeUnmount: (app) => { // 清理全局状态 cleanupGlobalState(app.name); } }); ``` ## 🔗 相关 API - [start](/zh-CN/api/start) - 启动 qiankun - [loadMicroApp](/zh-CN/api/load-micro-app) - 手动加载微应用 - [生命周期](/zh-CN/api/lifecycles) - 详细的生命周期说明 ================================================ FILE: docs/zh-CN/api/start.md ================================================ # start 启动 qiankun 框架。此函数初始化微前端系统并启用基于路由的微应用自动加载。 ## 🎯 函数签名 ```typescript function start(opts?: StartOpts): void ``` ## 📋 参数 ### opts - **类型**: `StartOpts` - **必填**: ❌ - **描述**: 启动配置选项 ```typescript interface StartOpts { prefetch?: boolean | 'all' | string[] | ((apps: RegistrableApp[]) => { criticalAppNames: string[]; minorAppsName: string[] }); sandbox?: boolean | { strictStyleIsolation?: boolean; experimentalStyleIsolation?: boolean; }; singular?: boolean; urlRerouteOnly?: boolean; // ... 其他 single-spa 启动选项 } ``` | 选项 | 类型 | 默认值 | 描述 | |------|------|--------|------| | `prefetch` | `boolean \| 'all' \| string[] \| Function` | `true` | 资源预取策略 | | `sandbox` | `boolean \| SandboxOpts` | `true` | 沙箱隔离配置 | | `singular` | `boolean` | `true` | 是否同时只能挂载一个微应用 | | `urlRerouteOnly` | `boolean` | `true` | 是否仅在 URL 变化时触发路由 | ## 💡 使用示例 ### 基础用法 ```typescript import { registerMicroApps, start } from 'qiankun'; // 先注册微应用 registerMicroApps([ { name: 'react-app', entry: '//localhost:7100', container: '#subapp-viewport', activeRule: '/react', }, { name: 'vue-app', entry: '//localhost:7101', container: '#subapp-viewport', activeRule: '/vue', }, ]); // 启动 qiankun start(); ``` ### 带配置的用法 ```typescript start({ prefetch: false, // 禁用预取 sandbox: true, // 启用沙箱 singular: true, // 同时只能有一个应用 urlRerouteOnly: true, // 仅在 URL 变化时路由 }); ``` ### 高级沙箱配置 ```typescript start({ sandbox: { strictStyleIsolation: true, // 启用严格样式隔离 experimentalStyleIsolation: true, // 启用实验性样式隔离 } }); ``` ### 自定义预取策略 ```typescript start({ prefetch: 'all', // 预取所有微应用 }); // 或者预取指定应用 start({ prefetch: ['react-app', 'vue-app'], // 只预取这些应用 }); // 或者自定义预取函数 start({ prefetch: (apps) => ({ criticalAppNames: ['dashboard', 'user-center'], // 关键应用立即预取 minorAppsName: ['analytics', 'settings'], // 次要应用稍后预取 }) }); ``` ## ⚙️ 配置选项 ### 预取策略 #### 1. 布尔值 ```typescript // 完全禁用预取 start({ prefetch: false }); // 启用默认预取行为 start({ prefetch: true }); ``` #### 2. 预取所有 ```typescript // 预取所有已注册的微应用 start({ prefetch: 'all' }); ``` #### 3. 选择性预取 ```typescript // 只预取指定的应用 start({ prefetch: ['critical-app1', 'critical-app2'] }); ``` #### 4. 动态预取策略 ```typescript start({ prefetch: (apps) => { // 自定义逻辑决定预取哪些应用 const criticalApps = apps .filter(app => app.name.includes('critical')) .map(app => app.name); const minorApps = apps .filter(app => !app.name.includes('critical')) .map(app => app.name); return { criticalAppNames: criticalApps, // 立即预取 minorAppsName: minorApps, // 空闲时预取 }; } }); ``` ### 沙箱配置 #### 1. 布尔型沙箱 ```typescript // 启用基础沙箱 start({ sandbox: true }); // 禁用沙箱(不推荐) start({ sandbox: false }); ``` #### 2. 高级沙箱 ```typescript start({ sandbox: { strictStyleIsolation: true, // 基于 Shadow DOM 的样式隔离 experimentalStyleIsolation: true, // 基于作用域 CSS 的样式隔离 } }); ``` ### 性能选项 ```typescript start({ singular: false, // 允许多个应用同时挂载 urlRerouteOnly: false, // 在 URL 和编程式变化时都触发路由 }); ``` ## 🚀 最佳实践 ### 1. 在注册后调用 ```typescript // ✅ 正确的顺序 registerMicroApps([...]); start(); // ❌ 错误的顺序 start(); registerMicroApps([...]); // 这样不会正常工作 ``` ### 2. 基于环境的配置 ```typescript const startOpts = { prefetch: process.env.NODE_ENV === 'production' ? 'all' : false, sandbox: { strictStyleIsolation: process.env.NODE_ENV === 'production', }, }; start(startOpts); ``` ### 3. 性能优化 ```typescript // 在生产环境中获得更好的性能 start({ prefetch: (apps) => ({ criticalAppNames: ['dashboard'], // 只预取关键应用 minorAppsName: [], // 不预取次要应用 }), singular: true, // 防止内存问题 sandbox: { strictStyleIsolation: false, // 使用轻量级样式隔离 experimentalStyleIsolation: true, }, }); ``` ### 4. 开发环境 vs 生产环境 ```typescript if (process.env.NODE_ENV === 'development') { start({ prefetch: false, // 更快的开发重载 sandbox: false, // 更容易调试 singular: false, // 更灵活的开发 }); } else { start({ prefetch: 'all', // 更好的用户体验 sandbox: true, // 更好的隔离 singular: true, // 稳定的性能 }); } ``` ## 🔧 集成模式 ### 1. 带加载状态 ```typescript import { registerMicroApps, start } from 'qiankun'; let isQiankunStarted = false; function startQiankunWithLoading() { if (isQiankunStarted) return; showGlobalLoading(); registerMicroApps([...], { beforeLoad: (app) => { console.log(`Loading ${app.name}...`); }, afterMount: (app) => { console.log(`${app.name} mounted`); hideGlobalLoading(); }, }); start({ prefetch: 'all', sandbox: true, }); isQiankunStarted = true; } ``` ### 2. 带错误处理 ```typescript function startQiankunSafely() { try { registerMicroApps([...]); start({ prefetch: 'all', sandbox: true, }); console.log('Qiankun started successfully'); } catch (error) { console.error('Failed to start qiankun:', error); // 回退到传统路由或显示错误页面 window.location.href = '/fallback'; } } ``` ### 3. 带特性检测 ```typescript import { isRuntimeCompatible } from 'qiankun'; if (isRuntimeCompatible()) { registerMicroApps([...]); start(); } else { console.warn('Browser not compatible with qiankun'); // 回退实现 initTraditionalRouting(); } ``` ## ⚠️ 重要注意事项 ### 1. 只调用一次 ```typescript // ❌ 错误:多次调用 start(); start(); // 这个调用会被忽略 // ✅ 正确:单次调用 start(); ``` ### 2. 顺序很重要 ```typescript // ✅ 正确顺序 registerMicroApps([...]); // 1. 先注册应用 start(); // 2. 然后启动 // ❌ 错误顺序 - 应用不会被正确注册 start(); registerMicroApps([...]); ``` ### 3. 预取注意事项 ```typescript // ⚠️ 在大型应用中要小心使用 'all' start({ prefetch: 'all' }); // 可能影响初始加载性能 // ✅ 更好:选择性预取 start({ prefetch: ['critical-app1', 'critical-app2'] }); ``` ## 🎯 常见用例 ### 1. 电商平台 ```typescript registerMicroApps([ { name: 'product-catalog', entry: '//catalog.example.com', activeRule: '/products' }, { name: 'shopping-cart', entry: '//cart.example.com', activeRule: '/cart' }, { name: 'user-account', entry: '//account.example.com', activeRule: '/account' }, ]); start({ prefetch: (apps) => ({ criticalAppNames: ['shopping-cart'], // 总是预取购物车 minorAppsName: ['user-account'], // 空闲时预取账户 }), sandbox: true, singular: true, }); ``` ### 2. 管理后台 ```typescript start({ prefetch: false, // 不预取 - 管理工具按需使用 sandbox: { strictStyleIsolation: true, // 防止管理工具间的样式冲突 }, singular: false, // 允许多个管理工具同时打开 }); ``` ### 3. 多租户平台 ```typescript const tenantId = getCurrentTenantId(); start({ prefetch: [`tenant-${tenantId}-dashboard`], // 只预取当前租户的应用 sandbox: true, // 隔离租户数据 singular: true, }); ``` ## 🔗 相关 API - [registerMicroApps](/zh-CN/api/register-micro-apps) - 注册微应用 - [loadMicroApp](/zh-CN/api/load-micro-app) - 手动加载微应用 - [isRuntimeCompatible](/zh-CN/api/is-runtime-compatible) - 检查浏览器兼容性 ================================================ FILE: docs/zh-CN/api/types.md ================================================ # TypeScript 类型 qiankun 提供了全面的 TypeScript 类型定义,确保类型安全和出色的开发者体验。本文档涵盖了所有可用的类型和接口。 ## 📋 核心类型 ### ObjectType **描述**:通用对象结构的基础类型。 ```typescript export type ObjectType = Record; ``` **用法**: ```typescript // 用作泛型类型的约束 function processApp(props: T): void { // T 可以是任何对象类型 } ``` ### HTMLEntry **描述**:微应用入口点的类型。 ```typescript export type HTMLEntry = string; ``` **用法**: ```typescript const appEntry: HTMLEntry = '//localhost:8080'; const appEntryWithPath: HTMLEntry = '//localhost:8080/micro-app'; ``` ## 🏗️ 应用类型 ### AppMetadata **描述**:微应用的基础元数据。 ```typescript type AppMetadata = { name: string; // 唯一的应用名称 entry: HTMLEntry; // 应用入口 URL }; ``` ### LoadableApp\ **描述**:手动加载微应用的配置。 ```typescript export type LoadableApp = AppMetadata & { container: HTMLElement; // DOM 容器元素 props?: T; // 传递给应用的自定义属性 }; ``` **用法**: ```typescript // 基础用法 const app: LoadableApp<{}> = { name: 'my-app', entry: '//localhost:8080', container: document.getElementById('app-container')!, }; // 带自定义属性 interface MyAppProps { theme: 'light' | 'dark'; userId: string; } const appWithProps: LoadableApp = { name: 'themed-app', entry: '//localhost:8080', container: document.getElementById('container')!, props: { theme: 'dark', userId: '123' } }; ``` ### RegistrableApp\ **描述**:基于路由的微应用配置。 ```typescript export type RegistrableApp = LoadableApp & { loader?: (loading: boolean) => void; // 加载状态回调 activeRule: RegisterApplicationConfig['activeWhen']; // 路由激活规则 }; ``` **用法**: ```typescript import { registerMicroApps } from 'qiankun'; interface UserAppProps { currentUser: { id: string; name: string }; } const apps: RegistrableApp[] = [ { name: 'user-dashboard', entry: '//localhost:8001', container: '#subapp-viewport', activeRule: '/dashboard', props: { currentUser: { id: '123', name: 'John' } }, loader: (loading) => { if (loading) { showLoadingSpinner(); } else { hideLoadingSpinner(); } } } ]; registerMicroApps(apps); ``` ## ⚙️ 配置类型 ### AppConfiguration **描述**:单个微应用的配置选项。 ```typescript export type AppConfiguration = Partial> & { sandbox?: boolean; // 启用沙箱隔离 globalContext?: WindowProxy; // 自定义全局上下文 }; ``` **用法**: ```typescript import { loadMicroApp } from 'qiankun'; const customConfig: AppConfiguration = { sandbox: true, globalContext: window, fetch: async (url, options) => { // 自定义 fetch 实现 return fetch(url, { ...options, headers: { ...options?.headers, 'Authorization': 'Bearer token' } }); }, nodeTransformer: (node, opts) => { // 转换 DOM 节点 if (node.tagName === 'SCRIPT') { node.setAttribute('data-app', 'my-app'); } return node; } }; loadMicroApp({ name: 'configured-app', entry: '//localhost:8080', container: document.getElementById('container')! }, customConfig); ``` ## 🔄 生命周期类型 ### LifeCycleFn\ **描述**:生命周期钩子函数的类型。 ```typescript export type LifeCycleFn = ( app: LoadableApp, global: WindowProxy ) => Promise; ``` **用法**: ```typescript const beforeLoadHook: LifeCycleFn<{ theme: string }> = async (app, global) => { console.log(`正在加载应用: ${app.name}`); global.__APP_THEME__ = app.props?.theme || 'default'; }; const afterMountHook: LifeCycleFn = async (app, global) => { console.log(`应用 ${app.name} 挂载成功`); // 跟踪分析 analytics.track('app_mounted', { appName: app.name }); }; ``` ### LifeCycles\ **描述**:完整的生命周期钩子配置。 ```typescript export type LifeCycles = { beforeLoad?: LifeCycleFn | Array>; beforeMount?: LifeCycleFn | Array>; afterMount?: LifeCycleFn | Array>; beforeUnmount?: LifeCycleFn | Array>; afterUnmount?: LifeCycleFn | Array>; }; ``` **用法**: ```typescript interface AppProps { userId: string; permissions: string[]; } const lifecycles: LifeCycles = { beforeLoad: async (app, global) => { // 加载前设置 global.__USER_ID__ = app.props?.userId; }, beforeMount: [ async (app, global) => { // 多个钩子作为数组 await setupAuthentication(app.props?.userId); }, async (app, global) => { await loadUserPermissions(app.props?.permissions); } ], afterMount: async (app) => { console.log(`${app.name} 已准备就绪`); }, beforeUnmount: async (app) => { // 卸载前清理 await saveUserState(app.name); }, afterUnmount: async (app) => { // 最终清理 await clearUserData(app.name); } }; ``` ## 🎯 微应用类型 ### MicroApp **描述**:已加载微应用的实例。 ```typescript export type MicroApp = Parcel; ``` `MicroApp` 类型扩展了 single-spa 的 `Parcel` 接口,包含以下方法: ```typescript interface MicroApp { mount(): Promise; // 挂载应用 unmount(): Promise; // 卸载应用 update(props: any): Promise; // 更新应用属性 getStatus(): string; // 获取当前状态 loadPromise: Promise; // 加载完成时解析的 Promise mountPromise: Promise; // 挂载完成时解析的 Promise unmountPromise: Promise; // 卸载完成时解析的 Promise } ``` **用法**: ```typescript import { loadMicroApp } from 'qiankun'; const microApp: MicroApp = loadMicroApp({ name: 'my-app', entry: '//localhost:8080', container: document.getElementById('container')! }); // 检查状态 console.log(microApp.getStatus()); // 'LOADING', 'MOUNTED', 'UNMOUNTED', 等 // 等待挂载 await microApp.mountPromise; console.log('应用已挂载'); // 更新属性 await microApp.update({ newTheme: 'dark' }); // 完成后卸载 await microApp.unmount(); ``` ### MicroAppLifeCycles **描述**:qiankun 使用的内部生命周期类型。 ```typescript export type MicroAppLifeCycles = FlattenArrayValue>; ``` 这个类型主要用于内部使用,表示微应用必须导出的扁平化生命周期函数。 ## 🌐 全局类型 ### Window 扩展 qiankun 为全局 `Window` 接口添加了特殊属性: ```typescript declare global { interface Window { __POWERED_BY_QIANKUN__?: boolean; // 指示应用运行在 qiankun 中 __INJECTED_PUBLIC_PATH_BY_QIANKUN__?: string; // 注入的公共路径 __QIANKUN_DEVELOPMENT__?: boolean; // 开发模式标志 Zone?: CallableFunction; // Zone.js 兼容性 __zone_symbol__setTimeout?: Window['setTimeout']; // Zone.js 超时 } } ``` **在微应用中的用法**: ```typescript // 检查是否在 qiankun 中运行 if (window.__POWERED_BY_QIANKUN__) { console.log('作为微应用运行'); // 使用注入的公共路径 const publicPath = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ || '/'; // 相应地配置你的应用 setupApp({ publicPath }); } else { console.log('独立运行'); setupApp({ publicPath: '/' }); } ``` ## 🎨 工具类型 ### 自定义类型守卫 为更好的类型安全创建类型守卫: ```typescript // LoadableApp 的类型守卫 function isLoadableApp( app: any ): app is LoadableApp { return ( typeof app === 'object' && typeof app.name === 'string' && typeof app.entry === 'string' && app.container instanceof HTMLElement ); } // RegistrableApp 的类型守卫 function isRegistrableApp( app: any ): app is RegistrableApp { return ( isLoadableApp(app) && (typeof app.activeRule === 'string' || typeof app.activeRule === 'function') ); } // 用法 function processApp(app: unknown) { if (isRegistrableApp(app)) { // TypeScript 在这里知道 app 是 RegistrableApp console.log(`注册应用: ${app.name},规则: ${app.activeRule}`); } else if (isLoadableApp(app)) { // TypeScript 在这里知道 app 是 LoadableApp console.log(`加载应用: ${app.name}`); } } ``` ### 通用辅助类型 为常见模式创建可重用的泛型类型: ```typescript // 支持主题的属性 type ThemedProps = T & { theme?: 'light' | 'dark'; }; // 带用户上下文的属性 type UserAwareProps = T & { currentUser?: { id: string; name: string; role: string; }; }; // 组合属性 type AppProps = ThemedProps>; // 用法 const app: LoadableApp> = { name: 'themed-user-app', entry: '//localhost:8080', container: document.getElementById('container')!, props: { theme: 'dark', currentUser: { id: '123', name: 'John', role: 'admin' }, customData: '自定义值' } }; ``` ## 📖 高级类型模式 ### 基于配置的条件类型 ```typescript // 基于环境的配置 type EnvironmentConfig = T extends 'development' ? { sandbox: false; prefetch: false; strictStyleIsolation: false; } : { sandbox: true; prefetch: 'all'; strictStyleIsolation: true; }; // 与环境检测一起使用 declare const NODE_ENV: 'development' | 'production'; type CurrentConfig = EnvironmentConfig; ``` ### 应用名称的品牌类型 ```typescript // 为应用名称创建品牌类型以防止混淆 type AppName = string & { readonly __brand: unique symbol }; function createAppName(name: string): AppName { return name as AppName; } // 带品牌名称的增强 LoadableApp type SafeLoadableApp = Omit, 'name'> & { name: AppName; }; // 用法 const appName = createAppName('my-secure-app'); const app: SafeLoadableApp<{}> = { name: appName, // 类型安全的应用名称 entry: '//localhost:8080', container: document.getElementById('container')! }; ``` ### 生命周期事件类型 ```typescript // 带事件数据的增强生命周期 type LifeCycleEvent = { app: LoadableApp; global: WindowProxy; timestamp: number; phase: 'beforeLoad' | 'beforeMount' | 'afterMount' | 'beforeUnmount' | 'afterUnmount'; }; type EnhancedLifeCycleFn = (event: LifeCycleEvent) => Promise; // 用法 const enhancedHook: EnhancedLifeCycleFn<{ userId: string }> = async (event) => { console.log(`阶段: ${event.phase},应用: ${event.app.name},时间: ${event.timestamp}`); if (event.phase === 'beforeMount') { // 设置用户上下文 event.global.__USER_ID__ = event.app.props?.userId; } }; ``` ## 🔍 类型推断示例 ### 自动属性类型推断 ```typescript // 带自动类型推断的辅助函数 function createTypedApp( config: { name: string; entry: string; container: HTMLElement; props: T; } ): LoadableApp { return config; // TypeScript 推断正确的类型 } // 用法 - TypeScript 自动推断属性类型 const app = createTypedApp({ name: 'inferred-app', entry: '//localhost:8080', container: document.getElementById('container')!, props: { theme: 'dark', userId: '123', features: ['feature1', 'feature2'] } // TypeScript 知道 props 类型是 { theme: string; userId: string; features: string[] } }); ``` ### 生命周期类型推断 ```typescript // 创建类型化生命周期的辅助函数 function createLifecycles( lifecycles: LifeCycles ): LifeCycles { return lifecycles; } // 带推断的用法 const typedLifecycles = createLifecycles({ beforeMount: async (app) => { // TypeScript 根据用法推断 app.props 类型 console.log(app.props?.theme); // TypeScript 知道这可能是 undefined } }); ``` ## ⚡ 最佳实践 ### 1. 使用严格类型 ```typescript // ✅ 好:严格类型 interface StrictAppProps { readonly userId: string; readonly theme: 'light' | 'dark'; readonly permissions: readonly string[]; } const app: LoadableApp = { name: 'strict-app', entry: '//localhost:8080', container: document.getElementById('container')!, props: { userId: '123', theme: 'dark', permissions: ['read', 'write'] } }; // ❌ 坏:松散类型 const looseApp: LoadableApp = { name: 'loose-app', entry: '//localhost:8080', container: document.getElementById('container')!, props: { anything: 'goes' } // 没有类型安全 }; ``` ### 2. 创建领域特定类型 ```typescript // 为你的领域创建特定类型 interface ECommerceAppProps { cartId: string; currency: 'USD' | 'EUR' | 'GBP'; customerSegment: 'premium' | 'standard'; features: { wishlist: boolean; recommendations: boolean; reviews: boolean; }; } type ECommerceApp = LoadableApp; type ECommerceLifecycles = LifeCycles; ``` ### 3. 使用泛型约束 ```typescript // 为更好的类型安全约束泛型类型 interface BaseAppProps { version: string; environment: 'development' | 'staging' | 'production'; } function createApp( config: Omit, 'container'> & { containerId: string; } ): LoadableApp { const container = document.getElementById(config.containerId); if (!container) { throw new Error(`容器 ${config.containerId} 未找到`); } return { ...config, container }; } ``` ## 🔗 相关文档 - [API 参考](/zh-CN/api/) - 主要 API 文档 - [生命周期](/zh-CN/api/lifecycles) - 详细的生命周期文档 - [配置选项](/zh-CN/api/configuration) - 配置选项 ================================================ FILE: docs/zh-CN/cookbook/error-handling.md ================================================ # 错误处理 在同一上下文中运行多个独立应用的微前端应用程序中,健壮的错误处理至关重要。本指南涵盖了处理错误、实现优雅降级以及在基于 qiankun 的微前端系统中维护应用程序稳定性的综合策略。 ## 🎯 微前端中的错误类型 ### 常见错误类别 微前端应用面临独特的错误场景: - **加载错误**:无法获取或解析微应用资源 - **运行时错误**:微应用内的 JavaScript 错误 - **通信错误**:应用间通信失败 - **网络错误**:API 调用和资源加载失败 - **沙箱错误**:JavaScript 和 CSS 隔离问题 - **生命周期错误**:mount/unmount 过程中的问题 - **版本冲突**:依赖版本不匹配 ### 错误影响评估 ```javascript // 微前端应用的错误严重程度级别 const ERROR_LEVELS = { CRITICAL: 'critical', // 主应用或核心功能受影响 HIGH: 'high', // 主要微应用功能丢失 MEDIUM: 'medium', // 部分微应用功能受影响 LOW: 'low', // 轻微功能或视觉问题 INFO: 'info' // 非阻塞性信息问题 }; const ErrorClassifier = { classify(error, appName, context) { // 严重:主应用崩溃或核心导航失败 if (appName === 'main' || context.includes('navigation')) { return ERROR_LEVELS.CRITICAL; } // 高:用户无法完成主要工作流程 if (context.includes('checkout') || context.includes('auth')) { return ERROR_LEVELS.HIGH; } // 中等:功能降级但应用仍可使用 if (error.name === 'ChunkLoadError' || error.name === 'TypeError') { return ERROR_LEVELS.MEDIUM; } // 其他错误默认为低级别 return ERROR_LEVELS.LOW; } }; ``` ## 🛡️ qiankun 错误边界 ### 全局错误处理 为整个微前端生态系统设置全局错误处理器: ```javascript import { addGlobalUncaughtErrorHandler, removeGlobalUncaughtErrorHandler } from 'qiankun'; // 所有微应用的全局错误处理器 const globalErrorHandler = (event) => { const { error, appName, lifecycleName } = event; console.error(`微应用 "${appName}" 在 "${lifecycleName}" 阶段发生错误:`, error); // 上报到错误跟踪服务 reportError({ error, appName, lifecycle: lifecycleName, timestamp: Date.now(), userAgent: navigator.userAgent, url: window.location.href }); // 实施恢复策略 handleMicroAppError(appName, error, lifecycleName); }; // 注册全局错误处理器 addGlobalUncaughtErrorHandler(globalErrorHandler); // 清理时移除(例如在应用卸载时) // removeGlobalUncaughtErrorHandler(globalErrorHandler); ``` ### 生命周期特定错误处理 ```javascript // 生命周期钩子中的错误处理 const errorHandlingLifecycles = { async beforeLoad(app) { try { // 预加载检查 const healthCheck = await fetch(`${app.entry}/health`); if (!healthCheck.ok) { throw new Error(`${app.name} 健康检查失败`); } } catch (error) { console.warn(`${app.name} 预加载健康检查失败:`, error); // 继续加载但标记为可能不稳定 markAppAsUnstable(app.name); } }, async beforeMount(app) { try { // 验证应用要求 validateAppRequirements(app); } catch (error) { // 尝试修复常见问题 await attemptAutoFix(app, error); } }, async afterMount(app) { // 验证挂载成功 setTimeout(() => { const container = document.querySelector(app.container); if (!container || container.children.length === 0) { console.error(`${app.name} 挂载验证失败`); showFallbackContent(app.container, app.name); } }, 1000); }, async beforeUnmount(app) { try { // 清理资源 cleanupAppResources(app.name); } catch (error) { console.warn(`${app.name} 清理错误:`, error); // 强制清理 forceCleanup(app.name); } } }; ``` ## 🚨 框架特定错误边界 ### React 错误边界 ```jsx // React 微应用错误边界 import React from 'react'; class MicroAppErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false, error: null, errorInfo: null, retryCount: 0, lastRetry: null }; } static getDerivedStateFromError(error) { return { hasError: true }; } componentDidCatch(error, errorInfo) { this.setState({ error, errorInfo }); // 上报错误 this.reportError(error, errorInfo); // 尝试自动恢复 this.attemptRecovery(error); } reportError = (error, errorInfo) => { const errorReport = { error: { name: error.name, message: error.message, stack: error.stack }, errorInfo, appName: this.props.appName, timestamp: Date.now(), url: window.location.href, userAgent: navigator.userAgent, retryCount: this.state.retryCount }; // 发送到错误跟踪服务 fetch('/api/errors', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(errorReport) }).catch(err => console.error('上报错误失败:', err)); }; attemptRecovery = (error) => { const { retryCount, lastRetry } = this.state; const now = Date.now(); // 防止过于频繁的重试 if (lastRetry && now - lastRetry < 5000) { return; } // 限制重试次数 if (retryCount >= 3) { console.error(`${this.props.appName} 已达到最大重试次数`); return; } setTimeout(() => { this.setState({ hasError: false, error: null, errorInfo: null, retryCount: retryCount + 1, lastRetry: now }); }, 2000 * Math.pow(2, retryCount)); // 指数退避 }; render() { if (this.state.hasError) { const { appName, fallbackComponent: FallbackComponent } = this.props; if (FallbackComponent) { return ( this.attemptRecovery(this.state.error)} /> ); } return (

应用错误

{appName} 应用遇到了错误。

错误详情
{this.state.error?.stack}
); } return this.props.children; } } // 微应用使用示例 function MicroAppContainer({ appName, entry }) { return (
); } ``` ### Vue 错误处理 ```javascript // Vue 微应用全局错误处理器 const app = createApp(MainApp); app.config.errorHandler = (err, instance, info) => { const appName = instance?.$root?.$options?.name || 'unknown'; console.error(`Vue 错误在 ${appName}:`, err, info); // 上报错误 reportVueError({ error: err, appName, info, timestamp: Date.now() }); // 尝试恢复 if (instance && typeof instance.$forceUpdate === 'function') { instance.$forceUpdate(); } }; // Vue 2 错误边界组件 Vue.component('ErrorBoundary', { data() { return { hasError: false, error: null }; }, errorCaptured(err, instance, info) { this.hasError = true; this.error = err; // 上报错误 this.reportError(err, info); // 阻止错误继续传播 return false; }, methods: { reportError(error, info) { // 错误上报逻辑 }, retry() { this.hasError = false; this.error = null; this.$forceUpdate(); } }, render(h) { if (this.hasError) { return h('div', { class: 'error-boundary' }, [ h('h3', '出现了错误'), h('button', { on: { click: this.retry } }, '重试'), h('pre', this.error?.message) ]); } return this.$slots.default; } }); ``` ## 🔄 优雅降级策略 ### 渐进式增强 ```javascript // 带回退的渐进式功能加载 class FeatureLoader { constructor() { this.features = new Map(); this.fallbacks = new Map(); } register(featureName, loader, fallback) { this.features.set(featureName, loader); this.fallbacks.set(featureName, fallback); } async load(featureName) { try { const loader = this.features.get(featureName); if (!loader) { throw new Error(`功能 "${featureName}" 未注册`); } const feature = await loader(); return feature; } catch (error) { console.warn(`加载功能 "${featureName}" 失败:`, error); const fallback = this.fallbacks.get(featureName); if (fallback) { return await fallback(); } throw error; } } } // 使用示例 const featureLoader = new FeatureLoader(); // 注册高级仪表板和回退 featureLoader.register( 'advanced-dashboard', () => import('./AdvancedDashboard'), () => import('./BasicDashboard') ); // 注册图表组件和静态回退 featureLoader.register( 'interactive-charts', () => import('./InteractiveCharts'), () => Promise.resolve(() => '
图表不可用
') ); ``` ### 回退 UI 组件 ```jsx // 综合回退组件 const ErrorFallbacks = { // 网络错误回退 NetworkError: ({ onRetry, appName }) => (
🌐

连接问题

无法加载 {appName}。请检查您的网络连接。

), // JavaScript 错误回退 JavaScriptError: ({ error, appName, onRetry }) => (
⚠️

应用错误

{appName} 应用遇到技术问题。

{process.env.NODE_ENV === 'development' && (
技术详情
{error.stack}
)}
), // 加载超时回退 LoadingTimeout: ({ appName, onRetry }) => (
⏱️

加载超时

{appName} 加载时间超出预期。

), // 通用回退 Generic: ({ error, appName, onRetry }) => (
🔧

临时问题

我们正在处理 {appName} 的技术困难。

) }; ``` ### 熔断器模式 ```javascript // 微应用加载的熔断器 class CircuitBreaker { constructor(threshold = 5, timeout = 60000, monitor = 30000) { this.failureThreshold = threshold; this.timeout = timeout; this.monitoringPeriod = monitor; this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN this.failureCount = 0; this.lastFailureTime = null; this.nextAttemptTime = null; } async execute(operation, appName) { if (this.state === 'OPEN') { if (Date.now() < this.nextAttemptTime) { throw new Error(`${appName} 的熔断器是 OPEN 状态`); } this.state = 'HALF_OPEN'; } try { const result = await operation(); this.onSuccess(); return result; } catch (error) { this.onFailure(); throw error; } } onSuccess() { this.failureCount = 0; this.state = 'CLOSED'; } onFailure() { this.failureCount++; this.lastFailureTime = Date.now(); if (this.failureCount >= this.failureThreshold) { this.state = 'OPEN'; this.nextAttemptTime = Date.now() + this.timeout; } } getState() { return this.state; } } // 使用熔断器加载微应用 const circuitBreakers = new Map(); const loadMicroAppWithCircuitBreaker = async (appConfig) => { const { name } = appConfig; if (!circuitBreakers.has(name)) { circuitBreakers.set(name, new CircuitBreaker()); } const breaker = circuitBreakers.get(name); try { return await breaker.execute(() => loadMicroApp(appConfig), name); } catch (error) { console.error(`熔断器阻止了 ${name} 的加载:`, error); throw error; } }; ``` ## 📊 错误监控和上报 ### 综合错误跟踪 ```javascript // 高级错误跟踪系统 class ErrorTracker { constructor(config) { this.config = { endpoint: '/api/errors', batchSize: 10, batchTimeout: 5000, maxRetries: 3, ...config }; this.errorQueue = []; this.batchTimeout = null; this.retryCount = new Map(); } track(error, context = {}) { const errorData = this.serializeError(error, context); // 添加到队列 this.errorQueue.push(errorData); // 如果队列已满则处理批次 if (this.errorQueue.length >= this.config.batchSize) { this.processBatch(); } else { // 设置批次处理超时 this.scheduleBatchProcessing(); } } serializeError(error, context) { return { id: this.generateErrorId(), timestamp: Date.now(), error: { name: error.name, message: error.message, stack: error.stack, fileName: error.fileName, lineNumber: error.lineNumber, columnNumber: error.columnNumber }, context: { appName: context.appName || 'unknown', userId: context.userId, sessionId: this.getSessionId(), url: window.location.href, userAgent: navigator.userAgent, viewport: { width: window.innerWidth, height: window.innerHeight }, ...context }, environment: { isDevelopment: process.env.NODE_ENV === 'development', timestamp: Date.now(), timezone: Intl.DateTimeFormat().resolvedOptions().timeZone } }; } scheduleBatchProcessing() { if (this.batchTimeout) { clearTimeout(this.batchTimeout); } this.batchTimeout = setTimeout(() => { this.processBatch(); }, this.config.batchTimeout); } async processBatch() { if (this.errorQueue.length === 0) return; const batch = this.errorQueue.splice(0, this.config.batchSize); try { await this.sendErrors(batch); // 成功时清除重试计数 batch.forEach(error => { this.retryCount.delete(error.id); }); } catch (error) { console.error('发送错误批次失败:', error); // 重试逻辑 batch.forEach(errorData => { const retries = this.retryCount.get(errorData.id) || 0; if (retries < this.config.maxRetries) { this.retryCount.set(errorData.id, retries + 1); this.errorQueue.unshift(errorData); // 添加回队列前端 } }); } } async sendErrors(errors) { const response = await fetch(this.config.endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ errors }) }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return response.json(); } generateErrorId() { return `error_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } getSessionId() { // 获取/生成会话ID的实现 return sessionStorage.getItem('sessionId') || 'anonymous'; } } // 初始化全局错误跟踪器 const errorTracker = new ErrorTracker(); // 跟踪未处理的错误 window.addEventListener('error', (event) => { errorTracker.track(event.error, { type: 'unhandled_error', source: 'window.onerror' }); }); // 跟踪未处理的 Promise 拒绝 window.addEventListener('unhandledrejection', (event) => { errorTracker.track(event.reason, { type: 'unhandled_rejection', source: 'unhandledrejection' }); }); ``` ### 性能影响监控 ```javascript // 监控错误对性能的影响 class ErrorImpactMonitor { constructor() { this.errorImpacts = new Map(); this.performanceBaseline = this.measureBaseline(); } measureBaseline() { return { loadTime: performance.now(), memoryUsage: performance.memory ? performance.memory.usedJSHeapSize : 0, timing: performance.timing }; } recordErrorImpact(errorId, appName) { const impact = { errorId, appName, timestamp: Date.now(), performance: { loadTime: performance.now(), memoryUsage: performance.memory ? performance.memory.usedJSHeapSize : 0, timing: performance.timing }, userExperience: { pageVisible: !document.hidden, userActive: this.isUserActive(), scrollPosition: window.scrollY } }; this.errorImpacts.set(errorId, impact); this.analyzeImpact(impact); } analyzeImpact(impact) { const { performance: current } = impact; const baseline = this.performanceBaseline; const memoryIncrease = current.memoryUsage - baseline.memoryUsage; const loadTimeIncrease = current.loadTime - baseline.loadTime; if (memoryIncrease > 50 * 1024 * 1024) { // 50MB console.warn('错误后检测到高内存影响:', impact); } if (loadTimeIncrease > 5000) { // 5秒 console.warn('错误后性能显著降级:', impact); } } isUserActive() { // 简单用户活动检测 return Date.now() - this.lastUserActivity < 30000; } } ``` ## 🔧 恢复机制 ### 自动恢复策略 ```javascript // 综合恢复系统 class RecoveryManager { constructor() { this.recoveryStrategies = new Map(); this.setupDefaultStrategies(); } setupDefaultStrategies() { // 网络错误恢复 this.register('NetworkError', async (error, context) => { await this.waitForConnection(); return this.reloadMicroApp(context.appName); }); // 块加载错误恢复 this.register('ChunkLoadError', async (error, context) => { // 清除 webpack 缓存 if (window.__webpack_require__ && window.__webpack_require__.cache) { delete window.__webpack_require__.cache[error.request]; } // 使用缓存破坏重新加载 return this.reloadWithCacheBust(context.appName); }); // 脚本错误恢复 this.register('TypeError', async (error, context) => { // 尝试重新加载依赖 await this.reloadDependencies(context.appName); return this.remountMicroApp(context.appName); }); // 内存错误恢复 this.register('RangeError', async (error, context) => { // 强制垃圾回收 if (window.gc) window.gc(); // 减少内存占用 await this.reducememoryFootprint(context.appName); return this.reloadMicroApp(context.appName); }); } register(errorType, strategy) { this.recoveryStrategies.set(errorType, strategy); } async recover(error, context) { const strategy = this.recoveryStrategies.get(error.name); if (strategy) { try { console.log(`尝试恢复 ${context.appName} 中的 ${error.name}`); const result = await strategy(error, context); console.log(`${context.appName} 恢复成功`); return result; } catch (recoveryError) { console.error(`${context.appName} 恢复失败:`, recoveryError); return this.fallbackRecovery(context); } } return this.fallbackRecovery(context); } async waitForConnection() { return new Promise((resolve) => { if (navigator.onLine) { resolve(); } else { const handleOnline = () => { window.removeEventListener('online', handleOnline); resolve(); }; window.addEventListener('online', handleOnline); } }); } async reloadMicroApp(appName) { // 卸载当前实例 try { await unmountMicroApp(appName); } catch (error) { console.warn(`卸载 ${appName} 失败:`, error); } // 重新加载微应用 const appConfig = getAppConfig(appName); return loadMicroApp(appConfig); } async reloadWithCacheBust(appName) { const appConfig = getAppConfig(appName); const cacheBustEntry = `${appConfig.entry}?t=${Date.now()}`; return loadMicroApp({ ...appConfig, entry: cacheBustEntry }); } async fallbackRecovery(context) { console.log(`使用 ${context.appName} 的回退恢复`); // 显示回退 UI showFallbackUI(context.appName); // 报告恢复失败 reportRecoveryFailure(context); return null; } } ``` ### 用户发起的恢复 ```jsx // 用户控制的恢复界面 const RecoveryPanel = ({ appName, error, onRecover, onDismiss }) => { const [recovering, setRecovering] = useState(false); const [lastAttempt, setLastAttempt] = useState(null); const handleRecover = async (strategy) => { setRecovering(true); setLastAttempt(Date.now()); try { await onRecover(strategy); } catch (error) { console.error('用户发起的恢复失败:', error); } finally { setRecovering(false); } }; const recoveryOptions = [ { key: 'reload', label: '重新加载应用', description: '从头重启应用', action: () => handleRecover('reload') }, { key: 'reset', label: '重置为默认', description: '清除所有数据并重新加载', action: () => handleRecover('reset') }, { key: 'safe-mode', label: '安全模式', description: '以最小功能加载', action: () => handleRecover('safe-mode') } ]; return (

{appName} 的恢复选项

错误: {error.message}

{lastAttempt && (

上次尝试: {new Date(lastAttempt).toLocaleTimeString()}

)}
{recoveryOptions.map(option => ( ))}
{recovering && (
正在尝试恢复...
)}
); }; ``` ## 🎯 最佳实践总结 ### ✅ 错误处理要做的 1. **实施全局错误处理器**进行全面覆盖 2. **在每个微应用中使用错误边界** 3. **为用户提供有意义的错误消息** 4. **实施带回退 UI 的优雅降级** 5. **系统性地监控和跟踪错误** 6. **在开发过程中测试错误场景** 7. **在可能的地方实施自动恢复** 8. **在报告中清除错误上下文** 9. **优雅地处理网络故障** 10. **提供用户恢复选项** ### ❌ 错误处理不要做的 1. **不要忽略错误**或静默失败 2. **不要向最终用户显示技术细节** 3. **不要无限制地重试** 4. **不要因为一个微应用错误阻塞整个应用** 5. **不要忘记在错误后清理** 6. **不要完全依赖自动恢复** 7. **不要用错误消息让用户不知所措** 8. **不要忘记错误场景中的内存泄漏** 9. **不要跳过生产环境类似环境的错误测试** 10. **不要忽略用户关于错误的反馈** ### 🔄 错误恢复检查清单 ```javascript // 综合错误处理检查清单 const errorHandlingChecklist = { prevention: { validation: '✓ 已实施输入验证', typeChecking: '✓ 使用 TypeScript 或 PropTypes', testing: '✓ 已测试错误场景', monitoring: '✓ 健康检查已就位' }, detection: { globalHandlers: '✓ 已设置全局错误处理器', boundaries: '✓ 已实施错误边界', logging: '✓ 全面错误日志记录', alerting: '✓ 实时错误警报' }, recovery: { gracefulDegradation: '✓ 已实施回退 UI', automaticRecovery: '✓ 自动恢复策略', userRecovery: '✓ 用户发起的恢复选项', resourceCleanup: '✓ 错误时适当清理' }, learning: { errorTracking: '✓ 错误分析已就位', trendAnalysis: '✓ 错误趋势监控', rootCauseAnalysis: '✓ RCA 流程已定义', continuousImprovement: '✓ 定期错误审查会议' } }; ``` ## 🔗 相关文档 - [性能优化](/cookbook/performance) - 错误对性能的影响 - [调试](/cookbook/debugging) - 错误调试技术 - [样式隔离](/cookbook/style-isolation) - CSS 错误处理 - [配置](/api/configuration) - 错误相关配置 ================================================ FILE: docs/zh-CN/cookbook/index.md ================================================ # 最佳实践 本节包含使用 qiankun 构建生产级微前端应用的实用指南和最佳实践。这些指南基于真实世界的经验和实施微前端架构时面临的常见挑战。 ## 🎯 概述 构建微前端需要仔细考虑各个方面,包括架构设计、性能优化、开发工作流和部署策略。这些指南将帮助你避免常见陷阱并实施稳健的解决方案。 ## 📚 可用指南 ### 🎨 [样式隔离](/zh-CN/cookbook/style-isolation) 学习如何防止微应用之间的 CSS 冲突并实施有效的样式隔离策略。 **你将学到:** - CSS 隔离技术 - Shadow DOM 实现 - CSS 作用域策略 - 运行时样式冲突解决 - 组件库最佳实践 ### ⚡ [性能优化](/zh-CN/cookbook/performance) 优化你的微前端应用以获得更好的加载时间和运行时性能。 **你将学到:** - 资源加载优化 - 包拆分策略 - 缓存机制 - 懒加载技术 - 性能监控 ### 🛠️ [错误处理](/zh-CN/cookbook/error-handling) 为微前端应用实施稳健的错误处理和恢复机制。 **你将学到:** - 错误边界实现 - 优雅降级策略 - 错误监控和报告 - 恢复机制 - 用户体验考虑 ### 🔍 [调试和开发](/zh-CN/cookbook/debugging) 掌握微前端应用的调试技术和开发工作流。 **你将学到:** - 开发环境设置 - 调试工具和技术 - 热重载配置 - 跨应用调试 - 生产调试策略 ### 🚀 [部署策略](/zh-CN/cookbook/deployment) 学习微前端应用的部署模式和 CI/CD 策略。 **你将学到:** - 独立部署工作流 - 版本管理 - 回滚策略 - 环境配置 - 零停机部署 ### 🔄 [状态管理](/zh-CN/cookbook/state-management) 在微应用之间实施有效的状态管理。 **你将学到:** - 跨应用状态共享 - 事件驱动通信 - 状态同步 - 数据流模式 - 存储管理 ### 🌐 [路由和导航](/zh-CN/cookbook/routing) 为微前端应用设计和实施导航模式。 **你将学到:** - 路由配置策略 - 深度链接支持 - 导航守卫 - 历史管理 - SEO 考虑 ### 🔒 [安全](/zh-CN/cookbook/security) 为微前端架构实施安全最佳实践。 **你将学到:** - 内容安全策略 (CSP) - 跨源资源共享 (CORS) - 身份验证和授权 - 安全通信模式 - 漏洞防护 ### 🧪 [测试策略](/zh-CN/cookbook/testing) 为微前端应用开发全面的测试策略。 **你将学到:** - 微应用单元测试 - 集成测试策略 - 端到端测试 - 视觉回归测试 - 性能测试 ### 📊 [监控和分析](/zh-CN/cookbook/monitoring) 为微前端应用实施监控和分析。 **你将学到:** - 性能监控 - 错误跟踪 - 用户分析 - 应用健康检查 - 业务指标 ## 🎯 入门指南 如果你是 qiankun 或微前端的新手,我们建议按以下顺序开始这些指南: 1. **[样式隔离](/zh-CN/cookbook/style-isolation)** - 防止 CSS 冲突的必备知识 2. **[错误处理](/zh-CN/cookbook/error-handling)** - 生产稳定性的关键 3. **[性能优化](/zh-CN/cookbook/performance)** - 用户体验的重要因素 4. **[调试和开发](/zh-CN/cookbook/debugging)** - 提高开发效率 ## 🏗️ 常见模式 ### 微前端架构模式 ```mermaid graph TB A[主应用] --> B[用户管理] A --> C[产品目录] A --> D[购物车] A --> E[订单处理] B --> F[用户服务] C --> G[产品服务] D --> H[购物车服务] E --> I[订单服务] F --> J[用户数据库] G --> K[产品数据库] H --> L[会话存储] I --> M[订单数据库] ``` ### 通信模式 ```mermaid sequenceDiagram participant M as 主应用 participant A as 微应用 A participant B as 微应用 B participant S as 共享存储 M->>A: 加载并挂载 A->>S: 订阅事件 M->>B: 加载并挂载 B->>S: 订阅事件 A->>S: 发出事件 S->>B: 通知事件 B->>S: 更新状态 S->>A: 广播更新 ``` ## 🎪 真实世界示例 ### 电商平台 典型的电商平台可能结构如下: - **主应用**:导航、布局、用户会话 - **产品目录**:浏览和搜索产品 - **购物车**:管理购物车项目和结账 - **用户账户**:个人资料管理和订单历史 - **管理面板**:内容管理和分析 ### 企业仪表板 企业仪表板可能包括: - **主框架**:身份验证和导航 - **分析模块**:商业智能和报告 - **用户管理**:角色和权限管理 - **内容管理**:动态内容编辑 - **设置模块**:系统配置 ## ⚠️ 常见陷阱 ### 1. 过度工程化 **问题**:为小功能创建太多微应用。 **解决方案**:从单体开始,当团队或领域自然分离时再提取微应用。 ### 2. 共享依赖 **问题**:微应用共享依赖导致版本冲突。 **解决方案**:使用适当的打包策略,考虑为共享库使用模块联邦。 ### 3. 性能问题 **问题**:多个微应用同时加载导致性能下降。 **解决方案**:实施懒加载、适当的缓存和资源优化。 ### 4. 测试复杂性 **问题**:独立测试微应用无法捕获集成问题。 **解决方案**:在单元测试的基础上实施全面的集成测试。 ## 🔧 开发工作流 ### 推荐的开发流程 1. **设计阶段** - 定义应用边界 - 规划通信模式 - 设计共享接口 2. **开发阶段** - 设置开发环境 - 实现微应用 - 配置构建和部署 3. **测试阶段** - 单独的应用单元测试 - 完整系统集成测试 - 性能和安全测试 4. **部署阶段** - 独立部署应用 - 监控应用健康 - 实施回滚策略 ### 团队组织 ```mermaid graph LR A[平台团队] --> B[共享基础设施] A --> C[主应用] D[功能团队 1] --> E[微应用 1] F[功能团队 2] --> G[微应用 2] H[功能团队 3] --> I[微应用 3] B --> E B --> G B --> I ``` ## 📖 进一步阅读 - [微前端架构](https://micro-frontends.org/) - [模块联邦](https://webpack.js.org/concepts/module-federation/) - [Single-SPA 文档](https://single-spa.js.org/) - [qiankun GitHub 仓库](https://github.com/umijs/qiankun) ## 🤝 贡献 有想要分享的模式或实践吗?欢迎为手册贡献内容!请遵循我们的[贡献指南](https://github.com/umijs/qiankun/blob/master/CONTRIBUTING.md)。 ## 🔗 相关文档 - [API 参考](/zh-CN/api/) - 完整的 API 文档 - [快速开始指南](/zh-CN/guide/quick-start) - qiankun 入门 - [生态系统](/zh-CN/ecosystem/) - UI 绑定和工具 ================================================ FILE: docs/zh-CN/cookbook/performance.md ================================================ # 性能优化 对于微前端应用来说,性能至关重要。由于多个应用同时加载和运行,优化资源加载、运行时性能和用户体验非常重要。本指南涵盖了优化基于 qiankun 的微前端应用的综合策略。 ## 🎯 性能概述 ### 常见性能挑战 微前端架构引入了独特的性能考虑因素: - **多包加载**:每个微应用都加载自己的 JavaScript 和 CSS - **资源重复**:共享依赖被多次加载 - **运行时开销**:多个应用实例同时运行 - **网络延迟**:每个微应用的额外 HTTP 请求 - **内存使用**:多个应用导致内存消耗增加 ### 需要监控的性能指标 ```javascript // 微前端性能的关键指标 const performanceMetrics = { // 加载性能 timeToFirstByte: 'TTFB', firstContentfulPaint: 'FCP', largestContentfulPaint: 'LCP', // 交互性 firstInputDelay: 'FID', timeToInteractive: 'TTI', // 微前端特定 microAppLoadTime: '自定义指标', microAppMountTime: '自定义指标', totalBundleSize: '自定义指标' }; ``` ## 🚀 资源加载优化 ### 预取策略 qiankun 提供了几种预取选项来改善加载性能: #### 基础预取 ```javascript import { start } from 'qiankun'; start({ prefetch: true // 启用默认预取 }); ``` #### 选择性预取 ```javascript start({ prefetch: ['critical-app-1', 'critical-app-2'] // 只预取特定应用 }); ``` #### 智能预取 ```javascript start({ prefetch: (apps) => { // 基于用户行为、时间或网络条件的自定义预取逻辑 const now = new Date().getHours(); const isBusinessHours = now >= 9 && now <= 17; if (isBusinessHours) { return { criticalAppNames: ['dashboard', 'user-management'], minorAppsName: ['analytics'] }; } return { criticalAppNames: ['dashboard'], minorAppsName: [] }; } }); ``` #### 网络感知预取 ```javascript // 基于网络条件的高级预取 const networkAwarePrefetch = (apps) => { const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection; if (!connection) { // 未知连接的默认行为 return { criticalAppNames: apps.slice(0, 2), minorAppsName: [] }; } const effectiveType = connection.effectiveType; const saveData = connection.saveData; if (saveData || effectiveType === 'slow-2g' || effectiveType === '2g') { // 慢速连接的最小预取 return { criticalAppNames: [], minorAppsName: [] }; } if (effectiveType === '3g') { // 3G 的适度预取 return { criticalAppNames: apps.slice(0, 1), minorAppsName: [] }; } // 4G 及以上的积极预取 return { criticalAppNames: apps.slice(0, 3), minorAppsName: apps.slice(3) }; }; start({ prefetch: networkAwarePrefetch }); ``` ### 懒加载 #### 基于路由的懒加载 ```javascript // 只在访问路由时加载微应用 registerMicroApps([ { name: 'user-management', entry: '//localhost:8080', container: '#container', activeRule: '/users', // 应用只在访问 /users 路由时加载 }, { name: 'analytics', entry: '//localhost:8081', container: '#container', activeRule: '/analytics', // 按需加载 } ]); ``` #### 条件加载 ```javascript // 基于用户权限或功能加载微应用 const userPermissions = getCurrentUserPermissions(); const microApps = [ { name: 'dashboard', entry: '//localhost:8080', container: '#container', activeRule: '/dashboard' } ]; // 有条件地添加管理应用 if (userPermissions.includes('admin')) { microApps.push({ name: 'admin-panel', entry: '//localhost:8082', container: '#container', activeRule: '/admin' }); } registerMicroApps(microApps); ``` #### 使用 Intersection Observer 进行懒加载 ```javascript // 当微应用进入视口时加载 const observerCallback = (entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting) { const appName = entry.target.dataset.app; loadMicroApp({ name: appName, entry: entry.target.dataset.entry, container: entry.target }); observer.unobserve(entry.target); } }); }; const observer = new IntersectionObserver(observerCallback, { threshold: 0.1 }); document.querySelectorAll('[data-lazy-app]').forEach(el => { observer.observe(el); }); ``` ## 📦 打包优化 ### 代码分割 #### 微应用级别分割 ```javascript // 微应用的 webpack.config.js module.exports = { optimization: { splitChunks: { chunks: 'all', cacheGroups: { vendor: { test: /[\\/]node_modules[\\/]/, name: 'vendors', chunks: 'all', priority: 10 }, common: { name: 'common', minChunks: 2, chunks: 'all', priority: 5 } } } } }; ``` #### 微应用中的动态导入 ```javascript // 带动态导入的 React 组件 import React, { Suspense, lazy } from 'react'; const HeavyComponent = lazy(() => import('./HeavyComponent')); function MyMicroApp() { return (

微应用

加载重组件中...
}>
); } ``` ### 共享依赖 #### 外部依赖 ```javascript // webpack.config.js - 外部化共享库 module.exports = { externals: { 'react': 'React', 'react-dom': 'ReactDOM', 'lodash': '_', 'moment': 'moment' } }; ``` #### 模块联邦 ```javascript // 主应用的 webpack.config.js const ModuleFederationPlugin = require('@module-federation/webpack'); module.exports = { plugins: [ new ModuleFederationPlugin({ name: 'shell', shared: { react: { singleton: true, requiredVersion: '^18.0.0' }, 'react-dom': { singleton: true, requiredVersion: '^18.0.0' } } }) ] }; // 微应用的 webpack.config.js module.exports = { plugins: [ new ModuleFederationPlugin({ name: 'microApp', shared: { react: { singleton: true, requiredVersion: '^18.0.0' }, 'react-dom': { singleton: true, requiredVersion: '^18.0.0' } } }) ] }; ``` ## 🏎️ 运行时性能 ### 内存管理 #### 卸载时清理 ```javascript // 生命周期钩子中的适当清理 const lifeCycles = { async afterUnmount(app) { // 清除定时器 if (window.microAppTimers) { window.microAppTimers.forEach(timer => clearInterval(timer)); window.microAppTimers = []; } // 移除事件监听器 if (window.microAppListeners) { window.microAppListeners.forEach(({ element, event, handler }) => { element.removeEventListener(event, handler); }); window.microAppListeners = []; } // 清除缓存 if (window.microAppCache) { window.microAppCache.clear(); } // 强制垃圾回收(如果可用) if (window.gc) { window.gc(); } } }; ``` #### 内存泄漏检测 ```javascript // 监控内存使用情况 const memoryMonitor = { baseline: null, measureBaseline() { this.baseline = performance.memory ? { usedJSHeapSize: performance.memory.usedJSHeapSize, totalJSHeapSize: performance.memory.totalJSHeapSize } : null; }, checkForLeaks(appName) { if (!performance.memory || !this.baseline) return; const current = { usedJSHeapSize: performance.memory.usedJSHeapSize, totalJSHeapSize: performance.memory.totalJSHeapSize }; const growth = current.usedJSHeapSize - this.baseline.usedJSHeapSize; const growthMB = growth / (1024 * 1024); if (growthMB > 50) { // 如果内存增长超过50MB则警告 console.warn(`${appName} 中可能存在内存泄漏: ${growthMB.toFixed(2)}MB 增长`); } } }; ``` ### 虚拟 DOM 优化 #### React 优化 ```javascript // 优化 React 微应用 import React, { memo, useMemo, useCallback } from 'react'; const OptimizedComponent = memo(({ data, onUpdate }) => { // 记忆化昂贵的计算 const processedData = useMemo(() => { return data.map(item => ({ ...item, calculated: expensiveCalculation(item) })); }, [data]); // 记忆化事件处理器 const handleUpdate = useCallback((id, newValue) => { onUpdate(id, newValue); }, [onUpdate]); return (
{processedData.map(item => ( ))}
); }); ``` #### Vue 优化 ```vue ``` ## 🗄️ 缓存策略 ### HTTP 缓存 #### 微应用资源 ```javascript // 为微应用资源配置缓存头 // nginx.conf server { location ~* \.(js|css|png|jpg|jpeg|gif|svg|woff|woff2)$ { expires 1y; add_header Cache-Control "public, immutable"; add_header Vary "Accept-Encoding"; } location /api/ { expires -1; add_header Cache-Control "no-cache, no-store, must-revalidate"; } } ``` #### Service Worker 缓存 ```javascript // sw.js - 微应用缓存的 Service Worker const CACHE_NAME = 'micro-app-cache-v1'; const MICRO_APP_URLS = [ '/micro-app-1/static/js/main.js', '/micro-app-1/static/css/main.css', '/micro-app-2/static/js/main.js', '/micro-app-2/static/css/main.css' ]; self.addEventListener('install', event => { event.waitUntil( caches.open(CACHE_NAME) .then(cache => cache.addAll(MICRO_APP_URLS)) ); }); self.addEventListener('fetch', event => { if (MICRO_APP_URLS.some(url => event.request.url.includes(url))) { event.respondWith( caches.match(event.request) .then(response => response || fetch(event.request)) ); } }); ``` ### 应用级缓存 #### 智能应用缓存 ```javascript // 缓存微应用实例以便更快地重新挂载 class MicroAppCache { constructor() { this.cache = new Map(); this.maxSize = 5; } set(appName, appInstance) { if (this.cache.size >= this.maxSize) { // 移除最少使用的应用 const firstKey = this.cache.keys().next().value; this.cache.delete(firstKey); } this.cache.set(appName, { instance: appInstance, timestamp: Date.now() }); } get(appName) { const cached = this.cache.get(appName); if (cached) { // 移动到末尾(标记为最近使用) this.cache.delete(appName); this.cache.set(appName, cached); return cached.instance; } return null; } has(appName) { return this.cache.has(appName); } clear() { this.cache.clear(); } } const appCache = new MicroAppCache(); ``` ## ⚡ 网络优化 ### 连接优化 #### HTTP/2 推送 ```javascript // 带有微应用资源 HTTP/2 推送的 Express.js 服务器 const express = require('express'); const spdy = require('spdy'); const app = express(); app.get('/main-app', (req, res) => { // 推送关键微应用资源 res.push('/micro-app-1/static/js/main.js'); res.push('/micro-app-1/static/css/main.css'); res.sendFile(__dirname + '/index.html'); }); const server = spdy.createServer(options, app); ``` #### 资源提示 ```html ``` ### CDN 策略 #### 多 CDN 设置 ```javascript // 基于性能的智能 CDN 选择 class CDNManager { constructor() { this.cdns = [ 'https://cdn1.example.com', 'https://cdn2.example.com', 'https://cdn3.example.com' ]; this.performanceCache = new Map(); } async getBestCDN() { if (this.performanceCache.size === 0) { await this.measureCDNPerformance(); } // 返回最快的 CDN return [...this.performanceCache.entries()] .sort((a, b) => a[1] - b[1])[0][0]; } async measureCDNPerformance() { const promises = this.cdns.map(async (cdn) => { const start = performance.now(); try { await fetch(`${cdn}/health-check`); const latency = performance.now() - start; this.performanceCache.set(cdn, latency); } catch (error) { this.performanceCache.set(cdn, Infinity); } }); await Promise.all(promises); } } ``` ## 📊 性能监控 ### 实时指标 #### Performance Observer ```javascript // 监控微应用加载性能 class MicroAppPerformanceMonitor { constructor() { this.metrics = new Map(); this.initObservers(); } initObservers() { // 监控加载性能 if ('PerformanceObserver' in window) { const loadObserver = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { if (entry.name.includes('micro-app')) { this.recordLoadTime(entry); } } }); loadObserver.observe({ entryTypes: ['navigation', 'resource'] }); // 监控布局偏移 const clsObserver = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { if (!entry.hadRecentInput) { this.recordLayoutShift(entry); } } }); clsObserver.observe({ entryTypes: ['layout-shift'] }); } } recordLoadTime(entry) { const appName = this.extractAppName(entry.name); this.metrics.set(`${appName}_load_time`, entry.loadEnd - entry.loadStart); } recordLayoutShift(entry) { const currentCLS = this.metrics.get('cumulative_layout_shift') || 0; this.metrics.set('cumulative_layout_shift', currentCLS + entry.value); } getMetrics() { return Object.fromEntries(this.metrics); } } ``` #### 自定义计时 API ```javascript // 微应用生命周期的自定义计时 class MicroAppTiming { static mark(name) { performance.mark(name); } static measure(name, startMark, endMark) { performance.measure(name, startMark, endMark); // 发送到分析服务 const measure = performance.getEntriesByName(name)[0]; this.sendToAnalytics({ metric: name, duration: measure.duration, timestamp: Date.now() }); } static sendToAnalytics(data) { // 发送到你的分析服务 fetch('/api/analytics/performance', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); } } // 微应用生命周期中的使用 const lifeCycles = { async beforeLoad(app) { MicroAppTiming.mark(`${app.name}_load_start`); }, async afterMount(app) { MicroAppTiming.mark(`${app.name}_mount_end`); MicroAppTiming.measure( `${app.name}_total_time`, `${app.name}_load_start`, `${app.name}_mount_end` ); } }; ``` ### 性能分析 #### 用户体验指标 ```javascript // 跟踪微应用的用户体验指标 class UXMetrics { constructor() { this.metrics = {}; this.initTracking(); } initTracking() { // 微应用的可交互时间 this.trackTimeToInteractive(); // 用户参与度指标 this.trackUserEngagement(); // 错误率 this.trackErrorRates(); } trackTimeToInteractive() { const observer = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { if (entry.entryType === 'measure' && entry.name.includes('tti')) { this.metrics.timeToInteractive = entry.duration; } } }); observer.observe({ entryTypes: ['measure'] }); } trackUserEngagement() { let interactions = 0; ['click', 'scroll', 'keydown'].forEach(event => { document.addEventListener(event, () => { interactions++; this.metrics.interactions = interactions; }); }); } trackErrorRates() { window.addEventListener('error', (event) => { const appName = this.getAppFromError(event); this.metrics.errors = this.metrics.errors || {}; this.metrics.errors[appName] = (this.metrics.errors[appName] || 0) + 1; }); } } ``` ## 🎨 UI/UX 性能 ### 加载状态 #### 骨架屏加载 ```jsx // 微应用加载的 React 骨架屏组件 import React from 'react'; const MicroAppSkeleton = ({ appName }) => { return (
); }; // 微应用加载使用示例 function MicroAppContainer({ appName, entry }) { const [loading, setLoading] = useState(true); const [app, setApp] = useState(null); useEffect(() => { loadMicroApp({ name: appName, entry, container: '#micro-app-container' }).then(() => { setLoading(false); }); }, [appName, entry]); if (loading) { return ; } return
; } ``` #### 渐进式增强 ```javascript // 微应用的渐进式增强 class ProgressiveLoader { constructor(container, config) { this.container = container; this.config = config; this.loadingStates = ['initial', 'skeleton', 'partial', 'complete']; this.currentState = 'initial'; } async load() { // 显示初始加载状态 this.setState('skeleton'); this.renderSkeleton(); try { // 首先加载关键 CSS await this.loadCriticalCSS(); // 显示部分内容 this.setState('partial'); await this.loadCriticalJS(); // 加载剩余资源 await this.loadRemainingAssets(); // 完成加载 this.setState('complete'); this.mountApp(); } catch (error) { this.handleLoadError(error); } } renderSkeleton() { this.container.innerHTML = this.config.skeletonHTML; } async loadCriticalCSS() { const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = `${this.config.entry}/critical.css`; return new Promise((resolve, reject) => { link.onload = resolve; link.onerror = reject; document.head.appendChild(link); }); } } ``` ### 动画性能 #### 硬件加速 ```css /* 优化微应用过渡动画 */ .micro-app-transition { /* 使用 transform 而不是改变布局属性 */ transform: translateX(0); transition: transform 0.3s ease-out; /* 启用硬件加速 */ will-change: transform; /* 使用 GPU 合成 */ transform: translateZ(0); } .micro-app-enter { transform: translateX(100%); } .micro-app-enter-active { transform: translateX(0); } .micro-app-exit { transform: translateX(0); } .micro-app-exit-active { transform: translateX(-100%); } ``` #### 使用 Intersection Observer 进行动画 ```javascript // 使用 Intersection Observer 高效触发动画 class AnimationManager { constructor() { this.observer = new IntersectionObserver( this.handleIntersection.bind(this), { threshold: 0.1 } ); } observe(element) { this.observer.observe(element); } handleIntersection(entries) { entries.forEach(entry => { if (entry.isIntersecting) { this.triggerAnimation(entry.target); this.observer.unobserve(entry.target); } }); } triggerAnimation(element) { // 使用 CSS 类进行硬件加速动画 element.classList.add('animate-in'); // 或使用 Web Animations API 进行复杂动画 element.animate([ { opacity: 0, transform: 'translateY(20px)' }, { opacity: 1, transform: 'translateY(0)' } ], { duration: 300, easing: 'ease-out' }); } } ``` ## 📱 移动端性能 ### 移动端特定优化 #### 触摸事件优化 ```javascript // 优化移动端微应用的触摸事件 class TouchOptimizer { constructor() { this.setupPassiveListeners(); this.optimizeTouchHandling(); } setupPassiveListeners() { // 使用被动监听器提高滚动性能 document.addEventListener('touchstart', this.handleTouchStart, { passive: true }); document.addEventListener('touchmove', this.handleTouchMove, { passive: true }); } optimizeTouchHandling() { // 防抖触摸事件 let touchTimeout; document.addEventListener('touchend', () => { clearTimeout(touchTimeout); touchTimeout = setTimeout(() => { // 延迟处理触摸结束以防止意外双击 }, 300); }); } handleTouchStart(event) { // 触摸开始时的最小处理 } handleTouchMove(event) { // 使用 requestAnimationFrame 进行平滑滚动 requestAnimationFrame(() => { // 处理触摸移动 }); } } ``` #### 视口管理 ```javascript // 为不同微应用优化视口 class ViewportManager { constructor() { this.defaultViewport = 'width=device-width, initial-scale=1.0'; this.viewportMeta = document.querySelector('meta[name="viewport"]'); } setViewportForApp(appName) { const appViewports = { 'mobile-first-app': 'width=device-width, initial-scale=1.0, user-scalable=no', 'desktop-app': 'width=1024, initial-scale=0.5', 'responsive-app': 'width=device-width, initial-scale=1.0' }; const viewport = appViewports[appName] || this.defaultViewport; this.viewportMeta.setAttribute('content', viewport); } resetViewport() { this.viewportMeta.setAttribute('content', this.defaultViewport); } } ``` ## 🔧 开发环境 vs 生产环境优化 ### 环境特定配置 #### 开发环境优化 ```javascript // 微应用的 webpack.config.dev.js module.exports = { mode: 'development', optimization: { // 禁用压缩以加快构建速度 minimize: false, // 分割块以便更好地调试 splitChunks: { chunks: 'all', cacheGroups: { vendor: { test: /[\\/]node_modules[\\/]/, name: 'vendors', chunks: 'all' } } } }, devServer: { // 启用热重载 hot: true, // 为开发优化 liveReload: true, // 微应用通信的 CORS headers: { 'Access-Control-Allow-Origin': '*' } } }; ``` #### 生产环境优化 ```javascript // 微应用的 webpack.config.prod.js const CompressionPlugin = require('compression-webpack-plugin'); const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); module.exports = { mode: 'production', optimization: { // 启用所有优化 minimize: true, sideEffects: false, // 高级块分割 splitChunks: { chunks: 'all', maxSize: 244000, cacheGroups: { vendor: { test: /[\\/]node_modules[\\/]/, name: 'vendors', chunks: 'all', priority: 10 }, common: { name: 'common', minChunks: 2, chunks: 'all', priority: 5 } } } }, plugins: [ // Gzip 压缩 new CompressionPlugin({ algorithm: 'gzip', test: /\.(js|css|html|svg)$/, threshold: 8192, minRatio: 0.8 }), // 包分析(可选) new BundleAnalyzerPlugin({ analyzerMode: 'static', openAnalyzer: false }) ] }; ``` ## 🎯 性能最佳实践总结 ### ✅ 应该做的 1. **为关键微应用实施预取** 2. **在微应用内使用代码分割** 3. **在多个级别利用缓存** 4. **持续监控性能** 5. **优化移动端体验** 6. **对非关键功能使用懒加载** 7. **在卸载钩子中实施适当清理** 8. **高效共享依赖** 9. **使用 Service Worker 进行缓存** 10. **使用硬件加速优化动画** ### ❌ 不应该做的 1. **不要一次加载所有微应用** 2. **不要忽略包大小** 3. **不要重复大型依赖** 4. **不要忘记内存清理** 5. **不要阻塞主线程** 6. **不要忽略移动端性能** 7. **不要在慢速连接上过度预取** 8. **不要使用同步操作** 9. **不要忽略错误边界** 10. **不要跳过性能监控** ### 📊 性能检查清单 ```javascript // 微应用性能审计检查清单 const performanceChecklist = { loading: { prefetchStrategy: '✓ 已实施智能预取', bundleSize: '✓ 包大小小于250KB gzipped', codesplitting: '✓ 关键路径已分离', caching: '✓ 启用积极缓存' }, runtime: { memoryLeaks: '✓ 已实施清理', animationPerf: '✓ 使用硬件加速', eventOptimization: '✓ 使用被动监听器', lazyLoading: '✓ 非关键功能懒加载' }, monitoring: { coreWebVitals: '✓ 监控 LCP、FID、CLS', customMetrics: '✓ 跟踪应用特定指标', errorTracking: '✓ 记录性能错误', analytics: '✓ 测量用户体验' } }; ``` ## 🔗 相关文档 - [样式隔离](/cookbook/style-isolation) - CSS 性能和隔离 - [错误处理](/cookbook/error-handling) - 错误的性能影响 - [配置](/api/configuration) - 性能相关配置 - [调试](/cookbook/debugging) - 性能调试技术 ================================================ FILE: docs/zh-CN/cookbook/style-isolation.md ================================================ # 样式隔离 样式隔离是微前端架构中最关键的方面之一。当多个应用在同一浏览器上下文中运行时,CSS 冲突可能导致视觉不一致和布局错误。本指南涵盖了在 qiankun 应用中实现有效样式隔离的各种策略。 ## 🎯 理解问题 ### 微前端中的 CSS 冲突 当多个微应用加载到同一页面时,它们共享相同的全局 CSS 命名空间。这可能导致: - **样式覆盖**:后加载的应用覆盖早期样式 - **选择器冲突**:相同的类名造成意外样式 - **全局污染**:微应用影响主应用样式 - **布局破坏**:由于样式冲突导致的意外布局变化 ### CSS 冲突示例 ```css /* 主应用 */ .button { background: blue; padding: 10px; } /* 微应用 */ .button { background: red; /* 这将覆盖主应用样式! */ border: none; } ``` ## 🛡️ qiankun 的内置样式隔离 qiankun 提供了几种内置的样式隔离机制,您可以通过配置启用。 ### 严格样式隔离 使用 Shadow DOM 的最强大隔离方法: ```javascript import { start } from 'qiankun'; start({ sandbox: { strictStyleIsolation: true } }); ``` **工作原理:** - 为每个微应用创建 Shadow DOM - 完全隔离应用之间的样式 - 防止任何样式泄漏 **优点:** - 完全样式隔离 - 不可能发生 CSS 冲突 - 易于实现 **缺点:** - 一些第三方库可能无法正常工作 - 调试可能更复杂 - 大型应用的性能开销 ### 实验性样式隔离 使用 CSS 作用域的侵入性较小的方法: ```javascript import { start } from 'qiankun'; start({ sandbox: { experimentalStyleIsolation: true } }); ``` **工作原理:** - 为 CSS 选择器添加唯一前缀 - 将样式作用域限定为微应用容器 - 保持 DOM 结构 **优点:** - 更好的第三方库兼容性 - 更容易调试 - 较少的性能开销 **缺点:** - 不如严格隔离强大 - 一些边缘情况可能仍会导致冲突 ## 🎨 CSS-in-JS 解决方案 CSS-in-JS 库通过生成唯一的类名提供天然的样式隔离。 ### Styled Components ```jsx // 使用 Styled Components 的微应用 import styled from 'styled-components'; const Button = styled.button` background: ${props => props.primary ? 'blue' : 'grey'}; padding: 10px 20px; border: none; border-radius: 4px; &:hover { opacity: 0.8; } `; function MyComponent() { return (
); } ``` ### Emotion ```jsx /** @jsxImportSource @emotion/react */ import { css } from '@emotion/react'; const buttonStyle = css` background: blue; color: white; padding: 10px 20px; border: none; border-radius: 4px; &:hover { background: darkblue; } `; function MyComponent() { return ; } ``` ### 动态导入的 CSS 模块 ```css /* Button.module.css */ .button { background: blue; color: white; padding: 10px 20px; border: none; border-radius: 4px; } .button:hover { background: darkblue; } ``` ```jsx // Button.jsx import styles from './Button.module.css'; function Button({ children, ...props }) { return ( ); } ``` ## 🏗️ CSS 作用域策略 ### BEM 方法论 使用 Block、Element、Modifier 命名约定避免冲突: ```css /* 主应用 */ .main-app__button { background: blue; } .main-app__button--primary { background: darkblue; } /* 微应用 */ .micro-app__button { background: red; } .micro-app__button--large { padding: 15px 30px; } ``` ### 命名空间前缀 为所有 CSS 类添加唯一前缀: ```css /* 用户管理微应用 */ .user-mgmt-container { } .user-mgmt-header { } .user-mgmt-button { } /* 产品目录微应用 */ .product-cat-container { } .product-cat-header { } .product-cat-button { } ``` ### CSS 自定义属性(变量) 使用 CSS 变量实现一致的主题,避免冲突: ```css /* 主应用 - 定义主题变量 */ :root { --primary-color: #007bff; --secondary-color: #6c757d; --success-color: #28a745; --danger-color: #dc3545; } /* 微应用 - 使用主题变量 */ .micro-app-button { background: var(--primary-color); color: white; } ``` ## 🎭 运行时样式管理 ### 动态 CSS 加载 在微应用挂载时动态加载 CSS: ```javascript // 动态 CSS 加载的生命周期钩子 const lifeCycles = { async beforeMount(app) { // 加载微应用特定的 CSS const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = `${app.entry}/static/css/main.css`; link.id = `${app.name}-styles`; document.head.appendChild(link); }, async afterUnmount(app) { // 移除微应用 CSS const link = document.getElementById(`${app.name}-styles`); if (link) { document.head.removeChild(link); } } }; registerMicroApps([ { name: 'micro-app', entry: '//localhost:8080', container: '#container', activeRule: '/micro-app' } ], lifeCycles); ``` ### 使用 PostCSS 进行 CSS 作用域 使用 PostCSS 插件自动为 CSS 添加作用域: ```javascript // postcss.config.js module.exports = { plugins: [ require('postcss-prefixwrap')('.micro-app-container') ] }; ``` **输入 CSS:** ```css .button { background: blue; } ``` **输出 CSS:** ```css .micro-app-container .button { background: blue; } ``` ## 🔧 框架特定解决方案 ### React 应用 #### 在 Create React App 中使用 CSS 模块 ```javascript // Button.module.css .button { background: blue; color: white; } // Button.jsx import styles from './Button.module.css'; function Button({ children }) { return ; } ``` #### 自定义 CSS 前缀 Hook ```javascript import { useMemo } from 'react'; function useCSSPrefix(prefix) { return useMemo(() => { return (className) => `${prefix}-${className}`; }, [prefix]); } // 用法 function MyComponent() { const cx = useCSSPrefix('user-mgmt'); return (
); } ``` ### Vue 应用 #### 作用域样式 ```vue ``` #### Vue 中的 CSS 模块 ```vue ``` ### Angular 应用 #### ViewEncapsulation ```typescript import { Component, ViewEncapsulation } from '@angular/core'; @Component({ selector: 'app-my-component', template: `
`, styles: [` .component { padding: 20px; } .button { background: blue; color: white; } `], encapsulation: ViewEncapsulation.Emulated // 默认 }) export class MyComponent { } ``` ## 🎪 组件库策略 ### 设计系统方法 创建所有微应用都使用的共享设计系统: ```javascript // shared-design-system/Button.js export const Button = ({ variant = 'primary', size = 'medium', children, ...props }) => { const baseClasses = 'btn'; const variantClasses = { primary: 'btn-primary', secondary: 'btn-secondary' }; const sizeClasses = { small: 'btn-sm', medium: 'btn-md', large: 'btn-lg' }; const className = [ baseClasses, variantClasses[variant], sizeClasses[size] ].join(' '); return ; }; ``` ### 用于主题化的 CSS 自定义属性 ```css /* 设计系统 CSS */ :root { --btn-primary-bg: #007bff; --btn-primary-color: white; --btn-secondary-bg: #6c757d; --btn-secondary-color: white; --btn-padding-sm: 8px 12px; --btn-padding-md: 10px 16px; --btn-padding-lg: 12px 20px; } .btn { border: none; border-radius: 4px; cursor: pointer; font-family: inherit; } .btn-primary { background: var(--btn-primary-bg); color: var(--btn-primary-color); } .btn-secondary { background: var(--btn-secondary-bg); color: var(--btn-secondary-color); } .btn-sm { padding: var(--btn-padding-sm); } .btn-md { padding: var(--btn-padding-md); } .btn-lg { padding: var(--btn-padding-lg); } ``` ## 🚫 第三方库处理 ### 隔离第三方样式 当使用注入全局样式的第三方库时: ```javascript // 在隔离环境中加载第三方 CSS const loadLibraryStyles = (libraryName, cssUrl) => { return new Promise((resolve) => { const iframe = document.createElement('iframe'); iframe.style.display = 'none'; document.body.appendChild(iframe); const iframeDoc = iframe.contentDocument; const link = iframeDoc.createElement('link'); link.rel = 'stylesheet'; link.href = cssUrl; link.onload = () => { // 提取并为 CSS 添加作用域 const styles = Array.from(iframeDoc.styleSheets[0].cssRules) .map(rule => rule.cssText) .join('\n'); const scopedStyles = scopeCSS(styles, `.${libraryName}-container`); const style = document.createElement('style'); style.textContent = scopedStyles; style.id = `${libraryName}-scoped-styles`; document.head.appendChild(style); document.body.removeChild(iframe); resolve(); }; iframeDoc.head.appendChild(link); }); }; ``` ### 第三方集成的 CSS-in-JS ```javascript import { createGlobalStyle } from 'styled-components'; // 将第三方样式作用域限定为特定容器 const AntdStyles = createGlobalStyle` .micro-app-container { .ant-btn { /* 专门为此微应用覆盖 Ant Design 按钮样式 */ border-radius: 8px; } .ant-table { /* 覆盖 Ant Design 表格样式 */ border: 1px solid #f0f0f0; } } `; function MicroApp() { return (
{/* 您的微应用内容 */}
); } ``` ## 🔍 调试样式冲突 ### 开发工具 #### 样式检查脚本 ```javascript // 添加到浏览器控制台进行调试 const findStyleConflicts = (selector) => { const elements = document.querySelectorAll(selector); elements.forEach((el, index) => { const styles = window.getComputedStyle(el); const rules = []; for (let i = 0; i < document.styleSheets.length; i++) { try { const sheet = document.styleSheets[i]; const cssRules = sheet.cssRules || sheet.rules; for (let j = 0; j < cssRules.length; j++) { if (el.matches(cssRules[j].selectorText)) { rules.push({ selector: cssRules[j].selectorText, rule: cssRules[j].cssText, sheet: sheet.href || 'inline' }); } } } catch (e) { // 跨域样式表 } } console.log(`元素 ${index + 1}:`, el); console.log('应用的样式:', rules); }); }; // 用法: findStyleConflicts('.button'); ``` #### 样式来源跟踪器 ```javascript // 跟踪哪个微应用加载了哪些样式 const styleTracker = { styles: new Map(), track(appName, styleElement) { if (!this.styles.has(appName)) { this.styles.set(appName, []); } this.styles.get(appName).push(styleElement); }, getByApp(appName) { return this.styles.get(appName) || []; }, getConflicts() { const allSelectors = new Map(); this.styles.forEach((styles, appName) => { styles.forEach(style => { // 解析 CSS 并检查选择器冲突 // 实现取决于 CSS 解析器 }); }); return allSelectors; } }; ``` ### 运行时冲突检测 ```javascript // 在运行时检测样式冲突 const detectStyleConflicts = () => { const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.type === 'childList') { mutation.addedNodes.forEach((node) => { if (node.tagName === 'STYLE' || node.tagName === 'LINK') { checkForConflicts(node); } }); } }); }); observer.observe(document.head, { childList: true }); }; const checkForConflicts = (styleNode) => { // 检查 CSS 选择器冲突的实现 console.warn('添加了新的样式节点:', styleNode); }; ``` ## 📊 性能考虑 ### CSS 加载性能 ```javascript // 优化微应用的 CSS 加载 const optimizedCSSLoader = { cache: new Map(), async loadCSS(url, appName) { if (this.cache.has(url)) { return this.cache.get(url); } const response = await fetch(url); const css = await response.text(); const optimizedCSS = this.optimizeCSS(css, appName); this.cache.set(url, optimizedCSS); return optimizedCSS; }, optimizeCSS(css, appName) { // 移除未使用的选择器 // 添加应用特定前缀 // 必要时压缩 return css.replace(/(\.[a-zA-Z-_]+)/g, `.${appName}-$1`); } }; ``` ### 样式的包分割 ```javascript // webpack.config.js - 按微应用分割 CSS module.exports = { optimization: { splitChunks: { cacheGroups: { styles: { name: 'styles', test: /\.css$/, chunks: 'all', enforce: true } } } } }; ``` ## 🎯 最佳实践总结 ### ✅ 应该做的 1. **尽可能使用 qiankun 的内置隔离** 2. **实施一致的命名约定**(BEM、命名空间) 3. **使用 CSS-in-JS** 进行自动隔离 4. **创建共享设计系统** 以保持一致性 5. **在开发中测试样式隔离** 6. **在生产中监控冲突** 7. **为您的团队记录样式指南** ### ❌ 不应该做的 1. **不要在微应用中依赖全局 CSS** 2. **不要使用过于通用的类名**(.button、.container) 3. **不要忘记在卸载时清理样式** 4. **不要忽略第三方库样式** 5. **不要跳过样式隔离测试** 6. **除非绝对必要,不要使用 !important** ### 🎪 真实世界示例 这是一个良好隔离的微应用的完整示例: ```jsx // MicroApp.jsx import React from 'react'; import styled, { createGlobalStyle } from 'styled-components'; // 作用域限定为此微应用的全局样式 const GlobalStyles = createGlobalStyle` .user-management-app { font-family: 'Inter', sans-serif; * { box-sizing: border-box; } } `; // 带隔离的样式组件 const Container = styled.div` padding: 20px; background: #f8f9fa; min-height: 100vh; `; const Header = styled.h1` color: #343a40; margin-bottom: 20px; `; const Button = styled.button` background: ${props => props.primary ? '#007bff' : '#6c757d'}; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; &:hover { opacity: 0.9; } `; function UserManagementApp() { return (
用户管理
); } export default UserManagementApp; ``` ## 🔗 相关文档 - [性能优化](/cookbook/performance) - 优化加载和运行时性能 - [错误处理](/cookbook/error-handling) - 处理 CSS 相关错误 - [配置](/api/configuration) - 配置样式隔离选项 - [调试](/cookbook/debugging) - 调试样式冲突 ================================================ FILE: docs/zh-CN/ecosystem/create-qiankun.md ================================================ # Create Qiankun `create-qiankun` 是一个专为 qiankun 微前端框架设计的 CLI 脚手架工具。它帮助开发者快速构建示例项目,高效开始微前端开发。 ## 🚀 快速开始 ### 使用 npm ```bash npx create-qiankun@latest ``` ### 使用 yarn ```bash yarn create qiankun@latest ``` ### 使用 pnpm ```bash pnpm dlx create-qiankun@latest ``` ## 🎯 特性 - **多种项目类型**:选择仅主应用、仅微应用或完整设置 - **框架支持**:React 18、Vue 3、Vue 2 和 Umi 4 模板 - **路由模式**:支持 hash 和 history 路由模式 - **包管理器选项**:npm、yarn、pnpm 或 pnpm workspace - **自动配置**:自动端口冲突检测和启动脚本注入 - **Monorepo 支持**:内置 pnpm workspace 设置管理多个应用 ## 📋 要求 - **Node.js**:v18 或更高版本(推荐:使用 [fnm](https://github.com/Schniz/fnm) 进行版本管理) - **包管理器**:npm、yarn 或 pnpm ## 🎮 交互式设置 运行 `create-qiankun` 时,您将通过交互式设置过程: ### 步骤 1:项目名称 ```bash ? Project name: › my-qiankun-project ``` ### 步骤 2:项目类型 ```bash ? Choose a way to create › ❯ Create main application and sub applications Just create main application Just create sub applications ``` **选项:** - **Create main application and sub applications**:完整设置,包含主应用和多个微应用 - **Just create main application**:仅创建主(shell)应用 - **Just create sub applications**:仅创建微应用 ### 步骤 3:主应用框架(如适用) ```bash ? Choose a framework for your main application › ❯ React18+Webpack Vue3+Webpack React18+umi4 ``` ### 步骤 4:路由模式(如适用) ```bash ? Choose a route pattern for your main application › ❯ hash history ``` ### 步骤 5:子应用框架(如适用) ```bash ? Choose a framework for your sub application › Space to select. Return to submit. ❯◯ React18+Webpack ◯ Vue3+Webpack ◯ Vue2+Webpack ◯ React18+umi4 ``` ### 步骤 6:包管理器 ```bash ? Which package manager do you want to use? › ❯ npm yarn pnpm pnpm with workspace ``` ## 📦 可用模板 ### 主应用模板 | 模板 | 描述 | 特性 | |----------|-------------|----------| | **React18+Webpack** | React 18 with Webpack 5 | 现代 React、TypeScript 支持、热重载 | | **Vue3+Webpack** | Vue 3 with Vue CLI | Composition API、TypeScript、Element Plus | | **React18+umi4** | Umi 4 框架 | 内置 qiankun 支持、Ant Design Pro | ### 子应用模板 | 模板 | 描述 | 状态 | 备注 | |----------|-------------|--------|-------| | **React18+Webpack** | React 18 微应用 | ✅ 稳定 | 生产就绪 | | **Vue3+Webpack** | Vue 3 微应用 | ✅ 稳定 | 生产就绪 | | **Vue2+Webpack** | Vue 2 微应用 | ⚠️ 有限 | pnpm workspace 存在问题 | | **React18+umi4** | Umi 4 微应用 | ✅ 稳定 | 内置微应用支持 | | **Vite+Vue3** | Vue 3 with Vite | 🚧 开发中 | 开发中 | | **Vite+React18** | React 18 with Vite | 🚧 开发中 | 开发中 | ## 🏗️ 项目结构 ### 单一项目结构 ``` my-qiankun-project/ ├── main-app/ # 主应用 │ ├── src/ │ ├── package.json │ └── webpack.config.js ├── react18-sub/ # React 微应用 │ ├── src/ │ ├── package.json │ └── webpack.config.js ├── vue3-sub/ # Vue 微应用 │ ├── src/ │ ├── package.json │ └── vue.config.js └── package.json ``` ### Pnpm Workspace 结构 ``` my-qiankun-project/ ├── packages/ │ ├── main-app/ # 主应用 │ ├── react18-sub/ # React 微应用 │ └── vue3-sub/ # Vue 微应用 ├── package.json # Workspace 配置 ├── pnpm-workspace.yaml # Workspace 定义 └── scripts/ └── checkPnpm.js # 包管理器验证 ``` ## 🔧 生成的配置 ### 主应用配置 主应用自动配置包含: ```typescript // 主应用微应用注册 import { registerMicroApps, start } from 'qiankun'; registerMicroApps([ { name: 'react18-sub', entry: '//localhost:8080', container: '#subapp-viewport', activeRule: '/react18-sub', }, { name: 'vue3-sub', entry: '//localhost:8081', container: '#subapp-viewport', activeRule: '/vue3-sub', } ]); start(); ``` ### 微应用配置 每个微应用包含: **React 微应用:** ```javascript // webpack.config.js const { QiankunWebpackPlugin } = require('@qiankunjs/webpack-plugin'); module.exports = { plugins: [ new QiankunWebpackPlugin() ] }; ``` **Vue 微应用:** ```javascript // vue.config.js const { defineConfig } = require('@vue/cli-service'); const { QiankunWebpackPlugin } = require('@qiankunjs/webpack-plugin'); module.exports = defineConfig({ configureWebpack: { plugins: [ new QiankunWebpackPlugin() ] } }); ``` ### 端口配置 自动端口分配防止冲突: ```json { "scripts": { "dev": "PORT=8080 react-scripts start", "check-port": "node scripts/checkPort.js" } } ``` ## 🎨 自定义选项 ### 环境特定配置 ```javascript // config/development.js module.exports = { microApps: [ { name: 'react-app', entry: '//localhost:8080', activeRule: '/react-app' } ] }; // config/production.js module.exports = { microApps: [ { name: 'react-app', entry: '//app.example.com', activeRule: '/react-app' } ] }; ``` ### 自定义路由 ```typescript // Hash 路由(默认) const router = createRouter({ history: createWebHashHistory(), routes: [...] }); // History 路由 const router = createRouter({ history: createWebHistory(), routes: [...] }); ``` ## 🚀 开发工作流 ### 单一包管理器 ```bash # 启动主应用 cd main-app && npm run dev # 启动微应用(在单独的终端) cd react18-sub && npm run dev cd vue3-sub && npm run dev ``` ### Pnpm Workspace ```bash # 安装所有依赖 pnpm install # 同时启动所有应用 pnpm run dev # 启动特定应用 pnpm --filter main-app run dev pnpm --filter react18-sub run dev ``` ### 生成的脚本 CLI 自动注入有用的脚本: ```json { "scripts": { "dev": "concurrently \"npm run dev:main\" \"npm run dev:subs\"", "dev:main": "cd main-app && npm run dev", "dev:subs": "concurrently \"cd react18-sub && npm run dev\" \"cd vue3-sub && npm run dev\"", "build": "npm run build:main && npm run build:subs", "clean": "rimraf node_modules **/*/node_modules" } } ``` ## 🔧 高级用法 ### 命令行参数 通过提供参数跳过交互式提示: ```bash npx create-qiankun my-project CreateMainAndSubApp react18-main hash react18-webpack-sub,vue3-webpack-sub pnpm ``` **参数顺序:** 1. 项目名称 2. 创建类型 (`CreateMainApp` | `CreateSubApp` | `CreateMainAndSubApp`) 3. 主应用模板(如适用) 4. 路由模式(如适用) 5. 子应用模板(逗号分隔,如适用) 6. 包管理器 ### 批量创建 ```bash # 创建多个项目 for project in app1 app2 app3; do npx create-qiankun $project CreateMainAndSubApp react18-main history react18-webpack-sub pnpm done ``` ### 自定义模板 您可以通过为项目贡献或分叉仓库来使用自定义模板扩展 CLI。 ## 🎯 项目示例 ### 完整的 React + Vue 设置 ```bash npx create-qiankun my-micro-frontend-app # 选择:Create main application and sub applications # 主应用:React18+Webpack # 路由:history # 子应用:React18+Webpack, Vue3+Webpack # 包管理器:pnpm with workspace ``` **结果:** - 带路由的主 React 应用 - React 18 微应用 - Vue 3 微应用 - 自动端口分配(3000、8080、8081) - Workspace 配置 - 开发脚本 ### 基于 Umi 的 Monorepo ```bash npx create-qiankun enterprise-app # 选择:Create main application and sub applications # 主应用:React18+umi4 # 路由:history # 子应用:React18+umi4, Vue3+Webpack # 包管理器:pnpm with workspace ``` **特性:** - 内置 qiankun 支持的 Umi 4 主应用 - Umi 4 微应用 - Vue 3 微应用 - Ant Design Pro 组件 - TypeScript 配置 ## 📚 最佳实践 ### 1. 使用描述性名称 ```bash # ✅ 好:描述性项目名称 npx create-qiankun e-commerce-platform npx create-qiankun admin-dashboard # ❌ 坏:通用名称 npx create-qiankun app1 npx create-qiankun project ``` ### 2. 选择适当的包管理器 ```bash # 对于简单项目 npm / yarn # 对于多团队的 monorepo pnpm with workspace ``` ### 3. 规划路由策略 ```bash # Hash 路由 - 更简单的部署 # History 路由 - 更好的 SEO,需要服务器配置 ``` ### 4. 考虑框架兼容性 - **React + Vue**:适合混合团队 - **相同框架**:更容易的依赖管理 - **Umi**:最适合企业应用 ## 🐛 故障排除 ### 端口冲突 CLI 自动检测和解决端口冲突。如果遇到问题: ```bash # 检查运行中的进程 lsof -i :8080 # 杀死冲突进程 kill -9 $(lsof -t -i:8080) ``` ### Pnpm Workspace 问题 ```bash # 清除 node_modules 并重新安装 pnpm run clean pnpm install # 检查 workspace 配置 cat pnpm-workspace.yaml ``` ### 构建错误 ```bash # 清除构建缓存 rm -rf dist/ build/ .cache/ # 重新安装依赖 rm -rf node_modules package-lock.json npm install ``` ### Vue 2 与 Pnpm Workspace 已知限制:Vue 2 模板与 pnpm workspace 存在兼容性问题。使用替代方法: ```bash # 使用常规 pnpm 替代 # 选择:pnpm(不是 pnpm with workspace) # 或对 Vue 2 项目使用 yarn/npm ``` ## 🔗 生成项目特性 ### 自动配置 - 微前端构建的 **Webpack 优化** - 跨域请求的 **CORS 处理** - 不同环境的 **Public path** 配置 - 本地开发的 **开发代理** 设置 ### 开发体验 - 所有应用中的 **热模块替换** - 微应用失败的 **错误边界** - 微应用转换期间的 **加载状态** - 适用的 **TypeScript 支持** ### 生产就绪 - 微前端部署的 **构建优化** - **资源优化** 和代码分割 - 不同阶段的 **环境配置** - **CI/CD 友好** 结构 ## 📖 下一步 创建项目后: 1. **探索生成的代码** 以了解结构 2. **根据需要自定义配置** 3. **随着项目增长添加更多微应用** 4. **设置 CI/CD 流水线** 进行自动化部署 5. **阅读 qiankun 文档** 了解高级特性 ## 🔗 相关文档 - [核心 APIs](/zh-CN/api/) - qiankun 核心 APIs - [React 绑定](/zh-CN/ecosystem/react) - React UI 绑定 - [Vue 绑定](/zh-CN/ecosystem/vue) - Vue UI 绑定 - [Webpack 插件](/zh-CN/ecosystem/webpack-plugin) - 构建工具配置 ## 🤝 贡献 想要添加新模板或改进 CLI?查看 [GitHub 仓库](https://github.com/umijs/qiankun) 并为 `packages/create-qiankun` 目录贡献。 ================================================ FILE: docs/zh-CN/ecosystem/index.md ================================================ # 生态系统 qiankun 提供了丰富的 UI 绑定和工具生态系统,帮助你高效地构建和维护微前端应用。 ## 🧩 UI 绑定 qiankun 为流行框架提供声明式 UI 组件,使在主应用中加载和管理微应用变得更加容易。 ### React **`@qiankunjs/react`** - qiankun 官方 React 绑定 - **特性**:声明式 MicroApp 组件、自动加载状态、错误边界 - **优势**:类型安全、React hooks 支持、无缝集成 - **适用场景**:基于 React 的主应用 ```bash npm install @qiankunjs/react ``` [了解更多 React 绑定 →](/zh-CN/ecosystem/react) ### Vue **`@qiankunjs/vue`** - qiankun 官方 Vue 绑定 - **特性**:Vue 2/3 兼容、组合式 API 支持、基于插槽的自定义 - **优势**:响应式加载状态、基于模板的方法、TypeScript 支持 - **适用场景**:基于 Vue 的主应用 ```bash npm install @qiankunjs/vue ``` [了解更多 Vue 绑定 →](/zh-CN/ecosystem/vue) ## 🛠️ 开发工具 ### Webpack 插件 **`@qiankunjs/webpack-plugin`** - 微应用的 Webpack 插件 - **特性**:自动公共路径注入、构建优化、开发模式支持 - **优势**:零配置设置、改善开发体验、生产就绪构建 - **适用场景**:基于 webpack 的微应用必备 ```bash npm install @qiankunjs/webpack-plugin --save-dev ``` [了解更多 Webpack 插件 →](/zh-CN/ecosystem/webpack-plugin) ### Create Qiankun **`create-qiankun`** - 脚手架工具用于创建 qiankun 项目 - **特性**:多种模板、主应用+微应用设置、包含最佳实践 - **优势**:快速项目初始化、生产就绪配置、现代工具 - **适用场景**:启动新的 qiankun 项目或添加微前端能力 ```bash npx create-qiankun my-micro-frontend-app ``` [了解更多 Create Qiankun →](/zh-CN/ecosystem/create-qiankun) ## 🎯 快速开始对比 ### 不使用 UI 绑定(核心 API) ```typescript import { loadMicroApp } from 'qiankun'; // 手动方式 const microApp = loadMicroApp({ name: 'my-app', entry: '//localhost:8080', container: '#subapp-container' }); // 手动生命周期管理 microApp.mountPromise.then(() => { setLoading(false); }).catch(error => { setError(error); }); ``` ### 使用 React 绑定 ```tsx import { MicroApp } from '@qiankunjs/react'; function App() { return ( ); } ``` ### 使用 Vue 绑定 ```vue ``` ## 🔄 集成流程 ```mermaid graph LR A[主应用] --> B[UI 绑定] B --> C[qiankun 核心] C --> D[微应用 1] C --> E[微应用 2] C --> F[微应用 3] G[Webpack 插件] --> D G --> E G --> F H[Create Qiankun] --> A H --> D H --> E H --> F ``` ## 📋 功能对比 | 功能 | 核心 API | React 绑定 | Vue 绑定 | |------|----------|------------|----------| | **加载状态** | 手动 | ✅ 自动 | ✅ 自动 | | **错误处理** | 手动 | ✅ 错误边界 | ✅ 错误边界 | | **自定义加载** | 手动 | ✅ 组件 | ✅ 插槽 | | **自定义错误** | 手动 | ✅ 组件 | ✅ 插槽 | | **TypeScript** | ✅ 完整 | ✅ 完整 | ✅ 完整 | | **框架集成** | 手动 | ✅ Hooks | ✅ 组合式 API | ## 🎨 使用模式 ### 1. 简单加载 **React:** ```tsx ``` **Vue:** ```vue ``` ### 2. 自定义加载和错误处理 **React:** ```tsx loading ? : null} errorBoundary={(error) => } /> ``` **Vue:** ```vue ``` ### 3. 属性传递 **React:** ```tsx ``` **Vue:** ```vue ``` ## 🚀 入门指南 ### 步骤 1:选择你的技术栈 1. **React 主应用** → 使用 `@qiankunjs/react` 2. **Vue 主应用** → 使用 `@qiankunjs/vue` 3. **其他框架** → 使用核心 qiankun API ### 步骤 2:创建项目脚手架 ```bash # 创建新项目 npx create-qiankun my-app # 选择模板: # - React 主应用 + React 微应用 # - Vue 主应用 + Vue 微应用 # - Umi 主应用 + 多个微应用 # - 自定义配置 ``` ### 步骤 3:配置微应用 为每个微应用添加 webpack 插件: ```javascript // webpack.config.js const { QiankunWebpackPlugin } = require('@qiankunjs/webpack-plugin'); module.exports = { plugins: [ new QiankunWebpackPlugin() ] }; ``` ### 步骤 4:开始开发 ```bash # 启动主应用 cd main-app && npm start # 启动微应用(在新终端中) cd micro-app && npm start ``` ## 🔧 高级配置 ### 基于环境的配置 ```typescript // React 主应用 const MicroAppConfig = { development: { entry: '//localhost:8080', autoSetLoading: true, autoCaptureError: true, }, production: { entry: '//your-domain.com/micro-app', autoSetLoading: false, // 自定义加载 autoCaptureError: true, } }; const config = MicroAppConfig[process.env.NODE_ENV]; function App() { return ; } ``` ### 多应用仪表板 ```tsx // React - 多个微应用 function Dashboard() { return (
); } ``` ## 📚 文档链接 - [React 绑定](/zh-CN/ecosystem/react) - 完整 React 集成指南 - [Vue 绑定](/zh-CN/ecosystem/vue) - 完整 Vue 集成指南 - [Webpack 插件](/zh-CN/ecosystem/webpack-plugin) - 构建工具配置 - [Create Qiankun](/zh-CN/ecosystem/create-qiankun) - 项目脚手架 - [API 参考](/zh-CN/api/) - 核心 qiankun API ## 🤝 社区 - [GitHub 讨论](https://github.com/umijs/qiankun/discussions) - 提问和分享想法 - [Issues](https://github.com/umijs/qiankun/issues) - 错误报告和功能请求 - [更新日志](https://github.com/umijs/qiankun/releases) - 最新更新和发布 选择最适合你项目需求的工具,开始构建强大的微前端应用! ================================================ FILE: docs/zh-CN/ecosystem/react.md ================================================ # React 绑定 qiankun 的官方 React 绑定提供了一种声明式的方式来将微应用集成到您的 React 主应用中。`@qiankunjs/react` 包提供了一个强大的 `` 组件,内置加载状态、错误处理和 TypeScript 支持。 ## 📦 安装 ```bash npm install @qiankunjs/react ``` **要求:** - React ≥ 16.9.0 - qiankun ≥ 3.0.0 ## 🚀 快速开始 ### 基本用法 ```tsx import React from 'react'; import { MicroApp } from '@qiankunjs/react'; function App() { return (

主应用

); } export default App; ``` ### 带加载状态 ```tsx import React from 'react'; import { MicroApp } from '@qiankunjs/react'; function App() { return ( ); } ``` ### 带错误处理 ```tsx import React from 'react'; import { MicroApp } from '@qiankunjs/react'; function App() { return ( ); } ``` ## 🎯 组件 API ### 属性 | 属性 | 类型 | 必需 | 默认值 | 描述 | |------|------|------|--------|------| | `name` | `string` | ✅ | - | 微应用的唯一名称 | | `entry` | `string` | ✅ | - | 微应用的入口 URL | | `autoSetLoading` | `boolean` | ❌ | `false` | 自动管理加载状态 | | `autoCaptureError` | `boolean` | ❌ | `false` | 自动处理错误 | | `loader` | `(loading: boolean) => React.ReactNode` | ❌ | `undefined` | 自定义加载组件 | | `errorBoundary` | `(error: any) => React.ReactNode` | ❌ | `undefined` | 自定义错误组件 | | `className` | `string` | ❌ | `undefined` | 微应用容器的 CSS 类 | | `wrapperClassName` | `string` | ❌ | `undefined` | 包装器的 CSS 类(使用 loader/errorBoundary 时) | | `settings` | `AppConfiguration` | ❌ | `{}` | qiankun 配置选项 | | `lifeCycles` | `LifeCycles` | ❌ | `undefined` | 生命周期钩子 | ### 额外属性 传递给 `` 的任何额外属性都会转发给微应用作为 props: ```tsx ``` ## 🔄 生命周期管理 ### 使用 Ref 访问微应用实例 ```tsx import React, { useRef, useEffect } from 'react'; import { MicroApp } from '@qiankunjs/react'; function App() { const microAppRef = useRef(); useEffect(() => { // 获取微应用状态 console.log(microAppRef.current?.getStatus()); }, []); const handleUnmount = () => { microAppRef.current?.unmount(); }; return (
); } ``` ### 应用状态 微应用实例提供这些状态值: - `NOT_LOADED` - 初始状态,尚未加载 - `LOADING_SOURCE_CODE` - 加载应用资源中 - `NOT_BOOTSTRAPPED` - 资源已加载,尚未引导 - `BOOTSTRAPPING` - 运行引导生命周期 - `NOT_MOUNTED` - 已引导但未挂载 - `MOUNTING` - 运行挂载生命周期 - `MOUNTED` - 成功挂载并运行 - `UPDATING` - 运行更新生命周期 - `UNMOUNTING` - 运行卸载生命周期 - `UNLOADING` - 清理资源 ## 🎨 自定义 ### 自定义加载组件 ```tsx import React from 'react'; import { MicroApp } from '@qiankunjs/react'; import { Spin, Alert } from 'antd'; const CustomLoader: React.FC<{ loading: boolean }> = ({ loading }) => { if (!loading) return null; return (

加载微应用中...

); }; function App() { return ( } /> ); } ``` ### 自定义错误边界 ```tsx import React from 'react'; import { MicroApp } from '@qiankunjs/react'; import { Alert, Button } from 'antd'; const CustomErrorBoundary: React.FC<{ error: Error }> = ({ error }) => { const handleRetry = () => { window.location.reload(); }; return (
重试 } />
); }; function App() { return ( } /> ); } ``` ### 样式设置 ```tsx import React from 'react'; import { MicroApp } from '@qiankunjs/react'; import './MicroApp.css'; function App() { return ( ); } ``` ```css /* MicroApp.css */ .micro-app-wrapper { border: 1px solid #e8e8e8; border-radius: 6px; overflow: hidden; } .micro-app-container { min-height: 400px; background: #fafafa; } ``` ## 🔧 高级用法 ### 多个微应用 ```tsx import React, { useState } from 'react'; import { MicroApp } from '@qiankunjs/react'; import { Tabs } from 'antd'; const { TabPane } = Tabs; function Dashboard() { const [activeTab, setActiveTab] = useState('dashboard'); return (
); } ``` ### 条件加载 ```tsx import React, { useState } from 'react'; import { MicroApp } from '@qiankunjs/react'; function ConditionalApp() { const [showMicroApp, setShowMicroApp] = useState(false); const [user, setUser] = useState(null); // 只有用户认证后才加载微应用 if (!user) { return
请登录以继续
; } return (
{showMicroApp && ( )}
); } ``` ### 动态入口 URL ```tsx import React, { useState } from 'react'; import { MicroApp } from '@qiankunjs/react'; function DynamicApp() { const [environment, setEnvironment] = useState('development'); const entryUrls = { development: '//localhost:8080', staging: '//staging.example.com', production: '//app.example.com' }; return (
); } ``` ## 🎮 状态管理 ### 使用 Context 共享状态 ```tsx import React, { createContext, useContext, useState } from 'react'; import { MicroApp } from '@qiankunjs/react'; // 创建共享状态的 Context const AppContext = createContext(); function MainApp() { const [sharedState, setSharedState] = useState({ user: { id: 1, name: 'John' }, theme: 'dark' }); return (
); } function MicroAppContainer() { const { sharedState } = useContext(AppContext); return ( ); } ``` ### 应用间通信 ```tsx import React, { useEffect, useRef } from 'react'; import { MicroApp } from '@qiankunjs/react'; function CommunicatingApps() { const microApp1Ref = useRef(); const microApp2Ref = useRef(); useEffect(() => { // 设置通信渠道 window.appCommunication = { sendMessage: (from, to, message) => { const event = new CustomEvent('microAppMessage', { detail: { from, to, message } }); window.dispatchEvent(event); } }; // 监听消息 const handleMessage = (event) => { console.log('收到消息:', event.detail); }; window.addEventListener('microAppMessage', handleMessage); return () => { window.removeEventListener('microAppMessage', handleMessage); delete window.appCommunication; }; }, []); return (
); } ``` ## 🔒 TypeScript 支持 ### 类型化属性 ```tsx import React from 'react'; import { MicroApp } from '@qiankunjs/react'; interface UserProfileProps { userId: string; theme: 'light' | 'dark'; permissions: string[]; } // 为额外属性添加类型 const UserProfileApp: React.FC = () => { const user = getCurrentUser(); return ( ); }; ``` ### 微应用自定义 Hook ```tsx import { useRef, useEffect, useState } from 'react'; import type { MicroApp as MicroAppType } from 'qiankun'; interface UseMicroAppOptions { onStatusChange?: (status: string) => void; onError?: (error: Error) => void; } export function useMicroApp(options: UseMicroAppOptions = {}) { const microAppRef = useRef(); const [status, setStatus] = useState('NOT_LOADED'); const [error, setError] = useState(null); useEffect(() => { const checkStatus = () => { if (microAppRef.current) { const currentStatus = microAppRef.current.getStatus(); if (currentStatus !== status) { setStatus(currentStatus); options.onStatusChange?.(currentStatus); } } }; const interval = setInterval(checkStatus, 1000); return () => clearInterval(interval); }, [status, options]); const handleError = (err: Error) => { setError(err); options.onError?.(err); }; return { microAppRef, status, error, handleError }; } // 使用方式 function App() { const { microAppRef, status, error } = useMicroApp({ onStatusChange: (status) => console.log('状态变化:', status), onError: (error) => console.error('应用错误:', error) }); return (

状态: {status}

{error &&

错误: {error.message}

}
); } ``` ## 🚀 性能优化 ### 懒加载 ```tsx import React, { Suspense, lazy } from 'react'; // 懒加载 MicroApp 组件 const LazyMicroApp = lazy(() => import('@qiankunjs/react').then(module => ({ default: module.MicroApp })) ); function App() { return ( 加载中...
}> ); } ``` ### 记忆化 ```tsx import React, { memo, useMemo } from 'react'; import { MicroApp } from '@qiankunjs/react'; const MemoizedMicroApp = memo(MicroApp); function OptimizedApp({ user, settings }) { const microAppProps = useMemo(() => ({ userId: user.id, theme: settings.theme, language: settings.language }), [user.id, settings.theme, settings.language]); return ( ); } ``` ## 🐛 错误处理与调试 ### 开发模式错误处理 ```tsx import React from 'react'; import { MicroApp } from '@qiankunjs/react'; function DevMicroApp() { const isDevelopment = process.env.NODE_ENV === 'development'; const handleError = (error: Error) => { console.error('微应用错误:', error); if (isDevelopment) { // 在开发环境显示详细错误 return (

开发环境错误

{error.stack}
); } // 在生产环境显示用户友好的错误 return (

出现了一些问题,请稍后再试。

); }; return ( ); } ``` ## 📚 最佳实践 ### 1. 使用描述性名称 ```tsx // ✅ 好:描述性名称 // ❌ 坏:通用名称 ``` ### 2. 始终处理加载状态 ```tsx // ✅ 好:处理加载状态 } /> // ❌ 坏:没有加载指示 ``` ### 3. 实现错误边界 ```tsx // ✅ 好:优雅地处理错误 } /> ``` ### 4. 使用环境特定的配置 ```tsx // ✅ 好:环境感知 const config = { development: { entry: '//localhost:8080', debug: true }, production: { entry: '//app.example.com', debug: false } }; ``` ## 🔗 相关文档 - [Vue 绑定](/zh-CN/ecosystem/vue) - Vue UI 绑定 - [核心 API](/zh-CN/api/) - qiankun 核心 API - [配置](/zh-CN/api/configuration) - 配置选项 - [生命周期](/zh-CN/api/lifecycles) - 生命周期钩子 ================================================ FILE: docs/zh-CN/ecosystem/vue.md ================================================ # Vue 绑定 qiankun 的官方 Vue 绑定提供了一种声明式的方式来将微应用集成到您的 Vue 主应用中。`@qiankunjs/vue` 包提供了一个强大的 `` 组件,支持 Vue 2/3 兼容性、Composition API 和基于插槽的自定义。 ## 📦 安装 ```bash npm install @qiankunjs/vue ``` **要求:** - Vue 2.0+ 或 Vue 3.0+ - qiankun ≥ 3.0.0 - 对于 Vue 2,您可能需要 `@vue/composition-api` ## 🚀 快速开始 ### Vue 3 与 Composition API ```vue ``` ### Vue 2 与 Options API ```vue ``` ### 带加载状态 ```vue ``` ### 带错误处理 ```vue ``` ## 🎯 组件 API ### 属性 | 属性 | 类型 | 必需 | 默认值 | 描述 | |------|------|------|--------|------| | `name` | `string` | ✅ | - | 微应用的唯一名称 | | `entry` | `string` | ✅ | - | 微应用的入口 URL | | `autoSetLoading` | `boolean` | ❌ | `false` | 自动管理加载状态 | | `autoCaptureError` | `boolean` | ❌ | `false` | 自动处理错误 | | `className` | `string` | ❌ | `undefined` | 微应用容器的 CSS 类 | | `wrapperClassName` | `string` | ❌ | `undefined` | 包装器的 CSS 类(使用插槽时) | | `appProps` | `Record` | ❌ | `undefined` | 传递给微应用的属性 | | `settings` | `AppConfiguration` | ❌ | `{}` | qiankun 配置选项 | | `lifeCycles` | `LifeCycles` | ❌ | `undefined` | 生命周期钩子 | ### 插槽 | 插槽 | 描述 | 参数 | |------|------|------| | `loader` | 自定义加载组件 | `{ loading: boolean }` | | `errorBoundary` | 自定义错误组件 | `{ error: Error }` | ## 🎨 自定义 ### 使用插槽自定义加载 ```vue ``` ### 自定义错误边界 ```vue ``` ### 样式设置 ```vue ``` ## 🔧 高级用法 ### 带标签页的多个微应用 ```vue ``` ### 条件加载 ```vue ``` ### 动态入口 URL ```vue ``` ## 🎮 状态管理 ### 使用 Pinia 进行状态共享 ```vue ``` ```typescript // stores/app.ts import { defineStore } from 'pinia'; export const useAppStore = defineStore('app', { state: () => ({ user: null, theme: 'dark', language: 'zh-CN' }), actions: { setUser(user) { this.user = user; }, setTheme(theme) { this.theme = theme; } } }); ``` ```vue ``` ### 应用间通信 ```vue ``` ## 🔒 TypeScript 支持 ### 自定义 Composable ```typescript // composables/useMicroApp.ts import { ref, onMounted, onUnmounted } from 'vue'; import type { Ref } from 'vue'; interface UseMicroAppOptions { onStatusChange?: (status: string) => void; onError?: (error: Error) => void; } export function useMicroApp(options: UseMicroAppOptions = {}) { const microAppRef: Ref = ref(); const status = ref('NOT_LOADED'); const error = ref(null); const checkStatus = () => { if (microAppRef.value?.microApp) { const currentStatus = microAppRef.value.microApp.getStatus(); if (currentStatus !== status.value) { status.value = currentStatus; options.onStatusChange?.(currentStatus); } } }; const handleError = (err: Error) => { error.value = err; options.onError?.(err); }; let interval: number; onMounted(() => { interval = window.setInterval(checkStatus, 1000); }); onUnmounted(() => { if (interval) { clearInterval(interval); } }); return { microAppRef, status, error, handleError }; } ``` ```vue ``` ## 🚀 性能优化 ### 使用 Suspense 进行懒加载 ```vue ``` ### 使用 computed 进行记忆化 ```vue ``` ### 基于路由的微应用使用 Keep-alive ```vue ``` ```vue ``` ## 🐛 错误处理与调试 ### 开发模式错误处理 ```vue ``` ```vue ``` ## 📚 Vue 2 兼容性 ### 在 Vue 2 中使用 ```vue ``` ### 在 Vue 2 中使用 Composition API ```vue ``` ## 📚 最佳实践 ### 1. 使用描述性名称 ```vue ``` ### 2. 始终处理加载状态 ```vue ``` ### 3. 实现错误边界 ```vue ``` ### 4. 使用响应式属性 ```vue ``` ### 5. 环境特定的配置 ```vue ``` ## 🔗 相关文档 - [React 绑定](/zh-CN/ecosystem/react) - React UI 绑定 - [核心 API](/zh-CN/api/) - qiankun 核心 API - [配置](/zh-CN/api/configuration) - 配置选项 - [生命周期](/zh-CN/api/lifecycles) - 生命周期钩子 ================================================ FILE: docs/zh-CN/ecosystem/webpack-plugin.md ================================================ # Webpack 插件 `@qiankunjs/webpack-plugin` 是专为 qiankun 微前端框架设计的 Webpack 插件。它简化并自动化了将微应用与 qiankun 集成所需的常见配置,确保正确的构建输出和运行时行为。 ## 🚀 安装 ### 使用 npm ```bash npm install @qiankunjs/webpack-plugin --save-dev ``` ### 使用 yarn ```bash yarn add @qiankunjs/webpack-plugin --dev ``` ### 使用 pnpm ```bash pnpm add @qiankunjs/webpack-plugin --save-dev ``` ## 🎯 功能特性 - **自动库配置**:设置正确的输出库名称和格式供 qiankun 使用 - **唯一 JSONP 函数**:确保唯一的 `jsonpFunction` 名称以防止微应用间冲突 - **浏览器兼容性**:将全局对象设置为 `window` 以确保在浏览器中正确执行 - **入口脚本标记**:自动为 HTML 中的主脚本标签添加 `entry` 属性 - **Webpack 4 & 5 支持**:兼容 Webpack 4 和 Webpack 5 - **零配置**:开箱即用,具有合理的默认设置 ## 📋 系统要求 - **Webpack**:4.x 或 5.x - **Node.js**:v14 或更高版本 - **package.json**:必须存在于项目根目录 ## 🚀 快速开始 ### 基本用法 ```javascript // webpack.config.js const { QiankunPlugin } = require('@qiankunjs/webpack-plugin'); module.exports = { entry: './src/index.js', plugins: [ new QiankunPlugin() ] }; ``` 这个基本配置将: - 使用 `package.json` 中的 `name` 字段作为库名称 - 自动为最后一个脚本标签添加 `entry` 属性 - 配置输出以供 qiankun 使用 ### 自定义选项 ```javascript // webpack.config.js const { QiankunPlugin } = require('@qiankunjs/webpack-plugin'); module.exports = { entry: './src/index.js', plugins: [ new QiankunPlugin({ packageName: 'my-micro-app', entrySrcPattern: /main\.[a-f0-9]+\.js$/ }) ] }; ``` ## 🎛️ 配置选项 ### `packageName` (可选) - **类型**:`string` - **默认值**:`package.json` 中的 name 字段值 - **描述**:指定 qiankun 用于识别微应用的输出库名称 ```javascript new QiankunPlugin({ packageName: 'my-custom-app-name' }) ``` ### `entrySrcPattern` (可选) - **类型**:`RegExp` - **默认值**:`null`(将标记最后一个脚本标签) - **描述**:用于匹配特定脚本标签以添加 `entry` 属性的正则表达式模式 ```javascript new QiankunPlugin({ entrySrcPattern: /index\.[a-f0-9]+\.js$/ }) ``` ## 🔧 框架集成 ### React 应用 ```javascript // webpack.config.js (Create React App with CRACO) const { QiankunPlugin } = require('@qiankunjs/webpack-plugin'); module.exports = { webpack: { plugins: { add: [ new QiankunPlugin({ packageName: 'react-micro-app' }) ] } } }; ``` ### Vue 应用 ```javascript // vue.config.js const { QiankunPlugin } = require('@qiankunjs/webpack-plugin'); module.exports = { configureWebpack: { plugins: [ new QiankunPlugin({ packageName: 'vue-micro-app' }) ] } }; ``` ### Angular 应用 ```javascript // custom-webpack.config.js const { QiankunPlugin } = require('@qiankunjs/webpack-plugin'); module.exports = { plugins: [ new QiankunPlugin({ packageName: 'angular-micro-app' }) ] }; ``` ## 🏗️ 插件功能 ### 1. 输出库配置 插件自动配置 webpack 输出以将微应用暴露为库: **Webpack 4:** ```javascript { output: { library: 'your-app-name', libraryTarget: 'window', jsonpFunction: 'webpackJsonp_your-app-name', globalObject: 'window', chunkLoadingGlobal: 'webpackJsonp_your-app-name' } } ``` **Webpack 5:** ```javascript { output: { library: { name: 'your-app-name', type: 'window' }, jsonpFunction: 'webpackJsonp_your-app-name', globalObject: 'window', chunkLoadingGlobal: 'webpackJsonp_your-app-name' } } ``` ### 2. 入口脚本标记 插件处理 HTML 文件并为适当的脚本标签添加 `entry` 属性: **处理前:** ```html 我的微应用
``` **处理后:** ```html 我的微应用
``` ## 🎨 高级配置 ### 自定义入口模式匹配 对于具有复杂构建输出的应用,您可以精确指定哪个脚本应被标记为入口: ```javascript new QiankunPlugin({ // 匹配特定命名模式的脚本 entrySrcPattern: /main\.[a-f0-9]{8}\.js$/ }) ``` ```javascript new QiankunPlugin({ // 匹配特定目录中的脚本 entrySrcPattern: /\/js\/app\./ }) ``` ```javascript new QiankunPlugin({ // 匹配特定前缀的脚本 entrySrcPattern: /^bundle\./ }) ``` ### 多个 HTML 文件 插件会处理项目中的所有 HTML 文件,对每个文件应用相同的入口标记逻辑。 ### 开发与生产环境 ```javascript const isDev = process.env.NODE_ENV === 'development'; new QiankunPlugin({ packageName: isDev ? 'my-app-dev' : 'my-app-prod', entrySrcPattern: isDev ? /main\.js$/ : /main\.[a-f0-9]+\.js$/ }) ``` ## ✅ 验证 ### 检查库暴露 ```bash # 构建微应用 npm run build # 检查主包是否包含库 grep -n "window\[.*your-app-name" dist/static/js/main.*.js ``` ### 验证入口属性 ```bash # 检查 HTML 是否包含 entry 属性 grep -n "entry" dist/index.html ``` ## 🐛 故障排除 ### 匹配到多个脚本标签 **错误:** `The regular expression matched multiple script tags, please check your regex.` **解决方案:** 使您的正则表达式模式更具体: ```javascript // ❌ 太宽泛 - 匹配多个文件 entrySrcPattern: /\.js$/ // ✅ 更具体 - 只匹配主文件 entrySrcPattern: /main\.[a-f0-9]+\.js$/ ``` ### 没有匹配到脚本标签 **错误:** `The provided regular expression did not match any scripts.` **解决方案:** 检查您的模式是否与实际生成的文件名匹配: ```javascript // 检查实际生成的文件 console.log(fs.readdirSync('dist/static/js/')); // 相应调整模式 entrySrcPattern: /app\.[a-f0-9]+\.js$/ ``` ### 库未暴露 **问题:** qiankun 找不到您的微应用 **解决方案:** 1. 检查 `package.json` 是否有有效的名称: ```json { "name": "my-micro-app" } ``` 2. 在浏览器控制台中验证库是否已暴露: ```javascript // 加载后应该存在 window['my-micro-app'] ``` 3. 确保插件已应用: ```javascript // 确保插件在 plugins 数组中 plugins: [ new QiankunPlugin() ] ``` ### JSONP 函数冲突 **问题:** 多个微应用导致冲突 **解决方案:** 使用不同的包名称: ```javascript // 应用 1 new QiankunPlugin({ packageName: 'app-dashboard' }) // 应用 2 new QiankunPlugin({ packageName: 'app-settings' }) ``` ## 🔧 集成示例 ### Create React App (CRACO) ```javascript // craco.config.js const { QiankunPlugin } = require('@qiankunjs/webpack-plugin'); module.exports = { webpack: { configure: (webpackConfig) => { webpackConfig.plugins.push( new QiankunPlugin({ packageName: process.env.REACT_APP_NAME || 'react-app' }) ); return webpackConfig; } } }; ``` ### Vue CLI ```javascript // vue.config.js const { defineConfig } = require('@vue/cli-service'); const { QiankunPlugin } = require('@qiankunjs/webpack-plugin'); module.exports = defineConfig({ configureWebpack: { plugins: [ new QiankunPlugin() ] }, // 额外的 qiankun 特定配置 devServer: { port: 8080, headers: { 'Access-Control-Allow-Origin': '*' } } }); ``` ### Next.js (使用自定义 webpack 配置) ```javascript // next.config.js const { QiankunPlugin } = require('@qiankunjs/webpack-plugin'); module.exports = { webpack: (config, { dev, isServer }) => { if (!isServer) { config.plugins.push( new QiankunPlugin({ packageName: 'nextjs-micro-app' }) ); } return config; } }; ``` ### Vite (使用 vite-plugin-qiankun) 虽然此插件是为 Webpack 设计的,但对于 Vite 用户: ```javascript // vite.config.js import { defineConfig } from 'vite'; import qiankun from 'vite-plugin-qiankun'; export default defineConfig({ plugins: [ qiankun('my-vite-app', { useDevMode: true }) ] }); ``` ## 📊 性能考虑 ### 包体积 插件对包的开销很小: - 库包装器:~100 字节 - JSONP 函数自定义:~50 字节 ### 构建时间 插件在 emit 阶段运行,通常增加: - HTML 处理:< 100ms - webpack 配置:< 50ms ### 运行时性能 - 无运行时性能影响 - 启用 qiankun 的高效加载机制 - 防止全局命名空间冲突 ## 🔒 安全考虑 ### 库命名 使用描述性、非冲突的库名称: ```javascript // ✅ 好 - 特定且唯一 packageName: 'company-dashboard-app' // ❌ 坏 - 太通用,可能冲突 packageName: 'app' ``` ### CORS 配置 确保您的微应用使用适当的 CORS 头提供服务: ```javascript // 开发服务器配置 devServer: { headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS', 'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization' } } ``` ## 📚 最佳实践 ### 1. 一致的命名约定 ```javascript // 跨环境使用一致的命名 const appName = process.env.NODE_ENV === 'production' ? 'company-app-prod' : 'company-app-dev'; new QiankunPlugin({ packageName: appName }) ``` ### 2. 环境特定的模式 ```javascript // 不同环境使用不同模式 const entrySrcPattern = process.env.NODE_ENV === 'production' ? /main\.[a-f0-9]+\.js$/ // 生产环境使用哈希 : /main\.js$/; // 开发环境使用简单名称 new QiankunPlugin({ entrySrcPattern }) ``` ### 3. 验证配置 ```javascript // 在构建过程中添加验证 const pkg = require('./package.json'); if (!pkg.name) { throw new Error('package.json 必须有 name 字段供 qiankun 使用'); } new QiankunPlugin({ packageName: pkg.name }) ``` ### 4. 测试集成 ```javascript // 测试您的配置 describe('Qiankun 集成', () => { it('应该在 window 上暴露库', () => { expect(window[process.env.REACT_APP_NAME]).toBeDefined(); }); it('应该有入口脚本标记', () => { const entryScript = document.querySelector('script[entry]'); expect(entryScript).toBeTruthy(); }); }); ``` ## 🔗 相关文档 - [核心 API](/zh-CN/api/) - qiankun 核心 API - [Create Qiankun](/zh-CN/ecosystem/create-qiankun) - 项目脚手架工具 - [React 绑定](/zh-CN/ecosystem/react) - React UI 绑定 - [Vue 绑定](/zh-CN/ecosystem/vue) - Vue UI 绑定 ## 📈 迁移指南 ### 从手动配置迁移 如果您之前手动配置 webpack for qiankun: **之前:** ```javascript module.exports = { output: { library: 'myApp', libraryTarget: 'window', jsonpFunction: 'webpackJsonp_myApp' } }; ``` **之后:** ```javascript const { QiankunPlugin } = require('@qiankunjs/webpack-plugin'); module.exports = { plugins: [ new QiankunPlugin({ packageName: 'myApp' }) ] }; ``` ### 从其他微前端解决方案迁移 该插件处理 qiankun 所需的 webpack 特定配置,消除了手动库设置和入口脚本标记的需要。 ## 🤝 贡献 发现问题或想要贡献?查看 [GitHub 仓库](https://github.com/umijs/qiankun) 并贡献到 `packages/webpack-plugin` 目录。 ================================================ FILE: docs/zh-CN/faq/index.md ================================================ # 常见问题 本 FAQ 涵盖了使用 qiankun 时遇到的最常见问题和问题。如果您找不到所需的答案,请查看我们的 [GitHub Issues](https://github.com/umijs/qiankun/issues) 或加入我们的社区讨论。 ## 🚀 入门指南 ### 问:什么是 qiankun,什么时候应该使用它? **答:** qiankun 是一个基于 single-spa 的微前端框架,通过组合多个较小的独立应用来构建大规模前端应用。在以下情况下应该考虑使用 qiankun: - 团队在不断壮大,需要跨多个团队扩展开发 - 有需要与新功能共存的遗留应用 - 希望在一个应用中使用不同的框架(React、Vue、Angular) - 需要为应用的不同部分提供独立部署能力 ### 问:qiankun 与其他微前端解决方案有何不同? **答:** qiankun 提供了几个关键优势: - **生产就绪**:由蚂蚁集团在大规模应用中构建和测试 - **框架无关**:适用于 React、Vue、Angular 和原生 JavaScript - **强大的沙箱**:开箱即用的 JavaScript 和 CSS 隔离 - **HTML 入口**:使用 HTML 文件作为入口点的简单配置 - **丰富的生态系统**:UI 绑定、CLI 工具和 webpack 插件 ### 问:我可以在现有应用中使用 qiankun 吗? **答:** 可以!qiankun 旨在与现有应用一起工作。您可以: 1. **包装现有应用**:将当前应用转换为 qiankun 主应用 2. **增量迁移**:逐步将功能提取到微应用中 3. **遗留集成**:在新微应用旁边运行遗留应用 4. **框架迁移**:逐步从一个框架迁移到另一个框架 ## 🔧 安装和设置 ### 问:加载微应用时遇到 CORS 错误,如何修复? **答:** CORS 错误在开发中很常见。以下是解决方案: **对于 webpack dev server:** ```javascript // webpack.config.js 或 vue.config.js module.exports = { devServer: { headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS', 'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization' } } }; ``` **对于 Create React App(使用 CRACO):** ```javascript // craco.config.js module.exports = { devServer: { headers: { 'Access-Control-Allow-Origin': '*' } } }; ``` **对于生产环境,配置您的服务器:** ```nginx # nginx.conf location / { add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS'; add_header Access-Control-Allow-Headers 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range'; } ``` ### 问:我的微应用无法加载,应该检查什么? **答:** 按照此故障排除清单: 1. **检查网络选项卡**:微应用资源是否有 404 错误? 2. **验证 CORS**:控制台中是否有 CORS 错误? 3. **检查入口点**:HTML 入口文件是否可访问? 4. **验证导出**:微应用是否导出了必需的生命周期方法? 5. **检查容器**:DOM 中是否存在容器元素? **正确的微应用导出示例:** ```javascript // 微应用入口文件 export async function bootstrap() { console.log('micro app bootstrapped'); } export async function mount(props) { console.log('micro app mounted', props); // 您的应用挂载逻辑 } export async function unmount(props) { console.log('micro app unmounted', props); // 您的应用清理逻辑 } ``` ### 问:如何处理微应用的不同基础路径? **答:** 在微应用中配置公共路径: **对于 webpack:** ```javascript // webpack.config.js module.exports = { output: { publicPath: process.env.NODE_ENV === 'production' ? 'https://mycdn.com/micro-app/' : 'http://localhost:8080/' } }; ``` **对于运行时配置:** ```javascript // 微应用中的 public-path.js if (window.__POWERED_BY_QIANKUN__) { __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; } ``` ## 🏗️ 架构和设计 ### 问:我应该如何构建微前端架构? **答:** 遵循以下架构原则: **1. 领域驱动设计:** ``` 主应用(Shell) ├── 用户管理(HR 领域) ├── 产品目录(商务领域) ├── 分析仪表板(BI 领域) └── 设置(系统领域) ``` **2. 共享与独立:** - **共享**:身份验证、导航、设计系统 - **独立**:业务逻辑、数据获取、内部状态 **3. 通信模式:** ```javascript // 事件驱动通信 window.dispatchEvent(new CustomEvent('user-updated', { detail: { userId: 123 } })); // 基于属性的通信 registerMicroApps([{ name: 'user-app', entry: '//localhost:8080', container: '#container', activeRule: '/users', props: { userPermissions: currentUser.permissions, onUserUpdate: handleUserUpdate } }]); ``` ### 问:如何在微应用之间共享依赖? **答:** 几种方法效果很好: **1. 外部依赖(推荐):** ```javascript // webpack.config.js module.exports = { externals: { 'react': 'React', 'react-dom': 'ReactDOM', 'lodash': '_' } }; ``` **2. 模块联邦:** ```javascript // 主应用 webpack 配置 new ModuleFederationPlugin({ name: 'shell', shared: { react: { singleton: true }, 'react-dom': { singleton: true } } }); ``` **3. CDN 方法:** ```html ``` ### 问:微应用可以相互通信吗? **答:** 可以,以下是推荐的模式: **1. 事件驱动通信:** ```javascript // 微应用 A const notifyOtherApps = (data) => { window.dispatchEvent(new CustomEvent('app-a-event', { detail: data })); }; // 微应用 B window.addEventListener('app-a-event', (event) => { console.log('从应用 A 接收到:', event.detail); }); ``` **2. 共享状态管理:** ```javascript // 全局存储 window.__SHARED_STORE__ = { user: null, subscribers: [], updateUser: (user) => { window.__SHARED_STORE__.user = user; window.__SHARED_STORE__.subscribers.forEach(callback => callback(user)); } }; ``` **3. 来自主应用的属性:** ```javascript // 主应用协调通信 const handleDataChange = (data) => { // 更新所有相关微应用的属性 updateMicroAppProps('app-a', { sharedData: data }); updateMicroAppProps('app-b', { sharedData: data }); }; ``` ## 🎨 样式和 CSS ### 问:微应用之间的 CSS 样式发生冲突,如何修复? **答:** 使用 qiankun 的内置样式隔离: **1. 严格样式隔离(Shadow DOM):** ```javascript import { start } from 'qiankun'; start({ sandbox: { strictStyleIsolation: true } }); ``` **2. 实验性样式隔离(CSS 作用域):** ```javascript start({ sandbox: { experimentalStyleIsolation: true } }); ``` **3. 手动 CSS 作用域:** ```css /* 为所有样式添加前缀 */ .my-micro-app .button { background: blue; } .my-micro-app .container { padding: 20px; } ``` 查看我们的[样式隔离指南](/cookbook/style-isolation)获取全面的解决方案。 ### 问:我可以在 qiankun 中使用 CSS-in-JS 库吗? **答:** 当然可以!CSS-in-JS 库与 qiankun 配合得很好: **Styled Components:** ```jsx import styled from 'styled-components'; const Button = styled.button` background: blue; color: white; `; ``` **Emotion:** ```jsx /** @jsxImportSource @emotion/react */ import { css } from '@emotion/react'; const buttonStyle = css` background: blue; color: white; `; ``` CSS-in-JS 提供天然隔离,因为样式作用域限定在组件中。 ## 🔄 路由和导航 ### 问:如何在微前端设置中处理路由? **答:** qiankun 支持多种路由策略: **1. 基于路由的微应用(推荐):** ```javascript registerMicroApps([ { name: 'user-management', entry: '//localhost:8080', container: '#container', activeRule: '/users' // 当路由以 /users 开头时加载 }, { name: 'product-catalog', entry: '//localhost:8081', container: '#container', activeRule: ['/products', '/categories'] // 多个路由 } ]); ``` **2. 编程式路由:** ```javascript // 在微应用之间导航 import { navigateToUrl } from 'single-spa'; const navigateToUsers = () => { navigateToUrl('/users'); }; ``` **3. 哈希路由:** ```javascript registerMicroApps([ { name: 'hash-app', entry: '//localhost:8080', container: '#container', activeRule: '#/app' // 基于哈希的路由 } ]); ``` ### 问:微应用可以有自己的内部路由吗? **答:** 可以!每个微应用都可以有自己的内部路由器: **React Router 示例:** ```jsx // 在您的微应用中 import { BrowserRouter, Routes, Route } from 'react-router-dom'; function App() { const basename = window.__POWERED_BY_QIANKUN__ ? '/users' : '/'; return ( } /> } /> } /> ); } ``` ## 🚀 性能 ### 问:我的微前端应用加载缓慢,如何提高性能? **答:** 遵循这些优化策略: **1. 启用预取:** ```javascript start({ prefetch: true // 或 'all' 或特定应用名称 }); ``` **2. 使用代码分割:** ```javascript // 微应用中的动态导入 const HeavyComponent = React.lazy(() => import('./HeavyComponent')); ``` **3. 优化包大小:** ```javascript // webpack.config.js module.exports = { optimization: { splitChunks: { chunks: 'all' } } }; ``` 查看我们的[性能优化指南](/cookbook/performance)获取详细策略。 ### 问:如何防止微应用中的内存泄漏? **答:** 实施适当的清理: ```javascript // 微应用生命周期 export async function unmount() { // 清除定时器 clearInterval(myInterval); // 移除事件监听器 window.removeEventListener('resize', handleResize); // 清理订阅 subscription.unsubscribe(); // 清除缓存 cache.clear(); } ``` ## 🛠️ 开发和调试 ### 问:如何在开发中调试微应用? **答:** 使用这些调试策略: **1. 启用源映射:** ```javascript // webpack.config.js module.exports = { devtool: 'source-map' }; ``` **2. 使用浏览器开发工具:** - 网络选项卡:检查资源加载 - 控制台:查看错误消息 - 元素:检查 DOM 结构 - 源码:使用断点调试 JavaScript **3. qiankun 调试:** ```javascript // 启用详细日志 localStorage.setItem('qiankun:debug', true); ``` ### 问:我可以在微应用中使用热重载吗? **答:** 可以,需要一些配置: **对于 webpack dev server:** ```javascript // webpack.config.js module.exports = { devServer: { hot: true, headers: { 'Access-Control-Allow-Origin': '*' } } }; ``` **注意**:热重载在每个微应用内工作,但主应用的更改可能需要完全刷新。 ## 🔒 安全 ### 问:如何处理跨微应用的身份验证? **答:** 在主应用中集中身份验证: **1. 基于令牌的身份验证:** ```javascript // 主应用处理身份验证 const userToken = await authenticate(credentials); localStorage.setItem('token', userToken); // 将令牌传递给微应用 registerMicroApps([{ name: 'secure-app', entry: '//localhost:8080', container: '#container', activeRule: '/secure', props: { token: userToken, user: currentUser } }]); ``` **2. 共享身份验证状态:** ```javascript // 全局身份验证状态 window.__AUTH_STATE__ = { user: currentUser, token: userToken, isAuthenticated: true }; ``` ### 问:微前端是否存在安全问题? **答:** 请注意这些安全考虑: **1. 内容安全策略(CSP):** ```html ``` **2. CORS 配置:** - 只允许受信任的来源 - 正确验证请求 - 在生产中使用 HTTPS **3. 依赖安全:** - 定期审计依赖 - 使用 `npm audit` 等工具 - 保持依赖更新 ## 📱 移动端和浏览器支持 ### 问:qiankun 在移动设备上工作吗? **答:** 可以,qiankun 在移动端工作,需要考虑: **1. 触摸事件优化:** ```javascript // 使用被动监听器 element.addEventListener('touchstart', handler, { passive: true }); ``` **2. 视口管理:** ```html ``` **3. 性能优化:** - 减少包大小 - 使用懒加载 - 优化图片和资源 ### 问:qiankun 支持哪些浏览器? **答:** qiankun 支持现代浏览器: - **Chrome**:49+ - **Firefox**:45+ - **Safari**:10+ - **Edge**:79+ - **IE**:不支持 对于较旧的浏览器,考虑使用 polyfill: ```html ``` ## 🚢 部署 ### 问:如何部署微前端应用? **答:** 使用独立部署策略: **1. 分别构建:** ```bash # 独立构建每个应用 cd main-app && npm run build cd micro-app-1 && npm run build cd micro-app-2 && npm run build ``` **2. CDN 部署:** ```javascript // 为每个应用配置不同的 CDN const microApps = [ { name: 'app-1', entry: 'https://cdn1.example.com/app-1/', container: '#container', activeRule: '/app-1' }, { name: 'app-2', entry: 'https://cdn2.example.com/app-2/', container: '#container', activeRule: '/app-2' } ]; ``` ### 问:如何处理版本控制和更新? **答:** 实施版本管理: **1. 语义化版本:** ```javascript // 每个微应用的 package.json { "name": "user-management-app", "version": "1.2.3" } ``` **2. 运行时版本检查:** ```javascript const requiredVersion = '1.2.0'; const currentVersion = window.__MICRO_APP_VERSION__; if (!semver.gte(currentVersion, requiredVersion)) { console.warn('微应用版本兼容性问题'); } ``` ## 🔗 集成 ### 问:我可以在服务端渲染(SSR)中使用 qiankun 吗? **答:** 微前端的 SSR 很复杂但是可能的: **1. 静态渲染:** - 在服务器上渲染微应用 - 在客户端进行水合 **2. 考虑因素:** - 每个微应用都需要 SSR 支持 - 应用之间的协调具有挑战性 - 性能影响 **替代方法:** - 使用边缘侧包含(ESI) - 在页面级别实施微前端 - 考虑客户端渲染与快速初始加载 ### 问:如何将 qiankun 与现有构建工具集成? **答:** qiankun 与各种构建工具配合使用: **Webpack:** 使用 `@qiankunjs/webpack-plugin` **Vite:** 使用 `vite-plugin-qiankun` **Rollup:** 手动配置 **Parcel:** 手动配置 查看我们的[生态系统](/ecosystem/)部分了解特定集成。 ## 🤝 社区和支持 ### 问:如果我遇到困难,在哪里可以获得帮助? **答:** 有多个支持渠道可用: 1. **GitHub Issues**:[umijs/qiankun](https://github.com/umijs/qiankun/issues) 2. **讨论**:GitHub 讨论用于提问 3. **Stack Overflow**:用 `qiankun` 标签提问 4. **Discord/Slack**:社区聊天室 ### 问:我如何为 qiankun 做贡献? **答:** 我们欢迎贡献: 1. **错误报告**:提交详细的问题报告 2. **功能请求**:提议新功能 3. **代码贡献**:提交拉取请求 4. **文档**:改进文档和示例 5. **社区**:帮助回答问题 查看我们的[贡献指南](https://github.com/umijs/qiankun/blob/master/CONTRIBUTING.md)了解详情。 --- ## 📚 其他资源 - [完整 API 参考](/api/) - [最佳实践指南](/cookbook/) - [生态系统工具](/ecosystem/) - [GitHub 仓库](https://github.com/umijs/qiankun) - [示例应用](https://github.com/umijs/qiankun/tree/master/examples) **找不到您要找的内容?** 请[提交问题](https://github.com/umijs/qiankun/issues/new)或开始[讨论](https://github.com/umijs/qiankun/discussions) - 我们在这里帮助您! ================================================ FILE: docs/zh-CN/guide/index.md ================================================ # 什么是 qiankun? qiankun 是一个基于 [single-spa](https://github.com/single-spa/single-spa) 的微前端实现库,旨在帮助大家能更简单、无痛地构建一个生产可用的微前端架构系统。 qiankun 孵化自蚂蚁金融科技基于微前端架构的云产品统一接入平台,在经过大量的线上应用运行验证及打磨后,我们将其微前端内核抽取出来并开源,希望能同时帮助社区有类似需求的系统更方便的构建自己的微前端系统,同时也希望通过社区的帮助将 qiankun 打磨的更加成熟完善。 目前 qiankun 已在蚂蚁内部服务了超过 2000 个线上应用,在易用性和完整性上,绝对是值得信赖的。 ## 💡 什么是微前端? > 微前端是一种通过独立发布功能来让多个团队共同构建现代 Web 应用的方式。 -- 微前端 微前端架构具有以下核心价值: - **技术栈无关** - 主框架不限制接入应用的技术栈,微应用具备完全自主权 - **独立开发、独立部署** - 微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新 - **增量升级** - 面对各种复杂场景,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略 - **独立运行时** - 每个微应用之间状态隔离,运行时状态不共享 微前端架构旨在解决单体应用在一个相对长的时间跨度下,由于参与的人员、团队的增多、变迁,从一个普通应用演变成一个巨石应用(Frontend Monolith)后,随之而来的应用不可维护的问题。这类问题在企业级 Web 应用中尤其常见。 ### 传统单体应用的问题 ```bash ┌─────────────────────────────────────┐ │ 单体前端应用 │ │ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │ │ │模块A│ │模块B│ │模块C│ │模块D│ │ │ └─────┘ └─────┘ └─────┘ └─────┘ │ │ 紧密耦合,难以维护 │ └─────────────────────────────────────┘ ``` ### 微前端架构 ```bash ┌─────────────────────────────────────┐ │ 主应用 │ │ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │ │ │应用A│ │应用B│ │应用C│ │应用D│ │ │ └─────┘ └─────┘ └─────┘ └─────┘ │ │ 独立开发、部署,技术栈无关 │ └─────────────────────────────────────┘ ``` ## 🎯 核心理念 qiankun 的核心设计理念是**去中心化运行时**,这意味着: - **🥄 简单** - 由于主应用微应用都能做到技术栈无关,qiankun 对于用户而言只是一个类似 jQuery 的库。你需要调用几个 qiankun 的 API 即可完成应用的微前端改造。同时由于 qiankun 的 HTML Entry 及沙箱的设计,使得微应用的接入像使用 iframe 一样简单。 - **🍡 解耦/技术栈无关** - 微前端的核心目标是将巨石应用拆解成若干可以自治的松耦合微应用,而 qiankun 的诸多设计均是秉持这一原则,如 HTML Entry、沙箱、应用间通信等。这样才能确保微应用真正具备独立开发、独立运行的能力。 ## 🏗️ 架构图 ```mermaid graph TD A[主应用] --> B[qiankun] B --> C[微应用 A] B --> D[微应用 B] B --> E[微应用 C] F[路由] --> A G[资源加载] --> B H[生命周期] --> B I[沙箱隔离] --> B ``` qiankun 基于以下核心能力: ### 🔄 生命周期管理 每个微应用都有完整的生命周期: - **bootstrap** - 应用初始化 - **mount** - 应用挂载 - **unmount** - 应用卸载 - **update** - 应用更新(可选) ### 🛡️ 沙箱隔离 - **JS 隔离** - 提供多种沙箱方案,确保应用间 JS 互不影响 - **CSS 隔离** - 通过样式作用域或 Shadow DOM 实现样式隔离 ### 📡 资源加载 - **HTML Entry** - 通过 HTML 作为入口加载微应用 - **预加载** - 支持应用资源预加载,提升用户体验 - **缓存** - 智能资源缓存策略 ## 🤔 为什么不是 iframe? 虽然 iframe 是实现微前端最自然的方案,但它也有一些严重的限制: - **URL 同步问题** - iframe 的 URL 和主应用的 URL 无法同步 - **UI 不一致** - iframe 处于完全隔离的上下文中,很难保持一致的 UI 样式 - **性能问题** - 每个 iframe 都会创建新的上下文,消耗更多内存和 CPU 资源 - **SEO 不友好** - 搜索引擎无法正确索引 iframe 内容 - **安全限制** - 跨域 iframe 通信存在安全限制 - **用户体验问题** - 浏览器历史记录、刷新、书签等功能存在问题 qiankun 通过提供完整的微前端解决方案来解决这些问题,既保持了 iframe 的隔离优势,又避免了其局限性。 ## ✨ 功能特性 qiankun 提供以下关键特性: - **📦 基于 single-spa** - 基于 single-spa 提供更加开箱即用的 API - **📱 技术栈无关** - 任意 JavaScript 框架都可以使用/接入,无论是 React/Vue/Angular/jQuery 还是其他框架 - **💪 HTML Entry 接入方式** - 让你接入微应用像使用 iframe 一样简单 - **🛡️ 样式隔离** - 确保应用间样式不会相互影响 - **🧳 JS 沙箱** - 确保微应用间全局变量/事件不冲突 - **⚡ 资源预加载** - 在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度 - **🔌 Umi 插件** - 提供了 @umijs/plugin-qiankun 供 umi 应用一键切换成微前端架构系统 ## 🎯 适用场景 qiankun 特别适合以下场景: - **大型企业应用** - 多团队协作开发 - **技术栈迁移** - 渐进式升级遗留系统 - **功能模块化** - 功能模块独立开发部署 - **第三方集成** - 集成外部应用或服务 ## 🚀 开始使用 准备开始使用 qiankun?查看我们的[快速开始](/zh-CN/guide/quick-start)指南,几分钟内构建你的第一个微前端应用! ## 📚 深入学习 - [教程](/zh-CN/guide/tutorial) - 从零开始的详细教程 - [核心概念](/zh-CN/guide/concepts) - 理解 qiankun 的设计原理 - [主应用](/zh-CN/guide/main-app) - 如何配置主应用 - [微应用](/zh-CN/guide/micro-app) - 如何改造现有应用 ================================================ FILE: docs/zh-CN/guide/quick-start.md ================================================ # 快速开始 本指南将帮助你在 5 分钟内搭建一个基础的 qiankun 微前端应用。 ## 前置条件 - Node.js 16+ - 基础的 JavaScript/TypeScript 知识 - 了解 React、Vue 或其他前端框架 ## 🚀 步骤 1:安装 qiankun ::: code-group ```bash [npm] npm install qiankun ``` ```bash [yarn] yarn add qiankun ``` ```bash [pnpm] pnpm add qiankun ``` ::: ## 🏠 步骤 2:主应用配置 ### 2.1 创建主应用 ```bash # 使用你喜欢的框架创建主应用 npx create-react-app main-app cd main-app npm install qiankun ``` ### 2.2 注册微应用 在主应用的入口文件中注册微应用: ```typescript // src/index.js import { registerMicroApps, start } from 'qiankun'; // 注册微应用 registerMicroApps([ { name: 'vue-app', // 微应用名称,唯一 entry: '//localhost:8080', // 微应用入口 container: '#subapp-viewport', // 微应用挂载节点 activeRule: '/vue', // 微应用激活规则 }, { name: 'react-app', entry: '//localhost:3001', container: '#subapp-viewport', activeRule: '/react', }, ]); // 启动 qiankun start(); // 正常渲染主应用 ReactDOM.render(, document.getElementById('root')); ``` ### 2.3 创建微应用容器 在主应用中为微应用预留挂载节点: ```jsx // src/App.js import React from 'react'; import { BrowserRouter as Router, Link } from 'react-router-dom'; function App() { return (

qiankun 主应用

{/* 微应用挂载点 */}
); } export default App; ``` ## 📦 步骤 3:微应用配置 ### 3.1 创建 Vue 微应用 ```bash npm install -g @vue/cli vue create vue-micro-app cd vue-micro-app ``` ### 3.2 导出生命周期 修改 `src/main.js`: ```javascript import { createApp } from 'vue'; import { createRouter, createWebHistory } from 'vue-router'; import App from './App.vue'; import routes from './router'; let instance = null; let router = null; /** * 渲染函数 * 两种情况:主应用生命周期调用 / 微应用独立运行 */ function render(props = {}) { const { container } = props; router = createRouter({ history: createWebHistory(window.__POWERED_BY_QIANKUN__ ? '/vue' : '/'), routes, }); instance = createApp(App); instance.use(router); instance.mount(container ? container.querySelector('#app') : '#app'); } // 独立运行 if (!window.__POWERED_BY_QIANKUN__) { render(); } export async function bootstrap() { console.log('[vue] vue 应用启动'); } export async function mount(props) { console.log('[vue] 来自主框架的参数', props); render(props); } export async function unmount() { instance.unmount(); instance._container.innerHTML = ''; instance = null; router = null; } ``` ### 3.3 配置 Webpack 修改 `vue.config.js`: ```javascript const { defineConfig } = require('@vue/cli-service'); const packageName = require('./package.json').name; module.exports = defineConfig({ transpileDependencies: true, devServer: { port: 8080, headers: { 'Access-Control-Allow-Origin': '*', }, }, configureWebpack: { output: { library: `${packageName}-[name]`, libraryTarget: 'umd', chunkLoadingGlobal: `webpackJsonp_${packageName}`, }, }, }); ``` ### 3.4 创建 React 微应用 ```bash npx create-react-app react-micro-app cd react-micro-app npm install react-app-rewired --save-dev ``` 修改 `src/index.js`: ```javascript import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; function render(props) { const { container } = props; ReactDOM.render( , container ? container.querySelector('#root') : document.querySelector('#root') ); } if (!window.__POWERED_BY_QIANKUN__) { render({}); } export async function bootstrap() { console.log('[react16] react 应用启动'); } export async function mount(props) { console.log('[react16] 来自主框架的参数', props); render(props); } export async function unmount(props) { const { container } = props; ReactDOM.unmountComponentAtNode( container ? container.querySelector('#root') : document.querySelector('#root') ); } ``` 创建 `config-overrides.js`: ```javascript const { name } = require('./package'); module.exports = { webpack: (config) => { config.output.library = `${name}-[name]`; config.output.libraryTarget = 'umd'; config.output.chunkLoadingGlobal = `webpackJsonp_${name}`; return config; }, devServer: function (configFunction) { return function(proxy, allowedHost) { const config = configFunction(proxy, allowedHost); config.port = 3001; config.headers = { 'Access-Control-Allow-Origin': '*', }; return config; }; }, }; ``` 修改 `package.json` 中的脚本: ```json { "scripts": { "start": "react-app-rewired start", "build": "react-app-rewired build", "test": "react-app-rewired test", "eject": "react-scripts eject" } } ``` ## 🎉 步骤 4:启动应用 ### 4.1 启动所有应用 ```bash # 终端 1:启动主应用 cd main-app npm start # 终端 2:启动 Vue 微应用 cd vue-micro-app npm run serve # 终端 3:启动 React 微应用 cd react-micro-app npm start ``` ### 4.2 访问应用 - 主应用:http://localhost:3000 - 点击导航切换到不同的微应用 ## ✅ 验证集成 如果一切配置正确,你应该看到: 1. ✅ 主应用正常加载 2. ✅ 点击导航链接切换到对应的微应用 3. ✅ 微应用可以独立访问(http://localhost:8080, http://localhost:3001) 4. ✅ 浏览器控制台显示生命周期日志 ## 🎯 常见问题 ::: warning CORS 问题 确保你的微应用 webpack devServer 配置了 CORS 头: ```javascript headers: { 'Access-Control-Allow-Origin': '*', } ``` ::: ::: warning 路由冲突 在集成模式下,微应用路由需要添加对应前缀: ```javascript // Vue Router history: createWebHistory(window.__POWERED_BY_QIANKUN__ ? '/vue' : '/') // React Router ``` ::: ## 🚀 下一步 恭喜!你已经成功构建了第一个 qiankun 微前端应用。接下来你可以: - [核心概念](/zh-CN/guide/concepts) - 深入理解 qiankun 的设计原理 - [主应用](/zh-CN/guide/main-app) - 了解更多主应用配置选项 - [微应用](/zh-CN/guide/micro-app) - 学习如何改造现有应用 - [最佳实践](/zh-CN/cookbook/) - 学习生产环境最佳实践 ================================================ FILE: docs/zh-CN/guide/tutorial.md ================================================ # 教程 本教程适合新接触 `qiankun` 的人群,从零开始介绍如何构建一个 `qiankun` 项目。 ## 主应用 主应用不限技术栈,只需要提供一个容器 DOM,然后注册微应用并启动 qiankun 即可。 首先安装 `qiankun`: ```shell $ yarn add qiankun # 或者 npm i qiankun -S ``` 注册微应用并启动: ```js import { registerMicroApps, start } from 'qiankun'; registerMicroApps([ { name: 'angularApp', entry: '//localhost:4200', container: '#container', activeRule: '/app-angular', }, { name: 'reactApp', entry: '//localhost:3000', container: '#container', activeRule: '/app-react', }, { name: 'vueApp', entry: '//localhost:8080', container: '#container', activeRule: '/app-vue', }, ]); // 启动 qiankun start(); ``` ## 微应用 微应用分为有 `webpack` 构建和无 `webpack` 构建项目,有 `webpack` 的微应用(主要是指 Vue、React、Angular)需要做的事情有: 1. 新增 `public-path.js` 文件,用于修改运行时的 `publicPath`。[什么是运行时的 publicPath?](https://webpack.js.org/guides/public-path/#on-the-fly) ::: warning 注意:运行时的 `publicPath` 和构建时的 `publicPath` 是不同的,两者不能等价替代。 ::: 2. 微应用建议使用 `history` 模式的路由,需要设置路由 `base`,值和它的 `activeRule` 是一样的。 3. 在入口文件最顶部引入 `public-path.js`,修改并导出三个生命周期函数。 4. 修改 `webpack` 配置,在开发环境下允许跨域、并以 `umd` 格式打包。 主要的修改就是以上 4 个,可能根据项目的不同情况而改变。比如,如果你的项目是 `index.html` 和其他所有文件分开部署的,说明你们已经将构建时的 `publicPath` 设置为了完整路径,则不用修改运行时的 `publicPath`(第 1 步可省略)。 对于无 `webpack` 构建的微应用,只需要将生命周期函数挂载到 `window` 上即可。 ### React 微应用 以 `create react app` 生成的 `react 16` 项目为例,搭配 `react-router-dom` 5.x。 1. 在 `src` 目录新增 `public-path.js`: ```js if (window.__POWERED_BY_QIANKUN__) { __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; } ``` 2. 设置 `history` 模式路由的 `base`: ```html ``` 3. 入口文件 `index.js` 修改,为了避免根 id `#root` 与其他的 DOM 冲突,需要限制查找范围。 ```js import './public-path'; import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; function render(props) { const { container } = props; ReactDOM.render(, container ? container.querySelector('#root') : document.querySelector('#root')); } if (!window.__POWERED_BY_QIANKUN__) { render({}); } export async function bootstrap() { console.log('[react16] react app bootstraped'); } export async function mount(props) { console.log('[react16] props from main framework', props); render(props); } export async function unmount(props) { const { container } = props; ReactDOM.unmountComponentAtNode(container ? container.querySelector('#root') : document.querySelector('#root')); } ``` ::: tip 重要提示:在通过 ReactDOM.render 挂载子应用时,需要确保每个子应用都使用新的路由实例加载。 ::: 4. 修改 `webpack` 配置 安装插件 `@rescripts/cli`,当然你也可以选择其他的插件,例如 `react-app-rewired`。 ```bash npm i -D @rescripts/cli ``` 根目录新增 `.rescriptsrc.js`: ```js const { name } = require('./package'); module.exports = { webpack: (config) => { config.output.library = `${name}-[name]`; config.output.libraryTarget = 'umd'; config.output.jsonpFunction = `webpackJsonp_${name}`; config.output.globalObject = 'window'; return config; }, devServer: (_) => { const config = _; config.headers = { 'Access-Control-Allow-Origin': '*', }; config.historyApiFallback = true; config.hot = false; config.watchContentBase = false; config.liveReload = false; return config; }, }; ``` 修改 `package.json`: ```diff - "start": "react-scripts start", + "start": "rescripts start", - "build": "react-scripts build", + "build": "rescripts build", - "test": "react-scripts test", + "test": "rescripts test", - "eject": "react-scripts eject" ``` ### React MicroApp 组件 1. 安装 ```bash npm i qiankun npm i @qiankunjs/react ``` 2. 使用 直接通过 `` 组件加载(或卸载)子应用,该组件提供了加载和错误捕获相关能力: ```tsx import { MicroApp } from '@qiankunjs/react'; export default function Page() { return ; } ``` ### Vue 微应用 以 `vue-cli 3+` 生成的 `vue 2.x` 项目为例,`vue 3` 版本等稳定之后再补充。 1. 在 `src` 目录新增 `public-path.js`: ```js if (window.__POWERED_BY_QIANKUN__) { __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; } ``` 2. 入口文件 `main.js` 修改,为了避免根 id `#app` 与其他的 DOM 冲突,需要限制查找范围。 ```js import './public-path'; import Vue from 'vue'; import VueRouter from 'vue-router'; import App from './App.vue'; import routes from './router'; import store from './store'; Vue.config.productionTip = false; let router = null; let instance = null; function render(props = {}) { const { container } = props; router = new VueRouter({ base: window.__POWERED_BY_QIANKUN__ ? '/app-vue/' : '/', mode: 'history', routes, }); instance = new Vue({ router, store, render: (h) => h(App), }).$mount(container ? container.querySelector('#app') : '#app'); } // 独立运行时 if (!window.__POWERED_BY_QIANKUN__) { render(); } export async function bootstrap() { console.log('[vue] vue app bootstraped'); } export async function mount(props) { console.log('[vue] props from main framework', props); render(props); } export async function unmount() { instance.$destroy(); instance.$el.innerHTML = ''; instance = null; router = null; } ``` 3. 修改 `webpack` 配置(`vue.config.js`): ```js const { name } = require('./package'); module.exports = { devServer: { headers: { 'Access-Control-Allow-Origin': '*', }, }, configureWebpack: { output: { library: `${name}-[name]`, libraryTarget: 'umd', // 把微应用打包成 umd 库格式 jsonpFunction: `webpackJsonp_${name}`, }, }, }; ``` ### Vue MicroApp 组件 1. 安装 ```bash npm i qiankun npm i @qiankunjs/vue ``` 2. 使用 直接通过 `` 组件加载(或卸载)子应用,该组件提供了加载和错误捕获相关能力: ```vue ``` ### Angular 微应用 以 `Angular-cli 9` 生成的 `angular 9` 项目为例,其他版本的 `angular` 后续再补充。 1. 在 `src` 目录新增 `public-path.js` 文件,内容为: ```js if (window.__POWERED_BY_QIANKUN__) { // eslint-disable-next-line no-undef __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; } ``` 2. 设置 `history` 模式路由的 `base`,`src/app/app-routing.module.ts` 文件: ```diff + import { APP_BASE_HREF } from '@angular/common'; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], // @ts-ignore + providers: [{ provide: APP_BASE_HREF, useValue: window.__POWERED_BY_QIANKUN__ ? '/app-angular' : '/' }] }) ``` 3. 修改入口文件,`src/main.ts` 文件: ```ts import './public-path'; import { enableProdMode, NgModuleRef } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; if (environment.production) { enableProdMode(); } let app: void | NgModuleRef; async function render() { app = await platformBrowserDynamic() .bootstrapModule(AppModule) .catch((err) => console.error(err)); } if (!(window as any).__POWERED_BY_QIANKUN__) { render(); } export async function bootstrap(props: Object) { console.log(props); } export async function mount(props: Object) { render(); } export async function unmount(props: Object) { console.log(props); // @ts-ignore app.destroy(); } ``` 4. 修改 `webpack` 打包配置 先安装 `@angular-builders/custom-webpack` 插件,**注意:`Angular 9` 项目只能安装 `9.x` 版本,`angular 10` 项目可以安装最新版本**。 ```bash npm i @angular-builders/custom-webpack@9.2.0 -D ``` 根目录增加 `custom-webpack.config.js` 文件,内容为: ```js const appName = require('./package.json').name; module.exports = { devServer: { headers: { 'Access-Control-Allow-Origin': '*', }, }, output: { library: `${appName}-[name]`, libraryTarget: 'umd', jsonpFunction: `webpackJsonp_${appName}`, }, }; ``` 修改 `angular.json`,将 `[项目名称] > architect > build > builder` 和 `[项目名称] > architect > serve > builder` 的值改为我们安装的插件,将我们的 webpack 配置文件加入到 `[项目名称] > architect > build > options`。 ```diff - "builder": "@angular-devkit/build-angular:browser", + "builder": "@angular-builders/custom-webpack:browser", "options": { + "customWebpackConfig": { + "path": "./custom-webpack.config.js" + } } ``` ```diff - "builder": "@angular-devkit/build-angular:dev-server", + "builder": "@angular-builders/custom-webpack:dev-server", ``` 5. 解决 `zone.js` 的问题 在**主应用**中导入 `zone.js`,需要在 `import qiankun` 之前导入。 将微应用的 `src/polyfills.ts` 里面的 `zone.js` 导入删除。 ```diff - import 'zone.js/dist/zone'; ``` 在微应用的 `src/index.html` 的 `` 中加入如下内容,用于独立访问微应用时使用。 ```html ``` 6. 修复 `ng build` 命令报错的问题,修改 `tsconfig.json` 文件,参考 [issues/431](https://github.com/umijs/qiankun/issues/431)。 ```diff - "target": "es2015", + "target": "es5", + "typeRoots": [ + "node_modules/@types" + ], ``` 7. 为了防止主应用或其他微应用也为 `angular` 时,`` 发生冲突,建议给 `` 加上一个唯一的 id,比如说当前应用名称。 src/index.html: ```diff - + ``` src/app/app.component.ts: ```diff - selector: 'app-root', + selector: '#angular9 app-root', ``` 当然,你也可以选择使用 `single-spa-angular` 插件,参考 [single-spa-angular 官网](https://single-spa.js.org/docs/ecosystem-angular) 和 [angular demo](https://github.com/umijs/qiankun/tree/master/examples/angular9) (**补充**)angular7 的步骤和 angular9 基本一致,除了第 4 步。angular7 修改 `webpack` 配置的步骤如下: 除了安装 7.x 版本的 `angular-builders/custom-webpack` 外,还需要安装 `angular-builders/dev-server`。 ```bash npm i @angular-builders/custom-webpack@7 -D npm i @angular-builders/dev-server -D ``` 根目录增加 `custom-webpack.config.js` 文件,内容同上。 修改 `angular.json`,`[项目名称] > architect > build > builder` 和 Angular9 一样,`[项目名称] > architect > serve > builder` 和 Angular9 不一样。 ```diff - "builder": "@angular-devkit/build-angular:browser", + "builder": "@angular-builders/custom-webpack:browser", "options": { + "customWebpackConfig": { + "path": "./custom-webpack.config.js" + } } ``` ```diff - "builder": "@angular-devkit/build-angular:dev-server", + "builder": "@angular-builders/dev-server:generic", ``` ### 无 webpack 构建的微应用 一些不是由 `webpack` 构建的应用,比如 `jQuery` 应用、`jsp` 应用,都可以按照这个处理。 修改前请确保你的项目里的图片、音视频等资源能够正常加载。如果这些资源的地址都是完整路径(例如 `https://qiankun.umijs.org/logo.png`),则没有问题。如果都是相对路径,需要先将这些资源上传到服务器,再引用完整路径。 我们需要声明一个 script 标签,来导出相应的生命周期钩子。 示例: 1. 声明 entry 入口 ```diff Purehtml Example
Purehtml Example
+ ``` 2. 在 entry js 里导出生命周期 ```javascript const render = ($) => { $('#purehtml-container').html('Hello, render with jQuery'); return Promise.resolve(); }; ((global) => { global['purehtml'] = { bootstrap: () => { console.log('purehtml bootstrap'); return Promise.resolve(); }, mount: () => { console.log('purehtml mount'); return render($); }, unmount: () => { console.log('purehtml unmount'); return Promise.resolve(); }, }; })(window); ``` 可以参考 [purehtml 例子](https://github.com/umijs/qiankun/tree/master/examples/purehtml) 同时,[子应用必须支持跨域](/faq#子应用静态资源一定要支持跨域吗) ### umi-qiankun 应用 关于 `umi-qiankun` 的教程请前往 [umi 官网](https://umijs.org/zh-CN/plugins/plugin-qiankun) 和 [umi-qiankun 官方 demo](https://github.com/umijs/umi-plugin-qiankun/tree/master/examples) ================================================ FILE: docs/zh-CN/index.md ================================================ --- layout: home hero: name: Qiankun text: 微前端解决方案 tagline: 可能是你见过的最完善的微前端解决方案🧐 image: src: /logo.png alt: Qiankun actions: - theme: brand text: 快速开始 link: /zh-CN/guide/quick-start - theme: alt text: 在 GitHub 上查看 link: https://github.com/umijs/qiankun features: - icon: 🚀 title: 简单 details: 兼容任何 JavaScript 框架。构建微前端系统就像使用 iframe 一样简单,但实际上不是 iframe。 - icon: 🛡️ title: 完整 details: 包含构建微前端系统所需的几乎所有基本功能,如样式隔离、JS 沙箱、预加载等。 - icon: 🔧 title: 生产就绪 details: 已经过蚂蚁集团内外大量线上应用的广泛测试和打磨,健壮性值得信赖。 - icon: ⚡ title: 高性能 details: 支持应用预加载,优化用户体验并提高应用切换速度。 - icon: 🎯 title: 技术栈无关 details: 主应用不限制接入应用的技术栈,微应用具备完全自主权。 - icon: 🔄 title: 状态隔离 details: 提供完整的 JS 沙箱机制,确保应用之间不会相互影响。 --- ## 📦 安装 ::: code-group ```bash [npm] npm install qiankun ``` ```bash [yarn] yarn add qiankun ``` ```bash [pnpm] pnpm add qiankun ``` ::: ## 🔨 快速开始 ### 主应用 ```typescript import { registerMicroApps, start } from 'qiankun'; // 注册微应用 registerMicroApps([ { name: 'reactApp', entry: '//localhost:7100', container: '#yourContainer', activeRule: '/yourActiveRule', }, { name: 'vueApp', entry: { scripts: ['//localhost:7100/main.js'] }, container: '#yourContainer2', activeRule: '/yourActiveRule2', }, ]); // 启动 qiankun start(); ``` ### 微应用 ```typescript /** * bootstrap 只会在微应用初始化的时候调用一次 * mount 会在每次进入微应用时调用 * unmount 会在每次切出/卸载微应用时调用 */ export async function bootstrap() { console.log('react app bootstraped'); } export async function mount(props) { ReactDOM.render(, props.container ? props.container.querySelector('#root') : document.getElementById('root')); } export async function unmount(props) { ReactDOM.unmountComponentAtNode( props.container ? props.container.querySelector('#root') : document.getElementById('root'), ); } ``` ## 🌟 为什么选择 qiankun?

🎯 零侵入

对现有应用几乎零侵入,只需要暴露必要的生命周期函数即可

📱 全场景

支持基于路由的微应用加载和手动加载模式

🔒 安全隔离

完整的沙箱解决方案,包括 JS 隔离和 CSS 隔离

⚡ 高性能

支持预加载、缓存等多种性能优化方案

## 👥 社区 | GitHub 讨论 | 钉钉群 | 微信群 | | --- | --- | --- | | [qiankun 讨论](https://github.com/umijs/qiankun/discussions) | 钉钉群二维码 | [查看群二维码](https://github.com/umijs/qiankun/discussions/2343) | ================================================ FILE: examples/main/index.html ================================================ Qiankun Micro-Frontend Demo
================================================ FILE: examples/main/package.json ================================================ { "name": "qiankun-main-app", "version": "2.0.0", "description": "Qiankun main application with modern tech stack", "type": "module", "scripts": { "dev": "vite", "build": "tsc && vite build", "preview": "vite preview", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "type-check": "tsc --noEmit" }, "dependencies": { "@ant-design/icons": "^5.2.6", "antd": "^5.12.0", "clsx": "^2.0.0", "qiankun": "^3.0.0-rc.0", "react": "^18.2.0", "react-dom": "^18.2.0", "tailwind-merge": "^2.1.0", "zustand": "^4.4.7" }, "devDependencies": { "@types/react": "^18.2.43", "@types/react-dom": "^18.2.17", "@typescript-eslint/eslint-plugin": "^6.14.0", "@typescript-eslint/parser": "^6.14.0", "@vitejs/plugin-react": "^4.2.1", "autoprefixer": "^10.4.16", "eslint": "^8.55.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.5", "postcss": "^8.4.32", "tailwindcss": "^3.3.6", "typescript": "^5.2.2", "vite": "^5.0.8" } } ================================================ FILE: examples/main/postcss.config.js ================================================ export default { plugins: { tailwindcss: {}, autoprefixer: {}, }, } ================================================ FILE: examples/main/src/App.tsx ================================================ import { useEffect } from 'react'; import { Layout } from 'antd'; import { useQiankunStore } from './store/qiankun'; import Sidebar from './components/Sidebar'; import Header from './components/Header'; import MicroAppContainer from './components/MicroAppContainer'; const { Content } = Layout; function App() { const { initGlobalState } = useQiankunStore(); useEffect(() => { // Initialize qiankun global state initGlobalState({ user: { name: 'Qiankun User', avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=qiankun', }, theme: 'light', }); }, [initGlobalState]); return (
); } export default App; ================================================ FILE: examples/main/src/components/Dashboard.tsx ================================================ import { Row, Col, Card, Statistic, Typography, Tag, Space, Button } from 'antd'; import { AppstoreOutlined, ThunderboltOutlined, SafetyOutlined, GlobalOutlined, RocketOutlined, CodeOutlined, BranchesOutlined, } from '@ant-design/icons'; import { useQiankunStore } from '../store/qiankun'; const { Title, Text, Paragraph } = Typography; const microApps = [ { name: 'React', icon: '⚛️', color: '#61DAFB', status: 'active' }, { name: 'Vue', icon: '💚', color: '#4FC08D', status: 'active' }, { name: 'Pure HTML', icon: '🌐', color: '#E34F26', status: 'active' }, { name: 'Vite App', icon: '⚡', color: '#646CFF', status: 'active' }, ]; const features = [ { icon: , title: '极速加载', desc: '基于 Qiankun 的微前端架构,实现秒级应用切换' }, { icon: , title: '沙箱隔离', desc: '完善的 JS/CSS 沙箱机制,确保应用间互不干扰' }, { icon: , title: '技术栈无关', desc: '支持 React、Vue 等多种前端框架' }, { icon: , title: '状态共享', desc: '内置全局状态管理,实现跨应用数据通信' }, ]; export default function Dashboard() { const { setActiveApp } = useQiankunStore(); return (
欢迎使用 Qiankun 下一代微前端解决方案演示平台
Qiankun 是一个基于 single-spa 的微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。
4
示例应用
{[ { title: '已接入应用', value: 4, suffix: '个', color: '#0ea5e9' }, { title: '支持框架', value: 3, suffix: '种', color: '#10b981' }, { title: '在线用户', value: 128, suffix: '人', color: '#f59e0b' }, { title: '系统运行', value: 99.9, suffix: '%', color: '#8b5cf6' }, ].map((stat, index) => ( ))} 子应用列表
} className="mb-8"> {microApps.slice(0, 8).map((app) => ( setActiveApp(app.name.toLowerCase().replace(' ', ''))}>
{app.icon}
{app.name}
{app.status === 'active' ? '运行中' : '即将上线'}
))}
{features.map((feature, index) => (
{feature.icon}
{feature.title}
{feature.desc}
))}
); } ================================================ FILE: examples/main/src/components/Header.tsx ================================================ import { Layout, Button, Badge, Avatar, Dropdown, Space, Typography, Tooltip, theme } from 'antd'; import { BellOutlined, GithubOutlined, FullscreenOutlined, FullscreenExitOutlined, GlobalOutlined, UserOutlined, SettingOutlined, LogoutOutlined, } from '@ant-design/icons'; import { useState } from 'react'; import { useQiankunStore } from '../store/qiankun'; const { Header: AntHeader } = Layout; const { Text } = Typography; export default function Header() { const { globalState } = useQiankunStore(); const [isFullscreen, setIsFullscreen] = useState(false); const { token } = theme.useToken(); const toggleFullscreen = () => { if (!document.fullscreenElement) { document.documentElement.requestFullscreen(); setIsFullscreen(true); } else { document.exitFullscreen(); setIsFullscreen(false); } }; const userMenuItems = [ { key: 'profile', icon: , label: '个人中心' }, { key: 'settings', icon: , label: '系统设置' }, { type: 'divider' }, { key: 'logout', icon: , label: '退出登录', danger: true }, ]; const notificationItems = [ { key: '1', label: (
React 16 应用已加载
2 分钟前
), }, { key: '2', label: (
Vue 3 应用加载失败
5 分钟前
), }, ]; return (
欢迎使用 Qiankun 微前端演示平台
, , ]} />
); } return (
{loading && (

正在加载子应用...

)}
); } ================================================ FILE: examples/main/src/components/Sidebar.tsx ================================================ import { useState } from 'react'; import { Layout, Menu, Typography, Space, Tag } from 'antd'; import { useQiankunStore } from '../store/qiankun'; import { HomeOutlined, CodeOutlined, NodeIndexOutlined, ThunderboltOutlined, Html5Outlined, MenuFoldOutlined, MenuUnfoldOutlined, SettingOutlined, QuestionCircleOutlined, ExperimentOutlined, } from '@ant-design/icons'; const { Sider } = Layout; const { Title, Text } = Typography; const microApps = [ { key: 'home', name: '首页', icon: , description: 'Dashboard 概览' }, { key: 'react', name: 'React', icon: , description: 'React 18 + Vite', color: '#61DAFB' }, { key: 'vue', name: 'Vue', icon: , description: 'Vue 3 + Vite', color: '#4FC08D' }, { key: 'purehtml', name: 'Pure HTML', icon: , description: 'Vanilla JS', color: '#E34F26' }, { key: 'vite', name: 'Vite App', icon: , description: 'Vite + React', color: '#646CFF' }, ]; export default function Sidebar() { const [collapsed, setCollapsed] = useState(false); const { activeApp, setActiveApp } = useQiankunStore(); const handleMenuClick = (key: string) => { setActiveApp(key === 'home' ? null : key); }; return (
Qiankun Micro-Frontend
handleMenuClick(key)} className="border-0 pt-2" style={{ background: 'transparent' }} > {microApps.map((app) => (
{app.name} {app.color && !collapsed && ( {app.key.includes('react') ? 'React' : app.key.includes('vue') ? 'Vue' : app.key.includes('angular') ? 'Angular' : 'Other'} )}
{!collapsed &&
{app.description}
}
))}
{!collapsed && (
v2.0.0
)}
); } ================================================ FILE: examples/main/src/main.tsx ================================================ import React from 'react'; import ReactDOM from 'react-dom/client'; import { ConfigProvider } from 'antd'; import App from './App'; import './styles/index.css'; // Ant Design 主题配置 const antdConfig = { theme: { token: { colorPrimary: '#0ea5e9', colorSuccess: '#10b981', colorWarning: '#f59e0b', colorError: '#ef4444', borderRadius: 8, fontFamily: 'Inter, system-ui, -apple-system, sans-serif', }, }, }; ReactDOM.createRoot(document.getElementById('root')!).render( ); ================================================ FILE: examples/main/src/store/qiankun.ts ================================================ import { create } from 'zustand'; export interface GlobalState { user?: { name: string; avatar?: string; }; theme?: 'light' | 'dark'; [key: string]: any; } interface QiankunStore { globalState: GlobalState; activeApp: string | null; loading: boolean; error: string | null; retryCount: number; // Actions initGlobalState: (state: GlobalState) => void; setGlobalState: (state: Partial) => void; setActiveApp: (app: string | null) => void; setLoading: (loading: boolean) => void; setError: (error: string | null) => void; retry: () => void; } export const useQiankunStore = create((set, get) => ({ globalState: {}, activeApp: null, loading: false, error: null, retryCount: 0, initGlobalState: (state: GlobalState) => { set({ globalState: state, }); }, setGlobalState: (state: Partial) => { const { globalState } = get(); set({ globalState: { ...globalState, ...state } }); }, setActiveApp: (app: string | null) => { set({ activeApp: app, error: null }); }, setLoading: (loading: boolean) => { set({ loading }); }, setError: (error: string | null) => { set({ error }); }, retry: () => { set((state) => ({ error: null, retryCount: state.retryCount + 1 })); }, })); ================================================ FILE: examples/main/src/styles/index.css ================================================ @tailwind base; @tailwind components; @tailwind utilities; /* Custom scrollbar */ ::-webkit-scrollbar { width: 6px; height: 6px; } ::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 3px; } ::-webkit-scrollbar-thumb:hover { background: #94a3b8; } /* Smooth scrolling */ html { scroll-behavior: smooth; } /* Focus outline */ *:focus-visible { outline: 2px solid #3b82f6; outline-offset: 2px; } /* Micro-app container styles */ .micro-app-container { min-height: calc(100vh - 64px); background: #f8fafc; } /* Loading animation */ @keyframes spin { to { transform: rotate(360deg); } } .animate-spin { animation: spin 1s linear infinite; } /* Fade in animation */ @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } .animate-fade-in { animation: fadeIn 0.3s ease-out forwards; } /* Card hover effect */ .card-hover { transition: all 0.3s ease; } .card-hover:hover { transform: translateY(-2px); box-shadow: 0 10px 40px -10px rgba(0, 0, 0, 0.1); } ================================================ FILE: examples/main/src/vite-env.d.ts ================================================ /// ================================================ FILE: examples/main/tailwind.config.js ================================================ /** @type {import('tailwindcss').Config} */ export default { content: [ "./index.html", "./src/**/*.{js,ts,jsx,tsx}", ], theme: { extend: { colors: { primary: { 50: '#f0f9ff', 100: '#e0f2fe', 200: '#bae6fd', 300: '#7dd3fc', 400: '#38bdf8', 500: '#0ea5e9', 600: '#0284c7', 700: '#0369a1', 800: '#075985', 900: '#0c4a6e', }, secondary: { 50: '#fdf4ff', 100: '#fae8ff', 200: '#f5d0fe', 300: '#f0abfc', 400: '#e879f9', 500: '#d946ef', 600: '#c026d3', 700: '#a21caf', 800: '#86198f', 900: '#701a75', }, }, fontFamily: { sans: ['Inter', 'system-ui', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'Helvetica Neue', 'Arial', 'sans-serif'], }, animation: { 'fade-in': 'fadeIn 0.5s ease-in-out', 'slide-in-right': 'slideInRight 0.3s ease-out', 'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite', }, keyframes: { fadeIn: { '0%': { opacity: '0' }, '100%': { opacity: '1' }, }, slideInRight: { '0%': { transform: 'translateX(100%)', opacity: '0' }, '100%': { transform: 'translateX(0)', opacity: '1' }, }, }, }, }, corePlugins: { preflight: false, }, plugins: [], } ================================================ FILE: examples/main/tsconfig.json ================================================ { "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, "moduleResolution": "bundler", "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", "strict": true, "noUnusedLocals": false, "noUnusedParameters": false, "noFallthroughCasesInSwitch": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "baseUrl": ".", "paths": { "@/*": ["src/*"] } }, "include": ["src"], "references": [{ "path": "./tsconfig.node.json" }] } ================================================ FILE: examples/main/tsconfig.node.json ================================================ { "compilerOptions": { "composite": true, "skipLibCheck": true, "module": "ESNext", "moduleResolution": "bundler", "allowSyntheticDefaultImports": true }, "include": ["vite.config.ts"] } ================================================ FILE: examples/main/vite.config.ts ================================================ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import path from 'path' // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], resolve: { alias: { '@': path.resolve(__dirname, './src'), }, }, server: { port: 7099, cors: true, }, build: { outDir: 'dist', sourcemap: true, }, }) ================================================ FILE: examples/purehtml/entry.js ================================================ const render = $ => { $('#purehtml-container').html('Hello, render with jQuery'); return Promise.resolve(); }; (global => { global['purehtml'] = { bootstrap: () => { console.log('purehtml bootstrap'); return Promise.resolve(); }, mount: () => { console.log('purehtml mount'); return render($); }, unmount: () => { console.log('purehtml unmount'); return Promise.resolve(); }, }; })(window); ================================================ FILE: examples/purehtml/index.html ================================================ Purehtml Example
Purehtml Example
================================================ FILE: examples/purehtml/package.json ================================================ { "name": "purehtml", "version": "1.0.0", "description": "", "main": "index.html", "scripts": { "start": "cross-env PORT=7104 http-server . --cors", "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "MIT", "devDependencies": { "cross-env": "^7.0.2", "http-server": "^0.12.1" } } ================================================ FILE: examples/react/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? ================================================ FILE: examples/react/README.md ================================================ # React + TypeScript + Vite This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. Currently, two official plugins are available: - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh ## Expanding the ESLint configuration If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: - Configure the top-level `parserOptions` property like this: ```js export default tseslint.config({ languageOptions: { // other options... parserOptions: { project: ['./tsconfig.node.json', './tsconfig.app.json'], tsconfigRootDir: import.meta.dirname, }, }, }) ``` - Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked` - Optionally add `...tseslint.configs.stylisticTypeChecked` - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config: ```js // eslint.config.js import react from 'eslint-plugin-react' export default tseslint.config({ // Set the react version settings: { react: { version: '18.3' } }, plugins: { // Add the react plugin react, }, rules: { // other rules... // Enable its recommended rules ...react.configs.recommended.rules, ...react.configs['jsx-runtime'].rules, }, }) ``` ================================================ FILE: examples/react/config/qiankunHtml.ts ================================================ import type { IndexHtmlTransformContext, PluginOption } from 'vite'; import { load } from 'cheerio'; export default function qiankunHtmlPlugin(): PluginOption { return { name: 'qiankun-html-transform', enforce: 'post', apply: 'build', transformIndexHtml(html: string, ctx: IndexHtmlTransformContext) { if (!ctx || !isQiankunBuild(ctx)) return html; return transformHtml(html); }, }; } function isQiankunBuild(ctx: IndexHtmlTransformContext): boolean { return ctx.bundle !== undefined && ctx.server === undefined; } function rewriteSystemImport(script: string): string { const parentUrl = 'window.__POWERED_BY_QIANKUN__ ? window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ : document.baseURI'; if (script.includes(parentUrl)) { return script; } const legacyEntryPattern = "document.getElementById('vite-legacy-entry').getAttribute('data-src')"; if (script.includes(legacyEntryPattern)) { return script.replace( `System.import(${legacyEntryPattern})`, `System.import(${legacyEntryPattern}, ${parentUrl})` ); } return script; } function transformHtml(html: string): string { const $ = load(html); $('script[type="module"]').remove(); $('link[rel="modulepreload"]').remove(); $('script[nomodule]').each((_, el) => { const scriptContent = $(el).html(); if (!scriptContent || !scriptContent.includes('System.import')) return; const updated = rewriteSystemImport(scriptContent); if (updated !== scriptContent) { $(el).text(updated); } }); return $.html(); } ================================================ FILE: examples/react/eslint.config.js ================================================ import js from '@eslint/js' import globals from 'globals' import reactHooks from 'eslint-plugin-react-hooks' import reactRefresh from 'eslint-plugin-react-refresh' import tseslint from 'typescript-eslint' export default tseslint.config( { ignores: ['dist'] }, { extends: [js.configs.recommended, ...tseslint.configs.recommended], files: ['**/*.{ts,tsx}'], languageOptions: { ecmaVersion: 2020, globals: globals.browser, }, plugins: { 'react-hooks': reactHooks, 'react-refresh': reactRefresh, }, rules: { ...reactHooks.configs.recommended.rules, 'react-refresh/only-export-components': [ 'warn', { allowConstantExport: true }, ], }, }, ) ================================================ FILE: examples/react/index.html ================================================ Vite + React + TS
================================================ FILE: examples/react/package.json ================================================ { "name": "react", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", "build": "tsc -b && vite build", "lint": "eslint .", "preview": "vite preview", "build:qiankun": "vite build --mode qiankun", "preview:qiankun": "vite preview --mode qiankun" }, "dependencies": { "@qiankunjs/react": "^0.0.1-rc.0", "qiankun": "^3.0.0-rc.0", "react": "^18.3.1", "react-dom": "^18.3.1", "vite-plugin-qiankun": "^1.0.15" }, "devDependencies": { "@eslint/js": "^9.13.0", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "@vitejs/plugin-legacy": "^5.4.2", "@vitejs/plugin-react": "^4.3.3", "cheerio": "^1.0.0", "eslint": "^9.13.0", "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-refresh": "^0.4.14", "globals": "^15.11.0", "typescript": "~5.6.2", "typescript-eslint": "^8.11.0", "vite": "^5.4.10" } } ================================================ FILE: examples/react/src/App.css ================================================ #root { max-width: 1280px; margin: 0 auto; padding: 2rem; text-align: center; } .logo { height: 6em; padding: 1.5em; will-change: filter; transition: filter 300ms; } .logo:hover { filter: drop-shadow(0 0 2em #646cffaa); } .logo.react:hover { filter: drop-shadow(0 0 2em #61dafbaa); } @keyframes logo-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } @media (prefers-reduced-motion: no-preference) { a:nth-of-type(2) .logo { animation: logo-spin infinite 20s linear; } } .card { padding: 2em; } .read-the-docs { color: #888; } ================================================ FILE: examples/react/src/App.tsx ================================================ import { useState, version } from 'react' import reactLogo from './assets/react.svg' import viteLogo from '/vite.svg' import './App.css' function App() { const [count, setCount] = useState(0) return ( <>

React Micro App

React Version: {version}

Bundler: Vite

Running in: {(window as any).__POWERED_BY_QIANKUN__ ? 'qiankun' : 'standalone'} mode

This React micro-app is built with modern tools and can run both standalone and inside a qiankun main app.

Generated by create-qiankun

) } export default App ================================================ FILE: examples/react/src/index.css ================================================ :root { font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; line-height: 1.5; font-weight: 400; color-scheme: light dark; color: rgba(255, 255, 255, 0.87); background-color: #242424; font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } a { font-weight: 500; color: #646cff; text-decoration: inherit; } a:hover { color: #535bf2; } body { margin: 0; display: flex; place-items: center; min-width: 320px; min-height: 100vh; } h1 { font-size: 3.2em; line-height: 1.1; } button { border-radius: 8px; border: 1px solid transparent; padding: 0.6em 1.2em; font-size: 1em; font-weight: 500; font-family: inherit; background-color: #1a1a1a; cursor: pointer; transition: border-color 0.25s; } button:hover { border-color: #646cff; } button:focus, button:focus-visible { outline: 4px auto -webkit-focus-ring-color; } @media (prefers-color-scheme: light) { :root { color: #213547; background-color: #ffffff; } a:hover { color: #747bff; } button { background-color: #f9f9f9; } } ================================================ FILE: examples/react/src/main.tsx ================================================ import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; import './index.css'; declare global { interface Window { __POWERED_BY_QIANKUN__?: boolean; [key: string]: unknown; } } let root: ReactDOM.Root | undefined; function render(props: { container?: Element } = {}) { const container = props.container?.querySelector('#root') ?? document.getElementById('root'); if (!container) return; root = ReactDOM.createRoot(container); root.render( , ); } function bootstrap() { console.log('[react] bootstrap'); return Promise.resolve(); } function mount(props: { container?: Element }) { console.log('[react] mount', props); render(props); return Promise.resolve(); } function unmount(props: { container?: Element }) { console.log('[react] unmount', props); if (root) { root.unmount(); root = undefined; } const container = props.container?.querySelector('#root') ?? document.getElementById('root'); if (container) { container.innerHTML = ''; } return Promise.resolve(); } // Standalone mode if (!window.__POWERED_BY_QIANKUN__) { render(); } // Export lifecycle functions to window - MUST be last global assignment (function(global) { global['react'] = { bootstrap, mount, unmount, }; })(window); ================================================ FILE: examples/react/src/vite-env.d.ts ================================================ /// ================================================ FILE: examples/react/tsconfig.app.json ================================================ { "compilerOptions": { "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "target": "ES2020", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, /* Bundler mode */ "moduleResolution": "Bundler", "allowImportingTsExtensions": true, "isolatedModules": true, "moduleDetection": "force", "noEmit": true, "jsx": "react-jsx", /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true }, "include": ["src"] } ================================================ FILE: examples/react/tsconfig.json ================================================ { "files": [], "references": [ { "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" } ] } ================================================ FILE: examples/react/tsconfig.node.json ================================================ { "compilerOptions": { "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", "target": "ES2022", "lib": ["ES2023"], "module": "ESNext", "skipLibCheck": true, /* Bundler mode */ "moduleResolution": "Bundler", "allowImportingTsExtensions": true, "isolatedModules": true, "moduleDetection": "force", "noEmit": true, /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true }, "include": ["vite.config.ts"] } ================================================ FILE: examples/react/vite.config.ts ================================================ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import legacy from '@vitejs/plugin-legacy'; import qiankunHtmlPlugin from './config/qiankunHtml'; import qiankun from 'vite-plugin-qiankun'; export default defineConfig(({ mode }) => { const isQiankun = mode === 'qiankun'; return { base: isQiankun ? './' : '/', plugins: [ react(), qiankun('react', { useDevMode: true }), isQiankun && legacy({ renderLegacyChunks: true }), isQiankun && qiankunHtmlPlugin(), ].filter(Boolean), server: { port: 7100, cors: true, headers: { 'Access-Control-Allow-Origin': '*', }, }, build: { lib: isQiankun ? undefined : { entry: './src/main.tsx', name: 'react', formats: ['umd'], fileName: 'react' }, rollupOptions: isQiankun ? undefined : { external: [], output: { globals: {} } } }, }; }); ================================================ FILE: examples/vite/.eslintrc.cjs ================================================ module.exports = { env: { browser: true, es2020: true }, extends: [ 'eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:react-hooks/recommended', ], parser: '@typescript-eslint/parser', parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, plugins: ['react-refresh'], rules: { 'react-refresh/only-export-components': 'warn', }, } ================================================ FILE: examples/vite/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? ================================================ FILE: examples/vite/index.html ================================================ Vite + React + TS
================================================ FILE: examples/vite/package.json ================================================ { "name": "vite-micro-app", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", "build": "tsc && vite build", "preview": "vite preview" }, "dependencies": { "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { "@types/react": "^18.2.40", "@types/react-dom": "^18.2.17", "@vitejs/plugin-react": "^4.2.1", "typescript": "^5.2.2", "vite": "^5.0.8" } } ================================================ FILE: examples/vite/src/App.css ================================================ #root { max-width: 1280px; margin: 0 auto; padding: 2rem; text-align: center; } .logo { height: 6em; padding: 1.5em; will-change: filter; transition: filter 300ms; } .logo:hover { filter: drop-shadow(0 0 2em #646cffaa); } .logo.react:hover { filter: drop-shadow(0 0 2em #61dafbaa); } @keyframes logo-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } @media (prefers-reduced-motion: no-preference) { a:nth-of-type(2) .logo { animation: logo-spin infinite 20s linear; } } .card { padding: 2em; } .read-the-docs { color: #888; } ================================================ FILE: examples/vite/src/App.tsx ================================================ import { useState } from 'react'; import reactLogo from './assets/react.svg'; import viteLogo from '/vite.svg'; import './App.css'; function App() { const [count, setCount] = useState(0); return ( <>

Vite + React

Edit src/App.tsx and save to test HMR

Click on the Vite and React logos to learn more

); } export default App; ================================================ FILE: examples/vite/src/index.css ================================================ :root { font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; line-height: 1.5; font-weight: 400; color-scheme: light dark; color: rgba(255, 255, 255, 0.87); background-color: #242424; font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; -webkit-text-size-adjust: 100%; } a { font-weight: 500; color: #646cff; text-decoration: inherit; } a:hover { color: #535bf2; } body { margin: 0; display: flex; place-items: center; min-width: 320px; min-height: 100vh; } h1 { font-size: 3.2em; line-height: 1.1; } button { border-radius: 8px; border: 1px solid transparent; padding: 0.6em 1.2em; font-size: 1em; font-weight: 500; font-family: inherit; background-color: #1a1a1a; cursor: pointer; transition: border-color 0.25s; } button:hover { border-color: #646cff; } button:focus, button:focus-visible { outline: 4px auto -webkit-focus-ring-color; } @media (prefers-color-scheme: light) { :root { color: #213547; background-color: #ffffff; } a:hover { color: #747bff; } button { background-color: #f9f9f9; } } ================================================ FILE: examples/vite/src/main.tsx ================================================ import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App.tsx'; import './index.css'; async function bootstrap() { console.log('[react15] react app bootstraped'); } const containerMap = new WeakMap(); async function mount(props: any) { console.log('[react18] props from main framework', props); const container = props?.container ? props.container.querySelector('#root') : document.getElementById('root'); const root = ReactDOM.createRoot(container); root.render( , ); containerMap.set(container, root); } async function unmount(props: any) { const container = props.container ? props.container.querySelector('#root') : document.getElementById('root'); const root = containerMap.get(container); root.unmount(); } // @ts-ignore if (!window.__POWERED_BY_QIANKUN__) { bootstrap().then(mount); } window.vite = { bootstrap, mount, unmount, }; ================================================ FILE: examples/vite/src/vite-env.d.ts ================================================ /// ================================================ FILE: examples/vite/tsconfig.json ================================================ { "compilerOptions": { "target": "ESNext", "lib": ["DOM", "DOM.Iterable", "ESNext"], "module": "ESNext", "skipLibCheck": true, /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true }, "include": ["src"], "references": [{ "path": "./tsconfig.node.json" }] } ================================================ FILE: examples/vite/tsconfig.node.json ================================================ { "compilerOptions": { "composite": true, "skipLibCheck": true, "module": "ESNext", "moduleResolution": "bundler", "allowSyntheticDefaultImports": true }, "include": ["vite.config.ts"] } ================================================ FILE: examples/vite/vite.config.ts ================================================ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], base: 'http://localhost:5173/', }); ================================================ FILE: examples/vue/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? ================================================ FILE: examples/vue/README.md ================================================ # Vue 3 + TypeScript + Vite This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` ================================================ FILE: examples/vue/package.json ================================================ { "name": "vue", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", "build": "vue-tsc -b && vite build", "preview": "vite preview", "build:qiankun": "vite build --mode qiankun", "preview:qiankun": "vite preview --mode qiankun" }, "dependencies": { "vue": "^3.5.12", "qiankun": "^3.0.0-rc.0", "@qiankunjs/vue": "^0.0.1-rc.0" }, "devDependencies": { "@vitejs/plugin-vue": "^5.1.4", "typescript": "~5.6.2", "vite": "^5.4.10", "vue-tsc": "^2.1.8", "@vitejs/plugin-legacy": "^5.4.2", "cheerio": "^1.0.0" } } ================================================ FILE: examples/vue/src/App.vue ================================================ ================================================ FILE: examples/vue/src/components/HelloWorld.vue ================================================ ================================================ FILE: examples/vue/src/main.ts ================================================ import { createApp } from 'vue'; import App from './App.vue'; import './style.css'; declare global { interface Window { __POWERED_BY_QIANKUN__?: boolean; [key: string]: unknown; } } let app: ReturnType | undefined; function render(props: { container?: Element } = {}) { const container = props.container?.querySelector('#app') ?? document.getElementById('app'); if (!container) return; app = createApp(App); app.mount(container); } function bootstrap() { console.log('[vue] bootstrap'); return Promise.resolve(); } function mount(props: { container?: Element }) { console.log('[vue] mount', props); render(props); return Promise.resolve(); } function unmount(props: { container?: Element }) { console.log('[vue] unmount', props); if (app) { app.unmount(); app = undefined; } const container = props.container?.querySelector('#app') ?? document.getElementById('app'); if (container) { container.innerHTML = ''; } return Promise.resolve(); } // Export lifecycle functions to window immediately (function(global) { global['vue'] = { bootstrap, mount, unmount, }; })(window); // Standalone mode if (!window.__POWERED_BY_QIANKUN__) { render(); } ================================================ FILE: examples/vue/src/style.css ================================================ :root { font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; line-height: 1.5; font-weight: 400; color-scheme: light dark; color: rgba(255, 255, 255, 0.87); background-color: #242424; font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } a { font-weight: 500; color: #646cff; text-decoration: inherit; } a:hover { color: #535bf2; } body { margin: 0; display: flex; place-items: center; min-width: 320px; min-height: 100vh; } h1 { font-size: 3.2em; line-height: 1.1; } button { border-radius: 8px; border: 1px solid transparent; padding: 0.6em 1.2em; font-size: 1em; font-weight: 500; font-family: inherit; background-color: #1a1a1a; cursor: pointer; transition: border-color 0.25s; } button:hover { border-color: #646cff; } button:focus, button:focus-visible { outline: 4px auto -webkit-focus-ring-color; } .card { padding: 2em; } #app { max-width: 1280px; margin: 0 auto; padding: 2rem; text-align: center; } @media (prefers-color-scheme: light) { :root { color: #213547; background-color: #ffffff; } a:hover { color: #747bff; } button { background-color: #f9f9f9; } } ================================================ FILE: examples/vue/src/vite-env.d.ts ================================================ /// ================================================ FILE: examples/vue/tsconfig.app.json ================================================ { "compilerOptions": { "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "target": "ES2020", "useDefineForClassFields": true, "module": "ESNext", "lib": ["ES2020", "DOM", "DOM.Iterable"], "skipLibCheck": true, /* Bundler mode */ "moduleResolution": "Bundler", "allowImportingTsExtensions": true, "isolatedModules": true, "moduleDetection": "force", "noEmit": true, "jsx": "preserve", /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true }, "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] } ================================================ FILE: examples/vue/tsconfig.json ================================================ { "files": [], "references": [ { "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" } ] } ================================================ FILE: examples/vue/tsconfig.node.json ================================================ { "compilerOptions": { "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", "target": "ES2022", "lib": ["ES2023"], "module": "ESNext", "skipLibCheck": true, /* Bundler mode */ "moduleResolution": "Bundler", "allowImportingTsExtensions": true, "isolatedModules": true, "moduleDetection": "force", "noEmit": true, /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true }, "include": ["vite.config.ts"] } ================================================ FILE: examples/vue/vite.config.ts ================================================ import { defineConfig } from 'vite'; import vue from '@vitejs/plugin-vue'; import legacy from '@vitejs/plugin-legacy'; import qiankunHtmlPlugin from './config/qiankunHtml'; export default defineConfig(({ mode }) => { const isQiankun = mode === 'qiankun'; return { base: isQiankun ? './' : '/', plugins: [ vue(), isQiankun && legacy({ renderLegacyChunks: true }), isQiankun && qiankunHtmlPlugin(), ].filter(Boolean), server: { port: 7101, cors: true, headers: { 'Access-Control-Allow-Origin': '*', }, }, }; }); ================================================ FILE: package.json ================================================ { "name": "qiankun-monorepo", "private": true, "engines": { "node": ">=16" }, "scripts": { "start:example": "pnpm run build && npm run start:main", "start:main": "cd ./examples/main && npm run dev", "build": "pnpm -r run build", "prerelease:alpha": "changeset pre enter alpha && changeset && changeset version", "release:alpha": "pnpm run build && changeset publish && changeset pre exit", "eslint": "eslint packages/", "prettier": "prettier --write .", "prettier:check": "prettier -c .", "docs:dev": "vitepress dev docs", "docs:build": "vitepress build docs", "docs:preview": "vitepress preview docs", "ci": "pnpm run build && pnpm run eslint && pnpm run prettier:check", "ci:publish": "changeset publish", "test": "pnpm -r run test", "prepare": "husky install", "clean": "rimraf node_modules **/*/node_modules" }, "devDependencies": { "@changesets/cli": "^2.29.8", "@commitlint/cli": "^20.4.1", "@commitlint/config-conventional": "^20.4.1", "@edge-runtime/vm": "^3.2.0", "@types/lodash": "^4.17.23", "@types/node": "^18.19.130", "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", "babel-plugin-import": "^1.13.8", "cross-env": "^7.0.3", "eslint": "^8.57.1", "eslint-config-prettier": "^9.1.2", "eslint-formatter-pretty": "^5.0.0", "father": "^4.6.13", "happy-dom": "^12.10.3", "husky": "^8.0.3", "lint-staged": "^9.5.0", "mermaid": "^11.12.2", "prettier": "^3.8.1", "rimraf": "^3.0.2", "typescript": "^5.9.3", "vitepress": "^1.6.4", "vitest": "^0.34.6" }, "repository": { "type": "git", "url": "git+https://github.com/umijs/qiankun.git" }, "license": "MIT", "bugs": { "url": "https://github.com/umijs/qiankun/issues" }, "homepage": "https://github.com/umijs/qiankun#readme", "lint-staged": { "**/*.{js,ts,json,css,md}": ["prettier --write --ignore-unknown"], "packages/*/{src,types}/**/*.ts": ["eslint --cache --ext .js,.jsx,.ts,.tsx --format=pretty --fix"], "packages/**/*.d.ts": ["eslint --ext .ts"] }, "packageManager": "pnpm@10.28.2", "publishConfig": { "registry": "https://registry.npmjs.org/" } } ================================================ FILE: packages/bundler-plugin/.fatherrc.js ================================================ export { default } from '../../.fatherrc.cjs'; ================================================ FILE: packages/bundler-plugin/CHANGELOG.md ================================================ # @qiankunjs/webpack-plugin ## 0.0.1-rc.1 ### Patch Changes - fa128ee: fix: improve QiankunPlugin webpack compatibility and error handling ## 0.0.1-rc.4 ### Patch Changes - 35f7863: fix: move cheerio to dependencies ## 0.0.1-rc.3 ### Patch Changes - b7ec9e79: fix: correct entry script identification and webpack version detection in Vue CLI 5 ## 0.0.1-rc.2 ### Patch Changes - cb1bd997: fix(webpack-plugin):fix webpack module not found during webpack-plugin build ================================================ FILE: packages/bundler-plugin/README-zh.md ================================================ # @qiankunjs/bundler-plugin `@qiankunjs/bundler-plugin` 是为 [qiankun](https://github.com/umijs/qiankun) 微前端框架设计的打包工具插件集。当前支持 Webpack (4 & 5),未来计划支持 Vite、Turbopack 等其他打包工具。 ## 安装 使用 npm: ```bash npm install @qiankunjs/bundler-plugin --save-dev ``` 或使用 yarn: ```bash yarn add @qiankunjs/bundler-plugin --dev ``` ## Webpack 插件 ### 功能 - 自动设置输出库的名称和格式 - 确保 `jsonpFunction`/`chunkLoadingGlobal` 名称的唯一性 - 设置全局对象为 `window`,确保库可以在浏览器中运行 - 自动为 HTML 中的入口 script 标签加上 entry 标记 - 同时支持 Webpack 4 和 Webpack 5 ### 使用 在您的 `webpack.config.js` 或其他配置文件中: ```javascript const { QiankunWebpackPlugin } = require('@qiankunjs/bundler-plugin'); module.exports = { plugins: [ new QiankunWebpackPlugin({ packageName: 'optionalPackageName', }), ], }; ``` ### 选项 - `packageName`: 指定输出库的名称。如果未提供,将使用 `package.json` 中的名称。 ### 入口脚本检测 插件会自动检测并标记 HTML 中的入口脚本: 1. 使用 webpack 的 `compilation.entrypoints` API 识别入口 chunk 2. 单入口构建:标记主入口 chunk 的脚本 3. 多入口构建:根据 HTML 文件名匹配对应的 entrypoint 4. 如果自动检测失败,回退到最后一个 script 标签 ## 贡献 欢迎任何形式的贡献!请提交 PR 或开启 issue 讨论。 ## 许可证 MIT ================================================ FILE: packages/bundler-plugin/README.md ================================================ # @qiankunjs/bundler-plugin Bundler plugins for the [qiankun](https://github.com/umijs/qiankun) micro-frontend framework. Currently supports Webpack (4 & 5), with plans to support other bundlers like Vite and Turbopack in the future. ## Installation Using npm: ```bash npm install @qiankunjs/bundler-plugin --save-dev ``` Or using yarn: ```bash yarn add @qiankunjs/bundler-plugin --dev ``` ## Webpack Plugin ### Features - Automatically sets the name and format of the output library - Ensures the uniqueness of the `jsonpFunction`/`chunkLoadingGlobal` name - Sets the global object to `window`, ensuring the library can run in the browser - Automatically adds an entry marker to the entry script tag in HTML - Supports both Webpack 4 and Webpack 5 ### Usage In your `webpack.config.js` or other configuration files: ```javascript const { QiankunWebpackPlugin } = require('@qiankunjs/bundler-plugin'); module.exports = { plugins: [ new QiankunWebpackPlugin({ packageName: 'optionalPackageName', }), ], }; ``` ### Options - `packageName`: Specifies the name of the output library. If not provided, the name from `package.json` will be used. ### Entry Script Detection The plugin automatically detects and marks the entry script in your HTML: 1. Uses webpack's `compilation.entrypoints` API to identify entry chunks 2. For single entry builds: marks the main entry chunk's script 3. For multi-entry builds: matches HTML filename to entrypoint name 4. Falls back to the last script tag if automatic detection fails ## Contributing Any form of contribution is welcome! Please submit PRs or open issues for discussion. ## License MIT ================================================ FILE: packages/bundler-plugin/package.json ================================================ { "name": "@qiankunjs/bundler-plugin", "version": "0.0.1-rc.1", "description": "Bundler plugins for qiankun micro-frontend framework", "repository": { "type": "git", "url": "git+https://github.com/umijs/qiankun.git" }, "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", "types": "./dist/esm/index.d.ts", "exports": { ".": { "import": "./dist/esm/index.js", "require": "./dist/cjs/index.js", "types": "./dist/esm/index.d.ts" }, "./webpack": { "import": "./dist/esm/webpack/index.js", "require": "./dist/cjs/webpack/index.js", "types": "./dist/esm/webpack/index.d.ts" } }, "scripts": { "build:webpack4": "cd ./tests/webpack4 && npm install && pnpm run build", "build:webpack5": "cd ./tests/webpack5 && npm install && pnpm run build", "test": "pnpm run build && pnpm run build:webpack4 && pnpm run build:webpack5 && vitest --run", "build": "father build" }, "files": [ "dist" ], "peerDependencies": { "webpack": "^4.0.0 || ^5.0.0" }, "peerDependenciesMeta": { "webpack": { "optional": true } }, "author": "Hermanna", "license": "MIT", "dependencies": { "webpack-sources": "^3.2.3", "cheerio": "^1.0.0-rc.12" }, "devDependencies": { "@types/webpack-sources": "^3.2.1", "webpack": "^5.0.0" } } ================================================ FILE: packages/bundler-plugin/src/index.ts ================================================ export { QiankunWebpackPlugin, type QiankunWebpackPluginOptions } from './webpack'; export { QiankunWebpackPlugin as default } from './webpack'; ================================================ FILE: packages/bundler-plugin/src/webpack/index.ts ================================================ import * as fs from 'fs'; import * as path from 'path'; import type { Compiler, Compilation } from 'webpack'; export interface QiankunWebpackPluginOptions { packageName?: string; } interface Chunk { files: Iterable | undefined; hasRuntime?: () => boolean; } interface Entrypoint { chunks: Chunk[] | undefined; getEntrypointChunk?: () => Chunk; getFiles?: () => string[]; } interface PackageJson { name?: string; } interface HtmlTagObject { tagName: string; voidTag: boolean; attributes: Record; innerHTML?: string; meta?: Record; } interface HtmlWebpackPluginData { assetTags: { scripts: HtmlTagObject[]; styles: HtmlTagObject[]; meta: HtmlTagObject[]; }; outputName: string; publicPath: string; plugin: unknown; } interface HtmlWebpackPluginHooks { alterAssetTags: { tapAsync: ( name: string, callback: (data: HtmlWebpackPluginData, cb: (err: Error | null, data: HtmlWebpackPluginData) => void) => void, ) => void; }; } interface HtmlWebpackPluginStatic { getHooks(compilation: Compilation): HtmlWebpackPluginHooks; } export class QiankunWebpackPlugin { private readonly packageName: string; private static cachedPackageJson: PackageJson | null = null; constructor(options: QiankunWebpackPluginOptions = {}) { this.packageName = options.packageName || QiankunWebpackPlugin.getPackageName(); } private static getPackageName(): string { if (!QiankunWebpackPlugin.cachedPackageJson) { const packageJsonPath = path.join(process.cwd(), 'package.json'); try { const content = fs.readFileSync(packageJsonPath, 'utf-8'); QiankunWebpackPlugin.cachedPackageJson = JSON.parse(content) as PackageJson; } catch { QiankunWebpackPlugin.cachedPackageJson = {}; } } return QiankunWebpackPlugin.cachedPackageJson.name || ''; } apply(compiler: Compiler): void { this.configureOutput(compiler); this.registerHtmlProcessing(compiler); } private configureOutput(compiler: Compiler): void { const output = compiler.options.output; // webpack 4 doesn't have compiler.webpack, use it for version detection const webpack = (compiler as unknown as { webpack?: { version?: string } }).webpack; const isWebpack5 = webpack?.version?.startsWith('5'); if (isWebpack5) { output.library = { name: this.packageName, type: 'window', }; } else { // @ts-expect-error webpack 4 specific options output.library = this.packageName; // @ts-expect-error webpack 4 specific options output.libraryTarget = 'window'; // @ts-expect-error webpack 4 specific options output.jsonpFunction = `webpackJsonp_${this.packageName}`; output.globalObject = 'window'; } } private registerHtmlProcessing(compiler: Compiler): void { compiler.hooks.compilation.tap('QiankunWebpackPlugin', (compilation: Compilation) => { const HtmlWebpackPlugin = this.findHtmlWebpackPlugin(compiler); if (HtmlWebpackPlugin) { this.hookIntoHtmlWebpackPlugin(compilation, HtmlWebpackPlugin); } }); } private findHtmlWebpackPlugin(compiler: Compiler): HtmlWebpackPluginStatic | null { const plugins = (compiler.options as unknown as { plugins?: unknown[] }).plugins ?? []; for (const plugin of plugins) { const ctor = (plugin as { constructor?: HtmlWebpackPluginStatic } | null)?.constructor; if (ctor && typeof ctor.getHooks === 'function') { return ctor; } } return null; } private hookIntoHtmlWebpackPlugin(compilation: Compilation, HtmlWebpackPlugin: HtmlWebpackPluginStatic): void { const hooks = HtmlWebpackPlugin.getHooks(compilation); hooks.alterAssetTags.tapAsync('QiankunWebpackPlugin', (data, callback) => { const scripts = data.assetTags.scripts; if (scripts.length === 0) { callback(null, data); return; } const hasEntry = scripts.some((script) => 'entry' in script.attributes); if (hasEntry) { callback(null, data); return; } const entryScriptSrc = this.findEntryScriptSrc(compilation, data.outputName); let marked = false; if (entryScriptSrc) { const entryScript = scripts.find((script) => { const src = script.attributes.src; return typeof src === 'string' && this.matchEntryScript(src, entryScriptSrc); }); if (entryScript) { entryScript.attributes.entry = true; marked = true; } } if (!marked) { scripts[scripts.length - 1].attributes.entry = true; } callback(null, data); }); } private findEntryScriptSrc(compilation: Compilation, htmlOutputName: string): string | null { const entrypoints = compilation.entrypoints as Map | undefined; if (!entrypoints || entrypoints.size === 0) { return null; } // For single entry, use the only entrypoint if (entrypoints.size === 1) { const entrypoint = entrypoints.values().next().value as Entrypoint; return this.getEntryChunkFile(entrypoint); } // For multiple entries, try to match by HTML filename const htmlBaseName = path.basename(htmlOutputName, '.html'); const matchedEntrypoint = entrypoints.get(htmlBaseName); if (matchedEntrypoint) { return this.getEntryChunkFile(matchedEntrypoint); } // Fallback: use the first entrypoint const firstEntrypoint = entrypoints.values().next().value as Entrypoint; return this.getEntryChunkFile(firstEntrypoint); } private getEntryChunkFile(entrypoint: Entrypoint): string | null { // Webpack 5: getEntrypointChunk returns the main chunk if (typeof entrypoint.getEntrypointChunk === 'function') { const chunk = entrypoint.getEntrypointChunk(); const files = chunk.files; if (files) { for (const file of files) { if (file.endsWith('.js')) { return file; } } } } // Webpack 4/5 fallback: iterate chunks to find the one with runtime or entry modules const chunks = entrypoint.chunks; if (chunks) { for (let i = chunks.length - 1; i >= 0; i--) { const chunk = chunks[i]; // In code-split scenarios, the entry chunk usually has runtime if (chunk.hasRuntime?.() || i === chunks.length - 1) { const files = Array.from(chunk.files ?? []); for (const file of files) { if (file.endsWith('.js')) { return file; } } } } } // Last resort: get files from entrypoint const allFiles = entrypoint.getFiles?.() || []; for (const file of allFiles) { if (file.endsWith('.js')) { return file; } } return null; } private matchEntryScript(scriptSrc: string, entryFile: string): boolean { // Handle both absolute and relative paths const normalizedSrc = scriptSrc.replace(/^\//, ''); const normalizedEntry = entryFile.replace(/^\//, ''); return ( normalizedSrc === normalizedEntry || normalizedSrc.endsWith('/' + normalizedEntry) || normalizedSrc.endsWith(normalizedEntry) ); } } export default QiankunWebpackPlugin; ================================================ FILE: packages/bundler-plugin/tests/fixtures/webpack4.html ================================================ Webpack Test
================================================ FILE: packages/bundler-plugin/tests/fixtures/webpack5.html ================================================ Webpack Test
================================================ FILE: packages/bundler-plugin/tests/plugin.test.ts ================================================ import * as fs from 'fs'; import * as path from 'path'; import { describe, expect, it } from 'vitest'; import { load as cheerioLoad } from 'cheerio'; function normalizeHtml(html: string): string { const $ = cheerioLoad(html); $('body') .contents() // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison -- cheerio's AnyNode has ElementType enum .filter((_, el) => el.type === 'text' && !(el as unknown as { data: string }).data.trim()) .remove(); return $.html(); } function readFixture(name: string): string { return fs.readFileSync(path.join(__dirname, 'fixtures', `${name}.html`), 'utf-8'); } describe('QiankunWebpackPlugin', () => { describe('integration tests', () => { it('should generate expected HTML for webpack 4', () => { const actual = normalizeHtml(fs.readFileSync(path.join(__dirname, 'webpack4/dist/index.html'), 'utf-8')); const expected = normalizeHtml(readFixture('webpack4')); expect(actual).toBe(expected); }); it('should generate expected HTML for webpack 5', () => { const actual = normalizeHtml(fs.readFileSync(path.join(__dirname, 'webpack5/dist/index.html'), 'utf-8')); const expected = normalizeHtml(readFixture('webpack5')); expect(actual).toBe(expected); }); }); }); ================================================ FILE: packages/bundler-plugin/tests/webpack4/.eslintrc.js ================================================ module.exports = { env: { node: true, commonjs: true, }, }; ================================================ FILE: packages/bundler-plugin/tests/webpack4/index.html ================================================ Webpack Test
================================================ FILE: packages/bundler-plugin/tests/webpack4/index.js ================================================ console.log('Hello from test demo!'); ================================================ FILE: packages/bundler-plugin/tests/webpack4/package.json ================================================ { "name": "webpack4", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "webpack" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "html-webpack-plugin": "^4.5.2", "webpack": "4", "webpack-cli": "4" } } ================================================ FILE: packages/bundler-plugin/tests/webpack4/webpack.config.js ================================================ const path = require('path'); const { QiankunWebpackPlugin } = require('../../dist/cjs'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { entry: './index.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist'), }, mode: 'development', plugins: [ new HtmlWebpackPlugin({ template: './index.html', filename: 'index.html', }), new QiankunWebpackPlugin(), ], }; ================================================ FILE: packages/bundler-plugin/tests/webpack5/.eslintrc.js ================================================ module.exports = { env: { node: true, commonjs: true, }, }; ================================================ FILE: packages/bundler-plugin/tests/webpack5/index.html ================================================ Webpack Test
================================================ FILE: packages/bundler-plugin/tests/webpack5/index.js ================================================ console.log('Hello from test demo!'); ================================================ FILE: packages/bundler-plugin/tests/webpack5/package.json ================================================ { "name": "webpack5", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "webpack" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "html-webpack-plugin": "^5.5.3", "webpack": "5", "webpack-cli": "5" } } ================================================ FILE: packages/bundler-plugin/tests/webpack5/webpack.config.js ================================================ const path = require('path'); const { QiankunWebpackPlugin } = require('../../dist/cjs'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { entry: './index.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist'), }, mode: 'development', plugins: [ new HtmlWebpackPlugin({ template: './index.html', filename: 'index.html', scriptLoading: 'blocking', }), new QiankunWebpackPlugin(), ], }; ================================================ FILE: packages/create-qiankun/.fatherrc.js ================================================ export default { platform: 'node', cjs: { input: 'src', output: 'dist', }, }; ================================================ FILE: packages/create-qiankun/CHANGELOG.md ================================================ # create-qiankun ## 0.0.1-rc.2 ### Patch Changes - fa128ee: fix: improve QiankunPlugin webpack compatibility and error handling - 466c9c2: feat: refactor create-qiankun cli ## 0.0.1-rc.1 ### Patch Changes - fb91153: fix: include template to publish field ## 0.1.0-rc.0 ### Patch Changes - 4c7a773: feat: introduce qiankun scaffold ================================================ FILE: packages/create-qiankun/Readme.md ================================================ # @qiankunjs/create-qiankun `@qiankunjs/create-qiankun` 是一个为 [qiankun](https://github.com/umijs/qiankun) 微前端框架设计的脚手架功能。旨在快速启动示例项目,方便开发者快速上手。 ## 功能 - 支持选择一个或多个子应用来创建一个新的项目 - 支持主,子应用路由模式 `(hash, history)` 选择 - 支持一键生成 `npm/yarn/pnpm/pnpm workspace` 工程 - 注入启动应用脚本以及端口冲突检测 ## 环境要求 1. 建议使用 Node.js 版本 v18 或更高版本。,推荐使用 [fnm](https://github.com/Schniz/fnm) 管理 node 版本 ## 安装 使用 npm: ```bash npx create-qiankun@latest ``` 或使用 yarn: ```bash yarn create qiankun@latest ``` 或使用 pnpm: ```bash pnpm dlx create-qiankun@latest ``` ## 使用 ## 模板列表 ### 主应用模板 | 模板名称 | | | --------------- | --- | | React18+Webpack | | | Vue3+Webpack | | | React18+umi | | ### 子应用模板 | 模板名称 | | | --------------- | --------------------------- | | React18+Webpack | | | React16+Webpack | | | Vue3+Webpack | | | Vue2+Webpack | ❗ 在 pnpm workspace 有问题 | | Vite+Vue3 | 🚧 建设中 | | Vite+React18 | 🚧 建设中 | ## 贡献 欢迎任何形式的贡献!请提交 PR 或开启 issue 讨论。 ## 许可证 MIT ================================================ FILE: packages/create-qiankun/package.json ================================================ { "name": "create-qiankun", "version": "0.0.1-rc.2", "description": "An easy way to start a qiankun sub-app with Vite", "repository": { "type": "git", "url": "git+https://github.com/umijs/qiankun.git" }, "bin": { "create-qiankun": "./dist/index.js" }, "main": "./dist/index.js", "scripts": { "dev": "father dev", "build": "father build", "test": "vitest run", "test:e2e": "vitest run --config vitest.e2e.config.ts" }, "files": [ "dist" ], "keywords": [ "qiankun", "micro-frontend", "vite", "scaffold" ], "author": "", "license": "ISC", "dependencies": { "create-vite": "^5.2.0", "execa": "^5.1.1", "fs-extra": "^10.1.0", "kolorist": "^1.8.0", "minimist": "^1.2.6", "prompts": "^2.4.2" }, "devDependencies": { "@types/fs-extra": "^11.0.2", "@types/minimist": "^1.2.3", "@types/prompts": "^2.4.4", "cheerio": "^1.0.0", "vitest": "^0.34.6" } } ================================================ FILE: packages/create-qiankun/src/index.ts ================================================ #!/usr/bin/env node import prompts from 'prompts'; import { green, red, bold } from 'kolorist'; import path from 'node:path'; import minimist from 'minimist'; import { isDirectory, detectWorkspaceRoot } from './shared/utils'; import { templateOptions, ViteTemplate } from './shared/types'; import type { PromptAnswers } from './shared/types'; import { generateViteApp } from './shared/generators/createVite'; import { patchViteSubApp } from './shared/patchers'; main().catch((e) => { console.error(e); process.exit(1); }); async function main() { console.log(); console.log(green('Welcome to create-qiankun!')); console.log(); const argv = minimist(process.argv.slice(2)); const argAppName = argv._[0]; // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const argTemplate = argv.template || argv.t; let answers: PromptAnswers; try { answers = (await prompts( [ { name: 'appName', type: argAppName ? null : 'text', message: 'Sub-app name:', initial: 'qiankun-sub-app', validate: (value: string) => { if (!value.trim()) return 'App name is required'; if (!/^[a-z0-9-]+$/.test(value)) return 'App name can only contain lowercase letters, numbers, and hyphens'; return true; }, }, { name: 'template', type: argTemplate ? null : 'select', message: 'Select a framework:', choices: templateOptions, }, ], { onCancel: () => { throw new Error('Operation cancelled'); }, }, )) as PromptAnswers; } catch { console.log(red('Operation cancelled')); process.exit(1); } const appName = argAppName || answers.appName; const template = (argTemplate || answers.template) as ViteTemplate; const validTemplates = Object.values(ViteTemplate); if (!validTemplates.includes(template)) { console.log(red(`Invalid template: ${String(template)}. Valid options: ${validTemplates.join(', ')}`)); process.exit(1); } const cwd = process.cwd(); const workspaceRoot = detectWorkspaceRoot(cwd); const isInWorkspace = !!workspaceRoot; let targetDir: string; if (isInWorkspace) { const packagesDir = path.join(workspaceRoot, 'packages'); targetDir = packagesDir; } else { targetDir = cwd; } const appPath = path.join(targetDir, appName); if (isDirectory(appPath)) { console.log(red(`Directory ${appPath} already exists`)); process.exit(1); } console.log(); console.log(green(`Creating ${appName} at ${appPath}...`)); console.log(); await generateViteApp(targetDir, appName, template); console.log(); console.log(green('Patching for qiankun...')); await patchViteSubApp(appPath, appName, template); console.log(); console.log(bold(green('Done!'))); console.log(); console.log('Next steps:'); console.log(` cd ${isInWorkspace ? `packages/${appName}` : appName}`); console.log(' pnpm install'); console.log(' pnpm dev # Run standalone'); console.log(' pnpm build:qiankun # Build for qiankun'); console.log(); } ================================================ FILE: packages/create-qiankun/src/shared/generators/createVite.ts ================================================ import fse from 'fs-extra'; import execa from 'execa'; import type { ViteTemplate } from '../types'; export async function generateViteApp(targetDir: string, appName: string, template: ViteTemplate): Promise { await fse.ensureDir(targetDir); const createViteBin = require.resolve('create-vite'); await execa(process.execPath, [createViteBin, appName, '--template', template], { cwd: targetDir, stdio: 'inherit', }); } ================================================ FILE: packages/create-qiankun/src/shared/patchers/entryFile.ts ================================================ import path from 'node:path'; import fse from 'fs-extra'; import type { ViteTemplate } from '../types'; import { isReactTemplate, isTypeScriptTemplate } from '../types'; export async function writeEntryFile(appRoot: string, appName: string, template: ViteTemplate): Promise { const isReact = isReactTemplate(template); const isTs = isTypeScriptTemplate(template); if (isReact) { await writeReactEntry(appRoot, appName, isTs); } else { await writeVueEntry(appRoot, appName, isTs); } } async function writeReactEntry(appRoot: string, appName: string, isTs: boolean): Promise { const ext = isTs ? 'tsx' : 'jsx'; const entryPath = path.join(appRoot, `src/main.${ext}`); const typeAnnotation = isTs ? ': ReactDOM.Root | undefined' : ''; const propsType = isTs ? ': { container?: Element }' : ''; const defaultPropsType = isTs ? ': { container?: Element } = {}' : ' = {}'; const content = `import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; import './index.css'; const appName = '${appName}'; let root${typeAnnotation}; function render(props${defaultPropsType}) { const container = props.container?.querySelector('#root') ?? document.getElementById('root'); if (!container) return; root = ReactDOM.createRoot(container); root.render( , ); } export async function bootstrap() { return Promise.resolve(); } export async function mount(props${propsType}) { render(props); } export async function unmount(props${propsType}) { if (root) { root.unmount(); root = undefined; } const container = props.container?.querySelector('#root') ?? document.getElementById('root'); if (container) { container.innerHTML = ''; } } declare global { interface Window { __POWERED_BY_QIANKUN__?: boolean; [key: string]: unknown; } } if (window.__POWERED_BY_QIANKUN__) { window[appName] = { bootstrap, mount, unmount }; } else { render(); } `; await fse.writeFile(entryPath, content, 'utf-8'); } async function writeVueEntry(appRoot: string, appName: string, isTs: boolean): Promise { const ext = isTs ? 'ts' : 'js'; const entryPath = path.join(appRoot, `src/main.${ext}`); const typeAnnotation = isTs ? ': ReturnType | undefined' : ''; const propsType = isTs ? ': { container?: Element }' : ''; const defaultPropsType = isTs ? ': { container?: Element } = {}' : ' = {}'; const declareGlobal = isTs ? ` declare global { interface Window { __POWERED_BY_QIANKUN__?: boolean; [key: string]: unknown; } } ` : ''; const content = `import { createApp } from 'vue'; import App from './App.vue'; import './style.css'; const appName = '${appName}'; let app${typeAnnotation}; function render(props${defaultPropsType}) { const container = props.container?.querySelector('#app') ?? document.getElementById('app'); if (!container) return; app = createApp(App); app.mount(container); } export async function bootstrap() { return Promise.resolve(); } export async function mount(props${propsType}) { render(props); } export async function unmount(props${propsType}) { if (app) { app.unmount(); app = undefined; } const container = props.container?.querySelector('#app') ?? document.getElementById('app'); if (container) { container.innerHTML = ''; } } ${declareGlobal} if (window.__POWERED_BY_QIANKUN__) { window[appName] = { bootstrap, mount, unmount }; } else { render(); } `; await fse.writeFile(entryPath, content, 'utf-8'); } ================================================ FILE: packages/create-qiankun/src/shared/patchers/index.ts ================================================ import type { ViteTemplate } from '../types'; import { patchPackageJson } from './packageJson'; import { writeQiankunHtmlPlugin } from './qiankunHtmlPlugin'; import { writeViteConfig } from './viteConfig'; import { writeEntryFile } from './entryFile'; export async function patchViteSubApp(appRoot: string, appName: string, template: ViteTemplate): Promise { await patchPackageJson(appRoot, appName, template); await writeQiankunHtmlPlugin(appRoot, template); await writeViteConfig(appRoot, template); await writeEntryFile(appRoot, appName, template); } ================================================ FILE: packages/create-qiankun/src/shared/patchers/packageJson.ts ================================================ import path from 'node:path'; import fse from 'fs-extra'; import type { ViteTemplate } from '../types'; import { isReactTemplate } from '../types'; const LEGACY_PLUGIN_VERSION = '^5.4.2'; const CHEERIO_VERSION = '^1.0.0'; const QIANKUN_VERSION = '^3.0.0-rc.0'; const QIANKUN_REACT_VERSION = '^0.0.1-rc.0'; const QIANKUN_VUE_VERSION = '^0.0.1-rc.0'; export async function patchPackageJson(appRoot: string, appName: string, template: ViteTemplate): Promise { const pkgPath = path.join(appRoot, 'package.json'); const pkg = (await fse.readJson(pkgPath)) as Record; pkg.name = appName; pkg.scripts = { ...(pkg.scripts || {}), 'build:qiankun': 'vite build --mode qiankun', 'preview:qiankun': 'vite preview --mode qiankun', }; pkg.devDependencies = { ...(pkg.devDependencies || {}), '@vitejs/plugin-legacy': LEGACY_PLUGIN_VERSION, cheerio: CHEERIO_VERSION, }; const qiankunBinding = isReactTemplate(template) ? { '@qiankunjs/react': QIANKUN_REACT_VERSION } : { '@qiankunjs/vue': QIANKUN_VUE_VERSION }; pkg.dependencies = { ...(pkg.dependencies || {}), qiankun: QIANKUN_VERSION, ...qiankunBinding, }; await fse.writeJson(pkgPath, pkg, { spaces: 2 }); } ================================================ FILE: packages/create-qiankun/src/shared/patchers/qiankunHtmlPlugin.ts ================================================ import path from 'node:path'; import fse from 'fs-extra'; import type { ViteTemplate } from '../types'; import { isTypeScriptTemplate } from '../types'; export async function writeQiankunHtmlPlugin(appRoot: string, template: ViteTemplate): Promise { const configDir = path.join(appRoot, 'config'); await fse.ensureDir(configDir); const ext = isTypeScriptTemplate(template) ? 'ts' : 'js'; const pluginPath = path.join(configDir, `qiankunHtml.${ext}`); const content = isTypeScriptTemplate(template) ? getTypeScriptPluginContent() : getJavaScriptPluginContent(); await fse.writeFile(pluginPath, content, 'utf-8'); } function getTypeScriptPluginContent(): string { return `import type { IndexHtmlTransformContext, PluginOption } from 'vite'; import { load } from 'cheerio'; export default function qiankunHtmlPlugin(): PluginOption { return { name: 'qiankun-html-transform', enforce: 'post', apply: 'build', transformIndexHtml(html: string, ctx: IndexHtmlTransformContext) { if (!ctx || !isQiankunBuild(ctx)) return html; return transformHtml(html); }, }; } function isQiankunBuild(ctx: IndexHtmlTransformContext): boolean { return ctx.bundle !== undefined && ctx.server === undefined; } function rewriteSystemImport(script: string): string { const parentUrl = 'window.__POWERED_BY_QIANKUN__ ? window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ : document.baseURI'; if (script.includes(parentUrl)) { return script; } const legacyEntryPattern = "document.getElementById('vite-legacy-entry').getAttribute('data-src')"; if (script.includes(legacyEntryPattern)) { return script.replace( \`System.import(\${legacyEntryPattern})\`, \`System.import(\${legacyEntryPattern}, \${parentUrl})\` ); } return script; } function transformHtml(html: string): string { const $ = load(html); $('script[type="module"]').remove(); $('link[rel="modulepreload"]').remove(); $('script[nomodule]').each((_, el) => { const scriptContent = $(el).html(); if (!scriptContent || !scriptContent.includes('System.import')) return; const updated = rewriteSystemImport(scriptContent); if (updated !== scriptContent) { $(el).text(updated); } }); return $.html(); } `; } function getJavaScriptPluginContent(): string { return `import { load } from 'cheerio'; export default function qiankunHtmlPlugin() { return { name: 'qiankun-html-transform', enforce: 'post', apply: 'build', transformIndexHtml(html, ctx) { if (!ctx || !isQiankunBuild(ctx)) return html; return transformHtml(html); }, }; } function isQiankunBuild(ctx) { return ctx.bundle !== undefined && ctx.server === undefined; } function rewriteSystemImport(script) { const parentUrl = 'window.__POWERED_BY_QIANKUN__ ? window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ : document.baseURI'; if (script.includes(parentUrl)) { return script; } const legacyEntryPattern = "document.getElementById('vite-legacy-entry').getAttribute('data-src')"; if (script.includes(legacyEntryPattern)) { return script.replace( \`System.import(\${legacyEntryPattern})\`, \`System.import(\${legacyEntryPattern}, \${parentUrl})\` ); } return script; } function transformHtml(html) { const $ = load(html); $('script[type="module"]').remove(); $('link[rel="modulepreload"]').remove(); $('script[nomodule]').each((_, el) => { const scriptContent = $(el).html(); if (!scriptContent || !scriptContent.includes('System.import')) return; const updated = rewriteSystemImport(scriptContent); if (updated !== scriptContent) { $(el).text(updated); } }); return $.html(); } `; } ================================================ FILE: packages/create-qiankun/src/shared/patchers/viteConfig.ts ================================================ import path from 'node:path'; import fse from 'fs-extra'; import type { ViteTemplate } from '../types'; import { isReactTemplate, isTypeScriptTemplate } from '../types'; export async function writeViteConfig(appRoot: string, template: ViteTemplate): Promise { const ext = isTypeScriptTemplate(template) ? 'ts' : 'js'; const configPath = path.join(appRoot, `vite.config.${ext}`); const content = isTypeScriptTemplate(template) ? getTypeScriptConfig(template) : getJavaScriptConfig(template); await fse.writeFile(configPath, content, 'utf-8'); } function getTypeScriptConfig(template: ViteTemplate): string { const frameworkImport = isReactTemplate(template) ? "import react from '@vitejs/plugin-react';" : "import vue from '@vitejs/plugin-vue';"; const pluginCall = isReactTemplate(template) ? 'react()' : 'vue()'; return `import { defineConfig } from 'vite'; ${frameworkImport} import legacy from '@vitejs/plugin-legacy'; import qiankunHtmlPlugin from './config/qiankunHtml'; export default defineConfig(({ mode }) => { const isQiankun = mode === 'qiankun'; return { base: isQiankun ? './' : '/', plugins: [ ${pluginCall}, isQiankun && legacy({ renderLegacyChunks: true }), isQiankun && qiankunHtmlPlugin(), ].filter(Boolean), server: { cors: true, headers: { 'Access-Control-Allow-Origin': '*', }, }, }; }); `; } function getJavaScriptConfig(template: ViteTemplate): string { const frameworkImport = isReactTemplate(template) ? "import react from '@vitejs/plugin-react';" : "import vue from '@vitejs/plugin-vue';"; const pluginCall = isReactTemplate(template) ? 'react()' : 'vue()'; return `import { defineConfig } from 'vite'; ${frameworkImport} import legacy from '@vitejs/plugin-legacy'; import qiankunHtmlPlugin from './config/qiankunHtml'; export default defineConfig(({ mode }) => { const isQiankun = mode === 'qiankun'; return { base: isQiankun ? './' : '/', plugins: [ ${pluginCall}, isQiankun && legacy({ renderLegacyChunks: true }), isQiankun && qiankunHtmlPlugin(), ].filter(Boolean), server: { cors: true, headers: { 'Access-Control-Allow-Origin': '*', }, }, }; }); `; } ================================================ FILE: packages/create-qiankun/src/shared/types.ts ================================================ export enum ViteTemplate { ReactTs = 'react-ts', React = 'react', VueTs = 'vue-ts', Vue = 'vue', } export interface TemplateOption { title: string; value: ViteTemplate; } export const templateOptions: TemplateOption[] = [ { title: 'React + TypeScript', value: ViteTemplate.ReactTs }, { title: 'React', value: ViteTemplate.React }, { title: 'Vue + TypeScript', value: ViteTemplate.VueTs }, { title: 'Vue', value: ViteTemplate.Vue }, ]; export interface PromptAnswers { appName: string; template: ViteTemplate; } export function isReactTemplate(template: ViteTemplate): boolean { return template === ViteTemplate.React || template === ViteTemplate.ReactTs; } export function isTypeScriptTemplate(template: ViteTemplate): boolean { return template === ViteTemplate.ReactTs || template === ViteTemplate.VueTs; } ================================================ FILE: packages/create-qiankun/src/shared/utils/index.ts ================================================ import fse from 'fs-extra'; import path from 'node:path'; export function isDirectory(targetPath: string): boolean { try { return fse.lstatSync(targetPath).isDirectory(); } catch { return false; } } export function isFile(targetPath: string): boolean { try { return fse.lstatSync(targetPath).isFile(); } catch { return false; } } export function detectWorkspaceRoot(targetDir: string): string | null { const parentDir = path.dirname(targetDir); if (isFile(path.join(parentDir, 'pnpm-workspace.yaml'))) { return parentDir; } return null; } ================================================ FILE: packages/create-qiankun/tests/e2e.cli.test.ts ================================================ /* eslint-disable @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access */ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import execa from 'execa'; import fse from 'fs-extra'; import path from 'node:path'; import os from 'node:os'; const CLI_PATH = path.resolve(__dirname, '../dist/index.js'); const FIXTURES_PATH = path.resolve(__dirname, 'fixtures'); const E2E_TIMEOUT = process.env.E2E_TIMEOUT ? parseInt(process.env.E2E_TIMEOUT, 10) : 180000; const APP_NAME_PLACEHOLDER = '{{APP_NAME}}'; async function runCli(cwd: string, appName: string, template: string): Promise { await execa('node', [CLI_PATH, appName, '--template', template], { cwd, stdio: 'inherit', }); } async function installAndBuild(appPath: string): Promise { await execa('pnpm', ['install'], { cwd: appPath, stdio: 'inherit' }); await execa('pnpm', ['build:qiankun'], { cwd: appPath, stdio: 'inherit' }); } function normalizeContent(content: string, appName: string): string { return content.replace(new RegExp(appName, 'g'), APP_NAME_PLACEHOLDER); } async function loadFixture(template: string, fileName: string): Promise { const fixturePath = path.join(FIXTURES_PATH, template, fileName); return fse.readFile(fixturePath, 'utf-8'); } async function assertFileMatchesFixture( actualPath: string, template: string, fixtureFileName: string, appName: string, ): Promise { const actualContent = await fse.readFile(actualPath, 'utf-8'); const expectedContent = await loadFixture(template, fixtureFileName); const normalizedActual = normalizeContent(actualContent, appName); expect(normalizedActual).toBe(expectedContent); } function assertQiankunHtml(html: string): void { expect(html).not.toMatch(/]*type=["']?module["']?[^>]*>/); expect(html).not.toMatch(/]*rel=["']?modulepreload["']?[^>]*>/); expect(html).toMatch(/id=["']?vite-legacy-entry["']?/); expect(html).toContain('window.__POWERED_BY_QIANKUN__'); expect(html).toContain('__INJECTED_PUBLIC_PATH_BY_QIANKUN__'); expect(html).toMatch(/nomodule/); expect(html).toMatch(/]*rel=["']?stylesheet["']?[^>]*>/); } describe('create-qiankun CLI e2e', () => { const testDir = path.join(os.tmpdir(), `create-qiankun-e2e-${Date.now()}`); beforeAll(async () => { await fse.ensureDir(testDir); }); afterAll(async () => { await fse.remove(testDir); }); describe('React + TypeScript template', () => { const appName = 'react-ts-sub'; const appPath = path.join(testDir, appName); const template = 'react-ts'; it( 'should scaffold, patch, and build successfully', async () => { await runCli(testDir, appName, template); expect(await fse.pathExists(appPath)).toBe(true); expect(await fse.pathExists(path.join(appPath, 'package.json'))).toBe(true); expect(await fse.pathExists(path.join(appPath, 'vite.config.ts'))).toBe(true); expect(await fse.pathExists(path.join(appPath, 'config/qiankunHtml.ts'))).toBe(true); expect(await fse.pathExists(path.join(appPath, 'src/main.tsx'))).toBe(true); await installAndBuild(appPath); expect(await fse.pathExists(path.join(appPath, 'dist/index.html'))).toBe(true); }, E2E_TIMEOUT, ); it('should produce valid qiankun HTML output', async () => { const html = await fse.readFile(path.join(appPath, 'dist/index.html'), 'utf-8'); assertQiankunHtml(html); }); it('should generate correct entry file', async () => { await assertFileMatchesFixture(path.join(appPath, 'src/main.tsx'), template, 'main.tsx.txt', appName); }); it('should generate correct vite.config.ts', async () => { await assertFileMatchesFixture(path.join(appPath, 'vite.config.ts'), template, 'vite.config.ts.txt', appName); }); it('should generate correct qiankunHtml plugin', async () => { await assertFileMatchesFixture( path.join(appPath, 'config/qiankunHtml.ts'), template, 'qiankunHtml.ts.txt', appName, ); }); it('should have correct package.json scripts and dependencies', async () => { const pkg = await fse.readJson(path.join(appPath, 'package.json')); expect(pkg.name).toBe(appName); expect(pkg.scripts['build:qiankun']).toBe('vite build --mode qiankun'); expect(pkg.devDependencies['@vitejs/plugin-legacy']).toBeDefined(); expect(pkg.devDependencies['cheerio']).toBeDefined(); expect(pkg.dependencies['qiankun']).toBeDefined(); expect(pkg.dependencies['@qiankunjs/react']).toBeDefined(); }); }); describe('Vue + TypeScript template', () => { const appName = 'vue-ts-sub'; const appPath = path.join(testDir, appName); const template = 'vue-ts'; it( 'should scaffold, patch, and build successfully', async () => { await runCli(testDir, appName, template); expect(await fse.pathExists(appPath)).toBe(true); expect(await fse.pathExists(path.join(appPath, 'package.json'))).toBe(true); expect(await fse.pathExists(path.join(appPath, 'vite.config.ts'))).toBe(true); expect(await fse.pathExists(path.join(appPath, 'config/qiankunHtml.ts'))).toBe(true); expect(await fse.pathExists(path.join(appPath, 'src/main.ts'))).toBe(true); await installAndBuild(appPath); expect(await fse.pathExists(path.join(appPath, 'dist/index.html'))).toBe(true); }, E2E_TIMEOUT, ); it('should produce valid qiankun HTML output', async () => { const html = await fse.readFile(path.join(appPath, 'dist/index.html'), 'utf-8'); assertQiankunHtml(html); }); it('should generate correct entry file', async () => { await assertFileMatchesFixture(path.join(appPath, 'src/main.ts'), template, 'main.ts.txt', appName); }); it('should generate correct vite.config.ts', async () => { await assertFileMatchesFixture(path.join(appPath, 'vite.config.ts'), template, 'vite.config.ts.txt', appName); }); it('should generate correct qiankunHtml plugin', async () => { await assertFileMatchesFixture( path.join(appPath, 'config/qiankunHtml.ts'), template, 'qiankunHtml.ts.txt', appName, ); }); it('should have correct package.json with Vue dependencies', async () => { const pkg = await fse.readJson(path.join(appPath, 'package.json')); expect(pkg.name).toBe(appName); expect(pkg.dependencies['@qiankunjs/vue']).toBeDefined(); expect(pkg.dependencies['vue']).toBeDefined(); }); }); }); ================================================ FILE: packages/create-qiankun/tests/fixtures/react-ts/main.tsx.txt ================================================ import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; import './index.css'; const appName = '{{APP_NAME}}'; let root: ReactDOM.Root | undefined; function render(props: { container?: Element } = {}) { const container = props.container?.querySelector('#root') ?? document.getElementById('root'); if (!container) return; root = ReactDOM.createRoot(container); root.render( , ); } export async function bootstrap() { return Promise.resolve(); } export async function mount(props: { container?: Element }) { render(props); } export async function unmount(props: { container?: Element }) { if (root) { root.unmount(); root = undefined; } const container = props.container?.querySelector('#root') ?? document.getElementById('root'); if (container) { container.innerHTML = ''; } } declare global { interface Window { __POWERED_BY_QIANKUN__?: boolean; [key: string]: unknown; } } if (window.__POWERED_BY_QIANKUN__) { window[appName] = { bootstrap, mount, unmount }; } else { render(); } ================================================ FILE: packages/create-qiankun/tests/fixtures/react-ts/qiankunHtml.ts.txt ================================================ import type { IndexHtmlTransformContext, PluginOption } from 'vite'; import { load } from 'cheerio'; export default function qiankunHtmlPlugin(): PluginOption { return { name: 'qiankun-html-transform', enforce: 'post', apply: 'build', transformIndexHtml(html: string, ctx: IndexHtmlTransformContext) { if (!ctx || !isQiankunBuild(ctx)) return html; return transformHtml(html); }, }; } function isQiankunBuild(ctx: IndexHtmlTransformContext): boolean { return ctx.bundle !== undefined && ctx.server === undefined; } function rewriteSystemImport(script: string): string { const parentUrl = 'window.__POWERED_BY_QIANKUN__ ? window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ : document.baseURI'; if (script.includes(parentUrl)) { return script; } const legacyEntryPattern = "document.getElementById('vite-legacy-entry').getAttribute('data-src')"; if (script.includes(legacyEntryPattern)) { return script.replace(`System.import(${legacyEntryPattern})`, `System.import(${legacyEntryPattern}, ${parentUrl})`); } return script; } function transformHtml(html: string): string { const $ = load(html); $('script[type="module"]').remove(); $('link[rel="modulepreload"]').remove(); $('script[nomodule]').each((_, el) => { const scriptContent = $(el).html(); if (!scriptContent || !scriptContent.includes('System.import')) return; const updated = rewriteSystemImport(scriptContent); if (updated !== scriptContent) { $(el).text(updated); } }); return $.html(); } ================================================ FILE: packages/create-qiankun/tests/fixtures/react-ts/vite.config.ts.txt ================================================ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import legacy from '@vitejs/plugin-legacy'; import qiankunHtmlPlugin from './config/qiankunHtml'; export default defineConfig(({ mode }) => { const isQiankun = mode === 'qiankun'; return { base: isQiankun ? './' : '/', plugins: [react(), isQiankun && legacy({ renderLegacyChunks: true }), isQiankun && qiankunHtmlPlugin()].filter( Boolean, ), server: { cors: true, headers: { 'Access-Control-Allow-Origin': '*', }, }, }; }); ================================================ FILE: packages/create-qiankun/tests/fixtures/vue-ts/main.ts.txt ================================================ import { createApp } from 'vue'; import App from './App.vue'; import './style.css'; const appName = '{{APP_NAME}}'; let app: ReturnType | undefined; function render(props: { container?: Element } = {}) { const container = props.container?.querySelector('#app') ?? document.getElementById('app'); if (!container) return; app = createApp(App); app.mount(container); } export async function bootstrap() { return Promise.resolve(); } export async function mount(props: { container?: Element }) { render(props); } export async function unmount(props: { container?: Element }) { if (app) { app.unmount(); app = undefined; } const container = props.container?.querySelector('#app') ?? document.getElementById('app'); if (container) { container.innerHTML = ''; } } declare global { interface Window { __POWERED_BY_QIANKUN__?: boolean; [key: string]: unknown; } } if (window.__POWERED_BY_QIANKUN__) { window[appName] = { bootstrap, mount, unmount }; } else { render(); } ================================================ FILE: packages/create-qiankun/tests/fixtures/vue-ts/qiankunHtml.ts.txt ================================================ import type { IndexHtmlTransformContext, PluginOption } from 'vite'; import { load } from 'cheerio'; export default function qiankunHtmlPlugin(): PluginOption { return { name: 'qiankun-html-transform', enforce: 'post', apply: 'build', transformIndexHtml(html: string, ctx: IndexHtmlTransformContext) { if (!ctx || !isQiankunBuild(ctx)) return html; return transformHtml(html); }, }; } function isQiankunBuild(ctx: IndexHtmlTransformContext): boolean { return ctx.bundle !== undefined && ctx.server === undefined; } function rewriteSystemImport(script: string): string { const parentUrl = 'window.__POWERED_BY_QIANKUN__ ? window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ : document.baseURI'; if (script.includes(parentUrl)) { return script; } const legacyEntryPattern = "document.getElementById('vite-legacy-entry').getAttribute('data-src')"; if (script.includes(legacyEntryPattern)) { return script.replace(`System.import(${legacyEntryPattern})`, `System.import(${legacyEntryPattern}, ${parentUrl})`); } return script; } function transformHtml(html: string): string { const $ = load(html); $('script[type="module"]').remove(); $('link[rel="modulepreload"]').remove(); $('script[nomodule]').each((_, el) => { const scriptContent = $(el).html(); if (!scriptContent || !scriptContent.includes('System.import')) return; const updated = rewriteSystemImport(scriptContent); if (updated !== scriptContent) { $(el).text(updated); } }); return $.html(); } ================================================ FILE: packages/create-qiankun/tests/fixtures/vue-ts/vite.config.ts.txt ================================================ import { defineConfig } from 'vite'; import vue from '@vitejs/plugin-vue'; import legacy from '@vitejs/plugin-legacy'; import qiankunHtmlPlugin from './config/qiankunHtml'; export default defineConfig(({ mode }) => { const isQiankun = mode === 'qiankun'; return { base: isQiankun ? './' : '/', plugins: [vue(), isQiankun && legacy({ renderLegacyChunks: true }), isQiankun && qiankunHtmlPlugin()].filter( Boolean, ), server: { cors: true, headers: { 'Access-Control-Allow-Origin': '*', }, }, }; }); ================================================ FILE: packages/create-qiankun/tsconfig.json ================================================ { "extends": "../../tsconfig.json" } ================================================ FILE: packages/create-qiankun/vitest.config.ts ================================================ import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { // Exclude E2E tests from regular test runs - they require built dist exclude: ['**/e2e*.test.ts', '**/node_modules/**'], passWithNoTests: true, }, }); ================================================ FILE: packages/create-qiankun/vitest.e2e.config.ts ================================================ import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { include: ['**/e2e*.test.ts'], testTimeout: Number(process.env.E2E_TIMEOUT) || 180_000, }, }); ================================================ FILE: packages/loader/.fatherrc.js ================================================ export { default } from '../../.fatherrc.cjs'; ================================================ FILE: packages/loader/AGENTS.md ================================================ # @qiankunjs/loader Streaming HTML entry loader using writable-dom. ## STRUCTURE ``` loader/ ├── index.ts # loadEntry() - main API ├── TagTransformStream.ts # → replacement ├── writable-dom/ # Forked streaming DOM engine │ └── index.ts # WritableDOMStream implementation └── parser.ts # Static HTML parsing for prefetch ``` ## WHERE TO LOOK | Task | File | Notes | | ------------------- | ----------------------- | -------------------------------------- | | Load micro-app | `index.ts` | `loadEntry(url, container, opts)` | | Head virtualization | `TagTransformStream.ts` | String-level tag replacement | | Streaming DOM | `writable-dom/index.ts` | Incremental parsing + blocking scripts | ## LOADING PIPELINE ``` 1. fetch(entry) ↓ 2. TextDecoderStream (bytes → string) ↓ 3. TagTransformStream ( → ) ↓ 4. WritableDOMStream (stream → live DOM) ├─ nodeTransformer called per node ├─ Blocks on sync scripts/styles └─ Preloads other assets while blocked ↓ 5. Resolve with sandbox.latestSetProp (app exports) ``` ## KEY PATTERNS ### Entry Script Detection ```typescript // Script with `entry` attribute = app's main export point // Loader resolves promise when entry script loads ``` ### Defer Script Ordering - Deferred queue ensures correct execution order - `prepareDeferredQueue` from @qiankunjs/shared ### Detached Parsing - HTML parsed in detached document first - Nodes transformed before moving to live DOM - Prevents premature script execution ## ANTI-PATTERNS - **NEVER** include >1 `entry` script per HTML entry (throws QiankunError) - **FIXME**: Non-standard HTML chunks lacking `` tag ## EXPORTS ```typescript export { loadEntry, type LoaderOpts } from './index'; ``` ================================================ FILE: packages/loader/CHANGELOG.md ================================================ # @qiankunjs/loader ## 0.0.1-rc.21 ### Patch Changes - 57a6129: feat: improve fetch error message by prepending url - Updated dependencies [7d8591e] - Updated dependencies [57a6129] - Updated dependencies [34069e1] - @qiankunjs/shared@0.0.1-rc.13 ## 0.0.1-rc.20 ### Patch Changes - 7d77699: feat(loader): supports passing Response as entry parameter for loadEntry function - Updated dependencies [ea18ce6] [9c56910] [6d252c6] - @qiankunjs/shared@0.0.1-rc.12 - @qiankunjs/sandbox@0.0.1-rc.17 ## 0.0.1-rc.19 ### Patch Changes - 99bf65f: feat: support huge inline-script who might be split into multiple chunks during transfer - Updated dependencies [56fef69] - Updated dependencies [99bf65f] - @qiankunjs/shared@0.0.1-rc.11 - @qiankunjs/sandbox@0.0.1-rc.16 ## 0.0.1-rc.18 ### Patch Changes - Updated dependencies [c3416647] - @qiankunjs/sandbox@0.0.1-rc.15 ## 0.0.1-rc.17 ### Patch Changes - Updated dependencies [8c526255] - @qiankunjs/sandbox@0.0.1-rc.14 ## 0.0.1-rc.16 ### Patch Changes - feb544f0: fix: dynamic append element should support for the same container between micro apps - 2e528c9d: fix: prefer reading script.dataset.src in script load error message - Updated dependencies [a826cf5e] - Updated dependencies [3e43a111] - Updated dependencies [f09c1538] - Updated dependencies [d904f5d8] - Updated dependencies [b2d2c11a] - Updated dependencies [feb544f0] - Updated dependencies [9082546e] - Updated dependencies [62048537] - @qiankunjs/shared@0.0.1-rc.10 - @qiankunjs/sandbox@0.0.1-rc.13 ## 0.0.1-rc.15 ### Patch Changes - bd12dbad: fix: defer scripts should wait until html loaded - Updated dependencies [bd12dbad] - @qiankunjs/sandbox@0.0.1-rc.12 - @qiankunjs/shared@0.0.1-rc.9 ## 0.0.1-rc.14 ### Patch Changes - 98b071bf: feat: support defer scripts and keep the executing order to consist with browser - Updated dependencies [98b071bf] - @qiankunjs/sandbox@0.0.1-rc.11 - @qiankunjs/shared@0.0.1-rc.8 ## 0.0.1-rc.13 ### Patch Changes - f2af2e36: feat: extract NodeTransformer type to shared package - Updated dependencies [f2af2e36] - @qiankunjs/sandbox@0.0.1-rc.10 - @qiankunjs/shared@0.0.1-rc.7 ## 0.0.1-rc.12 ### Patch Changes - 54b0878e: feat(loader): compatible with defer entry script - 7ba95cf2: feat: change script src before it execute thus we can be more consistent with the native browser logic - Updated dependencies [d3e9872d] - Updated dependencies [7cc06bd4] - Updated dependencies [54b0878e] - Updated dependencies [7ba95cf2] - Updated dependencies [312abbc7] - Updated dependencies [6f074136] - @qiankunjs/sandbox@0.0.1-rc.9 - @qiankunjs/shared@0.0.1-rc.6 ## 0.0.1-rc.11 ### Patch Changes - 43bf37a5: fix(sandbox): should get container from getter function in every accessing - Updated dependencies [43bf37a5] - Updated dependencies [a34a92a9] - Updated dependencies [7cf93b54] - Updated dependencies [32106b11] - @qiankunjs/sandbox@0.0.1-rc.8 ## 0.0.1-rc.10 ### Patch Changes - 8e54e129: feat: add isRuntimeCompatible api to check qiankun3 compatibility - Updated dependencies [5f77347b] - Updated dependencies [8e54e129] - @qiankunjs/sandbox@0.0.1-rc.7 - @qiankunjs/shared@0.0.1-rc.5 ## 0.0.1-rc.9 ### Patch Changes - 1b0ffa2f: fix(loader): we should invoke our script load listener before its own ## 0.0.1-rc.8 ### Patch Changes - Updated dependencies [2aca545c] - @qiankunjs/sandbox@0.0.1-rc.6 ## 0.0.1-rc.7 ### Patch Changes - 1d9adcaa: fix: transformer should be generated in every load ## 0.0.1-rc.6 ### Patch Changes - Updated dependencies [3d1d3367] - @qiankunjs/sandbox@0.0.1-rc.5 ## 0.0.1-rc.5 ### Patch Changes - 317961eb: ✨ add transformer options for app loader - 76b6bff7: 🐛 compatible with webpack chunk cache logic - Updated dependencies [488447ad] - Updated dependencies [dc4d9aef] - Updated dependencies [e7d788ef] - Updated dependencies [76b6bff7] - @qiankunjs/sandbox@0.0.1-rc.4 - @qiankunjs/shared@0.0.1-rc.4 ## 0.0.1-rc.4 ### Patch Changes - Updated dependencies [39301f19] - @qiankunjs/sandbox@0.0.1-rc.3 - @qiankunjs/shared@0.0.1-rc.3 ## 0.0.1-rc.3 ### Patch Changes - Updated dependencies [b23d3d7b] - @qiankunjs/shared@0.0.1-rc.2 - @qiankunjs/sandbox@0.0.1-rc.2 ## 0.0.1-rc.2 ### Patch Changes - Updated dependencies [ebb2bcaa] - @qiankunjs/shared@0.0.1-rc.1 - @qiankunjs/sandbox@0.0.1-rc.1 ## 0.0.1-beta.6 ### Patch Changes - ffd77800: ✨support to transform head/body tags to qiankun head/body in stream - Updated dependencies [ffd77800] - @qiankunjs/sandbox@0.0.1-beta.6 - @qiankunjs/shared@0.0.1-beta.6 ## 0.0.1-alpha.5 ### Patch Changes - Updated dependencies [fcb49aad] - Updated dependencies [065d2c54] - Updated dependencies [931dc1f7] - @qiankunjs/shared@0.0.1-alpha.5 - @qiankunjs/sandbox@0.0.1-alpha.5 ## 0.0.1-alpha.4 ### Patch Changes - 62d3b482: 🏷️ fix typings temporary - ⚡️ support preload with dependencies reusing - Updated dependencies [62d3b482] - @qiankunjs/shared@0.0.1-alpha.4 - @qiankunjs/sandbox@0.0.1-alpha.4 ## 0.0.1-alpha.3 ### Patch Changes - daaa9ccc: ✨support code block in sandbox - Updated dependencies [e12d29ae] - Updated dependencies [daaa9ccc] - @qiankunjs/shared@0.0.1-alpha.3 - @qiankunjs/sandbox@0.0.1-alpha.3 ## 0.0.1-alpha.2 ### Patch Changes - 33e65888: fix: changeset - Updated dependencies [33e65888] - @qiankunjs/sandbox@0.0.1-alpha.2 - @qiankunjs/shared@0.0.1-alpha.2 ## 0.0.1-alpha.1 ### Patch Changes - Updated dependencies - @qiankunjs/sandbox@0.0.1-alpha.1 - @qiankunjs/shared@0.0.1-alpha.1 ## 0.0.1-alpha.0 ### Patch Changes - 3.0 alpha - Updated dependencies - @qiankunjs/sandbox@0.0.1-alpha.0 - @qiankunjs/shared@0.0.1-alpha.0 ================================================ FILE: packages/loader/benchmarks/parser/html.js ================================================ export const htmlContent = `金融云控制台
`; ================================================ FILE: packages/loader/benchmarks/parser/huge-html/huge-html.js ================================================ export const hugeHtmlContent = `test1234567890
`; ================================================ FILE: packages/loader/benchmarks/parser/huge-html/import-html-entry.html ================================================ import html entry for huge html ================================================ FILE: packages/loader/benchmarks/parser/huge-html/parser.html ================================================ parser for huge html ================================================ FILE: packages/loader/benchmarks/parser/import-html-entry.html ================================================ import html entry for normal html ================================================ FILE: packages/loader/benchmarks/parser/parser.html ================================================ parser for normal html ================================================ FILE: packages/loader/benchmarks/parser/tern/html.js ================================================ export const htmlContent = `
`; ================================================ FILE: packages/loader/benchmarks/parser/tern/import-html-entry.html ================================================ import html entry for normal html ================================================ FILE: packages/loader/benchmarks/parser/tern/parser.html ================================================ parser for normal html ================================================ FILE: packages/loader/package.json ================================================ { "name": "@qiankunjs/loader", "version": "0.0.1-rc.21", "description": "", "repository": { "type": "git", "url": "git+https://github.com/umijs/qiankun.git" }, "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", "types": "./dist/esm/index.d.ts", "sideEffects": false, "scripts": { "build": "father build", "bench": "npm run build && tachometer ./benchmarks/parser/tern/import-html-entry.html ./benchmarks/parser/tern/parser.html --timeout=1" }, "author": "Kuitos", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.10", "@qiankunjs/sandbox": "workspace:^", "@qiankunjs/shared": "workspace:^", "lodash": "^4.17.11" }, "devDependencies": { "import-html-entry": "^1.12.0", "tachometer": "^0.5.10" }, "publishConfig": { "registry": "https://registry.npmjs.org/" }, "files": [ "dist" ] } ================================================ FILE: packages/loader/src/TagTransformStream.ts ================================================ type TagReplacement = { // start tag tag: string; // start tag replacement alt: string; }; type AutoCompleteTags = { head?: boolean; body?: boolean; }; export function createTagTransformStream( tagReplacements: TagReplacement[], autoCompleteTags: AutoCompleteTags, ): TransformStream { class TagTransformStream extends TransformStream { constructor(trs: TagReplacement[], acts: AutoCompleteTags) { let buffer = ''; super({ async transform(chunk: string, controller: TransformStreamDefaultController) { buffer += chunk; const data = trs.reduce((acc, replacement) => acc.replace(replacement.tag, replacement.alt), buffer); // while buffer is equal to data, it means that the data has not been replaced, and the data will be written to the buffer for checking next time if (buffer === data) { return; } controller.enqueue(data); buffer = ''; }, flush(controller: TransformStreamDefaultController) { if (buffer) { // FIXME It may be a non-standard HTML chunk that does not contain the head tag, in which case you need to manually fill in a head element if (buffer.indexOf(``) === -1 && acts.body) { buffer = `${buffer}`; } // if (buffer.indexOf(``) === -1) { // buffer = `${buffer}`; // } const data = trs.reduce((acc, replacement) => acc.replace(replacement.tag, replacement.alt), buffer); controller.enqueue(data); buffer = ''; } }, }); } } return new TagTransformStream(tagReplacements, autoCompleteTags); } ================================================ FILE: packages/loader/src/index.ts ================================================ import type { Sandbox } from '@qiankunjs/sandbox'; import { qiankunHeadTagName } from '@qiankunjs/sandbox'; import type { AssetsTranspilerOpts, BaseTranspilerOpts, NodeTransformer, ScriptTranspilerOpts, } from '@qiankunjs/shared'; import { Deferred, prepareDeferredQueue, QiankunError } from '@qiankunjs/shared'; import { createTagTransformStream } from './TagTransformStream'; import WritableDOMStream from './writable-dom'; type HTMLEntry = string; // type ConfigEntry = { html: string; scripts: [], styles: [] }; type Entry = HTMLEntry; // type EntryInstance = { // htmlDocument: Document; // prefetch: () => Promise; // execute: (executor?: Promise) => Promise; // }; // export type LoaderOpts = { streamTransformer?: () => TransformStream; nodeTransformer?: NodeTransformer; } & Omit & { sandbox?: Sandbox }; const isExternalScript = (script: HTMLScriptElement): boolean => { return script.tagName === 'SCRIPT' && !!(script.src || script.dataset.src); }; const isEntryScript = (script: HTMLScriptElement): boolean => { return isExternalScript(script) && script.hasAttribute('entry'); }; const isDeferScript = (script: HTMLScriptElement): boolean => { return isExternalScript(script) && script.hasAttribute('defer'); }; /** * @param entry * @param container * @param opts */ export async function loadEntry( entry: Entry | { url: string; res: Response }, container: HTMLElement, opts: LoaderOpts, ): Promise { const { fetch, streamTransformer, sandbox, nodeTransformer } = opts; const entryUrl = typeof entry === 'string' ? entry : entry.url; const res = typeof entry === 'string' ? await fetch(entry) : entry.res; if (res.body) { let foundEntryScript = false; const entryScriptLoadedDeferred = new Deferred(); const onEntryLoaded = () => { // the latest set prop is the entry script exposed global variable if (sandbox?.latestSetProp) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access entryScriptLoadedDeferred.resolve(sandbox.globalThis[sandbox.latestSetProp as number] as T); } else { // TODO support non sandbox mode? entryScriptLoadedDeferred.resolve({} as T); } }; // defer scripts must wait until the entry HTML loaded const deferQueue: Array> = []; const { deferred: entryHTMLLoadedDeferred, queue: queueEntryHTMLDeferred } = prepareDeferredQueue(deferQueue); queueEntryHTMLDeferred(); let readableStream = res.body.pipeThrough(new TextDecoderStream()); if (streamTransformer) { readableStream = readableStream.pipeThrough(streamTransformer()); } void readableStream .pipeThrough( createTagTransformStream( [ { tag: '', alt: `<${qiankunHeadTagName}>` }, { tag: '', alt: `` }, // TODO support body replacement // { tag: 'body', alt: 'qiankun-body' }, ], // { head: true }, {}, ), ) .pipeTo( new WritableDOMStream(container, null, (clone) => { let transformerOpts: AssetsTranspilerOpts = { fetch, sandbox, }; let queueDeferScript: () => void; const deferScriptMode = isDeferScript(clone as unknown as HTMLScriptElement); if (deferScriptMode) { const { deferred, prevDeferred, queue } = prepareDeferredQueue(deferQueue); transformerOpts = { ...transformerOpts, scriptTranspiledDeferred: deferred, prevScriptTranspiledDeferred: prevDeferred, } as ScriptTranspilerOpts; queueDeferScript = queue; } const transformedNode = nodeTransformer ? nodeTransformer(clone, transformerOpts) : clone; const script = transformedNode as unknown as HTMLScriptElement; // the script have no src attribute after transpile, indicating that the script needs to wait for the src to be filled if (deferScriptMode && !script.hasAttribute('src')) { queueDeferScript!(); } /* * If the entry script is executed, we can complete the entry process in advance * otherwise we need to wait until the last script is executed. * Notice that we only support external script as entry script thus we could do resolve the promise after the script is loaded. */ if (isEntryScript(script)) { if (foundEntryScript) { throw new QiankunError( `You should not include more than 1 entry scripts in a single HTML entry ${entryUrl} !`, ); } foundEntryScript = true; const onScriptComplete = ( prevListener: typeof HTMLScriptElement.prototype.onload | typeof HTMLScriptElement.prototype.onerror, event: Event, ) => { script.onload = script.onerror = null; // entryScriptLoadedDeferred not resolved or rejected yet if (!entryScriptLoadedDeferred.isSettled()) { if (event.type === 'load') { onEntryLoaded(); } else { entryScriptLoadedDeferred.reject( new QiankunError( `Entry ${entryUrl} load failed as entry script ${script.dataset.src || script.src} execution failed`, ), ); } } /* In order to avoid the inline script to be executed immediately after the prev onload is executed, resulting in the failure of the sandbox to obtain the latestSetProp here we must resolve the entryScriptLoadedDeferred firstly and then execute the prevListener */ prevListener?.call(script, event); }; script.onload = onScriptComplete.bind(null, script.onload); script.onerror = onScriptComplete.bind(null, script.onerror) as typeof HTMLScriptElement.prototype.onerror; } return transformedNode; }), ) .then(() => { // while the entry html stream is finished but there is no entry script found // we could use the latest set prop in sandbox to resolve the entry promise as fallback if (!foundEntryScript) { onEntryLoaded(); } entryHTMLLoadedDeferred.resolve(); }) .catch((e) => { entryScriptLoadedDeferred.reject(e); entryHTMLLoadedDeferred.reject(e); }); return entryScriptLoadedDeferred.promise; } throw new QiankunError(`The response body of entry ${entryUrl} is empty!`); } ================================================ FILE: packages/loader/src/parser.ts ================================================ type NormalizedEntry = { stylesheets: NodeListOf; scripts: NodeListOf; }; function getDocResources(container: Document) { const links = container.querySelectorAll('link[rel=stylesheet][href]:not([href=""])'); const scripts = container.querySelectorAll('script[src]:not([src=""])'); return { scripts, stylesheets: links, }; } export function parseHTML(htmlContent: string): NormalizedEntry { const domParser = new DOMParser(); const container = domParser.parseFromString(htmlContent, 'text/html'); const resources = getDocResources(container); return resources; } ================================================ FILE: packages/loader/src/utils.ts ================================================ export function isUrlHasOwnProtocol(url: string): boolean { const protocols = ['http://', 'https://', '//', 'blob:', 'data:']; return protocols.some((protocol) => url.startsWith(protocol)); } ================================================ FILE: packages/loader/src/writable-dom/README.md ================================================ # Fork from https://github.com/marko-js/writable-dom ================================================ FILE: packages/loader/src/writable-dom/index.ts ================================================ type Writable = { write: (html: string) => void; abort: (err: Error) => void; close: () => Promise; }; const createHTMLDocument = () => document.implementation.createHTMLDocument(''); let createDocument = (target: ParentNode, nextSibling: ChildNode | null): Document => { const testDoc = createHTMLDocument(); testDoc.write('', ), }); // Mock navigator.onLine Object.defineProperty(window, 'navigator', { value: { onLine: true, connection: undefined, }, writable: true, configurable: true, }); // Mock requestIdleCallback vi.stubGlobal('requestIdleCallback', (cb: IdleRequestCallback) => { cb({ didTimeout: false, timeRemaining: () => 50 }); return 1; }); }); afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); vi.resetModules(); }); it('should prefetch apps with given entries', async () => { const { prefetchApps } = await import('../prefetch'); const apps = [ { name: 'app1', entry: 'http://localhost:7100/' }, { name: 'app2', entry: 'http://localhost:7200/' }, ]; prefetchApps(apps, mockFetch as unknown as typeof fetch); // Wait for async operations await new Promise((resolve) => setTimeout(resolve, 100)); expect(mockFetch).toHaveBeenCalledWith('http://localhost:7100/'); expect(mockFetch).toHaveBeenCalledWith('http://localhost:7200/'); }); it('should prefetch external scripts and stylesheets', async () => { const { prefetchApps } = await import('../prefetch'); const apps = [{ name: 'app1', entry: 'http://localhost:7100/' }]; prefetchApps(apps, mockFetch as unknown as typeof fetch); await new Promise((resolve) => setTimeout(resolve, 100)); // Should fetch the HTML entry expect(mockFetch).toHaveBeenCalledWith('http://localhost:7100/'); // Should also fetch external resources (script and stylesheet) expect(mockFetch).toHaveBeenCalledWith('http://localhost:7100/app.js'); expect(mockFetch).toHaveBeenCalledWith('http://localhost:7100/style.css'); }); }); ================================================ FILE: packages/qiankun/src/apis/effects.ts ================================================ /** * @author Kuitos * @since 2019-02-19 */ import { getMountedApps, navigateToUrl } from 'single-spa'; const firstMountLogLabel = '[qiankun] first app mounted'; if (process.env.NODE_ENV === 'development') { // eslint-disable-next-line no-console console.time(firstMountLogLabel); } /** * Set default mount app, will navigate to the default app if no app is mounted * @param defaultAppLink - The default app route link */ export function setDefaultMountApp(defaultAppLink: string): void { // can not use addEventListener once option for ie support window.addEventListener('single-spa:no-app-change', function listener() { const mountedApps = getMountedApps(); if (!mountedApps.length) { navigateToUrl(defaultAppLink); } window.removeEventListener('single-spa:no-app-change', listener); }); } /** * Run a callback function after the first micro app is mounted * @param effect - The callback function to run */ export function runAfterFirstMounted(effect: () => void): void { // can not use addEventListener once option for ie support window.addEventListener('single-spa:first-mount', function listener() { if (process.env.NODE_ENV === 'development') { // eslint-disable-next-line no-console console.timeEnd(firstMountLogLabel); } effect(); window.removeEventListener('single-spa:first-mount', listener); }); } ================================================ FILE: packages/qiankun/src/apis/errorHandler.ts ================================================ /** * @author Kuitos * @since 2020-02-21 */ // Re-export single-spa error handlers export { addErrorHandler, removeErrorHandler } from 'single-spa'; ================================================ FILE: packages/qiankun/src/apis/isRuntimeCompatible.ts ================================================ export { isRuntimeCompatible } from '@qiankunjs/shared'; ================================================ FILE: packages/qiankun/src/apis/loadMicroApp.ts ================================================ import type { ParcelConfigObject } from 'single-spa'; import { mountRootParcel } from 'single-spa'; import type { ParcelConfigObjectGetter } from '../core/loadApp'; import loadApp from '../core/loadApp'; import type { AppConfiguration, LifeCycles, LoadableApp, MicroApp, ObjectType } from '../types'; import { getContainerXPath, toArray } from '../utils'; import { start, started } from './registerMicroApps'; const appConfigPromiseGetterMap = new Map>(); const containerMicroAppsMap = new Map(); export function loadMicroApp( app: LoadableApp, configuration?: AppConfiguration, lifeCycles?: LifeCycles, ): MicroApp { const { props, name, container } = app; // Must compute the container xpath at beginning to keep it consist around app running // If we compute it every time, the container dom structure most probably been changed and result in a different xpath value const containerXPath = getContainerXPath(container); const getContainerXPathKey = (xpath: string) => `${name}-${xpath}`; let microApp: MicroApp; const wrapParcelConfigForRemount = (config: ParcelConfigObject): ParcelConfigObject => { let microAppConfig = config; if (containerXPath) { const appContainerXPathKey = getContainerXPathKey(containerXPath); const containerMicroApps = containerMicroAppsMap.get(appContainerXPathKey); if (containerMicroApps?.length) { const mount = [ async () => { // While there are multiple micro apps mounted on the same container, we must wait until the prev instances all had unmounted // Otherwise it will lead some concurrent issues const prevLoadMicroApps = containerMicroApps.slice(0, containerMicroApps.indexOf(microApp)); const prevLoadMicroAppsWhichNotBroken = prevLoadMicroApps.filter( (v) => v.getStatus() !== 'LOAD_ERROR' && v.getStatus() !== 'SKIP_BECAUSE_BROKEN', ); await Promise.all(prevLoadMicroAppsWhichNotBroken.map((v) => v.unmountPromise)); }, ...toArray(microAppConfig.mount), ]; microAppConfig = { ...config, mount, }; } } return { ...microAppConfig, // empty bootstrap hook which should not run twice while it calling from cached micro app bootstrap: () => Promise.resolve(), }; }; /** * using name + container xpath as the micro app instance id, * it means if you're rendering a micro app to a dom which have been rendered before, * the micro app would not load and evaluate its lifecycles again */ const memorizedLoadingFn = async (): Promise => { const userConfiguration = configuration; if (containerXPath) { const appContainerXPathKey = getContainerXPathKey(containerXPath); const parcelConfigGetterPromise = appConfigPromiseGetterMap.get(appContainerXPathKey); if (parcelConfigGetterPromise) return wrapParcelConfigForRemount((await parcelConfigGetterPromise)(container)); } const parcelConfigObjectGetterPromise = loadApp(app, userConfiguration, lifeCycles); let parcelConfigObjectGetter: ParcelConfigObjectGetter | undefined; if (containerXPath) { const appContainerXPathKey = getContainerXPathKey(containerXPath); appConfigPromiseGetterMap.set(appContainerXPathKey, parcelConfigObjectGetterPromise); try { parcelConfigObjectGetter = await parcelConfigObjectGetterPromise; } catch (e) { appConfigPromiseGetterMap.delete(appContainerXPathKey); throw e; } } parcelConfigObjectGetter = parcelConfigObjectGetter || (await parcelConfigObjectGetterPromise); return parcelConfigObjectGetter(container); }; if (!started) { // We need to invoke start method of single-spa as the popstate event should be dispatched while the main app calling pushState/replaceState automatically, // but in single-spa it will check the start status before it dispatch popstate // see https://github.com/single-spa/single-spa/blob/f28b5963be1484583a072c8145ac0b5a28d91235/src/navigation/navigation-events.js#L101 // ref https://github.com/umijs/qiankun/pull/1071 start(); } microApp = mountRootParcel(memorizedLoadingFn, { domElement: document.createElement('div'), ...props }); if (containerXPath) { const appContainerXPathKey = getContainerXPathKey(containerXPath); // Store the microApps which they mounted on the same container const microAppsRef = containerMicroAppsMap.get(appContainerXPathKey) || []; microAppsRef.push(microApp); containerMicroAppsMap.set(appContainerXPathKey, microAppsRef); const cleanup = () => { const index = microAppsRef.indexOf(microApp); microAppsRef.splice(index, 1); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore microApp = null; }; // gc after unmount microApp.unmountPromise.then(cleanup).catch(cleanup); } return microApp; } ================================================ FILE: packages/qiankun/src/apis/prefetch.ts ================================================ /** * @author Kuitos * @since 2019-02-26 */ import type { AppMetadata } from '../types'; declare global { interface NetworkInformation { saveData: boolean; effectiveType: string; type: 'bluetooth' | 'cellular' | 'ethernet' | 'none' | 'wifi' | 'wimax' | 'other' | 'unknown'; } interface Navigator { connection?: NetworkInformation; } } // RIC and shim for browsers without requestIdleCallback const requestIdleCallback: typeof window.requestIdleCallback = typeof window.requestIdleCallback === 'function' ? window.requestIdleCallback.bind(window) : function requestIdleCallback(cb: IdleRequestCallback) { const start = Date.now(); return setTimeout(() => { cb({ didTimeout: false, timeRemaining() { return Math.max(0, 50 - (Date.now() - start)); }, }); }, 1) as unknown as number; }; const isSlowNetwork = navigator.connection ? navigator.connection.saveData || (navigator.connection.type !== 'wifi' && navigator.connection.type !== 'ethernet' && /([23])g/.test(navigator.connection.effectiveType)) : false; /** * prefetch assets, do nothing while in mobile network * @param entry * @param fetch */ async function prefetch(entry: string, fetch: typeof window.fetch = window.fetch): Promise { if (!navigator.onLine || isSlowNetwork) { // Don't prefetch if in a slow network or offline return; } requestIdleCallback(() => { void (async () => { try { // Fetch the HTML entry to warm up the cache const response = await fetch(entry); const html = await response.text(); // Parse HTML to find external resources const domParser = new DOMParser(); const doc = domParser.parseFromString(html, 'text/html'); // Prefetch external scripts const scripts = doc.querySelectorAll('script[src]'); scripts.forEach((script) => { const src = script.getAttribute('src'); if (src) { const absoluteSrc = new URL(src, entry).href; requestIdleCallback(() => { void fetch(absoluteSrc).catch(() => { // Ignore prefetch errors }); }); } }); // Prefetch external stylesheets const links = doc.querySelectorAll('link[rel="stylesheet"]'); links.forEach((link) => { const href = link.getAttribute('href'); if (href) { const absoluteHref = new URL(href, entry).href; requestIdleCallback(() => { void fetch(absoluteHref).catch(() => { // Ignore prefetch errors }); }); } }); } catch (e) { // Ignore prefetch errors if (process.env.NODE_ENV === 'development') { // eslint-disable-next-line no-console console.warn('[qiankun] prefetch error:', e); } } })(); }); } export type PrefetchStrategy = | boolean | 'all' | string[] | ((apps: AppMetadata[]) => { criticalAppNames: string[]; minorAppsName: string[] }); /** * Prefetch micro apps immediately * @deprecated This API is deprecated in qiankun 3.0. Micro apps are streamed with automatic preload now. * @param apps - Apps to prefetch * @param fetch - Custom fetch function */ export function prefetchApps(apps: AppMetadata[], fetch: typeof window.fetch = window.fetch): void { if (process.env.NODE_ENV === 'development') { // eslint-disable-next-line no-console console.warn('[qiankun] prefetchApps is deprecated in 3.0; streaming loader performs automatic preload.'); // eslint-disable-next-line no-console console.log('[qiankun] prefetch starting for apps...', apps); } apps.forEach(({ entry }) => { void prefetch(entry, fetch); }); } ================================================ FILE: packages/qiankun/src/apis/registerMicroApps.ts ================================================ import { Deferred } from '@qiankunjs/shared'; import { noop } from 'lodash'; import type { StartOpts } from 'single-spa'; import { registerApplication, start as startSingleSpa } from 'single-spa'; import loadApp from '../core/loadApp'; import type { AppConfiguration, LifeCycles, ObjectType, RegistrableApp } from '../types'; import { toArray } from '../utils'; export let started = false; export const microApps: Array>> = []; const frameworkConfiguration: AppConfiguration = {}; const frameworkStartedDefer = new Deferred(); export function registerMicroApps(apps: Array>, lifeCycles?: LifeCycles) { // Each app only needs to be registered once const unregisteredApps = apps.filter((app) => !microApps.some((registeredApp) => registeredApp.name === app.name)); microApps.push(...unregisteredApps); unregisteredApps.forEach((app) => { const { name, activeRule, loader = noop, props, entry, container } = app; registerApplication({ name, app: async () => { loader(true); await frameworkStartedDefer.promise; const { mount, ...otherMicroAppConfigs } = ( await loadApp({ name, entry, container, props }, frameworkConfiguration, lifeCycles) )(container); return { mount: [async () => loader(true), ...toArray(mount), async () => loader(false)], ...otherMicroAppConfigs, }; }, activeWhen: activeRule, customProps: props, }); }); } export function start(opts: StartOpts = {}) { if (!started) { // frameworkConfiguration = { prefetch: true, singular: true, sandbox: true, ...opts }; // const { prefetch, urlRerouteOnly = defaultUrlRerouteOnly, ...importEntryOpts } = frameworkConfiguration; // if (prefetch) { // doPrefetchStrategy(microApps, prefetch, importEntryOpts); // } // frameworkConfiguration = autoDowngradeForLowVersionBrowser(frameworkConfiguration); startSingleSpa(opts); started = true; frameworkStartedDefer.resolve(); } } ================================================ FILE: packages/qiankun/src/core/loadApp.ts ================================================ /** * @author Kuitos * @since 2023-04-25 */ import type { LoaderOpts } from '@qiankunjs/loader'; import { loadEntry } from '@qiankunjs/loader'; import type { Sandbox } from '@qiankunjs/sandbox'; import { createSandboxContainer, nativeGlobal } from '@qiankunjs/sandbox'; import { defineProperty, hasOwnProperty, makeFetchCacheable, makeFetchRetryable, makeFetchThrowable, moduleResolver as defaultModuleResolver, transpileAssets, warn, } from '@qiankunjs/shared'; import { concat, isFunction, mergeWith } from 'lodash'; import type { ParcelConfigObject } from 'single-spa'; import getAddOns from '../addons'; import { QiankunError } from '../error'; import type { AppConfiguration, LifeCycleFn, LifeCycles, LoadableApp, MicroAppLifeCycles, ObjectType } from '../types'; import { getPureHTMLStringWithoutScripts, performanceGetEntriesByName, performanceMark, performanceMeasure, toArray, } from '../utils'; import { version } from '../version'; export type ParcelConfigObjectGetter = (remountContainer: HTMLElement) => ParcelConfigObject; export default async function loadApp( app: LoadableApp, configuration?: AppConfiguration, lifeCycles?: LifeCycles, ): Promise { const { name: appName, entry, container } = app; const defaultNodeTransformer: AppConfiguration['nodeTransformer'] = (node, opts) => { const moduleResolver = (url: string) => defaultModuleResolver(url, microAppDOMContainer, document.head); return transpileAssets(node, entry, { ...opts, moduleResolver }); }; const { fetch = window.fetch, sandbox = true, globalContext = window, nodeTransformer = defaultNodeTransformer, ...restConfiguration } = configuration || {}; const enhancedFetch = makeFetchCacheable(makeFetchRetryable(makeFetchThrowable(fetch))); const markName = `[qiankun] App ${appName} Loading`; if (process.env.NODE_ENV === 'development') { performanceMark(markName); } let global = globalContext; let mountSandbox: (container: HTMLElement) => Promise = () => Promise.resolve(); let unmountSandbox = () => Promise.resolve(); let sandboxInstance: Sandbox | undefined; const instanceId = genInstanceId(appName); let mountTimes = 1; let microAppDOMContainer: HTMLElement = container; initContainer(microAppDOMContainer, appName, { sandboxCfg: sandbox, mountTimes, instanceId }); if (sandbox) { const sandboxContainer = createSandboxContainer(appName, () => microAppDOMContainer, { globalContext, extraGlobals: {}, fetch: enhancedFetch, nodeTransformer, }); sandboxInstance = sandboxContainer.instance; global = sandboxInstance.globalThis; mountSandbox = (domContainer) => sandboxContainer.mount(domContainer); unmountSandbox = () => sandboxContainer.unmount(); } if (instanceId > 1) { removeWebpackChunkCacheWhenAppHaveMultiInstance(appName); } const containerOpts: LoaderOpts = { fetch: enhancedFetch, sandbox: sandboxInstance, nodeTransformer, ...restConfiguration, }; const lifecyclesPromise = loadEntry(entry, microAppDOMContainer, containerOpts); const assetPublicPath = calcPublicPath(entry); const { beforeUnmount = [], afterUnmount = [], afterMount = [], beforeMount = [], beforeLoad = [], } = mergeWith({}, getAddOns(global, assetPublicPath), lifeCycles, (v1, v2) => concat((v1 ?? []) as LifeCycleFn, (v2 ?? []) as LifeCycleFn), ); // FIXME Due to the asynchronous execution of loadEntry, the DOM of the sub-app is inserted synchronously through appendChild, and inline scripts are also executed synchronously. Therefore, the beforeLoad may need to rely on transformer configuration to coordinate and ensure the order of asynchronous operations. await execHooksChain(toArray(beforeLoad), app, global); const lifecycles = await lifecyclesPromise; const { bootstrap, mount, unmount, update } = getLifecyclesFromExports( lifecycles, appName, global, sandboxInstance?.latestSetProp, ); return (mountContainer) => { const parcelConfig: ParcelConfigObject = { name: appName, // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore bootstrap, mount: [ async () => { if (process.env.NODE_ENV === 'development') { const marks = performanceGetEntriesByName(markName, 'mark'); // mark length is zero means the app is remounting if (marks && !marks.length) { performanceMark(markName); } } }, async () => { microAppDOMContainer = mountContainer; // while the micro app is remounting, we need to load the entry manually if (mountTimes > 1) { initContainer(mountContainer, appName, { sandboxCfg: sandbox, mountTimes, instanceId }); // html scripts should be removed to avoid repeatedly execute const htmlString = await getPureHTMLStringWithoutScripts(entry, enhancedFetch); await loadEntry( { url: entry, res: new Response(htmlString, { status: 200, statusText: 'OK' }) }, mountContainer, containerOpts, ); } }, async () => { await mountSandbox(mountContainer); }, // exec the chain after rendering to keep the behavior with beforeLoad async () => execHooksChain(toArray(beforeMount), app, global), async (props) => mount({ ...props, container: mountContainer }), // finish loading after app mounted async () => execHooksChain(toArray(afterMount), app, global), async () => { if (process.env.NODE_ENV === 'development') { const measureName = `[qiankun] App ${appName} Loading Consuming`; performanceMeasure(measureName, markName); } }, async () => { mountTimes++; }, ], unmount: [ async () => execHooksChain(toArray(beforeUnmount), app, global), async (props) => unmount({ ...props, container: mountContainer }), unmountSandbox, async () => execHooksChain(toArray(afterUnmount), app, global), async () => { clearContainer(mountContainer); }, ], }; if (typeof update === 'function') { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore parcelConfig.update = update; } return parcelConfig; }; } function initContainer( container: HTMLElement, appName: string, opts: { sandboxCfg: AppConfiguration['sandbox']; mountTimes: number; instanceId: number }, ): void { const { sandboxCfg, mountTimes, instanceId } = opts; while (container.firstChild) { container.removeChild(container.firstChild); } container.dataset.name = appName; container.dataset.version = version; container.dataset.sandboxCfg = JSON.stringify(sandboxCfg); if (mountTimes > 1) { container.dataset.mountTimes = String(mountTimes); } if (instanceId > 1) { container.dataset.instanceId = String(instanceId); } } function clearContainer(container: HTMLElement): void { while (container.firstChild) { container.removeChild(container.firstChild); } } function execHooksChain( hooks: Array>, app: LoadableApp, global: WindowProxy = window, ): Promise { if (hooks.length) { return hooks.reduce((chain, hook) => chain.then(() => hook(app, global)), Promise.resolve()); } return Promise.resolve(); } function getLifecyclesFromExports( scriptExports: MicroAppLifeCycles | undefined, appName: string, globalContext: WindowProxy, globalLatestSetProp?: PropertyKey, ): MicroAppLifeCycles { const validateExportLifecycle = (exports: ObjectType | undefined): exports is MicroAppLifeCycles => { const { bootstrap, mount, unmount } = exports ?? {}; return isFunction(bootstrap) && isFunction(mount) && isFunction(unmount); }; if (validateExportLifecycle(scriptExports)) { return scriptExports; } // fallback to sandbox latest set property if it had if (globalLatestSetProp) { const lifecycles = (globalContext as unknown as ObjectType)[globalLatestSetProp as never] as MicroAppLifeCycles; if (validateExportLifecycle(lifecycles)) { return lifecycles; } } if (process.env.NODE_ENV === 'development') { warn(`lifecycle not found from ${appName} entry exports, fallback to get from window['${appName}']`); } // fallback to globalContext variable who named with ${appName} while module exports not found const globalVariableExports = (globalContext as unknown as ObjectType)[appName as never] as MicroAppLifeCycles; if (validateExportLifecycle(globalVariableExports)) { return globalVariableExports; } throw new QiankunError( `You need to export lifecycle functions in ${appName} entry as neither globalLatestSetProp ${String( globalLatestSetProp, )} nor window['${appName}'] export correctly`, ); } function calcPublicPath(entry: string): string { try { const { origin, pathname } = new URL(entry, location.href); const paths = pathname.split('/'); paths.pop(); return `${origin}${paths.join('/')}/`; } catch (e) { console.warn(e); return ''; } } /** * To prevent webpack from skipping reload logic and causing the js not to re-execute when a micro app is loaded multiple times on the same viewport, * the data-webpack attribute of the script must be removed. * see https://github.com/webpack/webpack/blob/1f13ff9fe587e094df59d660b4611b1bd19aed4c/lib/runtime/LoadScriptRuntimeModule.js#L131-L136 */ function removeWebpackChunkCacheWhenAppHaveMultiInstance(appName: string): void { const mountedSameNameApps = document.querySelectorAll(`[data-name^="${appName}"]`); if (mountedSameNameApps.length > 1) { mountedSameNameApps.forEach((appContainerElement) => { appContainerElement.querySelectorAll('script[src]').forEach((script) => { script.removeAttribute('data-webpack'); }); }); } } const globalAppInstanceStoreKey = '__agii__'; declare global { interface Window { // app global instance id [globalAppInstanceStoreKey]?: Record; } } function genInstanceId(appName: string): number { if (!hasOwnProperty(nativeGlobal, globalAppInstanceStoreKey)) { defineProperty(nativeGlobal, globalAppInstanceStoreKey, { enumerable: false, configurable: false, writable: true, value: {}, }); } nativeGlobal[globalAppInstanceStoreKey]![appName] = nativeGlobal[globalAppInstanceStoreKey]![appName] ? nativeGlobal[globalAppInstanceStoreKey]![appName] + 1 : 1; return nativeGlobal[globalAppInstanceStoreKey]![appName]; } ================================================ FILE: packages/qiankun/src/error.ts ================================================ export class QiankunError extends Error { constructor(message: string) { super(`[qiankun]: ${message}`); } } ================================================ FILE: packages/qiankun/src/index.ts ================================================ export * from './apis/loadMicroApp'; export * from './apis/registerMicroApps'; export * from './apis/isRuntimeCompatible'; export * from './apis/prefetch'; export * from './apis/effects'; export * from './apis/errorHandler'; export * from './types'; ================================================ FILE: packages/qiankun/src/types.ts ================================================ /** * @author Kuitos * @since 2023-04-25 */ import type { LoaderOpts } from '@qiankunjs/loader'; import type { LifeCycles as ParcelLifeCycles, Parcel, RegisterApplicationConfig } from 'single-spa'; declare global { // eslint-disable-next-line @typescript-eslint/consistent-type-definitions interface Window { __POWERED_BY_QIANKUN__?: boolean; __INJECTED_PUBLIC_PATH_BY_QIANKUN__?: string; __QIANKUN_DEVELOPMENT__?: boolean; Zone?: CallableFunction; __zone_symbol__setTimeout?: Window['setTimeout']; } } export type ObjectType = Record; export type HTMLEntry = string; export type AppMetadata = { // app name name: string; // app entry entry: HTMLEntry; }; // just for manual loaded apps, in single-spa it called parcel export type LoadableApp = AppMetadata & { // where the app mount to container: HTMLElement; // props pass to app props?: T; }; // for the route-based apps export type RegistrableApp = LoadableApp & { loader?: (loading: boolean) => void; activeRule: RegisterApplicationConfig['activeWhen']; }; export type AppConfiguration = Partial> & { sandbox?: boolean; globalContext?: WindowProxy; }; export type LifeCycleFn = (app: LoadableApp, global: WindowProxy) => Promise; export type LifeCycles = { beforeLoad?: LifeCycleFn | Array>; // function before app load beforeMount?: LifeCycleFn | Array>; // function before app mount afterMount?: LifeCycleFn | Array>; // function after app mount beforeUnmount?: LifeCycleFn | Array>; // function before app unmount afterUnmount?: LifeCycleFn | Array>; // function after app unmount }; export type MicroApp = Parcel; type ExtraProps = { container: HTMLElement; }; type FlattenArray = T extends Array ? U : T; type FlattenArrayValue = { [P in keyof T]: FlattenArray; }; export type MicroAppLifeCycles = FlattenArrayValue>; ================================================ FILE: packages/qiankun/src/utils.ts ================================================ /** * @author Kuitos * @since 2023-04-25 */ export function toArray(array: T | T[]): T[] { return Array.isArray(array) ? array : [array]; } /** * copy from https://developer.mozilla.org/zh-CN/docs/Using_XPath * @param el * @param document */ function getXPathForElement(el: Node, document: Document): string | void { // not support that if el not existed in document yet(such as it not append to document before it mounted) if (!document.body.contains(el)) { return undefined; } let xpath = ''; let pos; let tmpEle; let element = el; while (element !== document.documentElement) { pos = 0; tmpEle = element; while (tmpEle) { if (tmpEle.nodeType === 1 && tmpEle.nodeName === element.nodeName) { // If it is ELEMENT_NODE of the same name pos += 1; } tmpEle = tmpEle.previousSibling; } xpath = `*[name()='${element.nodeName}'][${pos}]/${xpath}`; element = element.parentNode!; } xpath = `/*[name()='${document.documentElement.nodeName}']/${xpath}`; xpath = xpath.replace(/\/$/, ''); return xpath; } export function getContainerXPath(container: HTMLElement): string | void { return getXPathForElement(container, document); } const supportsUserTiming = typeof performance !== 'undefined' && typeof performance.mark === 'function' && typeof performance.clearMarks === 'function' && typeof performance.measure === 'function' && typeof performance.clearMeasures === 'function' && typeof performance.getEntriesByName === 'function'; export function performanceGetEntriesByName(markName: string, type?: string) { let marks = null; if (supportsUserTiming) { marks = performance.getEntriesByName(markName, type); } return marks; } export function performanceMark(markName: string) { if (supportsUserTiming) { performance.mark(markName); } } export function performanceMeasure(measureName: string, markName: string) { if (supportsUserTiming && performance.getEntriesByName(markName, 'mark').length) { performance.measure(measureName, markName); performance.clearMarks(markName); performance.clearMeasures(measureName); } } export async function getPureHTMLStringWithoutScripts(entry: string, fetch: typeof window.fetch): Promise { const htmlString = await fetch(entry).then((r) => r.text()); const domParser = new DOMParser(); const htmlDOM = domParser.parseFromString(htmlString, 'text/html'); // remove all script tags who are been loaded before htmlDOM.querySelectorAll('script').forEach((script) => script.remove()); htmlDOM.querySelectorAll('link[rel=prefetch],link[rel=preload]').forEach((link) => link.remove()); return htmlDOM.documentElement.outerHTML; } ================================================ FILE: packages/sandbox/.fatherrc.js ================================================ import { writeFileSync } from 'fs'; import globals from 'globals'; import { join } from 'path'; console.log('generate globals.ts...'); // generate globals.ts const globalsFilePath = join(__dirname, './src/core/globals.ts'); writeFileSync( globalsFilePath, `// generated from https://github.com/sindresorhus/globals/blob/main/globals.json es2015 part // only init its values while Proxy is supported // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition export const globalsInES2015 = window.Proxy ? ${JSON.stringify( Object.keys(globals.es2015), null, 2, )}.filter(p => /* just keep the available properties in current window context */ p in window) : []; export const globalsInBrowser = ${JSON.stringify(Object.keys(globals.browser), null, 2)}; `, ); console.log('generate globals.ts succeed...'); export { default } from '../../.fatherrc.cjs'; ================================================ FILE: packages/sandbox/AGENTS.md ================================================ # @qiankunjs/sandbox JS isolation engine using Proxy-based Membrane + Compartment execution model. ## STRUCTURE ``` sandbox/ ├── core/ │ ├── sandbox/ # StandardSandbox, createSandboxContainer │ ├── membrane/ # Proxy wrapper for global isolation │ ├── compartment/ # Code evaluation with `with(this)` binding │ └── globals.ts # Global property definitions ├── patchers/ │ ├── dynamicAppend/ # DOM appendChild/insertBefore interception │ ├── windowListener.ts # Event listener tracking │ ├── interval.ts # Timer tracking │ └── historyListener.ts └── consts.ts # qiankunHeadTagName, etc. ``` ## WHERE TO LOOK | Task | File | Notes | | --- | --- | --- | | Create sandbox | `core/sandbox/index.ts` | `createSandboxContainer()` returns mount/unmount | | Proxy logic | `core/membrane/index.ts` | Write → local target, Read → target → endowments → host | | Code execution | `core/compartment/index.ts` | `with(this)` scope binding | | DOM interception | `patchers/dynamicAppend/forStandardSandbox.ts` | Redirects to app container | | Side effect cleanup | `patchers/*.ts` | Each returns `free()` function | ## KEY PATTERNS ### Membrane (Proxy) - **Writes**: Trapped and stored in local `target` object - **Reads**: Check local → endowments → fallback to host window - **Native rebinding**: `fetch`, `console` rebound to avoid "Illegal invocation" ### Patcher/Free Pattern ```typescript // Every patcher returns cleanup function const free = patchWindowListener(sandbox); // On unmount: free(); // Removes all listeners added by micro-app ``` ### WeakMap Metadata - `sandboxConfigWeakMap`: Sandbox config per instance - `elementAttachSandboxConfigMap`: Tracks which app owns which DOM node ## ANTI-PATTERNS - **NEVER** access real `window.document.head` directly - use proxied version - **FIXME**: Indirect `eval` in membrane causes System.js scope escape - **FIXME**: Global variable for runtime container may miss monkey-patched logic ## EXPORTS ```typescript export { createSandboxContainer } from './core/sandbox'; export { StandardSandbox } from './core/sandbox/StandardSandbox'; export { Compartment } from './core/compartment'; export { qiankunHeadTagName } from './consts'; ``` ================================================ FILE: packages/sandbox/CHANGELOG.md ================================================ # @qiankunjs/sandbox ## 0.0.1-rc.17 ### Patch Changes - 9c56910: feat: support addEventListener with once options to avoid memory leak - 6d252c6: chore: optimize code - Updated dependencies [ea18ce6] - @qiankunjs/shared@0.0.1-rc.12 ## 0.0.1-rc.16 ### Patch Changes - 99bf65f: feat: support huge inline-script who might be split into multiple chunks during transfer - Updated dependencies [56fef69] - Updated dependencies [99bf65f] - @qiankunjs/shared@0.0.1-rc.11 ## 0.0.1-rc.15 ### Patch Changes - c3416647: fix: double quote link element href as selector ## 0.0.1-rc.14 ### Patch Changes - 8c526255: Revert "fix(sandbox): non-hijacking elements should be appended to global document (#2861)" ## 0.0.1-rc.13 ### Patch Changes - f09c1538: feat: pass container with parameters rather than getter function - d904f5d8: fix(sandbox): compatible with dynamically appending stylesheets to detached containers - b2d2c11a: feat: optimize lifecycle validate log - feb544f0: fix: dynamic append element should support for the same container between micro apps - 9082546e: fix(sandbox): compatible with dynamically appending scripts to detached containers - 62048537: fix(sandbox): non-hijacking elements should be appended to global document - Updated dependencies [a826cf5e] - Updated dependencies [3e43a111] - Updated dependencies [feb544f0] - @qiankunjs/shared@0.0.1-rc.10 ## 0.0.1-rc.12 ### Patch Changes - bd12dbad: fix: defer scripts should wait until html loaded - Updated dependencies [bd12dbad] - @qiankunjs/shared@0.0.1-rc.9 ## 0.0.1-rc.11 ### Patch Changes - 98b071bf: feat: support defer scripts and keep the executing order to consist with browser - Updated dependencies [98b071bf] - @qiankunjs/shared@0.0.1-rc.8 ## 0.0.1-rc.10 ### Patch Changes - f2af2e36: feat: extract NodeTransformer type to shared package - Updated dependencies [f2af2e36] - @qiankunjs/shared@0.0.1-rc.7 ## 0.0.1-rc.9 ### Patch Changes - d3e9872d: feat(sandbox): use cloneNode api instead of importNode for compatible - 7cc06bd4: feat(loader): add lru cache for assets fetch by default - Updated dependencies [54b0878e] - Updated dependencies [7ba95cf2] - Updated dependencies [312abbc7] - Updated dependencies [6f074136] - @qiankunjs/shared@0.0.1-rc.6 ## 0.0.1-rc.8 ### Patch Changes - 43bf37a5: fix(sandbox): should get container from getter function in every accessing - a34a92a9: feat(sandbox): micro app mounting should wait unit rebuilding link element loaded to avoid unstyleed content flash - 7cf93b54: fix(sandbox): createElement hijack must be paired to avoid rewriting leak - 32106b11: fix(sandbox): dynamic async script order should calculate on the fly ## 0.0.1-rc.7 ### Patch Changes - 5f77347b: feat(sandbox): support dynamic sync scripts executed by order in sandbox - Updated dependencies [5f77347b] - Updated dependencies [8e54e129] - @qiankunjs/shared@0.0.1-rc.5 ## 0.0.1-rc.6 ### Patch Changes - 2aca545c: fix: should invoke getContainer method to get container every time to avoid reference misordering ## 0.0.1-rc.5 ### Patch Changes - 3d1d3367: fix: should patch the container head/body element immediately rather than patch its functions with proxy ## 0.0.1-rc.4 ### Patch Changes - 488447ad: ✨ set proxy appendChild/insertBefore method for every sandbox rather than modify prototype on HTMLElement - dc4d9aef: 🐛parallel sandbox should use different compartment id - e7d788ef: feat: not rebind non-native global properties - 76b6bff7: 🐛 compatible with webpack chunk cache logic - Updated dependencies [76b6bff7] - @qiankunjs/shared@0.0.1-rc.4 ## 0.0.1-rc.3 ### Patch Changes - 39301f19: 🔀 merge master - @qiankunjs/shared@0.0.1-rc.3 ## 0.0.1-rc.2 ### Patch Changes - Updated dependencies [b23d3d7b] - @qiankunjs/shared@0.0.1-rc.2 ## 0.0.1-rc.1 ### Patch Changes - Updated dependencies [ebb2bcaa] - @qiankunjs/shared@0.0.1-rc.1 ## 0.0.1-beta.6 ### Patch Changes - ffd77800: ✨support to transform head/body tags to qiankun head/body in stream - Updated dependencies [ffd77800] - @qiankunjs/shared@0.0.1-beta.6 ## 0.0.1-alpha.5 ### Patch Changes - Updated dependencies [fcb49aad] - Updated dependencies [065d2c54] - Updated dependencies [931dc1f7] - @qiankunjs/shared@0.0.1-alpha.5 ## 0.0.1-alpha.4 ### Patch Changes - Updated dependencies [62d3b482] - @qiankunjs/shared@0.0.1-alpha.4 ## 0.0.1-alpha.3 ### Patch Changes - daaa9ccc: ✨support code block in sandbox - Updated dependencies [e12d29ae] - Updated dependencies [daaa9ccc] - @qiankunjs/shared@0.0.1-alpha.3 ## 0.0.1-alpha.2 ### Patch Changes - 33e65888: fix: changeset - Updated dependencies [33e65888] - @qiankunjs/shared@0.0.1-alpha.2 ## 0.0.1-alpha.1 ### Patch Changes - Updated dependencies - @qiankunjs/shared@0.0.1-alpha.1 ## 0.0.1-alpha.0 ### Patch Changes - 3.0 alpha - Updated dependencies - @qiankunjs/shared@0.0.1-alpha.0 ================================================ FILE: packages/sandbox/package.json ================================================ { "name": "@qiankunjs/sandbox", "version": "0.0.1-rc.17", "description": "qiankun sandbox", "repository": { "type": "git", "url": "git+https://github.com/umijs/qiankun.git" }, "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", "types": "./dist/esm/index.d.ts", "sideEffects": false, "scripts": { "build": "father build" }, "files": [ "dist" ], "author": "Kuitos", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.10", "@qiankunjs/shared": "workspace:^", "lodash": "^4.17.11" }, "devDependencies": { "globals": "^13.20.0" }, "publishConfig": { "registry": "https://registry.npmjs.org/" } } ================================================ FILE: packages/sandbox/src/consts.ts ================================================ /** * @author Kuitos * @since 2023-03-16 */ export const nativeGlobal = document.defaultView!; export const nativeDocument = nativeGlobal.document; export const qiankunHeadTagName = 'qiankun-head'; export const qiankunBodyTagName = 'qiankun-body'; ================================================ FILE: packages/sandbox/src/core/compartment/globalProps.ts ================================================ import { hasOwnProperty } from '@qiankunjs/shared'; let firstGlobalProp: string | undefined, secondGlobalProp: string | undefined, lastGlobalProp: string | undefined; export function getGlobalProp(global: WindowProxy, useFirstGlobalProp = false) { let cnt = 0; let foundLastProp, result; let hasIframe = false; for (const p in global) { // do not check frames because it could be removed during import if (shouldSkipProperty(global, p)) continue; // 遍历 iframe,检查 window 上的属性值是否是 iframe,是则跳过后面的 first 和 second 判断 for (let i = 0; i < window.frames.length && !hasIframe; i++) { const frame = window.frames[i]; if (frame === global[p as unknown as number]) { hasIframe = true; break; } } if ((!hasIframe && cnt === 0 && p !== firstGlobalProp) || (cnt === 1 && p !== secondGlobalProp)) return p; if (foundLastProp) { lastGlobalProp = p; result = (useFirstGlobalProp && result) || p; } else { foundLastProp = p === lastGlobalProp; } cnt++; } return result; } export function noteGlobalProps(global: WindowProxy) { // alternatively Object.keys(global).pop() // but this may be faster (pending benchmarks) firstGlobalProp = secondGlobalProp = undefined; for (const p in global) { // do not check frames because it could be removed during import if (shouldSkipProperty(global, p)) continue; if (!firstGlobalProp) firstGlobalProp = p; else if (!secondGlobalProp) secondGlobalProp = p; lastGlobalProp = p; } return lastGlobalProp; } const isIE11 = () => typeof navigator !== 'undefined' && navigator.userAgent.indexOf('Trident') !== -1; function shouldSkipProperty(global: WindowProxy, p: string | number): boolean { if (!hasOwnProperty(global, p) || (!isNaN(p as number) && (p as number) < global.length)) return true; if (isIE11()) { // https://github.com/kuitos/import-html-entry/pull/32,最小化 try 范围 try { return !!global[p as keyof WindowProxy] && typeof window !== 'undefined' && global[p as number].parent === window; } catch (err) { return true; } } else { return false; } } ================================================ FILE: packages/sandbox/src/core/compartment/index.ts ================================================ // type Transform = (source: string) => string; // type ModuleMap = Record; // // interface CompartmentOptions { // transforms?: Transform[]; // } import { nativeGlobal } from '../../consts'; const compartmentGlobalIdPrefix = '__compartment_globalThis__'; const compartmentGlobalIdSuffix = '__'; const getCompartmentGlobalId = (id: number): CompartmentGlobalId => `${compartmentGlobalIdPrefix}${String(id)}${compartmentGlobalIdSuffix}`; type CompartmentGlobalId = `${typeof compartmentGlobalIdPrefix}${string}${typeof compartmentGlobalIdSuffix}`; declare global { interface Window { [p: CompartmentGlobalId]: WindowProxy | undefined; } } let compartmentCounter = 0; export class Compartment { /** * Since the time of execution of the code in Compartment is determined by the browser, a unique compartmentSpecifier should be generated in Compartment */ private readonly id: CompartmentGlobalId; private readonly _globalThis: WindowProxy; private constantIntrinsicNames: string[] = []; constructor(globalProxy: WindowProxy) { this._globalThis = globalProxy; // make sure the compartmentSpecifier is unique while (nativeGlobal[getCompartmentGlobalId(compartmentCounter)]) { compartmentCounter++; } this.id = getCompartmentGlobalId(compartmentCounter); nativeGlobal[this.id] = globalProxy; } get globalThis(): WindowProxy { return this._globalThis; } protected addConstantIntrinsicNames(intrinsics: string[]): void { this.constantIntrinsicNames = [...intrinsics, ...this.constantIntrinsicNames]; } makeEvaluateFactory(source: string, sourceURL?: string): string { const sourceMapURL = sourceURL ? `//# sourceURL=${sourceURL}\n` : ''; const globalObjectOptimizer = this.constantIntrinsicNames.length ? `const {${this.constantIntrinsicNames.join(',')}} = this;` : ''; // eslint-disable-next-line max-len return `;(function(){with(this){${globalObjectOptimizer}${source}\n${sourceMapURL}}}).bind(window.${this.id})();`; } // TODO add return value // evaluate(code: string, options?: CompartmentOptions): void { // const { transforms } = options || {}; // const transformedCode = transforms?.reduce((acc, transform) => transform(acc), code) || code; // const codeFactory = this.makeEvaluateFactory(transformedCode); // // const script = document.createElement('script'); // script.textContent = codeFactory; // document.head.appendChild(script); // } } ================================================ FILE: packages/sandbox/src/core/globals.ts ================================================ // generated from https://github.com/sindresorhus/globals/blob/main/globals.json es2015 part // only init its values while Proxy is supported // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition export const globalsInES2015 = window.Proxy ? [ "Array", "ArrayBuffer", "Boolean", "constructor", "DataView", "Date", "decodeURI", "decodeURIComponent", "encodeURI", "encodeURIComponent", "Error", "escape", "eval", "EvalError", "Float32Array", "Float64Array", "Function", "hasOwnProperty", "Infinity", "Int16Array", "Int32Array", "Int8Array", "isFinite", "isNaN", "isPrototypeOf", "JSON", "Map", "Math", "NaN", "Number", "Object", "parseFloat", "parseInt", "Promise", "propertyIsEnumerable", "Proxy", "RangeError", "ReferenceError", "Reflect", "RegExp", "Set", "String", "Symbol", "SyntaxError", "toLocaleString", "toString", "TypeError", "Uint16Array", "Uint32Array", "Uint8Array", "Uint8ClampedArray", "undefined", "unescape", "URIError", "valueOf", "WeakMap", "WeakSet" ].filter(p => /* just keep the available properties in current window context */ p in window) : []; export const globalsInBrowser = [ "AbortController", "AbortSignal", "addEventListener", "alert", "AnalyserNode", "Animation", "AnimationEffectReadOnly", "AnimationEffectTiming", "AnimationEffectTimingReadOnly", "AnimationEvent", "AnimationPlaybackEvent", "AnimationTimeline", "applicationCache", "ApplicationCache", "ApplicationCacheErrorEvent", "atob", "Attr", "Audio", "AudioBuffer", "AudioBufferSourceNode", "AudioContext", "AudioDestinationNode", "AudioListener", "AudioNode", "AudioParam", "AudioProcessingEvent", "AudioScheduledSourceNode", "AudioWorkletGlobalScope", "AudioWorkletNode", "AudioWorkletProcessor", "BarProp", "BaseAudioContext", "BatteryManager", "BeforeUnloadEvent", "BiquadFilterNode", "Blob", "BlobEvent", "blur", "BroadcastChannel", "btoa", "BudgetService", "ByteLengthQueuingStrategy", "Cache", "caches", "CacheStorage", "cancelAnimationFrame", "cancelIdleCallback", "CanvasCaptureMediaStreamTrack", "CanvasGradient", "CanvasPattern", "CanvasRenderingContext2D", "ChannelMergerNode", "ChannelSplitterNode", "CharacterData", "clearInterval", "clearTimeout", "clientInformation", "ClipboardEvent", "ClipboardItem", "close", "closed", "CloseEvent", "Comment", "CompositionEvent", "confirm", "console", "ConstantSourceNode", "ConvolverNode", "CountQueuingStrategy", "createImageBitmap", "Credential", "CredentialsContainer", "crypto", "Crypto", "CryptoKey", "CSS", "CSSConditionRule", "CSSFontFaceRule", "CSSGroupingRule", "CSSImportRule", "CSSKeyframeRule", "CSSKeyframesRule", "CSSMatrixComponent", "CSSMediaRule", "CSSNamespaceRule", "CSSPageRule", "CSSPerspective", "CSSRotate", "CSSRule", "CSSRuleList", "CSSScale", "CSSSkew", "CSSSkewX", "CSSSkewY", "CSSStyleDeclaration", "CSSStyleRule", "CSSStyleSheet", "CSSSupportsRule", "CSSTransformValue", "CSSTranslate", "CustomElementRegistry", "customElements", "CustomEvent", "DataTransfer", "DataTransferItem", "DataTransferItemList", "defaultstatus", "defaultStatus", "DelayNode", "DeviceMotionEvent", "DeviceOrientationEvent", "devicePixelRatio", "dispatchEvent", "document", "Document", "DocumentFragment", "DocumentType", "DOMError", "DOMException", "DOMImplementation", "DOMMatrix", "DOMMatrixReadOnly", "DOMParser", "DOMPoint", "DOMPointReadOnly", "DOMQuad", "DOMRect", "DOMRectList", "DOMRectReadOnly", "DOMStringList", "DOMStringMap", "DOMTokenList", "DragEvent", "DynamicsCompressorNode", "Element", "ErrorEvent", "event", "Event", "EventSource", "EventTarget", "external", "fetch", "File", "FileList", "FileReader", "find", "focus", "FocusEvent", "FontFace", "FontFaceSetLoadEvent", "FormData", "FormDataEvent", "frameElement", "frames", "GainNode", "Gamepad", "GamepadButton", "GamepadEvent", "getComputedStyle", "getSelection", "HashChangeEvent", "Headers", "history", "History", "HTMLAllCollection", "HTMLAnchorElement", "HTMLAreaElement", "HTMLAudioElement", "HTMLBaseElement", "HTMLBodyElement", "HTMLBRElement", "HTMLButtonElement", "HTMLCanvasElement", "HTMLCollection", "HTMLContentElement", "HTMLDataElement", "HTMLDataListElement", "HTMLDetailsElement", "HTMLDialogElement", "HTMLDirectoryElement", "HTMLDivElement", "HTMLDListElement", "HTMLDocument", "HTMLElement", "HTMLEmbedElement", "HTMLFieldSetElement", "HTMLFontElement", "HTMLFormControlsCollection", "HTMLFormElement", "HTMLFrameElement", "HTMLFrameSetElement", "HTMLHeadElement", "HTMLHeadingElement", "HTMLHRElement", "HTMLHtmlElement", "HTMLIFrameElement", "HTMLImageElement", "HTMLInputElement", "HTMLLabelElement", "HTMLLegendElement", "HTMLLIElement", "HTMLLinkElement", "HTMLMapElement", "HTMLMarqueeElement", "HTMLMediaElement", "HTMLMenuElement", "HTMLMetaElement", "HTMLMeterElement", "HTMLModElement", "HTMLObjectElement", "HTMLOListElement", "HTMLOptGroupElement", "HTMLOptionElement", "HTMLOptionsCollection", "HTMLOutputElement", "HTMLParagraphElement", "HTMLParamElement", "HTMLPictureElement", "HTMLPreElement", "HTMLProgressElement", "HTMLQuoteElement", "HTMLScriptElement", "HTMLSelectElement", "HTMLShadowElement", "HTMLSlotElement", "HTMLSourceElement", "HTMLSpanElement", "HTMLStyleElement", "HTMLTableCaptionElement", "HTMLTableCellElement", "HTMLTableColElement", "HTMLTableElement", "HTMLTableRowElement", "HTMLTableSectionElement", "HTMLTemplateElement", "HTMLTextAreaElement", "HTMLTimeElement", "HTMLTitleElement", "HTMLTrackElement", "HTMLUListElement", "HTMLUnknownElement", "HTMLVideoElement", "IDBCursor", "IDBCursorWithValue", "IDBDatabase", "IDBFactory", "IDBIndex", "IDBKeyRange", "IDBObjectStore", "IDBOpenDBRequest", "IDBRequest", "IDBTransaction", "IDBVersionChangeEvent", "IdleDeadline", "IIRFilterNode", "Image", "ImageBitmap", "ImageBitmapRenderingContext", "ImageCapture", "ImageData", "indexedDB", "innerHeight", "innerWidth", "InputEvent", "IntersectionObserver", "IntersectionObserverEntry", "Intl", "isSecureContext", "KeyboardEvent", "KeyframeEffect", "KeyframeEffectReadOnly", "length", "localStorage", "location", "Location", "locationbar", "matchMedia", "MediaDeviceInfo", "MediaDevices", "MediaElementAudioSourceNode", "MediaEncryptedEvent", "MediaError", "MediaKeyMessageEvent", "MediaKeySession", "MediaKeyStatusMap", "MediaKeySystemAccess", "MediaList", "MediaMetadata", "MediaQueryList", "MediaQueryListEvent", "MediaRecorder", "MediaSettingsRange", "MediaSource", "MediaStream", "MediaStreamAudioDestinationNode", "MediaStreamAudioSourceNode", "MediaStreamEvent", "MediaStreamTrack", "MediaStreamTrackEvent", "menubar", "MessageChannel", "MessageEvent", "MessagePort", "MIDIAccess", "MIDIConnectionEvent", "MIDIInput", "MIDIInputMap", "MIDIMessageEvent", "MIDIOutput", "MIDIOutputMap", "MIDIPort", "MimeType", "MimeTypeArray", "MouseEvent", "moveBy", "moveTo", "MutationEvent", "MutationObserver", "MutationRecord", "name", "NamedNodeMap", "NavigationPreloadManager", "navigator", "Navigator", "NavigatorUAData", "NetworkInformation", "Node", "NodeFilter", "NodeIterator", "NodeList", "Notification", "OfflineAudioCompletionEvent", "OfflineAudioContext", "offscreenBuffering", "OffscreenCanvas", "OffscreenCanvasRenderingContext2D", "onabort", "onafterprint", "onanimationend", "onanimationiteration", "onanimationstart", "onappinstalled", "onauxclick", "onbeforeinstallprompt", "onbeforeprint", "onbeforeunload", "onblur", "oncancel", "oncanplay", "oncanplaythrough", "onchange", "onclick", "onclose", "oncontextmenu", "oncuechange", "ondblclick", "ondevicemotion", "ondeviceorientation", "ondeviceorientationabsolute", "ondrag", "ondragend", "ondragenter", "ondragleave", "ondragover", "ondragstart", "ondrop", "ondurationchange", "onemptied", "onended", "onerror", "onfocus", "ongotpointercapture", "onhashchange", "oninput", "oninvalid", "onkeydown", "onkeypress", "onkeyup", "onlanguagechange", "onload", "onloadeddata", "onloadedmetadata", "onloadstart", "onlostpointercapture", "onmessage", "onmessageerror", "onmousedown", "onmouseenter", "onmouseleave", "onmousemove", "onmouseout", "onmouseover", "onmouseup", "onmousewheel", "onoffline", "ononline", "onpagehide", "onpageshow", "onpause", "onplay", "onplaying", "onpointercancel", "onpointerdown", "onpointerenter", "onpointerleave", "onpointermove", "onpointerout", "onpointerover", "onpointerup", "onpopstate", "onprogress", "onratechange", "onrejectionhandled", "onreset", "onresize", "onscroll", "onsearch", "onseeked", "onseeking", "onselect", "onstalled", "onstorage", "onsubmit", "onsuspend", "ontimeupdate", "ontoggle", "ontransitionend", "onunhandledrejection", "onunload", "onvolumechange", "onwaiting", "onwheel", "open", "openDatabase", "opener", "Option", "origin", "OscillatorNode", "outerHeight", "outerWidth", "OverconstrainedError", "PageTransitionEvent", "pageXOffset", "pageYOffset", "PannerNode", "parent", "Path2D", "PaymentAddress", "PaymentRequest", "PaymentRequestUpdateEvent", "PaymentResponse", "performance", "Performance", "PerformanceEntry", "PerformanceLongTaskTiming", "PerformanceMark", "PerformanceMeasure", "PerformanceNavigation", "PerformanceNavigationTiming", "PerformanceObserver", "PerformanceObserverEntryList", "PerformancePaintTiming", "PerformanceResourceTiming", "PerformanceTiming", "PeriodicWave", "Permissions", "PermissionStatus", "personalbar", "PhotoCapabilities", "Plugin", "PluginArray", "PointerEvent", "PopStateEvent", "postMessage", "Presentation", "PresentationAvailability", "PresentationConnection", "PresentationConnectionAvailableEvent", "PresentationConnectionCloseEvent", "PresentationConnectionList", "PresentationReceiver", "PresentationRequest", "print", "ProcessingInstruction", "ProgressEvent", "PromiseRejectionEvent", "prompt", "PushManager", "PushSubscription", "PushSubscriptionOptions", "queueMicrotask", "RadioNodeList", "Range", "ReadableStream", "registerProcessor", "RemotePlayback", "removeEventListener", "reportError", "Request", "requestAnimationFrame", "requestIdleCallback", "resizeBy", "ResizeObserver", "ResizeObserverEntry", "resizeTo", "Response", "RTCCertificate", "RTCDataChannel", "RTCDataChannelEvent", "RTCDtlsTransport", "RTCIceCandidate", "RTCIceGatherer", "RTCIceTransport", "RTCPeerConnection", "RTCPeerConnectionIceEvent", "RTCRtpContributingSource", "RTCRtpReceiver", "RTCRtpSender", "RTCSctpTransport", "RTCSessionDescription", "RTCStatsReport", "RTCTrackEvent", "screen", "Screen", "screenLeft", "ScreenOrientation", "screenTop", "screenX", "screenY", "ScriptProcessorNode", "scroll", "scrollbars", "scrollBy", "scrollTo", "scrollX", "scrollY", "SecurityPolicyViolationEvent", "Selection", "self", "ServiceWorker", "ServiceWorkerContainer", "ServiceWorkerRegistration", "sessionStorage", "setInterval", "setTimeout", "ShadowRoot", "SharedWorker", "SourceBuffer", "SourceBufferList", "speechSynthesis", "SpeechSynthesisEvent", "SpeechSynthesisUtterance", "StaticRange", "status", "statusbar", "StereoPannerNode", "stop", "Storage", "StorageEvent", "StorageManager", "structuredClone", "styleMedia", "StyleSheet", "StyleSheetList", "SubmitEvent", "SubtleCrypto", "SVGAElement", "SVGAngle", "SVGAnimatedAngle", "SVGAnimatedBoolean", "SVGAnimatedEnumeration", "SVGAnimatedInteger", "SVGAnimatedLength", "SVGAnimatedLengthList", "SVGAnimatedNumber", "SVGAnimatedNumberList", "SVGAnimatedPreserveAspectRatio", "SVGAnimatedRect", "SVGAnimatedString", "SVGAnimatedTransformList", "SVGAnimateElement", "SVGAnimateMotionElement", "SVGAnimateTransformElement", "SVGAnimationElement", "SVGCircleElement", "SVGClipPathElement", "SVGComponentTransferFunctionElement", "SVGDefsElement", "SVGDescElement", "SVGDiscardElement", "SVGElement", "SVGEllipseElement", "SVGFEBlendElement", "SVGFEColorMatrixElement", "SVGFEComponentTransferElement", "SVGFECompositeElement", "SVGFEConvolveMatrixElement", "SVGFEDiffuseLightingElement", "SVGFEDisplacementMapElement", "SVGFEDistantLightElement", "SVGFEDropShadowElement", "SVGFEFloodElement", "SVGFEFuncAElement", "SVGFEFuncBElement", "SVGFEFuncGElement", "SVGFEFuncRElement", "SVGFEGaussianBlurElement", "SVGFEImageElement", "SVGFEMergeElement", "SVGFEMergeNodeElement", "SVGFEMorphologyElement", "SVGFEOffsetElement", "SVGFEPointLightElement", "SVGFESpecularLightingElement", "SVGFESpotLightElement", "SVGFETileElement", "SVGFETurbulenceElement", "SVGFilterElement", "SVGForeignObjectElement", "SVGGElement", "SVGGeometryElement", "SVGGradientElement", "SVGGraphicsElement", "SVGImageElement", "SVGLength", "SVGLengthList", "SVGLinearGradientElement", "SVGLineElement", "SVGMarkerElement", "SVGMaskElement", "SVGMatrix", "SVGMetadataElement", "SVGMPathElement", "SVGNumber", "SVGNumberList", "SVGPathElement", "SVGPatternElement", "SVGPoint", "SVGPointList", "SVGPolygonElement", "SVGPolylineElement", "SVGPreserveAspectRatio", "SVGRadialGradientElement", "SVGRect", "SVGRectElement", "SVGScriptElement", "SVGSetElement", "SVGStopElement", "SVGStringList", "SVGStyleElement", "SVGSVGElement", "SVGSwitchElement", "SVGSymbolElement", "SVGTextContentElement", "SVGTextElement", "SVGTextPathElement", "SVGTextPositioningElement", "SVGTitleElement", "SVGTransform", "SVGTransformList", "SVGTSpanElement", "SVGUnitTypes", "SVGUseElement", "SVGViewElement", "TaskAttributionTiming", "Text", "TextDecoder", "TextEncoder", "TextEvent", "TextMetrics", "TextTrack", "TextTrackCue", "TextTrackCueList", "TextTrackList", "TimeRanges", "toolbar", "top", "Touch", "TouchEvent", "TouchList", "TrackEvent", "TransformStream", "TransitionEvent", "TreeWalker", "UIEvent", "URL", "URLSearchParams", "ValidityState", "visualViewport", "VisualViewport", "VTTCue", "WaveShaperNode", "WebAssembly", "WebGL2RenderingContext", "WebGLActiveInfo", "WebGLBuffer", "WebGLContextEvent", "WebGLFramebuffer", "WebGLProgram", "WebGLQuery", "WebGLRenderbuffer", "WebGLRenderingContext", "WebGLSampler", "WebGLShader", "WebGLShaderPrecisionFormat", "WebGLSync", "WebGLTexture", "WebGLTransformFeedback", "WebGLUniformLocation", "WebGLVertexArrayObject", "WebSocket", "WheelEvent", "window", "Window", "Worker", "WritableStream", "XMLDocument", "XMLHttpRequest", "XMLHttpRequestEventTarget", "XMLHttpRequestUpload", "XMLSerializer", "XPathEvaluator", "XPathExpression", "XPathResult", "XSLTProcessor" ]; ================================================ FILE: packages/sandbox/src/core/membrane/index.ts ================================================ /* eslint-disable no-param-reassign */ import { create, defineProperty, freeze, getOwnPropertyDescriptor, getOwnPropertyNames, hasOwnProperty, keys, } from '@qiankunjs/shared'; import { nativeGlobal } from '../../consts'; import { isPropertyFrozen } from '../../utils'; import { globalsInBrowser } from '../globals'; import { array2TruthyObject } from '../utils'; import { rebindTarget2Fn } from './utils'; declare global { interface Window { __QIANKUN_DEVELOPMENT__?: boolean; } } export type MembraneTarget = Record; export type Endowments = Record; type SymbolTarget = 'target' | 'globalContext'; const variableWhiteListInDev = process.env.NODE_ENV === 'test' || process.env.NODE_ENV === 'development' || window.__QIANKUN_DEVELOPMENT__ ? [ // for react hot reload // see https://github.com/facebook/create-react-app/blob/66bf7dfc43350249e2f09d138a20840dae8a0a4a/packages/react-error-overlay/src/index.js#L180 '__REACT_ERROR_OVERLAY_GLOBAL_HOOK__', // for react development event issue, see https://github.com/umijs/qiankun/issues/2375 'event', ] : []; // who could escape the sandbox const globalVariableWhiteList: string[] = [ // FIXME System.js used a indirect call with eval, which would make it scope escape to global // To make System.js works well, we write it back to global window temporary // see https://github.com/systemjs/systemjs/blob/457f5b7e8af6bd120a279540477552a07d5de086/src/evaluate.js#L106 'System', // see https://github.com/systemjs/systemjs/blob/457f5b7e8af6bd120a279540477552a07d5de086/src/instantiate.js#L357 '__cjsWrapper', ...variableWhiteListInDev, ]; const useNativeWindowForBindingsProps = new Map([ ['fetch', true], ['mockDomAPIInBlackList', process.env.NODE_ENV === 'test'], ]); const isPropertyDescriptor = (v: unknown): boolean => { return ( typeof v === 'object' && v !== null && ['value', 'writable', 'get', 'set', 'configurable', 'enumerable'].some((p) => p in v) ); }; const cachedGlobalsInBrowser = array2TruthyObject( globalsInBrowser.concat(process.env.NODE_ENV === 'test' ? ['mockNativeWindowFunction'] : []), ); const isNativeGlobalProp = (prop: string): boolean => { return prop in cachedGlobalsInBrowser; }; export class Membrane { private locking = false; modifications = new Set(); realmGlobal: WindowProxy; target: MembraneTarget; latestSetProp: PropertyKey | undefined; constructor( incubatorContext: WindowProxy, unscopables: Record, opts?: { whitelist?: string[]; endowments?: Endowments; }, ) { const { endowments = {} } = opts || {}; const whitelistVars = [...(opts?.whitelist || []), ...globalVariableWhiteList]; const descriptorTargetMap = new Map(); const { target, propertiesWithGetter } = createMembraneTarget(endowments, incubatorContext); this.target = target; this.realmGlobal = new Proxy(this.target, { set: (membraneTarget, p, value: never) => { if (!this.locking) { // sync the property to incubatorContext if (typeof p === 'string' && whitelistVars.indexOf(p) !== -1) { // this.globalWhitelistPrevDescriptor[p] = Object.getOwnPropertyDescriptor(incubatorContext, p); incubatorContext[p as never] = value; } else { // We must keep its description while the property existed in incubatorContext before if (!hasOwnProperty(membraneTarget, p) && hasOwnProperty(incubatorContext, p)) { const descriptor = getOwnPropertyDescriptor(incubatorContext, p); const { writable, configurable, enumerable } = descriptor!; // only writable property can be overwritten // here we ignored accessor descriptor of incubatorContext as it makes no sense to trigger its logic(which might make sandbox escaping instead) // we force to set value by data descriptor if (writable || hasOwnProperty(descriptor, 'set')) { defineProperty(membraneTarget, p, { configurable, enumerable, writable: true, value }); } } else { membraneTarget[p] = value; } } this.modifications.add(p); this.latestSetProp = p; return true; } if (process.env.NODE_ENV === 'development') { // console.warn(`[qiankun] Set window.${p.toString()} while sandbox destroyed or inactive in ${name}!`); console.warn(`[qiankun] Set window.${p.toString()} while sandbox destroyed or inactive!`); } // 在 strict-mode 下,Proxy 的 handler.set 返回 false 会抛出 TypeError,在沙箱卸载的情况下应该忽略错误 return true; }, get: (membraneTarget, p, receiver) => { if (p === Symbol.unscopables) return unscopables; // properties in endowments returns directly if (hasOwnProperty(endowments, p)) { return membraneTarget[p]; } if (p === 'string' && whitelistVars.indexOf(p) !== -1) { return incubatorContext[p as never]; } const actualTarget = propertiesWithGetter.has(p) ? incubatorContext : p in membraneTarget ? membraneTarget : incubatorContext; const value = actualTarget[p as never]; // frozen value should return directly, see https://github.com/umijs/qiankun/issues/2015 if (isPropertyFrozen(actualTarget, p)) { return value; } // non-native property return directly to avoid rebind if (!isNativeGlobalProp(p as string) && !useNativeWindowForBindingsProps.has(p)) { return value; } /* Some dom api must be bound to native window, otherwise it would cause exception like 'TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation' See this code: const proxy = new Proxy(window, {}); const proxyFetch = fetch.bind(proxy); proxyFetch('https://qiankun.com'); */ const boundTarget = useNativeWindowForBindingsProps.get(p) ? nativeGlobal : incubatorContext; return rebindTarget2Fn(boundTarget, value, receiver); }, // trap in operator // see https://github.com/styled-components/styled-components/blob/master/packages/styled-components/src/constants.js#L12 has(membraneTarget: MembraneTarget, p: string | number | symbol): boolean { return p in membraneTarget || p in incubatorContext; }, getOwnPropertyDescriptor( membraneTarget: MembraneTarget, p: string | number | symbol, ): PropertyDescriptor | undefined { /* as the descriptor of top/self/window/mockTop in raw window are configurable but not in proxy membraneTarget, we need to get it from membraneTarget to avoid TypeError see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/getOwnPropertyDescriptor > A property cannot be reported as non-configurable, if it does not exist as an own property of the membraneTarget object or if it exists as a configurable own property of the membraneTarget object. */ if (hasOwnProperty(membraneTarget, p)) { const descriptor = getOwnPropertyDescriptor(membraneTarget, p); descriptorTargetMap.set(p, 'target'); return descriptor; } if (hasOwnProperty(incubatorContext, p)) { const descriptor = getOwnPropertyDescriptor(incubatorContext, p); descriptorTargetMap.set(p, 'globalContext'); // A property cannot be reported as non-configurable, if it does not exist as an own property of the membraneTarget object if (descriptor && !descriptor.configurable) { descriptor.configurable = true; } return descriptor; } return undefined; }, // trap to support iterator with sandbox ownKeys(membraneTarget: MembraneTarget): ArrayLike { return uniq(Reflect.ownKeys(incubatorContext).concat(Reflect.ownKeys(membraneTarget))); }, defineProperty: (membraneTarget, p, attributes) => { const from = descriptorTargetMap.get(p); /* Descriptor must be defined to native window while it comes from native window via Object.getOwnPropertyDescriptor(window, p), otherwise it would cause a TypeError with illegal invocation. */ switch (from) { case 'globalContext': return Reflect.defineProperty(incubatorContext, p, attributes); default: return Reflect.defineProperty(membraneTarget, p, attributes); } }, deleteProperty: (membraneTarget, p) => { if (hasOwnProperty(membraneTarget, p)) { delete membraneTarget[p]; this.modifications.delete(p); return true; } return true; }, // makes sure `window instanceof Window` returns truthy in micro app getPrototypeOf() { return Reflect.getPrototypeOf(incubatorContext); }, }) as unknown as WindowProxy; } addIntrinsics( intrinsics: | Record | ((rawTarget: MembraneTarget) => Record), ): void { const intrinsicsObj = typeof intrinsics === 'function' ? intrinsics(this.target) : intrinsics; keys(intrinsicsObj).forEach((key) => { defineProperty(this.target, key, intrinsicsObj[key]); }); } lock() { this.locking = true; } unlock() { this.locking = false; } } function createMembraneTarget( endowments: Endowments = {}, incubatorContext: WindowProxy, ): { target: MembraneTarget; propertiesWithGetter: Map; } { // map always has the best performance in `has` check scenario // see https://jsperf.com/array-indexof-vs-set-has/23 const propertiesWithGetter = new Map(); const target: MembraneTarget = keys(endowments).reduce((acc, key) => { const value = endowments[key]; if (isPropertyDescriptor(value)) { defineProperty(acc, key, value); } else { acc[key] = value; } return acc; }, {} as MembraneTarget); /* copy the non-configurable property of incubatorContext to membrane target to avoid TypeError see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/getOwnPropertyDescriptor > A property cannot be reported as non-configurable, if it does not exist as an own property of the target object or if it exists as a configurable own property of the target object. */ getOwnPropertyNames(incubatorContext) .filter((p) => { const descriptor = getOwnPropertyDescriptor(incubatorContext, p); return !hasOwnProperty(endowments, p) && !descriptor?.configurable; }) .forEach((p) => { const descriptor = getOwnPropertyDescriptor(incubatorContext, p); if (descriptor) { const hasGetter = hasOwnProperty(descriptor, 'get'); if (hasGetter) { propertiesWithGetter.set(p, true); } defineProperty( target, p, // freeze the descriptor to avoid being modified by zone.js // see https://github.com/angular/zone.js/blob/a5fe09b0fac27ac5df1fa746042f96f05ccb6a00/lib/browser/define-property.ts#L71 freeze(descriptor), ); } }); return { target, propertiesWithGetter, }; } /** * fastest(at most time) unique array method * @see https://jsperf.com/array-filter-unique/30 */ function uniq(array: Array) { return array.filter(function (this: Record, element) { return element in this ? false : (this[element] = true); }, create(null)); } ================================================ FILE: packages/sandbox/src/core/membrane/utils.ts ================================================ import { defineProperty, getOwnPropertyDescriptor, getOwnPropertyNames, hasOwnProperty } from '@qiankunjs/shared'; import { isBoundedFunction, isCallable, isConstructable } from '../../utils'; const functionBoundedValueMap = new WeakMap(); export function rebindTarget2Fn(target: unknown, fn: T, receiver: unknown): T { /* 仅绑定 isCallable && !isBoundedFunction && !isConstructable 的函数对象,如 window.console、window.atob 这类,不然微应用中调用时会抛出 Illegal invocation 异常 目前没有完美的检测方式,这里通过 prototype 中是否还有可枚举的拓展方法的方式来判断 @warning 这里不要随意替换成别的判断方式,因为可能触发一些 edge case(比如在 lodash.isFunction 在 iframe 上下文中可能由于调用了 top window 对象触发的安全异常) */ if (isCallable(fn) && !isBoundedFunction(fn) && !isConstructable(fn as CallableFunction)) { const typedValue = fn as CallableFunction; const cachedBoundFunction = functionBoundedValueMap.get(typedValue); if (cachedBoundFunction) { return cachedBoundFunction as T; } const boundValue = function proxyFunction(...args: unknown[]): unknown { return Function.prototype.apply.call( typedValue, target, args.map((arg) => (arg === receiver ? target : arg)), ); }; // some callable function has custom fields, we need to copy the own props to boundValue. such as moment function. getOwnPropertyNames(typedValue).forEach((key) => { // boundValue might be a proxy, we need to check the key whether exist in it if (!hasOwnProperty(boundValue, key)) { defineProperty(boundValue, key, getOwnPropertyDescriptor(typedValue, key)!); } }); // copy prototype if bound function not have but target one have // as prototype is non-enumerable mostly, we need to copy it from target function manually if (hasOwnProperty(typedValue, 'prototype') && !hasOwnProperty(boundValue, 'prototype')) { // we should not use assignment operator to set boundValue prototype like `boundValue.prototype = typedValue.prototype` // as the assignment will also look up prototype chain while it hasn't own prototype property, // when the lookup succeed, the assignment will throw an TypeError like `Cannot assign to read only property 'prototype' of function` if its descriptor configured with writable false or just have a getter accessor // see https://github.com/umijs/qiankun/issues/1121 defineProperty(boundValue, 'prototype', { value: typedValue.prototype as unknown, enumerable: false, writable: true, }); } // Some util, like `function isNative() { return typeof Ctor === 'function' && /native code/.test(Ctor.toString()) }` relies on the original `toString()` result // but bound functions will always return "function() {[native code]}" for `toString`, which is misleading if (typeof typedValue.toString === 'function') { const valueHasInstanceToString = hasOwnProperty(typedValue, 'toString') && !hasOwnProperty(boundValue, 'toString'); const boundValueHasPrototypeToString = boundValue.toString === Function.prototype.toString; if (valueHasInstanceToString || boundValueHasPrototypeToString) { const originToStringDescriptor = getOwnPropertyDescriptor( valueHasInstanceToString ? fn : Function.prototype, 'toString', ); Object.defineProperty( boundValue, 'toString', Object.assign( {}, originToStringDescriptor, originToStringDescriptor?.get ? null : { value: () => typedValue.toString() }, ), ); } } functionBoundedValueMap.set(fn, boundValue); return boundValue as T; } return fn; } ================================================ FILE: packages/sandbox/src/core/sandbox/StandardSandbox.ts ================================================ import { hasOwnProperty } from '@qiankunjs/shared'; import { without } from 'lodash'; import { Compartment } from '../compartment'; import { globalsInES2015 } from '../globals'; import type { Endowments } from '../membrane'; import { Membrane } from '../membrane'; import { array2TruthyObject } from '../utils'; import type { Sandbox } from './types'; import { SandboxType } from './types'; const whitelistBOMAPIs = ['requestAnimationFrame', 'cancelAnimationFrame']; export class StandardSandbox extends Compartment implements Sandbox { private readonly membrane: Membrane; readonly type = SandboxType.Standard; readonly name: string; constructor(name: string, globals: Endowments, incubatorContext: WindowProxy = window) { const getRealmGlobal = () => realmGlobal; const getTopValue = (p: 'top' | 'parent'): WindowProxy => { // if your master app in an iframe context, allow these props escape the sandbox if (incubatorContext === incubatorContext.parent) { return realmGlobal; } return incubatorContext[p]!; }; const intrinsics: Record = { // avoid who using window.window or window.self to escape the sandbox environment to touch the real window // see https://github.com/eligrey/FileSaver.js/blob/master/src/FileSaver.js#L13 window: { get: getRealmGlobal, enumerable: true, configurable: false }, self: { get: getRealmGlobal, enumerable: true, configurable: false }, globalThis: { get: getRealmGlobal, enumerable: false, configurable: true }, // proxy.hasOwnProperty would invoke getter firstly, then its value represented as incubatorContext.hasOwnProperty hasOwnProperty: { value: function hasOwnPropertyImpl(this: unknown, key: PropertyKey): boolean { // calling from hasOwnProperty.call(obj, key) if (this !== realmGlobal && this !== null && typeof this === 'object') { return hasOwnProperty(this, key); } return hasOwnProperty(target, key) || hasOwnProperty(incubatorContext, key); }, writable: true, enumerable: false, configurable: true, }, // eslint-disable-next-line no-eval eval: { value: eval, writable: true, enumerable: false, configurable: true }, top: { get() { return getTopValue('top'); }, configurable: false, enumerable: true, }, parent: { get() { return getTopValue('parent'); }, configurable: false, enumerable: true, }, // Temporarily occupy the document as it may be modified later document: { value: document, writable: true, enumerable: true, configurable: true }, }; if (process.env.NODE_ENV === 'test') { ['mockSafariTop', 'mockTop', 'mockGlobalThis'].forEach((key) => { intrinsics[key] = { get: getRealmGlobal, enumerable: false, configurable: true }; }); } const constantNames = Array.from(new Set(Object.keys(intrinsics).concat(globalsInES2015).concat(whitelistBOMAPIs))); // intrinsics should not be escaped from sandbox const unscopables = array2TruthyObject(without(constantNames, ...Object.keys(intrinsics))); const membrane = new Membrane(incubatorContext, unscopables, { whitelist: [], endowments: { ...intrinsics, ...globals }, }); const { realmGlobal, target } = membrane; super(realmGlobal); this.name = name; this.membrane = membrane; this.addConstantIntrinsicNames(constantNames); } get latestSetProp() { return this.membrane.latestSetProp; } addIntrinsics(intrinsics: Record) { this.membrane.addIntrinsics(intrinsics); } active() { this.membrane.unlock(); } inactive() { if (process.env.NODE_ENV === 'development') { console.info(`[qiankun:sandbox] ${this.name} modified global properties restore...`, [ ...this.membrane.modifications.keys(), ]); } this.membrane.lock(); } // TODO // destroy() { // // } } ================================================ FILE: packages/sandbox/src/core/sandbox/index.ts ================================================ /** * @author Kuitos * @since 2019-04-11 */ import { patchAtBootstrapping, patchAtMounting } from '../../patchers'; import type { SandboxConfig } from '../../patchers/dynamicAppend/types'; import type { Free, Rebuild } from '../../patchers/types'; import type { Endowments } from '../membrane'; import { StandardSandbox } from './StandardSandbox'; import type { Sandbox } from './types'; export type { Sandbox }; /** * @param appName * @param getContainer * @param opts */ export function createSandboxContainer( appName: string, getContainer: () => HTMLElement, opts: { globalContext?: WindowProxy; extraGlobals?: Endowments; } & Pick, ) { const { globalContext, extraGlobals = {}, ...sandboxCfg } = opts; let sandbox: Sandbox; // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (window.Proxy) { sandbox = new StandardSandbox(appName, extraGlobals, globalContext); } else { // TODO snapshot sandbox sandbox = new StandardSandbox(appName, extraGlobals, globalContext); } // some side effect could be invoked while bootstrapping, such as dynamic stylesheet injection with style-loader, especially during the development phase const bootstrappingFrees = patchAtBootstrapping(appName, getContainer, { sandbox, ...sandboxCfg }); // mounting frees are one-off and should be re-init at every mounting time let mountingFrees: Free[] = []; let sideEffectsRebuilds: Rebuild[] = []; return { instance: sandbox, /** * 沙箱被 mount * 可能是从 bootstrap 状态进入的 mount * 也可能是从 unmount 之后再次唤醒进入 mount */ async mount(container: HTMLElement) { /* ------------------------------------------ 因为有上下文依赖(window),以下代码执行顺序不能变 ------------------------------------------ */ /* ------------------------------------------ 1. 启动/恢复 沙箱------------------------------------------ */ sandbox.active(); const sideEffectsRebuildsAtBootstrapping = sideEffectsRebuilds.slice(0, bootstrappingFrees.length); const sideEffectsRebuildsAtMounting = sideEffectsRebuilds.slice(bootstrappingFrees.length); // must rebuild the side effects which added at bootstrapping firstly to recovery to nature state if (sideEffectsRebuildsAtBootstrapping.length) { for (const rebuildSideEffects of sideEffectsRebuildsAtBootstrapping) { await rebuildSideEffects(container); } } /* ------------------------------------------ 2. 开启全局变量补丁 ------------------------------------------*/ // render 沙箱启动时开始劫持各类全局监听,尽量不要在应用初始化阶段有 事件监听/定时器 等副作用 mountingFrees = patchAtMounting(appName, getContainer, { sandbox, ...sandboxCfg }); /* ------------------------------------------ 3. 重置一些初始化时的副作用 ------------------------------------*/ // 存在 rebuilds 则表明有些副作用需要重建 if (sideEffectsRebuildsAtMounting.length) { for (const rebuildSideEffects of sideEffectsRebuildsAtMounting) { await rebuildSideEffects(container); } } // clean up rebuilds sideEffectsRebuilds = []; }, /** * 恢复 global 状态,使其能回到应用加载之前的状态 */ async unmount() { // record the rebuilds of window side effects (event listeners or timers) // note that the frees of mounting phase are one-off as it will be re-init at next mounting sideEffectsRebuilds = [...bootstrappingFrees, ...mountingFrees].map((free) => free()); sandbox.inactive(); }, }; } ================================================ FILE: packages/sandbox/src/core/sandbox/types.ts ================================================ /** * @author Kuitos * @since 2023-05-04 */ import type { Compartment } from '../compartment'; export enum SandboxType { Standard = 'Standard', Snapshot = 'Snapshot', } export interface Sandbox extends Compartment { name: string; type: SandboxType; latestSetProp?: PropertyKey; active(): void; inactive(): void; addIntrinsics: (intrinsics: Record) => void; // TODO for gc // destroy(): void; } ================================================ FILE: packages/sandbox/src/core/utils.ts ================================================ /** * @author Kuitos * @since 2023-11-15 */ /** * transform the array to a truthy object for better performance with in operator check later * @param array */ export function array2TruthyObject(array: string[]): Record { return array.reduce( (obj, key) => { obj[key] = true; return obj; }, // Notes that babel will transpile spread operator to Object.assign({}, ...args), which will keep the prototype of Object in merged object, // while this result used as Symbol.unscopables, it will make properties in Object.prototype always be escaped from proxy sandbox as unscopables check will look up prototype chain as well, // such as hasOwnProperty, toString, valueOf, etc. // so we should use Object.create(null) to create a pure object without prototype chain here. Object.create(null) as Record, ); } ================================================ FILE: packages/sandbox/src/index.ts ================================================ export * from './core/sandbox'; export * from './core/compartment'; export * from './consts'; ================================================ FILE: packages/sandbox/src/patchers/consts.ts ================================================ ================================================ FILE: packages/sandbox/src/patchers/dynamicAppend/common.ts ================================================ /* eslint-disable @typescript-eslint/unbound-method */ import type { AssetsTranspilerOpts, ScriptTranspilerOpts } from '@qiankunjs/shared'; /** * @author Kuitos * @since 2019-10-21 */ import { prepareDeferredQueue, warn } from '@qiankunjs/shared'; import { qiankunHeadTagName } from '../../consts'; import type { SandboxConfig } from './types'; const SCRIPT_TAG_NAME = 'SCRIPT'; const LINK_TAG_NAME = 'LINK'; const STYLE_TAG_NAME = 'STYLE'; export const styleElementTargetSymbol = Symbol('target'); export const styleElementRefNodeNo = Symbol('refNodeNo'); const overwrittenSymbol = Symbol('qiankun-overwritten'); type DynamicDomMutationTarget = 'head' | 'body'; declare global { interface HTMLLinkElement { [styleElementTargetSymbol]: DynamicDomMutationTarget; [styleElementRefNodeNo]?: Exclude; } interface HTMLStyleElement { [styleElementTargetSymbol]: DynamicDomMutationTarget; [styleElementRefNodeNo]?: Exclude; } interface Function { [overwrittenSymbol]: boolean; } } export const getContainerHeadElement = (container: Element): HTMLHeadElement | null => { return container.querySelector(qiankunHeadTagName); }; export const getContainerBodyElement = (container: Element): HTMLBodyElement => { return container as HTMLBodyElement; }; export function isHijackingTag(tagName?: string) { return ( tagName?.toUpperCase() === LINK_TAG_NAME || tagName?.toUpperCase() === STYLE_TAG_NAME || tagName?.toUpperCase() === SCRIPT_TAG_NAME ); } /** * Check if a style element is a styled-component liked. * A styled-components liked element is which not have textContext but keep the rules in its styleSheet.cssRules. * Such as the style element generated by styled-components and emotion. * @param element */ export function isStyledComponentsLike(element: HTMLStyleElement): boolean { return Boolean(!element.textContent && (element.sheet?.cssRules.length || getStyledElementCSSRules(element)?.length)); } const appsCounterMap = new Map(); export function calcAppCount( appName: string, calcType: 'increase' | 'decrease', status: 'bootstrapping' | 'mounting', ): void { const appCount = appsCounterMap.get(appName) || { bootstrappingPatchCount: 0, mountingPatchCount: 0 }; switch (calcType) { case 'increase': appCount[`${status}PatchCount`] += 1; break; case 'decrease': // bootstrap patch just called once but its freer will be called multiple times if (appCount[`${status}PatchCount`] > 0) { appCount[`${status}PatchCount`] -= 1; } break; } appsCounterMap.set(appName, appCount); } export function isAllAppsUnmounted(): boolean { return Array.from(appsCounterMap.entries()).every( ([, { bootstrappingPatchCount: bpc, mountingPatchCount: mpc }]) => bpc === 0 && mpc === 0, ); } const defineNonEnumerableProperty = (target: unknown, key: string | symbol, value: unknown) => { Object.defineProperty(target, key, { configurable: true, enumerable: false, writable: true, value, }); }; const styledComponentCSSRulesMap = new WeakMap(); export function recordStyledComponentsCSSRules(styleElements: HTMLStyleElement[]): void { styleElements.forEach((styleElement) => { /* With a styled-components generated style element, we need to record its cssRules for restore next re-mounting time. We're doing this because the sheet of style element is going to be cleaned automatically by browser after the style element dom removed from document. see https://www.w3.org/TR/cssom-1/#associated-css-style-sheet */ if (styleElement instanceof HTMLStyleElement && isStyledComponentsLike(styleElement)) { if (styleElement.sheet) { // record the original css rules of the style element for restore styledComponentCSSRulesMap.set(styleElement, styleElement.sheet.cssRules); } } }); } export function getStyledElementCSSRules(styledElement: HTMLStyleElement): CSSRuleList | undefined { return styledComponentCSSRulesMap.get(styledElement); } export function getOverwrittenAppendChildOrInsertBefore( nativeFn: typeof HTMLElement.prototype.appendChild | typeof HTMLElement.prototype.insertBefore, getSandboxConfig: (element: HTMLElement) => SandboxConfig | undefined, target: DynamicDomMutationTarget = 'body', ) { function appendChildInSandbox( this: HTMLHeadElement | HTMLBodyElement, newChild: T, refChild: Node | null = null, ): T { const appendChild = nativeFn; const element = newChild as unknown as HTMLElement; const sandboxConfig = getSandboxConfig(element); // no attached sandbox config means the element is not created from the sandbox environment if (!isHijackingTag(element.tagName) || !sandboxConfig) { return appendChild.call(this, element, refChild) as T; } if (element.tagName) { switch (element.tagName) { case LINK_TAG_NAME: case STYLE_TAG_NAME: { const stylesheetElement = element as HTMLLinkElement | HTMLStyleElement; Object.defineProperty(stylesheetElement, styleElementTargetSymbol, { value: target, writable: true, configurable: true, }); const referenceNode = this.contains(refChild) ? refChild : null; let refNo: number | undefined; if (referenceNode) { refNo = Array.from(this.childNodes).indexOf(referenceNode as ChildNode); } const { sandbox, nodeTransformer, fetch } = sandboxConfig; const transpiledStyleSheetElement = nodeTransformer(stylesheetElement, { fetch, sandbox, }); const stylesheetTargetDetached = !document.contains(this); if (stylesheetTargetDetached) { warn( `Trying to append stylesheet element ${ ('href' in transpiledStyleSheetElement && transpiledStyleSheetElement.href) || transpiledStyleSheetElement.dataset.href } to a detached container which may cause unexpected behaviors!`, ); } // FIXME we have to set the target container to global document to trigger style rendering while the real container was detached const targetContainerDOM = stylesheetTargetDetached ? document[target] : this; const result = appendChild.call(targetContainerDOM, transpiledStyleSheetElement, referenceNode); // record refNo thus we can keep order while remounting if (typeof refNo === 'number' && refNo !== -1) { defineNonEnumerableProperty(transpiledStyleSheetElement, styleElementRefNodeNo, refNo); } const { dynamicStyleSheetElements } = sandboxConfig; // record dynamic style elements after insert succeed dynamicStyleSheetElements.push(transpiledStyleSheetElement); return result as T; } case SCRIPT_TAG_NAME: { const scriptElement = element as HTMLScriptElement; const { sandbox, dynamicExternalSyncScriptDeferredList, nodeTransformer, fetch } = sandboxConfig; const externalSyncMode = scriptElement.hasAttribute('src') && !scriptElement.hasAttribute('async'); let transformerOpts: AssetsTranspilerOpts = { fetch, sandbox, }; let queueSyncScript: () => void; if (externalSyncMode) { const { deferred, prevDeferred, queue } = prepareDeferredQueue(dynamicExternalSyncScriptDeferredList); transformerOpts = { ...transformerOpts, scriptTranspiledDeferred: deferred, prevScriptTranspiledDeferred: prevDeferred, } as ScriptTranspilerOpts; queueSyncScript = queue; } const transpiledScriptElement = nodeTransformer(scriptElement, transformerOpts); /* The target container of script element might be removed from current document. For example, the main application clears the DOM first during route switching, and then trigger unmount, at this time, the micro app may still be processing the logic of the route switching, and try to add nodes to the detached container, in this scenario, we have to append the script to global document head to trigger script evaluation */ const scriptTargetDetached = !document.contains(this); if (scriptTargetDetached) { warn( `Trying to append script element ${ transpiledScriptElement.src || transpiledScriptElement.dataset.src } to a detached container which may cause unexpected behaviors!`, ); } /* FIXME we have to set the target container to global document to trigger script evaluation while the real container was detached, as dynamic append script element to detached element will not trigger script evaluation automatically */ const targetContainerDOM = scriptTargetDetached ? document[target] : this; const result = appendChild.call(targetContainerDOM, transpiledScriptElement, refChild) as T; // the script have no src attribute after transpile, indicating that the script needs to wait for the src to be filled if (externalSyncMode && !transpiledScriptElement.hasAttribute('src')) { queueSyncScript!(); } return result; } default: break; } } return appendChild.call(this, element, refChild) as T; } appendChildInSandbox[overwrittenSymbol] = true; return appendChildInSandbox; } export function getNewRemoveChild( nativeFn: typeof HTMLElement.prototype.removeChild, containerConfigGetter: (element: HTMLElement) => SandboxConfig | undefined, ) { function removeChildInSandbox(this: HTMLHeadElement | HTMLBodyElement, child: T): T { const removeChild = nativeFn; const childElement = child as unknown as HTMLElement; const { tagName } = childElement; const containerConfig = containerConfigGetter(childElement); if (!isHijackingTag(tagName) || !containerConfig) { return removeChild.call(this, childElement) as T; } try { const { dynamicStyleSheetElements } = containerConfig; switch (tagName) { case STYLE_TAG_NAME: case LINK_TAG_NAME: { // try to remove the dynamic style sheet const dynamicElementIndex = dynamicStyleSheetElements.indexOf( childElement as HTMLLinkElement | HTMLStyleElement, ); if (dynamicElementIndex !== -1) { dynamicStyleSheetElements.splice(dynamicElementIndex, 1); } break; } default: { break; } } // container might have been removed while app unmounting if the removeChild action was async if (this.contains(childElement)) { return removeChild.call(this, childElement) as T; } } catch (e) { console.warn(e); } return removeChild.call(this, childElement) as T; } removeChildInSandbox[overwrittenSymbol] = true; return removeChildInSandbox; } export function rebuildCSSRules( styleSheetElements: Array, reAppendElement: (stylesheetElement: HTMLStyleElement | HTMLLinkElement) => Promise, ): Array> { return styleSheetElements.map(async (styleSheetElement) => { // re-append the dynamic stylesheet to sub-app container const appendSuccess = await reAppendElement(styleSheetElement); if (appendSuccess) { /* get the stored css rules from styled-components generated element, and the re-insert rules for them. note that we must do this after style element had been added to document, which stylesheet would be associated to the document automatically. check the spec https://www.w3.org/TR/cssom-1/#associated-css-style-sheet */ if (styleSheetElement instanceof HTMLStyleElement && isStyledComponentsLike(styleSheetElement)) { const cssRules = getStyledElementCSSRules(styleSheetElement); if (cssRules) { // eslint-disable-next-line no-plusplus for (let i = 0; i < cssRules.length; i++) { const cssRule = cssRules[i]; const cssStyleSheetElement = styleSheetElement.sheet as CSSStyleSheet; cssStyleSheetElement.insertRule(cssRule.cssText, cssStyleSheetElement.cssRules.length); } } } } }); } ================================================ FILE: packages/sandbox/src/patchers/dynamicAppend/forStandardSandbox.ts ================================================ /* eslint-disable */ /** * @author Kuitos * @since 2020-10-13 */ import { Deferred, QiankunError } from '@qiankunjs/shared'; import type { noop } from 'lodash'; import { nativeDocument, nativeGlobal, qiankunHeadTagName } from '../../consts'; import { rebindTarget2Fn } from '../../core/membrane/utils'; import type { Sandbox } from '../../core/sandbox'; import type { Free } from '../types'; import { calcAppCount, getContainerBodyElement, getContainerHeadElement, getNewRemoveChild, getOverwrittenAppendChildOrInsertBefore, isAllAppsUnmounted, rebuildCSSRules, recordStyledComponentsCSSRules, styleElementRefNodeNo, styleElementTargetSymbol, } from './common'; import type { SandboxConfig } from './types'; const elementAttachedSymbol = Symbol('attachedApp'); declare global { interface HTMLElement { [elementAttachedSymbol]: string; } interface Window { __currentLockingSandbox__?: Sandbox; } interface Document { [p: string]: unknown; } } // Get native global window with a sandbox disgusted way, thus we could share it between qiankun instances🤪 Object.defineProperty(nativeGlobal, '__sandboxConfigWeakMap__', { enumerable: false, writable: true }); Object.defineProperty(nativeGlobal, '__currentLockingSandbox__', { enumerable: false, writable: true, configurable: true, }); const sandboxConfigWeakMap = new WeakMap(); const elementAttachSandboxConfigMap = new WeakMap(); const patchCacheWeakMap = new WeakMap(); const getSandboxConfig = (element: HTMLElement) => elementAttachSandboxConfigMap.get(element); function patchDocument(sandbox: Sandbox, getContainer: () => HTMLElement): CallableFunction { const container = getContainer(); // dom container might be reused by multiple apps, // thus we check its attached sandbox is same with current to avoid duplicate patch if (patchCacheWeakMap.get(container) === sandbox) { return () => {}; } const unpatch = patchDocumentHeadAndBodyMethods(container); const attachElementToSandbox = (element: HTMLElement) => { const sandboxConfig = sandboxConfigWeakMap.get(sandbox); if (sandboxConfig) { elementAttachSandboxConfigMap.set(element, sandboxConfig); } }; const getDocumentHeadElement = () => { const container = getContainer(); const containerHeadElement = getContainerHeadElement(container); if (!containerHeadElement) { throw new QiankunError(`${sandbox.name} head element not existed while accessing document.head!`); } return containerHeadElement; }; const getDocumentBodyElement = () => { const container = getContainer(); return getContainerBodyElement(container); }; const modificationFns: { createElement?: typeof document.createElement; querySelector?: typeof document.querySelector; } = {}; const proxyDocument = new Proxy(document, { /** * Read and write must be paired, otherwise the write operation will leak to the global */ set: (target, p, value) => { switch (p) { case 'createElement': { modificationFns.createElement = value; break; } case 'querySelector': { modificationFns.querySelector = value; break; } default: target[p as keyof Document] = value; break; } return true; }, get: (target, p, receiver) => { switch (p) { case 'createElement': { // Must store the original createElement function to avoid error in nested sandbox const targetCreateElement = modificationFns.createElement || target.createElement; return function createElement(...args: Parameters) { if (!nativeGlobal.__currentLockingSandbox__) { nativeGlobal.__currentLockingSandbox__ = sandbox; } const element = targetCreateElement.call(target, ...args); // only record the element which is created by the current sandbox, thus we can avoid the element created by nested sandboxes if (nativeGlobal.__currentLockingSandbox__ === sandbox) { attachElementToSandbox(element); delete nativeGlobal.__currentLockingSandbox__; } return element; }; } case 'querySelector': { const targetQuerySelector = modificationFns.querySelector || target.querySelector; return function querySelector(...args: Parameters) { const selector = args[0]; switch (selector) { case 'head': { return getDocumentHeadElement(); } case 'body': { return getDocumentBodyElement(); } } return targetQuerySelector.call(target, ...args); }; } case 'head': { return getDocumentHeadElement(); } case 'body': { return getDocumentBodyElement(); } default: break; } const value = target[p as keyof Document]; // must rebind the function to the target otherwise it will cause illegal invocation error return rebindTarget2Fn(target, value, receiver); }, }); sandbox.addIntrinsics({ document: { value: proxyDocument, writable: false, enumerable: true, configurable: true }, }); patchCacheWeakMap.set(container, sandbox); return () => { unpatch(); }; } function patchDocumentHeadAndBodyMethods(container: HTMLElement): typeof noop { const patchHeadElementMethod = (headElement: HTMLHeadElement) => { headElement.appendChild = getOverwrittenAppendChildOrInsertBefore( document.head.appendChild, getSandboxConfig, 'head', ); headElement.insertBefore = getOverwrittenAppendChildOrInsertBefore( document.head.insertBefore, getSandboxConfig, 'head', ); headElement.removeChild = getNewRemoveChild(document.head.removeChild, getSandboxConfig); }; let containerHeadElement = getContainerHeadElement(container); if (!containerHeadElement) { // patch container head element after it is mounted const observer = new MutationObserver(() => { containerHeadElement = getContainerHeadElement(container); if (containerHeadElement) { patchHeadElementMethod(containerHeadElement); observer.disconnect(); } }); observer.observe(container, { subtree: true, childList: true }); } else { patchHeadElementMethod(containerHeadElement); } const containerBodyElement = container; containerBodyElement.appendChild = getOverwrittenAppendChildOrInsertBefore( document.body.appendChild, getSandboxConfig, 'body', ); containerBodyElement.insertBefore = getOverwrittenAppendChildOrInsertBefore( document.head.insertBefore, getSandboxConfig, 'body', ); containerBodyElement.removeChild = getNewRemoveChild(document.body.removeChild, getSandboxConfig); return () => { if (containerHeadElement) { // @ts-ignore delete containerHeadElement.appendChild; // @ts-ignore delete containerHeadElement.insertBefore; // @ts-ignore delete containerHeadElement.removeChild; } // @ts-ignore delete containerBodyElement.appendChild; // @ts-ignore delete containerBodyElement.insertBefore; // @ts-ignore delete containerBodyElement.removeChild; }; } function patchDOMPrototypeFns(): typeof noop { // patch MutationObserver.prototype.observe to avoid type error // https://github.com/umijs/qiankun/issues/2406 const nativeMutationObserverObserveFn = MutationObserver.prototype.observe; if (!patchCacheWeakMap.has(nativeMutationObserverObserveFn)) { const observe = function observe(this: MutationObserver, target: Node, options: MutationObserverInit) { const realTarget = target instanceof Document ? nativeDocument : target; return nativeMutationObserverObserveFn.call(this, realTarget, options); }; MutationObserver.prototype.observe = observe; patchCacheWeakMap.set(nativeMutationObserverObserveFn, observe); } // patch Node.prototype.compareDocumentPosition to avoid type error const prevCompareDocumentPosition = Node.prototype.compareDocumentPosition; if (!patchCacheWeakMap.has(prevCompareDocumentPosition)) { Node.prototype.compareDocumentPosition = function compareDocumentPosition(this: Node, node) { const realNode = node instanceof Document ? nativeDocument : node; return prevCompareDocumentPosition.call(this, realNode); }; patchCacheWeakMap.set(prevCompareDocumentPosition, Node.prototype.compareDocumentPosition); } // TODO https://github.com/umijs/qiankun/pull/2415 Not support yet as getCurrentRunningApp api is not reliable // patch parentNode getter to avoid document === html.parentNode // https://github.com/umijs/qiankun/issues/2408#issuecomment-1446229105 // const parentNodeDescriptor = Object.getOwnPropertyDescriptor(Node.prototype, 'parentNode'); // if (parentNodeDescriptor && !patchCacheWeakMap.has(parentNodeDescriptor)) { // const { get: parentNodeGetter, configurable } = parentNodeDescriptor; // if (parentNodeGetter && configurable) { // const patchedParentNodeDescriptor = { // ...parentNodeDescriptor, // get(this: Node) { // const parentNode = parentNodeGetter.call(this) as HTMLElement; // if (parentNode instanceof Document) { // const proxy = getCurrentRunningApp()?.window; // if (proxy) { // return proxy.document; // } // } // // return parentNode; // }, // }; // Object.defineProperty(Node.prototype, 'parentNode', patchedParentNodeDescriptor); // // patchCacheWeakMap.set(parentNodeDescriptor, patchedParentNodeDescriptor); // } // } return () => { MutationObserver.prototype.observe = nativeMutationObserverObserveFn; patchCacheWeakMap.delete(nativeMutationObserverObserveFn); Node.prototype.compareDocumentPosition = prevCompareDocumentPosition; patchCacheWeakMap.delete(prevCompareDocumentPosition); // if (parentNodeDescriptor) { // Object.defineProperty(Node.prototype, 'parentNode', parentNodeDescriptor); // patchCacheWeakMap.delete(parentNodeDescriptor); // } }; } // FIXME should not use global variable, should get it every time it is used, otherwise it may miss the runtime container or the business itself monkey patch logic const rawHeadInsertBefore = HTMLHeadElement.prototype.insertBefore; const rawHeadAppendChild = HTMLHeadElement.prototype.appendChild; export function patchStandardSandbox( appName: string, getContainer: () => HTMLElement, opts: { sandbox: Sandbox; mounting?: boolean; } & Pick, ): Free { const { sandbox, mounting = true, nodeTransformer, fetch } = opts; let sandboxConfig = sandboxConfigWeakMap.get(sandbox); if (!sandboxConfig) { sandboxConfig = { appName, sandbox, fetch, nodeTransformer, dynamicStyleSheetElements: [], dynamicExternalSyncScriptDeferredList: [], }; sandboxConfigWeakMap.set(sandbox, sandboxConfig); } // all dynamic style sheets are stored in proxy container const { dynamicStyleSheetElements } = sandboxConfig; const unpatchDocument = patchDocument(sandbox, getContainer); const unpatchDOMPrototype = patchDOMPrototypeFns(); if (!mounting) calcAppCount(appName, 'increase', 'bootstrapping'); if (mounting) calcAppCount(appName, 'increase', 'mounting'); return function free() { if (!mounting) calcAppCount(appName, 'decrease', 'bootstrapping'); if (mounting) calcAppCount(appName, 'decrease', 'mounting'); // release the overwritten document unpatchDocument(); // release the overwritten prototype after all the micro apps unmounted if (isAllAppsUnmounted()) { unpatchDOMPrototype(); } recordStyledComponentsCSSRules(dynamicStyleSheetElements as HTMLStyleElement[]); // As now the sub app content all wrapped with a special id container, // the dynamic style sheet could be removed automatically while unmounting return async function rebuild(container: HTMLElement) { const isElementExisted = (element: HTMLStyleElement | HTMLLinkElement) => { if (container.contains(element)) return true; if ('rel' in element && element.rel === 'stylesheet' && element.href) return !!container.querySelector(`link[rel=stylesheet][href="${element.href}"]`); return false; }; await Promise.all( rebuildCSSRules(dynamicStyleSheetElements, async (stylesheetElement) => { if (!isElementExisted(stylesheetElement)) { const mountDom = stylesheetElement[styleElementTargetSymbol] === 'head' ? (() => { const containerHeadElement = getContainerHeadElement(container); if (!containerHeadElement) { throw new QiankunError( `${appName} container ${qiankunHeadTagName} element not ready while rebuilding!`, ); } return containerHeadElement; })() : container; let styleElement = stylesheetElement; const deferred = new Deferred(); if ('rel' in styleElement && styleElement.rel === 'stylesheet' && styleElement.href) { // micro app rendering should wait unit the rebuilding link element is loaded, otherwise it may cause style blink // As one external link element will just trigger loaded event once, although we append it multiple times, we need to clone it before every appending styleElement = styleElement.cloneNode(true) as HTMLLinkElement; styleElement.onload = () => deferred.resolve(true); styleElement.onerror = () => deferred.resolve(false); } else { deferred.resolve(true); } const refNo = stylesheetElement[styleElementRefNodeNo]; if (typeof refNo === 'number' && refNo !== -1) { // the reference node may be dynamic script comment which is not rebuilt while remounting thus reference node no longer exists // in this case, we should append the style element to the end of mountDom const refNode = mountDom.childNodes[refNo]; rawHeadInsertBefore.call(mountDom, styleElement, refNode); } else { rawHeadAppendChild.call(mountDom, styleElement); } return deferred.promise; } return false; }), ); }; }; } ================================================ FILE: packages/sandbox/src/patchers/dynamicAppend/index.ts ================================================ /** * @author Kuitos * @since 2020-10-13 */ export { patchStandardSandbox } from './forStandardSandbox'; ================================================ FILE: packages/sandbox/src/patchers/dynamicAppend/types.ts ================================================ /** * @author Kuitos * @since 2023-05-04 */ import type { BaseLoaderOpts, NodeTransformer } from '@qiankunjs/shared'; import type { Deferred } from '@qiankunjs/shared'; import type { Sandbox } from '../../core/sandbox'; export type SandboxConfig = { appName: string; sandbox: Sandbox; dynamicStyleSheetElements: Array; dynamicExternalSyncScriptDeferredList: Array>; nodeTransformer: NodeTransformer; } & BaseLoaderOpts; ================================================ FILE: packages/sandbox/src/patchers/historyListener.ts ================================================ /** * @author Kuitos * @since 2019-04-11 */ import { isFunction, noop } from 'lodash'; declare global { interface Window { g_history?: { listen: (listener: typeof noop) => () => void; }; } } export default function patch() { // FIXME umi unmount feature request let rawHistoryListen = (_: unknown) => noop; const historyListeners: Array = []; const historyUnListens: Array = []; if (window.g_history && isFunction(window.g_history.listen)) { rawHistoryListen = window.g_history.listen.bind(window.g_history) as typeof rawHistoryListen; window.g_history.listen = (listener: typeof noop) => { historyListeners.push(listener); const unListen = rawHistoryListen(listener); historyUnListens.push(unListen); return () => { unListen(); historyUnListens.splice(historyUnListens.indexOf(unListen), 1); historyListeners.splice(historyListeners.indexOf(listener), 1); }; }; } return function free() { let rebuild = () => Promise.resolve(); /* 还存在余量 listener 表明未被卸载,存在两种情况 1. 应用在 unmount 时未正确卸载 listener 2. listener 是应用 mount 之前绑定的, 第二种情况下应用在下次 mount 之前需重新绑定该 listener */ if (historyListeners.length) { rebuild = async () => { // 必须使用 window.g_history.listen 的方式重新绑定 listener,从而能保证 rebuild 这部分也能被捕获到,否则在应用卸载后无法正确的移除这部分副作用 historyListeners.forEach((listener) => window.g_history?.listen(listener)); }; } // 卸载余下的 listener historyUnListens.forEach((unListen) => unListen()); // restore if (window.g_history && isFunction(window.g_history.listen)) { window.g_history.listen = rawHistoryListen; } return rebuild; }; } ================================================ FILE: packages/sandbox/src/patchers/index.ts ================================================ /** * @author Kuitos * @since 2019-04-11 */ import { SandboxType } from '../core/sandbox/types'; import { patchStandardSandbox } from './dynamicAppend'; import type { SandboxConfig } from './dynamicAppend/types'; import patchHistoryListener from './historyListener'; import patchInterval from './interval'; import type { Free } from './types'; import patchWindowListener from './windowListener'; export function patchAtBootstrapping( appName: string, getContainer: () => HTMLElement, opts: Pick, ): Free[] { const patchersInSandbox = { [SandboxType.Standard]: [() => patchStandardSandbox(appName, getContainer, { mounting: false, ...opts })], [SandboxType.Snapshot]: [], } as const; const { sandbox } = opts; return patchersInSandbox[sandbox.type].map((patch) => patch()); } export function patchAtMounting( appName: string, getContainer: () => HTMLElement, opts: Pick, ): Free[] { const { sandbox } = opts; const basePatchers = [ () => patchInterval(sandbox.globalThis), () => patchWindowListener(sandbox.globalThis), () => patchHistoryListener(), ]; const patchersInSandbox = { [SandboxType.Standard]: [ ...basePatchers, () => patchStandardSandbox(appName, getContainer, { mounting: true, ...opts }), ], [SandboxType.Snapshot]: basePatchers, }; return patchersInSandbox[sandbox.type].map((patch) => patch()); } ================================================ FILE: packages/sandbox/src/patchers/interval.ts ================================================ /** * @author Kuitos * @since 2019-04-11 */ const rawWindowInterval = window.setInterval; const rawWindowClearInterval = window.clearInterval; export default function patch(global: Window) { let intervals: number[] = []; global.clearInterval = (intervalId: number) => { intervals = intervals.filter((id) => id !== intervalId); return rawWindowClearInterval.call(window, intervalId); }; global.setInterval = (handler: CallableFunction, timeout?: number, ...args: unknown[]) => { const intervalId = rawWindowInterval(handler, timeout, ...args); intervals = [...intervals, intervalId]; return intervalId; }; return function free() { intervals.forEach((id) => global.clearInterval(id)); global.setInterval = rawWindowInterval; global.clearInterval = rawWindowClearInterval; return () => Promise.resolve(); }; } ================================================ FILE: packages/sandbox/src/patchers/types.ts ================================================ /** * @author Kuitos * @since 2023-05-04 */ export type Rebuild = (container: HTMLElement) => Promise; export type Free = () => Rebuild; export type Patch = () => Free; ================================================ FILE: packages/sandbox/src/patchers/windowListener.ts ================================================ /** * @author Kuitos * @since 2019-04-11 */ import { type Free } from './types'; const rawAddEventListener = window.addEventListener.bind(window); const rawRemoveEventListener = window.removeEventListener.bind(window); type ListenerMapObject = { listener: EventListenerOrEventListenerObject; options: AddEventListenerOptions; rawListener: EventListenerOrEventListenerObject; }; const DEFAULT_OPTIONS: AddEventListenerOptions = { capture: false, once: false, passive: false }; const normalizeOptions = (rawOptions?: boolean | AddEventListenerOptions | null): AddEventListenerOptions => { if (typeof rawOptions === 'object') { return rawOptions ?? DEFAULT_OPTIONS; } return { capture: !!rawOptions, once: false, passive: false }; }; const findListenerIndex = ( listeners: ListenerMapObject[], rawListener: EventListenerOrEventListenerObject, options: AddEventListenerOptions, ): number => listeners.findIndex((item) => item.rawListener === rawListener && item.options.capture === options.capture); const removeCacheListener = ( listenerMap: Map, type: string, rawListener: EventListenerOrEventListenerObject, rawOptions?: boolean | AddEventListenerOptions, ): ListenerMapObject => { const options = normalizeOptions(rawOptions); const cachedTypeListeners = listenerMap.get(type) || []; const findIndex = findListenerIndex(cachedTypeListeners, rawListener, options); if (findIndex > -1) { return cachedTypeListeners.splice(findIndex, 1)[0]; } return { listener: rawListener, rawListener, options }; }; const addCacheListener = ( listenerMap: Map, type: string, rawListener: EventListenerOrEventListenerObject, rawOptions?: boolean | AddEventListenerOptions, ): ListenerMapObject | undefined => { const options = normalizeOptions(rawOptions); const cachedTypeListeners = listenerMap.get(type) || []; const findIndex = findListenerIndex(cachedTypeListeners, rawListener, options); // avoid duplicated listener in the listener list if (findIndex > -1) return; let listener: EventListenerOrEventListenerObject = rawListener; if (options.once) { listener = (event: Event) => { (rawListener as EventListener)(event); removeCacheListener(listenerMap, type, rawListener, options); }; } const cacheListener = { listener, options, rawListener }; listenerMap.set(type, [...cachedTypeListeners, cacheListener]); return cacheListener; }; export default function patch(global: WindowProxy): Free { const listenerMap = new Map(); global.addEventListener = ( type: string, rawListener: EventListenerOrEventListenerObject, rawOptions?: boolean | AddEventListenerOptions, ) => { const addListener = addCacheListener(listenerMap, type, rawListener, rawOptions); if (!addListener) return; return rawAddEventListener(type, addListener.listener, addListener.options); }; global.removeEventListener = ( type: string, rawListener: EventListenerOrEventListenerObject, rawOptions?: boolean | AddEventListenerOptions, ) => { const { listener, options } = removeCacheListener(listenerMap, type, rawListener, rawOptions); return rawRemoveEventListener(type, listener, options); }; return function free() { listenerMap.forEach((listeners, type) => { listeners.forEach(({ rawListener, options }) => { global.removeEventListener(type, rawListener, options); }); }); listenerMap.clear(); global.addEventListener = rawAddEventListener; global.removeEventListener = rawRemoveEventListener; return () => Promise.resolve(); }; } ================================================ FILE: packages/sandbox/src/utils.ts ================================================ import { getOwnPropertyDescriptor, hasOwnProperty } from '@qiankunjs/shared'; const fnRegexCheckCacheMap = new WeakMap(); /** * 1. has prototype and prototype has defined a series of non-constructor attributes * 2. The function name starts with a capital letter * 3. class function * If one of them is satisfied, it can be regarded as a constructor function * @param fn */ export function isConstructable(fn: CallableFunction): fn is CallableFunction { // prototype methods might be changed while code running, so we need check it every time const hasPrototypeMethods = (fn.prototype)?.constructor === fn && Object.getOwnPropertyNames(fn.prototype).length > 1; if (hasPrototypeMethods) return true; const cachedResult = fnRegexCheckCacheMap.get(fn); if (typeof cachedResult !== 'undefined') { return cachedResult; } // fn.toString has a significant performance overhead, if hasPrototypeMethods check not passed, we will check the function string with regex const fnString = fn.toString(); const constructableFunctionRegex = /^function\b\s[A-Z].*/; const classRegex = /^class\b/; const constructable = constructableFunctionRegex.test(fnString) || classRegex.test(fnString); fnRegexCheckCacheMap.set(fn, constructable); return constructable; } const callableFnCacheMap = new WeakMap(); export function isCallable(fn: unknown): fn is CallableFunction { if (callableFnCacheMap.has(fn as CallableFunction)) { return true; } /* * We can not use typeof to confirm it is function as in some safari version * typeof document.all === 'undefined' // true * typeof document.all === 'function' // true */ const callable = typeof fn === 'function' && fn instanceof Function; if (callable) { callableFnCacheMap.set(fn, callable); } return callable; } const frozenPropertyCacheMap = new WeakMap>(); export function isPropertyFrozen(target: object, p?: PropertyKey): boolean { if (!p) { return false; } const targetPropertiesFromCache = frozenPropertyCacheMap.get(target) || {}; if (targetPropertiesFromCache[p]) { return targetPropertiesFromCache[p]; } const propertyDescriptor = getOwnPropertyDescriptor(target, p); const frozen = Boolean( propertyDescriptor && propertyDescriptor.configurable === false && (propertyDescriptor.writable === false || (propertyDescriptor.get && !propertyDescriptor.set)), ); targetPropertiesFromCache[p] = frozen; frozenPropertyCacheMap.set(target, targetPropertiesFromCache); return frozen; } const boundedMap = new WeakMap(); export function isBoundedFunction(fn: CallableFunction): fn is CallableFunction { const cachedValue = boundedMap.get(fn); if (typeof cachedValue !== 'undefined') { return cachedValue; } /* indexOf is faster than startsWith see https://jsperf.com/string-startswith/72 */ const bounded = fn.name.indexOf('bound ') === 0 && !hasOwnProperty(fn, 'prototype'); boundedMap.set(fn, bounded); return bounded; } ================================================ FILE: packages/shared/.fatherrc.js ================================================ export { default } from '../../.fatherrc.cjs'; ================================================ FILE: packages/shared/AGENTS.md ================================================ # @qiankunjs/shared Internal utilities for fetch, asset transpilation, and module resolution. ## STRUCTURE ``` shared/ ├── assets-transpilers/ # Script/link DOM node transformation │ ├── script.ts # Blob URL wrapping, sandbox execution │ └── link.ts # Preload optimization, URL rewriting ├── fetch-utils/ # Enhanced fetch wrappers │ ├── makeFetchCacheable.ts # LRU cache (50 entries) │ ├── makeFetchRetryable.ts # Automatic retries │ └── makeFetchThrowable.ts # Non-2xx throws error ├── module-resolver/ # Shared dependency resolution ├── reporter/ # QiankunError, logger ├── deferred-queue/ # Async task sequencing └── utils.ts # Deferred, resolveUrl, etc. ``` ## WHERE TO LOOK | Task | File | Notes | | ------------------ | ----------------------------------- | --------------------------------------------------- | | Script sandboxing | `assets-transpilers/script.ts` | Creates Blob URL with sandbox scope | | Link preload fix | `assets-transpilers/link.ts` | Changes `as="script"` to `as="fetch"` for sandbox | | Fetch with cache | `fetch-utils/makeFetchCacheable.ts` | Clones responses, prunes failed | | Dependency sharing | `module-resolver/index.ts` | Semver matching via ` will be resolved to http://localhost:8000/foo.js while read script.src const srcAttribute = script.getAttribute('src'); const { sandbox, scriptTranspiledDeferred } = opts; try { const { mode, result } = preTranspile( { src: srcAttribute || undefined, type: script.type, textContent: script.textContent, }, baseURI, opts, ); switch (mode) { case Mode.REMOTE_ASSETS_IN_SANDBOX: { const { fetch } = opts; const { src } = result; // We must remove script src to avoid self execution as we need to fetch the script content and transpile it script.removeAttribute('src'); script.dataset.src = src; const syncMode = !script.hasAttribute('async'); const priority: Priority = syncMode ? 'high' : 'low'; const credentials = getCredentials(script.crossOrigin); void fetch(src, { credentials, priority }) .then((res) => res.text()) .then(async (code) => { const { prevScriptTranspiledDeferred } = opts; // add preprocess code to dispatch a CustomEvent before the script is executed const beforeScriptExecuteEvent = 'q:bse'; const beforeExecutedListenerScript = `;(function(){var s=document.currentScript;var e=new CustomEvent('${beforeScriptExecuteEvent}',{detail:{s:s}});window.dispatchEvent(e);})();`; const codeFactory = beforeExecutedListenerScript + sandbox!.makeEvaluateFactory(code, src); if (syncMode) { // if it's a sync script and there is a previous sync script(mainly there are multiple defer scripts), we should wait it until loaded to consistent with the browser behavior if (prevScriptTranspiledDeferred && !prevScriptTranspiledDeferred.isSettled()) { await waitUntilSettled(prevScriptTranspiledDeferred.promise); } // HTMLScriptElement default fetchPriority is 'auto', we should set it to 'high' to make it execute earlier while it's not async script script.fetchPriority = 'high'; } // change the script src to the blob url to make it executed in the sandbox script.src = URL.createObjectURL(new Blob([codeFactory], { type: 'text/javascript' })); window.addEventListener(beforeScriptExecuteEvent, function listener(evt: CustomEventInit) { const { s } = evt.detail as { s: HTMLScriptElement }; if (s === script) { URL.revokeObjectURL(s.src); // change the script src to the original src while the script is executing // thus the script behavior can be more consistent with the native browser logic s.src = src; s.dataset.consumed = 'true'; delete s.dataset.src; window.removeEventListener(beforeScriptExecuteEvent, listener); } }); scriptTranspiledDeferred?.resolve(); }) .catch((e) => { scriptTranspiledDeferred?.reject(); throw e; }); return script; } case Mode.INLINE_CODE_IN_SANDBOX: { const { code } = result; script.textContent = sandbox!.makeEvaluateFactory(code); // mark the script have consumed script.dataset.consumed = 'true'; scriptTranspiledDeferred?.resolve(); return script; } case Mode.REUSED_DEP_IN_SANDBOX: case Mode.REUSED_DEP: { const { url, version, src } = result; script.dataset.src = src; script.dataset.version = version; const syncMode = !script.getAttribute('async'); // HTMLScriptElement default fetchPriority is 'auto', we should set it to 'high' to make it execute earlier while it's not async script if (syncMode) { script.fetchPriority = 'high'; } // When the script hits the dependency reuse logic, the current script is not executed, and an empty script is returned directly script.src = createReusingObjectUrl(src, url, 'text/javascript'); const onScriptComplete = ( prevListener: typeof HTMLScriptElement.prototype.onload | typeof HTMLScriptElement.prototype.onerror, event: Event, ) => { script.onload = script.onerror = null; script.src = src; script.dataset.consumed = 'true'; script.dataset.src = url; prevListener?.call(script, event); }; script.onload = onScriptComplete.bind(null, script.onload); script.onerror = onScriptComplete.bind(null, script.onerror) as typeof HTMLScriptElement.prototype.onerror; scriptTranspiledDeferred?.resolve(); return script; } case Mode.REMOTE_ASSETS: case Mode.NONE: default: { if (result?.src) { script.src = result.src; } scriptTranspiledDeferred?.resolve(); return script; } } } catch (e) { scriptTranspiledDeferred?.reject(e); throw e; } } ================================================ FILE: packages/shared/src/assets-transpilers/types.ts ================================================ /** * @author Kuitos * @since 2023-08-26 */ import type { BaseLoaderOpts } from '../common'; import type { MatchResult } from '../module-resolver'; import type { Deferred } from '../utils'; export type BaseTranspilerOpts = BaseLoaderOpts & { moduleResolver?: (url: string) => MatchResult | undefined; sandbox?: { makeEvaluateFactory(source: string, sourceURL?: string): string; }; }; export type AssetsTranspilerOpts = BaseTranspilerOpts; export type NodeTransformer = (node: T, opts: Omit) => T; export type ScriptTranspilerOpts = AssetsTranspilerOpts & ( | { prevScriptTranspiledDeferred: Deferred; scriptTranspiledDeferred: Deferred } | { prevScriptTranspiledDeferred?: undefined; scriptTranspiledDeferred?: undefined } ); export enum Mode { REMOTE_ASSETS_IN_SANDBOX = 'RAIS', REMOTE_ASSETS = 'RA', REUSED_DEP_IN_SANDBOX = 'RDIS', REUSED_DEP = 'RD', INLINE_CODE_IN_SANDBOX = 'ICIS', NONE = 'NONE', } ================================================ FILE: packages/shared/src/assets-transpilers/utils.ts ================================================ /** * @author Kuitos * @since 2023-10-09 */ import { memoize } from 'lodash'; export const createReusingObjectUrl = memoize( (src: string, url: string, type: 'text/javascript' | 'text/css'): string => { return URL.createObjectURL( new Blob([`/* ${src} is reusing the execution result of ${url} */`], { type, }), ); }, (src, url, type) => `${src}#${url}#${type}`, ); export const isValidJavaScriptType = (type?: string): boolean => { const handleTypes = [ 'text/javascript', 'module', 'application/javascript', 'text/ecmascript', 'application/ecmascript', ]; return !type || handleTypes.indexOf(type) !== -1; }; ================================================ FILE: packages/shared/src/common.ts ================================================ export type BaseLoaderOpts = { fetch: typeof window.fetch; }; ================================================ FILE: packages/shared/src/deferred-queue/index.ts ================================================ import { Deferred, waitUntilSettled } from '../utils'; export function prepareDeferredQueue(deferredQueue: Array>): { deferred: Deferred; prevDeferred?: Deferred; queue: () => void; } { const queueLength = deferredQueue.length; const prevDeferred = queueLength ? deferredQueue[deferredQueue.length - 1] : undefined; const deferred = new Deferred(); return { deferred, prevDeferred, queue: () => { deferredQueue.push(deferred); // clear the memory regardless the script loaded or failed void waitUntilSettled(deferred.promise).then(() => { const scriptIndex = deferredQueue.indexOf(deferred); deferredQueue.splice(scriptIndex, 1); }); }, }; } ================================================ FILE: packages/shared/src/fetch-utils/__tests__/makeFetchCacheable.test.ts ================================================ // @vitest-environment edge-runtime import { expect, it, vi } from 'vitest'; import { makeFetchCacheable } from '../makeFetchCacheable'; const slogan = 'Hello Qiankun 3.0'; it('should just call fetch once while multiple request invoked parallel', () => { const fetch = vi.fn(() => { return Promise.resolve(new Response(slogan, { status: 200, statusText: 'OK' })); }); const wrappedFetch = makeFetchCacheable(fetch); const url = 'https://success.qiankun.org'; wrappedFetch(url); wrappedFetch(url); wrappedFetch(url); expect(fetch).toHaveBeenCalledOnce(); }); it('should support read response body as a stream multi times', async () => { const fetch = vi.fn(() => { return Promise.resolve(new Response(slogan, { status: 200, statusText: 'OK' })); }); const wrappedFetch = makeFetchCacheable(fetch); const url = 'https://stream.qiankun.org'; const response1 = await wrappedFetch(url); const bodyStream1 = response1.body!; expect(bodyStream1.locked).toBe(false); const reader = bodyStream1.getReader(); const { done, value } = await reader.read(); expect(done).toBe(false); expect(value).toStrictEqual(new TextEncoder().encode('Hello Qiankun 3.0')); expect(bodyStream1.locked).toBe(true); const response2 = await wrappedFetch(url); const bodyStream2 = response2.body!; expect(bodyStream2.locked).toBe(false); }); it('should clear cache while respond error with invalid status code', async () => { const fetch = vi.fn(() => { return Promise.resolve(new Response(slogan, { status: 400 })); }); const wrappedFetch = makeFetchCacheable(fetch); const url = 'https://errorStatusCode.qiankun.org'; const response1 = await wrappedFetch(url); const result1 = await response1.text(); expect(result1).toBe(slogan); const response2 = await wrappedFetch(url); const result2 = await response2.text(); expect(result2).toBe(slogan); expect(fetch).toHaveBeenCalledTimes(2); }); it('should clear cache while respond error', async () => { const fetch = vi.fn(() => { return Promise.reject(new Error('error')); }); const wrappedFetch = makeFetchCacheable(fetch); const url = 'https://error.qiankun.org'; await expect(wrappedFetch(url)).rejects.toThrow('error'); await expect(wrappedFetch(url)).rejects.toThrow('error'); expect(fetch).toHaveBeenCalledTimes(2); }); ================================================ FILE: packages/shared/src/fetch-utils/__tests__/makeFetchRetryable.test.ts ================================================ // @vitest-environment edge-runtime import { expect, it, vi } from 'vitest'; import { makeFetchRetryable } from '../makeFetchRetryable'; const slogan = 'Hello Qiankun 3.0'; it('should retry automatically while fetch throw error', async () => { const retryTimes = 3; let count = 0; const fetch = vi.fn(() => { if (count < retryTimes) { count++; throw new Error('network error'); } return Promise.resolve(new Response(slogan, { status: 201 })); }); const wrappedFetch = makeFetchRetryable(fetch, retryTimes); const url = 'https://success.qiankun.org'; const res = await wrappedFetch(url); expect(res.status).toBe(201); expect(fetch).toHaveBeenCalledTimes(4); }); it('should work well while response status is 200', async () => { const fetch = vi.fn(() => { return Promise.resolve(new Response(slogan, { status: 200 })); }); const wrappedFetch = makeFetchRetryable(fetch); const url = 'https://success.qiankun.org'; const res = await wrappedFetch(url); expect(res.status).toBe(200); expect(fetch).toHaveBeenCalledTimes(1); }); ================================================ FILE: packages/shared/src/fetch-utils/__tests__/makeFetchThrowable.test.ts ================================================ /** * @author Kuitos * @since 2024-03-05 */ import { expect, it, vi } from 'vitest'; import { makeFetchThrowable } from '../makeFetchThrowable'; const slogan = 'Hello Qiankun 3.0'; it('should throw error while response status is not 200~400', async () => { expect.assertions(2); const fetch = vi.fn(() => { return Promise.resolve(new Response(slogan, { status: 400 })); }); const wrappedFetch = makeFetchThrowable(fetch); const url = 'https://success.qiankun.org'; try { await wrappedFetch(url); } catch (e) { const error = e as unknown as Error; expect(error.message).include('RESPONSE_ERROR_AS_STATUS_INVALID'); expect(error.message).include(url); } }); it('should prepend url to error message while fetch failed', async () => { expect.assertions(1); const fetch = vi.fn(() => { return Promise.reject(new Error('Failed to fetch')); }); const wrappedFetch = makeFetchThrowable(fetch); const url = 'https://fail.qiankun.org'; try { await wrappedFetch(url); } catch (e) { const error = e as unknown as Error; expect(error.message).toBe(`${url} Failed to fetch`); } }); it('should work well while response status is 200', async () => { const fetch = vi.fn(() => { return Promise.resolve(new Response(slogan, { status: 200 })); }); const wrappedFetch = makeFetchThrowable(fetch); const url = 'https://success.qiankun.org'; const res = await wrappedFetch(url); expect(res.status).toBe(200); }); it('should still throw error when error.message is readonly', async () => { expect.assertions(1); const readonlyError = new TypeError('Network error'); Object.defineProperty(readonlyError, 'message', { value: 'Network error', writable: false, configurable: false, }); const fetch = vi.fn(() => { return Promise.reject(readonlyError); }); const wrappedFetch = makeFetchThrowable(fetch); const url = 'https://readonly.qiankun.org'; try { await wrappedFetch(url); } catch (e) { // The error should still be thrown even if message is readonly expect(e).toBe(readonlyError); } }); it('should prepend url when url is a URL object', async () => { expect.assertions(1); const fetch = vi.fn(() => { return Promise.reject(new Error('Failed to fetch')); }); const wrappedFetch = makeFetchThrowable(fetch); const url = new URL('https://url-object.qiankun.org/path'); try { await wrappedFetch(url); } catch (e) { const error = e as unknown as Error; expect(error.message).toBe(`${url.href} Failed to fetch`); } }); it('should prepend url when url is a Request object', async () => { expect.assertions(1); const fetch = vi.fn(() => { return Promise.reject(new Error('Failed to fetch')); }); const wrappedFetch = makeFetchThrowable(fetch); const request = new Request('https://request-object.qiankun.org/path'); try { await wrappedFetch(request); } catch (e) { const error = e as unknown as Error; expect(error.message).toBe(`${request.url} Failed to fetch`); } }); ================================================ FILE: packages/shared/src/fetch-utils/makeFetchCacheable.ts ================================================ /** * @author Kuitos * @since 2023-11-06 * wrap fetch with lru cache */ import { once } from 'lodash'; import { LRUCache } from './miniLruCache'; import { type Fetch, isValidResponse } from './utils'; const getCacheKey = (input: Parameters[0]): string => { return typeof input === 'string' ? input : 'url' in input ? input.url : input.href; }; const getGlobalCache = once(() => { return new LRUCache>(50); }); export const makeFetchCacheable: (fetch: Fetch) => Fetch = (fetch) => { const lruCache = getGlobalCache(); const cachedFetch: Fetch = (input, init) => { const fetchInput = input; const cacheKey = getCacheKey(fetchInput); const wrapFetchPromise = async (promise: Promise): Promise => { try { const res = await promise; const { status } = res; if (!isValidResponse(status)) { lruCache.delete(cacheKey); } // must clone the response as one response body can only be read once as a stream return res.clone(); } catch (e) { lruCache.delete(cacheKey); throw e; } }; const cachedFetchPromise = lruCache.get(cacheKey); if (cachedFetchPromise) { return wrapFetchPromise(cachedFetchPromise); } const fetchPromise = fetch(fetchInput, init); lruCache.set(cacheKey, fetchPromise); return wrapFetchPromise(fetchPromise); }; return cachedFetch; }; ================================================ FILE: packages/shared/src/fetch-utils/makeFetchRetryable.ts ================================================ /** * @author Kuitos * @since 2024-03-05 */ import { type Fetch } from './utils'; export const makeFetchRetryable: (fetch: Fetch, retryTimes?: number) => Fetch = (fetch, retryTimes = 1) => { let retryCount = 0; const fetchWithRetryable: Fetch = async (input, init) => { try { return await fetch(input, init); } catch (e) { if (retryCount < retryTimes) { retryCount++; if (process.env.NODE_ENV === 'development') { console.debug( `[qiankun] fetch retrying --> url: ${ typeof input === 'string' ? input : 'url' in input ? input.url : input.href } , time: ${retryCount}`, ); } return await fetchWithRetryable(input, init); } throw e; } }; return fetchWithRetryable; }; ================================================ FILE: packages/shared/src/fetch-utils/makeFetchThrowable.ts ================================================ /** * @author Kuitos * @since 2024-03-05 * wrap fetch to throw error when response status is not 200~400 */ import { type Fetch, isValidResponse } from './utils'; export const makeFetchThrowable: (fetch: Fetch) => Fetch = (fetch) => { return async (url, init) => { const urlString = typeof url === 'string' ? url : 'url' in url ? url.url : url.href; let res: Response; try { res = await fetch(url, init); } catch (e) { // The error message of fetch failed is usually "Failed to fetch" // We need to prepend the url to the error message to make it easier to debug // e.g. "https://example.com/script.js Failed to fetch" try { if (e instanceof Error && !e.message.includes(urlString)) { e.message = `${urlString} ${e.message}`; } } catch (_) { // e.message may be readonly } throw e; } if (!isValidResponse(res.status)) { throw new Error(`${urlString} [RESPONSE_ERROR_AS_STATUS_INVALID] ${res.status} ${res.statusText}`); } return res; }; }; ================================================ FILE: packages/shared/src/fetch-utils/miniLruCache.ts ================================================ export class LRUCache { private readonly capacity: number; private cache: Map; constructor(capacity: number) { this.capacity = capacity; this.cache = new Map(); } get(key: K): V | undefined { const value = this.cache.get(key); if (value !== undefined) { this.cache.delete(key); this.cache.set(key, value); } return value; } set(key: K, value: V): void { if (this.cache.has(key)) { this.cache.delete(key); } this.cache.set(key, value); if (this.cache.size > this.capacity) { const firstKey = this.cache.keys().next().value as K; this.cache.delete(firstKey); } } delete(key: K): void { this.cache.delete(key); } } ================================================ FILE: packages/shared/src/fetch-utils/utils.ts ================================================ export type Fetch = typeof window.fetch; export const isValidResponse = (status: number): boolean => { return status >= 200 && status < 400; }; ================================================ FILE: packages/shared/src/index.ts ================================================ /** * @author Kuitos * @since 2023-05-06 */ export * from './assets-transpilers'; export * from './utils'; export * from './module-resolver'; export * from './common'; export * from './reporter'; export * from './fetch-utils/makeFetchCacheable'; export * from './fetch-utils/makeFetchRetryable'; export * from './fetch-utils/makeFetchThrowable'; export * from './deferred-queue'; ================================================ FILE: packages/shared/src/module-resolver/__tests__/index.test.ts ================================================ import { describe, expect, it } from 'vitest'; import { moduleResolver } from '../index'; describe('default module resolver', () => { const mainAppContainer = document.createElement('div'); mainAppContainer.innerHTML = ` `; it('should works well', () => { const microAppContainer = document.createElement('div'); microAppContainer.innerHTML = ` `; const result1 = moduleResolver('https://unpkg.com/4.0.1/antd', microAppContainer, mainAppContainer); expect(result1).toBeUndefined(); const result2 = moduleResolver('https://unpkg.com/4.0.1/lodash.js', microAppContainer, mainAppContainer); expect(result2).toStrictEqual({ name: 'lodash', version: '4.0.2', url: 'https://unpkg.com/4.0.2/lodash.js', }); const result3 = moduleResolver('https://unpkg.com/2.0.1/moment.js', microAppContainer, mainAppContainer); expect(result3).toStrictEqual({ name: 'moment', version: '2.1.1', url: 'https://unpkg.com/2.1.1/moment.js', }); const result4 = moduleResolver('https://unpkg.com/4.0.2/antd', microAppContainer, mainAppContainer); expect(result4).toBeUndefined(); }); }); ================================================ FILE: packages/shared/src/module-resolver/__tests__/satisfies.test.ts ================================================ import { describe, expect, it } from 'vitest'; import { satisfies } from '../satisfies'; describe('satisfies wrapper', () => { describe('full wildcards', () => { it('should match * wildcard', () => { expect(satisfies('1.2.3', '*')).toBe(true); expect(satisfies('0.0.1', '*')).toBe(true); }); it('should match x wildcard', () => { expect(satisfies('1.2.3', 'x')).toBe(true); expect(satisfies('1.2.3', 'X')).toBe(true); }); it('should not match prerelease by default', () => { expect(satisfies('1.0.0-alpha', '*')).toBe(false); expect(satisfies('1.0.0-alpha', 'x')).toBe(false); }); }); describe('short wildcards (N.x format)', () => { it('should match major.x format', () => { expect(satisfies('1.0.0', '1.x')).toBe(true); expect(satisfies('1.2.3', '1.x')).toBe(true); expect(satisfies('1.99.99', '1.x')).toBe(true); }); it('should not match different major version', () => { expect(satisfies('0.9.9', '1.x')).toBe(false); expect(satisfies('2.0.0', '1.x')).toBe(false); }); it('should handle * and X variants', () => { expect(satisfies('1.2.3', '1.*')).toBe(true); expect(satisfies('1.2.3', '1.X')).toBe(true); expect(satisfies('2.0.0', '1.*')).toBe(false); }); it('should not match prerelease by default', () => { expect(satisfies('1.0.0-alpha', '1.x')).toBe(false); expect(satisfies('1.0.0-alpha', '1.*')).toBe(false); }); }); describe('standard ranges (passthrough)', () => { it('should handle caret ranges', () => { expect(satisfies('1.2.3', '^1.0.0')).toBe(true); expect(satisfies('2.0.0', '^1.0.0')).toBe(false); }); it('should handle tilde ranges', () => { expect(satisfies('1.2.5', '~1.2.0')).toBe(true); expect(satisfies('1.3.0', '~1.2.0')).toBe(false); }); it('should handle full wildcards with segments', () => { expect(satisfies('1.2.3', '1.2.x')).toBe(true); expect(satisfies('1.2.3', '1.x.x')).toBe(true); expect(satisfies('1.2.3', '1.2.*')).toBe(true); }); }); describe('prerelease versions (semver-compatible)', () => { it('should not match prerelease when range has no prerelease', () => { expect(satisfies('1.0.0-alpha', '^1.0.0')).toBe(false); expect(satisfies('1.0.1-beta.1', '^1.0.0')).toBe(false); expect(satisfies('1.1.0-rc.1', '~1.0.0')).toBe(false); }); it('should match prerelease when range includes prerelease', () => { expect(satisfies('1.0.0-alpha', '1.0.0-alpha')).toBe(true); expect(satisfies('1.0.0-alpha', '>=1.0.0-alpha')).toBe(true); }); it('should handle numeric prerelease identifiers', () => { expect(satisfies('1.0.0-1', '>=1.0.0-1')).toBe(true); expect(satisfies('1.0.0-1', '^1.0.0')).toBe(false); }); it('should handle normal versions correctly', () => { expect(satisfies('1.0.0', '^1.0.0')).toBe(true); expect(satisfies('1.2.3', '^1.0.0')).toBe(true); }); }); }); ================================================ FILE: packages/shared/src/module-resolver/index.ts ================================================ import { satisfies } from './satisfies'; import type { MatchResult } from './types'; declare global { interface HTMLElement { __matched_deps__?: string[]; } } type Dependency = { url: string; version: string; range: string; peerDeps?: string[]; }; type NormalizedDependency = { name: string; } & Dependency; type DependencyMap = { dependencies: Record; }; export type { MatchResult } from './types'; export function moduleResolver( url: string, microAppContainer: HTMLElement, mainAppContainer: HTMLElement, ): MatchResult | undefined { const dependencyMapSelector = 'script[type=dependencymap]'; const microAppDependenciesString = microAppContainer.querySelector(dependencyMapSelector)?.innerHTML; if (microAppDependenciesString) { const { dependencies } = JSON.parse(microAppDependenciesString) as DependencyMap; const normalizedDependencies = normalizeDependencies(dependencies); const microAppDependency = normalizedDependencies.find((v) => v.url === url); if (microAppDependency) { const mainAppDependencyMapString = mainAppContainer.querySelector(dependencyMapSelector)?.innerHTML; if (mainAppDependencyMapString) { const mainAppDependencyMap = JSON.parse(mainAppDependencyMapString) as DependencyMap; const matchedDeps = (microAppContainer.__matched_deps__ ??= []); const matchedDep = findDependency( microAppDependency, normalizeDependencies(mainAppDependencyMap.dependencies), matchedDeps, ); if (matchedDep) { matchedDeps.push(matchedDep.name); return matchedDep; } } } } return undefined; } function findDependency( dependency: NormalizedDependency, mainAppDependencies: NormalizedDependency[], matchedDependencies: string[], ): MatchResult | undefined { const matched = mainAppDependencies.find( (mainAppDependency) => mainAppDependency.name === dependency.name && satisfies(mainAppDependency.version, dependency.range) && // peer dependencies must be cached before (dependency.peerDeps || []).every((peerDep) => matchedDependencies.indexOf(peerDep) !== -1), ); if (matched) { return { name: matched.name, version: matched.version, url: matched.url, }; } return undefined; } function normalizeDependencies(dependencies: DependencyMap['dependencies']): NormalizedDependency[] { return Object.keys(dependencies).map((name) => ({ name, ...dependencies[name], })); } ================================================ FILE: packages/shared/src/module-resolver/satisfies.ts ================================================ import { satisfies as compareSatisfies } from 'compare-versions'; /** * Semver-compatible satisfies check using compare-versions. * Handles edge cases that compare-versions doesn't support: * - Full wildcards (*, x, X) * - Short wildcards (1.x, 1.*) * - Prerelease version behavior */ export function satisfies(version: string, range: string): boolean { const trimmed = range.trim(); const hasPrerelease = version.includes('-'); const rangeHasPrerelease = /-[0-9A-Za-z]/.test(trimmed); // Full wildcards: *, x, X if (trimmed === '*' || trimmed.toLowerCase() === 'x') { return !hasPrerelease; } // Short wildcards: N.x, N.*, N.X (without third segment) const shortMatch = trimmed.match(/^(\d+)\.[xX*]$/); if (shortMatch) { const major = parseInt(shortMatch[1], 10); if (hasPrerelease && !rangeHasPrerelease) { return false; } return compareSatisfies(version, `>=${major}.0.0`) && compareSatisfies(version, `<${major + 1}.0.0`); } // Prerelease compatibility: if version is prerelease but range doesn't // include prerelease identifier, return false (matching semver behavior) if (hasPrerelease && !rangeHasPrerelease) { return false; } return compareSatisfies(version, range); } ================================================ FILE: packages/shared/src/module-resolver/types.ts ================================================ /** * @author Kuitos * @since 2023-08-26 */ export type MatchResult = { name: string; version: string; url: string; }; ================================================ FILE: packages/shared/src/reporter/QiankunError.ts ================================================ export class QiankunError extends Error { constructor(message: string) { super(`[qiankun]: ${message}`); } } ================================================ FILE: packages/shared/src/reporter/index.ts ================================================ export { QiankunError } from './QiankunError'; export * from './logger'; ================================================ FILE: packages/shared/src/reporter/logger.ts ================================================ export function warn(msg: string, ...args: unknown[]) { console.warn(`[qiankun]: ${msg}`, ...args); } ================================================ FILE: packages/shared/src/typings.d.ts ================================================ type Priority = 'high' | 'low' | 'auto'; interface HTMLScriptElement { fetchPriority?: Priority; } interface RequestInit { priority?: Priority; } ================================================ FILE: packages/shared/src/utils.ts ================================================ /** * @author Kuitos * @since 2023-04-26 */ // eslint-disable-next-line @typescript-eslint/unbound-method export const { create, defineProperty, getOwnPropertyDescriptor, getOwnPropertyNames, freeze, keys } = Object; export const hasOwnProperty = (caller: unknown, p: PropertyKey) => Object.prototype.hasOwnProperty.call(caller, p); export class Deferred { promise: Promise; #status: 'pending' | 'fulfilled' | 'rejected' = 'pending'; resolve!: (value: T | PromiseLike) => void; reject!: (reason?: unknown) => void; constructor() { this.promise = new Promise((resolve, reject) => { this.resolve = (value) => { this.#status = 'fulfilled'; resolve(value); }; this.reject = (reason) => { this.#status = 'rejected'; reject(reason); }; }); } isSettled(): boolean { return this.#status !== 'pending'; } } export async function waitUntilSettled(promise: Promise): Promise { try { await promise; } catch (e) { if (process.env.NODE_ENV === 'development') { console.warn('waitUntilSettled error', e); } } } export function resolveUrl(uri: string, baseURI: string): string { // should not handle an entire url especially protocol-relative url, e.g. //example.com/foo.js // otherwise it may occur that the user previously stored the script src as //example.com/foo.js, // resulting in a conversion to http://example.com/foo.js which does not match the original stored key. if (uri.startsWith('//') || uri.startsWith('http://') || uri.startsWith('https://')) { return uri; } const publicPath = new URL(baseURI, window.location.href); const entireUrl = new URL(uri, publicPath); return entireUrl.toString(); } /** * Check if the running environment support qiankun 3.0 * */ export function isRuntimeCompatible(): boolean { return ( // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition typeof Proxy === 'function' && typeof TransformStream === 'function' && typeof URL?.createObjectURL === 'function' ); } ================================================ FILE: packages/ui-bindings/react/.eslintrc.cjs ================================================ module.exports = { extends: ['plugin:react/recommended', require.resolve('../../../.eslintrc.cjs')], parserOptions: { tsconfigRootDir: __dirname, project: ['./tsconfig.json'], }, rules: { 'react/display-name': 'off', 'react/prop-types': 'off', }, }; ================================================ FILE: packages/ui-bindings/react/.fatherrc.js ================================================ export { default } from '../../../.fatherrc.cjs'; ================================================ FILE: packages/ui-bindings/react/CHANGELOG.md ================================================ # @qiankunjs/react ## 0.0.1-rc.14 ### Patch Changes - ac068ae: fix: remove unused umd bundle configuration - Updated dependencies [ac068ae] - @qiankunjs/ui-shared@0.0.1-rc.1 ## 0.0.1-rc.13 ### Patch Changes - 9ec15954: feat: refactor the code of microapp - Updated dependencies [9ec15954] - @qiankunjs/ui-shared@0.0.1-rc.0 ## 0.0.1-rc.12 ### Patch Changes - Updated dependencies [a8809ecf] - qiankun@3.0.0-rc.15 ## 0.0.1-rc.11 ### Patch Changes - qiankun@3.0.0-rc.14 ## 0.0.1-rc.10 ### Patch Changes - Updated dependencies [f2af2e36] - qiankun@3.0.0-rc.13 ## 0.0.1-rc.9 ### Patch Changes - Updated dependencies [7cc06bd4] - Updated dependencies [312abbc7] - qiankun@3.0.0-rc.12 ## 0.0.1-rc.8 ### Patch Changes - 43bf37a5: fix(sandbox): should get container from getter function in every accessing - Updated dependencies [43bf37a5] - qiankun@3.0.0-rc.11 ## 0.0.1-rc.7 ### Patch Changes - fix: use qiankun 2.x or 3.x as peerDependencies ## 0.0.1-rc.6 ### Patch Changes - Updated dependencies [2aca545c] - qiankun@3.0.0-rc.10 ## 0.0.1-rc.5 ### Patch Changes - Updated dependencies [fe68e878] - qiankun@3.0.0-rc.9 ## 0.0.1-rc.4 ### Patch Changes - Updated dependencies [1d9adcaa] - qiankun@3.0.0-rc.8 ## 0.0.1-rc.3 ### Patch Changes - qiankun@3.0.0-rc.7 ## 0.0.1-rc.2 ### Patch Changes - e7d788ef: feat: not rebind non-native global properties - Updated dependencies [317961eb] - Updated dependencies [e448082c] - Updated dependencies [76b6bff7] - qiankun@3.0.0-rc.6 ================================================ FILE: packages/ui-bindings/react/README.md ================================================ # qiankun ui binding for react ## Usage ```bash npm i @qiankunjs/react ``` ## MicroApp component Load (or unload) child apps directly through the `` component, which provides loading and error catching-related capabilities: ```tsx import { MicroApp } from '@qiankunjs/react'; export default function Page() { return ; } ``` When the sub-app loading animation or error capture capability is enabled, the sub-app accepts an additional style class `wrapperClassName`, and the rendered result is as follows: ```tsx
``` ### Load animation When this capability is enabled, loading animations are automatically displayed when child apps are loading. When the sub-application is mounted and changes to the `MOUNTED` state, the loading status ends and the sub-application content is displayed. Just pass `autoSetLoading` as a parameter: ```tsx import { MicroApp } from '@qiankunjs/react'; export default function Page() { return ; } ``` #### Custom loading animation If you want to override the default loading animation style, you can set a custom loading component `loader` as the loading animation for the child app. ```tsx import CustomLoader from '@/components/CustomLoader'; import { MicroApp } from '@qiankunjs/react'; export default function Page() { return ( } /> ); } ``` where `loading` is the `boolean` type parameter, `true` indicates that the loading state is still being loaded, and `false` indicates that the loading state has ended. ### Error catching When this capability is enabled, an error message is automatically displayed when a child app loads unexpectedly. You can pass the `autoCaptureError` property to the sub-app to enable sub-app error capture capabilities: ```tsx import { MicroApp } from '@qiankunjs/react'; export default function Page() { return ; } ``` #### Custom error capture If you want to override the default error capture component style, you can set a custom component `errorBoundary` as the error capture component for the child app: ```tsx import CustomErrorBoundary from '@/components/CustomErrorBoundary'; import { MicroApp } from '@qiankunjs/react'; export default function Page() { return ( } /> ); } ``` ### Component Props | Name | Required | Description | Type | Default | | --- | --- | --- | --- | --- | | `name` | yes | The name of the microapp | `string` | | `entry` | yes | The HTML address of the microapp | `string` | | `autoSetLoading` | no | Automatically set the loading state of your microapp | `boolean` | `false` | | `loader` | no | Custom microapps load state components | `(loading) => React.ReactNode` | `undefined` | | `autoCaptureError` | no | Automatically set up error capture for microapps | `boolean` | `false` | | `errorBoundary` | no | Custom microapp error capture component | `(error: any) => React.ReactNode` | `undefined` | | `className` | no | The style class for the microapp | `string` | `undefined` | | `wrapperClassName` | no | Wrap the microapp loading component, error capture component, and microapp's style classes, and are only valid when the load component or error capture component is enabled | `string` | `undefined` | ## Get the child app load status The loading status includes: "NOT_LOADED" | "LOADING_SOURCE_CODE" | "NOT_BOOTSTRAPPED" | "BOOTSTRAPPING" | "NOT_MOUNTED" | "MOUNTING" | "MOUNTED" | "UPDATING" | "UNMOUNTING" | "UNLOADING" | ```tsx import { useRef } from 'react'; import { MicroApp } from '@qiankunjs/react'; export default function Page() { const microAppRef = useRef(); useEffect(() => { // Get the child app load status console.log(microAppRef.current?.getStatus()); }, []); return ; } ``` ================================================ FILE: packages/ui-bindings/react/README.zh-CN.md ================================================ # qiankun ui binding for react ## 安装 ```bash npm i @qiankunjs/react ``` ## MicroApp 组件 直接通过 `` 组件加载(或卸载)子应用,该组件提供了 loading 以及错误捕获相关的能力: ```tsx import { MicroApp } from '@qiankunjs/react'; export default function Page() { return ; } ``` 当启用子应用加载动画或错误捕获能力时,子应用接受一个额外的样式类 `wrapperClassName`,渲染的结果如下所示: ```tsx
``` ### 加载动画 启用此能力后,当子应用正在加载时,会自动显示加载动画。当子应用挂载完成变成 `MOUNTED` 状态时,加载状态结束,显示子应用内容。 直接将 `autoSetLoading` 作为参数传入即可: ```tsx import { MicroApp } from '@qiankunjs/react'; export default function Page() { return ; } ``` #### 自定义加载动画 如果您希望覆盖默认的加载动画样式时,可以设置一个自定义的加载组件 `loader` 作为子应用的加载动画。 ```tsx import CustomLoader from '@/components/CustomLoader'; import { MicroApp } from '@qiankunjs/react'; export default function Page() { return ( } /> ); } ``` 其中,`loading` 为 `boolean` 类型参数,为 `true` 时表示仍在加载状态,为 `false` 时表示加载状态已结束。 ### 错误捕获 启用此能力后,当子应用加载出现异常时,会自动显示错误信息。可以向子应用传入 `autoCaptureError` 属性以开启子应用错误捕获能力: ```tsx import { MicroApp } from '@qiankunjs/react'; export default function Page() { return ; } ``` #### 自定义错误捕获 如果您希望覆盖默认的错误捕获组件样式时,可以设置一个自定义的组件 `errorBoundary` 作为子应用的错误捕获组件: ```tsx import CustomErrorBoundary from '@/components/CustomErrorBoundary'; import { MicroApp } from '@qiankunjs/react'; export default function Page() { return ( } /> ); } ``` ### 组件属性 | 属性 | 必填 | 说明 | 类型 | 默认值 | | --- | --- | --- | --- | --- | | `name` | 是 | 微应用的名称 | `string` | | `entry` | 是 | 微应用的 HTML 地址 | `string` | | `autoSetLoading` | 否 | 自动设置微应用的加载状态 | `boolean` | `false` | | `loader` | 否 | 自定义的微应用加载状态组件 | `(loading) => React.ReactNode` | `undefined` | | `autoCaptureError` | 否 | 自动设置微应用的错误捕获 | `boolean` | `false` | | `errorBoundary` | 否 | 自定义的微应用错误捕获组件 | `(error: any) => React.ReactNode` | `undefined` | | `className` | 否 | 微应用的样式类 | `string` | `undefined` | | `wrapperClassName` | 否 | 包裹微应用加载组件、错误捕获组件和微应用的样式类,仅在启用加载组件或错误捕获组件时有效 | `string` | `undefined` | ## 获取子应用加载状态 加载状态包括:"NOT_LOADED" | "LOADING_SOURCE_CODE" | "NOT_BOOTSTRAPPED" | "BOOTSTRAPPING" | "NOT_MOUNTED" | "MOUNTING" | "MOUNTED" | "UPDATING" | "UNMOUNTING" | "UNLOADING" | ```tsx import { useRef } from 'react'; import { MicroApp } from '@qiankunjs/react'; export default function Page() { const microAppRef = useRef(); useEffect(() => { // 获取子应用加载状态 console.log(microAppRef.current?.getStatus()); }, []); return ; } ``` ================================================ FILE: packages/ui-bindings/react/package.json ================================================ { "name": "@qiankunjs/react", "version": "0.0.1-rc.14", "description": "react binding for qiankun", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", "types": "./dist/esm/index.d.ts", "sideEffects": false, "scripts": { "build": "father build", "dev": "father dev" }, "author": "Bravepg", "license": "MIT", "dependencies": { "@qiankunjs/ui-shared": "workspace:^", "lodash": "^4.17.11" }, "devDependencies": { "@types/react": "^18.0.0", "eslint-plugin-react": "^7.33.2", "react": "^18.2.0", "react-dom": "^18.2.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" }, "publishConfig": { "registry": "https://registry.npmjs.org/" }, "files": [ "dist" ], "repository": "git@github.com:umijs/qiankun.git" } ================================================ FILE: packages/ui-bindings/react/src/ErrorBoundary.tsx ================================================ import React from 'react'; const ErrorBoundary: React.FC<{ error: Error }> = ({ error }) =>
{error.message}
; export default ErrorBoundary; ================================================ FILE: packages/ui-bindings/react/src/MicroApp.tsx ================================================ import { isEqual, noop } from 'lodash'; import { type SharedProps, type MicroAppType, type SharedSlots, unmountMicroApp, mountMicroApp, updateMicroApp, omitSharedProps, } from '@qiankunjs/ui-shared'; import React, { type Ref, forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react'; import ErrorBoundary from './ErrorBoundary'; import MicroAppLoader from './MicroAppLoader'; export type Props = SharedProps & SharedSlots & Record; function useDeepCompare(value: T): T { const ref = useRef(value); if (!isEqual(value, ref.current)) { ref.current = value; } return ref.current; } export const MicroApp = forwardRef((componentProps: Props, componentRef: Ref) => { const { name, autoSetLoading, autoCaptureError, wrapperClassName, className, loader, errorBoundary } = componentProps; const [loading, setLoading] = useState(true); const [error, setError] = useState(); const containerRef = useRef(null); const microAppRef = useRef(); // 未配置自定义 errorBoundary 且开启了 autoCaptureError 场景下,使用插件默认的 errorBoundary,否则使用自定义 errorBoundary const microAppErrorBoundary = errorBoundary || (autoCaptureError ? (e) => : null); // 配置了 errorBoundary 才改 error 状态,否则直接往上抛异常 const setComponentError = (e: Error | undefined) => { if (microAppErrorBoundary) { setError(e); // error log 出来,不要吞 if (e) { console.error(e); } } else if (e) { throw e; } }; const onError = (e: Error) => { setComponentError(e); setLoading(false); }; useImperativeHandle(componentRef, () => microAppRef.current); useEffect(() => { mountMicroApp({ prevMicroApp: microAppRef.current, container: containerRef.current!, componentProps, setLoading, setError: setComponentError, }) .then((app) => { microAppRef.current = app; }) .catch((e: Error) => { onError(e); }); return () => { const microApp = microAppRef.current; if (microApp && microApp.getStatus() === 'MOUNTED') { // 微应用 unmount 是异步的,中间的流转状态不能确定,所有需要一个标志位来确保 unmount 开始之后不会再触发 update microApp._unmounting = true; unmountMicroApp(microApp).catch((e: Error) => { onError(e); }); } }; }, [name]); useEffect(() => { updateMicroApp({ name, microApp: microAppRef.current, microAppProps: omitSharedProps(componentProps), setLoading, }); return noop; }, [useDeepCompare(omitSharedProps(componentProps))]); // 未配置自定义 loader 且开启了 autoSetLoading 场景下,使用插件默认的 loader,否则使用自定义 loader const microAppLoader = loader || (autoSetLoading ? (loadingStatus) => : null); const microAppWrapperClassName = wrapperClassName ? `${wrapperClassName} qiankun-micro-app-wrapper` : 'qiankun-micro-app-wrapper'; const microAppClassName = className ? `${className} qiankun-micro-app-container` : 'qiankun-micro-app-container'; return microAppLoader || microAppErrorBoundary ? (
{microAppLoader && microAppLoader(loading)} {microAppErrorBoundary && error && microAppErrorBoundary(error)}
) : (
); }); ================================================ FILE: packages/ui-bindings/react/src/MicroAppLoader.tsx ================================================ import React from 'react'; const MicroAppLoader: React.FC<{ loading: boolean }> = ({ loading }) => { if (loading) { return <>loading...; } return null; }; export default MicroAppLoader; ================================================ FILE: packages/ui-bindings/react/src/index.ts ================================================ export * from './MicroApp'; ================================================ FILE: packages/ui-bindings/react/tsconfig.json ================================================ { "extends": "../../../tsconfig.json", "compilerOptions": { "jsx": "react", "paths": { "qiankun": ["packages/qiankun/src"] } } } ================================================ FILE: packages/ui-bindings/shared/.fatherrc.js ================================================ export { default } from '../../../.fatherrc.cjs'; ================================================ FILE: packages/ui-bindings/shared/CHANGELOG.md ================================================ # @qiankunjs/ui-shared ## 0.0.1-rc.1 ### Patch Changes - ac068ae: fix: remove unused umd bundle configuration ## 0.0.1-rc.0 ### Patch Changes - 9ec15954: feat: refactor the code of microapp ================================================ FILE: packages/ui-bindings/shared/package.json ================================================ { "name": "@qiankunjs/ui-shared", "version": "0.0.1-rc.1", "description": "ui binding shared for qiankun", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", "types": "./src/index.ts", "sideEffects": false, "scripts": { "build": "father build", "dev": "father dev" }, "author": "linghaoSu", "license": "MIT", "dependencies": { "lodash": "^4.17.11" }, "devDependencies": { "qiankun": "workspace:^" }, "peerDependencies": { "qiankun": "^3.0.0-rc.15" }, "publishConfig": { "registry": "https://registry.npmjs.org/" }, "files": [ "dist" ], "repository": "git@github.com:umijs/qiankun.git" } ================================================ FILE: packages/ui-bindings/shared/src/index.ts ================================================ import type { AppConfiguration, MicroApp as MicroAppTypeDefinition, LifeCycles } from 'qiankun'; import { loadMicroApp } from 'qiankun'; import { mergeWith, concat, omit } from 'lodash'; import type { LifeCycleFn } from 'qiankun'; export type MicroAppType = { _unmounting?: boolean; _updatingPromise?: Promise; _updatingTimestamp?: number; } & MicroAppTypeDefinition; export type SharedProps = { name: string; entry: string; settings?: AppConfiguration; lifeCycles?: LifeCycles>; autoSetLoading?: boolean; autoCaptureError?: boolean; // 仅开启 loader 时需要 wrapperClassName?: string; className?: string; }; export type SharedSlots = { loader?: (loading: boolean) => T; errorBoundary?: (error: Error) => T; }; export const omitSharedProps = (props: Partial) => { return omit(props, ['wrapperClassName', 'className', 'lifeCycles', 'settings', 'entry', 'name']); }; export async function mountMicroApp({ prevMicroApp, container, componentProps, setLoading, setError, }: { prevMicroApp?: MicroAppType; container: HTMLDivElement; componentProps: SharedProps; setLoading?: (loading: boolean) => void; setError?: (error?: Error) => void; }) { if (!componentProps.name || !componentProps.entry) { console.error('the name and entry of MicroApp is needed'); return; } // 等待 prevMicroApp 卸载完成 if (prevMicroApp?._unmounting) { await prevMicroApp.unmountPromise; } setError?.(undefined); setLoading?.(true); const microAppProps = omitSharedProps(componentProps); const configuration = { globalContext: window, ...(componentProps.settings || {}), }; const microApp = loadMicroApp( { name: componentProps.name, entry: componentProps.entry, container, props: microAppProps, }, configuration, mergeWith( {}, componentProps.lifeCycles, (v1: LifeCycleFn>, v2: LifeCycleFn>) => concat(v1, v2), ), ); microApp.mountPromise .then(() => { if (componentProps.autoSetLoading) { setLoading?.(false); } }) .catch((err: Error) => { setError?.(err); setLoading?.(false); }); (['loadPromise', 'bootstrapPromise'] as const).forEach((key) => { const promise = microApp[key]; promise.catch((e: Error) => { setError?.(e); setLoading?.(false); }); }); return microApp; } export function updateMicroApp({ name, microApp, microAppProps, setLoading, }: { name?: string; microApp?: MicroAppType; microAppProps?: Record; setLoading?: (loading: boolean) => void; }) { if (microApp) { if (!microApp._updatingPromise) { // 初始化 updatingPromise 为 microApp.mountPromise,从而确保后续更新是在应用 mount 完成之后 microApp._updatingPromise = microApp.mountPromise; microApp._updatingTimestamp = Date.now(); } else { // 确保 microApp.update 调用是跟组件状态变更顺序一致的,且后一个微应用更新必须等待前一个更新完成 microApp._updatingPromise = microApp._updatingPromise.then(() => { const canUpdate = (app: MicroAppType) => app.update && app.getStatus() === 'MOUNTED' && !app._unmounting; if (canUpdate(microApp)) { const props = { ...microAppProps, setLoading(l: boolean) { setLoading?.(l); }, }; if (process.env.NODE_ENV === 'development') { const updatingTimestamp = microApp._updatingTimestamp!; if (Date.now() - updatingTimestamp < 200) { console.warn( `[@qiankunjs/ui-shared] It seems like microApp ${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(`[@qiankunjs/ui-shared}] MicroApp ${name} is updating with props: `, props); microApp._updatingTimestamp = Date.now(); } // 返回 microApp.update 形成链式调用 return microApp.update?.(props); } return void 0; }); } } } export async function unmountMicroApp(microApp: MicroAppType) { await microApp.mountPromise.then(() => microApp.unmount()); } ================================================ FILE: packages/ui-bindings/shared/tsconfig.json ================================================ { "extends": "../../../tsconfig.json", "compilerOptions": { "paths": { "qiankun": ["packages/qiankun/src"] } } } ================================================ FILE: packages/ui-bindings/vue/.fatherrc.js ================================================ export { default } from '../../../.fatherrc.cjs'; ================================================ FILE: packages/ui-bindings/vue/CHANGELOG.md ================================================ # @qiankunjs/vue ## 0.0.1-rc.2 ### Patch Changes - f6926d3: fix(vue): add unmount hook to unmount application ## 0.0.1-rc.1 ### Patch Changes - ac068ae: fix: remove unused umd bundle configuration - Updated dependencies [ac068ae] - @qiankunjs/ui-shared@0.0.1-rc.1 ## 0.0.1-rc.0 ### Patch Changes - 9ec15954: feat: refactor the code of microapp - Updated dependencies [9ec15954] - @qiankunjs/ui-shared@0.0.1-rc.0 ================================================ FILE: packages/ui-bindings/vue/README.md ================================================ # qiankun vue binding ## Usage ```bash npm i @qiankunjs/vue ``` ## MicroApp Component Load (or unload) a sub-application directly through the `` component, which provides capabilities related to loading and error capturing: ```vue ``` When enabling the sub-application loading animation or error capturing capabilities, an additional style class `wrapperClassName` is accepted by the sub-application. The rendered result is as follows: ```vue
``` ### Loading Animation After enabling this feature, a loading animation will automatically be displayed while the sub-application is loading. When the sub-application finishes mounting and becomes in the MOUNTED state, the loading state ends, and the sub-application content is displayed. Simply pass `autoSetLoading` as a parameter: ```vue ``` #### Custom Loading Animation If you wish to override the default loading animation style, you can customize the loading component by using the loader slot as the sub-application's loading animation. ```vue ``` Here, `loading` is a boolean type parameter; when true, it indicates that it is still in the loading state, and when false, it indicates that the loading state has ended. ### Error Capturing After enabling this feature, when the sub-application encounters an exception while loading, an error message will automatically be displayed. You can pass the `autoCaptureError` property to the sub-application to enable error capturing capabilities: ```vue ``` #### Custom Error Capturing If you wish to override the default error capturing component style, you can customize the error capturing component for the sub-application using the errorBoundary slot: ```vue ``` ### Component Properties | Property | Required | Description | Type | Default Value | | --- | --- | --- | --- | --- | | `name` | Yes | The name of the micro-application | `string` | | | `entry` | Yes | The HTML address of the micro-application | `string` | | | `autoSetLoading` | No | Automatically set the loading status of the micro-application | `boolean` | `false` | | `autoCaptureError` | No | Automatically set error capturing for the micro-application | `boolean` | `false` | | `className` | No | The style class for the micro-application | `string` | `undefined` | | `wrapperClassName` | No | The style class wrapping the micro-application's loading and error components | `string` | `undefined` | | `appProps` | No | Properties passed to the sub-application | `Record` | `undefined` | ### Component Slots | Slot | Description | | --------------- | -------------------- | | `loader` | Loading state slot | | `errorBoundary` | Error capturing slot | ================================================ FILE: packages/ui-bindings/vue/README.zh-CN.md ================================================ # qiankun vue binding ## Usage ```bash npm i @qiankunjs/vue ``` ## MicroApp 组件 直接通过 组件加载(或卸载)子应用,该组件提供了 loading 以及错误捕获相关的能力: ```vue ``` 当启用子应用加载动画或错误捕获能力时,子应用接受一个额外的样式类 wrapperClassName,渲染的结果如下所示: ```vue
``` ### 加载动画 启用此能力后,当子应用正在加载时,会自动显示加载动画。当子应用挂载完成变成 MOUNTED 状态时,加载状态结束,显示子应用内容。 直接将 autoSetLoading 作为参数传入即可: ```vue ``` #### 自定义加载动画 如果您希望覆盖默认的加载动画样式时,可以通过 loader slot 来自定义加载组件 loader 作为子应用的加载动画。 ```vue ``` 其中,loading 为 boolean 类型参数,为 true 时表示仍在加载状态,为 false 时表示加载状态已结束。 ### 错误捕获 启用此能力后,当子应用加载出现异常时,会自动显示错误信息。可以向子应用传入 autoCaptureError 属性以开启子应用错误捕获能力: ```vue ``` #### 自定义错误捕获 如果您希望覆盖默认的错误捕获组件样式时,可以通过 errorBoundary slot 来自定义子应用的错误捕获组件: ```vue ``` ### 组件属性 | 属性 | 必填 | 说明 | 类型 | 默认值 | | --- | --- | --- | --- | --- | | `name` | 是 | 微应用的名称 | `string` | | `entry` | 是 | 微应用的 HTML 地址 | `string` | | `autoSetLoading` | 否 | 自动设置微应用的加载状态 | `boolean` | `false` | | `autoCaptureError` | 否 | 自动设置微应用的错误捕获 | `boolean` | `false` | | `className` | 否 | 微应用的样式类 | `string` | `undefined` | | `wrapperClassName` | 否 | 包裹微应用加载组件、错误捕获组件和微应用的样式类,仅在启用加载组件或错误捕获组件时有效 | `string` | `undefined` | | `appProps` | 否 | 传递给子应用的属性 | `Record` | `undefined` | ### 组件插槽 | 插槽 | 说明 | | --------------- | ------------ | | `loader` | 加载状态插槽 | | `errorBoundary` | 错误捕获插槽 | ================================================ FILE: packages/ui-bindings/vue/package.json ================================================ { "name": "@qiankunjs/vue", "version": "0.0.1-rc.2", "description": "vue binding for qiankun", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", "types": "./src/index.ts", "sideEffects": false, "scripts": { "build": "father build", "dev": "father dev" }, "author": "linghaoSu", "license": "MIT", "dependencies": { "@qiankunjs/ui-shared": "workspace:^", "lodash": "^4.17.11", "vue-demi": "^0.14.6" }, "devDependencies": { "eslint-plugin-vue": "^9.18.1", "vue": "^3.3.9", "vue2": "npm:vue@2.6.11" }, "peerDependencies": { "@vue/composition-api": "^1.7.2", "vue": "^2.0.0 || >=3.0.0" }, "peerDependenciesMeta": { "@vue/composition-api": { "optional": true } }, "publishConfig": { "registry": "https://registry.npmjs.org/" }, "files": [ "dist" ], "repository": "git@github.com:umijs/qiankun.git" } ================================================ FILE: packages/ui-bindings/vue/src/ErrorBoundary.ts ================================================ import { defineComponent, h } from 'vue-demi'; export default defineComponent({ props: { error: { type: Error, default: undefined, }, }, render() { return h('div', this.error?.message); }, }); ================================================ FILE: packages/ui-bindings/vue/src/MicroApp.ts ================================================ import type { PropType } from 'vue-demi'; import { computed, defineComponent, h, onMounted, ref, onBeforeUnmount, shallowRef, toRefs, watch, isVue2, } from 'vue-demi'; import type { AppConfiguration, LifeCycles } from 'qiankun'; import type { MicroAppType } from '@qiankunjs/ui-shared'; import { mountMicroApp, omitSharedProps, unmountMicroApp, updateMicroApp } from '@qiankunjs/ui-shared'; import MicroAppLoader from './MicroAppLoader'; import ErrorBoundary from './ErrorBoundary'; export const MicroApp = defineComponent({ name: 'MicroApp', props: { name: { type: String, required: true, }, entry: { type: String, required: true, }, settings: { type: Object as PropType, default: () => ({ sandbox: true, }), }, lifeCycles: { type: Object as PropType>>, }, autoSetLoading: { type: Boolean, default: false, }, autoCaptureError: { type: Boolean, default: false, }, wrapperClassName: { type: String, default: undefined, }, className: { type: String, default: undefined, }, appProps: { type: Object, default: undefined, }, }, setup(props, { slots }) { const originProps = props; const { name, wrapperClassName, className, appProps, autoCaptureError } = toRefs(originProps); const loading = ref(false); const error = ref(); const containerRef = ref(null); const microAppRef = shallowRef(); const isNeedShowError = computed(() => { return slots.errorBoundary || autoCaptureError.value; }); // 配置了 errorBoundary 才改 error 状态,否则直接往上抛异常 const setComponentError = (err: Error | undefined) => { if (isNeedShowError.value) { error.value = err; // error log 出来,不要吞 if (err) { console.error(error); } } else if (err) { throw err; } }; const rootRef = ref(null); const unmount = () => { const microApp = microAppRef.value; if (microApp) { microApp._unmounting = true; unmountMicroApp(microApp).catch((err: Error) => { setComponentError(err); loading.value = false; }); microAppRef.value = undefined; } }; onMounted(() => { // watch name 变更切换子应用 watch( name, () => { const prevApp = microAppRef.value; // 销毁上一个子应用 unmount(); // 初始化下一个子应用 void mountMicroApp({ prevMicroApp: prevApp, container: containerRef.value!, componentProps: { ...originProps, ...appProps.value, }, setLoading: (l) => { loading.value = l; }, setError: (err?: Error) => { setComponentError(err); }, }).then((app) => { microAppRef.value = app; }); }, { immediate: true, }, ); watch( appProps, () => { updateMicroApp({ microApp: microAppRef.value, setLoading: (l) => { loading.value = l; }, microAppProps: { ...omitSharedProps(originProps), ...appProps.value, }, }); }, { deep: true, }, ); }); onBeforeUnmount(() => { unmount(); }); const microAppWrapperClassName = computed(() => wrapperClassName.value ? `${wrapperClassName.value} qiankun-micro-app-wrapper` : 'qiankun-micro-app-wrapper', ); const microAppClassName = computed(() => { return className.value ? `${className.value} qiankun-micro-app-container` : 'qiankun-micro-app-container'; }); return { loading, error, containerRef, microAppRef, microAppWrapperClassName, microAppClassName, rootRef, microApp: microAppRef, }; }, render() { return this.autoSetLoading || this.autoCaptureError || this.$slots.loader || this.$slots.errorBoundary ? h( 'div', { class: this.microAppWrapperClassName, }, [ this.$slots.loader ? typeof this.$slots.loader === 'function' ? this.$slots.loader(this.loading) : this.$slots.loader : this.autoSetLoading && h(MicroAppLoader, { ...(isVue2 ? { props: { loading: this.loading, }, } : { loading: this.loading, }), }), this.error ? this.$slots.errorBoundary ? typeof this.$slots.errorBoundary === 'function' ? this.$slots.errorBoundary(this.error) : this.$slots.errorBoundary : this.autoCaptureError && h(ErrorBoundary, { ...(isVue2 ? { props: { error: this.error, }, } : { error: this.error, }), }) : null, h('div', { class: this.microAppClassName, ref: 'containerRef', }), ], ) : h('div', { class: this.microAppClassName, ref: 'containerRef', }); }, }); ================================================ FILE: packages/ui-bindings/vue/src/MicroAppLoader.ts ================================================ import { defineComponent, h } from 'vue-demi'; export default defineComponent({ props: { loading: { type: Boolean, default: false, }, }, render() { return h('div', this.loading ? 'loading...' : ''); }, }); ================================================ FILE: packages/ui-bindings/vue/src/index.ts ================================================ export * from './MicroApp'; ================================================ FILE: packages/ui-bindings/vue/tsconfig.json ================================================ { "extends": "../../../tsconfig.json", "compilerOptions": { "paths": { "qiankun": ["packages/qiankun/src"] } } } ================================================ FILE: pnpm-workspace.yaml ================================================ packages: - "packages/*" - "packages/ui-bindings/*" # - "examples/react15" # - "examples/react16" # - "examples/main" ================================================ FILE: scripts/generate-release-notes.mjs ================================================ #!/usr/bin/env node /** * Generate unified release notes from individual package CHANGELOGs. * * Usage: node scripts/generate-release-notes.mjs '' * * Reads each published package's CHANGELOG.md, extracts the latest version entry, * filters out "Updated dependencies" noise, and outputs aggregated markdown to stdout. */ import { existsSync, readFileSync, readdirSync } from 'node:fs'; import { join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; const ROOT = resolve(fileURLToPath(import.meta.url), '../..'); /** * Scan packages/ directory and build a map of package name → directory path. * Handles nested structures like packages/ui-bindings/react/. */ function discoverPackageDirs() { const dirs = new Map(); const packagesDir = join(ROOT, 'packages'); function tryRegister(dir) { const pkgJsonPath = join(dir, 'package.json'); if (existsSync(pkgJsonPath)) { const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8')); if (pkg.name) { dirs.set(pkg.name, dir); } return true; } return false; } function scanDir(dir) { const registered = tryRegister(dir); // Also scan subdirectories for nested packages (e.g. ui-bindings/react) try { const entries = readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { if (entry.isDirectory() && !['node_modules', 'dist', 'src', '.'].includes(entry.name)) { const subDir = join(dir, entry.name); if (!registered || existsSync(join(subDir, 'package.json'))) { scanDir(subDir); } } } } catch { // ignore read errors } } const topLevelDirs = readdirSync(packagesDir, { withFileTypes: true }); for (const entry of topLevelDirs) { if (entry.isDirectory()) { scanDir(join(packagesDir, entry.name)); } } return dirs; } /** * Extract the latest version entry from a CHANGELOG.md content string. * Returns filtered lines with "Updated dependencies" noise removed, * or null if no entry is found. */ function extractLatestEntry(changelogContent) { const lines = changelogContent.split('\n'); // Find the range of the first ## version section let startIndex = -1; let endIndex = lines.length; for (let i = 0; i < lines.length; i++) { const line = lines[i]; // Match "## " headings (not ### sub-headings) if (/^## \S/.test(line)) { if (startIndex === -1) { startIndex = i; } else { endIndex = i; break; } } } if (startIndex === -1) return null; const entryLines = lines.slice(startIndex, endIndex); // Filter out "Updated dependencies" blocks: // - Updated dependencies [hash] // - Updated dependencies [hash] // - @pkg/name@version // - @pkg/name@version const filtered = []; let inUpdatedDeps = false; for (const line of entryLines) { if (line.startsWith('- Updated dependencies')) { inUpdatedDeps = true; continue; } if (inUpdatedDeps) { // Indented lines are dependency version references if (line.startsWith(' - ')) { continue; } // Any other line exits the block inUpdatedDeps = false; } filtered.push(line); } return filtered; } // --- Main --- const publishedPackagesJSON = process.argv[2]; if (!publishedPackagesJSON) { console.error('Usage: node scripts/generate-release-notes.mjs \'\''); process.exit(1); } let publishedPackages; try { publishedPackages = JSON.parse(publishedPackagesJSON); } catch (e) { console.error('Failed to parse publishedPackages JSON:', e.message); process.exit(1); } if (!Array.isArray(publishedPackages) || publishedPackages.length === 0) { console.error('No published packages provided.'); process.exit(1); } const packageDirs = discoverPackageDirs(); const sections = []; // Sort: qiankun first (facade package), then alphabetical const sorted = [...publishedPackages].sort((a, b) => { if (a.name === 'qiankun') return -1; if (b.name === 'qiankun') return 1; return a.name.localeCompare(b.name); }); for (const { name, version } of sorted) { const dir = packageDirs.get(name); if (!dir) { console.error(`Warning: could not find directory for package "${name}"`); continue; } const changelogPath = join(dir, 'CHANGELOG.md'); if (!existsSync(changelogPath)) { console.error(`Warning: no CHANGELOG.md found for "${name}" at ${changelogPath}`); continue; } const content = readFileSync(changelogPath, 'utf-8'); const entryLines = extractLatestEntry(content); if (!entryLines || entryLines.length === 0) { continue; } // Skip the "## version" line (first entry) — we'll use our own formatted header const restLines = entryLines.slice(1); // Trim leading/trailing blank lines while (restLines.length > 0 && restLines[0].trim() === '') restLines.shift(); while (restLines.length > 0 && restLines[restLines.length - 1].trim() === '') restLines.pop(); // If only headings remain with no actual change items, skip this package const hasActualChanges = restLines.some((l) => l.startsWith('- ')); if (!hasActualChanges) { continue; } sections.push(`## ${name} \`${version}\`\n\n${restLines.join('\n')}`); } if (sections.length === 0) { console.error('No changelog entries found for any published package.'); process.exit(1); } console.log(sections.join('\n\n')); ================================================ FILE: tsconfig.eslint.json ================================================ { // extend your base config to share compilerOptions, etc "extends": "./tsconfig.json", "compilerOptions": { // ensure that nobody can accidentally use this config for a build "noEmit": true }, "include": [ // whatever paths you intend to lint "packages/*/src", "packages/bundler-plugin/tests", "packages/ui-bindings/*/src", "vitest.workspace.ts", "vitest.config.ts", ".fatherrc.js" ] } ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "baseUrl": "./", "target": "esnext", "module": "esnext", "lib": ["es2018", "dom"], "declaration": true, "importHelpers": false, "downlevelIteration": true, "strict": true, "noImplicitAny": true, "strictNullChecks": true, "resolveJsonModule": true, "strictFunctionTypes": true, "strictPropertyInitialization": true, "noImplicitThis": true, "alwaysStrict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "moduleResolution": "node", "typeRoots": ["typings", "node_modules/@types", "node_modules"], "allowSyntheticDefaultImports": true, "esModuleInterop": true, "types": ["vitest/globals", "node"], "paths": { "@@/*": ["./.dumi/tmp/*"], "@qiankunjs/*": ["packages/*/src"] } }, "exclude": ["node_modules", "examples", "packages/**/dist"] } ================================================ FILE: vitest.config.ts ================================================ import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { globals: false, environment: 'happy-dom', }, }); ================================================ FILE: vitest.workspace.ts ================================================ export default ['packages/*', 'packages/ui-bindings/*'];