blazor wasm開發chrome外掛

俞正東發表於2021-11-15

用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攔截,轉發等

關注公眾號一起學習

blazor wasm開發chrome外掛

相關文章