
Автор: Евгений Клименко, Senior Developer, DataArt
О недостатках использования одного лишь JavaScript и jQuery при разработке клиентской части веб-приложения много говорить не будем — это всегда плохо поддерживаемый спагетти-код, необходимость внедрения в разметку скрытых полей для хранения данных, трудности с тестированием и, как следствие, большое количество дефектов.
Для преодоления этих недостатков предлагаются клиентские фреймворки, причем подчас трудно понять, какой из них лучше. В качестве иллюстрации приведу решение не очень сложной задачи с использованием двух принципиально различных фреймворков: Knockout, реализующего MVVM-паттерн, и React, который этот паттерн не реализует.
Итак, предположим, необходимо создать страницу, отображающую музыкальные произведения, отфильтрованные по жанрам, композиторам, исполнителям, сборникам и радиопередачам, в которых эти произведения можно услышать. Вот как может выглядеть такая страница:

Как видите, ничего особенного. В верхней части находятся фильтры, а в нижней показаны результаты фильтрации.
Серверный код источника данных рассматривать не будем. Сосредоточимся на клиентском коде (исходный код ASP.Net Web приложения, а также скрипт базы данных можно скачать).
1. Реализация с Knockout
Главный фрагмент страницы (MusicViaKnockout\Index.cshtml) выглядит стандартным для Knockout образом – разметка и привязки (data-bind) DOM-элементов к view-модели:

