Activiti動態表單開發技術分享

1. 動態表單特色

通常而言,工做流引擎經常使用表單有三種:普通表單、外置表單和動態表單。各自都有其優缺點,可根據具體場景靈活選用。須要說明的是,三種表單方式只是在任務節點上用戶的表單定義方式上面有差異,而流程的運起色制則徹底相同。 image.pngcss

如圖所示,區別於普通表單和外置表單,動態表單是直接將工做流節點處的表單嵌入流程定義文件BPMN中,系統利用JS或者模板引擎根據流程定義中表單定義的各個子控件及其屬性動態渲染出表單加載出來。html

2. 動態表單流程設計

在模板管理界面,點擊新增模板按鈕,進入流程模板設計頁面。前端

image.png

編輯流程信息:流程key、流程name等: image.pngjava

拖拽添加啓動節點,點擊啓動節點,在下面的屬性中點擊動態表單屬性: image.png數據庫

編輯啓動節點的動態表單屬性,編輯活動編號、名稱、類型、必輸、可讀、可寫等屬性後,點擊保存: image.pngapi

添加下一個活動事件,並編輯該節點屬性信息,重點是代理、和動態表單屬性信息: image.png安全

點擊代理,編輯該事件的代理人、候選人(組),點擊保存: image.png服務器

中間流程設計不詳細講述,最後添加一個結束節點並鏈接: image.pngdom

動態表單節點的經常使用屬性介紹: TIM截圖20180705173841.png TIM截圖20180705174046.png異步

流程所有設計完成後,點擊保存按鈕進行保存:

image.png

在流程玩法-流程列表界面點擊部署流程按鈕:

image.png

3. 流程列表

流程部署成功後,在待啓動流程列表界面能夠看到已部署的流程:

image.png

點擊啓動按鈕,彈出啓動節點的動態表單,輸入信息後點擊啓動流程按鈕:

image.png

4. 任務列表

啓動成功後,登陸流程設計的該節點候選人(組)用戶登陸,在任務列表界面能夠看到該流程,並能夠點擊簽收按鈕進行簽收:

image.png

簽收成功後,該項操做會變成「辦理」狀態,能夠點擊進行辦理:

image.png

點擊辦理按鈕,彈出該節點定義的動態表單,並進行提交操做:

image.png

5. 運行中流程

點擊運行中流程菜單能夠查看已啓動但未結束的流程列表,而且能夠查看每一個流程正在運行的節點:

image.png

點擊當前節點,能夠查看每一個流程圖及當前運行節點位置:

image.png

6. 已結束流程

在已結束流程頁面能夠看到已經結束的流程列表:

image.png

7. 動態表單開發關鍵點

標準流程的啓動和運轉直接調用Activiti的通用API便可實現,下面主要從如下幾個方面講解。

1) 動態表單渲染

動態表單將表單定義在了流程定義的文件中,所以在啓動節點和任務節點處能分別經過流程定義ID和任務ID去獲取節點處的表單屬性JSON,以下所示:

獲取啓動節點處表單數據連接:

<u>http://localhost:8083/form/dynamic/get-form/start/leave-dynamic-from:2:47515</u>

獲取表單定義數據結果: {

"form":{

"deploymentId":"47512",

"formKey":"",

"formProperties":[

{

"id":"startDate",

"name":"請假開始日期",

"readable":true,

"required":true,

"type":{

"name":"date"

},

"value":"",

"writable":true

},

{

"id":"endDate",

"name":"請假結束日期",

"readable":true,

"required":true,

"type":{

"name":"date"

},

"value":"",

"writable":true

},

{

"id":"reason",

"name":"請假緣由",

"readable":true,

"required":true,

"type":{

"mimeType":"text/plain",

"name":"string"

},

"value":"",

"writable":true

}

],

"processDefinition":""

}

}

獲取到了表單定義屬性文件後,就能夠利用JS或者模板引擎渲染出表單了。好比利用layui的模板引擎來渲染,就能夠定義以下模板:

vargetTpl = `

<form class="layui-form" lay-filter="form-tpl" style="padding: 10px;">

{{# $.each(d.taskFormData.formProperties, function(i,v1) { }} {{# console.log(i,v1)}}

<div>

{{# if(v1.type.name=="string" ){ }}

<div class="layui-form-item">

<label class="layui-form-label">{{v1.name}}</label>

<div class="layui-input-block">

<input class="layui-input" type="text" name="{{v1.writable ? 'fp_'+v1.id : v1.id}}" autocomplete="off" {{v1.required? 'lay-verify="required"': ''}} placeholder="{{v1.name}}" lay-blur lay-verType="tips" value="{{v1.value}}" {{v1.writable ? '':'disabled'}}/>

</div>

</div>

{{# } }}

<div></div>

{{# if(v1.type.name=="date" ){ }}

<div class="layui-form-item">

<label class="layui-form-label">{{v1.name}}</label>

<div class="layui-input-block">

<input class="layui-input date" type="text" name="{{v1.writable ? 'fp_'+v1.id : v1.id}}" autocomplete="off" {{v1.required? 'lay-verify="required|date"': ''}} placeholder="yyyy-MM-dd" lay-verType="tips" value="{{v1.value}}" {{v1.writable ? '':'disabled'}}/>

</div>

</div>

{{# } }}

<div></div>

{{# if(v1.type.name=="enum" ){ }}

<div class="layui-form-item">

<label class="layui-form-label">{{v1.name}}</label>

<div class="layui-input-block">

<select name="{{v1.writable ? 'fp_'+v1.id : v1.id}}" {{v1.writable ? '':'disabled'}}>

<option value=""></option>

{{# $.each(d[v1.id+''],function(i2,v2){ }}

<option value="{{i2}}" {{i2==v1.value ? 'selected':''}}>{{v2}}</option>

{{# }) }}

</select>

</div>

</div>

{{# } }}

</div>

{{# }) }}

<div style="display:none;">

<!--此處隱藏但不能省略,爲觸發事件準備-->

<button lay-submit>提交</button><button type="reset">重置</button>

</div>

</form>

<style type="text/css">

.layui-layer-page .layui-layer-content {

overflow: visible;

}

</style>

`**;

上述模板對常見的string、date、enum類型進行了解析和渲染,有更多類型能夠本身根據須要添加。

此外,須要注意的是,要區別開表單中可編輯參數與不可編輯參數的屬性配置,以下所示(紅色標註的部分),可編輯參數的name屬性值前面加上「fp_」(約定)。這樣配置的好處是後臺在接收到參數後能夠區分開哪些參數是當前節點須要保存的參數信息,以便進行保存(見下面第二點表單的參數解析部分)。

<input class="layui-input" type="text" name="{{v1.writable ? 'fp_'+v1.id : v1.id}}"* autocomplete="off" {{v1.required? 'lay-verify="required"': ''}} placeholder="{{v1.name}}" lay-blur lay-verType="tips" value="{{v1.value}}" {{v1.writable ? '':'disabled'}}/>*

使用方法是在須要進行動態表單渲染的頁面JS中引入該模板:

//引入動態表單渲染模板

$.use(ctx+'/static/js/dynamic-form-common.js', function*(){});*

而後在得到表單屬性數據後,調用renderForm方法,傳入data數據和須要渲染的頁面dom節點元素便可:

renderForm(JSON.parse(form), $form.get(0));

2) 表單參數解析

前文已經提到,在表單渲染時就已經經過設置不一樣的name屬性值來區分開了可編輯參數和不可編輯參數,所以在後臺進行參數解析時就能很方便地對可編輯參數進行提取:

// 從request中讀取參數而後轉換

Map<String, String[]> parameterMap*=* request*.getParameterMap();*

Set<Entry<String, String[]>> entrySet*=* parameterMap*.entrySet();*

for*(Entry<String, String[]>* entry*:* entrySet*) {*

String key*=* entry*.getKey();*

// fp_的意思是form <u>paremeter</u>

if* (StringUtils.defaultString(key).startsWith("fp_")) {      formProperties.put(key.replaceFirst("fp_",* ""**), entry*.getValue()[0]);* *} }

上述代碼就能將可編輯參數封裝在formProperties這個HashMap中。

3) 動態表單自定義類型

下面以上傳附件爲例,講述自定義表單類型的步驟和流程:

a. 定義表單擴展類型

***public******class****FileFormType* ***extends**** AbstractFormType {*

*/***

* **

* */*

***private******static******final******long******serialVersionUID**** = 1L;*

*@Override*

***public**** String getName() {*

*//* ***TODO**** Auto-generated method stub*

***return****"file"**;*

*}*

*@Override*

***public****Object convertFormValueToModelValue(String* *propertyValue**) {*

*//* ***TODO**** Auto-generated method stub*

***return****propertyValue**;*

*}*

*@Override*

***public****String convertModelValueToFormValue(Object* *modelValue**) {*

*//* ***TODO**** Auto-generated method stub*

***return**** (String)**modelValue**;*

*}*

*}*

b. 在activiti配置類中註冊表單擴展類型

*//註冊自定義表單類型*

*List<AbstractFormType>* *formTypes**=* ***new**** ArrayList<>();*

*formTypes**.add(****new**** FileFormType());    **processEngineConfiguration**.setCustomFormTypes(**formTypes**);*

c. 針對表單自定義類型新增動態渲染解析器

*{{# if(v1.type.name=="file" ){ }}*

*<div class="layui-form-item">*

*<label class="layui-form-label">{{v1.name}}</label>*

*<div>*

*<button type="button" class="layui-btn layui-btn-normal" id="test8">選擇文件</button>*

*</div>*

*</div>*

*{{# } }}*

d. 任務辦理時判斷是否包含附件類型,若是包含則應將表單類型設置爲「multipart/form-data」:

***if****($(**"#test8"**)){*

*     $form.attr(**'enctype'**,**'multipart/form-data'**);*

*}*

e. 在任務辦理時新增附件的保存邏輯:

首先接收參數應新增:

@RequestParam(value="file", required=false) MultipartFile file

在參數解析後,保存file:

**if**(**null**!= file) {

attachmentService.createAttachment(file, taskId, processInstanceId, formProperties.get("attachmentDescription"), request);        

}

4) 經常使用API總結

引擎API是與Activiti打交道的最經常使用方式。 從ProcessEngine中,你能夠得到不少囊括工做流/BPM方法的服務。 ProcessEngine和服務類都是線程安全的。 你能夠在整個服務器中僅保持它們的一個引用就能夠了。

ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();    
RuntimeService runtimeService = processEngine.getRuntimeService();  
RepositoryService repositoryService = processEngine.getRepositoryService();  
TaskService taskService = processEngine.getTaskService();  
ManagementService managementService = processEngine.getManagementService();  
IdentityService identityService = processEngine.getIdentityService();  
HistoryService historyService = processEngine.getHistoryService();  
FormService formService = processEngine.getFormService();

① ProcessEngines:

ProcessEngines.getDefaultProcessEngine()會在第一次調用時 初始化並建立一個流程引擎,之後再調用就會返回相同的流程引擎。 使用對應的方法能夠建立和關閉全部流程引擎:ProcessEngines.init()和 ProcessEngines.destroy()。

ProcessEngines會掃描全部activiti.cfg.xml和 activiti-context.xml 文件。 對於activiti.cfg.xml文件,流程引擎會使用Activiti的經典方式構建: ProcessEngineConfiguration.createProcessEngineConfigurationFromInputStream(inputStream).buildProcessEngine(). 對於activiti-context.xml文件,流程引擎會使用Spring方法構建:先建立一個Spring的環境, 而後經過環境得到流程引擎。

全部服務都是無狀態的。這意味着能夠在多節點集羣環境下運行Activiti,每一個節點都指向同一個數據庫, 不用擔憂哪一個機器實際執行前端的調用。 不管在哪裏執行服務都沒有問題。

② RepositoryService***:***

RepositoryService多是使用Activiti引擎時最早接觸的服務。 它提供了管理和控制發佈包和流程定義的操做。 這裏不涉及太多細節,流程定義是BPMN 2.0流程的java實現。 它包含了一個流程每一個環節的結構和行爲。 發佈包是Activiti引擎的打包單位。一個發佈包能夠包含多個BPMN 2.0 xml文件和其餘資源。 開發者能夠自由選擇把任意資源包含到發佈包中。 既能夠把一個單獨的BPMN 2.0 xml文件放到發佈包裏,也能夠把整個流程和相關資源都放在一塊兒。 (好比,'hr-processes'實例能夠包含hr流程相關的任何資源)。 能夠經過RepositoryService來部署這種發佈包。 發佈一個發佈包,意味着把它上傳到引擎中,全部流程都會在保存進數據庫以前分析解析好。 從這點來講,系統知道這個發佈包的存在,發佈包中包含的流程就已經能夠啓動了。

除此以外,服務能夠

ü 查詢引擎中的發佈包和流程定義。

ü 暫停或激活發佈包,對應所有和特定流程定義。 暫停意味着它們不能再執行任何操做了,激活是對應的反向操做。

ü 得到多種資源,像是包含在發佈包裏的文件, 或引擎自動生成的流程圖。

ü 得到流程定義的pojo版本, 能夠用來經過java解析流程,而沒必要經過xml。

③ RuntimeService***:***

正如RepositoryService負責靜態信息(好比,不會改變的數據,至少是不怎麼改變的), RuntimeService正好是徹底相反的。它負責啓動一個流程定義的新實例。 如上所述,流程定義定義了流程各個節點的結構和行爲。 流程實例就是這樣一個流程定義的實例。對每一個流程定義來講,同一時間會有不少實例在執行。 RuntimeService也能夠用來獲取和保存流程變量。 這些數據是特定於某個流程實例的,並會被不少流程中的節點使用 (好比,一個排他網關經常使用流程變量來決定選擇哪條路徑繼續流程)。 Runtimeservice也能查詢流程實例和執行。 執行對應BPMN 2.0中的'token'。基本上執行指向流程實例當前在哪裏。 最後,RuntimeService能夠在流程實例等待外部觸發時使用,這時能夠用來繼續流程實例。 流程實例能夠有不少暫停狀態,而服務提供了多種方法來'觸發'實例, 接受外部觸發後,流程實例就會繼續向下執行。

④ TaskService***:***

任務是由系統中真實人員執行的,它是Activiti這類BPMN引擎的核心功能之一。 全部與任務有關的功能都包含在TaskService中:

ü 查詢分配給用戶或組的任務

ü 建立獨立運行任務。這些任務與流程實例無關。

ü 手工設置任務的執行者,或者這些用戶經過何種方式與任務關聯。

ü 認領並完成一個任務。認領意味着一我的指望成爲任務的執行者, 即這個用戶會完成這個任務。完成意味着「作這個任務要求的事情」。 一般來講會有不少種處理形式。

⑤ IdentityService***:***

IdentityService很是簡單。它能夠管理(建立,更新,刪除,查詢...)羣組和用戶。 請注意, Activiti執行時並無對用戶進行檢查。 例如,任務能夠分配給任何人,可是引擎不會校驗系統中是否存在這個用戶。 這是Activiti引擎也可使用外部服務,好比ldap,活動目錄,等等。

⑥ FormService***:***

FormService是一個可選服務。即便不使用它,Activiti也能夠完美運行, 不會損失任何功能。這個服務提供了啓動表單任務表單兩個概念。 啓動表單會在流程實例啓動以前展現給用戶, 任務表單會在用戶完成任務時展現。Activiti支持在BPMN 2.0流程定義中設置這些表單。 這個服務以一種簡單的方式將數據暴露出來。再次重申,它時可選的, 表單也不必定要嵌入到流程定義中。

⑦ HistoryService***:***

HistoryService提供了Activiti引擎手機的全部歷史數據。 在執行流程時,引擎會保存不少數據(根據配置),好比流程實例啓動時間,任務的參與者, 完成任務的時間,每一個流程實例的執行路徑,等等。 這個服務主要經過查詢功能來得到這些數據。

⑧ ManagementService***:***

ManagementService在使用Activiti的定製環境中基本上不會用到。 它能夠查詢數據庫的表和表的元數據。另外,它提供了查詢和管理異步操做的功能。 Activiti的異步操做用途不少,好比定時器,異步操做, 延遲暫停、激活,等等。後續,會討論這些功能的更多細節。

如下總結下在開發工做流引擎動態表單相關功能時用到的一些API:

查詢流程列表:

ProcessDefinitionQuery dynamicQuery*=* repositoryService*.createProcessDefinitionQuery()*

*.*orderByDeploymentId().desc();

啓動流程:

identityService*.setAuthenticatedUserId(user.getId());*

processInstance*=* formService*.submitStartFormData(processDefinitionId,* formProperties*);*

讀取啓動節點表單數據:

StartFormDataImpl*<u>startFormData</u>** = (StartFormDataImpl)* formService*.getStartFormData(processDefinitionId);*

任務列表查詢:

TaskQuery taskQuery*=* taskService*.createTaskQuery()*

.taskCandidateOrAssigned(user== null*?* "kafeitu"**:user.getId())

.active().orderByTaskCreateTime().desc();

簽收任務:

taskService*.claim(taskId,* userId*);*

辦理任務:

identityService*.setAuthenticatedUserId(user.getId());*

formService*.submitTaskFormData(taskId, formProperties);*

讀取Task表單數據:

*TaskFormDataImpltaskFormData = (*TaskFormDataImpl)formService.getTaskFormData(taskId);

運行中流程列表查詢:

ProcessInstanceQuery dynamicQuery*=* runtimeService*.createProcessInstanceQuery()*

.orderByProcessInstanceId().desc();

已結束流程列表查詢:

HistoricProcessInstanceQuery dynamicQuery*=* historyService*.createHistoricProcessInstanceQuery()**.finished().orderByProcessInstanceEndTime().desc();*

掛起流程:

repositoryService*.suspendProcessDefinitionById(processDefinitionId,* isCascade*,* new* Date());*

激活流程:

repositoryService*.activateProcessDefinitionById(processDefinitionId,* isCascade*,* new* Date());*

刪除流程:

repositoryService*.deleteDeployment(deploymentId,* isCascade*);*

掛起流程實例:

runtimeService*.suspendProcessInstanceById(processInstanceId);*

激活流程實例:

runtimeService*.activateProcessInstanceById(processInstanceId);*

刪除流程實例:

runtimeService*.deleteProcessInstance(processInstanceId,* deleteReason*);*

關於Activiti的更多詳細介紹,請參考如下資料:

網址:http://www.mossle.com/docs/activiti/index.html#apiEngine

書籍:Activiti實戰

相關文章
相關標籤/搜索