@qlover/create-app 0.7.15 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/dist/configs/_common/.gitignore.template +6 -0
- package/dist/configs/_common/.prettierignore +17 -5
- package/dist/configs/_common/.vscode/settings.json +6 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/templates/next-app/.env.template +1 -1
- package/dist/templates/next-app/README.en.md +0 -1
- package/dist/templates/next-app/README.md +0 -1
- package/dist/templates/next-app/config/Identifier/api.ts +5 -5
- package/dist/templates/next-app/config/Identifier/common/admint.table.ts +69 -0
- package/dist/templates/next-app/config/Identifier/common/common.ts +76 -0
- package/dist/templates/next-app/config/Identifier/common/index.ts +3 -0
- package/dist/templates/next-app/config/Identifier/{validator.ts → common/validators.ts} +5 -5
- package/dist/templates/next-app/config/Identifier/index.ts +2 -12
- package/dist/templates/next-app/config/Identifier/pages/index.ts +6 -0
- package/dist/templates/next-app/config/Identifier/pages/page.admin.home.ts +27 -0
- package/dist/templates/next-app/config/Identifier/pages/page.admin.locales.ts +266 -0
- package/dist/templates/next-app/config/Identifier/pages/page.admin.user.ts +293 -0
- package/dist/templates/{react-app/config/Identifier → next-app/config/Identifier/pages}/page.home.ts +15 -22
- package/dist/templates/next-app/config/Identifier/{page.login.ts → pages/page.login.ts} +28 -34
- package/dist/templates/next-app/config/Identifier/{page.register.ts → pages/page.register.ts} +30 -29
- package/dist/templates/next-app/config/adminNavs.ts +19 -0
- package/dist/templates/next-app/config/common.ts +22 -13
- package/dist/templates/next-app/config/i18n/HomeI18n.ts +5 -5
- package/dist/templates/next-app/config/i18n/admin18n.ts +61 -19
- package/dist/templates/next-app/config/i18n/i18nConfig.ts +2 -0
- package/dist/templates/next-app/config/i18n/i18nKeyScheam.ts +36 -0
- package/dist/templates/next-app/config/i18n/loginI18n.ts +22 -22
- package/dist/templates/next-app/config/i18n/register18n.ts +23 -24
- package/dist/templates/next-app/docs/en/index.md +0 -1
- package/dist/templates/next-app/docs/en/project-structure.md +0 -1
- package/dist/templates/next-app/docs/zh/index.md +0 -1
- package/dist/templates/next-app/docs/zh/project-structure.md +0 -1
- package/dist/templates/next-app/make/generateLocales.ts +19 -12
- package/dist/templates/next-app/migrations/schema/LocalesSchema.ts +15 -0
- package/dist/templates/next-app/migrations/sql/1694244000000.sql +11 -0
- package/dist/templates/next-app/package.json +7 -3
- package/dist/templates/next-app/public/locales/en.json +172 -207
- package/dist/templates/next-app/public/locales/zh.json +172 -207
- package/dist/templates/next-app/src/app/[locale]/admin/locales/page.tsx +153 -0
- package/dist/templates/next-app/src/app/[locale]/admin/users/page.tsx +48 -50
- package/dist/templates/next-app/src/app/[locale]/login/LoginForm.tsx +2 -2
- package/dist/templates/next-app/src/app/api/admin/locales/create/route.ts +34 -0
- package/dist/templates/next-app/src/app/api/admin/locales/import/route.ts +40 -0
- package/dist/templates/next-app/src/app/api/admin/locales/route.ts +42 -0
- package/dist/templates/next-app/src/app/api/admin/locales/update/route.ts +32 -0
- package/dist/templates/next-app/src/app/api/locales/json/route.ts +44 -0
- package/dist/templates/next-app/src/base/cases/AdminPageManager.ts +1 -13
- package/dist/templates/next-app/src/base/cases/Datetime.ts +18 -0
- package/dist/templates/next-app/src/base/cases/DialogErrorPlugin.ts +12 -6
- package/dist/templates/next-app/src/base/cases/ResourceState.ts +17 -0
- package/dist/templates/next-app/src/base/cases/TranslateI18nInterface.ts +25 -0
- package/dist/templates/next-app/src/base/cases/ZodColumnBuilder.ts +200 -0
- package/dist/templates/next-app/src/base/port/ZodBuilderInterface.ts +8 -0
- package/dist/templates/next-app/src/base/services/AdminLocalesService.ts +20 -0
- package/dist/templates/next-app/src/base/services/AdminPageEvent.ts +26 -0
- package/dist/templates/next-app/src/base/services/AdminPageScheduler.ts +42 -0
- package/dist/templates/next-app/src/base/services/ResourceService.ts +122 -0
- package/dist/templates/next-app/src/base/services/adminApi/AdminLocalesApi.ts +104 -0
- package/dist/templates/next-app/src/base/services/adminApi/AdminUserApi.ts +38 -5
- package/dist/templates/next-app/src/base/services/appApi/AppApiPlugin.ts +1 -1
- package/dist/templates/next-app/src/i18n/request.ts +30 -1
- package/dist/templates/next-app/src/server/PageParams.ts +2 -10
- package/dist/templates/next-app/src/server/port/DBBridgeInterface.ts +5 -0
- package/dist/templates/next-app/src/server/port/DBTableInterface.ts +2 -0
- package/dist/templates/next-app/src/server/port/LocalesRepositoryInterface.ts +43 -0
- package/dist/templates/next-app/src/server/repositorys/LocalesRepository.ts +197 -0
- package/dist/templates/next-app/src/server/services/ApiLocaleService.ts +122 -0
- package/dist/templates/next-app/src/server/sqlBridges/SupabaseBridge.ts +60 -11
- package/dist/templates/next-app/src/server/validators/ExtendedExecutorError.ts +6 -0
- package/dist/templates/next-app/src/server/validators/LocalesValidator.ts +131 -0
- package/dist/templates/next-app/src/server/validators/LoginValidator.ts +2 -5
- package/dist/templates/next-app/src/server/validators/PaginationValidator.ts +32 -16
- package/dist/templates/next-app/src/styles/css/antd-themes/pagination/_default.css +2 -1
- package/dist/templates/next-app/src/styles/css/antd-themes/pagination/dark.css +28 -29
- package/dist/templates/next-app/src/styles/css/antd-themes/pagination/pink.css +2 -1
- package/dist/templates/next-app/src/uikit/components/AdminLayout.tsx +17 -3
- package/dist/templates/next-app/src/uikit/components/BaseHeader.tsx +5 -4
- package/dist/templates/next-app/src/uikit/components/BaseLayout.tsx +5 -4
- package/dist/templates/next-app/src/uikit/components/BootstrapsProvider.tsx +3 -2
- package/dist/templates/next-app/src/uikit/components/ComboProvider.tsx +1 -1
- package/dist/templates/next-app/src/uikit/components/EditableCell.tsx +118 -0
- package/dist/templates/next-app/src/uikit/components/LogoutButton.tsx +5 -6
- package/dist/templates/next-app/src/uikit/components/ThemeSwitcher.tsx +1 -1
- package/dist/templates/next-app/src/uikit/components/With.tsx +2 -2
- package/dist/templates/next-app/src/uikit/components/localesImportButton/LocalesImportButton.tsx +62 -0
- package/dist/templates/next-app/src/uikit/components/localesImportButton/LocalesImportEvent.ts +28 -0
- package/dist/templates/next-app/src/uikit/components/localesImportButton/import.module.css +6 -0
- package/dist/templates/next-app/src/uikit/hook/useI18nInterface.ts +8 -14
- package/dist/templates/next-app/src/uikit/hook/useWarnTranslations.ts +25 -0
- package/dist/templates/react-app/.prettierignore +17 -0
- package/dist/templates/react-app/README.en.md +71 -54
- package/dist/templates/react-app/README.md +35 -18
- package/dist/templates/react-app/__tests__/__mocks__/BootstrapTest.ts +14 -0
- package/dist/templates/react-app/__tests__/__mocks__/MockAppConfit.ts +1 -1
- package/dist/templates/react-app/__tests__/__mocks__/MockDialogHandler.ts +2 -2
- package/dist/templates/react-app/__tests__/__mocks__/MockLogger.ts +1 -1
- package/dist/templates/react-app/__tests__/__mocks__/components/TestApp.tsx +45 -0
- package/dist/templates/react-app/__tests__/__mocks__/components/TestBootstrapsProvider.tsx +34 -0
- package/dist/templates/react-app/__tests__/__mocks__/components/TestRouter.tsx +46 -0
- package/dist/templates/react-app/__tests__/__mocks__/components/index.ts +12 -0
- package/dist/templates/react-app/__tests__/__mocks__/createMockGlobals.ts +1 -2
- package/dist/templates/react-app/__tests__/__mocks__/testIOC/TestIOC.ts +51 -0
- package/dist/templates/react-app/__tests__/__mocks__/testIOC/TestIOCRegister.ts +69 -0
- package/dist/templates/react-app/__tests__/setup/index.ts +1 -51
- package/dist/templates/react-app/__tests__/setup/setupGlobal.ts +51 -0
- package/dist/templates/react-app/__tests__/src/App.structure.test.tsx +115 -0
- package/dist/templates/react-app/__tests__/src/base/cases/AppConfig.test.ts +2 -2
- package/dist/templates/react-app/__tests__/src/base/cases/AppError.test.ts +1 -1
- package/dist/templates/react-app/__tests__/src/base/cases/DialogHandler.test.ts +3 -5
- package/dist/templates/react-app/__tests__/src/base/cases/I18nKeyErrorPlugin.test.ts +13 -2
- package/dist/templates/react-app/__tests__/src/base/cases/InversifyContainer.test.ts +1 -1
- package/dist/templates/react-app/__tests__/src/base/cases/PublicAssetsPath.test.ts +1 -1
- package/dist/templates/react-app/__tests__/src/base/cases/RequestLogger.test.ts +5 -5
- package/dist/templates/react-app/__tests__/src/base/cases/RequestStatusCatcher.test.ts +1 -2
- package/dist/templates/react-app/__tests__/src/base/cases/RouterLoader.test.ts +25 -15
- package/dist/templates/react-app/__tests__/src/base/services/I18nService.test.ts +29 -15
- package/dist/templates/react-app/__tests__/src/core/IOC.test.ts +19 -9
- package/dist/templates/react-app/__tests__/src/core/bootstraps/BootstrapClient.test.ts +153 -0
- package/dist/templates/react-app/__tests__/src/core/bootstraps/BootstrapsApp.test.ts +9 -7
- package/dist/templates/react-app/__tests__/src/main.integration.test.tsx +4 -5
- package/dist/templates/react-app/__tests__/src/main.test.tsx +4 -4
- package/dist/templates/react-app/__tests__/src/uikit/components/BaseHeader.test.tsx +68 -59
- package/dist/templates/react-app/config/IOCIdentifier.ts +8 -8
- package/dist/templates/react-app/config/Identifier/{common.error.ts → common/common.error.ts} +5 -5
- package/dist/templates/react-app/config/Identifier/{common.ts → common/common.ts} +9 -9
- package/dist/templates/react-app/config/Identifier/common/index.ts +2 -0
- package/dist/templates/react-app/config/Identifier/index.ts +1 -9
- package/dist/templates/react-app/config/Identifier/pages/index.ts +8 -0
- package/dist/templates/react-app/config/Identifier/{page.about.ts → pages/page.about.ts} +34 -26
- package/dist/templates/react-app/config/Identifier/{page.executor.ts → pages/page.executor.ts} +47 -39
- package/dist/templates/{next-app/config/Identifier → react-app/config/Identifier/pages}/page.home.ts +24 -23
- package/dist/templates/react-app/config/Identifier/pages/page.identifiter.ts +102 -0
- package/dist/templates/react-app/config/Identifier/{page.jsonStorage.ts → pages/page.jsonStorage.ts} +18 -11
- package/dist/templates/react-app/config/Identifier/{page.login.ts → pages/page.login.ts} +37 -27
- package/dist/templates/react-app/config/Identifier/{page.register.ts → pages/page.register.ts} +37 -25
- package/dist/templates/react-app/config/Identifier/{page.request.ts → pages/page.request.ts} +34 -44
- package/dist/templates/react-app/config/app.router.ts +66 -69
- package/dist/templates/react-app/config/i18n/PageI18nInterface.ts +51 -0
- package/dist/templates/react-app/config/i18n/aboutI18n.ts +42 -0
- package/dist/templates/react-app/config/i18n/executorI18n.ts +51 -0
- package/dist/templates/react-app/config/i18n/homeI18n.ts +24 -0
- package/dist/templates/react-app/config/i18n/i18nConfig.ts +30 -0
- package/dist/templates/react-app/config/i18n/identifiter18n.ts +30 -0
- package/dist/templates/react-app/config/i18n/jsonStorage18n.ts +27 -0
- package/dist/templates/react-app/config/i18n/login18n.ts +42 -0
- package/dist/templates/react-app/config/i18n/notFoundI18n.ts +34 -0
- package/dist/templates/react-app/config/i18n/register18n.ts +40 -0
- package/dist/templates/react-app/config/i18n/request18n.ts +41 -0
- package/dist/templates/react-app/config/theme.ts +14 -4
- package/dist/templates/react-app/docs/en/bootstrap.md +1670 -341
- package/dist/templates/react-app/docs/en/development-guide.md +1021 -345
- package/dist/templates/react-app/docs/en/env.md +1132 -278
- package/dist/templates/react-app/docs/en/i18n.md +858 -147
- package/dist/templates/react-app/docs/en/index.md +733 -104
- package/dist/templates/react-app/docs/en/ioc.md +1228 -287
- package/dist/templates/react-app/docs/en/playwright/e2e-tests.md +321 -0
- package/dist/templates/react-app/docs/en/playwright/index.md +19 -0
- package/dist/templates/react-app/docs/en/playwright/installation-summary.md +332 -0
- package/dist/templates/react-app/docs/en/playwright/overview.md +222 -0
- package/dist/templates/react-app/docs/en/playwright/quickstart.md +325 -0
- package/dist/templates/react-app/docs/en/playwright/reorganization-notes.md +340 -0
- package/dist/templates/react-app/docs/en/playwright/setup-complete.md +290 -0
- package/dist/templates/react-app/docs/en/playwright/testing-guide.md +565 -0
- package/dist/templates/react-app/docs/en/store.md +1194 -184
- package/dist/templates/react-app/docs/en/why-no-globals.md +797 -0
- package/dist/templates/react-app/docs/zh/bootstrap.md +1670 -341
- package/dist/templates/react-app/docs/zh/development-guide.md +1021 -345
- package/dist/templates/react-app/docs/zh/env.md +1132 -275
- package/dist/templates/react-app/docs/zh/i18n.md +858 -147
- package/dist/templates/react-app/docs/zh/index.md +717 -104
- package/dist/templates/react-app/docs/zh/ioc.md +1229 -287
- package/dist/templates/react-app/docs/zh/playwright/e2e-tests.md +321 -0
- package/dist/templates/react-app/docs/zh/playwright/index.md +19 -0
- package/dist/templates/react-app/docs/zh/playwright/installation-summary.md +332 -0
- package/dist/templates/react-app/docs/zh/playwright/overview.md +222 -0
- package/dist/templates/react-app/docs/zh/playwright/quickstart.md +325 -0
- package/dist/templates/react-app/docs/zh/playwright/reorganization-notes.md +340 -0
- package/dist/templates/react-app/docs/zh/playwright/setup-complete.md +290 -0
- package/dist/templates/react-app/docs/zh/playwright/testing-guide.md +565 -0
- package/dist/templates/react-app/docs/zh/store.md +1192 -184
- package/dist/templates/react-app/docs/zh/why-no-globals.md +797 -0
- package/dist/templates/react-app/e2e/App.spec.ts +319 -0
- package/dist/templates/react-app/e2e/fixtures/base.fixture.ts +40 -0
- package/dist/templates/react-app/e2e/main.spec.ts +20 -0
- package/dist/templates/react-app/e2e/utils/test-helpers.ts +19 -0
- package/dist/templates/react-app/eslint.config.mjs +247 -0
- package/dist/templates/react-app/makes/eslint-utils.mjs +195 -0
- package/dist/templates/react-app/makes/generateTs2LocalesOptions.ts +26 -0
- package/dist/templates/react-app/package.json +31 -3
- package/dist/templates/react-app/playwright.config.ts +79 -0
- package/dist/templates/react-app/public/locales/en/common.json +190 -179
- package/dist/templates/react-app/public/locales/zh/common.json +190 -179
- package/dist/templates/react-app/src/App.tsx +15 -42
- package/dist/templates/react-app/src/base/apis/AiApi.ts +5 -5
- package/dist/templates/react-app/src/base/apis/feApi/FeApi.ts +1 -1
- package/dist/templates/react-app/src/base/apis/feApi/FeApiAdapter.ts +1 -1
- package/dist/templates/react-app/src/base/apis/feApi/FeApiBootstarp.ts +8 -8
- package/dist/templates/react-app/src/base/apis/feApi/FeApiType.ts +1 -1
- package/dist/templates/react-app/src/base/apis/userApi/UserApi.ts +6 -6
- package/dist/templates/react-app/src/base/apis/userApi/UserApiAdapter.ts +1 -1
- package/dist/templates/react-app/src/base/apis/userApi/UserApiBootstarp.ts +12 -14
- package/dist/templates/react-app/src/base/apis/userApi/UserApiType.ts +1 -1
- package/dist/templates/react-app/src/base/cases/DialogHandler.ts +5 -2
- package/dist/templates/react-app/src/base/cases/I18nKeyErrorPlugin.ts +3 -3
- package/dist/templates/react-app/src/base/cases/InversifyContainer.ts +3 -3
- package/dist/templates/react-app/src/base/cases/RequestLanguages.ts +2 -2
- package/dist/templates/react-app/src/base/cases/RequestLogger.ts +4 -4
- package/dist/templates/react-app/src/base/cases/RequestStatusCatcher.ts +1 -1
- package/dist/templates/react-app/src/base/cases/ResourceState.ts +23 -0
- package/dist/templates/react-app/src/base/cases/RouterLoader.ts +4 -4
- package/dist/templates/react-app/src/base/cases/TranslateI18nInterface.ts +26 -0
- package/dist/templates/react-app/src/base/port/ExecutorPageBridgeInterface.ts +2 -3
- package/dist/templates/react-app/src/base/port/I18nServiceInterface.ts +1 -1
- package/dist/templates/react-app/src/base/port/IOCInterface.ts +36 -0
- package/dist/templates/react-app/src/base/port/JSONStoragePageBridgeInterface.ts +2 -1
- package/dist/templates/react-app/src/base/port/ProcesserExecutorInterface.ts +1 -1
- package/dist/templates/react-app/src/base/port/RequestPageBridgeInterface.ts +2 -2
- package/dist/templates/react-app/src/base/port/RouteServiceInterface.ts +9 -5
- package/dist/templates/react-app/src/base/port/UserServiceInterface.ts +1 -1
- package/dist/templates/react-app/src/base/services/I18nService.ts +29 -29
- package/dist/templates/react-app/src/base/services/IdentifierService.ts +143 -0
- package/dist/templates/react-app/src/base/services/ProcesserExecutor.ts +3 -3
- package/dist/templates/react-app/src/base/services/RouteService.ts +27 -8
- package/dist/templates/react-app/src/base/services/UserService.ts +8 -8
- package/dist/templates/react-app/src/base/types/Page.ts +14 -2
- package/dist/templates/react-app/src/base/types/global.d.ts +1 -1
- package/dist/templates/react-app/src/core/IOC.ts +5 -46
- package/dist/templates/react-app/src/core/bootstraps/{BootstrapApp.ts → BootstrapClient.ts} +44 -17
- package/dist/templates/react-app/src/core/bootstraps/BootstrapsRegistry.ts +14 -7
- package/dist/templates/react-app/src/core/bootstraps/IocIdentifierTest.ts +1 -1
- package/dist/templates/react-app/src/core/bootstraps/PrintBootstrap.ts +1 -1
- package/dist/templates/react-app/src/core/clientIoc/ClientIOC.ts +40 -0
- package/dist/templates/react-app/src/core/{IocRegisterImpl.ts → clientIoc/ClientIOCRegister.ts} +35 -24
- package/dist/templates/react-app/src/core/globals.ts +9 -9
- package/dist/templates/react-app/src/main.tsx +4 -4
- package/dist/templates/react-app/src/pages/404.tsx +6 -3
- package/dist/templates/react-app/src/pages/500.tsx +5 -2
- package/dist/templates/react-app/src/pages/NoRouteFound.tsx +5 -0
- package/dist/templates/react-app/src/pages/auth/Layout.tsx +9 -6
- package/dist/templates/react-app/src/pages/auth/LoginPage.tsx +46 -56
- package/dist/templates/react-app/src/pages/auth/RegisterPage.tsx +46 -58
- package/dist/templates/react-app/src/pages/base/AboutPage.tsx +35 -40
- package/dist/templates/react-app/src/pages/base/ExecutorPage.tsx +51 -51
- package/dist/templates/react-app/src/pages/base/HomePage.tsx +14 -15
- package/dist/templates/react-app/src/pages/base/IdentifierPage.tsx +70 -11
- package/dist/templates/react-app/src/pages/base/JSONStoragePage.tsx +24 -25
- package/dist/templates/react-app/src/pages/base/Layout.tsx +2 -2
- package/dist/templates/react-app/src/pages/base/RedirectPathname.tsx +3 -2
- package/dist/templates/react-app/src/pages/base/RequestPage.tsx +41 -59
- package/dist/templates/react-app/src/styles/css/antd-themes/{_default.css → _common/_default.css} +85 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/{dark.css → _common/dark.css} +99 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/_common/index.css +3 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/{pink.css → _common/pink.css} +86 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/index.css +4 -3
- package/dist/templates/react-app/src/styles/css/antd-themes/menu/_default.css +108 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/menu/dark.css +67 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/menu/index.css +3 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/menu/pink.css +67 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/pagination/_default.css +34 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/pagination/dark.css +31 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/pagination/index.css +3 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/pagination/pink.css +36 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/table/_default.css +44 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/table/dark.css +43 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/table/index.css +3 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/table/pink.css +43 -0
- package/dist/templates/react-app/src/styles/css/page.css +4 -3
- package/dist/templates/react-app/src/styles/css/themes/_default.css +1 -0
- package/dist/templates/react-app/src/styles/css/themes/dark.css +1 -0
- package/dist/templates/react-app/src/styles/css/themes/pink.css +1 -0
- package/dist/templates/react-app/src/styles/css/zIndex.css +1 -1
- package/dist/templates/react-app/src/uikit/bridges/ExecutorPageBridge.ts +3 -3
- package/dist/templates/react-app/src/uikit/bridges/JSONStoragePageBridge.ts +2 -2
- package/dist/templates/react-app/src/uikit/bridges/NavigateBridge.ts +1 -1
- package/dist/templates/react-app/src/uikit/bridges/RequestPageBridge.ts +3 -3
- package/dist/templates/react-app/src/uikit/components/AppRouterProvider.tsx +35 -0
- package/dist/templates/react-app/src/uikit/components/BaseHeader.tsx +15 -11
- package/dist/templates/react-app/src/uikit/components/BaseRouteProvider.tsx +14 -11
- package/dist/templates/react-app/src/uikit/components/BaseRouteSeo.tsx +18 -0
- package/dist/templates/react-app/src/uikit/components/BootstrapsProvider.tsx +13 -0
- package/dist/templates/react-app/src/uikit/components/ClientSeo.tsx +62 -0
- package/dist/templates/react-app/src/uikit/components/ComboProvider.tsx +38 -0
- package/dist/templates/react-app/src/uikit/components/LanguageSwitcher.tsx +48 -27
- package/dist/templates/react-app/src/uikit/components/Loading.tsx +4 -2
- package/dist/templates/react-app/src/uikit/components/LocaleLink.tsx +4 -5
- package/dist/templates/react-app/src/uikit/components/LogoutButton.tsx +34 -11
- package/dist/templates/react-app/src/uikit/components/ProcessExecutorProvider.tsx +9 -5
- package/dist/templates/react-app/src/uikit/components/RouterRenderComponent.tsx +6 -3
- package/dist/templates/react-app/src/uikit/components/ThemeSwitcher.tsx +97 -40
- package/dist/templates/react-app/src/uikit/components/UserAuthProvider.tsx +5 -5
- package/dist/templates/react-app/src/uikit/components/With.tsx +17 -0
- package/dist/templates/react-app/src/uikit/contexts/BaseRouteContext.ts +17 -11
- package/dist/templates/react-app/src/uikit/contexts/IOCContext.ts +13 -0
- package/dist/templates/react-app/src/uikit/hooks/useAppTranslation.ts +26 -0
- package/dist/templates/react-app/src/uikit/hooks/useI18nGuard.ts +8 -11
- package/dist/templates/react-app/src/uikit/hooks/useI18nInterface.ts +25 -0
- package/dist/templates/react-app/src/uikit/hooks/useIOC.ts +35 -0
- package/dist/templates/react-app/src/uikit/hooks/useNavigateBridge.ts +3 -3
- package/dist/templates/react-app/src/uikit/hooks/useStrictEffect.ts +0 -1
- package/dist/templates/react-app/tsconfig.e2e.json +21 -0
- package/dist/templates/react-app/tsconfig.json +8 -1
- package/dist/templates/react-app/tsconfig.node.json +1 -1
- package/dist/templates/react-app/tsconfig.test.json +3 -1
- package/dist/templates/react-app/vite.config.ts +50 -34
- package/package.json +2 -1
- package/dist/configs/react-app/eslint.config.js +0 -94
- package/dist/templates/next-app/config/Identifier/common.error.ts +0 -41
- package/dist/templates/next-app/config/Identifier/common.ts +0 -69
- package/dist/templates/next-app/config/Identifier/page.about.ts +0 -181
- package/dist/templates/next-app/config/Identifier/page.admin.ts +0 -48
- package/dist/templates/next-app/config/Identifier/page.executor.ts +0 -272
- package/dist/templates/next-app/config/Identifier/page.identifiter.ts +0 -39
- package/dist/templates/next-app/config/Identifier/page.jsonStorage.ts +0 -72
- package/dist/templates/next-app/config/Identifier/page.request.ts +0 -182
- package/dist/templates/next-app/src/base/cases/ChatAction.ts +0 -21
- package/dist/templates/next-app/src/base/cases/FocusBarAction.ts +0 -36
- package/dist/templates/next-app/src/base/cases/RequestState.ts +0 -20
- package/dist/templates/next-app/src/base/port/AdminPageInterface.ts +0 -85
- package/dist/templates/next-app/src/base/port/AsyncStateInterface.ts +0 -7
- package/dist/templates/next-app/src/base/services/AdminUserService.ts +0 -45
- package/dist/templates/next-app/src/uikit/components/ChatRoot.tsx +0 -17
- package/dist/templates/next-app/src/uikit/components/chat/ChatActionInterface.ts +0 -30
- package/dist/templates/next-app/src/uikit/components/chat/ChatFocusBar.tsx +0 -65
- package/dist/templates/next-app/src/uikit/components/chat/ChatMessages.tsx +0 -59
- package/dist/templates/next-app/src/uikit/components/chat/ChatWrap.tsx +0 -28
- package/dist/templates/next-app/src/uikit/components/chat/FocusBarActionInterface.ts +0 -19
- package/dist/templates/next-app/src/uikit/hook/useMountedClient.ts +0 -17
- package/dist/templates/next-app/src/uikit/hook/useStore.ts +0 -15
- package/dist/templates/react-app/__tests__/__mocks__/I18nService.ts +0 -13
- package/dist/templates/react-app/__tests__/src/App.test.tsx +0 -139
- package/dist/templates/react-app/config/Identifier/page.identifiter.ts +0 -39
- package/dist/templates/react-app/config/i18n.ts +0 -15
- package/dist/templates/react-app/docs/en/project-structure.md +0 -434
- package/dist/templates/react-app/docs/zh/project-structure.md +0 -434
- package/dist/templates/react-app/src/base/cases/RequestState.ts +0 -20
- package/dist/templates/react-app/src/base/port/AsyncStateInterface.ts +0 -7
- package/dist/templates/react-app/src/uikit/hooks/useDocumentTitle.ts +0 -15
- package/dist/templates/react-app/src/uikit/hooks/useStore.ts +0 -15
|
@@ -1,240 +1,613 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Bootstrap Initializer
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 📋 Table of Contents
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
- [What is Bootstrap](#-what-is-bootstrap)
|
|
6
|
+
- [Why Bootstrap is Needed](#-why-bootstrap-is-needed)
|
|
7
|
+
- [Core Concepts](#-core-concepts)
|
|
8
|
+
- [Workflow](#-workflow)
|
|
9
|
+
- [Implementation in the Project](#-implementation-in-the-project)
|
|
10
|
+
- [Plugin System](#-plugin-system)
|
|
11
|
+
- [Practical Examples](#-practical-examples)
|
|
12
|
+
- [Testing: Core Advantage of Bootstrap](#-testing-core-advantage-of-bootstrap)
|
|
13
|
+
- [Best Practices](#-best-practices)
|
|
14
|
+
- [FAQ](#-faq)
|
|
6
15
|
|
|
7
|
-
|
|
16
|
+
---
|
|
8
17
|
|
|
9
|
-
|
|
10
|
-
- Load user information
|
|
11
|
-
- Initialize API configuration
|
|
12
|
-
- Set theme, language, etc.
|
|
18
|
+
## 🎯 What is Bootstrap
|
|
13
19
|
|
|
14
|
-
|
|
20
|
+
Bootstrap (Initializer) is the application's **initialization manager**, responsible for executing all necessary initialization logic before the application renders.
|
|
15
21
|
|
|
16
|
-
|
|
22
|
+
### Core Responsibilities
|
|
17
23
|
|
|
18
|
-
|
|
24
|
+
```
|
|
25
|
+
┌──────────────────────────────────────────────────┐
|
|
26
|
+
│ Bootstrap Initializer │
|
|
27
|
+
│ ┌────────────────────────────────────────────┐ │
|
|
28
|
+
│ │ 1. Create IOC Container │ │
|
|
29
|
+
│ │ 2. Inject Environment Variables │ │
|
|
30
|
+
│ │ 3. Encapsulate Global Variables │ │
|
|
31
|
+
│ │ 4. Register Business Plugins │ │
|
|
32
|
+
│ │ 5. Execute Initialization Logic │ │
|
|
33
|
+
│ └────────────────────────────────────────────┘ │
|
|
34
|
+
└──────────────────────────────────────────────────┘
|
|
35
|
+
↓
|
|
36
|
+
Application Starts Rendering
|
|
37
|
+
```
|
|
19
38
|
|
|
20
|
-
|
|
39
|
+
### Understanding by Analogy
|
|
21
40
|
|
|
22
|
-
|
|
41
|
+
Just like when a computer boots up, it needs to:
|
|
23
42
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
43
|
+
- ✅ Load drivers
|
|
44
|
+
- ✅ Start system services
|
|
45
|
+
- ✅ Check hardware status
|
|
46
|
+
- ✅ Initialize user environment
|
|
27
47
|
|
|
28
|
-
|
|
48
|
+
Bootstrap does similar things when the application starts:
|
|
29
49
|
|
|
30
|
-
|
|
50
|
+
- ✅ Initialize IOC container (dependency management)
|
|
51
|
+
- ✅ Inject environment configuration
|
|
52
|
+
- ✅ Encapsulate browser APIs
|
|
53
|
+
- ✅ Execute business initialization (user authentication, API configuration, etc.)
|
|
31
54
|
|
|
32
|
-
|
|
55
|
+
---
|
|
33
56
|
|
|
34
|
-
|
|
57
|
+
## 🤔 Why Bootstrap is Needed
|
|
35
58
|
|
|
36
|
-
|
|
59
|
+
### Problem: Pain Points of Traditional Approaches
|
|
37
60
|
|
|
38
|
-
|
|
39
|
-
export function App() {
|
|
40
|
-
const [loading, setLoading] = useState(false);
|
|
61
|
+
#### Example 1: Components Mixed with Initialization Logic
|
|
41
62
|
|
|
42
|
-
|
|
43
|
-
|
|
63
|
+
```typescript
|
|
64
|
+
// ❌ Traditional approach: handling initialization in components
|
|
65
|
+
function App() {
|
|
66
|
+
const [loading, setLoading] = useState(true);
|
|
67
|
+
const [user, setUser] = useState(null);
|
|
68
|
+
const [error, setError] = useState(null);
|
|
44
69
|
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
// Initialization logic mixed in component
|
|
45
72
|
fetchUserInfo()
|
|
46
|
-
.then(
|
|
47
|
-
|
|
73
|
+
.then(user => {
|
|
74
|
+
setUser(user);
|
|
75
|
+
// Also need to check permissions
|
|
76
|
+
if (!user.hasPermission) {
|
|
77
|
+
window.location.href = '/login';
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
.catch(error => {
|
|
81
|
+
setError(error);
|
|
48
82
|
})
|
|
49
|
-
.
|
|
83
|
+
.finally(() => {
|
|
50
84
|
setLoading(false);
|
|
51
85
|
});
|
|
52
86
|
}, []);
|
|
53
87
|
|
|
54
|
-
if (loading)
|
|
55
|
-
|
|
56
|
-
}
|
|
88
|
+
if (loading) return <div>Loading...</div>;
|
|
89
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
57
90
|
|
|
58
|
-
return <Router
|
|
91
|
+
return <Router />;
|
|
59
92
|
}
|
|
60
93
|
```
|
|
61
94
|
|
|
62
|
-
|
|
95
|
+
**Problems:**
|
|
63
96
|
|
|
64
|
-
|
|
97
|
+
- 😰 **Component overload**: UI components shouldn't handle business initialization
|
|
98
|
+
- 😰 **Complex state management**: Need to manage multiple states (loading, user, error)
|
|
99
|
+
- 😰 **Hard to test**: Initialization logic coupled with UI logic
|
|
100
|
+
- 😰 **Hard to reuse**: Initialization logic cannot be reused in other projects
|
|
101
|
+
- 😰 **Difficult to maintain**: Business logic changes affect component structure
|
|
65
102
|
|
|
66
|
-
|
|
103
|
+
#### Example 2: Multi-condition Initialization
|
|
67
104
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const [loading, setLoading] = useState(false);
|
|
105
|
+
```typescript
|
|
106
|
+
// ❌ More complex scenario: multiple initialization steps
|
|
107
|
+
function App() {
|
|
108
|
+
const [loading, setLoading] = useState(true);
|
|
73
109
|
const [userInfo, setUserInfo] = useState(null);
|
|
74
|
-
const [
|
|
110
|
+
const [permissions, setPermissions] = useState([]);
|
|
111
|
+
const [i18nLoaded, setI18nLoaded] = useState(false);
|
|
112
|
+
const [apiConfigured, setApiConfigured] = useState(false);
|
|
75
113
|
const location = useLocation();
|
|
76
114
|
|
|
77
115
|
useEffect(() => {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
116
|
+
const init = async () => {
|
|
117
|
+
try {
|
|
118
|
+
// Step 1: Configure API
|
|
119
|
+
await configureAPI();
|
|
120
|
+
setApiConfigured(true);
|
|
121
|
+
|
|
122
|
+
// Step 2: Load internationalization
|
|
123
|
+
await loadI18n();
|
|
124
|
+
setI18nLoaded(true);
|
|
125
|
+
|
|
126
|
+
// Step 3: Check user authentication
|
|
127
|
+
if (location.pathname !== '/login') {
|
|
128
|
+
const user = await fetchUserInfo();
|
|
84
129
|
setUserInfo(user);
|
|
85
130
|
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
window.location.href = '/
|
|
131
|
+
// Step 4: Load permissions
|
|
132
|
+
const perms = await fetchPermissions(user.id);
|
|
133
|
+
setPermissions(perms);
|
|
134
|
+
|
|
135
|
+
// Step 5: Permission check
|
|
136
|
+
if (!hasRequiredPermission(perms, location.pathname)) {
|
|
137
|
+
window.location.href = '/403';
|
|
138
|
+
return;
|
|
93
139
|
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
});
|
|
100
|
-
} else {
|
|
140
|
+
}
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.error('Initialization failed:', error);
|
|
143
|
+
window.location.href = '/error';
|
|
144
|
+
} finally {
|
|
101
145
|
setLoading(false);
|
|
102
146
|
}
|
|
103
|
-
|
|
147
|
+
};
|
|
104
148
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
return <div>Loading...</div>;
|
|
108
|
-
}
|
|
149
|
+
init();
|
|
150
|
+
}, [location.pathname]);
|
|
109
151
|
|
|
110
|
-
//
|
|
111
|
-
if (
|
|
112
|
-
return <
|
|
152
|
+
// Also need to handle various loading states...
|
|
153
|
+
if (loading || !apiConfigured || !i18nLoaded) {
|
|
154
|
+
return <LoadingScreen />;
|
|
113
155
|
}
|
|
114
156
|
|
|
115
157
|
return <Router />;
|
|
116
158
|
}
|
|
117
159
|
```
|
|
118
160
|
|
|
119
|
-
|
|
161
|
+
**Problems Further Aggravated:**
|
|
162
|
+
|
|
163
|
+
- 😰😰😰 **State explosion**: Need to manage multiple initialization states
|
|
164
|
+
- 😰😰😰 **Hard to extend**: Adding new initialization steps makes code more complex
|
|
165
|
+
- 😰😰😰 **Complex error handling**: Each step may fail, requiring extensive error handling code
|
|
166
|
+
- 😰😰😰 **Implicit dependencies**: Dependencies between steps are not clear
|
|
167
|
+
|
|
168
|
+
### Solution: Using Bootstrap
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
// ✅ Using Bootstrap: components become cleaner
|
|
172
|
+
function App() {
|
|
173
|
+
return (
|
|
174
|
+
<BootstrapsProvider>
|
|
175
|
+
<ComboProvider themeConfig={themeConfig}>
|
|
176
|
+
<AppRouterProvider pages={allPages} />
|
|
177
|
+
</ComboProvider>
|
|
178
|
+
</BootstrapsProvider>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// All initialization logic handled in Bootstrap
|
|
183
|
+
const bootstrap = new Bootstrap({
|
|
184
|
+
root: window,
|
|
185
|
+
logger,
|
|
186
|
+
ioc: { manager: IOC, register: new IocRegisterImpl({ pathname, appConfig }) },
|
|
187
|
+
envOptions: { /* environment variable config */ },
|
|
188
|
+
globalOptions: { /* global variable config */ }
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Register initialization plugins
|
|
192
|
+
bootstrap.use([
|
|
193
|
+
IOC(I18nService), // Internationalization service
|
|
194
|
+
new UserApiBootstrap(), // User API configuration
|
|
195
|
+
new FeApiBootstrap(), // Business API configuration
|
|
196
|
+
IOC(UserService) // User authentication service
|
|
197
|
+
]);
|
|
198
|
+
|
|
199
|
+
// Start application
|
|
200
|
+
await bootstrap.initialize();
|
|
201
|
+
await bootstrap.start();
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
**Advantages:**
|
|
120
205
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
206
|
+
- ✅ **Clear component responsibilities**: UI components only responsible for rendering
|
|
207
|
+
- ✅ **Logic separation**: Initialization logic independent of UI
|
|
208
|
+
- ✅ **Easy to test**: Can independently test each initialization step
|
|
209
|
+
- ✅ **Easy to extend**: Adding new initialization steps only requires adding new plugins
|
|
210
|
+
- ✅ **Easy to reuse**: Same initialization logic can be used in different projects
|
|
126
211
|
|
|
127
|
-
|
|
212
|
+
---
|
|
128
213
|
|
|
129
|
-
##
|
|
214
|
+
## 💡 Core Concepts
|
|
130
215
|
|
|
131
|
-
|
|
216
|
+
### 1. Plugin Architecture
|
|
132
217
|
|
|
133
|
-
|
|
134
|
-
2. **State Management**: Manage application state through store, achieving reactive UI updates
|
|
135
|
-
3. **Separation of Concerns**: Separate business logic from UI components
|
|
218
|
+
Bootstrap adopts a plugin design where each plugin is responsible for a specific initialization task.
|
|
136
219
|
|
|
137
|
-
|
|
220
|
+
```typescript
|
|
221
|
+
// Plugin interface
|
|
222
|
+
export interface BootstrapExecutorPlugin {
|
|
223
|
+
readonly pluginName: string;
|
|
138
224
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
- **Modular**: Support IOC container, environment variable injection, global variable injection, and other modules
|
|
225
|
+
// Execute before initialization
|
|
226
|
+
onBefore?(context: BootstrapContext): void | Promise<void>;
|
|
142
227
|
|
|
143
|
-
|
|
228
|
+
// Execute during initialization
|
|
229
|
+
onExecute?(context: BootstrapContext): void | Promise<void>;
|
|
144
230
|
|
|
231
|
+
// Execute after initialization
|
|
232
|
+
onAfter?(context: BootstrapContext): void | Promise<void>;
|
|
233
|
+
|
|
234
|
+
// Error handling
|
|
235
|
+
onError?(error: Error, context: BootstrapContext): void | Promise<void>;
|
|
236
|
+
}
|
|
145
237
|
```
|
|
146
|
-
|
|
238
|
+
|
|
239
|
+
### 2. Lifecycle
|
|
240
|
+
|
|
241
|
+
```
|
|
242
|
+
┌────────────────────────────────────────────────┐
|
|
243
|
+
│ Bootstrap Lifecycle │
|
|
244
|
+
│ │
|
|
245
|
+
│ initialize() │
|
|
246
|
+
│ ├── Create IOC container │
|
|
247
|
+
│ ├── Inject environment variables │
|
|
248
|
+
│ └── Encapsulate global variables │
|
|
249
|
+
│ │
|
|
250
|
+
│ start() │
|
|
251
|
+
│ ├── onBefore: Pre-initialization │
|
|
252
|
+
│ │ ├── Configure API │
|
|
253
|
+
│ │ ├── Load internationalization │
|
|
254
|
+
│ │ └── Check user authentication │
|
|
255
|
+
│ │ │
|
|
256
|
+
│ ├── onExecute: Execute main logic │
|
|
257
|
+
│ │ └── Execute business initialization │
|
|
258
|
+
│ │ │
|
|
259
|
+
│ ├── onAfter: Post-processing │
|
|
260
|
+
│ │ └── Cleanup resources, log records │
|
|
261
|
+
│ │ │
|
|
262
|
+
│ └── onError: Error handling │
|
|
263
|
+
│ └── Error capture and handling │
|
|
264
|
+
└────────────────────────────────────────────────┘
|
|
147
265
|
```
|
|
148
266
|
|
|
149
|
-
###
|
|
267
|
+
### 3. Dependency Injection
|
|
150
268
|
|
|
151
|
-
|
|
269
|
+
Bootstrap is deeply integrated with the IOC container, and all plugins can obtain services through dependency injection.
|
|
152
270
|
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const [data, setData] = useState(null);
|
|
271
|
+
```typescript
|
|
272
|
+
@injectable()
|
|
273
|
+
export class UserService implements ExecutorPlugin {
|
|
274
|
+
readonly pluginName = 'UserService';
|
|
158
275
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}, []);
|
|
276
|
+
constructor(
|
|
277
|
+
@inject(UserApi) private api: UserApi,
|
|
278
|
+
@inject(IOCIdentifier.AppConfig) private config: AppConfig,
|
|
279
|
+
@inject(IOCIdentifier.LocalStorageEncrypt) private storage: Storage
|
|
280
|
+
) {}
|
|
165
281
|
|
|
166
|
-
|
|
167
|
-
|
|
282
|
+
async onBefore(): Promise<void> {
|
|
283
|
+
// Use injected dependencies to execute initialization
|
|
284
|
+
const token = this.storage.getItem('token');
|
|
285
|
+
if (token) {
|
|
286
|
+
await this.api.getUserInfo(token);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
168
289
|
}
|
|
169
290
|
```
|
|
170
291
|
|
|
171
|
-
|
|
292
|
+
---
|
|
172
293
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
294
|
+
## 🔄 Workflow
|
|
295
|
+
|
|
296
|
+
### Complete Workflow Diagram
|
|
297
|
+
|
|
298
|
+
```
|
|
299
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
300
|
+
│ 1. main.tsx: Application entry point │
|
|
301
|
+
│ BootstrapClient.main({ root: window, bootHref, ioc }) │
|
|
302
|
+
└────────────────────┬────────────────────────────────────────┘
|
|
303
|
+
↓
|
|
304
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
305
|
+
│ 2. BootstrapClient: Create Bootstrap instance │
|
|
306
|
+
│ - Create IOC container │
|
|
307
|
+
│ - Configure environment variable injection │
|
|
308
|
+
│ - Configure global variable encapsulation │
|
|
309
|
+
└────────────────────┬────────────────────────────────────────┘
|
|
310
|
+
↓
|
|
311
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
312
|
+
│ 3. Bootstrap.initialize(): Initialize │
|
|
313
|
+
│ ✅ IOC container initialization │
|
|
314
|
+
│ ✅ Environment variables injected to AppConfig │
|
|
315
|
+
│ ✅ Global variables encapsulated (localStorage, window) │
|
|
316
|
+
└────────────────────┬────────────────────────────────────────┘
|
|
317
|
+
↓
|
|
318
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
319
|
+
│ 4. BootstrapsRegistry: Register business plugins │
|
|
320
|
+
│ - I18nService: Internationalization service │
|
|
321
|
+
│ - UserApiBootstrap: User API configuration │
|
|
322
|
+
│ - FeApiBootstrap: Business API configuration │
|
|
323
|
+
│ - UserService: User authentication service │
|
|
324
|
+
└────────────────────┬────────────────────────────────────────┘
|
|
325
|
+
↓
|
|
326
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
327
|
+
│ 5. Bootstrap.start(): Start │
|
|
328
|
+
│ ↓ │
|
|
329
|
+
│ onBefore phase: │
|
|
330
|
+
│ ├── I18nService.onBefore() → Load translation resources │
|
|
331
|
+
│ ├── UserApiBootstrap.onBefore() → Configure API plugins │
|
|
332
|
+
│ ├── FeApiBootstrap.onBefore() → Configure business API │
|
|
333
|
+
│ └── UserService.onBefore() → Check user authentication │
|
|
334
|
+
│ ↓ │
|
|
335
|
+
│ onExecute phase: │
|
|
336
|
+
│ └── Execute plugin main logic │
|
|
337
|
+
│ ↓ │
|
|
338
|
+
│ onAfter phase: │
|
|
339
|
+
│ └── Cleanup and logging │
|
|
340
|
+
└────────────────────┬────────────────────────────────────────┘
|
|
341
|
+
↓
|
|
342
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
343
|
+
│ 6. React rendering │
|
|
344
|
+
│ ReactDOM.render(<App />) │
|
|
345
|
+
└─────────────────────────────────────────────────────────────┘
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
## 🛠️ Implementation in the Project
|
|
351
|
+
|
|
352
|
+
### File Structure
|
|
353
|
+
|
|
354
|
+
```
|
|
355
|
+
src/
|
|
356
|
+
├── main.tsx # Application entry point
|
|
357
|
+
├── core/
|
|
358
|
+
│ ├── bootstraps/
|
|
359
|
+
│ │ ├── BootstrapClient.ts # Bootstrap initializer
|
|
360
|
+
│ │ ├── BootstrapsRegistry.ts # Plugin registry
|
|
361
|
+
│ │ ├── PrintBootstrap.ts # Print logging plugin
|
|
362
|
+
│ │ └── IocIdentifierTest.ts # IOC test plugin
|
|
363
|
+
│ ├── globals.ts # Global variable encapsulation
|
|
364
|
+
│ └── clientIoc/
|
|
365
|
+
│ ├── ClientIOC.ts # IOC container
|
|
366
|
+
│ └── ClientIOCRegister.ts # IOC registrar
|
|
367
|
+
├── base/
|
|
368
|
+
│ ├── services/
|
|
369
|
+
│ │ ├── UserService.ts # User service (plugin)
|
|
370
|
+
│ │ └── I18nService.ts # Internationalization service (plugin)
|
|
371
|
+
│ └── apis/
|
|
372
|
+
│ ├── userApi/
|
|
373
|
+
│ │ └── UserApiBootstrap.ts # User API configuration plugin
|
|
374
|
+
│ └── feApi/
|
|
375
|
+
│ └── FeApiBootstrap.ts # Business API configuration plugin
|
|
376
|
+
└── uikit/
|
|
377
|
+
└── components/
|
|
378
|
+
└── BootstrapsProvider.tsx # Bootstrap Provider
|
|
379
|
+
```
|
|
177
380
|
|
|
178
|
-
|
|
179
|
-
|
|
381
|
+
### 1. Entry File: main.tsx
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
// src/main.tsx
|
|
385
|
+
import 'reflect-metadata';
|
|
386
|
+
import { StrictMode } from 'react';
|
|
387
|
+
import { createRoot } from 'react-dom/client';
|
|
388
|
+
import App from './App.tsx';
|
|
389
|
+
import { BootstrapClient } from './core/bootstraps/BootstrapClient';
|
|
390
|
+
import { clientIOC } from './core/clientIoc/ClientIOC.ts';
|
|
391
|
+
|
|
392
|
+
// 🚀 Start Bootstrap
|
|
393
|
+
BootstrapClient.main({
|
|
394
|
+
root: window, // Inject browser environment
|
|
395
|
+
bootHref: window.location.href, // Inject startup URL
|
|
396
|
+
ioc: clientIOC // Inject IOC container
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// Render React application
|
|
400
|
+
createRoot(document.getElementById('root')!).render(
|
|
401
|
+
<StrictMode>
|
|
402
|
+
<App />
|
|
403
|
+
</StrictMode>
|
|
404
|
+
);
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### 2. Bootstrap Initializer: BootstrapClient.ts
|
|
408
|
+
|
|
409
|
+
```typescript
|
|
410
|
+
// src/core/bootstraps/BootstrapClient.ts
|
|
411
|
+
import { Bootstrap } from '@qlover/corekit-bridge';
|
|
412
|
+
import { envBlackList, envPrefix, browserGlobalsName } from '@config/common';
|
|
413
|
+
import * as globals from '../globals';
|
|
414
|
+
import { BootstrapsRegistry } from './BootstrapsRegistry';
|
|
415
|
+
|
|
416
|
+
export class BootstrapClient {
|
|
417
|
+
static async main(args: BootstrapClientArgs): Promise<BootstrapClientArgs> {
|
|
418
|
+
const { root, bootHref, ioc, iocRegister } = args;
|
|
419
|
+
const { logger, appConfig } = globals;
|
|
420
|
+
|
|
421
|
+
// 1️⃣ Create IOC container
|
|
422
|
+
const IOC = ioc.create({
|
|
423
|
+
pathname: bootHref,
|
|
424
|
+
appConfig: appConfig
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
// 2️⃣ Create Bootstrap instance
|
|
428
|
+
const bootstrap = new Bootstrap({
|
|
429
|
+
root,
|
|
430
|
+
logger,
|
|
431
|
+
// IOC container configuration
|
|
432
|
+
ioc: {
|
|
433
|
+
manager: IOC,
|
|
434
|
+
register: iocRegister
|
|
435
|
+
},
|
|
436
|
+
// Environment variable injection configuration
|
|
437
|
+
envOptions: {
|
|
438
|
+
target: appConfig, // Inject to AppConfig
|
|
439
|
+
source: Object.assign({}, import.meta.env, {
|
|
440
|
+
[envPrefix + 'BOOT_HREF']: bootHref // Add startup URL
|
|
441
|
+
}),
|
|
442
|
+
prefix: envPrefix, // Environment variable prefix
|
|
443
|
+
blackList: envBlackList // Blacklist
|
|
444
|
+
},
|
|
445
|
+
// Global variable encapsulation configuration
|
|
446
|
+
globalOptions: {
|
|
447
|
+
sources: globals, // Encapsulated global variables
|
|
448
|
+
target: browserGlobalsName // Mount target
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
try {
|
|
453
|
+
logger.info('bootstrap start...');
|
|
454
|
+
|
|
455
|
+
// 3️⃣ Initialize Bootstrap
|
|
456
|
+
await bootstrap.initialize();
|
|
457
|
+
|
|
458
|
+
// 4️⃣ Register business plugins
|
|
459
|
+
const bootstrapsRegistry = new BootstrapsRegistry(IOC);
|
|
460
|
+
|
|
461
|
+
// 5️⃣ Start application
|
|
462
|
+
await bootstrap.use(bootstrapsRegistry.register()).start();
|
|
463
|
+
|
|
464
|
+
logger.info('bootstrap completed successfully');
|
|
465
|
+
} catch (error) {
|
|
466
|
+
logger.error(`${appConfig.appName} startup error:`, error);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return args;
|
|
470
|
+
}
|
|
180
471
|
}
|
|
472
|
+
```
|
|
181
473
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
474
|
+
**Key Step Analysis:**
|
|
475
|
+
|
|
476
|
+
1. **Create IOC Container** - Manage all dependencies uniformly
|
|
477
|
+
2. **Create Bootstrap Instance** - Configure initialization parameters
|
|
478
|
+
3. **Initialize** - Execute IOC, environment variables, and global variable initialization
|
|
479
|
+
4. **Register Plugins** - Add business initialization logic
|
|
480
|
+
5. **Start** - Execute lifecycle methods of all plugins
|
|
481
|
+
|
|
482
|
+
### 3. Plugin Registry: BootstrapsRegistry.ts
|
|
483
|
+
|
|
484
|
+
```typescript
|
|
485
|
+
// src/core/bootstraps/BootstrapsRegistry.ts
|
|
486
|
+
import { IOCIdentifier } from '@config/IOCIdentifier';
|
|
487
|
+
import { UserApiBootstarp } from '@/base/apis/userApi/UserApiBootstarp';
|
|
488
|
+
import { FeApiBootstarp } from '@/base/apis/feApi/FeApiBootstarp';
|
|
489
|
+
import { AiApiBootstarp } from '@/base/apis/AiApi';
|
|
490
|
+
|
|
491
|
+
export class BootstrapsRegistry {
|
|
492
|
+
constructor(
|
|
493
|
+
protected IOC: IOCFunctionInterface<IOCIdentifierMap, IOCContainerInterface>
|
|
494
|
+
) {}
|
|
495
|
+
|
|
496
|
+
get appConfig(): EnvConfigInterface {
|
|
497
|
+
return this.IOC(IOCIdentifier.AppConfig);
|
|
198
498
|
}
|
|
199
|
-
});
|
|
200
499
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
500
|
+
/**
|
|
501
|
+
* Register all business plugins
|
|
502
|
+
*/
|
|
503
|
+
register(): BootstrapExecutorPlugin[] {
|
|
504
|
+
const IOC = this.IOC;
|
|
505
|
+
|
|
506
|
+
const bootstrapList = [
|
|
507
|
+
// 1. Internationalization service (needs to be initialized first)
|
|
508
|
+
IOC(IOCIdentifier.I18nServiceInterface),
|
|
509
|
+
|
|
510
|
+
// 2. API configuration plugins
|
|
511
|
+
new UserApiBootstarp(), // User API
|
|
512
|
+
new FeApiBootstarp(), // Business API
|
|
513
|
+
AiApiBootstarp, // AI API
|
|
514
|
+
|
|
515
|
+
// 3. Other plugins
|
|
516
|
+
IOC(IOCIdentifier.I18nKeyErrorPlugin),
|
|
517
|
+
IOC(IOCIdentifier.ProcesserExecutorInterface)
|
|
518
|
+
];
|
|
519
|
+
|
|
520
|
+
// Development environment: Add debug plugins
|
|
521
|
+
if (!this.appConfig.isProduction) {
|
|
522
|
+
bootstrapList.push(printBootstrap);
|
|
523
|
+
}
|
|
207
524
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
525
|
+
return bootstrapList;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
211
528
|
```
|
|
212
529
|
|
|
213
|
-
**
|
|
530
|
+
**Plugin Order is Important:**
|
|
531
|
+
|
|
532
|
+
- ✅ Internationalization service initialized first (other plugins may need translations)
|
|
533
|
+
- ✅ API configuration before business logic
|
|
534
|
+
- ✅ Development tools only loaded in development environment
|
|
535
|
+
|
|
536
|
+
---
|
|
537
|
+
|
|
538
|
+
## 🔌 Plugin System
|
|
214
539
|
|
|
215
|
-
|
|
216
|
-
- ✅ Business logic separated into bootstrapper
|
|
217
|
-
- ✅ Can independently test business logic
|
|
218
|
-
- ✅ Can reuse business logic in other UI frameworks
|
|
540
|
+
### Plugin Types
|
|
219
541
|
|
|
220
|
-
|
|
542
|
+
#### 1. Service Plugins (via IOC Injection)
|
|
543
|
+
|
|
544
|
+
```typescript
|
|
545
|
+
// src/base/services/I18nService.ts
|
|
546
|
+
@injectable()
|
|
547
|
+
export class I18nService implements ExecutorPlugin {
|
|
548
|
+
readonly pluginName = 'I18nService';
|
|
549
|
+
|
|
550
|
+
constructor(@inject(IOCIdentifier.AppConfig) private config: AppConfig) {}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Load translation resources before Bootstrap starts
|
|
554
|
+
*/
|
|
555
|
+
async onBefore(): Promise<void> {
|
|
556
|
+
await i18next.init({
|
|
557
|
+
lng: this.config.defaultLanguage,
|
|
558
|
+
fallbackLng: 'en',
|
|
559
|
+
resources: this.loadResources()
|
|
560
|
+
});
|
|
561
|
+
}
|
|
221
562
|
|
|
222
|
-
|
|
223
|
-
//
|
|
563
|
+
private loadResources() {
|
|
564
|
+
// Load translation resources
|
|
565
|
+
return {
|
|
566
|
+
/* ... */
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Registration method
|
|
572
|
+
bootstrap.use([
|
|
573
|
+
IOC(IOCIdentifier.I18nServiceInterface) // Get from IOC container
|
|
574
|
+
]);
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
#### 2. Configuration Plugins (Independent Instances)
|
|
578
|
+
|
|
579
|
+
```typescript
|
|
580
|
+
// src/base/apis/userApi/UserApiBootstrap.ts
|
|
224
581
|
export class UserApiBootstarp implements BootstrapExecutorPlugin {
|
|
225
582
|
readonly pluginName = 'UserApiBootstarp';
|
|
226
583
|
|
|
584
|
+
/**
|
|
585
|
+
* Configure User API plugins
|
|
586
|
+
*/
|
|
227
587
|
onBefore({ parameters: { ioc } }: BootstrapContext): void {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
588
|
+
const userApi = ioc.get<UserApi>(UserApi);
|
|
589
|
+
|
|
590
|
+
// Add URL handling plugin
|
|
591
|
+
userApi.usePlugin(new FetchURLPlugin());
|
|
592
|
+
|
|
593
|
+
// Add Mock plugin (development environment)
|
|
594
|
+
userApi.usePlugin(ioc.get(IOCIdentifier.ApiMockPlugin));
|
|
595
|
+
|
|
596
|
+
// Add request logging plugin
|
|
597
|
+
userApi.usePlugin(ioc.get(RequestLogger));
|
|
234
598
|
}
|
|
235
599
|
}
|
|
236
600
|
|
|
237
|
-
//
|
|
601
|
+
// Registration method
|
|
602
|
+
bootstrap.use([
|
|
603
|
+
new UserApiBootstarp() // Create instance directly
|
|
604
|
+
]);
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
#### 3. Business Logic Plugins
|
|
608
|
+
|
|
609
|
+
```typescript
|
|
610
|
+
// src/base/services/UserService.ts
|
|
238
611
|
@injectable()
|
|
239
612
|
export class UserService
|
|
240
613
|
extends UserAuthService<UserInfo>
|
|
@@ -243,8 +616,13 @@ export class UserService
|
|
|
243
616
|
readonly pluginName = 'UserService';
|
|
244
617
|
|
|
245
618
|
constructor(
|
|
246
|
-
@inject(
|
|
247
|
-
|
|
619
|
+
@inject(IOCIdentifier.RouteServiceInterface)
|
|
620
|
+
protected routerService: RouteServiceInterface,
|
|
621
|
+
@inject(UserApi)
|
|
622
|
+
userApi: UserAuthApiInterface<UserInfo>,
|
|
623
|
+
@inject(IOCIdentifier.AppConfig) appConfig: AppConfig,
|
|
624
|
+
@inject(IOCIdentifier.LocalStorageEncrypt)
|
|
625
|
+
storage: SyncStorageInterface<string, string>
|
|
248
626
|
) {
|
|
249
627
|
super(userApi, {
|
|
250
628
|
userStorage: {
|
|
@@ -258,305 +636,1256 @@ export class UserService
|
|
|
258
636
|
});
|
|
259
637
|
}
|
|
260
638
|
|
|
261
|
-
|
|
639
|
+
/**
|
|
640
|
+
* Check user authentication status on application startup
|
|
641
|
+
*/
|
|
262
642
|
async onBefore(): Promise<void> {
|
|
643
|
+
// If already logged in, return directly
|
|
263
644
|
if (this.isAuthenticated()) {
|
|
264
645
|
return;
|
|
265
646
|
}
|
|
266
647
|
|
|
648
|
+
// Try to restore user info from storage
|
|
267
649
|
const userToken = this.getToken();
|
|
268
650
|
if (!userToken) {
|
|
269
651
|
throw new AppError('NO_USER_TOKEN');
|
|
270
652
|
}
|
|
271
653
|
|
|
654
|
+
// Get user info
|
|
272
655
|
await this.userInfo();
|
|
273
|
-
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
getToken(): string | null {
|
|
659
|
+
return this.credential();
|
|
274
660
|
}
|
|
275
661
|
}
|
|
662
|
+
```
|
|
276
663
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
664
|
+
### Plugin Lifecycle Details
|
|
665
|
+
|
|
666
|
+
```typescript
|
|
667
|
+
export interface BootstrapExecutorPlugin {
|
|
668
|
+
readonly pluginName: string;
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* onBefore: Execute before initialization
|
|
672
|
+
*
|
|
673
|
+
* Use cases:
|
|
674
|
+
* - Configure API clients
|
|
675
|
+
* - Load resources (translations, themes, etc.)
|
|
676
|
+
* - Check user authentication
|
|
677
|
+
* - Initialize third-party libraries
|
|
678
|
+
*/
|
|
679
|
+
onBefore?(context: BootstrapContext): void | Promise<void>;
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* onExecute: Execute during initialization
|
|
683
|
+
*
|
|
684
|
+
* Use cases:
|
|
685
|
+
* - Execute main business logic
|
|
686
|
+
* - Start background tasks
|
|
687
|
+
*/
|
|
688
|
+
onExecute?(context: BootstrapContext): void | Promise<void>;
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* onAfter: Execute after initialization
|
|
692
|
+
*
|
|
693
|
+
* Use cases:
|
|
694
|
+
* - Cleanup temporary resources
|
|
695
|
+
* - Record startup logs
|
|
696
|
+
* - Send analytics data
|
|
697
|
+
*/
|
|
698
|
+
onAfter?(context: BootstrapContext): void | Promise<void>;
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* onError: Error handling
|
|
702
|
+
*
|
|
703
|
+
* Use cases:
|
|
704
|
+
* - Capture plugin errors
|
|
705
|
+
* - Error logging
|
|
706
|
+
* - Error recovery
|
|
707
|
+
*/
|
|
708
|
+
onError?(error: Error, context: BootstrapContext): void | Promise<void>;
|
|
709
|
+
}
|
|
710
|
+
```
|
|
285
711
|
|
|
286
|
-
|
|
287
|
-
bootstrap.use([
|
|
288
|
-
IOC(UserService), // User authentication service
|
|
289
|
-
new UserApiBootstarp(), // User API configuration
|
|
290
|
-
IOC(I18nService) // Internationalization service
|
|
291
|
-
]);
|
|
712
|
+
---
|
|
292
713
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
714
|
+
## 🎯 Practical Examples
|
|
715
|
+
|
|
716
|
+
### Example 1: Internationalization Plugin
|
|
717
|
+
|
|
718
|
+
```typescript
|
|
719
|
+
// src/base/services/I18nService.ts
|
|
720
|
+
import i18next from 'i18next';
|
|
721
|
+
import { injectable, inject } from 'inversify';
|
|
722
|
+
import { IOCIdentifier } from '@config/IOCIdentifier';
|
|
723
|
+
import type { AppConfig } from '@/base/cases/AppConfig';
|
|
724
|
+
|
|
725
|
+
@injectable()
|
|
726
|
+
export class I18nService implements ExecutorPlugin {
|
|
727
|
+
readonly pluginName = 'I18nService';
|
|
728
|
+
|
|
729
|
+
constructor(@inject(IOCIdentifier.AppConfig) private config: AppConfig) {}
|
|
730
|
+
|
|
731
|
+
async onBefore(): Promise<void> {
|
|
732
|
+
// Load translation resources
|
|
733
|
+
const resources = this.loadAllResources();
|
|
734
|
+
|
|
735
|
+
// Initialize i18next
|
|
736
|
+
await i18next.init({
|
|
737
|
+
lng: this.config.defaultLanguage || 'zh',
|
|
738
|
+
fallbackLng: 'en',
|
|
739
|
+
resources,
|
|
740
|
+
interpolation: {
|
|
741
|
+
escapeValue: false
|
|
742
|
+
}
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
console.log('✅ I18n initialized:', i18next.language);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
private loadAllResources() {
|
|
749
|
+
// Load all translation resources from config files
|
|
750
|
+
return {
|
|
751
|
+
zh: {
|
|
752
|
+
translation: require('@config/i18n/zh').default
|
|
753
|
+
},
|
|
754
|
+
en: {
|
|
755
|
+
translation: require('@config/i18n/en').default
|
|
756
|
+
}
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
t(key: string, options?: any): string {
|
|
761
|
+
return i18next.t(key, options);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
296
764
|
```
|
|
297
765
|
|
|
298
|
-
|
|
766
|
+
### Example 2: API Configuration Plugin
|
|
299
767
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
768
|
+
```typescript
|
|
769
|
+
// src/base/apis/feApi/FeApiBootstrap.ts
|
|
770
|
+
export class FeApiBootstarp implements BootstrapExecutorPlugin {
|
|
771
|
+
readonly pluginName = 'FeApiBootstarp';
|
|
304
772
|
|
|
305
|
-
|
|
773
|
+
onBefore({ parameters: { ioc } }: BootstrapContext): void {
|
|
774
|
+
const feApi = ioc.get<FeApi>(FeApi);
|
|
775
|
+
const appConfig = ioc.get<AppConfig>(IOCIdentifier.AppConfig);
|
|
776
|
+
|
|
777
|
+
// 1. Configure base URL
|
|
778
|
+
feApi.setBaseURL(appConfig.apiBaseUrl);
|
|
779
|
+
|
|
780
|
+
// 2. Add authentication plugin
|
|
781
|
+
feApi.usePlugin(
|
|
782
|
+
new AuthTokenPlugin({
|
|
783
|
+
getToken: () => {
|
|
784
|
+
const storage = ioc.get(IOCIdentifier.LocalStorageEncrypt);
|
|
785
|
+
return storage.getItem('token');
|
|
786
|
+
}
|
|
787
|
+
})
|
|
788
|
+
);
|
|
789
|
+
|
|
790
|
+
// 3. Add error handling plugin
|
|
791
|
+
feApi.usePlugin(
|
|
792
|
+
new ErrorHandlerPlugin({
|
|
793
|
+
onError: (error) => {
|
|
794
|
+
if (error.status === 401) {
|
|
795
|
+
// Unauthorized, redirect to login
|
|
796
|
+
const router = ioc.get(IOCIdentifier.RouteServiceInterface);
|
|
797
|
+
router.push('/login');
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
})
|
|
801
|
+
);
|
|
306
802
|
|
|
307
|
-
|
|
803
|
+
// 4. Add request logging plugin (development environment)
|
|
804
|
+
if (!appConfig.isProduction) {
|
|
805
|
+
feApi.usePlugin(new RequestLoggerPlugin());
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
```
|
|
308
810
|
|
|
309
|
-
|
|
811
|
+
### Example 3: User Authentication Plugin
|
|
310
812
|
|
|
311
|
-
|
|
813
|
+
```typescript
|
|
814
|
+
// src/base/services/UserService.ts
|
|
815
|
+
@injectable()
|
|
816
|
+
export class UserService
|
|
817
|
+
extends UserAuthService<UserInfo>
|
|
818
|
+
implements ExecutorPlugin
|
|
819
|
+
{
|
|
820
|
+
readonly pluginName = 'UserService';
|
|
312
821
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
822
|
+
constructor(
|
|
823
|
+
@inject(IOCIdentifier.RouteServiceInterface)
|
|
824
|
+
protected routerService: RouteServiceInterface,
|
|
825
|
+
@inject(UserApi) userApi: UserAuthApiInterface<UserInfo>,
|
|
826
|
+
@inject(IOCIdentifier.AppConfig) appConfig: AppConfig,
|
|
827
|
+
@inject(IOCIdentifier.LocalStorageEncrypt) storage: SyncStorageInterface
|
|
828
|
+
) {
|
|
829
|
+
super(userApi, {
|
|
830
|
+
userStorage: {
|
|
831
|
+
key: appConfig.userInfoStorageKey,
|
|
832
|
+
storage: storage
|
|
833
|
+
},
|
|
834
|
+
credentialStorage: {
|
|
835
|
+
key: appConfig.userTokenStorageKey,
|
|
836
|
+
storage: storage
|
|
837
|
+
}
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* Automatically restore user login state on application startup
|
|
843
|
+
*/
|
|
844
|
+
async onBefore(): Promise<void> {
|
|
845
|
+
try {
|
|
846
|
+
// Check if on login page
|
|
847
|
+
if (this.routerService.isLoginPage()) {
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// If already have user info, return directly
|
|
852
|
+
if (this.isAuthenticated()) {
|
|
853
|
+
console.log('✅ User already authenticated');
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// Try to restore token from storage
|
|
858
|
+
const token = this.getToken();
|
|
859
|
+
if (!token) {
|
|
860
|
+
// No token, redirect to login
|
|
861
|
+
throw new AppError('NO_USER_TOKEN');
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// Use token to get user info
|
|
865
|
+
const userInfo = await this.userInfo();
|
|
866
|
+
console.log('✅ User authenticated:', userInfo.name);
|
|
867
|
+
} catch (error) {
|
|
868
|
+
// Authentication failed, clear storage and redirect to login
|
|
869
|
+
this.clearAuth();
|
|
870
|
+
this.routerService.push('/login');
|
|
871
|
+
console.log('❌ User authentication failed, redirecting to login');
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
getToken(): string | null {
|
|
876
|
+
return this.credential();
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
private clearAuth() {
|
|
880
|
+
this.setCredential(null);
|
|
881
|
+
this.setUser(null);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
322
884
|
```
|
|
323
885
|
|
|
324
|
-
###
|
|
886
|
+
### Example 4: Development Tools Plugin
|
|
325
887
|
|
|
326
|
-
```
|
|
327
|
-
//
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
bootstrap.use([IOC(UserService)]);
|
|
888
|
+
```typescript
|
|
889
|
+
// src/core/bootstraps/PrintBootstrap.ts
|
|
890
|
+
export const printBootstrap: BootstrapExecutorPlugin = {
|
|
891
|
+
pluginName: 'PrintBootstrap',
|
|
331
892
|
|
|
332
|
-
|
|
333
|
-
const
|
|
334
|
-
bootstrap.use([IOC(UserService)]); // Same business logic
|
|
893
|
+
onAfter({ parameters: { logger, ioc } }: BootstrapContext): void {
|
|
894
|
+
const appConfig = ioc.get<AppConfig>(IOCIdentifier.AppConfig);
|
|
335
895
|
|
|
336
|
-
//
|
|
337
|
-
|
|
338
|
-
|
|
896
|
+
// Print application info
|
|
897
|
+
logger.info('🚀 Application started successfully!');
|
|
898
|
+
logger.info('📦 App Name:', appConfig.appName);
|
|
899
|
+
logger.info('🌍 Environment:', appConfig.env);
|
|
900
|
+
logger.info('🔗 API Base URL:', appConfig.apiBaseUrl);
|
|
901
|
+
|
|
902
|
+
// Print registered services
|
|
903
|
+
logger.info('📋 Registered Services:');
|
|
904
|
+
logger.info(' - UserService');
|
|
905
|
+
logger.info(' - I18nService');
|
|
906
|
+
logger.info(' - RouteService');
|
|
907
|
+
|
|
908
|
+
// Print warnings (if any)
|
|
909
|
+
if (!appConfig.isProduction && appConfig.mockEnabled) {
|
|
910
|
+
logger.warn('⚠️ Mock API is enabled');
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
};
|
|
339
914
|
```
|
|
340
915
|
|
|
341
|
-
|
|
916
|
+
---
|
|
342
917
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
918
|
+
## 🧪 Testing: Core Advantage of Bootstrap
|
|
919
|
+
|
|
920
|
+
### Why is Testing So Important?
|
|
921
|
+
|
|
922
|
+
One of the **most important advantages** of Bootstrap architecture is **testability**. By separating initialization logic from UI, we can:
|
|
923
|
+
|
|
924
|
+
- ✅ Test each plugin independently
|
|
925
|
+
- ✅ Easily mock dependencies
|
|
926
|
+
- ✅ Run tests quickly (no need to render UI)
|
|
927
|
+
- ✅ Improve test coverage
|
|
928
|
+
|
|
929
|
+
### Traditional Approach vs Bootstrap Approach
|
|
930
|
+
|
|
931
|
+
#### ❌ Traditional Approach: Components Mixed with Initialization Logic
|
|
932
|
+
|
|
933
|
+
```typescript
|
|
934
|
+
// ❌ Traditional component: hard to test
|
|
935
|
+
function App() {
|
|
936
|
+
const [loading, setLoading] = useState(true);
|
|
937
|
+
const [user, setUser] = useState(null);
|
|
938
|
+
const [i18nReady, setI18nReady] = useState(false);
|
|
939
|
+
const [error, setError] = useState(null);
|
|
940
|
+
|
|
941
|
+
useEffect(() => {
|
|
942
|
+
const init = async () => {
|
|
943
|
+
try {
|
|
944
|
+
// 1. Initialize internationalization
|
|
945
|
+
await i18next.init({
|
|
946
|
+
lng: 'zh',
|
|
947
|
+
resources: { /* ... */ }
|
|
948
|
+
});
|
|
949
|
+
setI18nReady(true);
|
|
950
|
+
|
|
951
|
+
// 2. Configure API
|
|
952
|
+
api.setBaseURL('https://api.example.com');
|
|
953
|
+
api.usePlugin(new AuthPlugin());
|
|
954
|
+
|
|
955
|
+
// 3. Check user authentication
|
|
956
|
+
const token = localStorage.getItem('token');
|
|
957
|
+
if (token) {
|
|
958
|
+
const userInfo = await fetch('/api/user', {
|
|
959
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
960
|
+
}).then(res => res.json());
|
|
961
|
+
setUser(userInfo);
|
|
962
|
+
}
|
|
963
|
+
} catch (err) {
|
|
964
|
+
setError(err);
|
|
965
|
+
} finally {
|
|
966
|
+
setLoading(false);
|
|
967
|
+
}
|
|
968
|
+
};
|
|
969
|
+
|
|
970
|
+
init();
|
|
971
|
+
}, []);
|
|
972
|
+
|
|
973
|
+
if (loading) return <div>Loading...</div>;
|
|
974
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
975
|
+
|
|
976
|
+
return <Router />;
|
|
977
|
+
}
|
|
351
978
|
```
|
|
352
979
|
|
|
353
|
-
|
|
980
|
+
**Test Code (Traditional Approach): 😰😰😰 Very Difficult**
|
|
981
|
+
|
|
982
|
+
```typescript
|
|
983
|
+
// ❌ Traditional approach testing: full of tricks and hacks
|
|
984
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
985
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
986
|
+
import App from './App';
|
|
987
|
+
|
|
988
|
+
describe('App (Traditional)', () => {
|
|
989
|
+
beforeEach(() => {
|
|
990
|
+
// 😰 Need to mock global variables
|
|
991
|
+
global.localStorage = {
|
|
992
|
+
getItem: vi.fn(),
|
|
993
|
+
setItem: vi.fn(),
|
|
994
|
+
removeItem: vi.fn(),
|
|
995
|
+
clear: vi.fn()
|
|
996
|
+
};
|
|
997
|
+
|
|
998
|
+
// 😰 Need to mock fetch
|
|
999
|
+
global.fetch = vi.fn();
|
|
1000
|
+
|
|
1001
|
+
// 😰 Need to mock i18next
|
|
1002
|
+
vi.mock('i18next', () => ({
|
|
1003
|
+
init: vi.fn().mockResolvedValue(undefined),
|
|
1004
|
+
t: vi.fn(key => key)
|
|
1005
|
+
}));
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
it('should initialize and load user', async () => {
|
|
1009
|
+
// 😰 Setup complex mocks
|
|
1010
|
+
vi.mocked(localStorage.getItem).mockReturnValue('mock-token');
|
|
1011
|
+
vi.mocked(fetch).mockResolvedValueOnce({
|
|
1012
|
+
ok: true,
|
|
1013
|
+
json: async () => ({ id: '1', name: 'John' })
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
render(<App />);
|
|
354
1017
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
1018
|
+
// 😰 Need to wait for multiple async operations
|
|
1019
|
+
await waitFor(() => {
|
|
1020
|
+
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
|
1021
|
+
}, { timeout: 3000 });
|
|
359
1022
|
|
|
360
|
-
|
|
1023
|
+
// 😰 Hard to verify intermediate states
|
|
1024
|
+
expect(fetch).toHaveBeenCalledWith('/api/user', expect.any(Object));
|
|
1025
|
+
});
|
|
1026
|
+
|
|
1027
|
+
it('should handle error', async () => {
|
|
1028
|
+
// 😰 Each test needs to reset mocks
|
|
1029
|
+
vi.mocked(fetch).mockRejectedValueOnce(new Error('Network error'));
|
|
361
1030
|
|
|
362
|
-
|
|
1031
|
+
render(<App />);
|
|
1032
|
+
|
|
1033
|
+
await waitFor(() => {
|
|
1034
|
+
expect(screen.getByText(/Error/)).toBeInTheDocument();
|
|
1035
|
+
});
|
|
1036
|
+
});
|
|
363
1037
|
|
|
364
|
-
|
|
1038
|
+
// 😰 Problems:
|
|
1039
|
+
// 1. Need to mock many global variables (localStorage, fetch, i18next)
|
|
1040
|
+
// 2. Tests run slowly (need to render components)
|
|
1041
|
+
// 3. Hard to test error scenarios
|
|
1042
|
+
// 4. Tests may interfere with each other
|
|
1043
|
+
// 5. Hard to test individual initialization steps
|
|
1044
|
+
});
|
|
1045
|
+
```
|
|
365
1046
|
|
|
366
|
-
|
|
367
|
-
// Create a simple plugin
|
|
368
|
-
export class SimplePlugin implements BootstrapExecutorPlugin {
|
|
369
|
-
readonly pluginName = 'SimplePlugin';
|
|
1047
|
+
#### ✅ Bootstrap Approach: Independent Plugin Testing
|
|
370
1048
|
|
|
371
|
-
|
|
372
|
-
|
|
1049
|
+
```typescript
|
|
1050
|
+
// ✅ Bootstrap approach: logic and UI separated
|
|
1051
|
+
// 1. Plugin implementation
|
|
1052
|
+
@injectable()
|
|
1053
|
+
export class UserService implements ExecutorPlugin {
|
|
1054
|
+
readonly pluginName = 'UserService';
|
|
1055
|
+
|
|
1056
|
+
constructor(
|
|
1057
|
+
@inject(UserApi) private api: UserApi,
|
|
1058
|
+
@inject(IOCIdentifier.LocalStorageEncrypt) private storage: Storage,
|
|
1059
|
+
@inject(IOCIdentifier.RouteServiceInterface) private router: RouteService
|
|
1060
|
+
) {}
|
|
1061
|
+
|
|
1062
|
+
async onBefore(): Promise<void> {
|
|
1063
|
+
const token = this.storage.getItem('token');
|
|
1064
|
+
if (!token) {
|
|
1065
|
+
throw new AppError('NO_USER_TOKEN');
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
const userInfo = await this.api.getUserInfo(token);
|
|
1069
|
+
this.setUser(userInfo);
|
|
373
1070
|
}
|
|
374
1071
|
}
|
|
1072
|
+
|
|
1073
|
+
// 2. UI component becomes simple
|
|
1074
|
+
function App() {
|
|
1075
|
+
return (
|
|
1076
|
+
<BootstrapsProvider>
|
|
1077
|
+
<ComboProvider themeConfig={themeConfig}>
|
|
1078
|
+
<AppRouterProvider pages={allPages} />
|
|
1079
|
+
</ComboProvider>
|
|
1080
|
+
</BootstrapsProvider>
|
|
1081
|
+
);
|
|
1082
|
+
}
|
|
375
1083
|
```
|
|
376
1084
|
|
|
377
|
-
|
|
1085
|
+
**Test Code (Bootstrap Approach): 😊😊😊 Very Simple**
|
|
1086
|
+
|
|
1087
|
+
```typescript
|
|
1088
|
+
// ✅ Bootstrap approach testing: clear, simple, fast
|
|
1089
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
1090
|
+
import { UserService } from '@/base/services/UserService';
|
|
1091
|
+
import { AppError } from '@/base/cases/AppError';
|
|
1092
|
+
|
|
1093
|
+
describe('UserService Plugin', () => {
|
|
1094
|
+
let userService: UserService;
|
|
1095
|
+
let mockApi: any;
|
|
1096
|
+
let mockStorage: any;
|
|
1097
|
+
let mockRouter: any;
|
|
1098
|
+
|
|
1099
|
+
beforeEach(() => {
|
|
1100
|
+
// ✅ Only need to mock dependency interfaces, no global variables
|
|
1101
|
+
mockApi = {
|
|
1102
|
+
getUserInfo: vi.fn()
|
|
1103
|
+
};
|
|
1104
|
+
|
|
1105
|
+
mockStorage = {
|
|
1106
|
+
getItem: vi.fn(),
|
|
1107
|
+
setItem: vi.fn()
|
|
1108
|
+
};
|
|
1109
|
+
|
|
1110
|
+
mockRouter = {
|
|
1111
|
+
push: vi.fn()
|
|
1112
|
+
};
|
|
1113
|
+
|
|
1114
|
+
// ✅ Create service instance
|
|
1115
|
+
userService = new UserService(mockApi, mockStorage, mockRouter);
|
|
1116
|
+
});
|
|
378
1117
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
1118
|
+
it('should load user when token exists', async () => {
|
|
1119
|
+
// ✅ Setup test data
|
|
1120
|
+
mockStorage.getItem.mockReturnValue('mock-token');
|
|
1121
|
+
mockApi.getUserInfo.mockResolvedValue({
|
|
1122
|
+
id: '1',
|
|
1123
|
+
name: 'John Doe'
|
|
1124
|
+
});
|
|
1125
|
+
|
|
1126
|
+
// ✅ Execute plugin lifecycle
|
|
1127
|
+
await userService.onBefore();
|
|
1128
|
+
|
|
1129
|
+
// ✅ Clear assertions
|
|
1130
|
+
expect(mockStorage.getItem).toHaveBeenCalledWith('token');
|
|
1131
|
+
expect(mockApi.getUserInfo).toHaveBeenCalledWith('mock-token');
|
|
1132
|
+
expect(userService.getUser()).toEqual({
|
|
1133
|
+
id: '1',
|
|
1134
|
+
name: 'John Doe'
|
|
1135
|
+
});
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
it('should throw error when token is missing', async () => {
|
|
1139
|
+
// ✅ Easy to test error scenarios
|
|
1140
|
+
mockStorage.getItem.mockReturnValue(null);
|
|
1141
|
+
|
|
1142
|
+
// ✅ Verify errors
|
|
1143
|
+
await expect(userService.onBefore()).rejects.toThrow(AppError);
|
|
1144
|
+
await expect(userService.onBefore()).rejects.toThrow('NO_USER_TOKEN');
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
it('should handle API error', async () => {
|
|
1148
|
+
// ✅ Easy to simulate API errors
|
|
1149
|
+
mockStorage.getItem.mockReturnValue('mock-token');
|
|
1150
|
+
mockApi.getUserInfo.mockRejectedValue(new Error('Network error'));
|
|
1151
|
+
|
|
1152
|
+
// ✅ Verify error handling
|
|
1153
|
+
await expect(userService.onBefore()).rejects.toThrow('Network error');
|
|
1154
|
+
});
|
|
1155
|
+
|
|
1156
|
+
// ✅ Advantages:
|
|
1157
|
+
// 1. No need to mock global variables
|
|
1158
|
+
// 2. Tests run fast (no need to render UI)
|
|
1159
|
+
// 3. Easy to test error scenarios
|
|
1160
|
+
// 4. Tests are completely independent
|
|
1161
|
+
// 5. Can test each initialization step individually
|
|
384
1162
|
});
|
|
1163
|
+
```
|
|
385
1164
|
|
|
386
|
-
|
|
387
|
-
|
|
1165
|
+
### Test Complexity Comparison
|
|
1166
|
+
|
|
1167
|
+
| Test Scenario | Traditional Approach | Bootstrap Approach | Improvement |
|
|
1168
|
+
| ------------------------ | ---------------------------------------------------------- | ------------------------------------------ | ----------- |
|
|
1169
|
+
| **Mock Complexity** | 😰😰😰 Need to mock global variables, fetch, i18next, etc. | 😊 Only need to mock dependency interfaces | **80%** |
|
|
1170
|
+
| **Test Run Speed** | 😰😰 Slow (need to render components, wait for async) | 😊😊😊 Fast (pure logic testing) | **5-10x** |
|
|
1171
|
+
| **Test Error Scenarios** | 😰😰😰 Difficult (need complex mock setups) | 😊😊😊 Simple (directly mock reject) | **90%** |
|
|
1172
|
+
| **Test Isolation** | 😰😰 Poor (global variables may interfere) | 😊😊😊 Good (each test independent) | **100%** |
|
|
1173
|
+
| **Test Readability** | 😰😰 Poor (full of mocks and hacks) | 😊😊😊 Good (clear inputs/outputs) | **80%** |
|
|
1174
|
+
| **Coverage** | 😰😰 Low (hard to cover all branches) | 😊😊😊 High (easy to cover all scenarios) | **50%** |
|
|
1175
|
+
|
|
1176
|
+
### Actual Project Test Examples
|
|
1177
|
+
|
|
1178
|
+
#### Example 1: Testing I18n Plugin
|
|
1179
|
+
|
|
1180
|
+
```typescript
|
|
1181
|
+
// src/base/services/I18nService.test.ts
|
|
1182
|
+
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
|
1183
|
+
import { I18nService } from '@/base/services/I18nService';
|
|
1184
|
+
import i18n from 'i18next';
|
|
1185
|
+
|
|
1186
|
+
// Mock i18next
|
|
1187
|
+
vi.mock('i18next', () => ({
|
|
1188
|
+
default: {
|
|
1189
|
+
use: vi.fn().mockReturnThis(),
|
|
1190
|
+
init: vi.fn(),
|
|
1191
|
+
t: vi.fn(),
|
|
1192
|
+
changeLanguage: vi.fn(),
|
|
1193
|
+
language: 'en',
|
|
1194
|
+
services: {
|
|
1195
|
+
languageDetector: {
|
|
1196
|
+
addDetector: vi.fn()
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
}));
|
|
388
1201
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
1202
|
+
describe('I18nService', () => {
|
|
1203
|
+
let service: I18nService;
|
|
1204
|
+
|
|
1205
|
+
beforeEach(() => {
|
|
1206
|
+
service = new I18nService('/en/test/path');
|
|
1207
|
+
vi.clearAllMocks();
|
|
1208
|
+
});
|
|
1209
|
+
|
|
1210
|
+
describe('onBefore', () => {
|
|
1211
|
+
it('should initialize i18n with correct configuration', () => {
|
|
1212
|
+
// ✅ Execute plugin lifecycle
|
|
1213
|
+
service.onBefore();
|
|
1214
|
+
|
|
1215
|
+
// ✅ Verify initialization configuration
|
|
1216
|
+
expect(i18n.use).toHaveBeenCalledTimes(3);
|
|
1217
|
+
expect(i18n.init).toHaveBeenCalledWith(
|
|
1218
|
+
expect.objectContaining({
|
|
1219
|
+
debug: false,
|
|
1220
|
+
detection: {
|
|
1221
|
+
order: ['pathLanguageDetector', 'navigator', 'localStorage'],
|
|
1222
|
+
caches: []
|
|
1223
|
+
}
|
|
1224
|
+
})
|
|
1225
|
+
);
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
it('should detect language from path correctly', () => {
|
|
1229
|
+
service.onBefore();
|
|
1230
|
+
|
|
1231
|
+
const detector = vi.mocked(i18n.services.languageDetector.addDetector)
|
|
1232
|
+
.mock.calls[0][0];
|
|
1233
|
+
|
|
1234
|
+
// ✅ Test language detection logic
|
|
1235
|
+
const language = detector.lookup();
|
|
1236
|
+
expect(language).toBe('en');
|
|
1237
|
+
});
|
|
1238
|
+
|
|
1239
|
+
it('should return fallback language for invalid path', () => {
|
|
1240
|
+
const invalidService = new I18nService('/invalid/path');
|
|
1241
|
+
invalidService.onBefore();
|
|
1242
|
+
|
|
1243
|
+
const detector = vi.mocked(i18n.services.languageDetector.addDetector)
|
|
1244
|
+
.mock.calls[0][0];
|
|
1245
|
+
|
|
1246
|
+
// ✅ Test edge cases
|
|
1247
|
+
const language = detector.lookup();
|
|
1248
|
+
expect(language).toBe('zh'); // fallback language
|
|
1249
|
+
});
|
|
1250
|
+
});
|
|
1251
|
+
|
|
1252
|
+
describe('changeLanguage', () => {
|
|
1253
|
+
it('should change language using i18n', async () => {
|
|
1254
|
+
await service.changeLanguage('en');
|
|
1255
|
+
expect(i18n.changeLanguage).toHaveBeenCalledWith('en');
|
|
1256
|
+
});
|
|
1257
|
+
|
|
1258
|
+
it('should handle language change error', async () => {
|
|
1259
|
+
// ✅ Test error scenarios
|
|
1260
|
+
vi.mocked(i18n.changeLanguage).mockRejectedValueOnce(
|
|
1261
|
+
new Error('Change failed')
|
|
1262
|
+
);
|
|
1263
|
+
|
|
1264
|
+
await expect(service.changeLanguage('en')).rejects.toThrow(
|
|
1265
|
+
'Change failed'
|
|
1266
|
+
);
|
|
1267
|
+
});
|
|
1268
|
+
});
|
|
1269
|
+
});
|
|
392
1270
|
```
|
|
393
1271
|
|
|
394
|
-
|
|
1272
|
+
#### Example 2: Testing Bootstrap Startup Process
|
|
1273
|
+
|
|
1274
|
+
```typescript
|
|
1275
|
+
// __tests__/src/core/bootstraps/BootstrapsApp.test.ts
|
|
1276
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
1277
|
+
import { BootstrapClient } from '@/core/bootstraps/BootstrapClient';
|
|
1278
|
+
import type { BootstrapClientArgs } from '@/core/bootstraps/BootstrapClient';
|
|
1279
|
+
import { InversifyContainer } from '@/base/cases/InversifyContainer';
|
|
1280
|
+
import { createIOCFunction } from '@qlover/corekit-bridge';
|
|
1281
|
+
import { browserGlobalsName } from '@config/common';
|
|
1282
|
+
|
|
1283
|
+
// Mock dependencies
|
|
1284
|
+
vi.mock('@/core/registers/IocRegisterImpl', () => ({
|
|
1285
|
+
IocRegisterImpl: vi.fn().mockImplementation(() => ({
|
|
1286
|
+
getRegisterList: vi.fn().mockReturnValue([]),
|
|
1287
|
+
register: vi.fn()
|
|
1288
|
+
}))
|
|
1289
|
+
}));
|
|
1290
|
+
|
|
1291
|
+
vi.mock('@/core/bootstraps/BootstrapsRegistry', () => ({
|
|
1292
|
+
BootstrapsRegistry: vi.fn().mockImplementation(() => ({
|
|
1293
|
+
register: vi.fn().mockReturnValue([])
|
|
1294
|
+
}))
|
|
1295
|
+
}));
|
|
1296
|
+
|
|
1297
|
+
describe('BootstrapClient', () => {
|
|
1298
|
+
let mockArgs: BootstrapClientArgs;
|
|
1299
|
+
let mockIOC: ReturnType<typeof createIOCFunction>;
|
|
1300
|
+
|
|
1301
|
+
beforeEach(() => {
|
|
1302
|
+
vi.clearAllMocks();
|
|
1303
|
+
|
|
1304
|
+
const container = new InversifyContainer();
|
|
1305
|
+
mockIOC = createIOCFunction(container);
|
|
1306
|
+
|
|
1307
|
+
mockArgs = {
|
|
1308
|
+
root: {},
|
|
1309
|
+
bootHref: 'http://localhost:3000',
|
|
1310
|
+
ioc: {
|
|
1311
|
+
create: vi.fn().mockReturnValue(mockIOC)
|
|
1312
|
+
}
|
|
1313
|
+
};
|
|
1314
|
+
});
|
|
1315
|
+
|
|
1316
|
+
describe('main', () => {
|
|
1317
|
+
it('should initialize bootstrap successfully', async () => {
|
|
1318
|
+
// ✅ Execute startup process
|
|
1319
|
+
const result = await BootstrapClient.main(mockArgs);
|
|
1320
|
+
|
|
1321
|
+
// ✅ Verify startup result
|
|
1322
|
+
expect(result.bootHref).toBe('http://localhost:3000');
|
|
395
1323
|
|
|
396
|
-
|
|
1324
|
+
// ✅ Verify global variable injection
|
|
1325
|
+
expect(
|
|
1326
|
+
(mockArgs.root as Record<string, unknown>)[browserGlobalsName]
|
|
1327
|
+
).toBeDefined();
|
|
397
1328
|
|
|
398
|
-
|
|
1329
|
+
const injectedGlobals = (mockArgs.root as Record<string, unknown>)[
|
|
1330
|
+
browserGlobalsName
|
|
1331
|
+
] as Record<string, unknown>;
|
|
399
1332
|
|
|
400
|
-
|
|
1333
|
+
expect(injectedGlobals).toHaveProperty('logger');
|
|
1334
|
+
expect(injectedGlobals).toHaveProperty('appConfig');
|
|
1335
|
+
});
|
|
401
1336
|
|
|
402
|
-
|
|
1337
|
+
it('should handle initialization error', async () => {
|
|
1338
|
+
// ✅ Test error scenarios
|
|
1339
|
+
mockArgs.ioc.create = vi.fn().mockImplementation(() => {
|
|
1340
|
+
throw new Error('IOC creation failed');
|
|
1341
|
+
});
|
|
403
1342
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
1343
|
+
// ✅ Verify error doesn't crash application
|
|
1344
|
+
await expect(BootstrapClient.main(mockArgs)).rejects.toThrow(
|
|
1345
|
+
'IOC creation failed'
|
|
1346
|
+
);
|
|
1347
|
+
});
|
|
1348
|
+
});
|
|
409
1349
|
});
|
|
410
|
-
|
|
1350
|
+
```
|
|
1351
|
+
|
|
1352
|
+
#### Example 3: Testing API Configuration Plugin
|
|
1353
|
+
|
|
1354
|
+
```typescript
|
|
1355
|
+
// __tests__/src/base/apis/UserApiBootstrap.test.ts
|
|
1356
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
1357
|
+
import { UserApiBootstarp } from '@/base/apis/userApi/UserApiBootstarp';
|
|
1358
|
+
|
|
1359
|
+
describe('UserApiBootstrap', () => {
|
|
1360
|
+
let plugin: UserApiBootstarp;
|
|
1361
|
+
let mockContext: any;
|
|
1362
|
+
let mockUserApi: any;
|
|
1363
|
+
|
|
1364
|
+
beforeEach(() => {
|
|
1365
|
+
plugin = new UserApiBootstarp();
|
|
1366
|
+
|
|
1367
|
+
// ✅ Create mock context
|
|
1368
|
+
mockUserApi = {
|
|
1369
|
+
usePlugin: vi.fn()
|
|
1370
|
+
};
|
|
1371
|
+
|
|
1372
|
+
mockContext = {
|
|
1373
|
+
parameters: {
|
|
1374
|
+
ioc: {
|
|
1375
|
+
get: vi.fn().mockReturnValue(mockUserApi)
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
};
|
|
1379
|
+
});
|
|
1380
|
+
|
|
1381
|
+
it('should have correct plugin name', () => {
|
|
1382
|
+
expect(plugin.pluginName).toBe('UserApiBootstarp');
|
|
1383
|
+
});
|
|
1384
|
+
|
|
1385
|
+
it('should configure API plugins in onBefore', () => {
|
|
1386
|
+
// ✅ Execute plugin lifecycle
|
|
1387
|
+
plugin.onBefore(mockContext);
|
|
1388
|
+
|
|
1389
|
+
// ✅ Verify API configuration
|
|
1390
|
+
expect(mockContext.parameters.ioc.get).toHaveBeenCalled();
|
|
1391
|
+
expect(mockUserApi.usePlugin).toHaveBeenCalled();
|
|
1392
|
+
});
|
|
1393
|
+
|
|
1394
|
+
it('should add multiple plugins to API', () => {
|
|
1395
|
+
plugin.onBefore(mockContext);
|
|
411
1396
|
|
|
412
|
-
//
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
// Mobile-specific configuration
|
|
1397
|
+
// ✅ Verify multiple plugins added
|
|
1398
|
+
expect(mockUserApi.usePlugin).toHaveBeenCalledTimes(3);
|
|
1399
|
+
});
|
|
416
1400
|
});
|
|
417
|
-
|
|
1401
|
+
```
|
|
1402
|
+
|
|
1403
|
+
### Testing Best Practices
|
|
1404
|
+
|
|
1405
|
+
#### 1. ✅ Use Vitest Testing Tools
|
|
1406
|
+
|
|
1407
|
+
```typescript
|
|
1408
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
1409
|
+
|
|
1410
|
+
describe('MyPlugin', () => {
|
|
1411
|
+
beforeEach(() => {
|
|
1412
|
+
// ✅ Reset mocks before each test
|
|
1413
|
+
vi.clearAllMocks();
|
|
1414
|
+
});
|
|
1415
|
+
|
|
1416
|
+
afterEach(() => {
|
|
1417
|
+
// ✅ Cleanup resources
|
|
1418
|
+
vi.restoreAllMocks();
|
|
1419
|
+
});
|
|
418
1420
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
// Mini program specific configuration
|
|
1421
|
+
it('should do something', () => {
|
|
1422
|
+
// Test logic
|
|
1423
|
+
});
|
|
423
1424
|
});
|
|
424
|
-
miniprogramBootstrap.use([IOC(UserService), IOC(MiniprogramSpecificService)]);
|
|
425
1425
|
```
|
|
426
1426
|
|
|
427
|
-
|
|
1427
|
+
#### 2. ✅ Test Plugin Lifecycle
|
|
1428
|
+
|
|
1429
|
+
```typescript
|
|
1430
|
+
describe('UserService Plugin', () => {
|
|
1431
|
+
it('should execute onBefore lifecycle', async () => {
|
|
1432
|
+
const service = new UserService(mockApi, mockStorage, mockRouter);
|
|
1433
|
+
|
|
1434
|
+
// ✅ Test onBefore
|
|
1435
|
+
await service.onBefore();
|
|
1436
|
+
|
|
1437
|
+
expect(mockApi.getUserInfo).toHaveBeenCalled();
|
|
1438
|
+
});
|
|
428
1439
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
1440
|
+
it('should execute onAfter lifecycle', async () => {
|
|
1441
|
+
const service = new UserService(mockApi, mockStorage, mockRouter);
|
|
1442
|
+
|
|
1443
|
+
// ✅ Test onAfter
|
|
1444
|
+
await service.onAfter?.();
|
|
1445
|
+
|
|
1446
|
+
// Verify cleanup logic
|
|
1447
|
+
});
|
|
1448
|
+
|
|
1449
|
+
it('should handle onError lifecycle', async () => {
|
|
1450
|
+
const service = new UserService(mockApi, mockStorage, mockRouter);
|
|
1451
|
+
const error = new Error('Test error');
|
|
1452
|
+
|
|
1453
|
+
// ✅ Test onError
|
|
1454
|
+
await service.onError?.(error, mockContext);
|
|
1455
|
+
|
|
1456
|
+
// Verify error handling
|
|
1457
|
+
});
|
|
434
1458
|
});
|
|
435
|
-
|
|
1459
|
+
```
|
|
1460
|
+
|
|
1461
|
+
#### 3. ✅ Test Edge Cases and Error Scenarios
|
|
1462
|
+
|
|
1463
|
+
```typescript
|
|
1464
|
+
describe('UserService Error Handling', () => {
|
|
1465
|
+
it('should handle missing token', async () => {
|
|
1466
|
+
mockStorage.getItem.mockReturnValue(null);
|
|
1467
|
+
|
|
1468
|
+
// ✅ Verify errors
|
|
1469
|
+
await expect(service.onBefore()).rejects.toThrow('NO_USER_TOKEN');
|
|
1470
|
+
});
|
|
1471
|
+
|
|
1472
|
+
it('should handle network error', async () => {
|
|
1473
|
+
mockStorage.getItem.mockReturnValue('token');
|
|
1474
|
+
mockApi.getUserInfo.mockRejectedValue(new Error('Network error'));
|
|
1475
|
+
|
|
1476
|
+
// ✅ Verify error handling
|
|
1477
|
+
await expect(service.onBefore()).rejects.toThrow('Network error');
|
|
1478
|
+
});
|
|
436
1479
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
1480
|
+
it('should handle invalid token', async () => {
|
|
1481
|
+
mockStorage.getItem.mockReturnValue('invalid-token');
|
|
1482
|
+
mockApi.getUserInfo.mockRejectedValue(new Error('401 Unauthorized'));
|
|
1483
|
+
|
|
1484
|
+
// ✅ Verify 401 error handling
|
|
1485
|
+
await expect(service.onBefore()).rejects.toThrow('401 Unauthorized');
|
|
1486
|
+
});
|
|
441
1487
|
});
|
|
442
|
-
|
|
1488
|
+
```
|
|
1489
|
+
|
|
1490
|
+
#### 4. ✅ Test Plugin Dependencies
|
|
443
1491
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
1492
|
+
```typescript
|
|
1493
|
+
describe('Plugin Dependencies', () => {
|
|
1494
|
+
it('should ensure I18n is initialized before UserService', async () => {
|
|
1495
|
+
const i18nService = new I18nService('/en/path');
|
|
1496
|
+
const userService = new UserService(mockApi, mockStorage, mockRouter);
|
|
1497
|
+
|
|
1498
|
+
// ✅ I18n initialized first
|
|
1499
|
+
await i18nService.onBefore();
|
|
1500
|
+
|
|
1501
|
+
// ✅ Then initialize UserService
|
|
1502
|
+
await userService.onBefore();
|
|
1503
|
+
|
|
1504
|
+
// ✅ Verify UserService can use translations
|
|
1505
|
+
expect(i18n.t('some.key')).toBeDefined();
|
|
1506
|
+
});
|
|
448
1507
|
});
|
|
449
|
-
appBBootstrap.use([IOC(UserService), IOC(AppBSpecificService)]);
|
|
450
1508
|
```
|
|
451
1509
|
|
|
452
|
-
###
|
|
1510
|
+
### Running Tests
|
|
453
1511
|
|
|
454
|
-
```
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
const [user, setUser] = useState(null);
|
|
458
|
-
useEffect(() => {
|
|
459
|
-
fetchUser().then(setUser);
|
|
460
|
-
}, []);
|
|
461
|
-
return <div>{user?.name}</div>;
|
|
462
|
-
}
|
|
1512
|
+
```bash
|
|
1513
|
+
# Run all tests
|
|
1514
|
+
npm run test
|
|
463
1515
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
1516
|
+
# Run tests and watch for file changes
|
|
1517
|
+
npm run test -- --watch
|
|
1518
|
+
|
|
1519
|
+
# Run tests for specific file
|
|
1520
|
+
npm run test -- UserService.test.ts
|
|
469
1521
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
bootstrap.use([IOC(UserService)]); // New logic
|
|
473
|
-
// Old logic can still continue to be used
|
|
1522
|
+
# Generate test coverage report
|
|
1523
|
+
npm run test -- --coverage
|
|
474
1524
|
```
|
|
475
1525
|
|
|
476
|
-
|
|
1526
|
+
### Test Coverage Goals
|
|
1527
|
+
|
|
1528
|
+
With Bootstrap architecture, we can easily achieve high coverage:
|
|
1529
|
+
|
|
1530
|
+
- **Plugin Logic**: > 90% coverage
|
|
1531
|
+
- **Service Layer**: > 85% coverage
|
|
1532
|
+
- **API Adapters**: > 80% coverage
|
|
1533
|
+
- **Overall Application**: > 75% coverage
|
|
477
1534
|
|
|
478
|
-
###
|
|
1535
|
+
### Summary: Value of Testing
|
|
479
1536
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
1537
|
+
Bootstrap architecture makes testing:
|
|
1538
|
+
|
|
1539
|
+
1. **Simpler** - No need to mock global variables and complex environments
|
|
1540
|
+
2. **Faster** - Pure logic testing, no need to render UI
|
|
1541
|
+
3. **More Reliable** - Tests are completely independent, no interference
|
|
1542
|
+
4. **More Comprehensive** - Easy to test all edge cases and error scenarios
|
|
1543
|
+
5. **More Confident** - High coverage ensures code quality
|
|
1544
|
+
|
|
1545
|
+
> 💡 **Important Note**: Testability is one of the biggest advantages of Bootstrap architecture. If you find a plugin hard to test, it's likely a design problem requiring reconsideration of responsibility distribution.
|
|
1546
|
+
|
|
1547
|
+
---
|
|
1548
|
+
|
|
1549
|
+
## 💎 Best Practices
|
|
1550
|
+
|
|
1551
|
+
### 1. Plugin Design Principles
|
|
1552
|
+
|
|
1553
|
+
#### ✅ Single Responsibility
|
|
1554
|
+
|
|
1555
|
+
```typescript
|
|
1556
|
+
// ✅ Good plugin design: does one thing
|
|
1557
|
+
export class ApiConfigPlugin implements BootstrapExecutorPlugin {
|
|
1558
|
+
readonly pluginName = 'ApiConfigPlugin';
|
|
484
1559
|
|
|
485
|
-
// Single responsibility
|
|
486
1560
|
onBefore({ parameters: { ioc } }: BootstrapContext): void {
|
|
487
|
-
//
|
|
488
|
-
ioc.get<
|
|
1561
|
+
// Only responsible for configuring API
|
|
1562
|
+
const api = ioc.get<FeApi>(FeApi);
|
|
1563
|
+
api.setBaseURL(config.apiBaseUrl);
|
|
1564
|
+
api.usePlugin(new AuthPlugin());
|
|
489
1565
|
}
|
|
490
1566
|
}
|
|
491
1567
|
|
|
492
|
-
// ❌ Bad plugin design
|
|
1568
|
+
// ❌ Bad plugin design: does too many things
|
|
493
1569
|
export class BadPlugin implements BootstrapExecutorPlugin {
|
|
494
1570
|
readonly pluginName = 'BadPlugin';
|
|
495
1571
|
|
|
496
|
-
// Does too many things
|
|
497
1572
|
onBefore({ parameters: { ioc } }: BootstrapContext): void {
|
|
498
1573
|
// Configure API
|
|
499
|
-
ioc.get<
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
//
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
1574
|
+
const api = ioc.get<FeApi>(FeApi);
|
|
1575
|
+
api.setBaseURL(config.apiBaseUrl);
|
|
1576
|
+
|
|
1577
|
+
// Initialize internationalization
|
|
1578
|
+
i18next.init({
|
|
1579
|
+
/* ... */
|
|
1580
|
+
});
|
|
1581
|
+
|
|
1582
|
+
// Check user authentication
|
|
1583
|
+
checkAuth();
|
|
1584
|
+
|
|
1585
|
+
// Configure router
|
|
1586
|
+
configureRouter();
|
|
1587
|
+
|
|
1588
|
+
// Too many responsibilities! ❌
|
|
506
1589
|
}
|
|
507
1590
|
}
|
|
508
1591
|
```
|
|
509
1592
|
|
|
510
|
-
|
|
1593
|
+
#### ✅ Explicit Dependencies
|
|
1594
|
+
|
|
1595
|
+
```typescript
|
|
1596
|
+
// ✅ Inject dependencies through constructor
|
|
1597
|
+
@injectable()
|
|
1598
|
+
export class UserService implements ExecutorPlugin {
|
|
1599
|
+
constructor(
|
|
1600
|
+
@inject(UserApi) private api: UserApi,
|
|
1601
|
+
@inject(IOCIdentifier.AppConfig) private config: AppConfig
|
|
1602
|
+
) {}
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
// ❌ Create dependencies directly
|
|
1606
|
+
export class BadUserService implements ExecutorPlugin {
|
|
1607
|
+
private api = new UserApi(); // ❌ Hardcoded dependency
|
|
1608
|
+
private config = new AppConfig(); // ❌ Hard to test
|
|
1609
|
+
}
|
|
1610
|
+
```
|
|
1611
|
+
|
|
1612
|
+
### 2. Error Handling
|
|
511
1613
|
|
|
512
|
-
```
|
|
1614
|
+
```typescript
|
|
513
1615
|
export class UserService implements ExecutorPlugin {
|
|
514
1616
|
readonly pluginName = 'UserService';
|
|
515
1617
|
|
|
516
1618
|
async onBefore(): Promise<void> {
|
|
517
1619
|
try {
|
|
518
|
-
|
|
519
|
-
|
|
1620
|
+
await this.initializeUser();
|
|
1621
|
+
} catch (error) {
|
|
1622
|
+
// ✅ Graceful error handling
|
|
1623
|
+
if (error instanceof AppError) {
|
|
1624
|
+
// Business error
|
|
1625
|
+
this.handleBusinessError(error);
|
|
1626
|
+
} else if (error instanceof NetworkError) {
|
|
1627
|
+
// Network error
|
|
1628
|
+
this.handleNetworkError(error);
|
|
1629
|
+
} else {
|
|
1630
|
+
// Unknown error
|
|
1631
|
+
this.logger.error('Unknown error:', error);
|
|
520
1632
|
}
|
|
521
1633
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
1634
|
+
// Don't let errors propagate and crash the app
|
|
1635
|
+
// Instead, perform appropriate degradation
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
526
1638
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
1639
|
+
private handleBusinessError(error: AppError) {
|
|
1640
|
+
if (error.code === 'NO_USER_TOKEN') {
|
|
1641
|
+
// Redirect to login page
|
|
1642
|
+
this.router.push('/login');
|
|
1643
|
+
} else if (error.code === 'TOKEN_EXPIRED') {
|
|
1644
|
+
// Refresh token
|
|
1645
|
+
this.refreshToken();
|
|
533
1646
|
}
|
|
534
1647
|
}
|
|
535
1648
|
}
|
|
536
1649
|
```
|
|
537
1650
|
|
|
538
|
-
### 3.
|
|
1651
|
+
### 3. Performance Optimization
|
|
1652
|
+
|
|
1653
|
+
```typescript
|
|
1654
|
+
// ✅ Load plugins on demand
|
|
1655
|
+
export class BootstrapsRegistry {
|
|
1656
|
+
register(): BootstrapExecutorPlugin[] {
|
|
1657
|
+
const plugins: BootstrapExecutorPlugin[] = [
|
|
1658
|
+
// Required plugins
|
|
1659
|
+
IOC(IOCIdentifier.I18nServiceInterface),
|
|
1660
|
+
new UserApiBootstarp()
|
|
1661
|
+
];
|
|
1662
|
+
|
|
1663
|
+
// Development environment plugins
|
|
1664
|
+
if (!this.appConfig.isProduction) {
|
|
1665
|
+
plugins.push(new DevToolsPlugin(), new MockDataPlugin());
|
|
1666
|
+
}
|
|
539
1667
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
1668
|
+
// Feature toggle plugins
|
|
1669
|
+
if (this.appConfig.features.analytics) {
|
|
1670
|
+
plugins.push(new AnalyticsPlugin());
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
return plugins;
|
|
1674
|
+
}
|
|
544
1675
|
}
|
|
1676
|
+
```
|
|
1677
|
+
|
|
1678
|
+
### 4. Logging
|
|
545
1679
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
1680
|
+
```typescript
|
|
1681
|
+
export class ApiConfigPlugin implements BootstrapExecutorPlugin {
|
|
1682
|
+
readonly pluginName = 'ApiConfigPlugin';
|
|
1683
|
+
|
|
1684
|
+
async onBefore({ parameters: { logger } }: BootstrapContext): Promise<void> {
|
|
1685
|
+
logger.info(`[${this.pluginName}] Configuring API...`);
|
|
1686
|
+
|
|
1687
|
+
try {
|
|
1688
|
+
await this.configureAPI();
|
|
1689
|
+
logger.info(`[${this.pluginName}] ✅ API configured successfully`);
|
|
1690
|
+
} catch (error) {
|
|
1691
|
+
logger.error(`[${this.pluginName}] ❌ API configuration failed:`, error);
|
|
1692
|
+
throw error;
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
549
1695
|
}
|
|
550
1696
|
```
|
|
551
1697
|
|
|
552
|
-
|
|
1698
|
+
---
|
|
1699
|
+
|
|
1700
|
+
## ❓ FAQ
|
|
1701
|
+
|
|
1702
|
+
### Q1: What's the relationship between Bootstrap and React lifecycle?
|
|
1703
|
+
|
|
1704
|
+
**A:** Bootstrap executes before React renders.
|
|
1705
|
+
|
|
1706
|
+
```
|
|
1707
|
+
Bootstrap initialization → Bootstrap startup → React rendering
|
|
1708
|
+
```
|
|
1709
|
+
|
|
1710
|
+
### Q2: Is plugin execution order important?
|
|
1711
|
+
|
|
1712
|
+
**A:** Very important! Plugins execute in registration order.
|
|
1713
|
+
|
|
1714
|
+
```typescript
|
|
1715
|
+
// ✅ Correct order
|
|
1716
|
+
bootstrap.use([
|
|
1717
|
+
IOC(I18nService), // 1. Initialize i18n first (other plugins may need it)
|
|
1718
|
+
new ApiConfigPlugin(), // 2. Then configure API
|
|
1719
|
+
IOC(UserService) // 3. Finally check user auth (depends on API)
|
|
1720
|
+
]);
|
|
1721
|
+
|
|
1722
|
+
// ❌ Wrong order
|
|
1723
|
+
bootstrap.use([
|
|
1724
|
+
IOC(UserService), // ❌ UserService depends on API, but API not configured yet
|
|
1725
|
+
new ApiConfigPlugin(), // Configure API
|
|
1726
|
+
IOC(I18nService) // I18n at the end (too late)
|
|
1727
|
+
]);
|
|
1728
|
+
```
|
|
1729
|
+
|
|
1730
|
+
### Q3: How to debug Bootstrap?
|
|
1731
|
+
|
|
1732
|
+
```typescript
|
|
1733
|
+
// Method 1: Use logging
|
|
1734
|
+
export class MyPlugin implements BootstrapExecutorPlugin {
|
|
1735
|
+
readonly pluginName = 'MyPlugin';
|
|
1736
|
+
|
|
1737
|
+
async onBefore({ parameters: { logger } }: BootstrapContext): Promise<void> {
|
|
1738
|
+
logger.info(`[${this.pluginName}] Starting...`);
|
|
1739
|
+
// ... your logic
|
|
1740
|
+
logger.info(`[${this.pluginName}] Completed`);
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
// Method 2: Use debug plugin
|
|
1745
|
+
export const debugPlugin: BootstrapExecutorPlugin = {
|
|
1746
|
+
pluginName: 'DebugPlugin',
|
|
1747
|
+
|
|
1748
|
+
onBefore(context) {
|
|
1749
|
+
console.log('onBefore context:', context);
|
|
1750
|
+
},
|
|
1751
|
+
|
|
1752
|
+
onAfter(context) {
|
|
1753
|
+
console.log('onAfter context:', context);
|
|
1754
|
+
}
|
|
1755
|
+
};
|
|
1756
|
+
```
|
|
1757
|
+
|
|
1758
|
+
### Q4: How to test plugins?
|
|
1759
|
+
|
|
1760
|
+
```typescript
|
|
1761
|
+
describe('UserService Plugin', () => {
|
|
1762
|
+
it('should initialize user on startup', async () => {
|
|
1763
|
+
// Create mock dependencies
|
|
1764
|
+
const mockApi = {
|
|
1765
|
+
getUserInfo: jest.fn().mockResolvedValue({ name: 'John' })
|
|
1766
|
+
};
|
|
1767
|
+
const mockStorage = {
|
|
1768
|
+
getItem: jest.fn().mockReturnValue('mock-token')
|
|
1769
|
+
};
|
|
1770
|
+
|
|
1771
|
+
// Create service
|
|
1772
|
+
const userService = new UserService(
|
|
1773
|
+
mockRouter,
|
|
1774
|
+
mockApi,
|
|
1775
|
+
mockConfig,
|
|
1776
|
+
mockStorage
|
|
1777
|
+
);
|
|
1778
|
+
|
|
1779
|
+
// Execute plugin lifecycle
|
|
1780
|
+
await userService.onBefore();
|
|
1781
|
+
|
|
1782
|
+
// Verify
|
|
1783
|
+
expect(mockApi.getUserInfo).toHaveBeenCalledWith('mock-token');
|
|
1784
|
+
});
|
|
1785
|
+
});
|
|
1786
|
+
```
|
|
1787
|
+
|
|
1788
|
+
### Q5: Is Bootstrap suitable for all projects?
|
|
1789
|
+
|
|
1790
|
+
**A:** Not necessarily. Bootstrap is more suitable for:
|
|
1791
|
+
|
|
1792
|
+
✅ **Suitable scenarios:**
|
|
1793
|
+
|
|
1794
|
+
- Medium to large applications
|
|
1795
|
+
- Complex initialization logic required
|
|
1796
|
+
- Multi-platform applications (Web, mobile, mini-programs)
|
|
1797
|
+
- Modularity and testability needed
|
|
1798
|
+
- Team collaborative development
|
|
1799
|
+
|
|
1800
|
+
❌ **Not suitable scenarios:**
|
|
1801
|
+
|
|
1802
|
+
- Simple display pages
|
|
1803
|
+
- Prototype projects
|
|
1804
|
+
- Projects without complex initialization logic
|
|
1805
|
+
|
|
1806
|
+
### Q6: How to ensure test coverage?
|
|
1807
|
+
|
|
1808
|
+
**A:** Bootstrap architecture naturally supports high coverage:
|
|
1809
|
+
|
|
1810
|
+
```typescript
|
|
1811
|
+
// ✅ Each plugin can be tested independently
|
|
1812
|
+
describe('UserService', () => {
|
|
1813
|
+
it('should initialize user', async () => {
|
|
1814
|
+
const service = new UserService(mockApi, mockStorage, mockRouter);
|
|
1815
|
+
await service.onBefore();
|
|
1816
|
+
expect(mockApi.getUserInfo).toHaveBeenCalled();
|
|
1817
|
+
});
|
|
1818
|
+
|
|
1819
|
+
// Easy to test all edge cases
|
|
1820
|
+
it('should handle missing token', async () => {
|
|
1821
|
+
mockStorage.getItem.mockReturnValue(null);
|
|
1822
|
+
await expect(service.onBefore()).rejects.toThrow('NO_USER_TOKEN');
|
|
1823
|
+
});
|
|
1824
|
+
|
|
1825
|
+
it('should handle API error', async () => {
|
|
1826
|
+
mockApi.getUserInfo.mockRejectedValue(new Error('Network error'));
|
|
1827
|
+
await expect(service.onBefore()).rejects.toThrow('Network error');
|
|
1828
|
+
});
|
|
1829
|
+
});
|
|
1830
|
+
```
|
|
1831
|
+
|
|
1832
|
+
**Coverage Goals:**
|
|
1833
|
+
|
|
1834
|
+
- Plugin logic: > 90%
|
|
1835
|
+
- Service layer: > 85%
|
|
1836
|
+
- API adapters: > 80%
|
|
1837
|
+
|
|
1838
|
+
### Q7: What's the difference between Vitest and Jest?
|
|
1839
|
+
|
|
1840
|
+
**A:** This project uses Vitest, a testing framework in the Vite ecosystem:
|
|
1841
|
+
|
|
1842
|
+
| Feature | Vitest | Jest |
|
|
1843
|
+
| ----------------- | ---------------------------------- | ---------------------------- |
|
|
1844
|
+
| **Speed** | ⚡ Very fast (based on Vite) | Slow |
|
|
1845
|
+
| **Configuration** | 🎯 Zero config (reuse vite.config) | Needs separate configuration |
|
|
1846
|
+
| **ESM Support** | ✅ Native support | ⚠️ Experimental |
|
|
1847
|
+
| **API** | Jest compatible | - |
|
|
1848
|
+
| **HMR** | ✅ Supported | ❌ Not supported |
|
|
1849
|
+
|
|
1850
|
+
```typescript
|
|
1851
|
+
// Vitest usage (almost identical to Jest)
|
|
1852
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
1853
|
+
|
|
1854
|
+
describe('MyTest', () => {
|
|
1855
|
+
beforeEach(() => {
|
|
1856
|
+
vi.clearAllMocks();
|
|
1857
|
+
});
|
|
1858
|
+
|
|
1859
|
+
it('should work', () => {
|
|
1860
|
+
expect(true).toBe(true);
|
|
1861
|
+
});
|
|
1862
|
+
});
|
|
1863
|
+
```
|
|
1864
|
+
|
|
1865
|
+
---
|
|
1866
|
+
|
|
1867
|
+
## 📚 Related Documentation
|
|
1868
|
+
|
|
1869
|
+
- [Project Architecture Design](./index.md) - Understand overall architecture
|
|
1870
|
+
- [IOC Container](./ioc.md) - Dependency injection details
|
|
1871
|
+
- [Environment Variables](./env.md) - Environment configuration management
|
|
1872
|
+
- [Global Variable Encapsulation](./global.md) - Browser API encapsulation
|
|
1873
|
+
|
|
1874
|
+
---
|
|
1875
|
+
|
|
1876
|
+
## 🎉 Summary
|
|
1877
|
+
|
|
1878
|
+
Bootstrap initializer is an important component of modern frontend architecture, helping us:
|
|
1879
|
+
|
|
1880
|
+
1. **Separation of Concerns** - UI and initialization logic separated
|
|
1881
|
+
2. **Improved Maintainability** - Modular design, clear responsibilities
|
|
1882
|
+
3. **Enhanced Testability** - Each plugin can be tested independently
|
|
1883
|
+
4. **Support Team Collaboration** - Different developers can develop plugins independently
|
|
1884
|
+
5. **Adapt to Changes** - Easy to extend and modify
|
|
553
1885
|
|
|
554
|
-
|
|
1886
|
+
Through Bootstrap, we build a more robust, maintainable, and testable frontend application architecture.
|
|
555
1887
|
|
|
556
|
-
|
|
557
|
-
2. **Improve Maintainability**: Modular design, easy to understand and modify
|
|
558
|
-
3. **Enhance Testability**: Each module can be tested independently
|
|
559
|
-
4. **Support Team Collaboration**: Different roles can focus on their domains
|
|
560
|
-
5. **Adapt to Changes**: Business logic changes don't affect UI, UI changes don't affect business logic
|
|
1888
|
+
---
|
|
561
1889
|
|
|
562
|
-
|
|
1890
|
+
**Feedback:**
|
|
1891
|
+
If you have any questions or suggestions about Bootstrap, please discuss in the team channel or submit an Issue.
|