Поделиться через


Шаблоны и производительность JavaScript

Много лет назад ASP.NET предоставили нам отрисовку элементов пользовательского интерфейса на стороне сервера, и это было хорошо. Однако для отрисовки на стороне сервера требуется код полного доверия. Теперь, когда мы перешли на SharePoint и Office 365, код полного доверия больше не является вариантом. Это означает, что отрисовка элементов управления пользовательским интерфейсом на стороне сервера больше не будет работать.

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

Теперь javaScript на стороне клиента — это способ отрисовки элементов управления пользовательским интерфейсом.

Шаблоны JavaScript

Так как клиентский JavaScript — это путь, какие лучшие способы реализации JavaScript на стороне клиента? Как начать работу?

Существует несколько вариантов:

Вариант Описание
Внедрение JavaScript Site.UserCustomActions или Web.UserCustomActions позволяют включать скрипт непосредственно в разметку страницы. Это используется в шаблоне загрузчика , описанном ниже.
Шаблоны отображения Применяется к представлениям и поиску. Вам не нужно развертывать какой-либо код, размещенный в приложении или поставщике. Это просто файл JavaScript, который можно отправить в (например) библиотеку стилей для настройки представлений. Вы можете создать любое требуемое представление с помощью JavaScript
Размещенные Add-Ins SharePoint Использует JSOM для обратного взаимодействия с хост-сайтом или веб-сайтом надстройки. Он предоставляет доступ к веб-прокси для междоменных вызовов
Размещенные у поставщика Add-Ins Позволяет создавать сложные приложения в различных технологических стеках, сохраняя безопасную интеграцию с SharePoint.
JSLink Позволяет загружать один или несколько файлов JavaScript во многих веб-частях и представлениях OOTB.
Веб-часть ScriptEditor Включение скрипта напрямую или загруженного через теги скрипта с разметкой для создания сложных одностраничных приложений, размещенных полностью на сайте SharePoint

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

Производительность JavaScript

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

Вариант Описание
Уменьшение количества запросов Меньшее количество запросов означает меньше циклов передачи на сервер, что снижает задержку.
Получение только необходимых данных Уменьшите объем данных, отправляемых по сети. Кроме того, снижается нагрузка на сервер.
Обеспечение хорошей загрузки страницы Следите за тем, чтобы пользовательский интерфейс реагировал на действия пользователя. Например, обновите меню на странице перед началом скачивания более 100 записей.
По возможности используйте асинхронные вызовы и шаблоны Опрос является более тяжелым бременем для производительности, чем использование асинхронного вызова или обратного вызова.
Кэширование является ключевым Кэширование еще больше снижает нагрузку на сервер, обеспечивая немедленное повышение производительности.
Подготовка к большему просмотру страниц, чем вы когда-либо себе представляли Целевая страница с большим объемом данных подходит, если у вас всего несколько попаданий. Но если вы получите тысячи хитов, это может действительно повлиять на производительность.

Что делает мой код

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

Уменьшение количества запросов

Всегда делайте наименьшие и минимальные запросы. Каждый устранимый запрос снижает нагрузку на производительность на сервере и на клиенте. Более мелкие запросы еще больше снижают нагрузку на производительность.

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

  • Ограничьте количество файлов JavaScript в рабочей среде. Разделение файлов JavaScript хорошо подходит для разработки, но не очень хорошо для рабочей среды. Объедините файлы JavaScript в один файл JavaScript или как можно меньше файлов JavaScript.
  • Сжатие размеров файлов. Сведите к минимуму рабочие файлы JavaScript, удалив разрывы строк, пробелы и комментарии. Существует несколько программ и веб-сайтов для миниификации JavaScript, которые можно использовать для существенного уменьшения размеров файлов JavaScript.
  • Используйте кэширование файлов браузера для сокращения запросов. Обновленный шаблон загрузчика ниже является хорошим способом расширить эту идею.

Получение только необходимых данных

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

  • Используйте фильтрацию, выбор и ограничения сервера, чтобы свести к минимуму трафик по проводу.
  • Не запрашивайте все статьи, если требуется только первые пять, в качестве другого примера.
  • Не запрашивайте весь контейнер свойств, если требуется только одно свойство. В одном из примеров скрипту требовалось только одно свойство, но запрашивался весь контейнер свойств, который оказался 800 КБ. Кроме того, помните, что размер объекта может изменяться со временем, поэтому то, что составляет всего несколько килобайт, может стать размером в мегабайтах позже в жизненном цикле продукта.

