blazor wasm开发chrome插件

[删除(380066935@qq.com或微信通知)]

更好的阅读体验请查看原文:https://blog.csdn.net/sD7O95O/article/details/121368472

用blazor(Wasm)开发了一个chrome插件感觉效率挺高的,分享给大家

先简单介绍下WebAssembly的原理:

“WebAssembly是一种用于基于堆栈的虚拟机的二进制指令格式”

e9645237d0efc5285181313ca17e35d6.png

image

如上图,浏览器在执行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

基于模板的话会帮你引入哪些包

5185718b942265884f1312eeecff0ffe.png

image

我也躺了很多坑,看看我给大佬提的issue,和大佬一起成长

5c68eba64c03e79f2c12cafb7931eab2.pngf7c65fafce3cae83eb8e927dc386a90e.png738e6740f7eac8a66cbdfb7903e305b4.png

这里我总结一套非常高效的方案给大家:

  1. Backgroud用csharp写

  2. popup,option等的html不要用balzor写,balzor加载html没有任何优势

  3. contentScript用js写,内嵌到网站的,如果是balzor的话会初始化的时候卡1~2s左右,这个会严重影响体验

js和csharp交互

这里把BackGround(csharp开发)作为插件后端 html和js作为插件的前端的方式

右键菜单扩展

在BackGround里面写,包括响应事件

  1. //选中跳转菜单
  2. await WebExtensions.ContextMenus.Create(new WebExtensions.Net.Menus.CreateProperties
  3. {
  4.     Title = "测试菜单",
  5.     Contexts = new List<ContextType>
  6.     {
  7.         ContextType.Selection
  8.     },
  9.     //data是选中的内容包装对象
  10.     Onclick = async (data, tab) => { await test(data).ConfigureAwait(false); }
  11. }, EmptyAction);
  1. //非选中跳转菜单
  2.  await WebExtensions.ContextMenus.Create(new WebExtensions.Net.Menus.CreateProperties
  3. {
  4.     Title = "跳转百度",
  5.     Onclick = async (d, tab) => { await OpenUrl("https://www.baidu.com").ConfigureAwait(false); }
  6. }, EmptyAction);

contentScript/popup等

用js写,有2种方式来和Backgroud通讯

1. 事件一来一回的方式

contentScript中发送消息给BackGround

chrome.runtime.sendMessage("消息体", function () { });
  1. chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) {
  2.     //处理backgroup发来的消息
  3.     
  4. });

BackGround注册事件用来接收js发过来的消息

  1. //注册事件接收js过来的消息
  2. await WebExtensions.Runtime.OnMessage.AddListener(OnReceivedCommand);
  3. //处理事件
  4. private bool OnReceivedCommand(object obj, MessageSender sender, Action action){
  5.     
  6.    Console.WriteLine("OnCommand:" + key + $",from TabId:{sender.Tab.Id}");
  7.    
  8.    //处理完成后发送事件给js那边
  9.     await WebExtensions.Tabs.SendMessage(sender.Tab.Id.Value, "处理完成了"new SendMessageOptions());
  10. }

2. 长连接方式

js端

  1. var port = chrome.extension.connect({
  2.     name: "test"
  3. });
  4. port.onMessage.addListener(function (msg) {
  5.     console.log(msg);
  6. });
  7. $('#test').click(e => {
  8.     port.postMessage('发消息');
  9. });

csharp端

  1. await WebExtensions.Runtime.OnConnect.AddListener(port =>
  2. {
  3.     Console.WriteLine(port.Name + "---》connection");
  4.     port.OnMessage.AddListener(new DelegateMethod(async (msg) =>
  5.     {
  6.         //处理消息
  7.     }));
  8. });

目前这种方式有一个需要优化,就是无法在csharp端主动推送消息给js端 给大佬提了issue了,相信很快可以fix https://github.com/mingyaulee/WebExtensions.Net/issues/14

配置/存储相关

有两种方法:

1. chrome.storage.local

