胖蔡说技术
随便扯扯

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

胖蔡阅读(4)

这是我经常听到的一个问题:有可能从渐变色而不是纯色中创建阴影吗?没有特定的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

胖蔡阅读(5)

介绍

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的内置属性、节点的默认配置

胖蔡阅读(11)

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

内置对象属性

上传我们可以发现其实这两个地方的实现是一样的,都是通过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

胖蔡阅读(23)

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插件实现自定义元素和自定义属性

胖蔡阅读(62)

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数据

胖蔡阅读(82)

开发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:一款轻量级创建工具库

胖蔡阅读(88)

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);

WordPress实现自建随机图API

amiko阅读(45)

前言

今天偶然看到likepoems的教程《免费随机图片api接口》,感觉自己可以做一下自建随机API。我之前就有一个需求,就是希望自己的博客背景图片可以随机切换为指定的某些图片。我知道有一些定制API,但上面的图不一定是我喜欢的,所以可以自建API是最好的。

测试了一下likepoems的方法,发现非常简单且可行。大致的原理就是建一个index.php文件,它定义了一些规则,并且调用和它在同一目录下的img.txt文件里的图片链接。所以理论上,只要这个index.php文件和img.txt文件在同一个目录下并且可以被Web访问,那就可以实现随机切换图片的效果:

下面,我们就讲(shui)一下如何在WordPress个人博客里自建随机图API (~ ̄▽ ̄)~

准备工作

首先,你需要安装一个插件,叫Add From Server。如果你看过我的看板娘教程,那应该安装过这个插件了。Add From Server的作用是让你的WordPress根目录以下的文件可以通过Web的方式进行访问。

然后,我们在Linux Shell里定义一下api目录:

# WordPress根目录。请按需改动work=~/docker/wordpress/app/ # 创建API目录mkdir -p $work/imgapi

这个目录的内容大致如下,每个人的可能略有不同:

drwxr-xr-x  2 www-data www-data 4.0K Jun  7 05:24 imgapi (这是我们的自建随机图API目录)-rw-r--r--  1 www-data www-data  405 Feb  6  2020 index.php-rw-r--r--  1 www-data www-data  20K May 25 01:35 license.txt-rw-r--r--  1 www-data www-data   68 Apr 10 11:06 php.ini-rw-r--r--  1 www-data www-data 7.3K May 25 01:35 readme.html-rw-r--r--  1 www-data www-data  325 Apr 26 11:49 wordfence-waf.php-rw-r--r--  1 www-data www-data 7.0K Jan 21  2021 wp-activate.phpdrwxr-xr-x  9 www-data www-data 4.0K Apr 10 06:10 wp-admin-rw-r--r--  1 www-data www-data  351 Feb  6  2020 wp-blog-header.php-rw-r--r--  1 www-data www-data 2.3K Apr 10 06:11 wp-comments-post.php-rw-rw-r--  1 www-data www-data 5.4K Dec 22 12:21 wp-config-docker.php-rw-------  1 www-data www-data 6.3K Jun  6 05:54 wp-config.php-rw-r--r--  1 www-data www-data 3.0K Apr 10 06:11 wp-config-sample.phpdrwxr-xr-x 11 www-data www-data 4.0K Jun  7 05:33 wp-content-rw-r--r--  1 www-data www-data 3.9K May 25 01:35 wp-cron.phpdrwxr-xr-x 26 www-data www-data  16K Jun  4 08:35 wp-includes-rw-r--r--  1 www-data www-data 2.5K May 25 01:35 wp-links-opml.php-rw-r--r--  1 www-data www-data 3.9K May 25 01:35 wp-load.php-rw-r--r--  1 www-data www-data  48K May 25 01:35 wp-login.php-rw-r--r--  1 www-data www-data 8.4K May 25 01:35 wp-mail.php-rw-r--r--  1 www-data www-data  24K May 25 01:35 wp-settings.php-rw-r--r--  1 www-data www-data  32K May 25 01:35 wp-signup.php-rw-r--r--  1 www-data www-data 4.7K May 25 01:35 wp-trackback.php-rw-r--r--  1 www-data www-data 3.2K Jun  8  2020 xmlrpc.php

