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:
{
"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
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' }).
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à
namecủa module; mỗi mụcmenuslà một mục trong nhóm. permissioncủa mỗi mục phải nằm trongpermissions. 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.pathmở đầu bằng/, chỉ gồm chữ, số và. _ ~ -; không//, không đoạn.hay...sdkVersionphải chứa phiên bản SDK của nền tảng (hiện1.0.0), ví dụ">=1.0 <2.0", không thì cài báoMODULE_SDK_INCOMPATIBLE.
3. Viết màn
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
| API | Dù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
import { mountStandalone } from '@platform/module-sdk/dev';
import App from './App';
mountStandalone(App, document.getElementById('root')!);
<!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 |
|---|---|
--manifest | module.yaml |
--dist | dist |
--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
- 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. - 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 đó. - Tenant Admin cài:
POST /api/v1/modules/{code}/install, theo dõi bằngGET /api/v1/modules/{code}/installationtới khistatuslàACTIVE. Cổng tenant chưa có màn cài module. - 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.