原文:Using JavaScript in Swift Projects: Building a Markdown to HTML Editor

作者:GABRIEL THEODOROPOULOS

譯者:kmyhy

一直想寫一篇文章,關於如何將 Swift 和 Javascript 結合在一起,以構建強大的支持富文本的 App。這並不是我們第一次聽人說要將 Javacript 代碼嵌入到 iOS 專案中了,但當你讀完本文后,你會感到這個過程會變得前所未有的簡單,仿佛魔術一般,你只需要做很少的工作。其中的奧妙就是一個叫做 JavaScriptCore framework 的框架。

你可能會想,为什麼總是有人愛用 JavaScript,為什麼不用 Swift 實現所有的功能?其實這也是我想問的,這裡我們陳述幾條理由:

  • 那些曾經寫過 web App 、已經忘記 Javascript 怎麼寫的 iOS 開發者,通過 JavaScriptCore 框架,就有機會再次使用他們所鐘愛的語言啦。
  • 對於某些任務,很可能已經有現成的 JavaScript 庫存在,它們和你要用 Swift 實現的功能其實並無區別。為什麼不使用現成的呢?
  • 很可能某些任務用 JavaScript 來做會更容易一點。
  • 你可能想遠程控制 App 的行為。可以將 JavaScript 程式碼放到服務器而不是 App bundle 里。這樣做時需要小心,因為這很可能會導致一場災難。
  • 可以讓你的 App 更具彈性和更加強大。
  • 你有強烈的好奇心,希望在你的 iOS 專案中使用 JavaScript。

當然,除此之外,你可能還想到了更好的在 iOS 使用 JavaScript 的理由。現在,你別忙著高興,讓我們看一下需要什麼必要的背景知識吧。首先,JavaScript 有它獨立的運行環境,或者更明確地說,它需要在虛擬機中運行。在 JavaScriptCore 框架中,用 JSVirtualMachine 類來代表虛擬機,當然通常你不會和它打交道。在一個 App 中可以運行多個虛擬機,它們之間無法直接交換數據。

其次,你使用得最多的其實是 jscontext。這個類相對於執行 JavaScript 腳本的真實環境(context)。在一個虛擬機(JSVirtualMachine)可以存在多個 context,你可以在 context 之間傳遞數據。如同你在後續內容中所看到, jscontext 會將 Swift 程式碼暴露給 JavaScript,將 JavaScript 程式碼暴露給 Swift。 我們會大量使用到它,但大部分用法都是相同的。

jscontext 中的所有值都是 JSValue 對象,JSValue 類用於表示任意類型的 JavaScript 值。如果你要從 Swift 中訪問 JavaScript 變數或函式,都可以用 JSValue 對象。當然也有將 JSValue 轉換成特定數據類型的方法。例如,轉換成字符串用 toString() 方法,轉換成字典用 toDictionary() 方法 (後面會看到)。在這裡有一個完整的方法列表。

我建議你閱讀官方的 JavaScriptCore 框架文檔。前面所說的這些可能會讓你對將要用到的工具有一個大概的了解,也有助你進一步理解後面的內容。

現在,讓我們正式開始。先來看一下今天的“菜譜”都有些什麼。

Demo 專案概覽

我們將通過一個簡單的示範專案來了解 JavaScriptCore 框架極其特性,這個專案演示了如何在 Swift 中使用 JavaScript。我們將使用經典 “Hello World” 示例(我最喜歡用的例子),它會把一個字符串值保存到 JavaScript 變數中。我們首先關心的是如何從 Swift 中訪問這個變數,我們不妨用 Xcode 控制台來將它打印出來。我們會連續做幾個簡單的例子,以逐步研究更多的特性。當然,我們不僅僅要學習如何從 JavaScript 專遞值給 Swift;我們也要研究反方向的傳遞。因此,我們既需要寫 Swift 代碼也要寫 JavaScript 代碼。但不用擔心,其實 JavaScript 並沒有那麼難打交道。一點也不難!注意,從這裡開始所有的輸出都在控制台中進行,這樣我們就可以將注意力放在真正值得注意的地方。

我們已經了解了足夠多的基礎知識了,我們可以來研究下如何在一種語言中使用另一種語言了。
為了更真實,我們先使用第三方 JavaScript 庫來試試。在專案的第二部分,我們會編寫一個 MarkDown/HTML 轉換器,或者說,我們會通過一個“轉換器的庫”來為我們干這個。我們的工作僅僅是從編輯框中(一個簡單的 UITextView)搜集用戶輸入的 MarkDown 文本,然後將它傳給 JavaScript 環境進行轉換,并將 JavaScript 環境返回的 HTML 顯示到一個 UIWebView 中。用一個按鈕來觸發轉換動作,并調用我們的程式碼。看下圖:

在第三部分和最後一部分,我們將演示如何傳遞帶屬性和方法的自定義類給 JavaScript Context。此外,我們還會在 JavaScript 中按照這個類的定義來創建一個對象并對其屬性進行賦值。我們最終會顯示一個 iPhone 從面世以來的設備類型列表(model 名),以及它們的最早和最晚的 OS 版本,以及它們的圖片。數據保存在一個 csv 檔案中,我們將用一個第三方庫進行解析。要獲得解析后的數據,我們將在 JavaScript 中使用我們的自定義 Swift 類,用這個類來渲染自定義對象的數據,然後將結果返回給 Swift。我們會用一個 TableView 來顯示這個列表。如下圖所示:

The above describe in general the three distinct tasks that will let us get to kNow the JavaScriptCore framework. As there are a lot of things wrapped up together in the package of one,we’ll have an initial menu screen that we’ll use to navigate to the proper part of the project:

為便於給你偷懶,我們提供了一個開始專案,你可以在這裡下載。當你下載完后,你就可以開始你的 JavaScriptCore 之旅了。在本文中,我們會做幾件事情,但最終會明白它們的大部分其實都是標準套路,為了實現最終目標,我們不得不重複這些套路而已。

開始出發吧!

從 Swift 中呼叫 JavaScript

就如介紹中所言,JavaScriptCore 中最主要的角色就是 jscontext 類。一個 jscontext 對象是位於 JavaScript 環境和本地 Javascript 腳本之間的橋樑。因此一開始我們就需要在 BasicsViewController 中宣告這個屬性。在 BasicsViewController.swift 檔案中,找到類的頭部,添加如下變數:

var jscontext: jscontext!

jscontext 對象必須是一個類屬性,如果你在方法體中初始化它為本地變數,那麼當方法一結束你就無法訪問到它了。

現在我們必須導入 JavaScriptCore 框架,在檔案頭部添加這句:

import JavaScriptCore

接下來要初始化 jscontext 對象,然後使用它。但在此之前,我們先寫點基本的 JavaScript 程式碼。我們將在一個 jssource.js 檔案中編寫它們,你可以在開始專案的專案導航器中找到這個檔案。我們會在裡面宣告一個 “Hello World” 的字符串變數,然後實現幾個簡單的函式,我們將通過 iOS 來訪問它們。如果你沒有學過 JavaScript 也沒關係,它們真的太簡單了,你一眼就能夠看懂。

打開 jssource.js 檔案,在開頭添加這個變數:

var helloWorld = "Hello World!"

在控制台中打印這個變數是我們接下來的第一目標!

回到 BasicsViewController.swift 檔案,創建一個方法來完成 2 個任務:

  1. 對我們早先宣告的 jscontext 屬性進行初始化。
  2. 加載 jssource.js 檔案,將檔案內容傳給 JavaScript 運行時,這樣它才能訪問檔案中編寫的程式碼。

BasicsViewController 中新建一個方法,初始化 jscontext 變數。方法非常簡單:

func initializeJS() {
    self.jscontext = jscontext()    

}

上面的第二條任務分成幾個步驟,但也非常簡單。我們先來看看一下源碼,然後在來進行討論:

func initializeJS() {
    ...

    // 指定 jssource.js 檔案路徑
    if let jsSourcePath = Bundle.main.path(forResource: "jssource",ofType: "js") {
        do {
            // 將檔案內容加載到 String 
            let jsSourceContents = try String(contentsOfFile: jsSourcePath)

            // 通過 jscontext 對象,將 jsSourceContents 中包含的腳本添加到 Javascript 運行時
            self.jscontext.evaluateScript(jsSourceContents)
        }
        catch {
            print(error.localizedDescription)
        }
    }    

}

源碼中的注釋很明白地解釋了它們的意思。首先,我們指定了 jssource.js 檔案路徑,然後加載檔案內容到 jsSourceContents 字符串中 (目前,這些內容就是你先前在 jssource.js 檔案中編寫的內容)。 如果成功,則接下來這句就重要了:我們用 jscontext 來“计算”这些 JavaScript 程式碼,通过这种方法我們可以立即將我們的 JS 程式碼傳遞到 JavaScript 環境。

接著增加一個全新的方法:

func helloWorld() {
    if let variableHelloWorld = self.jscontext.objectForKeyedSubscript("helloWorld") {
        print(variableHelloWorld.toString())
    }
}

