如何优雅的移植JavaScript组件到Blazor
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年的春节假期开始,一个人做真心的累和耗时,您的鼓励是我坚持下午的最大动力!

如何优雅的移植JavaScript组件到Blazor的更多相关文章
- 如何优雅的设计React组件
		如何优雅的设计 React 组件 如今的 web 前端已被 React.Vue 和 Angular 三分天下,一统江山十几年的 jQuery 显然已经很难满足现在的开发模式.那么,为什么大家会觉得 j ... 
- bootscript/javascript组件
		javascript组件 (1)每一个插件(带有js功能的组件),想使用bootstrap插件,一个是要去写他的html,第二个是使用CSS去修饰它,再引入相应的js文件. bootstrap框架 ... 
- 试试用有限状态机的思路来定义javascript组件
		本文是一篇学习性的文章,学习利用有限状态机的思想来定义javascript组件的方法,欢迎阅读,后续计划会写几篇专门介绍自己利用有限状态机帮助自己编写组件的博客,证明这种思路对于编程实现的价值,目前正 ... 
- Winjs – 微软开源技术发布的 JavaScript  组件集
		Winjs 是由微软开源技术的开发者推出的一组 JavaScript 组件,包括 ListView.ListView.Tooltip.DatePicker.Ratings 等等,帮助 Web 开发人员 ... 
- javascript组件化(转)
		javascript组件化(转) By purplebamboo 3月 16 2015 更新日期:3月 23 2015 文章目录 1. 最简陋的写法 2. 作用域隔离 3. 面向对象 4. 抽象出ba ... 
- javascript组件开发之基类继承实现
		上一篇文章大概的介绍了一下关于javascript组件的开发方式,这篇文章主要详细记一下基类的编写,这个基类主要是实现继承的功能 为什么要封装基类? 由于这次重构项目需要对各种组件进行封装,并且这些组 ... 
- 开发一个完整的JavaScript组件
		作为一名开发者,大家应该都知道在浏览器中存在一些内置的控件:Alert,Confirm等,但是这些控件通常根据浏览器产商的不同而形态各异,视觉效果往往达不到UI设计师的要求.更重要的是,这类内置控件的 ... 
- 优雅绝妙的Javascript跨域问题解决方案
		关于Javascript跨域问题的解决方案已在之前的一片文章中详细说明,详见:http://blog.csdn.net/sfdev/archive/2009/02/13/3887006.aspx: 除 ... 
- JavaScript 组件化开发之路(一)
		*:first-child{margin-top: 0 !important}.markdown-body>*:last-child{margin-bottom: 0 !important}.m ... 
随机推荐
- 从网络I/O模型到Netty,先深入了解下I/O多路复用
			微信搜索[阿丸笔记],关注Java/MySQL/中间件各系列原创实战笔记,干货满满. 本文是Netty系列第3篇 上一篇文章我们了解了Unix标准的5种网络I/O模型,知道了它们的核心区别与各自的优缺 ... 
- Nginx 服务介绍
			目录 静态 / 动态 Web 服务 Nginx 简介 Nginx 的优点 Nginx 和 Apache 的比较 Nginx 的安装 Nginx 相关文件 Nginx 主配置文件 Nginx 虚拟主机配 ... 
- Linux-输出/输入重定向
			目录 重定向的分类 输出重定向 将标准输出重定向到文件 将标准输出追加重定向到文件 将错误输出重定向到文件 将标准输出和错误输出都重定向到文件 将错误输出重定向到黑洞文件 输入重定向 重定向的分类 名 ... 
- 8.rabbitmq RPC模拟微服务架构中的服务调用
			标题 : 8.rabbitmq RPC模拟微服务架构中的服务调用 目录 : RabbitMQ 序号 : 8 { var connectionFactory = new ConnectionFactor ... 
- struct 和 class的区别
			struct和class如果按照在C的时代,还是有很大差别的. c中struct的定义如下: struct 结构名 { 成员表 }: 因为struct是一种数据类型,那么就肯定不能定义函数,所以 ... 
- 鸟哥的linux私房菜——第四章学习
			******************第四章学习****************** [热键] 1.Tab键:命令补全:文件补全: 2.Ctrl+c:中断目前指令: 3.Ctrl+d:离开当前文本界面: ... 
- ES6 Map to Array
			ES6 Map to Array function differentSymbolsNaive(str) { // write code here. const map = new Map(); co ... 
- 如何使用 js 实现一个 debounce 函数
			如何使用 js 实现一个 debounce 函数 原理 防抖: 是指在指定的单位时间内,如果重复触发了相同的事件,则取消上一次的事件,重新开始计时! 实现方式 "use strict&quo ... 
- CSS & Architecture
			CSS & Architecture https://sass-guidelin.es/#architecture https://sass-guidelin.es/#the-7-1-patt ... 
- Github history viewer
			Github history viewer https://github.githistory.xyz/ https://github.com/pomber/git-history https://c ... 
