自定义数据容器与控件
[基础]当原有控件或容器无法满足当前业务模块或者功能,自定义控件可以提供字段级别的控件自定义,自定义容器可以处理容器级别的自定义。Trantor会开放对应的 api 来支持自定义控件与容器的开发。
[模块]Trantor 自定义控件每一个工程可以划分为一个前端模块,或者附带于后端模块上,模块与模块之间互不影响。
一、准备工作(环境搭建与t-tools)
1、 切换 npm 源
npm config set registry https://registry.npm.terminus.io2、查看所有版本
npm view @terminus/t-tools versions3、全局安装 t-tools
npm i @terminus/t-tools -gOR
yarn add @terminus/t-tools -g4、初始化项目
# 进入后端项目工程跟目录t-tools init
cd frontend会出现 this 是否初始化示例。如果想初始化一个 容器,和控件。请选择 Y,否则选择 N。
? 是否初始化示例? › (Y/n)
注意:默认会再当前执行命令下创建 frontend 目录,默认打包到 frontend/dist 目录下,后续可以对此目录进行修改。
默认初始化配置
以下是默认初始化的配置:
项目名称 -> frontend
项目目录 -> frontend
打包路径 -> ./dist
开发者名 -> anonymous
自定义初始化配置 如果需要自定义以上默认配置使用
t-tools init -c# ORt-tools init --custom
cd frontend5、新建控件
注意:必须在前端项目目录(frontend)下执行以下命令
t-tools create如果是容器需要选择或填写的内容: 容器类型, 容器名称, 模型名称, 说明;
如果是控件需要选择或填写的内容: 组件类型, 组件名称, 名称, 说明;
6、打包组件
注意:必须在前端项目目录(frontend)下执行以下命令
t-tools build打包命令参数
-s, --server 启动服务 (default: false) # 启动服务,可用于调试-m, --source-map 是否启用 source map (default: false) # 是否有 soure map,用户调试比较方便-w, --watch 是否启用监听 (default: false) # 是否实时监听文件变化进行打包-d, --development 模式 (default: false) # 是否是dev 模式,dev模式不对文件进行压缩, 不会实际生成文件, 文件会生成到内存中, 方便线上调试。t-tools dev
相当于把所有 build 参数都开启, dev 是留给线上调试用的,请参照线上调试开发。不会真正生成文件,但是可以使用 URL 访问文件,如果需要生成文件,请参照 build 命令。
容器或控件列表
当开发者使用t-tools dev 或者使用 t-tools build -s会打开浏览器出现以下列表 方便开发者核对容器或者控件信息。

