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
相关产品推荐
相关产品推荐

