20190215のvue.jsに関する記事は6件です。

Vue.jsで扱うBootstrap4

Vue.jsでBootstarap4をいじってみよう!

タイトルの通りVue.jsのプロジェクトにBootstrapを導入しよう、使ってみよう!という記事です。昨今のVue界隈ではみなElementUIを使うっぽいですが自分は普通にVueでもいつも通りBootstrapを使いたかったので導入してみることにしました。

 非常にわかりやすい公式ドキュメントが存在するのですが英語で初心者にはとっつきにくいかなと思ったので公式ドキュメントの和訳もかねています。

以下の点に注意してください。
※今回はNuxt.jsについては触れていません
※VueやJavaScript, node.js, npm, yarn, webpack, bootstrapなどをそもそも知らないよ!という方向けの記事ではないです。その辺は他のめちゃくちゃわかりやすい記事や公式ドキュメントが存在するのでそちらを参考にしてください。

1. 導入編

導入方法としては二つ存在します。すでに作成したプロジェクト(webpack)に導入する方法と、vue-cliを用いてすでに導入済みのプロジェクトのテンプレートを作成する方法です。適宜使う方を参考にしてください。

Webpack

もしプロジェクトでwebpackを用いているならこの方法で導入できます(他のモジュールバンドラでは未検証)

①npmかyarnを用いてbootstrap-vueをインストールします。
以下のコマンドを入力してください。

bashなどのCLI
npm install bootstrap-vue

yarn add bootstrap-vue

②インストールが完了したらBootstrapVue プラグインを読み込みます。

src/index.js
import Vue from 'vue'
import BootstrapVue from 'bootstrap-vue'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'

Vue.use(BootstrapVue)

ちなみに、自分はESlintのStandardを使っているのでセミコロンは勝手に消しちゃいました。

③webpackの設定をします。以下の二つをインストールしてください。
webpack公式の解説はこちらを見て下さい。

bash
npm install --save-dev style-loader css-loader

④最後にwebpackの設定に以下を追記してください。

webpack.config.js
  const path = require('path');

  module.exports = {
    entry: './src/index.js',
    output: {
      filename: 'bundle.js',
      path: path.resolve(__dirname, 'dist')
    },
// ここから
    module: {
      rules: [
        {
          test: /\.css$/,
          use: [
            'style-loader',
            'css-loader'
          ]
        }
      ]
    }
// ここまで
  };

vue-cli

!注意!
公式ドキュメントにはvue-cliと書いてありますが、最新版は@vue/cliと名称を変更しています。また、vue init コマンドを実行するには@vue/cli-initをインストールする必要があるます。こちらも別途公式ドキュメントを参照して下さい。

vue initが実行できる環境であればあとはコマンドを実行するだけです。
eslintの設定などはお好みで選択してください。

bash
vue init bootstrap-vue/webpack-simple my-project
# Change into the directory
cd my-project
# Install dependencies
npm i
# Fire up the dev server with HMR
npm run dev

このときもちろんwebpack-simpleだけでなくwebpackテンプレートも使えます。

bash
vue init bootstrap-vue/webpack my-project

導入編は以上になります。npm run devできちんと機能してるか確認しておきましょう。

App.vue
<template>
  <div id="app">
    <h1>Home</h1>
    <a href="#">bootstrap導入成功</a>
    <b-alert show>Default Alert</b-alert>
    <b-alert variant="danger"
             dismissible
             :show="showDismissibleAlert"
             @dismissed="showDismissibleAlert=false">
      Dismissible Alert!
    </b-alert>
  </div>
</template>

<script>

export default {
  name: 'App'
}
</script>

<style>
html, body {
  width: 100vw;
  margin: 0;
}
</style>

成功時
seikou.png

失敗時
fail.png

無事導入できましたでしょうか?
続いてサンプルを紹介して実際にコンポーネントを使ってみよう、と言いたいところですがかなり長くなってしまいそうなので公式ドキュメントの和訳は以下の記事にまとめまることにします!必要に応じて見てみてください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.jsでよくある初心者の落とし穴にハマった記録

公式ドキュメントに書いてある落とし穴にしっかりとハマってきたので記録を残しておきます。

Viewが更新されない

Vueインスタンスのデータとして配列を持っているとします。

その配列の値をメソッドなどから下記のように直接インデックスを指定して更新します。

this.array[i] = 123;

この更新はDOMに反映されません!

公式ドキュメントのこちらにしっかりと記載されていました。

反映させたい場合

配列のインデックスを指定して要素の値を更新し、DOMにも反映させたい場合はarray.$set(index, value)メソッドが使えます。

他にも配列の変化を検出する方法はこちらに記載されています。

参考

よくある初心者の落とし穴

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.jsでよくある初心者の落とし穴にハマった話

公式ドキュメントに書いてある落とし穴にしっかりとハマってきたので記録を残しておきます。

Viewが更新されない

Vueインスタンスのデータとして配列を持っているとします。

