Node.js v0.10.17 Manual & Documentation


Table of Contents

ns.History#

В noscript для смены URL в адресной строке используется HTML5 History API, который не поддерживается в IE раньше 10.

Polyfill для IE#

В качестве полифилла можно использовать devote/HTML5-History-API. Скрипт предоставляет стандартизированное API и будет использовать смену хеш-фрагмента URL для навигации.

/notes/141 -> /#/notes/141

Кроме подключения самого скрипта на страницу нужно проделать небольшую работу:

  1. Организовать редирект до старта приложения:
// Тут может произойти смена URL и перезагрузка, поэтому какие-нибудь
// модели до редиректа запрашивать бессмысленно.
window.history.redirect();

ns.init();
  1. Переопределить вычисление текущего URL приложения:
var history = window.history;

if (history.emulate) {
    ns.page.getCurrentUrl = function() {
        return history.location.pathname + history.location.search;
    };
}

Инициализация приложения#

При использовании конфигурации по умолчанию вся инициализация сводится к вызову функции ns.init и запуску первого апдейта:

$(function() {
    ns.init();
    ns.page.go();
});

ns.init включает экшены, обрабатывает предварительно заданный роутинг и ищет в DOM ноду #app для использования ее в качестве контейнера для интерфейса. Вызов ns.page.go нужен для запуска первого глобального апдейта.

Конфигурация#

Базовый путь в URL#

До инициализации можно задать префиксный путь для всех ссылок. Это может пригодиться, когда ваше приложение находится не по корневому пути app.example.com, а, например, app.example.com/checkout:

ns.router.baseDir = '/checkout';

Заголовок страницы#

Noscript позволяет задавать заголовок страницы, зависящий от текущего URL, при переходах внутри приложения. Переопределите функцию ns.page.title:

ns.page.title = function(url) {
    if ('/settings' == url) {
        return 'App - Account Settings'
    }

    return 'App';
};

Примечание: При необходимости, для получения параметров страницы из полученного URL можно воспользоваться функцией ns.router:

ns.page.title = function(url) {
    var params = ns.router(url).params;
    // ...
};

URL запроса моделей#

По умолчанию фреймворк группирует запросы моделей, нужных для отрисовки интерфейса и запрашивает их по URL /models/ вне зависимости от ns.router.baseDir. Переопределите константу ns.request.URL для задания собственного пути:

ns.request.URL = '/models/v1/json/';

Дополнительные параметры при запросе моделей#

При необходимости пробросить дополнительные параметры при запросе моделей, добавьте их в объект ns.request.requestParams:

ns.request.requestParams.token = getAuthToken();
ns.request.requestParams.version = '0.1.1';

Это приведет к отправке запросов вида:

Request URL: http://example.com/models/?_m=todos

Query String Parameters:
  _m: todos

Form Data:
  category.0: home
  token: 6a5e516725c68c
  version: 0.1.1

Условная обработка ответа моделей#

Определение функции ns.request.canProcessResponse позволяет динамически заблокировать обработку ответа моделей, например, при несовпадении авторизации или рассинхронизации клиента с бекендом:

ns.request.canProcessResponse = function(response) {
    // На бекенде выехала новая версия, а текущий клиент засиделся.
    if (response.version != APP.version) {
        location.reload();
        return false;
    }

    return true;
};

Переопределение модуля Yate-шаблонов#

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

ns.Update.prototype.applyTemplate = function(tree, params, layout) {
    var module = 'main';

    if (params.context === 'setup') {
        module = 'setup';
    }

    return ns.renderString(tree, null, module);
};

Раскладка страницы (ns.Layout)#

Раскладка служит для декларативного описания структуры видов на странице. При определении раскладки указывается её id и декларация.

    ns.layout.define('main', {
        app: {
            view1: true,
            view2: {
                view21: true
            }
        }
    });

В примере создаётся раскладка страницы, состоящей из четырёх видов. Корневой вид app содержит в себе view1 и view2, а view2 содержит в себе view21.

Каждый узел декларации соответствует виду. Ключ указывает на класс вида, в значении содержится декларация вложенных видов.

Способы описания структуры видов

    ns.layout.define('main', {
        app: {

            // Для описания вида без вложенностей значение устанавливается в true
            view1: true,

            // Для описания одного вложенного вида в значении указывается его класс
            view2: 'view21',

            // Для описания статической структуры видов используется объект
            view3: {
                view31: true,
                view32: 'view321'
            },
            // Для описания динамической структуры видов узел объяляется боксом,
            // а вложенность задаётся функцией.
            // В params приходят параметры страницы.
            // Функция может вернуть любой из выше перечисленных форматов декларации
            // видов, или falsy, чтобы отменить добавление вида в страницу.
            view4@: function(params) {
                if (params.value1) {
                    return null;
                }
                if (params.value2) {
                    return 'view41';
                }
                if (params.value3) {
                    return {
                        'view42': {
                            'view421': true;
                        }
                    };
                }
            }
        }
    });

