跳到主要内容

【已废弃】实现

版权声明

根据 URL 生成站点入口页面

对站点入口页面的生成需满足如下要求:

  • 需在 API 网关过滤器之后路由剩余请求,对匹配到的站点请求,返回站点入口页面, 对未匹配到的非静态资源请求,返回默认的站点页面(一般为 404)
  • 支持对站点入口 HTML 页面的差量修订,从而按需实现定制化

按照 创建最小 Quarkus 应用服务启动器 的指导步骤可以创建出网关服务启动器 gateway-starter,其已内置对 IHttpServerFilter 过滤器的扫描和加载,故而,在 gateway-web 模块中直接实现针对站点入口页面生成的过滤器即可:

WebSiteHttpServerFilter.java
public class WebSiteHttpServerFilter implements IHttpServerFilter {
@Inject
protected WebSiteProvider provider;

@Override
public int order() {
return GatewayConstants.PRIORITY_WEB_SITE_FILTER;
}

@Override
public CompletionStage<Void> filterAsync(
IHttpServerContext context,
Supplier<CompletionStage<Void>> next
) {
return context.executeBlocking(() -> {
return doFilter(context, next);
})
.exceptionally((e) -> {
return handleError(context, e);
})
.thenApply(r -> null);
}

private CompletionStage<Void> doFilter(
IHttpServerContext context,
Supplier<CompletionStage<Void>> next
) {
String path = context.getRequestPath();
String html = this.provider.getSiteHtmlByRequestPath(path);

// 若无匹配的站点,且不是请求的静态资源,则返回默认站点页面
if (html == null && !WebStaticResourcesHelper.isFile(path)) {
html = this.provider.getSiteHtmlByRequestPath("*");
}

// 仍然无匹配的站点,则继续后续的路由,如,静态资源路由等
if (html == null) {
return next.get();
}

// 返回匹配站点的入口页面内容
context.setResponseContentType(ContentType.TEXT_HTML.getMimeType());
context.sendResponse(HttpStatus.SC_OK, html);

return null;
}

private CompletionStage<Void> handleError(
IHttpServerContext context, Throwable e
) {
context.sendResponse(500, "Server Error");
return null;
}
}

@Injectprivate 属性无效,其仅可标注在 protectedpublic 属性上,也可以直接标注在 setter 接口上。

WebSiteHttpServerFilter#doFilter 中会通过 WebSiteProvider 根据请求的路径去匹配已定义的站点, 若未匹配到站点,并且请求的也不是已存在的静态资源,则会尝试获取默认站点, 其以 * 为匹配条件,当存在 XWebSite#url* 的站点时, 便返回该站点的页面。默认站点应该始终有定义,故而,在最后一步仍未得到 HTML 页面,则继续后续的静态资源路由。

通过 IHttpServerContext#executeBlocking 会以异步方式调用 WebSiteHttpServerFilter#doFilter,以提高应用服务的并发能力。

WebSiteHttpServerFilter#order 决定了当前过滤器的优先级,其值越大, 其优先级越低。常量 GatewayConstants.PRIORITY_WEB_SITE_FILTER 是定义在 API 网关过滤器之后的一个数值。

站点页面由 WebSiteProvider 生成,其通过读取默认位置的 XWeb 的 DSL 得到全部站点的定义,再通过站点 HTML 生成模板得到其入口页面:

WebSiteProvider.java
public class WebSiteProvider {

@PostConstruct
public void init() {
EvalGlobalRegistry
.instance()
// 注册全局变量 $site,该变量在
// WebSiteGlobalVariable 内通过线程变量临时持有其实例对象
.registerVariable(
"$site", WebSiteGlobalVariable.instance()
);
}

public String getSiteHtmlByRequestPath(String path) {
XWeb web = (XWeb) new DslModelParser()
.parseFromVirtualPath(
"/duzhou/web/app.web.xml"
);
XWebSite site = web.getSiteByUrl(path);

return site == null
? null
// 更新当前线程的 $site 对象实例,
// 并执行站点 HTML 页面生成函数
: WebSiteGlobalVariable.with(site, this::genSiteHtml);
}

private String genSiteHtml() {
Object model = new DslModelParser()
.parseFromVirtualPath(
"/duzhou/web/app.site-html.xml"
);
XNode node = WebDslModelHelper
.toHtmlNode(
"/duzhou/schema/web/html.xdef",
model
);

String html = node.html();
html = StringHelper.unescapeXml(html);

return "<!DOCTYPE html>" + html;
}
}

