- 投稿日:2020-02-12T23:27:40+09:00
Vue.js開発の達人への道
Vue開発を初めて使用する場合、シングルページアプリ、動的 & 非同期コンポーネント、サーバーサイドレンダリングなど、多くの専門用語が使用されていることを聞いたことがあるでしょう。
また、Vuex、Webpack、Vue CLI、Nuxtなど、Vueと一緒によく言及されるツールやライブラリについて聞いたことがあるかもしれません。
おそらく、この無数の用語とツールはフラストレーションになると思うでしょう。その中であなたは一人ではありません。すべての経験レベルの開発者は、自分が知らないことや、そうすべきだと感じているという永続的なプレッシャーを感じています。
0. JavaScriptと基本的なWeb開発
中国語で書かれた本のすべてを学ぶように頼んだら、まず中国語を読むことを学ぶ必要がありますよね?
同様に、VueはWebユーザーインターフェイスを構築するためのJavaScriptフレームワークです。 Vueを使用する前に、JavaScriptとWeb開発の基本を理解する必要があります。
FreeCodeCampでJavaScriptと基本的なWeb開発から上級な開発まで無料で学べます
https://www.freecodecamp.com/1. Vueの基本的な概念
新しいVue開発者であれば、Vueコアライブラリ、Vueルーター、Vuexを含むVue.jsエコシステムの中核に注目する必要があります。
これらのツールは、ほとんどのVueアプリで機能し、このマップの他のほとんどの領域が構築するフレームワークを提供します。
Vueのコア機能
最も基本的なVueは、WebページをJavaScriptと同期します。これを実現するための重要な機能は、リアクティブデータ、およびディレクティブやテンプレート構文などのテンプレート機能です。これらは、初日に学ぶべきことです。
最初のVueアプリを作成するには、WebページにVueをインストールする方法を知って、Vueインスタンスのライフサイクルを理解する必要もあります。
コンポーネント
Vueコンポーネントは、再利用可能な分離されたUI要素です。コンポーネントの宣言方法、およびプロパティとイベントを介してコンポーネント間の通信方法を理解する必要があります。
また、コンポーネントで構成する方法を学ぶことも重要です。これは、Vueで堅牢でスケーラブルなアプリケーションを構築するための基本です。
シングルページアプリケーション
シングルページアプリケーション(SPA)アーキテクチャにより、ユーザーが移動するたびにページを再読み込みおよび再構築するという非効率性なしに、単一のWebページを従来のマルチページWebサイトのように動作させることができます。
Vueコンポーネントとして「ページ」を作成したら、Vueチームが管理するSPAを構築するためのツールであるVue Routerを使用して、それぞれを一意のパスにマッピングできます。
2.現実世界のVue
パート1で得たすべての知識を使用して、ローカルサーバー上であっても、高性能で効率的なVueアプリを構築できます。しかし、本番環境ではどのように立ち上がるのでしょうか?
Vue.jsベースの製品を実際のユーザーに出荷する場合は、さらに知っておく必要があります!
プロジェクトの足場
Vueアプリを頻繁にビルドすると、ほとんどすべてのプロジェクトに戻ってくる設定、セットアップ、開発者ツールがあります。
Vueチームは、Vue CLIと呼ばれるツールを維持しており、これにより数分で堅牢なVue開発環境を構築できます。
フルスタック/認証済みアプリ
Real Vueアプリは通常、データ駆動型のユーザーインターフェイスです。データは多くの場合、Node、Laravel、Rails、Django、またはその他のサーバーフレームワークで作成された安全なAPIから供給されます。
おそらく、データは従来のREST APIまたはGraphQLによって提供されるか、Webソケットを介したリアルタイムデータになる可能性があります。
また、Vueをフルスタック構成に統合するために一般的に使用される設計パターン、およびVueアプリのユーザーデータを安全に保つためのさまざまな考慮事項についても理解する必要があります。
次のVueアプリに最適なバックエンドを決定しようとしている場合は、この記事をご覧ください。
テスト
本番環境で保守可能で安定したVueアプリを作成したい場合、本当にテストを提供する必要があります。
Vueアプリでは、ユニットテストにより、コンポーネントが特定の入力(小道具やユーザー入力)に対して常に同じ出力(つまり、再レンダリングされたHTMLまたは放出されたイベント)を提供することが保証されます。
Vueチームは、Vue Test Utilsと呼ばれるツールを維持します。このツールを使用すると、分離されたVueコンポーネントでテストを作成および実行できます。Vueとは特に関係ありませんが、E2Eテストはプロジェクトの堅牢性も提供します。 Vue CLI 3を介してプロジェクトに追加できる優れたE2Eツールはサイプレスです。
最適化
アプリをリモートサーバーに展開し、ユーザーが低速の接続を介してアプリにアクセスすると、開発中のテストで経験した速度と効率が得られません。
Vueアプリを最適化するために、サーバー側レンダリングを含むさまざまな手法を使用できます。これは、Vueアプリがサーバーで実行され、出力がユーザーに配信されるHTMLページにキャプチャされる場所です。
最適化のための他の手法には、非同期コンポーネントとレンダリング関数の使用が含まれます。
基本的な機能を学び、無料の2時間のビデオコースBuild Your First Vue.js Appで実際のWebアプリを構築して、Vueを使い始めましょう。3.主要な関連ツール
これまで見てきたことはすべて、Vue.jsコアまたはエコシステムのツールからのものです。しかし、Vueは単独では存在しません。フロントエンドスタックの1つのレイヤーにすぎません。
上級Vue開発者は、Vueだけでなく、すべてのVueベースのプロジェクトの一部となる主要なツールに精通している必要があります。
モダーンJavaScriptとBabel
Vueアプリは、既存のほとんどすべてのブラウザーがサポートするJavaScriptの標準であるES5で効果的に構築できます。
Vue開発エクスペリエンスを強化し、新しいブラウザー機能を活用するために、最新のJavaScript標準ES2015の機能とES2016以降の提案機能を使用してVueアプリを構築できます。
ただし、最新のJavaScriptを使用することを選択した場合は、古いブラウザーをサポートする方法が必要になります。そうしないと、ほとんどのユーザーが製品を使用できません。
これを達成するためのツールはバベルです。その仕事は、アプリを出荷する前に、最新の機能を標準機能に「トランスパイル」(翻訳およびコンパイル)することです。
Webpack
Webpackはモジュールバンドラーです。つまり、コードが異なるモジュール(たとえば、異なるJavaScriptファイル)にまたがって書かれている場合、Webpackはこれらをブラウザーで読み取り可能な1つのファイルに「ビルド」できます。
また、Webpackはビルドパイプラインとしても機能するため、Babel、Sass、TypeScriptなどを使用して、ビルドする前にコードを変換したり、一連のプラグインでアプリを最適化することもできます。
多くの開発者はWebpackを把握するのが難しく、設定するのがさらに難しいと感じていますが、Webpackがなければ、単一ファイルコンポーネントのようなVueの最高の機能の一部にアクセスできません。
最近リリースされたVue CLI 3では、VueプロジェクトでWebpackを抽象化し、自動的に構成するためのソリューションが提供されています。
これは、学ぶ必要がないということですか? Webpackの構成をカスタマイズまたはデバッグする必要がある場合が必ずあるので、私はノーと言います。TypeScript
TypeScriptは、タイプ(文字列、ブール、数値など)を含むJavaScript言語のスーパーセットです。これの目的は、堅牢なコードを記述し、バグを早期に発見できるようにすることです。
2019年に登場するVue.js 3は、すべてTypeScriptで記述されます。これは、Vueプロジェクトで使用する必要があるという意味ではありませんが、Vueに貢献してその内部動作を理解する場合は、TypeScriptを理解する必要があります。
4. Vueフレームワーク
フレームワークはVueの上に構築されているため、サーバー側のレンダリングをゼロから実装したり、独自のコンポーネントライブラリを作成したり、その他の多くの一般的なタスクを行う必要がありません。
多くの優れたVueフレームワークがありますが、ここでは最も広く使用され重要な3つのフレームワークについて説明します。
Nuxt.js
高性能のVueアプリを構築する場合は、もちろん、コンポーネントベースのルーティング、サーバー側のレンダリング、コード分割、その他の最先端機能が必要になります。また、SEOタグなどの便利な制作機能も必要になります
Nuxt.jsフレームワークは、このすぐに使用できるすべての機能と、さまざまなコミュニティプラグインを通じて、PWAなどのさらに多くの機能のオプションを提供します。Nuxt.jsサイトの良い例をご覧になりたい場合は、今すぐご覧ください?
検証
Googleのマテリアルデザイン標準は、美しく論理的なユーザーインターフェースを構築するために広く使用されているガイドラインのシステムであり、AndroidのようなGoogleの製品やWeb全体で使用されています。
Vuetifyフレームワークは、一連のVueコンポーネントでマテリアルデザインを実装します。これにより、マテリアルデザインレイアウトとスタイリングに加えて、モーダル、アラート、ナビゲーションバー、ページネーションなどのウィジェットを備えたVueアプリをすばやく構築できます。
NativeScript-Vue
Vue.jsは、Webユーザーインターフェイスを構築するためのライブラリです。ネイティブのモバイルインターフェースに使用したい場合は、NativeScript-Vueフレームワークで使用できます。
NativeScriptはiOSおよびAndroidのネイティブユーザーインターフェイスコンポーネントを使用してアプリを構築するためのシステムであり、NativeScript-VueはNativeScriptの上にあるフレームワークであり、Vue構文とコンポーネントの使用を提供します。
5.その他
この最後のセクションでは、重要であるが重要ではないトピックまたは上記のカテゴリに当てはまらないトピックについて説明します。
プラグイン開発
プロジェクト全体でVue機能を再利用したり、Vueエコシステムに貢献したい場合は、Vueプラグインとしてインストール可能な機能を作成できます。
プラグインはVueコアの機能ですが、移植可能なVueコードの作成に役立つさまざまなツールと定型文もあります。
アニメーション
アニメーションが必要な場合は、Vueの移行システムも確認してください。これもVueコアの一部です。トランジションを使用すると、DOMに要素を追加または削除するたびにアニメーションを適用できます。
トランジションを行うには、CSSクラスを作成して、フェードイン、色の変更など、目的のアニメーション効果を定義します。 Vueは、要素がDOMに追加または削除されたことを検出し、移行中に適切なクラスを追加または削除します。
プログレッシブWebアプリ
プログレッシブWebアプリ(PWA)は通常のWebアプリに似ていますが、ユーザーエクスペリエンスを向上させる最新の機能で強化されています。たとえば、PWAにはオフラインキャッシュ、サーバーレンダリング、プッシュ通知などが含まれる場合があります。
ほとんどのPWA機能は、Vue CLI 3プラグインまたはNuxt.jsのようなフレームワークを使用してVueアプリに簡単に追加できますが、Webアプリマニフェストやサービスワーカーなどの主要なテクノロジーを理解する必要があります。
2020年に上級Vue開発者になります!
- 投稿日:2020-02-12T23:17:37+09:00
【Vue.js】Firebaseのデータを取得してリストを表示させよう
Vue.jsで最初に理解するのに時間がかかったのは、親子間のデータ伝達でした。
今回は自分への理解を兼ねて、Firebaseに保存してあるデータを取得し、そのデータを子に伝達して表示する一連のフローを解説したいと思います。現在はこれで勉強しております。
とてもいい教材なのでご興味ある方はぜひ。
「Vue.js & FirebaseでTwitterライクなSNSアプリを作ってみよう!」
https://www.techpit.jp/courses/enrolled/601067
まずFirebaseでデータを準備します。
Firebaseを開いてコンソールに移動します。
続いて[Database]から、[コレクションを開始]を選択してデータのコレクションを作成し、ドキュメントを作成します。
データは何でも構いませんが、後ほどコンテンツをタイムラインのように表示したいので、
- ユーザーidを示す[uid]
- 日付を示す[date]
- 内容を示す[content]
としておきました。
ドキュメントは3つほど作成しておきます。
続いてfirebaseとVueアプリケーションを連携させるために、firebaseでアプリを作成します。
[Project Overview]に戻り、「アプリを追加」→[web]を選んでアプリの名前をつけましょう。
その後表示されるスクリプトをコピーしておきます。続いてVueのコードに移ります。
Vueのファイルで最初に読み込まれるのはmain.js
で、そこにfirebaseのインポートを行います。
firebaseをインポートし、firebase/firestore
もインポートします。
さらに先ほどのスクリプトを参考にfirebaseの初期化を行い、Vue.use
でfirebaseを使用可能にします。
firestoreのデータベース処理を簡単にするため、vuefire
もインポートします。
最後に、他のファイルにもエクスポートできる形で、firestoreのインスタンスであるdb
を宣言します。import Vue from 'vue' import App from './App.vue' import router from './router' import { firestorePlugin } from 'vuefire' import firebase from 'firebase' import 'firebase/firestore' firebase.initializeApp({ apiKey: "", authDomain: "", databaseURL: "", projectId: "", storageBucket: "t", messagingSenderId: "", appId: "" }) Vue.use(firestorePlugin) export const db = firebase.firestore() Vue.config.productionTip = false new Vue({ router, render: function (h) { return h(App) } }).$mount('#app')さて、ここからが本番です。
今回やりたいことは、Home
という親コンポーネントに、Item
という子コンポーネントを入れ込み、その子コンポーネントに先ほど登録したデータをリスト表示させる、というものです。
どのように書くと、思ったように表示することができるでしょうか。firestoreからデータのidを受け取るのは、通常親コンポーネントです。
親コンポーネントがデータを受け取り、それを子コンポーネントに伝達し、それぞれの子コンポーネントがそれにあった表示をすることで画面が作成されます。なので、今回は
Home
がfirestoreのidを受け取ります。
そのidを受け取ると、そのidを子コンポーネントに伝達します。
親コンポーネントから子コンポーネントにデータを伝達するには、そのデータをprops
オブジェクトに格納する必要があります。
そしてそのidを受け取った子コンポーネントは、そのidに応じたコンテンツを取得します。
コンテンツを取得するためには、子コンポーネントでコンテンツをdata
オブジェクトに格納し、firestore
でそのコンテンツをfirestoreの中にあるidを照合することで取得することができます。
最後にそのコンテンツを子コンポーネントに表示すれば、1つのコンテンツを表示することができます。
そしてそれをv-for
ディレクティブで繰り返せば完成です。早速書いてみましょう。
Item.vue
を作成し、その中にfirestoreをインポートします。
同時に、main.js
で作成したdb
もインポートします。import { firestore } from 'firebase' import { db } from '../main'親子のコンポーネントを作成するので、
export default
の中にオブジェクトを記述します。
子コンポーネントのprops
には親とやり取りするためのid
を格納します。
また、data
オブジェクトには、取得して子コンポーネントに表示したいデータであるコンテンツ名(twit
という名前にしています)を書きます。export default { props:['id'], data(){ return{ twit: {} } }次に、firestoreと照合して、propsから伝達してきたidと同じidを持つドキュメントを
twit
の中に代入します。
代入するには、以下のように書きます。firestore(){ return{ twit: db.collection("twits").doc(this.$props.id) } }さらに、これを表示するためのコードをtemplate構文の中に書きましょう。
の中に書くようにしてください。
こちらは、単純にv-html
構文で divタグの中に書くだけで十分です。
最終的にリストにするので、<template> <li class="item"> <div class="user-box"> <div v-html="twit.content"></div> </div> </li> </template>これで、子コンポーネントの準備は完了です。
明日は親コンポーネントのコードを作成します。
- 投稿日:2020-02-12T18:46:42+09:00
【Laravel + Vue】 Laravel Airlock を使ってAPI認証を構築してみる
Laravel Airlockとは
Laravel Airlockは、SPAおよびシンプルなAPI向けの超軽量認証システムを提供します。
? I'd like to get some beta testers on Laravel Airlock... I have put up initial documentation here: https://t.co/BFPasv1BCZ
— Taylor Otwell ? (@taylorotwell) January 8, 2020github
※初期バージョンですので利用は自己責任でお願いします今回作業後のGithubに上げたサンプル
Laravel
前提
Laravel 6.x インストール済み
airlockインストール
composer require laravel/airlock構成ファイルの公開
php artisan vendor:publish --provider="Laravel\Airlock\AirlockServiceProvider"マイグレーション実行
php artisam migrate
CreatePersonalAccessTokensTable
が実行され、personal_access_tokens
テーブルが追加されます。
また、config/airlock.php
も追加されますミドルウェアの追加
app/Http/Kernel.php
に
use Laravel\Airlock\Http\Middleware\EnsureFrontendRequestsAreStateful;
EnsureFrontendRequestsAreStateful::class,
を追加しますapp/Http/Kernel.php<?php namespace App\Http; use Illuminate\Foundation\Http\Kernel as HttpKernel; // ↓追加 use Laravel\Airlock\Http\Middleware\EnsureFrontendRequestsAreStateful; class Kernel extends HttpKernel { /** * The application's global HTTP middleware stack. * * These middleware are run during every request to your application. * * @var array */ protected $middleware = [ \App\Http\Middleware\TrustProxies::class, \App\Http\Middleware\CheckForMaintenanceMode::class, \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, \App\Http\Middleware\TrimStrings::class, \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, ]; /** * The application's route middleware groups. * * @var array */ protected $middlewareGroups = [ 'web' => [ \App\Http\Middleware\EncryptCookies::class, \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, \Illuminate\Session\Middleware\StartSession::class, // \Illuminate\Session\Middleware\AuthenticateSession::class, \Illuminate\View\Middleware\ShareErrorsFromSession::class, \App\Http\Middleware\VerifyCsrfToken::class, \Illuminate\Routing\Middleware\SubstituteBindings::class, ], 'api' => [ EnsureFrontendRequestsAreStateful::class, //追加 'throttle:60,1', 'bindings', ], ]; ~略~ }APIトークンの発行
App\User
に
use Laravel\Airlock\HasApiTokens;
use HasApiTokens
を追加しますApp\User<?php namespace App; use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Laravel\Airlock\HasApiTokens; //追加 class User extends Authenticatable { // ↓追加 use HasApiTokens, Notifiable; /** * The attributes that are mass assignable. * * @var array */ protected $fillable = [ 'name', 'email', 'password', ]; /** * The attributes that should be hidden for arrays. * * @var array */ protected $hidden = [ 'password', 'remember_token', ]; /** * The attributes that should be cast to native types. * * @var array */ protected $casts = [ 'email_verified_at' => 'datetime', ]; }ルーティング
routes/api.php
を下記のようにしますroutes/api.php<?php use Illuminate\Http\Request; Route::group(['middleware' => ['auth:airlock']], function () { Route::get('user', function (Request $request) { return response()->json(['user' => $request->user()]); }); Route::post('logout', 'Auth\Api\LoginController@logout')->name('api.logout'); }); Route::post('register', 'Auth\Api\RegisterController@register')->name('api.register'); Route::post('login', 'Auth\Api\LoginController@login')->name('api.login');ユーザー登録
app/Http/Controllers/Auth
内にApi
フォルダを作成し、そこにapp/Http/Controllers/Auth
内のRegisterController
をコピーし、下記を貼り付けますApi/RegisterController.php<?php namespace App\Http\Controllers\Auth\Api; use App\Http\Controllers\Controller; use App\User; use Illuminate\Foundation\Auth\RegistersUsers; use Illuminate\Http\Request; use Illuminate\Support\Facades\{Validator, Hash, DB}; class RegisterController extends Controller { use RegistersUsers; public function register(Request $request) { // リクエストを検証します。 $this->validator($request->all())->validate(); // ユーザーの作成とトークンの作成します $data = DB::transaction(function () use ($request) { $user = $this->create($request->all()); $token = $user->createToken($request->device_name ?? 'undefined')->plainTextToken; return json_encode(['token' => $token, 'user' => $user]); }); // userとtokenのjsonを返却 return response($data); } /** * Get a validator for an incoming registration request. * * @param array $data * @return \Illuminate\Contracts\Validation\Validator */ protected function validator(array $data) { return Validator::make($data, [ 'name' => ['required', 'string', 'max:255'], 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'], 'password' => ['required', 'string', 'min:8', 'confirmed'], ]); } /** * Create a new user instance after a valid registration. * * @param array $data * @return \App\User */ protected function create(array $data) { return User::create([ 'name' => $data['name'], 'email' => $data['email'], 'password' => Hash::make($data['password']), ]); } }ログイン・ログアウト処理
登録のときと同様に、
Auth
内のLoginController
をコピーし
下記のように修正します。Api\LoginController<?php namespace App\Http\Controllers\Auth\Api; use App\Http\Controllers\Controller; use App\User; use Illuminate\Foundation\Auth\AuthenticatesUsers; use Illuminate\Http\Request; use Illuminate\Support\Facades\Hash; use Illuminate\Validation\ValidationException; class LoginController extends Controller { use AuthenticatesUsers; /** * Handle a login request to the application. * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse * * @throws \Illuminate\Validation\ValidationException */ public function login(Request $request) { // バリデーション $this->validateLogin($request); // ユーザーの取得 $user = User::where('email', $request->email)->first(); // 取得できない場合、パスワードが不一致の場合エラー if (!$user || !Hash::check($request->password, $user->password)) { throw ValidationException::withMessages([ 'email' => [__('failed')], ]); } // tokenの作成 $token = $user->createToken($request->device_name ?? 'undefined')->plainTextToken; return response()->json(['token' => $token, 'user' => $user], 200); } /** * Handle a logout request to the application. * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse */ public function logout(Request $request) { $user = $request->user(); // tokenの削除 foreach ($user->tokens as $token) { $token->delete(); } return response()->json(['message' => 'logouted']); } }これで登録・ログイン・ログアウトはできるようになりました。
Airlock テスト pic.twitter.com/UI75JJX9sq
— 棗(なつめ) (@natsume_aurlia) February 11, 2020※別ドメインの場合、
airlock.php
ファイルのstateful構成オプションを使用してくださいVue
前提
Vue、Vuex、VueRouterインストール済みのプロジェクト作成済み
Laravelに同居する形でもできますが、今回は分離させます。
みやすさ重視、改造しやすいようにスタイルは当ててませんこちらのコードを参考にさせていただきました
各エンドポイントへリクエストを送信し、認証後にトークンを受け取り
認証が必要なアクションにはそのトークンがを付与したリクエストを行います。ログイン画面
/src/views/Login.vue<template> <div> <h1>login</h1> <form @submit.prevent="login"> <label for="email">Email</label> <input type="text" id="email" v-model="loginForm.email" /> <label for="password">Password</label> <input type="password" id="password" v-model="loginForm.password" /> <button type="submit">login</button> </form> </div> </template> <script> export default { name: "login", components: {}, data() { return { loginForm: { email: "", password: "" } }; }, methods: { login() { this.$store.dispatch("auth/login", this.loginForm).then(() => { this.$router.push({ name: "dashboard" }); }); } } }; </script>
password
をv-model
でloginForm
にそれぞれ結びつけています。
submit
されたとき、画面遷移しないようにしlogin
のメソッドを呼び出します登録画面
/src/views/Register.vue<template> <div> <h1>Register</h1> <form @submit.prevent="register"> <label for="name">Name</label> <input type="text" id="name" v-model="registerForm.name" /> <label for="email">E-mail Address</label> <input type="email" id="email" v-model="registerForm.email" /> <label for="password">Password</label> <input type="password" id="password" v-model="registerForm.password" /> <label for="password_confirm">Confirm Password</label> <input type="password" id="password_confirm" v-model="registerForm.password_confirmation" /> <button type="submit">register</button> </form> </div> </template> <script> export default { name: "register", components: {}, data() { return { registerForm: { name: "", email: "", password: "", password_confirmation: "" } }; }, methods: { register() { this.$store.dispatch("auth/register", this.registerForm).then(() => { this.$router.push({ name: "dashboard" }); }); } } }; </script>ここも普通ですね
password_confirmation
なのはLaravel側でconfirmed
というバリデーションルールが有るためですフィールドがそのフィールド名+
_confirmation
フィールドと同じ値であることをバリデートします。
例えば、バリデーションするフィールドがpassword
であれば、
同じ値のpassword_confirmation
フィールドが入力に存在していなければなりません。ストア側
登録・ログインは下記のようにリクエストをするだけです。
auth.jsregister(context, data) { axios.post(BASE_URL + '/api/register', data).then((result) => { context.commit("setUser", result.data.user); context.commit("setToken", result.data.token); }).catch(error => { console.log(`Error! HTTP Status: ${error}`); }); },公式に下記のようにリクエストして、CSRF保護を初期化してからリクエストするようにありますが、省いてます。
axios.defaults.withCredentials = true; axios.get('/airlock/csrf-cookie').then(response => { // Login... });ログインや登録で取得したトークンはローカルストレージへ保存し
state
ではローカルストレージを参照するようにしますauth.jsconst state = { user: null, token: window.localStorage.getItem('token') } const mutations = { setUser(state, user) { state.user = user; }, setToken(state, token) { window.localStorage.setItem('token', token); } }ログアウト、ユーザ情報取得
保存してあるトークンを付与して、ログアウトのエンドポイントへリクエストを送信するだけです。
postやgetの第三引数に指定します。
もっと簡潔に出来るパッケージや手法もあるようです。auth.jslogout(context) { axios.post(BASE_URL + '/api/logout', null, { headers: { Authorization: `Bearer ${state.token}`, } }).then((result) => { context.commit("setUser", null); context.commit("setToken", null); }).catch(error => { console.log(`Error! HTTP Status: ${error}`); }); },参考記事
https://github.com/laravel/airlock
https://redfern.dev/vue-js-auth-using-laravel-airlock/
https://github.com/garethredfern/airlock-vue
日本語の情報が0で、英語の情報もほぼ公式しかなかったのできつかったです。
Laravel7がでて、いろんな方の情報が出てからでも良かった気がしてならない。
- 投稿日:2020-02-12T18:01:17+09:00
PlayCanvasで建築系のモデルを良い感じの3Dビュワーにしてみる [WebGL]
PlayCanvasの非ゲーム分野を開拓中のキドです。
今回はビジュアライゼーションで建築系に使えそうなコンテンツを作ってみました。
建築系ビジュアライズ
ここでコンテンツを見ることができます
https://playcanv.as/p/F1ZzFOYS/とあるビルのオフィス内の案内コンテンツ、という感じで作りました。
3Dコンテンツなのでカメラで外装を見たり、階層の番号をクリックすることでその階層の中を一望できたり。
あとはそれに応じたDOMも変更したり、レイキャストでクリックできるスポットを設置したりしました。FlashがWebでよく使われていた頃に、デパートとか施設のマップ案内をするWebページがありました。
その頃はFlashで実装されていましたが、2020年いっぱいでFlashも終わってしまいます。
今では画像だとかpdfだとかで実装されていて、若干見にくくなった気がしますね…そんな施設のマップ案内をFlashの代わりになりそうですよ。
作り方
今回の記事では完成したプロジェクトから紹介しているだけなので、丸々コピーしても正しく動作しない場合があるのでご了承を。
また、コードの説明については軽く触れますが詳しく説明しませんのでこちらもご了承を。
レシピ
今回は以下の3つ用意すればおk
- PlayCanvas
- 使用する3Dモデル
- Vue.js (
https://cdn.jsdelivr.net/npm/vue/dist/vue.js
)- PlayCanvas Tweenライブラリ
PlayCanvasから新規プロジェクトを作る際はテンプレートに「Model Viewer Starter Kit」を選んでください。
3Dモデルを配置
用意した3Dモデルは階層ごとにあらかじめ分けておきます。
今回はLightを使用せず、3Dモデルに陰影を焼き付けてから使用しています。HierarchyでのEntityの配置の仕方は、buildingを親として子に各階層を入れています。
3Dモデルが移動する位置を決める
先ほど配置した3DモデルのEntityの子に、横にスライドしたときの位置を決めたEntityを配置します。
同じ3Dモデルを使って位置を決めるとわかりやすくて良いですね。決められたらこのEntityはEnabledをfalseにして非表示しておきます。
3Dモデルの配置ができたら、次はCameraも移動する位置を決めます。
横にスライドした3Dモデルを俯瞰できるような位置に設置して、これもEnabledをfalseにして非表示にします。
Cameraの設定
テンプレートから作っていれば以下のScriptsがセットされていると思います。
- orbitCamera.js
- mouseInput.js
- touchInput.js
これらはこのままで。
orbitCamera.js
のFocus Entityを変更しますが、その原点となるEntityを設置します(2枚目の画像)
ここではorbitcamera-gentenと名前をつけていますが、原点にしか使わないのでなんでも良いです。
Focus Entityが変更できたらOKです。autoRotate.js
Cameraに新しくスクリプトを書きます。
自動回転するスクリプトですが、必要なければ無視しても問題ないと思います。この
autoRotate.js
はテンプレートにあるorbitCamera.js
の中のCameraのYawを操作しています。updateで
this.orbitCamera.yaw += this.speed;
とやっているのが、ここでの肝でしょうか。
this.timer += dt;
とかはAttributesのwaitと比較していて、急に回転するのを止めてくれています。
ちょっと待ってから自動回転する、そんな感じにしてくれます。
vueApp
と見慣れないものがありますが、これは後ほどDOMと連携させる際にVue.jsでコントロールしているのがここでも影響しているためです。var AutoRotate = pc.createScript('autoRotate'); AutoRotate.attributes.add('speed', {type: 'number',default: 1,title: 'Speed',description: 'The rotate speed of camera.'}); AutoRotate.attributes.add('wait', {type: 'number',default: 5,title: 'Wait',description: 'Enable auto rotate after seconds.'}); AutoRotate.prototype.initialize = function() { this.timer = 0; this.orbitCamera = this.entity.script.orbitCamera; }; AutoRotate.prototype.resetTimer = function() { this.timer = 0; }; AutoRotate.prototype.update = function(dt) { if(vueApp.rotateFlag) return false; if(!vueApp.openfloor&&!vueApp.clickAnimation){ this.timer += dt; if (this.timer > this.wait) { this.orbitCamera.yaw += this.speed; } } };DOMを追加
3Dモデルはこれぐらいでだいたいおkなので、HTMLとかCSSの方にいきましょう。
Vue.jsを使う
今回はVue.jsを使うのですが、PlayCanvasでVue.jsとかのCDNを使う場合にはSETTINGSのEXTERNAL SCRIPTSを使います。
changeFloor.js
orbitCamera.js
がCameraの動きの中枢でしたが、このchangeFloor.js
はこのコンテンツの柱なので他よりコード多めです。DOMを配置するためにdiv要素
wrapper
を配置したり、CSSはgetFileUrl()
を使いlink要素で読み込んだりしていますが、
一番の肝は選択された階層に対するCameraと3Dモデルの移動でしょうか。
vueApp
の箇所がVue.jsで操作する箇所ですね。
あまりVue.jsの綺麗な書き方ができていませんが許して。
tween()
はPlayCanvasのTweenライブラリです。
これを使ってCameraのPositionやAngle、、3DモデルのPositionを移動させています。
CameraのAngleですが、RotationではなくEulerAnglesで変更してあげないと挙動がおかしくなってしまうのでご注意です。このTweenの処理をtrue/flaseのFlagで管理しています。
階層がOpenなのかCloseなのか見て、それに対応した処理を行なっています。このFlagは先ほどの
autoRotate.js
の以下のコードも見ています。
ここではCloseの状態でTweenのアニメーションが切れていればautoRotateするようにしています。if(vueApp.rotateFlag) return false; if(!vueApp.openfloor&&!vueApp.clickAnimation){/*jshint esversion: 6, asi: true, laxbreak: true*/ const ChangeFloor = pc.createScript('changeFloor'); ChangeFloor.attributes.add("baseHtml", {type:"asset", assetType:"html"}); ChangeFloor.attributes.add("setCSS", {type:"asset", assetType:"css"}); ChangeFloor.attributes.add("target", {type:"entity"}); ChangeFloor.attributes.add("cameraTarget", {type:"entity"}); ChangeFloor.prototype.initialize = function() { let self = this; let canvas = document.getElementsByTagName("canvas")[0]; canvas.classList.add("pcCanvas"); let wrapper = document.createElement("div"); wrapper.classList.add("wrapper"); wrapper.innerHTML = self.baseHtml._resources[0]; canvas.parentNode.appendChild(wrapper); let css = document.createElement("link"); css.setAttribute("href", this.setCSS.getFileUrl()); css.setAttribute("rel", "stylesheet"); document.head.appendChild(css); let t_camera = self.cameraTarget; let cameraPosOri = Object.assign({},t_camera.getLocalPosition()); let cameraRotOri = Object.assign({},t_camera.getLocalEulerAngles()); let tHead = "とあるビルのオフィス内"; let tContent = "PlayCanvasでビル内の各階層を3Dで観ることができます。 <br>気になる箇所をクリックすることで詳細が観れます。"; vueApp = new Vue({ el: '#app', data: { openfloor: false, clickAnimation: false, floors: self.target.children, lastTarget: null, targetPosOri: [], DOMhead: tHead, DOMcontent: tContent, DOMraycastFlag: false, DOMraycastHead: "", DOMraycastContent: "", rotateFlag: false, }, methods: { onfloorClick: function(target,index) { const v_self = this; if(v_self.clickAnimation || v_self.openfloor) return; cameraPosOri = Object.assign({},t_camera.getLocalPosition()); cameraRotOri = Object.assign({},t_camera.getLocalEulerAngles()); v_self.clickAnimation = true; v_self.openfloor = true; v_self.lastTarget = index; v_self.targetPosOri[index] = Object.assign({}, target.getLocalPosition()); let cameraPos,cameraRot,targetPos; for(let i=0; i<target.children.length; i++) { if(target.children[i].name != "RootNode"){ if(target.children[i].camera){ cameraPos = Object.assign({}, target.children[i].getLocalPosition()); cameraRot = Object.assign({}, target.children[i].getLocalEulerAngles()); }else if(target.children[i].tags._list[0] === "tween"){ targetPos = Object.assign({}, target.children[i].getLocalPosition()); } // console.log(target.children[i]); } } // カメラ移動 t_camera.tween(t_camera.getLocalPosition()).to({ x:target.getLocalPosition().x+cameraPos.x, y:target.getLocalPosition().y+cameraPos.y, z:target.getLocalPosition().z+cameraPos.z }, 1, pc.SineOut).start(); t_camera.tween(t_camera.getLocalEulerAngles()).rotate(cameraRot, 1, pc.Linear).start(); // ビル階層移動 target.tween(target.getLocalPosition()).to({ x:target.getLocalPosition().x+targetPos.x, y:target.getLocalPosition().y+targetPos.y, z:target.getLocalPosition().z+targetPos.z }, 1, pc.SineOut).on("complete",function(){ target.tags.add("isopen"); v_self.clickAnimation = false; }).start(); v_self.DOMhead = "ただいま、" + target.name + "階"; v_self.DOMcontent = "ここは" + target.name + "階です。ここにはその階に応じた説明文を記入する感じになります。"; }, oncloseClick: function() { const v_self = this; v_self.floors[v_self.lastTarget].tags.remove("isopen"); // カメラ移動 t_camera.tween(t_camera.getLocalPosition()).to({ x:cameraPosOri.x, y:cameraPosOri.y, z:cameraPosOri.z }, 1, pc.SineOut).start(); t_camera.tween(t_camera.getLocalEulerAngles()).rotate(cameraRotOri, 1, pc.SineOut).start(); v_self.clickAnimation = true; // ビル階層移動 v_self.floors[v_self.lastTarget].tween(v_self.floors[v_self.lastTarget].getLocalPosition()).to(v_self.targetPosOri[v_self.lastTarget], 1, pc.SineOut) .on("complete",function(){ v_self.clickAnimation = false; }) .start(); v_self.openfloor = false; v_self.DOMhead = tHead; v_self.DOMcontent = tContent; }, oncloseRcClick: function() { const v_self = this; v_self.DOMraycastFlag = false; v_self.DOMraycastHead = ""; v_self.DOMraycastContent = ""; } } }); };私はこういうDOMを操作する系のスクリプトはRootに毎度のごとく登録しています。
特に意味はないですが、なんとなく早く読み込んでほしいと思いと見やすいし管理しやすいからですね。
Attributesに登録されているHTMLとCSSのコードも載せておきます。
base.html
Vueの構文が色々書いていますが、階層がOpenかCloseかをv-ifで見ていたり、どの階層をクリックしたかを@clickでイベント取得したりしています。
class="detail is-ray"
の要素は、レイキャストでクリックしたスポットの情報を表示するDOMですね。
レイキャストはこの後説明します。<div id="app" class="wrapper"> <nav v-if="!rotateFlag" class="select" :class="openfloor ? '' : 'is-open'"> <div class="select__inner"> <div class="item" v-for="(floor,index) in floors" @click="onfloorClick(floor,index)"><span>{{floor.name}}</span></div> </div> </nav> <div v-if="!rotateFlag" class="detail" :class="{'is-open':openfloor}"> <div class="closeBtn" v-if="openfloor&&!DOMraycastFlag" @click="oncloseClick()"></div> <div class="detail__inner"> <p class="domhead" v-html="DOMhead"></p> <p class="domcontent" v-html="DOMcontent"></p> </div> </div> <div class="detail is-ray" v-if="DOMraycastFlag"> <div class="closeBtn" @click="oncloseRcClick()"></div> <div class="detail__inner"> <p class="domhead" v-html="DOMraycastHead"></p> <p class="domcontent" v-html="DOMraycastContent"></p> </div> </div> <div class="rotateCheck" v-if="!openfloor"><label for="rotate"><input type="checkbox" id="rotate" v-model="rotateFlag" /><span>カメラ操作切り替え</span></label></div> </div>style.css
本来は
reset.css
を記述していますが、長くなってしまうので省きました。
自前のreset.css
を使用していましたが、多分普段使用しているものでも問題ないと思います。あと、これはモックなのでテキトーなスタイルなのであまり参考にならないかもしれません。
body { background-color: #b1b1b1; } .wrapper { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 10; } .select { position: absolute; top: 0; right: 0; z-index: 5; transform: translateX(400px); transition: transform .4s; } .select.is-open { transform: translateX(0); } .select__inner { position: absolute; top: 0; right: 0; width: 330px; padding: 5px; border-radius: 10px; background-color: rgba(255,255,255, .5); } .item { display: inline-block; vertical-align: middle; margin: 10px; padding: 10px; border: 1px solid #cccccc; border-radius: 50%; background-color: #cccccc; text-align: center; transition: background .3s; cursor: pointer; } .item span { color: #eeeeee; font-size: 2rem; line-height: 1; transition: color .3s; } .item:hover { background-color: #eeeeee; } .item:hover span { color: #333333; } .closeBtn { position: absolute; top: -10px; right: 0; width: 50px; height: 50px; background-color: #333333; transition: background .3s; cursor: pointer; } .closeBtn:before,.closeBtn:after { content: ""; position: absolute; top: 50%; left: 50%; width: 30px; height: 2px; border-radius: 25%; background-color: #eeeeee; transition: background .3s; } .closeBtn:before { transform: translate(-50%,-50%) rotate(45deg); } .closeBtn:after { transform: translate(-50%,-50%) rotate(135deg); } .closeBtn:hover { background-color: #eeeeee; } .closeBtn:hover:before,.closeBtn:hover:after { background-color: #333333; } .detail { position: absolute; bottom: 50%; left: 50%; z-index: 2; transform: translate(-50%,50%); max-width: 800px; min-width: 420px; width: 100%; padding: 50px 20px; transition: all .4s; pointer-events: none; } .detail.is-ray { pointer-events: auto; } .detail * { transition: all .6s; } .detail__inner { padding: 100px 30px; border-radius: 10%; background-color: rgba(35,35,35, .7); text-align: center; } .domhead { margin-bottom: .5em; color: #eeeeee; margin-bottom: 10px; font-size: 2rem; font-weight: bold; line-height: 2; } .domcontent { color: #eeeeee; font-size: 1.6rem; line-height: 2; } .rotateCheck { position: fixed; bottom: 0; right: 0; z-index: 10; pointer-events: auto; } .rotateCheck label { display: block; cursor: pointer; } .rotateCheck label input { display: none; } .rotateCheck label span { display: inline-block; padding: 1rem; background-color: #cccccc; font-size: 1rem; line-height: 1; transition: color .2s, background .2s; } .rotateCheck label input:checked+span { color: #eeeeee; background-color: #333333; pointer-events: none; } .detail.is-open { bottom: 0; left: 0; transform: translate(0,0); max-width: 100%; min-width: 0; width: 100%; padding: 20px; pointer-events: auto; } .detail.is-open .detail__inner { padding: 20px; border-radius: 0; background-color: rgba(255,255,255, .7); text-align: left; } .detail.is-open .domhead { color: #111111; font-size: 1.2rem; } .detail.is-open .domcontent { color: #111111; font-size: 1rem; }レイキャスト
各階層のスポットを紹介するためにレイキャストでクリックできるポインターを設置します。
各階層の子としてEntityを追加します。
配置は真上から見て良い感じの場所に配置させておきましょう。
domRaycast.js
ポインターを追加できたらスクリプトを作ります。
今回のレイキャストはDOMを使ったものになっています。
そのためstyle
のコードばかりですが、update
で書いているコードがレイキャストの処理になります。DOMでのレイキャスト以外にも方法ありますので、別記事を参照ください
/*jshint esversion: 6, asi: true, laxbreak: true*/ const DomRaycast = pc.createScript('domRaycast'); DomRaycast.attributes.add("cameraEntity", {type: "entity", title: "Camera Entity"}); DomRaycast.attributes.add("a_domhead", {type: "string", title: "DOM Head"}); DomRaycast.attributes.add("a_domcontent", {type: "string", title: "DOM Content"}); // g_domRC = {}; // g_domRC.flag = false; // g_domRC.domhead = "test"; // g_domRC.domcontent = "testest"; DomRaycast.prototype.initialize = function() { // init let self = this; self.directionToCamera = new pc.Vec3(); self.defaultForwardDirection = self.entity.forward.clone(); self.btn = document.createElement("div"); document.getElementsByTagName("canvas")[0].parentNode.appendChild(self.btn); self.btn.style.position = "absolute"; self.btn.style.width = "30px"; self.btn.style.height = "30px"; self.btn.style.borderRadius = "50%"; self.btn.style.background = "#111111"; self.btn.style.transition = "opacity .5s"; self.btn.style.zIndex = 10; self.btn.style.cursor = "pointer"; self.btn.style.pointerEvents = "none"; self.btn.addEventListener("mouseover",function(){ this.style.background = "#555555"; }); self.btn.addEventListener("mouseout",function(){ this.style.background = "#111111"; }); self.btn.addEventListener("mousedown",function(){ this.style.background = "#aaaaaa"; }); self.btn.addEventListener("mouseup",function(){ vueApp.DOMraycastFlag = true; vueApp.DOMraycastHead = self.a_domhead; vueApp.DOMraycastContent = self.a_domcontent; this.style.background = "#111111"; }); }; DomRaycast.prototype.update = function(dt) { // update let worldPos = this.entity.getPosition(); let screenPos = new pc.Vec3(); this.cameraEntity.camera.worldToScreen(worldPos, screenPos); this.directionToCamera.sub2(this.cameraEntity.getPosition(), this.entity.getPosition()); this.directionToCamera.normalize(); // let dot = this.directionToCamera.dot(this.defaultForwardDirection); if (this.entity.parent.tags._list[0] === "isopen" && !vueApp.DOMraycastFlag) { this.btn.style.pointerEvents = "auto"; this.btn.style.opacity = 1; } else { this.btn.style.pointerEvents = "none"; this.btn.style.opacity = 0; } this.btn.style.transform = "translate(" + screenPos.x + "px," + screenPos.y + "px)"; };AttributesはCameraとモーダルで表示させるテキスト情報を登録できます。
スクリプトを一つ作って各階層に登録していけるのがPlayCanvasの良いところですよね。背景透過
ここまでで処理は完成していますが、ついでにPlayCanvasのコンテンツ背景を透過させようと思います。
Cameraの設定のClear ColorのAlphaを0に。
SETTINGSのRENDERINGの設定内にあるTransparent Canvas
をtrueにします。PlayCanvasでのcanvasの透過方法ですが、これも別記事で説明しています。
詳しくはそちらでご参照ください完成
ここまで設定できたら完成です!
Vue.jsを使うことでデータの受け渡しが楽になるので良いですね。
Vue.jsの良いところはガチガチのフレームワークではなくライブラリ感覚で使えるところでしょうか。
今回はPlayCanvasの中でVue.jsを使ってみましたが、他のjsライブラリでも試して見るもの良いですね。
処理のフロー
コードについてあまり説明をできていませんが、流れについて説明しておきましょう。
画像は簡単なものなのでわかりづらいですが石とか投げないでください。
今回の大元は
changeFloor.js
ですが、基本的にはopenする階層を選択したら移動するというのが主な仕事です。
これを行うためには、各それぞれの要素がどんな動きをしているのか、どんな状態で待機しているのかを管理する必要がありました。
今回で言えば、autoRotate.js
は自動回転を始めてしまいますから、階層をopenにした後にも自動回転してしまっては思った挙動にならないです。
DOMについても表示すべき場面で表示させないといけません。
そのため、changeFloor.js
は名前的には階層を変える処理を行うのですが、他の処理をコントロールする中枢の処理を担っています。反省
なるべく管理のしやすい、見やすいを心がけて作りましたが、コードはあんまり綺麗にできませんでした…
ゲーム開発はやったことないのでどうするのが一番良いのかわからないですが、なるべく誰でも見た感じでわかるようなプロジェクトを作っていきたいですね。
- 投稿日:2020-02-12T18:00:14+09:00
Vue.jsでRadioButtonGroupコンポーネントを自作する
前提
なにがしたいの?
Vue.jsを利用していて、単品RadioButtonでことが済んでいたんだけれど(サーバーサイドテンプレートエンジンに対して、UIパーツを作る形でVue.jsを使っているプロジェクトで、formのpostとかjsでやってないし)
どうしても、RadioButtonGroupが欲しくなった。なんで?
- RadioButtonパーツのHTMLの構成を変えたくなかった
こんなカンジ<label class="radio"> <input type="radio"> <span class="radio-label">ラベル表示</span> <span class="radio-icon">まるポチ的なやつ表示用</span> </label>
まるポチではない見た目でUIを作ろうとしたら、labelに状態classをつけるために、あるモードが選択された後、他のモードを選択されることによって、別の子がオンになったよ~な通知が必要になった。
とはいえ、その通知はラジオボタンの話なので、マウント元にその切替用のことをいろいろ書きたくなかった。
他に言いたいことは
- でも、ラジオボタン自体はマウント元に書きたい。
つまり、こんなカンジにしたい。
<div hoge="わたしはマウント元"> <radio-button-group> <v-radio-button props="props" /> <v-radio-button props="props" /> <v-radio-button props="props" /> </radio-button-group> </div>。。。なんか、文章がへたくそね。
で、やったこと。
いろいろ検索する
Vue.jsでラジオボタン [radio button]
https://noumenon-th.net/programming/2019/05/31/radio/Vue.jsのScoped Slotを使って脱初心者なラジオボタンコンポーネントを作るまで
https://qiita.com/simezi9/items/702e2bd066eec352ef72上記でも紹介されてますが、MaterialUIやVuetifyの実装もちょっと見てみました。
…さらっとだけでよくわかってないけど。でもね
なんか、思ったようなものにならず。(そんな難しいことできないし。)
自分なりの実装をしてみました。ここからが、本当にやったこと
作成前提
そもそも、こんなRadioButtonコンポーネントがあります。
RadioButton<template> <label class="radio"> <input type="radio" :name="name" :value="value" :required="is_required" v-model="selectedItem" /> <span class="radio-icon"></span> <span class="radio-button-label">{{ label }}</span> </label> </template> <script> export default { name: 'ElRadioButton', props: { name: { type: String }, //name属性値 value: { type: String }, //value属性値 is_required: { type: Boolean }, //必須項目か is_defSelected: { type: Boolean }, //初期状態でチェックされているか label: { type: String } //表示項目名 }, computed: { selectedItem: { get: function() { if ( this.is_defSelected) { return this.value } }, set: function(val) { this.$emit('change', val) } }, } } </script>で、シンプルにこれをマウントするには
parent<div> <el-radio-button v-for="(menu, index) in menus" :key="index" name = "menutype" :value = "menu.name" :is_def-selected = "最初に選択されているものの条件式でboolean値" :is_required = false :label = "menu.label" @change = "changeRadio" ></el-radio-button> </div> <script> import ElRadioButtonGroup from '@/components/element/RadioButtonGroup' import ElRadioButton from '@/components/element/RadioButton' export default { name: 'TestPage', components: {ElRadioButtonGroup, ElRadioButton}, data() { return { menus: ラジオボタンの中身が記載されたJSON, } }, methods: { changeRadio(d) { // d: 現在選択されているradioButtonのvalue } } } </script>で、なにをもっても、RadioButtonGroupの作成
RadioButtonGroup<template> <div> <slot></slot> </div> </template> <script> export default { name: 'ElRadioButtonGroup' } </script>実際のRadioButtonはマウント元で記載する(propsとして、データを引き継がない)ので、
<slot>
を準備します。で、親コンポーネントにマウント
parent<template> <div> <el-radio-button-group> <el-radio-button v-for="(menu, index) in menus" :key="index" name = "menutype" :value = "menu.name" :is_def-selected = "最初に選択されているものの条件式でboolean値" :is_required = false :label = "menu.label" @change = "changeRadio" ></el-radio-button> </el-radio-button-group> </div> </template> <script> import ElRadioButtonGroup from '@/components/element/RadioButtonGroup' import ElRadioButton from '@/components/element/RadioButton' export default { name: 'Parent', components: {ElRadioButtonGroup, ElRadioButton}, data() { return { menus: ラジオボタンの中身が記載されたJSON, } }, methods: { changeRadio(d) { // d: 現在選択されているradioButtonのvalue } } } </script>RadioButtonGroupにも、今なにが選択されているのか教える
RadioButtonGroup⇒それぞれのRadioButtonにそれぞれどの状態になっていて欲しいか通知するため、RadioButtonGroupに、現状を教える
parent<template> <div> <el-radio-button-group :value="menu_now"> <el-radio-button v-for="(menu, index) in menus" ~省略~ @change = "changeRadio" ></el-radio-button> </el-radio-button-group> </div> </template> <script> ~省略~ data() { return { menus: ラジオボタンの中身が記載されたJSON, menu_now: デフォルトのメニューのvalueが入るようにする } }, methods: { changeRadio(d) { this.menu_now = d } } } </script>RadioButtonGroupからRadioButtonへ通達
RadioButtonGroup<template> ~省略~ </template> <script> export default { props: ['value'], watch: { value(nv) { this.$children.forEach(item => { if (item.value == nv) { item.is_selected = true } else { item.is_selected = false } }) } }, } </script>RadioButtonで受け取って、class名追加
RadioButton<template> <label class="radio" :class="{ 'is_selected' : is_selected}"> ~省略~ </label> </template> <script> ~省略~ data() { return { is_selected: this.is_defSelected //最初は初期値の設定を受け取る } }, computed: { selectedItem: { get: function() { if ( this.is_selected ) { //デフォルト設定ではなく、変更通知の受け取りフラグをもとにする return this.value } }, ~省略~ </script>まとめ
RadioButtonのチェック状態(is_defSelected)をBoolean値で持たせたことが、不幸のはじまりのような気がしました。
- 投稿日:2020-02-12T17:02:36+09:00
VueやNuxtでpugやstylusを使う設定
僕はpythonが好きで閉じタグを書くのがしんどく感じるのでpugやstylusが好きです。
coffeescriptはtypescriptに負けてて最近使ってませんが・・・
というわけでvueでも使えるようにしたいので毎回している初期の設定をメモしておこうと思います。loaderの用意
pugやstylusを利用するにはnpmでloaderを用意する必要があります。
pug
$ npm i --save-dev pug pug-plain-loaderstylus
$ npm i --save-dev stylus stylus-loader.vueファイルで
htmlの部分は
<template>
に、cssは<style>
にlang属性をいれます。<template lang="pug"> </template> <style lang="stylus"> </style>全体で
.styl
を読み込む共通の.stylファイルを.vueに書きたくないことや、どのvueファイルからでもmixinを呼び出したいときなどもあるでしょう。
そういった場合、@nuxtjs/style-resourcesを利用します。(nuxtの話)
npmでインストールして、$ npm i --save-dev @nuxtjs/style-resources共通で読み込みたい.stylファイルを
/assets/stylus/xxx.styl'
とすると
nuxt.config.js
に以下の記述を追記します。nuxt.config.jsexport default { modules: [ '@nuxtjs/style-resources', ], styleResources: { stylus: './assets/stylus/xxx.styl' } }複数の.stylファイルを読むこともできますが、メインの.stylを1つ読み込んでそのなかに
@import
を書いて上げる方法で良いと思います。nuxt-stylus-resources-loaderを利用して共通の.stylを読み込む方法もありますが、おそらく@nuxtjs/style-resourcesの方が推奨されます。
- 投稿日:2020-02-12T12:38:56+09:00
Vue.jsでドロワーメニュー(スライドメニュー)を実装
codesandboxに書いてみたので、動きはこれを見ていただけると!(App.vue参照)
https://codesandbox.io/embed/objective-flower-kuov7?fontsize=14&hidenavigation=1&theme=dark以下、ざっくり内容をざっくりメモ程度に。
template<template> <div id="app"> <div> <button @click="openDrawerMenu">ボタン</button> </div> <transition name="right"> <div v-if="drawerFlg" class="drawer-menu-wrapper"> <div class="drawer-menu"> <!-- ここにメニューの内容を書いていく --> </div> </div> </transition> </div> </template>script<script> export default { name: "App", data() { return { drawerFlg: false }; }, methods: { openDrawerMenu() { this.drawerFlg = true; } } }; </script>style<style> //右から出したい場合 .right-enter-active, .right-leave-active { transform: translate(0px, 0px); transition: transform 225ms cubic-bezier(0, 0, 0.2, 1) 0ms; } .right-enter, .right-leave-to { transform: translateX(100vw) translateX(0px); } //左から出したい場合 .left-enter-active, .left-leave-active { transform: translate(0px, 0px); transition: transform 225ms cubic-bezier(0, 0, 0.2, 1) 0ms; } .left-enter, .left-leave-to { transform: translateX(-100vw) translateX(0px); } //以下、メニューの形に合わせて良い具合に変更してください .drawer-menu-wrapper { position: absolute; z-index: 10; top: 0; right: 0; //右に出す場合 left: 0 //左に出す場合 width: 50%; height: 100%; background-color: white; } .drawer-menu { padding: 24px; } </style>
- 投稿日:2020-02-12T08:57:04+09:00
Vue.js (+TypeScript) × microCMSでお問い合わせフォームを作成する
経緯
前記事(ヘッドレスCMS × Static Site GeneratorでJamstackな爆速表示サイトを作ってみるチュートリアル)で、
できれば、(axiosとかで叩ける)お問い合わせ用APIとかあれば良いけど、なにか良いサービスないかな
と書いたところ、早速
こちらmicroCMSがPOSTに対応したのでお使いいただけるかと...!ぜひお試しください?
API経由でコンテンツを作成できるようになりました?
https://microcms.io/blog/http_post_api/とお知らせが!!
というわけで、勢いでお問い合わせフォームを実装してみます。
※今回は、とりあえずVueでローカルでサクッと試したのでJamstackではないです?実装
環境
Console% node -v v12.13.0 % npm -v 6.13.6プロジェクトの作成
Vue CLIを使ってお好みの設定でプロジェクトを作成します。
Console% npx @vue/cli create vue-contact-demo Vue CLI v4.2.2 ? Please pick a preset: Manually select features ? Check the features needed for your project: TS, Linter ? Use class-style component syntax? Yes ? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? No ? Pick a linter / formatter config: Prettier ? Pick additional lint features: (Press <space> to select, <a> to toggle all, <i> to invert selection)Lint o n save ? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files ? Save this as a preset for future projects? No開発サーバの起動
Console% cd vue-contact-demo % npm run serve
ブラウザで
http://localhost:8080/
へアクセスし表示確認を行います。
microCMSの設定
フォームの作成
APIを呼び出すために、axiosを追加します。
Console% npm i axios % npm i -D @types/axios
フォームを実装します。
src/components/ContactForm.vue<template> <form> <dl> <dt>名前</dt> <dd><input type="text" v-model="inputData.name"></dd> <dt>お問い合わせ内容</dt> <dd><textarea v-model="inputData.text"></textarea></dd> </dl> <button type="button" @click="send">送信</button> </form> </template> <script lang="ts"> import { Component, Vue } from "vue-property-decorator"; import axios from 'axios'; interface contactData { name:string text:string } @Component export default class ContactForm extends Vue { /** * 入力データ */ private inputData: contactData = { name: '', text: '' }; /** * 送信処理 */ private async send(): Promise<void> { await axios.post( // url 'https://jamstack-demo.microcms.io/api/v1/contact', // data this.inputData, // config { headers: { 'Content-Type': 'application/json', 'X-WRITE-API-KEY': '86f1db0b-f655-4568-b32f-f8be991f150f', }, } ); } } </script>?ざざっと作ってみただけなので、ちゃんと実装する場合は、しっかり設計、ファイル分割しましょう。
とりあえず、メインのコンポーネントに追加します。
src/App.vue<template> <div id="app"> <img alt="Vue logo" src="./assets/logo.png" /> <HelloWorld msg="Welcome to Your Vue.js + TypeScript App" /> + <ContactForm /> </div> </template> <script lang="ts"> import { Component, Vue } from "vue-property-decorator"; import HelloWorld from "./components/HelloWorld.vue"; + import ContactForm from "./components/ContactForm.vue"; @Component({ components: { - HelloWorld + HelloWorld, + ContactForm } })動作確認
ブラウザで
http://localhost:8080/
へアクセスし、フォームを投稿し、
microCMS側で投稿確認を行います。
まとめ
- ちょっとしたユーザ→運営のアクションであればアリかなと思います。
- microCMS側の設定で、コンテンツ投稿時(今回の場合は、お問い合わせがあった時)にslackやchatworkに投稿することができます。
- 実プロジェクトで導入する場合は、コンテンツの投入上限があるため、(最近よくある)画像認証などを使って、できるだけスパムとかは除きたいところ。。。。
以上です。
- 投稿日:2020-02-12T06:39:44+09:00
【環境構築】Rails6 + Vue.jsで hello_vue.jsの名前をmain.jsに変更したいときの手順
はじめに
先日、こちらの記事を書きました。
【環境構築】Docker + Rails6 + Vue.js + Vuetifyの環境構築手順 - Qiitaそこで自動生成される、
app/javascript/hello_vue.js
の名前をmain.js
に変更したい方のために手順を記載しておきます。(対象が狭い!笑)環境
OS: macOS Catalina 10.15.3 Ruby: 2.6.5 Rails: 6.0.2.1 Docker: 19.03.5 docker-compose: 1.24.1 Vue: 2.6.10 vuetify: 2.1.0前提
こちらの記事に沿って環境構築が完了したとします。
1.ファイル名を変更
app/javascript/packs/hello_vue.js
の名前を変更します。
hello_vue.js
↓
main.js
2.home/index.html/erbを変更
app/views/home/index.html.erb<%= javascript_pack_tag 'hello_vue' %> <%= stylesheet_pack_tag 'hello_vue' %>app/views/home/index.html.erb<%= javascript_pack_tag 'main' %> <%= stylesheet_pack_tag 'main' %>3.
manifest.json
内を変更最後に、Webpackが出力した
manifest.json
をいじる必要があります。public/packs/manifest.json{ // 略 "entrypoints": { // 略 "hello_vue": { "js": [ "/packs/js/hello_vue-343ebbfc5aed29c10bf6.js" ], "js.map": [ "/packs/js/hello_vue-343ebbfc5aed29c10bf6.js.map" ] }, // 略 }, "hello_vue.js": "/packs/js/hello_vue-343ebbfc5aed29c10bf6.js", "hello_vue.js.map": "/packs/js/hello_vue-343ebbfc5aed29c10bf6.js.map", // 略 }↓
public/packs/manifest.json{ // 略 "entrypoints": { // 略 "main": { "js": [ "/packs/js/main-343ebbfc5aed29c10bf6.js" ], "js.map": [ "/packs/js/main-343ebbfc5aed29c10bf6.js.map" ] }, // 略 }, "main.js": "/packs/js/main-343ebbfc5aed29c10bf6.js", "main.js.map": "/packs/js/main-343ebbfc5aed29c10bf6.js.map", // 略 }以上です!
おわりに
最後まで読んで頂きありがとうございました
どなたかの参考になれば幸いです
前回の記事
- 投稿日:2020-02-12T00:51:58+09:00
cssのfilterプロパティのプレイグラウンドを作ってみた
- 投稿日:2020-02-12T00:01:31+09:00
EmacsでVueファイルを編集するときの設定
Emacsでvueファイルの編集をするときの設定です。
ありがたいことにVueを書く機会が増えてきたので、ちゃんと設定しておこうかなと。
vue-mode
という拡張を使ってみたけど、全然シンタックスハイライトしなかったので諦めました。
vue-modeはどうやらmmm-mode
という拡張を使用しているとのことだったので、「じゃあmmm-modeとやらで自分で設定すれば良いんじゃね?」と思い設定したわけです。Vueファイル編集するときの設定
結論から。
色々と問題は有りますが、下記のような設定に落ち着きました。;; ************************************************************************ ;; mmm-mode ;; ************************************************************************ (use-package mmm-mode :ensure t :config (setq mmm-global-mode 'maybe) (setq mmm-submode-decoration-level 2) (set-face-background 'mmm-default-submode-face "gray13") (mmm-add-classes '((vue-embeded-slim-mode :submode slim-mode :front "^<template.*lang=\"pug\">\n" :back "^</template>") (vue-embeded-web-mode :submode web-mode :front "^<template>\n" :back "^</template>\n") (vue-embeded-js-mode :submode js-mode :front "^<script>\n" :back "^</script>") (vue-embeded-scss-mode :submode scss-mode :front "^<style.*lang=\"scss\">\n" :back "^</style>"))) (mmm-add-mode-ext-class nil "\\.vue\\'" 'vue-embeded-slim-mode) (mmm-add-mode-ext-class nil "\\.vue\\'" 'vue-embeded-web-mode) (mmm-add-mode-ext-class nil "\\.vue\\'" 'vue-embeded-js-mode) (mmm-add-mode-ext-class nil "\\.vue\\'" 'vue-embeded-scss-mode))設定の解説
サブモードが適用されている範囲の背景色を設定する
背景色を
gray13
に設定しています。(set-face-background 'mmm-default-submode-face "gray13")補足)
色指定は#xxxxxx
形式でも良いですが、M-x list-colors-display
でも確認できます。サブモードの設定をする
(vue-embeded-slim-mode ; サブモード名 :submode slim-mode ; 適用させるモード名 :front "^<template.*lang=\"pug\">\n" ; どこから :back "^</template>") ; どこまでfront と back は正規表現で指定します。
正規表現はM-x regexp-builder
でtry and errorしながら設定しました。
Vueの場合は下記のようなケースがあるので、^
を指定してあげないとだめです。
(私のVueの書き方がおかしいのかもしれない気がしてきた。)<template> <div id="hoge"> <h1>Hoge</h1> <template> <h2>fuga</h2> </template> </div> </template>mmm-modeについて
1つのメジャーモードの中で複数のメジャーモードを適用させることができるパッケージです。
例えば、hoge.rb
ファイルを開いたときにメジャーモードとしてruby-mode
が適用される設定の場合、コードのとある箇所はjs-mode
を適用する、みたいなことができるようになります。(そんなことしたいシチュエーションは無いと思うが...)cf. https://github.com/purcell/mmm-mode
最後に
- 予測変換が動かないぽい(company)
- 別に背景色は変えなくても良いかな
- たまにインデントが変な感じになる(特にslim-mode)
- 都度
web-mode-reload
しないといけなさそう
- 案1) saveのときにhookさせるか
- Land of Lispって本読み中だよ〜
VSCode便利すぎない?参考