რა ვუყოთ ქართულ მთავრულ ასოებს უნიკოდში?

შარშან მაისში Unicode-მა ქართული მხედრული ანბანის 46 მთავრული ასონიშანი დაამტკიცა.

შეიძლება ჯერ კიდევ ადრეა, მაგრამ მომავალში ოპერაციული სისტემების განახლებებში მაინც აისახება ეს ცვლილება. ცნობისმოყვარეობის გამო გადავწყვიტე პატარა მოკვლევა გამეკეთებინა, თუ რას გვიცვლის ეს ამბები პროგრამისტებს და ამ პოსტში გაგიზიარებთ.

მანამდე ყოველი შემთხვევისთვის რამდენიმე განმარტება:
Unicode – სტანდარტი, რომელიც ყველა სიმბოლოს უნიკალური რიცხვითი კოდით განსაზღვრავს. ასევე აღწერს სხვადასხვა ენისთვის საჭირო სპეციფიურ წესებს. ამ სტანდარტმა მთელი ტექნიკური სამყარო მოიცვა და დღეს ყველა იყენებს მას, ვისაც ტექსტის დამუშავება / გამოსახვა სჭირდება – ოპერაციული სისტემები, პლატფორმები, ბრაუზერები…

UTF-8 – უნიკოდს სიმბოლოების და რიცხვების სია აქვს, თუმცა არ ანაღვლებს, როგორ მოხდება ამ ინფორმაციის შენახვა მეხსიერებაში. ამისთვის კოდირების სხვადასხვა ალგორითმი არსებობს. UTF-8 ერთ-ერთი და ყველაზე გავრცელებული მათგანია, რადგან ოპტიმალურად იყენებს ადგილს და ზედმეტ ბაიტებს არ გამოყოფს სიმბოლოსთვის, თუ ის ერთ ცალშიც ეტევა. კოდირების სხვა მაგალითებია UCS-2, UTF-16, UTF-32…

სტანდარტში ცვლილებები იმპლემენტაციებში ცვლილებებს იწვევს, რაც არც ისე სწრაფად ხდება. მაგალითად, ₾ ლარის სიმბოლო უნიკოდის მერვე ვერსიაში დაემატა 2015 წლის 17 მაისს და ვინდოუსის განახლება ამ სიმბოლოს ასახვისთვის 2016 წლის 19 იანვარს გამოვიდა.

ოპერაციულებმა უნდა განაახლონ კლავიატურის დრაივერი, რომ მომხმარებლებს CAPS რეჟიმის ჩართვით შეეძლოთ მთავრულის წერა. და ასევე უნდა განაახლონ სისტემური ფონტები, რომ font fallback-ის დროს სწორი სიმბოლოები გამოჩნდეს.

იმის გამო, რომ მთავრულის და არამთავრულის ერთსა და იმავე სიმბოლოს სხვადასხვა კოდი აქვს, პროგრამისტებს რაღაცების გათვალისწინება გვიწევს ხოლმე – აქამდე მხოლოდ სხვა ენებისთვის, ახლა კი ქართულისთვისაც. მაგალითად, როცა ხდება სტრიქონების შედარება, ძებნა, regex პატერნები, სორტირება, ბაზაში შენახვა და ა.შ.

 

მონაცემთა ბაზა

MS SQL server-ს ჩაშენებული აქვს უნიკოდის მხარდაჭერა და ოპერაციებისას ისედაც სტანდარტს უყურებს, მთავარია სწორ ვერსიას უყუროს: SQL Fiddle

აი, MySQL-თან ცოტა სხვაგვარადაა – აქ თითოეულ ბაზას, ცხრილს ან საერთოდ ველს შეიძლება ჰქონდეს განსაზღვრული კონკრეტული collation (წყობა?), იმის მიხედვით თუ რა ტიპის ინფორმაციას ინახავს. ჩვენ დაჩვეულები ვართ, რომ utf8_general_ci გამოვიყენოთ, რომელსაც ქართული ასოებიც ‘ესმის’. ეს collation სრულყოფილად არ აკეთებს უნიკოდის იმპლემენტაციას utf8_unicode_ci-ისგან განსხვავებით. უბრალოდ მეტი წარმადობისთვის იყენებდნენ. ახლანდელი პროცესორების პირობებში დიდი სხვაობა აღარ არის. სამაგიეროდ, utf8_unicode_ci ქართულ წყობას კარგად გაიგებს.

აი, მაგალითს მოვიყვან:
უნიკოდს კოდებთან ერთად განსაზღვრული აქვს სიმბოლოების მიმდევრობაც, რომელსაც სორტირებისას ვიყენებთ. მაგალითად ამ სიაში ყველა ქართული ‘ა’ სიმბოლო მიყოლებით არის – ნუსხურიც, ასომთავრულიც და მხედრულიც. მათ შემდეგ მოდის ‘ბ’ ასოს ვარიანტები. სავარაუდოდ ახალი მთავრულიც ანალოგიურად დაემატება.

SQL Fiddle

CREATE TABLE IF NOT EXISTS `test` (
  `content` varchar(200) NOT NULL
) DEFAULT CHARSET=utf8 COLLATE utf8_general_ci;
INSERT INTO `test` (`content`) VALUES
  ('აბგ'),  ('ააააა'),  ('Ⴁააააა'),  ('Ⴀააააა'),  ('bcd'),  ('ab.'),  ('Ⴄ'),  ('ж'),  ('Ж'),  ('ц'),  ('Ц');
  

CREATE TABLE IF NOT EXISTS `test_better` (
  `content` varchar(200) NOT NULL
) DEFAULT CHARSET=utf8 COLLATE utf8_unicode_ci;
INSERT INTO `test_better` (`content`) VALUES
  ('აბგ'),  ('ააააა'),  ('Ⴁააააა'),  ('Ⴀააააა'),  ('bcd'),  ('ab.'),  ('Ⴄ'),  ('ж'),  ('Ж'),  ('ц'),  ('Ц');


select * from `test` d order by d.content;
select * from `test_better` d order by d.content;

შედეგი:

ab., bcd, Ж, ж, ц, Ц, Ⴀააააა, Ⴁააააა, Ⴄ, ააააა, აბგ
ab., bcd, ж, Ж, ц, Ц, ააააა, Ⴀააააა, აბგ, Ⴁააააა, Ⴄ

