Extindeți TMS WEB Core cu bibliotecile JS cu Andrew: Epic JSON Primer (partea 2)

20: Performanță relativă.

Am auzit adesea că, deși este posibil să folosim varianta TJSONObject a acestor abordări (codul PAS la care am lucrat atât de sârguincios), este de preferat să folosim varianta WC, deoarece va fi mai performantă. Vom pune acest lucru la încercare aici într-un exemplu artificial. Adesea, lizibilitatea și reutilizarea codului sunt mai importante decât performanța directă, în special pentru codul dificil care nu este executat frecvent. Dar, în același timp, cu siguranță apar situații în care un pic de cod este executat frecvent într-o buclă strânsă și este important să stoarceți fiecare fragment de performanță.

 procedura TForm1.WebButton1Click(Expeditor: TObject);
var
WC_Object: TJSObject;
PAS_Object: TJSONObject;
ElapsedTime: TDateTime;
i: întreg;
Număr: întreg;
ÎNCEPE
ElapsedTime := Acum;
// JS creează 1.000.000 de obiecte
asm
var JS_Object = {};
pentru (var i = 0; i <= 999999; i++) {
JS_Object['test '+i] = 'test '+i;
}
Sfârşit;
console.log('JS Create: '+IntToStr(MillisecondsBetween(Now,ElapsedTime))+' ms');
ElapsedTime := Acum;
// JS Count Objects
asm
Count = Object.keys(JS_Object).lungime;
Sfârşit;
console.log('Număr JS: '+IntToStr(MillisecondsBetween(Acum,ElapsedTime))+' ms');
ElapsedTime := Acum;
// WC Creați 1.000.000 de obiecte
WC_Object := TJSObject.new;
pentru i := 0 la 999999 do
WC_Object['test '+IntToStr(i)] := 'test '+IntToStr(i);
console.log('WC Create: '+IntToStr(MillisecondsBetween(Now,ElapsedTime))+' ms');
ElapsedTime := Acum;
// WC Count Objects
Număr := lungime(TJSObject.keys(WC_Object));
console.log('Număr WC: '+IntToStr(MillisecondsBetween(Acum,ElapsedTime))+' ms');
ElapsedTime := Acum;
// PAS creează 10.000 de obiecte
PAS_Object := TJSONObject.Create;
pentru i := 0 până la 9999 do
PAS_Object.AddPair('test '+IntToStr(i),'test '+IntToStr(i));
console.log('PAS Create: '+IntToStr(MillisecondsBetween(Now,ElapsedTime))+' ms');
ElapsedTime := Acum;
// PAS Count Objects
Count := PAS_Object.Count;
console.log('Număr PAS: '+IntToStr(MillisecondsBetween(Acum,ElapsedTime))+' ms');
Sfârşit;
Ieșire console.log:
JS Create: 1154 ms
Număr JS: 301 ms
Creare WC: 1006 ms
Număr WC: 272 ms
PAS Creare: 27439 ms
Număr PAS: 0 ms

Așa că a fost deschis ochii! De fapt, a trebuit să reduc bucla PAS la doar 10.000 de obiecte în loc de cele 1.000.000 de obiecte ale buclelor JS și WC doar pentru a se finaliza. Și totuși a fost nevoie de aproape 30 de ori mai mult pentru a crea obiectele JSON (deci extrapolând, este de 3.000 de ori mai lent?!) Se pare că cineva undeva are ceva de făcut de optimizare. Rețineți că numărul este banal în cazul PAS, așa că nici măcar nu se înregistrează, în cazul în care, ca și în cazul JS și WC, trebuie de fapt să depună destulă muncă pentru a ajunge la acel număr. La pachet? Ei bine, la fel ca înainte, într-adevăr, doar surprinzător că penalizarea este atât de mare. Sperăm că situația se va îmbunătăți în timp. Sau dacă nu, acesta este un argument destul de solid pentru migrarea codului PAS care se află în orice fel de buclă pentru a utiliza varianta JS sau WC. Pentru a fi corect, acesta este probabil singurul cod din acest articol în care orice diferență de performanță ar fi chiar detectabilă.

De asemenea, rețineți că, rulând testul din nou și din nou, timpii JS și WC Create variază destul de mult, uneori până la jumătate. Dar Count times nu au fluctuat prea mult deloc. PAS a rămas în mare parte același. Acest lucru s-ar putea datora multor lucruri, dar există o mulțime de optimizare JS care se întâmplă peste tot, așa că nu este prea surprinzător. Totuși, este o provocare să veniți cu criterii de referință rezonabile - trebuie să rulați o mulțime de teste pentru a obține o cifră medie pentru ca aceasta să fie semnificativă și chiar și atunci trebuie să luați în considerare condițiile din jurul testului cu mare atenție - a fost aplicația s-a încărcat deja, a existat o animație care rulează undeva etc.

21: Iterați prin elementele obiect JSON.

Am avut deja exemple de astfel de lucruri ici și colo, dar iată un exemplu mai explicit pentru toate cele trei variante.

 procedura TForm1.WebButton1Click(Expeditor: TObject);
var
WC_Object: TJSObject;
PAS_Object: TJSONObject;
i: întreg;
const
SampleObjectData = '{"apple":"fructe","banana":"fructe","orange":"fructe","morcov":"legume","cartofi":"legume"}';
ÎNCEPE
asm var JS_Object = JSON.parse(SampleObjectData); Sfârşit;
WC_Object := TJSJSON.parseObject(SampleObjectData);
PAS_Object := TJSONObject.ParseJSONValue(SampleObjectData) ca TJSONObject;
asm
pentru (var Key in JS_Object) {
console.log(„Cheie JS: „+Key+” Valoare: „+JS_Object[Key])
}
Sfârşit;
i := 0;
în timp ce (i < lungime(TJSOBject.keys(WC_Object))) do
ÎNCEPE
console.log('Cheie WC: '+șir(TJSObject.keys(WC_Object)[i])+' Valoare: '+șir(WC_Object[TJSObject.keys(WC_Object)[i]]));
i := i + 1;
Sfârşit;
i := 0;
în timp ce (i < PAS_Object.Count) do
ÎNCEPE
console.log('Cheie PAS: '+PAS_Object.Pairs[i].JSONString.Value+' Valoare: '+PAS_Object.Pairs[i].JSONValue.Value);
i := i + 1;
Sfârşit;
Sfârşit;
Ieșire console.log:
Cheie JS: măr Valoare: fructe
Cheie JS: banană Valoare: fructe
Cheie JS: portocaliu Valoare: fructe
Cheie JS: morcov Valoare: legume
Cheie JS: cartof Valoare: legume
Cheie WC: măr Valoare: fructe
Cheie WC: banană Valoare: fructe
Cheie WC: portocaliu Valoare: fructe
Cheie WC: morcov Valoare: legume
Cheie WC: cartof Valoare: legume
Cheie PAS: măr Valoare: fructe
Cheie PAS: banană Valoare: fructe
Cheie PAS: portocaliu Valoare: fructe
Cheie PAS: morcov Valoare: legume
Cheie PAS: cartof Valoare: legume

22: traversează întregul obiect JSON.

Acum avem tot ce ne trebuie pentru a aborda orice tip de JSON care ne iese în cale. De obicei, nu este nevoie să traversați direct un întreg obiect JSON. În mod normal, căutați doar să alegeți anumite valori sau să actualizați altele. Totuși, un caz de utilizare este acela de a scoate o versiune formatată a obiectului JSON. Există o mulțime de resurse online pentru a face acest tip de lucru pentru dvs. - doar copiați și lipiți JSON-ul pe care doriți să îl formatați și va scoate o versiune mai frumoasă (drăguță sau înfrumusețată) destul de ușor. Aici facem ceva similar, adnotând orice găsim. Rețineți că rezultatul nu este deloc JSON, ci doar o listă cu ceea ce este în ea și detalii despre tipurile de date. Cu toate acestea, ar trebui să poată regurgita orice JSON prin care treceți. Nu este frumos, dar ar trebui să acopere totul.

 procedura TForm1.WebButton1Click(Expeditor: TObject);
