Pull to refresh
0
InterSystems
InterSystems IRIS: СУБД, ESB, BI, Healthcare

Cache + jQuery. Быстрый старт

Reading time15 min
Views13K
В статье показывается способ применения jQuery для работы с базой данных Caché, в результате чего реализуется простой функционал по вставке и удалению данных в ajax веб-приложении.

Предполагается, что читатель обладает средним уровнем знаний HTML, CSS и Javasсript и имеет минимальный опыт работы с Intersystems Cache. Загрузить последнюю версию Caché можно здесь. Начальный опыт работы с Caché можно обрести тут.


Зачем нужен jQuery в CSP-приложении?

CSP(Zen) — базовая технология, предлагаемая Caché для быстрого создания ajax веб-приложений, работающих с базой данных Caché.
Чем может помочь jQuery вашему CSP(ZEN)-приложению?
1. Удобный доступ к любому элементу html-страницы

Фреймворк jQuery позволяет получать доступ к элементам c помощью синтаксиса селекторов CSS, реализуя этот функционал наиболее предпочтительным для каждого конкретного браузера способом.
2. Кроссбраузерность

Кроссбраузерность – неотъемлемое требование к современному Web-приложению. Разработчики фреймворка утверждают, что разработанный с помощью jQuery код, будет единообразно функционировать в браузерах — IE 6.0+, FF 3.6+, Safari 5.0+, Opera, Chrome.
Плагины

jQuery предоставляет удобный способ для расширения своей функциональности с помощью плагинов. С момента создания фреймворка (в 2006 г.) их было написано не мало. Отдельного упоминания заслуживает библиотека клиентского интерфейса jQueryUI.

Как подключить jQuery к вашей CSP (ZEN) странице?


Скачайте с официального сайта сжатую версию библиотеки, поместите ее в каталог CSP приложения, укажите на csp (zen) странице, что будет использоваться файл jQuery определенной версии.

Например:
CSP
<script type="text/javascript" src="jquery-1.7.1.min.js"></script>


ZEN

Parameter JSINCLUDES As STRING="jquery-1.7.1.min.js"

CLS

w "<script type='text/javascript' src='jquery-1.7.1.min.js'></script>"


CSP, ZEN и CLS — три подхода создания серверных страниц в Caché.
Каждая CSP-страница при компиляции превращается стандартный класс Cache — наследник класса %CSP.Page — CSP. Отсюда следуюет второй способ — кодоориентированный, предполагает изначальную разработку страницы с помощью классов Cache, наследников класса %CSP.Page — CLS подход. Подробнее о различиях смотрите здесь.
Третий способ — разработка с использованием Zen объединяет два этих способа — используются как Zen-теги так и объектный код. В приводимых примерах обозначения CSP,CLS,ZEN, обозначают способы разбработки страницы, который использовался при создании примера


Для интернет-приложений предпочтительным способом будет использование сетей доставки контента (CDN – content delivery network). В этом случае файл jQuery будет находиться на серверах одной из перечисленных сетей. Пример подключения с использованием CDN:
CSP: 
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>

ZEN: 

Parameter JSINCLUDES As STRING="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js";

CLS:
&html<
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
>


Использование jQuery


Самая используемая функция jQuery так и называется — jQuery(), но чаще используется ее синоним — $(). Результат этой функции будет зависеть от типа и количества переданных аргументов. Основные варианты вызова этой функции:
1. В качестве аргумента передается строка:
Метод будет пытаться трактовать строку или как селектор jQuery (CSS) или как html код:
$( "div#1973" ) // вернет элементы div с идентификатором 1973
$( ".doc td" ) // вернет элементы <td> дочерние по отношению к элементам с классом doc
$( "<div>Write less, do more</div>" ) // создаст* новый элемент


2. В качестве аргумента передается элемент страницы:
$( document.body ) // элемент body будет «упакован» в объект jQuery
$( document.forms[0] ) // элемент первая форма страницы будет «упакована» в объект jQuery


3. В качестве аргумента передается функция:
function loadHandler() { alert( "Документ загружен" ); }
$( loadHandler ); 

или анонимная функция:
$( function (){ alert( "Документ загружен" ); } ); 

Переданная функция будет выполнена по событию загрузки документа. Такой вызов можно использовать вместо:
<body onload="loadHandler">


