• Microsoft .NET
  • ASP.NET
  • Замена URL с помощью ASP.NET для поисковой оптимизации

Замена URL с помощью ASP.NET для поисковой оптимизации - Замена с помощью global.asax, модуля HTTP и обработчика HTTP

ОГЛАВЛЕНИЕ

Замена с помощью global.asax

Файл global.asax позволяет обрабатывать события уровня приложения и сессии и находится в корневой папке приложения. Можно реализовать простую замену URL с использованием обработчика события Application_BeginRequest из этого файла, вызываемого всякий раз, когда новый запрос отправляется ASP.NET из IIS на обработку:

void Application_BeginRequest(object sender, EventArgs e)
{
    HttpApplication app = sender as HttpApplication;
    if(app.Request.Path.IndexOf("FriendlyPage.html") > 0)
    {
        app.Context.RewritePath("/UnfriendlyPage.aspx?SomeQuery=12345");
    }
}

В фрагменте кода выше заменяются любые запросы к странице FriendlyPage.html на страницу UnfriendlyPage.aspx со строкой запроса SomeQuery=12345. По мере прохождения запроса по конвейеру он будет использовать вновь замененный ресурс вместо исходного.

Это очень простой, жестко заданный пример замены URL, не учитывающий пути приложения. Обычно замена не выполняется в виде жестко заданных записей в обработчике global.asax, а осуществляется в специальном модуле HTTP или с помощью обработчика HTTP.

Как сказано ранее, пример выше будет работать, только если IIS настроен на отправку ASP.NET запросов к ресурсу *.html, но этот пример с тем же успехом работает для любого типа запроса, включая каталог (если в IIS задана привязка *) и запросы к другим страницам aspx.

Замена с помощью модуля HTTP

Модуль HTTP является классом, реализующим интерфейс IHttpModule. Он требует реализации двух методов:
•    Init, применяемый для подключения событий конвейера, которые модуль должен обрабатывать
•    Dispose – освобождает любые выделенные ресурсы

Замена URL посредством модуля HTTP работает аналогично показанному ранее подходу global.asax. Модули HTTP встраиваются в конвейер обработки приложения ASP.NET путем определения их в файле web.config. ASP.NET автоматически загрузит и создаст экземпляры всех определенных модулей и вызовет их методы Init(). Метод Init() можно использовать для подписки на другие события в конвейере запроса.

Модули HTTP выполняются последовательно, один за другим, в порядке их указания в файле web.config, и их методы вызываются до эквивалентных событий в global.asax.
Следующий фрагмент показывает, как пишется типичный (очень простой) модуль HTTP:

public class UrlRewritingModule : IHttpModule
{
    public UrlRewritingModule()
    {
    }

    public String ModuleName
    {
        get
        {
            return "UrlRewritingModule";
        }
    }

    const string ORIGINAL_PATH = "OriginalRequestPathInfo";

    public void Init(HttpApplication application)
    {
        application.AuthorizeRequest += new EventHandler(application_AuthorizeRequest);
        application.PreRequestHandlerExecute +=
        new EventHandler(application_PreRequestHandlerExecute);
        application.Context.Items[ORIGINAL_PATH] = null;
    }

    void application_PreRequestHandlerExecute(object sender, EventArgs e)
    {
        HttpApplication app = sender as HttpApplication;
        String strOriginalPath = app.Context.Items[ORIGINAL_PATH] as String;
        if (strOriginalPath != null && strOriginalPath.Length > 0)
        {
            app.Context.RewritePath(strOriginalPath);
        }
    }

    void application_AuthorizeRequest(object sender, EventArgs e)
    {
        HttpApplication app = sender as HttpApplication;
        String strVirtualPath = "";
        String strQueryString = "";
        MapFriendlyUrl(app.Context, out strVirtualPath, out strQueryString);

        if (strVirtualPath.Length>0)
        {
            app.Context.Items[ORIGINAL_PATH] = app.Request.Path;
            app.Context.RewritePath(strVirtualPath, String.Empty, strQueryString);
        }
    }