その配列の値をメソッドなどから下記のように直接インデックスを指定して更新します。

this.array[i] = 123;

この更新はDOMに反映されません!

公式ドキュメントのこちらにしっかりと記載されていました。

反映させたい場合

配列のインデックスを指定して要素の値を更新し、DOMにも反映させたい場合はarray.$set(index, value)メソッドが使えます。

他にも配列の変化を検出する方法はこちらに記載されています。

参考

よくある初心者の落とし穴

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vueテンプレート構文:条件で属性を付けたり外したり【備忘録】

属性値のバインディングはわかるけど、
属性自体の切替はどうやるんだと迷ったので備忘録。

やりたいこと

  • blankフラグがfalseのときはtarget属性無し
  • blankフラグがtrueのときはtarget属性有り
template.vue
<template lang="pug">
.component
  template(v-for="item in linkList")
    // blankフラグがfalseのときはtarget属性なし
    a(:href="item.href")
      |{{item.text}}
    // blankフラグがtrueのときはtarget="_blank"
    a(:href="item.href" target="_blank")
      |{{item.text}}
</template>
<script>
export default {
  data(){
    return {
      linkList: [
       {
         text: "同窓リンク",
         href: "hoge.hoge",
         blank: false
       },
       {
         text: "別窓リンク",
         href: "fuga.fuga",
         blank: true
       },
      ]
    }
  }
}
</script>

やり方

三項演算子を使って不一致時にfalsenullを渡してあげると属性ごと消えてくれた

属性が一個だけの時

OK.vue
<template lang="pug">
.component
  template(v-for="item in linkList")
    a(:href="item.href" :target="item.blank? '_blank':false")
      |{{item.text}}
</template>

OK.html
<a href="hoge.hoge">同窓リンク</a>
<a href="fuga.fuga" target="_blank">別窓リンク</a>

属性が複数の時

v-bindの引数を省略すると複数の属性をオブジェクトで渡せるので、
rel="noopener noreferrer"もつけたりとか、複数の属性をつけるならこっちが楽

OK.vue
<template lang="pug">
.component
  template(v-for="item in linkList")
    a(:href="item.href" v-bind="item.blank? {target:'_blank',rel:'noopener noreferrer'}:false")
      |{{item.text}}
</template>

OK.html
<a href="hoge.hoge">同窓リンク</a>
<a href="fuga.fuga" target="_blank" rel="noopener noreferrer">別窓リンク</a>

NGな例

最初なにも考えずにクラスバインディングのノリでオブジェクト構文で書いてみたらダメだった
オブジェクトを渡せるのはv-bind(引数省略時)と、v-bind:classv-bind:styleのみ
https://jp.vuejs.org/v2/api/#v-bind

NG.vue
<template lang="pug">
.component
  template(v-for="item in linkList")
    a(:href="item.href" :target="{_blank:item.blank}")
      |{{item.text}}
</template>

NG.html
<a href="hoge.hoge" target="[object Object]">同窓リンク</a>
<a href="fuga.fuga" target="[object Object]">別窓リンク</a>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

STEP14:Laravel5.7 + Vue2.5 で操作ログを自動記録する

Laravel のミドルウェアを使って操作のログを自動で記録してみます

操作ログはデータベースへ保存して、ログイン済みであればユーザIDも自動で記録

記録した操作ログを一覧表示する画面も作成しときます

ログテーブル作成

まずは操作(アクション)ログを記録するテーブルを作成します

