Шаблонизация в JavaScript с использованием Razor

в 13:36, , рубрики: .net, asp.net mvc, html, javascript, msbuild, razor, метки: , , , , ,

В силу всё большего и большего усложнения веб-приложений на стороне клиента, хочется иметь шаблонизаторы, которые работали бы прямо на клиенте. И таких средств, надо сказать, появилось не мало. Но так как я легких путей не ищу все они мне не нравятся, я решил сделать свой с блэкджеком и дамами лёгкого поведения (я так понял, на Хабре жестко карают и банят, если этой фразы нет в посте).

И вот я решил создать строготипизированный шаблонизатор на Razor.

Razor

Для тех, кто пропустил, Razor — это такой элегантный язык для генерации разметки, в который встраивается C# или VB. Он используется в ASP.NET MVC и WebMatrix.

Все его замечательные свойства расписывать не буду, так как за меня это уже сделал Scott Guthrie, а на Хабре даже перевели.

Важно то, что он строготипизированный, и имеет отличную поддержку в Visual Studio, включая IntelliSense.

Более того, т.к. я веб-проекты разрабатываю на .Net с использованием ASP.NET MVC, а представления описываю как раз на Razor'е, то для меня это идеальный вариант. Тем более, при должном старании, можно избавиться от повторного написания кода — использовать одни и те же шаблоны на сервере и на клиенте.

Использовать Razor вне MVC не является большой проблемой, но вот код, который он генерирует — C#, а нам нужен JavaScript. И вот тут мне на помощь пришел…

SharpKit

Этот замечательный инструмент, почему-то обойден вниманием Хабра. Он позволяет конвертировать код на языке C# в JavaScript.
Например из такого

using SharpKit.JavaScript;
using SharpKit.jQuery;

namespace Namespace
{
    [JsType(JsMode.Global)]
    public class MyPageClient : jQueryContext
    {
        public static void Hello(string name)
        {
            J(document.body).append(J("<h1/>").text("Hello, " + name.ToUpper()));
        }
    }
}

превратится в

function Hello(name)
{
    $(document.body).append($("<h1/>").text("Hello, " + name.toUpperCase()));
};

На официальном сайте проекта есть сносная документация. А также можно поиграться конвертированием online.

SharpKit платный, но, совершенно точно, стоит своих денег. Для open-source проектов можно получить бесплатную лицензию. А для коммерческих, можно бесплатно конвертировать до 2500 строк JavaScript кода, что иногда вполне может хватить.
К слову сказать, хоть и хорошо разбираюсь в JavaScript, уже давно использую SharpKit, и вам советую. Все таки гораздо удобнее писать с типами, нормальным intellisense, и проверкой ошибок на этапе компиляции.

Что-то я отвлекся, но, как вы поняли, именно с помощью SharpKit шаблончики на Razor, сначала превращенные в C#, превращаются в JavaScript.

MSBuild

Ну а для того, чтобы это все выполнялось во время сборки проекта, интегрировать все это дело решил через MSBuild, для чего реализовал задачу.

И вот, представляю проект

SharpKit Razor

Да, никакого оригинального названия не придумал.

Официальный сайт проекта на CodePlex
Эх, с трудом мне далось описание на английском, но гугл меня так и не понимает.

Там пока только исходники, демка.

А теперь по-подробнее — с кодами, да побольше

Ну да, это же хабр, придется совсем все кишки распотрошить. Читать только тем, кто еще не все понял как все реализовано.

База для Razor

Когда движок Razor обрабатывает код, он из чего-то такого

@inherit MyBase<string[]>
<ul>
@foreach (var item in Model)
{
  <li>@item</li>
}
</ul>

на выходе генерирует класс что-то типа такого:

namespace MyNamespace
{
  public class MyView: MyBase<string[]>
  {
    public override void Execute()
    {
      WriteLiteral("<ul>rn");
      foreach (var item in Model)
      {
        WriteLiteral("t<li>");
        Write(item);
        WriteLiteral("</li>rn");
      }
      WriteLiteral("</ul>");
    }
  }
}

Поэтому делаем базовый класс такой, чтобы он мог исполняться (метод Execute), и писать в выходной поток данные (экранированные — метод Write, неэкранированные — метод WriteLiteral).
Далее, чтобы было удобно оперировать нашими классами представлений в общем виде, выделим также интерфейс IRenderingArea.
Получается у нас вот такое:

public interface IRenderingArea
{
	[JsProperty(NativeField = false)]
	object Model { get; set; }