7、删除容器或控件
注意:必须在前端项目目录(frontend)下执行以下命令
t-tools remove
会显示容器组件列表,可多选删除
8、setting.js
const { dynamicExternals } = require("@terminus/t-tools-externals-lazy-pkgs");
module.exports = { // 打包存放目录 dist: "./dist",
// 开发配置 dev: { port: 8011, browser: true, },
// 是否是 trantor 项目 isTrantor: true,
// 依赖 externals: [ // 内部依赖 "@terminus/nusi", "@terminus/nusi-engine", "@terminus/nusi-components", "react", "classnames", "lodash", "mobx", "moment", "react-dom", "superagent", "superagent-use", ],
dynamicExternals,
// 本地扩展查找路径 false 不查找任何 node_module (禁止引入任自定义何第三方组件), 此配置如果设置为 true 则允许查找本地 node_modules 目录下文件进行打包 // 但是不要轻易使用 node_modules=true,这样会导致整个包越来越大,除非真的需要第三方依赖。 node_modules: false,
// 打包工具 webpack | rollup tool: "webpack",};二、自定义控件目录结构
.├── manifest.json # 自动生成组件描述文件,可手动修改描述等问题,结构禁止手动修改。├── package.json # 依赖文件├── redeme.md # README 文件├── setting.js # 配置文件,有相关端口,打包目录配置├── src # 源码文件,所有需要编译的文件都应该放在此文件目录下面│ ├── assets # 资源文件 图片等资源。│ │ └── logo.svg│ ├── components # 组件文件夹│ │ ├── Menus│ │ │ ├── index.less│ │ │ └── index.tsx│ │ └── Tabs│ │ ├── input.tsx│ │ └── show.tsx│ ├── style # 样式文件,less 禁止使用 通用样式,只允许使用函数,变量。原因:会导致整体打包变大│ │ └── index.less│ └── typings.d.ts # 全局类型文件└── tsconfig.json # TS 配置文件,如果需要更多ts功能支持请配置此文件三、自定义控件写法约束
1、依赖限制
- 🔞 不要引用
externals以外的依赖@terminus/nusi@terminus/nusi-engine@terminus/nusi-componentsreactclassnameslodashmobxmomentreact-dom。 - 🔞 不要再组件内部引用
externals子文件,只允许整体引用,子文件引用会导致这个文件重复打包。 - 🔞 除了类型声明可以引用子文件夹以外,变量声明禁止引用子文件。
2、公共 less 文件限制
- 🔞 不要在公共的 less 文件内使用生成样式的语法表述,尽量只使用
VariablesMixins。
3、样式问题使用 OR 无法生效问题
- 由于自定义组件起名可能会冲突,强制了使用 css module 请参照 css module 或者使用 classnames/bind
四、自定义控件与容器的实践
自定义控件
当Trantor现有的控件满足不了现有的业务场景,我们可以使用自定义控件去满足当前场景。
控件分为只读控件和写入控件,一般去自定义控件的时候,我们都会去提供这两种类型的控件(看实际业务场景),如果你的业务只需要展示性控件,可以只提供只读控件,不需要写自定义写入组件, 反之 …
-
首先提供的自定义控件只读控件一定包含props的value值,写入控件类型一定包含props的onChange事件
value: 当前控件的基础值,如果是可写控件可做默认值填充,只读控件就当当前值填充
onChange: onChange事件监听当前value值并向下抛出当前控件最新的值
value和onChange传值表现可直接理解为React父子组件之间传值的表现
-
props:
// 字段配置信息import { IField } from '@terminus/nusi-engine'// 显示状态(show)组件的Propsinterface IShowProps<T=any> {field: IFieldvalue: T // 基础value值, 只读控件的话会直接显示context: anypage: IPageInstance // nusi-engine的页面实例}// 编辑状态(input)组件的Propsinterface IInputProps<T=any> extends IShowProps{id: stringsize: 'large' | 'small' | 'default'onChange(value: T): void_registerCallback: (validateCheck: (errs: any, values: any) => {}, itemProps: { help: string }) => {}}注
- IField 基础field类型,其中会包含一些对Field的基础描述字段,比如type, tag, isNullable, show等,详见
本文末尾附录 IField - props.context 为全量数据,在 Table 或者 TableForm 中为单行数据。
- props.context 为了处理form联动,或者字段判断渲染。
- context._setValues({[FieldName: string]: value}) 方法,可以设置表单的其他字段。
- context 是一个 readonly 不可以直接修改值
- context._setValues() 只能在 form 里面使用
- props.page 是nusi-engine的页面实例,若组件中需要调用metaService,service,跳转视图,当前所在页面信息等,都可以在page实例中找到方法,详见
本文末尾附录 IPageInstance
- IField 基础field类型,其中会包含一些对Field的基础描述字段,比如type, tag, isNullable, show等,详见
自定义容器
如果当前的Trantor容器满足当前的业务需求,我们可以自定义容器来满足当前的业务需求
- 精简的Props
interface IContainerProps { id?: string model: string // 模型 title?: string // 容器标题 fields?: IFields // fields 配置 actions?: IActions // 容器自定义操作}-
容器类型
- 布局容器 自定义布局容器无需继承,按照需求实现自定义布局
- 数据容器
-
从016版本开始,Trantor支持function/class组件类型的自定义数据容器。
-
class的容器组件可以继承nusi-engine提供的继承基础容器提供的能力,比如基础的请求模型,刷新容器,初始化容器能力。
-
数据容器类型
容器使用的mobx进行的状态管理,数据容器必须继承以下一个基类
- ItemContainer 继承自DataContainer,DataType为’Single’ 使用场景:单条详情、单个编辑,比如Trantor当前提供的Detail,Form,Record数据容器
- ListContainer 继承自DataContainer,DataType为’List’ 使用场景:多条/多个, 比如Trantor当前提供的Table, TableForm数据容器
- TreeContainer 继承自 BaseContainer, DataType为’List’ 使用场景: 自关联模型展示, 比如Trantor当前提供的CascadeList数据容器
- EmptyContainer 空容器,不会运行默认的数据加载机制
-
数据容器相关接口
- BaseContainer
interface BaseContainer {// 如果数据容器需要包裹高阶组件,可以实现这个方法static exportComponent?: (component: React.Component) => React.Componentdata: IDictionary | IDictionary[] // 获取模型数据config: ParseConfigs // 获取当前数据容器 DSL 配置fields: IField[] // 获取当前数据容器字段配置actions: IAction[] // 获取当前数据容器 Action 配置lookupContext?: { // 当 lookup 的数据容器和主数据容器一起提交时需要配置data: any || ()=> any, // 当前数据容器需要提交的数据,默认为 this.datavalidate?: async ()=> boolean // 随其他数据容器一起提交时的校验方法}getActionParam: (data: any, action: Action) => { // 获取执行 Action 的上下文,可覆盖record: IDictionary | IDictionary[], // 覆盖后可修改 Action 执行的上下文}}- DataContainer
interface DataContainer extends BaseContainer {extraFields: IField[] // 需要额外获取除配置之外的字段// dataProcessor 取代了旧版的 fetchDataEnd ,并且需要将处理后的数据做返回dataProcessor: (result: {singleResult: boolean // 是否单结果data: IDictionary | IDictionary[] // 单结果为 map ,多结果为 list<map>count?: number // 本次查询总条数,用于分页}) => IDictionary | IDictionary[] // 需要将处理后的数据返回}- ListContainer 多条数据数据容器父类
interface ListContainer extends DataContainer {}- ItemContainer 单条数据数据容器父类
interface ItemContainer extends DataContainer {}- TreeContainer 自关联数据容器父类
interface TreeContainer extends BaseContainer {getNodes: () => ITreeNode[] // 获取所有的 tree 数据reverseTree: (parentId: string) // 根据 parentId 反向构建树fetchNodes: (parentId: string, fetchCondition?: string) // parentId 和 fetchCondition 获取节点数据}
五、前端模块部署 Deploy
注意:t-tools 再 0.1.4 版本后支持,t-tools 分为两个版本,一个是自有版本,再 Trantor 0.13.0 后 将转向 t-tools 1.13.0 版本
Trantor < 0.13.0 && t-tools 大于 0.1.4 小于 0.1.6
// deploy.js 配置module.exports = { // 配置 options: { NAME: 'Trantor前端资源模块', // 模块名称 META_STORE_URL: '', // 例如: https://terminuss.io/t-project/trantor-deployment/meta-store 需要根据项目来写 MODULE_KEY: 'frontend', // 这个可以自定义前端资源key,第一次定完后,不在可以变更 VERSION: '0.0.1-SNAPSHOT', // 每次发版需要更换版本,需要走模块发布流程 },
// 发布前操作 Hook async before(){
},
// 发布后操作 Hook async after(){
}}Trantor >= 0.13.0 && t-tools 大于 1.13.0
// deploy.js 配置module.exports = { // 配置 options: { NAME: 'Trantor前端资源模块', DEPLOY_URL: '', // 例如: https://test-gateway.app.terminus.io/t-project/trantor-deployment/meta-store/${tenantKey}/${projectKey} MODULE_KEY: 'custom_frontend', // 模块名称: 自定义名称 VERSION: '0.0.2-SNAPSHOT', // 模块版本: 自定义版本 PRODUCT: { KEY: 'test', // 与 projectKey 相同 VERSION: '3' // 目前定制项目的版本是固定的, 都是 0.0.0-SNAPSHOT }, },
// 发布前操作 Hook async before(){
},
// 发布后操作 Hook async after(){
}}六、自定义 webpack配置(1.5.x以上版本支持)
使用 setting.webpackConfig 配置,值为:false | object | function
// ObjectwebpackConfig = { // 这里是 webpack 完整的配置使用 webpack-merge 与以生成的webpack配置进行合并}
// FunctionwebpackConfig = function(webpackConfig, webpackMerge, options){ // webpackConfig t-tools 生成的webpack配置 // webpackMerge webpack-merge 模块包的功能本体 // options 是setting.js 配置内容
// return 最终实际使用的webpack配置 return webpackConfig}七、本地调试
1.启动
cd [自定义组件项目目录]npm run dev2.在线调试
- DSL 中引入需要调试的数据容器和控件
- 在需要调试的页面 url 后面加上 _debug 参数, 例如: http://xxx.com?aaa=bbb&_debug
例子-在线调试
<View title="用户"> <Definition> <Containers> <!-- 引入需要调试的数据容器 --> <Container key="MyContainer" url="//127.0.0.1/MyContainer.js" /> </Containers> <Widgets> <!-- 引入需要调试的控件 --> <Widget key="MyWidget" url="//127.0.0.0.1/MyWidget.js" /> </Widgets> </Definition> <!-- 使用调试容器 --> <MyContainer /></View>控件使用方式
// 与字段同级<RenderType> <custom_InputNumberHasUnit unit="productionUnit"/></RenderType><Field renderType='custom_InputNumberHasUnit' />自定义组件中调用 LogicFlow/LogicFunction/getDataSource/OpenView
// 旧版 serverAction viewActionimport { triggerServerAction, triggerViewAction } from '@terminus/nusi-engine'const result = await triggerServerAction('serverActionKey', actionContext)triggerViewAction('viewActionKey', params)
// 新版 logicFlow 和 logicFunction// data入参是实际提交的数据内容,这里不再需要modelKey,因为func和flow是属于具体模型的资源。// 在容器内部通过this调用export class CustomContainer extends ItemContainer { public method () { const result = await this.triggerLogicFlow('logicFlowKey', data) const result = await this.triggerLogicFunction('logicFlowFunction', data) // this.getDataSource的入参格式见下方 const result = await this.getDataSource(dataSourceType, params) this.openView('viewKey', params) }}
// 在自定义组件中通过props.page.service/调用export default (props) => { const { service, openView } = props.page const result = await service.triggerLogicFlow('logicFlowKey', data) const result = await service.triggerLogicFunction('logicFlowFunction', data) // this.getDataSource的入参格式见下方 const result = await service.getDataSource(dataSourceType, params) const openView('viewKey', params) ...}
// 深层次组件调用,engine提供了PageContextimport { PageContext } from '@terminus/nusi-engine'import React, { useContext } from 'react'
export default (props) => { const { service, openView } = useContext(PageContext) const result = await service.triggerLogicFlow('logicFlowKey', data) const result = await service.triggerLogicFunction('logicFlowFunction', data) // this.getDataSource的入参格式见下方 const result = await service.getDataSource(dataSourceType, params) const openView('viewKey', params)}
// 业务可视需要将service部分集中做成hooks,方便其他组件调用export const useTrantorService = () => { const { service, openView } = useContext(PageContext) ...}
// openView参数定义type openView = (viewKey: string, params: { openViewType?: 'Self' | 'Dialog' | 'Drawer' | 'Columns'; openViewSize?: 's' | 'm' | 'l'; payloadCallback?: (ctx: IActionContext) => void; context?: { modelKey: string; record: IDictionary | IDictionary[]; }; pageState?: IDictionary; env?: IDictionary; }) => void;
// getDataSource方法入参// 第一个参数dataSourceType类型export enum DataSourceType { DataStore = 'DataStore', Flow = 'Flow', Function = 'Function',}
export interface IDataSourceBaseParams { modelKey: string searchModelKey?: string fields: string[] // # page?: { no: number size: number } order?: { asc?: boolean field: string } fuzzyValue?: string}
// 第二个参数params(当第一个参数类型为func、flow)export interface ILogicDataParams extends IDataSourceBaseParams { dataSourceKey: string params?: IDictionary}
// 第二个参数params(当第一个参数类型为dataStore)export interface IDataStoreParams extends IDataSourceBaseParams { singleResult?: boolean // 期望返回值是否为单个record,默认为false search?: IDictionary searchValue?: IDictionary<ISearchValue> condition?: { expression: string params?: any[] } queryParams?: ILoadDataQuery}
export interface ISearchValue { value: any range?: boolean fullMatch?: boolean operator?: IFieldOperator}
export interface ILoadDataQuery { postFlow?: string postFunc?: string}获取附件的 OSS 访问地址
import { getOssAccessUrl } from '@terminus/nusi-engine';const url = await getOssAccessUrl('//ali-oss.com/xxx.jpg');八、017自定义容器接口调整🔔
目前兼容原有的Class 继承的方式书写自定义容器(原有的ItemContainer, ListContainer, TreeContainer, EmptyContainer),但有容器中的部分方法有差异, 如果在自定义容器中使用到这些方法,则需要修改。
| 容器方法 | 新版变更 | 原因 | 场景 |
|---|---|---|---|
| fetchDataEnd(result, actionConfig) => void | dataProcessor(data) => data | 旧版中由fetchDataEnd对获取的数据进行处理,然后直接对容器this.data进行设置。v2中容器的数据实际上由engine进行管理,所以这里改为processor,处理完之后需要将处理的数据进行返回。 | 业务中有可能使用到fetchDataEnd来触发获取数据后的逻辑 |
| getActionContext | getActionParam | Action数据Getter,在新版中去掉了context的概念,方法名改为getActionParam。在返回的结构中也有一个context属性(代表具体的数据),为避免混淆,此属性也更名为record。 | 在自定义容器中可以定义Actions执行时可以获取到的数据 |
其他
- 默认生成的是模板性质的代码,作为初始化后的初期约束
- 当不能满足调试需求时可以手动进行修改
- 按照各个文件的注释信息进行操作