天天看點

基于QT的webkit與ExtJs開發CB/S結構的企業應用管理系統

一:源起

    1.何為CB/S的應用程式

    C/S結構的應用程式,是用戶端/服務端形式的應用程式,這種應用程式要在客戶電腦上安裝一個程式,客戶使用這個程式與服務端通信,完成一定的操作。

    B/S結構的應用程式,是浏覽器/服務端形式的應用程式,這種應用程式不用在用戶端部署任何東西,客戶隻需要通過浏覽器與服務端通信,來完成一定的操作。

    兩種類型的程式優缺點對比:

對比内容

C/S結構的應用程式

B/S結構的應用程式

部署

較困難

友善

更新

對用戶端的控制權限

資料實時性

較高

通信效率

跨平台性

    由上可知,兩種形式的應用程式各有利弊。架構師在做技術選型的時候,往往會根據項目需要,對比這兩種技術形式的優缺點,做出正确的選擇。

    然而,國内大多數企業應用程式,需要頻繁、及時的更新更新、需要更高的用戶端控制權限、需要更高的資料實時性和更高的通信效率,但卻不在意部署上的問題。

    這時,架構師就考慮把C/S結構的應用程式和B/S結構的應用程式結合起來,讓用戶端嵌套一個浏覽器以與伺服器通信,完成一定的操作。這樣的程式就是CB/S結構的應用程式。

    這樣做的好處是一般的業務邏輯隻要在服務端更新更新,即可展現在用戶端。對于用戶端系統權限、基于Socket的通信等浏覽器核心無法完成的操作,可以由用戶端來完成。用戶端可以直接與服務端通信,也可以通過浏覽器核心與服務端通信。

    下圖為CB/S結構應用程式的基本示意圖:

基于QT的webkit與ExtJs開發CB/S結構的企業應用管理系統

目前還有一種介于C/S和B/S結構的應用程式之間的應用程式:RIA富網際網路應用程式,這種結構的應用程式一般都是基于浏覽器插件來運作的,它有較高的用戶端控制權限(比B/S程式高,但比C/S程式低),通信方式也有較多的選擇(不隻是基于HTTP協定),目前較常見的RIA技術有:Adobe的flex技術、微軟的Silverlight技術、Oracle的WebStart技術。架構師在做技術選型的時候,也可以綜合權衡采用這些技術。

    2.為何選擇QT的WebKit與Extjs開發企業應用

    ExtJs是一個用于建立Web使用者界面的JS架構,提供了豐富的界面部件及布局方式,對于web開發者來說,實作企業應用所需的各種畫面隻要掌握JS語言即可。不必再引入flash或silverlight技術,而且能很容易的建立風格統一的企業應用程式。

    雖然ExtJs支援各種流行的浏覽器,甚至包括IE6,但是它在IE系浏覽器下運作、渲染的效率不高。在谷歌浏覽器下表現最好,FireFox浏覽器次之(這得益于谷歌浏覽器的JS腳本引擎)。

    然而谷歌浏覽器和FireFox浏覽器的核心都是WebKit(蘋果公司開源的浏覽器核心,負責解析HTML文本,并呈現到界面上),是以,要想讓我們的CB/S+ExtJs結構的應用程式能有更好的表現,我們必須采用WebKit核心的浏覽器。

    雖然我們能很友善的獲得WebKit的源碼,然而編譯它卻十分耗時費力,不但要選對編譯工具,還要安裝一系列的SDK,編譯時間更是長的驚人(這幾乎是大型C++項目的通病)。編譯出來的DLL使用起來也不是很友善(要翻閱大量的WebKit的API)。

    幸運的是QT界面庫為我們做了這些工作,QT庫中包含webkit的浏覽器控件,并且這個C++庫是跨平台的,也就是說基于這幾項技術開發的CB/S企業應用可以部署在Linux系統内。

    除了使用QT界面庫,還可以選擇gtk+和wxWidgets兩個界面庫,而且這兩個界面庫都對WebKit做過包裝,但是從開發方式,生産效率,運作速度等多方面考慮,還是QT最為合适。

    QT界面庫也分為兩個版本,一個是收費的digia提供的QT,另一個是免費的qt-project提供的QT(GPL V3 LGPL V2),這裡我們選擇免費版的QT,本文第三節會介紹如何搭建開發環境。

