@qlover/create-app 0.6.2 → 0.7.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 +53 -0
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/templates/react-app/README.en.md +257 -0
- package/dist/templates/react-app/README.md +29 -231
- package/dist/templates/react-app/__tests__/__mocks__/I18nService.ts +13 -0
- package/dist/templates/react-app/__tests__/__mocks__/MockAppConfit.ts +48 -0
- package/dist/templates/react-app/__tests__/__mocks__/MockDialogHandler.ts +16 -0
- package/dist/templates/react-app/__tests__/__mocks__/MockLogger.ts +14 -0
- package/dist/templates/react-app/__tests__/__mocks__/createMockGlobals.ts +92 -0
- package/dist/templates/react-app/__tests__/setup/index.ts +51 -0
- package/dist/templates/react-app/__tests__/src/App.test.tsx +139 -0
- package/dist/templates/react-app/__tests__/src/base/cases/AppConfig.test.ts +288 -0
- package/dist/templates/react-app/__tests__/src/base/cases/AppError.test.ts +102 -0
- package/dist/templates/react-app/__tests__/src/base/cases/DialogHandler.test.ts +228 -0
- package/dist/templates/react-app/__tests__/src/base/cases/I18nKeyErrorPlugin.test.ts +207 -0
- package/dist/templates/react-app/__tests__/src/base/cases/InversifyContainer.test.ts +181 -0
- package/dist/templates/react-app/__tests__/src/base/cases/PublicAssetsPath.test.ts +61 -0
- package/dist/templates/react-app/__tests__/src/base/cases/RequestLogger.test.ts +199 -0
- package/dist/templates/react-app/__tests__/src/base/cases/RequestStatusCatcher.test.ts +192 -0
- package/dist/templates/react-app/__tests__/src/base/cases/RouterLoader.test.ts +235 -0
- package/dist/templates/react-app/__tests__/src/base/services/I18nService.test.ts +224 -0
- package/dist/templates/react-app/__tests__/src/core/IOC.test.ts +257 -0
- package/dist/templates/react-app/__tests__/src/core/bootstraps/BootstrapsApp.test.ts +72 -0
- package/dist/templates/react-app/__tests__/src/main.integration.test.tsx +62 -0
- package/dist/templates/react-app/__tests__/src/main.test.tsx +46 -0
- package/dist/templates/react-app/__tests__/src/uikit/components/BaseHeader.test.tsx +88 -0
- package/dist/templates/react-app/config/app.router.ts +155 -0
- package/dist/templates/react-app/config/common.ts +9 -1
- package/dist/templates/react-app/docs/en/bootstrap.md +562 -0
- package/dist/templates/react-app/docs/en/development-guide.md +523 -0
- package/dist/templates/react-app/docs/en/env.md +482 -0
- package/dist/templates/react-app/docs/en/global.md +509 -0
- package/dist/templates/react-app/docs/en/i18n.md +268 -0
- package/dist/templates/react-app/docs/en/index.md +173 -0
- package/dist/templates/react-app/docs/en/ioc.md +424 -0
- package/dist/templates/react-app/docs/en/project-structure.md +434 -0
- package/dist/templates/react-app/docs/en/request.md +425 -0
- package/dist/templates/react-app/docs/en/router.md +404 -0
- package/dist/templates/react-app/docs/en/store.md +321 -0
- package/dist/templates/react-app/docs/en/test-guide.md +782 -0
- package/dist/templates/react-app/docs/en/theme.md +424 -0
- package/dist/templates/react-app/docs/en/typescript-guide.md +473 -0
- package/dist/templates/react-app/docs/zh/bootstrap.md +7 -0
- package/dist/templates/react-app/docs/zh/development-guide.md +523 -0
- package/dist/templates/react-app/docs/zh/env.md +24 -25
- package/dist/templates/react-app/docs/zh/global.md +28 -27
- package/dist/templates/react-app/docs/zh/i18n.md +268 -0
- package/dist/templates/react-app/docs/zh/index.md +173 -0
- package/dist/templates/react-app/docs/zh/ioc.md +44 -32
- package/dist/templates/react-app/docs/zh/project-structure.md +434 -0
- package/dist/templates/react-app/docs/zh/request.md +429 -0
- package/dist/templates/react-app/docs/zh/router.md +408 -0
- package/dist/templates/react-app/docs/zh/store.md +321 -0
- package/dist/templates/react-app/docs/zh/test-guide.md +782 -0
- package/dist/templates/react-app/docs/zh/theme.md +424 -0
- package/dist/templates/react-app/docs/zh/typescript-guide.md +473 -0
- package/dist/templates/react-app/package.json +9 -20
- package/dist/templates/react-app/src/base/cases/AppConfig.ts +16 -9
- package/dist/templates/react-app/src/base/cases/PublicAssetsPath.ts +7 -1
- package/dist/templates/react-app/src/base/services/I18nService.ts +15 -4
- package/dist/templates/react-app/src/base/services/RouteService.ts +43 -7
- package/dist/templates/react-app/src/core/bootstraps/BootstrapApp.ts +31 -10
- package/dist/templates/react-app/src/core/bootstraps/BootstrapsRegistry.ts +1 -1
- package/dist/templates/react-app/src/core/globals.ts +1 -3
- package/dist/templates/react-app/src/core/registers/RegisterCommon.ts +5 -3
- package/dist/templates/react-app/src/main.tsx +6 -1
- package/dist/templates/react-app/src/pages/404.tsx +0 -1
- package/dist/templates/react-app/src/pages/500.tsx +1 -1
- package/dist/templates/react-app/src/pages/base/RedirectPathname.tsx +3 -1
- package/dist/templates/react-app/src/styles/css/antd-themes/dark.css +3 -1
- package/dist/templates/react-app/src/styles/css/antd-themes/index.css +1 -1
- package/dist/templates/react-app/src/styles/css/antd-themes/pink.css +6 -1
- package/dist/templates/react-app/src/styles/css/page.css +1 -1
- package/dist/templates/react-app/src/uikit/components/BaseHeader.tsx +9 -2
- package/dist/templates/react-app/src/uikit/components/LocaleLink.tsx +5 -3
- package/dist/templates/react-app/src/uikit/hooks/useI18nGuard.ts +4 -6
- package/dist/templates/react-app/tsconfig.json +2 -1
- package/dist/templates/react-app/tsconfig.test.json +13 -0
- package/dist/templates/react-app/vite.config.ts +3 -2
- package/package.json +1 -1
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
# Router System
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The router system uses configuration-based routing, implementing a flexible and extensible routing management solution through `RouteService` and `RouterLoader`. Main features:
|
|
6
|
+
|
|
7
|
+
- **Configuration Driven**: Define routes through configuration files, no need to manually write router components
|
|
8
|
+
- **Code Splitting**: Automatically handle component lazy loading
|
|
9
|
+
- **Type Safety**: Complete TypeScript type support
|
|
10
|
+
- **State Management**: Seamless integration with Store system
|
|
11
|
+
- **Internationalization Support**: Built-in multi-language route support
|
|
12
|
+
- **Metadata Extension**: Support route-level metadata configuration
|
|
13
|
+
|
|
14
|
+
## Route Configuration
|
|
15
|
+
|
|
16
|
+
### 1. Path Configuration (path)
|
|
17
|
+
|
|
18
|
+
`path` defines the URL path pattern for the route:
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
{
|
|
22
|
+
// Basic path
|
|
23
|
+
path: '/about', // Matches /about
|
|
24
|
+
|
|
25
|
+
// Dynamic parameters
|
|
26
|
+
path: '/user/:id', // Matches /user/123
|
|
27
|
+
|
|
28
|
+
// Optional parameters
|
|
29
|
+
path: '/posts/:id?', // Matches /posts and /posts/1
|
|
30
|
+
|
|
31
|
+
// Multi-level paths
|
|
32
|
+
path: '/blog/:category/:id', // Matches /blog/tech/123
|
|
33
|
+
|
|
34
|
+
// Wildcards
|
|
35
|
+
path: '*', // Matches any undefined path
|
|
36
|
+
|
|
37
|
+
// Internationalized paths
|
|
38
|
+
path: '/:lng/dashboard', // Matches /en/dashboard, /zh/dashboard
|
|
39
|
+
|
|
40
|
+
// Index routes (default child route)
|
|
41
|
+
index: true // Default rendering content for parent route
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### 2. Component Configuration (element)
|
|
46
|
+
|
|
47
|
+
`element` uses string identifiers to reference actual components, this design has the following advantages:
|
|
48
|
+
|
|
49
|
+
1. **Automatic Code Splitting**:
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
// App.tsx
|
|
53
|
+
function getAllPages() {
|
|
54
|
+
// Automatically scan all components under pages directory
|
|
55
|
+
const modules = import.meta.glob('./pages/**/*.tsx');
|
|
56
|
+
return Object.keys(modules).reduce((acc, path) => {
|
|
57
|
+
// Convert file path to component identifier
|
|
58
|
+
const componentName = path.replace(/^\.\/pages\/(.*)\.tsx$/, '$1');
|
|
59
|
+
// Create lazy-loaded component
|
|
60
|
+
acc[componentName] = () =>
|
|
61
|
+
lazy(
|
|
62
|
+
modules[path] as () => Promise<{
|
|
63
|
+
default: React.ComponentType<unknown>;
|
|
64
|
+
}>
|
|
65
|
+
);
|
|
66
|
+
return acc;
|
|
67
|
+
}, {} as ComponentValue);
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
2. **Component Mapping Rules**:
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
{
|
|
75
|
+
// Basic component mapping
|
|
76
|
+
element: 'HomePage', // Maps to pages/HomePage.tsx
|
|
77
|
+
|
|
78
|
+
// Subdirectory components
|
|
79
|
+
element: 'user/Profile', // Maps to pages/user/Profile.tsx
|
|
80
|
+
|
|
81
|
+
// Layout components
|
|
82
|
+
element: 'layouts/Default', // Maps to pages/layouts/Default.tsx
|
|
83
|
+
|
|
84
|
+
// Special pages
|
|
85
|
+
element: '404', // Maps to pages/404.tsx
|
|
86
|
+
|
|
87
|
+
// Authentication related pages
|
|
88
|
+
element: 'auth/LoginPage' // Maps to pages/auth/LoginPage.tsx
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
3. **Component Loading Process**:
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
class RouterLoader {
|
|
96
|
+
// Get component implementation
|
|
97
|
+
getComponent(element: string): () => RouteComponentType {
|
|
98
|
+
const maps = this.getComponentMaps();
|
|
99
|
+
const component = maps[element];
|
|
100
|
+
|
|
101
|
+
if (!component) {
|
|
102
|
+
throw new Error(`Component not found: ${element}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return component;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Convert to route object
|
|
109
|
+
toRoute(route: RouteConfigValue): RouteObject {
|
|
110
|
+
const { element, children, ...rest } = route;
|
|
111
|
+
const component = this.getComponent(element || '404');
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
...rest,
|
|
115
|
+
element: this.render({ ...rest, element: component }),
|
|
116
|
+
children: children?.map((child) => this.toRoute(child))
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
4. **Component Rendering Process**:
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
// RouterRenderComponent.tsx
|
|
126
|
+
export const RouterRenderComponent: RouterLoaderRender = (route) => {
|
|
127
|
+
const Component = route.element(); // Lazy-loaded component
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<Suspense fallback={<Loading fullscreen />}>
|
|
131
|
+
<BaseRouteProvider {...route.meta}>
|
|
132
|
+
<Component />
|
|
133
|
+
</BaseRouteProvider>
|
|
134
|
+
</Suspense>
|
|
135
|
+
);
|
|
136
|
+
};
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### 3. Complete Route Example
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
export const baseRoutes: RouteConfigValue[] = [
|
|
143
|
+
// Redirect route
|
|
144
|
+
{
|
|
145
|
+
path: '/',
|
|
146
|
+
element: 'base/RedirectPathname' // Handle default language redirect
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
// Main layout route
|
|
150
|
+
{
|
|
151
|
+
path: '/:lng', // Language parameter
|
|
152
|
+
element: 'base/Layout', // Main layout component
|
|
153
|
+
meta: {
|
|
154
|
+
category: 'main'
|
|
155
|
+
},
|
|
156
|
+
children: [
|
|
157
|
+
// Home route
|
|
158
|
+
{
|
|
159
|
+
index: true, // Default child route
|
|
160
|
+
element: 'base/HomePage', // Home page component
|
|
161
|
+
meta: {
|
|
162
|
+
title: 'PAGE_HOME_TITLE',
|
|
163
|
+
icon: 'home',
|
|
164
|
+
localNamespace: 'common'
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
// Feature page route
|
|
169
|
+
{
|
|
170
|
+
path: 'dashboard', // Dashboard page
|
|
171
|
+
element: 'base/DashboardPage',
|
|
172
|
+
meta: {
|
|
173
|
+
title: 'PAGE_DASHBOARD_TITLE',
|
|
174
|
+
icon: 'dashboard'
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
]
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
// Auth layout route
|
|
181
|
+
{
|
|
182
|
+
path: '/:lng',
|
|
183
|
+
element: 'auth/Layout',
|
|
184
|
+
meta: {
|
|
185
|
+
category: 'auth'
|
|
186
|
+
},
|
|
187
|
+
children: [
|
|
188
|
+
{
|
|
189
|
+
path: 'login',
|
|
190
|
+
element: 'auth/LoginPage',
|
|
191
|
+
meta: {
|
|
192
|
+
title: 'PAGE_LOGIN_TITLE'
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
]
|
|
196
|
+
}
|
|
197
|
+
];
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### 2. Route Type Definitions
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
interface RouteConfigValue {
|
|
204
|
+
path?: string; // Route path
|
|
205
|
+
element: string; // Component identifier
|
|
206
|
+
children?: RouteConfigValue[]; // Child routes
|
|
207
|
+
meta?: RouteMeta; // Route metadata
|
|
208
|
+
index?: boolean; // Whether it's an index route
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
interface RouteMeta {
|
|
212
|
+
title?: string; // Page title
|
|
213
|
+
description?: string; // Page description
|
|
214
|
+
icon?: string; // Page icon
|
|
215
|
+
category?: string; // Route category
|
|
216
|
+
localNamespace?: string; // Internationalization namespace
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Core Components
|
|
221
|
+
|
|
222
|
+
### 1. RouterLoader
|
|
223
|
+
|
|
224
|
+
`RouterLoader` is responsible for converting route configurations into actual route components:
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
class RouterLoader {
|
|
228
|
+
constructor(private readonly options: RouterLoaderOptions) {
|
|
229
|
+
if (!options.render) {
|
|
230
|
+
throw new Error('RouterLoader render is required');
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Convert route configuration to React Router route object
|
|
235
|
+
toRoute(route: RouteConfigValue): RouteObject {
|
|
236
|
+
const { render } = this.options;
|
|
237
|
+
const { element, children, ...rest } = route;
|
|
238
|
+
|
|
239
|
+
const component = this.getComponent(element || '404');
|
|
240
|
+
const Element = render({ ...rest, element: component });
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
...rest,
|
|
244
|
+
element: Element,
|
|
245
|
+
children: children?.map((child) => this.toRoute(child))
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### 2. RouteService
|
|
252
|
+
|
|
253
|
+
`RouteService` manages route state and navigation:
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
class RouteService extends StoreInterface<RouterServiceState> {
|
|
257
|
+
// Compose path (add language prefix)
|
|
258
|
+
composePath(path: string): string {
|
|
259
|
+
const targetLang = I18nService.getCurrentLanguage();
|
|
260
|
+
return `/${targetLang}${path}`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Route navigation
|
|
264
|
+
goto(path: string, options?: NavigateOptions): void {
|
|
265
|
+
path = this.composePath(path);
|
|
266
|
+
this.navigate?.(path, options);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Update route configuration
|
|
270
|
+
changeRoutes(routes: RouteConfigValue[]): void {
|
|
271
|
+
this.emit({ routes });
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### 3. RouterRenderComponent
|
|
277
|
+
|
|
278
|
+
Route rendering component, handles lazy loading and page context:
|
|
279
|
+
|
|
280
|
+
```tsx
|
|
281
|
+
export const RouterRenderComponent: RouterLoaderRender = (route) => {
|
|
282
|
+
const Component = route.element();
|
|
283
|
+
|
|
284
|
+
return (
|
|
285
|
+
<Suspense fallback={<Loading fullscreen />}>
|
|
286
|
+
<BaseRouteProvider {...route.meta}>
|
|
287
|
+
<Component />
|
|
288
|
+
</BaseRouteProvider>
|
|
289
|
+
</Suspense>
|
|
290
|
+
);
|
|
291
|
+
};
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
## Usage
|
|
295
|
+
|
|
296
|
+
### 1. Application Configuration
|
|
297
|
+
|
|
298
|
+
Configure the router system at the application entry point:
|
|
299
|
+
|
|
300
|
+
```tsx
|
|
301
|
+
function App() {
|
|
302
|
+
// Initialize router loader
|
|
303
|
+
const routerLoader = new RouterLoader({
|
|
304
|
+
componentMaps: getAllPages(),
|
|
305
|
+
render: RouterRenderComponent
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// Get route configuration
|
|
309
|
+
const routes = useStore(IOC(RouteService), (state) => state.routes);
|
|
310
|
+
|
|
311
|
+
// Create router
|
|
312
|
+
const routerBase = useMemo(() => {
|
|
313
|
+
const routeList = routes.map((route) => routerLoader.toRoute(route));
|
|
314
|
+
return createBrowserRouter(routeList, {
|
|
315
|
+
basename: routerPrefix
|
|
316
|
+
});
|
|
317
|
+
}, [routes]);
|
|
318
|
+
|
|
319
|
+
return <RouterProvider router={routerBase} />;
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### 2. Page Navigation
|
|
324
|
+
|
|
325
|
+
Use route service for navigation in components:
|
|
326
|
+
|
|
327
|
+
```tsx
|
|
328
|
+
function LoginButton() {
|
|
329
|
+
const routeService = IOC(RouteService);
|
|
330
|
+
|
|
331
|
+
const handleLogin = () => {
|
|
332
|
+
routeService.goto('/login');
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
return <button onClick={handleLogin}>Login</button>;
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### 3. Route Guards
|
|
340
|
+
|
|
341
|
+
Implement route-level functionality through `BaseRouteProvider`:
|
|
342
|
+
|
|
343
|
+
```tsx
|
|
344
|
+
function BaseRouteProvider(props: PropsWithChildren<RouteMeta>) {
|
|
345
|
+
const { t } = useTranslation();
|
|
346
|
+
|
|
347
|
+
// Automatically set page title
|
|
348
|
+
useDocumentTitle(props.title ? t(props.title) : IOC('AppConfig').appName);
|
|
349
|
+
|
|
350
|
+
return (
|
|
351
|
+
<BaseRoutePageContext.Provider value={props}>
|
|
352
|
+
{props.children}
|
|
353
|
+
</BaseRoutePageContext.Provider>
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
## Best Practices
|
|
359
|
+
|
|
360
|
+
1. **Route Organization**
|
|
361
|
+
- Organize route configurations by feature modules
|
|
362
|
+
- Use nested routes for complex page structures
|
|
363
|
+
- Use route metadata appropriately
|
|
364
|
+
|
|
365
|
+
2. **Code Splitting**
|
|
366
|
+
- Apply code splitting to large page components
|
|
367
|
+
- Use Suspense to handle loading states
|
|
368
|
+
- Preload critical route components
|
|
369
|
+
|
|
370
|
+
3. **Type Safety**
|
|
371
|
+
- Define types for all route configurations
|
|
372
|
+
- Use constants to manage route paths
|
|
373
|
+
- Avoid hardcoding route strings
|
|
374
|
+
|
|
375
|
+
4. **Internationalization**
|
|
376
|
+
- Use route parameters for multi-language support
|
|
377
|
+
- Configure page-level translation namespaces
|
|
378
|
+
- Automatically handle title and description translations
|
|
379
|
+
|
|
380
|
+
## Common Issues
|
|
381
|
+
|
|
382
|
+
### 1. Routes Not Working
|
|
383
|
+
|
|
384
|
+
Check the following:
|
|
385
|
+
|
|
386
|
+
- Ensure route configuration format is correct
|
|
387
|
+
- Check if component mapping is properly configured
|
|
388
|
+
- Verify paths include language prefix
|
|
389
|
+
|
|
390
|
+
### 2. Page Loading Failures
|
|
391
|
+
|
|
392
|
+
Possible solutions:
|
|
393
|
+
|
|
394
|
+
- Check if components are correctly exported
|
|
395
|
+
- Ensure lazy loading is properly configured
|
|
396
|
+
- Check if network requests are working correctly
|
|
397
|
+
|
|
398
|
+
### 3. Type Errors
|
|
399
|
+
|
|
400
|
+
Common solutions:
|
|
401
|
+
|
|
402
|
+
- Ensure route configurations match type definitions
|
|
403
|
+
- Check if component properties are complete
|
|
404
|
+
- Use correct route metadata types
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
# Store State Management
|
|
2
|
+
|
|
3
|
+
## Core Concepts
|
|
4
|
+
|
|
5
|
+
The Store design philosophy is based on the following core concepts:
|
|
6
|
+
|
|
7
|
+
1. **Logic and UI Separation**
|
|
8
|
+
- Business logic centralized in Store management
|
|
9
|
+
- UI components only handle rendering and user interaction
|
|
10
|
+
- Logic dependency injection through IOC container
|
|
11
|
+
|
|
12
|
+
2. **Reactive Data Flow**
|
|
13
|
+
- Based on publish-subscribe pattern
|
|
14
|
+
- State changes automatically trigger UI updates
|
|
15
|
+
- Precise component re-rendering control
|
|
16
|
+
|
|
17
|
+
3. **State Slice Management**
|
|
18
|
+
- Break down complex states into independent slices
|
|
19
|
+
- Each slice handles specific business domain
|
|
20
|
+
- Slices can be combined and communicate
|
|
21
|
+
|
|
22
|
+
## How It Works
|
|
23
|
+
|
|
24
|
+
### 1. State Subscription Mechanism
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
// Store implements publish-subscribe mechanism internally
|
|
28
|
+
class SliceStore<T> {
|
|
29
|
+
private listeners = new Set<(state: T) => void>();
|
|
30
|
+
|
|
31
|
+
// Publish state updates
|
|
32
|
+
protected emit(newState: T) {
|
|
33
|
+
this.state = newState;
|
|
34
|
+
this.listeners.forEach((listener) => listener(this.state));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Subscribe to state changes
|
|
38
|
+
subscribe(listener: (state: T) => void) {
|
|
39
|
+
this.listeners.add(listener);
|
|
40
|
+
return () => this.listeners.delete(listener);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### 2. State Update Flow
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
User Action → Call Store Method → Update State → Notify Subscribers → UI Update
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
1. User triggers action (e.g., button click)
|
|
52
|
+
2. Calls method in Store
|
|
53
|
+
3. Store uses emit to publish new state
|
|
54
|
+
4. Components subscribed to that state receive notification
|
|
55
|
+
5. Components re-render, displaying latest state
|
|
56
|
+
|
|
57
|
+
### 3. Component Integration
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
// Using Store in components
|
|
61
|
+
function UserProfile() {
|
|
62
|
+
// useStore hook automatically handles subscription and unsubscription
|
|
63
|
+
const user = useStore(IOC(UserService), (state) => state.userInfo);
|
|
64
|
+
|
|
65
|
+
return <div>{user.name}</div>;
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 4. State Slice Example
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
// Authentication slice
|
|
73
|
+
class AuthStore extends StoreInterface<AuthState> {
|
|
74
|
+
constructor() {
|
|
75
|
+
super(() => ({
|
|
76
|
+
isLoggedIn: false,
|
|
77
|
+
user: null
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
login(credentials: Credentials) {
|
|
82
|
+
// Handle login logic
|
|
83
|
+
this.emit({
|
|
84
|
+
isLoggedIn: true,
|
|
85
|
+
user: userData
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Theme settings slice
|
|
91
|
+
class ThemeStore extends StoreInterface<ThemeState> {
|
|
92
|
+
constructor() {
|
|
93
|
+
super(() => ({
|
|
94
|
+
mode: 'light',
|
|
95
|
+
colors: defaultColors
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
toggleTheme() {
|
|
100
|
+
const mode = this.state.mode === 'light' ? 'dark' : 'light';
|
|
101
|
+
this.emit({
|
|
102
|
+
...this.state,
|
|
103
|
+
mode
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Overview
|
|
110
|
+
|
|
111
|
+
Store is the application's state management solution, implemented based on `@qlover/slice-store-react`. It manages state using slices and has the following features:
|
|
112
|
+
|
|
113
|
+
- **Type Safety**: Based on TypeScript, providing complete type inference
|
|
114
|
+
- **Lightweight**: No complex configuration, easy to use
|
|
115
|
+
- **High Performance**: Precise component updates, avoiding unnecessary renders
|
|
116
|
+
- **Modular**: Supports state slicing, convenient for managing large applications
|
|
117
|
+
- **IOC Integration**: Perfect integration with dependency injection system
|
|
118
|
+
|
|
119
|
+
## Core Concepts
|
|
120
|
+
|
|
121
|
+
### 1. Store Interface
|
|
122
|
+
|
|
123
|
+
The Store system is based on two core interfaces:
|
|
124
|
+
|
|
125
|
+
#### StoreStateInterface
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
/**
|
|
129
|
+
* Store state interface
|
|
130
|
+
*
|
|
131
|
+
* Purpose: Define contract for store state objects
|
|
132
|
+
* Core Idea: Enforce consistent structure for store states
|
|
133
|
+
* Main Function: Serve as base for all store state types
|
|
134
|
+
* Main Goal: Ensure state type safety and extensibility
|
|
135
|
+
*/
|
|
136
|
+
interface StoreStateInterface {
|
|
137
|
+
// Define your own properties here
|
|
138
|
+
// ...
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
#### StoreInterface
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
/**
|
|
146
|
+
* Store interface
|
|
147
|
+
*
|
|
148
|
+
* Purpose: Abstract base class for all state stores
|
|
149
|
+
* Core Idea: Provide unified state management API, including reset and clone helper methods
|
|
150
|
+
* Main Function: Extend SliceStore, add resetState and cloneState utility methods
|
|
151
|
+
* Main Goal: Simplify store implementation and ensure consistency
|
|
152
|
+
*/
|
|
153
|
+
abstract class StoreInterface<
|
|
154
|
+
T extends StoreStateInterface
|
|
155
|
+
> extends SliceStore<T> {
|
|
156
|
+
constructor(protected stateFactory: () => T) {
|
|
157
|
+
super(stateFactory);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Reset store state
|
|
161
|
+
resetState(): void {
|
|
162
|
+
this.emit(this.stateFactory());
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Clone store state
|
|
166
|
+
cloneState(source?: Partial<T>): T {
|
|
167
|
+
const cloned = clone(this.state);
|
|
168
|
+
if (typeof cloned === 'object' && cloned !== null) {
|
|
169
|
+
Object.assign(cloned, source);
|
|
170
|
+
}
|
|
171
|
+
return cloned;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### 2. State Slices
|
|
177
|
+
|
|
178
|
+
State slices divide application state into independent parts:
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
// User state slice example
|
|
182
|
+
class UserState implements StoreStateInterface {
|
|
183
|
+
isLoggedIn: boolean = false;
|
|
184
|
+
userInfo: {
|
|
185
|
+
name: string;
|
|
186
|
+
role: string;
|
|
187
|
+
} | null = null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Store implementation example
|
|
191
|
+
export class UserStore extends StoreInterface<UserState> {
|
|
192
|
+
constructor() {
|
|
193
|
+
super(() => new UserState());
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Usage in Project
|
|
199
|
+
|
|
200
|
+
### 1. Creating Store Controller
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
import { StoreInterface, StoreStateInterface } from '@qlover/corekit-bridge';
|
|
204
|
+
|
|
205
|
+
interface ExecutorState extends StoreStateInterface {
|
|
206
|
+
helloState: string;
|
|
207
|
+
tasks: Task[];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
@injectable()
|
|
211
|
+
export class ExecutorController extends StoreInterface<ExecutorState> {
|
|
212
|
+
constructor() {
|
|
213
|
+
super(() => ({
|
|
214
|
+
helloState: '',
|
|
215
|
+
tasks: []
|
|
216
|
+
}));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Selectors
|
|
220
|
+
selector = {
|
|
221
|
+
helloState: (state: ExecutorState) => state.helloState,
|
|
222
|
+
tasks: (state: ExecutorState) => state.tasks
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### 2. Using in Components
|
|
228
|
+
|
|
229
|
+
Use the `useStore` Hook to access state:
|
|
230
|
+
|
|
231
|
+
```tsx
|
|
232
|
+
function MyComponent() {
|
|
233
|
+
// Get complete state
|
|
234
|
+
const state = useStore(IOC(ExecutorController));
|
|
235
|
+
|
|
236
|
+
// Use selector to get specific state
|
|
237
|
+
const helloState = useStore(
|
|
238
|
+
IOC(ExecutorController),
|
|
239
|
+
(controller) => controller.selector.helloState
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
return (
|
|
243
|
+
<div>
|
|
244
|
+
<h1>{helloState}</h1>
|
|
245
|
+
</div>
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### 3. Updating State
|
|
251
|
+
|
|
252
|
+
Update state through controller methods:
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
@injectable()
|
|
256
|
+
class ExecutorController extends StoreInterface<ExecutorState> {
|
|
257
|
+
// ... constructor and other code
|
|
258
|
+
|
|
259
|
+
updateHelloState(newState: string) {
|
|
260
|
+
this.emit({ ...this.state, helloState: newState });
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async fetchTasks() {
|
|
264
|
+
const tasks = await api.getTasks();
|
|
265
|
+
this.emit({ ...this.state, tasks });
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Use cloneState for state updates
|
|
269
|
+
updateWithClone(newState: Partial<ExecutorState>) {
|
|
270
|
+
this.emit(this.cloneState(newState));
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
## Best Practices
|
|
276
|
+
|
|
277
|
+
1. **State Organization**
|
|
278
|
+
- Divide state by functional modules
|
|
279
|
+
- Avoid state redundancy
|
|
280
|
+
- Keep state flat
|
|
281
|
+
|
|
282
|
+
2. **Performance Optimization**
|
|
283
|
+
- Use selectors to get state, avoid unnecessary re-renders
|
|
284
|
+
- Reasonably split components, avoid large components subscribing to too many states
|
|
285
|
+
- Use `cloneState` method to ensure state update immutability
|
|
286
|
+
|
|
287
|
+
3. **Type Safety**
|
|
288
|
+
- Define interfaces for all states
|
|
289
|
+
- Use TypeScript's type inference
|
|
290
|
+
- Avoid using any type
|
|
291
|
+
|
|
292
|
+
4. **Bootstrap Integration**
|
|
293
|
+
- Initialize store during Bootstrap phase
|
|
294
|
+
- Manage store instances through IOC container
|
|
295
|
+
- Use plugin system to extend functionality
|
|
296
|
+
|
|
297
|
+
## Common Issues
|
|
298
|
+
|
|
299
|
+
### 1. State Updates Not Working
|
|
300
|
+
|
|
301
|
+
Check the following:
|
|
302
|
+
|
|
303
|
+
- Ensure correct use of `emit` method to update state
|
|
304
|
+
- Use `cloneState` method to ensure state immutability
|
|
305
|
+
- Check if component correctly subscribes to state
|
|
306
|
+
|
|
307
|
+
### 2. Component Re-rendering Issues
|
|
308
|
+
|
|
309
|
+
Possible solutions:
|
|
310
|
+
|
|
311
|
+
- Use selectors to subscribe only to needed state
|
|
312
|
+
- Check if dependencies are correctly set
|
|
313
|
+
- Consider using React.memo for component optimization
|
|
314
|
+
|
|
315
|
+
### 3. TypeScript Type Errors
|
|
316
|
+
|
|
317
|
+
Common solutions:
|
|
318
|
+
|
|
319
|
+
- Ensure correct inheritance from StoreInterface
|
|
320
|
+
- Check if generic parameters are correct
|
|
321
|
+
- Ensure state types implement StoreStateInterface
|