Русский
Русский
English
Статистика
Реклама

Await

Перевод Использование глобального await в JavaScript

19.10.2020 14:23:26 | Автор: admin


Новая возможность, которая может изменить наш подход к написанию кода

JavaScript очень гибкий и мощный язык, который определяет развитие современного веба. Одна из основных причин главенства JavaScript в сфере веб-разработки заключается в его стермительном развитии и постоянном совершенствовании.

Одним из предложений по улучшению JavaScript является предложение под названием top-level await (await верхнего уровня, глобальный await). Цель данного предложения состоит в превращении ES модулей в некое подобие асинхронных функций. Это позволит модулям получать готовые к использованию ресурсы и блокировать модули, импортирующие их. Модули, которые импортируют ожидаемые ресурсы, смогут запускать выполнение кода только после получения ресурсов и их предварительной подготовки к использованию.

Указанное предложение в настоящее время находится на 3 стадии рассмотрения, поэтому данную возможность пока нельзя использовать в продакшне. Однако можно быть уверенным в том, что в ближайшем будущем она непременно будет реализована.

Не переживайте из-за этого. Продолжайте читать. Я покажу, как можно использовать названную фичу уже сейчас.

Что не так с обычным await?


Если вы попытаетесь использовать ключевое слово await за пределами асинхронной функции, то получите синтаксическую ошибку. Во избежание этого разработчики используют немедленно вызываемые функциональные выражения (Immediately Invoked Function Expression, IIFE).

await Promise.resolve(console.log("")); // Ошибка(async () => {    await Promise.resolve(console.log(""))})();

Указанная проблема и ее решение это лишь вершина айсберга


При работе с ES6 модулями вы, как правило, имеете дело с большим количеством экземпляров, экспортирующих и импортирующих значения. Рассмотрим пример:

// library.jsexport const sqrt = Math.sqrt;export const square = (x) => x * x;export const diagonal = (x, y) => sqrt((square(x) + square(y)));// middleware.jsimport { square, diagonal } from "./library.js";console.log("From Middleware");let squareOutput;let diagonalOutput;const delay = (ms) => new Promise((resolve) => {    const timer = setTimeout(() => {        resolve(console.log(""));        clearTimeout(timer);    }, ms);});// IIFE(async () => {    await delay(1000);    squareOutput = square(13);    diagonalOutput = diagonal(12, 5);})();export { squareOutput, diagonalOutput };

В приведенном примере мы экспортируем и импортируем переменные между library.js и middleware.js. Вы можете назвать файлы как угодно.

Функция delay возвращает промис, разрешающийся после задержки. Поскольку данная функция является асинхронной, мы используем ключевое слово await внутри IIFE для ожидания ее завершения. В реальном приложении вместо функции delay будет вызов fetch (запроса на получение данных) или другая асинхронная задача. После разрешения промиса, мы присваиваем значение нашей переменной. Это означает, что до разрешения промиса наша переменная будет иметь значение undefined.

В конце кода мы экспортируем наши переменные для того, чтобы они могли использоваться в другом коде.

Давайте взглянем на код, в котором эти переменные импортируются и используются:

// main.jsimport { squareOutput, diagonalOutput } from "./middleware.js";console.log(squareOutput); // undefinedconsole.log(diagonalOutput); // undefinedconsole.log("From Main");const timer1 = setTimeout(() => {    console.log(squareOutput);    clearTimeout(timer1);}, 2000); // 169const timer2 = setTimeout(() => {    console.log(diagonalOutput);    clearTimeout(timer2);}, 2000); // 13

Если вы запустите этот код, то в первых двух случаях получите undefined, а в третьем и четвертом 169 и 13, соответственно. Почему так происходит?

Это происходит из-за того, что мы пытаемся получить значения переменных, экспортируемых из middleware.js, в main.js до завершения выполнения асинхронной функции. Вы помните, что у нас там промис, ожидающий разрешения?

Для решения данной проблемы, нам нужно каким-то образом сообщить импортирующему модулю о готовности переменных к использованию.

Обходные пути

Существует, как минимум, два способа решить обозначенную проблему.

1. Экспорт промиса для инициализации

Во-первых, можно экспортировать IIFE. Ключевое слово async делает метод асинхронным, такой метод всегда возвращает промис. Вот почему в приведенном ниже примере асинхронное IIFE возвращает промис.

