socialgekon.com
  • Главни
  • Животни Циклус Производа
  • Мобиле
  • Остало
  • Уи Десигн
Бацк-Енд

Напишите код за преписивање кода: јсцодесхифт

Кодови са јсцодехифт-ом

Колико пута сте користили функцију пронађи и замени у директоријуму да бисте изменили ЈаваСцрипт датотеке? Ако сте добри, обожавали сте се и користили регуларне изразе са хватањем група, јер вреди се потрудити ако је ваша база кода велика. Регек има ограничења. За нетривијалне промене потребан вам је програмер који разуме код у контексту и такође је спреман да предузме дуг, мучан и склон грешкама процес.

Овде долазе „кодемови“.

Кодемоди су скрипте које се користе за преписивање других скрипти. Схватите их као функцију за проналажење и замену која може читати и писати код. Можете их користити за ажурирање изворног кода како би одговарао конвенцијама кодирања тима, уношење широких промена када се АПИ модификује или чак аутоматско поправљање постојећег кода када ваш јавни пакет изврши промену.



Комплет алата јсцодесхифт је одличан за рад са кодним кодовима.

Замислите кодемоде као скриптирану функцију за проналажење и замену која може читати и писати код. Твеет

У овом чланку ћемо истражити скуп алата за кодне програме под називом „јсцодесхифт“, док ћемо креирати три кодемова све веће сложености. На крају ћете бити широко изложени важним аспектима јсцодесхифт и биће спремни да започнете писање сопствених кодова. Проћи ћемо кроз три вежбе које покривају неке основне, али сјајне примене кодемода, а изворни код за ове вежбе можете погледати на мом гитхуб пројекат .

Шта је јсцодесхифт?

Комплет алата јсцодесхифт вам омогућава да пумпу гомиле изворних датотека преобразите и замените оним што изађе на другом крају. Унутар трансформације рашчлањујете извор у апстрактно стабло синтаксе (АСТ), провлачите се да бисте унели промене, а затим регенеришете извор из измењеног АСТ.

Интерфејс који пружа јсцодесхифт је омотач око recast и ast-types пакети. recast обрађује конверзију из извора у АСТ и назад док ast-types рукује интеракцијом на ниском нивоу са АСТ чворовима.

Подесити

Да бисте започели, инсталирајте јсцодесхифт глобално од нпм.

npm i -g jscodeshift

Постоје опције тркача које можете користити и самопоуздано подешавање теста које покретање пакета тестова путем Јест-а (отвореног кода ЈаваСцрипт оквира за тестирање) чини заиста једноставним, али за сада ћемо то заобићи у корист једноставности:

jscodeshift -t some-transform.js input-file.js -d -p

Ово ће се покретати input-file.js кроз трансформацију some-transform.js и одштампајте резултате без промене датотеке.

Пре него што ускочите, важно је разумети три главна типа објекта с којима се бави јсцодесхифт АПИ: чворови, путање чворова и колекције.

Чворови

Чворови су основни градивни елементи АСТ-а, који се често називају „АСТ чворови“. То су оно што видите приликом истраживања кода помоћу АСТ Екплорера. То су једноставни објекти и не пружају никакве методе.

Стазе чворова

Стазе чворова су омотачи око АСТ чвора обезбеђених од ast-types као начин преласка апстрактног стабла синтаксе (АСТ, сећате се?). Изолирано, чворови немају никакве информације о свом родитељу или опсегу, па се стазе чворова о томе брину. Омотаном чвору можете приступити преко node и постоји неколико доступних метода за промену основног чвора. стазе чворова често се називају само „стазама“.

Колекције

Колекције су групе нулте или више чворовских путања које АПИ јсодесхифт враћа када постављате упит АСТ-у. Имају свакакве корисне методе, од којих ћемо неке истражити.

Колекције садрже путање чворова, стазе чворова садрже чворове, а чворови су оно од чега је АСТ направљен. Имајте то на уму и биће лако разумјети АПИ за јсцодесхифт упит.

Може бити тешко пратити разлике између ових објеката и њихових одговарајућих АПИ могућности, тако да постоји сјајна алатка која се зове јсцодесхифт-хелпер који евидентира тип објекта и пружа друге кључне информације.

Познавање разлике између чворова, путања чворова и колекција је важно.

Познавање разлике између чворова, путања чворова и колекција је важно.

1. вежба: Уклоните позиве на конзолу

