动态主机页面

Jason Hall,软件工程师

这是一个很常见的问题:你想要只向已登录的用户显示你的基于 GWT 的应用。在本文中,我们将探讨几种实现此目的的方法,并优先考虑那些高效利用网络的方法。

  1. 带有 RPC 的静态主机页面
  2. web.xml 中的安全约束
  3. Servlet 作为主机页面
  4. 基于模板的主机页面

带有 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()
  }
}

让我们看看如果用户未登录,这里会发生什么。

  1. 你的应用被请求,并且你的 GWT 主机页面 (YourModule.html) 被下载。
  2. module.nocache.js 被页面请求并下载。
  3. MD5.cache.html 根据浏览器选择并下载。
  4. 你的模块加载并进行一个 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 应用明显更快。