php artisan make:migration create_actlogs_table --create=actlogs
database/migrations/*actlogs*.php
<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateActlogsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('actlogs', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('user_id') -> unsigned() -> nullable() -> comment("ユーザID");
            $table->string('route') -> nullable() -> comment("route.webで設定した名称");
            $table->string('url') -> nullable() -> comment("要求Path");
            $table->string('method') -> nullable() -> comment("要求メソッド Get Post");
            $table->integer('status') -> unsigned() -> nullable() -> comment("要求結果 200 OK とか 301 move 等");
            $table->text('data') -> nullable() -> comment("要求内容(暗号化して保存)");
            $table->string('remote_addr') -> nullable() -> comment("クライアントIPアドレス");
            $table->string('user_agent') -> nullable() -> comment("ブラウザ名");
            $table->timestamps();

            $table->index(['created_at']);
            $table->index(['user_id']);
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('actlogs');
    }
}

ログに記録するのは以下
 ・ユーザID
 ・ページ名(routes/web.php の name でつけといた名前)
 ・要求パス
 ・要求メソッド(GETとかPOSTとか)
 ・要求結果 200 OK とか 301 Move とか
 ・要求内容(あれば)
 ・接続元 IPアドレス
 ・ブラウザ名(UserAgent)
 ・作成時間(DB記録時間)

定義から実テーブルの作成もしときます

php artisan migrate:refresh --seed

テーブルに対応するモデルも作成

app/Actlog.php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Crypt;

class Actlog extends Model
{
    // 更新日時記録無効化
    const UPDATED_AT = null;

    // Jsonに追加で含める
    protected $appends = ['name'];

    // ユーザの氏名を取得 -- リレーションできなかった場合は空文字を返す
    public function getNameAttribute()
    {
        $user = $this -> user;
        if ($user) return $user -> name;
        else       return '';
    }

    // カラム暗号化 - 要求内容は暗号化して保存する
    public function setDataAttribute($value)
    {
        if ($value) {
            $this->attributes['data'] = Crypt::encrypt( serialize($value) );
        }
    }

    // カラム複合化 - 要求内容を取り出すときに複合化する
    public function getDataAttribute($value)
    {
        if ($value) {
            return unserialize( Crypt::decrypt($value) );
        } else {
            return $value;
        }
    }

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'user_id',
        'route',
        'url',
        'method',
        'status',
        'data',
        'remote_addr',
        'user_agent',
    ];

    // Json に出力する項目
    protected $visible = [
        'user_id',
        'route',
        'url',
        'method',
        'status',
        'data',
        'remote_addr',
        'user_agent',
        'created_at',
        'name',
    ];

    public function user()
    {
        return $this -> belongsTo('App\User', 'user_id', 'id');
    }
}

通信内容( 'data' )は暗号化して保存
あと、UPDATEはしないので記録しないように設定しときます
一覧で見やすいようにユーザ名( 'name' )も返すようにしてあります

ミドルウェア作成

実際にログを記録するミドルウェアを作成です

$ php artisan make:middleware ActlogMiddleware
app/Http/Middleware/ActlogMiddleware.php
<?php

namespace App\Http\Middleware;

use Closure;
use App\Actlog;
use \Route;

class ActlogMiddleware
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $response = $next($request);
        $this->actlog($request, $response -> getStatusCode());
        return $response;
    }

    public function actlog($request, $status)
    {
        $user = $request -> user();
        $data = [
            'user_id' => $user ? $user->id : null,
            'route' => Route::currentRouteName(),
            'url' => $request -> path(),
            'method' => $request -> method(),
            'status' => $status,
            'data' => count($request->toArray()) != 0 ? json_encode($request->toArray()) : null,
            'remote_addr' => $request -> ip(),
            'user_agent' => $request -> userAgent(),
        ];
        Actlog::create($data);
    }
}

ルーティング情報を取得するために Route を利用してます
ログインユーザは$request -> user() から取得
要求内容( 'data' )はJson形式で保存しときます

ミドルウェア登録

app/Http/Kernel.php
    protected $middleware = [
        ~~~~

      \App\Http\Middleware\ActlogMiddleware::class,
    ];

    ~~~~

$middleware の中に追加すると グローバルとして必ず呼ばれるミドルウェアとして登録されるみたいです
詳しくはこちら
https://readouble.com/laravel/5.6/ja/middleware.html

ログアウト対応

このミドルウェアの実行タイミングは各処理が「終わった後」になるので「誰」がログアウトしたのか操作者のIDが記録できません
なのでログアウトの実行前にログを記録するようにしておきます

ログアウトを実際に行うのは 「 vendor/laravel/framework/src/Illuminate/Foundation/Auth/AuthenticatesUsers.php 」のようなので
ログアウト操作をこっち「 app/Http/Controllers/Auth/LoginController.php 」で上書きしてやります

app/Http/Controllers/Auth/LoginController.php
<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;

class LoginController extends Controller
{
  ~~~~~~~~

    /**
     * Log the user out of the application.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function logout(Request $request)
    {
        $actlog = new \App\Http\Middleware\ActlogMiddleware;
        $actlog -> actlog($request, 999);

        $this->guard()->logout();

        $request->session()->invalidate();

        return $this->loggedOut($request) ?: redirect('/');
    }
}

実際にログアウトを実行する「$this->guard()->logout();」前に、actlogを記録してやります
見分けやすいようにステータスを 「999」で記録しときます

動作確認

1)DBに接続してテーブル定義とテーブル内容を確認

mysql -u lara_user -p lara

mysql> desc actlogs;
+-------------+------------------+------+-----+---------+----------------+
| Field       | Type             | Null | Key | Default | Extra          |
+-------------+------------------+------+-----+---------+----------------+
| id          | int(10) unsigned | NO   | PRI | NULL    | auto_increment |
| user_id     | int(10) unsigned | YES  | MUL | NULL    |                |
| route       | varchar(255)     | YES  |     | NULL    |                |
| url         | varchar(255)     | YES  |     | NULL    |                |
| method      | varchar(255)     | YES  |     | NULL    |                |
| status      | int(10) unsigned | YES  |     | NULL    |                |
| data        | text             | YES  |     | NULL    |                |
| remote_addr | varchar(255)     | YES  |     | NULL    |                |
| user_agent  | varchar(255)     | YES  |     | NULL    |                |
| created_at  | timestamp        | YES  | MUL | NULL    |                |
| updated_at  | timestamp        | YES  |     | NULL    |                |
+-------------+------------------+------+-----+---------+----------------+

mysql> SELECT  *  FROM   actlogs;
Empty set (0.00 sec)

2)ログイン画面表示

ブラウザでログイン画面を開きます

mysql> SELECT  *  FROM   actlogs;
+----+---------+------------+-------+--------+--------+------+-------------+--------------------------------------------------------------------------------------------------------------------+---------------------+------------+
| id | user_id | route      | url   | method | status | data | remote_addr | user_agent                                                                                                         | created_at          | updated_at |
+----+---------+------------+-------+--------+--------+------+-------------+--------------------------------------------------------------------------------------------------------------------+---------------------+------------+
|  1 |    NULL | NULL       | /     | GET    |    302 | NULL | 172.16.0.1  | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36 | 2019-02-08 07:42:45 | NULL       |
|  2 |    NULL | login.show | login | GET    |    200 | NULL | 172.16.0.1  | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36 | 2019-02-08 07:42:45 | NULL       |
+----+---------+------------+-------+--------+--------+------+-------------+--------------------------------------------------------------------------------------------------------------------+---------------------+------------+
2 rows in set (0.01 sec)

最初 / にアクセスして 302で遷移
ログイン画面(login.show)を表示したログが記録されてます

3)ログイン実行

ブラウザでログイン操作

+----+---------+------------+-------+--------+--------+----------------------------------------------------------------------+-------------+--------------------------------------------------------------------------------------------------------------------+---------------------+------------+
| id | user_id | route      | url   | method | status | data                                                                 | remote_addr | user_agent                                                                                                         | created_at          | updated_at |
+----+---------+------------+-------+--------+--------+----------------------------------------------------------------------+-------------+--------------------------------------------------------------------------------------------------------------------+---------------------+------------+
|  1 |    NULL | NULL       | /     | GET    |    302 | NULL                                                                 | 172.16.0.1  | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36 | 2019-02-08 07:42:45 | NULL       |
|  2 |    NULL | login.show | login | GET    |    200 | NULL                                                                 | 172.16.0.1  | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36 | 2019-02-08 07:42:45 | NULL       |
|  3 |       1 | login      | login | POST   |    302 | eyJpdiI6IjVmNE5UZ2ExSUw2QWYrTVh6ZUpqRnc9PSIsInZhbHVlZTRjYzAwZGI0NCJ9 | 172.16.0.1  | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36 | 2019-02-08 07:44:28 | NULL       |
|  4 |       1 | NULL       | /     | GET    |    200 | NULL                                                                 | 172.16.0.1  | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36 | 2019-02-08 07:44:28 | NULL       |
+----+---------+------------+-------+--------+--------+----------------------------------------------------------------------+-------------+--------------------------------------------------------------------------------------------------------------------+---------------------+------------+
4 rows in set (0.00 sec)

id:3 の行でログイン後、
id:4 / に行って Vue の画面を表示してます( routes/web.php で指定 )

要求内容もちゃんと暗号化されているようです
ユーザIDも記録されてますね

4)ページ遷移

別記事で作成済みの「社員一覧」ページを開いてみます

mysql> SELECT  *  FROM   actlogs;
+----+---------+------------------+----------------+--------+--------+----------------------------------------------------------------------+-------------+--------------------------------------------------------------------------------------------------------------------+---------------------+------------+
| id | user_id | route            | url            | method | status | data                                                                 | remote_addr | user_agent                                                                                                         | created_at          | updated_at |
+----+---------+------------------+----------------+--------+--------+----------------------------------------------------------------------+-------------+--------------------------------------------------------------------------------------------------------------------+---------------------+------------+
|  1 |    NULL | NULL             | /              | GET    |    302 | NULL                                                                 | 172.16.0.1  | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36 | 2019-02-08 07:42:45 | NULL       |
|  2 |    NULL | login.show       | login          | GET    |    200 | NULL                                                                 | 172.16.0.1  | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36 | 2019-02-08 07:42:45 | NULL       |
|  3 |       1 | login            | login          | POST   |    302 | eyJpdiI6IjVmNE5UZ2UxOWRhMzhjNDdiN2I4NTAxNDcwMDZlODVlZTRjYzAwZGI0NCJ9 | 172.16.0.1  | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36 | 2019-02-08 07:44:28 | NULL       |
|  4 |       1 | NULL             | /              | GET    |    200 | NULL                                                                 | 172.16.0.1  | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36 | 2019-02-08 07:44:28 | NULL       |
|  5 |       1 | admin.user.index | api/admin/user | POST   |    200 | NULL                                                                 | 172.16.0.1  | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36 | 2019-02-08 07:47:39 | NULL       |
+----+---------+------------------+----------------+--------+--------+----------------------------------------------------------------------+-------------+--------------------------------------------------------------------------------------------------------------------+---------------------+------------+

id:5 でadmin.user.index ページを開いたログが記録されてます

5)ログアウト

ログアウト操作をしてみます

|  5 |       1 | admin.user.index | api/admin/user | POST   |    200 | NULL                                                                 | 172.16.0.1  | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36 | 2019-02-08 07:47:39 | NULL       |
|  6 |       1 | logout           | logout         | POST   |    999 | NULL                                                                 | 172.16.0.1  | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36 | 2019-02-08 07:50:28 | NULL       |
|  7 |    NULL | logout           | logout         | POST   |    302 | NULL                                                                 | 172.16.0.1  | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36 | 2019-02-08 07:50:28 | NULL       |
|  8 |    NULL | NULL             | /              | GET    |    401 | NULL                                                                 | 172.16.0.1  | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36 | 2019-02-08 07:50:28 | NULL       |
|  9 |    NULL | NULL             | /              | GET    |    302 | NULL                                                                 | 172.16.0.1  | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36 | 2019-02-08 07:50:28 | NULL       |
| 10 |    NULL | login.show       | login          | GET    |    200 | NULL                                                                 | 172.16.0.1  | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36 | 2019-02-08 07:50:28 | NULL       |
+----+---------+------------------+----------------+--------+--------+----------------------------------------------------------------------+-------------+--------------------------------------------------------------------------------------------------------------------+---------------------+------------+
10 rows in set (0.00 sec)

id:6 で実際のログアウト処理の前にログアウトを記録(999)して
id:7 で実際にログアウト
id:8 で 401(未認証)だから / に行って
id:9 で / から 302 でリダイレクトして
id:10 で ログイン画面を表示

ってことになってますね
ちゃんと操作のログが記録されているようです

Vue 操作ログの一覧画面

ログ記録の確認が取れたので、画面側も作っておきます

resources/js/components/Admin/ActlogComponent.vue
<template>
  <v-flex>
    <v-card xs12 class="m-3 px-3">

      <v-card-title class="title">
        <v-icon class="pr-2">{{ $route.meta.icon }}</v-icon> {{ $route.meta.name }} {{ /* 操作ログ */ }}
        <v-spacer></v-spacer>
        <v-spacer></v-spacer>
        <v-text-field
          v-model="search"
          prepend-icon="search"
          label="Search"
          single-line
          hide-details
          clearable
        ></v-text-field>
      </v-card-title>

      <v-data-table
        :headers="headers"
        :items="tabledata"
        :pagination.sync="pagination"
        :rows-per-page-items='[10,25,50,{"text":"All","value":-1}]'
        :loading="loading"
        :search="search"
        class="elevation-0 p-1"
      >
        <v-progress-linear slot="progress" color="blue" indeterminate></v-progress-linear>

        <template slot="items" slot-scope="props">
          <tr>
            <td class="text-xs-center" xs1>{{ (props.index + 1) + (pagination.page - 1) * pagination.rowsPerPage }}</td>
            <template v-for="n in (headers.length - 1)">
              <td :class="'text-xs-' + headers[n].align" style="white-space: nowrap;" v-text="props.item[headers[n].value]"></td>
            </template>
          </tr>
        </template>
      </v-data-table>

      <v-spacer></v-spacer>

      <v-card-actions>
        <csv-download url="/api/admin/actlog/download" color="primary" @axios-logout="$emit('axios-logout')"></csv-download>
      </v-card-actions>
    </v-card>
  </v-flex>
