Bỏ qua, đến nội dung

@digiforce-nc/client

Package lõi phía client - khởi tạo ứng dụng React, quản lý plugin, schema-driven UI, data source, routing, và API.

typescript
import { Application, Plugin } from '@digiforce-nc/client';

Tổng quan

Hãy hình dung ứng dụng client như một tòa nhà thông minh:

  • Application = tòa nhà - nền tảng chứa mọi thứ, quản lý vòng đời.
  • Provider chain = hệ thống ống dẫn - mỗi provider bọc bên ngoài cung cấp "tiện ích" (API, theme, i18n…) cho toàn bộ component bên trong.
  • PluginManager = ban quản lý tòa nhà - load và khởi tạo plugin (cả static lẫn remote).
  • RouterManager = hệ thống thang máy & hành lang - điều hướng user đến đúng trang.
  • SchemaComponent = bản thiết kế phòng - mô tả UI bằng JSON schema, render thành React component.
  • DataSourceManager = kho dữ liệu - quản lý collection, field, và data source từ server.
  • APIClient = đường dây liên lạc - giao tiếp HTTP/WebSocket với server.
  • Designable = chế độ sửa chữa - cho phép kéo thả, thêm bớt component trực tiếp trên UI.

Dependencies chính

PackageVai trò
react 18+UI framework
antd 5Component library
@formily/core, @formily/reactSchema-driven form & reactive state
react-router-dom 6Client routing
axios (qua @digiforce-nc/sdk)HTTP client
@digiforce-nc/sdkAPIClient base, Auth
@digiforce-nc/ui-coreUiCore widget system, observer
@digiforce-nc/utilsRegistry, tiện ích chung
@emotion/cssCSS-in-JS
@dnd-kitDrag & Drop
i18nextĐa ngôn ngữ

Application - trung tâm ứng dụng

Kiến trúc tổng thể

ApplicationOptions

typescript
interface ApplicationOptions {
  name?: string;
  publicPath?: string;

  apiClient?: APIClientOptions | APIClient;
  ws?: WebSocketClientOptions | boolean;
  i18n?: i18next;
  router?: RouterOptions;

  providers?: (ComponentType | [ComponentType, props])[];
  plugins?: PluginType[];
  components?: Record<string, ComponentType>;
  scopes?: Record<string, any>;

  schemaSettings?: SchemaSettings[];
  schemaInitializers?: SchemaInitializer[];
  pluginSettings?: Record<string, PluginSettingOptions>;
  dataSourceManager?: DataSourceManagerOptions;

  designable?: boolean;
  loadRemotePlugins?: boolean;
  devDynamicImport?: Function;
  disableAcl?: boolean;
}

Constructor - thứ tự khởi tạo

Thứ tự rất quan trọng - mỗi bước phụ thuộc bước trước:

Reactive state với Formily

Application sử dụng @formily/reactive để biến state thành reactive - bất kỳ component nào observe đều tự động re-render khi state thay đổi:

typescript
import { define, observable } from '@formily/reactive';

define(this, {
  loading: observable.ref,
  maintaining: observable.ref,
  error: observable.ref,
});
StateKiểuÝ nghĩa
loadingbooleantrue trong quá trình load()
maintainingbooleantrue khi server đang bảo trì
maintainedbooleantrue khi server đã hoàn tất bảo trì (chờ reload)
errorobject | nullLỗi nghiêm trọng (LOAD_ERROR, BLOCKED_IP…)
hasLoadErrorbooleantrue nếu load() thất bại - trigger auto-reload khi server recovery

So sánh với Server Application

Khía cạnhServerClient
Base classKoa + AsyncEmitterStandalone (không kế thừa)
Event systemAsyncEmitter (async)EventTarget (DOM standard)
State managementIn-memory@formily/reactive
MiddlewareToposort pipelineProvider chain (React context)
Plugin loadingFilesystem + DBStatic + Remote (requirejs)
RenderingHTTP responseReact DOM

Vòng đời - từ mount đến render

Render gates trong AppComponent

AppComponent (wrapped bởi observer) kiểm tra state theo thứ tự:

MainComponent

MainComponent kết hợp tất cả providers thành layout bọc ngoài router:

typescript
function MainComponent() {
  const BaseLayout = app.getComposeProviders();
  const RouterComponent = app.router.getRouterComponent();

  return (
    <BaseLayout>
      <RouterComponent />
    </BaseLayout>
  );
}