    void MapFriendlyUrl(HttpContext context,
    out String strVirtualPath, out String strQueryString)
    {
        strVirtualPath = ""; strQueryString = "";

        // Сделать: Эта процедура должна проверять свойства context.Request и реализовывать
        //       надлежащую систему привязки.
        //
        //       Присвоить strVirtualPath виртуальный путь целевой страницы aspx.
        //       Присвоить strQueryString все строки запроса, необходимые странице.

        if (context.Request.Path.IndexOf("FriendlyPage.html") >= 0)
        {
            strVirtualPath = "~/Main.aspx";
            strQueryString = "Message=You smell of updated cheese!";
        }
    }

    public void Dispose()
    {
    }
}

Как видно, метод Application_AuthorizeRequest делает то же самое, что и процедура в обработчике global.asax Application_BeginRequest. Можно подписаться на событие BeginRequest в модуле HTTP, но вместо этого лучше использовать AuthorizeRequest, так как он учитывает аутентификацию форм, которая может выполнять перенаправление для получения регистрационных данных; если это происходит и URL уже был заменен событием BeginRequest, то клиент будет отправлен обратно на замененную страницу, а не на исходную удобную страницу. Осуществление замены внутри события AuthorizeRequest гарантирует, что подсистема аутентификации форм вернет клиента на удобное имя ресурса.

Для осуществления привязки был реализован метод-заглушка MapFriendlyUrl, выясняющий, как надо заменить запрошенный ресурс. Это полностью зависит от ваших настроек. В данном примере используется небрежно сделанный жестко заданый тест для любого запроса к “FriendlyPage.html”, преобразуемый в “UnfriendlyPage.aspx?FirstQuery=1&SecondQuery=2”. Разумеется, надо дополнить этот метод, чтобы он делал то, что вам надо, следя, чтобы выходные параметры “strVirtualPath”  и “strQueryString” были заполнены. Если ресурс не удалось преобразовать, метод возвращает пустой путь.

В обработчике PreRequestHandlerExecute есть дополнительный вызов метода RewritePath. Это сделано, потому что любая форма отправляется обратно, что происходит при отправке целевой страницы обратно на исходный удобный URL, а не на неприятный замененный. В примере выше все запросы к "FriendlyPage.html” заменяются на “UnfriendlyPage.aspx?FirstQuery=1&SecondQuery=2”. Если пропустить вторую замену на этапе PreRequestHandlerExecute, все обратные отправки на странице UnfriendlyPage.aspx отправятся обратно на UnfriendlyPage.aspx, а не на FriendlyPage.html.

Однако есть один интересный побочный эффект; несмотря на то, что сама страница восстанавливается, любые запросы, используемые в замененной странице, все еще появятся как часть ссылки обратной отправки. Есть некоторые интересные идеи по решению данной проблемы, в том числе переопределение интерпретации атрибута action тега <form> страниц с помощью адаптера контроля CSS, но можно более ловко избежать этой проблемы, используя обработчик HTTP вместо модуля HTTP, что позволяет использовать тот же прием двойного RewritePath, но в лучшем месте в конвейере.

Другая любопытная особенность заключается в том, что используется свойство управления состоянием HttpContext.Items для хранения и разделения пар ключ/значение между отдельными каркасными вызовами обработчиков событий модуля HTTP. Это свойство также помогает передавать информацию о состоянии между модулями HTTP и обработчиками HTTP. В таком случае необходимо записать исходный путь, чтобы восстановить его позже в конвейере, и снова заменить на него.

Замена с помощью обработчика HTTP

Обработчик HTTP является классом, реализующим интерфейс IHttpHandler, служащим для реализации специальной реакции на конкретный тип запроса к ресурсу, оправленному механизму ASP.NET. Файл web.config приложения содержит привязки, указывающие, какой обработчик какими ресурсами должен заниматься и для каких команд HTTP. Например, ASP.NET умеет использовать стандартный обработчик страницы всегда, когда поступает запрос к ресурсу *.aspx. Напротив, модули HTTP вызываются для всех запросов, и приходится определять, надо ли что-то делать с запросом внутри модуля.