// middleware.jsimport { square, diagonal } from "./library.js";console.log("From Middleware");let squareOutput;let diagonalOutput;const delay = (ms) => new Promise((resolve) => {    const timer = setTimeout(() => {        resolve(console.log(""));        clearTimeout(timer);    }, ms);});// обходной маневр или, как еще говорят, костыльexport default (async () => {    await delay(1000);    squareOutput = square(13);    diagonalOutput = diagonal(12, 5);})();export { squareOutput, diagonalOutput };

При получении доступа к экспортируемым переменным в main.js можно подождать выполнения IIFE.

// main.jsimport promise, { squareOutput, diagonalOutput } from "./middleware.js";promise.then(() => {    console.log(squareOutput); // 169    console.log(diagonalOutput); // 169    console.log("From Main");});const timer1 = setTimeout(() => {    console.log(squareOutput);    clearTimeout(timer1);}, 2000); // 169const timer2 = setTimeout(() => {    console.log(diagonalOutput);    clearTimeout(timer2);}, 2000); // 13

Несмотря на то, что данный сниппет решает задачу, он приводит к другим проблемам.

  • При использовании указанного шаблона приходится искать нужный промис
  • Если в другом модуле также используются переменные squareOutput и diagonalOutput, мы должны обеспечить реэкспорт IIFE

Существует и другой способ.

2. Разрешение промиса IIFE с экспортируемыми переменными

В данном случае, вместо экспорта переменных по-отдельности, мы возвращаем их из нашего асинхронного IIFE. Это позволяет файлу main.js просто ждать разрешения промиса и извлекать его значение.

// middleware.jsimport { square, diagonal } from "./library.js";console.log("From Middleware");let squareOutput;let diagonalOutput;const delay = (ms) => new Promise((resolve) => {    const timer = setTimeout(() => {        resolve(console.log(""));        clearTimeout(timer);    }, ms);});// обходной маневрexport default (async () => {    await delay(1000);    squareOutput = square(13);    diagonalOutput = diagonal(12, 5);    return { squareOutput, diagonalOutput };})();// main.jsimport promise from "./middleware.js";promise.then(({ squareOutput, diagonalOutput }) => {    console.log(squareOutput); // 169    console.log(diagonalOutput); // 169    console.log("From Main");});const timer1 = setTimeout(() => {    console.log(squareOutput);    clearTimeout(timer1);}, 2000); // 169const timer2 = setTimeout(() => {    console.log(diagonalOutput);    clearTimeout(timer2);}, 2000); // 13

Однако у такого решения также имеются некоторые недостатки.

Согласно предложению данный шаблон имеет серьезный недостаток, заключающийся в необходимости существенной реорганизации связанного источника ресурсов в более динамические шаблоны и размещении большей части тела модуля в функции обратного вызова .then() для обеспечения возможности использования динамических модулей. Это представляет собой значительный регресс с точки зрения возможности статического анализа, тестируемости, эргономики и многого другого по сравнению с модулями ES2015.

Как глобальный await решает данную проблему?


await верхнего уровня позволяет модульной системе заботиться о разрешении промисов и их взаимодействии между собой.

// middleware.jsimport { square, diagonal } from "./library.js";console.log("From Middleware");let squareOutput;let diagonalOutput;const delay = (ms) => new Promise((resolve) => {    const timer = setTimeout(() => {        resolve(console.log(""));        clearTimeout(timer);    }, ms);});// "глобальный" awaitawait delay(1000);squareOutput = square(13);diagonalOutput = diagonal(12, 5);export { squareOutput, diagonalOutput };// main.jsimport { squareOutput, diagonalOutput } from "./middleware.js";console.log(squareOutput); // 169console.log(diagonalOutput); // 13console.log("From Main");const timer1 = setTimeout(() => {    console.log(squareOutput);    clearTimeout(timer1);}, 2000); // 169const timer2 = setTimeout(() => {    console.log(diagonalOutput);    clearTimeout(timer2);}, 2000); // 13

Ни одна из инструкций в main.js не выполняется до разрешения промисов в middleware.js. Это гораздо более чистое решение по сравнению с обходными путями.

Заметка

