Конечные автоматы в JavaScript. Часть 1: Разработаем виджет (исходники)

Эдвард Принг

На протяжении нескольких лет web-дизайнеры без особого оживления использовали интерпретаторы JavaScript в популярных web-браузерах, чтобы улучшить внешний вид своих web-сайтов. Чаще всего для этого они копировали короткие фрагменты кода в свои web-страницы. Сейчас же, благодаря недавнему распространению технологий AJAX, инженеры по программному обеспечению стали, кроме того, использовать JavaScript для разработки нового поколения приложений, которые выполняются в web-браузере. Поскольку размер приложений на основе браузеров становится все больше, то для них все чаще требуются те же шаблоны проектирования и дисциплина разработки, которые характерны для других сред выполнения.

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

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

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

  • Функции - это объекты первого уровня : их можно создавать, назначать переменным и передавать в качестве аргумента, как и любые другие объекты. Функции могут быть определены в другой функции и назначены глобальным переменным или возвращены в качестве результата. Такие функции будут продолжать существовать после того, как будет возвращена функция, которая их определяет;
  • Функции могут ссылаться на любую переменную в своем лексическом контексте (вложенные фигурные скобки, в которые заключено описание функции), такие как локальные переменные функции, которые их определяют. Эти переменные становятся частью замыкания функции (функции, ее собственных переменных и любых переменных, которые она использует и которые определены где-либо в ее лексическом контексте), и они также продолжают существовать после того, как будет возвращена определяющая их функция;
  • Функции могут храниться в ассоциативных массивах (массивах, которые индексируются по именам, а не по номерам).

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

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

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

Обычные подсказки

Большинство современных приложений с графическим интерфейсом могут кратковременно отображать маленькие текстовые окна, содержащие полезные определения, инструкции или рекомендации, когда курсор останавливается над некоторыми визуальными элементами управления, такими как кнопка, поле выбора или поле ввода. Эти полезные текстовые окна получили название "balloon help" (всплывающие пояснения) в ранних системах Apple. В некоторых продуктах IBM они назывались infopops (всплывающей информацией), а в некоторых продуктах Microsoft - ScreenTips (экранными подсказками). В этой статье используется более общий термин подсказка.

Такие популярные web-браузеры, как Netscape Navigator, Microsoft Internet Explorer, Opera и Mozilla Firefox отображают подсказки для любого HTML- элемента, который имеет атрибут title. Например, в листинге 1 показано три HTML-элемента с атрибутами title.

Листинг 1. HTML-код для отображения подсказок браузером

                
Here are some 
<span title='Move your cursor a bit to the right, please.'>
fields with built-in tooltips
</span>: 
<input type='text' 
       title='Type your bank account and PIN numbers here, please ...' 
       size=25>
<input type='button' 
       title='Go ahead. Press it. What's the harm? Trust me.' 
       value='Press this button'>

Страница примеров показывает, как ваш браузер интерпретирует HTML-элементы с атрибутом title. Обратите внимание, что подсказки появляются и исчезают, когда вы перемещаете курсор над элементами. Текстовые окна содержат простой текст без какого-либо форматирования или стиля. Они появляются после короткой паузы в перемещении курсора и внезапно исчезают либо по истечении некоторого произвольного времени, либо после того, как курсор сместится с HTML- элемента, либо после нажатия какой-либо клавиши. Браузер никогда не отображает более одного текстового окна за один раз. Вид и поведение этих подсказок жестко встроены в ваш браузер; изменить их нельзя.

Более совершенные подсказки

Встроенные подсказки имеют много возможностей для улучшений, а последние версии популярных браузеров предоставляют все исходные ингредиенты для создания более совершенных подсказок. TML-элемент Division создает текстовое окно, которое можно расположить в любом окне браузера. Почти любой аспект внешнего вида этого окна можно определить с помощью каскадных таблиц стилей (CSS). Перемещения курсора, запрограммированные на языке JavaScript, могут запускать различные действия для любых видимых элементов в окне браузера. Для организации последовательности этих действий можно программировать таймеры.

