# Umi UI 插件开发

# 示例

index.js

// 普通的 umi 插件写法,新增 api.onUISocket 和 api.addUIPlugin 接口
export default api => {
  // 处理 socket 通讯数据
  api.onUISocket(({ action, send, log }) => {
    // 通过 action 处理
    // 处理完后 send 数据到客户端
    send({ type, payload });
    // 过程中的日志通过 log 打到客户端
    log(`Adding block Foo/Bar...`);
  });
  // 添加编辑态的插件
  api.addUIPlugin(require.resolve('./dist/client.umd'));
};

ui.js(通过 father-build 打包到 dist/ui.umd.js

// 这个文件打 umd 到 ./dist/client.umd.js,external react、react-dom 和 antd,用 father-build 很容易打出来
export default api => {
  const {
    // 调用服务端方法
    callRemote,
  } = api;

  function Blocks() {
    return <h1>Blocks</h1>;
  }
  // 添加 panel,类似 vscode 点击左边的 Icon 后切换 Panel
  api.addPanel({
    title: '区块管理',
    icon: 'home',
    path: '/blocks',
    component: Blocks,
    // 顶部右侧按钮
    actions: [
      {
        title: '打开配置文件',
        // antd Button type
        type: 'default',
        // 点击后的 action
        action: {
          type: '@@actions/openConfigFile',
          payload: {
            projectPath: api.currentProject.path,
          }
        },
        onClick: () => {},
      },
    ],
  });
  // 更多功能...
};

# 服务端接口

可访问 所有插件接口和属性,以下是几个与 UI 相关 API。

# api.onUISocket

处理 socket 数据相关,比如:

api.onUISocket(({ type, payload }, { log, send, success, failure }) => {
  if (type === 'config/fetch') {
    send({ type: `${type}/success`, payload: getConfig() });
  }
});

注:

  1. 按约定,如果客户端用 api.callRemote 调用服务端接口,处理完数据需 send/success/failure 后缀的数据表示成功和失败。

# send({ type, payload })

向客户端发送消息。

# success(payload)

send({ type: `${type}/success` }) 的快捷方式。

# failure(payload)

send({ type: `${type}/failure` }) 的快捷方式。

# progress(payload)

send({ type: `${type}/progress` }) 的快捷方式。

# log(level, message)

在控制台和客户端同时打印日志。

示例:

log('info', 'abc');
log('error', 'abc');

# api.addUIPlugin

注册 UI 插件,指向客户端文件。

api.addUIPlugin(require.resolve('./dist/ui'));

注:

  1. 文件需是 umd 格式(例如 ./dist/ui.umd.js

# 客户端接口

# api.callRemote()

调服务端接口,并等待 type 加上 /success/failure 消息的返回。若有进度的返回,可通过 onProgress 处理回调。

参数如下:

api.callRemote({
  // 接口名称
  type: string;
  // 传入参数
  payload: object;
  // 监听服务端推送来的数据
  onProgress: (data) => void;
  // 是否建立长久连接
  keep: boolean;
})

示例:

import React from 'react';

const { useState } = React;

// 组件 props api 从插件传入
export default (props) => {
  const { api } = props;
  const [progress, setProgress] = useState(0);

  const handleClick = async () => {
    await api.callRemote({
      type: 'org.umi.plugin.bar.create',
      payload: {
        id: 'id',
      },
      onProgress: async (data) => {
        useState(data);
      }
    })
  }

  return (
    <div>
      <button onClick={handleClick}>Click</button>
      <p>progress: {progress}</p>
    </div>
  )
}

注:

  1. callRemote 会自动带上 lang 属性,供服务端区分语言
  2. keep 属性,则不会在 success 或 failure 后清除掉

# api.listenRemote()

监听 socket 请求,有消息时通过 onMessage 处理回调。

返回一个 unlisten 函数,用于取消监听。

示例:

const unlisten = api.listenRemote({
  // 接口名称
  type: 'org.umi.plugin.foo',
  onMessage: (data) => {
    // 函数处理
  }
});

// 组件卸载时可调用,取消监听
unlisten();

# api.send()

发送消息到服务端。

# api.addDashboard()

添加入口卡片到『总览』页

调用参数如下:

(dashboard: IDashboard | IDashboard[]) => void;
interface IDashboard {
  /** card key 唯一标识,约定格式为:org.umi.dashboard.card.${key} */
  key: string;
  /** card 标题 */
  title: ReactNode;
  /** card 描述 */
  description: ReactNode;
  /** icon 图标 */
  icon: ReactNode;
  /** icon 图标背景色,默认为 #459BF7 */
  backgroundColor?: string;
  /** card body */
  content: ReactNode | ReactNode[];
  /** card 右侧区域扩展 */
  right?: ReactNode;
  /** 栅格,默认为 { xl: 6, sm: 12, lg: 12, xs: 24 } */
  span?: Partial<{
    /** default 6 */
    xl: number;
    /** default 12 */
    sm: number;
    /** default 12 */
    lg: number;
    /** default 24 */
    xs: number;
  }>;
}

示例:

import React from 'react';
import { ControlFilled } from '@ant-design/icons';

export default () => {
  api.addDashboard({
    key: 'org.umi.dashboard.card.testId',
    title: '卡片标题',
    description: '卡片描述',
    icon: <ControlFilled />,
    content: [
      <a onClick={() => alert('部署成功')}>
        一键部署
      </a>,
    ],
  });
}

image

# api.addPanel()

添加客户端插件入口及路由,调用此方法会在 Umi UI 中增加一级菜单。

调用参数有:

api.addPanel({
  // 插件路由
  path: string;
  // 组件
  component: ReactNode;;
  // 图标,同 antd icon
  icon: IconType | string;
  // 全局操作按钮,位于插件面板右上角
  actions?: ReactNode | React.FC | {
    // 标题
    title: string;
    // 按钮样式
    type?: 'default' | 'primary';
    // 与 callRemote 参数一致
    action?: IAction;
    // 额外的点击事件
    onClick?: () => void;
  }[];
  // 实验室插件,开启后插件会加入到实验室中,默认 false
  beta?: boolean;
});

示例:

// ui.(jsx|tsx)
import React from 'react';
import Template from './ui/index';

export default (api) => {
  api.addPanel({
    title: '插件模板',
    path: '/plugin-bar',
    icon: 'environment',
    // api 透传至组件
    component: () => <Template api={api} />,
  });
};

添加插件全局操作区(动态修改全局操作区,参考 api.setActionPanel):

// ui.(jsx|tsx)
import React from 'react';
import Template from './ui/index';

export default (api) => {
  const ActionComp = () => (
    <Input.Search />
  )

  api.addPanel({
    title: '插件模板',
    path: '/plugin-bar',
    icon: 'environment',
    actions: [
      ActionComp,
      <button>Button</button>,
      {
        title: '打开编辑器',
        type: 'default',
        action: {
          // 通过 api.onUISocket 定义
          type: '@@actions/openConfigFile',
          payload: {
            projectPath: api.currentProject.path,
          },
        }
    }]
    // api 透传至组件
    component: () => <Template api={api} />,
  });
};

image

# api.addLocales()

添加全局国际化信息。

例如:

添加国际化字段

// ui.(jsx|tsx)
import React from 'react';
import Template from './ui/index';

export default (api) => {
  // 你也可以在顶部
  // import zh from './your-locale/zh.js'
  // import en from './your-locale/en.js'
  // { 'zh-CN': zh, 'en-US': en }
  api.addLocales({
    'zh-CN': {
      'org.sorrycc.react.name': '陈成',
    },
    'en-US': {
      'org.sorrycc.react.name': 'chencheng',
    },
  });
};

# api.intl.*

使用国际化,使用 api.addLocale 添加国际化字段后,可以在组件里使用 api.intl.* 处理国际化。

提供的国际化方法如下:

type PickIntl = Pick<typeof intl,
  'FormattedDate' |
  'FormattedTime' |
  'FormattedRelative' |
  'FormattedNumber' |
  'FormattedPlural' |
  'FormattedMessage' |
  'FormattedHTMLMessage' |
  'formatMessage' |
  'formatHTMLMessage' |
  'formatDate' |
  'formatTime' |
  'formatRelative' |
  'formatNumber' |
  'formatPlural'
>

例如:

// ui.(jsx|tsx)
import React from 'react';

export default (api) => {
  // 用法 api 参考 https://github.com/formatjs/react-intl/blob/1c7b6f87d5cc49e6ef3f5133cacf8b066df53bde/docs/API.md
  const {
    FormattedMessage,
    formatMessage,
  } = api.intl;
  const Component = (
    <div>
      <p>{formatMessage({ id: 'org.sorrycc.react.home' })}</p>
      <FormattedMessage id="org.sorrycc.react.foo" />
      <p>api.intl alias `api.intl.formatMessage`: {api.intl({ id: 'org.sorrycc.react.name' })}</p>
    </div>
  )
  api.addPanel({
    title: '插件模板',
    path: '/plugin-bar',
    icon: 'environment',
    component: Component,
  });
};

api.intl()formatMessage 参数一致,api.intl()api.intl.formatMessage() 等价。

image

# api.getLocale()

返回当前语言,zh-CNen-US 等。

# api.hooks.*

集成 UI 开发中常用 hooks,更多 API 见 [http://hooks.umijs.org/]。

例如:

import React from 'react';

export default (api) => {
  const { useDebounceFn } = api.hooks;
  const [value, setValue] = useState(0);
  const { run } = useDebounceFn(() => {
    setValue(value + 1);
  }, 500);
  const Component = (
     <div>
      <p style={{ marginTop: 16 }}> Clicked count: {value} </p>
      <Button onClick={run}>Click fast!</Button>
    </div>
  )
  api.addPanel({
    title: '插件模板',
    path: '/plugin-bar',
    icon: 'environment',
    component: Component,
  });
};

# api.showLogPanel()

打开 Umi UI 底部日志栏。

# api.hideLogPanel()

隐藏 Umi UI 底部日志栏。

# api.moment

moment 一致。

# api.event

events 一致。

# api.TwoColumnPanel

两栏布局组件

比如:

const { TwoColumnPanel } = api;

function Configuration() {
  return (
    <TwoColumnPanel
      sections={[
        {
          // 访问 /${插件路由}?active=${key}
          // 可定位到具体插件的具体面板
          key?: 'basic',
          title: '基本配置', description,
          icon: '',
          component: C1
        },
        {
          key?: 'config',
          title: 'umi-plugin-react 配置',
          description,
          icon: '',
          component: C2
        },
      ]}
    />
  );
}

api.addPanel({
  component: Configuration,
});

# api.Terminal

终端命令行组件,基于 xterm.js@4.x

参数如下:

interface ITerminalProps {
  /** Terminal title */
  title?: ReactNode;
  className?: string;
  terminalClassName?: string;
  /** defaultValue in Terminal */
  defaultValue?: string;
  /** terminal init event */
  onInit?: (ins: XTerminal, fitAddon: any) => void;
  /** https://xtermjs.org/docs/api/terminal/interfaces/iterminaloptions/ */
  config?: ITerminalOptions;
  onResize?: (ins: XTerminal) => void;
  [key: string]: any;
}

示例:

使用终端实例,调用日志输出函数:

import React, { useState } from 'react'

export default (api) => {
  const { Terminal } = api;

  function Component() {
    let terminal;

    const handleClick = () => {
      terminal.write('Hello World');
    }

    return (
      <div>
        <Terminal
          title="插件日志"
          onInit={ins => {
            terminal = ins;
          }}
        />
        <button onClick={handleClick}>开始</button>
      </div>
    );
  }

  api.addPanel({
    component: Component,
  });
}

image

终端更多用法见 文档

# api.DirectoryForm

目录选择表单控件

参数如下:

interface IDirectoryForm {
  /** path, default  */
  value?: string;
  onChange?: (value: string) => void;
}

示例:

import React from 'react';
import { Form } from 'antd';

export default () => {
  const [form] = Form.useForm();
  return (
    <Form
      form={form}
      onFinish={values => {
        console.log('values', values)
      }}
      initialValues={{
        baseDir: cwd,
      }}
    >
      <Form.Item
        label={null}
        name="baseDir"
        rules={[
          {
            validator: async (rule, value) => {
              await validateDirPath(value);
            },
          },
        ]}
      >
        <DirectoryForm />
      </Form.Item>
    </Form>
  )
}

image

# api.StepForm

步骤表单组件

使用示例:

<StepForm onFinish={handleSubmit} className={stepCls}>
  <StepForm.StepItem title="a-form">
    <Form>
      <Form.Item name="a">
        <input />
      </Form.Item>
    </Form>
  </StepForm.StepItem>

  <StepForm.StepItem title="b-form">
    <Form>
      <Form.Item name="b">
        <input />
      </Form.Item>
    </Form>
  </StepForm.StepItem>
</StepForm>

# api.Field

配置表单组件,结合 antd 4.x 一起使用,简化表单组件,使用配置式生成表单。

api.Field 参数如下:

interface IFieldProps {
  /** 表单类型 */
  /** 具体类型有:"string" | "boolean" | "object" | "string[]" | "object[]" | "list" | "textarea" | "any" */
  type: IConfigTypes;
  /** 表单 字段名,通过 `.` 来确定字段之间的联动关系  */
  name: string;
  /** 可选列表,只用在 type 为 object  */
  defaultValue?: IValue;
  /** 主要用于数组表单类型,提供可选值列表 */
  options?: string[];
  /** antd 4.x form 实例 */
  form: object;
  /** antd label, 如果是 object,则使用内置的 <Label /> 组件 */
  /** object 参数有 { title: string, description: string, link?: string } */
  label: string | ReactNode | IFieldLabel;
  /** 控件大小, 默认是 default */
  size?: 'default' | 'small' | 'large';
  /** 其它类型与 Form.Item 一致 */
  [key: string]: any;
}

interface IFieldLabel {
  /** label title */
  title: string;
  /** label description */
  description: string;
  /** description detail link */
  link: string;
}

例如,联动示例 :

import { Form } from 'antd'
const { TwoColumnPanel } = api;

function Configuration() {
  const [form] = Form.useForm();

  return (
    <Form
      form={form}
      onFinish={values => {
        console.log('valuesvalues', values);
      }}
      initialValues={{
        'parent.child2': ['**/a.js', '**/b.js'],
        'parent.child3': '<script>alert("Hello")</script>',
        'parent2.child': 'Method1',
      }}
    >
      <Field form={form} name="parent" label="SpeedUp-boolean" type="boolean" />
        <Field form={form} name="parent.child" label="Speed-string" type="string" />
        <Field
          form={form}
          name="parent.child2"
          label="Speed-string[]"
          type="string[]"
        />
        <Field form={form} name="parent.child3" label="Speed-textarea" type="textarea" />
        <Field form={form} name="parent.child4" label="Speed-any" type="any" />

      <Field form={form} name="parent2" label="Config-boolean" type="boolean" />
        <Field
          form={form}
          name="parent2.child"
          label="Config-list"
          type="list"
          options={['Method1', 'Method2']}
        />
        <Field
          form={form}
          name="parent2.child2"
          label="Config-list"
          type="object"
          options={['Target1', 'Target2']}
        />

      <Form.Item shouldUpdate>
        {({ getFieldsValue }) => <pre>{JSON.stringify(getFieldsValue(), null, 2)}</pre>}
      </Form.Item>
      <Button htmlType="submit">Submit</Button>
    </Form>
  );
}

api.addPanel({
  component: Configuration,
});

# api.ConfigForm

配置表单页面,对 api.Field 的上层封装,增加查询、修改接口即可生成表单页面:

api.ConfigForm 参数如下:

interface IConfigFormProps {
  /** config title in the top */
  title: string;
  /** list config interface */
  list: string;
  /** edit config interface */
  edit: string;
  /** enable Toc, default false */
  enableToc?: boolean;
  /** Search fuse options, detail in https://github.com/krisk/Fuse */
  fuseOpts?: FuseOptions<number>;
}

使用示例:

服务端

// server
export default (api) => {
  // more options in `api.Field` IFieldProps
  const data = [
    {
      "name": "base",
      "group": "Group1",
      "type": "string",
      "default": "/",
      "title": "group1",
      "description": "description1",
    },
    {
      "group": "Group2",
      "name": "group2",
      "title": "title2",
      "description": "description2",
      "type": "boolean",
      "default": false,
    },
    {
      "group": "Group2",
      // if you want link parent config, use `.` dot split
      "name": "group2.bar",
      "title": "title3",
      "description": "description3",
      "type": "boolean",
      "default": false,
    },
  ]

  api.onUISocket(({ action, failure, success }) => {
    const { type, payload, lang } = action;
    switch (type) {
      case 'org.umi.plugin.bar.config.list':
        success({
          data,
        });
        break;
      case 'org.umi.plugin.bar.config.edit':
        let config = payload.key;
        if (typeof payload.key === 'string') {
          config = {
            [payload.key]: payload.value,
          };
        }
        try {
          // your validate function
          // validateConfig(config);
          // (api as any).service.runCommand('config', {
          //   _: ['set', config],
          // });
          success();
        } catch (e) {
          failure({
            message: e.message,
            errors: e.errors,
          });
        }
        break;
      default:
        break;
    }
  });
}

客户端

// client
const { ConfigForm } = api;

api.addPanel({
  component: (
    <ConfigForm
      title="title Config"
      list="org.umi.plugin.bar.config.list"
      edit="org.umi.plugin.bar.config.edit"
    />
  ),
});

image

# api.notify()

调用 Umi UI 通知栏,若用户停留在当前浏览器窗口,通知栏样式为 antd Notification,否则为系统原生通知栏。

传入参数:

{
  title: string;
  message: string;
  /** notify type, default info */
  type?: 'error' | 'info' | 'warning' | 'success';
  subtitle?: string;
  /** URL to open on click */
  open?: string;
  /**
   * The amount of seconds before the notification closes.
   * Takes precedence over wait if both are defined.
   */
  timeout?: number;
}

比如:

const { notify } = api;

notify({
  /** 前提已经调用过 api.addLocales 添加 key */
  title: 'org.umi.ui.blocks.notify.title',
  message: '可以不使用国际化',
  type: 'success',
});

# api.redirect()

项目详情内的路由跳转,在不同插件之间进行跳转。

示例:

const { redirect } = api;

export default () => (
  <Button
    onClick={() => redirect('/project/select')}
  >
    跳转到项目列表
  </Button>
);

# api.currentProject

获取当前项目基本信息,信息包括:

{
  // KEY
  key?: string;
  // 应用名
  name?: string;
  // 应用路径
  path?: string;
}

示例:

const { currentProject } = api;

export default () => (
  <div>
    <p>当前应用名:{currentProject.name}</p>
    <p>当前路径:{currentProject.path}</p>
  </div>
);

# api.debug()

debug API

调试插件的时候,localStorage 修改为 debug: umiui:UIPlugin*。就可以看到所有插件的 debug 信息。

使用(以配置管理插件为例):

export default () => {
  const { debug } = api;
  // 声明插件 namespace
  const _log = api.debug.extend('configuration');
  _log('Hello UI Configuration');
}

image

如果需要调试 mini 版日志,再增加一个 debugMini: umiui

image

不建议在插件里使用 console.log 调用。

# api.getCwd()

获取 Umi UI 启动时的路径。

示例:

const cwd = await api.getCwd();
// "/private/tmp/xxxx"

# api.getSharedDataDir()

获取当前项目的临时目录。

示例:

const dir = await api.getSharedDataDir();
// "/Users/xxxxx/.umi/ui/shared-data/5bc6bd"

# api.detectLanguage()

获取当前项目的语言类型。

示例:

const language = await api.detectLanguage();
// "JavaScript" | "TypeScript"

# api.detectNpmClients()

获取当前项目可能在用的 npm 客户端数组。

示例:

const npmClients = await api.detectNpmClients();
// ["tnpm", "ayarn", "npm", "yarn"]

# api.addConfigSection()

添加配置区块。

示例:

api.addConfigSection({
  key: 'umi-plugin-react',
  title: 'umi-plugin-react 配置',
  description: '配置 dva、antd、按需加载、国际化等',
  icon: (
    <img
      src="https://img.alicdn.com/tfs/TB1aqdSeEY1gK0jSZFMXXaWcVXa-64-64.png"
      width={32}
      height={32}
    />
  ),
  component: () => <div>TODO</div>,
});

# api.isMini() / api.mini

获取当前环境是否是 Umi UI mini。

示例:

const isMini = api.isMini();  // true / false
// or const isMini = api.mini

image

# api.showMini()

打开 Umi UI mini 窗口(mini 环境下启用)。

# api.hideMini()

关闭 Umi UI mini 窗口(mini 环境下启用)。

# api.setActionPanel()

运行时动态修改右上角全局操作区,与 React 中 setState 类似。

参数如下:

示例:

// ui.(jsx|tsx)
import React from 'react';

export default (api) => {
  // init Action
  const ActionComp = () => (
    <Input />
  )

  api.addPanel({
    title: '插件模板',
    path: '/plugin-bar',
    icon: 'environment',
    actions: [
      ActionComp
    ]
    // api 透传至组件
    component: () => {
      const handleClick = () => {
        // 类似于 React 中 setState
        api.setActionPanel((actions) => {
            return [
              ...actions,
              () => <Button onClick={() => alert('hello')}>New Button</Button>
            ]
          })
        }}
      }
      return (
        <Button onClick={handleClick}>
          添加一个新操作按钮
        </Button>
      )
    },
  });
};