そうそうそれ!

と言ってもらえるように頑張ります

オープンソースのmastodonからみるRails + React構成や採用されているライブラリについて

mastodonについて

mastodonという新興SNSが国内でも流行りだしています。フェーズとしてはまだイノベータからアーリーアダプターが様子を見てるって感じかなと個人的には思います。

mstdn.jpという日本ドメインインスタンスでは、現時点(2017/4/22)で全インスタンスと比べても世界一のユーザ数を誇っています。ただ、実際に投稿内容を見てみるとユーザもまだテスト投稿したりという感じのようです。(もっとコアなドメインインスタンスだとそのコンテキストの投稿がもっと盛んにされているのかもしれないですが)

このサービスの面白いところは、分散型ネットワーク+OSSというところなのかなと勝手に解釈しています。つまり、個人もしくは企業がTwitterのようなSNSサービスを(手軽に)独自運営していけてそれが本家の改善にもつながる仕組みになっているところに魅力があります。もう少し具体的に言うと本体のソースコードOSSなので大抵はForkしたものをデプロイすることになると思うのですが、そのインスタンス特有の機能を入れて本体とは少し違ったアプリケーションにしたり、場合によっては追加した機能や見つかったバグの修正を本体側にPull-Requestするなどサービス自体が分散して成長していく仕組みが取り入れられているのです。

コードを覗いてみてみる

前置きが少し長くなりましたがコードを実際に見てみるとRails+Reactで構成されていることがわかります。流行りのWebアプリケーションの構成だと思うので、参考になるところが多いです。

サーバーサイド構成

Rubyは2.4.1、Railsのバージョンは現時点で5.0.2で比較的新しい構成であることがわかります。

ruby '2.4.1'

gem 'rails', '~> 5.0.2'
APIの管理

app/controllers/api_controller.rbにApiControllerと言う基底クラスを定義しています。また、app/controllers/api/v1配下に実際のAPIをApiControllerクラスの具象クラスとして配置しています。これでいわゆるAPIのバージョニング管理をしているようです。

concernの実装

model/concerns配下では以下のような共通の振る舞いが定義されています。

cacheable.rb
paginable.rb
streamable.rb
targetable.rb

paginable.rbを見てみると内部的にarel_tableを使用してqueryを組み立てていることがわかります。

scope :paginate_by_max_id, -> (limit, max_id = nil, since_id = nil) {
      query = order(arel_table[:id].desc).limit(limit)
      query = query.where(arel_table[:id].lt(max_id)) unless max_id.blank?
      query = query.where(arel_table[:id].gt(since_id)) unless since_id.blank?
      query
}

フロントエンド構成

app/assets/javascripts/components配下にReactのコードが配置されています。JavascriptはECMAScirpt 2015記法で書かれています。Gemfileをみると以下の記述が確認できます。

gem 'react-rails'
gem 'browserify-rails'
gem 'autoprefixer-rails'
React + Redux

reactをrailsに導入するアプローチはいくつかありますが、mastodonではasset_pipline上にReactをのせるためreact-railsを採用しています。また、browserifyでbabelを使ってコンパイルするようにbrowserify-railsを採用しています。Reactでのスタイル適用はインラインでもscssでもどちらにも書かれています。

Ajax

ajax通信ライブラリとしてはaxiosが採用されています。ただし、使用するにはapi.jsxというラッパーモジュールから呼び出しています。

レンダリング対策

react-addons-pure-render-mixinが採用されています。

途中。気づいたら追記

Azure AppServiceで任意のタイムゾーンを設定する

タイムゾーンを日本に合わせたい人へ

ポータル > AppService > アプリケーション設定 でアプリ設定にWEBSITE_TIME_ZONEを「Tokyo Standard Time」で保存すればアプリが自動的に反映してくれます。

f:id:sousousore1:20160913182548p:plain

ASP.NET Coreでスクレイピングしたものを表示(HtmlAgilityPackを使用)

概要

