【React, TypeScript】Firestoreでリアルタイムアップデート

自分用のメモ

type MembersType =
  firebase.firestore.QueryDocumentSnapshot<firebase.firestore.DocumentData>[];

const Members = () => {
  const [members, setMembers] = useState<MembersType>([]);

  useEffect(() => {
    const unsubscribe = db
      .collection('members')
      .orderBy('createdAt', 'desc')
      .limit(10)
      .onSnapshot((querySnapshot) => {
        setMembers(querySnapshot.docs);
      });
    return () => {
      unsubscribe();
    };
  }, []);

  return (
  // code
  );

ノーコード・ローコードまとめ

Webサービス・アプリ

Webサービスやアプリとして提供されることを想定したサービス。

  • Adalo
  • bubble
  • Glide
  • Thunkable
  • Yappli

Webサイト制作

主に企業サイトや静的サイトとしての利用を想定したサービス。

社内ツール・業務アプリ

社内など少人数向けの業務効率化ツールとしての利用を想定したサービス。

WordPressでプラグインなしで記事ごとにnoindexを設定する方法

個別記事ごとにnoindexを設定したい場合のコードです。 add_meta_box()を使ってカスタムフィールドをサイドバーに追加します。

投稿の設定の場合、wp_postmetaテーブルにデータを入れることが多いと思いますが、投稿IDの配列だけ保存できれば良いので「ブログのトップに固定(sticky_posts)」と同様にwp_optionsテーブルに保存します。

<?php

class Noindex_Settings
{
    public function __construct()
    {
        add_action('add_meta_boxes', [$this, 'custom_meta_boxes']);
        add_action('save_post', [$this, 'update']);
    }

    /**
     * カスタムフィールドのボックスを追加する
     */
    function custom_meta_boxes()
    {
        global $post_type;

        add_meta_box(
            'noindex_setting',
            '検索エンジン設定',
            [$this, 'insert_fields'],
            'post',
            'side',
            'high'
        );
    }

    /**
     * カスタムフィールドの入力エリアを設定する
     */
    function insert_fields()
    {
        global $post;

        $noindex_posts = get_option('noindex_posts');
        $checked = '';

        if (is_array($noindex_posts)) {
            $noindex_posts = array_unique(array_map('intval', $noindex_posts));

            if (in_array($post->ID, $noindex_posts, true)) {
                $checked = 'checked';
            }
        }

        echo '<label>
    <input type="checkbox" name="noindex_posts" value="checked" ' .
            $checked .
            '> 検索エンジンに登録しない(noindex)</label>';
    }

    /**
     * save_postフックでカスタムフィールドの値を保存
     * @param int $post_id
     */
    function update($post_id)
    {
        //自動保存ならアップデートしない
        if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
            return;
        }

        $post_id = (int) $post_id;
        $noindex_posts = get_option('noindex_posts');

        if ($_POST['noindex_posts']) {
            //noindex_postsにpost_idを追加
            if (!is_array($noindex_posts)) {
                $noindex_posts = [$post_id];
            } else {
                $noindex_posts = array_unique(
                    array_map('intval', $noindex_posts)
                );
            }

            if (!in_array($post_id, $noindex_posts, true)) {
                $noindex_posts[] = $post_id;
            }

            update_option('noindex_posts', array_values($noindex_posts));
        } else {
            //noindex_postsからpost_idを削除
            if (!is_array($noindex_posts)) {
                return;
            }

            $noindex_posts = array_values(
                array_unique(array_map('intval', $noindex_posts))
            );

            if (!in_array($post_id, $noindex_posts, true)) {
                return;
            }

            $offset = array_search($post_id, $noindex_posts, true);
            if (false === $offset) {
                return;
            }

            array_splice($noindex_posts, $offset, 1);
            update_option('noindex_posts', $noindex_posts);
        }
    }
}

new Noindex_Settings();

あとはnoindexかを判定する関数を作って、metaタグでnoindex設定すればOKです。

<?php

/**
 * noindexにするページを判定する関数
 *
 * @return boolean
 */
function is_noindex()
{
    $post_id = get_the_ID();
    $noindex_posts = get_option('noindex_posts');

    if (is_array($noindex_posts)) {
        $noindex_posts = array_map('intval', $noindex_posts);
        $is_noindex = in_array($post_id, $noindex_posts, true);
    } else {
        $is_noindex = false;
    }

    //アーカイブや404もnoindex判定に追加
    if (
        $is_noindex ||
        is_date() ||
        is_404() ||
        is_search() ||
        is_author() ||
        is_paged() ||
        is_attachment()
    ) {
        return true;
    } else {
        return false;
    }
}
<?php if (is_noindex()): ?>
    <meta name="robots" content="noindex">