var
WC_Object: TJSObject;
PAS_Object: TJSONObject;
const
SampleObjectData = '[[[123,{"key":true,"other key":"abc","mai multe chei":[1,2,false]},null]]]';
funcția WC_Print(Element:TJSObject; Indentation:String; Key:String):String;
var
str: String;
tip: String;
ÎNCEPE
str := '';
tip:= '';
if (Element = nil) then begin str := 'Null (Null)'; tip := 'Null'; Sfârşit
else if (Element.ToString = 'true') then begin str := 'True (Boolean)'; tip := 'Boolean'; Sfârşit
else if (Element.ToString = 'false') then begin str := 'False (Boolean)'; tip := 'Boolean'; Sfârşit
else if (TJSJSON.stringify(Element).startsWith('"')) atunci începe str := Element.toString+' (String)'; typ := 'String'; sfârşit
else if (TJSJSON.stringify(Element).startsWith('{')) then begin str := 'Obiect {'; typ := 'Obiect'; Sfârşit
else if (TJSJSON.stringify(Element).startsWith('[')) atunci începe str := 'Matrice ['; typ := 'Obiect'; Sfârşit
else begin str := Element.ToString+' (Număr)'; tip := 'Număr'; Sfârşit;
dacă (Cheie = '')
apoi console.log(Indentation+' '+str)
else console.log(Indentation+' '+Key+': '+str);
Rezultat := tip;
Sfârşit;
procedura WC_Output(JSONObject:TJSObject; Indentation:String);
var
i: întreg;
tip: sfoară;
const
indentare = '____';
ÎNCEPE
dacă (TJSJSON.stringify(JSONObject).startsWith('[')) atunci
ÎNCEPE
dacă (Indentație = '') atunci
ÎNCEPE
console.log('WC [');
Indentare := indentare;
Sfârşit;
pentru i := 0 la (lungime(TJSObject.keys(JSONObject))-1) nu
ÎNCEPE
tip := WC_Print(TJSObject(JSONObject[String(TJSObject.keys(JSONObject)[i])]), Indentation, IntToStr(i));
if (typ = 'Obiect') atunci
ÎNCEPE
WC_Output(TJSObject(JSONObject[String(TJSObject.keys(JSONObject)[i])]), Indentation+indent);
console.log(Indentation+' }');
Sfârşit
else if (typ = 'Matrice') atunci
ÎNCEPE
WC_Output(TJSObject(JSONObject[String(TJSObject.keys(JSONObject)[i])]), Indentation+indent);
console.log(Indentation+' ]');
Sfârşit;
Sfârşit;
if (Indentation = indent) atunci console.log(' ]');
Sfârşit
else if (TJSJSON.stringify(JSONObject).startsWith('{')) atunci
ÎNCEPE
dacă (Indentație = '') atunci
ÎNCEPE
console.log('WC {');
Indentare := '____';
Sfârşit;
pentru i := 0 la (lungime(TJSObject.keys(JSONObject))-1) nu
ÎNCEPE
tip := WC_Print(TJSObject(JSONObject[String(TJSObject.keys(JSONObject)[i])]), Indentation, string(TJSObject.keys(JSONObject)[i]));
if (typ = 'Obiect') atunci
ÎNCEPE
WC_Output(TJSObject(JSONObject[String(TJSObject.keys(JSONObject)[i])]), Indentation+indent);
console.log(Indentation+' }');
Sfârşit
else if (typ = 'Matrice') atunci
ÎNCEPE
WC_Output(TJSObject(JSONObject[String(TJSObject.keys(JSONObject)[i])]), Indentation+indent);
console.log(Indentation+' ]');
Sfârşit;
Sfârşit;
if (Indentation = indent) atunci console.log('}');
Sfârşit
altfel
ÎNCEPE
WC_Print(JSONObject, Indentation,'')
Sfârşit;
Sfârşit;
funcția PAS_Print(Element:TJSONValue; Indentation:String; Key:String):String;
var
str: String;
tip: String;
ÎNCEPE
str := '';
tip:= '';
if (Element.ClassName = 'TJSONNull') then begin str := 'Null (Null)'; tip := 'Null'; Sfârşit
else if (Element.ClassName = 'TJSONTrue') then begin str := 'True (Boolean)'; tip := 'Boolean'; Sfârşit
else if (Element.ClassName = 'TJSONFalse') then begin str := 'False (Boolean)'; tip := 'Boolean'; Sfârşit
else if (Element.ClassName = 'TJSONString') atunci începe str := Element.toString+' (String)'; typ := 'Șir'; Sfârşit
else if (Element.ClassName = 'TJSONNumber') atunci începe str := Element.toString+' (Număr)'; typ := 'Șir'; Sfârşit
else if (Element.ClassName = 'TJSONObject') then begin str := 'Obiect {'; typ := 'Obiect'; Sfârşit
else if (Element.ClassName = 'TJSONArray') atunci începe str := 'Matrice ['; typ := 'Obiect'; Sfârşit;
dacă (Cheie = '')
apoi console.log(Indentation+' '+str)
else console.log(Indentation+' '+Key+': '+str);
Rezultat := tip;
Sfârşit;
procedura PAS_Output(JSONObject:TJSONObject; Indentation:String);
var
i: întreg;
tip: sfoară;
const
indentare = '____';
ÎNCEPE
dacă (JSONObject.ClassName = 'TJSONArray'), atunci
ÎNCEPE
dacă (Indentație = '') atunci
ÎNCEPE
console.log('PAS [');
Indentare := indentare;
Sfârşit;
for i := 0 to ((JSONObject as TJSONArray).Count - 1) do
ÎNCEPE
typ := PAS_Print((JSONObject ca TJSONArray)[i] ca TJSONValue, Indentation, IntToStr(i));
if (typ = 'Obiect') atunci
ÎNCEPE
PAS_Output((JSONObject ca TJSONArray)[i] ca TJSONObject, Indentation+indent);
console.log(Indentation+' }');
Sfârşit
else if (typ = 'Matrice') atunci
ÎNCEPE
PAS_Output((JSONObject ca TJSONArray)[i] ca TJSONArray, Indentation+indent);
console.log(Indentation+' ]');
Sfârşit;
Sfârşit;
if (Indentation = indent) atunci console.log(']');
Sfârşit
else if (JSONObject.ClassName = 'TJSONObject'), atunci
ÎNCEPE
dacă (Indentație = '') atunci
ÎNCEPE
console.log('PAS {');
Indentare := '____';
Sfârşit;
pentru i := 0 la (JSONObject.Count - 1) do
ÎNCEPE
typ := PAS_Print(JSONObject.Items[i], Indentation, JSONObject.Pairs[i].JSONString.Value);
if (typ = 'Obiect') atunci
ÎNCEPE
PAS_Output(JSONObject.Items[i] ca TJSONObject, Indentation+indent);
console.log(Indentation+' }');
Sfârşit
else if (typ = 'Matrice') atunci
ÎNCEPE
PAS_Output(JSONObject.Items[i] ca TJSONArray, Indentation+indent);
console.log(Indentation+' ]');
Sfârşit;
Sfârşit;
if (Indentation = indent) atunci console.log('}');
Sfârşit
altfel
ÎNCEPE
PAS_Print(JSONObject, Indentation,'')
Sfârşit;
Sfârşit;
ÎNCEPE
asm var JS_Object = JSON.parse(SampleObjectData); Sfârşit;
WC_Object := TJSJSON.parseObject(SampleObjectData);
PAS_Object := TJSONObject.ParseJSONValue(SampleObjectData) ca TJSONObject;
asm
funcția JS_Print(Element, Indentare, Cheie) {
var str = '';
var typ = '';
if (Element === null) { str = 'Null (Null)'; tip = 'Null'; }
else if (Element === true) { str = 'True (Boolean)'; typ = 'Boolean'; }
else if (Element === false) { str = 'Fals (Boolean)'; typ = 'Boolean'; }
else if (Array.isArray(Element)) { str = 'Matrice ['; tip = 'Matrice'; }
else if (typeof Element === 'șir') { str = '"'+Element+'" (Șir)'; typ = 'Șir'; }
else if (typeof Element === 'număr') { str = Element+' (Număr)'; tip = „Număr”; }
else if (typeof Element === 'obiect') { str = 'Obiect {'; typ = 'Obiect'; }
if (Cheie === nedefinit) { console.log(Indentation+' '+str); }
else { console.log(Indentation+' '+Key+': '+str); }
returnare (tip)
}
funcția JS_Output(JSONObject, Indentation) {
const indent = '____';
dacă (Array.isArray(JSONObject)) {
if (Indentație == '') {
console.log("JS [");
Indentation = indentare;
}
pentru (var i = 0; i < JSONObject.length; i++) {
comutator (JS_Print(JSONObject[i], Indentation,i)) {
caz „Obiect”: JS_Output(JSONObject[i], Indentation+indent); console.log(Indentation+" }"); pauză;
caz „Matrice”: JS_Output(JSONObject[i],Indentation+indent); console.log(Indentation+" ]"); pauză;
}
}
if (Indentation == indent) { console.log("]") }
}
else if (tip de JSONObject === „obiect”) {
if (Indentație == '') {
console.log("JS {");
Indentație = „____”;
}
pentru (cheie var în JSONObject) {
comutator (JS_Print(JSONObject[cheie],Indentation,key)) {
caz „Obiect”: JS_Output(JSONObject[cheie],Indentation+indent); console.log(Indentation+" }"); pauză;
caz „Matrice”: JS_Output(JSONObject[cheie],Indentation+indent); console.log(Indentation+" ]"); pauză;
}
}
if (Indentation == indent) { console.log("}") }
}
else {
JS_Print(JSONObject,Indentation);
}
}
Sfârşit;
asm JS_Output(JS_Object,''); Sfârşit;
WC_Output(WC_Object,'');
PAS_Output(PAS_Object,'');
Sfârşit;
Ieșirea console.log (doar JS, WC și PAS sunt aceleași):
JS [
____ 0: Matrice [
________ 0: Matrice [
____________ 0: 123 (număr)
____________ 1: Obiect {
cheie ________________: adevărat (boolean)
________________ altă cheie: „abc” (Șir)
________________ mai multe chei: Array [
____________________ 0: 1 (număr)
____________________ 1: 2 (număr)
____________________ 2: Fals (boolean)
________________ ]
____________ }
____________ 2: nul (nul)
________ ]
____ ]
]

23: Comparați JSON.