Глобальный await работает только с ES модулями. Используемые зависимости должны быть указаны явно. Приведенный ниже пример из репозитория предложения хорошо это демонстирует.

// x.mjsconsole.log("X1");await new Promise(r => setTimeout(r, 1000));console.log("X2");// y.mjsconsole.log("Y");// z.mjsimport "./x.mjs";import "./y.mjs";// X1// Y// X2

Данный сниппет не выведет в консоль X1, X2, Y, как можно ожидать, поскольку x и y отдельные модули, не связанные между собой.

Я настоятельно рекомендую вам изучить рездел часто задаваемые вопросы предложения для лучшего пониманию рассматриваемой фичи.

Реализация


V8

Вы можете протестировать данную возможность уже сейчас.

Для этого перейдите в директорию, в которой размещается Chrome на вашей машине. Убедитесь в том, что все вкладки браузера закрыты. Откройте терминал и введите следующую команду:

chrome.exe --js-flags="--harmony-top-level-await"

Вы таже можете попробовать данную фичу в Node.js. Прочитайте это руководство, чтобы узнать больше.

ES модули

Убедитесь, что добавили тегу script атрибут type со значением module.

<script type="module" src="./index.js"></script>

Обратите внимание, что в отличие от обычных скриптов, ES6 модули следуют политике общего происхождения (одного источника) (SOP) и совместного использования ресурсов (CORS). Поэтому с ними лучше работать на сервере.

Случаи использования


Согласно предложению случаями использования глобального await является следующее:

Динамический путь зависимости

const strings = await import(`/i18n/${navigator.language}`);

Это позволяет модулям использовать значения среды выполнения для вычисления путей зависимостей и может быть полезным для разделения разработка/продакшн код, интернационализации, разделения кода в зависимости от среды выполнения (браузер, Node.js) и т.д.

Инициализация ресурсов

const connection = await dbConnector()

Это помогает модулям получать готовые к использованию ресурсы и выбрасывать исключения в случае, когда модуль не может быть использован. Такой подход может использоваться для подстраховки, как будет показано ниже.

Запасной вариант

В приведенном ниже примере показано, как глобальный await может использоваться для загрузки зависимости с реализацией запасного варианта. Если импорт из CDN A провалился, осуществляется импорт из CDN B:

let jQuery;try {  jQuery = await import('https://cdn-a.example.com/jQuery');} catch {  jQuery = await import('https://cdn-b.example.com/jQuery');}

Критика


Rich Harris составил список критических замечаний относительно await верхнего уровня. Он включает в себя следующее:

  • Глобальный await может блокировать выполнение кода
  • Глобальный await может блокировать получение ресурсов
  • Отсутствует поддержка CommonJS модулей

Вот какие ответы на эти замечания даются в FAQ предложения:

  • Поскольку дочерние узлы (модули) имеют возможность выполнения, блокировка кода, в конечном счете, отсутствует
  • Глобальный await используется на стадии выполнения графа модулей. На данном этапе все ресурсы получены и связаны, поэтому риска блокировки получения ресурсов не усматривается
  • await верхнего уровня ограничен ES6 модулями. Поддержка CommonJS модулей, как и обычных скриптов, изначально не планировалась

Я снова настоятельно рекомендую ознакомиться с FAQ предложения.

Надеюсь, мне удалось доступно объяснить суть рассматриваемого предложения. Собираетесь ли использовать эту возможность? Делитесь своим мнением в комментариях.
Подробнее..

Перевод Магические сигнатуры методов в C

01.07.2020 14:11:33 | Автор: admin

Представляю вашему вниманию перевод статьи The Magical Methods in C# автора CEZARY PITEK.


Есть определенный набор сигнатур методов в C#, имеющих поддержку на уровне языка. Методы с такими сигнатурами позволяют использовать специальный синтаксис со всеми его преимуществами. Например, с их помощью можно упростить наш код или создать DSL для того, чтобы выразить решение проблемы более красивым образом. Я встречаюсь с такими методами повсеместно, так что я решил написать пост и обобщить все мои находки по этой теме, а именно:


  • Синтаксис инициализации коллекций
  • Синтаксис инициализации словарей
  • Деконструкторы
  • Пользовательские awaitable типы
  • Паттерн query expression

Синтаксис инициализации коллекций


