动态主机页面
Jason Hall,软件工程师
这是一个很常见的问题:你想要只向已登录的用户显示你的基于 GWT 的应用。在本文中,我们将探讨几种实现此目的的方法,并优先考虑那些高效利用网络的方法。
带有 RPC 的静态主机页面
一个常见的解决方案是在 EntryPoint 类的 onModuleLoad() 方法中调用一个 GWT-RPC 服务,以检查用户是否已登录。这会在 GWT 模块加载时立即启动一个 GWT-RPC 请求。
public void onModuleLoad() {
// loginService is a GWT-RPC service that checks if the user is logged in
loginService.checkLoggedIn(new AsyncCallback<Boolean> {
public void onSuccess(Boolean loggedIn) {
if (loggedIn) {
showApp();
} else {
Window.Location.assign("/login");
}
}
// ...onFailure()
}
}
让我们看看如果用户未登录,这里会发生什么。
- 你的应用被请求,并且你的 GWT 主机页面 (YourModule.html) 被下载。
- module.nocache.js 被页面请求并下载。
- MD5.cache.html 根据浏览器选择并下载。
- 你的模块加载并进行一个 GWT-RPC 调用,以检查用户是否已登录 - 由于他们没有登录,因此他们被重定向到登录页面。
这最多需要四次服务器请求(取决于缓存的内容)才能将你的用户发送到登录页面。并且步骤 3 包括下载你的整个 GWT 应用,只是为了将你的用户发送走。即使你利用了代码分割,也必须下载至少一部分代码才能检查用户是否已登录。
理想的解决方案是在用户经过身份验证后才提供你的 GWT 代码。也就是说,除非用户已登录,否则永远不要到达步骤 2。
web.xml 中的安全约束
一种实现方法是在 web.xml 中使用安全约束。例如,使用 Google App Engine,你可以定义一个安全约束,将对所有页面(包括静态 GWT 主机页面)的访问限制为已登录的 Google 帐户用户(参见安全性和身份验证)。如果用户未登录,App Engine 会将用户重定向到 Google 帐户登录页面。
Servlet 作为主机页面
另一种更强大的方法是从 Java servlet 而不是静态 HTML 页面提供你的 HTML 主机页面。这种更灵活的方法允许使用自定义身份验证方案,并且能够根据用户更改主机页面的内容。以下是一个简单的作为 servlet 编写的示例主机页面
public class GwtHostingServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.setContentType("text/html");
resp.setCharacterEncoding("UTF-8");
// Print a simple HTML page including a <script> tag referencing your GWT module as the response
PrintWriter writer = resp.getWriter();
writer.append("<html><head>")
.append("<script type=\"text/javascript\" src=\"sample/sample.nocache.js\"></script>")
.append("</head><body><p>Hello, world!</p></body></html>");
}
}
此 servlet 发送的响应将加载并执行你的 GWT 代码,就像它被引用在静态 HTML 主机页面中一样。但是,现在我们正在 servlet 中编写 HTML,因此我们可以根据请求更改正在提供的页面的内容。这让我们可以做一些更有趣的事情。
以下示例使用 App Engine 用户 API 来查看用户是否已登录。即使你没有使用 App Engine,你也可以想象这段代码在你的 servlet 环境中如何略有不同。
// In GwtHostingServlet's doGet() method...
PrintWriter writer = resp.getWriter();
writer.append("<html><head>");
UserService userService = UserServiceFactory.getUserService();
if (userService.isUserLoggedIn()) {
// Add a <script> tag to serve your app's generated JS code
writer.append("<script type=\"text/javascript\" src=\"sample/sample.nocache.js\"></script>");
writer.append("</head><body>");
// Add a link to log out
writer.append("<a href=\"" + userService.createLogoutURL("/") + "\">Log out</a>");
} else {
writer.append("</head><body>");
// Add a link to log in
writer.append("<a href=\"" + userService.createLoginURL("/") + "\">Log in</a>");
}
writer.append("</body></html>");
此 servlet 现在只会为已登录的用户提供你的 GWT 代码,并且将在页面上显示一个链接,用于登录或注销。
但我们还可以用这个动态托管 servlet 做更多有趣的事情。假设你想要将一些数据(例如用户的电子邮件地址)从 servlet 传递到 GWT 代码,以便在 GWT 模块加载时立即可用。
你可以在 onModuleLoad() 方法中进行一个 GWT-RPC 调用以获取此数据,但这意味着你正在进行一个请求来下载你的 GWT 模块,然后立即进行另一个请求来获取此数据。一种更有效的方法是将初始数据作为 Javascript 变量写入主机页面本身。
// In GwtHostingServlet's doGet() method...
writer.append("<html><head>");
writer.append("<script type=\"text/javascript\" src=\"sample/sample.nocache.js\"></script>");
// Open a second <script> tag where we will define some extra data
writer.append("<script type=\"text/javascript\">");
// Define a global JSON object called "info" which can contain some simple key/value pairs
writer.append("var info = { ");
// Include the user's email with the key "email"
writer.append("\"email\" : \"" + userService.getCurrentUser().getEmail() + "\"");
// End the JSON object definition
writer.append(" };");
// End the <script> tag
writer.append("</script>");
writer.append("</head><body>Hello, world!</body></html>");
现在你的 GWT 代码可以使用 JSNI 访问数据,如下所示
public native String getEmail() /*-{
return $wnd.info['email'];
}-*/;
或者,你可以利用 GWT 的Dictionary 类
public void onModuleLoad() {
// Looks for a JS variable called "info" in the global scope
Dictionary info = Dictionary.getDictionary("info");
String email = info.get("email");
Window.alert("Welcome, " + email + "!");
}
基于模板的主机页面
随着你的托管页面变得更加动态,可能需要考虑使用像 JSP 这样的模板语言,以使你的代码更易读。以下示例是作为 JSP 页面而不是 servlet 的示例
<!-- gwt-hosting.jsp -->
<html>
<head>
<%
UserService userService = UserServiceFactory.getUserService();
if (userService.isUserLoggedIn()) {
%>
<script type="text/javascript" src="sample/sample.nocache.js"></script>
<script type="text/javascript">
var info = { "email" : "<%= userService.getCurrentUser().getEmail() %>" };
</script>
</head>
<body>
<a href="<%= userService.createLogoutURL(request.getRequestURI()) %>">Log out</a>
<%
} else {
%>
</head>
<body>
<a href="<%= userService.createLoginURL(request.getRequestURI()) %>">Log in</a>
<%
}
%>
</body>
</html>
你可以通过在 web.xml 文件中指定它,将此 JSP 页面设置为你的欢迎文件
<welcome-file-list>
<welcome-file>gwt-hosting.jsp</welcome-file>
</welcome-file-list>
这些是通过动态托管你的 GWT 应用来最大程度地减少 HTTP 请求的一些基本示例。使用这些技术,你应该能够消除在模块加载时立即进行的 GWT-RPC 请求,这意味着用户等待时间更短,并且 GWT 应用明显更快。