Cel mai rapid (sau cel puțin, cel mai simplu) mod de a compara două obiecte JSON ar fi să le convertiți în șiruri de caractere și să verificați dacă se potrivesc. Dar acest lucru poate să nu fie corect, având în vedere natura neordonată a elementelor obiect JSON. Dacă tot ce ai este o matrice sau dacă știi că obiectul tău este sortat după cheie sau printr-o altă metodă deterministă (ai creat elementele într-o anumită ordine, de exemplu), atunci compararea șirurilor ar putea funcționa în continuare. Dar dacă nu este cazul, este necesară o altă abordare pentru a determina această echivalență „semantică”. Aici repetăm unul dintre obiectele JSON și apoi verificăm că putem găsi totul în locul potrivit într-un al doilea obiect JSON. Pentru obiecte, doar căutăm cheile, astfel încât acestea să se potrivească și ordinea nu contează. Pentru Arrays, folosim indexul și ordinea contează într-adevăr. Această abordare face, de asemenea, posibilă ajustarea strictității comparației. De exemplu, dacă ați dori să aveți o potrivire care nu ține seama de majuscule și minuscule pe valorile șirului, de exemplu, sau mai puțină acuratețe (sau mai mult!) atunci când vine vorba de compararea numerelor.

 procedura TForm1.WebButton1Click(Expeditor: TObject);
var
WC_Object1: TJSObject;
WC_Object2: TJSObject;
PAS_Object1: TJSONObject;
PAS_Object2: TJSONObject;
const
SampleObjectData1 = '[[[123,{"key":true,"other key":"abc","mai multe chei":[1,3,false]},null,[{"a":"1", "b":2,"c":"3"}]]]]';
SampleObjectData2 = '[[[123,{"other key":"abc","key":true,"mai multe chei":[1,3,false]},null,[{"c":"3", "b":2,"a":"1"}]]]]';
funcția WC_Compare(Element1: TJSObject; Element2:TJSObJect):Boolean;
var
test: boolean;
i: întreg;
ÎNCEPE
test := adevărat;
dacă ((Element1 = zero) și (Element2 = zero)) atunci testează := adevărat
else if (Element1 = nil) sau (Element2 = nil) atunci testează := fals
else if ((Element1.toString = 'adevărat') şi (Element2.toString = 'adevărat')) atunci testează := adevărat
else if ((Element1.toString = 'false') și (Element2.toString = 'false')) atunci testează := adevărat
altfel dacă (TJSJSON.stringify(Element1).startsWith('[')) și (TJSJSON.stringify(Element2).startsWith('[')) și (lungime(TJSObject.keys(Element1)) = lungime(TJSObject.keys (Element2))) atunci
ÎNCEPE
pentru i := 0 la (lungime(TJSObject.keys(Element1))-1) nu
ÎNCEPE
dacă nu (WC_Compare(TJSObject(Element1[String(TJSObject.keys(Element1)[i])]),TJSObject(Element2[String(TJSObject.keys(ELement2)[i])]))) atunci
ÎNCEPE
test := fals;
Sfârşit;
Sfârşit;
Sfârşit
altfel dacă (TJSJSON.stringify(Element1).startsWith('{')) și (TJSJSON.stringify(Element2).startsWith('{')) și (lungime(TJSObject.keys(Element1)) = lungime(TJSObject.keys (Element2))) atunci
ÎNCEPE
pentru i := 0 la (lungime(TJSObject.keys(Element1))-1) nu
ÎNCEPE
dacă nu (WC_Compare(TJSObject(Element1[String(TJSObject.keys(Element1)[i])]),TJSObject(Element2[String(TJSObject.keys(Element1)[i])]))) atunci
ÎNCEPE
test := fals;
Sfârşit;
Sfârşit;
Sfârşit
altfel dacă (TJSJSON.stringify(Element1).startsWith('"')) și (TJSJSON.stringify(Element2).startsWith('"')) și (Element1.toString = Element2.toString) atunci testează := adevărat
else if (Element1.toString = Element2.toString) atunci testează := adevărat
altfel
ÎNCEPE
test := fals;
Sfârşit;
// dacă nu(test), atunci console.log(TJSJSON.stringify(Element1)+' <> '+TJSJSON.stringify(Element2));
Rezultat := test;
Sfârşit;
funcția PAS_Compare(Element1: TJSONValue; Element2:TJSONValue):Boolean;
var
test: boolean;
i: întreg;
ÎNCEPE
test := adevărat;
dacă ((Element1.ClassName = 'TJSONNull') și (Element2.Classname = 'TJSONNull')) atunci testează := adevărat
else if ((Element1.ClassName = 'TJSONNull') sau (Element2.Classname = 'TJSONNull')) atunci testează := false
else if ((Element1.ClassName = 'TJSONTrue') și (Element2.Classname = 'TJSONTrue')) atunci testează := adevărat
else if ((Element1.ClassName = 'TJSONFalse') și (Element2.Classname = 'TJSONFalse')) atunci testează := adevărat
else if ((Element1.ClassName = 'TJSONString') și (Element2.Classname = 'TJSONString')) și (Element1.ToString = Element2.ToString) atunci testează := adevărat
else if ((Element1.ClassName = 'TJSONNumber') și (Element2.Classname = 'TJSONNumber')) și (Element1.ToString = Element2.ToString) atunci testează := adevărat
else if ((Element1.ClassName = 'TJSONArray') și (Element2.Classname = 'TJSONArray')) și ((Element1 ca TJSONArray).Count = (Element2 ca TJSONArray).Count) atunci
ÎNCEPE
for i := 0 to ((Element1 as TJSONArray).Count - 1) do
ÎNCEPE
dacă nu(PAS_Compare((Element1 ca TJSONArray).Items[i], ((Element2 ca TJSONArray).Items[i]))) atunci
ÎNCEPE
test := fals;
Sfârşit;
Sfârşit;
Sfârşit
else if ((Element1.ClassName = 'TJSONObject') și (Element2.Classname = 'TJSONObject')) și ((Element1 ca TJSONOBject).Count = (Element2 ca TJSONObject).Count) atunci
ÎNCEPE
for i := 0 to ((Element1 as TJSONObject).Count - 1) do
ÎNCEPE
dacă nu(PAS_Compare((Element1 ca TJSONObject).Pairs[i].JSONValue,(Element2 ca TJSONObject).Get((Element1 ca TJSONObject).Pairs[i].JSONString.value).JSONValue)) atunci
ÎNCEPE
test := fals;
Sfârşit;
Sfârşit;
Sfârşit
altfel
ÎNCEPE
dacă Element1.ToString <> Element2.toString
apoi testeaza := false
test else := adevărat;
Sfârşit;
// dacă nu (test), atunci console.log(Element1.ToString+' <> '+Element2.ToString);
Rezultat := test;
Sfârşit;
ÎNCEPE
asm var JS_Object1 = JSON.parse(SampleObjectData1); Sfârşit;
asm var JS_Object2 = JSON.parse(SampleObjectData2); Sfârşit;
WC_Object1 := TJSJSON.parseObject(SampleObjectData1);
WC_Object2 := TJSJSON.parseObject(SampleObjectData2);
PAS_Object1 := TJSONObject.ParseJSONValue(SampleObjectData1) ca TJSONObject;
PAS_Object2 := TJSONObject.ParseJSONValue(SampleObjectData2) ca TJSONObject;
asm
funcția JS_Compare(Element1, Element2) {
var test = adevărat;
dacă ((Element1 === nul) && (Element2 === nul)) {test = adevărat}
else if ((Element1 === nul) || (Element2 === nul)) {test = fals}
else if ((Element1 === adevărat) && (Element2 === adevărat)) {test = adevărat}
else if ((Element1 === fals) && (Element2 === fals)) {test = adevărat}
else if ((Array.isArray(Element1)) && (Array.isArray(Element2)) && (Element1.length == Element2.length)) {
pentru (var i = 0; i < Element1.lungime; i++) {
dacă (!JS_Compare(Element1[i],Element2[i])) {
test = fals;
}
}
}
else if ((tip de Element1 === „șir”) && (tip de Element2 === „șir”) && (Element1 === Element2)) {test = adevărat}
else if ((tip de Element1 === „număr”) && (tip de Element2 === „număr”) && (Element1 === Element2)) {test = adevărat}
else if ((tip de Element1 === „obiect”) && (tip de Element2 === „obiect”) && (Object.keys(Element1).length == Object.keys(Element2).lungime)) {
pentru (cheie var în Element1) {
dacă (!JS_Compare(Element1[cheie],Element2[cheie])) {
test = fals;
}
}
}
else {
test = fals;
}
// dacă (!test) {console.log(JSON.stringify(Element1)+' <> '+JSON.stringify(Element2))};
return(test);
}
Sfârşit;
asm console.log('JS '+JS_Compare(JS_Object1, JS_Object2)); Sfârşit;
console.log('WC '+BoolToStr(WC_Compare(WC_Object1,WC_Object2),true));
console.log('PAS '+BoolToStr(PAS_Compare(PAS_Object1,PAS_Object2),true));
Sfârşit;
Ieșire console.log:
JS adevărat
WC Adevărat
PAS Adevărat

24: Sortați elementele obiectului JSON.

Obiectele JSON nu sunt ordonate, dar adesea este de dorit să le afișați sortate. Rețineți că nu actualizăm în mod deliberat obiectul JSON cu elementele sortate, ci mai degrabă repetăm prin ele, astfel încât rezultatul să fie sortat. Esența acestui lucru este doar extragerea tuturor cheilor din obiectul JSON, sortarea cheilor după orice metodă este la îndemână și apoi afișarea cheilor în ordinea lor nou sortată împreună cu valoarea corespunzătoare fiecăreia.

 procedura TForm1.WebButton1Click(Expeditor: TObject);