</template>

<script>
  import csv_download from './CsvDownload.vue'

  export default {
    name: 'ActlogComponent',

    components: {
      'csv-download': csv_download,
    },

    props: {
    },

    data: () => ({
      loading: false,
      search: '',
      pagination: { sortBy: 'created_at', descending: true, },

      tabledata: [],
      headers: [
        { align: 'center', sortable: false, text: 'No',       },
        { align: 'left',   sortable: true,  text: '日時',     value: 'created_at' },
        { align: 'left',   sortable: true,  text: '氏名',     value: 'name' },
        { align: 'left',   sortable: true,  text: '操作',     value: 'action' },
        { align: 'center', sortable: true,  text: '結果',     value: 'status' },
        { align: 'left',   sortable: true,  text: 'データ',   value: 'data' },
        { align: 'left',   sortable: true,  text: 'IP',     value: 'remote_addr' },
        { align: 'left',   sortable: true,  text: 'UA',     value: 'user_agent' },
      ],
    }),

    created() {
      if (process.env.MIX_DEBUG) console.log('Actlog Component created.')
      this.initialize()
    },

    methods: {
      initialize() {
        this.getData()
      },

      getData() {
        if (process.env.MIX_DEBUG) console.log('Actlog Component getData')
        this.loading = true
        axios.post('/api/admin/actlog')

        .then( function (response) {
          this.loading = false
          if (process.env.MIX_DEBUG) console.log(response)
          if (response.data.data) {
            this.tabledata = this.setAction(response.data.data)
          }
        }.bind(this))

        .catch(function (error) {
          this.loading = false
          console.log(error)
          if (error.response && [401, 419].includes(error.response.status)) {
            this.$emit('axios-logout')
          }
        }.bind(this))
      },

      setAction(data) {
        if (process.env.MIX_DEBUG) console.log('Actlog Component setAction')
        var wk = ''
        for (let i=0; i<data.length; i++) {
          switch (data[i].route) {

            // Login - Logout
            case 'show.login' : wk = 'ログイン画面'; break;
            case 'login' :      wk = 'ログイン'; break;
            case 'logout' :     wk = 'ログアウト'; break;

            // USER
            case 'admin.user.index' :    wk = '社員一覧'; break;
            case 'admin.user.store' :    wk = '社員追加'; break;
            case 'admin.user.destroy' :  wk = '社員削除'; break;
            case 'admin.user.download' : wk = '社員CSV_DL'; break;
            case 'admin.user.upload' :   wk = '社員CSV_UP'; break;

            // OTHER
            default: wk = data[i].route

          }
          data[i].action = wk
        }
        return data
      },
    },
  }
