跳到主要内容

在 Swift App 中使用 JavaScript

· 阅读需 10 分钟
GoSwiftUI
goswiftui.com

在 iOS 应用程序中,使用 C、C++、Objective-C、Objective-C++ 或 JavaScript 编写的代码在技术上相当容易。在本文中,我们将了解如何使用 JavaScriptCore 从 Swift 调用 JavaScript 代码。例如,我将通过向我的iOS 阅读应用程序添加 JavaScript 依赖项来从 URL 中删除跟踪参数的步骤。

如果您正在使用 Swift 编写 iOS 应用程序并尝试解决您确定以前已经解决的问题,您可能会寻找解决该问题的现有代码。您可能首先会想到寻找用 Swift 编写的开源代码,您可以使用 Swift Package Manager 将其集成到您的项目中。例如,通过搜索Swift Package Index。然而,我们不需要将自己局限于 Swift。

在 iOS 应用程序中,使用 C、C++、Objective-C、Objective-C++ 或 JavaScript 编写的代码在技术上相当容易。在本文中,我们将了解如何使用 JavaScriptCore 从 Swift 调用 JavaScript 代码。例如,我将通过向我的iOS 阅读应用程序添加 JavaScript 依赖项来从 URL 中删除跟踪参数的步骤。

我不是在谈论像 React Native 这样的混合技术,它可以让你用 JavaScript 编写应用程序的 UI。这是关于逻辑中的特定组件——甚至是单个功能。

案例研究:从 JavaScript 中的 URL 中删除跟踪参数​

我制作了一个阅读应用程序,让用户可以保存网页以供日后阅读。某些网页链接在共享时添加了跟踪参数,这些参数不会为用户提供任何价值。我决定如果我的应用程序在保存文章时透明地删除这些跟踪参数,那对用户来说会很好。

例如这个网址:

https://example.com/something?utm_source=whatever

会变成:

https://example.com/something

通常,查询参数对于坚持很重要,因为它们会影响页面内容,因此解决此问题的主要挑战是了解哪些参数可能用于跟踪。当有开源选项时,社区已经投资于保持最新的跟踪参数列表,我不想自己组装一个常用跟踪参数列表。

引起我注意的一个选项是Chris Newhouse 的 URL Tracking Stripper Chrome 扩展程序。在这个项目中,我可以看到 URL 修改是由文件执行的trackers.js。

特别是,这个文件定义了一个ALL_TRACKERS常量和这个函数:

function removeTrackersFromUrl(url, trackers) {
...
}

这似乎是解决我的问题的可行方法。

整合依赖​

如果您有很多 JavaScript 依赖项,而这些依赖项又有自己的依赖项,那么使用 JavaScript 依赖项管理器(例如npm )可能是个好主意。在我的例子中,我没有很多 JavaScript 依赖项,所以我只是将这个存储库添加为 Git 子模块。我将跳过这方面的细节。

接下来我添加trackers.js到我的 Xcode 项目并将其添加为 Xcode 中我的目标的资源。Swift 代码被编译成机器代码。另一方面,JavaScript 是一种解释型语言,源代码完全按照您所见在应用程序包中交付。

创建 JavaScript 环境​

为了从我们的应用程序运行 JavaScript,我们使用了 Apple 的JavaScriptCore框架。

import JavaScriptCore

JavaScriptCore 提供了执行 JavaScript 的环境。没有网页、UI 或文档对象模型 (DOM)。

JavaScript 环境与我们的 Swift 代码运行的环境是分开的,因此我们需要在 Swift 和 JavaScript 环境之间明确地移动数据。JSContext事实上,通过从我们的 Swift 代码创建类的实例,您可以拥有任意多的 JavaScript 环境。

加载 JavaScript 代码​

首先,我们需要加载trackers.js到我们的 JavaScript 环境中:

let context = JSContext()!
let scriptURL = Bundle.main.url(forResource: "trackers", withExtension: "js")!
let script = try! String(contentsOf: scriptURL)
context.evaluateScript(script)