這個方法雖然很簡單,但作用可不小。這個方法的核心部分是 objectForKeyedSubscript(_:) 一句,我們通過它來訪問 JavasScript 中的 hellowWorl 變量。第一條語句返回的是一個 JSValue對象(如果沒有值則返回為 nil),同時把它放到 variableHelloWorld 中保存。簡單說,這就完成了我們的第一個目標,因為我們在 Swift 中寫了一些 JavaScript,我們可以用任何方式來處理它!我們要怎樣處理這個保存著 “Hello World” 字符串的變量呢?把它輸出到控制台中而已。

現在,我們在 viewDidAppear(_:) 中呼叫這兩個新方法:

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)

    self.initializeJS()
    self.helloWorld()
}

運行 App,點擊第一個標題為 Basics 的按鈕。打開 Xcode 的控制台,我們的 “Hello World” 字樣被 JavaScriptCore 框架輸出到了控制台!

在混合使用 Swift 和 JavaScript 時,肯定不僅僅是為了定義幾個變量,然後打印它們的值。因此,讓我們來創建第一個 JavaScript 函式吧,讓我們來看看要如何使用它。

我找不到其他簡單的例子,因此使用下面這個函式,用於將姓和名組合成全名。在 jssource.js 檔案中加入:

function getFullname(firstname,lastname) {
    return firstname + " " + lastname;
}

人名中的姓和名分別被作為函式的兩個參數。保存檔案,返回 BasicsViewController.swift

在 Swift 中呼叫 JavaScript 函式有兩步:

首先,詢問 jscontext 要呼叫的函式名稱,這會返回一個 JSValue 對象,這和我們訪問 helloWorld 變量是一樣的。然後,通過方法名來呼叫這個函式,將它需要的參數傳入。一會你就明白了,現在先實現一個新方法:

func jsDemo1() {
    let firstname = "Mickey"
    let lastname = "Mouse"

    if let functionFullname = self.jscontext.objectForKeyedSubscript("getFullname") {

    }
}

現在,Swift 通過 functionFullname 引用了getFullname JS 函式。然後是第二步呼叫這個 JS 函式:

func jsDemo1() {
    let firstname = "Mickey"
    let lastname = "Mouse"

    if let functionFullname = self.jscontext.objectForKeyedSubscript("getFullname") {
        // Call the function that composes the fullname.
        if let fullname = functionFullname.call(withArguments: [firstname,lastname]) {
            print(fullname.toString())
        }
    }
}

call(withArguments:) 方法用於呼叫 getFullName 函式,并導致它的執行。call 方法只接收一個參數,這是一個任意對象類型的陣列,如果函式沒有參數,你可以傳遞一個 nil。在我們的例子中,我們傳遞了 firstname 和 lastname。這個方法的返回值也是一個 JSValue 對象,我們會將它打印到控制台中。在後面你會看到,方法的返回值對我們來說不一定是有意義的,因此我們也會不使用它。

現在,讓我們呼叫 jsDemo1() 方法:

override func viewDidAppear(_ animated: Bool) {
    ...

    self.jsDemo1()
}

運行項目,會在控制台中看到如下輸出:

這一點也不有趣,但你要明白你所看到的是在 Swift 中呼叫 JS 函式所得到的結果。同時,我們通過這部分內容可以總結出這樣一個固定流程:

  1. 構建一個 jscontext 對象。
  2. 裝載 JavaScript 程式碼,計算(evaluate)它的值 (或者說將它傳遞給 JavaScript 環境)。
  3. 通過 jscontextobjectForKeyedSubscript(_:) 方法訪問 JS 函式。
  4. 呼叫 JS 函式,處理返回值(可選)。

處理 JavaScript 異常

在開發中,編碼時現錯誤總是不可避免的,但錯誤出現必須讓開發者看到,這樣他們才會去解決它。如果進行 JS 和 Swift 混合編程,你怎麼知道應該去哪兒調試?Swift 還是 JS?在 Swift 中對錯誤進行輸出很容易,但我們能看到發生在 JS 端的錯誤嗎?

幸好,JavaScriptCore 框架提供了一個在 Swift 中捕捉 JS 環境中出現的異常的方法。觀察異常是一種標準程序,我們會在後面了解,但如何處理它們很顯然是一件很主觀的事情。

回到我們剛剛編寫的程式碼,我們來修改一下 initializeJS() 方法,以捕捉 JS 運行時異常。在這個方法中,在 jscontext 初始化之後,添加如下語句:

func initializeJS() {
    self.jscontext = jscontext()

    // Add an exception handler.
    self.jscontext.exceptionHandler = { context,exception in
        if let exc = exception {
            print("JS Exception:",exc.toString())
        }
    }

    ...
}

看到了吧,exceptionHandler 是一個閉包,每當 jscontext 發生一個錯誤時都會呼叫這個閉包。它有兩個參數:異常發生時所在的 context (即jscontext),以及異常本身。這個 exception 是一個 JSValue 對象。在這裡,我們為了簡單起見,僅僅將異常消息打印到控制台。

我們來試著製造一個異常,以測試這種方法是否行得通。為此,我們必須在 jssource.js 中編寫另一個 JS 函式,這個函式用一個整數陣列作為參數(整數和負數),返回一個包含了這個陣列中最大值、最小值和平均值的字典。

打開 jssource.js 檔案,添加函式:

function maxMinAverage(values) {
    var max = Math.max.apply(null,values);
    var min = Math.min.apply(null,values);
    var average = Math.average(values);

    return {
        "max": max,"min": min,"average": average
    };
}

代碼中的錯誤在於,在 Math 對象中根本沒有一個 average 函式,因此這句完全不對:

var average = Math.average(values);

假裝我們不知道這個情況,回到 BasicsViewController.swift,添加一個新方法:

func jsDemo2() {
    let values = [10,-5,22,14,-35,101,-55,16,14]

    if let functionMaxMinAverage = self.jscontext.objectForKeyedSubscript("maxMinAverage") {
        if let results = functionMaxMinAverage.call(withArguments: [values]) {
            if let resultsDict = results.toDictionary() {
                for (key,value) in resultsDict {
                    print(key,value)
                }
            }
        }
    }
}

首先,我們創建了一個隨機數字構成的陣列。我們用它作為調用 maxMinAverage 方法時的參數,這個方法在 Swift 中通過 functionMaxMinAverage 對象來引用。在呼叫 call 方法時,我們將這個陣列作為唯一參數傳遞。如果一切正常,我們會按照 Dictionary(注意 toDictionary() 方法)的方式來處理返回結果,將其中的值一一打印到控制台(maxMinAverage方法返回的是字典,因此我們同時打印了 key 和 value)

是時候測試一下了,但我們必須先呼叫這個 jsDemo2() 方法:

override func viewDidAppear(_ animated: Bool) {
    ...

    self.jsDemo2()
}

運行 App,我們期望打印出陣列的最大、最小和平均值。
但是,我們從 JS 運行時環境得到的上一個醜陋的、非常直白的異常:

JS Exception: TypeError: Math.average is not a function. (In 'Math.average(values)','Math.average' is undefined)

在解決這個有意製造的錯誤之前,讓我們先想一下這樣做的意義。試想,如果不能捕捉到 JS 異常,則你根本不可能找出錯誤真正的所在。為了節省我們的時間,尤其對於大型的複雜的 App 來說,錯誤並不是我們有意設計的,那麼兩眼一抹黑地去查找錯誤真的是一件讓人痛苦的事情。

因此,說教完之後,我們該來解決下問題了。在 jssource.js 檔案中,修改 code>minMaxAverage 函式為:

function maxMinAverage(values) {
    var max = Math.max.apply(null,values);

    var average = null;
    if (values.length > 0) {
        var sum = 0;
        for (var i=0; i 

在 Swift 專案中使用 Javascript:編寫一個將 Markdown 轉為 HTML 的編輯器的更多相关文章

  1. html5 拖拽及用 js 实现拖拽功能的示例代码

    这篇文章主要介绍了html5 拖拽及用 js 实现拖拽,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  2. 基于JavaScript编写一个图片转PDF转换器

    本文为大家介绍了一个简单的 JavaScript 项目,可以将图片转换为 PDF 文件。你可以从本地选择任何一张图片,只需点击一下即可将其转换为 PDF 文件,感兴趣的可以动手尝试一下

  3. HTML5数字输入仅接受整数的实现代码

    这篇文章主要介绍了HTML5数字输入仅接受整数的实现代码,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  4. amaze ui 的使用详细教程

    这篇文章主要介绍了amaze ui 的使用详细教程,本文通过多种方法给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  5. html+js 实现markdown编辑器效果

    这篇文章主要介绍了html+js 实现markdown编辑器效果,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

  6. html5简介_动力节点Java学院整理

    这篇文章主要介绍了html5简介,用于指定构建网页的元素,这些元素中的大多数都用于描述网页内容,有兴趣的可以了解一下

  7. ios – 如何将PhoneGap调试控制台与CLI集成?

    我需要在config.xml中添加任何内容吗?

  8. 无法使用xCode 4.4启动控制台应用程序

    我有一个包含两个目标的项目–一个iOS应用程序和一个OSX控制台应用程序.后者是使用XcodeFile->NewTarget并选择“CommandLineTool”创建的.此控制台应用程序用于准备iOS应用程序所需的默认数据库–使用CoreData.这一直很好,直到我升级到MountainLion和xCode4.4.现在,当我尝试运行命令行工具时,我收到“无法启动–权限被拒绝”错误.我试过玩签名证

  9. ios 8 Homescreen webapp,关闭和打开iPad停止javascript

    我有一个适用于iPad的全屏HTML5网络应用程序,并且刚刚安装了IOS8来试用它,它一切正常,直到你关闭并重新启动iPad.一旦web应用程序重新启动javascript就会停止并加载新页面不会重新启动它.在iPad上的Safari中打开同一页面时,关闭和打开iPad会继续按预期工作.其他人注意到了这个或想出了一个解决方案吗?解决方法这似乎是我在iOS8.1.1更新中解决的.

  10. iOS 6 javascript与object.defineProperty的间歇性问题

    当访问使用较新的Object.defineProperty语法定义属性的对象的属性时,有没有其他人注意到新iOS6javascript引擎中的间歇性错误/问题?https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/defineProperty我正在看到javascript失败的情况,说

随机推荐

  1. Swift UITextField,UITextView,UISegmentedControl,UISwitch

    下面我们通过一个demo来简单的实现下这些控件的功能.首先,我们拖将这几个控件拖到storyboard,并关联上相应的属性和动作.如图:关联上属性和动作后,看看实现的代码:

  2. swift UISlider,UIStepper

    我们用两个label来显示slider和stepper的值.再用张图片来显示改变stepper值的效果.首先,这三个控件需要全局变量声明如下然后,我们对所有的控件做个简单的布局:最后,当slider的值改变时,我们用一个label来显示值的变化,同样,用另一个label来显示stepper值的变化,并改变图片的大小:实现效果如下:

  3. preferredFontForTextStyle字体设置之更改

    即:

  4. Swift没有异常处理,遇到功能性错误怎么办?

    本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请发送邮件至dio@foxmail.com举报,一经查实,本站将立刻删除。

  5. 字典实战和UIKit初探

    ios中数组和字典的应用Applicationschedule类别子项类别名称优先级数据包contactsentertainment接触UIKit学习用Swift调用CocoaTouchimportUIKitletcolors=[]varbackView=UIView(frame:CGRectMake(0.0,0.0,320.0,CGFloat(colors.count*50)))backView

  6. swift语言IOS8开发战记21 Core Data2

    上一话中我们简单地介绍了一些coredata的基本知识,这一话我们通过编程来实现coredata的使用。还记得我们在coredata中定义的那个Model么,上面这段代码会加载这个Model。定义完方法之后,我们对coredata的准备都已经完成了。最后强调一点,coredata并不是数据库,它只是一个框架,协助我们进行数据库操作,它并不关心我们把数据存到哪里。

  7. swift语言IOS8开发战记22 Core Data3

    上一话我们定义了与coredata有关的变量和方法,做足了准备工作,这一话我们来试试能不能成功。首先打开上一话中生成的Info类,在其中引用头文件的地方添加一个@objc,不然后面会报错,我也不知道为什么。

  8. swift实战小程序1天气预报

    在有一定swift基础的情况下,让我们来做一些小程序练练手,今天来试试做一个简单地天气预报。然后在btnpressed方法中依旧增加loadWeather方法.在loadWeather方法中加上信息的显示语句:运行一下看看效果,如图:虽然显示出来了,但是我们的text是可编辑状态的,在storyboard中勾选Editable,再次运行:大功告成,而且现在每次单击按钮,就会重新请求天气情况,大家也来试试吧。

  9. 【iOS学习01】swift ? and !  的学习

    如果不初始化就会报错。

  10. swift语言IOS8开发战记23 Core Data4

    接着我们需要把我们的Rest类变成一个被coredata管理的类,点开Rest类,作如下修改:关键字@NSManaged的作用是与实体中对应的属性通信,BinaryData对应的类型是NSData,CoreData没有布尔属性,只能用0和1来区分。进行如下操作,输入类名:建立好之后因为我们之前写的代码有些地方并不适用于coredata,所以编译器会报错,现在来一一解决。

返回
顶部