<?php endif; ?>

MySQLで集計するときに使える小技

期間を分けて集計

過去7日間とその前の7日間を比較するといった場合は、CASE文が使えます。

クエリ

SELECT CASE
           WHEN (DATE(m.created_at) >= DATE_ADD(CURRENT_DATE(), INTERVAL - 7 DAY)) THEN "過去7日間"
           ELSE "比較期間"
       END AS period,
       COUNT(DISTINCT m.id) AS count
FROM members AS m
WHERE DATE(m.created_at) BETWEEN DATE_SUB(CURRENT_DATE(), INTERVAL 14 DAY) AND DATE_SUB(CURRENT_DATE(), INTERVAL 1 DAY)
GROUP BY period

期間毎に集計する

対象期間を、7日間毎や30分毎に分けて集計したい場合は、TRANCATE()が使えます。 以下は、過去28日間の登録者数を7日間毎に集計するという例です。

クエリ

SELECT if(TRUNCATE((DATEDIFF(CURRENT_DATE(), DATE(m.created_at)) - 1) / 7, 0) >= 1, "比較期間", "過去7日間") AS period,
       COUNT(DISTINCT m.id) AS count
FROM members AS m
WHERE DATE(m.created_at) BETWEEN DATE_SUB(CURRENT_DATE(), INTERVAL 28 DAY) AND DATE_SUB(CURRENT_DATE(), INTERVAL 1 DAY)
GROUP BY TRUNCATE((DATEDIFF(CURRENT_DATE(), DATE(m.created_at)) - 1) / 7, 0)

(DATEDIFF(CURRENT_DATE(), DATE(m.created_at)) - 1)で、今日とm.created_atの差を出します。 -1しているので、差は0~13になります。

この差を7で割り、少数点以下をTRANCATE()で切り捨てることで、0~3のグループに分けることができます。

Macにrbenvを入れてからRails + Vue.jsをインストールする

rbenvでRubyのバージョン管理

$ brew update
$ brew install rbenv ruby-build
$ rbenv --version
  rbenv 1.1.1
$ rbenv install --list
$ rbenv install 2.4.1
$ rbenv global 2.4.1
$ rbenv rehash
$ ruby -v
  ruby 2.4.1p111 (2017-03-22 revision 58053) [x86_64-darwin15]

Railsを導入する

$ gem install bundler
$ mkdir new_app
$ cd new_app
$ bundle init

# gem 'rails'のコメントアウトをはずす
$ vi Gemfile 

# オプション指定でディレクトリ配下のvendor/bundleにインストール
$ bundle install --path vendor/bundle --jobs=4

# オプションを確認
$ bundle exec rails new -h 

# カレントディレクトリに作成。GemfileはOverwriteするか聞かれるのでY。
$ bundle exec rails new ./ -B -d mysql --skip-turbolinks --skip-test-unit --webpack=vue

サーバーを起動して、http://localhost:3000で確認。

$bundle exec rails s

Vueを入れる

Rails 5.1からはwebpackをサポートするようになったそうで、Vueを簡単に導入できます。

# Gemfileに`gem 'webpacker'`を追加
$ vi Gemfile
$ bundle install

# webpackとvueのインストール
$ bundle exec rails webpacker:install
$ bundle exec rails webpacker:install:vue

webpackまたはwebpack-dev-serverを使うことで、webpackを起動することができます。 webpack-dev-serverの場合は、以下のようなことをしてくれるそうですが、今回は試していません。

  • ファイルの変更を検知して、自動リビルド&ブラウザの自動リロード
  • ローカルサーバーを起動
# 以下のようなエラーがでる
$ bin/webpack
 Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
 Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.


# エラーのコメント通りに実行
$ bundle binstubs bundler --force

# 再実行
$ bin/webpack

これで最低限恩開発環境のセットは終了です。

参考: - Rails 5.1 + Vue.js で開発を行う - part1 環境構築 - Qiita - 【動画付き】Rails 5.1で作るVue.jsアプリケーション ~Herokuデプロイからシステムテストまで~ - Qiita - rails newの先輩オススメ手順 - まえとうしろ

AWS S3からほかのサーバーにファイルを一括でダウンロードする

AWS S3からファイルを一括ダウンロードするには、公式クライアントであるaws-cliが便利です。 ほかにもツールがあるようですが、今回の用途のためには公式ツール一択と考えて良いでしょう。