Provider chain - hệ thống context lồng nhau

Provider chain là cách React cung cấp dữ liệu/dịch vụ cho toàn bộ component tree mà không cần truyền props.

Thứ tự provider mặc định

Thứ tự quan trọng - provider bên ngoài bọc provider bên trong:

Thêm provider từ plugin

Plugin có thể thêm provider vào chain:

typescript
class MyPlugin extends Plugin {
  async load() {
    this.app.use(MyCustomProvider, { someProp: 'value' });
  }
}

Providers được compose thành chuỗi lồng nhau tự động:

typescript
// app.use(A); app.use(B); app.use(C);
// Kết quả tương đương:
<A>
  <B>
    <C>
      {children}
    </C>
  </B>
</A>

PluginManager - quản lý plugin phía client

Hai nguồn plugin

Plugin lifecycle

Plugin base class

typescript
class Plugin<T = any> {
  constructor(public options: T, protected app: Application) {}

  // Getters tiện ích
  get pm(): PluginManager { ... }
  get router(): RouterManager { ... }
  get schemaInitializerManager(): SchemaInitializerManager { ... }
  get schemaSettingsManager(): SchemaSettingsManager { ... }
  get dataSourceManager(): DataSourceManager { ... }
  get pluginSettingsManager(): PluginSettingsManager { ... }

  // Lifecycle hooks (override trong plugin con)
  async afterAdd(): Promise<void> {}
  async beforeLoad(): Promise<void> {}
  async load(): Promise<void> {}

  // i18n helper
  t(text: string, options?: TOptions): string { ... }
}

Ví dụ plugin client

typescript
import { Plugin } from '@digiforce-nc/client';

class PluginKanban extends Plugin {
  async afterAdd() {
    // Đăng ký route sớm (trước load)
    this.router.add('admin.kanban', {
      path: '/admin/kanban',
      Component: 'KanbanPage',
    });
  }

  async load() {
    // Đăng ký components
    this.app.addComponents({
      KanbanPage: () => import('./pages/KanbanPage'),
      KanbanBlock: () => import('./blocks/KanbanBlock'),
    });

    // Đăng ký plugin settings page
    this.pluginSettingsManager.add('kanban', {
      title: 'Kanban Settings',
      icon: 'AppstoreOutlined',
      Component: KanbanSettings,
    });

    // Đăng ký schema initializer item
    this.schemaInitializerManager.addItem('page:addBlock', 'kanban', {
      title: 'Kanban',
      Component: KanbanInitializerItem,
    });
  }
}

Remote plugins & RequireJS

Khi loadRemotePlugins = true, PluginManager gọi API pm:listEnabled để lấy danh sách plugin đã bật, rồi tải code qua RequireJS:

defineGlobalDeps() khai báo React, ReactDOM, và các shared libraries dưới dạng RequireJS module - đảm bảo remote plugins không bundle riêng bản copy.


RouterManager - điều hướng

Flat → nested route tree

RouterManager lưu routes dưới dạng flat map với dot notation, rồi tự động build thành nested route tree cho React Router:

typescript
// Đăng ký routes (flat)
app.router.add('admin', {
  path: '/admin',
  Component: AdminLayout,
});

app.router.add('admin.dashboard', {
  path: '/admin/dashboard',
  Component: DashboardPage,
});

app.router.add('admin.settings', {
  path: '/admin/settings',
  Component: SettingsLayout,
});

app.router.add('admin.settings.general', {
  path: '/admin/settings/general',
  Component: GeneralSettings,
});

Chuyển đổi tự động:

RouteType

typescript
interface RouteType {
  path?: string;
  Component?: ComponentType | string;  // String → resolve từ app.components
  element?: ReactElement;
  skipAuthCheck?: boolean;  // Bỏ qua kiểm tra auth
  handle?: { path?: string };
}

Router types

TypeTạo bằngDùng cho
hashcreateHashRouterURL dạng /#/admin (mặc định)
browsercreateBrowserRouterURL dạng /admin (cần server config)
memorycreateMemoryRouterTesting, SSR

API

MethodMô tả
add(name, route)Thêm route (dot notation cho nesting)
remove(name)Xóa route
get(name)Lấy route config
getRouterComponent()Tạo RouterProvider component
getRoutesTree()Build nested route tree
getHref(name)Lấy URL path cho route

