пятница, 1 апреля 2016 г.

JavaScript Observer, Iterator, Generator, Promise and Observable Patterns

Итератор перебирает подряд набор значений и по очереди выдает элементы iterator.next(), как array.shift()
Генератор по очереди принимает элементы generator.next(1), как array.push(1)
Промис по очереди выполняет цепочку зависимых друг от друга асинхронных событий
Обозреватель добавляет к объекту к объекту функцию события и вызывает её, когда это событие генерируется в коде.
Обозреваемое добавляет к последовательности элементов или асинхронных событий итератор, который по порядку перебирает элементы и обозреватель, который вызывает для перебираемых элементов функции события.
Декоратор обертывает одну функцию в другую функцию decorate(base, wrapper).
Фабрика конструирует объекты по заданным параметрам.
Фасад скрывает сложную функциональность, выдавая вместо неё простую функцию.
Прокси оборачивает функцию в декоратор.

Observer Pattern

// Observer

function Observer () {
    this.events = {};
}

Observer.prototype = {
      addEvent: function (name, func) {if (this.events[name]) {this.events[name].push(func);} else {this.events[name] = [func];}}
    , removeEvent: function (name) {if (this.events[name]) {delete this.events[name];}}
    , dispatchEvent: function (name, args) {if (this.events[name]) {this.events[name].forEach(function (func) {func.apply(null, args);});}}
};

// Observer Test

var observer = new Observer();
observer.addEvent('one', function (a) {console.log('1: ' + a);});
observer.addEvent('one', function (b) {console.log('2: ' + b);});
observer.addEvent('two', function (c) {console.log('3: ' + c);});
observer.dispatchEvent('one', ['one']);
observer.dispatchEvent('two', ['two']);
observer.removeEvent('one');
observer.dispatchEvent('one', ['one']);

Iterator Pattern

// Iterator

function Iterator (items) {
    var i = 0;
    return {
        next: function () {
            var done = (i >= items.length)
                , value = !done ? items[i++] : undefined;
            return {value: value, done: done};
        }
    };
}

// Iterator Test

var iterator = new Iterator([1, 2, 3]);
console.log(iterator.next()); // {value: 1, done: false}
console.log(iterator.next()); // {value: 2, done: false}
console.log(iterator.next()); // {value: 3, done: false}
console.log(iterator.next()); // {value: undefined, done: true}

Generator Pattern

// Generator

function Generator (items) {
    var i = 0;
    return {
        next: function (value) {
            items = items.slice();
            var done = (i >= items.length)
            if (!done) {items[i] = value; i++;}
            return {value: items, done: done};
        }
    };
}

// Generator Test

var generator = new Generator(new Array(3));
console.log(generator.next(1)); // {value: [1, undefined, undefined], done: false}
console.log(generator.next(2)); // {value: [1, 2, undefined], done: false}
console.log(generator.next(3)); // {value: [1, 2, 3], done: false}
console.log(generator.next(4)); // {value: [1, 2, 3], done: true}

Promise Pattern

// Promise

function Promise () {
    this.promises = [];
}

Promise.prototype = {
      then: function (success, error) {
        if (Object.prototype.toString.call(success) !== '[object Function]') {success = function () {};}
        if (Object.prototype.toString.call(error) !== '[object Function]') {error = function () {};}
        this.promises.push({success: success, error: error});
        return this;
      }
    , success: function () {
        if (this.promises.length) {
            var args = Array.prototype.slice.call(arguments);
            args.unshift(this);
            this.promises.shift().success.apply(null, args);
        }
      }
    , error: function () {
        if (this.promises.length) {
            this.promises.shift().error.apply(null, arguments);
        }
      }
};

// Promise Test

function timeout1 (seconds, error) {
    var promise = new Promise();
    setTimeout(function () {
        if (seconds > 2) {
            promise.error(seconds);
        } else {
            console.log('Success 1: ' + seconds);
            promise.success(seconds);
        }
    }, seconds * 1000);
    return promise;
}

function timeout2 (promise, seconds) {
    setTimeout(function () {
        if (seconds > 2) {
            promise.error(seconds);
        } else {
            console.log('Success 2: ' + seconds);
            promise.success(seconds);
        }
    }, seconds * 1000);
    return promise;
}

function timeout3 (promise, seconds) {
    setTimeout(function () {
        if (seconds > 1) {
            promise.error(seconds);
        } else {
            console.log('Success 3: ' + seconds);
            promise.success(seconds);
        }
    }, seconds * 1000);
    return promise;
}

function done (promise, seconds) {
    console.log('Done: ' + seconds);
}


