优维低代码:定制 Providers

Posted 优维科技EasyOps

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了优维低代码:定制 Providers相关的知识,希望对你有一定的参考价值。

优维低代码技术专栏,是一个全新的、技术为主的专栏,由优维技术委员会成员执笔,基于优维7年低代码技术研发及运维成果,主要介绍低代码相关的技术原理及架构逻辑,目的是给广大运维人提供一个技术交流与学习的平台。


连载第四十三期

《现场定制:定制Providers》

# 何为 provider ?

provider 也是一种构件,设计的原意是为了封装后台接口,提供统一的前端 SDK 。在介绍 provider 之前,要先介绍下优维科技在 2019 年开始推行的“契约为中心”的开发模式。

在 2019 年前,优维科技的后台主流开发语言为 python 和 php ,前端则为 javascript。因为这弎都是弱类型,开发者一不注意,接口的输入和输出就会出现了大量的 map。随着系统的不断膨胀,在接口对接过程中,总是会出现各种字段不一致的情况,特别是在重构的时候,就更加是“动态类型一时爽,代码重构火葬场”。因此,2019 年,整个优维技术研发部开始推行以“契约为中心”的开发模式,后台主流开发语言切到了 go,前端开发语言也切到了 TypeScript。

在开发一个接口的时候,都要先定义契约(点击查看 接口契约介绍 ),然后再基于该契约直接生成前端的 SDK(provider)和后端的框架代码及后端的 SDK(go,python)及 API 文档。这样,前后台都强制遵循契约精神,保证各方统一。

# 定制化 provider

我们推行前端尽量少写处理逻辑,当前我们绝大部分 provider 都是自动生成的(点击查看内置 provider 文档),不需要写任何一行代码就可以将展示构件与后台接口对接起来。但,不可否认的,在某些特殊场景还是需要写些处理逻辑,另外,如果有第三方 API 数据接入的时候,也需要写定制 provider。

yarn yo脚手架封装了 provider 的生成。参考如下,按提示执行:

➜  brick-next git:(master) ✗ yarn yo
yarn run v1.12.3
$ brick-scripts
? What do you want? a new custom provider brick
? which package do you want to put the new brick in? search
? What's the name of your new brick (in lower-kebab-case)? provider-demo-provider
File created: ./bricks/search/src/data-providers/DemoProvider.spec.ts
File created: ./bricks/search/src/data-providers/DemoProvider.ts
File updated: ./bricks/search/src/index.ts


No worries!
✨  Done in 53.99s.

# 示例

# 封装第三方 API 接口请求

import  createProviderClass  from "@next-core/brick-utils";
import  http  from "@next-core/brick-http";


export interface TestParams 
  a: string;
  b: string;



export async function Test(
  params: TestParams
): Promise<any> 
  return http.put(
    "http://localhost:8080/test",
    params
  );



customElements.define(
  "demo.provider-test",
  createProviderClass(Test)
);

注意:请检查项目一级 package.json 的 devDependencies 有没声明@next-core/brick-http 的依赖,如果没有,请加入:

  • "@next-core/brick-http": "^1.0.0",
  • "@next-core/brick-dll": "^1.0.61",

第三方接口接入优维的 api_gateway

如上示例直接请求后端接口
http://localhost:8080/test 会有几个问题:

  1. 跨域的问题
  2. 安全的问题

建议统一接入到优维的 api_gateway 来转发,具体配置方式见第三方接口接入。由此,这里需要改为:

import  createProviderClass  from "@next-core/brick-utils";
import  http  from "@next-core/brick-http";


export interface TestParams 
  a: string;
  b: string;



export async function Test(
  params: TestParams
): Promise<any> 
  return http.put(
    // 注意不要写成全路径/api,而应该写成 api
    "api/gateway/your-api-prefix/test",
    params
  );



customElements.define(
  "demo.provider-test",
  createProviderClass(Test)
);

# 纯逻辑处理的 provider

index.ts

import  createProviderClass  from "@next-core/brick-utils";


import  listBrickStory, categoryList  from "./processor";


customElements.define(
  "developers.providers-of-brick-story",
  createProviderClass(listBrickStory)
);


customElements.define(
  "developers.get-category-list",
  createProviderClass(categoryList)
);

processor.ts

