摘要:資訊系統開發中難免會有要操作攝像頭、高拍儀、指紋儀等硬體外設,或者諸如獲取機器簽名、硬體授權保護(加密鎖)檢測等情況。受限於Web本身執行機制,就不得不使用
Active
、瀏覽器外掛進行能力擴充套件了。本文主要向大分享一種基於URL Scheme
的Web功能擴充套件方法,供大家參考。
一、方案對比
1.1 ActiveX
早期的IE瀏覽器擴充套件方法,可以使用VB6、C++、.Net等編寫,曾經使用VB6編寫過指紋儀操控,IE6時代還好配置,後面IE7、IE8出來後相容性就實在頭大了,其它瀏覽器出來後就更難於解決了,便再沒有使用過此方案了。缺點是對瀏覽器限制太多、相容性太差,難於部署及呼叫,且只支援IE。
1.2 Chrome擴充套件外掛
Chrome系瀏覽器的外掛擴充套件方法,由於對此不熟,沒有實際使用過,在此不作介紹。明顯的缺點便是隻支援Chrome系列。
1.3 自定義URL Scheme方案
此方案便是本文介紹的方案,方案大致過程是,Web頁面使用自定義URL協議驅動起協議程式
, 再由協議程式
拉起擴充套件功能WinForm應用程式
,Web頁通過HTTP
與擴充套件應用通訊,操控擴充套件功能,如下圖所示:
二、方案實現
2.1 協議程式
協議程式主要功能是:註冊、反註冊
URL Scheme
;協議呼叫時負責檢查擴充套件功能WinForm應用是否已啟動,如果沒有啟動則拉起擴充套件應用,否則直接退出無動作。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Web;
using System.Threading.Tasks;
using System.Diagnostics;
using System.Reflection;
using System.Windows.Forms;
using Microsoft.Win32;
namespace UrlHook
{
/// <summary>
/// 協議入口程式
/// </summary>
class Program
{
#region 私有成員
private const string PROTOCOL = "Hdc";
private const string URL_FILE = "origin.set";
#endregion
#region 入口點
/// <summary>
/// 應用程式的主入口點。
/// </summary>
[STAThread]
static void Main(params string[] args)
{
if (args.Length < 1)
return;
var first = args[0].Trim().ToLower();
var second = false;
if (args.Length >= 2)
second = args[1].Trim().ToLower() == "-q";
switch (first)
{
case "-i":
RegistrUrl(second);
return;
case "-u":
UnregistrUrl(second);
return;
}
try
{
if (Process.GetProcessesByName("HdcClient").Any())
{
return;
}
//啟動程式
Process p = new Process();
p.StartInfo.FileName = Assembly.GetExecutingAssembly().Location;
p.StartInfo.FileName = p.StartInfo.FileName.Substring(0, p.StartInfo.FileName.LastIndexOf("\\"));
p.StartInfo.WorkingDirectory = p.StartInfo.FileName;
p.StartInfo.FileName += "\\HdcClient.exe";
p.Start();
}
catch (Exception ex)
{
MessageBox.Show(ex.ToString(), "提示", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
#endregion
#region 私有方法
/// <summary>
/// 向登錄檔註冊URL地址
/// </summary>
private static void RegistrUrl(bool quiet = false)
{
var exePath = Assembly.GetExecutingAssembly().Location;
RegistryKey root = Registry.ClassesRoot.CreateSubKey(PROTOCOL);
root.SetValue(null, "Url:" + PROTOCOL);
root.SetValue("URL Protocol", exePath);
var deficon = root.CreateSubKey("DefaultIcon");
deficon.SetValue(null, exePath + ",1", RegistryValueKind.String);
var shell = root.CreateSubKey("shell")
.CreateSubKey("open")
.CreateSubKey("command");
shell.SetValue(null, "\"" + exePath + "\" \"%1\"");
shell.Close();
deficon.Close();
root.Close();
if (!quiet)
{
MessageBox.Show("恭喜,協義註冊成功;如果仍不生效,請償試重啟瀏覽器...", "提示"
, MessageBoxButtons.OK, MessageBoxIcon.Information);
}
}
/// <summary>
/// 解除協議註冊
/// </summary>
private static void UnregistrUrl(bool quiet = false)
{
RegistryKey root = Registry.ClassesRoot.OpenSubKey(PROTOCOL, true);
if (root != null)
{
root.DeleteSubKeyTree("shell", false);
Registry.ClassesRoot.DeleteSubKeyTree(PROTOCOL, false);
root.Close();
}
Registry.ClassesRoot.Close();
if (!quiet)
{
MessageBox.Show("協議解除成功,客戶端已經失效。", "提示"
, MessageBoxButtons.OK, MessageBoxIcon.Information);
}
}
#endregion
}
}
2.2 擴充套件應用整合Http Server
Web頁是通過
HTTP
協議與擴充套件WinForm應用通訊的,所以我們要在擴充套件應用中整合一個HTTP Server
,這裡我們採用的是.Net的OWIN庫中的Kestrel
,輕量、使用簡單,雖然在WinForm中只支援Web API
,但已經夠用了。這裡我們的監聽的是http://localhost:27089
,埠選擇儘量選5位數的埠,以免與客戶機的其它應用衝突。
//main.cs
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using System.Diagnostics;
using System.IO;
using Microsoft.Owin.Hosting;
using System.Reflection;
namespace HdcClient
{
static class Program
{
/// <summary>
/// 應用程式的主入口點。
/// </summary>
[STAThread]
static void Main()
{
//不能同時啟動多個
var query = from p in Process.GetProcesses()
where p.ProcessName == "HdcClient"
&& p.Id != Process.GetCurrentProcess().Id
select p;
if (query.Any())
{
IntPtr winHan = query.First().MainWindowHandle;
if (winHan.ToInt32() == 0 && File.Exists("win.hwd"))
{
winHan = (IntPtr)Convert.ToInt64(File.ReadAllText("win.hwd"), 16);
}
ShowWindow(winHan, 4);
SetForegroundWindow(winHan);
return;
}
//重定向路徑
var path = Assembly.GetExecutingAssembly().Location;
path = Path.GetDirectoryName(path);
Directory.SetCurrentDirectory(path);
//啟動服務通訊
WebApp.Start<Startup>("http://localhost:27089");
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new FrmMain());
}
}
}
//Startup.cs
using System.IO;
using System.Web.Http;
using Microsoft.Owin;
using Microsoft.Owin.FileSystems;
using Microsoft.Owin.StaticFiles;
using Owin;
namespace HdcClient
{
/// <summary>
///Web啟動程式
/// </summary>
public class Startup
{
/// <summary>
/// 配置各中介軟體
/// </summary>
/// <param name="appBuilder"></param>
public void Configuration(IAppBuilder appBuilder)
{
//配置API路由
HttpConfiguration config = new HttpConfiguration();
config.Routes.MapHttpRoute(
name: "MediaApi",
routeTemplate: "api/media/{action}/{key}",
defaults: new
{
controller = "media"
}
);
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new
{
id = RouteParameter.Optional
}
);
appBuilder.Use<Api.CorsOptionsMiddleware>();
appBuilder.UseWebApi(config);
}
}
}
2.3 關鍵問題跨域訪問
助手雖然使用
localhost
本地地址與Web頁通訊,但是像chrome
這樣嚴格檢測跨域訪問的瀏覽器,仍然會有跨域無法訪問擴充套件應用的問題。因此,HTTP Server
要開啟允許跨域訪問,我們定義一箇中介軟體來處理跨域,程式碼如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Owin;
namespace HdcClient.Api
{
/// <summary>
/// CORS時OPTIONS提交響應
/// </summary>
public class CorsOptionsMiddleware : OwinMiddleware
{
#region 構造方法
/// <summary>
/// 初始化中介軟體
/// </summary>
/// <param name="next"></param>
public CorsOptionsMiddleware(OwinMiddleware next)
:base(next)
{
}
#endregion
#region 重寫方法
/// <summary>
/// 響應跨域請求
/// </summary>
/// <param name="context">請求上下文</param>
/// <returns></returns>
public override Task Invoke(IOwinContext context)
{
if (context.Request.Method.ToUpper() != "OPTIONS")
return this.Next.Invoke(context);
var response = context.Response;
response.Headers.Append("Access-Control-Allow-Origin", "*");
response.Headers.Append("Access-Control-Allow-Methods", "*");
response.Headers.Append("Access-Control-Allow-Headers", "x-requested-with");
response.Headers.Append("Access-Control-Allow-Headers", "content-type");
response.Headers.Append("Access-Control-Allow-Headers", "content-length");
return Task.FromResult<string>("OK");
}
#endregion
}
}
三、Web頁如何呼叫?
3.1 調起助手
/*
* 硬體裝置控制訪問控制
*
* @Alphaair
* 20151114 create.
* 20190723 移值,更換AJAX庫。
**/
import http from "axios";
const hdc = {
VERSION: '2.0.0',
CLIENT_URL: 'http://localhost:27089/',
getRootUrl: function () {
///<summary>獲取當前訪問地址根URL</summary>
var url = location.protocol + '//';
url += location.host;
return url;
},
openClient: function (callback, count) {
///<summary>開啟客戶端元件</summary>
let url = `${this.CLIENT_URL}api/pipe/test`;
http.get(url, {
responseType: 'text'
}).then(rsp => {
//錯誤
if (rsp.stack)
throw rsp;
try {
if (callback)
callback();
}
catch (err) {
alert(err.message);
console.error(err);
}
}).catch(err => {
console.error(err);
if (count >= 10) {
alert("客戶端元件啟動失敗,請確認是否已經正常安裝或者償試手工啟動!");
return;
}
count = count || 1;
if (count < 3) {
let origin = this.getRootUrl();
origin = encodeURIComponent(origin);
window.open(`Hdc://startup/${origin}`);
}
//遞迴
setTimeout(function () {
count = count || 1;
count++;
hdc.openClient(callback, count);
}, 5000);
});
},
/**
* 啟動身份證讀取
*
* @param {Function} callback 讀取回撥
*
*/
readIdCard: function (callback) {
const self = this;
let url = `${self.CLIENT_URL}/api/IdReader/Reading`;
http.get(url, {
params: {
_ds: (new Date()).getTime()
}
}).then(rsp => {
let fkb = rsp.data;
if (fkb.Success) {
callback(fkb);
}
else {
alert(fkb.Message);
}
}).catch(err => {
console.error('身份證閱讀器啟動失敗,可能客戶端未開啟.', err);
callback(false);
});
},
/**
* 獲取身份證號碼掃描結果
*
* @param {Function} callback 讀取回撥
* @param {Boolean} isLoop 是否迴圈讀取
*/
getIdCard: function (callback, isLoop) {
//獲取身份證掃描結果
var self = this;
if (!isLoop)
self._cancelGetIdCard = false;
else
self._cancelGetIdCard = true;
let url = `${self.CLIENT_URL}/api/IdReader/GetIdCard`;
http.get(url, {
params: {
_ds: (new Date()).getTime()
}
}).then(rsp => {
let fkb = rsp.data;
if (fkb.Success) {
callback(fkb);
return;
}
//一秒後重新發起監聽
if (self._cancelGetIdCard) {
setTimeout(function () {
self.getIdCard(callback, true);
}, 1000);
}
}).catch(err => {
console.error('獲取身份證識別結果失敗,請確認客戶端正常.', err);
callback(false);
});
},
cancelIdCard: function () {
this._cancelGetIdCard = false;
}
};
export default hdc;
3.2 操作反饋結果獲取
像高拍儀拍攝這樣的操控需要一定時長,真實場景也無法確認使用者什麼時候操作完成,如果使用發起連線等待操作完成,勢必有可能引起因為HTTP超時而失敗。所以本方案採用操控發起與結果連線分離的方法,控制請求只負責調起相應的功能,不返回操控結果,結果採用輪詢
的方式獲取,如下面程式碼:
http.get(url, {
params: {
_ds: (new Date()).getTime()
}
}).then(rsp => {
...
//一秒後重新發起監聽
if (self._cancelGetIdCard) {
setTimeout(function () {
self.getIdCard(callback, true);
}, 1000);
}
}).catch(err => {
...
});
四、實現效果
受篇幅限制,這裡僅展示部分關鍵程式碼,如果有需要了解方案更詳細資訊,請按下面方式聯絡我們。