function timeout1error (seconds) {console.log('Error 1 seconds: ' + seconds);}
function timeout2error (seconds) {console.log('Error 2 seconds: ' + seconds);}
function timeout3error (seconds) {console.log('Error 3 seconds: ' + seconds);}

timeout1(2).then(timeout2, timeout1error).then(timeout3, timeout2error).then(done, timeout3error);

Observable Pattern = Interator + Promise for async + Observer

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

// Observable

var Observable = {
    from: function (items) {
        var iterator = new Iterator(items);
        return {subscribe: function (next, error, complete) {
            var item = iterator.next();
            while (!item.done) {
                next(item.value);
                item = iterator.next();
            }
            complete();
        }};
    }
};

// Observable Test

Observable.from(['Adria', 'Jen', 'Sergi']).subscribe(
      function onNext (value) {console.log('Next: ' + value);}
    , function onError (error) {console.log('Error: ' + error);}
    , function onCompleted () {console.log('Completed');}
);

вторник, 29 марта 2016 г.

Error handling in JavaScript

Modern Chrome and Opera fully support the HTML 5 draft spec for ErrorEvent and window.onerror. In both of these browsers you can either use window.onerror, or bind to the 'error' event properly:

// Only Chrome & Opera pass the error object.
window.onerror = function (message, file, line, col, error) {
    console.log(message, "from", error.stack);
    // You can send data to your server
    // sendError(data);
};

// Only Chrome & Opera have an error attribute on the event.
window.addEventListener("error", function (e) {
    console.log(e.error.message, "from", e.error.stack);
    // You can send data to your server
    // sendError(data);
})

Unfortunately Firefox, Safari and IE are still around and we have to support them too. As the stacktrace is not available in window.onerror we have to do a little bit more work.

It turns out that the only thing we can do to get stacktraces from errors is to wrap all of our code in a try{ }catch(e){ } block and then look at e.stack. We can make the process somewhat easier with a function called wrap that takes a function and returns a new function with good error handling.

function wrap(func) {
    // Ensure we only wrap the function once.
    if (!func._wrapped) {
        func._wrapped = function () {
            try{
                func.apply(this, arguments);
            } catch(e) {
                console.log(e.message, "from", e.stack);
                // You can send data to your server
                // sendError(data);
                throw e;
            }
        }
    }
    return func._wrapped;
};

This works. Any function that you wrap manually will have good error handling, but it turns out that we can actually do it for you automatically in most cases.

By changing the global definition of addEventListener so that it automatically wraps the callback we can automatically insert try{ }catch(e){ } around most code. This lets existing code continue to work, but adds high-quality exception tracking.

var addEventListener = window.EventTarget.prototype.addEventListener;
window.EventTarget.prototype.addEventListener = function (event, callback, bubble) {
    addEventListener.call(this, event, wrap(callback), bubble);
}

We also need to make sure that removeEventListener keeps working. At the moment it won't because the argument to addEventListener is changed. Again we only need to fix this on the prototype object:

var removeEventListener = window.EventTarget.prototype.removeEventListener;
window.EventTarget.prototype.removeEventListener = function (event, callback, bubble) {
    removeEventListener.call(this, event, callback._wrapped || callback, bubble);
}

Transmit error data to your backend

You can send error data using image tag as follows

function sendError(data) {
    var img = newImage(),
        src = 'http://yourserver.com/jserror&data=' + encodeURIComponent(JSON.stringify(data));

    img.crossOrigin = 'anonymous';
    img.onload = function success() {
        console.log('success', data);
    };
    img.onerror = img.onabort = function failure() {
        console.error('failure', data);
    };
    img.src = src;
}

среда, 16 марта 2016 г.

Support ECMAScript 5, 6, 7 Shims and Shams

ECMAScript 5 (есть в es5-shim)

Object
- Object.keys

Array
- Array.isArray
- [].indexOf
- [].lastIndexOf
- [].every
- [].some
- [].forEach
- [].map
- [].filter
- [].reduce
- [].reduceRight

String
- "".trim
- "".split - crossbrowser (нет в es5-shim)

Function
- (function(){}).bind

Date
- Date.now
- (new Date()).toISOString
- (new Date()).toJSON
- (new Date()).parse -> return NaN for invalid dates

JSON (нет в es5-shim)
- JSON.parse
- JSON.stringify

ECMAScript 6 (есть в es6-shim)

Object
- Object.assign
- Object.is
Только, если есть поддержка ES5
- Object.getPrototypeOf
- Object.getOwnPropertyDescriptor
- Object.getOwnPropertyNames
- Object.seal
- Object.freeze
- Object.preventExtensions
- Object.isSealed
- Object.isExtensible
- Object.keys

