HTML Diff
0 added 0 removed
Original 2026-01-01
Modified 2026-03-10
1 <p>Начну с предыстории самого проекта. Мысль пришла в голову совершенно случайно - мне явно не хватало для работы над своими проектами какой-то дополнительной ответственности. Вот и решил создать<strong>портал</strong>, где я смог бы стимулировать собственную мотивацию, публично рискуя репутацией и деньгами.</p>
1 <p>Начну с предыстории самого проекта. Мысль пришла в голову совершенно случайно - мне явно не хватало для работы над своими проектами какой-то дополнительной ответственности. Вот и решил создать<strong>портал</strong>, где я смог бы стимулировать собственную мотивацию, публично рискуя репутацией и деньгами.</p>
2 <p>Ну а теперь перейду к делу. Тема обширная, но я надеюсь, что на выходе у меня получится донести картину целиком и вспомнить все подводные камни, которые всплыли до момента создания проекта. Я буду указывать все первоисточники, которые я использовал, чтобы помочь тем, кто хочет написать своё приложение на<strong>angular</strong>. Да, собственно, все желающие смогут найти ответы на большинство интересующих их вопросов по данной теме в одном цикле статей.</p>
2 <p>Ну а теперь перейду к делу. Тема обширная, но я надеюсь, что на выходе у меня получится донести картину целиком и вспомнить все подводные камни, которые всплыли до момента создания проекта. Я буду указывать все первоисточники, которые я использовал, чтобы помочь тем, кто хочет написать своё приложение на<strong>angular</strong>. Да, собственно, все желающие смогут найти ответы на большинство интересующих их вопросов по данной теме в одном цикле статей.</p>
3 <p>Я давно уже лелеял мысль апробировать<strong>material.angularjs.org</strong>на каком-то боевом проекте. Тут возникла идея и я решился… С виду всё казалось довольно просто - набор готовых компонентов = быстрая разработка, на backend знакомый Yii и… Но я не рассчитывал, что маленькое приложение окажется немного больше, чем планировалось вначале, и предстоит такая возня с веб-сервером. Как говорится, упс…</p>
3 <p>Я давно уже лелеял мысль апробировать<strong>material.angularjs.org</strong>на каком-то боевом проекте. Тут возникла идея и я решился… С виду всё казалось довольно просто - набор готовых компонентов = быстрая разработка, на backend знакомый Yii и… Но я не рассчитывал, что маленькое приложение окажется немного больше, чем планировалось вначале, и предстоит такая возня с веб-сервером. Как говорится, упс…</p>
4 <p>Началось всё с конфигурации<strong>nginx</strong>. Получалось, что все запросы, кроме REST location, мне надо было перенаправлять на index.html, где у меня и начинал отрабатывать angular. Выглядела первая конфигурация примерно так:</p>
4 <p>Началось всё с конфигурации<strong>nginx</strong>. Получалось, что все запросы, кроме REST location, мне надо было перенаправлять на index.html, где у меня и начинал отрабатывать angular. Выглядела первая конфигурация примерно так:</p>
5 server { charset utf-8; listen 80; server_name truemania.ru; root /path/to/root; access_log /path/to/root/log/access.log; error_log /path/to/root/log/error.log; location / { # Angular app conf root /path/to/root/frontend/web; try_files $uri $uri/ /index.html =404; } location ~* \.php$ { include fastcgi_params; #fastcgi_pass 127.0.0.1:9000; fastcgi_pass unix:/var/run/php5-fpm.sock; try_files $uri =404; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } # avoid processing of calls to non-existing static files by Yii (uncomment if necessary) location ~* \.(css|js|jpg|jpeg|png|gif|bmp|ico|mov|swf|pdf|zip|rar)$ { try_files $uri =404; } location ~* \.(htaccess|htpasswd|svn|git) { deny all; } location /api-location { client_max_body_size 2000M; alias /path/to/root/frontend/web; try_files $uri /frontend/web/index.php?$args; location ~* ^/api-location/(.+\.php)$ { try_files $uri /frontend/web/$1?$args; } } }<p>Здесь всё наше API находится по locationapi-location. Конфигурация angular $routeProvider:</p>
5 server { charset utf-8; listen 80; server_name truemania.ru; root /path/to/root; access_log /path/to/root/log/access.log; error_log /path/to/root/log/error.log; location / { # Angular app conf root /path/to/root/frontend/web; try_files $uri $uri/ /index.html =404; } location ~* \.php$ { include fastcgi_params; #fastcgi_pass 127.0.0.1:9000; fastcgi_pass unix:/var/run/php5-fpm.sock; try_files $uri =404; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } # avoid processing of calls to non-existing static files by Yii (uncomment if necessary) location ~* \.(css|js|jpg|jpeg|png|gif|bmp|ico|mov|swf|pdf|zip|rar)$ { try_files $uri =404; } location ~* \.(htaccess|htpasswd|svn|git) { deny all; } location /api-location { client_max_body_size 2000M; alias /path/to/root/frontend/web; try_files $uri /frontend/web/index.php?$args; location ~* ^/api-location/(.+\.php)$ { try_files $uri /frontend/web/$1?$args; } } }<p>Здесь всё наше API находится по locationapi-location. Конфигурация angular $routeProvider:</p>
6 app.config(['$routeProvider', '$locationProvider', function ($routeProvider, $locationProvider) { $routeProvider. when('/route1', { templateUrl: '/views/route1.html', controller: 'route1Ctrl' }). when('/route2', { templateUrl: '/views/route2.html', controller: 'route2Ctrl' }). when('/route3', { templateUrl: '/views/route3.html', controller: 'route3Ctrl' }). otherwise({ redirectTo: '/route1' }); // use the HTML5 History API $locationProvider.html5Mode({ enabled: true, requireBase: false }); }]);<h2>Но как angular-сайт будет индексироваться?</h2>
6 app.config(['$routeProvider', '$locationProvider', function ($routeProvider, $locationProvider) { $routeProvider. when('/route1', { templateUrl: '/views/route1.html', controller: 'route1Ctrl' }). when('/route2', { templateUrl: '/views/route2.html', controller: 'route2Ctrl' }). when('/route3', { templateUrl: '/views/route3.html', controller: 'route3Ctrl' }). otherwise({ redirectTo: '/route1' }); // use the HTML5 History API $locationProvider.html5Mode({ enabled: true, requireBase: false }); }]);<h2>Но как angular-сайт будет индексироваться?</h2>
7 <p>В голову сразу пришло решение, что статику надо отдавать отдельно. Немного погуглив, нашел информацию о ?_escaped_fragment. Нужно было отдельно генерировать статику и отдавать на запросы типа truemania.ru/?_escaped_fragment готовые для индексации фрагменты.</p>
7 <p>В голову сразу пришло решение, что статику надо отдавать отдельно. Немного погуглив, нашел информацию о ?_escaped_fragment. Нужно было отдельно генерировать статику и отдавать на запросы типа truemania.ru/?_escaped_fragment готовые для индексации фрагменты.</p>
8 <p>При недолгом поиске наткнулся на<a>статью</a>, где был подробно описан механизм индексации для angular-сайтов, как раз для сервера nginx. В конфигурацию было добавлено ещё несколько location:</p>
8 <p>При недолгом поиске наткнулся на<a>статью</a>, где был подробно описан механизм индексации для angular-сайтов, как раз для сервера nginx. В конфигурацию было добавлено ещё несколько location:</p>
9 if ($args ~ "_escaped_fragment_=(.*)") { rewrite ^ /snapshot${uri}; } location /snapshot { proxy_pass http://help.truemania.ru/snapshot; proxy_connect_timeout 60s; }<p>Создаём домен второго уровня, где будет происходить обработка запросов на отдачу готовых фрагментов. На запрос типа</p>
9 if ($args ~ "_escaped_fragment_=(.*)") { rewrite ^ /snapshot${uri}; } location /snapshot { proxy_pass http://help.truemania.ru/snapshot; proxy_connect_timeout 60s; }<p>Создаём домен второго уровня, где будет происходить обработка запросов на отдачу готовых фрагментов. На запрос типа</p>
10 http://truemania.ru/user/50?_escaped_fragment_=<p>вы получите</p>
10 http://truemania.ru/user/50?_escaped_fragment_=<p>вы получите</p>
11 http://help.truemania.ru/snapshot/user/50<p>Остаётся только создавать все необходимые слепки, которые нужно отдавать поисковому боту. При этом я пользовался стандартами микроразметки schema.org. Кто не знаком с миром семантической разметки, советую ознакомиться с в<a>этой статье</a>.</p>
11 http://help.truemania.ru/snapshot/user/50<p>Остаётся только создавать все необходимые слепки, которые нужно отдавать поисковому боту. При этом я пользовался стандартами микроразметки schema.org. Кто не знаком с миром семантической разметки, советую ознакомиться с в<a>этой статье</a>.</p>
12 <p>Создание динамического<strong>sitemap</strong>очень подробно описано в<a>этой статье</a>- советую прочесть. Но жаль, что тут описано решение для первой версии Yii. Sitemap создаётся при каждом новом запросе заново, что может вызвать весьма высокую нагрузку на сервер. Выход - создание консольного контроллера и обновление sitemap с интервалом 10 минут, используя<strong>crontab</strong>. Совсем немного изменив исходный код, я получил годное решение для Yii2 console:</p>
12 <p>Создание динамического<strong>sitemap</strong>очень подробно описано в<a>этой статье</a>- советую прочесть. Но жаль, что тут описано решение для первой версии Yii. Sitemap создаётся при каждом новом запросе заново, что может вызвать весьма высокую нагрузку на сервер. Выход - создание консольного контроллера и обновление sitemap с интервалом 10 минут, используя<strong>crontab</strong>. Совсем немного изменив исходный код, я получил годное решение для Yii2 console:</p>
13 &lt;?php namespace console\models; use Yii; /** * @author ElisDN &lt;mail@elisdn.ru&gt; * @link http://www.elisdn.ru */ class DSitemap { const ALWAYS = 'always'; const HOURLY = 'hourly'; const DAILY = 'daily'; const WEEKLY = 'weekly'; const MONTHLY = 'monthly'; const YEARLY = 'yearly'; const NEVER = 'never'; protected $items = array(); /** * @param $url * @param string $changeFreq * @param float $priority * @param int $lastMod */ public function addUrl($url, $changeFreq=self::DAILY, $priority = 0.5, $lastMod = 0) { $host = Yii::$app-&gt;urlManager-&gt;getBaseUrl(); $item = array( 'loc' =&gt; $host . $url, 'changefreq' =&gt; $changeFreq, 'priority' =&gt; $priority ); if ($lastMod) $item['lastmod'] = $this-&gt;dateToW3C($lastMod); $this-&gt;items[] = $item; } /** * @param \yii\db\ActiveRecord[] $models * @param string $changeFreq * @param float $priority */ public function addModels($models, $changeFreq=self::DAILY, $priority=0.5) { $host = Yii::$app-&gt;urlManager-&gt;getBaseUrl(); foreach ($models as $model) { $item = array( 'loc' =&gt; $host . $model-&gt;getUrl(), 'changefreq' =&gt; $changeFreq, 'priority' =&gt; $priority ); if ($model-&gt;hasAttribute('create_date')) $item['lastmod'] = $this-&gt;dateToW3C($model-&gt;create_date); $this-&gt;items[] = $item; } } /** * @return string XML code */ public function render() { $dom = new \DOMDocument('1.0', 'utf-8'); $urlset = $dom-&gt;createElement('urlset'); $urlset-&gt;setAttribute('xmlns','http://www.sitemaps.org/schemas/sitemap/0.9'); foreach($this-&gt;items as $item) { $url = $dom-&gt;createElement('url'); foreach ($item as $key=&gt;$value) { $elem = $dom-&gt;createElement($key); $elem-&gt;appendChild($dom-&gt;createTextNode($value)); $url-&gt;appendChild($elem); } $urlset-&gt;appendChild($url); } $dom-&gt;appendChild($urlset); return $dom-&gt;saveXML(); } protected function dateToW3C($date) { if (is_int($date)) return date(DATE_W3C, $date); else return date(DATE_W3C, strtotime($date)); } }<p>Консольный action:</p>
13 &lt;?php namespace console\models; use Yii; /** * @author ElisDN &lt;mail@elisdn.ru&gt; * @link http://www.elisdn.ru */ class DSitemap { const ALWAYS = 'always'; const HOURLY = 'hourly'; const DAILY = 'daily'; const WEEKLY = 'weekly'; const MONTHLY = 'monthly'; const YEARLY = 'yearly'; const NEVER = 'never'; protected $items = array(); /** * @param $url * @param string $changeFreq * @param float $priority * @param int $lastMod */ public function addUrl($url, $changeFreq=self::DAILY, $priority = 0.5, $lastMod = 0) { $host = Yii::$app-&gt;urlManager-&gt;getBaseUrl(); $item = array( 'loc' =&gt; $host . $url, 'changefreq' =&gt; $changeFreq, 'priority' =&gt; $priority ); if ($lastMod) $item['lastmod'] = $this-&gt;dateToW3C($lastMod); $this-&gt;items[] = $item; } /** * @param \yii\db\ActiveRecord[] $models * @param string $changeFreq * @param float $priority */ public function addModels($models, $changeFreq=self::DAILY, $priority=0.5) { $host = Yii::$app-&gt;urlManager-&gt;getBaseUrl(); foreach ($models as $model) { $item = array( 'loc' =&gt; $host . $model-&gt;getUrl(), 'changefreq' =&gt; $changeFreq, 'priority' =&gt; $priority ); if ($model-&gt;hasAttribute('create_date')) $item['lastmod'] = $this-&gt;dateToW3C($model-&gt;create_date); $this-&gt;items[] = $item; } } /** * @return string XML code */ public function render() { $dom = new \DOMDocument('1.0', 'utf-8'); $urlset = $dom-&gt;createElement('urlset'); $urlset-&gt;setAttribute('xmlns','http://www.sitemaps.org/schemas/sitemap/0.9'); foreach($this-&gt;items as $item) { $url = $dom-&gt;createElement('url'); foreach ($item as $key=&gt;$value) { $elem = $dom-&gt;createElement($key); $elem-&gt;appendChild($dom-&gt;createTextNode($value)); $url-&gt;appendChild($elem); } $urlset-&gt;appendChild($url); } $dom-&gt;appendChild($urlset); return $dom-&gt;saveXML(); } protected function dateToW3C($date) { if (is_int($date)) return date(DATE_W3C, $date); else return date(DATE_W3C, strtotime($date)); } }<p>Консольный action:</p>
14 public function actionGetsitemap() { $sitemap = new DSitemap(); $sitemap-&gt;addModels(Model1::find()-&gt;active()-&gt;all(), DSitemap::HOURLY); $sitemap-&gt;addModels(Model2::find()-&gt;all(), DSitemap::HOURLY); $sitemap-&gt;addModels(Model3::find()-&gt;all(), DSitemap::HOURLY); $path = \Yii::getAlias("@frontend/web") . DIRECTORY_SEPARATOR . "sitemap.xml"; return file_put_contents($path, $sitemap-&gt;render()); }<p>Конфигурация crontab для запуска через каждые 10 мин.</p>
14 public function actionGetsitemap() { $sitemap = new DSitemap(); $sitemap-&gt;addModels(Model1::find()-&gt;active()-&gt;all(), DSitemap::HOURLY); $sitemap-&gt;addModels(Model2::find()-&gt;all(), DSitemap::HOURLY); $sitemap-&gt;addModels(Model3::find()-&gt;all(), DSitemap::HOURLY); $path = \Yii::getAlias("@frontend/web") . DIRECTORY_SEPARATOR . "sitemap.xml"; return file_put_contents($path, $sitemap-&gt;render()); }<p>Конфигурация crontab для запуска через каждые 10 мин.</p>
15 */10 * * * * /path/to/yii cron/getsitemap &gt;&gt; /path/to/log/command_log/getsitemap.log;<p>Это решение оптимальное и весьма производительное. Таким образом мы получаем довольно актуальные данные. При необходимости можно пересоздавать sitemap с более частым или более редким интервалом.</p>
15 */10 * * * * /path/to/yii cron/getsitemap &gt;&gt; /path/to/log/command_log/getsitemap.log;<p>Это решение оптимальное и весьма производительное. Таким образом мы получаем довольно актуальные данные. При необходимости можно пересоздавать sitemap с более частым или более редким интервалом.</p>
16 <p>Далее пошла работа над красивым выводом ссылок в соцсетях. Для тех, кто не в теме, - это стандарт разметки<a>http://ogp.me/</a>. Меня постигло очень большое разочарование, что боты не понимают meta-тег:</p>
16 <p>Далее пошла работа над красивым выводом ссылок в соцсетях. Для тех, кто не в теме, - это стандарт разметки<a>http://ogp.me/</a>. Меня постигло очень большое разочарование, что боты не понимают meta-тег:</p>
17 &lt;meta name="fragment" content="!" /&gt;<p>На данном этапе я немного застопорился, так как элементарно в лоб решения не нашлось. Я хотел заставить ботов понимать, что за страницей скрывается реальный фрагмент. Погуглив, я принял решение отдавать фрагменты по<strong>user-agent</strong>. Пришлось изучить документацию для соответствующего сервиса, чтобы получить примерные user-agent, которые можно было бы извлечь, пользуясь регулярными выражениями.</p>
17 &lt;meta name="fragment" content="!" /&gt;<p>На данном этапе я немного застопорился, так как элементарно в лоб решения не нашлось. Я хотел заставить ботов понимать, что за страницей скрывается реальный фрагмент. Погуглив, я принял решение отдавать фрагменты по<strong>user-agent</strong>. Пришлось изучить документацию для соответствующего сервиса, чтобы получить примерные user-agent, которые можно было бы извлечь, пользуясь регулярными выражениями.</p>
18 <p>Моя конфигурация для отдачи статики ботам соцсетей:</p>
18 <p>Моя конфигурация для отдачи статики ботам соцсетей:</p>
19 # Вот тут происходит обработка user-agent - если это бот соцсетей, отдаём статику if ( $http_user_agent ~* (facebookexternalhit|facebot|twitterbot|tinterest|google.*snippet|vk.com|vkshare) ){ rewrite ^ /snapshot${uri}; }<p>Естественно, осталось включить в мои слепки информацию о разметке<strong>open graph</strong>.</p>
19 # Вот тут происходит обработка user-agent - если это бот соцсетей, отдаём статику if ( $http_user_agent ~* (facebookexternalhit|facebot|twitterbot|tinterest|google.*snippet|vk.com|vkshare) ){ rewrite ^ /snapshot${uri}; }<p>Естественно, осталось включить в мои слепки информацию о разметке<strong>open graph</strong>.</p>
20 <p>Далее я захотел использовать в некоторых очень выгодных моментах websocket - это отлично подходило для решения таких задач, как состояние online/offline для пользователя. Конечно, сами websocket вещь весьма нестандартная для PHP, но готовое решение быстро нашлось -<a>http://socketo.me/</a>.</p>
20 <p>Далее я захотел использовать в некоторых очень выгодных моментах websocket - это отлично подходило для решения таких задач, как состояние online/offline для пользователя. Конечно, сами websocket вещь весьма нестандартная для PHP, но готовое решение быстро нашлось -<a>http://socketo.me/</a>.</p>
21 <p>Осталось только понять, как мне эти сокеты запустить на Yii2 в ubuntu. Собственно, создал консольный контроллер, и вот как выглядел action:</p>
21 <p>Осталось только понять, как мне эти сокеты запустить на Yii2 в ubuntu. Собственно, создал консольный контроллер, и вот как выглядел action:</p>
22 public function actionWebsocketaction() { $server = IoServer::factory( new HttpServer( new WsServer( new UserOnline() ) ), 8099, '127.0.0.1' ); $server-&gt;run(); }<p>Ну, и далее прилагаю саму модель UserOnline:</p>
22 public function actionWebsocketaction() { $server = IoServer::factory( new HttpServer( new WsServer( new UserOnline() ) ), 8099, '127.0.0.1' ); $server-&gt;run(); }<p>Ну, и далее прилагаю саму модель UserOnline:</p>
23 &lt;?php namespace console\models; use Yii; use common\modules\core\models\User; use Ratchet\MessageComponentInterface; use Ratchet\ConnectionInterface; use yii\web\ServerErrorHttpException; class UserOnline implements MessageComponentInterface { /** * Люблю константы, не люблю цифры */ const USER_OFFLINE = 0; const USER_ONLINE = 1; //При открытии нового соединения выведем в лог resourceId public function onOpen(ConnectionInterface $conn) { echo "New connection! ({$conn-&gt;resourceId})\n"; } //Если было получено сообщение, ставим данному пользователю статус online public function onMessage(ConnectionInterface $from, $username) { $model = UserOnlineConnections::findByUsername($username); if(empty($model)) { $model = new UserOnlineConnections(); //Параметры передаются с символом переноса строки, пришлось выпилить их регуляркой $model-&gt;username = preg_replace('/\\r\\n$/', '', $username); $model-&gt;conn_id = $from-&gt;resourceId; if(!($model-&gt;validate() &amp;&amp; $model-&gt;save())) throw new ServerErrorHttpException(json_encode($model-&gt;getErrors())); } else { $model-&gt;conn_id = $from-&gt;resourceId; if(!($model-&gt;validate() &amp;&amp; $model-&gt;save())) throw new ServerErrorHttpException(json_encode($model-&gt;getErrors())); } echo "New user online $model-&gt;username \n"; self::setUserStatus($username, self::USER_ONLINE); } //Если соединение закрылось - пользователя в offline public function onClose(ConnectionInterface $conn) { echo "Close connection! ({$conn-&gt;resourceId})\n"; $username = UserOnlineConnections::findByConnId($conn-&gt;resourceId)-&gt;username; if($username) { //Set status offline echo "User offline $username \n"; self::setUserStatus($username, self::USER_OFFLINE); } } //Если ошибка - пользователя в offline public function onError(ConnectionInterface $conn, \Exception $e) { $username = UserOnlineConnections::findByConnId($conn-&gt;resourceId)-&gt;username; if($username) { //Set status offline echo "User offline $username \n"; self::setUserStatus($username, self::USER_OFFLINE); echo "An error has occurred: {$e-&gt;getMessage()}\n"; $conn-&gt;close(); } } /** * Устанавливаем пользователю нужный статус * @param $username * @param $status * @return bool * @throws ServerErrorHttpException */ public function setUserStatus($username, $status) { $model = User::findByUsername($username); if ($model) { $model-&gt;online = $status; if(!($model-&gt;validate() &amp;&amp; $model-&gt;save())) throw new ServerErrorHttpException(json_encode($model-&gt;getErrors())); return true; } if($status == self::USER_OFFLINE) { UserOnlineConnections::deleteAll( "username=".$username ); } } }<p>Осталось только всё это запустить. Нужно было сделать вывод stderr в stdout, но &amp;&gt; почему-то не хотел работать. Решение пришло с помощью<strong>nohup</strong>. Запуск сокета выглядел вот так:</p>
23 &lt;?php namespace console\models; use Yii; use common\modules\core\models\User; use Ratchet\MessageComponentInterface; use Ratchet\ConnectionInterface; use yii\web\ServerErrorHttpException; class UserOnline implements MessageComponentInterface { /** * Люблю константы, не люблю цифры */ const USER_OFFLINE = 0; const USER_ONLINE = 1; //При открытии нового соединения выведем в лог resourceId public function onOpen(ConnectionInterface $conn) { echo "New connection! ({$conn-&gt;resourceId})\n"; } //Если было получено сообщение, ставим данному пользователю статус online public function onMessage(ConnectionInterface $from, $username) { $model = UserOnlineConnections::findByUsername($username); if(empty($model)) { $model = new UserOnlineConnections(); //Параметры передаются с символом переноса строки, пришлось выпилить их регуляркой $model-&gt;username = preg_replace('/\\r\\n$/', '', $username); $model-&gt;conn_id = $from-&gt;resourceId; if(!($model-&gt;validate() &amp;&amp; $model-&gt;save())) throw new ServerErrorHttpException(json_encode($model-&gt;getErrors())); } else { $model-&gt;conn_id = $from-&gt;resourceId; if(!($model-&gt;validate() &amp;&amp; $model-&gt;save())) throw new ServerErrorHttpException(json_encode($model-&gt;getErrors())); } echo "New user online $model-&gt;username \n"; self::setUserStatus($username, self::USER_ONLINE); } //Если соединение закрылось - пользователя в offline public function onClose(ConnectionInterface $conn) { echo "Close connection! ({$conn-&gt;resourceId})\n"; $username = UserOnlineConnections::findByConnId($conn-&gt;resourceId)-&gt;username; if($username) { //Set status offline echo "User offline $username \n"; self::setUserStatus($username, self::USER_OFFLINE); } } //Если ошибка - пользователя в offline public function onError(ConnectionInterface $conn, \Exception $e) { $username = UserOnlineConnections::findByConnId($conn-&gt;resourceId)-&gt;username; if($username) { //Set status offline echo "User offline $username \n"; self::setUserStatus($username, self::USER_OFFLINE); echo "An error has occurred: {$e-&gt;getMessage()}\n"; $conn-&gt;close(); } } /** * Устанавливаем пользователю нужный статус * @param $username * @param $status * @return bool * @throws ServerErrorHttpException */ public function setUserStatus($username, $status) { $model = User::findByUsername($username); if ($model) { $model-&gt;online = $status; if(!($model-&gt;validate() &amp;&amp; $model-&gt;save())) throw new ServerErrorHttpException(json_encode($model-&gt;getErrors())); return true; } if($status == self::USER_OFFLINE) { UserOnlineConnections::deleteAll( "username=".$username ); } } }<p>Осталось только всё это запустить. Нужно было сделать вывод stderr в stdout, но &amp;&gt; почему-то не хотел работать. Решение пришло с помощью<strong>nohup</strong>. Запуск сокета выглядел вот так:</p>
24 nohup /path/to/yii ws/useronline &gt;&gt; /path/to/log/command_log/useronline.log;<p>Также в случае падения надо перезапустить данный процесс. Не нашёл решения более элегантного, как через каждую минуту запускать команду в<strong>crontab</strong>. В случае если порт занят, ничего не произойдет (выйдет ошибка), но если порт свободен, процесс будет запущен заново.</p>
24 nohup /path/to/yii ws/useronline &gt;&gt; /path/to/log/command_log/useronline.log;<p>Также в случае падения надо перезапустить данный процесс. Не нашёл решения более элегантного, как через каждую минуту запускать команду в<strong>crontab</strong>. В случае если порт занят, ничего не произойдет (выйдет ошибка), но если порт свободен, процесс будет запущен заново.</p>
25 <p>Далее надо websocet проксировать с помощью<strong>nginx</strong>. И тут в конфигурацию были добавлены следующие строки:</p>
25 <p>Далее надо websocet проксировать с помощью<strong>nginx</strong>. И тут в конфигурацию были добавлены следующие строки:</p>
26 upstream useronline { server 127.0.0.1:8099; } map $http_upgrade $connection_upgrade { default upgrade; '' close; } # Добавка в секцию server server { #ws proxy location /useronline { proxy_set_header X-Real-IP $remote_addr; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_pass http://useronline; } }<p>Вот теперь наш веб-сокет будет доступен по адресу ws://truemania.ru/useronline.</p>
26 upstream useronline { server 127.0.0.1:8099; } map $http_upgrade $connection_upgrade { default upgrade; '' close; } # Добавка в секцию server server { #ws proxy location /useronline { proxy_set_header X-Real-IP $remote_addr; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_pass http://useronline; } }<p>Вот теперь наш веб-сокет будет доступен по адресу ws://truemania.ru/useronline.</p>
27 <p>И последнее, с чем я столкнулся (из настроек веб-сервера) в процессе разработки - это переход на протокол https. Проблема была в следующем - facebook и google+ хотели, чтобы картинки отдавались по http и упорно не хотели выводить в превью картинку. Для этого пришлось изменить конфигурацию, а именно - заставить сервер отдавать медиафайлы по http:</p>
27 <p>И последнее, с чем я столкнулся (из настроек веб-сервера) в процессе разработки - это переход на протокол https. Проблема была в следующем - facebook и google+ хотели, чтобы картинки отдавались по http и упорно не хотели выводить в превью картинку. Для этого пришлось изменить конфигурацию, а именно - заставить сервер отдавать медиафайлы по http:</p>
28 server { listen 80; server_name truemania.ru; root /path/to/frontend/web; location / { return 301 https://$server_name$request_uri; # enforce https } #отдать статику по http location ~* \.(css|js|jpg|jpeg|png|gif|bmp|ico|mov|swf|pdf|zip|rar)$ { try_files $uri =404; } } server { charset utf-8; listen 443 ssl; ssl_certificate /path/to/ssl/truemania.crt; ssl_certificate_key /path/to/ssl/truemania.key; }<p>Также после того, как протокол поменялся, обращение к socet происходит по адресу wss://truemania.ru/useronline.</p>
28 server { listen 80; server_name truemania.ru; root /path/to/frontend/web; location / { return 301 https://$server_name$request_uri; # enforce https } #отдать статику по http location ~* \.(css|js|jpg|jpeg|png|gif|bmp|ico|mov|swf|pdf|zip|rar)$ { try_files $uri =404; } } server { charset utf-8; listen 443 ssl; ssl_certificate /path/to/ssl/truemania.crt; ssl_certificate_key /path/to/ssl/truemania.key; }<p>Также после того, как протокол поменялся, обращение к socet происходит по адресу wss://truemania.ru/useronline.</p>
29  
29