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