Веб-работники без отдельного файла Javascript?

291

Насколько я могу судить, веб-работники должны быть написаны в отдельном файле JavaScript и называться так:

new Worker('longrunning.js')

Я использую закрывающий компилятор для объединения и минимизации всего моего исходного кода JavaScript, и я бы предпочел не иметь своих работников в отдельных файлах для распространения. Есть ли способ сделать это?

new Worker(function() {
    //Long-running work here
});

Учитывая, что первоклассные функции так важны для JavaScript, почему стандартный способ выполнения фоновой работы должен загружать целый другой файл JavaScript с веб-сервера?

Бен Дилтс
источник
7
Это связано с тем, что сохранение контекста выполнения исключительно в поточно-ориентированном виде еще более важно, чем функции первого класса :-)
Pointy
1
Я работаю над этим (или, вернее, над минимизацией проблемы): DynWorker . Вы можете сделать: var worker = new DynWorker(); worker.inject("foo", function(){...});...
Félix Saparelli
1
ОП удалил вопрос «Обучающий работник принимать функцию вместо исходного файла JavaScript». Ответ находится здесь
Роб W
Я разработал файл task.js, чтобы сделать это намного проще. Большую часть времени вы пытаетесь снять нагрузку с небольших задач блокировки.
Чад Скира

Ответы:

225

http://www.html5rocks.com/en/tutorials/workers/basics/#toc-inlineworkers

Что если вы хотите создать рабочий сценарий на лету или создать автономную страницу без необходимости создания отдельных рабочих файлов? С помощью Blob () вы можете «встроить» своего работника в тот же HTML-файл, что и основная логика, создав дескриптор URL для рабочего кода в виде строки


Полный пример BLOB встроенного работника:

<!DOCTYPE html>
<script id="worker1" type="javascript/worker">
  // This script won't be parsed by JS engines because its type is javascript/worker.
  self.onmessage = function(e) {
    self.postMessage('msg from worker');
  };
  // Rest of your worker code goes here.
</script>
<script>
  var blob = new Blob([
    document.querySelector('#worker1').textContent
  ], { type: "text/javascript" })

  // Note: window.webkitURL.createObjectURL() in Chrome 10+.
  var worker = new Worker(window.URL.createObjectURL(blob));
  worker.onmessage = function(e) {
    console.log("Received: " + e.data);
  }
  worker.postMessage("hello"); // Start the worker.
</script>

VSync
источник
Решение только для Google Chrome, кажется, Firefox 10 будет его поддерживать, я не знаю о других браузерах
4esn0k
2
BlobBuiler устарела . Вместо этого используйте Blob . В настоящее время поддерживается в последних версиях Firefox / WebKit / Opera и IE10, см. Таблицы совместимости для старых браузеров.
Феликс Сапарелли
3
Конструктор BLOB-объектов может поддерживаться в IE10, но вы все равно не можете передать через него javascript веб-работнику (даже в IE11): connect.microsoft.com/IE/feedback/details/801810/… .
Джаярджо
1
@albanx - какие тесты? в Интернете уже есть миллиард демонстрационных страниц, показывающих, что многопоточность не останавливает браузер годами.
vsync
2
@albanx - не могли бы вы хотя бы сказать, какой эзотерический браузер вы используете, который зависает? это демо висит у вас? ie.microsoft.com/testdrive/Graphics/WorkerFountains/...
VSync
162

Решение html5rocks по встраиванию кода веб-работника в HTML довольно ужасно.
К тому же бланк экранированного JavaScript-as-a-string не лучше, не в последнюю очередь потому, что он усложняет рабочий процесс (компилятор Closure не может работать со строками).

Лично мне действительно нравятся методы toString, но @ dan-man ЭТО регулярное выражение!

Мой предпочтительный подход:

// Build a worker from an anonymous function body
var blobURL = URL.createObjectURL( new Blob([ '(',

function(){
    //Long-running work here
}.toString(),

')()' ], { type: 'application/javascript' } ) ),

worker = new Worker( blobURL );

