胖蔡说技术
随便扯扯

axios请求使用指南

胖蔡阅读(9)

Axio是一个基于Promise的网络请求库,同时适配于浏览器端和服务端(nodejs)。在服务端它使用原生 node.js http 模块, 而在客户端 (浏览端) 则使用 XMLHttpRequests。本文介绍axios的常规使用方法以及axios的主要API功能。

安装

支持各类工具安装使用:

$ npm i axios
$ yarn add axios
$ pnpm i axios

也可以直接通过cdn加载axios

<script src="https://unpkg.com/axios/dist/axios.min.js"></script>

请求示例

axios内置请求方法:request、get、delete、head、options、put、patch、put方法进行对应方法请求,也可以通过axios配置config进行配置请求:

// axios配置请求
axios({
  method:'get',
  url:'http://bit.ly/2mTM3nY',
  responseType:'stream'
})
  .then(function(response) {
  response.data.pipe(fs.createWriteStream('ada_lovelace.jpg'))
});


axios.request(config)
axios.get(url[, config])
axios.delete(url[, config])
axios.head(url[, config])
axios.options(url[, config])
axios.post(url[, data[, config]])
axios.put(url[, data[, config]])
axios.patch(url[, data[, config]])

接下来我们就通过不同类型的axios请求示例来了解如何使用axios发起一个请求:

1、发起get请求

// 为给定 id的 user 创建请求
axios.get('/user?id=12345')
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.log(error);
  });

2、发起post请求

axios.post('/user', {
    firstName: 'Fred',
    lastName: 'Flintstone'
  })
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.log(error);
  });

3、发起put请求

const axios = require('axios');
 
// 设置请求的URL,以及需要更新的数据
const url = 'https://www.enjoytoday.cn/api/data';
const data = {
  key: 'value'
};
 
// 发起PUT请求
axios.put(url, data)
  .then(response => {
    console.log('PUT请求成功:', response);
  })
  .catch(error => {
    console.error('PUT请求出错:', error);
  });

4、发起delete请求

const axios = require('axios');

// 发送DELETE请求
axios.delete('https://www.enjoytoday.cn/resource')
  .then(function (response) {
    // 处理响应数据
    console.log(response.data);
  })
  .catch(function (error) {
    // 处理错误情况
    console.log(error);
  });

5、发起form-data请求

form-data请求,通常用于上传文件或提交表单数据。这是之前浏览器端设计的表单配置的模式,这里提供两种方式来进行,一种纯表单数据可以使用’application/x-www-form-urlencoded‘头部信息,一种需要上传文件,需要设置:’multipart/form-data‘ 头部信息。如上可总结两种方式进行form-data参数请求,这里以post方法为例

  • FormData对象参数上传
  • FormData对象上传 【也可以只是单独传参】
// 对象上传
const axios = require('axios'); // 引入axios库
axios({
  method: 'post',
  url: 'http://example.com/login',
  headers: {'Content-Type': 'application/x-www-form-urlencoded'},
  data: {
    username: 'enjoytoday.cn',
    password: 'pass'
  }
})
.then(function(response) {
  console.log('Response:', response);
})
.catch(function(error) {
  console.log('Error:', error);
});


//  formData对象请求

const axios = require('axios'); // 引入axios库
const formData = new FormData()
formData.append('username', 'enjoytoday.cn'); // 添加键值对
formData.append('nickname', '胖蔡'); // 添加键值对
formData.append('file', file); // 添加文件

// 发送POST请求
axios.post('http://example.com/upload', formData, {
  headers: {
     'Content-Type': 'multipart/form-data',
  }
})
.then(response => {
    console.log('Success:', response.data);
})
.catch(error => {
    console.error('Error:', error);
});

配置与APIS

实例配置

常规的我们会通过创建一个axios示例配置一些公用配置,如:请求前缀、请求超时时间、共用请求头等,创建示例如下:

const instance: AxiosInstance = axios.create({
  baseURL,
  timeout: 60000,
  headers: { 'Content-Type': 'application/json;charset=UTF-8'},
})

创建配置的options,其支持配置属性如下:

export interface AxiosRequestConfig<D = any> {
  url?: string;
  method?: Method | string;
  baseURL?: string;
  transformRequest?: AxiosRequestTransformer | AxiosRequestTransformer[];
  transformResponse?: AxiosResponseTransformer | AxiosResponseTransformer[];
  headers?: (RawAxiosRequestHeaders & MethodsHeaders) | AxiosHeaders;
  params?: any;
  paramsSerializer?: ParamsSerializerOptions | CustomParamsSerializer;
  data?: D;
  timeout?: Milliseconds;
  timeoutErrorMessage?: string;
  withCredentials?: boolean;
  adapter?: AxiosAdapterConfig | AxiosAdapterConfig[];
  auth?: AxiosBasicCredentials;
  responseType?: ResponseType;
  responseEncoding?: responseEncoding | string;
  xsrfCookieName?: string;
  xsrfHeaderName?: string;
  onUploadProgress?: (progressEvent: AxiosProgressEvent) => void;
  onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void;
  maxContentLength?: number;
  validateStatus?: ((status: number) => boolean) | null;
  maxBodyLength?: number;
  maxRedirects?: number;
  maxRate?: number | [MaxUploadRate, MaxDownloadRate];
  beforeRedirect?: (options: Record<string, any>, responseDetails: {headers: Record<string, string>, statusCode: HttpStatusCode}) => void;
  socketPath?: string | null;
  transport?: any;
  httpAgent?: any;
  httpsAgent?: any;
  proxy?: AxiosProxyConfig | false;
  cancelToken?: CancelToken;
  decompress?: boolean;
  transitional?: TransitionalOptions;
  signal?: GenericAbortSignal;
  insecureHTTPParser?: boolean;
  env?: {
    FormData?: new (...args: any[]) => object;
  };
  formSerializer?: FormSerializerOptions;
  family?: AddressFamily;
  lookup?: ((hostname: string, options: object, cb: (err: Error | null, address: LookupAddress | LookupAddress[], family?: AddressFamily) => void) => void) |
      ((hostname: string, options: object) => Promise<[address: LookupAddressEntry | LookupAddressEntry[], family?: AddressFamily] | LookupAddress>);
  withXSRFToken?: boolean | ((config: InternalAxiosRequestConfig) => boolean | undefined);
}

改配置属性在发起请求时通用,若和axios实例已存在配置会自动覆盖,且发起请求的配置除了url是必须属性外,其他均为非必需属性。

全局配置

通过实例配置想要生效的话需要所有请求都是通过该实例发起的,也就是axios.create创建产生的对象,当我们想要对axios创建的所有对象均生效的话,我们可以通过配置axios.defaults来实现,配置如下:

axios.defaults.baseURL = 'https://www.enjoytoday.cn';
axios.defaults.headers.common['Authorization'] = 'Authorization';
axios.defaults.headers.post['Content-Type'] = 'application/json';

defautls支持所有AxiosRequestConfig的配置项。

拦截器

可以通过拦截器对网络请求体和请求返回数据进行拦截处理,如响应数据格式化,响应状态全局处理等,也可以对一些特殊请求场景进行差异化处理,如上传下载文件可以拦截请求配置对应请配置。

// 添加请求拦截器
axios.interceptors.request.use(function (config) {
    // 在发送请求之前做些什么
    return config;
  }, function (error) {
    // 对请求错误做些什么
    return Promise.reject(error);
  });

// 添加响应拦截器
axios.interceptors.response.use(function (response) {
    // 对响应数据做点什么
    return response;
  }, function (error) {
    // 对响应错误做点什么
    return Promise.reject(error);
  });

APIS

如下是一些axios请求支持的高级配置用法:

  • transformRequest:允许在向服务器发送前,修改请求数据,适用于’PUT’, ‘POST’ 和 ‘PATCH’ 这几个请求方法
  • transformResponse:在传递给 then/catch 前,允许修改响应数据
  • onUploadProgress:允许为上传处理进度事件
  • onDownloadProgress:为下载处理进度事件

更多全局配置如下:

{
   // `url` 是用于请求的服务器 URL
  url: '/user',

  // `method` 是创建请求时使用的方法
  method: 'get', // default

  // `baseURL` 将自动加在 `url` 前面,除非 `url` 是一个绝对 URL。
  // 它可以通过设置一个 `baseURL` 便于为 axios 实例的方法传递相对 URL
  baseURL: 'https://some-domain.com/api/',

  // `transformRequest` 允许在向服务器发送前,修改请求数据
  // 只能用在 'PUT', 'POST' 和 'PATCH' 这几个请求方法
  // 后面数组中的函数必须返回一个字符串,或 ArrayBuffer,或 Stream
  transformRequest: [function (data, headers) {
    // 对 data 进行任意转换处理
    return data;
  }],

  // `transformResponse` 在传递给 then/catch 前,允许修改响应数据
  transformResponse: [function (data) {
    // 对 data 进行任意转换处理
    return data;
  }],

  // `headers` 是即将被发送的自定义请求头
  headers: {'X-Requested-With': 'XMLHttpRequest'},

  // `params` 是即将与请求一起发送的 URL 参数
  // 必须是一个无格式对象(plain object)或 URLSearchParams 对象
  params: {
    ID: 12345
  },

   // `paramsSerializer` 是一个负责 `params` 序列化的函数
  // (e.g. https://www.npmjs.com/package/qs, http://api.jquery.com/jquery.param/)
  paramsSerializer: function(params) {
    return Qs.stringify(params, {arrayFormat: 'brackets'})
  },

  // `data` 是作为请求主体被发送的数据
  // 只适用于这些请求方法 'PUT', 'POST', 和 'PATCH'
  // 在没有设置 `transformRequest` 时,必须是以下类型之一:
  // - string, plain object, ArrayBuffer, ArrayBufferView, URLSearchParams
  // - 浏览器专属:FormData, File, Blob
  // - Node 专属: Stream
  data: {
    firstName: 'Fred'
  },

  // `timeout` 指定请求超时的毫秒数(0 表示无超时时间)
  // 如果请求话费了超过 `timeout` 的时间,请求将被中断
  timeout: 1000,

   // `withCredentials` 表示跨域请求时是否需要使用凭证
  withCredentials: false, // default

  // `adapter` 允许自定义处理请求,以使测试更轻松
  // 返回一个 promise 并应用一个有效的响应 (查阅 [response docs](#response-api)).
  adapter: function (config) {
    /* ... */
  },

 // `auth` 表示应该使用 HTTP 基础验证,并提供凭据
  // 这将设置一个 `Authorization` 头,覆写掉现有的任意使用 `headers` 设置的自定义 `Authorization`头
  auth: {
    username: 'janedoe',
    password: 's00pers3cret'
  },

   // `responseType` 表示服务器响应的数据类型,可以是 'arraybuffer', 'blob', 'document', 'json', 'text', 'stream'
  responseType: 'json', // default

  // `responseEncoding` indicates encoding to use for decoding responses
  // Note: Ignored for `responseType` of 'stream' or client-side requests
  responseEncoding: 'utf8', // default

   // `xsrfCookieName` 是用作 xsrf token 的值的cookie的名称
  xsrfCookieName: 'XSRF-TOKEN', // default

  // `xsrfHeaderName` is the name of the http header that carries the xsrf token value
  xsrfHeaderName: 'X-XSRF-TOKEN', // default

   // `onUploadProgress` 允许为上传处理进度事件
  onUploadProgress: function (progressEvent) {
    // Do whatever you want with the native progress event
  },

  // `onDownloadProgress` 允许为下载处理进度事件
  onDownloadProgress: function (progressEvent) {
    // 对原生进度事件的处理
  },

   // `maxContentLength` 定义允许的响应内容的最大尺寸
  maxContentLength: 2000,

  // `validateStatus` 定义对于给定的HTTP 响应状态码是 resolve 或 reject  promise 。如果 `validateStatus` 返回 `true` (或者设置为 `null` 或 `undefined`),promise 将被 resolve; 否则,promise 将被 rejecte
  validateStatus: function (status) {
    return status >= 200 && status < 300; // default
  },

  // `maxRedirects` 定义在 node.js 中 follow 的最大重定向数目
  // 如果设置为0,将不会 follow 任何重定向
  maxRedirects: 5, // default

  // `socketPath` defines a UNIX Socket to be used in node.js.
  // e.g. '/var/run/docker.sock' to send requests to the docker daemon.
  // Only either `socketPath` or `proxy` can be specified.
  // If both are specified, `socketPath` is used.
  socketPath: null, // default

  // `httpAgent` 和 `httpsAgent` 分别在 node.js 中用于定义在执行 http 和 https 时使用的自定义代理。允许像这样配置选项:
  // `keepAlive` 默认没有启用
  httpAgent: new http.Agent({ keepAlive: true }),
  httpsAgent: new https.Agent({ keepAlive: true }),

  // 'proxy' 定义代理服务器的主机名称和端口
  // `auth` 表示 HTTP 基础验证应当用于连接代理,并提供凭据
  // 这将会设置一个 `Proxy-Authorization` 头,覆写掉已有的通过使用 `header` 设置的自定义 `Proxy-Authorization` 头。
  proxy: {
    host: '127.0.0.1',
    port: 9000,
    auth: {
      username: 'mikeymike',
      password: 'rapunz3l'
    }
  },

  // `cancelToken` 指定用于取消请求的 cancel token
  // (查看后面的 Cancellation 这节了解更多)
  cancelToken: new CancelToken(function (cancel) {
  })
}

JS 实现Date日期格式的本地化

胖蔡阅读(31)

为了更好的更新多语言日期的显示,所以希望实现日期的本地化格式显示要求,常规的特殊字符型格式化无法满足显示要求,这里整理了几种我思考实现的本地化实现功能。

通过多方查找,总结了实现的思路主要有如下三个方向:

  • 官方基础支持:javascript自支持Intl.DateTimeFormat实现本地化
  • 三方工具:如dayjs使用‘dayjs/plugin/localeData
  • 自己实现

DateTimeFormat实现本地化

JavaScript已经提供了可以使用的本地化功能:Intl.DateTimeFormat,只需要传入当前语言和日期基本可以完成本地化的输出,如下给出一些基础的实现:

const date = new Date(Date.UTC(2012, 11, 20, 3, 0, 0));

// 假定下面输出的结果使用了洛杉矶时区(UTC-0800,太平洋标准时间)

// 美式英语 (US English) 使用  month-day-year 格式
console.log(new Intl.DateTimeFormat("en-US").format(date));
// "12/19/2012"

// 英式英语 (British English) 使用 day-month-year 格式
console.log(new Intl.DateTimeFormat("en-GB").format(date));
// "19/12/2012"

