【ionic2】ログイン機能を実装する
はじめに
今回やりたかったのは、まぁ普通のログイン機能をもつアプリ。実装したい要件としては以下
・未ログインの場合は、ログイン画面を表示。
・ログイン済の場合は、他の画面が見れる。
実装方法としては以下のような形で行ってみた。
・ログイン済みかどうかはローカルストレージに保存して判定する。
・app.component.ts の platform.ready()でローカルストレージをチェック。
・ログイン済みの場合は他の画面を表示
・未ログインの場合はログイン画面を表示。ログインに成功したらローカルストレージに書き込んでindex.htmlをリロードする。リロードすることでplatform.ready()が再度呼ばれるので次はログインと判定されるはず。
実装
src/app/app.component.ts
・・・ import { Storage } from '@ionic/storage'; ・・・ export class MyApp { rootPage:any; constructor(platform: Platform, statusBar: StatusBar, splashScreen: SplashScreen, storage: Storage) { platform.ready().then(() => { statusBar.styleDefault(); splashScreen.hide(); storage.get('auth').then((val) => { if (val == null) { // 未ログインの場合はログイン画面を表示 this.rootPage = 'signin'; } else { // ログイン済みの場合はその他の画面を表示 this.rootPage = 'home'; } }); }); } }
src/pages/signin/signin.ts
import { Storage } from '@ionic/storage'; ・・・ export class SigninPage { ・・・ // ログイン signin() { this.ApiService.get('/signin', {}) .then(data => { // ログインに成功したらログイン情報をローカルストレージに保存して、リロード this.storage.set('auth', { token:data.token }); location.reload(); }) .catch(messages => { alert(messages); }); } /* ログアウトも手抜きここで書いちゃう */ signout() { // ローカルストレージを削除してリロード this.storage.remove('auth'); location.reload(); } }
なんか正当な方法じゃなさそうだけど、自分がやってるアプリだとこんなので十分事足りそう。以上です。
【ionic2】apiリクエストをproviderで共通処理化する
APIのURLを開発と本番で切り替える
こちらのURLのとおりにやったらできた。
http://roblouie.com/article/296/ionic-2-environment-variables-the-best-way/
webpackの設定とかはURLのとおりにやればできる。あとは以下のように各々呼び出せるようになる。
import { Injectable, Inject } from '@angular/core'; import { EnvVariables } from '../app/environment-variables/environment-variables.token'; @Injectable() export class ApiService { constructor(http: Http, @Inject(EnvVariables) public envVariables) { } test() { // this.envVariables.apiEndpoint } }
今回は上の記事に書いてあるそのまんまつかわせてもらった。
src/app/environment-variables/development.ts
export const devVariables = { apiEndpoint: 'http://dev.example.com', }
src/app/environment-variables/production.ts
export const prodVariables = { apiEndpoint: 'http://prod.example.com', }
APIリクエストを共通化する
Angularではproviderとして作るのが一般的ぽいのでそのようにしてみる。
ひな形作成
$ ionic g provider api-service
実行すると src/providers/api-service.ts が生成されるのでここに実装する。
今回は認証用のトークンをローカルストレージに保存しておいて、リクエストのたびにhttpヘッダーにさしこむようにした。
src/providers/api-service.ts
import { Injectable, Inject } from '@angular/core'; import { Http, Headers } from '@angular/http'; import 'rxjs/add/operator/map'; import { Storage } from '@ionic/storage'; import { EnvVariables } from '../app/environment-variables/environment-variables.token'; @Injectable() export class ApiService { constructor(public http: Http, @Inject(EnvVariables) public envVariables, private storage: Storage) { /* コンストラクタ */ } request(method, action, params) { return new Promise((resolve, reject) => { this.storage.get('auth.token') .then(token => { var url = this.envVariables.apiEndpoint + action; var http = null; var headers = { headers: new Headers({ 'Authorization': 'Token ' + token }) }; if (method == 'get') { http = this.http.get(url, headers) } else if (method == 'post') { http = this.http.post(url, data, headers) } if (http !== null) { http.subscribe((response) => { if (response.status == 200) { var result = response.json(); if (result.status == "success") { resolve(result); } else { reject(result.messages); } } else { reject('通信エラーが発生しました'); } },err => { reject(err); }); } }); }); } get(url, params) { return this.request('get', url, params); } post(url, data) { return this.request('post', url, params); } }
これでコンポーネントから呼び出せる
src/pages/hello.js
import { ApiService } from '../../providers/api-service'; ・・・ @Component({ selector: 'page-hello', templateUrl: 'hello.html', providers: [ApiService] }) export class SamplePage { constructor(public navCtrl: NavController, private ApiService: ApiService) { } test() { this.ApiService.get('/users/', {}) .then(data => { // success console.log(data); }) .catch(messages => { // error alert(messages); }); } }
以上です。
【rails】authenticate_with_http_token を使ってapiの認証機能を実装する
はじめに
今回やりたかったことはアプリからのリクエストを受け付けるapiの開発。やりたいことと流れは以下のようなイメージ
・Emailとパスワードでログインを行う
・ログインに成功したらトークンと有効期限を発行してクライアントに返す
・クライアントは毎リクエストトークンをhttpヘッダーに入れてリクエストする
また、今回はrails5のapiモードを使用しました。
テーブル設計とか準備
・users
id | ユーザーid |
---|---|
メール | |
password_digest | パスワード |
token | トークン |
token_expire | トークン有効期限 |
password_digestはrailsのhas_secure_passwordというのを使う場合にこのカラムが必要なようなので追加。has_secure_passwordはパスワード認証の仕組みを簡単に実装できる仕組みみたい。
とりあえずモデルとテーブルを作成する
$ ./bin/rails g model User
migrationファイルが生成されるのでカラムを追記する
db/migrate/20171009182615_create_users.rb
class CreateUsers < ActiveRecord::Migration[5.1] def change create_table :users do |t| t.string :email t.string :password_digest t.string :token t.datetime :token_expire t.timestamps end end end
modelファイルも生成されるのでバリデーションを追加
app/models/user.rb
class User < ApplicationRecord VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i validates :email, presence: true, format: { with: VALID_EMAIL_REGEX } has_secure_password validates :password, presence: true, length: { minimum: 6 } end
has_secure_passwordを使うためにbcryptというのが必要なのでGemfileに追記
Gemfile
gem 'bcrypt', '~> 3.1.7'
ここまでできたら以下を実行
$ ./bin/bundle install $ ./bin/rails db:migrate
実装
あとはプログラムを実装していく。
config/routes.rb
Rails.application.routes.draw do post '/users/signup', to:'users#signup' post '/users/signin', to:'users#signin' end
app/controller/users_controller.rb
# coding: utf-8 class UsersController < ApplicationController ## authenticate_with_http_tokenを使うために include ActionController::HttpAuthentication::Token::ControllerMethods ## signinとsingup以外で認証処理を行う before_action :auth, except: [:signin, :signup] ## サインアップ def signup user = User.new(user_params) if user.save success else failed(user.errors.full_messages) end end ## サインイン def signin ## 認証に成功したらランダム文字列でトークンを生成して有効期限1日をセット user = User.find_by(email: params[:email]) if user && user.authenticate(params[:password]) user.token = create_token user.token_expire = DateTime.now + 1 if user.save!(:validate => false) success({ token: user.token }) else error end else error('email または passwordが正しくありません') end end ## レスポンス返却用の簡易メソッド def success(data = nil) render json:{ status: 'success', result: data } end def error(messages = nil) render json:{ status: 'failed', messages: messages } end ## 以下 authenticate_with_http_token を使った処理 def auth ## 認証に失敗したらエラーレスポンスを返す authenticate_token || render_unauthorized end ## httpヘッダーに入っているtokenでdbを検索して有効期限のチェック ## authenticate_with_http_tokenの書き方でこのように書くみたい def authenticate_token authenticate_with_http_token do |token, options| user = User.find_by(token: token) user && DateTime.now <= user.token_expire end end def render_unauthorized render json:{ result:'failed' } end ## ランダム文字列を作る def create_token return ((0..9).to_a + ("a".."z").to_a + ("A".."Z").to_a).sample(50).join end def user_params params.permit(:email, :password) end end
確認はこんな感じで
$ curl -H 'Authorization: Token xxxxxxxxxxxxxxxxxxxxxxxxxx' http://localhost:3000/users
今回はトークンをDBに入れているけど、少し規模の大きいサービスだったらredisとかにもたせるのがよいのかな。
以上です。
rails console で group by の結果を確認
やりかったことはemailが重複しているレコードの件数をしらべたかっただけ。
実行されているSQLが期待通りだったのでこれでいいのかと思ってたけど、全然期待した結果にならない。。
$ User.select('email,count(*)').group('email') > User.select('email,count(*)').group('email') User Load (0.3ms) SELECT email,count(*) FROM "users" GROUP BY "users"."email" [#<User:0x007fc90f100ce0 id: nil, email: "test@test.com">, #<User:0x007fc90f0f80b8 id: nil, email: "test2@test.com">, ・・・
結果、こうするらしい
$ User.group(:email).count > User.group(:email).count (0.3ms) SELECT COUNT(*) AS count_all, "users"."email" AS users_email FROM "users" GROUP BY "users"."email" {"test@test.com"=>1, "test2@test.com"=>1, ・・・
慣れかな。以上です
【ionic2】ionicPageで各画面をurlに対応させる
はじめに
ionic1のルーティングまわりはui-routerで検索すれば色々出て来たけどionic2で何で検索すればよいのかまだよくわかってないけどとりあえずやり方はわかったのでメモしておく。デバッグのときとかはブラウザでリロードしたいし。あれ、けど目的はそれくらい。。?
とりあえずHome → Page1 → Page2 みたいな感じで画面遷移できるように。
それぞれ /#/home → /#/page1 → /#/page2 とURLもつようしてみる。
app/app.component.ts
・・・ import { HomePage } from '../pages/home/home'; @Component({ templateUrl: 'app.html' }) export class MyApp { //rootPage:any = HomePage; rootPage:any = 'home'; // ★ ココをあとでionicPageで指定する名前に変更 ・・・ }
app/app.module.ts
・・・ @NgModule({ declarations: [ MyApp //HomePage ], ・・・ entryComponents: [ MyApp //HomePage ], ・・・
以降、各ページ
pages/home/home.module.ts
import { NgModule } from '@angular/core'; import { IonicPageModule } from 'ionic-angular'; import { HomePage } from '../home/home'; @NgModule({ declarations: [ HomePage ], imports: [ IonicPageModule.forChild(HomePage) ], entryComponents: [ HomePage ] }) export class HomePageModule {}
pages/home/home.ts
import { Component } from '@angular/core'; import { NavController, IonicPage } from 'ionic-angular'; import { Page1Page } from '../page1/page1'; @IonicPage({ name: 'home' }) @Component({ selector: 'page-home', templateUrl: 'home.html' }) export class HomePage { constructor(public navCtrl: NavController) { } goPage1() { this.navCtrl.push('page1'); } }
page1、page2も同じように作ればNavControllerにpushするとurlも書き換わることが確認できた。以上です。
【ionic2】画面遷移のアニメーションを変更する
やりたかったこととしては画面の遷移の際のアニメーションでandroidでもiosみたいに左から右にスライドするようにしたいとういこと。
本来はプラットフォームごとに奨励されているアニメーションを使うべきだとは思うのだがやっぱりiosをベースで考えている人だとandroidでもそのように実装してほしいというのはよくあるのかと。
アプリ全体で設定する場合
app/app.module.ts
import { BrowserModule } from '@angular/platform-browser'; import { ErrorHandler, NgModule } from '@angular/core'; ・・・ @NgModule({ declarations: [ MyApp, HomePage, SignupPage ], imports: [ BrowserModule, // ★ ここで設定 IonicModule.forRoot(MyApp, { pageTransition: 'ios-transition' }) ], ・・・ }) export class AppModule {}
画面ごとに設定したい場合
各pagesのコンポーネント
this.navCtrl.push(MyPage, null, { animation: 'ios-transition' });
app.module.ts で設定するやり方は他にも色々設定できるみたいなので覚えた方がよさそう。
http://ionicframework.com/docs/api/config/Config/
以上です
【ionic2】無限スクロールの使い方
なかなか手をだせにずにいたionic2を次の案件では使えるように準備中。とりあえずionic1でできてたことをionic2でもできるようになることが当面の目標。今回はapiから取得したjsonからリスト表示で、無限スクロールを実装してみる。
blankでプロジェクト作成
$ ionic start test blank
まずはhttpをつかえるように。
app/app.module.ts
import { BrowserModule } from '@angular/platform-browser'; ・・・ import { HttpModule } from '@angular/http'; ## ★ これを追加 @NgModule({ ・・・ imports: [ BrowserModule, IonicModule.forRoot(MyApp), HttpModule ## ★ これを追加 ], ・・・ }) export class AppModule {}
つづいてコントローラ?を実装
src/pages/home/home.ts
import { Component } from '@angular/core'; import { Http } from '@angular/http'; import 'rxjs/add/operator/map'; @Component({ selector: 'page-home', templateUrl: 'home.html' c}) export class HomePage { page = 1; items = []; completed = false; constructor(public http: Http) { this.getUsers().subscribe(data => { if (0 < data.users.length) { this.items = this.items.concat(data.users); this.page++; } else { this.completed = true; } }); } doInfinite(infiniteScroll) { this.getUsers().subscribe(data => { if (0 < data.users.length) { this.items = this.items.concat(data.users); this.page++; } else { this.completed = true; } infiniteScroll.complete(); }); } getUsers() { return this.http .get('http://localhost:3000/users?page=' + this.page) .map(res => res.json()); } }
さいごにテンプレートを実装
src/pages/home/home.html
・・・ <ion-content padding> <ion-list> <ion-item *ngFor="let item of items"> {{ item.name }} </ion-item> </ion-list> <ion-infinite-scroll (ionInfinite)="doInfinite($event)" *ngIf="completed"> <ion-infinite-scroll-content></ion-infinite-scroll-content> </ion-infinite-scroll> </ion-content>
typescriptとかrxjsとかまだ全然わかってないけどとりあえずこれでできた。以上です。