Сериализация GWT RPC в запрашиваемую страницу для индексирования приложения поисковиками и ускорения загрузки

в 14:00, , рубрики: Google Web Toolkit, GWT, java, RPC, поисковая оптимизация, метки: , , ,

Как известно, поисковики не очень любят динамически создаваемые страницы, а страница (приложение) GWT как раз является динамической.

Чтобы поисковый робот смог проиндексировать некий контент, этот контент должен быть доступен в html странице на момент ее загрузки, а в типичных GWT приложениях контент запрашивается с сервера путем RPC запроса и формирует интерфейс.
Для обхождения этой проблемы может быть применен способ сериализации в страницу того контента, который запрашивается у сервера при загрузке приложения.
Дополнительный бонус данного способа — ускорения старта приложения, т.к. нет необходимости ходить на сервер за данными.

Принцип действия такой:

  • Для DTO, которые передаются с сервера, добавляется дополнительный метод(например, путем добавления абстрактного родительского класса, который преобразует содержимое в вид, предпочтительный для поисковиков.
    public abstract class GWTBootstrapDTO implements IsSerializable {
    
        public abstract String getBootstrap();
    }
    

  • При формировании хост-страницы на сервере, делается запрос к сервлету с RPC интерфейсом и полученный контент сериализуется (о сериализации чуть позже) непосредственно в страницу.
  • Далее в сформированную страницу в тег &ltnoscript&gt загружается контент для индексирования.
  • После того, как страница была получена пользователем, стартует приложение, а все первоначальные данные десериализуются из хост-страницы.
  • Данные для поисковиков так же присутствуют в странице и доступны для индексирования.

Исходные данные.
Сервис:

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&lt;/br&gt;Название топика: некий топик2&lt;/br&gt;Название топика: некий топик3&lt;/br&gt;
            </noscript>
        </div>
    </body>
</html>

Надеюсь, моя статья станет кому-то полезной.

Автор: Agb

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js