aws-cliをダウンロード

S3からほかのサーバーにファイルを移行する場合は、aws-cliが便利です。 まずは、移行先のサーバーにaws-cliをインストールします。

公式ドキュメントは以下の通り。

docs.aws.amazon.com

Python 2.6.5以上またはPython3.3以上が必要なので、もしインストールされていなければインストールします。

yum info python
yum install -y python python-pip
pip install awscli --upgrade --user

awsコマンドを使えるようにパスを通して、aws --versionで確認をします。

$ ls ~/.local/bin
  aws
$ export PATH=~/.local/bin:$PATH
$ source ~/.bash_profile
$ aws --version
  aws-cli/1.11.84 Python/3.6.2 Linux/4.4.0-59-generic botocore/1.5.47

次にaws-cliの設定を行います。

docs.aws.amazon.com

aws-cliの設定には、IAMユーザーのアクセスキーとシークレットアクセスキーが必要です。 IAMユーザーとは、Identity and Access Managementを省略されたもので、ユーザー毎にアクセス権限が設定されています。

初めて作成するAWS アカウントはルートユーザーと呼ばれ、AWS上のすべてのサービスに対して完全なアクセス権限を持っています。 サーバーで言うrootユーザーと同じで権限が強すぎるため、適切に権限を割り当てたIAMユーザーを作成して、これを使うことが推奨されています。

まだ、ユーザーを作成していない場合やユーザーを追加したい場合は、IAMコンソールにアクセスし「ユーザー追加」をクリックします。 作成途中の権限設定で、「AmazonS3FullAccess」を選択するようにしてください。

$ aws configure
  AWS Access Key ID [None]: アクセスキーを入力
  AWS Secret Access Key [None]: シークレットアクセスキーを入力
  Default region name [None]: 東京の場合はap-northeast-1と入力
  Default output format [None]: json

設定がうまくいっているか確認するために、s3のバケットを確認します。

$ aws s3 ls
  2016-10-17 18:49:32 example.com

S3からダウンロード

ファイルのダウンロードはコマンドひとつで可能です。 ディレクトリ内のファイルをすべてダウンロードする場合は、オプション--recursiveをつけます。

aws s3 cp s3://バケット名/ディレクトリ/ /保存先ディレクトリ/ --recursive

たとえば、以下のようなコマンドになります。

aws s3 cp s3://example.com/wp-content/uploads/ /var/www/html/wp-content/ --recursive

kaminariを使って外部APIのページネーションをする

外部のAPIから取得したデータをkaminariを使ってページングするときのメモです。

結論

kaminariのpaginate_arrayメソッドに、以下のように配列とtotal_countを指定することでできる。

@posts = Kaminari.paginate_array(配列, total_count: 総数)
  .page(現在のページ)
  .per(各ページ毎の表示数)

具体例

今回は、jsonを出力するAPIを用意し、そのjsonをパースしてページネーションをするということをします。

API

投稿一覧を出力するだけのシンプルなAPIを作成します。 こちらもKaminariをインストール済みとします。

コントローラー

class PostsController < ApplicationController

  def index
    @posts = Post.published.page(params[:page]).per(1)

    respond_to do |format|
      format.html { render :index }
      format.json { render 'index.json.jbuilder' }
    end
  end

end

ビュー(jbuilder

json.meta do # kaminariのメソッド
  json.total_pages @posts.total_pages
  json.current_page @posts.current_page
  json.total_count @posts.total_count
  json.limit_value @posts.limit_value
end
json.data do
  json.array! @posts do |post|
    json.id post.id
    json.title post.title
  end
end

json

以下のようなjsonが出力されます。

{
  "meta":{
    "total_pages":2,
    "current_page":1,
    "total_count":2,
    "limit_value":1
  },
  "data":[
    {
      "id":1,
      "title":"タイトル1",
    }
    {
      "id":2,
      "title":"タイトル2",
    }
  ]
}

ページネーション

コントローラー

class ExternalPostsController < ApplicationController

  def index
    require 'net/http'
    require 'uri'
    require 'json'

    end_point = "https://sample.com/posts.json?page=#{params[:page]}"
    uri = URI.parse(end_point)
    response = Net::HTTP.get_response(uri)
    body = JSON.parse(response.body)
    meta = body["meta"]

    @posts = Kaminari.paginate_array(body["data"], total_count: meta["total_count"])
      .page(meta["current_page"])
      .per(meta["limit_value"])
  end
end

ビュー

- @posts.each do |post|
    # postを表示する処理
- end

= paginate @posts