Schema-driven UI - kiến trúc giao diện

Đây là kiến trúc cốt lõi của Digiforce: UI được mô tả bằng JSON Schema thay vì viết JSX trực tiếp.

Ý tưởng

Thay vì hard-code UI bằng JSX:

jsx
// ❌ Cách truyền thống - hard-code
<Table columns={[...]} dataSource={[...]} />

Digiforce lưu UI dưới dạng schema:

json
{
  "type": "void",
  "x-component": "Table",
  "x-component-props": { "rowKey": "id" },
  "properties": {
    "name": {
      "type": "string",
      "x-component": "Input",
      "title": "Tên"
    }
  }
}

Và render bằng SchemaComponent:

jsx
<SchemaComponent schema={schema} />

SchemaComponent - trình render schema

PropÝ nghĩa
schemaJSON Schema hoặc Formily Schema object
componentsMap tên → React component (override/extend)
scopeVariables & functions dùng trong schema expressions
memoizedNếu true, schema được memo - tạo subtree độc lập

Schema expressions

Schema hỗ trợ expression trong chuỗi "":

json
{
  "x-component-props": {
    "title": "{{ t('Hello') }}",
    "onClick": "{{ () => console.log('clicked') }}"
  }
}

SchemaComponentProvider đăng ký compiler cho - Formily parse và evaluate expression với scope context.

SchemaComponentProvider

Provider này thiết lập environment cho schema rendering:

Chế độ designable được lưu trong localStorage (key DIGIFORCE_${appName}_DESIGNABLE) để giữ trạng thái qua refresh.


SchemaInitializer - thêm block/field vào UI

SchemaInitializer quản lý menu "thêm mới" - khi user click nút "+" trong design mode, hệ thống hiển thị danh sách component có thể thêm.

Cấu trúc

SchemaInitializerOptions

typescript
interface SchemaInitializerOptions {
  name: string;
  items: SchemaInitializerItemType[];
  Component?: ComponentType;     // Button/trigger component
  insert?: (schema: ISchema) => void;  // Custom insert logic
  wrap?: (schema: ISchema) => ISchema; // Transform trước khi insert
  popover?: boolean;             // Hiển thị popup menu
}

Đăng ký và sử dụng

typescript
// Tạo initializer
const pageAddBlock = new SchemaInitializer({
  name: 'page:addBlock',
  items: [
    {
      name: 'dataBlocks',
      title: 'Data blocks',
      type: 'itemGroup',
      children: [
        { name: 'table', title: 'Table', Component: TableInitItem },
        { name: 'form', title: 'Form', Component: FormInitItem },
      ],
    },
    {
      name: 'otherBlocks',
      title: 'Other blocks',
      type: 'itemGroup',
      children: [
        { name: 'markdown', title: 'Markdown', Component: MarkdownInitItem },
      ],
    },
  ],
});

// Đăng ký vào manager
app.schemaInitializerManager.add(pageAddBlock);

// Plugin thêm item vào initializer đã có
app.schemaInitializerManager.addItem('page:addBlock', 'otherBlocks.kanban', {
  title: 'Kanban',
  Component: KanbanInitItem,
});

SchemaInitializerManager API

MethodMô tả
add(initializer)Đăng ký SchemaInitializer
get(name)Lấy initializer theo tên
addItem(initializerName, itemName, item)Thêm item vào initializer (hỗ trợ dot notation cho nesting)
removeItem(initializerName, itemName)Xóa item

Nếu initializer chưa được đăng ký khi addItem được gọi, action sẽ được queue và áp dụng sau khi initializer register - giải quyết vấn đề thứ tự load giữa plugins.


SchemaSettings - cấu hình component

SchemaSettings quản lý menu cấu hình cho từng component trong design mode - khi user click biểu tượng gear/settings, hệ thống hiển thị các tùy chọn.

Cấu trúc

Đăng ký

typescript
const blockSettings = new SchemaSettings({
  name: 'BlockSettings',
  items: [
    { name: 'editTitle', type: 'modal', Component: EditTitleSetting },
    { name: 'divider1', type: 'divider' },
    { name: 'remove', type: 'remove', Component: RemoveBlockSetting },
  ],
});

app.schemaSettingsManager.add(blockSettings);

API giống SchemaInitializerManager - add, get, addItem, removeItem với cùng cơ chế deferred queue.


