2024年1月

Web Components浅析

Web Components 是一种使用封装的、可重用的 HTML 标签、样式和行为来创建自定义元素的 Web 技术。

Web Components 自己本身不是一个规范,而是一套整体技术,包含下面3个独立规范:

  1. Custom Elements
    :允许开发者定义自己的 HTML 标签(考虑SEO,还是语义化为好)。

  2. Shadow DOM
    :用于封装样式和标记,不受外部 DOM 影响——天然自带Scope能力(本质是一组 JS API)。

  3. HTML Templates
    (<template> 和 <slot>):声明式的重用 HTML 代码片段。

但是,我觉得还应该包含:
ES Modules

Custom Elements 和 Shadow DOM 的可靠性是确定的,毕竟是标准的一部分。

Custom Elements

在2008年W3C 发布了第一个HTML公开草案,其是就是可以使用自定义标签的——2000年W3C准备用XHTML来替代HTML4,结果被抛弃!

XHTML,或者更准确地说是 XHTML 1.0,是一种基于 XML 的标记语言,旨在在网页设计中取代HTML 4.01。它由 W3C 推出,其规范在2000年1月成为官方的推荐标准。
XHTML1.0实际上是HTML 4.01的严格版本,并要求开发者遵循更加严格的语法规则
——XHTML基于XML,它对标记的正确性有更高的要求:

  • XHTML 元素必须被正确地嵌套。

  • XHTML 元素必须被关闭

  • 标签名必须用小写字母。

  • XHTML 文档必须拥有根元素。

  • XHTML需要开发者
    在文档开头声明正确的DOCTYPE
    ,而在实际的实践中,由于历史原因和混乱的标准,很多时候开发者并没有遵循正确的声明,导致页面以兼容模式而不是标准模式渲染。

  • 为了正确地作为XHTML传送,
    Web服务器需要设置MIME类型为application/xhtml+xml
    。不幸的是,一些浏览器对这种MIME类型的处理不理想,这使得开发者们更倾向于使用更通行的text/html,这实际上使XHTML变成了浏览器中被当作HTML解析的标记语言。

在 HTML5 之前,使用非标准标签通常会被视为不良实践,因为这可能导致不可预测的行为,尤其是在不同的浏览器之间。

然而,
HTML5 引入了一种更加宽容的解析规则,允许这些非标准标签存在
,浏览器不会因为碰到未知标签而破坏整个页面。即使如此,这些自定义标签没有任何默认的样式或行为,它们就像普通的 HTML 元素(默认为内联元素),除非通过 CSS 或 JavaScript 给予样式和行为。

自定义标签和自定义元素是两个相关但不同的概念
。它们代表着 web 开发中自定义组件的不同方面和不同阶段的发展。

自定义标签与自定义元素

自定义标签(非标准标签)

自定义标签:Custom Tags、Non-standard Tags、User-defined Tags……

自定义标签仅在语义上是自定义的
,而没有附加任何特殊的行为;

自定义元素(Custom Elements)

自定义元素是 Web Components 规范的一部分
,它允许开发者创建完全定制化和可重用的 HTML 元素。

与仅仅创建一个新的标签名不同,自定义元素能够拥有自己独特的行为和属性。

Custom Elements 规范定义了如何注册新的元素、如何附加行为、以及如何处理元素的生命周期事件(如创建、连接到文档、断开连接和属性更改时)。

自定义元素通常使用 customElements.define() 方法在 JavaScript 中注册,这样,当元素被添加到 DOM 时,就会与一个 JavaScript 类关联起来。这个类继承自 HTMLElement,允许它具备 DOM 接口的所有特性,并添加自定义的逻辑和样式。这意味着自定义元素不仅仅是形式上的定制,而是实现了真正的封装和功能拓展。

区别总结

  • 语义
    :自定义标签仅在语义上是自定义的,而没有附加任何特殊的行为;相反,自定义元素通过 Custom Elements API 注册,并可以包括复杂的逻辑和状态。

  • 功能性
    :自定义元素支持完整的生命周期管理,提供创建时、附加到 DOM、属性变动等时机的钩子,而自定义标签则没有这些功能。

  • 标准化
    :自定义元素是 Web Components 的官方标准之一,得到了浏览器的广泛支持;而自定义标签顾名思义,是非标准的,它们允许存在,但并不是 HTML 规范的一部分。

  • 兼容性
    :自定义元素需要浏览器支持相关的标准,虽然现在大多数现代浏览器都提供了支持,但在一些旧的浏览器中可能需要 polyfills;而自定义标签通常哪种浏览器都能解析,只是作为普通的元素看待。

custom element生命周期

在custom element的构造函数中,可以指定多个不同的回调函数,它们将会在元素的不同生命时期被调用:

  1. connectedCallback
    :当 custom element首次被插入文档DOM时,被调用。

  2. disconnectedCallback
    :当 custom element从文档DOM中删除时,被调用。

  3. adoptedCallback
    :当 custom element被移动到新的文档时,被调用。

  4. attributeChangedCallback
    : 当 custom element增加、删除、修改自身属性时,被调用。

具体参看:
https://developer.mozilla.org/zh-CN/docs/Web/API/Web_components/Using_custom_elements

Shadow DOM

Shadow DOM 主要目的:
封装与隔离
——相比iframe、frame更加轻量级。

  • IFrame是一个独立的html页面,shadow DOM是当前html页面的一个代码片段,

  • 不需要创建额外的渲染环境——不需要创建一个完整的文档环境,而是基于现有的上下文中创建封闭的DOM结构。

Shadow DOM接口是关键所在:它可以将一个隐藏的、独立的DOM附加到一个元素上,它以shadow root节点为起始根节点,在这个根节点的下方,可以是任意元素,和普通的DOM元素一样,但是这棵子树不在主DOM树中——即影子DOM是一种不属于主DOM树的独立的结构,所以Shadow DOM内部的元素
始终不会影响到它外部的元素(除了:focus-within),这就为封装提供了便利

  • Shadow host
    : 一个常规DOM节点,Shadow DOM会被附加到这个节点上。

  • Shadow tree
    : Shadow DOM内部的DOM树。

  • Shadow boundary
    : Shadow DOM结束的地方,也是常规DOM开始的地方。

  • Shadow root
    : Shadow tree的根节点。

Shadow DOM都不是一个新事物,在过去的很长一段时间里,浏览器用它来封装一些元素的内部结构,以一个有着默认播放控制按钮的<video>元素为例,我们所能看到的只是一个<video>标签,实际上,在它的Shadow DOM中,包含来一系列的按钮和其他控制器。

chrome设置Show user agent shadow DOM
shadow DOM标签示例

其结构如下:

而现在,我们可以来自己制造相关的标签(如video类似的功能模块)

怎么使用Shadow DOM

看这个就好:
https://developer.mozilla.org/zh-CN/docs/Web/API/Web_components/Using_shadow_DOM

但是,
https://github.com/Tencent/omi
等框架应该更加适合你!

HTML templates(HTML 模板)

这个用过vue的理解应该不难:

  • <template>包含一个 HTML 片段,不会在文档初始化时渲染。<slot>插槽,类似占位符,可以填充自己的内容。、

  • <slot>插槽,类似占位符,可以填充自己的内容。

但是真的要用的话,还是用omi等类似的框架。

为什么不用原生API

这个问题就是,为什么要用jQuery?为什么放弃jQuery使用vue或react?

其是Web Components 了解一下就好。

为什么不推荐使用Web Components

React 和 Vue 在组件化开发方面有自己的实现,并没有直接采用 Web Components 作为内部实现——不过,它们两者都提供了与 Web Components 兼容的接口。

下面是我个人感觉他们放弃Web Components的原因:

React放弃Web Components

  • 封装性
    :React 组件经常需要和一个复杂的状态以及生命周期方法交互,这些都不是 Web Components 标准提供的。

  • 性能优化
    :React 的虚拟 DOM 可以通过最小化实际的 DOM 操作来提升性能,这一点在批量更新 UI 或大型应用中尤为明显。

  • 跨平台
    :React 不仅仅用于 web 开发(通过 React Native,它也被用于移动应用开发),而 Webb Components 专注于 web 标准和浏览器环境。

  • 生态系统
    :React 拥有非常庞大且成熟的生态系统,包括状态管理(如 Redux)、路由(如 React Router)等各种工具和库。

Vue3放弃Web Components

  • 响应式系统
    :Vue 的响应式系统使得数据和视图能够自动同步更新,而 Web Components 没有内建这样的响应式机制。

  • 模板语法
    :Vue 通过其简洁的模板语法扩展了普通的 HTML,使开发者可以更加容易地描述复杂的 UI 结构,而 Web Components 使用的是普通 HTML 搭配 JavaScript。

  • 工具链支持
    :Vue CLI 提供了非常强大的工具链支持,包括项目脚手架、开发服务器、热重载等,而这些在 Web Components 中不是直接可用的。

  • 生态系统
    :与 React 类似,Vue 也拥有广泛的插件和支持库,例如 Vuex、Vue Router 等,这些让 Vue 应用开发更为完善。

尽管 React 和 Vue 没有直接采用 Web Components 作为内部实现,但它们都提供了对 Web Components 的支持

