【转】了解使用 ASP.NET AJAX 进行局部页面更新

简介

Microsoft的 ASP.NET 技术提供了一个面向对象、事件驱动的编程模型,并将其与已编译代码的优势结合起来。但其服务器端的处理模型仍存在技术本身所固有的几点不足:

  • 进行页面更新需要往返服务器,因此需要页面刷新;
  • 来回往返不会保留 Javascript 或其他客户端技术(如 Adobe Flash)生成的任何效果。
  • 在回传过程中,除 Microsoft Internet Explorer 之外的浏览器都不支持自动存储滚动位置。而即使在Internet Explorer 中,页面刷新时仍然会出现闪屏现象。
  • 回传可能会导致占用较多的带宽,这是因为 __VIEWSTATE 表单字段可能会变大,尤其是在处理 GridView 或 Repeater 等控件时。
  • 没有统一的模型用于通过JavaScript或其他客户端技术访问 Web 服务。

进入 Microsoft 的 ASP.NET AJAX 扩展。AJAX 的全称为 Asynchronous JavaScript And XML(异步 JavaScript 和XML),它是一个集成框架,用于通过跨平台的JavaScript 提供增量页面更新。AJAX 包括含有Microsoft AJAX Framework的服务器侧代码,以及一个名为 Microsoft AJAX Script Library 的脚本组件。ASP.NET AJAX 扩展还跨平台支持通过JavaScript 访问ASP.NET Web 服务。

本白皮书将深入探讨 ASP.NET AJAX Extensions的局部页面更新功能,包括ScriptManager 组件、UpdatePanel 控件及UpdateProgress控件,以及适合及不适合应用它们的场景。

本白皮书基于 Visual Studio 2008的Beta 2 版本和.NET Framework 3.5。.NET Framework 3.5将ASP.NET AJAX Extensions集成到了基础类库中(之前它是 ASP.NET 2.0 的一个插件组件)。本白皮书还假定您使用的是Visual Studio 2008 而非Visual Web Developer Express Edition。因为本教程引用的某些项目模板对于Visual Web Developer Express 用户可能是不可用的。

局部页面更新

