很久沒有更新博客了.最近一直陷身在項目中難以有時間抽身梳理總結.關於博客確實很多想寫的主題.節前大概草草 的梳理一下大概就有十幾個主題.只能趁着放假的時間來逐漸把這批文章力所能及系統的更新出來. 主要涉及到我們團隊現在Windows phone 項目開發中實際碰到一些問題和對應解決方案.如果想關注即時了解每天動態信息可以直接在Sina微博@chenkaiHome 溝通交流.
在開始更新這批博文前.一直在顧慮先更新那個主題為好.回頭一想索性就說說這半個月有些苦惱的Windows phone中處理 WebBrowser在我們項目中表現出來問題.
話說去年.技術團隊提出要優化產品在各個平台[IOS/Android/WP7/QT]客戶端開發業務流程.提出這個問題主要是為了把原來通用業務邏輯流程封裝到能力更大的服務器端來做.各個客戶端在通過WebView統一的形式調用.這樣做目的.主要是解決原來各個客戶端在業務升級后更新客戶端版本時減少重新開發量.這樣一來把核心的業務邏輯變動全部集中服務器端.需求變化自上而下傳遞過程中.在各個平台之間可以復用. 在每次更迭客戶端版本時提升開發團隊效率.
最開始我們采用的方案焦點主要是考慮到WebView封裝通用的業務邏輯流程涉及到與對應平台原生應用程序的交互問題上.所以也就理所當然有人提出跨平台移動框架PhoneGap[平台交互]+HTML 5[UI呈現]的處理方案. 原來我們設想真的很簡單.也天真的認為PhoneGap+HTML 5搭配會把通用業務流程問題迎刃而解.
首先.PhoneGap[PS參考:Windows phone 應用開發[8]-體驗PhoneGap]作為移動跨平台框架.着重解決的問題是通過JavaScript實現跨平台API交互.並沒有UI.頁面還需要借助HTML 5效果.即使如此.也是難以和原生應用程序界面相媲美的.這就需要在使用過程中.要犧牲掉大量原生應用程序交互細節.用戶體驗上打了一個折扣.當然這和我們解決的核心問題做出犧牲還是值得.但問題各個平台兼容性需要調整各個平台適配問題大大出乎我們預估.而在性能差異上更是難以兼顧保證的.而對於初次使用PhoneGap團隊解決這些問題所耗費的開發周期時間.卻遠大於開發Native Application原生應用時間還要長.這完全和使用初衷相背離. 其實問題只是換了一種形式存在. 我們只是從一個熟悉能夠預估量的泥潭跳到另外完全未知泥潭中.不斷嘗試掙扎…
談到這.說一個細節.類似在WebBrowser瀏覽器控件中.打開一個新窗口的問題.Android平台可以采用LoadUrl方法直接打開一個新窗體實現Js控制的頁面跳轉. 而目前Windows phone WebBrowser情況不支持在當前頁打開新窗體.雖然可以通過InvokeJavaScript()放在LoadComplated方法中注入Js控制方法替換打開方式來實現. 但在實際調試中會發現.開發人員在C# 后台代碼中調試JavaScript來說是一個挑戰. 一來WebBrowser在注入和執行Js過程返回的錯誤或異常都是簡單的代碼80開頭Code.而沒有具體的堆棧信息. 這對找錯和確認問題照成很大障礙.另外在C#后台代碼處理JS缺乏有效的調試工具支持.這對PhoneGap封裝出來頁面復雜的Js調用或數據交互操作.照成一定難題.
說白了.在PhoenGap中通過JS實現Windows phone應用平台交互主要體現在兩個點上.第一就是通過在Js中調用:
【JAvaScript:】
window.external.Notify(“”);
方法把頁面交互數據通過WebBrowser控件SCriptNotify事件接收傳遞給原生應用程序. 另外一個點.就是可以在在Windows phone應用程序直接調用WEbBrowser控件InvokeScript()方法來調用JavaScript函數. 這兩個方法.實現了PhoneGap數據傳遞和交互整個過程.
那在Windows phone應用程序使用WebBrowser有哪些常見需要解決的問題?
[1]異常處理.
well.這里不得不首先說在WebBrowser調用JavaSCript時需要處理的異常問題.Windows Phone 提供一個基於桌面版本的 Silverlight 的 WebBrowser 控件,也就是說WindowsPhone 目前的WEbBrowser控件是基於Silverlight桌面版本的WebBrowser控件而來,但仍然有幾處不同[WEbBrowser與Silverlight版本不同地方].其中兩個版本在開發最大不同主要有如下幾點:
Windows phone WebBrowser控件與Silverlight 桌面版本的不同:
[1]Windows phone 版本相對Silverlight版本具有直接使用 IsolateStroage獨立存儲的權限
[2]相對Silverlight在執行InvokeScript()方法時限制了執行范圍必須是XAP 程序包相同的站點中加載的腳本.而Windows phone 解除該限制.
[3]在Windows phone版本時從獨立存儲加載的內容或使用 NavigateToString(String) 方法加載的內容沒有跨站點訪問限制。
那么在Windows phone WebBrowser中調用JavaScript常見的可能出現的異常主要有兩個,如下重現這兩個異常情況.首先創建一個Windows Phone Application 應用程序. Mainpage.CS:
1: <!--ContentPanel - place additional content here-->
2: <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
3: <StackPanel>
4: <phone:WebBrowser x:Name="ComponentContent_WB" Height="450"/>
5: <Button x:Name="ExcuteScript_BT" Content="Excute JavaScript" Margin="0,50,0,0" Click="ExcuteScript_BT_Click"></Button>
6: </StackPanel>
7: </Grid>
定義一個WebBrowser和Button按鈕用來在頁面加載完成后執行JavaSCripti函數事件.當然在執行JavaSCript需要設置WEbbrowser可以調用JS的. 設置IsScriptEnabled="True" BehindCode 如下:
1: // Constructor
2: public MainPage()
3: {
4: InitializeComponent();
5: this.Loaded += new RoutedEventHandler(MainPage_Loaded);
6: }
7:
8: void MainPage_Loaded(object sender, RoutedEventArgs e)
9: {
10: string navigateUrl = @"http://www.163.com";
11: this.ComponentContent_WB.Navigate(new Uri(navigateUrl, UriKind.RelativeOrAbsolute));
12: }
13:
14: private void ExcuteScript_BT_Click(object sender, RoutedEventArgs e)
15: {
16: //Button Client Event Excute InvokeJavaScript Method
17: try
18: {
19: this.ComponentContent_WB.InvokeScript("DefineNoExistJSMethod");
20: }
21: catch (Exception se)
22: {
23: MessageBox.Show("Excute JavaScript Have Exception:" + se.Message);
24: }
25: }
調用通用網易站點.在加載頁面完成后通過Button按鈕執行一個不來就不存在JavaScript函數.執行效果如下":
因這個JavaScript函數不存在所以執行肯定報錯.注意這里報錯信息是以80020006為開頭的UnKnowError如下:
可見在堆棧的異常信息一欄中對JavaScripit提供的信息非常有限.這個Message代碼為80020006.其實就是在當前應用程序執行范圍找不到該JavaScript方法.另外一種情況恰恰相反.在執行已經定義JavaScript Function 函數出現的異常. 類似找到163.com站點中一個任意JAvaScript函數在后台方法調用:
1: function NTESAutoComplete ( inputElem, nextElem ) {
2: var t = this;
3: t._inputElem = inputElem;
4: t._nextElem = nextElem;
5: t._idName = "login_auto_list";
6: t._className = "login-auto-list";
7: }
注意InvokeScript方法在執行帶有JAvaScript參數時. 參數傳遞是以String[]數組方式傳遞給JAvaScript函數.調用:
1: private void ExcuteScript_BT_Click(object sender, RoutedEventArgs e)
2: {
3: //Button Client Event Excute InvokeJavaScript Method
4: try
5: {
6: this.ComponentContent_WB.InvokeScript("NTESAutoComplete",new string[]{"NoExistElement",
"NoExistStringArgument"});
7: }
8: catch (Exception se)
9: {
10: MessageBox.Show("Excute JavaScript Have Exception:" + se.Message);
11: }
12: }
執行效果如下:
執行過程中InvokeScript得到異常 "An unknown error has occurred. Error: 80020101".而這個異常是在往往執行過程JavaScript內部錯誤引起.因在后台代碼沒有有效的工具.支持.所以對於JavaScript的錯誤是很難查找確認問題具體在那. 這個問題出現一般會有兩種大概原因.
第一點.在調用InvokeScript()是WebBrowser控件事件執行順序.其實針對WEbBrowser控件.除了從FrameworkElement類和Control類繼承了通用了UIElement屬性和方法外.WEbBrowser重點擴展自身導航操作.類似其中三個比較中重要的方法.Navigating、Navigated 和 LoadCompleted事件.
那么說到這 這個三個事件在實際操作執行順序是?
WebBrowser導航事件的執行順序:
Navigating > Navigated > LoadCompleted
Navigating是執行Navigate方法表示當前WEbBrowser正在執行加載URL操作、Navigated事件WebBrowser 控件成功導航后發生 和 LoadCompleted事件在 WebBrowser 控件成功加載內容后發生.
如果在這種情況下.即使我們發現我們Codebehind中InvokeScript()調用JS沒有問題.同時HTML JavaScript函數測試也沒有問題.這就導致我們始終無法通過程序測試找到JS 報錯80020101異常在那. 這是在后台代碼調試JAvaScript最讓人痛苦的地方.比如我們在如下方法掉用如上JavaScript函數:
void Wb_Navigated(object sender, System.Windows.Navigation.NavigationEventArgs e)
{
Wb.InvokeScript("eval", "document.forms[0].submit();"); // Throws 80020101
}
private void MainPage_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
Wb.InvokeScript("eval", "document.forms[0].submit();"); // Works
}
可以發現.如果在Navigated事件觸發時.即使我們后台代碼調用和JavaScript函數都沒有錯誤.依然還是爆出80020101的異常.這主要是因為DOM對象操作在頁面觸發Navigated事件還沒有完全初始化.導致調用頁面執行時出現異常.
第二點.則是比較常見的即使需要對JavaSCript做一定修改.確保JS函數在執行時不會出錯. 則這個80020101異常一般都會在如上兩種情況下出現.
[2]現實靜態頁面.
在WEbBrowser中.可能需要在沒有網絡情況下.需要在某一些情況下通過后台應用程序操作HTML頁面. 而在Windows phone提供兩種方式來加載本地靜態的HTML頁面.
在Windows phone 中WEbBrowser中提供NavigateToString方法將 HTML 字符串置於 Web 瀏覽器控件中以便進行呈現.操作也是簡單的:
1: string defineHtmlStr = @"<html>
2: <head>
3: <script>
4: function DefineExistFun(elementStr)
5: {
6: var getElems=document.getElementByTag(elementStr);
7: alert(elementStr);
8: }
9: </script>
10: <body>
11: <a href=" + "http://chenkai.cnblogs.com" + ">Test</a>"
12: + "</body>"
13: + "</head></html>";
14: this.ComponentContent_WB.NavigateToString(defineHtmlStr);
加載頁面效果:
另外一種方式.則是加載一定定制好靜態HTML頁面.使用 WebBrowser 控件在應用程序中顯示已設置格式的靜態內容。例如,開發人員可能希望在應用程序包中包含幫助文本,以便用戶可以隨時訪問.創建一個靜態HTML界面:
1: <html>
2: <head>
3: <script>
4: function DefineExistFun(elementStr)
5: {
6: var getElems=document.getElementByTag(elementStr);
7: alert(elementStr);
8: }
9: </script>
10: <body>
11: <a href="http://chenkai.cnblogs.com">Test</a>
12: </body>
13: </head>
14: </html>
在執行第一步需要把該CreateProduct.html頁面添加解決方案.設置引用資源為Content.需要向獨立存儲中添加存儲靜態文件.:
1: private void SaveFilesToIsoStore()
2: {
3: //These files must match what is included in the application package,
4: //or BinaryStream.Dispose below will throw an exception.
5: string[] files = {
6: "CreateProduct.html"
7: };
8:
9: IsolatedStorageFile isoStore = IsolatedStorageFile.GetUserStoreForApplication();
10:
11: if (false == isoStore.FileExists(files[0]))
12: {
13: foreach (string f in files)
14: {
15: StreamResourceInfo sr = Application.GetResourceStream(new Uri(f, UriKind.Relative));
16: using (BinaryReader br = new BinaryReader(sr.Stream))
17: {
18: byte[] data = br.ReadBytes((int)sr.Stream.Length);
19: SaveToIsoStore(f, data);
20: }
21: }
22: }
23: }
24:
25: private void SaveToIsoStore(string fileName, byte[] data)
26: {
27: string strBaseDir = string.Empty;
28: string delimStr = "/";
29: char[] delimiter = delimStr.ToCharArray();
30: string[] dirsPath = fileName.Split(delimiter);
31:
32: //Get the IsoStore.
33: IsolatedStorageFile isoStore = IsolatedStorageFile.GetUserStoreForApplication();
34:
35: //Re-create the directory structure.
36: for (int i = 0; i < dirsPath.Length - 1; i++)
37: {
38: strBaseDir = System.IO.Path.Combine(strBaseDir, dirsPath[i]);
39: isoStore.CreateDirectory(strBaseDir);
40: }
41:
42: //Remove the existing file.
43: if (isoStore.FileExists(fileName))
44: {
45: isoStore.DeleteFile(fileName);
46: }
47:
48: //Write the file.
49: using (BinaryWriter bw = new BinaryWriter(isoStore.CreateFile(fileName)))
50: {
51: bw.Write(data);
52: bw.Close();
53: }
54: }
把需要展示的靜態HTML頁面在調用前需要存儲到獨立存儲中.調用如下:
1: void MainPage_Loaded(object sender, RoutedEventArgs e)
2: {
3: SaveFilesToIsoStore();
4: ComponentContent_WB.Navigate(new Uri("CreateProduct.html", UriKind.Relative));
5: }
針對在Windows phone WEbBrowser中於JavaScript交付問題的問題出現的異常.在后台代碼上處理是非常弱的.首先在CodeBehind中沒有成行JS調試工具支持.這對不熟悉前段JavaSCript代碼的開發人員來說是一個挑戰. 另外一個問題就是一旦調用JavaScript出現異常情況.很難確認問題源頭.這也大大影響開發效率.
當然在WEbBroser還涉及到頁面加載控制. 新窗口打開. 控制WEbBrowser頁面縮放等問題.這里就不再一一贅述.
關於本片源碼詳見:https://github.com/chenkai/WebBrowser-Case-Windows-phone-Sample
源碼下載:/Files/chenkai/WebBrowserWP7Demo.rar
如有問題可以Weibo上溝通交流:http://weibo.com/chenkaihome