А вот как выглядит view-модель:
$(function() { var musicViewModel = { genres: ko.observableArray(), genresFound: ko.observableArray(), performers: ko.observableArray(), composers: ko.observableArray(), albums: ko.observableArray(), casts: ko.observableArray(), musics: ko.observableArray() }; mapServerData = function(data) { if (!data) return; musicViewModel.genres(data.Filters.Genres); musicViewModel.genresFound(data.Filters.GenresFound); musicViewModel.performers( data.Filters.Performers); musicViewModel.performers.unshift({PerformerID: -1, Performer: 'All'}); musicViewModel.composers(data.Filters.Composers); musicViewModel.composers.unshift({ComposerID: -1, Composer: 'All'}); musicViewModel.albums(data.Filters.Albums); musicViewModel.albums.unshift({AlbumID: -1, Album: 'All'}); musicViewModel.casts(data.Filters.Casts); musicViewModel.casts.unshift({CastID: -1, Cast: 'All'}); musicViewModel.musics(data.Filters.Musics); }, genresChanged = function (data, event) { $.ajax({ url: "/Music/FilterMusics", type: "GET", data: { genreIDs: ko.toJS(musicViewModel.genresFound), "composerID": null, "albumID": null, "performerID": null, "castID": null }, traditional: true, timeout: 30000, success: function (data) { mapServerData(data); } }); } $.getJSON("/api/MusicWebAPI", function (data) { mapServerData(data); ko.applyBindings(musicViewModel); }); });
Если вы знакомы с Knockout, все очень просто. При загрузке страницы данные считываются вызовом
$.getJSON("/api/MusicWebAPI"
…, после чего они привязываются к view-модели. Отображение данных модели фреймворк берет на себя. Как видите, и код, и разметка просты и лаконичны. Это — неоспоримое достоинство Knockout.
2. Реализация на React
Страница (../Views/Music/Index.cshtml):
@using MusicChoice2.HtmlHelpers @using Newtonsoft.Json @using React.Web.Mvc @model MusicChoice.ViewModels.MusicViewModel @{ ViewBag.Title = "title"; } <script src="@Url.Content("~/Scripts/jquery.min.js")"></script> <script src="@Url.Content("~/Scripts/underscore-min.js")"></script> <script src="@Url.Content("~/Scripts/react.js")"></script> <script src="@Url.Content("~/Scripts/react-dom.js")"></script> <script src="@Url.Content("~/Scripts/MusicChoice/MusicFilters.jsx")"></script> <script src="@Url.Content("~/Scripts/MusicChoice/MusicChoice.js")"></script> <link rel="stylesheet" href="../../Styles/music.css"> <div> @Html.React("MusicFilters", new { albums = Model.Filters.Albums, casts = Model.Filters.Casts, composers = Model.Filters.Composers, genres = Model.Filters.Genres, performers = Model.Filters.Performers, genresFound = Model.Filters.GenresFound, musics = Model.Filters.Musics, albumID = Model.Filters.AlbumID, castID = Model.Filters.CastID, composerID = Model.Filters.ComposerID, performerID = Model.Filters.PerformerID, onFilterChanged = "music.onFilterChanged" }, "containerId") @Html.ReactInitJavaScriptAndAssignIds(new[] { "music.filters" }) </div>
Здесь главное явление — MVC helper (@Html.React(«MusicFilters»…), наполняющий свойства React-компонента данными сервера.
Вот как выглядит сам React-компонент:
var MusicFilters = React.createClass({ getDefaultProps: function() { return { defaultLoadParameters : {} } }, getInitialState: function() { return { performers: this.props.performers , genres: this.props.genres , genresFound: this.props.genresFound , musics: this.props.musics , albums: this.props.albums , composers: this.props.composers , casts: this.props.casts , selected: { performerID: this.props.performerID , musicID: this.props.musicID , albumID: this.props.albumID , composerID: this.props.composerID , castID: this.props.castID } } }, render: function() { var onFilterChanged = this.onFilterChanged; var musicFilters = this; var genresContains = function(genreID) { var contains = false; musicFilters.state.genresFound.forEach(function (g) { if (g == genreID) { contains = true; } }); return contains; }; var onGenreChanged = this.onGenreChanged; var renderDropDown = function(title, id, values, selectedValue, optionIndex, optionValue, selectedParameter, labelStyle) { return (values !== undefined && values !== null)? <div> <label style={labelStyle}>{title + ':'}</label> <select id={id} value={selectedValue || -1} onChange={onFilterChanged.bind(musicFilters, selectedParameter)}> <option key={-1} value={-1}>All</option> { values.map(function (val, idx) { return <option key={idx} value={val[optionIndex]}>{val[optionValue]}</option> }) } </select> </div>:<div>No data</div> }; return (this.state.genres !== undefined && this.state.genres !== null)? <div> <div> <div> <div className="genres" id="genres"> <div style={{"font-weight":"bold"}}>Genres:</div> { this.state.genres.map(function (val, idx) { return <div key={idx}> <input checked={genresContains(val["GenreID"])} type="checkbox" value={val["GenreID"]||null} onChange={onGenreChanged.bind(musicFilters)}/> <span className="js-genres">{val["Genre"]}</span> </div> }) } </div> <div className="selects"> { renderDropDown("Performers", "performers", this.state.performers, musicFilters.state.selected.performerID, "PerformerID", "Performer", "performerID" ) } { renderDropDown("Composers", "composers", this.state.composers, musicFilters.state.selected.composerID, "ComposerID", "Composer", "composerID" ) } { renderDropDown("Albums", "albums", this.state.albums, musicFilters.state.selected.albumID, "AlbumID", "Album", "albumID" ) } { renderDropDown("Casts", "casts", this.state.casts, musicFilters.state.selected.castID, "CastID", "Cast", "castID", {clear:"both"}) } </div> </div> <div className="musicContainer"> <div className="musics"> <label>Musics:</label><br/> <div style={{"overflow-y": "scroll", "height": "500px"}}> <table> <tbody> { this.state.musics.map(function (val, idx) { return <tr><td>{val["Music"]}</td></tr>; }) } </tbody> </table> </div> </div> </div> </div> </div>: <div>No data</div>; }, // end of lifetime methods ////// raiseEvent: function(eventHandler, eventArgs) { if (eventHandler) { if (typeof(eventHandler) === 'string') { eval(eventHandler(this, eventArgs)); } else { eventHandler(this, eventArgs); } } }, onFilterChanged: function(selectedParameter, e) { var selected = _.clone(this.state.selected); selected[selectedParameter] = parseInt($(e.currentTarget).val()) || null; this.setState( {selected: selected} , this.raiseEvent.bind(this, eval(this.props.onFilterChanged), selected) ); }, onGenreChanged: function() { var genresFound = []; $("#genres").find("input:checked").map(function() { genresFound.push(parseInt($(this).val())); }); var stateCopy = music.deepCopy(this.state); stateCopy.genresFound = genresFound; this.setState(stateCopy); music.reLoadDetailsAndFilters( {genreIDs: genresFound, performerID: this.state.selected.performerID, albumID: this.state.selected.albumID, composerID: this.state.selected.composerID, castID: this.state.selected.castID}); }, areNullablesEqual: function(first, second) { return first === second || (first === null && second === null); } });
Как видите, React оказался гораздо многословнее. Главным образом, из-за функции render. Это естественно, потому что React возлагает отрисовку содержимого на программиста.
Естественно, я склонился в сторону Knockout. Однако заметил одну странность — это, на первый взгляд, малозаметный временной интервал между отображением группы элементов, состоящих из первого чекбокса и пустых дропдаунов, и остального содержимого. Т. е. сначала пользователь видит набор пустых элементов:

И только спустя некоторое время эти элементы наконец будут заполнены данными:

Отмечу, что количество отфильтрованных записей составило порядка 300.
Я увеличил выборку до 15 000 и описанная задержка стала отчетливой:
На той же выборке данных (15 000 записей) страница с React-компонентом ведет себя идеально:
Вот как выглядят соответствующие метрики Knockout:

Время отрисовки страницы — 4.15 сек.
React с рендерингом на клиенте:

Время отрисовки — 2.53 с.
React c рендерингом на сервере:

Время отрисовки — 2.54 с.
Однако обратите внимание на длинную синюю полоску перед рендерингом — эта линия порядка 3 секунд — время, необходимое для формирования разметки на сервере.
Выводы
1) Самым быстрым из рассмотренных вариантов решения задачи оказался React с рендерингом на клиенте. Время отрисовки содержимого страницы с 15 000 записей не превысило 3 секунд. Решение на Knockout потребовало 4 секунды. С учетом того, что 40% пользователей уходят со страницы, которая грузится более 3 секунд, React может оказаться безальтернативным вариантом.
2) Рендеринг на Knockout может сопровождаться неприятными задержками в отображении элементов страницы, особенно в случае больших объемов данных.
3) В данных условиях серверный рендеринг на React сопровождается недопустимо долгим скрытым формированием разметки на сервере.
Общие рекомендации
1) Если объем данных, обрабатываемых на клиенте невелик, и задержки в отрисовке элементов страницы не заметны конечному пользователю, применяйте Knockout. Вы получите лаконичный, прекрасно читаемый код.
2) Если приходится обрабатывать большой объем данных на клиенте, если качество и скорость отображения имеют критическое значение, применяйте React c рендерингом на клиенте.
ссылка на оригинал статьи https://habrahabr.ru/post/314322/
Добавить комментарий