ახლა რომ MySQL 8-ის ბეტა რელიზი გამოვიდა, იქ ჯერ უნიკოდის მეცხე ვერსიაა. ჩვენი მთავრული ასოები კი მეთერთემეტეშია 🙂

 

ჯავასკრიპტი

მართალია ბევრი რეალიზაცია არსებობს, მაგრამ V8-ს ვერ ავცდებით, ამიტო მაგის მიხედვით განვიხილავ.

ჯავასკრიპტს უნიკოდის მხარდაჭერა კი აქვს, თუმცა რაღაცები მაინც პრობლემურია (მაგალითად რეგექსი). თუ საიტზე გვჭირდება სორტირება ან შედარება (მაგალითად თუ ფილტრები გვაქვს), მაშინ ჩვეულებრივი სორტირება არ გამოგვადგება და Locale უნდა მივუთითოთ, რომ უნიკოდის წესები გათვალისწინოს:
მაგალითად,

let a = ['აბგ','ააააა','Ⴁააააა','Ⴀააააა','bcd','ab.','Ⴄ','ж','Ж','ц','Ц'];
console.log(a.sort());
console.log(a.sort(Intl.Collator('ru').compare));

ქართული Collation-ის მხარდაჭერა სამწუხაროდ საერთოდ არ აქვს. ამიტომ ნუსხურთან და ასომთავრულთან ერთად სწორად ვერ დაასორტირებს. ნუ, ეს მაინც ძალიან იშვიათი შემთხვევაა სადარდებლად რომ ღირდეს. სტანდარტულად, code point-ების მიხედვით ალაგებს და ანბანის მიხედვით გამოვა, მხოლოდ მთავრულ “ა” ასოს ჩვეულებრივი “ჰ”-ს მერე დასვამს ან პირიქით.

ამ პრობლემას სხვა ენებში სტრიქონების ერთ რეგისტრში გადაყვანით ვჭრით. გიორგიმ იდეა მომაწოდა:

myArray.sort(function(s1,s2){ return s1.toLowerCase() > s2.toLowerCase()}));

ქართულზეც სავარაუდოდ ეგრე იმუშავებს, მას შემდეგ რაც V8-ში უნიკოდის იმპლემენტაცია განახლდება. ახლა მაგალითად ასომთავრულზე და ნუსხურზე ეგრეა: "Ⴀ".toLowerCase() => "ⴀ"

როგორც ჩანს, იმის გამო რომ სტანდარტში ასომთავრულს CAPITAL აწერია, ხოლო ნუსხურს SMALL, ის ნუსხურის მთავრულ ვარიანტად არის იმპლემენტირებული (ეს v8 source ფაილი: unicode.cc, კოდებია პირდაპირ შესაბამებული.)
მხედრული ახლა caseless არის. საინტერესოა როგორი აღნიშნვა ექნება. მგონი სხვა ენა არ არსებობს, რომელსაც ორნაირი მთავრული ასოები აქვს.
Anyway, ამასაც ვერსიის განახლება სჭირდება.

ახლა გამახსენდა, რომ V8 საერთოდ open source პროექტია და მოხალისეს შეუძლია საერთოდ ქართული locale დაამატოს. ჯერ-ჯერობით ამის შედეგი ცარიელია:

Intl.Collator.supportedLocalesOf('ka')

 

ჯავა

ჯავაც არ ჩქარობს განახლებებს. JDK 9 უნიკოდის მერვე ვერსიით (სადაც ლარის სიმბოლო დაემატა) ორი წლის შემდეგ – 2017 წლის სექტემბერში გამოვიდა.
აქ სტრიქონების შედარება ხდება equals მეთოდით. მომავალში ქართულისთვისაც მოგვიწევს რომ equalsIgnoreCase გამოვიყენოთ:

"Ⴀ".equals("ⴀ")  => false
"Ⴀ".equalsIgnoreCase("ⴀ")  => true

ერთი მთავრული რადგან უკვე გვაქვს, მაგის გამოყენებით ვტესტავ, უბრალოდ პრაქტიკაში არ ვიყენებთ მაგ ასოებს.

ასევე რეგექსის დროსაც პირდაპირ ვერ მოვძებნით. ჩვეულებრივი i – ignore case არ გამოგვადგება, უნიკოდს სხვანაირად ამუშავებს. ამიტო:

"A".matches("(?i)[a]")  => true
"Ⴀ".matches("(?i)[ⴀ]") => false

Pattern.compile("[ⴀ]", Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE).matcher("Ⴀ").matches();  => true

შესაბამისად ყველგან სადაც სადაც სტრიქონებს ვიყენებთ, გათვალისწინება იქნება საჭირო – maps, sets, etc.

 

PHP

აქ საერთოდ უნიკოდთან ცუდი სამუშაოა + დაემატება აქაც გადაყვანა-გადმოყვანა.

 

 
კიდევ ცვლილება დაგვჭირდება ერთ ადგილას – ძებნის ძალიან მოსახერხებელ ხელსაწყოებში – grep და მისთანები. grep-ის case insensitive ოპცია ახლაც არ მუშაობს ქართულ ასომთავრულ ასოებზე. იმედია მაგათ ვერსიების განახლებაშიც აისახება, უებარი პროგრამებია დიდ (და პატარაც) ტექსტებში და ფაილებში რეგექსით საძებნად.

ბევრი აქაური სისტემა მაშინვე ვერც განაახლებს ალბათ პლატფორმებს, რადგან დიდი ტესტირება დასჭირდება. მაგათ ალბათ ფრონტში მოუწევთ ვალიდატორების და კონვერტერების დაყენება, რომ მომხმარებლის მთავრული ასოები ძველ ჯავაში ან სხვაგან კარგად წავიდეს.

საერთო ჯამში, მე მომწონს რომ მთავრული დაამატეს (რამდენიმე ადამიანის დიდი შრომის შედეგად). ქართული ენის ნაწილია და არ უნდა დაიკარგოს.

თქვენ ხომ არ გაქვთ იდეები, კიდევ რაზე აისახება ეს ცვლილება?

ბმულები მთავრული ასოების შესახებ:
On.ge – UNICODE-მა ქართული მხედრული ანბანის 46 მთავრული ასონიშანი დაამტკიცა
DevFest 2016: Akaki Razmadze –  ❤  [I LOVE UNICODE]
DevFest 2017: Akaki Razmadze – გუტენბერგი, სტივ ჯობსი, გუგლი, ხინკალი, უნიკოდი
DevFest 2016: Michael Everson – The confusing case history of Georgian in Unicode

Integration ტესტები მონაცემთა ბაზებით (Node.js + Mocha მაგალითზე)

კოდის ავტომატური ტესტები რამდენიმე ჯგუფად იყოფა. მოკლედ რომ ვთქვათ, Unit ტესტი გამოიყენება კოდის მცირე ფრაგმენტების გასატესტად. მაგალითად, თუ გვაქვს ტელეფონის ნომრის ფორმატირების ფუნქცია, მისთვის შეიძლება რამდენიმე unit ტესტი გვქონდეს სხვადასხვა შემთხვევის დასაფარად. მაგრამ თუ გვინდა შევამოწმოთ, როგორ რეგისტრირდება მომხმარებელი ამ ტელეფონის ნომრით და შემდეგ ავტორიზაციას გადის, უკვე რამდენიმე კომპონენტის ურთიერთობა უნდა მოვაქციოთ ჩვენს ტესტში – ეს integration ტესტია.

როგორც წესი, Integration ტესტთან გვაქვს საქმე, თუ ტესტი იყენებს:

  • მონაცემთა ბაზას
  • ქსელს
  • რაიმე გარე სისტემას (მაგ: მეილ სერვერს)
  • I/O ოპერაციებს

ზუსტად ამაშია მისი სირთულე, რომ უწყინარი unit ტესტების მსგავსად გარე სისტემებზე პირდაპირ ვერ შევასრულებთ სატესტო ოპერაციებს. მაგალითად, ასობით სატესტო მეილს ვერ გავგზავნით შემთხვევით გენერირებულ მისამართებზე. ასეთი პრობლემის გადასაჭრელად რამდენიმე გზა არსებობს, გააჩნია ჩვენ რისი ტესტირება გვსურს. მოდი ვარიანტებს შევხედოთ:

 

სერვისის იმიტაცია (Stubs, Mocks)

მაგალითად, ვწერთ კლიენტ აპლიკაციას, რომელიც სხვადასხვა სერვერზე არსებულ სერვისებს იძახებს. ანუ, ჩვენთვის კლიენტის კოდის შემოწმებაა მნიშვნელოვანი და არ გვსურს, რომ მან რეალურად გამოიყენოს სერვისები. ასეთ დროს შეგვიძლია სერვისის stub, ანუ სერვისის იმიტაცია გავაკეთოთ, რომელსაც ზუსტად იგივე ფუნქციები და პარამეტრები აქვს რაც ნამდვილს, უბრალოდ ლოგიკის შესრულების ნაცვლად პირდაპირ ჩვენს მიერ გაწერილ ფიქსირებულ პასუხებს აბრუნებს.

function sendMail(email, content) {
    console.log(‘Email sent to: ‘ + email);
    return true;
}

როდესაც ტესტირების რეჟიმში გავუშვებთ პროგრამას, მას ნამდვილი სერვისის ობიექტის ნაცვლად ჩვენი ყალბი სერვისი უნდა გამოვაყენებინოთ (ამის დეტალებს ცალკე პოსტში შემოგთავაზებთ).

 

მონაცემთა ბაზის გამოყენება

ახლა, ვთქვათ, სერვისს ვწერთ, რომელიც ინტენსიურად იყენებს მონაცემთა ბაზას და მის შესამოწმებლად გვინდა integration ტესტები. ცხადია, შეგვიძლია რომ ბაზის ფენაც stub-ით ჩავანაცვლოთ და select-ებმა, insert-ებმა, ა.შ. ფიქსირებული მნიშვნელობები დაგვიბრუნოს. თუმცა ხშირ შემთხვევაში ეს არაპრაქტიკულია და რეალურად პროცესებს შორის ურთიერთობა ვერ მოწმდება. მაგალითად, მინდა რომ მომხმარებელი დარეგისტრირდეს, გაააქტიუროს თავისი ანგარიში და გაიაროს ავტორიზაცია. ასეთი ოპერაციები ბაზის რამდენიმე ცხრილს იყენებს და მირჩევნია, რომ რეალურად ბაზაზე შესრულდეს.

ასეთ შემთხვევაშიც არსებობს რამდენიმე გამოსავალი. მაგალითად, მე ვამჯობინებ, რომ ცალკე ცარიელი ბაზა მქონდეს. არც in-memory და არც ბაზის მსუბუქი ალტერნატივა, არამედ ზუსტად იგივენაირი ბაზა, ოღონდ მხოლოდ ტესტებისთვის განკუთვნილი.
როდესაც ტესტირების რეჟიმში გაეშვება ჩემი პროგრამა, ის შესაბამისი კონფიგურაციიდან აიღებს სატესტო ბაზის მისამართს და სწორედ მას გამოიყენებს არენად. ტესტების ყოველი გაშვების წინ კი ცხრილებიდან ჩანაწერებს წაშლის, რომ არსებულმა მონაცემებმა ხელი არ შეუშალოს.

Node-ისა და Mocha-ს მაგალითზე გავაკეთებ ამას

წინა პოსტში სხვადასხვა გარემოს დაკონფიგურირებაზე ვსაუბრობდი. Mocha-ს ტესტებს მთლად ცალკე გარემოდ არ ვთვლი, რადგან შეიძლება გვქონდეს dev და test, ან კიდევ build სერვერები და ყველა მათგანზე ვუშვებდეთ ტესტებს. თუმცა ანალოგიურ მიდგომას – environment ცვლადებს გამოვიყენებ ტესტების გარემოს დასაკონფიგურირებლადაც და შევქმნი ფაილს .env.mocha

აქვე აღვნიშნავ: dotenv-ის აღწერაში თუ შეიხედავთ, იქ მუქად წერია, რომ არ არის რეკომენდირებული გვქონდეს .env, env.test, env.prod და ასე გამოყოფილი ფაილები, არამედ მხოლოდ ერთი .env ფაილი, რომელსაც სხვადასხვა სერვერზე სხვადასხვა შიგთავსი აქვს. ჩემი აზრით .env.mocha სულ სხვა მიზანს ემსახურება და ამ წესში არ შედის.

შემდეგი ნაბიჯია, რომ ტესტების გაშვების დროს ნამდვილის ნაცვლად .env.mocha ფაილი იყოს გამოყენებული. ინტერნეტში ამის კროს-პლატფორმული მუშა მაგალითი არ არის, მე კი ვინდოუსზე მიყვარს მუშაობა. ამიტომ ჩემს ვარიანტს შემოგთავაზებთ, სადაც ყველა ტესტ ფაილში არ მოგიწევთ კონფიგურაციის ჩატვირთვა:

  • პროექტის საქაღალდეში .env-ის გვერდით შევქმნათ .env.mocha და შევავსოთ სასურველი კონფიგურაციით.
  • test საქაღალდეში შევქმნათ ფაილი setup.js და შიგნით ჩავწეროთ ეს ხაზი:
    require('dotenv').config({path:__dirname + '/../.env.mocha'});
  • test საქაღალდეში შევქმნათ კიდევ ერთი ფაილი – mocha.opts და შიგნით ჩავწეროთ:
    --require test/setup.js

სულ ეს არის.
npm test-ს რომ გავუშვებთ პროექტზე, ტესტები ავტომატურად ყველგან .env.mocha კონფიგურაციას გამოიყენებენ.

თავი რომ დავიზღვიო და დარწმუნებული ვიყო რომ mocha-ს გაშვებისას ნამდვილად შესაბამისი კონფიგურაცია არის ჩატვირთული და სამუშაო ბაზა არ წავშალო, კიდევ რაიმე ცვლადს ჩავამატებ .env.mocha ფაილში და მხოლოდ მისი არსებობის შემთხვევაში გავაგრძელებ setup.js-ის შესრულებას (მაგალითად: MOCHA_CONFIG_LOADED=yes)

მე ასევე მინდა, რომ ყოველ გაშვებაზე ცხრილები დაცარიელდეს. Mocha-ს აქვს სხვადასხვა ჰუკები, მათ შორის before(), რომელსაც თუ describe-ის შიგნით აღვწერთ, მაშინ test suite-ის გაშვების წინ შესრულდება, ხოლო თუ გლობალურად აღვწერთ, მაშინ სულ ყველა ტესტამდე ერთხელ შესრულდება. სწორად მას გამოვიყენებ, რომ ტესტირების დაწყებისას ცხრილები დავაცარიელო. კარგი იქნებოდა, რომ setup.js ფაილშივე შეიძლებოდეს მისი აღწერა, მაგრამ თუ ცდით, ნახავთ რომ იქ mocha არ არის ჯერ ჩატვირთული და before ცვლადს ვერ იპოვის. ამიტომ test საქაღალდეში დავამატე ფაილი hooks.js და ამ ფაილში აღვწერე ჩემი გლობალური ჰუკები.

თუ Integration ტესტების შესრულებას დიდი დრო მიაქვს, შესაძლებელია ისე დააკონფიგურიროთ package.json-ში სკრიპტები, რომ unit ტესტებისა და integration ტესტების გაშვება (დირექტორიის დონეზე) ცალცალკე ბრძანებით ხდებოდეს.

dev/test/prod გარემოების კონფიგურაცია + Node.js


პროგრამულ პროექტზე მუშაობისას, როგორც წესი, რამდენიმე გარემო გვაქვს. ერთი დეველოპმენტის, ერთი რეალური, შეიძლება კიდევ ერთი სატესტოც იყოს. თითოეულ მათგანში პროექტის კონფიგურაცია განსხვავებულია – მაგალითად, მონაცემთა ბაზის ან სერვისების მისამართები, API გასაღებები, პაროლები, სხვადასხვა ოფციები..

ჩვენი მიზანია, რომ სხვადასხვა გარემოსთვის სხვადასხვა კონფიგურაცია არსებობდეს, თანაც ისე, რომ ამისთვის გარემოს ყოველი შეცვლისას კოდის გადაკეთება, ip-ების ჩასწორება და რაღაცების დაკომენტარება არ დაგვჭირდეს. კოდს არ უნდა შევეხოთ. მოკლედ, მთავარი წესი ეს არის, რომ კოდი და კონფიგურაცია მკაცრად იყოს გაყოფილი ერთმანეთისგან.

ინტერნეტში კონფიგურაციის მართვის სხვადასხვა გზას შეხვდებით. პირველ რიგში, გააჩნია როგორი ინფრასტრუქტურა გაქვთ. ზოგიერთ პლატფორმას ან ფრეიმვორკს თავისი სპეციფიური მიდგომა აქვს რეალიზებული. ასევე, თუ პროექტი დიდ, განაწილებულ სისტემაშია დანერგილი, შეიძლება რაიმე მზა პროდუქტის გამოყენება იყოს მიზანშეწონილი, რომელიც მთელ ინფრასტრუქტურას აკონფიგურირებს ავტომატიზაციით (meta: Terraform, Puppet, Chef) . ამ პოსტში ვისაუბრებ შემთხვევაზე, როცა ერთ ან რამდენიმე (ცოტა) სერვერზე ვნერგავთ ჩვენს პროექტს.

 

კონფიგურაციის ბილდში ჩაყოლება

შეიძლება შეგხვდეთ ასეთი ვარიანტი, რომ პროექტის გამზადების დროს შესაბამის ბილდერ ხელსაწყოს ეთითება არგუმენტი, თუ რომელი გარემოსთვის დაბილდოს პროექტი. მერე ეს ბილდერი ნახულობს შესაბამის კონფიგურაციას და შესაბამის ფაილს სვამს ბილდში.

კარგ ვარიანტს ჰგავს, რადგან საბოლოოდ მხოლოდ ერთი გამზადებული არქივი გვაქვს და პირდაპირ გავუშვებთ ჩვენს სერვერსა და მის კლონებზე, მაგრამ რაღაც მინუსები აქვს ასეთ გზას:

  • ვინც კი დაბილდავს, მისთვის ხელმისაწვდომი უნდა იყოს რეალური გარემოს კონფიგურაცია
  • თუ კონფიგურაცია შეიცვალა, ან სხვა გარემოზე გვინდა ბილდის გაშვება, თავიდან გახდება გასაკეთებელი ან ხელით უნდა ჩავასწოროთ შიგნით კონფიგურაცია, თუ შესაძლებელია (გააჩნია როგორ მოხდა დაბილდვა).
  • კიდევ ხომ არ გაქვთ იდეა?

თუმცა ზოგ შემთხვევაში მე მგონი მეტი გზა არ არის, მაგალითად, კლიენტის მხარეს თუ არის პროგრამა – მობილურის აპლიკაციას როცა ვბილდავთ სხვადასხვა გარემოსთვის და მაინც ერთ ტელეფონზე ვუშვებთ ყველას, ან ჯავასკრიპტის პროექტი თუა, ბრაუზერში გასაშვები. ისე, ასეთ დროს პაროლები არც ინახება ხოლმე კონფიგურაციის ფაილში.

 

Environment ცვლადები

ეს გზა სულ უფრო პოპულარული ხდება და თანამედროვე ფრეიმვორკებში ხშირად შეხვდებით .env ფაილებს. ანუ, კონფიგურაცია გატანილია ოპერაციული სისტემის (ან კონტეინერის) environment ცვლადებში და განსხვავდება სხვადასხვა სერვერზე, აპლიკაციის ბილდი კი ყველგან ერთი და იგივეა.

ამ გზას თუ აირჩევთ, უნდა დარწმუნდეთ რომ აღნიშნული ცვლადები დაცულად არის შენახული სერვერზე OS-ის სხვა იუზერებისგან და რამე პროცესი არ ლოგავს სადმე. ასევე, რომ სხვა პროგრამა არ აწერს თავზე ან არ იყენებს (რადგან namespace-ები არ არის). როგორც ვიცი, ამის უზრუნველყოფა არ არის რთული, თუ სპეციალურად გამოყოფთ იუზერს თქვენი აპლიკაციისთვის. ან თუ რაიმე კონტეინერს იყენებთ (მაგ: Docker), იქაც იზოლირებულია env ცვლადები.

 

Env ცვლადები Node.js პროექტში

დეველოპმენტის პროცესი რომ მარტივი იყოს ამ მხრივ და სამუშაო მანქანებზე არ გვჭირდებოდეს env ცვლადების ხელით შევსება, არსებობს პატარა მოდული – dotenv, რომელიც პროექტში არსებულ .env ფაილს უყურებს და გარემოს ცვლადებს აინიციალიზირებს შესაბამისად.
.env ფაილი არ უნდა ინახებოდეს version control სისტემაში (.gitignore-ში არის ჩასამატებელი მაგალითად, თუ გიტს ვიყენებთ).

თუ რომელიმე ცვლადი უკვე არსებობს ოპერაციულში, მაშინ მას თავზე არ გადაეწერება .env ფაილის მონაცემები. როგორც წესი, dotenv მხოლოდ დეველოპმენტის გარემოსთვის არის და –save-dev პარამეტრით აყენებენ, თუმცა თუ რეალურ გარემოში ჩვეულებრივ სერვერზე ვდებთ პროექტს და რამე სპეციფიური გზით არ ხდება env ცვლადების კონფიგურირება (Docker, Heroku), პრინციპში, იქაც შეიძლება ამის გამოყენება. თუ არა და შესაბამისად დავსეტავთ ამ ცვლადებს და პროექტს .env ფაილის გარეშე დავნერგავთ.

რჩევა: env ცვლადებში ჯობია boolean ტიპის პარამეტრები არ შევინახოთ, რომ შემოწმებისას არარსებული პარამეტრი false-ად არ აღიქვას. ჯავასკრიპტს მაინც მუხთალი კონვერტაციები აქვს ტიპებს შორის 🙂

 
ამ პოსტში ყველაფერს ვერ შევეხე, იმედია აზრი გადმოვეცი 🙂 თქვენ როგორი მეთოდით აგვარებთ ხოლმე გარემოს კონფიგურირების თემას?

Node.js + REST + Testing + Code coverage


აქამდე Node.js არასდროს გამომიყენებია production გარემოში. ახლა ერთ-ერთ პროექტზე გადავწყვიტეთ მისი ცდა და მინდოდა პატარა კვლევები აქაც გამომექვეყნებინა. რომ გავუშვებთ, შედეგებზე და პრობლემებზე მერე დავწერ.

ზოგადად, ერთი ხელის მოსმით რთული გასაკეთებელია არჩევანი. იმის მიუხედავად, რომ ჯავასკრიპტზეც არც ისე ცოტა მიწერია, არასდროს ისე მყარად არ ვგრძნობ თავს, როგორც ჯავაში ან სხვა strongly typed ენაში. მექანიკური შეცდომების პოვნა საკმაოდ რთულია და არც ვიცი ხოლმე გადავაწყდები თუ არა როდესმე ტესტირებისას. შეცდომების მართვა (Error handling) ცალკე თავისტკივილია, რადგან შეიძლება ერთმა შეცდომამ მთელი სერვისი მოკლას. ბიბლიოთეკებს შორის არ არის მათი სტაბილური და ერთნაირი დამუშავება.

რაღაც მხრივ, node-ზე მაინც საყვარლად და კომპაქტურად იწერება სერვისები. არის პრეცედენტები, რომ დიდი კომპანიები გადავიდნენ node-ზე და ჯერჯერობით თავს ართმევენ (მაგ: Paypal). ჩვენს შემთხვევაში სოკეტები და ბრაუზერის ჯავასკრიპტი გვჭირდებოდა ინტენსიურად, ამიტომ ბოლოს ისევ node-ზე გავჩერდით.

მაშ ასე: ამოცანა არის REST API-ის გაკეთება.
ყველა პატარა კლასის თუ ფუნქციის გამო მოდულების ჩასმა არ მიყვარს, რადგან თითოეული dependency პოტენციური პრობლემაა. ყველაფრის ნოლიდან წერასაც არ ვაპირებ, ამიტომ ვცდილობ ბალანსი დავიცვა და მინიმალური რაოდენობის და ფუნქციების მოდულები გამოვიყენო.

მარშრუტებისთვის (routing) Restify-ზე შევჩერდი. ვებ საიტისთვის Express კარგიაო, მაგრამ სერვისების შემთხვევაში ბევრი რამე უფრო მარტივად არის, ამიტომ Restify საკმარისი ჩანს.

ტესტირებისთვის Mocha ავიღე. Mocha-ს ნებისმიერ assertion ბიბლიოთეკასთან შეუძლია მუშაობა. სტატიებში შეგხვდებათ ტუტორიალები – Should.js, Chai, expect.js, better-assert, unexpected.. მაგრამ ჩემი აზრით სულ ზედმეტია ეგ ყველაფერი. Node-ის ჩაშენებული assert მოდული მშვენივრად ართმევს თავს ყველაფერს, სხვა დანარჩენი კი უბრალოდ სიტყვების გადალაგება-გადმოლაგებაა.

ადრე ტესტებს საერთოდ არ ვწერდი. რაღაცნაირად უმეტესად ისეთ პროექტებზე ვხვდებოდი, რომელიც ან საიტი იყო ან წებო სხვადასხვა სისტემას შორის, ამიტომ ავტომატური ტესტირება არ გამოდიოდა. ახლა ვხვდები, რომ ტესტების გარეშე არ შემიძლია. შეიძლება იფიქროთ, რომ მეტ დროს გახარჯინებთ, მაგრამ პირიქით ძალიან ბევრ დროს ზოგავს და რუტინულ სამუშაოს გაცილებთ თავიდან. განსაკუთრებით მადლობას ეტყვით საკუთარ თავს ცვლილებების დროს (Regression testing), რადგან ერთი ბაგის გასწორების დროს ხშირად ბევრ ახალს ვაჩენთ სხვა ადგილებში.

ტესტების დამხმარედ არსებობს Code coverage ხელსაწყოები, რომელიც გვაჩვენებს ჩვენმა ტესტებმა კოდის რა ნაწილები დაფარა. ჯავასკრიპტზეც არის რამდენიმე, მე Instanbul-ზე შევჩერდი, რადგან ბევრი აქებდა.

Unit ტესტებისთვის ეს საკმარისია, თუმცა მე API-ის გატესტვაც მინდა და ამისთვის რამე პატარა http client მჭირდება. ცხადია, ცარიელი node-ითაც შეიძლება გაკეთება, მაგრამ მოდი კიდევ ერთ მოდულს დავამატებ სრული კომფორტისთვის – Supertest.

ამ პოსტში ყველაფერს ერთად ავამუშავებ. ოღონდ აუცილებლად უნდა აღვნიშნო, რომ ნამდვილი პროექტი უკეთ უნდა იყოს სტრუქტურირებული. მარშრუტები სხვა ფაილში, კონტროლერების ლოგიკა ცალკე, მოდელები და ბაზის ფენა ცალკე.. ამ მაგალითებში ერთ ადგილას არის მთელი სერვისი და სერვერის კონფიგურაცია-გაშვებაც.

პროექტის შექმნა

პირველ რიგში ინსტალირებული უნდა გქონდეთ Node.js და npm (პაკეტების მენეჯერი). ბრძანებების გაშვება ხდება ტერმინალში.

გადავიდეთ პროექტის საქაღალდეში და გავუშვათ ბრძანება

npm init

ეს რამდენიმე კითხვას დაგისვამთ და შედეგად შექმნის package.json ფაილს, რომელშიც მომავალში ჩვენი dependence-ები აღიწერება.

მოდულების ინსტალაცია

ისევ დავდგეთ პროექტის საქაღალდეში და გავუშვათ ბრძანება

npm install restify --save

შეიქმნება node_modules საქაღალდე და იქ გადმოიწერება მოდული. –save პარამეტრის შედეგად კი ჩვენს package.json-ში ჩაემატება.

ეს იმიტომ არის საჭირო, რომ მაგალითად node_modules საქაღალდეს პროექტს თან არ აყოლებენ ხოლმე, თუნდაც version control სისტემაში. ვისაც პროექტის გაშვება დასჭირდება, package.json ეყოფა, რომ საჭირო dependence-ები ჩამოტვირთოს.

ანალოგიურად გადმოვწეროთ Mocha, Instanbul და supertest:

npm install mocha --save-dev
npm install nyc --save-dev
npm install supertest --save-dev

ამ შემთხვევაში –save-dev-ს ვიყენებ, რადგან ეს ბიბლიოთეკები მხოლოდ დეველოპმენტის პროცესშია საჭირო და Production გარემოში მათი ინსტალაცია ზედმეტია.

სერვერის შექმნა და მარშრუტების გაწერა

შევქმნი მარტივ GET სერვისს, რომელსაც მისამართში გადმოეცემა name პარამეტრი და მისალმების ტექსტს აბრუნებს პასუხად. თუ კლიენტმა application/json ფორმატი მოითხოვა, json ობიექტად დააბრუნებს, სხვა შემთხვევაში კი ჩვეულებრივ ტექსტად.

index.js ფაილი

var restify = require('restify');

// შევქმნათ restify-ის სერვერი და აღვწეროთ მარშრუტები
var server = restify.createServer();
server.get('/hello/:name', sayHello);
server.head('/hello/:name', sayHello);

// ფუნქცია მოთხოვნის დასამუშავებლად
function sayHello(req, res, next) {
    let content;
    let name = req.params.name;

    // თუ json-ს ითხოვს კლიენტი, json ობიექტი დაუბრუნდეს
    // წინააღმდეგ შემთხვევაში ჩვეულებრივი ტექსტი
    if (req.headers.accept.match(/json/i)) {
        content = { hello: name };
        // Restify ავტომატურად json-ს დააბრუნებს ასეთ დროს
    } else {
        content = 'hello ' + name;
        res.header('Content-Type','text/plain');
    }
    res.send(content);
    next();
}

// გავუშვათ სერვერი
server.listen(8080, function() {
  console.log('%s listening at %s', server.name, server.url);
});

// ექსპორტი საჭიროა, რომ შემდეგ Mocha-დან მივწვდეთ სერვერს.
module.exports = server;

ტესტების შექმნა

პროექტის საქაღალდეში შევქმნათ საქაღალდე ‘test’ და მასში ჩავწეროთ ნებისმიერი სახელის ფაილი ტესტებისთვის. ასეთი ფაილები ნებისმიერი რაოდენობით შეიძლება გვქონდეს.
test/testHello.js

let request = require('supertest');
let server = require('../index');
let assert = require('assert');

describe('Hello', function () {
    
    it('should say hello', done => {
        request(server)
            .get('/hello/elle')
            .set('Accept', 'text/plain')
            .expect('Content-type', 'text/plain')
            .expect(200, "hello elle", done);
    });

    it('should say hello with json', done => {
        request(server)
            .get('/hello/elle')
            .set('Accept', 'application/json')
            .expect('Content-Type', /json/)
            .expect(200, {
                hello: "elle"
            }, done);
    });
});

შემდეგ package.json-ის scripts-ში გავწეროთ ტესტის გასაშვები ბრძანება:

"scripts": {
  "test": "mocha"
}

შედეგად პროექტის საქაღალდიდან შეგვიძლია გავუშვათ ბრძანება

npm test

ასე გამოიყურება ტესტის შედეგი ჩემთან:

მოდი დავამატოთ code coverage. package.json-ში “test”: “mocha” შევცვალოთ შემდეგით: “test”: “nyc mocha” და ისევ გავუშვათ npm test.
ჩემს მაგალითზე კონსოლში გამოიტანა:

ახლა სრულად არის კოდი დაფარული ტესტებით. რაიმე ზედმეტ ფუნქციას ჩავამატებ და Uncovered Lines რაოდენობა გაიზრდება, თან ტერმინალში კარგად არ ჩანს შედეგი. html-ით რეპორტინგი გამოვიყენოთ. package.json-ში კვლავ შევცვალოთ კონფიგურაცია შემდეგით:

"test": "nyc --reporter=html --reporter=text mocha"

და კვლავ გავუშვათ npm test
პროექტის საქაღალდეში დაინახავთ, რომ შეიქმნა ახალი დირექტორია – coverage და მასში არის რეპორტის ფაილები. მაგალითად, ჩემი index.js ფაილის ანგარიში ასე გამოიყურება:

თუ ვერსიის კონტროლს იყენებთ, coverage და .nyc_output საქაღალდეები უმჯობესია .gitignore ფაილში დაამატოთ.

ერთიანი პროექტის გადმოწერა

პროექტის საქაღალდეში არ გამოვაყოლე მოდულები. შეგიძლიათ გაუშვათ npm install ბრძანება პროექტის დირექტორიიდან და package.json-ის მიხედვით დააინსტალირებს საჭირო მოდულებს იმავე დირექტორიაში.

ერთი კოდი – სხვადასხვა პლატფორმა: NativeScript და React Native

ამასწინათ გვინდოდა ერთი პროექტის გადაწერა ისეთი ფრეიმვორკით, რომ საერთო codebase გვქონოდა რამდენიმე პლატფორმაზე – ვებზე, Android-ზე და iOS-ზე. ახლა სამივესთვის ცალცალკე ერთისა და იმავეს წერა ცოტა ძვირიც ჯდება და დროც მეტი მიაქვს.

ამიტომ ეს სამი დღე კვლევისთვის მქონდა მოცემული. შედეგად ისეთი დასკვნა გამომივიდა, რომ ჯერჯერობით ისევ native-ზე ვრჩებით. თუმცა მინდა, რომ ეს პატარა კვლევა გავაზიარო პოსტში. თუ თქვენ გამოგიყენებიათ რომელიმე მათგანი ან რამე აპლიკაცია გაქვთ მაგით გამოქვეყნებული (NativeScript, React Native), დიდი სიამოვნებით მოგისმენდით. გულწრფელად მაინტერესებს რეკომენდაციები.

მაშ ასე: ჰიბრიდები თავიდანვე გამოვრიცხეთ. ჰიბრიდია მობილურის ისეთი აპლიკაცია, რომელიც html/css/javascript-ით იწერება და შემდეგ web view კომპონენტით იტვირთება აპლიკაციაში. ჩვეულებრივი საიტისგან განსხვავებით რაღაც დონეზე ხელმისაწვდომია მობილურის ოპერაციულის API, თუმცა ჩვეულებრივი საიტივით რენდერდება. Native აპლიკაცია მასთან შედარებით გაცილებით გლუვი და სწრაფია, user experience მაინც განსხვავდება, ამიტომ ჰიბრიდებს ძირითადად პატარა აპლიკაციების ან პროტოტიპებისთვის ირჩევენ ხოლმე.

ჰიბრიდის შესაქმნელი ფრეიმვორკები მრავლად არსებობს: Cordova (ყოფილი PhoneGap), Ionic, Sencha Touch, Mobile Angular UI, ა.შ.

გაცილებით ნაკლები ვიპოვე ისეთები, რომლებიც, საიტად რენდერის ნაცვლად, ამ ჯავასკრიპტის კოდს native ელემენტებს უსაბამებენ და ფაქტიურად native აპლიკაციებს ქმნიან. მაგალითად,

 

NativeScript

კომპანია Telerik-მა, რომელსაც შეიძლება ჰიბრიდის ბიბლიოთეკით იცნობთ, შექმნა NativeScript – ფრეიმვორკი, სადაც ანგულარის, typescript-ის ან ჯავასკრიპტის გამოყენებით შეგვიძლია შევქმნათ მობილურის native აპლიკაციები.

ტელეფონზე ეშვება ჯავასკრიპტის ვირტუალური მანქანა და reflection-ის, C++-ის და OS-ის აპის გამოყენებით ხიდს ქმნის native კომპონენტებთან. მეტი background დეტალებისთვის შეგიძლიათ ეს გვერდი ნახოთ: How NativeScript Works
თუმცა, ამ ხიდის მიუხედავად, ნამდვილი native-ის წარმადობასთან სხვაობას ვერ შეამჩნევთ.

ფრეიმვორკის ძირითადი ღირსებები ესენია:
– Native UI
– Extensible (Easily reuse existing plugins from npm, CocoaPods (iOS) and Gradle (Android) directly in NativeScript projects)
– Cross-Platform: Write and deploy native mobile apps for iOS, Android and (soon) Windows from a single code base
– Open source: Apache 2 license.

სწავლის მხრივაც საკმაოდ ადვილი ასათვისებელია. ბოლო დროს ანგულართან გაერთიანდნენ და იგივე არქიტექტურას და კლასების სახელებს იყენებენ. გაერთიანება ცოტა ხმამაღალი სიტყვაა მგონი, უბრალოდ აიღეს და ზუსტად იგივე სახელები დაარქვეს api-ის, რომ მომხმარებელს სწავლა არ მოუწიოს თავიდან.

საიტისგან განსხვავებით NativeScript-შიც და React Native-შიც view-ებს html-ით არ ვწერთ, არამედ xml-ით, რომელიც ანდროიდის ვიუებს ჰგავს.

<StackLayout>
	<Button ios:text="foo" android:text="bar"></Button>
</StackLayout>

შეგვიძლია კონკრეტული პლატფორმისთვის სხვა ლოგიკა ჩავდოთ. ერთი მაგალითი ზევით მოცემული ღილაკის ტექსტია. ჯავასკრიპტში შეგვიძლია დავწეროთ if (page.ios) { … }

დიზაინის ნაწილი – ფერები, ტექსტის ზომა, flex განლაგება – css სტილებით ხდება. ცხადია, ყველა სტილი არ არის ხელმისაწვდომი, მაგრამ native კომპონენტებთან რისი შესაბამებაც შეძლეს, ის სტილებია ფრეიმვორკში.

მე იმან მიმიზიდა, რომ ანგულართან გაერთიანების შედეგად მესამე – ვებ პლატფორმასთანაც შეიძლებაო კოდის გაზიარება. თუმცა არც ისეთი მარტივი აღმოჩნდა ეს გაზიარება. დიდი ნაწილი მაინც ცალცალკე რჩება. ჯავასკრიპტის რაღაც ნაწილები შეიძლება იქნას გატანილი ცალკე npm მოდულად, რასაც ორივე გამოიყენებს.
(არის ისეთი გზაც, სადაც ერთ პროექტში ერთიანდება ყველაფერი, მაგრამ ჩემი აზრით ძალიან ბევრ სირთულე და if -ები დაუმატა, ამიტომ ერთი ნახვით ვერ მოვიხიბლე).

მობილურის პლატფორმებს შორის კი კარგად ზიარდება თითქმის ყველაფერი.

დასატესტად ავიღე აპლიკაციის ერთ-ერთი შედარებით კომპლექსური გვერდი და იმის აწყობა ვცადე. საკმაოდ მარტივად გაკეთდა რაღაცები, თუმცა როგორც კი ცოტა რთული UI დამჭირდა, გავიჭედე. მაგალითად, ერთ-ერთი იყო ანდროიდის bottom tab bar და ამჯერად შეუძლებელია მაგის გაკეთება. საერთოდ, მათი showcase რომ ვნახე, იქიდან შემეპარა ეჭვი. ძალიან მარტივი და არც ისე მიმზიდველი აპლიკაციები იდო.
ჯერ ახალია და ერთ-ორ წელში ალბათ გაცილებით კარგი ფრეიმვორკი იქნება.

 

React Native

ამას გაცილებით შთამბეჭდავი გალერეა აქვს. ათასობით აპლიკაციაში ყოფილა გამოყენებული. React Native ფრეიმვორკი Facebook-ისგან მოდის და React-ს ჰგავს. ეს რომ ვცადე, ნამდვილად დავეთანხმე რამდენიმე სტატიას – NativeScript-თან შედარებით გაცილებით მეტი კონტროლი გაქვს UI-ზე, ამიტომ დიზაინში უფრო თავისუფალი ხარ, მაგრამ, სამაგიეროდ, მეტ კონტროლს მეტი კოდი მოჰყვება თან.

React Native ანალოგიურად ერთი კოდის პრინციპს იცავს, თუმცა პლატფორმების სპეციალურ სხვაობებს მაინც დიდ ყურადღებას აქცევს. როგორც ამბობენ, დიდ აპლიკაციებში 100% კოდს ვერ გააზიარებ iOS და Android-ს შორის. კი შეგიძლია სხვა იუზერების პლაგინები და კომპონენტები გამოიყენო და ერთნაირი გახადო, მაგრამ თუ პლატფორმების თავისებურებებს ‘პატივს სცემ’, ზოგ შემთხვევაში ცალცალკე კოდის წერა მოგიწევს.
მარტივი მაგალითი რომ მოვიყვანო, თარიღის ასარჩევი ველი სრულებით განსხვავდება ამ ორ პლატფორმაზე, როგორც გარეგნულად, ისე კონფიგურაციით.

React Native კი სწორედ ამას აძლევს პრიორიტეტს, რომ პლატფორმებს თავისებურება არ დაუკარგოს.
ზოგიერთ სტატიაში მის ლიცენზიაზე წუწუნებენ – BSD ლიცენზიით ვრცელდება, თუმცა მე მაინცდამაინც ვერ ვნახე დიდი შეზღუდვები.

 

დასკვნა

პროექტის გადაწერის მაგივრად, ნოლიდან რომ ვიწყებდე ახლა რაიმეს ცოტა კომპლექსური ინტერფეისით, ალბათ React Native-ს ავირჩევდი. თუმცა ესეც არ არის, რომ მეორე დღესვე მარტივად დაიწყებ წერას. იმ რამდენიმე გვერდისთვისაც კი ბევრი trial and error დამჭირდა – ცალკე გარემოს ვერსიების შეთავსებაში ინსტალაციისას და ცალკე უცხო ფრეიმვორკის ალღოს აღებაში. ამ ორივე ფრეიმვორკს ანდროიდის სქილები ეხმარება, რადგან ანდროიდშიც ანალოგიურად ხდება UI-ის აწყობა.

არსებობს კიდევ ერთი – Xamarin, სადაც C#-ზე წერ, უბრალოდ ეს ვინდოუსის სამყაროა და საჩვენო არ იყო. ისე Xamarin ცოტა განსხვავებულად იქცევა. მაგალითად, iOS-ის პროექტს პირდაპირ დივაისის ასემბლერში აკომპილირებს, ანდროიდისთვის კი Mono .Net ფრეიმვორკს იყენებს. დეტალები შეგიძლიათ საიტზე ნახოთ: Understanding the Xamarin mobile platform

სხვა ანალოგიური ფრეიმვორკი ვერ ვიპოვე. თქვენ ხომ არ იცით რამე მსგავსი?