• <samp id="frm2q"><ins id="frm2q"></ins></samp>
      1. <samp id="frm2q"></samp>
        <progress id="frm2q"><bdo id="frm2q"><strong id="frm2q"></strong></bdo></progress>
        <menuitem id="frm2q"><ins id="frm2q"><u id="frm2q"></u></ins></menuitem>

        <progress id="frm2q"></progress>
      2. iOS下JS與OC互相調用(八)--Cordova詳解+實戰

        小編:管理員 97閱讀 2022.09.13

        扯兩句,可以跳過

        由于項目中Cordova相關功能一直是同事在負責,所以也沒有仔細的去探究Cordova到底是怎么使用的,又是如何實現JS 與 OC 的交互。所以我基本上是從零開始研究和學習Cordova的使用,從上篇在官網實現命令行創建工程,到工程運行起來,實際項目中怎么使用Cordova,可能還有一些人并不懂,其實我當時執行完那些命令后也不懂。 后來搜索了一下關于Cordova 講解的文章,沒有找到一篇清晰將出如何使用Cordova,大多都是講如何將Cordova.xcodeproj拖進工程等等。我不喜歡工程里多余的東西太多,其實并不需要將Cordova 整個工程拖進去,只需要一部分就夠了,下面我會一一道來。

        1.新建工程,添加Cordova 關鍵類

        我這里用Xcode 8 新建了一個工程,叫JS_OC_Cordova,然后將Cordova關鍵類添加進工程。 有哪些關鍵類呢? 這里添加config.xml、Private和Public兩個文件夾里的所有文件。工程目錄結構如下:

        然后運行工程,? ? ? ,你會發現報了一堆的錯誤:

        為什么有會這么多報錯呢? 原因是Cordova 部分類中,并沒有#import <Foundation/Foundation.h>,但是它們卻使用了這個庫里的NSArray、NSString 等類型。 為什么用在終端里用命令行創建的工程就正常呢? 那是因為用命令行創建的工程里已經包含了pch 文件,并且已經import 了 Foundation框架。截圖為證:

        其實這里有兩種解決方案:1、在報錯的類里添加上#import <Foundation/Foundation.h>;2、添加一個pch 文件,在pch文件里加上#import <Foundation/Foundation.h>。 我選擇第二種方案:

        再次編譯、運行,依然報錯。 What the fuck ? ? ? !!! 不用急,這里報錯是因為Cordova的類引用錯誤,在命令行創建的工程里Cordova 是以子工程的形式加入到目標工程中,兩個工程的命名空間不同,所以import 是用 類似這樣的方式#import <Cordova/CDV.h>,但是我們現在是直接在目標工程里添加Cordova,所以要把#import <Cordova/CDV.h>改為#import "CDV.h"。其他的文件引用報錯同理。

        當然,如果想偷懶,也可以從后面我給的示例工程里拷貝,我修改過的Cordova庫。

        2.設置網頁控制器,添加網頁

        首先將ViewController的父類改為CDVViewController。如下圖所示:

        這里分兩種情況,加載本地HTML 和遠程HTML 地址。 ** 加載本地HTML ** 加載本地HTML,為了方便起見,首先新建一個叫www的文件夾,然后在文件夾里放入要加載的HTML和cordova.js。 這里把www添加進工程時,需要注意勾選的是create foler references,創建的是藍色文件夾。

        最終的目錄結構如下:

        上面為什么說是方便起見呢? 先說答案,因為CDVViewController有兩個屬性wwwFolderName和startPage,wwwFolderName的默認值為www,startPage的默認值為index.html。

        在CDVViewController的viewDidLoad方法中,調用了與網頁相關的三個方法:- loadSetting、- createGapView、- appUrl。 先看- loadSetting,這里會對wwwFolderName和startPage設置默認值,代碼如下:

        - (void)loadSettings
        {
            CDVConfigParser* delegate = [[CDVConfigParser alloc] init];
        
            [self parseSettingsWithParser:delegate];
        
            // Get the plugin dictionary, whitelist and settings from the delegate.
            self.pluginsMap = delegate.pluginsDict;
            self.startupPluginNames = delegate.startupPluginNames;
            self.settings = delegate.settings;
        
            // And the start folder/page.
            if(self.wwwFolderName == nil){
                self.wwwFolderName = @"www";
            }
            if(delegate.startPage && self.startPage == nil){
                self.startPage = delegate.startPage;
            }
            if (self.startPage == nil) {
                self.startPage = @"index.html";
            }
        
            // Initialize the plugin objects dict.
            self.pluginObjects = [[NSMutableDictionary alloc] initWithCapacity:20];
        }
        復制

        要看- createGapView,是因為這個方法內部先調用了一次- appUrl,所以關鍵還是- appUrl。源碼如下:

        - (NSURL*)appUrl
        {
            NSURL* appURL = nil;
        
            if ([self.startPage rangeOfString:@"://"].location != NSNotFound) {
                appURL = [NSURL URLWithString:self.startPage];
            } else if ([self.wwwFolderName rangeOfString:@"://"].location != NSNotFound) {
                appURL = [NSURL URLWithString:[NSString stringWithFormat:@"%@/%@", self.wwwFolderName, self.startPage]];
            } else if([self.wwwFolderName hasSuffix:@".bundle"]){
                // www folder is actually a bundle
                NSBundle* bundle = [NSBundle bundleWithPath:self.wwwFolderName];
                appURL = [bundle URLForResource:self.startPage withExtension:nil];
            } else if([self.wwwFolderName hasSuffix:@".framework"]){
                // www folder is actually a framework
                NSBundle* bundle = [NSBundle bundleWithPath:self.wwwFolderName];
                appURL = [bundle URLForResource:self.startPage withExtension:nil];
            } else {
                // CB-3005 strip parameters from start page to check if page exists in resources
                NSURL* startURL = [NSURL URLWithString:self.startPage];
                NSString* startFilePath = [self.commandDelegate pathForResource:[startURL path]];
        
                if (startFilePath == nil) {
                    appURL = nil;
                } else {
                    appURL = [NSURL fileURLWithPath:startFilePath];
                    // CB-3005 Add on the query params or fragment.
                    NSString* startPageNoParentDirs = self.startPage;
                    NSRange r = [startPageNoParentDirs rangeOfCharacterFromSet:[NSCharacterSet characterSetWithCharactersInString:@"?#"] options:0];
                    if (r.location != NSNotFound) {
                        NSString* queryAndOrFragment = [self.startPage substringFromIndex:r.location];
                        appURL = [NSURL URLWithString:queryAndOrFragment relativeToURL:appURL];
                    }
                }
            }
        
            return appURL;
        }
        復制

        此時運行效果圖:

        ** 加載遠程HTML ** 項目里一般都是這種情況,接口返回H5地址,然后用網頁加載H5地址。 只需要設置下self.startPage就好了。

        這里有幾個需要注意的地方:

        1. self.startPage的賦值,必須在[super viewDidLoad]之前,否則self.startPage 會被默認賦值為index.html。
        2. 需要在config.xml中修改一下配置,否則加載遠程H5時,會自動打開瀏覽器加載。 需要添加的配置是:
        <allow-navigation href="https://*/*" />
        <allow-navigation href="http://*/*"  />
        復制
        1. 遠程H5中也要引用cordova.js文件。
        2. 在info.plist中添加App Transport Security Setting的設置。

        運行效果圖:

        3.創建插件,配置插件

        在插件中實現JS要調用的原生方法,插件要繼承自CDVPlugin,示例代碼如下:

        #import "CDV.h"
        
        @interface HaleyPlugin : CDVPlugin
        
        - (void)scan:(CDVInvokedUrlCommand *)command;
        
        - (void)location:(CDVInvokedUrlCommand *)command;
        
        - (void)pay:(CDVInvokedUrlCommand *)command;
        
        - (void)share:(CDVInvokedUrlCommand *)command;
        
        - (void)changeColor:(CDVInvokedUrlCommand *)command;
        
        - (void)shake:(CDVInvokedUrlCommand *)command;
        
        - (void)playSound:(CDVInvokedUrlCommand *)command;
        
        @end
        復制

        配置插件,是在config.xml的widget中添加自己創建的插件。 如下圖所示:

        關于插件中方法的實現有幾個注意點: 1、如果你發現類似如下的警告:

        THREAD WARNING: ['scan'] took '290.006104' ms. Plugin should use a background thread.
        復制

        那么直需要將實現改為如下方式即可:

        [self.commandDelegate runInBackground:^{
              // 這里是實現
        }];
        復制

        示例代碼:

        - (void)scan:(CDVInvokedUrlCommand *)command
        {
            [self.commandDelegate runInBackground:^{
                dispatch_async(dispatch_get_main_queue(), ^{
                    UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"原生彈窗" message:nil delegate:nil cancelButtonTitle:@"知道了" otherButtonTitles:nil, nil];
                    [alertView show];
                });
            }];
        }
        復制

        2、如何獲取JS 傳過來的參數呢?CDVInvokedUrlCommand參數,其實有四個屬性,分別是arguments、callbackId、className、methodName。其中arguments,就是參數數組。 看一個獲取參數的示例代碼:

        - (void)share:(CDVInvokedUrlCommand *)command
        {
            NSUInteger code = 1;
            NSString *tip = @"分享成功";
            NSArray *arguments = command.arguments;
            if (arguments.count < 3) {;
                code = 2;
                tip = @"參數錯誤";
                NSString *jsStr = [NSString stringWithFormat:@"shareResult('%@')",tip];
                [self.commandDelegate evalJs:jsStr];
                return;
            }
            
            NSLog(@"從H5獲取的分享參數:%@",arguments);
            NSString *title = arguments[0];
            NSString *content = arguments[1];
            NSString *url = arguments[2];
            
            // 這里是分享的相關代碼......
            
            // 將分享結果返回給js
            NSString *jsStr = [NSString stringWithFormat:@"shareResult('%@','%@','%@')",title,content,url];
            [self.commandDelegate evalJs:jsStr];
        }
        復制

        3、如何將Native的結果回調給JS ? 這里有兩種方式:第一種是直接執行JS,調用UIWebView 的執行js 方法。示例代碼如下:

        // 將分享結果返回給js
            NSString *jsStr = [NSString stringWithFormat:@"shareResult('%@','%@','%@')",title,content,url];
            [self.commandDelegate evalJs:jsStr];
        復制

        第二種是,使用Cordova 封裝好的對象CDVPluginResult和API。 使用這種方式時,在JS 調用原生功能時,必須設置執行成功的回調和執行失敗的回調。即設置cordova.exec(successCallback, failCallback, service, action, actionArgs)的第一個參數和第二個參數。像這樣:

        function locationClick() { 
            cordova.exec(setLocation,locationError,"HaleyPlugin","location",[]);
        }
        復制

        然后,Native 調用JS 的示例代碼:

        - (void)location:(CDVInvokedUrlCommand *)command
        {
            // 獲取定位信息......
            
            // 下一行代碼以后可以刪除
        //    NSString *locationStr = @"廣東省深圳市南山區學府路XXXX號";
            NSString *locationStr = @"錯誤信息";
            
        //    NSString *jsStr = [NSString stringWithFormat:@"setLocation('%@')",locationStr];
        //    [self.commandDelegate evalJs:jsStr];
            
            [self.commandDelegate runInBackground:^{
                CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:locationStr];
                [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
            }];
        }
        復制4.JS 調用Native 功能

        終于到重點了,JS想要調用原生代碼,如何操作呢?我用本地HTML 來演示。 首先,HTML中需要加載cordova.js,需要注意該js 文件的路徑,因為我的cordova.js與HTML放在同一個文件夾,所以src 是這樣寫:

        <script type="text/javascript" src="cordova.js"></script>
        復制

        然后,在HTML中創建幾個按鈕,以及實現按鈕的點擊事件,示例代碼如下:

        <input type="button" value="掃一掃" onclick="scanClick()" />
                <input type="button" value="獲取定位" onclick="locationClick()" />
                <input type="button" value="修改背景色" onclick="colorClick()" />
                <input type="button" value="分享" onclick="shareClick()" />
                <input type="button" value="支付" onclick="payClick()" />
                <input type="button" value="搖一搖" onclick="shake()" />
                <input type="button" value="播放聲音" onclick="playSound()" />
        復制

        點擊事件對應的關鍵的JS代碼示例:

        function scanClick() {
            cordova.exec(null,null,"HaleyPlugin","scan",[]);
        }
        
        function shareClick() {
            cordova.exec(null,null,"HaleyPlugin","share",['測試分享的標題','測試分享的內容','http://m.rblcmall.com/share/openShare.htm?share_uuid=shdfxdfdsfsdfs&share_url=http://m.rblcmall.com/store_index_32787.htm&imagePath=http://c.hiphotos.baidu.com/image/pic/item/f3d3572c11dfa9ec78e256df60d0f703908fc12e.jpg']);
        }
        
        function locationClick() {
            cordova.exec(setLocation,locationError,"HaleyPlugin","location",[]);
        }
        
        function locationError(error) {
            asyncAlert(error);
            document.getElementById("returnValue").value = error;
        }
        
        function setLocation(location) {
            asyncAlert(location);
            document.getElementById("returnValue").value = location;
        }
        復制

        JS 要調用原生,執行的是:

        // successCallback : 成功的回調方法
        // failCallback : 失敗的回調方法
        // server : 所要請求的服務名字,就是插件類的名字
        // action : 所要請求的服務具體操作,其實就是Native 的方法名,字符串。
        // actionArgs : 請求操作所帶的參數,這是個數組。
        cordova.exec(successCallback, failCallback, service, action, actionArgs);
        復制

        cordova,是cordova.js里定義的一個var結構體,里面有一些方法以及其他變量,關于exec ,可以看 iOSExec這個js 方法。 大致思想就是,在JS中定義一個數組和一個字典(鍵值對)。 數組中存放的就是:

        callbackId與服務、操作、參數的對應關系轉成json 存到上面全局數組中。
         var command = [callbackId, service, action, actionArgs];
        
            // Stringify and queue the command. We stringify to command now to
            // effectively clone the command arguments in case they are mutated before
            // the command is executed.
         commandQueue.push(JSON.stringify(command));
        復制

        而字典里存的是回調,當然回調也是與callbackId對應的,這里的callbackId與上面的callbackId是同一個:

        callbackId = service + cordova.callbackId++;
        cordova.callbacks[callbackId] =
                    {success:successCallback, fail:failCallback};
        復制

        ** iOSExec 里又是如何調用到原生方法的呢?** 依然是做一個假的URL 請求,然后在UIWebView的代理方法中攔截請求。 JS 方法iOSExec中會調用 另一個JS方法pokeNative,而這個pokeNative,看到他的代碼實現就會發現與UIWebView 開啟一個URL 的操作是一樣的:

        function pokeNative() {
            // CB-5488 - Don't attempt to create iframe before document.body is available.
            if (!document.body) {
                setTimeout(pokeNative);
                return;
            }
            
            // Check if they've removed it from the DOM, and put it back if so.
            if (execIframe && execIframe.contentWindow) {
                execIframe.contentWindow.location = 'gap://ready';
            } else {
                execIframe = document.createElement('iframe');
                execIframe.style.display = 'none';
                execIframe.src = 'gap://ready';
                document.body.appendChild(execIframe);
            }
            failSafeTimerId = setTimeout(function() {
                if (commandQueue.length) {
                    // CB-10106 - flush the queue on bridge change
                    if (!handleBridgeChange()) {
                        pokeNative();
                     }
                }
            }, 50); // Making this > 0 improves performance (marginally) in the normal case (where it doesn't fire).
        }
        復制

        看到這里,我們只需要去搜索一下攔截URL 的代理方法,然后驗證我們的想法接口。 我搜索webView:shouldStartLoadWIthRequest:navigationType方法,然后打上斷點,看如下的堆棧調用:

        關鍵代碼是這里,判斷url 的scheme 是否等于gap。

        if ([[url scheme] isEqualToString:@"gap"]) {
                [vc.commandQueue fetchCommandsFromJs];
                // The delegate is called asynchronously in this case, so we don't have to use
                // flushCommandQueueWithDelayedJs (setTimeout(0)) as we do with hash changes.
                [vc.commandQueue executePending];
                return NO;
            }
        復制

        fetchCommandsFromJs是調用js 中的nativeFetchMessages(),獲取commandQueue里的json 字符串;executePending中將json 字符串轉換為CDVInvokedUrlCommand對象,以及利用runtime,將js 里的服務和 方法,轉換對象,然后調用objc_msgSend 直接調用執行,這樣就進入了插件的對應的方法中了。

        這一套思想與WebViewJavascriptBridge的思想很相似。

        5. Native 調用 JS 方法

        這個非常簡單,如果是在控制器中,那么只需要像如下這樣既可:

        - (void)testClick
        {
            // 方式一:
            NSString *jsStr = @"asyncAlert('哈哈啊哈')";
            [self.commandDelegate evalJs:jsStr];
            
        }
        復制

        這里的evalJs內部調用的其實是UIWebView的stringByEvaluatingJavaScriptFromString方法。

        6.如果你在使用Xcode 8時,覺得控制臺里大量的打印很礙眼,可以這樣設置來去掉。

        首先:

        然后,添加一個環境變量:

        好了,到這里關于Cordova 的講解就結束了。

        示例工程的github地址:JS_OC_Cordova

        Have Fun!

        關聯標簽:
        日本漂亮人妻被强制

      3. <samp id="frm2q"><ins id="frm2q"></ins></samp>
          1. <samp id="frm2q"></samp>
            <progress id="frm2q"><bdo id="frm2q"><strong id="frm2q"></strong></bdo></progress>
            <menuitem id="frm2q"><ins id="frm2q"><u id="frm2q"></u></ins></menuitem>

            <progress id="frm2q"></progress>