Синтаксис инициализации коллекции довольно старая фича, т. к. она существует с C# 3.0 (выпущен в конце 2007 года). Напомню, синтаксис инициализации коллекции позволяет создать список с элементами в одном блоке:


var list = new List<int> { 1, 2, 3 };

Этот код эквивалентен приведенному ниже:


var list = new List<int>();list.Add(1);list.Add(2);list.Add(3);

Возможность использования синтаксиса инициализации коллекции не ограничивается только классами из BCL. Он может быть использован с любым типом, удовлетворяющим следующим условиям:


  • тип имплементирует интерфейс IEnumerable
  • тип имеет метод с сигнатурой void Add(T item)

public class CustomList<T>: IEnumerable{    public IEnumerator GetEnumerator() => throw new NotImplementedException();    public void Add(T item) => throw new NotImplementedException();}

Мы можем добавить поддержку синтаксиса инициализации коллекции, определив Add как метод расширения:


public static class ExistingTypeExtensions{    public static void Add<T>(ExistingType @this, T item) => throw new NotImplementedException();}

Этот синтаксис также можно использовать для вставки элементов в поле-коллекцию без публичного сеттера:


class CustomType{    public List<string> CollectionField { get; private set; }  = new List<string>();}class Program{    static void Main(string[] args)    {        var obj = new CustomType        {            CollectionField =            {                "item1",                "item2"            }        };    }}

Синтаксис инициализации коллекции полезен при инициализации коллекции известным числом элементов. Но что если мы хотим создать коллекцию с переменным числом элементов? Для этого есть менее известный синтаксис:


var obj = new CustomType{    CollectionField =    {        { existingItems }    }};

Такое возможно для типов, удовлетворяющих следующим условиям:


  • тип имплементирует интерфейс IEnumerable
  • тип имеет метод с сигнатурой void Add(IEnumerable<T> items)

public class CustomList<T>: IEnumerable{    public IEnumerator GetEnumerator() => throw new NotImplementedException();    public void Add(IEnumerable<T> items) => throw new NotImplementedException();}

К сожалению, массивы и коллекции из BCL не реализуют метод void Add(IEnumerable<T> items), но мы можем изменить это, определив метод расширения для существующих типов коллекций:


public static class ListExtensions{    public static void Add<T>(this List<T> @this, IEnumerable<T> items) => @this.AddRange(items);}

Благодаря этому мы можем написать следующее:


var obj = new CustomType{    CollectionField =    {        { existingItems.Where(x => /*Filter items*/).Select(x => /*Map items*/) }    }};

Или даже собрать коллекцию из смеси индивидуальных элементов и результатов нескольких перечислений (IEnumerable):


var obj = new CustomType{    CollectionField =    {        individualElement1,        individualElement2,        { list1.Where(x => /*Filter items*/).Select(x => /*Map items*/) },        { list2.Where(x => /*Filter items*/).Select(x => /*Map items*/) },    }};

Без подобного синтаксиса очень сложно получить подобный результат в блоке инициализации.


Я узнал об этой фиче совершенно случайно, когда работал с маппингами для типов с полями-коллекциями, сгенерированными из контрактов protobuf. Для тех, кто не знаком с protobuf: если вы используете grpctools для генерации типов .NET из файлов proto, все поля-коллекции генерируются подобным образом:


[DebuggerNonUserCode]public RepeatableField<ItemType> SomeCollectionField{    get    {        return this.someCollectionField_;    }}

Как можно заметить, поля-коллекции не имеют сеттер, но RepeatableField реализует метод void Add(IEnumerable items), так что мы по-прежнему можем инициализировать их в блоке инициализации:


/// <summary>/// Adds all of the specified values into this collection. This method is present to/// allow repeated fields to be constructed from queries within collection initializers./// Within non-collection-initializer code, consider using the equivalent <see cref="AddRange"/>/// method instead for clarity./// </summary>/// <param name="values">The values to add to this collection.</param>public void Add(IEnumerable<T> values){    AddRange(values);}

Синтаксис инициализации словарей


Одна из крутых фич C# 6.0 инициализация словаря по индексу, которая упростила синтаксис инициализации словарей. Благодаря ей мы можем писать более читаемый код:


var errorCodes = new Dictionary<int, string>{    [404] = "Page not Found",    [302] = "Page moved, but left a forwarding address.",    [500] = "The web server can't come out to play today."};