var
WC_Object: TJSObject;
PAS_Object: TJSONObject;
i: întreg;
WC_Keys: TStringList;
PAS_Keys: TStringList;
const
SampleObjectData = '{"apple":"fructe","banana":"fructe","orange":"fructe","morcov":"legume","cartofi":"legume"}';
ÎNCEPE
asm var JS_Object = JSON.parse(SampleObjectData); Sfârşit;
WC_Object := TJSJSON.parseObject(SampleObjectData);
PAS_Object := TJSONObject.ParseJSONValue(SampleObjectData) ca TJSONObject;
asm
var chei = Object.keys(JS_Object).sort();
pentru (var i = 0; i < keys.length; i++) {
console.log('Cheie JS: '+keys[i]+' Value: '+JS_Object[keys[i]])
}
Sfârşit;
// Există și alte moduri de sortare, dar nu am avut prea mult noroc aici.
// De exemplu, System.Generics.Collections ar bloca compilatorul din anumite motive
WC_Keys := TStringList.Create;
pentru i := 0 la lungime(TJSObject.keys(WC_Object))-1 do
WC_Keys.Add(TJSObject.keys(WC_Object)[i]);
WC_Keys.sort;
pentru i := 0 la WC_Keys.Count -1 do
console.log('Cheie WC: '+WC_Keys[i]+' Valoare: '+string(WC_Object[WC_Keys[i]]));
// Aceeași abordare folosită aici din același motiv
PAS_Keys := TStringList.Create;
pentru i := 0 la PAS_Object.Count-1 do
PAS_Keys.Add(PAS_Object.Pairs[i].JSONString.Value);
PAS_Keys.sort;
pentru i := 0 la PAS_Keys.Count -1 do
console.log('Cheie PAS: '+PAS_Keys[i]+' Valoare: '+PAS_Object.Get(PAS_Keys[i]).JSONValue.ToString);
Sfârşit;
Ieșire console.log:
Cheie JS: măr Valoare: fructe
Cheie JS: banană Valoare: fructe
Cheie JS: morcov Valoare: legume
Cheie JS: portocaliu Valoare: fructe
Cheie JS: cartof Valoare: legume
Cheie WC: măr Valoare: fructe
Cheie WC: banană Valoare: fructe
Cheie WC: morcov Valoare: legume
Cheie WC: portocaliu Valoare: fructe
Cheie WC: cartof Valoare: legume
Cheie PAS: măr Valoare: „fructe”
Cheie PAS: banană Valoare: „fructe”
Cheie PAS: morcov Valoare: „legume”
Cheie PAS: portocaliu Valoare: „fructe”
Cheie PAS: cartof Valoare: "legume"

25: Mutare de la JS la WC sau PAS.

Să presupunem că ați folosit JavaScript pentru a crea un pic de JSON sau se întâmplă să îl aveți dintr-un cod JavaScript din alt motiv, dar apoi doriți să-l faceți referire în Delphi folosind fie variantele WC, fie PAS pe care le-am folosit până acum. Aici definim un JS_Object ca o variabilă Delphi de tip JSValue, astfel încât să putem face referire la el mai târziu. Și apoi îl accesăm folosind doar funcțiile WC și funcțiile PAS pe care le-am folosit până acum, fără a fi nevoie măcar să definim variabile pentru a le păstra.

 procedura TForm1.WebButton1Click(Expeditor: TObject);
var
JS_Object: JSValue;
const
SampleObjectData = '{"apple":"fructe","banana":"fructe","orange":"fructe","morcov":"legume","cartofi":"legume"}';
ÎNCEPE
asm JS_Object = JSON.parse(SampleObjectData); Sfârşit;
// WC: cod Delphi pentru a accesa obiectul JSON JavaScript
// Încheierea lui în TJSObject() îl face echivalent cu un WC TJSObject
console.log('WC: '+TJSJSON.stringify(JS_Object));
console.log('WC: '+IntToStr(lungime(TJSObject.keys(TJSObject(JS_Object)))));
// PAS: Conversia la un TJSONObject prin Strings funcționează
console.log('PAS: '+(TJSONObject.parseJSONValue(TJSJSON.stringify(JS_Object)) ca TJSONObject).ToString);
console.log('PAS: '+IntToStr((TJSONObject.parseJSONValue(TJSJSON.stringify(JS_Object)) ca TJSONObject).Count));
Sfârşit;
Ieșire console.log:
WC: {"apple":"fructe","banana":"fructe","orange":"fructe","morcov":"legume","cartofi":"legume"}
WC: 5
PAS: {"apple":"fructe","banana":"fructe","orange":"fructe","morcov":"legume","cartofi":"legume"}
PAS: 5

26: Mutați de la WC la JS sau PAS.

Aici, WC_Object poate fi pur și simplu accesat direct în JS. Și codul PAS este practic același ca data trecută.

 procedura TForm1.WebButton1Click(Expeditor: TObject);
var
WC_Object: TJSObject;
const
SampleObjectData = '{"apple":"fructe","banana":"fructe","orange":"fructe","morcov":"legume","cartofi":"legume"}';
ÎNCEPE
WC_Object := TJSJSON.parseObject(SampleObjectData);
// JS: Funcționează ca și cum ar fi definit nativ în JS
asm
console.log('JS: '+JSON.stringify(WC_Object));
console.log('JS: '+Object.keys(WC_Object).lungime);
Sfârşit;
// PAS: Convertirea într-un TJSONObject prin Strings funcționează ca înainte
console.log('PAS: '+(TJSONObject.parseJSONValue(TJSJSON.stringify(WC_Object)) ca TJSONObject).ToString);
console.log('PAS: '+IntToStr((TJSONObject.parseJSONValue(TJSJSON.stringify(WC_Object)) ca TJSONObject).Count));
Sfârşit;
Ieșire console.log:
JS: {"apple":"fructe","banana":"fructe","orange":"fructe","morcov":"legume","cartofi":"legume"}
JS: 5
PAS: {"apple":"fructe","banana":"fructe","orange":"fructe","morcov":"legume","cartofi":"legume"}
PAS: 5


27: Treceți de la PAS la JS sau WC.

Și aici, poate ați creat ceva JSON într-o aplicație VCL folosind varianta TJSONObject (PAS) și apoi ați descoperit că doriți să îl utilizați direct în JavaScript sau cu alt cod care folosește varianta WC. Interesant, dacă scoateți PAS TJSONObject în timp ce utilizați JS, veți vedea structura de bază și câteva indicii despre motivul pentru care este mai costisitor de utilizat:

 {"fjv":{"apple":"fructe","banana":"fructe","orange":"fructe","morcov":"legume","cartofi":"legume"},"fjo$1 ":{"apple":"fructe","banana":"fructe","portocale":"fructe","morcov":"legume","cartofi":"legume"},"FMembers":{" FList":{"FList":[],"FCount":0,"FCacity":0}}}

Dar oferă și un indiciu despre cum să îl utilizați în JS - doar referiți la „fjv” și veți avea JSON. Conversia prin șiruri este posibilă și aici, dar există o proprietate JSObject convenabilă care face parte din TJSONObject, ceea ce face conversia la varianta WC aproape la fel de ușoară.

 procedura TForm1.WebButton1Click(Expeditor: TObject);
var
PAS_Object: TJSONObject;
const
SampleObjectData = '{"apple":"fructe","banana":"fructe","orange":"fructe","morcov":"legume","cartofi":"legume"}';
ÎNCEPE
PAS_Object := TJSONObject.parseJSONValue(SampleObjectData) ca TJSONObject;
// JS
asm
console.log('JS: '+JSON.stringify(PAS_Object['fjv']));
console.log('JS: '+Object.keys(PAS_Object['fjv']).lungime);
Sfârşit;
// TOALETA
console.log('WC: '+TJSJSON.stringify(PAS_Object.JSObject));
console.log('WC: '+IntToStr(lungime(TJSObject.keys(PAS_Object.JSObject))));
Sfârşit;
Ieșire console.log:
JS: {"apple":"fructe","banana":"fructe","orange":"fructe","morcov":"legume","cartofi":"legume"}
JS: 5
WC: {"apple":"fructe","banana":"fructe","orange":"fructe","morcov":"legume","cartofi":"legume"}
WC: 5

28: FireDAC la JSON (simplu).

JSON este adesea folosit pentru a conține seturi de date tradiționale - coloane și rânduri de date cu care probabil suntem mai obișnuiți. În acest exemplu, vom lua un exemplu de set de date și vom arăta cum arată când este convertit în JSON. FireDAC este o alegere populară, dar orice altă bază de date are probabil un mecanism similar pentru a trece de la o interogare sau tabel sau un fel de set de date într-o reprezentare JSON. Aici, se presupune că acest cod rulează ca parte a unei aplicații VCL, deoarece FireDAC în sine nu este (în întregime?) disponibil direct în TMS WEB Core. În acest caz, folosim metoda BatchMove, în care doar datele în sine sunt incluse în JSON. Eșantionul de date este doar ceva pe care l-am extras direct dintr-un proiect. Nimic deosebit de interesant sau de proprietate, doar o mână de coloane și câteva înregistrări care să arate cum arată.

 procedura TMyService.GetJSONData(ParametersOfSomeKind: String):TStream;
var
clientDB: TFDConnection;
qry: TFDQuery;
bm: TFDBatchMove;
bw: TFDBatchMoveJSONWriter;
br: TFDBatchMoveDataSetReader;
ÎNCEPE
Rezultat := TMemoryStream.Create;
// Un fel de conexiune la baza de date
clientDB := TFDConnection.Create(nil);
clientDB.ConnectionDefName := SomeDatabaseConnection;
clientDB.Connected := Adevărat;
// Un fel de interogare
qry.Conection := clientDB;
qry.SQL.Text := 'selectați * din DATAMARK.LUTS;';
qry.Deschis;
// Convertiți interogarea în JSON simplificat
bm := TFDBatchMove.Create(nil);
bw := TFDBatchMoveJSONWriter.Create(nil);
br := TFDBatchMoveDataSetReader.Create(nil);
încerca
br.Dataset := qry;
bw.Stream := Rezultat;
bm.Reader := br;
bm.Writer := bw;
bm.Execute;
in cele din urma
br.Free;
bw.Free;
bm.Free;
Sfârşit;
// Curățare după aceea
qry.Free;
clientDB.Connected := False;
clientDB.Free;
Sfârşit;

JSON rezultat ar putea arăta cam așa.

 [{"ID":1,"LOOKUP":0,"SORTORDER":0,"RESPONSE":"Administrație","MODIFICATOR":"ASIMARD","MODIFIED":"2021-11-17T19:42:34 ","GROUPTYPE":16},{"ID":2,"LOOKUP":1,"SORTORDER":1,"RESPONSE":"Labour","MODIFIER":"ASIMARD","MODIFIED":"2021 -11-17T19:47:52","GROUPTYPE":16},{"ID":3,"LOOKUP":2,"SORTORDER":2,"RESPONSE":"IT","MODIFICATOR":"ASIMARD ","MODIFICAT":"2021-11-17T19:42:56","GROUPTYPE":16}]

Dacă îl rulăm printr-un formatator JSON, devine puțin mai lizibil.

 [
{
„ID”: 1,
„CĂUTARE”: 0,
„SORTARE”: 0,
„RESPONSE”: „Administrație”,
„MODIFICATOR”: „ASIMARD”,
„MODIFICAT”: „2021-11-17T19:42:34”,
„TIP DE GRUP”: 16
},
{
„ID”: 2,
„CĂUTARE”: 1,
„SORTOR”: 1,
„RĂSPUNS”: „Munca de muncă”,
„MODIFICATOR”: „ASIMARD”,
„MODIFICAT”: „2021-11-17T19:47:52”,
„TIP DE GRUP”: 16
},
{
„ID”: 3,
„CĂUTARE”: 2,
„SORTOR”: 2,
„RĂSPUNS”: „IT”,
„MODIFICATOR”: „ASIMARD”,
„MODIFICAT”: „2021-11-17T19:42:56”,
„TIP DE GRUP”: 16
}
]

Acest lucru nu este teribil de eficient în ceea ce privește stocarea - o mulțime de text care se repetă, așa ceva. Deci, nu ați dori cu adevărat să faceți toate lucrările bazei de date exclusiv în JSON. Cu toate acestea, este compact în sensul că nu există nimic străin aici - Cheile reprezintă coloanele și Valorile reprezintă datele. Este un Array, așa că este ușor să ne dăm seama câte înregistrări există și utilizează câteva dintre tipurile de date JSON acceptate - Strings and Numbers. Există o dată/oră codificată în varianta mea preferată de ISO8601.

NOTĂ: Mi-am modificat sursa FireDAC pentru a scoate acest format. În mod implicit, cred că folosește YYYYMMDDTHHMMSS (fără separatori). Există o postare în Centrul de asistență TMS care discută acest lucru.

Un set de date formatat în acest fel în JSON este destul de util așa cum este. Chiar dacă nu știți în mod specific care sunt tipurile de câmpuri ale bazei de date, aveți o idee destul de bună. ID, LOOKUP, SORTORDER și GROUPTYPE par a fi numere întregi. RESPONSE și MODIFIER par a fi șiruri. Și MODIFIED pare a fi un marcaj de timp. Facem presupuneri aici, dar uneori este suficient, iar uneori nu este. Dar este simplu, așa că are asta pentru asta. Există multe situații în care ați putea să luați aceste date și să rulați cu ele fără alte considerații.

29 : FireDAC la JSON (Complex).

Similar cu cele de mai sus, exportăm un set de date în JSON. În acest caz, FireDAC are propria metodă de export în JSON, care include și metadate care descriu tipurile de date ale fiecărei coloane și o serie de alte lucruri care sunt probabil mai puțin interesante, dar utile dacă se deplasează între comenzile FireDAC în medii diferite.

 procedura TMyService.GetJSONData(ParametersOfSomeKind: String):TStream;
var
clientDB: TFDConnection;
qry: TFDQuery;
ÎNCEPE
Rezultat := TMemoryStream.Create;
// Un fel de conexiune la baza de date
clientDB := TFDConnection.Create(nil);
clientDB.ConnectionDefName := SomeDatabaseConnection;
clientDB.Connected := Adevărat;
// Un fel de interogare
qry.Conection := clientDB;
qry.SQL.Text := 'selectați * din eșantion;';
qry.Deschis;
// Convertiți interogarea în JSON FireDAC
qry.SaveToStream(Rezultat, sfJSON);
// Curățare după aceea
qry.Free;
clientDB.Connected := False;
clientDB.Free;
Sfârşit;

Nu prea multe de privit însă.

 {"FDBS":{"Version":15,"Manager":{"UpdatesRegistry":true,"TableList":[{"class":"Tabel","Name":"DATAMARK.LUTS","SourceName" :"DATAMARK.LUTS","SourceID":1,"TabID":0,"EnforceConstraints":false,"MinimumCapacity":50,"ColumnList":[{"class":"Column","Name":" ID","SourceName":"ID","SourceID":1,"DataType":"Int64","Precision":19,"Searchable":true,"Base":true,"OInUpdate":true," OInWhere":true,"OriginColName":"ID","SourcePrecision":19},{"class":"Column","Name":"LOOKUP","SourceName":"LOOKUP","SourceID":2 ,"DataType":"Int32","Precision":10,"Searchable":true,"Base":true,"OInUpdate":true,"OInWhere":true,"OInKey":true,"OriginColName":" LOOKUP","SourcePrecision":10},{"class":"Column","Name":"SORTORDER","SourceName":"SORTORDER","SourceID":3,"DataType":"Int32"," Precision":10,"Searchable":true,"Base":true,"OInUpdate":true,"OInWhere":true,"OriginColName":"SORTORDER","SourcePrecision":10},{"class":" Column","Name":"RESPONSE","SourceName":"RESPONSE","SourceID":4,"DataType":"AnsiString","Size":200,"Searchable":true," Base":true,"OInUpdate":true,"OInWhere":true,"OriginColName":"RESPONSE","SourcePrecision":200,"SourceSize":200},{"class":"Column","Name" :"DESCRIPTION","SourceName":"DESCRIPTION","SourceID":5,"DataType":"AnsiString","Size":250,"Searchable":true,"AllowNull":true,"Base":true ,"OAllowNull":true,"OInUpdate":true,"OInWhere":true,"OriginColName":"DESCRIPTION","SourcePrecision":250,"SourceSize":250},{"class":"Column"," Name":"MODIFIER","SourceName":"MODIFIER","SourceID":6,"DataType":"AnsiString","Size":32,"Searchable":true,"FixedLen":true,"Base" :true,"OInUpdate":true,"OInWhere":true,"OriginColName":"MODIFICATOR","SourcePrecision":32,"SourceSize":32},{"class":"Column","Name":" MODIFIED","SourceName":"MODIFIED","SourceID":7,"DataType":"DateTimeStamp","Searchable":true,"AllowNull":true,"Base":true,"OAllowNull":true," OInUpdate":true,"OInWhere":true,"OriginColName":"MODIFIED","SourcePrecision":26},{"class":"Column","Name":"GROUPTYPE","SourceName":"GROUPTYPE" ,"SourceID":8,"DataType":"Int32","Precision":10, "Searchable":true,"Base":true,"OInUpdate":true,"OInWhere":true,"OInKey":true,"OriginColName":"GROUPTYPE","SourcePrecision":10}],"ConstraintList": [],"ViewList":[],"RowList":[{"RowID":0,"Original":{"ID":1,"LOOKUP":0,"SORTORDER":0,"RESPONSE":" Administrare","MODIFIER":"ASIMARD","MODIFIED":"2021-11-17T19:42:34","GROUPTYPE":16}},{"RowID":1,"Original":{"ID" :2,"LOOKUP":1,"SORTORDER":1,"RESPONSE":"Labour","MODIFIER":"ASIMARD","MODIFIED":"2021-11-17T19:47:52","GROUPTYPE" :16}},{"RowID":2,"Original":{"ID":3,"LOOKUP":2,"SORTORDER":2,"RESPONSE":"IT","MODIFICATOR":"ASIMARD" ,"MODIFIED":"2021-11-17T19:42:56","GROUPTYPE":16}}]}],"RelationList":[],"UpdatesJournal":{"Changes":[]}}}}

Dacă îl rulăm printr-un formatator, este puțin mai ușor să vedem ce se întâmplă.

 {
„FDBS”: {
„Versiune”: 15,
"Administrator": {
„UpdatesRegistry”: adevărat,
„TableList”: [
{
"class": "Tabel",
„Nume”: „DATAMARK.LUTS”,
„SourceName”: „DATAMARK.LUTS”,
„SourceID”: 1,
„TabID”: 0,
„EnforceConstraints”: fals,
„Capacitate minimă”: 50,
„ColumnList”: [
{
"class": "Coloană",
„Nume”: „ID”,
"SourceName": "ID",
„SourceID”: 1,
„DataType”: „Int64”,
„Precizie”: 19,
„Se poate căuta”: adevărat,
„Baza”: adevărat,
„OInUpdate”: adevărat,
„OInWhere”: adevărat,
"OriginColName": "ID",
„SourcePrecision”: 19
},
{
"class": "Coloană",
„Nume”: „CĂUTARE”,
"SourceName": "CAUTĂ",
„SourceID”: 2,
„DataType”: „Int32”,
„Precizie”: 10,
„Se poate căuta”: adevărat,
„Baza”: adevărat,
„OInUpdate”: adevărat,
„OInWhere”: adevărat,
„OInKey”: adevărat,
"OriginColName": "CAUTĂ",
„SourcePrecision”: 10
},
{
"class": "Coloană",
„Nume”: „SORTORDER”,
"SourceName": "SORTORDER",
„SourceID”: 3,
„DataType”: „Int32”,
„Precizie”: 10,
„Se poate căuta”: adevărat,
„Baza”: adevărat,
„OInUpdate”: adevărat,
„OInWhere”: adevărat,
"OriginColName": "SORTORDER",
„SourcePrecision”: 10
},
{
"class": "Coloană",
„Nume”: „RĂSPUNS”,
„SourceName”: „RESPONSE”,
„SourceID”: 4,
„DataType”: „AnsiString”,
„Dimensiune”: 200,
„Se poate căuta”: adevărat,
„Baza”: adevărat,
„OInUpdate”: adevărat,
„OInWhere”: adevărat,
"OriginColName": "RĂSPUNS",
„SourcePrecision”: 200,
„SourceSize”: 200
},
{
"class": "Coloană",
„Nume”: „DESCRIPTION”,
„SourceName”: „DESCRIPTION”,
„SourceID”: 5,
„DataType”: „AnsiString”,
„Dimensiune”: 250,
„Se poate căuta”: adevărat,
„AllowNull”: adevărat,
„Baza”: adevărat,
„OAllowNull”: adevărat,
„OInUpdate”: adevărat,
„OInWhere”: adevărat,
"OriginColName": "DESCRIPTION",
„SourcePrecision”: 250,
„SourceSize”: 250
},
{
"class": "Coloană",
„Nume”: „MODIFICATOR”,
„SourceName”: „MODIFICATOR”,
„SourceID”: 6,
„DataType”: „AnsiString”,
„Dimensiune”: 32,
„Se poate căuta”: adevărat,
„FixedLen”: adevărat,
„Baza”: adevărat,
„OInUpdate”: adevărat,
„OInWhere”: adevărat,
"OriginColName": "MODIFICATOR",
„SourcePrecision”: 32,
„SourceSize”: 32
},
{
"class": "Coloană",
„Nume”: „MODIFICAT”,
„SourceName”: „MODIFICAT”,
„SourceID”: 7,
"DataType": "DateTimeStamp",
„Se poate căuta”: adevărat,
„AllowNull”: adevărat,
„Baza”: adevărat,
„OAllowNull”: adevărat,
„OInUpdate”: adevărat,
„OInWhere”: adevărat,
"OriginColName": "MODIFICAT",
„SourcePrecision”: 26
},
{
"class": "Coloană",
„Nume”: „GROUPTYPE”,
„SourceName”: „GROUPTYPE”,
„SourceID”: 8,
„DataType”: „Int32”,
„Precizie”: 10,
„Se poate căuta”: adevărat,
„Baza”: adevărat,
„OInUpdate”: adevărat,
„OInWhere”: adevărat,
„OInKey”: adevărat,
„OriginColName”: „GROUPTYPE”,
„SourcePrecision”: 10
}
],
„ConstraintList”: [],
„ViewList”: [],
„RowList”: [
{
„RowID”: 0,
„Original”: {
„ID”: 1,
„CĂUTARE”: 0,
„SORTARE”: 0,
„RESPONSE”: „Administrație”,
„MODIFICATOR”: „ASIMARD”,
„MODIFICAT”: „2021-11-17T19:42:34”,
„TIP DE GRUP”: 16
}
},
{
„RowID”: 1,
„Original”: {
„ID”: 2,
„CĂUTARE”: 1,
„SORTOR”: 1,
„RĂSPUNS”: „Munca de muncă”,
„MODIFICATOR”: „ASIMARD”,
„MODIFICAT”: „2021-11-17T19:47:52”,
„TIP DE GRUP”: 16
}
},
{
„RowID”: 2,
„Original”: {
„ID”: 3,
„CĂUTARE”: 2,
„SORTOR”: 2,
„RĂSPUNS”: „IT”,
„MODIFICATOR”: „ASIMARD”,
„MODIFICAT”: „2021-11-17T19:42:56”,
„TIP DE GRUP”: 16
}
}
]
}
],
„RelationList”: [],
„UpdatesJournal”: {
"Schimbări": []
}
}
}
}

Este mult teren de acoperit pentru aceleași trei înregistrări, dar nu există absolut nicio presupunere când vine vorba de tipurile de date pe care le folosim. Ceea ce căutăm aici. În mod curios, puteți vedea că există un câmp DESCRIERE în baza de date, dar probabil că nu avea date, așa că a fost exclus din înregistrările ulterioare. Exact genul de optimizare care are loc în culise și care va înnebuni o persoană!

30: JSON la TDataset (simplu).

Folosind JSON din varianta simplă, tot ceea ce am acoperit se aplică în ceea ce privește căutarea datelor sau sortarea sau orice altceva ar putea fi necesar. Adesea, unul dintre obiective este să introducem datele JSON într-un set TDataset local unde putem reveni cu bucurie la utilizarea tuturor instrumentelor noastre Delphi. În TMS WEB Core, una dintre modalitățile de a face acest lucru este utilizarea componentei TXDataWebDataSet . Puteți plasa unul dintre acestea într-un formular și apoi faceți dublu clic pe el pentru a afișa editorul familiar Fields în timp de proiectare. Când sunteți gata să încărcați date în setul de date, puteți apela SetJSONData cu o matrice JSON pentru a încărca setul de date. Există, de asemenea, familiarele TWebDataSet și TWebGrid. La momentul proiectării, ar putea arăta așa.

TMS Software Componente Delphi

Pentru a introduce datele în setul de date, există o funcție SetJSONData (). Există câteva avertismente aici. În primul rând, datele trebuie să fie în formatul care corespunde definițiilor coloanei sau vor fi excluse. Coloanele DateTime trebuie să aibă un anumit format, altfel vor fi excluse. Lucruri de genul acela. Așa că este puțin dificil de rezolvat primele două ori. De asemenea, nu există prea multe în ceea ce privește feedback-ul atunci când primește date cu care nu știe ce să facă. Iată cum arată.

 procedura TForm1.WebButton1Click(Expeditor: TObject);
var
WC_Object: TJSObject;
const
SampleObjectData = '[{"ID":1,"LOOKUP":0,"SORTORDER":0,"RESPONSE":"Administration","MODIFICATOR":"ASIMARD","MODIFIED":"2021-11-17T19: 42:34","GROUPTYPE":16},'+
'{"ID":2,"LOOKUP":1,"SORTORDER":1,"RESPONSE":"Labour","MODIFIER":"ASIMARD","MODIFIED":"2021-11-17T19:47:52 ","GROUPTYPE":16},'+
'{"ID":3,"LOOKUP":2,"SORTORDER":2,"RESPONSE":"IT","MODIFICATOR":"ASIMARD","MODIFIED":"2021-11-17T19:42:56 ","GROUPTYPE":16}]';
ÎNCEPE
WC_Object := TJSJSON.parseObject(SampleObjectData);
XDataWebDataSet1.SetJSONData(WC_Object);
XdataWebDataSet1.Open;
Sfârşit;

Rețineți că am făcut toată munca grea în mediul de proiectare - ne dăm seama ce coloane dorim, cât de lățime trebuie să fie și așa mai departe. Dacă lucrurile merg conform planului, ești gata să mergi cu o grilă care ar putea arăta cam așa.

TMS Software Componente Delphi

Și ai plecat și fugi!

31: JSON la TDataset (Complex).

Și în cele din urmă ajungem la problema pe care am întâlnit-o prima dată când am folosit TMS WEB Core. Ideea de bază este să folosesc aplicația TMS WEB Core ca client pentru, în cazul meu, un server XData. Serverul trimite date JSON din oricare dintre sutele de interogări diferite. Același lucru ar putea fi făcut pentru orice număr de alte surse de date, cum ar fi datele meteorologice și așa mai departe, care nu au nimic de-a face cu XData. JSON care ajunge apoi trebuie să fie gestionat de client.

Inițial, intenția a fost de a crea automat un TDataSet local cu datele, gata de funcționare. Cu toate acestea, fără a cunoaște toate definițiile câmpurilor din timp și fără a dori să le creăm pe toate în timpul proiectării, nu a existat nicio modalitate de a încărca datele folosind apelul SetJSONData (). Și în această etapă, datele JSON care vin au fost mai mult un mecanism de comunicare. Câmpurile sunt definite ca rezultat al interogării inițiale și vor trebui să fie cunoscute atunci când interfața de utilizare finală este prezentată utilizatorului, dar între ele chiar nu ne interesează ce se întâmplă - JSON este doar numele dat lui fluxul de date - conținutul acestuia nu prea contează în anumite etape.

Deci ce să fac? Ei bine, după cum am văzut, versiunea FireDAC a JSON are tot ce am avea vreodată nevoie să știm despre tipurile de date. Trucul este apoi să analizăm acea versiune a JSON pentru a obține numele coloanelor, tipurile de date, lățimile și așa mai departe, astfel încât să putem construi dinamic un TDataSet în timpul execuției fără a avea niciun fel de componente de proiectare. Afaceri complicate însă, deoarece trebuie să analizăm puternic datele JSON pentru a obține ceea ce avem nevoie. Inițial am reușit să găsesc exemple în Centrul de asistență TMS care m-au făcut să pornesc, dar a fost un pic un mister, deoarece totul era JSON în stil WC - deci nu varianta TJSONObject PAS cu care eram obișnuit, nici varianta JS omniprezentă. Dar a funcționat . Să ne dăm seama ce face de fapt, acum că știm ceva sau două despre JSON pe care nu le cunoșteam când am început.
 funcția GetQueryData(Endpoint: String; Dataset: TXDataWebDataSet):Integer;