import i18next from "i18next";
import  MenuIcon  from "@next-core/brick-types";
import  atomBook  from "../stories/chapters/atom-bricks";
import  businessBook  from "../stories/chapters/business-bricks";
import  Story, Chapter, I18nString  from "../stories/interfaces";


export const categoryList = (storyType: string): Promise<string[]> => 
  let books: Chapter[] = [];
  if (storyType === "atom") 
    books = atomBook;
   else if (storyType === "business") 
    books = businessBook;
  
  const lang = i18next.language
    ? (i18next.language.split("-")[0] as keyof I18nString)
    : "zh";
  const categoryList = books.map((book: Chapter) => 
    return book.title[lang];
  );
  return Promise.resolve(categoryList);
;


// 省略 listBrickStory 函数

# 基于已有 SDK 修改

import  createProviderClass  from "@next-core/brick-utils";
import  HttpOptions  from "@next-core/brick-http";
import  InstanceTreeApi  from "@sdk/cmdb-sdk";
import  AntTreeNodeProps  from "antd/lib/tree";
import  MenuIcon  from "@next-core/brick-types";
import  CustomIconComponentProps  from "antd/lib/icon";


import  Instance  from "../../interfaces";


interface Business extends Instance 
  _businesses_APP?: Instance[];
  _sub_system?: Business[];



type TreeIcon = MenuIcon | React.ComponentType<CustomIconComponentProps>;


export interface BrickTreeNodeProps extends AntTreeNodeProps 
  title?: string;
  icon?: TreeIcon;
  children?: BrickTreeNodeProps[];



function convertBusinessesToTreeNodes(businesses: Business[]) 
  const treeNodes: BrickTreeNodeProps[] = [];


  businesses.forEach((business) => 
    let children: BrickTreeNodeProps[] = [];


    if (business._sub_system) 
      children = children.concat(
        convertBusinessesToTreeNodes(business._sub_system)
      );
    


    business._businesses_APP &&
      business._businesses_APP.forEach((app) => 
        children.push(
          title: app.name,
          key: app.instanceId,
          icon:  lib: "fa", icon: "cube" ,
        );
      );


    if (children.length > 0) 
      treeNodes.push(
        title: business.name,
        key: `_$business.instanceId`,
        icon:  lib: "fa", icon: "briefcase" ,
        selectable: false,
        children,
      );
    
  );


  return treeNodes;



async function getBusinessAppTree(options?: HttpOptions) 
  const data: 
    BUSINESS?: Business[];
    APP?: Instance[];
   = await InstanceTreeApi.instanceTree(
    
      tree: 
        object_id: "BUSINESS",
        fields:  name: true ,
        child: [
           relation_field_id: "_sub_system", fields:  name: true  ,
           relation_field_id: "_businesses_APP", fields:  name: true  ,
        ],
      ,
    ,
    options
  );
  let treeNodes: BrickTreeNodeProps[] = [];


  if (data.BUSINESS) 
    treeNodes = treeNodes.concat(convertBusinessesToTreeNodes(data.BUSINESS));
  


  data.APP &&
    data.APP.forEach((app) => 
      treeNodes.push(
        title: app.name,
        key: app.instanceId,
        icon:  lib: "fa", icon: "cube" ,
      );
    );


  return treeNodes;



customElements.define(
  "app-deploy-statistics.provider-business-app-tree",
  createProviderClass(getBusinessAppTree)
);

# 使用方式

