@qlover/create-app 0.7.14 → 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 +27 -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 +130 -0
- package/dist/templates/next-app/README.md +114 -20
- 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/api.md +387 -0
- package/dist/templates/next-app/docs/en/component.md +544 -0
- package/dist/templates/next-app/docs/en/database.md +496 -0
- package/dist/templates/next-app/docs/en/development-guide.md +727 -0
- package/dist/templates/next-app/docs/en/env.md +563 -0
- package/dist/templates/next-app/docs/en/i18n.md +287 -0
- package/dist/templates/next-app/docs/en/index.md +165 -0
- package/dist/templates/next-app/docs/en/page.md +457 -0
- package/dist/templates/next-app/docs/en/project-structure.md +176 -0
- package/dist/templates/next-app/docs/en/router.md +427 -0
- package/dist/templates/next-app/docs/en/theme.md +532 -0
- package/dist/templates/next-app/docs/en/validator.md +478 -0
- package/dist/templates/next-app/docs/zh/api.md +387 -0
- package/dist/templates/next-app/docs/zh/component.md +544 -0
- package/dist/templates/next-app/docs/zh/database.md +496 -0
- package/dist/templates/next-app/docs/zh/development-guide.md +727 -0
- package/dist/templates/next-app/docs/zh/env.md +563 -0
- package/dist/templates/next-app/docs/zh/i18n.md +287 -0
- package/dist/templates/next-app/docs/zh/index.md +165 -0
- package/dist/templates/next-app/docs/zh/page.md +457 -0
- package/dist/templates/next-app/docs/zh/project-structure.md +176 -0
- package/dist/templates/next-app/docs/zh/router.md +427 -0
- package/dist/templates/next-app/docs/zh/theme.md +532 -0
- package/dist/templates/next-app/docs/zh/validator.md +476 -0
- 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/docs/env.md +0 -94
- 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 启动器
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## 📋 目录
|
|
4
4
|
|
|
5
|
-
Bootstrap
|
|
5
|
+
- [什么是 Bootstrap](#-什么是-bootstrap)
|
|
6
|
+
- [为什么需要 Bootstrap](#-为什么需要-bootstrap)
|
|
7
|
+
- [核心概念](#-核心概念)
|
|
8
|
+
- [工作流程](#-工作流程)
|
|
9
|
+
- [项目中的实现](#-项目中的实现)
|
|
10
|
+
- [插件系统](#-插件系统)
|
|
11
|
+
- [实战示例](#-实战示例)
|
|
12
|
+
- [测试:Bootstrap 的核心优势](#-测试bootstrap-的核心优势)
|
|
13
|
+
- [最佳实践](#-最佳实践)
|
|
14
|
+
- [常见问题](#-常见问题)
|
|
6
15
|
|
|
7
|
-
|
|
16
|
+
---
|
|
8
17
|
|
|
9
|
-
|
|
10
|
-
- 加载用户信息
|
|
11
|
-
- 初始化 API 配置
|
|
12
|
-
- 设置主题、语言等
|
|
18
|
+
## 🎯 什么是 Bootstrap
|
|
13
19
|
|
|
14
|
-
|
|
20
|
+
Bootstrap(启动器)是应用程序的**初始化管理器**,负责在应用渲染前执行所有必要的初始化逻辑。
|
|
15
21
|
|
|
16
|
-
###
|
|
22
|
+
### 核心职责
|
|
17
23
|
|
|
18
|
-
|
|
24
|
+
```
|
|
25
|
+
┌──────────────────────────────────────────────────┐
|
|
26
|
+
│ Bootstrap 启动器 │
|
|
27
|
+
│ ┌────────────────────────────────────────────┐ │
|
|
28
|
+
│ │ 1. 创建 IOC 容器 │ │
|
|
29
|
+
│ │ 2. 注入环境变量 │ │
|
|
30
|
+
│ │ 3. 封装全局变量 │ │
|
|
31
|
+
│ │ 4. 注册业务插件 │ │
|
|
32
|
+
│ │ 5. 执行初始化逻辑 │ │
|
|
33
|
+
│ └────────────────────────────────────────────┘ │
|
|
34
|
+
└──────────────────────────────────────────────────┘
|
|
35
|
+
↓
|
|
36
|
+
应用开始渲染
|
|
37
|
+
```
|
|
19
38
|
|
|
20
|
-
|
|
39
|
+
### 类比理解
|
|
21
40
|
|
|
22
|
-
|
|
41
|
+
就像电脑开机时需要:
|
|
23
42
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
43
|
+
- ✅ 加载驱动程序
|
|
44
|
+
- ✅ 启动系统服务
|
|
45
|
+
- ✅ 检查硬件状态
|
|
46
|
+
- ✅ 初始化用户环境
|
|
27
47
|
|
|
28
|
-
|
|
48
|
+
Bootstrap 在应用启动时做类似的事情:
|
|
29
49
|
|
|
30
|
-
|
|
50
|
+
- ✅ 初始化 IOC 容器(依赖管理)
|
|
51
|
+
- ✅ 注入环境配置
|
|
52
|
+
- ✅ 封装浏览器 API
|
|
53
|
+
- ✅ 执行业务初始化(用户认证、API 配置等)
|
|
31
54
|
|
|
32
|
-
|
|
55
|
+
---
|
|
33
56
|
|
|
34
|
-
|
|
57
|
+
## 🤔 为什么需要 Bootstrap
|
|
35
58
|
|
|
36
|
-
|
|
59
|
+
### 问题:传统方式的痛点
|
|
37
60
|
|
|
38
|
-
|
|
39
|
-
export function App() {
|
|
40
|
-
const [loading, setLoading] = useState(false);
|
|
61
|
+
#### 示例 1:组件中混杂初始化逻辑
|
|
41
62
|
|
|
42
|
-
|
|
43
|
-
|
|
63
|
+
```typescript
|
|
64
|
+
// ❌ 传统方式:在组件中处理初始化
|
|
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
|
+
// 初始化逻辑混在组件中
|
|
45
72
|
fetchUserInfo()
|
|
46
|
-
.then(
|
|
47
|
-
|
|
73
|
+
.then(user => {
|
|
74
|
+
setUser(user);
|
|
75
|
+
// 还要检查权限
|
|
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
|
+
**问题:**
|
|
63
96
|
|
|
64
|
-
|
|
97
|
+
- 😰 **组件职责过重**:UI 组件不应该处理业务初始化
|
|
98
|
+
- 😰 **状态管理复杂**:需要管理多个状态(loading、user、error)
|
|
99
|
+
- 😰 **难以测试**:初始化逻辑和 UI 逻辑耦合
|
|
100
|
+
- 😰 **难以复用**:初始化逻辑无法在其他项目中复用
|
|
101
|
+
- 😰 **维护困难**:业务逻辑变化会影响组件结构
|
|
65
102
|
|
|
66
|
-
|
|
103
|
+
#### 示例 2:多条件初始化
|
|
67
104
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const [loading, setLoading] = useState(false);
|
|
105
|
+
```typescript
|
|
106
|
+
// ❌ 更复杂的场景:多个初始化步骤
|
|
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
|
+
// 步骤 1:配置 API
|
|
119
|
+
await configureAPI();
|
|
120
|
+
setApiConfigured(true);
|
|
121
|
+
|
|
122
|
+
// 步骤 2:加载国际化
|
|
123
|
+
await loadI18n();
|
|
124
|
+
setI18nLoaded(true);
|
|
125
|
+
|
|
126
|
+
// 步骤 3:检查用户认证
|
|
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
|
+
// 步骤 4:加载权限
|
|
132
|
+
const perms = await fetchPermissions(user.id);
|
|
133
|
+
setPermissions(perms);
|
|
134
|
+
|
|
135
|
+
// 步骤 5:权限检查
|
|
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
|
+
// 还要处理各种加载状态...
|
|
153
|
+
if (loading || !apiConfigured || !i18nLoaded) {
|
|
154
|
+
return <LoadingScreen />;
|
|
113
155
|
}
|
|
114
156
|
|
|
115
157
|
return <Router />;
|
|
116
158
|
}
|
|
117
159
|
```
|
|
118
160
|
|
|
119
|
-
|
|
161
|
+
**问题进一步恶化:**
|
|
162
|
+
|
|
163
|
+
- 😰😰😰 **状态爆炸**:需要管理多个初始化状态
|
|
164
|
+
- 😰😰😰 **难以扩展**:添加新的初始化步骤会让代码更复杂
|
|
165
|
+
- 😰😰😰 **错误处理复杂**:每一步都可能失败,需要大量错误处理代码
|
|
166
|
+
- 😰😰😰 **依赖关系隐式**:步骤之间的依赖关系不清晰
|
|
167
|
+
|
|
168
|
+
### 解决方案:使用 Bootstrap
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
// ✅ 使用 Bootstrap:组件变得简洁
|
|
172
|
+
function App() {
|
|
173
|
+
return (
|
|
174
|
+
<BootstrapsProvider>
|
|
175
|
+
<ComboProvider themeConfig={themeConfig}>
|
|
176
|
+
<AppRouterProvider pages={allPages} />
|
|
177
|
+
</ComboProvider>
|
|
178
|
+
</BootstrapsProvider>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 所有初始化逻辑在 Bootstrap 中处理
|
|
183
|
+
const bootstrap = new Bootstrap({
|
|
184
|
+
root: window,
|
|
185
|
+
logger,
|
|
186
|
+
ioc: { manager: IOC, register: new IocRegisterImpl({ pathname, appConfig }) },
|
|
187
|
+
envOptions: { /* 环境变量配置 */ },
|
|
188
|
+
globalOptions: { /* 全局变量配置 */ }
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// 注册初始化插件
|
|
192
|
+
bootstrap.use([
|
|
193
|
+
IOC(I18nService), // 国际化服务
|
|
194
|
+
new UserApiBootstrap(), // 用户 API 配置
|
|
195
|
+
new FeApiBootstrap(), // 业务 API 配置
|
|
196
|
+
IOC(UserService) // 用户认证服务
|
|
197
|
+
]);
|
|
198
|
+
|
|
199
|
+
// 启动应用
|
|
200
|
+
await bootstrap.initialize();
|
|
201
|
+
await bootstrap.start();
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
**优势:**
|
|
120
205
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
206
|
+
- ✅ **组件职责清晰**:UI 组件只负责渲染
|
|
207
|
+
- ✅ **逻辑分离**:初始化逻辑独立于 UI
|
|
208
|
+
- ✅ **易于测试**:可以独立测试每个初始化步骤
|
|
209
|
+
- ✅ **易于扩展**:添加新的初始化步骤只需添加新插件
|
|
210
|
+
- ✅ **易于复用**:同一套初始化逻辑可以在不同项目中使用
|
|
126
211
|
|
|
127
|
-
|
|
212
|
+
---
|
|
128
213
|
|
|
129
|
-
|
|
214
|
+
## 💡 核心概念
|
|
130
215
|
|
|
131
|
-
|
|
216
|
+
### 1. 插件化架构
|
|
132
217
|
|
|
133
|
-
|
|
134
|
-
2. **状态管理**:通过 store 来管理应用状态,实现 UI 的响应式更新
|
|
135
|
-
3. **关注点分离**:将业务逻辑从 UI 组件中分离出来
|
|
218
|
+
Bootstrap 采用插件化设计,每个插件负责一个特定的初始化任务。
|
|
136
219
|
|
|
137
|
-
|
|
220
|
+
```typescript
|
|
221
|
+
// 插件接口
|
|
222
|
+
export interface BootstrapExecutorPlugin {
|
|
223
|
+
readonly pluginName: string;
|
|
138
224
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
- **模块化**:支持 IOC 容器、环境变量注入、全局变量注入等模块
|
|
225
|
+
// 在初始化前执行
|
|
226
|
+
onBefore?(context: BootstrapContext): void | Promise<void>;
|
|
142
227
|
|
|
143
|
-
|
|
228
|
+
// 在初始化时执行
|
|
229
|
+
onExecute?(context: BootstrapContext): void | Promise<void>;
|
|
144
230
|
|
|
231
|
+
// 在初始化后执行
|
|
232
|
+
onAfter?(context: BootstrapContext): void | Promise<void>;
|
|
233
|
+
|
|
234
|
+
// 错误处理
|
|
235
|
+
onError?(error: Error, context: BootstrapContext): void | Promise<void>;
|
|
236
|
+
}
|
|
145
237
|
```
|
|
146
|
-
|
|
238
|
+
|
|
239
|
+
### 2. 生命周期
|
|
240
|
+
|
|
241
|
+
```
|
|
242
|
+
┌────────────────────────────────────────────────┐
|
|
243
|
+
│ Bootstrap 生命周期 │
|
|
244
|
+
│ │
|
|
245
|
+
│ initialize() │
|
|
246
|
+
│ ├── 创建 IOC 容器 │
|
|
247
|
+
│ ├── 注入环境变量 │
|
|
248
|
+
│ └── 封装全局变量 │
|
|
249
|
+
│ │
|
|
250
|
+
│ start() │
|
|
251
|
+
│ ├── onBefore: 前置初始化 │
|
|
252
|
+
│ │ ├── 配置 API │
|
|
253
|
+
│ │ ├── 加载国际化 │
|
|
254
|
+
│ │ └── 检查用户认证 │
|
|
255
|
+
│ │ │
|
|
256
|
+
│ ├── onExecute: 执行主逻辑 │
|
|
257
|
+
│ │ └── 执行业务初始化 │
|
|
258
|
+
│ │ │
|
|
259
|
+
│ ├── onAfter: 后置处理 │
|
|
260
|
+
│ │ └── 清理资源、记录日志 │
|
|
261
|
+
│ │ │
|
|
262
|
+
│ └── onError: 错误处理 │
|
|
263
|
+
│ └── 错误捕获和处理 │
|
|
264
|
+
└────────────────────────────────────────────────┘
|
|
147
265
|
```
|
|
148
266
|
|
|
149
|
-
|
|
267
|
+
### 3. 依赖注入
|
|
150
268
|
|
|
151
|
-
|
|
269
|
+
Bootstrap 与 IOC 容器深度集成,所有插件都可以通过依赖注入获取服务。
|
|
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
|
+
// 使用注入的依赖执行初始化
|
|
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
|
+
## 🔄 工作流程
|
|
295
|
+
|
|
296
|
+
### 完整流程图
|
|
297
|
+
|
|
298
|
+
```
|
|
299
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
300
|
+
│ 1. main.tsx: 应用入口 │
|
|
301
|
+
│ BootstrapClient.main({ root: window, bootHref, ioc }) │
|
|
302
|
+
└────────────────────┬────────────────────────────────────────┘
|
|
303
|
+
↓
|
|
304
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
305
|
+
│ 2. BootstrapClient: 创建 Bootstrap 实例 │
|
|
306
|
+
│ - 创建 IOC 容器 │
|
|
307
|
+
│ - 配置环境变量注入 │
|
|
308
|
+
│ - 配置全局变量封装 │
|
|
309
|
+
└────────────────────┬────────────────────────────────────────┘
|
|
310
|
+
↓
|
|
311
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
312
|
+
│ 3. Bootstrap.initialize(): 初始化 │
|
|
313
|
+
│ ✅ IOC 容器初始化 │
|
|
314
|
+
│ ✅ 环境变量注入到 AppConfig │
|
|
315
|
+
│ ✅ 全局变量封装(localStorage、window 等) │
|
|
316
|
+
└────────────────────┬────────────────────────────────────────┘
|
|
317
|
+
↓
|
|
318
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
319
|
+
│ 4. BootstrapsRegistry: 注册业务插件 │
|
|
320
|
+
│ - I18nService: 国际化服务 │
|
|
321
|
+
│ - UserApiBootstrap: 用户 API 配置 │
|
|
322
|
+
│ - FeApiBootstrap: 业务 API 配置 │
|
|
323
|
+
│ - UserService: 用户认证服务 │
|
|
324
|
+
└────────────────────┬────────────────────────────────────────┘
|
|
325
|
+
↓
|
|
326
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
327
|
+
│ 5. Bootstrap.start(): 启动 │
|
|
328
|
+
│ ↓ │
|
|
329
|
+
│ onBefore 阶段: │
|
|
330
|
+
│ ├── I18nService.onBefore() → 加载翻译资源 │
|
|
331
|
+
│ ├── UserApiBootstrap.onBefore() → 配置 API 插件 │
|
|
332
|
+
│ ├── FeApiBootstrap.onBefore() → 配置业务 API │
|
|
333
|
+
│ └── UserService.onBefore() → 检查用户认证 │
|
|
334
|
+
│ ↓ │
|
|
335
|
+
│ onExecute 阶段: │
|
|
336
|
+
│ └── 执行插件主逻辑 │
|
|
337
|
+
│ ↓ │
|
|
338
|
+
│ onAfter 阶段: │
|
|
339
|
+
│ └── 清理和日志记录 │
|
|
340
|
+
└────────────────────┬────────────────────────────────────────┘
|
|
341
|
+
↓
|
|
342
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
343
|
+
│ 6. React 渲染 │
|
|
344
|
+
│ ReactDOM.render(<App />) │
|
|
345
|
+
└─────────────────────────────────────────────────────────────┘
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
## 🛠️ 项目中的实现
|
|
351
|
+
|
|
352
|
+
### 文件结构
|
|
353
|
+
|
|
354
|
+
```
|
|
355
|
+
src/
|
|
356
|
+
├── main.tsx # 应用入口
|
|
357
|
+
├── core/
|
|
358
|
+
│ ├── bootstraps/
|
|
359
|
+
│ │ ├── BootstrapClient.ts # Bootstrap 启动器
|
|
360
|
+
│ │ ├── BootstrapsRegistry.ts # 插件注册器
|
|
361
|
+
│ │ ├── PrintBootstrap.ts # 打印日志插件
|
|
362
|
+
│ │ └── IocIdentifierTest.ts # IOC 测试插件
|
|
363
|
+
│ ├── globals.ts # 全局变量封装
|
|
364
|
+
│ └── clientIoc/
|
|
365
|
+
│ ├── ClientIOC.ts # IOC 容器
|
|
366
|
+
│ └── ClientIOCRegister.ts # IOC 注册器
|
|
367
|
+
├── base/
|
|
368
|
+
│ ├── services/
|
|
369
|
+
│ │ ├── UserService.ts # 用户服务(插件)
|
|
370
|
+
│ │ └── I18nService.ts # 国际化服务(插件)
|
|
371
|
+
│ └── apis/
|
|
372
|
+
│ ├── userApi/
|
|
373
|
+
│ │ └── UserApiBootstrap.ts # 用户 API 配置插件
|
|
374
|
+
│ └── feApi/
|
|
375
|
+
│ └── FeApiBootstrap.ts # 业务 API 配置插件
|
|
376
|
+
└── uikit/
|
|
377
|
+
└── components/
|
|
378
|
+
└── BootstrapsProvider.tsx # Bootstrap Provider
|
|
379
|
+
```
|
|
177
380
|
|
|
178
|
-
|
|
179
|
-
|
|
381
|
+
### 1. 入口文件: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
|
+
// 🚀 启动 Bootstrap
|
|
393
|
+
BootstrapClient.main({
|
|
394
|
+
root: window, // 注入浏览器环境
|
|
395
|
+
bootHref: window.location.href, // 注入启动 URL
|
|
396
|
+
ioc: clientIOC // 注入 IOC 容器
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// 渲染 React 应用
|
|
400
|
+
createRoot(document.getElementById('root')!).render(
|
|
401
|
+
<StrictMode>
|
|
402
|
+
<App />
|
|
403
|
+
</StrictMode>
|
|
404
|
+
);
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### 2. Bootstrap 启动器: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️⃣ 创建 IOC 容器
|
|
422
|
+
const IOC = ioc.create({
|
|
423
|
+
pathname: bootHref,
|
|
424
|
+
appConfig: appConfig
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
// 2️⃣ 创建 Bootstrap 实例
|
|
428
|
+
const bootstrap = new Bootstrap({
|
|
429
|
+
root,
|
|
430
|
+
logger,
|
|
431
|
+
// IOC 容器配置
|
|
432
|
+
ioc: {
|
|
433
|
+
manager: IOC,
|
|
434
|
+
register: iocRegister
|
|
435
|
+
},
|
|
436
|
+
// 环境变量注入配置
|
|
437
|
+
envOptions: {
|
|
438
|
+
target: appConfig, // 注入到 AppConfig
|
|
439
|
+
source: Object.assign({}, import.meta.env, {
|
|
440
|
+
[envPrefix + 'BOOT_HREF']: bootHref // 添加启动 URL
|
|
441
|
+
}),
|
|
442
|
+
prefix: envPrefix, // 环境变量前缀
|
|
443
|
+
blackList: envBlackList // 黑名单
|
|
444
|
+
},
|
|
445
|
+
// 全局变量封装配置
|
|
446
|
+
globalOptions: {
|
|
447
|
+
sources: globals, // 封装的全局变量
|
|
448
|
+
target: browserGlobalsName // 挂载目标
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
try {
|
|
453
|
+
logger.info('bootstrap start...');
|
|
454
|
+
|
|
455
|
+
// 3️⃣ 初始化 Bootstrap
|
|
456
|
+
await bootstrap.initialize();
|
|
457
|
+
|
|
458
|
+
// 4️⃣ 注册业务插件
|
|
459
|
+
const bootstrapsRegistry = new BootstrapsRegistry(IOC);
|
|
460
|
+
|
|
461
|
+
// 5️⃣ 启动应用
|
|
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
|
+
**关键步骤解析:**
|
|
475
|
+
|
|
476
|
+
1. **创建 IOC 容器** - 统一管理所有依赖
|
|
477
|
+
2. **创建 Bootstrap 实例** - 配置初始化参数
|
|
478
|
+
3. **初始化** - 执行 IOC、环境变量、全局变量的初始化
|
|
479
|
+
4. **注册插件** - 添加业务初始化逻辑
|
|
480
|
+
5. **启动** - 执行所有插件的生命周期方法
|
|
481
|
+
|
|
482
|
+
### 3. 插件注册器: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
|
+
* 注册所有业务插件
|
|
502
|
+
*/
|
|
503
|
+
register(): BootstrapExecutorPlugin[] {
|
|
504
|
+
const IOC = this.IOC;
|
|
505
|
+
|
|
506
|
+
const bootstrapList = [
|
|
507
|
+
// 1. 国际化服务(需要最先初始化)
|
|
508
|
+
IOC(IOCIdentifier.I18nServiceInterface),
|
|
509
|
+
|
|
510
|
+
// 2. API 配置插件
|
|
511
|
+
new UserApiBootstarp(), // 用户 API
|
|
512
|
+
new FeApiBootstarp(), // 业务 API
|
|
513
|
+
AiApiBootstarp, // AI API
|
|
514
|
+
|
|
515
|
+
// 3. 其他插件
|
|
516
|
+
IOC(IOCIdentifier.I18nKeyErrorPlugin),
|
|
517
|
+
IOC(IOCIdentifier.ProcesserExecutorInterface)
|
|
518
|
+
];
|
|
519
|
+
|
|
520
|
+
// 开发环境:添加调试插件
|
|
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
|
+
**插件顺序很重要:**
|
|
531
|
+
|
|
532
|
+
- ✅ 国际化服务最先初始化(其他插件可能需要翻译)
|
|
533
|
+
- ✅ API 配置在业务逻辑之前
|
|
534
|
+
- ✅ 开发工具仅在开发环境加载
|
|
535
|
+
|
|
536
|
+
---
|
|
537
|
+
|
|
538
|
+
## 🔌 插件系统
|
|
214
539
|
|
|
215
|
-
|
|
216
|
-
- ✅ 业务逻辑被分离到启动器中
|
|
217
|
-
- ✅ 可以独立测试业务逻辑
|
|
218
|
-
- ✅ 可以复用业务逻辑到其他 UI 框架
|
|
540
|
+
### 插件类型
|
|
219
541
|
|
|
220
|
-
|
|
542
|
+
#### 1. 服务类插件(通过 IOC 注入)
|
|
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
|
+
* 在 Bootstrap 启动前加载翻译资源
|
|
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
|
+
// 加载翻译资源
|
|
565
|
+
return {
|
|
566
|
+
/* ... */
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// 注册方式
|
|
572
|
+
bootstrap.use([
|
|
573
|
+
IOC(IOCIdentifier.I18nServiceInterface) // 从 IOC 容器获取
|
|
574
|
+
]);
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
#### 2. 配置类插件(独立实例)
|
|
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
|
+
* 配置 User API 的插件
|
|
586
|
+
*/
|
|
227
587
|
onBefore({ parameters: { ioc } }: BootstrapContext): void {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
588
|
+
const userApi = ioc.get<UserApi>(UserApi);
|
|
589
|
+
|
|
590
|
+
// 添加 URL 处理插件
|
|
591
|
+
userApi.usePlugin(new FetchURLPlugin());
|
|
592
|
+
|
|
593
|
+
// 添加 Mock 插件(开发环境)
|
|
594
|
+
userApi.usePlugin(ioc.get(IOCIdentifier.ApiMockPlugin));
|
|
595
|
+
|
|
596
|
+
// 添加请求日志插件
|
|
597
|
+
userApi.usePlugin(ioc.get(RequestLogger));
|
|
234
598
|
}
|
|
235
599
|
}
|
|
236
600
|
|
|
237
|
-
//
|
|
601
|
+
// 注册方式
|
|
602
|
+
bootstrap.use([
|
|
603
|
+
new UserApiBootstarp() // 直接创建实例
|
|
604
|
+
]);
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
#### 3. 业务逻辑插件
|
|
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
|
+
* 在应用启动时检查用户认证状态
|
|
641
|
+
*/
|
|
262
642
|
async onBefore(): Promise<void> {
|
|
643
|
+
// 如果已登录,直接返回
|
|
263
644
|
if (this.isAuthenticated()) {
|
|
264
645
|
return;
|
|
265
646
|
}
|
|
266
647
|
|
|
648
|
+
// 尝试从存储中恢复用户信息
|
|
267
649
|
const userToken = this.getToken();
|
|
268
650
|
if (!userToken) {
|
|
269
651
|
throw new AppError('NO_USER_TOKEN');
|
|
270
652
|
}
|
|
271
653
|
|
|
654
|
+
// 获取用户信息
|
|
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
|
+
### 插件生命周期详解
|
|
665
|
+
|
|
666
|
+
```typescript
|
|
667
|
+
export interface BootstrapExecutorPlugin {
|
|
668
|
+
readonly pluginName: string;
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* onBefore: 在初始化前执行
|
|
672
|
+
*
|
|
673
|
+
* 适用场景:
|
|
674
|
+
* - 配置 API 客户端
|
|
675
|
+
* - 加载资源(翻译、主题等)
|
|
676
|
+
* - 检查用户认证
|
|
677
|
+
* - 初始化第三方库
|
|
678
|
+
*/
|
|
679
|
+
onBefore?(context: BootstrapContext): void | Promise<void>;
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* onExecute: 在初始化时执行
|
|
683
|
+
*
|
|
684
|
+
* 适用场景:
|
|
685
|
+
* - 执行主要业务逻辑
|
|
686
|
+
* - 启动后台任务
|
|
687
|
+
*/
|
|
688
|
+
onExecute?(context: BootstrapContext): void | Promise<void>;
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* onAfter: 在初始化后执行
|
|
692
|
+
*
|
|
693
|
+
* 适用场景:
|
|
694
|
+
* - 清理临时资源
|
|
695
|
+
* - 记录启动日志
|
|
696
|
+
* - 发送统计数据
|
|
697
|
+
*/
|
|
698
|
+
onAfter?(context: BootstrapContext): void | Promise<void>;
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* onError: 错误处理
|
|
702
|
+
*
|
|
703
|
+
* 适用场景:
|
|
704
|
+
* - 捕获插件错误
|
|
705
|
+
* - 错误日志记录
|
|
706
|
+
* - 错误恢复
|
|
707
|
+
*/
|
|
708
|
+
onError?(error: Error, context: BootstrapContext): void | Promise<void>;
|
|
709
|
+
}
|
|
710
|
+
```
|
|
285
711
|
|
|
286
|
-
|
|
287
|
-
bootstrap.use([
|
|
288
|
-
IOC(UserService), // 用户认证服务
|
|
289
|
-
new UserApiBootstarp(), // 用户 API 配置
|
|
290
|
-
IOC(I18nService) // 国际化服务
|
|
291
|
-
]);
|
|
712
|
+
---
|
|
292
713
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
714
|
+
## 🎯 实战示例
|
|
715
|
+
|
|
716
|
+
### 示例 1:国际化插件
|
|
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
|
+
// 加载翻译资源
|
|
733
|
+
const resources = this.loadAllResources();
|
|
734
|
+
|
|
735
|
+
// 初始化 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
|
+
// 从配置文件加载所有翻译资源
|
|
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
|
+
### 示例 2:API 配置插件
|
|
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. 配置基础 URL
|
|
778
|
+
feApi.setBaseURL(appConfig.apiBaseUrl);
|
|
779
|
+
|
|
780
|
+
// 2. 添加认证插件
|
|
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. 添加错误处理插件
|
|
791
|
+
feApi.usePlugin(
|
|
792
|
+
new ErrorHandlerPlugin({
|
|
793
|
+
onError: (error) => {
|
|
794
|
+
if (error.status === 401) {
|
|
795
|
+
// 未授权,跳转登录
|
|
796
|
+
const router = ioc.get(IOCIdentifier.RouteServiceInterface);
|
|
797
|
+
router.push('/login');
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
})
|
|
801
|
+
);
|
|
306
802
|
|
|
307
|
-
|
|
803
|
+
// 4. 添加请求日志插件(开发环境)
|
|
804
|
+
if (!appConfig.isProduction) {
|
|
805
|
+
feApi.usePlugin(new RequestLoggerPlugin());
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
```
|
|
308
810
|
|
|
309
|
-
###
|
|
811
|
+
### 示例 3:用户认证插件
|
|
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
|
+
* 在应用启动时自动恢复用户登录状态
|
|
843
|
+
*/
|
|
844
|
+
async onBefore(): Promise<void> {
|
|
845
|
+
try {
|
|
846
|
+
// 检查是否在登录页
|
|
847
|
+
if (this.routerService.isLoginPage()) {
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// 如果已经有用户信息,直接返回
|
|
852
|
+
if (this.isAuthenticated()) {
|
|
853
|
+
console.log('✅ User already authenticated');
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// 尝试从存储中恢复 token
|
|
858
|
+
const token = this.getToken();
|
|
859
|
+
if (!token) {
|
|
860
|
+
// 没有 token,跳转登录
|
|
861
|
+
throw new AppError('NO_USER_TOKEN');
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// 使用 token 获取用户信息
|
|
865
|
+
const userInfo = await this.userInfo();
|
|
866
|
+
console.log('✅ User authenticated:', userInfo.name);
|
|
867
|
+
} catch (error) {
|
|
868
|
+
// 认证失败,清理存储并跳转登录
|
|
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
|
+
### 示例 4:开发工具插件
|
|
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)]); // 相同的业务逻辑
|
|
893
|
+
onAfter({ parameters: { logger, ioc } }: BootstrapContext): void {
|
|
894
|
+
const appConfig = ioc.get<AppConfig>(IOCIdentifier.AppConfig);
|
|
335
895
|
|
|
336
|
-
//
|
|
337
|
-
|
|
338
|
-
|
|
896
|
+
// 打印应用信息
|
|
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
|
+
// 打印已注册的服务
|
|
903
|
+
logger.info('📋 Registered Services:');
|
|
904
|
+
logger.info(' - UserService');
|
|
905
|
+
logger.info(' - I18nService');
|
|
906
|
+
logger.info(' - RouteService');
|
|
907
|
+
|
|
908
|
+
// 打印警告(如果有)
|
|
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
|
+
## 🧪 测试:Bootstrap 的核心优势
|
|
919
|
+
|
|
920
|
+
### 为什么测试如此重要?
|
|
921
|
+
|
|
922
|
+
Bootstrap 架构的一个**最重要的优势**就是**可测试性**。通过分离初始化逻辑和 UI,我们可以:
|
|
923
|
+
|
|
924
|
+
- ✅ 独立测试每个插件
|
|
925
|
+
- ✅ 轻松 mock 依赖
|
|
926
|
+
- ✅ 快速运行测试(不需要渲染 UI)
|
|
927
|
+
- ✅ 提高测试覆盖率
|
|
928
|
+
|
|
929
|
+
### 传统方式 vs Bootstrap 方式
|
|
930
|
+
|
|
931
|
+
#### ❌ 传统方式:组件中混杂初始化逻辑
|
|
932
|
+
|
|
933
|
+
```typescript
|
|
934
|
+
// ❌ 传统组件:难以测试
|
|
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. 初始化国际化
|
|
945
|
+
await i18next.init({
|
|
946
|
+
lng: 'zh',
|
|
947
|
+
resources: { /* ... */ }
|
|
948
|
+
});
|
|
949
|
+
setI18nReady(true);
|
|
950
|
+
|
|
951
|
+
// 2. 配置 API
|
|
952
|
+
api.setBaseURL('https://api.example.com');
|
|
953
|
+
api.usePlugin(new AuthPlugin());
|
|
954
|
+
|
|
955
|
+
// 3. 检查用户认证
|
|
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
|
+
**测试代码(传统方式):😰😰😰 非常困难**
|
|
981
|
+
|
|
982
|
+
```typescript
|
|
983
|
+
// ❌ 传统方式的测试:充满技巧和 hack
|
|
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
|
+
// 😰 需要 mock 全局变量
|
|
991
|
+
global.localStorage = {
|
|
992
|
+
getItem: vi.fn(),
|
|
993
|
+
setItem: vi.fn(),
|
|
994
|
+
removeItem: vi.fn(),
|
|
995
|
+
clear: vi.fn()
|
|
996
|
+
};
|
|
997
|
+
|
|
998
|
+
// 😰 需要 mock fetch
|
|
999
|
+
global.fetch = vi.fn();
|
|
1000
|
+
|
|
1001
|
+
// 😰 需要 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
|
+
// 😰 设置复杂的 mock
|
|
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
|
+
// 😰 需要等待多个异步操作
|
|
1019
|
+
await waitFor(() => {
|
|
1020
|
+
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
|
1021
|
+
}, { timeout: 3000 });
|
|
359
1022
|
|
|
360
|
-
|
|
1023
|
+
// 😰 难以验证中间状态
|
|
1024
|
+
expect(fetch).toHaveBeenCalledWith('/api/user', expect.any(Object));
|
|
1025
|
+
});
|
|
1026
|
+
|
|
1027
|
+
it('should handle error', async () => {
|
|
1028
|
+
// 😰 每个测试都需要重新设置 mock
|
|
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
|
+
// 😰 问题:
|
|
1039
|
+
// 1. 需要 mock 大量全局变量(localStorage, fetch, i18next)
|
|
1040
|
+
// 2. 测试运行慢(需要渲染组件)
|
|
1041
|
+
// 3. 难以测试错误场景
|
|
1042
|
+
// 4. 测试之间可能互相干扰
|
|
1043
|
+
// 5. 难以测试初始化的各个步骤
|
|
1044
|
+
});
|
|
1045
|
+
```
|
|
365
1046
|
|
|
366
|
-
|
|
367
|
-
// 创建一个简单的插件
|
|
368
|
-
export class SimplePlugin implements BootstrapExecutorPlugin {
|
|
369
|
-
readonly pluginName = 'SimplePlugin';
|
|
1047
|
+
#### ✅ Bootstrap 方式:独立测试插件
|
|
370
1048
|
|
|
371
|
-
|
|
372
|
-
|
|
1049
|
+
```typescript
|
|
1050
|
+
// ✅ Bootstrap 方式:逻辑和 UI 分离
|
|
1051
|
+
// 1. 插件实现
|
|
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 组件变得简单
|
|
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
|
+
**测试代码(Bootstrap 方式):😊😊😊 非常简单**
|
|
1086
|
+
|
|
1087
|
+
```typescript
|
|
1088
|
+
// ✅ Bootstrap 方式的测试:清晰、简单、快速
|
|
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
|
+
// ✅ 只需要 mock 依赖接口,不需要 mock 全局变量
|
|
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
|
+
// ✅ 创建服务实例
|
|
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
|
+
// ✅ 设置测试数据
|
|
1120
|
+
mockStorage.getItem.mockReturnValue('mock-token');
|
|
1121
|
+
mockApi.getUserInfo.mockResolvedValue({
|
|
1122
|
+
id: '1',
|
|
1123
|
+
name: 'John Doe'
|
|
1124
|
+
});
|
|
1125
|
+
|
|
1126
|
+
// ✅ 执行插件生命周期
|
|
1127
|
+
await userService.onBefore();
|
|
1128
|
+
|
|
1129
|
+
// ✅ 清晰的断言
|
|
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
|
+
// ✅ 轻松测试错误场景
|
|
1140
|
+
mockStorage.getItem.mockReturnValue(null);
|
|
1141
|
+
|
|
1142
|
+
// ✅ 验证错误
|
|
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
|
+
// ✅ 轻松模拟 API 错误
|
|
1149
|
+
mockStorage.getItem.mockReturnValue('mock-token');
|
|
1150
|
+
mockApi.getUserInfo.mockRejectedValue(new Error('Network error'));
|
|
1151
|
+
|
|
1152
|
+
// ✅ 验证错误处理
|
|
1153
|
+
await expect(userService.onBefore()).rejects.toThrow('Network error');
|
|
1154
|
+
});
|
|
1155
|
+
|
|
1156
|
+
// ✅ 优势:
|
|
1157
|
+
// 1. 不需要 mock 全局变量
|
|
1158
|
+
// 2. 测试运行快(不需要渲染 UI)
|
|
1159
|
+
// 3. 易于测试错误场景
|
|
1160
|
+
// 4. 测试之间完全独立
|
|
1161
|
+
// 5. 可以单独测试每个初始化步骤
|
|
384
1162
|
});
|
|
1163
|
+
```
|
|
385
1164
|
|
|
386
|
-
|
|
387
|
-
|
|
1165
|
+
### 测试复杂度对比
|
|
1166
|
+
|
|
1167
|
+
| 测试场景 | 传统方式 | Bootstrap 方式 | 提升 |
|
|
1168
|
+
| ---------------- | -------------------------------------------- | ------------------------------- | --------- |
|
|
1169
|
+
| **Mock 复杂度** | 😰😰😰 需要 mock 全局变量、fetch、i18next 等 | 😊 只需 mock 依赖接口 | **80%** |
|
|
1170
|
+
| **测试运行速度** | 😰😰 慢(需要渲染组件,等待异步) | 😊😊😊 快(纯逻辑测试) | **5-10x** |
|
|
1171
|
+
| **测试错误场景** | 😰😰😰 困难(需要复杂的 mock 设置) | 😊😊😊 简单(直接 mock reject) | **90%** |
|
|
1172
|
+
| **测试隔离性** | 😰😰 差(全局变量可能互相影响) | 😊😊😊 好(每个测试独立) | **100%** |
|
|
1173
|
+
| **测试可读性** | 😰😰 差(充满 mock 和 hack) | 😊😊😊 好(清晰的输入输出) | **80%** |
|
|
1174
|
+
| **覆盖率** | 😰😰 低(难以覆盖所有分支) | 😊😊😊 高(易于覆盖所有场景) | **50%** |
|
|
1175
|
+
|
|
1176
|
+
### 实际项目中的测试示例
|
|
1177
|
+
|
|
1178
|
+
#### 示例 1:测试 I18n 插件
|
|
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
|
+
// ✅ 执行插件生命周期
|
|
1213
|
+
service.onBefore();
|
|
1214
|
+
|
|
1215
|
+
// ✅ 验证初始化配置
|
|
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
|
+
// ✅ 测试语言检测逻辑
|
|
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
|
+
// ✅ 测试边界情况
|
|
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
|
+
// ✅ 测试错误场景
|
|
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
|
+
#### 示例 2:测试 Bootstrap 启动流程
|
|
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 依赖
|
|
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
|
+
// ✅ 执行启动流程
|
|
1319
|
+
const result = await BootstrapClient.main(mockArgs);
|
|
1320
|
+
|
|
1321
|
+
// ✅ 验证启动结果
|
|
1322
|
+
expect(result.bootHref).toBe('http://localhost:3000');
|
|
395
1323
|
|
|
396
|
-
|
|
1324
|
+
// ✅ 验证全局变量注入
|
|
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
|
+
// ✅ 测试错误场景
|
|
1339
|
+
mockArgs.ioc.create = vi.fn().mockImplementation(() => {
|
|
1340
|
+
throw new Error('IOC creation failed');
|
|
1341
|
+
});
|
|
403
1342
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
1343
|
+
// ✅ 验证错误不会导致应用崩溃
|
|
1344
|
+
await expect(BootstrapClient.main(mockArgs)).rejects.toThrow(
|
|
1345
|
+
'IOC creation failed'
|
|
1346
|
+
);
|
|
1347
|
+
});
|
|
1348
|
+
});
|
|
409
1349
|
});
|
|
410
|
-
|
|
1350
|
+
```
|
|
1351
|
+
|
|
1352
|
+
#### 示例 3:测试 API 配置插件
|
|
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
|
+
// ✅ 创建 mock 上下文
|
|
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
|
+
// ✅ 执行插件生命周期
|
|
1387
|
+
plugin.onBefore(mockContext);
|
|
1388
|
+
|
|
1389
|
+
// ✅ 验证 API 配置
|
|
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
|
-
// 移动端特定配置
|
|
1397
|
+
// ✅ 验证添加了多个插件
|
|
1398
|
+
expect(mockUserApi.usePlugin).toHaveBeenCalledTimes(3);
|
|
1399
|
+
});
|
|
416
1400
|
});
|
|
417
|
-
|
|
1401
|
+
```
|
|
1402
|
+
|
|
1403
|
+
### 测试最佳实践
|
|
1404
|
+
|
|
1405
|
+
#### 1. ✅ 使用 Vitest 的测试工具
|
|
1406
|
+
|
|
1407
|
+
```typescript
|
|
1408
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
1409
|
+
|
|
1410
|
+
describe('MyPlugin', () => {
|
|
1411
|
+
beforeEach(() => {
|
|
1412
|
+
// ✅ 每个测试前重置 mock
|
|
1413
|
+
vi.clearAllMocks();
|
|
1414
|
+
});
|
|
1415
|
+
|
|
1416
|
+
afterEach(() => {
|
|
1417
|
+
// ✅ 清理资源
|
|
1418
|
+
vi.restoreAllMocks();
|
|
1419
|
+
});
|
|
418
1420
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
// 小程序特定配置
|
|
1421
|
+
it('should do something', () => {
|
|
1422
|
+
// 测试逻辑
|
|
1423
|
+
});
|
|
423
1424
|
});
|
|
424
|
-
miniprogramBootstrap.use([IOC(UserService), IOC(MiniprogramSpecificService)]);
|
|
425
1425
|
```
|
|
426
1426
|
|
|
427
|
-
####
|
|
1427
|
+
#### 2. ✅ 测试插件的生命周期
|
|
1428
|
+
|
|
1429
|
+
```typescript
|
|
1430
|
+
describe('UserService Plugin', () => {
|
|
1431
|
+
it('should execute onBefore lifecycle', async () => {
|
|
1432
|
+
const service = new UserService(mockApi, mockStorage, mockRouter);
|
|
1433
|
+
|
|
1434
|
+
// ✅ 测试 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
|
+
// ✅ 测试 onAfter
|
|
1444
|
+
await service.onAfter?.();
|
|
1445
|
+
|
|
1446
|
+
// 验证清理逻辑
|
|
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
|
+
// ✅ 测试 onError
|
|
1454
|
+
await service.onError?.(error, mockContext);
|
|
1455
|
+
|
|
1456
|
+
// 验证错误处理
|
|
1457
|
+
});
|
|
434
1458
|
});
|
|
435
|
-
|
|
1459
|
+
```
|
|
1460
|
+
|
|
1461
|
+
#### 3. ✅ 测试边界情况和错误场景
|
|
1462
|
+
|
|
1463
|
+
```typescript
|
|
1464
|
+
describe('UserService Error Handling', () => {
|
|
1465
|
+
it('should handle missing token', async () => {
|
|
1466
|
+
mockStorage.getItem.mockReturnValue(null);
|
|
1467
|
+
|
|
1468
|
+
// ✅ 验证错误
|
|
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
|
+
// ✅ 验证错误处理
|
|
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
|
+
// ✅ 验证 401 错误处理
|
|
1485
|
+
await expect(service.onBefore()).rejects.toThrow('401 Unauthorized');
|
|
1486
|
+
});
|
|
441
1487
|
});
|
|
442
|
-
|
|
1488
|
+
```
|
|
1489
|
+
|
|
1490
|
+
#### 4. ✅ 测试插件之间的依赖
|
|
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 先初始化
|
|
1499
|
+
await i18nService.onBefore();
|
|
1500
|
+
|
|
1501
|
+
// ✅ 然后初始化 UserService
|
|
1502
|
+
await userService.onBefore();
|
|
1503
|
+
|
|
1504
|
+
// ✅ 验证 UserService 可以使用翻译
|
|
1505
|
+
expect(i18n.t('some.key')).toBeDefined();
|
|
1506
|
+
});
|
|
448
1507
|
});
|
|
449
|
-
appBBootstrap.use([IOC(UserService), IOC(AppBSpecificService)]);
|
|
450
1508
|
```
|
|
451
1509
|
|
|
452
|
-
|
|
1510
|
+
### 运行测试
|
|
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
|
+
# 运行所有测试
|
|
1514
|
+
npm run test
|
|
463
1515
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
1516
|
+
# 运行测试并监听文件变化
|
|
1517
|
+
npm run test -- --watch
|
|
1518
|
+
|
|
1519
|
+
# 运行特定文件的测试
|
|
1520
|
+
npm run test -- UserService.test.ts
|
|
469
1521
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
bootstrap.use([IOC(UserService)]); // 新逻辑
|
|
473
|
-
// 旧逻辑仍然可以继续使用
|
|
1522
|
+
# 生成测试覆盖率报告
|
|
1523
|
+
npm run test -- --coverage
|
|
474
1524
|
```
|
|
475
1525
|
|
|
476
|
-
###
|
|
1526
|
+
### 测试覆盖率目标
|
|
1527
|
+
|
|
1528
|
+
在 Bootstrap 架构下,我们可以轻松达到高覆盖率:
|
|
1529
|
+
|
|
1530
|
+
- **插件逻辑**:> 90% 覆盖率
|
|
1531
|
+
- **服务层**:> 85% 覆盖率
|
|
1532
|
+
- **API 适配器**:> 80% 覆盖率
|
|
1533
|
+
- **整体应用**:> 75% 覆盖率
|
|
477
1534
|
|
|
478
|
-
|
|
1535
|
+
### 总结:测试的价值
|
|
479
1536
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
1537
|
+
Bootstrap 架构通过分离关注点,让测试变得:
|
|
1538
|
+
|
|
1539
|
+
1. **更简单** - 不需要 mock 全局变量和复杂的环境
|
|
1540
|
+
2. **更快速** - 纯逻辑测试,不需要渲染 UI
|
|
1541
|
+
3. **更可靠** - 测试之间完全独立,不会互相干扰
|
|
1542
|
+
4. **更全面** - 易于测试所有边界情况和错误场景
|
|
1543
|
+
5. **更有信心** - 高覆盖率保证代码质量
|
|
1544
|
+
|
|
1545
|
+
> 💡 **重要提示**:可测试性是 Bootstrap 架构最大的优势之一。如果你发现某个插件难以测试,很可能是设计有问题,需要重新考虑职责划分。
|
|
1546
|
+
|
|
1547
|
+
---
|
|
1548
|
+
|
|
1549
|
+
## 💎 最佳实践
|
|
1550
|
+
|
|
1551
|
+
### 1. 插件设计原则
|
|
1552
|
+
|
|
1553
|
+
#### ✅ 单一职责
|
|
1554
|
+
|
|
1555
|
+
```typescript
|
|
1556
|
+
// ✅ 好的插件设计:只做一件事
|
|
1557
|
+
export class ApiConfigPlugin implements BootstrapExecutorPlugin {
|
|
1558
|
+
readonly pluginName = 'ApiConfigPlugin';
|
|
484
1559
|
|
|
485
|
-
// 单一职责
|
|
486
1560
|
onBefore({ parameters: { ioc } }: BootstrapContext): void {
|
|
487
|
-
//
|
|
488
|
-
ioc.get<
|
|
1561
|
+
// 只负责配置 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
|
-
// ❌
|
|
1568
|
+
// ❌ 不好的插件设计:做了太多事
|
|
493
1569
|
export class BadPlugin implements BootstrapExecutorPlugin {
|
|
494
1570
|
readonly pluginName = 'BadPlugin';
|
|
495
1571
|
|
|
496
|
-
// 做了太多事情
|
|
497
1572
|
onBefore({ parameters: { ioc } }: BootstrapContext): void {
|
|
498
1573
|
// 配置 API
|
|
499
|
-
ioc.get<
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
//
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
1574
|
+
const api = ioc.get<FeApi>(FeApi);
|
|
1575
|
+
api.setBaseURL(config.apiBaseUrl);
|
|
1576
|
+
|
|
1577
|
+
// 初始化国际化
|
|
1578
|
+
i18next.init({
|
|
1579
|
+
/* ... */
|
|
1580
|
+
});
|
|
1581
|
+
|
|
1582
|
+
// 检查用户认证
|
|
1583
|
+
checkAuth();
|
|
1584
|
+
|
|
1585
|
+
// 配置路由
|
|
1586
|
+
configureRouter();
|
|
1587
|
+
|
|
1588
|
+
// 太多职责!❌
|
|
506
1589
|
}
|
|
507
1590
|
}
|
|
508
1591
|
```
|
|
509
1592
|
|
|
510
|
-
####
|
|
1593
|
+
#### ✅ 明确依赖
|
|
1594
|
+
|
|
1595
|
+
```typescript
|
|
1596
|
+
// ✅ 通过构造函数注入依赖
|
|
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
|
+
// ❌ 直接创建依赖
|
|
1606
|
+
export class BadUserService implements ExecutorPlugin {
|
|
1607
|
+
private api = new UserApi(); // ❌ 硬编码依赖
|
|
1608
|
+
private config = new AppConfig(); // ❌ 难以测试
|
|
1609
|
+
}
|
|
1610
|
+
```
|
|
1611
|
+
|
|
1612
|
+
### 2. 错误处理
|
|
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
|
+
// ✅ 优雅的错误处理
|
|
1623
|
+
if (error instanceof AppError) {
|
|
1624
|
+
// 业务错误
|
|
1625
|
+
this.handleBusinessError(error);
|
|
1626
|
+
} else if (error instanceof NetworkError) {
|
|
1627
|
+
// 网络错误
|
|
1628
|
+
this.handleNetworkError(error);
|
|
1629
|
+
} else {
|
|
1630
|
+
// 未知错误
|
|
1631
|
+
this.logger.error('Unknown error:', error);
|
|
520
1632
|
}
|
|
521
1633
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
1634
|
+
// 不要让错误传播,导致应用崩溃
|
|
1635
|
+
// 而是进行适当的降级处理
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
526
1638
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
1639
|
+
private handleBusinessError(error: AppError) {
|
|
1640
|
+
if (error.code === 'NO_USER_TOKEN') {
|
|
1641
|
+
// 跳转登录页
|
|
1642
|
+
this.router.push('/login');
|
|
1643
|
+
} else if (error.code === 'TOKEN_EXPIRED') {
|
|
1644
|
+
// 刷新 token
|
|
1645
|
+
this.refreshToken();
|
|
533
1646
|
}
|
|
534
1647
|
}
|
|
535
1648
|
}
|
|
536
1649
|
```
|
|
537
1650
|
|
|
538
|
-
|
|
1651
|
+
### 3. 性能优化
|
|
1652
|
+
|
|
1653
|
+
```typescript
|
|
1654
|
+
// ✅ 按需加载插件
|
|
1655
|
+
export class BootstrapsRegistry {
|
|
1656
|
+
register(): BootstrapExecutorPlugin[] {
|
|
1657
|
+
const plugins: BootstrapExecutorPlugin[] = [
|
|
1658
|
+
// 必需插件
|
|
1659
|
+
IOC(IOCIdentifier.I18nServiceInterface),
|
|
1660
|
+
new UserApiBootstarp()
|
|
1661
|
+
];
|
|
1662
|
+
|
|
1663
|
+
// 开发环境插件
|
|
1664
|
+
if (!this.appConfig.isProduction) {
|
|
1665
|
+
plugins.push(new DevToolsPlugin(), new MockDataPlugin());
|
|
1666
|
+
}
|
|
539
1667
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
1668
|
+
// 功能开关插件
|
|
1669
|
+
if (this.appConfig.features.analytics) {
|
|
1670
|
+
plugins.push(new AnalyticsPlugin());
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
return plugins;
|
|
1674
|
+
}
|
|
544
1675
|
}
|
|
1676
|
+
```
|
|
1677
|
+
|
|
1678
|
+
### 4. 日志记录
|
|
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
|
+
## ❓ 常见问题
|
|
1701
|
+
|
|
1702
|
+
### Q1: Bootstrap 和 React 的生命周期有什么关系?
|
|
1703
|
+
|
|
1704
|
+
**A:** Bootstrap 在 React 渲染之前执行。
|
|
1705
|
+
|
|
1706
|
+
```
|
|
1707
|
+
Bootstrap 初始化 → Bootstrap 启动 → React 渲染
|
|
1708
|
+
```
|
|
1709
|
+
|
|
1710
|
+
### Q2: 插件执行顺序重要吗?
|
|
1711
|
+
|
|
1712
|
+
**A:** 非常重要!插件按照注册顺序依次执行。
|
|
1713
|
+
|
|
1714
|
+
```typescript
|
|
1715
|
+
// ✅ 正确的顺序
|
|
1716
|
+
bootstrap.use([
|
|
1717
|
+
IOC(I18nService), // 1. 先初始化国际化(其他插件可能需要)
|
|
1718
|
+
new ApiConfigPlugin(), // 2. 再配置 API
|
|
1719
|
+
IOC(UserService) // 3. 最后检查用户认证(依赖 API)
|
|
1720
|
+
]);
|
|
1721
|
+
|
|
1722
|
+
// ❌ 错误的顺序
|
|
1723
|
+
bootstrap.use([
|
|
1724
|
+
IOC(UserService), // ❌ 用户服务依赖 API,但 API 还没配置
|
|
1725
|
+
new ApiConfigPlugin(), // 配置 API
|
|
1726
|
+
IOC(I18nService) // 国际化在最后(太晚了)
|
|
1727
|
+
]);
|
|
1728
|
+
```
|
|
1729
|
+
|
|
1730
|
+
### Q3: 如何调试 Bootstrap?
|
|
1731
|
+
|
|
1732
|
+
```typescript
|
|
1733
|
+
// 方法 1:使用日志
|
|
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
|
+
// ... 你的逻辑
|
|
1740
|
+
logger.info(`[${this.pluginName}] Completed`);
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
// 方法 2:使用调试插件
|
|
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: 如何测试插件?
|
|
1759
|
+
|
|
1760
|
+
```typescript
|
|
1761
|
+
describe('UserService Plugin', () => {
|
|
1762
|
+
it('should initialize user on startup', async () => {
|
|
1763
|
+
// 创建 mock 依赖
|
|
1764
|
+
const mockApi = {
|
|
1765
|
+
getUserInfo: jest.fn().mockResolvedValue({ name: 'John' })
|
|
1766
|
+
};
|
|
1767
|
+
const mockStorage = {
|
|
1768
|
+
getItem: jest.fn().mockReturnValue('mock-token')
|
|
1769
|
+
};
|
|
1770
|
+
|
|
1771
|
+
// 创建服务
|
|
1772
|
+
const userService = new UserService(
|
|
1773
|
+
mockRouter,
|
|
1774
|
+
mockApi,
|
|
1775
|
+
mockConfig,
|
|
1776
|
+
mockStorage
|
|
1777
|
+
);
|
|
1778
|
+
|
|
1779
|
+
// 执行插件生命周期
|
|
1780
|
+
await userService.onBefore();
|
|
1781
|
+
|
|
1782
|
+
// 验证
|
|
1783
|
+
expect(mockApi.getUserInfo).toHaveBeenCalledWith('mock-token');
|
|
1784
|
+
});
|
|
1785
|
+
});
|
|
1786
|
+
```
|
|
1787
|
+
|
|
1788
|
+
### Q5: Bootstrap 适合所有项目吗?
|
|
1789
|
+
|
|
1790
|
+
**A:** 不一定。Bootstrap 更适合:
|
|
1791
|
+
|
|
1792
|
+
✅ **适合使用的场景:**
|
|
1793
|
+
|
|
1794
|
+
- 中大型应用
|
|
1795
|
+
- 需要复杂初始化逻辑
|
|
1796
|
+
- 多端应用(Web、移动端、小程序)
|
|
1797
|
+
- 需要模块化和可测试性
|
|
1798
|
+
- 团队协作开发
|
|
1799
|
+
|
|
1800
|
+
❌ **不适合的场景:**
|
|
1801
|
+
|
|
1802
|
+
- 简单的展示页面
|
|
1803
|
+
- 原型项目
|
|
1804
|
+
- 没有复杂初始化逻辑的项目
|
|
1805
|
+
|
|
1806
|
+
### Q6: 如何保证测试覆盖率?
|
|
1807
|
+
|
|
1808
|
+
**A:** Bootstrap 架构天然支持高覆盖率:
|
|
1809
|
+
|
|
1810
|
+
```typescript
|
|
1811
|
+
// ✅ 每个插件都可以独立测试
|
|
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
|
+
// 易于测试所有边界情况
|
|
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
|
+
**覆盖率目标:**
|
|
1833
|
+
|
|
1834
|
+
- 插件逻辑:> 90%
|
|
1835
|
+
- 服务层:> 85%
|
|
1836
|
+
- API 适配器:> 80%
|
|
1837
|
+
|
|
1838
|
+
### Q7: Vitest 和 Jest 有什么区别?
|
|
1839
|
+
|
|
1840
|
+
**A:** 本项目使用 Vitest,它是 Vite 生态的测试框架:
|
|
1841
|
+
|
|
1842
|
+
| 特性 | Vitest | Jest |
|
|
1843
|
+
| ------------ | ----------------------------- | ------------ |
|
|
1844
|
+
| **速度** | ⚡ 非常快(基于 Vite) | 慢 |
|
|
1845
|
+
| **配置** | 🎯 零配置(复用 vite.config) | 需要单独配置 |
|
|
1846
|
+
| **ESM 支持** | ✅ 原生支持 | ⚠️ 实验性 |
|
|
1847
|
+
| **API** | 与 Jest 兼容 | - |
|
|
1848
|
+
| **HMR** | ✅ 支持 | ❌ 不支持 |
|
|
1849
|
+
|
|
1850
|
+
```typescript
|
|
1851
|
+
// Vitest 使用方式(与 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
|
+
## 📚 相关文档
|
|
1868
|
+
|
|
1869
|
+
- [项目架构设计](./index.md) - 了解整体架构
|
|
1870
|
+
- [IOC 容器](./ioc.md) - 依赖注入详解
|
|
1871
|
+
- [环境变量](./env.md) - 环境配置管理
|
|
1872
|
+
- [全局变量封装](./global.md) - 浏览器 API 封装
|
|
1873
|
+
|
|
1874
|
+
---
|
|
1875
|
+
|
|
1876
|
+
## 🎉 总结
|
|
1877
|
+
|
|
1878
|
+
Bootstrap 启动器是现代前端架构的重要组成部分,它帮助我们:
|
|
1879
|
+
|
|
1880
|
+
1. **分离关注点** - UI 和初始化逻辑分离
|
|
1881
|
+
2. **提高可维护性** - 模块化设计,职责清晰
|
|
1882
|
+
3. **增强可测试性** - 每个插件可独立测试
|
|
1883
|
+
4. **支持团队协作** - 不同开发者可以独立开发插件
|
|
1884
|
+
5. **适应变化** - 易于扩展和修改
|
|
553
1885
|
|
|
554
|
-
Bootstrap
|
|
1886
|
+
通过 Bootstrap,我们构建了一个更加健壮、可维护、可测试的前端应用架构。
|
|
555
1887
|
|
|
556
|
-
|
|
557
|
-
2. **提高可维护性**:模块化设计,易于理解和修改
|
|
558
|
-
3. **增强可测试性**:每个模块都可以独立测试
|
|
559
|
-
4. **支持团队协作**:不同角色可以专注于自己的领域
|
|
560
|
-
5. **适应变化**:业务逻辑变化不影响 UI,UI 变化不影响业务逻辑
|
|
1888
|
+
---
|
|
561
1889
|
|
|
562
|
-
|
|
1890
|
+
**问题反馈:**
|
|
1891
|
+
如果你对 Bootstrap 有任何疑问或建议,请在团队频道中讨论或提交 Issue。
|