我们经常从评估 sgcHTML 的用户那里听到这样一个问题:它内置了大量现成的组件,但如果我已经有喜欢的 JavaScript 库该怎么办?也许你更喜欢用 D3.js 或 Chart.js 绘制图表,或者你已经购买了 DevExtreme Grid 并想使用它。简单的答案是,sgcHTML 从一开始就被设计成开放的。它在服务器端通过 Delphi、C++Builder 或 .NET 代码生成完整页面,而任何能在浏览器中运行的库都可以通过三个小小的钩子接入。如果你经常使用同一个库,可以将它封装成可复用的组件,这样应用的其余部分就完全不需要接触 JavaScript 了。
三个钩子
每个第三方控件在页面上都需要相同的三样东西:必须加载库本身,必须有一个元素供其渲染,并且在 DOM 就绪后需要运行一行初始化代码。TsgcHTMLComponent_Site 组件为每一项都提供了对应的属性或方法。
CustomHead将标记注入<head>,这是加载库所需的<script>或<link>的天然位置。AddContent将目标元素输出到页面主体中。BodyEndHTML在库加载完成后、</body>之前运行你的初始化代码。
下面是一个完整的 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,模板层提供了 HeadNodes 和 BodyEndNodes 列表,配合 AddScript 和 AddStylesheet 辅助方法,以及 TsgcHTMLScript(带有 Src、Code、Defer、Async)和 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,任何浏览器库都一样,只需三个钩子即可嵌入,再加一个小小的子类就能让它可复用。
有问题或反馈?欢迎联系我们,回复你的正是编写这些代码的人。
