@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,168 +1,368 @@
|
|
|
1
1
|
# Store 状态管理
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 📋 目录
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
- [核心理念](#-核心理念) - 应用层通知 UI 层
|
|
6
|
+
- [什么是 Store](#-什么是-store) - 状态容器
|
|
7
|
+
- [为什么需要 Store](#-为什么需要-store) - 解决通信问题
|
|
8
|
+
- [核心问题](#-核心问题) - 应用层如何通知 UI 层
|
|
9
|
+
- [项目中的实现](#-项目中的实现) - 实战指南
|
|
10
|
+
- [使用方式](#-使用方式) - Service + Store + useStore
|
|
11
|
+
- [测试](#-测试) - 独立测试和组合测试
|
|
12
|
+
- [最佳实践](#-最佳实践) - 7 条核心实践
|
|
13
|
+
- [常见问题](#-常见问题) - FAQ
|
|
6
14
|
|
|
7
|
-
|
|
8
|
-
- 业务逻辑集中在 Store 中管理
|
|
9
|
-
- UI 组件只负责渲染和用户交互
|
|
10
|
-
- 通过 IOC 容器实现逻辑的依赖注入
|
|
15
|
+
---
|
|
11
16
|
|
|
12
|
-
|
|
13
|
-
- 基于发布订阅模式
|
|
14
|
-
- 状态变更自动触发 UI 更新
|
|
15
|
-
- 精确的组件重渲染控制
|
|
17
|
+
## 🎯 核心理念
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
- 将复杂状态分解为独立的分片
|
|
19
|
-
- 每个分片负责特定的业务领域
|
|
20
|
-
- 分片之间可以组合和通信
|
|
19
|
+
> **🚨 核心问题:应用层(Service)如何通知 UI 层更新,同时保持分离?**
|
|
21
20
|
|
|
22
|
-
|
|
21
|
+
> **⭐ 解决方案:Service 包含 Store,通过 `emit` 发布状态,UI 通过 `useStore` 订阅状态!**
|
|
23
22
|
|
|
24
|
-
###
|
|
23
|
+
### 核心概念
|
|
25
24
|
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
```
|
|
26
|
+
┌──────────────────────────────────────────────┐
|
|
27
|
+
│ 问题:UI 和逻辑已经分离了,但如何通信? │
|
|
28
|
+
│ │
|
|
29
|
+
│ Service (应用层) │
|
|
30
|
+
│ ├── 业务逻辑 │
|
|
31
|
+
│ └── 数据处理 │
|
|
32
|
+
│ ↓ 如何通知? │
|
|
33
|
+
│ Component (UI 层) │
|
|
34
|
+
│ └── UI 渲染 │
|
|
35
|
+
│ │
|
|
36
|
+
│ ❌ 问题:Service 改变了数据,UI 如何知道? │
|
|
37
|
+
└──────────────────────────────────────────────┘
|
|
38
|
+
|
|
39
|
+
┌──────────────────────────────────────────────┐
|
|
40
|
+
│ 解决方案:Store 作为桥梁 │
|
|
41
|
+
│ │
|
|
42
|
+
│ Service (应用层) │
|
|
43
|
+
│ ├── 业务逻辑 │
|
|
44
|
+
│ ├── Store (状态容器) │
|
|
45
|
+
│ │ ├── state (状态) │
|
|
46
|
+
│ │ └── emit() (发布状态) │
|
|
47
|
+
│ │ │
|
|
48
|
+
│ │ ↓ 发布订阅模式 │
|
|
49
|
+
│ │ │
|
|
50
|
+
│ └── useStore (订阅) │
|
|
51
|
+
│ ↓ │
|
|
52
|
+
│ Component (UI 层) │
|
|
53
|
+
│ └── 自动更新 UI │
|
|
54
|
+
│ │
|
|
55
|
+
│ ✅ Service 通过 emit 发布状态 │
|
|
56
|
+
│ ✅ UI 通过 useStore 订阅状态 │
|
|
57
|
+
│ ✅ 保持分离,解耦合 │
|
|
58
|
+
└──────────────────────────────────────────────┘
|
|
59
|
+
```
|
|
30
60
|
|
|
31
|
-
|
|
32
|
-
protected emit(newState: T) {
|
|
33
|
-
this.state = newState;
|
|
34
|
-
this.listeners.forEach((listener) => listener(this.state));
|
|
35
|
-
}
|
|
61
|
+
---
|
|
36
62
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
63
|
+
## 🗂️ 什么是 Store
|
|
64
|
+
|
|
65
|
+
Store 是一个**响应式状态容器**,基于**发布订阅模式**实现。
|
|
66
|
+
|
|
67
|
+
### 简单理解
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
Store = 状态 + 发布订阅
|
|
71
|
+
|
|
72
|
+
Service 拥有 Store
|
|
73
|
+
Service 通过 Store.emit() 发布状态
|
|
74
|
+
UI 通过 useStore() 订阅状态
|
|
43
75
|
```
|
|
44
76
|
|
|
45
|
-
###
|
|
77
|
+
### 类比理解
|
|
46
78
|
|
|
47
79
|
```
|
|
48
|
-
|
|
80
|
+
Store 就像一个广播电台:
|
|
81
|
+
|
|
82
|
+
📻 电台(Store)
|
|
83
|
+
- 有节目内容(state)
|
|
84
|
+
- 可以广播节目(emit)
|
|
85
|
+
- 听众可以收听(subscribe)
|
|
86
|
+
|
|
87
|
+
🎤 主持人(Service)
|
|
88
|
+
- 制作节目内容(业务逻辑)
|
|
89
|
+
- 通过电台广播(emit)
|
|
90
|
+
|
|
91
|
+
📱 听众(UI Component)
|
|
92
|
+
- 收听电台(useStore)
|
|
93
|
+
- 听到新内容自动反应(自动更新 UI)
|
|
49
94
|
```
|
|
50
95
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## 🤔 为什么需要 Store
|
|
99
|
+
|
|
100
|
+
### 核心问题:UI 和逻辑分离后,如何通信?
|
|
101
|
+
|
|
102
|
+
我们已经通过 IOC 实现了 UI 和逻辑分离,但问题来了:
|
|
56
103
|
|
|
57
|
-
|
|
104
|
+
#### ❌ 问题示例:没有 Store
|
|
58
105
|
|
|
59
|
-
```
|
|
60
|
-
//
|
|
106
|
+
```typescript
|
|
107
|
+
// Service(逻辑层)
|
|
108
|
+
@injectable()
|
|
109
|
+
export class UserService {
|
|
110
|
+
private user: UserInfo | null = null;
|
|
111
|
+
|
|
112
|
+
async login(username: string, password: string) {
|
|
113
|
+
const response = await this.api.login({ username, password });
|
|
114
|
+
this.user = response.user; // ✅ 登录成功,user 已更新
|
|
115
|
+
|
|
116
|
+
// ❌ 问题:UI 如何知道 user 已经更新?
|
|
117
|
+
// ❌ Service 无法通知 UI
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// UI 组件
|
|
61
122
|
function UserProfile() {
|
|
62
|
-
|
|
63
|
-
|
|
123
|
+
const userService = useIOC('UserServiceInterface');
|
|
124
|
+
|
|
125
|
+
// ❌ 问题:如何获取 userService.user?
|
|
126
|
+
// ❌ userService.user 更新后,如何触发 UI 重新渲染?
|
|
64
127
|
|
|
65
|
-
return <div>{user
|
|
128
|
+
return <div>{/* 无法显示 user */}</div>;
|
|
66
129
|
}
|
|
130
|
+
|
|
131
|
+
// 😰😰😰 问题总结:
|
|
132
|
+
// 1. UI 无法获取 Service 的内部状态
|
|
133
|
+
// 2. Service 状态更新后,UI 不知道
|
|
134
|
+
// 3. 需要手动调用某个方法来获取状态?(打破分离原则)
|
|
135
|
+
// 4. 需要轮询检查状态?(性能差)
|
|
67
136
|
```
|
|
68
137
|
|
|
69
|
-
|
|
138
|
+
#### ✅ 解决方案:使用 Store
|
|
70
139
|
|
|
71
140
|
```typescript
|
|
72
|
-
//
|
|
73
|
-
|
|
141
|
+
// Service(逻辑层)
|
|
142
|
+
@injectable()
|
|
143
|
+
export class UserService extends StoreInterface<UserState> {
|
|
74
144
|
constructor() {
|
|
75
145
|
super(() => ({
|
|
76
|
-
|
|
77
|
-
|
|
146
|
+
user: null,
|
|
147
|
+
loading: false
|
|
78
148
|
}));
|
|
79
149
|
}
|
|
80
150
|
|
|
81
|
-
login(
|
|
82
|
-
//
|
|
151
|
+
async login(username: string, password: string) {
|
|
152
|
+
// 设置加载状态
|
|
153
|
+
this.emit({ ...this.state, loading: true });
|
|
154
|
+
|
|
155
|
+
const response = await this.api.login({ username, password });
|
|
156
|
+
|
|
157
|
+
// ✅ 通过 emit 发布新状态,自动通知所有订阅者
|
|
83
158
|
this.emit({
|
|
84
|
-
|
|
85
|
-
|
|
159
|
+
user: response.user,
|
|
160
|
+
loading: false
|
|
86
161
|
});
|
|
87
162
|
}
|
|
88
163
|
}
|
|
89
164
|
|
|
90
|
-
//
|
|
91
|
-
|
|
165
|
+
// UI 组件
|
|
166
|
+
function UserProfile() {
|
|
167
|
+
const userService = useIOC('UserServiceInterface');
|
|
168
|
+
|
|
169
|
+
// ✅ 通过 useStore 订阅状态
|
|
170
|
+
const { user, loading } = useStore(userService);
|
|
171
|
+
|
|
172
|
+
// ✅ userService.emit() 时,组件会自动重新渲染
|
|
173
|
+
|
|
174
|
+
if (loading) return <div>Loading...</div>;
|
|
175
|
+
return <div>{user?.name}</div>;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ✅✅✅ 优势总结:
|
|
179
|
+
// 1. UI 可以订阅 Service 的状态
|
|
180
|
+
// 2. Service 状态更新后,UI 自动更新
|
|
181
|
+
// 3. 保持分离(Service 不知道有哪些 UI 在监听)
|
|
182
|
+
// 4. 高性能(只有订阅的组件才会更新)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### 对比总结
|
|
186
|
+
|
|
187
|
+
| 特性 | 没有 Store | 有 Store |
|
|
188
|
+
| ---------------- | ---------------------- | --------------------- |
|
|
189
|
+
| **状态获取** | ❌ 无法获取内部状态 | ✅ 通过 useStore 订阅 |
|
|
190
|
+
| **状态更新通知** | ❌ UI 不知道状态变化 | ✅ emit 自动通知 |
|
|
191
|
+
| **UI 更新** | ❌ 需要手动触发 | ✅ 自动重新渲染 |
|
|
192
|
+
| **解耦** | ❌ Service 需要知道 UI | ✅ 完全解耦 |
|
|
193
|
+
| **性能** | ❌ 轮询或全局更新 | ✅ 精确更新订阅者 |
|
|
194
|
+
| **可测试性** | ❌ 难以测试状态变化 | ✅ 易于测试状态 |
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## ❓ 核心问题
|
|
199
|
+
|
|
200
|
+
### 应用层如何通知 UI 层,同时保持分离?
|
|
201
|
+
|
|
202
|
+
#### 问题拆解
|
|
203
|
+
|
|
204
|
+
1. **应用层(Service)有状态** - 如用户信息、加载状态
|
|
205
|
+
2. **UI 层需要显示这些状态** - 显示用户名、显示加载动画
|
|
206
|
+
3. **应用层状态会变化** - 登录成功后,用户信息更新
|
|
207
|
+
4. **UI 层需要自动更新** - 用户信息变化后,UI 自动显示新名字
|
|
208
|
+
5. **保持分离** - Service 不应该直接操作 UI,UI 不应该直接访问 Service 内部
|
|
209
|
+
|
|
210
|
+
#### 解决方案:发布订阅模式
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
// 1. Service 定义状态
|
|
214
|
+
interface UserState {
|
|
215
|
+
user: UserInfo | null;
|
|
216
|
+
loading: boolean;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// 2. Service 继承 StoreInterface
|
|
220
|
+
@injectable()
|
|
221
|
+
export class UserService extends StoreInterface<UserState> {
|
|
92
222
|
constructor() {
|
|
93
223
|
super(() => ({
|
|
94
|
-
|
|
95
|
-
|
|
224
|
+
user: null,
|
|
225
|
+
loading: false
|
|
96
226
|
}));
|
|
97
227
|
}
|
|
98
228
|
|
|
99
|
-
|
|
100
|
-
|
|
229
|
+
// 3. Service 通过 emit 发布状态
|
|
230
|
+
async login(username: string, password: string) {
|
|
231
|
+
this.emit({ ...this.state, loading: true }); // 发布:开始加载
|
|
232
|
+
|
|
233
|
+
const response = await this.api.login({ username, password });
|
|
234
|
+
|
|
101
235
|
this.emit({
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
});
|
|
236
|
+
user: response.user,
|
|
237
|
+
loading: false
|
|
238
|
+
}); // 发布:加载完成,用户已登录
|
|
105
239
|
}
|
|
240
|
+
|
|
241
|
+
// 4. Service 不需要知道谁在监听
|
|
242
|
+
// ✅ 完全解耦
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// 5. UI 通过 useStore 订阅状态
|
|
246
|
+
function LoginPage() {
|
|
247
|
+
const userService = useIOC('UserServiceInterface');
|
|
248
|
+
const { loading } = useStore(userService);
|
|
249
|
+
|
|
250
|
+
const handleLogin = () => {
|
|
251
|
+
userService.login('user', 'pass');
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
// 6. 当 Service emit 新状态时,UI 自动更新
|
|
255
|
+
return (
|
|
256
|
+
<button onClick={handleLogin} disabled={loading}>
|
|
257
|
+
{loading ? 'Logging in...' : 'Login'}
|
|
258
|
+
</button>
|
|
259
|
+
);
|
|
106
260
|
}
|
|
107
261
|
```
|
|
108
262
|
|
|
109
|
-
|
|
263
|
+
#### 工作流程
|
|
110
264
|
|
|
111
|
-
|
|
265
|
+
```
|
|
266
|
+
┌─────────────────────────────────────────────┐
|
|
267
|
+
│ 完整的状态更新流程 │
|
|
268
|
+
│ │
|
|
269
|
+
│ 1. 用户点击按钮 │
|
|
270
|
+
│ ↓ │
|
|
271
|
+
│ 2. UI 调用 Service 方法 │
|
|
272
|
+
│ userService.login() │
|
|
273
|
+
│ ↓ │
|
|
274
|
+
│ 3. Service 执行业务逻辑 │
|
|
275
|
+
│ - 调用 API │
|
|
276
|
+
│ - 处理数据 │
|
|
277
|
+
│ ↓ │
|
|
278
|
+
│ 4. Service 通过 emit 发布新状态 │
|
|
279
|
+
│ this.emit({ user: ..., loading: false })│
|
|
280
|
+
│ ↓ │
|
|
281
|
+
│ 5. Store 通知所有订阅者 │
|
|
282
|
+
│ listeners.forEach(listener => ...) │
|
|
283
|
+
│ ↓ │
|
|
284
|
+
│ 6. useStore 收到通知 │
|
|
285
|
+
│ 触发组件重新渲染 │
|
|
286
|
+
│ ↓ │
|
|
287
|
+
│ 7. UI 显示最新状态 │
|
|
288
|
+
│ 显示用户名 / 隐藏加载动画 │
|
|
289
|
+
└─────────────────────────────────────────────┘
|
|
290
|
+
```
|
|
112
291
|
|
|
113
|
-
|
|
114
|
-
- **轻量级**:无需复杂的配置,易于使用
|
|
115
|
-
- **高性能**:精确的组件更新,避免不必要的渲染
|
|
116
|
-
- **模块化**:支持状态分片,便于管理大型应用
|
|
117
|
-
- **IOC 集成**:与依赖注入系统完美配合
|
|
292
|
+
---
|
|
118
293
|
|
|
119
|
-
##
|
|
294
|
+
## 🛠️ 项目中的实现
|
|
120
295
|
|
|
121
|
-
### 1.
|
|
296
|
+
### 1. 文件结构
|
|
122
297
|
|
|
123
|
-
|
|
298
|
+
```
|
|
299
|
+
src/
|
|
300
|
+
├── base/
|
|
301
|
+
│ ├── services/
|
|
302
|
+
│ │ ├── UserService.ts # Service 继承 StoreInterface
|
|
303
|
+
│ │ ├── RouteService.ts # Service 继承 StoreInterface
|
|
304
|
+
│ │ └── I18nService.ts # Service 继承 StoreInterface
|
|
305
|
+
│ └── port/
|
|
306
|
+
│ └── UserServiceInterface.ts # Service 接口
|
|
307
|
+
└── uikit/
|
|
308
|
+
└── hooks/
|
|
309
|
+
└── useStore.ts (from @brain-toolkit/react-kit)
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### 2. Store 基类
|
|
124
313
|
|
|
125
|
-
|
|
314
|
+
Store 系统基于 `@brain-toolkit/react-kit` 的 `SliceStore`:
|
|
126
315
|
|
|
127
316
|
```typescript
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
//
|
|
138
|
-
|
|
317
|
+
// 来自 @brain-toolkit/react-kit
|
|
318
|
+
export class SliceStore<T> {
|
|
319
|
+
protected state: T;
|
|
320
|
+
private listeners = new Set<(state: T) => void>();
|
|
321
|
+
|
|
322
|
+
constructor(stateFactory: () => T) {
|
|
323
|
+
this.state = stateFactory();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// 发布状态
|
|
327
|
+
protected emit(newState: T) {
|
|
328
|
+
this.state = newState;
|
|
329
|
+
// 通知所有订阅者
|
|
330
|
+
this.listeners.forEach((listener) => listener(this.state));
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// 订阅状态
|
|
334
|
+
subscribe(listener: (state: T) => void) {
|
|
335
|
+
this.listeners.add(listener);
|
|
336
|
+
// 返回取消订阅函数
|
|
337
|
+
return () => this.listeners.delete(listener);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// 获取当前状态
|
|
341
|
+
getState(): T {
|
|
342
|
+
return this.state;
|
|
343
|
+
}
|
|
139
344
|
}
|
|
140
345
|
```
|
|
141
346
|
|
|
142
|
-
|
|
347
|
+
### 3. StoreInterface 基类
|
|
348
|
+
|
|
349
|
+
项目中的 Store 基类,提供额外的工具方法:
|
|
143
350
|
|
|
144
351
|
```typescript
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
*
|
|
148
|
-
* 作用:所有状态存储的抽象基类
|
|
149
|
-
* 核心思想:提供统一的状态管理 API,包含重置和克隆辅助方法
|
|
150
|
-
* 主要功能:扩展 SliceStore,添加 resetState 和 cloneState 工具方法
|
|
151
|
-
* 主要目的:简化 store 实现并确保一致性
|
|
152
|
-
*/
|
|
153
|
-
abstract class StoreInterface<
|
|
352
|
+
// 来自 @qlover/corekit-bridge
|
|
353
|
+
export abstract class StoreInterface<
|
|
154
354
|
T extends StoreStateInterface
|
|
155
355
|
> extends SliceStore<T> {
|
|
156
356
|
constructor(protected stateFactory: () => T) {
|
|
157
357
|
super(stateFactory);
|
|
158
358
|
}
|
|
159
359
|
|
|
160
|
-
//
|
|
360
|
+
// 重置状态
|
|
161
361
|
resetState(): void {
|
|
162
362
|
this.emit(this.stateFactory());
|
|
163
363
|
}
|
|
164
364
|
|
|
165
|
-
//
|
|
365
|
+
// 克隆状态(用于更新)
|
|
166
366
|
cloneState(source?: Partial<T>): T {
|
|
167
367
|
const cloned = clone(this.state);
|
|
168
368
|
if (typeof cloned === 'object' && cloned !== null) {
|
|
@@ -173,149 +373,957 @@ abstract class StoreInterface<
|
|
|
173
373
|
}
|
|
174
374
|
```
|
|
175
375
|
|
|
176
|
-
###
|
|
376
|
+
### 4. 状态接口
|
|
377
|
+
|
|
378
|
+
```typescript
|
|
379
|
+
// 所有状态必须实现此接口
|
|
380
|
+
export interface StoreStateInterface {
|
|
381
|
+
// 可以在这里定义通用属性
|
|
382
|
+
// loading?: boolean;
|
|
383
|
+
// error?: Error | null;
|
|
384
|
+
}
|
|
385
|
+
```
|
|
177
386
|
|
|
178
|
-
|
|
387
|
+
---
|
|
388
|
+
|
|
389
|
+
## 📝 使用方式
|
|
390
|
+
|
|
391
|
+
### 1. 定义状态接口
|
|
179
392
|
|
|
180
393
|
```typescript
|
|
181
|
-
//
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
role: string;
|
|
187
|
-
} | null = null;
|
|
394
|
+
// src/base/services/UserService.ts
|
|
395
|
+
export interface UserState extends StoreStateInterface {
|
|
396
|
+
user: UserInfo | null;
|
|
397
|
+
loading: boolean;
|
|
398
|
+
error: Error | null;
|
|
188
399
|
}
|
|
400
|
+
```
|
|
189
401
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
402
|
+
### 2. Service 继承 StoreInterface
|
|
403
|
+
|
|
404
|
+
```typescript
|
|
405
|
+
// src/base/services/UserService.ts
|
|
406
|
+
import { StoreInterface } from '@qlover/corekit-bridge';
|
|
407
|
+
import { injectable, inject } from 'inversify';
|
|
408
|
+
|
|
409
|
+
@injectable()
|
|
410
|
+
export class UserService extends StoreInterface<UserState> {
|
|
411
|
+
constructor(
|
|
412
|
+
@inject(UserApi) private api: UserApi,
|
|
413
|
+
@inject(IOCIdentifier.AppConfig) private config: AppConfig
|
|
414
|
+
) {
|
|
415
|
+
// 初始化状态
|
|
416
|
+
super(() => ({
|
|
417
|
+
user: null,
|
|
418
|
+
loading: false,
|
|
419
|
+
error: null
|
|
420
|
+
}));
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// 业务方法:通过 emit 发布状态
|
|
424
|
+
async login(username: string, password: string) {
|
|
425
|
+
// 1. 开始加载
|
|
426
|
+
this.emit({
|
|
427
|
+
...this.state,
|
|
428
|
+
loading: true,
|
|
429
|
+
error: null
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
try {
|
|
433
|
+
// 2. 调用 API
|
|
434
|
+
const response = await this.api.login({ username, password });
|
|
435
|
+
|
|
436
|
+
// 3. 成功:发布新状态
|
|
437
|
+
this.emit({
|
|
438
|
+
user: response.user,
|
|
439
|
+
loading: false,
|
|
440
|
+
error: null
|
|
441
|
+
});
|
|
442
|
+
} catch (error) {
|
|
443
|
+
// 4. 失败:发布错误状态
|
|
444
|
+
this.emit({
|
|
445
|
+
...this.state,
|
|
446
|
+
loading: false,
|
|
447
|
+
error: error as Error
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async logout() {
|
|
453
|
+
this.emit({
|
|
454
|
+
user: null,
|
|
455
|
+
loading: false,
|
|
456
|
+
error: null
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// 使用 cloneState 简化更新
|
|
461
|
+
setUser(user: UserInfo) {
|
|
462
|
+
this.emit(this.cloneState({ user }));
|
|
194
463
|
}
|
|
195
464
|
}
|
|
196
465
|
```
|
|
197
466
|
|
|
198
|
-
|
|
467
|
+
### 3. UI 订阅状态
|
|
199
468
|
|
|
200
|
-
|
|
469
|
+
```typescript
|
|
470
|
+
// src/pages/LoginPage.tsx
|
|
471
|
+
import { useStore } from '@brain-toolkit/react-kit/hooks/useStore';
|
|
472
|
+
import { useIOC } from '@/uikit/hooks/useIOC';
|
|
473
|
+
|
|
474
|
+
function LoginPage() {
|
|
475
|
+
const userService = useIOC('UserServiceInterface');
|
|
476
|
+
|
|
477
|
+
// ✅ 方式 1:订阅完整状态
|
|
478
|
+
const { user, loading, error } = useStore(userService);
|
|
479
|
+
|
|
480
|
+
const handleLogin = async () => {
|
|
481
|
+
await userService.login('username', 'password');
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
if (loading) {
|
|
485
|
+
return <div>Loading...</div>;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return (
|
|
489
|
+
<div>
|
|
490
|
+
{error && <div>Error: {error.message}</div>}
|
|
491
|
+
<button onClick={handleLogin}>Login</button>
|
|
492
|
+
</div>
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
### 4. 使用选择器(性能优化)
|
|
201
498
|
|
|
202
499
|
```typescript
|
|
203
|
-
|
|
500
|
+
// src/pages/UserProfile.tsx
|
|
501
|
+
function UserProfile() {
|
|
502
|
+
const userService = useIOC('UserServiceInterface');
|
|
204
503
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
504
|
+
// ✅ 方式 2:只订阅需要的状态(性能更好)
|
|
505
|
+
const user = useStore(userService, (state) => state.user);
|
|
506
|
+
|
|
507
|
+
// ✅ 只有 user 变化时才重新渲染,loading 变化不会触发
|
|
508
|
+
|
|
509
|
+
return <div>{user?.name}</div>;
|
|
208
510
|
}
|
|
511
|
+
```
|
|
209
512
|
|
|
513
|
+
### 5. 定义选择器(推荐)
|
|
514
|
+
|
|
515
|
+
```typescript
|
|
516
|
+
// src/base/services/UserService.ts
|
|
210
517
|
@injectable()
|
|
211
|
-
export class
|
|
212
|
-
|
|
213
|
-
super(() => ({
|
|
214
|
-
helloState: '',
|
|
215
|
-
tasks: []
|
|
216
|
-
}));
|
|
217
|
-
}
|
|
518
|
+
export class UserService extends StoreInterface<UserState> {
|
|
519
|
+
// ... 其他代码
|
|
218
520
|
|
|
219
|
-
//
|
|
521
|
+
// ✅ 定义选择器
|
|
220
522
|
selector = {
|
|
221
|
-
|
|
222
|
-
|
|
523
|
+
user: (state: UserState) => state.user,
|
|
524
|
+
loading: (state: UserState) => state.loading,
|
|
525
|
+
error: (state: UserState) => state.error,
|
|
526
|
+
isLoggedIn: (state: UserState) => state.user !== null
|
|
223
527
|
};
|
|
224
528
|
}
|
|
529
|
+
|
|
530
|
+
// 使用
|
|
531
|
+
function UserProfile() {
|
|
532
|
+
const userService = useIOC('UserServiceInterface');
|
|
533
|
+
|
|
534
|
+
// ✅ 使用预定义的选择器
|
|
535
|
+
const user = useStore(userService, userService.selector.user);
|
|
536
|
+
const isLoggedIn = useStore(userService, userService.selector.isLoggedIn);
|
|
537
|
+
|
|
538
|
+
return <div>{isLoggedIn ? user?.name : 'Please login'}</div>;
|
|
539
|
+
}
|
|
225
540
|
```
|
|
226
541
|
|
|
227
|
-
###
|
|
542
|
+
### 6. 实际项目示例
|
|
228
543
|
|
|
229
|
-
|
|
544
|
+
#### 示例 1:UserService
|
|
230
545
|
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
546
|
+
```typescript
|
|
547
|
+
// src/base/services/UserService.ts
|
|
548
|
+
@injectable()
|
|
549
|
+
export class UserService extends UserAuthServiceInterface {
|
|
550
|
+
constructor(
|
|
551
|
+
@inject(UserApi) userApi: UserApi,
|
|
552
|
+
@inject(IOCIdentifier.AppConfig) appConfig: AppConfig,
|
|
553
|
+
@inject(IOCIdentifier.LocalStorageEncrypt) storage: Storage
|
|
554
|
+
) {
|
|
555
|
+
super(userApi, {
|
|
556
|
+
userStorage: {
|
|
557
|
+
key: appConfig.userInfoStorageKey,
|
|
558
|
+
storage: storage
|
|
559
|
+
},
|
|
560
|
+
credentialStorage: {
|
|
561
|
+
key: appConfig.userTokenStorageKey,
|
|
562
|
+
storage: storage
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
}
|
|
235
566
|
|
|
236
|
-
//
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
567
|
+
// ✅ UserService 继承的基类包含 store
|
|
568
|
+
override get store(): UserAuthStore<UserApiState> {
|
|
569
|
+
return super.store as UserAuthStore<UserApiState>;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
override async logout(): Promise<void> {
|
|
573
|
+
await super.logout();
|
|
574
|
+
// ✅ store 会自动通知 UI
|
|
575
|
+
this.routerService.gotoLogin();
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// 使用
|
|
580
|
+
function Layout() {
|
|
581
|
+
const userService = useIOC(IOCIdentifier.UserServiceInterface);
|
|
582
|
+
|
|
583
|
+
// ✅ 订阅 userService.store
|
|
584
|
+
useStore(userService.store);
|
|
585
|
+
|
|
586
|
+
if (userService.isAuthenticated()) {
|
|
587
|
+
return <Navigate to="/" replace />;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
return <Outlet />;
|
|
591
|
+
}
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
#### 示例 2:RouteService
|
|
595
|
+
|
|
596
|
+
```typescript
|
|
597
|
+
// src/base/services/RouteService.ts
|
|
598
|
+
export class RouteService extends StoreInterface<RouterServiceState> {
|
|
599
|
+
constructor(
|
|
600
|
+
protected uiBridge: UIBridgeInterface<NavigateFunction>,
|
|
601
|
+
protected i18nService: I18nServiceInterface,
|
|
602
|
+
protected options: RouterServiceOptions
|
|
603
|
+
) {
|
|
604
|
+
super(
|
|
605
|
+
() => new RouterServiceState(options.routes, !!options.hasLocalRoutes)
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// ✅ 通过 emit 发布路由变化
|
|
610
|
+
override changeRoutes(routes: RouteConfigValue[]): void {
|
|
611
|
+
this.emit(this.cloneState({ routes }));
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
override goto(path: string, options?: NavigateOptions): void {
|
|
615
|
+
const composedPath = this.composePath(path);
|
|
616
|
+
this.uiBridge.getUIBridge()(composedPath, options);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// 使用
|
|
621
|
+
function AppRouterProvider() {
|
|
622
|
+
const routerService = useIOC(IOCIdentifier.RouteServiceInterface);
|
|
623
|
+
|
|
624
|
+
// ✅ 订阅 routes 变化
|
|
625
|
+
const routes = useStore(routerService, (state) => state.routes);
|
|
626
|
+
|
|
627
|
+
const router = createBrowserRouter(routes);
|
|
628
|
+
|
|
629
|
+
return <RouterProvider router={router} />;
|
|
630
|
+
}
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
#### 示例 3:I18nService
|
|
634
|
+
|
|
635
|
+
```typescript
|
|
636
|
+
// src/base/services/I18nService.ts
|
|
637
|
+
export class I18nService extends StoreInterface<I18nServiceState> {
|
|
638
|
+
constructor(protected pathname: string) {
|
|
639
|
+
super(() => new I18nServiceState(i18n.language as I18nServiceLocale));
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
selector = {
|
|
643
|
+
loading: (state: I18nServiceState) => state.loading,
|
|
644
|
+
language: (state: I18nServiceState) => state.language
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
override async changeLanguage(lng: string): Promise<void> {
|
|
648
|
+
// ✅ 发布加载状态
|
|
649
|
+
this.emit(this.cloneState({ loading: true }));
|
|
650
|
+
|
|
651
|
+
await i18n.changeLanguage(lng);
|
|
652
|
+
|
|
653
|
+
// ✅ 发布完成状态
|
|
654
|
+
this.emit({
|
|
655
|
+
language: lng as I18nServiceLocale,
|
|
656
|
+
loading: false
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// 使用
|
|
662
|
+
function LanguageSwitcher() {
|
|
663
|
+
const i18nService = useIOC(IOCIdentifier.I18nServiceInterface);
|
|
664
|
+
|
|
665
|
+
// ✅ 只订阅 loading 状态
|
|
666
|
+
const loading = useStore(i18nService, i18nService.selector.loading);
|
|
241
667
|
|
|
242
668
|
return (
|
|
243
|
-
<
|
|
244
|
-
|
|
245
|
-
|
|
669
|
+
<Select
|
|
670
|
+
value={i18n.language}
|
|
671
|
+
loading={loading}
|
|
672
|
+
onChange={(lng) => i18nService.changeLanguage(lng)}
|
|
673
|
+
/>
|
|
246
674
|
);
|
|
247
675
|
}
|
|
248
676
|
```
|
|
249
677
|
|
|
250
|
-
|
|
678
|
+
---
|
|
679
|
+
|
|
680
|
+
## 🧪 测试
|
|
681
|
+
|
|
682
|
+
### 核心优势:Store 可以独立测试,UI 可以 mock Store
|
|
251
683
|
|
|
252
|
-
|
|
684
|
+
#### 1. 测试 Service 和 Store(逻辑测试)
|
|
253
685
|
|
|
254
686
|
```typescript
|
|
687
|
+
// __tests__/src/base/services/UserService.test.ts
|
|
688
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
689
|
+
import { UserService } from '@/base/services/UserService';
|
|
690
|
+
|
|
691
|
+
describe('UserService (逻辑测试)', () => {
|
|
692
|
+
let userService: UserService;
|
|
693
|
+
let mockApi: any;
|
|
694
|
+
|
|
695
|
+
beforeEach(() => {
|
|
696
|
+
mockApi = {
|
|
697
|
+
login: vi.fn(),
|
|
698
|
+
getUserInfo: vi.fn()
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
userService = new UserService(mockApi, mockConfig, mockStorage);
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
it('should update store state when login success', async () => {
|
|
705
|
+
// ✅ 测试状态变化
|
|
706
|
+
mockApi.login.mockResolvedValue({
|
|
707
|
+
user: { name: 'John', id: 1 },
|
|
708
|
+
token: 'test-token'
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
// 订阅状态变化
|
|
712
|
+
const states: any[] = [];
|
|
713
|
+
userService.subscribe((state) => {
|
|
714
|
+
states.push({ ...state });
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
// 调用登录
|
|
718
|
+
await userService.login('user', 'pass');
|
|
719
|
+
|
|
720
|
+
// ✅ 验证状态变化序列
|
|
721
|
+
expect(states).toHaveLength(2);
|
|
722
|
+
|
|
723
|
+
// 第一次 emit:loading = true
|
|
724
|
+
expect(states[0]).toEqual({
|
|
725
|
+
user: null,
|
|
726
|
+
loading: true,
|
|
727
|
+
error: null
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
// 第二次 emit:loading = false, user = John
|
|
731
|
+
expect(states[1]).toEqual({
|
|
732
|
+
user: { name: 'John', id: 1 },
|
|
733
|
+
loading: false,
|
|
734
|
+
error: null
|
|
735
|
+
});
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
it('should update store state when login fails', async () => {
|
|
739
|
+
mockApi.login.mockRejectedValue(new Error('Invalid credentials'));
|
|
740
|
+
|
|
741
|
+
const states: any[] = [];
|
|
742
|
+
userService.subscribe((state) => states.push({ ...state }));
|
|
743
|
+
|
|
744
|
+
await expect(userService.login('user', 'wrong')).rejects.toThrow();
|
|
745
|
+
|
|
746
|
+
// ✅ 验证错误状态
|
|
747
|
+
expect(states[1]).toEqual({
|
|
748
|
+
user: null,
|
|
749
|
+
loading: false,
|
|
750
|
+
error: expect.any(Error)
|
|
751
|
+
});
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
it('should emit logout state', () => {
|
|
755
|
+
// 先设置用户登录
|
|
756
|
+
userService.emit({
|
|
757
|
+
user: { name: 'John', id: 1 },
|
|
758
|
+
loading: false,
|
|
759
|
+
error: null
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
// 登出
|
|
763
|
+
userService.logout();
|
|
764
|
+
|
|
765
|
+
// ✅ 验证状态被重置
|
|
766
|
+
expect(userService.getState()).toEqual({
|
|
767
|
+
user: null,
|
|
768
|
+
loading: false,
|
|
769
|
+
error: null
|
|
770
|
+
});
|
|
771
|
+
});
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
// ✅✅✅ 优势:
|
|
775
|
+
// 1. 不需要渲染 UI
|
|
776
|
+
// 2. 可以测试所有状态变化
|
|
777
|
+
// 3. 可以验证 emit 的调用序列
|
|
778
|
+
// 4. 测试运行快速
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
#### 2. 测试 UI 组件(UI 测试)
|
|
782
|
+
|
|
783
|
+
```typescript
|
|
784
|
+
// __tests__/src/pages/LoginPage.test.tsx
|
|
785
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
786
|
+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
787
|
+
import { LoginPage } from '@/pages/LoginPage';
|
|
788
|
+
import { IOCProvider } from '@/uikit/contexts/IOCContext';
|
|
789
|
+
|
|
790
|
+
describe('LoginPage (UI 测试)', () => {
|
|
791
|
+
it('should show loading when login', async () => {
|
|
792
|
+
// ✅ Mock Service 和 Store
|
|
793
|
+
const mockStore = {
|
|
794
|
+
user: null,
|
|
795
|
+
loading: false,
|
|
796
|
+
error: null
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
const mockUserService = {
|
|
800
|
+
login: vi.fn().mockImplementation(() => {
|
|
801
|
+
// 模拟状态变化
|
|
802
|
+
mockStore.loading = true;
|
|
803
|
+
return Promise.resolve();
|
|
804
|
+
}),
|
|
805
|
+
subscribe: vi.fn(),
|
|
806
|
+
getState: () => mockStore
|
|
807
|
+
};
|
|
808
|
+
|
|
809
|
+
const mockIOC = (identifier: string) => {
|
|
810
|
+
if (identifier === 'UserServiceInterface') return mockUserService;
|
|
811
|
+
};
|
|
812
|
+
|
|
813
|
+
// ✅ 渲染组件
|
|
814
|
+
const { rerender } = render(
|
|
815
|
+
<IOCProvider value={mockIOC}>
|
|
816
|
+
<LoginPage />
|
|
817
|
+
</IOCProvider>
|
|
818
|
+
);
|
|
819
|
+
|
|
820
|
+
// 点击登录按钮
|
|
821
|
+
const loginButton = screen.getByText('Login');
|
|
822
|
+
fireEvent.click(loginButton);
|
|
823
|
+
|
|
824
|
+
// ✅ 验证 Service 被调用
|
|
825
|
+
expect(mockUserService.login).toHaveBeenCalled();
|
|
826
|
+
|
|
827
|
+
// 模拟状态更新
|
|
828
|
+
mockStore.loading = true;
|
|
829
|
+
rerender(
|
|
830
|
+
<IOCProvider value={mockIOC}>
|
|
831
|
+
<LoginPage />
|
|
832
|
+
</IOCProvider>
|
|
833
|
+
);
|
|
834
|
+
|
|
835
|
+
// ✅ 验证 UI 显示加载状态
|
|
836
|
+
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
it('should show error message when login fails', () => {
|
|
840
|
+
const mockStore = {
|
|
841
|
+
user: null,
|
|
842
|
+
loading: false,
|
|
843
|
+
error: new Error('Invalid credentials')
|
|
844
|
+
};
|
|
845
|
+
|
|
846
|
+
const mockUserService = {
|
|
847
|
+
login: vi.fn(),
|
|
848
|
+
subscribe: vi.fn(),
|
|
849
|
+
getState: () => mockStore
|
|
850
|
+
};
|
|
851
|
+
|
|
852
|
+
const mockIOC = (identifier: string) => {
|
|
853
|
+
if (identifier === 'UserServiceInterface') return mockUserService;
|
|
854
|
+
};
|
|
855
|
+
|
|
856
|
+
render(
|
|
857
|
+
<IOCProvider value={mockIOC}>
|
|
858
|
+
<LoginPage />
|
|
859
|
+
</IOCProvider>
|
|
860
|
+
);
|
|
861
|
+
|
|
862
|
+
// ✅ 验证错误消息显示
|
|
863
|
+
expect(screen.getByText('Error: Invalid credentials')).toBeInTheDocument();
|
|
864
|
+
});
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
// ✅✅✅ 优势:
|
|
868
|
+
// 1. 不需要真实的 Service 实现
|
|
869
|
+
// 2. 可以轻松模拟各种状态
|
|
870
|
+
// 3. UI 测试专注于 UI 逻辑
|
|
871
|
+
```
|
|
872
|
+
|
|
873
|
+
#### 3. 组合测试(集成测试)
|
|
874
|
+
|
|
875
|
+
```typescript
|
|
876
|
+
// __tests__/src/integration/UserLogin.test.tsx
|
|
877
|
+
import { describe, it, expect } from 'vitest';
|
|
878
|
+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
879
|
+
import { LoginPage } from '@/pages/LoginPage';
|
|
880
|
+
import { UserService } from '@/base/services/UserService';
|
|
881
|
+
import { IOCProvider } from '@/uikit/contexts/IOCContext';
|
|
882
|
+
|
|
883
|
+
describe('User Login Flow (组合测试)', () => {
|
|
884
|
+
it('should complete login flow', async () => {
|
|
885
|
+
// ✅ 使用真实的 Service 和 Store
|
|
886
|
+
const mockApi = {
|
|
887
|
+
login: vi.fn().mockResolvedValue({
|
|
888
|
+
user: { name: 'John', id: 1 },
|
|
889
|
+
token: 'test-token'
|
|
890
|
+
})
|
|
891
|
+
};
|
|
892
|
+
|
|
893
|
+
const userService = new UserService(mockApi, mockConfig, mockStorage);
|
|
894
|
+
|
|
895
|
+
const mockIOC = (identifier: string) => {
|
|
896
|
+
if (identifier === 'UserServiceInterface') return userService;
|
|
897
|
+
};
|
|
898
|
+
|
|
899
|
+
// ✅ 渲染真实 UI
|
|
900
|
+
render(
|
|
901
|
+
<IOCProvider value={mockIOC}>
|
|
902
|
+
<LoginPage />
|
|
903
|
+
</IOCProvider>
|
|
904
|
+
);
|
|
905
|
+
|
|
906
|
+
// ✅ 模拟用户操作
|
|
907
|
+
const loginButton = screen.getByText('Login');
|
|
908
|
+
fireEvent.click(loginButton);
|
|
909
|
+
|
|
910
|
+
// ✅ 验证加载状态
|
|
911
|
+
await waitFor(() => {
|
|
912
|
+
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
// ✅ 验证登录成功
|
|
916
|
+
await waitFor(() => {
|
|
917
|
+
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
|
918
|
+
expect(userService.getState().user).toEqual({
|
|
919
|
+
name: 'John',
|
|
920
|
+
id: 1
|
|
921
|
+
});
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
// ✅ 验证 API 被调用
|
|
925
|
+
expect(mockApi.login).toHaveBeenCalled();
|
|
926
|
+
});
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
// ✅✅✅ 优势:
|
|
930
|
+
// 1. 测试真实的用户流程
|
|
931
|
+
// 2. 验证 Service 和 UI 的集成
|
|
932
|
+
// 3. 发现集成问题
|
|
933
|
+
```
|
|
934
|
+
|
|
935
|
+
### 测试策略总结
|
|
936
|
+
|
|
937
|
+
```
|
|
938
|
+
┌────────────────────────────────────────┐
|
|
939
|
+
│ 测试金字塔 │
|
|
940
|
+
│ │
|
|
941
|
+
│ △ UI 测试 (10%) │
|
|
942
|
+
│ ╱ ╲ │
|
|
943
|
+
│ ╱ ╲ 组合测试 (20%) │
|
|
944
|
+
│ ╱ ╲ │
|
|
945
|
+
│ ╱───────╲ │
|
|
946
|
+
│ ╱ ╲ Store + Service 测试 (70%) │
|
|
947
|
+
│╱═══════════╲ │
|
|
948
|
+
│ │
|
|
949
|
+
│ Store 测试:测试状态变化逻辑 │
|
|
950
|
+
│ 组合测试:测试 Service + UI 集成 │
|
|
951
|
+
│ UI 测试:测试 UI 交互 │
|
|
952
|
+
└────────────────────────────────────────┘
|
|
953
|
+
```
|
|
954
|
+
|
|
955
|
+
---
|
|
956
|
+
|
|
957
|
+
## 💎 最佳实践
|
|
958
|
+
|
|
959
|
+
### 1. ✅ Service 继承 StoreInterface
|
|
960
|
+
|
|
961
|
+
```typescript
|
|
962
|
+
// ✅ 好的做法:Service 继承 StoreInterface
|
|
255
963
|
@injectable()
|
|
256
|
-
class
|
|
257
|
-
|
|
964
|
+
export class UserService extends StoreInterface<UserState> {
|
|
965
|
+
constructor() {
|
|
966
|
+
super(() => ({
|
|
967
|
+
user: null,
|
|
968
|
+
loading: false
|
|
969
|
+
}));
|
|
970
|
+
}
|
|
258
971
|
|
|
259
|
-
|
|
260
|
-
this.emit({ ...this.state,
|
|
972
|
+
async login(username: string, password: string) {
|
|
973
|
+
this.emit({ ...this.state, loading: true });
|
|
974
|
+
// ...
|
|
261
975
|
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// ❌ 不好的做法:Service 不继承 StoreInterface
|
|
979
|
+
@injectable()
|
|
980
|
+
export class UserService {
|
|
981
|
+
private user: UserInfo | null = null;
|
|
982
|
+
|
|
983
|
+
// 问题:UI 无法订阅状态
|
|
984
|
+
}
|
|
985
|
+
```
|
|
986
|
+
|
|
987
|
+
### 2. ✅ 使用 emit 发布状态
|
|
988
|
+
|
|
989
|
+
```typescript
|
|
990
|
+
// ✅ 好的做法:通过 emit 发布状态
|
|
991
|
+
async login(username: string, password: string) {
|
|
992
|
+
this.emit({ ...this.state, loading: true });
|
|
993
|
+
|
|
994
|
+
const response = await this.api.login({ username, password });
|
|
995
|
+
|
|
996
|
+
this.emit({
|
|
997
|
+
user: response.user,
|
|
998
|
+
loading: false
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// ❌ 不好的做法:直接修改 state
|
|
1003
|
+
async login(username: string, password: string) {
|
|
1004
|
+
this.state.loading = true; // ❌ 不会通知订阅者
|
|
1005
|
+
|
|
1006
|
+
const response = await this.api.login({ username, password });
|
|
1007
|
+
|
|
1008
|
+
this.state.user = response.user; // ❌ 不会通知订阅者
|
|
1009
|
+
}
|
|
1010
|
+
```
|
|
1011
|
+
|
|
1012
|
+
### 3. ✅ 使用 cloneState 简化更新
|
|
1013
|
+
|
|
1014
|
+
```typescript
|
|
1015
|
+
// ✅ 好的做法:使用 cloneState
|
|
1016
|
+
setUser(user: UserInfo) {
|
|
1017
|
+
this.emit(this.cloneState({ user }));
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
setLoading(loading: boolean) {
|
|
1021
|
+
this.emit(this.cloneState({ loading }));
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// ⚠️ 也可以:手动展开
|
|
1025
|
+
setUser(user: UserInfo) {
|
|
1026
|
+
this.emit({ ...this.state, user });
|
|
1027
|
+
}
|
|
1028
|
+
```
|
|
1029
|
+
|
|
1030
|
+
### 4. ✅ 定义选择器
|
|
1031
|
+
|
|
1032
|
+
```typescript
|
|
1033
|
+
// ✅ 好的做法:定义选择器
|
|
1034
|
+
@injectable()
|
|
1035
|
+
export class UserService extends StoreInterface<UserState> {
|
|
1036
|
+
selector = {
|
|
1037
|
+
user: (state: UserState) => state.user,
|
|
1038
|
+
loading: (state: UserState) => state.loading,
|
|
1039
|
+
isLoggedIn: (state: UserState) => state.user !== null
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// 使用
|
|
1044
|
+
const isLoggedIn = useStore(userService, userService.selector.isLoggedIn);
|
|
1045
|
+
|
|
1046
|
+
// ❌ 不好的做法:内联选择器
|
|
1047
|
+
const isLoggedIn = useStore(userService, (state) => state.user !== null);
|
|
1048
|
+
// 问题:每次渲染都创建新函数
|
|
1049
|
+
```
|
|
262
1050
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
1051
|
+
### 5. ✅ 使用选择器优化性能
|
|
1052
|
+
|
|
1053
|
+
```typescript
|
|
1054
|
+
// ✅ 好的做法:只订阅需要的状态
|
|
1055
|
+
function UserName() {
|
|
1056
|
+
const userService = useIOC('UserServiceInterface');
|
|
1057
|
+
|
|
1058
|
+
// 只订阅 user,loading 变化不会触发重新渲染
|
|
1059
|
+
const user = useStore(userService, (state) => state.user);
|
|
1060
|
+
|
|
1061
|
+
return <span>{user?.name}</span>;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// ❌ 不好的做法:订阅完整状态
|
|
1065
|
+
function UserName() {
|
|
1066
|
+
const userService = useIOC('UserServiceInterface');
|
|
1067
|
+
|
|
1068
|
+
// loading 变化也会触发重新渲染
|
|
1069
|
+
const { user, loading } = useStore(userService);
|
|
1070
|
+
|
|
1071
|
+
return <span>{user?.name}</span>;
|
|
1072
|
+
}
|
|
1073
|
+
```
|
|
1074
|
+
|
|
1075
|
+
### 6. ✅ 状态保持不可变
|
|
1076
|
+
|
|
1077
|
+
```typescript
|
|
1078
|
+
// ✅ 好的做法:创建新对象
|
|
1079
|
+
updateUser(changes: Partial<UserInfo>) {
|
|
1080
|
+
this.emit({
|
|
1081
|
+
...this.state,
|
|
1082
|
+
user: {
|
|
1083
|
+
...this.state.user,
|
|
1084
|
+
...changes
|
|
1085
|
+
}
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// ❌ 不好的做法:直接修改对象
|
|
1090
|
+
updateUser(changes: Partial<UserInfo>) {
|
|
1091
|
+
this.state.user.name = changes.name; // ❌ 直接修改
|
|
1092
|
+
this.emit(this.state); // ❌ 引用相同,可能不触发更新
|
|
1093
|
+
}
|
|
1094
|
+
```
|
|
1095
|
+
|
|
1096
|
+
### 7. ✅ 合理划分状态
|
|
1097
|
+
|
|
1098
|
+
```typescript
|
|
1099
|
+
// ✅ 好的做法:每个 Service 管理自己的状态
|
|
1100
|
+
class UserService extends StoreInterface<UserState> {
|
|
1101
|
+
// 只管理用户相关状态
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
class ThemeService extends StoreInterface<ThemeState> {
|
|
1105
|
+
// 只管理主题相关状态
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
class I18nService extends StoreInterface<I18nState> {
|
|
1109
|
+
// 只管理国际化相关状态
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// ❌ 不好的做法:全局大 Store
|
|
1113
|
+
class GlobalStore extends StoreInterface<GlobalState> {
|
|
1114
|
+
// 包含所有状态:用户、主题、国际化等
|
|
1115
|
+
// 问题:任何状态变化都会影响所有订阅者
|
|
1116
|
+
}
|
|
1117
|
+
```
|
|
1118
|
+
|
|
1119
|
+
---
|
|
1120
|
+
|
|
1121
|
+
## ❓ 常见问题
|
|
1122
|
+
|
|
1123
|
+
### Q1: 为什么不用 Redux?
|
|
1124
|
+
|
|
1125
|
+
**A:**
|
|
1126
|
+
|
|
1127
|
+
| 特性 | Redux | Store (SliceStore) |
|
|
1128
|
+
| ------------------- | ------------------------------------ | ------------------------- |
|
|
1129
|
+
| **复杂度** | ❌ 高(Action, Reducer, Middleware) | ✅ 低(emit + subscribe) |
|
|
1130
|
+
| **学习曲线** | ❌ 陡峭 | ✅ 平缓 |
|
|
1131
|
+
| **TypeScript 支持** | ⚠️ 需要额外配置 | ✅ 原生支持 |
|
|
1132
|
+
| **IOC 集成** | ⚠️ 需要额外工作 | ✅ 天然集成 |
|
|
1133
|
+
| **性能** | ✅ 好 | ✅ 好 |
|
|
1134
|
+
| **适用场景** | 大型应用 | 中小型应用 |
|
|
1135
|
+
|
|
1136
|
+
**我们的选择:**
|
|
1137
|
+
|
|
1138
|
+
- 项目已经使用 IOC,不需要 Redux 的全局状态管理
|
|
1139
|
+
- 每个 Service 管理自己的状态,更清晰
|
|
1140
|
+
- SliceStore 足够简单和强大
|
|
1141
|
+
|
|
1142
|
+
### Q2: Store 和 React Context 有什么区别?
|
|
1143
|
+
|
|
1144
|
+
**A:**
|
|
1145
|
+
|
|
1146
|
+
| 特性 | React Context | Store |
|
|
1147
|
+
| ------------------- | ------------------------- | ------------------------- |
|
|
1148
|
+
| **作用域** | 组件树 | 全局(通过 IOC) |
|
|
1149
|
+
| **性能** | ⚠️ 任何值变化都会重新渲染 | ✅ 只有订阅的值变化才渲染 |
|
|
1150
|
+
| **选择器** | ❌ 无 | ✅ 有 |
|
|
1151
|
+
| **与 Service 集成** | ⚠️ 需要手动 | ✅ 天然集成 |
|
|
1152
|
+
|
|
1153
|
+
**建议:**
|
|
1154
|
+
|
|
1155
|
+
- 使用 Store 管理应用状态(Service 状态)
|
|
1156
|
+
- 使用 Context 管理 UI 状态(如模态框、临时表单数据)
|
|
1157
|
+
|
|
1158
|
+
### Q3: 如何避免重复渲染?
|
|
1159
|
+
|
|
1160
|
+
**A:** 使用选择器
|
|
1161
|
+
|
|
1162
|
+
```typescript
|
|
1163
|
+
// ❌ 问题:订阅完整状态
|
|
1164
|
+
const { user, loading, error } = useStore(userService);
|
|
1165
|
+
// loading 变化会导致组件重新渲染
|
|
1166
|
+
|
|
1167
|
+
// ✅ 解决:只订阅需要的状态
|
|
1168
|
+
const user = useStore(userService, (state) => state.user);
|
|
1169
|
+
// 只有 user 变化才会重新渲染
|
|
1170
|
+
```
|
|
1171
|
+
|
|
1172
|
+
### Q4: 可以在 Service 外部调用 emit 吗?
|
|
1173
|
+
|
|
1174
|
+
**A:** 不建议
|
|
1175
|
+
|
|
1176
|
+
```typescript
|
|
1177
|
+
// ❌ 不好的做法
|
|
1178
|
+
function SomeComponent() {
|
|
1179
|
+
const userService = useIOC('UserServiceInterface');
|
|
1180
|
+
|
|
1181
|
+
// ❌ 直接调用 emit
|
|
1182
|
+
userService.emit({ user: newUser, loading: false });
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// ✅ 好的做法:通过 Service 方法
|
|
1186
|
+
function SomeComponent() {
|
|
1187
|
+
const userService = useIOC('UserServiceInterface');
|
|
1188
|
+
|
|
1189
|
+
// ✅ 调用 Service 方法
|
|
1190
|
+
userService.setUser(newUser);
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
// Service 中
|
|
1194
|
+
@injectable()
|
|
1195
|
+
export class UserService extends StoreInterface<UserState> {
|
|
1196
|
+
setUser(user: UserInfo) {
|
|
1197
|
+
this.emit(this.cloneState({ user }));
|
|
266
1198
|
}
|
|
1199
|
+
}
|
|
1200
|
+
```
|
|
1201
|
+
|
|
1202
|
+
**原因:**
|
|
1203
|
+
|
|
1204
|
+
- 保持封装性
|
|
1205
|
+
- 方便测试
|
|
1206
|
+
- 业务逻辑集中在 Service
|
|
1207
|
+
|
|
1208
|
+
### Q5: Store 状态更新不生效?
|
|
1209
|
+
|
|
1210
|
+
**A:** 检查以下几点:
|
|
1211
|
+
|
|
1212
|
+
```typescript
|
|
1213
|
+
// ❌ 常见错误 1:直接修改 state
|
|
1214
|
+
this.state.loading = true; // 不会触发更新
|
|
1215
|
+
|
|
1216
|
+
// ✅ 正确:使用 emit
|
|
1217
|
+
this.emit({ ...this.state, loading: true });
|
|
1218
|
+
|
|
1219
|
+
// ❌ 常见错误 2:没有创建新对象
|
|
1220
|
+
const state = this.state;
|
|
1221
|
+
state.loading = true;
|
|
1222
|
+
this.emit(state); // 引用相同,可能不触发更新
|
|
1223
|
+
|
|
1224
|
+
// ✅ 正确:创建新对象
|
|
1225
|
+
this.emit({ ...this.state, loading: true });
|
|
1226
|
+
|
|
1227
|
+
// ❌ 常见错误 3:忘记订阅
|
|
1228
|
+
function MyComponent() {
|
|
1229
|
+
const userService = useIOC('UserServiceInterface');
|
|
1230
|
+
// 没有调用 useStore,无法接收更新
|
|
1231
|
+
|
|
1232
|
+
return <div>{userService.getState().user?.name}</div>;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// ✅ 正确:使用 useStore 订阅
|
|
1236
|
+
function MyComponent() {
|
|
1237
|
+
const userService = useIOC('UserServiceInterface');
|
|
1238
|
+
const user = useStore(userService, (state) => state.user);
|
|
1239
|
+
|
|
1240
|
+
return <div>{user?.name}</div>;
|
|
1241
|
+
}
|
|
1242
|
+
```
|
|
1243
|
+
|
|
1244
|
+
### Q6: 如何在 Service 之间共享状态?
|
|
267
1245
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
1246
|
+
**A:** 通过 IOC 注入
|
|
1247
|
+
|
|
1248
|
+
```typescript
|
|
1249
|
+
// Service A
|
|
1250
|
+
@injectable()
|
|
1251
|
+
export class UserService extends StoreInterface<UserState> {
|
|
1252
|
+
// ...
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
// Service B 依赖 Service A
|
|
1256
|
+
@injectable()
|
|
1257
|
+
export class ProfileService {
|
|
1258
|
+
constructor(
|
|
1259
|
+
@inject('UserServiceInterface')
|
|
1260
|
+
private userService: UserService
|
|
1261
|
+
) {}
|
|
1262
|
+
|
|
1263
|
+
async updateProfile(data: ProfileData) {
|
|
1264
|
+
// ✅ 访问 UserService 的状态
|
|
1265
|
+
const user = this.userService.getState().user;
|
|
1266
|
+
|
|
1267
|
+
// ✅ 也可以订阅 UserService 的状态
|
|
1268
|
+
this.userService.subscribe((state) => {
|
|
1269
|
+
console.log('User state changed:', state);
|
|
1270
|
+
});
|
|
271
1271
|
}
|
|
272
1272
|
}
|
|
273
1273
|
```
|
|
274
1274
|
|
|
275
|
-
|
|
1275
|
+
---
|
|
276
1276
|
|
|
277
|
-
|
|
278
|
-
- 按功能模块划分状态
|
|
279
|
-
- 避免状态冗余
|
|
280
|
-
- 保持状态扁平化
|
|
1277
|
+
## 📚 相关文档
|
|
281
1278
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
1279
|
+
- [项目架构设计](./index.md) - 了解整体架构
|
|
1280
|
+
- [IOC 容器](./ioc.md) - 依赖注入和 UI 分离
|
|
1281
|
+
- [Bootstrap 启动器](./bootstrap.md) - 应用启动和初始化
|
|
1282
|
+
- [测试指南](./test-guide.md) - 详细的测试策略
|
|
286
1283
|
|
|
287
|
-
|
|
288
|
-
- 为所有状态定义接口
|
|
289
|
-
- 使用 TypeScript 的类型推导
|
|
290
|
-
- 避免使用 any 类型
|
|
1284
|
+
---
|
|
291
1285
|
|
|
292
|
-
|
|
293
|
-
- 在 Bootstrap 阶段初始化 store
|
|
294
|
-
- 通过 IOC 容器管理 store 实例
|
|
295
|
-
- 使用插件系统扩展功能
|
|
1286
|
+
## 🎉 总结
|
|
296
1287
|
|
|
297
|
-
|
|
1288
|
+
Store 状态管理的核心价值:
|
|
298
1289
|
|
|
299
|
-
|
|
1290
|
+
1. **解决通信问题** 📡 - 应用层通知 UI 层,同时保持分离
|
|
1291
|
+
2. **发布订阅模式** 🔔 - Service emit,UI useStore
|
|
1292
|
+
3. **自动更新 UI** ⚡ - 状态变化时,UI 自动重新渲染
|
|
1293
|
+
4. **保持解耦** 🔗 - Service 不知道有哪些 UI 在监听
|
|
1294
|
+
5. **易于测试** 🧪 - Store 可以独立测试
|
|
1295
|
+
6. **性能优化** 🚀 - 选择器只订阅需要的状态
|
|
1296
|
+
7. **类型安全** 🔒 - TypeScript 完整支持
|
|
300
1297
|
|
|
301
|
-
|
|
1298
|
+
**记住核心模式:**
|
|
302
1299
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
1300
|
+
```typescript
|
|
1301
|
+
// 1. Service 继承 StoreInterface
|
|
1302
|
+
class MyService extends StoreInterface<MyState> {
|
|
1303
|
+
// 2. 通过 emit 发布状态
|
|
1304
|
+
doSomething() {
|
|
1305
|
+
this.emit({ ...this.state, data: newData });
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
306
1308
|
|
|
307
|
-
|
|
1309
|
+
// 3. UI 通过 useStore 订阅状态
|
|
1310
|
+
function MyComponent() {
|
|
1311
|
+
const myService = useIOC('MyServiceInterface');
|
|
1312
|
+
const data = useStore(myService, (state) => state.data);
|
|
308
1313
|
|
|
309
|
-
|
|
1314
|
+
return <div>{data}</div>;
|
|
1315
|
+
}
|
|
1316
|
+
```
|
|
310
1317
|
|
|
311
|
-
|
|
312
|
-
- 检查依赖项是否正确设置
|
|
313
|
-
- 考虑使用 React.memo 优化组件
|
|
1318
|
+
**核心原则:**
|
|
314
1319
|
|
|
315
|
-
|
|
1320
|
+
- ✅ Service 通过 emit 发布状态
|
|
1321
|
+
- ✅ UI 通过 useStore 订阅状态
|
|
1322
|
+
- ✅ 使用选择器优化性能
|
|
1323
|
+
- ✅ 状态保持不可变
|
|
1324
|
+
- ✅ 每个 Service 管理自己的状态
|
|
316
1325
|
|
|
317
|
-
|
|
1326
|
+
---
|
|
318
1327
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
- 确保状态类型实现了 StoreStateInterface
|
|
1328
|
+
**问题反馈:**
|
|
1329
|
+
如果你对 Store 状态管理有任何疑问或建议,请在团队频道中讨论或提交 Issue。
|