</script>

一覧を取ってきて、route名をわかりやすい日本語にして表示
ってことをしています

操作履歴はデータ量が多そうなので、取得範囲やら「誰」やらの検索条件指定などもそのうち追加しなくちゃですかね

Vue ナビゲーション追加

ページを追加したのでナビゲーションも追加しときます

まずは定義ファイル

resources/js/router/index.js
        import Vue from 'vue'
        import Router from 'vue-router'
        Vue.use(Router)

        //
        import example_component from '../components/ExampleComponent.vue'
        import admin_component   from '../components/AdminComponent.vue'
        import r_link            from '../components/RouterLink.vue'

        //
        Vue.component('example-component', example_component)
        Vue.component('admin-component', admin_component)
        Vue.component('r-link', r_link)

        //
        import home              from '../components/HomeComponent.vue'
        import admin_user        from '../components/Admin/UserComponent.vue'
        import admin_payslip     from '../components/Admin/PayslipComponent.vue'
     import admin_actlog      from '../components/Admin/ActlogComponent.vue'

        export default new Router({
          mode: 'history',
          routes: [
            { path: '/admin/user',   name: 'admin_user',    component: admin_user,    meta: {name: '社員管理', icon: 'supervisor_account'}},
            { path: '/home',         name: 'home',          component: home,          meta: {name: 'ホーム',   icon: 'home'}},
            { path: '/admin/payslip',name: 'admin_payslip', component: admin_payslip, meta: {name: '給与明細', icon: 'fa-file-invoice-dollar'}},
         { path: '/admin/actlog', name: 'admin_actlog',  component: admin_actlog,  meta: {name: '操作履歴', icon: 'list'}},
            { path: '*',             redirect: '/home' },
          ],
        })