SgmlReaderが.NET Core対応していないので.NET Core対応しているHtmlAgilityPackを使うことにしました。 ASP.NET Coreでスクレイピングしたページを表示する簡単なWebアプリケーションを作ってみたいと思います。 おっさんにyoしてWebアプリケーションの雛形ができたところからスタートとしたいと思います。

github.com

project.jsonへの登録

dependenceisのところにこんな感じで追加。 するとVSCodeが空気を呼んでrestoreするか聞いてくれるのでやっちゃう。

"HtmlAgilityPack.NetCore": "1.5.0.1"

適当なModelを用意する

    public class Availability
    {
        public string Title {get; set;}
        public IEnumerable<DateTime> Dates {get;set;}

        public string ToString(DateTime date)
        {
            return date.ToString("yyyy年MM月dd日(dddd)");
        }
    }
    public class IndexViewModel
    {
        public List<Availability> Availabilities {get; set;}
    }

Controllerに処理を書く

SelectNodesしたときは//を入れるルールらしい。 スクレイピング自体はやっつけなのでサイトの構成変わったら動かんと思います。

        public async Task<IActionResult> Index()
        {
            // service_category
            var rootUrl = new Uri("https://as.its-kenpo.or.jp/service_category/index");
            var serviceCategoryDom = await GetHtmlAsync(rootUrl);
            var serviceGroupUrl = serviceCategoryDom.DocumentNode.SelectNodes("//a")
                .Where(x => x.InnerText == "直営・通年・夏季保養施設(空き照会)")
                .Select(x => x.Attributes["href"].Value)
                .Select(x => $"{rootUrl.Scheme}://{rootUrl.Host}" + x)
                .Select(x => new Uri(x))
                .First();

            // service_group
            var serviceGroupDom = await GetHtmlAsync(serviceGroupUrl);
            var serviceApplyUrls = serviceGroupDom.DocumentNode.SelectNodes("//li")
                .SelectMany(x => x.Descendants("a"))
                .Select(x => x.Attributes["href"].Value)
                .Select(x => $"{rootUrl.Scheme}://{rootUrl.Host}" + x)
                .Select(x => new Uri(x));
            
            // service_apply
            var availabilities = new List<Availability>();
            foreach (var serviceApplyUrl in serviceApplyUrls)
            {
                var serviceApplyDom = await GetHtmlAsync(serviceApplyUrl);
                var applyUrls = serviceApplyDom.DocumentNode.SelectNodes("//li")
                    .SelectMany(x => x.Descendants("a"))
                    .Select(x => x.Attributes["href"].Value)
                    .Select(x => $"{rootUrl.Scheme}://{rootUrl.Host}" + x)
                    .Select(x => new Uri(x));
                    
                // apply
                foreach (var applyUrl in applyUrls)
                {
                    var applyDom = await GetHtmlAsync(applyUrl);
                    var title = applyDom.DocumentNode.SelectNodes("//table")
                        .Where(x => x.Attributes["class"] != null)
                        .Where(x => x.Attributes["class"].Value == "tform_new")
                        .Select(x => x.Descendants("tr").First())
                        .Select(x => x.Descendants("td").Last().InnerText)
                        .First();
                    var dates = applyDom.DocumentNode.SelectNodes("//select")
                        .Where(x => x.Attributes["id"] != null)
                        .Where(x => x.Attributes["id"].Value == "apply_join_time")
                        .SelectMany(x => x.Descendants("option"))
                        .Select(x => x.Attributes["value"].Value)
                        .Where(x => !string.IsNullOrEmpty(x))
                        .Select(x => Convert.ToDateTime(x));

                    var availability = new Availability();
                    availability.Title = title;
                    availability.Dates = dates;
                    availabilities.Add(availability);
                }

            }

            var model = new IndexViewModel();
            model.Availabilities = availabilities;
            return View(model);
        }

Viewにモデルをバインド

@model Kenpo.Models.HomeViewModels.IndexViewModel

<ul>
@foreach(var availability in Model.Availabilities)
{
    <li>
        <span>@availability.Title</span>
        <ul>
            @foreach(var date in availability.Dates)
            {
                <li>@availability.ToString(date)</li>
            }
        </ul>
    </li>
}
</ul>

