快速入门

在几分钟内渲染您的第一份文档

本指南将一个空的 Program.cs ASP.NET Core 应用程序转变为在浏览器中渲染的文档:服务器注册、完整的 Viewer 包(Viewer 工具栏、Viewer 挂载点以及可选的搜索/注释功能区)、资源引用、客户端初始化、文档打开以及执行。

宿主应用程序必须面向支持 netstandard2.1 的运行时——.NET Core 3.0 或更高版本。本次发布附带的参考应用程序目标为 net8.0。

服务器设置

AddDoconut() 注册服务;UseDoconutResources() 和 UseDoconut() 接线中间件。资源调用必须先于其他调用。会话调用也是必需的——Doconut 的默认文档安全会对每个页面请求进行 ASP.NET 会话状态验证。会话状态需要缓存:AddDistributedMemoryCache() 提供缓存。MVC 或 Razor Pages 应用已经注册了它,但最小 API 应用没有,如果缺少它,应用将在启动时因 Unable to resolve service for type 'IDistributedCache' 而停止。已经在安装 中注册了 Doconut?请直接跳到下一节。

csharp
builder.Services.AddDoconut(options =>
{
    options.LicensePath = "Doconut.Viewer.lic";
});
builder.Services.AddDistributedMemoryCache(); // 会话状态需要缓存(MVC/Razor Pages 已经添加了一个)
builder.Services.AddSession(); // Doconut 文档安全依赖于 ASP.NET 会话状态

app.UseSession();          // 在调用 UseDoconut() 之前先调用 UseSession()
app.UseDoconutResources(); // 必须在 UseDoconut() 之前注册
app.UseDoconut();

参考应用程序保持了历史的 DocImage.axd 请求形态,并在匹配的分支上挂载文档中间件。这是当你将现有 .NET Standard 应用向前迁移时需要复制的安排,因为客户端始终会调用相同的 URL:

csharp
builder.Services.AddDoconut(options =>
{
    options.UnsafeMode      = false;   // 保持 Doconut 的会话检查已启用
    options.ShowDoconutInfo = false;
});
builder.Services.AddDistributedMemoryCache(); // 会话状态需要缓存(MVC/Razor Pages 已经添加了一个)
builder.Services.AddSession(options =>
{
    options.IdleTimeout        = TimeSpan.FromSeconds(3600);
    options.Cookie.HttpOnly    = true;
    options.Cookie.IsEssential = true;
});

// 某些文档格式需要多字节编码支持
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

app.UseStaticFiles();
app.UseRouting();
app.UseSession();
app.UseDoconutResources();   // 在文档分支之前
app.MapWhen(
    context => context.Request.Path.Value?.EndsWith("DocImage.axd", StringComparison.OrdinalIgnoreCase) == true,
    branch => branch.UseDoconut());

UseDoconut() 在本次发布中不接受任何选项——所有配置已迁移至 AddDoconut()。MiddlewarePath 是一个协调值;它本身并不映射 ASP.NET Core 分支。ResourcesPath 在 /doconut-res 提供嵌入式包,而小部件的图像资源路径因此为 ResPath: 'doconut-res/images'。

将查看器添加到页面

Viewer 是页面必需的核心。它的渲染表面使用两个嵌套的 div:

html
<div id="divDocViewer">
    <div id="div_ctlDoc"></div>
</div>

将工具栏、模块挂载点和 Viewer 表面视为一个页面组合。搜索和注释会将其嵌入的功能区注入可选挂载点,但这些模块从不单独存在:它们始终附加在同一页面的 Viewer 上。

html
<nav id="toolbar" aria-label="Document viewer controls">
    <!-- 查看器导航、缩放、搜索和注释按钮 -->
</nav>

<div id="searchBarMount"></div>
<div id="annBarMount"></div>

<div id="divDocViewer">
    <div id="div_ctlDoc"></div>
</div>

引用查看器资源

在 Razor 视图中,注入的 Viewer 服务按依赖顺序输出查看器的 <link> 和 <script> 标签——小部件是一个 jQuery 插件,因此必须先加载 jQuery 再加载查看器脚本:

html
@inject Doconut.Viewer Viewer

@Html.Raw(Viewer.ReferenceCss(new CssConfig
{
    IncludeBootstrapCss = true,
    IncludeViewerCss    = true
}))

@Html.Raw(Viewer.ReferenceScripts(new ScriptConfig
{
    IncludeJQuery        = true,
    IncludeBootstrap     = true,
    IncludeViewerScripts = true
}))

要获取完整的 Viewer 包,请一起请求 Viewer 和模块资源:

html
@Html.Raw(Viewer.ReferenceCss(new CssConfig
{
    IncludeBootstrapCss  = true,
    IncludeViewerCss     = true,
    IncludeSearchCss     = true,
    IncludeAnnotationCss = true
}))

@Html.Raw(Viewer.ReferenceScripts(new ScriptConfig
{
    IncludeJQuery            = true,
    IncludeBootstrap         = true,
    IncludeViewerScripts     = true,
    IncludeSearchScripts     = true,
    IncludeSearchBar         = true,
    IncludeAnnotationScripts = true,
    IncludeAnnotationBar     = true
}))

IncludeViewerCss 和 IncludeViewerScripts 是必需的核心标志。绝不要在没有它们、Viewer 挂载以及 docViewer 实例的情况下发布搜索或批注功能区示例。ReferenceCss 和 ReferenceScripts 在当前许可证不具备相应能力时会省略可选模块的资源;核心 Viewer 仍会启动。

这些调用取代了手动复制的脚本文件。如果应用仍然加载自己的 docViewer.js、documentLinks.js 或 docViewer.UI.js,在页面能够使用已输出资源后请移除这些标签——参见迁移指南。

初始化查看器

客户端小部件是一个 jQuery 插件。这是一组最小的真实初始化选项(非伪代码):

javascript
let searchBar = null;
let annBar = null;

const objViewer = $('#div_ctlDoc').docViewer({
    showThumbs: true,
    autoLoad:   false,
    pageZoom:   100,
    FitType:    'width',
    BasePath:   '/',
    ResPath:    'doconut-res/images',
    onViewerReady: function () {
        // 页面已可见;此处可以安全地隐藏加载指示器
    },
    // 当嵌入的功能区存在时,将注释生命周期事件转发给它。
    onAnnLoaded:    () => annBar?.handleAnnLoaded(),
    onAnnSaved:     () => annBar?.handleAnnSaved(),
    onAnnSaveError: () => annBar?.handleAnnSaveError(),
    onAnnClosed:    () => annBar?.handleAnnClosed(),
    onError: function (message) {
        console.error('Doconut viewer error:', message);
    }
});

有两个细节决定此页面是否能正常工作:

  • 回调是选项。 onViewerReady、onViewerBusy、onCopy 和 onAutoLoadStatus 作为选项对象传入。之前的查看器会根据容器 id 推导全局函数名并调用它们;而现在不会。仅以全局函数声明的处理程序永远不会运行,也不会有任何报告。
  • ResPath 指向资源前缀,而不是应用根目录。使用默认的 ResourcesPath 时,该值为 doconut-res/images。如果你修改了 ResourcesPath,请改用自己的前缀。

这两者都会静默失败——参见故障排查和迁移指南。

选项的大小写确实混杂——showThumbs、autoLoad、pageZoom 为 camelCase,而 FitType、BasePath、ResPath 为 PascalCase。没有统一规则;大小写写错会导致选项被静默忽略(小部件会回退到默认值,而不是抛出错误)。

组装完整的 Viewer 包

两个参考应用都会在同一页面上一起安装以下部分:

包的组成部分必要性连接方式
Viewer 资源、挂载以及 objViewer必需核心文档渲染器
Viewer 工具栏在参考组合中必需主机标记;按钮调用同一个 objViewer
搜索功能区可选,需许可证的模块doconutSearchBar(...).attach(objViewer)
批注功能区可选,需许可证的模块doconutAnnotationBar(...).attach(objViewer)

虽然主 Viewer 工具栏是主机标记,但它与 Viewer 一起安装,绝不能作为独立控件单独文档化。这样可以让其布局、标签、图标和授权规则始终受你的应用控制,同时每个按钮都驱动同一个 Viewer 实例:

html
<nav id="toolbar" aria-label="Document viewer controls">
    <button type="button" onclick="objViewer.GotoPage(1)">First</button>
    <button type="button" onclick="objViewer.Next(false)">Previous</button>
    <button type="button" onclick="objViewer.Next(true)">Next</button>
    <button type="button" onclick="objViewer.GotoPage(objViewer.TotalPages())">Last</button>
    <button type="button" onclick="objViewer.Zoom(false)">Zoom out</button>
    <button type="button" onclick="objViewer.Zoom(true)">Zoom in</button>
    <button type="button" onclick="objViewer.FitType('width')">Fit width</button>
    <button type="button" onclick="objViewer.FitType('height')">Fit height</button>
    <button type="button" id="openSearch">Search</button>
    <button type="button" id="openAnnotations">Annotations</button>
