胖蔡说技术
随便扯扯

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

胖蔡阅读(233)

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阅读(148)

前言

今天偶然看到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阅读(144)

对于面向对象类的语言而言,继承是基础实现之一,也是编程过程中讨论的较多的话题。常见的继承方式分为两种:接口继承和实现继承。前者继承方法签名,后者继承实际方法。由于在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实现全屏显示和退出全屏功能

胖蔡阅读(171)

开发需求对于某些页面元素较多,或者需要突出显示部分元素避免干扰的组件(主要挂载在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的回调函数配合记录更新当前状态均可。

聊一聊bpmn-js中的elementFactory模块

胖蔡阅读(197)

bpmn-js 阅读指南:

上一篇文章里我们了解了bpmn-js使用palette模块进行左侧小工具区域(也可以理解为调色板区域)的功能扩展,今天这个话题则是延续上期的palette进行开展的。

从上篇文章《聊一聊bpmn-js中的Palette》我们知道,PaletteProvider通过getPaletteEntries方法提供小工具Map对象,而单个小工具绘制对象格式如下:

type PaletteEntry = {
  action: (event: Event, autoActivate: boolean) => any || {}; // 事件回调,或者是事件对象如{dragStart:()=>{}}
  className?: string; // 类名,这里可用作加载字体图标
  group?: string; // 区域划分
  html?: string; // 自定义html显示
  imageUrl?: string; // 图标链接地址
  separator?: boolean;
  title?: string; // 悬停显示文字
};

这里action作为palette与主建模编辑的交互模块,palette中引用了elementFactorycreate模块进行绘制创建和管理。而本篇文章我们的主角就是工具的元素创建工具类:ElementFactory,其插件模块位于diagram-js/lib/core/ElementFactory.js,在PaletteProvider中的使用如下:

// 声明注入模块
PaletteProvider.$inject = [
......
  'create',
  'elementFactory',
......
];
// 初始化
export default function PaletteProvider(...,create, elementFactory,...){
......
  this._create = create;
  this._elementFactory = elementFactory;
......
}


PaletteProvider.prototype.getPaletteEntries = function()  {
 function createAction(type, group, className, title, options){
    // 事件交互创建shape
    function createListener(event) {
      var shape = elementFactory.createShape(assign({ type: type }, options));
      if (options) {
        var di = getDi(shape);
        di.isExpanded = options.isExpanded;
      }
      create.start(event, shape);
    }
}
......
}

上述可以发现,元素的绘制包括两个模块:elementFactorycreatecreate主要是处理触发事件后的动作操作,elementFactory用于创建当前元素,本篇文章主要来介绍diagram-js中的elementFactory模块。

元素类型

elementFactorydiagram-js中用于提供创建元素模型的一个工厂类,主要还是以创建元素模型为主(即元素结构,元素绘制由单独的defaultRenderer模块负责)。在elementFactory模块中将元素的模型分为四类,当且仅当绘制的类型是这四类才能成功创建,否则将会抛出异常:unknown type

elementFactory的元素类型通过一个变量来定义控制,这也是为了便于扩展,日后由其他新增的类型也不是没有可能的,且所有类型都是基于基础类型ElementImpl的基础上进行的扩展,我们可以通过代码了解:

var types = {
  connection: ConnectionImpl,
  shape: ShapeImpl,
  label: LabelImpl,
  root: RootImpl
};

// 基础类型
function ElementImpl() {
  Object.defineProperty(this, 'businessObject', {
    writable: true
  });

  Object.defineProperty(this, 'label', {
    get: function() {
      return this.labels[0];
    },
    set: function(newLabel) {

      var label = this.label,
          labels = this.labels;

      if (!newLabel && label) {
        labels.remove(label);
      } else {
        labels.add(newLabel, 0);
      }
    }
  });

  // 实现关联元素的双向引用
  parentRefs.bind(this, 'parent');
  labelRefs.bind(this, 'labels');
  outgoingRefs.bind(this, 'outgoing');
  incomingRefs.bind(this, 'incoming');
}

通过上述代码可以发现ElementImpl给所有元素定义了一些基础实现和绑定:

  • businessObject:这是元素的属性信息汇总对象
  • label:支持单标签,将映射到多标签阵列
  • 绑定: ElmenetImpl实现了元素的父类、流入、流出、多标签绑定

elementFactory中的所有绑定关系将会交由elementFactory模块内部统一维护:

var parentRefs = new Refs({ name: 'children', enumerable: true, collection: true }, { name: 'parent' }),
    labelRefs = new Refs({ name: 'labels', enumerable: true, collection: true }, { name: 'labelTarget' }),
    attacherRefs = new Refs({ name: 'attachers', collection: true }, { name: 'host' }),
    outgoingRefs = new Refs({ name: 'outgoing', collection: true }, { name: 'source' }),
    incomingRefs = new Refs({ name: 'incoming', collection: true }, { name: 'target' });

1、connection

connection元素就是连接线用于将两个元素进行连接,其中主要包含定义元素来源和元素流入的方向绑定:

function ConnectionImpl() {
  ElementImpl.call(this);

 // 来源于
  outgoingRefs.bind(this, 'source');

  // 流向
  incomingRefs.bind(this, 'target');
}

// 创建示例
import * as Model from 'diagram-js/lib/model';

const connection = Model.create('connection', {
   waypoints: [
     { x: 100, y: 100 },
     { x: 200, y: 100 }
  ]
});

2、shape

shape类型主要就是用于创建有形状的元素,类似开始、流程、判断等工作流元素。

function ShapeImpl() {
  ElementImpl.call(this);

  // 绑定子元素
  parentRefs.bind(this, 'children');
  attacherRefs.bind(this, 'host');
  attacherRefs.bind(this, 'attachers');
}

// 创建示例
import * as Model from 'diagram-js/lib/model';

const shape = Model.create('shape', {
   x: 100,
   y: 100,
   width: 100,
   height: 100
});

3、label

label类型主要是给其他元素提供文字管理,它继承于一个shape类型,而非基础的element类型。

function LabelImpl() {
  ShapeImpl.call(this);


  labelRefs.bind(this, 'labelTarget');
}
// 示例创建
 import * as Model from 'diagram-js/lib/model';
 
 const label = Model.create('label', {
   x: 100,
   y: 100,
   width: 100,
   height: 100,
   labelTarget: shape
 });

4、root

这里就是当前bpmn-js流程图的根节点了,也就是bpmn:process元素,更节点包含的元素较为简单主要是存在与所有子元素的关联:

function RootImpl() {
  ElementImpl.call(this);


  parentRefs.bind(this, 'children');
}

// 示例创建
 import * as Model from 'diagram-js/lib/model';
 
 const root = Model.create('root', {
   x: 100,
   y: 100,
   width: 100,
   height: 100
 });

工厂方法

elementFactory提供的工厂方法比较直球,基本你看到方法名称就知道它是创建什么元素的:

  • createRoot:创建root元素
  • createLabel:创建label
  • createShape:创建带有形状的元素
  • createConnection:创建连接线
  • create:这是所有工厂方法的实现

聊一聊bpmn-js中的Palette

胖蔡阅读(224)

bpmn-js 阅读指南:

bpmn-js中使用Modeler进行流程图的建模,而Palette则是其内部创建的提供左侧工具栏的插件。插件使用didi实现的依赖注入,其创建使用参考上一篇文章:推荐前端一个轻量级别的依赖注入库:didi

Palette实现主要依托三个功能模块:Eventbus(详情参考bpmn-js 事件总线处理)、diagram-jsPalette插件(之后简称Palette)以及bpmn-js中的PaletteProvider(之后简称PaletteProvider)。左侧工具栏的加载原理是:作为Palette的提供方PalettProvider则需要在插件注册之前通过Palette提供的注册器registerProvider注册监听并通过_rebuild会发送一个palette.getProviders事件,将当前提供者添加到Palette的集合中。Palette加载后通过eventbus监听整体页面绘制事件,当页面绘制后Palette将会回调内置_rebuild方法获取小工具栏组件信息,这时候我们的PaletteProvider也已经完成了注册,Palette会通过_rebuild重绘完成整体小工具栏区域的加载。接下来我们了解下bpmn-js中的左侧小工具栏的加载流程。

PaletteProvider

可以简单的认为它是最终的提供方,小工具栏上面有什么说到底还是由它决定的。在bpmn-js源码中内置了一个”paletteProvider“,也就是说我们可以什么都不做会有一套默认的小工具栏供我们使用。我们想对其进行修改bpmn-js提供给我们两种方式:完全自定义动态增加。我们先暂时忘掉这两种方式,首先来看下如何去实现一个PaletteProvider的角色(插件定义细节请查看:推荐前端一个轻量级别的依赖注入库:didi)。

注册

想要成为一个PaletteProvider的角色,我们就必须要通过diagram-jspalette的插件进行注册。

// Palette提供者
function PaletteProvider(palette,...){
  ....
  palette.registerProvider(this);
}

//必须要有diagram-js的palette插件
PaletteProvider.$inject = ['palette']

提供

注册完成后当Palette需要我们绘制的时候我们就需要提供给Palette到底绘制哪些工具?如何绘制?这时候我们需要提供,这里有个核心的方法就是getPaletteEntries,这个方法是Palette获取小工具集合的必须包含的方法,该方法需要我们返回一个对象,对象里可以有多个工具,对象格式姑且以PaletteEntry标识,且所有注册的提供者的集合会被整合。

// 工具对象格式
type PaletteEntry = {
  action: (event: Event, autoActivate: boolean) => any || {}; // 事件回调,或者是事件对象如{dragStart:()=>{}}
  className?: string; // 类名,这里可用作加载字体图标
  group?: string; // 区域划分
  html?: string; // 自定义html显示
  imageUrl?: string; // 图标链接地址
  separator?: boolean;
  title?: string; // 悬停显示文字
};

// 改方法必须有
PaletteProvider.prototype.getPaletteEntries = function() {
   return  {
      'hand-tool': {
        group: 'tools',
        className: 'bpmn-icon-hand-tool',
        title: '手型工具',
        action: {
          click: function(event) {
            handTool.activateHand(event);
          }
        }
      },
    }
}

上述是手型工具的提供方式,我们常规使用的事件处理也就dragstart、click两种,以下是bpmn-js提供的创建action的方式:

  function createAction(type, group, className, title, options) {

    function createListener(event) {
      var shape = elementFactory.createShape(assign({ type: type }, options));

      if (options) {
        var di = getDi(shape);
        di.isExpanded = options.isExpanded;
      }

      create.start(event, shape);
    }

    return {
      group: group,
      className: className,
      title: title,
      action: {
        dragstart: createListener,
        click: createListener
      }
    };
  }

// 上述用到了其他的依赖插件,需要我们在$inject中声明并通过构建函数传入
PaletteProvider.$inject = [
  'palette',
  'create',
  'elementFactory',
  'spaceTool',
  'lassoTool',
  'handTool',
  'globalConnect',
  'translate'
];

如上createListener提供的是工具的绘制方法,这里使用的bpmn-js提供的几种svg图形绘制,当然若有需要我们也可以自己绘制,这个后续文章会深入讨论。

PaletteProvider既然已经了解完成,这里我们来说下开始的时候说的两种小工具的方式:完全自定义动态增加

  • 完全自定义:将插件名命名为:‘paletteProvider’,会自动覆盖已有的paletteProvider插件。
// 将声明设置为paletteProvider
export default {
  __init__: ['paletteProvider'],
  paletteProvider: ['type', PaletteProvider],
}

// 修改PaletteProvider.js 的getPaletteEntries 
// 改方法必须有
PaletteProvider.prototype.getPaletteEntries = function() {
   ......
   return  {
      'hand-tool': {
        group: 'tools',
        className: 'bpmn-icon-hand-tool',
        title: '手型工具',
        action: {
          click: function(event) {
            handTool.activateHand(event);
          }
        }
      },
    }
}

这样加载后就如上一样只剩一个小小的手型工具了。

  • 动态增加:若只是想加一个新的工具进去,完全可以自定义一个名字,设置号group分区就可以,我们这里还是以上述为例修改下group,需要注意的是工具名需要修改, 不然可能会无效,试试效果:
// 自定义名称
export default {
  __init__: ['customProvider'],
  customProvider: ['type', PaletteProvider],
}

// 修改PaletteProvider.js 的getPaletteEntries 
// 改方法必须有
PaletteProvider.prototype.getPaletteEntries = function() {
   ......
   return  {
      'hand-tool-2': {
        group: 'activity',
        className: 'bpmn-icon-hand-tool',
        title: '手型工具2',
        action: {
          click: function(event) {
            handTool.activateHand(event);
          }
        }
      },
    }
}

这样在activity区域就多了一个手型工具了

Palette

diagram-jsPalette插件可以说是整个bpmn-js的左侧工具的管理模块了,它定义了PaletteProvider的注册方式、提供方法已经工具的类型格式。这里我们来看下几个核心的功能部分。

提供注册

Palette内部实现了一个registerProvider方法来让PaletteProvider进行注册登记,并通知Palette构建组件信息,通过eventbus方式将其加载到内部的event


// 注册实现
Palette.prototype.registerProvider = function(priority, provider) {

   ...
  // 添加监听将provider加载到内部的event中
  this._eventBus.on('palette.getProviders', priority, function(event) {
    event.providers.push(provider);
  });

  this._rebuild();
};

// 组件构建部分
Palette.prototype._rebuild = function() {
  // 这里用于发送信息让注册方法的监听添加provider
  var providers = this._getProviders();
 ...
  this._update();
};

// 这里是发送获取组件的消息
Palette.prototype._getProviders = function(id) {
  var event = this._eventBus.createEvent({
    type: 'palette.getProviders',
    providers: []
  });
  this._eventBus.fire(event);
  return event.providers;
};

监听绘制

Palette插件开始加载的时候会监听diagram-js的绘制事件,并在合适的时机重写加载绘制组件:

export default function Palette(eventBus, canvas) {



......
 // toolMananger插件用于协助管理palette组件的编辑状态
  eventBus.on('tool-manager.update', function(event) {
    var tool = event.tool;
    self.updateToolHighlight(tool);
  });

  // 国际化监听
  eventBus.on('i18n.changed', function() {
    self._update();
  });

  // diagram初始化监听
  eventBus.on('diagram.init', function() {
    self._diagramInitialized = true;
    self._rebuild();
  });
}

如上就是本篇文章的所有的内容了,若是感兴趣或是觉得对你有所帮助,欢迎关注一直在前端路上陪伴你的胖蔡~

我是如何用CSS自定义属性制作图标系统的

胖蔡阅读(179)

SVG是网站上图标的最佳格式,这一点毋庸置疑。无论屏幕像素密度如何,它都允许您拥有清晰的图标,您可以在悬停时更改SVG的样式,甚至可以使用CSS或JavaScript对图标进行动画处理。

在页面上包含SVG的方法有很多种,每种技术都有自己的优缺点。在过去的几年里,我一直在使用Sass函数直接在CSS中导入我的图标,以避免弄乱我的HTML标记。

我有一个Sass列表,里面有我图标的所有源代码。然后,每个图标都用Sass函数编码到一个数据URI中,并存储在页面根上的自定义属性中。

我在这里为您提供的是一个Sass函数,它可以直接在CSS中创建一个SVG图标库。

SVG源代码是使用Sass函数编译的,该函数将它们编码在数据URI中,然后将图标存储在CSS自定义属性中。然后,您可以在CSS中的任何位置使用任何图标,就像它是一个外部图像一样。

这是一个直接从我的个人网站代码中提取的示例:

.c-filters__summary h2:after {
  content: var(--svg-down-arrow);
  position: relative;
  top: 2px;
  margin-left: auto;
  animation: closeSummary .25s ease-out;
}

示例

// html代码
<button>Menu</button>

//scss代码
/*
* Sass functions forked from https://github.com/Threespot/frontline-sass/blob/3a7c6de247bb031eeb437846c0c53758dc4c31ec/src/functions/_svg-url.scss
*/

/**
* List of all the SVG icons of the project
*/
$svg-icons: (
    burger: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24.8 18.92" width="24.8" height="18.92"><path d="M23.8,9.46H1m22.8,8.46H1M23.8,1H1" fill="none" stroke="#000" stroke-linecap="round" stroke-width="2"/></svg>'
);

/**
* Characters to escape from SVGs
* Source: https://github.com/Threespot/frontline-sass/blob/master/src/variables/_escape-chars.scss
*/
$fs-escape-chars: (
    ' ': '%20',
    '\'': '%22',
    '"': '%27',
    '#': '%23',
    '/': '%2F',
    ':': '%3A',
    '(': '%28',
    ')': '%29',
    '%': '%25',
    '<': '%3C',
    '>': '%3E',
    '\\': '%5C',
    '^': '%5E',
    '{': '%7B',
    '|': '%7C',
    '}': '%7D',
);

/**
 * Helper to get URL-escaped inline SVG code
 */
@function svg($name) {
    // Check if icon exists
    @if not map-has-key($svg-icons, $name) {
        @error 'icon “#{$name}” does not exists in $svg-icons map';
        @return false;
    }

    // Get icon data
    $icon-map: map-get($svg-icons, $name);
  
    $escaped-string: '';
    $unquote-icon: unquote($icon-map);
    // Loop through each character in string
    @for $i from 1 through str-length($unquote-icon) {
        $char: str-slice($unquote-icon, $i, $i);

        // Check if character is in symbol map
        $char-lookup: map-get($fs-escape-chars, $char);

        // If it is, use escaped version
        @if $char-lookup != null {
            $char: $char-lookup;
        }

        // Append character to escaped string
        $escaped-string: $escaped-string + $char;
    }

    // Return inline SVG data
    @return url('data:image/svg+xml, #{$escaped-string} ');
}

/**
 * Convert all icons into custom properties
 */

:root {
  @each $name, $code in $svg-icons {
    --svg-#{$name}: #{svg($name)};
  }
}

button {
  margin: 1rem;
  font-size: 20px;
  padding: 10px 25px;
  border-radius: 10px;
  border: 2px solid black;
  background: none;
  &::after {
    /* Import inline SVG */
    content: var(--svg-burger);
    vertical-align: middle;
    margin-left: 1rem;
  }
}

Sass结构

/* All the icons source codes */
$svg-icons: (
  burger: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0...'
);

/* Sass function to encode the icons */
@function svg($name) {
  @return url('data:image/svg+xml, #{$encodedSVG} ');
}

/* Store each icon into a custom property */
:root {
  @each $name, $code in $svg-icons {
    --svg-#{$name}: #{svg($name)};
  }
}

/* Append a burger icon in my button */
.menu::after {
  content: var(--svg-burger);
}		

此技术既有优点也有缺点,因此在您的项目上实施此解决方案之前,请将其考虑在内:

  • 没有对SVG文件的HTTP请求。
  • 所有图标都存储在一个位置。
  • 如果你需要更新一个图标,你不必检查每个HTML模板文件。
  • 图标与CSS一起缓存。
  • 您可以手动编辑图标的源代码。
  • 它不会因为添加额外的标记而污染HTML。
  • 您仍然可以使用CSS更改图标的颜色或某些方面。
  • 不能使用CSS对SVG的特定部分进行动画制作或更新。
  • 图标越多,CSS编译的文件就越重。

我主要将这种技术用于图标,而不是徽标或插图。编码的SVG总是会比它的原始文件重,所以我仍然用一个带有<img>标记的外部文件或在带有url(path/to/file.SVG)CSS中加载复杂的SVG

将SVG编码为数据URI

将SVG编码为数据URI并不是什么新鲜事。事实上,Chris Coyier在10多年前写了一篇关于这项技术的帖子,解释了如何使用这项技术以及为什么你应该(或不应该)使用它。

有两种方法可以在带有数据URI的CSS中使用SVG:

  • 作为外部图像(使用背景图像、边框图像、列表样式图像等)
  • 作为伪元素的内容(例如::before或::after)

以下是一个基本示例,展示了如何使用这两种方法:

<ul>
  <li>Super Tomato</li>
  <li>Captain Spinach</li>
</ul>
<button>Hover me</button>

ul {
  list-style-image: url("data:image/svg+xml, %3Csvg%20xmlns=%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox=%270%200%2040%2038.4%27%20width=%2740%27%20height=%2738.4%27%3E%3Cpath%20d=%27m16%2022-4-5a3%203%200%200%201%200-4%2012%2012%200%200%201%203-2C8%2014%201%2020%200%2021s5%202%206%208c1%205%208%207%209%205a43%2043%200%200%201%203-7v-4a3%203%200%200%201-2-1ZM38%209H24a13%2013%200%200%200-10%205%202%202%200%200%200%200%202l3%205a2%202%200%200%200%201%201%202%202%200%200%200%201-1h1v1l-1%2014a2%202%200%201%200%204%200l1-13h1l1%2013a2%202%200%200%200%202%202%202%202%200%200%200%202-2l-1-14v-9h9a2%202%200%200%200%202-2%202%202%200%200%200-2-2ZM20%2019l-2-4a10%2010%200%200%201%202-1Zm8-15a4%204%200%201%201-4-4%204%204%200%200%201%204%204%27%20%2F%3E%3C%2Fsvg%3E ");
  margin: 1rem;
  li {
    font-size: 1.8rem;
    padding-left: 1rem;
    margin-bottom: 1rem;
  }
}

button {
  margin: 1rem;
  font-size: 20px;
  padding: 10px 25px;
  border-radius: 10px;
  border: 2px solid black;
  background: none;
  &::after {
    content: url("data:image/svg+xml, %3Csvg%20width=%277%27%20height=%2712.19%27%20xmlns=%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox=%270%200%207%2012.19%27%3E%3Cpath%20d=%27M.27,10.64a.89.89,0,0,0,0,1.28.91.91,0,0,0,1.28,0L6.73,6.73a.89.89,0,0,0,0-1.28L1.55.27A.89.89,0,0,0,.27.27a.89.89,0,0,0,0,1.28L4.81,6.1Z%27%2F%3E%3C%2Fsvg%3E ");
    margin-left: 10px;
    transition: 0.2s ease-out;
  }
  &:hover::after {
    margin-left: 20px;
  }
}

这个特定实现的主要问题是,每次需要新图标时,都必须手动转换SVG,而在CSS中有这么长的不可读代码串并不是很愉快。

使用Sass函数

通过使用Sass,我们可以将SVG的源代码直接复制到我们的代码库中,让Sass对它们进行正确编码,以避免任何浏览器错误,从而简化我们的生活。

该解决方案的灵感主要来自Threespot Media开发的现有功能,该功能可在其存储库中使用。

以下是此技术的四个步骤:

  • 创建一个列出所有SVG图标的变量。
  • 列出数据URI需要跳过的所有字符。
  • 实现一个函数,将SVG编码为数据URI格式。
  • 在代码中使用您的函数。

1. 图标列表

/**
* Add all the icons of your project in this Sass list
*/
$svg-icons: (
  burger: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24.8 18.92" width="24.8" height="18.92"><path d="M23.8,9.46H1m22.8,8.46H1M23.8,1H1" fill="none" stroke="#000" stroke-linecap="round" stroke-width="2"/></svg>'
);

2. 转义字符列表

/**
* Characters to escape from SVGs
* This list allows you to have inline CSS in your SVG code as well
*/
$fs-escape-chars: (
  ' ': '%20',
  '\'': '%22',
  '"': '%27',
  '#': '%23',
  '/': '%2F',
  ':': '%3A',
  '(': '%28',
  ')': '%29',
  '%': '%25',
  '<': '%3C',
  '>': '%3E',
  '\\': '%5C',
  '^': '%5E',
  '{': '%7B',
  '|': '%7C',
  '}': '%7D',
);

3. 编码函数

/**
* You can call this function by using `svg(nameOfTheSVG)`
*/
@function svg($name) {
  // Check if icon exists
  @if not map-has-key($svg-icons, $name) {
    @error 'icon “#{$name}” does not exists in $svg-icons map';
    @return false;
  }

  // Get icon data
  $icon-map: map-get($svg-icons, $name);

  $escaped-string: '';
  $unquote-icon: unquote($icon-map);
  // Loop through each character in string
  @for $i from 1 through str-length($unquote-icon) {
    $char: str-slice($unquote-icon, $i, $i);

    // Check if character is in symbol map
    $char-lookup: map-get($fs-escape-chars, $char);

    // If it is, use escaped version
    @if $char-lookup != null {
        $char: $char-lookup;
    }

    // Append character to escaped string
    $escaped-string: $escaped-string + $char;
  }

  // Return inline SVG data
  @return url('data:image/svg+xml, #{$escaped-string} ');
}		

4.在页面中添加SVG

button {
  &::after {
    /* Import inline SVG */
    content: svg(burger);
  }
}

如果您遵循了这些步骤,Sass应该正确编译您的代码并输出以下内容:

button::after {
  content: url("data:image/svg+xml, %3Csvg%20xmlns=%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox=%270%200%2024.8%2018.92%27%20width=%2724.8%27%20height=%2718.92%27%3E%3Cpath%20d=%27M23.8,9.46H1m22.8,8.46H1M23.8,1H1%27%20fill=%27none%27%20stroke=%27%23000%27%20stroke-linecap=%27round%27%20stroke-width=%272%27%2F%3E%3C%2Fsvg%3E ");
}	

自定义属性

现在实现的Sass-svg()函数非常有效。但它最大的缺陷是,代码中多个位置所需的图标将被复制,并可能使编译后的CSS文件的重量增加很多!

为了避免这种情况,我们可以将所有图标存储到CSS变量中,并使用对变量的引用,而不是每次都输出编码的URI。

我们将保留以前的代码,但这次我们将首先将Sass列表中的所有图标输出到我们网页的根目录中:

/**
  * Convert all icons into custom properties
  * They will be available to any HTML tag since they are attached to the :root
  */

:root {
  @each $name, $code in $svg-icons {
    --svg-#{$name}: #{svg($name)};
  }
}

现在,我们不需要每次需要图标时都调用svg()函数,而是必须使用以–svg前缀创建的变量。

button::after {
  /* Import inline SVG */
  content: var(--svg-burger);
}

优化SVG

此技术不会对您正在使用的SVG的源代码进行任何优化。确保你没有留下不必要的代码;否则,它们也将被编码,并将增加您的CSS文件大小。

您可以查看这个工具列表以及有关如何正确优化SVG的信息。我最喜欢的工具是Jake Archibald的SVGOMG——只需将文件拖到那里并复制输出的代码。

悬停时更新图标

使用此技术,我们无法使用CSS选择SVG的特定部分。例如,当用户悬停按钮时,无法更改图标的填充颜色。但是,我们可以使用CSS来修改图标的外观。

例如,如果你有一个黑色的图标,并且你想在悬停时让它变成白色,你可以使用inverse()CSS过滤器。我们也可以使用hue-rotate()过滤器。

<p>All those buttons are using the same SVG icon</p>
<button class="burger burger--negative">Negative button</button><br>
<button class="burger burger--color burger--green">Green button</button><br>
<button class="burger burger--color burger--blue">Blue button</button>

/*
* Sass functions forked from https://github.com/Threespot/frontline-sass/blob/3a7c6de247bb031eeb437846c0c53758dc4c31ec/src/functions/_svg-url.scss
*/

/**
* List of all the SVG icons of the project
*/
$svg-icons: (
    burger: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24.8 18.92" width="24.8" height="18.92"><path d="M23.8,9.46H1m22.8,8.46H1M23.8,1H1" fill="none" stroke="#f00" stroke-linecap="round" stroke-width="2"/></svg>'
);

使用CSS mask image属性更新图标

另一个能够改变图标颜色的技巧是将其用作带有背景的伪元素上的遮罩。将伪元素设置为具有背景颜色的内联块,并定义所需大小的宽度和高度。

一旦您有了一个具有所需颜色的矩形,请应用这四个值以仅保持所需SVG的形状:

  • mask-image:var(–svg-burger):对我们图标的引用。
  • mask-repeat:不重复:防止蒙版被复制。
  • mask-size:包含:使图标完全适合矩形。
  • mask-position:centre:将我们的图标放在伪元素的中心。

不要忘记,截至2022年9月,大多数浏览器的所有CSS掩码属性仍然需要以-webkit为前缀。

<p>All those buttons are using the same SVG icon used as mask-image</p>
<button class="burger">Negative button</button><br>
<button class="burger burger--green">Green button</button><br>
<button class="burger burger--blue">Blue button</button>
/*
* Sass functions forked from https://github.com/Threespot/frontline-sass/blob/3a7c6de247bb031eeb437846c0c53758dc4c31ec/src/functions/_svg-url.scss
*/

/**
* List of all the SVG icons of the project
*/
$svg-icons: (
  burger:
    '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24.8 18.92" width="24.8" height="18.92"><path d="M23.8,9.46H1m22.8,8.46H1M23.8,1H1" fill="none" stroke="#fff" stroke-linecap="round" stroke-width="2"/></svg>'
);

/**
* Characters to escape from SVGs
* Source: https://github.com/Threespot/frontline-sass/blob/master/src/variables/_escape-chars.scss
*/
$fs-escape-chars: (
  " ": "%20",
  "'": "%22",
  '"': "%27",
  "#": "%23",
  "/": "%2F",
  ":": "%3A",
  "(": "%28",
  ")": "%29",
  "%": "%25",
  "<": "%3C",
  ">": "%3E",
  "\\":"%5C",
  "^": "%5E",
  "{": "%7B",
  "|": "%7C",
  "}": "%7D"
);

/**
 * Helper to get URL-escaped inline SVG code
 */
@function svg($name) {
  // Check if icon exists
  @if not map-has-key($svg-icons, $name) {
    @error 'icon “#{$name}” does not exists in $svg-icons map';
    @return false;
  }

  // Get icon data
  $icon-map: map-get($svg-icons, $name);

  $escaped-string: "";
  $unquote-icon: unquote($icon-map);
  // Loop through each character in string
  @for $i from 1 through str-length($unquote-icon) {
    $char: str-slice($unquote-icon, $i, $i);

    // Check if character is in symbol map
    $char-lookup: map-get($fs-escape-chars, $char);

    // If it is, use escaped version
    @if $char-lookup != null {
      $char: $char-lookup;
    }

    // Append character to escaped string
    $escaped-string: $escaped-string + $char;
  }

  // Return inline SVG data
  @return url("data:image/svg+xml, #{$escaped-string} ");
}

/**
 * Convert all icons into custom properties
 */

:root {
  @each $name, $code in $svg-icons {
    --svg-#{$name}: #{svg($name)};
  }
}

.burger {
  margin: 1rem;
  font-size: 20px;
  padding: 10px 25px;
  border-radius: 10px;
  border: 2px solid black;
  background: none;
  transition: 0.15s ease-out;
  border-color: currentColor;
  &:hover {
    background: #000;
    color: white;
  }
  &::after {
    content: "";
    /* Import inline SVG */
    -webkit-mask-image: var(--svg-burger);
    mask-image: var(--svg-burger);
    
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
    
    -webkit-mask-size: contain;
    mask-size: contain;
    
    -webkit-mask-position: center;
    mask-position: center;
    
    width: 24px;
    height: 24px;
    display: inline-block;
    background: currentColor;
    vertical-align: middle;
    margin-left: 1rem;
    transition: inherit;
  }
}
.burger--green {
  color: #007a00;
  &:hover {
    background: #007a00;
  }
}
.burger--blue {
  color: #009cff;
  &:hover {
    background: #009cff;
  }
}

聊一聊bpmn-js中的依赖注入框架didi

胖蔡阅读(203)

最近在用bpmn-js来进行flowable流程图的开发工作,不可避免地希望对其运行机制做一个大致的了解,在使用bpmn-js的过程中,bpmn-js基于diagram-js实现的插件式开发方式引起了我的兴趣。于是通过查阅源码希望对齐进一步了解发现:diagram-js是基于一个叫didi的实现的依赖注入功能并贯穿全局,深度使用。接下来我们一起来了解下didi是如何实现依赖注入的。每一个贡献都值得尊敬,顺手给didi开源库一个Star

如何使用?

首先通过didigithub上的文档介绍来了解下如何去使用didi来实现依赖注入。我这里基于文档做了一些结构上的调整,介绍如何创建一个didi的注入组件。依赖注入无非就三个过程:模块实现模块声明模块注入,由于didi组件有其特殊格式要求,我们倒着来介绍。

1、模块注入

didi提供一个Injector对象来对所有注入对象进行初始化:

import { Injector } from 'didi';
const modules = []; // 所有需要注入对象的集合
const injector = new Injector(modules );
// 初始化所有对象
injector.init();

2、模块声明

通过Injector初始化注入的对象需要满足特定的格式:

// ModuleDefinition 导入插件格式
{
  __exports__: [],
  __modules__: [ 'engine', 'license' ]; // 这里的对象最可作为初始化组件的时候传入的参数
  __init__: ['hifiComponent'],// 当前定义的模块名
  __depends__:[    
    AlignElementsModule,
    ContextPadModule,
    PopupMenuModule
  ],
  hifiComponent: [ 'type', HifiComponent ]
}
  • __modules__:这里的对象最可作为初始化组件的时候传入的参数,这里的用法和定义$inject的模块方式等同。
// 源码处理详细请看:didi/lib/annotation.js
export function annotate(...args) {

  if (args.length === 1 && isArray(args[0])) {
    args = args[0];
  }
  args = [ ...args ];
  const fn = args.pop();
  fn.$inject = args;
  return fn;
}
  • __depends__:注入当前定义模块内部的依赖, 在加载当前模块之前会预先加载这些依赖模块。
  • __exports__:导入内部依赖模块
  • __init__:这里定义当前导入的模块名,数组格式可定义多个,定义后需要添加对应的组件的初始化方式。
  • hifiComponent: [ 'type', HifiComponent ]:通过__init__定义后的模块指定配置对应的初始化方法

3、模块实现

模块的注入实现如下,通过$inject或者__modules__配置的参数可作为模块的参数传入

function Car(e, license) {
  // will inject components bound to 'engine' and 'license'
}

Car.$inject = [ 'engine', 'license' ]; // 这里和__modules__用法一致,

4、重写

模块的注册注入需要保证__init__定义的模块名的唯一性,若模块名相同则会导致后注册的模块功能被覆盖。这可以作为功能自定义覆盖的一种手段来使用。

使用细节

1、模块注册声明方式

上述,我们通过__init__声明模块名后需要提供模块注册方式:

const carModule = {

  // asked for 'car', the injector will call new Car(...) to produce it
  'car': ['type', Car],

  // asked for 'engine', the injector will call createPetrolEngine(...) to produce it
  'engine': ['factory', createPetrolEngine],

  // asked for 'power', the injector will give it number 1184
  'power': ['value', 1184] // probably Bugatti Veyron
};

如上示例,模块注册提供集中方式注册:

  • factory:通过factory方式注入则是直接调用函数进行注入,如上述:creatPetrolEngine(...)
  • value:这种方式直接给模块指定赋值
  • type:指定type方式会默认调用构造函数来注册,如上述:new Car(...)

聊一聊bpmn-js中的Viewer和Modeler

胖蔡阅读(190)

bpmn-js 阅读指南:

通过之前对于bpmn-js的学习,可以完成一个基础的Bpmn编辑器(或者叫建模器)的显示和简单绘制,若需要做更多工作还需加强对其的内部实现的了解。通过使用我们可以知道bpmn-js中有两个比较重要的操作对象:bpmnViewerbpmnModeler

  • Viewer:一般习惯性对齐命名对象称之为bpmnViewer,这是bpmn-js提供的一个基础的bpmn流程图的查看器,其主要是用于控制流程图绘制的显示区域部分。
  • ModelerBpmn建模器,这是整个流程编辑的核心构成部分,负责流程图的构建编辑,也是我们操作较多的部分。

我们使用的常规逻辑:viewer负责展示,modeler负责构建。仔细查看bpmn-js的实现可以看出Modeler我们可以理解成在Viewer上的一层扩展。

// viewer 创建
var bpmnViewer = new Viewer({ additionalModules: [ extensionModule ] });

// Viewer源码:构造函数
export default function Viewer(options) {
  BaseViewer.call(this, options);
}

// modeler 创建
var bpmnModeler = new Modeler({ additionalModules: [ overrideModule ]});

// Modeler.js源码:构建函数
export default function Modeler(options) {
  BaseModeler.call(this, options);
}

// BaseModler.js源码:构建函数
export default function BaseModeler(options) {
  BaseViewer.call(this, options);

  // hook ID collection into the modeler
  this.on('import.parse.complete', function(event) {
    if (!event.error) {
      this._collectIds(event.definitions, event.elementsById);
    }
  }, this);

  this.on('diagram.destroy', function() {
    this.get('moddle').ids.clear();
  }, this);
}

上述代码可以看出无论是Viewer还是Modeler其构建核心都是由BaseViewer提供的,查看BaseViewer构建的options配置参数类型如下:

// 构建参数
export type BaseViewerOptions = {
    width?: number | string; 
    height?: number | string;
    position?: string;
    container?: string | HTMLElement; // 绘制挂载父节点
    moddleExtensions?: ModdleExtensions; // 格式定义,通常适用于描述Activiti、flowable、camunda的描述文件
    additionalModules?: ModuleDeclaration[];// 扩展插件,这是bpmn-js中的核心功能
} & Record<string, any>;


// 默认参数
const DEFAULT_OPTIONS = {
  width: '100%',
  height: '100%',
  position: 'relative'
};

Modeler就是基于additionalModules功能给BaseViewer扩展了编辑建模功能,它扩展了Palette、AutoPlace、ContextPad、Move、Modeling等等模块来组件成一个建模器,这也是Bpmn-js留给开发人员扩展插件的入口,原则上可以完全由开发者自定义实现。

// additionalModules 扩展支持插件格式
export type ModuleDeclaration = {
  [name: string]: ServiceDeclaration<unknown> | unknown;
  __init__?: Array<string|InitializerFunction>;
  __depends__?: Array<ModuleDeclaration>;
  __exports__?: Array<string>;
  __modules__?: Array<ModuleDeclaration>;
};

如下为Modeler中的内置扩展模块,又或者叫建模组件:

  • AlignElements:这是几个元素对齐方式功能的插件的集合,包含contenxtpad部分、bpmn图形图像部分、popupMenu部分。
  • AutoPlacebpmn元素自动放置行为插件
  • AutoScroll:这个是使用的diagram-js中的auto-scroll特性功能插件
  • AutoResizeBPMN特定的调整大小行为
  • ContextPadBPMN特定的上下文填充提供程序【即元素选择右侧快捷面板操作区域】。
  • CopyPasteBPMN中的复制粘贴功能
  • DistributElements:为当前不支持分发的元素注册元素排除筛选器
  • EditorActions:注册并执行BPMN特定的编辑器操作。
  • GridSnapping:追踪网格移动事件,本质就是一个eventbus的监听器插件
  • InteractionEventsBPMN特定的命中区域和交互修复。
  • Keyboard:键盘绑定插件
  • LabelEditingBPMN流程图元素SVG中绘制label的展示和编辑插件
  •  Modeling:这是BPMNModeler的核心功能插件,实现modeler的主要功能:更新编辑
  • Palette:这是Modeler的工具栏区域,用于提供可绘制的BPMN元素来绘制BPMN流程图
  • ReplacePreview:替换上下文中所有可替换元素的视觉效果
  • Search:提供搜索BPMN元素的功能,不一定能用得到。

还有几个使用didi或者是diagram-js中的自带功能插件不一一介绍了,若有需要会另外提及。本篇文章先整体对bpmn-js中的模块做一个了解,后续会根据功能需要对其中的单个功能一一介绍使用以及如何扩展我们需要的定制功能。

【一周一荐】| 推荐一款Vite中加载svg的小工具

胖蔡阅读(181)

最近开发中使用到一个好玩的Vite三方小插件vite-plugin-svg-icons很实用,可以辅助我们开发过程中快速加载svg小图标。其原理是在Vite编译器的时候通过一次性的DOM操作将SVG插入DOM结构中,然后通过使用内联SVG进行加载访问,极大的方便了我们的小图标引用模式。 若只是少量的svg图片加载建议食用,若是svg图片数量过多并不建议采用(过多的svg文件插入也会导致我们的html文件过大)。

安装

通过上述了解后,接下我们来配置使用vite-plugin-svg-icons插件,该插件需要Vite支持。

$ npm i  vite-plugin-svg-icons -D // 或
$ yarn add vite-plugin-svg-icons -D // 或
$ pnpm i vite-plugin-svg-icons -D

由于其主要是在编译打包期生效,所以放在devDependencies中即可。

配置

安装完成后,需要在vite.config.ts中引入该插件,并配置对应svg地址:

// vite.config.ts
import * as path from 'node:path'
import type { ConfigEnv, UserConfig } from 'vite'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import Unocss from 'unocss/vite'

export default ({ mode }: ConfigEnv): UserConfig => {

  return {
    plugins: [
      vue(),
      vueJsx(),
      Unocss(),
      createSvgIconsPlugin({
        // 指定需要缓存的图标文件夹
        iconDirs: [path.resolve(process.cwd(), 'src/bpmn-icons')],
        // 指定symbolId格式
        symbolId: '[name]',
        customDomId: '__svg__icons__dom__',
      }),
    ],
}

vite.config.ts配置完成后还需要再程序的入口处引入virtual:svg-icons-register 库用于注册插入SVGDOM树:

// main.ts
import 'virtual:svg-icons-register'
.......

封装组件

完成上述的配置操作后,我们就可以封装一个内联SVG方便随时使用了,我这里使用的VUE3作为开发框架,你也可以使用React、nextjs等进行封装,原理相同。

// svg-icon.vue
<script lang="ts">
import { defineComponent } from 'vue'

export default defineComponent({
  name: 'BpmnIcon',
  inheritAttrs: false,
  props: {
    name: {
      type: String,
      default: 'bpmn-icon-process',
    },
    color: {
      type: String,
      default: '#1d1d1f',
    },
  },
})
</script>

<template>
  <svg class="bpmn-icon h-[40px] w-[40px]" aria-hidden="true">
    <use :xlink:href="`#${name}`" :fill="color" />
  </svg>
</template>

上述有用到unocss方式进行样式渲染,配置使用请参考前文:【开发一个Vue3组件】 | 如何创建一个本地库项目

使用

通过上述封装操作后,我们就可以和其他组件一样来使用这个组件了,这里使用的是我一个已有项目的代码不额外写一个测试页面了,因为使用的确特别的简单,示例代码如下:

// test.vue

<div class="panel-header flex items-center gap-5 bg-light-300 px-3 py-5">
     <SvgIcon name={bpmnIconName.value}></SvgIcon>
     <div class='inline-block'>
       <p class="mb-0 mb-1 text-[1.3em] font-bold text-black">{elementType.value}</p>
       <p class="mb-0 pb-0 text-[1em] text-black">{bpmnName.value}</p>
      </div>
</div>

展示效果如下:

插入的svg Dom如下:

多说一句,这个插件大家根据需求选择是否使用,若SVG实在过大过多还不如单次加载SVG来的更好,并非什么都是越全越好,选择合适的更为重要!