二:思路

    1.目标

    搭建一個CB/S結構的企業應用程式

    盡量保證系統的執行效率

    盡量保證系統更新更新的便利性

    盡量保證系統的可擴充性

    2.方案

    ExtJs架構是一個比較龐大的架構,一般B/S結構的程式使用ExtJS架構,都是把ExtJs的架構放在服務端,這樣使用者每次請求頁面的時候,都會去通路ExtJS架構的JS檔案,進而産生大量的磁盤IO和網絡消耗,這也是ExtJS架構看起來渲染很慢的一個因素。B/S結構的應用程式無法解決這個問題,主要是因為無法控制用戶端的浏覽器,CB/S結構的程式就能輕松解決這個問題。可以把ExtJs架構打包進用戶端程式中,随用戶端程式分發給使用者,使用者請求頁面時,使用的是本地的ExtJS架構的JS檔案,業務邏輯程式則仍舊使用服務端的。這樣做減少了磁盤IO和網絡消耗,保證了系統的執行效率;服務端對業務邏輯程式依舊保持着很好的控制權,保證了系統更新更新的便利性

    關于系統的可擴充性,ExtJs就能很好的處理,在下一節中會有較長的描述。

    3.難點

    CB/S結構的應用程式其實就是一個高度定制的浏覽器。為了讓這個浏覽器完成指定的功能(比如:包含ExtJs架構的js檔案,做成cookie,發起請求等)難免會有很多用戶端和浏覽器核心的互動。這些互動涉及到C++,Js,HTML,CSS等的互操作,是系統在技術上的難點。

三:用戶端浏覽器實作

    1.搭建開發環境

    安裝完成後,就可以使用Qt Creator來建立你自己的基于Qt的桌面程式,你可以在Qt Creator的歡迎界面看到入門程式、示例程式和幫助文檔。Qt的開發方式并不是本文所講述的重點,建議讀者到官網學習。

    雖然我們可以成功在Qt Creator内編譯并成功執行程式,但到windows目錄下通過輕按兩下執行編譯出的exe程式,就不能正常運作,這是因為可執行程式所需的動态連結庫并沒有與可執行程式在同一個目錄内,至于可執行程式依賴哪些動态連結庫,我們将在本文第四節較長的描述。

    2.邊框和标題欄

    目前大部分windows桌面程式都使用自定義的邊框和标題欄,比如QQ,360安全衛士等,使用MFC或Windows API自定義視窗的标題欄和邊框并不是一件容易的事情,使用Qt來開發Windows桌面程式也有一樣的困難。

    由于我們開發的是企業應用系統,這類系統一般情況下都出于最大化狀态,是以我們在考慮自定義标題欄和邊框的時候就可以不用考慮還原按鈕、拖拽改變視窗大小和位置的功能。但是,我們需要為标題欄增加一個下拉菜單按鈕,以使使用者完成系統設定、打開調試器等相關功能。

    另外,為了使标題欄和業務界面中ExtJs的風格一緻,我們索性去掉了主視窗的标題欄和邊框,直接使用ExtJs來生成。

    在Qt中去掉标題欄和邊框是很容易的事,建立視窗的時候設定一個WindowFlags即可,見如下代碼:    

w.setWindowFlags(Qt::FramelessWindowHint);

    但設定此WindowFlags之後随之帶來的問題是,視窗将撐滿整個螢幕,把系統的工作列也遮住了,這顯然不是我們想要的,解決此問題需要重寫Qt視窗類的changeEvent槽,見如下代碼:

if(event->WindowStateChange)