結果

勘の良い人ならわかるかもしれませんがこんな感じでIT健保の(使いにくい)サイトから空き日を一覧にしてくれるWebアプリケーションが完成します。

直営・通年・夏季保養施設 トスラブ箱根ビオーレ 2016年度9月分申込
2016年09月29日(木曜日)
直営・通年・夏季保養施設 トスラブ箱根ビオーレ 2016年度10月分申込
2016年10月24日(月曜日)
2016年10月26日(水曜日)
2016年10月31日(月曜日)
直営・通年・夏季保養施設 トスラブ箱根和奏林 2016年度9月分申込
2016年09月20日(火曜日)
直営・通年・夏季保養施設 トスラブ箱根和奏林 2016年度10月分申込
2016年10月06日(木曜日)
2016年10月20日(木曜日)
2016年10月24日(月曜日)
直営・通年・夏季保養施設 トスラブ湯沢 2016年度9月分申込
2016年09月19日(月曜日)
2016年09月20日(火曜日)
2016年09月22日(木曜日)
2016年09月26日(月曜日)
2016年09月28日(水曜日)
2016年09月29日(木曜日)
直営・通年・夏季保養施設 トスラブ湯沢 2016年度10月分申込
2016年10月02日(日曜日)
2016年10月03日(月曜日)
2016年10月04日(火曜日)
2016年10月05日(水曜日)
2016年10月06日(木曜日)
2016年10月12日(水曜日)
2016年10月13日(木曜日)
2016年10月17日(月曜日)
2016年10月18日(火曜日)
2016年10月19日(水曜日)
2016年10月20日(木曜日)
2016年10月24日(月曜日)
2016年10月25日(火曜日)
2016年10月26日(水曜日)
2016年10月27日(木曜日)
2016年10月31日(月曜日)
直営・通年・夏季保養施設 トスラブ館山ルアーナ 2016年度9月分申込
2016年09月29日(木曜日)
直営・通年・夏季保養施設 トスラブ館山ルアーナ 2016年度10月分申込
直営・通年・夏季保養施設 中沢ヴィレッジ 2016年度9月分申込
2016年09月29日(木曜日)
直営・通年・夏季保養施設 中沢ヴィレッジ 2016年度10月分申込
2016年10月03日(月曜日)
2016年10月04日(火曜日)
2016年10月05日(水曜日)
2016年10月06日(木曜日)
2016年10月11日(火曜日)
2016年10月12日(水曜日)
2016年10月13日(木曜日)
2016年10月17日(月曜日)
2016年10月18日(火曜日)
2016年10月19日(水曜日)
2016年10月20日(木曜日)
2016年10月24日(月曜日)
2016年10月25日(火曜日)
2016年10月26日(水曜日)
2016年10月27日(木曜日)
2016年10月31日(月曜日)
直営・通年・夏季保養施設 ホテルハーヴェスト那須 2016年度9月分申込
2016年09月27日(火曜日)
直営・通年・夏季保養施設 ホテルハーヴェスト那須 2016年度10月分申込
2016年10月31日(月曜日)
直営・通年・夏季保養施設 ホテルハーヴェスト斑尾 2016年度9月分申込
2016年09月23日(金曜日)
2016年09月26日(月曜日)
2016年09月27日(火曜日)
2016年09月28日(水曜日)
2016年09月29日(木曜日)
2016年09月30日(金曜日)
直営・通年・夏季保養施設 ホテルハーヴェスト斑尾 2016年度10月分申込
2016年10月02日(日曜日)
2016年10月03日(月曜日)
2016年10月04日(火曜日)
2016年10月05日(水曜日)
2016年10月06日(木曜日)
2016年10月07日(金曜日)
2016年10月11日(火曜日)
2016年10月12日(水曜日)
2016年10月13日(木曜日)
2016年10月16日(日曜日)
2016年10月17日(月曜日)
2016年10月18日(火曜日)
2016年10月19日(水曜日)
2016年10月20日(木曜日)
2016年10月21日(金曜日)
2016年10月23日(日曜日)
2016年10月24日(月曜日)
2016年10月25日(火曜日)
2016年10月26日(水曜日)
2016年10月27日(木曜日)
2016年10月30日(日曜日)
2016年10月31日(月曜日)
直営・通年・夏季保養施設 ブルーベリーヒル勝浦 2016年度9月分申込
2016年09月19日(月曜日)
2016年09月20日(火曜日)
2016年09月21日(水曜日)
2016年09月28日(水曜日)
2016年09月29日(木曜日)
直営・通年・夏季保養施設 ブルーベリーヒル勝浦 2016年度10月分申込
2016年10月02日(日曜日)
2016年10月03日(月曜日)
2016年10月04日(火曜日)
2016年10月05日(水曜日)
2016年10月06日(木曜日)
2016年10月11日(火曜日)
2016年10月12日(水曜日)
2016年10月13日(木曜日)
2016年10月16日(日曜日)
2016年10月18日(火曜日)
2016年10月19日(水曜日)
2016年10月20日(木曜日)
2016年10月23日(日曜日)
2016年10月24日(月曜日)
2016年10月25日(火曜日)
2016年10月26日(水曜日)
2016年10月27日(木曜日)
2016年10月30日(日曜日)
2016年10月31日(月曜日)
直営・通年・夏季保養施設 ホテルハーヴェスト伊東 2016年度9月分申込
2016年09月30日(金曜日)
直営・通年・夏季保養施設 ホテルハーヴェスト伊東 2016年度10月分申込
2016年10月03日(月曜日)
2016年10月04日(火曜日)
2016年10月05日(水曜日)
2016年10月06日(木曜日)
2016年10月11日(火曜日)
2016年10月12日(水曜日)
2016年10月13日(木曜日)
2016年10月17日(月曜日)
2016年10月18日(火曜日)
2016年10月19日(水曜日)
2016年10月20日(木曜日)
2016年10月24日(月曜日)
2016年10月25日(火曜日)
2016年10月26日(水曜日)
2016年10月27日(木曜日)
直営・通年・夏季保養施設 ホテルハーヴェスト スキージャム勝山 2016年度9月分申込
2016年09月25日(日曜日)
2016年09月26日(月曜日)
2016年09月27日(火曜日)
2016年09月28日(水曜日)
2016年09月29日(木曜日)
2016年09月30日(金曜日)
直営・通年・夏季保養施設 ホテルハーヴェスト スキージャム勝山 2016年度10月分申込
2016年10月01日(土曜日)
2016年10月02日(日曜日)
2016年10月03日(月曜日)
2016年10月04日(火曜日)
2016年10月05日(水曜日)
2016年10月06日(木曜日)
2016年10月07日(金曜日)
2016年10月10日(月曜日)
2016年10月11日(火曜日)
2016年10月12日(水曜日)
2016年10月13日(木曜日)
2016年10月14日(金曜日)
2016年10月15日(土曜日)
2016年10月16日(日曜日)
2016年10月17日(月曜日)
2016年10月18日(火曜日)
2016年10月19日(水曜日)
2016年10月20日(木曜日)
2016年10月21日(金曜日)
2016年10月23日(日曜日)
2016年10月24日(月曜日)
2016年10月25日(火曜日)
2016年10月26日(水曜日)
2016年10月27日(木曜日)
2016年10月28日(金曜日)
2016年10月30日(日曜日)
2016年10月31日(月曜日)
直営・通年・夏季保養施設 琵琶レイクオーツカ 2016年度9月分申込
2016年09月28日(水曜日)
直営・通年・夏季保養施設 琵琶レイクオーツカ 2016年度10月分申込
2016年10月02日(日曜日)
2016年10月03日(月曜日)
2016年10月04日(火曜日)
2016年10月05日(水曜日)
2016年10月06日(木曜日)
2016年10月07日(金曜日)
2016年10月11日(火曜日)
2016年10月12日(水曜日)
2016年10月13日(木曜日)
2016年10月14日(金曜日)
2016年10月16日(日曜日)
2016年10月17日(月曜日)
2016年10月18日(火曜日)
2016年10月19日(水曜日)
2016年10月20日(木曜日)
2016年10月21日(金曜日)
2016年10月24日(月曜日)
2016年10月25日(火曜日)
2016年10月26日(水曜日)
2016年10月27日(木曜日)
2016年10月31日(月曜日)
直営・通年・夏季保養施設 ホテル日航プリンセス京都 2016年度9月分申込
2016年09月26日(月曜日)
2016年09月27日(火曜日)
直営・通年・夏季保養施設 ホテル日航プリンセス京都 2016年度10月分申込
2016年10月31日(月曜日)
直営・通年・夏季保養施設 ホテルハーヴェスト南紀田辺 2016年度9月分申込
2016年09月23日(金曜日)
2016年09月25日(日曜日)
2016年09月26日(月曜日)
2016年09月27日(火曜日)
2016年09月28日(水曜日)
2016年09月29日(木曜日)
2016年09月30日(金曜日)
直営・通年・夏季保養施設 ホテルハーヴェスト南紀田辺 2016年度10月分申込
2016年10月02日(日曜日)
2016年10月03日(月曜日)
2016年10月04日(火曜日)
2016年10月05日(水曜日)
2016年10月06日(木曜日)
2016年10月07日(金曜日)
2016年10月10日(月曜日)
2016年10月11日(火曜日)
2016年10月12日(水曜日)
2016年10月13日(木曜日)
2016年10月14日(金曜日)
2016年10月16日(日曜日)
2016年10月17日(月曜日)
2016年10月18日(火曜日)
2016年10月19日(水曜日)
2016年10月20日(木曜日)
2016年10月21日(金曜日)
2016年10月23日(日曜日)
2016年10月24日(月曜日)
2016年10月25日(火曜日)
2016年10月26日(水曜日)
2016年10月27日(木曜日)
2016年10月28日(金曜日)
2016年10月30日(日曜日)
2016年10月31日(月曜日)
直営・通年・夏季保養施設 角間温泉 岩屋館 2016年度9月分申込
2016年09月26日(月曜日)
2016年09月27日(火曜日)
2016年09月30日(金曜日)
直営・通年・夏季保養施設 角間温泉 岩屋館 2016年度10月分申込
2016年10月03日(月曜日)
2016年10月04日(火曜日)
2016年10月05日(水曜日)
2016年10月06日(木曜日)
2016年10月11日(火曜日)
2016年10月12日(水曜日)
2016年10月13日(木曜日)
2016年10月17日(月曜日)
2016年10月18日(火曜日)
2016年10月19日(水曜日)
2016年10月20日(木曜日)
2016年10月25日(火曜日)
2016年10月26日(水曜日)
2016年10月27日(木曜日)
直営・通年・夏季保養施設 ホテルハーヴェスト旧軽井沢 2016年度9月分申込
直営・通年・夏季保養施設 リゾートホテル蓼科 2016年度9月分申込
2016年09月28日(水曜日)
2016年09月29日(木曜日)
直営・通年・夏季保養施設 鎌倉パークホテル 2016年度9月分申込

