用blazor(Wasm)开发了一个chrome插件感觉效率挺高的,分享给大家
先简单介绍下WebAssembly的原理:
“WebAssembly是一种用于基于堆栈的虚拟机的二进制指令格式”
如上图,浏览器在执行js时是会经历 Parser转成语法树->Compiler转成字节码->JIT即时字节码解释执行
因为WebAssembly 模块已经被编译成一种 JavaScript 字节码形式,现代支持 WebAssembly 的 JavaScript 引擎可以在其 JIT 组件中可以直接解释执行!
mono团队把开源跨平台.NET运行时Mono(也是unity3d的运行时)编译成了WebAssembly ,那么开发的.net程序就可以通过这个运行时在浏览器中加载net程序执行。
近日vs2022发布了,blazor的功能得到进一步提升,
支持AOT将.NET代码直接编译为WebAssembly字节码
支持NativeFileReference添加c语言和rust等原生依赖(手动狗头)
进入正题
开发浏览器插件,常见的就是按照插件的这几块api来进行扩展
右键菜单扩展
Backgroud(可以理解为每个插件都有一个后台一直运行的模块)
popup(浏览器右上角点击插件弹出的窗口模块)
contentScript(嵌入到你想要嵌入的网站内执行)
devtools(开发面板扩展模块)
首先基于这个大佬的模板搭建工程
https://github.com/mingyaulee/Blazor.BrowserExtension
基于模板的话会帮你引入哪些包
我也躺了很多坑,看看我给大佬提的issue,和大佬一起成长
这里我总结一套非常高效的方案给大家:
Backgroud用csharp写
popup,option等的html不要用balzor写,balzor加载html没有任何优势
contentScript用js写,内嵌到网站的,如果是balzor的话会初始化的时候卡1~2s左右,这个会严重影响体验
js和csharp交互
这里把BackGround(csharp开发)作为插件后端 html和js作为插件的前端的方式
右键菜单扩展
在BackGround里面写,包括响应事件
//选中跳转菜单
await WebExtensions.ContextMenus.Create(new WebExtensions.Net.Menus.CreateProperties
{Title = "测试菜单",Contexts = new List<ContextType>{ContextType.Selection},//data是选中的内容包装对象Onclick = async (data, tab) => { await test(data).ConfigureAwait(false); }
}, EmptyAction);
//非选中跳转菜单await WebExtensions.ContextMenus.Create(new WebExtensions.Net.Menus.CreateProperties
{Title = "跳转百度",Onclick = async (d, tab) => { await OpenUrl("https://www.baidu.com").ConfigureAwait(false); }
}, EmptyAction);
contentScript/popup等
用js写,有2种方式来和Backgroud通讯
1. 事件一来一回的方式
contentScript中发送消息给BackGround
chrome.runtime.sendMessage("消息体", function () { });
chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) {//处理backgroup发来的消息});
BackGround注册事件用来接收js发过来的消息
//注册事件接收js过来的消息
await WebExtensions.Runtime.OnMessage.AddListener(OnReceivedCommand);//处理事件
private bool OnReceivedCommand(object obj, MessageSender sender, Action action){Console.WriteLine("OnCommand:" + key + $",from TabId:{sender.Tab.Id}");//处理完成后发送事件给js那边await WebExtensions.Tabs.SendMessage(sender.Tab.Id.Value, "处理完成了", new SendMessageOptions());
}
2. 长连接方式
js端
var port = chrome.extension.connect({name: "test"
});port.onMessage.addListener(function (msg) {console.log(msg);
});$('#test').click(e => {port.postMessage('发消息');
});
csharp端
await WebExtensions.Runtime.OnConnect.AddListener(port =>
{Console.WriteLine(port.Name + "---》connection");port.OnMessage.AddListener(new DelegateMethod(async (msg) =>{//处理消息}));});
目前这种方式有一个需要优化,就是无法在csharp端主动推送消息给js端 给大佬提了issue了,相信很快可以fix https://github.com/mingyaulee/WebExtensions.Net/issues/14
配置/存储相关
有两种方法:
1. chrome.storage.local
这里我封装了一个类专门操作
public class ChromLocalStorage
{private readonly IWebExtensionsApi _webExtensionsApi;private readonly IJSRuntime _jsRuntime;public ChromLocalStorage(IWebExtensionsApi webExtensionsApi, IJSRuntime JsRuntime){_webExtensionsApi = webExtensionsApi;_jsRuntime = JsRuntime;}/// <summary>/// 调用chrom.storage.local set 把 key 和 value设置进去/// key返回/// </summary>/// <param name="value"></param>/// <param name="existKey"></param>/// <returns></returns>public async Task<string> localSet(string value,string existKey = null){var key = existKey ?? "key_" + DateTime.Now.ToString("yyyyMMddHHmmss");byte[] bytes = Encoding.UTF8.GetBytes(value);var encode = Convert.ToBase64String(bytes);var jss = "var " + key + " = {'" + key + "':'" + encode + "'}";await _jsRuntime.InvokeVoidAsync("eval", jss);object data2 = await _jsRuntime.InvokeAsync<object>("eval", key);await _jsRuntime.InvokeVoidAsync("chrome.storage.local.set", data2);Console.WriteLine($"call chrome.storage.local.set,key:{key},value:{value},base64Value:{encode}");return key;}public async Task<string> localSet<T>(T value){if (value is string s){return await localSet(s,null);}//转成jsonstringvar serialize = JsonSerializer.Serialize(value);return await localSet(serialize,null);}public async Task<T> localGet<T>(string key){var data = await localGet(key);T deserialize = JsonSerializer.Deserialize<T>(data);return deserialize;}public async Task<string> localGet(string key,bool remove=true){try{var local = await _webExtensionsApi.Storage.GetLocal();var getData = await local.Get(new StorageAreaGetKeys(key));var data = getData.ToString();if (string.IsNullOrEmpty(data)){return string.Empty;}var value = data.Split(new string[] { ":\"" }, StringSplitOptions.None)[1].Split(new string[] { "\"" }, StringSplitOptions.None)[0];var str = Convert.FromBase64String(value);var bastStr = Encoding.UTF8.GetString(str);//Console.WriteLine($"call chrome.storage.local.get,key:{key},value:{bastStr},base64Value:{value}");if (remove) await local.Remove(new StorageAreaRemoveKeys(key));return bastStr;}catch (Exception e){return "";}}public async Task localRemove(string key){var local = await _webExtensionsApi.Storage.GetLocal();await local.Remove(new StorageAreaRemoveKeys(key));}
}
2. 6.0推出的新技术:采用EFCore + Sqlite
需要用到native的库 https://github.com/SteveSandersonMS/BlazeOrbital/blob/main/BlazeOrbital/ManufacturingHub/Data/e_sqlite3.o
下载下来后放入工程中,然后引入
这里还有一个关键
https://github.com/SteveSandersonMS/BlazeOrbital/blob/main/BlazeOrbital/ManufacturingHub/wwwroot/dbstorage.js
下载这个js后放入工程中,这个js是将sqlite和本地的indexdb进行同步的
//EF的DbContext
public class ClientSideDbContext : DbContext
{//定义你要存储的表模型public DbSet<Part> Parts { get; set; } = default!;public ClientSideDbContext(DbContextOptions<ClientSideDbContext> options): base(options){}protected override void OnModelCreating(ModelBuilder modelBuilder){base.OnModelCreating(modelBuilder);//设置你的表的索引等modelBuilder.Entity<Part>().HasIndex(x => x.Id);modelBuilder.Entity<Part>().HasIndex(x => x.Name);modelBuilder.Entity<Part>().Property(x => x.Name).UseCollation("nocase");}
}//sqlite的初始化以及获取DBContext的方法封装
public class DataSynchronizer
{public const string SqliteDbFilename = "app.db";private readonly Task firstTimeSetupTask;private readonly IDbContextFactory<ClientSideDbContext> dbContextFactory;public DataSynchronizer(IJSRuntime js, IDbContextFactory<ClientSideDbContext> dbContextFactory){this.dbContextFactory = dbContextFactory;firstTimeSetupTask = FirstTimeSetupAsync(js);}public async Task<ClientSideDbContext> GetPreparedDbContextAsync(){await firstTimeSetupTask;return await dbContextFactory.CreateDbContextAsync();}private async Task FirstTimeSetupAsync(IJSRuntime js){//只加载一次 让sqlite和indexdb同步var module = await js.InvokeAsync<IJSObjectReference>("import", "./js/dbstorage.js");if (RuntimeInformation.IsOSPlatform(OSPlatform.Create("browser"))){await module.InvokeVoidAsync("synchronizeFileWithIndexedDb", SqliteDbFilename);}using var db = await dbContextFactory.CreateDbContextAsync();await db.Database.EnsureCreatedAsync();}}
在Program.cs进行注册
那么你就可以在Backgroud里面注入并在初始化方法中拿到db上下文
[Inject] public DataSynchronizer DataSynchronizer { get; set; }//db上下文
private ClientSideDbContext db;protected override async Task OnInitializedAsync()
{await base.OnInitializedAsync();db = await DataSynchronizer.GetPreparedDbContextAsync();
}
推荐用新的方式,EF写起来更爽更高效,拿到db上下文 就可以很简单的操作插件里面所有用到存储配置等!
这种方式比较适合了解.net生态的人,结合.net的一些库还可以实现很多好玩的功能
excel导出
二维码生成
ajax拦截,转发等
我是正东,开发chrome插件其实很简单,这种方式对于我来说比较高效 哈哈
欢迎白嫖 顺手点个赞吧!