Giao diện
@digiforce-nc/ui-core
Engine UI cốt lõi - widget system, reactive context, behavior execution, design mode, và JavaScript sandbox.
typescript
import { UiCore, UiWidget, observer } from '@digiforce-nc/ui-core';Tổng quan
Hãy hình dung ui-core như hệ điều hành cho giao diện:
- UiCore = kernel - điều phối widget, context, event, và behavior.
- UiWidget = process - mỗi block/component trên UI là một widget instance với state riêng.
- UiContext = bộ nhớ chia sẻ - hệ thống property động (computed, cached, observable) xuyên suốt component tree.
- UiConfig = bảng điều khiển - quản lý design mode (bật/tắt chỉnh sửa giao diện).
- UiBehavior = automation - logic tự động chạy trước/sau khi widget render hoặc khi event xảy ra.
- Observer = reactive wrapper - tự động re-render component khi dữ liệu thay đổi.
- RunJS = sandbox - chạy JavaScript tùy chỉnh trong môi trường cách ly an toàn.
Vị trí trong hệ thống
Dependencies chính
| Package | Vai trò |
|---|---|
@formily/reactive | Observable state, define, observable |
@formily/antd-v5 | UI components cho settings form |
@digiforce-nc/sdk | API client base |
@digiforce-nc/shared | Utilities dùng chung |
ahooks | React hooks (useRequest, etc.) |
slate | Rich text editor engine |
ses | Secure ECMAScript sandbox |
pino | Logging |
UiCore - engine trung tâm
UiCore là class trung tâm, quản lý toàn bộ widget, action, event, behavior, và context.
Constructor
typescript
const uiCore = new UiCore();Constructor không nhận tham số - khởi tạo theo thứ tự:
Thuộc tính và API chính
| Nhóm | API | Mô tả |
|---|---|---|
| Context | context | UiCoreContext (lazy - tạo lần đầu truy cập) |
| Config | uiConfig | Design mode, toolbar, settings dialog |
| Render | reactView | React mount / render system |
| Behavior | executor | UiBehaviorExecutor - chạy automation |
| Logger | logger | Pino logger instance |
| Emitter | emitter | Event emitter (sync + async) |
Widget class registry
UiCore quản lý class registry - đăng ký widget class trước, rồi tạo instance khi cần:
typescript
// Đăng ký widget class
uiCore.registerUiWidgets({ TableWidget, FormWidget, ChartWidget });
// Đăng ký lazy loader (chỉ load khi cần)
uiCore.registerUiWidgetLoaders({
KanbanWidget: () => import('./widgets/KanbanWidget'),
});
// Lấy class
const TableClass = uiCore.getUiWidgetClass('TableWidget');
const KanbanClass = await uiCore.getUiWidgetClassAsync('KanbanWidget');
// Lọc theo parent class
const dataWidgets = uiCore.getChildClassesOf(DataWidget);| Method | Mô tả |
|---|---|
registerUiWidgets(map) | Đăng ký widget classes (eager) |
registerUiWidgetLoaders(map) | Đăng ký lazy loaders (dynamic import) |
getUiWidgetClass(name) | Lấy class (sync - chỉ eager) |
getUiWidgetClassAsync(name) | Lấy class (async - hỗ trợ lazy) |
getUiWidgetClasses() | Tất cả classes đã đăng ký |
getChildClassesOf(parent) | Lọc classes kế thừa từ parent |
preloadUiWidgetLoaders() | Tải trước tất cả lazy widgets |
Widget instance management
typescript
// Tạo widget instance
const table = uiCore.createUiWidget({
use: 'TableWidget',
uid: 'table-1',
props: { rowKey: 'id', bordered: true },
});
// Tìm widget
const widget = uiCore.getUiWidget('table-1');
// Tìm trong cả view stack (global)
const widget = uiCore.getUiWidget('table-1', true);
// Xóa widget
uiCore.removeUiWidget('table-1');
// Lưu widget (persist qua repository)
await uiCore.saveUiWidget(widget);
// Duplicate, replace, move
await uiCore.duplicateUiWidget(widget, targetParent);
await uiCore.replaceUiWidget(widget, newWidgetOptions);
await uiCore.moveUiWidget(widget, newParent, index);| Method | Mô tả |
|---|---|
createUiWidget(options) | Tạo instance mới |
createUiWidgetAsync(options) | Tạo async (hỗ trợ lazy class) |
getUiWidget(uid, global?) | Tìm instance theo UID |
forEachUiWidget(callback) | Duyệt tất cả instances |
removeUiWidget(uid) | Xóa instance |
loadUiWidget(data) | Load từ serialized data |
saveUiWidget(widget) | Persist qua repository |
destroyUiWidget(widget) | Hủy hoàn toàn (cleanup) |
duplicateUiWidget(widget, parent) | Nhân bản widget |
replaceUiWidget(widget, options) | Thay thế bằng widget mới |
moveUiWidget(widget, parent, idx) | Di chuyển vị trí |
Action & Event registry
typescript
// Đăng ký action (logic có thể gọi)
uiCore.registerActions({
refreshData: {
label: 'Refresh data',
handler: async (ctx) => { await ctx.resource.refresh(); },
},
});
// Đăng ký event (sự kiện widget phát ra)
uiCore.registerEvents({
rowClick: {
label: 'Row clicked',
schema: { rowId: { type: 'string' } },
},
});View stack
UiCore hỗ trợ view stack cho navigation giữa các view - mỗi view có UiCore riêng, liên kết qua previousUiCore / nextUiCore:
Khi gọi getUiWidget(uid, true), hệ thống tìm từ view hiện tại ngược lại qua stack.
Resource system
typescript
// Đăng ký resource types
uiCore.registerResources({
CustomResource: MyCustomResource,
});
// Tạo resource instance
const resource = uiCore.createResource('APIResource', {
url: '/api/users',
});UiConfig - design mode
UiConfig quản lý chế độ thiết kế giao diện - cho phép user chỉnh sửa layout, cấu hình widget trực tiếp trên browser.
State machine
| Method | Hành vi |
|---|---|
enable() | Preload tất cả widget loaders → enabled = true |
disable() | enabled = false (no-op nếu forceEnabled) |
forceEnable() | Preload → set forced flag → enabled = true |
forceDisable() | Clear forced flag → enabled = false |
enabled là Formily observable - mọi component observe sẽ tự re-render khi toggle.
Đồng bộ với context
typescript
uiCore.context.defineProperty('uiConfigEnabled', {
get: () => this.enabled,
cache: false, // Luôn đọc giá trị mới nhất
});Widget kiểm tra context.uiConfigEnabled để quyết định hiển thị toolbar, settings menu, hay widget đang ẩn.
Toolbar
UiConfig quản lý toolbar items (nút trên thanh công cụ design mode):
typescript
uiConfig.addToolbarItem({
key: 'my-button',
sort: 100,
Component: MyToolbarButton,
});
// Mặc định có sẵn: settings-menu (DefaultUiConfigIcon)
const items = uiConfig.getToolbarItems(); // sorted by `sort`Component & scope registry
UiConfig quản lý Formily components và scopes cho settings form:
typescript
// Đăng ký components cho settings UI
uiConfig.registerComponents({
ColorPicker: MyColorPicker,
});
// Lazy loading
uiConfig.registerComponentLoaders({
RichEditor: () => import('./RichEditor'),
});
// Đăng ký scope (biến dùng trong schema expression)
uiConfig.registerScopes({
t: uiCore.translate,
customHelper: myHelper,
});Settings dialog
uiConfig.open(options) mở dialog cấu hình cho widget:
UiWidget - widget instance
Mỗi block, form, table, chart trên UI là một UiWidget instance - có state riêng, lifecycle, và reactive rendering.
Cấu trúc
Observable state
Widget sử dụng Formily define để biến thuộc tính thành reactive:
typescript
define(this, {
hidden: observable,
props: observable, // shallow
childWidgets: observable, // shallow
stepParams: observable,
});Khi props, stepParams, hay hidden thay đổi → component tự re-render (nhờ observer wrapper).
Tạo widget - luồng chi tiết
Render - reactive wrapping
Mỗi widget có method render() trả về React element. Khi tạo, setupReactiveRender() tự động wrap method này bằng observer:
Observer wrapper đảm bảo:
- Auto re-render khi
props,stepParams,hiddenthay đổi. - Hidden widget không render (tiết kiệm tài nguyên), trừ khi đang ở design mode (hiện mờ để user thấy).
- Lifecycle events:
onMount,onUnmountvà emituiWidget:mounted,uiWidget:unmounted.
Events & behaviors
Widget phát ra event → behavior executor chạy automation:
typescript
// Phát event
widget.dispatchEvent('rowClick', { rowId: '123' });
// beforeRender - event đặc biệt, chạy trước mỗi lần render
widget.dispatchEvent('beforeRender');
// → sequential, cached (không chạy lại nếu params không đổi)
// Apply behavior thủ công
await widget.applyBehavior('fetchData', { url: '/api/users' });
// Force re-render (xóa cache beforeRender)
widget.rerender();Widget context
Mỗi widget có UiWidgetContext riêng - delegate lên uiCore.context:
Khi đọc property từ UiWidgetContext, nếu không tìm thấy → tự động tìm trong delegate (UiCoreContext).
UiContext - hệ thống context thông minh
UiContext là hệ thống property động mạnh mẽ nhất trong ui-core - mỗi property có thể static, computed, cached, observable, hoặc async.
Ý tưởng
Thay vì React context truyền thống (một object cố định), UiContext cho phép define property bất kỳ lúc nào, với getter/setter, caching, và reactivity:
typescript
const ctx = uiCore.context;
// Static value
ctx.defineProperty('appName', { value: 'My App' });
// Computed (cached)
ctx.defineProperty('currentUser', {
get: async () => {
const res = await api.request({ url: 'auth:check' });
return res.data;
},
cache: true, // Chỉ gọi API lần đầu, sau đó dùng cache
observable: true, // Re-render component khi giá trị thay đổi
});
// Computed (không cache)
ctx.defineProperty('uiConfigEnabled', {
get: () => uiConfig.enabled,
cache: false, // Luôn đọc giá trị mới nhất
});
// Define once (không ghi đè)
ctx.defineProperty('immutableProp', {
value: 42,
once: true, // Lần define sau sẽ bị bỏ qua
});PropertyOptions
typescript
interface PropertyOptions {
value?: any; // Giá trị static
get?: (ctx) => T; // Getter (sync hoặc async)
cache?: boolean; // Cache kết quả (mặc định: true)
observable?: boolean; // Dùng observable cache (trigger re-render)
once?: boolean; // Chỉ define 1 lần
meta?: object; // Metadata cho variable picker UI
info?: object; // API documentation
}Caching - 3 chế độ
| Chế độ | Cache | Observable | Dùng cho |
|---|---|---|---|
cache: false | Không | Không | Giá trị thay đổi liên tục (uiConfigEnabled) |
cache: true | _cache (plain) | Không | Dữ liệu tĩnh hoặc ít thay đổi |
cache: true, observable: true | _observableCache (reactive) | Có | Dữ liệu cần trigger re-render (themeToken) |
Async getter - deduplication
Khi getter trả về Promise, UiContext deduplicate - nhiều component đọc cùng lúc chỉ gọi getter 1 lần:
Delegation chain
UiContext hỗ trợ delegation - khi property không tìm thấy, tìm tiếp trong delegate:
Thứ tự tìm:
- Property trên chính context
- Depth-first search qua
_delegates[]
Proxy implementation
UiContext dùng ES Proxy để intercept mọi property access:
typescript
// Bên trong constructor
return createProxy(); // → new Proxy(target, handler)
// handler.get(target, key):
// 1. Own property trên instance → trả về
// 2. _props[key] → _getOwnProperty(key)
// 3. _methods[key] → _getOwnMethod(key)
// 4. Tìm trong _delegates (depth-first)UiCoreContext - context mặc định
UiCoreContext kế thừa UiContext, tự động define nhiều property hữu ích:
Properties có sẵn
| Property | Kiểu | Mô tả |
|---|---|---|
uiCore | UiCore | Reference đến UiCore |
dataSourceManager | DataSourceManager | Quản lý data source |
sql | function | SQL query helper |
t | function | i18n translation |
resolveJsonTemplate | function | Template engine ( resolution) |
getVar | function | Đọc biến từ context path |
token | string | Auth token |
role | string | Current role |
urlSearchParams | object | URL query params |
logger | Logger | Pino logger |
auth | object | Auth utilities |
date | object | Date utilities |
acl | object | ACL helper |
getAction / getActions | function | Lấy registered actions |
getEvents | function | Lấy registered events |
runAction | function | Chạy action |
createResource | function | Tạo resource instance |
requireAsync / importAsync | function | Dynamic import |
createJSRunner | function | Tạo RunJS sandbox instance |
Properties do host inject
Những property sau được Application (host) inject sau khi mount:
| Property | Injected by | Mô tả |
|---|---|---|
app | Application constructor | Application instance |
api | Application constructor | APIClient |
router | Application constructor | React Router instance |
route | Application constructor | Current route info |
location | Application constructor | Browser location (observable) |
viewer | UiCoreGlobalsContextProvider | Dialog/drawer/page opener |
modal | UiCoreGlobalsContextProvider | Antd Modal instance |
message | UiCoreGlobalsContextProvider | Antd Message instance |
notification | UiCoreGlobalsContextProvider | Antd Notification instance |
themeToken | UiCoreGlobalsContextProvider | Antd theme token (observable) |
antdConfig | UiCoreGlobalsContextProvider | Antd ConfigProvider |
Observer - reactive rendering
Vấn đề
React component bình thường không tự re-render khi Formily observable thay đổi. Cần wrapper.
Giải pháp: observer()
typescript
import { observer } from '@digiforce-nc/ui-core';
const MyComponent = observer(() => {
const widget = useUiWidget();
// Tự động re-render khi widget.props thay đổi
return <div>{widget.props.title}</div>;
});Cơ chế bên trong
observer của ui-core wrap @formily/reactive-react observer với custom scheduler thông minh:
Tại sao cần custom scheduler? Khi user chuyển tab hoặc navigate sang trang khác, component không visible - re-render lúc này lãng phí và có thể gây infinite loop. Scheduler kiểm tra pageActive và tabActive trước khi cho phép update.
UiWidgetRenderer - render pipeline
UiWidgetRenderer là React component render một widget instance theo pipeline đầy đủ:
beforeRender - behavior đặc biệt
beforeRender là event chạy trước mỗi lần render, dùng để fetch data, tính toán, chuẩn bị state:
typescript
// Behavior đăng ký trên widget
widget.behaviorRegistry.add('fetchUsers', {
event: 'beforeRender',
handler: async (ctx) => {
const users = await ctx.api.resource('users').list();
ctx.uiWidget.setProps({ dataSource: users });
},
});Đặc điểm:
- Sequential - các behavior chạy tuần tự (không song song).
- Cached - nếu
stepParamskhông đổi, không chạy lại. - Force rerun - gọi
widget.rerender()để xóa cache và chạy lại.
UiBehavior - automation engine
Behavior là logic tự động gắn với widget event:
Hai cấp behavior registry
| Cấp | Registry | Phạm vi |
|---|---|---|
| Global | GlobalUiBehaviorRegistry | Tất cả instance của widget class |
| Instance | InstanceUiBehaviorRegistry | Chỉ widget instance cụ thể |
dispatchEvent flow
Emitter - event system
Emitter nhẹ, hỗ trợ cả sync và async:
typescript
import { Emitter } from '@digiforce-nc/ui-core';
const emitter = new Emitter();
// Đăng ký listener
emitter.on('dataChanged', (data) => {
console.log('Data:', data);
});
// Phát event (sync)
emitter.emit('dataChanged', { id: 1 });
// Phát event (async - chờ tất cả listener hoàn tất)
await emitter.emitAsync('dataChanged', { id: 1 });
// Tạm dừng events
emitter.setPaused(true);
// ... events bị giữ lại ...
emitter.setPaused(false);
// → events được phát ra| Emitter | Vị trí | Events tiêu biểu |
|---|---|---|
uiCore.emitter | UiCore level | uiWidget:created, uiWidget:destroyed |
uiWidget.emitter | Widget level | uiWidget:mounted, uiWidget:unmounted |
uiConfig.#emitter | UiConfig (private) | beforeOpen |
RunJS - JavaScript sandbox
Cho phép user viết logic JavaScript tùy chỉnh chạy trong môi trường cách ly (dùng ses - Secure ECMAScript):
typescript
import { compileRunJs, registerRunJSLib } from '@digiforce-nc/ui-core';
// Đăng ký thư viện cho sandbox
registerRunJSLib('lodash', _);
registerRunJSLib('dayjs', dayjs);
// Biên dịch code thành function
const fn = compileRunJs(`
const name = lodash.capitalize(input.name);
const date = dayjs().format('YYYY-MM-DD');
return { name, date };
`);
// Chạy với context
const result = fn({ input: { name: 'john' } });
// → { name: 'John', date: '2026-04-08' }| API | Mô tả |
|---|---|
compileRunJs(code) | Biên dịch JS string thành callable function |
registerRunJSLib(name, lib) | Đăng ký thư viện cho sandbox access |
JSRunner | Class quản lý vòng đời sandbox instance |
createJSRunner | Factory (trên context) |
Providers - kết nối vào React tree
Provider hierarchy
UiCoreProvider
Cung cấp UiCore instance cho toàn bộ React tree:
tsx
<UiCoreProvider uiCore={uiCore}>
{children}
</UiCoreProvider>Bên trong wrap UiContextProvider với uiCore.context.
UiCoreGlobalsContextProvider
Phải nằm dưới Antd App component - sử dụng App.useApp() để lấy modal, message, notification, rồi inject vào context:
typescript
// Bên trong UiCoreGlobalsContextProvider:
context.defineProperty('viewer', {
cache: false,
get: () => new UiViewer(/* dialog, drawer, page, popover */),
});
context.defineProperty('modal', { value: antdApp.modal });
context.defineProperty('message', { value: antdApp.message });
context.defineProperty('notification', { value: antdApp.notification });
context.defineProperty('themeToken', {
observable: true,
cache: true,
get: () => antdThemeToken,
});UiWidgetProvider
Wrap mỗi widget, cung cấp uiWidget.context (delegate lên uiCore.context):
tsx
<UiWidgetProvider uiWidget={widget}>
{/* Widget render output */}
</UiWidgetProvider>Hooks
| Hook | Mô tả |
|---|---|
useUiCore() | Lấy UiCore instance (từ UiCoreProvider) |
useUiCoreContext() | Shortcut cho useUiCore().context |
useUiContext() | Lấy UiContext hiện tại (core hoặc widget level) |
useUiWidget() | Lấy UiWidget instance (từ UiWidgetProvider) |
useUiWidgetContext() | Lấy widget context |
useUiWidgetById(uid) | Tìm hoặc tạo widget theo UID |
useApplyAutoBehaviors() | Chạy beforeRender behaviors (dùng trong renderer) |
useUiConfigContext() | Context trong settings dialog |
useUiStep() | Step context trong UiConfig settings flow |
useUiCoreView() | Lấy current view từ context |
Ví dụ sử dụng hooks
tsx
import { useUiCore, useUiWidget, useUiContext, observer } from '@digiforce-nc/ui-core';
const MyBlock = observer(() => {
const uiCore = useUiCore();
const ctx = useUiContext();
// Đọc property từ context (có thể async, cached, observable)
const user = ctx.currentUser;
const theme = ctx.themeToken;
return (
<div style={{ color: theme?.colorPrimary }}>
Hello, {user?.name}
</div>
);
});
const WidgetContent = observer(() => {
const widget = useUiWidget();
return (
<div>
<h3>{widget.props.title}</h3>
{widget.childWidgets.map(child => (
<UiWidgetRenderer key={child.uid} uiWidget={child} />
))}
</div>
);
});View scoped UiCore
Cho phép tạo UiCore riêng cho mỗi view hoặc block, tránh xung đột state:
typescript
import { createViewScopedUiCore, createBlockScopedUiCore } from '@digiforce-nc/ui-core';
// UiCore riêng cho một view (chia sẻ state giữa blocks trong view)
const viewCore = createViewScopedUiCore('dashboard-view');
// UiCore cô lập cho một block (state hoàn toàn độc lập)
const blockCore = createBlockScopedUiCore('chart-block-1');| Factory | Phạm vi | Use case |
|---|---|---|
createViewScopedUiCore(viewId) | Toàn view | Dashboard có nhiều block chia sẻ filter |
createBlockScopedUiCore(blockId) | Một block | Widget embed độc lập |
Kiến trúc tổng hợp - luồng render widget
Đọc thêm
- @digiforce-nc/client - Application sử dụng UiCore
- Client Architecture - kiến trúc tổng thể client
- Mã nguồn trên GitHub