На странице примеров показаны также некоторые HTML-элементы с более совершенными подсказками. Если у вас последняя версия одного из популярных браузеров, вы можете сравнить более совершенные подсказки со встроенными подсказками:

 
Если у вас устаревший браузер, то вы, возможно, не сможете увидеть все аспекты этого поведения. Например, в браузерах Opera до версии 9 подсказки просто появляются и исчезают, вместо того, чтобы делать это постепенно, потому что в Opera сравнительно поздно была добавлена реализация свойства стиля opacity (непрозрачность).
 
  • Они постепенно появляются, а впоследствии постепенно исчезают из виду, а не выскакивают и пропадают;
  • Они содержат текст и изображения, форматирование и стиль;
  • Когда они видимы, они перемещаются вместе с курсором;
  • Эффект плавного исчезновения изменяет направление на обратное (с исчезновения на появление), когда курсор смещается с HTML-элемента, и становится обычным, когда курсор возвращается на HTML-элемент;
  • Параллельно может отображаться более одной подсказки, одни из них исчезают, пока другие появляются.

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

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

Конечные автоматы

Конечные автоматы моделируют поведение, при котором реакции на будущие события зависят от предыдущих событий. Существует огромный пласт научной литературы по этой теме, но удобное рабочее определение очень простое. Конечный автомат - это компьютерная программа, которая состоит из:

  • Событий , на которые реагирует программа;
  • Состояний , в которых программа пребывает между событиями;
  • Переходов между состояниями при реагировании на события;
  • Действий , выполняемых в процессе переходов;
  • Переменных , которые содержат значения, необходимые для выполнения действий между событиями.

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

Состояния - это метод запоминания предыдущих событий, а переходы - метод организации реагирования на будущие события. Одно из событий должно быть помечено как исходное состояние. Может существовать также конечное состояние, но это необязательно, и у виджета FadingTooltip конечного состояния нет.

Существует два распространенных представления конечных автоматов:

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

Эти представления эквивалентны, но делают акцент на разных аспектах проекта. Оба представления полезны и используются в данной статье.

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

Конечные автоматы и среда выполнения

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

  • Тип наступившего события (например, перемещение курсора или истечение времени таймера);
  • Контекст события (например, над каким HTML-элементом находится курсор или какой сетевой запрос выполнен);
  • Размещение собственных переменных и методов конечных автоматов.

Язык JavaScript прекрасно подходит для построения управляемых событиями конечных автоматов. На самом деле, JavaScript, возможно, подходит для этого даже слишком хорошо -- в языке возможны три разных способа привязки событий. Каждая из этих моделей событий не отличается сложностью, однако, чтобы программы могли выполняться в любом популярном браузере, они должны реализовать все три модели. Контекст события передается непосредственно обработчикам событий по двум из этих моделей событий; для еще одной модели охватить контекст события обработчиком этого события позволяют замыкания функций JavaScript.

JavaScript предоставляет объектную модель , которая может показаться несколько необычной программистам на Java и C++, тем не менее, она полностью пригодна для программирования переменных и методов конечных автоматов. Кроме того, ассоциативные массивы JavaScript позволяют напрямую программировать представления конечных автоматов в виде двумерных таблиц.

Методичная разработка поведения

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

  • Может ли событие наступить в этом состоянии;
  • Какое действие следует выполнить для обработки событий;
  • В какое состояние будет выполнен переход после наступления события;
  • Какие переменные следует запомнить между событиями.

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

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

Эта методичная процедура проектирования конечных автоматов оправдана, хотя и утомительна. Заполненная таблица, которую вы видите на рисунке 4, показывает всю логику поведения и может быть переведена непосредственно в программный код (см. исходный код actionTransitionFunctions).

О возможностях JavaScript

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

Все популярные браузеры способны передавать события коду JavaScript, когда курсор проходит над HTML-элементом на web-странице. Эти события называются mouseover , mousemove и mouseout ; названия показывают, что курсор перемещается над, движется в пределах и смещается за пределы HTML-элемента. Браузер передает текущие координаты положения курсора вместе с этими событиями. При наступлении события программа на JavaScript может динамически создавать HTML-элементы Division, вносить в них текст, изображения и разметку, и помещать их рядом с курсором.

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

