📜 ⬆️ ⬇️

As I wrote the web app angular + material and REST on Yii2 + webserver nginx

I'll start with the background of the project itself. The thought came to my mind quite by accident - I clearly did not have enough additional responsibility to work on my projects. So I decided to create a portal where I could stimulate my own motivation, publicly risking reputation and money.

Well, now get down to business. The topic is extensive, but I hope that at the output I will be able to convey the whole picture and recall all the pitfalls that surfaced before the project was created. I will point out all the primary sources that I used to help those who want to write their application on angular. Yes, actually, everyone will be able to find answers to most of their questions on this topic in a single series of articles.

I have long cherished the idea of ​​testing material.angularjs.org on some kind of combat project. Then an idea arose and I decided ... It looked pretty simple in appearance - a set of ready-made components = fast development, familiar Yii and ... on the backend, but ... I didn’t expect that a small application would be a little more than originally planned, and there would be such a fuss with the web server. As they say, oops ...

It all started with the nginx configuration. It turned out that I needed to redirect all requests, except for a certain REST location, to index.html, where I started to work on angular. The first configuration looked like this:
')
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; } } } 

Here all our API is located by location / api-location. Angular $ routeProvider configuration:

 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 }); }]); 

But how will the angular site be indexed? The decision at once came to a head that it is necessary to give a statics separately. A little googling, found information about? _Escaped_fragment. It was necessary to separately generate statics and give on requests of the type truemania.ru/?_escaped_fragment truemania.ru/?_escaped_fragment ready for indexing fragments.

During a short search, I came across an article where the indexing mechanism for angular sites was described in detail, just for the nginx server. A few more locations have been added to the configuration:

 if ($args ~ "_escaped_fragment_=(.*)") { rewrite ^ /snapshot${uri}; } location /snapshot { proxy_pass http://help.truemania.ru/snapshot; proxy_connect_timeout 60s; } 

We create a second-level domain, where requests for the return of ready-made fragments will be processed. On request type
 http://truemania.ru/user/50?_escaped_fragment_= 

You'll get
 http://help.truemania.ru/snapshot/user/50 


It remains only to create all the necessary impressions that need to give the search bot. In doing so, I used the standards of micromaps schema.org . Who is not familiar with the world of semantic markup, I advise you to read in this article .

Creating a dynamic sitemap is described in great detail in this article - I advise you to read. But it is a pity that the solution for the first version of Yii is described here. Sitemap is created with each new request again, which can cause a very high load on the server. Exit - creating a console controller and updating the sitemap at intervals of 10 minutes using crontab. Having changed the source code quite a bit, I got a good solution for the Yii2 console:

 <?php namespace console\models; use Yii; /** * @author ElisDN <mail@elisdn.ru> * @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->urlManager->getBaseUrl(); $item = array( 'loc' => $host . $url, 'changefreq' => $changeFreq, 'priority' => $priority ); if ($lastMod) $item['lastmod'] = $this->dateToW3C($lastMod); $this->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->urlManager->getBaseUrl(); foreach ($models as $model) { $item = array( 'loc' => $host . $model->getUrl(), 'changefreq' => $changeFreq, 'priority' => $priority ); if ($model->hasAttribute('create_date')) $item['lastmod'] = $this->dateToW3C($model->create_date); $this->items[] = $item; } } /** * @return string XML code */ public function render() { $dom = new \DOMDocument('1.0', 'utf-8'); $urlset = $dom->createElement('urlset'); $urlset->setAttribute('xmlns','http://www.sitemaps.org/schemas/sitemap/0.9'); foreach($this->items as $item) { $url = $dom->createElement('url'); foreach ($item as $key=>$value) { $elem = $dom->createElement($key); $elem->appendChild($dom->createTextNode($value)); $url->appendChild($elem); } $urlset->appendChild($url); } $dom->appendChild($urlset); return $dom->saveXML(); } protected function dateToW3C($date) { if (is_int($date)) return date(DATE_W3C, $date); else return date(DATE_W3C, strtotime($date)); } } 

Console action:

 public function actionGetsitemap() { $sitemap = new DSitemap(); $sitemap->addModels(Model1::find()->active()->all(), DSitemap::HOURLY); $sitemap->addModels(Model2::find()->all(), DSitemap::HOURLY); $sitemap->addModels(Model3::find()->all(), DSitemap::HOURLY); $path = \Yii::getAlias("@frontend/web") . DIRECTORY_SEPARATOR . "sitemap.xml"; return file_put_contents($path, $sitemap->render()); } 

Configuration crontab to run every 10 min.

*/10 * * * * /path/to/yii cron/getsitemap >> /path/to/log/command_log/getsitemap.log;


This solution is optimal and very productive, so we get quite relevant data. If necessary, you can recreate the sitemap with more frequent or less frequent intervals.

Then went to work on a beautiful output of links in social networks. For those who are not in the subject, this is the markup standard http://ogp.me/ . I was very disappointed that the bots did not understand the meta
—Tag:

 <meta name="fragment" content="!" /> 

At this stage, I stumbled a bit, since there was no solution elementary in the forehead. I wanted to make bots understand that there is a real fragment behind the page. Googling, I decided to give fragments on the user-agent. I had to study the documentation for the corresponding service to get an approximate user-agent, which could be extracted using regular expressions.

My configuration for sharing statics to social networking bots:

 #     user-agent —    ,   if ( $http_user_agent ~* (facebookexternalhit|facebot|twitterbot|tinterest|google.*snippet|vk.com|vkshare) ){ rewrite ^ /snapshot${uri}; } 

Naturally, it remains to include information on the open graph markup in my casts.

Next, I wanted to use websocket in some very beneficial points - it was perfectly suited for solving such tasks as online / offline state for the user. Of course, the websocket themselves is a very non-standard thing for PHP, but a ready-made solution was quickly found - http://socketo.me/ .

It remains only to understand how I run these sockets on Yii2 in ubuntu. Actually, I created a console controller, and this is what action looked like:

 public function actionWebsocketaction() { $server = IoServer::factory( new HttpServer( new WsServer( new UserOnline() ) ), 8099, '127.0.0.1' ); $server->run(); } 

Well, and further I apply the UserOnline model itself:

 <?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->resourceId})\n"; } //   ,     online public function onMessage(ConnectionInterface $from, $username) { $model = UserOnlineConnections::findByUsername($username); if(empty($model)) { $model = new UserOnlineConnections(); //     ,     $model->username = preg_replace('/\\r\\n$/', '', $username); $model->conn_id = $from->resourceId; if(!($model->validate() && $model->save())) throw new ServerErrorHttpException(json_encode($model->getErrors())); } else { $model->conn_id = $from->resourceId; if(!($model->validate() && $model->save())) throw new ServerErrorHttpException(json_encode($model->getErrors())); } echo "New user online $model->username \n"; self::setUserStatus($username, self::USER_ONLINE); } //   —   offline public function onClose(ConnectionInterface $conn) { echo "Close connection! ({$conn->resourceId})\n"; $username = UserOnlineConnections::findByConnId($conn->resourceId)->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->resourceId)->username; if($username) { //Set status offline echo "User offline $username \n"; self::setUserStatus($username, self::USER_OFFLINE); echo "An error has occurred: {$e->getMessage()}\n"; $conn->close(); } } /** *     * @param $username * @param $status * @return bool * @throws ServerErrorHttpException */ public function setUserStatus($username, $status) { $model = User::findByUsername($username); if ($model) { $model->online = $status; if(!($model->validate() && $model->save())) throw new ServerErrorHttpException(json_encode($model->getErrors())); return true; } if($status == self::USER_OFFLINE) { UserOnlineConnections::deleteAll( "username=".$username ); } } } 

It remains only to run it all. It was necessary to conclude stderr in stdout, but for some reason &> did not want to work. The solution came through nohup. The launch of the socket looked like this:

 nohup /path/to/yii ws/useronline >> /path/to/log/command_log/useronline.log; 

Also in the event of a fall, you must restart this process. I didn’t find a more elegant solution, like to run the command in crontab every minute. If the port is busy, nothing will happen (an error will be issued), but if the port is free, the process will be restarted.

Next, you need to proxy the websocet using nginx. And here the following lines were added to the configuration:

 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; } } 

Now our web socket will be available at ws: //truemania.ru/useronline.

And the last thing I encountered (from the settings of the web server) in the development process is the transition to the https protocol. The problem was the following - facebook and google + wanted the pictures to be given over http, and stubbornly did not want to display a picture in the preview. To do this, I had to change the configuration, namely, to force the server to give the media files via http:

 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; } 

Also, after the protocol has changed, the call to socet occurs at wss: //truemania.ru/useronline.

Well, if you like it, then in the next article I will tell you how the web application itself + backend wrote, describe interesting solutions on angular - such as authorization, permissions for authorized and unauthorized users, use requireJS.

Source: https://habr.com/ru/post/274867/


All Articles