Chuyển tới nội dung chính

Viết giao diện module

Một module có giao diện xuất hiện trong cổng tenant như một nhóm riêng trên thanh điều hướng, ngang "Tổng quan", "Cấu hình hệ thống"…, mỗi mục menus trong module.yaml là một màn. Người dùng thao tác trên màn module như mọi màn khác của cổng: cùng đầu trang, cùng component antd, cùng chủ đề sáng hoặc tối.

Bạn viết màn bằng React và @platform/module-sdk, build, đóng vào gói zip cùng module.yaml, ký và nộp. Platform Team duyệt; tenant cài; nhóm menu hiện ra. Mã chạy trong trình duyệt chính là mã trong gói đã ký và đã duyệt: nền tảng phục vụ thẳng từ gói, không tải từ máy chủ nào khác.

Module mẫu chạy được trong kho: modules/demo-sdk (màn Tổng quan có thẻ số liệu và màn Công việc có bảng, form thêm, xoá, trang chi tiết).

1. Nhận SDK​

SDK là tệp platform-module-sdk-<phiên bản>.tgz do FoxAI gửi (chưa phát hành lên registry công khai). Đặt tệp cạnh dự án rồi khai trong package.json:

package.json
{
"name": "module-crm",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"pack:module": "vite build && platform-module pack"
},
"dependencies": {
"@platform/module-sdk": "file:./platform-module-sdk-0.1.0.tgz",
"antd": "^6.6.6",
"@ant-design/icons": "^6.1.0",
"react": "^19.3.1",
"react-dom": "^19.3.1"
},
"devDependencies": {
"vite": "^8.3.4",
"@types/react": "^19.3.0",
"typescript": "^5.9.3"
}
}

npm install hay pnpm install đều được. Gói SDK gồm ba phần: runtime React (@platform/module-sdk), preset Vite (@platform/module-sdk/vite), và lệnh platform-module để sinh khoá và đóng gói.

2. Bố cục dự án​

module-crm/
├── module.yaml # khai báo module, xem trang "module.yaml"
├── package.json
├── vite.config.ts
├── index.html # chỉ để chạy thử độc lập, không vào gói
└── src/
├── App.tsx # default export = defineModule(App): cổng nạp tệp này
└── dev.tsx # chạy thử độc lập
vite.config.ts
import { defineConfig } from 'vite';
import { platformModule } from '@platform/module-sdk/vite';

export default defineConfig({ plugins: [platformModule()] });

Preset tự cấu hình Module Federation: build ra dist/remoteEntry.js, expose ./src/App.tsx, dùng chung react, react-dom, antd, @ant-design/icons với cổng. Đổi tệp gốc bằng platformModule({ entry: './src/Goc.tsx' }).

module.yaml (phần giao diện)
permissions:
- crm:khach-hang:read
- crm:khach-hang:write

menus:
- path: / # /client/m/crm
label: Khách hàng # tên mục trên thanh điều hướng, không dịch
permission: crm:khach-hang:read
- path: /bao-cao # /client/m/crm/bao-cao
label: Báo cáo
permission: crm:khach-hang:read

frontend:
entry: remoteEntry.js # tệp trong frontend/ của gói; preset luôn sinh tên này
  • Tên nhóm trên thanh điều hướng là name của module; mỗi mục menus là một mục trong nhóm.
  • permission của mỗi mục phải nằm trong permissions. Người dùng thiếu quyền không thấy mục đó; Tenant Admin có mọi quyền của module đã cài.
  • path mở đầu bằng /, chỉ gồm chữ, số và . _ ~ -; không //, không đoạn . hay ...
  • sdkVersion phải chứa phiên bản SDK của nền tảng (hiện 1.0.0), ví dụ ">=1.0 <2.0", không thì cài báo MODULE_SDK_INCOMPATIBLE.

3. Viết màn​

src/App.tsx
import { App as AntdApp, Card, Table } from 'antd';
import { Link, Routes, defineModule, useModule, useParams } from '@platform/module-sdk';

function DanhSach() {
const { hasPermission } = useModule();
return (
<Card title="Khách hàng" extra={hasPermission('crm:khach-hang:write') && <Link to="/moi">Thêm</Link>}>
<Table rowKey="id" dataSource={[]} columns={[{ title: 'Tên', dataIndex: 'ten' }]} />
</Card>
);
}

function ChiTiet() {
const { id } = useParams<{ id: string }>();
return <Card title={`Khách hàng ${id}`} />;
}

function App() {
return (
<AntdApp>
<Routes
routes={[
{ path: '/', element: <DanhSach /> },
{ path: '/khach-hang/:id', element: <ChiTiet /> },
]}
fallback={<Card>Không có màn này</Card>}
/>
</AntdApp>
);
}

export default defineModule(App);

Cổng vẽ sẵn đầu trang (breadcrumb "tên module › mục đang xem", tiêu đề, mã và phiên bản module). Module chỉ vẽ phần thân bên dưới. Bọc bằng <AntdApp> để message, Modal.confirm theo đúng chủ đề cổng.

API của SDK​

