├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .fatherrc.js
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ ├── bug_report_cn.md
│ ├── feature_request.md
│ └── rfc_cn.md
├── PULL_REQUEST_TEMPLATE.md
└── workflows
│ ├── announcement-notify.yml
│ ├── ci.yml
│ ├── emoji-helper.yml
│ ├── github-pages.yml
│ ├── issue-checker.yml
│ ├── issue-close-inactive.yml
│ ├── issue-reply.yml
│ ├── pr-checker.yml
│ ├── publish-1.x.yml
│ ├── publish-latest.yml
│ ├── publish-next.yml
│ └── release-notify.yml
├── .gitignore
├── .prettierignore
├── .prettierrc.js
├── .umirc.ts
├── LICENSE
├── README.md
├── contributors.json
├── docs
├── README.md
├── README.zh.md
├── api
│ ├── README.md
│ └── README.zh.md
├── cookbook
│ ├── README.md
│ └── README.zh.md
├── faq
│ ├── README.md
│ └── README.zh.md
└── guide
│ ├── README.md
│ ├── README.zh.md
│ ├── getting-started.md
│ ├── getting-started.zh.md
│ ├── tutorial.md
│ └── tutorial.zh.md
├── examples
├── angular9
│ ├── .editorconfig
│ ├── .gitignore
│ ├── README.md
│ ├── angular.json
│ ├── browserslist
│ ├── e2e
│ │ ├── protractor.conf.js
│ │ ├── src
│ │ │ ├── app.e2e-spec.ts
│ │ │ └── app.po.ts
│ │ └── tsconfig.json
│ ├── extra-webpack.config.js
│ ├── karma.conf.js
│ ├── package.json
│ ├── src
│ │ ├── app
│ │ │ ├── app-routing.module.ts
│ │ │ ├── app.component.css
│ │ │ ├── app.component.html
│ │ │ ├── app.component.spec.ts
│ │ │ ├── app.component.ts
│ │ │ ├── app.module.ts
│ │ │ └── empty-route
│ │ │ │ └── empty-route.component.ts
│ │ ├── assets
│ │ │ └── .gitkeep
│ │ ├── environments
│ │ │ ├── environment.prod.ts
│ │ │ └── environment.ts
│ │ ├── favicon.ico
│ │ ├── index.html
│ │ ├── main.qiankun.ts
│ │ ├── polyfills.ts
│ │ ├── single-spa
│ │ │ ├── asset-url.ts
│ │ │ └── single-spa-props.ts
│ │ ├── styles.css
│ │ └── test.ts
│ ├── tsconfig.app.json
│ ├── tsconfig.json
│ ├── tsconfig.spec.json
│ └── tslint.json
├── example.gif
├── main
│ ├── index.html
│ ├── index.js
│ ├── index.less
│ ├── multiple.html
│ ├── multiple.js
│ ├── package.json
│ ├── render
│ │ ├── ReactRender.jsx
│ │ └── VueRender.js
│ └── webpack.config.js
├── purehtml
│ ├── entry.js
│ ├── index.html
│ └── package.json
├── react15
│ ├── App.jsx
│ ├── components
│ │ ├── HelloModal.jsx
│ │ └── Logo.jsx
│ ├── dynamic.css
│ ├── index.css
│ ├── index.html
│ ├── index.js
│ ├── package.json
│ ├── public-path.js
│ └── webpack.config.js
├── react16
│ ├── .env
│ ├── .rescriptsrc.js
│ ├── README.md
│ ├── package.json
│ ├── public
│ │ ├── favicon.ico
│ │ ├── index.html
│ │ ├── logo192.png
│ │ ├── logo512.png
│ │ ├── manifest.json
│ │ └── robots.txt
│ └── src
│ │ ├── App.css
│ │ ├── App.js
│ │ ├── App.test.js
│ │ ├── components
│ │ ├── HelloModal.js
│ │ └── LibVersion.js
│ │ ├── index.js
│ │ ├── logo.svg
│ │ ├── pages
│ │ ├── About.js
│ │ └── Home.js
│ │ ├── public-path.js
│ │ └── serviceWorker.js
├── vue
│ ├── .browserslistrc
│ ├── .gitignore
│ ├── README.md
│ ├── babel.config.js
│ ├── package.json
│ ├── postcss.config.js
│ ├── src
│ │ ├── App.vue
│ │ ├── assets
│ │ │ └── logo.png
│ │ ├── components
│ │ │ └── HelloWorld.vue
│ │ ├── main.js
│ │ ├── public-path.js
│ │ ├── router
│ │ │ └── index.js
│ │ ├── store
│ │ │ └── index.js
│ │ └── views
│ │ │ ├── About.vue
│ │ │ └── Home.vue
│ └── vue.config.js
└── vue3
│ ├── .gitignore
│ ├── README.md
│ ├── babel.config.js
│ ├── package.json
│ ├── public
│ ├── favicon.ico
│ └── index.html
│ ├── src
│ ├── App.vue
│ ├── assets
│ │ └── logo.png
│ ├── components
│ │ └── HelloWorld.vue
│ ├── main.js
│ ├── public-path.js
│ ├── router
│ │ └── index.js
│ ├── store
│ │ ├── count.js
│ │ └── index.js
│ └── views
│ │ ├── About.vue
│ │ └── Home.vue
│ └── vue.config.js
├── package.json
├── src
├── __tests__
│ ├── globalState.test.ts
│ └── utils.test.ts
├── addons
│ ├── engineFlag.ts
│ ├── index.ts
│ └── runtimePublicPath.ts
├── apis.ts
├── effects.ts
├── error.ts
├── errorHandler.ts
├── globalState.ts
├── index.ts
├── interfaces.ts
├── loader.ts
├── prefetch.ts
├── sandbox
│ ├── __tests__
│ │ ├── common.test.ts
│ │ ├── proxySandbox.speedy.test.ts
│ │ ├── proxySandbox.test.ts
│ │ └── snapshotSandBox.test.ts
│ ├── common.ts
│ ├── globals.ts
│ ├── index.ts
│ ├── legacy
│ │ ├── __tests__
│ │ │ └── sandbox.test.ts
│ │ └── sandbox.ts
│ ├── patchers
│ │ ├── __tests__
│ │ │ ├── css.test.ts
│ │ │ └── interval.test.ts
│ │ ├── css.ts
│ │ ├── dynamicAppend
│ │ │ ├── __tests__
│ │ │ │ ├── common.test.ts
│ │ │ │ └── forStrictSandbox.test.ts
│ │ │ ├── common.ts
│ │ │ ├── forLooseSandbox.ts
│ │ │ ├── forStrictSandbox.ts
│ │ │ └── index.ts
│ │ ├── historyListener.ts
│ │ ├── index.ts
│ │ ├── interval.ts
│ │ └── windowListener.ts
│ ├── proxySandbox.ts
│ └── snapshotSandbox.ts
├── utils.ts
└── version.ts
└── tsconfig.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
12 | [*.md]
13 | trim_trailing_whitespace = false
14 |
15 | [Makefile]
16 | indent_style = tab
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | /examples
2 | /dist
3 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: [require.resolve('@umijs/fabric/dist/eslint')],
3 | rules: {
4 | '@typescript-eslint/prefer-interface': 0,
5 | '@typescript-eslint/no-explicit-any': 0,
6 | '@typescript-eslint/array-type': ['error', { default: 'array-simple' }],
7 | 'no-return-assign': 0,
8 | semi: ['error', 'always'],
9 | 'no-confusing-arrow': 0,
10 | 'no-console': 0,
11 | 'max-len': ['error', { code: 120, ignoreComments: true, ignoreStrings: true }],
12 | // see https://github.com/prettier/prettier/issues/3847
13 | 'space-before-function-paren': ['error', { anonymous: 'never', named: 'never', asyncArrow: 'always' }],
14 | 'no-underscore-dangle': 0,
15 | 'no-plusplus': 0,
16 | },
17 | parserOptions: {
18 | tsconfigRootDir: __dirname,
19 | project: './tsconfig.json',
20 | createDefaultProgram: true,
21 | },
22 | };
23 |
--------------------------------------------------------------------------------
/.fatherrc.js:
--------------------------------------------------------------------------------
1 | import { writeFileSync } from 'fs';
2 | import { join } from 'path';
3 | import { version } from './package.json';
4 | import globals from 'globals';
5 |
6 | // generate version.ts
7 | const versionFilePath = join(__dirname, './src/version.ts');
8 | writeFileSync(versionFilePath, `export const version = '${version}';`);
9 |
10 | // generate globals.ts
11 | const globalsFilePath = join(__dirname, './src/sandbox/globals.ts');
12 | writeFileSync(
13 | globalsFilePath,
14 | `// generated from https://github.com/sindresorhus/globals/blob/main/globals.json es2015 part
15 | // only init its values while Proxy is supported
16 | export const globalsInES2015 = window.Proxy ? ${JSON.stringify(
17 | Object.keys(globals.es2015),
18 | null,
19 | 2,
20 | )}.filter(p => /* just keep the available properties in current window context */ p in window) : [];
21 |
22 | export const globalsInBrowser = ${JSON.stringify(Object.keys(globals.browser), null, 2)};
23 | `,
24 | );
25 |
26 | export default {
27 | target: 'browser',
28 | esm: 'babel',
29 | cjs: 'babel',
30 | umd: {
31 | minFile: true,
32 | sourcemap: true,
33 | },
34 | runtimeHelpers: true,
35 | extraBabelPlugins: [
36 | [
37 | 'babel-plugin-import',
38 | {
39 | libraryName: 'lodash',
40 | libraryDirectory: '',
41 | camel2DashComponentName: false,
42 | },
43 | ],
44 | ],
45 | };
46 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 'Bug report'
3 | about: 'Report a bug to help us improve'
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 | ---
8 |
9 | ## What happens?
10 |
11 | A clear and concise description of what the bug is.
12 |
13 | ## Mini Showcase Repository(REQUIRED)
14 |
15 | > Provide a mini GitHub repository which can reproduce the issue.
16 |
17 |
18 |
19 | ## How To Reproduce
20 |
21 | **Steps to reproduce the behavior:** 1. 2.
22 |
23 | **Expected behavior** 1. 2.
24 |
25 | ## Context
26 |
27 | - **qiankun Version**:
28 | - **Platform Version**:
29 | - **Browser Version**:
30 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report_cn.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: '缺陷问题反馈'
3 | about: '反馈问题以帮助我们改进'
4 | title: '[Bug]请遵循下文模板提交问题,否则您的问题会被关闭'
5 | labels: ''
6 | assignees: ''
7 | ---
8 |
9 | # 提问之前强烈建立您能先阅读一下[《如何正确的提出一个 Issue》](https://github.com/umijs/qiankun/issues/1115)
10 |
11 |
14 |
15 | ## What happens?
16 |
17 |
18 |
19 | ## 最小可复现仓库
20 |
21 | 为节约大家的时间,无复现步骤的 ISSUE 会被关闭,提供之后再 REOPEN
22 |
23 |
24 |
25 | ## 复现步骤,错误日志以及相关配置
26 |
27 |
28 |
29 |
30 | ## 相关环境信息
31 |
32 | - **qiankun 版本**
33 | - **浏览器版本**:
34 | - **操作系统**:
35 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 'Feature request'
3 | about: 'Suggest an idea for this project'
4 | title: '[Feature Request] say something'
5 | labels: ''
6 | assignees: ''
7 | ---
8 |
9 | ## Background
10 |
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | ## Proposal
14 |
15 | Describe the solution you'd like, better to provide some pseudo code.
16 |
17 | ## Additional context
18 |
19 | Add any other context or screenshots about the feature request here.
20 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/rfc_cn.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 'RFC Proposals'
3 | about: 'Provide a solution for this project'
4 | title: '[RFC] say something'
5 | labels: 'type: proposals'
6 | assignees: ''
7 | ---
8 |
9 | ## 背景
10 |
11 | > 描述你希望解决的问题的现状,附上相关的 issue 地址
12 |
13 | ## 思路
14 |
15 | > 描述大概的解决思路,可以包含 API 设计和伪代码等
16 |
17 | ## 跟进
18 |
19 | - [ ] some task
20 | - [ ] PR URL
21 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
10 |
11 | ##### Checklist
12 |
13 |
14 |
15 | - [ ] `npm test` passes
16 | - [ ] tests are included
17 | - [ ] documentation is changed or added
18 | - [ ] commit message follows commit guidelines
19 |
20 | ##### Description of change
21 |
22 |
23 |
24 | - any feature?
25 | - close https://github.com/umijs/qiankun/ISSUE_URL
26 |
--------------------------------------------------------------------------------
/.github/workflows/announcement-notify.yml:
--------------------------------------------------------------------------------
1 | name: Annoucement Notify
2 |
3 | on:
4 | discussion:
5 | types: [created]
6 |
7 | jobs:
8 | notify:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: Send DingGroup1 Anouncement Notify
12 | uses: zcong1993/actions-ding@master
13 | if: github.event.discussion.category.name == 'Announcements'
14 | with:
15 | dingToken: ${{ secrets.DING_GROUP_1_TOKEN }}
16 | secret: ${{ secrets.DING_GROUP_1_SIGN }}
17 | body: |
18 | {
19 | "msgtype": "markdown",
20 | "markdown": {
21 | "title": "Qiankun News",
22 | "text": "# 新闻播报📢 [${{github.event.discussion.title}}](${{github.event.discussion.html_url}}) \n${{github.event.discussion.body}}",
23 | }
24 | }
25 |
26 | - name: Send DingGroup2 Anouncement Notify
27 | uses: zcong1993/actions-ding@master
28 | if: github.event.discussion.category.name == 'Announcements'
29 | with:
30 | dingToken: ${{ secrets.DING_GROUP_2_TOKEN }}
31 | secret: ${{ secrets.DING_GROUP_2_SIGN }}
32 | body: |
33 | {
34 | "msgtype": "markdown",
35 | "markdown": {
36 | "title": "Qiankun News",
37 | "text": "# 新闻播报📢 [${{github.event.discussion.title}}](${{github.event.discussion.html_url}}) \n${{github.event.discussion.body}}",
38 | }
39 | }
40 |
41 | - name: Send DingGroup3 Anouncement Notify
42 | uses: zcong1993/actions-ding@master
43 | if: github.event.discussion.category.name == 'Announcements'
44 | with:
45 | dingToken: ${{ secrets.DING_GROUP_3_TOKEN }}
46 | secret: ${{ secrets.DING_GROUP_3_SIGN }}
47 | body: |
48 | {
49 | "msgtype": "markdown",
50 | "markdown": {
51 | "title": "Qiankun News",
52 | "text": "# 新闻播报📢 [${{github.event.discussion.title}}](${{github.event.discussion.html_url}}) \n${{github.event.discussion.body}}",
53 | }
54 | }
55 |
56 | - name: Send DingGroup4 Anouncement Notify
57 | uses: zcong1993/actions-ding@master
58 | if: github.event.discussion.category.name == 'Announcements'
59 | with:
60 | dingToken: ${{ secrets.DING_GROUP_4_TOKEN }}
61 | secret: ${{ secrets.DING_GROUP_4_SIGN }}
62 | body: |
63 | {
64 | "msgtype": "markdown",
65 | "markdown": {
66 | "title": "Qiankun News",
67 | "text": "# 新闻播报📢 [${{github.event.discussion.title}}](${{github.event.discussion.html_url}}) \n${{github.event.discussion.body}}",
68 | }
69 | }
70 |
71 | - name: Send DingGroup5 Anouncement Notify
72 | uses: zcong1993/actions-ding@master
73 | if: github.event.discussion.category.name == 'Announcements'
74 | with:
75 | dingToken: ${{ secrets.DING_GROUP_5_TOKEN }}
76 | secret: ${{ secrets.DING_GROUP_5_SIGN }}
77 | body: |
78 | {
79 | "msgtype": "markdown",
80 | "markdown": {
81 | "title": "Qiankun News",
82 | "text": "# 新闻播报📢 [${{github.event.discussion.title}}](${{github.event.discussion.html_url}}) \n${{github.event.discussion.body}}",
83 | }
84 | }
85 |
86 | - name: Send DingGroup6 Anouncement Notify
87 | uses: zcong1993/actions-ding@master
88 | if: github.event.discussion.category.name == 'Announcements'
89 | with:
90 | dingToken: ${{ secrets.DING_GROUP_6_TOKEN }}
91 | secret: ${{ secrets.DING_GROUP_6_SIGN }}
92 | body: |
93 | {
94 | "msgtype": "markdown",
95 | "markdown": {
96 | "title": "Qiankun News",
97 | "text": "# 新闻播报📢 [${{github.event.discussion.title}}](${{github.event.discussion.html_url}}) \n${{github.event.discussion.body}}",
98 | }
99 | }
100 |
101 | - name: Send DingGroup7 Anouncement Notify
102 | uses: zcong1993/actions-ding@master
103 | if: github.event.discussion.category.name == 'Announcements'
104 | with:
105 | dingToken: ${{ secrets.DING_GROUP_7_TOKEN }}
106 | secret: ${{ secrets.DING_GROUP_7_SIGN }}
107 | body: |
108 | {
109 | "msgtype": "markdown",
110 | "markdown": {
111 | "title": "Qiankun News",
112 | "text": "# 新闻播报📢 [${{github.event.discussion.title}}](${{github.event.discussion.html_url}}) \n${{github.event.discussion.body}}",
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches:
7 | - master
8 | - next
9 | - 1.x
10 |
11 | jobs:
12 | check:
13 | runs-on: ubuntu-latest
14 |
15 | strategy:
16 | matrix:
17 | node-version: [14.x, 16.x]
18 |
19 | steps:
20 | - uses: actions/checkout@v2
21 |
22 | - name: Use Node.js ${{ matrix.node-version }}
23 | uses: actions/setup-node@v2
24 | with:
25 | node-version: ${{ matrix.node-version }}
26 |
27 | - name: Get yarn cache directory path
28 | id: yarn-cache-dir-path
29 | run: echo "::set-output name=dir::$(yarn cache dir)"
30 |
31 | - uses: actions/cache@v2
32 | id: yarn-cache
33 | with:
34 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
35 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
36 | restore-keys: |
37 | ${{ runner.os }}-yarn-
38 | - run: yarn
39 | - run: yarn ci
40 | - run: yarn docs:build
41 |
--------------------------------------------------------------------------------
/.github/workflows/emoji-helper.yml:
--------------------------------------------------------------------------------
1 | name: Emoji Helper
2 |
3 | on:
4 | release:
5 | types: [published]
6 |
7 | jobs:
8 | emoji:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions-cool/emoji-helper@v1.0.0
12 | with:
13 | type: 'release'
14 | emoji: '+1, laugh, heart, hooray, rocket, eyes'
15 |
--------------------------------------------------------------------------------
/.github/workflows/github-pages.yml:
--------------------------------------------------------------------------------
1 | name: Qiankun Github Pages Deploy
2 | on:
3 | push:
4 | branches:
5 | - master
6 | jobs:
7 | deploy-gh-pages:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/checkout@v2
11 | - uses: actions/setup-node@v2
12 | with:
13 | node-version: '14'
14 | check-latest: true
15 | registry-url: 'https://registry.npmjs.org'
16 | - name: build
17 | run: |
18 | yarn
19 | yarn docs:build
20 |
21 | - name: Deploy
22 | uses: peaceiris/actions-gh-pages@v3
23 | with:
24 | github_token: ${{ secrets.GITHUB_TOKEN }}
25 | publish_dir: ./dist
26 |
--------------------------------------------------------------------------------
/.github/workflows/issue-checker.yml:
--------------------------------------------------------------------------------
1 | name: Issue Checker
2 |
3 | on:
4 | issues:
5 | types: [opened, edited]
6 |
7 | jobs:
8 | check-permission:
9 | runs-on: ubuntu-latest
10 | outputs:
11 | require-result: ${{ steps.checkUser.outputs.require-result }}
12 | steps:
13 | - uses: actions-cool/check-user-permission@v2
14 | id: checkUser
15 | with:
16 | require: 'write'
17 |
18 | log-conditions:
19 | runs-on: ubuntu-latest
20 | needs: check-permission
21 | env:
22 | ISSUE_TITLE: ${{ github.event.issue.title }}
23 | ISSUE_BODY: ${{ github.event.issue.body }}
24 | ISSUE_STATE: ${{ github.event.issue.state }}
25 | REQUIRE_RESULT: ${{ needs.check-permission.outputs.require-result }}
26 | steps:
27 | - run: echo "require-result = $REQUIRE_RESULT"
28 | - run: echo "issue state = $ISSUE_STATE"
29 | - run: echo "contains '[Feature Request]' in title: ${{ contains(env.ISSUE_TITLE, '[Feature Request]') }}"
30 | - run: echo "contains 'https://github.com' in body: ${{ contains(env.ISSUE_BODY, 'https://github.com') }}"
31 |
32 | check-open:
33 | runs-on: ubuntu-latest
34 | needs: check-permission
35 | env:
36 | ISSUE_TITLE: ${{ github.event.issue.title }}
37 | ISSUE_BODY: ${{ github.event.issue.body }}
38 | ISSUE_STATE: ${{ github.event.issue.state }}
39 | REQUIRE_RESULT: ${{ needs.check-permission.outputs.require-result }}
40 | if: env.REQUIRE_RESULT == 'false' && env.ISSUE_STATE == 'open' && contains(env.ISSUE_TITLE, '[Feature Request]') == false && contains(env.ISSUE_BODY, 'https://github.com') == false && contains(env.ISSUE_BODY, 'https://stackblitz.com') == false && contains(env.ISSUE_BODY, 'https://codesandbox.io') == false
41 | steps:
42 | - uses: actions-cool/maintain-one-comment@v3
43 | with:
44 | body: 由于缺乏足够的信息(github、stackblitz、codesandbox等可复现仓库),我们暂时关闭了该 Issue。请修改(不要回复) Issue 提供[最小重现](https://stackoverflow.com/help/minimal-reproducible-example)以重新开启。谢谢。如果只是单独的技术咨询,可移步 https://qiankun.umijs.org/#-community 交流~
45 | - uses: actions-cool/issues-helper@v3
46 | with:
47 | actions: close-issue
48 |
49 | check-close:
50 | runs-on: ubuntu-latest
51 | needs: check-permission
52 | env:
53 | ISSUE_BODY: ${{ github.event.issue.body }}
54 | ISSUE_STATE: ${{ github.event.issue.state }}
55 | REQUIRE_RESULT: ${{ needs.check-permission.outputs.require-result }}
56 | if: env.REQUIRE_RESULT == 'false' && env.ISSUE_STATE == 'closed' && (contains(env.ISSUE_BODY, 'https://github.com') == true || contains(env.ISSUE_BODY, 'https://stackblitz.com') == true || contains(env.ISSUE_BODY, 'https://codesandbox.io') == true)
57 | steps:
58 | - uses: actions-cool/issues-helper@v3
59 | with:
60 | actions: open-issue
61 |
--------------------------------------------------------------------------------
/.github/workflows/issue-close-inactive.yml:
--------------------------------------------------------------------------------
1 | name: Issue Close Inactive
2 |
3 | on:
4 | schedule:
5 | - cron: "0 0 * * *"
6 |
7 | jobs:
8 | close-issues:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: close inactive issue without reprodction
12 | uses: actions-cool/issues-helper@v2.2.1
13 | with:
14 | actions: 'close-issues'
15 | labels: 'Need Reproduction'
16 | inactive-day: 30
17 | body: |
18 | 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.
19 | 由于该 issue 被标记为需要可复现步骤,却 30 天未收到回应。现关闭 issue,若有任何问题,可评论回复。
20 |
21 | - name: close inactive issue not use template
22 | uses: actions-cool/issues-helper@v2.2.1
23 | with:
24 | actions: 'close-issues'
25 | labels: 'pls use issue template'
26 | inactive-day: 30
27 | body: |
28 | 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.
29 | 由于该 issue 被标记为需要使用模板,却 30 天未收到回应。现关闭 issue,若有任何问题,可评论回复。
30 |
31 | - name: close inactive issue out of scope
32 | uses: actions-cool/issues-helper@v2.2.1
33 | with:
34 | actions: 'close-issues'
35 | labels: 'out-of-scope'
36 | inactive-day: 30
37 | body: |
38 | 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.
39 | 由于该 issue 被标记为与本项目无关,却 30 天未收到回应。现关闭 issue,若有任何问题,可评论回复。
40 |
41 | - name: close inactive issue
42 | uses: actions-cool/issues-helper@v2.2.1
43 | with:
44 | actions: 'close-issues'
45 | labels: 'inactive'
46 | inactive-day: 7
47 | body: |
48 | 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.
49 | 由于该 issue 被标记为不活跃,且 7 天未收到回应。现关闭 issue,若有任何问题,可评论回复。
50 |
--------------------------------------------------------------------------------
/.github/workflows/issue-reply.yml:
--------------------------------------------------------------------------------
1 | name: Issue Reply
2 |
3 | on:
4 | issues:
5 | types: [labeled]
6 |
7 | jobs:
8 | reply-helper:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: help wanted
12 | if: github.event.label.name == 'pr welcome' || github.event.label.name == 'help wanted'
13 | uses: actions-cool/issues-helper@v2.0.0
14 | with:
15 | actions: 'create-comment'
16 | issue-number: ${{ github.event.issue.number }}
17 | body: |
18 | 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!
19 |
20 | 你好 @${{ github.event.issue.user.login }},我们完全同意你的提议/反馈,欢迎直接在此仓库创建一个 Pull Request 来解决这个问题。请务必填写 Pull Request 内的预设模板,提供改动所需相应的 changelog、测试用例、文档等,并确保 CI 通过,我们会尽快进行 Review,提前感谢和期待您的贡献。
21 |
22 | - name: pls use issue template
23 | if: github.event.label.name == 'pls use issue template'
24 | uses: actions-cool/issues-helper@v2.0.0
25 | with:
26 | actions: 'create-comment, close-issue'
27 | issue-number: ${{ github.event.issue.number }}
28 | body: |
29 | Hello @${{ github.event.issue.user.login }}. To save both time, please use the issue template to report. This issue will be closed.
30 |
31 | 你好 @${{ github.event.issue.user.login }},为节约大家的时间,请使用 issue 模板反馈问题。该 issue 将要被关闭。
32 |
33 | - name: Need Reproduction
34 | if: github.event.label.name == 'Need Reproduction'
35 | uses: actions-cool/issues-helper@v2.0.0
36 | with:
37 | actions: 'create-comment'
38 | issue-number: ${{ github.event.issue.number }}
39 | body: |
40 | 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.
41 |
42 | 你好 @${{ github.event.issue.user.login }}, 为了方便定位和排查问题,我们需要你提供一个重现实例,请提供一个尽可能精简的链接 [codesandbox](https://codesandbox.io/) 或直接 clone [qiankun examples](https://github.com/umijs/qiankun/tree/master/examples),并上传到你的 GitHub 仓库。
43 |
44 | 
45 |
--------------------------------------------------------------------------------
/.github/workflows/pr-checker.yml:
--------------------------------------------------------------------------------
1 | name: PullRequest Checker
2 |
3 | on:
4 | pull_request:
5 | types:
6 | - closed
7 | paths-ignore:
8 | - 'docs/**'
9 |
10 | jobs:
11 | read-file:
12 | runs-on: ubuntu-latest
13 | outputs:
14 | require-result: ${{ steps.contributors.outputs.content }}
15 | steps:
16 | - name: Checkout
17 | uses: actions/checkout@v1
18 | - name: Read contributors.json
19 | id: contributors
20 | uses: juliangruber/read-file-action@v1
21 | with:
22 | path: ./contributors.json
23 |
24 | output-log:
25 | runs-on: ubuntu-latest
26 | needs: read-file
27 | steps:
28 | - name: contributors.json
29 | run: echo "${{ needs.read-file.outputs.require-result }}"
30 | - name: creator
31 | run: echo "${{ github.event.pull_request.user.login }}"
32 | - name: contains
33 | run: echo "${{ contains(fromJSON(needs.read-file.outputs.require-result), github.event.pull_request.user.login) }}"
34 | - name: merged
35 | run: echo "${{ github.event.pull_request.merged }}"
36 |
37 | check-merged:
38 | runs-on: ubuntu-latest
39 | needs: read-file
40 | permissions:
41 | issues: write
42 | pull-requests: write
43 | if: contains(fromJSON(needs.read-file.outputs.require-result), github.event.pull_request.user.login) == false && github.event.pull_request.merged == true
44 | steps:
45 | - uses: actions-cool/maintain-one-comment@v3
46 | with:
47 | body: 感谢 PR!如果有兴趣一起参与维护 Qiankun,可加入我们的 Qiankun Contributors 群。加入方式是先用钉钉扫下方二维码加我钉钉,记得注明 github id,然后我会拉你到群里。
48 |
--------------------------------------------------------------------------------
/.github/workflows/publish-1.x.yml:
--------------------------------------------------------------------------------
1 | name: Publish 1.x Version
2 |
3 | on:
4 | push:
5 | tags:
6 | - v1.*
7 |
8 | jobs:
9 | publish:
10 |
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - uses: actions/checkout@v2
15 |
16 | - uses: actions/setup-node@v2
17 | with:
18 | node-version: '14.x'
19 | check-latest: true
20 | registry-url: 'https://registry.npmjs.org'
21 |
22 | - run: yarn
23 | - run: yarn publish --tag qiankun1
24 | env:
25 | NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}
26 |
--------------------------------------------------------------------------------
/.github/workflows/publish-latest.yml:
--------------------------------------------------------------------------------
1 | name: Publish Latest Version
2 |
3 | on:
4 | push:
5 | tags:
6 | - v2.*
7 |
8 | jobs:
9 | publish:
10 |
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - uses: actions/checkout@v2
15 |
16 | - uses: actions/setup-node@v2
17 | with:
18 | node-version: '14.x'
19 | check-latest: true
20 | registry-url: 'https://registry.npmjs.org'
21 |
22 | - run: yarn
23 | - run: yarn publish --tag latest
24 | env:
25 | NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}
26 |
--------------------------------------------------------------------------------
/.github/workflows/publish-next.yml:
--------------------------------------------------------------------------------
1 | name: Publish Next Version
2 |
3 | on:
4 | push:
5 | tags:
6 | - v3.*
7 |
8 | jobs:
9 | publish:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v2
13 |
14 | - uses: actions/setup-node@v2
15 | with:
16 | node-version: '14'
17 | check-latest: true
18 | registry-url: 'https://registry.npmjs.org'
19 |
20 | - run: yarn
21 | - run: yarn publish --tag next
22 | env:
23 | NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}
24 |
--------------------------------------------------------------------------------
/.github/workflows/release-notify.yml:
--------------------------------------------------------------------------------
1 | name: Release Notify
2 |
3 | on:
4 | release:
5 | types: [published]
6 |
7 | jobs:
8 | notify:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: Send DingGroup1 Notify
12 | uses: zcong1993/actions-ding@master
13 | with:
14 | dingToken: ${{ secrets.DING_GROUP_1_TOKEN }}
15 | secret: ${{ secrets.DING_GROUP_1_SIGN }}
16 | body: |
17 | {
18 | "msgtype": "markdown",
19 | "markdown": {
20 | "title": "qiankun ${{github.event.release.tag_name}} 发布公告",
21 | "text": "# qiankun [${{github.event.release.tag_name}}](${{github.event.release.html_url}}) 发布公告\n${{github.event.release.body}}",
22 | }
23 | }
24 |
25 | - name: Send DingGroup2 Notify
26 | uses: zcong1993/actions-ding@master
27 | with:
28 | dingToken: ${{ secrets.DING_GROUP_2_TOKEN }}
29 | secret: ${{ secrets.DING_GROUP_2_SIGN }}
30 | body: |
31 | {
32 | "msgtype": "markdown",
33 | "markdown": {
34 | "title": "qiankun ${{github.event.release.tag_name}} 发布公告",
35 | "text": "# qiankun [${{github.event.release.tag_name}}](${{github.event.release.html_url}}) 发布公告\n${{github.event.release.body}}",
36 | }
37 | }
38 |
39 | - name: Send DingGroup3 Notify
40 | uses: zcong1993/actions-ding@master
41 | with:
42 | dingToken: ${{ secrets.DING_GROUP_3_TOKEN }}
43 | secret: ${{ secrets.DING_GROUP_3_SIGN }}
44 | body: |
45 | {
46 | "msgtype": "markdown",
47 | "markdown": {
48 | "title": "qiankun ${{github.event.release.tag_name}} 发布公告",
49 | "text": "# qiankun [${{github.event.release.tag_name}}](${{github.event.release.html_url}}) 发布公告\n${{github.event.release.body}}",
50 | }
51 | }
52 |
53 | - name: Send DingGroup4 Notify
54 | uses: zcong1993/actions-ding@master
55 | with:
56 | dingToken: ${{ secrets.DING_GROUP_4_TOKEN }}
57 | secret: ${{ secrets.DING_GROUP_4_SIGN }}
58 | body: |
59 | {
60 | "msgtype": "markdown",
61 | "markdown": {
62 | "title": "qiankun ${{github.event.release.tag_name}} 发布公告",
63 | "text": "# qiankun [${{github.event.release.tag_name}}](${{github.event.release.html_url}}) 发布公告\n${{github.event.release.body}}",
64 | }
65 | }
66 |
67 | - name: Send DingGroup5 Notify
68 | uses: zcong1993/actions-ding@master
69 | with:
70 | dingToken: ${{ secrets.DING_GROUP_5_TOKEN }}
71 | secret: ${{ secrets.DING_GROUP_5_SIGN }}
72 | body: |
73 | {
74 | "msgtype": "markdown",
75 | "markdown": {
76 | "title": "qiankun ${{github.event.release.tag_name}} 发布公告",
77 | "text": "# qiankun [${{github.event.release.tag_name}}](${{github.event.release.html_url}}) 发布公告\n${{github.event.release.body}}",
78 | }
79 | }
80 |
81 | - name: Send DingGroup6 Notify
82 | uses: zcong1993/actions-ding@master
83 | with:
84 | dingToken: ${{ secrets.DING_GROUP_6_TOKEN }}
85 | secret: ${{ secrets.DING_GROUP_6_SIGN }}
86 | body: |
87 | {
88 | "msgtype": "markdown",
89 | "markdown": {
90 | "title": "qiankun ${{github.event.release.tag_name}} 发布公告",
91 | "text": "# qiankun [${{github.event.release.tag_name}}](${{github.event.release.html_url}}) 发布公告\n${{github.event.release.body}}",
92 | }
93 | }
94 |
95 | - name: Send DingGroup7 Notify
96 | uses: zcong1993/actions-ding@master
97 | with:
98 | dingToken: ${{ secrets.DING_GROUP_7_TOKEN }}
99 | secret: ${{ secrets.DING_GROUP_7_SIGN }}
100 | body: |
101 | {
102 | "msgtype": "markdown",
103 | "markdown": {
104 | "title": "qiankun ${{github.event.release.tag_name}} 发布公告",
105 | "text": "# qiankun [${{github.event.release.tag_name}}](${{github.event.release.html_url}}) 发布公告\n${{github.event.release.body}}",
106 | }
107 | }
108 |
109 | - name: Send DingGroupInc Notify
110 | uses: zcong1993/actions-ding@master
111 | with:
112 | dingToken: ${{ secrets.DING_GROUP_INC_TOKEN }}
113 | secret: ${{ secrets.DING_GROUP_INC_SIGN }}
114 | body: |
115 | {
116 | "msgtype": "markdown",
117 | "markdown": {
118 | "title": "qiankun ${{github.event.release.tag_name}} 发布公告",
119 | "text": "# qiankun [${{github.event.release.tag_name}}](${{github.event.release.html_url}}) 发布公告\n${{github.event.release.body}}",
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | pids
2 | logs
3 | node_modules
4 | npm-debug.log
5 | coverage/
6 | run
7 | dist
8 | .DS_Store
9 | .nyc_output
10 | config.local.js
11 | .umi
12 | .umi-production
13 | .idea/
14 | .cache
15 | yarn.lock
16 | es
17 | lib
18 | package-lock.json
19 | .eslintcache
20 | .history
21 | .now
22 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | /test/fixtures
2 | **/*.gif
3 | /dist
4 | /docs
5 | /es
6 | /lib
7 | /coverage
8 | .cache
9 | examples
10 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | const fabric = require('@umijs/fabric');
2 |
3 | module.exports = {
4 | ...fabric.prettier,
5 | printWidth: 120,
6 | };
7 |
--------------------------------------------------------------------------------
/.umirc.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'dumi';
2 |
3 | export default defineConfig({
4 | mode: 'site',
5 | hash: true,
6 | ssr: {},
7 | publicPath: process.env.NOW_DEPLOY ? '/' : '/qiankun/',
8 | base: process.env.NOW_DEPLOY ? '/' : '/qiankun',
9 | resolve: {
10 | includes: ['docs'],
11 | previewLangs: [],
12 | },
13 | locales: [
14 | ['en', 'English'],
15 | ['zh', '中文'],
16 | ],
17 | navs: {
18 | en: [
19 | null,
20 | {
21 | title: 'Version Notice',
22 | children: [
23 | { title: 'Changelog', path: 'https://github.com/umijs/qiankun/releases' },
24 | { title: 'Upgrade Guide', path: '/cookbook#upgrade-from-1x-version-to-2x-version' },
25 | { title: '1.x Version', path: 'https://v1.qiankun.umijs.org/' },
26 | ],
27 | },
28 | { title: 'GitHub', path: 'https://github.com/umijs/qiankun' },
29 | ],
30 | zh: [
31 | null,
32 | {
33 | title: '版本公告',
34 | children: [
35 | { title: '发布日志', path: 'https://github.com/umijs/qiankun/releases' },
36 | { title: '升级指南', path: '/zh/cookbook#从-1x-版本升级到-2x-版本' },
37 | { title: '1.x 版本', path: 'https://v1.qiankun.umijs.org/zh/' },
38 | ],
39 | },
40 | { title: 'GitHub', path: 'https://github.com/umijs/qiankun' },
41 | ],
42 | },
43 | metas: [
44 | {
45 | name: 'keywords',
46 | content:
47 | 'microfrontend, micro frontend, micro frontends, micro-frontend, micro-frontends, microservice, javascript',
48 | },
49 | ],
50 | analytics: {
51 | ga: 'UA-157295698-1',
52 | baidu: '0f738d9b0ac90574c09183ea85bcfa2e',
53 | },
54 | exportStatic: {},
55 | logo: 'https://gw.alipayobjects.com/zos/bmw-prod/8a74c1d3-16f3-4719-be63-15e467a68a24/km0cv8vn_w500_h500.png',
56 | favicon: 'https://gw.alipayobjects.com/mdn/rms_655822/afts/img/A*4sIUQpcos_gAAAAAAAAAAAAAARQnAQ',
57 | theme: {
58 | '@c-primary': '#6451AB',
59 | },
60 | styles: [
61 | `html .__dumi-default-layout-hero {
62 | padding: 140px 0;
63 | background: #21144d no-repeat bottom/cover url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1600 900'%3E%3Cpolygon fill='%23f3c200' points='957 450 539 900 1396 900'/%3E%3Cpolygon fill='%23e26003' points='957 450 872.9 900 1396 900'/%3E%3Cpolygon fill='%23ecb500' points='-60 900 398 662 816 900'/%3E%3Cpolygon fill='%23d86200' points='337 900 398 662 816 900'/%3E%3Cpolygon fill='%23e4a800' points='1203 546 1552 900 876 900'/%3E%3Cpolygon fill='%23ce6400' points='1203 546 1552 900 1162 900'/%3E%3Cpolygon fill='%23dc9b00' points='641 695 886 900 367 900'/%3E%3Cpolygon fill='%23c46500' points='587 900 641 695 886 900'/%3E%3Cpolygon fill='%23d38f00' points='1710 900 1401 632 1096 900'/%3E%3Cpolygon fill='%23ba6600' points='1710 900 1401 632 1365 900'/%3E%3Cpolygon fill='%23ca8300' points='1210 900 971 687 725 900'/%3E%3Cpolygon fill='%23b16600' points='943 900 1210 900 971 687'/%3E%3C/svg%3E");;
64 | }
65 | html .__dumi-default-layout-hero h1 {
66 | color: #f3f3f3;
67 | text-shadow: 0 2px 8px rgba(0,0,0,.3);
68 | }
69 | html .__dumi-default-layout-hero .markdown {
70 | color: #eee;
71 | text-shadow: 0 2px 5px rgba(0,0,0,.3);
72 | }`,
73 | ],
74 | });
75 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Kuitos
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | # qiankun(乾坤)
16 |
17 | > In Chinese, `qian(乾)` means heaven and `kun(坤)` earth. `qiankun` is the universe.
18 |
19 | 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/CanopyTax/single-spa).
20 |
21 | ## 🤔 Motivation
22 |
23 | A quick recap about the concept of `Micro Frontends`:
24 |
25 | > Techniques, strategies and recipes for building a **modern web app** with **multiple teams** using **different JavaScript frameworks**. — [Micro Frontends](https://micro-frontends.org/)
26 |
27 | 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.
28 |
29 | The path was never easy, we stepped on every challenge there could possibly be. Just to name a few:
30 |
31 | - In what form do micro-apps publish static resources?
32 | - How does the framework integrate individual micro-apps?
33 | - How to ensure that sub-applications are isolated from one another (development independence and deployment independence) and runtime sandboxed?
34 | - Performance issues? What about public dependencies?
35 | - The list goes on long ...
36 |
37 | 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.
38 |
39 | **TLDR: Qiankun is probably the most complete micro-frontend solution you ever met🧐.**
40 |
41 | ## :sparkles: Features
42 |
43 | Qiankun inherits many benefits from [single-spa](https://github.com/CanopyTax/single-spa):
44 |
45 | - 📦 **Micro-apps Independent Deployment**
46 | - 🛴 **Lazy Load**
47 | - 📱 **Technology Agnostic**
48 |
49 | And on top of these, it offers:
50 |
51 | - 💃 **Elegant API**
52 | - 💪 **HTML Entry Access Mode**
53 | - 🛡 **Style Isolation**
54 | - 🧳 **JS Sandbox**
55 | - ⚡ **Prefetch Assets**
56 | - 🔌 **[Umi Plugin](https://github.com/umijs/plugins/tree/master/packages/plugin-qiankun) Integration**
57 |
58 | ## 📦 Installation
59 |
60 | ```shell
61 | $ yarn add qiankun # or npm i qiankun
62 | ```
63 |
64 | ## 📖 Documentation
65 |
66 | You can find the Qiankun documentation [on the website](https://qiankun.umijs.org/)
67 |
68 | Check out the [Getting Started](https://qiankun.umijs.org/guide/getting-started) page for a quick overview.
69 |
70 | The documentation is divided into several sections:
71 |
72 | - [Tutorial](https://qiankun.umijs.org/cookbook)
73 | - [API Reference](https://qiankun.umijs.org/api)
74 | - [FAQ](https://qiankun.umijs.org/faq)
75 | - [Community](https://qiankun.umijs.org/#-community)
76 |
77 | ## 💿 Examples
78 |
79 | Inside the `examples` folder, there is a sample Shell app and multiple mounted Micro FE apps. To get it running, first clone `qiankun`:
80 |
81 | ```shell
82 | $ git clone https://github.com/umijs/qiankun.git
83 | $ cd qiankun
84 | ```
85 |
86 | Now install and run the example:
87 |
88 | ```shell
89 | $ yarn install
90 | $ yarn examples:install
91 | $ yarn examples:start
92 | ```
93 |
94 | Visit `http://localhost:7099`.
95 |
96 | 
97 |
98 | ## 🎯 Roadmap
99 |
100 | See [Qiankun 3.0 Roadmap](https://github.com/umijs/qiankun/discussions/1378)
101 |
102 | ## 👥 Contributors
103 |
104 | Thanks to all the contributors!
105 |
106 |
107 |
108 |
109 |
110 | ## 🎁 Acknowledgements
111 |
112 | - [single-spa](https://github.com/CanopyTax/single-spa) What an awesome meta-framework for micro-frontends!
113 | - [import-html-entry](https://github.com/kuitos/import-html-entry/) An assets loader that supports html entry.
114 |
115 | ## 📄 License
116 |
117 | Qiankun is [MIT licensed](./LICENSE).
118 |
--------------------------------------------------------------------------------
/contributors.json:
--------------------------------------------------------------------------------
1 | ["kuitos", "gongshun", "aladdin-add"]
2 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: qiankun
3 | hero:
4 | title: qiankun
5 | desc: Probably the most complete micro-frontends solution you ever met🧐
6 | actions:
7 | - text: Get Started →
8 | link: /guide
9 | features:
10 | - title: Simple
11 | desc: Works with any javascript framework. Build your micro-frontend system just like using with iframe, but not iframe actually.
12 | - title: Complete
13 | desc: Includes almost all the basic capabilities required to build a micro-frontend system, such as style isolation, js sandbox, preloading, and so on.
14 | - title: Production-Ready
15 | desc: Had been extensively tested and polished by a large number of online applications both inside and outside of Ant Financial, the robustness is trustworthy.
16 | footer: MIT Licensed | Copyright © 2019-present
Powered by [dumi](https://d.umijs.org)
17 | ---
18 |
19 | ## 📦 Installation
20 |
21 | ```shell
22 | $ yarn add qiankun # or npm i qiankun -S
23 | ```
24 |
25 | ## 🔨 Getting Started
26 |
27 | ```tsx
28 | import { loadMicroApp } from 'qiankun';
29 |
30 | // load micro app
31 | loadMicroApp({
32 | name: 'reactApp',
33 | entry: '//localhost:7100',
34 | container: '#container',
35 | props: {
36 | slogan: 'Hello Qiankun',
37 | },
38 | });
39 | ```
40 |
41 | See details:[Getting Started](/guide/getting-started)
42 |
43 | ## 👬 Community
44 |
45 | | Github Discussions | DingTalk Group | WeChat Group |
46 | | --- | --- | --- |
47 | | [qiankun discussions](https://github.com/umijs/qiankun/discussions) |
| [view group QR code](https://github.com/umijs/qiankun/discussions/2343) |
48 |
--------------------------------------------------------------------------------
/docs/README.zh.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: qiankun
3 | hero:
4 | title: qiankun
5 | desc: 可能是你见过最完善的微前端解决方案🧐
6 | actions:
7 | - text: 快速开始 →
8 | link: /zh/guide
9 | features:
10 | - title: 简单
11 | desc: 任意 js 框架均可使用。微应用接入像使用接入一个 iframe 系统一样简单,但实际不是 iframe。
12 | - title: 完备
13 | desc: 几乎包含所有构建微前端系统时所需要的基本能力,如 样式隔离、js 沙箱、预加载等。
14 | - title: 生产可用
15 | desc: 已在蚂蚁内外经受过足够大量的线上系统的考验及打磨,健壮性值得信赖。
16 | footer: MIT Licensed | Copyright © 2019-present
Powered by [dumi](https://d.umijs.org)
17 | ---
18 |
19 | ## 📦 安装
20 |
21 | ```shell
22 | $ yarn add qiankun # or npm i qiankun -S
23 | ```
24 |
25 | ## 🔨 使用
26 |
27 | ```tsx
28 | import { loadMicroApp } from 'qiankun';
29 |
30 | // 加载微应用
31 | loadMicroApp({
32 | name: 'reactApp',
33 | entry: '//localhost:7100',
34 | container: '#container',
35 | props: {
36 | slogan: 'Hello Qiankun',
37 | },
38 | });
39 | ```
40 |
41 | 参考:[快速上手](/zh/guide/getting-started)。
42 |
43 | ## 👬 社区
44 |
45 | | Github Discussions | 钉钉群 | 微信群 |
46 | | --- | --- | --- |
47 | | [qiankun discussions](https://github.com/umijs/qiankun/discussions) |
| [点击查看群二维码](https://github.com/umijs/qiankun/discussions/2343) |
48 |
--------------------------------------------------------------------------------
/docs/guide/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | nav:
3 | order: 0
4 | toc: menu
5 | ---
6 |
7 | # Introduction
8 |
9 | Qiankun is an implementation of [Micro Frontends](https://micro-frontends.org/), which based on [single-spa](https://github.com/CanopyTax/single-spa). It aims to make it easier and painless to build a production-ready microfront-end architecture system.
10 |
11 | Qiankun hatched from [Ant Financial](https://en.wikipedia.org/wiki/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.
12 |
13 | 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.
14 |
15 | ## What Are Micro FrontEnds
16 |
17 | > Techniques, strategies and recipes for building a **modern web app** with **multiple teams** that can **ship features independently**. -- [Micro Frontends](https://micro-frontends.org/)
18 |
19 | Micro Frontends architecture has the following core values:
20 |
21 | - Technology Agnostic
22 |
23 | The main framework does not restrict access to the technology stack of the application, and the sub-applications have full autonomy.
24 |
25 | - Independent Development and Deployment
26 |
27 | The sub application repo is independent, and the frontend and backend can be independently developed. After deployment, the main framework can be updated automatically.
28 |
29 | - Incremental Upgrade
30 |
31 | 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.
32 |
33 | - Isolated Runtime
34 |
35 | State is isolated between each subapplication and no shared runtime state.
36 |
37 | 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](https://www.youtube.com/watch?v=pU1gXA0rfwc) then becomes unmaintainable. Such a problem is especially common in enterprise web applications.
38 |
39 | For more related introductions about micro frontends, I recommend that you check out these articles:
40 |
41 | - [Micro Frontends](https://micro-frontends.org/)
42 | - [Micro Frontends from martinfowler.com](https://martinfowler.com/articles/micro-frontends.html)
43 |
44 | ## Core Design Philosophy Of qiankun
45 |
46 | - 🥄 Simple
47 |
48 | 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.
49 |
50 | - 🍡 Decoupling/Technology Agnostic
51 |
52 | 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.
53 |
54 | ## How Does Qiankun Works
55 |
56 | TODO
57 |
58 | ## Why Not Iframe
59 |
60 | Check this article [Why Not Iframe](https://www.yuque.com/kuitos/gky7yw/gesexv)
61 |
62 | ## Features
63 |
64 | - 📦 **Based On [single-spa](https://github.com/CanopyTax/single-spa)** , provide a more out-of-box APIs.
65 | - 📱 **Technology Agnostic**,any javascript framework can use/integrate, whether React/Vue/Angular/JQuery or the others.
66 | - 💪 **HTML Entry access mode**, allows you to access the son as simple application like use the iframe.
67 | - 🛡 **Style Isolation**, make sure styles don't interfere with each other.
68 | - 🧳 **JS Sandbox**, ensure that global variables/events do not conflict between sub-applications.
69 | - ⚡ **Prefetch Assets**, prefetch unopened sub-application assets during the browser idle time to speed up the sub-application opening speed.
70 | - 🔌 **Umi Plugin**, [@umijs/plugin-qiankun](https://github.com/umijs/plugins/tree/master/packages/plugin-qiankun) is provided for umi applications to switch to a micro frontends architecture system with one line code.
71 |
--------------------------------------------------------------------------------
/docs/guide/README.zh.md:
--------------------------------------------------------------------------------
1 | ---
2 | nav:
3 | title: 指南
4 | order: 0
5 | toc: menu
6 | ---
7 |
8 | # 介绍
9 |
10 | qiankun 是一个基于 [single-spa](https://github.com/CanopyTax/single-spa) 的[微前端](https://micro-frontends.org/)实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。
11 |
12 | qiankun 孵化自蚂蚁金融科技基于微前端架构的云产品统一接入平台,在经过一批线上应用的充分检验及打磨后,我们将其微前端内核抽取出来并开源,希望能同时帮助社区有类似需求的系统更方便的构建自己的微前端系统,同时也希望通过社区的帮助将 qiankun 打磨的更加成熟完善。
13 |
14 | 目前 qiankun 已在蚂蚁内部服务了超过 2000+ 线上应用,在易用性及完备性上,绝对是值得信赖的。
15 |
16 | ## 什么是微前端
17 |
18 | > Techniques, strategies and recipes for building a **modern web app** with **multiple teams** that can **ship features independently**. -- [Micro Frontends](https://micro-frontends.org/)
19 | >
20 | > 微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略。
21 |
22 | 微前端架构具备以下几个核心价值:
23 |
24 | - 技术栈无关
25 | 主框架不限制接入应用的技术栈,微应用具备完全自主权
26 |
27 | - 独立开发、独立部署
28 | 微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新
29 |
30 | - 增量升级
31 |
32 | 在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略
33 |
34 | - 独立运行时
35 | 每个微应用之间状态隔离,运行时状态不共享
36 |
37 | 微前端架构旨在解决单体应用在一个相对长的时间跨度下,由于参与的人员、团队的增多、变迁,从一个普通应用演变成一个巨石应用([Frontend Monolith](https://www.youtube.com/watch?v=pU1gXA0rfwc))后,随之而来的应用不可维护的问题。这类问题在企业级 Web 应用中尤其常见。
38 |
39 | 更多关于微前端的相关介绍,推荐大家可以去看这几篇文章:
40 |
41 | - [Micro Frontends](https://micro-frontends.org/)
42 | - [Micro Frontends from martinfowler.com](https://martinfowler.com/articles/micro-frontends.html)
43 | - [可能是你见过最完善的微前端解决方案](https://zhuanlan.zhihu.com/p/78362028)
44 | - [微前端的核心价值](https://zhuanlan.zhihu.com/p/95085796)
45 |
46 | ## qiankun 的核心设计理念
47 |
48 | - 🥄 简单
49 |
50 | 由于主应用微应用都能做到技术栈无关,qiankun 对于用户而言只是一个类似 jQuery 的库,你需要调用几个 qiankun 的 API 即可完成应用的微前端改造。同时由于 qiankun 的 HTML entry 及沙箱的设计,使得微应用的接入像使用 iframe 一样简单。
51 |
52 | - 🍡 解耦/技术栈无关
53 |
54 | 微前端的核心目标是将巨石应用拆解成若干可以自治的松耦合微应用,而 qiankun 的诸多设计均是秉持这一原则,如 HTML entry、沙箱、应用间通信等。这样才能确保微应用真正具备 独立开发、独立运行 的能力。
55 |
56 | ## 它是如何工作的
57 |
58 | TODO
59 |
60 | ## 为什么不是 iframe
61 |
62 | 看这里 [Why Not Iframe](https://www.yuque.com/kuitos/gky7yw/gesexv)
63 |
64 | ## 特性
65 |
66 | - 📦 **基于 [single-spa](https://github.com/CanopyTax/single-spa)** 封装,提供了更加开箱即用的 API。
67 | - 📱 **技术栈无关**,任意技术栈的应用均可 使用/接入,不论是 React/Vue/Angular/JQuery 还是其他等框架。
68 | - 💪 **HTML Entry 接入方式**,让你接入微应用像使用 iframe 一样简单。
69 | - 🛡 **样式隔离**,确保微应用之间样式互相不干扰。
70 | - 🧳 **JS 沙箱**,确保微应用之间 全局变量/事件 不冲突。
71 | - ⚡️ **资源预加载**,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。
72 | - 🔌 **umi 插件**,提供了 [@umijs/plugin-qiankun](https://github.com/umijs/plugins/tree/master/packages/plugin-qiankun) 供 umi 应用一键切换成微前端架构系统。
73 |
--------------------------------------------------------------------------------
/docs/guide/getting-started.md:
--------------------------------------------------------------------------------
1 | ---
2 | toc: menu
3 | ---
4 |
5 | # Getting Started
6 |
7 | ## Master Application
8 |
9 | ### 1. Installation
10 |
11 | ```bash
12 | $ yarn add qiankun # or npm i qiankun -S
13 | ```
14 |
15 | ### 2. Register Sub Apps In Master Application
16 |
17 | ```ts
18 | import { registerMicroApps, start } from 'qiankun';
19 |
20 | registerMicroApps([
21 | {
22 | name: 'react app', // app name registered
23 | entry: '//localhost:7100',
24 | container: '#yourContainer',
25 | activeRule: '/yourActiveRule',
26 | },
27 | {
28 | name: 'vue app',
29 | entry: { scripts: ['//localhost:7100/main.js'] },
30 | container: '#yourContainer2',
31 | activeRule: '/yourActiveRule2',
32 | },
33 | ]);
34 |
35 | start();
36 | ```
37 |
38 | After the sub-application information is registered, the matching logic of the qiankun will be automatically triggered once the browser url changes, and all the render methods corresponding to the subapplications whose activeRule methods returns `true` will be called, at the same time the subapplications' exposed lifecycle hooks will be called in turn.
39 |
40 | ## Sub Application
41 |
42 | Sub applications do not need to install any additional dependencies to integrate to qiankun master application.
43 |
44 | ### 1. Exports Lifecycles From Sub App Entry
45 |
46 | The child application needs to export `bootstrap`,`mount`, `unmount` three lifecycle hooks in its own entry js (usually the entry js of webpack you configure) for the main application to call at the appropriate time.
47 |
48 | ```jsx
49 | /**
50 | * The bootstrap will only be called once when the child application is initialized.
51 | * The next time the child application re-enters, the mount hook will be called directly, and bootstrap will not be triggered repeatedly.
52 | * Usually we can do some initialization of global variables here,
53 | * such as application-level caches that will not be destroyed during the unmount phase.
54 | */
55 | export async function bootstrap() {
56 | console.log('react app bootstraped');
57 | }
58 |
59 | /**
60 | * The mount method is called every time the application enters,
61 | * usually we trigger the application's rendering method here.
62 | */
63 | export async function mount(props) {
64 | ReactDOM.render(, props.container ? props.container.querySelector('#root') : document.getElementById('root'));
65 | }
66 |
67 | /**
68 | * Methods that are called each time the application is switched/unloaded,
69 | * usually in this case we uninstall the application instance of the subapplication.
70 | */
71 | export async function unmount(props) {
72 | ReactDOM.unmountComponentAtNode(
73 | props.container ? props.container.querySelector('#root') : document.getElementById('root'),
74 | );
75 | }
76 |
77 | /**
78 | * Optional lifecycle,just available with loadMicroApp way
79 | */
80 | export async function update(props) {
81 | console.log('update props', props);
82 | }
83 | ```
84 |
85 | As qiankun based on single-spa, you can find more documentation about the sub-application lifecycle [here](https://single-spa.js.org/docs/building-applications.html#registered-application-lifecycle).
86 |
87 | Refer to [example without bundler](/guide/tutorial#micro-app-built-without-webpack)
88 |
89 | ### 2. Config Sub App Bundler
90 |
91 | In addition to exposing the corresponding life-cycle hooks in the code, in order for the main application to correctly identify some of the information exposed by the sub-application, the sub-application bundler needs to add the following configuration:
92 |
93 | #### webpack:
94 |
95 | If using Webpack v5:
96 | ```js
97 | const packageName = require('./package.json').name;
98 |
99 | module.exports = {
100 | output: {
101 | library: `${packageName}-[name]`,
102 | libraryTarget: 'umd',
103 | chunkLoadingGlobal: `webpackJsonp_${packageName}`,
104 | },
105 | };
106 | ```
107 |
108 | If using Webpack v4:
109 | ```js
110 | const packageName = require('./package.json').name;
111 |
112 | module.exports = {
113 | output: {
114 | library: `${packageName}-[name]`,
115 | libraryTarget: 'umd',
116 | jsonpFunction: `webpackJsonp_${packageName}`,
117 | },
118 | };
119 | ```
120 |
121 | You can check the configuration description from [webpack doc](https://webpack.js.org/configuration/output/#outputlibrary)。
122 |
--------------------------------------------------------------------------------
/docs/guide/getting-started.zh.md:
--------------------------------------------------------------------------------
1 | ---
2 | toc: menu
3 | ---
4 |
5 | # 快速上手
6 |
7 | ## 主应用
8 |
9 | ### 1. 安装 qiankun
10 |
11 | ```bash
12 | $ yarn add qiankun # 或者 npm i qiankun -S
13 | ```
14 |
15 | ### 2. 在主应用中注册微应用
16 |
17 | ```ts
18 | import { registerMicroApps, start } from 'qiankun';
19 |
20 | registerMicroApps([
21 | {
22 | name: 'react app', // app name registered
23 | entry: '//localhost:7100',
24 | container: '#yourContainer',
25 | activeRule: '/yourActiveRule',
26 | },
27 | {
28 | name: 'vue app',
29 | entry: { scripts: ['//localhost:7100/main.js'] },
30 | container: '#yourContainer2',
31 | activeRule: '/yourActiveRule2',
32 | },
33 | ]);
34 |
35 | start();
36 | ```
37 |
38 | 当微应用信息注册完之后,一旦浏览器的 url 发生变化,便会自动触发 qiankun 的匹配逻辑,所有 activeRule 规则匹配上的微应用就会被插入到指定的 container 中,同时依次调用微应用暴露出的生命周期钩子。
39 |
40 | 如果微应用不是直接跟路由关联的时候,你也可以选择手动加载微应用的方式:
41 |
42 | ```ts
43 | import { loadMicroApp } from 'qiankun';
44 |
45 | loadMicroApp({
46 | name: 'app',
47 | entry: '//localhost:7100',
48 | container: '#yourContainer',
49 | });
50 | ```
51 |
52 | ## 微应用
53 |
54 | 微应用不需要额外安装任何其他依赖即可接入 qiankun 主应用。
55 |
56 | ### 1. 导出相应的生命周期钩子
57 |
58 | 微应用需要在自己的入口 js (通常就是你配置的 webpack 的 entry js) 导出 `bootstrap`、`mount`、`unmount` 三个生命周期钩子,以供主应用在适当的时机调用。
59 |
60 | ```jsx
61 | /**
62 | * bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
63 | * 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
64 | */
65 | export async function bootstrap() {
66 | console.log('react app bootstraped');
67 | }
68 |
69 | /**
70 | * 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
71 | */
72 | export async function mount(props) {
73 | ReactDOM.render(, props.container ? props.container.querySelector('#root') : document.getElementById('root'));
74 | }
75 |
76 | /**
77 | * 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
78 | */
79 | export async function unmount(props) {
80 | ReactDOM.unmountComponentAtNode(
81 | props.container ? props.container.querySelector('#root') : document.getElementById('root'),
82 | );
83 | }
84 |
85 | /**
86 | * 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效
87 | */
88 | export async function update(props) {
89 | console.log('update props', props);
90 | }
91 | ```
92 |
93 | qiankun 基于 single-spa,所以你可以在[这里](https://single-spa.js.org/docs/building-applications.html#registered-application-lifecycle)找到更多关于微应用生命周期相关的文档说明。
94 |
95 | 无 webpack 等构建工具的应用接入方式请见[这里](/zh/guide/tutorial#%E9%9D%9E-webpack-%E6%9E%84%E5%BB%BA%E7%9A%84%E5%BE%AE%E5%BA%94%E7%94%A8)
96 |
97 | ### 2. 配置微应用的打包工具
98 |
99 | 除了代码中暴露出相应的生命周期钩子之外,为了让主应用能正确识别微应用暴露出来的一些信息,微应用的打包工具需要增加如下配置:
100 |
101 | #### webpack:
102 | webpack v5:
103 | ```js
104 | const packageName = require('./package.json').name;
105 |
106 | module.exports = {
107 | output: {
108 | library: `${packageName}-[name]`,
109 | libraryTarget: 'umd',
110 | chunkLoadingGlobal: `webpackJsonp_${packageName}`,
111 | },
112 | };
113 | ```
114 |
115 | webpack v4:
116 | ```js
117 | const packageName = require('./package.json').name;
118 |
119 | module.exports = {
120 | output: {
121 | library: `${packageName}-[name]`,
122 | libraryTarget: 'umd',
123 | jsonpFunction: `webpackJsonp_${packageName}`,
124 | },
125 | };
126 | ```
127 |
128 | 相关配置介绍可以查看 [webpack 相关文档](https://webpack.js.org/configuration/output/#outputlibrary)。
129 |
--------------------------------------------------------------------------------
/examples/angular9/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see https://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.md]
12 | max_line_length = off
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/examples/angular9/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # compiled output
4 | /dist
5 | /tmp
6 | /out-tsc
7 | # Only exists if Bazel was run
8 | /bazel-out
9 |
10 | # dependencies
11 | /node_modules
12 |
13 | # profiling files
14 | chrome-profiler-events*.json
15 | speed-measure-plugin*.json
16 |
17 | # IDEs and editors
18 | /.idea
19 | .project
20 | .classpath
21 | .c9/
22 | *.launch
23 | .settings/
24 | *.sublime-workspace
25 |
26 | # IDE - VSCode
27 | .vscode/*
28 | !.vscode/settings.json
29 | !.vscode/tasks.json
30 | !.vscode/launch.json
31 | !.vscode/extensions.json
32 | .history/*
33 |
34 | # misc
35 | /.sass-cache
36 | /connect.lock
37 | /coverage
38 | /libpeerconnection.log
39 | npm-debug.log
40 | yarn-error.log
41 | testem.log
42 | /typings
43 |
44 | # System Files
45 | .DS_Store
46 | Thumbs.db
47 |
--------------------------------------------------------------------------------
/examples/angular9/README.md:
--------------------------------------------------------------------------------
1 | # Angular9
2 |
3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 9.0.3.
4 |
5 | ## Development server
6 |
7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
8 |
9 | ## Code scaffolding
10 |
11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
12 |
13 | ## Build
14 |
15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
16 |
17 | ## Running unit tests
18 |
19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
20 |
21 | ## Running end-to-end tests
22 |
23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
24 |
25 | ## Further help
26 |
27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).
28 |
--------------------------------------------------------------------------------
/examples/angular9/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "version": 1,
4 | "cli": {
5 | "packageManager": "yarn"
6 | },
7 | "newProjectRoot": "projects",
8 | "projects": {
9 | "angular9": {
10 | "projectType": "application",
11 | "schematics": {},
12 | "root": "",
13 | "sourceRoot": "src",
14 | "prefix": "angular9",
15 | "architect": {
16 | "build": {
17 | "builder": "@angular-builders/custom-webpack:browser",
18 | "options": {
19 | "outputPath": "dist/angular9",
20 | "index": "src/index.html",
21 | "main": "src/main.qiankun.ts",
22 | "polyfills": "src/polyfills.ts",
23 | "tsConfig": "tsconfig.app.json",
24 | "aot": true,
25 | "assets": ["src/favicon.ico", "src/assets"],
26 | "styles": ["src/styles.css"],
27 | "scripts": [],
28 | "customWebpackConfig": {
29 | "path": "./extra-webpack.config.js"
30 | }
31 | },
32 | "configurations": {
33 | "production": {
34 | "fileReplacements": [
35 | {
36 | "replace": "src/environments/environment.ts",
37 | "with": "src/environments/environment.prod.ts"
38 | }
39 | ],
40 | "optimization": true,
41 | "outputHashing": "all",
42 | "sourceMap": false,
43 | "extractCss": true,
44 | "namedChunks": false,
45 | "extractLicenses": true,
46 | "vendorChunk": false,
47 | "buildOptimizer": true,
48 | "budgets": [
49 | {
50 | "type": "initial",
51 | "maximumWarning": "2mb",
52 | "maximumError": "5mb"
53 | },
54 | {
55 | "type": "anyComponentStyle",
56 | "maximumWarning": "6kb",
57 | "maximumError": "10kb"
58 | }
59 | ]
60 | }
61 | }
62 | },
63 | "serve": {
64 | "builder": "@angular-builders/custom-webpack:dev-server",
65 | "options": {
66 | "browserTarget": "angular9:build"
67 | },
68 | "configurations": {
69 | "production": {
70 | "browserTarget": "angular9:build:production"
71 | }
72 | }
73 | },
74 | "extract-i18n": {
75 | "builder": "@angular-devkit/build-angular:extract-i18n",
76 | "options": {
77 | "browserTarget": "angular9:build"
78 | }
79 | },
80 | "test": {
81 | "builder": "@angular-devkit/build-angular:karma",
82 | "options": {
83 | "main": "src/test.ts",
84 | "polyfills": "src/polyfills.ts",
85 | "tsConfig": "tsconfig.spec.json",
86 | "karmaConfig": "karma.conf.js",
87 | "assets": ["src/favicon.ico", "src/assets"],
88 | "styles": ["src/styles.css"],
89 | "scripts": []
90 | }
91 | },
92 | "lint": {
93 | "builder": "@angular-devkit/build-angular:tslint",
94 | "options": {
95 | "tsConfig": ["tsconfig.app.json", "tsconfig.spec.json", "e2e/tsconfig.json"],
96 | "exclude": ["**/node_modules/**"]
97 | }
98 | },
99 | "e2e": {
100 | "builder": "@angular-devkit/build-angular:protractor",
101 | "options": {
102 | "protractorConfig": "e2e/protractor.conf.js",
103 | "devServerTarget": "angular9:serve"
104 | },
105 | "configurations": {
106 | "production": {
107 | "devServerTarget": "angular9:serve:production"
108 | }
109 | }
110 | }
111 | }
112 | }
113 | },
114 | "defaultProject": "angular9"
115 | }
116 |
--------------------------------------------------------------------------------
/examples/angular9/browserslist:
--------------------------------------------------------------------------------
1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
2 | # For additional information regarding the format and rule options, please see:
3 | # https://github.com/browserslist/browserslist#queries
4 |
5 | # You can see what browsers were selected by your queries by running:
6 | # npx browserslist
7 |
8 | > 0.5%
9 | last 2 versions
10 | Firefox ESR
11 | not dead
12 | not IE 9-11 # For IE 9-11 support, remove 'not'.
--------------------------------------------------------------------------------
/examples/angular9/e2e/protractor.conf.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | // Protractor configuration file, see link for more information
3 | // https://github.com/angular/protractor/blob/master/lib/config.ts
4 |
5 | const { SpecReporter } = require('jasmine-spec-reporter');
6 |
7 | /**
8 | * @type { import("protractor").Config }
9 | */
10 | exports.config = {
11 | allScriptsTimeout: 11000,
12 | specs: ['./src/**/*.e2e-spec.ts'],
13 | capabilities: {
14 | browserName: 'chrome',
15 | },
16 | directConnect: true,
17 | baseUrl: 'http://localhost:7103/',
18 | framework: 'jasmine',
19 | jasmineNodeOpts: {
20 | showColors: true,
21 | defaultTimeoutInterval: 30000,
22 | print: function() {},
23 | },
24 | onPrepare() {
25 | require('ts-node').register({
26 | project: require('path').join(__dirname, './tsconfig.json'),
27 | });
28 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
29 | },
30 | };
31 |
--------------------------------------------------------------------------------
/examples/angular9/e2e/src/app.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { AppPage } from './app.po';
2 | import { browser, logging } from 'protractor';
3 |
4 | describe('workspace-project App', () => {
5 | let page: AppPage;
6 |
7 | beforeEach(() => {
8 | page = new AppPage();
9 | });
10 |
11 | it('should display welcome message', () => {
12 | page.navigateTo();
13 | expect(page.getTitleText()).toEqual('angular9 app is running!');
14 | });
15 |
16 | afterEach(async () => {
17 | // Assert that there are no errors emitted from the browser
18 | const logs = await browser
19 | .manage()
20 | .logs()
21 | .get(logging.Type.BROWSER);
22 | expect(logs).not.toContain(
23 | jasmine.objectContaining({
24 | level: logging.Level.SEVERE,
25 | } as logging.Entry),
26 | );
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/examples/angular9/e2e/src/app.po.ts:
--------------------------------------------------------------------------------
1 | import { browser, by, element } from 'protractor';
2 |
3 | export class AppPage {
4 | navigateTo(): Promise {
5 | return browser.get(browser.baseUrl) as Promise;
6 | }
7 |
8 | getTitleText(): Promise {
9 | return element(by.css('app-root .content span')).getText() as Promise;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/examples/angular9/e2e/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/e2e",
5 | "module": "commonjs",
6 | "target": "es5",
7 | "types": ["jasmine", "jasminewd2", "node"]
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/examples/angular9/extra-webpack.config.js:
--------------------------------------------------------------------------------
1 | const singleSpaAngularWebpack = require('single-spa-angular/lib/webpack').default;
2 | const webpackMerge = require('webpack-merge');
3 | const { name } = require('./package');
4 |
5 | module.exports = (angularWebpackConfig, options) => {
6 | const singleSpaWebpackConfig = singleSpaAngularWebpack(angularWebpackConfig, options);
7 |
8 | const singleSpaConfig = {
9 | output: {
10 | library: `${name}-[name]`,
11 | libraryTarget: 'umd',
12 | },
13 | externals: {
14 | 'zone.js': 'Zone',
15 | },
16 | };
17 | const mergedConfig = webpackMerge.smart(singleSpaWebpackConfig, singleSpaConfig);
18 | return mergedConfig;
19 | };
20 |
--------------------------------------------------------------------------------
/examples/angular9/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration file, see link for more information
2 | // https://karma-runner.github.io/1.0/config/configuration-file.html
3 |
4 | module.exports = function(config) {
5 | config.set({
6 | basePath: '',
7 | frameworks: ['jasmine', '@angular-devkit/build-angular'],
8 | plugins: [
9 | require('karma-jasmine'),
10 | require('karma-chrome-launcher'),
11 | require('karma-jasmine-html-reporter'),
12 | require('karma-coverage-istanbul-reporter'),
13 | require('@angular-devkit/build-angular/plugins/karma'),
14 | ],
15 | client: {
16 | clearContext: false, // leave Jasmine Spec Runner output visible in browser
17 | },
18 | coverageIstanbulReporter: {
19 | dir: require('path').join(__dirname, './coverage/angular9'),
20 | reports: ['html', 'lcovonly', 'text-summary'],
21 | fixWebpackSourcePaths: true,
22 | },
23 | reporters: ['progress', 'kjhtml'],
24 | port: 9876,
25 | colors: true,
26 | logLevel: config.LOG_INFO,
27 | autoWatch: true,
28 | browsers: ['Chrome'],
29 | singleRun: false,
30 | restartOnFileChange: true,
31 | });
32 | };
33 |
--------------------------------------------------------------------------------
/examples/angular9/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular9",
3 | "version": "0.0.0",
4 | "scripts": {
5 | "ng": "ng",
6 | "start": "ng serve",
7 | "build": "ng build",
8 | "test": "ng test",
9 | "lint": "ng lint",
10 | "e2e": "ng e2e",
11 | "build:qiankun": "ng build --prod --deploy-url http://localhost:7103/",
12 | "serve:qiankun": "ng serve --disable-host-check --port 7103 --base-href /angular9 --live-reload false"
13 | },
14 | "private": true,
15 | "dependencies": {
16 | "@angular-builders/custom-webpack": "^9.2.0",
17 | "@angular/animations": "~9.0.2",
18 | "@angular/common": "~9.0.2",
19 | "@angular/compiler": "~9.0.2",
20 | "@angular/core": "~9.0.2",
21 | "@angular/forms": "~9.0.2",
22 | "@angular/platform-browser": "~9.0.2",
23 | "@angular/platform-browser-dynamic": "~9.0.2",
24 | "@angular/router": "~9.0.2",
25 | "rxjs": "~6.5.4",
26 | "single-spa-angular": "^4.3.1",
27 | "tslib": "^1.10.0",
28 | "zone.js": "~0.10.2"
29 | },
30 | "devDependencies": {
31 | "@angular-devkit/build-angular": "~0.900.3",
32 | "@angular/cli": "~9.0.3",
33 | "@angular/compiler-cli": "~9.0.2",
34 | "@angular/language-service": "~9.0.2",
35 | "@types/jasmine": "~3.5.0",
36 | "@types/jasminewd2": "~2.0.3",
37 | "@types/node": "^12.11.1",
38 | "codelyzer": "^5.1.2",
39 | "jasmine-core": "~3.5.0",
40 | "jasmine-spec-reporter": "~4.2.1",
41 | "karma": "~4.3.0",
42 | "karma-chrome-launcher": "~3.1.0",
43 | "karma-coverage-istanbul-reporter": "~2.1.0",
44 | "karma-jasmine": "~2.0.1",
45 | "karma-jasmine-html-reporter": "^1.4.2",
46 | "protractor": "~5.4.3",
47 | "ts-node": "~8.3.0",
48 | "tslint": "~5.18.0",
49 | "typescript": "~3.7.5"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/examples/angular9/src/app/app-routing.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { Routes, RouterModule } from '@angular/router';
3 |
4 | const routes: Routes = [];
5 |
6 | @NgModule({
7 | imports: [RouterModule.forRoot(routes)],
8 | exports: [RouterModule],
9 | })
10 | export class AppRoutingModule {}
11 |
--------------------------------------------------------------------------------
/examples/angular9/src/app/app.component.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umijs/qiankun/88c658395a83a9eca36b5b6560a5ef879cc42fa1/examples/angular9/src/app/app.component.css
--------------------------------------------------------------------------------
/examples/angular9/src/app/app.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed, async } from '@angular/core/testing';
2 | import { RouterTestingModule } from '@angular/router/testing';
3 | import { AppComponent } from './app.component';
4 |
5 | describe('AppComponent', () => {
6 | beforeEach(async(() => {
7 | TestBed.configureTestingModule({
8 | imports: [RouterTestingModule],
9 | declarations: [AppComponent],
10 | }).compileComponents();
11 | }));
12 |
13 | it('should create the app', () => {
14 | const fixture = TestBed.createComponent(AppComponent);
15 | const app = fixture.componentInstance;
16 | expect(app).toBeTruthy();
17 | });
18 |
19 | it(`should have as title 'angular9'`, () => {
20 | const fixture = TestBed.createComponent(AppComponent);
21 | const app = fixture.componentInstance;
22 | expect(app.title).toEqual('angular9');
23 | });
24 |
25 | it('should render title', () => {
26 | const fixture = TestBed.createComponent(AppComponent);
27 | fixture.detectChanges();
28 | const compiled = fixture.nativeElement;
29 | expect(compiled.querySelector('.content span').textContent).toContain('angular9 app is running!');
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/examples/angular9/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-root',
5 | templateUrl: './app.component.html',
6 | styleUrls: ['./app.component.css'],
7 | })
8 | export class AppComponent {
9 | title = 'angular9';
10 | }
11 |
--------------------------------------------------------------------------------
/examples/angular9/src/app/app.module.ts:
--------------------------------------------------------------------------------
1 | import { BrowserModule } from '@angular/platform-browser';
2 | import { NgModule } from '@angular/core';
3 |
4 | import { AppRoutingModule } from './app-routing.module';
5 | import { AppComponent } from './app.component';
6 |
7 | @NgModule({
8 | declarations: [AppComponent],
9 | imports: [BrowserModule, AppRoutingModule],
10 | providers: [],
11 | bootstrap: [AppComponent],
12 | })
13 | export class AppModule {}
14 |
--------------------------------------------------------------------------------
/examples/angular9/src/app/empty-route/empty-route.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'angular9-empty-route',
5 | template: '',
6 | })
7 | export class EmptyRouteComponent {}
8 |
--------------------------------------------------------------------------------
/examples/angular9/src/assets/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umijs/qiankun/88c658395a83a9eca36b5b6560a5ef879cc42fa1/examples/angular9/src/assets/.gitkeep
--------------------------------------------------------------------------------
/examples/angular9/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: true,
3 | };
4 |
--------------------------------------------------------------------------------
/examples/angular9/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | // This file can be replaced during build by using the `fileReplacements` array.
2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
3 | // The list of file replacements can be found in `angular.json`.
4 |
5 | export const environment = {
6 | production: false,
7 | };
8 |
9 | /*
10 | * For easier debugging in development mode, you can import the following file
11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
12 | *
13 | * This import should be commented out in production mode because it will have a negative impact
14 | * on performance if an error is thrown.
15 | */
16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI.
17 |
--------------------------------------------------------------------------------
/examples/angular9/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umijs/qiankun/88c658395a83a9eca36b5b6560a5ef879cc42fa1/examples/angular9/src/favicon.ico
--------------------------------------------------------------------------------
/examples/angular9/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | qiankun-angular9
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/examples/angular9/src/main.qiankun.ts:
--------------------------------------------------------------------------------
1 | import { enableProdMode, NgZone } from '@angular/core';
2 |
3 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
4 | import { Router } from '@angular/router';
5 | import { AppModule } from './app/app.module';
6 | import { environment } from './environments/environment';
7 | import { singleSpaAngular, getSingleSpaExtraProviders } from 'single-spa-angular';
8 | import { singleSpaPropsSubject } from './single-spa/single-spa-props';
9 |
10 | if (environment.production) {
11 | enableProdMode();
12 | }
13 |
14 | if (!(window as any).__POWERED_BY_QIANKUN__) {
15 | platformBrowserDynamic()
16 | .bootstrapModule(AppModule)
17 | .catch(err => console.error(err));
18 | }
19 |
20 | const { bootstrap, mount, unmount } = singleSpaAngular({
21 | bootstrapFunction: singleSpaProps => {
22 | singleSpaPropsSubject.next(singleSpaProps);
23 | return platformBrowserDynamic(getSingleSpaExtraProviders()).bootstrapModule(AppModule);
24 | },
25 | template: '',
26 | Router,
27 | NgZone: NgZone,
28 | });
29 |
30 | export { bootstrap, mount, unmount };
31 |
--------------------------------------------------------------------------------
/examples/angular9/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file includes polyfills needed by Angular and is loaded before the app.
3 | * You can add your own extra polyfills to this file.
4 | *
5 | * This file is divided into 2 sections:
6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
8 | * file.
9 | *
10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
13 | *
14 | * Learn more in https://angular.io/guide/browser-support
15 | */
16 |
17 | /***************************************************************************************************
18 | * BROWSER POLYFILLS
19 | */
20 |
21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */
22 | // import 'classlist.js'; // Run `npm install --save classlist.js`.
23 |
24 | /**
25 | * Web Animations `@angular/platform-browser/animations`
26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
28 | */
29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`.
30 |
31 | /**
32 | * By default, zone.js will patch all possible macroTask and DomEvents
33 | * user can disable parts of macroTask/DomEvents patch by setting following flags
34 | * because those flags need to be set before `zone.js` being loaded, and webpack
35 | * will put import in the top of bundle, so user need to create a separate file
36 | * in this directory (for example: zone-flags.ts), and put the following flags
37 | * into that file, and then add the following code before importing zone.js.
38 | * import './zone-flags';
39 | *
40 | * The flags allowed in zone-flags.ts are listed here.
41 | *
42 | * The following flags will work for all browsers.
43 | *
44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
47 | *
48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge
50 | *
51 | * (window as any).__Zone_enable_cross_context_check = true;
52 | *
53 | */
54 |
55 | /***************************************************************************************************
56 | * Zone JS is required by default for Angular itself.
57 | */
58 | import 'zone.js/dist/zone'; // Included with Angular CLI.
59 |
60 | /***************************************************************************************************
61 | * APPLICATION IMPORTS
62 | */
63 |
--------------------------------------------------------------------------------
/examples/angular9/src/single-spa/asset-url.ts:
--------------------------------------------------------------------------------
1 | // In single-spa, the assets need to be loaded from a dynamic location,
2 | // instead of hard coded to `/assets`. We use webpack public path for this.
3 | // See https://webpack.js.org/guides/public-path/#root
4 |
5 | export function assetUrl(url: string): string {
6 | // @ts-ignore
7 | const publicPath = __webpack_public_path__;
8 | const publicPathSuffix = publicPath.endsWith('/') ? '' : '/';
9 | const urlPrefix = url.startsWith('/') ? '' : '/';
10 |
11 | return `${publicPath}${publicPathSuffix}assets${urlPrefix}${url}`;
12 | }
13 |
--------------------------------------------------------------------------------
/examples/angular9/src/single-spa/single-spa-props.ts:
--------------------------------------------------------------------------------
1 | import { ReplaySubject } from 'rxjs';
2 | import { AppProps } from 'single-spa';
3 |
4 | export const singleSpaPropsSubject = new ReplaySubject(1);
5 |
6 | // Add any custom single-spa props you have to this type def
7 | // https://single-spa.js.org/docs/building-applications.html#custom-props
8 | export type SingleSpaProps = AppProps & {};
9 |
--------------------------------------------------------------------------------
/examples/angular9/src/styles.css:
--------------------------------------------------------------------------------
1 | /* You can add global styles to this file, and also import other style files */
2 |
--------------------------------------------------------------------------------
/examples/angular9/src/test.ts:
--------------------------------------------------------------------------------
1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files
2 |
3 | import 'zone.js/dist/zone-testing';
4 | import { getTestBed } from '@angular/core/testing';
5 | import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing';
6 |
7 | declare const require: {
8 | context(
9 | path: string,
10 | deep?: boolean,
11 | filter?: RegExp,
12 | ): {
13 | keys(): string[];
14 | (id: string): T;
15 | };
16 | };
17 |
18 | // First, initialize the Angular testing environment.
19 | getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());
20 | // Then we find all the tests.
21 | const context = require.context('./', true, /\.spec\.ts$/);
22 | // And load the modules.
23 | context.keys().map(context);
24 |
--------------------------------------------------------------------------------
/examples/angular9/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "./out-tsc/app",
5 | "types": []
6 | },
7 | "files": ["src/main.qiankun.ts", "src/polyfills.ts"],
8 | "include": ["src/**/*.d.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/examples/angular9/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "baseUrl": "./",
5 | "outDir": "./dist/out-tsc",
6 | "sourceMap": true,
7 | "declaration": false,
8 | "downlevelIteration": true,
9 | "experimentalDecorators": true,
10 | "module": "esnext",
11 | "moduleResolution": "node",
12 | "importHelpers": true,
13 | "target": "es2015",
14 | "typeRoots": ["node_modules/@types"],
15 | "lib": ["es2018", "dom"]
16 | },
17 | "angularCompilerOptions": {
18 | "fullTemplateTypeCheck": true,
19 | "strictInjectionParameters": true
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/examples/angular9/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "./out-tsc/spec",
5 | "types": ["jasmine", "node"]
6 | },
7 | "files": ["src/test.ts", "src/polyfills.ts"],
8 | "include": ["src/**/*.spec.ts", "src/**/*.d.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/examples/angular9/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tslint:recommended",
3 | "rules": {
4 | "array-type": false,
5 | "arrow-parens": false,
6 | "deprecation": {
7 | "severity": "warning"
8 | },
9 | "component-class-suffix": true,
10 | "contextual-lifecycle": true,
11 | "directive-class-suffix": true,
12 | "directive-selector": [true, "attribute", "angular9", "camelCase"],
13 | "component-selector": [true, "element", "angular9", "kebab-case"],
14 | "import-blacklist": [true, "rxjs/Rx"],
15 | "interface-name": false,
16 | "max-classes-per-file": false,
17 | "max-line-length": [true, 140],
18 | "member-access": false,
19 | "member-ordering": [
20 | true,
21 | {
22 | "order": ["static-field", "instance-field", "static-method", "instance-method"]
23 | }
24 | ],
25 | "no-consecutive-blank-lines": false,
26 | "no-console": [true, "debug", "info", "time", "timeEnd", "trace"],
27 | "no-empty": false,
28 | "no-inferrable-types": [true, "ignore-params"],
29 | "no-non-null-assertion": true,
30 | "no-redundant-jsdoc": true,
31 | "no-switch-case-fall-through": true,
32 | "no-var-requires": false,
33 | "object-literal-key-quotes": [true, "as-needed"],
34 | "object-literal-sort-keys": false,
35 | "ordered-imports": false,
36 | "quotemark": [true, "single"],
37 | "trailing-comma": false,
38 | "no-conflicting-lifecycle": true,
39 | "no-host-metadata-property": true,
40 | "no-input-rename": true,
41 | "no-inputs-metadata-property": true,
42 | "no-output-native": true,
43 | "no-output-on-prefix": true,
44 | "no-output-rename": true,
45 | "no-outputs-metadata-property": true,
46 | "template-banana-in-box": true,
47 | "template-no-negated-async": true,
48 | "use-lifecycle-interface": true,
49 | "use-pipe-transform-interface": true
50 | },
51 | "rulesDirectory": ["codelyzer"]
52 | }
53 |
--------------------------------------------------------------------------------
/examples/example.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umijs/qiankun/88c658395a83a9eca36b5b6560a5ef879cc42fa1/examples/example.gif
--------------------------------------------------------------------------------
/examples/main/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | QianKun Example
7 |
8 |
9 |
10 |
11 |
12 |
15 |
16 |
17 |
18 | - React16
19 | - React15
20 | - Vue
21 | - Vue3
22 | - Angular9
23 | - Purehtml
24 |
25 |
26 |
27 |
28 |
29 |
30 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/examples/main/index.js:
--------------------------------------------------------------------------------
1 | import 'zone.js'; // for angular subapp
2 | import { initGlobalState, registerMicroApps, runAfterFirstMounted, setDefaultMountApp, start } from '../../es';
3 | import './index.less';
4 | /**
5 | * 主应用 **可以使用任意技术栈**
6 | * 以下分别是 React 和 Vue 的示例,可切换尝试
7 | */
8 | import render from './render/ReactRender';
9 | // import render from './render/VueRender';
10 |
11 | /**
12 | * Step1 初始化应用(可选)
13 | */
14 | render({ loading: true });
15 |
16 | const loader = (loading) => render({ loading });
17 |
18 | /**
19 | * Step2 注册子应用
20 | */
21 |
22 | registerMicroApps(
23 | [
24 | {
25 | name: 'react16',
26 | entry: '//localhost:7100',
27 | container: '#subapp-viewport',
28 | loader,
29 | activeRule: '/react16',
30 | },
31 | {
32 | name: 'react15',
33 | entry: '//localhost:7102',
34 | container: '#subapp-viewport',
35 | loader,
36 | activeRule: '/react15',
37 | },
38 | {
39 | name: 'vue',
40 | entry: '//localhost:7101',
41 | container: '#subapp-viewport',
42 | loader,
43 | activeRule: '/vue',
44 | },
45 | {
46 | name: 'angular9',
47 | entry: '//localhost:7103',
48 | container: '#subapp-viewport',
49 | loader,
50 | activeRule: '/angular9',
51 | },
52 | {
53 | name: 'purehtml',
54 | entry: '//localhost:7104',
55 | container: '#subapp-viewport',
56 | loader,
57 | activeRule: '/purehtml',
58 | },
59 | {
60 | name: 'vue3',
61 | entry: '//localhost:7105',
62 | container: '#subapp-viewport',
63 | loader,
64 | activeRule: '/vue3',
65 | },
66 | ],
67 | {
68 | beforeLoad: [
69 | (app) => {
70 | console.log('[LifeCycle] before load %c%s', 'color: green;', app.name);
71 | },
72 | ],
73 | beforeMount: [
74 | (app) => {
75 | console.log('[LifeCycle] before mount %c%s', 'color: green;', app.name);
76 | },
77 | ],
78 | afterUnmount: [
79 | (app) => {
80 | console.log('[LifeCycle] after unmount %c%s', 'color: green;', app.name);
81 | },
82 | ],
83 | },
84 | );
85 |
86 | const { onGlobalStateChange, setGlobalState } = initGlobalState({
87 | user: 'qiankun',
88 | });
89 |
90 | onGlobalStateChange((value, prev) => console.log('[onGlobalStateChange - master]:', value, prev));
91 |
92 | setGlobalState({
93 | ignore: 'master',
94 | user: {
95 | name: 'master',
96 | },
97 | });
98 |
99 | /**
100 | * Step3 设置默认进入的子应用
101 | */
102 | setDefaultMountApp('/react16');
103 |
104 | /**
105 | * Step4 启动应用
106 | */
107 | start();
108 |
109 | runAfterFirstMounted(() => {
110 | console.log('[MainApp] first app mounted');
111 | });
112 |
--------------------------------------------------------------------------------
/examples/main/index.less:
--------------------------------------------------------------------------------
1 | // 主应用慎用 reset 样式
2 | body {
3 | margin: 0;
4 | }
5 |
6 | .mainapp {
7 | // 防止被子应用的样式覆盖
8 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Helvetica Neue, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;
9 | line-height: 1;
10 | }
11 |
12 | .mainapp-header {
13 | >h1 {
14 | color: #333;
15 | font-size: 36px;
16 | font-weight: 700;
17 | margin: 0;
18 | padding: 36px;
19 | }
20 | }
21 |
22 | .mainapp-main {
23 | display: flex;
24 |
25 | .mainapp-sidemenu {
26 | width: 130px;
27 | list-style: none;
28 | margin: 0;
29 | margin-left: 40px;
30 | padding: 0;
31 | border-right: 2px solid #aaa;
32 |
33 | >li {
34 | color: #aaa;
35 | margin: 20px 0;
36 | font-size: 18px;
37 | font-weight: 400;
38 | cursor: pointer;
39 |
40 | &:hover {
41 | color: #444;
42 | }
43 |
44 | &:first-child {
45 | margin-top: 5px;
46 | }
47 | }
48 | }
49 | }
50 |
51 | // 子应用区域
52 | #subapp-container {
53 | flex-grow: 1;
54 | position: relative;
55 | margin: 0 40px;
56 |
57 | .subapp-loading {
58 | color: #444;
59 | font-size: 28px;
60 | font-weight: 600;
61 | text-align: center;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/examples/main/multiple.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | qiankun multiple demo
6 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | react loading...
22 | vue loading...
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/examples/main/multiple.js:
--------------------------------------------------------------------------------
1 | import { loadMicroApp } from '../../es';
2 |
3 | let app;
4 |
5 | function mount() {
6 | app = loadMicroApp(
7 | { name: 'react15', entry: '//localhost:7102', container: '#react15' },
8 | { sandbox: { experimentalStyleIsolation: true } },
9 | );
10 | }
11 |
12 | function unmount() {
13 | app.unmount();
14 | }
15 |
16 | document.querySelector('#mount').addEventListener('click', mount);
17 | document.querySelector('#unmount').addEventListener('click', unmount);
18 |
19 | loadMicroApp({ name: 'vue', entry: '//localhost:7101', container: '#vue' });
20 |
--------------------------------------------------------------------------------
/examples/main/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "main",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "webpack-dev-server",
8 | "start:multiple": "cross-env MODE=multiple webpack-dev-server",
9 | "test": "echo \"Error: no test specified\" && exit 1"
10 | },
11 | "author": "",
12 | "devDependencies": {
13 | "@babel/core": "^7.7.2",
14 | "@babel/plugin-transform-react-jsx": "^7.7.0",
15 | "@babel/preset-env": "^7.7.1",
16 | "babel-loader": "^8.0.6",
17 | "css-loader": "^3.2.0",
18 | "html-webpack-plugin": "^3.2.0",
19 | "less-loader": "^6.2.0",
20 | "style-loader": "^1.0.0",
21 | "webpack": "^4.41.2",
22 | "webpack-cli": "^3.3.10",
23 | "webpack-dev-server": "^3.9.0",
24 | "cross-env": "^7.0.2"
25 | },
26 | "dependencies": {
27 | "react": "^16.13.1",
28 | "react-dom": "^16.13.1",
29 | "vue": "^2.6.11",
30 | "zone.js": "^0.10.2"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/examples/main/render/ReactRender.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 |
4 | /**
5 | * 渲染子应用
6 | */
7 | function Render(props) {
8 | const { loading } = props;
9 |
10 | return (
11 | <>
12 | {loading && Loading...
}
13 |
14 | >
15 | );
16 | }
17 |
18 | export default function render({ loading }) {
19 | const container = document.getElementById('subapp-container');
20 | ReactDOM.render(, container);
21 | }
22 |
--------------------------------------------------------------------------------
/examples/main/render/VueRender.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue/dist/vue.esm';
2 |
3 | function vueRender({ loading }) {
4 | return new Vue({
5 | template: `
6 |
10 | `,
11 | el: '#subapp-container',
12 | data() {
13 | return {
14 | loading,
15 | };
16 | },
17 | });
18 | }
19 |
20 | let app = null;
21 |
22 | export default function render({ loading }) {
23 | if (!app) {
24 | app = vueRender({ loading });
25 | } else {
26 | app.loading = loading;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/examples/main/webpack.config.js:
--------------------------------------------------------------------------------
1 | const HtmlWebpackPlugin = require('html-webpack-plugin');
2 | const { name } = require('./package');
3 |
4 | module.exports = {
5 | entry: process.env.MODE === 'multiple' ? './multiple.js' : './index.js',
6 | devtool: 'source-map',
7 | devServer: {
8 | open: true,
9 | port: '7099',
10 | clientLogLevel: 'warning',
11 | disableHostCheck: true,
12 | compress: true,
13 | headers: {
14 | 'Access-Control-Allow-Origin': '*',
15 | },
16 | historyApiFallback: true,
17 | overlay: { warnings: false, errors: true },
18 | },
19 | output: {
20 | publicPath: '/',
21 | },
22 | mode: 'development',
23 | resolve: {
24 | extensions: ['.js', '.jsx', '.ts', '.tsx'],
25 | },
26 | module: {
27 | rules: [
28 | {
29 | test: /\.jsx?$/,
30 | exclude: /node_modules/,
31 | use: {
32 | loader: 'babel-loader',
33 | options: {
34 | presets: ['@babel/preset-env'],
35 | plugins: ['@babel/plugin-transform-react-jsx'],
36 | },
37 | },
38 | },
39 | {
40 | test: /\.(le|c)ss$/,
41 | use: ['style-loader', 'css-loader', 'less-loader'],
42 | },
43 | ],
44 | },
45 | plugins: [
46 | new HtmlWebpackPlugin({
47 | filename: 'index.html',
48 | template: process.env.MODE === 'multiple' ? './multiple.html' : './index.html',
49 | minify: {
50 | removeComments: true,
51 | collapseWhitespace: true,
52 | },
53 | }),
54 | ],
55 | };
56 |
--------------------------------------------------------------------------------
/examples/purehtml/entry.js:
--------------------------------------------------------------------------------
1 | const render = $ => {
2 | $('#purehtml-container').html('Hello, render with jQuery');
3 | return Promise.resolve();
4 | };
5 |
6 | (global => {
7 | global['purehtml'] = {
8 | bootstrap: () => {
9 | console.log('purehtml bootstrap');
10 | return Promise.resolve();
11 | },
12 | mount: () => {
13 | console.log('purehtml mount');
14 | return render($);
15 | },
16 | unmount: () => {
17 | console.log('purehtml unmount');
18 | return Promise.resolve();
19 | },
20 | };
21 | })(window);
22 |
--------------------------------------------------------------------------------
/examples/purehtml/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Purehtml Example
7 |
9 |
10 |
11 |
12 | Purehtml Example
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/examples/purehtml/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "purehtml",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.html",
6 | "scripts": {
7 | "start": "cross-env PORT=7104 http-server . --cors",
8 | "test": "echo \"Error: no test specified\" && exit 1"
9 | },
10 | "author": "",
11 | "license": "MIT",
12 | "devDependencies": {
13 | "cross-env": "^7.0.2",
14 | "http-server": "^0.12.1"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/examples/react15/App.jsx:
--------------------------------------------------------------------------------
1 | import React, { version as reactVersion } from 'react';
2 | import { version as antdVersion } from 'antd';
3 |
4 | import Logo from './components/Logo'
5 | import HelloModal from './components/HelloModal'
6 |
7 | export default class App extends React.Component {
8 | render() {
9 | return (
10 |
11 |
12 |
13 | React version: {reactVersion}, AntD version: {antdVersion}
14 |
15 |
16 |
17 | );
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/examples/react15/components/HelloModal.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Button, Modal } from 'antd';
3 |
4 | export default class HelloModal extends React.Component {
5 |
6 | constructor() {
7 | super();
8 | this.state = {
9 | visible: false,
10 | };
11 | this.setVisible = visible => this.setState({ visible });
12 | }
13 |
14 | render() {
15 | const { visible } = this.state;
16 |
17 | return (
18 |
19 |
22 | this.setVisible(false)}
26 | onCancel={() => this.setVisible(false)}
27 | title="Install"
28 | >
29 | $ yarn add qiankun # or npm i qiankun -S
30 |
31 |
32 | );
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/examples/react15/components/Logo.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default class ReactLogo extends React.Component {
4 | render() {
5 | return (
6 |
10 | )
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/examples/react15/dynamic.css:
--------------------------------------------------------------------------------
1 | /* 动态加载的样式 */
2 |
3 | .react15-lib {
4 | color: #818ff7;
5 | }
6 |
--------------------------------------------------------------------------------
/examples/react15/index.css:
--------------------------------------------------------------------------------
1 | .react15-main {
2 | display: flex;
3 | flex-direction: column;
4 | align-items: center;
5 | }
6 |
7 | .react15-icon {
8 | width: 140px;
9 | }
10 |
11 | .react15-lib {
12 | margin: 32px 0 24px;
13 | font-size: 16px;
14 | color: #2c3e50;
15 | }
16 |
--------------------------------------------------------------------------------
/examples/react15/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Title
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/examples/react15/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Kuitos
3 | * @since 2019-05-16
4 | */
5 | import 'antd/dist/antd.min.css';
6 | import React from 'react';
7 | import ReactDOM from 'react-dom';
8 | import App from './App';
9 | import './index.css';
10 | import './public-path';
11 |
12 | export async function bootstrap() {
13 | console.log('[react15] react app bootstraped');
14 | }
15 |
16 | export async function mount(props = {}) {
17 | console.log('[react15] props from main framework', props);
18 | const { container } = props;
19 | ReactDOM.render(
20 | ,
21 | container ? container.querySelector('#react15Root') : document.getElementById('react15Root'),
22 | );
23 | import('./dynamic.css').then(() => {
24 | console.log('[react15] dynamic style load');
25 | });
26 |
27 | const styleElement = document.createElement('style');
28 | styleElement.innerText = '.react15-icon { height: 400px }';
29 | document.head.appendChild(styleElement);
30 |
31 | setTimeout(() => {
32 | document.head.removeChild(styleElement);
33 | }, 2000);
34 | }
35 |
36 | export async function unmount(props) {
37 | const { container } = props;
38 | ReactDOM.unmountComponentAtNode(
39 | container ? container.querySelector('#react15Root') : document.getElementById('react15Root'),
40 | );
41 | }
42 |
43 | if (!window.__POWERED_BY_QIANKUN__) {
44 | bootstrap().then(mount);
45 | }
46 |
--------------------------------------------------------------------------------
/examples/react15/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react15",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "webpack-dev-server",
8 | "test": "echo \"Error: no test specified\" && exit 1"
9 | },
10 | "author": "",
11 | "dependencies": {
12 | "antd": "2.13.14",
13 | "react": "15.6.2",
14 | "react-dom": "15.6.2"
15 | },
16 | "devDependencies": {
17 | "@babel/core": "^7.7.2",
18 | "@babel/plugin-transform-react-jsx": "^7.7.0",
19 | "@babel/preset-env": "^7.7.1",
20 | "babel-loader": "^8.0.6",
21 | "css-loader": "^3.2.0",
22 | "html-webpack-plugin": "^3.2.0",
23 | "style-loader": "^1.0.0",
24 | "webpack": "^4.41.2",
25 | "webpack-cli": "^3.3.10",
26 | "webpack-dev-server": "^3.9.0"
27 | },
28 | "browserslist": [
29 | "last 2 Chrome versions"
30 | ]
31 | }
32 |
--------------------------------------------------------------------------------
/examples/react15/public-path.js:
--------------------------------------------------------------------------------
1 | if (window.__POWERED_BY_QIANKUN__) {
2 | __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
3 | }
4 |
--------------------------------------------------------------------------------
/examples/react15/webpack.config.js:
--------------------------------------------------------------------------------
1 | const HtmlWebpackPlugin = require('html-webpack-plugin');
2 | const { name } = require('./package');
3 |
4 | module.exports = {
5 | entry: './index.js',
6 | devtool: 'source-map',
7 | devServer: {
8 | port: '7102',
9 | clientLogLevel: 'warning',
10 | disableHostCheck: true,
11 | compress: true,
12 | headers: {
13 | 'Access-Control-Allow-Origin': '*',
14 | },
15 | overlay: { warnings: false, errors: true },
16 | },
17 | output: {
18 | library: `${name}-[name]`,
19 | libraryTarget: 'umd',
20 | jsonpFunction: `webpackJsonp_${name}`,
21 | },
22 | mode: 'development',
23 | resolve: {
24 | extensions: ['.js', '.jsx', '.ts', '.tsx'],
25 | },
26 | module: {
27 | rules: [
28 | {
29 | test: /\.jsx?$/,
30 | exclude: /node_modules/,
31 | use: {
32 | loader: 'babel-loader',
33 | options: {
34 | presets: ['@babel/preset-env'],
35 | plugins: ['@babel/plugin-transform-react-jsx'],
36 | },
37 | },
38 | },
39 | {
40 | test: /\.css$/,
41 | use: ['style-loader', 'css-loader'],
42 | },
43 | ],
44 | },
45 | plugins: [
46 | new HtmlWebpackPlugin({
47 | filename: 'index.html',
48 | template: './index.html',
49 | minify: {
50 | removeComments: true,
51 | collapseWhitespace: true,
52 | },
53 | }),
54 | ],
55 | };
56 |
--------------------------------------------------------------------------------
/examples/react16/.env:
--------------------------------------------------------------------------------
1 | SKIP_PREFLIGHT_CHECK=true
2 | BROWSER=none
3 | PORT=7100
4 | WDS_SOCKET_PORT=7100
5 |
--------------------------------------------------------------------------------
/examples/react16/.rescriptsrc.js:
--------------------------------------------------------------------------------
1 | const { name } = require('./package');
2 |
3 | module.exports = {
4 | webpack: config => {
5 | config.output.library = `${name}-[name]`;
6 | config.output.libraryTarget = 'umd';
7 | config.output.jsonpFunction = `webpackJsonp_${name}`;
8 | config.output.globalObject = 'window';
9 |
10 | return config;
11 | },
12 |
13 | devServer: _ => {
14 | const config = _;
15 |
16 | config.headers = {
17 | 'Access-Control-Allow-Origin': '*',
18 | };
19 | config.historyApiFallback = true;
20 |
21 | config.hot = false;
22 | config.watchContentBase = false;
23 | config.liveReload = false;
24 |
25 | return config;
26 | },
27 | };
28 |
--------------------------------------------------------------------------------
/examples/react16/README.md:
--------------------------------------------------------------------------------
1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
2 |
3 | ## Available Scripts
4 |
5 | In the project directory, you can run:
6 |
7 | ### `yarn start`
8 |
9 | Runs the app in the development mode.
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
10 |
11 | The page will reload if you make edits.
You will also see any lint errors in the console.
12 |
13 | ### `yarn test`
14 |
15 | Launches the test runner in the interactive watch mode.
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
16 |
17 | ### `yarn build`
18 |
19 | Builds the app for production to the `build` folder.
It correctly bundles React in production mode and optimizes the build for the best performance.
20 |
21 | The build is minified and the filenames include the hashes.
Your app is ready to be deployed!
22 |
23 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
24 |
25 | ### `yarn eject`
26 |
27 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
28 |
29 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
30 |
31 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
32 |
33 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
34 |
35 | ## Learn More
36 |
37 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
38 |
39 | To learn React, check out the [React documentation](https://reactjs.org/).
40 |
41 | ### Code Splitting
42 |
43 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting
44 |
45 | ### Analyzing the Bundle Size
46 |
47 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size
48 |
49 | ### Making a Progressive Web App
50 |
51 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app
52 |
53 | ### Advanced Configuration
54 |
55 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration
56 |
57 | ### Deployment
58 |
59 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment
60 |
61 | ### `yarn build` fails to minify
62 |
63 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify
64 |
--------------------------------------------------------------------------------
/examples/react16/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react16",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "antd": "^3.25.2",
7 | "react": "^16.12.0",
8 | "react-dom": "^16.12.0",
9 | "react-router-dom": "^5.1.2"
10 | },
11 | "scripts": {
12 | "start": "rescripts start",
13 | "build": "rescripts build",
14 | "test": "rescripts test",
15 | "eject": "rescripts eject"
16 | },
17 | "browserslist": {
18 | "production": [
19 | ">0.2%",
20 | "not dead",
21 | "not op_mini all"
22 | ],
23 | "development": [
24 | "last 1 chrome version",
25 | "last 1 firefox version",
26 | "last 1 safari version"
27 | ]
28 | },
29 | "devDependencies": {
30 | "@rescripts/cli": "^0.0.14",
31 | "react-scripts": "^3.4.1"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/examples/react16/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umijs/qiankun/88c658395a83a9eca36b5b6560a5ef879cc42fa1/examples/react16/public/favicon.ico
--------------------------------------------------------------------------------
/examples/react16/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/examples/react16/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umijs/qiankun/88c658395a83a9eca36b5b6560a5ef879cc42fa1/examples/react16/public/logo192.png
--------------------------------------------------------------------------------
/examples/react16/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umijs/qiankun/88c658395a83a9eca36b5b6560a5ef879cc42fa1/examples/react16/public/logo512.png
--------------------------------------------------------------------------------
/examples/react16/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/examples/react16/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 |
--------------------------------------------------------------------------------
/examples/react16/src/App.css:
--------------------------------------------------------------------------------
1 | .app-main {
2 | display: flex;
3 | flex-direction: column;
4 | align-items: center;
5 | }
6 |
7 | .app-title {
8 | font-size: 30px;
9 | margin: 0;
10 | margin-bottom: 32px;
11 | }
12 |
13 | .app-lib {
14 | font-size: 16px;
15 | color: #2c3e50;
16 | }
17 |
18 | .app-nav-item {
19 | margin-top: 16px;
20 | padding: 12px 24px;
21 | border: 2px solid #2c3e50;
22 | }
23 |
--------------------------------------------------------------------------------
/examples/react16/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { lazy, Suspense } from 'react';
2 | import { BrowserRouter as Router, Link, Route, Switch } from 'react-router-dom';
3 | import { Divider } from 'antd';
4 |
5 | import 'antd/dist/antd.min.css';
6 | import './App.css';
7 |
8 | import LibVersion from './components/LibVersion';
9 | import HelloModal from './components/HelloModal';
10 |
11 | import Home from './pages/Home';
12 | const About = lazy(() => import('./pages/About'));
13 |
14 | const RouteExample = () => {
15 | return (
16 |
17 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | );
30 | };
31 |
32 | export default function App() {
33 | return (
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/examples/react16/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render(, div);
8 | ReactDOM.unmountComponentAtNode(div);
9 | });
10 |
--------------------------------------------------------------------------------
/examples/react16/src/components/HelloModal.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Button, Modal } from 'antd';
3 |
4 | export default function() {
5 | const [visible, setVisible] = useState(false);
6 | return (
7 | <>
8 |
9 | setVisible(false)} onCancel={() => setVisible(false)} title="qiankun">
10 | Probably the most complete micro-frontends solution you ever met
11 |
12 | >
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/examples/react16/src/components/LibVersion.js:
--------------------------------------------------------------------------------
1 | import React, { version as reactVersion } from 'react';
2 | import { version as antdVersion } from 'antd';
3 |
4 | export default function() {
5 | return (
6 | <>
7 | React Demo
8 |
9 | React version: {reactVersion}, AntD version: {antdVersion}
10 |
11 | >
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/examples/react16/src/index.js:
--------------------------------------------------------------------------------
1 | import './public-path';
2 | import React from 'react';
3 | import ReactDOM from 'react-dom';
4 | import App from './App';
5 | import * as serviceWorker from './serviceWorker';
6 |
7 | function render(props) {
8 | const { container } = props;
9 | ReactDOM.render(, container ? container.querySelector('#root') : document.querySelector('#root'));
10 | }
11 |
12 | function storeTest(props) {
13 | props.onGlobalStateChange((value, prev) => console.log(`[onGlobalStateChange - ${props.name}]:`, value, prev), true);
14 | props.setGlobalState({
15 | ignore: props.name,
16 | user: {
17 | name: props.name,
18 | },
19 | });
20 | }
21 |
22 | if (!window.__POWERED_BY_QIANKUN__) {
23 | render({});
24 | }
25 |
26 | export async function bootstrap() {
27 | console.log('[react16] react app bootstraped');
28 | }
29 |
30 | export async function mount(props) {
31 | console.log('[react16] props from main framework', props);
32 | storeTest(props);
33 | render(props);
34 | }
35 |
36 | export async function unmount(props) {
37 | const { container } = props;
38 | ReactDOM.unmountComponentAtNode(container ? container.querySelector('#root') : document.querySelector('#root'));
39 | }
40 |
41 | // If you want your app to work offline and load faster, you can change
42 | // unregister() to register() below. Note this comes with some pitfalls.
43 | // Learn more about service workers: https://bit.ly/CRA-PWA
44 | serviceWorker.unregister();
45 |
--------------------------------------------------------------------------------
/examples/react16/src/pages/About.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function() {
4 | return (
5 |
6 | About
7 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/examples/react16/src/pages/Home.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function() {
4 | return (
5 |
6 | Home
7 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/examples/react16/src/public-path.js:
--------------------------------------------------------------------------------
1 | if (window.__POWERED_BY_QIANKUN__) {
2 | // eslint-disable-next-line no-undef
3 | __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
4 | }
5 |
--------------------------------------------------------------------------------
/examples/react16/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.1/8 is considered localhost for IPv4.
18 | window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/),
19 | );
20 |
21 | export function register(config) {
22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
23 | // The URL constructor is available in all browsers that support SW.
24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
25 | if (publicUrl.origin !== window.location.origin) {
26 | // Our service worker won't work if PUBLIC_URL is on a different origin
27 | // from what our page is served on. This might happen if a CDN is used to
28 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
29 | return;
30 | }
31 |
32 | window.addEventListener('load', () => {
33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
34 |
35 | if (isLocalhost) {
36 | // This is running on localhost. Let's check if a service worker still exists or not.
37 | checkValidServiceWorker(swUrl, config);
38 |
39 | // Add some additional logging to localhost, pointing developers to the
40 | // service worker/PWA documentation.
41 | navigator.serviceWorker.ready.then(() => {
42 | console.log(
43 | 'This web app is being served cache-first by a service ' +
44 | 'worker. To learn more, visit https://bit.ly/CRA-PWA',
45 | );
46 | });
47 | } else {
48 | // Is not localhost. Just register service worker
49 | registerValidSW(swUrl, config);
50 | }
51 | });
52 | }
53 | }
54 |
55 | function registerValidSW(swUrl, config) {
56 | navigator.serviceWorker
57 | .register(swUrl)
58 | .then(registration => {
59 | registration.onupdatefound = () => {
60 | const installingWorker = registration.installing;
61 | if (installingWorker == null) {
62 | return;
63 | }
64 | installingWorker.onstatechange = () => {
65 | if (installingWorker.state === 'installed') {
66 | if (navigator.serviceWorker.controller) {
67 | // At this point, the updated precached content has been fetched,
68 | // but the previous service worker will still serve the older
69 | // content until all client tabs are closed.
70 | console.log(
71 | 'New content is available and will be used when all ' +
72 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.',
73 | );
74 |
75 | // Execute callback
76 | if (config && config.onUpdate) {
77 | config.onUpdate(registration);
78 | }
79 | } else {
80 | // At this point, everything has been precached.
81 | // It's the perfect time to display a
82 | // "Content is cached for offline use." message.
83 | console.log('Content is cached for offline use.');
84 |
85 | // Execute callback
86 | if (config && config.onSuccess) {
87 | config.onSuccess(registration);
88 | }
89 | }
90 | }
91 | };
92 | };
93 | })
94 | .catch(error => {
95 | console.error('Error during service worker registration:', error);
96 | });
97 | }
98 |
99 | function checkValidServiceWorker(swUrl, config) {
100 | // Check if the service worker can be found. If it can't reload the page.
101 | fetch(swUrl)
102 | .then(response => {
103 | // Ensure service worker exists, and that we really are getting a JS file.
104 | const contentType = response.headers.get('content-type');
105 | if (response.status === 404 || (contentType != null && contentType.indexOf('javascript') === -1)) {
106 | // No service worker found. Probably a different app. Reload the page.
107 | navigator.serviceWorker.ready.then(registration => {
108 | registration.unregister().then(() => {
109 | window.location.reload();
110 | });
111 | });
112 | } else {
113 | // Service worker found. Proceed as normal.
114 | registerValidSW(swUrl, config);
115 | }
116 | })
117 | .catch(() => {
118 | console.log('No internet connection found. App is running in offline mode.');
119 | });
120 | }
121 |
122 | export function unregister() {
123 | if ('serviceWorker' in navigator) {
124 | navigator.serviceWorker.ready.then(registration => {
125 | registration.unregister();
126 | });
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/examples/vue/.browserslistrc:
--------------------------------------------------------------------------------
1 | > 1%
2 | last 2 versions
3 |
--------------------------------------------------------------------------------
/examples/vue/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 |
5 | # local env files
6 | .env.local
7 | .env.*.local
8 |
9 | # Log files
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 |
14 | # Editor directories and files
15 | .idea
16 | .vscode
17 | *.suo
18 | *.ntvs*
19 | *.njsproj
20 | *.sln
21 | *.sw?
22 |
--------------------------------------------------------------------------------
/examples/vue/README.md:
--------------------------------------------------------------------------------
1 | # sub-app1
2 |
3 | ## Project setup
4 |
5 | ```
6 | npm install
7 | ```
8 |
9 | ### Compiles and hot-reloads for development
10 |
11 | ```
12 | npm run serve
13 | ```
14 |
15 | ### Compiles and minifies for production
16 |
17 | ```
18 | npm run build
19 | ```
20 |
21 | ### Run your tests
22 |
23 | ```
24 | npm run test
25 | ```
26 |
27 | ### Lints and fixes files
28 |
29 | ```
30 | npm run lint
31 | ```
32 |
33 | ### Customize configuration
34 |
35 | See [Configuration Reference](https://cli.vuejs.org/config/).
36 |
--------------------------------------------------------------------------------
/examples/vue/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ['@vue/cli-plugin-babel/preset'],
3 | };
4 |
--------------------------------------------------------------------------------
/examples/vue/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue",
3 | "version": "0.1.0",
4 | "scripts": {
5 | "start": "vue-cli-service serve",
6 | "serve": "vue-cli-service serve",
7 | "build": "vue-cli-service build"
8 | },
9 | "dependencies": {
10 | "core-js": "^3.3.2",
11 | "element-ui": "^2.12.0",
12 | "vue": "^2.6.10",
13 | "vue-router": "^3.1.3",
14 | "vuex": "^3.0.1"
15 | },
16 | "devDependencies": {
17 | "@vue/cli-plugin-babel": "^4.0.0",
18 | "@vue/cli-service": "^4.0.0",
19 | "vue-template-compiler": "^2.6.10"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/examples/vue/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | autoprefixer: {},
4 | },
5 | };
6 |
--------------------------------------------------------------------------------
/examples/vue/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Home
5 | |
6 | About
7 |
8 |
9 |
10 |
11 |
12 |
35 |
--------------------------------------------------------------------------------
/examples/vue/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umijs/qiankun/88c658395a83a9eca36b5b6560a5ef879cc42fa1/examples/vue/src/assets/logo.png
--------------------------------------------------------------------------------
/examples/vue/src/components/HelloWorld.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ msg }}
4 |
5 |
6 | vue version: {{ vueVersion }}, element-ui version: {{ elementVersion }}
7 |
8 |
9 |
10 |
11 |
28 |
29 |
30 |
35 |
--------------------------------------------------------------------------------
/examples/vue/src/main.js:
--------------------------------------------------------------------------------
1 | import './public-path';
2 | import ElementUI from 'element-ui';
3 | import 'element-ui/lib/theme-chalk/index.css';
4 | import Vue from 'vue';
5 | import VueRouter from 'vue-router';
6 | import App from './App.vue';
7 | import routes from './router';
8 | import store from './store';
9 |
10 | Vue.config.productionTip = false;
11 |
12 | Vue.use(ElementUI);
13 |
14 | let router = null;
15 | let instance = null;
16 |
17 | function render(props = {}) {
18 | const { container } = props;
19 | router = new VueRouter({
20 | base: window.__POWERED_BY_QIANKUN__ ? '/vue' : '/',
21 | mode: 'history',
22 | routes,
23 | });
24 |
25 | instance = new Vue({
26 | router,
27 | store,
28 | render: h => h(App),
29 | }).$mount(container ? container.querySelector('#app') : '#app');
30 | }
31 |
32 | if (!window.__POWERED_BY_QIANKUN__) {
33 | render();
34 | }
35 |
36 | function storeTest(props) {
37 | props.onGlobalStateChange &&
38 | props.onGlobalStateChange(
39 | (value, prev) => console.log(`[onGlobalStateChange - ${props.name}]:`, value, prev),
40 | true,
41 | );
42 | props.setGlobalState &&
43 | props.setGlobalState({
44 | ignore: props.name,
45 | user: {
46 | name: props.name,
47 | },
48 | });
49 | }
50 |
51 | export async function bootstrap() {
52 | console.log('[vue] vue app bootstraped');
53 | }
54 |
55 | export async function mount(props) {
56 | console.log('[vue] props from main framework', props);
57 | storeTest(props);
58 | render(props);
59 | }
60 |
61 | export async function unmount() {
62 | instance.$destroy();
63 | instance.$el.innerHTML = '';
64 | instance = null;
65 | router = null;
66 | }
67 |
--------------------------------------------------------------------------------
/examples/vue/src/public-path.js:
--------------------------------------------------------------------------------
1 | if (window.__POWERED_BY_QIANKUN__) {
2 | // eslint-disable-next-line no-undef
3 | __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
4 | }
5 |
--------------------------------------------------------------------------------
/examples/vue/src/router/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import VueRouter from 'vue-router';
3 | import Home from '../views/Home.vue';
4 |
5 | Vue.use(VueRouter);
6 |
7 | const routes = [
8 | {
9 | path: '/',
10 | name: 'home',
11 | component: Home,
12 | },
13 | {
14 | path: '/about',
15 | name: 'about',
16 | // route level code-splitting
17 | // this generates a separate chunk (about.[hash].js) for this route
18 | // which is lazy-loaded when the route is visited.
19 | component: () => import(/* webpackChunkName: "about" */ '../views/About.vue'),
20 | },
21 | ];
22 |
23 | export default routes;
24 |
--------------------------------------------------------------------------------
/examples/vue/src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import Vuex from 'vuex';
3 |
4 | Vue.use(Vuex);
5 |
6 | export default new Vuex.Store({
7 | state: {},
8 | mutations: {},
9 | actions: {},
10 | modules: {},
11 | });
12 |
--------------------------------------------------------------------------------
/examples/vue/src/views/About.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
This is about page
4 |
5 |
6 |
7 |
12 |
--------------------------------------------------------------------------------
/examples/vue/src/views/Home.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |

4 |
5 |
6 |
Open Dialog
7 |
8 |
13 | dialog message
14 |
18 |
19 |
20 |
21 |
22 |
23 |
49 |
--------------------------------------------------------------------------------
/examples/vue/vue.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const { name } = require('./package');
3 |
4 | function resolve(dir) {
5 | return path.join(__dirname, dir);
6 | }
7 |
8 | const port = 7101; // dev port
9 |
10 | module.exports = {
11 | /**
12 | * You will need to set publicPath if you plan to deploy your site under a sub path,
13 | * for example GitHub Pages. If you plan to deploy your site to https://foo.github.io/bar/,
14 | * then publicPath should be set to "/bar/".
15 | * In most cases please use '/' !!!
16 | * Detail: https://cli.vuejs.org/config/#publicpath
17 | */
18 | outputDir: 'dist',
19 | assetsDir: 'static',
20 | filenameHashing: true,
21 | // tweak internal webpack configuration.
22 | // see https://github.com/vuejs/vue-cli/blob/dev/docs/webpack.md
23 | devServer: {
24 | // host: '0.0.0.0',
25 | hot: true,
26 | disableHostCheck: true,
27 | port,
28 | overlay: {
29 | warnings: false,
30 | errors: true,
31 | },
32 | headers: {
33 | 'Access-Control-Allow-Origin': '*',
34 | },
35 | },
36 | // 自定义webpack配置
37 | configureWebpack: {
38 | resolve: {
39 | alias: {
40 | '@': resolve('src'),
41 | },
42 | },
43 | output: {
44 | // 把子应用打包成 umd 库格式
45 | library: `${name}-[name]`,
46 | libraryTarget: 'umd',
47 | jsonpFunction: `webpackJsonp_${name}`,
48 | },
49 | },
50 | };
51 |
--------------------------------------------------------------------------------
/examples/vue3/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 |
5 |
6 | # local env files
7 | .env.local
8 | .env.*.local
9 |
10 | # Log files
11 | npm-debug.log*
12 | yarn-debug.log*
13 | yarn-error.log*
14 | pnpm-debug.log*
15 |
16 | # Editor directories and files
17 | .idea
18 | .vscode
19 | *.suo
20 | *.ntvs*
21 | *.njsproj
22 | *.sln
23 | *.sw?
24 |
--------------------------------------------------------------------------------
/examples/vue3/README.md:
--------------------------------------------------------------------------------
1 | # hello-vue3
2 |
3 | ## Project setup
4 |
5 | ```
6 | npm install
7 | ```
8 |
9 | ### Compiles and hot-reloads for development
10 |
11 | ```
12 | npm run serve
13 | ```
14 |
15 | ### Compiles and minifies for production
16 |
17 | ```
18 | npm run build
19 | ```
20 |
21 | ### Lints and fixes files
22 |
23 | ```
24 | npm run lint
25 | ```
26 |
27 | ### Customize configuration
28 |
29 | See [Configuration Reference](https://cli.vuejs.org/config/).
30 |
--------------------------------------------------------------------------------
/examples/vue3/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ['@vue/cli-plugin-babel/preset'],
3 | };
4 |
--------------------------------------------------------------------------------
/examples/vue3/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue3",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "start": "vue-cli-service serve",
7 | "serve": "vue-cli-service serve",
8 | "build": "vue-cli-service build",
9 | "lint": "vue-cli-service lint"
10 | },
11 | "dependencies": {
12 | "core-js": "^3.6.5",
13 | "vue": "^3.0.0-0",
14 | "vue-router": "^4.0.0-beta.11",
15 | "vuex": "^4.0.0-beta.4"
16 | },
17 | "devDependencies": {
18 | "@vue/cli-plugin-babel": "~4.5.0",
19 | "@vue/cli-plugin-eslint": "~4.5.0",
20 | "@vue/cli-service": "~4.5.0",
21 | "@vue/compiler-sfc": "^3.0.0-0",
22 | "babel-eslint": "^10.1.0",
23 | "eslint": "^6.7.2",
24 | "eslint-plugin-vue": "^7.0.0-0",
25 | "less": "^3.0.4",
26 | "less-loader": "^5.0.0"
27 | },
28 | "eslintConfig": {
29 | "root": true,
30 | "env": {
31 | "node": true
32 | },
33 | "extends": [
34 | "plugin:vue/vue3-essential",
35 | "eslint:recommended"
36 | ],
37 | "parserOptions": {
38 | "parser": "babel-eslint"
39 | },
40 | "rules": {}
41 | },
42 | "browserslist": [
43 | "> 1%",
44 | "last 2 versions",
45 | "not dead"
46 | ]
47 | }
48 |
--------------------------------------------------------------------------------
/examples/vue3/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umijs/qiankun/88c658395a83a9eca36b5b6560a5ef879cc42fa1/examples/vue3/public/favicon.ico
--------------------------------------------------------------------------------
/examples/vue3/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | <%= htmlWebpackPlugin.options.title %>
9 |
10 |
11 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/examples/vue3/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Home |
5 | About
6 |
7 |
8 |
9 |
10 |
11 |
33 |
--------------------------------------------------------------------------------
/examples/vue3/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umijs/qiankun/88c658395a83a9eca36b5b6560a5ef879cc42fa1/examples/vue3/src/assets/logo.png
--------------------------------------------------------------------------------
/examples/vue3/src/components/HelloWorld.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ msg }}
4 |
Vue version: {{ version }}
5 |
6 |
7 |
8 |
48 |
49 |
54 |
55 |
56 |
75 |
--------------------------------------------------------------------------------
/examples/vue3/src/main.js:
--------------------------------------------------------------------------------
1 | import './public-path';
2 | import { createApp } from 'vue';
3 | import { createRouter, createWebHistory } from 'vue-router';
4 | import App from './App.vue';
5 | import routes from './router';
6 | import store from './store';
7 |
8 | let router = null;
9 | let instance = null;
10 | let history = null;
11 |
12 |
13 | function render(props = {}) {
14 | const { container } = props;
15 | history = createWebHistory(window.__POWERED_BY_QIANKUN__ ? '/vue3' : '/');
16 | router = createRouter({
17 | history,
18 | routes,
19 | });
20 |
21 | instance = createApp(App);
22 | instance.use(router);
23 | instance.use(store);
24 | instance.mount(container ? container.querySelector('#app') : '#app');
25 | }
26 |
27 | if (!window.__POWERED_BY_QIANKUN__) {
28 | render();
29 | }
30 |
31 | export async function bootstrap() {
32 | console.log('%c%s', 'color: green;', 'vue3.0 app bootstraped');
33 | }
34 |
35 | function storeTest(props) {
36 | props.onGlobalStateChange &&
37 | props.onGlobalStateChange(
38 | (value, prev) => console.log(`[onGlobalStateChange - ${props.name}]:`, value, prev),
39 | true,
40 | );
41 | props.setGlobalState &&
42 | props.setGlobalState({
43 | ignore: props.name,
44 | user: {
45 | name: props.name,
46 | },
47 | });
48 | }
49 |
50 | export async function mount(props) {
51 | storeTest(props);
52 | render(props);
53 | instance.config.globalProperties.$onGlobalStateChange = props.onGlobalStateChange;
54 | instance.config.globalProperties.$setGlobalState = props.setGlobalState;
55 | }
56 |
57 | export async function unmount() {
58 | instance.unmount();
59 | instance._container.innerHTML = '';
60 | instance = null;
61 | router = null;
62 | history.destroy();
63 | }
64 |
--------------------------------------------------------------------------------
/examples/vue3/src/public-path.js:
--------------------------------------------------------------------------------
1 | if (window.__POWERED_BY_QIANKUN__) {
2 | // eslint-disable-next-line no-undef
3 | __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
4 | }
5 |
--------------------------------------------------------------------------------
/examples/vue3/src/router/index.js:
--------------------------------------------------------------------------------
1 | const routes = [
2 | { path: '/', name: 'home', component: () => import(/* webpackChunkName: "home" */ '@/views/Home') },
3 | { path: '/about', name: 'about', component: () => import(/* webpackChunkName: "about" */ '@/views/About') },
4 | ];
5 |
6 | export default routes;
7 |
--------------------------------------------------------------------------------
/examples/vue3/src/store/count.js:
--------------------------------------------------------------------------------
1 | export default {
2 | namespaced: true,
3 | state: {
4 | count: 0,
5 | },
6 | getters: {
7 | evenOrOdd: state => (state.count % 2 === 0 ? 'even' : 'odd'),
8 | },
9 | mutations: {
10 | increment(state) {
11 | state.count++;
12 | },
13 | decrement(state) {
14 | state.count--;
15 | },
16 | },
17 | actions: {
18 | increment: ({ commit }) => commit('increment'),
19 | decrement: ({ commit }) => commit('decrement'),
20 | incrementIfOdd({ commit, state }) {
21 | if ((state.count + 1) % 2 === 0) {
22 | commit('increment');
23 | }
24 | },
25 | incrementAsync({ commit }) {
26 | return new Promise(resolve => {
27 | setTimeout(() => {
28 | commit('increment');
29 | resolve();
30 | }, 1000);
31 | });
32 | },
33 | },
34 | };
35 |
--------------------------------------------------------------------------------
/examples/vue3/src/store/index.js:
--------------------------------------------------------------------------------
1 | import { createStore } from 'vuex';
2 | import count from './count';
3 |
4 | export default createStore({
5 | state: {},
6 | mutations: {},
7 | actions: {},
8 | getters: {},
9 | modules: {
10 | count,
11 | },
12 | });
13 |
--------------------------------------------------------------------------------
/examples/vue3/src/views/About.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
This is about page
4 |
5 |
6 |
7 |
13 |
14 |
15 |
20 |
--------------------------------------------------------------------------------
/examples/vue3/src/views/Home.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
27 |
28 |
38 |
--------------------------------------------------------------------------------
/examples/vue3/vue.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const { name } = require('./package');
3 |
4 | function resolve(dir) {
5 | return path.join(__dirname, dir);
6 | }
7 |
8 | const port = 7105;
9 |
10 | module.exports = {
11 | outputDir: 'dist',
12 | assetsDir: 'static',
13 | filenameHashing: true,
14 | devServer: {
15 | hot: true,
16 | disableHostCheck: true,
17 | port,
18 | overlay: {
19 | warnings: false,
20 | errors: true,
21 | },
22 | headers: {
23 | 'Access-Control-Allow-Origin': '*',
24 | },
25 | },
26 | // 自定义webpack配置
27 | configureWebpack: {
28 | resolve: {
29 | alias: {
30 | '@': resolve('src'),
31 | },
32 | },
33 | output: {
34 | // 把子应用打包成 umd 库格式
35 | library: `${name}-[name]`,
36 | libraryTarget: 'umd',
37 | jsonpFunction: `webpackJsonp_${name}`,
38 | },
39 | },
40 | };
41 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "qiankun",
3 | "version": "2.10.16",
4 | "description": "A completed implementation of Micro Frontends",
5 | "keywords": [
6 | "micro frontend",
7 | "microfrontend",
8 | "micro frontends",
9 | "micro-frontend",
10 | "micro-frontends",
11 | "microservice"
12 | ],
13 | "homepage": "https://github.com/kuitos/qiankun#readme",
14 | "bugs": {
15 | "url": "https://github.com/kuitos/qiankun/issues"
16 | },
17 | "repository": {
18 | "type": "git",
19 | "url": "git+https://github.com/kuitos/qiankun.git"
20 | },
21 | "license": "MIT",
22 | "author": "Kuitos",
23 | "sideEffects": false,
24 | "main": "./lib/index.js",
25 | "module": "./es/index.js",
26 | "types": "./es/index.d.ts",
27 | "files": [
28 | "dist",
29 | "es",
30 | "lib"
31 | ],
32 | "scripts": {
33 | "examples:install": "npm-run-all --serial build install:*",
34 | "examples:start": "npm-run-all --parallel start:*",
35 | "examples:start-multiple": "run-p start-main-multiple start:react15 start:vue",
36 | "install:main": "cd examples/main && yarn",
37 | "start:main": "cd examples/main && yarn start",
38 | "start-main-multiple": "cd examples/main && yarn start:multiple",
39 | "install:react16": "cd examples/react16 && yarn",
40 | "start:react16": "cd examples/react16 && yarn start",
41 | "install:react15": "cd examples/react15 && yarn",
42 | "start:react15": "cd examples/react15 && yarn start",
43 | "install:vue": "cd examples/vue && yarn",
44 | "start:vue": "cd examples/vue && yarn start",
45 | "install:angular9": "cd examples/angular9 && yarn",
46 | "start:angular9": "cd examples/angular9 && yarn serve:qiankun",
47 | "install:purehtml": "cd examples/purehtml && yarn",
48 | "start:purehtml": "cd examples/purehtml && yarn start",
49 | "install:vue3": "cd examples/vue3 && yarn",
50 | "start:vue3": "cd examples/vue3 && yarn start",
51 | "build": "father-build",
52 | "release": "np --no-cleanup --yolo --no-publish",
53 | "prepublishOnly": "yarn ci",
54 | "lint": "yarn lint:js && yarn lint:prettier",
55 | "lint:js": "eslint --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src",
56 | "lint:fix": "yarn lint:js -- --fix",
57 | "lint:prettier": "prettier -c ./src/**/*",
58 | "prettier": "prettier --write ./src/**/*",
59 | "docs:dev": "dumi dev",
60 | "docs:build": "dumi build",
61 | "ci": "yarn lint && yarn build && yarn test",
62 | "test": "cross-env NODE_ENV=test jest"
63 | },
64 | "husky": {
65 | "hooks": {
66 | "pre-commit": "lint-staged",
67 | "pre-push": "yarn test"
68 | }
69 | },
70 | "lint-staged": {
71 | "**/*.{js,ts,json,css,md}": [
72 | "prettier -w"
73 | ],
74 | "**/*.{js,ts}": [
75 | "yarn lint:fix"
76 | ]
77 | },
78 | "jest": {
79 | "coveragePathIgnorePatterns": [
80 | "/node_modules/",
81 | "/__tests__/",
82 | "/dist/"
83 | ],
84 | "moduleFileExtensions": [
85 | "js",
86 | "ts"
87 | ],
88 | "testMatch": [
89 | "/src/**/__tests__/**/*.test.ts"
90 | ],
91 | "testPathIgnorePatterns": [
92 | "/node_modules/",
93 | "/fixtures/"
94 | ],
95 | "transform": {
96 | "^.+\\.ts$": "/node_modules/ts-jest"
97 | }
98 | },
99 | "resolutions": {
100 | "@types/history": "^4.x",
101 | "@types/react": "^17.x"
102 | },
103 | "dependencies": {
104 | "@babel/runtime": "^7.10.5",
105 | "import-html-entry": "^1.15.1",
106 | "lodash": "^4.17.11",
107 | "single-spa": "^5.9.2"
108 | },
109 | "devDependencies": {
110 | "@types/jest": "^25.1.4",
111 | "@types/lodash": "^4.14.129",
112 | "@types/vfile-message": "1.x",
113 | "@umijs/fabric": "^2.0.7",
114 | "babel-plugin-import": "^1.12.1",
115 | "cross-env": "^7.0.2",
116 | "dumi": "^1.1.0-beta.24",
117 | "father-build": "^1.7.0",
118 | "globals": "^13.17.0",
119 | "husky": "^2.3.0",
120 | "jest": "^25.2.2",
121 | "levenary": "^1.1.1",
122 | "lint-staged": "^10.5.4",
123 | "np": "^5.0.3",
124 | "npm-run-all": "^4.1.5",
125 | "prettier": "^2.1.2",
126 | "rimraf": "^3.0.0",
127 | "ts-jest": "^25.2.1",
128 | "typescript": "^4.8.2"
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/src/__tests__/globalState.test.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @author dbkillerf6
3 | * @since 2020-04-10
4 | */
5 |
6 | import { getMicroAppStateActions, initGlobalState } from '../globalState';
7 |
8 | const master = initGlobalState({ user: 'qiankun' });
9 |
10 | test('test master to master actions', () => {
11 | const callback1 = (state: Record, prevState: Record) => {
12 | expect(state).toEqual({ ignore: 'matser', user: 'master' });
13 | expect(prevState).toEqual({ user: 'qiankun' });
14 | };
15 | master.onGlobalStateChange(callback1);
16 | master.setGlobalState({
17 | ignore: 'matser',
18 | user: 'master',
19 | });
20 |
21 | const callback2 = (state: Record, prevState: Record) => {
22 | expect(state).toEqual({ ignore: 'matser', user: 'master' });
23 | expect(prevState).toEqual({ ignore: 'matser', user: 'master' });
24 | };
25 | master.onGlobalStateChange(callback2, true);
26 |
27 | // 注销监听保证下一个 case 测试
28 | master.offGlobalStateChange();
29 | });
30 |
31 | // global: { ignore: 'matser', user: 'master' }
32 |
33 | const slaveA = getMicroAppStateActions('slaveA');
34 |
35 | test('test master to slave actions', () => {
36 | const slaveCallback1 = (state: Record, prevState: Record) => {
37 | expect(state).toEqual({ ignore: 'matser', user: 'master2' });
38 | expect(prevState).toEqual({ ignore: 'matser', user: 'master' });
39 | };
40 | slaveA.onGlobalStateChange(slaveCallback1);
41 |
42 | master.setGlobalState({
43 | user: 'master2',
44 | });
45 |
46 | const slaveCallback2 = (state: Record, prevState: Record) => {
47 | expect(state).toEqual({ ignore: 'matser', user: 'master2' });
48 | expect(prevState).toEqual({ ignore: 'matser', user: 'master2' });
49 | };
50 | slaveA.onGlobalStateChange(slaveCallback2, true);
51 |
52 | // 注销监听保证下一个 case 测试
53 | slaveA.offGlobalStateChange();
54 | });
55 |
56 | // global: { ignore: 'matser', user: 'master2' }
57 |
58 | test('test slave to master actions', () => {
59 | const callback1 = (state: Record, prevState: Record) => {
60 | expect(state).toEqual({ ignore: 'slaveA', user: 'slaveA' });
61 | expect(prevState).toEqual({ ignore: 'matser', user: 'master2' });
62 | };
63 | master.onGlobalStateChange(callback1);
64 |
65 | slaveA.setGlobalState({
66 | ignore: 'slaveA',
67 | user: 'slaveA',
68 | });
69 |
70 | const callback2 = (state: Record, prevState: Record) => {
71 | expect(state).toEqual({ ignore: 'slaveA', user: 'slaveA' });
72 | expect(prevState).toEqual({ ignore: 'slaveA', user: 'slaveA' });
73 | };
74 | master.onGlobalStateChange(callback2, true);
75 |
76 | // 注销监听保证下一个 case 测试
77 | master.offGlobalStateChange();
78 | });
79 |
80 | // global: { ignore: 'slaveA', user: 'slaveA' }
81 |
82 | const slaveB = getMicroAppStateActions('slaveB');
83 | test('test slave to slave actions', () => {
84 | const callback1 = (state: Record, prevState: Record) => {
85 | expect(state).toEqual({ ignore: 'slaveA', user: 'slaveB' });
86 | expect(prevState).toEqual({ ignore: 'slaveA', user: 'slaveA' });
87 | };
88 | slaveA.onGlobalStateChange(callback1);
89 |
90 | slaveB.setGlobalState({
91 | ignore2: 'slaveB',
92 | user: 'slaveB',
93 | });
94 |
95 | const callback2 = (state: Record, prevState: Record) => {
96 | expect(state).toEqual({ ignore: 'slaveA', user: 'slaveB' });
97 | expect(prevState).toEqual({ ignore: 'slaveA', user: 'slaveB' });
98 | };
99 | slaveA.onGlobalStateChange(callback2, true);
100 | });
101 |
--------------------------------------------------------------------------------
/src/addons/engineFlag.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Kuitos
3 | * @since 2020-05-15
4 | */
5 |
6 | import type { FrameworkLifeCycles } from '../interfaces';
7 |
8 | export default function getAddOn(global: Window): FrameworkLifeCycles {
9 | return {
10 | async beforeLoad() {
11 | // eslint-disable-next-line no-param-reassign
12 | global.__POWERED_BY_QIANKUN__ = true;
13 | },
14 |
15 | async beforeMount() {
16 | // eslint-disable-next-line no-param-reassign
17 | global.__POWERED_BY_QIANKUN__ = true;
18 | },
19 |
20 | async beforeUnmount() {
21 | // eslint-disable-next-line no-param-reassign
22 | delete global.__POWERED_BY_QIANKUN__;
23 | },
24 | };
25 | }
26 |
--------------------------------------------------------------------------------
/src/addons/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Kuitos
3 | * @since 2020-03-02
4 | */
5 |
6 | import { concat, mergeWith } from 'lodash';
7 | import type { FrameworkLifeCycles, ObjectType } from '../interfaces';
8 | import getEngineFlagAddon from './engineFlag';
9 | import getRuntimePublicPathAddOn from './runtimePublicPath';
10 |
11 | export default function getAddOns(global: Window, publicPath: string): FrameworkLifeCycles {
12 | return mergeWith({}, getEngineFlagAddon(global), getRuntimePublicPathAddOn(global, publicPath), (v1, v2) =>
13 | concat(v1 ?? [], v2 ?? []),
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/src/addons/runtimePublicPath.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Kuitos
3 | * @since 2019-11-12
4 | */
5 | import type { FrameworkLifeCycles } from '../interfaces';
6 |
7 | const rawPublicPath = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
8 |
9 | export default function getAddOn(global: Window, publicPath = '/'): FrameworkLifeCycles {
10 | let hasMountedOnce = false;
11 |
12 | return {
13 | async beforeLoad() {
14 | // eslint-disable-next-line no-param-reassign
15 | global.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ = publicPath;
16 | },
17 |
18 | async beforeMount() {
19 | if (hasMountedOnce) {
20 | // eslint-disable-next-line no-param-reassign
21 | global.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ = publicPath;
22 | }
23 | },
24 |
25 | async beforeUnmount() {
26 | if (rawPublicPath === undefined) {
27 | // eslint-disable-next-line no-param-reassign
28 | delete global.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
29 | } else {
30 | // eslint-disable-next-line no-param-reassign
31 | global.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ = rawPublicPath;
32 | }
33 |
34 | hasMountedOnce = true;
35 | },
36 | };
37 | }
38 |
--------------------------------------------------------------------------------
/src/effects.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Kuitos
3 | * @since 2019-02-19
4 | */
5 | import { getMountedApps, navigateToUrl } from 'single-spa';
6 |
7 | const firstMountLogLabel = '[qiankun] first app mounted';
8 | if (process.env.NODE_ENV === 'development') {
9 | console.time(firstMountLogLabel);
10 | }
11 |
12 | export function setDefaultMountApp(defaultAppLink: string) {
13 | // can not use addEventListener once option for ie support
14 | window.addEventListener('single-spa:no-app-change', function listener() {
15 | const mountedApps = getMountedApps();
16 | if (!mountedApps.length) {
17 | navigateToUrl(defaultAppLink);
18 | }
19 |
20 | window.removeEventListener('single-spa:no-app-change', listener);
21 | });
22 | }
23 |
24 | export function runDefaultMountEffects(defaultAppLink: string) {
25 | console.warn(
26 | '[qiankun] runDefaultMountEffects will be removed in next version, please use setDefaultMountApp instead',
27 | );
28 | setDefaultMountApp(defaultAppLink);
29 | }
30 |
31 | export function runAfterFirstMounted(effect: () => void) {
32 | // can not use addEventListener once option for ie support
33 | window.addEventListener('single-spa:first-mount', function listener() {
34 | if (process.env.NODE_ENV === 'development') {
35 | console.timeEnd(firstMountLogLabel);
36 | }
37 |
38 | effect();
39 |
40 | window.removeEventListener('single-spa:first-mount', listener);
41 | });
42 | }
43 |
--------------------------------------------------------------------------------
/src/error.ts:
--------------------------------------------------------------------------------
1 | export class QiankunError extends Error {
2 | constructor(message: string) {
3 | super(`[qiankun]: ${message}`);
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/errorHandler.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Kuitos
3 | * @since 2020-02-21
4 | */
5 |
6 | export { addErrorHandler, removeErrorHandler } from 'single-spa';
7 |
8 | export function addGlobalUncaughtErrorHandler(errorHandler: OnErrorEventHandlerNonNull): void {
9 | window.addEventListener('error', errorHandler);
10 | window.addEventListener('unhandledrejection', errorHandler);
11 | }
12 |
13 | export function removeGlobalUncaughtErrorHandler(errorHandler: (...args: any[]) => any) {
14 | window.removeEventListener('error', errorHandler);
15 | window.removeEventListener('unhandledrejection', errorHandler);
16 | }
17 |
--------------------------------------------------------------------------------
/src/globalState.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @author dbkillerf6
3 | * @since 2020-04-10
4 | */
5 |
6 | import { cloneDeep } from 'lodash';
7 | import type { OnGlobalStateChangeCallback, MicroAppStateActions } from './interfaces';
8 |
9 | let globalState: Record = {};
10 |
11 | const deps: Record = {};
12 |
13 | // 触发全局监听
14 | function emitGlobal(state: Record, prevState: Record) {
15 | Object.keys(deps).forEach((id: string) => {
16 | if (deps[id] instanceof Function) {
17 | deps[id](cloneDeep(state), cloneDeep(prevState));
18 | }
19 | });
20 | }
21 |
22 | export function initGlobalState(state: Record = {}) {
23 | if (process.env.NODE_ENV === 'development') {
24 | console.warn(`[qiankun] globalState tools will be removed in 3.0, pls don't use it!`);
25 | }
26 |
27 | if (state === globalState) {
28 | console.warn('[qiankun] state has not changed!');
29 | } else {
30 | const prevGlobalState = cloneDeep(globalState);
31 | globalState = cloneDeep(state);
32 | emitGlobal(globalState, prevGlobalState);
33 | }
34 | return getMicroAppStateActions(`global-${+new Date()}`, true);
35 | }
36 |
37 | export function getMicroAppStateActions(id: string, isMaster?: boolean): MicroAppStateActions {
38 | return {
39 | /**
40 | * onGlobalStateChange 全局依赖监听
41 | *
42 | * 收集 setState 时所需要触发的依赖
43 | *
44 | * 限制条件:每个子应用只有一个激活状态的全局监听,新监听覆盖旧监听,若只是监听部分属性,请使用 onGlobalStateChange
45 | *
46 | * 这么设计是为了减少全局监听滥用导致的内存爆炸
47 | *
48 | * 依赖数据结构为:
49 | * {
50 | * {id}: callback
51 | * }
52 | *
53 | * @param callback
54 | * @param fireImmediately
55 | */
56 | onGlobalStateChange(callback: OnGlobalStateChangeCallback, fireImmediately?: boolean) {
57 | if (!(callback instanceof Function)) {
58 | console.error('[qiankun] callback must be function!');
59 | return;
60 | }
61 | if (deps[id]) {
62 | console.warn(`[qiankun] '${id}' global listener already exists before this, new listener will overwrite it.`);
63 | }
64 | deps[id] = callback;
65 | if (fireImmediately) {
66 | const cloneState = cloneDeep(globalState);
67 | callback(cloneState, cloneState);
68 | }
69 | },
70 |
71 | /**
72 | * setGlobalState 更新 store 数据
73 | *
74 | * 1. 对输入 state 的第一层属性做校验,只有初始化时声明过的第一层(bucket)属性才会被更改
75 | * 2. 修改 store 并触发全局监听
76 | *
77 | * @param state
78 | */
79 | setGlobalState(state: Record = {}) {
80 | if (state === globalState) {
81 | console.warn('[qiankun] state has not changed!');
82 | return false;
83 | }
84 |
85 | const changeKeys: string[] = [];
86 | const prevGlobalState = cloneDeep(globalState);
87 | globalState = cloneDeep(
88 | Object.keys(state).reduce((_globalState, changeKey) => {
89 | if (isMaster || _globalState.hasOwnProperty(changeKey)) {
90 | changeKeys.push(changeKey);
91 | return Object.assign(_globalState, { [changeKey]: state[changeKey] });
92 | }
93 | console.warn(`[qiankun] '${changeKey}' not declared when init state!`);
94 | return _globalState;
95 | }, globalState),
96 | );
97 | if (changeKeys.length === 0) {
98 | console.warn('[qiankun] state has not changed!');
99 | return false;
100 | }
101 | emitGlobal(globalState, prevGlobalState);
102 | return true;
103 | },
104 |
105 | // 注销该应用下的依赖
106 | offGlobalStateChange() {
107 | delete deps[id];
108 | return true;
109 | },
110 | };
111 | }
112 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Kuitos
3 | * @since 2019-04-25
4 | */
5 |
6 | export { loadMicroApp, registerMicroApps, start } from './apis';
7 | export { initGlobalState } from './globalState';
8 | export { getCurrentRunningApp as __internalGetCurrentRunningApp } from './sandbox';
9 | export * from './errorHandler';
10 | export * from './effects';
11 | export * from './interfaces';
12 | export { prefetchImmediately as prefetchApps } from './prefetch';
13 |
--------------------------------------------------------------------------------
/src/interfaces.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @author kuitos
3 | * @since 2019-05-16
4 | */
5 | import type { ImportEntryOpts } from 'import-html-entry';
6 | import type { RegisterApplicationConfig, StartOpts, Parcel } from 'single-spa';
7 |
8 | declare global {
9 | // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
10 | interface Window {
11 | __POWERED_BY_QIANKUN__?: boolean;
12 | __INJECTED_PUBLIC_PATH_BY_QIANKUN__?: string;
13 | __QIANKUN_DEVELOPMENT__?: boolean;
14 | Zone?: CallableFunction;
15 | __zone_symbol__setTimeout?: Window['setTimeout'];
16 | }
17 | }
18 |
19 | export type ObjectType = Record;
20 |
21 | export type Entry =
22 | | string
23 | | {
24 | scripts?: string[];
25 | styles?: string[];
26 | html?: string;
27 | };
28 |
29 | export type HTMLContentRender = (props: { appContent: string; loading: boolean }) => any;
30 |
31 | export type AppMetadata = {
32 | // app name
33 | name: string;
34 | // app entry
35 | entry: Entry;
36 | };
37 |
38 | // just for manual loaded apps, in single-spa it called parcel
39 | export type LoadableApp = AppMetadata & {
40 | /* props pass through to app */ props?: T;
41 | } & (
42 | | {
43 | // legacy mode, the render function all handled by user
44 | render: HTMLContentRender;
45 | }
46 | | {
47 | // where the app mount to, mutual exclusive with the legacy custom render function
48 | container: string | HTMLElement;
49 | }
50 | );
51 |
52 | // for the route-based apps
53 | export type RegistrableApp = LoadableApp & {
54 | loader?: (loading: boolean) => void;
55 | activeRule: RegisterApplicationConfig['activeWhen'];
56 | };
57 |
58 | export type PrefetchStrategy =
59 | | boolean
60 | | 'all'
61 | | string[]
62 | | ((apps: AppMetadata[]) => { criticalAppNames: string[]; minorAppsName: string[] });
63 |
64 | type QiankunSpecialOpts = {
65 | /**
66 | * @deprecated internal api, don't used it as normal, might be removed after next version
67 | */
68 | $$cacheLifecycleByAppName?: boolean;
69 | prefetch?: PrefetchStrategy;
70 | sandbox?:
71 | | boolean
72 | | {
73 | strictStyleIsolation?: boolean;
74 | experimentalStyleIsolation?: boolean;
75 | /**
76 | * @deprecated We use strict mode by default
77 | */
78 | loose?: boolean;
79 | /**
80 | * use speed sandbox mode, enabled by default from 2.9.0
81 | */
82 | speedy?: boolean;
83 | patchers?: Patcher[];
84 | };
85 | /*
86 | with singular mode, any app will wait to load until other apps are unmouting
87 | it is useful for the scenario that only one sub app shown at one time
88 | */
89 | singular?: boolean | ((app: LoadableApp) => Promise);
90 | /**
91 | * skip some scripts or links intercept, like JSONP
92 | */
93 | excludeAssetFilter?: (url: string) => boolean;
94 |
95 | globalContext?: typeof window;
96 | };
97 | export type FrameworkConfiguration = QiankunSpecialOpts & ImportEntryOpts & StartOpts;
98 |
99 | export type LifeCycleFn = (app: LoadableApp, global: typeof window) => Promise;
100 | export type FrameworkLifeCycles = {
101 | beforeLoad?: LifeCycleFn | Array>; // function before app load
102 | beforeMount?: LifeCycleFn | Array>; // function before app mount
103 | afterMount?: LifeCycleFn | Array>; // function after app mount
104 | beforeUnmount?: LifeCycleFn | Array>; // function before app unmount
105 | afterUnmount?: LifeCycleFn | Array>; // function after app unmount
106 | };
107 |
108 | export type MicroApp = Parcel;
109 |
110 | export type Rebuilder = () => void;
111 | export type Freer = () => Rebuilder;
112 | export type Patcher = () => Freer;
113 |
114 | export enum SandBoxType {
115 | Proxy = 'Proxy',
116 | Snapshot = 'Snapshot',
117 |
118 | // for legacy sandbox
119 | // https://github.com/umijs/qiankun/blob/0d1d3f0c5ed1642f01854f96c3fabf0a2148bd26/src/sandbox/legacy/sandbox.ts#L22...L25
120 | LegacyProxy = 'LegacyProxy',
121 | }
122 |
123 | export type SandBox = {
124 | /** 沙箱的名字 */
125 | name: string;
126 | /** 沙箱的类型 */
127 | type: SandBoxType;
128 | /** 沙箱导出的代理实体 */
129 | proxy: WindowProxy;
130 | /** 沙箱是否在运行中 */
131 | sandboxRunning: boolean;
132 | /** latest set property */
133 | latestSetProp?: PropertyKey | null;
134 | patchDocument: (doc: Document) => void;
135 | /** 启动沙箱 */
136 | active: () => void;
137 | /** 关闭沙箱 */
138 | inactive: () => void;
139 | };
140 |
141 | export type OnGlobalStateChangeCallback = (state: Record, prevState: Record) => void;
142 |
143 | export type MicroAppStateActions = {
144 | onGlobalStateChange: (callback: OnGlobalStateChangeCallback, fireImmediately?: boolean) => void;
145 | setGlobalState: (state: Record) => boolean;
146 | offGlobalStateChange: () => boolean;
147 | };
148 |
--------------------------------------------------------------------------------
/src/prefetch.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Kuitos
3 | * @since 2019-02-26
4 | */
5 |
6 | import type { Entry, ImportEntryOpts } from 'import-html-entry';
7 | import { importEntry } from 'import-html-entry';
8 | import { isFunction } from 'lodash';
9 | import { getAppStatus, getMountedApps, NOT_LOADED } from 'single-spa';
10 | import type { AppMetadata, PrefetchStrategy } from './interfaces';
11 |
12 | declare global {
13 | interface NetworkInformation {
14 | saveData: boolean;
15 | effectiveType: string;
16 | }
17 | }
18 |
19 | function idleCall(cb: IdleRequestCallback, start: number) {
20 | cb({
21 | didTimeout: false,
22 | timeRemaining() {
23 | return Math.max(0, 50 - (Date.now() - start));
24 | },
25 | });
26 | }
27 |
28 | // RIC and shim for browsers setTimeout() without it idle
29 | let requestIdleCallback: (cb: IdleRequestCallback) => any;
30 | if (typeof window.requestIdleCallback !== 'undefined') {
31 | requestIdleCallback = window.requestIdleCallback;
32 | } else if (typeof window.MessageChannel !== 'undefined') {
33 | // The first recommendation is to use MessageChannel because
34 | // it does not have the 4ms delay of setTimeout
35 | const channel = new MessageChannel();
36 | const port = channel.port2;
37 | const tasks: IdleRequestCallback[] = [];
38 | channel.port1.onmessage = ({ data }) => {
39 | const task = tasks.shift();
40 | if (!task) {
41 | return;
42 | }
43 | idleCall(task, data.start);
44 | };
45 | requestIdleCallback = function(cb: IdleRequestCallback) {
46 | tasks.push(cb);
47 | port.postMessage({ start: Date.now() });
48 | };
49 | } else {
50 | requestIdleCallback = (cb: IdleRequestCallback) => setTimeout(idleCall, 0, cb, Date.now());
51 | }
52 |
53 | declare global {
54 | interface Navigator {
55 | connection: {
56 | saveData: boolean;
57 | effectiveType: string;
58 | type: 'bluetooth' | 'cellular' | 'ethernet' | 'none' | 'wifi' | 'wimax' | 'other' | 'unknown';
59 | };
60 | }
61 | }
62 |
63 | const isSlowNetwork = navigator.connection
64 | ? navigator.connection.saveData ||
65 | (navigator.connection.type !== 'wifi' &&
66 | navigator.connection.type !== 'ethernet' &&
67 | /([23])g/.test(navigator.connection.effectiveType))
68 | : false;
69 |
70 | /**
71 | * prefetch assets, do nothing while in mobile network
72 | * @param entry
73 | * @param opts
74 | */
75 | function prefetch(entry: Entry, opts?: ImportEntryOpts): void {
76 | if (!navigator.onLine || isSlowNetwork) {
77 | // Don't prefetch if in a slow network or offline
78 | return;
79 | }
80 |
81 | requestIdleCallback(async () => {
82 | const { getExternalScripts, getExternalStyleSheets } = await importEntry(entry, opts);
83 | requestIdleCallback(getExternalStyleSheets);
84 | requestIdleCallback(getExternalScripts);
85 | });
86 | }
87 |
88 | function prefetchAfterFirstMounted(apps: AppMetadata[], opts?: ImportEntryOpts): void {
89 | window.addEventListener('single-spa:first-mount', function listener() {
90 | const notLoadedApps = apps.filter((app) => getAppStatus(app.name) === NOT_LOADED);
91 |
92 | if (process.env.NODE_ENV === 'development') {
93 | const mountedApps = getMountedApps();
94 | console.log(`[qiankun] prefetch starting after ${mountedApps} mounted...`, notLoadedApps);
95 | }
96 |
97 | notLoadedApps.forEach(({ entry }) => prefetch(entry, opts));
98 |
99 | window.removeEventListener('single-spa:first-mount', listener);
100 | });
101 | }
102 |
103 | export function prefetchImmediately(apps: AppMetadata[], opts?: ImportEntryOpts): void {
104 | if (process.env.NODE_ENV === 'development') {
105 | console.log('[qiankun] prefetch starting for apps...', apps);
106 | }
107 |
108 | apps.forEach(({ entry }) => prefetch(entry, opts));
109 | }
110 |
111 | export function doPrefetchStrategy(
112 | apps: AppMetadata[],
113 | prefetchStrategy: PrefetchStrategy,
114 | importEntryOpts?: ImportEntryOpts,
115 | ) {
116 | const appsName2Apps = (names: string[]): AppMetadata[] => apps.filter((app) => names.includes(app.name));
117 |
118 | if (Array.isArray(prefetchStrategy)) {
119 | prefetchAfterFirstMounted(appsName2Apps(prefetchStrategy as string[]), importEntryOpts);
120 | } else if (isFunction(prefetchStrategy)) {
121 | (async () => {
122 | // critical rendering apps would be prefetch as earlier as possible
123 | const { criticalAppNames = [], minorAppsName = [] } = await prefetchStrategy(apps);
124 | prefetchImmediately(appsName2Apps(criticalAppNames), importEntryOpts);
125 | prefetchAfterFirstMounted(appsName2Apps(minorAppsName), importEntryOpts);
126 | })();
127 | } else {
128 | switch (prefetchStrategy) {
129 | case true:
130 | prefetchAfterFirstMounted(apps, importEntryOpts);
131 | break;
132 |
133 | case 'all':
134 | prefetchImmediately(apps, importEntryOpts);
135 | break;
136 |
137 | default:
138 | break;
139 | }
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/src/sandbox/__tests__/common.test.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Kuitos
3 | * @since 2021-04-12
4 | */
5 |
6 | import { rebindTarget2Fn } from '../common';
7 |
8 | describe('getTargetValue', () => {
9 | it('should work well', () => {
10 | const a1 = rebindTarget2Fn(window, undefined);
11 | expect(a1).toEqual(undefined);
12 |
13 | const a2 = rebindTarget2Fn(window, null);
14 | expect(a2).toEqual(null);
15 |
16 | const a3 = rebindTarget2Fn(window, function bindThis(this: any) {
17 | return this;
18 | });
19 | const a3returns = a3();
20 | expect(a3returns).toEqual(window);
21 | });
22 |
23 | it('should work well while function added prototype methods after first running', () => {
24 | function prototypeAddedAfterFirstInvocation(this: any, field: string) {
25 | this.field = field;
26 | }
27 | const notConstructableFunction = rebindTarget2Fn(window, prototypeAddedAfterFirstInvocation);
28 | // `this` of not constructable function will be bound automatically, and it can not be changed by calling with special `this`
29 | const result = {};
30 | notConstructableFunction.call(result, '123');
31 | expect(result).toStrictEqual({});
32 | expect(window.field).toEqual('123');
33 |
34 | prototypeAddedAfterFirstInvocation.prototype.addedFn = () => {};
35 | const constructableFunction = rebindTarget2Fn(window, prototypeAddedAfterFirstInvocation);
36 | // `this` coule be available if it be predicated as a constructable function
37 | const result2 = {};
38 | constructableFunction.call(result2, '456');
39 | expect(result2).toStrictEqual({ field: '456' });
40 | // window.field not be affected
41 | expect(window.field).toEqual('123');
42 | // reference should be stable after first running
43 | expect(constructableFunction).toBe(rebindTarget2Fn(window, prototypeAddedAfterFirstInvocation));
44 | });
45 |
46 | it('should work well while value have a readonly prototype on its prototype chain', () => {
47 | function callableFunction() {}
48 |
49 | const functionWithReadonlyPrototype = () => {};
50 | Object.defineProperty(functionWithReadonlyPrototype, 'prototype', {
51 | writable: false,
52 | enumerable: false,
53 | configurable: false,
54 | value: 123,
55 | });
56 |
57 | Object.setPrototypeOf(callableFunction, functionWithReadonlyPrototype);
58 |
59 | const boundFn = rebindTarget2Fn(window, callableFunction);
60 | expect(boundFn.prototype).toBe(callableFunction.prototype);
61 | });
62 |
63 | it("should work well while function's toString()'s return value keeps the same as the origin", () => {
64 | function callableFunction1() {}
65 | function callableFunction2() {}
66 | function callableFunction3() {}
67 | callableFunction2.toString = () => 'instance toString';
68 | Object.defineProperty(callableFunction3, 'toString', {
69 | get() {
70 | return () => 'instance toString';
71 | },
72 | });
73 |
74 | const boundFn1 = rebindTarget2Fn(window, callableFunction1);
75 | const boundFn2 = rebindTarget2Fn(window, callableFunction2);
76 | const boundFn3 = rebindTarget2Fn(window, callableFunction3);
77 |
78 | expect(boundFn1.toString()).toBe(callableFunction1.toString());
79 | expect(boundFn2.toString()).toBe(callableFunction2.toString());
80 | expect(boundFn3.toString()).toBe(callableFunction3.toString());
81 | });
82 | });
83 |
--------------------------------------------------------------------------------
/src/sandbox/__tests__/proxySandbox.speedy.test.ts:
--------------------------------------------------------------------------------
1 | import ProxySandbox from '../proxySandbox';
2 |
3 | it('should never throw errors although globalThis is unavailable in current global context', () => {
4 | const { proxy } = new ProxySandbox('globalThis-always-available');
5 | // @ts-ignore
6 | window.proxy = proxy;
7 |
8 | expect('mockGlobalThis' in window).toBe(false);
9 | expect('mockGlobalThis' in proxy).toBe(true);
10 |
11 | const code = `(function() {
12 | with (window.proxy) {
13 | (function(mockGlobalThis){
14 | mockGlobalThis.testName = 'kuitos';
15 | })(mockGlobalThis);
16 | }
17 | })()`;
18 | // eslint-disable-next-line no-eval
19 | const geval = eval;
20 | geval(code);
21 |
22 | // @ts-ignore
23 | expect(proxy.testName).toBe('kuitos');
24 | // @ts-ignore
25 | expect(window.testName).toBeUndefined();
26 | });
27 |
28 | it('should throw errors while variable not existed in current global context', () => {
29 | const { proxy } = new ProxySandbox('invalid-throw-error');
30 | // @ts-ignore
31 | window.proxy = proxy;
32 |
33 | expect('invalidVariable' in window).toBe(false);
34 | expect('invalidVariable' in proxy).toBe(false);
35 |
36 | const code = `(function() {
37 | with (window.proxy) {
38 | (function(mockGlobalThis){
39 | (0, invalidVariable);
40 | })(mockGlobalThis);
41 | }
42 | })()`;
43 | // eslint-disable-next-line no-eval
44 | const geval = eval;
45 | try {
46 | geval(code);
47 | } catch (e: any) {
48 | expect(e.message).toBe('invalidVariable is not defined');
49 | }
50 | });
51 |
52 | it('should never hijack native method of Object.prototype expect hasOwnProperty', () => {
53 | const { proxy } = new ProxySandbox('native-object-method');
54 | // @ts-ignore
55 | window.proxy = proxy;
56 |
57 | const code = `(function() {
58 | with (window.proxy) {
59 | (function(mockGlobalThis){
60 | window.nativeHasOwnCheckResult = hasOwnProperty.call({nativeHas: 123}, 'nativeHas');
61 | window.proxyHasOwnCheck = window.hasOwnProperty.call({nativeHas: '123'}, 'nativeHas');
62 | window.selfCheck = window.hasOwnProperty('nativeHasOwnCheckResult');
63 |
64 | window.enumerableCheckResult = propertyIsEnumerable.call({nativeHas: 123}, 'nativeHas');
65 | })(mockGlobalThis);
66 | }
67 | })()`;
68 | // eslint-disable-next-line no-eval
69 | const geval = eval;
70 | geval(code);
71 | expect(window.proxy.nativeHasOwnCheckResult).toBeTruthy();
72 | expect(window.proxy.proxyHasOwnCheck).toBeTruthy();
73 | expect(window.proxy.selfCheck).toBeTruthy();
74 | expect(window.proxy.enumerableCheckResult).toBeTruthy();
75 | });
76 |
--------------------------------------------------------------------------------
/src/sandbox/__tests__/snapshotSandBox.test.ts:
--------------------------------------------------------------------------------
1 | import SnapshotSandbox from '../snapshotSandbox';
2 |
3 | it('modify, add and delete props should isolated at sandbox', () => {
4 | window.globalProps = { a: 1 };
5 | window.globalDeletedProps = { b: [] };
6 | const sandbox1 = new SnapshotSandbox('app1');
7 | const sandbox2 = new SnapshotSandbox('app2');
8 | sandbox1.active();
9 | window.globalProps = { a: 2 };
10 | window.globalAddedProps = 3;
11 | delete window.globalDeletedProps;
12 | sandbox1.inactive();
13 | // change to sandbox2
14 | sandbox2.active();
15 | expect(window.globalProps).toEqual({ a: 1 });
16 | expect(window.globalAddedProps).not.toBeDefined();
17 | expect(window.globalDeletedProps).toEqual({ b: [] });
18 | sandbox2.inactive();
19 | // restore sandbox1
20 | sandbox1.active();
21 | expect(window.globalProps).toEqual({ a: 2 });
22 | expect(window.globalAddedProps).toEqual(3);
23 | expect(window.globalDeletedProps).not.toBeDefined();
24 | // add delete props
25 | window.globalDeletedProps = { b: [2] };
26 | sandbox1.inactive();
27 | expect(window.globalDeletedProps).toEqual({ b: [] });
28 | sandbox1.active();
29 | expect(window.globalDeletedProps).toEqual({ b: [2] });
30 | // delete props again
31 | delete window.globalDeletedProps;
32 | sandbox1.inactive();
33 | expect(window.globalDeletedProps).toEqual({ b: [] });
34 | sandbox1.active();
35 | expect(window.globalDeletedProps).not.toBeDefined();
36 | // 1. add props 2. modify props 2. delete props
37 | window.globalDeletedProps = { b: [2] };
38 | window.globalDeletedProps = { b: [3] };
39 | delete window.globalDeletedProps;
40 | sandbox1.inactive();
41 | expect(window.globalDeletedProps).toEqual({ b: [] });
42 | sandbox1.active();
43 | expect(window.globalDeletedProps).not.toBeDefined();
44 | // 1. delete props 2. add props 3. modify props
45 | delete window.globalAddedProps;
46 | window.globalAddedProps = 3;
47 | window.globalAddedProps = 4;
48 | sandbox1.inactive();
49 | expect(window.globalAddedProps).not.toBeDefined();
50 | sandbox1.active();
51 | expect(window.globalAddedProps).toEqual(4);
52 | });
53 |
--------------------------------------------------------------------------------
/src/sandbox/common.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Kuitos
3 | * @since 2020-04-13
4 | */
5 |
6 | import { isBoundedFunction, isCallable, isConstructable } from '../utils';
7 |
8 | type AppInstance = { name: string; window: WindowProxy };
9 | let currentRunningApp: AppInstance | null = null;
10 |
11 | /**
12 | * get the app that running tasks at current tick
13 | */
14 | export function getCurrentRunningApp() {
15 | return currentRunningApp;
16 | }
17 |
18 | export function setCurrentRunningApp(appInstance: { name: string; window: WindowProxy }) {
19 | // Set currentRunningApp and it's proxySandbox to global window, as its only use case is for document.createElement from now on, which hijacked by a global way
20 | currentRunningApp = appInstance;
21 | }
22 |
23 | export function clearCurrentRunningApp() {
24 | currentRunningApp = null;
25 | }
26 |
27 | const functionBoundedValueMap = new WeakMap();
28 |
29 | export function rebindTarget2Fn(target: any, fn: any): any {
30 | /*
31 | 仅绑定 isCallable && !isBoundedFunction && !isConstructable 的函数对象,如 window.console、window.atob 这类,不然微应用中调用时会抛出 Illegal invocation 异常
32 | 目前没有完美的检测方式,这里通过 prototype 中是否还有可枚举的拓展方法的方式来判断
33 | @warning 这里不要随意替换成别的判断方式,因为可能触发一些 edge case(比如在 lodash.isFunction 在 iframe 上下文中可能由于调用了 top window 对象触发的安全异常)
34 | */
35 | if (isCallable(fn) && !isBoundedFunction(fn) && !isConstructable(fn)) {
36 | const cachedBoundFunction = functionBoundedValueMap.get(fn);
37 | if (cachedBoundFunction) {
38 | return cachedBoundFunction;
39 | }
40 |
41 | const boundValue = Function.prototype.bind.call(fn, target);
42 |
43 | // some callable function has custom fields, we need to copy the own props to boundValue. such as moment function.
44 | Object.getOwnPropertyNames(fn).forEach((key) => {
45 | // boundValue might be a proxy, we need to check the key whether exist in it
46 | if (!boundValue.hasOwnProperty(key)) {
47 | Object.defineProperty(boundValue, key, Object.getOwnPropertyDescriptor(fn, key)!);
48 | }
49 | });
50 |
51 | // copy prototype if bound function not have but target one have
52 | // as prototype is non-enumerable mostly, we need to copy it from target function manually
53 | if (fn.hasOwnProperty('prototype') && !boundValue.hasOwnProperty('prototype')) {
54 | // we should not use assignment operator to set boundValue prototype like `boundValue.prototype = fn.prototype`
55 | // as the assignment will also look up prototype chain while it hasn't own prototype property,
56 | // 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
57 | // see https://github.com/umijs/qiankun/issues/1121
58 | Object.defineProperty(boundValue, 'prototype', { value: fn.prototype, enumerable: false, writable: true });
59 | }
60 |
61 | // Some util, like `function isNative() { return typeof Ctor === 'function' && /native code/.test(Ctor.toString()) }` relies on the original `toString()` result
62 | // but bound functions will always return "function() {[native code]}" for `toString`, which is misleading
63 | if (typeof fn.toString === 'function') {
64 | const valueHasInstanceToString = fn.hasOwnProperty('toString') && !boundValue.hasOwnProperty('toString');
65 | const boundValueHasPrototypeToString = boundValue.toString === Function.prototype.toString;
66 |
67 | if (valueHasInstanceToString || boundValueHasPrototypeToString) {
68 | const originToStringDescriptor = Object.getOwnPropertyDescriptor(
69 | valueHasInstanceToString ? fn : Function.prototype,
70 | 'toString',
71 | );
72 |
73 | Object.defineProperty(
74 | boundValue,
75 | 'toString',
76 | Object.assign(
77 | {},
78 | originToStringDescriptor,
79 | originToStringDescriptor?.get ? null : { value: () => fn.toString() },
80 | ),
81 | );
82 | }
83 | }
84 |
85 | functionBoundedValueMap.set(fn, boundValue);
86 | return boundValue;
87 | }
88 |
89 | return fn;
90 | }
91 |
--------------------------------------------------------------------------------
/src/sandbox/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Kuitos
3 | * @since 2019-04-11
4 | */
5 | import type { Freer, Rebuilder, SandBox } from '../interfaces';
6 | import LegacySandbox from './legacy/sandbox';
7 | import { patchAtBootstrapping, patchAtMounting } from './patchers';
8 | import ProxySandbox from './proxySandbox';
9 | import SnapshotSandbox from './snapshotSandbox';
10 |
11 | export { getCurrentRunningApp } from './common';
12 | export { css } from './patchers';
13 |
14 | /**
15 | * 生成应用运行时沙箱
16 | *
17 | * 沙箱分两个类型:
18 | * 1. app 环境沙箱
19 | * app 环境沙箱是指应用初始化过之后,应用会在什么样的上下文环境运行。每个应用的环境沙箱只会初始化一次,因为子应用只会触发一次 bootstrap 。
20 | * 子应用在切换时,实际上切换的是 app 环境沙箱。
21 | * 2. render 沙箱
22 | * 子应用在 app mount 开始前生成好的的沙箱。每次子应用切换过后,render 沙箱都会重现初始化。
23 | *
24 | * 这么设计的目的是为了保证每个子应用切换回来之后,还能运行在应用 bootstrap 之后的环境下。
25 | *
26 | * @param appName
27 | * @param elementGetter
28 | * @param scopedCSS
29 | * @param useLooseSandbox
30 | * @param excludeAssetFilter
31 | * @param globalContext
32 | * @param speedySandBox
33 | */
34 | export function createSandboxContainer(
35 | appName: string,
36 | elementGetter: () => HTMLElement | ShadowRoot,
37 | scopedCSS: boolean,
38 | useLooseSandbox?: boolean,
39 | excludeAssetFilter?: (url: string) => boolean,
40 | globalContext?: typeof window,
41 | speedySandBox?: boolean,
42 | ) {
43 | let sandbox: SandBox;
44 | if (window.Proxy) {
45 | sandbox = useLooseSandbox
46 | ? new LegacySandbox(appName, globalContext)
47 | : new ProxySandbox(appName, globalContext, { speedy: !!speedySandBox });
48 | } else {
49 | sandbox = new SnapshotSandbox(appName);
50 | }
51 |
52 | // some side effect could be invoked while bootstrapping, such as dynamic stylesheet injection with style-loader, especially during the development phase
53 | const bootstrappingFreers = patchAtBootstrapping(
54 | appName,
55 | elementGetter,
56 | sandbox,
57 | scopedCSS,
58 | excludeAssetFilter,
59 | speedySandBox,
60 | );
61 | // mounting freers are one-off and should be re-init at every mounting time
62 | let mountingFreers: Freer[] = [];
63 |
64 | let sideEffectsRebuilders: Rebuilder[] = [];
65 |
66 | return {
67 | instance: sandbox,
68 |
69 | /**
70 | * 沙箱被 mount
71 | * 可能是从 bootstrap 状态进入的 mount
72 | * 也可能是从 unmount 之后再次唤醒进入 mount
73 | */
74 | async mount() {
75 | /* ------------------------------------------ 因为有上下文依赖(window),以下代码执行顺序不能变 ------------------------------------------ */
76 |
77 | /* ------------------------------------------ 1. 启动/恢复 沙箱------------------------------------------ */
78 | sandbox.active();
79 |
80 | const sideEffectsRebuildersAtBootstrapping = sideEffectsRebuilders.slice(0, bootstrappingFreers.length);
81 | const sideEffectsRebuildersAtMounting = sideEffectsRebuilders.slice(bootstrappingFreers.length);
82 |
83 | // must rebuild the side effects which added at bootstrapping firstly to recovery to nature state
84 | if (sideEffectsRebuildersAtBootstrapping.length) {
85 | sideEffectsRebuildersAtBootstrapping.forEach((rebuild) => rebuild());
86 | }
87 |
88 | /* ------------------------------------------ 2. 开启全局变量补丁 ------------------------------------------*/
89 | // render 沙箱启动时开始劫持各类全局监听,尽量不要在应用初始化阶段有 事件监听/定时器 等副作用
90 | mountingFreers = patchAtMounting(appName, elementGetter, sandbox, scopedCSS, excludeAssetFilter, speedySandBox);
91 |
92 | /* ------------------------------------------ 3. 重置一些初始化时的副作用 ------------------------------------------*/
93 | // 存在 rebuilder 则表明有些副作用需要重建
94 | if (sideEffectsRebuildersAtMounting.length) {
95 | sideEffectsRebuildersAtMounting.forEach((rebuild) => rebuild());
96 | }
97 |
98 | // clean up rebuilders
99 | sideEffectsRebuilders = [];
100 | },
101 |
102 | /**
103 | * 恢复 global 状态,使其能回到应用加载之前的状态
104 | */
105 | async unmount() {
106 | // record the rebuilders of window side effects (event listeners or timers)
107 | // note that the frees of mounting phase are one-off as it will be re-init at next mounting
108 | sideEffectsRebuilders = [...bootstrappingFreers, ...mountingFreers].map((free) => free());
109 |
110 | sandbox.inactive();
111 | },
112 | };
113 | }
114 |
--------------------------------------------------------------------------------
/src/sandbox/legacy/__tests__/sandbox.test.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Kuitos
3 | * @since 2021-07-19
4 | */
5 |
6 | import LooseSandbox from '../sandbox';
7 |
8 | describe('loose sandbox', () => {
9 | it('should record the mutation from Object.defineProperty', () => {
10 | const sandbox = new LooseSandbox('defineProperty');
11 |
12 | const { proxy } = sandbox;
13 |
14 | proxy.prop1 = 123;
15 | Object.defineProperty(proxy, 'prop2', { value: 456, configurable: true, writable: true });
16 |
17 | expect(proxy.prop1).toBe(123);
18 | expect(window.prop1).toBe(123);
19 | expect(proxy.prop2).toBe(456);
20 | expect(window.prop2).toBe(456);
21 |
22 | sandbox.inactive();
23 |
24 | expect(window.prop1).toBeUndefined();
25 | expect(window.prop2).toBeUndefined();
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/src/sandbox/patchers/__tests__/interval.test.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Kuitos
3 | * @since 2020-03-30
4 | */
5 |
6 | import { sleep } from '../../../utils';
7 | import patch from '../interval';
8 |
9 | test('patch setInterval', async () => {
10 | const free = patch(window);
11 |
12 | const clearedListener = jest.fn();
13 | const unclearedListener = jest.fn();
14 | const unclearedListenerWithArgs = jest.fn();
15 |
16 | const interval1 = window.setInterval(clearedListener, 60);
17 | window.setInterval(unclearedListener, 8);
18 | window.setInterval(unclearedListenerWithArgs, 30, 'kuitos');
19 |
20 | window.clearInterval(interval1);
21 |
22 | await sleep(10);
23 | free();
24 |
25 | expect(clearedListener).toBeCalledTimes(0);
26 | expect(unclearedListener).toBeCalledTimes(1);
27 | expect(unclearedListenerWithArgs).toBeCalledTimes(0);
28 | });
29 |
--------------------------------------------------------------------------------
/src/sandbox/patchers/dynamicAppend/__tests__/common.test.ts:
--------------------------------------------------------------------------------
1 | import { getStyledElementCSSRules, rebuildCSSRules, recordStyledComponentsCSSRules } from '../common';
2 |
3 | jest.mock('import-html-entry', () => ({
4 | execScripts: jest.fn(),
5 | }));
6 |
7 | const cssRuleText1 = '#foo { color: red; }';
8 | const cssRuleText2 = 'span { font-weight: bold; }';
9 |
10 | const createStyleElement = () => {
11 | document.body.innerHTML = '';
12 | return document.getElementById('style-under-test') as HTMLStyleElement;
13 | };
14 |
15 | beforeEach(() => {
16 | document.body.innerHTML = '';
17 | });
18 |
19 | test('should record Styled-Component CSS Rules correctly', () => {
20 | const styleElement = createStyleElement();
21 | const cssStyleSheet = styleElement.sheet;
22 | cssStyleSheet?.insertRule(cssRuleText1);
23 | cssStyleSheet?.insertRule(cssRuleText2);
24 |
25 | recordStyledComponentsCSSRules([styleElement]);
26 |
27 | const cssRules: CSSRuleList | undefined = getStyledElementCSSRules(styleElement);
28 | expect(cssRules).toBeDefined();
29 | expect(cssRules?.length).toEqual(2);
30 | expect((cssStyleSheet?.cssRules[0] as CSSStyleRule).selectorText).toEqual('span');
31 | expect((cssStyleSheet?.cssRules[1] as CSSStyleRule).selectorText).toEqual('#foo');
32 | });
33 |
34 | test('should rebuild Styled-Component CSS Rules in the correct order', () => {
35 | const styleElement = createStyleElement();
36 | const cssStyleSheet = styleElement.sheet;
37 | cssStyleSheet?.insertRule(cssRuleText1);
38 | cssStyleSheet?.insertRule(cssRuleText2);
39 |
40 | recordStyledComponentsCSSRules([styleElement]);
41 |
42 | expect((cssStyleSheet?.cssRules[0] as CSSStyleRule).selectorText).toEqual('span');
43 | expect((cssStyleSheet?.cssRules[1] as CSSStyleRule).selectorText).toEqual('#foo');
44 |
45 | // Set sheet to be writiable so we can overwrite the value for sheet
46 | Object.defineProperty(window.HTMLStyleElement.prototype, 'sheet', {
47 | writable: true,
48 | value: {},
49 | });
50 | // @ts-ignore
51 | styleElement.sheet = new CSSStyleSheet();
52 | rebuildCSSRules([styleElement], () => true);
53 |
54 | expect(styleElement.sheet.cssRules.length).toEqual(2);
55 | // Verify that the order of the styles is the same as the recorded ones
56 | expect((cssStyleSheet?.cssRules[0] as CSSStyleRule).selectorText).toEqual('span');
57 | expect((cssStyleSheet?.cssRules[1] as CSSStyleRule).selectorText).toEqual('#foo');
58 | });
59 |
--------------------------------------------------------------------------------
/src/sandbox/patchers/dynamicAppend/__tests__/forStrictSandbox.test.ts:
--------------------------------------------------------------------------------
1 | import { SandBoxType } from '../../../../interfaces';
2 | import { noop } from 'lodash';
3 | import { patchStrictSandbox } from '../forStrictSandbox';
4 |
5 | jest.mock('import-html-entry', () => ({
6 | execScripts: jest.fn(),
7 | }));
8 |
9 | describe('forStrictSandbox test', () => {
10 | const {
11 | prototype: { createTreeWalker: originalCreateTreeWalker },
12 | } = Document;
13 |
14 | beforeAll(() => {
15 | Document.prototype.createTreeWalker = function createTreeWalker(
16 | this: Document,
17 | root: Node,
18 | whatToShow?: number | undefined,
19 | filter?: NodeFilter | null | undefined,
20 | ) {
21 | if (document !== root) {
22 | throw new TypeError('error');
23 | }
24 | return originalCreateTreeWalker.call(this, root, whatToShow, filter);
25 | };
26 | });
27 |
28 | afterAll(() => {
29 | Document.prototype.createTreeWalker = originalCreateTreeWalker;
30 | });
31 |
32 | it('should not throw on patched document', () => {
33 | let patchedDocument!: Document;
34 | const appName = 'test-app';
35 | const wrapper = document.createElement('div');
36 | const sandbox = {
37 | name: appName,
38 | type: SandBoxType.Proxy,
39 | proxy: window,
40 | sandboxRunning: true,
41 | latestSetProp: null,
42 | patchDocument: (patched: Document) => {
43 | patchedDocument = patched;
44 | },
45 | active: noop,
46 | inactive: noop,
47 | };
48 | patchStrictSandbox(appName, () => wrapper, sandbox, true, false, undefined, true);
49 |
50 | expect(patchedDocument).toBeDefined();
51 | expect(() => patchedDocument?.createTreeWalker(patchedDocument)).not.toThrow();
52 | });
53 | });
54 |
--------------------------------------------------------------------------------
/src/sandbox/patchers/dynamicAppend/forLooseSandbox.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Kuitos
3 | * @since 2020-10-13
4 | */
5 |
6 | import { checkActivityFunctions } from 'single-spa';
7 | import type { Freer, SandBox } from '../../../interfaces';
8 | import {
9 | calcAppCount,
10 | isAllAppsUnmounted,
11 | patchHTMLDynamicAppendPrototypeFunctions,
12 | rebuildCSSRules,
13 | recordStyledComponentsCSSRules,
14 | } from './common';
15 |
16 | /**
17 | * Just hijack dynamic head append, that could avoid accidentally hijacking the insertion of elements except in head.
18 | * Such a case: ReactDOM.createPortal(, container),
19 | * this could make we append the style element into app wrapper but it will cause an error while the react portal unmounting, as ReactDOM could not find the style in body children list.
20 | * @param appName
21 | * @param appWrapperGetter
22 | * @param sandbox
23 | * @param mounting
24 | * @param scopedCSS
25 | * @param excludeAssetFilter
26 | */
27 | export function patchLooseSandbox(
28 | appName: string,
29 | appWrapperGetter: () => HTMLElement | ShadowRoot,
30 | sandbox: SandBox,
31 | mounting = true,
32 | scopedCSS = false,
33 | excludeAssetFilter?: CallableFunction,
34 | ): Freer {
35 | const { proxy } = sandbox;
36 |
37 | let dynamicStyleSheetElements: Array = [];
38 |
39 | const unpatchDynamicAppendPrototypeFunctions = patchHTMLDynamicAppendPrototypeFunctions(
40 | /*
41 | check if the currently specified application is active
42 | While we switch page from qiankun app to a normal react routing page, the normal one may load stylesheet dynamically while page rendering,
43 | but the url change listener must wait until the current call stack is flushed.
44 | This scenario may cause we record the stylesheet from react routing page dynamic injection,
45 | and remove them after the url change triggered and qiankun app is unmounting
46 | see https://github.com/ReactTraining/history/blob/master/modules/createHashHistory.js#L222-L230
47 | */
48 | () => checkActivityFunctions(window.location).some((name) => name === appName),
49 | () => ({
50 | appName,
51 | appWrapperGetter,
52 | proxy,
53 | strictGlobal: false,
54 | speedySandbox: false,
55 | scopedCSS,
56 | dynamicStyleSheetElements,
57 | excludeAssetFilter,
58 | }),
59 | );
60 |
61 | if (!mounting) calcAppCount(appName, 'increase', 'bootstrapping');
62 | if (mounting) calcAppCount(appName, 'increase', 'mounting');
63 |
64 | return function free() {
65 | if (!mounting) calcAppCount(appName, 'decrease', 'bootstrapping');
66 | if (mounting) calcAppCount(appName, 'decrease', 'mounting');
67 |
68 | // release the overwrite prototype after all the micro apps unmounted
69 | if (isAllAppsUnmounted()) unpatchDynamicAppendPrototypeFunctions();
70 |
71 | recordStyledComponentsCSSRules(dynamicStyleSheetElements);
72 |
73 | // As now the sub app content all wrapped with a special id container,
74 | // the dynamic style sheet would be removed automatically while unmounting
75 |
76 | return function rebuild() {
77 | rebuildCSSRules(dynamicStyleSheetElements, (stylesheetElement) => {
78 | const appWrapper = appWrapperGetter();
79 | if (!appWrapper.contains(stylesheetElement)) {
80 | // Using document.head.appendChild ensures that appendChild invocation can also directly use the HTMLHeadElement.prototype.appendChild method which is overwritten at mounting phase
81 | document.head.appendChild.call(appWrapper, stylesheetElement);
82 | return true;
83 | }
84 |
85 | return false;
86 | });
87 |
88 | // As the patcher will be invoked every mounting phase, we could release the cache for gc after rebuilding
89 | if (mounting) {
90 | dynamicStyleSheetElements = [];
91 | }
92 | };
93 | };
94 | }
95 |
--------------------------------------------------------------------------------
/src/sandbox/patchers/dynamicAppend/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Kuitos
3 | * @since 2020-10-13
4 | */
5 |
6 | export { patchLooseSandbox } from './forLooseSandbox';
7 | export { patchStrictSandbox } from './forStrictSandbox';
8 |
--------------------------------------------------------------------------------
/src/sandbox/patchers/historyListener.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Kuitos
3 | * @since 2019-04-11
4 | */
5 |
6 | import { isFunction, noop } from 'lodash';
7 |
8 | export default function patch() {
9 | // FIXME umi unmount feature request
10 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
11 | let rawHistoryListen = (_: any) => noop;
12 | const historyListeners: Array = [];
13 | const historyUnListens: Array = [];
14 |
15 | if ((window as any).g_history && isFunction((window as any).g_history.listen)) {
16 | rawHistoryListen = (window as any).g_history.listen.bind((window as any).g_history);
17 |
18 | (window as any).g_history.listen = (listener: typeof noop) => {
19 | historyListeners.push(listener);
20 |
21 | const unListen = rawHistoryListen(listener);
22 | historyUnListens.push(unListen);
23 |
24 | return () => {
25 | unListen();
26 | historyUnListens.splice(historyUnListens.indexOf(unListen), 1);
27 | historyListeners.splice(historyListeners.indexOf(listener), 1);
28 | };
29 | };
30 | }
31 |
32 | return function free() {
33 | let rebuild = noop;
34 |
35 | /*
36 | 还存在余量 listener 表明未被卸载,存在两种情况
37 | 1. 应用在 unmout 时未正确卸载 listener
38 | 2. listener 是应用 mount 之前绑定的,
39 | 第二种情况下应用在下次 mount 之前需重新绑定该 listener
40 | */
41 | if (historyListeners.length) {
42 | rebuild = () => {
43 | // 必须使用 window.g_history.listen 的方式重新绑定 listener,从而能保证 rebuild 这部分也能被捕获到,否则在应用卸载后无法正确的移除这部分副作用
44 | historyListeners.forEach((listener) => (window as any).g_history.listen(listener));
45 | };
46 | }
47 |
48 | // 卸载余下的 listener
49 | historyUnListens.forEach((unListen) => unListen());
50 |
51 | // restore
52 | if ((window as any).g_history && isFunction((window as any).g_history.listen)) {
53 | (window as any).g_history.listen = rawHistoryListen;
54 | }
55 |
56 | return rebuild;
57 | };
58 | }
59 |
--------------------------------------------------------------------------------
/src/sandbox/patchers/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Kuitos
3 | * @since 2019-04-11
4 | */
5 |
6 | import type { Freer, SandBox } from '../../interfaces';
7 | import { SandBoxType } from '../../interfaces';
8 | import * as css from './css';
9 | import { patchLooseSandbox, patchStrictSandbox } from './dynamicAppend';
10 | import patchHistoryListener from './historyListener';
11 | import patchInterval from './interval';
12 | import patchWindowListener from './windowListener';
13 |
14 | export function patchAtMounting(
15 | appName: string,
16 | elementGetter: () => HTMLElement | ShadowRoot,
17 | sandbox: SandBox,
18 | scopedCSS: boolean,
19 | excludeAssetFilter?: CallableFunction,
20 | speedySandBox?: boolean,
21 | ): Freer[] {
22 | const basePatchers = [
23 | () => patchInterval(sandbox.proxy),
24 | () => patchWindowListener(sandbox.proxy),
25 | () => patchHistoryListener(),
26 | ];
27 |
28 | const patchersInSandbox = {
29 | [SandBoxType.LegacyProxy]: [
30 | ...basePatchers,
31 | () => patchLooseSandbox(appName, elementGetter, sandbox, true, scopedCSS, excludeAssetFilter),
32 | ],
33 | [SandBoxType.Proxy]: [
34 | ...basePatchers,
35 | () => patchStrictSandbox(appName, elementGetter, sandbox, true, scopedCSS, excludeAssetFilter, speedySandBox),
36 | ],
37 | [SandBoxType.Snapshot]: [
38 | ...basePatchers,
39 | () => patchLooseSandbox(appName, elementGetter, sandbox, true, scopedCSS, excludeAssetFilter),
40 | ],
41 | };
42 |
43 | return patchersInSandbox[sandbox.type]?.map((patch) => patch());
44 | }
45 |
46 | export function patchAtBootstrapping(
47 | appName: string,
48 | elementGetter: () => HTMLElement | ShadowRoot,
49 | sandbox: SandBox,
50 | scopedCSS: boolean,
51 | excludeAssetFilter?: CallableFunction,
52 | speedySandBox?: boolean,
53 | ): Freer[] {
54 | const patchersInSandbox = {
55 | [SandBoxType.LegacyProxy]: [
56 | () => patchLooseSandbox(appName, elementGetter, sandbox, false, scopedCSS, excludeAssetFilter),
57 | ],
58 | [SandBoxType.Proxy]: [
59 | () => patchStrictSandbox(appName, elementGetter, sandbox, false, scopedCSS, excludeAssetFilter, speedySandBox),
60 | ],
61 | [SandBoxType.Snapshot]: [
62 | () => patchLooseSandbox(appName, elementGetter, sandbox, false, scopedCSS, excludeAssetFilter),
63 | ],
64 | };
65 |
66 | return patchersInSandbox[sandbox.type]?.map((patch) => patch());
67 | }
68 |
69 | export { css };
70 |
--------------------------------------------------------------------------------
/src/sandbox/patchers/interval.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-param-reassign */
2 | /**
3 | * @author Kuitos
4 | * @since 2019-04-11
5 | */
6 |
7 | import { noop } from 'lodash';
8 |
9 | const rawWindowInterval = window.setInterval;
10 | const rawWindowClearInterval = window.clearInterval;
11 |
12 | export default function patch(global: Window) {
13 | let intervals: number[] = [];
14 |
15 | global.clearInterval = (intervalId: number) => {
16 | intervals = intervals.filter((id) => id !== intervalId);
17 | return rawWindowClearInterval.call(window, intervalId as any);
18 | };
19 |
20 | global.setInterval = (handler: CallableFunction, timeout?: number, ...args: any[]) => {
21 | const intervalId = rawWindowInterval(handler, timeout, ...args);
22 | intervals = [...intervals, intervalId];
23 | return intervalId;
24 | };
25 |
26 | return function free() {
27 | intervals.forEach((id) => global.clearInterval(id));
28 | global.setInterval = rawWindowInterval;
29 | global.clearInterval = rawWindowClearInterval;
30 |
31 | return noop;
32 | };
33 | }
34 |
--------------------------------------------------------------------------------
/src/sandbox/patchers/windowListener.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Kuitos
3 | * @since 2019-04-11
4 | */
5 |
6 | import { noop } from 'lodash';
7 |
8 | const rawAddEventListener = window.addEventListener;
9 | const rawRemoveEventListener = window.removeEventListener;
10 |
11 | type ListenerMapObject = {
12 | listener: EventListenerOrEventListenerObject;
13 | options: AddEventListenerOptions;
14 | rawListener: EventListenerOrEventListenerObject;
15 | };
16 |
17 | const DEFAULT_OPTIONS: AddEventListenerOptions = { capture: false, once: false, passive: false };
18 |
19 | const normalizeOptions = (rawOptions?: boolean | AddEventListenerOptions): AddEventListenerOptions => {
20 | if (typeof rawOptions === 'object') {
21 | return rawOptions ?? DEFAULT_OPTIONS;
22 | }
23 | return { capture: !!rawOptions, once: false, passive: false };
24 | };
25 |
26 | const findListenerIndex = (
27 | listeners: ListenerMapObject[],
28 | rawListener: EventListenerOrEventListenerObject,
29 | options: AddEventListenerOptions,
30 | ): number =>
31 | listeners.findIndex((item) => item.rawListener === rawListener && item.options.capture === options.capture);
32 |
33 | const removeCacheListener = (
34 | listenerMap: Map,
35 | type: string,
36 | rawListener: EventListenerOrEventListenerObject,
37 | rawOptions?: boolean | AddEventListenerOptions,
38 | ): ListenerMapObject => {
39 | const options = normalizeOptions(rawOptions);
40 | const cachedTypeListeners = listenerMap.get(type) || [];
41 |
42 | const findIndex = findListenerIndex(cachedTypeListeners, rawListener, options);
43 | if (findIndex > -1) {
44 | return cachedTypeListeners.splice(findIndex, 1)[0];
45 | }
46 |
47 | return { listener: rawListener, rawListener, options };
48 | };
49 |
50 | const addCacheListener = (
51 | listenerMap: Map,
52 | type: string,
53 | rawListener: EventListenerOrEventListenerObject,
54 | rawOptions?: boolean | AddEventListenerOptions,
55 | ): ListenerMapObject | undefined => {
56 | const options = normalizeOptions(rawOptions);
57 | const cachedTypeListeners = listenerMap.get(type) || [];
58 |
59 | const findIndex = findListenerIndex(cachedTypeListeners, rawListener, options);
60 | // avoid duplicated listener in the listener list
61 | if (findIndex > -1) return;
62 |
63 | let listener: EventListenerOrEventListenerObject = rawListener;
64 | if (options.once) {
65 | listener = (event: Event) => {
66 | (rawListener as EventListener)(event);
67 | removeCacheListener(listenerMap, type, rawListener, options);
68 | };
69 | }
70 |
71 | const cacheListener = { listener, options, rawListener };
72 | listenerMap.set(type, [...cachedTypeListeners, cacheListener]);
73 | return cacheListener;
74 | };
75 |
76 | export default function patch(global: WindowProxy) {
77 | const listenerMap = new Map();
78 |
79 | global.addEventListener = (
80 | type: string,
81 | rawListener: EventListenerOrEventListenerObject,
82 | rawOptions?: boolean | AddEventListenerOptions,
83 | ) => {
84 | const addListener = addCacheListener(listenerMap, type, rawListener, rawOptions);
85 |
86 | if (!addListener) return;
87 | return rawAddEventListener.call(global, type, addListener.listener, addListener.options);
88 | };
89 |
90 | global.removeEventListener = (
91 | type: string,
92 | rawListener: EventListenerOrEventListenerObject,
93 | rawOptions?: boolean | AddEventListenerOptions,
94 | ) => {
95 | const { listener, options } = removeCacheListener(listenerMap, type, rawListener, rawOptions);
96 | return rawRemoveEventListener.call(global, type, listener, options);
97 | };
98 |
99 | return function free() {
100 | listenerMap.forEach((listeners, type) => {
101 | listeners.forEach(({ rawListener, options }) => {
102 | global.removeEventListener(type, rawListener, options);
103 | });
104 | });
105 | listenerMap.clear();
106 | global.addEventListener = rawAddEventListener;
107 | global.removeEventListener = rawRemoveEventListener;
108 | return noop;
109 | };
110 | }
111 |
--------------------------------------------------------------------------------
/src/sandbox/snapshotSandbox.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Hydrogen
3 | * @since 2020-3-8
4 | */
5 | import type { SandBox } from '../interfaces';
6 | import { SandBoxType } from '../interfaces';
7 |
8 | function iter(obj: typeof window | Record, callbackFn: (prop: any) => void) {
9 | // eslint-disable-next-line guard-for-in, no-restricted-syntax
10 | for (const prop in obj) {
11 | // patch for clearInterval for compatible reason, see #1490
12 | if (obj.hasOwnProperty(prop) || prop === 'clearInterval') {
13 | callbackFn(prop);
14 | }
15 | }
16 | }
17 |
18 | /**
19 | * 基于 diff 方式实现的沙箱,用于不支持 Proxy 的低版本浏览器
20 | */
21 | export default class SnapshotSandbox implements SandBox {
22 | proxy: WindowProxy;
23 |
24 | name: string;
25 |
26 | type: SandBoxType;
27 |
28 | sandboxRunning = true;
29 |
30 | private windowSnapshot!: Window;
31 |
32 | private modifyPropsMap: Record = {};
33 |
34 | private deletePropsSet: Set = new Set();
35 |
36 | constructor(name: string) {
37 | this.name = name;
38 | this.proxy = window;
39 | this.type = SandBoxType.Snapshot;
40 | }
41 |
42 | active() {
43 | // 记录当前快照
44 | this.windowSnapshot = {} as Window;
45 | iter(window, (prop) => {
46 | this.windowSnapshot[prop] = window[prop];
47 | });
48 |
49 | // 恢复之前的变更
50 | Object.keys(this.modifyPropsMap).forEach((p: any) => {
51 | window[p] = this.modifyPropsMap[p];
52 | });
53 |
54 | // 删除之前删除的属性
55 | this.deletePropsSet.forEach((p: any) => {
56 | delete window[p];
57 | });
58 |
59 | this.sandboxRunning = true;
60 | }
61 |
62 | inactive() {
63 | this.modifyPropsMap = {};
64 |
65 | this.deletePropsSet.clear();
66 |
67 | iter(window, (prop) => {
68 | if (window[prop] !== this.windowSnapshot[prop]) {
69 | // 记录变更,恢复环境
70 | this.modifyPropsMap[prop] = window[prop];
71 | window[prop] = this.windowSnapshot[prop];
72 | }
73 | });
74 |
75 | iter(this.windowSnapshot, (prop) => {
76 | if (!window.hasOwnProperty(prop)) {
77 | // 记录被删除的属性,恢复环境
78 | this.deletePropsSet.add(prop);
79 | window[prop] = this.windowSnapshot[prop];
80 | }
81 | });
82 |
83 | if (process.env.NODE_ENV === 'development') {
84 | console.info(
85 | `[qiankun:sandbox] ${this.name} origin window restore...`,
86 | Object.keys(this.modifyPropsMap),
87 | this.deletePropsSet.keys(),
88 | );
89 | }
90 |
91 | this.sandboxRunning = false;
92 | }
93 |
94 | patchDocument(): void {}
95 | }
96 |
--------------------------------------------------------------------------------
/src/version.ts:
--------------------------------------------------------------------------------
1 | export { version } from '../package.json';
2 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "module": "esnext",
5 | "lib": ["es2018", "dom"],
6 | "declaration": true,
7 | "outDir": "./esm",
8 | "rootDir": "./",
9 | "importHelpers": false,
10 | "downlevelIteration": true,
11 | "strict": true,
12 | "noImplicitAny": true,
13 | "strictNullChecks": true,
14 | "resolveJsonModule": true,
15 | "strictFunctionTypes": true,
16 | "strictPropertyInitialization": true,
17 | "noImplicitThis": true,
18 | "alwaysStrict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "noImplicitReturns": true,
22 | "noFallthroughCasesInSwitch": true,
23 | "moduleResolution": "node",
24 | "typeRoots": ["typings", "node_modules/@types"],
25 | "allowSyntheticDefaultImports": true,
26 | "esModuleInterop": true,
27 | "experimentalDecorators": true,
28 | "paths": {
29 | "qiankun": ["./src"]
30 | }
31 | },
32 | "exclude": ["node_modules", "examples"]
33 | }
34 |
--------------------------------------------------------------------------------