Да бисмо се смочили, почнимо са уклањањем позива свим методама конзоле у ​​нашој бази кода. Иако то можете да урадите помоћу проналажења и замене и мало регуларног израза, почиње да постаје замршено са вишередним исказима, литерарним предлошцима и сложенијим позивима, тако да је идеалан пример за почетак.

Прво створите две датотеке, remove-consoles.js и remove-consoles.input.js:

//remove-consoles.js export default (fileInfo, api) => { }; //remove-consoles.input.js export const sum = (a, b) => { console.log('calling sum with', arguments); return a + b; }; export const multiply = (a, b) => { console.warn('calling multiply with', arguments); return a * b; }; export const divide = (a, b) => { console.error(`calling divide with ${ arguments }`); return a / b; }; export const average = (a, b) => { console.log('calling average with ' + arguments); return divide(sum(a, b), 2); };

Ево наредбе коју ћемо користити у терминалу да бисмо је прогурали кроз јсцодесхифт:

jscodeshift -t remove-consoles.js remove-consoles.input.js -d -p

Ако је све правилно постављено, када покренете, требало би да видите нешто попут овога.

Processing 1 files... Spawning 1 workers... Running in dry mode, no files will be written! Sending 1 files to free worker... All done. Results: 0 errors 0 unmodified 1 skipped 0 ok Time elapsed: 0.514seconds

ОК, то је било помало антиклимактично, јер наша трансформација заправо још ништа не чини, али бар знамо да све то функционише. Ако се уопште не покреће, уверите се да сте јсцодесхифт инсталирали глобално. Ако је наредба за покретање трансформације нетачна, видећете поруку „ЕРРОР Трансформ филе… не постоји“ или „ТипеЕррор: путања мора бити низ или ме успремник“ ако улазну датотеку није могуће пронаћи. Ако сте нешто натукнули, требало би да буде лако уочити са врло описним грешкама у трансформацији.

Повезан: АпееСцапе-ов брзи и практични ЈаваСцрипт варалица: ЕС6 и даље

Наш крајњи циљ је, после успешне трансформације, да видимо овај извор:

export const sum = (a, b) => { return a + b; }; export const multiply = (a, b) => { return a * b; }; export const divide = (a, b) => { return a / b; }; export const average = (a, b) => { return divide(sum(a, b), 2); };

Да бисмо тамо стигли, морамо да претворимо извор у АСТ, пронађемо конзоле, уклонимо их, а затим измењени АСТ вратимо у извор. Први и последњи корак су једноставни, то је само:

remove-consoles.js export default (fileInfo, api) => { const j = api.jscodeshift; const root = j(fileInfo.source); return root.toSource(); };

Али како да пронађемо конзоле и уклонимо их? Ако не поседујете неко изузетно знање о Мозилла Парсер АПИ-ју, вероватно ће вам требати алат који ће вам помоћи да разумете како АСТ изгледа. За то можете користити АСТ Екплорер . Налепите садржај remove-consoles.input.js у њега и видећете АСТ. Много је података чак и у најједноставнијем коду, па помаже у сакривању података о локацији и метода. Видљивост својстава у АСТ Екплореру можете да пребацујете помоћу поља за потврду изнад стабла.

Можемо видети да се позиви на методе конзоле називају CallExpressions, па како да их пронађемо у нашој трансформацији? Користимо упите јсцодесхифт, сјећајући се наше раније расправе о разликама између Збирки, стаза чворова и самих чворова:

//remove-consoles.js export default (fileInfo, api) => { const j = api.jscodeshift; const root = j(fileInfo.source); return root.toSource(); };

Ред const root = j(fileInfo.source); враћа колекцију једне путање до чвора која обавија коријенски АСТ чвор. Можемо да користимо колекцију find метода за претрагу потомствених чворова одређеног типа, на пример:

const callExpressions = root.find(j.CallExpression);

Ово враћа другу колекцију путева чворова који садрже само чворове који су ЦаллЕкпрессионс. На почетку руменило изгледа као да желимо, али је прешироко. На крају бисмо могли да покренемо стотине или хиљаде датотека кроз наше трансформације, тако да морамо бити прецизни да бисмо имали било каквог поверења да ће радити како је предвиђено. Наивни find горе не би само пронашао конзолу ЦаллЕкпрессионс, већ би пронашао сваки ЦаллЕкпрессион у извору, укључујући

require('foo') bar() setTimeout(() => {}, 0)

Да бисмо постигли већу специфичност, пружамо други аргумент за .find: Објекат додатних параметара, сваки чвор мора бити укључен у резултате. Можемо погледати АСТ Екплорер да видимо да ли наша конзола. * Позиви имају облик:

{ 'type': 'CallExpression', 'callee': { 'type': 'MemberExpression', 'object': { 'type': 'Identifier', 'name': 'console' } } }

Са тим знањем, знамо да свој упит дорадимо помоћу спецификатора који ће вратити само ону врсту ЦаллЕкпрессионс који нас занимају:

const callExpressions = root.find(j.CallExpression, { callee: { type: 'MemberExpression', object: { type: 'Identifier', name: 'console' }, }, });

Сад кад смо добили тачну збирку локација за позиве, уклонимо их из АСТ-а. Погодно је да тип објекта колекције има remove метода која ће учинити управо то. Наши remove-consoles.js датотека ће сада изгледати овако:

//remove-consoles.js export default (fileInfo, api) => { const j = api.jscodeshift; const root = j(fileInfo.source) const callExpressions = root.find(j.CallExpression, { callee: { type: 'MemberExpression', object: { type: 'Identifier', name: 'console' }, }, } ); callExpressions.remove(); return root.toSource(); };

Сада, ако покренемо нашу трансформацију из командне линије користећи jscodeshift -t remove-consoles.js remove-consoles.input.js -d -p, требало би да видимо:

Processing 1 files... Spawning 1 workers... Running in dry mode, no files will be written! Sending 1 files to free worker... export const sum = (a, b) => { return a + b; }; export const multiply = (a, b) => { return a * b; }; export const divide = (a, b) => { return a / b; }; export const average = (a, b) => { return divide(sum(a, b), 2); }; All done. Results: 0 errors 0 unmodified 0 skipped 1 ok Time elapsed: 0.604seconds

Изгледа добро. Сада када наша трансформација мења основни АСТ, користећи .toSource() генерише различит низ од оригинала. Опција -п из наше команде приказује резултат, а на дну је приказан распоред распореда за сваку обрађену датотеку. Уклањање опције -д из наше команде, заменило би садржај ремове-цонсолес.инпут.јс излазом из трансформације.

Наша прва вежба је завршена ... скоро. Код је бизарног изгледа и вероватно веома увредљив за све функционалне пуристе, тако да је, како би побољшао проток кода за трансформисање, јсцодесхифт већину ствари учинио доступном. Ово нам омогућава да преправимо трансформацију тако:

// remove-consoles.js export default (fileInfo, api) => { const j = api.jscodeshift; return j(fileInfo.source) .find(j.CallExpression, { callee: { type: 'MemberExpression', object: { type: 'Identifier', name: 'console' }, }, } ) .remove() .toSource(); };

Много боље. Да поновимо вежбу 1, умотали смо извор, поставили упит за колекцију путања чворова, променили АСТ и затим регенерирали тај извор. Прилично смо једноставним примером смочили ноге и дотакли се најважнијих аспеката. Урадимо нешто занимљивије.

Вежба 2: Замена позива увезених метода

За овај сценарио имамо модул „геометрија“ са методом названом „цирцлеАреа“ коју смо застарели у корист „гетЦирцлеАреа“. Те бисмо лако могли пронаћи и заменити са /geometry.circleArea/g, али шта ако је корисник увезао модул и доделио му друго име? На пример:

import g from 'geometry'; const area = g.circleArea(radius);

Како бисмо знали да заменимо g.circleArea уместо geometry.circleArea? Свакако не можемо претпоставити да су сви circleArea позиви су они које тражимо, потребан нам је контекст. Овде кодемоди почињу да показују своју вредност. Почнимо са прављењем две датотеке, deprecated.js и deprecated.input.js.

//deprecated.js export default (fileInfo, api) => { const j = api.jscodeshift; const root = j(fileInfo.source); return root.toSource(); }; deprecated.input.js import g from 'geometry'; import otherModule from 'otherModule'; const radius = 20; const area = g.circleArea(radius); console.log(area === Math.pow(g.getPi(), 2) * radius); console.log(area === otherModule.circleArea(radius));

Сада покрените ову наредбу да покренете кодни мод.

jscodeshift -t ./deprecated.js ./deprecated.input.js -d -p

Требали бисте видјети излаз који указује на извршену трансформацију, али још ништа није промијенио.

Processing 1 files... Spawning 1 workers... Running in dry mode, no files will be written! Sending 1 files to free worker... All done. Results: 0 errors 1 unmodified 0 skipped 0 ok Time elapsed: 0.892seconds

