僕とDDDとClean ArchitectureとやっぱりDDD
2022/04/21更新 ふりかえってみて、この記事は手段と目的をごっちゃにしちゃった自分がよくわかる記事です。 DDDは「どうやってコードを書くか」が問題ではありません。その点を勘違いしちゃってるエンジニアの話として、続きを読みたい人は読んでください🙏
DDD(Domain Driven Design)って難しいですよね。難しい難しいとばかり考えていた僕もようやく最近になって少しずつわかってきた気がします。そのきっかけとなった書籍と僕のストーリーを本記事で紹介できたらと思います。
TL;DR
- Clean Architectureはなんとなくわかる
- DDDは難しい
と感じている人は「Domain-Driven Design in PHP」を読むと道が拓けるかもしれない。
僕とDDD
DDDといえばEvansのドメイン駆動設計:
そしてVernonの実践ドメイン駆動設計:
が有名です。「ドメイン駆動設計」の内容をより実際の設計に当てはめている「実践ドメイン駆動設計」の方がわかりやすいという意見を聞いたりしますが、僕にとっては両方ともなかなかに難易度の高い書籍です。少しずつ読んでは「なんとなく言ってることはわかる」と思いながらも今ひとつ身についた気がしない類のものでした。このように感じている人はそれなりにいるんじゃないかなと思ってます。
僕とClean Architecture
DDD難しいし、実際のプロジェクトに適用しようと思っても手が止まる。。。
ということでしばらく本棚に戻していたのですが、次に見つけたのがClean Architectureなるものでした。Qiitaの記事で見つけたのがきっかけだったのですが、実際に自分の馴染みのあるコード(Rest APIだったりAndroidのアプリだったり)で書かれていたこともあって「これは実際のプロジェクトでも書けそう」と思えたのが「僕とDDD」の出会いとの大きな違いかなと思います。
実際にはQiitaの記事だけでは心もとないので、以下のような記事や講演で学びながら実案件でも適用していきました。
Uncle BobのClean Architectureの記事
Uncle BobによるClean Architectureの講演
(いくつかありますが、最初にみて面白かったのがこれです)
Uncle Bobと息子のMicah MartinがペアプロしながらWebシステムを作る講義
GitHub上のソースコード
Clean Architecture(書籍)
僕がClean Architecture学び始めたころはまだ無かったのですが、正式に書籍も発売されています。
Clean Architectureでモヤッとしたこと
Clean Architectureを採用したプロジェクトでは、オレオレClean Architectureでありながらもそれなりにルールを守った自信はあります(特に依存関係やScreaming Architecture部分)。が、いざ実装するとなったときに「ロジックをどこに置くのか」というところで結構悩むことがありました。
- ○○ロジックはUseCase(Interactor)に入れる?
- △△ロジックはEntityに入れる?
- 同じようなロジック(例えばValidation)が複数箇所にあるけどこれは誰の役割?
- などなど
Clean Architectureの書籍に「第20章 ビジネスルール」という章があるのですが、
- エンティティは最重要ビジネスルールを含む
- ユースケースはアプリケーション固有のビジネスルールを含む
と言われてもなかなかエモくて直感的にわかりにくいと感じました。そして結構こういうところがコードレビューでチーム内で意見が分かれたりして変に時間がかかっちゃったりしたのを覚えています。「Clean Architecture良い!」という思いとともに「まだすっきりしない点が残る」という状態が続きました。「やっぱりDDDなマインドが足りていないからなのか?」ということでDDD学習に戻ることにしたのがここ1年くらいの話です。
DDDとClean Architecture
「実践ドメイン駆動設計」を再び読み直してみたのですが、不思議なことにClean Architectureを経てから読み直してみると見えてくるものが変わりました。たぶんですけど、
馴染みのあるコードからClean Architectureを学んだ ↓ Clean Architecture(書籍)を読んだら答え合わせのように腑に落ちる点が多かった ↓ この書籍内にドメイン駆動的な話も入っていた(んじゃないかと思う) ↓ 無意識のうちにDDDの概念が少し増えた状態で改めて「実践ドメイン駆動設計」を読み始めた ↓ 書籍内に出てくる単語や概念に馴染みが増していたので以前より想像しやすくなった
ということだと思います。
ただ、以前より理解がしやすくなったとは言え、モヤッとは残ったままで、以下のトピックを理解したいと思いつつもいまいちピンとこない感じでした。
- Application Service
- Domain Service
- Aggregate
- Entity
- Value Object
- Domain Event
結局繰り返しになるんですけど、「なんとなく言ってることはわかる」で止まってしまって、いざ自分のコードに入れようとすると「どうしたら良いんだろ?」になっちゃうんですね。(つまりわかっていない)
今にして思うと「わかろうとするモチベーションが足りなかった」だけな気もします。
そんな中最近「Domain-Driven-Design in PHP」という本に出会いました。
Domain-Driven-Design in PHP
O'Reillyのサブスクリプションがある方はこちらのリンクからも読むことができます!
この書籍は結構前から知っていたんですけど、PHPというだけで避けてました。(ごめんなさい。でもそう思った人は僕だけじゃないはず!)
最近は毎日のようにPHP触ってますし、PHPへの抵抗も昔よりだいぶ無くなったので読んで見ることにしました。
久しぶりにこんなにワクワクする技術書を読んだ。
というのが素直な感想です。僕がちょうどモヤッとしているところとジャストミートな内容だった、というのもあるんだと思いますが:
- ちょうど良いボリューム感
- 「ドメイン駆動設計」, 「実践ドメイン駆動設計」, そして「Clean Architecture」にも言及している
- DDDの概念とリンクする豊富なサンプルコード
- 書籍用に作ったシステムがGitHub上に公開してある
という4つのポイントが僕の中で高評価です。
ちょうど良いボリューム感
ページ数は394ページとそれなりにあるのですが、コードが占めている比率が高いので思ったよりすぐ読めます(僕は3,4日で読めました)。
目次は以下の通り。
* Getting Started with Domain-Driven Design * Architectural Styles * Value Objects * Entities * Services * Domain Events * Modules * Aggregates * Factories * Repositories * Application * Integrating Bounded Contexts * Appendix: Hexagonal Architecture with PHP
「実践ドメイン駆動設計」と比べてみても、概ねトピックはカバーできていると言えます。
* Getting Started with DDD * Domains, Subdomains, and Bounded Contexts * Context Maps * Architecture * Entities * Value Objects * Services * Domain Events * Modules * Aggregates * Factories * Repositories * Integrating Bounded Contexts * Application * Aggregates and Event Sourcing: A+ES
「ドメイン駆動設計」, 「実践ドメイン駆動設計」, そして「Clean Architecture」にも言及している
要所要所でEvansの「ドメイン駆動設計」とVernonの「実践ドメイン駆動設計」の引用を使って説明をしてくれます。
例えばDomain Eventについて「実践ドメイン駆動設計」からは:
Vaughn Vernon defines a Domain Event as:
An occurrence of something that happened in the domain.
そして「ドメイン駆動設計」からは:
Eric Evans defines a Domain Event as:
A full-fledged part of the Domain Model, a representation of something that happened in the Domain. Ignore irrelevant Domain activity while making explicit the events that the Domain Experts want to track or be notified of, or which are associated with state change in the other Model objects.
というように両者を引用で出してくれます。書籍内でのわかりやすさも増すのですが、この引用をもとに「ドメイン駆動設計」と「実践ドメイン駆動設計」をリファランスとして要所要所で読み直していける点もすごく良いです。
また、さらにうれしいのはUncle BobのClean Architectureにも言及している点です。(この書籍が発売したときには「Clean Architecture」はまだ出版されていないので、あくまでUncle Bob(Robert C. Martin)について言及しています)
As Robert C. Martin says: The Web is a delivery mechanism [...] Your system architecture should be as ignorant as possible about how it is to be delivered. You should be able to deliver it as a console app, a web app, or even a web service app, without undue complication or any change to the fundamental architecture.
DDDの概念とリンクする豊富なサンプルコード
(ちょっと大げさですけど)文章よりコードが多いんじゃないかってくらいサンプルコードが都度紹介されています。そして、コードの要所要所に関して丁寧にわかりやすく説明がされているので、DDDの概念とコードがどう結びついていくのかが想像しやすいです。
書籍用に作ったシステムがGitHub上に公開してある
書籍用に作られた「Last Wishes」というシステムのコードがGitHub上に公開されています。
コンセプトは「ユーザーが亡くなった場合に、あらかじめ登録されていたお願い(Wish)を、指定したメールアドレス宛に送る」というものです。お願い(Wish)を登録した場合にはゲーム要素もあって、一定ポイントたまるとバッジが付与されるようになっています。
このシステムは大きく2つのアプリケーションに分かれています。
- ユーザーのWishを管理し、メールを送信するコア部分のアプリ
- ゲーム要素を提供する、Wishに応じたポイントを換算し、ユーザー毎に管理するアプリ
それが各々以下で公開されています。
システム構成は下の図のようになっていて、それなりにまともなメンツが揃ったシステムになっています。
サンプルアプリではあまり見られない:
- CQRSとEvent Sourcing
- Bounded Context(Last WishesとLast Wishes Gamify)
というのも学ぶことができるのでかなり貴重な素材だと思います。このシステムと書籍を照らし合わせていくことで答え合わせもしていけるので、理解が進みやすいという印象もあります。
まとめ
- 「Domain-Driven-Design in PHP」を読んだ
- 馴染みやすいコードでDDDを体系的に学べる良書だった
- 「ドメイン駆動設計」「実践ドメイン駆動設計」「Clean Architecture」をリンクさせてくれる1冊だと思った
と、思いをつらつらと書きましたが、同じように迷い、悩んでいる方がもしいるのであれば「Domain-Driven-Design in PHP」はかなりおすすめできる1冊なのでぜひ読んでみてください!ということを伝えたかったです。
この記事が何かしら一歩先に進めるきっかけになれば幸いです。
今後の予定
- Vernonの「実践ドメイン駆動設計」を読み直す
- Evansの「ドメイン駆動設計」を読み直す
- Uncle Bobの「Clean Architecture」を読み直す
- Last WishesをLumenに移行してみる
- ↑ができたらgoにも移行してみる
- 自分でScratchからアプリを作ってみる
という流れで精進していけたらいいなと思います!
Zoomで読書会「アーキ部#2」に参加しました!
会社のSlackで流れていたのを見たのがきっかけで「アーキ部」なるものに初参加しました!
connpassでのイベントはこちら↓
概要
connpassにも記載されてますが、Release It!: Design and Deploy Production-Ready Software (Second Edition) のオンライン読書会です!
Release It!: Design and Deploy Production-Ready Software
- 作者: Michael T. Nygard
- 出版社/メーカー: Pragmatic Bookshelf
- 発売日: 2018/01/18
- メディア: ペーパーバック
- この商品を含むブログを見る
目次
- Ch.4 Stability Antipatterns(安定性のアンチパターン)
- Integration Points(統合点)
- Chain Reactions(連鎖反応)
- Cascading Failures(カスケード障害)
- Users(ユーザ) ←ここの途中まで
参加するにあたって準備するもの
Zoomのクライアント
勉強会はオンラインで開かれていて、Zoomを使います。今回は20時ごろにconnpass経由でZoomのURLが送られてきました。Zoomのクライアントはあらかじめ用意しておくとスムーズに参加できそうです。
参加人数は今回40人を超えていましたが、主催者の共有画面を参照するだけですし、参加者もみなビデオはOFFにしてるので回線速度が気になることは無かったです!
Release It!: Design and Deploy Production-Ready Software (Second Edition)
本は無くてもOKですが、あればより理解を深めることができるんじゃないかなと思います。Second Editionは現時点(2019-07-26)では英語版しかありません。(間違えて初版の日本語版を買わないように注意!)
ちなみにSafari Books Onlineを購読している人であれば下記にあります。
ドリンク、軽食
オンライン勉強会なのでご自由に、というところですね!マイクONにしてボリボリ何か食べちゃわないようには気をつけたほうがいいですね(笑)
参加してみた感想まとめ
- 司会(@kawasimaさん) が本の内容とともに実体験を話してくれるのですごく良い
- 本の内容とともにたまに関連記事を紹介してくれるのうれしい
- ハッシュタグ(#アーキ部)で一緒に参加している人のツイートも見れるの良い
- オンラインなので、勉強会が普段遠隔にしかない人(例えば山に住んでるような僕)にとっては本当にうれしい
- オンライン勉強会に懇親会が最後に無いのが寂しいですが、この課題は難しい
本の内容に関してピックアップ
- 統合点はシステムにとって殺し屋No.1
- (ダイアグラムを見て)新米アーキテクトは箱にフォーカスするが、ベテランアーキテクトは、矢印により着目する
- ぐっすり寝れるかどうかが安定性のKPI
- 明らかに遅いレスポンスはレスポンスが無いことよりも悪い
- ディフェンシブなコードになってないと、道連れに自システムも障害してしまうことになる
- バグのあるアプリケーションをオートスケールすると、札束がとんでいく
次回!
たぶん日程はまだ決まっていないのですが、connpassのメンバーになっておけば通知がとんできますので、興味がある方はぜひ!
このイベントをきっかけに「Release It!」読み始めましたが、かなり面白いです。が、リアルなので胃も痛くなってきます。書評にも挑戦したいですね。
関連記事
「CircleCIのconfig.ymlを守ろうとした話」を発表しました
「【大阪】CircleCI ユーザーコミュニティミートアップ #1」に参加し、「CircleCIのconfig.ymlを守ろうとした話」という題名でLTをしてきました。
発表資料
ストーリー
組織の中でCircleCIを多数のリポジトリで使い始めたとき、config.ymlに一定のルールを課したくなりませんか?セキュリティ的な意味と、Lint的な意味の両方で僕は欲しくなってきます。
ということでconfig.yml
にどのようにガバナンスを効かせることができるのか、という試みについて話しました。主にconftest
でCIにルールを課す方法について述べています。
興味がある方はぜひ下の記事も読んでみてください!
また、ルールを書くときに用いているRegoについては、Open Policy Agentとともに以下の記事で紹介しています!
僕の発表内容には答えがなく、「みなさんはどのようにガバナンス効かせていますか?」と問うて終わりました!
いろんな人のconfig.yml
話聞きたいですねー!
おまけ
質疑応答コーナーがあったのですが、「config.ymlへのガバナンスの効かせ方で悩んでいる方いますか?」という質問に誰も反応しなかったので、僕が勝手に悩んでいるだけなのかもしれない、ということはそれとなくわかりました(笑)
そして、イベントでは「ワタシハサークルシーアイチョットデキル」Tシャツいただきましたー!感謝感謝です!
Netlifyへのデプロイがタイムアウト
最近は仕事から趣味まで作ったもの(主にフロントエンド関連)のデプロイが簡単にNetlifyにできてしまうので多用しています。
直近ではVue.jsのコンポーネントをStorybookで公開しようとしたのですが、単純なタスクなはずが思いの外ハマってしまったので対処法をシェアしようと思います。
TL;DR
問題
npm run build
のようなビルドタスクが30分以上かかってしまってNetlify側でタイムアウトしてしまう
解決方法
ビルド時のログの出力量に注意!特にwebpackのようにビルド時に大量にログを出力するツールを使う場合は、出力しないオプションを付加しましょう
Netlifyがタイムアウトする
発生していた問題とは、Netlifyがタイムアウトするというものです。
上の画面はVue.jsのStorybookをビルドしている最中のものです。静止画だけで伝えられないのですが、ビルドの進捗がものすごく遅いんです。このときは開始したのが12:45ごろだったので、およそ30分くらいこの調子でちょっとずつ%が上がるだけで、そのあと静かにデプロイが失敗します。
サポートに問い合わせ
Netlifyはエラー文言も無しに静かに失敗し、ローカルではなんの問題もなくサクッとStorybookを開始できるのでお手上げでした。ということでサポートに問い合わせしてみることにしました。デプロイに失敗すると簡単にサポートに連絡できるようにリンクが用意されています。
ここから問い合わせフォームが下のように入力できるようになっていて、Netlify側もデバッグしやすいようにリンクも自動で貼り付けてあるので、送信するだけです。
僕の場合、送信してから半日くらいでサポートから返事がきました。
内容を要約すると
「ビルドのログが多すぎて、Netlifyの Log Serviceが問題を起こしていた。Storybookのビルドするなら —quiet
オプション付加してみて」
ってことでした。
なるほど、確かにログの表示が変に空白が多かったりしてましたね。
ログ出力量に注意!
ということでbuild-storybook
に—quiet
オプションをつけることにしました。公式サイトでもドキュメントされています。
https://storybook.js.org/docs/configurations/cli-options/
更新して再度デプロイしてみると、サクッと終わってサクッとNetlify上でStorybookが公開されました!
変に長時間悩み続けることなく、サポートに問い合わせてみてよかったです!
まとめ
- Netlifyのビルドタスクのログ出力量には要注意
- 可能であればログ出力が抑えられるオプションを付加しておくこと
- NetlifyのサポートのUX最高でした
調べても同じ問題に遭遇している人を見つけられなかったので、この記事が誰かの助けになればと思います。
PHPの`$`と`->`がつらくなったらマクロを使えば良いことに気づいた
最近PHPを書くことが多いのですが、変数の前につける$
(ドル記号)とメソッド呼び出しやプロパティ呼び出しに使う->
(矢印)の指の動きの効率悪さに耐えられなくなりました。。。
PHPStormを使っているといい感じに$
を入力しなくても変換してくれたりするのですが、その動きもまた気持ち悪く感じてしまうのでどうしたものかと思っていました。
そこで今更ながら発見したのがエディタの「マクロ」機能です!
マクロ作成
PHPStormであれば上のキャプチャのように「Edit > Macros > Start Macro Recording」を実行することでマクロの記録を始めることができます。
記録中は下図のようなインジケーターが表示されます。
ここでマクロとして入力したいキーを押します。$
用のマクロを作りたいので$
を入力すると、記録されているのがわかります。
そして赤い■をクリックするとマクロの記録が終わるので、このマクロに名前をつけてあげます。わかりやすく$
にしておきます。
これでマクロは完成です!
同じ要領で->
も作っておきましょう。
マクロのショートカット作成
マクロが完成したら、今度は自分の好きなショートカットでこれが入力されるようにします。
「Preferences > Keymap」に進んで、虫眼鏡のところでmacro
と入力しましょう。下のように絞り込まれるはずです。
ここに先程作成した$
と->
があるのがわかります。あとはここに自分の好きなキーの組み合わせを設定することで、以後このキーを入力すれば$
と->
が入力できるようになります!
ちなみに僕は
$
→ctrl+s
->
→ctrl+.
という設定にしました。ちょっとですけど快適になった気がします!
他の皆様がどうしているのか気になったりします(むしろ気にしてないですかね。。。)
まとめ
- PHPStormのマクロを設定した
- マクロならコードスニペットとは違ったショートカットを作ることができる
explainshell.comはシェルの強力な助っ人
最近explainshell.comなるものを今更ながら知ることができたので紹介します!
概要
シェルコマンドを全部覚えるのって大変です。なんとなく覚えてても、オプションがどんな意味をもっていたかまではなかなか思い出せないです。おまけにシェルは強力なのでパイプ(|)でどんどん繋げていい感じにコマンドを書いていくことができます。そんな芸術ともいうコマンドを見ては、man
を見たりググったりして、分解しながらコマンドを理解していっているのはきっと僕だけじゃないはず。
そこで便利なのがこのexplainshell.comというサイトです!
使い方
使い方はいたって簡単で、インプットに知りたいコマンドを入力するだけ。
試しに最近覗いてみた下のk3sのインストールスクリプトから抜粋してみます。
lsof | sed -e 's/^[^0-9]*//g; s/ */\t/g' | grep -w 'k3s/data/[^/]*/bin/containerd-shim' | cut -f1 | sort -n -u
すごい!全部のコマンドに丁寧な説明がいい感じにつくんです!
そして「←→」で順番にコマンドの解説を表示していくことも可能。
とってもシンプルですけど、ブックマークしておけば困ったときにサクッと使えて重宝しそうです!
おまけ
おまけですが、僕の場合はAlfredにショートカットを作ってすぐに調べられるようにしてます。
参考までにWeb Searchの設定は下のようにしています。
そうすると下のようにすぐに調べることができます!
まとめ
- explainshell.comを試した
- シェルコマンド調べるときに便利
- Alfredなどツールと連携すればさらに強力!
Node.jsではてなの自分の記事一覧を取得する
はてなの自分の記事一覧をサクッと取得できるようにしたかったのですが、意外と
- Node.jsで書かれている
- 結果をJSONにする
ものが見当たらなかったので結局自分で作ることにしました!
ツールの流れ
はてなから自分の記事一覧を取得する流れは下のようになります。
- はてなのAtomPubを使う(認証はこの記事ではBasic認証を使います)
- エンドポイントをGETすると最新の記事が10件とれる
- 結果はXMLなのでJSONに変換する
- 2のレスポンスに次の10件のURLが記載されているので、全部取得終わるまで2, 3を繰り返す
はてなのAtomPub
はてなのAtomPubの仕様については以下Hatena Developer Centerに記載されています。
認証のところに書かれていますが、取得時に使える認証は
の3種類です。個人的にしか使わないし、簡単な方法でとりたいので今回はBasic認証を使うことにしました!
Basic認証に必要なユーザー名ははてなIDで、パスワードはAPIキーを使用します。
APIキーははてなブログの管理画面から設定>詳細設定に行った場所にあります!
下の方を見るとAtomPubの項目があり、そこにAPIキーがあるのでメモっておきましょう!
ブラウザからGETしてみる
それでは、試しに記事一覧をブラウザから取得してみます。
ドキュメントにも記載がありますが、以下のURLで取得できます。
GET https://blog.hatena.ne.jp/{はてなID}/{ブログID}/atom/entry
僕の場合はてなID=kenev
でブログID=kenfdev.hateblo.jp
なので、以下になります。
https://blog.hatena.ne.jp/kenev/kenfdev.hateblo.jp/atom/entry
これをブラウザに入れると、下のようにBasic認証が聞かれるはずです。
ここにユーザー名とAPIキーを入力すると、下のようなXMLが表示されます。
<?xml version="1.0" encoding="utf-8"?> <feed xmlns="http://www.w3.org/2005/Atom" xmlns:app="http://www.w3.org/2007/app"> <link rel="first" href="https://blog.hatena.ne.jp/{はてなID}}/{ブログID}/atom/entry" /> <link rel="next" href="https://blog.hatena.ne.jp/{はてなID}/{ブログID}/atom/entry?page=1377584217" /> <title>ブログタイトル</title> <link rel="alternate" href="http://{ブログID}/"/> <updated>2013-08-27T15:17:06+09:00</updated> <author> <name>{はてなID}</name> </author> <generator uri="http://blog.hatena.ne.jp/" version="100000000">Hatena::Blog</generator> <id>hatenablog://blog/2000000000000</id> <entry> <id>tag:blog.hatena.ne.jp,2013:blog-{はてなID}-20000000000000-3000000000000000</id> <link rel="edit" href="https://blog.hatena.ne.jp/{はてなID}/ ブログID}/atom/edit/2500000000"/> <link rel="alternate" type="text/html" href="http://{ブログID}/entry/2013/09/02/112823"/> <author><name>{はてなID}</name></author> <title>記事タイトル</title> <updated>2013-09-02T11:28:23+09:00</updated> <published>2013-09-02T11:28:23+09:00</published> <app:edited>2013-09-02T11:28:23+09:00</app:edited> <summary type="text"> 記事本文 リスト1 リスト2 内容 </summary> <content type="text/x-hatena-syntax"> ** 記事本文 - リスト1 - リスト2 内容 </content> <hatena:formatted-content type="text/html" xmlns:hatena="http://www.hatena.ne.jp/info/xmlns#"> <div class="section"> <h4>記事本文</h4> <ul> <li>リスト1</li> <li>リスト2</li> </ul><p>内容</p> </div> </hatena:formatted-content> <app:control> <app:draft>no</app:draft> </app:control> </entry> <entry> ... </entry> ... </feed>
XMLが取得できそうだとわかったところで、Node.jsで実装してみます!
Node.jsで取得する
まずはソースを載せちゃってからポイントを解説します。
const axios = require('axios'); const parseString = require('xml2js').parseString; const hatenaAPIUser = '<USER_ID>'; const hatenaAPIPassword = '<API_KEY>'; const entryUrl = `https://blog.hatena.ne.jp/<USER_ID>/<BLOG_ID>/atom/entry`; const creds = `${hatenaAPIUser}:${hatenaAPIPassword}`; const encoded = Buffer.from(creds).toString('base64'); const authorizationHeader = `Basic ${encoded}`; /** * Promise based timeout * @param {number} msec wait milliseconds */ function wait(msec) { return new Promise(resolve => { setTimeout(() => { resolve(); }, msec); }); } /** * Promise based XML->JSON Parser * @param {object} data XML Data */ function parseStringPromise(data) { return new Promise((resolve, reject) => { parseString(data, (err, result) => { if (err) { reject(err); return; } resolve(result); }); }); } /** * Get all Hatena entries recursively * @param {string} url Entry URL */ async function getAllEntries(url) { console.log('fetching:', url); const entries = await axios.get(url, { headers: { Authorization: authorizationHeader, }, }); const { feed: { entry, link }, } = await parseStringPromise(entries.data); const next = link.find(l => l.$.rel === 'next'); const parsedEntries = entry .filter(e => { const { 'app:control': [ { 'app:draft': [isDraft], }, ], } = e; return !(isDraft === 'yes'); }) .map(e => { const categories = e.category ? e.category : []; return { id: e.id[0], title: e.title[0], url: e.link[1].$.href, published: e.published[0], publishedAt: new Date(e.published[0]).getTime(), updated: e.updated[0], updatedAt: new Date(e.updated[0]).getTime(), summary: e.summary[0]._, categories: categories.map(c => c.$.term), }; }); if (next) { // recursively fetch if 'next' exists const nextHref = next.$.href; await wait(500); const nextEntries = await getAllEntries(nextHref); return [...parsedEntries, ...nextEntries]; } else { return parsedEntries; } } getAllEntries(entryUrl).then(console.log);
ポイントとしては
- 外部リクエストにはaxiosを使い、BASIC認証用のHTTPヘッダーを作成している
- XMLからJSONにはxml2jsというライブラリを使っている
- 無限ループで負荷かけちゃうの怖いのでリクエスト間に500ミリ秒待つ
- 最終的なJSONの形は自分で決める
となります。順番に説明します!
外部リクエストにはaxiosを使い、BASIC認証用のHTTPヘッダーを作成している
最近axiosを使う機会が多かったので、なんとなく外部リクエストのライブラリはaxiosにしました。 ライブラリは割とどうでもいいのですが、ポイントはBASIC認証のHTTPヘッダーを作っている部分です。
const creds = `${hatenaAPIUser}:${hatenaAPIPassword}`; const encoded = Buffer.from(creds).toString('base64'); const authorizationHeader = `Basic ${encoded}`; ... const entries = await axios.get(url, { headers: { Authorization: authorizationHeader, }, }); ...
BASIC認証を実現するためにはHTTPヘッダーに Authorization
というものを作って、中身はBasic <ユーザー名:パスワードをbase64エンコードした値>
となります。
XMLからJSONにはxml2jsというライブラリを使っている
XMLからJSONにするために下のライブラリを使っています。
axiosで取得したXMLの中身をこのライブラリに通すことで、JSONを取得することができます。
const parseString = require('xml2js').parseString; /** * Promise based XML->JSON Parser * @param {object} data XML Data */ function parseStringPromise(data) { return new Promise((resolve, reject) => { parseString(data, (err, result) => { if (err) { reject(err); return; } resolve(result); }); }); } ... const { feed: { entry, link }, } = await parseStringPromise(entries.data); ...
個人的にCallbackがあまり好きじゃないので、Promiseにわざわざ変換したfunction
書いてますけど、これは別に必要ないです。Promiseにしてawait
したかっただけです。
無限ループで負荷かけちゃうの怖いのでリクエスト間に500ミリ秒待つ
コードがバグっていてはてなさんのサーバーに変な攻撃を起こしてしまうのが怖いので、リクエスト間に待ちを発生させるようにしてます。
/** * Promise based timeout * @param {number} msec wait milliseconds */ function wait(msec) { return new Promise(resolve => { setTimeout(() => { resolve(); }, msec); }); } ... if (next) { // recursively fetch if 'next' exists const nextHref = next.$.href; await wait(500); const nextEntries = await getAllEntries(nextHref); return [...parsedEntries, ...nextEntries]; } else { return parsedEntries; } ...
先程と同じく、setTimeout
をPromiseにしてawait
使いたかったのでわざわざfunction
作っています。あとは再帰呼び出しをする直前にawait wait(500)
と書いて500ミリ秒待つようにしています。
最終的なJSONの形は自分で決める
XMLからJSONに変換したものは、そのままではものすごく使いづらいので、適宜興味のある項目だけ抜き取って最終的な形に整形します。
... const categories = e.category ? e.category : []; return { id: e.id[0], title: e.title[0], url: e.link[1].$.href, published: e.published[0], publishedAt: new Date(e.published[0]).getTime(), updated: e.updated[0], updatedAt: new Date(e.updated[0]).getTime(), summary: e.summary[0]._, categories: categories.map(c => c.$.term), }; ...
こうすると、下のようなJSONになります。
{ "id": "xxxxxxxxxxxxxxxxxxx", "title": "AWS SAA再認定のために5日間頑張ったこと", "url": "https://kenfdev.hateblo.jp/entry/saa-recertification", "published": "2019-03-08T20:42:58+09:00", "publishedAt": 1552045378000, "updated": "2019-03-08T20:42:58+09:00", "updatedAt": 1552045378000, "summary": "昨日AWSソリューションアーキテクト・アソシエイト(以下SAA)に再認定されました。3月中に取得しないといけないというミッションがあったので慌てて先週申し込みました。試験会場の関係で試験日まで5日しか無かったのですが、合格できたので備忘録を残しておきます。 TL;DR(まとめ) …", "categories": ["AWS"] }
まとめ
以上ではてなの記事一覧の取得ができます!最後はfs
を使ったりしてファイルにJSONを保存すれば何か他の用途に使うことができるかと思います。
以下リポジトリにコードを格納していますので、興味がある方は使ってみてください!