这将使removeTrackersFromUrl可用并加载一些常量,例如ALL_TRACKERS.

您可能已经注意到我强制解包了对JSContext. 大多数具有 Objective-C API 的 Apple 框架都通过添加可空性注释和其他改进很好地桥接到 Swift。JavaScriptCore 还没有这样做,所以它的许多 API 使用隐式展开的可选值桥接到 Swift ,我们真的不知道这些 API 是否可能返回 nil。在这种情况下,我不知道创建 aJSContext可能会失败的任何原因,所以我冒了风险,但更优雅地处理 nil 可能是明智的。

从 Swift 调用 JavaScript 函数​

现在我们可以removeTrackersFromUrl像这样在 JavaScript 环境中运行该函数:

let output = context.evaluateScript("removeTrackersFromUrl('https://example.com/something?utm_source=whatever', ALL_TRACKERS)")

由于此脚本直接调用函数,evaluateScript(_:) 因此会将 JavaScript 函数的输出作为 .swift 文件返回到 Swift 环境中JSValue。

第二个参数很容易传入,因为我们使用了一个已经在 JavaScript 环境中定义的常量。

将值从 Swift 动态传递到 JavaScript​

对于 JavaScript 函数的第一个参数,我们需要传入 URL。该值需要来自 Swift 环境。

我们可以像这样使用字符串插值:

let output = context.evaluateScript("removeTrackersFromUrl('\(inputURL.absoluteString)', ALL_TRACKERS)")

这种代码会引发代码注入安全漏洞。相反,我们可以将输入 URL 设置为 JavaScript 环境中的变量,然后通过名称引用它。

JSContext让我们objectForKeyedSubscript(_:)使用setObject(_:forKeyedSubscript). 奇怪的是,这个 API 更适合在 Objective-C 中使用,因为它们映射到下标语法,因此您可以像在字典中一样读取和设置值。下标语法在这里似乎在 Swift 中不起作用。

这是使用变量设置输入 URL:

// The prefix is very defensive to avoid overwriting a variable name already in use.
let inputName = "dh_inputURL" as NSString
context.setObject(inputURL.absoluteString, forKeyedSubscript: inputName)
let output = context.evaluateScript("removeTrackersFromUrl(\(inputName), ALL_TRACKERS)")
// Not strictly necessary, but we could clean up:
context.setObject(nil, forKeyedSubscript: inputName)

JSValueJavaScriptCore 使用 Swift 中的类对来自 JavaScript 环境的值进行建模。它是一个包装器对象,您可以从中获取特定类型的值,如字符串和整数。

我们的案例研究剩下的就是将 a 转换output为JSValueaString然后再转换为 a URL:

if let outputString = output?.toString(), let outputURL = URL(string: outputString) {
return outputURL
} else {
// The output is missing or invalid for some reason. Handle error.
}

结论​

从 Swift 代码调用 JavaScript 很容易,尽管这并非没有摩擦。互操作性远不及 Swift 和 Objective-C 之间的互操作性。同样明显的是,JavaScriptCore API 是为 Objective-C 设计的,并没有针对 Swift 进行适当的改进。也就是说,最后,我宁愿有一个更强大的问题解决方案,而不管用于实现该解决方案的编程语言如何,即使这意味着更多的摩擦。

通过使用更广泛的编程语言搜索开源解决方案,我们扩大了搜索范围,因此更有可能找到解决我们问题的现有解决方案。对于 JavaScript 尤其如此,因为 Web 开发人员比 iOS 开发人员多得多。

我在这里使用的例子很简单。trackers.js手动将整个文件从 JavaScript 翻译成 Swift 不会花很长时间。然而,直接使用开源代码的好处是,如果跟踪器列表随时间变化,我可以更新我的项目以仅通过更新子模块来使用它。您可以通过使用 JavaScript 依赖项管理器来更进一步。