В JavaScript есть два типа таймеров: разовые таймеры, которые генерируют событие timeout по истечении установленного времени, и тикеры, которые генерируют события timetick периодически. Для виджета FadingTooltip нам понадобятся оба этих типа.

Рисуем набросок графа состояния

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

Нашему конечному автомату придется реагировать на такие события:

  • Браузер может передать события mouseover, mousemove и mouseout коду JavaScript соответственно при перемещении курсора над определенным HTML-элементом, в его пределах и случайного смещения с элемента;
  • В JavaScript можно запрограммировать события timeout, чтобы сигнализировать, когда курсор задерживается на достаточно продолжительное время или подсказка отображается достаточно долго, и события timetick, чтобы анимировать увеличение или уменьшение непрозрачности подсказки для имитации постепенного появления или исчезания.

Мы создадим некоторые состояния, в которых конечный автомат будет ожидать между событиями. Давайте назовем исходное состояние виджета Inactive; это такое состояние, при котором он будет ожидать активации событием mouseover. В состоянии Pause виджет будет ожидать, пока событие timeout не покажет, что курсор задержался над HTML-элементом на достаточно продолжительное время. Затем, в состоянии FadeIn, виджет будет ожидать, пока выполняется анимирование эффекта постепенного появления событиями timetick, и далее, в состоянии Display, ожидать наступления следующего события timeout. Наконец, в состоянии FadeOut виджет будет ожидать, пока выполняется анимирование эффекта постепенного исчезновения следующими событиями timetick. После этого виджет возвращается в состояние Inactive, в котором он будет ожидать следующего события mouseover.

На рисунке 1 этот процесс отображен в виде графа, на котором состояния отображены в виде эллипсов, переходы в виде соединяющих эти эллипсы стрелок, а события - как надписи над стрелками. Двойной обводкой выделен эллипс исходного состояния.

Рисунок 1. Первоначальный набросок графа состояний
Первоначальный набросок графа состояний

Виджету FadingTooltip нужно выполнять некоторые действия для каждого события, которое он обработает.

  • При наступлении события mouseover в состоянии Inactive ему нужно будет запустить разовый таймер до того, как перейти к ожиданию в состоянии Pause:
  • При наступлении события timeout виджету нужно будет создать подсказку (с начальной непрозрачностью равной нулю) и запустить тикер до перехода к ожиданию в состоянии FadeIn;
  • При каждом наступлении события timetick ему нужно будет слегка увеличивать непрозрачность подсказки. По достижении максимальной непрозрачности он должен будет отменить таймер и запустить другой таймер до перехода к ожиданию в состоянии Display;
  • При наступлении события timeout виджет должен запустить другой тикер до перехода к ожиданию в состоянии FadeOut;
  • При каждом наступлении события timetick в состоянии FadeOut виджету нужно будет слегка уменьшать непрозрачность подсказки. По достижении нулевой непрозрачности подсказки виджет должен отменить тикер, удалить подсказку и вернуться в состояние Inactive, в котором он будет ожидать следующей активации другим событием mouseover.

На рисунке 2 эти действия показаны под событиями, которые их запускают.

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

Перевод графа состояния в таблицу состояния

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

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

Рисунок 3. Первоначальная таблица состояний, соответствующая графу состояний
Первоначальная таблица состояний, соответствующая графу состояний

Заполняем таблицу состояний

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

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

Состояние Inactive
Во время этого состояния должно наступить только инициирующее событие, которое мы уже рассмотрели, поскольку событиям mousemove и mouseout должно предшествовать событие mouseover, причем ни один таймер не должен быть запущен. Поэтому можно написать во всех остальных ячейках первого столбца "не должно наступить".

Впрочем, прежде, чем двигаться дальше, давайте продумаем событие mouseover для этого состояния на несколько шагов вперед. Когда, в конце концов, будет создан HTML-элемент Division для подсказки, вам потребуется поместить его рядом с курсором, поэтому нужно сохранить текущее положение курсора, которое браузер передает вместе с событием. Кроме того, неплохой идеей будет отмена всех запущенных таймеров перед запуском нового таймера. Добавьте эти действия в ячейку mouseover.

