在 Blazor Server 中集成 docx-preview.js 实现高保真 Word 预览

前言

这两天在做一个在线预览各种类型文档的模块,主要是针对pdf和word,pdf好说,方案一大把,选一个最合适的就好,我这里的管理项目是基于MudBlazor的,所以我使用了官方推荐的Pdf扩展组件Gotho.BlazorPdf,当然即便不用原生组件,自己基于pdf.js等前端方案来封装也是完全没问题的,这个我就不多说了。

这里主要想聊聊在线预览word文档的实现思路,总结起来基本就是3条路线

  • 第一条是先把word格式转成pdf,然后再通过pdf预览组件来预览,这条路线实现方案也很多,问题就是装换的实现如果你之前没有写过类似的功能,可能要费一番功夫,当然不差钱的话,可以用一些商用组件,比如Aspose, Spire.Doc等,虽然价格略高,但专业性高,功能极强,物有所值。当然除了转化成pdf还可以转换成html或者markdown等,总之就是转换的路线,这里不再赘述。
  • 第二条路线是借助微软原生的 Office Online 服务或者Google Viewer等方案,实现在线预览,当然这也有一个要求,就是你的文档需要能在公网访问,或者有运维能力的话,可以在本地部署一个私有Office Online服务,这个微软官方有详细的文档(https://learn.microsoft.com/zh-cn/officeonlineserver/deploy-office-online-server),对这种方式感兴趣的小伙伴可以试试(笔者不推荐私有部署的方式,如果你的场景里文档允许外网访问的话,推荐直接使用在线方式,最简单)。
  • 第三条路线是,使用一些纯前端方案,更加的轻量级,当然他会有一些限制条件,比如一些表现好的组件不支持原始的doc格式,只能是docx,超过10M的渲染可能也会很慢。所以选型时要考虑这些条件,笔者采用的就是这条路线。

方案介绍

近几年前端发展迅猛在线预览复杂的word文档已经有了成熟方案,比如mammoth.js,docx-preview.js,amis.js等,这里面mammoth还提供了.net的nuget包,方便Blazor环境使用,但有个问题是,他渲染出来的文档会影响原文布局,所以如果只是"看内容"不在乎排版受影响,那.net环境下,使用前端方案实现文档预览,mammoth毫无疑问是最佳方案。

但我这里是一个"审核"的场景,需要对源文件实现"公文级",设置"像素级"的还原,也就是和word文档几乎一样,所以更适合我这里的方案是"docx-preview.js",仓库地址👉:https://github.com/VolodymyrBaydalka/docxjs

至于另外一个amis.js,这个是国内大厂百度出品的一个组件,文档很全,也是一个不错的路线。

实现步骤

引入组件

因为是客户端方案,我们可以在外边通过npm等方式先把核心组件拉到本地,当然直接在VS里添加客户端库也可以。

bash 复制代码
npm install docx-preview

编写隔离型 JS 互操作层

在 wwwroot/assets/js/docxInterop.js 中,我们不仅要处理预览逻辑,还要处理环境隔离。

javascript 复制代码
/**
 * 我这里因为用到了Monaco Editor组件,因此要处理一下AMD加载器的冲突
 */
export async function renderDocxFromUrl(url, containerId) {
  const container = document.getElementById(containerId);
  if (!container) return;

  const loadScriptWithIsolation = (src) => {
    return new Promise((resolve, reject) => {
      if (document.querySelector(`script[src="${src}"]`)) {
        resolve(); return;
      }
      // 临时屏蔽全局define函数,防止与Monaco Editor等库的AMD加载器冲突
      const _backupDefine = window.define;
      window.define = undefined;

      const script = document.createElement('script');
      script.src = src;
      script.onload = () => {
        window.define = _backupDefine; // 加载后立即还原
        resolve();
      };
      script.onerror = reject;
      document.head.appendChild(script);
    });
  };

  try {
    // 1. 加载依赖
    await loadScriptWithIsolation('./assets/js/jszip.min.js');
    await loadScriptWithIsolation('./assets/js/docx-preview.min.js');

    // 2. 获取文件流
    const response = await fetch(url);
    const arrayBuffer = await response.arrayBuffer();

    // 3. 调用预览逻辑
    const options = { className: "docx-preview", inWrapper: true, breakPages: true };
    await window.docx.renderAsync(arrayBuffer, container, null, options);
  } catch (e) {
    console.error("预览失败:", e);
    container.innerHTML = "文档加载失败";
  }
}

封装 Blazor 预览组件

使用 IJSObjectReference 确保 JS 逻辑的模块化,避免污染全局命名空间。

csharp 复制代码
@inject IJSRuntime JS
@implements IAsyncDisposable

<div id="@_containerId" class="docx-render-area" style="height:@Height; overflow:auto;"></div>

@code {
    [Parameter] 
    public string Height { get; set; } = "700px";
    private string _containerId = $"docx-{Guid.NewGuid():N}";

    // JS 模块引用
    private IJSObjectReference? _module;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            // 动态加载 JS 模块文件
            _module = await JS.InvokeAsync<IJSObjectReference>("import", "/assets/js/docxInterop.js");
        }
    }

    public async Task LoadFromUrlAsync(string url)
    {
        if (_module == null) return;
        try
        {
            //_isLoading = true;
            StateHasChanged();
            // 直接把 URL 传给 JS 处理,避免大数组在 SignalR 中传输
            await _module.InvokeVoidAsync("renderDocxFromUrl", url, _containerId);
        }
        finally
        {
            //_isLoading = false;
            StateHasChanged();
        }
    }

    public async Task LoadFromStreamAsync(Stream stream)
    {
        if (_module == null) return;

        using var ms = new MemoryStream();
        await stream.CopyToAsync(ms);

        await _module.InvokeVoidAsync("renderDocx", ms.ToArray(), _containerId);
    }

    // 释放模块引用,防止内存泄漏
    public async ValueTask DisposeAsync()
    {
        if (_module != null)
        {
            try
            {
                // 只有当连接还活着的时候才去调用 Dispose
                await _module.DisposeAsync();
            }
            catch (JSDisconnectedException)
            {
                // 忽略连接断开导致的异常,这是正常的
            }
        }
    }
}