ASP.NET Core でFacebook連携

基本的にはここ

Enabling authentication using Facebook, Google and other external providers — ASP.NET documentation

開発時点では環境をDevelopmentモードで起動する

本番ではUserSecretのファイルではなく環境変数を使用することになるのでDevelopmentモードで起動しなくてはいけない。やり方はふた通り

  • dotnet runのオプションとして渡す場合
dotnet run ASPNETCORE_ENVIRONMENT=Development
  • マシンの環境変数にあらかじめ設定しておく
export ASPNETCORE_ENVIRONMENT=Development
dotnet run

ruby2.0とrails4.0をrvmでインストールした

最近は、とてもとても忙しくてなかなかお家で開発作業を進めることができなかったけど、
手がけていたアプリがとりあえずリリースできて少しだけ落ち着いたので早速rubyを導入する。
ということで、ruby2.0、ails4.0(beta1)をインストールしてみた。

opensslのバージョンが古いとダメっぽいのでそこのupdateから始める。
(環境はmaxos10.7.5)
全部で結構時間かかったけどとりあえずできた。

brewのupdate

$ brew update 
$ brew install openssl


rvmのupdateからのruby2.0インストール

$ rvm get stable
$ rvm reload
$ rvm install 2.0.0
$ rvm use 2.0.0 --default

