zoukankan      html  css  js  c++  java
  • blazor wasm开发chrome插件

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

    先简单介绍下WebAssembly的原理:

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

    image
    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

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

    image
    image

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

    image image image

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

    1. Backgroud用csharp写
    2. popup,option等的html不要用balzor写,balzor加载html没有任何优势
    3. 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);
            }
    
            //转成jsonstring
    
            var 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

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

    image
    image

    这里还有一个关键

    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();
        }
    
    }
    
    image
    image

    在Program.cs进行注册 image

    那么你就可以在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拦截,转发等

    关注公众号一起学习


    如果您觉得阅读本文对您有帮助,请点一下“推荐”按钮,您的“推荐”将是我最大的写作动力!欢迎各位转载,转载文章之后须在文章页面明显位置给出作者和原文连接,谢谢。
  • 相关阅读:
    Selenium中解决输入法导致sendKeys输入内容与预期不一致的问题
    java代码中启动exe程序最简单的方法
    安装node.js
    安装MongoDB流程。
    阿里云RocketMQ定时/延迟消息队列实现
    Camunda工作流引擎简单入门
    因是子静坐养生汇编PDF下载-蒋维乔
    倪海厦天纪系列之天机道
    倪海厦天纪系列之地脉道
    张志顺老道长八部金刚功长寿功PDF下载
  • 原文地址:https://www.cnblogs.com/yudongdong/p/15557520.html
Copyright © 2011-2022 走看看