{

   switch(this->windowState())

   {

   case Qt::WindowMinimized:

this->hide();

event->ignore();

   break;

   case Qt::WindowMaximized:

   QDesktopWidget* desktopWidget =QApplication::desktop();

   QRect deskRect =desktopWidget->availableGeometry();

   this->resize(deskRect.width(), deskRect.height());

   }

}

    這樣建立的Qt視窗将不具有标題欄和邊框,至于如何用ExtJs來渲染标題欄,以及如何實作标題欄的最小化及關閉等功能,将在後續小節講述。

  3.打開新視窗

    使用Qt的WebKit非常簡單,直接把QWebView控件拖放到界面中去即可,但是預設的QWebView在實作上有些缺憾,比如無法打開新視窗,無法下載下傳檔案,無法列印等。然而這些功能是一個浏覽器所必備的功能,我們的CB/S企業應用系統也需要這些功能。要想讓浏覽器支撐這些功能,隻能通過重寫QWebView來完成。

    要想讓自制的浏覽器打開新視窗,需要重寫QWebView的createWindow方法,見如下代碼:(UtmpWebView即為QWebView的子類)

    UtmpWebView* webView = new UtmpWebView;

    QWebPage* newWeb = new QWebPage;

    if(type == QWebPage::WebModalDialog)

    {

        webView->setWindowModality(Qt::ApplicationModal);

    }

    webView->setAttribute(Qt::WA_DeleteOnClose,true);

    webView->setPage(newWeb);

    webView->show();

    return webView;

    然而,這隻能應對a标簽的target屬性為_blank的新視窗連結,無法應對使用javascript通過window.open的方式打開新視窗的場景。要想滿足這一點,必須在QWebView的構造函數裡,更改一下浏覽器的配置參數,代碼如下:

QWebSettings* default_settings = QWebSettings::globalSettings();

default_settings->setAttribute(QWebSettings::JavascriptEnabled,true);

default_settings->setAttribute(QWebSettings::JavascriptCanOpenWindows,true);

    4.列印

    我們經常在網頁中通過javascript使用window.print的方式來調用列印機列印HTML頁面,常見的浏覽器都會支援這個功能,然而QWebView預設并不支援此功能,要想讓我們定制的浏覽器支援此功能必須為其做一個事件連結,代碼如下:

connect(this->page(), SIGNAL(printRequested(QWebFrame*)),this,SLOT(customPrintRequested(QWebFrame*)));

    this->page()->setForwardUnsupportedContent(true);

customPrintRequested槽的實作如下:

    QPrinter* p = new QPrinter(QPrinter::HighResolution);

    QPrintDialog printDialog(p, this);

    printDialog.setWindowTitle("UTMP列印");

    if(printDialog.exec() != QDialog::Accepted)

        return;

    frame->print(p);

    5.下載下傳

    同樣QWebView預設也不支援下載下傳檔案。所有的浏覽器把請求的響應分為兩類,一類是浏覽器可以解析的(Html文本),另一類是浏覽器無法解析的(檔案),常見的浏覽器遇到無法解析的檔案,往往會下載下傳到本地給使用者使用,要想讓QWebView支援下載下傳,就必須截獲浏覽器的unsupportedContent信号,該信号所對應的槽的代碼實作如下

