@qlover/create-app 0.7.15 → 0.9.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 +21 -0
- package/dist/configs/_common/.github/workflows/general-check.yml +1 -1
- package/dist/configs/_common/.github/workflows/release.yml +2 -2
- package/dist/configs/_common/.gitignore.template +6 -0
- package/dist/configs/_common/.prettierignore +17 -5
- package/dist/configs/_common/.vscode/settings.json +6 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/templates/next-app/.env.template +1 -1
- package/dist/templates/next-app/README.en.md +0 -1
- package/dist/templates/next-app/README.md +0 -1
- package/dist/templates/next-app/config/Identifier/api.ts +5 -5
- package/dist/templates/next-app/config/Identifier/common/admint.table.ts +69 -0
- package/dist/templates/next-app/config/Identifier/common/common.ts +76 -0
- package/dist/templates/next-app/config/Identifier/common/index.ts +3 -0
- package/dist/templates/next-app/config/Identifier/{validator.ts → common/validators.ts} +5 -5
- package/dist/templates/next-app/config/Identifier/index.ts +2 -12
- package/dist/templates/next-app/config/Identifier/pages/index.ts +6 -0
- package/dist/templates/next-app/config/Identifier/pages/page.admin.home.ts +27 -0
- package/dist/templates/next-app/config/Identifier/pages/page.admin.locales.ts +266 -0
- package/dist/templates/next-app/config/Identifier/pages/page.admin.user.ts +293 -0
- package/dist/templates/{react-app/config/Identifier → next-app/config/Identifier/pages}/page.home.ts +15 -22
- package/dist/templates/next-app/config/Identifier/{page.login.ts → pages/page.login.ts} +28 -34
- package/dist/templates/next-app/config/Identifier/{page.register.ts → pages/page.register.ts} +30 -29
- package/dist/templates/next-app/config/adminNavs.ts +19 -0
- package/dist/templates/next-app/config/common.ts +22 -13
- package/dist/templates/next-app/config/i18n/HomeI18n.ts +5 -5
- package/dist/templates/next-app/config/i18n/admin18n.ts +61 -19
- package/dist/templates/next-app/config/i18n/i18nConfig.ts +2 -0
- package/dist/templates/next-app/config/i18n/i18nKeyScheam.ts +36 -0
- package/dist/templates/next-app/config/i18n/loginI18n.ts +22 -22
- package/dist/templates/next-app/config/i18n/register18n.ts +23 -24
- package/dist/templates/next-app/docs/en/index.md +0 -1
- package/dist/templates/next-app/docs/en/project-structure.md +0 -1
- package/dist/templates/next-app/docs/zh/index.md +0 -1
- package/dist/templates/next-app/docs/zh/project-structure.md +0 -1
- package/dist/templates/next-app/make/generateLocales.ts +19 -12
- package/dist/templates/next-app/migrations/schema/LocalesSchema.ts +15 -0
- package/dist/templates/next-app/migrations/sql/1694244000000.sql +11 -0
- package/dist/templates/next-app/package.json +7 -3
- package/dist/templates/next-app/public/locales/en.json +172 -207
- package/dist/templates/next-app/public/locales/zh.json +172 -207
- package/dist/templates/next-app/src/app/[locale]/admin/locales/page.tsx +153 -0
- package/dist/templates/next-app/src/app/[locale]/admin/users/page.tsx +48 -50
- package/dist/templates/next-app/src/app/[locale]/login/LoginForm.tsx +2 -2
- package/dist/templates/next-app/src/app/api/admin/locales/create/route.ts +34 -0
- package/dist/templates/next-app/src/app/api/admin/locales/import/route.ts +40 -0
- package/dist/templates/next-app/src/app/api/admin/locales/route.ts +42 -0
- package/dist/templates/next-app/src/app/api/admin/locales/update/route.ts +32 -0
- package/dist/templates/next-app/src/app/api/locales/json/route.ts +44 -0
- package/dist/templates/next-app/src/base/cases/AdminPageManager.ts +1 -13
- package/dist/templates/next-app/src/base/cases/Datetime.ts +18 -0
- package/dist/templates/next-app/src/base/cases/DialogErrorPlugin.ts +12 -6
- package/dist/templates/next-app/src/base/cases/ResourceState.ts +17 -0
- package/dist/templates/next-app/src/base/cases/TranslateI18nInterface.ts +25 -0
- package/dist/templates/next-app/src/base/cases/ZodColumnBuilder.ts +200 -0
- package/dist/templates/next-app/src/base/port/ZodBuilderInterface.ts +8 -0
- package/dist/templates/next-app/src/base/services/AdminLocalesService.ts +20 -0
- package/dist/templates/next-app/src/base/services/AdminPageEvent.ts +26 -0
- package/dist/templates/next-app/src/base/services/AdminPageScheduler.ts +42 -0
- package/dist/templates/next-app/src/base/services/ResourceService.ts +122 -0
- package/dist/templates/next-app/src/base/services/adminApi/AdminLocalesApi.ts +104 -0
- package/dist/templates/next-app/src/base/services/adminApi/AdminUserApi.ts +38 -5
- package/dist/templates/next-app/src/base/services/appApi/AppApiPlugin.ts +1 -1
- package/dist/templates/next-app/src/i18n/request.ts +30 -1
- package/dist/templates/next-app/src/server/PageParams.ts +2 -10
- package/dist/templates/next-app/src/server/port/DBBridgeInterface.ts +5 -0
- package/dist/templates/next-app/src/server/port/DBTableInterface.ts +2 -0
- package/dist/templates/next-app/src/server/port/LocalesRepositoryInterface.ts +43 -0
- package/dist/templates/next-app/src/server/repositorys/LocalesRepository.ts +197 -0
- package/dist/templates/next-app/src/server/services/ApiLocaleService.ts +122 -0
- package/dist/templates/next-app/src/server/sqlBridges/SupabaseBridge.ts +60 -11
- package/dist/templates/next-app/src/server/validators/ExtendedExecutorError.ts +6 -0
- package/dist/templates/next-app/src/server/validators/LocalesValidator.ts +131 -0
- package/dist/templates/next-app/src/server/validators/LoginValidator.ts +2 -5
- package/dist/templates/next-app/src/server/validators/PaginationValidator.ts +32 -16
- package/dist/templates/next-app/src/styles/css/antd-themes/pagination/_default.css +2 -1
- package/dist/templates/next-app/src/styles/css/antd-themes/pagination/dark.css +28 -29
- package/dist/templates/next-app/src/styles/css/antd-themes/pagination/pink.css +2 -1
- package/dist/templates/next-app/src/uikit/components/AdminLayout.tsx +17 -3
- package/dist/templates/next-app/src/uikit/components/BaseHeader.tsx +5 -4
- package/dist/templates/next-app/src/uikit/components/BaseLayout.tsx +5 -4
- package/dist/templates/next-app/src/uikit/components/BootstrapsProvider.tsx +3 -2
- package/dist/templates/next-app/src/uikit/components/ComboProvider.tsx +1 -1
- package/dist/templates/next-app/src/uikit/components/EditableCell.tsx +118 -0
- package/dist/templates/next-app/src/uikit/components/LogoutButton.tsx +5 -6
- package/dist/templates/next-app/src/uikit/components/ThemeSwitcher.tsx +1 -1
- package/dist/templates/next-app/src/uikit/components/With.tsx +2 -2
- package/dist/templates/next-app/src/uikit/components/localesImportButton/LocalesImportButton.tsx +62 -0
- package/dist/templates/next-app/src/uikit/components/localesImportButton/LocalesImportEvent.ts +28 -0
- package/dist/templates/next-app/src/uikit/components/localesImportButton/import.module.css +6 -0
- package/dist/templates/next-app/src/uikit/hook/useI18nInterface.ts +8 -14
- package/dist/templates/next-app/src/uikit/hook/useWarnTranslations.ts +25 -0
- package/dist/templates/react-app/.prettierignore +17 -0
- package/dist/templates/react-app/README.en.md +71 -54
- package/dist/templates/react-app/README.md +35 -18
- package/dist/templates/react-app/__tests__/__mocks__/BootstrapTest.ts +14 -0
- package/dist/templates/react-app/__tests__/__mocks__/MockAppConfit.ts +1 -1
- package/dist/templates/react-app/__tests__/__mocks__/MockDialogHandler.ts +2 -2
- package/dist/templates/react-app/__tests__/__mocks__/MockLogger.ts +1 -1
- package/dist/templates/react-app/__tests__/__mocks__/components/TestApp.tsx +45 -0
- package/dist/templates/react-app/__tests__/__mocks__/components/TestBootstrapsProvider.tsx +34 -0
- package/dist/templates/react-app/__tests__/__mocks__/components/TestRouter.tsx +46 -0
- package/dist/templates/react-app/__tests__/__mocks__/components/index.ts +12 -0
- package/dist/templates/react-app/__tests__/__mocks__/createMockGlobals.ts +1 -2
- package/dist/templates/react-app/__tests__/__mocks__/testIOC/TestIOC.ts +51 -0
- package/dist/templates/react-app/__tests__/__mocks__/testIOC/TestIOCRegister.ts +69 -0
- package/dist/templates/react-app/__tests__/setup/index.ts +1 -51
- package/dist/templates/react-app/__tests__/setup/setupGlobal.ts +51 -0
- package/dist/templates/react-app/__tests__/src/App.structure.test.tsx +115 -0
- package/dist/templates/react-app/__tests__/src/base/cases/AppConfig.test.ts +2 -2
- package/dist/templates/react-app/__tests__/src/base/cases/AppError.test.ts +1 -1
- package/dist/templates/react-app/__tests__/src/base/cases/DialogHandler.test.ts +3 -5
- package/dist/templates/react-app/__tests__/src/base/cases/I18nKeyErrorPlugin.test.ts +13 -2
- package/dist/templates/react-app/__tests__/src/base/cases/InversifyContainer.test.ts +1 -1
- package/dist/templates/react-app/__tests__/src/base/cases/PublicAssetsPath.test.ts +1 -1
- package/dist/templates/react-app/__tests__/src/base/cases/RequestLogger.test.ts +5 -5
- package/dist/templates/react-app/__tests__/src/base/cases/RequestStatusCatcher.test.ts +1 -2
- package/dist/templates/react-app/__tests__/src/base/cases/RouterLoader.test.ts +25 -15
- package/dist/templates/react-app/__tests__/src/base/services/I18nService.test.ts +29 -15
- package/dist/templates/react-app/__tests__/src/core/IOC.test.ts +19 -9
- package/dist/templates/react-app/__tests__/src/core/bootstraps/BootstrapClient.test.ts +153 -0
- package/dist/templates/react-app/__tests__/src/core/bootstraps/BootstrapsApp.test.ts +9 -7
- package/dist/templates/react-app/__tests__/src/main.integration.test.tsx +4 -5
- package/dist/templates/react-app/__tests__/src/main.test.tsx +4 -4
- package/dist/templates/react-app/__tests__/src/uikit/components/BaseHeader.test.tsx +68 -59
- package/dist/templates/react-app/__tests__/src/uikit/components/chatMessage/ChatRoot.test.tsx +274 -0
- package/dist/templates/react-app/config/IOCIdentifier.ts +11 -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/components/component.chatMessage.ts +56 -0
- package/dist/templates/react-app/config/Identifier/components/component.messageBaseList.ts +103 -0
- package/dist/templates/react-app/config/Identifier/index.ts +1 -9
- package/dist/templates/react-app/config/Identifier/pages/index.ts +9 -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/pages/page.message.ts +20 -0
- 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 +81 -61
- 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/chatMessageI18n.ts +17 -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/messageBaseListI18n.ts +22 -0
- package/dist/templates/react-app/config/i18n/messageI18n.ts +14 -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/components/chat-message-component.md +314 -0
- package/dist/templates/react-app/docs/en/components/chat-message-refactor.md +270 -0
- package/dist/templates/react-app/docs/en/components/message-base-list-component.md +172 -0
- 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/components/chat-message-component.md +314 -0
- package/dist/templates/react-app/docs/zh/components/chat-message-refactor.md +270 -0
- package/dist/templates/react-app/docs/zh/components/message-base-list-component.md +172 -0
- 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 +233 -179
- package/dist/templates/react-app/public/locales/zh/common.json +233 -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/MessagePage.tsx +40 -0
- 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/MessageBaseList.tsx +240 -0
- 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/components/chatMessage/ChatMessageBridge.ts +176 -0
- package/dist/templates/react-app/src/uikit/components/chatMessage/ChatRoot.tsx +21 -0
- package/dist/templates/react-app/src/uikit/components/chatMessage/FocusBar.tsx +106 -0
- package/dist/templates/react-app/src/uikit/components/chatMessage/MessageApi.ts +271 -0
- package/dist/templates/react-app/src/uikit/components/chatMessage/MessageItem.tsx +102 -0
- package/dist/templates/react-app/src/uikit/components/chatMessage/MessagesList.tsx +86 -0
- package/dist/templates/react-app/src/uikit/contexts/BaseRouteContext.ts +17 -11
- package/dist/templates/react-app/src/uikit/contexts/IOCContext.ts +13 -0
- package/dist/templates/react-app/src/uikit/hooks/useAppTranslation.ts +26 -0
- package/dist/templates/react-app/src/uikit/hooks/useI18nGuard.ts +8 -11
- package/dist/templates/react-app/src/uikit/hooks/useI18nInterface.ts +25 -0
- package/dist/templates/react-app/src/uikit/hooks/useIOC.ts +35 -0
- package/dist/templates/react-app/src/uikit/hooks/useNavigateBridge.ts +3 -3
- package/dist/templates/react-app/src/uikit/hooks/useStrictEffect.ts +0 -1
- package/dist/templates/react-app/tsconfig.e2e.json +21 -0
- package/dist/templates/react-app/tsconfig.json +8 -1
- package/dist/templates/react-app/tsconfig.node.json +1 -1
- package/dist/templates/react-app/tsconfig.test.json +3 -1
- package/dist/templates/react-app/vite.config.ts +50 -34
- package/package.json +2 -1
- package/dist/configs/react-app/eslint.config.js +0 -94
- package/dist/templates/next-app/config/Identifier/common.error.ts +0 -41
- package/dist/templates/next-app/config/Identifier/common.ts +0 -69
- package/dist/templates/next-app/config/Identifier/page.about.ts +0 -181
- package/dist/templates/next-app/config/Identifier/page.admin.ts +0 -48
- package/dist/templates/next-app/config/Identifier/page.executor.ts +0 -272
- package/dist/templates/next-app/config/Identifier/page.identifiter.ts +0 -39
- package/dist/templates/next-app/config/Identifier/page.jsonStorage.ts +0 -72
- package/dist/templates/next-app/config/Identifier/page.request.ts +0 -182
- package/dist/templates/next-app/src/base/cases/ChatAction.ts +0 -21
- package/dist/templates/next-app/src/base/cases/FocusBarAction.ts +0 -36
- package/dist/templates/next-app/src/base/cases/RequestState.ts +0 -20
- package/dist/templates/next-app/src/base/port/AdminPageInterface.ts +0 -85
- package/dist/templates/next-app/src/base/port/AsyncStateInterface.ts +0 -7
- package/dist/templates/next-app/src/base/services/AdminUserService.ts +0 -45
- package/dist/templates/next-app/src/uikit/components/ChatRoot.tsx +0 -17
- package/dist/templates/next-app/src/uikit/components/chat/ChatActionInterface.ts +0 -30
- package/dist/templates/next-app/src/uikit/components/chat/ChatFocusBar.tsx +0 -65
- package/dist/templates/next-app/src/uikit/components/chat/ChatMessages.tsx +0 -59
- package/dist/templates/next-app/src/uikit/components/chat/ChatWrap.tsx +0 -28
- package/dist/templates/next-app/src/uikit/components/chat/FocusBarActionInterface.ts +0 -19
- package/dist/templates/next-app/src/uikit/hook/useMountedClient.ts +0 -17
- package/dist/templates/next-app/src/uikit/hook/useStore.ts +0 -15
- package/dist/templates/react-app/__tests__/__mocks__/I18nService.ts +0 -13
- package/dist/templates/react-app/__tests__/src/App.test.tsx +0 -139
- package/dist/templates/react-app/config/Identifier/page.identifiter.ts +0 -39
- package/dist/templates/react-app/config/i18n.ts +0 -15
- package/dist/templates/react-app/docs/en/project-structure.md +0 -434
- package/dist/templates/react-app/docs/zh/project-structure.md +0 -434
- package/dist/templates/react-app/src/base/cases/RequestState.ts +0 -20
- package/dist/templates/react-app/src/base/port/AsyncStateInterface.ts +0 -7
- package/dist/templates/react-app/src/uikit/hooks/useDocumentTitle.ts +0 -15
- package/dist/templates/react-app/src/uikit/hooks/useStore.ts +0 -15
|
@@ -1,422 +1,1364 @@
|
|
|
1
|
-
# IOC 容器
|
|
1
|
+
# IOC 容器 (依赖注入)
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 📋 目录
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
- [核心理念](#-核心理念) - UI 分离,逻辑独立
|
|
6
|
+
- [什么是 IOC](#-什么是-ioc) - 控制反转
|
|
7
|
+
- [为什么需要 IOC](#-为什么需要-ioc) - 解决的核心问题
|
|
8
|
+
- [两个关键问题](#-两个关键问题) - 为什么需要接口?为什么简单组件也要分离?
|
|
9
|
+
- [项目中的实现](#-项目中的实现) - Bootstrap 集成
|
|
10
|
+
- [使用方式](#-使用方式) - 实战指南
|
|
11
|
+
- [测试](#-测试) - 独立测试和组合测试
|
|
12
|
+
- [最佳实践](#-最佳实践) - 8 条核心实践
|
|
13
|
+
- [常见问题](#-常见问题) - FAQ
|
|
6
14
|
|
|
7
|
-
|
|
15
|
+
---
|
|
8
16
|
|
|
9
|
-
##
|
|
17
|
+
## 🎯 核心理念
|
|
10
18
|
|
|
11
|
-
|
|
19
|
+
> **🚨 重要原则:UI 就是 UI,逻辑就是逻辑,两者必须分离!**
|
|
12
20
|
|
|
13
|
-
|
|
14
|
-
- **TypeScript**:提供类型安全
|
|
15
|
-
- **装饰器模式**:使用 `@injectable()` 和 `@inject()` 装饰器
|
|
21
|
+
> **⭐ 核心优势:UI 和逻辑可以独立测试,也可以组合测试!**
|
|
16
22
|
|
|
17
|
-
###
|
|
23
|
+
### 核心概念
|
|
18
24
|
|
|
19
25
|
```
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
│
|
|
25
|
-
│
|
|
26
|
-
│
|
|
27
|
-
│
|
|
28
|
-
│
|
|
29
|
-
│
|
|
30
|
-
|
|
31
|
-
│
|
|
32
|
-
│
|
|
26
|
+
┌─────────────────────────────────────────┐
|
|
27
|
+
│ 传统方式:UI 和逻辑混在一起 │
|
|
28
|
+
│ │
|
|
29
|
+
│ Component (组件) │
|
|
30
|
+
│ ├── UI 渲染 │
|
|
31
|
+
│ ├── 业务逻辑 │
|
|
32
|
+
│ ├── API 调用 │
|
|
33
|
+
│ ├── 状态管理 │
|
|
34
|
+
│ └── 数据处理 │
|
|
35
|
+
│ │
|
|
36
|
+
│ ❌ 问题: │
|
|
37
|
+
│ - 难以测试(需要渲染组件) │
|
|
38
|
+
│ - 逻辑无法复用 │
|
|
39
|
+
│ - 职责不清晰 │
|
|
40
|
+
└─────────────────────────────────────────┘
|
|
41
|
+
|
|
42
|
+
┌─────────────────────────────────────────┐
|
|
43
|
+
│ IOC 方式:UI 和逻辑完全分离 │
|
|
44
|
+
│ │
|
|
45
|
+
│ Component (UI 层) │
|
|
46
|
+
│ └── 只负责渲染 │
|
|
47
|
+
│ ↓ 通过 IOC 获取 │
|
|
48
|
+
│ Service (逻辑层) │
|
|
49
|
+
│ ├── 业务逻辑 │
|
|
50
|
+
│ ├── API 调用 │
|
|
51
|
+
│ ├── 状态管理 │
|
|
52
|
+
│ └── 数据处理 │
|
|
53
|
+
│ │
|
|
54
|
+
│ ✅ 优势: │
|
|
55
|
+
│ - UI 和逻辑可以独立测试 │
|
|
56
|
+
│ - 逻辑可以复用 │
|
|
57
|
+
│ - 职责清晰 │
|
|
58
|
+
└─────────────────────────────────────────┘
|
|
33
59
|
```
|
|
34
60
|
|
|
35
|
-
|
|
61
|
+
---
|
|
36
62
|
|
|
37
|
-
|
|
63
|
+
## 🔄 什么是 IOC
|
|
38
64
|
|
|
39
|
-
|
|
40
|
-
// config/IOCIdentifier.ts
|
|
41
|
-
export const IOCIdentifier = Object.freeze({
|
|
42
|
-
JSON: 'JSON',
|
|
43
|
-
LocalStorage: 'LocalStorage',
|
|
44
|
-
Logger: 'Logger',
|
|
45
|
-
AppConfig: 'AppConfig'
|
|
46
|
-
// ... 更多标识符
|
|
47
|
-
});
|
|
48
|
-
```
|
|
65
|
+
IOC(Inversion of Control,控制反转)= **不要自己 new,让容器帮你创建和管理对象**
|
|
49
66
|
|
|
50
|
-
###
|
|
67
|
+
### 传统方式 vs IOC
|
|
51
68
|
|
|
52
|
-
```
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
// ... 更多映射
|
|
69
|
+
```typescript
|
|
70
|
+
// ❌ 传统方式:自己创建依赖(强耦合)
|
|
71
|
+
class UserComponent {
|
|
72
|
+
private userService = new UserService(); // 自己 new
|
|
73
|
+
private storage = new LocalStorage(); // 自己 new
|
|
74
|
+
private api = new UserApi(); // 自己 new
|
|
75
|
+
|
|
76
|
+
async loadUser() {
|
|
77
|
+
return await this.userService.getUser();
|
|
78
|
+
}
|
|
63
79
|
}
|
|
80
|
+
|
|
81
|
+
// 问题:
|
|
82
|
+
// 1. UserComponent 依赖具体的实现类
|
|
83
|
+
// 2. 无法替换 UserService 的实现
|
|
84
|
+
// 3. 测试时无法 mock UserService
|
|
85
|
+
// 4. UserService 的依赖需要手动创建
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
// ✅ IOC 方式:容器注入依赖(松耦合)
|
|
89
|
+
function UserComponent() {
|
|
90
|
+
// 从 IOC 容器获取服务
|
|
91
|
+
const userService = useIOC('UserServiceInterface'); // 容器提供
|
|
92
|
+
|
|
93
|
+
async function loadUser() {
|
|
94
|
+
return await userService.getUser();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// UI 只负责渲染
|
|
98
|
+
return <div>...</div>;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 优势:
|
|
102
|
+
// 1. UserComponent 依赖接口,不依赖实现
|
|
103
|
+
// 2. 可以轻松替换 UserService 的实现
|
|
104
|
+
// 3. 测试时可以 mock UserService
|
|
105
|
+
// 4. UserService 的依赖由容器管理
|
|
64
106
|
```
|
|
65
107
|
|
|
66
|
-
###
|
|
108
|
+
### 类比理解
|
|
67
109
|
|
|
68
|
-
```tsx
|
|
69
|
-
// core/IOC.ts
|
|
70
|
-
export const IOC = createIOCFunction<IOCIdentifierMap>(
|
|
71
|
-
new InversifyContainer()
|
|
72
|
-
);
|
|
73
110
|
```
|
|
111
|
+
传统方式 = 自己做饭
|
|
112
|
+
- 需要买菜(创建依赖)
|
|
113
|
+
- 需要做饭(管理生命周期)
|
|
114
|
+
- 需要洗碗(清理资源)
|
|
115
|
+
|
|
116
|
+
IOC 方式 = 去餐厅
|
|
117
|
+
- 点菜(告诉容器需要什么)
|
|
118
|
+
- 等待上菜(容器提供服务)
|
|
119
|
+
- 不需要关心厨房的事(依赖管理由容器负责)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
74
123
|
|
|
75
|
-
##
|
|
124
|
+
## 🤔 为什么需要 IOC
|
|
76
125
|
|
|
77
|
-
###
|
|
126
|
+
### 核心问题:UI 和逻辑混在一起
|
|
78
127
|
|
|
79
|
-
|
|
80
|
-
// 使用类名获取
|
|
81
|
-
const userService = IOC(UserService);
|
|
128
|
+
#### ❌ 问题示例:没有 UI 分离
|
|
82
129
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const
|
|
130
|
+
```typescript
|
|
131
|
+
// ❌ 传统组件:UI 和逻辑混在一起
|
|
132
|
+
function UserProfile() {
|
|
133
|
+
const [user, setUser] = useState(null);
|
|
134
|
+
const [loading, setLoading] = useState(false);
|
|
135
|
+
const [error, setError] = useState(null);
|
|
136
|
+
|
|
137
|
+
// 😰 业务逻辑混在组件中
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
setLoading(true);
|
|
140
|
+
|
|
141
|
+
// 😰 API 调用在组件中
|
|
142
|
+
fetch('/api/user')
|
|
143
|
+
.then(res => res.json())
|
|
144
|
+
.then(data => {
|
|
145
|
+
// 😰 数据处理在组件中
|
|
146
|
+
const processedData = {
|
|
147
|
+
...data,
|
|
148
|
+
fullName: `${data.firstName} ${data.lastName}`
|
|
149
|
+
};
|
|
150
|
+
setUser(processedData);
|
|
151
|
+
})
|
|
152
|
+
.catch(err => setError(err))
|
|
153
|
+
.finally(() => setLoading(false));
|
|
154
|
+
}, []);
|
|
87
155
|
|
|
88
|
-
//
|
|
89
|
-
const
|
|
156
|
+
// 😰 更多业务逻辑
|
|
157
|
+
const handleLogout = () => {
|
|
158
|
+
localStorage.removeItem('token');
|
|
159
|
+
localStorage.removeItem('user');
|
|
160
|
+
window.location.href = '/login';
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// UI 渲染
|
|
164
|
+
if (loading) return <div>Loading...</div>;
|
|
165
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<div>
|
|
169
|
+
<h1>{user?.fullName}</h1>
|
|
170
|
+
<button onClick={handleLogout}>Logout</button>
|
|
171
|
+
</div>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 😰😰😰 问题总结:
|
|
176
|
+
// 1. UI 和逻辑混在一起,难以维护
|
|
177
|
+
// 2. 逻辑无法复用(如果另一个组件也需要用户信息怎么办?)
|
|
178
|
+
// 3. 难以测试(需要渲染组件才能测试业务逻辑)
|
|
179
|
+
// 4. 职责不清晰(组件做了太多事)
|
|
180
|
+
// 5. 无法单独测试逻辑(必须通过 UI 测试)
|
|
90
181
|
```
|
|
91
182
|
|
|
92
|
-
|
|
183
|
+
#### ✅ 解决方案:IOC + UI 分离
|
|
93
184
|
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
185
|
+
```typescript
|
|
186
|
+
// ✅ 步骤 1:定义接口(Port)
|
|
187
|
+
export interface UserServiceInterface {
|
|
188
|
+
getUser(): Promise<UserInfo>;
|
|
189
|
+
logout(): Promise<void>;
|
|
190
|
+
isAuthenticated(): boolean;
|
|
191
|
+
}
|
|
99
192
|
|
|
193
|
+
// ✅ 步骤 2:实现服务(逻辑层)
|
|
100
194
|
@injectable()
|
|
101
|
-
export class UserService
|
|
195
|
+
export class UserService implements UserServiceInterface {
|
|
102
196
|
constructor(
|
|
103
|
-
@inject(
|
|
104
|
-
@inject(
|
|
105
|
-
@inject(IOCIdentifier.
|
|
106
|
-
@inject(IOCIdentifier.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
197
|
+
@inject(UserApi) private api: UserApi,
|
|
198
|
+
@inject(IOCIdentifier.AppConfig) private config: AppConfig,
|
|
199
|
+
@inject(IOCIdentifier.LocalStorageEncrypt) private storage: Storage,
|
|
200
|
+
@inject(IOCIdentifier.RouteServiceInterface) private router: RouteService
|
|
201
|
+
) {}
|
|
202
|
+
|
|
203
|
+
// 纯逻辑:获取用户信息
|
|
204
|
+
async getUser(): Promise<UserInfo> {
|
|
205
|
+
const data = await this.api.getUserInfo();
|
|
206
|
+
|
|
207
|
+
// 数据处理
|
|
208
|
+
return {
|
|
209
|
+
...data,
|
|
210
|
+
fullName: `${data.firstName} ${data.lastName}`
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// 纯逻辑:退出登录
|
|
215
|
+
async logout(): Promise<void> {
|
|
216
|
+
this.storage.removeItem(this.config.userTokenStorageKey);
|
|
217
|
+
this.storage.removeItem(this.config.userInfoStorageKey);
|
|
218
|
+
await this.router.push('/login');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
isAuthenticated(): boolean {
|
|
222
|
+
return !!this.storage.getItem(this.config.userTokenStorageKey);
|
|
119
223
|
}
|
|
120
224
|
}
|
|
225
|
+
|
|
226
|
+
// ✅ 步骤 3:UI 组件(UI 层)
|
|
227
|
+
function UserProfile() {
|
|
228
|
+
// 从 IOC 容器获取服务
|
|
229
|
+
const userService = useIOC('UserServiceInterface');
|
|
230
|
+
const [user, setUser] = useState(null);
|
|
231
|
+
const [loading, setLoading] = useState(false);
|
|
232
|
+
|
|
233
|
+
useEffect(() => {
|
|
234
|
+
setLoading(true);
|
|
235
|
+
// ✅ UI 只调用服务,不包含业务逻辑
|
|
236
|
+
userService.getUser()
|
|
237
|
+
.then(setUser)
|
|
238
|
+
.finally(() => setLoading(false));
|
|
239
|
+
}, []);
|
|
240
|
+
|
|
241
|
+
// ✅ UI 只负责渲染和事件绑定
|
|
242
|
+
if (loading) return <div>Loading...</div>;
|
|
243
|
+
|
|
244
|
+
return (
|
|
245
|
+
<div>
|
|
246
|
+
<h1>{user?.fullName}</h1>
|
|
247
|
+
<button onClick={() => userService.logout()}>Logout</button>
|
|
248
|
+
</div>
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ✅✅✅ 优势总结:
|
|
253
|
+
// 1. UI 和逻辑完全分离,职责清晰
|
|
254
|
+
// 2. 逻辑可以复用(其他组件也可以使用 UserService)
|
|
255
|
+
// 3. 易于测试(可以独立测试 UserService,不需要渲染 UI)
|
|
256
|
+
// 4. 易于维护(修改逻辑不影响 UI,修改 UI 不影响逻辑)
|
|
257
|
+
// 5. 可以单独测试逻辑(不依赖 UI)
|
|
121
258
|
```
|
|
122
259
|
|
|
123
|
-
###
|
|
260
|
+
### 对比总结
|
|
261
|
+
|
|
262
|
+
| 特性 | 没有 UI 分离 | IOC + UI 分离 |
|
|
263
|
+
| -------------- | --------------------------- | --------------------------- |
|
|
264
|
+
| **职责清晰度** | ❌ UI 和逻辑混在一起 | ✅ UI 只负责渲染,逻辑独立 |
|
|
265
|
+
| **可测试性** | ❌ 必须渲染组件才能测试 | ✅ 逻辑可以独立测试 |
|
|
266
|
+
| **可复用性** | ❌ 逻辑无法复用 | ✅ 逻辑可以在多个组件中复用 |
|
|
267
|
+
| **可维护性** | ❌ 修改逻辑影响 UI | ✅ UI 和逻辑独立修改 |
|
|
268
|
+
| **测试速度** | ❌ 慢(需要渲染 UI) | ✅ 快(纯逻辑测试) |
|
|
269
|
+
| **测试复杂度** | ❌ 高(需要 mock 很多东西) | ✅ 低(只需 mock 接口) |
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## ❓ 两个关键问题
|
|
274
|
+
|
|
275
|
+
### 问题 1:为什么一个实现类也需要一个接口?
|
|
276
|
+
|
|
277
|
+
很多开发者会问:"如果 `UserService` 只有一个实现类,为什么还要定义 `UserServiceInterface` 接口?"
|
|
278
|
+
|
|
279
|
+
#### 答案:为了可测试性和灵活性
|
|
280
|
+
|
|
281
|
+
```typescript
|
|
282
|
+
// ❌ 没有接口:难以测试
|
|
283
|
+
class UserComponent {
|
|
284
|
+
constructor(
|
|
285
|
+
@inject(UserService) private userService: UserService // 依赖具体实现
|
|
286
|
+
) {}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// 测试时:
|
|
290
|
+
describe('UserComponent', () => {
|
|
291
|
+
it('should load user', () => {
|
|
292
|
+
// ❌ 问题:无法 mock UserService
|
|
293
|
+
// UserService 有很多依赖(API、Storage、Router 等)
|
|
294
|
+
// 需要创建所有这些依赖才能创建 UserService
|
|
295
|
+
|
|
296
|
+
const userApi = new UserApi(); // 需要创建
|
|
297
|
+
const storage = new Storage(); // 需要创建
|
|
298
|
+
const router = new Router(); // 需要创建
|
|
299
|
+
const config = new AppConfig(); // 需要创建
|
|
300
|
+
|
|
301
|
+
const userService = new UserService(userApi, config, storage, router);
|
|
302
|
+
const component = new UserComponent(userService);
|
|
303
|
+
|
|
304
|
+
// 😰 太复杂了!
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// ✅ 有接口:易于测试
|
|
309
|
+
class UserComponent {
|
|
310
|
+
constructor(
|
|
311
|
+
@inject('UserServiceInterface') // 依赖接口
|
|
312
|
+
private userService: UserServiceInterface
|
|
313
|
+
) {}
|
|
314
|
+
}
|
|
124
315
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
316
|
+
// 测试时:
|
|
317
|
+
describe('UserComponent', () => {
|
|
318
|
+
it('should load user', () => {
|
|
319
|
+
// ✅ 只需要 mock 接口
|
|
320
|
+
const mockUserService: UserServiceInterface = {
|
|
321
|
+
getUser: jest.fn().mockResolvedValue({ name: 'John' }),
|
|
322
|
+
logout: jest.fn(),
|
|
323
|
+
isAuthenticated: jest.fn().mockReturnValue(true)
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const component = new UserComponent(mockUserService);
|
|
327
|
+
|
|
328
|
+
// ✅ 简单清晰!
|
|
329
|
+
});
|
|
330
|
+
});
|
|
132
331
|
```
|
|
133
332
|
|
|
134
|
-
|
|
333
|
+
**关键优势:**
|
|
334
|
+
|
|
335
|
+
1. **测试简单** - 只需 mock 接口方法,不需要创建真实依赖
|
|
336
|
+
2. **隔离性** - 测试 UserComponent 时不需要关心 UserService 的实现细节
|
|
337
|
+
3. **灵活性** - 将来可以轻松替换实现(如添加 MockUserService、CacheUserService 等)
|
|
338
|
+
4. **解耦** - 组件只依赖接口,不依赖具体实现
|
|
339
|
+
|
|
340
|
+
**即使只有一个实现类,接口也是必需的,因为:**
|
|
341
|
+
|
|
342
|
+
- ✅ 测试时需要 mock
|
|
343
|
+
- ✅ 将来可能有新的实现
|
|
344
|
+
- ✅ 组件不应该依赖具体实现
|
|
345
|
+
- ✅ 接口是契约,实现是细节
|
|
346
|
+
|
|
347
|
+
### 问题 2:为什么一个简单的 UI 组件也需要 UI 分离?
|
|
135
348
|
|
|
136
|
-
|
|
349
|
+
很多开发者会问:"我的组件很简单,只是显示一个用户名,为什么还要分离?"
|
|
137
350
|
|
|
138
|
-
|
|
139
|
-
// core/registers/RegisterGlobals.ts
|
|
140
|
-
export const RegisterGlobals: IOCRegister = {
|
|
141
|
-
register(container, _, options): void {
|
|
142
|
-
// 注册应用配置
|
|
143
|
-
container.bind(IOCIdentifier.AppConfig, options!.appConfig);
|
|
351
|
+
#### 答案:为了可测试性和未来的扩展性
|
|
144
352
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
353
|
+
```typescript
|
|
354
|
+
// ❌ 简单组件,没有分离
|
|
355
|
+
function UserName() {
|
|
356
|
+
const [name, setName] = useState('');
|
|
148
357
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
358
|
+
useEffect(() => {
|
|
359
|
+
// 😰 即使很简单,逻辑也混在 UI 中
|
|
360
|
+
fetch('/api/user')
|
|
361
|
+
.then(res => res.json())
|
|
362
|
+
.then(data => setName(data.name));
|
|
363
|
+
}, []);
|
|
364
|
+
|
|
365
|
+
return <span>{name}</span>;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// 问题:
|
|
369
|
+
// 1. 无法测试逻辑(必须渲染组件)
|
|
370
|
+
// 2. 如果逻辑变复杂了怎么办?(加缓存、加错误处理等)
|
|
371
|
+
// 3. 如果其他组件也需要用户名怎么办?(复制粘贴?)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
// ✅ 简单组件,但有分离
|
|
375
|
+
// 1. 服务(逻辑层)
|
|
376
|
+
@injectable()
|
|
377
|
+
export class UserService implements UserServiceInterface {
|
|
378
|
+
constructor(@inject(UserApi) private api: UserApi) {}
|
|
379
|
+
|
|
380
|
+
async getUserName(): Promise<string> {
|
|
381
|
+
const user = await this.api.getUserInfo();
|
|
382
|
+
return user.name;
|
|
153
383
|
}
|
|
154
|
-
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// 2. UI 组件(UI 层)
|
|
387
|
+
function UserName() {
|
|
388
|
+
const userService = useIOC('UserServiceInterface');
|
|
389
|
+
const [name, setName] = useState('');
|
|
390
|
+
|
|
391
|
+
useEffect(() => {
|
|
392
|
+
userService.getUserName().then(setName);
|
|
393
|
+
}, []);
|
|
394
|
+
|
|
395
|
+
return <span>{name}</span>;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// 优势:
|
|
399
|
+
// 1. ✅ 可以独立测试 getUserName 逻辑
|
|
400
|
+
// 2. ✅ 将来逻辑变复杂时,只需修改 UserService
|
|
401
|
+
// 3. ✅ 其他组件可以复用 UserService
|
|
402
|
+
// 4. ✅ UI 组件保持简单,只负责渲染
|
|
155
403
|
```
|
|
156
404
|
|
|
157
|
-
|
|
405
|
+
**关键场景:逻辑逐步变复杂**
|
|
158
406
|
|
|
159
|
-
```
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
407
|
+
```typescript
|
|
408
|
+
// ❌ 没有分离:逻辑变复杂后,组件变得臃肿
|
|
409
|
+
function UserName() {
|
|
410
|
+
const [name, setName] = useState('');
|
|
411
|
+
const [loading, setLoading] = useState(false);
|
|
412
|
+
const [error, setError] = useState(null);
|
|
164
413
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
storage: container.get(IOCIdentifier.LocalStorageEncrypt)
|
|
168
|
-
});
|
|
414
|
+
useEffect(() => {
|
|
415
|
+
setLoading(true);
|
|
169
416
|
|
|
170
|
-
|
|
171
|
-
|
|
417
|
+
// 😰 加缓存
|
|
418
|
+
const cached = localStorage.getItem('userName');
|
|
419
|
+
if (cached) {
|
|
420
|
+
setName(cached);
|
|
421
|
+
setLoading(false);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
172
424
|
|
|
173
|
-
//
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
storage: localStorage
|
|
425
|
+
// 😰 加错误处理
|
|
426
|
+
fetch('/api/user')
|
|
427
|
+
.then(res => {
|
|
428
|
+
if (!res.ok) throw new Error('Failed');
|
|
429
|
+
return res.json();
|
|
179
430
|
})
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
container.bind(
|
|
184
|
-
RouteService,
|
|
185
|
-
new RouteService({
|
|
186
|
-
routes: baseRoutes,
|
|
187
|
-
logger
|
|
431
|
+
.then(data => {
|
|
432
|
+
setName(data.name);
|
|
433
|
+
localStorage.setItem('userName', data.name);
|
|
188
434
|
})
|
|
189
|
-
|
|
435
|
+
.catch(err => setError(err))
|
|
436
|
+
.finally(() => setLoading(false));
|
|
437
|
+
}, []);
|
|
438
|
+
|
|
439
|
+
// 😰 组件变复杂了
|
|
440
|
+
if (loading) return <span>Loading...</span>;
|
|
441
|
+
if (error) return <span>Error</span>;
|
|
442
|
+
return <span>{name}</span>;
|
|
443
|
+
}
|
|
444
|
+
|
|
190
445
|
|
|
191
|
-
|
|
192
|
-
|
|
446
|
+
// ✅ 有分离:逻辑变复杂后,只需修改服务
|
|
447
|
+
@injectable()
|
|
448
|
+
export class UserService implements UserServiceInterface {
|
|
449
|
+
constructor(
|
|
450
|
+
@inject(UserApi) private api: UserApi,
|
|
451
|
+
@inject(IOCIdentifier.LocalStorageEncrypt) private storage: Storage
|
|
452
|
+
) {}
|
|
453
|
+
|
|
454
|
+
// ✅ 逻辑在服务中,清晰明了
|
|
455
|
+
async getUserName(): Promise<string> {
|
|
456
|
+
// 缓存逻辑
|
|
457
|
+
const cached = this.storage.getItem('userName');
|
|
458
|
+
if (cached) return cached;
|
|
459
|
+
|
|
460
|
+
// API 调用
|
|
461
|
+
const user = await this.api.getUserInfo();
|
|
462
|
+
|
|
463
|
+
// 缓存
|
|
464
|
+
this.storage.setItem('userName', user.name);
|
|
465
|
+
|
|
466
|
+
return user.name;
|
|
193
467
|
}
|
|
194
|
-
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// ✅ UI 组件保持简单
|
|
471
|
+
function UserName() {
|
|
472
|
+
const userService = useIOC('UserServiceInterface');
|
|
473
|
+
const [name, setName] = useState('');
|
|
474
|
+
const [loading, setLoading] = useState(false);
|
|
475
|
+
|
|
476
|
+
useEffect(() => {
|
|
477
|
+
setLoading(true);
|
|
478
|
+
userService.getUserName()
|
|
479
|
+
.then(setName)
|
|
480
|
+
.finally(() => setLoading(false));
|
|
481
|
+
}, []);
|
|
482
|
+
|
|
483
|
+
if (loading) return <span>Loading...</span>;
|
|
484
|
+
return <span>{name}</span>;
|
|
485
|
+
}
|
|
195
486
|
```
|
|
196
487
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
488
|
+
**总结:即使组件很简单,也要分离,因为:**
|
|
489
|
+
|
|
490
|
+
- ✅ **现在简单,不代表将来简单** - 需求会变化
|
|
491
|
+
- ✅ **逻辑可以复用** - 其他组件可能也需要
|
|
492
|
+
- ✅ **易于测试** - 逻辑可以独立测试
|
|
493
|
+
- ✅ **职责清晰** - UI 只负责渲染,逻辑独立
|
|
494
|
+
- ✅ **易于维护** - 修改逻辑不影响 UI
|
|
495
|
+
|
|
496
|
+
---
|
|
497
|
+
|
|
498
|
+
## 🛠️ 项目中的实现
|
|
499
|
+
|
|
500
|
+
### 1. 文件结构
|
|
501
|
+
|
|
502
|
+
```
|
|
503
|
+
src/
|
|
504
|
+
├── base/
|
|
505
|
+
│ ├── port/ # 接口定义层
|
|
506
|
+
│ │ ├── UserServiceInterface.ts
|
|
507
|
+
│ │ ├── I18nServiceInterface.ts
|
|
508
|
+
│ │ └── RouteServiceInterface.ts
|
|
509
|
+
│ └── services/ # 服务实现层
|
|
510
|
+
│ ├── UserService.ts
|
|
511
|
+
│ ├── I18nService.ts
|
|
512
|
+
│ └── RouteService.ts
|
|
513
|
+
├── core/
|
|
514
|
+
│ ├── clientIoc/
|
|
515
|
+
│ │ ├── ClientIOC.ts # IOC 容器
|
|
516
|
+
│ │ └── ClientIOCRegister.ts # 注册器
|
|
517
|
+
│ └── globals.ts # 全局实例
|
|
518
|
+
├── uikit/
|
|
519
|
+
│ ├── hooks/
|
|
520
|
+
│ │ └── useIOC.ts # React Hook
|
|
521
|
+
│ └── contexts/
|
|
522
|
+
│ └── IOCContext.tsx # React Context
|
|
523
|
+
└── config/
|
|
524
|
+
└── IOCIdentifier.ts # 标识符定义
|
|
525
|
+
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
### 2. IOC 标识符定义
|
|
529
|
+
|
|
530
|
+
```typescript
|
|
531
|
+
// config/IOCIdentifier.ts
|
|
532
|
+
export interface IOCIdentifierMap {
|
|
533
|
+
AppConfig: AppConfig;
|
|
534
|
+
Logger: LoggerInterface;
|
|
535
|
+
LocalStorageEncrypt: SyncStorageInterface<string, string>;
|
|
536
|
+
UserServiceInterface: UserServiceInterface;
|
|
537
|
+
I18nServiceInterface: I18nServiceInterface;
|
|
538
|
+
RouteServiceInterface: RouteServiceInterface;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
export const IOCIdentifier = {
|
|
542
|
+
AppConfig: 'AppConfig',
|
|
543
|
+
Logger: 'Logger',
|
|
544
|
+
LocalStorageEncrypt: 'LocalStorageEncrypt',
|
|
545
|
+
UserServiceInterface: 'UserServiceInterface',
|
|
546
|
+
I18nServiceInterface: 'I18nServiceInterface',
|
|
547
|
+
RouteServiceInterface: 'RouteServiceInterface'
|
|
548
|
+
} as const;
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
### 3. 服务注册
|
|
552
|
+
|
|
553
|
+
```typescript
|
|
554
|
+
// src/core/clientIoc/ClientIOCRegister.ts
|
|
555
|
+
export class ClientIOCRegister implements IOCRegisterInterface {
|
|
556
|
+
constructor(protected options: IocRegisterOptions) {}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* 注册全局服务
|
|
560
|
+
*/
|
|
561
|
+
protected registerGlobals(ioc: IOCContainerInterface): void {
|
|
562
|
+
const { appConfig } = this.options;
|
|
563
|
+
const { dialogHandler, localStorageEncrypt, JSON, logger } = globals;
|
|
564
|
+
|
|
565
|
+
// ✅ 注册全局实例
|
|
566
|
+
ioc.bind(IOCIdentifier.JSONSerializer, JSON);
|
|
567
|
+
ioc.bind(IOCIdentifier.Logger, logger);
|
|
568
|
+
ioc.bind(IOCIdentifier.AppConfig, appConfig);
|
|
569
|
+
ioc.bind(IOCIdentifier.LocalStorageEncrypt, localStorageEncrypt);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* 注册业务服务
|
|
574
|
+
*/
|
|
575
|
+
protected registerImplement(ioc: IOCContainerInterface): void {
|
|
576
|
+
// ✅ 注册服务实现
|
|
577
|
+
ioc.bind(
|
|
578
|
+
IOCIdentifier.I18nServiceInterface,
|
|
579
|
+
new I18nService(this.options.pathname)
|
|
580
|
+
);
|
|
581
|
+
|
|
582
|
+
ioc.bind(IOCIdentifier.RouteServiceInterface, new RouteService(/* ... */));
|
|
583
|
+
|
|
584
|
+
// ✅ 服务可以依赖其他服务
|
|
585
|
+
ioc.bind(IOCIdentifier.UserServiceInterface, ioc.get(UserService));
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* 注册入口
|
|
590
|
+
*/
|
|
591
|
+
register(ioc: IOCContainerInterface): void {
|
|
592
|
+
this.registerGlobals(ioc);
|
|
593
|
+
this.registerImplement(ioc);
|
|
215
594
|
}
|
|
216
595
|
}
|
|
217
596
|
```
|
|
218
597
|
|
|
219
|
-
|
|
598
|
+
### 4. 创建 IOC 容器
|
|
220
599
|
|
|
221
|
-
|
|
600
|
+
```typescript
|
|
601
|
+
// src/core/clientIoc/ClientIOC.ts
|
|
602
|
+
import { createIOCFunction } from '@qlover/corekit-bridge';
|
|
603
|
+
import { InversifyContainer } from '@/base/cases/InversifyContainer';
|
|
604
|
+
import { ClientIOCRegister } from './ClientIOCRegister';
|
|
222
605
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
606
|
+
export const clientIOC = {
|
|
607
|
+
create(options: IocRegisterOptions) {
|
|
608
|
+
// 创建容器
|
|
609
|
+
const container = new InversifyContainer();
|
|
610
|
+
|
|
611
|
+
// 创建 IOC 函数
|
|
612
|
+
const IOC = createIOCFunction(container);
|
|
613
|
+
|
|
614
|
+
// 注册服务
|
|
615
|
+
const register = new ClientIOCRegister(options);
|
|
616
|
+
register.register(container, IOC);
|
|
617
|
+
|
|
618
|
+
return IOC;
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
### 5. Bootstrap 中初始化
|
|
624
|
+
|
|
625
|
+
```typescript
|
|
626
|
+
// src/core/bootstraps/BootstrapClient.ts
|
|
627
|
+
export class BootstrapClient {
|
|
628
|
+
static async main(args: BootstrapClientArgs) {
|
|
629
|
+
const { root, bootHref, ioc } = args;
|
|
630
|
+
|
|
631
|
+
// ✅ 创建 IOC 容器
|
|
632
|
+
const IOC = ioc.create({
|
|
633
|
+
pathname: bootHref,
|
|
634
|
+
appConfig: appConfig
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
// Bootstrap 中使用 IOC
|
|
638
|
+
const bootstrap = new Bootstrap({
|
|
639
|
+
root,
|
|
640
|
+
logger,
|
|
641
|
+
ioc: {
|
|
642
|
+
manager: IOC,
|
|
643
|
+
register: iocRegister
|
|
241
644
|
}
|
|
242
645
|
});
|
|
646
|
+
|
|
647
|
+
await bootstrap.initialize();
|
|
648
|
+
await bootstrap.start();
|
|
243
649
|
}
|
|
650
|
+
}
|
|
651
|
+
```
|
|
244
652
|
|
|
245
|
-
|
|
246
|
-
if (this.isAuthenticated()) {
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
653
|
+
---
|
|
249
654
|
|
|
250
|
-
|
|
251
|
-
if (!userToken) {
|
|
252
|
-
throw new AppError('NO_USER_TOKEN');
|
|
253
|
-
}
|
|
655
|
+
## 📝 使用方式
|
|
254
656
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
657
|
+
### 1. 定义接口(Port)
|
|
658
|
+
|
|
659
|
+
```typescript
|
|
660
|
+
// src/base/port/UserServiceInterface.ts
|
|
661
|
+
export interface UserServiceInterface {
|
|
662
|
+
getUser(): Promise<UserInfo>;
|
|
663
|
+
login(username: string, password: string): Promise<void>;
|
|
664
|
+
logout(): Promise<void>;
|
|
665
|
+
isAuthenticated(): boolean;
|
|
258
666
|
}
|
|
259
667
|
```
|
|
260
668
|
|
|
261
|
-
### 2.
|
|
669
|
+
### 2. 实现服务
|
|
670
|
+
|
|
671
|
+
```typescript
|
|
672
|
+
// src/base/services/UserService.ts
|
|
673
|
+
import { injectable, inject } from 'inversify';
|
|
674
|
+
|
|
675
|
+
@injectable()
|
|
676
|
+
export class UserService implements UserServiceInterface {
|
|
677
|
+
constructor(
|
|
678
|
+
@inject(UserApi) private api: UserApi,
|
|
679
|
+
@inject(IOCIdentifier.AppConfig) private config: AppConfig,
|
|
680
|
+
@inject(IOCIdentifier.LocalStorageEncrypt) private storage: Storage,
|
|
681
|
+
@inject(IOCIdentifier.RouteServiceInterface) private router: RouteService
|
|
682
|
+
) {}
|
|
683
|
+
|
|
684
|
+
async getUser(): Promise<UserInfo> {
|
|
685
|
+
const token = this.storage.getItem(this.config.userTokenStorageKey);
|
|
686
|
+
if (!token) throw new Error('No token');
|
|
687
|
+
|
|
688
|
+
return await this.api.getUserInfo(token);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
async login(username: string, password: string): Promise<void> {
|
|
692
|
+
const response = await this.api.login({ username, password });
|
|
693
|
+
this.storage.setItem(this.config.userTokenStorageKey, response.token);
|
|
694
|
+
}
|
|
262
695
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
696
|
+
async logout(): Promise<void> {
|
|
697
|
+
this.storage.removeItem(this.config.userTokenStorageKey);
|
|
698
|
+
await this.router.push('/login');
|
|
699
|
+
}
|
|
266
700
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
ioc
|
|
270
|
-
.get<UserApi>(UserApi)
|
|
271
|
-
.usePlugin(new FetchURLPlugin())
|
|
272
|
-
.usePlugin(IOC.get(IOCIdentifier.ApiMockPlugin))
|
|
273
|
-
.usePlugin(IOC.get(RequestLogger));
|
|
701
|
+
isAuthenticated(): boolean {
|
|
702
|
+
return !!this.storage.getItem(this.config.userTokenStorageKey);
|
|
274
703
|
}
|
|
275
704
|
}
|
|
276
705
|
```
|
|
277
706
|
|
|
278
|
-
### 3.
|
|
707
|
+
### 3. 在 UI 组件中使用
|
|
708
|
+
|
|
709
|
+
```typescript
|
|
710
|
+
// src/pages/UserProfile.tsx
|
|
711
|
+
import { useIOC } from '@/uikit/hooks/useIOC';
|
|
279
712
|
|
|
280
|
-
```tsx
|
|
281
|
-
// 在 React 组件中使用 IOC 服务
|
|
282
713
|
function UserProfile() {
|
|
283
|
-
|
|
284
|
-
const
|
|
714
|
+
// ✅ 从 IOC 容器获取服务
|
|
715
|
+
const userService = useIOC('UserServiceInterface');
|
|
716
|
+
const [user, setUser] = useState<UserInfo | null>(null);
|
|
717
|
+
|
|
718
|
+
useEffect(() => {
|
|
719
|
+
userService.getUser().then(setUser);
|
|
720
|
+
}, []);
|
|
285
721
|
|
|
722
|
+
const handleLogout = () => {
|
|
723
|
+
userService.logout();
|
|
724
|
+
};
|
|
725
|
+
|
|
726
|
+
// ✅ UI 只负责渲染
|
|
286
727
|
return (
|
|
287
728
|
<div>
|
|
288
|
-
<h1
|
|
289
|
-
<button onClick={
|
|
729
|
+
<h1>{user?.name}</h1>
|
|
730
|
+
<button onClick={handleLogout}>Logout</button>
|
|
290
731
|
</div>
|
|
291
732
|
);
|
|
292
733
|
}
|
|
293
734
|
```
|
|
294
735
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
### 1. 服务设计原则
|
|
736
|
+
### 4. 在服务中使用其他服务
|
|
298
737
|
|
|
299
|
-
```
|
|
300
|
-
//
|
|
738
|
+
```typescript
|
|
739
|
+
// src/base/services/ProfileService.ts
|
|
301
740
|
@injectable()
|
|
302
|
-
export class
|
|
741
|
+
export class ProfileService {
|
|
303
742
|
constructor(
|
|
304
|
-
|
|
305
|
-
@inject(IOCIdentifier.
|
|
743
|
+
// ✅ 服务可以依赖其他服务
|
|
744
|
+
@inject(IOCIdentifier.UserServiceInterface)
|
|
745
|
+
private userService: UserServiceInterface,
|
|
746
|
+
@inject(IOCIdentifier.I18nServiceInterface)
|
|
747
|
+
private i18n: I18nServiceInterface
|
|
306
748
|
) {}
|
|
307
|
-
|
|
308
|
-
|
|
749
|
+
|
|
750
|
+
async getUserProfile(): Promise<string> {
|
|
751
|
+
const user = await this.userService.getUser();
|
|
752
|
+
return this.i18n.t('profile.welcome', { name: user.name });
|
|
309
753
|
}
|
|
310
754
|
}
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
---
|
|
758
|
+
|
|
759
|
+
## 🧪 测试
|
|
760
|
+
|
|
761
|
+
### 核心优势:UI 和逻辑可以独立测试,也可以组合测试
|
|
762
|
+
|
|
763
|
+
#### 1. 独立测试逻辑(不需要 UI)
|
|
764
|
+
|
|
765
|
+
```typescript
|
|
766
|
+
// __tests__/src/base/services/UserService.test.ts
|
|
767
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
768
|
+
import { UserService } from '@/base/services/UserService';
|
|
769
|
+
|
|
770
|
+
describe('UserService (逻辑测试)', () => {
|
|
771
|
+
let userService: UserService;
|
|
772
|
+
let mockApi: any;
|
|
773
|
+
let mockStorage: any;
|
|
774
|
+
let mockRouter: any;
|
|
775
|
+
let mockConfig: any;
|
|
776
|
+
|
|
777
|
+
beforeEach(() => {
|
|
778
|
+
// ✅ 只需 mock 接口
|
|
779
|
+
mockApi = {
|
|
780
|
+
getUserInfo: vi.fn(),
|
|
781
|
+
login: vi.fn()
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
mockStorage = {
|
|
785
|
+
getItem: vi.fn(),
|
|
786
|
+
setItem: vi.fn(),
|
|
787
|
+
removeItem: vi.fn()
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
mockRouter = {
|
|
791
|
+
push: vi.fn()
|
|
792
|
+
};
|
|
793
|
+
|
|
794
|
+
mockConfig = {
|
|
795
|
+
userTokenStorageKey: '__test_token__'
|
|
796
|
+
};
|
|
797
|
+
|
|
798
|
+
// ✅ 创建服务
|
|
799
|
+
userService = new UserService(mockApi, mockConfig, mockStorage, mockRouter);
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
it('should get user when token exists', async () => {
|
|
803
|
+
// ✅ 设置 mock 返回值
|
|
804
|
+
mockStorage.getItem.mockReturnValue('test-token');
|
|
805
|
+
mockApi.getUserInfo.mockResolvedValue({ name: 'John' });
|
|
806
|
+
|
|
807
|
+
// ✅ 测试逻辑
|
|
808
|
+
const user = await userService.getUser();
|
|
809
|
+
|
|
810
|
+
// ✅ 验证结果
|
|
811
|
+
expect(user.name).toBe('John');
|
|
812
|
+
expect(mockStorage.getItem).toHaveBeenCalledWith('__test_token__');
|
|
813
|
+
expect(mockApi.getUserInfo).toHaveBeenCalledWith('test-token');
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
it('should throw error when no token', async () => {
|
|
817
|
+
// ✅ 测试错误场景
|
|
818
|
+
mockStorage.getItem.mockReturnValue(null);
|
|
819
|
+
|
|
820
|
+
await expect(userService.getUser()).rejects.toThrow('No token');
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
it('should login and save token', async () => {
|
|
824
|
+
// ✅ 测试登录逻辑
|
|
825
|
+
mockApi.login.mockResolvedValue({ token: 'new-token' });
|
|
826
|
+
|
|
827
|
+
await userService.login('user', 'pass');
|
|
828
|
+
|
|
829
|
+
expect(mockApi.login).toHaveBeenCalledWith({
|
|
830
|
+
username: 'user',
|
|
831
|
+
password: 'pass'
|
|
832
|
+
});
|
|
833
|
+
expect(mockStorage.setItem).toHaveBeenCalledWith(
|
|
834
|
+
'__test_token__',
|
|
835
|
+
'new-token'
|
|
836
|
+
);
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
it('should logout and clear token', async () => {
|
|
840
|
+
// ✅ 测试登出逻辑
|
|
841
|
+
await userService.logout();
|
|
842
|
+
|
|
843
|
+
expect(mockStorage.removeItem).toHaveBeenCalledWith('__test_token__');
|
|
844
|
+
expect(mockRouter.push).toHaveBeenCalledWith('/login');
|
|
845
|
+
});
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
// ✅✅✅ 优势:
|
|
849
|
+
// 1. 不需要渲染 UI
|
|
850
|
+
// 2. 测试运行快(纯逻辑)
|
|
851
|
+
// 3. 易于 mock(只需 mock 接口)
|
|
852
|
+
// 4. 可以测试所有边界情况
|
|
853
|
+
```
|
|
854
|
+
|
|
855
|
+
#### 2. 独立测试 UI(不需要真实逻辑)
|
|
856
|
+
|
|
857
|
+
```typescript
|
|
858
|
+
// __tests__/src/pages/UserProfile.test.tsx
|
|
859
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
860
|
+
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
|
|
861
|
+
import { UserProfile } from '@/pages/UserProfile';
|
|
862
|
+
import { IOCProvider } from '@/uikit/contexts/IOCContext';
|
|
863
|
+
|
|
864
|
+
describe('UserProfile (UI 测试)', () => {
|
|
865
|
+
it('should display user name', async () => {
|
|
866
|
+
// ✅ Mock 服务
|
|
867
|
+
const mockUserService = {
|
|
868
|
+
getUser: vi.fn().mockResolvedValue({ name: 'John Doe' }),
|
|
869
|
+
logout: vi.fn(),
|
|
870
|
+
isAuthenticated: vi.fn().mockReturnValue(true)
|
|
871
|
+
};
|
|
872
|
+
|
|
873
|
+
const mockIOC = (identifier: string) => {
|
|
874
|
+
if (identifier === 'UserServiceInterface') return mockUserService;
|
|
875
|
+
};
|
|
876
|
+
|
|
877
|
+
// ✅ 渲染组件
|
|
878
|
+
render(
|
|
879
|
+
<IOCProvider value={mockIOC}>
|
|
880
|
+
<UserProfile />
|
|
881
|
+
</IOCProvider>
|
|
882
|
+
);
|
|
883
|
+
|
|
884
|
+
// ✅ 验证 UI
|
|
885
|
+
await waitFor(() => {
|
|
886
|
+
expect(screen.getByText('John Doe')).toBeInTheDocument();
|
|
887
|
+
});
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
it('should call logout when button clicked', async () => {
|
|
891
|
+
const mockUserService = {
|
|
892
|
+
getUser: vi.fn().mockResolvedValue({ name: 'John' }),
|
|
893
|
+
logout: vi.fn(),
|
|
894
|
+
isAuthenticated: vi.fn().mockReturnValue(true)
|
|
895
|
+
};
|
|
896
|
+
|
|
897
|
+
const mockIOC = (identifier: string) => {
|
|
898
|
+
if (identifier === 'UserServiceInterface') return mockUserService;
|
|
899
|
+
};
|
|
900
|
+
|
|
901
|
+
render(
|
|
902
|
+
<IOCProvider value={mockIOC}>
|
|
903
|
+
<UserProfile />
|
|
904
|
+
</IOCProvider>
|
|
905
|
+
);
|
|
906
|
+
|
|
907
|
+
// ✅ 模拟用户操作
|
|
908
|
+
const logoutButton = screen.getByText('Logout');
|
|
909
|
+
fireEvent.click(logoutButton);
|
|
910
|
+
|
|
911
|
+
// ✅ 验证服务调用
|
|
912
|
+
expect(mockUserService.logout).toHaveBeenCalled();
|
|
913
|
+
});
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
// ✅✅✅ 优势:
|
|
917
|
+
// 1. 不需要真实的服务实现
|
|
918
|
+
// 2. 可以轻松模拟各种场景
|
|
919
|
+
// 3. UI 测试专注于 UI 逻辑
|
|
920
|
+
```
|
|
921
|
+
|
|
922
|
+
#### 3. 组合测试(UI + 逻辑)
|
|
923
|
+
|
|
924
|
+
```typescript
|
|
925
|
+
// __tests__/src/integration/UserFlow.test.tsx
|
|
926
|
+
import { describe, it, expect } from 'vitest';
|
|
927
|
+
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
|
|
928
|
+
import { UserProfile } from '@/pages/UserProfile';
|
|
929
|
+
import { UserService } from '@/base/services/UserService';
|
|
930
|
+
import { IOCProvider } from '@/uikit/contexts/IOCContext';
|
|
931
|
+
|
|
932
|
+
describe('User Flow (组合测试)', () => {
|
|
933
|
+
it('should complete user login flow', async () => {
|
|
934
|
+
// ✅ 使用真实的服务实现
|
|
935
|
+
const mockApi = {
|
|
936
|
+
getUserInfo: vi.fn().mockResolvedValue({ name: 'John' }),
|
|
937
|
+
login: vi.fn().mockResolvedValue({ token: 'test-token' })
|
|
938
|
+
};
|
|
939
|
+
|
|
940
|
+
const mockStorage = {
|
|
941
|
+
getItem: vi.fn(),
|
|
942
|
+
setItem: vi.fn(),
|
|
943
|
+
removeItem: vi.fn()
|
|
944
|
+
};
|
|
945
|
+
|
|
946
|
+
const mockRouter = { push: vi.fn() };
|
|
947
|
+
const mockConfig = { userTokenStorageKey: '__token__' };
|
|
948
|
+
|
|
949
|
+
// ✅ 创建真实服务
|
|
950
|
+
const userService = new UserService(
|
|
951
|
+
mockApi,
|
|
952
|
+
mockConfig,
|
|
953
|
+
mockStorage,
|
|
954
|
+
mockRouter
|
|
955
|
+
);
|
|
956
|
+
|
|
957
|
+
const mockIOC = (identifier: string) => {
|
|
958
|
+
if (identifier === 'UserServiceInterface') return userService;
|
|
959
|
+
};
|
|
960
|
+
|
|
961
|
+
// ✅ 渲染真实 UI
|
|
962
|
+
render(
|
|
963
|
+
<IOCProvider value={mockIOC}>
|
|
964
|
+
<UserProfile />
|
|
965
|
+
</IOCProvider>
|
|
966
|
+
);
|
|
967
|
+
|
|
968
|
+
// ✅ 测试完整流程
|
|
969
|
+
await waitFor(() => {
|
|
970
|
+
expect(screen.getByText('John')).toBeInTheDocument();
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
// ✅ 点击登出
|
|
974
|
+
fireEvent.click(screen.getByText('Logout'));
|
|
975
|
+
|
|
976
|
+
// ✅ 验证整个流程
|
|
977
|
+
expect(mockStorage.removeItem).toHaveBeenCalledWith('__token__');
|
|
978
|
+
expect(mockRouter.push).toHaveBeenCalledWith('/login');
|
|
979
|
+
});
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
// ✅✅✅ 优势:
|
|
983
|
+
// 1. 测试真实的用户流程
|
|
984
|
+
// 2. 可以发现 UI 和逻辑的集成问题
|
|
985
|
+
// 3. 更接近真实使用场景
|
|
986
|
+
```
|
|
987
|
+
|
|
988
|
+
### 测试策略总结
|
|
989
|
+
|
|
990
|
+
```
|
|
991
|
+
┌─────────────────────────────────────────┐
|
|
992
|
+
│ 测试金字塔 │
|
|
993
|
+
│ │
|
|
994
|
+
│ △ UI 测试 (少量) │
|
|
995
|
+
│ ╱ ╲ │
|
|
996
|
+
│ ╱ ╲ │
|
|
997
|
+
│ ╱ ╲ │
|
|
998
|
+
│ ╱───────╲ 组合测试 (适量) │
|
|
999
|
+
│ ╱ ╲ │
|
|
1000
|
+
│╱═══════════╲ 逻辑测试 (大量) │
|
|
1001
|
+
│ │
|
|
1002
|
+
│ 逻辑测试:快速、稳定、覆盖全面 │
|
|
1003
|
+
│ 组合测试:验证集成、发现问题 │
|
|
1004
|
+
│ UI 测试:验证用户交互 │
|
|
1005
|
+
└─────────────────────────────────────────┘
|
|
1006
|
+
```
|
|
1007
|
+
|
|
1008
|
+
**推荐测试比例:**
|
|
1009
|
+
|
|
1010
|
+
- 70% 逻辑测试(UserService.test.ts)
|
|
1011
|
+
- 20% 组合测试(UserFlow.test.tsx)
|
|
1012
|
+
- 10% UI 测试(UserProfile.test.tsx)
|
|
1013
|
+
|
|
1014
|
+
---
|
|
1015
|
+
|
|
1016
|
+
## 💎 最佳实践
|
|
1017
|
+
|
|
1018
|
+
### 1. ✅ 始终定义接口
|
|
1019
|
+
|
|
1020
|
+
```typescript
|
|
1021
|
+
// ✅ 好的做法:先定义接口
|
|
1022
|
+
export interface UserServiceInterface {
|
|
1023
|
+
getUser(): Promise<UserInfo>;
|
|
1024
|
+
logout(): Promise<void>;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// 然后实现
|
|
1028
|
+
@injectable()
|
|
1029
|
+
export class UserService implements UserServiceInterface {
|
|
1030
|
+
// ...
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// ❌ 不好的做法:直接写实现
|
|
1034
|
+
@injectable()
|
|
1035
|
+
export class UserService {
|
|
1036
|
+
// 没有接口,难以测试
|
|
1037
|
+
}
|
|
1038
|
+
```
|
|
1039
|
+
|
|
1040
|
+
### 2. ✅ UI 和逻辑完全分离
|
|
1041
|
+
|
|
1042
|
+
```typescript
|
|
1043
|
+
// ✅ 好的做法:UI 只负责渲染
|
|
1044
|
+
function UserProfile() {
|
|
1045
|
+
const userService = useIOC('UserServiceInterface');
|
|
1046
|
+
const [user, setUser] = useState(null);
|
|
1047
|
+
|
|
1048
|
+
useEffect(() => {
|
|
1049
|
+
userService.getUser().then(setUser);
|
|
1050
|
+
}, []);
|
|
1051
|
+
|
|
1052
|
+
return <div>{user?.name}</div>;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// ❌ 不好的做法:逻辑混在 UI 中
|
|
1056
|
+
function UserProfile() {
|
|
1057
|
+
const [user, setUser] = useState(null);
|
|
1058
|
+
|
|
1059
|
+
useEffect(() => {
|
|
1060
|
+
fetch('/api/user')
|
|
1061
|
+
.then(res => res.json())
|
|
1062
|
+
.then(setUser);
|
|
1063
|
+
}, []);
|
|
311
1064
|
|
|
312
|
-
|
|
1065
|
+
return <div>{user?.name}</div>;
|
|
1066
|
+
}
|
|
1067
|
+
```
|
|
1068
|
+
|
|
1069
|
+
### 3. ✅ 使用依赖注入
|
|
1070
|
+
|
|
1071
|
+
```typescript
|
|
1072
|
+
// ✅ 好的做法:通过构造函数注入
|
|
313
1073
|
@injectable()
|
|
314
|
-
export class
|
|
1074
|
+
export class UserService {
|
|
315
1075
|
constructor(
|
|
316
|
-
@inject(UserApi) private
|
|
317
|
-
@inject(
|
|
318
|
-
@inject(ThemeService) private themeService: ThemeService,
|
|
319
|
-
@inject(I18nService) private i18nService: I18nService
|
|
1076
|
+
@inject(UserApi) private api: UserApi,
|
|
1077
|
+
@inject(IOCIdentifier.AppConfig) private config: AppConfig
|
|
320
1078
|
) {}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// ❌ 不好的做法:直接创建依赖
|
|
1082
|
+
export class UserService {
|
|
1083
|
+
private api = new UserApi();
|
|
1084
|
+
private config = new AppConfig();
|
|
1085
|
+
}
|
|
1086
|
+
```
|
|
1087
|
+
|
|
1088
|
+
### 4. ✅ 服务单一职责
|
|
1089
|
+
|
|
1090
|
+
```typescript
|
|
1091
|
+
// ✅ 好的做法:每个服务只负责一件事
|
|
1092
|
+
@injectable()
|
|
1093
|
+
export class UserService {
|
|
1094
|
+
// 只负责用户相关逻辑
|
|
1095
|
+
async getUser() {
|
|
1096
|
+
/* ... */
|
|
1097
|
+
}
|
|
1098
|
+
async logout() {
|
|
1099
|
+
/* ... */
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
@injectable()
|
|
1104
|
+
export class ThemeService {
|
|
1105
|
+
// 只负责主题相关逻辑
|
|
1106
|
+
setTheme() {
|
|
1107
|
+
/* ... */
|
|
1108
|
+
}
|
|
1109
|
+
getTheme() {
|
|
1110
|
+
/* ... */
|
|
327
1111
|
}
|
|
328
1112
|
}
|
|
1113
|
+
|
|
1114
|
+
// ❌ 不好的做法:一个服务做多件事
|
|
1115
|
+
@injectable()
|
|
1116
|
+
export class ApplicationService {
|
|
1117
|
+
async getUser() {
|
|
1118
|
+
/* ... */
|
|
1119
|
+
}
|
|
1120
|
+
setTheme() {
|
|
1121
|
+
/* ... */
|
|
1122
|
+
}
|
|
1123
|
+
changeLanguage() {
|
|
1124
|
+
/* ... */
|
|
1125
|
+
}
|
|
1126
|
+
// 太多职责!
|
|
1127
|
+
}
|
|
329
1128
|
```
|
|
330
1129
|
|
|
331
|
-
###
|
|
1130
|
+
### 5. ✅ 依赖接口,不依赖实现
|
|
332
1131
|
|
|
333
|
-
```
|
|
334
|
-
// ✅
|
|
1132
|
+
```typescript
|
|
1133
|
+
// ✅ 好的做法
|
|
335
1134
|
@injectable()
|
|
336
1135
|
export class UserService {
|
|
337
1136
|
constructor(
|
|
338
|
-
@inject('UserApiInterface') private
|
|
1137
|
+
@inject('UserApiInterface') private api: UserApiInterface // 接口
|
|
339
1138
|
) {}
|
|
340
1139
|
}
|
|
341
1140
|
|
|
342
|
-
//
|
|
1141
|
+
// ❌ 不好的做法
|
|
343
1142
|
@injectable()
|
|
344
|
-
export class
|
|
1143
|
+
export class UserService {
|
|
345
1144
|
constructor(
|
|
346
|
-
@inject(
|
|
347
|
-
@inject(IOCIdentifier.AppConfig) private appConfig: AppConfig
|
|
1145
|
+
@inject(UserApi) private api: UserApi // 具体实现
|
|
348
1146
|
) {}
|
|
349
1147
|
}
|
|
350
1148
|
```
|
|
351
1149
|
|
|
352
|
-
###
|
|
1150
|
+
### 6. ✅ 即使简单也要分离
|
|
353
1151
|
|
|
354
|
-
```
|
|
1152
|
+
```typescript
|
|
1153
|
+
// ✅ 好的做法:即使很简单也分离
|
|
355
1154
|
@injectable()
|
|
356
|
-
export class
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
} catch (error) {
|
|
363
|
-
this.logger.error('操作失败:', error);
|
|
364
|
-
throw error;
|
|
365
|
-
}
|
|
1155
|
+
export class CounterService {
|
|
1156
|
+
private count = 0;
|
|
1157
|
+
|
|
1158
|
+
increment() {
|
|
1159
|
+
this.count++;
|
|
1160
|
+
return this.count;
|
|
366
1161
|
}
|
|
367
1162
|
}
|
|
368
|
-
```
|
|
369
|
-
|
|
370
|
-
## 调试和测试
|
|
371
1163
|
|
|
372
|
-
|
|
1164
|
+
function Counter() {
|
|
1165
|
+
const counterService = useIOC('CounterService');
|
|
1166
|
+
const [count, setCount] = useState(0);
|
|
373
1167
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
const isRegistered = container.isBound(UserService);
|
|
1168
|
+
const handleClick = () => {
|
|
1169
|
+
setCount(counterService.increment());
|
|
1170
|
+
};
|
|
378
1171
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
```
|
|
1172
|
+
return <button onClick={handleClick}>{count}</button>;
|
|
1173
|
+
}
|
|
382
1174
|
|
|
383
|
-
|
|
1175
|
+
// ❌ 不好的做法:简单逻辑也混在 UI 中
|
|
1176
|
+
function Counter() {
|
|
1177
|
+
const [count, setCount] = useState(0);
|
|
384
1178
|
|
|
385
|
-
|
|
386
|
-
|
|
1179
|
+
return (
|
|
1180
|
+
<button onClick={() => setCount(count + 1)}>
|
|
1181
|
+
{count}
|
|
1182
|
+
</button>
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1185
|
+
```
|
|
387
1186
|
|
|
388
|
-
|
|
389
|
-
let container: Container;
|
|
390
|
-
let userService: UserService;
|
|
1187
|
+
### 7. ✅ 编写全面的测试
|
|
391
1188
|
|
|
392
|
-
|
|
393
|
-
|
|
1189
|
+
```typescript
|
|
1190
|
+
// ✅ 好的做法:逻辑测试 + UI 测试 + 组合测试
|
|
1191
|
+
describe('UserService (逻辑)', () => {
|
|
1192
|
+
it('should get user', async () => {
|
|
1193
|
+
/* ... */
|
|
1194
|
+
});
|
|
1195
|
+
it('should handle error', async () => {
|
|
1196
|
+
/* ... */
|
|
1197
|
+
});
|
|
1198
|
+
});
|
|
394
1199
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
.toConstantValue(mockStorage);
|
|
1200
|
+
describe('UserProfile (UI)', () => {
|
|
1201
|
+
it('should display user', async () => {
|
|
1202
|
+
/* ... */
|
|
1203
|
+
});
|
|
1204
|
+
});
|
|
401
1205
|
|
|
402
|
-
|
|
1206
|
+
describe('User Flow (组合)', () => {
|
|
1207
|
+
it('should complete flow', async () => {
|
|
1208
|
+
/* ... */
|
|
403
1209
|
});
|
|
1210
|
+
});
|
|
404
1211
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
1212
|
+
// ❌ 不好的做法:只有 UI 测试
|
|
1213
|
+
describe('UserProfile', () => {
|
|
1214
|
+
it('should work', async () => {
|
|
1215
|
+
// 只测 UI,逻辑没有测试
|
|
408
1216
|
});
|
|
409
1217
|
});
|
|
410
1218
|
```
|
|
411
1219
|
|
|
412
|
-
|
|
1220
|
+
### 8. ✅ 使用类型安全的标识符
|
|
1221
|
+
|
|
1222
|
+
```typescript
|
|
1223
|
+
// ✅ 好的做法:类型安全的标识符
|
|
1224
|
+
const userService = useIOC('UserServiceInterface');
|
|
1225
|
+
// TypeScript 知道 userService 的类型
|
|
1226
|
+
|
|
1227
|
+
// ❌ 不好的做法:字符串字面量
|
|
1228
|
+
const userService = useIOC('UserService');
|
|
1229
|
+
// 容易拼写错误,没有类型检查
|
|
1230
|
+
```
|
|
1231
|
+
|
|
1232
|
+
---
|
|
1233
|
+
|
|
1234
|
+
## ❓ 常见问题
|
|
1235
|
+
|
|
1236
|
+
### Q1: IOC 会增加复杂度吗?
|
|
1237
|
+
|
|
1238
|
+
**A:** 短期看可能增加复杂度,但长期看大大降低复杂度:
|
|
1239
|
+
|
|
1240
|
+
**短期(小项目):**
|
|
1241
|
+
|
|
1242
|
+
- 需要定义接口
|
|
1243
|
+
- 需要注册服务
|
|
1244
|
+
- 需要学习 IOC 概念
|
|
1245
|
+
|
|
1246
|
+
**长期(项目变大):**
|
|
1247
|
+
|
|
1248
|
+
- ✅ 易于测试(节省大量测试时间)
|
|
1249
|
+
- ✅ 易于维护(清晰的依赖关系)
|
|
1250
|
+
- ✅ 易于扩展(添加新功能很简单)
|
|
1251
|
+
- ✅ 团队协作(职责清晰)
|
|
1252
|
+
|
|
1253
|
+
### Q2: 所有组件都要用 IOC 吗?
|
|
1254
|
+
|
|
1255
|
+
**A:** 不一定,但建议:
|
|
1256
|
+
|
|
1257
|
+
**需要使用 IOC 的场景:**
|
|
1258
|
+
|
|
1259
|
+
- ✅ 包含业务逻辑的组件
|
|
1260
|
+
- ✅ 需要调用 API 的组件
|
|
1261
|
+
- ✅ 需要访问 Storage 的组件
|
|
1262
|
+
- ✅ 需要测试的组件
|
|
1263
|
+
|
|
1264
|
+
**可以不用 IOC 的场景:**
|
|
1265
|
+
|
|
1266
|
+
- 纯展示组件(只接收 props)
|
|
1267
|
+
- 非常简单的 UI 组件(如 Button、Icon)
|
|
1268
|
+
|
|
1269
|
+
### Q3: 为什么不直接 import 服务?
|
|
1270
|
+
|
|
1271
|
+
**A:**
|
|
1272
|
+
|
|
1273
|
+
```typescript
|
|
1274
|
+
// ❌ 直接 import
|
|
1275
|
+
import { userService } from '@/services/UserService';
|
|
1276
|
+
|
|
1277
|
+
function UserProfile() {
|
|
1278
|
+
// 问题:
|
|
1279
|
+
// 1. userService 是单例,无法测试时替换
|
|
1280
|
+
// 2. userService 的依赖在模块加载时就创建了
|
|
1281
|
+
// 3. 难以 mock
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
// ✅ 使用 IOC
|
|
1285
|
+
function UserProfile() {
|
|
1286
|
+
const userService = useIOC('UserServiceInterface');
|
|
1287
|
+
|
|
1288
|
+
// 优势:
|
|
1289
|
+
// 1. 测试时可以提供 mock 实现
|
|
1290
|
+
// 2. 依赖由容器管理,按需创建
|
|
1291
|
+
// 3. 易于 mock
|
|
1292
|
+
}
|
|
1293
|
+
```
|
|
1294
|
+
|
|
1295
|
+
### Q4: 如何测试使用 IOC 的组件?
|
|
1296
|
+
|
|
1297
|
+
**A:** 提供 mock IOC:
|
|
1298
|
+
|
|
1299
|
+
```typescript
|
|
1300
|
+
const mockIOC = (identifier: string) => {
|
|
1301
|
+
if (identifier === 'UserServiceInterface') {
|
|
1302
|
+
return mockUserService;
|
|
1303
|
+
}
|
|
1304
|
+
// ... 其他服务
|
|
1305
|
+
};
|
|
1306
|
+
|
|
1307
|
+
render(
|
|
1308
|
+
<IOCProvider value={mockIOC}>
|
|
1309
|
+
<UserProfile />
|
|
1310
|
+
</IOCProvider>
|
|
1311
|
+
);
|
|
1312
|
+
```
|
|
1313
|
+
|
|
1314
|
+
### Q5: IOC 和 Context 有什么区别?
|
|
1315
|
+
|
|
1316
|
+
**A:**
|
|
1317
|
+
|
|
1318
|
+
| 特性 | React Context | IOC 容器 |
|
|
1319
|
+
| ------------ | ---------------- | ------------ |
|
|
1320
|
+
| **作用域** | React 组件树 | 全局 |
|
|
1321
|
+
| **依赖管理** | ❌ 无 | ✅ 有 |
|
|
1322
|
+
| **生命周期** | 组件生命周期 | 应用生命周期 |
|
|
1323
|
+
| **测试** | ⚠️ 需要 Provider | ✅ 易于 mock |
|
|
1324
|
+
| **类型安全** | ⚠️ 需要手动定义 | ✅ 自动推导 |
|
|
1325
|
+
|
|
1326
|
+
**建议:**
|
|
1327
|
+
|
|
1328
|
+
- 使用 IOC 管理服务(逻辑)
|
|
1329
|
+
- 使用 Context 管理 UI 状态
|
|
1330
|
+
|
|
1331
|
+
---
|
|
1332
|
+
|
|
1333
|
+
## 📚 相关文档
|
|
1334
|
+
|
|
1335
|
+
- [项目架构设计](./index.md) - 了解整体架构
|
|
1336
|
+
- [Bootstrap 启动器](./bootstrap.md) - IOC 在 Bootstrap 中的应用
|
|
1337
|
+
- [环境变量管理](./env.md) - AppConfig 的注入
|
|
1338
|
+
- [Store 状态管理](./store.md) - 应用层如何通知 UI 层(IOC + Store)
|
|
1339
|
+
- [测试指南](./test-guide.md) - 详细的测试策略
|
|
1340
|
+
|
|
1341
|
+
---
|
|
1342
|
+
|
|
1343
|
+
## 🎉 总结
|
|
1344
|
+
|
|
1345
|
+
IOC 容器的核心价值:
|
|
1346
|
+
|
|
1347
|
+
1. **UI 分离** 🎨 - UI 就是 UI,逻辑就是逻辑
|
|
1348
|
+
2. **可测试性** 🧪 - 逻辑可以独立测试,UI 可以独立测试,也可以组合测试
|
|
1349
|
+
3. **必须接口** 🔌 - 即使只有一个实现,也需要接口(为了测试)
|
|
1350
|
+
4. **全面分离** 🏗️ - 即使简单组件,也要分离(为了未来)
|
|
1351
|
+
5. **依赖管理** 📦 - 容器统一管理所有依赖
|
|
1352
|
+
6. **解耦合** 🔗 - 组件不依赖具体实现
|
|
1353
|
+
7. **易维护** 🛠️ - 清晰的依赖关系
|
|
1354
|
+
8. **易扩展** 🚀 - 轻松添加新功能
|
|
1355
|
+
|
|
1356
|
+
**记住两个核心原则:**
|
|
413
1357
|
|
|
414
|
-
|
|
1358
|
+
1. **UI 就是 UI,逻辑就是逻辑,两者必须分离!**
|
|
1359
|
+
2. **即使只有一个实现,也需要接口;即使组件很简单,也要分离!**
|
|
415
1360
|
|
|
416
|
-
|
|
417
|
-
2. **类型安全**:通过 TypeScript 提供编译时类型检查
|
|
418
|
-
3. **可测试性**:便于进行单元测试和模拟
|
|
419
|
-
4. **可维护性**:清晰的依赖关系,易于理解和修改
|
|
420
|
-
5. **可扩展性**:轻松添加新的服务和依赖
|
|
1361
|
+
---
|
|
421
1362
|
|
|
422
|
-
|
|
1363
|
+
**问题反馈:**
|
|
1364
|
+
如果你对 IOC 容器有任何疑问或建议,请在团队频道中讨论或提交 Issue。
|