app.web.xmlapp.site-html.xml 的加载可采用 自定义 DSL 模型加载器 机制,以避免写死代码,从而提供更加灵活的定制能力。不过,需要注意的是, app.site-html.xml 的解析结果与全局变量 $site 绑定,故而,不能缓存其解析结果。

注意,WebSiteProviderWebSiteHttpServerFilter 需通过 Nop IoC 机制创建实例(详细说明见 Nop Beans 的创建与加载):

/nop/autoconfig/duzhou-gateway-web.beans
/duzhou/web/beans/default.beans.xml
/duzhou/web/beans/default.beans.xml
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:x="/nop/schema/xdsl.xdef" xmlns:ioc="ioc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"
x:schema="/nop/schema/beans.xdef"
>

<bean id="webSiteHttpServerFilter"
class="io.crazydan.duzhou.framework.gateway.web.filter.WebSiteHttpServerFilter"
ioc:default="true" />

<bean id="webSiteProvider"
class="io.crazydan.duzhou.framework.gateway.web.WebSiteProvider" />
</beans>

WebSiteProvider#getSiteHtmlByRequestPath 中, 会解析 XWeb 的固定位置的 DSL 定义文件 /duzhou/web/app.web.xml, 并根据请求路径得到匹配的站点对象 XWebSite。若是匹配到站点,则调用 WebSiteProvider#genSiteHtml 生成该站点的 HTML 页面。

站点的 HTML 页面结构定义在 /duzhou/web/app.site-html.xml 中,其也是一个 XDSL,可支持对其做差量定制,通过 DslModelParser 可以解析得到 DynamicObject 类型的 DSL 模型对象。 该模型对象需要通过 WebDslModelHelper#toHtmlNode 将其转换为 XNode 树,再调用 XNode#html 得到 HTML 文本内容,对其内容做 XML 反转义后,便可返回给 Web 客户端处理。

站点的 HTML 页面 XDSL 的定义如下:

/duzhou/web/app.site-html.xml
<html xmlns:x="/nop/schema/xdsl.xdef"
xmlns:xpl="xpl" xmlns:web="web"
x:schema="/duzhou/schema/web/html.xdef"
>

<x:gen-extends>
<web:GenSiteHtml
site="${$site}"
xpl:lib="/duzhou/web/xlib/web.xlib"
/>
</x:gen-extends>
</html>

该 DSL 通过定义在 /duzhou/web/xlib/web.xlib 中的 xpl 函数 GenSiteHtml 根据当前站点对象 $site 展开其 HTML 页面结构。

x:gen-extends 中引用全局变量的解释见 编写 x:gen-extends 的运行代码

站点 HTML 的 XDef 定义了一个标准 HTML 的简单子集, 仅用于满足对站点入口页面的定制化需求:

/duzhou/schema/web/html.xdef
<html xmlns:x="/nop/schema/xdsl.xdef"
xmlns:xdef="/nop/schema/xdef.xdef"
x:schema="/nop/schema/xdef.xdef"
xdef:unknown-attr="string"
>

<xdef:define
name="!string"
xdef:name="NormalNode"
xdef:unique-attr="name"
xdef:value="string"
xdef:unknown-attr="string" />

<head>
<meta name="!string" xdef:unique-attr="name"
xdef:unknown-attr="string" />

<title xdef:value="string" />
<!-- 在 head 中仅内嵌基础的 css,并外链站点图标 -->
<style name="!string" xdef:ref="NormalNode" />
<link name="!string" xdef:ref="NormalNode" />
</head>