能够进行局部或增量页面更新,而无需执行向服务器的完全回传,也无需更改代码,只需要做很少的标记更改。这可能算得上ASP.NET AJAX Extensions 最显而易见的特性了。其优势有很多:不会改变您的多媒体状态(如Adobe Flash 或 Windows Media,减少了带宽成本,且客户端也不会出现通常与回传有关的闪屏现象。

局部页面解析功能被集成到ASP.NET 中,并对您的项目做到最小程度的更改。

演练:将局部解析集成到现有项目中
  1. 在 Microsoft Visual Studio 2008 中新建一个 ASP.NET Web Site项目:打开 File –> New –> Web Site…,在对话框中选择 ASP.NET Web Site。您自己为项目命名,Location选择“文件系统”或者IIS 都可以。
  2. 这时会出现一个空白默认页面,以及基本 ASP.NET 标记(一个服务器侧表单以及一个@Page指令)。在页面的表单元素中,添加一个名为 Label1的 Label 以及一个名为 Button1的 Button。您可自行设定它们的文本属性。
  3. 在设计视图中双击 Button1以生成一个代码文件 event handler。在此 event handler 中将 Label1.Text设为 "You clicked the button!"。

程序列表1:启用局部解析之前 default.aspx的标记

程序列表2:default.aspx.cs中的代码文件

  1. 按下 F5 打开您的网站。Visual Studio 将提示您添加一个 web.config 文件以启用调试。照此提示操作。注意当您单击按钮时,页面将刷新以更改标签中的文本,刷新页面时会出现短暂的闪屏现象。
  2. 关闭浏览器窗口后,返回 Visual Studio 和标记页面。在 Visual Studio 工具箱中向下滚动,找到标记为AJAX Extensions 的选项卡。(如果由于您使用的是AJAX 或 Atlas 扩展的早期版本而没有此选项卡,请参考本白皮书稍后部分中关于注册AJAX Extensions 工具箱项的过程的内容,或使用网络上可下载的Windows Installer 安装新版本。

已知问题:如果您的计算机上已经安装了带有ASP.NET 2.0 AJAX Extensions 的 Visual Studio 2005,那么当您安装 Visual Studio 2008 Beta 2时,Visual Studio 2008将导入 AJAX Extensions工具箱项。您可以查看组件的工具提示来确定自己计算机上的软件版本,版本应为Version 3.5.0.0。如果显示版本为 Version 2.0.0.0,说明您已经导入了原有的工具箱项,并且需要使用Visual Studio 的 Choose Toolbox Items 对话框来手动导入。不能通过设计器添加Version 2 控件。

  1. <asp:Label>标记开始之前,创建一个空白行,并双击工具箱中的 UpdatePanel 控件。注意,页面顶部包含了一个新的@Register 指令,表示应使用 asp:prefix 导入 System.Web.UI 命名空间中的控件。
  2. </asp:UpdatePanel>结束标记拖过 Button元素的末尾,以使元素有效包装 Label和 Button控件。
  3. <asp:UpdatePanel>标记之后添加一个新标记。注意,IntelliSense 将提示有两个选项。此处请选择创建<ContentTemplate> 标记。确保用此标记包括您的Label 和 Button控件,以使标记有效。
  1. <form>元素中的任何位置,双击工具箱中的 ScriptManager项,以包含一个 ScriptManager 项。
  2. 编辑 <asp:ScriptManager>标记以使其包含 EnablePartialRendering=”true”属性。

程序列表3:启用局部解析的default.aspx的标记

  1. 打开 web.config 文件。注意,Visual Studio 自动添加了一个指向 System.Web.Extensions.dll的编译引用。

Visual Studio 2008 的新特性:ASP.NET 网站项目模板附带的 web.config 自动包含了所有必需的到ASP.NET AJAX Extensions 的引用,且包含配置信息中可删除注释以启用附加功能的加注释部分。在安装了ASP.NET 2.0 AJAX Extensions后,Visual Studio 2005 也拥有相似的模板。但在 Visual Studio 2008 中,AJAX Extensions 默认是被引用的,但是可以选择其不被引用。

  1. 按下 F5 启动您的网站。注意,对局部解析的支持并不要求进行任何源代码更改,只更改标记。

启动您的网站时,应该看见现在已经启用了局部解析。因为您单击按钮时没有出现闪屏现象,页面的滚动位置也没有更改(示例并未说明这点)。如果您要在单击按钮后查看页面的被解析过的源代码文件,将可以确认实际上没有产生回传:原有的标签文本仍是源标记的一部分,标签是通过JavaScript 更改的。

Visual Studio 2008 Beta 2并没有自带ASP.NET AJAX-Enabled网站模板。但是在Visual Studio 2005中提供了这样的模板,当然,前提条件是你安装了 Visual Studio 2005和ASP.NET 2.0 AJAX Extensions。因此,配置网站和从 AJAX-Enabled Web Site 模板开始似乎要简单一些,因为模板默认包含一个经过完全配置的web.config 文件(支持所有 ASP.NET AJAX Extensions,包括Web 服务访问和 JSON 序列化:JavaScript Object Notation),且Web 表单主页面中包含一个 UpdatePanel 和 ContentTemplate。为这样一个默认页面启用局部解析非常简单,只需要重新执行本过程的步骤10 并将需要的空间拖拽到页面中即可。

ScriptManager 控件ScriptManager 控件引用

启用标记的属性:

属性名称

类型

说明

AllowCustomErrors-Redirect

Bool

指定是否使用 web.config 文件的定制错误部分来处理错误。

AsyncPostBackError-Message

String

返回或设置在出现错误时向客户端发送的错误消息。

AsyncPostBack-Timeout

Int32

返回或设置客户端应等待异步请求完成的默认时间。

EnableScript-Globalization

Bool

返回或设置是否启用脚本全球化。

EnableScript-Localization

Bool

返回或设置是否启用脚本本地化。

ScriptLoadTimeout

Int32

确定向客户端载入脚本允许的秒数。

ScriptMode

Enum (Auto, Debug, Release, Inherit)

返回或设置是否呈现脚本的版本号。

ScriptPath

String

返回或设置要向客户端发送的脚本文件的位置的根路径。

只用代码实现的属性:

属性名称

类型

说明

AuthenticationService

AuthenticationService-Manager

返回关于要向客户端发送的ASP.NET Authentication Service 代理的详细信息。

IsDebuggingEnabled

Bool

返回是否启用代码调试。

IsInAsyncPostback

Bool

返回关于页面当前是否处于异步回传请求状态的信息。

ProfileService

ProfileService-Manager

返回关于要向客户端发送的ASP.NET配置文件服务代理的详细信息。

Scripts

Collection<Script-Reference>

返回一个要向客户端发送的脚本引用集合。

Services

Collection<Service-Reference>

返回一个要向客户端发送的Web服务器代理引用的集合。

SupportsPartialRendering

Bool

返回关于当前客户端是否支持局部解析的信息。如果属性返回false,那么所有页面请求都是标准回传。

公共代码方法:

方法名称

类型

说明

SetFocus(string)

Void

将客户端在请求完成时的焦点设置为某个特定控件。

标记子项:

标签

说明

<AuthenticationService>

提供关于代理 ASP.NET 验证服务的详细信息。

<ProfileService>

提供关于代理ASP.NET 配置文件服务的详细信息。

<Scripts>

提供额外的脚本引用。

<asp:ScriptReference>

表示某个特定的脚本引用。

<Service>

提供将有代理类生成的额外的Web服务引用。

<asp:ServiceReference>

表示某个特定的 Web 服务引用。

ScriptManager 控件是 ASP.NET AJAX Extensions根本核心。它提供对脚本库(包括大量客户端脚本类型系统)、支持局部解析的访问、并提供支持大量额外的ASP.NET 服务(如验证和配置文件服务,以及其他Web 服务)。此外,ScriptManager 控件还提供队客户端脚本的全球化和本地化支持。

提供变更和补充脚本

由于 Microsoft ASP.NET 2.0 AJAX Extensions 的调试版本和发布版本都在引用程序集中嵌入了完整的脚本代码,开发者可以随意重定向ScriptManager 到定制脚本文件,或是注册所需的其他脚本。

通过注册 ScriptManager 类的ResolveScriptReference事件,可以覆写常规包含的脚本(如支持 Sys.WebForms 命名空间及定制分类系统)的默认绑定关系。此方法被调用时,event handler 可以更改指向相关脚本文件的路径。然后脚本管理器将向客户端发送一个其他或定制的副本。

此外,可以通过编码或通过标记包含脚本引用(以ScriptReference类表示)。操作方法为:通过编码更改ScriptManager.Scripts 集合,或将 <asp:ScriptReference>标记插入 <Scripts> 标记下方。<Scripts> 标记是 ScriptManager 控件的一级子标记。

定制 UpdatePanels 的错误处理

更新是通过由 UpdatePanel 控件指定的触发器处理的。但对错误处理和定制错误消息的支持则是由页面的ScriptManager 控件实例处理的。它通过将一个AsyncPostBackError事件暴露到稍后将提供定制异常处理逻辑的页面中来实现。

您可以通过使用AsyncPostBackError事件指定AsyncPostBackErrorMessage属性,然后将触发一个警告框,使其在回调结束时显示。

如果不使用默认的警告框,可以在客户端进行定制。例如,您可能想要显示一个定制的<div> 元素,而不是默认的浏览器模式对话框,这样就可在客户端脚本中处理错误。

程序列表 5:显示定制错误的客户端脚本

上述脚本只是向客户端 AJAX 运行时注册了一个回调,要求返回异步请求完成的时间。然后检查是否报告了任何错误。如果有,处理错误的详细信息,并最终向运行时显示错误已在定制脚本中解决。

全球化和本地化支持

ScriptManager 控件提供了广泛的脚本字符串和用户界面组件的本地化支持,但这些内容不在本白皮书范围之内。更多信息请参见《ASP.NET AJAX Extensions 的全球化支持》白皮书。

UpdatePanel 控件UpdatePanel 控件引用

启用标记的属性:

属性名称

类型

说明

ChildrenAsTriggers

bool

指定子控件是否根据回传自动调用刷新。

RenderMode

enum (Block, Inline)

指定将内容显示为可见的方式。

UpdateMode

enum (Always, Conditional)

指定是否总是在局部解析时刷新UpdatePanel,还是只在触发器被触发时刷新。

只用代码实现的属性:

属性名称

类型

说明

IsInPartialRendering

bool

返回 UpdatePanel 是否为当前请求支持局部解析。

ContentTemplate

ITemplate

返回更新请求的标记模板。

ContentTemplateContainer

Control

返回更新请求的编程模板。

Triggers

UpdatePanel-
TriggerCollection

返回与当前 UpdatePanel 关联的触发器的列表。

公共代码方法:

方法名称

类型

说明

Update()   

Void   

通过编码更新指定 UpdatePanel。允许某个服务器请求触发某个未通过其他方式触发UpdatePanel的局部解析。

标记子项:

标签

说明

<ContentTemplate>

指定要用于呈现局部解析结果的标记。 <asp:UpdatePanel> 的子元素。

<Triggers>

指定一个包含 n 个与更新此 UpdatePanel 关联的控件的集合。<asp:UpdatePanel>的子元素。

<asp:AsyncPostBackTrigger>

指定调用特定UpdatePanel 的局部页面呈现的触发器。该触发器可能是有问题的UpdatePanel的一个子代控件,也可能不是。精确到事件的<Triggers>的 name.Child。

<asp:PostBackTrigger>

指定一个引发整个页面更新的控件。该控件可能是有问题的UpdatePanel的一个子代控件,也可能不是。精确到事件的<Triggers>的 object.Child。

UpdatePanel控件用于分隔服务器端的将参与AJAX Extensions 的局部解析功能的内容。一个页面上的 UpdatePanel 控件数量没有限制,而且也可以嵌套。每个 UpdatePanel 都是孤立的,以便都能独立工作(您可以同时运行两个UpdatePanel,解析页面的不同部分,与页面的回传无关)。

UpdatePanel 控件主要用于处理控件触发器:在默认情况下,UpdatePanel 的 ContentTemplate包含的任何创建回传的控件都注册为UpdatePanel的一个触发器。也就是说,UpdatePanel 可以与默认的数据绑定控件(如GridView)一起运行,也可以在脚本中通过编程实现。

默认情况下,当触发一个局部页面解析时,页面上的所有 UpdatePanel 都将刷新,而不论这些UpdatePanel 控件是否定义了此动作的触发器。例如,如果某个UpdatePanel 定义了一个 Button 控件,那么单击此 Button 控件时,该页面上的所有UpdatePanel 控件都将被默认刷新。这是因为UpdatePanel 的 UpdateMode 属性被默认设为 Always。您也可以将 UpdateMode 的属性改为 Conditional,这样 UpdatePanel 将只在触发特定触发器时才会被刷新。

定制控件注释

可以向任何用户控件或定制控件添加 UpdatePanel。但这些控件所在的页面必须包含一个 ScriptManager 控件,且其EnablePartialRendering属性被设为 true

使用 Web Custom Controls时,添加UpdatePanel 的一个方法就是覆写CompositeControl类的受保护的 CreateChildControls()方法。这样,如果您确定页面支持局部解析,就可以向控件的子控件和外部之间插入一个UpdatePanel;否则,您可以直接将子控件放入一个容器Control 实例中。

UpdatePanel 注意事项

UpdatePanel 的运行类似于一个黑匣子,它在 JavaScript XMLHttpRequest的内部将 ASP.NET 回传进行包装。但其在行为和速度方面的性能都有一些重要事项需要牢记于心。要了解UpdatePanel 的原理以便能从最佳的角度决定其使用,就应检查AJAX 交换。下面的示例使用一个现有的网站以及安装了Firebug 扩展的 Mozilla Firefox (Firebug 获取 XMLHttpRequest 数据)。

试想在一个表单中,除了其他元素还有一个邮政编码文本框,用于填充表单或控件上的城市和国家字段。此表单最后将收集成员信息,包括用户的姓名、地址及联系方式。根据特定项目的不同要求,设计时有多个事项需要考虑。

在该应用的原迭代中,建立了一个控件,包含完整的用户注册数据,其中就有邮政编码、城市和国家信息。整个控件都包装在一个UpdatePanel 中,并被拖放到一个 Web 表单上。用户输入邮政编码时,UpdatePanel 检测到事件(后端对应的TextChanged 事件,通过指定触发器或使用设为true 的ChildrenAsTriggers 属性实现)。AJAX 将所有字段放入 UpdatePanel,这些字段就是 FireBug 获取的数据(参见右面的图示)。

如屏幕截图所示,UpdatePanel中每个控件的值以及ViewState字段都被传递了(本例中都为空)。发送的数据总共有9kb,而产生此特定请求需要的数据只有5 字节。而响应则更大:总共有57kb 发送到客户端,作用仅是更新一个文本字段和一个下拉字段。

来看看 ASP.NET AJAX 如何更新显示也许很有意思。UpdatePanel 的更新请求的响应部分是在左面的 Firebug 控制台显示上显示的。这是一个专门生成、用竖线分隔的字符串,由客户端脚本打乱顺序,然后在页面上重新组合起来。ASP.NET AJAX 特地在表示您的 UpdatePanel 的客户端上设置 HTML 元素的 innerHTML 属性。由于浏览器要重新生成DOM,将会有短暂延迟,时间根据需要处理的信息量而定。

重新生成 DOM 将触发多个其他事件:

  • 如果焦点 HTML 元素是在 UpdatePanel 中的,将成为非焦点元素。因此,按下Tab 键以退出邮政编码文本框的用户,接下来将进入City 文本框。但一旦 UpdatePanel 刷新了显示,表单就不会有焦点,按下Tab 键将开始突出显示焦点元素(如链接)。
  • 如果有任何类型的定制客户端脚本处于使用中,且使用过程需要访问 DOM 元素,那么功能所保存的引用可能在局部回传后消失。

UpdatePanel 并不是能解决一切问题的办法。它们只是为特定情况提供快速解决方案,包括建模、小控件更新等,并为熟悉.NET 对象模型但不熟悉DOM 的ASP.NET 开发者提供一个熟悉的界面。根据不同的应用场景,有多种办法可用于提升性能:

  • 如果开发者考虑使用 PageMethods 和 JSON (JavaScript 对象注解),可以如同调用 Web 服务一样在页面上调用静态方法。由于这些方法是静态的,因此不需要状态。脚本调用程序提供参数,将返回异步结果。
  • 如果需要在同一个应用程序的多个不同位置使用某一个控件,则考虑使用一个 Web 服务和 JSON。这也需要少量的额外工作,且是异步运行的。

通过 Web 服务或页面方法集成功能也有其缺点。首先最重要的是,ASP.NET 开发者通常倾向于在用户控件(.ascx files)中建立小的功能组件。页面方法不能包含在这些文件中,只能包含在实际的.aspx 页面类中。而与此相似,Web 服务也必须包含在 .asmx 类中。根据应用程序不同,这种体系结构可能与单个职责原则相冲突。因为单个组件的功能现在跨越了联系很小甚至没有的两个或多个物理组件。

最后,如果应用程序要求使用UpdatePanel,下面这些原则将有助于故障排除和维护。

  • 尽量不嵌套UpdatePanel,不论是在代码单元内部嵌套或跨代码单元嵌套都应避免。例如,页面上有一个包装了一个Control 的 UpdatePanel,而此 Control 也包含一个 UpdatePanel,后者又包含另一个 Control,此 Control 又包含 一个 UpdatePanel,则是跨单元嵌套。这将有助于搞清楚哪些元素应被刷新,避免出现对子UpdatePanel 的意外刷新。
  • 保持ChildrenAsTriggers属性设为false,且明确设置触发事件。利用 <Triggers>集合可以更清楚地处理事件,且能避免不必要的操作,从而有助于维护任务的执行,且促使开发者“决定参与”某个事件。
  • 尽可能使用最小的单元来实现功能。正如邮政编码服务示例中的讨论所示,只包装最少的内容可以减少到服务器的时间、总处理以及占用的客户端-服务器交换,从而提升性能。
UpdateProgress控件UpdateProgress控件引用

标记启用的属性:

属性名称

类型

说明

AssociatedUpdate-PanelID

String

指定此 UpdateProgress应对其报告的 UpdatePanel 的 ID。

DisplayAfter

Int

指定在异步请求开始后到显示控件前的超时毫秒数。

DynamicLayout

bool

指定是否动态呈现进度。

子代标记:

标签

说明

<ProgressTemplate>

包含将用此控件显示的内容的控件模板集。

UpdateProgress控件可用于进行反馈,以便在向服务器传输时保持用户的兴趣。它可以帮助您的用户知道您正在执行某项操作,这项操作甚至会是不太直观的。在大多数用户已经习惯了页面更新和查看状态栏高亮显示的情况下,这尤其有用。

注意,UpdateProgress控件可以在页面层级结构中的任何位置显示。但如果局部回传是从某个子 UpdatePanel (嵌套在另一个 UpdatePanel 中的一个UpdatePanel)发起的,触发此子 UpdatePanel 的触发器将显示子UpdatePanel 以及父UpdatePanel 的UpdateProgress模板。但如果触发器是UpdatePanel 的一个直接子UpdatePanel,那么只显示与父UpdatePanel 相关联的UpdateProgress模板。

小结

Microsoft ASP.NET AJAX 扩展属于尖端产品,旨在协助以使 web 内容更易于访问,并为您的 web 应用程序提供更丰富的用户体验。作为ASP.NET AJAX Extensions 的一部分,局部页面呈现控件,包括ScriptManager、UpdatePanel 和UpdateProgress 控件,都是工具箱中最明显的一些组件。

ScriptManager 组件集成了 Extensions 的客户端 JavaScript 提供功能,并启用了多种服务器端和客户端组件,将开发成本减到最低。

UpdatePanel 控件是明显的“魔法箱”:UpdatePanel 中的标记可以有服务器端的Codebehind且不触发页面刷新。UpdatePanel 控件可以嵌套,也可与其他UpdatePanel 中的控件相关联。默认情况下,UpdatePanel 对所有其子代控件调用的回传都进行处理,但您也可声明或通过编码对其进行调整。

使用 UpdatePanel 控件时,开发者应了解可能产生的性能影响。可能的替代方案包括web 服务和页面方法,但也应考虑到应用程序的设计问题。

UpdateProgress控件可以使用户知道操作仍在进行中,以及在页面未对用户输入进行任何响应时,幕后的请求正在执行。它还提供了放弃局部解析结果的功能。

通过降低服务器的运行对用户的可见性以及保持工作流的连续性,这些工具共同创建丰富无缝的用户体验。

快乐编程!

<script type=”text/javascript”> <!-- Sys.WebForms.PageRequestManager.getInstance().add_EndRequest(Request_End); function Request_End(sender, args) { if (args.get_error() != undefined) { var errorMessage = “”; if (args.get_response().get_statusCode() == “200”) { errorMessage = args.get_error().message; } else { // the server wasn’t the problem... errorMessage = “An unknown error occurred...”; } // do something with the errorMessage here. // now make sure the system knows we handled the error. args.set_errorHandled(true); } } // --></script> <%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %><%@ Register Assembly="System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" Namespace="System.Web.UI" TagPrefix="asp" %><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html > <head runat="server"> <title>Untitled Page</title> </head> <body> <form id="form1" runat="server"> <asp:ScriptManager EnablePartialRendering="true" ID="ScriptManager1" runat="server"></asp:ScriptManager> <div> <asp:UpdatePanel ID="UpdatePanel1" runat="server"> <ContentTemplate> <asp:Label ID="Label1" runat="server" Text="This is a label!"></asp:Label> <asp:Button ID="Button1" runat="server" Text="Click Me" OnClick="Button1_Click" /> </ContentTemplate> </asp:UpdatePanel> </div> </form> </body></html> public partial class _Default : System.Web.UI.Page{ protected void Button1_Click(object sender, EventArgs e) { Label1.Text = "You clicked the button!"; }} <%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html > <head runat="server"> <title>Untitled Page</title> </head> <body> <form id="form1" runat="server"> <div> <asp:Label ID="Label1" runat="server" Text="This is a label!"> </asp:Label> <asp:Button ID="Button1" runat="server" Text="Click Me" OnClick="Button1_Click" /> </div> </form> </body></html>
上一篇:SpringBoot整合Freemarker模板引擎遇到的(坑)


下一篇:基于NET Framework使用阿里云AMQP