// Won't be needing this anymore
URL.revokeObjectURL( blobURL );

Поддержка - это пересечение этих трех таблиц:

Однако это не сработает для SharedWorker , поскольку URL-адрес должен точно соответствовать, даже если необязательный параметр name совпадает. Для SharedWorker вам понадобится отдельный файл JavaScript.


Обновление 2015 года - наступает особенность ServiceWorker

Теперь есть еще более мощный способ решения этой проблемы. Опять же, сохраните рабочий код как функцию (а не статическую строку) и преобразуйте ее с помощью .toString (), затем вставьте код в CacheStorage под выбранным статическим URL-адресом.

// Post code from window to ServiceWorker...
navigator.serviceWorker.controller.postMessage(
 [ '/my_workers/worker1.js', '(' + workerFunction1.toString() + ')()' ]
);

// Insert via ServiceWorker.onmessage. Or directly once window.caches is exposed
caches.open( 'myCache' ).then( function( cache )
{
 cache.put( '/my_workers/worker1.js',
  new Response( workerScript, { headers: {'content-type':'application/javascript'}})
 );
});

Есть два возможных отступления. ObjectURL, как описано выше, или, что более удобно, поместите настоящий файл JavaScript в /my_workers/worker1.js

Преимущества этого подхода:

  1. SharedWorkers также могут поддерживаться.
  2. Вкладки могут совместно использовать одну кэшированную копию по фиксированному адресу. Подход BLOB-объектов распространяет случайные объектные URL для каждой вкладки.
Adria
источник
4
Как будет выглядеть совместимость браузера в этом решении?
Бен Дилтс
Можете ли вы уточнить это решение, как оно работает? Что такое worker1.js? Это отдельный файл JS? Я пытаюсь использовать это, но не могу заставить его работать. В частности, я пытаюсь заставить его работать на SharedWorker
Yehuda
Если бы вы только могли обернуть это в полезную функцию!
ммм
@ Ben Dilts: совместимость браузера будет выглядеть так, как если бы вы запускали свой код через babel: babeljs.io/repl
Джек Гиффин,
Стандарт не гарантирует, что Function.prototype.toString () вернет тело функции в виде строки. Вы, вероятно, должны добавить предупреждение к ответу.
RD
37

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

(function(global) {
    var is_worker = !this.document;
    var script_path = is_worker ? null : (function() {
        // append random number and time to ID
        var id = (Math.random()+''+(+new Date)).substring(2);
        document.write('<script id="wts' + id + '"></script>');
        return document.getElementById('wts' + id).
            previousSibling.src;
    })();
    function msg_parent(e) {
        // event handler for parent -> worker messages
    }
    function msg_worker(e) {
        // event handler for worker -> parent messages
    }
    function new_worker() {
        var w = new Worker(script_path);
        w.addEventListener('message', msg_worker, false);
        return w;
    }
    if (is_worker)
        global.addEventListener('message', msg_parent, false);

    // put the rest of your library here
    // to spawn a worker, use new_worker()
})(this);

Как видите, скрипт содержит весь код как для точки зрения родителей, так и для работника, проверяя, является ли его собственный отдельный экземпляр работником !document. Несколько громоздкое script_pathвычисление используется для точного вычисления пути сценария относительно родительской страницы, поскольку предоставленный путь new Workerотносится к родительской странице, а не к сценарию.

Делан Азабани
источник
4
Ваш сайт, похоже, исчез; у вас есть новый URL?
BrianFreud
1
Это интересный подход. FWIW, я определяю особенности Web Workers, проверяя наличие «я» (глобальный объект Web Worker) против «окна».
pwnall
Я изучал, как PapaParse работает с веб-работниками, и они, похоже, используют этот подход github.com/mholt/PapaParse
JP DeVries,
Я думаю, что тестирование с использованием 'typeof importScripts! == null' может определить, работает ли скрипт в рабочей области.
MeTTeO
1
Я не понимаю, что предыдущий Сиблинг из элемента-скрипта. Может кто-нибудь объяснить мне?
Teemoh
28

Используя Blobметод, как об этом для рабочего завода:

var BuildWorker = function(foo){
   var str = foo.toString()
             .match(/^\s*function\s*\(\s*\)\s*\{(([\s\S](?!\}$))*[\s\S])/)[1];
   return  new Worker(window.URL.createObjectURL(
                      new Blob([str],{type:'text/javascript'})));
}

Таким образом, вы можете использовать это так ...

var myWorker = BuildWorker(function(){
   //first line of worker
   self.onmessage(){....};
   //last line of worker
});

РЕДАКТИРОВАТЬ:

Я просто расширил эту идею, чтобы упростить межпотоковое взаимодействие: bridged-worker.js .

РЕДАКТИРОВАТЬ 2:

Приведенная выше ссылка относится к сущности, которую я создал. Кто-то позже превратил это в реальный репо .

дан человек
источник
11

Веб-работники работают в совершенно разных контекстах, как отдельные программы.

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

Это снова означает, что веб-работники должны быть инициализированы с кодом в исходной форме.

Спецификация от WHATWG говорит

Если источник результирующего абсолютного URL-адреса не совпадает с источником сценария ввода, то генерируется исключение SECURITY_ERR.

Таким образом, сценарии должны быть внешними файлами по той же схеме, что и исходная страница: вы не можете загрузить сценарий из data: URL или javascript: URL, а страница https: не может запустить рабочих, использующих сценарии с http: URL.

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

Шон Кинси
источник
6

лучше читать способ для встроенного работника ..

    var worker_fn = function(e) 
    {
        self.postMessage('msg from worker');            
    };

    var blob = new Blob(["onmessage ="+worker_fn.toString()], { type: "text/javascript" });

    var worker = new Worker(window.URL.createObjectURL(blob));
    worker.onmessage = function(e) 
    {
       alert(e.data);
    };
    worker.postMessage("start"); 
Крис Тобба
источник
Я сделал то, что я создал функцию со всем рабочим кодом, передал эту функцию toString(), извлек тело и затем поместил это в BLOB-объект. Посмотрите на последний ответ, у меня есть пример
Фернандо Карвахаль
5

Взять ответ Адрии и поместить его в копируемую функцию, которая работает с текущими Chrome и FF, но не с IE10 (работник из blob вызывает ошибку безопасности ).

var newWorker = function (funcObj) {
    // Build a worker from an anonymous function body
    var blobURL = URL.createObjectURL(new Blob(
        ['(', funcObj.toString(), ')()'],
        {type: 'application/javascript'}
     ));

    var worker = new Worker(blobURL);

    // Won't be needing this anymore
    URL.revokeObjectURL(blobURL);

    return worker;
}

И вот рабочий пример http://jsfiddle.net/ubershmekel/YYzvr/

ubershmekel
источник
5

Недавний ответ (2018)

Вы можете использовать Гринлет :

Переместите асинхронную функцию в ее собственный поток. Упрощенная однофункциональная версия Workerize .

Пример:

import greenlet from 'greenlet'

const getName = greenlet(async username => {
  const url = `https://api.github.com/users/${username}`
  const res = await fetch(url)
  const profile = await res.json()
  return profile.name
})

console.log(await getName('developit'))
GG.
источник
3

В зависимости от вашего варианта использования вы можете использовать что-то вроде

task.js Упрощенный интерфейс для запуска кода с интенсивным использованием процессора на всех ядрах (node.js и web)

Примером будет

function blocking (exampleArgument) {
    // block thread
}

// turn blocking pure function into a worker task
const blockingAsync = task.wrap(blocking);

// run task on a autoscaling worker pool
blockingAsync('exampleArgumentValue').then(result => {
    // do something with result
});
Чад Скира
источник
2

Взгляните на плагин vkThread. С помощью плагина htis вы можете взять любую функцию в своем основном коде и выполнить ее в потоке (веб-работник). Таким образом, вам не нужно создавать специальный «файл веб-работника».

http://www.eslinstructor.net/vkthread/

--Vadim

vadimk
источник
1

Вы можете использовать веб-работников в одном и том же JavaScript, используя встроенных веб-работников.

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

