Discovery.js गाइड: क्विक स्टार्ट

यह और निम्नलिखित गाइड आपको डिस्कवरी.जेएस परियोजना के आधार पर एक समाधान बनाने की प्रक्रिया के माध्यम से मार्गदर्शन करेंगे। हमारा लक्ष्य NPM निर्भरता के लिए एक इंस्पेक्टर बनाना है, अर्थात, node_modules की संरचना की जांच करने के लिए एक इंटरफ़ेस है।



नोट: Discovery.js विकास के प्रारंभिक चरण में है, इसलिए समय के साथ, कुछ सरल हो जाएगा और अधिक उपयोगी हो जाएगा। यदि आपके पास कुछ सुधार करने के बारे में विचार हैं, तो हमें लिखें

अमूर्त


नीचे आपको Discovery.js की प्रमुख अवधारणाओं का अवलोकन मिलेगा। आप GitHub पर रिपॉजिटरी में संपूर्ण मैनुअल कोड सीख सकते हैं, या आप यह कोशिश कर सकते हैं कि यह ऑनलाइन कैसे काम करता है


प्रारंभिक शर्तें


सबसे पहले, हमें विश्लेषण के लिए एक परियोजना चुनने की आवश्यकता है। यह एक हौसले से बनाई गई परियोजना या मौजूदा एक हो सकती है, मुख्य बात यह है कि इसमें node_modules (हमारे विश्लेषण की वस्तु) शामिल है।


सबसे पहले, discoveryjs कोर पैकेज और उसके कंसोल टूल्स को स्थापित करें:


 npm install @discoveryjs/discovery @discoveryjs/cli 

इसके बाद, Discovery.js सर्वर लॉन्च करें:


 > npx discovery No config is used Models are not defined (model free mode is enabled) Init common routes ... OK Server listen on http://localhost:8123 

यदि आप ब्राउज़र में http://localhost:8123 खोलते हैं, तो आप निम्नलिखित देख सकते हैं:


चर्चा के बिना डिस्कवरी


यह एक मॉडल के बिना एक मोड है, अर्थात्, एक मोड जब कुछ भी कॉन्फ़िगर नहीं किया जाता है। लेकिन अब, "लोड डेटा" बटन का उपयोग करके, आप किसी भी JSON फ़ाइल का चयन कर सकते हैं, या बस इसे पृष्ठ पर खींच सकते हैं और विश्लेषण शुरू कर सकते हैं।


हालाँकि, हमें कुछ विशिष्ट चाहिए। विशेष रूप से, हमें node_modules संरचना का एक दृश्य प्राप्त करने की आवश्यकता है। ऐसा करने के लिए, कॉन्फ़िगरेशन जोड़ें।


कॉन्फ़िगरेशन जोड़ें


जैसा कि आपने देखा होगा, सर्वर शुरू होने पर संदेश No config is used । आइए निम्नलिखित सामग्रियों के साथ .discoveryrc.js कॉन्फ़िगरेशन फ़ाइल बनाएँ:


 module.exports = { name: 'Node modules structure', data() { return { hello: 'world' }; } }; 

नोट: यदि आप वर्तमान वर्किंग डायरेक्टरी (प्रोजेक्ट के रूट में) में एक फाइल बनाते हैं, तो और कुछ नहीं चाहिए। अन्यथा, आपको --config विकल्प का उपयोग करके कॉन्फ़िगरेशन फ़ाइल में पथ पास करना होगा या package.json में पथ सेट करना होगा। package.json :


 { ... "discovery": "path/to/discovery/config.js", ... } 

सर्वर को पुनरारंभ करें ताकि कॉन्फ़िगरेशन लागू हो:


 > npx discovery Load config from .discoveryrc.js Init single model default Define default routes ... OK Cache: DISABLED Init common routes ... OK Server listen on http://localhost:8123 

जैसा कि आप देख सकते हैं, अब हमारे द्वारा बनाई गई फ़ाइल का उपयोग किया जाता है। और हमारे द्वारा वर्णित डिफ़ॉल्ट मॉडल लागू किया गया है (डिस्कवरी कई मॉडलों के मोड में काम कर सकती है, हम निम्नलिखित मैनुअल में इस सुविधा के बारे में बात करेंगे)। आइए देखें कि ब्राउज़र में क्या बदल गया है:


बुनियादी विन्यास के साथ