Чтобы произвести замену для ресурсов *.html, надо создать обработчик, занимающийся запросами *.html. Интерфейс IHttpHandler требует реализации двух методов:
•    ProcessRequest, вызываемый каркасом ASP.NET, когда соответствующий запрос требует обработки, и обязанный реализовывать надлежащий ответ.
•    IsReusable, возвращающий логический флаг, показывающий, может ли использоваться один и тот же обработчик для множественных запросов или нет.

Типичная ошибка, допускаемая при реализации замены с помощью обработчика HTTP, заключается в том, что вызывается метод HttpContext.RewritePath для замены запроса, и ожидается, что он как-то будет работать. Обработчик HTTP должен по-настоящему обрабатывать запрос, чтобы дать ответ. Простая замена запроса *.html на запрос *.aspx работает в модуле HTTP или в обработчике global.asax, потому что каркас ASP.NET вызывает правильный обработчик (т.е. стандартный обработчик страницы *.aspx), потому что информация запроса была заменена до выбора и вызова нужного обработчика. Как только обработчик был вызван, он отвечает за генерацию правильного ответа.

Чтобы реализовать обработчик HTTP для обработки ресурсов *.html, надо воспользоваться стандартным обработчиком страницы и перенаправить ему замененный запрос после замены исходного URL. К счастью, можно использовать метод PageParse.GetCompiledPageInstance для возврата экземпляра стандартного обработчика страницы для конкретного ресурса. Задав экземпляр стандартного обработчика страницы для целевого ресурса aspx, можно непосредственно вызвать его метод ProcessRequest из метода ProcessRequest собственного обработчика.
Поскольку используется метод GetCompiledPageInstance для возврата экземпляра страницы, исходя из требуемой целевой страницы aspx, не надо использовать метод RewritePath для изменения запрошенной страницы, только для строк запроса. Ниже приведена реализация простого обработчика HTTP:

public class UrlRewriter : IHttpHandler
{
    public void ProcessRequest(HttpContext context)
    {
        // Привязать удобный URL к серверному..
        String strVirtualPath = "";
        String strQueryString = "";
        MapFriendlyUrl(context, out strVirtualPath, out strQueryString);

        if(strVirtualPath.Length>0)
        {
            // Применить требуемые строки запроса к запросу
            context.RewritePath(context.Request.Path, string.Empty, strQueryString);

            // Получить обработчик страницы для требуемой страницы ASPX с использованием этого контекста.
            Page aspxHandler = (Page)PageParser.GetCompiledPageInstance
        (strVirtualPath, context.Server.MapPath(strVirtualPath), context);

            // Выполнить обработчик..
            aspxHandler.PreRenderComplete +=
        new EventHandler(AspxPage_PreRenderComplete);
            aspxHandler.ProcessRequest(context);
        }
    }

    void MapFriendlyUrl(HttpContext context,
    out String strVirtualPath, out String strQueryString)
    {
        strVirtualPath = ""; strQueryString = "";

        // Сделать: Эта процедура должна проверять
        // свойства context.Request и реализовать
        //       надлежащую систему привязки.
        //
        //       Присвоить strVirtualPath виртуальный путь целевой страницы aspx.
        //       Присвоить strQueryString все строки запроса, необходимые странице.

        if (context.Request.Path.IndexOf("FriendlyPage.html") >= 0)
        {
            // Пример жестко заданной привязки "FriendlyPage.html"
       // к "UnfriendlyPage.aspx"

            strVirtualPath = "~/UnfriendlyPage.aspx";
            strQueryString = "FirstQuery=1&SecondQuery=2";
        }
    }

    void AspxPage_PreRenderComplete(object sender, EventArgs e)
    {
        HttpContext.Current.RewritePath(HttpContext.Current.Request.Path,
        String.Empty, String.Empty);
    }

