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のリポゞトリがありたす 。

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


All Articles