在 sgcHTML 中集成第三方 JavaScript 库 | eSeGeCe 博客

在 sgcHTML 中集成第三方 JavaScript 库

· 组件

我们经常从评估 sgcHTML 的用户那里听到这样一个问题:它内置了大量现成的组件,但如果我已经有喜欢的 JavaScript 库该怎么办?也许你更喜欢用 D3.jsChart.js 绘制图表,或者你已经购买了 DevExtreme Grid 并想使用它。简单的答案是,sgcHTML 从一开始就被设计成开放的。它在服务器端通过 Delphi、C++Builder 或 .NET 代码生成完整页面,而任何能在浏览器中运行的库都可以通过三个小小的钩子接入。如果你经常使用同一个库,可以将它封装成可复用的组件,这样应用的其余部分就完全不需要接触 JavaScript 了。

三个钩子

每个第三方控件在页面上都需要相同的三样东西:必须加载库本身,必须有一个元素供其渲染,并且在 DOM 就绪后需要运行一行初始化代码。TsgcHTMLComponent_Site 组件为每一项都提供了对应的属性或方法。

下面是一个完整的 D3.js 示例。加载库,放入一个容器,然后让 D3 在其中绘制。

uses
  sgcHTML_Component_Site;

var
  oSite: TsgcHTMLComponent_Site;
begin
  oSite := TsgcHTMLComponent_Site.Create(nil);
  try
    oSite.Title := 'Sales Dashboard';

    // 1) Load the library (a public CDN, or a file you serve yourself)
    oSite.CustomHead := '<script src="https://d3js.org/d3.v7.min.js"></script>';

    // 2) Emit the element your library renders into
    oSite.AddContent('<div id="chart"></div>');

    // 3) Run your init code at the end of the body
    oSite.BodyEndHTML :=
      '<script>' +
      '  const data = [4, 8, 15, 16, 23, 42];' +
      '  d3.select("#chart").selectAll("div").data(data).enter()' +
      '    .append("div").attr("class", "bar")' +
      '    .style("width", d => (d * 12) + "px").text(d => d);' +
      '</script>';

    WebModule.Response := oSite.HTML;
  finally
    oSite.Free;
  end;
end;

这就是完整的模式。替换 URL 和初始化代码,同样的三行代码就能承载 Chart.js、DevExtreme 控件、日历、地图或其他任何东西。

CDN 还是自托管

上面的示例指向公共 CDN,这是最快的尝试方式。对于内网应用、离线部署,或者当你想锁定某个确切版本时,可以从 sgc 服务器自行提供库文件,并用本地路径引用它。有几个内置组件已经这样做了:内置的 Chart 组件从本地的 /chart.umd.min.js 加载 Chart.js,而不是从 CDN 加载,因此页面没有任何外部依赖。

// The .js and .css files sit next to your server executable and are
// served as static files, so the page references them by local path:
oSite.CustomHead :=
  '<link rel="stylesheet" href="/assets/mylib.min.css">' +
  '<script src="/assets/mylib.min.js"></script>';

如果你更喜欢用节点而不是原始字符串来构建 head,模板层提供了 HeadNodesBodyEndNodes 列表,配合 AddScriptAddStylesheet 辅助方法,以及 TsgcHTMLScript(带有 SrcCodeDeferAsync)和 TsgcHTMLStylesheet 节点。原始字符串和类型化节点可以互换使用,选择在你代码中读起来更顺的那种即可。

几行代码搞定 Chart.js

Chart.js 是一个常见的需求,下面通过同样的三个钩子来实现。加载库,输出一个 <canvas>,然后构建图表。

oSite.CustomHead := '<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>';

oSite.AddContent('<canvas id="revenue" width="600" height="300"></canvas>');

oSite.BodyEndHTML :=
  '<script>' +
  '  new Chart(document.getElementById("revenue"), {' +
  '    type: "bar",' +
  '    data: {' +
  '      labels: ["Q1", "Q2", "Q3", "Q4"],' +
  '      datasets: [{ label: "Revenue", data: [12, 19, 7, 15] }]' +
  '    }' +
  '  });' +
  '</script>';