PluginSettingsManager - trang cấu hình plugin

Quản lý các trang settings trong Admin → Settings, mỗi plugin có thể đăng ký trang riêng.

Đăng ký

typescript
// Trong plugin.load()
this.pluginSettingsManager.add('kanban', {
  title: 'Kanban',
  icon: 'AppstoreOutlined',
  Component: KanbanSettingsPage,
});

// Nested settings (dot notation)
this.pluginSettingsManager.add('kanban.boards', {
  title: 'Board Management',
  Component: BoardManagementPage,
});

Bên trong, add() tự động gọi app.router.add() để tạo route tương ứng.

ACL tích hợp

Mỗi settings page tự động có ACL snippet dạng pm.${name}. Plugin có thể custom:

typescript
this.pluginSettingsManager.add('kanban', {
  title: 'Kanban',
  Component: KanbanSettingsPage,
  aclSnippet: 'pm.kanban.admin',  // Custom snippet
  // hoặc
  skipAclConfigure: true,          // Bỏ qua ACL check
});

DataSourceManager - dữ liệu phía client

Kiến trúc

DataSourceManager

typescript
interface DataSourceManagerOptions {
  collectionTemplates?: CollectionTemplate[];
  fieldInterfaces?: CollectionFieldInterface[];
  fieldInterfaceGroups?: Record<string, { label: string }>;
  collectionMixins?: CollectionMixin[];
  dataSources?: DataSource[];
  collections?: CollectionOptions[];
}
MethodMô tả
getDataSource(key?)Lấy data source (mặc định 'main')
addDataSources(request, DataSource)Thêm data source mới
reload()Refresh tất cả data sources
getCollectionManager(dataSource?)Shortcut lấy CollectionManager

CollectionManager

Quản lý collection (bảng dữ liệu) trên client - build Collection instances từ server metadata.

typescript
// Lấy collection
const usersCollection = collectionManager.getCollection('users');

// Hỗ trợ association path
const ordersCollection = collectionManager.getCollection('users.orders');
// → resolve: users → field "orders" (hasMany) → target collection "orders"

// Lấy field
const emailField = collectionManager.getCollectionField('users.email');
MethodMô tả
getCollection(path)Lấy collection, hỗ trợ a.b.c association path
getCollectionField(path)Lấy field definition
getCollectionFields(name)Lấy tất cả fields của collection
getFilterByTargetKey(name)Lấy filter key cho association

Collection class

typescript
class Collection {
  name: string;
  title?: string;
  fields: CollectionFieldOptions[];
  primaryKey: string;
  filterTargetKey: string;

  getField(name: string): CollectionFieldOptions;
  getFields(predicate?): CollectionFieldOptions[];
}

CollectionField component

CollectionField là React component (Formily connect) - resolve component phù hợp dựa trên field interface và UI schema:

Provider chain cho data source


APIClient - giao tiếp với server

Kế thừa

Client mở rộng SDK APIClient với:

Tính năngMô tả
app referenceLiên kết ngược về Application
notificationAnt Design notification (set bởi AntdAppProvider)
servicesCache kết quả cho useRequest với uid
silent()Clone instance không hiển thị notification lỗi

Headers tự động

HeaderGiá trị
X-AppTên app (app.name)
X-TimezoneTimezone trình duyệt
X-Hostnamewindow.location.hostname
X-LocaleNgôn ngữ hiện tại
X-RoleRole hiện tại
AuthorizationBearer token
X-With-ACL-MetaACL metadata flag
X-AuthenticatorAuthenticator name

Interceptors - xử lý response

Sử dụng trong component

typescript
import { useAPIClient, useRequest } from '@digiforce-nc/client';

function MyComponent() {
  // Truy cập trực tiếp
  const api = useAPIClient();

  // useRequest - tự động fetch & cache
  const { data, loading, run } = useRequest({
    url: 'users:list',
    params: { pageSize: 20 },
  });

  // useRequest với uid - đăng ký vào api.services
  const { data: profile } = useRequest(
    { url: 'users:get', params: { filterByTk: 1 } },
    { uid: 'currentUser' },
  );

  // Resource pattern
  const resource = api.resource('orders');
  await resource.list({ pageSize: 10 });
  await resource.create({ values: { title: 'New order' } });
}

WebSocketClient - kết nối real-time

Xử lý sự kiện

