Laravel 認証カスタマイズ 複数tableを結合しての認証で Auth::user() に必要な値を入れる方法
Laravelの認証機能をカスタマイズして、認証時に3つのカラム条件で認証をし、さらに認証後にAuth::user() ファサードに複数tableからの値を取得することをしました。
環境Laravel5.6
windows10Pro 64bit
vagrant環境にて実施
そもそもの経緯として、DB構造が特殊になり、 通常の users tableのみでの認証では必要な値を取得できなくなっていました。
全体のDB設計をしてからやれば済む問題だったのですが、単数のtableでの認証を前提として、システムを作り込んでしまい。後からどうしても複数tableからの値を取得する必要に迫られて、Laravelの認証系のドキュメントやサイトをかなり読込むことになりました。
結果を先に書いておくと 複数tableをjoinしたMySQLのviewを migration で書いて、そのviewを認証用table,auth_users
として登録し、viewから一意の値を導きます。
通常はusers tableから、 email, password と2種類のカラムから一意、かつパスワードの一致で認証をしますが、今回は unique_name, password, community_id の3つのカラム条件から認証条件を導き出しています。
table構造図
(パワポで書いたのでちょっと変ですが意図は伝わるかと思います。)
user は複数のコミュニティに任意に所属・登録できるというシステムで、これを中間tableである community_user でどのユーザーがどのコミュニティに所属しているかを管理しています。
さらに community_user tableにはと1対1(hasOne)の関係で追加table communities_users_statuses があり、特定のユーザーの特定のコミュニティ内での情報(権限レベルのidや表示設定、日時等)が保存されています。さらにその先には roles(権限)tableがあり、idと権限名が記載されているという、かなりがっつりなtable設計をした構造になってます。
これら
users
community_user
communities_users_statuses
roles
上記4つのtabelのからのカラムデータを、Laravelのファサード Auth::user()->name
といった具合に、 Auth::user()->role
も Auth::user()->user_id
も Auth::user()->community_id
も "->"を一つ書くだけで一気に取得できる様にして、その値をアプリケーションの様々な処理のトリガーとして利用したいのです。(というかそういう風に作ってしまって後から変えるの凄いしんどい。)
まず、通常のLaravelの標準の認証系の追加をさくっとします。この辺は他のサイトにお任せします。
で、色々ファイルが作られますので、それをどう変えるか、というお話です。
まず、MySQLのviewを auth_users
という名前でマイグレーションファイルで作成します。通常のマイグレーションファイルの記述方法とは少し異なります。
use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; // 認証時に使用される view Auth::user() に収納される // $credentials array ('id', 'unique_name', 'password' ) class CreateViewAuthUsersTable20181114 extends Migration { /** * Run the migrations. * * @return void */ public function up() { DB::statement( 'DROP VIEW IF EXISTS auth_users' ); DB::statement( " CREATE VIEW auth_users AS SELECT community_user.id, community_user.user_id, community_user.community_id, users.name, users.unique_name, users.email, users.facebook_id, users.password, users.remember_token, communities.user_id AS reader_id, communities.name AS community_unique_name, communities.service_name, communities_users_statuses.role_id, communities_users_statuses.hide, communities_users_statuses.last_access, communities_users_statuses.created_at, communities_users_statuses.updated_at, roles.role FROM community_user JOIN users ON (community_user.user_id = users.id) JOIN communities ON (community_user.community_id = communities.id) JOIN communities_users_statuses ON (community_user.id = communities_users_statuses.id) JOIN roles ON (roles.id = communities_users_statuses.role_id) " ); } /** * Reverse the migrations. * * @return void */ public function down() { DB::statement( 'DROP VIEW IF EXISTS auth_users' ); } }
がっつりとview作成のSQL文を書いています。
DB::statement は通常のSQL文を書くという事ですね。 宣言の部分に use DB; は書かなくても大丈夫でした。書いたら逆にエラーになった。
で、以下のコマンドを実施
php artisan migrate
でSQL文を走らせて view,auth_users
を作ります。
この方法はこちらのサイトを参考にさせてもらいました。
LaravelのマイグレーションでView Tableを作成する
これにより先ほど説明した4つのtableが結合されたviewが作成されます。
viewは一つの大きなtableの様に扱う事ができます。出来上がったviewには所により同じ値が2度以上でますが、 unique_name password community_id の3つの値が全て重複するレコードは存在せず、これにより一意の認証が可能となります。
次にこのviewを認証用のtableとして使うための認証用モデルを作ります。
これは、標準で認証に使われているファイル app\User.php をコピーして書き換えました。
app\AuthUser.php
namespace App; use Illuminate\Notifications\Notifiable; use Illuminate\Foundation\Auth\User as Authenticatable; class AuthUser extends Authenticatable { use Notifiable; /** * モデルと関連しているテーブル * * @var string */ protected $table = 'auth_users'; /** * The attributes that should be hidden for arrays. * JSON出力の際誤って含めいない属性という事らしい * @var array */ protected $hidden = [ 'password', 'remember_token', ]; // 日時表記変更の ->format('Y-m-d') を使いたいカラム名を指定する protected $dates = [ 'last_access', 'created_at', 'updated_at', ]; }
これが結構忘れがちですが大事です!
次に上記の app\AuthUser.php を認証時に使うように config ファイルの model を書き換えます。
config/auth.php (抜粋)
'providers' => [ 'users' => [ 'driver' => 'eloquent', 'model' => App\AuthUser::class, ],
また、login画面のviewファイルにはコミュニティ毎にログイン画面のURLが異なる仕様となっています。
ここにformの hidden で表示されているログイン画面の community_id の値をPOSTできる様にしています。また、標準のemailの入力欄であるフォームを ユーザーID(unique_name)の入力欄に変更しています。
resources\views\auth\login.blade.php
省略 <form method="POST" action="{{ route('login') }}" aria-label="{{ __('Login') }}"> @csrf <input type="hidden" name="community_id" value="{{$community->id}}"> 省略 <input id="unique_name" type="text" class="form-control{{ $errors->has('unique_name') ? ' is-invalid' : '' }}" name="unique_name" value="{{ old('unique_name') }}" required autofocus>
最後?に LoginController を書き換えます。
app\Http\Controllers\Auth\LoginController.php (抜粋)
// 前後省略 use DB; use Illuminate\Support\Facades\Auth; //Class宣言部分 省略 // ログイン時に使用するユニークであるカラムを指定 public function username() { return 'id'; } public function authenticate(Request $request) { $request->validate([ 'unique_name' => ['required', 'string', 'min:6', 'max:40', 'regex:/^[a-zA-Z0-9@_\-.]{6,40}$/u'], 'password' => 'required|string|min:6|max:100', ]); // 該当の community_user の id を取得 $community_user_id = DB::table('community_user') ->leftJoin('users', 'users.id', '=', 'community_user.user_id') ->where([ ['unique_name', $request->unique_name], ['community_id', $request->community_id], ])->pluck('community_user.id')->first(); if (!$community_user_id) { // 他のコミュニティで認証が取れるか? $result_bool = $this->CheckOtherCommunityExists($request->unique_name, $request->password); if ($result_bool) { // 他のコミュニティにいる場合中間table等にレコード追加 $community_user_id = $this->InsertNewStatuses($request->community_id, $request->unique_name); } else { // 他のコミュニティにいない場合 return redirect()->back()->withErrors(array('unique_name' => 'ユーザーIDかPasswordが正しくありません'))->withInput(); } } // community_user_id を含めた通常の承認フロー $credentials = array( 'unique_name' => $request->unique_name, 'password' => $request->password, 'id' => $community_user_id, ); // 認証許可 if (Auth::attempt($credentials)) { // session にcommunity値保存 $request->session()->put('community_id', $request->community_id); $request->session()->put('community_user_id', $community_user_id); return redirect('/')->with('message', 'ログインしました'); } else { return redirect()->back()->withErrors(array('unique_name' => 'ユーザーIDかPasswordが正しくありません'))->withInput(); } }
通常は$credentials の配列に email と password のみを指定して Auth::attempt($credentials) と渡してやれば認証を結果を返してくれますが、auth_users
から一意の値として取得できる条件として id を配列に追加してやります。
$credentials = array( 'unique_name' => $request->unique_name, 'password' => $request->password, 'id' => $community_user_id, );
こうすることにより、 MySQLのviewで作られた auth_users table を探しに行き、一意の値を取得して認証が行われるという訳です。
ちなみにコードの中ほどの独自メソッド群は、$community_user_id を特定する処理として、認証前に値を探しに行ったり、ユーザーがまだログインしたことのないコミュニティに初ログインした際に既存ユーザーであるか?の確認をしたり、community_user にtableに値を追加したり、と、このWebアプリケーション独自の処理を色々してます。
こっからは蛇足ですが、その処理の際に、認証を全て自分でカスタマイズできそうなヒントになりそうな処理を書いたので紹介。
// return bool 他のコミュニティに存在するかを判定する public function CheckOtherCommunityExists($unique_name, $password) { $hash_password = DB::table('users')->where([ ['unique_name', $unique_name], ])->pluck('password')->first(); if ($hash_password) { if (Hash::check($password, $hash_password)) { return true; } } return false; }
上記の自作メソッドは 認証時の id と password を使って、クイックスタートで作られる認証系の処理を通さないで ユーザー認証の判定ができています。(unique_name とpassword)この処理を書いていたら図らずも結構普通な感じの認証処理になってました。
なので、単純に自分で認証処理を作りたい際は上記を参考に返り値となるboolの判定を基に
Auth::login($user);
とやってしまえば、ログイン認証っぽい事はできるようです。 $user
はAuth::user()
に入る一意のユーザーのオブジェクト(DBのrecord)ですね。ただ、tokenに値を入れたり、リダイレクト先を指定したりの処理がどこまで必要になるかは未検証です。
Laravel 5.6 認証 イントロダクション
https://readouble.com/laravel/5.6/ja/authentication.html
【輪読会資料】基礎から学ぶVue.js CHAPTER7 より大規模なアプリ開発 読書メモ
以下の記事は2019/3/28 コワーキングスペース秋葉原Weeybleで行われる輪読会
[秋葉原] 基礎から学ぶVue.js輪読会 ch7 より大規模なアプリ開発(初心者歓迎!)のための読書メモとなります。
以下の書籍の CHAPTER7 より大規模なアプリ開発 のメモです。
- 作者: mio
- 出版社/メーカー: シーアンドアール研究所
- 発売日: 2018/05/29
- メディア: 単行本(ソフトカバー)
- この商品を含むブログを見る
CHAPTER 7 より大規模なアプリケーション開発
SECTION 33 アプリケーションを拡張しよう
Vue.js本体は画面の描画に関わる機能しか備えていないが、以下の様なエコシステムを導入すると機能を拡張できる。
Vuex
複数のコンポーネントでデータを共有したり、アプリケーション全体の状態を一元管理する状態管理用のライブラリ
Vue Router
コンポーネントで構造化された複数の画面をURLと紐づけるSPA(Single Page Application)構築の為のルーティング用のライブラリ
Vuex と Vue Routerは機能として独立しており、状況に応じて片方だけでも構築が可能。 それぞれの特徴はCHAP8,CHAP9で解説している。
ここでは単一ファイルコンポーネントを使ったコンポーネント構築とVueCLIを使ったビルド環境の導入を前提としての説明をする。
SECTION 34 Vue CLIとは
Vue CLIとはVue.jsを使ったコマンドラインインターフェース、Vue.jsの開発環境構築が簡単にできる
ソースコードを分けて管理
分割して作ったファイルをコンパイル時等に結合して一つのファイル化させる方法
モジュール化でさらに快適に!
モジュール化で作られたファイルは多くの機能から必要なものだけピックアップして組み込める。
Vue.jsのモジュール化は「単一ファイルコンポーネント」を採用している。
単純に結合しても動くわけではなく、補完する機能として代表的なものに「webpack」といったバンドルツールがある。
webpack以外にも多くのツールの複雑な設定を自動管理してくれるのが「Vue CLI」となる
webpackとは
モジュール化した複数のファイルをまとめるバンドルツール、他にも[Rollup]や[Parcel]といった別種が存在する。詳細は公式サイトを参照の事 webpack 公式サイト
モジュール化の際は、必要なリソースだけをバンドルするようになっているので、自動的に余計なものが付かないで最小単位で出力されるようになっている。
SECTION 35 単一ファイルコンポーネントとは
「単一ファイルコンポーネント」はコンポーネントの定義方法の一つでSFC(Single File Components)
とも言われる
一つのファイルで管理しきれないHTML,JavaScript,CSSを[.vue]という拡張子のファイルにまとめて管理する。
単一ファイルコンポーネントの例
Example.vue
<template> <div class="example"> <span class="title">{{ text }}</span> </div> </template> <script> export default { name: 'Example', data() { return { text: 'example' } } } </script> <!-- scoped CSS --> <style scoped> .title { color: #ffbb00; } </style>
こいつをビルド環境でプレーンなJavaScriptにプリコンパイルする
スコープ付きCSS(Scoped CSS)
<style>
タグでscoped
オプションを付けるとスコープ付きCSSを利用できる。(<style scoped>
)このテンプレート内でしかスタイルが適用されないようにできる。
出力される際には以下の様なユニーク属性で管理される
<div class="example" data-v-xxxxx> <span class="title" data-v-xxxxx> hoge </span> </div>
span.title[data-v-xxxxx] { color: #ff0000; }
これでクラス名の衝突を気にしなくて良くなる。なお、全くクラスの指定がないとパフォーマンスが落ちるので classでスタイル定義をするのが推奨されている。
子コンポーネントの扱い
テンプレート内に子コンポーネントが存在する場合、子のルート要素とスロットの要素は親と子の両方のスコープを持つ
<div class="example"> <child-comp/> </div>
親と子両方に<style scoped>
をを指定した際は以下の様に展開される
<div class="example" data-v-aaaaa> <div data-v-aaaaa data-v-bbbbb><!-- このルート要素 --> <span data-v-bbbbb>child-comp</span> </div> </div>
この様にルート要素のスタイルはどちらのファイルにも書くことができるので、柔軟にレイアウト変更が可能となる。
スコープをまたぐ設定
お互いにスコープの付いたコンポーネントから子のセレクタ .b
を指定したい例
cssの場合
<style scoped> .a >>> .b {color: #ff0000; } </style>
scssの場合
<style lang="scss" scoped> .a /deep/ .b {color: #ff0000; } </style>
外部ファイルの読み込み
<template src="./template.html"></template> <style src="./script.js"></style> <style src="./style1.css"></style> <style src="./style2.css" lang="scss" scoped></style>
他のマークアップ言語やスタイルシート言語の仕様
cssやscssの他に、[Pug],[Sass]を使用等をすることができる。Vue CLIで作成したプロジェクトなら、パッケージを以下の様にインストールしてから…
npm install pug pug-loader --save-dev
以下の様に書くだけで使用可能
<template lang="pug> div#example span {{ text }} </template>
SECTION 36 ES2015モジュールの書き方
ES2015モジュールは .js でファイルを作成するが、モジュールファイルは独自のスコープを持ち、他のファイルとは切り離される、変数の衝突や他の機能への干渉を気にしなくて良くなる反面、モジュール内で定義したデータや関数は他のファイルから直接アクセスできなくなる。
モジュールを定義する
Example.js
// stateオブジェクトはこのモジュール内でしか使用できない var state = { count :1 }
モジュール内のデータや関数を別のファイルに渡すにはエクスポート&インポートが必要
次の例はstate
オブジェクトをエクスポートして、外部でインポートできる様にした例
Example.js
var state = { count :1 } // デフォルトのインポート文で呼ばれた時に返すデータ export default state
モジュールを使用する
前述のExample.js
のstate
オブジェクトをExample
という変数名でインポートした例
main.js
import Example from './Example.js' console.log(Example.count) // => 1と出力
このような書き方は Vuex, Vue Router ファイルを書く際に必要なので覚えておくこと!
SECTION 37 Node.jsの導入
node.js
はJavaScriptの実行環境、サーバーサイドでJSを動かす際に必須
npm
はJSのパッケージ管理ツール、node.jsに一体化して付いてくる。 PHPでいう所のcomposer的な奴
以下からインストール
node.js 日本語公式サイト
windows10の場合はそのまま画面の指示に従いインストール
コマンドプロンプトで以下のコマンドでバージョンを出してインストールされたか確認する。
今回は推奨版10.15.3 LTS をインストールした
node -v v10.15.3 npm -v 6.4.1
Babelとは
ECMAScript準拠およびJSXのトランスパイラ、Vue CLIはデフォルトでBabelをインストールしてトランスパイルを行う。
トランスパイル(transpile)
一言で言えば、ある言語で書かれたコードを元に別の言語のコードを生成すること。
BabelはES2015のコードを古いES5のコードに変換してくれる
ES2015のコード | トランスパイル | ES5のコード |
---|---|---|
const count = 1 | => | var count = 1 |
Babelの目的やメリットは最新の記法を使って書ける様にすることにある
SECTION 38 Vue CLIの導入
Vue CLIをインストール
npm install -g vue-cli
バージョンを確認
vue --version 2.9.6
プロジェクト作成コマンドのひな形
vue init [テンプレート名] [プロジェクト名] cd [プロジェクト名] npm install
テンプレートは[webpack], [webpack-simple]が良く使われる,他にも[PWA],[Electron]等の様々なアプリケーション用のテンプレートが公開されている。
ここでは[webpack]を使って説明をして行く
新しいプロジェクトの作成
アプリを入れたい適当なフォルダに移動(cd)してから次のコマンドを実行
(Git bashで操作)
作ったフォルダ自体が localhost:8080 の実行環境になってそのまま開発環境のsever化してくれるので、本当にフォルダの場所とか適当で良い様です。
# htdocsに移動 $ cd /c/xampp/htdocs # プロジェクトの作成 vue init webpack vue-my-app $ vue init webpack vue-my-app # 質問が出るので色々選択しながら進む、今回は以下の設定で進んだ。 ? Project name (vue-my-app) ? Project name vue-my-app ? Project description (A Vue.js project) ? Project description A Vue.js project ? Author (Taro Yamada <yamada@gmail.com>) ? Author Taro Yamada <yamada@gmail.com> ? Vue build runtime ? Install vue-router? (Y/n) n ? Install vue-router? No ? Use ESLint to lint your code? (Y/n) n ? Use ESLint to lint your code? No ? Set up unit tests (Y/n) n ? Set up unit tests No ? Setup e2e tests with Nightwatch? (Y/n) ? Setup e2e tests with Nightwatch? Yes ? Should we run `npm install` for you after the project has been created? (recom ? Should we run `npm install` for you after the project has been created? (recom mended) npm
すると htdocs
フォルダ内に vue-my-app
フォルダが作られ、中に色々ファイルが作られる
$ ls -a vue-my-app/ ./ .babelrc .gitignore build/ index.html package.json README.md static/ ../ .editorconfig .postcssrc.js config/ node_modules/ package-lock.json src/ test/
フォルダとファイルの構成
[webpack]を使用した際、いろんなファイルやフォルダが作られるが、基本的に[src]フォルダ以下の物しか使わない
単一ファイルコンポーネントの[.vue]ファイルは慣例的に src/components
にまとめる
src/main.js でコンストラクタの呼び出しが行われている
/* eslint-disable no-new */ new Vue({ el: '#app', render: h => h(App) })
開発サーバーを起動する
vue-my-app
フォルダ内に cd してから以下のコマンドを入れてサーバーを起動する
npm run dev
ブラウザでhttp://localhost:8080/
にアクセスしてVue.jsのロゴとWelcome to Your Vue.js App のメッセージが出力されればOK!
サーバーを起動させたコンソールは開きっぱなしにしておくこと
ホットリロード
ホットリロードはブラウザリロード不要でファイル変更の際に更新してくれる機能
vue-my-app/src/components/HelloWorld.vue
ファイル内のmsg: 'Welcome to Your Vue.js App'
を編集して上書きすると即座ぶブラウザが更新されるのが確認できる。超便利!
プロジェクトをビルドする
実際に運用モードとしてビルドするには以下のコマンドを入れる
npm run build
数分かかる。ビルドが終わると[dist]フォルダが作成される。
この[dist]フォルダだけを本番サーバーにアップロードすれば、ローカルからのビルドとデプロイが完了する。
ちなみに以下のサポートページで、静的サイト向けの高機能なホスティングサービスNetlify
の自動デプロイの方法についての記述がある。
Vue.js+Netlifyで自動デプロイ
開発時のAPIのパス統一やクロスドメイン対策
本番環境と開発環境でAPIのパスが異なる場合や、別ドメインのAPIからデータを取得する際はプロキシ設定が必要となる。config/index.js
に以下の様に記述することで切り替えができる
before config/index.js
proxyTable: {},
after config/index.js
proxyTable: { '/api' : { taget: 'http://localhost:8081', changeOrigin: true, pathRewrite: { '^api'; '/vue-my-app/api' } } },
例えばAPIで /api/user
を叩くと 異なるドメインから user のAPIを取得できるように実装がされているとする。
プロキシを使うと以下へのアクセスは
http://localhost:8080/api/user
以下の様に変更できる
http://localhost:8081/vue-my-app/api/user
より詳細はwebpackのドキュメントを参照のこと
SECTION 39 Vue.jsプラグイン
CHAPTER1で紹介されたElement,Awesome Vue,VueCurated等のUIコンポーネントや、Vuex,Vue RouterはVue.jsプラグインとして作成されている。
ここではVue.jsプラグインの使い方と自作プラグインの作成方法の紹介となる。
Vue.jsプラグインの使い方
コンストラクタを使用する前にグローバルメソッドのVue.use
を使用してVue.jsに登録する
src/main.js の上の行に書く?
// VueとVuexのモジュールを読み込む import Vue from 'vue' import Vuex from 'vuex' // VueにVuexを登録 Vue.use(Vuex)
スタンドアロン版でプラグインを使用する場合
Vuexの様な公式提供の奴はVue.use
を使わずとも良い。
Vue.js本体を読み込んだ後にプラグインのファイルを読み込む
公開されているリソースはプラグインだけじゃない
コンポーネントやカスタムディレクティブとして登録するのものある
プラグインを自作してみる
入門向けではないが一応説明、大規模アプリの開発リソースの再利用の際にプラグイン作成できると便利だそうな。
具体的にどのファイルのどこに書けばいいのか分らずなのだが、それはここまで読んだ内容を理解してないせいか、後の章で書かれるかだと思います。
スクロール数値を共有するプラグイン
var windowPlugin = { install: function(Vue) { // プラグインデータ用にVueインスタンスを利用する var store = new Vue({ data: { scrollY: 0 } }) // ウィンドウのスクロールイベントをハンドル var timer = null window.addEventListener('scroll', function() { if (timer === null) { timer = setTimeout(function() { // 200ms間隔でscrollYプロパティに代入 store.scrollY = window.scrollY clearTimeout(timer) timer = null }, 200) } }) // インスタンスプロパティに登録 Vue.prototype.$window = store.$data } }
プラグインを登録
Vue.use(windowPlugin)
すべての Vue インスタンスで使用可能
Vue.component('my-component', { template: '<div>{{ scrollY }}</div>', computed: { scrollY: function() { return this.$window.scrollY } } })
SECTION 40 ES2015で書いてみよう
新しいJavaScriptの書き方、ES2015(ES6)は対応してないブラウザがあるが、既に説明した通り、Babelが古いJS形式にトランスパイルしてくれるので、Vue CLI
ではES2015形式で遠慮せずに書ける。ここではES2015の書き方の説明となる。
正直ちゃんと追って無かったので、この知識はありがたい。
変数宣言
ES5
var x = 0
ES2015
// 再代入可能 let x = 0 // 定数 再代入不可 const x = 0
ES2015はスコープで仕切られる
ES5
{ var x = 1 } console.lod(x) // 1
ES2015
{ let x = 1 } console.lod(x) // not defined
constで宣言した配列は空配列を再代入できない。か、 length
を0にすると配列を空にできる
const array = [1, 2] array.push(3) console.log(array) // -> (3) [1, 2, 3] array.length = 0 console.log(array) // -> []
関数とメソッドの書き方
functionの省略
ES5
new Vue({ methods: { handleClick: function() { ... } } })
function は書かなくても良い
ES2015
new Vue({ methods: { handleClick() { ... } } })
アロー関数
functionを省略して =>
で書ける関数thisの扱いが従来の関数と異なり、アロー関数式で宣言された関数は、宣言された時点で、thisを確定(=束縛)させてしまう。
その為メソッド内のthis
が必要な場面では使用できないが無名関数の定義で便利とのこと
【JavaScript】アロー関数式を学ぶついでにthisも復習する話
let normalFunc = function(x){ console.log(x); } normalFunc('今までの関数'); let arrowFunc = (y) => { console.log(y); } arrowFunc('アロー関数式');
STEP1
const newArray = array.map(el => { return el * 2 })
STEP2 returnを省略
const newArray = array.map(el => el * 2)
慣れの問題だが、ここまで省略されると認識し辛いなぁ
STEP3 複数の引数
const newArray = array.map((el, index) => el * 2)
STEP4 オブジェクトの return
const newArray = array.map(el => ({ value: el * 2 }))
簡単な奴から使って慣れると良いよ。と作者もおっしゃってます。
テンプレートリテラル
クォーテーションで囲んでバックスラッシュで改行
ES5
var name = 'hoge' var template = '\ <div class="template">\ <strong>' + this.name + '</strong>\ </div>' console.log(template)
バッククォートで囲むだけで改行含めて受け付けてくれる
ES2015
const name = 'hoge' const template = ` <div class="template"> <strong>${ name }</strong> </div>` console.log(template)
オブジェクトプロパティのショートハンド
ES2015では変数名とプロパティが同じなら省略して書ける
ES5
const newObject = {a: a, b: b}
ES2015
const newObject = {a, b}
分割代入
べんりー
ES2015
// 配列要素1,2をそれぞれ変数a,bに代入 const [a, b] = [1, 2] console.log(a) // -> 1 // nameプロパティだけ代入 const { name } = { id: 1, name: 'りんご' } console.log(name) // -> りんご
引数のオブジェクトを分割して受け取ることも可能
ES2015
function myFunction({ id, name }) { console.log(name) // -> りんご } myFunction({ id: 1, name: 'りんご' })
v-for
ディレクティブでも使える
<ul> <li v-for="{ id, name } in list" :key="id">...</li> </ul>
スプレッド演算子
配列、オブジェクトのリテラルを展開する ...hoge
ES2015
const array = [1, 2, 3] // バラバラの3つの引数として渡す myFunction(...array) // arrayを展開して4を加えた新しい一次配列を作成 const newArray = [...array, 4] // -> (4) [1, 2, 3, 4]
配列メソッド
findメソッド
条件に一致した最初の要素の値を1つ返す
(そういえばこういう処理、スゲー大変だった気がする)
ES2015
const array = [ { id: 1, name: 'りんご' }, { id: 2, name: 'ばなな' } ] const result = array.find(el => el.id === 2) console.log(result) // -> { id: 2, name: 'ばなな' }
見つからない場合は undefind が返る
findindex
条件に一致した最初の要素のインデックスを1つ返す
const array = [ { id: 1, name: 'りんご' }, { id: 2, name: 'ばなな' } ] const result = array.findIndex(el => el.id === 2) console.log(result) // -> 1 (indexは0始まりなので2番目は1)
Promise
非同期処理を抽象化したオブジェクト。(もうajaxとかじゃない!)
Promiseで非同期処理の完了を知ることができる。
引数としてresolve
(解決した)と、reject
(拒否した)のコールバック関数を受け取る。
( $.ajaxの .done .fail みたいな感じか )
成功を知る例
function myFunction() { return new Promise((resolve, reject) => { setTimeout(() => { // 成功したことを通知 resolve('success!') }, 1000) }) } // 1秒後にmyFunctionが終わった知らせを受けてthenの処理が行われる myFunction().then(value => { console.log(value) // -> success! })
拒否(失敗)した事を知る例、かつ成功、失敗の両方の処理を書いた例
function myFunction(num) { return new Promise((resolve, reject) => { if (num < 10) { resolve('success!') } else { reject('error!') } }) } myFunction(100).catch(value => { console.log(value) // -> error! })
成功、拒否、どちらでも処理する際はfinally
を使う
myFunction().then().catch().finally(() => { // 成功でも失敗でも行われる })
Gitリポジトリからcloneして動かす際
プロジェクトフォルダをそのままリモートリポジトリに上げる。 別の環境でnode.jsをインストールして任意のフォルダで以下の操作をするだけでOK
git clone **url** npm install npm run dev
これだけで動く
□まとめ
- Vue CLI はVue.jsプロジェクトのベースを構築してくれる
- Vue CLI で単一ファイルコンポーネントを手軽に使用できる
- ES2015以降のECMAScriptを使ったコーディングが可能、基本を把握しておく
【勉強会資料】Laravelのルーティングについてのいろいろ
以下は[秋葉原] 第二回 初心者向けLaravel 勉強会 (ルーティング/テスト)にて行われた発表用の資料となります。 Laravel5.6の有志制作のリファレンス・ルーティングや、いくつかの記事を参考にかかせていただいたものです。
ルーティング知っているようで、知らない事が多くありました。自分の書き方が古いバージョンのものだけど、動いているとか。普段は自分に必要な事柄だけしか読まないのですが、改めてリファレンスの隅々まで読んだ事で、より効率的に、汎用的な事が出来そうです。
Gitの履歴変更で盛大に失敗した話をメモベースで書く
GitHubに公開リポジトリを使ってアプリを作っているが、上げちゃいけないモノをアップしてしまったのをきっかけに色々やったメモです。要はバットノウハウ、失敗談、こんな風になっちゃだめですよという自分への戒めです。
そもそもの経緯
branchを二つ作って、個別に新機能を開発をしていた。
branchB(ゆっくり目の作業)で .gitignoreを編集した後 branchA(急ぎ目の作業)をcheckoutして別作業を続ける。
branchBでしていた .gitignore の設定がなくなった為、 branchAでgit add .
すると 隠したいはずの追加ファイルがステージングに上がり、そのまま気付かずコミットしてしまう。(ぎゃー)
しばらくしてその事実に気付き、リモートリポジトリの履歴から該当ファイルを削除しようと以下の記事を見つつ試みる
Git リポジトリに上がっているファイルを履歴ごと消すには? - Qiita
ところが、コマンドの意味を理解しないままガチャガチャと操作(ぎゃー)
禁断の git push -f
を良くわからないまま実行する(ぎゃー)
何が起こったか?
まずはこいつを見てくれ、どう思う?
すごく…複雑です。
盛大にやらかして色々やってこりゃ駄目だ終わった状態のリポジトリである。 画面にはローカルとリポートのbranch両方が表示されて同じコミットのコメントが複数にコピーされてしまっている。
何が起こったかを自分なりに理解した範囲で解説。
本当はもっと根本を理解してからまとめて述べた方が、為になると思うのだけど、自分がやりたいのはアプリの制作であってツールに振り回された顛末を詳しく分析することじゃない。従ってこんな風にややこしい自体にハマるGitという複雑なツールを理解できない俺が悪いのか、ツールが悪いのかという議論はしないし、受け付けない。(けど、こういう事じゃない?という見解は良かったらくれると助かります。)
git filter-branch --tree-filter "rm -f [消したいファイルパス]" HEAD
このコマンドですべての履歴から特定のファイルの履歴を消せるとあったので消したが、 上記サイトを参考に実行した、このコマンドかこの後行った操作が、どうも過去の履歴に渡るコミット時のhash値を変えるという操作だったらしい。そもそも変更されたファイルの状態からhashを生成しているっぽいので、もしかして履歴が変わればhash値もブロックチェーン的に変わる感じかな?
で、Gitのそもそも論の記事がある。
P117~P146あたりにrebase絡みでコミットのリビジョンについての解説がある。で、おそらく状況としては、履歴の書き換えをすることで、rebaseに近いhash値の書き換えが発生、それに気づかないまま、強制pushであるgit push -f
を怖さも知らずに実行し、ローカル、リモート、それぞれで古い履歴を消さないまま、良くわからず複数のbranchで履歴の削除を繰り返した結果、複数の履歴が存在し、収集が付かなくなった。という事の様だ。
こえーよ、Gitまじこえーよ。理解してない人間が使っちゃいけないオーバーテクノロジーだよ。
そもそもの問題の発生は .gitignore ファイルを編集した状態で別branchをcheckoutした事が原因でこんなことになるとは思いもよらなんだ。カスケード破壊ってやつですか。
という事で、理解するのは諦めてこの後、何をしたかという話ですが…。
実は、履歴の変更をしようと決めた時点で、リモートリポジトリ非公開にしてから潰すつもりだったので、ローカルリポジトリにバックアップを取って、上みたいになるまで安心して弄り倒して壊しまくってました。
現在はバックアップを取ったリポジトリのbranchを整理してから master branch で該当ファイルの履歴からの削除を行いました。ヤバいファイルを履歴から完全に削除したのですが、どうもこれで コミット時のhash値は、初期のオリジナルの物とは変わってしまいました。どうもそれは仕方のない事らしいというのが昨日今日で調べた範囲で判明している事です。(そんなこと無いって見解と知識が欲しい)チーム開発でこれをやったら終わりですが、ほとんど個人でやっていたので、リセットさえすりゃ簡単な事態であった事は確かです。 しかし、これチームで履歴の編集をやらかす事態が発生すると、チーム全員に一旦 Cloneでやり直しをさせるって事になるのか…。これは申し訳ない事態だ。
Laravelの場合はせいぜい .envファイルのコピーと storage/log の chmod 777 位の手間ですが、他のプロジェクトでは色々面倒な初期設定も他のメンバーにしてもらわんといけないかもなので、履歴を書き換えるような事態は発生させてはいけませんね。
閑話休題。現在は奇麗になったリポジトリを再度リネームしてGitHubに上げたのと、開発環境から本番環境にpushするだけでデプロイできるようにサーバー側も再設定しました。(本当はSeleniumを挟んでデプロイ出来るような環境をそろそろ作りたいのですが)
今回のリポジトリの再設定の際は、これまで sourceTreeに頼りっぱなしでコマンドラインをあまり使ってませんでしたが、VPSサーバーにpushする際の.ssh/config
やGit configをそこそこ設定したり、testでcommitした際は全てコマンドラインで作業を行い。以前よりはGUIに頼らず操作ができる様になってきた感じです。ですが、やはり分岐しまくったbranchの状況を追うにはGUIでの可視化が便利ですね。
という訳で履歴管理ツールに悩まされるなんていうアホな事態はとっとと終わらせてモノを作ります。
【輪読会資料】基礎から学ぶVue.js CHAPTER5 コンポーネントでUI部品を作る 読書メモ
今回は輪読担当ではなかったので、完全な自分のメモですが、アップしておきます。
var で定義できるローカルのコンポーネントもある、
// コンポーネントを定義 var myComponent = { template: '<p>MyComponets</p>' } new Vue({ el: '#app', componets: { // 処理 'myComponet' : myComponet } })
コンポーネント間の通信
P155
親から子
// コンポーネントの定義 comp-child Vue.component('comp-child', { // テンプレートで受け取ったvalを使用 template: '<p>{{ val }}</p>', // 受け取る属性名を指定 props: ['val'] }) // 親コンポーネント new Vue({ el: '#app', data: { valueA: 'これは子A', valueB: 'これは子B' } })
<div id="app"> <comp-child val="これは子A"></comp-child> <comp-child val="これは子B"></comp-child> </div>
出力結果 子コンポーネントの
で囲む状態が出力されている
<p>これは子A</p> <p>これは子B</p>
props
で受け取ってないものは子供側では上書きが基本、class等の属性は親と子でマージされ両方反映
コンポーネントのルートタグ
Vue.component('comp-child', { template: '<p id="child" class="child">ChildComponent</p>', })
コンポーネントのカスタムタグ
<comp-child id="parent" class="parent"></comp-child>
出力される実態
<comp-child id="parent" class="child parent">ChildComponent</comp-child>
// 子 Vue.component('comp-child', { template: '<li>{{ name }} HP.{{ hp }}</li>', props: ['name', 'hp'] }) // 親 new Vue({ el: '#app', data: { list: [ { id: 1, name: 'スライム', hp: 100 }, { id: 2, name: 'ゴブリン', hp: 200 }, { id: 3, name: 'ドラゴン', hp: 500 } ] } })
list は親を使っている name, hp は 子のprops定義で使える様になっている
<!-- これも親となる comp-child で子を呼び出し--> <ul> <comp-child v-for="item in list" v-bind:key="item.id" v-bind:name="item.name" v-bind:hp="item.hp"></comp-child> </ul>
子コンポーネントで値を書き換えてもconsoleに[Vue warn]が出力される。
当然親の値は書き換えられない
Vue.component('comp-child', { template: '<li>{{ name }} HP.{{ hp }}\ <button v-on:click="doAttack">攻撃する</button></li>', props: ['name', 'hp'], methods: { doAttack: function () { // 勝手に攻撃! this.hp -= 10 // -> [Vue warn] error! } } })
もし、値の書き換えを行いたいなら算出プロパティを使って新しいデータを作成する。
もし親のデータ自体を変更したい場合は、$emit
を使って親にアクションを起こさせる。等する
props
で値を受け取る際は、データ型を指定しておくのが推奨されている。
Vue.component('comp-child', { props: { val: String // 文字列型のみ許可 } ])
型の指定方法一覧
データ型 | 説明 | 例 |
---|---|---|
String | 文字列 | '1' |
Number | 数値 | 1 |
Boolean | 真偽値 | true, false |
Function | 関数 | function() {} |
Object | オブジェクト | { name: 'foo' } |
Array | 配列 | [1, 2, 3], [{ id: 1 }, { id: 2 }] |
カスタム | インスタンス | new Cat() |
null | すべての型 | 1, '1', [1] |
型チェック無しの場合の書き方
Vue.component('example', { props: ['value'] // どんな型も受け入れる })
型チェックする場合の書き方
Vue.component('example', { props: { value: // ここにデータ型を指定 } })
型チェック以外にオプションで様々なバリデーションに対応できる
参照元
ほとんどControllerみたいになってきた。
Vue.component('my-component', { props: { // 基本的な型の検査 (`null` と `undefined` は全てのバリデーションにパスします) propA: Number, // 複数の型の許容 propB: [String, Number], // 文字列型を必須で要求する propC: { type: String, required: true }, // デフォルト値つきの数値型 propD: { type: Number, default: 100 }, // デフォルト値つきのオブジェクト型 propE: { type: Object, // オブジェクトもしくは配列のデフォルト値は // 必ずそれを生み出すための関数を返す必要があります。 default: function () { return { message: 'hello' } } }, // カスタマイズしたバリデーション関数 propF: { validator: function (value) { // プロパティの値は、必ずいずれかの文字列でなければならない return ['success', 'warning', 'danger'].indexOf(value) !== -1 } } } })
子から親
P161
字のデータを親に渡すには$emit
を使う
親のコード
on で受け取る
<child-comp v-on:childs-event="parentMethod">
子のコード
$emitで渡す
this.$emit('childs-event')
parentsMethod
がchilds-event
を通じて親に渡る
<comp-child v-on:childs-event="parentsMethod"></comp-child>
Vue.component('comp-child', { template: '<button v-on:click="handleClick">イベント発火</button>', methods: { // ボタンのクリックイベントのハンドラでchilds-eventを発火する handleClick: function () { // `parentsMethod`が`childs-event`を通じて親に渡る this.$emit('childs-event') } } }) new Vue({ el: '#app', methods: { // childs-eventが発生した! parentsMethod: function () { alert('イベントをキャッチ! ') } } })
親が持つデータを操作する
前述のモンスターのHPを子のメソッドで処理していた例を動くものにしたのが以下の例
良く読み込んで動きはなんとか理解はしたけど、今は書ける気がしない...。
<ul> <comp-child v-for="item in list" v-bind:key="item.id" v-bind:name="item" v-on:attack="handleAttack"></comp-child> </ul>
Vue.component('comp-child', { template: '<li>{{ name }} HP.{{ hp }}\ <button v-on:click="doAttack">攻撃する</button></li>', props: { id: Number, name: String, hp: Number }, methods: { // ボタンのクリックイベントのハンドラから$emitでattackを発火する doAttack: function () { // 引数として自分のIDを渡す v-on:attackの中身`handleAttack`を親に渡す this.$emit('attack', this.id) } } }) new Vue({ el: '#app', data: { list: [ { id: 1, name: 'スライム', hp: 100 }, { id: 2, name: 'ゴブリン', hp: 200 }, { id: 3, name: 'ドラゴン', hp: 500 } ] }, methods: { // attackが発生した! handleAttack: function (id) { // 引数のIDから要素を検索 var item = this.list.find(function (el) { return el.id === id }) // HPが0より多ければ10減らす if (item !== undefined && item.hp > 0) item.hp -= 10 } } })
カスタムタグのイベントハンドリンク
この書き方だと、コンポーネント側からclick
を$emit
で呼び出さないと発火しないらしい。
<my-icon v-on:click="handleClick"></my-icon>
元々のイベントは直接発火したい場合は.native
修飾子を付ける。
<my-icon v-on:click.native="handleClick"></my-icon>
非親子コンポーネント
親子関係以外だと、this
,props
の通信はできない。
Vueインスタンスを「イベントバス」として使用するが、多用しない方が良い。コードがカオス化するので、詳細はP166参照
子コンポーネントを参照する「$refs」
親コンポ―ネント
<comp-child ref="child">
親のメソッド内
new Vue({ el: '#app', methods: { handleClick: function () { // 子コンポーネントのイベントを発火 this.$refs.child.$emit('open') } } })
子コンポーネント内
Vue.component('comp-child', { template: '<div>...</div>', created: function () { // 自分自身のイベント this.$on('open', function () { console.log('なにか処理') }) } })
親側からはv-on
を使うようにする
コンポーネントの属性のスコープ
コンポーネントの属性の値部分は親のスコープになる
<comp-child v-on:child-event="親のメソッド">
<comp-child v-on:child-event="parentMethod(親のデータ)">
子コンポーネントが引数をもって$emit
を実行している場合、子コンポーネントの引数は$event
変数で使用できる
<comp-child v-on:child-event="parentMethod($event, parantsData)">
new Vue({ el: '#app', data: { parentsData: '親のデータ' }, methods: { methods: { parentsMethod: function(childArg, parentsArg) { // } } } })
$event
変数は$emit
の第一引数しか持たないので、複数の引数を渡す際は1つのオブジェクトにまとめる。
this.$emit('childs-event', {id: 1, name: '新しい名前'})
【輪読会資料】基礎から学ぶVue.js CHAPTER4 データの監視と加工 読書メモ
以下の記事は2019/2/21 コワーキングスペース秋葉原Weeybleで行われる輪読会
[秋葉原] 基礎から学ぶVue.js輪読会 ch4 データの監視と加工 (初心者歓迎!)のための読書メモとなります。
以下の書籍の CHAPTER4 データの監視と加工 のメモです。
- 作者: mio
- 出版社/メーカー: シーアンドアール研究所
- 発売日: 2018/05/29
- メディア: 単行本(ソフトカバー)
- この商品を含むブログを見る
さすがに輪読担当4回中3回ともなると疲れて来た…。
書籍用のサイトのCHAPTER4記述ページ(サンプルコード有り)
これまでの輪読会資料
CAHPTER 1
【輪読会資料】基礎から学ぶVue.js CHAPTER1 Vue.jsとフレームワークの基礎知識 読書メモ - 作りたいものがありすぎる
CHAPTER 2
【輪読会資料】基礎から学ぶVue.js CHAPTER2 データの登録と更新 - Qiita
CHAPTER 3
【輪読会資料】基礎から学ぶVue.js CHAPTER3 イベントとフォーム入力の受け取り 読書メモ - 作りたいものがありすぎる
さきにまとめを書いて全体を把握
- 算出プロパティは結果のキャッシュをしてくれる
- データの状態に対して処理をしたいならウォッチャ
- カスタムディレクティブは監視をしながらDOMの操作ができる
- 更新後のDOMにアクセスしたいならnextTick
リアルタイムにDOMの値を調べて、処理(関数)をして、その結果を画面にリアルタイムに反映できる!という事っぽい。
CHAPTER4 データの監視と加工
SECTION 16 算出プロパティで処理を含むデータを作成する
要は普通に自分で作った関数での処理ができてデータもバインドできるので、リアルタイムな処理が可能という事らしい。
算出プロパティの使い方
function
とあるのでメソッド(関数)っぽいけど、プロパティ(変数・値)
従来散々使ったVueのdata:
と並行した値でcomputed:
を持たせ、そこに各プロパティを定義した関数を入れる構成
computed
はのプロパティ以降は関数で定義する。
マスタッシュ({{ hoge }}
)等でプロパティとして使える
<p>{{ width }} の半分は {{ halfWidth }}</p>
new Vue({ el: '#app', data: { width: 800 }, computed: { // 算出プロパティhalfWidthを定義 halfWidth: function() { return this.width / 2 } } })
算出プロパティを組み合わせて使用しよう
算出プロパティを組み合わせたり、配列を使う事もできる
this.プロパティ名
で他の算出プロパティで定義したり計算した結果を引っ張って利用するとこができる。かなり普通のプログラムっぽい感じになる。
たぶん、画面の高さや幅を算出して、必要な位置に表示物を出す際なんかに強力に使える感じ?
<p>X: {{ halfPoint.x }}</p><!-- 400 --> <p>Y: {{ halfPoint.y }}</p><!-- 300 -->
new Vue({ el: '#app', data: { width: 800, height: 600 }, computed: { halfWidth: function() { return this.width / 2 // 400 }, halfHeight: function() { return this.height / 2 // 300 }, // 「width × height」の中心座標をオブジェクトで返す halfPoint: function() { return { x: this.halfWidth, // 400 y: this.halfHeight // 300 } } } })
ゲッターとセッター
上の例では算出プロパティの値を代入(ユーザーからの入力等)しても実はエラーになってしまう
しかし、セッターもあるので、入力への対応も可能方法はこんな感じ
先週やった相互に値を影響し合える v-model
を使う!
セッターの例 halfWidth
をユーザーフォームにバインディングする
width:<input v-model.number="width"> {{ width }} <br> halfWidth:<input v-model.number="halfWidth"> {{ halfWidth }}
セッターを利用するには以下の様にget
,set
の2つの名前決め打ちのプロパティを定義すること
new Vue({ el: '#app', data: { width: 800 }, computed: { halfWidth: { // width の半分の値を取得する get: function() { return this.width / 2 }, // halfWidth の2倍の数値を width に代入する set: function(val) { this.width = val * 2 } } } })
これをデモすると、2つの入力フォームに数値が入っており、片方の値を入れると、もう片方の値に影響を与える事ができる!
halfWidth:
プロパティ(内の各function)がしていること
get
でwidth
の値を取得して
set
でhalfWidth
の値に代入する
つまり値を順繰りに回しているループしないか不安になるが、これで値をインタラクティブで双方向に連携ができる
算出プロパティのキャッシュ機能
算出プロパティとメソッド(関数)の違い
算出プロパティ リアクティブなデータをキャッシュ化してしまう。
<p>算出プロパティ キャッシュ化されるので一度しか処理が走らず同じ値が出力される</p> <ol> <li>{{ computedData }}</li> <li>{{ computedData }}</li> </ol> <p>メソッド 毎回処理が走る為、値が変わる</p> <ol> <li>{{ methodsData() }}</li> <li>{{ methodsData() }}</li> </ol>
new Vue({ el: '#app', computed: { computedData: function () { return Math.random() } }, methods: { methodsData: function () { return Math.random() } } })
ここがわかった上での使い分けが大事
リストの絞り込みに利用しよう
算出プロパティは内のfunction
は一度だけ処理されキャッシュ化されるので、複雑な処理に向いている
例えば絞り込みリスト等を作る際に便利
<div id="app"> <input v-model.number="budget"> 円以下に絞り込む <input v-model.number="limit"> 件を表示 <p>{{ matched.length }} 件中 {{ limited.length }} 件を表示中</p> <ul> <!-- v-forでは最終結果、算出プロパティのlimitedを使用する --> <li v-for="item in limited" v-bind:key="item.id"> {{ item.name }} {{ item.price }}円 </li> </ul> </div>
new Vue({ el: '#app', data: { // フォームの入力と紐付けるデータ budget: 300, // 表示件数 limit: 2, // もとになるリスト list: [ { id: 1, name: 'りんご', price: 100 }, { id: 2, name: 'ばなな', price: 200 }, { id: 3, name: 'いちご', price: 400 }, { id: 4, name: 'おれんじ', price: 300 }, { id: 5, name: 'めろん', price: 500 } ] }, computed: { // budget以下のリストを返す算出プロパティ matched: function () { return this.list.filter(function (el) { return el.price <= this.budget }, this) }, // matchedで返ったデータをlimit件返す算出プロパティ limited: function () { return this.matched.slice(0, this.limit) } } })
一回読み込んだ、mached
処理のキャッシュの上にlimited
の処理が乗ってlimited
のみで算出の修正が行われている事になるらしい。
ソート機能を追加しよう
さらに書籍には 上記にソート機能を追加したコードがのっているが、最初動かなかった。
_.orderBy
ってのが怪しんで、最初はタイポで本来どう書けばいいのか悩む。JSにはない関数でVue.jsの1.X系にはある?でも _.
って何をしている事なのか不明だったが、よく見るとLodash
入れろとあった…。だめじゃん、俺
ということでLodash
を入れる。この1行追加
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.5/lodash.min.js"></script>
<input v-model.number="budget"> 円以下に絞り込む <input v-model.number="limit"> 件を表示 <!-- 追加されたボタン --> <button v-on:click="order=!order">切り替え</button> <p>{{ matched.length }} 件中 {{ limited.length }} 件を表示中</p> <ul> <!-- v-forでは最終結果、算出プロパティのlimitedを使用する --> <li v-for="item in limited" v-bind:key="item.id"> {{ item.name }} {{ item.price }}円 </li> </ul>
new Vue({ el: '#app', data: { // 追加 // フォームの入力と紐付けるデータ budget: 300, limit: 2, list: [ { id: 1, name: 'りんご', price: 100 }, { id: 2, name: 'ばなな', price: 200 }, { id: 3, name: 'いちご', price: 400 }, { id: 4, name: 'おれんじ', price: 300 }, { id: 5, name: 'めろん', price: 500 } ] }, computed: { // budget以下のリストを返す算出プロパティ matched: function() { return this.list.filter(function(el) { return el.price <= this.budget }, this) }, // sortedを新しく追加 この _.orderBy ってのが Lodash のメソッドらしい // https://lodash.com/docs/#orderBy sorted: function() { return _.orderBy(this.matched, 'price', this.order ? 'desc' : 'asc') }, // limitedで使用するリストをsortedに変更 limited: function() { return this.sorted.slice(0, this.limit) } } })
Lodashの_.orderBy
関数。
_.orderBy(collection, [iteratees=[_.identity]], [orders])
今回の例の第三引数はthis.order ? 'desc' : 'asc'
三項演算子としている。
SECTION 17 ウォッチャでデータを監視して処理を自動化する
ウォッチャとは特定のデータ、算出プロパティの状態を監視して、データに変化があった際に処理を実行してくれるフック機能のこと
ウォッチャ自体は値を返さない
ウォッチャの使い方
コンポーネントにwatch
を用意して、監視対象の名前と、変化した際の処理を書く
第二引数は無くてもOK
オプションを使用しない場合
new Vue({ // ... watch: { 監視するデータ: function (新しい値, 古い値) { // valueが変化したときに行いたい処理 }, 'item.value': function (newVal, oldVal) { // オブジェクトのプロパティも監視できる } } })
オプションを使用する場合
プロパティ | 値 | 説明 |
---|---|---|
deep | bool | ネストされたオブジェクトも監視 |
immdiate | bool | 読み込み時に即呼び出し |
new Vue({ // ... watch: { list: { handler: function (newVal, oldVal) { // listが変化したときに行いたい処理 }, // ここからがオプション deep: true, immediate: true } } })
インスタンスメソッドでの登録
this.$watch
と書くと各メソッド内でウォッチャが使える
this.$watch('value', function(newVal, oldVal) { // ... })
こんな風に書く,オプションの位置が独特
created: function () { this.$watch('value', function () { // ...処理をかく }, { immediate: true, deep: true }) }
ウォッチャの解除
インスタンスメソッドで登録した場合、返り値を使って監視を解除できる
var unwatch = this.$watch('value', handler) unwatch() // value の監視を解除
一度だけ動作するウォッチャ
unwatch
を利用する
new Vue({ el: '#app', data: { edited: false, list: [ { id: 1, name: 'りんご', price: 100 }, { id: 2, name: 'ばなな', price: 200 }, ] }, created: function() { var unwatch = this.$watch('list', function () { // listが編集されたことを記録する this.edited = true // 監視を解除 unwatch() }, { deep: true }) } })
実行頻度の制御
フォーム入力などで監視を使うと、高頻度で値が変わるので、非同期通信が度々発生してパフォーマンスに影響する。その為setTimeout
やLodash
などを利用してウォッチャの実行頻度を制御すると良い。
watch: { // "_." なのでLodashですね 指定ミリ秒後にコールバック呼び出しをする value: _.debounce(function (newVal) { // ここへコストの高い処理を書く console.log(newVal) }, // valueの変化が終わるのを待つ時間をミリ秒で指定 500) }
デモ、 最後のミリ秒経過後に、フォーム入力後の値がconsoleに出力される
<input type="text" v-model="value">
new Vue({ el: '#app', data: { value: '編集してみてね' }, watch: { value: _.debounce(function (newVal) { // ここへコストの高い処理を書く console.log(newVal) }, // valueの変化が終わるのを待つ時間をミリ秒で指定 1500) } })
複数の値を監視する
インスタンスメソッドを使い監視対象を関数にして登録する。
this.$watch(function () { return [this.width, this.height] }, function(){ // 上で言ってる関数ってここの funtionの事? // width , height が変化した際の処理 })
解らん場合はリファレンス
Vue.js インスタンスメソッド / データ
vm.$watch( expOrFn, callback, [options] )
今回は時間がないけど、余裕があるならリファレンスを書籍を行ったり来たりすると理解は進みそう。
オプションに登録する場合は、算出プロパティを監視することができる
computed: { watchTarget: function () { return [this.width, this.height] } }, // ここからオプション watch: { // これで算出プロパティを監視できる watchTarget: function() { ... } }
POINT
大事そうだけど省略
フォームを監視してAPIからデータを取得しよう
GitHub APIからリポジトリ一覧を取得するかんたんなアプリのデモ
axios
のAJAX(非同期通信)のgetでAPIからの値を取得して画面に表示している。
<div id="app"> <select v-model="current"> <option v-for="topic in topics" v-bind:value="topic.value"> {{ topic.name }} </option> </select> <div v-for="item in list">{{ item.full_name }}</div> </div>
new Vue({ el: '#app', data: { list: [], current: '', topics: [ { value: 'vue', name: 'Vue.js' }, { value: 'jQuery', name: 'jQuery' } ] }, watch: { current: function (val) { // GitHubのAPIからトピックのリポジトリを検索 axios.get('https://api.github.com/search/repositories', { params: { q: 'topic:' + val } }).then(function (response) { this.list = response.data.items }.bind(this)) } }, })
htmlのv-model="current"
になんでもかんでも入れてる感じ?
開発コンソールを開きNetwork
タブにあるgetパラメータをクリックすれば
取得した内容が見れる
要は以下の様なURLを生成している
https://api.github.com/search/repositories?q=topic:vue
https://api.github.com/search/repositories?q=topic:jQuery
Vue内のcurrent:
では以下のGETのパラメータを生成している
q=topic:vue
q=topic:jQuery
で、帰ったJSONをlist
に入れて、html側でv-for="item in list
のfor文で回して
items
内の配列の中に存在する項目full_name
をhtml側で表示している。
SECTION 18 フィルタでテキストの変換処理を行う
ここでいう「フィルタ」とは、文字数を丸める。数字にカンマを入れる。等のテキストベースの変換処理を指す。
注意: ローカルに登録した場合でもthis
へのアクセスはできない
フィルタの使い方
{{ Mustache }} か、v-bind
ディレクティブに |
で繋げて呼び出せる
<!-- Mustashのケース --> {{ 対象のデータ | フィルタの名前 }} <!-- v-bindのケース --> <div v-bind:id="対象のデータ| フィルタの名前"></div>
ローカルへの登録
コンポーネントのfilters
オプションに登録することで特定のコンポーネント内のみで仕様できる
new Vue({ el: '#app', data: { price: 19800 }, filters: { localeNum: function (val) { return val.toLocaleString() } } })
{{ price | localeNum }}円
こんな感じにつかえる
出力例 カンマ付き
19,800円
グローバルへの登録
グローバルメソッドVue.filter
に登録すると全てのコンポーネントから利用できる
Vue.filter('localeNum' ,function(val){ return val.toLocaleString() })
フィルタに引数を持たせる
引数を持たせることもできる
{{ message | filter(foo, 100) }}
この例は
第一引数message
プロパティの値
第二引数foo
プロパティの値
第三引数100
が受け取れる
複数のフィルタをつなげて使用する
{{ value | filter1 | filter2 }}
new Vue({ el: '#app', filters: { // 小数点以下を第2位に丸めるフィルタ round: function (val) { return Math.round(val * 100) / 100 }, // 度からラジアンに変換するフィルタ radian: function (val) { return val * Math.PI / 180 } } })
180 度は {{ 180 | radian | round }} ラジアンだよ
こんな風に180度のラジアンを出して、それを四捨五入表記にできる
SECTION 19 力スタムディレクティブでデータを監視しながらDOMを操作する
v-bind
の様なディレクティブを自作できる機能、DOMのAPIを使いたいケース等で、DOMの状態を検知しながらDOM操作ができる様になる。仮想DOMではないので描画の最適化はされない。
カスタムディレクティブの使い方
こんな風に任意の名前を付けられる?
<div v-hoge> </div>
トリガとなるデータを値としてバインドすることもできる
<!-- valueが変化すれば呼び出される --> <div v-hoge="value"> </div>
ローカルへの登録
カスタムディレクティブはdirectives:
オプションに登録すると特定のコンポーネントで使える
new Vue({ el: '#app', directives: { focus: { // 紐付いている要素がDOMに挿入されるとき inserted: function (el) { el.focus() // 要素にフォーカスを当てる(文字入力待ち状態) } } } })
<input type="text"><!-- フォーカスされない --> <input type="text" v-focus><!-- フォーカスされる -->
上のデモをすると入力フォームの二番目に |
の点滅が入り、文字入力受付状態が確認できる。
グローバルへの登録
グローバルメソッドに登録する方法はVue.directive
メソッドを使う
(コンポーネント以外でどこでも使う汎用的なものとかを登録すると良いかも?)
Vue.directive('forcus', { inserted: function (el) { el.focus() // 要素にフォーカスを当てる(文字入力待ち状態) } })
使用可能なフック
メソッド名 | タイミング |
---|---|
bind | ディレクティブが初めて要素と紐づいた時に |
inserted | 紐づいた要素が親Nodeに挿入された時に |
update | (仮想DOM更新時)紐づいた要素を包含しているコンポーネントのVNodeが更新された時に |
componentUpdated | (仮想DOM更新時)包含しているコンポーネントと子コンポーネントのVNodeが更新された時に |
unbind | 紐づいていた要素からディレクティブが削除される時に |
Vue.directive('example', { // ディレクティブが初めて要素と紐づいた時に bind: function (el, binding) { console.log('v-example bind') }, // 紐づいた要素が親Nodeに挿入された時に inserted: function (el, binding) { console.log('v-example inserted') }, // 紐づいた要素を包含しているコンポーネントのVNodeが更新された時に update: function (el, binding) { console.log('v-example update') }, // 包含しているコンポーネントと子コンポーネントのVNodeが更新された時に componentUpdated: function (el, binding) { console.log('v-example componentUpdated') }, // 紐づいていた要素からディレクティブが削除される時に unbind: function (el, binding) { console.log('v-example unbind') } })
フックの引数
vnode
から呼び出したコンポーネントを参照できるが難しいらしいので割愛。
『公式ガイド、仮想DOMを読め』とある
引数 | 内容 |
---|---|
el | 以下省略、書籍参照のこと |
binding | |
vnode | |
oldVnode |
フックの関数による省略方法
第二引数に関数を入れると bind
と update
にフックして同じ処理を呼び出せる。
Vue.directive('example', function(el, vnode, oldVnode) { // bind と updateで呼び出される }
動画の再生を操作する例
<!-- 動画1 --> <video src="demo1.mp4" v-video="video1" width="400"></video> <button v-on:click="video1=true">再生</button> <button v-on:click="video1=false">停止</button> <br> <!-- 動画2 --> <video src="demo2.mp4" v-video="video2" width="400"></video> <button v-on:click="video2=true">再生</button> <button v-on:click="video2=false">停止</button>
new Vue({ el: '#app', data: { video1: false, video2: false }, directives: { video(el, binding) { binding.value ? el.play() : el.pause() } } })
上記のコードをデモすると2つの動画をボタンで再生・停止できるようになる
基本的な読み方がいまだあいまいなので解説
html中のv-video
でバインドしたvideo1
の値はまず,スクリプト内でfalseに設定されている
video1: false,
再生ボタンを押すとvideo1
の値がhtml側でtrue
にされる
その値はスクリプト側のでdirectives:
のhtmlと紐づけたvideo( ... )
で監視されていて
el
で渡ったの引数に収納されたboolに対して、play()
,またはpause()
メソッドを実行している
という感じか?
ただし、この実装だと、video1のプロパティを変更すると、video2プロパティの要素のディレクティブも一緒に呼び出されるので、複雑な処理を書く際には不向きらしい。
次の方法はこれを改善できるという
前の状態と比較して処理を行う
力スタムディレクティブはメソッドの引数に古いVnodeと古いバインドデータを受け取れるので、バインドしたデータに変化があった際のみに処理を行わせることが可能。
arg | 引数 |
---|---|
modifiers | 修飾子のオブジェクト |
value | 新しい値 |
odlvalue | 古い値(updateとcomponetUpdateのみ) |
では先ほどの動画デモを修正してoldValue
プロパティの比較で無駄な処理を走らせないように改修したのが下の例
new Vue({ el: '#app', data: { video1: false, video2: false }, directives: { video(el, binding) { // ここに oldValue での判定を加えた if (binding.value !== binding.oldValue) { binding.value ? el.play() : el.pause() } } } })
SECTION 20 nextTickで更新後のDOMにアクセスする
非同期処理が走ると、JSのコードの中では処理が遅れて、更新状態を判定できない事がままある。 (なので、非同期じゃないPHPなんかでPOSTとかすると、値が返るまでそこで処理が止まる。)
特にJSで非同期でDOM絡みの操作を書くと、非同期でPOST、GETをして値を取ったので、JS内で監視してても、それは先にプログラムが走り終わってるので、取得した値を元に処理ができない!
なんてことが発生してしまう。それを何とかしてくれるのがnextTick
らしい
DOM更新を待ち受けて処理をしてくれるそうだ
nextTickの使い方
方法1, グローバルメソッドVue.nextTick
のコールバック関数として定義する。
方法2, インスタンスメソッドとしてthis.$nextTick
として使う。
this.$nextTick(function(){ // DOM更新後に行いたい処理を書く })
これで、関数内の処理がDOM更新後に行うように予約できる
更新後のDOMの高さを取得しよう
例えば、プルダウンのリストの高さを通常のVue.js的な書き方で取得しようとした場合。
DOMへのアクセスをするなら$refs
を使う
console.log("通常:", this.$refs.list.offsetHeight);
CH2のおさらい ここから------
DOMにアクセスするには、インスタンスプロパティ$el
,$refs
を使用する。
但し、ライフサイクルフックのmounted
以降でないと使えない
$refsの使い方
<div id="app"> <p ref="hello">hello</p> <!-- p要素にhelloと名を付けた --> </div>
以下の様にアクセス
new Vue({ el: '#app', mounted: function() { console.log(this.$refs.hello) // p要素のDOMとなる } })
$el や$refsは一時的な変更です!
これらは仮想DOMではないので描画処理の最適化をしない
操作の都度描画するので注意
CH2のおさらい ここまで------
でも、これだと処理の後にDOMが追加されてもその値を拾う事ができない。
こんなデモがある
new Vue({ el: "#app", data: { list: [] }, watch: { list: function() { // 更新後のul要素の高さを取得できない… console.log("通常:", this.$refs.list.offsetHeight); // nextTickを使えばできる! this.$nextTick(function() { console.log("nextTick:", this.$refs.list.offsetHeight); }); } } });
<!-- list. の高さは最初は0なので数値1からを出力させる処理が,実はここに書いてある --> <button v-on:click="list.push(list.length+1)">追加</button> <ul ref="list"> <li v-for="item in list">{{ item }}</li> </ul>
開発タブでconsole見てボタンを押して<li>
を追加しても 通常:
の方の値は0となって取得できてない。が、$nextTick
を使った方は上書きしたDOMからの値が取得できている
nextTickの注意点:
ウェブフォント、画像が含まれていた場合、それらのロード(詳細情報?)は持たないので、画像の高さは決め打ちにして計算して使う。といったテクニックが必要になる。ということらしいです。
まとめ
- 算出プロパティは結果のキャッシュをしてくれる
- データの状態に対して処理をしたいならウォッチャ
- カスタムディレクティブは監視をしながらDOMの操作ができる
- 更新後のDOMにアクセスしたいならnextTick
Homestead環境で複数環境がある場合、外部から接続するアプリを選択する際の小技
先に結論
homestead(Laravel Homestead', '7.12.0)で複数のアプリを設定して外部端末から公開ipアドレスでアクセスする場合。
sites: の -map に指定するドメインのアルファベットの最昇順のサイトが表示される。
sites: - map: whois.test to: /home/vagrant/code/Laravel/public schedule: true - map: zero.test to: /home/vagrant/zero/Laravel/public # アルファベット順に最初のこれが外部アクセスからのデフォルト表示となる。 - map: aaa.test to: /home/vagrant/hoge/Laravel/public # 省略 networks: - type: "public_network" ip: "192.168.11.99" bridge: "en1: Wi-Fi (AirPort)"
こんな方法で設定するのは不本意なのですが、有志の日本語ドキュメントの以下の方法では上手くいきませんでした。
そもそもの話
windows10でvagrantのHomestead環境を使ってLaravelの開発をしていますが、複数のアプリケーションのプロジェクトを動かしているため、Homestead.yamlとhostに複数のアプリの環境を設定しています。
詳しくは以前の記事を参照の事 windows vagrant Homestead環境でLaravelアプリを追加する際の覚書 - 作りたいものがありすぎる
以下の例の様に複数のアプリの環境を設定できる。
Homested.yaml
folders: - map: C:/Vagrant/Whois to: /home/vagrant/code - map: C:/Vagrant/zero to: /home/vagrant/zero sites: - map: whois.test to: /home/vagrant/code/Laravel/public schedule: true - map: zero.test to: /home/vagrant/zero/Laravel/public
また、詳細は省きますがwindowsのhosts
ファイルにもローカルドメインとipを設定します。
この特定のアプリをローカルネットワーク(wi-fi環境)で他のマシンからも開発環境が見れるようにする必要がありましたので、これまではHomested.yaml
ファイルに以下の様な記述をすることで、他のPCから閲覧できるようにしていました。
Homested.yaml
networks: - type: "public_network" ip: "192.168.11.99" bridge: "en1: Wi-Fi (AirPort)"
上記の設定の192.168.11.99
に別のPCからアクセスすれば、これまで問題なく閲覧出来ていたのですが、先日の記事を書いて以降、意図しない別の開発環境が表示されてしまい、困った事になりました。
しかしこれまで、意図したアプリの画面が出ていたことがむしろ偶然の幸運で、複数アプリを設定してる場合、外部からアクセス可能なipでどのアプリが出るかを設定項目はyamlには書いて無い訳です。従来はたまたま偶然に望みのアプリが出ていた。という訳です。
そこで、どうすれば意図したアプリを外部から接続するか調べましたが、上手く行かず結論として上記の様なかなりその場しのぎの方法となりました。
環境の共有
共同作業者やクライアントと、現在作業中の内容を共有したい場合もあるでしょう。Vagrantには、vagrant shareにより、これをサポートする方法が組み込み済みです。しかし、この方法はHomestead.yamlファイルに複数サイトを設定している場合には動作しません。この問題を解決するため、Homesteadは独自のshareコマンドを持っています。使用を開始するには、vagrant sshによりHomesteadマシンとSSH接続し、share homestead.testを実行してください。これにより、Homestead.yaml設定ファイルのhomestead.testサイトが共有されます。もちろん、homestead.testの代わりに他の設定済みサイトを指定できます。
share homestead.test
とあるので、上記の通りshare whois.test
とか打った所以下の様な画面が表示されました。
vagrant@homestead:~$ share whois.test ngrok by @inconshreveable (Ctrl+C to quit) Session Status connecting Session Status online Sesion Expires 7 hours, 58 minutes Versionerface 2.2.8 10:4040 R gio United States (us) Web Interface http://192.168.10.10:4040t5 p50 p90 Forwarding http://a3e2a379.ngrok.io -> localhost:80 Forwarding https://a3e2a379.ngrok.io -> localhost:80 Connections ttl opn rt1 rt5 p50 p90 0 1 0.00 0.00 0.00 0.00 HTTP Requests ------------- GET /favicon.ico 200 OK
しかし、望みのサイトはip 192.168.11.99 や 192.168.10.10:4040t5 のいずれにアクセスしても他のアプリがでてきたり、アクセスできなかったりでした。
また、コンソールに表示されたhttps://a3e2a379.ngrok.io
にもアクセスしましたが、cssが適用されない画面であるうえ、レスポンスが大変遅く意図したもにはなりえません。
という事で困ったなー。でも外部から見た際に複数アプリがある場合、そもそもどういう法則で、今のアプリが表示されるんだろう?と悩ませている際にたまたま閃いて試したらドンピシャだったという訳です。
でもいずれ複数のアプリで外部PCからアクセスしたくなった場合は…。
これ以上良い方法がない場合はいちいちyamlフィアル書き換えるって事で対処します。