Не запрашивайте данные, которые будут удалены

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

  • Запрашивайте только нужные поля, например Имя и Адрес, а не всю запись.
  • Создавать конкретные, преднамеренные запросы фильтров. Например, если вы хотите получить список доступных статей, получите заголовки, PublishingDate и Author. Оставьте остальные поля в запросе.

Обеспечение хорошего взаимодействия с пользователем

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

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

Все асинхронно

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

  • Узнайте, как работает код во времени.
  • Используйте события и обратные вызовы вместо опроса.
  • Использование обещаний. В jQuery они называются отложенными объектами . В Q, WinJS и ES6 существуют аналогичные концепции.
  • Используйте асинхронный вызов в пользу несинхронного вызова.
  • Используйте асинхронный режим каждый раз, когда может возникнуть задержка:
    • Во время запроса AJAX.
    • Во время любых важных манипуляций DOM.

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

Кэширование на стороне клиента

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

Существует три разных места, где можно кэшировать данные:

Вариант Описание
Хранилище сеансов Хранит данные в виде пары "ключ-значение" на клиенте. Это хранилище сеанса, которое всегда хранится в виде строк.

JSON.stringify() преобразует объекты JavaScript в строки, которые помогают хранить объекты.
Локальное хранилище Хранит данные в виде пары "ключ-значение" на клиенте. Эта функция сохраняется в сеансах, которые всегда хранятся в виде строк.

JSON.stringify() преобразует объекты JavaScript в строки, которые помогают хранить объекты.
Локальная база данных Хранит реляционные данные на клиенте. Часто использует SQL-Lite в качестве ядра СУБД.

Хранилище локальной базы данных не всегда доступно во всех браузерах— проверьте поддержку целевого браузера.

При кэшировании учитывайте доступные ограничения хранилища и актуальность данных.

  • Если вы достигли предела хранилища, может быть разумно удалить старые или менее важные кэшированные данные.
  • Различные типы данных могут устареть быстрее, чем другие. Список новостей может быть устаревшим в течение пяти-десяти минут, но имя профиля пользователя часто можно безопасно кэшировать в течение 24 часов или более.

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

Цена популярности

Как часто просматривается ваша страница? В классическом сценарии домашняя страница организации устанавливается в качестве страницы запуска для всех браузеров в организации. Тогда вы вдруг получите гораздо больше трафика, чем вы когда-либо себе представляли. Каждый байт содержимого внезапно увеличится в производительности сервера и пропускной способности, потребляемой домашней страницей.

Решение. Перейдите на домашнюю страницу и привяжите ссылку на другое содержимое.

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

Шаблон загрузчика

Цель шаблона загрузчика — предоставить способ внедрения неизвестного количества удаленных скриптов на сайт без необходимости обновлять сайт. Обновления можно выполнить в удаленной сети CDN и обновят все сайты.

Шаблон загрузчика создает URL-адрес с меткой даты и времени в конце, чтобы файл не кэшировались. Он настраивает jQuery в качестве зависимости от файла загрузчика, а затем выполняет функцию в загрузчике. Это гарантирует, что пользовательский Код JavaScript будет загружен после завершения загрузки jQuery.

PnP-dev\Samples\Core.JavaScript\Core.JavaScript.Embedder\Program.cs:

static void Main(string[] args)
{
    ContextManager.WithContext((context) =>
        // this is the script block that will be embedded into the page
        // in practice this can be done during provisioning of the site/web
        // make sure to include ';' at end to play nice with page embedding
        // using the script on demand feature built into SharePoint we load jQuery, then our remote loader(pnp-loader.js or pnp-loader-cached.js) file using a dependency
        var script = @"(function (loaderFile, nocache) {
                                var url = loaderFile + ((nocache) ? '?' + encodeURIComponent((new Date()).getTime()) : '');
                                SP.SOD.registerSod('pnp-jquery.js', 'https://localhost:44324/js/jquery.js');
                                SP.SOD.registerSod('pnp-loader.js', url);
                                SP.SOD.registerSodDep('pnp-loader.js', 'pnp-jquery.js');
                                SP.SOD.executeFunc('pnp-loader.js', null, function() {});
                        })('https://localhost:44324/pnp-loader.js', true);";


        // this version of the script along with pnp-loaderMDS.js (or pnp-loaderMDS-cached.js) handles pages where the minimum download strategy is active
        var script2 = @"ExecuteOrDelayUntilBodyLoaded(function () {
                            var url = 'https://localhost:44324/js/pnp-loaderMDS.js?' + encodeURIComponent((new Date()).getTime());
                            SP.SOD.registerSod('pnp-jquery.js', 'https://localhost:44324/js/jquery.js');
                            SP.SOD.registerSod('pnp-loader.js', url);
                            SP.SOD.registerSodDep('pnp-loader.js', 'pnp-jquery.js');
                            SP.SOD.executeFunc('pnp-loader.js', null, function () {
                                if (typeof pnpLoadFiles === 'undefined') {
                                    RegisterModuleInit('https://localhost:44324/js/pnp-loaderMDS.js', pnpLoadFiles);
                                } else {
                                    pnpLoadFiles();
                                }
                            });
                        });";

        // load the collection of existing links
        var links = context.Site.RootWeb.UserCustomActions;
        context.Load(links, ls => ls.Include(l => l.Title));
        context.ExecuteQueryRetry();

        // this block handles deleting previous test custom actions
        var doDelete = false;

        foreach (var link in links.ToArray().Where(l => l.Title.Equals("MyTestCustomAction", StringComparison.OrdinalIgnoreCase)))
        {
            link.DeleteObject();
            doDelete = true;
        }

        if (doDelete)
        {
            context.ExecuteQueryRetry();
        }

        // now we embed our script into the user custom action
        var newLink = context.Site.RootWeb.UserCustomActions.Add();
        newLink.Title = "MyTestCustomAction";
        newLink.Description = "Doing some testing.";
        newLink.ScriptBlock = script2;
        newLink.Location = "ScriptLink";
        newLink.Update();
        context.ExecuteQueryRetry();
    });
}

Задает SP.SOD.registerSodDep('pnp-loader.js', 'pnp-jquery.js'); зависимость и SP.SOD.executeFunc('pnp-loader.js', null, function() {}); принудительно загружает jQuery полностью перед загрузкой пользовательского JavaScript.

И newLink.ScriptBlock = script2;newLink.Location = "ScriptLink"; являются ключевыми частями добавления этого элемента в действие пользователя клиента.

Затем файл pnp-loader.js загружает список файлов JavaScript с обещанием, которое можно запустить при загрузке каждого файла.

PnP-dev\Samples\Core.JavaScript\Core.JavaScript.CDN\js\pnp-loader.js:

(function () {

    var urlbase = 'https://localhost:44324';
    var files = [
        '/js/pnp-settings.js',
        '/js/pnp-core.js',
        '/js/pnp-clientcache.js',
        '/js/pnp-config.js',
        '/js/pnp-logging.js',
        '/js/pnp-devdashboard.js',
        '/js/pnp-uimods.js'
    ];

    // create a promise
    var promise = $.Deferred();

    // this function will be used to recursively load all the files
    var engine = function () {

        // maintain context
        var self = this;

        // get the next file to load
        var file = self.files.shift();

        var fullPath = urlbase + file;

        // load the remote script file
        $.getScript(fullPath).done(function () {
            if (self.files.length > 0) {
                engine.call(self);
            }
            else {
                self.promise.resolve();
            }
        }).fail(self.promise.reject);
    };

    // create our "this" we will apply to the engine function
    var ctx = {
        files: files,
        promise: promise
    };

    // call the engine with our context
    engine.call(ctx);

    // give back the promise
    return promise.promise();

})().done(function () {
    /* all scripts are loaded and I could take actions here */
}).fail(function () {
    /* something failed, take some action here if needed */
});

Файл pnp-loader.js не кэшируются, что хорошо подходит для среды разработки. Файл pnp-loader-cached.js заменяет $.getScript функцию $.ajax функцией, которая обеспечивает кэширование файлов в браузере и лучше подходит для рабочей среды.

Из PnP-dev\Samples\Core.JavaScript\Core.JavaScript.CDN\js\pnp-loader.js

    // load the remote script file
    $.ajax({
        type: 'GET',
        url: fullPath,
        cache: true,
        dataType: 'script'
    }).done(function () {
        if (self.files.length > 0) {
            engine.call(self);
        }
        else {
            self.promise.resolve();
        }
    }).fail(self.promise.reject);

Этот шаблон упрощает развертывание и обновление сайтов. Это особенно полезно при развертывании или обновлении в тысячах семейств веб-сайтов.

Кэширование текущего пользователя

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

При этом также используется отложенная (jQuery версия promise). Если мы получаем данные из кэша или с сервера, функция "Отложено" разрешается. Если возникает ошибка, отложенный объект отклоняется.