    public bool IsReusable
    {
        get
        {
            return true;
        }
    }
}

Сначала обработчик определяет целевую страницу aspx, требуемую для обработки запроса, и определяет все строки запроса, которые должны быть переданы целевой странице. Для простоты не была включена никакая конкретная реализация привязки – она остается на ваше усмотрение и может быть простыми жестко заданными страницами или включать в себя поиск в конфигурационном файле, содержащем регулярные выражения. Метод MapFriendlyUrl присваивает выходному параметру “strVirtualPath” виртуальный путь целевой страницы aspx, и выходному параметру “strQueryString” присваивает строки запроса, необходимые целевому скрипту.

Далее метод HttpContext.RewritePath применяется для замены только строк запроса (так как путь к исходному удобному URL уже правильный). Затем создается экземпляр стандартного обработчика страницы для целевой страницы aspx. Это делается, потому что при его выполнении страница aspx увидит все требуемые параметры запроса, но все еще будет воспринимать URL как удобный – не придется менять путь запроса к странице aspx, так как она вызывается вручную.

Перед фактической обработкой запроса подключается обработчик события страницы PreRenderComplete. Это событие возбуждается, когда страница завершила создание всех своих управляющих элементов, разбиение на страницы завершено, состояние просмотра готово к записи, и итоговый HTML готов к генерации. Подключение к этому событию позволяет выполнить прием двойного RewritePath (похож на то, что было сделано в подходе модуля HTTP, показанном ранее). Обработчик PreRenderComplete вызывает HttpContext.RewritePath для удаления запросов, добавленных при последней замене (отменяя ее). Это гарантирует, что цель, сгенерированная для обратных отправок, является полностью удобным URL и, в отличие от реализации модуля HTTP, не содержит дополнительных строк запроса, добавленных позже.

Несмотря на то, что теперь обработчик успешно заменяет удобную страницу *.html на требуемые страницы *.aspx с запросами, он все еще имеет несколько проблем:
•    Состояние сессии недоступно внутри целевой страницы aspx
Это проблема для большинства людей, использующих состояние сессии внутри страниц aspx, но ее легко устранить. Добавление интерфейса IReadOnlySessionState или IRequiresSessionState к классу позволяет получить доступ только для чтения или для чтения и записи соответственно. Не надо реализовывать ничего по-другому в обработчике, потому что эти интерфейсы не предоставляют методов и являются маркерными, приказывая ASP.NET активизировать доступ к состоянию сессии при использовании обработчика.
•    Запросы к фактическим файлам *.html больше не обслуживаются.
Это может быть или не быть проблемой. Если все еще необходимо обслуживать фактические статические файлы *.html, то требуется обеспечить, чтобы обработчик делал это. IIS было приказано перенаправлять ASP.NET все запросы к *.html для обработки. Проблема решается путем написания кода в начале (или в конце) обработчика, проверяющего, предназначен ли запрос для реальной страницы, и если да –  обслуживающего его.
•    Удобные URL не могут сами содержать строки запроса.
Пожалуй, это не проблема, так как главная причина для замены – удаление запросов из URL. Однако может пригодиться наличие возможности внутренне вызвать удобную страницу с программными запросами, чтобы поддержать убеждение, что единообразная страница *.html является обслуживаемым ресурсом, вместо того чтобы возвращаться к неприятной странице *.aspx. Необходимо внести изменения, чтобы записать исходные запросы и восстановить их в обработчике события страницы PreRenderComplete. Также необходимо аккуратно объединить привязанные запросы с запрошенными запросами.
•    Обработчик должен изящно обрабатывать ошибки «страница не найдена» при необходимости.
В зависимости от того, как работает система привязки, порой заданная удобная страница фактически ни к чему не относится. Можно перенаправлять ответ на страницу ошибки, или страница aspx может реагировать соответственно, но в противном случае обработчик должен быть написан так, чтобы отвечать надлежащим сообщением и возвращать код ответа 404. Если не сделать этого, то обработчик будет возвращать пустой ответ.