这里我封装了一个类专门操作

  1. public class ChromLocalStorage
  2. {
  3.     private readonly IWebExtensionsApi _webExtensionsApi;
  4.     private readonly IJSRuntime _jsRuntime;
  5.     public ChromLocalStorage(IWebExtensionsApi webExtensionsApi, IJSRuntime JsRuntime)
  6.     {
  7.         _webExtensionsApi = webExtensionsApi;
  8.         _jsRuntime = JsRuntime;
  9.     }
  10.     /// <summary>
  11.     /// 调用chrom.storage.local set 把 key 和 value设置进去
  12.     /// key返回
  13.     /// </summary>
  14.     /// <param name="value"></param>
  15.     /// <param name="existKey"></param>
  16.     /// <returns></returns>
  17.     public async Task<string> localSet(string value,string existKey  = null)
  18.     {
  19.         var key = existKey ?? "key_" + DateTime.Now.ToString("yyyyMMddHHmmss");
  20.         byte[] bytes = Encoding.UTF8.GetBytes(value);
  21.         var encode = Convert.ToBase64String(bytes);
  22.         var jss = "var " + key + " = {'" + key + "':'" + encode + "'}";
  23.         await _jsRuntime.InvokeVoidAsync("eval", jss);
  24.         object data2 = await _jsRuntime.InvokeAsync<object>("eval", key);
  25.         await _jsRuntime.InvokeVoidAsync("chrome.storage.local.set", data2);
  26.         Console.WriteLine($"call chrome.storage.local.set,key:{key},value:{value},base64Value:{encode}");
  27.         return key;
  28.     }
  29.     public async Task<string> localSet<T>(T value)
  30.     {
  31.         if (value is string s)
  32.         {
  33.             return await localSet(s,null);
  34.         }
  35.         //转成jsonstring
  36.         var serialize = JsonSerializer.Serialize(value);
  37.         return await localSet(serialize,null);
  38.     }
  39.     public async Task<T> localGet<T>(string key)
  40.     {
  41.         var data = await localGet(key);
  42.         T deserialize = JsonSerializer.Deserialize<T>(data);
  43.         return deserialize;
  44.     }
  45.     public async Task<string> localGet(string key,bool remove=true)
  46.     {
  47.         try
  48.         {
  49.             var local = await _webExtensionsApi.Storage.GetLocal();
  50.             var getData = await local.Get(new StorageAreaGetKeys(key));
  51.             var data = getData.ToString();
  52.             if (string.IsNullOrEmpty(data))
  53.             {
  54.                 return string.Empty;
  55.             }
  56.             var value = data.Split(new string[] { ":\"" }, StringSplitOptions.None)[1]
  57.                 .Split(new string[] { "\"" }, StringSplitOptions.None)[0];
  58.             var str = Convert.FromBase64String(value);
  59.             var bastStr = Encoding.UTF8.GetString(str);
  60.             //Console.WriteLine($"call chrome.storage.local.get,key:{key},value:{bastStr},base64Value:{value}");
  61.             if (remove) await local.Remove(new StorageAreaRemoveKeys(key));
  62.             return bastStr;
  63.         }
  64.         catch (Exception e)
  65.         {
  66.             return "";
  67.         }
  68.         
  69.     }
  70.     public async Task localRemove(string key)
  71.     {
  72.         var local = await _webExtensionsApi.Storage.GetLocal();
  73.         await local.Remove(new StorageAreaRemoveKeys(key));
  74.     }
  75. }

2. 6.0推出的新技术:采用EFCore + Sqlite

需要用到native的库 https://github.com/SteveSandersonMS/BlazeOrbital/blob/main/BlazeOrbital/ManufacturingHub/Data/e_sqlite3.o

下载下来后放入工程中,然后引入

8283d232060d588910f53c8416a4db9e.png

image

这里还有一个关键

https://github.com/SteveSandersonMS/BlazeOrbital/blob/main/BlazeOrbital/ManufacturingHub/wwwroot/dbstorage.js

下载这个js后放入工程中,这个js是将sqlite和本地的indexdb进行同步的

  1. //EF的DbContext
  2. public class ClientSideDbContext : DbContext
  3. {
  4.     //定义你要存储的表模型
  5.     public DbSet<Part> Parts { get; set; } = default!;
  6.     public ClientSideDbContext(DbContextOptions<ClientSideDbContext> options)
  7.         : base(options)
  8.     {
  9.     }
  10.     protected override void OnModelCreating(ModelBuilder modelBuilder)
  11.     {
  12.         base.OnModelCreating(modelBuilder);
  13.         //设置你的表的索引等
  14.         modelBuilder.Entity<Part>().HasIndex(x => x.Id);
  15.         modelBuilder.Entity<Part>().HasIndex(x => x.Name);
  16.         modelBuilder.Entity<Part>().Property(x => x.Name).UseCollation("nocase");
  17.     }
  18. }
  19. //sqlite的初始化以及获取DBContext的方法封装
  20. public class DataSynchronizer
  21. {
  22.     public const string SqliteDbFilename = "app.db";
  23.     private readonly Task firstTimeSetupTask;
  24.     private readonly IDbContextFactory<ClientSideDbContext> dbContextFactory;
  25.     public DataSynchronizer(IJSRuntime js, IDbContextFactory<ClientSideDbContext> dbContextFactory)
  26.     {
  27.         this.dbContextFactory = dbContextFactory;
  28.         firstTimeSetupTask = FirstTimeSetupAsync(js);
  29.     }
  30.     public async Task<ClientSideDbContext> GetPreparedDbContextAsync()
  31.     {
  32.         await firstTimeSetupTask;
  33.         return await dbContextFactory.CreateDbContextAsync();
  34.     }
  35.     private async Task FirstTimeSetupAsync(IJSRuntime js)
  36.     {
  37.         //只加载一次 让sqlite和indexdb同步
  38.         var module = await js.InvokeAsync<IJSObjectReference>("import""./js/dbstorage.js");
  39.         if (RuntimeInformation.IsOSPlatform(OSPlatform.Create("browser")))
  40.         {
  41.             await module.InvokeVoidAsync("synchronizeFileWithIndexedDb", SqliteDbFilename);
  42.         }
  43.         using var db = await dbContextFactory.CreateDbContextAsync();
  44.         await db.Database.EnsureCreatedAsync();
  45.     }
  46. }

0ecb4bb7f362c9ba6304b1a68f1fa503.png

image

在Program.cs进行注册9c527d069b5d2fd54038e56862755164.png

那么你就可以在Backgroud里面注入并在初始化方法中拿到db上下文

  1. [Inject] public DataSynchronizer DataSynchronizer { get; set; }
  2. //db上下文
  3. private ClientSideDbContext db;
  4. protected override async Task OnInitializedAsync()
  5. {
  6.     await base.OnInitializedAsync();
  7.     db = await DataSynchronizer.GetPreparedDbContextAsync();
  8. }

推荐用新的方式,EF写起来更爽更高效,拿到db上下文 就可以很简单的操作插件里面所有用到存储配置等!

这种方式比较适合了解.net生态的人,结合.net的一些库还可以实现很多好玩的功能

  • excel导出

  • 二维码生成

  • ajax拦截,转发等