Состояние Pause

Ожидая, пока истечет время таймера, курсор может перемещаться в пределах HTML-элемента или сместиться за его пределы. Решите, какие действия следует выполнить, если наступят эти события, и каким должно быть следующее состояние. Если в этом состоянии наступит событие mouseout, вам будет нужно, чтобы виджет FadingTooltip вернулся в состояние Inactive state, как если бы вообще не проходил над HTML-элементом, но вам придется отменить таймер. Запишите это действие и переход в ячейку mouseout.

С другой стороны, для события mousemove нужно, чтобы виджет продолжал ожидать остановки курсора над элементом, для чего необходимо отменить и перезапустить таймер. Поскольку необходимо, чтобы подсказка появлялась рядом с курсором, придется обновить сохраненное положение курсора. При пересмотре вы можете сделать так, чтобы действия и переход для события mousemove в состоянии Pause были такими же, как для события mouseover в состоянии Inactive. Вместо того, чтобы повторять всю информацию в обеих ячейках, запишите информацию о повторении прямо в ячейку mousemove. Во всех остальных ячейках этого столбца напишите "не должно наступить".

Состояние FadeIn
Во время этого состояния, в процессе имитации постепенного усиления при помощи событий timetick, курсор может продолжать перемещаться в пределах HTML-элемента. Если наступит событие mousemove, переместите подсказку до совмещения с курсором и оставьте в этом состоянии. Если наступит событие mouseout, выполните переход в состояние FadeOut при работающем тикере, чтобы события timetick увеличивали непрозрачность подсказки по сравнению с текущим значением. Запишите эти события и переходы в соответствющие ячейки, а в остальных ячейках этого столбца напишите "не должно наступить".
Состояние Display
Курсор, безусловно, может продолжать перемещаться. Если он перемещается внутри HTML-элемента, то вам необходимо то же действие, что и в состоянии FadeIn - перемещение подсказки до совмещения с положением курсора. Если курсор смещается за пределы HTML-элемента, то выполните то же действие и тот же переход, что и для события timeout в состоянии Display. Отметьте оба этих повторения непосредственно в ячейках mousemove и mouseout, а в остальных ячейках напишите "не должно наступить".
Состояние FadeOut
Во время этого состояния, при имитации постепенного исчезновения подсказки с помощью событий timetick, курсор может продолжать перемещаться. Если он перемещается в пределах HTML-элемента, выполните то же действие, что и для состояний FadeIn и Display. Если мышь выходит за пределы HTML-элемента, то вам ничего не придется делать -- тикер будет продолжать работать, чтобы следующие события timetick уменьшали непрозрачность по сравнению с текущим значением, пока оно не достигнет нуля.

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

На рисунке 4 показаны все эти дополнительные действия и переходы. Ячейки, оставшиеся пустыми, соответствуют ситуации "не должно наступить".

Рисунок 4. Таблица состояний для виджета FadingTooltip widget после разработки
Таблица состояний для виджета FadingTooltip widget после разработки

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

Рисунок 5. Граф состояний для виджета FadingTooltip после разработки
Граф состояний для виджета FadingTooltip после разработки

Составление списка переменных состояния

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

Листинг 2. Первоначальный список переменных состояний

                
currentState         string value equal to one of the state names
currentTimer         pointer to timer object, obtained when set, used to cancel
currentTicker        pointer to ticker object, obtained when started, used to cancel
currentOpacity       float that varies from 0.0 (invisible) to 1.0 (fully visible)
lastCursorPosition   floats obtained from cursor events, used when an HTML Division 
                       element is created
tooltipDivision      pointer to HTML Division element, set when created, used when 
                       faded, moved, or deleted

Хотя переменные JavaScript являются нетипизированными, значения, которые они содержат, типизированы (то есть, значения любого типа могут быть присвоены любой переменной). В этом же духе я выберу имена для переменных состояния и, в примечаниях, укажу типы, которые я собираюсь им присвоить.

Все готово для реализации проекта

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


Страница сайта http://www.interface.ru
Оригинал находится по адресу http://www.interface.ru/home.asp?artId=7648