Кроме описания структуры видов раскладка так же позволяет декларировать специальные атрибуты видов.

Бокс#

Бокс - это специальный вид-контейнер. Он не имеет собственного html-содержимого и представлен в DOM только одним узлом, содержащим непосредственно в себе все вложенности. Бокс позволяет решать следующие задачи.

Кеширование экземпляров вида

    ns.layout.define('main', {
        app: {
            box@: 'view1'
        }
    });

В примере box будет содержать один вложенный вид view1. Если view1 зависит от параметров, то при изменении параметров предыдущие html-узлы будут скрываться, но оставаться в DOM-дереве, а новые - добавляться в box и показываться. При возврате к одному из предыдущих наборов параметров будет показан ранее сгенерированный соответствующий ему html-узел.

Создание динамической раскладки

    ns.layout.define('main', {
        app: {
            view1: true,
            view2@: function(params) {
                if (params.value1) {
                    return null;
                }
                if (params.value2) {
                    return 'view41';
                }
                if (params.value3) {
                    return {
                        'view42': 'view421',
                        'view43': 'view431'
                    };
                }
            }
        }
    });

В примере вид в зависимости от параметров может отсутствовать, содержать единственный вид view41, или содержать view42 и view43, содержащие в свою очередь соответственно view421 и view431. Для создания такой структуры view2 обязательно должен быть боксом. Обычный view, содержащий вложенные виды, при их исчезновении после обновления может работать некорректно.

Асинхронные виды#

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

    ns.layout.define('main', {
        app: {
            viewLight: true,
            viewHard&: true
        }
    });
    ns.View.define('viewLight', {
        models: ['modelLight']
    })
    ns.View.define('viewHard', {
        models: ['modelHard']
    })
    match .view[id="viewLight"] ns-view {
        // основное html-содержимое вида viewLight
    }


    match .view[id="viewHard"] ns-view-async {
        // html-содержимое заглушки для вида viewHard
    }
    match .view[id="viewHard"] ns-view {
        // основное html-содержимое вида viewHard
    }

Предположим, что modelLight запросить легко, а запрос modelHard требует заметного времени. При обновлении Updater запросит модели отдельно: сначала modelLight, а затем modelHard. Получив данные для modelLight, Updater отрисует страницу. Вид viewHard при этом отрисуется в виде заглушки (с использованием шаблона ns-view-async). После получения данных для modelHard Updater сделает ещё один такт обновления и вместо заглушки отрисует основное содержимое viewHard.

Наследование#

Один layout может наследовать от другого. Наследование реализуется следующим образом

// объявляет общий layout для всех страниц
ns.layout.define('common', {
    'app': {
        // каждая страница в проекте состоит из шапки, левой колонки и правой колонки
        'header-box@': {},
        'left-box@': {},
        'right-box@': {},
    }
});

// страница 1
// обратите внимание, что header-box@ не доопределяелся и берется как есть из common
ns.layout.define('posts', {
    // чтобы заново не описывать структуру, путь до видов указывается через пробел
    'app left-box@': {
        'navigation': {}
    },
    'app right-box@': {
        'posts': {}
    }
// последним параметром для ns.layout.define указывает, что layout наследуется от common
}, 'common');

// страница 2
ns.layout.define('profile', {
    'app header-box@': {
        'user-header': {}
    },
    'app left-box@': {
        'navigation': {}
    },
    'app right-box@': {
        'profile': {}
    }
}, 'common');

ns.Model#

Модель представляет собой данные. Она однозначно идентифицируется своим ключом, который строится во время инициализации. Разный ключ всегда означает разный экземпляр модели.

Декларация#

Определение новой модели происходит через статическую функцию ns.Model.define

ns.Model.define('modelName', modelDeclObject[, baseModel])

Объект-декларация состоит из следующих свойств.

ctor#

ctor - это функция-конструтор. Обратите внимание, что он вызывается самым первым, до инициализации самой модели, т.о. в конструкторе еще не доступны некоторые свойства.

Полностью готовый экземпляр бросает событие ns-model-init.

/**
 * @classdesc prj.mMyModel
 * @augments ns.Model
 */
ns.Model.define('my-model', {
    /**
     * @constructs prj.mMyModel
     */
    ctor: function() {
        this._state = 'initial';
        this.CONST = 100;
    }
});

