You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Blazor 8全局WebAssembly封装Quill.js的InputBase组件抛异常求助

解决方案

问题根源

Quill.js初始化时会完全替换目标DOM元素的内部结构,而Blazor的虚拟DOM渲染系统在更新组件(比如设置CurrentValue触发重新渲染)时,会尝试操作最初渲染的DOM节点,此时该节点已被Quill修改或移除,导致removeChild空引用异常。

正确实现步骤

1. 组件结构与生命周期管理

用嵌套容器隔离Quill操作的DOM,在组件生命周期中正确初始化/销毁Quill,避免DOM冲突:

@inherits InputBase<string>
@inject IJSRuntime JSRuntime
@implements IAsyncDisposable

<div class="quill-container" @ref="containerElement">
    <!-- Quill将挂载到这个内部div,Blazor只操作外层容器 -->
    <div @ref="quillTargetElement"></div>
</div>

@code {
    private ElementReference containerElement;
    private ElementReference quillTargetElement;
    private IJSObjectReference? quillModule;
    private bool isQuillInitialized;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender && !isQuillInitialized)
        {
            // 导入Quill模块并初始化
            quillModule = await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./scripts/quill-wrapper.js");
            await quillModule.InvokeVoidAsync("initQuill", quillTargetElement, CurrentValueAsString, DotNetObjectReference.Create(this));
            isQuillInitialized = true;
        }
        else if (isQuillInitialized && !string.Equals(CurrentValueAsString, await GetQuillContent()))
        {
            // 同步.NET值到Quill(仅当值不一致时)
            await quillModule!.InvokeVoidAsync("setContent", quillTargetElement, CurrentValueAsString);
        }
    }

    // 从Quill获取内容的方法
    private async Task<string> GetQuillContent()
    {
        return await quillModule!.InvokeAsync<string>("getContent", quillTargetElement);
    }

    // 接收Quill的失焦回调(JS调用)
    [JSInvokable]
    public async Task OnQuillBlur(string content)
    {
        // 使用InputBase的安全方法更新值,避免直接修改CurrentValue导致的渲染冲突
        await SetCurrentValueAsStringAsync(content);
        // 触发验证(如果需要)
        EditContext?.NotifyFieldChanged(FieldIdentifier);
    }

    public async ValueTask DisposeAsync()
    {
        if (quillModule != null)
        {
            await quillModule.InvokeVoidAsync("destroyQuill", quillTargetElement);
            await quillModule.DisposeAsync();
        }
    }

    // 重写InputBase的格式化方法,处理空值
    protected override bool TryParseValueFromString(string? value, out string result, out string validationErrorMessage)
    {
        result = value ?? string.Empty;
        validationErrorMessage = string.Empty;
        return true;
    }
}

2. JavaScript封装(quill-wrapper.js)

处理Quill的初始化、内容获取/设置、销毁,以及失焦事件回调:

import Quill from 'quill';
import 'quill/dist/quill.snow.css';

// 存储Quill实例,避免重复初始化
const quillInstances = new WeakMap();

export function initQuill(element, initialContent, dotNetRef) {
    const quill = new Quill(element, {
        theme: 'snow',
        modules: {
            toolbar: [
                ['bold', 'italic', 'underline'],
                ['link', 'code-block'],
                [{ list: 'ordered' }, { list: 'bullet' }]
            ]
        }
    });

    // 设置初始内容
    if (initialContent) {
        quill.root.innerHTML = initialContent;
    }

    // 绑定失焦事件,回调.NET方法
    quill.root.addEventListener('blur', () => {
        const content = quill.root.innerHTML;
        dotNetRef.invokeMethodAsync('OnQuillBlur', content);
    });

    quillInstances.set(element, quill);
}

export function getContent(element) {
    const quill = quillInstances.get(element);
    return quill ? quill.root.innerHTML : '';
}

export function setContent(element, content) {
    const quill = quillInstances.get(element);
    if (quill) {
        quill.root.innerHTML = content || '';
    }
}

export function destroyQuill(element) {
    const quill = quillInstances.get(element);
    if (quill) {
        quill.destroy();
        quillInstances.delete(element);
    }
}

3. 使用示例(在EditForm中)

<EditForm Model="@model" OnValidSubmit="@HandleSubmit">
    <DataAnnotationsValidator />
    <ValidationSummary />

    <div class="form-group">
        <label>富文本内容</label>
        <InputRichTextEditor @bind-Value="@model.Content" />
        <ValidationMessage For="@(() => model.Content)" />
    </div>

    <button type="submit">提交</button>
</EditForm>

@code {
    private MyModel model = new();

    private void HandleSubmit()
    {
        // 处理提交逻辑
    }

    public class MyModel
    {
        [Required(ErrorMessage = "请输入内容")]
        public string Content { get; set; } = string.Empty;
    }
}

关键注意事项

  • 隔离DOM元素:Blazor仅操作外层容器,Quill挂载到内部div,避免Blazor的虚拟DOM与Quill修改的DOM冲突。
  • 使用InputBase的安全方法:通过SetCurrentValueAsStringAsync更新值,而非直接赋值CurrentValue,该方法会正确处理组件状态同步,避免渲染异常。
  • 生命周期管理:在OnAfterRenderAsync中初始化Quill(确保DOM已渲染),在IAsyncDisposable中销毁Quill,避免内存泄漏。
  • 值同步控制:仅当.NET值与Quill内容不一致时才同步,避免不必要的DOM操作。

内容的提问来源于stack exchange,提问作者MikeT

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 07:18:26