添加文件

imgapi文件夹中添加2个文件——img.txtindex.php

img.txt

创建img.txt文件:

vim $work/imgapi/img.txt

填写一些你可以放一些自己喜欢的图片的链接,比如:

https://static.likepoems.com/2020/09/19/14d4607426a4a4e341f8144a56fbac570.jpghttps://static.likepoems.com/2020/09/19/ad30a36bd3c3d6bfd07f0357fa25ab710.jpg

这有个小建议,如果你的VPS流量比较小的话,你可以通过在Github上托管图片,并添加JsDelivr的CDN缓存链接。我为了照顾国内用户,目前暂时选择了DogeCloud的CDN进行背景图片的加速。

index.php方案1

创建index.php文件:

vim $work/imgapi/index.php

填入以下内容:

<?php//存放api随机图链接的文件名img.txt$filename = "img.txt";if(!file_exists($filename)){    die('文件不存在');} //从文本获取链接$pics = [];$fs = fopen($filename, "r");while(!feof($fs)){    $line=trim(fgets($fs));    if($line!=''){        array_push($pics, $line);    }} //从数组随机获取链接$pic = $pics[array_rand($pics)]; //返回指定格式$type=$_GET['type'];switch($type){ //JSON返回case 'json':    header('Content-type:text/json');    die(json_encode(['pic'=>$pic])); default:    die(header("Location: $pic"));} ?>

index.php方案2(推荐)

参考PHP判断用户是否是移动端访问的办法进行改良

因为移动端和PC端的界面不太一样,PC端多是横屏,而移动端多是竖屏。如果你的网站对移动设备进行了适配,你可能需要根据访客设备的不同(移动端/PC端)显示不同规格的壁纸。这里的方案是:你可以增加一个img_mobile.txt文件以提供移动端壁纸;而原本的img.txt则只为PC端提供壁纸。用法同上,与index.php放在同一个文件夹里即可,只是这个index.php增加了对访客设备的识别。

创建index.php文件:

vim $work/imgapi/index.php

填入以下内容:

<?php// 函数:访客设备function is_mobile() {    if (empty($_SERVER['HTTP_USER_AGENT']) ||         strpos($_SERVER['HTTP_USER_AGENT'], 'iPad') !== false) {        // 因为iPad有类似于PC的长宽比,所以我设置为电脑端            $is_mobile = false;        } elseif ( strpos($_SERVER['HTTP_USER_AGENT'], 'Mobile') !== false             || strpos($_SERVER['HTTP_USER_AGENT'], 'Android') !== false            || strpos($_SERVER['HTTP_USER_AGENT'], 'Silk/') !== false            || strpos($_SERVER['HTTP_USER_AGENT'], 'Kindle') !== false            || strpos($_SERVER['HTTP_USER_AGENT'], 'BlackBerry') !== false            || strpos($_SERVER['HTTP_USER_AGENT'], 'Opera Mini') !== false            || strpos($_SERVER['HTTP_USER_AGENT'], 'Opera Mobi') !== false ) {        $is_mobile = true;    } else {        $is_mobile = false;    }    return $is_mobile;} // 电脑与手机用不同的壁纸if(is_mobile()){   // 手机壁纸   $filename = "img_mobile.txt";}else{   // 电脑壁纸   $filename = "img.txt";} //存放api随机图链接的文件名img.txtif(!file_exists($filename)){    die('文件不存在');} //从文本获取链接$pics = [];$fs = fopen($filename, "r");while(!feof($fs)){    $line=trim(fgets($fs));    if($line!=''){        array_push($pics, $line);    }} //从数组随机获取链接$pic = $pics[array_rand($pics)]; //返回指定格式$type=$_GET['type'];switch($type){ //JSON返回case 'json':    header('Content-type:text/json');    die(json_encode(['pic'=>$pic])); default:    die(header("Location: $pic"));} ?>   

