@qlover/create-app 0.7.15 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/dist/configs/_common/.gitignore.template +6 -0
- package/dist/configs/_common/.prettierignore +17 -5
- package/dist/configs/_common/.vscode/settings.json +6 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/templates/next-app/.env.template +1 -1
- package/dist/templates/next-app/README.en.md +0 -1
- package/dist/templates/next-app/README.md +0 -1
- package/dist/templates/next-app/config/Identifier/api.ts +5 -5
- package/dist/templates/next-app/config/Identifier/common/admint.table.ts +69 -0
- package/dist/templates/next-app/config/Identifier/common/common.ts +76 -0
- package/dist/templates/next-app/config/Identifier/common/index.ts +3 -0
- package/dist/templates/next-app/config/Identifier/{validator.ts → common/validators.ts} +5 -5
- package/dist/templates/next-app/config/Identifier/index.ts +2 -12
- package/dist/templates/next-app/config/Identifier/pages/index.ts +6 -0
- package/dist/templates/next-app/config/Identifier/pages/page.admin.home.ts +27 -0
- package/dist/templates/next-app/config/Identifier/pages/page.admin.locales.ts +266 -0
- package/dist/templates/next-app/config/Identifier/pages/page.admin.user.ts +293 -0
- package/dist/templates/{react-app/config/Identifier → next-app/config/Identifier/pages}/page.home.ts +15 -22
- package/dist/templates/next-app/config/Identifier/{page.login.ts → pages/page.login.ts} +28 -34
- package/dist/templates/next-app/config/Identifier/{page.register.ts → pages/page.register.ts} +30 -29
- package/dist/templates/next-app/config/adminNavs.ts +19 -0
- package/dist/templates/next-app/config/common.ts +22 -13
- package/dist/templates/next-app/config/i18n/HomeI18n.ts +5 -5
- package/dist/templates/next-app/config/i18n/admin18n.ts +61 -19
- package/dist/templates/next-app/config/i18n/i18nConfig.ts +2 -0
- package/dist/templates/next-app/config/i18n/i18nKeyScheam.ts +36 -0
- package/dist/templates/next-app/config/i18n/loginI18n.ts +22 -22
- package/dist/templates/next-app/config/i18n/register18n.ts +23 -24
- package/dist/templates/next-app/docs/en/index.md +0 -1
- package/dist/templates/next-app/docs/en/project-structure.md +0 -1
- package/dist/templates/next-app/docs/zh/index.md +0 -1
- package/dist/templates/next-app/docs/zh/project-structure.md +0 -1
- package/dist/templates/next-app/make/generateLocales.ts +19 -12
- package/dist/templates/next-app/migrations/schema/LocalesSchema.ts +15 -0
- package/dist/templates/next-app/migrations/sql/1694244000000.sql +11 -0
- package/dist/templates/next-app/package.json +7 -3
- package/dist/templates/next-app/public/locales/en.json +172 -207
- package/dist/templates/next-app/public/locales/zh.json +172 -207
- package/dist/templates/next-app/src/app/[locale]/admin/locales/page.tsx +153 -0
- package/dist/templates/next-app/src/app/[locale]/admin/users/page.tsx +48 -50
- package/dist/templates/next-app/src/app/[locale]/login/LoginForm.tsx +2 -2
- package/dist/templates/next-app/src/app/api/admin/locales/create/route.ts +34 -0
- package/dist/templates/next-app/src/app/api/admin/locales/import/route.ts +40 -0
- package/dist/templates/next-app/src/app/api/admin/locales/route.ts +42 -0
- package/dist/templates/next-app/src/app/api/admin/locales/update/route.ts +32 -0
- package/dist/templates/next-app/src/app/api/locales/json/route.ts +44 -0
- package/dist/templates/next-app/src/base/cases/AdminPageManager.ts +1 -13
- package/dist/templates/next-app/src/base/cases/Datetime.ts +18 -0
- package/dist/templates/next-app/src/base/cases/DialogErrorPlugin.ts +12 -6
- package/dist/templates/next-app/src/base/cases/ResourceState.ts +17 -0
- package/dist/templates/next-app/src/base/cases/TranslateI18nInterface.ts +25 -0
- package/dist/templates/next-app/src/base/cases/ZodColumnBuilder.ts +200 -0
- package/dist/templates/next-app/src/base/port/ZodBuilderInterface.ts +8 -0
- package/dist/templates/next-app/src/base/services/AdminLocalesService.ts +20 -0
- package/dist/templates/next-app/src/base/services/AdminPageEvent.ts +26 -0
- package/dist/templates/next-app/src/base/services/AdminPageScheduler.ts +42 -0
- package/dist/templates/next-app/src/base/services/ResourceService.ts +122 -0
- package/dist/templates/next-app/src/base/services/adminApi/AdminLocalesApi.ts +104 -0
- package/dist/templates/next-app/src/base/services/adminApi/AdminUserApi.ts +38 -5
- package/dist/templates/next-app/src/base/services/appApi/AppApiPlugin.ts +1 -1
- package/dist/templates/next-app/src/i18n/request.ts +30 -1
- package/dist/templates/next-app/src/server/PageParams.ts +2 -10
- package/dist/templates/next-app/src/server/port/DBBridgeInterface.ts +5 -0
- package/dist/templates/next-app/src/server/port/DBTableInterface.ts +2 -0
- package/dist/templates/next-app/src/server/port/LocalesRepositoryInterface.ts +43 -0
- package/dist/templates/next-app/src/server/repositorys/LocalesRepository.ts +197 -0
- package/dist/templates/next-app/src/server/services/ApiLocaleService.ts +122 -0
- package/dist/templates/next-app/src/server/sqlBridges/SupabaseBridge.ts +60 -11
- package/dist/templates/next-app/src/server/validators/ExtendedExecutorError.ts +6 -0
- package/dist/templates/next-app/src/server/validators/LocalesValidator.ts +131 -0
- package/dist/templates/next-app/src/server/validators/LoginValidator.ts +2 -5
- package/dist/templates/next-app/src/server/validators/PaginationValidator.ts +32 -16
- package/dist/templates/next-app/src/styles/css/antd-themes/pagination/_default.css +2 -1
- package/dist/templates/next-app/src/styles/css/antd-themes/pagination/dark.css +28 -29
- package/dist/templates/next-app/src/styles/css/antd-themes/pagination/pink.css +2 -1
- package/dist/templates/next-app/src/uikit/components/AdminLayout.tsx +17 -3
- package/dist/templates/next-app/src/uikit/components/BaseHeader.tsx +5 -4
- package/dist/templates/next-app/src/uikit/components/BaseLayout.tsx +5 -4
- package/dist/templates/next-app/src/uikit/components/BootstrapsProvider.tsx +3 -2
- package/dist/templates/next-app/src/uikit/components/ComboProvider.tsx +1 -1
- package/dist/templates/next-app/src/uikit/components/EditableCell.tsx +118 -0
- package/dist/templates/next-app/src/uikit/components/LogoutButton.tsx +5 -6
- package/dist/templates/next-app/src/uikit/components/ThemeSwitcher.tsx +1 -1
- package/dist/templates/next-app/src/uikit/components/With.tsx +2 -2
- package/dist/templates/next-app/src/uikit/components/localesImportButton/LocalesImportButton.tsx +62 -0
- package/dist/templates/next-app/src/uikit/components/localesImportButton/LocalesImportEvent.ts +28 -0
- package/dist/templates/next-app/src/uikit/components/localesImportButton/import.module.css +6 -0
- package/dist/templates/next-app/src/uikit/hook/useI18nInterface.ts +8 -14
- package/dist/templates/next-app/src/uikit/hook/useWarnTranslations.ts +25 -0
- package/dist/templates/react-app/.prettierignore +17 -0
- package/dist/templates/react-app/README.en.md +71 -54
- package/dist/templates/react-app/README.md +35 -18
- package/dist/templates/react-app/__tests__/__mocks__/BootstrapTest.ts +14 -0
- package/dist/templates/react-app/__tests__/__mocks__/MockAppConfit.ts +1 -1
- package/dist/templates/react-app/__tests__/__mocks__/MockDialogHandler.ts +2 -2
- package/dist/templates/react-app/__tests__/__mocks__/MockLogger.ts +1 -1
- package/dist/templates/react-app/__tests__/__mocks__/components/TestApp.tsx +45 -0
- package/dist/templates/react-app/__tests__/__mocks__/components/TestBootstrapsProvider.tsx +34 -0
- package/dist/templates/react-app/__tests__/__mocks__/components/TestRouter.tsx +46 -0
- package/dist/templates/react-app/__tests__/__mocks__/components/index.ts +12 -0
- package/dist/templates/react-app/__tests__/__mocks__/createMockGlobals.ts +1 -2
- package/dist/templates/react-app/__tests__/__mocks__/testIOC/TestIOC.ts +51 -0
- package/dist/templates/react-app/__tests__/__mocks__/testIOC/TestIOCRegister.ts +69 -0
- package/dist/templates/react-app/__tests__/setup/index.ts +1 -51
- package/dist/templates/react-app/__tests__/setup/setupGlobal.ts +51 -0
- package/dist/templates/react-app/__tests__/src/App.structure.test.tsx +115 -0
- package/dist/templates/react-app/__tests__/src/base/cases/AppConfig.test.ts +2 -2
- package/dist/templates/react-app/__tests__/src/base/cases/AppError.test.ts +1 -1
- package/dist/templates/react-app/__tests__/src/base/cases/DialogHandler.test.ts +3 -5
- package/dist/templates/react-app/__tests__/src/base/cases/I18nKeyErrorPlugin.test.ts +13 -2
- package/dist/templates/react-app/__tests__/src/base/cases/InversifyContainer.test.ts +1 -1
- package/dist/templates/react-app/__tests__/src/base/cases/PublicAssetsPath.test.ts +1 -1
- package/dist/templates/react-app/__tests__/src/base/cases/RequestLogger.test.ts +5 -5
- package/dist/templates/react-app/__tests__/src/base/cases/RequestStatusCatcher.test.ts +1 -2
- package/dist/templates/react-app/__tests__/src/base/cases/RouterLoader.test.ts +25 -15
- package/dist/templates/react-app/__tests__/src/base/services/I18nService.test.ts +29 -15
- package/dist/templates/react-app/__tests__/src/core/IOC.test.ts +19 -9
- package/dist/templates/react-app/__tests__/src/core/bootstraps/BootstrapClient.test.ts +153 -0
- package/dist/templates/react-app/__tests__/src/core/bootstraps/BootstrapsApp.test.ts +9 -7
- package/dist/templates/react-app/__tests__/src/main.integration.test.tsx +4 -5
- package/dist/templates/react-app/__tests__/src/main.test.tsx +4 -4
- package/dist/templates/react-app/__tests__/src/uikit/components/BaseHeader.test.tsx +68 -59
- package/dist/templates/react-app/config/IOCIdentifier.ts +8 -8
- package/dist/templates/react-app/config/Identifier/{common.error.ts → common/common.error.ts} +5 -5
- package/dist/templates/react-app/config/Identifier/{common.ts → common/common.ts} +9 -9
- package/dist/templates/react-app/config/Identifier/common/index.ts +2 -0
- package/dist/templates/react-app/config/Identifier/index.ts +1 -9
- package/dist/templates/react-app/config/Identifier/pages/index.ts +8 -0
- package/dist/templates/react-app/config/Identifier/{page.about.ts → pages/page.about.ts} +34 -26
- package/dist/templates/react-app/config/Identifier/{page.executor.ts → pages/page.executor.ts} +47 -39
- package/dist/templates/{next-app/config/Identifier → react-app/config/Identifier/pages}/page.home.ts +24 -23
- package/dist/templates/react-app/config/Identifier/pages/page.identifiter.ts +102 -0
- package/dist/templates/react-app/config/Identifier/{page.jsonStorage.ts → pages/page.jsonStorage.ts} +18 -11
- package/dist/templates/react-app/config/Identifier/{page.login.ts → pages/page.login.ts} +37 -27
- package/dist/templates/react-app/config/Identifier/{page.register.ts → pages/page.register.ts} +37 -25
- package/dist/templates/react-app/config/Identifier/{page.request.ts → pages/page.request.ts} +34 -44
- package/dist/templates/react-app/config/app.router.ts +66 -69
- package/dist/templates/react-app/config/i18n/PageI18nInterface.ts +51 -0
- package/dist/templates/react-app/config/i18n/aboutI18n.ts +42 -0
- package/dist/templates/react-app/config/i18n/executorI18n.ts +51 -0
- package/dist/templates/react-app/config/i18n/homeI18n.ts +24 -0
- package/dist/templates/react-app/config/i18n/i18nConfig.ts +30 -0
- package/dist/templates/react-app/config/i18n/identifiter18n.ts +30 -0
- package/dist/templates/react-app/config/i18n/jsonStorage18n.ts +27 -0
- package/dist/templates/react-app/config/i18n/login18n.ts +42 -0
- package/dist/templates/react-app/config/i18n/notFoundI18n.ts +34 -0
- package/dist/templates/react-app/config/i18n/register18n.ts +40 -0
- package/dist/templates/react-app/config/i18n/request18n.ts +41 -0
- package/dist/templates/react-app/config/theme.ts +14 -4
- package/dist/templates/react-app/docs/en/bootstrap.md +1670 -341
- package/dist/templates/react-app/docs/en/development-guide.md +1021 -345
- package/dist/templates/react-app/docs/en/env.md +1132 -278
- package/dist/templates/react-app/docs/en/i18n.md +858 -147
- package/dist/templates/react-app/docs/en/index.md +733 -104
- package/dist/templates/react-app/docs/en/ioc.md +1228 -287
- package/dist/templates/react-app/docs/en/playwright/e2e-tests.md +321 -0
- package/dist/templates/react-app/docs/en/playwright/index.md +19 -0
- package/dist/templates/react-app/docs/en/playwright/installation-summary.md +332 -0
- package/dist/templates/react-app/docs/en/playwright/overview.md +222 -0
- package/dist/templates/react-app/docs/en/playwright/quickstart.md +325 -0
- package/dist/templates/react-app/docs/en/playwright/reorganization-notes.md +340 -0
- package/dist/templates/react-app/docs/en/playwright/setup-complete.md +290 -0
- package/dist/templates/react-app/docs/en/playwright/testing-guide.md +565 -0
- package/dist/templates/react-app/docs/en/store.md +1194 -184
- package/dist/templates/react-app/docs/en/why-no-globals.md +797 -0
- package/dist/templates/react-app/docs/zh/bootstrap.md +1670 -341
- package/dist/templates/react-app/docs/zh/development-guide.md +1021 -345
- package/dist/templates/react-app/docs/zh/env.md +1132 -275
- package/dist/templates/react-app/docs/zh/i18n.md +858 -147
- package/dist/templates/react-app/docs/zh/index.md +717 -104
- package/dist/templates/react-app/docs/zh/ioc.md +1229 -287
- package/dist/templates/react-app/docs/zh/playwright/e2e-tests.md +321 -0
- package/dist/templates/react-app/docs/zh/playwright/index.md +19 -0
- package/dist/templates/react-app/docs/zh/playwright/installation-summary.md +332 -0
- package/dist/templates/react-app/docs/zh/playwright/overview.md +222 -0
- package/dist/templates/react-app/docs/zh/playwright/quickstart.md +325 -0
- package/dist/templates/react-app/docs/zh/playwright/reorganization-notes.md +340 -0
- package/dist/templates/react-app/docs/zh/playwright/setup-complete.md +290 -0
- package/dist/templates/react-app/docs/zh/playwright/testing-guide.md +565 -0
- package/dist/templates/react-app/docs/zh/store.md +1192 -184
- package/dist/templates/react-app/docs/zh/why-no-globals.md +797 -0
- package/dist/templates/react-app/e2e/App.spec.ts +319 -0
- package/dist/templates/react-app/e2e/fixtures/base.fixture.ts +40 -0
- package/dist/templates/react-app/e2e/main.spec.ts +20 -0
- package/dist/templates/react-app/e2e/utils/test-helpers.ts +19 -0
- package/dist/templates/react-app/eslint.config.mjs +247 -0
- package/dist/templates/react-app/makes/eslint-utils.mjs +195 -0
- package/dist/templates/react-app/makes/generateTs2LocalesOptions.ts +26 -0
- package/dist/templates/react-app/package.json +31 -3
- package/dist/templates/react-app/playwright.config.ts +79 -0
- package/dist/templates/react-app/public/locales/en/common.json +190 -179
- package/dist/templates/react-app/public/locales/zh/common.json +190 -179
- package/dist/templates/react-app/src/App.tsx +15 -42
- package/dist/templates/react-app/src/base/apis/AiApi.ts +5 -5
- package/dist/templates/react-app/src/base/apis/feApi/FeApi.ts +1 -1
- package/dist/templates/react-app/src/base/apis/feApi/FeApiAdapter.ts +1 -1
- package/dist/templates/react-app/src/base/apis/feApi/FeApiBootstarp.ts +8 -8
- package/dist/templates/react-app/src/base/apis/feApi/FeApiType.ts +1 -1
- package/dist/templates/react-app/src/base/apis/userApi/UserApi.ts +6 -6
- package/dist/templates/react-app/src/base/apis/userApi/UserApiAdapter.ts +1 -1
- package/dist/templates/react-app/src/base/apis/userApi/UserApiBootstarp.ts +12 -14
- package/dist/templates/react-app/src/base/apis/userApi/UserApiType.ts +1 -1
- package/dist/templates/react-app/src/base/cases/DialogHandler.ts +5 -2
- package/dist/templates/react-app/src/base/cases/I18nKeyErrorPlugin.ts +3 -3
- package/dist/templates/react-app/src/base/cases/InversifyContainer.ts +3 -3
- package/dist/templates/react-app/src/base/cases/RequestLanguages.ts +2 -2
- package/dist/templates/react-app/src/base/cases/RequestLogger.ts +4 -4
- package/dist/templates/react-app/src/base/cases/RequestStatusCatcher.ts +1 -1
- package/dist/templates/react-app/src/base/cases/ResourceState.ts +23 -0
- package/dist/templates/react-app/src/base/cases/RouterLoader.ts +4 -4
- package/dist/templates/react-app/src/base/cases/TranslateI18nInterface.ts +26 -0
- package/dist/templates/react-app/src/base/port/ExecutorPageBridgeInterface.ts +2 -3
- package/dist/templates/react-app/src/base/port/I18nServiceInterface.ts +1 -1
- package/dist/templates/react-app/src/base/port/IOCInterface.ts +36 -0
- package/dist/templates/react-app/src/base/port/JSONStoragePageBridgeInterface.ts +2 -1
- package/dist/templates/react-app/src/base/port/ProcesserExecutorInterface.ts +1 -1
- package/dist/templates/react-app/src/base/port/RequestPageBridgeInterface.ts +2 -2
- package/dist/templates/react-app/src/base/port/RouteServiceInterface.ts +9 -5
- package/dist/templates/react-app/src/base/port/UserServiceInterface.ts +1 -1
- package/dist/templates/react-app/src/base/services/I18nService.ts +29 -29
- package/dist/templates/react-app/src/base/services/IdentifierService.ts +143 -0
- package/dist/templates/react-app/src/base/services/ProcesserExecutor.ts +3 -3
- package/dist/templates/react-app/src/base/services/RouteService.ts +27 -8
- package/dist/templates/react-app/src/base/services/UserService.ts +8 -8
- package/dist/templates/react-app/src/base/types/Page.ts +14 -2
- package/dist/templates/react-app/src/base/types/global.d.ts +1 -1
- package/dist/templates/react-app/src/core/IOC.ts +5 -46
- package/dist/templates/react-app/src/core/bootstraps/{BootstrapApp.ts → BootstrapClient.ts} +44 -17
- package/dist/templates/react-app/src/core/bootstraps/BootstrapsRegistry.ts +14 -7
- package/dist/templates/react-app/src/core/bootstraps/IocIdentifierTest.ts +1 -1
- package/dist/templates/react-app/src/core/bootstraps/PrintBootstrap.ts +1 -1
- package/dist/templates/react-app/src/core/clientIoc/ClientIOC.ts +40 -0
- package/dist/templates/react-app/src/core/{IocRegisterImpl.ts → clientIoc/ClientIOCRegister.ts} +35 -24
- package/dist/templates/react-app/src/core/globals.ts +9 -9
- package/dist/templates/react-app/src/main.tsx +4 -4
- package/dist/templates/react-app/src/pages/404.tsx +6 -3
- package/dist/templates/react-app/src/pages/500.tsx +5 -2
- package/dist/templates/react-app/src/pages/NoRouteFound.tsx +5 -0
- package/dist/templates/react-app/src/pages/auth/Layout.tsx +9 -6
- package/dist/templates/react-app/src/pages/auth/LoginPage.tsx +46 -56
- package/dist/templates/react-app/src/pages/auth/RegisterPage.tsx +46 -58
- package/dist/templates/react-app/src/pages/base/AboutPage.tsx +35 -40
- package/dist/templates/react-app/src/pages/base/ExecutorPage.tsx +51 -51
- package/dist/templates/react-app/src/pages/base/HomePage.tsx +14 -15
- package/dist/templates/react-app/src/pages/base/IdentifierPage.tsx +70 -11
- package/dist/templates/react-app/src/pages/base/JSONStoragePage.tsx +24 -25
- package/dist/templates/react-app/src/pages/base/Layout.tsx +2 -2
- package/dist/templates/react-app/src/pages/base/RedirectPathname.tsx +3 -2
- package/dist/templates/react-app/src/pages/base/RequestPage.tsx +41 -59
- package/dist/templates/react-app/src/styles/css/antd-themes/{_default.css → _common/_default.css} +85 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/{dark.css → _common/dark.css} +99 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/_common/index.css +3 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/{pink.css → _common/pink.css} +86 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/index.css +4 -3
- package/dist/templates/react-app/src/styles/css/antd-themes/menu/_default.css +108 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/menu/dark.css +67 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/menu/index.css +3 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/menu/pink.css +67 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/pagination/_default.css +34 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/pagination/dark.css +31 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/pagination/index.css +3 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/pagination/pink.css +36 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/table/_default.css +44 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/table/dark.css +43 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/table/index.css +3 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/table/pink.css +43 -0
- package/dist/templates/react-app/src/styles/css/page.css +4 -3
- package/dist/templates/react-app/src/styles/css/themes/_default.css +1 -0
- package/dist/templates/react-app/src/styles/css/themes/dark.css +1 -0
- package/dist/templates/react-app/src/styles/css/themes/pink.css +1 -0
- package/dist/templates/react-app/src/styles/css/zIndex.css +1 -1
- package/dist/templates/react-app/src/uikit/bridges/ExecutorPageBridge.ts +3 -3
- package/dist/templates/react-app/src/uikit/bridges/JSONStoragePageBridge.ts +2 -2
- package/dist/templates/react-app/src/uikit/bridges/NavigateBridge.ts +1 -1
- package/dist/templates/react-app/src/uikit/bridges/RequestPageBridge.ts +3 -3
- package/dist/templates/react-app/src/uikit/components/AppRouterProvider.tsx +35 -0
- package/dist/templates/react-app/src/uikit/components/BaseHeader.tsx +15 -11
- package/dist/templates/react-app/src/uikit/components/BaseRouteProvider.tsx +14 -11
- package/dist/templates/react-app/src/uikit/components/BaseRouteSeo.tsx +18 -0
- package/dist/templates/react-app/src/uikit/components/BootstrapsProvider.tsx +13 -0
- package/dist/templates/react-app/src/uikit/components/ClientSeo.tsx +62 -0
- package/dist/templates/react-app/src/uikit/components/ComboProvider.tsx +38 -0
- package/dist/templates/react-app/src/uikit/components/LanguageSwitcher.tsx +48 -27
- package/dist/templates/react-app/src/uikit/components/Loading.tsx +4 -2
- package/dist/templates/react-app/src/uikit/components/LocaleLink.tsx +4 -5
- package/dist/templates/react-app/src/uikit/components/LogoutButton.tsx +34 -11
- package/dist/templates/react-app/src/uikit/components/ProcessExecutorProvider.tsx +9 -5
- package/dist/templates/react-app/src/uikit/components/RouterRenderComponent.tsx +6 -3
- package/dist/templates/react-app/src/uikit/components/ThemeSwitcher.tsx +97 -40
- package/dist/templates/react-app/src/uikit/components/UserAuthProvider.tsx +5 -5
- package/dist/templates/react-app/src/uikit/components/With.tsx +17 -0
- package/dist/templates/react-app/src/uikit/contexts/BaseRouteContext.ts +17 -11
- package/dist/templates/react-app/src/uikit/contexts/IOCContext.ts +13 -0
- package/dist/templates/react-app/src/uikit/hooks/useAppTranslation.ts +26 -0
- package/dist/templates/react-app/src/uikit/hooks/useI18nGuard.ts +8 -11
- package/dist/templates/react-app/src/uikit/hooks/useI18nInterface.ts +25 -0
- package/dist/templates/react-app/src/uikit/hooks/useIOC.ts +35 -0
- package/dist/templates/react-app/src/uikit/hooks/useNavigateBridge.ts +3 -3
- package/dist/templates/react-app/src/uikit/hooks/useStrictEffect.ts +0 -1
- package/dist/templates/react-app/tsconfig.e2e.json +21 -0
- package/dist/templates/react-app/tsconfig.json +8 -1
- package/dist/templates/react-app/tsconfig.node.json +1 -1
- package/dist/templates/react-app/tsconfig.test.json +3 -1
- package/dist/templates/react-app/vite.config.ts +50 -34
- package/package.json +2 -1
- package/dist/configs/react-app/eslint.config.js +0 -94
- package/dist/templates/next-app/config/Identifier/common.error.ts +0 -41
- package/dist/templates/next-app/config/Identifier/common.ts +0 -69
- package/dist/templates/next-app/config/Identifier/page.about.ts +0 -181
- package/dist/templates/next-app/config/Identifier/page.admin.ts +0 -48
- package/dist/templates/next-app/config/Identifier/page.executor.ts +0 -272
- package/dist/templates/next-app/config/Identifier/page.identifiter.ts +0 -39
- package/dist/templates/next-app/config/Identifier/page.jsonStorage.ts +0 -72
- package/dist/templates/next-app/config/Identifier/page.request.ts +0 -182
- package/dist/templates/next-app/src/base/cases/ChatAction.ts +0 -21
- package/dist/templates/next-app/src/base/cases/FocusBarAction.ts +0 -36
- package/dist/templates/next-app/src/base/cases/RequestState.ts +0 -20
- package/dist/templates/next-app/src/base/port/AdminPageInterface.ts +0 -85
- package/dist/templates/next-app/src/base/port/AsyncStateInterface.ts +0 -7
- package/dist/templates/next-app/src/base/services/AdminUserService.ts +0 -45
- package/dist/templates/next-app/src/uikit/components/ChatRoot.tsx +0 -17
- package/dist/templates/next-app/src/uikit/components/chat/ChatActionInterface.ts +0 -30
- package/dist/templates/next-app/src/uikit/components/chat/ChatFocusBar.tsx +0 -65
- package/dist/templates/next-app/src/uikit/components/chat/ChatMessages.tsx +0 -59
- package/dist/templates/next-app/src/uikit/components/chat/ChatWrap.tsx +0 -28
- package/dist/templates/next-app/src/uikit/components/chat/FocusBarActionInterface.ts +0 -19
- package/dist/templates/next-app/src/uikit/hook/useMountedClient.ts +0 -17
- package/dist/templates/next-app/src/uikit/hook/useStore.ts +0 -15
- package/dist/templates/react-app/__tests__/__mocks__/I18nService.ts +0 -13
- package/dist/templates/react-app/__tests__/src/App.test.tsx +0 -139
- package/dist/templates/react-app/config/Identifier/page.identifiter.ts +0 -39
- package/dist/templates/react-app/config/i18n.ts +0 -15
- package/dist/templates/react-app/docs/en/project-structure.md +0 -434
- package/dist/templates/react-app/docs/zh/project-structure.md +0 -434
- package/dist/templates/react-app/src/base/cases/RequestState.ts +0 -20
- package/dist/templates/react-app/src/base/port/AsyncStateInterface.ts +0 -7
- package/dist/templates/react-app/src/uikit/hooks/useDocumentTitle.ts +0 -15
- package/dist/templates/react-app/src/uikit/hooks/useStore.ts +0 -15
|
@@ -0,0 +1,797 @@
|
|
|
1
|
+
# 为什么要禁止直接使用浏览器全局变量?
|
|
2
|
+
|
|
3
|
+
## 📋 目录
|
|
4
|
+
|
|
5
|
+
- [核心理念](#-核心理念)
|
|
6
|
+
- [禁用的全局变量](#-禁用的全局变量)
|
|
7
|
+
- [允许使用的位置](#-允许使用的位置)
|
|
8
|
+
- [为什么要这样做](#-为什么要这样做)
|
|
9
|
+
- [实际应用场景](#-实际应用场景)
|
|
10
|
+
- [最佳实践](#-最佳实践)
|
|
11
|
+
- [常见问题](#-常见问题)
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 🎯 核心理念
|
|
16
|
+
|
|
17
|
+
在我们的项目中,禁止在业务代码中直接使用浏览器全局变量(如 `window`、`document`、`localStorage` 等),而是要求**通过应用入口或封装层传入**。
|
|
18
|
+
|
|
19
|
+
### 简单来说:
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
// ❌ 不允许:在业务组件中直接使用
|
|
23
|
+
function MyComponent() {
|
|
24
|
+
const width = window.innerWidth; // ESLint 错误!
|
|
25
|
+
return <div>宽度:{width}</div>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ✅ 推荐:从封装层导入
|
|
29
|
+
import { localStorage } from '@/core/globals';
|
|
30
|
+
|
|
31
|
+
function MyComponent() {
|
|
32
|
+
const token = localStorage.getItem('token'); // 正确!
|
|
33
|
+
return <div>Token: {token}</div>;
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 🚫 禁用的全局变量
|
|
40
|
+
|
|
41
|
+
以下全局变量在 `src/**/*.{ts,tsx,js,jsx}` 中被禁止直接使用:
|
|
42
|
+
|
|
43
|
+
- `window` - 浏览器窗口对象
|
|
44
|
+
- `document` - DOM 文档对象
|
|
45
|
+
- `localStorage` - 本地存储
|
|
46
|
+
- `sessionStorage` - 会话存储
|
|
47
|
+
- `navigator` - 浏览器信息
|
|
48
|
+
- `location` - URL 信息
|
|
49
|
+
- `history` - 浏览器历史
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## ✅ 允许使用的位置
|
|
54
|
+
|
|
55
|
+
### 1. **应用入口** (`src/main.tsx`)
|
|
56
|
+
|
|
57
|
+
这是唯一允许直接访问浏览器环境的地方,因为它是应用的启动点:
|
|
58
|
+
|
|
59
|
+
```typescript:1:19:src/main.tsx
|
|
60
|
+
// !only this file use `window`, `document` ...global variables
|
|
61
|
+
import 'reflect-metadata';
|
|
62
|
+
import { StrictMode } from 'react';
|
|
63
|
+
import { createRoot } from 'react-dom/client';
|
|
64
|
+
import App from './App.tsx';
|
|
65
|
+
import { BootstrapClient } from './core/bootstraps/BootstrapClient';
|
|
66
|
+
import { clientIOC } from './core/clientIoc/ClientIOC.ts';
|
|
67
|
+
|
|
68
|
+
BootstrapClient.main({
|
|
69
|
+
root: window, // ✅ 直接使用 window
|
|
70
|
+
bootHref: window.location.href, // ✅ 直接使用 location
|
|
71
|
+
ioc: clientIOC
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
createRoot(document.getElementById('root')!).render( // ✅ 直接使用 document
|
|
75
|
+
<StrictMode>
|
|
76
|
+
<App />
|
|
77
|
+
</StrictMode>
|
|
78
|
+
);
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**为什么?** 因为 `main.tsx` 负责将浏览器环境注入到应用中,它是"依赖注入"的起点。
|
|
82
|
+
|
|
83
|
+
### 2. **全局变量封装层** (`src/core/globals.ts`)
|
|
84
|
+
|
|
85
|
+
这是统一封装和管理全局变量的地方:
|
|
86
|
+
|
|
87
|
+
```typescript:38:48:src/core/globals.ts
|
|
88
|
+
/**
|
|
89
|
+
* Override localStorage to use the global local storage
|
|
90
|
+
*/
|
|
91
|
+
export const localStorage = new SyncStorage(new ObjectStorage(), [
|
|
92
|
+
JSON,
|
|
93
|
+
new Base64Serializer(),
|
|
94
|
+
window.localStorage as unknown as SyncStorageInterface<string> // ✅ 封装 localStorage
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
export const localStorageEncrypt = localStorage;
|
|
98
|
+
|
|
99
|
+
export const cookieStorage = new CookieStorage();
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**为什么?** 这里是封装层,负责将原始的浏览器 API 包装成统一的、类型安全的接口。
|
|
103
|
+
|
|
104
|
+
### 3. **特殊的基础设施层**
|
|
105
|
+
|
|
106
|
+
某些基础设施代码(如 IOC 容器初始化)可能需要访问全局变量,但应该:
|
|
107
|
+
|
|
108
|
+
#### ⚠️ 情况 A:不推荐但可接受
|
|
109
|
+
|
|
110
|
+
在 `ClientIOC.ts` 中直接使用:
|
|
111
|
+
|
|
112
|
+
```typescript:28:30:src/core/clientIoc/ClientIOC.ts
|
|
113
|
+
const register = new ClientIOCRegister({
|
|
114
|
+
pathname: window.location.pathname, // ⚠️ 特殊情况,可以使用
|
|
115
|
+
appConfig: appConfig
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**说明:** IOC 容器初始化时需要 `pathname`,这是可以接受的,但不是最佳实践。
|
|
120
|
+
|
|
121
|
+
#### ✅ 情况 B:更好的做法(推荐)
|
|
122
|
+
|
|
123
|
+
通过 `main.tsx` 传入:
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
// main.tsx
|
|
127
|
+
BootstrapClient.main({
|
|
128
|
+
root: window,
|
|
129
|
+
bootHref: window.location.href, // 在入口获取
|
|
130
|
+
pathname: window.location.pathname, // 通过参数传入
|
|
131
|
+
ioc: clientIOC
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// ClientIOC.ts
|
|
135
|
+
create(pathname: string) { // 接收参数而不是直接访问
|
|
136
|
+
const register = new ClientIOCRegister({
|
|
137
|
+
pathname: pathname, // ✅ 使用传入的参数
|
|
138
|
+
appConfig: appConfig
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## 🤔 为什么要这样做?
|
|
146
|
+
|
|
147
|
+
### 1. **测试友好** 🧪
|
|
148
|
+
|
|
149
|
+
直接使用全局变量会让测试变得**极其困难甚至不可能**。
|
|
150
|
+
|
|
151
|
+
#### ❌ 问题示例:难以测试的组件
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
// UserProfile.tsx - 直接使用全局变量
|
|
155
|
+
function UserProfile() {
|
|
156
|
+
const [user, setUser] = useState(null);
|
|
157
|
+
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
// 直接使用 fetch
|
|
160
|
+
fetch('/api/user')
|
|
161
|
+
.then(res => res.json())
|
|
162
|
+
.then(data => {
|
|
163
|
+
// 直接使用 localStorage
|
|
164
|
+
localStorage.setItem('lastUser', data.id);
|
|
165
|
+
setUser(data);
|
|
166
|
+
});
|
|
167
|
+
}, []);
|
|
168
|
+
|
|
169
|
+
return <div>{user?.name}</div>;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ❌ 测试代码 - 几乎无法测试
|
|
173
|
+
describe('UserProfile', () => {
|
|
174
|
+
it('should load and display user', async () => {
|
|
175
|
+
// 问题 1:如何 mock fetch?需要 polyfill 或全局 mock
|
|
176
|
+
global.fetch = jest.fn();
|
|
177
|
+
|
|
178
|
+
// 问题 2:如何 mock localStorage?需要手动实现
|
|
179
|
+
const mockLocalStorage = {
|
|
180
|
+
setItem: jest.fn()
|
|
181
|
+
};
|
|
182
|
+
global.localStorage = mockLocalStorage as any;
|
|
183
|
+
|
|
184
|
+
// 问题 3:需要清理全局状态,否则影响其他测试
|
|
185
|
+
// 问题 4:多个测试之间可能互相干扰
|
|
186
|
+
|
|
187
|
+
render(<UserProfile />);
|
|
188
|
+
// 难以验证行为...
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**问题:**
|
|
194
|
+
|
|
195
|
+
- 😰 需要 mock 全局变量(fetch、localStorage)
|
|
196
|
+
- 😰 测试之间可能互相干扰
|
|
197
|
+
- 😰 难以测试错误场景
|
|
198
|
+
- 😰 测试代码充满技巧和 hack
|
|
199
|
+
- 😰 在 Node.js 环境中可能根本无法运行
|
|
200
|
+
|
|
201
|
+
#### ✅ 解决方案 1:从封装层导入
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
// UserProfile.tsx - 从封装层导入
|
|
205
|
+
import { localStorage } from '@/core/globals';
|
|
206
|
+
|
|
207
|
+
function getUser() {
|
|
208
|
+
return fetch('/api/user').then((res) => res.json());
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ✅ 测试代码 - 更容易测试
|
|
212
|
+
jest.mock('@/core/globals', () => ({
|
|
213
|
+
localStorage: {
|
|
214
|
+
setItem: jest.fn()
|
|
215
|
+
}
|
|
216
|
+
}));
|
|
217
|
+
|
|
218
|
+
describe('UserProfile', () => {
|
|
219
|
+
it('should save user to localStorage', () => {
|
|
220
|
+
// 相对容易 mock,但仍需处理 fetch
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
#### ⭐ 解决方案 2:使用 IOC 容器(最佳)
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
// UserProfile.tsx - 使用 IOC 容器
|
|
229
|
+
import { useIoc } from '@/uikit/hooks/useIoc';
|
|
230
|
+
|
|
231
|
+
function UserProfile() {
|
|
232
|
+
const userService = useIoc('UserService');
|
|
233
|
+
const [user, setUser] = useState(null);
|
|
234
|
+
|
|
235
|
+
useEffect(() => {
|
|
236
|
+
userService.getCurrentUser().then(setUser);
|
|
237
|
+
}, []);
|
|
238
|
+
|
|
239
|
+
return <div>{user?.name}</div>;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ✅✅ 测试代码 - 非常容易!
|
|
243
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
244
|
+
import { IocProvider } from '@/contexts/IocContext';
|
|
245
|
+
|
|
246
|
+
describe('UserProfile', () => {
|
|
247
|
+
it('should load and display user', async () => {
|
|
248
|
+
// ✅ 只需要 mock 服务,不需要 mock 全局变量
|
|
249
|
+
const mockUserService = {
|
|
250
|
+
getCurrentUser: jest.fn().mockResolvedValue({
|
|
251
|
+
id: '1',
|
|
252
|
+
name: 'John Doe'
|
|
253
|
+
})
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const mockIoc = (serviceName: string) => {
|
|
257
|
+
if (serviceName === 'UserService') return mockUserService;
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
render(
|
|
261
|
+
<IocProvider value={mockIoc}>
|
|
262
|
+
<UserProfile />
|
|
263
|
+
</IocProvider>
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
// ✅ 清晰的断言
|
|
267
|
+
await waitFor(() => {
|
|
268
|
+
expect(screen.getByText('John Doe')).toBeInTheDocument();
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// ✅ 验证服务调用
|
|
272
|
+
expect(mockUserService.getCurrentUser).toHaveBeenCalledTimes(1);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('should handle error', async () => {
|
|
276
|
+
// ✅ 轻松测试错误场景
|
|
277
|
+
const mockUserService = {
|
|
278
|
+
getCurrentUser: jest.fn().mockRejectedValue(new Error('Network error'))
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
// 测试错误处理...
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('should handle loading state', () => {
|
|
285
|
+
// ✅ 轻松测试加载状态
|
|
286
|
+
const mockUserService = {
|
|
287
|
+
getCurrentUser: jest.fn().mockReturnValue(new Promise(() => {})) // 永不 resolve
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
// 测试加载中状态...
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
#### 对比总结
|
|
296
|
+
|
|
297
|
+
| 测试场景 | 直接使用全局变量 | 使用封装层 | 使用 IOC 容器 |
|
|
298
|
+
| --------------- | ---------------- | ---------- | ------------- |
|
|
299
|
+
| Mock 复杂度 | 😰😰😰 很难 | 😐 中等 | 😊😊😊 简单 |
|
|
300
|
+
| 测试隔离性 | ❌ 差 | ⚠️ 一般 | ✅ 好 |
|
|
301
|
+
| 测试错误场景 | ❌ 困难 | ⚠️ 可以 | ✅ 容易 |
|
|
302
|
+
| 测试代码可读性 | ❌ 差 | ⚠️ 一般 | ✅ 好 |
|
|
303
|
+
| 在 Node.js 运行 | ❌ 困难 | ✅ 可以 | ✅ 可以 |
|
|
304
|
+
|
|
305
|
+
**关键优势:**
|
|
306
|
+
|
|
307
|
+
- ✅ **Mock 简单**:只需 mock 一个服务对象,不需要 mock 全局环境
|
|
308
|
+
- ✅ **测试隔离**:每个测试有独立的 mock,互不干扰
|
|
309
|
+
- ✅ **易测错误**:轻松模拟各种错误场景(网络错误、超时、权限错误等)
|
|
310
|
+
- ✅ **快速运行**:不需要真实的浏览器环境,测试跑得更快
|
|
311
|
+
- ✅ **代码清晰**:测试代码简单直观,易于维护
|
|
312
|
+
|
|
313
|
+
### 2. **SSR/多环境兼容** 🌐
|
|
314
|
+
|
|
315
|
+
如果你的应用需要支持服务端渲染(如 Next.js),直接使用全局变量会导致错误:
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
// ❌ SSR 时会报错
|
|
319
|
+
function MyComponent() {
|
|
320
|
+
const width = window.innerWidth; // ReferenceError: window is not defined
|
|
321
|
+
return <div>{width}</div>;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ✅ 安全的方式
|
|
325
|
+
import { getWindow } from '@/core/globals';
|
|
326
|
+
|
|
327
|
+
function MyComponent() {
|
|
328
|
+
const win = getWindow(); // 封装层可以处理 SSR 情况
|
|
329
|
+
const width = win ? win.innerWidth : 0;
|
|
330
|
+
return <div>{width}</div>;
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### 3. **类型安全和错误处理** 🛡️
|
|
335
|
+
|
|
336
|
+
封装层可以提供更好的类型和错误处理:
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
// src/core/globals.ts
|
|
340
|
+
export const localStorage = new SyncStorage(/* ... */); // 有完整的类型定义
|
|
341
|
+
|
|
342
|
+
// 业务代码
|
|
343
|
+
import { localStorage } from '@/core/globals';
|
|
344
|
+
|
|
345
|
+
// ✅ 有完整的类型提示和错误处理
|
|
346
|
+
localStorage.setItem('key', value); // TypeScript 会检查类型
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### 4. **代码可追踪性** 🔍
|
|
350
|
+
|
|
351
|
+
通过 ESLint 规则,我们可以:
|
|
352
|
+
|
|
353
|
+
- **一眼看出** 哪些代码依赖浏览器环境
|
|
354
|
+
- **轻松查找** 所有使用浏览器 API 的地方(搜索 `from '@/core/globals'`)
|
|
355
|
+
- **方便重构** 统一修改所有浏览器 API 调用
|
|
356
|
+
|
|
357
|
+
```typescript
|
|
358
|
+
// 想知道哪里用了 localStorage?
|
|
359
|
+
// 搜索:import { localStorage } from '@/core/globals'
|
|
360
|
+
// 而不是在整个项目中搜索 "localStorage"(会有很多误报)
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### 5. **统一的降级和 Polyfill** 🔄
|
|
364
|
+
|
|
365
|
+
在封装层可以统一处理兼容性和降级:
|
|
366
|
+
|
|
367
|
+
```typescript
|
|
368
|
+
// src/core/globals.ts
|
|
369
|
+
export const localStorage = (() => {
|
|
370
|
+
try {
|
|
371
|
+
const storage = window.localStorage;
|
|
372
|
+
// 测试是否可用
|
|
373
|
+
storage.setItem('__test__', '1');
|
|
374
|
+
storage.removeItem('__test__');
|
|
375
|
+
return storage;
|
|
376
|
+
} catch {
|
|
377
|
+
// 降级到内存存储(如隐私模式)
|
|
378
|
+
console.warn('localStorage 不可用,使用内存存储');
|
|
379
|
+
return new MemoryStorage();
|
|
380
|
+
}
|
|
381
|
+
})();
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### 6. **防止意外耦合** 🚫
|
|
385
|
+
|
|
386
|
+
强制开发者思考:
|
|
387
|
+
|
|
388
|
+
- 这段代码真的需要依赖浏览器环境吗?
|
|
389
|
+
- 可以写成纯函数吗?
|
|
390
|
+
- 是否可以通过参数传入而不是直接访问?
|
|
391
|
+
|
|
392
|
+
```typescript
|
|
393
|
+
// ❌ 紧密耦合浏览器环境
|
|
394
|
+
function isDesktop() {
|
|
395
|
+
return window.innerWidth > 768;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// ✅ 解耦:通过参数传入
|
|
399
|
+
function isDesktop(width: number) {
|
|
400
|
+
return width > 768;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// 在调用处传入
|
|
404
|
+
const desktop = isDesktop(window.innerWidth);
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
---
|
|
408
|
+
|
|
409
|
+
## 💡 实际应用场景
|
|
410
|
+
|
|
411
|
+
### 场景 1:需要操作 localStorage
|
|
412
|
+
|
|
413
|
+
```typescript
|
|
414
|
+
// ❌ 错误做法:直接使用浏览器 API
|
|
415
|
+
function saveToken(token: string) {
|
|
416
|
+
localStorage.setItem('token', token); // ESLint 错误!
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// ✅ 正确做法 1:从 globals 导入封装的 storage
|
|
420
|
+
import { localStorage } from '@/core/globals';
|
|
421
|
+
|
|
422
|
+
function saveToken(token: string) {
|
|
423
|
+
localStorage.setItem('token', token); // 使用封装的 localStorage
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// ✅ 正确做法 2:通过 IOC 容器获取服务(推荐)
|
|
427
|
+
import { useIoc } from '@/uikit/hooks/useIoc';
|
|
428
|
+
|
|
429
|
+
function useAuth() {
|
|
430
|
+
const authService = useIoc('AuthService'); // 从 IOC 容器获取服务
|
|
431
|
+
|
|
432
|
+
const saveToken = (token: string) => {
|
|
433
|
+
authService.setToken(token); // 服务内部已封装 storage 操作
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
return { saveToken };
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// 在组件中使用
|
|
440
|
+
function LoginComponent() {
|
|
441
|
+
const { saveToken } = useAuth();
|
|
442
|
+
|
|
443
|
+
const handleLogin = async () => {
|
|
444
|
+
const token = await login();
|
|
445
|
+
saveToken(token); // 不需要关心底层是用 localStorage 还是其他存储
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
**为什么 IOC 方式更好?**
|
|
451
|
+
|
|
452
|
+
- 服务层已经封装了所有存储逻辑
|
|
453
|
+
- 业务代码不需要关心存储实现细节
|
|
454
|
+
- 易于切换存储方式(localStorage → IndexedDB → 服务器)
|
|
455
|
+
- 服务可以包含更多业务逻辑(加密、验证、过期处理等)
|
|
456
|
+
|
|
457
|
+
### 场景 2:需要获取当前路径
|
|
458
|
+
|
|
459
|
+
```typescript
|
|
460
|
+
// ❌ 错误做法:在组件中直接访问
|
|
461
|
+
function MyComponent() {
|
|
462
|
+
const path = window.location.pathname; // ESLint 错误!
|
|
463
|
+
// ...
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// ✅ 正确做法 1:使用 React Router
|
|
467
|
+
import { useLocation } from 'react-router-dom';
|
|
468
|
+
|
|
469
|
+
function MyComponent() {
|
|
470
|
+
const location = useLocation();
|
|
471
|
+
const path = location.pathname; // 通过 Router 提供的 hook
|
|
472
|
+
// ...
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// ✅ 正确做法 2:通过 IOC 容器获取路由服务
|
|
476
|
+
import { useIoc } from '@/uikit/hooks/useIoc';
|
|
477
|
+
|
|
478
|
+
function MyComponent() {
|
|
479
|
+
const routerService = useIoc('RouterService'); // 从 IOC 获取路由服务
|
|
480
|
+
const path = routerService.getCurrentPath(); // 通过服务获取路径
|
|
481
|
+
|
|
482
|
+
// 路由服务还可以提供更多功能
|
|
483
|
+
const navigate = (path: string) => {
|
|
484
|
+
routerService.navigate(path); // 统一的路由跳转
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
### 场景 3:需要发起 HTTP 请求
|
|
490
|
+
|
|
491
|
+
```typescript
|
|
492
|
+
// ❌ 错误做法:直接使用 fetch
|
|
493
|
+
async function getUserInfo(id: string) {
|
|
494
|
+
const response = await fetch(`/api/users/${id}`); // 直接使用全局 fetch
|
|
495
|
+
return response.json();
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// ✅ 正确做法:通过 IOC 容器获取 HTTP 服务
|
|
499
|
+
import { useIoc } from '@/uikit/hooks/useIoc';
|
|
500
|
+
|
|
501
|
+
function useUserService() {
|
|
502
|
+
const httpService = useIoc('HttpService'); // 从 IOC 获取 HTTP 服务
|
|
503
|
+
|
|
504
|
+
const getUserInfo = async (id: string) => {
|
|
505
|
+
// HTTP 服务已经封装了:
|
|
506
|
+
// - 统一的错误处理
|
|
507
|
+
// - 请求拦截器(添加 token)
|
|
508
|
+
// - 响应拦截器(处理错误码)
|
|
509
|
+
// - 请求取消
|
|
510
|
+
// - 超时控制
|
|
511
|
+
return httpService.get(`/users/${id}`);
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
return { getUserInfo };
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// 在组件中使用
|
|
518
|
+
function UserProfile({ userId }: { userId: string }) {
|
|
519
|
+
const { getUserInfo } = useUserService();
|
|
520
|
+
const [user, setUser] = useState(null);
|
|
521
|
+
|
|
522
|
+
useEffect(() => {
|
|
523
|
+
getUserInfo(userId).then(setUser);
|
|
524
|
+
}, [userId]);
|
|
525
|
+
|
|
526
|
+
return <div>{user?.name}</div>;
|
|
527
|
+
}
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
### 场景 4:需要国际化翻译
|
|
531
|
+
|
|
532
|
+
```typescript
|
|
533
|
+
// ❌ 错误做法:直接依赖全局 i18n 实例
|
|
534
|
+
import i18n from 'i18next';
|
|
535
|
+
|
|
536
|
+
function MyComponent() {
|
|
537
|
+
const text = i18n.t('common.welcome'); // 直接依赖全局实例
|
|
538
|
+
return <div>{text}</div>;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// ✅ 正确做法:通过 IOC 容器获取 I18n 服务
|
|
542
|
+
import { useIoc } from '@/uikit/hooks/useIoc';
|
|
543
|
+
|
|
544
|
+
function MyComponent() {
|
|
545
|
+
const i18nService = useIoc('I18nService'); // 从 IOC 获取服务
|
|
546
|
+
const text = i18nService.t('common.welcome'); // 通过服务翻译
|
|
547
|
+
|
|
548
|
+
// I18n 服务还提供更多功能
|
|
549
|
+
const changeLanguage = (lang: string) => {
|
|
550
|
+
i18nService.changeLanguage(lang);
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
return (
|
|
554
|
+
<div>
|
|
555
|
+
{text}
|
|
556
|
+
<button onClick={() => changeLanguage('en')}>English</button>
|
|
557
|
+
</div>
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
### 场景 5:需要获取窗口宽度
|
|
563
|
+
|
|
564
|
+
```typescript
|
|
565
|
+
// ❌ 错误做法
|
|
566
|
+
function useWindowSize() {
|
|
567
|
+
const [size, setSize] = useState(window.innerWidth); // ESLint 错误!
|
|
568
|
+
// ...
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// ✅ 正确做法 1:从 globals 导入
|
|
572
|
+
import { window } from '@/core/globals';
|
|
573
|
+
|
|
574
|
+
function useWindowSize() {
|
|
575
|
+
const [size, setSize] = useState(window?.innerWidth || 0);
|
|
576
|
+
// ...
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// ✅ 正确做法 2:通过 IOC 容器获取 Window 服务(最佳)
|
|
580
|
+
import { useIoc } from '@/uikit/hooks/useIoc';
|
|
581
|
+
|
|
582
|
+
function useWindowSize() {
|
|
583
|
+
const windowService = useIoc('WindowService');
|
|
584
|
+
const [size, setSize] = useState(windowService.getWidth());
|
|
585
|
+
|
|
586
|
+
useEffect(() => {
|
|
587
|
+
const unsubscribe = windowService.onResize((newSize) => {
|
|
588
|
+
setSize(newSize.width);
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
return unsubscribe; // 服务内部管理事件监听器
|
|
592
|
+
}, []);
|
|
593
|
+
|
|
594
|
+
return size;
|
|
595
|
+
}
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
---
|
|
599
|
+
|
|
600
|
+
## 📖 最佳实践
|
|
601
|
+
|
|
602
|
+
### 1. **优先使用 IOC 容器获取服务(推荐)** ⭐
|
|
603
|
+
|
|
604
|
+
```typescript
|
|
605
|
+
// ✅ 最佳实践:通过 IOC 容器获取服务
|
|
606
|
+
import { useIoc } from '@/uikit/hooks/useIoc';
|
|
607
|
+
|
|
608
|
+
function MyComponent() {
|
|
609
|
+
const authService = useIoc('AuthService');
|
|
610
|
+
const i18nService = useIoc('I18nService');
|
|
611
|
+
const httpService = useIoc('HttpService');
|
|
612
|
+
|
|
613
|
+
// 业务逻辑...
|
|
614
|
+
}
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
**为什么?**
|
|
618
|
+
|
|
619
|
+
- 服务已经封装了所有底层依赖(包括全局变量)
|
|
620
|
+
- 易于测试(可以 mock 整个服务)
|
|
621
|
+
- 业务代码不需要关心实现细节
|
|
622
|
+
- 统一的依赖管理
|
|
623
|
+
|
|
624
|
+
### 2. **在应用入口注入依赖**
|
|
625
|
+
|
|
626
|
+
```typescript
|
|
627
|
+
// main.tsx
|
|
628
|
+
BootstrapClient.main({
|
|
629
|
+
root: window,
|
|
630
|
+
bootHref: window.location.href,
|
|
631
|
+
ioc: clientIOC,
|
|
632
|
+
// 其他需要的浏览器信息
|
|
633
|
+
initialWindowSize: {
|
|
634
|
+
width: window.innerWidth,
|
|
635
|
+
height: window.innerHeight
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
### 3. **优先使用 React 生态的解决方案**
|
|
641
|
+
|
|
642
|
+
- 使用 `react-router-dom` 而不是直接访问 `location`
|
|
643
|
+
- 使用 CSS 媒体查询或 `useMediaQuery` 而不是读取 `window.innerWidth`
|
|
644
|
+
- 使用 React 的事件系统而不是 `document.addEventListener`
|
|
645
|
+
|
|
646
|
+
### 4. **次选:通过封装层访问**
|
|
647
|
+
|
|
648
|
+
如果没有相应的服务,可以从 `@/core/globals` 导入:
|
|
649
|
+
|
|
650
|
+
```typescript
|
|
651
|
+
// src/core/globals.ts
|
|
652
|
+
export const getDocument = () => {
|
|
653
|
+
if (typeof document === 'undefined') {
|
|
654
|
+
throw new Error('document is not available in SSR');
|
|
655
|
+
}
|
|
656
|
+
return document;
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
// 业务代码
|
|
660
|
+
import { getDocument } from '@/core/globals';
|
|
661
|
+
|
|
662
|
+
const doc = getDocument();
|
|
663
|
+
const element = doc.getElementById('root');
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
### 5. **特殊情况要文档化**
|
|
667
|
+
|
|
668
|
+
如果某个基础设施层必须直接访问全局变量,添加注释说明原因:
|
|
669
|
+
|
|
670
|
+
```typescript
|
|
671
|
+
// ClientIOC.ts
|
|
672
|
+
create() {
|
|
673
|
+
// 注意:这里直接使用 window.location.pathname
|
|
674
|
+
// 原因:IOC 容器初始化时需要,且在 main.tsx 之后执行,浏览器环境确保可用
|
|
675
|
+
// TODO: 考虑通过 BootstrapClient 传入,避免直接访问
|
|
676
|
+
const pathname = window.location.pathname;
|
|
677
|
+
// ...
|
|
678
|
+
}
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
### 6. **推荐的解决方案优先级**
|
|
682
|
+
|
|
683
|
+
```
|
|
684
|
+
1️⃣ 使用 IOC 容器服务 (useIoc('XxxService')) ⭐ 最佳
|
|
685
|
+
2️⃣ 使用 React 生态方案 (useLocation, useMediaQuery) 👍 推荐
|
|
686
|
+
3️⃣ 从 globals 导入 (import { xxx } from '@/core/globals') ✅ 可以
|
|
687
|
+
4️⃣ 直接访问全局变量 (window.xxx) ❌ 禁止
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
---
|
|
691
|
+
|
|
692
|
+
## ❓ 常见问题
|
|
693
|
+
|
|
694
|
+
### Q1: 我的代码很简单,为什么还要这么麻烦?
|
|
695
|
+
|
|
696
|
+
**A:** 架构规范不是为了"当前",而是为了:
|
|
697
|
+
|
|
698
|
+
- 未来可能的 SSR 需求
|
|
699
|
+
- 更容易编写单元测试
|
|
700
|
+
- 团队协作时的一致性
|
|
701
|
+
- 代码的可维护性和可追踪性
|
|
702
|
+
|
|
703
|
+
### Q2: 如果我确实需要在某个文件中直接使用全局变量怎么办?
|
|
704
|
+
|
|
705
|
+
**A:** 在 `eslint.config.mjs` 中添加例外:
|
|
706
|
+
|
|
707
|
+
```javascript
|
|
708
|
+
{
|
|
709
|
+
files: [
|
|
710
|
+
'src/main.tsx',
|
|
711
|
+
'src/core/globals.ts',
|
|
712
|
+
'src/utils/dom-helper.ts' // 添加你的文件
|
|
713
|
+
],
|
|
714
|
+
rules: {
|
|
715
|
+
'no-restricted-globals': 'off'
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
但要慎重考虑,并添加注释说明原因。
|
|
721
|
+
|
|
722
|
+
### Q3: `@/core/globals` 和直接使用 `window.xxx` 有什么区别?
|
|
723
|
+
|
|
724
|
+
**A:** 主要区别:
|
|
725
|
+
|
|
726
|
+
1. **类型安全**:封装层提供完整的 TypeScript 类型
|
|
727
|
+
2. **错误处理**:封装层可以处理 SSR、隐私模式等特殊情况
|
|
728
|
+
3. **统一管理**:所有浏览器 API 访问都在一个地方,便于追踪和修改
|
|
729
|
+
4. **可测试性**:可以轻松 mock 整个 `@/core/globals` 模块
|
|
730
|
+
|
|
731
|
+
### Q4: 为什么 `ClientIOC` 可以直接使用 `window.location.pathname`?
|
|
732
|
+
|
|
733
|
+
**A:** 这是一个**权衡**:
|
|
734
|
+
|
|
735
|
+
- **可以接受**:因为 `ClientIOC` 是基础设施层,且在 `main.tsx` 之后执行,浏览器环境确保可用
|
|
736
|
+
- **更好的做法**:通过 `BootstrapClient.main()` 传入 `pathname` 参数
|
|
737
|
+
- **未来改进**:计划重构为依赖注入方式
|
|
738
|
+
|
|
739
|
+
---
|
|
740
|
+
|
|
741
|
+
## 🎯 总结
|
|
742
|
+
|
|
743
|
+
### 允许使用全局变量的位置
|
|
744
|
+
|
|
745
|
+
| 位置 | 是否允许 | 说明 |
|
|
746
|
+
| --------------------------------- | ----------- | ---------------------------- |
|
|
747
|
+
| `src/main.tsx` | ✅ 允许 | 应用入口,负责注入依赖 |
|
|
748
|
+
| `src/core/globals.ts` | ✅ 允许 | 封装层,统一管理全局变量 |
|
|
749
|
+
| `src/core/clientIoc/ClientIOC.ts` | ⚠️ 特殊情况 | 基础设施层,建议改为注入方式 |
|
|
750
|
+
| 其他业务代码 | ❌ 禁止 | 必须通过封装层或依赖注入访问 |
|
|
751
|
+
|
|
752
|
+
### 业务代码如何访问浏览器 API
|
|
753
|
+
|
|
754
|
+
```typescript
|
|
755
|
+
// 优先级从高到低
|
|
756
|
+
|
|
757
|
+
// 🥇 方式 1:通过 IOC 容器获取服务(最推荐)
|
|
758
|
+
const authService = useIoc('AuthService');
|
|
759
|
+
authService.setToken(token); // 服务内部处理 storage
|
|
760
|
+
|
|
761
|
+
// 🥈 方式 2:使用 React 生态方案
|
|
762
|
+
const location = useLocation(); // react-router-dom
|
|
763
|
+
const path = location.pathname;
|
|
764
|
+
|
|
765
|
+
// 🥉 方式 3:从 globals 导入封装
|
|
766
|
+
import { localStorage } from '@/core/globals';
|
|
767
|
+
localStorage.setItem('key', value);
|
|
768
|
+
|
|
769
|
+
// ❌ 方式 4:直接访问(禁止!)
|
|
770
|
+
window.localStorage.setItem('key', value); // ESLint 错误
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
### 记住三个原则:
|
|
774
|
+
|
|
775
|
+
1. **在入口注入** - `main.tsx` 是唯一直接访问浏览器环境的地方
|
|
776
|
+
2. **在封装层封装** - `core/globals.ts` 或服务层提供统一接口
|
|
777
|
+
3. **在业务层使用** - 优先通过 IOC 容器获取服务,次选从封装层导入
|
|
778
|
+
|
|
779
|
+
### 为什么要这样做?
|
|
780
|
+
|
|
781
|
+
✅ **易于测试** - 可以轻松 mock 服务或封装层
|
|
782
|
+
✅ **SSR 兼容** - 封装层可以处理服务端渲染场景
|
|
783
|
+
✅ **类型安全** - 完整的 TypeScript 类型支持
|
|
784
|
+
✅ **易于追踪** - 统一的依赖管理,便于查找和重构
|
|
785
|
+
✅ **降级处理** - 统一处理浏览器兼容性和降级策略
|
|
786
|
+
✅ **解耦业务** - 业务代码不依赖具体实现
|
|
787
|
+
|
|
788
|
+
---
|
|
789
|
+
|
|
790
|
+
**相关文档:**
|
|
791
|
+
|
|
792
|
+
- [ESLint 配置说明](../../eslint.config.mjs)
|
|
793
|
+
- [依赖注入模式](./dependency-injection.md)
|
|
794
|
+
- [项目架构设计](./index.md)
|
|
795
|
+
|
|
796
|
+
**需要帮助?**
|
|
797
|
+
如果你不确定某个场景应该如何处理,请在团队频道中询问或提交 Issue。
|