如果你只是想要图表,并不需要特定的库,请注意 sgcHTML 已经内置了 TsgcHTMLComponent_Chart 组件,它已经为你接好了 Chart.js,并提供了 CustomOptions 属性,可以直接透传任意原始 Chart.js 配置。本节适用于你想自己驱动该库的情况。

DevExtreme Grid 示例

DevExtreme 是一个较重的商业套件的典型例子。DataGrid 需要加载它的样式表和打包文件、一个用于挂载的元素,以及一个配置对象。整体方法没有任何变化。

oSite.CustomHead :=
  '<link rel="stylesheet" href="https://cdn3.devexpress.com/jslib/23.2.3/css/dx.light.css">' +
  '<script src="https://cdn3.devexpress.com/jslib/23.2.3/js/dx.all.js"></script>';

oSite.AddContent('<div id="grid"></div>');

oSite.BodyEndHTML :=
  '<script>' +
  '  new DevExpress.ui.dxDataGrid(document.getElementById("grid"), {' +
  '    dataSource: [' +
  '      { id: 1, name: "Widget", price: 9.90 },' +
  '      { id: 2, name: "Gadget", price: 14.50 }' +
  '    ],' +
  '    columns: ["id", "name", "price"],' +
  '    showBorders: true' +
  '  });' +
  '</script>';

有意思的部分在于 dataSource 从何而来。你可以在服务器端将数据集序列化为 JSON 并像上面这样内联嵌入,也可以让 grid 指向 sgc 服务器暴露的某个端点,实时加载数据。demos 文件夹中已经有一个现成的 DevExtreme Grid 示例,它从 sgc WebSocket 服务器为 grid 提供数据,因此无需刷新页面数据就能实时更新。

将其封装为可复用组件

偶尔传递原始字符串是没问题的。但当你发现自己在一个又一个页面中反复使用同一个库时,就该把它提升为组件了:继承 TsgcHTMLComponent 并重写 GetHTML,返回你自己的标记以及库的粘合代码。之后在应用的其他地方,你只需设置一个属性并调用 HTML,就像使用内置组件一样。

uses
  sgcHTML_Component;

type
  TsgcHTMLComponent_D3Bars = class(TsgcHTMLComponent)
  private
    FData: string;   // a JSON array, e.g. '[4,8,15,16,23,42]'
  published
    property Data: string read FData write FData;
  public
    function GetHTML: string; override;
  end;

function TsgcHTMLComponent_D3Bars.GetHTML: string;
begin
  Result :=
    '<div id="d3bars"></div>' +
    '<script src="https://d3js.org/d3.v7.min.js"></script>' +
    '<script>' +
    '  const data = ' + FData + ';' +
    '  d3.select("#d3bars").selectAll("span").data(data).enter()' +
    '    .append("span").style("padding-right", d => d + "px").text(d => d);' +
    '</script>';
end;

这并不是什么特例,许多内置组件正是这样构建的。Map 组件封装了 Leaflet,RichEditor 封装了 Quill,KanbanBoard 封装了 SortableJS,PDFViewer 封装了 pdf.js,它们各自都是通过正是这几个钩子来加载库并输出初始化代码的。你自己的封装组件会与它们平起平坐,成为一等公民组件,如果库需要样式,你还可以添加对应的 GetCSS 重写,sgcHTML 会为你在整个页面范围内收集并去重这些样式。

动手试试

总而言之,sgcHTML 从不把你限制住。内置组件已经覆盖了大量场景,而当你需要某个特定的东西时,D3.js、Chart.js、DevExtreme 的 Grid 或 Chart,任何浏览器库都一样,只需三个钩子即可嵌入,再加一个小小的子类就能让它可复用。

有问题或反馈?欢迎联系我们,回复你的正是编写这些代码的人。