2011年,Alex Russel首次提出了Web Components的概率并首次演示了demo,这时候整套技术包括三个方面:scoped css,shadow DOM和Web components。W3C也在此时开始推进Web Components规范。

2012年,HTML Template很快被实现,作为wrapper包裹内容,在页面加载时不使用,在之后运行时实例化。同时Shadow DOM V0标准发布并被实现,并且
Ember和Angular开始计划支持Web Components,甚至基于它去做改造,但最终没有结果

至2018年,Web Components在主流浏览器中均被支持,但是并未达到普及程度,具体参看:
https://caniuse.com/?search=Web%20Components%20

但是,比如视频播放器、sql编辑器等超大件,还是非常适合Web Components的。不过这里还是推荐使用框架来做。

vue3项目Web Components案例

Vue 和 Web Components 是互补的技术,具体可以看官方文档:
https://cn.vuejs.org/guide/extras/web-components

Vue
在 Custom Elements Everywhere 测试中取得了 100% 的分数
。在 Vue 应用中使用自定义元素基本上与使用原生 HTML 元素的效果相同!

Vue 提供了一个和定义一般 Vue 组件几乎完全一致的 defineCustomElement 方法来支持创建自定义元素。这个方法接收的参数和 defineComponent 完全相同。

import { defineCustomElement } from 'vue'
const MyVueElement = defineCustomElement({
  // 同平常一样的 Vue 组件选项——正常的 Vue 组件选项都有
  props: {},
  emits: {},
  template: `...`,
  // defineCustomElement 特有的:注入进 shadow root 的 CSS
  styles: [`/* inlined css */`]
})
// 注册自定义元素,注册之后,所有此页面中的 `<my-vue-element>` 标签 都会被升级
customElements.define('my-vue-element', MyVueElement)
// 你也可以编程式地实例化元素(必须在注册之后) :
document.body.appendChild(
  new MyVueElement({
    // 初始化 props(可选)
  })
)
// 也可以直接使用
export default defineComponent({
  setup(){
    return ()=>(<my-vue-element/>)
  }
})

这样用,是不是非常爽!

vue3 使用Web Components需要注意的点:

failed to resolve component

默认情况下,Vue 会优先尝试将一个非原生的 HTML 标签解析为一个注册的 Vue 组件,如果失败则会将其渲染为自定义元素。这种行为会导致在开发模式下的 Vue 发出“failed to resolve component”的警告。所以需要告诉 Vue 将某些确切的元素作为自定义元素处理并跳过组件解析。在 vite.config.ts 配置:

{resolve: {
  alias: {//组件提供模板选项,但是在 Vue 这个构建中不支持运行时编译
    'vue': 'vue/dist/vue.esm-bundler.js',//需要配置你的 bundler 别名 vue: vue/dist/vue.esm-bundler.js
    '@': resolve(__dirname, 'src')
  }
},
plugins: [vue({
  template: {
    compilerOptions: {
      isCustomElement: tag => tag.startsWith('cus-')// 以 cus- 开头的作为自定义元素处理
    }
  }
})]}

Provide / Inject API

Provide / Inject API 和相应的组合式 API 在 Vue 定义的自定义元素中都可以正常工作。但是请注意,
依赖关系只在自定义元素之间起作用

但是为推荐费必要

插槽

在组件内部,可以像往常一样使用 <slot/> 渲染插槽。但是在解析最终生成的元素时,它只接受原生插槽语法:

  • 不支持作用域插槽。

  • 传递命名插槽时,请使用 slot attribute 而非 v-slot 指令

React项目Web Components案例

说实话,react原生来写干嘛呢?

如果要在react项目里面写,推荐使用
https://lit.dev/

或者使用
https://github.com/Tencent/omi/
来写个项目,打包成组件库,然后再业务里面使用!

Web Components 生态

Lit
:Lit是一个轻量的库,但它依然保留了web组件的所有特性。

Omi
:Web Components 框架.

Vaadin
: Vaadin 是以java作为开发语言的前端框架,它提供了一套以Web Components为基础的丰富的企业级UI组件库,关键他和spring结合的非常爽,比GWT用起来。

Ionic Framework
: 本来是为Angular构建的(4.x适配Angular、Vue 、React),Ionic4 Web端基于Web Components——具有更好的运行速度,相比以前版本的Ionic框架性能提升很多!优异的性能则
让 Ionic 成为了构建高性能 PWA 的最佳 UI 框架

说实话吧,Web Components 相比周边生态还是没有起来。可以作为大型项目某些模块的补充技术!

svelte
:前端框架新秀,原生支持Web Components。个人不是很了解,跳过!

Google 从 2013 年开始一直在持续推进的基于 Web Components 封装的类库,同时还开放了基于 Polymer 开发的组件集合
PolymerElements · GitHub
和开发周边。 2015 年 Google 正式发布 Polymer 1.0 ,注意时间点,当时还是Custom Elements v0 版标准 2017年Custom Elements v1 版标准在各大浏览器落地,Polymer 发布了 2.0,并且不再封装 Custom Elements API,不再默认使用shadowDOM、目标兼容各种框架,开始变成轻量级类库。

  • 由于起步几乎最早以及 google 背书,可能到现在也是影响用户数最大的 Web Components 基础库,Youtube 基于Polymer 对整站做了重构,Google 很多产品包括 Android 和 ChromeOS 平台也都用了 Polymer。

但个人觉得总体上相比与彼时流行的其他框架 Polymer 还是不温不火,Google 似乎也有同感、随着 Polymer 的轻量化升级,于是在 2018 年又发布了更现代化的 lit
GitHub - lit/lit: Lit is a simple library for building fast, lightweight web components.
包括 lit-html 模板渲染库
lit/packages/lit-html at main · lit/lit · GitHub
和基于 lit-html 的 lit-element
lit/packages/lit-element at main · lit/lit · GitHub
创建 Web Component 的 base class 。 Lit-html 基于 ES 的模板自变量和 template 标签,用注释节点去动态填充,没有JSX 转换虚拟 dom的过程,把大部分模板创建渲染的事都交给浏览器去做,提供了轻量的 api 让我们可以在 JS 中写 HTML-Templates。 Lit-Element 的 Reactive properties 、Scoped styles 等面向现代化 JS 语法的特点让他现在很受欢迎。 Google 推荐新用户使用 lit,但也将 Polymer 推到了 3.0 版本,放弃了 HTML Imports 转向 JS modules,并且支持 Polymer 跟 lit 混用,目前持续又维护和支持,
Slack Channel
上一直很活跃。

除了 Google 自己, 微软的 PWA stater
GitHub - pwa-builder/pwa-starter: Welcome to the PWABuilder pwa-starter! Looking to build a new Progressive Web App and not sure where to get started? This is what you are looking for!
,选择 lit 框架和 封装的 Web Components 作为基础库。 Adobe 基于 LitElement 封装并开放了
Spectrum Web Components
Sap 基于 Lit-html 封装并开源了
ui5-webcomponents/02-custom-UI5-Web-Components.md at master · SAP/ui5-webcomponents · GitHub
Red hat
GitHub - 1-Platform/op-components: One platform component library.
等众多公司使用了 lit 开发自己的组件库或平台。

另一个类库
GitHub - skatejs/skatejs: Effortless custom elements powered by modern view libraries.
也是基于 lit-html 的。

Web Components  头部案例

目前生成环境使用Web Components 的案例有这些(非全部使用!)

Twitter

Twitter 2016 年开始将自己的嵌入式推文  从 iframe 切换成  ShadowDOM,减少了内存消耗、加快了渲染速度,并批量渲染的时候保持丝滑。
Upcoming Change to Embedded Tweet Display on Web

Youtube

Youtube 作为 google 系的产品,很早就在全站用上了 Web Cmponents,并且开源了自己播放器组件
GitHub - GoogleWebComponents/google-youtube: YouTube video playback web component
此外 google 开源的 Web Components 还是很多的,
Google Web Components · GitHub
,包括地图、drive、日历等等。

Google Earth
:

Google Earth 的网页版使用了Web Components技术来创建用户交互界面的一部分。

EA

EA 的游戏工作室分布在全球各地,为了保证不同团队和工作室的设计开发体验统一,EA 基于 Web Components 构建了自己的 Network Design System,同时也支持这自己的 UIaaS。

Github

github 对 Web Components 的使用很早,具体可以看:
How we use Web Components at GitHub | The GitHub Blog
2014 年 Custom Elements v0 specification 出现的时候 github 就开始关注:
Search · topic:web-components org:github · GitHub
,并且开源了其中一系列 Web Components
GitHub - github/github-elements: GitHub’s Web Component collection.
2017 年 Custom Elements v1 版本在 chrome 和 safari 上相继实现之后,github 开始大范围使用

要知道 github 2018 年才刚刚完全移除 jquery
Removing jQuery from GitHub.com frontend | The GitHub Blog
这既得益于 github 自身项目组件化的架构,也 Web Components 本身与框架无关的特性非常识合作老项目升级。

github 还开源了 用于开发Web Components 的库 Catalyst:
GitHub - github/catalyst: Catalyst is a set of patterns and techniques for developing components within a complex application.

而他的思路借鉴了
Stimulus