var
Conexiune: TXDataWebConnection;
Client: TXDataWebClient;
Răspuns: TXDataClientResponse;
JFDBS: TJSObject;
JManager: TJSObject;
JTableList: TJSObject;
JColumnList: TJSArray;
JRowList: TJSArray;
StringFields: Matrice de TStringField;
IntegerFields: Matrice de TIntegerField;
// Probabil mai aveți nevoie de câteva tipuri de date aici
i: întreg;
ÎNCEPE
// Valoare pentru a indica că solicitarea a fost nereușită
Rezultat := -1;
// Configurați conexiunea la XData Server
Conn := TXDataWebConnection.Create(nil);
Conn.URL := DM1.CarnivalCoreServer; // ar putea fi, de asemenea, un parametru
Conn.OnRequest := PopulateJWT; // Vezi mai jos
Client := TXDataWebClient.Create(nil);
Client.Connection := Conn;
await(Conn.OpenAsync);
// Faceți cererea
// Probabil să aibă o altă versiune a funcției care include mai mulți parametri de punct final
încerca
Răspuns:= await(Client.RawInvokeAsync(Endpoint, ['FireDAC']));
cu exceptia
on Error: Exception do
ÎNCEPE
Client.Free;
Conn.Free;
console.log('...ceva este în neregulă...');
Ieșire;
Sfârşit;
Sfârşit;
// Procesează JSON-ul specific FireDAC care a fost returnat
JFDBS := TJSObject(TJSObject(TJSJson.Parse(string(Response.Result)))['FDBS']);
JManager := TJSObject(JFDBS['Manager']);
JTableList := TJSObject(TJSArray(JManager['TableList'])[0]);
JColumnList := TJSArray(JTableList['ColumnList']);
JRowList := TJSArray(JTableList['RowList']);
// Nu vreau cu adevărat „Original” în numele câmpurilor, așa că mai întâi să-l eliminăm din JSON
// Probabil un one-liner mai bun, dar asta pare să funcționeze
pentru i := 0 la JRowList.Length - 1 do
JRowList.Elements[i] := TJSObject(JRowList.Elements[i])['Original'];
// Presupunem că parametrul Dataset este nou creat și gol.
// Mai întâi, adăugați toate câmpurile din JSON
// NOTĂ: Foarte probabil, aici trebuie adăugate mai multe tipuri de date
pentru i := 0 la JColumnList.Length-1 do
ÎNCEPE
dacă (String(TJSObject(JColumnList.Elements[i])['DataType']) = 'AnsiString') atunci
ÎNCEPE
// NOTĂ: Diferite tipuri de date pot avea nevoie de un set de valori diferite (de exemplu: Dimensiune pentru șiruri)
SetLength(StringFields, Length(StringFields) + 1);
StringFields[Lungime(StringFields)-1] := TStringField.Create(Dataset);
StringFields[Length(StringFields)-1].FieldName := String(TJSObject(JColumnList.Elements[i])['Name']);
StringFields[Length(StringFields)-1].Size := Integer(TJSObject(JColumnList.Elements[i])['Size']);
StringFields[Length(StringFields)-1].Dataset := Dataset;
Sfârşit
else if (String(TJSObject(JColumnList.Elements[i])['DataType']) = 'Int32') atunci
ÎNCEPE
SetLength(IntegerFields, Length(IntegerFields) + 1);
IntegerFields[Lungime(IntegerFields)-1] := TIntegerField.Create(Dataset);
IntegerFields[Lungime(IntegerFields)-1].FieldName := String(TJSObject(JColumnList.Elements[i])['Nume']);
IntegerFields[Length(IntegerFields)-1].Dataset := Dataset;
Sfârşit
altfel
ÎNCEPE
console.log('EROARE: Câmpul ['+String(TJSObject(JColumnList.Elements[i])['Nume'])+'] are un tip de date neașteptat ['+String(TJSObject(JColumnList.Elements[i])[ 'DataType'])+']');
Sfârşit;
Sfârşit;
// Adăugați datele și returnați setul de date așa cum a fost deschis
Dataset.SetJSONData(JRowList);
Dataset.Open;
// Doar pentru distractie
Rezultat := Dataset.RecordCount;
// Nu există chestii de eliberare a setului de date deoarece setul de date a fost creat de apelant și
// toate câmpurile create au fost create cu acel set de date ca părinte
Client.Free;
Conn.Free;
Sfârşit;

Pentru mine, partea misterioasă a fost întotdeauna aceasta:

 // Procesează JSON-ul specific FireDAC care a fost returnat
JFDBS := TJSObject(TJSObject(TJSJson.Parse(string(Response.Result)))['FDBS']);
JManager := TJSObject(JFDBS['Manager']);
JTableList := TJSObject(TJSArray(JManager['TableList'])[0]);
JColumnList := TJSArray(JTableList['ColumnList']);
JRowList := TJSArray(JTableList['RowList']);
// Nu vreau cu adevărat „Original” în numele câmpurilor, așa că mai întâi să-l eliminăm din JSON
// Probabil un one-liner mai bun, dar asta pare să funcționeze
pentru i := 0 la JRowList.Length - 1 do
JRowList.Elements[i] := TJSObject(JRowList.Elements[i])['Original'];
Dacă împingem eșantionul nostru prin asta, putem vedea mai clar ce se întâmplă.
 JFDBS := TJSObject(TJSObject(TJSJson.Parse(string(Response.Result)))['FDBS']);

Aceasta se încarcă inițial JSON (Response.Result vine direct de la server) și se elimină învelișul exterior cel mai de sus, „FDBS”.

 JManager := TJSObject(JFDBS['Manager']);

Aceasta elimină un alt înveliș exterior, „Manager”.

 JTableList := TJSObject(TJSArray(JManager['TableList'])[0]);

Acest lucru elimină un alt înveliș exterior, „TableList”, dar încă mai are destule lucruri transportate.

 {
"class": "Tabel",
„Nume”: „DATAMARK.LUTS”,
„SourceName”: „DATAMARK.LUTS”,
„SourceID”: 1,
„TabID”: 0,
„EnforceConstraints”: fals,
„Capacitate minimă”: 50,
„ColumnList”: [
{
"class": "Coloană",
„Nume”: „ID”,
"SourceName": "ID",
„SourceID”: 1,
„DataType”: „Int64”,
„Precizie”: 19,
„Se poate căuta”: adevărat,
„Baza”: adevărat,
„OInUpdate”: adevărat,
„OInWhere”: adevărat,
"OriginColName": "ID",
„SourcePrecision”: 19
},
{
"class": "Coloană",
„Nume”: „CĂUTARE”,
"SourceName": "CAUTĂ",
„SourceID”: 2,
„DataType”: „Int32”,
„Precizie”: 10,
„Se poate căuta”: adevărat,
„Baza”: adevărat,
„OInUpdate”: adevărat,
„OInWhere”: adevărat,
„OInKey”: adevărat,
"OriginColName": "CAUTĂ",
„SourcePrecision”: 10
},
{
"class": "Coloană",
„Nume”: „SORTORDER”,
"SourceName": "SORTORDER",
„SourceID”: 3,
„DataType”: „Int32”,
„Precizie”: 10,
„Se poate căuta”: adevărat,
„Baza”: adevărat,
„OInUpdate”: adevărat,
„OInWhere”: adevărat,
"OriginColName": "SORTORDER",
„SourcePrecision”: 10
},
{
"class": "Coloană",
„Nume”: „RĂSPUNS”,
"SourceName": "RESPONSE",
„SourceID”: 4,
„DataType”: „AnsiString”,
„Dimensiune”: 200,
„Se poate căuta”: adevărat,
„Baza”: adevărat,
„OInUpdate”: adevărat,
„OInWhere”: adevărat,
"OriginColName": "RĂSPUNS",
„SourcePrecision”: 200,
„SourceSize”: 200
},
{
"class": "Coloană",
„Nume”: „DESCRIPTION”,
„SourceName”: „DESCRIPTION”,
„SourceID”: 5,
„DataType”: „AnsiString”,
„Dimensiune”: 250,
„Se poate căuta”: adevărat,
„AllowNull”: adevărat,
„Baza”: adevărat,
„OAllowNull”: adevărat,
„OInUpdate”: adevărat,
„OInWhere”: adevărat,
"OriginColName": "DESCRIPTION",
„SourcePrecision”: 250,
„SourceSize”: 250
},
{
"class": "Coloană",
„Nume”: „MODIFICATOR”,
„SourceName”: „MODIFICATOR”,
„SourceID”: 6,
„DataType”: „AnsiString”,
„Dimensiune”: 32,
„Se poate căuta”: adevărat,
„FixedLen”: adevărat,
„Baza”: adevărat,
„OInUpdate”: adevărat,
„OInWhere”: adevărat,
"OriginColName": "MODIFICATOR",
„SourcePrecision”: 32,
„SourceSize”: 32
},
{
"class": "Coloană",
„Nume”: „MODIFICAT”,
„SourceName”: „MODIFICAT”,
„SourceID”: 7,
"DataType": "DateTimeStamp",
„Se poate căuta”: adevărat,
„AllowNull”: adevărat,
„Baza”: adevărat,
„OAllowNull”: adevărat,
„OInUpdate”: adevărat,
„OInWhere”: adevărat,
"OriginColName": "MODIFICAT",
„SourcePrecision”: 26
},
{
"class": "Coloană",
„Nume”: „GROUPTYPE”,
„SourceName”: „GROUPTYPE”,
„SourceID”: 8,
„DataType”: „Int32”,
„Precizie”: 10,
„Se poate căuta”: adevărat,
„Baza”: adevărat,
„OInUpdate”: adevărat,
„OInWhere”: adevărat,
„OInKey”: adevărat,
„OriginColName”: „GROUPTYPE”,
„SourcePrecision”: 10
}
],
„ConstraintList”: [],
„ViewList”: [],
„RowList”: [
{
„RowID”: 0,
„Original”: {
„ID”: 1,
„CĂUTARE”: 0,
„SORTARE”: 0,
„RESPONSE”: „Administrație”,
„MODIFICATOR”: „ASIMARD”,
„MODIFICAT”: „2021-11-17T19:42:34”,
„TIP DE GRUP”: 16
}
},
{
„RowID”: 1,
„Original”: {
„ID”: 2,
„CĂUTARE”: 1,
„SORTOR”: 1,
„RĂSPUNS”: „Munca de muncă”,
„MODIFICATOR”: „ASIMARD”,
„MODIFICAT”: „2021-11-17T19:47:52”,
„TIP DE GRUP”: 16
}
},
{
„RowID”: 2,
„Original”: {
„ID”: 3,
„CĂUTARE”: 2,
„SORTOR”: 2,
„RĂSPUNS”: „IT”,
„MODIFICATOR”: „ASIMARD”,
„MODIFICAT”: „2021-11-17T19:42:56”,
„TIP DE GRUP”: 16
}
}
]
}