// 韩国使用 year-month-day 格式
console.log(new Intl.DateTimeFormat("ko-KR").format(date));
// "2012. 12. 19."

// 大部分阿拉伯国家使用阿拉伯字母 (real Arabic digits)
console.log(new Intl.DateTimeFormat("ar-EG").format(date));
// "١٩‏/١٢‏/٢٠١٢"

// 在日本,应用可能想要使用日本日历,
// 2012 年是平成 24 年(平成是是日本天皇明仁的年号,由 1989 年 1 月 8 日起开始计算直至现在)
console.log(new Intl.DateTimeFormat("ja-JP-u-ca-japanese").format(date));
// "24/12/19"

// 当请求可能不支持的语言,如巴厘语(ban)时,若同时指定了备用的语言,
// 那么将使用备用的语言输出(本例为印尼语(id))
console.log(new Intl.DateTimeFormat(["ban", "id"]).format(date));
// "19/12/2012"

同时,提供给我们使用options进行格式化的返回,这个很大程度已经足够使用了:

const date = new Date(Date.UTC(2012, 11, 20, 3, 0, 0));

// 请求参数 (options) 中包含参数星期 (weekday),并且该参数的值为长类型 (long)
let options = {
  weekday: "long",
  year: "numeric",
  month: "long",
  day: "numeric",
};
console.log(new Intl.DateTimeFormat("de-DE", options).format(date));
// "Donnerstag, 20. Dezember 2012"

// 应用可能需要使用世界标准时间 (UTC),并且 UTC 使用短名字 (short) 展示
options.timeZone = "UTC";
options.timeZoneName = "short";
console.log(new Intl.DateTimeFormat("en-US", options).format(date));
// "Thursday, December 20, 2012, GMT"

// 有时需要更精确的选项
options = {
  hour: "numeric",
  minute: "numeric",
  second: "numeric",
  timeZone: "Australia/Sydney",
  timeZoneName: "short",
};
console.log(new Intl.DateTimeFormat("en-AU", options).format(date));
// "2:00:00 pm AEDT"

// 再精确些...
options.fractionalSecondDigits = 3; // 秒数的有效数字数量
console.log(new Intl.DateTimeFormat("en-AU", options).format(date));
// "2:00:00.200 pm AEDT"

// 即便是美国,有时也需要使用 24 小时制
options = {
  year: "numeric",
  month: "numeric",
  day: "numeric",
  hour: "numeric",
  minute: "numeric",
  second: "numeric",
  hour12: false,
  timeZone: "America/Los_Angeles",
};
console.log(new Intl.DateTimeFormat("en-US", options).format(date));
// "12/19/2012, 19:00:00"

// 要使用选项,但是需要使用浏览器的默认区域,请使用 'default'
console.log(new Intl.DateTimeFormat("default", options).format(date));
// "12/19/2012, 19:00:00"
// 有时需要包含一天的时段
options = { hour: "numeric", dayPeriod: "short" };
console.log(new Intl.DateTimeFormat("en-US", options).format(date));
// 10 at night

将其封装成方法如下:

function formatLocalTime(date) {
  const options = {
    year: 'numeric',
    month: 'long',
  }
 
  // 我这里将lang写在html标签进行全局切换
  const formatter = new Intl.DateTimeFormat(document.documentElement.lang, options)
  return formatter.format(date)
}
const  date = new Date()
formatLocalTime(date) // 2024年3月

三方库提供的本地化

其他的日期库没了解,这里介绍dayjs库使用的本地化操作,dayjs自生无法直接进行本地化,是需要通过插件dayjs/plugin/localeData来配合实现的。

1、安装

$ npm install dayjs 
$ npm install dayjs/plugin/localeData

2、使用

// 引入 dayjs 和 locale 插件
const dayjs = require('dayjs');
const localeData = require('dayjs/plugin/localeData');
const zh = require('dayjs/locale/zh-cn'); // 需要加载对应的语言包

dayjs.extend(localeData);
dayjs.locale(zh);

const date = dayjs('2023-01-01');
console.log(date.format('MMMM D, YYYY')); // 一月 1, 2023

自己封装

原理比较简单,我们通过解析Date数据格式,使用国际化插件配置对应的本地化数据进行格式化填充即可,原理很简单,但有点费时费力,如果实在无法实现的时间格式可以考虑自己封装实现,具体实现不提供了。

Vscode中的行尾序列CRLF/LF不兼容问题

胖蔡阅读(37)

最近开发的的时候,打开项目文件经常会出现爆红错误提示信息,显示如下图:

这东西太烦人了,毕竟谁都不希望在遍地都是爆红的代码里写东西,就像能解决这个问题,根据提示可以知道这是vscode中使用的prettier插件导致的,prettier插件常被用在vscode的代码格式化上,于是乎临时就想到了两种思路来解决这个问题:

  • 解决格式问题:根据prettier要求修改格式
  • 过滤格式问题:修改prettier格式要求

解决格式问题

解决问题之前先来了解下问题产生的原因,这是由于不同操作系统上使用的换行符不一致,在 Windows 操作系统中使用的换行符通常是回车换行符 \r\n (CRLF),而LinuxUnix使用简单换行 符 \n (LF)prettier为了保证环境的一致性所设置的配置。我们要是像保持这项规则,也很简单,将文件的CRLF切换为LF就可以。

1、单文件切换

VSCODE底部提供一个行尾序列切换的功能,直接切换就行,但这种方法有个问题就是,单次只能切换一个文件,操作比较费事。

2、git配置

git有一个autocrlf配置功能,我们可以通过如下配置:

git config --global core.autocrlf false

它的作用是告诉Git在检出代码时不要自动将行尾转换为CRLF(Windows风格的换行符),而是保留原来的LF(Unix风格的换行符)。取值类型如下:

  • true:在检出代码时自动将行尾转换为CRLF,在提交代码时自动将行尾转换为LF。
  • input:在检出代码时不自动转换行尾,在提交代码时自动将行尾转换为LF
  • false:在检出和提交代码时都不自动转换行尾。

由于git安装默认会将 core.autocrlf设置为true,这样就会导致当我们使用clone将代码拉下来后换行符自动由LF转为CRLF,我们只需要将这个配置关掉,然后重写通过clone拉下来换行符就不会变为CRLF了(由于上传的时候会自动转为LF所以也不用担心仓库的行尾序列类型)。

过滤格式问题

上面的方式是通过修改格式来适配prettier,但换个思路我们也可以反其道而行,若实在是不喜欢这种方式,不考虑系统换行符的兼容问题,我们也可以直接考虑修改prettier或者eslint的配置信息彻底解决换行符问题【有关prettiereslint的关系请参考prettier/prettier】。

直接打开VSCODE的设置搜索prettier,修改prettier设置如下:

end of line修改为auto即可:

几种设置CSS渐变阴影的方法

胖蔡阅读(59)

这是我经常听到的一个问题:有可能从渐变色而不是纯色中创建阴影吗?没有特定的CSS属性可以做到这一点(相信我,我已经看过了),你发现的任何关于它的博客文章基本上都是很多CSS技巧来近似梯度。实际上,我们会边走边报道其中的一些。

但首先…另一篇关于渐变阴影的文章?真正地

是的,这是关于这个话题的另一篇帖子,但不同。我们将一起突破极限,找到一个涵盖我在其他地方从未见过的东西的解决方案:透明度。如果元素有不透明的背景,大多数技巧都有效,但如果我们有透明的背景呢?我们将在这里探讨这个案例!

在我们开始之前,让我介绍一下我的渐变阴影生成器。您所要做的就是调整配置,并获取代码。但请继续阅读,因为我将帮助您理解生成代码背后的所有逻辑。

不透明解决方案

让我们从适用于80%大多数情况的解决方案开始。最典型的情况是:你使用的是一个有背景的元素,你需要给它添加一个渐变阴影。没有透明度问题需要考虑。

解决方案是依赖于定义梯度的伪元素。将其放置在实际元素后面,并对其应用模糊过滤器。

.box {
  position: relative;
}
.box::before {
  content: "";
  position: absolute;
  inset: -5px; /* control the spread */
  transform: translate(10px, 8px); /* control the offsets */
  z-index: -1; /* place the element behind */
  background: /* your gradient here */;
  filter: blur(10px); /* control the blur */
}

它看起来像很多代码,这是因为它确实如此。以下是如果我们使用纯色而不是渐变色,我们可以用方框阴影来完成它的方法。

box-shadow: 10px 8px 10px 5px orange;

这应该能让您很好地了解第一个代码段中的值在做什么。我们有X和Y偏移、模糊半径和扩散距离。请注意,我们需要一个来自inset属性的传播距离的负值。

下面是一个演示,显示了经典长方体阴影旁边的渐变阴影:

如果你仔细观察,你会发现两个阴影都有点不同,尤其是模糊部分。这并不奇怪,因为我很确定filter属性的算法与长方体阴影的算法工作方式不同。这没什么大不了的,因为最终的结果是非常相似的。

这个解决方案很好,但仍有一些与z-index:-1声明相关的缺点。是的,那里正在发生“堆叠上下文”!

我对主要元素进行了转换,砰!阴影不再位于元素下方。这不是一个bug,而是堆栈上下文的逻辑结果。别担心,我不会开始对堆叠上下文进行无聊的解释(我已经在Stack Overflow线程中这样做了),但我仍然会向您展示如何处理它。

我建议的第一个解决方案是使用三维变换:

.box {
  position: relative;
  transform-style: preserve-3d;
}
.box::before {
  content: "";
  position: absolute;
  inset: -5px;
  transform: translate3d(10px, 8px, -1px); /* (X, Y, Z) */
  background: /* .. */;
  filter: blur(10px);
}

我们将使用沿z轴的负平移,而不是使用z索引:-1。我们将把所有内容都放在translate3d()中。不要忘记使用转换样式:preserve-3d对主要元素;否则,3D变换将不会生效。

据我所知,这个解决方案没有副作用……但也许你看到了。如果是这样的话,请在评论区分享,让我们试着找到解决方案!

如果由于某种原因无法使用三维变换,另一种解决方案是依赖于两个伪元素——:before和:after。一个创建渐变阴影,另一个再现主背景(以及您可能需要的其他样式)。这样,我们可以很容易地控制两个伪元素的堆叠顺序。