Морамо знати који су наши geometry модул је увезен као. Погледајмо АСТ Екплорер и схватимо шта тражимо. Наш увоз има овај облик.

{ 'type': 'ImportDeclaration', 'specifiers': [ { 'type': 'ImportDefaultSpecifier', 'local': { 'type': 'Identifier', 'name': 'g' } } ], 'source': { 'type': 'Literal', 'value': 'geometry' } }

Можемо одредити тип објекта како бисмо пронашли колекцију чворова попут ове:

const importDeclaration = root.find(j.ImportDeclaration, { source: { type: 'Literal', value: 'geometry', }, });

Ово нам даје ИмпортДецларатион која се користи за увоз „геометрије“. Одатле копајте доле да бисте пронашли локално име које се користи за задржавање увезеног модула. Будући да то радимо први пут, истакнимо важну и збуњујућу тачку приликом првог покретања.

Напомена: Важно је знати да root.find() враћа колекцију чворова-стаза. Одатле, .get(n) метода враћа путању чвора у индексу n у тој колекцији и да бисмо добили стварни чвор, користимо .node. Чвор је у основи оно што видимо у АСТ Екплореру. Запамтите, путања чвора је углавном информација о опсегу и односима чвора, а не чвор сам.

// find the Identifiers const identifierCollection = importDeclaration.find(j.Identifier); // get the first NodePath from the Collection const nodePath = identifierCollection.get(0); // get the Node in the NodePath and grab its 'name' const localName = nodePath.node.name;

То нам омогућава да динамички схватимо шта је наше geometry модул је увезен као. Даље проналазимо места на којима се користи и мењамо их. Гледајући АСТ Екплорер, можемо видети да треба да пронађемо МемберЕкпрессионс који изгледају овако:

{ 'type': 'MemberExpression', 'object': { 'name': 'geometry' }, 'property': { 'name': 'circleArea' } }

Међутим, имајте на уму да је наш модул можда увежен под другим именом, па то морамо узети у обзир чинећи да наш упит изгледа овако:

j.MemberExpression, { object: { name: localName, }, property: { name: 'circleArea', }, })

Сада када имамо упит, можемо да добијемо колекцију свих локација за позиве према нашој старој методи, а затим користимо колекцију | | + + | начин да их замените. Тхе replaceWith() метода се понавља кроз колекцију, прослеђујући сваки пут до чвора функцији повратног позива. АСТ чвор се затим замењује било којим чвором који вратите из повратног позива.

Цодемодови вам омогућавају скрипте

Опет, разумевање разлике између колекција, путања чворова и чворова је неопходно да би ово имало смисла.

Када завршимо са заменом, генеришемо извор као и обично. Ево наше готове трансформације:

replaceWith()

Када покренемо извор кроз трансформацију, видимо да је позив застарелој методи у //deprecated.js export default (fileInfo, api) => { const j = api.jscodeshift; const root = j(fileInfo.source); // find declaration for 'geometry' import const importDeclaration = root.find(j.ImportDeclaration, { source: { type: 'Literal', value: 'geometry', }, }); // get the local name for the imported module const localName = // find the Identifiers importDeclaration.find(j.Identifier) // get the first NodePath from the Collection .get(0) // get the Node in the NodePath and grab its 'name' .node.name; return root.find(j.MemberExpression, { object: { name: localName, }, property: { name: 'circleArea', }, }) .replaceWith(nodePath => { // get the underlying Node const { node } = nodePath; // change to our new prop node.property.name = 'getCircleArea'; // replaceWith should return a Node, not a NodePath return node; }) .toSource(); }; модул је промењен, али је остатак остао непромењен, овако:

geometry

Вежба 3: Промена потписа методе

У претходним вежбама смо обрађивали испитивање колекција за одређене типове чворова, уклањање чворова и мењање чворова, али шта је са стварањем потпуно нових чворова? То је оно чиме ћемо се позабавити у овој вежби.

У овом сценарију имамо потпис методе који је измакнуо контроли појединачним аргументима како софтвер расте, па је одлучено да је боље прихватити објекат који садржи те аргументе.

Уместо import g from 'geometry'; import otherModule from 'otherModule'; const radius = 20; const area = g.getCircleArea(radius); console.log(area === Math.pow(g.getPi(), 2) * radius); console.log(area === otherModule.circleArea(radius));

волели бисмо да видимо

car.factory('white', 'Kia', 'Sorento', 2010, 50000, null, true);

Почнимо са израдом трансформације и улазне датотеке за тестирање:

const suv = car.factory({ color: 'white', make: 'Kia', model: 'Sorento', year: 2010, miles: 50000, bedliner: null, alarm: true, }); //signature-change.js export default (fileInfo, api) => { const j = api.jscodeshift; const root = j(fileInfo.source); return root.toSource(); };

Наша наредба за покретање трансформације биће //signature-change.input.js import car from 'car'; const suv = car.factory('white', 'Kia', 'Sorento', 2010, 50000, null, true); const truck = car.factory('silver', 'Toyota', 'Tacoma', 2006, 100000, true, true); а кораци који су нам потребни за извођење ове трансформације су:

  • Пронађите локално име за увезени модул
  • Пронађите све локације позива методом .фацтори
  • Прочитајте све аргументе који се предају
  • Замените тај позив једним аргументом који садржи објекат са оригиналним вредностима

Користећи АСТ Екплорер и поступак који смо користили у претходним вежбама, прва два корака су лака:

jscodeshift -t signature-change.js signature-change.input.js -d -p

За читање свих аргумената који се тренутно предају користимо //signature-change.js export default (fileInfo, api) => { const j = api.jscodeshift; const root = j(fileInfo.source); // find declaration for 'car' import const importDeclaration = root.find(j.ImportDeclaration, { source: { type: 'Literal', value: 'car', }, }); // get the local name for the imported module const localName = importDeclaration.find(j.Identifier) .get(0) .node.name; // find where `.factory` is being called return root.find(j.CallExpression, { callee: { type: 'MemberExpression', object: { name: localName, }, property: { name: 'factory', }, } }) .toSource(); }; методу у нашој колекцији ЦаллЕкпрессионс за замену сваког од чворова. Нови чворови ће заменити ноде.аргументс новим појединачним аргументом, објектом.

Једноставно замените аргументе метода са јсцодесхифт!

Промените потписе метода са 'реплацевитх ()' и замените читаве чворове.

Покушајмо са једноставним објектом да бисмо били сигурни да знамо како то функционише пре него што употребимо одговарајуће вредности:

replaceWith()

Када покренемо ово ( .replaceWith(nodePath => { const { node } = nodePath; node.arguments = [{ foo: 'bar' }]; return node; }) ), трансформација ће експлодирати са:

jscodeshift -t signature-change.js signature-change.input.js -d -p

Испоставило се да не можемо само заглавити обичне предмете у наше АСТ чворове. Уместо тога, морамо користити градитеље за стварање одговарајућих чворова.

Повезан: Ангажујте најбољих 3% слободних програмера Јавасцрипта.

Ноде Буилдерс

Градитељи нам омогућавају да правилно направимо нове чворове; пружа их ERR signature-change.input.js Transformation error Error: {foo: bar} does not match type Printable и испливао кроз јсцодехифт. Они ригидно проверавају да ли су различити типови чворова исправно створени, што може бити фрустрирајуће када хакујете ролну, али на крају, ово је добра ствар. Да бисте разумели како се користе градитељи, имајте на уму две ствари:

Сви доступни типови АСТ чворова дефинисани су у директоријуму ast-types у аст-типес гитхуб пројекат , углавном у цоре.јс-у Постоје градитељи за све типове АСТ чворова, али они користе обложен девама верзија типа чвора, не паскал-случај . (Ово није изричито наведено, али можете видети да је то случај у извор аст-типова

Ако користимо АСТ Екплорер са примером онога што желимо да буде резултат, то можемо прилично лако саставити. У нашем случају желимо да нови појединачни аргумент буде ОбјецтЕкпрессион са гомилом својстава. Гледајући горе поменуте дефиниције типова, можемо видети шта то подразумева:

def

Дакле, код за изградњу АСТ чвора за {фоо: ‘бар’} би изгледао овако:

def('ObjectExpression') .bases('Expression') .build('properties') .field('properties', [def('Property')]); def('Property') .bases('Node') .build('kind', 'key', 'value') .field('kind', or('init', 'get', 'set')) .field('key', or(def('Literal'), def('Identifier'))) .field('value', def('Expression'));

Узмите тај код и прикључите га у нашу трансформацију тако:

j.objectExpression([ j.property( 'init', j.identifier('foo'), j.literal('bar') ) ]);

Покретањем овог добијамо резултат:

.replaceWith(nodePath => { const { node } = nodePath; const object = j.objectExpression([ j.property( 'init', j.identifier('foo'), j.literal('bar') ) ]); node.arguments = [object]; return node; })

Сада када знамо како да направимо одговарајући АСТ чвор, лако је прелазити кроз старе аргументе и уместо тога генерисати нови објекат који ћемо користити. Ево шта је наш import car from 'car'; const suv = car.factory({ foo: 'bar' }); const truck = car.factory({ foo: 'bar' }); датотека изгледа сада:

signature-change.js

Покрените трансформацију (//signature-change.js export default (fileInfo, api) => { const j = api.jscodeshift; const root = j(fileInfo.source); // find declaration for 'car' import const importDeclaration = root.find(j.ImportDeclaration, { source: { type: 'Literal', value: 'car', }, }); // get the local name for the imported module const localName = importDeclaration.find(j.Identifier) .get(0) .node.name; // current order of arguments const argKeys = [ 'color', 'make', 'model', 'year', 'miles', 'bedliner', 'alarm', ]; // find where `.factory` is being called return root.find(j.CallExpression, { callee: { type: 'MemberExpression', object: { name: localName, }, property: { name: 'factory', }, } }) .replaceWith(nodePath => { const { node } = nodePath; // use a builder to create the ObjectExpression const argumentsAsObject = j.objectExpression( // map the arguments to an Array of Property Nodes node.arguments.map((arg, i) => j.property( 'init', j.identifier(argKeys[i]), j.literal(arg.value) ) ) ); // replace the arguments with our new ObjectExpression node.arguments = [argumentsAsObject]; return node; }) // specify print options for recast .toSource({ quote: 'single', trailingComma: true }); }; ) и видећемо да су потписи ажурирани како се очекивало:

jscodeshift -t signature-change.js signature-change.input.js -d -p

Кодови са јсцодесхифт Рецап-ом

Требало је мало времена и труда да се дође до ове тачке, али благодати су огромне када се суочите са масовним преуређивањем. Дистрибуција група датотека у различите процесе и њихово паралелно покретање је нешто у чему се јсодесхифт истиче, омогућавајући вам да покренете сложене трансформације у огромној бази кода у секунди. Како будете постајали вештији у кодним кодовима, започињете са пренаменом постојећих скрипти (као што је одлагалиште реакције-кодемод гитхуб-а или писање сопственог за све врсте задатака, а то ће учинити вас, ваш тим и кориснике пакета ефикаснијим.

Како направити фото колаж на иПхоне-у: апликације и идеје

Уређивање

Како направити фото колаж на иПхоне-у: апликације и идеје
Супер једноставан водич за иконографију

Супер једноставан водич за иконографију

Уи Десигн

Популар Постс
Зашто размотрити редизајн веб странице - савети и препоруке
Зашто размотрити редизајн веб странице - савети и препоруке
Шта вреди стартуп? Смернице и најбоље праксе
Шта вреди стартуп? Смернице и најбоље праксе
Развој паметних сатова: да ли паметни сатови вреде проблема?
Развој паметних сатова: да ли паметни сатови вреде проблема?
Да ли су сви трендови вредни тога? 5 најчешћих УКС грешака које дизајнери праве
Да ли су сви трендови вредни тога? 5 најчешћих УКС грешака које дизајнери праве
Како уређивати ИоуТубе видео записе на иПхоне-у помоћу ИоуТубе уређивача
Како уређивати ИоуТубе видео записе на иПхоне-у помоћу ИоуТубе уређивача
 
Соул хиперцикл и талас нових фитнес бутика
Соул хиперцикл и талас нових фитнес бутика
Стратегије одређивања цена за успех: Практични водич
Стратегије одређивања цена за успех: Практични водич
Изазов Интернет цензура: Како сам направио веб локацију за прикупљање проверених микроблога
Изазов Интернет цензура: Како сам направио веб локацију за прикупљање проверених микроблога
Аутоматски поставите веб апликације користећи ГитХуб Вебхоокс
Аутоматски поставите веб апликације користећи ГитХуб Вебхоокс
Водич за стримовање Апацхе Спарк-а: Идентификовање хасхтагова у тренду са Твиттера
Водич за стримовање Апацхе Спарк-а: Идентификовање хасхтагова у тренду са Твиттера
Категорије
Рисе Оф РемотеПрофитабилност И ЕфикасностНаука О Подацима И Базе ПодатакаИнтернет Фронт-ЕндДизајн БрендаПројектни МенаџментЉуди И ТимовиТрендовиЖивот ДизајнераБудућност Посла

© 2023 | Сва Права Задржана

socialgekon.com