rails4.0 beta1のインストール

$ gem install rails -v 4.0.0.beta1

rails の seeds.rbの使い方について

連休だったので、久しぶりにC#から離れてRailsのお勉強。
もうすぐRuby2.0&Rails4.0がリリースされるみたいなので、もう一度参入するチャンス!
で、今回はRailsのseeds.rbの簡単な使い方をまとめる。

概要

  • マスタデータの投入。つまり初期データの投入に使える。

コマンド

rake db:seed

使い方

seeds.rbの編集

コマンドを呼び出すたびにseeds.rbの中身が追加されてしまうので、一回全部消去してidのautoincrementもクリアして入れなおす。
例えばBlogというモデルがあった場合は、一回delete_allを読んで


Blog.delete_all # 一回全部消して
Blog.connection.execute("delete from sqlite_sequence where name='blogs'") # autoincrementをクリアして
Blog.create(:name => "hoge blog") # データ投入

sqlite3で確認

コマンド呼び出したら実際にデータが投入されているか確認する

sqlite3 db/development.sqlite3

> select * from blogs;

注意点

日本語を扱う場合はファイルの先頭に以下を忘れないようにする。

# encoding: utf-8

依存関係がある場合は親のインスタンスを渡す

blog1 = Blog.create(:name => "hoge blog")
Entry.create(:blog => blog1, context => 'hogehogehoge')

