- 投稿日:2020-03-14T23:42:05+09:00
Rails + Vue.js + AjaxでCRUDのサンプルプロジェクト [Hello World]
Vue.js初心者が公式サイトで基礎を学んだ後に作るVue.js + AjaxによるCRUD(作成/読み込み/更新/削除)のサンプルプロジェクトです。
動作確認はChrome、FireFox、Microsoft Edge、IE11です。恐らくマックさんのブラウザでも動作するはずです。
DEMO
https://www.petitmonte.com/rails-demo/vue_crud
ソース一式
https://github.com/TakeshiOkamoto/mpp_vue_crud
※学習用の為、ライセンスはパブリックドメイン
- 投稿日:2020-03-14T23:31:40+09:00
API Gatewayでリクエストして、Lambdaで処理させて、AppSyncで受け取る
API Gatewayでリクエストして、Lambdaで処理させて、AppSyncで受け取る
この記事はサーバーレスWebアプリ Mosaicを開発して得た知見を振り返り定着させるためのハンズオン記事の1つです。
以下を見てからこの記事をみるといい感じです。
はじめに
検出した顔を並べて表示しました。次はいよいよ顔にモザイクをかけてゆきます。
処理をキックするためにAPI GatewayとLambdaを利用します。また、実行結果はAppSyncのSubscriptionで受け取ります。顔の位置情報を渡す
前回の記事(Lambda(Python) + Rekognition で顔検出)では顔を検出して切抜き画像を作って表示するだけでした。この時に顔の位置情報(座標)が分かってます。モザイク処理する時にこの座標が必要になりますのでAppSync経由で渡してあげましょう。
(キックした後にまた検出し直すこともできますが、それだと無駄が多いですよね。)以前書いた顔検出するサンプルコードにpointsという位置情報を入れておくための変数を追加します。
lambda_function.pydef uploadImage(image, localFilePath, bucket, s3Key, group, points): logger.info("start uploadImage({0}, {1}, {2}, {3})".format(localFilePath, bucket, s3Key, group)) try: cv2.imwrite(localFilePath, image) s3.upload_file(Filename=localFilePath, Bucket=bucket, Key=s3Key) apiCreateTable(group, s3Key, points) except Exception as e: logger.exception(e) raise e finally: if os.path.exists(localFilePath): os.remove(localFilePath) def apiCreateTable(group, path, points): logger.info("start apiCreateTable({0}, {1}, {2})".format(group, path, points)) try: query = gql(""" mutation create {{ createSampleAppsyncTable(input:{{ group: \"{0}\" path: \"{1}\" points: \"{2}\" }}){{ group path points }} }} """.format(group, path, points)) _client.execute(query) except Exception as e: logger.exception(e) raise e def detectFaces(bucket, key, fileName, image, group, dirPathOut): logger.info("start detectFaces ({0}, {1}, {2}, {3}, {4})".format(bucket, key, fileName, group, dirPathOut)) try: response = rekognition.detect_faces( Image={ "S3Object": { "Bucket": bucket, "Name": key, } }, Attributes=[ "DEFAULT", ] ) name, ext = os.path.splitext(fileName) imgHeight = image.shape[0] imgWidth = image.shape[1] index = 0 for faceDetail in response["FaceDetails"]: index += 1 faceFileName = "face_{0:03d}".format(index) + ext box = faceDetail["BoundingBox"] x = max(int(imgWidth * box["Left"]), 0) y = max(int(imgHeight * box["Top"]), 0) w = int(imgWidth * box["Width"]) h = int(imgHeight * box["Height"]) points = "{0},{1}|{2},{3}|{4},{5}|{6},{7}".format(x, y, x+w, y, x+w, y+h, x, y+h) logger.info("BoundingBox({0},{1},{2},{3})".format(x, y, w, h)) faceImage = image[y:min(y+h, imgHeight-1), x:min(x+w, imgWidth)] localFaceFilePath = os.path.join("/tmp/", faceFileName) uploadImage(faceImage, localFaceFilePath, bucket, os.path.join(dirPathOut, faceFileName), group, points) cv2.rectangle(image, (x, y), (x+w, y+h), (0, 0, 255), 3) processedFileName = "faces-" + fileName processedFilePath = "/tmp/" + processedFileName uploadImage(image, processedFilePath, bucket, os.path.join(dirPathOut, processedFileName), group, points) except Exception as e: logger.exception(e) raise eAppSyncのスキーマも更新しましょう。
AWSコンソール > AppSync > 目的のAPI > スキーマ
input(引数)とtype(戻り値)のデータに points: String を加えておきます。input CreateSampleAppsyncTableInput { group: String! path: String! points: String } type SampleAppsyncTable { group: String! path: String! points: String }フロントのWebアプリ側も更新します。
getListやsubscriptionでpointsを受け取るため、graphqlファイルを更新します。src/graphql/queries.jsexport const listSampleAppsyncTables = `query listSampleAppsyncTables($group: String) { listSampleAppsyncTables( limit: 1000000 filter: { group: {eq:$group} } ) { items { group path points } } } `;src/graphql/subscriptions.jsexport const onCreateSampleAppsyncTable = `subscription OnCreateSampleAppsyncTable($group: String) { onCreateSampleAppsyncTable(group : $group) { group path points } } `;さぁこれで、検出した顔の座標をクライアント側に渡すことができるようになりました。
受け取ったpointsをメンバ変数にセットしたり、ログに出したりして確認してみましょう。src/components/List.vue: <script> : async getList() { this.group = this.$route.query.group; console.log("group : " + this.group); if(!this.group){ return; } let apiResult = await API.graphql(graphqlOperation(listSampleAppsyncTables, { group : this.group })); let listAll = apiResult.data.listSampleAppsyncTables.items; for(let data of listAll) { let tmp = { path : data.path, image : "", points : data.points }; let list = [...this.dataList, tmp]; this.dataList = list; console.log("path : " + data.path); console.log("points : " + data.points); Storage.get(data.path.replace('public/', ''), { level: 'public', expires: dataExpireSeconds }).then(result => { tmp.image = result; console.log("image : " + result); }).catch(err => console.log(err)); } API.graphql( graphqlOperation(onCreateSampleAppsyncTable, { group : this.group } ) ).subscribe({ next: (eventData) => { let data = eventData.value.data.onCreateSampleAppsyncTable; let tmp = { path : data.path, image : "", points : data.points }; let list = [...this.dataList, tmp]; this.dataList = list; console.log("path : " + data.path); console.log("points : " + data.points); Storage.get(data.path.replace('public/', ''), { level: 'public', expires: dataExpireSeconds }).then(result => { tmp.image = result; console.log("image : " + result); }).catch(err => console.log(err)); } }); }, :顔を選択するための実装は今回は割愛し、検出された全ての顔にモザイクをかけるようにしたいと思います。
全ての顔にモザイクをかける処理を実行するためのボタンを設置し、押下したらAPI Gatewayに対して処理をリクエストします。モザイク処理された画像は他の画像と同様にS3にアップロードしたらAppSync経由でパスを通知し、クライアント側でそれを受け取るような流れです。API GatewayとLambdaのセットアップ
以下のような流れでAPI GatewayとLambdaをセットアップしてゆきます。
Lambdaの作成
先にLambdaの関数を作成しておきます。
AWSコンソール > Lambda > 関数 > 関数の作成
一から作成, 任意の関数名, ランタイムにはPython3.6, 実行ロールは「基本的な Lambda アクセス権限で新しいロールを作成」を選択して作成します。
関数名は「sample_lambda_apply」としておきました。関数コードはひとまずインラインで以下のようにしておきます。
lambda_function.pyimport json import logging logger = logging.getLogger() logger.setLevel(logging.INFO) def lambda_handler(event, context): logger.info("Hello from lambda! - sample_lambda_apply") return { 'statusCode': 200, 'body': json.dumps('Hello from Lambda! - sample_lambda_apply') }LambdaのIAMロール編集
LambdaがS3操作やAppSyncアクセスできるようにします。
AWSコンソール > IAM > ロール
Lambdaと一緒に作成された sample_lambda_apply-role-xxxxxxxx というロールを表示します。
アクセス権限タブの「+インラインポリシーの追加」から追加してください。
ポリシーのJSONは以下のような感じになります。{ "Version": "2012-10-17", "Statement": [ { "Action": [ "s3:PutObject", "s3:GetObject", "appsync:GraphQL" ], "Resource": [ "arn:aws:s3:::sample-vue-project-bucket-work/*", "arn:aws:appsync:ap-northeast-1:888888888888:apis/xxxxxxxxxxxxxxxxxxxxxxxxxx/*" ], "Effect": "Allow" } ] }API Gatewayの作成
続いて、API Gatewayを作成します。
AWSコンソール > API Gateway
APIを作成ボタンを押下し、APIタイプを選択画面でREST APIを選択します。
REST, 新しいAPI, 任意の名前を設定し, エンドポイントタイプはリージョンとし、APIの作成ボタンを押下します。
processという名前の子リソースを作成し、そこにPOSTメソッドを作成してゆこうと思います。リソースを作成する際、CORSを有効にするチェックはONにしておきましょう。
POSTのセットアップでは、統合タイプはLambda関数とし、Lambda関数は先程作成した「sample_lambda_apply」を選択し、保存ボタンを押下します。
CORSの有効化をしないと、クライアント側からリクエストした際に以下のような例外が発生すると思います。
Access to XMLHttpRequest at 'https://j2byqj306a.execute-api.ap-northeast-1.amazonaws.com/work/process' from origin 'https://fed9513d88324171b593944f5acca30f.vfs.cloud9.ap-northeast-1.amazonaws.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. Error: Network Error at createError (createError.js?2d83:16) at XMLHttpRequest.handleError (xhr.js?b50d:81)CORSの有効化をしたら、APIのデプロイも再実施しましょう。
念の為テストを実行してstatusCode=200が返ってくることも確認しておきましょう。
確認できたら、APIのデプロイから任意のステージ名を設定し、デプロイを行っておいてください。
VueのWebアプリからAPI Gatewayに対してリクエストする
WebアプリからAPI Gatewayを呼べるようにします。
src/components/List.vue: <v-list> <v-list-item v-for="data in this.dataList" :key="data.path"> <v-list-item-content> <a :href="data.image" target=”_blank”> <v-list-item-title v-text="data.path"></v-list-item-title> </a> </v-list-item-content> <v-list-item-avatar> <v-img :src="data.image"></v-img> </v-list-item-avatar> </v-list-item> </v-list> <v-btn v-if="dataList.length > 0" @click="processMosaic"> PROCESS MOSAIC </v-btn> : <script> import { API, graphqlOperation, Storage } from 'aws-amplify'; import { listSampleAppsyncTables } from "../graphql/queries"; import { onCreateSampleAppsyncTable } from "../graphql/subscriptions"; import axios from 'axios'; const apiUrl = "https://j2byqj306a.execute-api.ap-northeast-1.amazonaws.com/work/process"; const config = {headers: { 'Content-Type': 'application/json' }} :  processMosaic() { let pointsList = []; let orgKey = ""; for(let index = 0; index < this.dataList.length; index++) { let data = this.dataList[index]; if(data.points != "-"){ pointsList.push(data.points); }else if(data.path.startsWith("processed") == false){ orgKey = data.path; } } this.myGuid = this.getGUIDString(new Date()); let requestData = { guid: this.myGuid, orgKey: orgKey, pointsList: pointsList }; console.log(requestData); axios .post(apiUrl, requestData, config) .then(response => { let result = response.data console.log(result) }) .catch(error => console.log(error)) }, getGUIDString(date){ let random = date.getTime() + Math.floor(100000 * Math.random()); random = Math.random() * random; random = Math.floor(random).toString(16); return random; }, :PROCESS MOSAICボタンを押したらstatusCode=200が返ってくることを確認しておきましょう。
Lambdaの実装
Lambdaファンクションの実装をしてゆきます。
以前の記事「Lambda + OpenCVで画像処理 (グレー画像作成)」と同じ要領で必要なライブラリをインストールし、lambdafunction.pyを実装し、zip圧縮して、Lambdaにデプロイします。必要なライブラリのインストール
$ pip install opencv-python -t . $ pip install gql -t .実装
lambda_function.py# coding: UTF-8 import json import boto3 import os import datetime import numpy as np import cv2 import logging logger = logging.getLogger() logger.setLevel(logging.INFO) s3 = boto3.client("s3") BUCKET_INPUT = "sample-vue-project-bucket-work" BUCKET_OUTPUT = "sample-vue-project-bucket-work" from gql import gql, Client from gql.transport.requests import RequestsHTTPTransport ENDPOINT = "https://xxxxxxxxxxxxxxxxxxxxxxxxxx.appsync-api.ap-northeast-1.amazonaws.com/graphql" API_KEY = "da2-XXXXXXXXXXXXXXXXXXXXXXXXXX" _headers = { "Content-Type": "application/graphql", "x-api-key": API_KEY, } _transport = RequestsHTTPTransport( headers = _headers, url = ENDPOINT, use_json = True, ) _client = Client( transport = _transport, fetch_schema_from_transport = True, ) def lambda_handler(event, context): try: logger.info(event) guid = event["guid"] orgKey = event["orgKey"] pointsList = event["pointsList"] pathList = orgKey.split("/") name, ext = os.path.splitext(os.path.basename(orgKey)) fileName = "mosaic.w2or3w.com." + guid + ext dirPath = os.path.dirname(orgKey) dirName = os.path.basename(dirPath) rootDirName = pathList[0] localTmpPath = u'/tmp/' + os.path.basename(orgKey) s3.download_file(Bucket = BUCKET_INPUT, Key = orgKey, Filename = localTmpPath) before = cv2.imread(localTmpPath) after = cv2.imread(localTmpPath) after = mosaicFromPointsList(pointsList, before, after) uploadAppliedImage(after, BUCKET_OUTPUT, os.path.join(rootDirName, "processed", dirName), fileName) return { 'statusCode': 200, 'body': json.dumps("completed") } except Exception as e: logger.exception(e) return { "statusCode": 500, "body": json.dumps("failed") } finally: if os.path.exists(localTmpPath): os.remove(localTmpPath) def mosaicFromPointsList(pointsList, before, after): try: height = before.shape[0] width = before.shape[1] mosaicImg = mosaic(before, 0.08) mask = np.tile(np.uint8(0), (height, width, 1)) for points in pointsList: pointList = points.split('|') lt = Point(pointList[0]) rt = Point(pointList[1]) rb = Point(pointList[2]) lb = Point(pointList[3]) contours = np.array( [ [lt.x, lt.y], [rt.x, rt.y], [rb.x, rb.y], [lb.x, lb.y], ] ) cv2.fillConvexPoly(mask, contours, color=(255, 255, 255)) after = np.where(mask != 0, mosaicImg, after) return after except Exception as e: logger.exception(e) raise e def mosaic(src, ratio=0.1): try: small = cv2.resize(src, None, fx=ratio, fy=ratio, interpolation=cv2.INTER_NEAREST) return cv2.resize(small, src.shape[:2][::-1], interpolation=cv2.INTER_NEAREST) except Exception as e: logger.exception(e) raise e def uploadAppliedImage(img, bucket, dirPath, name): tmp = "/tmp/" + name guid = os.path.basename(dirPath) s3key = dirPath + "/" + name try: cv2.imwrite(tmp, img) s3.upload_file(Filename=tmp, Bucket=bucket, Key=s3key) apiCreateMosaicTable(guid, s3key) except Exception as e: logger.exception(e) raise e finally: if os.path.exists(tmp): os.remove(tmp) def apiCreateMosaicTable(guid, s3key): logger.info("apiCreateMosaicTable : guid={0}, s3key={1}".format(guid, s3key)) time = datetime.datetime.now() time = time + datetime.timedelta(minutes=30) epocTime = int(time.timestamp()) try: query = gql(""" mutation create {{ createSampleAppsyncTable(input:{{ group: \"{0}\" path: \"{1}\" points: \"-\" deleteTime: {2} }}){{ group path points }} }} """.format(guid, s3key, epocTime)) _client.execute(query) except Exception as e: logger.exception(e) raise e class Point: def __init__(self, text): tmp = text.strip("("")") tmpList = tmp.split(',') self.x = int(tmpList[0]) self.y = int(tmpList[1])モザイク処理の詳細についてはこちらの記事(画像に様々な形のモザイクをかける(Python, OpenCV))も見てみてください。
Lambdaにデプロイしたら、Webアプリに追加したPROCESS MOSAICボタンを押下しましょう。顔にモザイク処理された画像が追加されましたね。
API GatewayにCognito認証を設定する
現状、API Gatewayには認証制限を設けてません。呼び放題です。
フロント側はCognito認証を利用してますので、このCognito認証をAPI Gatewayにも適用させましょう。AWSコンソール > API Gateway > 作成したAPI > オーソライザー
「+オーソライザーの作成」ボタンを押下。Cognitoユーザープールのオーソライザーを作成します。
POSTメソッドリクエストを編集します。
認可に対して作成したCognitoユーザープールオーソライザーを指定、HTTPリクエストヘッダに「Authorization」を必須にして追加します。
そして、APIをデプロイしましょう。
WebアプリのPROCESS MOSAICボタンを押すと、以下のような例外が帰ってくるようになりました。Access to XMLHttpRequest at 'https://j2byqj306a.execute-api.ap-northeast-1.amazonaws.com/work/process' from origin 'https://fed9513d88324171b593944f5acca30f.vfs.cloud9.ap-northeast-1.amazonaws.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. List.vue?9185:114 Error: Network Error at createError (createError.js?2d83:16) at XMLHttpRequest.handleError (xhr.js?b50d:81)CORSの有効化をしない時の例外と同じ感じでイマイチ判断つかないですが、とにかく失敗し、Cognito認証が効いてるらしいことが確認できました。
それでは、Webアプリの実装を更新してゆきましょう。
API GatewayにリクエストするHeaderに、Cognito認証でLocal Storageに保存されているidTokenの値を設定してあげます。
<script> : const currSession = await Auth.currentSession(); config.headers["Authorization"] = currSession.getIdToken().getJwtToken(); axios .post(apiUrl, requestData, config) .then(response => { let result = response.data console.log(result) }) .catch(error => console.log(error)) :WebアプリのPROCESS MOSAICボタンを押すと、成功するようになりましたね。
あとがき
久しぶりにこのシリーズの記事を追加しました。
薄々分かっていましたが、やっぱりこの記事は内容が盛りだくさんになってしまいました。最近AppSync関係の記事にお熱で、本当はAppSyncのデータソースを複数登録するヤツをやりたいと思っているのです。モザイク処理をキックする手段として、この記事でも書いた通りAPI Gatewayを利用しているのですが、AppSyncだけでイケるんじゃね?って思いまして。
それを実際にやってみて記事を書くにあたり、API Gatewayでやってるこの記事が必要だなと思い、超重たい腰を上げて書いた次第です。
後回しにした上にこんな不純な動機で、ごめんね、API Gateway。
- 投稿日:2020-03-14T23:25:39+09:00
Vue.js、Capacitorでアプリ開発
ウェブ開発ノウハウを利用してスマートフォンアプリの開発を行うハイブリッドアプリについて、ソリューションの一つとしてCapacitor+Vue.jsを考えてみました。
メリットは、最初のプロジェクトの作成が非常に簡単にできることですので、プロジェクトの作成手順を紹介します。Vue.jsプロジェクトの作成
https://qiita.com/jjjjjj/items/a43922631f1350bb9c65
を参照Capacitorのインストール
以下を参考にしました。
https://capacitor.ionicframework.jp/docs/getting-started$ npm install --save @capacitor/core @capacitor/cliCapacitor初期設定
$ npx cap init
index.htmlファイルの場所を変更
capacitor.config.jsonのwebDirをwwwからdistに変更する。
プラットフォームのインストール
$ npx cap add ios $ npx cap add androidVueプロジェクトをネイティブにコピーする
$ npx cap copy ios $ npx cap copy androidIDEを開いてビルド、実行する
$ npx cap open ios $ npx cap open android
- 投稿日:2020-03-14T23:23:50+09:00
CakePHP4 で Vue.js ( Laravel-Mix ) を使えるようにする
CakePHP4 を docker-compose で動くようにする
の続きです。
CakePHP4 のプロジェクトから Vue.js を使えるようにします。
Vue.js は Laravel-Mix を使って導入します。この記事でわかること
- CakePHP4 へ Laravel-Mix 導入し Vue.js を使えるようにします。
- この記事内のソースは以下で公開しています。
事前準備
- Docker Desktop のインストール
- CakePHP4 プロジェクトの作成
- 以下が参考になります。
僕の Mac の各種バージョンは以下です。
バージョン docker Docker version 19.03.5, build 633a0ea docker-compose docker-compose version 1.25.4, build 8d51620a CakePHP4 4.0.4 Vue.js 2.6.11 laravel-mix 5.0.1 手順の流れの説明
Laravel の仮プロジェクト作り、そこから必要なファイルをコピーします。
その後、CakePHP4 のディレクトリ構成に合わせた変更や docker-compose の変更を行います。Laravel 仮プロジェクトの作成
CakePHP プロジェクトと同列(同じディレクトリ)に Laravel の仮プロジェクトを作成します。
ここでは CakePHPプロジェクト「cakephp-vue-study」のディレクトリにいるところ始めるので、まずは1つ上のディレクトリへ移動します。ローカル環境に依存したくないで一時的なコンテナを立ち上げて入ります。
一時的なコンテナが独自なのは、最低限必要なライブラリを入れているためです。
( https://github.com/katsuhiko/docker-php-fpm-base/blob/master/Dockerfile )cd .. docker run --rm -it -v "$(pwd):/home/app" -w /home/app katsuhikonagashima/php-fpm-base:7.4-buster /bin/bashここからはコンテナ内での作業になります。
composer を取得して、Laravelプロジェクトを作ります。
その後、ui として Vue.js を導入します。curl -sS https://getcomposer.org/installer | php php composer.phar create-project --prefer-dist laravel/laravel laravel-template cd laravel-template php ../composer.phar require laravel/ui php artisan ui vue exitコンテナ内での作業はここまでです。
必要なファイルをコピー
CakePHP4プロジェクトへ必要なファイルをコピーします。
Vue.js の各種ファイルは./assets/
配下に格納して開発していきます。
Laravel に合わせて./resources/
配下でも良いと思いますが、自分は locale 系のファイルと一緒にあることに違和感を感じるので./assets/
配下にしています。cd ./cakephp-vue-study/ cp ../laravel-template/package.json ./ cp ../laravel-template/webpack.mix.js ./ mkdir -p ./assets cp -r ../laravel-template/resources/js ./assets/js cp -r ../laravel-template/resources/sass ./assets/sassライブラリのインストール
npm install
も一時的なコンテナを立ち上げて実施します。
一応、現時点で LTS となっている node 12系を使っています。docker run --rm -it -v "$(pwd):/home/app" -w /home/app node:12 npm install必要になると思うので、このタイミングで vue-router を入れても良いと思います。
docker run --rm -it -v "$(pwd):/home/app" -w /home/app node:12 npm install --save-dev vue-routerwebpack.mix.js の変更
./webpack.mix.js
をCakePHP4 のディレクトリ構成にあった設定へ変更します。
./assets/
を./webroot/assets/
配下へビルドした結果が格納されるようにします。webpack.mix.jsmix.setPublicPath('webroot') .js('assets/js/app.js', 'assets/js') .sass('assets/sass/app.scss', 'assets/css') .sourceMaps().webpackConfig({devtool: 'source-map'});javascript, css ファイルにタイムスタンプをつける
javascript, css ファイルのキャッシュが表示されてしまう問題に対応するために CakePHP の Asset.timestamp 機能を使います。
./config/app.php
を変更します。本番でも有効にしたいので、Asset.timestamp を
force
を指定します。config/app.php'Asset' => [ 'timestamp' => 'force', // 'cacheTime' => '+1 year' ],docker-compose.yml への記述追加
./docker-compose.yml
で Vue.js に関するファイルが変更された場合、ビルドを実施するコンテナ watch を追加します。docker-compose.ymlservices: # ... 以下が追加の記述です。 watch: image: node:12 container_name: watch working_dir: /home/app command: "npm run watch" volumes: - ./:/home/app networks: - frontendコンテナを立ち上げ直します。
docker-compose down docker-compose up -d
./webroot/assets/js
,./webroot/assets/css
配下に js ファイルや map ファイルができていたら適切にビルドできています。※ Windowsの場合、変更ファイルの検知ができない可能性があるため
command: "npm run watch"
をcommand: "npm run watch-poll"
にする必要があると思います。.gitignore への記述追加
ライブラリとビルドで作られるファイルを
./.gitignore
へ追加しソース管理の対象から外します。.gitignore# Laravel Mix specific files # ############################## /node_modules /webroot/assets /webroot/mix-manifest.json動作確認のための変更
簡易的な対応を実施して、Vue.js が動作していることを確認します。
routes.php の変更
./config/routes.php
で、どんなアクセスがあっても同じ PagesController#index() が呼び出されるようにします。config/routes.php$routes->scope('/', function (RouteBuilder $builder) { $builder->registerMiddleware('csrf', new CsrfProtectionMiddleware([ 'httpOnly' => true, ])); $builder->applyMiddleware('csrf'); //$builder->connect('/', ['controller' => 'Pages', 'action' => 'display', 'home']); $builder->connect('/pages/*', ['controller' => 'Pages', 'action' => 'display']); // Vue.js のルートページ表示用です。 $builder->connect('/*', ['controller' => 'Pages', 'action' => 'index']); $builder->fallbacks(); });PagesController の変更
./src/Controller/PagesController.php
へ index メソッドを追加します。src/Controller/PagesController.phpclass PagesController extends AppController { // この index メソッドを追加します。 public function index(): ?Response { return $this->render(); } // ... }Pages/index.php の追加
./templates/Pages/index.php
ファイルを追加します。
CakePHP4 から ctp ファイルではなく、php ファイルになったようです。
確認のため、example-component タグを記述しています。templates/Pages/index.php<div id="app"> <example-component></example-component> </div> <?= $this->Html->script('/assets/js/app.js')?>動作確認
http://localhost し以下の文言が表示されていることを確認してください。
Example Component I'm an example component.補足) テストの追加と実行方法
./tests/TestCase/Controller/PagesControllerTest.php
へ PagesController で追加したメソッドのテストを追加します。tests/TestCase/Controller/PagesControllerTest.phpclass PagesControllerTest extends TestCase { // ... 以下の2つのテストを追加します。 /** * @return void */ public function testVueルートページを表示() { $this->get('/'); $this->assertResponseOk(); $this->assertResponseContains('<div id="app">'); } /** * @return void */ public function testどんなアクセスでもVueルートページを表示() { $this->get('/hoge/bar'); $this->assertResponseOk(); $this->assertResponseContains('<div id="app">'); } // ... }コンテナを使って、テストを実行します。
docker exec -it app php composer.phar testコミット前に
composer check
を行うと test と codesniffer による check を行えます。docker exec -it app php composer.phar checkこの composer で実行するコマンドは、 composer.json の scripts に記載があります。
- 投稿日:2020-03-14T23:23:50+09:00
CakePHP4 で Vue.js ( Laravel-Mix ) を使えるようにして docker-compose で動くようにする
CakePHP4 を docker-compose で動くようにする
の続きです。
CakePHP4 のプロジェクトから Vue.js を使えるようにします。
Vue.js は Laravel-Mix を使って導入します。この記事でわかること
- CakePHP4 へ Laravel-Mix 導入し Vue.js を使えるようにします。
- この記事内のソースは以下で公開しています。
事前準備
- Docker Desktop のインストール
- CakePHP4 プロジェクトの作成
- 以下が参考になります。
僕の Mac の各種バージョンは以下です。
バージョン docker Docker version 19.03.5, build 633a0ea docker-compose docker-compose version 1.25.4, build 8d51620a CakePHP4 4.0.4 Vue.js 2.6.11 laravel-mix 5.0.1 手順の流れの説明
Laravel の仮プロジェクト作り、そこから必要なファイルをコピーします。
その後、CakePHP4 のディレクトリ構成に合わせた変更や docker-compose の変更を行います。Laravel 仮プロジェクトの作成
CakePHP プロジェクトと同列(同じディレクトリ)に Laravel の仮プロジェクトを作成します。
ここでは CakePHPプロジェクト「cakephp-vue-study」のディレクトリにいるところ始めるので、まずは1つ上のディレクトリへ移動します。ローカル環境に依存したくないで一時的なコンテナを立ち上げて入ります。
一時的なコンテナが独自なのは、最低限必要なライブラリを入れているためです。
( https://github.com/katsuhiko/docker-php-fpm-base/blob/master/Dockerfile )cd .. docker run --rm -it -v "$(pwd):/home/app" -w /home/app katsuhikonagashima/php-fpm-base:7.4-buster /bin/bashここからはコンテナ内での作業になります。
composer を取得して、Laravelプロジェクトを作ります。
その後、ui として Vue.js を導入します。curl -sS https://getcomposer.org/installer | php php composer.phar create-project --prefer-dist laravel/laravel laravel-template cd laravel-template php ../composer.phar require laravel/ui php artisan ui vue exitコンテナ内での作業はここまでです。
必要なファイルをコピー
CakePHP4プロジェクトへ必要なファイルをコピーします。
Vue.js の各種ファイルは./assets/
配下に格納して開発していきます。
Laravel に合わせて./resources/
配下でも良いと思いますが、自分は locale 系のファイルと一緒にあることに違和感を感じるので./assets/
配下にしています。cd ./cakephp-vue-study/ cp ../laravel-template/package.json ./ cp ../laravel-template/webpack.mix.js ./ mkdir -p ./assets cp -r ../laravel-template/resources/js ./assets/js cp -r ../laravel-template/resources/sass ./assets/sassライブラリのインストール
npm install
も一時的なコンテナを立ち上げて実施します。
一応、現時点で LTS となっている node 12系を使っています。docker run --rm -it -v "$(pwd):/home/app" -w /home/app node:12 npm install必要になると思うので、このタイミングで vue-router を入れても良いと思います。
docker run --rm -it -v "$(pwd):/home/app" -w /home/app node:12 npm install --save-dev vue-routerwebpack.mix.js の変更
./webpack.mix.js
をCakePHP4 のディレクトリ構成にあった設定へ変更します。
./assets/
を./webroot/assets/
配下へビルドした結果が格納されるようにします。webpack.mix.jsmix.setPublicPath('webroot') .js('assets/js/app.js', 'assets/js') .sass('assets/sass/app.scss', 'assets/css') .sourceMaps().webpackConfig({devtool: 'source-map'});javascript, css ファイルにタイムスタンプをつける
javascript, css ファイルのキャッシュが表示されてしまう問題に対応するために CakePHP の Asset.timestamp 機能を使います。
./config/app.php
を変更します。本番でも有効にしたいので、Asset.timestamp へ
force
を指定します。config/app.php'Asset' => [ 'timestamp' => 'force', // 'cacheTime' => '+1 year' ],docker-compose.yml への記述追加
./docker-compose.yml
で Vue.js に関するファイルが変更された場合、ビルドを実施するコンテナ watch を追加します。docker-compose.ymlservices: # ... 以下が追加の記述です。 watch: image: node:12 container_name: watch working_dir: /home/app command: "npm run watch" volumes: - ./:/home/app networks: - frontendコンテナを立ち上げ直します。
docker-compose down docker-compose up -d
./webroot/assets/js
,./webroot/assets/css
配下に js ファイルや map ファイルができていたら適切にビルドできています。※ Windowsの場合、変更ファイルの検知ができない可能性があるため
command: "npm run watch"
をcommand: "npm run watch-poll"
にする必要があると思います。.gitignore への記述追加
ライブラリとビルドで作られるファイルを
./.gitignore
へ追加しソース管理の対象から外します。.gitignore# Laravel Mix specific files # ############################## /node_modules /webroot/assets /webroot/mix-manifest.json動作確認のための変更
簡易的な対応を実施して、Vue.js が動作していることを確認します。
routes.php の変更
./config/routes.php
で、どんなアクセスがあっても同じ PagesController#index() が呼び出されるようにします。config/routes.php$routes->scope('/', function (RouteBuilder $builder) { $builder->registerMiddleware('csrf', new CsrfProtectionMiddleware([ 'httpOnly' => true, ])); $builder->applyMiddleware('csrf'); //$builder->connect('/', ['controller' => 'Pages', 'action' => 'display', 'home']); $builder->connect('/pages/*', ['controller' => 'Pages', 'action' => 'display']); // Vue.js のルートページ表示用です。 $builder->connect('/*', ['controller' => 'Pages', 'action' => 'index']); $builder->fallbacks(); });PagesController の変更
./src/Controller/PagesController.php
へ index メソッドを追加します。src/Controller/PagesController.phpclass PagesController extends AppController { // この index メソッドを追加します。 public function index(): ?Response { return $this->render(); } // ... }Pages/index.php の追加
./templates/Pages/index.php
ファイルを追加します。
CakePHP4 から ctp ファイルではなく、php ファイルになったようです。
確認のため、example-component タグを記述しています。templates/Pages/index.php<div id="app"> <example-component></example-component> </div> <?= $this->Html->script('/assets/js/app.js')?>動作確認
http://localhost し以下の文言が表示されていることを確認してください。
Example Component I'm an example component.補足) テストの追加と実行方法
./tests/TestCase/Controller/PagesControllerTest.php
へ PagesController で追加したメソッドのテストを追加します。tests/TestCase/Controller/PagesControllerTest.phpclass PagesControllerTest extends TestCase { // ... 以下の2つのテストを追加します。 /** * @return void */ public function testVueルートページを表示() { $this->get('/'); $this->assertResponseOk(); $this->assertResponseContains('<div id="app">'); } /** * @return void */ public function testどんなアクセスでもVueルートページを表示() { $this->get('/hoge/bar'); $this->assertResponseOk(); $this->assertResponseContains('<div id="app">'); } // ... }コンテナを使って、テストを実行します。
docker exec -it app php composer.phar testコミット前に
composer check
を行うと test と codesniffer による check を行えます。docker exec -it app php composer.phar checkこの composer で実行するコマンドは、 composer.json の scripts に記載があります。
- 投稿日:2020-03-14T23:01:57+09:00
Vue.jsプロジェクトのセットアップ
Vue.jsのプロジェクトを最初から作る手順を紹介します。
前提条件
- npm、yarnがインストール済みであること
@vue/cliのインストール
グローバルに@vue/cliをインストールしてvueコマンドを使用できるようにします。
$ npm install -g @vue/clivueプロジェクトの作成
$ vue create project-name
? Please pick a preset: (Use arrow keys) ❯ default (babel, eslint) Manually select features開発環境の起動
$ yarn serve
起動後、ブラウザからhttp://localhost:8080/で表示できます。
production用build
$ yarn build
成功すると、distディレクトリにファイルが作られます。
- 投稿日:2020-03-14T22:06:03+09:00
Nuxt.jsとvue-awesome-swiperでスワイパーする
Nuxt.jsでスワイパーを作ります。
前提条件
- Nuxt.jsとPugとStylusがインストールされていること
インストール
$ npm i --save vue-awesome-swiper使い方
touch
コマンドでプラグイン用ファイルを作成する$ touch plugins/vue-awesome-swiper.jsplugins/vue-awesome-swiper.jsimport Vue from 'vue' import VueAwesomeSwiper from 'vue-awesome-swiper' // require styles import 'swiper/dist/css/swiper.css' Vue.use(VueAwesomeSwiper /* , { default global options } */)
nuxt.config.js
でプラグインを読み込むnuxt.config.jsmodule.exports = { plugins: [ { src: '~/plugins/vue-awesome-swiper', mode: 'client' } ] }
client-only
の中にswiper
、その下にswiper-slide
を欲しいスライド分だけ配置する。
swiper
のoptions
属性に渡しているオブジェクトで、スワイパーのオプションを設定する。
この例では1セットに3枚のスライドを表示し、スライドをループさせる状態にある。pages/test.vue<template lang="pug"> .test client-only swiper(:options="swiperOption") swiper-slide(v-for="item in 10" :key="item") .test-item {{ `Slide ${item}` }} </template> <script> export default { data: () => ({ swiperOption: { slidesPerView: 3, // 1セットに何枚のスライドを表示するか loop: true, // スライドをループさせるか } }) } </script> <style lang="stylus"> .test width 100vw height 100vh .swiper-container width 100% height 100% .test-item display flex justify-content center align-items center width 100% height 100% </style>その他のオプションは参考文献のgithubを参照してください。
参考文献
この記事は以下の情報を参考にして執筆しました。
- 投稿日:2020-03-14T18:36:03+09:00
Vue.jsの$emitでpromiseしたいでござるの巻
先日書いた記事で、出来るだけtemplateで$emitした方が良いと書きました。
<template> <dl> <dt>名前入力</dt> <dd> <input type="text" @input="$emit('customEvent', hoge('hogehoge'))" /> </dd> </dl> </template>こういう感じにemitは直接templateから指定して、引数からメソッドを呼んで必要に応じて成型した戻り値を親コンポーネントへ送ります。
みたいなことを書いたわけですが、$emit時に返す値がAPIの読み込みなどの非同期な場合に上手くいかない事がありました。
なのでそういう場合は
hoge(e){ axios.get('fuga', e).then(res => { this.$emit('fuga', res.data) }) }みたいな感じに、イベントから呼び出した関数内でemitするパターンになってしまうのですが…。
async hoge(e){ const param = await axios.get('fuga', e).then(res => res.data) // 実際にはここでparamの加工とかする return param }とか言う感じでpromiseで返してあげると
<template> <div id="app"> <SampleComponent @customEvent="fuga" /> </div> </template> <script> export default { components: { SampleComponent }, methods:{ fuga(e){ e.then(result => alert(result)) } } } </script>親側で受け取った際に、引数にPromiseオブジェクトで帰ってくるので、引数に
then
とか付けれました。
まぁ、よく考えれば、そりゃそうだろうって感じですが…。<button @click="hoge().then(res => $emit('hogehoge', res))">TEST2</button>ちなみに、Promiseオブジェクトで送るのがなんか嫌でこう書いたら動くんですが、vue側でエラーを出してきます。
[Vue warn]: Property or method "then" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property.
こんな感じのエラーになりました。英語に疎いのでちょっと意味が…。
とまぁ、何の役にも立ちそうにない小話です。
できれば$emitがpromiseにも対応してくれればいいなぁと思った今日この頃です。
- 投稿日:2020-03-14T17:02:02+09:00
【瞬時に解決】vue.jsのエラー:Elements in iteration expect to have 'v-bind:key' directives vue/require-v-for-key
vue cliを使用してプロジェクトを作成していると、このようなエラーに遭遇するかもしれません。
Elements in iteration expect to have 'v-bind:key' directives vue/require-v-for-key
対処法は意外にも簡単でした。
vue.jsの公式ドキュメントにも解決策が載っています。たとえば
v-for
を使って、monkeyの名前をli
タグの中に出したいとしましょう。例
<template> <ul> <li v-for="monkey in monkeys">{{ monkey }}</li> </ul> </template> <script> export default { data() { return { monkeys: ['Taro', 'Hanako', 'Jiro'] } } } </script>こう書くとエラーが発生してしまいアウトプットされません。
ところが以下のように
v-bind:key="monkey"
をli
タグのなかに追加するとエラーが消えます解決!
<template> <ul> <li v-for="(monkey, dog) in monkeys" v-bind:key="dog">{{ monkey }}</li> </ul> </template>どうやらv-forを使うときは、
v-bind:key=""
を付け加えてあげる必要があるそうです。これはvue.jsの公式ドキュメントでも推奨されています。
v-bind:key=""
の中身は、in
の手前にくるもの(この場合は(monkey, dog)
)とそろえる必要があります。ただし、最初のオブジェクトとかぶるのはNGのようで、他とかぶらない名前を(monkey, dog
このようにしてオブジェクトのあとにカンマで区切って付け加えるといいようです。ここではdog
という名前をつけました。括弧にくくらないと、またエラーが出てしまいます。たとえば以下のようでは効きません。誤り
<template> <ul> <li v-for="monkey,dog in monkeys" v-bind:key="dog">{{ monkey }}</li> </ul> </template> //以下も効きません。 <template> <ul> <li v-for="monkey in monkeys" v-bind:key="dog">{{ monkey }}</li> </ul> </template>?ご注意ください。
このようにして解決することができました!
とはいえ初心者なので、もし何かあればご指摘ください
- 投稿日:2020-03-14T13:51:21+09:00
nuxtでGTMタグを<head>と<body>に埋め込む(非SPA、モジュール不使用、シンプル)
クライアント案件で、GTMタグを埋め込みたいとの連絡をいただいた。
通常の静的サイトであれば、GTM用のタグを
<head>
内と<body>
直後の全ページに設置するだけで良いのだけれど、
これをnuxtでgenerate出力した場合のサイトにシンプルに適用する方法について共有します。
(20/02/19 時点で動作確認)対象サイト
- nuxt.js(v2.0.0)で構築
- universalモード(非SPA)
- generateで生成した静的Webサイト(下層ページあり)
やりたいこと
GTMから発行される下記タグを全ページに設置する(あるいはそれと同等のことをする)。
<head>
〜</head>
に*.html<!-- Google Tag Manager --> <script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0], j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); })(window,document,'script','dataLayer','GTM-XXXXXXX');</script> <!-- End Google Tag Manager -->
<body>
直後に*.html<!-- Google Tag Manager (noscript) --> <noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXXXXX" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript> <!-- End Google Tag Manager (noscript) -->いろんなやり方ある・・・が、自社案件じゃないと歯がゆい
公式モジュール
nuxt公式が出しているGoogleTagManager用のモジュールを使ってみる。
ところが情報色々ぐぐってみると、
デフォルトではページビューの計測オプションがfalseになっており、
計測したい場合はこちらのオプション有効化することに加え、
管理画面側でページビューを拾うためのタグの設定などが必要だそう。先方管理画面のログイン権限がないような今回のケースだと、
担当者にタグの設定を一発で的確に指示できる自信があまりないし、
設定の不備による計測ミスなど状況をややこしくしたくない、、
実装サイドだけで完結させられないか、他の方法を検討。自前でプラグイン実装
これも公式にのっている方法で、
プラグインファイルを作ってnuxt.config.js
で読み込めばいいよというやつ。この方法は、nuxtに対して「毎回遷移時にコード送ってね」というコードを書いているので、
(つまるところそれを簡単に使えるようしてくれているのが先程のモジュールなわけで、)
nuxtの内部で処理されるため、<head>
とか<body>
にコードが出力されるわけではない。
し、どちらにせよイベント送信してゴニョゴニョするってことは
受信側でタグ設定が必要になりそうなので、ナシ。いやいや、もっとシンプルに・・該当の箇所に埋め込みたいだけなのだ・・?
ということで調べて、たどり着きました。【結論】nuxt.config.js(Vue meta)で記述できる
nuxt.jsのAPI:headを利用し、vue-metaのルールに則って記述するだけで大丈夫でした。
nuxt.config.js に下記を追記しましょう。
nuxt.config.jsconst gtmID = 'GTM-XXXXXXX' const gtmHeadTag = `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer','${gtmID}');` const gtmBodyTag = `<iframe src="https://www.googletagmanager.com/ns.html?id=${gtmID}" height="0" width="0" style="display:none;visibility:hidden"></iframe>` export default { head: { script: [ { hid: 'gtmHead', innerHTML: gtmHeadTag } ], noscript: [ { hid: 'gtmBody', innerHTML: gtmBodyTag, pbody: true } ], __dangerouslyDisableSanitizersByTagID: { 'gtmHead': ['innerHTML'], 'gtmBody': ['innerHTML'] } } }コンパイル後のソース
*.html<head>... <script data-n-head="ssr" data-hid="gtmHead">(function (w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer','GTM-XXXXXXX');</script> ...</head> <body> <noscript data-n-head="ssr" data-hid="gtmBody" data-pbody="true"><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXXXXX" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript> ...解説(補足)
script:[]
<script>
タグを<head></head>
内に埋め込めます。
共通で読み込みたいsrcファイルなどを指定(公式)する際にも使えますが、
innerHTML
というキーに文字列を渡すと、<script>~<script>
内に文字列を挿入できます。
noscript:[]
<noscript>
タグを<body></body>
内に埋め込めます。
オプションで、挿入位置を指定できます。
<body>
直後の場合はpbody:true
、</body>
直前の場合はbody:true
を指定します。https://vue-meta.nuxtjs.org/api/#noscript
https://vue-meta.nuxtjs.org/api/#noscript-text
__dangerouslyDisableSanitizersByTagID:
①inject許可
vue.jsで構築されたアプリケーションは、DOM攻撃(XSS等)の危険性があるので、
管理外の<head>
や<body>
へのタグの追加・変更に堅いです。
ここで指定されたキーバリューは、例外となりinjectを許可されます。https://vue-meta.nuxtjs.org/api/#dangerouslydisablesanitizersbytagid
※注:同様に
__dangerouslyDisableSanitizers
というのがありますが、
こちらは指定したタグ全般に対し例外適用されるので、ID指定のほうを使いましょう。②escapeされない
また、ここで指定された値はrawData扱いされるため、
htmlに出力される際escapeされないというところもポイントとなります。やってみていただくとわかるのですが、こちらのオプションをつけずに出力した場合、
<body>
側に埋め込んだ<iframe>
タグは下記のように<iframe
と出力されてしまいます。
(これを解決するのに、DOM Parserなども試したが駄目で、ハマりました・・)*.html<body> <noscript data-n-head="ssr" data-hid="gtmBody" data-pbody="true"><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXXXXX" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>その他注意
vue-meta公式だとIDに設定するキーの名前が
vmid
になっているのですが、
nuxtの場合はhid
にしてあげないと効きません。(こちらも地味にハマった、、)おわり
シンプルな実装で、計測も無事確認できたので、同じような状況の方はためしてみてください?
(ちなみにこのスクリプト挿入はGTMタグ以外にも応用可能です)
- 投稿日:2020-03-14T01:02:08+09:00
Vue.js:カッコイイナビゲーションを作る
最近マイブームの「アレ、どうやって作るんだろう?」的なメモです。
たまに見かける「メニューを開くと文字がフワフワっと出てくる」アレ。
実は以前から「どうやって作るんだろう??」と思ってたんです。
transition-delay
を付けても出来そうなんですけど、
何か1文字ずつズレて表示されてるし、
人力で手作業でやってるワケないしな〜と思ったので、調べました。調べた結果、おそらく
SplitText
っていうエフェクトの
カスタムの一種だろうと思いました。
gsap
っていうJavascriptアニメーションフレームワーク
があるんですけど、
それの高機能なバージョンに付属している機能です。Vue.jsで試してみる。
今回も
Vue.js
です。前回の投稿で「≡」を作った時のソースの流用をします。
https://qiita.com/DaisukeNishi/items/999709a4636f600ffcfaApp.vue<template> <div id="app"> <header class="header"> <h1 class="title"> Website </h1> <div class="menu" data-state="hoge" data-mouse="fuga" @click="click" @mouseenter="enter" @mouseleave="leave" > <div class="line line-1"></div> <div class="line line-2"></div> <div class="line line-3"></div> <div class="line-close line-close-1"></div> <div class="line-close line-close-2"></div> </div> </header> <div class="cnt-cover"> <div class="cover"></div> <!--白背景--> </div> <nav class="split-text"> <div id="quote" style="text-align:left;"> メニュー表示<br/> 会社概要<br/> プライバシーポリシー<br/> あいうえお<br/> かきくけこ </div> </nav> <img alt="Vue logo" src="./assets/logo.png"> <HelloWorld msg="Welcome to Your Vue.js App"/> </div> </template>App.vue.scss.cnt-cover { position: fixed; top: 0; bottom: 0; left: 0; right: 0; z-index: 10; pointer-events: none } .cnt-cover .cover { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: #fff; z-index: 1; will-change: opacity; opacity: 0; } .split-text { position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 20; color: #000; opacity: 0; }(あれっ!?しまった、書いてる途中に気がついたんですが、
これopacity
だけだとメニューの後ろにあるリンクが押せなくなりますね。
z-index
も一緒に変えれば動くとおもう・・・)→(未検証)App.vue<script> import gsap from 'gsap'; import SplitText from './js/SplitText.js' import $ from 'jquery'; import HelloWorld from './components/HelloWorld.vue' export default { name: 'App', components: { HelloWorld }, methods:{ //ハンバーガーメニューのイベント click(e){ if(e.target.dataset.state !== 'batsu'){ e.target.dataset.state = 'batsu'; this.show(); }else{ e.target.dataset.state = 'hamburger'; this.hide(); } }, enter(e){ e.target.dataset.mouse = 'enter'; }, leave(e){ e.target.dataset.mouse = 'leave'; }, show(){ //gsapで表示 const wt = $(".cnt-cover .cover"); const tl = new gsap.timeline({}); tl.to(wt, .15, { autoAlpha:1 }); //jQueryで表示 $(".split-text").css("opacity","1") //テキストアニメーション const tl2 = gsap.timeline(), mySplitText = new SplitText("#quote", {type:"words,chars"}), chars = mySplitText.chars; gsap.set("#quote", {perspective: 400}); tl2.from(chars, {duration: 0.8, opacity:0, scale:0, y:80, rotationX:180, transformOrigin:"50% 50% -20", ease:"back", stagger: 0.01}, "+=0"); }, hide(){ //gsapで非表示 const wt = $(".cnt-cover .cover "); const tl = new gsap.timeline({}); tl.to(wt, 0.15, { autoAlpha:0 }); //jQueryで非表示 $(".split-text").css("opacity","0") }, }, } </script>使用方法の説明にはこう書いてあります。
an array of all the divs that wrap each character
div
の中にある全ての文字をアニメーションさせる、的な意味かと。
多分、親要素に書いてあげれば子要素に適用できるはず。変数をconst3連続で定義してます。
こちらのSplitText
はimport
してないとエラーになります。App.vue.jsconst tl2 = gsap.timeline(), mySplitText = new SplitText("#quote", {type:"words,chars"}), chars = mySplitText.chars;パースペクティブの意味はちょっと調べてないですが、文字数制限的な感じかと。
ココで要素のid
を指定しておきます。App.vue.jsgsap.set("#quote", {perspective: 400});App.vue.jstl2.from( chars, { duration: 0.8, //スピード opacity:0, //開始時の透明度 scale:0, //開始時の大きさ y:80, //開始時のy軸? rotationX:180, //開始時にx軸の方向に180度倒す transformOrigin:"50% 50% -20", //元の大きさとか? ease:"back", //イージングとか stagger: 0.01 //ぐらつきの度合い }, "+=0"); //?なんだろう?動きました。細かいセッティングが可能です。
プラグインが微妙に高い。
App.vue.jsimport SplitText from './js/SplitText.js'こちら、本来は
gsap
のパッケージに入ってるのだろうと思います。
有料プラグインは、買うしかないんじゃないですかね商用ライセンスだし。いやはや「試しにコレ使ってみたいな」で1万円は高いッス。。高いッスよ・・・
CodePen
では無料で使えるらしいですが、まず案件に使えるのかを試したい。
JSプラグイン
を有料で買うって文化が、日本にはまだないので稟議を通らないと思うんですよね。。
オシャレなデザイン事務所様だったら稟議通るかもですけど。お試しでローカルで使ってみたい(案件で使う時は買う)という話だったら、
β版のSplitText
がどこかに転がってないか探してみたら良いかと思います。
動作保証できないですが、案外バージョンが合わなくても動く気がします。
https://github.com/wh1100717/me.emptystack.net/blob/9b54f1aba0e5140bb873df63c1c5ea614e622e4f/asset/gsap/utils/SplitText.js
https://github.com/pslhrd/PORTFOLIO_V2/blob/50a0224456ed4de14d88f8cfb550d80e425183c1/src/js/SplitText.js■参考資料
https://goworkship.com/magazine/text-effect-typography/
https://greensock.com/SplitText
https://codepen.io/collection/KiEhr?grid_type=list
https://www.webprofessional.jp/fancy-web-animations-made-easy-greensock-plugins/