String
- String.raw
- String.fromCodePoint
- "".codePointAt
- "".repeat
- "".startsWith
- "".endsWith
- "".includes

Number
- Number.isFinite
- Number.isInteger
- Number.isSafeInteger
- Number.isNaN
- Number.EPSILON
- Number.MIN_SAFE_INTEGER
- Number.MAX_SAFE_INTEGER
- Number('0o1')
- Number('0b1')

Array
- Array.from
- Array.of
- [].copyWithin
- [].find
- [].fineIndex
- [].fill
- [].keys
- [].values
- [].entries

RegExp
- (//).flags

Math
- Math.clz32
- Math.imul
- Math.sign
- Math.log10
- Math.log2
- Math.log1p
- Math.expm1
- Math.cosh
- Math.sinh
- Math.acosh
- Math.asinh
- Math.atanh
- Math.trunc
- Math.fround
- Math.cbrt
- Math.hypot

Map
Set
Reflect
Promise

ECMAScript 7 (есть в es7-shim)

Object
- Object.values
- Object.entries
- Object.getOwnPropertyDescriptors

Array
- [].includes

String
- "".padStart
- "".padEnd
- "".trimLeft
- "".trimRight
- "".at

Map
- (new Map()).toJSON

Set
- (new Set()).toJSON

Последовательность включения новых функций в проект:
- es5-shim.min.js
- es5-sham.min.js
- json3.min.js
- es6-shim.min.js
- es6-sham.min.js
- es7-shim.min.js
...other-libs.js...

понедельник, 14 марта 2016 г.

JavaScript Decorator

function decorate (baseFunction, decoratorFunction) {
    return function () {decoratorFunction(baseFunction, arguments);};
}

function a (a) {
    console.log(a);
    return 'OK';
}

function b (baseFunction, baseFunctionArguments) {
    console.log('start');
    baseFunction.apply(null, baseFunctionArguments);
    console.log('b');
    console.log('stop');
}

a = decorate(a, b);

a('a');

function trace (baseFunction, baseFunctionArguments) {
    var result = baseFunction.apply(null, baseFunctionArguments);
    console.log('TRACE: ' + baseFunction.name + ' (' + Array.prototype.slice.call(baseFunctionArguments).join(', ') + ') => ' + result);
    return result;
}

var c = decorate(a, trace);

c('a', 'b', 1, 2, {});

среда, 24 февраля 2016 г.

TypeScript Playground

Файл index.html

<!DOCTYPE html>
<html>
<head>
    <style>
        body {
            font-family: Verdana;
            font-size: 62.5%;
        }
        table {
            width: 100%;
        }
        table tr td {
            width: 50%;
            vertical-align: top;
            border: 1px solid black;
        }
        textarea {
            width: 100%;
            height: 500px;
            margin: 0;
            padding: 5px;
            border: none;
            resize: none;
            -webkit-box-sizing: border-box;
            -moz-box-sizing: border-box;
            box-sizing: border-box;
            outline: 0;
            font-size: 2.5em;
        }
        div {
            height: 500px;
            margin: 0;
            padding: 5px;
            border: none;
            font-size: 2.5em;
        }
        pre {
            margin: 0;
            padding: 0;
        }
    </style>
</head>
<body>
    <table>
        <tr>
            <td><textarea id="source"></textarea></td>
            <td><div><pre id="result"></pre></div></td>
        </tr>
    </table>
    <script type="text/javascript" src="js/typescriptServices.js"></script>
    <script type="text/javascript" src="js/transpiler.js"></script>
</body>
</html>

Файл typescriptService.js

https://rawgit.com/Microsoft/TypeScript/master/lib/typescriptServices.js

Файл transpiler.js

;(function(){
    document.getElementById('source').addEventListener('keyup', function(){
        document.getElementById('result').innerHTML = ts.transpile(document.getElementById('source').value);
    }, false);
})();

Упрощенный код использования TypeScript в браузере

Файл index.html

<!DOCTYPE html>
<html>
<head>
    <script type="text/typescript">
        setTimeout(()=>console.log('hello'));
    </script>
    <script type="text/javascript" src="js/typescriptServices.js"></script>
    <script type="text/javascript" src="js/transpiler.js"></script>
</head>
<body>
</body>
</html>

Файл typescriptService.js

https://rawgit.com/Microsoft/TypeScript/master/lib/typescriptServices.js

Файл transpiler.js

;(function(){
    var scripts = document.getElementsByTagName('script')
        , script;
    for (var i = 0, len = scripts.length; i < len; i++) {
        if (scripts[i].type === 'text/typescript') {
            script = document.createElement('script');
            script.type = 'text/javascript';
            script.innerHTML = '// Compiled TypeScript:\n\n' + ts.transpile(scripts[i].innerHTML);
            document.getElementsByTagName('head')[0].appendChild(script);
        }
    }
})();

How to compile TypeScript in the browser

Add the following lines at the bottom of your page:

<script src="https://rawgit.com/Microsoft/TypeScript/master/lib/typescriptServices.js"></script>
<script src="https://rawgit.com/basarat/typescript-script/master/transpiler.js"></script>

And then you can use script tags that load .ts files or even have typescript inline:

<script type="text/typescript" src="script.ts"></script>
<script type="text/typescript">
    setTimeout(()=>console.log('hello'));
</script>

Example

index.html source:

<!DOCTYPE html>
<html>
<head>
    <link rel="stylesheet" href="style.css">
   
    <script type="text/typescript" src="script.ts"></script>
    <script type="text/typescript">
        setTimeout(()=>console.log('hello'));
    </script>

    <script src="https://rawgit.com/Microsoft/TypeScript/master/lib/typescriptServices.js"></script>
    <script src="https://rawgit.com/basarat/typescript-script/master/transpiler.js"></script>
</head>
<body>
    <h1>Hello</h1>
</body>
</html>

transpiler.js source:

// BASED on https://github.com/niutech/typescript-compile but using 1.5 transpile function

(function () {
    //Keep track of the number of scripts to be pulled, and fire the compiler
    //after the number of loaded reaches the total
    var scripts = {
        total: 0, //total number of scripts to be loaded
        loaded: 0, //current number of loaded scripts
        data: [], //file data
        name: [] //file name
    };

    //Function loads each script and pushes its content into scripts.data
    var load = function (url) {
        var xhr = window.ActiveXObject ? new window.ActiveXObject('Microsoft.XMLHTTP') : new window.XMLHttpRequest();;
        xhr.open('GET', url, true);
        if ('overrideMimeType' in xhr) xhr.overrideMimeType('text/plain');
        xhr.onreadystatechange = function () {
            if (xhr.readyState !== 4) return;
            if (xhr.status === 0 || xhr.status === 200) {
                scripts.loaded++;
                scripts.data.push(xhr.responseText);
                scripts.name.push(url);
                if (scripts.loaded === scripts.total) compile();
                return xhr.responseText;
            } else {
                console.log('Could not load ' + url);
            } //end if
        }; //end xhr.onreadystatechange()
        return xhr.send(null);
    };

    //Compiles each of the scripts found within scripts.data
    var compile = function () {
        if (scripts.data.length == 0 || scripts.data.length != scripts.name.length) return; //no reason to compile when there are no scripts
        var elem, source = '',
            body = document.getElementsByTagName('body')[0];
        scripts.total = 0; //clear the 'queue' incase the xhr response was super quick and happened before the initializer finished
        var hashCode = function (s) {
            var hsh = 0,
                chr, i;
            if (s.length == 0) {
                return hsh;
            }
            for (i = 0; i < s.length; i++) {
                chr = s.charCodeAt(i);
                hsh = (hsh << 5) - hsh + chr;
                hsh = hsh & hsh; //Convert to 32bit integer
            }
            return hsh;
        };
        if (window.sessionStorage && sessionStorage.getItem('typescript' + hashCode(scripts.data.join('')))) {
            source = sessionStorage.getItem('typescript' + hashCode(scripts.data.join('')));
        } else {
            (function () {
                var filename;
                for (num = 0; num < scripts.data.length; num++) {
                    filename = scripts.name[num] = scripts.name[num].slice(scripts.name[num].lastIndexOf('/') + 1);
                    var src = scripts.data[num];
                    source += ts.transpile(src);
                }
            })();
        }
        elem = document.createElement('script');
        elem.type = 'text/javascript';
        elem.innerHTML = '//Compiled TypeScript\n\n' + source;
        body.appendChild(elem);
    };

    (function () {
        //Polyfill for older browsers
        if (!window.console) window.console = {
            log: function () {}
        };
        var script = document.getElementsByTagName('script');
        var i, src = [];
        for (i = 0; i < script.length; i++) {
            if (script[i].type == 'text/typescript') {
                if (script[i].src) {
                    scripts.total++
                    load(script[i].src);
                } else {
                    scripts.data.push(script[i].innerHTML);
                    scripts.name.push('innerHTML'+scripts.total);
                    scripts.total++;
                    scripts.loaded++;
                }
            }
        }
        if (scripts.loaded === scripts.total) compile(); //only fires if all scripts are innerHTML, else this is fired on XHR response
    })();
})();