跳转到内容

自定义数据容器与控件

[基础]当原有控件或容器无法满足当前业务模块或者功能,自定义控件可以提供字段级别的控件自定义,自定义容器可以处理容器级别的自定义。Trantor会开放对应的 api 来支持自定义控件与容器的开发。
[模块]Trantor 自定义控件每一个工程可以划分为一个前端模块,或者附带于后端模块上,模块与模块之间互不影响。

一、准备工作(环境搭建与t-tools)

1、 切换 npm 源

Terminal window
npm config set registry https://registry.npm.terminus.io

2、查看所有版本

Terminal window
npm view @terminus/t-tools versions

3、全局安装 t-tools

Terminal window
npm i @terminus/t-tools -g

OR

Terminal window
yarn add @terminus/t-tools -g

4、初始化项目

Terminal window
# 进入后端项目工程跟目录
t-tools init
cd frontend

会出现 this 是否初始化示例。如果想初始化一个 容器,和控件。请选择 Y,否则选择 N。 ? 是否初始化示例? › (Y/n) 注意:默认会再当前执行命令下创建 frontend 目录,默认打包到 frontend/dist 目录下,后续可以对此目录进行修改。

默认初始化配置

以下是默认初始化的配置: 项目名称 -> frontend 项目目录 -> frontend 打包路径 -> ./dist 开发者名 -> anonymous

自定义初始化配置 如果需要自定义以上默认配置使用

Terminal window
t-tools init -c
# OR
t-tools init --custom
cd frontend

5、新建控件


注意:必须在前端项目目录(frontend)下执行以下命令

Terminal window
t-tools create

如果是容器需要选择或填写的内容:  容器类型,  容器名称,  模型名称, 说明;
如果是控件需要选择或填写的内容: 组件类型, 组件名称, 名称, 说明;

6、打包组件

注意:必须在前端项目目录(frontend)下执行以下命令

Terminal window
t-tools build

打包命令参数

Terminal window
-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会打开浏览器出现以下列表 方便开发者核对容器或者控件信息。

image0.png

7、删除容器或控件

注意:必须在前端项目目录(frontend)下执行以下命令

Terminal window
t-tools remove


会显示容器组件列表,可多选删除

image1.png

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",
};

二、自定义控件目录结构

Terminal window
.
├── 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-components react classnames lodash mobx moment react-dom
  • 🔞 不要再组件内部引用 externals 子文件,只允许整体引用,子文件引用会导致这个文件重复打包。
  • 🔞 除了类型声明可以引用子文件夹以外,变量声明禁止引用子文件。

2、公共 less 文件限制

  • 🔞 不要在公共的 less 文件内使用生成样式的语法表述,尽量只使用  Variables Mixins

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)组件的Props
    interface IShowProps<T=any> {
    field: IField
    value: T // 基础value值, 只读控件的话会直接显示
    context: any
    page: IPageInstance // nusi-engine的页面实例
    }
    // 编辑状态(input)组件的Props
    interface IInputProps<T=any> extends IShowProps{
    id: string
    size: 'large' | 'small' | 'default'
    onChange(value: T): void
    _registerCallback: (validateCheck: (errs: any, values: any) => {}, itemProps: { help: string }) => {}
    }

    1. IField 基础field类型,其中会包含一些对Field的基础描述字段,比如type, tag, isNullable, show等,详见本文末尾附录 IField
    2. props.context 为全量数据,在 Table 或者 TableForm 中为单行数据。
    3. props.context 为了处理form联动,或者字段判断渲染。
    4. context._setValues({[FieldName: string]: value}) 方法,可以设置表单的其他字段。
    5. context 是一个 readonly 不可以直接修改值
    6. context._setValues() 只能在 form 里面使用
    7. props.page 是nusi-engine的页面实例,若组件中需要调用metaService,service,跳转视图,当前所在页面信息等,都可以在page实例中找到方法,详见本文末尾附录 IPageInstance
  • 点击查看具体Demo

自定义容器

如果当前的Trantor容器满足当前的业务需求,我们可以自定义容器来满足当前的业务需求

  • 精简的Props