यहाँ क्या देखा जा सकता है:


  • name उपयोग पृष्ठ शीर्षक के रूप में किया जाता है;
  • data पद्धति को कॉल करने का परिणाम पृष्ठ की मुख्य सामग्री के रूप में प्रदर्शित होता है।

नोट: data विधि को डेटा या वादा वापस करना चाहिए, जो डेटा को हल करता है।

मूल सेटिंग्स बनाई गई हैं, आप आगे बढ़ सकते हैं।


प्रसंग


आइए देखें कस्टम रिपोर्ट पेज ( Make report क्लिक Make report ):


रिपोर्ट पेज


पहली नज़र में, यह प्रारंभ पृष्ठ से बहुत अलग नहीं है ... लेकिन यहां आप सब कुछ बदल सकते हैं! उदाहरण के लिए, हम प्रारंभ पृष्ठ की उपस्थिति को आसानी से बना सकते हैं:


प्रारंभ पृष्ठ को फिर से बनाना


ध्यान दें कि हेडर कैसे परिभाषित किया गया है: "h1:#.name" । यह #.name की सामग्री के साथ प्रथम स्तर का शीर्ष लेख है, जो कि एक जोरा अनुरोध है। # अनुरोध संदर्भ को संदर्भित करता है। इसकी सामग्री देखने के लिए, क्वेरी संपादक में बस # दर्ज करें और डिफ़ॉल्ट प्रदर्शन का उपयोग करें:


प्रासंगिकता


अब आप जानते हैं कि वर्तमान पृष्ठ की आईडी, इसके मापदंडों और अन्य उपयोगी मूल्यों को कैसे प्राप्त किया जाए।


डेटा संग्रह


अब हम वास्तविक डेटा के बजाय प्रोजेक्ट में एक स्टब का उपयोग करते हैं, लेकिन हमें वास्तविक डेटा की आवश्यकता होती है। ऐसा करने के लिए, एक मॉड्यूल बनाएं और कॉन्फ़िगरेशन में data मान को बदलें (वैसे, इन परिवर्तनों के बाद सर्वर को पुनरारंभ करना आवश्यक नहीं है:


 module.exports = { name: 'Node modules structure', data: require('./collect-node-modules-data') }; 

collect-node-modules-data.js :


 const path = require('path'); const scanFs = require('@discoveryjs/scan-fs'); module.exports = function() { const packages = []; return scanFs({ include: ['node_modules'], rules: [{ test: /\/package.json$/, extract: (file, content) => { const pkg = JSON.parse(content); if (pkg.name && pkg.version) { packages.push({ name: pkg.name, version: pkg.version, path: path.dirname(file.filename), dependencies: pkg.dependencies }); } } }] }).then(() => packages); }; 

मैंने @discoveryjs/scan-fs पैकेज का इस्तेमाल किया, जो फाइल सिस्टम स्कैनिंग को सरल बनाता है। पैकेज का उपयोग करने का एक उदाहरण इसकी रीडमी में वर्णित है, मैंने इस उदाहरण को आधार के रूप में लिया और आवश्यकतानुसार अंतिम रूप दिया। अब हमारे पास node_modules की सामग्री के बारे में कुछ जानकारी है:


डेटा एकत्र किया गया


आपको क्या चाहिए! और इस तथ्य के बावजूद कि यह सामान्य JSON है, हम पहले से ही इसका विश्लेषण कर सकते हैं और कुछ निष्कर्ष निकाल सकते हैं। उदाहरण के लिए, डेटा संरचना के पॉपअप का उपयोग करके, आप पैकेटों की संख्या का पता लगा सकते हैं और पता लगा सकते हैं कि उनमें से कितने में एक से अधिक भौतिक उदाहरण हैं (संस्करणों में अंतर या उनके कटौती के साथ समस्याएं)।


डेटा संरचना अनुसंधान


इस तथ्य के बावजूद कि हमारे पास पहले से ही कुछ डेटा है, हमें अधिक विवरण की आवश्यकता है। उदाहरण के लिए, यह जानना अच्छा होगा कि कौन सा भौतिक उदाहरण किसी विशेष मॉड्यूल की घोषित निर्भरता में से प्रत्येक को हल करता है। हालांकि, डेटा निष्कर्षण में सुधार पर काम इस गाइड के दायरे से परे है। इसलिए, हम डेटा को पुनः प्राप्त करने और पैकेज के बारे में आवश्यक विवरण प्राप्त करने के लिए इसे @discoveryjs/node-modules पैकेज (जो @discoveryjs/scan-fs पर भी आधारित है) से बदल देंगे। परिणामस्वरूप, collect-node-modules-data.js बहुत सरल किया जाता है:


 const fetchNodeModules = require('@discoveryjs/node-modules'); module.exports = function() { return fetchNodeModules(); }; 

अब node_modules बारे में जानकारी इस प्रकार है:


नई डेटा संरचना


स्क्रिप्ट तैयार करना


जैसा कि आपने देखा होगा, पैकेजों का वर्णन करने वाली कुछ वस्तुओं में deps होते हैं - निर्भरता की सूची। प्रत्येक निर्भरता में एक resolved क्षेत्र होता है जिसका मूल्य पैकेज के भौतिक उदाहरण का संदर्भ होता है। इस तरह के लिंक में से एक पैकेज का path मूल्य है, यह अद्वितीय है। पैकेज के लिंक को हल करने के लिए, आपको अतिरिक्त कोड (उदाहरण के लिए, #.data.pick(<path=resolved>) का उपयोग करने की आवश्यकता है। और निश्चित रूप से, यह बहुत सुविधाजनक होगा यदि इस तरह के लिंक पहले से ही वस्तु संदर्भों में हल किए गए थे।


दुर्भाग्य से, डेटा संग्रह के चरण में, हम लिंक को हल नहीं कर सकते हैं, क्योंकि यह परिपत्र कनेक्शन को जन्म देगा, जो JSON के रूप में इस तरह के डेटा को स्थानांतरित करने की समस्या पैदा करेगा। हालांकि, एक समाधान है: यह एक विशेष prepare स्क्रिप्ट है। इसे कॉन्फ़िगरेशन में परिभाषित किया गया है और हर बार एक नया डेटा डिस्कवरी उदाहरण को सौंपा जाता है। चलो विन्यास के साथ शुरू करते हैं:


 module.exports = { ... prepare: __dirname + '/prepare.js', // :   ,    ... }; 

prepare.js परिभाषित करें।


 discovery.setPrepare(function(data) { //  -  data /   discovery }); 

इस मॉड्यूल में, हमने डिस्कवरी उदाहरण के लिए prepare फ़ंक्शन को परिभाषित किया। डिस्कवरी उदाहरण में डेटा लगाने से पहले हर बार इस फ़ंक्शन को कहा जाता है। वस्तु संदर्भों में मूल्यों को अनुमति देने के लिए यह एक अच्छी जगह है:


 discovery.setPrepare(function(data) { const packageIndex = data.reduce((map, pkg) => map.set(pkg.path, pkg), new Map()); data.forEach(pkg => pkg.deps.forEach(dep => dep.resolved = packageIndex.get(dep.resolved) ) ); }); 

यहां हमने एक पैकेज इंडेक्स बनाया है जिसमें कुंजी पैकेज path मूल्य (अद्वितीय) है। फिर हम सभी पैकेज और उनकी निर्भरता से गुजरते हैं, और निर्भरता में हम पैकेज ऑब्जेक्ट के संदर्भ में resolved मूल्य को प्रतिस्थापित करते हैं। परिणाम:


अभिजात वर्ग का प्रतिरूप


अब निर्भरता ग्राफ प्रश्नों को बनाना बहुत आसान है। यह है कि आप एक विशिष्ट पैकेज के लिए निर्भरता (यानी निर्भरता, निर्भरता निर्भरता, आदि) का एक समूह कैसे प्राप्त कर सकते हैं:


निर्भरता क्लस्टर उदाहरण


एक अप्रत्याशित सफलता की कहानी: मैनुअल लिखने के दौरान डेटा का अध्ययन करते समय, मैंने @discoveryjs/cli .[deps.[not resolved]] @discoveryjs/cli (क्वेरी का उपयोग करके एक समस्या की @discoveryjs/cli .[deps.[not resolved]] ), जिसकी सहकर्मी निर्भरता में एक टाइपो था। समस्या तुरंत ठीक हो गई थी । मामला इस बात का एक अच्छा उदाहरण है कि ऐसे उपकरण कैसे मदद करते हैं।

शायद प्रारंभ पृष्ठ पर कई नंबरों और पैकेजों के साथ दिखाने का समय आ गया है।


प्रारंभ पृष्ठ को अनुकूलित करें


सबसे पहले, हमें एक पेज मॉड्यूल बनाने की जरूरत है, उदाहरण के लिए, pages/default.js । हम default उपयोग करते हैं, क्योंकि यह प्रारंभ पृष्ठ के लिए पहचानकर्ता है, जिसे हम (Discovery.js में, आप बहुत अधिक ओवरराइड कर सकते हैं) ओवरराइड कर सकते हैं। आइए कुछ सरल से शुरू करें, उदाहरण के लिए:


 discovery.page.define('default', [ 'h1:#.name', 'text:"Hello world!"' ]); 

अब कॉन्फ़िगरेशन में आपको पृष्ठ मॉड्यूल कनेक्ट करने की आवश्यकता है:


 module.exports = { name: 'Node modules structure', data: require('./collect-node-modules-data'), view: { assets: [ 'pages/default.js' //     ] } }; 

ब्राउज़र में जांचें:


ओवरराइड ओपन पेज


यह काम करता है!


चलिए अब कुछ काउंटर मिलते हैं। ऐसा करने के लिए, pages/default.js परिवर्तन करें:


 discovery.page.define('default', [ 'h1:#.name', { view: 'inline-list', item: 'indicator', data: `[ { label: 'Package entries', value: size() }, { label: 'Unique packages', value: name.size() }, { label: 'Dup packages', value: group(<name>).[value.size() > 1].size() } ]` } ]); 

यहां हम संकेतकों की एक इनलाइन सूची को परिभाषित करते हैं। data मान एक जोरा क्वेरी है जो रिकॉर्ड की एक सरणी बनाता है। संकुल की सूची (डेटा रूट) का उपयोग प्रश्नों के आधार के रूप में किया जाता है, इसलिए हमें सूची की लंबाई ( size() ), अद्वितीय पैकेज नामों की संख्या ( name.size() ) और डुप्लिकेट वाले पैकेज नामों की संख्या ( group(<name>).[value.size() > 1].size() name.size() ) name.size() group(<name>).[value.size() > 1].size() )।


प्रारंभ पृष्ठ पर कनेक्टिविटी जोड़ें


बुरा नहीं है। फिर भी, संख्याओं के अलावा, संबंधित नमूनों से लिंक होना बेहतर होगा:


 discovery.page.define('default', [ 'h1:#.name', { view: 'inline-list', data: [ { label: 'Package entries', value: '' }, { label: 'Unique packages', value: 'name' }, { label: 'Dup packages', value: 'group(<name>).[value.size() > 1]' } ], item: `indicator:{ label, value: value.query(#.data, #).size(), href: pageLink('report', { query: value, title: label }) }` } ]); 

सबसे पहले, हमने data के मूल्य को बदल दिया, अब यह कुछ वस्तुओं के साथ एक नियमित सरणी है। साथ ही, size() पद्धति को मूल्य अनुरोधों से हटा दिया गया है।


इसके अलावा, indicator दृश्य में एक उप-वर्ग जोड़ा गया है। इस प्रकार के प्रश्न प्रत्येक तत्व के लिए एक नई वस्तु बनाते हैं जिसमें value और href की गणना की जाती है। value , query() विधि का उपयोग करके एक क्वेरी निष्पादित की जाती है, जिसमें डेटा को संदर्भ से स्थानांतरित किया जाता है, और फिर क्वेरी परिणाम पर size() पद्धति लागू की जाती है। pageLink() लिए, pageLink() विधि का उपयोग किया जाता है, जो एक विशिष्ट अनुरोध और हेडर के साथ रिपोर्ट पेज का लिंक उत्पन्न करता है। इन सभी परिवर्तनों के बाद, संकेतक क्लिक करने योग्य हो गए (ध्यान दें कि उनके मूल्य नीले हो गए हैं) और अधिक कार्यात्मक।


योग्य कनेक्टिविटी पर क्लिक करें


प्रारंभ पृष्ठ को अधिक उपयोगी बनाने के लिए, डुप्लिकेट वाले पैकेजों के साथ एक तालिका जोड़ें।


 discovery.page.define('default', [ // ...      'h2:"Packages with more than one physical instance"', { view: 'table', data: ` group(<name>) .[value.size() > 1] .sort(<value.size()>) .reverse() `, cols: [ { header: 'Name', content: 'text:key' }, { header: 'Version & Location', content: { view: 'list', data: 'value.sort(<version>)', item: [ 'badge:version', 'text:path' ] } } ] } ]); 

तालिका उसी डेटा का उपयोग करती है जैसे कि Dup packages इंडिकेटर। संकुल की सूची को समूह आकार द्वारा उल्टे क्रम में क्रमबद्ध किया गया था। शेष सेटअप कॉलम से संबंधित है (वैसे, आमतौर पर उन्हें ट्यून करने की आवश्यकता नहीं होती है)। Version & Location कॉलम के लिए, हमने एक नेस्टेड सूची (संस्करण द्वारा क्रमबद्ध) को परिभाषित किया है, जिसमें प्रत्येक तत्व संस्करण संख्या और उदाहरण के लिए पथ की एक जोड़ी है।


उन पैकेजों के साथ जोड़ा गया तालिका जिसमें एक से अधिक भौतिक उदाहरण हैं


पैकेज पृष्ठ


अब हमारे पास संकुल का केवल एक सामान्य अवलोकन है। लेकिन किसी विशेष पैकेज के बारे में विवरण के साथ एक पृष्ठ होना उपयोगी होगा। ऐसा करने के लिए, एक नया मॉड्यूल pages/package.js बनाएँ pages/package.js और एक नया पृष्ठ परिभाषित करें:


 discovery.page.define('package', { view: 'context', data: `{ name: #.id, instances: .[name = #.id] }`, content: [ 'h1:name', 'table:instances' ] }); 

इस मॉड्यूल में, हमने पहचानकर्ता package साथ पृष्ठ को परिभाषित किया। प्रारंभिक घटक के रूप में context घटक का उपयोग किया गया था। यह एक गैर-दृश्य घटक है जो आपको नेस्टेड मैपिंग के लिए डेटा को परिभाषित करने में मदद करता है। ध्यान दें कि हमने पैकेज का नाम पाने के लिए #.id का उपयोग किया था, जिसे इस http://localhost:8123/#package:{id} जैसे URL से पुनर्प्राप्त किया जाता है।


कॉन्फ़िगरेशन में नया मॉड्यूल शामिल करना न भूलें:


 module.exports = { ... view: { assets: [ 'pages/default.js', 'pages/package.js' //   ] } }; 

ब्राउज़र में परिणाम:


पैकेज पृष्ठ का उदाहरण


बहुत प्रभावशाली नहीं है, लेकिन अभी के लिए। हम बाद के मैनुअल में अधिक जटिल मैपिंग बनाएंगे।


साइड पैनल


चूंकि हमारे पास पहले से ही एक पैकेज पृष्ठ है, इसलिए सभी पैकेजों की एक सूची रखना अच्छा होगा। ऐसा करने के लिए, आप एक विशेष दृश्य - sidebar को परिभाषित कर सकते हैं, जिसे प्रदर्शित किया जाता है यदि इसे परिभाषित किया गया है (डिफ़ॉल्ट रूप से परिभाषित नहीं किया गया है)। एक नया मॉड्यूल views/sidebar.js बनाएं। views/sidebar.js :


 discovery.view.define('sidebar', { view: 'list', data: 'name.sort()', item: 'link:{ text: $, href: pageLink("package") }' }); 

अब हमारे पास सभी पैकेजों की एक सूची है:


जोड़ा साइडबार


यह अच्छा लग रहा है। लेकिन एक फिल्टर के साथ यह और भी बेहतर होगा। हम sidebar की परिभाषा का विस्तार करते हैं:


 discovery.view.define('sidebar', { view: 'content-filter', content: { view: 'list', data: 'name.[no #.filter or $~=#.filter].sort()', item: { view: 'link', data: '{ text: $, href: pageLink("package"), match: #.filter }', content: 'text-match' } } }); 

यहां हमने सूची को एक content-filter घटक में लपेटा है जो इनपुट फ़ील्ड को नियमित अभिव्यक्तियों में इनपुट मान को परिवर्तित करता है (या यदि फ़ील्ड खाली है) तो इसे संदर्भ में filter मान के रूप में सहेजता है ( name विकल्प के साथ name बदला जा सकता है)। इसके अलावा, सूची के लिए डेटा को फ़िल्टर करने के लिए, हमने #.filter उपयोग किया। अंत में, हमने text-match साथ मिलान वाले हिस्सों को उजागर करने के लिए लिंक मैपिंग लागू किया। परिणाम:


फ़िल्टर सूची


यदि आपको डिफ़ॉल्ट डिज़ाइन पसंद नहीं है, तो आप अपनी इच्छानुसार शैलियों को अनुकूलित कर सकते हैं। मान लीजिए कि आप साइडबार की चौड़ाई बदलना चाहते हैं, इसके लिए आपको एक स्टाइल फाइल बनाने की आवश्यकता है (जैसे, views/sidebar.css ):


 .discovery-sidebar { width: 300px; } 

और कॉन्फ़िगरेशन में इस फ़ाइल के लिए एक लिंक जोड़ें, साथ ही जावास्क्रिप्ट मॉड्यूल:


 module.exports = { ... view: { assets: [ ... 'views/sidebar.css', //  assets    *.css  'views/sidebar.js' ] } }; 

स्वत: लिंक


इस गाइड का अंतिम अध्याय लिंक्स के लिए समर्पित है। इससे पहले, pageLink() विधि का उपयोग करते हुए, हमने पैकेज पेज के लिए एक लिंक बनाया। लेकिन लिंक के अलावा, आपको लिंक टेक्स्ट भी सेट करना होगा। लेकिन हम इसे कैसे आसान बनाएंगे?


लिंक के काम को सरल बनाने के लिए, हमें लिंक बनाने के लिए एक नियम को परिभाषित करने की आवश्यकता है। यह prepare स्क्रिप्ट में सबसे अच्छा किया जाता है:


 discovery.setPrepare(function(data) { ... const packageIndex = data.reduce( (map, item) => map .set(item, item) // key is item itself .set(item.name, item), // and `name` value new Map() ); discovery.addEntityResolver(value => { value = packageIndex.get(value) || packageIndex.get(value.name); if (value) { return { type: 'package', id: value.name, name: value.name }; } }); }); 

हमने पैकेजों का एक नया नक्शा (इंडेक्स) जोड़ा और इसे इकाई रिज़ॉल्वर के लिए उपयोग किया। इकाई रिज़ॉल्वर, यदि संभव हो, तो इसके लिए दिए गए मान को एक इकाई विवरणक में परिवर्तित करने की कोशिश करता है। वर्णनकर्ता में शामिल हैं:


  • type - इकाई प्रकार
  • id - एक id रूप में लिंक में उपयोग की जाने वाली इकाई उदाहरण के लिए एक अद्वितीय संदर्भ
  • name - लिंक पाठ के रूप में उपयोग किया जाता है

अंत में, आपको इस प्रकार को एक विशिष्ट पृष्ठ पर निर्दिष्ट करने की आवश्यकता है (लिंक को कहीं और ले जाना चाहिए, सही?)।


 discovery.page.define('package', { ... }, { resolveLink: 'package' //    `package`    }); 

इन परिवर्तनों का पहला परिणाम यह है कि struct दृश्य में कुछ मान अब पैकेज पृष्ठ के लिंक के साथ चिह्नित हैं:


संरचना संरचना औटो लि


और अब आप auto-link घटक को किसी ऑब्जेक्ट या पैकेज के नाम पर भी लागू कर सकते हैं:


औब-लिग का उपयोग करना


और, एक उदाहरण के रूप में, आप साइडबार को फिर से काम कर सकते हैं:


  //   item: { view: 'link', data: '{ text: $, href: pageLink("package"), match: #.filter }', content: 'text-match' }, //   `auto-link` item: { view: 'auto-link', content: 'text-match:{ text, match: #.filter }' } 

निष्कर्ष


अब आपको Discovery.js की प्रमुख अवधारणाओं की एक बुनियादी समझ है। निम्नलिखित गाइड में हम शामिल विषयों पर एक करीब से नज़र डालेंगे।


आप गाइड के संपूर्ण स्रोत कोड को GitHub पर रिपॉजिटरी में देख सकते हैं या कोशिश कर सकते हैं कि यह ऑनलाइन कैसे काम करता है


ताजा खबरों से बचने के लिए ट्विटर पर @js_discovery को फॉलो करें !

Source: https://habr.com/ru/post/hi469115/


All Articles