Giao diện
Client - Application
1) Application - class trung tâm client
Class structure
So sánh với Server Application
| Server Application | Client Application | |
|---|---|---|
| Kế thừa | Koa + AsyncEmitter | Standalone class |
| Runtime | Node.js | Browser |
| Plugin loader | Từ filesystem/DB | Static bundle + remote (requirejs) |
| Data access | Database, Repository | HTTP API (APIClient) |
| Middleware | Toposort (Koa + data source) | Provider chain (React context) |
| Event system | AsyncEmitter (async) | EventTarget (DOM-standard) |
| State management | Trong memory | observable (MobX-like reactive) |
2) Constructor flow - chi tiết từng bước
Phase 1: Module loader (bước 1)
initRequireJs() - Setup AMD module loader cho remote plugin.
Tại sao cần RequireJS?
Remote plugin được build thành UMD bundle. Browser cần AMD loader (requirejs) để tải và resolve dependency. defineGlobalDeps() expose các library dùng chung (React, antd, @digiforce-nc/client...) để plugin không bundle lại.
Phase 2: Reactive state (bước 2)
Application có 4 state quan trọng, đều reactive (thay đổi → UI tự re-render):
| State | Type | Ý nghĩa |
|---|---|---|
loading | boolean | true khi đang load (plugin, config) - UI hiển thị spinner |
maintaining | boolean | true khi server đang maintaining - UI chặn thao tác |
maintained | boolean | true sau khi maintaining kết thúc lần đầu - trigger reload |
error | Error | null | Lỗi load - UI hiển thị error page |
Phase 3: APIClient (bước 3)
APIClient wrap axios với:
- Base URL từ options.
- Auto-attach bearer token (từ
auth:getToken). - Error interceptor: map error →
AuthError,NetworkError, etc. apiClient.resource(name)→ shortcut gọi REST action.
Phase 4-8: Managers (bước 4-8)
Thứ tự tạo manager quan trọng vì có dependency:
| Manager | Dependency | Tạo tại bước |
|---|---|---|
RouterManager | - | 4 |
SchemaSettingsManager | - | 5 |
PluginManager | Cần RouterManager, SchemaSettingsManager | 6 |
SchemaInitializerManager | - | 7 |
DataSourceManager | - | 8 |
Phase 5: UiCore + Providers (bước 9-12)
Phase 6: Transport + listeners (bước 13-16)
3) Provider chain - kiến trúc context
Default providers - thứ tự và vai trò
| Provider | Context cung cấp | Consumer chính |
|---|---|---|
APIClientProvider | apiClient instance | Data hooks, action buttons |
I18nextProvider | i18n, t() function | Mọi component cần dịch |
GlobalThemeProvider | Theme tokens (colors, spacing) | Ant Design components |
CSSVariableProvider | CSS variables | Custom styling |
AppSchemaComponentProvider | Component registry | Schema Renderer |
AntdAppProvider | message, modal, notification API | Action feedback |
DataSourceApplicationProvider | Collection metadata, field types | Form builder, filter |
OpenModeProvider | Dialog/drawer/page open mode | Block containers |
UiCoreProvider | app, router, api, i18n, ... | Mọi component |
UiCoreGlobalsContextProvider | Global variables | Variable expressions |
UiCore context keys
uiCore.context cung cấp qua UiCoreProvider:
| Key | Type | Source |
|---|---|---|
app | Application | Application instance |
routeRepository | RouteRepository | Route metadata từ server |
appInfo | AppInfo | Thông tin app (name, version, logo) |
api | APIClient | HTTP client |
i18n | i18next | i18n instance |
router | RouterManager | Client router |
documentTitle | string | Tiêu đề trang hiện tại |
route | RouteObject | Current route |
location | Location | React Router location |
pageInfo | PageInfo | Page-level metadata |
Plugin thêm provider
typescript
class MyPlugin extends Plugin {
async load() {
this.app.addProviders([
[MyThemeProvider, { theme: 'custom' }], // [Component, props]
MyDataProvider, // Component only
]);
}
}Provider mới được nối vào cuối chuỗi - bọc ngoài App Content, bên trong UiCoreGlobalsContextProvider.
4) RouterManager - route tree động
Kiến trúc
Dot-path → nested route tree
Route key dạng dot-path (a.b.c) tự động build thành tree:
add(name, route) - đăng ký route
Quy tắc:
namedùng dot notation → xác định vị trí trong tree.route.pathlà URL path thực tế.route.Componenthoặcroute.elementlà React component render.route.handlechứa metadata (auth check, breadcrumb, etc.).
getRoutesTree() - build tree
Method này chuyển flat Map thành nested RouteObject[] cho React Router:
- Duyệt tất cả route trong Map.
- Với mỗi route
a.b.c, tìm parenta.btrong Map. - Nếu parent tồn tại → gắn làm child.
- Nếu parent không tồn tại → tạo parent placeholder.
- Return tree từ root routes.
getRouterComponent() - tạo router React
Providers bọc router:
RouterBridge- bridge giữa legacy router API và React Router v6.CustomRouterContextProvider- custom context cho Digiforce routing.VariablesProvider- biến context (current record, user, etc.).RouterProvider- React Router v6 provider.
isSkippedAuthCheckRoute(pathname) - skip auth
Route có thể khai báo skipAuthCheck: true trong handle:
typescript
router.add('auth.login', {
path: '/auth/login',
Component: LoginPage,
handle: { skipAuthCheck: true },
});Application dùng method này trong load() để quyết định có cần gọi auth API trước khi render hay không.
5) PluginManager client - lifecycle chi tiết
Hai nguồn plugin
initPlugins - quá trình chi tiết
add(pluginClass, options) - đăng ký plugin
load() - hai vòng giống server
Tại sao tách hai vòng?
Lý do giống server: plugin B có thể cần biết plugin A đã đăng ký gì (component, route) trong beforeLoad(). Tách đảm bảo tất cả beforeLoad() hoàn thành trước khi bất kỳ load() nào chạy.
Plugin API trong load() - gì có thể đăng ký
typescript
class MyPlugin extends Plugin {
async load() {
// Đăng ký component cho Schema Renderer
this.app.addComponents({
MyBlock: MyBlockComponent,
MyField: MyFieldComponent,
});
// Đăng ký route
this.app.router.add('admin.myPlugin', {
path: '/admin/my-plugin',
Component: MyPluginPage,
});
// Đăng ký schema settings
this.app.schemaSettingsManager.add('myBlockSettings', {
items: [/* ... */],
});
// Đăng ký schema initializer
this.app.schemaInitializerManager.add('myBlockInitializer', {
items: [/* ... */],
});
// Đăng ký plugin settings page
this.app.pluginSettingsManager.add('my-plugin', {
title: 'My Plugin Settings',
icon: 'SettingOutlined',
Component: MyPluginSettingsPage,
});
// Đăng ký provider
this.app.addProviders([MyPluginProvider]);
}
}6) WebSocketClient - realtime chi tiết
Connection lifecycle
Message handling chi tiết
initListeners() - token và maintaining sync
Reconnection strategy
Khi connection mất:
serverDownevent fire →maintaining = true.- WebSocketClient auto reconnect với exponential backoff.
- Khi reconnect thành công →
openevent fire. - Nếu có token → gửi
auth:tokenđể re-authenticate. - Server trả trạng thái hiện tại (ready/maintaining).
7) load() và mount() - startup sequence
load() - nạp plugin và config
mount(containerOrSelector) - render React app
getRootComponent() - build component tree
Thứ tự kiểm tra: loading → error → maintaining → render content.
8) Schema system integration
SchemaSettingsManager
Plugin đăng ký settings cho block:
typescript
schemaSettingsManager.add('tableBlockSettings', {
items: [
{ name: 'setPageSize', Component: SetPageSizeItem },
{ name: 'setColumns', Component: SetColumnsItem },
{ name: 'divider1', type: 'divider' },
{ name: 'delete', Component: DeleteBlockItem },
],
});SchemaInitializerManager
Plugin đăng ký initializer:
typescript
schemaInitializerManager.add('tableBlockInitializer', {
items: [
{ name: 'addTextField', Component: AddTextFieldItem },
{ name: 'addNumberField', Component: AddNumberFieldItem },
{ name: 'addRelationField', Component: AddRelationFieldItem },
],
});Component map
Schema Renderer dùng component map để resolve x-component name → React component:
Plugin đăng ký component:
typescript
this.app.addComponents({
'FormV2': FormV2Component,
'Input': InputComponent,
'Select': SelectComponent,
'MyCustomBlock': MyCustomBlockComponent,
});9) DataSourceManager (client) - metadata layer
Khác biệt với server DataSourceManager
| Server | Client | |
|---|---|---|
| Truy cập DB | Trực tiếp (Sequelize) | Qua HTTP API |
| Pipeline | Koa middleware → Repository | Không có |
| ACL | Engine runtime | Đọc permission từ API response |
| Mục đích | Xử lý request, query data | Cung cấp metadata cho UI |
Client DataSourceManager cung cấp gì
| Feature | Mô tả | Consumer |
|---|---|---|
| Collection metadata | Tên, title, fields, relations | Form builder |
| Field type info | Input type, validation rules | Field component |
| Relation info | Foreign key, target collection | Relation picker |
| Data source selection | Header x-data-source cho API calls | APIClient |
10) PluginSettingsManager - settings page
Cơ chế hoạt động
ACL cho settings page
| Method | Mô tả |
|---|---|
add(name, options) | Đăng ký settings page. Options: title, icon, Component, aclSnippet |
getAclSnippet(name) | Trả snippet ACL. Mặc định: pm.<name> |
hasAuth(name) | Kiểm tra user hiện tại có quyền truy cập. Dựa vào snippet deny list từ backend. |
getList(filterAuth) | Trả danh sách settings page user được phép truy cập |
11) Error handling và recovery
Load error flow
WebSocket error recovery
| Tình huống | Hành vi |
|---|---|
| Connection lost | Auto reconnect (exponential backoff) |
| Server maintaining | UI hiển thị maintaining state |
| Token expired | Auth interceptor refresh token → re-send |
| Server restart | WS reconnect → re-auth → sync state |
Plugin load error
| Tình huống | Hành vi |
|---|---|
| Static plugin constructor throw | load() catch → error state |
| Remote plugin fetch fail | Network error → error state |
Plugin beforeLoad() throw | load() catch → error state |
Plugin load() throw | load() catch → error state |
Plugin error = app error
Khác server (plugin error có thể isolate), client plugin error trong load() sẽ block toàn bộ app. Plugin client cần handle error cẩn thận.
12) Performance considerations
Bundle size
| Factor | Ảnh hưởng | Mitigation |
|---|---|---|
| Static plugins | Tăng initial bundle | Code split, lazy route |
| Remote plugins | Tải sau initial load | RequireJS async |
| Provider chain | Negligible (context only) | - |
| Schema rendering | Có thể chậm với tree lớn | Virtualization, lazy render |
| Component map | Tăng bundle nếu register nhiều | Tree shaking, dynamic import |
Render optimization
- Schema Renderer chỉ re-render node thay đổi (key-based reconciliation).
- Data hooks (
useRequest,useCollection) tự quản lý cache. - Provider chain không gây re-render cascade (context value stability).
Đọc tiếp
- Server - Application - server-side counterpart
- Resourcer & Actions - API endpoints mà client gọi
- DataSourceManager - server data source (so sánh)
- Auth & AuthManager - JWT flow, auth middleware
- Architecture: Client - cái nhìn kiến trúc tổng thể
- FAQ - câu hỏi thường gặp