- 投稿日:2019-12-23T23:16:53+09:00
【Vue.js】エラーの表示を簡単に実装してみる ~ vue-toasted ~
概要
本記事では、Vue.jsで使える便利なNotificationライブラリである vue-toastedについてまとめていきます。
vue-toasted って?
vue-toasted とは、概要にも記述した通り、Vue.jsで使えるNotificationライブラリです。
実装自体も簡単で、3分あれば実装できてしまうとても便利なものです。
トーストでお知らせを表示させたい時にぜひ使ってみてください!公式ページ : https://www.npmjs.com/package/vue-toasted
環境
Vue.js 2.9.6
インストール
yarn、npm、CDNののいずれかでインストールしてください。
yarn
yarn add vue-toasted
npm
npm install vue-toasted --save
CDN
index.html<script src="https://unpkg.com/vue-toasted"></script>実装方法
- vue-toastedを読み込む
main.jsimport Vue from 'vue' import App from './App.vue' import Toasted from 'vue-toasted'; Vue.use(Toasted); new Vue({ render: h => h(App) }).$mount('#app')※CDNの場合はimportの記述は必要ありません。
2.Vueコンポーネントで呼び出す
App.vue<template> <div id="app"> <button @click="btnClick">クリック</button> </div> </template> <script> export default { methods: { btnClick() { this.errToast("エラーです!"); }, errToast:function(msg){ // main.jsで読み込んだので this.$toasted で呼び出せる this.$toasted.error(msg); }, } } </script> <style> .toasted-container .toasted { /* スタイルを修正することもできます */ } </style>これで実装は完了です。
今回はエラーの表示の時のトーストを実装したため、赤色のトーストが表示されたと思います。
エラー以外にも、オプションを設定すれば様々なトーストを実装することができます。応用編
オプション
index.jsimport Vue from 'vue' import App from './App.vue' import Toasted from 'vue-toasted'; // オプション設定 // 今回はポジション・表示されてから消えるまでの時間・横幅についてのオプション追加 var options = { position: 'top-center', duration: 2000, fullWidth: true, } Vue.use(Toasted, options); new Vue({ render: h => h(App) }).$mount('#app')このようにオプションを渡してあげることによって、思い通りのトーストを簡単に実装することができます。
下記URLから、ぽちぽち操作して目で見ながら理想のトーストを作っていくこともできるのでおすすめです!
https://shakee93.github.io/vue-toasted/
- 投稿日:2019-12-23T23:12:14+09:00
Atomのgit-controlでPushができないと思ったら、pre-commitでeslintにより弾かれていた件
これはTOWN Advent Calendar 2019 23日目のエントリーです。
Atomのgit-controlプラグインを使ってGitの操作をしているのですが、コミットはできるもののプッシュができない(ボタンが選択可能にならない)現象が発生しました。
(よくよく見ると画面の下の方にエラーが出ていたのですが最初気がつかづ。。。)こういうときには面倒ですがTerminalからコマンドを直接入力して確認をしてみます。
% git commit husky > pre-commit (node v13.5.0) ✔ Stashing changes... ❯ Running linters... ❯ Running tasks for *.{js,vue} ✖ eslint ↓ Updating stash... [skipped] → Skipping stash update since some tasks exited with errors ✔ Restoring local changes... ✖ eslint found some errors. Please fix them and try committing again. /project/plugins/firestore.js 1:28 error Delete `;` prettier/prettier 2:42 error Delete `;` prettier/prettier 4:32 error Delete `;` prettier/prettier 7:18 error Delete `;` prettier/prettier ✖ 4 problems (4 errors, 0 warnings) 4 errors and 0 warnings potentially fixable with the `--fix` option. husky > pre-commit hook failed (add --no-verify to bypass)どうやらpre-commit時にeslintが効いてコミットが弾かれている、ということがわかります。
該当するファイルを修正して再度コミットすることでPushができるようになりました。
git-controlは画面上のウィンドウサイズをを変更できないので長いログが出たときに見落としがちなので注意です。
- 投稿日:2019-12-23T22:46:04+09:00
Vue.jsとaxiosでAPIを叩く ( GET, POST, PUT, DELETE )
概要
本記事では、axoisを用いたAPIリクエストの基本的なことについてまとめていきます。
※Vuexについては記述しません。環境変数
認証情報は
.envファイル
に記述し、認証情報が必要な場合はこちらから読み込ませて利用します。.envTOKEN='取得したトークン' BASE_URL = 'リクエスト先のURL'GET リクエスト
index.jsimport Vue from 'vue' import Vuex from 'vuex' import axios from 'axios' Vue.use(Vuex) // .envファイルから環境変数(トークン・URLを読み込ませる) var TOKEN = process.env.TOKEN var BASE_URL = process.env.BASE_URL export default new Vuex.Store({ actions: { get_event(context,param) { return axios.get(BASE_URL + param, { headers: { "Content-Type": "application/json", "Authorization": 'Bearer ' + TOKEN }, responseType: 'json', }) .then(() => { // レスポンスが200の時の処理 console.log("取れたよ") }) .catch(err => { if(err.response) { // レスポンスが200以外の時の処理 } }); } } })
渡すパラメータが複数の場合には、一つのオブジェクトにまとめて記述する必要があります。index.jsimport Vue from 'vue' import Vuex from 'vuex' import axios from 'axios' Vue.use(Vuex) var TOKEN = process.env.TOKEN var BASE_URL = process.env.BASE_URL export default new Vuex.Store({ actions: { get_event(context,{ param, id }) { return axios.get(BASE_URL + param + id, { headers: { "Content-Type": "application/json", "Authorization": 'Bearer ' + TOKEN }, responseType: 'json', }) .then(() => { // レスポンスが200以外の時の処理 console.log("取れたよ") }) .catch(err => { if(err.response) { // レスポンスが200以外の時の処理 } }); } } })POST リクエスト
hoge: hoge
としているところで、引数で渡ってきたデータをRequest payloadとしてサーバー側に送信することができます。index.jsimport Vue from 'vue' import Vuex from 'vuex' import axios from 'axios' Vue.use(Vuex) var TOKEN = process.env.TOKEN var BASE_URL = process.env.BASE_URL export default new Vuex.Store({ actions: { get_checkin_statuses(context, {param, hoge}) { return axios.post(BASE_URL + param, { hoge: hoge, }, { headers: { "Content-Type": "application/json", "Authorization": 'Bearer ' + TOKEN } }) .then(() => { // レスポンスが200の時の処理 console.log("送れたよ") }) .catch(err => { if(err.response) { // レスポンスが200以外の時の処理 } }); } } })PUT リクエスト
hoge: hoge
としているところで、引数で渡ってきたデータをRequest payloadとしてサーバー側に送信することができます。index.jsimport Vue from 'vue' import Vuex from 'vuex' import axios from 'axios' Vue.use(Vuex) var TOKEN = process.env.TOKEN var BASE_URL = process.env.BASE_URL export default new Vuex.Store({ actions: { async postCode(context,{ param, hoge }) { await axios.put(BASE_URL + param, { hoge: hoge, }), { headers: { "Content-Type": "application/json", "Authorization": 'Bearer ' + TOKEN } }) .then(res => { // レスポンスが200の時の処理 console.log("更新したよ") }) .catch(err => { if(err.response) { // レスポンスが200以外の時の処理 } }); } } })DELETEリクエスト
index.jsimport Vue from 'vue' import Vuex from 'vuex' import axios from 'axios' Vue.use(Vuex) var TOKEN = process.env.TOKEN var BASE_URL = process.env.BASE_URL export default new Vuex.Store({ actions: { async delCheckin(context, param) { await axios.delete(BASE_URL + param, { headers: { "Content-Type": "application/json", "Authorization": 'Bearer ' + TOKEN } }) .then(res => { // レスポンスが200の時の処理 console.log("消しといたよ") }) .catch(err => { if(err.response) { // レスポンスが200以外の時の処理 } }); }, } })
- 投稿日:2019-12-23T20:42:04+09:00
.preventで画面遷移をさせない ❏Vue.js❏
リンクがあるけど、画面遷移をさせたくない時を考えます。
そんな時あんのか開発環境はJSFiddle
https://qiita.com/ITmanbow/items/9ae48d37aa5b847f1b3bhtml<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <div id="app"> <a @click.prevent href="https://google.com">google</a> </div>
preventDefault
を仕込むと画面遷移を無効にできます。
本来はJS側でevent.preventDefault()
と記述します。しかし、Vue.jsでは、html側で
@click.prevent
と繋げることで簡単に実装できます。
ではまた!
- 投稿日:2019-12-23T20:33:02+09:00
初心者によるプログラミング学習ログ 192日目
100日チャレンジの192日目
twitterの100日チャレンジ#タグ、#100DaysOfCode実施中です。
すでに100日超えましたが、継続。100日チャレンジは、ぱぺまぺの中ではプログラミングに限らず継続学習のために使っています。
192日目は
おはようざいます
— ぱぺまぺ@webエンジニアを目指したい社畜 (@yudapinokio) December 22, 2019
192日目
udemyで、Vue.js
webサイトコーディングの練習#100DaysOfCode #早起きチャレンジ#駆け出しエンジニアとつながりたい
- 投稿日:2019-12-23T20:15:15+09:00
【環境構築】Docker + Rails6 + Vue.js + Vuetifyの環境構築手順
はじめに
Docker + Rails6 + Vue.js + Vuetifyの開発環境構築手順をまとめました。
以下の記事を参考にさせて頂きました!ありがとうございます
- webpackerを使ってRuby on Rails 6.0とVue.jsを連携する方法(フロントエンド編)
- Rails+Vue.js+Vuetify環境の構築手順 - Qiita
- 【Rails6】10分でRails + Vue + Vuetifyの環境を構築する - Qiita
環境
OS: macOS Catalina 10.15.1 zsh: 5.7.1 Ruby: 2.6.5 Rails: 6.0.2 Docker: 19.03.5 docker-compose: 1.24.1 Vue: 2.6.10 vue-router: 2.6.10 vuex: 3.1.2 vuetify: 2.1.01.準備
作成するアプリケーション名はhogeappとします。
まずは以下ファイルを作成して下さい。Dockerfile
yarnが必要になるので、Dockerfileに反映しています。
hogeapp/DockerfileFROM ruby:2.6.5 RUN apt-get update -qq && \ apt-get install -y build-essential \ libpq-dev \ nodejs \ vim RUN apt-get update && apt-get install -y curl apt-transport-https wget && \ curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \ echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \ apt-get update && apt-get install -y yarn RUN mkdir /app_name ENV APP_ROOT /app_name WORKDIR $APP_ROOT COPY ./Gemfile $APP_ROOT/Gemfile COPY ./Gemfile.lock $APP_ROOT/Gemfile.lock RUN bundle install COPY entrypoint.sh /usr/bin/ RUN chmod +x /usr/bin/entrypoint.sh ENTRYPOINT [ "entrypoint.sh" ] ADD . $APP_ROOTdocker-compose.yml
hogeapp/docker-compose.ymlversion: "3" services: db: image: mysql:5.7 environment: MYSQL_ROOT_PASSWORD: password MYSQL_DATABASE: root volumes: - db-data:/var/lib/mysql ports: - "3306:3306" web: build: . command: /bin/sh -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'" volumes: - .:/app_name ports: - "3000:3000" links: - db tty: true stdin_open: true depends_on: - db data: image: busybox volumes: - db-data:/var/lib/mysql tty: true volumes: db-data: driver: localGemfile
hogeapp/Gemfilesource 'https://rubygems.org' git_source(:github) { |repo| "https://github.com/#{repo}.git" } gem 'rails', '~> 6.0.2', '>= 6.0.2.1'Gemfile.lock
中身は空で作成します。
hogeapp/Gemfile.lockentrypoint.sh
hogeapp/entrypoint.sh#!/bin/bash set -e # Remove a potentially pre-existing server.pid for Rails. rm -f /app_name/tmp/pids/server.pid # Then exec the container's main process (what's set as CMD in the Dockerfile). exec "$@"これで必要なファイルが揃ったので、次はRailsアプリの作成です。
2.Railsアプリの作成
rails new
$ docker-compose run web rails new . --force --database=mysql --skip-bundle※このエラーが出たら
Error response from daemon: OCI runtime create failed: container_linux.go:346: starting container process caused "exec: \"rails\": executable file not found in $PATH": unknown↓
$ docker-compose build
※何らかのgemが不足しているようなエラーが出たら
自分は以下のようなエラーが発生しました。
Could not find public_suffix-4.0.1 in any of the sources
↓
$ docker-compose run web bundle install一度
bundle install
を試してみて下さい。database.ymlの変更
config/database.ymldefault: &default adapter: mysql2 encoding: utf8mb4 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root password: password #ここを変更 host: db #ここを変更コンテナが立ち上がるか確認
docker-compose up -d
だとログが見えないので、無事に環境構築が完了するまでは-d
なしが良いと思います。$ docker-compose up
db:create
無事にコンテナが立ち上がったら、DBを作成しましょう。
$ docker-compose exec web rails db:createYay! You’re on Rails!
localhost:3000
にアクセスして確認してみましょう。これでRailsはOKなので、次はVue.jsです!
3.Vue.jsの導入
webpackerのインストール
$ docker-compose exec web rails webpacker:installVue.jsのインストール
$ docker-compose exec web rails webpacker:install:vueVue.jsとの連携を確認
Railsでコントローラーを作ってみて、Vue.jsと連携出来るかを確認してみます。
$ docker-compose exec web rails g controller home indexapp/views/home/index.html.erb<%= javascript_pack_tag 'hello_vue' %> <%= stylesheet_pack_tag 'hello_vue' %>このように変更し、
hello_vue.js
を読み込みます。※Vue.jsのインストール時に
hello_vue.js
はデフォルトで作成されています。Railsのルーティングを設定
config/routes.rbroot to: 'home#index'ブラウザで確認
localhost:3000にアクセスし、下記画面が出力されているか確認してみて下さい。
これでVue.jsの導入はOKですが、他の単一ファイルコンポーネントも作成し、読み込めるかどうかを確認しておきます。
4.他の単一ファイルコンポーネントが読み込めるかどうか確認する
Top.vueの作成
app/javascript/components/
ディレクトリを作成し、その中にTop.vue
を作成します。
Top.vue
の中身は以下のようにしました。app/javascript/components/Top.vue<template> <section id="top"> <h1>This is Top.vue!</h1> </section> </template> <script> export default { name: 'Top' } </script> <style> h1 { text-align: center; } </style>
app.vue
を変更app/javascript/app.vue<template> <div id="app"> <p>{{ message }}</p> <Top/> //追記 </div> </template> <script> import Top from "./components/Top"; //追記 export default { data: function () { return { message: "Hello Vue!" } }, components: { Top, //追記 } } </script> ...以下略ブラウザで確認
再度読み込みすると、以下のような画面になっているはずです。
これできちんと単一ファイルコンポーネントが読み込まれていることが確認できたので、Vue.jsはOKです。
次はラスト!Vuetifyの導入です。
5.Vuetifyの導入
Vuetifyのインストール
$ docker-compose exec web yarn add vuetify -D
hello_vue.js
に追記hello_vue.jsimport Vue from 'vue' import Vuetify from 'vuetify' //追加 import "vuetify/dist/vuetify.min.css" //追加 import App from '../app.vue' Vue.use(Vuetify) //追加 const vuetify = new Vuetify(); //追加 document.addEventListener('DOMContentLoaded', () => { const app = new Vue({ vuetify, //追加 render: h => h(App) }).$mount() document.body.appendChild(app.$el) console.log(app) })application.html.erbの変更
application.html.erb
でVuetify用にフォントとアイコンの読み込みと不要な箇所の削除を行います。app/views/layouts/application.html.erb<head> ...略 <link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/@mdi/font@4.x/css/materialdesignicons.min.css" rel="stylesheet"> ...略 </head>下記タグは不要なので削除しておきます。
app/views/layouts/application.html.erb<%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>これで準備が整ったので、あとはVuetifyのコンポーネントが反映されるかどうか、作成して確認してみましょう。
Header.vueの作成
新規にHeader.vueを作成し、
<v-app-bar>
を組み込んでみます。app/javascript/components/Header.vue<template> <header id="header"> <v-app-bar> <v-app-bar-nav-icon></v-app-bar-nav-icon> <v-toolbar-title>This is Header.vue</v-toolbar-title> </v-app-bar> </header> </template> <script> export default { name: 'Header', } </script>
app.vue
を変更
Header.vue
を読み込みつつ、全体をまとめている<div>
を<v-app>
タグに変更しておきます。app/javascript/app.vue<template> <v-app id="app"> //divから変更 <Header/> //追記 <h1>This is app.vue</h1> //一応追記。どっちでもいいです <p>{{ message }}</p> <Top/> </v-app> </template> <script> import Header from "./components/Header"; //追記 import Top from "./components/Top"; export default { data: function () { return { message: "Hello Vue!" } }, components: { Header, //追記 Top, } } </script>これで再度localhost:3000にアクセスし、問題ないかを確認してみます。
無事に画像のようなヘッダーが表示されていれば完了です!
以上です!お疲れ様でした!
おわりに
最後まで読んで頂きありがとうございました
どなたかのお役に立てれば幸いです
参考にさせて頂いたサイト(いつもありがとうございます)
- 投稿日:2019-12-23T16:35:44+09:00
new.Vue を export default に書き換える
タイトルの通り、
new.Vue
をexport default
に書き換えるときの備忘録です。例えばこういうのがあれば↓
new Vue({ el: "#app", data: () => ({ /* data */ }, methods: { /* methods */ } })
export default
を使うとこんなふうになります。export default { name: 'App', data: () => ({ /* data */ }, methods: { /* methods */ } }これがうまくわからず手こずっていました
![]()
- 投稿日:2019-12-23T16:00:42+09:00
Vue CLIでconsole.logを有効化する方法
はじめに
Vue CLI(ESLint)のデフォルト設定では、
console.log
がエラーを吐く仕様になっています。
でも簡単なデバッグなどでは手軽に使えたら嬉しいですよね。
そこで、Vue CLIでconsole.log
を有効にする方法をメモしておきます。
非常に簡単なので、お困りの方がいらしたらぜひお試しください。環境
Vue CLIバージョン:3.9.3
1. 永続的に有効化したい場合
■変更対象ファイル(src/package.json)
・eslintConfig
内のrules
に"no-console": 0
を設定package.json"eslintConfig": { "root": true, "env": { "node": true }, "extends": [ "plugin:vue/essential", "eslint:recommended" ], "rules": { "no-console": 0 }, "parserOptions": { "parser": "babel-eslint" } }2. 一時的に有効化したい場合
console.logを使いたいファイルに、以下のコメントを追加
/* eslint-disable no-console */.jsファイルであればファイルの一番上に、
.vueファイルであればscriptタグ内の一番上に記述すれば有効化されます!他にも色々と設定方法が存在しますが、この2つの方法だけでも特に開発作業に問題はないかと思います。
以上です!!ぺーぺーなので、間違いなどあったら優しく教えていただけると泣いて喜びます( ;∀;)
- 投稿日:2019-12-23T15:04:56+09:00
vue.js redis 足跡機能を作ろうぜ
足跡機能を作りたい。
参考
https://qiita.com/maip0902/items/eafc35ed762648ddadbf基礎知識。
来訪ユーザーが重複した場合には、削除され、新しく追加される。
よって、重複して登録されることは無い。Okwscontroller.phpuse Illuminate\Support\Facades\Redis;//忘れずに。 public static function getAshiato(request $request) { $res = Redis::zrevrange('ashiato:'.$request->home_user_id,0,-1);//全員分の足跡をタイムスタンプが新しい順に取得 return response()->json(['res'=> $res]); } public static function addAshiato(request $request) { $res = true; Redis::zadd('ashiato:'.$request->home_user_id, time(),$request->user_id);//どのユーザーのページに、いつ訪問したか、来訪したユーザーを追加 return response()->json(['res'=> $res]); }Ashiato.vuecreated() { this.addAshiato(); this.getAshiato(); }, methods: { //足跡を追加(例えばユーザーページに入れる) addAshiato(){ let dataform = new FormData(); dataform.append('home_user_id',100); dataform.append('user_id',333); axios.post('/okws/addAshiato/', dataform).then(e => { console.log(e.data.res); console.log("登録成功"); }).catch((error) => { console.log("エラー"); }); }, //足跡を読み込み getAshiato(){ let dataform = new FormData(); dataform.append('home_user_id',100); axios.post('/okws/getAshiato/', dataform).then(e => { console.log(e.data.res); console.log("取得成功"); }).catch((error) => { console.log("エラー"); }); } }
- 投稿日:2019-12-23T14:37:17+09:00
【Vuex】storeの複雑化を抑えた長期運用可能なwebアプリのための方策草案
検討中の内容をまとめた。
storeの記述は可能な限りシンプルに、複雑さはコンポーネントに押し込める事を基本とする。モチベ
処理をstoreに寄せるとstoreが膨らむ。どのviewに影響するか分からない処理で膨らんだstoreはリーディングコストが高い。
複雑化したコンポーネントを捨てるのは影響範囲が限定されるが、複雑化したstoreはアプリ全体に影響を及ぼすため安易に改修出来ない。
手が入れ辛い方から複雑性を排除したい。思考実験段階のため、実情に即して随時修正加筆を行う。
stateのコンポーネントでの利用単位ごとにgettersを作成する
mapStateは用いない
全てmapGettersで行うgettersは抽出のみとする
抽出以上の表示用データ整形はコンポーネント側で行う
コンポーネントとの密結合を避ける目的store moduleは独立させる
module間の伝達は行わない
複数moduleにまたがるstore管理は人類には容易でないひとつのmutationsはひとつのstateを更新するのみとする
複数のstateを更新しない
mutations内でデータ加工を行わない
コンポーネントからmutationsを直接叩かない 必ずactionsを経由させるmutationsひとつに対しactionsひとつを基本とする
コンポーネントから叩いてよい
必要であれば非同期処理を書いてよい
複数mutationsをcommitしないactionsに複雑な処理は書かない
事情により必要な場合はstore外に切り出した関数をactions内で呼び出す形とする
actionsでのデータ整形は最小限にとどめる
payloadのデータ整形はコンポーネントで行う
2つ以上のコンポーネントで共通して用いるデータ整形があればstore側に置いてもよい
ただしstoreの外に定義し呼び出す形とする
※store/module/helpers.js等が考えられる※本記事でいうstore外に定義とは以下のsomeHelperのような形を指す
actionsやgettersとは独立して定義するjs// helpers.js const someHelper = () = { // ... some functions return someData } // store.js import { someHelper } from './helpers.js' const state = {} const getters = {} const mutations = {} const actions = {} export default { state, getters, mutations, actions }
- 投稿日:2019-12-23T11:26:26+09:00
弊社アドベントカレンダーで一番いいね!を獲得したユーザは誰か?
この記事は
FORK Advent Calendar 2019 の 23日目の記事です。
Vue.js (Vue CLI with TypeScript) と Qiita API で Oraganization ビューアを作ったお話です。モチベーション
ふと先日「アドベントカレンダーでいちばん多くいいね!を獲得したユーザに、サンタが何かをプレゼントしてくれれば、良質な記事が社内のエンジニアたちからわしゃわしゃ集まる」ことに気が付いてしまいました。そこで Qiita API と Vue CLI を使って、弊社の Qiita メンバーの月毎の投稿記事の「いいね!」数を表示する Qiita ビューアを作りました。
おことわり
- 突貫で開発を行ったため、アプリの挙動が若干あやしい(言い訳)
- 一般公開すると Qiita API のリクエスト制限数(後述)を超える可能性がある(多分ないけど)
以上の理由から完成版の URL は開示しておりません。
開発のポイントと併せて、サンプルのコードを記載しておりますので、なんとなく実装の内容をお伝えできれば幸いです。完成したアプリのキャプチャ
こんな感じのアプリです。本日までのランキングでは @yoh_zzzz さんの Nuxt + Firebase の記事がトップですね。
開発のポイント
開発の要所を 3 分間クッキング
でお届けします。
1. Qiita API のインターフェースを定義する
開発は TypeScript で行います。(しれっと言い切るスタイル)
ブラウザで API を叩いたときのレスポンスや、JSON Schema を確認しながら、API のインターフェースを定義します。型定義ファイルはどこに配置しても良いのですが、僕は Vuex のモジュールディレクトリに用意しました。余談ですが、個人的にこの作業がとても楽しく、不安になります。▼ ユーザ周りインターフェース
/src/store/user/types.tsexport interface QiitaUser { description: string | null facebook_id: string | null followees_count: number followers_count: number github_login_name: string | null id: string items_count: number linkedin_id: string | null location: string | null name: string | null organization: string | null permanent_id: number profile_image_url: string team_only: boolean twitter_screen_name: string | null website_url: string | null } export interface UserState { list: QiitaUser[] }開くとソースコードが表示されます。
投稿記事周りインターフェース
/src/store/post/types.tsexport interface QiitaGroup { created_at: string id: number name: string private: boolean updated_at: string url_name: string } export interface QiitaTag { name: string versions: string[] } export interface QiitaPost { rendered_body: string body: string coediting: boolean comments_count: number created_at: string group: QiitaGroup | null id: string likes_count: number private: boolean reactions_count: number tags: QiitaTag[] title: string updated_at: string url: string user: QiitaUser page_views_count: number | null } export interface PostState { list: QiitaPost[] }2. API 呼び出し時にリクエストトークンを含める
Qiita API は get リクエストの場合はトークンなしでも呼び出しが可能なのですが、トークンの有り/無しで 利用制限 (1 時間あたりに呼び出しが可能な回数)に大きな差があります。60 回 / 1 時間以上のリクエストが必要な場合はトークンが必要にります。トークンの取得は OAuthを利用した認可フローか、ユーザの管理画面 から発行できます。今回は時間もなかったため後者を選択しました。
以下は、Vuex store モジュールの action として定義した、投稿記事取得 API のリクエストです。
投稿記事取得 action
/src/store/post/actions.tsconst actions: ActionTree<PostState, RootState> = { async fetchPosts(context: ActionContext<PostState, RootState>, payload: { userId: string, page?: number, perPage?: number }): Promise<QiitaPost[]> { return new Promise(async (resolve, reject) => { const url: string = `https://qiita.com/api/v2/users/${payload.userId}/items?page=${payload.page}&per_page=${payload.perPage}` const accessToken: string = 'XXXXXXXXX' const config: AxiosRequestConfig = { params: { time: Math.floor(Date.now() / 1000 / 60 / 60) }, // キャッシュ対策(1時間毎に異なる文字列を設定) headers: { Authorization: `Bearer ${accessToken}` }, // リクエストヘッダにトークンを含める } const response: AxiosResponse<QiitaPost[]> = await axios.get<QiitaPost[]>(url, config) if (response.status === 200) { const posts: QiitaPost[] = response.data if (posts.length) context.commit('add', posts) resolve(posts) } else { reject(response.status) } }) }, } export default actions3. リクエスト回数を最小限にする
一意の Ogranization に所属している Qiita ユーザを取得する API が用意されていないため、弊社の Organization 所属ユーザは、ハードコーディングで用意した静的 JSON ファイルにユーザ ID を列挙することにしました。ユーザ情報は、/api/v2/users/:user_id から取得できるのですが、この API から各ユーザの投稿記事の合計いいね!数は取得することはできず、すべての投稿記事から各々のいいね!数を合算する必要があります。また投稿記事一覧を取得する API (/api/v2/users/:user_id/items) の戻り値に、記事のユーザ情報がそのまま含まれているため、全所属ユーザの記事一覧を取得し、そこからユーザリストを抽出する Vuex の getters 関数を用意しました。
ユーザリスト取得 getters
/src/store/post/getters.tsexport const getters: GetterTree<PostState, RootState> = { getUsers(state: PostState): QiitaUser[] { const users: QiitaUser[] = [] state.list.forEach((post: QiitaPost) => { const user: QiitaUser = post.user if (!users.some((usr: QiitaUser): boolean => usr.id === user.id)) { users.push(user) } }) return users }, } export default gettersなお記事取得 API では、1 回のリクエストで最大 100 件の投稿記事を取得することができます。投稿記事数が 100 件を超えるユーザについては、未取得の残りの記事を追加でリクエストするようにしました。
一連の流れを整理すると以下になります。
- Organization 所属のユーザ ID を自分で用意した JSON から読み込み
- 全ユーザの投稿記事を 100 件ずつ API から取得 (Vuex action)
- 取得した投稿記事からユーザー情報を取得 (Vuex getters)
- 投稿記事が 100 件を超えるユーザについて、残りの記事を取得 (Vuex action)
おわりに
駆け足での解説となりましたが、ざっくりと要所の雰囲気だけでも伝わったでしょうか。
おそらく今年最後の投稿になるであろう本記事にて Vue のことを書きましたが、実際の仕事でも 2019 年は Vue が活躍してくれました。来年は Vue 3.0 と React を学ぼうと思ってます。それではみなさま良い年末年始をお過ごしください。
FORK Advent Calendar 2019
22日目 正規表現にマッチした文字列を replace を使わずにハイライトさせる @yshrkn
24日目 Nuxt.jsとmysqlを連携してデータを表示してみた @ktn
- 投稿日:2019-12-23T10:06:15+09:00
json からデータを検索、取得し computed にて反映
vue.js で設定ファイルの json に書く
config.json"work":[ {"key":0,"value":"マッチング後にまずは会いたい"}, {"key":1,"value":"気が合えば会いたい"}, {"key":2,"value":"条件が合えば会いたい"}, {"key":3,"value":"メッセージを重ねて"}, {"key":4,"value":"その他"} ],jsonを読み込む
hoge.vueimport configJson from '../assets/config.json' export default { data () { return { configJson:configJson,対応したデータを取得
computed: { work() { const key = this.user.work;//2。 const computed_tmp = this.configJson.work.find((v) => v.key === key); return computed_tmp.value;//条件が合えば会いたい }んで取得したデータを表示
{{work}}//条件が合えば会いたいと表示される。
まぁ、これがベストなんじゃないでしょうか。
- 投稿日:2019-12-23T10:00:24+09:00
vue.js NAN になるのを防ぐ
どう NAN ?
数値を入れるところに一瞬 NAN と表示されるのは。
ちょっと見苦しいじゃありませんか。ということで。
{{age||'-'}} 歳とやると
・数値が異常な場合
・ageに値が代入される前は
- 歳
と表示される。
- 投稿日:2019-12-23T08:53:36+09:00
【後編】GridsomeとContentfulを使って、簡単に更新できる静的LPページを作ってみる
diffeasyアドベントカレンダー23日目の記事で、前回の「【前編】GridsomeとContentfulを使って、簡単に更新できる静的LPページを作ってみる」の続きです。
みなさん、あと2日でクリスマスですがいかがお過ごしでしょうか?
僕はというもの、12月に入ってから彼女からアドベントカレンダー、自称:沙枝ベントカレンダーを毎日もらっては開けているのですが、早くメインのプレゼントが開けたくて開けたくて喉から手が出そうです?(自室の手の届くところにすでに置いてあるので…)そんなことはさておき、前回
1. Contentfulでお知らせコンテンツの作成 2. Gridsomeで 1. で作ったお知らせコンテンツの表示までを作りました。
今回は、1. Firebase Hostingへのデプロイ 2. Github Actionsを使ってContentfulでの変更を検知し、自動デプロイを作っていきたいと思います?
サンプルリポジトリも作ったので、よかったら見てください?
https://github.com/tk07Sky/deadvent-2019-gridsome-sample事前準備?♂️
今回はFirebase Hostingを使うので、Firebaseプロジェクトの作成(名前はなんでもいいです)と、Firebase CLIのインストールを行ってください。
また、GitHubも使うので、アカウントの作成や、Gridsomeで作ったアプリケーションをアップするリポジトリ(名前はなんでもいいです)を作成しておいてください。Firebase Hostingにデプロイしてみる
Firebase Hostingのセットアップ
まず、ターミナルで
firebase init
を打って、プロジェクトにFirebaseの設定を行っていきます。
次に、どの設定を組み込むのかを問われるので、Hostingのみにチェックを入れます。
次にプロジェクトを作るか選択するか問われるので
Use an existing project
を選択し、事前準備で用意してもらっているプロジェクト名を選択します。
その後は、一旦エンターを押し続けてFirebaseの設定を終わらせます。
Firebaseの設定を終えると、プロジェクトルートに
public
フォルダができていると思いますが、使わないので削除しておいてください。Gridsomeコマンドで静的ページを生成し、Hostingのアップロード対象に設定する
Gridsomeの
build
コマンドを使って、Firebase Hostingにアップロードするための静的ページを作ります。$ npm run build
上コマンドを実行すると、プロジェクトルートに
dist
フォルダができるので、このフォルダをFirebase Hostingのアップロード対象に設定します。
firebase init
をした際に、プロジェクトルートにfirebase.json
が生成されていると思うので、中身を書き換えていきます。firebase.json{ "hosting": { "public": "dist", // ←ここ "ignore": [ "firebase.json", "**/.*", "**/node_modules/**" ] } }デプロイしてみる
ターミナルで
$ firebase deploy --only hostingを実行してみましょう!
成功すれば、下画像のように Deploy complete! の文字とHosting先のURLが表示されるはずです。?????
Github Actionsを使ってContentfulでの変更を検知し、自動デプロイされるようにする
Firebase Hostingのアクセストークンを取得する
自動デプロイする際に必要なので取得していきます。
ターミナルで$ firebase login:ci
を入力すると、
firebase login
したときと同じように認証画面に飛ぶので、流れに沿って進みます。
認証が終わるとターミナルにトークンが表示されるので、コピーしておきます。Github Actionsを設定する
まずはGithub Actionsのワークフローを作っていきます。
Github Actionsのワークフローはプロジェクト直下に.github/workflows/**.yml
の形で追加してきます。$ touch .github/workflows/ci.yml $ vim .github/workflows/ci.ymlまずワークフロー名を決めます
.github/workflows/ci.yml# 今回はciと名付けます name: ci次に、何のアクションを起点として発火するかを決めます。
今回はmasterブランチにpushされたときか、外部からリポジトリにGitHub APIを叩かれたときに発火するよう設定します。
外部からのイベントをキャッチするには、repository_dispatchというGitHubのWebhookイベントを使用します。.github/workflows/ci.ymlon: push: branches: - master repository_dispatch:次に、GitHub Actions上でGridsomeのビルドとデプロイをするjobを書きます。
大まかな部分の説明は公式ドキュメントやプラグインのドキュメントに載っているので割愛しますが、今回知ってほしい点を説明すると
- 外部に公開できない
.env
の内容やFirebase tokenなどは、GitHubのSecretsという機能を使って、そこから呼び出すようにする- 開発の際に使っていた
.env
はgitの監視外にありそのまま使えないので、ビルドするときに指定するようにするという感じです。
.github/workflows/ci.ymljobs: build: name: build runs-on: ubuntu-latest steps: - name: Checkout Repo uses: actions/checkout@master - name: Install Dependencies run: yarn install - name: Build run: yarn build env: GRIDSOME_CONTENTFUL_SPACE_ID: ${{ secrets.CONTENTFUL_SPACE_ID }} CONTENTFUL_ACCESS_TOKEN: ${{ secrets.CONTENTFUL_ACCESS_TOKEN }} - name: Archive Production Artifact uses: actions/upload-artifact@master with: name: dist path: dist deploy: name: deploy needs: build runs-on: ubuntu-latest steps: - name: Checkout Repo uses: actions/checkout@master - name: Download Artifact uses: actions/download-artifact@master with: name: dist - name: Deploy to Firebase uses: w9jds/firebase-action@master with: args: deploy --only hosting env: FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}以上の説明をガッチャンコすると、下のようになります。
.github/workflows/ci.ymlname: ci on: push: branches: - master repository_dispatch: jobs: build: name: build runs-on: ubuntu-latest steps: - name: Checkout Repo uses: actions/checkout@master - name: Install Dependencies run: yarn install - name: Build run: yarn build env: GRIDSOME_CONTENTFUL_SPACE_ID: ${{ secrets.CONTENTFUL_SPACE_ID }} CONTENTFUL_ACCESS_TOKEN: ${{ secrets.CONTENTFUL_ACCESS_TOKEN }} - name: Archive Production Artifact uses: actions/upload-artifact@master with: name: dist path: dist deploy: name: deploy needs: build runs-on: ubuntu-latest steps: - name: Checkout Repo uses: actions/checkout@master - name: Download Artifact uses: actions/download-artifact@master with: name: dist - name: Deploy to Firebase uses: w9jds/firebase-action@master with: args: deploy --only hosting env: FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}Secretsを登録する
GitHubには、外部に公開したくない情報を管理する
Secrets
という機能があります。
ContentfulのAPIキーやFirebase tokenは外部に漏らすとまずいので、ここに設定していきます。
事前に作ってもらっていたリポジトリの画面からSettings → Secrets
を選択してください。Secretsには今回
- CONTENTFUL_SPACE_ID
- CONTENTFUL_ACCESS_TOKEN
- FIREBASE_TOKEN
の3つを登録します。
登録は一つずつ行ってください。登録例
Name CONTENTFUL_SPACE_ID Value YOUR_CONTENTFUL_SPACE_ID下画像のようになれば、登録完了です✨
GitHub Personal access tokenを取得する
外部(今回はContentful)からのWebhookイベントを受信するために必要なPersonal access tokenを取得します。
右上の自分のアイコン → Settings → Developer settings → Personal access tokens
からGenerate new token
を選択します。
下のような画面が出てくるので、Noteには覚えやすい名前を設定し、repoにチェックを入れてください。最後に一番下の
generate
を押すとトークンが発行されます。次で必要になるので、どこかに控えておいてください。ContentfulのWebhookの設定をする
最後に、ContentfulのWebhook設定をします。
NameとURLの設定
Contentfulの画面を開いて
Settings → Webhooks
から、右上のAdd Webhook
を選択してください。
すると、下画像の画面が開くので、Nameには好きな名前を、URLにはhttps://api.github.com/repos/${YOUR_GITHUB_ID}/${YOUR_RIPOSITORY_NAME}/dispatches
を指定してください。トリガーの設定
そしてこのままだとContentfulで行ったすべての行動がWebhookを発動するトリガーになるので、Triggersは下のようにしておいたほうがいいです!
Headerの設定
Headersも追加していきましょう。
今回は
- Accept
- Authorization
- User-Agent
を追加していきます。
HeadersセクションのAdd custom header
を選択して、下のように追加してください(User-Agentは自分のGitHub IDに変更してください)。
ついでにContent type
もapplication/json
にしましょう。Payloadの設定
PayloadはGitHubのAPIで
{"event_type": "event name"}
を指定しないといけないらしいので、指定していきます。
Customize the webhook payload
を選択し、下のように指定します。すべて設定が終わったら、右上の
Save
を押して保存しましょう!
保存後、新しく記事をPublishすると、GitHub Actionsがうまく動いてるはずです!?これですべて終了です!
お疲れさまでした?終わりに
いかがでしたか?
思ってた以上に結構ボリューミーになったのですが、作って公開までできればサクッとメンテしたりページ追加できると思うので、よかったらぜひ試してみてください✨
- 投稿日:2019-12-23T08:51:53+09:00
Vue.js で Android 向けに明朝体を使いたい
Andoroid に明朝体がないのは割と知られてる話で、そういう場合は Google Web フォントとか使うのが一般的ですが、
Vue.js でやったときにちょっとハマったのでメモ。TL;DR
- npm パッケージのフォントだとダメっぽい(パッケージ内での実装方法にもよるかも?)
- CSS の
@import
で読ませればOK- または vue-head パッケージ使ってるならそれを使ってもOK
Vue.js で Google Fonts を使う
実は、Google Fonts で公開されているフォントはほとんどが npm パッケージからの導入もできて、
typaface/xxx (あるいは typeface-xxx)みたいな感じで検索するとでてきます。main.js でインポートすればすぐつかえるので、私はいつもこの方法でやっていました。
たとえば、さわらび明朝ならこんなふうに。
インストール
npm install typeface-sawarabi-mincho --save # or yarn add typeface-sawarabi-minchoインポート
main.jsimport 'typeface-sawarabi-mincho/index.css'ところが、この方法だと Android でちゃんとウェブフォントが反映してくれませんでした。
Vue.js で Google Fonts をつかう
じゃあ、どうすればよいのかという話ですが、普通に CSS の
@import
で CDN からインポートすればOKでした。
というかこうしないとうまく行かなかった…。FrontPage.vue<style> @import url('https://fonts.googleapis.com/css?family=Sawarabi+Mincho'); </style>もしも、普段は sass とか scss で書いていて、 style ブロックに
lang="scss"
とかつけていた場合は、それとは別に style ブロックを追加してあげます。FrontPage.vue<style> @import url('https://fonts.googleapis.com/css?family=Sawarabi+Mincho'); </style> <style lang="scss"> html { font-size: 62.5%; font-family: "Sawarabi Mincho", "Hiragino Mincho Pro", "MS Mincho", sans-serif; } #app { .section { // ... } } </style>これでOKでした。
vue-head で読ませる
CSS の
@import
はつかいたくないなぁ、って場合は、HEADタグに突っ込むわけですが、できれば public/index.html は触りたくないなぁ、ということで、<header></header>
にいろいろ追加できる vue-head を使えばできます。
これは、meta とか title を書き換えるのに使うことが多いですが、スタイルシートや JS、favicon なんかも読ませることができます。
(これらも HEAD 内に書いてるんでまぁ当たり前なのかもしれませんが、意外と忘れがちなのは私だけ?)FrontPage.vue<script> export default { head: { link: [ { rel: 'stylesheet', href: 'https://fonts.googleapis.com/css?family=Sawarabi+Mincho' }, ] }, } </script>まとめ
- とりあえず、vue-router も使ってて、タイトル書き換えとかしてるなら vue-head つかってるだろうから、そこで読ませる。
- ルーター使ってないような LP などの超シンプルページの場合は、 style ブロックに
@import
するか、それでもヘッダーにはいろいろ書くだろうから、vue-head 入れちゃうのがやっぱり楽かも。- Vue + Electron みたいな感じで、PC/Mac 向けのアプリにするとかで、Android 全く考慮しないで良い&どうしてもオフラインで使わなくちゃいけないってときは、 npm の typeface シリーズを使ってもよいのかも
- 投稿日:2019-12-23T08:49:33+09:00
Nuxt.js、Google Apps Script、スプレッドシート(DB)を組み合わせて検索アプリケーションを作ってみた
Google Apps Script(GAS)で Nuxt 動かしてみたら面白いのでは?
と思い試していたら動いたので、スプレッドシートをDBにして検索アプリケーションを作ってみました!Nuxt.js の SPAモードを GAS を使って実際に GAS で Nuxt を動かしているURL
https://script.google.com/macros/s/AKfycbw9rOqkFPqP4Ym3n7goiL0tI4V3cx0UTOjVM8DTHT8FRG3ogjJs/execそして Google サイトで上記 URL を埋め込んで完成です。(GAS の 分かりづらい URL を隠すため)
https://sites.google.com/view/nuxt-gas-webapp/本記事では Nuxt を GAS 上で実行するためのやりかたを主に解説してきます。
まずnuxt build
を実行した結果がどうなっているのかを見ていきましょう。そもそも Nuxt はどういうファイルを出力するのか
pages/index.vue
とpages/dev.vue
を作っただけのシンプルな Nuxt を build してみます。
ソース: https://github.com/howdy39/nuxt-gas-webapp/tree/master/nuxt/pagesbuild結果Asset Size Chunks Chunk Names ../server/client.manifest.json 7.64 KiB [emitted] 4dc3b569e856e43cdf74.js 2.33 KiB 4 [emitted] [immutable] runtime 59dd7e691e85c4d93bde.js 162 KiB 1 [emitted] [immutable] commons.app 6f78c935aa007f5c95d3.js 44.6 KiB 0 [emitted] [immutable] app LICENSES 510 bytes [emitted] c963b7fb6c8e63b5a272.js 1.19 KiB 2 [emitted] [immutable] pages/dev ea45f7c48fea148ed3c9.js 2.81 KiB 3 [emitted] [immutable] pages/index + 1 hidden asset Entrypoint app = 4dc3b569e856e43cdf74.js 59dd7e691e85c4d93bde.js 6f78c935aa007f5c95d3.jstreeを表示howdy39$ tree dist [~/projects/nuxt-gas-webapp] dist ├── 200.html ├── README.md ├── _nuxt │ ├── 4dc3b569e856e43cdf74.js │ ├── 59dd7e691e85c4d93bde.js │ ├── 6f78c935aa007f5c95d3.js │ ├── LICENSES │ ├── c963b7fb6c8e63b5a272.js │ └── ea45f7c48fea148ed3c9.js ├── dev │ └── index.html ├── favicon.ico └── index.html 2 directories, 11 filesこの中で実行に最低限必要なのは次の3つのファイル群です。
- ルーティングに必要な html
- Entrypoint である
runtime
commons.app
app
の js- ページごとの js である
pages/index
pages/dev
の jsつまり1〜3のファイル群を GAS 上で適切に読み込ませることができれば、 GAS で Nuxt を動かせます。
次は一旦 Nuxt から離れて GAS で Web 画面を作る手順を見ていきましょう。
GAS は HTML や JavaScript をホスティングすることはできない
GAS は HTML, JavaScript, CSS, 画像 などのファイルをホスティングすることはできません。
じゃあ Web サーバーとしては使えないのか、というとそうではありません。次の図のようにベースとなる html を GAS の HtmlService.createTemplateFromFile を使ってレスポンスに設定すると HTML を返す Web サーバーになります。
この html に CSS や JavaScript のファイルを include して一つのの html にまとめてしまえば、CSS や JavaScript を含めることができます。
※ または CDN を使う手法もあります。(CDNを使うのが一般的です)※詳細は公式ドキュメントを参照ください。
HTML Service: Best Practices | Apps Script | Google DevelopersNuxt の構成と GAS で Web 画面を配信する仕組みはわかりましたね。
次は Nuxt との差分をどう埋めていくかです。GAS で Nuxt が出力する HTML や JavaScript を読み込ませるために何を変えなければならないのか
次の2点が必要です。
- js ファイルを
<script>...</script>
の形式に変換、include できるように拡張子を html に変えるindex.html
を作成し、1で変換したファイルを include するポイントとして、Nuxt が生成した html は流用しません。
ほとんど JavaScript を読み込んでいるだけなので、自作して、include を使った形にするだけで十分だからです。これらの作業を手動でやるのはしんどいので、自動で行うスクリプトを Node で作ります。
nuxt build
の結果の js ファイルの一覧を読み込んでゴニョゴニョする感じです。clas-build.jsconst fs = require('fs') const path = require('path') const chalk = require('chalk') const ejs = require('ejs') const rimraf = require('rimraf') const mkdirp = require('mkdirp') const sourceDirectory = 'dist/_nuxt' const destDirectory = 'build' try { console.log(chalk.blue('Start') + ' clasp-build') rimraf.sync(destDirectory) mkdirp(destDirectory) const filenames = fs.readdirSync(sourceDirectory).filter(name => name.endsWith('.js')) // GASで読み込むためにjsをhtmlに変換 filenames.forEach((fileName) => { const data = fs.readFileSync(path.join(sourceDirectory, fileName), 'utf8') const writeData = `<script> ${data} </script>` fs.writeFileSync(path.join(destDirectory, fileName.replace('.js', '.html')), writeData) console.log(`Wrote ${fileName}`) }) // index.htmlを生成して上書き const templateHtml = ` <!doctype html> <html> <body> <div id="__nuxt"><style>#nuxt-loading{visibility:hidden;opacity:0;position:absolute;left:0;right:0;top:0;bottom:0;display:flex;justify-content:center;align-items:center;flex-direction:column;animation:nuxtLoadingIn 10s ease;-webkit-animation:nuxtLoadingIn 10s ease;animation-fill-mode:forwards;overflow:hidden}@keyframes nuxtLoadingIn{0%{visibility:hidden;opacity:0}20%{visibility:visible;opacity:0}100%{visibility:visible;opacity:1}}@-webkit-keyframes nuxtLoadingIn{0%{visibility:hidden;opacity:0}20%{visibility:visible;opacity:0}100%{visibility:visible;opacity:1}}#nuxt-loading>div,#nuxt-loading>div:after{border-radius:50%;width:5rem;height:5rem}#nuxt-loading>div{font-size:10px;position:relative;text-indent:-9999em;border:.5rem solid #f5f5f5;border-left:.5rem solid #fff;-webkit-transform:translateZ(0);-ms-transform:translateZ(0);transform:translateZ(0);-webkit-animation:nuxtLoading 1.1s infinite linear;animation:nuxtLoading 1.1s infinite linear}#nuxt-loading.error>div{border-left:.5rem solid #ff4500;animation-duration:5s}@-webkit-keyframes nuxtLoading{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes nuxtLoading{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}</style><script>window.addEventListener("error",function(){var e=document.getElementById("nuxt-loading");e&&(e.className+=" error")})</script><div id="nuxt-loading" aria-live="polite" role="status"><div>Loading...</div></div></div> <% filenames.forEach(function (value, key) { %> <?!= include('<%= value %>'); ?> <% }); %> </body> </html> ` const html = ejs.render(templateHtml, { filenames: filenames.map(f => f.replace('.js', '')) }) fs.writeFileSync(path.join(destDirectory, 'index.html'), html) console.log(`Wrote index.html`) } catch (e) { console.log(chalk.red(e)) } console.log(chalk.green(`Success`) + ' clasp-build')これを実行した結果を
clasp push
で GAS にデプロイします。
※ GAS のGoogle謹製CLIツール clasp - Qiitaすると次のようなファイルになります。
もうひとつのポイント
GAS の Web アプリケーション は
https://script.google.com/macros/s/AKfycbwZEWZGSK7fFJD74gmvna8efkJQkjTNYUzt4gfHtCEXhfEiC4GZ/exec
のような URL でアクセスしますがこの URL が HTML をそのまま返すわけではありません。
次のような多重のサンドボックス構造になっているのです。つまり exec ページが読み込まれるわけでもありませんし、 「URLを
〜exec/dev
にしてpages/dev.vue
を表示しよう!」 なんてこともできません。そのため次のように
nuxt.config.js
のルーティングを変えてかならずindex.vue
が読み込まれるようにします。nuxt.config.jsrouter: { extendRoutes (routes, resolve) { routes.push({ name: 'custom', path: '*', component: resolve(__dirname, 'nuxt/pages/index.vue') }) } },ウェブアプリケーションとして導入
デプロイまで終わったら 「公開」 ー 「ウェブアプリケーションとして導入」 を実行するだけです。
では、実際にウェブアプリケーションを見てみましょう。
Nuxt のページが動いていますし、index ページと devページの行き来ができることも確認できます。https://script.google.com/macros/s/AKfycbwZEWZGSK7fFJD74gmvna8efkJQkjTNYUzt4gfHtCEXhfEiC4GZ/exec
この技術を応用して検索アプリケーション作りました!
それが記事冒頭のアプリケーションです。
https://sites.google.com/view/nuxt-gas-webapp/ポイントは GAS の HTML Service で作ったブラウザのグローバルオブジェクトに
google.script.run().GASの関数名
の形で実行することで、JavaScript から任意の GAS の function を実行できます。※ GAS は全ユーザーの情報を JSON で返すだけで、絞り込み自体はブラウザ側の JavaScript でやっています。
(2回目以降の検索は GAS にアクセスせずにブラウザだけで完結するようにするため)さいごに
この構成はアリ・ナシでいうと、会社などで G Suite(有料版 Gmail) を使っていて社内向けの簡易的なシステムを作るなら、かなりアリかなと思います。
GAS の Web アプリケーションの公開時に
Who has access to the app
という公開した URL に誰がアクセスできるのかという設定項目があります。
次の画像は筆者が持っているtecthetoarster.org
という G Suite のWho has access to the app
の選択項目です。
Anyone within Tech The Toaster
を選べば、@tecthetoarster.org
のアカウントにログインしていないと実行できない URL になります。
特定のドメイン内限定で使える Web アプリケーションを作るのは面倒ですよね。お手軽にセキュアな Web アプリケーションを作れるのはメリットかなと思います。全コードは GitHub にあげてあるので良かったら参考にしてみてください。
howdy39/nuxt-gas-webapp - シンプルなやつ
howdy39/nuxt-gas-webapp - ユーザー検索アプリケーション
- 投稿日:2019-12-23T07:50:36+09:00
モバイル環境でNetwork Errorでハマった。「EC2にDocker Caddy Laravel, S3にVuejsの構成」
指摘などウェルカムです。
クロスオリジン(CORS)に関連するエラーなんだと思うけどPCでは動くが、携帯 IOS Iphone7(古い)からのブラウザ(最新)ではsafariもchromeも動かないって現象で結構ハマった。環境
大体の構成
route53から分岐
-> CloudFront+S3にvuejs
-> ALBからEC2(Ubuntu)にDocker Caddy Laravel解決策
サーバー側のクロスオリジンの設定をちゃんと設定する。
Access-Control-Allow-Origin
Access-Control-Allow-Headers
をざっくり設定して * としていた。
これが原因だった。
ちゃんとフロントs3側のホスト名を指定したら動いた。
この記事で必須と書いてあるので、ん?と思って設定したら動いた。:OK.php <?php namespace App\Http\Middleware; use Closure; class Cors { public function handle($request, Closure $next) { return $next($request) //ちゃんとホスティングしてるs3のドメインを指定 ->header(‘Access-Control-Allow-Origin’, ‘https://hoge.s3.com’) ->header(‘Access-Control-Allow-Methods’, ‘GET, POST, PUT, DELETE, OPTIONS’) ->header(‘Access-Control-Allow-Headers’, ‘X-Requested-With, Content-Type, X-Token-Auth, Authorization’); } }駄目.php<?php namespace App\Http\Middleware; use Closure; class Cors { public function handle($request, Closure $next) { return $next($request) ->header(‘Access-Control-Allow-Origin’, ‘*’) ->header(‘Access-Control-Allow-Methods’, ‘GET, POST, PUT, DELETE, OPTIONS’) ->header(‘Access-Control-Allow-Headers’, ‘X-Requested-With, Content-Type, X-Token-Auth, Authorization’); } }https://whatsupguys.net/programming-learning-135/
調べた履歴
Laravelのルーティングについて結構大事だと思う。
axiosでget, post以外にoptionも使うのでルーティング注意。Route::match(['options', 'patch']'/{test_id}', 'TestController@update'); Route::match(['options', 'delete'], '/{test_id}', 'TestController@destroy');CORSを許可したLaravel製APIサーバーでput, patch, deleteが出来なくて泣いてたけど、ようやく解決出来た話
CSRFが問題か?
X-Requested-Withヘッダか?Laravel/Vue SPAs: How to send AJAX requests and not run into CSRF token mismatch exceptions
https://medium.com/@serhii.matrunchyk/laravel-vue-spas-how-to-send-ajax-requests-and-not-run-into-csrf-tokens-mismatch-exceptions-da3b71b287ab
https://www.techalyst.com/posts/vuejs-axios-laravel-x-csrf-token-x-xsrf-token-csrf-protectionaxiosが悪いのか?
https://qiita.com/terrierscript/items/ccb56b6fc05aa7821c42crossのエラーぽいが。
https://stackoverflow.com/questions/50873764/cross-origin-read-blocking-corb
https://stackoverflow.com/questions/38749605/cors-access-control-allow-origin-on-laravel
https://stackoverflow.com/questions/20035101/why-does-my-javascript-code-get-a-no-access-control-allow-origin-header-is-pr
https://github.com/laravel/framework/issues/13643
https://medium.com/@petehouston/allow-cors-in-laravel-2b574c51d0c1サーバの.htaccessが悪いのか?
https://forum.laragon.org/topic/1435/access-control-allow-origin-is-already-set/6モバイルではそもそも動かないのか?
Axios doesn't work with Android (emulator) raising a Network Error
https://github.com/axios/axios/issues/973
- 投稿日:2019-12-23T07:14:20+09:00
plunkerでvue その43
概要
plunkerでvueやってみた。
俺のvueがie11で、動かない。SCRIPT1002: 構文エラーです。
ie11は、アローが無い。
axios.get("cart.php").then(response => (this.shop = response.data))var self = this axios.get("cart.php").then(function(response) { self.shop = response.data })SCRIPT1003: ':' がありません。
ie11は、省略を、許さない。
computed: { total() { var total = 0; for (var i = 0; i < this.items.length; i++) { total += this.items[i].price; } return total; } },computed: { total: function() { var total = 0; for (var i = 0; i < this.items.length; i++) { total += this.items[i].price; } return total; } },SCRIPT5009: 'Promise' は定義されていません。
ie11は、以下を追加。
<script src="https://www.promisejs.org/polyfills/promise-6.1.0.min.js"></script>以上。
- 投稿日:2019-12-23T05:40:04+09:00
【Vue.js】v-forループ内でFont awesome5アイコンを動的に表示するサンプルコード
はじめに
v-forループ内でFont awesome5アイコンを動的に表示するサンプルコードです。
環境
OS: macOS Catalina 10.15.1 Vue: 2.6.10 vue-cli: 4.1.1前提
Vue-CLIでFont awesome5を使う手順については、以下記事をご覧下さい。
【Vue.js】Vue-CLI4.1.1でFont awesome5を使う手順 - Qiita
指定方法
各アイコンのクラス名から、
prefix
とicon
に使う文字列を確認しておきます。表示方法
Font awesomeのアイコン指定方法は下記のように複数選択可能です。
<font-awesome-icon :icon="['fas', 'dog']"/><font-awesome-icon fas icon="dog" /><font-awesome-icon prefix="fas" icon="dog" />今回は一番下のものを使ってみます。
:prefix
、:icon
としてv-bind
を適用します。コード
<template> ...略 <ul> <li v-for="item in items" :key="item.id"> <p> <font-awesome-icon :prefix="item.prefix" :icon="item.icon" /> {{ item.message }} </p> </li> </ul> ...略 </template> <script> export default { name: 'attention', data(){ return { items: [ { id: 1, prefix: 'fas', icon: 'phone-square', message: 'お電話はこちら'}, { id: 2, prefix: 'fas', icon: 'dog', message: 'ペット可'}, { id: 3, prefix: 'fas', icon: 'question-circle', message: '心配毎はご相談下さい'} ] } }, } </script>おわりに
最後まで読んで頂きありがとうございました
どなたかの参考になれば幸いです
参考にさせて頂いたサイト(いつもありがとうございます)
- 投稿日:2019-12-23T04:46:12+09:00
Laravel6系 に Vuetify を入れてみる
こんにばんわ! @ktoshi です。
今回は私が困ったときに縋りつく Laravel についてお話します。
ちなみに私はインフラエンジニアです。メインは。目的
Laravel6 に Vuetify を導入したい。
Laravel6 より Vue.js などが標準ライブラリから外れたため、Laravel5 以前の記事ではそのまま導入ができなくなりました。
毎度導入しているときに複数の記事を見ながら、導入しているので備忘録もかねて。Vuetify とは
公式HP
Vue.js のコンポーネント集です。
ボタンやテーブルなどを描画する際のコンポーネントがあつまっているので、
モダンなデザインを容易に作ることができます。環境
OS: Windows 10 Pro
PHP: 7.3.10Composer インストール
Laravel で使用するパッケージ管理に利用します。
みなさんご存じですよね。多くは語りません。
Composerインストール手順(Windows) を参考されたし。node.js インストール
node.js 自体をサーバとして利用も可能ですが、Laravel では主に Vue.js の
ビルドを目的に利用します。
みなさんご存じですよね。多くは語りません。
Node.js / npmをインストールする(for Windows) を参考されたし。
私の環境ではバージョンは下記でした。node --version v12.13.0 npm --version 6.12.0Laravel インストール
みなさんご存じですよね。多くは語りません。
composer create-project laravel/laravel --prefer-dist <PROJECT NAME>これでカレントディレクトリ以下に PROJECT_NAME のフォルダが作成され、
その配下に Laravel が導入された状態となっています。Vue.js のインストール
先でも述べたように Laravel5 以前であれば、この状態で
npm install
をしてやると
Vue.js が導入できましたが、 Laravel6 以降ではひと手間必要です。
ひと手間が料理をおいしくすると同じくひと手間が Laravel をおいしくします。(知りません# プロジェクトフォルダへ移動 cd <PROJECT NAME> # 必要なライブラリをインストール composer require laravel/ui # 私は Vue を使うねん!と宣言 php artisan ui vue # おら! npm installこれで Vue.js の導入が完了です。
Laravel5 以前でnpm install
を叩いた状態と同じですね。Vuetify インストール
Vuetify は npm(Node.js のパッケージ管理ツール)を用いてインストールします。
(いや、そのコマンドさっき使ってたのに今更説明かい。)# Vuetify の最新パッケージのインストール npm install vuetifyインストールが終われば、Laravel 側で Vuetify を利用するように設定します。
resoureces/js/app.js/** * First we will load all of this project's JavaScript dependencies which * includes Vue and other libraries. It is a great starting point when * building robust, powerful web applications using Vue and Laravel. */ require('./bootstrap'); window.Vue = require('vue'); import Vuetify from 'vuetify'; import 'vuetify/dist/vuetify.min.css'; Vue.use(Vuetify); /** * The following block of code may be used to automatically register your * Vue components. It will recursively scan this directory for the Vue * components and automatically register them with their "basename". * * Eg. ./components/ExampleComponent.vue -> <example-component></example-component> */ // const files = require.context('./', true, /\.vue$/i) // files.keys().map(key => Vue.component(key.split('/').pop().split('.')[0], files(key).default)) Vue.component('example-component', require('./components/ExampleComponent.vue').default); /** * Next, we will create a fresh Vue application instance and attach it to * the page. Then, you may begin adding components to this application * or customize the JavaScript scaffolding to fit your unique needs. */ const app = new Vue({ el: '#app', vuetify: new Vuetify(), });これで Laravel にてVuetify を利用できる状態となりました。
実践
では、実際にVuetify を使ったページを作ってみましょう。
元々用意されているサンプルのファイルを書き換えてみます。resources\js\components\ExampleComponent.vue<template> <v-app> <v-container> <v-row> <v-col> <v-card class="mx-auto"> <v-card-text> <p class="text--primary"> Hello Vuetify World!! </p> </v-card-text> <v-divider></v-divider> <v-card-actions> <v-btn>やったね!!</v-btn> </v-card-actions> </v-card> </v-col> </v-row> </v-container> </v-app> </template>resources\views\welcome.blade.php<!doctype html> <html lang="{{ app()->getLocale() }}"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- CSRF Token --> <meta name="csrf-token" content="{{ csrf_token() }}"> <title>{{ config('app.name', 'Laravel') }}</title> <!-- Styles --> <link rel="stylesheet" href="{{ mix('css/app.css') }}"> </head> <body> <div id="app"> <v-app> <!-- これがさっき修正したファイル --> <example-component></example-component> </v-app> </div> <!-- Scripts --> <script src="{{ mix('js/app.js') }}"></script> </body> </html>次に Vue.js のビルドと Laravel を起動させます。
なお、各コマンドともに実行状態が続くので二つのプロンプトで作業をしてください。# Laravel の起動 php artisan serve Laravel development server started: http://127.0.0.1:8000 # これが出ればOK# ビルドコマンドを発行。watch にしておくと変更を検知して勝手にビルドしてくれるので楽。 npm run watch DONE Compiled successfully in XXXms # これが出ればOKそれぞれが起動すれば
http://localhost:8000/
へ接続してみましょう。
下記のようなページが表示されていればOKです。
まとめ
Laravel6 から標準でなくなった Vue.js のインストールまで行ってしまえば、
後は Laravel5 と同様の方法で Vuetify を利用することが可能です。私は今まで element-ui を主に使っていましたが、今回は Vuetify を利用する、という記事を書いてみました。
なんで変えたかって?飽きたからです。
ただ、実際に Vuetify を使ってみて思ったのはデザインなどは非常に好みでした。
テーブルに関しても element-ui より柔軟でよかったです。
ただ、timepicker のデザインが不満だったのと element-ui の datetimepicker がマジ神だなと感じました。それでは皆様、よい Vuetify ライブをお送りください。
- 投稿日:2019-12-23T04:29:26+09:00
Vue.jsでぷ○コンみたいなジョイスティックを作った
概要
タイトルそのままなのだけど
ぷ○コン作りました。PWAの時代来ると思うし。
きっとVueで手軽にゲーム作りたいなぁってときにこういうのいると思うし。
移動のインターフェイスとしてはとても使い勝手がいいし。(○にコンと言いつつ正確にはタップした場所を始点に差分を出してるのが同じなだけで見た目は別物)
デモ
https://punidemo.firebaseapp.com/
たんにヒヨコ(?)を移動させるだけ。
マウスとタップどちらにも対応してるのでPCブラウザでもスマホでも動くはず。
リセットボタンは位置が初期位置に戻るだけ。(以下説明用に円の色変え)
1番外側はタップした際の表示位置に固定で表示される。
1番内側のは一定距離まで動かすと距離が固定されるやつ。
正直これは見た目用なので機能としてはいらないやつ。
(タップの距離が中心と離れすぎるとどの方向に伸ばしてるのか分からなくなるので目印に)
環境
Vue.jsと書いてるけどデモ自体はNuxt.jsで作成。
試してないけど、nuxtの要素は特に無いのでたぶんvue-cliとかで作ったプロジェクトでも動くはず…?
vueは2.6.10
nuxtは2.10.2
PCのchromeとスマホのsafariでしか確認してない。コンポーネントの概要
タッチ位置を始点にして差分を検出、呼び出し元に対して
・x: 始点からの差分の単位ベクトルのx
・y: 始点からの差分の単位ベクトルのy
・v: 始点からの差分の絶対値
・rad: 始点からのラジアン
の4つを通知している。
※デモではradは使ってないし合ってるかも検証してない、なんとなく今後使いみちあるんじゃと思ってつけたソースコード
puni.vue<template> <div style="position:absolute;width:100%;height:100%;top:0px;left:0px;z-index:1000;" @mousemove="mousemove($event)" @mouseup="touchend($event)" @mouseleave="touchend($event)" @touchmove="touchmove($event)" @touchend="touchend($event)" @mousedown="mousestart($event)" @touchstart="touchstart($event)" > <div style="user-select: none" :style="padArea.style" v-show="isMousedown" > <div style="position:relative;width:100%;height:100%;"> <div :style="padHead.style" /> <div :style="padRoot.style" /> </div> </div> </div> </template> <script> export default { mixins: [], components: {}, head() { return {}; }, data() { return { isMousedown: false, padViewPosition: { x: 0, y: 0 }, padMovePosition: { x: 0, y: 0 }, padHead: { size: 48, edgeSize: 5, color: "#aaa5", zIndex: 1100, style: {} }, padRoot: { size: 32, edgeSize: 3, color: "#aaa5", zIndex: 1050, style: {} }, padArea: { size: 100, edgeSize: 3, color: "#aaa5", zIndex: 1010, style: {} } }; }, watch: { padMovePosition: { handler(newValue, oldValue) { const x = this.padMovePosition.x - (this.padViewPosition.x + this.padArea.size / 2); const y = this.padMovePosition.y - (this.padViewPosition.y + this.padArea.size / 2); const vel = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)); // 呼び出し元へ通知 this.$emit("input", { x: x == 0 ? 0 : x / vel, y: y == 0 ? 0 : y / vel, v: vel, rad: Math.atan2(y, x) }); // タップ先端のstyle this.padHead.style = { position: "absolute", width: this.padHead.size + this.padHead.edgeSize * 2 + "px", height: this.padHead.size + this.padHead.edgeSize * 2 + "px", top: this.padMovePosition.y - this.padViewPosition.y - this.padHead.size / 2 - this.padHead.edgeSize + "px", left: this.padMovePosition.x - this.padViewPosition.x - this.padHead.size / 2 - this.padHead.edgeSize + "px", "z-index": this.padHead.zIndex, "border-radius": this.padHead.size / 2 + this.padHead.edgeSize + "px", border: this.padHead.edgeSize + "px solid " + this.padHead.color, "box-sizing": "border-box" }; // タップの根本のstyle const padRootPosition = (() => { if (vel > this.padArea.size / 2) { let value = this.padArea.size / 2 / vel; return { x: x * value + (this.padViewPosition.x + this.padArea.size / 2), y: y * value + (this.padViewPosition.y + this.padArea.size / 2) }; } else { return { x: this.padMovePosition.x, y: this.padMovePosition.y }; } })(); this.padRoot.style = { position: "absolute", width: this.padRoot.size + this.padRoot.edgeSize * 2 + "px", height: this.padRoot.size + this.padRoot.edgeSize * 2 + "px", top: padRootPosition.y - this.padViewPosition.y - this.padRoot.size / 2 - this.padRoot.edgeSize + "px", left: padRootPosition.x - this.padViewPosition.x - this.padRoot.size / 2 - this.padRoot.edgeSize + "px", "z-index": this.padRoot.zIndex, "border-radius": this.padRoot.size / 2 + this.padRoot.edgeSize + "px", border: this.padRoot.edgeSize + "px solid " + this.padRoot.color, "box-sizing": "border-box" }; }, deep: true }, padViewPosition: { handler(newValue, oldValue) { this.padArea.style = { background: "#eee5", position: "absolute", width: this.padArea.size + this.padArea.edgeSize * 2 + "px", height: this.padArea.size + this.padArea.edgeSize * 2 + "px", top: this.padViewPosition.y + "px", left: this.padViewPosition.x + "px", "user-select": "none", "z-index": this.padArea.zIndex, "border-radius": this.padArea.size / 2 + this.padArea.edgeSize + "px", border: this.padArea.edgeSize + "px solid " + this.padArea.color, "box-sizing": "border-box" }; }, deep: true } }, created() {}, mounted() { this.padMovePosition = { x: this.padViewPosition.x + this.padArea.size / 2, y: this.padViewPosition.y + this.padArea.size / 2 }; }, computed: {}, methods: { // 触ったときの処理(マウス) mousestart(e) { this.isMousedown = true; this.padViewPosition.x = e.pageX - this.padArea.size / 2; this.padViewPosition.y = e.pageY - this.padArea.size / 2; }, // 触ったときの処理(タップ) touchstart(e) { this.isMousedown = true; let touch = e.targetTouches[0]; this.padViewPosition.x = touch.pageX - this.padArea.size / 2; this.padViewPosition.y = touch.pageY - this.padArea.size / 2; }, // 動いている間の処理(マウス) mousemove(e) { if (this.isMousedown) { this.padMovePosition.x = e.pageX; this.padMovePosition.y = e.pageY; } }, // 動いている間の処理(タップ) touchmove(e) { if (this.isMousedown) { if (e.targetTouches.length == 1) { let touch = e.targetTouches[0]; this.padMovePosition.x = touch.pageX; this.padMovePosition.y = touch.pageY; } } }, // 離したときの処理(マウス・タップ共通) touchend(e) { if (this.isMousedown) { this.isMousedown = false; this.padMovePosition.x = this.padViewPosition.x + this.padArea.size / 2; this.padMovePosition.y = this.padViewPosition.y + this.padArea.size / 2; } } } }; </script>ざっくりした使い方
①呼び出し元の方でimportする
import puni from "~/components/puni";export default { components: { puni, },②設置
<puni v-model="puniInfo"/>シンプルなのでv-modelでコンポーネントからの通知を省略しているけど、特に画面の方からコンポーネント側に何かをバインドしているわけではない(コンポーネント側ではpropsの設定はしていない)ので、ちゃんと書くなら下記の方が適切かもしれない。
<puni @input="puniInfo = $event"/>③コンポーネントから通知された値を使って移動
setInterval(() => { if (this.puniInfo && !(this.puniInfo.x == 0 && this.puniInfo.y == 0)) { this.position.x += this.puniInfo.x * 3 * (this.puniInfo.v <= 45 ? 0.5:1); this.position.y += this.puniInfo.y * 3 * (this.puniInfo.v <= 45 ? 0.5:1); } }, 16 );ヒヨコの座標がposition。
3は良い感じのスピード。
45は移動スピードの閾値(よくある、普通の移動とゆっくり移動の設定)。
setIntervalの16はFPS60だとそれくらいかなって。ざっくりした変更の仕方
円の大きさを変えたいとか色を変えたい!程度のことはdataのここをいじればいけるはず。
padHead: { size: 48, edgeSize: 5, color: "#aaa5", zIndex: 1100, style: {} }, padRoot: { size: 32, edgeSize: 3, color: "#aaa5", zIndex: 1050, style: {} }, padArea: { size: 100, edgeSize: 3, color: "#aaa5", zIndex: 1010, style: {} }size: 円の直径
edgeSize: borderのサイズ
color: 名前が適切じゃない、colorだけどborderに設定してる色の事
zIndex: そのまま
style: これはwatchで自動設定するやつなので変更してはいけないざっくりした注意点
コンポーネント自体が
position:absolute; top:0; left:0; width:100%; height:100%;となっている。
つまり画面全体を覆いつつ(0,0)からの位置を前提にしている。
なのでposition:relativeと位置座標が設定されているタグの中に放り込むと位置がズレる。
あとz-indexを1000代に設定している。
(コンポーネントを変更せずにそのまま使いつつ)もしボタンを設定するなら、コンポーネントが画面全体を覆っている関係でz-indexが適用されるようにしつつz-index2000以上を使わないといけない。
(「z-index 効かない」とかでぐぐるとすぐに出てくる。大抵positionの設定が漏れてる。)悩みどころ
コンポーネントが画面全体を覆う作りって微妙じゃないかなって…
タップの始点はpropsで受け取るようにした方が…と思いつつも機能的には始点から差分まで求めるのはコンポーネントで完結させたいよなぁ…おわりに
コンポーネントの主要なパラメータがまだベタ書きで変更にも弱い箇所あったりするので改良していく所存。
来年はこれ使ってクソアプリ作るぞ!
- 投稿日:2019-12-23T04:29:26+09:00
Vue.jsでぷ○コンを作ってコンポーネント化した
概要
タイトルそのままなのだけど
ぷ○コン作りました。PWAの時代来ると思うし。
きっとVueで手軽にゲーム作りたいなぁってときにこういうのいると思うし。
移動のインターフェイスとしてはとても使い勝手がいいし。(○にコンと言いつつ正確にはタップした場所を始点に差分を出してるのが同じなだけで見た目は別物)
デモ
https://punidemo.firebaseapp.com/
たんにヒヨコ(?)を移動させるだけ。
マウスとタップどちらにも対応してるのでPCブラウザでもスマホでも動くはず。
リセットボタンは位置が初期位置に戻るだけ。(以下説明用に円の色変え)
1番外側はタップした際の表示位置に固定で表示される。
1番内側のは一定距離まで動かすと距離が固定されるやつ。
正直これは見た目用なので機能としてはいらないやつ。
(タップの距離が中心と離れすぎるとどの方向に伸ばしてるのか分からなくなるので目印に)
環境
Vue.jsと書いてるけどデモ自体はNuxt.jsで作成。
試してないけど、nuxtの要素は特に無いのでたぶんvue-cliとかで作ったプロジェクトでも動くはず…?
vueは2.6.10
nuxtは2.10.2
PCのchromeとスマホのsafariでしか確認してない。コンポーネントの概要
タッチ位置を始点にして差分を検出、呼び出し元に対して
・x: 始点からの差分の単位ベクトルのx
・y: 始点からの差分の単位ベクトルのy
・v: 始点からの差分の絶対値
・rad: 始点からのラジアン
の4つを通知している。
※デモではradは使ってないし合ってるかも検証してない、なんとなく今後使いみちあるんじゃと思ってつけたソースコード
puni.vue<template> <div style="position:absolute;width:100%;height:100%;top:0px;left:0px;z-index:1000;" @mousemove="mousemove($event)" @mouseup="touchend($event)" @mouseleave="touchend($event)" @touchmove="touchmove($event)" @touchend="touchend($event)" @mousedown="mousestart($event)" @touchstart="touchstart($event)" > <div style="user-select: none" :style="padArea.style" v-show="isMousedown" > <div style="position:relative;width:100%;height:100%;"> <div :style="padHead.style" /> <div :style="padRoot.style" /> </div> </div> </div> </template> <script> export default { mixins: [], components: {}, head() { return {}; }, data() { return { isMousedown: false, padViewPosition: { x: 0, y: 0 }, padMovePosition: { x: 0, y: 0 }, padHead: { size: 48, edgeSize: 5, color: "#aaa5", zIndex: 1100, style: {} }, padRoot: { size: 32, edgeSize: 3, color: "#aaa5", zIndex: 1050, style: {} }, padArea: { size: 100, edgeSize: 3, color: "#aaa5", zIndex: 1010, style: {} } }; }, watch: { padMovePosition: { handler(newValue, oldValue) { const x = this.padMovePosition.x - (this.padViewPosition.x + this.padArea.size / 2); const y = this.padMovePosition.y - (this.padViewPosition.y + this.padArea.size / 2); const vel = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)); // 呼び出し元へ通知 this.$emit("input", { x: x == 0 ? 0 : x / vel, y: y == 0 ? 0 : y / vel, v: vel, rad: Math.atan2(y, x) }); // タップ先端のstyle this.padHead.style = { position: "absolute", width: this.padHead.size + this.padHead.edgeSize * 2 + "px", height: this.padHead.size + this.padHead.edgeSize * 2 + "px", top: this.padMovePosition.y - this.padViewPosition.y - this.padHead.size / 2 - this.padHead.edgeSize + "px", left: this.padMovePosition.x - this.padViewPosition.x - this.padHead.size / 2 - this.padHead.edgeSize + "px", "z-index": this.padHead.zIndex, "border-radius": this.padHead.size / 2 + this.padHead.edgeSize + "px", border: this.padHead.edgeSize + "px solid " + this.padHead.color, "box-sizing": "border-box" }; // タップの根本のstyle const padRootPosition = (() => { if (vel > this.padArea.size / 2) { let value = this.padArea.size / 2 / vel; return { x: x * value + (this.padViewPosition.x + this.padArea.size / 2), y: y * value + (this.padViewPosition.y + this.padArea.size / 2) }; } else { return { x: this.padMovePosition.x, y: this.padMovePosition.y }; } })(); this.padRoot.style = { position: "absolute", width: this.padRoot.size + this.padRoot.edgeSize * 2 + "px", height: this.padRoot.size + this.padRoot.edgeSize * 2 + "px", top: padRootPosition.y - this.padViewPosition.y - this.padRoot.size / 2 - this.padRoot.edgeSize + "px", left: padRootPosition.x - this.padViewPosition.x - this.padRoot.size / 2 - this.padRoot.edgeSize + "px", "z-index": this.padRoot.zIndex, "border-radius": this.padRoot.size / 2 + this.padRoot.edgeSize + "px", border: this.padRoot.edgeSize + "px solid " + this.padRoot.color, "box-sizing": "border-box" }; }, deep: true }, padViewPosition: { handler(newValue, oldValue) { this.padArea.style = { background: "#eee5", position: "absolute", width: this.padArea.size + this.padArea.edgeSize * 2 + "px", height: this.padArea.size + this.padArea.edgeSize * 2 + "px", top: this.padViewPosition.y + "px", left: this.padViewPosition.x + "px", "user-select": "none", "z-index": this.padArea.zIndex, "border-radius": this.padArea.size / 2 + this.padArea.edgeSize + "px", border: this.padArea.edgeSize + "px solid " + this.padArea.color, "box-sizing": "border-box" }; }, deep: true } }, created() {}, mounted() { this.padMovePosition = { x: this.padViewPosition.x + this.padArea.size / 2, y: this.padViewPosition.y + this.padArea.size / 2 }; }, computed: {}, methods: { // 触ったときの処理(マウス) mousestart(e) { this.isMousedown = true; this.padViewPosition.x = e.pageX - this.padArea.size / 2; this.padViewPosition.y = e.pageY - this.padArea.size / 2; }, // 触ったときの処理(タップ) touchstart(e) { this.isMousedown = true; let touch = e.targetTouches[0]; this.padViewPosition.x = touch.pageX - this.padArea.size / 2; this.padViewPosition.y = touch.pageY - this.padArea.size / 2; }, // 動いている間の処理(マウス) mousemove(e) { if (this.isMousedown) { this.padMovePosition.x = e.pageX; this.padMovePosition.y = e.pageY; } }, // 動いている間の処理(タップ) touchmove(e) { if (this.isMousedown) { if (e.targetTouches.length == 1) { let touch = e.targetTouches[0]; this.padMovePosition.x = touch.pageX; this.padMovePosition.y = touch.pageY; } } }, // 離したときの処理(マウス・タップ共通) touchend(e) { if (this.isMousedown) { this.isMousedown = false; this.padMovePosition.x = this.padViewPosition.x + this.padArea.size / 2; this.padMovePosition.y = this.padViewPosition.y + this.padArea.size / 2; } } } }; </script>ざっくりした使い方
①呼び出し元の方でimportする
import puni from "~/components/puni";export default { components: { puni, },②設置
<puni v-model="puniInfo"/>シンプルなのでv-modelでコンポーネントからの通知を省略しているけど、特に画面の方からコンポーネント側に何かをバインドしているわけではない(コンポーネント側ではpropsの設定はしていない)ので、ちゃんと書くなら下記の方が適切かもしれない。
<puni @input="puniInfo = $event"/>③コンポーネントから通知された値を使って移動
setInterval(() => { if (this.puniInfo && !(this.puniInfo.x == 0 && this.puniInfo.y == 0)) { this.position.x += this.puniInfo.x * 3 * (this.puniInfo.v <= 45 ? 0.5:1); this.position.y += this.puniInfo.y * 3 * (this.puniInfo.v <= 45 ? 0.5:1); } }, 16 );ヒヨコの座標がposition。
3は良い感じのスピード。
45は移動スピードの閾値(よくある、普通の移動とゆっくり移動の設定)。
setIntervalの16はFPS60だとそれくらいかなって。ざっくりした変更の仕方
円の大きさを変えたいとか色を変えたい!程度のことはdataのここをいじればいけるはず。
padHead: { size: 48, edgeSize: 5, color: "#aaa5", zIndex: 1100, style: {} }, padRoot: { size: 32, edgeSize: 3, color: "#aaa5", zIndex: 1050, style: {} }, padArea: { size: 100, edgeSize: 3, color: "#aaa5", zIndex: 1010, style: {} }size: 円の直径
edgeSize: borderのサイズ
color: 名前が適切じゃない、colorだけどborderに設定してる色の事
zIndex: そのまま
style: これはwatchで自動設定するやつなので変更してはいけないざっくりした注意点
コンポーネント自体が
position:absolute; top:0; left:0; width:100%; height:100%;となっている。
つまり画面全体を覆いつつ(0,0)からの位置を前提にしている。
なのでposition:relativeと位置座標が設定されているタグの中に放り込むと位置がズレる。
あとz-indexを1000代に設定している。
(コンポーネントを変更せずにそのまま使いつつ)もしボタンを設定するなら、コンポーネントが画面全体を覆っている関係でz-indexが適用されるようにしつつz-index2000以上を使わないといけない。
(「z-index 効かない」とかでぐぐるとすぐに出てくる。大抵positionの設定が漏れてる。)悩みどころ
コンポーネントが画面全体を覆う作りって微妙じゃないかなって…
タップの始点はpropsで受け取るようにした方が…と思いつつも機能的には始点から差分まで求めるのはコンポーネントで完結させたいよなぁ…おわりに
コンポーネントの主要なパラメータがまだベタ書きで変更にも弱い箇所あったりするので改良していく所存。
来年はこれ使ってクソアプリ作るぞ!
- 投稿日:2019-12-23T01:32:23+09:00
Vue.js+element uiでTableUIを遊び倒す
この記事は、「NJC Advent Calendar 2019」 12月23日の記事です。
はじめに
Vue.js+element uiで開発をした際、element uiのTableコンポーネントを色々触ったので備忘録として書きます。
備忘録ですみません。正直な話良いネタが思いつかなかったです
また、実現したいことが日本のドキュメントであまりなかったので記事で残してたら需要あるかなと。。。環境
Vue.js 2.6.10
element-ui 2.13.0環境構築
今回は、Codepenで作成していくのでそちらの環境作成手順です。
①Settingsを開きます。
②JavaScriptタブで設定をします。
③Add External Scripts/Pensで以下のワードで検索して追加します。
・vue
・element-ui
⑤以下画面になっていればsave & Closeを押して設定を保存します。
実際の開発環境については先人の方々の資料を参考にしてください。
・Vue.js環境構築参考
https://qiita.com/yoshi0518/items/76195d52b794c4b81816
・Elemnt UI環境構築参考
https://qiita.com/Sa2Knight/items/7386a83c2058e53f0eef実際に作成してみる
今回は以下二種類の機能を実装する。
・①RowクリックイベントでRowの詳細を出す機能
・②各Rowの文字が改行しないで「...」にする機能簡単なTable
Basic tableをそのまま作成する。公式サイト参考Table
html<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css"> <div id="app"> <template> <el-table :data="tableData" style="width: 100%" > <el-table-column prop="date" label="Date" width="180"> </el-table-column> <el-table-column prop="name" label="Name" width="180"> </el-table-column> <el-table-column prop="address" label="Address"> </el-table-column> </el-table> </template> </div>CSS#app{ width: 600px; }main.jsVue.config.devtools = true; new Vue({ el: '#app', data() { return { tableData: [{ date: '2016-05-03', name: 'Tom', address: 'No. 189, Grove St, Los Angeles' }, { date: '2016-05-02', name: 'Tom', address: 'No. 189, Grove St, Los Angeles' }, { date: '2016-05-04', name: 'Tom', address: 'No. 189, Grove St, Los Angeles' }, { date: '2016-05-01', name: 'Tom', address: 'No. 189, Grove St, Los Angeles' }] } } });①RowクリックイベントでRowの詳細を出す
やりたいこと
expandable-rowを用いることで以下画像のように各Rowに詳細の表示切替ができる。しかし、公式サイトの情報は赤枠部分のクリックで実現している情報のみである。
これを各Row全体がクリックされても表示切替ができるようにする。
実装
html<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css"> <div id="app"> <template> <el-table :data="tableData" style="width: 100%" @row-click="rowClicked" ref="tableData" id="clickable-rows" > <el-table-column type="expand"> <template slot-scope="props"> Old: {{ props.row.details.old }}<br> Like: {{ props.row.details.like }}<br> DisLike: {{ props.row.details.dislike }}<br> </template> </el-table-column> <el-table-column prop="date" label="Date" width="180"> </el-table-column> <el-table-column prop="name" label="Name" width="180"> </el-table-column> <el-table-column prop="address" label="Address"> </el-table-column> </el-table> </template> </div>css#app{ width: 600px; } .clickable-rows { tbody tr td { cursor: pointer; } } .el-table__expanded-cell { cursor: default; } }main.jsVue.config.devtools = true; new Vue({ el: '#app', methods: { rowClicked(row) { this.$refs.tableData.toggleRowExpansion(row); } }, data() { return { tableData: [{ date: '2016-05-03', name: 'Tom', address: 'No. 189, Grove St, Los Angeles', details: { old: '16 years old', like: 'play Baseball', dislike: 'Math' } }, { date: '2016-05-02', name: 'Tom', address: 'No. 189, Grove St, Los Angeles', details: { old: '16 years old', like: 'play Baseball', dislike:'Math' } }, { date: '2016-05-04', name: 'Tom', address: 'No. 189, Grove St, Los Angeles' }, { date: '2016-05-01', name: 'Tom', address: 'No. 189, Grove St, Los Angeles', details: { old: '16 years old', like: 'play Baseball', dislike: 'Math' } }] } } });解説
<el-table-column type="expand">
でexpandを設定して、そのタグ内に<template slot-scope="props">
を記載することで、Rowクリック時に下に表示させる部分を準備する。実際はこの時点でtype="expand"
を設定したカラムに詳細行の表示切替実装できているが、Row全体のクリックイベントではない。
Row全体に表示切替機能を実装するには、<el-table>
で設定した@row-click="rowClicked"
、ref="tableData"
を用いる。
@row-click="rowClicked"
イベントで呼び出される際にメソッド側で選択したRow情報が引数として使用することができる。
メソッド内でthis.$refs.tableData.toggleRowExpansion(row);
を実行することによりクリックしたRowの詳細切替を実装することができる。②各Rowの文字が改行させないで「・・・」にする
やりたいこと
以下画像のように文字数がカラムの幅を超えた場合に、改行せずに
「・・・」
と省略するように実装する。
実装
htmlとCSSを変更していく。
html<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css"> <div id="app"> <template> <el-table :data="tableData" style="width: 100%" @row-click="rowClicked" ref="tableData" id="clickable-rows" > <el-table-column type="expand"> <template slot-scope="props"> Old: {{ props.row.details.old }}<br> Like: {{ props.row.details.like }}<br> DisLike: {{ props.row.details.dislike }}<br> </template> </el-table-column> <el-table-column prop="date" label="Date" width="180"> </el-table-column> <el-table-column prop="name" label="Name" width="180"> </el-table-column> <el-table-column label="Address"> <template slot-scope="scope"> <div id="textOver"> {{ scope.row.address }} </div> </template> </el-table-column> </el-table> </template> </div>css#app{ width: 600px; list-style: none; } #textOver { text-overflow: ellipsis; overflow: hidden; white-space: nowrap; } .clickable-rows { tbody tr td { cursor: pointer; } } .el-table__expanded-cell { cursor: default; } }解説
CSSで設定する。
#textOver
の設定で「・・・」を実装できる。
※「・・・」のやり方は調べれば結構情報落ちてます。
cssで設定した#textOver
をhtmlに反映させるために、以下のようにel-table-column
を変更します。
template slot-scope="scope"
とすることによりel-table
で渡された:data="tableData"
をscopeとして受け取ることでtemplateタグ内で様々なデザインを可能にすることができます。変更部分<el-table-column prop="address" label="Address"> </el-table-column> ↓↓↓下記のように変更↓↓↓ <el-table-column label="Address"> <template slot-scope="scope"> <div id="textOver"> {{ scope.row.address }} </div> </template> </el-table-column>成果物
See the Pen qBEmaVJ by Kento_tj (@Kento-GREEN) on CodePen.
最後に
Element UIは部品が豊富な印象を受けました。
また、実装のやりやすさであまり工数をかけたくないとか、簡単なフロントの作成に向いているなと感じました。
ただ、ちょっとしたデザインを実装する際は日本のドキュメントは少ないなと。。。
実装の方法とか忘れそうなんで、他のElement UIの部品も備忘録として残そうかと思います!!
批判、アドバイス等大歓迎です。
お手柔らかにお願いします。おまけ
「②各Rowの文字が改行させないで「・・・」にする」に関して補足。
今回の実装では、Safariで確認すると文字省略、改行せずに文字がカラムを抜きぬけて表示されるみたいです。
なぜかはわからないですが、、、
回避方法としては、それぞれのel-table-column
にwidthをpxで設定してあげることが必要みたいです。
- 投稿日:2019-12-23T00:54:31+09:00
SkyWay API + Rails6 + Vue でビデオチャットアプリを作る② 複数人同時接続
SkyWayAPIを使って複数人でのビデオチャットアプリに挑戦します!
先週投稿した「SkyWay API + Rails6 + Vue でビデオチャットアプリを作る①」の続きです。目標物
複数人が同時に参加できるビデオチャットアプリの作成。
部屋は既に作られていて、そこに入室したところから開始です。注意
前回の回で使ったコードを基本使い回します。
railsのプロジェクトがあること、webpackerがインストールされていることを前提に進めていきます。サンプルコードの分析
SkyWayが提供している複数同時接続のパターンのDEMOです。
https://example.webrtc.ecl.ntt.com/room/index.htmlそのソースコードです。
パッとみてよくわからない部分があったので、上から順にコメントをつけていきました。githubリポジトリ
https://github.com/skyway/skyway-js-sdk/tree/master/examples/roomscript.js//Peerモデルを定義 const Peer = window.Peer; (async function main() { //操作がDOMをここで取得 const localVideo = document.getElementById('js-local-stream'); const joinTrigger = document.getElementById('js-join-trigger'); const leaveTrigger = document.getElementById('js-leave-trigger'); const remoteVideos = document.getElementById('js-remote-streams'); const roomId = document.getElementById('js-room-id'); const roomMode = document.getElementById('js-room-mode'); const localText = document.getElementById('js-local-text'); const sendTrigger = document.getElementById('js-send-trigger'); const messages = document.getElementById('js-messages'); const meta = document.getElementById('js-meta'); const sdkSrc = document.querySelector('script[src*=skyway]'); meta.innerText = ` UA: ${navigator.userAgent} SDK: ${sdkSrc ? sdkSrc.src : 'unknown'} `.trim(); //同時接続モードがSFUなのかMESHなのかをここで設定 const getRoomModeByHash = () => (location.hash === '#sfu' ? 'sfu' : 'mesh'); //divタグに接続モードを挿入 roomMode.textContent = getRoomModeByHash(); //接続モードの変更を感知するリスナーを設置 window.addEventListener( 'hashchange', () => (roomMode.textContent = getRoomModeByHash()) ); //自分の映像と音声をlocalStreamに代入 const localStream = await navigator.mediaDevices .getUserMedia({ audio: true, video: true, }) .catch(console.error); // localStreamをdiv(localVideo)に挿入 localVideo.muted = true; localVideo.srcObject = localStream; localVideo.playsInline = true; await localVideo.play().catch(console.error); // Peerのインスタンス作成 const peer = (window.peer = new Peer({ key: window.__SKYWAY_KEY__, debug: 3, })); // 「div(joinTrigger)が押される&既に接続が始まっていなかったら接続」するリスナーを設置 joinTrigger.addEventListener('click', () => { if (!peer.open) { return; } //部屋に接続するメソッド(joinRoom) const room = peer.joinRoom(roomId.value, { mode: getRoomModeByHash(), stream: localStream, }); //部屋に接続できた時(open)に一度だけdiv(messages)に=== You joined ===を表示 room.once('open', () => { messages.textContent += '=== You joined ===\n'; }); //部屋に誰かが接続してきた時(peerJoin)、いつでもdiv(messages)に下記のテキストを表示 room.on('peerJoin', peerId => { messages.textContent += `=== ${peerId} joined ===\n`; }); //重要: streamの内容に変更があった時(stream)videoタグを作って流す room.on('stream', async stream => { const newVideo = document.createElement('video'); newVideo.srcObject = stream; newVideo.playsInline = true; // 誰かが退出した時どの人が退出したかわかるように、data-peer-idを付与 newVideo.setAttribute('data-peer-id', stream.peerId); remoteVideos.append(newVideo); await newVideo.play().catch(console.error); }); //重要: 誰かがテキストメッセージを送った時、messagesを更新 room.on('data', ({ data, src }) => { messages.textContent += `${src}: ${data}\n`; }); // 誰かが退出した場合、div(remoteVideos)内にある、任意のdata-peer-idがついたvideoタグの内容を空にして削除する room.on('peerLeave', peerId => { const remoteVideo = remoteVideos.querySelector( `[data-peer-id=${peerId}]` ); //videoストリームを止める上では定番の書き方らしい。https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack/stop remoteVideo.srcObject.getTracks().forEach(track => track.stop()); remoteVideo.srcObject = null; remoteVideo.remove(); messages.textContent += `=== ${peerId} left ===\n`; }); // 自分が退出した場合の処理 room.once('close', () => { //メッセージ送信ボタンを押せなくする sendTrigger.removeEventListener('click', onClickSend); //messagesに== You left ===\nを表示 messages.textContent += '== You left ===\n'; //remoteVideos以下の全てのvideoタグのストリームを停めてから削除 Array.from(remoteVideos.children).forEach(remoteVideo => { remoteVideo.srcObject.getTracks().forEach(track => track.stop()); remoteVideo.srcObject = null; remoteVideo.remove(); }); }); // ボタン(sendTrigger)を押すとonClickSendを発動 sendTrigger.addEventListener('click', onClickSend); // ボタン(leaveTrigger)を押すとroom.close()を発動 leaveTrigger.addEventListener('click', () => room.close(), { once: true }); //テキストメッセージを送る処理 function onClickSend() { room.send(localText.value); messages.textContent += `${peer.id}: ${localText.value}\n`; localText.value = ''; } }); peer.on('error', console.error); })();index.html<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>SkyWay - Room example</title> <link rel="stylesheet" href="../_shared/style.css"> </head> <body> <div class="container"> <h1 class="heading">Room example</h1> <p class="note"> Change Room mode (before join in a room): <a href="#">mesh</a> / <a href="#sfu">sfu</a> </p> <div class="room"> <div> <video id="js-local-stream"></video> <span id="js-room-mode"></span>: <input type="text" placeholder="Room Name" id="js-room-id"> <button id="js-join-trigger">Join</button> <button id="js-leave-trigger">Leave</button> </div> <div class="remote-streams" id="js-remote-streams"></div> <div> <pre class="messages" id="js-messages"></pre> <input type="text" id="js-local-text"> <button id="js-send-trigger">Send</button> </div> </div> <p class="meta" id="js-meta"></p> </div> <script src="//cdn.webrtc.ecl.ntt.com/skyway-latest.js"></script> <script src="../_shared/key.js"></script> <script src="./script.js"></script> </body> </html>サンプルコードのvue.js化
上記の内容をvue.jsに書き換えていきます!
サンプルと違う部分
- スタイルは一旦全部無視です。
- 接続モードは本来2種類ありますが、今回はsfuをデフォルトに実装しました。
- railsのviewを経由してroomIdを取得するようにしています。
room.vue<template> <div id="app"> <template v-for="stream in remoteStreams"> <!-- ①srcObjectをバインドする --> <video autoplay playsinline :srcObject.prop="stream" ></video> </template> <video id="my-video" muted="true" width="500" autoplay playsinline></video> <p>ROOM ID: <span id="room-id">{{ roomId }}</span></p> <button v-if="roomOpened === true" @click="leaveRoom" class="button--green">Leave</button> <button v-else @click="joinRoom" class="button--green">Join</button> <br /> <div> マイク: <select v-model="selectedAudio" @change="onChange"> <option disabled value="">Please select one</option> <option v-for="(audio, key, index) in audios" v-bind:key="index" :value="audio.value"> {{ audio.text }} </option> </select> カメラ: <select v-model="selectedVideo" @change="onChange"> <option disabled value="">Please select one</option> <option v-for="(video, key, index) in videos" v-bind:key="index" :value="video.value"> {{ video.text }} </option> </select> </div> <template v-for="message in messages"> <p>{{message}}</p> </template> </div> </template> <script> const API_KEY = "6d7fe6d0-40c7-4acd-9586-063dd7b633dd"; // const Peer = require('../skyway-js'); export default { data: function () { return { audios: [], videos: [], selectedAudio: '', selectedVideo: '', localStream: {}, messages: [], roomId: "", remoteStreams: [], roomOpened: false } }, methods: { // 端末のカメラ音声設定 onChange: function(){ if(this.selectedAudio != '' && this.selectedVideo != ''){ this.connectLocalCamera(); } }, connectLocalCamera: async function(){ const constraints = { audio: this.selectedAudio ? { deviceId: { exact: this.selectedAudio } } : false, video: this.selectedVideo ? { deviceId: { exact: this.selectedVideo } } : false } const stream = await navigator.mediaDevices.getUserMedia(constraints); document.getElementById('my-video').srcObject = stream; this.localStream = stream; }, leaveRoom: function(){ if (!this.peer.open) { return; } this.roomOpened = false; this.room.close(); }, // 「div(joinTrigger)が押される&既に接続が始まっていなかったら接続」するリスナーを設置 joinRoom: function(){ if (!this.peer.open) { return; } this.roomOpened = true; //部屋に接続するメソッド(joinRoom) this.room = this.peer.joinRoom(this.roomId, { mode: "sfu", stream: this.localStream, }); //部屋に接続できた時(open)に一度だけdiv(messages)に=== You joined ===を表示 this.room.once('open', () => { this.messages.push('=== You joined ==='); }); //部屋に誰かが接続してきた時(peerJoin)、いつでもdiv(messages)に下記のテキストを表示 this.room.on('peerJoin', peerId => { this.messages.push(`=== ${peerId} joined ===`); }); //重要: streamの内容に変更があった時(stream)videoタグを作って流す this.room.on('stream', async stream => { await this.remoteStreams.push(stream); }); //重要: 誰かがテキストメッセージを送った時、messagesを更新 this.room.on('data', ({ data, src }) => { this.messages.push(`${src}: ${data}`); }); // 誰かが退出した場合、div(remoteVideos)内にある、任意のdata-peer-idがついたvideoタグの内容を空にして削除する this.room.on('peerLeave', peerId => { const index = this.remoteStreams.findIndex((v) => v.peerId === peerId); const removedStream = this.remoteStreams.splice(index, 1); this.messages.push(`=== ${peerId} left ===`); }); // 自分が退出した場合の処理 this.room.once('close', () => { //メッセージ送信ボタンを押せなくする this.messages.length = 0; }); } }, created: async function(){ const element = document.getElementById("room") const data = JSON.parse(element.getAttribute('data')) this.roomId = data.roomId //ここでpeerのリスナーを設置 this.peer = new Peer({key: API_KEY, debug: 3}); //新規にPeerオブジェクトの作成 //デバイスへのアクセス const deviceInfos = await navigator.mediaDevices.enumerateDevices(); //オーディオデバイスの情報を取得 deviceInfos .filter(deviceInfo => deviceInfo.kind === 'audioinput') .map(audio => this.audios.push({text: audio.label || `Microphone ${this.audios.length + 1}`, value: audio.deviceId})); //カメラの情報を取得 deviceInfos .filter(deviceInfo => deviceInfo.kind === 'videoinput') .map(video => this.videos.push({text: video.label || `Camera ${this.videos.length - 1}`, value: video.deviceId})); } } </script> <style scoped> p { font-size: 2em; text-align: center; } </style>vue化のコツ
本記事の趣旨とは異なりますが、素のJSをvueに書き換える時のコツです。
- 定数の定義をcreatedフックに集める
- クリック系のリスナーは全部関数に切り出してDOMの@ clickで発火するようにする
- その他のリスナーはcreatedフックに集める(又は、任意のアクション内)
- 変数をdataに整理する
- createElementやappendなどでDOMを挿入するケースは、dataとfor文をうまく使ってまとめる罠
videoタグのsrcをバインドさせる時、srcがオブジェクトの場合 :srcObject.prop="オブジェクト"という形で渡してあげないとエラーになります。
参考記事:vue.jsで複数のvideoタグを扱う
https://qiita.com/dbgso/items/271d903237b41dffcc6dRails側のコード
rails側の設定です。
rooms/show.html.erb// roomIdを渡す処理 <% props = { roomId: "aiueo" }.to_json %> <div id='room' data="<%= props %>"> <room/> </div> <%= javascript_pack_tag 'room' %> <%= stylesheet_pack_tag 'room' %>routes.rbRails.application.routes.draw do get 'rooms/show' root 'rooms#show' # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html endrooms_controller.rbclass RoomsController < ApplicationController def show end end結果
これでうまく動くとこんな感じです。
4つのウィンドウを開いてテストしています。残念なお知らせ
相手が部屋から出た時の挙動にバグがあります。近日中に直す予定です?♂️
最後に
今回はこれで以上です。
同時接続もSkyWayAPIを使うと楽チンです。
コピペでも動くと思うので、是非ご自身でも試してみてください。