@Wahson
2017-12-15T07:18:53.000000Z
字数 21119
阅读 1574
前端技术
- 是什么?—— MetaUI是一个基于元数据的前端框架,它是在Polymer2.0之上开发, 能够通过配置元数据,快速开发前端页面。
- 为什么?—— 曾经无数次给公司的运营系统开发不同的数据页面。但是这些界面大部分无外乎一个查询条件栏,接着是查询和重置条件的按钮,接下来是查询结果(以表格显示),然后每一行数据都会定义有不同的操作按钮,对应不同的操作,点击按钮,要么是弹窗对数据进行编辑保存操作,要么直接提交表单。来来回回都是做这些的重复性的工作,更重要是耗时。那么为什么不能设计出一个框架,通过配置元数据的形式,自动就把这种类型的页面自动完成呢?然后把剩下的时间放在差异点的实现上。甚至如果使用足够简单的话,即使是非开发人员也能通过培训,迅速搭建出界面原型,岂不很有意思?
- 怎么样?—— 且继续往下看。
bower install metaui
或者
bower install git://github.com/isuwang/metaui.git
MetaUI是一个基于Polymer的框架,所以你读到这里的时候,我已经默许你了解过Polymer,并且也知道
polymer-cli
如何使用。
那么接下来,我们通过polymer-cli脚手架快速搭建一个polymer工程,并开始使用MetaUI。
metaui-demo
mkdir metaui-demo && cd metaui-demo
polymer init
选择polymer-2-application
选项,然后按照提示输入。
如图:
bower install --save git://github.com/isuwang/metaui.git
polymer serve
然后复制输出的url到浏览器打开,我这里的url是http://127.0.0.1:8081/components/metaui-demo/
。
如图:
你的浏览器中能看到Hello metaui-demo!
吗?看不到的话,别问我为什么。
vi src/metaui-demo/metaui-demo.html
引入metaui中的m2-crud组件
<link rel="import" href="../../bower_components/metaui/m2-crud.html">
在<template>
中声明<m2-crud>
<m2-crud metadata="{{metadata}}"></m2-crud>
初始化metadata属性
static get properties() {
return {
metadata: {
type: Object,
value: {
query: {
formUrl: "/query.do",
fields: [ { name: "username", label: "用户名", editable: true, visible: true } ]
},
result: {
grids: [
{ name: "username", label: "用户名" },
{ name: "phone", label: "联系电话" },
{ name: "address", label: "联系地址" },
{ metaType: {type: "ACTION"}, name: "actions", label: "操作" }
]
}
}
}
};
}
刷新一下网页,duang! duang! duang!
Opps!看到了错误警告(我开始慌了~,是不是我又~做错了什么?)--不要慌,弹出层的错误提示暂时无需搭理,因为组件加载后会自动根据metadata中配置的
query.formUrl
进行查询,但是你懂的,我们还没有提供接口服务支持。
先来两张图来窥探一下MetaUI的结构:
主页:
弹窗:
MetaUI的结构还是比较简单的,看蓝字上可以找到每个元数据配置对应的界面元素。
MetaUI中提供了
<m2-crud>
、<m2-grid>
、<m2-crud-query>
、<m2-action>
、<m2-form>
、<m2-field>
6个基础元数据组件。
<m2-crud>
=> DomainMeta/CrudMetaDemo & API Doc
Simple demo:
<m2-crud metadata="{{metadata}}"></m2-crud>
metadata = {
query: {
formUrl: "/query.do",
fields: [
{
name: "username",
label: "用户名",
editable: true,
visible: true
}
]
},
actions: {
"record-edit": {
actionId: "record-edit",
actionName: "Edit",
operType: 1,
formMeta: {
formUrl: "/url-to-send-form",
confirm: "Sure to submit?",
fields: [
{
name: "name",
label: "Name"
},
{
name: "phone",
label: "Phone"
},
],
}
}
},
result: {
grids: [
{
name: "username",
label: "用户名"
},
{
name: "phone",
label: "联系电话"
}
]
}
}
字段名 | 类型 | Required | 描述 |
---|---|---|---|
query | FormMeta | 是 | 表格数据请求表单的元数据 |
actions | array.<ActionMeta> |
是 | 针对domain上的操作元数据 |
result | GridMeta | 是 | 表格的元数据 |
<m2-grid>
=> GridMeta
<m2-grid metadata="{{metadata}}" value="{{value}}"></m2-crud>
metadata = {
title: "I am the title!",
grids: [
{ name: "name", label: "Name" },
{ name: "phone", label: "Phone" },
{ metaType: {type: "ACTION"}, name: "actions", label: "Actions", }
],
actions: {
"action-1": {
actionId: "action-1",
actionName: "Action1",
group:"GROUP",
operType: 1,
formMeta: {
formUrl: "/url-to-send-form",
confirm: "Sure to submit?",
fields: [
{ name: "name", label: "Name" },
{ name: "phone", label: "Phone" },
]
}
},
"action-2": {
actionId: "action-2",
actionName: "Action2",
group:"GROUP",
operType: 2,
formMeta: {
title: "Dialog Title",
formUrl: "/url-to-send-form",
confirm: "Sure to submit?",
fields: [
{ name: "name", label: "Name", visible: true, editable: true },
{ name: "phone", label: "Phone", visible: true, editable: true },
]
}
}
},
style: `
.grid-column {font-size: 16px;}
`
};
value = [
{
name: "foo",
phone: "10086",
actions: ["action-1", "action-2"]
},
{
name: "foo1",
phone: "10086",
actions: ["action-1"]
}
];
字段名 | 类型 | Required | 描述 | 描述 | 描述 | 描述 |
---|---|---|---|---|---|---|
grids | array.<FieldMeta> |
是 | 表格sdfsdfsdfsdfsfsdfsdfsdfsdfsdfsdfsdfsdfsdfsdfsdfsdfsfsdfsdfsdfsdfsdfsdfsfdsfssdf单元格元数据 | :-- | :-- | |
actions | map.<string, ActionMeta> |
是 | 操作集 | :-- | :-- | |
title | string | 否 | 表格标题 | :-- | :-- | |
editable | boolean | 否 | 是否可编辑 | :-- | :-- |
<m2-action>
=> ActionMetaDemo & API Doc
Simple demo:
<m2-action metadata="{{metadata}}" context="{{context}}"></m2-action>
metadata = {
actionId: "record-add",
actionName: "添加记录",
operType: 1,
formMeta: {
formUrl: "/url-to-send-form",
confirm: "Sure to submit?",
fields: [
{
name: "name",
label:"Name"
},
{
name: "phone",
label:"Phone"
}
]
}
};
context = {
name: "foo",
phone: "10086"
}
字段名 | 类型 | Required | 描述 |
---|---|---|---|
actionId | string | 是 | 操作的唯一标识 |
actionName | string | 是 | 操作名,如 “收货” |
group | string | 否 | 操作组,如 “更多” |
operType | number | 是 | 操作类型,1:表单,2.弹窗 |
formMeta | FormMeta | 是 | 表单meta |
modelSrc | string | 否 | 弹窗的model请求url |
modelQueryParams | array.<string> |
否 | 弹窗的model请求参数字段名,如 ["id", "name"] |
customAction | Function | 否 | 自定义按钮动作,声明了该字段后,会忽略默认的按钮动作 |
<m2-form>
=> FormMetaDemo & API Doc
Simple demo:
<m2-form metadata="{{metadata}}" context="{{context}}"></m2-form>
metadata = {
formUrl: "/url-to-send-form",
title: "我是标题",
confirm: "Sure to submit?",
fields: [
{
name: "name",
label: "姓名",
required: true,
editable: true,
visible: true
},
{
name: "phone",
label: "电话",
required: true,
editable: true,
visible: true
}
],
style: `
.container {
grid-column-gap: 4%;
grid-template-columns: 48% 48%;
}
.container__item {
border: 1px dashed
}
`,
};
context = {
name: "foo",
phone: "10086"
}
字段名 | 类型 | Required | 描述 |
---|---|---|---|
title | string | 是 | 表单头 |
formUrl | string | 是 | 表单提交的url |
fields | array. | 是 | 字段元数据 |
confirm | string | 否 | 表单提交前确认提示 |
hideSubmitBtn | boolean | 否 | 是否禁用提交按钮 |
responseHandleAs | string | 否 | 请求返回实体处理:text,json,blob,arrayBuffer, formData |
sendAsSearchParams | boolean | 否 | 请求是否以 URLSearchParams 格式发送 |
style | string | 否 | 组件自定义样式 |
<m2-crud-query>
=> 阉割的FormMeta
<m2-crud-query metadata="{{metadata}}" query-after-reset></m2-crud-query>
metadata = {
formUrl: "/url-to-send-form",
fields: [
{
name: "name",
label: "姓名",
required: true,
editable: true,
visible: true
},
{
name: "phone",
label: "电话",
required: true,
editable: true,
visible: true
}
],
noPaging: true
}
字段名 | 类型 | Required | 描述 |
---|---|---|---|
fields | array. | 是 | 字段元数据 |
noPaging | boolean | 否 | 是否取消分页 |
<m2-field>
=> FieldMeta
<m2-field metadata="{{metadata}}" context="{{context}}"></m2-field>
metadata = {
name: "sex",
label: "Please select your sex: ",
element: "m2-select-list",
required: true,
candidates: [
{ label: "Male", value: "1" },
{ label: "Female", value: "2" }
],
candidateLabel: "label",
candidateValue: "value",
render: (model) => `<div style="color:blue">${model.name}</div>`
};
context = {
name: "foo",
phone: "10086",
sex: 1
}
字段名 | 类型 | Required | 描述 |
---|---|---|---|
name | string | 是 | 字段名,如“orderNo” |
label | string | 是 | 字段名,如“订单号”,缺省值:name字段的值 |
require | boolean | 否 | 是否必填,缺省值:false |
mutil | boolean | 否 | 是否多选,缺省值:false |
editable | boolean | 否 | 是否可编辑,缺省值:false |
validate | boolean | 否 | 是否需要校验,缺省值:false |
visible | boolean | 否 | 界面是否可见,仅对表单中的字段有效,缺省值:false |
disabled | boolean | 否 | 是否禁用组件,缺省值:false |
element | string | 否 | ui元素,缺省时,提供默认ui:m2-text |
format | string | 否 | 显示格式,用于日期显示,如“yyyy-MM-dd” |
length | number | 否 | 字符长度 |
maxLength | number | 否 | string允许的最大长度 |
minLength | number | 否 | string允许的最小长度 |
regexp | string | 否 | 正则匹配 |
prompt | string | 否 | 校验失败时的提示语,如“请输入备注” |
min | number | 否 | 数字最小值 |
max | number | 否 | 数字最大值 |
placeholder | string | 否 | placeholder |
candidates | string | 否 | 候选值,下拉选项中的值,json串,如 "[{value: 1,label: '男'}, {value: 2,label: '女'}]" |
candidateLabel | string | 否 | 组件显示字段名,针对下拉组件、radio-group组件等,“label” |
candidateValue | string | 否 | 组件值字段名,针对下拉组件、radio-group组件等,“value” |
srcKey | string | 否 | 当一个字段的值是来源于另外一个字段,则通过srcKey配置值来源字段的字段名,如 "id" |
metaType | MetaType或string | 否 | 元数据类型,如果缺少metaType,默认渲染的组件是;在表格元数据中定义操作列时,可以省略操作的元数据,如 metaType:"ACTION" See h2-grid |
render | Function(Closure) | 否 | 只读模式下会执行此闭包生成显示内容 |
MetaType定义
字段名 | 类型 | Required | 描述 |
---|---|---|---|
type | string | 是 | 数据类型:BOOLEAN、BYTE、SHORT、INTEGER、LONG、DOUBLE、STRING、LIST、MAP、SET、STRUCT、ACTION |
metadata | object | 是 | 如果type是list,map,set中的一种,则metadata是这些嵌套类型的元数据 |
<m2-form>
以及<m2-crud-query>
中提供了样式扩展能力,你可以通过配置metadata.style属性了传入自定义的样式。
例如:
metadata.style = `
.container {
grid-column-gap: 4%;
grid-template-columns: 48% 48%;
}
.container__item {
border: 1px dashed
}
`;
这些组件默认使用CSS Grid Layout布局,你可以根据需要在
.container
类选择器中修改。你可以通过选择器,对<m2-form>
和<m2-crud-query>
的shadow dom中的任何元素进行样式修改。
<m2-field>
组件在只读模式下,会查找metadata.render,如果该字段有值且是一个函数,组件内部会执行这个函数并传入组件的context
属性值作为参数,并把函数返回值作为显示内容,渲染到shadow dom中。render函数除了返回字符串,也可以返回一个HTMLElement。
例如:
// return string
metadata.render = ({name}) => `<div style="color:blue">${name}</div>`;
// return HTMLElement
metadata.render = ({id, taskNo}) => {
const aTag = document.createElement("a");
aTag.href = "javascript:void(0)";
aTag.onclick = (e) => {
// do something here
};
aTag.innerHTML = taskNo;
return aTag;
}
<m2-action>
默认的点击操作是弹窗或者提交表单,但是你可以metadata.customAction字段传入函数来覆盖默认的行为。customAction函数执行时会传入当前m2-action
中的context
属性值作为参数。
metadata.customAction = (context) => {
console.log(context.name);
}
m2-crud-query
组件中定义了一个slot插槽,允许你通过light dom的形式定义额外的查询条件。
<m2-crud-query metadata="{{metadata}}">
<div slot="ext-condition">
<o-status-select label="状态" on-status-selected="onStatusSelect"></o-status-select>
</div>
</m2-crud-query>
onStatusSelect(e) {
e.target.dispatchEvent(
new CustomEvent('query-domain',
{
// e.detail的值是组件中返回的状态值,比如 {status: 1}
detail: e.detail,
composed: true,
bubbles: true
}
)
);
}
<m2-crud-query>
内部监听了query-domain
事件,并且会把通过e.detail传递过来的字段作为请求参数的一部分,发起查询请求。另外需要注意dispatch事件的元素必须是绑定到slot中的元素(上例中可以是div[slot=ext-condition]
或o-status-select
)。
m2-field
组件只是一个壳,内部会首先判断metadata.element
是否指定了组件名,如果有,创建改组件,并添加到shadow dom 中,否则判断metadata.metaType的值:
switch (metaType) {
case "ACTION":
return "m2-action";
case "FORM":
return "m2-form";
case "GRID":
return "m2-grid";
default:
return "m2-text";
}
匹配不上时默认渲染出
<m2-text>
。
你可以定义你的组件,并在metadata.element中指定该组件名。
自定义组件时,你需要遵循以下规定:
1. 组件需要提供value属性,来指定组件的值。而且value属性需要声明notify: true
。
2. 组件需要提供validate()方法,返回一个布尔值,用来检测组件中的值是否合法。
3. 你可以直接继承 M2WidgetBase父类
(/bower_components/metaui/widgets/m2-widget-base.html, M2WidgetBase提供了value的定义以及validate()的默认实现。
参考例子:
<link rel="import" href="../bower_components/metaui/widgets/m2-widget-base.html">
<link rel="import" href="../bower_components/paper-input/paper-input.html">
<dom-module id="your-widget-name">
<template>
<paper-input
id="input"
always-float-label
value="{{ value }}"
label="[[ metadata.label ]]"
auto-validate
required="[[ metadata.required]]"
error-message="[[ metadata.prompt ]]"
min="[[ metadata.min ]]"
minlength="[[ metadata.minLength ]]"
max="[[ metadata.max ]]"
maxlength="[[ metadata.maxLength ]]"
pattern="[[ metadata.regexp ]]"
readonly="[[ !metadata.editable ]]">
</paper-input>
</template>
<script>
class YourWidget extends M2WidgetBase {
static get is() {
return "your-widget-name";
}
/**
* Before a form being submitted, validate() will be called automatically to check if the value is legal.
* You can override it if needed.
*/
validate() {
// return this.$.input.validate();
return true;
}
// more methods here
}
window.customElements.define(YourWidget.is, YourWidget);
</script>
</dom-module>
m2-field
中的context
属性值也会自动绑定到自定义组件内部。
在
m2-form
组件像是一个m2-field
容器,所有的m2-field
会共享m2-form
中context属性值,任何一个m2-field中的value发生变化,会同步到context属性上,而且在任何一个m2-field
中你可以监听另外一个m2-field
值的变化。看下面例子:
metadata = {
formUrl: "/url-to-send-form",
title: "Form Title",
confirm: "Sure to submit?",
fields: [
{
name: "bar",
label: "bar",
required: true,
editable: true,
visible: true
},
{
name: "foo",
label: "foo",
required: true,
editable: true,
visible: true,
onAttached: (fieldEle, formEle) => {
formEle.watch(['bar'], ({bar}, whoChange) => {
console.log(whoChange);
fieldEle.style = bar.length > 4 ?
'background:red;' : 'background:green;'
});
}
}
]
}
重点看看metadata.fields[1].onAttached
:
onAttached: (fieldEle, formEle) => {
/**
* ['bar'] 订阅bar字段的变化
* ({bar}, whoChange) => {...} 回调函数,入参是m2-field对应的context, 以及值发生变化的字段名。
*/
formEle.watch(['bar'], ({bar}, whoChange) => {
console.log(whoChange); // "bar"
fieldEle.style = bar.length > 4 ?
'background:red;' : 'background:green;'
});
}
这里在
foo
的字段上,订阅了bar
字段值的变化。当foo字段对应的m2-field
被依附到m2-form的shadow dom时,onAttached
函数会被调用来初始化订阅,通过m2-form内部的watch()
方法来完成订阅。bar
字段的值一旦变化,传入watch()
方法的回调就会执行。因此在此回调函数里面,你可以定义实现一些功能,比如根据bar值来调整foo字段所在的m2-field
的样式,等等。
isuwang-soa
各位看官已经见识到单独使用
MetaUI
的威力,但是随着开发的界面越来越多,很多重复性的配置难免会带来一些枯燥感。接下来,我们结合 isuwang-soa,江湖人称dapeng框架
,以及meta-constructor,带来更快速的开发体验。
isuwang-soa
是一个SOA框架,具体的使用不在这里介绍,请移步https://github.com/isuwang/isuwang-soa
。
接下来,我们开始编写IDL(接口定义语言),这个过程我们通过编写Thrift文件来完成。
先看一个简单的例子:
TaskAdminService {
/**
* 根据条件查询任务
*/
task_domain.TTaskResponse findTasks(task_domain.TTaskRequest request)
(m.form.url="/task/taskadmin/findTasks.do")
/**
* 新建任务
*/
void createTask(task_domain.TCreateTaskRequest request)
(m.form.url="/task/taskadmin/createTask.do",m.type="domain-action",m.id="task_add",
m.name="新增任务", m.operType="2", m.form.title="新增任务")
/**
* 修改任务
*/
void updateTask(task_domain.TUpdateTaskRequest request)
(m.form.url="/task/taskadmin/updateTask.do",m.type="entity-action",m.id="task_update",
m.name="编辑任务", m.operType="2", m.form.title="编辑任务")
/**
* 删除任务
*/
void deleteTask(i32 id)
(m.form.url="/task/taskadmin/deleteTask.do",m.type="entity-action",m.id="task_delete",
m.name="删除", m.operType="1", m.form.title="删除",m.form.confirm="确定删除?",m.form.sendAsSearchParams = "true")
}
dapeng
内嵌了对thrift文件的解析,通过dapeng-code-generator插件
可以基于thrift文件生成接口以及实体的代码,另外还会生成一组描述每个服务以及服务相关实体元信息的xml文件,诸如(m.form.url="/task/taskadmin/findTasks.do")
这类的注释也会解析到xml文件中。这个xml文件,将会是MetaUI的数据来源,我们稍后会介绍。
现在先来介绍一下对thrift注释所做的扩展:
注释key | 对应元数据字段 | 取值 | 描述 |
---|---|---|---|
m.type | domain-action, entity-action | domain-action:domain作用域的操作,比如添加记录,entity-action:实体作用域的操作,比如删除或修改一个记录 | |
m.id | ActionMeta.actionId | 默认值:方法名称 | 操作id,建议跟权限名相同 |
m.name | ActionMeta.actionName | 操作名称 | |
m.operType | ActionMeta.operType | "1" 或 "2" | "1":直接表单提交,"2":弹窗 |
m.modelSrc | ActionMeta.modelSrc | ||
m.group | ActionMeta.group | 操作分组 | |
m.form.confirm | FormMeta.confirm | 表单提交前的确认提示 | |
m.form.title | FormMeta.title | 表单页面的标题 | |
m.form.url | FormMeta.url | 表单提交的url,与前端controller对应 | |
m.form.hideSubmitBtn | Form.hideSubmitBtn | "true" or "false",缺省值:"false" | 表单中是否隐藏默认的保存按钮 |
m.form.sendAsSearchParams | FormMeta.sendAsSearchParams | "true" or "false",缺省值:"false" | 请求是否以 URLSearchParams 格式发送 |
看一个Struct上添加注释的例子:
struct TTaskRequest {
1: base_model.TPageRequest pageRequest
2: optional string taskNo (m.label="任务编号", ... ),
3: optional i32 taskId (m.srcKey="id", m.visible="false",...)
4: ...
}
在实体字段上配置的注释,key跟FieleMeta上的字段是完全一致的。
你也可以直接在接口入参后面添加注释,下面是一个简单的例子,但是这个例子有点为了举例而举例的嫌疑,因为直接把参数名改为 id
就可以避免 m.srcKey="id"
的配置了。
TaskAdminService {
// ...
void deleteTask(i32 taskId (m.srcKey="id"))
(m.form.url="...", ...)
// ...
}
刚才我们讲到服务的元数据会解析到一个xml文件中,该文件以
服务的namespace+名字
命名,如com.isuwang.soa.task.service.TaskAdminService.xml
。
我们依稀记得MetaUI元数据的配置字段可远不止这些,但是先不着急,通过下图,我希望能够让你对MetaUI跟Thrift配置的关系有一个更清晰的认识。
!图在这
实体(Struct)的每个字段对应的是一个<m2-field/>
,FieldMeta的信息会在数据库中配置。
DROP DATABASE IF EXISTS metadb;
CREATE DATABASE IF NOT EXISTS metadb DEFAULT CHARSET utf8 COLLATE utf8_general_ci;
DROP TABLE IF EXISTS `metadb`.`fields`;
CREATE TABLE `fields` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`struct_name` varchar(200) NOT NULL COMMENT '实体名',
`name` varchar(50) NOT NULL COMMENT '字段名,如"orderNo"',
`element` varchar(50) DEFAULT NULL COMMENT '替换元素,如 "paper-input" ',
`label` varchar(50) NOT NULL COMMENT '字段名,如"订单号"',
`required` smallint(1) NOT NULL DEFAULT '0' COMMENT '是否必填 1: 是 0:否',
`multi` smallint(1) NOT NULL DEFAULT '0' COMMENT '是否多选 1: 是 0:否',
`format` varchar(50) DEFAULT NULL COMMENT '显示格式,用于日期显示,如“yyyy-MM-dd”',
`editable` smallint(1) NOT NULL DEFAULT '0' COMMENT '是否可编辑 1: 是 0:否',
`validate` smallint(1) NOT NULL DEFAULT '0' COMMENT '前是否需要校验, 1: 是 0:否',
`length` int(11) DEFAULT NULL COMMENT '字符允许长度',
`max_length` int(11) DEFAULT NULL COMMENT '允许最大长度',
`min_length` int(11) DEFAULT NULL COMMENT '允许最小长度',
`regexp` varchar(50) DEFAULT NULL COMMENT '正则匹配',
`prompt` varchar(50) DEFAULT NULL COMMENT '校验失败时的提示语,如“请输入备注”',
`min` int(11) DEFAULT NULL COMMENT '数字最小值',
`max` int(11) DEFAULT NULL COMMENT '数字最大值',
`candidates` varchar(1000) DEFAULT NULL COMMENT '下拉选项中的值,json串,如 [{value:1,label:"男"},{value:2,label:"女"}]',
`candidate_label` varchar(50) DEFAULT NULL COMMENT '组件显示字段名,针对下拉组件、radio-group组件等,“label”',
`candidate_value` varchar(50) DEFAULT NULL COMMENT '组件值字段名,针对下拉组件、radio-group组件等,“value”',
`placeholder` varchar(50) DEFAULT NULL COMMENT 'placeholder',
`src_key` varchar(50) DEFAULT NULL COMMENT '源字段名',
`visible` smallint(1) NOT NULL DEFAULT '1' COMMENT '界面是否可见 1: 是 0:否',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`created_by` int(11) DEFAULT NULL,
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`updated_by` int(11) DEFAULT NULL,
`disabled` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `field_unique` (`struct_name`,`name`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='字段元数据';
看一个插入语句的例子:
INSERT INTO metadb.fields (struct_name, name, element, label, required, multi, format, editable, validate, length, max_length, min_length, `regexp`, prompt, min, max, candidates, candidate_label, candidate_value, placeholder, src_key, visible, created_at, created_by, updated_at, updated_by, disabled) VALUES ('com.isuwang.soa.task.domain.TTaskRequest', 'taskNo', 'm2-task-picker', '任务编号', 0, 0, null, 1, 0, null, null, null, null, null, null, null, null, null, null, null, null, 1, '2017-11-09 17:51:03', null, '2017-11-23 18:08:04', null, null);
你应该注意到了,thrift文件和数据库中都可以配置FieldMeta字段的值,比如在上面例子中taskId配置了
m.visible="false"
,同时metadb.fields表
中的name="taskId"
的记录中visible
字段的值是1 (会转换成true),那么谁的优先级更高呢?这取决于你如何解析他们。MetaUI提供了一个工具,在utils目录下的<meta-constructor />
,它把thrift跟数据库中定义的元数据,组装成MetaUI组件认识的结构。<meta-constructor />
处理元数据时,thrift上定义的FieldMeta字段值优先级更高。待会会详细介绍<meta-constructor />
。
dapeng-metaui-servlet
来部署元数据查询服务
dapeng-metaui-servlet
是一个简单的servlet,servlet会从请求参数中读取serviceName
和version
,然后通过反射的方式查找到服务元数据的xml文件,解析xml文件,从中获取到跟改service相关的实体信息,到metadb.fields表
中查询相关的元数据记录,xml会装换成json,跟元数据记录一并返回。
<dependency>
<groupId>com.isuwang</groupId>
<artifactId>dapeng-metaui-servlet</artifactId>
<version>1.0.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.25</version>
</dependency>
web.xml
中配置servlet
<servlet>
<servlet-name>metadataServlet</servlet-name>
<servlet-class>com.isuwang.dapeng.metadata.servlet.MetadataServlet</servlet-class>
<init-param>
<param-name>driverClassName</param-name>
<param-value>com.mysql.jdbc.Driver</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>metadataServlet</servlet-name>
<url-pattern>/dapeng/metadata</url-pattern>
</servlet-mapping>
init.properties
配置文件,写入以下内容并修改数据库配置
DB_METADB_URL=jdbc:mysql://127.0.0.1:3306/metadb?useUnicode=true&characterEncoding=utf8
DB_METADB_USER=your-db-username
DB_METADB_PASSWD=your-db-password
也可以设置系统的环境变量,如:
![]()
设置环境变量后,会优先读取环境变量的值。
这里是一个请求的例子:
fetch("/dapeng/metadata?serviceName=com.isuwang.soa.task.service.TaskAdminService&version=1.0.0", {method:"POST",credentials:"include"})
返回的结果:
{
fieldsInStruct: {
com.isuwang.soa.task.domain.TTaskRequest: [...],
...
},
serviceMeta: {
enumDefinitions: [],
meta: {version: "1.0.0", timeout: 30000},
methods: [...],
name: "TaskAdminService",
namespace: "com.isuwang.soa.task.service",
structDefinitions: [...]
}
}
<meta-constructor />
<meta-constructor>
是一个工具组件,它会自动请求dapeng/metadata
获取元数据,并把返回的数据进行重新组装成<m2-* />
组件能读懂的结构,然后通过metadata
字段返回。看下面的例子:
<link rel="import" href="../bower_components/polymer/polymer-element.html">
<link rel="import" href="../bower_components/metaui/utils/meta-constructor.html">
<link rel="import" href="../bower_components/metaui/m2-crud.html">
<dom-module id="task-admin-index">
<template>
<meta-constructor src="/dapeng/metadata"
metadata="{{ metadata }}"
option="[[ option ]]"
on-return="[[ onReturn() ]]"></meta-constructor>
<m2-crud class="main"
id="crud"
metadata="[[ metadata ]]"
key-for-page-result="pageResponse.results"
result-decorator="[[ _getResultChangeHandler() ]]"></m2-crud>
</template>
<script>
class TaskAdminIndex extends Polymer.Element {
static get is() {
return "task-admin-index";
}
static get properties() {
return {
metadata: {
type: Object
},
option: {
type: Object,
value: function () {
return {
serviceName: "com.isuwang.soa.task.service.TaskAdminService",
version: "1.0.0",
methodName: "findTasks"
};
}
}
};
}
_getResultChangeHandler() {
return (result) => {
// do something with result
return result;
}
}
onReturn() {
return (metadata) => {
// do something with metadata
return metadata;
}
}
}
window.customElements.define(TaskAdminIndex.is, TaskAdminIndex);
</script>
</dom-module>
首先强调一点,我这里讲的mock不是大家所熟知的 mockjs(当然未来可能会集成进来),请大家注意不要混淆。mock使用一种对代码尽可能小侵入的方式(两处侵入:1. head标签中的代码库引入,2. URI侵入),实现请求的模拟。
接下来看看mock如何使用:
<head>
...
<script type="text/javascript" src="../bower_components/h2-elements/utils/mock_setup.js"></script>
...
</head>
For Example:
import {data} from "./data.js";
MockDataPool.when("POST", "/path/to/index.do")
.withExpectedHeader("Content-Type", "application/json;charset=utf-8")
.withExpectedHeader("Cache-Control", "no-cache")
.responseWith({status: 200, body: JSON.stringify(data)});
For example:
http://127.0.0.1:8000/components/h2-elements/demo/h2-fetch/index.html?mock=/your/path/to/mockData.js
,
/your/path/to/
index.html 到 mockData.js 的相对路径。比如,如果index.html和mockData.js在一个相同的目录下,则mock的值可以是index.html?mock=mockData.js
或者index.html?mock=./mockData.js
。
MockDataPool.match( request: Request|Requestable-object ): Response|Responsable-object
// demo
const response = MockDataPool.match({
url: "/path/to/index.do",
method: "POST",
headers: {
"Content-Type": "application/json;charset=utf-8",
"Cache-Control", "no-cache"
}
});
“如果一个动物看起来像鸭子,叫起来也像鸭子,那么它就是一个鸭子。” -- 鲁迅
Requestable-object就是一个看起来像Request的对象,它跟Request有相似的字段跟行为。
Responsable-object同理。
建议你直接使用 h2-elements/h2-fetch.html 组件,里面封装了MockDataPool的使用。
用法1:
<h2-fetch request="{{request}}" response="{{response}}" error="{{error}}"></h2-fetch>
request = {
url: "/path/to/index.do",
method: "POST",
headers: {
"Cache-Control": "no-cache"
}
}
用法2:
const request = {
url: "/path/to/index.do",
method: "POST",
headers: {
"Cache-Control": "no-cache"
}
};
new H2Fetch().fetchIt(request)
.then(res => res.json())
.then(console.log)
.catch(console.log);
你仍然需要执行 步骤1
和 步骤2
,因为它们是使用mock的前提。下面是 <h2-fetch/>
中使用MockDataPool的代码:
if (window.__mockEnabled) { // mock开关标志,mock_setup.js 初始化时会设置该值
const matchedRes = MockDataPool.match(requset);
if(matchedRes) {
return Promise.resolve(new Response(matchedRes.body, matchedRes));
}
}
return window.fetch(collectedReq);
<h2-fetch />
会首先使用match方法去匹配,匹配上了则返回封装的Response,否则发起真实的fetch请求。