在 Swift App 中使用 JavaScript
在 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)
JSValue
JavaScriptCore 使用 Swift 中的类对来自 JavaScript 环境的值进行建模。它是一个包装器对象,您可以从中获取特定类型的值,如字符串和整数。
我们的案例研究剩下的就是将 a 转换output
为JSValue
aString
然后再转换为 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 依赖项管理器来更进一步。