Освоение в веб-мастерах

kirankumar
источник
1

Я думаю, что лучший способ сделать это - использовать объект Blob, ниже вы можете увидеть простой пример.

// create a Blob object with a worker code
var blob = new Blob(["onmessage = function(e) { postMessage('msg from worker'); }"]);

// Obtain a blob URL reference to our worker 'file'.
var blobURL = window.URL.createObjectURL(blob);

// create a Worker
var worker = new Worker(blobURL);
worker.onmessage = function(e) {
  console.log(e.data);
};
worker.postMessage("Send some Data"); 
Мигель В.
источник
1

Попробуйте использовать jThread. https://github.com/cheprasov/jThread

// You can use simple calling like this
jThread(
    function(arr){
        //... some code for Worker
        return arr;
    }
    ,function(arr){
        //... done code
    }
)( [1,2,3,4,5,6,7] ); // some params
Александр Чепрасов
источник
1

здесь консоль:

var worker=new Worker(window.URL.createObjectURL(new Blob([function(){
  //Long-running work here
  postMessage('done');
}.toString().split('\n').slice(1,-1).join('\n')],{type:'text/javascript'})));

worker.addEventListener('message',function(event){
  console.log(event.data);
});
59naga
источник
1

https://developer.mozilla.org/es/docs/Web/Guide/Performance/Using_web_workers

    // Syntax: asyncEval(code[, listener])

var asyncEval = (function () {

  var aListeners = [], oParser = new Worker("data:text/javascript;charset=US-ASCII,onmessage%20%3D%20function%20%28oEvent%29%20%7B%0A%09postMessage%28%7B%0A%09%09%22id%22%3A%20oEvent.data.id%2C%0A%09%09%22evaluated%22%3A%20eval%28oEvent.data.code%29%0A%09%7D%29%3B%0A%7D");

  oParser.onmessage = function (oEvent) {
    if (aListeners[oEvent.data.id]) { aListeners[oEvent.data.id](oEvent.data.evaluated); }
    delete aListeners[oEvent.data.id];
  };


  return function (sCode, fListener) {
    aListeners.push(fListener || null);
    oParser.postMessage({
      "id": aListeners.length - 1,
      "code": sCode
    });
  };

})();
hamboy75
источник
1

Используйте мой крошечный плагин https://github.com/zevero/worker-create

var worker_url = Worker.createURL(function(e){
  self.postMessage('Example post from Worker'); //your code here
});
var worker = new Worker(worker_url);
ЗЕВЕРО
источник
1

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

const workerScript = `
self.addEventListener('message', function(e) {
  var data = e.data;
  console.log('worker recieved: ',data);
  self.postMessage('worker added! :'+ addOne(data.value));
  self.close();//kills the worker
}, false);
`;

Вот суть этого подхода .

Обратите внимание, что мы можем добавить любые дополнительные зависимости функций, которые мы хотим, в работника, просто собрав их в массив и запустив .toString для каждого из них, чтобы также сократить их до строк (должно работать, пока они являются объявлениями функций) и затем просто добавив это к строке сценария. Таким образом, нам не нужно импортировать скрипты, которые мы, возможно, уже включили в область написанного нами кода.

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

Dtipson
источник
1

Это всего лишь дополнение к вышеупомянутому - у меня есть хорошие шаблоны для тестирования веб-работников в jsFiddle. Вместо Blob он использует jsFiddles ?jsapi:

function workerFN() {
  self.onmessage = function(e) {
    switch(e.data.name) {
      case "" : 
      break;
      default:
        console.error("Unknown message:", e.data.name);
    }
  }
}
// This is a trick to generate real worker script that is loaded from server
var url = "/echo/js/?js="+encodeURIComponent("("+workerFN.toString()+")()");
var worker = new Worker(url);
worker.addEventListener("message", function(e) {
  switch(e.data.name) {
    case "" : 
    break;
    default:
      console.error("Unknown message:", e.data.name);
  }
})

Доступны обычные веб-рабочие и общие рабочие шаблоны.