父组件引入

父组件的引入的时候只要给一个高度参数就可以了

csharp 复制代码
// blazor页面部分,引入组件
<DocxViewer @ref="_docxViewer" Height="75vh" />

// code部分编写引入逻辑
private DocxViewer _docxViewer;
private async Task LoadFile(string url)
{
     if (_docxViewer != null)
     await _docxViewer.LoadFromUrlAsync(url);
}

最后的效果如下

服务器配置

在服务注入的入口中适当调高 SignalR 的传输上限,这个仅限Blazor Server模式,BlazerWSAM或者Hybird方式不需要:

csharp 复制代码
builder.Services.AddServerSideBlazor()
    .AddHubOptions(options => {
        options.MaximumReceiveMessageSize = 32 * 1024 * 1024; // 32MB
    });

* 避坑

我在集成过程中,遇到了类似"Uncaught Error: Can only have one anonymous define call"的报错。排查后的原因是:

  1. 使用了Monaco Editor这样的库自带了 AMD 加载器(loader.js)。
  2. docx-preview 检测到define函数后会尝试注册模块,导致冲突。
  3. 解决方案:采用动态加载脚本,并在加载期间暂时"抹除"全局define。

总结

通过这种方式,不仅在 Blazor Server 中实现了 Word 文档的高保真预览,而且足够轻量化,还可以方便的将其用到任何需要预览功能的页面中。我真的越来越喜欢Blazor这个组件化的开发模式了,好了,就这些,下次再见。

相关推荐
Kapaseker2 小时前
前端已死...了吗
android·前端·javascript
Moment2 小时前
富文本编辑器技术选型,到底是 Prosemirror 还是 Tiptap 好 ❓❓❓
前端·javascript·面试
xkxnq2 小时前
第二阶段:Vue 组件化开发(第 18天)
前端·javascript·vue.js
晓得迷路了2 小时前
栗子前端技术周刊第 112 期 - Rspack 1.7、2025 JS 新星榜单、HTML 状态调查...
前端·javascript·html
怕浪猫2 小时前
React从入门到出门 第五章 React Router 配置与原理初探
前端·javascript·react.js
鹏多多2 小时前
前端2025年终总结:借着AI做大做强再创辉煌
前端·javascript
哈__3 小时前
React Native 鸿蒙跨平台开发:Vibration 实现鸿蒙端设备的震动反馈
javascript·react native·react.js
WebGISer_白茶乌龙桃3 小时前
Cesium实现“悬浮岛”式,三维立体的行政区划
javascript·vue.js·3d·web3·html5·webgl
hixiong1233 小时前
C# OpenvinoSharp部署DDDDOCR验证码识别模型
opencv·c#·ocr·openvino