从浏览器与桌面应用程序通信






4.84/5 (25投票s)
一个非常简单的应用程序,它使用文本转语音功能朗读您网页浏览器中当前选定的文本块。
引言
在许多情况下,将桌面应用程序与 Web 浏览器集成非常有用。考虑到如今大多数用户花费大部分时间使用他们选择的浏览器冲浪网络,为您的桌面应用程序提供某种集成是有意义的。通常,这就像提供一种将当前 URL 或选定文本块导出到您的应用程序的方法一样简单。对于本文,我创建了一个非常简单的应用程序,它使用文本转语音功能来朗读您网页浏览器中当前选定的文本块。
Internet Explorer 提供了许多集成应用程序逻辑到浏览器的挂钩,其中最受欢迎的是支持添加自定义工具栏。Code Project 上有许多关于如何做到这一点的精彩文章,例如这篇 Win32 文章和这篇 .NET 文章。您还可以使用 Firefox 的 Chrome 插件架构为 Firefox 创建工具栏,该架构使用 XML 进行 UI 布局,使用 JavaScript 进行应用程序逻辑(请参阅 CodeProject 文章)。
其他浏览器,如 Google Chrome、Safari 和 Opera 呢?考虑到所有浏览器没有通用的插件架构,您可以看到需要巨大的开发工作来为每种浏览器提供单独的工具栏实现。
能够编写一个可用于所有浏览器的工具栏就好了。这在今天是不可能的,但您可以使用 书签小工具 来达到几乎相同的效果。
书签小工具
书签小工具是一个特殊的 URL,单击后可以运行一个 JavaScript 应用程序。JavaScript 将在当前页面的上下文中执行。像任何其他 URL 一样,它可以被收藏并添加到您的收藏夹菜单或放置在收藏夹工具栏上。这是一个简单的书签小工具示例
javascript:alert('Hello World.');
这里有一个稍微复杂一点的示例,它将在消息框中显示当前选定的文本
<a href="javascript:var q = ''; if (window.getSelection)
q = window.getSelection().toString(); else if (document.getSelection)
q = document.getSelection(); else if (document.selection)
q = document.selection.createRange().text; if(q.length > 0)
alert(q); else alert('You must select some text first');">Show Selected Text</a>
在页面上选择一段文本并单击上面的链接。文本将在消息框中显示。
现在,将书签小工具拖放到您的收藏夹工具栏上(对于 IE,您需要右键单击,选择“添加到收藏夹”,然后在*收藏夹栏*中创建它)。导航到一个新页面,选择一段文本,然后单击*显示选定的文本*按钮。再次,选定的文本将显示在消息框中。
您可以看到书签小工具的潜力。您可以为应用程序的每个命令创建一个书签小工具,并将它们显示在网页上。然后,用户可以选择他们想要使用的命令,并将它们添加到他们的收藏夹(工具栏或菜单)。
缺点是安装起来比单个工具栏稍微麻烦一些,但优点是它为用户提供了很大的灵活性。他们只需要选择他们感兴趣的命令,就可以选择是否希望从工具栏或菜单中访问它们。
从开发者的角度来看,书签小工具非常棒,因为它们得到了所有主流浏览器的支持。您唯一需要担心的是确保您的 JavaScript 代码处理浏览器实现的差异,这在今天已经得到了很好的记录和理解(尽管仍然非常令人头疼)。
从 JavaScript 与桌面应用程序通信
书签小工具允许您在单击按钮时执行任意 JavaScript 代码块,但您如何使用它与桌面应用程序通信?
答案是在您的桌面应用程序中构建一个 Web 服务器,并使用 HTTP 请求从您的 JavaScript 代码发出命令。
现在,在您畏惧在应用程序中构建 Web 服务器的想法之前,实际上非常简单。您不需要完整的 Web 服务器实现。您只需要能够处理简单的 HTTP GET 请求。基本实现如下。
- 监听特定端口的套接字连接(80是 HTTP 协议的默认端口,但您应该为您的应用程序使用不同的端口)。
- 接受连接并读取 HTTP GET 请求。
- 从 GET 标头中提取 URL 并执行应用程序中的相关命令。
- 将 HTTP 响应发送回浏览器。
- 关闭连接。
- 回到 1。
.NET Framework 2.0 包含一个 `HttpListener` 类以及相关的 `HttpListenerRequest` 和 `HttpListenerResponse` 类,允许您用几行代码实现上述功能。
在浏览器端,您需要一种从 JavaScript 代码发出 HTTP 请求的方法。有许多方法可以从 JavaScript 发出 HTTP 请求。
最简单的方法是向 `document.location` 属性写入一个新的 URL。这会导致浏览器导航到新位置。但是,这不是我们想要的。我们不想在用户单击我们的书签小工具时将他们重定向到一个新页面。相反,我们只想在同一页面上向我们的应用程序发出命令。
这听起来像是 AJAX 和 `HttpXmlRequest` 的工作。AJAX 提供了一种方便的方法,可以在后台发出到 Web 服务器的请求,而不会影响当前页面。但是,`HttpXmlRequest` 有一个重要的限制,称为*同域源策略*。浏览器将 `HttpXmlRequest` 限制在为当前页面提供服务的域。例如,如果您正在查看来自 codeproject.com 的页面,您只能向 codeproject.com 发出 `HttpXmlRequest`。到另一个域(例如 google.com)的请求将被浏览器阻止。这是一项重要的安全措施,可确保恶意脚本无法在用户不知情的情况下在后台将信息发送到完全不同的服务器。
此限制意味着我们无法使用 `HttpXmlRequest` 与我们的桌面应用程序通信。请记住,JavaScript 书签小工具是在当前页面的上下文中执行的。我们需要能够从任何域(例如 codeproject.com)向我们的桌面应用程序发送请求,该应用程序将位于 localhost 域。
为了克服这个问题,我转向 Google 寻求灵感。Google 需要能够精确地做到这一点,才能收集网站的分析信息。如果您不熟悉 Google Analytics,它可用于收集有关您网站访问者的各种信息,例如访问者数量、他们来自哪里以及他们访问您网站的哪些页面。所有这些信息都会被收集、传输回 Google,并出现在您分析帐户的各种报告中。
要为您的网站添加跟踪,只需在您网站的每个页面底部添加对 Google Analytics JavaScript 的调用。
每当访客访问您的页面时,JavaScript 就会运行,访客详细信息就会被发送回您的 Google Analytics 帐户。
问题是 Google 怎么做到的?当然,他们不能使用 `HttpXmlRequest`,因为它会违反*同域源策略*?他们没有。相反,他们使用只能被描述为一种非常巧妙的技术。
使用 JavaScript Image 类进行异步跨域请求
JavaScript `Image` 类是一个非常简单的类,可用于异步加载图像。要请求图像,您只需将 `source` 属性设置为图像的 URL。如果图像加载成功,将调用 `onload()` 方法。如果发生错误,将调用 `onerror()` 方法。与 `HttpXmlRequest` 不同,*没有同域源策略*。源图像可以位于任何服务器上。它不需要托管在与当前页面相同的站点上。
如果我们意识到源 URL 可以包含任何信息,包括查询字符串,我们就可以利用这种行为向我们的桌面应用程序(或任何域)发送任意请求。唯一的要求是它必须返回一个图像。这是一个示例 URL
<a href="https://:60024/speaktext/dummy.gif?text=Hello">
https://:60024/speaktext/dummy.gif?text=Hello%20world</a>
我们可以轻松地将此 URL 映射到我们应用程序中的以下命令。
public void speaktext(string text);
为了确保请求无错误地完成,将返回一个 1x1 像素的 GIF 图像。此图像实际上从未显示给用户。使用微小的图像是为了最小化传输的字节数。
最重要的一点是 **所有通信都是单向的,从浏览器到桌面应用程序**。桌面应用程序无法将信息发送回浏览器。但是,对于许多应用程序来说,这不成问题。
Google 使用 JavaScript Image 技术将您的页面(托管在 yourdomain.com 上)的访问者信息发送回您的 Google Analytics 帐户(托管在 google.com 上)。
最大 URL 长度
您需要注意,URL 有一个最大长度,该长度因浏览器而异(大约 2K - **请检查**)。这限制了您在一次请求中可以发送的信息量。如果您需要发送大量信息,则需要将其分解成更小的块并发送多个请求。示例应用程序 BrowserSpeak 使用此技术来朗读任意长度的文本。
文本编码
JavaScript 将自动将您传递给 `Image.src` 的 URL 编码为 UTF-8。但是,当将任意文本作为 URL 的一部分传递时,您需要转义 `&` 和 `=` 字符。这些字符用于分隔在 URL 的查询字符串部分中传递的名称/值对(或参数)。这可以使用 JavaScript `escape()` 函数来完成。
避免缓存
Web 浏览器会将图像(以及许多其他资源)缓存在本地,以避免对同一资源多次请求服务器。这种行为对我们的应用程序来说是灾难性的。第一个命令会成功发送到我们的桌面应用程序,浏览器会将*dummy.gif*缓存在本地。后续的请求将永远无法到达我们的桌面应用程序,因为它们可以从本地缓存中得到满足。
这个问题有几种解决方案。一种方法是设置 HTTP 响应中的缓存过期指令,指示浏览器永远不要缓存结果。
另一种方法(BrowserSpeak 应用程序使用的方法)是确保每个请求都有一个唯一的 URL。这是通过附加包含当前日期和时间的时间戳来完成的。例如
var request = "http://" + server + "/" +
command + "/dummy.gif" + args +
"×tamp=" + new Date().getTime();
BrowserSpeak - 一个具体的例子
现在是时候将所有这些理论付诸实践,创建一个具有实际用途的示例应用程序。
BrowserSpeak 是一个 C# 应用程序,可以朗读网页上的文本。当您厌倦在屏幕上阅读大量文本时可以使用它。它使用 .NET Framework 3.0 中的 `System.Speech.Synthesis` 组件进行文本转语音功能。
BrowserSpeak 提供以下命令,可通过其 Web 界面和 UI 使用。
- SpeakText
- StopSpeaking
- PauseSpeaking
- ResumeSpeaking
它还提供了一个从 Web 界面可用的*BufferText*命令。此命令用于将文本块从 Web 浏览器发送到桌面应用程序。它将文本分成 1500 字节的块,因此不受 URL 最大大小的限制。它由*Speak Selected*书签小工具使用,用于在朗读之前将选定的文本传输到 BrowserSpeak 应用程序。
BrowserSpeak 使用以下书签小工具(*将这些拖到您的收藏夹栏上即可从您的浏览器中使用*)
“Speak Selected”命令最复杂,也最有趣。它列在下面
// A bookmarklet to send a speaktext command to the BrowserSpeak application.
var server = "localhost:60024";
// Change the port number for your app to something unique.
var maxreqlength = 1500;
// This is a conservative limit that should work with all browsers.
var selectedText = _getSelectedText();
if(selectedText)
{
_bufferText(escape(selectedText));
_speakText();
}
void 0;
// Return from bookmarklet, ensuring no result is displayed.
function _getSelectedText()
{
// Get the current text selection using
// a cross-browser compatible technique.
if (window.getSelection)
return window.getSelection().toString();
else if (document.getSelection)
return document.getSelection();
else if (document.selection)
return document.selection.createRange().text;
return null;
}
function _formatCommand(command, args)
{
// Add a timestamp to ensure the URL is always unique and hence
// will never be cached by the browser.
return "http://" + server + "/" + command +
"/dummy.gif" + args +
"×tamp=" + new Date().getTime();
}
function _speakText()
{
var image = new Image(1,1);
image.onerror = function() { _showerror(); };
image.src = _formatCommand("speaktext", "?source=" + document.URL);
}
function _bufferText(text)
{
var clearExisting = "true";
var reqs = Math.floor((text.length + maxreqlength - 1) / maxreqlength);
for(var i = 0; i < reqs; i++)
{
var start = i * maxreqlength;
var end = Math.min(text.length, start + maxreqlength);
var image = new Image(1,1);
image.onerror = function() _showerror(); };
image.src = _formatCommand("buffertext",
"?totalreqs=" + reqs + "&req=" + (i + 1) +
"&text=" + text.substring(start, end) +
"&clear=" + clearExisting);
clearExisting = "false";
}
}
function _showerror()
{
// Display the most likely reason for an error
alert("BrowserSpeak is not running. You must start BrowserSpeak first.");
}
大部分代码都是不言自明的。但是,解释 `_bufferText()` 循环的行为很重要。如果发送的文本大于 1500 字节,则会发出多个请求。请记住,就浏览器而言,它正在请求一个图像。现代浏览器会并行发出多个图像请求。这将导致多个*buffertext*命令并行发出。不仅如此,请求很有可能以乱序到达 BrowserSpeak 桌面应用程序。因此,每个请求都包含参数 `req`(请求编号)和 `totalreqs`(总请求数)。这允许 BrowserSpeak 应用程序将文本重新组合成正确的顺序。
管理书签小工具代码
书签小工具的代码必须格式化为单行。对于非常小的应用程序,这不是问题。但是,当您开始开发更大、更复杂的应用程序时,您将希望在多行代码中进行开发,并有足够的空格和注释。我发现使用 JavaScript 压缩工具,特别是免费的 YUI Compressor,是将一段普通的 JavaScript 转换为适合在书签小工具中使用的单行代码的绝佳方法。理想情况下,您应该将此步骤添加到您的自动化构建过程中。
C# 应用程序
主要的应用程序特定逻辑位于 `MainForm` 类中。首先,它在构造函数中启动一个 `HttpCommandDispatcher` 实例,该实例负责接收和分派 HTTP 命令(从书签小工具发送)。然后,`MainForm` 类监听各种事件并更新 UI 以反映其当前状态。
- 它监听按钮点击事件并发出相应的命令。
- 它监听 `SpeechController` 的状态更改,并相应地更新按钮状态(例如,在语音播放时启用“停止”按钮)。
- 它监听 `TextBuffer` 的更改(由 `BufferTextCommand` 更新)并在主窗体上的 `TextBox` 中反映其内容。
- 它监听 `HttpCommandDispatcher.RequestReceived` 事件,并在“请求”窗口中显示收到的请求。
`HttpCommandDispatcher` 使用 `System.Net` 中的 `HttpListener` 类监听 HTTP 请求。收到请求后,它从 URL 中提取命令,查找相应的 `HttpCommand`,然后调用 `HttpCommand.Execute()` 方法。它还将发送一个带有*dummy.gif*图像的响应(该图像已预加载并存储在 `byte[]` 数组中)。
关于文本编码和从 URL 提取参数。`HttpListenerRequest` 具有一个 `QueryString` 属性,该属性是一个名称/值集合,包含在 URL 的查询字符串部分中收到的参数。不幸的是,我发现您无法使用此属性,因为参数值未从其 UTF-8 编码正确解码。相反,我手动解析 `RawUrl` 属性,并在每个参数值上调用 `HttpUtility.DecodeUrl()` 方法。这可以正确处理我们从 JavaScript 收到的 UTF-8 编码字符串。
您可能会认出*命令*模式。您必须为希望通过 HTTP 接口公开的每个命令创建一个派生自 `HttpCommand` 的类。每个命令都必须使用 `HttpCommandDispatcher.AddCommand()` 方法添加。
提供了一个抽象的 `TextCommand`,供需要从浏览器接收大量文本的命令使用(例如 `SpeakTextCommand`)。`TextCommand` 会监听 `TextBuffer`,并在新文本到达时调用抽象的 `TextAvailable` 方法。派生类需要覆盖此方法,并在调用此方法时执行其操作。这处理了命令在所有命令操作的文本到达之前从浏览器到达的情况。
还有一个未在示例应用程序中实际使用的附加功能是 `ImageLocator` 类。此类将采用图像的 HTTP 请求,从应用程序的资源中查找相应的图像,并以请求的格式返回图像。例如,您可以使用以下 URL 查看“关于”按钮使用的图标
<a href="https://:60024/house.png">https://:60024/house.png</a>
上述类位于 `HttpServer` 命名空间中,并且在很大程度上与 BrowserSpeak 应用程序分离。您应该能够将它们提取出来并放入您自己的应用程序中,而无需更改。
HttpListener 和 Vista
如果您尝试在 Vista 上运行*BrowserSpeak*,当您尝试启动 `HttpListener` 时,您将收到一个*访问被拒绝*异常。Vista 不允许标准用户注册并监听特定 URL 的请求。您可以以*管理员*身份运行您的应用程序,但更好的方法是授予所有用户注册和监听您的应用程序 URL 的权限。这样,您就可以以标准用户身份运行您的应用程序。您可以使用*netsh*实用程序来实现这一点。要授予*BrowserSpeak*的 URL 用户权限,请以*管理员*身份运行命令提示符,然后执行以下命令。
netsh http add urlacl url=http://localhost:60024/ user=BUILTIN\Users listen=yes
此设置是持久的,并且会持续到重启。部署应用程序时,应在安装程序中执行此命令。
文本转语音
应用程序使用的文本转语音功能位于 `SpeechController` 类中。由于 .NET 3.0 中的 `System.Speech.Synthesis` 命名空间提供的功能,此类几乎什么都不做。它只是通过 Microsoft `SpeechSynthesizer` 类的一个实例进行委托。如果您想消除对 .NET 3.0 的依赖,可以重新实现 `SpeechController` 类,并使用 COM 互操作来访问本地 Microsoft Speech API (SAPI)。
最终想法
我希望我在本文中已经演示了书签小工具的强大功能,并为您提供了一些关于如何提供 Web 浏览器与桌面应用程序之间有用集成的想法。
有几件事我用这种技术未能令人满意地实现
- 似乎无法为书签小工具指定显示的工具提示。大多数浏览器只显示一部分 JavaScript 代码 - 对用户来说不是很有启发性。
- 似乎没有可靠的方法为书签小工具指定关联的图标。Safari 在其收藏夹栏上根本不使用图标。我希望其他浏览器至少会使用网站的 favicon。您甚至可以通过将每个书签小工具放在自己的页面上,并使用类似以下的 HTML 在页面标题中为每个命令指定唯一的 favicons 来使用不同的 favicon。
<link rel="shortcut icon" href="speaktext.ico" type="image/vnd.microsoft.icon"/>
不幸的是,浏览器似乎不使用 favicon 作为书签小工具。
我在我最新的商业文本转语音应用程序*Text2Go*(Text2Go)中使用了这种技术。我还使用这种技术的变体将 JavaScript 书签小工具的菜单添加到了*Internet Explorer 8 的加速器*预览窗口(Internet Explorer 8's Accelerator)中。