Blazor作為一個新興的互動式 Web UI 的框架,有其自身的優缺點,如果現有的 JavaScript 元件能移植到 Blazor,無疑讓 Blazor 如虎添翼,本文就介紹一下自己在開發 BulmaRazor 元件庫的時,封裝現有的 JavaScript 元件的方法,文中以 TuiEditor 為例。
開始
首先找到現有 TuiEditor 的主頁或者文件,這一步很簡單,我們找到官網 https://ui.toast.com/tui-editor/ ,分析一下元件的使用方法,一般都是有樣式檔案,有 JavaScript 檔案,有一個 options 物件來初始化一個主物件,主物件上有方法和事件,大概就是這些了,我們先下載所需的檔案,然後一步一步處理。
樣式部分
該元件需要兩個樣式 codemirror.min.css 和 toastui-editor.min.css ,由於一個元件庫不只這一個元件,為了引用方便,我們需要使用 BuildBundlerMinifier 合併檔案,不知道 BuildBundlerMinifier 的同學網上查一下。
在網站的根目錄需要有 BuildBundlerMinifier 所需的配置檔案 bundleconfig.json,對應的配置如下 :
{
"outputFileName": "wwwroot/bulmarazor.min.css",
"inputFiles": [
"wwwroot/css/tuieditor/codemirror.min.css",
"wwwroot/css/tuieditor/toastui-editor.min.css"
]
},
專案中很可能還有其他的樣式檔案,一起合併就好了,引用的時候我們只需要一個樣式檔案,這裡就是 bulmarazor.min.css。
指令碼部分
tuieditor 的 JavaScript 檔案只有一個,當然一般 JavaScript 元件的指令碼檔案都是一個,如果是普通的 web 開發的話直接引入就可以了,但是在 Blazor 中有些麻煩,需要使用 JavaScript 互操作,互操作是指 C# 程式碼可呼叫到 JavaScript 程式碼,而 JavaScript 程式碼也可呼叫到 C# 程式碼。
C# 呼叫 JavaScript 程式碼有兩種方法,一種是使用 IJSRuntime 呼叫掛載到 window 物件上的方法,另一種是使用模組隔離的方式呼叫,這裡我們需要模組隔離,因為有以下優點:
- 匯入的 JavaScript 不再汙染全域性名稱空間。
- 庫和元件的使用者不需要引用相關的 JavaScript。
關於 JavaScript 模組,可以參考這裡 這裡 ,使用 JavaScript 模組依賴於 import 和 export,而一般的 JavaScript 類庫並不支援,所以我們需要些一些匯出的程式碼,檔案結構如下:
我們忽視紅色標註,先來看一下 toastui-editor-export.js 這個檔案:
export function initEditor(options) {
options.el = document.getElementById(options.elid);
let editor = new toastui.Editor.factory(options);
return editor;
}
toastui-editor-all.min.JavaScript 這個檔案就是 JavaScript 元件檔案,我們不用去改它,也不應該去改它,因為後續升級了我們可以直接覆蓋的,toastui-editor-export.js 就是我們專門寫的一個匯出類庫中所需功能的匯出檔案。為了引用方便我們還是需要合併一下,就是圖片示現的那樣,合併配置如下:
{
"outputFileName": "wwwroot/js/tuieditor.min.js",
"inputFiles": [
"wwwroot/jsplugin/tuieditor/toastui-editor-all.min.js",
"wwwroot/jsplugin/tuieditor/toastui-editor-export.js"
]
}
現在我們使用隔離的方式引用 wwwroot/js/tuieditor.min.js 就可以了。當我們新建一個Razor元件專案的時候,會帶有呼叫的例子,我們比貓畫虎搞定:
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
namespace BulmaRazor.Components
{
public class BulmaRazorJsInterop : IAsyncDisposable
{
private readonly Lazy<Task<IJSObjectReference>> tuiEditorModuleTask;
public BulmaRazorJsInterop(IJSRuntime jsRuntime)
{
tuiEditorModuleTask = new(() => jsRuntime.InvokeAsync<IJSObjectReference>(
"import", "./_content/BulmaRazor/js/tuieditor.min.js").AsTask());
}
public async ValueTask<IJSObjectReference> TuiEditorInit(TuiEditorOptions options)
{
var module = await tuiEditorModuleTask.Value;
return await module.InvokeAsync<IJSObjectReference>("initEditor", options.ToParams());
}
public async ValueTask DisposeAsync()
{
if (tuiEditorModuleTask.IsValueCreated)
{
var module = await tuiEditorModuleTask.Value;
await module.DisposeAsync();
}
}
}
}
Blazor 元件部分
元件檔案是 TuiEditor.razor,UI程式碼是非常簡單的,就是一個帶有 id 屬性的 div 容器,id 很重要,是我們互操作的基礎,這裡我們使用GUID生成唯一的id。
我們需要在 blazor 元件呈現之後呼叫 JavaScript 程式碼來初始化我們的 JavaScript 元件,呼叫 JavaScript 程式碼之後返回了js 物件的引用editor,注意editor和上述 var module = await tuiEditorModuleTask.Value;
中的 module 是一樣的,都是 JavaScript 物件引用。大致的程式碼如下:
@inject BulmaRazorJsInterop JsInterop
<div id="@Id"></div>
@code {
readonly string Id = "tuiEditor_" + Guid.NewGuid().ToString("N");
IJSObjectReference editor;
[Parameter]
public TuiEditorOptions Options { get; set; }
protected override void OnInitialized()
{
if (Options == null)
Options = new TuiEditorOptions();
Options.elid = Id;
base.OnInitialized();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
editor = await JsInterop.TuiEditorInit(Options);
}
}
Options選項部分
TuiEditor 元件中有個引數 TuiEditorOptions ,是要對應 JavaScript 中的 options 引數的,我們需要自己定義一個,這裡我們使用兩個類來使用,一個是針對 JavaScript 的 JsParams 類似字典的物件,一個是針對使用者的 TuiEditorOptions 。
JsParams 就是一個Dictionary<string,object>,為了方便,我們過濾了空值:
internal class JsParams:Dictionary<string,object>
{
public void AddNotNull(string key, object value)
{
if (value != null)
{
base.Add(key,value);
}
}
}
TuiEditorOptions 類除了引數之外,包含一個 ToParams() 的方法把自己轉換成 JsParams:
public class TuiEditorOptions
{
internal string elid { get; set; }
/// <summary>
/// Editor's height style value. Height is applied as border-box ex) '300px', '100%', 'auto'
/// </summary>
public string Height { get; set; }
/// <summary>
/// 是否是檢視器
/// </summary>
public bool? Viewer { get; set; }
//...其他引數
internal JsParams ToParams()
{
JsParams ps = new JsParams();
var def = BulmaRazorOptions.DefaultOptions.TuiEditorOptions;
ps.AddNotNull("elid", elid);
ps.AddNotNull("viewer",Viewer);
ps.AddNotNull("height", Height ?? def.Height);
//...其他引數
return ps;
}
}
有幾個原因使用 JsParams :
- null值可以不傳遞,因為js的options一般都用預設值,減少傳輸;
- 可以使用預設設定,如上有個BulmaRazorOptions.DefaultOptions.TuiEditorOptions;
- 可以靈活的手動處理引數,上面例子沒有提現出來,不過元件寫多了肯定會遇到這種情況;
物件的方法
JavaScript 元件一般也會公開許多例項方法,比如獲得焦點,設定內容,獲取內容等等,在在前面我們一直儲存了 JavaScript 元件例項的引用,也就是在 TuiEditor 中的 editor 物件,向公開哪些方法在 TuiEditor.razor 中新增就是了:
public void Focus()
{
editor?.InvokeVoidAsync("focus");
}
public ValueTask<string> GetMarkdown()
{
return editor?.InvokeAsync<string>("getMarkdown") ?? new ValueTask<string>("");
}
public void InsertText(string text)
{
editor?.InvokeVoidAsync("insertText", text);
}
public ValueTask<bool> IsViewer()
{
return editor?.InvokeAsync<bool>("isViewer") ?? new ValueTask<bool>(false);
}
//...其他需要的方法
物件事件
JavaScript 元件物件有自己的事件,在 JavaScript 中直接設定 JavaScript 函式就可以了,但是並不能把 C# 方法或者委託傳遞給 js,這裡就需要用到 JavaScript 呼叫C#方法了。
Blazor 框架中 JavaScript 只能呼叫靜態方法,而我們實際中是基於物件來寫邏輯的,所有我專門寫了一個類來處理js的呼叫,JSCallbackManager:
public static class JSCallbackManager
{
private static ConcurrentDictionary<string, Dictionary<string, Delegate>> eventHandlerDict = new();
public static void AddEventHandler(string objId, string eventKey, Delegate @delegate)
{
var eventHandlerList = eventHandlerDict.GetOrAdd(objId, (key) => new Dictionary<string, Delegate>());
eventHandlerList[eventKey]= @delegate;
}
public static void DisposeObject(string objId)
{
if (eventHandlerDict.Remove(objId, out Dictionary<string, Delegate> handlers))
{
handlers.Clear();
}
}
[JSInvokable]
public static object JSCallback(string objId, string eventKey)
{
if (eventHandlerDict.TryGetValue(objId, out Dictionary<string, Delegate> handlers))
{
if (handlers.TryGetValue(eventKey, out Delegate d))
{
var obj = d.DynamicInvoke();
return obj;
}
}
return null;
}
}
我們使用一個巢狀的字典來儲存了Blazor元件的回撥委託,每一個元件物件都有一個唯一的Id,每一個元件型別都可以有不同名稱的 JavaScript 事件回撥。
比如我們想訂閱 JavaScript 元件例項的 load 事件,我們需要改兩個地方,第一個是 toastui-editor-export.js 匯出檔案:
export function initEditor(options) {
options.el = document.getElementById(options.elid);
options.events = {
load: function () {
DotNet.invokeMethodAsync("BulmaRazor", "JSCallback", options.elid, "load");
}
}
let editor = new toastui.Editor.factory(options);
return editor;
}
JavaScript 的事件還是需要用 js來做,然後在js方法內部呼叫 C# 方法。第二個是需要在 TuiEditor 中新增回撥委託:
[Parameter]
public EventCallback<TuiEditor> OnLoad { get; set; }
protected override void OnInitialized()
{
if (Options == null)
Options = new TuiEditorOptions();
Options.elid = Id;
//這裡新增回撥委託,並把js事件公開成了Blazor元件事件
JSCallbackManager.AddEventHandler(Id, "load", new Func<Task>(() => OnLoad.InvokeAsync(this)));
base.OnInitialized();
}
protected override ValueTask DisposeAsync(bool disposing)
{
//移除物件的所有回撥委託
JSCallbackManager.DisposeObject(Id);
return base.DisposeAsync(disposing);
}
這樣我們就把 JavaScript 元件事件移植到了 Blazor 元件。
修整
經過上述不知,元件基本移植完了,但還不能很好的使用,第一,因為介面是 js在操作,所以我們應該禁用 Blazor元件的渲染:
protected override bool ShouldRender()
{
return false;
}
在js的options中有個initialValue屬性,是初始化內容的,我們改成Blazor的形式,最好是可以繫結:
[Parameter]
public EventCallback<TuiEditor> OnBlur { get; set; }
protected override void OnInitialized()
{
if (Options == null)
Options = new TuiEditorOptions();
Options.InitialValue = _value;
Options.elid = Id;
//這裡也是通過js事件觸發
JSCallbackManager.AddEventHandler(Id, "blur", new Func<Task>(async () =>
{
await setValue();
await OnBlur.InvokeAsync(this);
}));
base.OnInitialized();
}
private string _value;
[Parameter]
public string Value
{
get { return _value; }
set
{
_value = value;
SetMarkdown(value, true);
}
}
[Parameter]
public EventCallback<string> ValueChanged { get; set; }
private async Task setValue()
{
_value = await GetMarkdown();
await ValueChanged.InvokeAsync(_value);
}
public void SetMarkdown(string markdown, bool cursorToEnd = true)
{
editor?.InvokeVoidAsync("setMarkdown", markdown, cursorToEnd);
}
這樣我們就可以使用 Blazor 繫結語法了:
<TuiEditor @bind-Value="markdown"></TuiEditor>
@code{
private string markdown = "# Init Title";
}
效果如下:
原始碼
希望喜歡 Blazor 和 BulmaRazor 的朋友給個Star鼓勵一下!該專案從2021年的春節假期開始,一個人做真心的累和耗時,您的鼓勵是我堅持下去的最大動力!