そしてリンク埋め込み

resources/js/components/AdminComponent.vue
<template>
  <v-app id="app">
    <v-navigation-drawer v-model="drawer" clipped fixed app >
      <v-list dense>
        <r-link linkname='home'></r-link>
        <r-link linkname='admin_user'></r-link>
        <r-link linkname='admin_payslip'></r-link><r-link linkname='admin_actlog'></r-link>
      </v-list>
    </v-navigation-drawer>

    <v-toolbar color="primary" dark fixed app clipped-left>
~~~~

Laravel 操作ログのコントローラ

クライアント(Vue)からの一覧表示要求に答えるコントローラを追加しときます
ついでにCSV ダウンロード機能も

app/Http/Controllers/ActlogController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use App\Actlog;
use App\Facades\Csv;

class ActlogController extends Controller
{
    public function index()
    {
        Log::Debug(__CLASS__.':'.__FUNCTION__);

        $actlog = Actlog::where('user_id', '>', '0')
            -> whereNotNull('route')
            -> orderby('created_at', 'desc')
            -> limit(100)
            -> get();
        return ['data' => $actlog];
    }


    public function download(Request $request)
    {
        Log::Debug(__CLASS__.':'.__FUNCTION__, $request->all());

        // 取得項目設定
        $head = ['created_at', 'route', 'status', 'remote_addr', 'user_agent', 'user_id'];

        // 抽出
        $data = Actlog::select( $head )
            -> where('user_id', '>', '0')
            -> whereNotNull('route')
            -> orderby('created_at', 'desc')
            -> get()
            -> toArray();

        // 自動付与の名前をヘッダーに追加
        $head[] = 'name';

        // CSV DOWNLOAD
        return Csv::download($data, $head, 'test.csv');
    }
}

ログイン後の操作履歴( user_id > 0 )だけを表示するようにしてます
さらに正規の要求(whereNotNull route)の履歴だけを対象に
ついでに直近100件(limit 100)のみを表示対象としときます

Laravel ルーティング設定

コントローラを追加したのでルーティングも設定

routes/web.php
<?php

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Route::get('/', function () {
    return view('home');
})->middleware('auth');