Кроме поиска элементов на странице, jQuery содержит методы для манипуляций с ними. Список часто используемых функций:
html()	возвращает  или установливает html-содержимое элемента.
val()	возвращает  или устанавливает значение элемента управления (input,select)
attr()	возвращает или устанавливает значение аттрибута элемента
append(), appendTo()	добавляет элемент к другому, создает и добавляет элемент к другому
remove()	удаляет элемент
bind(), unbind()	Добавление, удаление обработчиков событий. Для основных событий вместо bind() можно использовать синонимы click(), keypress(), hover() и т.д.
addClass(), removeClass(), toggleClass()
добавляет, удаляет, переключает класс(ы) элемента
css()	возвращает или устанавливает значения стиля
show(), hide()	показывает, прячет элемент
parents(),parent() children()	возвращает родительские или дочерние элементы


*Полный список функций можно посмотреть здесь

Необходимо отметить некоторые правила, реализованные в jQuery:
— результат запроса упаковывается в объект jQuery;
— большинство функций возвращают контекст вызова.

Соблюдение этих правил делает код «ненавязчивым» и позволяет использовать «цепочечный» синтаксис. Пример кода с использованием jQuery, выполняющий поиск элемента и замену его значения:

<script type="text/javascript" src="jquery-1.7.1.min.js"></script>
<script type="text/javascript">
$(function(){
   
   $( ".age38" ) // Найти любые элементы с классом age38
    .val( "Write less, do more" ) //их значение будет заменено на девиз
    .css( "font-size", "110%" ) // а также изменен размер шрифта
    .click( function(){ $(this).val(""); } ) //при клике сбросим значение
    // Эту «цепочку» из функций можно продолжить дальше.
	
});
</script>


«Ненавязчивость» заключается в том, что для указанного выше кода, при отсутствии на странице элементов с классом age38, ошибки выполнения не возникает и выполнение кода не прерывается

Ниже показан пример, javascript с аналогичным функционалом, но без jQuery
<script type="text/javascript">
 //объект в который будем складывать все свои переменные и функции
 //чтобы не "засорять" пространство имен window
 var page={}; // window.page=new Object();

 // Функция отбора элементов страницы по их классу
 // вернет массив элементов с таким классом
 page.getByClass=(document.getElementsByClassName) ? function(clnm){

       //В IE9, FireFox 3, Opera 9.5, Safari 3.1 есть специальный метод
	return document.getElementsByClassName(clnm); 

  } : function(clnm){ 
  	
	///в IE 6.0-IE 8.0 такого метода нет
	var arr=[]; /// сюда будем складывать найденные по классу элементы
	/// Рекурсивный проход по дочерним элементам
	var scanDown=function(obj,handler){
		for (var child=obj.firstChild;child;child=child.nextSibling){
			if (typeof(handler)=="function") handler(child);
			scanDown(child,handler);
		}
	};
	/// определим регулярное выражение для поиска класса 
	/// так как одному элементу может быть присвоено несколько классов
	var rgxp=new RegExp("(\\s|^)" + clnm + "(\\s|$)");
	/// Обработчик элементов
	var classSelect=function( obj ){
		if ( ( !obj ) || ( !obj.className )) return;
		var cls=obj.className;
		if (!cls.match( rgxp )) return;
		arr[arr.length]=obj; ///нашли - запомнили
	};
	scanDown( document, classSelect ); //запускаем рекурсию
	return arr;
 }
 /// Определим “универсальную” функцию привязки событий
 page.bind=(page.ie)? function(obj,evt,func){
	 obj.attachEvent("on"+evt,func);
	} : function(obj,evt,func){
	 obj.addEventListener(evt,func);					
 };
 /// Основная функция обработки найденных элементов
 page.main=function(obj){
	if (!obj) return; if (obj.nodeType!=1) return; //проверки и еще раз проверки
	obj.value="Write less, do more";
	var FS="110%";
	if (obj.style) {
		obj.style.fontSize=FS;
	} else {
		obj.style="font-size:"+FS;
	}	
    	page.bind(obj,"click",function(){
		obj.value="";
	});
 };
 /// Запускаем после загрузки
 page.bind( window, "load", function() {
	var arr=page.getByClass("age38"); //найдем элементы
	for (var i in arr){
		page.main(arr[i]);	//обработаем каждый из них
	}
 });
