WebKit on the iPhone
如果你開發一個應用程序,它是顯示一個web頁麵或HTML文件,您可以使用WebKit框架,這是MacOS和iPhone OS的一部分。
但是即使再mac上,webkit的框架也提供了獎金160個公共的頭文件甚至有更多的類和方法,你可以使用他控製很多東西,包括加載,渲染顯示和修改頁麵。但iPhone上僅僅給我們了一個類(UIWebView)來控製這一係列錯作。盡管UIWebView用的是相同的WebKit組建,但mac是作為公有的API,而iPhone是作為私有的API,所以不能使用。很少的UIWebView的方法足夠漂亮的文本,但是對於一個瀏覽器(iCab Moblie)或者其他基於網頁的應用這是不夠的,缺失很多的必要的方法。
一些例子:
UIWebView沒有提供一個方法來獲取當前顯示的標題web頁麵,
它隻是忽略所有試圖打開鏈接,旨在打開新窗口或選項卡
它不允許訪問HTML樹
WebKit本身為所有這些任務提供了許多類,但他們都是私有的和iPhone所不具備的。
一些在應用商店的更改的瀏覽器都剛剛宣布這些限製作為一種特性(例如他們廣告無法打開新窗口或標榜為“沒有煩人的彈出式窗口”)。這聽起來很不錯,但現實使用中,這並不使這樣的瀏覽器很有用。
所以我們能做些什麼來克服這些局限性的UIWebView類?在Mac在iPhone沒有違反與蘋果iPhone SDK協議下,我們可以(重新)實現所有很酷的特性是可用的WebKit框架,讓iPhone和mac一樣。不幸的是,我們不能。但我們可以實現許多丟失的特性。
如果你看一下可用的方法,隻有一個,這將允許訪問web頁麵的內容,這是或多或少的惟一辦法回丟失的特性。和這種方法是
- (NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;
這個方法是用來執行JavaScript代碼上下文中的當前web頁麵,並返回一個字符串作為結果。這意味著我們必須使用JavaScript代碼來實現我們需要的特性。
讓我們先從一些非常容易。我們實現方法來獲取標題和URL的當前顯示的web頁麵。我們實現這個作為一個Objective-C“類別”,所以我們不需要子類UIWebView:
File: MyWebViewAdditions.h
@interface UIWebView (MyWebViewAdditions) - (NSString*)title; - (NSURL*)url; @end
File: MyWebViewAdditions.m
#import "MyWebViewAdditions.h" @implementation UIWebView (MyWebViewAdditions) - (NSString*)title { return [self stringByEvaluatingJavaScriptFromString:@"document.title"]; } - (NSURL*)url { NSString *urlString = [self stringByEvaluatingJavaScriptFromString:@"location.href"]; if (urlString) { return [NSURL URLWithString:urlString]; } else { return nil; } } @end
我們現在在做什麼呢?
從JavaScript的觀點是,一個網頁所代表的“文檔”對象有幾個屬性。一個屬性是“title”包含頁麵的標題。所以用“document.title”我們可以在JavaScript訪問文檔的標題。而這正是我們需要傳遞參數的方法”stringByEvaluatingJavaScriptFromString:“獲得文檔標題。
對於URL我們做類似的事情。
所以每當我們需要得到title或URL的web頁麵,顯示在一個UIWebView對象,我們隻需要調用“title”或“URL”方法:
NSString *title = [anyUIWebViewObject title];
接下來我們想做的限製可能是想地址是不能打開鏈接將打開一個新窗口。在Mac上的WebKit隻會調用一個委托方法的主機應用程序請求一個新的WebView對象創建一個URL請求。應用程序將創建一個新的WebView對象並加載新頁。但是在iPhone的UIWebView並不支持這樣一個委托方法,因此所有試圖打開一個鏈接隻是忽略。
這些鏈接做通常看起來像這樣:
<a href="destination" target="_blank">Link Text</a>
The “target” attribute defines where the link will open. The value can be a name of a frame (if the web page has frames), the name of a window or some reserved target names like “_blank” (opens a new window), “_self” (the window itself), “_parent” (the parent frame, if there are nested frames) and “_top” (the top-level or root frame, or identical to “_self” if the page doesn’t use frames).
As a first step, we want to tap on a such a link in our iPhone App, and the link should open like any other normal link in the same UIWebView object. What we need to do is simple: we need to find all links with a “target” attribute set to “_blank” and change its value to “_self“. Then the UIWebView object will no longer ignore these links. To be able to modify all of the link targets we have to wait until the page has finished loading and the whole web page content is available. Fortunately UIWebView provides the delegate method
- (void)webViewDidFinishLoad:(UIWebView *)webView;
which will be called when the web page has finished loading. So we have everything we need: We get notified when the page has loaded, and we know a way to access and modify the web page content (using “stringByEvaluatingJavaScriptFromString:“).
First we write our JavaScript code. Because this will be a little bit more code than what was needed to get the document title, it’s a good idea to create an extra file for our JavaScript code and then we add this file to the resources of our project in XCode:
File: ModifyLinkTargets.js:
function MyIPhoneApp_ModifyLinkTargets() { var allLinks = document.getElementsByTagName('a'); if (allLinks) { var i; for (i=0; i<allLinks.length; i++) { var link = allLinks[i]; var target = link.getAttribute('target'); if (target && target == '_blank') { link.setAttribute('target','_self'); } } } }
What is this JavaScript function doing, when called?
It gets an array of all links (“a”
tags) and then loops through all of these tags, checks if there’s a target attribute with the value “_blank“.
If this is the case it changes the value to “_self“.
Note: There are other tags which can have a “target” attribute, like the “form” tag and the “area” tag. So you can use the “getElementsByTagName()” call to get these tags as well and modify their target attributes in the same way as I’ve done this for the “a” tag.
In our iPhone App we need to define a delegate for the UIWebView object and this delegate object will be called whenever the web page has finished loading. This is the method that is called in the delegate by the UIWebView object:
- (void)webViewDidFinishLoad:(UIWebView *)webView { NSString *path = [[NSBundle mainBundle] pathForResource:@"ModifyLinkTargets" ofType:@"js"]; NSString *jsCode = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil]; [webView stringByEvaluatingJavaScriptFromString:jsCode]; [webView stringByEvaluatingJavaScriptFromString:@"MyIPhoneApp_ModifyLinkTargets()"]; }
What is this code doing?
At first the access path of the JavaScript file we created before is retreived from the application bundle and we load the content of the file (the JavaScript code) into a string. Then we execute/inject this code into the web page and finally we call out JavaScript
function
which is modifying the link targets.
Some notes:
- Getting the JavaScript file from the application bundle and loading it into a string should be usually done somewhere in the init methods of your UIWebView delegate object. This way the string with our JavaScript code is only loaded once and can be simply reused whenever a new link is clicked and a new page is loaded.
- Using a long name for our JavaScript function which also includes a prefix like “MyIPhoneApp_” makes it unlikely that the code we inject into a web page will interfere or confict with functions and variables which the web page itself has already defined for its own purposes. This is especially important when we modify web pages we haven’t created ourselves and where we can not predict which function or variable names the JavaScript code of the web page is already using.
- Using separate calls of “stringByEvaluatingJavaScriptFromString” to first injecting our own JavaScript code and then calling our own JavaScript function to start modifying the link targets seems to be more complicated that necessary. And for this simple example you would be right. But it is likely that you’ll define much more additional JavaScript functions for many different tasks as well. Some of the tasks are started when the page has finished loading (like modifying the link targets), but some tasks will be started later and maybe even multiple times. And so it makes much sense that injecting the code and calling the functions are done in separate calls.
- The delegate method “webViewDidFinishLoad:(UIWebView *)webView” is called for each frame, not only when the page itself has finished loading. This means that this delegate method can be called multiple times while a single web page is loaded. I think that this can be called a bug in the iPhone OS, but nevertheless it is important to know. When you modify the web page, be aware that this might be done multiple times and so make sure that none of your modifications will have bad side effectes when being modified a second time.
What next?
- The above example code does not cover web pages where new windows are opened using JavaScript.
- The links will open in the same window, which is fine because they are no longer ignored. But they still don’t open in a new window or Tab.
More about this topic and the cases which are not yet covered will come in the second part of the “WebKit on the iPhone” article.
Feel free to ask questions and write comments. I’d like to get some feedback.
最後更新:2017-04-04 07:03:32