【已废弃】实现
- 文章作者: flytreeleft - flytreeleft@crazydan.org
- 文章链接: https://duzhou.crazydan.io/docs/development/framework/server/gateway/discard-tech
- 版权声明: 本文章采用许可协议《署名 4.0 国际 (CC BY 4.0)》。 转载或商用请注明来自渡舟平台!
根据 URL 生成站点入口页面
对站点入口页面的生成需满足如下要求:
- 需在 API 网关过滤器之后路由剩余请求,对匹配到的站点请求,返回站点入口页面, 对未匹配到的非静态资源请求,返回默认的站点页面(一般为 404)
- 支持对站点入口 HTML 页面的差量修订,从而按需实现定制化
按照 创建最小 Quarkus 应用服务启动器
的指导步骤可以创建出网关服务启动器 gateway-starter
,其已内置对
IHttpServerFilter
过滤器的扫描和加载,故而,在 gateway-web
模块中直接实现针对站点入口页面生成的过滤器即可:
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;
}
}
@Inject
对private
属性无效,其仅可标注在protected
或public
属性上,也可以直接标注在 setter 接口上。
在 WebSiteHttpServerFilter#doFilter
中会通过
WebSiteProvider
根据请求的路径去匹配已定义的站点,
若未匹配到站点,并且请求的也不是已存在的静态资源,则会尝试获取默认站点,
其以 *
为匹配条件,当存在 XWebSite#url
为 *
的站点时,
便返回该站点的页面。默认站点应该始终有定义,故而,在最后一步仍未得到
HTML 页面,则继续后续的静态资源路由。
通过 IHttpServerContext#executeBlocking
会以异步方式调用
WebSiteHttpServerFilter#doFilter
,以提高应用服务的并发能力。
WebSiteHttpServerFilter#order
决定了当前过滤器的优先级,其值越大,
其优先级越低。常量 GatewayConstants.PRIORITY_WEB_SITE_FILTER
是定义在 API 网关过滤器之后的一个数值。
站点页面由 WebSiteProvider
生成,其通过读取默认位置的 XWeb
的
DSL 得到全部站点的定义,再通过站点 HTML 生成模板得到其入口页面:
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.xml
和app.site-html.xml
的加载可采用 自定义 DSL 模型加载器 机制,以避免写死代码,从而提供更加灵活的定制能力。不过,需要注意的是,app.site-html.xml
的解析结果与全局变量$site
绑定,故而,不能缓存其解析结果。
注意,WebSiteProvider
与 WebSiteHttpServerFilter
需通过
Nop IoC 机制创建实例(详细说明见
Nop Beans 的创建与加载):
/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 的定义如下:
<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 的简单子集, 仅用于满足对站点入口页面的定制化需求:
<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 中
style
、link
、script
等节点都通过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
的转换逻辑,
以去掉冗余的包裹层和无关的节点属性:
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
继承模式即可:
<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
。