Madogiwa Blog

主に技術系の学習メモに使っていきます。

はてなブログの記事を取得してカード表示するVueComponentを作った📦✨

はてなブログの記事を取得してカード表示するVueComponentを作ったので、そのへんの話をメモしておきますm( )m

コードだけ見たい人はこちら👇

はてなブログの記事をカードで表示するVueコンポーネント · GitHub

イメージ

このような形で表示されます🙌 f:id:madogiwa0124:20190413193738p:plain

使い方

BlogsコンポーネントPropsとしてendpointrss取得用のurl(https://madogiwa0124.hatenablog.com/rss等)と、displayCountに表示件数を設定してください。

<Blogs
  endpoint="https://madogiwa0124.hatenablog.com/rss"
  displayCount="6"
/>

カードのデザインを修正したい場合は、BlogCard.vuestyleを変更していただければ👩‍🎨

実装のはなし

はてなブログの記事を取得する

はてなブログの記事一覧を取得するのは意外と簡単で、ブログURL/rssrssフィードを取得できるので、まずはそれを取得します。axiosを使ってリクエストを送信すればOKです👀

import axios from 'axios';
public created() {
  axios.get(this.endpoint).then((res)  => { const rss = res.data });
}

取得した記事をParseする

取得した記事のParseには、DOMParserを使用しています。RSSxmlなのでtext/xmlを指定してあげればParse出来ます👀
buildRssBlogItemsitem(各記事)をDocumentにParseしてblogPropsでthumbnail、title、linkを持つobjectを生成してBlogの一覧(blogs)を作成しています🙌

  import axios from 'axios';
  
  public blogs: object[] = [];
  public parser: DOMParser = new DOMParser();

  public created() {
    axios.get(this.endpoint).then((res)  => {
      const rssDom = this.parser.parseFromString(res.data, 'text/xml');
      this.buildRssBlogItems(rssDom);
    });
  }
  private buildRssBlogItems(dom: Document): void {
    dom.querySelectorAll('item').forEach((item) => {
      const itemDom = this.parser.parseFromString(item.innerHTML, 'text/html');
      this.blogs.push(this.blogProps(itemDom));
    });
    this.blogs = this.blogs.slice(0, this.displayCount);
  }
  private blogProps(dom: Document): object  {
    const thumbnail = dom.querySelector('enclosure')!.getAttribute('url');
    const title = dom.querySelector('title')!.text;
    const link = dom.querySelector('body')!.firstChild!.textContent!.trim();
    return { title, thumbnail, link };
  }

おわりに

今回ははてなブログの記事の一覧を表示するVueコンポーネントを作成しました📦
DOMParserを今まであまり使ったことなかったのですが、でHTMLやXMLをParseできるのでスクレイピングの結果やRSSを使って、いろいろ出来そうですね👀

Vue.jsでpropsを使ってimgタグのsrc属性を設定する方法

Vue.jsを使っていて親要素から子要素へpropsを使ってimgタグのsrc属性を設定する方法で、ハチャメチャにハマってましたが、一応出来たのでやり方をメモしておきます✍

今回やりたかったこと

下記のような親子関係のコンポーネントを作って親コンポーネントとからpropsを使って子コンポーネントfileSrcを渡して、そのままassets配下の画像のURLを設定したかった。

// Skill.vue
<template>
  <div class="skills">
    <Card fileSrc="../asstes/ruby.png" />
  </div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import Card from './Card.vue';

@Component({
  components: { Card },
})
export default class AboutMe extends Vue {
}
</script>
<style lang="scss" scoped>
</style>
// Card.vue
<template>
  <div class="card">
    <div class="card--image">
      <img :src="fileSrc" />
    </div>
  </div>
</template>
<script lang="ts">
import { Component, Vue, Prop } from 'vue-property-decorator';

@Component
export default class Card extends Vue {
  @Prop() public fileSrc!: string;
}
</script>
<style lang="scss" scoped>
</style>

しかし、上記のやり方だとError in render: "Error: Cannot find module '../asstes/ruby.png'"が発生し、パスの解決が出来ずに画像が表示されなかった😭

またネット上の情報では:src = require(path)とすれば良いといった情報があったが、この対応を行っても同様のエラーが発生し、上手くいかなかった😭

vue-loader-v14.vuejs.org

forum.vuejs.org

解決策

下記のようにloadImg()メソッドを定義し、:src="loadImg()"としたところ解決した👀
ポイントは「require(../assets/${this.fileName})」です、引数ではファイル名だけ渡すようにして、 式展開をおこなってpathを生成してrequireしてあげると上手くいきました(正しい解決方法かどうかはあれですが。。。)💦

// Card.vue
<template>
  <div class="card">
    <div class="card--image">
      <img :src="loadImg()" />
    </div>
  </div>
</template>
<script lang="ts">
import { Component, Vue, Prop } from 'vue-property-decorator';

@Component
export default class Card extends Vue {
  @Prop() public fileSrc!: string;
  
  public loadImg(): any {
    return require(`../assets/${this.fileName}`);
  }
}
</script>
<style lang="scss" scoped>
</style>

ちょっといろいろ引数を変えてみてみたのですが、謎・・・🤔

return require(`../assets/${this.imgName}`); //OK
return require('../assets/image.png'); // OK
return require(this.imgSrc); // NG

変数を引数に渡してしまうと上手くパスを解決出来ないみたいですね😥

javascript: Cloud Firestoreのはじめ方とCRUD系クエリMEMO🔥

ちょっとCloud Firestoreを触ってみたので、はじめ方とCRUD系のクエリのサンプルとか次触る時に忘れそうなのでメモしておきます✍
※サンプルコードはjavascriptです🙇‍♂️

Cloud Firestoreとは?

Cloud FirestoreとはFirebaseのキーバリューストアです📦

Cloud Firestore は、Firebase と Google Cloud Platform からのモバイル、ウェブ、サーバー開発に対応した、柔軟でスケーラブルなデータベースです。 https://firebase.google.com/docs/firestore/?hl=ja

Realtime Databaseとの違いは下記のようです👀
(私はあまりわかってません😇)

Realtime Database は従来からある Firebase のデータベースです。リアルタイムのクライアント間同期が必要なモバイルアプリのための、効率的でレイテンシが低いソリューションです。 Cloud Firestore は、Firebase のモバイルアプリ開発用の新しい主力データベースです。直感的な新しいデータモデルで、Realtime Database をさらに効果的にしています。Cloud Firestore は、Realtime Database よりも多彩で高速なクエリと高性能なスケーリングが特長です。

Cloud Firestoreをとりあえず使ってみる

使い方は簡単で下記の手順で画面でポチポチするだけでCloud Firestoreを立ち上げることができました🙌

  1. Firebase Consoleから新しいプロジェクトを作成
  2. Databaseからデータベースの作成をクリック
  3. 表示されたモーダルでセキュリティルールを設定し有効にするをクリック(動作確認で使ったので、今回はテストモードで作成しました。)

あとは、公式ドキュメントの通りにFirebaseをアプリに導入しました🔥

$ npm install firebase --save
var firebase = require("firebase");
// configの情報は、Project Overviewのアプリを追加にあるWeb等のアイコン押下時に表示されるPopupから設定
var config = {
  apiKey: "hoge",
  authDomain: "sample-project-zzzzz.firebaseapp.com",
  databaseURL: "https://sample-project-zzzzz.firebaseio.com",
  projectId: "sample-project-zzzzz",
  storageBucket: "sample-project-zzzzz.appspot.com",
  messagingSenderId: "99999999999"
};
// configの内容で初期化
firebase.initializeApp(config);

// firestore管理用のobjectを生成
var db = firebase.firestore();

詳しい始め方は、下記の公式ドキュメントを参照してください👀

firebase.google.com

firebase.google.com

CRUD系のクエリMEMO

とりあえず基本的なCRUD系のクエリの発行方法をメモしておきます。

データ追加

.set: 1件のデータを追加する

// docRef: 登録パスを設定
var docRef = db.collection('users').doc('alovelace');

// .setの引数に値をobject形式で設定
var setAda = docRef.set({
  first: 'Ada',
  last: 'Lovelace',
  born: 1815
});

データの取得

.get: collectionに紐づくデータのリストを取得する

// collection('collection_name').get()でPromiseが返却される
db.collection('users').get().then((snapshot) => {
  // .idでid、.data()でプロパティを取得出来る
  snapshot.forEach((doc) => { console.log(doc.id, doc.data()); });
}).catch((err) => { console.log(err); });

.doc(docRef): ドキュメントを取得する

// .docにdocRefを渡すと該当するドキュメントを取得出来る。
db.collection("collection_name").doc(docRef)

データの削除

.delete: ドキュメントを削除する

// .docで取得したドキュメントに対して.delete()で削除出来る
db.collection("collection_name").doc(docRef).delete()

データの更新

.update: ドキュメントを更新する

// 取得したドキュメントに.updateを呼び出して引数に`key: 値`で更新できます
// Promiseが返却されます
var docRef = db.collection("cities").doc("SF");
docRef.update({ name: 'San Francisco_2' }).then(() => {
  console.log('updated')
}).catch(e => { console.log(e) })

おわりに

今回はCloud Firestoreの始め方と、基本的なCRUD系のクエリを整理してみました✍
ちょっと触ってみた感じですが手軽に始められて、割と扱いやすい感じに見えたので、 個人サービスで登録時にそこまでデータの加工等が必要ないサービスであれば、 フロントエンド + Cloud Firestoreの構成でも良さそうな気がしました👀!

ピュアRubyでAtomも対応したRSS Parserを作ってみたMEMO

みなさん、こんにちは(・∀・) rubyの標準RSSライブラリが思ったよりも高機能でびっくりしたので、gemを使わずにAtomRssのParserを作ってみたので、そのへんのやり方をメモしておきますm( )m

Ruby標準のRSSライブラリ

標準ライブラリを使用する場合は、下記のような形でhttpリクエストを送信したParseされたRSSフィードを取得することができます👀

require 'rss'
rss_source = Net::HTTP.get(URI.parse(endpoint))
rss = RSS::Parser.parse(rss_source)
=> #<RSS::Rss:0x00007fe3dfcc0bd0...

RSS::Parser.parseの返り値は、引数で渡されたRSSの形式によって 、Rssの場合はRSS::RssAtomの場合は、RSS::Atom::Feedのオブジェクトが返却されます。(デフォルトでAtomまで対応してる🙌)

厳密には、下記とのこと

  • RSS 1.0をパースした場合は RSS::RDF オブジェクト
  • RSS 0.9x/2.0をパースした場合は RSS::Rss オブジェクト
  • Atom をパースした場合は RSS::Atom::Feed オブジェクト

下記に詳細が乗っています👀
https://docs.ruby-lang.org/ja/latest/library/rss.html

共通のプロパティを持つオブジェクトを返却するRSSParserを作ってみる

標準ライブラリでAtomでもRssでも共通のオブジェクトを返却するRSSParserを作ってみようと思います👩‍🔧

イメージは下記のような感じ👀

# AtomでもRssでもParsedItemのオブジェクトが返却される
RssCliant.new(endpoint).parsed_items
=> [#<ParsedItem:0x00007fe3e2e3ab48
  @description="みなさん、...",
  @eye_catching_image="https://cdn.blog.st-hatena.com/images/theme/og-image-1500.png",
  @link="https://madogiwa0124.hatenablog.com/entry/2019/03/09/194825",
  @published_at=2019-03-09 19:48:25 +0900,
  @title="vue-cliで作ったアプリをGithub Pagesでサクッとリリースする">,
 #<ParsedItem:0x00007fe3e1c0ce08
  @description=
   "自分が作っている...",
  @eye_catching_image="https://cdn-ak.f.st-hatena.com/images/fotolife/m/madogiwa0124/20190303/20190303213625.gif",
  @link="https://madogiwa0124.hatenablog.com/entry/2019/03/03/214203",
  @published_at=2019-03-03 21:42:03 +0900,
  @title="railsとVueを使って無限スクロール機能を実装するMEMO🌀">,

まずはCliant部分を作ってみる

まずは下記のようなRssCliantをというClassを作ってみました👀 endpointを受け取って標準ライブラリを使ってParse、その後AtomRssかによって使用するParserを切り替え共通のオブジェクトを返却します。

class RssClient
  def initialize(endpoint)
    @endpoint = endpoint
    @rss_source = Net::HTTP.get(URI.parse(endpoint))
  end

  attr_reader :endpoint, :rss_source

  # 不正な形式だった場合に、バリデーションなしでParseする
  def parsed_rss!
    RSS::Parser.parse(rss_source)
  rescue RSS::InvalidRSSError
    RSS::Parser.parse(rss_source, false)
  end

  def parsed_items
    parsed_xml = parsed_rss!
    # オブジェクトのClass名でAtom or Rss用のParserを使って共通のオブジェクトにParse
    case parsed_xml.class.name
    when 'RSS::Atom::Feed' then Parser::Atom.call(parsed_xml)
    when 'RSS::Rss' then Parser::Rss.call(parsed_xml)
    else []
    end
  end
end

Atom or RssのParser部分

Rss.parseでParseされたXMLを引数をもとに、build_parsed_itemPasedItemという共通のオブジェクトを生成し、それらのリストを返却するようにしています🙌 他の形式に対応する場合はParser::Hogeが増えていくようなイメージですね👀
※またAtomの場合は、記事のアイキャッチ画像の取得方法が、ちょっと不明だったので一旦nilを設定するようにしてます💦

Rss

class Parser::Rss
  def self.call(parsed_xml)
    new.call(parsed_xml)
  end

  def call(parsed_xml)
    @parsed_xml = parsed_xml
    items
  end

  attr_reader :parsed_xml

  def items
    @items = parsed_xml.items.map { |item| build_parsed_item(item) }
  end

  def build_parsed_item(item)
    Parser::ParsedItem.new(
      title: item.title,
      description: item.description,
      published_at: item.pubDate,
      link: item.link,
      eye_catching_image: item.enclosure&.url
    )
  end
end

Atom

class Parser::Atom
  def self.call(parsed_xml)
    new.call(parsed_xml)
  end

  def call(parsed_xml)
    @parsed_xml = parsed_xml
    items
  end

  attr_reader :parsed_xml

  def items
    @items = parsed_xml.entries.map { |entry| build_parsed_item(entry) }
  end

  def build_parsed_item(item)
    Parser::ParsedItem.new(
      title: item.title.content,
      description: item.content.content,
      published_at: item.published.content,
      link: item.link.href,
      eye_catching_image: nil # AtomにアイキャッチのURLなさそうなので一旦NULLを設定
    )
  end
end

完成形

これでイメージどおりの完成形ができました🙌

# AtomでもRssでもParsedItemのオブジェクトが返却される
RssCliant.new(endpoint).parsed_items
=> [#<ParsedItem:0x00007fe3e2e3ab48
  @description="みなさん、...",
  @eye_catching_image="https://cdn.blog.st-hatena.com/images/theme/og-image-1500.png",
  @link="https://madogiwa0124.hatenablog.com/entry/2019/03/09/194825",
  @published_at=2019-03-09 19:48:25 +0900,
  @title="vue-cliで作ったアプリをGithub Pagesでサクッとリリースする">,
 #<ParsedItem:0x00007fe3e1c0ce08
  @description=
   "自分が作っている...",
  @eye_catching_image="https://cdn-ak.f.st-hatena.com/images/fotolife/m/madogiwa0124/20190303/20190303213625.gif",
  @link="https://madogiwa0124.hatenablog.com/entry/2019/03/03/214203",
  @published_at=2019-03-03 21:42:03 +0900,
  @title="railsとVueを使って無限スクロール機能を実装するMEMO🌀">,

おわりに

今回はRubyの標準ライブラリだけを使ってRssParserを実装してみました。RssParserはFeedjira等のGemが有名ですが、そんなに凝ったことしないのであればRubyの標準ライブラリが使いやすく高機能なので充分なのでは?という気持ちになりました🙌

他にも標準ライブラリには便利そうな機能がありそうだったので、ちょっと見てみると良さそうですね👀

vue-cliで作ったアプリをGithub Pagesでサクッとリリースする

みなさん、こんにちは(・∀・)
今回は、vue-cliでつくったアプリをGithubPageを使ってサクッと公開する方法をメモします✍

ちなみに公開したサービスはこちら、タブが使えるMarkdownEditor「MTM」というサービスです📝

madogiwa0124.github.io

環境

私の使っているvue-cliの環境は下記の通りです。

$ vue -V
2.9.6 

やり方

↓サクッと対応したコミットだけみたい人はこちら↓

production build · Madogiwa0124/multi-tab-markdown@91cdfea · GitHub change Relative path for production build · Madogiwa0124/multi-tab-markdown@0e870de · GitHub

まずはアプリをproduction用にbuildします。実行するとdist配下にビルドされたファイルが作成されます🎁

$ npm run build 
  File                                   Size              Gzipped
  dist/js/chunk-vendors.edad0ee4.js      867.43 kb         297.29 kb
  dist/js/app.9d6dfc4d.js                16.96 kb          6.15 kb
  dist/css/app.c259b10b.css              189.93 kb         24.46 kb
  dist/css/chunk-vendors.27530976.css    0.66 kb           0.31 kb

次に初期のindex.htmlのcssやjsのパスがdistディレクトからの絶対パスになっているので、href=/src=/href=./src=./に置換します👩‍🔧
※これをやらないとindex.htmlをローカルやGithubPageで見た時にjsやcssのファイルが見つからなくなってしまいます。。。

最後に.gitignoreからdistを削除してgit管理下に置くようにして下記コマンドでpushします。

$ git push origin master

あとは、GithubリポジトリのSettingsからGithubPageの設定をしてあげて、下記のようにdist/index.htmlにアクセスすると無事に公開されているはずです🙌

https://madogiwa0124.github.io/multi-tab-markdown/dist/index.html

参考

qiita.com

railsとVueを使って無限スクロール機能を実装するMEMO🌀

自分が作っているSPAっぽいrailsのサービスでrailsとVueで無限スクロール∞を作ったので、そのやり方をメモしておきますm( )m

つくるもの

下記のようにスクロール時にAPIでデータを取得して表示していくような機能を作っていきます🌀

f:id:madogiwa0124:20190303213625g:plain

使うもの

今回は無限スクロールの導入に、vue-infinite-loadingを使いました🙌
結構チュートリアルも充実してて使いやすかったです👀

peachscript.github.io

実際のコード

今回は、つくるもので紹介したようなFeedのCardのコンポーネントをリスト表示するような機能の実装で説明していきます。

View

<main class="column">
  <feed-card-collection />
</main>
<%= javascript_pack_tag 'boards/new' %>

ポイントは、<infinite-loading @infinite="infiniteHandler" />で∞スクロールのコンポーネントを入れてあげることと、infiniteHandlerの中で、api呼び出し及びfeedのリストに結果を追加していく部分です👀

<template>
  <div class="entries is-multiline columns">
    <page-loader :init_is_loading="isLoading" />
    <div
      v-for="feed in feeds"
      :key="feed.id"
      class="column is-4"
    >
      <feed-card
        :feed="feed"
        :lastEntry="feedLastEntry(feed)"
      />
    </div>
    <infinite-loading @infinite="infiniteHandler" />
  </div>
</template>
<script>
import FeedCard from './FeedCard';
import InfiniteLoading from 'vue-infinite-loading';
import axios from 'axios';

const feedsApi = '/api/feeds';

export default {
  name: 'FeedCardCollection',
  components: { FeedCard, InfiniteLoading },
  props: ['init_feeds', 'init_last_entries'],
  data: function () {
    return {
      page: 1,
      feeds: [],
      last_entries: [],
      isLoading: true
    };
  },
  mounted: function () {
    // MEMO: 初回表示時にデータ取得するため実行
    this.infiniteHandler();
    this.$nextTick(function () {
      this.isLoading = false;
    });
  },
  methods: {
    feedLastEntry: function(feed) {
      return this.last_entries.filter(entry => entry.feed_id === feed.id)[0];
    },
    infiniteHandler($state) {
      axios.get(feedsApi, {
        params: { page: this.page },
      }).then(({ data }) => {
        if (data.feeds.length) {
          this.page += 1;
          this.feeds.push(...data.feeds);
          this.last_entries.push(...data.last_entries);
          $state.loaded();
        } else {
          $state.complete();
        }
      });
    },
  }
};
</script>
<style lang="scss">
</style>

Controller

ポイントは、params[:page]でページ番号を取得出来るので、そちらを使ってpagingを考慮して結果を取得する必要があるので、Model側に追加したpagerを使って取得するようにしている部分です。

class Api::FeedsController < ApplicationController
  PER_PAGE = 6

  def index
    @feeds = Feed.recent.pager(page: params[:page], per: PER_PAGE)
    @last_entries = @feeds.includes(:last_entry).map(&:last_entry)
    object = { feeds: @feeds, last_entries: @last_entries }
    render json: object
  end
end

Model

スコープpagerの中でlimitoffsetを使ってページングを考慮してデータを取得しています📖

class Feed < ApplicationRecord
  scope :pager, ->(page: 1, per: 10) {
    num = page.to_i.positive? ? page.to_i - 1 : 0
    limit(per).offset(per * num)
  }
end

おわりに

railsとvueを使った無限スクロールの実装方法を書いてみました。
プラグインを使うと結構簡単に実装出来ますね、OSSに感謝🙏

参考

peachscript.github.io

www.shookuro.com

rails: Rssフィードの作り方MEMO

最近、railsrssフィードを作ったので、そのへんのやり方をメモしておきますm( )m

作るもの

今回は、Rssフィード(Feed)とそれに紐づく記事(Entry)を元にRSSフィードを作成します。
イメージは/feeds/id.rssにアクセスした際に下記のようなxmlを生成するイメージです👀

<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>無題のボード</title>
    <description>「MadogiwaBlog、yahoo天気(東京)、Codezine」をまとめたRSSフィードです。</description>
    <link>https://example.com</link>
    <lastBuildDate>Sun, 06 May 2018 23:11:20 +0900</lastBuildDate>
    <language>ja</language>
    <copyright>© copyright 2019 Madogiwa All Rights Reserved.</copyright>
    <item>
      <title>【 24日(日) 東京(東京) 】 曇り - 14℃/3℃ - Yahoo!天気・災害</title>
      <description>曇り - 14℃/3℃</description>
      <pubDate>Sun, 24 Feb 2019 20:00:00 +0900</pubDate>
      <link>https://rdsig.yahoo.co.jp/weather/rss/RV=1/RU=aHR0cHM6Ly93ZWF0aGVyLnlhaG9vLmNvLmpwL3dlYXRoZXIvanAvMTMvNDQxMC5odG1sP2Q9MjAxOTAyMjQ-</link>
    </item>

実際のコード

作り方は意外とシンプルで、respond_toformat.rssshow.rss.builderレンダリングしてあげればOKです🙆‍♂️

class FeedsController < ApplicationController
  def show
    @feed = Feed.find(params[:id])
    @entries = @feed.entries.recent
    respond_to do |format|
      format.html
      format.rss { render layout: false }
    end
  end
end

show.rss.builder内の日付系の項目はrfc2822形式でformatする必要があることに注意です👀

xml.instruct! :xml, version: '1.0'
xml.rss(version: '2.0') do
  xml.channel do
    xml.title @feed.title
    xml.description @feed.description
    xml.link 'https://example.com'
    xml.lastBuildDate @entries.last.published_at.rfc2822
    xml.language 'ja'
    xml.copyright '© copyright 2019 Madogiwa All Rights Reserved.'
    @entries.each do |entry|
      xml.item do
        xml.title entry.title
        xml.description entry.description
        xml.pubDate entry.published_at.rfc2822
        xml.link entry.link
      end
    end
  end
end

特にGemとか使わなくても結構簡単に出来るんですね🙌

参考

miner.hatenablog.com

apidock.com