portaldacalheta.pt
  • Основен
  • Agile Talent
  • Дизайнерски Живот
  • Възходът На Дистанционното
  • Рентабилност И Ефективност
Back-End

5 неща, които никога не сте правили с REST спецификация



Повечето разработчици от преден и заден план вече са се занимавали с REST спецификации и RESTful API. Но не всички RESTful API са създадени еднакво. Всъщност те рядко са RESTful изобщо ...

Какво Е RESTful API?

Това е мит.



Ако смятате, че вашият проект има RESTful ПОЖАР , най-вероятно грешите. Идеята на RESTful API е да се разработи по начин, който следва всички архитектурни правила и ограничения, описани в спецификацията REST. Реално обаче това на практика е до голяма степен невъзможно.



От една страна, REST съдържа твърде много размазани и двусмислени определения. Например, на практика някои термини от речниците на HTTP метода и кода на състоянието се използват в противоречие с предвидените им цели или изобщо не се използват.



От друга страна, разработването на REST създава твърде много ограничения. Например използването на атомни ресурси е неоптимално за реални API, които се използват в мобилни приложения. Пълното отказване на съхранение на данни между заявки по същество забранява механизма „потребителска сесия“, който се вижда почти навсякъде.

Но чакай, не е толкова лошо!



Шаблон на документ за проектиране за разработка на софтуер

За какво ви е необходима спецификация REST API?

Въпреки тези недостатъци, с разумен подход, REST все още е невероятна концепция за създаване на наистина страхотни API . Тези API могат да бъдат последователни и да имат ясна структура, добра документация и голямо покритие на единични тестове. Можете да постигнете всичко това с високо качество Спецификация на API .

Обикновено REST API спецификацията е свързана с неговата документация . За разлика от спецификацията - официално описание на вашия API - документацията е предназначена за четене от човека: например, четена от разработчиците на мобилното или уеб приложението, което използва вашия API.



Правилното описание на API не се отнася само до доброто писане на документация за API. В тази статия искам да споделя примери за това как можете:

  • Направете вашите модулни тестове по-опростени и по-надеждни;
  • Настройте предварителна обработка и проверка на въведените от потребителя;
  • Автоматизирайте сериализацията и осигурете последователност на отговорите; и дори
  • Насладете се на предимствата на статичното писане.

Но първо, нека започнем с въведение в света на спецификациите на API.



OpenAPI

Понастоящем OpenAPI е най-широко приеманият формат за спецификации на REST API. Спецификацията е написана в един файл във формат JSON или YAML, състоящ се от три раздела:

  1. Заглавка с име, описание и версия на API, както и всякаква допълнителна информация.
  2. Описания на всички ресурси, включително идентификатори, HTTP методи, всички входни параметри, кодове за отговор и типове данни на тялото, с връзки към дефиниции.
  3. Всички дефиниции, които могат да се използват за въвеждане или извеждане, във формат JSON Schema (които, да, също могат да бъдат представени в YAML.)

Структурата на OpenAPI има два съществени недостатъка: Тя е твърде сложна и понякога излишна. Малък проект може да има JSON спецификация от хиляди редове. Поддържането на този файл ръчно става невъзможно. Това е значителна заплаха за идеята за поддържане на актуализацията на спецификацията, докато се разработва API.



Има множество редактори, които ви позволяват да опишете API и да създадете изход OpenAPI. Допълнителните услуги и облачните решения, базирани на тях, включват Swagger, Apiary, Stoplight, Restlet и много други.

Тези услуги обаче бяха неудобни за мен поради сложността на бързото редактиране на спецификацията и привеждането му в съответствие с промените в кода. Освен това списъкът с функции зависи от конкретна услуга. Например създаването на пълноценни модулни тестове въз основа на инструментите на облачна услуга е почти невъзможно. Генерирането на кодове и подигравателните крайни точки, макар и да изглеждат практични, на практика се оказват предимно безполезни. Това е най-вече защото поведението на крайната точка обикновено зависи от различни неща, като потребителски разрешения и входни параметри, които може да са очевидни за архитекта на API, но не са лесни за автоматично генериране от спецификация на OpenAPI.