APIDùng để
defineModule(App)Bọc component gốc; bắt buộc cho default export của src/App.tsx
useModule(){ basePath, path, locale, navigate, hasPermission, getAccessToken, api }
api.get/post/put/patch/delete(url, body?)fetch gắn sẵn token của người dùng, nhận và trả JSON, lỗi ném ApiError (status, problem theo Problem Details)
navigate(path)Chuyển màn trong module, path tương đối với gốc module (/khach-hang/12)
hasPermission(code)Người dùng có mã quyền {module}:{tài nguyên}:{hành động} không; dùng để ẩn nút, không phải để chặn (Gateway và backend mới chặn)
<Routes routes fallback>, useParams()Chọn màn theo đường dẫn, hỗ trợ tham số :id
<Link to>Liên kết trong module; Ctrl, ⌘, chuột giữa vẫn mở tab mới
useText({ vi, en })Chọn bộ chuỗi theo ngôn ngữ người dùng chọn ở cổng
matchPath(pattern, path)Khớp đường dẫn thủ công

Gọi backend của chính module qua Gateway: api.get('/api/v1/m/crm/khach-hang'). Gọi API nền tảng: api.get('/api/v1/users/me/context').

Giao diện​

  • Dùng component antd: cổng cung cấp antd của nó, nên màu nhấn, bo góc, chế độ tối người dùng chọn tự áp vào màn module.
  • Cần màu ngoài antd thì dùng biến CSS của cổng: var(--bg-surface), var(--text-primary), var(--text-secondary), var(--text-muted), var(--border-default), var(--color-primary-600), var(--color-danger-600). Không ghim mã màu, không viết riêng cho chế độ tối.
  • Class Tailwind của cổng không dùng được trong module: cổng chỉ sinh class mà mã của chính nó dùng.
  • Ngày hiển thị dd/MM/yyyy, số dùng dấu chấm phân nhóm, như mọi màn của cổng.

4. Chạy thử độc lập​

src/dev.tsx
import { mountStandalone } from '@platform/module-sdk/dev';
import App from './App';

mountStandalone(App, document.getElementById('root')!);
index.html
<!doctype html>
<html lang="vi">
<body>
<div id="root"></div>
<script type="module" src="/src/dev.tsx"></script>
</body>
</html>

npm run dev mở màn ngoài cổng, đường dẫn trình duyệt là path của module (/khach-hang/12), có mọi quyền. Truyền mountStandalone(App, el, { token: '...' }) khi cần gọi API thật. Chủ đề lúc này là antd mặc định; màu đúng của cổng chỉ thấy sau khi cài.

5. Khoá ký​

Mỗi bên nộp gói một cặp khoá Ed25519, sinh một lần:

npx platform-module keygen --out .khoa

Lệnh ghi khoá riêng vào .khoa/module-private-key.b64 (không commit, thêm vào .gitignore) và in khoá công khai. Gửi khoá công khai cho Platform Team để đăng ký làm khoá tin cậy; chưa đăng ký thì mọi gói báo MODULE_SIGNATURE_INVALID. Trên macOS, openssl genpkey -algorithm ed25519 không chạy được (LibreSSL), nên dùng lệnh này.

6. Đóng gói​

PLATFORM_MODULE_PRIVATE_KEY="$(cat .khoa/module-private-key.b64)" npm run pack:module

platform-module pack gom module.yaml, README.md (nếu có) và mọi tệp ở dist/ vào frontend/ của gói, bỏ index.html và source map, ký theo trang Ký gói, ghi <code>-<version>.zip. Lệnh in khoá công khai suy từ khoá riêng: so với khoá đã đăng ký trước khi nộp.

Tham sốMặc định
--manifestmodule.yaml
--distdist
--out<code>-<version>.zip
--key <tệp>đọc biến PLATFORM_MODULE_PRIVATE_KEY
--include <đường dẫn>thêm tệp hoặc thư mục khác, ví dụ backend/migrations

Nền tảng chỉ phục vụ tệp ở gốc dist/ và dist/assets/; preset đã sắp đúng như vậy. Gói tối đa 50 MB.

7. Từ gói tới thanh điều hướng​

  1. Nộp gói ở Quản lý module của cổng nền tảng (trang Nộp gói). Gói lên IN_REVIEW.
  2. Platform Team đọc mã trong gói và duyệt; bản đó lên PUBLISHED. Từ lúc này nền tảng mới phục vụ tệp giao diện của bản đó.
  3. Tenant Admin cài: POST /api/v1/modules/{code}/install, theo dõi bằng GET /api/v1/modules/{code}/installation tới khi status là ACTIVE. Cổng tenant chưa có màn cài module.
  4. Người dùng của tenant tải lại cổng: nhóm mang tên module hiện trên thanh điều hướng, mỗi mục là một màn.

Cổng tự xử lý các trạng thái: đang tải, module chưa cài, module không có giao diện, không nạp được giao diện (kèm nút "Thử lại"). Màn module ném lỗi lúc hiển thị chỉ hỏng vùng của nó, phần còn lại của cổng vẫn dùng được.

8. Nâng cấp​

Sửa mã, tăng version trong module.yaml, đóng gói và nộp lại. Tenant chưa nâng cấp vẫn chạy giao diện của bản đang cài; nâng cấp xong thì cổng tự nạp bản mới.