改用户

最后,别忘记将文件将改为www-data所有:

sudo chown -R 33:33 $work/imgapi/

使用方法

访问一下https://<博客域名>/imgapi/img.txt,看看文件是否生效。如果生效,会返回你添加的图片链接。

最后,将https://<博客域名>/imgapi/index.php这个链接当作图片URL,即可生效。你可以多开几个新标签访问这个地址,可以看到图片将被随机切换,说明API建设成功!

小结

整个过程还是蛮简单的。另外,如果不想自建,likepoems大佬还收集了好多api网站,自己可以按需食用(特别是二次元爱好者)。不过,在源站托管背景图时,随机图API会对访客相关的网站性能造成轻微的损害。由于PHP是动态资源,背景图片将无法受益于CloudFlare CDN和Workers规则,这将导致访客每次访问网络时背景图片的流量都需要指向源站。如果你(或者你的用户)比较在意性能损失,可能并不太适合将图片托管在源站,而应该通过CDN之类的手段进行缓存

JavaScript 继承实现

amiko阅读(46)

对于面向对象类的语言而言,继承是基础实现之一,也是编程过程中讨论的较多的话题。常见的继承方式分为两种:接口继承和实现继承。前者继承方法签名,后者继承实际方法。由于在JavaScript中没有签名,实现继承称为JavaScript中的唯一继承方法,而JavaScript中的继承也是通过原型链来实现的。更多有关原型与原型链的知识,请阅读《JS 的原型与原型链》,这里就不在重复赘述。

原型与继承

原型与实例的关系可以通过两种方式来确定。第一种方式是通过instanceof操作符,若一个实例的原型链中出现对应的构造函数,则instanceof返回true。如下实例所示:

function SuperType(){
this.property = true;
}
SuperType.prototype.getSuperValue = function () {
return this.property;
}
function SubType() {
this.subproperty =  false;
}
// 通过将SubType的 prototype指向SuperType实例,从而实现继承SuperType的属性和方法
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function () {
return this.subproperty;
}
let instance  = new SubType();
console.log(instance.getSuperValue()); // true

原型式继承

通过原型来实现JS的继承,但这种方式存在的缺点就是多个实例共用属性的问题,如下为示例:

function Person(name, age) {
this.name = [name]
this.age = age
}
Person.prototype.fun = function () {
console.log(this.name);
}
function Son(sex) {
this.sex = sex
}
Son.prototype = new Person('zs', 14);//这样设置导致s1和s2都是一样的name和age,肯定不合理
//不写也不影响,但是原型链有个规则,构造函数的原型.constructor指向构造函数,如果不写会发现Son.prototype.constructor;//Person
Son.prototype.constructor = Son;
let s1 = new Son('男')
let s2 = new Son('女')
s1.name.push('科比')//子类改变父类的引用类型
//导致了s2一起改变
console.log(s1.name);// ['zs', '科比']
console.log(s2.name);// ['zs', '科比']
s1.fun()// ['zs', '科比']
s2.fun()// ['zs', '科比']
// 所以这样的继承,有缺点,不实用

盗用构造函数

盗用构造函数又被称之为对象伪装或者是经典继承,是一种使用call或者bind方式调用父类构造函数,从而避免prototype继承导致的prototype属性在多个实例之间共有的问题。其实现如下:

function Person () {
this.name = {
firstName: 'San',
lastName: 'Zhang'
};
this.age = 20;
this.sayName = function () {
console.log(this.name.lastName, this.name.firstName);
}
}
function Student () {
this.school = 'Tsinghua University';
Person.call(this);
}
let stu1 = new Student();
let stu2 = new Student();
stu1.name.lastName = 'Li'; //改变了stu1对象实例的name属性
console.log(stu1.name, stu1.age, stu1.school);
console.log(stu2.name, stu2.age, stu2.school);
// { firstName: 'San', lastName: 'Li' } 20 Tsinghua University
// { firstName: 'San', lastName: 'Zhang' } 20 Tsinghua University。stu2的name属性并没有改变!

