@snack-kit/core
v0.4.1
Published
Snack by Para FED
Readme
@snack-kit/core
Snack 动态模块加载与渲染 SDK,基于 React + RequireJS 构建。支持从服务端加载 UMD 格式的 Snack 模块,并将设计器页面 JSON 配置渲染为 React 组件树,同时提供完整的表单数据管理、校验、国际化与事件系统。
安装
npm install @snack-kit/corePeer Dependencies
npm install react react-dom @snack-kit/lib快速开始
import React from 'react';
import ReactDOM from 'react-dom';
import { createRoot } from 'react-dom/client';
import { SnackSDK } from '@snack-kit/core';
import * as SnackKitLib from '@snack-kit/lib';
const sdk = new SnackSDK({
service: 'https://your-snack-service.com',
importMaps: {
'react': React,
'react-dom': ReactDOM,
'@snack-kit/lib': SnackKitLib
}
});
// 渲染设计器页面为 React 组件
const Page = await sdk.createPageComponent({ id: 'page-id', type: 'portal' });
createRoot(document.getElementById('app')!).render(<Page />);SnackSDK 构造参数
| 参数 | 类型 | 说明 |
|------|------|------|
| service | string | Snack 服务端地址 |
| importMaps | object | 模块依赖注入,key 为模块名,value 为模块对象 |
| defaultFormAttr | SnackFormAttribute | 表单组件 FormAttr 全局默认值 |
| runtime | boolean | 是否为运行时模式,默认 true;设计器模式设为 false |
| runtimeMapping | object | 运行时模块名映射 |
| modUrl | string | 模块加载根路径,默认 service + modPath |
| modPath | string | 模块请求路径,默认 /package |
| cdnUrl | string | CDN 根路径,默认 service + cdnPath |
| cdnPath | string | CDN 路径,默认 /lib |
| pageUrl | string | 页面数据请求根路径 |
| pagePath | string | 页面请求路径,默认 /page |
| staticUrl | string | 模块静态资源 CDN 路径 |
| importModules | ImportModules | 预置 Snack 模块 class,加载时优先从此查找 |
| basicsType | string | 基础模块分类名,默认 basics |
| i18n | I18nData | 国际化数据复写 |
| localeMapping | object | 国际化语言映射关系,例如 { zh: 'zh_CN' } |
| localeDefault | string | 默认国际化语言,默认 zh |
| message | object | 消息提示对象,供 evalJS 脚本使用 |
| argv | Record<string, ...> | 全局环境变量,在表达式中通过 $.argv 访问 |
核心 API
页面渲染
sdk.createPageComponent(params, config?)
将设计器页面渲染为 React 函数组件。
const Page = await sdk.createPageComponent(
{ id: 'my-page', type: 'portal' },
{
data: { username: 'admin' }, // 表单回填数据
onComplete: (event) => {
console.log('渲染完成', event.fields);
}
}
);
<Page />sdk.createPageForElement(el, params, config?)
将页面直接渲染到指定 DOM 元素。
await sdk.createPageForElement(document.getElementById('container')!, { id: 'my-page' });sdk.renderPage(params, config?)
渲染页面并返回根容器 DropOjb 实例(低层 API)。支持传入数组一次渲染多个页面到同一表单容器。
表单操作
渲染完成后,onComplete 回调参数 event: SnackPageCompleteEvent 提供以下方法:
// 获取表单数据(全量)
const formData = event.getData();
// 获取单个字段值
const username = event.getData('username');
// 设置表单数据
event.setData('username', 'new-value');
event.setData({ username: 'a', password: 'b' });
// 表单校验(返回 Promise<boolean>)
const passed = await event.verify();
const fieldPassed = await event.verify('username');
// 清空表单
event.empty();
// 销毁页面(解绑事件、释放模块)
event.destroy();模块渲染
sdk.createModuleComponent(args, data?)
加载并渲染单个 Snack 模块为 React 函数组件。
const Editor = await sdk.createModuleComponent(
{ name: 'codeeditor', type: 'code-editor' },
{ readOnly: false }
);
<Editor />sdk.createClassModule(SnackClass, data?, main?)
通过 Snack Class 直接创建模块实例。
const mod = sdk.createClassModule(MySnackClass, { value: 'hello' });sdk.createClassModuleComponent(SnackClass, data?, main?)
通过 Snack Class 创建模块并返回 React 函数组件。
模块加载
sdk.module(config)
按模块名称加载 UMD 模块,返回 Snack class 键值对。
const { mymodule } = await sdk.module({
module: [{ name: 'mymodule', type: 'custom' }]
});sdk.moduleURL(config)
按模块 URL 加载模块。
const mods = await sdk.moduleURL({
urls: 'https://cdn.example.com/package/custom/mymodule/index.js',
cdnUrl: 'https://cdn.example.com/lib'
});事件系统
// 注册事件
sdk.addEvent('onComplete', (root, params) => {
console.log('页面加载完成', root);
});
// 页面内组件事件(onChange、onBlur 等)由 SDK 自动分发
// 手动发送事件
sdk.sendEvent('custom-event', [arg1, arg2]);
// 卸载指定回调
sdk.removeEvent('onComplete', myCallback);
// 卸载所有同名事件
sdk.removeEvent('onComplete');内置事件
| 事件名 | 触发时机 |
|--------|----------|
| onComplete | 整个页面加载渲染完成 |
| onRemove | 模块被移除 |
| snack-event | 组件内部事件(onChange、onBlur 等)分发 |
国际化
// SDK 级别
sdk.intl('required'); // 返回 string | ReactNode
sdk.setI18n({ zh: { required: '此字段必填' } });
sdk.language('en-US'); // 切换所有已加载模块的语言
// 模块内(Snack 基础类)
this.$intl({ id: 'required' });
this.$intl('required', { label: '用户名' });
this.$lang; // 获取当前语言
this.$lang = 'en-US'; // 设置语言基础类
Snack<T>
所有 Snack 模块的基础类,继承此类实现自定义模块。
import { Snack, SnackData } from '@snack-kit/core';
interface MyData extends SnackData {
value: string;
label: { zh: string; en: string };
}
export class MyModule extends Snack<MyData> {
public $component(data?: MyData) {
return <div>{data?.value}</div>;
}
}常用属性与方法
| 成员 | 说明 |
|------|------|
| data | 模块数据对象 |
| sdk | 所属 SDK 实例 |
| $id | 模块唯一实例 ID |
| $name | 模块名称 |
| $parent | 父级 Snack 实例(通常为 Drag 容器) |
| $page | 所属页面事件对象 |
| $forceUpdate(reload?) | 强制重绘 |
| $style(key?, value?) | 读写样式并触发重绘 |
| $display(visible) | 显示 / 隐藏当前组件 |
| $intl(ops, params?) | 获取国际化文本 |
| $language(lang) | 切换语言 |
| $destroy() | 销毁模块,从 SDK moduleMaps 中移除 |
| FC(data) | React 渲染入口,由 SDK 调用 |
| $component(data?) | 子类重写此方法实现 UI |
SnackSetting
设置面板模块的基础类,通过 $setData 同步修改关联主模块(main)的数据。
import { SnackSetting } from '@snack-kit/core';
export class MyModuleSetting extends SnackSetting {
public $component() {
return (
<input
value={String(this.main.data?.value ?? '')}
onChange={(e) => this.$setData('value', e.target.value)}
/>
);
}
}表达式系统
页面 JSON 配置中支持 {{$.xxx}} 占位符,在运行时由 SDK 自动替换:
{
"data": {
"M": {
"label": "{{$.argv.username}}"
}
}
}可用变量
| 变量 | 说明 |
|------|------|
| $.argv | SDK 构造时传入的 argv 全局变量 |
| $.data | 当前组件的 data 对象 |
| $.sdk | SDK 实例 |
模块打包(UMD 格式)
Snack 模块需打包为 UMD 格式并通过 snackdefine 注册(由 scripts/config/webpack.snack.config.js 提供):
// webpack.snack.config.js 关键配置
output: {
filename: 'index.js',
library: { type: 'umd' }
},
externals: {
'react': 'react',
'@snack-kit/lib': '@snack-kit/lib'
}打包产物头部会自动注入:
if (typeof window !== 'undefined' && window.snackdefine) {
var define = window.snackdefine;
}开发
# 本地开发调试(webpack-dev-server)
npm run dev
# 构建库(ESM + CJS + Types)
npm run build
# 运行单元测试
npm test
# 类型检查
npm run lint
# 生成 API 文档
npm run docsChangelog
0.4.0
Bug 修复
- 修复 tsup ESM 构建产物扩展名问题:新增
outExtension: () => ({ js: '.js' })配置,强制输出dist/es/index.js,与package.json的exports.import路径一致,解决 Vite 等构建工具无法解析@snack-kit/core入口("Failed to resolve entry")的问题
0.2.0
新增
- 新增
src/utils/reactAdapter.ts:createReactRoot/SnackReactRoot运行时兼容层,支持 React 17 / 18 / 19 三版本并存- React 18+:使用
createRoot(react-dom/client) - React 17:回退至旧版
ReactDOM.render/unmountComponentAtNode - 通过变量形式 require,绕过 webpack 静态分析,避免 React 17 宿主环境因
react-dom/client不存在报 "Module not found"
- React 18+:使用
- 导出
createReactRoot与SnackReactRoot供外部使用
依赖调整
react/react-dom由dependencies移至peerDependencies,支持版本范围>=17.0.0,宿主项目可自行管理 React 版本
0.1.0
破坏性变更
- 移除 Vue 2.0 支持(删除
SnackVue、SnackVueSetting及相关导出) - 依赖由
@paraview/lib迁移至@snack-kit/lib(@paraview/lib作为 importMaps 别名保持向后兼容) - 构建工具由 webpack 迁移至 tsup,产物格式调整为 ESM + CJS + Types
安全修复
- 移除所有
new Function动态字段访问,改用safeGet/safeSet路径工具(消除代码注入风险) window.define全局污染修复:改用window.snackdefine,不再覆盖标准 AMDdefine
Bug 修复
- 修复
$defaultDisplay(false)逻辑反转问题,调用后组件现可正确隐藏 - 修复模块渲染失败时
childCount未递减,导致onComplete永不触发的问题 - 修复渲染错误日志缺少模块 key 信息,现输出
key/field辅助定位
新增
- 新增
src/utils/pathAccess.ts:safeGet/safeSet支持.与[]混合路径语法 - 并发加载去重:同一 URL 的模块并发请求只发出一次网络请求
destroy()方法现会清理所有待处理的verifyTimeout防抖计时器,防止内存泄漏- 新增 vitest 单元测试(
tests/utils/pathAccess.test.ts) - 新增 typedoc 支持,
npm run docs生成 API 文档
类型系统
- 全局类型安全审查,大幅减少
any使用 - 新增导出类型:
SnackRequireFunction、SnackDefineFunction、SnackSettingModelItem、ExprArgv - 支持 React 19
License
ISC