ShellExecuteA(NULL, "open", reply->url().toString().toStdString().c_str(), "", "", SW_SHOW);

    注意,要想讓上面的代碼正确執行,必須在頭檔案中引入windows.h(這也展現出QT架構與NativeAPI能沒有任何限制的輕松互動)。上面的代碼是調用了系統預設的浏覽器來完成下載下傳。當然讀者也可以考慮自己實作下載下傳線程并提示下載下傳進度、儲存位址等。

    6.與頁面腳本互動

    我們既然選擇自己開發浏覽器,那麼浏覽器一定能自如的讓頁面執行一些特殊腳本,頁面也可以通過腳本讓浏覽器完成一些腳本無法完成的操作。此功能一般的浏覽器都無法支撐,隻有我們自定義的QWebView可以輕松實作。

    我們知道javascript在頁面中執行都會用到window對象,比如,我們調用alert()方法時,其實是調用window.alert()方法,使用document對象時,其實是使用window.document對象,要想讓浏覽器能與頁面腳本互動,我們必須讓浏覽器給頁面的window對象注冊一個子對象(window對象的屬性)。

    遇到的第一個問題并不是如何注冊此對象,而是在何時注冊。由于在頁面加載之初,window對象就已經初始化完成了,此時為其注冊子對象已為時已晚,必須在其初始化之前為其注冊,為此QWebView專門提供了javaScriptWindowObjectCleared信号,在重新整理網頁、打開新網頁和加載嵌套的iframe頁面時(window對象初始化時),此信号都會被觸發。與此信号關聯的槽,代碼如下:

this->page()->mainFrame()->addToJavaScriptWindowObject("QtWinFrame", this);

如你所見,我們為window對象注冊了一個名為QtWinFrame的對象。這就像浏覽器為window對象注冊document子對象一樣,要想讓頁面腳本能調用浏覽器核心的方法,必須為讓浏覽器核心提供相應的方法才行,由于我們在第二小節已經把視窗預設的标題欄和邊框去掉了,是以必須通過頁面javascript來關閉浏覽器和最小化浏覽器,假設我們在浏覽器核心中實作的方法代碼如下:

void UtmpWebView::SetFrameWindow(int flag)

    switch(flag)

        case 0:

            this->close();

            break;

        case 1:

            this->showMinimized();

在浏覽器頁面内,隻要通過如下javascript代碼,即可讓浏覽器核心執行相應的操作:

QtWinFrame.SetFrameWindow(1);QtWinFrame.SetFrameWindow(0);

相對于“腳本讓浏覽器執行工作”來說,“浏覽器讓腳本執行工作”就簡單很多,隻需要在浏覽器中調用evaluateJavaScript方法即可,見如下代碼:

this->page()->mainFrame()->evaluateJavaScript("testFun();");

注意:這有些類似于javascirpt中的eval()方法,如果前端架構中引入了ExtJs,最好不要直接使用此方法來調用ExtJs提供的函數,執行效率非常慢。可以先在頁面上用普通的js函數包裝一下ExtJs提供的函數,再來調用。

    7.打開腳本調試器

    調試javascript代碼一直以來都是開發人員面臨的老大難的問題,自從有了FireBug和谷歌浏覽器自帶的javascript調試器之後,這個問題得到了很大程度的解決,是以有個好的javascript調試器十分關鍵。QWebView也提供了相應的調試工具(我認為就是谷歌浏覽器的javascript調試器,但未經驗證。)。使浏覽器核心打開調試器的代碼如下:

QDialog* d = new QDialog(this,(Qt::WindowMinimizeButtonHint|Qt::WindowMaximizeButtonHint|Qt::WindowCloseButtonHint));

d->setAttribute(Qt::WA_DeleteOnClose, true);

QWebInspector* wi = new QWebInspector(d);

wi->setPage(this->page());

d->setLayout(new QVBoxLayout());

d->layout()->setMargin(0);

d->layout()->addWidget(wi);

d->show();

d->resize(600,350);

    由于我們在系統啟動的時候,使用Qt::FramelessWindowHint屬性禁用掉了視窗的标題欄和邊框,是以在打開調試器子視窗的時候,要恢複該子視窗的标題欄和邊框,為此我們多做了一些工作,讀者也可以自己實作QDialog類型的父類,以應對更多子視窗業務。

 8.截獲浏覽器請求

    既然我們對浏覽器有最大的控制權,那麼我們就希望當浏覽器完成指定工作時通知我們,好讓我們做一些前期或後期的處理。最常見的工作莫過于浏覽器發起請求了。我們知道浏覽器解析一個網頁的過程中,可能會發起多次請求,比如圖檔标簽的src路徑,iframe标簽的src路徑,js/css資源的路徑等等。要想知道這些請求何時發起,何時終結需要重寫QNetworkAccessManager,然後通過如下方式,讓浏覽器加載自定義的QNetworkAccessManager

