Bỏ qua, đến nội dung

Client - Application


1) Application - class trung tâm client

Class structure

So sánh với Server Application

Server ApplicationClient Application
Kế thừaKoa + AsyncEmitterStandalone class
RuntimeNode.jsBrowser
Plugin loaderTừ filesystem/DBStatic bundle + remote (requirejs)
Data accessDatabase, RepositoryHTTP API (APIClient)
MiddlewareToposort (Koa + data source)Provider chain (React context)
Event systemAsyncEmitter (async)EventTarget (DOM-standard)
State managementTrong memoryobservable (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):

StateTypeÝ nghĩa
loadingbooleantrue khi đang load (plugin, config) - UI hiển thị spinner
maintainingbooleantrue khi server đang maintaining - UI chặn thao tác
maintainedbooleantrue sau khi maintaining kết thúc lần đầu - trigger reload
errorError | nullLỗ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:

ManagerDependencyTạo tại bước
RouterManager-4
SchemaSettingsManager-5
PluginManagerCần RouterManager, SchemaSettingsManager6
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ò

ProviderContext cung cấpConsumer chính
APIClientProviderapiClient instanceData hooks, action buttons
I18nextProvideri18n, t() functionMọi component cần dịch
GlobalThemeProviderTheme tokens (colors, spacing)Ant Design components
CSSVariableProviderCSS variablesCustom styling
AppSchemaComponentProviderComponent registrySchema Renderer
AntdAppProvidermessage, modal, notification APIAction feedback
DataSourceApplicationProviderCollection metadata, field typesForm builder, filter
OpenModeProviderDialog/drawer/page open modeBlock containers
UiCoreProviderapp, router, api, i18n, ...Mọi component
UiCoreGlobalsContextProviderGlobal variablesVariable expressions

UiCore context keys

uiCore.context cung cấp qua UiCoreProvider:

KeyTypeSource
appApplicationApplication instance
routeRepositoryRouteRepositoryRoute metadata từ server
appInfoAppInfoThông tin app (name, version, logo)
apiAPIClientHTTP client
i18ni18nexti18n instance
routerRouterManagerClient router
documentTitlestringTiêu đề trang hiện tại
routeRouteObjectCurrent route
locationLocationReact Router location
pageInfoPageInfoPage-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:

  • name dùng dot notation → xác định vị trí trong tree.
  • route.path là URL path thực tế.
  • route.Component hoặc route.element là React component render.
  • route.handle chứa metadata (auth check, breadcrumb, etc.).

getRoutesTree() - build tree

Method này chuyển flat Map thành nested RouteObject[] cho React Router:

  1. Duyệt tất cả route trong Map.
  2. Với mỗi route a.b.c, tìm parent a.b trong Map.
  3. Nếu parent tồn tại → gắn làm child.
  4. Nếu parent không tồn tại → tạo parent placeholder.
  5. 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:

  1. serverDown event fire → maintaining = true.
  2. WebSocketClient auto reconnect với exponential backoff.
  3. Khi reconnect thành công → open event fire.
  4. Nếu có token → gửi auth:token để re-authenticate.
  5. Server trả trạng thái hiện tại (ready/maintaining).

7) load()mount() - startup sequence

load() - nạp plugin và config

mount(containerOrSelector) - render React app

getRootComponent() - build component tree

Thứ tự kiểm tra: loadingerrormaintaining → 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

ServerClient
Truy cập DBTrực tiếp (Sequelize)Qua HTTP API
PipelineKoa middleware → RepositoryKhông có
ACLEngine runtimeĐọc permission từ API response
Mục đíchXử lý request, query dataCung cấp metadata cho UI

Client DataSourceManager cung cấp gì

FeatureMô tảConsumer
Collection metadataTên, title, fields, relationsForm builder
Field type infoInput type, validation rulesField component
Relation infoForeign key, target collectionRelation picker
Data source selectionHeader x-data-source cho API callsAPIClient

10) PluginSettingsManager - settings page

Cơ chế hoạt động

ACL cho settings page

MethodMô 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ốngHành vi
Connection lostAuto reconnect (exponential backoff)
Server maintainingUI hiển thị maintaining state
Token expiredAuth interceptor refresh token → re-send
Server restartWS reconnect → re-auth → sync state

Plugin load error

Tình huốngHành vi
Static plugin constructor throwload() catch → error state
Remote plugin fetch failNetwork error → error state
Plugin beforeLoad() throwload() catch → error state
Plugin load() throwload() 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ưởngMitigation
Static pluginsTăng initial bundleCode split, lazy route
Remote pluginsTải sau initial loadRequireJS async
Provider chainNegligible (context only)-
Schema renderingCó thể chậm với tree lớnVirtualization, lazy render
Component mapTăng bundle nếu register nhiềuTree 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