</nav>

保持两个参考应用使用的包初始化顺序:

  1. 为 Viewer 和已授权模块输出 CSS。
  2. 渲染 Viewer 工具栏、搜索/批注挂载点以及 Viewer 挂载点。
  3. 为 Viewer 和已授权模块输出脚本。
  4. 加载主机应用自己的工具栏辅助脚本。
  5. 初始化 docViewer 并保留得到的 objViewer。
  6. 初始化每个已授权的搜索或批注功能区。
  7. 对每个功能区调用 attach(objViewer)。
  8. 打开文档并保留其 token,以供 Viewer 和模块请求使用。

服务器端的守卫很重要:当可选功能不可用时,其脚本不会被输出,从而其 jQuery 插件函数不存在。

html
<script>
    let currentToken = '';

    const refitViewer = () =>
        requestAnimationFrame(() => objViewer.Refit());

    @if (Viewer.IsSearchEnabled)
    {
        <text>
    searchBar = $('#searchBarMount').doconutSearchBar({
        docId: 'ctlDoc',
        getRequestParams: () => ({ token: currentToken }),
        onLayout: refitViewer
    });
    searchBar.attach(objViewer);
        </text>
    }

    @if (Viewer.IsAnnotationEnabled)
    {
        <text>
    annBar = $('#annBarMount').doconutAnnotationBar({
        docId: 'ctlDoc',
        getRequestParams: () => ({ token: currentToken }),
        onLayout: refitViewer
    });
    annBar.attach(objViewer);
        </text>
    }

    document.getElementById('openSearch').addEventListener('click', () => {
        if (!searchBar) return;
        searchBar.isOpen() ? searchBar.close() : searchBar.open();
    });

    document.getElementById('openAnnotations').addEventListener('click', () => {
        if (!annBar) return;
        annBar.isOpen() ? annBar.close() : annBar.open();
    });
</script>

两个嵌入组件各自生成自己的功能区 DOM。搜索功能区包含“查找”“选项”“结果”组。批注功能区包含作者工具、样式控制、保存操作以及可选的导出/图片操作。功能区公开 open()、close()、reset() 和 isOpen();在创建后务必调用一次 attach(objViewer)。

上面的示例省略了可选的主机回调和批注导出/图片端点,以保持启动最小化。请参阅搜索和批注了解完整的特定功能设置,或查看自定义主题以为主机拥有的 Viewer 工具栏设置样式或替换它。

打开文档

服务器端只有一个端点:注入的 Viewer 服务打开文档并返回会话令牌。此版本中打开是异步的。

csharp
app.MapPost("/api/open", async (Viewer viewer) =>
{
    // 令牌是不透明的——将其交给小部件,切勿记录或持久化。
    string token = await viewer.OpenDocumentAsync("wwwroot/files/Sample.pdf");
    return Results.Ok(new { token });
});

客户端获取该令牌并使用 objViewer.View(token) 将其交给小部件:

javascript
fetch('/api/open', { method: 'POST' })
    .then(resp => resp.json())
    .then(data => {
        currentToken = data.token;
        objViewer.View(currentToken);
    });

在第一次打开之前无需任何预热。如果你在迁移应用程序,删除所有 InitCache() 调用以及其保护标志——该方法已不再存在。

关闭文档

当用户离开查看器或打开替代文档时调用 objViewer.Close()。在服务器驱动的工作流中,viewer.CloseDocument(token) 会立即移除缓存的会话,释放渲染引擎,删除其安全标记,并撤销令牌。滑动过期最终会执行相同的清理,但对于大文档建议显式关闭。

完整的请求流程如下:

text
AddDoconut + middleware
    -> render CSS/scripts and mount div
    -> initialize docViewer
    -> OpenDocumentAsync
    -> return opaque token
    -> objViewer.View(token)
    -> page/search/annotation requests
    -> Close / CloseDocument

将令牌视为持有者凭证:切勿记录,切勿持久化,只交给小部件使用。它标识服务器上的活跃文档会话,并在会话过期后失效——重新打开文档即可获取新的令牌。

运行示例

在 wwwroot/files/Sample.pdf 放置一个 PDF,运行 dotnet run,然后打开承载小部件的页面。第一页会在查看器中渲染,左侧显示缩略图面板。如果没有显示,请参阅故障排除。

未授权时的表现

缺少许可证不会抛出异常。查看器仍会正常渲染,但每页都会带有评估水印。请参阅许可证设置了解 Doconut 如何查找许可证以及找到后会有哪些变化。

此页面对您有帮助吗?