Giao diện
@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
| Package | Vai trò |
|---|---|
react 18+ | UI framework |
antd 5 | Component library |
@formily/core, @formily/react | Schema-driven form & reactive state |
react-router-dom 6 | Client routing |
axios (qua @digiforce-nc/sdk) | HTTP client |
@digiforce-nc/sdk | APIClient base, Auth |
@digiforce-nc/ui-core | UiCore widget system, observer |
@digiforce-nc/utils | Registry, tiện ích chung |
@emotion/css | CSS-in-JS |
@dnd-kit | Drag & 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,
});| State | Kiểu | Ý nghĩa |
|---|---|---|
loading | boolean | true trong quá trình load() |
maintaining | boolean | true khi server đang bảo trì |
maintained | boolean | true khi server đã hoàn tất bảo trì (chờ reload) |
error | object | null | Lỗi nghiêm trọng (LOAD_ERROR, BLOCKED_IP…) |
hasLoadError | boolean | true nếu load() thất bại - trigger auto-reload khi server recovery |
So sánh với Server Application
| Khía cạnh | Server | Client |
|---|---|---|
| Base class | Koa + AsyncEmitter | Standalone (không kế thừa) |
| Event system | AsyncEmitter (async) | EventTarget (DOM standard) |
| State management | In-memory | @formily/reactive |
| Middleware | Toposort pipeline | Provider chain (React context) |
| Plugin loading | Filesystem + DB | Static + Remote (requirejs) |
| Rendering | HTTP response | React 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
| Type | Tạo bằng | Dùng cho |
|---|---|---|
hash | createHashRouter | URL dạng /#/admin (mặc định) |
browser | createBrowserRouter | URL dạng /admin (cần server config) |
memory | createMemoryRouter | Testing, SSR |
API
| Method | Mô 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 |
|---|---|
schema | JSON Schema hoặc Formily Schema object |
components | Map tên → React component (override/extend) |
scope | Variables & functions dùng trong schema expressions |
memoized | Nế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
| Method | Mô 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[];
}| Method | Mô 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');| Method | Mô 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ăng | Mô tả |
|---|---|
app reference | Liên kết ngược về Application |
notification | Ant Design notification (set bởi AntdAppProvider) |
services | Cache kết quả cho useRequest với uid |
silent() | Clone instance không hiển thị notification lỗi |
Headers tự động
| Header | Giá trị |
|---|---|
X-App | Tên app (app.name) |
X-Timezone | Timezone trình duyệt |
X-Hostname | window.location.hostname |
X-Locale | Ngôn ngữ hiện tại |
X-Role | Role hiện tại |
Authorization | Bearer token |
X-With-ACL-Meta | ACL metadata flag |
X-Authenticator | Authenticator 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ác | API endpoint |
|---|---|
insertAdjacent | POST /uiSchemas:insertAdjacent |
patch | PATCH /uiSchemas:patch |
remove | POST /uiSchemas:remove |
batchPatch | PATCH /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ử | Source | Mô tả |
|---|---|---|
[drag] | DnD handle | Kéo thả di chuyển block |
| Title | x-toolbar-props.title | Tên block / data source |
[+] | SchemaInitializer | Thêm field/column |
[⚙] | SchemaSettings | Cấ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
algorithmtrong theme config. - Custom algorithm -
addCustomAlgorithmToThemethêm algorithm tùy chỉnh. - Compat -
compatOldThemechuyể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.notification và app.notification, cho phép hiển thị notification từ bất kỳ đâu (không chỉ trong React tree).
Hooks tổng hợp
| Hook | Mô 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
- Client Architecture - kiến trúc tổng thể phía client
- Plugin Architecture - hệ thống plugin
- Server - Application - đối chiếu với server
- @digiforce-nc/sdk - APIClient base, Auth
- @digiforce-nc/ui-core - UiCore widget system
- Mã nguồn