Auto-recovery: khi server trở lại từ bảo trì và client có hasLoadError (load thất bại trước đó), browser tự reload để khôi phục.


Designable - chỉnh sửa UI trực tiếp

Designable system cho phép user chỉnh sửa layout và cấu hình component trực tiếp trên trình duyệt.

Designable class

typescript
class Designable {
  current: Schema;       // Schema node hiện tại
  uiWidget: UiWidget;
  api: APIClient;

  // Thao tác schema
  insertAdjacent(position, schema);  // Thêm node
  insertBeforeBegin(schema);         // Thêm trước element
  insertAfterBegin(schema);          // Thêm vào đầu children
  insertBeforeEnd(schema);           // Thêm vào cuối children
  insertAfterEnd(schema);            // Thêm sau element
  remove();                          // Xóa node

  // Cập nhật
  patch(schema);         // Cập nhật props
  shallowMerge(schema);  // Shallow merge
  deepMerge(schema);     // Deep merge
}

Kết nối với server

Mọi thao tác designable tự động gọi REST API để persist vào DB:

Thao tácAPI endpoint
insertAdjacentPOST /uiSchemas:insertAdjacent
patchPATCH /uiSchemas:patch
removePOST /uiSchemas:remove
batchPatchPATCH /uiSchemas:batchPatch

useDesignable hook

typescript
import { useDesignable } from '@digiforce-nc/client';

function MyBlock() {
  const { designable, dn, remove, insertAfterEnd } = useDesignable();

  if (!designable) return <div>View mode</div>;

  return (
    <div>
      <button onClick={() => dn.patch({ 'x-component-props': { title: 'New' } })}>
        Edit
      </button>
      <button onClick={() => remove()}>Delete</button>
    </div>
  );
}

SchemaToolbar - thanh công cụ design mode

Khi design mode bật, mỗi block hiển thị toolbar với các nút thao tác:

┌──────────────────────────────────┐
│ [drag] Table Block    [+] [⚙]   │  ← SchemaToolbar
├──────────────────────────────────┤
│                                  │
│  (Nội dung block)                │
│                                  │
└──────────────────────────────────┘
Phần tửSourceMô tả
[drag]DnD handleKéo thả di chuyển block
Titlex-toolbar-props.titleTên block / data source
[+]SchemaInitializerThêm field/column
[⚙]SchemaSettingsCấu hình block

Schema khai báo toolbar qua:

json
{
  "x-toolbar": "SchemaToolbar",
  "x-settings": "BlockSettings",
  "x-initializer": "table:configureColumns"
}

Theming - giao diện

GlobalThemeProvider

Quản lý Ant Design theme (light/dark mode, custom colors):

typescript
interface ThemeState {
  theme: ThemeConfig;        // Ant Design theme config
  setTheme: (theme) => void;
  isDarkTheme: boolean;
}

Hỗ trợ:

  • Dark mode - phát hiện qua algorithm trong theme config.
  • Custom algorithm - addCustomAlgorithmToTheme thêm algorithm tùy chỉnh.
  • Compat - compatOldTheme chuyển đổi theme format cũ sang Ant Design 5.

AntdAppProvider

Cung cấp Ant Design App context - đặc biệt sync notification instance vào apiClient.notificationapp.notification, cho phép hiển thị notification từ bất kỳ đâu (không chỉ trong React tree).


Hooks tổng hợp

HookMô tả
useApp()Lấy Application instance
usePlugin(name | Class)Lấy plugin instance
useAPIClient()Lấy APIClient
useRequest(service, options?)Fetch data (ahooks useRequest wrapper)
useDesignable()Designable context (dn, patch, remove, insertAdjacent…)
useCollection()Collection hiện tại
useCollectionField()Field hiện tại
useCollectionManager()CollectionManager
useDataSourceManager()DataSourceManager
useSchemaToolbar()SchemaToolbar context

useRequest chi tiết

typescript
// Function service - gọi hàm tùy ý
const { data } = useRequest(() => api.resource('users').list());

// Object service - auto-convert thành api.request(...)
const { data } = useRequest({
  url: 'users:list',
  params: { pageSize: 20 },
});

// Với uid - kết quả cache trong api.services
const { data } = useRequest(
  { url: 'auth:check' },
  { uid: 'auth' },
);
// Sau đó: api.services['auth'] chứa kết quả

Luồng khởi tạo hoàn chỉnh


Đọc thêm