@qlover/create-app 0.7.15 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/dist/configs/_common/.gitignore.template +6 -0
- package/dist/configs/_common/.prettierignore +17 -5
- package/dist/configs/_common/.vscode/settings.json +6 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/templates/next-app/.env.template +1 -1
- package/dist/templates/next-app/README.en.md +0 -1
- package/dist/templates/next-app/README.md +0 -1
- package/dist/templates/next-app/config/Identifier/api.ts +5 -5
- package/dist/templates/next-app/config/Identifier/common/admint.table.ts +69 -0
- package/dist/templates/next-app/config/Identifier/common/common.ts +76 -0
- package/dist/templates/next-app/config/Identifier/common/index.ts +3 -0
- package/dist/templates/next-app/config/Identifier/{validator.ts → common/validators.ts} +5 -5
- package/dist/templates/next-app/config/Identifier/index.ts +2 -12
- package/dist/templates/next-app/config/Identifier/pages/index.ts +6 -0
- package/dist/templates/next-app/config/Identifier/pages/page.admin.home.ts +27 -0
- package/dist/templates/next-app/config/Identifier/pages/page.admin.locales.ts +266 -0
- package/dist/templates/next-app/config/Identifier/pages/page.admin.user.ts +293 -0
- package/dist/templates/{react-app/config/Identifier → next-app/config/Identifier/pages}/page.home.ts +15 -22
- package/dist/templates/next-app/config/Identifier/{page.login.ts → pages/page.login.ts} +28 -34
- package/dist/templates/next-app/config/Identifier/{page.register.ts → pages/page.register.ts} +30 -29
- package/dist/templates/next-app/config/adminNavs.ts +19 -0
- package/dist/templates/next-app/config/common.ts +22 -13
- package/dist/templates/next-app/config/i18n/HomeI18n.ts +5 -5
- package/dist/templates/next-app/config/i18n/admin18n.ts +61 -19
- package/dist/templates/next-app/config/i18n/i18nConfig.ts +2 -0
- package/dist/templates/next-app/config/i18n/i18nKeyScheam.ts +36 -0
- package/dist/templates/next-app/config/i18n/loginI18n.ts +22 -22
- package/dist/templates/next-app/config/i18n/register18n.ts +23 -24
- package/dist/templates/next-app/docs/en/index.md +0 -1
- package/dist/templates/next-app/docs/en/project-structure.md +0 -1
- package/dist/templates/next-app/docs/zh/index.md +0 -1
- package/dist/templates/next-app/docs/zh/project-structure.md +0 -1
- package/dist/templates/next-app/make/generateLocales.ts +19 -12
- package/dist/templates/next-app/migrations/schema/LocalesSchema.ts +15 -0
- package/dist/templates/next-app/migrations/sql/1694244000000.sql +11 -0
- package/dist/templates/next-app/package.json +7 -3
- package/dist/templates/next-app/public/locales/en.json +172 -207
- package/dist/templates/next-app/public/locales/zh.json +172 -207
- package/dist/templates/next-app/src/app/[locale]/admin/locales/page.tsx +153 -0
- package/dist/templates/next-app/src/app/[locale]/admin/users/page.tsx +48 -50
- package/dist/templates/next-app/src/app/[locale]/login/LoginForm.tsx +2 -2
- package/dist/templates/next-app/src/app/api/admin/locales/create/route.ts +34 -0
- package/dist/templates/next-app/src/app/api/admin/locales/import/route.ts +40 -0
- package/dist/templates/next-app/src/app/api/admin/locales/route.ts +42 -0
- package/dist/templates/next-app/src/app/api/admin/locales/update/route.ts +32 -0
- package/dist/templates/next-app/src/app/api/locales/json/route.ts +44 -0
- package/dist/templates/next-app/src/base/cases/AdminPageManager.ts +1 -13
- package/dist/templates/next-app/src/base/cases/Datetime.ts +18 -0
- package/dist/templates/next-app/src/base/cases/DialogErrorPlugin.ts +12 -6
- package/dist/templates/next-app/src/base/cases/ResourceState.ts +17 -0
- package/dist/templates/next-app/src/base/cases/TranslateI18nInterface.ts +25 -0
- package/dist/templates/next-app/src/base/cases/ZodColumnBuilder.ts +200 -0
- package/dist/templates/next-app/src/base/port/ZodBuilderInterface.ts +8 -0
- package/dist/templates/next-app/src/base/services/AdminLocalesService.ts +20 -0
- package/dist/templates/next-app/src/base/services/AdminPageEvent.ts +26 -0
- package/dist/templates/next-app/src/base/services/AdminPageScheduler.ts +42 -0
- package/dist/templates/next-app/src/base/services/ResourceService.ts +122 -0
- package/dist/templates/next-app/src/base/services/adminApi/AdminLocalesApi.ts +104 -0
- package/dist/templates/next-app/src/base/services/adminApi/AdminUserApi.ts +38 -5
- package/dist/templates/next-app/src/base/services/appApi/AppApiPlugin.ts +1 -1
- package/dist/templates/next-app/src/i18n/request.ts +30 -1
- package/dist/templates/next-app/src/server/PageParams.ts +2 -10
- package/dist/templates/next-app/src/server/port/DBBridgeInterface.ts +5 -0
- package/dist/templates/next-app/src/server/port/DBTableInterface.ts +2 -0
- package/dist/templates/next-app/src/server/port/LocalesRepositoryInterface.ts +43 -0
- package/dist/templates/next-app/src/server/repositorys/LocalesRepository.ts +197 -0
- package/dist/templates/next-app/src/server/services/ApiLocaleService.ts +122 -0
- package/dist/templates/next-app/src/server/sqlBridges/SupabaseBridge.ts +60 -11
- package/dist/templates/next-app/src/server/validators/ExtendedExecutorError.ts +6 -0
- package/dist/templates/next-app/src/server/validators/LocalesValidator.ts +131 -0
- package/dist/templates/next-app/src/server/validators/LoginValidator.ts +2 -5
- package/dist/templates/next-app/src/server/validators/PaginationValidator.ts +32 -16
- package/dist/templates/next-app/src/styles/css/antd-themes/pagination/_default.css +2 -1
- package/dist/templates/next-app/src/styles/css/antd-themes/pagination/dark.css +28 -29
- package/dist/templates/next-app/src/styles/css/antd-themes/pagination/pink.css +2 -1
- package/dist/templates/next-app/src/uikit/components/AdminLayout.tsx +17 -3
- package/dist/templates/next-app/src/uikit/components/BaseHeader.tsx +5 -4
- package/dist/templates/next-app/src/uikit/components/BaseLayout.tsx +5 -4
- package/dist/templates/next-app/src/uikit/components/BootstrapsProvider.tsx +3 -2
- package/dist/templates/next-app/src/uikit/components/ComboProvider.tsx +1 -1
- package/dist/templates/next-app/src/uikit/components/EditableCell.tsx +118 -0
- package/dist/templates/next-app/src/uikit/components/LogoutButton.tsx +5 -6
- package/dist/templates/next-app/src/uikit/components/ThemeSwitcher.tsx +1 -1
- package/dist/templates/next-app/src/uikit/components/With.tsx +2 -2
- package/dist/templates/next-app/src/uikit/components/localesImportButton/LocalesImportButton.tsx +62 -0
- package/dist/templates/next-app/src/uikit/components/localesImportButton/LocalesImportEvent.ts +28 -0
- package/dist/templates/next-app/src/uikit/components/localesImportButton/import.module.css +6 -0
- package/dist/templates/next-app/src/uikit/hook/useI18nInterface.ts +8 -14
- package/dist/templates/next-app/src/uikit/hook/useWarnTranslations.ts +25 -0
- package/dist/templates/react-app/.prettierignore +17 -0
- package/dist/templates/react-app/README.en.md +71 -54
- package/dist/templates/react-app/README.md +35 -18
- package/dist/templates/react-app/__tests__/__mocks__/BootstrapTest.ts +14 -0
- package/dist/templates/react-app/__tests__/__mocks__/MockAppConfit.ts +1 -1
- package/dist/templates/react-app/__tests__/__mocks__/MockDialogHandler.ts +2 -2
- package/dist/templates/react-app/__tests__/__mocks__/MockLogger.ts +1 -1
- package/dist/templates/react-app/__tests__/__mocks__/components/TestApp.tsx +45 -0
- package/dist/templates/react-app/__tests__/__mocks__/components/TestBootstrapsProvider.tsx +34 -0
- package/dist/templates/react-app/__tests__/__mocks__/components/TestRouter.tsx +46 -0
- package/dist/templates/react-app/__tests__/__mocks__/components/index.ts +12 -0
- package/dist/templates/react-app/__tests__/__mocks__/createMockGlobals.ts +1 -2
- package/dist/templates/react-app/__tests__/__mocks__/testIOC/TestIOC.ts +51 -0
- package/dist/templates/react-app/__tests__/__mocks__/testIOC/TestIOCRegister.ts +69 -0
- package/dist/templates/react-app/__tests__/setup/index.ts +1 -51
- package/dist/templates/react-app/__tests__/setup/setupGlobal.ts +51 -0
- package/dist/templates/react-app/__tests__/src/App.structure.test.tsx +115 -0
- package/dist/templates/react-app/__tests__/src/base/cases/AppConfig.test.ts +2 -2
- package/dist/templates/react-app/__tests__/src/base/cases/AppError.test.ts +1 -1
- package/dist/templates/react-app/__tests__/src/base/cases/DialogHandler.test.ts +3 -5
- package/dist/templates/react-app/__tests__/src/base/cases/I18nKeyErrorPlugin.test.ts +13 -2
- package/dist/templates/react-app/__tests__/src/base/cases/InversifyContainer.test.ts +1 -1
- package/dist/templates/react-app/__tests__/src/base/cases/PublicAssetsPath.test.ts +1 -1
- package/dist/templates/react-app/__tests__/src/base/cases/RequestLogger.test.ts +5 -5
- package/dist/templates/react-app/__tests__/src/base/cases/RequestStatusCatcher.test.ts +1 -2
- package/dist/templates/react-app/__tests__/src/base/cases/RouterLoader.test.ts +25 -15
- package/dist/templates/react-app/__tests__/src/base/services/I18nService.test.ts +29 -15
- package/dist/templates/react-app/__tests__/src/core/IOC.test.ts +19 -9
- package/dist/templates/react-app/__tests__/src/core/bootstraps/BootstrapClient.test.ts +153 -0
- package/dist/templates/react-app/__tests__/src/core/bootstraps/BootstrapsApp.test.ts +9 -7
- package/dist/templates/react-app/__tests__/src/main.integration.test.tsx +4 -5
- package/dist/templates/react-app/__tests__/src/main.test.tsx +4 -4
- package/dist/templates/react-app/__tests__/src/uikit/components/BaseHeader.test.tsx +68 -59
- package/dist/templates/react-app/config/IOCIdentifier.ts +8 -8
- package/dist/templates/react-app/config/Identifier/{common.error.ts → common/common.error.ts} +5 -5
- package/dist/templates/react-app/config/Identifier/{common.ts → common/common.ts} +9 -9
- package/dist/templates/react-app/config/Identifier/common/index.ts +2 -0
- package/dist/templates/react-app/config/Identifier/index.ts +1 -9
- package/dist/templates/react-app/config/Identifier/pages/index.ts +8 -0
- package/dist/templates/react-app/config/Identifier/{page.about.ts → pages/page.about.ts} +34 -26
- package/dist/templates/react-app/config/Identifier/{page.executor.ts → pages/page.executor.ts} +47 -39
- package/dist/templates/{next-app/config/Identifier → react-app/config/Identifier/pages}/page.home.ts +24 -23
- package/dist/templates/react-app/config/Identifier/pages/page.identifiter.ts +102 -0
- package/dist/templates/react-app/config/Identifier/{page.jsonStorage.ts → pages/page.jsonStorage.ts} +18 -11
- package/dist/templates/react-app/config/Identifier/{page.login.ts → pages/page.login.ts} +37 -27
- package/dist/templates/react-app/config/Identifier/{page.register.ts → pages/page.register.ts} +37 -25
- package/dist/templates/react-app/config/Identifier/{page.request.ts → pages/page.request.ts} +34 -44
- package/dist/templates/react-app/config/app.router.ts +66 -69
- package/dist/templates/react-app/config/i18n/PageI18nInterface.ts +51 -0
- package/dist/templates/react-app/config/i18n/aboutI18n.ts +42 -0
- package/dist/templates/react-app/config/i18n/executorI18n.ts +51 -0
- package/dist/templates/react-app/config/i18n/homeI18n.ts +24 -0
- package/dist/templates/react-app/config/i18n/i18nConfig.ts +30 -0
- package/dist/templates/react-app/config/i18n/identifiter18n.ts +30 -0
- package/dist/templates/react-app/config/i18n/jsonStorage18n.ts +27 -0
- package/dist/templates/react-app/config/i18n/login18n.ts +42 -0
- package/dist/templates/react-app/config/i18n/notFoundI18n.ts +34 -0
- package/dist/templates/react-app/config/i18n/register18n.ts +40 -0
- package/dist/templates/react-app/config/i18n/request18n.ts +41 -0
- package/dist/templates/react-app/config/theme.ts +14 -4
- package/dist/templates/react-app/docs/en/bootstrap.md +1670 -341
- package/dist/templates/react-app/docs/en/development-guide.md +1021 -345
- package/dist/templates/react-app/docs/en/env.md +1132 -278
- package/dist/templates/react-app/docs/en/i18n.md +858 -147
- package/dist/templates/react-app/docs/en/index.md +733 -104
- package/dist/templates/react-app/docs/en/ioc.md +1228 -287
- package/dist/templates/react-app/docs/en/playwright/e2e-tests.md +321 -0
- package/dist/templates/react-app/docs/en/playwright/index.md +19 -0
- package/dist/templates/react-app/docs/en/playwright/installation-summary.md +332 -0
- package/dist/templates/react-app/docs/en/playwright/overview.md +222 -0
- package/dist/templates/react-app/docs/en/playwright/quickstart.md +325 -0
- package/dist/templates/react-app/docs/en/playwright/reorganization-notes.md +340 -0
- package/dist/templates/react-app/docs/en/playwright/setup-complete.md +290 -0
- package/dist/templates/react-app/docs/en/playwright/testing-guide.md +565 -0
- package/dist/templates/react-app/docs/en/store.md +1194 -184
- package/dist/templates/react-app/docs/en/why-no-globals.md +797 -0
- package/dist/templates/react-app/docs/zh/bootstrap.md +1670 -341
- package/dist/templates/react-app/docs/zh/development-guide.md +1021 -345
- package/dist/templates/react-app/docs/zh/env.md +1132 -275
- package/dist/templates/react-app/docs/zh/i18n.md +858 -147
- package/dist/templates/react-app/docs/zh/index.md +717 -104
- package/dist/templates/react-app/docs/zh/ioc.md +1229 -287
- package/dist/templates/react-app/docs/zh/playwright/e2e-tests.md +321 -0
- package/dist/templates/react-app/docs/zh/playwright/index.md +19 -0
- package/dist/templates/react-app/docs/zh/playwright/installation-summary.md +332 -0
- package/dist/templates/react-app/docs/zh/playwright/overview.md +222 -0
- package/dist/templates/react-app/docs/zh/playwright/quickstart.md +325 -0
- package/dist/templates/react-app/docs/zh/playwright/reorganization-notes.md +340 -0
- package/dist/templates/react-app/docs/zh/playwright/setup-complete.md +290 -0
- package/dist/templates/react-app/docs/zh/playwright/testing-guide.md +565 -0
- package/dist/templates/react-app/docs/zh/store.md +1192 -184
- package/dist/templates/react-app/docs/zh/why-no-globals.md +797 -0
- package/dist/templates/react-app/e2e/App.spec.ts +319 -0
- package/dist/templates/react-app/e2e/fixtures/base.fixture.ts +40 -0
- package/dist/templates/react-app/e2e/main.spec.ts +20 -0
- package/dist/templates/react-app/e2e/utils/test-helpers.ts +19 -0
- package/dist/templates/react-app/eslint.config.mjs +247 -0
- package/dist/templates/react-app/makes/eslint-utils.mjs +195 -0
- package/dist/templates/react-app/makes/generateTs2LocalesOptions.ts +26 -0
- package/dist/templates/react-app/package.json +31 -3
- package/dist/templates/react-app/playwright.config.ts +79 -0
- package/dist/templates/react-app/public/locales/en/common.json +190 -179
- package/dist/templates/react-app/public/locales/zh/common.json +190 -179
- package/dist/templates/react-app/src/App.tsx +15 -42
- package/dist/templates/react-app/src/base/apis/AiApi.ts +5 -5
- package/dist/templates/react-app/src/base/apis/feApi/FeApi.ts +1 -1
- package/dist/templates/react-app/src/base/apis/feApi/FeApiAdapter.ts +1 -1
- package/dist/templates/react-app/src/base/apis/feApi/FeApiBootstarp.ts +8 -8
- package/dist/templates/react-app/src/base/apis/feApi/FeApiType.ts +1 -1
- package/dist/templates/react-app/src/base/apis/userApi/UserApi.ts +6 -6
- package/dist/templates/react-app/src/base/apis/userApi/UserApiAdapter.ts +1 -1
- package/dist/templates/react-app/src/base/apis/userApi/UserApiBootstarp.ts +12 -14
- package/dist/templates/react-app/src/base/apis/userApi/UserApiType.ts +1 -1
- package/dist/templates/react-app/src/base/cases/DialogHandler.ts +5 -2
- package/dist/templates/react-app/src/base/cases/I18nKeyErrorPlugin.ts +3 -3
- package/dist/templates/react-app/src/base/cases/InversifyContainer.ts +3 -3
- package/dist/templates/react-app/src/base/cases/RequestLanguages.ts +2 -2
- package/dist/templates/react-app/src/base/cases/RequestLogger.ts +4 -4
- package/dist/templates/react-app/src/base/cases/RequestStatusCatcher.ts +1 -1
- package/dist/templates/react-app/src/base/cases/ResourceState.ts +23 -0
- package/dist/templates/react-app/src/base/cases/RouterLoader.ts +4 -4
- package/dist/templates/react-app/src/base/cases/TranslateI18nInterface.ts +26 -0
- package/dist/templates/react-app/src/base/port/ExecutorPageBridgeInterface.ts +2 -3
- package/dist/templates/react-app/src/base/port/I18nServiceInterface.ts +1 -1
- package/dist/templates/react-app/src/base/port/IOCInterface.ts +36 -0
- package/dist/templates/react-app/src/base/port/JSONStoragePageBridgeInterface.ts +2 -1
- package/dist/templates/react-app/src/base/port/ProcesserExecutorInterface.ts +1 -1
- package/dist/templates/react-app/src/base/port/RequestPageBridgeInterface.ts +2 -2
- package/dist/templates/react-app/src/base/port/RouteServiceInterface.ts +9 -5
- package/dist/templates/react-app/src/base/port/UserServiceInterface.ts +1 -1
- package/dist/templates/react-app/src/base/services/I18nService.ts +29 -29
- package/dist/templates/react-app/src/base/services/IdentifierService.ts +143 -0
- package/dist/templates/react-app/src/base/services/ProcesserExecutor.ts +3 -3
- package/dist/templates/react-app/src/base/services/RouteService.ts +27 -8
- package/dist/templates/react-app/src/base/services/UserService.ts +8 -8
- package/dist/templates/react-app/src/base/types/Page.ts +14 -2
- package/dist/templates/react-app/src/base/types/global.d.ts +1 -1
- package/dist/templates/react-app/src/core/IOC.ts +5 -46
- package/dist/templates/react-app/src/core/bootstraps/{BootstrapApp.ts → BootstrapClient.ts} +44 -17
- package/dist/templates/react-app/src/core/bootstraps/BootstrapsRegistry.ts +14 -7
- package/dist/templates/react-app/src/core/bootstraps/IocIdentifierTest.ts +1 -1
- package/dist/templates/react-app/src/core/bootstraps/PrintBootstrap.ts +1 -1
- package/dist/templates/react-app/src/core/clientIoc/ClientIOC.ts +40 -0
- package/dist/templates/react-app/src/core/{IocRegisterImpl.ts → clientIoc/ClientIOCRegister.ts} +35 -24
- package/dist/templates/react-app/src/core/globals.ts +9 -9
- package/dist/templates/react-app/src/main.tsx +4 -4
- package/dist/templates/react-app/src/pages/404.tsx +6 -3
- package/dist/templates/react-app/src/pages/500.tsx +5 -2
- package/dist/templates/react-app/src/pages/NoRouteFound.tsx +5 -0
- package/dist/templates/react-app/src/pages/auth/Layout.tsx +9 -6
- package/dist/templates/react-app/src/pages/auth/LoginPage.tsx +46 -56
- package/dist/templates/react-app/src/pages/auth/RegisterPage.tsx +46 -58
- package/dist/templates/react-app/src/pages/base/AboutPage.tsx +35 -40
- package/dist/templates/react-app/src/pages/base/ExecutorPage.tsx +51 -51
- package/dist/templates/react-app/src/pages/base/HomePage.tsx +14 -15
- package/dist/templates/react-app/src/pages/base/IdentifierPage.tsx +70 -11
- package/dist/templates/react-app/src/pages/base/JSONStoragePage.tsx +24 -25
- package/dist/templates/react-app/src/pages/base/Layout.tsx +2 -2
- package/dist/templates/react-app/src/pages/base/RedirectPathname.tsx +3 -2
- package/dist/templates/react-app/src/pages/base/RequestPage.tsx +41 -59
- package/dist/templates/react-app/src/styles/css/antd-themes/{_default.css → _common/_default.css} +85 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/{dark.css → _common/dark.css} +99 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/_common/index.css +3 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/{pink.css → _common/pink.css} +86 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/index.css +4 -3
- package/dist/templates/react-app/src/styles/css/antd-themes/menu/_default.css +108 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/menu/dark.css +67 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/menu/index.css +3 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/menu/pink.css +67 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/pagination/_default.css +34 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/pagination/dark.css +31 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/pagination/index.css +3 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/pagination/pink.css +36 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/table/_default.css +44 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/table/dark.css +43 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/table/index.css +3 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/table/pink.css +43 -0
- package/dist/templates/react-app/src/styles/css/page.css +4 -3
- package/dist/templates/react-app/src/styles/css/themes/_default.css +1 -0
- package/dist/templates/react-app/src/styles/css/themes/dark.css +1 -0
- package/dist/templates/react-app/src/styles/css/themes/pink.css +1 -0
- package/dist/templates/react-app/src/styles/css/zIndex.css +1 -1
- package/dist/templates/react-app/src/uikit/bridges/ExecutorPageBridge.ts +3 -3
- package/dist/templates/react-app/src/uikit/bridges/JSONStoragePageBridge.ts +2 -2
- package/dist/templates/react-app/src/uikit/bridges/NavigateBridge.ts +1 -1
- package/dist/templates/react-app/src/uikit/bridges/RequestPageBridge.ts +3 -3
- package/dist/templates/react-app/src/uikit/components/AppRouterProvider.tsx +35 -0
- package/dist/templates/react-app/src/uikit/components/BaseHeader.tsx +15 -11
- package/dist/templates/react-app/src/uikit/components/BaseRouteProvider.tsx +14 -11
- package/dist/templates/react-app/src/uikit/components/BaseRouteSeo.tsx +18 -0
- package/dist/templates/react-app/src/uikit/components/BootstrapsProvider.tsx +13 -0
- package/dist/templates/react-app/src/uikit/components/ClientSeo.tsx +62 -0
- package/dist/templates/react-app/src/uikit/components/ComboProvider.tsx +38 -0
- package/dist/templates/react-app/src/uikit/components/LanguageSwitcher.tsx +48 -27
- package/dist/templates/react-app/src/uikit/components/Loading.tsx +4 -2
- package/dist/templates/react-app/src/uikit/components/LocaleLink.tsx +4 -5
- package/dist/templates/react-app/src/uikit/components/LogoutButton.tsx +34 -11
- package/dist/templates/react-app/src/uikit/components/ProcessExecutorProvider.tsx +9 -5
- package/dist/templates/react-app/src/uikit/components/RouterRenderComponent.tsx +6 -3
- package/dist/templates/react-app/src/uikit/components/ThemeSwitcher.tsx +97 -40
- package/dist/templates/react-app/src/uikit/components/UserAuthProvider.tsx +5 -5
- package/dist/templates/react-app/src/uikit/components/With.tsx +17 -0
- package/dist/templates/react-app/src/uikit/contexts/BaseRouteContext.ts +17 -11
- package/dist/templates/react-app/src/uikit/contexts/IOCContext.ts +13 -0
- package/dist/templates/react-app/src/uikit/hooks/useAppTranslation.ts +26 -0
- package/dist/templates/react-app/src/uikit/hooks/useI18nGuard.ts +8 -11
- package/dist/templates/react-app/src/uikit/hooks/useI18nInterface.ts +25 -0
- package/dist/templates/react-app/src/uikit/hooks/useIOC.ts +35 -0
- package/dist/templates/react-app/src/uikit/hooks/useNavigateBridge.ts +3 -3
- package/dist/templates/react-app/src/uikit/hooks/useStrictEffect.ts +0 -1
- package/dist/templates/react-app/tsconfig.e2e.json +21 -0
- package/dist/templates/react-app/tsconfig.json +8 -1
- package/dist/templates/react-app/tsconfig.node.json +1 -1
- package/dist/templates/react-app/tsconfig.test.json +3 -1
- package/dist/templates/react-app/vite.config.ts +50 -34
- package/package.json +2 -1
- package/dist/configs/react-app/eslint.config.js +0 -94
- package/dist/templates/next-app/config/Identifier/common.error.ts +0 -41
- package/dist/templates/next-app/config/Identifier/common.ts +0 -69
- package/dist/templates/next-app/config/Identifier/page.about.ts +0 -181
- package/dist/templates/next-app/config/Identifier/page.admin.ts +0 -48
- package/dist/templates/next-app/config/Identifier/page.executor.ts +0 -272
- package/dist/templates/next-app/config/Identifier/page.identifiter.ts +0 -39
- package/dist/templates/next-app/config/Identifier/page.jsonStorage.ts +0 -72
- package/dist/templates/next-app/config/Identifier/page.request.ts +0 -182
- package/dist/templates/next-app/src/base/cases/ChatAction.ts +0 -21
- package/dist/templates/next-app/src/base/cases/FocusBarAction.ts +0 -36
- package/dist/templates/next-app/src/base/cases/RequestState.ts +0 -20
- package/dist/templates/next-app/src/base/port/AdminPageInterface.ts +0 -85
- package/dist/templates/next-app/src/base/port/AsyncStateInterface.ts +0 -7
- package/dist/templates/next-app/src/base/services/AdminUserService.ts +0 -45
- package/dist/templates/next-app/src/uikit/components/ChatRoot.tsx +0 -17
- package/dist/templates/next-app/src/uikit/components/chat/ChatActionInterface.ts +0 -30
- package/dist/templates/next-app/src/uikit/components/chat/ChatFocusBar.tsx +0 -65
- package/dist/templates/next-app/src/uikit/components/chat/ChatMessages.tsx +0 -59
- package/dist/templates/next-app/src/uikit/components/chat/ChatWrap.tsx +0 -28
- package/dist/templates/next-app/src/uikit/components/chat/FocusBarActionInterface.ts +0 -19
- package/dist/templates/next-app/src/uikit/hook/useMountedClient.ts +0 -17
- package/dist/templates/next-app/src/uikit/hook/useStore.ts +0 -15
- package/dist/templates/react-app/__tests__/__mocks__/I18nService.ts +0 -13
- package/dist/templates/react-app/__tests__/src/App.test.tsx +0 -139
- package/dist/templates/react-app/config/Identifier/page.identifiter.ts +0 -39
- package/dist/templates/react-app/config/i18n.ts +0 -15
- package/dist/templates/react-app/docs/en/project-structure.md +0 -434
- package/dist/templates/react-app/docs/zh/project-structure.md +0 -434
- package/dist/templates/react-app/src/base/cases/RequestState.ts +0 -20
- package/dist/templates/react-app/src/base/port/AsyncStateInterface.ts +0 -7
- package/dist/templates/react-app/src/uikit/hooks/useDocumentTitle.ts +0 -15
- package/dist/templates/react-app/src/uikit/hooks/useStore.ts +0 -15
|
@@ -1,424 +1,1365 @@
|
|
|
1
|
-
# IOC Container
|
|
1
|
+
# IOC Container (Dependency Injection)
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 📋 Table of Contents
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
- [Core Philosophy](#-core-philosophy) - UI separation, logic independence
|
|
6
|
+
- [What is IOC](#-what-is-ioc) - Inversion of Control
|
|
7
|
+
- [Why Need IOC](#-why-need-ioc) - Core problems it solves
|
|
8
|
+
- [Two Key Questions](#-two-key-questions) - Why need interfaces? Why separate even simple components?
|
|
9
|
+
- [Implementation in the Project](#-implementation-in-the-project) - Bootstrap integration
|
|
10
|
+
- [How to Use](#-how-to-use) - Practical guide
|
|
11
|
+
- [Testing](#-testing) - Independent testing and combination testing
|
|
12
|
+
- [Best Practices](#-best-practices) - 8 core practices
|
|
13
|
+
- [FAQ](#-faq) - Common questions
|
|
6
14
|
|
|
7
|
-
|
|
15
|
+
---
|
|
8
16
|
|
|
9
|
-
##
|
|
17
|
+
## 🎯 Core Philosophy
|
|
10
18
|
|
|
11
|
-
|
|
19
|
+
> **🚨 Important Principle: UI is UI, logic is logic, they must be separated!**
|
|
12
20
|
|
|
13
|
-
|
|
14
|
-
- **TypeScript**: Provides type safety
|
|
15
|
-
- **Decorator Pattern**: Uses `@injectable()` and `@inject()` decorators
|
|
21
|
+
> **⭐ Core Advantage: UI and logic can be tested independently, and also in combination!**
|
|
16
22
|
|
|
17
|
-
### Core
|
|
23
|
+
### Core Concept
|
|
18
24
|
|
|
19
25
|
```
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
│
|
|
25
|
-
│
|
|
26
|
-
│
|
|
27
|
-
│
|
|
28
|
-
│
|
|
29
|
-
│
|
|
30
|
-
|
|
31
|
-
│
|
|
32
|
-
│
|
|
26
|
+
┌─────────────────────────────────────────┐
|
|
27
|
+
│ Traditional Approach: UI and Logic Mixed│
|
|
28
|
+
│ │
|
|
29
|
+
│ Component │
|
|
30
|
+
│ ├── UI rendering │
|
|
31
|
+
│ ├── Business logic │
|
|
32
|
+
│ ├── API calls │
|
|
33
|
+
│ ├── State management │
|
|
34
|
+
│ └── Data processing │
|
|
35
|
+
│ │
|
|
36
|
+
│ ❌ Problems: │
|
|
37
|
+
│ - Hard to test (need to render) │
|
|
38
|
+
│ - Logic can't be reused │
|
|
39
|
+
│ - Unclear responsibilities │
|
|
40
|
+
└─────────────────────────────────────────┘
|
|
41
|
+
|
|
42
|
+
┌─────────────────────────────────────────┐
|
|
43
|
+
│ IOC Approach: UI and Logic Completely │
|
|
44
|
+
│ Separated │
|
|
45
|
+
│ │
|
|
46
|
+
│ Component (UI Layer) │
|
|
47
|
+
│ └── Only responsible for rendering │
|
|
48
|
+
│ ↓ Get through IOC │
|
|
49
|
+
│ Service (Logic Layer) │
|
|
50
|
+
│ ├── Business logic │
|
|
51
|
+
│ ├── API calls │
|
|
52
|
+
│ ├── State management │
|
|
53
|
+
│ └── Data processing │
|
|
54
|
+
│ │
|
|
55
|
+
│ ✅ Advantages: │
|
|
56
|
+
│ - UI and logic can be tested separately │
|
|
57
|
+
│ - Logic can be reused │
|
|
58
|
+
│ - Clear responsibilities │
|
|
59
|
+
└─────────────────────────────────────────┘
|
|
33
60
|
```
|
|
34
61
|
|
|
35
|
-
|
|
62
|
+
---
|
|
36
63
|
|
|
37
|
-
|
|
64
|
+
## 🔄 What is IOC
|
|
38
65
|
|
|
39
|
-
|
|
40
|
-
// config/IOCIdentifier.ts
|
|
41
|
-
export const IOCIdentifier = Object.freeze({
|
|
42
|
-
JSON: 'JSON',
|
|
43
|
-
LocalStorage: 'LocalStorage',
|
|
44
|
-
Logger: 'Logger',
|
|
45
|
-
AppConfig: 'AppConfig'
|
|
46
|
-
// ... more identifiers
|
|
47
|
-
});
|
|
48
|
-
```
|
|
66
|
+
IOC (Inversion of Control) = **Don't create objects yourself, let the container create and manage them**
|
|
49
67
|
|
|
50
|
-
###
|
|
68
|
+
### Traditional Approach vs IOC
|
|
51
69
|
|
|
52
|
-
```
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
// ... more mappings
|
|
70
|
+
```typescript
|
|
71
|
+
// ❌ Traditional approach: Create dependencies yourself (tight coupling)
|
|
72
|
+
class UserComponent {
|
|
73
|
+
private userService = new UserService(); // Create yourself
|
|
74
|
+
private storage = new LocalStorage(); // Create yourself
|
|
75
|
+
private api = new UserApi(); // Create yourself
|
|
76
|
+
|
|
77
|
+
async loadUser() {
|
|
78
|
+
return await this.userService.getUser();
|
|
79
|
+
}
|
|
63
80
|
}
|
|
81
|
+
|
|
82
|
+
// Problems:
|
|
83
|
+
// 1. UserComponent depends on concrete implementations
|
|
84
|
+
// 2. Cannot replace UserService implementation
|
|
85
|
+
// 3. Cannot mock UserService in tests
|
|
86
|
+
// 4. Need to manually create UserService dependencies
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
// ✅ IOC approach: Container injects dependencies (loose coupling)
|
|
90
|
+
function UserComponent() {
|
|
91
|
+
// Get service from IOC container
|
|
92
|
+
const userService = useIOC('UserServiceInterface'); // Container provides
|
|
93
|
+
|
|
94
|
+
async function loadUser() {
|
|
95
|
+
return await userService.getUser();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// UI only responsible for rendering
|
|
99
|
+
return <div>...</div>;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Advantages:
|
|
103
|
+
// 1. UserComponent depends on interface, not implementation
|
|
104
|
+
// 2. Can easily replace UserService implementation
|
|
105
|
+
// 3. Can mock UserService in tests
|
|
106
|
+
// 4. UserService dependencies managed by container
|
|
64
107
|
```
|
|
65
108
|
|
|
66
|
-
###
|
|
109
|
+
### Analogy
|
|
67
110
|
|
|
68
|
-
```tsx
|
|
69
|
-
// core/IOC.ts
|
|
70
|
-
export const IOC = createIOCFunction<IOCIdentifierMap>(
|
|
71
|
-
new InversifyContainer()
|
|
72
|
-
);
|
|
73
111
|
```
|
|
112
|
+
Traditional Approach = Cook at home
|
|
113
|
+
- Need to buy groceries (create dependencies)
|
|
114
|
+
- Need to cook (manage lifecycle)
|
|
115
|
+
- Need to wash dishes (clean up resources)
|
|
116
|
+
|
|
117
|
+
IOC Approach = Go to restaurant
|
|
118
|
+
- Order food (tell container what you need)
|
|
119
|
+
- Wait for food (container provides service)
|
|
120
|
+
- Don't need to worry about kitchen (dependency management handled by container)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
74
124
|
|
|
75
|
-
##
|
|
125
|
+
## 🤔 Why Need IOC
|
|
76
126
|
|
|
77
|
-
###
|
|
127
|
+
### Core Problem: UI and Logic Mixed Together
|
|
78
128
|
|
|
79
|
-
|
|
80
|
-
// Using class name
|
|
81
|
-
const userService = IOC(UserService);
|
|
129
|
+
#### ❌ Problem Example: No UI Separation
|
|
82
130
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const
|
|
131
|
+
```typescript
|
|
132
|
+
// ❌ Traditional component: UI and logic mixed
|
|
133
|
+
function UserProfile() {
|
|
134
|
+
const [user, setUser] = useState(null);
|
|
135
|
+
const [loading, setLoading] = useState(false);
|
|
136
|
+
const [error, setError] = useState(null);
|
|
137
|
+
|
|
138
|
+
// 😰 Business logic mixed in component
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
setLoading(true);
|
|
141
|
+
|
|
142
|
+
// 😰 API call in component
|
|
143
|
+
fetch('/api/user')
|
|
144
|
+
.then(res => res.json())
|
|
145
|
+
.then(data => {
|
|
146
|
+
// 😰 Data processing in component
|
|
147
|
+
const processedData = {
|
|
148
|
+
...data,
|
|
149
|
+
fullName: `${data.firstName} ${data.lastName}`
|
|
150
|
+
};
|
|
151
|
+
setUser(processedData);
|
|
152
|
+
})
|
|
153
|
+
.catch(err => setError(err))
|
|
154
|
+
.finally(() => setLoading(false));
|
|
155
|
+
}, []);
|
|
87
156
|
|
|
88
|
-
//
|
|
89
|
-
const
|
|
157
|
+
// 😰 More business logic
|
|
158
|
+
const handleLogout = () => {
|
|
159
|
+
localStorage.removeItem('token');
|
|
160
|
+
localStorage.removeItem('user');
|
|
161
|
+
window.location.href = '/login';
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// UI rendering
|
|
165
|
+
if (loading) return <div>Loading...</div>;
|
|
166
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<div>
|
|
170
|
+
<h1>{user?.fullName}</h1>
|
|
171
|
+
<button onClick={handleLogout}>Logout</button>
|
|
172
|
+
</div>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// 😰😰😰 Problem Summary:
|
|
177
|
+
// 1. UI and logic mixed, hard to maintain
|
|
178
|
+
// 2. Logic can't be reused (what if another component needs user info?)
|
|
179
|
+
// 3. Hard to test (need to render component to test business logic)
|
|
180
|
+
// 4. Unclear responsibilities (component does too much)
|
|
181
|
+
// 5. Can't test logic separately (must test through UI)
|
|
90
182
|
```
|
|
91
183
|
|
|
92
|
-
|
|
184
|
+
#### ✅ Solution: IOC + UI Separation
|
|
93
185
|
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
186
|
+
```typescript
|
|
187
|
+
// ✅ Step 1: Define interface (Port)
|
|
188
|
+
export interface UserServiceInterface {
|
|
189
|
+
getUser(): Promise<UserInfo>;
|
|
190
|
+
logout(): Promise<void>;
|
|
191
|
+
isAuthenticated(): boolean;
|
|
192
|
+
}
|
|
99
193
|
|
|
194
|
+
// ✅ Step 2: Implement service (Logic layer)
|
|
100
195
|
@injectable()
|
|
101
|
-
export class UserService
|
|
196
|
+
export class UserService implements UserServiceInterface {
|
|
102
197
|
constructor(
|
|
103
|
-
@inject(
|
|
104
|
-
@inject(
|
|
105
|
-
@inject(IOCIdentifier.
|
|
106
|
-
@inject(IOCIdentifier.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
198
|
+
@inject(UserApi) private api: UserApi,
|
|
199
|
+
@inject(IOCIdentifier.AppConfig) private config: AppConfig,
|
|
200
|
+
@inject(IOCIdentifier.LocalStorageEncrypt) private storage: Storage,
|
|
201
|
+
@inject(IOCIdentifier.RouteServiceInterface) private router: RouteService
|
|
202
|
+
) {}
|
|
203
|
+
|
|
204
|
+
// Pure logic: Get user info
|
|
205
|
+
async getUser(): Promise<UserInfo> {
|
|
206
|
+
const data = await this.api.getUserInfo();
|
|
207
|
+
|
|
208
|
+
// Data processing
|
|
209
|
+
return {
|
|
210
|
+
...data,
|
|
211
|
+
fullName: `${data.firstName} ${data.lastName}`
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Pure logic: Logout
|
|
216
|
+
async logout(): Promise<void> {
|
|
217
|
+
this.storage.removeItem(this.config.userTokenStorageKey);
|
|
218
|
+
this.storage.removeItem(this.config.userInfoStorageKey);
|
|
219
|
+
await this.router.push('/login');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
isAuthenticated(): boolean {
|
|
223
|
+
return !!this.storage.getItem(this.config.userTokenStorageKey);
|
|
119
224
|
}
|
|
120
225
|
}
|
|
226
|
+
|
|
227
|
+
// ✅ Step 3: UI component (UI layer)
|
|
228
|
+
function UserProfile() {
|
|
229
|
+
// Get service from IOC container
|
|
230
|
+
const userService = useIOC('UserServiceInterface');
|
|
231
|
+
const [user, setUser] = useState(null);
|
|
232
|
+
const [loading, setLoading] = useState(false);
|
|
233
|
+
|
|
234
|
+
useEffect(() => {
|
|
235
|
+
setLoading(true);
|
|
236
|
+
// ✅ UI only calls service, contains no business logic
|
|
237
|
+
userService.getUser()
|
|
238
|
+
.then(setUser)
|
|
239
|
+
.finally(() => setLoading(false));
|
|
240
|
+
}, []);
|
|
241
|
+
|
|
242
|
+
// ✅ UI only responsible for rendering and event binding
|
|
243
|
+
if (loading) return <div>Loading...</div>;
|
|
244
|
+
|
|
245
|
+
return (
|
|
246
|
+
<div>
|
|
247
|
+
<h1>{user?.fullName}</h1>
|
|
248
|
+
<button onClick={() => userService.logout()}>Logout</button>
|
|
249
|
+
</div>
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ✅✅✅ Advantages Summary:
|
|
254
|
+
// 1. UI and logic completely separated, clear responsibilities
|
|
255
|
+
// 2. Logic can be reused (other components can use UserService)
|
|
256
|
+
// 3. Easy to test (can test UserService independently, no need to render UI)
|
|
257
|
+
// 4. Easy to maintain (changing logic doesn't affect UI, changing UI doesn't affect logic)
|
|
258
|
+
// 5. Can test logic separately (no dependency on UI)
|
|
121
259
|
```
|
|
122
260
|
|
|
123
|
-
###
|
|
261
|
+
### Comparison Summary
|
|
262
|
+
|
|
263
|
+
| Feature | No UI Separation | IOC + UI Separation |
|
|
264
|
+
| ------------------------------- | ---------------------------------- | --------------------------------------------- |
|
|
265
|
+
| **Clarity of Responsibilities** | ❌ UI and logic mixed | ✅ UI only renders, logic independent |
|
|
266
|
+
| **Testability** | ❌ Must render component to test | ✅ Logic can be tested independently |
|
|
267
|
+
| **Reusability** | ❌ Logic can't be reused | ✅ Logic can be reused in multiple components |
|
|
268
|
+
| **Maintainability** | ❌ Changing logic affects UI | ✅ UI and logic modified independently |
|
|
269
|
+
| **Test Speed** | ❌ Slow (need to render UI) | ✅ Fast (pure logic tests) |
|
|
270
|
+
| **Test Complexity** | ❌ High (need to mock many things) | ✅ Low (only need to mock interfaces) |
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## ❓ Two Key Questions
|
|
275
|
+
|
|
276
|
+
### Question 1: Why does an implementation class also need an interface?
|
|
277
|
+
|
|
278
|
+
Many developers ask: "If `UserService` only has one implementation class, why define a `UserServiceInterface` interface?"
|
|
279
|
+
|
|
280
|
+
#### Answer: For testability and flexibility
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
// ❌ No interface: Hard to test
|
|
284
|
+
class UserComponent {
|
|
285
|
+
constructor(
|
|
286
|
+
@inject(UserService) private userService: UserService // Depend on concrete implementation
|
|
287
|
+
) {}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// In tests:
|
|
291
|
+
describe('UserComponent', () => {
|
|
292
|
+
it('should load user', () => {
|
|
293
|
+
// ❌ Problem: Cannot mock UserService
|
|
294
|
+
// UserService has many dependencies (API, Storage, Router, etc.)
|
|
295
|
+
// Need to create all these dependencies to create UserService
|
|
296
|
+
|
|
297
|
+
const userApi = new UserApi(); // Need to create
|
|
298
|
+
const storage = new Storage(); // Need to create
|
|
299
|
+
const router = new Router(); // Need to create
|
|
300
|
+
const config = new AppConfig(); // Need to create
|
|
301
|
+
|
|
302
|
+
const userService = new UserService(userApi, config, storage, router);
|
|
303
|
+
const component = new UserComponent(userService);
|
|
304
|
+
|
|
305
|
+
// 😰 Too complex!
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// ✅ With interface: Easy to test
|
|
310
|
+
class UserComponent {
|
|
311
|
+
constructor(
|
|
312
|
+
@inject('UserServiceInterface') // Depend on interface
|
|
313
|
+
private userService: UserServiceInterface
|
|
314
|
+
) {}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// In tests:
|
|
318
|
+
describe('UserComponent', () => {
|
|
319
|
+
it('should load user', () => {
|
|
320
|
+
// ✅ Only need to mock interface
|
|
321
|
+
const mockUserService: UserServiceInterface = {
|
|
322
|
+
getUser: jest.fn().mockResolvedValue({ name: 'John' }),
|
|
323
|
+
logout: jest.fn(),
|
|
324
|
+
isAuthenticated: jest.fn().mockReturnValue(true)
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const component = new UserComponent(mockUserService);
|
|
124
328
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
IOC(UserService), // User service
|
|
129
|
-
IOC(I18nService), // Internationalization service
|
|
130
|
-
new UserApiBootstarp() // API configuration
|
|
131
|
-
]);
|
|
329
|
+
// ✅ Simple and clear!
|
|
330
|
+
});
|
|
331
|
+
});
|
|
132
332
|
```
|
|
133
333
|
|
|
134
|
-
|
|
334
|
+
**Key Advantages:**
|
|
335
|
+
|
|
336
|
+
1. **Simple testing** - Only need to mock interface methods, no need to create real dependencies
|
|
337
|
+
2. **Isolation** - When testing UserComponent, don't need to care about UserService implementation details
|
|
338
|
+
3. **Flexibility** - Can easily replace implementation in future (like adding MockUserService, CacheUserService, etc.)
|
|
339
|
+
4. **Decoupling** - Component only depends on interface, not concrete implementation
|
|
340
|
+
|
|
341
|
+
**Even with only one implementation class, interface is necessary because:**
|
|
342
|
+
|
|
343
|
+
- ✅ Need to mock in tests
|
|
344
|
+
- ✅ May have new implementations in future
|
|
345
|
+
- ✅ Component shouldn't depend on concrete implementation
|
|
346
|
+
- ✅ Interface is contract, implementation is detail
|
|
135
347
|
|
|
136
|
-
###
|
|
348
|
+
### Question 2: Why does a simple UI component also need UI separation?
|
|
137
349
|
|
|
138
|
-
|
|
139
|
-
// core/registers/RegisterGlobals.ts
|
|
140
|
-
export const RegisterGlobals: IOCRegister = {
|
|
141
|
-
register(container, _, options): void {
|
|
142
|
-
// Register application configuration
|
|
143
|
-
container.bind(IOCIdentifier.AppConfig, options!.appConfig);
|
|
350
|
+
Many developers ask: "My component is simple, just displays a username, why separate?"
|
|
144
351
|
|
|
145
|
-
|
|
146
|
-
container.bind(Logger, logger);
|
|
147
|
-
container.bind(IOCIdentifier.Logger, logger);
|
|
352
|
+
#### Answer: For testability and future extensibility
|
|
148
353
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
354
|
+
```typescript
|
|
355
|
+
// ❌ Simple component, not separated
|
|
356
|
+
function UserName() {
|
|
357
|
+
const [name, setName] = useState('');
|
|
358
|
+
|
|
359
|
+
useEffect(() => {
|
|
360
|
+
// 😰 Even if simple, logic is mixed in UI
|
|
361
|
+
fetch('/api/user')
|
|
362
|
+
.then(res => res.json())
|
|
363
|
+
.then(data => setName(data.name));
|
|
364
|
+
}, []);
|
|
365
|
+
|
|
366
|
+
return <span>{name}</span>;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Problems:
|
|
370
|
+
// 1. Cannot test logic (must render component)
|
|
371
|
+
// 2. What if logic becomes complex? (add cache, error handling, etc.)
|
|
372
|
+
// 3. What if other components need username? (copy-paste?)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
// ✅ Simple component, but separated
|
|
376
|
+
// 1. Service (Logic layer)
|
|
377
|
+
@injectable()
|
|
378
|
+
export class UserService implements UserServiceInterface {
|
|
379
|
+
constructor(@inject(UserApi) private api: UserApi) {}
|
|
380
|
+
|
|
381
|
+
async getUserName(): Promise<string> {
|
|
382
|
+
const user = await this.api.getUserInfo();
|
|
383
|
+
return user.name;
|
|
153
384
|
}
|
|
154
|
-
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// 2. UI component (UI layer)
|
|
388
|
+
function UserName() {
|
|
389
|
+
const userService = useIOC('UserServiceInterface');
|
|
390
|
+
const [name, setName] = useState('');
|
|
391
|
+
|
|
392
|
+
useEffect(() => {
|
|
393
|
+
userService.getUserName().then(setName);
|
|
394
|
+
}, []);
|
|
395
|
+
|
|
396
|
+
return <span>{name}</span>;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Advantages:
|
|
400
|
+
// 1. ✅ Can test getUserName logic independently
|
|
401
|
+
// 2. ✅ When logic becomes complex in future, only need to modify UserService
|
|
402
|
+
// 3. ✅ Other components can reuse UserService
|
|
403
|
+
// 4. ✅ UI component stays simple, only responsible for rendering
|
|
155
404
|
```
|
|
156
405
|
|
|
157
|
-
|
|
406
|
+
**Key Scenario: Logic Gradually Becomes Complex**
|
|
158
407
|
|
|
159
|
-
```
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
408
|
+
```typescript
|
|
409
|
+
// ❌ Not separated: Component becomes bloated when logic gets complex
|
|
410
|
+
function UserName() {
|
|
411
|
+
const [name, setName] = useState('');
|
|
412
|
+
const [loading, setLoading] = useState(false);
|
|
413
|
+
const [error, setError] = useState(null);
|
|
164
414
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
storage: container.get(IOCIdentifier.LocalStorageEncrypt)
|
|
168
|
-
});
|
|
415
|
+
useEffect(() => {
|
|
416
|
+
setLoading(true);
|
|
169
417
|
|
|
170
|
-
|
|
171
|
-
|
|
418
|
+
// 😰 Add cache
|
|
419
|
+
const cached = localStorage.getItem('userName');
|
|
420
|
+
if (cached) {
|
|
421
|
+
setName(cached);
|
|
422
|
+
setLoading(false);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
172
425
|
|
|
173
|
-
//
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
storage: localStorage
|
|
426
|
+
// 😰 Add error handling
|
|
427
|
+
fetch('/api/user')
|
|
428
|
+
.then(res => {
|
|
429
|
+
if (!res.ok) throw new Error('Failed');
|
|
430
|
+
return res.json();
|
|
179
431
|
})
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
container.bind(
|
|
184
|
-
RouteService,
|
|
185
|
-
new RouteService({
|
|
186
|
-
routes: baseRoutes,
|
|
187
|
-
logger
|
|
432
|
+
.then(data => {
|
|
433
|
+
setName(data.name);
|
|
434
|
+
localStorage.setItem('userName', data.name);
|
|
188
435
|
})
|
|
189
|
-
|
|
436
|
+
.catch(err => setError(err))
|
|
437
|
+
.finally(() => setLoading(false));
|
|
438
|
+
}, []);
|
|
439
|
+
|
|
440
|
+
// 😰 Component becomes complex
|
|
441
|
+
if (loading) return <span>Loading...</span>;
|
|
442
|
+
if (error) return <span>Error</span>;
|
|
443
|
+
return <span>{name}</span>;
|
|
444
|
+
}
|
|
190
445
|
|
|
191
|
-
|
|
192
|
-
|
|
446
|
+
|
|
447
|
+
// ✅ With separation: When logic becomes complex, only modify service
|
|
448
|
+
@injectable()
|
|
449
|
+
export class UserService implements UserServiceInterface {
|
|
450
|
+
constructor(
|
|
451
|
+
@inject(UserApi) private api: UserApi,
|
|
452
|
+
@inject(IOCIdentifier.LocalStorageEncrypt) private storage: Storage
|
|
453
|
+
) {}
|
|
454
|
+
|
|
455
|
+
// ✅ Logic in service, clear and straightforward
|
|
456
|
+
async getUserName(): Promise<string> {
|
|
457
|
+
// Cache logic
|
|
458
|
+
const cached = this.storage.getItem('userName');
|
|
459
|
+
if (cached) return cached;
|
|
460
|
+
|
|
461
|
+
// API call
|
|
462
|
+
const user = await this.api.getUserInfo();
|
|
463
|
+
|
|
464
|
+
// Cache
|
|
465
|
+
this.storage.setItem('userName', user.name);
|
|
466
|
+
|
|
467
|
+
return user.name;
|
|
193
468
|
}
|
|
194
|
-
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// ✅ UI component stays simple
|
|
472
|
+
function UserName() {
|
|
473
|
+
const userService = useIOC('UserServiceInterface');
|
|
474
|
+
const [name, setName] = useState('');
|
|
475
|
+
const [loading, setLoading] = useState(false);
|
|
476
|
+
|
|
477
|
+
useEffect(() => {
|
|
478
|
+
setLoading(true);
|
|
479
|
+
userService.getUserName()
|
|
480
|
+
.then(setName)
|
|
481
|
+
.finally(() => setLoading(false));
|
|
482
|
+
}, []);
|
|
483
|
+
|
|
484
|
+
if (loading) return <span>Loading...</span>;
|
|
485
|
+
return <span>{name}</span>;
|
|
486
|
+
}
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
**Summary: Even if component is simple, still separate because:**
|
|
490
|
+
|
|
491
|
+
- ✅ **Simple now doesn't mean simple later** - Requirements change
|
|
492
|
+
- ✅ **Logic can be reused** - Other components may need it
|
|
493
|
+
- ✅ **Easy to test** - Logic can be tested independently
|
|
494
|
+
- ✅ **Clear responsibilities** - UI only renders, logic independent
|
|
495
|
+
- ✅ **Easy to maintain** - Changing logic doesn't affect UI
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
## 🛠️ Implementation in the Project
|
|
500
|
+
|
|
501
|
+
### 1. File Structure
|
|
502
|
+
|
|
503
|
+
```
|
|
504
|
+
src/
|
|
505
|
+
├── base/
|
|
506
|
+
│ ├── port/ # Interface definition layer
|
|
507
|
+
│ │ ├── UserServiceInterface.ts
|
|
508
|
+
│ │ ├── I18nServiceInterface.ts
|
|
509
|
+
│ │ └── RouteServiceInterface.ts
|
|
510
|
+
│ └── services/ # Service implementation layer
|
|
511
|
+
│ ├── UserService.ts
|
|
512
|
+
│ ├── I18nService.ts
|
|
513
|
+
│ └── RouteService.ts
|
|
514
|
+
├── core/
|
|
515
|
+
│ ├── clientIoc/
|
|
516
|
+
│ │ ├── ClientIOC.ts # IOC container
|
|
517
|
+
│ │ └── ClientIOCRegister.ts # Registrar
|
|
518
|
+
│ └── globals.ts # Global instances
|
|
519
|
+
├── uikit/
|
|
520
|
+
│ ├── hooks/
|
|
521
|
+
│ │ └── useIOC.ts # React Hook
|
|
522
|
+
│ └── contexts/
|
|
523
|
+
│ └── IOCContext.tsx # React Context
|
|
524
|
+
└── config/
|
|
525
|
+
└── IOCIdentifier.ts # Identifier definitions
|
|
526
|
+
|
|
195
527
|
```
|
|
196
528
|
|
|
197
|
-
###
|
|
198
|
-
|
|
199
|
-
```
|
|
200
|
-
//
|
|
201
|
-
export
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
529
|
+
### 2. IOC Identifier Definition
|
|
530
|
+
|
|
531
|
+
```typescript
|
|
532
|
+
// config/IOCIdentifier.ts
|
|
533
|
+
export interface IOCIdentifierMap {
|
|
534
|
+
AppConfig: AppConfig;
|
|
535
|
+
Logger: LoggerInterface;
|
|
536
|
+
LocalStorageEncrypt: SyncStorageInterface<string, string>;
|
|
537
|
+
UserServiceInterface: UserServiceInterface;
|
|
538
|
+
I18nServiceInterface: I18nServiceInterface;
|
|
539
|
+
RouteServiceInterface: RouteServiceInterface;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
export const IOCIdentifier = {
|
|
543
|
+
AppConfig: 'AppConfig',
|
|
544
|
+
Logger: 'Logger',
|
|
545
|
+
LocalStorageEncrypt: 'LocalStorageEncrypt',
|
|
546
|
+
UserServiceInterface: 'UserServiceInterface',
|
|
547
|
+
I18nServiceInterface: 'I18nServiceInterface',
|
|
548
|
+
RouteServiceInterface: 'RouteServiceInterface'
|
|
549
|
+
} as const;
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
### 3. Service Registration
|
|
553
|
+
|
|
554
|
+
```typescript
|
|
555
|
+
// src/core/clientIoc/ClientIOCRegister.ts
|
|
556
|
+
export class ClientIOCRegister implements IOCRegisterInterface {
|
|
557
|
+
constructor(protected options: IocRegisterOptions) {}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Register global services
|
|
561
|
+
*/
|
|
562
|
+
protected registerGlobals(ioc: IOCContainerInterface): void {
|
|
563
|
+
const { appConfig } = this.options;
|
|
564
|
+
const { dialogHandler, localStorageEncrypt, JSON, logger } = globals;
|
|
565
|
+
|
|
566
|
+
// ✅ Register global instances
|
|
567
|
+
ioc.bind(IOCIdentifier.JSONSerializer, JSON);
|
|
568
|
+
ioc.bind(IOCIdentifier.Logger, logger);
|
|
569
|
+
ioc.bind(IOCIdentifier.AppConfig, appConfig);
|
|
570
|
+
ioc.bind(IOCIdentifier.LocalStorageEncrypt, localStorageEncrypt);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Register business services
|
|
575
|
+
*/
|
|
576
|
+
protected registerImplement(ioc: IOCContainerInterface): void {
|
|
577
|
+
// ✅ Register service implementations
|
|
578
|
+
ioc.bind(
|
|
579
|
+
IOCIdentifier.I18nServiceInterface,
|
|
580
|
+
new I18nService(this.options.pathname)
|
|
581
|
+
);
|
|
582
|
+
|
|
583
|
+
ioc.bind(IOCIdentifier.RouteServiceInterface, new RouteService(/* ... */));
|
|
584
|
+
|
|
585
|
+
// ✅ Service can depend on other services
|
|
586
|
+
ioc.bind(IOCIdentifier.UserServiceInterface, ioc.get(UserService));
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Registration entry point
|
|
591
|
+
*/
|
|
592
|
+
register(ioc: IOCContainerInterface): void {
|
|
593
|
+
this.registerGlobals(ioc);
|
|
594
|
+
this.registerImplement(ioc);
|
|
215
595
|
}
|
|
216
596
|
}
|
|
217
597
|
```
|
|
218
598
|
|
|
219
|
-
|
|
599
|
+
### 4. Create IOC Container
|
|
220
600
|
|
|
221
|
-
|
|
601
|
+
```typescript
|
|
602
|
+
// src/core/clientIoc/ClientIOC.ts
|
|
603
|
+
import { createIOCFunction } from '@qlover/corekit-bridge';
|
|
604
|
+
import { InversifyContainer } from '@/base/cases/InversifyContainer';
|
|
605
|
+
import { ClientIOCRegister } from './ClientIOCRegister';
|
|
222
606
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
607
|
+
export const clientIOC = {
|
|
608
|
+
create(options: IocRegisterOptions) {
|
|
609
|
+
// Create container
|
|
610
|
+
const container = new InversifyContainer();
|
|
611
|
+
|
|
612
|
+
// Create IOC function
|
|
613
|
+
const IOC = createIOCFunction(container);
|
|
614
|
+
|
|
615
|
+
// Register services
|
|
616
|
+
const register = new ClientIOCRegister(options);
|
|
617
|
+
register.register(container, IOC);
|
|
618
|
+
|
|
619
|
+
return IOC;
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
### 5. Initialize in Bootstrap
|
|
625
|
+
|
|
626
|
+
```typescript
|
|
627
|
+
// src/core/bootstraps/BootstrapClient.ts
|
|
628
|
+
export class BootstrapClient {
|
|
629
|
+
static async main(args: BootstrapClientArgs) {
|
|
630
|
+
const { root, bootHref, ioc } = args;
|
|
631
|
+
|
|
632
|
+
// ✅ Create IOC container
|
|
633
|
+
const IOC = ioc.create({
|
|
634
|
+
pathname: bootHref,
|
|
635
|
+
appConfig: appConfig
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
// Use IOC in Bootstrap
|
|
639
|
+
const bootstrap = new Bootstrap({
|
|
640
|
+
root,
|
|
641
|
+
logger,
|
|
642
|
+
ioc: {
|
|
643
|
+
manager: IOC,
|
|
644
|
+
register: iocRegister
|
|
241
645
|
}
|
|
242
646
|
});
|
|
647
|
+
|
|
648
|
+
await bootstrap.initialize();
|
|
649
|
+
await bootstrap.start();
|
|
243
650
|
}
|
|
651
|
+
}
|
|
652
|
+
```
|
|
244
653
|
|
|
245
|
-
|
|
246
|
-
if (this.isAuthenticated()) {
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
654
|
+
---
|
|
249
655
|
|
|
250
|
-
|
|
251
|
-
if (!userToken) {
|
|
252
|
-
throw new AppError('NO_USER_TOKEN');
|
|
253
|
-
}
|
|
656
|
+
## 📝 How to Use
|
|
254
657
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
658
|
+
### 1. Define Interface (Port)
|
|
659
|
+
|
|
660
|
+
```typescript
|
|
661
|
+
// src/base/port/UserServiceInterface.ts
|
|
662
|
+
export interface UserServiceInterface {
|
|
663
|
+
getUser(): Promise<UserInfo>;
|
|
664
|
+
login(username: string, password: string): Promise<void>;
|
|
665
|
+
logout(): Promise<void>;
|
|
666
|
+
isAuthenticated(): boolean;
|
|
258
667
|
}
|
|
259
668
|
```
|
|
260
669
|
|
|
261
|
-
### 2.
|
|
670
|
+
### 2. Implement Service
|
|
671
|
+
|
|
672
|
+
```typescript
|
|
673
|
+
// src/base/services/UserService.ts
|
|
674
|
+
import { injectable, inject } from 'inversify';
|
|
675
|
+
|
|
676
|
+
@injectable()
|
|
677
|
+
export class UserService implements UserServiceInterface {
|
|
678
|
+
constructor(
|
|
679
|
+
@inject(UserApi) private api: UserApi,
|
|
680
|
+
@inject(IOCIdentifier.AppConfig) private config: AppConfig,
|
|
681
|
+
@inject(IOCIdentifier.LocalStorageEncrypt) private storage: Storage,
|
|
682
|
+
@inject(IOCIdentifier.RouteServiceInterface) private router: RouteService
|
|
683
|
+
) {}
|
|
684
|
+
|
|
685
|
+
async getUser(): Promise<UserInfo> {
|
|
686
|
+
const token = this.storage.getItem(this.config.userTokenStorageKey);
|
|
687
|
+
if (!token) throw new Error('No token');
|
|
262
688
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
689
|
+
return await this.api.getUserInfo(token);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
async login(username: string, password: string): Promise<void> {
|
|
693
|
+
const response = await this.api.login({ username, password });
|
|
694
|
+
this.storage.setItem(this.config.userTokenStorageKey, response.token);
|
|
695
|
+
}
|
|
266
696
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
697
|
+
async logout(): Promise<void> {
|
|
698
|
+
this.storage.removeItem(this.config.userTokenStorageKey);
|
|
699
|
+
await this.router.push('/login');
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
isAuthenticated(): boolean {
|
|
703
|
+
return !!this.storage.getItem(this.config.userTokenStorageKey);
|
|
274
704
|
}
|
|
275
705
|
}
|
|
276
706
|
```
|
|
277
707
|
|
|
278
|
-
### 3.
|
|
708
|
+
### 3. Use in UI Components
|
|
709
|
+
|
|
710
|
+
```typescript
|
|
711
|
+
// src/pages/UserProfile.tsx
|
|
712
|
+
import { useIOC } from '@/uikit/hooks/useIOC';
|
|
279
713
|
|
|
280
|
-
```tsx
|
|
281
|
-
// Using IOC services in React components
|
|
282
714
|
function UserProfile() {
|
|
283
|
-
|
|
284
|
-
const
|
|
715
|
+
// ✅ Get service from IOC container
|
|
716
|
+
const userService = useIOC('UserServiceInterface');
|
|
717
|
+
const [user, setUser] = useState<UserInfo | null>(null);
|
|
285
718
|
|
|
719
|
+
useEffect(() => {
|
|
720
|
+
userService.getUser().then(setUser);
|
|
721
|
+
}, []);
|
|
722
|
+
|
|
723
|
+
const handleLogout = () => {
|
|
724
|
+
userService.logout();
|
|
725
|
+
};
|
|
726
|
+
|
|
727
|
+
// ✅ UI only responsible for rendering
|
|
286
728
|
return (
|
|
287
729
|
<div>
|
|
288
|
-
<h1>
|
|
289
|
-
<button onClick={
|
|
730
|
+
<h1>{user?.name}</h1>
|
|
731
|
+
<button onClick={handleLogout}>Logout</button>
|
|
290
732
|
</div>
|
|
291
733
|
);
|
|
292
734
|
}
|
|
293
735
|
```
|
|
294
736
|
|
|
295
|
-
|
|
737
|
+
### 4. Use Other Services in Services
|
|
296
738
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
```tsx
|
|
300
|
-
// ✅ Good design: Single responsibility
|
|
739
|
+
```typescript
|
|
740
|
+
// src/base/services/ProfileService.ts
|
|
301
741
|
@injectable()
|
|
302
|
-
export class
|
|
742
|
+
export class ProfileService {
|
|
303
743
|
constructor(
|
|
304
|
-
|
|
305
|
-
@inject(IOCIdentifier.
|
|
744
|
+
// ✅ Service can depend on other services
|
|
745
|
+
@inject(IOCIdentifier.UserServiceInterface)
|
|
746
|
+
private userService: UserServiceInterface,
|
|
747
|
+
@inject(IOCIdentifier.I18nServiceInterface)
|
|
748
|
+
private i18n: I18nServiceInterface
|
|
306
749
|
) {}
|
|
307
750
|
|
|
308
|
-
async
|
|
309
|
-
|
|
751
|
+
async getUserProfile(): Promise<string> {
|
|
752
|
+
const user = await this.userService.getUser();
|
|
753
|
+
return this.i18n.t('profile.welcome', { name: user.name });
|
|
310
754
|
}
|
|
311
755
|
}
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
---
|
|
759
|
+
|
|
760
|
+
## 🧪 Testing
|
|
761
|
+
|
|
762
|
+
### Core Advantage: UI and logic can be tested independently, and also in combination
|
|
763
|
+
|
|
764
|
+
#### 1. Test Logic Independently (No UI needed)
|
|
765
|
+
|
|
766
|
+
```typescript
|
|
767
|
+
// __tests__/src/base/services/UserService.test.ts
|
|
768
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
769
|
+
import { UserService } from '@/base/services/UserService';
|
|
770
|
+
|
|
771
|
+
describe('UserService (Logic Test)', () => {
|
|
772
|
+
let userService: UserService;
|
|
773
|
+
let mockApi: any;
|
|
774
|
+
let mockStorage: any;
|
|
775
|
+
let mockRouter: any;
|
|
776
|
+
let mockConfig: any;
|
|
777
|
+
|
|
778
|
+
beforeEach(() => {
|
|
779
|
+
// ✅ Only need to mock interfaces
|
|
780
|
+
mockApi = {
|
|
781
|
+
getUserInfo: vi.fn(),
|
|
782
|
+
login: vi.fn()
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
mockStorage = {
|
|
786
|
+
getItem: vi.fn(),
|
|
787
|
+
setItem: vi.fn(),
|
|
788
|
+
removeItem: vi.fn()
|
|
789
|
+
};
|
|
790
|
+
|
|
791
|
+
mockRouter = {
|
|
792
|
+
push: vi.fn()
|
|
793
|
+
};
|
|
794
|
+
|
|
795
|
+
mockConfig = {
|
|
796
|
+
userTokenStorageKey: '__test_token__'
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
// ✅ Create service
|
|
800
|
+
userService = new UserService(mockApi, mockConfig, mockStorage, mockRouter);
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
it('should get user when token exists', async () => {
|
|
804
|
+
// ✅ Set mock return value
|
|
805
|
+
mockStorage.getItem.mockReturnValue('test-token');
|
|
806
|
+
mockApi.getUserInfo.mockResolvedValue({ name: 'John' });
|
|
807
|
+
|
|
808
|
+
// ✅ Test logic
|
|
809
|
+
const user = await userService.getUser();
|
|
810
|
+
|
|
811
|
+
// ✅ Verify result
|
|
812
|
+
expect(user.name).toBe('John');
|
|
813
|
+
expect(mockStorage.getItem).toHaveBeenCalledWith('__test_token__');
|
|
814
|
+
expect(mockApi.getUserInfo).toHaveBeenCalledWith('test-token');
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
it('should throw error when no token', async () => {
|
|
818
|
+
// ✅ Test error scenario
|
|
819
|
+
mockStorage.getItem.mockReturnValue(null);
|
|
820
|
+
|
|
821
|
+
await expect(userService.getUser()).rejects.toThrow('No token');
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
it('should login and save token', async () => {
|
|
825
|
+
// ✅ Test login logic
|
|
826
|
+
mockApi.login.mockResolvedValue({ token: 'new-token' });
|
|
827
|
+
|
|
828
|
+
await userService.login('user', 'pass');
|
|
829
|
+
|
|
830
|
+
expect(mockApi.login).toHaveBeenCalledWith({
|
|
831
|
+
username: 'user',
|
|
832
|
+
password: 'pass'
|
|
833
|
+
});
|
|
834
|
+
expect(mockStorage.setItem).toHaveBeenCalledWith(
|
|
835
|
+
'__test_token__',
|
|
836
|
+
'new-token'
|
|
837
|
+
);
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
it('should logout and clear token', async () => {
|
|
841
|
+
// ✅ Test logout logic
|
|
842
|
+
await userService.logout();
|
|
843
|
+
|
|
844
|
+
expect(mockStorage.removeItem).toHaveBeenCalledWith('__test_token__');
|
|
845
|
+
expect(mockRouter.push).toHaveBeenCalledWith('/login');
|
|
846
|
+
});
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
// ✅✅✅ Advantages:
|
|
850
|
+
// 1. Don't need to render UI
|
|
851
|
+
// 2. Tests run fast (pure logic)
|
|
852
|
+
// 3. Easy to mock (only need to mock interfaces)
|
|
853
|
+
// 4. Can test all edge cases
|
|
854
|
+
```
|
|
855
|
+
|
|
856
|
+
#### 2. Test UI Independently (No real logic needed)
|
|
857
|
+
|
|
858
|
+
```typescript
|
|
859
|
+
// __tests__/src/pages/UserProfile.test.tsx
|
|
860
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
861
|
+
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
|
|
862
|
+
import { UserProfile } from '@/pages/UserProfile';
|
|
863
|
+
import { IOCProvider } from '@/uikit/contexts/IOCContext';
|
|
864
|
+
|
|
865
|
+
describe('UserProfile (UI Test)', () => {
|
|
866
|
+
it('should display user name', async () => {
|
|
867
|
+
// ✅ Mock service
|
|
868
|
+
const mockUserService = {
|
|
869
|
+
getUser: vi.fn().mockResolvedValue({ name: 'John Doe' }),
|
|
870
|
+
logout: vi.fn(),
|
|
871
|
+
isAuthenticated: vi.fn().mockReturnValue(true)
|
|
872
|
+
};
|
|
873
|
+
|
|
874
|
+
const mockIOC = (identifier: string) => {
|
|
875
|
+
if (identifier === 'UserServiceInterface') return mockUserService;
|
|
876
|
+
};
|
|
877
|
+
|
|
878
|
+
// ✅ Render component
|
|
879
|
+
render(
|
|
880
|
+
<IOCProvider value={mockIOC}>
|
|
881
|
+
<UserProfile />
|
|
882
|
+
</IOCProvider>
|
|
883
|
+
);
|
|
884
|
+
|
|
885
|
+
// ✅ Verify UI
|
|
886
|
+
await waitFor(() => {
|
|
887
|
+
expect(screen.getByText('John Doe')).toBeInTheDocument();
|
|
888
|
+
});
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
it('should call logout when button clicked', async () => {
|
|
892
|
+
const mockUserService = {
|
|
893
|
+
getUser: vi.fn().mockResolvedValue({ name: 'John' }),
|
|
894
|
+
logout: vi.fn(),
|
|
895
|
+
isAuthenticated: vi.fn().mockReturnValue(true)
|
|
896
|
+
};
|
|
897
|
+
|
|
898
|
+
const mockIOC = (identifier: string) => {
|
|
899
|
+
if (identifier === 'UserServiceInterface') return mockUserService;
|
|
900
|
+
};
|
|
901
|
+
|
|
902
|
+
render(
|
|
903
|
+
<IOCProvider value={mockIOC}>
|
|
904
|
+
<UserProfile />
|
|
905
|
+
</IOCProvider>
|
|
906
|
+
);
|
|
907
|
+
|
|
908
|
+
// ✅ Simulate user action
|
|
909
|
+
const logoutButton = screen.getByText('Logout');
|
|
910
|
+
fireEvent.click(logoutButton);
|
|
911
|
+
|
|
912
|
+
// ✅ Verify service call
|
|
913
|
+
expect(mockUserService.logout).toHaveBeenCalled();
|
|
914
|
+
});
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
// ✅✅✅ Advantages:
|
|
918
|
+
// 1. Don't need real service implementation
|
|
919
|
+
// 2. Can easily simulate various scenarios
|
|
920
|
+
// 3. UI tests focus on UI logic
|
|
921
|
+
```
|
|
922
|
+
|
|
923
|
+
#### 3. Combination Testing (UI + Logic)
|
|
924
|
+
|
|
925
|
+
```typescript
|
|
926
|
+
// __tests__/src/integration/UserFlow.test.tsx
|
|
927
|
+
import { describe, it, expect } from 'vitest';
|
|
928
|
+
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
|
|
929
|
+
import { UserProfile } from '@/pages/UserProfile';
|
|
930
|
+
import { UserService } from '@/base/services/UserService';
|
|
931
|
+
import { IOCProvider } from '@/uikit/contexts/IOCContext';
|
|
932
|
+
|
|
933
|
+
describe('User Flow (Integration Test)', () => {
|
|
934
|
+
it('should complete user login flow', async () => {
|
|
935
|
+
// ✅ Use real service implementation
|
|
936
|
+
const mockApi = {
|
|
937
|
+
getUserInfo: vi.fn().mockResolvedValue({ name: 'John' }),
|
|
938
|
+
login: vi.fn().mockResolvedValue({ token: 'test-token' })
|
|
939
|
+
};
|
|
940
|
+
|
|
941
|
+
const mockStorage = {
|
|
942
|
+
getItem: vi.fn(),
|
|
943
|
+
setItem: vi.fn(),
|
|
944
|
+
removeItem: vi.fn()
|
|
945
|
+
};
|
|
946
|
+
|
|
947
|
+
const mockRouter = { push: vi.fn() };
|
|
948
|
+
const mockConfig = { userTokenStorageKey: '__token__' };
|
|
949
|
+
|
|
950
|
+
// ✅ Create real service
|
|
951
|
+
const userService = new UserService(
|
|
952
|
+
mockApi,
|
|
953
|
+
mockConfig,
|
|
954
|
+
mockStorage,
|
|
955
|
+
mockRouter
|
|
956
|
+
);
|
|
312
957
|
|
|
313
|
-
|
|
958
|
+
const mockIOC = (identifier: string) => {
|
|
959
|
+
if (identifier === 'UserServiceInterface') return userService;
|
|
960
|
+
};
|
|
961
|
+
|
|
962
|
+
// ✅ Render real UI
|
|
963
|
+
render(
|
|
964
|
+
<IOCProvider value={mockIOC}>
|
|
965
|
+
<UserProfile />
|
|
966
|
+
</IOCProvider>
|
|
967
|
+
);
|
|
968
|
+
|
|
969
|
+
// ✅ Test complete flow
|
|
970
|
+
await waitFor(() => {
|
|
971
|
+
expect(screen.getByText('John')).toBeInTheDocument();
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
// ✅ Click logout
|
|
975
|
+
fireEvent.click(screen.getByText('Logout'));
|
|
976
|
+
|
|
977
|
+
// ✅ Verify entire flow
|
|
978
|
+
expect(mockStorage.removeItem).toHaveBeenCalledWith('__token__');
|
|
979
|
+
expect(mockRouter.push).toHaveBeenCalledWith('/login');
|
|
980
|
+
});
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
// ✅✅✅ Advantages:
|
|
984
|
+
// 1. Test real user flow
|
|
985
|
+
// 2. Can discover UI and logic integration issues
|
|
986
|
+
// 3. Closer to real usage scenarios
|
|
987
|
+
```
|
|
988
|
+
|
|
989
|
+
### Testing Strategy Summary
|
|
990
|
+
|
|
991
|
+
```
|
|
992
|
+
┌─────────────────────────────────────────┐
|
|
993
|
+
│ Testing Pyramid │
|
|
994
|
+
│ │
|
|
995
|
+
│ △ UI Tests (Few) │
|
|
996
|
+
│ ╱ ╲ │
|
|
997
|
+
│ ╱ ╲ │
|
|
998
|
+
│ ╱ ╲ │
|
|
999
|
+
│ ╱───────╲ Integration Tests (Some) │
|
|
1000
|
+
│ ╱ ╲ │
|
|
1001
|
+
│╱═══════════╲ Logic Tests (Many) │
|
|
1002
|
+
│ │
|
|
1003
|
+
│ Logic Tests: Fast, stable, comprehensive│
|
|
1004
|
+
│ Integration Tests: Verify integration │
|
|
1005
|
+
│ UI Tests: Verify user interactions │
|
|
1006
|
+
└─────────────────────────────────────────┘
|
|
1007
|
+
```
|
|
1008
|
+
|
|
1009
|
+
**Recommended Test Ratio:**
|
|
1010
|
+
|
|
1011
|
+
- 70% Logic tests (UserService.test.ts)
|
|
1012
|
+
- 20% Integration tests (UserFlow.test.tsx)
|
|
1013
|
+
- 10% UI tests (UserProfile.test.tsx)
|
|
1014
|
+
|
|
1015
|
+
---
|
|
1016
|
+
|
|
1017
|
+
## 💎 Best Practices
|
|
1018
|
+
|
|
1019
|
+
### 1. ✅ Always Define Interface
|
|
1020
|
+
|
|
1021
|
+
```typescript
|
|
1022
|
+
// ✅ Good practice: Define interface first
|
|
1023
|
+
export interface UserServiceInterface {
|
|
1024
|
+
getUser(): Promise<UserInfo>;
|
|
1025
|
+
logout(): Promise<void>;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// Then implement
|
|
314
1029
|
@injectable()
|
|
315
|
-
export class
|
|
1030
|
+
export class UserService implements UserServiceInterface {
|
|
1031
|
+
// ...
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// ❌ Bad practice: Write implementation directly
|
|
1035
|
+
@injectable()
|
|
1036
|
+
export class UserService {
|
|
1037
|
+
// No interface, hard to test
|
|
1038
|
+
}
|
|
1039
|
+
```
|
|
1040
|
+
|
|
1041
|
+
### 2. ✅ Complete UI and Logic Separation
|
|
1042
|
+
|
|
1043
|
+
```typescript
|
|
1044
|
+
// ✅ Good practice: UI only responsible for rendering
|
|
1045
|
+
function UserProfile() {
|
|
1046
|
+
const userService = useIOC('UserServiceInterface');
|
|
1047
|
+
const [user, setUser] = useState(null);
|
|
1048
|
+
|
|
1049
|
+
useEffect(() => {
|
|
1050
|
+
userService.getUser().then(setUser);
|
|
1051
|
+
}, []);
|
|
1052
|
+
|
|
1053
|
+
return <div>{user?.name}</div>;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// ❌ Bad practice: Logic mixed in UI
|
|
1057
|
+
function UserProfile() {
|
|
1058
|
+
const [user, setUser] = useState(null);
|
|
1059
|
+
|
|
1060
|
+
useEffect(() => {
|
|
1061
|
+
fetch('/api/user')
|
|
1062
|
+
.then(res => res.json())
|
|
1063
|
+
.then(setUser);
|
|
1064
|
+
}, []);
|
|
1065
|
+
|
|
1066
|
+
return <div>{user?.name}</div>;
|
|
1067
|
+
}
|
|
1068
|
+
```
|
|
1069
|
+
|
|
1070
|
+
### 3. ✅ Use Dependency Injection
|
|
1071
|
+
|
|
1072
|
+
```typescript
|
|
1073
|
+
// ✅ Good practice: Inject through constructor
|
|
1074
|
+
@injectable()
|
|
1075
|
+
export class UserService {
|
|
316
1076
|
constructor(
|
|
317
|
-
@inject(UserApi) private
|
|
318
|
-
@inject(
|
|
319
|
-
@inject(ThemeService) private themeService: ThemeService,
|
|
320
|
-
@inject(I18nService) private i18nService: I18nService
|
|
1077
|
+
@inject(UserApi) private api: UserApi,
|
|
1078
|
+
@inject(IOCIdentifier.AppConfig) private config: AppConfig
|
|
321
1079
|
) {}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// ❌ Bad practice: Create dependencies directly
|
|
1083
|
+
export class UserService {
|
|
1084
|
+
private api = new UserApi();
|
|
1085
|
+
private config = new AppConfig();
|
|
1086
|
+
}
|
|
1087
|
+
```
|
|
1088
|
+
|
|
1089
|
+
### 4. ✅ Single Responsibility for Services
|
|
1090
|
+
|
|
1091
|
+
```typescript
|
|
1092
|
+
// ✅ Good practice: Each service responsible for one thing
|
|
1093
|
+
@injectable()
|
|
1094
|
+
export class UserService {
|
|
1095
|
+
// Only responsible for user-related logic
|
|
1096
|
+
async getUser() {
|
|
1097
|
+
/* ... */
|
|
1098
|
+
}
|
|
1099
|
+
async logout() {
|
|
1100
|
+
/* ... */
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
322
1103
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
1104
|
+
@injectable()
|
|
1105
|
+
export class ThemeService {
|
|
1106
|
+
// Only responsible for theme-related logic
|
|
1107
|
+
setTheme() {
|
|
1108
|
+
/* ... */
|
|
1109
|
+
}
|
|
1110
|
+
getTheme() {
|
|
1111
|
+
/* ... */
|
|
329
1112
|
}
|
|
330
1113
|
}
|
|
1114
|
+
|
|
1115
|
+
// ❌ Bad practice: One service does multiple things
|
|
1116
|
+
@injectable()
|
|
1117
|
+
export class ApplicationService {
|
|
1118
|
+
async getUser() {
|
|
1119
|
+
/* ... */
|
|
1120
|
+
}
|
|
1121
|
+
setTheme() {
|
|
1122
|
+
/* ... */
|
|
1123
|
+
}
|
|
1124
|
+
changeLanguage() {
|
|
1125
|
+
/* ... */
|
|
1126
|
+
}
|
|
1127
|
+
// Too many responsibilities!
|
|
1128
|
+
}
|
|
331
1129
|
```
|
|
332
1130
|
|
|
333
|
-
###
|
|
1131
|
+
### 5. ✅ Depend on Interfaces, Not Implementations
|
|
334
1132
|
|
|
335
|
-
```
|
|
336
|
-
// ✅
|
|
1133
|
+
```typescript
|
|
1134
|
+
// ✅ Good practice
|
|
337
1135
|
@injectable()
|
|
338
1136
|
export class UserService {
|
|
339
1137
|
constructor(
|
|
340
|
-
@inject('UserApiInterface') private
|
|
1138
|
+
@inject('UserApiInterface') private api: UserApiInterface // Interface
|
|
341
1139
|
) {}
|
|
342
1140
|
}
|
|
343
1141
|
|
|
344
|
-
//
|
|
1142
|
+
// ❌ Bad practice
|
|
345
1143
|
@injectable()
|
|
346
|
-
export class
|
|
1144
|
+
export class UserService {
|
|
347
1145
|
constructor(
|
|
348
|
-
@inject(
|
|
349
|
-
@inject(IOCIdentifier.AppConfig) private appConfig: AppConfig
|
|
1146
|
+
@inject(UserApi) private api: UserApi // Concrete implementation
|
|
350
1147
|
) {}
|
|
351
1148
|
}
|
|
352
1149
|
```
|
|
353
1150
|
|
|
354
|
-
###
|
|
1151
|
+
### 6. ✅ Separate Even If Simple
|
|
355
1152
|
|
|
356
|
-
```
|
|
1153
|
+
```typescript
|
|
1154
|
+
// ✅ Good practice: Separate even if simple
|
|
357
1155
|
@injectable()
|
|
358
|
-
export class
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
} catch (error) {
|
|
365
|
-
this.logger.error('Operation failed:', error);
|
|
366
|
-
throw error;
|
|
367
|
-
}
|
|
1156
|
+
export class CounterService {
|
|
1157
|
+
private count = 0;
|
|
1158
|
+
|
|
1159
|
+
increment() {
|
|
1160
|
+
this.count++;
|
|
1161
|
+
return this.count;
|
|
368
1162
|
}
|
|
369
1163
|
}
|
|
370
|
-
```
|
|
371
|
-
|
|
372
|
-
## Debugging and Testing
|
|
373
1164
|
|
|
374
|
-
|
|
1165
|
+
function Counter() {
|
|
1166
|
+
const counterService = useIOC('CounterService');
|
|
1167
|
+
const [count, setCount] = useState(0);
|
|
375
1168
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
const isRegistered = container.isBound(UserService);
|
|
1169
|
+
const handleClick = () => {
|
|
1170
|
+
setCount(counterService.increment());
|
|
1171
|
+
};
|
|
380
1172
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
```
|
|
1173
|
+
return <button onClick={handleClick}>{count}</button>;
|
|
1174
|
+
}
|
|
384
1175
|
|
|
385
|
-
|
|
1176
|
+
// ❌ Bad practice: Simple logic also mixed in UI
|
|
1177
|
+
function Counter() {
|
|
1178
|
+
const [count, setCount] = useState(0);
|
|
386
1179
|
|
|
387
|
-
|
|
388
|
-
|
|
1180
|
+
return (
|
|
1181
|
+
<button onClick={() => setCount(count + 1)}>
|
|
1182
|
+
{count}
|
|
1183
|
+
</button>
|
|
1184
|
+
);
|
|
1185
|
+
}
|
|
1186
|
+
```
|
|
389
1187
|
|
|
390
|
-
|
|
391
|
-
let container: Container;
|
|
392
|
-
let userService: UserService;
|
|
1188
|
+
### 7. ✅ Write Comprehensive Tests
|
|
393
1189
|
|
|
394
|
-
|
|
395
|
-
|
|
1190
|
+
```typescript
|
|
1191
|
+
// ✅ Good practice: Logic tests + UI tests + Integration tests
|
|
1192
|
+
describe('UserService (Logic)', () => {
|
|
1193
|
+
it('should get user', async () => {
|
|
1194
|
+
/* ... */
|
|
1195
|
+
});
|
|
1196
|
+
it('should handle error', async () => {
|
|
1197
|
+
/* ... */
|
|
1198
|
+
});
|
|
1199
|
+
});
|
|
396
1200
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
.toConstantValue(mockStorage);
|
|
1201
|
+
describe('UserProfile (UI)', () => {
|
|
1202
|
+
it('should display user', async () => {
|
|
1203
|
+
/* ... */
|
|
1204
|
+
});
|
|
1205
|
+
});
|
|
403
1206
|
|
|
404
|
-
|
|
1207
|
+
describe('User Flow (Integration)', () => {
|
|
1208
|
+
it('should complete flow', async () => {
|
|
1209
|
+
/* ... */
|
|
405
1210
|
});
|
|
1211
|
+
});
|
|
406
1212
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
1213
|
+
// ❌ Bad practice: Only UI tests
|
|
1214
|
+
describe('UserProfile', () => {
|
|
1215
|
+
it('should work', async () => {
|
|
1216
|
+
// Only test UI, logic not tested
|
|
410
1217
|
});
|
|
411
1218
|
});
|
|
412
1219
|
```
|
|
413
1220
|
|
|
414
|
-
|
|
1221
|
+
### 8. ✅ Use Type-safe Identifiers
|
|
1222
|
+
|
|
1223
|
+
```typescript
|
|
1224
|
+
// ✅ Good practice: Type-safe identifiers
|
|
1225
|
+
const userService = useIOC('UserServiceInterface');
|
|
1226
|
+
// TypeScript knows userService type
|
|
1227
|
+
|
|
1228
|
+
// ❌ Bad practice: String literals
|
|
1229
|
+
const userService = useIOC('UserService');
|
|
1230
|
+
// Easy to misspell, no type checking
|
|
1231
|
+
```
|
|
1232
|
+
|
|
1233
|
+
---
|
|
1234
|
+
|
|
1235
|
+
## ❓ FAQ
|
|
1236
|
+
|
|
1237
|
+
### Q1: Does IOC increase complexity?
|
|
1238
|
+
|
|
1239
|
+
**A:** Short-term maybe, but long-term it greatly reduces complexity:
|
|
1240
|
+
|
|
1241
|
+
**Short-term (small projects):**
|
|
1242
|
+
|
|
1243
|
+
- Need to define interfaces
|
|
1244
|
+
- Need to register services
|
|
1245
|
+
- Need to learn IOC concepts
|
|
1246
|
+
|
|
1247
|
+
**Long-term (project grows):**
|
|
1248
|
+
|
|
1249
|
+
- ✅ Easy to test (save lots of testing time)
|
|
1250
|
+
- ✅ Easy to maintain (clear dependency relationships)
|
|
1251
|
+
- ✅ Easy to extend (adding new features is simple)
|
|
1252
|
+
- ✅ Team collaboration (clear responsibilities)
|
|
1253
|
+
|
|
1254
|
+
### Q2: Do all components need IOC?
|
|
1255
|
+
|
|
1256
|
+
**A:** Not necessarily, but recommended:
|
|
1257
|
+
|
|
1258
|
+
**Scenarios that need IOC:**
|
|
1259
|
+
|
|
1260
|
+
- ✅ Components with business logic
|
|
1261
|
+
- ✅ Components that call APIs
|
|
1262
|
+
- ✅ Components that access Storage
|
|
1263
|
+
- ✅ Components that need testing
|
|
1264
|
+
|
|
1265
|
+
**Scenarios that can skip IOC:**
|
|
1266
|
+
|
|
1267
|
+
- Pure presentational components (only receive props)
|
|
1268
|
+
- Very simple UI components (like Button, Icon)
|
|
1269
|
+
|
|
1270
|
+
### Q3: Why not directly import service?
|
|
1271
|
+
|
|
1272
|
+
**A:**
|
|
1273
|
+
|
|
1274
|
+
```typescript
|
|
1275
|
+
// ❌ Direct import
|
|
1276
|
+
import { userService } from '@/services/UserService';
|
|
1277
|
+
|
|
1278
|
+
function UserProfile() {
|
|
1279
|
+
// Problems:
|
|
1280
|
+
// 1. userService is singleton, can't replace in tests
|
|
1281
|
+
// 2. userService dependencies created at module load time
|
|
1282
|
+
// 3. Hard to mock
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
// ✅ Use IOC
|
|
1286
|
+
function UserProfile() {
|
|
1287
|
+
const userService = useIOC('UserServiceInterface');
|
|
1288
|
+
|
|
1289
|
+
// Advantages:
|
|
1290
|
+
// 1. Can provide mock implementation in tests
|
|
1291
|
+
// 2. Dependencies managed by container, created on demand
|
|
1292
|
+
// 3. Easy to mock
|
|
1293
|
+
}
|
|
1294
|
+
```
|
|
1295
|
+
|
|
1296
|
+
### Q4: How to test components using IOC?
|
|
1297
|
+
|
|
1298
|
+
**A:** Provide mock IOC:
|
|
1299
|
+
|
|
1300
|
+
```typescript
|
|
1301
|
+
const mockIOC = (identifier: string) => {
|
|
1302
|
+
if (identifier === 'UserServiceInterface') {
|
|
1303
|
+
return mockUserService;
|
|
1304
|
+
}
|
|
1305
|
+
// ... other services
|
|
1306
|
+
};
|
|
1307
|
+
|
|
1308
|
+
render(
|
|
1309
|
+
<IOCProvider value={mockIOC}>
|
|
1310
|
+
<UserProfile />
|
|
1311
|
+
</IOCProvider>
|
|
1312
|
+
);
|
|
1313
|
+
```
|
|
1314
|
+
|
|
1315
|
+
### Q5: What's the difference between IOC and Context?
|
|
1316
|
+
|
|
1317
|
+
**A:**
|
|
1318
|
+
|
|
1319
|
+
| Feature | React Context | IOC Container |
|
|
1320
|
+
| ------------------------- | ------------------------- | --------------------- |
|
|
1321
|
+
| **Scope** | React component tree | Global |
|
|
1322
|
+
| **Dependency Management** | ❌ None | ✅ Yes |
|
|
1323
|
+
| **Lifecycle** | Component lifecycle | Application lifecycle |
|
|
1324
|
+
| **Testing** | ⚠️ Need Provider | ✅ Easy to mock |
|
|
1325
|
+
| **Type Safety** | ⚠️ Need manual definition | ✅ Auto-inference |
|
|
1326
|
+
|
|
1327
|
+
**Recommendation:**
|
|
1328
|
+
|
|
1329
|
+
- Use IOC to manage services (logic)
|
|
1330
|
+
- Use Context to manage UI state
|
|
1331
|
+
|
|
1332
|
+
---
|
|
1333
|
+
|
|
1334
|
+
## 📚 Related Documentation
|
|
1335
|
+
|
|
1336
|
+
- [Project Architecture Design](./index.md) - Understand overall architecture
|
|
1337
|
+
- [Bootstrap Initializer](./bootstrap.md) - IOC in Bootstrap application
|
|
1338
|
+
- [Environment Variable Management](./env.md) - AppConfig injection
|
|
1339
|
+
- [Store State Management](./store.md) - How application layer notifies UI layer (IOC + Store)
|
|
1340
|
+
- [Testing Guide](./test-guide.md) - Detailed testing strategies
|
|
1341
|
+
|
|
1342
|
+
---
|
|
1343
|
+
|
|
1344
|
+
## 🎉 Summary
|
|
1345
|
+
|
|
1346
|
+
Core value of IOC container:
|
|
1347
|
+
|
|
1348
|
+
1. **UI Separation** 🎨 - UI is UI, logic is logic
|
|
1349
|
+
2. **Testability** 🧪 - Logic can be tested independently, UI can be tested independently, and also in combination
|
|
1350
|
+
3. **Interfaces Required** 🔌 - Even with only one implementation, still need interface (for testing)
|
|
1351
|
+
4. **Complete Separation** 🏗️ - Even simple components, still separate (for future)
|
|
1352
|
+
5. **Dependency Management** 📦 - Container uniformly manages all dependencies
|
|
1353
|
+
6. **Decoupling** 🔗 - Components don't depend on concrete implementations
|
|
1354
|
+
7. **Easy to Maintain** 🛠️ - Clear dependency relationships
|
|
1355
|
+
8. **Easy to Extend** 🚀 - Easy to add new features
|
|
1356
|
+
|
|
1357
|
+
**Remember two core principles:**
|
|
415
1358
|
|
|
416
|
-
|
|
1359
|
+
1. **UI is UI, logic is logic, they must be separated!**
|
|
1360
|
+
2. **Even with only one implementation, still need interface; even if component is simple, still separate!**
|
|
417
1361
|
|
|
418
|
-
|
|
419
|
-
2. **Type Safety**: Compile-time type checking through TypeScript
|
|
420
|
-
3. **Testability**: Easy to unit test and mock
|
|
421
|
-
4. **Maintainability**: Clear dependency relationships, easy to understand and modify
|
|
422
|
-
5. **Extensibility**: Easy to add new services and dependencies
|
|
1362
|
+
---
|
|
423
1363
|
|
|
424
|
-
|
|
1364
|
+
**Feedback:**
|
|
1365
|
+
If you have any questions or suggestions about the IOC container, please discuss in the team channel or submit an Issue.
|