Этот код эквивалентен следующему:


var errorCodes = new Dictionary<int, string>();errorCodes[404] = "Page not Found";errorCodes[302] = "Page moved, but left a forwarding address.";errorCodes[500] = "The web server can't come out to play today.";

Это немного, но это определенно упрощает написание и чтение кода.


Лучшее в инициализации по индексу это то, что она не ограничивается классом Dictionary<T> и может быть использована с любым другим типом, определившим индексатор:


class HttpHeaders{    public string this[string key]    {        get => throw new NotImplementedException();        set => throw new NotImplementedException();    }}class Program{    static void Main(string[] args)    {        var headers = new HttpHeaders        {            ["access-control-allow-origin"] = "*",            ["cache-control"] = "max-age=315360000, public, immutable"        };    }}

Деконструкторы


В C# 7.0 помимо кортежей был добавлен механизм деконструкторов. Они позволяют декомпозировать кортеж в набор отдельных переменных:


var point = (5, 7);// decomposing tuple into separated variablesvar (x, y) = point;

Что эквивалентно следующему:


ValueTuple<int, int> point = new ValueTuple<int, int>(1, 4);int x = point.Item1;int y = point.Item2;

Этот синтаксис позволяет обменять значения двух переменных без явного объявления третьей:


int x = 5, y = 7;//switch(x, y) = (y,x);

Или использовать более краткий метод инициализации членов класса:


class Point{    public int X { get; }    public int Y { get; }    public Point(int x, int y)  => (X, Y) = (x, y);}

Деконструкторы могут быть использованы не только с кортежами, но и с другими типами. Для использования деконструкции типа этот тип должен реализовывать метод, подчиняющийся следующим правилам:


  • метод называется Deconstruct
  • метод возвращает void
  • все параметры метода имеют модификатор out

Для нашего типа Point мы можем объявить деконструктор следующим образом:


class Point{    public int X { get; }    public int Y { get; }    public Point(int x, int y) => (X, Y) = (x, y);    public void Deconstruct(out int x, out int y) => (x, y) = (X, Y);}

Пример использования приведен ниже:


var point = new Point(2, 4);var (x, y) = point;

"Под капотом" он превращается в следующее:


int x;int y;new Point(2, 4).Deconstruct(out x, out y);

Деконструкторы могут быть добавлены к типам с помощью методов расширения:


public static class PointExtensions{     public static void Deconstruct(this Point @this, out int x, out int y) => (x, y) = (@this.X, @this.Y);}

Один из самых полезных примеров применения деконструкторов это деконструкция KeyValuePair<TKey, TValue>, которая позволяет с легкостью получить доступ к ключу и значению во время итерирования по словарю:


foreach (var (key, value) in new Dictionary<int, string> { [1] = "val1", [2] = "val2" }){    //TODO: Do something}

KeyValuePair<TKey, TValue>.Deconstruct(TKey, TValue) доступно только с netstandard2.1. Для предыдущих версий netstandard нам нужно использовать ранее приведенный метод расширения.


Пользовательские awaitable типы


C# 5.0 (выпущен вместе с Visual Studio 2012) ввел механизм async/await, который стал переворотом в области асинхронного программирования. Прежде вызов асинхронного метода представлял собой запутанный код, особенно когда таких вызовов было несколько:


void DoSomething(){    DoSomethingAsync().ContinueWith((task1) => {        if (task1.IsCompletedSuccessfully)        {            DoSomethingElse1Async(task1.Result).ContinueWith((task2) => {                if (task2.IsCompletedSuccessfully)                {                    DoSomethingElse2Async(task2.Result).ContinueWith((task3) => {                        //TODO: Do something                    });                }            });        }    });}private Task<int> DoSomethingAsync() => throw new NotImplementedException();private Task<int> DoSomethingElse1Async(int i) => throw new NotImplementedException();private Task<int> DoSomethingElse2Async(int i) => throw new NotImplementedException();

Это может быть переписано намного красивее с использованием синтаксиса async/await:


async Task DoSomething(){    var res1 = await DoSomethingAsync();    var res2 = await DoSomethingElse1Async(res1);    await DoSomethingElse2Async(res2);}

Это может прозвучать удивительно, но ключевое слово await не зарезервировано только под использование с типом Task. Оно может быть использовано с любым типом, который имеет метод GetAwaiter, возвращающий удовлетворяющий следующим требованиям тип:


  • тип имплементирует интерфейс System.Runtime.CompilerServices.INotifyCompletion и реализует метод void OnCompleted(Action continuation)
  • тип имеет свойство IsCompleted логического типа
  • тип имеет метод GetResult без параметров

Для добавления поддержки ключевого слова await к пользовательскому типу мы должны определить метод GetAwaiter, возвращающий TaskAwaiter<TResult> или пользовательский тип, удовлетворяющий приведенным выше условиям:


class CustomAwaitable{    public CustomAwaiter GetAwaiter() => throw new NotImplementedException();}class CustomAwaiter: INotifyCompletion{    public void OnCompleted(Action continuation) => throw new NotImplementedException();    public bool IsCompleted => throw new NotImplementedException();    public void GetResult() => throw new NotImplementedException();}

Вы можете спросить: "Каков возможный сценарий использования синтаксиса await с пользовательским awaitable типом?". Если это так, то я рекомендую вам прочитать статью Stephen Toub под названием "await anything", которая показывает множество интересных примеров.


Паттерн query expression


Лучшее нововведение C# 3.0 Language-Integrated Query, также известное как LINQ, предназначенное для манипулирования коллекциями с SQL-подобным синтаксисом. LINQ имеет две вариации: SQL-подобный синтаксис и синтаксис методов расширения. Я предпочитаю второй вариант, т. к. по моему мнению он более читаем, а также потому что я привык к нему. Интересный факт о LINQ заключается в том, что SQL-подобный синтаксис во время компиляции транслируется в синтаксис методов расширения, т. к. это фича C#, а не CLR. LINQ был разработан в первую очередь для работы с типами IEnumerable, IEnumerable<T> и IQuerable<T>, но он не ограничен только ими, и мы можем использовать его с любым типом, удовлетворяющим требованиям паттерна query expression. Полный набор сигнатур методов, используемых LINQ, таков:


class C{    public C<T> Cast<T>();}class C<T> : C{    public C<T> Where(Func<T,bool> predicate);    public C<U> Select<U>(Func<T,U> selector);    public C<V> SelectMany<U,V>(Func<T,C<U>> selector, Func<T,U,V> resultSelector);    public C<V> Join<U,K,V>(C<U> inner, Func<T,K> outerKeySelector, Func<U,K> innerKeySelector, Func<T,U,V> resultSelector);    public C<V> GroupJoin<U,K,V>(C<U> inner, Func<T,K> outerKeySelector, Func<U,K> innerKeySelector, Func<T,C<U>,V> resultSelector);    public O<T> OrderBy<K>(Func<T,K> keySelector);    public O<T> OrderByDescending<K>(Func<T,K> keySelector);    public C<G<K,T>> GroupBy<K>(Func<T,K> keySelector);    public C<G<K,E>> GroupBy<K,E>(Func<T,K> keySelector, Func<T,E> elementSelector);}class O<T> : C<T>{    public O<T> ThenBy<K>(Func<T,K> keySelector);    public O<T> ThenByDescending<K>(Func<T,K> keySelector);}class G<K,T> : C<T>{    public K Key { get; }}

Разумеется, мы не обязаны реализовывать все эти методы для того, чтобы использовать синтаксис LINQ с нашим пользовательским типом. Список обязательных операторов и методов LINQ для них можно посмотреть здесь. Действительно хорошее объяснение того, как это сделать, можно найти в статье Understand monads with LINQ автора Miosz Piechocki.


Подведение итогов


Цель этой статьи заключается вовсе не в том, чтобы убедить вас злоупотреблять этими синтаксическими трюками, а в том, чтобы сделать их более понятными. С другой стороны, их нельзя всегда избегать. Они были разработаны для того, чтобы их использовать, и иногда они могут сделать ваш код лучше. Если вы боитесь, что получившийся код будет непонятен вашим коллегам, вам нужно найти способ поделиться знаниями с ними (или хотя бы ссылкой на эту статью). Я не уверен, что это полный набор таких "магических методов", так что если вы знаете еще какие-то пожалуйста, поделитесь в комментариях.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru