1 頁 (共 1 頁)

(2023.05.24)mod_harbour support SSE.

發表於 : 2023-05-24, 22:09
admin
Server-Sent Events (SSE) : 允許網頁從伺服器端取得更新資料.
參考網址: https://www.w3schools.com/html/html5_se ... events.asp

當我們要從伺服器端抓取資料,又不想用一般的進度表顯示,SSE 是一種好方法.
mod_harbour 有沒有支援 SSE 呢?
經過反覆測試下,終於完成 SSE 功能.

SSE is a good way when we want to fetch data from the server and don't want to use the usual progress meter.
Does mod_harbour support SSE? After repeated testing, the SSE function is finally completed.

javascript:

代碼: 選擇全部

			let param=self.url+'?func=9&help='+(self.lHelp ? 1 : 0 )+'&progress=1&data='+JSON.stringify(data);	// object into url parameter string, because SSE can only do that!!
			source = new EventSource(param,{
				 withCredentials: false	// Whether to allow cross domain requests, default: false
			});
			// Handle postback events 'message'
			source.addEventListener('message',function(e){
				let o=JSON.parse(e.data);	// analyze
				if(o.lEnd == 0){	// lEnd=1:End
					// Screen display progress processing
					let per=Math.round( o.step/o.total*100,2 );	// calculate percentage
					$('#progress').val(per);
					$('#progress_down').text( per+'%' );
				}else{
					source.close();	// Close SSE!! Don't send data back!!
					// download file
					if(o.err_no != 0){
						w2alert('Failed to read data!!'+o.err_msg, 'Error!!').ok(event => {
							w2popup.close();
						});
						return;
					}
					// After processing, receive the excel data
					let $a = $("<a>");
					// Because there will be problems when sending '/' in the backend, so move this keyword to the front to deal with it!!
					$a.attr("href",'data:application\/vnd.ms-excel;base64,'+o.errmsg);
					$("body").append($a);
					$a.attr("download",self.cXlsName);
					$a[0].click();
					$a.remove();
					w2popup.close();
				}
			});
			// Handle the event on start 'open'
			source.addEventListener('open', function(e) {
				// 
			});
			// Events when errors are handled 'error'
			source.addEventListener('error', function(e) {
				if(e.target.readyState == EventSource.CLOSED){
					w2alert('connect be close!!','error!!');
					source.close(); // stop SSE 
				}else{
					console.log('error!!: '+dump(e,2));
				}
			});
			// Handle system status events 'status'
			source.addEventListener('status', function(e) {
				console.log('System status is now: ' + e.data);
			});

prg:

代碼: 選擇全部

METHOD printData() CLASS TPER
	LOCAL hRes		:= {;
	   'err_no'=>0;		// 錯誤碼
	  ,'errmsg'=>'';	// 錯誤訊息
	  ,'errtil'=>'';	// 錯誤抬頭
	  ,'lEnd'=>0;			// 結束?<*0:No/1:Yes>
	  ,'step'=>0;			// 目前進度指標(every)
	  ,'total'=>0;		// 總筆數(total)
	}
	//
	LOCAL hParam			:= hb_hash(),;
				hPostData		:= hb_hash(),;
				hSortData		:= hb_hash(),;
				hData				:= hb_hash(),;
				cDef				:= '',;
				hObj				:= hb_hash(),;
				nObjCount		:= 0,;
				cMethod			:= AP_Method(),;
				nProgress		:= 0,;
				lClientStop	:= .F.	// 前端中斷?
	//
	LOCAL	i						:= 0,;
				j						:= 0,;
				k						:= 0,;
				aHD					:= {},;
				nRow				:= 0
	//
	LOCAL PR_NUM1			:= '',;
	      PR_NUM2     := '',;
	      PR_NAM      := '',;
	      cWhere			:= '',;
	      cQry				:= '',;
	      oQry				:= NIL,;
	      nTolRec			:= 0
	//
	LOCAL oExcel			:= NIL,;
				oBook				:= NIL,;
				oSheet			:= NIL,;
				oFnt1, oFnt2, oFnt3,;
				oFmtR, oFmtL, oFmt0, oFmt1, oFmt2, oFmt3
	
	// 送出 SSE 專用表頭
	AP_SetContentType( "Cache-Control: no-cache" ) // 關閉快取
	AP_SetContentType( "text/event-stream;charset=UTF-8" ) // 送出表頭格式

	// 取得資料傳遞方式
  IF cMethod == 'POST'
    hParam := AP_PostPairs()
  ELSE
    hParam := AP_GetPairs()
  ENDIF
  // 取得參數 progress: 是否顯示進度表<*0:No/1:Yes>
  IF hb_HHasKey(hParam, 'progress')
    nProgress := Val( hParam['progress'])
  ELSE
    nProgress := 0
  ENDIF
	//
	DO WHILE .T.
	  // 接收參數: 換頁方式處理方式,直接利用 data 帶過來
	  hPostData={;  // 設定預設值: array
	     "PR_NUM1"=>"";
	    ,"PR_NUM2"=>"";
	    ,"PR_NAM"=>"";
	    ,"OUT_TYPE"=>0;  // 輸出類型:<*0:Excel/1:PDF>
	  }
	  hSortData := {;  // 設定預設值: array
	    ["field"=>"r.PR_NUM";  // 排序欄位:用陣列包住,模擬 w2grid.load()
	    ,"direction"=>"asc"];  // 排序方是
	  }
	  cDef := hb_jsonencode({;	// 預設參數: array to json string
	     "postData"=>hPostData;
	    ,"sortData"=>hSortData;
	  })
	  IF hb_HHasKey(hParam, 'data')
	    cData := hParam['data']
	  ELSE
	    cData := cDef
	  ENDIF
	  hObj := hb_jsondecode(cData)			// 將字串資料轉為 json 物件
          nObjCount := Len( hb_hKeys(hObj))	// 取得 hObj 裡面 keys 數量
	  IF nObjCount < 2
	    hRes['err_no'] := -1
	    hRes['errmsg'] := "參數資料有問題!!"
	    hRes['errtil'] := "錯誤!!"+ProcName()+"("+hb_ntos(ProcLine())+")"
	    EXIT
	  ELSE
	    hPostData := hObj['postData']
	    hSortData := hObj['sortData']
	  ENDIF
	
	  // 從物件中取得傳遞參數
	  PR_NUM1 := hPostData['PR_NUM1']	// 員工編號-1
	  PR_NUM2 := hPostData['PR_NUM2']	// 員工編號-2
	  PR_NAM  := hPostData['PR_NAM' ]	// 員工姓名
	
	  // 整理資料
	  PR_NUM1 := AllTrim( PR_NUM1 )
	  PR_NUM2 := AllTrim( PR_NUM2 )
	  PR_NAM  := AllTrim( PR_NAM  )
	
	  //-----------------------
	  // 檢查資料
	  //-----------------------
	  //-----------------------
	  // 組合 $where
	  //-----------------------
	  cWhere := ""
	  IF ! Empty(PR_NUM1)
	    cWHERE += IF( ! Empty(cWHERE), ' AND ', '' )
	    cWHERE +="r.PR_NUM>='"+AllTrim( PR_NUM1 )+"'"
	  ENDIF
	  IF ! Empty(PR_NUM2)
	    cWHERE += IF( ! Empty(cWHERE), ' AND ', '' )
	    cWHERE += "r.PR_NUM<='"+AllTrim( PR_NUM2 )+"z'"
	  ENDIF
	  IF ! Empty(PR_NAM)
	    cWHERE += IF( ! Empty(cWHERE), ' AND ', '' )
	    cWHERE += "r.PR_NAM LIKE '%"+AllTrim( PR_NAM )+"%'"
	  ENDIF
	  IF Empty(cWHERE)
	    cWHERE := "1>0"
	  ENDIF
	  // 抓取資料
	  cQry := "SELECT r.* "
	  cQry += "FROM "+::db:table['per']+" r "
	  cQry += "WHERE "+cWHERE+" "
	  cQry += "ORDER BY "+hSortData[1,'field']+" "+Upper( hSortData[1, 'direction'])
	  oQry := ::db:Qry(cQry)
	  IF ::db:err_no <> 0
	    hRes['err_no'] := ::db:err_no
	    hRes['errmsg'] := ::db:err_msg
	    hRes['errtil'] := "錯誤!!"+ProcName()+"("+hb_ntos( ProcLine() )+")"
	    EXIT
	  ENDIF
	  nTolRec := oQry:RecCount()	// 總筆數

		//	  
		oExcel := TLibXL():New()
		oBook   := oExcel:CreateXMLBook() // .xlsx oExcel:CreateXMLBook() // .xlsx
		oBook:setLocale('UTF-8') // 'en_US.UTF-8': 不能加 'en_US' 不然輸出資料會是亂碼
		oSheet := oBook:AddSheet( '員工基本資料' )
		// Font
		oBook:setDefaultFont('細明體',12) // 設定整體字型
		oFnt1 := oBook:AddFont(); oFnt1:SetName('細明體'); oFnt1:SetSize(12)
		oFnt2 := oBook:AddFont(); oFnt2:SetName('細明體'); oFnt2:SetSize(16)
		// Format
		oFmtR := oBook:AddFormat(); oFmtR:setAlignH( ALIGNH_RIGHT          ) // 水平靠右
		oFmtL := oBook:AddFormat(); oFmtL:setAlignH( ALIGNH_LEFT           ) // 水平靠左
		oFmt1 := oBook:AddFormat(); oFmt1:setNumFormat(NUMFORMAT_TEXT      )
		oFmt2 := oBook:AddFormat(); oFmt1:setNumFormat(NUMFORMAT_NUMBER    ) // ex: 1000
		oFmt3 := oBook:AddFormat(); oFmt1:setNumFormat(NUMFORMAT_NUMBER_D2 ) // ex: 1000.00
		//
		aHD := Array( 2, 3 )
		aHD[01] := { "員工編號"  , 10, 'C' }
		aHD[02] := { "員工姓名"  , 12, 'C' }
	
	  // 抓取對應 grid 資料
	  i := 0
	  nPer := ceiling( Round( nTolRec/20, 2 ))  // 無條件進位取整數, /25 等於四筆顯示一次
	  // 回傳進度表資料[progress(完成百分比)、count(目前進度指標)、total(總筆數)]
	  IF nProgress == 1
	    hRes['step' ] := i        // 目前進度
	    hRes['total'] := nTolRec	// 總筆數
	    ::sendMsg( hb_jsonencode( hRes ) )  // 將訊息傳回前端
	  ENDIF
	  //
		nRow := 1
	  //
	  DO WHILE ! oQry:Eof()
	    IF i == 0
				oSheet:writeStr( nRow, 1, '產品基本資料')
				nRow += 1
				// 輸出表頭
				FOR j := 1 To Len(aHD)
					oSheet:SetCol(j, j, aHD[j, 2], oFmtL )
					oSheet:writeStr( nRow, j, aHD[j, 1] )
				NEXT j
	    ENDIF
	    i++
	    // 回傳進度表資料
	    IF nProgress == 1
	        hRes['step' ] := i                            // 目前進度
	        hRes['total'] := nTolRec                     // 總數
	        ::sendMsg( hb_jsonencode( hRes ) )      // 送資料到前端
	    ENDIF
	    // 利用迴圈輸出資料
	    j := i + 1
	    nRow++
	    //
			oSheet:writeStr( nRow,  1, RTrim( oQry:FieldGet("PR_NUM")))
			oSheet:writeStr( nRow,  2, RTrim( oQry:FieldGet("PR_NAM")))
	    //
	    oQry:Skip()
	    //
	  ENDDO // end: while(...)
	  oQry:End()
	  //
	  IF lClientStop
			oBook:End()
			oExcel:End() // 先關閉
	    EXIT  // 前端中斷
	  ELSE
			oBook:Save( ::cMainDir+"per.xlsx" )
			oBook:End()
			oExcel:End() // 先關閉
	  ENDIF
	  EXIT
	ENDDO // end: while(...)
	//
	IF lClientStop
	  RETURN // 使用者中斷,離開,不用再送什麼資料回前端!!
	ENDIF
	
	hRes['lEnd'] := 1 // 利用此變數通知前端處理完畢!! 產生 Excel 檔案
	IF hRes['err_no'] == 0
		// 輸出
		hRes['errmsg'] := hb_base64Encode( memoRead(::cMainDir+'per.xlsx') ) // 做 base64 編碼回傳
		fErase(::cMainDir+'per.xlsx')
	ENDIF
	::sendMsg( hb_jsonencode( hRes ))  // 將資料送至前端
	//
RETURN NIL  // $this->printData()

METHOD sendMsg(cRes) CLASS TPER
	AP_SetContentType( "text/event-stream;charset=UTF-8" ) // 送出表頭格式
	AP_RWRITE( e"event: message\n" )
	AP_RWRITE( e"id: "+hb_ntos( int( hb_TToMSec( hb_dateTime() )))+e"\n" )
	AP_RWRITE( e"retry: 15000\n" )  // 一個整數值,指示斷開連接時的重新連接時間
	AP_RWRITE( e"data: "+cRes+e"\n" )
	AP_RWRITE( e"\n" ) // 跟上面 hb_eol() 組合成兩個 \n \n 代表送出完畢!!
	// 測試用,暫停 SLEEP_SEC 秒數
	::Delay(200)
RETURN NIL  // end: this->sebdMsg()