O mulțime de lucruri acolo de care nu avem nevoie. Vom ajunge la asta într-o clipă. Acum ne trecem la definițiile coloanelor.

 JColumnList := TJSArray(JTableList['ColumnList']);

Acesta are doar „ColumnList” de mai sus, dar încă multe extra.
 JRowList := TJSArray(JTableList['RowList']);

Acum avem de-a face doar cu secțiunea „RowList”, care este ceea ce avem nevoie pentru ca SetJSONData să-și facă treaba. Acesta arată ca următorul. Similar cu simplul JSON, care este, desigur, ceea ce căutăm în ceea ce privește datele.

 [
{
„RowID”: 0,
„Original”: {
„ID”: 1,
„CĂUTARE”: 0,
„SORTARE”: 0,
„RESPONSE”: „Administrație”,
„MODIFICATOR”: „ASIMARD”,
„MODIFICAT”: „2021-11-17T19:42:34”,
„TIP DE GRUP”: 16
}
},
{
„RowID”: 1,
„Original”: {
„ID”: 2,
„CĂUTARE”: 1,
„SORTOR”: 1,
„RĂSPUNS”: „Munca de muncă”,
„MODIFICATOR”: „ASIMARD”,
„MODIFICAT”: „2021-11-17T19:47:52”,
„TIP DE GRUP”: 16
}
},
{
„RowID”: 2,
„Original”: {
„ID”: 3,
„CĂUTARE”: 2,
„SORTOR”: 2,
„RĂSPUNS”: „IT”,
„MODIFICATOR”: „ASIMARD”,
„MODIFICAT”: „2021-11-17T19:42:56”,
„TIP DE GRUP”: 16
}
}
]
Când acesta este încărcat într-un TDataset, acea mică grupare de obiecte „originale” este puțin enervantă. Acest lucru scapă de el prin înlocuirea obiectului „Original” cu conținutul său.
 // Nu vreau cu adevărat „Original” în numele câmpurilor, așa că mai întâi să-l eliminăm din JSON
// Probabil un one-liner mai bun, dar asta pare să funcționeze
pentru i := 0 la JRowList.Length - 1 do
JRowList.Elements[i] := TJSObject(JRowList.Elements[i])['Original'];

Ceea ce duce la aceasta, forma finală care este importată prin SetJSONData:

 [
{
„ID”: 1,
„CĂUTARE”: 0,
„SORTARE”: 0,
„RESPONSE”: „Administrație”,
„MODIFICATOR”: „ASIMARD”,
„MODIFICAT”: „2021-11-17T19:42:34”,
„TIP DE GRUP”: 16
},
{
„ID”: 2,
„CĂUTARE”: 1,
„SORTOR”: 1,
„RĂSPUNS”: „Munca de muncă”,
„MODIFICATOR”: „ASIMARD”,
„MODIFICAT”: „2021-11-17T19:47:52”,
„TIP DE GRUP”: 16
},
{
„ID”: 3,
„CĂUTARE”: 2,
„SORTOR”: 2,
„RĂSPUNS”: „IT”,
„MODIFICATOR”: „ASIMARD”,
„MODIFICAT”: „2021-11-17T19:42:56”,
„TIP DE GRUP”: 16
}
]
Dar înainte de a putea încărca acest lucru în TDataset, trebuie să creăm toate câmpurile. Este un pic o corvoadă, în funcție de câte tipuri diferite de domenii ai de-a face, dar, în general, este un efort o singură dată pentru a sorta și apoi nu trebuie să te mai gândești la asta.

// Presupunem că parametrul Dataset este nou creat și gol.
// Mai întâi, adăugați toate câmpurile din JSON
// NOTĂ: Foarte probabil, aici trebuie adăugate mai multe tipuri de date
pentru i := 0 la JColumnList.Length-1 do
ÎNCEPE
dacă (String(TJSObject(JColumnList.Elements[i])['DataType']) = 'AnsiString') atunci
ÎNCEPE
// NOTĂ: Diferite tipuri de date pot avea nevoie de un set de valori diferite (de exemplu: Dimensiune pentru șiruri)
SetLength(StringFields, Length(StringFields) + 1);
StringFields[Lungime(StringFields)-1] := TStringField.Create(Dataset);
StringFields[Length(StringFields)-1].FieldName := String(TJSObject(JColumnList.Elements[i])['Name']);
StringFields[Length(StringFields)-1].Size := Integer(TJSObject(JColumnList.Elements[i])['Size']);
StringFields[Length(StringFields)-1].Dataset := Dataset;
Sfârşit
else if (String(TJSObject(JColumnList.Elements[i])['DataType']) = 'Int32') atunci
ÎNCEPE
SetLength(IntegerFields, Length(IntegerFields) + 1);
IntegerFields[Lungime(IntegerFields)-1] := TIntegerField.Create(Dataset);
IntegerFields[Lungime(IntegerFields)-1].FieldName := String(TJSObject(JColumnList.Elements[i])['Nume']);
IntegerFields[Length(IntegerFields)-1].Dataset := Dataset;
Sfârşit
altfel
ÎNCEPE
console.log('EROARE: Câmpul ['+String(TJSObject(JColumnList.Elements[i])['Nume'])+'] are un tip de date neașteptat ['+String(TJSObject(JColumnList.Elements[i])[ 'DataType'])+']');
Sfârşit;
Sfârşit;

Acum nu este atât de misterios să vedem ce se întâmplă, tipurile de date din coloană fiind extrase din JColumnList și, acolo unde este necesar, informații suplimentare despre tipurile de date. Deoarece acesta a fost scris inițial, a fost extins pentru a accepta câteva tipuri de date pe care le folosesc în mod regulat, dar să fii atent la mesajul „tip de date neașteptate” din console.log este o idee bună.

Concluzie.

Bine. Cam asta o acoperă. Cred că orice întrebări ulterioare care sunt de natură tehnică, îmbunătățiri sugerate pentru blocurile individuale de cod și așa mai departe ar trebui probabil tratate prin postări din Centrul de asistență TMS, unde avem puțin mai mult control asupra formatării și răspunsurilor în fire și curând. Dacă acesta se dovedește a fi un subiect popular, atunci crearea unui depozit GitHub pentru acesta poate avea, de asemenea, sens.

Și nu mă îndoiesc deloc că există îmbunătățiri care pot fi găsite în multe dintre aceste exemple. Mai mult de jumătate din secțiuni au fost scrise doar pentru acest articol, în special variantele WC. Deoarece sunt relativ nou în abordarea WC, este foarte probabil să fi trecut cu vederea sau să fi înțeles greșit unele dintre opțiunile disponibile. Acest lucru a fost scris și cu TMS WEB Core 1.9.8.3. Fără îndoială, versiunile viitoare ale TMS WEB Core și proiectul pas2js de bază vor introduce modificări și perfecționări care vor face posibile îmbunătățiri care ar putea să nu fie posibile astăzi. TJSONObject îi lipsesc câteva metode, de exemplu. JavaScript în sine a evoluat și va continua să evolueze, așa că sunt la fel de probabile îmbunătățiri în timp.

După cum am menționat la început, codul JS este JavaScript pur, iar codul WC și PAS sunt Delphi pur. Există cel puțin câteva locuri în care relaxarea acestei restricții ar putea simplifica sau îmbunătăți în alt mod și, în general, ar putea accelera unele părți din cod. Nu există cu adevărat niciun motiv pentru a păstra o separare strictă între ele, în afară de scopul nostru de a demonstra că există mai multe moduri de a face lucrurile, începând cu cel puțin trei. Cel mai bine este să utilizați orice variație sau combinație de variații care vă rezolvă cel mai bine problema, indiferent de unde se termină limitele unei variații și unde începe alta. Acesta este, desigur, scopul acestui articol - pentru a vă ajuta să vă oferiți instrumentele pentru a realiza cele mai uimitoare aplicații TMS WEB Core pe care le puteți!

Andrew Simard.