// Authentication Routes...
Route::get('/login',   'Auth\LoginController@showLoginForm') -> name('login.show');
Route::post('/login',  'Auth\LoginController@login')         -> name('login');
Route::post('/logout', 'Auth\LoginController@logout')        -> name('logout');

// Admin
Route::GROUP(['middleware' => ['auth', 'can:admin']], function() {
    Route::prefix('/api/admin/') -> name('admin.') -> group(function() {

        // USER
        Route::name('user.') -> group(function() {
            Route::post('user',          'UserController@index')    -> name('index');
            Route::post('user/store',    'UserController@store')    -> name('store');
            Route::post('user/destroy',  'UserController@destroy')  -> name('destroy');
            Route::post('user/download', 'UserController@download') -> name('download');
            Route::post('user/upload',   'UserController@upload')   -> name('upload');
        });

        // CsvPayslip
        Route::name('csvpayslip.') -> group(function() {
            Route::post('csvpayslip/index',   'CsvPayslipController@index')   -> name('index');
            Route::post('csvpayslip/upload',  'CsvPayslipController@upload')  -> name('upload');
            Route::post('csvpayslip/delete',  'CsvPayslipController@delete')  -> name('delete');
            Route::post('csvpayslip/publish', 'CsvPayslipController@publish') -> name('publish');
        });

        // Payslip
        Route::name('payslip.') -> group(function() {
            Route::post('payslip/index',  'PayslipController@index')  -> name('index');
            Route::post('payslip/delete', 'PayslipController@delete') -> name('delete');
            Route::post('payslip/pdf',    'PayslipController@pdf')    -> name('pdf');
        });

      // Actlog
        Route::name('actlog.') -> group(function() {
            Route::post('actlog',          'ActlogController@index')    -> name('index');
            Route::post('actlog/download', 'ActlogController@download') -> name('download');
        });
    });
});

// Other
Route::get('/{any}', function () {
  return view('home');
})->middleware('auth')->where('any', '.*');

一覧画面動作確認

a3.png


以上
ミドルウェアとして登録したことで処理系には一切手を入れずにアクションログを自動で記録できるようになりました

Laravel ステキ

今回もソースはこちら
https://github.com/u9m31/u9m31/tree/step14

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue on RailsでActive Storageを使って画像を保存する

はじめに

Rails API モードと Vue.js で作成した 自作ブログ で Active Storage を使う際に、画像の受け渡しでハマったので実装方法を残します。
実装するのは Active Storage を使って、eyecatch (アイキャッチ画像) 付きの Post (記事) を投稿できるようなサンプルです。

環境

$ ruby -v
ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-darwin16]

$ bundle exec rails --version
Rails 5.2.2

実装方法

API 部分 (Rails)

Ⅰ. サンプルアプリケーション新規作成

Rails API モードで作成します。

$ bundle exec rails new sampleapp --database=mysql --api --force
$ cd sampleapp/
$ bundle exec rails db:create

Ⅱ. Active Storage インストール

Active Storage をインストールします。

$ bundle exec rails active_storage:install
$ bundle exec rails db:migrate

Ⅲ. Post リソースの作成

今回利用する Post モデルとコントローラを作成します。

$ bundle exec rails g resource post title body:text
$ bundle exec rails db:migrate

Ⅳ. 各種ファイルの修正

各 Post に eyecatch を設定できるようにモデルを修正します。
追加する eyecatch= メソッドで、Active Storage に画像を保存します。

このメソッドは、Base64 形式で受け取った image データをエンコードし、一時的に /tmp 配下に画像ファイルを作成、作成した画像ファイルをアタッチ、その後画像ファイルを削除します。