</script>


Получившийся код меньше чем библиотека jQuery, но не такой универсальный и требует больших усилий от разработчика при создании и сопровождении.

Cache + jQuery

Использование jQuery на CSP-странице

Рассмотрим небольшой пример реализации чата с минимальной функциональностью и следующим интерфейсом:
image

Особенности примера:
— интерфейс полностью формируется с помощью jQuery
— используются асинхронные вызовы сервера #call()#, которые возвращают ответ через вызов функции на клиенте
— данные хранятся в глобале

Исходный код страницы:
<!DOCTYPE html>
<html><head><title>	jqChaté </title>
<style type="text/css">.msg span {padding: 0px 0px 0px 5px}</style>
</head>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
<script type="text/javascript">
$( function(){
 var USERNAME="#($USERNAME)#" //храним в переменной пользователя Cache
 , box=$( "<div/>" ).css("overflow","auto") //создали окошко, добавили прокрутку
	.css("border","1px solid #ccc").width( 340 ).height( 280 ) //формат
	.appendTo( $(document.body) ) //и добавили в документ
 ;
 box.parent() //перешли от окошка чата к body
  //добавляем к body input, который сразу форматируем и подписываем
 .append( $("<input />").css( "width",200 )
 .attr("maxlength",50).before( USERNAME+": ") )
 .append( //добавляем кнопку
   $("<button/>").text( "Send" ) //к ней текст и обработчик
      .click( function(){ //асинхронно передадим на сервер
	  #call(..Add($(this).prev().val()))#;	//значение элемента перед кнопкой (input)
	  $("input").val("").focus(); //textbox очистим, и вернем ему фокус ввода
})
 );
 var drawMsg=function(/*[id,time,user,msg]*/ array ){ //вывод сообщения
   var USER=2, MSG=3, SOFT="#999", BRIGHT="#339" //константы
   , msg = 
$( "<div><span>"	+ array.join("</span><span>") + "</span></div>") //html сообщения
	   .appendTo( box ).hide() //добавили и спрятали - вдруг служебное сообщение
	   .addClass("msg") //выставили класс
	   .children(":first").hide() //id - скрытое поле
	   .next().css("color", SOFT) //time
	   //подсветка пользователя
	   .next().css("color", (USERNAME==array[USER]) ? SOFT : BRIGHT ).append(": ")
	   .parent() //восстановили контекст и вернули в переменную msg
   ;
   if (array[MSG]!=="") { // сообщение не служебное
	msg.show(); //отображаем
	var div=msg.get(0); //получаем доступ непосредственно к html-элементу
	if (div) div.scrollIntoView(); //подкручиваем в область видимости
   }
 }; //drawMsg

window.chat=function(array){ //главная функция
   if ( $.isArray(array) ) drawMsg( array ); //есть данные - выводим их
   //ждем немного и спрашиваем сервер - какое следующее?
   setTimeout( 
//Последнее полученное ищем с помощью jQuery
function(){ #call(..GetNext($(".msg:last span:first").text()))# }
	, #($get(^chatSettings("timeout"),300))# 
   );	 //настройки тоже на сервере
}; //window.chat

chat(); //запускаем цикл запрос-ответ
});
</script><body></body></html>

<script runat="server" method="Add" arguments="msg:%String" language="cache">
  #; просто записываем данные
  s ^chat($increment(^chat))=$listbuild( $zt( $now() ), $USERNAME, msg )
</script>

<script runat="server" method="GetNext" arguments="lastMsg:%String" language="cache">
 #; разбираемся c переданными аргументами
 set lastMsg=$get(lastMsg), prevMsg=$order( ^chat(lastMsg),-1 ), isNewUser=( lastMsg = "" )
 set callBack="window.parent.chat("  ;
 set isNewChat=( isNewUser && (prevMsg="") ) if ( isNewChat ) write callBack,")" Quit  
 if ( isNewUser ) write callBack,"[", prevMsg, ",'','',''])" Quit  ;инициализация
 set nextMsg=$order( ^chat( lastMsg ) ) if ( nextMsg="" ) write callBack,")" Quit 
 set stored=$get(^chat(nextMsg)) ;выводим сообщение
 write callBack,"[",nextMsg,", ", ..QuoteJS( $listget(stored,1) )
 	, ",", ..QuoteJS( $listget(stored,2) )
        , ",", ..QuoteJS( ..EscapeHTML( $listget(stored,3 )))
 , "])"
</script>

Для воспроизведения примера, откройте Студию в любой области, создайте новую CSP-страницу, вставьте скопированный код страницы из примера. Сохраните страницу с любым именем — выберите при этом какое-либо существующее CSP-приложение. Затем откомпилируйте страницу (Ctrl+F7) и откройте ее в браузере ( F5).

Использование jQuery на Zen-странице

Применение jQuery на Zen странице полностью аналогично ипользованию в CSP (поиск и изменение элементов), но с учетом того, что Zen — это клиент-серверный фреймворк, который поддерживает одинаковую объектную модель в браузере и на сервере Cache. Поэтому необходимо учитывать следующие моменты:
— Модель Zen-страницы начинает формироваться после события загрузки страницы.
— Для изменения модели страницы предпочтительно использовать методы Zen
— Zen формирует разметку с помощью дополнительных скрытых элементов

Практический пример Cache + jQuery
Приведем более сложный пример c использованием jQuery для создания и удаления объектов хранимого класса со следующим интерфейсом:
Особенности примера:
— Пример работает на Cache версии 2010 и выше.
— Используется динамическая привязка событий, весь javascript код вынесен в отдельный javascript файл
— Часть содержимого страницы генерируется непосредственно в браузере
— Не используются «гипер»-события и «динамические» (генерируемые на лету) метод-страницы
— Сервер генерирует ответы в различных форматах данных в зависимости от аргументов запроса
— Показан вывод серверного объекта в формате XML с помощью %XML.Writer и разбор ответа сервера в формате XML на стороне клиента с помощью jQuery

1. Создайте в области User хранимый класс с тестовыми данными
/// Класс с тестовыми данными
Class test.data Extends (%Persistent, %Populate, %XML.Adaptor) {
/// Обозначение
Property code As %String;
/// Наименование
Property name As %String;
/// Переопределенный "конструктор", рассчитанный на прием данных непосредственно со страницы
Method %OnNew(ByRef args as %String="") As %Status [ Private, ServerOnly = 1 ] {
s ..code=$g(args("code",1)), ..name=$g(args("name",1))
Quit $$$OK
}
}


2. Создайте тестовые данные в test.data, выполнив из терминала Cache вызов метода класса.
USER>d ##class(test.data).Populate()

3. Создайте класс-страницу:
/// Страница работы с экземплярами класса test.data
Class wui.testData Extends %CSP.Page {

/// Рекомендуемое значение
Parameter CHARSET="utf-8";

/// Переопределим метод вывода страницы
ClassMethod OnPage() As %Status {
	
	#; Эта страница обрабатывает три типа запросов
	#; 1.просто вывод данных 
	#; 2.запрос на создание объекта
	#; 3.запрос на удаление объекта
	#; Тип запроса различаются по значению аргумента <var>oper</var>

	m args=%request.Data ;Заберем все данные из запроса 
	
	if $d(args("oper")){ //если определен аргумент oper
		s oper=$g(args("oper",1)) //узнаем тип запроса
		Q:oper="add" ..Add(.args) //перейдем к созданию объекта в методе этого класса
		Q:oper="del" ..Del(.args) //перейдем к удалению объекта
	}
	
	#; в остальных случаях просто выводим страницу
	&html<<html><head><title>Класс test.data</title>
	
	<!-- Часть оформления вынесем в CSS -->
	<style type="text/css">
	table {border-collapse: collapse; border: 1px solid #777;}
	td, th {border: 1px solid #777;}
	td {padding: 2px;} 
	th {font-weight: normal; color:#fff;background-color:#aaa;}
	</style>
	</head>
	<body>
	<table><caption>Объекты класса test.data</caption>
	<thead><tr><th>ID</th><th>code</th><th>name</th><th><!--Кнопка--></th></tr></thead>
	<tbody>>
	
	#; Выведем имеющиеся объекты в виде таблицы
	s sql="Select ID,code,name From test.data"
	s rs=##class(%SQL.Statement).%ExecDirect(.stm, sql) ; У вас версия Cache > 2009 ?
 	while rs.%Next() {
			w !,"<tr id='",rs.ID,"'>"
			, "<th>",rs.ID,"</th>" ;порядковый номер строки
			, "<td>",rs.code,"</td>"	 
			, "<td>",rs.name,"</td>"	 
			, "</tr>"
	}
	
	&html<</tbody></table>
	<!--подключаем jQuery-->
	<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
	<!—весь клиентский код вынесем в отдельный файл-->
	<script src="wui.testData.js"></script>
</body></html>>
	Quit $$$OK
}

///Добавление объекта на стороне сервера и ответ клиенту в формате XML 
ClassMethod Add(ByRef args as %String="") as %Status{
	
	#; выполним добавление нового объекта
	s obj=##class(test.data).%New(.args), sc=obj.%Save()
	
	#; выведем результат добавления и сам объект в виде xml
	s wr=##class(%XML.Writer).%New() ;начнем вывод xml
	
	d wr.RootElement("response") ; <response
	, wr.WriteAttribute("id",obj.%Id()) ; id='' 
	
	#;новый объект в виде xml 
	d wr.Object(obj) ; ><data><code>123</code><name>Name</name></data>
	if $$$ISERR(sc) {
		d wr.Element("error") ;<error>
		d wr.WriteChars($system.Status.GetOneErrorText(sc)) ;errorText
		d wr.EndElement() ;</error>
	}
	d wr.EndRootElement() ;</response>
	
	Q $$$OK
}

///Удаление объекта на стороне сервера и ответ в виде обычного текста
ClassMethod Del(ByRef args as %String="") as %Status{
	s id=$g(args("id",1)), sc=##class(test.data).%DeleteId(id)
	w $CASE(sc
,1: 1
, : $system.Status.GetOneErrorText(sc)
)
Q $$$OK
}
}


Далее приведен Javascript код, вынесенный в файл wui.testData.js. Файл должен находиться в каталоге csp-приложения. Для области USER каталог находится в [Директория установки Cache]\CSP\USER\.

$(function(){ //обработчик document.ready
    
    //Добавим к таблице интерфейс для создания объектов
    var footer=[ //используется один из вариантов объявления массива
		"<tfoot><tr>"
	    ,"<th>*</th>" //колонка "порядковый номер"
	    ,"<th><input /></th>" //колонка "код"
	    ,"<th><input /></th>" //колонка "наименование"
	    ,"<th><button> + </button></th>" //кнопка сохранения новой записи
	    ,"</tr></tfoot>"
    ].join( "" ); //Объединим в строку (оптимизация работы со строкой)
    
    //Создадим элемент с таким содержимым
    $( footer ).appendTo( $( "table" ) ); //И добавим его к таблице
    
    //доберемся до только что созданной кнопки добавления записи
    $( "tfoot button" ).click( callAdd ); //и привяжем к ней обработчик
       
    //к каждой tr из tbody (строки с данными test.Data) добавим кнопку удаления
    $("tbody tr").each( addDelBtn ); //создание кнопок удаления
    
    //Функция добавления кнопки удаления к строке с данными
	function addDelBtn (){ //для каждой строки данных создадим кнопки удаления
     
        var $tr=$(this); //упакуем контекст (tr из tbody) в jQuery
        $( "<th><button class='bDel'></button></th>" ) //создание элемента
			.appendTo( $tr ) //добавим в конец строки ячейку с кнопкой удаления
			.children( ".bDel" ) //для дочерних элементов c классом bDel
			.html( "X" ) //Заменить содержимое кнопки
			.attr( "title", "Удалить запись" ) // добавить подсказку
			.css( "color", "#f00" ) //изменить цвет надписи на предупреждающий
			.click( callDel ) //при клике на кнопку вызываем удаление данных
        ;
	};
    
    // Функция удаления данных на клиенте и сервере
    function callDel(){
		//контекст вызова - кнопка удаления
		var $btnDel=$( this ).hide(); //спрячем кнопку и сохраним ссылку на нее
		var $row=$button.parents( "tr" ); //ищем родительский элемент tr
		var id=$row.attr( "id" ); //узнаем код объекта

		// Отправим запрос на сохранение данных с помощью
		// функции $.ajax- http://api.jquery.com/jQuery.ajax/
		$.ajax({ //ajax options
	        url: window.location //вызов адреса страницы - wui.testData.cls
	        ,data: "oper=del&id="+id //параметры запроса
	        ,dataType: "text" //ожидаем ответ в формате text
            ,success: function(txt){ //при успешном выполнении запроса
	            if ( txt!="1" ) { //при удалении произошла ошибка
	                $btnDel.show(); //вернем видимость кнопке удаления
	                alert(txt); //покажем ошибку
	                return;
	            }
	            $row.remove(); //удалим строку на клиенте
	        }
   		}); //ajax()
	};

    // Функция создания данных чуть сложнее
    function callAdd(){
	    
	    var values = [] // в этот массив соберем значения для новой записи
	      , $inputs = $( "tfoot input" ) //найдем элементы ввода
	      		//и для каждого из них
	      		.each( function(){
	            	values.push( //в массив значений поместим
	            		$( this ).val() //значение input
	            	)
	             } //function
	            ) //each
        ; //var
          
        //проверим, что введены хоть какие-нибудь данные
        if ( values.join( "" ) == "" ){
        	alert( "Укажите хотя бы одно значение" ); return;
        };
        
        //до выполнения операции на сервере прячем интерфейс добавления
        var $addRow = $( "tfoot tr" ).hide();
           
        // Отправим запрос на сохранение данных с помощью функции
        // $.ajax- http://api.jquery.com/jQuery.ajax/
        $.ajax( { //настройки вызова сервера
                url: window.location //адрес - wui.testData.cls
                //,async: true // по умолчанию запрос выполняется асинхронно
                , type: "POST" // тип запроса - POST или GET
                , data: { //передаваемые на сервер данные в виде объекта
	                oper: "add" //операция добавления
	                , code: values[0] //значение кода
	                , name: values[1] //значение наименования
                }
                , dataType: 'text' //обработаем ответ сервера как строку.
                
                //если вызов завершен успешно
                , success: function( text ){
                   
                     // ожидаем c сервера текст в XML следующего вида
                     // <response id='newid'>
                     // <data><code>Code</code><name>Name</name></data>
                     // <error>Error Text</error>
                     // </response>
                     // разбираем документ с помощью функции jQuery
                     var xml=$.parseXML(text)
                     , $xml=$( xml ) //оборачиваем его в jQuery
                     , error=$xml
                     			.find("error") //ищем node с ошибкой
                     			.text() //получаем текстовое значение
                     ;
                     if ( error!=="" ){ //если была
                            alert(error); //вывод ошибки
                            return;
                     }
                     //Разбор ответа на составляющие элементы
                     var id=$xml.find("response").attr("id")
                     , code=$xml.find("code").text()
                     , name=$xml.find("name").text();
                       
                     var tr=[ //сформируем строку с полученными данными
                            "<tr id='",id,"'>"
                            , "<th>",id,"</th>"
                            , "<td>",code,"</td>" 
                            , "<td>",name,"</td>" 
                            ,"</tr>"
                     ].join( "" );
                       
                     $( tr )
                     	.appendTo( $("table tbody") ) //добавим строку в таблицу
                     	//для новой строки вызовем функцию добавим кнопку удаления
                     	.each( addDelBtn )
                     ;
                     $inputs.val( "" ) //очистим элементы ввода
                     	.focus() //установим фокус
                     ;
                     
                    } //success
                    
                    , error: function(err){ //если вызов завершен с ошибкой
                        alert("Error: "+err);
                    }
                    
//обработчик события, возникающего после success и error
                    , complete: function(){
                        $addRow.show(); //покажем строку добавления объекта
                    }
                   
                } //настройки
            ); //$.ajax
        }; //callAdd
        
}); //document.ready 



Некоторые итоги:
  • Использование библиотеки jQuery значительно упрощает разработку web-интерфейса;
  • jQuery позволяет удобно разделить динамическое и статическое содержимое страницы;
  • в jQuery несложно вынести большую часть логики интерфейса на сторону браузера, тем самым снизив нагрузку на сервер Caché.
Tags:
Hubs:
+1
Comments14

Articles

Information

Website
www.intersystems.com
Registered
Founded
1978
Employees
1,001–5,000 employees
Location
США