Madogiwa Blog

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

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

javascript: ファイルダウンロード処理を実装するMEMO

最近MarkdownEditorのサービスを作っていて、作成したMarkdownファイルをjavascriptでダウンロード処理を実装したので、 やり方をMEMOしておきますφ(・

やったこと

私のサービスではMarkdownファイルをダウンロードする機能を作りたかったので、 propsで受け取ったitemの本文(markdownText)とタイトル(title)を使ってファイルを生成し、 ダウンロード出来るVueコンポーネントを作りました👀

実際のコード

実装の流れは下記の通りです🙌

  1. 引数のitemの本文を元にBlobインスタンスを作成
  2. リンク要素aを作成
  3. リンクのhrefBlobインスタンスのダウンロードURLを設定
  4. リンクのdownloadにitemのタイトルを元にファイル名を設定
  5. リンクをクリックしてダウンロードを実行

↓実際のVueコンポーネントはこちらです↓

<template>
  <i
    class="fas fa-file-download download"
    @click="handleOnDownload($event, item)"
  />
</template>
<script>
export default {
  name: 'DownloadLink',
  components: {},
  props: ['item'],
  methods: {
    handleOnDownload: function(e, item) {
      e.preventDefault()
      const blob = new Blob([item.markdownText], { 'type' : 'text/plain' })
      let link = document.createElement('a')
      link.href = window.URL.createObjectURL(blob)
      link.download = `${item.title}.md`
      link.click()
    }
  }
}
</script>

javascriptでファイルダウンロードするときは、ダウンロードリンクを動的に生成して、それをクリックするような流れ行うんですねー🤔

multi-tab-markdown/DownloadLink.vue at master · Madogiwa0124/multi-tab-markdown · GitHub

参考

www.tagindex.com

helloworld-blog.tech

railsのViewからVueの単一ファイルコンポーネントへモデルのインスタンスを受け渡すMEMO

railsのviewからVueの単一ファイルコンポーネントへモデルのインスタンスをいい感じに渡す方法を模索して、結構ハマってたのですが、自分なりに落ち着いたのでやり方をメモしておきます✍

今回やりたかったこと

やりたかったのは、下記のようにモデルのインスタンスの配列を、そのまま単一ファイルコンポーネントと渡すということがやりたかった。

しかし、viewから単一ファイルコンポーネントへ行くと、railsの世界から離れてしまうので、いい感じに渡す方法がイマイチ思いつかなかった。。。 ※ネットで調べるとviews側にscriptタグ使ってVueのinstanceを生成するとかも書いてあったけど、いまいちな気がしてしまい。。。

<div class="feed-show" id="feeds-entries">
  <h1 class="title"><%= @feed.title %></h1>
  <entry-card-collection entries="<%= @entries %>"></entry-card-collection>
</div>
<%= javascript_pack_tag 'feeds' %>
class FeedsController < ApplicationController
  def show
    @feed = Feed.find(params[:id])
    @entries = @feed.entries.order(published_at: :desc)
  end
end

どうやったか

いろいろハマったけど、やった方法は下記です。

<div class="feed-show" id="feeds-entries">
  <h1 class="title"><%= @feed.title %></h1>
  <entry-card-collection :entries="<%= @entries.to_json %>"></entry-card-collection>
</div>
<%= javascript_pack_tag 'feeds' %>

ポイントは2つ。

  • v-bindで値を受け渡すこと
  • to_jsonしてjson形式で値を渡すこと

あとは、単一ファイルコンポーネント側でpropsを定義して普通に使えばOKです🙌 ※取得する際にJSON.parseする必要があるかなと思いましたが、しなくてもいい感じにデフォルトでParseしてくれるみたいですね🤔

<template>
<div class="entries">
  <entry-card
    v-for="entry in entries"
    :key="entry.id"
    :entry="entry">
  </entry-card>
</div>
</template>
<script>
import EntryCard from './EntryCard'

export default {
  name: 'EntryCardCollection',
  components: { EntryCard },
  props: ['entries']
}
</script>
<style lang="scss">
</style>

参考

www.reddit.com

railsとruby標準ライブラリで作るRSSリーダー的なやつの作り方Memo📝

はじめに

みなさん、こんばんは。まどぎわです(・∀・)
rubyの標準ライブラリにRSS用のライブラリがあることを最近知り、railsと標準ライブラリを使ってRSSリーダー的なものを作ってみたので、作り方とかをメモしておきます✍

作るもの

はてなブログRSSフィードのエンドポイントを登録したら、そこからRSSをパースして表示出来るようなアプリを作っていきます👀

こんな感じのものをイメージしてもらえれば! f:id:madogiwa0124:20190203232035g:plain

使うもの

作り方

モデルを作る

とりあえずモデルを作ります。作るものを下記の2つです。

  • Feed.rb
  • Entry.rb

Feed.rbにはRSSフィードのタイトルとエンドポイントのURLを登録します。Entry.rbには、フィードから取得したエントリーを登録します。

schemeはこんな感じです。

  create_table "entries", force: :cascade do |t|
    t.bigint "feed_id"
    t.string "title"
    t.string "link"
    t.text "description"
    t.datetime "published_at"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["feed_id"], name: "index_entries_on_feed_id"
  end

  create_table "feeds", force: :cascade do |t|
    t.string "title", null: false
    t.string "endpoint", null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

RSSを読み込んでParseする

それでは、RSSを読み込んでパースする部分は結構簡単で、ruby標準のrssライブラリをrequire 'rss'で読み、Net::HTTP.get(URI.parse(endpoint))xmlドキュメントを取得して、RSS::Parser.parseを使ってParseします。
私は、Feedモデルにparsed_xmlを定義してParseしたxmlを取得出来るようにしてみました👀

class Feed < ApplicationRecord
  require 'rss'

  has_many :entries

  def parsed_xml
    xml = Net::HTTP.get(URI.parse(endpoint))
    RSS::Parser.parse(xml)
  end
end

エントリーを取得して保存する

その後は、Parseしたxmlitemsに記事の一覧が入っているので、下記のようなEntryCreaterクラスを作成して、画面登録時に記事一覧を取得して登録するようにしました🙌

class FeedsController < ApplicationController
  def new
    @feed = Feed.new
  end

  def create
    @feed = Feed.new(feed_params)
    if @feed.save
      Feed::EntryCreater.new(@feed).execute
      redirect_to feed_path(@feed)
    else
      render :new
    end
  end

  private

  def feed_params
    params.require(:feed).permit(:title, :endpoint)
  end
end
class Feed::EntryCreater
  attr_reader :feed

  def initialize(feed)
    @feed = feed
  end

  def execute
    Entry.where(feed: feed).delete_all
    feed.parsed_xml.items.map do |item|
      Entry.create(
        feed: feed,
        title: item.title,
        description: strip_tags(item.description).truncate(300),
        published_at: item.pubDate,
        link: item.link
      )
    end
  end

  private

  def strip_tags(text)
    ActionController::Base.helpers.strip_tags text
  end
end

保存したエントリーを表示する

保存したエントリーを表示するのは普通に表示してあげればOKです🙆‍♂️

class FeedsController < ApplicationController
  def show
    @feed = Feed.includes(:entries).find(params[:id])
  end
end
<div class="feed-show">
  <h1 class="title"><%= @feed.title %></h1>
  <div class="entries">
    <%= render partial: 'entry_collection', collection: @feed.entries, as: 'entry' %>
  </div>
</div>
<div class="card">
  <header class="card-header">
    <p class="card-header-title"><%= entry.title %></p>
  </header>
  <div class="card-content">
    <div class="content">
      <%= entry.description %>
      <%= link_to 'サイトで読む', entry.link %>
    </div>
  </div>
  <footer class="card-footer">
    <p class="card-footer-item">
      <%= link_to entry.feed.title, feed_path(entry.feed) %>
    </p>
    <p class="card-footer-item">
      公開日: <%= l(entry.published_at, format: :long) %>
    </p>
  </footer>
</div>

おわりに

rubyの標準ライブラリを使うとRSSを簡単にParseしてRSSリーダー的なものがすぐ作れるんですねー。
他にも標準ライブラリは結構あるので、いろいろ見てみようかと思いました🙇‍♂️

参考

docs.ruby-lang.org

www.buildinsider.net

FactoryBotでtrait付きの関連(Association)を定義する

こんにちは、まどぎわです(・∀・)

今回はFactoryBotでtrait付きの関連(Association)を定義する方法を知ったのでメモしておきます✍

前提: Quizの正解数ランキングを集計する処理の検証

今回は下記のようなクイズへのユーザーの解答を集計してランキングを作成するようなテストコードを書いたものとします、そこまで悪くなさそうですがcreate(:quiz, :with_choices)が何度も出てきてしまっているのがイマイチですね😥

let(:no1_user) { create(:user) }
let(:no2_user) { create(:user) }
let(:no3_user) { create(:user) }

before do
  create(:quiz_user_answer, quiz: create(:quiz, :with_choices), user: no1_user, correct: true)
  create(:quiz_user_answer, quiz: create(:quiz, :with_choices), user: no1_user, correct: true)
  create(:quiz_user_answer, quiz: create(:quiz, :with_choices), user: no2_user, correct: true)
  create(:quiz_user_answer, quiz: create(:quiz, :with_choices), user: no2_user, correct: false)
  create(:quiz_user_answer, quiz: create(:quiz, :with_choices), user: no3_user, correct: false)
  create(:quiz_user_answer, quiz: create(:quiz, :with_choices), user: no3_user, correct: false)
end

it '正解数の降順で取得出来ること' do
  ranking = described_class.ranking
  expect(ranking.map(&:user_id)).to eq [no1_user.id, no2_user.id, no3_user.id]
end

trait付きの関連(Association)を定義してリファクタリング

こういう場合にtrait付きの関連をfactoryに定義すると便利です👀

FactoryBot.define do
  factory :quiz_user_answer do
    association :quiz, :with_choices
    user
    correct { [true, false].sample }
  end
end

こんな感じで繰り返しcreate(:quiz, :with_choices)書かなくていいのでスッキリしますね🙌

let(:no1_user) { create(:user) }
let(:no2_user) { create(:user) }
let(:no3_user) { create(:user) }

before do
  create(:quiz_user_answer, user: no1_user, correct: true)
  create(:quiz_user_answer, user: no1_user, correct: true)
  create(:quiz_user_answer, user: no2_user, correct: true)
  create(:quiz_user_answer, user: no2_user, correct: false)
  create(:quiz_user_answer, quiz: user: no3_user, correct: false)
  create(:quiz_user_answer, quiz: user: no3_user, correct: false)
end

it '正解数の降順で取得出来ること' do
  ranking = described_class.ranking
  expect(ranking.map(&:user_id)).to eq [no1_user.id, no2_user.id, no3_user.id]
end

参考

github.com