QNetworkAccessManager *oldManager = webview->page->networkAccessManager();

MyNetworkAccessManager *newManager = new MyNetworkAccessManager(oldManager, this);

webview->page->setNetworkAccessManager(newManager);

然後,我們可以在自定義的MyNetworkAccessManager類中重寫createRequest(QNetworkAccessManager::Operation operation,const QNetworkRequest &request, QIODevice *device)方法,其中request參數,包含了原始請求的URL資訊,此方法需要傳回一個QNetworkReply對象,假設我們想改變原始請求的路徑,可以按如下操作方式來完成

return QNetworkAccessManager::createRequest(operation, myrequest, device);

如你所見,我們用QNetworkAccessManager建立了一個請求(createRequest的傳回值為QNetworkReply類型),該請求中myrequest實參的類型為QNetworkRequest,其他兩個實參從原始方法中獲得。

    9.本地化ExtJs庫

    現在我們開發自己的浏覽器,就可以把Extjs庫(不包含業務JS代碼,因為業務JS代碼易于變化,不适合當作資源放在用戶端)當作資源放在用戶端,對于一個用戶端來說,體積越小越好,然而以ext4.2.1 gpl版為例,官方提供的壓縮包裡,有很多内容不适合打包到用戶端中。比如:教程、文檔、源碼、示例等,讀者可以自行将這些内容删掉,然後把精簡後的ExtJs類庫放到浏覽器應用程式編譯檔案夾内([appDirectory]\build-UTMP-Desktop_Qt_5_1_1_MinGW_32bit-Debug\debug),這樣Extjs類庫就與我們的浏覽器可執行程式在同一個目錄下了,如果讓浏覽器使用Extjs類庫的資源,還應該在此目錄下建立一個靜态檔案,以引入同目錄下的靜态資源,代碼如下:

    <link href="ext-4.2.1.883/resources/Css/ext-all.css" rel="stylesheet" type="text/css" />

    <script src="ext-4.2.1.883/ext-all-debug.js"></script>

    當然,單單引入資源,還無法呈現ExtJs的絢麗界面,此時還需要引入一個伺服器端的JS檔案,此檔案通過Extjs的類庫加載機制,加載更多的業務JS,以達到實作特定業務邏輯的目的。我們在下一節中會詳細介紹這些内容。

    <script src="http://localhost:8080/UTMP/app.js"></script>

    在QT中隻需要通過本地路徑加載這個靜态頁面即可,代碼如下:

UtmpWebView w;    

QDir dir(QDir::currentPath());

QUrl url = url.fromLocalFile(dir.path()+"/debug/index.html");

w.load(url);

    由此可見,儲存在用戶端的資源基本都是業務無關的、比較穩定的、不易變更的資源。儲存在服務端的内容,都是與業務有關的,比較容易變更的内容,這種機制主要意圖是保證了業務的可更新性。    

