Как мы писали фронтенд собственной панели управления хостингом: фреймворк и бэкдоры
Время на прочтение
6 мин
Количество просмотров 8.6K
В прошлой статье мы рассказывали, как пришли к идее написать собственную панель управления хостингом и об общей структуре готовой панели.
Сегодня наш фронтенд разработчик Артыш расскажет, как писал фронтенд этой панели: какой выбрали фреймворк, какой антипаттерн считаем хорошим тоном и как защищаться от бэкдоров, если пользуетесь готовыми библиотеками.
Выбор фреймворка: почему искали новый
Предыдущая панель была реализована на собственном фреймворке, написанном на jQuery. Мы сидели на VMManager, он требовал много доработок: по интерфейсу и функционалу, было тяжело сопровождать такой код. Добавление нового функционала в панель со стороны фронта занимало много времени. Понятно, что при желании и на jQuery можно реализовать хороший фреймворк (я до сих пор люблю jQuery) или даже подобие CMS, но это был не оптимальный вариант: начиная скудной документацией по самописному фреймворку и заканчивая не совсем корректной архитектурой самого приложения.
Старая панель была реализована в виде Single Page Application и на этом его хорошие качества заканчивались. После решения очередной головоломки по добавлению кнопки в список, пришло понимание, что нужна альтернатива. Выбор пал на Vue.
Почему SPA?
Single Page Application — идеальный выбор для панели управления. Панель управления в плане рендеринга довольно простая штука, эту работу можно легко доверить браузеру пользователя. К тому же панели не важна SEO-оптимизация, для этого у нас есть основной сайт. Ну и требуемое время на начальную загрузку всех необходимых скриптов пользователи панели воспринимают спокойно в силу специфики самих этих пользователей. Опять же, бекенд у нас получился классическим RestAPI сервисом — для предоставления в будущем открытого API нашим клиентам.
SPA приложение получилось таким легким, что хорошо работает с браузера телефонов и планшетов — мы просто сделали адаптивную верстку и создавать отдельное приложение не пришлось.
Почему Vue?
3 года назад Vue был относительно молодым фреймворком, но уже тогда о нем много говорили и писали, и когда вышел релиз версии 2.0, мы решили сделать ставку на него — и не прогадали. Сначала планировали просто постепенно заменять какие-то компоненты написанные на jQuery и Vue это позволял делать легко. Но потом, после того, как были переписаны довольно объемные компоненты, все таки решили, что лучше переписать вообще все приложение на Vue.
Это бы рискованный шаг и мы решили его сделать по 4 причинам:
- Vue — простой декларативный фреймворк, его понимают даже верстальщики. Если что, под него легко найти разработчика или просто научить товарища. А значит у нас не будет проблем с поиском нового разработчика и его вхождением в проект, если меня переедет трамвай (хвала богам, в моем городе их нет).
- Vue объективно хорош для написания SPA приложений.
- У меня перед глазами был опыт развития React и я предположил, что популярность Vue будет расти так же. Сейчас фреймворк входит в TOP-3 популярных JS-фреймворков (это легко проверить поисковым запросом), уступая только React и Angular. У него хорошая поддержка, развитая экосистема и большое комьюнити.
- Скорость разработки. Лично я сразу стал воспринимать Vue как этакий конструктор и разработка на нем идет довольно быстро: если мне нужен, например, компонент выбора даты, скорее всего на Vue он уже существует, свободен в использовании и опробован сообществом. Я просто устанавливаю компонент в проект, пишу тег и все работает. По сути, наша панель состоит на 70-80% из готовых библиотек. Я имею в виду именно использование компонента, а не размер кодовой базы, который можно проверить командой типа: npx intrinsic/loc
При реализации проекта всегда учитываешь его перспективы, особенно перспективы развития. И то, что в экосистеме Vue уже имеются такие инструменты как Weex, Quasar Framework или Nuxt по мне существенно расширяют горизонты развития нашей панели.
На Хабре есть замечательная статья о Vue от его создателя, а я расскажу о некоторых особенностях нашего приложения.
Синхронизация Vuex с сервисом RestAPI
Часть данных глобального хранилища Vuex в нашем приложении синхронизируется с RestAPI путем обыкновенных запросов по соответствующим адресам. Зачем мы так сделали? Ну хотя бы для того, чтобы основные настройки пользователя не были привязаны к конкретному браузеру конкретного устройства. Можно зайти в нашу панель с компьютера жены или из игрового клуба и при этом получить в то же знакомое окружение, что и было у вас на своей родной машине.
Кроме того, когда синхронизация была только с localStorage, некоторые браузеры при обновлениях теряли содержимое localStorage — оно полностью удалялось. Да и в последнее время прослеживается какая то тенденция к ужесточению политики хранения данных пользователей в куках, например функция в WebKit Intelligent Tracking Prevention — не ровен час они доберутся и до localStorage.
Шина событий
Да, мы используем глобальную шину событий. Как и в любом другом крупном приложении с множеством компонентов, рано или поздно возникает необходимость наладить взаимодействие между не связанными между собой компонентами. Даже через глобальное хранилище. Понятно, что если есть связь родитель-потомок, их взаимодействие стандартно организуется через свойства props в одну сторону и методом $emit в другую, ну или через хранилище, как и описано в рекомендациях Vue.
Но в документации описана и возможность использования глобальной шины событий. У нас в проекте куча форм с разными наборами полей и в некоторых случаях (их немного, но все из них принципиальные) нужно как-то по особенному реагировать на изменение значения поля. Хранить в глобальном хранилище значения всех полей каждой формы не имеет смысла:
- Во-первых, из-за редкой необходимости
- Во-вторых, все наши формы генерируются динамически и набор полей у любой формы может поменяться кардинально.
Поэтому я решил использовать механизм шины событий. При этом ничто не мешает использовать свой Event emitter — главное использовать этот механизм аккуратно, только для исключительных ситуаций и тщательно все подчищать за собой.
Взаимодействие RestAPI с панелью
Для большей отзывчивости интерфейса в старом jQuery-фреймворке обратная связь от RestAPI к клиентскому приложению эмулировалась через хитрую систему таймеров: она производила опросы RestAPI с определенным интервалом и перерисовывала узлы DOM, которые затронули изменения.
Это не было идеальным решением: во всех современных браузерах таймеры практически полностью замирают, когда вкладка становится неактивной и получает низкий приоритет. Как результат запрос к RestAPI сервису может запоздать и получить уже неактуальные данные.
Для решения этой проблемы в новой панели я решил использовать связку из модуля Nchan для веб сервера Nginx и новых возможностей HTML5-интерфейсов — EventSource и WebWorker.
Модуль Nchan поддерживает отправку сообщений через Websocket, EventSource и Long-Polling. Я провел несколько тестов и решил использовать EventSource: сообщения могут быть только текстовыми и поток сообщений осуществляется только в одну сторону (от сервера). Это полностью решало поставленную задачу.
Сейчас работу интерфейса EventSource осуществляет в отдельном фоновом потоке WebWorker, независимо от активности вкладки. В этом же потоке организована примитивная очередь сообщений, чтобы ничего не потерялось. Очередь отправляется в основной поток приложения, который свою очередь производит необходимые перерисовки интерфейса, когда ему удобно и позволяет браузер.
Бэкдоры: как и зачем я проверяю безопасность компонентов
Перед подключением библиотеки ее обязательно проверить на безопасность: был случай, когда компонент специально внедрили бэкдор, который позволял заходить на сайт и скачивать данные.
Но чаще дыры в безопасности появляются скорее по неаккуратности разработчиков. В маркетах приложений есть команда, которая проверяет компоненты на безопасность, но она не слишком зубастая и библиотеки лучше проверять вручную.
Я всегда проверяю пакеты на наличие хуков preinstall, install и postinstall в поле “scripts” файла “package.json”. Кроме того, использую статические анализаторы пакетов, такие как retire, snyk и команду “audit” пакетного менеджера npm.
Предупреждения безопасности бывают разных уровней, чаще всего при проверке попадаются некритичные. Иногда, чтобы пролечить библиотеку достаточно обновиться — разработчики библиотек сами следят за безопасностью.
Если библиотека однажды себя скомпрометировала, лучше ее заменить — это признак неблагонадежности, поэтому при предупреждениях я выбираю искать другую библиотеку.
После того, как пакет прошел анализ, я обязательно фиксирую его версию. Если нужна другая версия — пакет проходит анализ заново. Да, это занимает время, но оно того стоит.
Пока бэкдоры ни разу не попадали к нам на продакшн.
Много-много комментариев
Как я уже говорил, Vue был выбран за простоту и декларативность. В дополнение к этому я пишу много комментариев, практически к каждой строчке: чтобы в случае чего новый разработчик мог легко войти в проект и чтобы я сам легко возвращался к старым кускам кода.
За что я полюбил новый фронтенд и панель в целом
Стало проще поддерживать код
Разработка заняла полгода. Теперь я скорее занимаюсь поддержкой панели, свой код не жмет и не натирает.
Клиенты могут быстро получать то, что запрашивали
Стало быстро и удобно добавлять новые функции, которые появились в бекенде: например, оплату для юридических лиц я добавил за 2 дня, снепшоты — за 1 день.
Я открыт к вопросам
В этой статье я приоткрыл часть секретов, связанных с фронтендом нашей панели. Если у вас возникли вопросы ― добро пожаловать в комментарии, я постараюсь ответить.
И, конечно, приглашаю высказать пожелания по улучшению панели.
От автора: в этом руководстве мы создадим адаптивный макет панели администрирования с помощью CSS и немного JavaScript. Чтобы создать его, мы позаимствуем некоторые идеи из панели управления WordPress, например, ее боковое меню. В процессе создания мы столкнемся со множеством проблем, но они дадут нам хорошую практику для повышения навыков.
Что мы будем создавать
Без дальнейших предисловий, давайте посмотрим на окончательную демонстрацию панели администрирования (нажмите кнопку Collapse в нижней части боковой панели, чтобы увидеть раскрывающееся меню в действии, и посмотрите полноэкранную версию, чтобы поэкспериментировать с адаптивностью):
1. Начинаем с разметки страницы
Для начала нам понадобятся SVG, header и section:
<svg style=“display:none;”>...</svg> <header class=“page-header”>...</header> <section class=“page-content”>...</section> |
SVG-спрайты
Как вы можете представить, нам понадобится куча иконок. К счастью, Envato Elements предоставляет постоянно расширяющуюся коллекцию полезных векторных иконок, поэтому давайте воспользуемся этой библиотекой и загрузим эти Trade and Dashboard Icons.
Вместо того, чтобы включать их непосредственно в страницу с помощью тега img или svg, давайте создадим SVG-спрайт. Для этого мы обернем все иконки в контейнер SVG. Контейнер должен быть скрытым, поэтому мы применим к нему display: none. Если мы не скроем его, в верхней части страницы появится большая пустая область.
Каждая иконка будет размещена внутри элемента symbol с уникальным идентификатором и атрибутом viewBox, который будет зависеть от размера иконки. Затем мы можем отобразить целевую иконку в любое время, вызвав элемент use (я покажу, как это делается чуть позже).
А пока давайте просто ознакомимся с разметкой, необходимой для SVG-спрайта:
<svg style=“display:none;”> <symbol id=“down” viewBox=“0 0 16 16”> <polygon points=“3.81 4.38 8 8.57 12.19 4.38 13.71 5.91 8 11.62 2.29 5.91 3.81 4.38” /> </symbol> <symbol id=“users” viewBox=“0 0 16 16”> <path d=“M8,0a8,8,0,1,0,8,8A8,8,0,0,0,8,0ZM8,15a7,7,0,0,1-5.19-2.32,2.71,2.71,0,0,1,1.7-1,13.11,13.11,0,0,0,1.29-.28,2.32,2.32,0,0,0,.94-.34,1.17,1.17,0,0,0-.27-.7h0A3.61,3.61,0,0,1,5.15,7.49,3.18,3.18,0,0,1,8,4.07a3.18,3.18,0,0,1,2.86,3.42,3.6,3.6,0,0,1-1.32,2.88h0a1.13,1.13,0,0,0-.27.69,2.68,2.68,0,0,0,.93.31,10.81,10.81,0,0,0,1.28.23,2.63,2.63,0,0,1,1.78,1A7,7,0,0,1,8,15Z” /> </symbol> <!— more symbols here —> </svg> |
И это все, что нам нужно для создания встроенного SVG-спрайта.
Header
Перейдем к макету панели администрирования, давайте посмотрим на header страницы. В нем мы определим элемент nav, который будет выполнять роль оболочки для следующих элементов:
Логотип
Кнопка Collapse, которая переключает меню на экранах мобильных устройств.
Само меню, которое будет содержать ссылки меню, два заголовка и кнопку свертывания / развертывания. Возможно, было бы более семантически верно создать два отдельных меню и размещать заголовки вне их, но вы можете использовать подход, который нравится вам.
Вот как это будет выглядеть на широких экранах (> 767 пикселей):
Структура заголовка:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
<header class=“page-header”> <nav> <a href=“#0”> <img class=“logo” src=“logo.svg” alt=“forecastr logo”> </a> <button class=“toggle-mob-menu” aria–expanded=“false” aria–label=“open menu”> <svg width=“20” height=“20” aria–hidden=“true”> <use xlink:href=“#down”></use> </svg> </button> <ul class=“admin-menu”> <li class=“menu-heading”> <h3>Admin</h3> </li> <li> <a href=“#0”> <svg> <use xlink:href=“#pages”></use> </svg> <span>Pages</span> </a> </li> <!— more list items here —> <li> <button class=“collapse-btn” aria–expanded=“true” aria–label=“collapse menu”> <svg aria–hidden=“true”> <use xlink:href=“#collapse”></use> </svg> <span>Collapse</span> </button> </li> </ul> </nav> </header> |
Обратите внимание на две вещи:
Как мы используем use элемент для ссылки на целевые иконки.
Атрибуты ARIA (aria-expanded, aria-label, aria-hidden) , которые мы добавляем к кнопкам переключения. Эти атрибуты помогут нам сделать компонент немного более доступным. Позже мы рассмотрим, как их значения будут обновляться в зависимости от состояния кнопки.
Section
Section будет содержать два вложенных раздела.
Section 1
Внутри первого раздела мы разместим форму поиска и некоторую информацию (имя, аватар и уведомления) о текущем вошедшем в систему пользователе. Вот его отображение на широких экранах (> 767 пикселей):
Структура Section:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<section class=“search-and-user”> <form> <input type=“search” placeholder=“Search Pages…”> <button type=“submit” aria–label=“submit form”> <svg aria–hidden=“true”> <use xlink:href=“#search”></use> </svg> </button> </form> <div class=“admin-profile”> <span class=“greeting”>...</span> <div class=“notifications”> <span class=“badge”>...</span> <svg> <use xlink:href=“#users”></use> </svg> </div> </div> </section> |
Опять же, обратите внимание, что мы добавляем некоторые атрибуты ARIA к кнопке отправки.
Section 2
Во втором разделе, просто для того, чтобы заполнить демо-версию некоторым фиктивным контентом, мы разместим несколько заполнителей статей. Как правило, они могут содержать табличные данные, диаграммы или каналы какого-либо рода.
«Используйте максимум 5–7 различных виджетов для создания представления. В противном случае пользователю будет сложно сосредоточиться и получить четкое понимание». — Тарас Бакусевич
Вот, как панель отображается на широких экранах (> 767 пикселей):
Структура раздела:
<section class=“page-content”> <section class=“grid”> <article></article> <article></article> <article></article> <article></article> <article></article> <article></article> <article></article> <article></article> </section> </section> |
2. Определение основных стилей
Когда разметка для панели администрирования будет готова, мы перейдем к CSS. Первый шаг, как всегда, заключается в указании некоторых переменных CSS и общих стилей сброса:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
:root { –page-header-bgColor: #242e42; –page-header-bgColor-hover: #1d2636; –page-header-txtColor: #dde9f8; –page-header-headingColor: #7889a4; –page-header-width: 220px; –page-content-bgColor: #f0f1f6; –page-content-txtColor: #171616; –page-content-blockColor: #fff; –white: #fff; –black: #333; –blue: #00b9eb; –red: #ec1848; –border-radius: 4px; –box-shadow: 0 0 10px -2px rgba(0, 0, 0, 0.075); } * { padding: 0; margin: 0; box-sizing: border-box; } ul { list-style: none; } a, button { color: inherit; } a { text-decoration: none; } button { background: none; cursor: pointer; } input { -webkit-appearance: none; } button, input { border: none; } svg { display: block; } body { font: 16px/1.5 “Lato”, sans-serif; } |
Примечание: Для простоты я не буду рассматривать все правила CSS в руководстве. Здесь почти 400 строк CSS. Если вы хотите, вы можете проверить их все, нажав на вкладку CSS демонстрационного проекта.
3. Определение основных стилей панели администрирования
На данный момент мы готовы сосредоточиться на стилях страницы.
Стили header
Header будет элементом с фиксированной позицией. Его ширина будет 220px, а высота равна высоте окна просмотра. Если его содержимое превышает высоту области просмотра, появится вертикальная полоса прокрутки. Элемент nav будет вести себя как flex-контейнер с минимальной высотой 100%. Помните, что у него три дочерних элемента:
логотип,
кнопка переключения мобильного меню,
и меню.
Кнопка переключения будет видна только на маленьких экранах (< 768 пикселей). Вот стили, которые нам нужны:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
/*CUSTOM VARIABLES HERE*/ .page-header { position: fixed; top: 0; left: 0; right: 0; bottom: 0; overflow: auto; padding-top: 20px; width: var(–page-header-width); color: var(–page-header-txtColor); background: var(–page-header-bgColor); } .page-header nav { display: flex; flex-direction: column; min-height: 100%; } .page-header .toggle-mob-menu { display: none; } |
Совет: Если вы предпочитаете абсолютно позиционированный заголовок, который охватывает всю высоту страницы, добавьте следующие стили:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
body { position: relative; } .page-header { position: absolute; top: 0; left: 0; height: 100%; /*Comment these styles*/ /*position: fixed; top: 0; left: 0; right: 0; bottom: 0; overflow: auto;*/ } |
Стили меню
Меню будет flex-контейнером, и мы зададим для него flex: 1, чтобы оно расширялось и охватывало всю высоту родительского контейнера.
Для последнего пункта меню мы зададим margin-top: auto, потому что он должен быть расположен в самом низу меню. Это поведение будет понятнее, когда нет полосы прокрутки заголовка. Чтобы проверить это, попробуйте удалить некоторые пункты меню или посмотреть демонстрацию на большом экране.
Ссылки и кнопка внутри меню также будут действовать как flex-контейнеры, а их содержимое (текст и иконки) должны быть выровнены по вертикали.
Заголовки меню будут немного меньше по сравнению с другими элементами меню. Кроме того, мы увеличим для них расстояние между символами. Вот часть стилей меню:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
/*CUSTOM VARIABLES HERE*/ .page-header .admin-menu { display: flex; flex-direction: column; flex-grow: 1; margin-top: 35px; } .page-header .admin-menu li:last-child { margin-top: auto; margin-bottom: 20px; } .page-header .admin-menu li > * { width: 100%; padding: 12px 15px; } .page-header .admin-menu a, .page-header .admin-menu button { display: flex; align-items: center; font-size: 0.9rem; transition: background 0.2s, color 0.2s; } .page-header .admin-menu .menu-heading h3 { text-transform: uppercase; letter-spacing: 0.15em; font-size: 12px; margin-top: 12px; color: var(–page-header-headingColor); } |
Стили содержимого страницы
Помните, что раздел .page-content содержит два подраздела. Этот раздел будет размещен на расстоянии 220px от левого края области просмотра. Плюс мы зададим для него width: calc(100% — 220px). Обратите внимание, что значение его свойства left равно ширине заголовка. Его стили:
/*CUSTOM VARIABLES HERE*/ .page-content { position: relative; left: var(–page-header-width); width: calc(100% – var(–page-header-width)); min-height: 100vh; padding: 30px; color: var(–page-content-txtColor); background: var(–page-content-bgColor); } |
Поиск и пользовательские стили
Также помните, что раздел .search-and-user содержит два элемента: форму поиска и .admin-profile. Чтобы разместить их, мы будем использовать CSS Grid. Форма поиска будет охватывать все доступное пространство, и между ней и ее смежным элементом будет зазор в 50 пикселей. Оба элемента будут выровнены вертикально.
Кнопка отправки внутри формы будет позиционирована абсолютно. Она будет содержать только декоративную иконку, и поэтому нам понадобится атрибут ARIA, который позволит программам чтения с экрана интерпретировать ее и тем самым сделать ее доступной.
Объект .admin-profile, содержащий два элемента, будет вести себя как flex-контейнер с вертикально центрированным содержимым. Элемент badge (counter) будет абсолютно позиционирован внутри своего родителя с горизонтально и вертикально центрированным содержимым. Вот часть необходимых стилей для этого раздела:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
/*CUSTOM VARIABLES HERE*/ .search-and-user { display: grid; grid-template-columns: 1fr auto; grid-column-gap: 50px; align-items: center; background: var(–page-content-bgColor); margin-bottom: 30px; } .search-and-user form { position: relative; } .search-and-user form button { position: absolute; top: 50%; right: 15px; transform: translateY(-50%); } .search-and-user .admin-profile { display: flex; align-items: center; } .search-and-user .admin-profile .notifications { position: relative; } .search-and-user .admin-profile .badge { display: flex; align-items: center; justify-content: center; position: absolute; top: -10px; right: -3px; width: 18px; height: 18px; border-radius: 50%; font-size: 10px; color: var(–white); background: var(–red); } |
Стили сетки
Чтобы разместить статьи панели администрирования, мы воспользуемся CSS сеткой. Мы зададим всем статьям фиксированную высоту 300 пикселей. Помимо первой и последней статей, которые будут охватывать всю ширину родительского элемента, все остальные будут частью макета из двух столбцов. Связанные стили:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
/*CUSTOM VARIABLES HERE*/ .page-content .grid { display: grid; grid-template-columns: repeat(2, 1fr); grid-gap: 30px; } .page-content .grid > article { display: flex; height: 300px; background: var(–page-content-blockColor); border-radius: var(–border-radius); box-shadow: var(–box-shadow); } .page-content .grid > article:first-child, .page-content .grid > article:last-child { grid-column: 1 / -1; } |
4. Переключение заголовка
Каждый раз, когда мы нажимаем кнопку «Свернуть / развернуть», состояние заголовка будет меняться.
Имейте в виду, что эта функция будет доступна только на экранах с разрешением более 767 пикселей. Для небольших экранов наш заголовок будет иметь другую компоновку, о которой мы вскоре расскажем.
При свернутом состояния заголовка тело получает класс collapsed. В этот момент происходят следующие вещи:
Заголовок сжимается. Его ширина уменьшается с 220 до 40 пикселей.
В ответ на это раздел .page-content увеличивается. В частности, его ширина меняется с width: calc(100% — 220px) на width: calc(100% — 40px). Кроме того, значение его свойства left становится 40px вместо 220px.
Логотип, заголовки меню, текст ссылок меню и текст кнопки меню исчезают.
Значении атрибутов aria-expanded и aria-label переключателя обновляется. Кроме того, его значок поворачивается на 180 градусов, поэтому он выглядит как значок расширения.
Вот код JavaScript, который реализует это поведение:
const body = document.body; const collapseBtn = document.querySelector(“.admin-menu button”); const collapsedClass = “collapsed”; collapseBtn.addEventListener(“click”, function() { this.getAttribute(“aria-expanded”) == “true” ? this.setAttribute(“aria-expanded”, “false”) : this.setAttribute(“aria-expanded”, “true”); this.getAttribute(“aria-label”) == “collapse menu” ? this.setAttribute(“aria-label”, “expand menu”) : this.setAttribute(“aria-label”, “collapse menu”); body.classList.toggle(collapsedClass); }); |
И все связанные стили:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
/*CUSTOM VARIABLES HERE*/ @media screen and (min-width: 768px) { .collapsed .page-header { width: 40px; } .collapsed .page-header .admin-menu li > * { padding: 10px; } .collapsed .page-header .logo, .collapsed .page-header .admin-menu span, .collapsed .page-header .admin-menu .menu-heading { display: none; } .collapsed .page-header .admin-menu svg { margin-right: 0; } .collapsed .page-header .collapse-btn svg { transform: rotate(180deg); } .collapsed .page-content { left: 40px; width: calc(100% – 40px); } } |
5. Отображение всплывающей подсказки в пунктах меню
Теперь давайте добавим еще одну новую функцию в заголовок. Как мы уже говорили в предыдущем разделе, когда заголовок свернут, текст ссылок меню исчезнет. Это означает, что в этот момент будут видны только значки SVG. Итак, давайте отобразим всплывающую подсказку, которая поможет пользователям лучше понять, что делает каждая ссылка.
Для этого при наведении на ссылку меню (иконку) мы добавляем ей атрибут title, значение которого представляет собой обычный текст. Но опять же, это должно происходить только тогда, когда заголовок свернут и ширина окна составляет не менее 768 пикселей.
Вот соответствующий JavaScript:
const body = document.body; const menuLinks = document.querySelectorAll(“.admin-menu a”); const collapsedClass = “collapsed”; for (const link of menuLinks) { link.addEventListener(“mouseenter”, function() { body.classList.contains(collapsedClass) && window.matchMedia(“(min-width: 768px)”).matches ? this.setAttribute(“title”, this.textContent) : this.removeAttribute(“title”); }); } |
6. Адаптивность
На экранах шириной до 767 пикселей наша страница будет выглядеть следующим образом:
Это большая разница с нашей боковой панелью, верно? Давайте выделим наиболее важные различия по сравнению с настольной версией:
И заголовок, и .page-content имеют position: static и width: 100%.
flex-направление для элемента nav изменяется с column на row.
Кнопка переключения мобильного меню становится видимой.
Меню абсолютно позиционировано прямо под заголовком и изначально скрыто. Оно будет отображаться каждый раз, когда мы нажимаем на кнопку переключения.
Кнопка свертывания / развертывания и элемент .greeting скрыты.
Раздел .search-and-user абсолютно позиционирован и помещен рядом с кнопкой переключения мобильного меню.
Ниже вы можете увидеть часть адаптивных стилей:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
@media screen and (max-width: 767px) { .page-header, .page-content { position: static; width: 100%; } .page-header nav { flex-direction: row; } .page-header .toggle-mob-menu { display: block; } .page-header .admin-menu { position: absolute; left: 98px; top: 57px; margin-top: 0; z-index: 2; border-radius: var(–border-radius); background: var(–page-header-bgColor); visibility: hidden; opacity: 0; transform: scale(0.95); transition: all 0.2s; } .page-header .admin-menu li:last-child, .search-and-user .admin-profile .greeting { display: none; } .search-and-user { position: absolute; left: 131px; top: 10px; padding: 0; grid-column-gap: 5px; width: calc(100% – 141px); border-radius: var(–border-radius); background: transparent; } } |
7. Переключение мобильного меню
Каждый раз, когда мы нажимаем на кнопку переключения, состояние меню будет меняться. Если его развернуть, оно раздвинется, и наоборот.
В расширенном состоянии тело меню получает класс mob-menu-opened. В этот момент происходят следующие вещи:
Меню отображается.
Значении атрибутов aria-expanded и aria-label переключателя обновляются. Кроме того, его иконка поворачивается на 180 градусов, и выглядит, как иконка расширения.
Вот необходимый код JavaScript:
const body = document.body; const toggleMobileMenu = document.querySelector(“.toggle-mob-menu”); toggleMobileMenu.addEventListener(“click”, function() { this.getAttribute(“aria-expanded”) == “true” ? this.setAttribute(“aria-expanded”, “false”) : this.setAttribute(“aria-expanded”, “true”); this.getAttribute(“aria-label”) == “open menu” ? this.setAttribute(“aria-label”, “close menu”) : this.setAttribute(“aria-label”, “open menu”); body.classList.toggle(“mob-menu-opened”); }); |
И связанный CSS:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
.page-header .toggle-mob-menu svg { transition: transform 0.2s; } .page-header .admin-menu { transition: all 0.2s; } .mob-menu-opened .toggle-mob-menu svg { transform: rotate(180deg); } .mob-menu-opened .page-header .admin-menu { transform: scale(1); visibility: visible; opacity: 1; } |
Заключение
Вот и все, ребята! Мы успешно создали полнофункциональный макет панели администрирования. Вы сможете расширить эту основу для создания интерфейсов различных видов. Надеюсь, вам понравилось это руководство.
Просто заметка напоследок. Я, конечно, не эксперт по доступности, но я попытался сделать этот компонент более доступным, добавив некоторые общие атрибуты ARIA. Во время этого процесса я посмотрел для справки информационные панели WordPress и Codepen. Мы могли бы включить в код и другие атрибуты ARIA. Например, я исключил атрибут aria-controls, который отвечает за идентификацию связанного содержимого, но это произошло потому, что Aria-Controls — это Poop.
Если я что-то пропустил или вы думаете, что некоторые вещи должны были быть сделаны по-другому, дайте мне знать в комментариях ниже. Как всегда, спасибо за прочтение!
Автор: George Martsoukos
Источник: //webdesign.tutsplus.com
Редакция: Команда webformyself.
#Руководства
- 7 мар 2019
-
12
Узнайте, для чего нужна панель управления сайтом, какие в ней есть функции и как её разработать самостоятельно.
vlada_maestro / shutterstock
Пишет о программировании, в свободное время создаёт игры. Мечтает открыть свою студию и выпускать ламповые RPG.
Инструменты для управления сайтом:
- phpmyadmin для работы с базой данных;
- FTP-клиент, чтобы загружать или удалять файлы;
- графический редактор, который сжимает изображения;
- текстовый редактор, в котором оформляются статьи;
- сервис аналитики для оценки эффективности контента и рекламы.
Лучше всего собрать их в одном месте, которое и называется панелью управления сайтом.
В первую очередь создадим файл admin.php в корне сайта. Пока у него будет такой вид:
<? include("includes/db.php");
$echo = "Тут скоро что-то будет";
?>
<!DOCTYPE html>
<html>
<head>
<title>Админка</title>
<link rel="stylesheet" href="admin.css">
</head>
<body>
<div class='wrapper'>
<main class='main' id='main'>
<?echo $echo;?>
</main>
</div>
</body>
</html>
Этот код получает данные методами GET и POST, обрабатывает их, а потом выводит что-либо через переменную $echo. Чтобы поисковики не пытались зайти в админку, запретите её индексировать в файле robots.txt:
Robot.txt
User-agent: *
Disallow: /admin.php
- Авторизация.
- Главная страница.
- Инструменты редактирования.
- Работа с базой данных.
- Окно статистики.
Дополнительные возможности управления сайтом относятся к разным направлениям PHP, которые невозможно охватить в одной статье.
Теперь приступаем к написанию функций, запросов и валидаций.
Чтобы ограничить доступ, создадим форму входа в панель управления:
$echo = "<div class='table'>
<div class='tale-wrapper'>
<div class='table-title'>Войти в панель администратора</div>
<div class='table-content'>
<form method='post' id='login-form' class='login-form'>
<input type='text' placeholder='Логин' class='input'
name='login' required><br>
<input type='password' placeholder='Пароль' class='input'
name='password' required><br>
<input type='submit' value='Войти' class='button'>
</form>
</div>
</div>
</div>";
В переменную $echo прописываем HTML-код формы, которая передает данные на эту же страницу. Затем они обрабатываются:
function login($db,$login,$password) {
//Обязательно нужно провести валидацию логина и пароля, чтобы
исключить вероятность инъекции
//Запрос в базу данных
$loginResult = mysqli_query($db,"SELECT * FROM userlist WHERE login='$login'
AND password='$password' AND admin='1'");
if(mysqli_num_rows($loginResult) == 1) { //Если есть совпадение,
возвращается true
return true;
} else {//Если такого пользователя не существует, данные стираются,
а возвращается false
unset($_SESSION['login'],$_SESSION['password']);
return false;
}
}
if(isset($_POST['login']) && isset($_POST['password'])) {
$_SESSION['login'] = $_POST['login'];
$_SESSION['password'] = $_POST['password'];
}
if(isset($_SESSION['login']) && isset($_SESSION['password'])) {
if(login($db,$_SESSION['login'],$_SESSION['password'])) {//Попытка авторизации
//Тут будут проходить все операции
$echo = null; //Обнуление переменной, чтобы удалить из вывода
форму авторизации
}
}
После отправки формы данные переносятся в супермассив $_SESSION. Затем вызывается функция login (). Она делает запрос в базу данных. Если он проходит удачно, возвращается значение true, если нет — данные из $_SESSION удаляются и возвращается false.
Соответствия в запросе ищутся только среди администраторов, но можно давать ограниченный доступ и для других групп пользователей — например, для модераторов, чтобы они проверяли комментарии и статьи, но не имели доступа к глобальным настройкам и удалению записей.
Теперь создадим условия, чтобы пользователь делал что-то полезное. Для этого методом GET передаётся название страницы:
if(isset($_GET['act'])) {$act = $_GET['act'];} else {$act = 'home';}
switch($act) {
case 'home':
$article_result = mysqli_query($db,"SELECT * FROM articles");
if(mysqli_num_rows($article_result) >= 1) {
while($article_array = mysqli_fetch_array($article_result)) {
$articles .= "<div class='table-content__list-item'><a href='? act=edit_article&id=$article_array[id]'>$article_array[id] |
$article_array[title]</a></div>";
}
} else {
$articles = "Статей пока нет";
}
$users_result = mysqli_query($db,"SELECT * FROM userlist");
if(mysqli_num_rows($users_result) >= 1) {
while($users_array = mysqli_fetch_array($users_result)) {
$users .= "<div class='table-content__list-item'><a href='? act=edit_user&id=$users_array[id]'>$users_array[id] |
$users_array[login]</a></div>";
}
} else {
$users = "Статей пока нет";
}
$echo = "<div class='tables'>
<div class='table'>
<div class='table-wrapper'>
<div class='table-title'>Страницы</div>
<div class='table-content'>
$articles
<a href='?act=add_article' class='table__add-button' id='add_article'>+</a>
</div>
</div>
</div>
<div class='table'>
<div class='table-wrapper'>
<div class='table-title'>Пользователи</div>
<div class='table-content'>
$users
<a href='?act=add_user' class='table__add-button'
id='add_user'>+</a>
</div>
</div>
</div>
</div>";
break;
}
В переменную $act попадает значение из $_GET[‘act’], а если его не существует, то просто home. Дальше с помощью функции switch () прописываются действия для каждой страницы.
На главной странице админки размещают все компоненты сайта с возможностью управления:
- пользователями;
- статьями и комментариями;
- карточками товаров;
- файлами и изображениями;
- статистикой и рекламными блоками.
Должна быть возможность редактировать и общие настройки сайта: поменять метатеги, изменить параметры капчи, обновить политику конфиденциальности и так далее.
Как вы могли заметить, в таблицах каждая строчка — это ссылка такого вида:
<a href='?act=edit_article&id=$article_array[id]'>$article_array[id] | $article_array[title]</a>
В переменной act передаётся значение edit_article, а в id — идентификатор статьи. Перейдя по ссылке, администратор попадает на страницу редактирования:
case 'edit_article':
if(isset($_GET['id'])) {
$id = $_GET['id'];
$result = mysqli_query($db,"SELECT * FROM articles WHERE id='$id'");
if(mysqli_num_rows($result) == 1) {
if(isset($_POST['title']) && isset($_POST['description']) && isset($_POST['text'])) {
//Тут должна быть валидация
//Обновление таблицы
$update = mysqli_query($db,"UPDATE articles SET title='$_POST[title]', description='$_POST[description]', text='$_POST[text]' WHERE id='$id'");
if($update) {
//Если обновление прошло успешно, получаются новые данные
$result = mysqli_query($db,"SELECT * FROM articles WHERE id='$id'");
$message = "Успешно обновлено!";
}
}
$article = mysqli_fetch_array($result);//Получение информации в массив
//Форма редактирования
$echo = "<div class='table'>
<div class='table-wrapper'>
<div class='table-title'>Редактирование статьи</div>
<div class='table-content'>
<a href='?act=home'><- Вернуться</a><br>
$message
<form method='post' class='article-form'>
<b>Название:</b> <input type='text' name='title' value='$article[title]'><br>
<b>Описание:</b> <textarea name='description'>$article[description]</textarea><br>
<b>Текст:</b> <textarea name='text'>$article[text]</textarea></br>
<input type='submit' class='button' value='Сохранить'>
</form>
</div>
</div>
</div>";
}
}
break;
Сначала в базу данных отправляется запрос на получение статьи. Затем, если всё в порядке, выводится форма редактирования. Если перед этим пользователь отправил изменённый текст, то он отправляется на сервер, таблица обновляется, а потом с помощью ещё одного запроса выводится актуальная информация.
Чтобы создать функцию добавления пользователя (или любого другого элемента на сайте), парсим форму и её обработчик:
case 'add_user':
if(isset($_POST['reglogin']) && isset($_POST['regpassword'])) {
$check = mysqli_query($db,"SELECT * FROM userlist WHERE login='$_POST[reglogin]'");
if(mysqli_num_rows($check) == 0) {
$insert = mysqli_query($db,"INSERT INTO userlist (login,password,admin) VALUE ('$_POST[reglogin]','$_POST[regpassword]','$_POST[regadmin]')");
if($insert) {
$message = "Пользователь успешно добавлен!";
} else {
$message = "Ошибка! ".mysqli_error($db);
}
} else {
$message = "Пользователь с таким логином уже существует!";
}
}
$echo = "<div class='table'>
<div class='table-wrapper'>
<div class='table-title'>Новый пользователь</div>
<div class='table-content'>
<a href='?act=home'><- Вернуться</a><br>
$message
<form method='post' class='user-form'>
<b>Логин:</b> <input type='text' name='reglogin' required><br>
<b>Пароль:</b> <input type='text' name='regpassword' required><br>
<b>Админ:</b> <input type='checkbox' name='regadmin'></br>
<input type='submit' class='button' value='Добавить'>
</form>
</div>
</div>
</div>";
break;
Сначала проверяем, свободен ли указанный логин. Если да, то в базу вносятся соответствующие данные. Также обратите внимание, что существование переменной regadmin не проверяется, потому что форма её не отправляет, если оставить чекбокс пустым.
Чтобы смотреть статистику посещений, комментирования и другой активности, создаём в базе данных таблицу с полями:
- ID;
- Date;
- Views;
- Comments.
В них записывается общее количество просмотров и комментариев за каждый день, которое выводится на графике. Чтобы это сделать, пропишем HTML-код таблицы:
<div class='table'>
<div class='table-wrapper'>
<div class='table-title'>Статистика</div>
<div class='table-content'>
<img src='stats.php' class='statistics-img'> <br>
Красный: просмотры <br>
Синий: комментарии <br>
1 шаг — 1 день
</div>
</div>
</div>
В качестве источника картинки указывается PHP-файл с кодом:
<?include("includes/db.php");
$width = 400;
$height = 200;
$canv = imagecreatetruecolor($width, $height);
//Цвета
$white = imagecolorallocate($canv, 255, 255, 255);
$gray = imagecolorallocate($canv, 150, 150, 150);
$black = imagecolorallocate($canv, 0, 0, 0);
$red = imagecolorallocate($canv, 255, 0, 0);
$blue = imagecolorallocate($canv, 0, 0, 255);
imagefill($canv,0,0,$white);
//Рисуется квадрат
imagerectangle($canv, 15, 5, $width-5, $height-15, $gray);
//Горизонтальные линии
for($i = 1; $i <= 5; $i++) {
imageline($canv, 15, $height-$i*35, $width-5, $height-$i*35, $gray);
}
//Вертикальные линии
for($i = 1; $i <= 15; $i++) {
imageline($canv, 15+($i*30), 5, 15+($i*30), $height-15, $gray);
}
//Получение статистики из базы данных
$stats_result = mysqli_query($db,"SELECT * FROM statistics");
if($stats_result) {
$last_y = [0,0];
$x = 15;
//Рисуется график
while($stats = mysqli_fetch_array($stats_result)) {
imageline($canv, $x, ($height-15)-$last_y[0], $x+30, ($height-15)-($stats['views']/10), $red);
imageline($canv, $x, ($height-15)-$last_y[1], $x+30, ($height-15)-($stats['comments']/10), $blue);
$last_y[0] = $stats['views']/10;
$last_y[1] = $stats['comments']/10;
$x += 30;
}
} else {echo mysqli_error($db);}
//Вывод изображения
header("Content-type: image/png");
imagepng($canv);
//Освобождение памяти
imagedestroy($canv);
?>
Сначала создаются изображение, рамка и деления. Затем идёт запрос в базу данных, чтобы получить статистику, которая отрисовывается в виде графика. Изображение сохраняется для разового вывода, а потом удаляется. Когда пользователь обратится к статистике в следующий раз, информация будет визуализирована заново.
То, о чём мы успели поговорить, — лишь часть того, что должно быть в админке.
Например, дополнительно можно реализовать:
- премодерацию комментариев;
- удаление записей из базы данных;
- работу с файлами;
- продвинутый редактор статей;
- подробную статистику каждой отдельной страницы и так далее.
По мере того как компании все больше переводят свои производственные операции на автоматизацию, панели управления становятся все более сложными. Производители добавляют более сложные машины, они накладывают датчики и системы прогнозирующего обслуживания для обеспечения бесперебойной работы оборудования, а также добавляют программное обеспечение, машины и средства обработки данных от разных поставщиков. Это усложняет работу по мере того, как пользователи работают над интеграцией разрозненных технологий. Подробнее про проектирование электрощитов с автоматикой здесь.
Проектирование систем автоматизации-сложный бизнес. Производители уходят от тех дней, когда они выбирали одного поставщика автоматизации, и этот поставщик работал бы с интегратором, чтобы убедиться, что все подходит и работает успешно. Все чаще производители ищут компоненты и программное обеспечение для автоматизации от различных поставщиков. Часто они работают с системным интегратором, чтобы убедиться, что все работает в гармонии. Другой альтернативой является работа с дистрибьютором компонентов и автоматизации это обеспечивает концентратор, в котором инженеры-проектировщики проверяют совместимость компонентов системы.
Джим Дэвис: Что касается размещения промышленных панелей управления, я считаю, что проводка играет абсолютно важную роль в эффективном проектировании и строительстве. Во-первых, вы должны разделить силовую и управляющую проводку, чтобы снизить вероятность того, что электрический шум может повлиять на управляющие сигналы. Во – вторых, вам необходимо отделить полевую проводку от внутренней проводки, разместив клеммные колодки таким образом, чтобы для каждой из них были проводные каналы, а это означает выбор проводных каналов надлежащего размера, чтобы сэкономить как можно больше места в корпусе. Наконец, вы должны убедиться в правильном заземлении панели и устройств, включив в панель ряд заземляющих клемм. Подробнее здесь https://pia.su/produkcziya/vru/.
Новости дизайна: Как эти трудности решались в прошлом?
Джим Дэвис: В основном вручную или с помощью инструментов проектирования создателями панелей, основанных на том, какие компоненты они хорошо знают или использовали в прошлом. Это удобно, но иногда может быть ограничением в плане того, какие компоненты можно использовать и каковы их возможности.
Новости дизайна: Schneider Electric и Allied Electronics & Automation недавно создали то, что они называют «центром» для создания панелей управления. Как помогает этот центр?
Джим Дэвис: Цель цифрового концентратора Schneider/Allied-обеспечить центральное расположение технических руководств, а также информации о продуктах для широкого спектра компонентов, которые могут быть выбраны изготовителями панелей. Это также облегчает поиск новых компонентов панели управления, которые могут сэкономить место или предоставить возможности, о которых разработчик ранее не знал.
Новости дизайна: Каковы некоторые из основных основных проблем, связанных со строительством промышленных панелей управления?
Шеннон Чайлз: Основная задача разработчиков промышленных панелей управления может быть сформулирована одним словом: эффективность. В условиях растущей конкуренции на рынке эффективный выбор продукции, эффективная закупка продукции и эффективное интегрированное приложение чрезвычайно важны для успеха разработчика панелей управления.
Шеннон Чайлз: В промышленном контроле постоянно возрастает сложность. Клиенты разработчика панелей управления всегда ищут лучшие технологии, которые хорошо работают вместе. Это может быть что угодно-от удаленного мониторинга до беспроводных технологий и доступа к облачной аналитике. Кроме того, стандарты UL постоянно меняются. Создатели панелей управления должны понимать эти изменения и то, как они применяются к создаваемым ими панелям.
В прошлой статье мы с Вами разбирали создание собственного движка для сайта. Однако, очень часто приходится управлять сайтом: добавлять новые материалы, управлять пользователями, голосованиями. Безусловно, это можно делать через PHPMyAdmin, но это весьма неудобно, поэтому хорошим решением будет – создать Admin-панель для сайта. И как это сделать, Вы узнаете в этой статье.
Итак, давайте вновь распишу порядок действий, которые необходимо выполнить:
- Сверстайте все страницы сайта. В принципе, Вы вообще можете обойтись без дизайна, но элементарная структура должна быть. То есть табличные данные должны быть в таблице, а не сплошным текстом.
- Примите решение о том, какие данные Вы хотите видеть в Admin-панели. Например, наверняка Вам потребуется видеть пользователей Вашего сайта, Ваши статьи, может быть, какие-нибудь опросы.
- Создайте класс для управления Admin-панелью. Здесь должны быть созданы методы, которые позволяют делать выборку из самых разных таблиц, а также добавлять и редактировать записи в них. И сделать подобные методы нужно для всех таблиц, с которыми Вы хотите работать в Admin-панели (Вы уже должны были выбрать это в предыдущем пункте). Например, самый простой пример с пользователями. Минимальный набор требуемых методов: выборка всех пользователей, добавление нового пользователя, изменение пользователя. Безусловно, все эти задачи должны быть Вами уже реализованы при создании движка для сайта, поэтому здесь Вам надо будет только обратиться к старым классам и, возможно, как-то изменить данные конкретно для Admin-панели.
- Разбейте Ваш шаблон сайта на отдельные части и скопируйте их в отдельные файлы с расширением tpl. Также поставьте элементы шаблона, например, так: “Пользователь {username} зарегистрировался {regdate}“. Это всего лишь пример, а данные, вообще говоря, удобнее выводить в таблицах.
- Создайте класс, который будет заниматься подстановкой вместо элементов шаблона соответствующие данные.
- Собирайте по кирпичикам все страницы Admin-панели Вашего сайта с помощью созданного в предыдущем пункте класса.
- При желании можете сделать дизайн, хотя, безусловно, это исключительно для Вас и других администраторов.
Как видите, последние 4 пункта идентичны тем, которые Вы выполняли при создании движка. Здесь объём работы будет значительно меньше, поэтому, думаю, Вы с этим без проблем справитесь.
Я где-то три месяца сидел на своём сайте без Admin-панели, добавляя новые материалы напрямую через базу данных. Скажу честно, тратил много лишнего времени, а когда создал Admin-панель на своём сайте, то процесс управления сайтом значительно упростился. Так что рекомендую Вам сделать то же самое.
Пример создания Admin-панели Вы найдёте в этом обучающем курсе: http://srs.myrusakov.ru/im
-
Создано 29.05.2011 13:49:54
-
Михаил Русаков
Копирование материалов разрешается только с указанием автора (Михаил Русаков) и индексируемой прямой ссылкой на сайт (http://myrusakov.ru)!
Добавляйтесь ко мне в друзья ВКонтакте: http://vk.com/myrusakov.
Если Вы хотите дать оценку мне и моей работе, то напишите её в моей группе: http://vk.com/rusakovmy.
Если Вы не хотите пропустить новые материалы на сайте,
то Вы можете подписаться на обновления: Подписаться на обновления
Если у Вас остались какие-либо вопросы, либо у Вас есть желание высказаться по поводу этой статьи, то Вы можете оставить свой комментарий внизу страницы.
Если Вам понравился сайт, то разместите ссылку на него (у себя на сайте, на форуме, в контакте):
-
Кнопка:
Она выглядит вот так:
-
Текстовая ссылка:
Она выглядит вот так: Как создать свой сайт
- BB-код ссылки для форумов (например, можете поставить её в подписи):