Из PnP-dev\Samples\Core.JavaScript\Core.JavaScript.CDN\js\pnp-core.js:

    getCurrentUserInfo: function (ctx) {

        var self = this;

        if (self._currentUserInfoPromise == null) {

            self._currentUserInfoPromise = $.Deferred(function (def) {

                var cachingTest = $pnp.session !== 'undefined' && $pnp.session.enabled;

                // if we have the caching module loaded
                if (cachingTest) {
                    var userInfo = $pnp.session.get(self._currentUserInfoCacheKey);
                    if (userInfo !== null) {
                        self._currentUserInfo = userInfo;
                        def.resolveWith(ctx || self._currentUserInfo, [self._currentUserInfo]);
                        return;
                    }
                }

                // send the request and allow caching
                $.ajax({
                    method: 'GET',
                    url: '/_api/SP.UserProfiles.PeopleManager/GetMyProperties?$select=AccountName,DisplayName,Title',
                    headers: { "Accept": "application/json; odata=verbose" },
                    cache: true
                }).done(function (response) {

                    // we also parse and add some custom properties as an example
                    self._currentUserInfo = $.extend(response.d,
                        {
                            ParsedLoginName: $pnp.core.getUserIdFromLogin(response.d.AccountName)
                        });

                    if (cachingTest) {
                        $pnp.session.add(self._currentUserInfoCacheKey, self._currentUserInfo);
                    }

                    def.resolveWith(ctx || self._currentUserInfo, [self._currentUserInfo]);

                }).fail(function (jqXHR, textStatus, errorThrown) {

                    console.error('[PNP]=>[Fatal Error] Could not load current user data data from /_api/SP.UserProfiles.PeopleManager/GetMyProperties. status: ' + textStatus + ', error: ' + errorThrown);
                    def.rejectWith(ctx || null);
                });
            });
        }

        return this._currentUserInfoPromise.promise();
    }
}

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

Другой шаблон кэширования можно найти в pnp-clientcache.js storageTest, который берется из средства модернизации storageTest. Он содержит функции для добавления, получения, удаления и getOrAdd, которые будут возвращать кэшированные данные, если они хранятся в кэше, или извлекать данные с сервера и добавлять их в кэш, если они отсутствуют в кэше, который сохраняет повторяющийся код в вызывающей функции. get использует JSON.parse для проверки срока действия, так как срок действия не является функцией в локальном хранилище. _createPersistable сохраняет объект JavaScript в локальном кэше хранилища.

Из PnP-dev\Samples\Core.JavaScript\Core.JavaScript.CDN\js\pnp-clientcache.js:

// adds the client cache capability
caching: {

    // determine if we have local storage once
    enabled: storageTest(),

    add: function (/*string*/ key, /*object*/ value, /*datetime*/ expiration) {

        if (this.enabled) {
            localStorage.setItem(key, this._createPersistable(value, expiration));
        }
    },

    // gets an item from the cache, checking the expiration and removing the object if it is expired
    get: function (/*string*/ key) {

        if (!this.enabled) {
            return null;
        }

        var o = localStorage.getItem(key);

        if (o == null) {
            return o;
        }

        var persistable = JSON.parse(o);

        if (new Date(persistable.expiration) <= new Date()) {

            this.remove(key);
            o = null;

        } else {

            o = persistable.value;
        }

        return o;
    },

    // removes an item from local storage by key
    remove: function (/*string*/ key) {

        if (this.enabled) {
            localStorage.removeItem(key);
        }
    },

    // gets an item from the cache or adds it using the supplied getter function
    getOrAdd: function (/*string*/ key, /*function*/ getter) {

        if (!this.enabled) {
            return getter();
        }

        if (!$.isFunction(getter)) {
            throw 'Function expected for parameter "getter".';
        }

        var o = this.get(key);

        if (o == null) {
            o = getter();
            this.add(key, o);
        }

        return o;
    },

    // creates the persisted object wrapper using the value and the expiration, setting the default expiration if none is applied
    _createPersistable: function (/*object*/ o, /*datetime*/ expiration) {

        if (typeof expiration === 'undefined') {
            expiration = $pnp.core.dateAdd(new Date(), 'minute', $pnp.settings.localStorageDefaultTimeoutMinutes);
        }

        return JSON.stringify({
            value: o,
            expiration: expiration
        });
    }
},

Более сложное использование асинхронных и отложенных можно найти на панели мониторинга разработчика в pnp-clientcache.js

Ресурсы

См. также

Примеры