四:服務端業務腳本

 1.OPOA模式

    使用Extjs的企業應用系統大多都是OPOA模式(One Page One Application),OPOA模式的WEB系統隻有一個頁面,在這個頁面中會引入extjs的資源并通過js來渲染一個架構頁面,然後根據使用者的操作載入更多的js代碼,來完成不同的業務。對于我們的系統來說這個頁面就是放在用戶端本地debug目錄下的靜态頁面。這個頁面引入了一個伺服器端的js檔案(http://localhost:8080/UTMP/app.js),通過此檔案以及由此檔案加載的其他js檔案,我們渲染出了一個架構頁面,見如下代碼:

Ext.application({

    name:'UTMP',

    appFolder:'http://10.0.7.109:8080/UTMP/app',

    controllers:["sys.index"],

    views:["sys.menuTree","sys.titleBar","sys.contentTabPanel"],

    launch:function(){    

        Ext.create('Ext.Viewport',{

           layout:'border',

           items:[

               {xtype: 'menuTree'},

               {xtype: 'titleBar'},

               {xtype: 'contentTabPanel'}

           ]

        });

});

 2.定制子產品加載基址

    Extjs有一套獨特的子產品加載機制,它可以通過js類的名稱空間來加載相應的js代碼檔案,比如視圖檔案的名稱空間是UTMP.sys.menuTree,ExtJs架構會從appFolder指定的路徑下找sys目錄下的menuTree.js檔案。在普通的ExtJs項目中,appFolder屬性并不用設定為絕對路徑,隻需要使用相對路徑即可,但由于我們的項目的首頁(靜态頁面)是放在用戶端本地的,如果使用相對路徑的話,ExtJs架構就會在用戶端本地尋找相應的資源,然而我們的業務JS檔案都是放在服務端的,是以勢必會無法加載這些資源。

 3.定制AJAX請求基址

    子產品加載機制可以通過設定appFolder基路徑來解決,但是對于業務JS代碼随處可見的AJAX請求該如何處理呢?确實,AJAX請求也會面臨這種問題,而且更為突出。因為在ExtJs中對AJAX請求做了很多封裝:proxy、store、request、load等,随處可見ajax的身影。幸而ExtJs是一個對象化程度較高的js類庫,使得這個問題能很容易的解決。

    在ExtJs中所有Ajax請求都離不開Ext.data.Connection類的支撐,我們可以使用ExtJs提供的觀察者模式來注冊Ext.data.Connection類的beforerequest事件(這是一種侵入性較強的做法),這樣所有的AJAX請求,不管是proxy發起的,還是request發起的,都逃不出我們的手心,具體實作代碼如下:

Ext.util.Observable.observe(Ext.data.Connection,{

    beforerequest: function(conn, options, eOpts){

        options.url = "http://10.0.7.109:8080/UTMP/"+options.url;

五:分發

 1.依賴的動态連結庫

    在使用QTCreator開發基于QT的應用程式時,不管是debug編譯還是release編譯,都無法到編譯目錄下,通過輕按兩下exe程式來執行應用(會提示“無法啟動此程式,因為計算機中丢失xxxx.dll....”的錯誤資訊),之是以在IDE内能順利執行,是因為IDE已經為程式執行建立好了環境,但倘若不解決此問題,就無法把應用程式分發給直接使用者。

     要解決此問題隻要把Qt類庫提供的dll檔案放在可執行程式的目錄下或其所在目錄的子目錄下即可,在C:\Qt\Qt5.1.1\5.1.1\mingw48_32\bin目錄下有Qt類庫提供的大多數dll,這些dll名稱以字母d結尾的是debug編譯的應用程式所依賴的類庫,不以字母d結尾的則是release編譯的應用程式所需要的類庫,除了此目錄内的dll外,在C:\Qt\Qt5.1.1\5.1.1\mingw48_32\plugins目錄下還有一些應用程式需要的dll類庫。

    如此數量衆多的dll,都需要打包到我們最終的安裝程式中去嗎?當然不用這麼做。通過IDE執行我們的應用程式時,我們隻需要通過processExplorer工具來檢視應用程式程序所依賴的dll,即可判定哪些dll是需要打包到安裝包中去的(大多數情況下可以這麼做,如果是開發人員通過代碼動态加載的類庫,那麼我相信你也會自行甄别的)。

 2.打包

    可能有的讀者會問:“我可不可以把類庫靜态編譯到exe中去呢?”當然可以,但是非常麻煩,你需要自己靜态編譯整個QT工程,還需要對IDE做出相應的調整(要編譯QT的Webkit還需要做更多的工作),這是一項耗時、耗力還不一定能成功的工作,我不建議這麼做。

請不要讓我調試代碼,老夫很忙!懶的管!

繼續閱讀