Томаш Зато - Восстановить Монику
источник
1

Я обнаружил, что CodePen в настоящее время не выделяет синтаксис встроенных <script>тегов, которых нет type="text/javascript"(или которые не имеют атрибута типа).

Поэтому я разработал похожее, но немного отличающееся решение, используя помеченные блоки with break, и это единственный способ, с помощью которого вы можете освободить <script>тег из тега, не создавая функцию-обертку (которая не нужна).

<!DOCTYPE html>
<script id="worker1">
  worker: { // Labeled block wrapper

    if (typeof window === 'object') break worker; // Bail if we're not a Worker

    self.onmessage = function(e) {
      self.postMessage('msg from worker');
    };
    // Rest of your worker code goes here.
  }
</script>
<script>
  var blob = new Blob([
    document.querySelector('#worker1').textContent
  ], { type: "text/javascript" })

  // Note: window.webkitURL.createObjectURL() in Chrome 10+.
  var worker = new Worker(window.URL.createObjectURL(blob));
  worker.onmessage = function(e) {
    console.log("Received: " + e.data);
  }
  worker.postMessage("hello"); // Start the worker.
</script>

msanford
источник
1

Простая обещанная версия, Function#callAsWorkerкоторая принимает thisArg и аргументы (точно так же, как call) и возвращает обещание:

Function.prototype.callAsWorker = function (...args) {
    return new Promise( (resolve, reject) => {
        const code = `self.onmessage = e => self.postMessage((${this.toString()}).call(...e.data));`,
            blob = new Blob([code], { type: "text/javascript" }),
            worker = new Worker(window.URL.createObjectURL(blob));
        worker.onmessage = e => (resolve(e.data), worker.terminate());
        worker.onerror = e => (reject(e.message), worker.terminate());
        worker.postMessage(args);
    });
}

// Demo
function add(...nums) {
    return nums.reduce( (a,b) => a+b );
}
// Let the worker execute the above function, with the specified arguments
add.callAsWorker(null, 1, 2, 3).then(function (result) {
    console.log('result: ', result);
});

trincot
источник
Вы должны добавить close()метод, чтобы закрыть ваш хук жизни веб-работника. developer.mozilla.org/en-US/docs/Web/API/WorkerGlobalScope/…
Шахар ド ー ン Леви
@ Shahar ド ー ン Леви, closeфункция устарела. Тем не менее, рабочие могут быть уволены . Я добавил это сейчас.
Trincot
0

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

const worker = createWorker();

createWorker() {
    const scriptContent = getWorkerScript();
    const blob = new Blob([
        scriptContent,
    ], {
        type: "text/javascipt"
    });
    const worker = new Worker(window.URL.createObjectURL(blob));
    return worker;
}

getWorkerScript() {
    const script = {
        onmessage: function (e) {
            console.log(e);
            let result = "Hello " + e.data
            postMessage(result);
        }
    };
    let content = "";
    for (let prop in script){
        content += `${prop}=${script[prop].toString()}`;
    }
    return content;
}

Z.JC
источник
Посмотрите на мой ответ , я только что сделал это, но я написал целый класс для того, чтобы абстрагироваться от того, как передавать обратные вызовы
Фернандо Карвахал
0

Да, это возможно, я сделал это, используя файлы Blob и передавая обратный вызов

Я покажу вам, что делает класс, который я написал, и как он управляет выполнением обратных вызовов в фоновом режиме.

Сначала вы создаете экземпляр GenericWebWorkerс любыми данными, которые вы хотите передать обратному вызову, который будет выполняться в Web Worker, который включает в себя функции, которые вы хотите использовать, в данном случае число, дату и функцию с именемblocker

var worker = new GenericWebWorker(100, new Date(), blocker)

Эта блокирующая функция будет выполнять бесконечное время в течение n миллисекунд

function blocker (ms) {
    var now = new Date().getTime();
    while(true) {
        if (new Date().getTime() > now +ms)
            return;
    }   
}

а затем вы используете это так