Тиниспек

В тази статия ще използвам примери, базирани на моя собствен формат за дефиниция на REST API, tinyspec . Определенията се състоят от малки файлове с интуитивен синтаксис. Те описват крайни точки и модели на данни, които се използват в проект. Файловете се съхраняват до кода, осигурявайки бърза справка и възможност за редактиране по време на писането на код. Tinyspec се компилира автоматично в пълноценен формат OpenAPI, който може веднага да бъде използван във вашия проект.

Също така ще използвам примери Node.js (Koa, Express) и Ruby on Rails, но практиките, които ще демонстрирам, са приложими за повечето технологии, включително Python, PHP и Java.

Където спецификацията на API скали

Сега, след като имаме известен опит, можем да проучим как да извлечем максимума от правилно посочения API.

1. Тестове за крайни точки

Управленското поведение (BDD) е идеално за разработване на REST API. Най-добре е да пишете модулни тестове не за отделни класове, модели или контролери, а за конкретни крайни точки. Във всеки тест вие емулирате реална HTTP заявка и проверявате отговора на сървъра. За Node.js има супертест и chai-http пакети за емулиране на заявки, а за Ruby on Rails има във въздуха .

Да приемем, че имаме User схема и a GET /users крайна точка, която връща всички потребители. Ето малко синтаксис на tinyspec, който описва това:

# user.models.tinyspec User {name, isAdmin: b, age?: i} # users.endpoints.tinyspec GET /users => {users: User[]}

И ето как бихме написали съответния тест:

Node.js

describe('/users', () => { it('List all users', async () => { const { status, body: { users } } = request.get('/users'); expect(status).to.equal(200); expect(users[0].name).to.be('string'); expect(users[0].isAdmin).to.be('boolean'); expect(users[0].age).to.be.oneOf(['boolean', null]); }); });

Рубин на релси

describe 'GET /users' do it 'List all users' do get '/users' expect_status(200) expect_json_types('users.*', { name: :string, isAdmin: :boolean, age: :integer_or_null, }) end end

Когато вече имаме спецификацията, която описва отговорите на сървъра, можем да опростим теста и просто да проверим дали отговорът следва спецификацията. Можем да използваме tinyspec модели, всеки от които може да се трансформира в спецификация OpenAPI, която следва формата JSON Schema.

Всеки буквален обект в JS (или Hash в Ruby, dict в Python, асоциативен масив в PHP и дори Map в Java) може да бъде проверена за съответствие с JSON Schema. Има дори подходящи приставки за тестване на рамки, например is-ajv (над морското равнище), chai-ajv-json-schema (npm) и json_matchers за RSpec (rubygem).

Преди да използваме схеми, нека ги импортираме в проекта. Първо генерирайте openapi.json файл, базиран на спецификацията на tinyspec (можете да направите това автоматично преди всяко пробно изпълнение):

tinyspec -j -o openapi.json

Node.js

Сега можете да използвате генерирания JSON в проекта и да получите definitions ключ от него. Този ключ съдържа всички JSON схеми. Схемите могат да съдържат кръстосани препратки ($ref), така че ако имате вградени схеми (например Blog {posts: Post[]}), трябва да ги разгънете, за да ги използвате при проверка. За това ще използваме json-schema-deref-sync (над морското равнище).

import deref from 'json-schema-deref-sync'; const spec = require('./openapi.json'); const schemas = deref(spec).definitions; describe('/users', () => { it('List all users', async () => { const { status, body: { users } } = request.get('/users'); expect(status).to.equal(200); // Chai expect(users[0]).to.be.validWithSchema(schemas.User); // Jest expect(users[0]).toMatchSchema(schemas.User); }); });

Рубин на релси

json_matchers модул знае как да обработва $ref препратки, но изисква отделни файлове със схеми на посоченото място, така че ще трябва разделете swagger.json първо в множество по-малки файлове :

# ./spec/support/json_schemas.rb require 'json' require 'json_matchers/rspec' JsonMatchers.schema_root = 'spec/schemas' # Fix for json_matchers single-file restriction file = File.read 'spec/schemas/openapi.json' swagger = JSON.parse(file, symbolize_names: true) swagger[:definitions].keys.each do |key| File.open('spec/schemas/#{key}.json', 'w') do |f| f.write(JSON.pretty_generate({ '$ref': 'swagger.json#/definitions/#{key}' })) end end

Ето как ще изглежда тестът:

describe 'GET /users' do it 'List all users' do get '/users' expect_status(200) expect(result[:users][0]).to match_json_schema('User') end end

Писането на тестове по този начин е невероятно удобно. Особено така, ако вашата IDE поддържа провеждане на тестове и отстраняване на грешки (например WebStorm, RubyMine и Visual Studio). По този начин можете да избегнете използване на друг софтуер , а целият цикъл на разработка на API е ограничен до три стъпки:

  1. Проектиране на спецификацията в tinyspec файлове.
  2. Написване на пълен набор от тестове за добавени / редактирани крайни точки.
  3. Внедряване на кода, който удовлетворява тестовете.

2. Проверка на входните данни

OpenAPI описва не само формата на отговора, но и входните данни. Това ви позволява да проверите изпратените от потребителя данни по време на изпълнение и да осигурите последователност и сигурен актуализации на базата данни.

Да кажем, че имаме следната спецификация, която описва корекцията на потребителски запис и всички налични полета, които могат да бъдат актуализирани:

# user.models.tinyspec UserUpdate !{name?, age?: i} # users.endpoints.tinyspec PATCH /users/:id {user: UserUpdate} => {success: b}

Преди проучихме приставките за проверка по време на теста, но за по-общи случаи има ajv (npm) и json-схема (rubygem) модули за валидиране. Нека ги използваме, за да напишем контролер с валидиране:

Node.js (Koa)

Това е пример за Koa, наследник на Express, но еквивалентният Express код ще изглежда подобно.

import Router from 'koa-router'; import Ajv from 'ajv'; import { schemas } from './schemas'; const router = new Router(); // Standard resource update action in Koa. router.patch('/:id', async (ctx) => { const updateData = ctx.body.user; // Validation using JSON schema from API specification. await validate(schemas.UserUpdate, updateData); const user = await User.findById(ctx.params.id); await user.update(updateData); ctx.body = { success: true }; }); async function validate(schema, data) { const ajv = new Ajv(); if (!ajv.validate(schema, data)) { const err = new Error(); err.errors = ajv.errors; throw err; } }

В този пример сървърът връща 500 Internal Server Error отговор, ако входът не отговаря на спецификацията. За да избегнем това, можем да хванем грешката на валидатора и да формираме свой собствен отговор, който да съдържа по-подробна информация за конкретни полета, които не са успели при валидирането, и да следваме спецификацията.

Нека добавим определението за FieldsValidationError:

# error.models.tinyspec Error {error: b, message} InvalidField {name, message} FieldsValidationError

И сега нека го изброим като един от възможните отговори на крайната точка:

# users.endpoints.tinyspec PATCH /users/:id {user: UserUpdate} => 200 {success: b} => 422 FieldsValidationError

Този подход ви позволява да пишете модулни тестове, които тестват верността на сценариите за грешки, когато невалидни данни идват от клиента.

3. Сериализация на модела

Почти всички съвременни сървърни рамки използват обектно-релационно картографиране (ORM) по един или друг начин. Това означава, че по-голямата част от ресурсите, които API използва, са представени от модели и техните екземпляри и колекции.

Извиква се процесът на формиране на JSON представителства за тези обекти, които да бъдат изпратени в отговора сериализация .

акцент върху принципите на дизайна

Има редица приставки за извършване на сериализация: Например, sequelize-to-json (над морското равнище), act_as_api (rubygem) и jsonapi-rails (rubygem). По принцип тези приставки ви позволяват да предоставите списъка с полета за конкретен модел, които трябва да бъдат включени в обекта JSON, както и допълнителни правила. Например можете да преименувате полета и да изчислявате динамично техните стойности.

Става по-трудно, когато имате нужда от няколко различни JSON представления за един модел или когато обектът съдържа вложени обекти - асоциации. След това започвате да се нуждаете от функции като наследяване, повторно използване и свързване на сериализатор.

Различните модули предоставят различни решения, но нека разгледаме следното: Може ли спецификацията да помогне отново? По принцип цялата информация за изискванията за представяне на JSON, всички възможни комбинации от полета, включително вградени обекти, вече са в него. И това означава, че можем да напишем един автоматизиран сериализатор.

Позволете ми да представя малкото секвелизира-сериализира (npm) модул, който поддържа това за Sequelize модели. Той приема екземпляр от модел или масив и необходимата схема и след това се итерира през него, за да изгради сериализирания обект. Той също така отчита всички задължителни полета и използва вложени схеми за свързаните с тях обекти.

Така че, да кажем, че трябва да върнем всички потребители с публикации в блога, включително коментарите към тези публикации, от API. Нека го опишем със следната спецификация:

# models.tinyspec Comment {authorId: i, message} Post {topic, message, comments?: Comment[]} User {name, isAdmin: b, age?: i} UserWithPosts {users: UserWithPosts[]}

Сега можем да изградим заявката с Sequelize и да върнем сериализирания обект, който съответства точно на описаната по-горе спецификация:

import Router from 'koa-router'; import serialize from 'sequelize-serialize'; import { schemas } from './schemas'; const router = new Router(); router.get('/blog/users', async (ctx) => { const users = await User.findAll({ include: [{ association: User.posts, required: true, include: [Post.comments] }] }); ctx.body = serialize(users, schemas.UserWithPosts); });

Това е почти вълшебно, нали?

4. Статично въвеждане

Ако сте достатъчно готини да използвате TypeScript или Flow, може би вече сте попитали: „Какво от моите скъпоценни статични типове ?!“ С sw2dts или swagger-to-flowtype модули можете да генерирате всички необходими статични типове въз основа на JSON схеми и да ги използвате в тестове, контролери и сериализатори.

tinyspec -j sw2dts ./swagger.json -o Api.d.ts --namespace Api

Сега можем да използваме типове в контролери:

router.patch('/users/:id', async (ctx) => { // Specify type for request data object const userData: Api.UserUpdate = ctx.request.body.user; // Run spec validation await validate(schemas.UserUpdate, userData); // Query the database const user = await User.findById(ctx.params.id); await user.update(userData); // Return serialized result const serialized: Api.User = serialize(user, schemas.User); ctx.body = { user: serialized }; });

И тестове:

it('Update user', async () => { // Static check for test input data. const updateData: Api.UserUpdate = { name: MODIFIED }; const res = await request.patch('/users/1', { user: updateData }); // Type helper for request response: const user: Api.User = res.body.user; expect(user).to.be.validWithSchema(schemas.User); expect(user).to.containSubset(updateData); });

Имайте предвид, че генерираните дефиниции на типове могат да се използват не само в проекта на API, но и в проекти на клиентски приложения, за да опишат типове във функции, които работят с API. (Angular разработчиците ще бъдат особено доволни от това.)

5. Предаване на типове низове на заявки

Ако вашият API по някаква причина консумира заявки с application/x-www-form-urlencoded MIME тип вместо application/json, тялото на заявката ще изглежда така:

param1=value¶m2=777¶m3=false

Същото важи и за параметрите на заявката (например в GET заявки). В този случай уеб сървърът няма да успее автоматично да разпознае типове: Всички данни ще бъде в низ формат , така че след анализирането ще получите този обект:

{ param1: 'value', param2: '777', param3: 'false' }

В този случай заявката ще се провали при проверка на схемата, така че трябва да проверите ръчно форматите на правилните параметри и да ги предадете на правилните типове.

Както се досещате, можете да го направите с нашите стари стари схеми от спецификацията. Да предположим, че имаме тази крайна точка и следната схема:

# posts.endpoints.tinyspec GET /posts?PostsQuery # post.models.tinyspec PostsQuery { search, limit: i, offset: i, filter: { isRead: b } }

Ето как изглежда заявката към тази крайна точка:

GET /posts?search=needle&offset=10&limit=1&filter[isRead]=true

Нека напишем castQuery функция за предаване на всички параметри към необходимите типове:

function castQuery(query, schema) { _.mapValues(query, (value, key) => { const { type } = schema.properties[key] || {}; if (!value || !type) { return value; } switch (type) { case 'integer': return parseInt(value, 10); case 'number': return parseFloat(value); case 'boolean': return value !== 'false'; default: return value; } }); }

По-пълно изпълнение с поддръжка за вложени схеми, масиви и null типове се предлага в отливка със схема (npm) модул. Сега нека го използваме в нашия код:

router.get('/posts', async (ctx) => { // Cast parameters to expected types const query = castQuery(ctx.query, schemas.PostsQuery); // Run spec validation await validate(schemas.PostsQuery, query); // Query the database const posts = await Post.search(query); // Return serialized result ctx.body = { posts: serialize(posts, schemas.Post) }; });

Имайте предвид, че три от четирите реда код използват схеми за спецификация.

Най-добри практики

Има редица най-добри практики, които можем да следваме тук.

Използвайте отделни схеми за създаване и редактиране

Обикновено схемите, които описват отговорите на сървъра, са различни от тези, които описват входовете и се използват за създаване и редактиране на модели. Например списъкът с полета, налични в POST и PATCH заявките трябва да бъдат строго ограничени и PATCH обикновено има всички полета, маркирани като незадължителни. Схемите, които описват отговора, могат да бъдат по-свободни.

Когато ти генерирайте CRUDL крайни точки автоматично , tinyspec използва New и Update постфикси. User* схеми могат да бъдат дефинирани по следния начин:

User {id, email, name, isAdmin: b} UserNew !{email, name} UserUpdate !{email?, name?}

Опитайте се да не използвате едни и същи схеми за различни типове действия, за да избегнете случайни проблеми със сигурността поради повторно използване или наследяване на по-стари схеми.

Следвайте конвенциите за имена на схеми

Съдържанието на едни и същи модели може да варира за различните крайни точки. Използвайте With* и For* постфикси в имената на схеми, за да покажат разликата и целта. В tinyspec моделите също могат да наследяват един от друг. Например:

User {name, surname} UserWithPhotos

Постфиксите могат да бъдат разнообразни и комбинирани. Името им все още трябва да отразява същността и да прави документацията по-лесна за четене.

Разделяне на крайни точки въз основа на типа клиент

Често една и съща крайна точка връща различни данни въз основа на типа клиент или ролята на потребителя, изпратил заявката. Например GET /users и GET /messages крайните точки могат да бъдат значително различни за потребителите на мобилни приложения и мениджърите на бек офиса. Промяната на името на крайната точка може да бъде режийни.

За да опишете една и съща крайна точка няколко пъти, можете да добавите нейния тип в скоби след пътя. Това също улеснява използването на тагове: Разделяте документацията за крайната точка на групи, всяка от които е предназначена за конкретна клиентска група на API. Например:

Mobile app: GET /users (mobile) => UserForMobile[] CRM admin panel: GET /users (admin) => UserForAdmin[]

Инструменти за документация на REST API

След като получите спецификацията във формат tinyspec или OpenAPI, можете да генерирате добре изглеждаща документация в HTML формат и да я публикувате. Това ще зарадва разработчиците, които използват вашия API, и със сигурност е по-добро попълване на ръчен шаблон за документация за REST API.

Освен облачните услуги, споменати по-рано, има CLI инструменти, които конвертират OpenAPI 2.0 в HTML и PDF, които могат да бъдат внедрени във всеки статичен хостинг. Ето няколко примера:

  • bootprint-openapi (npm, използвано по подразбиране в tinyspec)
  • swagger2markup-cli (буркан, има пример за използване , ще се използва в tinyspec Cloud )
  • redoc-cli (над морското равнище)
  • widdershins (над морското равнище)

Имате ли още примери? Споделете ги в коментарите.

За съжаление, въпреки че беше пуснат преди година, OpenAPI 3.0 все още се поддържа слабо и не успях да намеря правилни примери за документация, базирана на него, както в облачни решения, така и в CLI инструменти. По същата причина tinyspec все още не поддържа OpenAPI 3.0.

Публикуване в GitHub

Един от най-простите начини за публикуване на документацията е Страници на GitHub . Просто активирайте поддръжката за статични страници за вашия /docs папка в настройките на хранилището и съхранявайте HTML документацията в тази папка.

Хостинг на HTML документацията на вашата REST спецификация от папката / docs чрез GitHub Pages.

Можете да добавите командата за генериране на документация чрез tinyspec или друг CLI инструмент във вашия scripts/package.json файл за автоматично актуализиране на документацията след всеки ангажимент:

'scripts': { 'docs': 'tinyspec -h -o docs/', 'precommit': 'npm run docs' }

Непрекъсната интеграция

Можете да добавите генериране на документация към вашия цикъл на CI и да я публикувате, например, в Amazon S3 под различни адреси в зависимост от средата или версията на API (като /docs/2.0, /docs/stable и /docs/staging. )

Облак Тиниспек

Ако харесвате синтаксиса на tinyspec, можете да станете ранен осиновител на tinyspec.cloud . Планираме да изградим облачна услуга въз основа на нея и CLI за автоматизирано внедряване на документация с богат избор от шаблони и възможност за разработване на персонализирани шаблони.

REST Спецификация: Чудесен мит

Разработването на REST API е може би един от най-приятните процеси в развитието на съвременните уеб и мобилни услуги. Няма браузър, операционна система и зоологически градини с размер на екрана и всичко е изцяло под ваш контрол, на една ръка разстояние.

Този процес се улеснява още повече от поддръжката за автоматизация и актуални спецификации. API, използващ описаните от мен подходи, става добре структуриран, прозрачен и надежден.

Изводът е, че ако правим мит, защо да не го направим прекрасен мит?

Свързани: ActiveResource.js ORM: Бързо изграждане на мощен JavaScript SDK за вашия JSON API

Разбиране на основите

Какво е REST?

REST е архитектурен стил на уеб услуга, дефиниращ набор от необходими ограничения. Той се основава на ресурси с уникални идентификатори (URI) и операции с тези ресурси. Освен това спецификацията REST изисква модел клиент-сървър, еднакъв интерфейс и отсъствие на съхранено в сървъра състояние.

Какво представлява спецификацията OpenAPI?

Спецификацията OpenAPI е общоприет формат за описание на REST API. Спецификацията се състои от един JSON или YAML файл с обща информация за API, описания за всички използвани ресурси и данни във формат JSON Schema.

Какво е Swagger?

Swagger е името на спецификацията на Open API преди 2016 г. В момента Swagger е отделен проект, с редица инструменти с отворен код и търговски инструменти и облачни услуги за изготвяне и разработване на спецификации на OpenAPI. Той също така предоставя инструменти за генериране на сървърния код и автоматизирано тестване на крайни точки.

Какво представлява JSON Schema?

JSON Schema е спецификация на JSON обект (документ). Състои се от списък на всички налични свойства и техните типове, включително списък на необходимите свойства.

Какво точно представлява API?

Интерфейсът за приложно програмиране (API) е начин за комуникация между софтуерни единици. Обикновено това е набор от налични методи, команди и дефиниции, които един софтуерен компонент предоставя на останалите.

c корпоративна разлика в корпорацията

Какво представлява спецификацията на API?

Спецификацията на API е специално форматиран документ, който предоставя ясни дефиниции на методите и характеристиките на API, както и техните възможни конфигурации.

Какво се разбира под документация за API?

Документацията на API е документ, четим от хора, предназначен за разработчици на трети страни да научат функциите на API и да създадат свой собствен софтуер, който използва описания API.

Какво прави RESTful API?

Това е HTTP API, където се подразбира, че той следва стандарти и ограничения, определени от архитектурния стил REST. На практика обаче почти никакви API не са 100% RESTful.

Какво се разбира под поведенческо развитие?

BDD е техника за разработване на софтуер, която предполага, че всяка малка промяна на програмата трябва да бъде тествана спрямо предварително определено поведение. Така че разработчиците първо определят очаквано поведение, често под формата на автоматизиран модулен тест, и след това прилагат кода, като се уверят, че всички тестове за поведение преминават.

Какво е tinyspec?

Tinyspec е стенография за REST API документация, компилирана във формат OpenAPI. Целта му е да улесни проектирането и разработването на спецификацията. Например, тя ви позволява да разделяте дефинициите на крайни точки (ресурси) и да съхранявате модели на данни в отделни по-малки файлове до вашия изходен код.

продуктов специалист

Други

продуктов специалист
Моделът за публикуване-абониране на релси: Урок за внедряване

Моделът за публикуване-абониране на релси: Урок за внедряване

Уеб Интерфейс

Популярни Публикации
Широки срещу тесни набори от умения: Демистифицирани умения за софтуерно инженерство
Широки срещу тесни набори от умения: Демистифицирани умения за софтуерно инженерство
Талантът не е стока
Талантът не е стока
iOS 9 Betas и WatchOS 2 за разработчици
iOS 9 Betas и WatchOS 2 за разработчици
Ръководство за многообработващи мрежови сървърни модели
Ръководство за многообработващи мрежови сървърни модели
Как да създадете персонализирани шрифтове: 7 стъпки и 3 казуса
Как да създадете персонализирани шрифтове: 7 стъпки и 3 казуса
 
Настройка на производителността на базата данни на SQL за разработчици
Настройка на производителността на базата данни на SQL за разработчици
Събуждане на спяща индустрия: Нарушаване на индустрията на матраците
Събуждане на спяща индустрия: Нарушаване на индустрията на матраците
Figma срещу Sketch срещу Axure - Преглед, основан на задачи
Figma срещу Sketch срещу Axure - Преглед, основан на задачи
Разбиране на нюансите на класификацията на шрифтовете
Разбиране на нюансите на класификацията на шрифтовете
Възраст преди красотата - Ръководство за дизайн на интерфейси за възрастни възрастни
Възраст преди красотата - Ръководство за дизайн на интерфейси за възрастни възрастни
Популярни Публикации
  • при вземането на бизнес решения, мениджърите обикновено разглеждат двата основни фактора:
  • има ли класове в javascript
  • c++ включва файл
  • настройка на производителността на sql server 2014
  • гещалт принципите на организацията
Категории
  • Agile Talent
  • Дизайнерски Живот
  • Възходът На Дистанционното
  • Рентабилност И Ефективност
  • © 2022 | Всички Права Запазени

    portaldacalheta.pt