PlayFramework 2.2とScalaでRSSを再生する





良い一日、親愛なるハブラフチアン。



私たち、 pogromプログラマーは、新しい言語XまたはフレームワークYを学習するときに、同じ問題に遭遇することが非常に多くあります。 X / Yの長所と短所を示すことができますが、それほど時間はかかりません。



私の仲間はよく似た質問をしました。 その結果、RSSリーダーを作成するという簡単な考えが生まれました。 ここで、ネットワーク、XMLパーサー、およびデータベースを操作できます。テンプレートエンジンを見てください。 はい、あなたは決して知りません。



したがって、ここからは、バックエンドのPlay Framework 2.2 + Scala + MongoDBスタックと、フロントエンドのAngularJS + CoffeeScriptへのエキサイティングな旅が始まります。



TL; DR
プロジェクト全体は、Scalaでは250〜300行、CSでは150行に配置されました。 まあ、いくつかのHTML。

Bitbucketで利用可能なコード




そして、最初の目的は質問です。なぜJavaではなくScalaなのでしょうか。 そして、なぜ同じPlayではなくPlayなのでしょうか?


答えは非常にシンプルで主観的です。

Scalaは、コードのために、より高いレベルの抽象化とより少ないコードを提供します。 あらゆる場面で200のメソッドを備えた標準リストのドキュメントを見たとき...真剣に、自分で試してください。

フレームワークの選択に関しては、Liftの簡単な例では、localhostで〜150ミリ秒の間ページが表示されましたが、これはデータベースを使用していません。 同時に、同じマシンと同じJVMプレイでは、約5〜10ミリ秒かかりました。 わからない、多分星はそうだね。

そして、プレイコンソールでかわいい。



Playをインストールして開始する方法については、公式ドキュメント(すべて、 お気に入りのIDEのプロジェクトを生成するまで)にすべてが完全に盛り込まれているため、一部を見逃してしまいます。



リクエストパス


アプリケーションを解析する最も明白な方法は、クライアントの要求に従うことです。

特にNetty上に構築されているため、フレームワーク自体によるリクエスト処理のブラックボックスをスキップする方が良いでしょう。 たぶん中国へ。

各川は小川で始まるため、Playのアプリケーションはルーティングで始まります。これは、

conf /ルート
 #ルート
 #このファイルは、すべてのアプリケーションルートを定義します(優先順位の高いルートが最初)
 #~~~~

 #ニュースを入手
 GET / news controllers.NewsController.news(tag:String?= ""、PubDate:Int?=(System.currentTimeMillis()/ 1000).toInt)

 #解析ニュース
 GET / parse controllers.NewsController.parseRSS

 #タグを取得
 GET / tags controllers.TagsController.tags

 #静的リソースを/パブリックフォルダーから/アセットのURLパスにマップします
 GET / asset / * file controllers.Assets.at(path = "/ public"、file)

 #ホームページ
 GET / controllers.Application.index





マージンのマージン:

指定されたメソッドに渡される引数にデフォルト値を設定する可能性に加えて、式を指定できることを別に強調したいと思います。 たとえば、現在のタイムスタンプを取得します。

ちなみに、Playのルーティングは非常に機能的で、リクエストを処理するときの正規表現までです。



チケットをプレゼント!


タイトルから推測できるように、ストーリーはコントローラーで続きます。 Playでは、ユーザーコントローラーはcontrollers



パッケージに含まれ、 Controller



トレイトを使用し、ルーティングに従ってユーザーリクエストを受け入れて応答するメソッドを持つオブジェクトです。

アプリケーションはAJAXを介してサーバーからデータを受信するため、メインページをレンダリングするためのコントローラーは正方形として簡単であり、HTML / CS / JSスクリプトの読み込みにのみ必要です。



入力せずに20行
 package controllers import play.api.mvc._ /** * playRSS entry point */ object Application extends Controller { /** * Main page. So it begins... * @return */ def index = Action { Ok(views.html.index()) } }
      
      







Ok



は、ページのヘッダーと本文を含むplay.api.mvc.SimpleResult



インスタンスを返します。 最も注意深い人が推測したように、サーバーからの応答は200 OK



ます。



しかし

アプリケーション全体の完全なコントローラーが20行に収まる場合、ルーブルで書いている可能性が非常に高くなります。



それでは、AJAXクライアントにニュースを受信するリクエストを与える最良の方法は何ですか? そう、JSON。

NewsController



NewsController







オブジェクトNewsController
 package controllers import play.api.mvc._ import scala.concurrent._ import models.News import play.api.libs.concurrent.Execution.Implicits.defaultContext import models.parsers.Parser import com.mongodb.casbah.Imports._ object NewsController extends Controller { /** * Get news JSON * @param tag optional tag filter * @param pubDate optional pubDate filter for loading news before this UNIX timestamp * @return */ def news(tag: String, pubDate: Int) = Action.async { val futureNews = Future { try { News asJson News.allNews(tag, pubDate) } catch { case e: MongoException => throw e } } futureNews.map { news => Ok(news).as("application/json") }.recover { case e: MongoException => InternalServerError("{error: 'DB Error: " + e.getMessage + "'}").as("application/json") } } /** * Start new RSS parsing and return first N news * @return */ def parseRSS = Action.async { val futureParse = scala.concurrent.Future { try { Parser.downloadItems(News.addNews(_)) News asJson News.allNews() } catch { case e: Exception => throw e } } futureParse.map(newsJson => Ok(newsJson).as("application/json")).recover { case e: MongoException => InternalServerError("{error: 'DB Error: " + e.getMessage + "'}").as("application/json") case e: Exception => InternalServerError("{error: 'Parse Error: " + e.getMessage + "'}").as("application/json") } } }
      
      







Future



Async



。 ここで初めて興味深いものになります。

まず、Playは非同期であり、原則として、ストリームを操作する必要はまったくありません。 しかし、データベースへのアクセス、ファイルからのデータの読み取り、または別の遅いI / O手順を実行するために数値π緊急に計算する必要がある場合、 Future



が助けになります。これにより、メインストリームをブロックせずに非同期で操作を実行できます。 Future



は実行に別のコンテキストを使用するため、スレッドについて心配する必要はありません。

関数はFuture[SimpleResult]



ではなくFuture[SimpleResult]



返すようになったため、 ActionBuilder



トレイトのasync



メソッドがActionBuilder



(これはAction



オブジェクトを使用します)



風景


この非同期の悪夢をやめて、私たちの目を引くテンプレートを見てみましょう。 Playは通常のHTMLで動作する機能を提供します。 Scalaコードを挿入した通常のHTML。 テンプレートは自動的にソースファイルにコンパイルされ、パラメータを渡したり、他のテンプレートを接続(呼び出し)できる通常の機能です。 ところで、多くの人は、非常にHTMLをコードにコンパイルする時間が比較的遅いため、新しいテンプレートエンジンを嫌っていました。 元気です。

index.scala.html
 <!DOCTYPE html> <html> <head> <title> playRSS </title> <link rel="shortcut icon" href='@routes.Assets.at("images/favicon.png")' type="image/png"> <link rel="stylesheet" href="http://netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css"/> <link rel="stylesheet" href='@routes.Assets.at("stylesheets/main.css")'> @helper.requireJs(core = routes.Assets.at("javascripts/require.js").url, module = routes.Assets.at("javascripts/main").url) </head> <body> <div class="container" id="container" ng-controller="MainCtrl"> <a href="/"><h1>playRSS</h1></a> @control() <div class="row"> <div class="col-lg-12"> @news() </div> </div> </div> </body> </html>
      
      







ソースからわかるように、ちょっとした魔法です。 @helper



は、フレームワーク自体が提供する@helper



接続し、フロントエンドが初期化されるmain.jsへのパスを示します。 @news()



および@control()



は、それぞれnews.scala.htmlおよびcontrol.scala.htmlテンプレートです。 関数を実行し、現在のテンプレート内に結果を表示します。 いいね

そしてまた

if / elseなどのループを使用できます。 詳細なドキュメントがあります



カスバ山


データベースでの作業を続けましょう。 私の場合、Mongoが選択されました。 私はテーブルを作成するのが面倒だから:)

Casbahは、麺棒でMongoDBを操作するための公式ドライバーです。 その利点は、シンプルさと機能性を同時に備えていることです。 そして、主な欠点は最後に考慮されます。



ドライバーはかなり単純に接続されています。





そして、コードについて少し。 リーダーは複雑ではないので、MongoDBから貧しい人々のコレクションに配布するオブジェクトが作成されました。 これまでのところ、DAOまたはDIをフェンスする正しい言葉は単に不要です。



オブジェクトデータベース
 package models import com.mongodb.casbah.Imports._ import play.api.Play /** * Simple object for DB connection */ object Database { private val db = MongoClient( Play.current.configuration.getString("mongo.host").get, Play.current.configuration.getInt("mongo.port").get). getDB(Play.current.configuration.getString("mongo.db").get) /** * Get collection by its name * @param collectionName * @return */ def collection(collectionName:String) = db(collectionName) /** * Clear collection by its name * @param collectionName * @return */ def clearCollection(collectionName:String) = db(collectionName).remove(MongoDBObject()) }
      
      







マージンのマージン:

Scalaでは、オブジェクトは実際にはシングルトーンです。 退屈モードを有効にすると、静的メソッドを持つ匿名クラスが作成され、インスタンス化されます(Java / JVMビューで)。 そのため、オブジェクトが作成されると接続が確立され、アプリケーションの作業サイクル全体で利用可能になります。


今こそ、ScalaとCasbahのベースとの連携を実証するときです。



オブジェクトニュース
 /** * Default news container * @param id MongoID * @param title * @param link * @param content * @param tags Sequence of tags. Since categories could be joined into one * @param pubDate */ case class News(val id: String = "0", val title: String, val link: String, val content: String, val tags: Seq[String], val pubDate: Long) /** * News object allows to operate with news in database. Companion object for News class */ object News { .... /** * Method to add news to database * @param news filled News object * @return */ def addNews(news: News) = { val toInsert = MongoDBObject("title" -> news.title, "content" -> news.content, "link" -> news.link, "tags" -> news.tags, "pubDate" -> news.pubDate) try { col.insert(toInsert) } catch { case e: Exception => } } .... /** * Get news from DB * @param filter filter for find() method * @param sort object for sorting. by default sorts by pubDate * @param limit limit for news select. by default equals to newsLimit * @return */ def getNews(filter: MongoDBObject, sort: MongoDBObject = MongoDBObject("pubDate" -> -1), limit: Int = newsLimit): Array[News] = { try { col.find(filter). sort(sort). limit(limit). map((o: DBObject) => { new News( id = o.as[ObjectId]("_id").toString, title = o.as[String]("title"), link = o.as[String]("link"), content = o.as[String]("content"), tags = o.as[MongoDBList]("tags").map(_.toString), pubDate = o.as[Long]("pubDate")) }).toArray } catch { case e: MongoException => throw e } } }
      
      







MongoDB、API、およびケースクラスのNewsインスタンスのインスタンスの簡単な充填を扱ったすべての人に精通しています。 これまでのところ、すべてが基本です。 でも多すぎる。

もっと面白いものが必要です。 集約はどうですか?



タグを引き出す
 /** * News tag container * @param name * @param total */ case class Tags(name: String, total: Int) /** * Tags object allows to operate with tags in DB */ object Tags { /** * News collection contains all tag info */ private val col: MongoCollection = Database.collection("news") /** * Get all tags as [{name: "", total: 0}] array of objects * @return */ def allTags: Array[Tags] = { val group = MongoDBObject("$group" -> MongoDBObject( "_id" -> "$tags", "total" -> MongoDBObject("$sum" -> 1) )) val sort = MongoDBObject("$sort" -> MongoDBObject("total"-> -1)) try { col.aggregate(group,sort).results.map((o: DBObject) => { val name = o.as[MongoDBList]("_id").toSeq.mkString(", ") val total = o.as[Int]("total") Tags(name, total) }).toArray } catch { case e: MongoException => throw e } } }
      
      







.aggregate



使用すると、 .aggregate



なしで不思議なことができます。 また、Scalaでの作業の原則は、コンソールの場合と同じです。 コンマだけで区切られた一種のパイプライン。 タグでグループ化され、合計で同じ合計し、全体をソートしました。 素晴らしい。



ところで、 カスバは要塞です



JSON-XMLを使用しています


決してあきらめない

あなたを失望させない



静的に型付けされた言語の場合、この場合のXML / JSONの操作はデマのように見えるためです。 疑わしいほど短い。

実際、ScalaでのXML解析は(Javaの大規模なファクトリーの後の)私の目を楽しませています。

XMLパーサー
 package models.parsers import scala.xml._ import models.News import java.util.Locale import java.text.{SimpleDateFormat, ParseException} import java.text._ import play.api.Play import collection.JavaConversions._ /** * Simple XML parser */ object Parser { /** * RSS urls from application.conf */ val urls = try { Play.current.configuration.getStringList("rss.urls").map(_.toList).getOrElse(List()) } catch { case e: Throwable => List() } /** * Download and parse XML, fill News object and pass it to callback * @param cb */ def downloadItems(cb: (News) => Unit) = { urls.foreach { (url: String) => try { parseItem(XML.load(url)).foreach(cb(_)) } catch { case e: Exception => throw e } } } /** * Parse standart RSS time * @param s * @return */ def parseDateTime(s: String): Long = { try { new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.ENGLISH).parse(s).getTime / 1000 } catch { case e: ParseException => 0 } } /** * For all items in RSS parse its content and return list of News objects * @param xml * @return */ def parseItem(xml: Elem): List[News] = (xml \\ "item").map(buildNews(_)).toList /** * Fill and return News object * @param node * @return */ def buildNews(node: Node) = new News( title = (node \\ "title").text, link = (node \\ "link").text, content = (node \\ "description").text, pubDate = parseDateTime((node \\ "pubDate").text), tags = Seq((node \\ "category").text)) }
      
      







同意する

最初に、\または\\という形式の名前のメソッドは、混乱状態に陥ります。 ただし、JavaからBigIntegerを呼び出す場合、これはある程度意味があります。


JSONはどうですか? ScalaのネイティブJSONは、これまでのところ主観的なものではありません。 遅くて怖い。

困難な時期には、Playとplay.api.libs.json



パッケージからのその書き込み/読み取りがplay.api.libs.json



ます。 PHP 5.4のJsonSerializable



インターフェイスを知っているJsonSerializable



ますか? Playの方が簡単です!



JSON書き込み
 case class News(val id: String = "0", val title: String, val link: String, val content: String, val tags: Seq[String], val pubDate: Long) /** * News object allows to operate with news in database. Companion object for News class */ object News { /** * Play Magic * @return */ implicit def newsWrites = Json.writes[News] /** * Converts array of news to json * @param src Array of News instances * @return JSON string */ def asJson(src: Array[News]) = { Json.stringify(Json.toJson(src)) } }
      
      







単純なシリアル化の場合の1行のメソッドsomeObjectWrites



は、すべての問題を取り除きます。 Scalaの暗黙的な変換は、実際に使用される強力で便利なツールです。

しかし、これは非常に平凡なケースです。 特別なものや複雑なものが必要な場合は、 機能主義と組み合わせが役立ちます。



とげを通して星へ


ユーザーが退屈し、スクリプトによってサーバーに送信された要求への応答を待っている間...待ってください。 別のフロントエンド。

約束どおり、CoffeeScriptとAngularJSが使用されました。 実稼働環境でこのバンドルの使用を開始した後、ユーザーインターフェイスを開発する際の苦痛の数は78.5%減少しました。 コードの量が好きです。

この理由から、これらのスタイリッシュでファッショナブルな若者向けテクノロジーを読者に使用することにしました。 また、私の選択したフレームワークにはCoffeeScriptとLESSコンパイラが搭載されているためです。

実際、経験豊富な開発者は新しい興味深いことを何も学ばないので、興味深いトリックをいくつか紹介します。



多くの場合、角度コントローラー間でデータを交換する必要があります。 そして、洗練された紳士だけが行かないもの(localStorageへの書き込みなど)...

そして、が開きます。

サービスを作成し、必要なコントローラーに実装するだけで十分です
発表する

 define ["angular","ngInfinite"],(angular,infiniteScroll) -> newsModule = angular.module("News", ['infinite-scroll']) newsModule.factory 'broadcastService', ["$rootScope", ($rootScope) -> broadcastService = message: {}, broadcast: (sub, msg)-> if typeof msg == "number" then msg = {} this.message[sub] = angular.copy msg $rootScope.$broadcast(sub) ] newsModule
      
      







送ります

 define ["app/NewsModule"], (newsModule)-> newsModule.controller "PanelCtrl", ["$scope", "$http", "broadcastService", ($scope, $http, broadcastService)-> $scope.loadByTag = (tag) -> if tag.active tag.active = false broadcastService.broadcast("loadAll",0) else broadcastService.broadcast("loadByTag",tag.name) ]
      
      







取得します

 define ["app/NewsModule","url"], (newsModule,urlParser)-> newsModule.controller "NewsCtrl", ["$scope", "$http", "broadcastService", ($scope, $http, broadcastService)-> #recieving message $scope.$on "loadAll", ()-> $scope.after = 0 $scope.tag = false $scope.busy = false $scope.loadByTag() ]
      
      







角度で

サービスはシングルトンです。 したがって、インスタンスを作成せずにメッセージをやり取りできます。



すべて来る


このような混intoとした腸内への行き帰りの旅の後、要約する価値があります。

長所と短所、致命的ではなく、誰もが自分で選択する必要があります。 貨物を栽培するのではなく、適切な場所でツールを使用しますか?



好きだった:



気に入らなかった:





また、開発時には、Futureを使用してブロック操作を操作する際に注意する必要があります。 ただし、1つだけあります。 メインの実行スレッドがブロックされないという事実にもかかわらず、別のスレッドはブロックされます。 スレッドが十分にあり、競合するリクエストがあまり多くない場合は良いことです。 もしもし? この場合、Play開発者は、同じデータベースに対して非同期ドライバーを使用することをお勧めします。 たとえば、CasbahではなくReactiveMongo。 または、少なくともアクターとスレッドプールを構成します。 しかし、これは全く異なる話です...



ご清聴ありがとうございました。



PS

この落書きが少し見えた場合、ここにBitbucketリポジトリがあります



All Articles