events#

events - объект с декларацией подписок на события noscript.

Любая подписка имеет вид:

{
    "на что подписаться": "обработчик"
}

Обработчиком может быть название метода из прототипа или функция.

Пример:

{
    "my-custom-event": "onCustomEvent",
    "my-custom-show@show": "onCustomShow"
}

methods#

methods - объект с методами. По сути является прототипом объекта.

/**
 * @classdesc prj.mMyModel
 * @augments ns.Model
 */
ns.Model.define('my-model', {
    /** @lends prj.mMyModel.prototype */
    methods: {
        BAR: 100
        foo: function(){}
    }
});

params#

Параметры нужны для как для построения ключа, так и для запроса моделей с сервера.

ns.Model.define('my-model', {
    params: {
        //  Любое значение, кроме null расценивается как дефолтное значение этого параметра.
        'author-login': null,
        'album-id': null,

        //  Этим двум параметрам заданы дефолтные значения.
        'page': 0,
        'pageSize': 20
    }
});

Получение экземпляра модели#

  • ns.Model.get('modelName', params) - строит ключ из params и возвращает соответствующую модель. Если такого экземпляра нет, то он будет создан.
  • ns.Model.getValid('modelName', params) - тоже самое что и ns.Model.get. Только экземпляр еще проверяется на валидность. Если валидный экземпляр не найден, то возвращается null.

Работа с данными#

Методы для получения данных: - #getData() - возвращает весь объект данных модели. Этот метод можно переопределять для доп. обработки данных. Например, для коллекции этот метод собирает актуальные данных из всех элементов. - #get(jpath) - выбирает данные по jpath и приводит результат к упрощенному виду. Результат приведения зависит как от самих данных, так и от jpath. Поэтому при изменениях формат результата может меняться.

{
    "foo": "1",
    "bar": [
        { "id": 1 }
    ]
}
this.get('.foo') -> "1"
this.get('.bar.id') -> ["1"]
  • #select(jpath) - выбирает данные по jpath. В отличии от #get, не занимается приведением и всегда возвращает массив результатов выборки, т.о. формат результат остается стабильным при изменениях.
    {
     "foo": "1",
     "bar": [
         { "id": 1 }
     ]
    }
    this.get('.foo') -> ["1"]
    this.get('.bar.id') -> ["1"]

Методы для изменения данных: - #set(jpath, value) - изменяет данные по jpath. Поддерживаются только несложные jpath.

this.set('.foo', 2);
  • #setData(data) - устаналивает полностью новые данные. В частности, этот метод вызывается при получении данных с сервера.

Пре- и постобработка данных#

extractData#

Метод извлекает данные из ответа сервера. По умолчанию берется поле data из ответа. Если метод не возвращает данные, то считается, что модель загружена с ошибкой.

ns.Model.define('my-model', {
    methods: {
        extractData: function(serverResponse) {
            if (serverResponse) {
                return serverResponse.result;
            }
        }
    }
});

extractError#

Метода извлекает данные об ошибке сервера. По умолчанию берется поле error из ответа.

Метод вызывается, когда #extractData() не вернул данные.

ns.Model.define('my-model', {
    methods: {
        extractError: function(serverResponse) {
            if (serverResponse) {
                return serverResponse.error;
            }
        }
    }
});

hasDataChanged#

Этот метод может контроллировать изменились ли данные на самом деле, чтобы не вызывать лишних события и перерисовок. Аргументом метода являются новые данные, а старые можно получить способами описанными выше, например #getData. Должен вернуть boolean.

ns.Model.define('my-model', {
    methods: {
        hasDataChanged: function(newData) {
            var oldData = this.getData;
            // изменяем данные, только если изменилось поле id
            return oldData.id !== newData.id
        }
    }
});

preprocessData#

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

ns.Model.define('my-model', {
    methods: {
        _index: null,
        preprocessData: function(newData) {
            var that = this;
            // строим индекс для быстрого поиска
            newData.forEach(function(item) {
                that._index[item.id] = item;
            });
            return newData;
        }
    }
});

