Как известно, поисковики не очень любят динамически создаваемые страницы, а страница (приложение) GWT как раз является динамической.
Чтобы поисковый робот смог проиндексировать некий контент, этот контент должен быть доступен в html странице на момент ее загрузки, а в типичных GWT приложениях контент запрашивается с сервера путем RPC запроса и формирует интерфейс.
Для обхождения этой проблемы может быть применен способ сериализации в страницу того контента, который запрашивается у сервера при загрузке приложения.
Дополнительный бонус данного способа — ускорения старта приложения, т.к. нет необходимости ходить на сервер за данными.
Принцип действия такой:
- Для DTO, которые передаются с сервера, добавляется дополнительный метод(например, путем добавления абстрактного родительского класса, который преобразует содержимое в вид, предпочтительный для поисковиков.
public abstract class GWTBootstrapDTO implements IsSerializable { public abstract String getBootstrap(); }
- При формировании хост-страницы на сервере, делается запрос к сервлету с RPC интерфейсом и полученный контент сериализуется (о сериализации чуть позже) непосредственно в страницу.
- Далее в сформированную страницу в тег <noscript> загружается контент для индексирования.
- После того, как страница была получена пользователем, стартует приложение, а все первоначальные данные десериализуются из хост-страницы.
- Данные для поисковиков так же присутствуют в странице и доступны для индексирования.
Исходные данные.
Сервис:
package com.oshift.ui.client.forummanamegemt;
import com.google.gwt.user.client.rpc.RemoteService;
import com.google.gwt.user.client.rpc.RemoteServiceRelativePath;
import com.oshift.ui.shared.forum.GWTForum;
import java.util.ArrayList;
@RemoteServiceRelativePath("forummanagementservice")
public interface ForumManagementService extends RemoteService {
public ArrayList<GWTForum> getForums();
}
Сервлет:
package com.oshift.ui.server.forummanamegemt;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;
import com.oshift.ui.client.forummanamegemt.ForumManagementService;
import com.oshift.ui.shared.forum.GWTForum;
import com.oshift.ws.db.forummanagement.Forum;
import com.oshift.ws.db.forummanagement.Mapper;
import com.oshift.ws.ejb.ForumManagerBean;
import java.util.ArrayList;
import javax.ejb.EJB;
import javax.servlet.ServletException;
public class ForumManagementServiceImpl extends RemoteServiceServlet implements ForumManagementService {
public static ForumManagementServiceImpl instance = null;
//сохраняем ссылку на сервлет
@Override
public void init() throws ServletException {
super.init();
instance = this;
}
//каким-то образом нужно получить данные, например, с помощью EJB
@EJB
private ForumManagerBean forumBean;
@Override
public ArrayList<GWTForum> getForums() {
ArrayList<GWTForum> res = new ArrayList<GWTForum>();
for (Forum f : forumBean.getForums()) {
GWTForum toGwt = Mapper.ForumMap.toGwt(f);
res.add(toGwt);
}
return res;
}
}
Объекты DTO для сериализации:
*Важный момент, для избавления себя от мук настройки SerializationPolicy, данные объекты реализуют интерфейс IsSerializable.
package com.oshift.ui.shared.forum;
import com.google.gwt.safehtml.shared.SafeHtml;
import com.google.gwt.safehtml.shared.SafeHtmlUtils;
import com.google.gwt.user.client.rpc.IsSerializable;
import java.util.ArrayList;
public class GWTForum extends GWTBootstrapDTO implements IsSerializable {
public String forumName = "просто форум";
public ArrayList<GWTTopic> topics = new ArrayList<GWTTopic>();
public GWTForum() {
}
@Override
public String getBootstrap() {
String res = "Forum name: " + forumName + "<br>";
for (GWTTopic t : topics) {
res += t.getBootstrap();
}
//эскейп, мало ли что там
SafeHtml fromString = SafeHtmlUtils.fromString(res);
return fromString.asString();
}
}
package com.oshift.ui.shared.forum;
import com.google.gwt.safehtml.shared.SafeHtml;
import com.google.gwt.safehtml.shared.SafeHtmlUtils;
import com.google.gwt.user.client.rpc.IsSerializable;
public class GWTTopic extends GWTBootstrapDTO implements IsSerializable {
public String topicName = "";
@Override
public String getBootstrap() {
String res = "Название топика: " + topicName + "</br>";
SafeHtml fromString = SafeHtmlUtils.fromString(res);
return fromString.asString();
}
}
Процесс сериализации на стороне сервера(ради простоты, непосредственно в jsp):
<%@page import="com.google.gwt.safehtml.shared.SafeHtmlUtils"%>
<%@page import="com.oshift.ws.ejb.ForumManagerBean"%>
<%@page import="javax.ejb.EJB"%>
<%@page import="java.lang.reflect.Method"%>
<%@page import="com.oshift.ui.client.forummanamegemt.ForumManagementService"%>
<%@page import="com.google.gwt.user.server.rpc.RPC"%>
<%@page import="java.util.ArrayList"%>
<%@page import="com.oshift.ui.shared.forum.GWTForum"%>
<%@page import="com.oshift.ui.server.forummanamegemt.ForumManagementServiceImpl"%>
<%@page contentType="text/html" pageEncoding="UTF-8"%>
<%
//получаем сервис
ArrayList<GWTForum> forumsList = ForumManagementServiceImpl.instance.getForums();
//получаем сервисный метод
Method m = ForumManagementService.class.getMethod("getForums");
//получаем сериализованный экранированный контент для вставки в хост-страницу
String forums = SafeHtmlUtils.fromString(RPC.encodeResponseForSuccess(m, forumsList)).asString();
//получаем контент для noscript тега
StringBuilder noscriptSb = new StringBuilder();
for (GWTForum f : forumsList) {
noscriptSb.append(f.getBootstrap());
}
String noscript = noscriptSb.toString();
%>
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" language="javascript">
var forums='<%=forums%>';
</script>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Forums index</title>
<link href="res/gfStyle.css" type="text/css" rel="stylesheet"/>
<link href="res/login.css" type="text/css" rel="stylesheet"/>
<meta name='gwt:module' content='com.oshift.ui.index=com.oshift.ui.index'>
<script type="text/javascript" src="com.oshift.ui.index/com.oshift.ui.index.nocache.js"></script>
</head>
<body>
...
<noscript>
<%=noscript%>
</noscript>
Процесс десериализации на стороне клиента:
package com.oshift.ui.client;
import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.core.shared.GWT;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.rpc.SerializationException;
import com.google.gwt.user.client.rpc.SerializationStreamFactory;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.Widget;
import com.oshift.ui.client.forummanamegemt.ForumManagementServiceAsync;
import com.oshift.ui.client.mainpage.ForumComposite;
import com.oshift.ui.shared.forum.GWTForum;
import java.util.ArrayList;
public class IndexEntryPoint implements EntryPoint {
//Сервис
private final ForumManagementServiceAsync svc = ServicesFactory.getForumManagementServiceAsync();
@Override
public void onModuleLoad() {
//Необходимо сделать unescape для сериализованного контента
String forums = new HTML(getForums()).getText();
SerializationStreamFactory ssf = (SerializationStreamFactory) svc;
try {
ArrayList<GWTForum> readObject = (ArrayList<GWTForum>) ssf.createStreamReader(forums).readObject();
for (GWTForum f : readObject) {
//делаем что нам нужно с полученным объектом
process(f);
}
} catch (SerializationException ex) {
//Обработка ошибки
Window.alert("Не удалось десериализовать со страницы форумы для отображения: " + ex);
}
...продолжаем делать что обычно
}
//Получаем сериализованный контент со страницы
private native String getForums()/*-{
return eval("$wnd.forums");
}-*/;
}
Сам контент на странице выглядит так:
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" language="javascript">
//эти данные заэскейплены, поверьте.
var forums='//OK[7,4,6,4,5,4,3,1,3,2,1,1,["java.util.ArrayList/4159755760","com.oshift.ui.shared.forum.GWTForum/1236332786","форум1","com.oshift.ui.shared.forum.GWTTopic/1653537274","некий топик1","некий топик2","некий топик3"],0,7]';
</script>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Forums index</title>
<link href="res/gfStyle.css" type="text/css" rel="stylesheet"/>
<link href="res/login.css" type="text/css" rel="stylesheet"/>
<meta name='gwt:module' content='com.oshift.ui.index=com.oshift.ui.index'>
<script type="text/javascript" src="com.oshift.ui.index/com.oshift.ui.index.nocache.js"></script>
</head>
<body>
...
<noscript>
Forum name: форум1<br>Название топика: некий топик1</br>Название топика: некий топик2</br>Название топика: некий топик3</br>
</noscript>
</div>
</body>
</html>
Надеюсь, моя статья станет кому-то полезной.
Автор: Agb