LitElement

  • 既然提到了 Stimulus,就叉开讲讲这个东西,Stimulus 很适合对老项目改造,尤其是 ruby on rails、jsp 服务端渲染、没有 webpack 之类的前端工具链,技术栈多且混乱的项目。Stimulus 的思路就是通过 MutationObserver 监控元素的变化, 然后取元素、补绑事件或者修改引用。他的定位就很轻盈,就是配合HTML页面,提供动态交互支持,不像现在的很多框架,动辄就是整站重写。

同时 github 还开源了一个 View Component 框架用来在 ruby on rails 里面构建同构应用
GitHub - github/view_component: A framework for building reusable, testable & encapsulated view components in Ruby on Rails.

SalesForce

SalesForce 作为一家 ToB 服务的公司,面对各种不同技术栈的客户,选择 Web Components 原因有两点,一是需要一套统一的通用组件面向所有客户,二是在很多特定领域,很多客户很难对他们的传统技术体系做大规模升级,而引入 Web Components 可以避免这类技术改造风险。

他们开源了自己的 Web Components 组件库
Component Library
,并提供一整套基于 的企业级研发工具
GitHub - salesforce/lwc: LWC - A Blazing Fast, Enterprise-Grade Web Components Foundation
除了通过 LWC,让客户可以在自己的环境中基于组件库配置、开发、部署应用,SalesForce 还开放了自己的 SalesForce 工作平台 ,平台为所有客户提供一站式配置、部署和升级的能力。

Oracle

Oracle 在 2017 年开始在自己的
GitHub - oracle/oraclejet: Oracle JET is a modular JavaScript Extension Toolkit for developers working on client-side applications.
构建工具中增加了对 CustomElement 的支持,在此之前是用的是 jQueryUI。Oracle 对 WebComponents 对态度其实很值得 ToB 同行学习,他并没有刻意想拜托 jQuery,而是让 WebComponents 与现有的 jQuery、Knockout 并行使用,只在新功能上推进 WebComponents ,保持老项目稳定,在历史遗留和新技术之间保持了合理的平衡。而在 jet 的生态方面,他们也在持续建设 Web Component 驱动的共享组件中心
Building the future of Oracle JET Ecosystem | by João Tiago | Digital Transformation Research Group | Medium

ING:

荷兰国际集团(ING)在他们的网站和网上银行平台中大量使用了 Web Components,他们通过使用 Lion Web Components 库共享跨项目的UI组件。

Comcast:

Comcast 的 Xfinity产品线中的某些web应用使用了 Web Components。

Adobe Spectrum:

该站点是一个基于 Web Components 的 UI 框架产品

参考文章:

神奇的Shadow DOM
https://jelly.jd.com/article/6006b1045b6c6a01506c87ac

Vue3.2 实现 Web Components
https://ainyi.com/125

https://www.albertaz.com/blog/web-components-ststus


转载
本站
文章《
Web Components从技术解析到生态应用个人心得指北
》,
请注明出处:
https://www.zhoulujun.cn/html/webfront/SGML/htmlBase/2012_0823_9020.html

工作流是我们开发企业应用几乎必备的一项功能,工作流引擎发展至今已经有非常多的产品。最近正好在接触
Camunda
,所以来做个简单的入门整合介绍。如果您也刚好在调研或者刚开始计划接入,希望本文对您有所帮助。如果您是一名Java开发或Spring框架爱好者,欢迎关注我
程序猿DD
,持续非常技术干货。

Camunda简介

Camunda是一个灵活的工作流和流程自动化框架。其核心是一个运行在Java虚拟机内部的原生BPMN 2.0流程引擎。它可以嵌入到任何Java应用程序和任何运行时容器中。

动手整合Camunda

下面就来一步步动手尝试一下吧。

准备工作

  1. 使用Camunda提供的项目初始化工具
    Camunda Automation Platform 7 Initializr

如上图,包名之类的根据自己需要做好配置,最后输入管理账号和密码,点击
Generate Project
按钮,自动下载工程。

  1. 解压下载后的工程,使用
    IntelliJ IDEA
    打开,其项目结构

  1. 打开
    pom.xml
    文件,添加camunda依赖:
<dependency>
    <groupId>org.camunda.connect</groupId>
    <artifactId>camunda-connect-core</artifactId>
</dependency>

<dependency>
    <groupId>org.camunda.bpm</groupId>
    <artifactId>camunda-engine-plugin-connect</artifactId>
</dependency>

由于Camunda Automation Platform 7 Initializr默认的Spring Boot版本已经是3.1了,所以如果要做一些降级调整,可以手工修改
pom.xml

dependencyManagement
配置,比如下面这样:

<dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-dependencies</artifactId>
        <version>2.6.4</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>

      <dependency>
        <groupId>org.camunda.bpm</groupId>
        <artifactId>camunda-bom</artifactId>
        <version>7.15.0</version>
        <scope>import</scope>
        <type>pom</type>
      </dependency>
    </dependencies>
</dependencyManagement>
  1. 打开配置文件
    application.yaml
    ,可以看到类似下面的内容
spring.datasource.url: jdbc:h2:file:./camunda-h2-database

camunda.bpm.admin-user:
  id: transduck
  password: 666666666666
  • spring.datasource.url
    :工作流引擎使用的数据库配置,您也可以根据官网文档去调整到其他数据库中(尤其生产环境)。
  • camunda.bpm.admin-user
    :管理员账户配置,可以在这里修改用户名和密码

创建一个简单的工作流

下面我们尝试创建一个简单的工作流:

第一步,我们将请求用户提供两个输入:name和message
第二步,我们将这些输入传递给我们的服务以创建消息输出

开始编码:

  1. 创建第一步提到的数据模型
public class model {
    
    private String message;
    private String name;
    
    public model() { }
    
    public String getMessage() {
        return message;
    }
    
    public void setMessage(String message) {
        this.message = message;
    }
    
    public String getName() {
        return name;
    }
    
    public void setName(String name) {
        this.name = name;
    }
    
    @Override
    public String toString() {
        return "" + message + ", " + name;
    }
}
  1. 根据第二步,创建接收消息的接口
@RequestMapping("/")
@RestController
public class controller {

    Logger logger = Logger.getLogger(this.getClass().getName());

    @PostMapping("/message")
    public model createMessage(@RequestBody model model) {
        logger.info("-------Message Creator Initialized-------");

        model m = new model();
        m.setMessage(model.getMessage());
        m.setName(model.getName());

        logger.info("Message created --> " + m.toString());
        return m;
    }
}
  1. 下面我们可以开始创建工作流程图。在
    Camunda Modeler
    中打开我们项目的
    resources
    下的
    process.bpmn
    ,我们将看到类似下面的流程图:

图中带有小人的框称为
User Tasks
,是执行与用户相关的操作的步骤。如前面部分所述,在工作流程的第一步中,我们将请求用户输入两个输入:姓名和消息。无需添加新任务,更新现有的
User Tasks
即可解决问题。单击
User Tasks
,打开属性面板,在打开的面板中定义适合我们案例内容。

  1. 完成基本信息填写后,转到
    Form
    选项卡。

这是定义呈现给用户的表单选项卡。由于我们需要用户输入姓名和消息,因此我们定义两个名为“name”和“message”的表单字段。要定义表单字段,请单击“表单字段”旁边的加号图标。在打开的表单中,相应地填写 ID、类型和标签字段。对每个表单字段重复相同的步骤。

  1. 开始配置第二步,调用我们的接口。添加
    Service Task

具体操作方法:单击左侧菜单中的
Create Task
图标,然后将任务拖放到随机位置。单击任务后,单击
Change Type
图标,然后从菜单中选择
Service Task

  1. 填写基本信息

  1. 切换到
    Connector
    选项卡。这是定义 HTTP 信息和有关服务的数据的选项卡,在这里配置刚才定义的接口,具体如下图所示:


  1. Service Task
    连接到工作流程中。先删除
    User Tasks

    End Event
    之间的箭头。然后,单击
    User Tasks
    并从菜单中选择箭头图标。将箭头连接到
    Service Task
    。最后,再连接
    Service Task

    End Event

启动测试

在完成了上面的编码和工作流程配置后,我们就可以在调试模式下运行项目了。

启动完成后,在浏览器上访问地址
http://localhost:8080/
,您将看到 Camunda 登录页面:

输入您在
application.yaml
中配置的管理员配置信息,进入后台:

从应用程序主页中选择
Tasklist
,可看到如下界面:

然后在任务列表页面上单击
Add a simple filter
选项。单击后,您将看到名为
All Tasks (0)
的过滤器已添加到列表中,继续单击
Start process
选项来运行我们准备好的工作流程。

选择您的工作流进程,然后单击
Start button
,无需提供任何其他信息。

最后,单击
Created
下列出的
Get Input
任务。如果您没有看到该任务,请刷新页面。

您将看到我们在第一步中定义的表单。要填写表格,请单击右上角
Claim
选项。然后,根据您的喜好填写表格并单击
Complete
按钮。

当工作流执行
Service Task
并且服务运行时,您将看到列表再次变空。如果工作流成功执行了第二步,我们应该能够在控制台中看到输出。

小结

本文介绍了使用Spring Boot和Camunda创建一个简单工作流的完整步骤,希望对您有所帮助。如果您学习过程中如遇困难?可以加入我们超高质量的
Spring技术交流群
,参与交流与讨论,更好的学习与进步!更多
Spring Boot教程可以点击直达!
,欢迎收藏与转发支持!

欢迎关注我的公众号:程序猿DD。第一时间了解前沿行业消息、分享深度技术干货、获取优质学习资源

这应该是java最好用的orm之一了

说起orm大家肯定都不会陌生,作者是一个.net菜鸟。并且是在.net繁荣的orm圈子下成长的,所以这次给大家带来的是媲美efcore,freesql,sqlsugar的java的orm.如果你是一位.net转java的开发,或者是一名需要经常和数据库打交道的开发者和作者一样是一名crud仔那么这个orm肯定是你不应该错过的,我愿称之为java最好用的orm之一。

介绍

easy-query

文档地址
https://xuejm.gitee.io/easy-query-doc/

GITHUB地址
https://github.com/xuejmnet/easy-query

GITEE地址
https://gitee.com/xuejm/easy-query

复杂sql一

话不多说来一个复杂sql

        SELECT
        YEAR(日期) AS 年份,
        MONTH(日期) AS 月份
        SUM(收入) AS 月收入
        FROM
                your_table
        WHERE
        日期 >= CURDATE()- INTERVAL 3 MONTH
        GROUP BY
        年份,月份
        ORDER BY
        年份,月份;

用java的写法写sql,并且函数自适应easy-query支持的所有数据库,切库0成本

List<Draft3<Integer, Integer, Integer>> list = easyEntityQuery.queryable(BlogEntity.class)
          .where(o -> o.createTime().gt(o._now().plusMonths(-3))) //WHERE 日期 >= CURDATE()- INTERVAL 3 MONTH
          .groupBy(o -> GroupKeys.TABLE1.of(o.createTime().year(), o.createTime().month()))//GROUP BY 年份,月份
          .orderBy(o -> {
              o.key1().asc();  // ORDER BY 年份,月份;
              o.key2().asc();
          }).selectDraft(o -> Select.draft( //采用草稿类型
                  o.key1(), //YEAR(日期) AS 年份,
                  o.key2(), //MONTH(日期) AS 月份
                  o.sum(o.group().star())  //SUM(收入) AS 月收入
          )).toList();


==> Preparing: SELECT YEAR(t.`create_time`) AS `value1`,MONTH(t.`create_time`) AS `value2`,SUM(t.`star`) AS `value3` FROM `t_blog` t WHERE t.`deleted` = ? AND  t.`create_time` > date_add(NOW(), interval (?) month) GROUP BY YEAR(t.`create_time`),MONTH(t.`create_time`) ORDER BY YEAR(t.`create_time`) ASC,MONTH(t.`create_time`) ASC
==> Parameters: false(Boolean),-3(Integer)
<== Time Elapsed: 4(ms)
<== Total: 0

复杂sql二


select a.id,a.name
from table a
where (select count(*) as num from table b where b.box_id=a.id ) = 0

//条件里面不直接使用列

        List<Draft2<String, String>> list = easyEntityQuery.queryable(BlogEntity.class)
                .where(o -> {

                    Query<Long> longQuery = easyEntityQuery.queryable(Topic.class)
                            .where(x -> x.id().eq(o.id())).selectCount();//创建子查询的count然后和0常量进行比较

                    o.SQLParameter().valueOf(0L)
                            .eq(longQuery);
                }).selectDraft(o -> Select.draft(
                        o.id(),
                        o.url()
                )).toList();

==> Preparing: SELECT t.`id` AS `value1`,t.`url` AS `value2` FROM `t_blog` t WHERE t.`deleted` = ? AND ? = (SELECT COUNT(*) FROM `t_topic` t1 WHERE t1.`id` = t.`id`)
==> Parameters: false(Boolean),0(Long)

复杂sql三

//select 查询子表联结
select a,b,c,(select count(*) from a t1 where t.id=b.id) as xx from b


List<Draft3<String, String, Long>> list = easyEntityQuery.queryable(BlogEntity.class)
                .where(o -> {
                    o.id().eq("123");
                }).selectDraft(o -> Select.draft(
                        o.id(),
                        o.url(),
                        o.subQuery(() -> easyEntityQuery.queryable(Topic.class).where(x -> x.id().eq(o.id())).selectCount())
                )).toList();

==> Preparing: SELECT t.`id` AS `value1`,t.`url` AS `value2`,(SELECT COUNT(*) FROM `t_topic` t1 WHERE t1.`id` = t.`id`) AS `value3` FROM `t_blog` t WHERE t.`deleted` = ? AND t.`id` = ?
==> Parameters: false(Boolean),123(String)

可能会有人说这不就是拼sql吗,对的你没有说错就是但是这是强类型的并且是支持所有库的,还有一点jpa你说无法控制sql你不想用,我这个框架完全自主控制sql支持强类型我想你应该没有拒绝的理由。

单表

//根据条件查询表中的第一条记录
List<Topic> topics = easyEntityProxy
                .queryable(Topic.class)
                .limit(1)
                .toList();
==> Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM t_topic t LIMIT 1
<== Total: 1

//根据条件查询表中的第一条记录
Topic topic = easyEntityProxy
                .queryable(Topic.class)
                .firstOrNull();
==> Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM t_topic t LIMIT 1
<== Total: 1 

//根据条件查询id为3的记录
Topic topic = easyEntityProxy
        .queryable(Topic.class)
        .where(o->o.id().eq("3"))
        .firstOrNull();
==> Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM t_topic t WHERE t.`id` = ? LIMIT 1
==> Parameters: 3(String)
<== Total: 1


Topic topic = easyEntityProxy
        .queryable(Topic.class)
        .where(o->{
                o.id().eq("3");
                o.title().like("3");
        })
        .firstOrNull();
==> Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM t_topic t WHERE t.`id` = ? AND t.`title` like ? LIMIT 1
==> Parameters: 3(String),%3%(String)
<== Total: 1

多表


Topic topic = easyEntityQuery
        .queryable(Topic.class)
        .leftJoin(BlogEntity.class, (t,b) -> t.id().eq(b.id()))
        .where((t,b) -> t.id().eq("3"))
        .firstOrNull();

==> Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM t_topic t LEFT JOIN t_blog t1 ON t.`id` = t1.`id` WHERE t.`id` = ? LIMIT 1
==> Parameters: 3(String)
<== Total: 1

List<BlogEntity> blogEntities = easyEntityQuery
        .queryable(Topic.class)
        //join 后面是多参数委托,第一个主表,第二个参数为join表
        .innerJoin(BlogEntity.class, (t,b) -> t.id().eq(b.id()))
        .where((t,b) -> {
                t.title().isNotNull();
                b.id().eq("3");
        })
        //select 参数个数和join表个数一样,group后参数为一个,返回一个对象代理
        //可以对其进行自定义赋值比如id().set(t.title())将title赋值给id属性
        .select((t,b)->new BlogEntityProxy().selectAll(t))
        .toList();

==> Preparing: SELECT t1.`id`,t1.`create_time`,t1.`update_time`,t1.`create_by`,t1.`update_by`,t1.`deleted`,t1.`title`,t1.`content`,t1.`url`,t1.`star`,t1.`publish_time`,t1.`score`,t1.`status`,t1.`order`,t1.`is_top`,t1.`top` FROM t_topic t INNER JOIN t_blog t1 ON t.`id` = t1.`id` WHERE t1.`title` IS NOT NULL AND t.`id` = ?
==> Parameters: 3(String)
<== Total: 1

数据库函数支持

提供了常用的字符串
trim
,
leftPad
,
subString
,
toLower
,
isBank
,
nullOrDefault
等,时间类型的
format
,
druation
,
plus
,
year
,
month
等函数...


            List<Topic> list2 = easyEntityQuery.queryable(Topic.class)
                    .where(o -> {
                        o.createTime().le(o.createTime().nullOrDefault(LocalDateTime.of(2022, 1, 1, 1, 1)));
                        o.id().isNotBank();
                        o.id().nullOrDefault("" ).eq(o.title().nullOrDefault(c -> c.column(o.id())));
                        o.title().isEmpty();
                    })
                    .toList();

==> Preparing: SELECT `id`,`stars`,`title`,`create_time` FROM `t_topic` WHERE  `create_time` <= IFNULL(`create_time`,?) AND (`id` IS NOT NULL AND `id` <> '' AND LTRIM(`id`) <> '') AND IFNULL(`id`,?) = IFNULL(`title`,`id`) AND (`title` IS NULL OR `title` = '')
==> Parameters: 2022-01-01T01:01(LocalDateTime),(String)
<== Time Elapsed: 4(ms)
<== Total: 2

//pgsql
Draft7<Long, Long, Long, Long, Long, Long, Long> draft3 = entityQuery.queryable(BlogEntity.class)
                .whereById(id)
                .selectDraft(o -> Select.draft(
                        o.createTime().duration(o.updateTime(), DateTimeDurationEnum.Days).abs(),//计算createTime和updateTime相差的天数如果createTime小则返回负数 因为是abs所以返回的是肯定是相差天数
                        o.createTime().duration(o.updateTime(), DateTimeDurationEnum.Hours),//同理返回的是小时数
                        o.createTime().duration(o.updateTime(), DateTimeDurationEnum.Minutes),//同理返回分钟数
                        o.createTime().duration(o.updateTime(), DateTimeDurationEnum.Seconds),//同理返回秒数
                        o.createTime().duration(o.createTime().plus(1,TimeUnit.DAYS), DateTimeDurationEnum.Days),//计算createTime和createTime加上1天后的相差天数
                        o.createTime().duration(o.createTime().plus(2,TimeUnit.SECONDS),DateTimeDurationEnum.Seconds),
                        o.createTime().duration(o.createTime().plus(3,TimeUnit.MINUTES),DateTimeDurationEnum.Minutes)
                )).firstOrNull();

==> Preparing: SELECT ABS((extract(epoch from (t."create_time")::timestamp-(t."update_time")::timestamp)/86400)::int) AS "value1",(extract(epoch from (t."create_time")::timestamp-(t."update_time")::timestamp)/3600)::int AS "value2",(extract(epoch from (t."create_time")::timestamp-(t."update_time")::timestamp)/60)::int AS "value3",(extract(epoch from (t."create_time")::timestamp-(t."update_time")::timestamp))::int AS "value4",(extract(epoch from (t."create_time")::timestamp-((t."create_time" + INTERVAL '86400 second'))::timestamp)/86400)::int AS "value5",(extract(epoch from (t."create_time")::timestamp-((t."create_time" + INTERVAL '2 second'))::timestamp))::int AS "value6",(extract(epoch from (t."create_time")::timestamp-((t."create_time" + INTERVAL '180 second'))::timestamp)/60)::int AS "value7" FROM "t_blog" t WHERE t."deleted" = ? AND t."id" = ? LIMIT 1
==> Parameters: false(Boolean),123456zz9(String)
<== Time Elapsed: 3(ms)
<== Total: 1

匿名类型平替

List<Draft4<String, String, String, String>> list = easyEntityQuery.queryable(Topic.class)
                .where(o -> {
                    o.title().subString(1, 2).eq("123");
                    o.title().toLower().subString(1, 2).eq("123");
                    o.title().toLower().toUpper().toLower().subString(1, 2).eq("123");
                    o.createTime()
                            .format("yyyy-MM")//日期先格式化
                            .toLower()//然后转成小写
                            .subString(1, 10)//分割从第一位
                            .like("023-01");
                })
                .selectDraft(o -> Select.draft(
                        o.id(),
                        o.title().toLower().replace("123","456"),
                        o.title().toUpper(),
                        o.title().toLower().subString(1, 2)
                ))
                .toList();

==> Preparing: SELECT t.`id` AS `value1`,REPLACE(LOWER(t.`title`),?,?) AS `value2`,UPPER(t.`title`) AS `value3`,SUBSTR(LOWER(t.`title`),2,2) AS `value4` FROM `t_topic` t WHERE SUBSTR(t.`title`,2,2) = ? AND SUBSTR(LOWER(t.`title`),2,2) = ? AND SUBSTR(LOWER(UPPER(LOWER(t.`title`))),2,2) = ? AND SUBSTR(LOWER(DATE_FORMAT(t.`create_time`,'%Y-%m')),2,10) LIKE ?
==> Parameters: 123(String),456(String),123(String),123(String),123(String),%023-01%(String)
<== Time Elapsed: 2(ms)
<== Total: 0

强类型

因为topicType为枚举类型所以后续的操作都用枚举类型并且有智能提示如果使用其他类型ide会进行报错无法编译通过


@Data
@Table("t_topic_type")
@EntityFileProxy
@ToString
public class TopicTypeTest1 implements ProxyEntityAvailable<TopicTypeTest1 , TopicTypeTest1Proxy> {

    @Column(primaryKey = true)
    private String id;
    private Integer stars;
    private String title;
    @Column(value = "topic_type",conversion = EnumConverter.class)
    private TopicTypeEnum topicType;
    private LocalDateTime createTime;

    @Override
    public Class<TopicTypeTest1Proxy> proxyTableClass() {
        return TopicTypeTest1Proxy.class;
    }
}


public enum TopicTypeEnum implements IEnum<TopicTypeEnum> {
    STUDENT(1),

    TEACHER(3),

    CLASSER(9);
    @EnumValue
    private final Integer code;

    TopicTypeEnum(Integer code){

        this.code = code;
    }
    @Override
    public Integer getCode() {
        return code;
    }
//......省略
}
List<TopicTypeTest1> list1 = easyEntityQuery.queryable(TopicTypeTest1.class)
                .where(o -> {
                    o.topicType().nullOrDefault(TopicTypeEnum.CLASSER).eq(TopicTypeEnum.STUDENT);
                    o.topicType().eq(TopicTypeEnum.STUDENT);
                }).toList();

==> Preparing: SELECT `id`,`stars`,`title`,`topic_type`,`create_time` FROM `t_topic_type` WHERE IFNULL(`topic_type`,?) = ? AND `topic_type` = ?
==> Parameters: 9(Integer),1(Integer),1(Integer)
<== Time Elapsed: 2(ms)
<== Total: 0

开箱即用的api

第一条

Topic topic = easyEntityQuery.queryable(Topic.class)
                    .where(o -> o.id().eq("123"))
                    .firstOrNull();

==> Preparing: SELECT `id`,`stars`,`title`,`create_time` FROM `t_topic` WHERE `id` = ? LIMIT 1
==> Parameters: 123(String)

至多一条

Topic topic = easyEntityQuery.queryable(Topic.class)
                    .where(o -> o.id().eq("123"))
                    .singleOrNull();

==> Preparing: SELECT `id`,`stars`,`title`,`create_time` FROM `t_topic` WHERE `id` = ?
==> Parameters: 123(String)

多条

List<Topic> topics = easyEntityQuery.queryable(Topic.class)
                    .where(o -> o.id().eq("123"))
                    .toList();

==> Preparing: SELECT `id`,`stars`,`title`,`create_time` FROM `t_topic` WHERE `id` = ?
==> Parameters: 123(String)

分页

EasyPageResult<Topic> topicPageResult = easyEntityQuery
                .queryable(Topic.class)
                .where(o -> o.id().isNotNull())
                .toPageResult(1, 20);

==> Preparing: SELECT  COUNT(*)  FROM t_topic t WHERE t.`id` IS NOT NULL
<== Total: 1
==> Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM t_topic t WHERE t.`id` IS NOT NULL LIMIT 20
<== Total: 20

streamApi配合


        Optional<Topic> traceId1 = easyProxyQuery.queryable(TopicProxy.createTable())
                .filterConfigure(NotNullOrEmptyValueFilter.DEFAULT)
                .where(o -> o.eq(o.t().id(), "1"))
                .fetch(o -> {//o为Stream<Topic>类型
                    return o.findFirst();
                },1);

==> Preparing: SELECT `id`,`stars`,`title`,`create_time` FROM `t_topic` WHERE `id` = ?
==> Parameters: 1(String)
<== Time Elapsed: 2(ms)

所见所得的sql


@Data
@EntityFileProxy
public class  QueryVO {
    private String id;
    private String field1;
    private String field2;
}

List<QueryVO> list = easyEntityQuery.queryable(Topic.class)
                    //第一个join采用双参数,参数1表示第一张表Topic 参数2表示第二张表 BlogEntity
                    .leftJoin(BlogEntity.class, (t, t1) -> t.id().eq(t1.id()))
                    //第二个join采用三参数,参数1表示第一张表Topic 参数2表示第二张表 BlogEntity 第三个参数表示第三张表 SysUser
                    .leftJoin(SysUser.class, (t, t1, t2) -> t.id().eq(t2.id()))
                    .where(o -> o.id().eq("123"))//单个条件where参数为主表Topic
                    //支持单个参数或者全参数,全参数个数为主表+join表个数 链式写法期间可以通过then来切换操作表
                    .where((t, t1, t2) -> {
                        t.id().eq("123");
                        t1.title().like("456");
                        t2.createTime().eq(LocalDateTime.of(2021, 1, 1, 1, 1));
                    })
                    .select((t, t1, t2) -> new QueryVOProxy().adapter(r->{
                        r.selectAll(t);//因为结果只有并没有其他属性所以能够映射上的只有t.id所以只会查询t.id
                        r.selectIgnores(t.title());//可写可不写因为VO没有title所以不会映射查询
                        r.field1().set(t1.title());//别名映射
                        r.field2().set(t2.id());//别名映射
                    })).toList();

==> Preparing: SELECT t.`id`,t1.`title` AS `field1`,t2.`id` AS `field2` FROM `t_topic` t LEFT JOIN `t_blog` t1 ON t1.`deleted` = ? AND t.`id` = t1.`id` LEFT JOIN `easy-query-test`.`t_sys_user` t2 ON t.`id` = t2.`id` WHERE t.`id` = ? AND t.`id` = ? AND t1.`title` LIKE ? AND t2.`create_time` = ?
==> Parameters: false(Boolean),123(String),123(String),%456%(String),2021-01-01T01:01(LocalDateTime)
<== Time Elapsed: 2(ms)
<== Total: 0

group感知

因为group后无法对结果进行展开所以需要有group感知

 List<BlogEntity> page = easyEntityQuery
                .queryable(Topic.class)
                .innerJoin(BlogEntity.class, (t, t1) -> t.id().eq(t1.id()))
                .where((t, t1) -> t1.title().isNotNull())
                .groupBy((t, t1) -> GroupKeys.TABLE2.of(t1.id()))
                .select((g) -> new BlogEntityProxy().adapter(r->{
                    r.selectExpression(g.key1());//group只对t1.id进行了分组所以这边只有key1可以选择
                    r.score().set(g.sum(g.group().t2.score()));//因为是join了一张表所以g.group里面其实是tuple2里面有t1和t2两张表
                }))
                .toList();

==> Preparing: SELECT t1.`id`,SUM(t1.`score`) AS `score` FROM `t_topic` t INNER JOIN `t_blog` t1 ON t1.`deleted` = ? AND t.`id` = t1.`id` WHERE t1.`title` IS NOT NULL GROUP BY t1.`id`
==> Parameters: false(Boolean)
<== Time Elapsed: 5(ms)
<== Total: 100

匿名类型平替

无需定义别名可以直接返回并且拥有强类型,可以作为匿名表继续查询无需定义中间表


        List<Draft2<String, String>> list = easyEntityQuery.queryable(Topic.class)
                .where(o -> {
                    o.title().trimEnd().trimStart().eq(o.id().trimStart());
                    o.createTime().format("yyyy-MM-dd").subString(0, 4).eq("2021");
                })
                .selectDraft(o -> Select.draft(
                        o.id(),
                        o.title().toLower()
                ))
                .toList();

==> Preparing: SELECT t.`id` AS `value1`,LOWER(t.`title`) AS `value2` FROM `t_topic` t WHERE LTRIM(RTRIM(t.`title`)) = LTRIM(t.`id`) AND SUBSTR(DATE_FORMAT(t.`create_time`,'%Y-%m-%d'),1,4) = ?
==> Parameters: 2021(String)
<== Time Elapsed: 2(ms)
<== Total: 0

之前看到有人发过.net的orm语法实现,这次我们通过模拟那个作者的orm语法来看看easy-query和其相差多少

var query = rep.GetLambdaQuery().Take(100);
var join = query.Select(b => new { a1 = b.Id, a2 = b.F_String }).Join<TestEntityItem>((a, b) => a.a1 == b.TestEntityId);//第一次关联
varjoin2 = join.Select((a, b) => new { a3 = a.a1, a4 = b.Name })
      .Join<TestEntity>((a, b) => a.a3 == b.Id);//第二次关联
  join2.Select((a, b) => new
  {
      a.a4,
      b.Id
  });

这个SQL是一个很明显的匿名sql之间的join处理,一眼看过去基本就大致猜到具体的sql含义所以在表达式这方面其实还是很容易只晓得

select 
  t4.[a4], 
  t1.[Id] 
from 
  (
    select 
      t2.[a1] as a3, 
      t3.[Name] as a4 
    from 
      (
        select 
          t1.[Id] as a1, 
          t1.[F_String] as a2 
        from 
          [TestEntity] t1 
        LIMIT 0, 100
      ) t2 
      Inner join [TestEntityItem] t3 on t2.a1 = t3.[TestEntityId]
  ) t4 
  Inner join [TestEntity] t1 on t4.a3 = t1.[Id]
//selectDraft简单理解为.net的匿名类型但是在java这边没有匿名类型所以通过一种草稿类型来对应可以防止简单功能需要重新定义类本质是元祖tuple1-10
    EntityQueryable<TopicProxy, Topic> query = easyEntityQuery.queryable(Topic.class).limit(100);

    EntityQueryable2<Draft2Proxy<String, Integer>, Draft2<String, Integer>, BlogEntityProxy, BlogEntity> join = query.selectDraft(o -> Select.draft(o.id(), o.stars()))
            .leftJoin(BlogEntity.class, (t, t1) -> t.value1().eq(t1.id()));
    EntityQueryable2<Draft2Proxy<String, String>, Draft2<String, String>, BlogEntityProxy, BlogEntity> join2 = join.selectDraft((a, b) -> Select.draft(a.value1(), b.url()))
            .innerJoin(BlogEntity.class, (t, t1) -> t.value2().eq(t1.id()));

    List<Draft2<String, String>> list = join2.selectDraft((a, b) -> Select.draft(a.value1(), b.url())).toList();

//我们把局部变量去掉

            List<Draft2<String, String>> list = easyEntityQuery.queryable(Topic.class).limit(100)
                    .selectDraft(o -> Select.draft(o.id(), o.stars()))
                    .leftJoin(BlogEntity.class, (t, t1) -> t.value1().eq(t1.id()))
                    .selectDraft((a, b) -> Select.draft(a.value1(), b.url()))
                    .innerJoin(BlogEntity.class, (t, t1) -> t.value2().eq(t1.id()))
                    .selectDraft((a, b) -> Select.draft(a.value1(), b.url())).toList();

SELECT
    t3.`value1` AS `value1`,
    t4.`url` AS `value2` 
FROM
    (SELECT
        t1.`value1` AS `value1`,
        t2.`url` AS `value2` 
    FROM
        (SELECT
            t.`id` AS `value1`,
            t.`stars` AS `value2` 
        FROM
            `t_topic` t LIMIT 100) t1 
    LEFT JOIN
        `t_blog` t2 
            ON t2.`deleted` = false 
            AND t1.`value1` = t2.`id`
        ) t3 
INNER JOIN
    `t_blog` t4 
        ON t4.`deleted` = false 
        AND t3.`value2` = t4.`id`

或许你是一位java原住民,或许你是一位c#开发,在java语言贫瘠的时候我相信一款优雅的orm能够让你在编写crud的时候放松神经不需要去考虑mybatis这种sql模版带来的心智负担,因为mybatis把所有问题都抛给了用户所以你们一直觉得mybatis好,其实是你们被mybatis调教的好,因为一个框架只要提供的功能足够少那么他需要维护的就足够少,出问题也会足够少,所以你拿诺基亚半个月不需要充电和2024年的智能机一天一冲比较哪个优秀我觉得你应该是赢了

最后

可能有很多小伙伴会推荐我jpa或者jooq我想说如果我没能力那么我可能会选择他们,如果他们支持国产数据库我可能会选择他们,但是你我更愿意推荐
easy-query
因为我会聆听开发者的声音起码你叫的动我,我是一个在crud混的菜鸟开发,crud的困难,orm的困难必须是一个混迹在业务开发的程序员才能开发出来的好框架,在没开发出这个api的时候已经有很多小伙伴使用lambda的api进行了开发反向非常不错,期待您的使用。

easy-query

文档地址
https://xuejm.gitee.io/easy-query-doc/

GITHUB地址
https://github.com/xuejmnet/easy-query

GITEE地址
https://gitee.com/xuejm/easy-query

一、前言

到这篇文章为止,关于.NET "温故知新"系列的基础知识就完结了,从这一系列的系统回顾和再学习,对于.NET core、ASP.NET CORE又有了一个新的认识。

不光是从使用,还包括这些知识点的原理,虽然深入原理谈不上,但对于日常使用也够了,我想的是知其然,知其所以然。

在实际开发过程中可能是知道怎么使用就行,但系统学习了这些基本的框架、组件、或者说原理后,对于我们软件设计、开发、扩展和解决问题还是有帮助的。

刚好到2023新年前赶着写完,也算对自己这个系列的一个交代,实际上我平时基本不使用ASP.NET CORE,目前我主要开发桌面程序,还是用的winform。

写这个系列的初衷是想紧跟.NET的发展进程,同时储备基础知识,平时还搞一些微服务(Java)、NLP、OCR、知识图谱、前端(Vue3),只要需要反正啥都搞,没必要固执,技术只是手段,不是目的。

那么接下来就继续简单的梳理一下中间件,欢迎对这个系列拍砖!

二、中间件

中间件是一种装配到应用管道以处理请求和响应的软件。 每个组件:

  • 选择是否将请求传递到管道中的下一个组件。
  • 可在管道中的下一个组件前后执行工作。

这个是关于中间件概念的概括,官方的概括是相当精准,那么我们就围绕管道、传递、组件来看看中间件。

请求委托用于生成请求管道。 请求委托处理每个 HTTP 请求。使用 Run、Map 和 Use 扩展方法来配置请求委托。

我们照例新建一个ASP.NET CORE Web API 项目:WebAPI_Middleware

namespace WebAPI_Middleware
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            // Add services to the container.

            builder.Services.AddControllers();
            // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
            builder.Services.AddEndpointsApiExplorer();
            builder.Services.AddSwaggerGen();

            var app = builder.Build();

            // Configure the HTTP request pipeline.
            if (app.Environment.IsDevelopment())
            {
                app.UseSwagger();
                app.UseSwaggerUI();
            }

            app.UseHttpsRedirection();

            app.UseAuthorization();


            app.MapControllers();

            app.Run();
        }
    }
}

在Program.cs 中我们看到前面部分
builder
是配置依赖注入的东西,这部分可以参看
.net 温故知新【13】:Asp.Net Core WebAPI 使用依赖注入DI

app
使用
Use
扩展用于中间件添加到管道中

Map
基于给定请求路径的匹配项来创建请求管道分支

Run
委托始终为终端,用于终止管道。

中间件的执行顺序过程如下:

image

三、Map

我们将上面自动创建的东西全都删除,用
Map
来匹配路由,然后通过不同的代理处理请求。

    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            var app = builder.Build();
           
            //匹配map1 请求
            app.Map("/map1", new Action<IApplicationBuilder>((app) =>
            {
                app.Run(async context =>
                {
                    await context.Response.WriteAsync("map1 run");
                });
            }));
            //匹配map2 请求
            app.Map("/map2", new Action<IApplicationBuilder>((app) =>
            {
                app.Run(async context =>
                {
                    await context.Response.WriteAsync("map2 run");
                });
            }));

            app.Run();
        }
    }
  • 请求map1 我们输出:map1 run

image

  • 请求map2 我们输出:map2 run

image

Asp.Net Core
MapControllers
的扩展方法也是类似道理,用来匹配路由调用处理程序。

四、Run

在上面的 Map 后面我们使用的处理方法中 Run 用于终止管道。也就是说在该管道中如果调用了 Run 那么就直接返回了,即使你后面还添加了 Use 也不会执行。

app.Run(async context =>
{
    await context.Response.WriteAsync("map1 run");
});

Map 相当于是迎客进门,Map 上了就用指定的管道进行处理,如果没有 Map 上就调用主管道,也就是主管道上的其他中间件也会执行处理。比如我们再加一个 Run 用于没匹配上路由也输出点信息。

image

加了
context.Response.ContentType = "text/plain; charset=utf-8";
不然中文会乱码。

image

因为 Run 是终结点,那这个管道中我还想加其他处理怎么办呢,这个时候就该轮到 Use 出场了。

五、Use

用 Use 将多个请求委托链接在一起。 next 参数表示管道中的下一个委托。 可通过不调用 next 参数使管道短路。

首先我们在外面添加两个 Use,不放到 Map 中,这样的话就只有未匹配到的路由会调用

    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            var app = builder.Build();

            

            app.Map("/map1", new Action<IApplicationBuilder>((app) =>
            {
                app.Run(async context =>
                {
                    await context.Response.WriteAsync("map1 run");
                });
            }));

            app.Map("/map2", new Action<IApplicationBuilder>((app) =>
            {
                app.Run(async context =>
                {
                    await context.Response.WriteAsync("map2 run");
                });
            }));
            //Use1
            app.Use(async (context, next) =>
            {
                context.Response.ContentType = "text/plain; charset=utf-8";

                await context.Response.WriteAsync("第 1 个Use   开始!\r\n", Encoding.UTF8);

                await next();

                await context.Response.WriteAsync("第 1 个Use   结束!\r\n", Encoding.UTF8);

            });
            
            //Use2
            app.Use(async (context, next) =>
            {
                await context.Response.WriteAsync("第 2 个Use   开始!\r\n", Encoding.UTF8);

                await next();

                await context.Response.WriteAsync("第 2 个Use   结束!\r\n", Encoding.UTF8);

            });
            //结束管道处理
            app.Run(async context =>
            {
                await context.Response.WriteAsync("未匹配处理!\r\n", Encoding.UTF8);
            });

            app.Run();
        }
    }

最后执行的路径和最开始的图是一致的。

image

为什么将
context.Response.ContentType = "text/plain; charset=utf-8";
放到第一个 Use 呢,因为如果放到 Run 里面会报错,改变了 Header 标头。所以理论上也不要在 Use 里面发送响应WriteAsync,此处为了演示所以这么写。

image

六、中间件类

上面的代理方法可以移动到类中,这个类就是中间件类。中间件类需要如下要求:

  • 具有类型为 RequestDelegate 的参数的公共构造函数。
  • 名为 Invoke 或 InvokeAsync 的公共方法。 此方法必须:
    返回 Task。
    接受类型 HttpContext 的第一个参数。

构造函数和 Invoke/InvokeAsync 的其他参数由依赖关系注入 (DI) 填充。

将上面的未匹配路由处理逻辑移动到中间件类中:

  • TestMiddleware1:
    public class TestMiddleware1
    {
        private readonly RequestDelegate _next;

        public TestMiddleware1(RequestDelegate next)
        {
            _next = next;
        }

        public async Task InvokeAsync(HttpContext context)
        {

            context.Response.ContentType = "text/plain; charset=utf-8";

            await context.Response.WriteAsync("第 1 个Use   开始!\r\n", Encoding.UTF8);

            await _next(context);

            await context.Response.WriteAsync("第 1 个Use   结束!\r\n", Encoding.UTF8);
        }
    }
  • TestMiddleware2
    public class TestMiddleware2
    {
        private readonly RequestDelegate _next;

        public TestMiddleware2(RequestDelegate next)
        {
            _next = next;
        }

        public async Task InvokeAsync(HttpContext context)
        {


            await context.Response.WriteAsync("第 2 个Use   开始!\r\n", Encoding.UTF8);

            await _next(context);

            await context.Response.WriteAsync("第 2 个Use   结束!\r\n", Encoding.UTF8);
        }
    }
  • Program
    image

  • 运行
    image

此处的中间件使用有顺序问题,如果我先
app.UseMiddleware<TestMiddleware2>()
因为 TestMiddleware1 修改了标头,根据约定是不允许的,所以程序是有报错。

image

因此中间件组件的顺序定义了针对请求调用这些组件的顺序,以及响应的相反顺序。 此顺序对于安全性、性能和功能至关重要。

七、中间件顺序

image

以上是内置中间件的默认顺序规则,具体如何使用内置中间件,可参阅官方资料。

八、写在最后

以上就是关于中间件的部分知识,结合我自己的理解做了前后衔接的梳理逻辑。

官方网站更多的是讲解每个知识点的细节,前后需要结合起来理解,当然我还是强烈建议跟着官方文档学习,而且是最权威最可信的:
ASP.NET Core 中间件

这个系列历时2年,工作生活都比较忙,也有放纵啥事不相干的时候,中间断断续续的,总算是坚持完了。很多东西就是这样,累了就休息一下贵在坚持,即使再慢,积累的成果也有收获。

前言

在.NET平台中操作生成PDF的类库有很多如常见的有iTextSharp、PDFsharp、Aspose.PDF等,今天我们分享一个用于生成PDF文档的现代开源.NET库:QuestPDF,本文将介绍QuestPDF并使用它快速实现发票PDF文档生成功能。

QuestPDF介绍

QuestPDF 是一个用于生成 PDF 文档的现代开源 .NET 库。QuestPDF 由简洁易用的 C# Fluent API 提供全面的布局引擎。轻松生成 PDF 报告、发票、导出等。QuestPDF它提供了一个布局引擎,在设计时考虑了完整的分页支持。与其他库不同,它不依赖于 HTML 到 PDF 的转换,这在许多情况下是不可靠的。相反,它实现了自己的布局引擎,该引擎经过优化,可以满足所有与分页相关的要求。

QuestPDF License

分为社区版、专业版、和企业版。

项目源代码

创建一个控制台应用

创建一个名为
QuestPDFTest
的控制台应用。

安装QuestPDF Nuget包

搜索:
QuestPDF
包进行安装。

快速实现发票PDF文档生成

创建InvoiceModel

namespace QuestPDFTest
{
    public class InvoiceModel
    {

        /// <summary>
        /// 发票号码
        /// </summary>
        public int InvoiceNumber { get; set; }

        /// <summary>
        /// 发票开具日期
        /// </summary>
        public DateTime IssueDate { get; set; }

        /// <summary>
        /// 发票到期日期
        /// </summary>
        public DateTime DueDate { get; set; }

        /// <summary>
        /// 卖方公司名称
        /// </summary>
        public string SellerCompanyName { get; set; }

        /// <summary>
        /// 买方公司名称
        /// </summary>
        public string CustomerCompanyName { get; set; }

        /// <summary>
        /// 订单消费列表
        /// </summary>
        public List<OrderItem> OrderItems { get; set; }

        /// <summary>
        /// 备注
        /// </summary>
        public string Comments { get; set; }
    }

    public class OrderItem
    {
        /// <summary>
        /// 消费类型
        /// </summary>
        public string Name { get; set; }

        /// <summary>
        /// 消费金额
        /// </summary>
        public decimal Price { get; set; }

        /// <summary>
        /// 消费数量
        /// </summary>
        public int Quantity { get; set; }
    }
}

CreateInvoiceDetails

namespace QuestPDFTest
{
    public class CreateInvoiceDetails
    {
        private static readonly Random _random = new Random();

        public enum InvoiceType
        {
            餐饮费,
            交通费,
            住宿费,
            日用品,
            娱乐费,
            医疗费,
            通讯费,
            教育费,
            装修费,
            旅游费
        }

        /// <summary>
        /// 获取发票详情数据
        /// </summary>
        /// <returns></returns>
        public static InvoiceModel GetInvoiceDetails()
        {
            return new InvoiceModel
            {
                InvoiceNumber = _random.Next(1_000, 10_000),
                IssueDate = DateTime.Now,
                DueDate = DateTime.Now + TimeSpan.FromDays(14),
                SellerCompanyName = "追逐时光者",
                CustomerCompanyName = "DotNetGuide技术社区",
                OrderItems = Enumerable
                .Range(1, 20)
                .Select(_ => GenerateRandomOrderItemInfo())
                .ToList(),
                Comments = "DotNetGuide技术社区是一个面向.NET开发者的开源技术社区,旨在为开发者们提供全面的C#/.NET/.NET Core相关学习资料、技术分享和咨询、项目推荐、招聘资讯和解决问题的平台。在这个社区中,开发者们可以分享自己的技术文章、项目经验、遇到的疑难技术问题以及解决方案,并且还有机会结识志同道合的开发者。我们致力于构建一个积极向上、和谐友善的.NET技术交流平台,为广大.NET开发者带来更多的价值和成长机会。"
            };
        }

        /// <summary>
        /// 订单信息生成
        /// </summary>
        /// <returns></returns>
        private static OrderItem GenerateRandomOrderItemInfo()
        {
            var types = (InvoiceType[])Enum.GetValues(typeof(InvoiceType));
            return new OrderItem
            {
                Name = types[_random.Next(types.Length)].ToString(),
                Price = (decimal)Math.Round(_random.NextDouble() * 100, 2),
                Quantity = _random.Next(1, 10)
            };
        }
    }
}

CreateInvoiceDocument

using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;

namespace QuestPDFTest
{
    public class CreateInvoiceDocument : IDocument
    {
        /// <summary>
        /// 获取Logo的的Image对象
        /// </summary>
        public static Image LogoImage { get; } = Image.FromFile("dotnetguide.png");

        public InvoiceModel Model { get; }

        public CreateInvoiceDocument(InvoiceModel model)
        {
            Model = model;
        }

        public DocumentMetadata GetMetadata() => DocumentMetadata.Default;

        public void Compose(IDocumentContainer container)
        {
            container
                .Page(page =>
                {
                    //设置页面的边距
                    page.Margin(50);

                    //字体默认大小18号字体
                    page.DefaultTextStyle(x => x.FontSize(18));

                    //页眉部分
                    page.Header().Element(BuildHeaderInfo);

                    //内容部分
                    page.Content().Element(BuildContentInfo);

                    //页脚部分
                    page.Footer().AlignCenter().Text(text =>
                    {
                        text.CurrentPageNumber();
                        text.Span(" / ");
                        text.TotalPages();
                    });
                });
        }

        #region 构建页眉部分
        void BuildHeaderInfo(IContainer container)
        {
            container.Row(row =>
            {
                row.RelativeItem().Column(column =>
                {
                    column.Item().Text($"发票编号 #{Model.InvoiceNumber}").FontFamily("fangsong").FontSize(20).SemiBold().FontColor(Colors.Blue.Medium);

                    column.Item().Text(text =>
                    {
                        text.Span("发行日期: ").FontFamily("fangsong").FontSize(13).SemiBold();
                        text.Span($"{Model.IssueDate:d}");
                    });

                    column.Item().Text(text =>
                    {
                        text.Span("终止日期: ").FontFamily("fangsong").FontSize(13).SemiBold();
                        text.Span($"{Model.DueDate:d}");
                    });
                });

                //在当前行的常量项中插入一个图像
                row.ConstantItem(130).Image(LogoImage);
            });
        }

        #endregion

        #region 构建内容部分

        void BuildContentInfo(IContainer container)
        {
            container.PaddingVertical(40).Column(column =>
            {
                column.Spacing(20);

                column.Item().Row(row =>
                {
                    row.RelativeItem().Component(new AddressComponent("卖方公司名称", Model.SellerCompanyName));
                    row.ConstantItem(50);
                    row.RelativeItem().Component(new AddressComponent("客户公司名称", Model.CustomerCompanyName));
                });

                column.Item().Element(CreateTable);

                var totalPrice = Model.OrderItems.Sum(x => x.Price * x.Quantity);
                column.Item().PaddingRight(5).AlignRight().Text($"总计: {totalPrice}").FontFamily("fangsong").SemiBold();

                if (!string.IsNullOrWhiteSpace(Model.Comments))
                    column.Item().PaddingTop(25).Element(BuildComments);
            });
        }

        /// <summary>
        /// 创建表格
        /// </summary>
        /// <param name="container">container</param>
        void CreateTable(IContainer container)
        {
            var headerStyle = TextStyle.Default.SemiBold();

            container.Table(table =>
            {
                table.ColumnsDefinition(columns =>
                {
                    columns.ConstantColumn(25);
                    columns.RelativeColumn(3);
                    columns.RelativeColumn();
                    columns.RelativeColumn();
                    columns.RelativeColumn();
                });

                table.Header(header =>
                {
                    header.Cell().Text("#").FontFamily("fangsong");
                    header.Cell().Text("消费类型").Style(headerStyle).FontFamily("fangsong");
                    header.Cell().AlignRight().Text("花费金额").Style(headerStyle).FontFamily("fangsong");
                    header.Cell().AlignRight().Text("数量").Style(headerStyle).FontFamily("fangsong");
                    header.Cell().AlignRight().Text("总金额").Style(headerStyle).FontFamily("fangsong");
                    //设置了表头单元格的属性
                    header.Cell().ColumnSpan(5).PaddingTop(5).BorderBottom(1).BorderColor(Colors.Black);
                });

                foreach (var item in Model.OrderItems)
                {
                    var index = Model.OrderItems.IndexOf(item) + 1;

                    table.Cell().Element(CellStyle).Text($"{index}").FontFamily("fangsong");
                    table.Cell().Element(CellStyle).Text(item.Name).FontFamily("fangsong");
                    table.Cell().Element(CellStyle).AlignRight().Text($"{item.Price}").FontFamily("fangsong");
                    table.Cell().Element(CellStyle).AlignRight().Text($"{item.Quantity}").FontFamily("fangsong");
                    table.Cell().Element(CellStyle).AlignRight().Text($"{item.Price * item.Quantity}").FontFamily("fangsong");
                    static IContainer CellStyle(IContainer container) => container.BorderBottom(1).BorderColor(Colors.Grey.Lighten2).PaddingVertical(5);
                }
            });
        }

        #endregion

        #region 构建页脚部分

        void BuildComments(IContainer container)
        {
            container.ShowEntire().Background(Colors.Grey.Lighten3).Padding(10).Column(column =>
            {
                column.Spacing(5);
                column.Item().Text("DotNetGuide技术社区介绍").FontSize(14).FontFamily("fangsong").SemiBold();
                column.Item().Text(Model.Comments).FontFamily("fangsong");
            });
        }

        #endregion
    }

    public class AddressComponent : IComponent
    {
        private string Title { get; }
        private string CompanyName { get; }

        public AddressComponent(string title, string companyName)
        {
            Title = title;
            CompanyName = companyName;
        }

        public void Compose(IContainer container)
        {
            container.ShowEntire().Column(column =>
            {
                column.Spacing(2);

                column.Item().Text(Title).FontFamily("fangsong").SemiBold();
                column.Item().PaddingBottom(5).LineHorizontal(1);
                column.Item().Text(CompanyName).FontFamily("fangsong");
            });
        }
    }
}

Program

using QuestPDF;
using QuestPDF.Fluent;
using QuestPDF.Infrastructure;

namespace QuestPDFTest
{
    internal class Program
    {
        static void Main(string[] args)
        {
            // 1、请确保您有资格使用社区许可证,不设置的话会报异常。
            Settings.License = LicenseType.Community;

            // 2、禁用QuestPDF库中文本字符可用性的检查
            Settings.CheckIfAllTextGlyphsAreAvailable = false;

            // 3、PDF Document 创建
            var invoiceSourceData = CreateInvoiceDetails.GetInvoiceDetails();
            var document = new CreateInvoiceDocument(invoiceSourceData);

            // 4、生成 PDF 文件并在默认的查看器中显示
            document.GeneratePdfAndShow();
        }
    }
}

完整示例源代码

https://github.com/YSGStudyHards/QuestPDFTest

示例运行效果图

注意问题

中文报异常

QuestPDF.Drawing.Exceptions.DocumentDrawingException:“Could not find an appropriate font fallback for glyph: U-53D1 '发'. Font families available on current environment that contain this glyph: Microsoft JhengHei, Microsoft JhengHei UI, Microsoft YaHei, Microsoft YaHei UI, SimSun, NSimSun, DengXian, FangSong, KaiTi, SimHei, FZCuHeiSongS-B-GB. Possible solutions: 1) Use one of the listed fonts as the primary font in your document. 2) Configure the fallback TextStyle using the 'TextStyle.Fallback' method with one of the listed fonts. You can disable this check by setting the 'Settings.CheckIfAllTextGlyphsAreAvailable' option to 'false'. However, this may result with text glyphs being incorrectly rendered without any warning.”

加上这段代码:

// 2、禁用QuestPDF库中文本字符可用性的检查
Settings.CheckIfAllTextGlyphsAreAvailable = false;

原因:

默认情况下,使用 QuestPDF 生成 PDF 文档时,它会检查所使用的字体是否支持文本中的所有字符,并在发现不能显示的字符时输出一条警告消息。这个选项可以确保文本中的所有字符都能正确地显示在生成的 PDF 文件中。

中文乱码问题

解决方案:

假如Text("")中为汉字一定要在后面加上FontFamily("fangsong")[仿宋字体]或FontFamily("simhei")[黑体字体],否则中文无法正常显示。

项目源码地址



更多项目实用功能和特性欢迎前往项目开源地址查看