События#

  • ns-model-changed - модель изменилась. В аргументах приходит jpath, по которому было сделано изменение. Если он пустой, то изменилась вся модель (обычно методом #setData())
  • ns-model-changed<.jpath> - изменились данные по указанному jpath. В аргументах приходит jpath, по которому было сделано изменение. События кидаются иерархично, т.о. для .for.bar будет три события: ns-model-changed.foo.bar, ns-model-changed.foo, ns-model-changed
  • ns-model-destroyed - модель была инвалидированна и уничтожена.
  • ns-model-init - модель создана и проинициализованна
  • ns-model-touched - у модели изменилась версия. Такое событие будет как результатом изменения данных через #set или #setData, так и прямым вызовом метода #touch()

ns.ModelCollection#

ModelCollection - это коллеция (по сути, массив) ns.Model.

Может иметь собственные данные. Данные коллекции непосредственно не хранит, а собирает динамически из актуальных ns.Model.

Коллеция не может содержать одинаковых моделей.

ns.ModelCollection наследуется от ns.Model и добавляет к ней некоторые методы: - #clear() - очищает коллекцию - #insert(models[, index = last]) - добавляет models в коллекцию на позицию index. - #remove(models) - удаляет models из коллекции.

При добавлении элементов бросает событие ns-model-insert со списком новых моделей.

При удалении элементов бросает событие ns-model-remove со списком удаленных моделей.

Декларация#

Декларация отличается наличием поля split

ns.Model.define('my-model-collection', {
    split: {
        items: '.message',
        params: {
            'mid': '.mid'
        },
        model_id: 'message'
    }
});

split.items - jpath до элементов коллекции. После получения данных коллекции выберет элементы по этому jpath и сделает из каждого элемента модель. Это и будет коллекция. split.model_id - название модели, из которых будет состоять коллекции split.params - параметры для элементов коллекции

Если модель наполняется вручную, то split можно не указывать, а указать флаг isCollection === true.

Для таких колекций так же можно указать jpath, по которому будет лежать коллекция - jpathItems (по умолчанию, .items).

ns.Model.define('my-model-collection', {
    isCollection: true,
    jpathItems: '.files'
});

ns.Model.define('my-model-item', {
    params: {
        id: null
    }
});

var collection = ns.Model.get('my-model-collection');
var collectionItem1 = ns.Model.get('my-model-item', {id : 1}).setData({'foo': 'bar'});
var collectionItem2 = ns.Model.get('my-model-item', {id : 2}).setData({'foo': 'baz'});

// добавляем элементы в коллекцию
collection.insert(collectionItem1);
collection.insert(collectionItem2);

// т.к. указан jpathItems, то данные коллекции будут выглядет вот так
{
    "files": [
        {
            "foo": "bar"
        },
        {
            "foo": "baz"
        }
    ]
}

Переходы по страницам (ns.page)#

ns.page - специальный модуль для перехода по страницам внутри ns-приложения.

ns.page.go - главный метод. Разроваричивает адрес через ns.router, выбирает layout, запускает ns.Update и производит необходимые операции по смене урла в адресной строке и обновления названия (document.title) страницы. Метод возвращает промис от ns.Update, но иногда может вернуть отклоненный промис со статусами: - block - переход был заблокирован через ns.page.block

ns.page.title - точка расширения приложения. Позволяет задавать заголовки страниц.

Также модуль предоставляет полезные данные: - ns.page.current - текущие параметры страницы - ns.page.current.page - название текущего layout - ns.page.current.params - текущие параметры - ns.page.currentUrl - адрес текущей страницы

ns.page.block#

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

ns.View.define('my-view', {
    events: {
        'ns-view-show': function() {
            // после показа вида, добавляем функцию блокировки
            ns.page.block.add( this.checkChanges.bind(this) );
        },
        'ns-view-hide': function() {
            // после скрытия вида, очищаем функции
            ns.page.block.clear();
        }
    },
    methods: {
        /**
         * @param {string} url ссылка, по которой выполняется переход
         */
        checkChanges: function(url) {
            if (this.hasUnsavedChanges()) {
                // здесь можно показать какое-то сообщение

                // функция блокировки должна вернуть false, если переход нельзя осуществить
                return false;
            }

            return true;
        }
    }
 });

ns.page.history#

Этот модуль хранит историю приложения. Он нужен, т.к. в History API нельзя получить произвольное состояние на N шагов назад.

Имеет два метода: - ns.page.history.back - переход "назад". Этот метод не аналогичен кнопке "Назад" в браузере. Так, при отсутствии истории, этот метод перейдет на дефолтную страницу приложения (ns.page.getDefaultUrl), а не выйдет из него. - ns.page.history.getPrevious(n) - возвращает урл N страниц назад. 0 - предыдующая страница.

ns.router#

Умеет: - получать из урла - id страницы (layout) и параметры params - генерировать url-ы по id страницы и параметрам params

API#

ns.router.baseDir: {string}#

Базовая часть урла (если приложение располагается не в корне сайта.

ns.router(url): { page:string, params:{object} }#

Выполняет роутинг: вычисляет по url какая это страница page (это id layout-а) и вытаскивает параметры из урла. Если в урле были GET параметры - они подклеиваются в итоговый набор params.

Когда выполняется роутинг выполняются: - (опционально) redirect-ы (получаем новый урл после redirect-а и ещё раз выполняем роутинг) - (опционально) rewrite (текущий урл заменяется на прописанный в rewrite-е, параметры подклеиваются в конце как GET параметры) - роутинг (ищем первый подходящий шаблон урла, подробнее см. ns.router.routes. Если не удалось заматчится - считаем, что получили страницу с not-found) - (опционально) rewrite параметров (при желании, меняем что-то в полученном объекте с параметрами params).

ns.router.url(url): { string }#

Генерация урла, когда урл известен и нужно только дописать базовую часть. Странный метод, лучше использовать ns.router.generateUrl

ns.router.generateUrl(id, params): {string}#

Генерация урла по id страницы (layout) и по набору параметров. Это операция, обратная той, которую делает ns.router. Умеет разворачивать rewrite-ы (после генерации урла проверяет, есть ли rewrite правила для полученного урла и выполняет их в обратную сторону). В случае неуспеха - кидает ошибку.

ns.router.routes: {object}#

Это объект, в котором нужно указать все урлы, rewrite-ы и redirect-ы. Кроме этого поддерживается rewrite параметров.

redirect - прописываются редиректы. Можно указать шаблон урла, который надо заматчить и можно указать функцию, которая вычисляет, куда делается редирект.

rewriteUrl - тут указаны урлы (не шаблоны урлов) и можно указать rewrite для конкретного урла на другой конкретный урл.

route - тут прописано соответствие шаблона урла - странице (layout-у). Матчинг урла выполняется сверху вниз. А значит у урла, который выше - больше приоритет. Отсюда правило - более общие шаблоны урлов указывать ниже. Матчинг выполняется до первого успешного сопоставления. Одной и тоже странице может соответствовать несколько шаблонов урлов.

rewriteParams - для страницы (layout-а) можно указать функцию, в которой произвольным образом поменять params.

ns.router.routes = {
    redirect: {
        '/': '/inbox',
        '/inbox/old/{int:int}': '/inbox',
        '/inbox/my': function() {
            return '/inbox';
        },
        '/inbox/my/{int:int}': function(params) {
            return '/inbox/' + params.int;
        }
    },
    rewriteUrl: {
        '/page1': '/page/1'
    },
    route: {
        '/inbox': 'messages',
        '/message/{mid:int}': 'message',
        '/page/prefix{page:int}': 'url-with-prefix',
        '/search/{request:any}': 'search'
    },
    rewriteParams: {
        'message': function(params) {
            return { id: params.mid };
        }
    }
};

ns.router.regexps: {object}#

Тут задаются типы параметров в виде регулярных выражения.

Начальный набор такой:

ns.router.regexps = {
    'id': '[A-Za-z_][A-Za-z0-9_-]*',
    'int': '[0-9]+'
};

Параметры#

Параметры в урле задаются в {}, к примеру, /message/{message-id}. Параметр может быть как между /-ами, так и в промежутках, к примеру, /archive/{year}-{month}-{day}.

Тип указывается после имени параметра и отделяется :: {page:int}. Если параметр указан без типа - ему присваивается тип id. Т.е. {message-id} соответствует {message-id:id}.

Параметр может быть опциональным. В этом случае, слеш перед ним тоже становится опциональным. Чтобы указать, что параметр опционален - нужно дописать = или =default после имени параметра (или типа, если он указан), примерно так: {page=}, {page=0}, {page:int=} или {page:int=0}. Если в исходном урле параметр не задан, но указано дефолтное значение - оно будет в итоговом наборе параметров страницы params.

Можно указать фильтр значения параметра. В этом случае параметр должен иметь строго указанное значение, только в этом случае урл будет заматчен. Чтобы указать фильтр нужно дописать ==filter после имени параметра (или типа, если он указан), примерно так: {color==green} или {color:colors==green}.

Можно указать либо дефолтное значение, либо фильтр.

@include ns.update.logic

ns.View#

Вид представляет собой элемент интерфейса. Он однозначно идентифицируется своим ключом, который строится во время инициализации исходя из параметров вида. Разный ключ всегда означает разный экземпляр вида.

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

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

Декларация#

Определение нового вида происходит через статическую функцию ns.View.define

ns.View.define('viewName', viewDeclObject[, baseView])

Объект-декларация состоит из следующих свойств.

ctor#

ctor - это функция-конструтор. Обратите внимание, что он вызывается самым первым, до инициализации самого вида, т.о. в конструкторе еще не доступеы некоторые свойства.

Полностью готовый экземпляр бросает событие ns-view-init.

/**
 * @classdesc prj.vMyView
 * @augments ns.View
 */
ns.View.define('my-view', {
    /**
     * @constructs prj.vMyView
     */
    ctor: function() {
        this._state = 'initial';
        this.CONST = 100;
    }
});

events#

events - объект с декларацией подписок на события, как DOM, так и noscript.

Любая подписка имеет вид:

{
    "на что подписаться@когда": "обработчик"
}

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

События с суффиксом @show вешаются во время показа вида (событие ns-view-show) и снимаются во время скрытия (событие ns-view-hide). Аналогично, суффикс @init означает, что событие будет активировано на ns-view-htmlinit и деактивировано на ns-view-htmldestroy.

DOM-события#

DOM-события от события noscript различаются согласно массиву ns.V.DOM_EVENTS. Все, что не входит в этот массив, является "космическим" событием noscript.

DOM-события навешиваются через механизм делегирования.

Примеры деклараций:

{
    // событие click на корневой ноде вида
    "click": "onClick",
    // событие click на нодах к классом selector внутри вида
    "click .selector": "onSelectorClick",
    "click@init .selector": "onInitSelectorClick"
}

"Космические" события noscript#

Декларируются как и остальные события

{
    "my-custom-event": "onCustomEvent",
    "my-custom-init@init": "onCustomInit"
}

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

"Косимческие" события работают через единую шину ns.events

ns.events.trigger('my-custom-event');

Встроенные события#

Список событий: ns-view-hide - вида был скрыт и больше не виден на странице ns-view-htmldestroy - старая нода у вида была уничтожена ns-view-htmlinit - у вида появилась новая нода ns-view-async - у async-view появилась заглушка. Это единственное событие, которое генерируется для заглушки async-view ns-view-show - view был показан и теперь виден на странице ns-view-touch - view виден и был затронут в процессе обновления страницы

  1. События генерируются снизу вверх, т.е. сначала их получают дочерние вида, потом родительские.
  2. События генерируются пачками, т.е. сначала одно событие у всех view, потом другое событие у всех view.
  3. События генерируются в строго определенном порядке, указанном выше

Примеры последовательностей событий: инициализация view: ns-view-htmlinit -> ns-view-show -> ns-view-touch перерисовка страница, если view валиден: ns-view-touch view был скрыт: ns-view-hide (без ns-view-touch) view был показан: ns-view-show -> ns-view-touch * view был перерисован: ns-view-hide -> ns-view-htmldestroy -> ns-view-htmlinit -> ns-view-show -> ns-view-touch (ns-view-hide тут вызывается из тех соображений, что могут быть обработчики, которые вешаются на ns-view-show/ns-view-hide и при обновлении ноды, они должны быть переинициализированы)

methods#

methods - объект с методами вида. По сути является прототипом объекта.

/**
 * @classdesc prj.vMyView
 * @augments ns.View
 */
ns.View.define('my-view', {
    /** @lends prj.vMyView.prototype
    methods: {
        BAR: 100
        foo: function(){}
    }
});

models#

models позволяет указать модели, от которых зависит вид. Зависимость означает, что 1. параметры вида будут собраны на основе параметров связанных моделей 2. в шаблонах будут доступны вида данные связанных моделей 3. некоторые методы вида будут подписаны на события связанных моделей

По умолчанию вид подписывается на следующие стандартные события модели: - ns-model-changed - ns-model-insert - ns-model-remove - ns-model-destroyed и не подписывается на событие ns-model-touched.

Если обработчики явно не указаны, то в качестве обработчика стандартных событий устанавливается метод invalidate.

ns.View.define('super-view', {
  models: ['album', 'photo']
});

В приведённом примере вид будет инвалидироваться при любом стандартном событии модели.

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

ns.View.define('super-view', {
  models: {album: {
    'ns-model-boof': 'invalidate'
  }}
});

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

ns.View.define('super-view', {
  models: {album: {
    `ns-model-changed`: 'keepValid'
  }}
});

В приведённом примере при наступлении события ns-model-changed вид будет оставаться валидным и не будет перерисован при последующих update'ах. При любом другом стандартном событии модели он будет проинвалидирован.

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

ns.View.define('super-view', {
  models: {album: 'keepValid'}
});

В приведённом примере события модели album не будут влиять на валидность вида.

'invalidate' и 'keepValid' - это имена реальных методов. Вместо них можно указать имя любого другого метода вида.

Если нужно в качестве обработчика события использовать произвольный метод, и при этом инвалидировать вид, достаточно внутри метода вызвать this.invalidate();.

Для краткости вместо методов invalidate и keepValid можно указывать их краткую форму: true и false соответственно. 2 варианта деклараций в следующем примере работают одинаково.

ns.View.define('super-view', {
  models: {
    photo: 'invalidate',
    album: 'keepValid'
  }
});

ns.View.define('super-view', {
  models: {
    photo: true,
    album: false
  }
});

Для большей краткости зависимости от моделей можно указывать в виде массива. Это будет эквивалентно указанию в качестве обработчика их событий метода invalidate.

ns.View.define('super-view', {
  models: ['photo', 'album']
});

Параметры#

Параметры нужны для построения ключа.

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

Если params явно заданы — нельзя использовать params+ / params-.

Если ключ view нельзя построить бросается исключение.

params+#

Добавляет в результирующий набор дополнительные параметры:

ns.View.define('super-view', {
  "models": [ 'album', 'photo' ],
  "params+": { page: 23 }
});

params-#

Удаляет из результирующего набора указанные параметры:

ns.View.define('super-view', {
  "models": [ 'album', 'photo' ],
  "params-": [ 'album-id' ]
});

params#

params может быть массивов объектов или функцией. Также можно указать объект - это короткая запись массива с одним элементом.

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

ns.View.define('super-view', {
  params: [
    { "context": "album", "album-id": null },
    { "context": null }
  ]
});

Как строится ключ: - каждое свойство объекта — это обязательный параметр - если значение свойства null — параметр обязателен, но значение его может быть любым - если значение свойства не null — это фильтр, параметр из урла должен иметь именно это значение - если есть все нужные параметры и выполняются все фильтры — ключ можно строить - иначе — пытаемся строить по следующей группе параметров

Т.о. при использовании params все параметры являются обязательными. Чтобы сделать их необязательными, используйте params+.

Если указана функция, то она строит ключ сама на основе переданных параметров.

ns.View.define('view', {
  // ns.key - готовая функция для склеивания параметров в строку
  params: ns.key
})

rewriteParamsOnInit#

При декларации вида можно объявить специальную функцию для обработки параметров. Аргументом функции приходят параметры страницы, а функция должна вернуть обработанные параметры.

Примеры использования: - вид зависит не от параметров страницы, а от названия layout - в параметры необходимо добавить какое-то фиксированное значение

ns.View.define('my-view', {
    rewriteParamsOnInit: function(params) {
        return {
            layout: ns.page.current.page 
        }
    }
});

Валидность#

Валидность view считается по двум факторам: - собственный статус ns.V.STATUS - статус привязанных моделей

При отрисовке вид запоминает все версии моделей и в дальшейшем сравнимает их. Если версия изменилась, то вид будет перерисован.

Также у вида есть собственный статус this.static значением, которого может быть тип ns.V.STATUS. Если статус не ns.V.STATUS.OK, то вид будет перерисован.

Инвалидировать вид можно методом this.invalidate().

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

Взаимодействие#

В noscript нет какого-либо способа получить созданный экземпляр вида. Поэтому любое внешнее взаимодействие с ним осуществляется исключительно через механизм событий noscript

async#

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

Схема работы: 1. Если у вида есть все необходимые данные (все модели валидны) для отрисовки, то он отрисуется в общем потоке. 2. Если модели не валидны, то сначала отрисуется заглушка - мода ns-view-async-content, где будут доступны все валидные на данный момент данные, и сделан запрос за остальными моделями. У вида будет вызвано событие ns-view-async. 3. После получения данных вид будет перерисован с обычной модой ns-view-content и поведет себя как обычно

@include ns.view.yate

ns.ViewCollection#

ViewCollection - это коллеция ns.View, привязанная к ns.ModelCollection. При изменении коллекции позволяет перерисовывать только изменившиеся элементы.

По сути, образуется следующая зависимость один-к-одному:

ViewCollection      ->  ModelCollection
    view-item-1     ->      model-item-1
    view-item-2     ->      model-item-2
                    ...
    view-item-N     ->      model-item-N

ns.ViewCollection может зависит только от одной ns.ModelCollection.

ns.ViewCollection может содержать внутренние виды и иметь собственную html-разметку.

Декларация#

ns.ViewCollection.define('my-view-collection', {
    models: [ 'my-model-collection' ],
    split: {
        view_id: 'my-view-collection-item'
    }
});

Опция split.view_id определяет из каких ns.View состоит коллекция.

Опция models, как и в ns.View определяет зависимость от моделей и подписки на их события. По умолчанию ViewCollection делает следующие подписки: - обработчиком собственных событий ns-model-changed и ns-model-destroyed любых моделей устанавливается invalidate. Эти события наступают при изменении данных, по которым рисуется собственная html-разметка viewCollection'а, поэтому вид по умолчанию становится невалидным, чтобы перерисоваться. - обработчиком ns-model-insert и ns-model-remove модели-коллекции устанавливается keepValid. Эти события наступают при изменении состава модели-коллекции, по которой рисуются вложенные виды viewCollection'а. Собственная html-разметка при этом не затрагивается, поэтому вид по умолчанию остаётся валидным. События моделей, вложенных в коллекцию игнорируются и подписаться на них через декларацию нельзя.

Декларация элемента ns.ViewCollection выглядит так:

ns.View.define('my-view-collection-item', {
    models: [ 'my-model-collection-item' ]
});

Элемент коллекции ведет себя как обычный ns.View и ничего не знает про коллекцию.

Элементы коллекции помещаются в узел-контейнер, размеченный классом ns-view-container-desc. Узел-контейнер обязательно должен быть указан. Вне этого контейнера можно делать собcтвенную html-разметку.

Страница#

Noscript служит для создания одностраничных приложений. Поэтому Страница в контексте noscript - это то же самое, что и приложение. В приложении может быть неограниченное количество логических страниц, но все они будут показываться в рамках одного физического html-документа. Он и есть Страница. В noscript страница представлена объектом ns.page.

Адрес страницы#

Основное состояние страницы определяется Адресом страницы (url). Он определяет то состояние, которое должно быть показано при загрузке/перезагрузке страницы. Основное состояние может определяться следующими атрибутами: - идентификатор логической страницы (раздел сайта) - идентификатор сущности, оторбражаемой в приложении (id фотки, id файла) - атрибут состояния интерфейса, который хочется иметь возможность задавать извне (идентификатор открытого диалога) Адрес страницы служит её внешним API.

Ключевые сущности#

Параметры страницы#

Параметры страницы (ns.page.params) - это параметры, получаемые из адреса страницы. ЧПУ преобразуется в объект, с которым в дальнейшем работают сущности приложения.

Маршрутизатор#

Для преобразования адреса в параметры используется маршрутизатор (ns.router). Кроме параметров маршрутизатор так же возвращает идентификатор раскладки страницы (ns.layout).

Модель#

Модель - это элемент данных. Все данные, которые говорит и показывает интерфейс, должны быть представлены моделями. Модель может быть как клиентским представлением данных на сервере, так и локальным элементом данных, относящихся только к интерфейсу (модели состояний интерфейса).

Для работы с данными сначала декларируются прототипы моделей (ns.Model.define). Затем создаются конкретные экземпляры модели. Уникальным идентификатором экземпляра модели является свойство key. Свойство id у экземпляра модели указывает на прототип модели.

Ключ модели key строится на основе параметров, указанных в декларации модели. К ключам стоит относиться как к хешам. Попытка парсинга ключа - это дорога в ад.

Вид#

Вид - это элемент интерфейса, из видов состоит страница. Вид может отображать статическое содержимое, значения параметров страницы, или данные моделей. Вид состоит из декларации и шаблона. Декларация содержит - id вида, определяющий прототип - модели, от которых зависит вид - параметры страницы, от которых зависит вид.

Экземпляры видов идентифицируются атрибутом key. Ключ вида строится на основании параметров моделей, от которых зависит вид, и параметров, указанных в декларации вида.

Экземпляры видов создаются только автоматически в результате работы контроллера обновления (ns.Updater) Виды могут быть вложенны друг в друга. На странице всегда существует корневой вид, внутри которого находятся остальные виды приложения. Существуют специальные виды: бокс, асинхронный вид, вид-коллексия. Шаблон вида может иметь различную структуру в зависимости от используемого шаблонизатора. В комплекте с noscript идёт набор .yate шаблонов, задающих определённую структуру. Так же есть ряд нюансов, которые нужно учитывать при написании собственных шаблонов.

Раскладка страницы#

Раскладка страницы (layout) - это декларация, по которой в зависимости от параметров определяется структура видов. Приложение может иметь несколько раскладок. Раскладка выбирается по идентификатору раскладки, который возвращается маршрутизатором. Раскладка представляет собой древовидный json-объект. Каждый узел дерева соответствует виду. В ключе объекта - идентификатор прототипа вида. В значении объекта - вложенные виды. Единственный вложенный вид может быть задан строкой. Более сложная структура может быть задана объектом. Структура, зависящая от каких-то условий может быть задана функцией, которая возвращает одну из перечисленных структур. Чтобы определить вид без вложенностей, значение нужно установить в true.

Контроллер обновления#

Контроллер обновления (ns.Updater) - объект, реализующий логику построения и обновления страницы.