点击查看[构件事件传递](
/next-docs/docs/micro-app/brick-event#调用 provider)

优维低代码:Best Practice

优维低代码:Best

优维低代码:Best

导语

优维低代码技术专栏,是一个全新的、技术为主的专栏,由优维技术委员会成员执笔,基于优维7年低代码技术研发及运维成果,主要介绍低代码相关的技术原理及架构逻辑,目的是给广大运维人提供一个技术交流与学习的平台。


连载第十二期

《编排详解:Best Practice》

# Storyboard 最佳结构

  • 第一级路由type:route
  • Provider 的声明尽量放到路由第一级
  • 一级菜单统一在一级menu写,这样一级菜单也直接复用了

过去:

  • provider 重复定义,也不清楚到底我用了哪些接口

我们能看到很多 provider 是在bricks[]定义的,比如:


"bricks": [

"brick": "providers-of-cmdb.instance-api-post-search",
"bg": true
,

"brick": "providers-of-cmdb.cmdb-object-api-get-object-all",
"bg": true


...

]
  • Menu 重复定义

我们能看到很多同学将menu抽出一个函数,然后在 ts 里面直接调用,但实际上这样生成的storyboard.json也是每个path都定义了一次的。

有人会说,有什么关系呢,反正我就一个函数嘛。因为生成的 storyboard.json 越大,意味着页面首次加载越慢,大伙看下 162 目前的 bootstrap 接口的数据量及耗时就知道了

建议:


"app": "name": "敏捷管理", "id": "agile", "homepage": "/agile" ,
"routes": [

"path": "$APP.homepage",
"menu":
...
,
"providers": [
"providers-of-cmdb.instance-api-post-search",
"providers-of-cmdb.instance-api-get-detail",
"providers-of-cmdb.cmdb-object-api-get-object-all",
"providers-of-cmdb.instance-api-delete-instance",
"providers-of-cmdb.instance-api-update-instance-v2",
"providers-of-cmdb.instance-api-create-instance",
"providers-of-cmdb.instance-api-aggregate-count-v2",
"providers-of-cmdb.instance-api-import-instance",
"providers-of-cmdb.instance-api-group-instance",
"providers-of-topboard.topboard-api-create-issue",
"providers-of-topboard.topboard-api-create-comment",
"providers-of-topboard.topboard-api-update-comment",
"providers-of-topboard.topboard-api-update-issue",
"providers-of-topboard.topboard-api-update-issue-step",
"developers.provider-providers-sub-menu",
"developers.provider-service-data",
"developers.providers-of-brick-story",
"developers.providers-of-brick-docs",
"providers-of-notify.oplog-api-list-operation-log"
],
"type": "routes",
"routes": [
...
]

]

# 多用事件回调(Callback)

上面说到,provider 都定义在一级路由,那这里有同学可能会说,调用了provider后我希望弹出提示框呢,怎么搞?

事实上,很多时候也是因为此,所以要多次定义 provider

框架提供了新的 callback 能力,可以直接在 callback 写对应的处理,更加简单直接,代码量直接少 1/4。


brick: "presentational-bricks.modal-confirm",
properties:
id: "sprint-complete-confirm",
title: "确定关闭迭代?",
content: "未完成任务将转移到需求池"
,
events:
"confirm.ok": [

target: "providers-of-cmdb\\\\.instance-api-update-instance-v2",
method: "resolve",
args: [
"_SPRINT",
"$sprintId",

status: "completed"

],
callback:
success: [

action: "message.success",
args: ["迭代关闭成功,进入下一个迭代"]
,

action: "history.push",
args: ["$APP.homepage/product/$productId/sprint"]

],
error:
action: "handleHttpError"


,


]

另外,大伙是否注意到message.success,我们已经将
presentational-bricks.brick-utils的能力封装到框架了,意味着不用再写下面的定义了


brick: "presentational-bricks.brick-utils",
bg: true

# callback 的时候别只写 success,而不写 error

如果是单元测试的话,这就是路径没覆盖。

# 什么时候 provider 应该用 id 来调用

如果有用到provider的暂存数据能力,比如调用了updateArgs,updateArgsAndExecute,setArgs,setArgsAndExecute,这种情况下就应该去声明id,用id来调用。

如果直接providers-of-xx.xxx调用的话,很容易就被其他编排者污染了你的参数。

# 尽量依照调用顺序去传递数据,不要去改别人内部的东西

场景一:表格里面有个按钮去调用 provider


"brick": "presentational-bricks.brick-table",
"properties":
"columns": [

"title": "Tools",
"width": "180px",
"useBrick": [

"brick": "basic-bricks.general-custom-buttons",
"transform":
"dataSource": "@rowData"
,
"properties":
"isMoreButton": true,
"alignment": "start",
"customButtons": [

"isDropdown": false,
"tooltip": "Active Issue",
"icon": "like",
"eventName": "issue.like",
"buttonType": "link"

]
,
"events":
"issue.like": [

"target": "#updateProvider",
"method": "executeWithArgs",
"args": [
"_ISSUE",
"$EVENT.detail.instanceId",
"status": "active"
]

]


]

]

大伙注意到,数据的传递方向是 table -> button -> provider。尽量不要在某个地方去给provider去updateArgs

现在我们交互原子构件(比如button、custom-buttons、brick-link)都具备数据(dataSource或detail字段)的暂存能力了

场景二:点击按钮弹出 modal,modal 里面有个 form 表单

这个场景比较复杂,现在还没有一个最佳实践,用新的 custom template 封装吧。准备后面专门做个modal-form来解决

# 在没有地方暂存数据的时候,记得 html element 是可以任意存储数据的

事实上,你可以在 event 或者 lifeCycle 的时候,给某个构件写入任意属性去暂存数据,这样在这个构件的事件发出来的时候,你可以通过$EVENT.target.xxx获得数据


brick: "agile.comment-brick",
properties:
id: "issue-comment",
placeholder: "说点什么"
,
events:
"add.comment":
target:
"providers-of-topboard\\\\.topboard-api-create-comment",
method: "resolve",
args: [

body: "$EVENT.detail.body",
author: [
instanceId: "$SYS.userInstanceId"
],
issue: [

instanceId: "$EVENT.target.issueInstanceId"

]

],
callback:






# 动态构件列表渲染

general-card-list卡片列表我们封的比较死,只能使用到里面的card-item,而且他是一个老版的template,很难用事件来触发他更新数据,怎么办?我们现在有个神奇list-container+userBrick机制,可以随便搞动态。


brick:
"basic-bricks.list-container",
properties:
containerStyle:
display: "grid",
gap: "20px",
gridTemplateColumns:
"repeat(auto-fill, minmax(130px, 1fr))"
,
useBrick:
brick:
"presentational-bricks.entry-card-item",
transform:
cardTitle: "@objectName",
id: "@objectId",
iconColor: "@iconColor",
icon: "@icon",
urlTemplate:
"$APP.homepage/@objectId"




# modal,drawer 等弹窗容器(默认不可见)类型的构件:

  • 设置portal: true
  • 不要设置bg: true

# 搜索框都放在 search-bar 里面,这个构件已经按设计做好了间距

searchable-table足够强大,但有些时候也不够灵活,故专门开发了search-bar容器,用于常见的search-bar+brick-table。不要再自己写 css 啦

#在表格里面添加链接,不要用general-button type=link,而是用brick-link

因为:

  • button 会有 margin-left
  • button 的文字不能拖动选择复制

# 复杂的数据加工,记得我们除了有 pipe 之外,还可以直接写 js 表达式

注意transform里面的<% %>


"columns": [

"title": "任务数",
"dataIndex": "issues.length",
"key": "issues",
"useBrick":
"brick": "presentational-bricks.brick-value-mapping",
"transform":
"value": "<% _.filter(DATA.rowData.issues, item => item.resolution || item.resolution===\\"\\").length + \\" / \\" + DATA.rowData.issues.length %>"
,
"properties":
"mapping":
"*":
"color": "blue"




,

"title": "完成进度",
"dataIndex": "progress",
"key": "progress",
"useBrick":
"brick": "presentational-bricks.basic-progress",
"transform":
"value": "<% Math.round(_.filter(DATA.rowData.issues, item => item.resolution || item.resolution===\\"\\").length/DATA.rowData.issues.length*100) %>"
,
"properties":
"configProps":
"size": "small"
,
"value": 75,
"colorMap": [

"progress": 60,
"color": "red"
,

"progress": 80,
"color": "orange"
,

"progress": 100,
"color": "green"

],
"type": "line"



]

最终效果是:

优维低代码:Best

# 根据不同的条件触发不同的动作

  1. 可以通过框架提供的if能力,但那个意味着需要写两个,他控制粒度只能到brick级别
  2. 用script-brick提供的条件事件。如下,根据 url 参数是否具备fullscreen参数来做不同的渲染

优维低代码:Best

以上就是今天关于优维低代码的分享,截止这一期基于构建框架的代码开发的所有内容都分享完了,希望对你有所收获!

以上是关于优维低代码:定制 Providers的主要内容,如果未能解决你的问题,请参考以下文章

优维低代码:构件 slot 说明

优维低代码:Use Resolves

优维低代码:Provider 构件

优维低代码:Best Practice

优维低代码:构件渲染子构件

优维低代码:Placeholders 占位符