组合继承

组合继承就是将原型链继承和盗用构造函数继承集成在一起,其示例代码如下:

function Person(name, age) {
this.name = [name]
this.age = age
}
Person.prototype.fun = function () {
console.log(this.name);
}
function Son(name, age, sex) {
// 通过在子类Son中调用父类构造函数,实现给每个子类单独设置属性
Person.call(this, name, age)
this.sex = sex
}
//通过原型让子类型继承父类型中的方法
Son.prototype = new Person();
Son.prototype.constructor = Son
let s1 = new Son('哈登', 30, '男')
let s2 = new Son('小哈', 21, '男')
s1.fun()//['哈登']
s2.fun()//['小哈']

寄生式继承

是一种基于原型式的继承方式,它通过创建一个仅用于封装继承过程的函数,该函数在内部调用原型式继承创建一个对象,然后增强该对象,最后返回这个对象。其实就是原型式样继承和工厂模式的实现组合。

function createAnother(original){ 
var clone = object(original);  //通过调用函数创建一个新对象    
clone.sayHi = function(){
//以某种方式来增强这个对象         
alert("hi");     
};     
return clone;         //返回这个对象 
}
var person = {     
name: "Nicholas",     
friends: ["Shelby", "Court", "Van"] 
}; 
var anotherPerson = createAnother(person); 
anotherPerson.sayHi(); //"hi"

寄生式组合继承

即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。 不必为了指定子类型的原型而调用父类的构造函数,我们所需要的无非就是父类原型的一个副本而已。

function inheritPrototype(subType, superType){
var prototype = object(superType.prototype); //创建对象
prototype.constructor = subType; //增强对象
subType.prototype = prototype; //指定对象
} 
function inheritPrototype(subType, superType){
var prototype = Object.create(superType.prototype); // 创建对象,创建父类原型的一个副本
prototype.constructor = subType;                    // 增强对象,弥补因重写原型而失去的默认的constructor 属性
subType.prototype = prototype;                      // 指定对象,将新创建的对象赋值给子类的原型
}
// 父类初始化实例属性和原型属性
function SuperType(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
alert(this.name);
};
// 借用构造函数传递增强子类实例属性(支持传参和避免篡改)
function SubType(name, age){
SuperType.call(this, name);
this.age = age;
}
// 将父类原型指向子类
inheritPrototype(SubType, SuperType);
// 新增子类原型属性
SubType.prototype.sayAge = function(){
alert(this.age);
}
var instance1 = new SubType("jack", 23);
var instance2 = new SubType("rose", 20);
instance1.colors.push("2"); // ["red", "blue", "green", "2"]
instance1.colors.push("3"); // ["red", "blue", "green", "3"]

JS实现全屏显示和退出全屏功能

胖蔡阅读(78)

开发需求对于某些页面元素较多,或者需要突出显示部分元素避免干扰的组件(主要挂载在HtmlElement元素下)来说,document提供了一个全屏显示的api来方便我们操作。

我们同时也可以通过onresize全局事件用于监听是否进行全屏操作,代码如下:

/**
* 全屏/退出全屏显示切换开关
* @param element  当前显示container
*/
export function togglefullScrollFunc() {
if (!document.fullscreenElement) {
// 没有fullscreenelement元素,可以进行全屏显示操作, 全屏模式
document.getElementById('flowable-process-designer')?.requestFullscreen()
}
else {
// 退出全屏模式
document.exitFullscreen()
}
}
window.onresize = ()=> {
if (document.fullscreenElement) {
console.log('进入全屏')
this.rightOffset = '0px'
} else {
this.quanPing = false
this.rightOffset = '-30px'
}
}

如上可以通过onresize配合document.fullscreenElement进行监听或者是通过requestFullscreen的回调函数配合记录更新当前状态均可。