interface IContainerProps {
id?: string
model: string // 模型
title?: string // 容器标题
fields?: IFields // fields 配置
actions?: IActions // 容器自定义操作
}
  • 容器类型

    1. 布局容器 自定义布局容器无需继承,按照需求实现自定义布局
    2. 数据容器
    • 从016版本开始,Trantor支持function/class组件类型的自定义数据容器。

    • class的容器组件可以继承nusi-engine提供的继承基础容器提供的能力,比如基础的请求模型,刷新容器,初始化容器能力。

    • 数据容器类型

      容器使用的mobx进行的状态管理,数据容器必须继承以下一个基类

      1. ItemContainer 继承自DataContainer,DataType为’Single’ 使用场景:单条详情、单个编辑,比如Trantor当前提供的Detail,Form,Record数据容器
      2. ListContainer 继承自DataContainer,DataType为’List’ 使用场景:多条/多个, 比如Trantor当前提供的Table, TableForm数据容器
      3. TreeContainer 继承自 BaseContainer, DataType为’List’ 使用场景: 自关联模型展示, 比如Trantor当前提供的CascadeList数据容器
      4. EmptyContainer 空容器,不会运行默认的数据加载机制
  • 数据容器相关接口

    1. BaseContainer
    interface BaseContainer {
    // 如果数据容器需要包裹高阶组件,可以实现这个方法
    static exportComponent?: (component: React.Component) => React.Component
    data: IDictionary | IDictionary[] // 获取模型数据
    config: ParseConfigs // 获取当前数据容器 DSL 配置
    fields: IField[] // 获取当前数据容器字段配置
    actions: IAction[] // 获取当前数据容器 Action 配置
    lookupContext?: { // 当 lookup 的数据容器和主数据容器一起提交时需要配置
    data: any || ()=> any, // 当前数据容器需要提交的数据,默认为 this.data
    validate?: async ()=> boolean // 随其他数据容器一起提交时的校验方法
    }
    getActionParam: (data: any, action: Action) => { // 获取执行 Action 的上下文,可覆盖
    record: IDictionary | IDictionary[], // 覆盖后可修改 Action 执行的上下文
    }
    }
    1. DataContainer
    interface DataContainer extends BaseContainer {
    extraFields: IField[] // 需要额外获取除配置之外的字段
    // dataProcessor 取代了旧版的 fetchDataEnd ,并且需要将处理后的数据做返回
    dataProcessor: (result: {
    singleResult: boolean // 是否单结果
    data: IDictionary | IDictionary[] // 单结果为 map ,多结果为 list<map>
    count?: number // 本次查询总条数,用于分页
    }) => IDictionary | IDictionary[] // 需要将处理后的数据返回
    }
    1. ListContainer 多条数据数据容器父类
    interface ListContainer extends DataContainer {}
    1. ItemContainer 单条数据数据容器父类
    interface ItemContainer extends DataContainer {}
    1. TreeContainer 自关联数据容器父类
    interface TreeContainer extends BaseContainer {
    getNodes: () => ITreeNode[] // 获取所有的 tree 数据
    reverseTree: (parentId: string) // 根据 parentId 反向构建树
    fetchNodes: (parentId: string, fetchCondition?: string) // parentId 和 fetchCondition 获取节点数据
    }
  • 点击查看具体Demo

五、前端模块部署 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

// Object
webpackConfig = {
// 这里是 webpack 完整的配置使用 webpack-merge 与以生成的webpack配置进行合并
}
// Function
webpackConfig = function(webpackConfig, webpackMerge, options){
// webpackConfig t-tools 生成的webpack配置
// webpackMerge webpack-merge 模块包的功能本体
// options 是setting.js 配置内容
// return 最终实际使用的webpack配置
return webpackConfig
}

七、本地调试

1.启动

Terminal window
cd [自定义组件项目目录]
npm run dev

2.在线调试

  • 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 viewAction
import { 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提供了PageContext
import { 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) => voiddataProcessor(data) => data旧版中由fetchDataEnd对获取的数据进行处理,然后直接对容器this.data进行设置。v2中容器的数据实际上由engine进行管理,所以这里改为processor,处理完之后需要将处理的数据进行返回。业务中有可能使用到fetchDataEnd来触发获取数据后的逻辑
getActionContextgetActionParamAction数据Getter,在新版中去掉了context的概念,方法名改为getActionParam。在返回的结构中也有一个context属性(代表具体的数据),为避免混淆,此属性也更名为record。在自定义容器中可以定义Actions执行时可以获取到的数据

其他

  • 默认生成的是模板性质的代码,作为初始化后的初期约束
  • 当不能满足调试需求时可以手动进行修改
  • 按照各个文件的注释信息进行操作

相关例子