	[JsProperty(NativeField = false)]
	string Result { get; }

	void Execute();
}

public interface IRenderingArea<T>: IRenderingArea
{
	[JsProperty(NativeField = false)]
	new T Model { get; set; }
}

Здесь интерфейсы сразу с типизированным и нетипизированным вариантом. С помощью JsProperty помечено, чтобы SharpKit превращал Result не в поле, а в функцию get_Result(). Так будет больше пространства для хитрых маневров.
Ну а сам базовый класс, будет таким:

[JsType(JsMode.Prototype)]
public abstract class HtmlArea<T>: JsContext, IRenderingArea<T>
{
	private string _result = "";

	public string Result { get { return _result; } }

	[JsField(Export = false)]
	private T _model;

	public T Model { get { return _model; } set { _model = value; } }

	[JsProperty(Export = false)]
	object IRenderingArea.Model { get { return Model; } set { Model = value.As<T>(); } }

	protected virtual void Write(object value)
	{
		if (value != null)
			_result += EscapeValue(value.As<JsObject>().toString());
	}

	protected virtual string EscapeValue(JsString value)
	{
		return value
			.replace("&", "&amp;")
			.replace("<", "&lt;")
			.replace(">", "&gt;")
			.replace(""", "&quot;")
			.replace("'", "'");
	}

	protected virtual void WriteLiteral(string value)
	{
		_result += value;
	}

	public abstract void Execute();
}

Как можно видеть, здесь класс адаптирован для того, чтобы он использовался в JavaScript.
Экранирование выполняется тупой заменой нескольких спецсимволов.

Чисто для справки, привожу получившийся JavaScript:

/*Generated by SharpKit v4.24.9000*/
if(typeof(XWeb) == "undefined")
    XWeb = {};
if(typeof(XWeb.SharpKit) == "undefined")
    XWeb.SharpKit = {};
if(typeof(XWeb.SharpKit.Razor) == "undefined")
    XWeb.SharpKit.Razor = {};
XWeb.SharpKit.Razor.AreaExtensions = function()
{
};
XWeb.SharpKit.Razor.AreaExtensions.Execute = function(view,model)
{
    var area=view();
    if(typeof(model) != "undefined")
        area.set_Model(model);
    area.Execute();
    return area.get_Result();
};
XWeb.SharpKit.Razor.HtmlArea = function()
{
    this._result = "";
};
XWeb.SharpKit.Razor.HtmlArea.prototype.get_Result = function()
{
    return this._result;
};
XWeb.SharpKit.Razor.HtmlArea.prototype.get_Model = function()
{
    return this._model;
};
XWeb.SharpKit.Razor.HtmlArea.prototype.set_Model = function(value)
{
    this._model = value;
};
XWeb.SharpKit.Razor.HtmlArea.prototype.Write = function(value)
{
    if(value != null)
        this._result += this.EscapeValue(value.toString());
};
XWeb.SharpKit.Razor.HtmlArea.prototype.EscapeValue = function(value)
{
    return value.replace("&","&amp;").replace("<","&lt;").replace(">","&gt;").replace(""","&quot;").replace("'","'");
};
XWeb.SharpKit.Razor.HtmlArea.prototype.WriteLiteral = function(value)
{
    this._result += value;
};

Теперь, чтобы полученные классы представлений можно было бы запускать, и получать из них результат, необходим какой-то механизм.
Для начала, нужно определиться, какая информация нам нужна, чтобы запустить шаблон. Ссылаться на шаблоны по имени — можно, но как-то не кошерно в нашем строготипизированном мире C#. Поэтому, я решил, что мне просто нужна информация о том как создать экземпляр шаблона. Т.е. функция создания шаблона и будет информацией о шаблоне. И тогда, для того, чтобы запустить шаблон, добавляем специальное расширение:

[JsType(JsMode.Prototype)]
public static class AreaExtensions
{
	[JsMethod(OmitOptionalParameters = true)]
	public static string Execute<T>(this Func<IRenderingArea<T>> view, T model = default(T))
	{
		var area = view();
		if (JsContext.JsTypeOf(model) != JsTypes.undefined)
			area.Model = model;
		area.Execute();
		return area.Result;
	}
}

Это расширение просто создает экземпляр шаблона, устанавливает модель, запускает и извлекает результат.

Генерация классов

Далее в игру вступает движок Razor, который должен сгенерировать класс шаблона.
Тут все достаточно просто:

Ой-ёй! Хабр больше не разрешает писать. Так что ждите продолжения.

Автор: Igorbek

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


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