.box {
  position: relative;
  z-index: 0; /* We force a stacking context */
}
/* Creates the shadow */
.box::before {
  content: "";
  position: absolute;
  z-index: -2;
  inset: -5px;
  transform: translate(10px, 8px);
  background: /* .. */;
  filter: blur(10px);
}
/* Reproduces the main element styles */
.box::after {
  content: """;
  position: absolute;
  z-index: -1;
  inset: 0;
  /* Inherit all the decorations defined on the main element */
  background: inherit;
  border: inherit;
  box-shadow: inherit;
}

需要注意的是,我们通过在主元素上声明z-index:0或任何其他相同的属性来强制主元素创建堆栈上下文。此外,不要忘记伪元素将主元素的填充框视为引用。因此,如果主元素有边界,那么在定义伪元素样式时需要考虑到这一点。您会注意到,我使用inset:-2px on::after来说明在主元素上定义的边界。

正如我所说,在大多数需要渐变阴影的情况下,只要不需要支持透明度,这种解决方案可能就足够了。但我们在这里是为了挑战和突破极限,所以即使你不需要接下来的内容,也请和我呆在一起。你可能会学到新的CSS技巧,可以在其他地方使用。

透明解决方案

让我们从3D变换的中断处开始,并从主元素中删除背景。我将从一个偏移量和扩散距离都等于0的阴影开始。

其想法是找到一种方法来剪切或隐藏元素区域内(绿色边界内)的所有内容,同时保留外部内容。我们将使用剪辑路径。但您可能想知道剪辑路径是如何在元素内部进行剪切的。

事实上,没有办法做到这一点,但我们可以使用特定的多边形模式来模拟它:

clip-path: polygon(-100vmax -100vmax,100vmax -100vmax,100vmax 100vmax,-100vmax 100vmax,-100vmax -100vmax,0 0,0 100%,100% 100%,100% 0,0 0)

我们有一个支持透明度的渐变阴影。我们所做的只是在前面的代码中添加一个剪辑路径。这是一个图来说明多边形部分。

蓝色区域是应用片段路径后的可见部分。我只是用蓝色来说明这个概念,但实际上,我们只会看到那个区域内的阴影。正如你所看到的,我们定义了四个值很大的点(B)。我的最大值是100vmax,但它可以是你想要的任何大值。这个想法是为了确保我们有足够的空间来放置阴影。我们还有四个点,它们是伪元素的角。

箭头显示了定义多边形的路径。我们从(-B,-B)开始,直到到达(0,0)。我们总共需要10分。不是八个点,因为两个点在路径中重复两次((-B,-B)和(0,0))。

我们还有一件事要做,那就是考虑传播距离和偏移量。上面的演示之所以有效,是因为这是一个偏移和扩展距离等于0的特殊情况。

让我们定义传播,看看会发生什么。请记住,我们使用带有负值的insert来执行此操作:

伪元素现在比主元素大,所以剪辑路径的剪切量超过了我们的需要。记住,我们总是需要剪切主元素内部的部分(示例中绿色边界内的区域)。我们需要调整剪辑路径内四个点的位置。

.box {
  --s: 10px; /* the spread  */
  position: relative;
}
.box::before {
  inset: calc(-1 * var(--s));
  clip-path: polygon(
    -100vmax -100vmax,
     100vmax -100vmax,
     100vmax 100vmax,
    -100vmax 100vmax,
    -100vmax -100vmax,
    calc(0px  + var(--s)) calc(0px  + var(--s)),
    calc(0px  + var(--s)) calc(100% - var(--s)),
    calc(100% - var(--s)) calc(100% - var(--s)),
    calc(100% - var(--s)) calc(0px  + var(--s)),
    calc(0px  + var(--s)) calc(0px  + var(--s))
  );
}

我们已经为展开距离定义了一个CSS变量–s,并更新了多边形点。我没有触及我使用大值的地方。我只更新定义伪元素角的点。我将所有零值增加-s,将100%的值减少-s。

偏移也是同样的逻辑。当我们平移伪元素时,阴影不对齐,我们需要再次校正多边形并将点向相反的方向移动。

.box {
  --s: 10px; /* the spread */
  --x: 10px; /* X offset */
  --y: 8px;  /* Y offset */
  position: relative;
}
.box::before {
  inset: calc(-1 * var(--s));
  transform: translate3d(var(--x), var(--y), -1px);
  clip-path: polygon(
    -100vmax -100vmax,
     100vmax -100vmax,
     100vmax 100vmax,
    -100vmax 100vmax,
    -100vmax -100vmax,
    calc(0px  + var(--s) - var(--x)) calc(0px  + var(--s) - var(--y)),
    calc(0px  + var(--s) - var(--x)) calc(100% - var(--s) - var(--y)),
    calc(100% - var(--s) - var(--x)) calc(100% - var(--s) - var(--y)),
    calc(100% - var(--s) - var(--x)) calc(0px  + var(--s) - var(--y)),
    calc(0px  + var(--s) - var(--x)) calc(0px  + var(--s) - var(--y))
  );
}

偏移还有两个变量:-x和-y。我们在变换中使用它们,还更新剪辑路径值。我们仍然不接触具有大值的多边形点,但我们偏移了所有其他点——我们从x坐标减少了-x,从y坐标减少了-y。

现在我们所要做的就是更新一些变量来控制渐变阴影。当我们处理它的时候,让我们也让模糊半径成为一个变量:

这完全取决于边界。不要忘记伪元素的引用是填充框,所以如果将边界应用于主元素,则会出现重叠。可以保留三维变换技巧,也可以更新插入值以考虑边界。

这是之前的演示,使用更新的插入值代替3D变换:

我想说这是一种更合适的方式,因为传播距离将更准确,因为它从边界框而不是填充框开始。但您需要根据主元素的边界调整插入值。有时,元素的边界是未知的,您必须使用以前的解决方案。

使用早期的不透明解决方案,您可能会面临堆叠上下文问题。有了透明的解决方案,你可能会面临边境问题。现在你有了解决这些问题的选择和方法。3D转换技巧是我最喜欢的解决方案,因为它解决了所有问题(在线生成器也会考虑它)

添加边界半径

如果在使用我们开始使用的不透明解决方案时尝试向元素添加边界半径,那么这是一项相当琐碎的任务。您所需要做的就是从main元素继承相同的值,就完成了。

即使你没有边界半径,定义边界半径也是个好主意:inherit。这说明了你以后可能想添加的任何潜在边界半径或来自其他地方的边界半径。

在处理透明解决方案时,情况就不同了。不幸的是,这意味着要找到另一个解决方案,因为剪辑路径无法处理曲率。这意味着我们将无法剪切主元素内部的区域。

我们将在混合物中引入遮罩特性。

这部分非常乏味,我很难找到一个不依赖幻数的通用解决方案。我最终得到了一个非常复杂的解决方案,它只使用了一个伪元素,但代码只是一块意大利面条,只涵盖了少数特定情况。我认为这条路不值得探索。

为了简化代码,我决定插入一个额外的元素。以下是标记:

<div class="box">
  <sh></sh>
</div>

我正在使用一个自定义元素,以避免与外部CSS发生任何潜在冲突。我本可以使用<div>,但由于它是一个公共元素,它很容易被来自其他地方的另一个CSS规则所攻击,这可能会破坏我们的代码。

第一步是定位元素并有意创建溢出:

.box {
  --r: 50px;
  position: relative;
  border-radius: var(--r);
}
.box sh {
  position: absolute;
  inset: -150px;
  border: 150px solid #0000;
  border-radius: calc(150px + var(--r));
}

代码看起来可能有点奇怪,但我们将在进行过程中了解其背后的逻辑。接下来,我们使用的伪元素创建渐变阴影。

.box {
  --r: 50px;
  position: relative;
  border-radius: var(--r);
  transform-style: preserve-3d;
}
.box sh {
  position: absolute;
  inset: -150px;
  border: 150px solid #0000;
  border-radius: calc(150px + var(--r));
  transform: translateZ(-1px)
}
.box sh::before {
  content: "";
  position: absolute;
  inset: -5px;
  border-radius: var(--r);
  background: /* Your gradient */;
  filter: blur(10px);
  transform: translate(10px,8px);
}

正如您所看到的,pseudo元素使用与前面所有示例相同的代码。唯一的区别是在元素而不是伪元素上定义的3D变换。目前,我们有一个没有透明度功能的渐变阴影:

请注意,元素的区域是用黑色轮廓定义的。我为什么这么做?因为这样,我就可以在上面戴上口罩,将溢出的部分隐藏在绿色区域内,并将其保留在我们需要看到阴影的地方。

我知道这有点棘手,但与剪辑路径不同,mask属性不考虑元素外部的区域来显示和隐藏内容。这就是为什么我有义务引入额外的元素——模拟“外部”区域。

另外,请注意,我正在使用边框和插图的组合来定义该区域。这允许我保持该额外元素的填充框与主元素相同,这样伪元素就不需要额外的计算。

我们从使用额外元素中得到的另一个有用的东西是,元素是固定的,只有伪元素在移动(使用translate)。这将使我能够轻松定义掩码,这是这个技巧的最后一步。

mask:
  linear-gradient(#000 0 0) content-box,
  linear-gradient(#000 0 0);
mask-composite: exclude;

完成了!我们有我们的梯度阴影,它支持边界半径!您可能期望一个具有大量梯度的复杂遮罩值,但没有!我们只需要两个简单的梯度和一个掩模组合就可以完成魔术。

让我们隔离元素,以了解那里发生了什么:

.box sh {
  position: absolute;
  inset: -150px;
  border: 150px solid red;
  background: lightblue;
  border-radius: calc(150px + var(--r));
}

这是我们得到的:

请注意内部半径如何与主元素的边界半径相匹配。我定义了一个大边界(150px)和一个等于大边界加上主元素半径的边界半径。在外面,我有一个等于150px+R的半径。在里面,我有150px+R-150px=R

我们必须隐藏内部(蓝色)部分,并确保边框(红色)部分仍然可见。为此,我定义了两个遮罩层——一个仅覆盖内容框区域,另一个覆盖边框区域(默认值)。然后我把一个排除在另一个之外,以揭示边界。

mask:
  linear-gradient(#000 0 0) content-box,
  linear-gradient(#000 0 0);
mask-composite: exclude;

修复相对容易:为<sh>元素的插入添加边框的宽度。

.box {
  --r: 50px;
  border-radius: var(--r);
  border: 2px solid;
}
.box sh {
  position: absolute;
  inset: -152px; /* 150px + 2px */
  border: 150px solid #0000;
  border-radius: calc(150px + var(--r));
}

另一个缺点是我们对边界使用的值太大(本例中为150px)。这个值应该足够大以包含阴影,但不能太大以避免溢出和滚动条问题。幸运的是,在线生成器将考虑所有参数来计算最优值。

我知道的最后一个缺点是,当您使用复杂的边界半径时。例如,如果希望将不同的半径应用于每个角,则必须为每条边定义一个变量。我想,这并不是一个真正的缺点,但它会使代码更难维护。

.box {
  --r-top: 10px;
  --r-right: 40px;
  --r-bottom: 30px;
  --r-left: 20px;
  border-radius: var(--r-top) var(--r-right) var(--r-bottom) var(--r-left);
}
.box sh {
  border-radius: calc(150px + var(--r-top)) calc(150px + var(--r-right)) calc(150px + var(--r-bottom)) calc(150px + var(--r-left));
}
.box sh:before {
  border-radius: var(--r-top) var(--r-right) var(--r-bottom) var(--r-left);
}

为了简单起见,在线生成器只考虑统一的半径,但如果您想考虑复杂的半径配置,现在您已经知道如何修改代码了。

总结

我们已经到了终点!渐变阴影背后的魔力不再是个谜。我试图涵盖你可能面临的所有可能性和任何可能的问题。如果我错过了什么,或者你发现了任何问题,请随时在评论区报告,我会查看的。

同样,考虑到事实上的解决方案将覆盖您的大多数用例,很多这可能都是过头了。尽管如此,了解这个技巧背后的“为什么”和“如何”,以及如何克服它的局限性,还是很好的。此外,我们还进行了很好的CSS剪辑和屏蔽练习。

MikroORM一款基于数据映射器、工作单元和身份映射模式的Node.js的TypeScript ORM

胖蔡阅读(70)

介绍

MikroORM是基于数据映射器、工作单元和身份映射模式的Node.jsTypeScript ORM。在本指南中,您将了解这些单词的含义、如何建立一个简单的API项目、如何测试它等等。

本指南的目的是展示MikroORM最重要的功能以及一些更小众的功能。它将引导您使用以下技术为博客创建一个简单的API

  • SQLite驱动程序的MikroORM
  • Fastify作为网络框架
  • 测试用Vitest
  • ECMAScript模块
  • JWT身份验证
  • 通过ts变形进行反射

ORM由几个包组成,我们将使用其中的重要包:

  • @mikro-orm/core: 带有ORM代码的主包
  • @mikro-orm/cli: CLI程序包,需要在本地安装
  • @mikro-orm/sqlite: sqlite驱动程序包(您也可以使用不同的驱动程序)
  • @mikro-orm/reflection: 启用具有ts变形反射的DRY实体
  • @mikro-orm/migrations: 用于管理架构迁移的包
  • @mikro-orm/seeder: 用于为数据库设定测试数据种子的包

核心和驱动程序包是必需的,此列表的其余部分是可选的,如果您愿意,可以是开发依赖项。我们将使用sqlite驱动程序,主要是为了简单起见,因为它不需要任何额外的设置,并且提供了一个方便的内存数据库,我们将在测试中使用它。

还有更多的程序包,有些程序包也位于mikro-orm/mikro-ormmonorepo之外,如@mikro-or/nestjs@mikro-orm/sql-highlighter-与monorepo中的程序包不同,这些程序包通常具有不同的版本行。

当前可用驱动程序的完整列表:

  • @mikro-orm/mysql
  • @mikro-orm/mariadb
  • @mikro-orm/postgresql
  • @mikro-orm/sqlite
  • @mikro-orm/better-sqlite
  • @mikro-orm/mongodb

安装

首先通过您选择的软件包管理器安装模块,可以使用如下任一工具。也不要忘记安装数据库驱动程序:

1、使用npm

# for mongodb
npm install @mikro-orm/core @mikro-orm/mongodb

# for mysql (works with mariadb too)
npm install @mikro-orm/core @mikro-orm/mysql

# for mariadb (works with mysql too)
npm install @mikro-orm/core @mikro-orm/mariadb

# for postgresql (works with cockroachdb too)
npm install @mikro-orm/core @mikro-orm/postgresql

# for sqlite
npm install @mikro-orm/core @mikro-orm/sqlite

# for better-sqlite
npm install @mikro-orm/core @mikro-orm/better-sqlite

2、使用yarn

# for mongodb
yarn add @mikro-orm/core @mikro-orm/mongodb

# for mysql (works with mariadb too)
yarn add @mikro-orm/core @mikro-orm/mysql

# for mariadb (works with mysql too)
yarn add @mikro-orm/core @mikro-orm/mariadb

# for postgresql (works with cockroachdb too)
yarn add @mikro-orm/core @mikro-orm/postgresql

# for sqlite
yarn add @mikro-orm/core @mikro-orm/sqlite

# for better-sqlite
yarn add @mikro-orm/core @mikro-orm/better-sqlite

3、使用pnpm

# for mongodb
pnpm add @mikro-orm/core @mikro-orm/mongodb

# for mysql (works with mariadb too)
pnpm add @mikro-orm/core @mikro-orm/mysql

# for mariadb (works with mysql too)
pnpm add @mikro-orm/core @mikro-orm/mariadb

# for postgresql (works with cockroachdb too)
pnpm add @mikro-orm/core @mikro-orm/postgresql

# for sqlite
pnpm add @mikro-orm/core @mikro-orm/sqlite

# for better-sqlite
pnpm add @mikro-orm/core @mikro-orm/better-sqlite

接下来,您需要通过以下方式在tsconfig.json中启用对decorator以及esModuleInterop的支持(装饰器是选择性的,如果您使用不同的方式来定义实体元数据(如EntitySchema),则不需要启用它们。):

"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"esModuleInterop": true

然后调用MikroORM.init作为引导应用程序的一部分:

import { MikroORM } from '@mikro-orm/postgresql'; // or any other driver package

const orm = await MikroORM.init({
  entities: ['./dist/entities'], // path to your JS entities (dist), relative to `baseDir`
  dbName: 'my-db-name',
});
console.log(orm.em); // access EntityManager via `em` property

要访问特定于驱动程序的方法,如em.createQueryBuilder(),您需要从驱动程序包导入MikroORM/EntityManager/EntityRepository类。或者,您可以将orm.em强制转换为从驱动程序包导出的EntityManager

import { EntityManager } from '@mikro-orm/postgresql';
const em = orm.em as EntityManager;
const qb = em.createQueryBuilder(...);

基于文件位置配置entity

还可以通过实体阵列提供存储实体的路径。路径是通过内部globby解析的,因此可以使用globbing模式,包括负globs

const orm = await MikroORM.init({
  entities: ['./dist/app/**/*.entity.js'],  // 基于项目目录配置最终部署文件位置
  entitiesTs: ['./src/app/**/*.entity.ts'], // 基于当前项目目录配置ts所在源码位置
  // ...
});

如果在基于文件夹的查找中遇到问题,请尝试使用mikro orm debug CLI命令检查实际使用的路径。

TypeScript中的实体配置

默认的元数据提供程序是ReflectMetadataProvider。如果要使用基于ts-morph的发现(通过编译器API读取实际的ts类型),则需要安装@mikro-orm/reflection包。

import { MikroORM } from '@mikro-orm/postgresql';
import { TsMorphMetadataProvider } from '@mikro-orm/reflection';

const orm = await MikroORM.init({
  metadataProvider: TsMorphMetadataProvider,
  // ...
});

您也可以使用不同的默认ReflectMetadataProvider,甚至可以编写自定义的ReflectMetadata Provider。使用EntitySchema是定义实体的另一种方式,完全不依赖于元数据提供程序。

import { MikroORM } from '@mikro-orm/postgresql';

const orm = await MikroORM.init({
  // default since v4, so not needed to specify explicitly
  metadataProvider: ReflectMetadataProvider,
  // ...
});

同步初始化

与异步MikroORM.init方法相反,您可以更喜欢使用同步变体initSync。这种方法有一些局限性:

  • 数据库连接将在您第一次与数据库交互时建立(或者您可以显式使用orm.connect()
  • 没有加载配置文件,options参数是必需的
  • 不支持基于文件夹的发现
  • 不检查不匹配的包版本

RequestContext 

现在,您需要为每个请求派生实体管理器,这样它们的标识映射就不会发生冲突。要执行此操作,请使用RequestContext帮助程序:

const app = express();

app.use((req, res, next) => {
  RequestContext.create(orm.em, next);
});

您应该在请求处理程序之前和任何使用ORM的自定义中间件之前将此中间件注册为最后一个中间件。当您在queryParserbodyParser等请求处理中间件之前注册它时,可能会出现问题,所以一定要在它们之后注册上下文。

Entity定义

现在,您可以开始定义Entity(在其中一个Entity文件夹中)。如下是一个简单Entity定义的实例:

// ./entities/Book.ts
@Entity()
export class Book {

  @PrimaryKey()
  id: bigint;

  @Property()
  title: string;

  @ManyToOne(() => Author)
  author: Author;

  @ManyToMany(() => BookTag)
  tags = new Collection<BookTag>(this);

  constructor(title: string, author: Author) {
    this.title = title;
    this.author = author;
  }

}

或者,如果要使用UUID主键:

// ./entities/Book.ts
import { v4 } from 'uuid';

@Entity()
export class Book {

  @PrimaryKey({ type: 'uuid' })
  uuid = v4();

  // ...

}

EntityManager

定义实体后,可以通过EntityManager开始使用ORM

若要将实体状态保存到数据库,您需要将其持久化。persist确定是使用insert还是update,并计算适当的更改集。尚未持久化(没有标识符)的实体引用将自动级联持久化。

// use constructors in your entities for required parameters
const author = new Author('Jon Snow', 'snow@wall.st');
author.born = new Date();

const publisher = new Publisher('7K publisher');

const book1 = new Book('My Life on The Wall, part 1', author);
book1.publisher = publisher;
const book2 = new Book('My Life on The Wall, part 2', author);
book2.publisher = publisher;
const book3 = new Book('My Life on The Wall, part 3', author);
book3.publisher = publisher;

// just persist books, author and publisher will be automatically cascade persisted
await em.persist([book1, book2, book3]).flush();

要从数据库中获取实体,可以使用EntityManagerfind()findOne()

const authors = em.find(Author, {});

for (const author of authors) {
  console.log(author); // instance of Author entity
  console.log(author.name); // Jon Snow

  for (const book of author.books) { // iterating books collection
    console.log(book); // instance of Book entity
    console.log(book.title); // My Life on The Wall, part 1/2/3
  }
}

设置命令行工具

MikroORM附带了许多在开发过程中非常有用的命令行工具,如SchemaGeneratorEntityGenerator。您可以从NPM二进制目录调用此命令,也可以使用npx

# install the CLI package first!
$ yarn add @mikro-orm/cli

# manually
$ node node_modules/.bin/mikro-orm

# via npx
$ npx mikro-orm

# or via yarn
$ yarn mikro-orm

为了使CLI能够访问您的数据库,您需要创建导出orm配置的mikro-orm.config.js文件。

要启用TypeScript支持,请将useTsNode标志添加到package.json文件的mikro-orm部分。默认情况下,当未启用useTsNode时,CLI将忽略.ts文件,因此,如果您想排除这种行为,请启用alwaysAllowTs选项。如果您想将MikroORMBun一起使用,这将非常有用,Bun具有开箱即用的TypeScript支持。

您还可以在package.json中设置mikro-orm.config.*文件的可能路径阵列,以及使用不同的文件名。package.json文件可以位于当前工作目录中,也可以位于其父文件夹中。

// package.json
{
  "name": "your-app",
  "dependencies": { ... },
  "mikro-orm": {
    "useTsNode": true,
    "configPaths": [
      "./src/mikro-orm.config.ts",
      "./dist/mikro-orm.config.js"
    ]
  }
}

控制这些CLI相关设置的另一种方法是使用环境变量:

  • MIKRO_ORM_CLI_CONFIGORM配置文件的路径
  • MIKRO_ORM_CLI_USE_TS_NODE:为TypeScript支持注册TS节点
  • MIKRO_ORM_CLI_TS_CONFIG_PATHtsconfig.json的路径(用于TS节点)
  • MIKRO_ORM_CLI_ALWAYS_ALLOW_TS:启用不使用TS节点的.TS文件
  • MIKRO_ORM_CLI_VERBOSE:启用详细日志记录(例如,打印种子程序或模式困难中使用的查询)

或者,您也可以通过--config选项指定配置路径:

$ npx mikro-orm debug --config ./my-config.ts

当您运行应用程序时(只要它是process.argv的一部分),而不仅仅是当您使用CLI时,–config标志也会受到尊重。

MikroORM将始终尝试根据configPaths中的顺序加载第一个可用的配置文件。如果禁用了useTsNode,或者尚未注册或检测到ts节点,则ts配置文件将被忽略。

创建配置对象的首选方式是使用defineConfig助手。即使在JavaScript文件中,它也将提供智能感知,而不需要通过jsdoc进行类型提示:

import { defineConfig } from '@mikro-orm/sqlite';

export default defineConfig({
  entities: [Author, Book, BookTag],
  dbName: 'my-db-name',
  // this is inferred as you import `defineConfig` from sqlite package
  // driver: SqliteDriver,
});

如果从驱动程序包导入帮助程序,则使用defineConfig还会自动为您推断驱动程序选项。这意味着您不必显式地提供驱动程序选项。

或者,可以使用“选项”类型:

// ./src/mikro-orm.config.ts
import { Options } from '@mikro-orm/sqlite';

const config: Options = {
  entities: [Author, Book, BookTag],
  dbName: 'my-db-name',
  driver: SqliteDriver,
};

export default config;

正确设置CLI配置后,可以省略MikroORM.init()options参数,CLI配置将自动使用。如果您使用使用摇树的捆扎机,此过程可能会失败。由于配置文件没有在任何地方静态引用,因此不会对其进行编译,因此最好的方法是显式提供配置:

import config from './mikro-orm.config';
const orm = await MikroORM.init(config);

现在您应该可以开始使用CLI了。CLI帮助中列出了所有可用的命令:

$ npx mikro-orm

Usage: mikro-orm <command> [options]

Commands:
  mikro-orm cache:clear             Clear metadata cache
  mikro-orm cache:generate          Generate metadata cache
  mikro-orm generate-entities       Generate entities based on current database
                                    schema
  mikro-orm database:create         Create your database if it does not exist
  mikro-orm database:import <file>  Imports the SQL file to the database
  mikro-orm seeder:run              Seed the database using the seeder class
  mikro-orm seeder:create <seeder>  Create a new seeder class
  mikro-orm schema:create           Create database schema based on current
                                    metadata
  mikro-orm schema:drop             Drop database schema based on current
                                    metadata
  mikro-orm schema:update           Update database schema based on current
                                    metadata
  mikro-orm schema:fresh            Drop and recreate database schema based on
                                    current metadata
  mikro-orm migration:create        Create new migration with current schema
                                    diff
  mikro-orm migration:up            Migrate up to the latest version
  mikro-orm migration:down          Migrate one step down
  mikro-orm migration:list          List all executed migrations
  mikro-orm migration:check         Check if migrations are needed. Useful for
                                    bash scripts.
  mikro-orm migration:pending       List all pending migrations
  mikro-orm migration:fresh         Clear the database and rerun all migrations
  mikro-orm debug                   Debug CLI configuration

Options:
      --config   Set path to the ORM configuration file                 [string]
  -v, --version  Show version number                                   [boolean]
  -h, --help     Show help                                             [boolean]

Examples:
  mikro-orm schema:update --run  Runs schema synchronization

bpmn-js中实现shape的内置属性、节点的默认配置

胖蔡阅读(60)

bpmn-js 阅读指南:

bpmn-js中使用elementfactory模块来构建一个元素的结构,其构建构成和元素属性的组成可参考:聊一聊bpmn-js中的elementFactory模块。构建元素的属性会自动帮我们生成一个对应类型的shapeId,其余属性均为空,需要我们后续手动添加。

ElementFactory.prototype.create = function(type, attrs) {
  attrs = assign({}, attrs || {});
  if (!attrs.id) {
    attrs.id = type + '_' + (this._uid++); // 自动生成id
  }
  return create(type, attrs);
};

为了方便用户操作和隐藏某些固定配置信息,现在我希望在用户创建的时候就将某些配置信息固定配置进入对应的shape,以节省流程编辑器制作时间,也防止某些敏感配置出现不可预知的错误配置。通过对bpmn-js的结构的了解和分析,针对不同使用常见我总结了两种方式对齐进行配置的注入操作。

期望达到的效果

对于配置注入的最终结果有如下描述:

  • 单个组件创建会自动配置一个默认名称
  • bpmn:UserTask配置默认的任务监听器
  • 配置内置扩展属性

通过EventBus实现

bpmn-js使用内置的eventbus事件总线的方式进行事件的监听和传递,我们可以借助事件总线中的内置事件选择合适的时机对元素进行属性配置操作。这种方式的好处就是不需要额外去修改palettecontextPad插件即可实现,侵入性小,操作比较独立。

通过eventbus实现功能前,先来了解下几个比较重要的内置事件:

  • element.changed:选中元素发生改变【所有类型元素】
  • shape.addedshape类型新增元素事件,当首次导入的时候,每个shape的新增也都会触发
  • shape.removedshape类型图形移除事件
  • import.parse.startxml开始导入事件
  • import.donexml导入结束事件

如下是bpmn-js中这个几个事件的执行顺序图示:

需要注意的几个点:

  • shape.added的时机有两个地方:一个是导入加载已有数据,一个是新增shape的时候
  • shape的属性写入必须要在element.changed之后操作才可生效,shape.added监听后不可直接对element进行操作

几个写入的方法

根据上述了解,我们需要实现如下几个功能:

如下为我封装的几个写入操作的函数,代码如下:


// 创建一个元素id
export function uuid(
  length = 8,
  chars: any = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
) {
  let result = ''
  const charsString = chars
  for (let i = length; i > 0; --i)
    result += charsString[Math.floor(Math.random() * charsString.length)]
  return result
}
/**
 * 更新bpmn-js中元素的属性更新操作
 * @param modeler bpmn-js中的操作对象modeler
 * @param element 当前待操作的元素
 * @param key 需要更新的key
 * @param value 更新后的value
 */
export const updateProperty = (modeler: any, element: any, key: string, value: any) => {
  const modeling = modeler.get('modeling') // 依据didi设计的插件获取方式

  const attrObj = Object.create(null)
  attrObj[key] = value

  if (element && element[key] === attrObj[key]) {
    console.log('属性值未发生改变,请忽略:', element[key])
    return
  }
  if (modeling && element) {
    if (key === 'id') {
      // 更新属性
      modeling.updateProperties(element, {
        id: value,
        di: { id: `${value}_di` },
      })
    }
    else {
      modeling.updateProperties(element, attrObj)
    }
  }
}

/**
 * 添加扩展属性
 * @param modeler bpmn-js中的操作对象modeler
 * @param element 当前待操作的元素
 * @param key 需要更新的key
 * @param value 更新后的value
 */
export const addExtensionProperty = (modeler: any, element: any, key: string, value: any) => {
  const modeling = modeler.get('modeling') // 依据didi设计的插件获取方式
  const elementRegistry = modeler.get('elementRegistry')
  const moddle = modeler.get('moddle')
  const targetElement = elementRegistry.get(element.id)
  if (!targetElement)
    return
  // bpmn:properties
  const otherPropertis: any = []
  const properties = targetElement.businessObject?.extensionElements?.values.filter((ex: any) => {
    const type = ex.$type.split(':')[1] || ''
    if (type !== 'Properties')
      otherPropertis.push(ex)
    return type === 'Properties'
  }) ?? []
  const values: any[] = properties.reduce((last: any[], current: any) => last.concat(current.values), [])

  const current = values.find((item: any) => item.name === key) // 存在当前key

  if (current) {
    // 当前key已存在,需要进行更新
    modeling.updateModdleProperties(targetElement, current, { name: key, value })
  }
  else {
    // 当前key不存在,需要创建一个
    const newPropertyObject = moddle.create('flowable:Property', { name: key, value })
    const propertiesObject = moddle.create(('flowable:Properties'), {
      values: values.concat([newPropertyObject]),
    })

    const extensionElements = moddle.create('bpmn:ExtensionElements', {
      values: otherPropertis.concat([propertiesObject]),
    })
    modeling.updateProperties(targetElement, {
      extensionElements,
    })
  }
}

const createScriptObject = (moddle: any, options: any) => {
  const { scriptType, scriptFormat, value, resource } = options
  const scriptConfig: any = { scriptFormat }
  if (scriptType === 'inlineScript')
    scriptConfig.value = value
  else scriptConfig.resource = resource

  return moddle.create('flowable:Script', scriptConfig)
}

/**
 * 添加任务监听器
 * @param modeler bpmn-js中的操作对象modeler
 * @param element 当前待操作的元素
 * @param model
 */
export const addTaskListenerProperty = (modeler: any, element: any, model: any) => {
  const modeling = modeler.get('modeling') // 依据didi设计的插件获取方式
  const elementRegistry = modeler.get('elementRegistry')
  const moddle = modeler.get('moddle')
  const targetElement = elementRegistry.get(element.id)
  if (!targetElement)
    return
  const otherExtensionList: any[] = []
  const properties = targetElement.businessObject?.extensionElements?.values?.filter(
    (ex: any) => {
      if (ex.$type !== 'flowable:TaskListener')
        otherExtensionList.push(ex)
      return ex.$type === 'flowable:TaskListener'
    },
  ) ?? []

  const listenerObj = Object.create(null)
  listenerObj.event = model.event
  switch (model.listenerType) {
    case 'scriptListener':
      listenerObj.script = createScriptObject(moddle, model)
      break

    case 'expressionListener':
      listenerObj.expression = model.expression
      break

    case 'delegateExpressionListener':
      listenerObj.delegateExpression = model.delegateExpression
      break

    default:
      listenerObj.class = model.class
  }

  if (model.event === 'timeout' && !!model.eventDefinitionType) {
    // 超时定时器
    const timeDefinition = moddle.create('bpmn:FormalExpression', {
      body: model.eventTimeDefinitions,
    })
    const TimerEventDefinition = moddle.create('bpmn:TimerEventDefinition', {
      id: `TimerEventDefinition_${uuid(8)}`,
      [`time${model.eventDefinitionType.replace(/^\S/, (s: string) => s.toUpperCase())}`]:
        timeDefinition,
    })
    listenerObj.eventDefinitions = [TimerEventDefinition]
  }

  const listenerObject = moddle.create('flowable:TaskListener', listenerObj)

  properties.push(listenerObject)

  const extensionElements = moddle.create('bpmn:ExtensionElements', {
    values: otherExtensionList.concat(properties),
  })
  modeling.updateProperties(targetElement, {
    extensionElements,
  })
}

实现

东风具备,接下来就是如何实现扩展属性等的创建插入了,原理就是参考上述的执行事件顺序,通过记录状态在shape.added中添加element.changed方法【原理是元素创建后会自定聚焦当前元素,会主动发起一次element.changed事件】,去除监听【可参考bpmn-js 事件总线处理了解更多事件总线的操作】,以防止重复占有元素聚焦导致逻辑死循环。伪代码实现如下:

const importDone = ref<boolean>(false) // 导入状态

....
// 确认导入是否完成
modeler.on('import.parse.start', () => {
   importDone.value = false
})

modeler.on('import.done', (e: any) => {
   importDone.value = true
})

modeler.on('shape.added', (event: any) => {
 if (importDone.value) {
        // 编辑过程中新增元素
    const listener = ({ element }: any) => {
     if (element.id === event?.element.id) {
          modeler.get('eventBus').off('element.changed', listener)
          updateProperty(modeler, element, 'name', '测试节点名称')
          addExtensionProperty(modeler, element, 'test', 'property') // 添加扩展属性
          addTaskListenerProperty(modeler, element, {
            event: 'create',
            listenerType: 'classListener',
            class: 'cn.enjoytoday.bpmnClassLisener',
          }) // 添加默认执行任务
     }
   }

    modeler.get('eventBus').on('element.changed', listener)
 }
})
...

结果

使用palettecontextpad追加两种方式测试新增节点,获取xml文件如下:

结果成功!

自定义Palette和ContextPad

若是深度定制可以通过在shape创建的时候配置shape属性实现,在开始添加内置属性之前,我们先来了解下shapebpmn-js中直接创建的场景,以及内置属性创建的具体格式。

创建场景

bpmn-js提供的建模器来说内置创建元素模块主要分为两个地方:PalettecontextPad,其具体代码部分如下:

1、PaletteProvider.js

2、ContextPadProvider.js

需要注意的是追加操作有两次需要添加,一个是appendStart方法,一个是append方法,这两个方法均需要实现。

内置对象属性

上传我们可以发现其实这两个地方的实现是一样的,都是通过elementFactory.createShape来创建一个shape对象,然后通过create.start进行创建。由于createShape方法只是生成了一个id,所以为了创建内置属性配置,我们就需要自己新增属性,在开始实现之前,我们首先需要了解到shape元素的描述属性都是在shape.businessObject对象下的,如下是一个shape元素的数据结构:

由于palettecontextpad的实现本质一致,我这里就在自定义的palettepalette自定义参考:聊一聊bpmn-js中的Palette)中实现内置属性挂载。其实现代码:

// PaletteProvider.js 由于创建ModdleElement对象需要用到moddle模块,需要在inject中添加moddle
    function createListener(event) {
      const shape = elementFactory.createShape(assign({ type }, options))

      if (options) {
        !shape.businessObject.di && (shape.businessObject.di = {})
        shape.businessObject.di.isExpanded = options.isExpanded
      }

       // 这里开始是插入内置属性代码
      shape.businessObject.name = '测试节点名称'

      const testProp = moddle.create('flowable:Property', { name: 'test', value: '123' })
      const task = moddle.create('flowable:TaskListener', { event: 'creat', class: 'cn.enjoytoday.bpmnClassLisener' })
      const a = moddle.create('flowable:Properties', {
        values: [testProp],
      })
      const extensionElements = moddle.create('bpmn:ExtensionElements', {
        values: [a, task],
      })

      shape.businessObject.extensionElements = extensionElements
     //将属性插入到extensionElements中


      create.start(event, shape)
    }

结果

测试发现,可正常实现内置属性插入,得出xml文件如下:

总结

两种方式均可实现内置属性节点的配置插入,第一种方式通过适配式的方式实现,尽可能少的影响建模其的独立性和完整性,后一种方式比较直接,一次性完成创建配置,减少shape的绘制次数,但代码侵入性较高,不利于不同场景的适配共用。 具体可根据需求选择使用何种方式进行实现。

聊一聊bpmn-js中的contextpad

胖蔡阅读(76)

bpmn-js 阅读指南:

bpmn-js内置提供了一个’contextPadprovider‘右键面板,来协助我们快速创建和修改图形模块,其原理类似Palette方式,使用的是didi以插件方式来实现的动态或覆盖两种方式的创建。接下来我们就来快速了解下bpmn-js中的contextPadprovider已经如何对它进行修改定制操作。

如上图,contextPadprovider就是右键元素显示的元素操作面板区域,contextPadprovider通过当前选中元素的不同进行提示不同的操作元素显示,一辅助我们能进行更加快速的创建操作,降低非必要的左侧palette拖拽操作,提高制图效率和用户体验度。

了解context-pad

在对context-pad进行定制修改之前,我们先来了解下bpmn-js源码中的context-pad是什么样子的。如下我们了解几个比较重要的context-pad配置。

1、插件注册参数

如下为插件的基础注册配置:

export default {
  __depends__: [
    AppendPreviewModule,
    DirectEditingModule,
    ContextPadModule,
    SelectionModule,
    ConnectModule,
    CreateModule,
    PopupMenuModule
  ],
  __init__: [ 'contextPadProvider' ], // 注册插件名为:contextPadProvider
  contextPadProvider: [ 'type', ContextPadProvider ]
};

2、插件核心方法

插件的使用类似与Palette中的模式,通过diagram-js代理全局的context-pad注册与生产,自定义的ContextPadProvider插件需要满足两个条件:注册、生成。

注册

diagram-jsContextPad模块中提供registerProvider方法供我们进行contextpad提供器的注册操作,其代码如下:

//  diagram-js/lib/features/context-pad/ContextPad.js 
/**
 * 
 * 提供支持contextpad的注册,并指定优先级
 *
 * @param {number} priority
 * @param {ContextPadProvider} provider
 */
ContextPad.prototype.registerProvider = function(priority, provider) {
  if (!provider) {
    provider = priority;
    priority = DEFAULT_PRIORITY;
  }

  this._eventBus.on('contextPad.getProviders', priority, function(event) {
    event.providers.push(provider);
  });
};

// 插件内使用
 contextPad.registerProvider(this);

配置追加操作

ContextPadProvider插件提供的核心方法就两个,分别用于单个元素选中操作:getContextPadEntries,这也是最常用到的api,和批量操作getMultiElementContextPadEntries。两个方法均是返回一个map集合用于显示追加元素。方法格式如下:

// 单个操作元素
getContextPadEntries?: (element: ElementType) => ContextPadEntriesCallback<ElementType> | ContextPadEntries<ElementType>;


// 多元素操作
getMultiElementContextPadEntries?: (elements: ElementType[]) => ContextPadEntriesCallback<ElementType> | ContextPadEntries<ElementType>;

其操作位于源码位置:diagram-js/lib/features/context-pad/ContextPad.js

ContextPad.prototype.getEntries = function(target) {
  var providers = this._getProviders();

  var provideFn = isArray(target)
    ? 'getMultiElementContextPadEntries'
    : 'getContextPadEntries';

  var entries = {};

  // loop through all providers and their entries.
  // group entries by id so that overriding an entry is possible
  forEach(providers, function(provider) {

    if (!isFunction(provider[provideFn])) {
      return;
    }

    var entriesOrUpdater = provider[provideFn](target);

    if (isFunction(entriesOrUpdater)) {
      entries = entriesOrUpdater(entries);
    } else {
      forEach(entriesOrUpdater, function(entry, id) {
        entries[id] = entry;
      });
    }
  });

  return entries;
};

自定义ContextPad

通过上述的了解,可以对contextpad有个大致的了解,想要自定义contextpad,只需要两个步骤:

  • 通过contextpad注册提供器
  • 实现getContextPadEntries方法(暂不考虑多选批量操作情况),返回操作元素

根据didi插件机制的实现来分析,我们可以通过两种方式来实现我们的需求:追加contextPad和重写覆盖。

追加方式

这里使用官方提供的示例:CustomContextPad.js,如下是代码:

const SUITABILITY_SCORE_HIGH = 100,
      SUITABILITY_SCORE_AVERGE = 50,
      SUITABILITY_SCORE_LOW = 25;

export default class CustomContextPad {
  constructor(bpmnFactory, config, contextPad, create, elementFactory, injector, translate) {
    this.bpmnFactory = bpmnFactory;
    this.create = create;
    this.elementFactory = elementFactory;
    this.translate = translate;

    if (config.autoPlace !== false) {
      this.autoPlace = injector.get('autoPlace', false);
    }

    contextPad.registerProvider(this); // 注册
  }

  // 该方法提供当前element元素的contextpad配置,是一个对象格式
  getContextPadEntries(element) {
    const {
      autoPlace,
      bpmnFactory,
      create,
      elementFactory,
      translate
    } = this;

    function appendServiceTask(suitabilityScore) {
      return function(event, element) {
        if (autoPlace) {
          const businessObject = bpmnFactory.create('bpmn:Task');

          businessObject.suitable = suitabilityScore;

          const shape = elementFactory.createShape({
            type: 'bpmn:Task',
            businessObject: businessObject
          });

          autoPlace.append(element, shape);
        } else {
          appendServiceTaskStart(event, element);
        }
      };
    }

    function appendServiceTaskStart(suitabilityScore) {
      return function(event) {
        const businessObject = bpmnFactory.create('bpmn:Task');

        businessObject.suitable = suitabilityScore;

        const shape = elementFactory.createShape({
          type: 'bpmn:Task',
          businessObject: businessObject
        });

        create.start(event, shape, element);
      };
    }

    return {
      'append.low-task': {
        group: 'model',
        className: 'bpmn-icon-task red',
        title: translate('Append Task with low suitability score'),
        action: {
          click: appendServiceTask(SUITABILITY_SCORE_LOW),
          dragstart: appendServiceTaskStart(SUITABILITY_SCORE_LOW)
        }
      },
      'append.average-task': {
        group: 'model',
        className: 'bpmn-icon-task yellow', // 可以通过指定的类名通过css设置颜色
        title: translate('Append Task with average suitability score'),
        action: {
          click: appendServiceTask(SUITABILITY_SCORE_AVERGE),
          dragstart: appendServiceTaskStart(SUITABILITY_SCORE_AVERGE)
        }
      },
      'append.high-task': {
        group: 'model',
        className: 'bpmn-icon-task green',
        title: translate('Append Task with high suitability score'),
        action: {
          click: appendServiceTask(SUITABILITY_SCORE_HIGH),
          dragstart: appendServiceTaskStart(SUITABILITY_SCORE_HIGH)
        }
      }
    };
  }
}

// 需要依赖使用的插件
CustomContextPad.$inject = [
  'bpmnFactory',
  'config',
  'contextPad',
  'create',
  'elementFactory',
  'injector',
  'translate'
];

// 导出定义index.js
export default {
  __init__: [ 'customContextPad'],
  customContextPad: [ 'type', CustomContextPad ],

};

通过上述插件定义和实现后,只需要在modeler中加载就可以实现:

// 使用
import BpmnModeler from "bpmn-js/lib/Modeler";
import CustomContextPad from '../CustomContextPad'

const bpmnModeler = new BpmnModeler({
        container: this.$refs["bpmn-canvas"],
        additionalModules: [CustomContextPad ],

});

覆盖重写

覆盖重写和上述的追加方式唯一的不同就是插件的__init__定义为contextPadProvider,这样我们定义的插件就会覆盖bpmn-js中的ContextpadProvider插件。

// 导出定义index.js
export default {
  __init__: [ 'contextPadProvider'],
contextPadProvider: [ 'type', CustomContextPad ],
};

几个知识点

1、getContextPadEntries(element)返回数据格式

getContextPadEntries接收参数为当前操作的元素,返回参数格式如下:

return {
    'replace': {   // 唯一key
        group: 'edit', // 分组
        className: 'bpmn-icon-screw-wrench', // 指定类名,这里可以用于配置预览图和预留配置自定义css
        title: translate('Change type'), // hover上去显示的提示文字
        action: {         // 事件操作,常规处理dragstart和click就可以
          click(event, element) {
            console.log('get 修改类型的popup:', element)
            const position = assign(getReplaceMenuPosition(element), {
              cursor: { x: event.x, y: event.y },
            })

            popupMenu.open(element, 'bpmn-replace', position)
          },
        },
      },
}

2、group有什么用?

contextpad使用group将追加元素进行分类,同一个model的追加元素放在一起,每个model都是由一个block布局包裹,且内置将block的宽度固定死只能单行放三个元素,若希望改变大小,可以通过css修改尺寸。

我当前定义的contextpad中存在三种类型group:model、edit、connect。若追加可将新增元素放置在已配置group内,也可以单独定义一个新的group,但需要考虑布局排版是否合适。

3、replace修改元素类型的配置在哪儿?

bpmn-js中给我们提供的contextpad中有一个edit的分类操作用于存放操作型功能,这里放了两个操作:

 // 删除操作
 if (this._isDeleteAllowed(elements)) {
    assign(actions, {
      'delete': {
        group: 'edit',
        className: 'bpmn-icon-trash',
        title: this._translate('Remove'),
        action: {
          click: function(event, elements) {
            modeling.removeElements(elements.slice());
          }
        }
      }
    });
  }

 // 替换元素操作
  if (!popupMenu.isEmpty(element, 'bpmn-replace')) {

    // Replace menu entry
    assign(actions, {
      'replace': {
        group: 'edit',
        className: 'bpmn-icon-screw-wrench',
        title: translate('Change type'),
        action: {
          click: function(event, element) {

            var position = assign(getReplaceMenuPosition(element), {
              cursor: { x: event.x, y: event.y }
            });

            popupMenu.open(element, 'bpmn-replace', position, {
              title: translate('Change element'),
              width: 300,
              search: true
            });
          }
        }
      }
    });
  }

上述代码可知,修改元素的类型具体实现在popupMenu插件中。

bpmn-js通过moddle插件实现自定义元素和自定义属性

胖蔡阅读(108)

bpmn-js 阅读指南:

bpmn-js是一个基于BPMN 2.0规范定义的一个bpmn文件读写一体化编辑解决方案,其中bpmn的读写功能依托于bpmn-moddle工具库来实现。使用bpmn-moddle来进行bpmn文件的读取和写入不是随心所欲的,它又一套自己的校验标准,默认的bpmn-moddle是基于BPMN 2.0 元模型进行校验输入并生成对应的xml文件的。使用bpmn-moddle进行模型创建和维护的控制也是为了保证我们模型的元素和属性的一致性,方便我们的模型迁移后者复用。当校验不通过,我们的建模器将无法成功创建bpmn模型,bpmn-js中的显示效果如下:

BPMN 2.0 元模型

为了保证建模的一致性原则,我们所有基于BPMN 2.0实现的建模生成器,都必须要满足BPMN 2.0元模型描述说明, BPMN 2.0元模型要求必须支持如下的所有子元素和对应元素属性:

元素属性
participant (pool)id, name, processRef
laneSetid, lane with name, childLaneSet, flowElementRef
sequenceFlow (unconditional)id, name, sourceRef, targetRef
messageFlowid, name, sourceRef, targetRef
exclusiveGatewayid, name
parallelGatewayid, name
task (None)id, name
userTaskid, name
serviceTaskid, name
subProcess (expanded)id, name, flowElement
subProcess (collapsed)id, name, flowElement
CallActivityid, name, calledElement
DataObjectid, name
TextAnnotationid, text
association/dataAssociationid, name, sourceRef, targetRef, associationDirection
dataStoreReferenceid, name, dataStoreRef
startEvent (None)id, name
endEvent (None)id, name
messageStartEventid, name, messageEventDefinition
messageEndEventid, name, messageEventDefinition
timerStartEventid, name, timerEventDefinition
terminateEndEventid, name, terminateEventDefinition
documentationtext
Groupid, categoryRef

bpmn-moddle是通过描述文件进行控制元素一致性校验的,bpmn-moddle内置了一些基础的json描述文件配置,几种包含di.json、dc.json定义基础的数据辅助类型、形状、颜色的信息等,如下为bpmn的基础元素描述定义文件bpmn.json,想要了解更多内置定义可以参考:bpmn-moddle/resources/bpmn 位置下的所有描述定义文件:

{
  "name": "BPMNDI",
  "uri": "http://www.omg.org/spec/BPMN/20100524/DI",
  "prefix": "bpmndi",
  "types": [
    {
      "name": "BPMNDiagram",
      "properties": [
        {
          "name": "plane",
          "type": "BPMNPlane",
          "redefines": "di:Diagram#rootElement"
        },
        {
          "name": "labelStyle",
          "type": "BPMNLabelStyle",
          "isMany": true
        }
      ],
      "superClass": [
        "di:Diagram"
      ]
    },
    {
      "name": "BPMNPlane",
      "properties": [
        {
          "name": "bpmnElement",
          "isAttr": true,
          "isReference": true,
          "type": "bpmn:BaseElement",
          "redefines": "di:DiagramElement#modelElement"
        }
      ],
      "superClass": [
        "di:Plane"
      ]
    },
    {
      "name": "BPMNShape",
      "properties": [
        {
          "name": "bpmnElement",
          "isAttr": true,
          "isReference": true,
          "type": "bpmn:BaseElement",
          "redefines": "di:DiagramElement#modelElement"
        },
        {
          "name": "isHorizontal",
          "isAttr": true,
          "type": "Boolean"
        },
        {
          "name": "isExpanded",
          "isAttr": true,
          "type": "Boolean"
        },
        {
          "name": "isMarkerVisible",
          "isAttr": true,
          "type": "Boolean"
        },
        {
          "name": "label",
          "type": "BPMNLabel"
        },
        {
          "name": "isMessageVisible",
          "isAttr": true,
          "type": "Boolean"
        },
        {
          "name": "participantBandKind",
          "type": "ParticipantBandKind",
          "isAttr": true
        },
        {
          "name": "choreographyActivityShape",
          "type": "BPMNShape",
          "isAttr": true,
          "isReference": true
        }
      ],
      "superClass": [
        "di:LabeledShape"
      ]
    },
    {
      "name": "BPMNEdge",
      "properties": [
        {
          "name": "label",
          "type": "BPMNLabel"
        },
        {
          "name": "bpmnElement",
          "isAttr": true,
          "isReference": true,
          "type": "bpmn:BaseElement",
          "redefines": "di:DiagramElement#modelElement"
        },
        {
          "name": "sourceElement",
          "isAttr": true,
          "isReference": true,
          "type": "di:DiagramElement",
          "redefines": "di:Edge#source"
        },
        {
          "name": "targetElement",
          "isAttr": true,
          "isReference": true,
          "type": "di:DiagramElement",
          "redefines": "di:Edge#target"
        },
        {
          "name": "messageVisibleKind",
          "type": "MessageVisibleKind",
          "isAttr": true,
          "default": "initiating"
        }
      ],
      "superClass": [
        "di:LabeledEdge"
      ]
    },
    {
      "name": "BPMNLabel",
      "properties": [
        {
          "name": "labelStyle",
          "type": "BPMNLabelStyle",
          "isAttr": true,
          "isReference": true,
          "redefines": "di:DiagramElement#style"
        }
      ],
      "superClass": [
        "di:Label"
      ]
    },
    {
      "name": "BPMNLabelStyle",
      "properties": [
        {
          "name": "font",
          "type": "dc:Font"
        }
      ],
      "superClass": [
        "di:Style"
      ]
    }
  ],
  "enumerations": [
    {
      "name": "ParticipantBandKind",
      "literalValues": [
        {
          "name": "top_initiating"
        },
        {
          "name": "middle_initiating"
        },
        {
          "name": "bottom_initiating"
        },
        {
          "name": "top_non_initiating"
        },
        {
          "name": "middle_non_initiating"
        },
        {
          "name": "bottom_non_initiating"
        }
      ]
    },
    {
      "name": "MessageVisibleKind",
      "literalValues": [
        {
          "name": "initiating"
        },
        {
          "name": "non_initiating"
        }
      ]
    }
  ],
  "associations": []
}

自定义元素模型

稍微了解下就能知道支持BPMN 2.0的工作流模型并非只有一家,有最开始的activiti,以及后续分家后的camundaflowable,他们都基于BPMN 2.0之后进行的后续功能的支持,为了实现flowable或者camunda元素模型的适配,又或者进行我们自己自定义元素的扩展,我们都需要引入一个三方的元素模型配置,而bpmn-js也给我们提供了自定义加载配置的方式。bpmn-js支持在创建模型的时候通过moddleExtensions来配置自定义元素模型。

import BpmnModeler from "bpmn-js/lib/Modeler";
import flowableModdleDescriptor from '../descriptor/flowableDescriptor.json'

const bpmnModeler = new BpmnModeler({
        container: this.$refs["bpmn-canvas"],

        additionalModules: this.additionalModules,
        moddleExtensions: {
            flowable: flowableModdleDescriptor
        }, // 这里配置适配flowable
});

其中flowableDescriptor.json的描述定义内容如下:

{
"name": "Flowable",
"uri": "http://flowable.org/bpmn",
"prefix": "flowable",
"xml": {
"tagAlias": "lowerCase"
},
"associations": [],
"types": [
{
"name": "InOutBinding",
"superClass": [
"Element"
],
"isAbstract": true,
"properties": [
{
"name": "source",
"isAttr": true,
"type": "String"
},
{
"name": "sourceExpression",
"isAttr": true,
"type": "String"
},
{
"name": "target",
"isAttr": true,
"type": "String"
},
{
"name": "businessKey",
"isAttr": true,
"type": "String"
},
{
"name": "local",
"isAttr": true,
"type": "Boolean",
"default": false
},
{
"name": "variables",
"isAttr": true,
"type": "String"
}
]
},
{
"name": "In",
"superClass": [
"InOutBinding"
],
"meta": {
"allowedIn": [
"bpmn:CallActivity"
]
}
},
{
"name": "Out",
"superClass": [
"InOutBinding"
],
"meta": {
"allowedIn": [
"bpmn:CallActivity"
]
}
},
{
"name": "AsyncCapable",
"isAbstract": true,
"extends": [
"bpmn:Activity",
"bpmn:Gateway",
"bpmn:Event"
],
"properties": [
{
"name": "async",
"isAttr": true,
"type": "Boolean",
"default": false
},
{
"name": "asyncBefore",
"isAttr": true,
"type": "Boolean",
"default": false
},
{
"name": "asyncAfter",
"isAttr": true,
"type": "Boolean",
"default": false
},
{
"name": "exclusive",
"isAttr": true,
"type": "Boolean",
"default": true
}
]
},
{
"name": "JobPriorized",
"isAbstract": true,
"extends": [
"bpmn:Process",
"flowable:AsyncCapable"
],
"properties": [
{
"name": "jobPriority",
"isAttr": true,
"type": "String"
}
]
},
{
"name": "SignalEventDefinition",
"isAbstract": true,
"extends": [
"bpmn:SignalEventDefinition"
],
"properties": [
{
"name": "async",
"isAttr": true,
"type": "Boolean",
"default": false
}
]
},
{
"name": "ErrorEventDefinition",
"isAbstract": true,
"extends": [
"bpmn:ErrorEventDefinition"
],
"properties": [
{
"name": "errorCodeVariable",
"isAttr": true,
"type": "String"
},
{
"name": "errorMessageVariable",
"isAttr": true,
"type": "String"
}
]
},
{
"name": "Error",
"isAbstract": true,
"extends": [
"bpmn:Error"
],
"properties": [
{
"name": "flowable:errorMessage",
"isAttr": true,
"type": "String"
}
]
},
{
"name": "PotentialStarter",
"superClass": [
"Element"
],
"properties": [
{
"name": "resourceAssignmentExpression",
"type": "bpmn:ResourceAssignmentExpression"
}
]
},
{
"name": "FormSupported",
"isAbstract": true,
"extends": [
"bpmn:StartEvent",
"bpmn:UserTask"
],
"properties": [
{
"name": "formHandlerClass",
"isAttr": true,
"type": "String"
},
{
"name": "formKey",
"isAttr": true,
"type": "String"
},
{
"name": "localScope",
"isAttr": true,
"type": "Boolean",
"default": false
}
]
},
{
"name": "TemplateSupported",
"isAbstract": true,
"extends": [
"bpmn:Process",
"bpmn:FlowElement"
],
"properties": [
{
"name": "modelerTemplate",
"isAttr": true,
"type": "String"
}
]
},
{
"name": "Initiator",
"isAbstract": true,
"extends": [ "bpmn:StartEvent" ],
"properties": [
{
"name": "initiator",
"isAttr": true,
"type": "String"
}
]
},
{
"name": "ScriptTask",
"isAbstract": true,
"extends": [
"bpmn:ScriptTask"
],
"properties": [
{
"name": "resultVariable",
"isAttr": true,
"type": "String"
},
{
"name": "resource",
"isAttr": true,
"type": "String"
}
]
},
{
"name": "Process",
"isAbstract": true,
"extends": [
"bpmn:Process"
],
"properties": [
{
"name": "candidateStarterGroups",
"isAttr": true,
"type": "String"
},
{
"name": "candidateStarterUsers",
"isAttr": true,
"type": "String"
},
{
"name": "processCategory",
"isAttr": true,
"type": "String"
},
{
"name": "versionTag",
"isAttr": true,
"type": "String"
},
{
"name": "historyTimeToLive",
"isAttr": true,
"type": "String"
},
{
"name": "isStartableInTasklist",
"isAttr": true,
"type": "Boolean",
"default": true
}
]
},
{
"name": "EscalationEventDefinition",
"isAbstract": true,
"extends": [
"bpmn:EscalationEventDefinition"
],
"properties": [
{
"name": "escalationCodeVariable",
"isAttr": true,
"type": "String"
}
]
},
{
"name": "xcField",
"isAbstract": true,
"properties": [
{
"name": "xcString",
"isMany": true,
"type": "Element"
} ,
{
"name": "name",
"isAttr": true,
"type": "String"
}
]
},
{
"name": "xcString",
"isAbstract": true,
"properties": [
{
"name": "body",
"isBody": true,
"type": "String"
}
]
},
{
"name": "FormalExpression",
"isAbstract": true,
"extends": [
"bpmn:FormalExpression"
],
"properties": [
{
"name": "resource",
"isAttr": true,
"type": "String"
}
]
},
{
"name": "Assignable",
"extends": [ "bpmn:UserTask" ],
"properties": [
{
"name": "dataType",
"isAttr": true,
"type": "String"
},
{
"name": "assignee",
"isAttr": true,
"type": "String"
},
{
"name": "xcformKey",
"isAttr": true,
"type": "String"
},
{
"name": "candidateUsers",
"isAttr": true,
"type": "String"
},
{
"name": "candidateGroups",
"isAttr": true,
"type": "String"
},
{
"name": "text",
"isAttr": true,
"type": "String"
},
{
"name": "dueDate",
"isAttr": true,
"type": "String"
},
{
"name": "followUpDate",
"isAttr": true,
"type": "String"
},
{
"name": "priority",
"isAttr": true,
"type": "String"
}
]
},
{
"name": "CallActivity",
"extends": [ "bpmn:CallActivity" ],
"properties": [
{
"name": "calledElementBinding",
"isAttr": true,
"type": "String",
"default": "latest"
},
{
"name": "calledElementVersion",
"isAttr": true,
"type": "String"
},
{
"name": "calledElementVersionTag",
"isAttr": true,
"type": "String"
},
{
"name": "calledElementTenantId",
"isAttr": true,
"type": "String"
},
{
"name": "caseRef",
"isAttr": true,
"type": "String"
},
{
"name": "caseBinding",
"isAttr": true,
"type": "String",
"default": "latest"
},
{
"name": "caseVersion",
"isAttr": true,
"type": "String"
},
{
"name": "caseTenantId",
"isAttr": true,
"type": "String"
},
{
"name": "variableMappingClass",
"isAttr": true,
"type": "String"
},
{
"name": "variableMappingDelegateExpression",
"isAttr": true,
"type": "String"
}
]
},
{
"name": "ServiceTaskLike",
"extends": [
"bpmn:ServiceTask",
"bpmn:BusinessRuleTask",
"bpmn:SendTask",
"bpmn:MessageEventDefinition"
],
"properties": [
{
"name": "expression",
"isAttr": true,
"type": "String"
},
{
"name": "class",
"isAttr": true,
"type": "String"
},
{
"name": "delegateExpression",
"isAttr": true,
"type": "String"
},
{
"name": "resultVariable",
"isAttr": true,
"type": "String"
}
]
},
{
"name": "DmnCapable",
"extends": [
"bpmn:BusinessRuleTask"
],
"properties": [
{
"name": "decisionRef",
"isAttr": true,
"type": "String"
},
{
"name": "decisionRefBinding",
"isAttr": true,
"type": "String",
"default": "latest"
},
{
"name": "decisionRefVersion",
"isAttr": true,
"type": "String"
},
{
"name": "mapDecisionResult",
"isAttr": true,
"type": "String",
"default": "resultList"
},
{
"name": "decisionRefTenantId",
"isAttr": true,
"type": "String"
}
]
},
{
"name": "ExternalCapable",
"extends": [
"flowable:ServiceTaskLike"
],
"properties": [
{
"name": "type",
"isAttr": true,
"type": "String"
},
{
"name": "topic",
"isAttr": true,
"type": "String"
}
]
},
{
"name": "TaskPriorized",
"extends": [
"bpmn:Process",
"flowable:ExternalCapable"
],
"properties": [
{
"name": "taskPriority",
"isAttr": true,
"type": "String"
}
]
},
{
"name": "Properties",
"superClass": [
"Element"
],
"meta": {
"allowedIn": [ "*" ]
},
"properties": [
{
"name": "values",
"type": "Property",
"isMany": true
}
]
},
{
"name": "Property",
"superClass": [
"Element"
],
"properties": [
{
"name": "id",
"type": "String",
"isAttr": true
},
{
"name": "name",
"type": "String",
"isAttr": true
},
{
"name": "value",
"type": "String",
"isAttr": true
}
]
},
{
"name": "Connector",
"superClass": [
"Element"
],
"meta": {
"allowedIn": [
"flowable:ServiceTaskLike"
]
},
"properties": [
{
"name": "inputOutput",
"type": "InputOutput"
},
{
"name": "connectorId",
"type": "String"
}
]
},
{
"name": "InputOutput",
"superClass": [
"Element"
],
"meta": {
"allowedIn": [
"bpmn:FlowNode",
"flowable:Connector"
]
},
"properties": [
{
"name": "inputOutput",
"type": "InputOutput"
},
{
"name": "connectorId",
"type": "String"
},
{
"name": "inputParameters",
"isMany": true,
"type": "InputParameter"
},
{
"name": "outputParameters",
"isMany": true,
"type": "OutputParameter"
}
]
},
{
"name": "InputOutputParameter",
"properties": [
{
"name": "name",
"isAttr": true,
"type": "String"
},
{
"name": "value",
"isBody": true,
"type": "String"
},
{
"name": "definition",
"type": "InputOutputParameterDefinition"
}
]
},
{
"name": "InputOutputParameterDefinition",
"isAbstract": true
},
{
"name": "List",
"superClass": [ "InputOutputParameterDefinition" ],
"properties": [
{
"name": "items",
"isMany": true,
"type": "InputOutputParameterDefinition"
}
]
},
{
"name": "Map",
"superClass": [ "InputOutputParameterDefinition" ],
"properties": [
{
"name": "entries",
"isMany": true,
"type": "Entry"
}
]
},
{
"name": "Entry",
"properties": [
{
"name": "key",
"isAttr": true,
"type": "String"
},
{
"name": "value",
"isBody": true,
"type": "String"
},
{
"name": "definition",
"type": "InputOutputParameterDefinition"
}
]
},
{
"name": "Value",
"superClass": [
"InputOutputParameterDefinition"
],
"properties": [
{
"name": "id",
"isAttr": true,
"type": "String"
},
{
"name": "name",
"isAttr": true,
"type": "String"
},
{
"name": "value",
"isBody": true,
"type": "String"
}
]
},
{
"name": "Script",
"superClass": [ "InputOutputParameterDefinition" ],
"properties": [
{
"name": "scriptFormat",
"isAttr": true,
"type": "String"
},
{
"name": "resource",
"isAttr": true,
"type": "String"
},
{
"name": "value",
"isBody": true,
"type": "String"
}
]
},
{
"name": "Field",
"superClass": [ "Element" ],
"meta": {
"allowedIn": [
"flowable:ServiceTaskLike",
"flowable:ExecutionListener",
"flowable:TaskListener"
]
},
"properties": [
{
"name": "name",
"isAttr": true,
"type": "String"
},
{
"name": "expression",
"type": "String"
},
{
"name": "stringValue",
"isAttr": true,
"type": "String"
},
{
"name": "string",
"type": "String"
}
]
},
{
"name": "InputParameter",
"superClass": [ "InputOutputParameter" ]
},
{
"name": "OutputParameter",
"superClass": [ "InputOutputParameter" ]
},
{
"name": "Collectable",
"isAbstract": true,
"extends": [ "bpmn:MultiInstanceLoopCharacteristics" ],
"superClass": [ "flowable:AsyncCapable" ],
"properties": [
{
"name": "collection",
"isAttr": true,
"type": "String"
},
{
"name": "elementVariable",
"isAttr": true,
"type": "String"
}
]
},
{
"name": "FailedJobRetryTimeCycle",
"superClass": [ "Element" ],
"meta": {
"allowedIn": [
"flowable:AsyncCapable",
"bpmn:MultiInstanceLoopCharacteristics"
]
},
"properties": [
{
"name": "body",
"isBody": true,
"type": "String"
}
]
},
{
"name": "ExecutionListener",
"superClass": [ "Element" ],
"meta": {
"allowedIn": [
"bpmn:Task",
"bpmn:ServiceTask",
"bpmn:UserTask",
"bpmn:BusinessRuleTask",
"bpmn:ScriptTask",
"bpmn:ReceiveTask",
"bpmn:ManualTask",
"bpmn:ExclusiveGateway",
"bpmn:SequenceFlow",
"bpmn:ParallelGateway",
"bpmn:InclusiveGateway",
"bpmn:EventBasedGateway",
"bpmn:StartEvent",
"bpmn:IntermediateCatchEvent",
"bpmn:IntermediateThrowEvent",
"bpmn:EndEvent",
"bpmn:BoundaryEvent",
"bpmn:CallActivity",
"bpmn:SubProcess",
"bpmn:Process"
]
},
"properties": [
{
"name": "expression",
"isAttr": true,
"type": "String"
},
{
"name": "class",
"isAttr": true,
"type": "String"
},
{
"name": "delegateExpression",
"isAttr": true,
"type": "String"
},
{
"name": "event",
"isAttr": true,
"type": "String"
},
{
"name": "script",
"type": "Script"
},
{
"name": "fields",
"type": "Field",
"isMany": true
}
]
},
{
"name": "TaskListener",
"superClass": [ "Element" ],
"meta": {
"allowedIn": [
"bpmn:UserTask"
]
},
"properties": [
{
"name": "expression",
"isAttr": true,
"type": "String"
},
{
"name": "class",
"isAttr": true,
"type": "String"
},
{
"name": "delegateExpression",
"isAttr": true,
"type": "String"
},
{
"name": "event",
"isAttr": true,
"type": "String"
},
{
"name": "script",
"type": "Script"
},
{
"name": "fields",
"type": "Field",
"isMany": true
}
]
},
{
"name": "FormProperty",
"superClass": [ "Element" ],
"meta": {
"allowedIn": [
"bpmn:StartEvent",
"bpmn:UserTask"
]
},
"properties": [
{
"name": "id",
"type": "String",
"isAttr": true
},
{
"name": "name",
"type": "String",
"isAttr": true
},
{
"name": "type",
"type": "String",
"isAttr": true
},
{
"name": "required",
"type": "String",
"isAttr": true
},
{
"name": "readable",
"type": "String",
"isAttr": true
},
{
"name": "writable",
"type": "String",
"isAttr": true
},
{
"name": "variable",
"type": "String",
"isAttr": true
},
{
"name": "expression",
"type": "String",
"isAttr": true
},
{
"name": "datePattern",
"type": "String",
"isAttr": true
},
{
"name": "default",
"type": "String",
"isAttr": true
},
{
"name": "values",
"type": "Value",
"isMany": true
}
]
},
{
"name": "FormProperty",
"superClass": [ "Element" ],
"properties": [
{
"name": "id",
"type": "String",
"isAttr": true
},
{
"name": "label",
"type": "String",
"isAttr": true
},
{
"name": "type",
"type": "String",
"isAttr": true
},
{
"name": "datePattern",
"type": "String",
"isAttr": true
},
{
"name": "defaultValue",
"type": "String",
"isAttr": true
},
{
"name": "properties",
"type": "Properties"
},
{
"name": "validation",
"type": "Validation"
},
{
"name": "values",
"type": "Value",
"isMany": true
}
]
},
{
"name": "Validation",
"superClass": [ "Element" ],
"properties": [
{
"name": "constraints",
"type": "Constraint",
"isMany": true
}
]
},
{
"name": "Constraint",
"superClass": [ "Element" ],
"properties": [
{
"name": "name",
"type": "String",
"isAttr": true
},
{
"name": "config",
"type": "String",
"isAttr": true
}
]
},
{
"name": "ExtensionElements",
"properties": [
{
"name": "operationList",
"type": "String",
"isAttr": true
}
]
},
{
"name": "OperationList",
"superClass": [ "Element" ],
"meta": {
"allowedIn": [
"bpmn:UserTask"
]
},
"properties": [
{
"name": "operationList",
"type": "FormOperation",
"isMany": true
}
]
},
{
"name": "FormOperation",
"superClass": [ "Element" ],
"properties": [
{
"name": "id",
"type": "String",
"isAttr": true
},
{
"name": "label",
"type": "String",
"isAttr": true
},
{
"name": "type",
"type": "String",
"isAttr": true
},
{
"name": "showOrder",
"type": "String",
"isAttr": true
},
{
"name": "multiSignAssignee",
"type": "String",
"isAttr": true
}
]
},
{
"name": "VariableList",
"superClass": [ "Element" ],
"meta": {
"allowedIn": [
"bpmn:UserTask"
]
},
"properties": [
{
"name": "variableList",
"type": "FormVariable",
"isMany": true
}
]
},
{
"name": "FormVariable",
"superClass": [ "Element" ],
"properties": [
{
"name": "id",
"type": "String",
"isAttr": true
}
]
},
{
"name": "DeptPostList",
"superClass": [ "Element" ],
"meta": {
"allowedIn": [
"bpmn:UserTask"
]
},
"properties": [
{
"name": "deptPostList",
"type": "DeptPost",
"isMany": true
}
]
},
{
"name": "DeptPost",
"superClass": [ "Element" ],
"properties": [
{
"name": "id",
"type": "String",
"isAttr": true
},
{
"name": "type",
"type": "String",
"isAttr": true
},
{
"name": "postId",
"type": "String",
"isAttr": true
},
{
"name": "deptPostId",
"type": "String",
"isAttr": true
}
]
},
{
"name": "UserCandidateGroups",
"superClass": [ "Element" ],
"meta": {
"allowedIn": [
"bpmn:UserTask"
]
},
"properties": [
{
"name": "type",
"type": "String",
"isAttr": true
},
{
"name": "value",
"type": "String",
"isAttr": true
}
]
},
{
"name": "CustomCondition",
"superClass": [ "Element" ],
"meta": {
"allowedIn": [
"bpmn:SequenceFlow"
]
},
"properties": [
{
"name": "type",
"type": "String",
"isAttr": true
},
{
"name": "operationType",
"type": "String",
"isAttr": true
},
{
"name": "parallelRefuse",
"type": "Boolean",
"isAttr": true,
"default": false
}
]
},
{
"name": "AssigneeList",
"superClass": [ "Element" ],
"meta": {
"allowedIn": [
"bpmn:StartEvent",
"bpmn:UserTask"
]
},
"properties": [
{
"name": "assigneeList",
"type": "Assignee",
"isMany": true
},
{
"name": "type",
"type": "String",
"isAttr": true
}
]
},
{
"name": "Assignee",
"superClass": [ "Element" ],
"properties": [
{
"name": "id",
"type": "String",
"isAttr": true
}
]
},
{
"name": "ConditionalEventDefinition",
"isAbstract": true,
"extends": [
"bpmn:ConditionalEventDefinition"
],
"properties": [
{
"name": "variableName",
"isAttr": true,
"type": "String"
},
{
"name": "variableEvent",
"isAttr": true,
"type": "String"
}
]
}
],
"emumerations": [ ]
}

如上,即为适配flowable实现的描述文件定义,就上述描述文件内容,来具体了解下描述定义文件格式。

元素模型定义格式

元素模型的定义校验是基于moddle库实现的,就上述的定义文件flowableDescriptor.json,我们来结构下文件结构,具体了解下每部分属性和结构的具体代码含义是什么,可以看出在上述的json描述文件是由nameprefixtypesenumerationsassociationsurixml等属性组成的,各自属性

{
"name": "Flowable",
"uri": "http://flowable.org/bpmn",
"prefix": "flowable",
"xml": {
"tagAlias": "lowerCase"
},
"associations": [],
"types": [],
"enumerations": [],
}
  • prefix:是元素模型的前缀标识,用于区别不同元素模型,常见的如:bpmn、flowable、activiti、camunda等,这里我们可以定义属于我们自己的标识前缀
  • name:包名,元素模型定义包名
  • enumerationsassociations:这是设计留待后用的
  • types:这里用于定义元素模型的所有元素和属性信息
  • uri:这里是定义模型的地址,需要保证唯一,内部会对prefixuri进行唯一校验
  • xml:指定配置xml的一些格式要求

元素类型定义

types是一组由自定义元素组成的定义元素类型集合,元素定义格式如下:

    {
"name": "Definitions",
"isAbstract": true,
"extends": ["bpmn:Definitions"],
"superClass": [ "Base" ],
"properties": [
{
"name": "diagramRelationId",
"isAttr": true,
"type": "String"
},  
"meta": {
"allowedIn": [
"bpmn:StartEvent",
"bpmn:UserTask"
]
}
]
},
  • name:元素名,转化为xml则为‘<flowable:Definitions>
  • isAbstract:是否可以被实例化,若为true则可以通过 moddle.create 创建。
  • allowedIn:设置运行放在哪些元素标签内
  • properties:支持属性集合
  • superClass:类型可以通过指定superClass属性从一个或多个超类型继承。通过从超类型继承,类型继承了在超类型层次结构中声明的所有属性。继承的属性将根据它们在类型层次结构中声明的顺序显示在自己的属性之前。
  • extends:一些元模型要求它将新的属性插入到某些现有的模型元素中。这可以使用extends扩展字段来简化。

定义属性格式

如上述properties中定义的属性集合,支持定义配置如下:

  • name:属性名
  • type:属性类型,支持类型有:StringBooleanInteger 、Real或者自定义的类型
  • isMany:是否支持多个属性
  • isAttr:是否是标签属性值,当为true可以直接加到标签属性上,如: ‘<flowable:Definitions diagramRelationId ='333' > </flowable:Definitions>
  • isBody:是否作为子元素包裹,相对于isAttr属性,该属性包裹name中的内容,如:<flowable:Definitions >diagramRelationId</flowable:Definitions>
  • default:默认属性值
  • redefines:重新定义从superClass继承的属性,重写名称、类型和限定符
  • isReference:是否通过id属性应用另外一个对象作为属性值

标签操作

bpmn-js中使用moddle模块来进行维护属性标签的管理。通过上述定义后,接下来了解下如何创建和读取bpmnxml标签。

1、创建type

const moddle = bpmnModeler.get("moddle");
//  当前操作shape
const bpmnElement =  bpmnInstances.value?.bpmnElement
// 创建标签
const scriptTag= moddle.create("flowable:taskListener", { class: "test", event: "create" });
// bpmn内置支持的extensions isMany为true
const extensions = moddle.create('bpmn2:ExtensionElements', { values: [],})
// 将flowable:Script添加到bpmn:ExtensionElements内部
extensions.values.push(scriptTag)
// 通过modeling来更新
bpmnModeler.get("modeling").updateProperties(bpmnElement, {
extensionElements: extensions
});

2、读取标签信息

读取比较简单,标签直接以属性的方式挂载在businessObject对象下,如上述的extensionElements标签,可以使用如下方式进行读取:

const bpmnElement = bpmnInstances.value?.bpmnElement // 当前操作元素
const  target = bpmnElement.businessObject?.extensionElements?.values // extensions标签下的数组type,以上述方式写入则格式为:Object('flowable:Script')[]

实现结果

如上,我们通过flowDescriptor.json配置支持flowable的自由属性来实现flowable的适配,这里尝试写入上述taskListener标签,结果如下:

bpmn-js中实现xml数据转为json数据

胖蔡阅读(121)

开发bpmn-js建模器,希望将bpmn数据格式转为json数据格式更加清晰的展示数据层次,以结果为导向分析需求,实现功能的思路有两种方式:

  • 通过bpmn-js转化为JS数据对象,然后通过JS中提供的JSON模块转换为json数据
  • xml解析成dom对象,通过dom对象转化为json格式数据
  • 三方库

这里主要介绍上面两种方式,三方库转换如xml-jsx2js详细使用查看官方使用教程。

对象转换

bpmn-js中使用bpmn-moddle模块的fromXML方法解析成对象,然后通过JSON实现数据格式转换:

import BpmnModdle from 'bpmn-moddle'; 
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_Process_1709042749982" targetNamespace="http://bpmn.io/schema/bpmn">
<bpmn:process id="Process_1709042749982" name="业务流程_1709042749982" isExecutable="true">
<bpmn:startEvent id="Event_19kysyf">
<bpmn:outgoing>Flow_1wrzkha</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:userTask id="Activity_0ajgzb4">
<bpmn:incoming>Flow_1wrzkha</bpmn:incoming>
<bpmn:outgoing>Flow_1cc5muf</bpmn:outgoing>
</bpmn:userTask>
<bpmn:sequenceFlow id="Flow_1wrzkha" sourceRef="Event_19kysyf" targetRef="Activity_0ajgzb4" />
<bpmn:userTask id="Activity_13l6c40">
<bpmn:incoming>Flow_1cc5muf</bpmn:incoming>
<bpmn:outgoing>Flow_0gddaev</bpmn:outgoing>
</bpmn:userTask>
<bpmn:sequenceFlow id="Flow_1cc5muf" sourceRef="Activity_0ajgzb4" targetRef="Activity_13l6c40" />
<bpmn:endEvent id="Event_0jyo997">
<bpmn:incoming>Flow_0gddaev</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_0gddaev" sourceRef="Activity_13l6c40" targetRef="Event_0jyo997" />
<bpmn:textAnnotation id="TextAnnotation_0rrak2v" />
<bpmn:association id="Association_0p607id" sourceRef="Event_19kysyf" targetRef="TextAnnotation_0rrak2v" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1709042749982">
<bpmndi:BPMNShape id="TextAnnotation_0rrak2v_di" bpmnElement="TextAnnotation_0rrak2v">
<dc:Bounds x="300" y="240" width="100.00000762939453" height="30.000001907348633" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_1wrzkha_di" bpmnElement="Flow_1wrzkha">
<di:waypoint x="218" y="350" />
<di:waypoint x="360" y="350" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1cc5muf_di" bpmnElement="Flow_1cc5muf">
<di:waypoint x="480" y="350" />
<di:waypoint x="622" y="350" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0gddaev_di" bpmnElement="Flow_0gddaev">
<di:waypoint x="742" y="350" />
<di:waypoint x="884" y="350" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="Event_19kysyf_di" bpmnElement="Event_19kysyf">
<dc:Bounds x="182" y="332" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0ajgzb4_di" bpmnElement="Activity_0ajgzb4">
<dc:Bounds x="360" y="290" width="120" height="120" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_13l6c40_di" bpmnElement="Activity_13l6c40">
<dc:Bounds x="622" y="290" width="120" height="120" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0jyo997_di" bpmnElement="Event_0jyo997">
<dc:Bounds x="884" y="332" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Association_0p607id_di" bpmnElement="Association_0p607id">
<di:waypoint x="215" y="340" />
<di:waypoint x="326" y="270" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>
`
const bpmnModdle =  new BpmnModdle()
const jsonStr = await moddle.fromXML(xml)
const targetJson = JSON.stringify(jsonStr, null, 2)

使用DOMParser

DOMParserJS内置的一个可以将XML或者HTML文本信息解析成一个DOM对象的功能,那么我们就可以通过DOMParser文本解析成一个DOM对象,然后再将DOM对象转化为一个JSON数据。

解析成DOM对象

首先需要将xml文本转化为DOM对象,如下:

const  xmlString = <?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_Process_1709042749982" targetNamespace="http://bpmn.io/schema/bpmn">
<bpmn:process id="Process_1709042749982" name="业务流程_1709042749982" isExecutable="true">
<bpmn:startEvent id="Event_19kysyf">
<bpmn:outgoing>Flow_1wrzkha</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:userTask id="Activity_0ajgzb4">
<bpmn:incoming>Flow_1wrzkha</bpmn:incoming>
<bpmn:outgoing>Flow_1cc5muf</bpmn:outgoing>
</bpmn:userTask>
<bpmn:sequenceFlow id="Flow_1wrzkha" sourceRef="Event_19kysyf" targetRef="Activity_0ajgzb4" />
<bpmn:userTask id="Activity_13l6c40">
<bpmn:incoming>Flow_1cc5muf</bpmn:incoming>
<bpmn:outgoing>Flow_0gddaev</bpmn:outgoing>
</bpmn:userTask>
<bpmn:sequenceFlow id="Flow_1cc5muf" sourceRef="Activity_0ajgzb4" targetRef="Activity_13l6c40" />
<bpmn:endEvent id="Event_0jyo997">
<bpmn:incoming>Flow_0gddaev</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_0gddaev" sourceRef="Activity_13l6c40" targetRef="Event_0jyo997" />
<bpmn:textAnnotation id="TextAnnotation_0rrak2v" />
<bpmn:association id="Association_0p607id" sourceRef="Event_19kysyf" targetRef="TextAnnotation_0rrak2v" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1709042749982">
<bpmndi:BPMNShape id="TextAnnotation_0rrak2v_di" bpmnElement="TextAnnotation_0rrak2v">
<dc:Bounds x="300" y="240" width="100.00000762939453" height="30.000001907348633" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_1wrzkha_di" bpmnElement="Flow_1wrzkha">
<di:waypoint x="218" y="350" />
<di:waypoint x="360" y="350" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1cc5muf_di" bpmnElement="Flow_1cc5muf">
<di:waypoint x="480" y="350" />
<di:waypoint x="622" y="350" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0gddaev_di" bpmnElement="Flow_0gddaev">
<di:waypoint x="742" y="350" />
<di:waypoint x="884" y="350" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="Event_19kysyf_di" bpmnElement="Event_19kysyf">
<dc:Bounds x="182" y="332" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0ajgzb4_di" bpmnElement="Activity_0ajgzb4">
<dc:Bounds x="360" y="290" width="120" height="120" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_13l6c40_di" bpmnElement="Activity_13l6c40">
<dc:Bounds x="622" y="290" width="120" height="120" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0jyo997_di" bpmnElement="Event_0jyo997">
<dc:Bounds x="884" y="332" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Association_0p607id_di" bpmnElement="Association_0p607id">
<di:waypoint x="215" y="340" />
<di:waypoint x="326" y="270" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>
`
const domParser =  new DOMParser(); // 创建DOMParser对象
const xmlElement = domParser.parseFromString(xmlString, 'text/xml') // 指定解析文本类型

解析DOM对象成JSON数据

获取到了dom对象后,我们可以通过便利dom对象层级解析拼接JSON数据:

// 解析element对象
function parseElement(element) {
const json ={}
if (element.hasChildNodes()) {
for (let i = 0; i < element.childNodes.length; i++) {
const child = element.childNodes[i];
if (child.nodeType === 1) {
if (child.hasChildNodes()) {
json[child.nodeName] = parseElement(child);
} else {
json[child.nodeName] = child.textContent;
}
}
}
}
return json
}
// 解析成json
const jsonString = parseElement(xmlElement)

格式化json数据

获取了json数据后,为了美化json数据的展示格式,我们需要将json数据进行格式化处理:

const targetJson = JSON.stringify(jsonString , null, 2)

测试

如下给出一个测试的html代码及展示样式:

// test.html
<html>
<head>
<title>测试xml转json数据</title>
</head>
<body>
<pre id="code"> 
<script>
const  xmlString = `<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_Process_1709042749982" targetNamespace="http://bpmn.io/schema/bpmn">
<bpmn:process id="Process_1709042749982" name="业务流程_1709042749982" isExecutable="true">
<bpmn:startEvent id="Event_19kysyf">
<bpmn:outgoing>Flow_1wrzkha</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:userTask id="Activity_0ajgzb4">
<bpmn:incoming>Flow_1wrzkha</bpmn:incoming>
<bpmn:outgoing>Flow_1cc5muf</bpmn:outgoing>
</bpmn:userTask>
<bpmn:sequenceFlow id="Flow_1wrzkha" sourceRef="Event_19kysyf" targetRef="Activity_0ajgzb4" />
<bpmn:userTask id="Activity_13l6c40">
<bpmn:incoming>Flow_1cc5muf</bpmn:incoming>
<bpmn:outgoing>Flow_0gddaev</bpmn:outgoing>
</bpmn:userTask>
<bpmn:sequenceFlow id="Flow_1cc5muf" sourceRef="Activity_0ajgzb4" targetRef="Activity_13l6c40" />
<bpmn:endEvent id="Event_0jyo997">
<bpmn:incoming>Flow_0gddaev</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_0gddaev" sourceRef="Activity_13l6c40" targetRef="Event_0jyo997" />
<bpmn:textAnnotation id="TextAnnotation_0rrak2v" />
<bpmn:association id="Association_0p607id" sourceRef="Event_19kysyf" targetRef="TextAnnotation_0rrak2v" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1709042749982">
<bpmndi:BPMNShape id="TextAnnotation_0rrak2v_di" bpmnElement="TextAnnotation_0rrak2v">
<dc:Bounds x="300" y="240" width="100.00000762939453" height="30.000001907348633" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_1wrzkha_di" bpmnElement="Flow_1wrzkha">
<di:waypoint x="218" y="350" />
<di:waypoint x="360" y="350" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1cc5muf_di" bpmnElement="Flow_1cc5muf">
<di:waypoint x="480" y="350" />
<di:waypoint x="622" y="350" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0gddaev_di" bpmnElement="Flow_0gddaev">
<di:waypoint x="742" y="350" />
<di:waypoint x="884" y="350" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="Event_19kysyf_di" bpmnElement="Event_19kysyf">
<dc:Bounds x="182" y="332" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0ajgzb4_di" bpmnElement="Activity_0ajgzb4">
<dc:Bounds x="360" y="290" width="120" height="120" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_13l6c40_di" bpmnElement="Activity_13l6c40">
<dc:Bounds x="622" y="290" width="120" height="120" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0jyo997_di" bpmnElement="Event_0jyo997">
<dc:Bounds x="884" y="332" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Association_0p607id_di" bpmnElement="Association_0p607id">
<di:waypoint x="215" y="340" />
<di:waypoint x="326" y="270" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>
`
const domParser =  new DOMParser(); // 创建DOMParser对象
const xmlElement = domParser.parseFromString(xmlString, 'text/xml') // 指定解析文本类型
// 解析element对象
function parseElement(element) {
const json ={}
if (element.hasChildNodes()) {
for (let i = 0; i < element.childNodes.length; i++) {
const child = element.childNodes[i];
if (child.nodeType === 1) {
if (child.hasChildNodes()) {
json[child.nodeName] = parseElement(child);
} else {
json[child.nodeName] = child.textContent;
}
}
}
}
return json
}
// 解析成json
const jsonString = parseElement(xmlElement)
const targetJson = JSON.stringify(jsonString , null, 2)
document.write(targetJson)
</script>
</pre>
</body>
</html>

显示效果如下:

【一周一荐】| tiny-svg:一款轻量级创建工具库

胖蔡阅读(132)

bpmn-js越了解越觉得是一个宝藏库,bpmn-js中用于使用绘制元素svgtiny-svg库也是bpmn-io组织自行开发的一款轻量级SVG工具类库,周下载量足达61398次,本篇文章就来了解下如何使用tiny-svg来实现SVG的绘制。

什么是SVG?

SVG即可缩放矢量图(Scalable Vector Graphics),是一个以XML格式定义的一个标记文件,SVG图片相较于像素图片而言有如下特点:

  • 可放大缩小且保持原有质量,不失真
  • W3C制定的XML标准
  • 模块可交互,支持DOM

tiny-svg特性

  • 没有包装魔法,直接使用原生的DOM操作
  • 模块化
  • 库大小只有2kb,库代码已被压缩和混淆
  • 支持innerSvg
  • 简化的svg属性处理
  • 几何辅助支持

安装

安装可以选择使用npm、pnpm、yarn进行安装

$ npm i tiny-svg // 或
$ yarn add tiny-svg

使用

tiny-svg使用最简单的方式通过string文本方式来创建和维护svg的绘制,这种方式会让我们的创建更加灵活轻便,操作复杂度降低,但同时由于缺少足够的api来操作使用,需要使用者对于svg的结构标准、功能支持、坐标计算需要足够多的了解才能更加完美的创建和绘制。

import {
appendTo,
classes,
create,
innerSVG
} from 'tiny-svg';
var container = document.createElement('div');
var element = appendTo(create('svg'), container);
var g = appendTo(create('g'), element);
// add classes, SVG style!
classes(g).add('foo');
var text = `
<g class="foo bar">
<rect x="0" y="0" width="0" height="0" rx="50" ry="50"/>
</g>
`;
// set innerSVG
innerSVG(g, text);