Windowsアプリケーションで矢印キーとEnterでのフォーカス移動

というトンデモな仕様を実装しろとのこと。
明らかにWindows標準APIを無視した設計だったので、
仕様の再検討を!!ってまずお願いしたけど、一応調べてみた。

TextBoxなどでは、文字列にカーソル当たっている状態では左右キーは文字間を移動するのに使われる。
その為、キャレット位置が先頭のときに←を押すと前のコントロールへフォーカス
キャレット位置が最終のときに→を押すと次のコントロールへフォーカスという動きを考えた。
TextBoxには、SelectionStartというプロパティがあって簡単にキャレット位置を判定できたが、
こまったのはそれ以外のコントロールについて。
例えば、ComboBoxなんかも任意文字列が入力できるのでキャレット位置を取得したい。
TextBoxにあったSelectionStartプロパティが使えたらなーっと思ってたらこっちにもあった。

DRYじゃねぇ・・・。

とおもった。
TextBoxBaseクラスを継承してたり、Interfaceとかで提供されてて欲しかった。

一応コード
TextBoxのみ文字間移動を対応。その他については考慮していない
あと、実装したのは共通で利用するForm。(KeyPreviewプロパティをTrueにしておく)
共通で利用するFormを継承したFormで自動的に適用される。

        private void CommonForm_KeyDown(object sender, KeyEventArgs e)
        {
            if (e.KeyCode == Keys.Left)
            {
                if (this.ActiveControl is TextBox)
                {
                    TextBox textBox = (TextBox)this.ActiveControl;
                    if (textBox.SelectionStart != 0)
                    {
                        return;
                    }   
                }

                this.SelectNextControl(this.ActiveControl, false, true, true, true);
            }

            if (e.KeyCode == Keys.Right)
            {
                if (this.ActiveControl is TextBox)
                {
                    TextBox textBox = (TextBox)this.ActiveControl;
                    if (textBox.SelectionStart != textBox.Text.Length)
                    {
                        return;
                    }
                }

                this.SelectNextControl(this.ActiveControl, true, true, true, true);
            }

            if (e.KeyCode == Keys.Enter)
            {
                bool forward = e.Modifiers != Keys.Shift;
                this.SelectNextControl(this.ActiveControl, forward, true, true, true);
            }
        }