app/models/post.rb
class Post < ApplicationRecord
  has_one_attached :eyecatch
  attr_accessor :image

  def eyecatch=(image)
    if image.present?
      prefix = image[/(image|application)(\/.*)(?=\;)/]
      type = prefix.sub(/(image|application)(\/)/, '')
      data = Base64.decode64(image.sub(/data:#{prefix};base64,/, ''))
      filename = "#{Time.zone.now.strftime('%Y%m%d%H%M%S%L')}.#{type}"
      File.open("#{Rails.root}/tmp/#{filename}", 'wb') do |f|
        f.write(data)
      end
      eyecatch.detach if eyecatch.attached?
      eyecatch.attach(io: File.open("#{Rails.root}/tmp/#{filename}"), filename: filename)
      FileUtils.rm("#{Rails.root}/tmp/#{filename}")
    end
  end
end

モデルで追加した eyecatch= メソッドに POST で受け取る画像のパラメータを渡すように修正します。

app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def create
    post = Post.new(post_params)

    if post.save
      post.eyecatch = post_params[:image]
      render json: post, status: :created
    else
      render json: post.errors, status: :unprocessable_entity
    end
  end

  private

  def post_params
    params.require(:post).permit(:title, :body, :image)
  end
end

Ⅴ. 動作確認

アプリケーションを起動し、curl コマンドでアイキャッチ付き Post が作成できるか確認します。

$ bundle exec rails s

$ curl -v -H "Accept: application/json" -H "Content-type: application/json" -X POST -d '{"post": {"title": "Sample title.", "body": "Sample body.", "image": "data:application/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAKCAYAAACALL/6AAAACXBIWXMAADXU\nAAA11AFeZeUIAAAAgUlEQVQYlZWQMQ6DMAxFnyMGJFZCc4be/yisRYyVmDN0\niTuQULCCoH9z/PLtb/hTsi8SaA1yO85ZWHxgAqb8HuVoJAX+iKPtB17L++D+\nyN6drpOa0mj7AYBg1pmz99NmSKDiVzzmKTM/uOTYMjgQNetYuKoEqj7oCHp2\nteqn2/CVvuDZJy0n3DrVAAAAAElFTkSuQmCC\n"}}' http://localhost:3000/posts

curl コマンドで作成後、Rails Console でアイキャッチが正常に追加できているか確認します。

$ bundle exec rails c
irb(main):001:0> Post.find_by(title: "Sample title.").eyecatch.attached?
=> true

# 画像のパスは以下のように取得できます。
# irb(main):002:0> app.url_for(Post.find_by(title: "Sample title.").eyecatch)
# => "画像のパス"

フロント部分 (Vue.js)

Ⅰ. Webpacker をインストール

webpacker gem を追加する。

Gemfile
gem 'webpacker', '~> 3.5'

Webpacker をインストールします。

$ bundle
$ bundle exec rails webpacker:install

Ⅱ. Vue.js をインストール

Webpacker で Vue.js をインストールします。

$ bundle exec rails webpacker:install:vue

Ⅲ. Home ページを作成

Vue.js を返すための Home ページを作成します。

$ bundle exec rails g controller Pages Home

Root パスに Home ページを設定します。

config/routes.rb
Rails.application.routes.draw do

  root 'pages#home'

end

標準の JSON ではなく、ERB を返すために ActionController::Base に修正します。

app/controllers/pages_controller.rb
class PagesController < ActionController::Base
  def home
  end
end

Home ページに利用する View を作成します。

$ mkdir -p app/views/pages/
$ touch app/views/pages/home.html.erb

Ⅳ. Vue.js を利用

Vue.js を利用するための設定を行います。

app/views/pages/home.html.erb
<%= javascript_pack_tag 'main' %>

利用する各種ファイルを作成します。

$ touch app/javascript/packs/main.js
$ touch app/javascript/packs/App.vue
app/javascript/packs/main.js
import Vue from 'vue'
import App from './App.vue'

document.addEventListener('DOMContentLoaded', () => {
  const el = document.body.appendChild(document.createElement('main'))
  new Vue({
    el,
    render: h => h(App)
  })
})
app/javascript/packs/App.vue
<template>
  <div id="app">
    <p>投稿フォーム</p>
  </div>
</template>

Ⅴ. 投稿フォームを作成

API コールに利用する axios をインストールします。

$ yarn add axios

画像投稿するフォームを用意します。
画像は POST する前に Base64 にデコードしています。

app/javascript/packs/App.vue
<template>
  <div id="app">
    <p>投稿フォーム</p>
    <form v-on:submit.prevent="postItem()">
      <p>
        <label>Title</label>
        <input name="post.title" type="text" v-model="post.title"><br />
      </p>
      <p>
        <label>Body</label>
        <input name="post.body" type="text" v-model="post.body"><br />
      </p>
      <p>
        <label>画像</label>
        <input name="uploadedImage" type="file" ref="file" v-on:change="onFileChange()"><br />
      </p>
      <input type="submit" value="Submit">
    </form>
  </div>
</template>

<script>
import axios from 'axios'

export default {
  data() {
    return {
      post: {},
      uploadedImage: ''
    }
  },
  methods: {
    onFileChange() {
      let file = event.target.files[0] || event.dataTransfer.files
      let reader = new FileReader()
      reader.onload = () => {
        this.uploadedImage = event.target.result
        this.post.image = this.uploadedImage
      }
      reader.readAsDataURL(file)
    },
    postItem() {
      return new Promise((resolve, _) => {
        axios({
          url: '/posts',
          data: {
            post: this.post
          },
          method: 'POST'
        }).then(res => {
          this.post = {}
          this.uploadedImage = ''
          this.$refs.file.value = ''
          resolve(res)
        }).catch(e => {
          console.log(e)
        })
      })
    }
  }
}
</script>

Ⅵ. 動作確認

アプリケーションを起動し、投稿フォームから画像を投稿します。
http://localhost:3000/

$ bundle exec rails s

img1.png

Rails Console でアイキャッチが正常に追加できているか確認します。

$ bundle exec rails c
irb(main):001:0> Post.last.eyecatch.attached?
=> true

参考記事

https://qiita.com/ozin/items/5ec81a4b126b8ebf7a96

最後に

読んでいただいてありがとうございます。
間違っている点などがありましたら、ご指摘いただけると喜びます!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む