worker.exec((num, date, fnBlocker) => {
    /*Everithing here does not block the main thread
      and this callback has access to the number, date and the blocker */
    fnBlocker(10000) //All of this run in backgrownd
    return num*10

}).then(d => console.log(d)) //Print 1000

Теперь пришло время увидеть магию в примере ниже

/*https://github.com/fercarvo/GenericWebWorker*/
class GenericWebWorker {
    constructor(...ags) {
        this.args = ags.map(a => (typeof a == 'function') ? {type:'fn', fn:a.toString()} : a)
    }

    async exec(cb) {
        var wk_string = this.worker.toString();
        wk_string = wk_string.substring(wk_string.indexOf('{') + 1, wk_string.lastIndexOf('}'));            
        var wk_link = window.URL.createObjectURL( new Blob([ wk_string ]) );
        var wk = new Worker(wk_link);

        wk.postMessage({ callback: cb.toString(), args: this.args });
 
        var resultado = await new Promise((next, error) => {
            wk.onmessage = e => (e.data && e.data.error) ? error(e.data.error) : next(e.data);
            wk.onerror = e => error(e.message);
        })

        wk.terminate(); window.URL.revokeObjectURL(wk_link);
        return resultado
    }

    async parallel(arr, cb) {
        var res = [...arr].map(it => new GenericWebWorker(it, ...this.args).exec(cb))
        var all = await Promise.all(res)
        return all
    }

    worker() {
        onmessage = async function (e) {
            try {                
                var cb = new Function(`return ${e.data.callback}`)();
                var args = e.data.args.map(p => (p.type == 'fn') ? new Function(`return ${p.fn}`)() : p);

                try {
                    var result = await cb.apply(this, args); //If it is a promise or async function
                    return postMessage(result)

                } catch (e) { throw new Error(`CallbackError: ${e}`) }
            } catch (e) { postMessage({error: e.message}) }
        }
    }
}


function blocker (ms) {
    var now = new Date().getTime();
    while(true) {
        if (new Date().getTime() > now +ms)
            return;
    }   
}

setInterval(()=> console.log("Not blocked " + Math.random()), 1000)

console.log("\n\nstarting blocking code in Worker\n\n")

var worker = new GenericWebWorker(100, new Date(), blocker)

worker.exec((num, date, fnBlocker) => {
    fnBlocker(7000) //All of this run in backgrownd
    return num*10    
})
.then(d => console.log(`\n\nEnd of blocking code: result ${d}\n\n`)) //Print 1000

Фернандо Карвахаль
источник
0

Вы можете поместить содержимое вашего файла worker.js внутри обратных галочек (что допускает многострочную строковую константу) и создать работника из большого двоичного объекта следующим образом:

var workerScript = `
    self.onmessage = function(e) {
        self.postMessage('message from worker');
    };
    // rest of worker code goes here
`;

var worker =
    new Worker(createObjectURL(new Blob([workerScript], { type: "text/javascript" })));

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

samgak
источник
0

Другое решение - просто обернуть Worker в функцию, а затем создать BLOB-объект, вызывающий функцию следующим образом:

     function workerCode() {
        self.onmessage = function (e) {
          console.log("Got message from parent", e.data);
        };
        setTimeout(() => {
          self.postMessage("Message From Worker");
        }, 2000);
      }

      let blob = new Blob([
        "(" + workerCode.toString() + ")()"
      ], {type: "text/javascript"});

      // Note: window.webkitURL.createObjectURL() in Chrome 10+.
      let worker = new Worker(window.URL.createObjectURL(blob));
      worker.onmessage = function (e) {
        console.log("Received: " + e.data);
      };
      worker.postMessage("hello"); // Start the worker.
Шломи Шварц
источник
-1

Однострочник для запуска функций у рабочих:

const FunctionalWorker = fn => new Worker(window.URL.createObjectURL(new Blob(["(" + workerCode.toString() + ")()"], {type: "text/javascript"})));

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

let fn = FunctionalWorker(() => {
    self.postMessage("hi");
});
fn.onmessage = msg => {
    console.log(msg);
};
ifeelbadformyoldquestions
источник