elegant-javascript-senior-developerler-bunu-nece-yazir

Elegant Javascript:Senior Developerlər bunu necə yazır?

Javascript dili multiparadiqmalı bir dil olmaqla funksional, imperativ, hadisə əsaslı , aspektyönümlü və prototipə əsaslanan Obyektyönümlü paradiqmaları dəstəkləyir. Son bir neçə ildə Ecma standartına gətirilən yeniliklər imkan yaradırki, JS dilində yalnızca görünüş baxımından deyil, həmçinin OYP baxımında da yaxşı kod yazaq.

               Bu məqalədə JS dilində bir senior developerin yazacağı eleqant kod nümunəsi ilə tanış olacağıq. Məqalənin əsas məqsədi, düzgün başa düşülərsə JS dilində də digər OYP dilləri kimi gözoxşayan, genişlənə bilən , rahat başa düşülən kod yazmağın asan olduğunu göstərmək, həmçinin dili öyrənənlər üçün real bir developer gözündən kod yazmağı göstərməkdir.

               Ölkəmizdə hazırda bir çox proqramçılar JS dilini sadəcə funksional aspektdən istifadə edir. Bu proyekt onlar üçün də OYP-yə yaxşı bir entry point olacaq.

Məqalədə JS dilinin aşağıdakı funksionallıqları istifadə olunub:

1)     modulyar komponentlər və import/export

2)     async/await

3)     promislər( dolayı yolla fetch ilə)

4)     düzgün modullara parçalama

5)     prototip əsasında genişləndirmə

6)     abstract klas yaratma yanaşması

7)     JS OOP və.s


 Proqramla tanışlıq

               Hazırladığımız proqram müasir dövrdə ən çox tələb olunan API ilə işə həsr olunub. Bu kod nümunəsində backendin verdiyi bir APİ necə emal olunmasına baxacağıq və bunu bir senior developer yanaşması ilə etməyə çalışacağıq.

PS: Tam kodu bu linkdən əldə edə bilərsiniz.

               Proyektin sadə təsviri belədir: Index səhifəmizdə açılan APİ-yə sorğu gedir və bizim müəyyən etdiyimiz və görünməsini istədiyimiz məlumatlar səhifələnmə əsasında(paginatable) ekranda görünür. Daha sonra istənilən bir elementin şəklinə klik etməklə onun haqqında detallı məlumat göstərən səhifə açılır. Əlbəttə, ilk başdan bəsit görünsə də bu nümunə ilə front proqramın core hissəsini yığmış oluruq, sonrakı səhifələri əlavə etmək cəmi bir neçə dəqiqəmizi alır.

               PS: caching realizə olunmayıb! Bir challenge kimi, caching funksionallığını siz realizə edə bilərsiniz.


Hər hansı bir istifadəçiyə klik etdikdə onun haqqında detallı məlumat görünür.


Let’s get started!

Index.html səhifəsində məlumatların görünməsini təmin edən bütün modullar initialize.js faylından paylanır.

import { UrlBuilder } from '/js/lib/extensions/urlBuilderExtensions.js'
import RestAPI from '/js/lib/restAPI.js'
import DOMRender from '/js/lib/domRender.js'
import PaginationRender from '/js/lib/paginationRender.js'
import SearchParser from '/js/lib/searchParser.js'
export { UrlBuilder, RestAPI, DOMRender, PaginationRender, SearchParser }

Səhifədə çağırmaq üçün isə

 import { UrlBuilder, RestAPI, PaginationRender, DOMRender, SearchParser } from './js/initialize.js'

Göründüyü kimi proqramın core hissəsini təşkil edən 5 modulumuz var.

UrlBuilder- dinamik şəkildə url yaratmağa imkan verir

RestAPİ – APİ sorğular verir

PaginationRender- Ekranda məlumatları səhiflənmə(pagination) prinsipi ilə göstərir

DomRender- gələn api məlumatlarını səhifədə render edir.

SearchParser- pagination işləməsi üçün urldən parametrləri qəbul edərək emal edir.

               İlk öndə URLbuilderdən başlayaq. Urlbuilder dinamik şəkildə url generasiyasını təmin etmək, manage etmək üçündür. Klas həmçinin düzgün ardıcıllıqla url generasiyasına cavabdehdir. Misalçün queryStringdən sonra segment yazmaq kimi url səhvlərinin qarşısını alır. Həmçinin bir necə queryString qurulması prosesini təmin edir.

import String from './extensions/stringExtensions.js'
export default class UrlBuilder {

    #url = new String().empty();
    #segmentSeparator = "/";
    #withQueryString = "?";
    #andwithQueryString = "&";

    constructor(baseUrl) {
        this.#url = baseUrl;
    }

    segment(segment) {
        this.#url += this._makeSegment(segment);
        return this;
    }

    withQueryString(key, value) {
        this.#url += this._makewithQueryString(key, value);
        return this;
    }

    build() {
        return this.#url;
    }

    _alreadyHaswithQueryString() {
        return this.#url.indexOf(this.#withQueryString) > 0;
    }


    _makeSegment(segment) {
        if (this._alreadyHaswithQueryString())
            throw new Error("Url building is invalid! Use segments before query string!!");
        return this.#segmentSeparator + segment;
    }


    _makewithQueryString(qStr, val) {
        if (this._alreadyHaswithQueryString())
            return this.#andwithQueryString.concat(qStr, "=", val);
        else
            return this.#withQueryString.concat(qStr, "=", val)
    }
}

Urlbuilder ilə proyektə uyğun adaptive işləməni təmin etmək üçün onu extend edərək extensionlar yazmışıq. Bu extensionlar daha asan formada url qurmağa, kontekst daxilində məsələnin həllinə yönəlməyə kömək edir.

import UrlBuilder from '../urlBuilder.js'
//most used param, so as prototype
UrlBuilder.prototype.id = function (id{
    return this.segment(id);
}

UrlBuilder.prototype.posts = function () {
    return this.segment("posts");
}

UrlBuilder.prototype.comments = function () {
    return this.segment("comments");
}

UrlBuilder.prototype.users = function () {
    return this.segment("users");
}

export { UrlBuilder }

Proyekt daxilində urlBuilderi belə istifadə edirik:

                //prepare url
            let url = new UrlBuilder(baseUrl)
                .users()
                .build();

            console.log(url);
            //console pəncrəsində bu görünür: https://jsonplaceholder.typicode.com/users

Səhifələnmə prosesini təmin etmək üçün SearchParser klasımız var. Həmin klas url hissəsindən verilən parametrləri emal etməyə köməklik edir. Url hissəsindən parametrlər bizdə pagination işləyən zaman istifadə olunur. Əgər misalçün sayt/index.html?page=1 gələrsə deməli pagination üçün ilk səhifə göstərilməlidir. Məhz url hissəsindən lazımi əmrlərin alınmasını təmin ermək üçün SearchParser klasımız var.

export default class SearchParser {
    #key = null;
    constructor(key) {
        this._searchContent = location.search;
        this.#key = key == undefined ? "page" : key;
    }

    get Key() {
        return this._searchContent.substring(1this._searchContent.indexOf("="));
    }

    get Value() {
        let searchValue = this._searchContent.substr(this._searchContent.indexOf("=") + 1);
        if (this.Key == this.#key) {
            let value = 1;
            if (searchValue != null && searchValue != undefined) {
                value = parseInt(searchValue);
            }
            return value;
        }
        else
            return 1;
    }

    isPaginatable() {
        return this.Key == this.#key;
    }
}

Klas url hissəsindən konstruktorda verilən parameterə uyğun queryString argumenti verildiyini yoxlayır. Əgər konstruktorda heçnə göstərilməyibsə ozaman default olaraq page sözünü queryStringdə axtarır.

Sorğu verəcəyimiz url və search parametri tənzimləndikdən sonra RestAPİ klasımız initialize olunur. Bu klasın əsas məqsədi sadəcə verilən urlə asinxron sorğu göndərməkdir. (Klas proyektə uyğun hələki tam realizə olunmayıb, Command operationları yerinə yetirə bilmir)

export default class RestAPI {
    async queryDataAsync(url) {
        return await fetch(url)
            .then(rspns => rspns.json());
    }
    async commandDataAsync(url, params) {
         //not realized yet. 
        await fetch(url, params)
            .then(response => response.json())
    }
}

Sorğudan cavab alındıqdan sonra artıq həmin cavabı sadəcə DomRender klasına veririk. Bu klas məlumatları ekranda birbaşa və ya rekursiv formada əks etdirir.Bu klas abstrakt klas olan DataRender klasından əmələ gəlib. JS-də abstract klas konsepsiyasını göstərmək üçün sadəcə metodları elan edib exception qaldırmaq kifayət edir.

export default class DataRender {
    _data = [];
    getData() {
        throw new Error("method is not implemented yet");
    }
    render() {
        throw new Error("method is not implemented yet");
    }

    setData(data) {
        throw new Error("method is not implemented yet");
    }
}

 DomRender klası məlumatları heç bir pagination tətbiq etmədən, aldığı informasiyaların hamısını əlks etdirmək qabiliyyətinə malik olan DataRender törəməsidir.

import DataRender from './dataRender.js';
import ProfileBuilder from './profileBuilder.js'
export default class DOMRender extends DataRender {
    constructor(data) {
        super();
        this._data = data;
    }

    getData() {
        return this._data;
    }

    setData(data) {
        this._data = data;
    }

    _renderRecursively(obj, arr, root) {
        for (let key in obj) {
            if (typeof obj[key] == "object") {
                this._renderRecursively(obj[key], arr, key);
            }
            else {
                if (typeof root != "string") {
                    arr.set(key, obj[key]);
                }
                else {
                    arr.set(root + "-" + key, obj[key]);
                }
            }
        }
    }
    _makeSelector(obj) {
        let keyValuePair = {
            keyObject.keys(obj)[0],
            valueObject.values(obj)[0]
        }
        return keyValuePair;
    }
    _renderPartially(selector, ...props) {
        for (let dataItem of this._data) {
            let containerSelector = this._makeSelector(dataItem);
            let profile = new ProfileBuilder(containerSelector)
                .addPhoto('img/user.png');
            props.forEach((x) => {
                profile = profile.addElement(x, dataItem[x]);
            });
            $(selector).append(profile.build());
        }
    }
    _renderAll(selector) {
        let dictionary = new Map();
        for (let dataItem of this._data) {
            this._renderRecursively(dataItem, dictionary, this._data);
            let containerSelector = this._makeSelector(dataItem);
            let profile = new ProfileBuilder(containerSelector).addPhoto('img/user.png');
            dictionary.forEach((val, key) => {
                profile.addElement(key, val);
            });
            $(selector).append(profile.build());
        }
    }
    render(selector, ...props) {
        if (props.length == 0) {
            this._renderAll(selector);
        }
        else {
            this._renderPartially(selector, ...props);
        }
    }
}

DomRender klasını çağırdıqda sadəcə bütün məlumatları render edəcək. DomRender məlumatları div containerlərə yığmaq üçün ProfileBuilder klası istifadə edir. Klasın əsas məqsədi ekranda görünəcək hər bir istifadəçi məlumatını qurmaqdan ibarətdir.

export default class ProfileBuilder {
    #domComponent = $("<div class='profile-container'></div>");
    constructor(marker) {
        let _class = "profile-container";
        this.#domComponent = $(`<div class='${_class}'></div>`);
        if (marker != undefined && marker != null && marker != "") {
            this.#domComponent.attr(`data-${marker.key}`, marker.value);
        }
    }
    
    addPhoto(src) {
        let img = $(`<img src='${src}' class='profile-img'>`);
        this.#domComponent.append(img);
        return this;
    }

    addElement(key, text) {
        let domItem = $(`<div class='profile-item profile-${key}'></div>`)
            .html(`<p><span class='profile-item-key profile-item-${key}'>${key}</span> >span class='profile-item-value profile-item-${text}'>${text} </span></p>`);
        this.#domComponent.append(domItem);
        return this;
    }
    build() {
        return this.#domComponent;
    }
}

ProfileBuilder imkan verirki hər dəfə alınan bir massiv elementindən (bir json parçasından) istifadəsi profili formalaşdıraq.

            //prepare url
            let url = new UrlBuilder(baseUrl)
                .users()
                .build();

            let searchHelper = new SearchParser();

            //prepare api
            let restAPI = new RestAPI();

            //send query
            let response = await restAPI.queryDataAsync(url);

            //prepare to render
            let domRender = new DOMRender(response);

            //render all information without pagination
            domRender.render("#container""name""email");

DomRenderi çağırdıqda ekranda məlumatları bu formada, heç bir pagination-sız görürük.


Həmçinin parametrlər vermədən(render metoduna name email vermədən) bütün məlumatları görə bilirik.

Lakin verilən tələblər sırasında , hər birində 6 məlumat olmaqla pagination ilə dataları göstərmək olduğuna görə bu klasın üzərinə bir örtük yazaraq mövcud funksionallığı delegate etməklə yeni bir klas (PaginationRender) yaradırıq.

export default class PaginationRender {
    #itemsPerPage = 6;
    #data = [];
    #totalPages = 0;
    constructor(dataRender) {
        this._dataRender = dataRender;
        this.#data = dataRender.getData();
        this.#totalPages = Math.ceil(this.#data.length / this.#itemsPerPage);
    }

    render(selector, page, ...props) {
        let from = (page - 1) * this.#itemsPerPage;
        let to = from + this.#itemsPerPage;
        let filteredData = this.#data.filter(x => x.id >= (from + 1) && x.id <= to);
        this._dataRender.setData(filteredData);
        this._dataRender.render(selector, ...props);
        this._renderPagination();
    }

    _renderPagination() {
        let paginationContainer = $("<div class='pagination-container'></div>");
        for (let i = 1; i <= this.#totalPages; i++) {
            let linkElement = $(`<a class='btn btn-pagination' href='?page=${i}'>${i}</a>`);
            paginationContainer.append(linkElement);
        }
        $(document.body).append(paginationContainer);
    }
}

Klasın əsas işi DataRender ailəsinə məxsus hər hansı bir klası extend etməkdir. İçəridə həmin klası saxlamaqla onun lazımi funksionallığını delegate edir və kod təkrarına imkan yaradır. PagnationRender klası datalara uyğun avtomatik pagination generasiya edir və verilən arqumentə görə data yüklənməsini təmin edir.

            //prepare to render
            // let pagination = new DOMRender(response);
            let pagination = new PaginationRender(new DOMRender(response));

            pagination.render("#container", searchHelper.Value, "name""email");

PaginationRender klası DomRender kimi həm aldığı bütün key-value kombinasıyasını , ya da Rest params üzərindən verilən propertylərə uyğun məlumatları ekranda göstərə bilir.


Həmçinin pagination-nın render metodu rest params qəbul etməsə hər şeyi ekranda əks etdirir(rekursiv formada)

Səhifənin pagination ilə işləməsi aşağıdakı şəkildə göstərilib:

Index səhifəsinin sonunda isə bütün şəkillərə klik funksionallığı yazılıb. Bunu ayrı modula çıxartmağa ehtiyac yoxdur, çünki kontekst məsələsinə aiddir. Ona görə birbaşa səhifənin özündə(təbii ki ayrıca js faylına çıxartmaq olar , amma burada əsas məsələ bu deyil) göstərilir.

        import { UrlBuilder, RestAPI, PaginationRender, DOMRender, SearchParser } from './js/initialize.js'

        $(document).ready(async function () {

            const baseUrl = "https://jsonplaceholder.typicode.com";

            //prepare url
            let url = new UrlBuilder(baseUrl)
                .users()
                .build();

            console.log(url);
            //console pəncrəsində bu görünür: https://jsonplaceholder.typicode.com/users

            let searchHelper = new SearchParser();

            //prepare api
            let restAPI = new RestAPI();

            //send query
            let response = await restAPI.queryDataAsync(url);

            //prepare to render
            // let pagination = new DOMRender(response);
            let pagination = new PaginationRender(new DOMRender(response));

            pagination.render("#container", searchHelper.Value, "name""email");

            $('img').click(function () {

                let itemId = $(this).parent().attr('data-id');

                $(location).attr('href'`detail.html?user=${itemId}`);
            })
        });

İndi isə details səhifəsinə baxaq. Səhifənin kodu demək olar ki index ilə eynidir, sadəcə urlBuilding prosesi sadəcə bir məlumatın (1 user haqda məlumat) gətirilməsinə xidmət edir, həmçinin yalnız bir məlumat olacaq deyə pagination funksionallığına ehtiyac yoxdur.(detail.html səhifə kodu)

 import { UrlBuilder, RestAPI, DOMRender, SearchParser } from './js/initialize.js'

        $(document).ready(async function () {

            const baseUrl = "https://jsonplaceholder.typicode.com";

            let searchHelper = new SearchParser("user");

            //prepare url
            let url = new UrlBuilder(baseUrl)
                .users()
                .id(searchHelper.Value)
                .build();

            //prepare api
            let restAPI = new RestAPI();

            //send query
            let response = await restAPI.queryDataAsync(url);

            //prepare dom
            let domRender = new DOMRender(new Array(response));

            domRender.render("#container");
        });

 Detail səhifəsində məlumatın gprünməsi:

Tural

Tural Süleymani

Süleymani Tural Microsoft-un MCSD statuslu mütəxəssisidir, 2008-ci ildən bu yana proqramlaşdırma üzrə tədris aparır

Müəllifin bu dildə ən son postları

Bu yazıları da bəyənə bilərsiniz