コンポーネントのマウント時にリクエストし、アンマウント時にキャンセルする axios リクエスト
とりあえず解はこうなった。
ライブラリのバージョン
- axio 0.18.0
- react 16.8.3
- react-dom 16.8.3
動作確認はこちら:
ボタンクリックでコンポーネントのマウント・アンマウントが切り替えできる。APIリクエスト中にアンマウントされると、下のようなエラーメッセージと共にリクエストがキャンセルされる。
解説
解説というものでもないかもしれないが、ちょっと説明。
useEffect
でマウント時のAPIリクエストのメソッドにキャンセルトークンソースを渡す。また、useEffect
の戻り値はアンマウント時に実行されるので、ここでAPIリクエストのキャンセルを定義する。
function useAxios(fetchAction) { useEffect(() => { const signal = axios.CancelToken.source(); fetchAction({ signal }); return () => { signal.cancel("Api is being canceled"); }; }, []); }
あとはaxios
にキャンセルトークンを渡すだけ:
useAxios(({ signal }) => { async function getUser() { const res = await axios.get("https://randomuser.me/api/", { cancelToken: signal.token }); setResult(res.data.results[0]); } getUser(); });
react-router
との組み合わせが便利なんじゃなかろうか。
TypeScript handbook Enums
TypeScript handbook Enums
https://www.typescriptlang.org/docs/handbook/enums.html
Enums
Enums
enums では、名前付きの定数群を定義します。その定数群の内容を明示したり、それぞれ異なるケースを簡単に作成することができます。TypeScript では、数値ベースと文字列ベースの enums を提供しています。
Numeric enums
まずは、他の言語でも馴染みのある数値 enums を見ていきます。enum
キーワードを用いて定義します。
enum Direction { Up = 1, Down, Left, Right }
ここでは、初期値1
を持つUp
の数値 enum を定義しています。それ以降のメンバーは自動的にインクリメントされるので、Direction.Up
は1
、Down
は2
、Left
は3
、Right
は4
となります。
また、初期値は省くこともできます。
enum Direction { Up, Down, Left, Right }
この場合、Up
は0
となり、Down
は1
となります。このオートインクリメントは、値自体は何でも良いが、同じ enum 内で値を区別するのに役立ちます。
enum の利用法はとてもシンプルです。プロパティにアクセスするように enum のメンバーにアクセスし、enum の名前を使って型を宣言するだけです。
enum Response { No = 0, Yes = 1 } function respond(recipient: string, message: Response): void { // ... } respond("Princess Caroline", Response.Yes);
数値 enum は、計算値と定数を混在させることができます。つまり、初期値のない enum は最初か、数値定数や他の定数 enum のメンバーの初期値を持つ数値 enum の後ろでなければなりません。したがって次のようには宣言できません。
enum E { A = getSomeValue(), B // エラー! 'A' は定数で初期化されていないため、'B'には初期値が必要 }
String enums
文字列 enum も似たようなコンセプトですが、後述するように実行時のふるまいに少しだけ違いがあります。文字列 enum では、メンバーはそれぞれ文字列リテラルかあるいは他の文字列 enum メンバーを初期値として持たなければなりません。
enum Direction { Up = "UP", Down = "DOWN", Left = "LEFT", Right = "RIGHT" }
文字列 enum はオートインクリメントしませんが、そのかわりうまく"serialize"することができます。例えばデバッグで、数値 enum の実行中の値を読み取ろうとしても、その値自体には意味はない(リバースマッピングはしばしば役に立ちますが)ので困難ですが、文字列 enum であれば、実行中の値からもメンバーの名前以上の意味を汲み取れます。
Heterogeneous enums
必要性は低いですが、文字列 enum と数値 enum を混在させることは可能です。
enum BooleanLikeHeterogeneousEnum { No = 0, Yes = "YES" }
JavaScript の実行中のふるまいをうまく利用しようとしているのでない場合は、おすすめしません。
Computed and constant members
enum メンバーはそれぞれ定数か計算値を持ちます。次のように定義した場合、enum メンバーは定数を持っているとみなされます。
- enum の最初のメンバーが初期値を持たない場合
この場合メンバーの値は 0 となります。
// E.X は定数: enum E { X }
- 初期値を持たず、かつ一つ前のメンバーが数値定数である場合。
この場合、メンバーの値は一つ前のメンバーの値に 1 を足した数値定数となります。
// 'E1' と 'E2' のすべてのメンバーは定数 enum E1 { X, Y, Z } enum E2 { A = 1, B, C }
定数 enum 式とは、コンパイル時に評価可能な式のことで、次のような場合に当てはまります。
- リテラル enum 式 (基本的には文字列リテラルか数値リテラル)
- すでに定義した enum メンバーへの参照 (他の enum でも可能)
- 括弧付きの定数 enum 式
+
、-
、~
の単項演算子が使われた定数 enum 式+
、-
、*
、/
、%
、<<
、>>
、>>>
、&
、|
、^
の二項演算子が使われた定数 enum 式。ただし、式の結果がNaN
かInfinity
の場合はコンパイルエラーとなります。
上記以外の場合、enum メンバーはすべて計算値としてみなされます。
enum FileAccess { // 定数メンバー None, Read = 1 << 1, Write = 1 << 2, ReadWrite = Read | Write, // 計算値メンバー G = "123".length }
Union enums and enum member types
特殊な enum メンバーのひとつに、リテラル enum メンバーがあります。リテラル enum メンバーは、初期値を持たないか、あるいは、次のような初期値を持ちます。
すべてのメンバーがリテラル enum メンバーであるとき、いくつかの特殊な構文が使用できます。
ひとつめは、enum メンバーは型としても使用することができるようになります。たとえば、特定のメンバーは、enum メンバーの値だけを持つことができるというように定義することができます。
enum ShapeKind { Circle, Square } interface Circle { kind: ShapeKind.Circle; radius: number; } interface Square { kind: ShapeKind.Square; sideLength: number; } let c: Circle = { kind: ShapeKind.Square, // ~~~~~~~~~~~~~~~~ エラー! radius: 100 };
また、enum 型はそれ自身がメンバーの union 型となります。union 型についてはまだ説明していませんが、union enums では、enum 自身に存在する値の集まりであること利用するということを知っておいてください。これにより、TypeScript は値を比較するときに起こしがちなささいなバグを防ぐことが可能になります。次の例を見てください。
enum E { Foo, Bar } function f(x: E) { if (x !== E.Foo || x !== E.Bar) { // ~~~~~~~~~~~ // エラー! '!=='オペレーターは'E.Foo'と'E.Bar'には適用できない } }
この例では、x
がE.Foo
ではないかどうかをチェックしています。このチェックがパスすれば、||
はスキップされて、if
文中が実行されます。しかし、前述のチェックがパスしなかった場合、x
はE.Foo
に限定されます。そのため、x
がE.Bar
であるかどうかを確認することは無駄ということになります。
Enums at runtime
enums は実行時にも存在する実オブジェクトです。例えば次の enum は
enum E { X, Y, Z }
実際に次の関数に渡すことができます。
function f(obj: { X: number }) { return obj.X; } // 'E'は数値のプロパティである'X'を持つため動作する f(E);
Reverse mappings
enum はメンバーの名前と同じプロパティを持つオブジェクトを作成することに加えて、enum の値からその名前に対する reverse mapping も生成します。例えば、次のようなコードは
enum Enum { A } let a = Enum.A; let nameOfA = Enum[a]; // "A"
次のような JavaScript にコンパイルされます。
var Enum; (function(Enum) { Enum[(Enum["A"] = 0)] = "A"; })(Enum || (Enum = {})); var a = Enum.A; var nameOfA = Enum[a]; // "A"
生成されたコードからは、enum は順方向(name
-> value
)と逆方向(value
-> name
)の両方のマッピングを持つオブジェクトにコンパイルされることがわかります。他の enum のメンバーへの参照は常にプロパティアクセスとして出力され、インラインされることはありません。
また、文字列 enum のメンバーは reverse mapping を生成しないことに注意してください。
const
enums
ほとんどのケースでは、enums は有効な解決策ですが、より厳格な条件下で使用したいこともあるでしょう。余分に生成されるコードや enum の値へアクセスする間接的な参照のコストを避けるために、const
enums を使用することができます。const enums はconst
修飾子を enums につけて定義します。
const enum Enum { A = 1, B = A * 2 }
const
enums は定数 enum 式にのみ用います。通常の enums とは異なり、コンパイル時に完全に取り除かれます。また、const enums メンバーはインラインされます。このために計算値を持ちません。
const enum Directions { Up, Down, Left, Right } let directions = [ Directions.Up, Directions.Down, Directions.Left, Directions.Right ];
上記コードは、次のようにコンパイルされます。
var directions = [0 /* Up */, 1 /* Down */, 2 /* Left */, 3 /* Right */];
Ambient enums
ambient enums はすでにある enum 型を宣言するために使用します。
declare enum Enum { A = 1, B, C = 2 }
ambient と ambient ではない enums の大きな違いは、通常の enums では前のメンバーが定数であり、かつ、初期値を持たなければそのメンバーも定数とみなされるのに対して、ambient (const ではない) enum メンバーは初期値を持たなければ、常に計算値としてみなされます。
Rails 5 API + devise でパスワードのリセット
Rails 5 API + devise でパスワードのリセット
前回: http://uraway.hatenablog.com/entry/2016/07/11/090206
Rails 5 API + devise の資料があんまりなかったのでメモ。前提として devise を使ったユーザー登録、ログイン機能を持っている API を作成しているとします。
また、 この一連のチュートリアルでは devise_token_auth
は使用しません。
ソースコードは GitHub にあるので、gem のバージョンやフォルダ構成などの参考にしてください。
- rails (5.2.2)
- devise (4.5.0)
mailcatcher の導入
今回はメーラーを使用するので、mailcatcher
を導入します。設定が楽なので Docker を使いますが必須ではないです。
# Dockerfile FROM ruby:2.5 ENV APP_ROOT /usr/src/devise-api RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs # SQLite RUN apt-get install sqlite3 libsqlite3-dev -y RUN mkdir $APP_ROOT WORKDIR $APP_ROOT COPY Gemfile $APP_ROOT/Gemfile COPY Gemfile.lock $APP_ROOT/Gemfile.lock RUN bundle install COPY . $APP_ROOT
# docker-compose.yml version: "3" services: app: build: ./ command: bundle exec rails s -p 3000 -b '0.0.0.0' volumes: - .:/usr/src/devise-api ports: - "3000:3000" tty: true stdin_open: true mailcatcher: image: schickling/mailcatcher ports: - "1080:1080" - "1025:1025"
mailcatcher の設定を書き加えます。Docker を使用している場合はホスト名にコンテナ名を使用することができます。
# config/environments/development.rb config.action_mailer.smtp_settings = { address: 'mailcatcher', port: 1025 }
docker-compose up
して、http://localhost:1080 にアクセスしましょう。
パスワードリセットメールを送信する
devise の recoverable
をオンにします。
# app/models/user.rb class User < ApplicationRecord devise :database_authenticatable, :registerable, :validatable, :recoverable
passwords_controller.rb
を新たに作り、ルートを割り当てます。send_reset_password_instructions
は devise が用意したメソッドで、ユーザーにパスワードリセットメールを送信します。
このとき注意したいのがレスポンスに関してで、リクエストパラメータのメールアドレスを持つユーザーが存在しないとき、その有無のエラーメッセージを返すのではなく、有無にかかわらず同じメッセージを返したほうが良いでしょう。悪意のある送信者に、そのメールアドレスのユーザーが登録されているかを知られることを防ぐことができます。
# app/controllers/passwords_controller.rb module V1 class PasswordsController < ApplicationController skip_before_action :authenticate_user_from_token!, only: [:create] def create user = User.find_by(email: create_params[:email]) user&.send_reset_password_instructions render json: {} end private def create_params params.require(:user).permit(:email) end end end
# config/routes.rb ... resource :passwords, only: [:create] ...
send_reset_password_instructions
で送信されるメールテンプレートは、app/views/devise/mailers/reset_password_instructions.html.erb
にて次のように定義されています。
<p>Hello <%= @resource.email %>!</p> <p> Someone has requested a link to change your password. You can do this through the link below. </p> <p> <%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %> </p> <p>If you didn't request this, please ignore this email.</p> <p> Your password won't change until you access the link above and create a new one. </p>
edit_password_url
を token 付きのフロントエンドのパスワードリセットページ URL に変えます。
<p> <%= link_to 'Change my password', "https://yourfrontend.com?token=#{@token}"%> </p>
mailcatcher を使ってメールが送信されることを確かめてみましょう。メールアドレスが user@example.com
のユーザーが存在するとします。
$ curl -X POST -H "Content-Type: application/json" localhost:3000/v1/passwords --data '{"user": { "email": "user@example.com" } }' {}
http://localhost:1080 にアクセスして、メールの内容を確認してみます。
Change my password をクリックすると、次のようなリセットパスワードトークン付きの URL が取得できます: https://yourfrontend.com/?token=8XAXEzhFxCnn6zidyb9B
トークンを受け取ってパスワードリセットする
リセットパスワードトークンは暗号化されてデータベースに保存されるため、リセットパスワードトークンからユーザーを見つけ出し、パスワードをリセットするには、devise によって追加されたクラスメソッド User.reset_password_by_token
を使用します。このメソッドは reset_password_token
/password
/password_confirmation
を受け取ります。
リセットパスワードトークンと新しいパスワードを受け取り、パスワードを更新するコントローラーを定義しましょう。
# app/controllers/passwords_controller.rb module V1 class PasswordsController < ApplicationController skip_before_action :authenticate_user_from_token!, only: [:create, :update] ... def update user = User.reset_password_by_token(update_params) render json: user, status: :ok, serializer: V1::UserSerializer end private ... def update_params params.require(:user).permit(:password, :password_confirmation, :reset_password_token) end end end
# config/routes.rb ... resource :passwords, only: [:create, :update] ...
パスワードのリセットを試してみましょう。先程送信したメールのクエリにあるトークンと、新しく設定したいパスワードをデータに PUT リクエストを実行します。
$ curl -X PUT -H "Content-Type: application/json" localhost:3000/v1/passwords --data '{"user": {"password": "newpass" ,"reset_password_token": "nysLs8eSLHHZm3eNw2fg"}}' {"email":"user@example.com"}
パスワードが新しく設定できたかどうかも確認してみましょう。
$ curl -X POST -H "Content-Type: application/json" localhost:3000/v1/login --data '{"email": "user@example.com", "password": "newpass"}' {"email":"user@example.com","token_type":"Bearer","user_id":1,"access_token":"1:yyjsJ_4oLN6k6z9_rZxE"}
rspec を書いてみよう
リセットパスワードトークンはその期限が config/initializers/devise.rb
にて定義されており、任意に変更できます。
# Time interval you can reset your password with a reset password key. # Don't put a too small interval or your users won't have the time to # change their passwords. config.reset_password_within = 6.hours
rspec を書いてみることで、トークンの期限が正しいかを確認しましょう。rspec 自体の導入方法については割愛します。
Gemfile
に以下の 2 つの gem を追記して、bundle install
します:
gem 'factory_bot_rails', '~> 4.0' gem 'rspec-rails', '~> 3.8'
TimeHelpers#travel_to
を使って、次のようにリセットパスワードトークンが正しく期限切れになるかどうか確認することができます。
# spec/requests/passwords_spec.rb require 'rails_helper' include ActiveSupport::Testing::TimeHelpers ... describe 'PUT /v1/passwords' do before do @raw, enc = Devise.token_generator.generate(User, :reset_password_token) user.reset_password_token = enc user.reset_password_sent_at = Time.now.utc user.save(validate: false) end context 'when params have a new password and a valid token' do it 'resets password' do travel_to(Time.current + 5.hours) params = { user: { password: 'newpass', password_confirmation: 'newpass', reset_password_token: @raw } } put '/v1/passwords', params: params expect(User.first.valid_password? 'newpass').to eq true end end context 'when params have invalid token' do it 'does not reset password with expired token' do travel_to(Time.current + 7.hours) params = { user: { password: 'newpass', password_confirmation: 'newpass', reset_password_token: @raw } } put '/v1/passwords', params: params expect(User.first.valid_password? 'newpass').to eq false end end ... end
Docker で始める golang 入門
Docker で始める golang 入門
Docker 環境
動作確認済のバージョン:
- Version 2.0.0.0-mac81 (29211)
こちら http://studio-andy.hatenablog.com/entry/go-todo-crud がすごいわかりやすくて良いので、これに沿うような Docker 環境を整えてみる。
まずは何はともあれDockerfile
FROM golang:1.10.2 WORKDIR /go/src/go-todo/ RUN go get -u github.com/golang/dep/cmd/dep \ bitbucket.org/liamstask/goose/cmd/goose
dep
はパッケージマネージャで、goose
はマイグレーションツール。
WORKDIR
は/go/src
以下でないとパスが通らなくなる。
次にdocker-compose.yml
を下記の内容で作成:
version: "3" services: db: image: mysql:8 volumes: - ./docker-runtime/mysql:/var/lib/mysql environment: MYSQL_ROOT_PASSWORD: password MYSQL_DATABASE: sampledb MYSQL_USER: sampleuser MYSQL_PASSWORD: password app: build: . volumes: - .:/go/src/go-todo/ ports: - "8080:8080" tty: true stdin_open: true links: - db
ビルドしてバックグラウンド実行し、初期ロックファイルGopkg.lock
を作成する:
$ docker-compose up -d --build $ docker-compose exec app dep init
volumes
してあるので、ファイルがホストとも共有される。
Hello World
基本となるコントローラーファイルsrc/controller/index.go
を作成:
package controller import ( "net/http" "github.com/gin-gonic/gin" ) func IndexGET(c *gin.Context) { c.String(http.StatusOK, "Hello World") }
上記のコントローラーをルートに割り振る。
main.go
:
package main import ( "go-todo/src/controller" "github.com/gin-gonic/gin" ) func main() { router := gin.Default() router.GET("/", controller.IndexGET) router.Run(":8080") }
docker-compose.yml
のapp
にcommand
を追加:
app: command: go run main.go build: . volumes: - .:/go/src/go-todo/ ports: - "8080:8080" tty: true stdin_open: true links: - db
これでdocker-compose up -d
するとサーバーが立ち上がるので、http://localhost:8080 にアクセスできる :tada:
DBの用意
mysql のコンテナに入ってデータベースを作成:
docker-compose exec db mysql -uroot -ppassword Enter password: mysql> CREATE DATABASE sampledb;
データベース接続のための設定ファイル。ホスト名はdocker-compose.yml
で記述したdb
になることに注意。
db/dbconf.yml
:
development: driver: mymysql open: tcp:db:3306*sampledb/root/password
接続の確認:
$ docker-compose exec app goose status goose: status for environment 'development' Applied At Migration =======================================
次のコマンドで、マイグレーションファイル(db/migrations/timestamp_createTask.sql
)が作成されるので、SQL を追記。
$ docker-compose exec app goose create createTask sql
-- +goose Up -- SQL in section 'Up' is executed when this migration is applied CREATE TABLE IF NOT EXISTS task ( id INT UNSIGNED NOT NULL, created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), title VARCHAR(255) NOT NULL, PRIMARY KEY(id) ); -- +goose Down -- SQL section 'Down' is executed when this migration is rolled back DROP TABLE task;
マイグレーションを実行:
$ docker-compose exec app bash goose up
モデルの接続
$ docker-compose exec app bash /go/src/go-todo # go get -u golang.org/x/tools/cmd/goimports /go/src/go-todo # go get -u github.com/xo/xo
xo ドライバ://ユーザー:パスワード@ホスト/データベース
コマンドで各 model ファイルを作成:
/go/src/go-todo # mkdir -p models /go/src/go-todo # xo mysql://root:password@db/sampledb -o models
models/model.go
ファイルを作成し、接続情報を記述する:
package models import ( "database/sql" "log" // mysql driver _ "github.com/go-sql-driver/mysql" ) // DBConnect returns *sql.DB func DBConnect() (db *sql.DB) { dbconf := "root:password@tcp(db:3306)/sampledb" db, err := sql.Open("mysql", dbconf) if err != nil { log.Fatal(err) } return db }
あとは同じなので省略!
VSCode からはてなブログに投稿できる拡張作りました
VSCodeからはてなブログに投稿できる拡張作りました
作りました
気に入ったらスターください
前に作ったAtom版はこっち
下準備
使用には以下の3つが必要です
Blog IDとAPI Keyははてなブログ詳細設定のページから確認できます。Blog IDはルートエンドポイントに次のように埋め込まれてます。API Keyは公開しないでくださいね
https://blog.hatena.ne.jp/{はてなID}/{ブログID}/atom
プラグインダウンロード後に、Code
> Preferences
> Settings
からこれらを設定します。
エントリの投稿
コマンドパレットからHatenablogger: Post or Update
を選択するとプロンプトが出てくるので、タイトル、カテゴリー(カンマ区切り)、 公開するかどうか(yes
なら公開)を入力すればエントリが投稿され、元のファイルにコンテキストコメントが挿入されます。
エントリの更新
同じく、コマンドパレットからHatenablogger: Post or Update
を選択します。コンテキストコメントの有無で投稿か更新かを判別しています。ただしAPIの仕様なのか、一度公開したエントリを下書きに戻すことはできないみたいです。
イメージのアップロード
コマンドパレットからHatenablogger: Upload Image
を選択して、はてなフォトライフに画像をアップロード、カーソル位置に画像リンクを挿入します。
とりあえず今の段階では3つの機能だけ。予約投稿したい場合は、残念ながらそのためのAPIは用意されていないので、はてなブログ上から設定するしかありません。
VSCodeならtextlint
も使えるのでかなり快適な執筆環境が構築できます。
S3 + CloudFront の CORS 設定
手順をメモっとく
S3 の設定
[Permissions] > [CORS configuration] から以下のように設定を追加:
<CORSConfiguration> <CORSRule> <AllowedOrigin>*</AllowedOrigin> <AllowedMethod>GET</AllowedMethod> </CORSRule> </CORSConfiguration>
CloudFront の設定
[Behavior] > [Cache Based on Selected Request Headers] から Origin ヘッダだけをS3に通す設定を行う:
で、最後に curl で確認して、access-control-allow-origin: *
が返ってきていればよし
$ curl -X GET -I -H "Origin: https://example.com" https://example.cloundfront.net/path/to/image.png HTTP/2 200 content-type: image/png content-length: 43881 date: Wed, 28 Nov 2018 14:24:17 GMT access-control-allow-origin: * access-control-allow-methods: GET last-modified: Mon, 30 Oct 2017 07:22:28 GMT etag: "" accept-ranges: bytes server: AmazonS3 vary: Origin x-cache: Miss from cloudfront
PWAでオフラインでも使える辞書アプリ作ってみた
いつも英語学習にはアルクの英辞郎の辞書アプリ使ってたんだけど、フィリピンのネットスピードでは使いづらいものがあって、オフラインでも使える辞書アプリ探すかなーと思っていたところ
Chrome拡張の高速な英語辞書ツールをつくりました(Mouse Dictionary) - Qiita
という記事の中で、英辞郎の辞書データを購入できることを知ったので、どうせならとPWAで辞書アプリを自作してみた。
https://uraway.github.io/dictionary-app/
Chrome CanaryとiPhoneのSafariでオフライン動作することを確認している
ソースコード
もちろんCRA製
GitHub - uraway/dictionary-app
lighthouseはこんな感じ。フィリピンというかセブではそもそも4Gですらないが…