<body xdef:unknown-attr="string">
<!-- 仅支持差量定制 div 节点,定制时,其余 html 节点均需放在 div 中 -->
<div id="!string" xdef:unique-attr="id"
xdef:value="xml"
xdef:unknown-attr="string" />
<link name="!string" xdef:ref="NormalNode" />
<script name="!string" xdef:ref="NormalNode" />
</body>
</html>

在 DSL 中 stylelinkscript 等节点都通过 name 进行定位(div 通过 id 定位), 所以,在同一父节点下,各子节点的定位属性值需保持唯一性。

以上 XDef 结构解析出的 DSL 模型对象与真实的 HTML 结构之间存在一定的差异, 比如,div 节点的内容内会被包裹在以 body 作为该 div 的子节点的节点内:

<body>
<div id="tips" class="tips">
<span>This is just a tip.</span>
</div>
</body>

<!-- 以上站点的入口 HTML 页面结构,会生成以下 HTML 内容 -->
<body>
<div id="tips" class="tips">
<body id="tips" class="tips">
<span>This is just a tip.</span>
</body>
</div>
</body>

所以,需要自定义对该 DSL 模型对象到 XNode 的转换逻辑, 以去掉冗余的包裹层和无关的节点属性:

WebDslModelHelper.java
public class WebDslModelHelper {

public static XNode toHtmlNode(
String xdefPath, Object model
) {
IObjMeta objMeta = SchemaLoader.loadXMeta(xdefPath);

XNode node = new XNodeTransformer(objMeta)
.transformToXNode(model);
node.removeAttr(XDslKeys.DEFAULT.SCHEMA);
node.removeAttr("xmlns:x");

return node;
}

private static class XNodeTransformer
extends DslModelToXNodeTransformer {

public XNodeTransformer(IObjMeta objMeta) {
super(objMeta);
}

protected void addToNode(
IObjSchema schema, XNode node,
Object map, String key, Object value
) {
IObjPropMeta propMeta = schema.getProp(key);

if (propMeta != null
&& "body".equals(key)
&& value instanceof XNode
// xdef:value 为 xml 的节点中的内容都是挂载到
// 假节点上的
&& ((XNode) value).isDummyNode()
) {
// Note:子节点和文本的添加顺序不影响二者的定义顺序,
// 在输出 xml 时,相对顺序不会发生变化
node.appendContent(((XNode) value).content());
node.appendChildren(((XNode) value).detachChildren());
}
// 忽略以 xmlns: 开头的属性
else if (!key.startsWith("xmlns:")) {
super.addToNode(schema, node, map, key, value);
}
}
}
}

若是需要定制站点的入口页面,可以直接在 Delta 层创建同名文件,并采用 super 继承模式即可:

_vfs/_delta/v2.0/duzhou/web/app.site-html.xml
<html xmlns:x="/nop/schema/xdsl.xdef" xmlns:c="c"
x:schema="/duzhou/schema/web/html.xdef"
x:extends="super"
lang="zh_CN">

<head>
<title>
<x:gen-extends>
<c:script><![CDATA[
import io.crazydan.duzhou.framework.schema.RunInEnv;

// Note:运算符 += 只能用于数字运算
let suffix = '';
if ($site.runInEnv == RunInEnv.development) {
suffix = ' (开发中...)';
} else if ($site.runInEnv == RunInEnv.testing) {
suffix = ' (测试中...)';
}

const title = $site.subTitle + ' - ' + $site.title + suffix;
]]></c:script>
<!--
任意标签名称均可,最终都是将该标签内的子节点与
`x:gen-extends` 父节点的子节点做合并
-->
<_>${title}</_>
</x:gen-extends>
</title>
</head>

<body>
<div id="tips" class="tips">
<span>This is just a tip.</span>
</div>
</body>
</html>

*.site-html.xml 中的 xpl 脚本中只能引用站点对象的全局变量 $site