- 投稿日:2020-05-19T23:09:03+09:00
@nuxt/componentsを使って、コンポーネントを自動でimportしよう
@nuxt/componentsとは
Nuxt.jsでの開発において、コンポーネントを自動でimportしてくれるモジュールです。
import
文とcomponents
フィールドの定義を省略できます。
https://github.com/nuxt/component使用例
READMEのUSAGEに従って進めます。
https://github.com/nuxt/components#usage通常、コンポーネントを利用する際はimportする必要がありますが、
index.vue<template> <div> <ComponentFoo /> </div> </template> <script> import ComponentFoo from '~/components/ComponentFoo.vue' export default { components: { ComponentFoo } } </script>@nuxt/componentsを使うと、
<script>
にimportの記載が不要になります。index.vue<template> <div> <ComponentFoo /> </div> </template>動的なコンポーネントの場合
Lazy
をコンポーネントの先頭につけます。index.vue<template> <div> <LazyComponentFoo v-if='foo' /> <button @click='loadFoo'>Load Foo</button> </div> </template> <script> export default { data () { return { foo: null } }, methods: { async loadFoo () { this.foo = await this.getFoo() }, getFoo () { return new Promise(resolve => { setTimeout(() => { resolve('foo'); }, 2000); }); } } } </script>ネストした同名のコンポーネントの場合
たとえば以下のように、異なる階層に同名の
Bar.vue
がある場合。components/ Bar.vue foo/ Bar.vue
nuxt.config.js
に以下を追加します。nuxt.config.jscomponents: { dirs: [ '~/components/', { path: '~/components/foo/', prefix: 'foo' } ] },
foo/
は以下のコンポーネントは先頭にFoo
をつけます。index.vue<template> <div> // components/ のBar.vue <Bar /> // components/foo/ のBar.vue <FooBar /> </div> </template>参考
- 投稿日:2020-05-19T23:07:57+09:00
DataTablesを使って遊戯王カードの表を作る!(2)
はじめに
こんにちは。hyです。
前回、前の記事ではDatatablesを用いて遊戯王カードを表示する表を表示するアプリを作りました。
今回はそのアプリのコードについて解説していこうと思います。使う技術
- DataTables
- 簡単に綺麗な表や機能を追加できるjQueryライブラリ
- Mustache.js
- javascriptのテンプレートエンジンで,今回は主にDataTableと併用して使います。
- Vue.js
- javascriptのフレームワークの一つ。学習コストが低いことが特徴
- Yu-Gi-Oh! API by YGOPRODeck
- 遊戯王カードの画像や効果などの情報を取得できるAPI
作成したアプリ
See the Pen datatable-app by higakin (@hgaiji) on CodePen.
解説
以下がDataTableで表を表示するためのコードです。
html<table id="ygo_table" class="table table-bordered"> <thead> <tr> <th>name</th> <th>desc</th> </tr> </thead> <tbody> </tbody> </table>javascriptmakeTables : function() { const self = this; const options = { serverSide: true, ajax: { url:"https://db.ygoprodeck.com/api/v7/cardinfo.php?level=4", dataSrc:function(json) { return json.data.slice().sort(function(){ return Math.random() - 0.5; }).slice(0, 3); }, }, columns:[ {data:"name",render: self.renderName}, {data:"desc"}, ] } $("#ygo_table").DataTable(options); }, renderName :function(data, type, row, meta) { const template = '<div class="container">' + ' <div class="row">' + ' <div class="card" style="width: 18rem;">' + ' <img src={{renderSrc}} class="card-img-top" alt="カード画像">' + ' <div class="card-body">' + ' <p class="card-text">{{cardInfo.name}}</p>' + ' </div>' + ' </div>' + ' </div>' + '</div>'; const view = { cardInfo: row, renderSrc :function() { return this.cardInfo.card_images[0].image_url; } }; return Mustache.render(template,view); } }Datatablesで表を表示する
$("#ygo_table").DataTable(options);この部分の記述で指定いたIDのtableに対して表を表示します。
また、表を表示する時にオプションを設定することができます。//Datatablesのオプションんの設定 const options = { serverSide: true, ajax: { url:"https://db.ygoprodeck.com/api/v7/cardinfo.php?level=4", dataSrc:function(json) { return json.data.slice().sort(function(){ return Math.random() - 0.5; }).slice(0, 3); }, }, columns:[ {data:"name",render: self.renderName}, {data:"desc"}, ] }それぞれのオプションについて解説します。
- 表のデータの取得などをサーバ側で処理するためのオプション
- jOuery.ajaxの機能をほとんど引き続いて使用できるオプション
- サーバから取得したデータを好きに編集するためのオプション
- ajax.urlで取得したいサーバーのURLなどを設定
- ex)
url:"https://db.ygoprodeck.com/api/v7/cardinfo.php?level=4"
- サーバから得たデータを一部だけ使用するなどが可能
- 表の列を設定するためのオプション
- 各列の詳細な設定を定義することができる
columns.render( data, type, row, meta)
- 列のデータの表示の仕方をより詳細に設定できる
- 例えば、取得したデータの一部をリンクにするなど
- このrender関数で Mustache.jsを使用している
columns.renderとMustache.jsの併用
Datatablesのrender関数とMustache.jsを併用して使うことでより詳細なデータの設定ができます。
該当コードは以下の通りです。renderName :function(data, type, row, meta) { const template = '<div class="container">' + ' <div class="row">' + ' <div class="card" style="width: 18rem;">' + ' <img src={{renderSrc}} class="card-img-top" alt="カード画像">' + ' <div class="card-body">' + ' <p class="card-text">{{cardInfo.name}}</p>' + ' </div>' + ' </div>' + ' </div>' + '</div>'; const view = { cardInfo: row, renderSrc :function() { return this.cardInfo.card_images[0].image_url; } }; return Mustache.render(template,view); }
Mustache.render(template,view)
- template: {{}}(口髭)を含んだテンプレート
- view: テンプレートにレンダリングするためのデータオブジェクト
See the Pen Mustache.js sample by higakin (@hgaiji) on CodePen.
Vue.jsの中でjQueryを使うには?
いろいろなやり方があるのですが・・
以下のように、基本的にはmounted
の中にjQueryの処理を書いてやればいいです。
mounted
はVue.jsのライフサイクルでVueインスタンスがマウントされた(マウントされたDOMが生成された)ちょうど後に呼ばれます。なので JQueryのようなidなどを指定して操作を行う物はこのライフサイクルフックに入れてやればいいんです。new Vue({ el:"#app", data: { }, mounted:function(){ this.makeTables(); }, /////// }まとめ
- Datatablesの表の表示方法
- Datatablesのオプションについて
- datatablesとMustache.jsの併用について
- vueとJqueryの共存方法などについて
最後に
何かわからないこと疑問点など会ったら気軽にコメントお願いします。
また、もっとこんなコードにしたらいいんじゃないと言うコメントがあればそちらもぜひお願いいたします。
- 投稿日:2020-05-19T22:59:13+09:00
Nuxt.js + Sentry の本番運用での設定に戸惑った
フロントエンドのエラーを検知するのは、バグの早期発見、場合によってはリリースバージョンを切り戻しの判断をしたりする上で非常に重要です。
Sentryを使えば、かなり簡単にフロントエンドのログ収集の仕組みを実現出来ます。また、他の同様のツール(Rollbarなど)に比べてモダンなUIかつ、ドキュメントやTipsが豊富な気がします。また、nuxt-community にて sentry-module が提供されてる点もgood。
SentryにはReleaseという機能があり、運用環境へのデプロイとログを紐付けることが出来ます。また、SourceMapを一緒にアップロードしておくことで、本番環境でも stack trace を追えるようになります。
しかし、今回
@nuxtjs/sentry
を使ってReleaseしようとした際、公式の記述がさらっとしていてちょっと困ったので備忘録として残しておきますやりたかったこと
NustからSentryにReleaseを作成する。
Releaseの際にはSourceMapをSentryにアップロードする。
本番環境側ではSourceMapは見えないようにしたい。
解決方法
※ Sentry Project 立ててある前提です
Sentry側で用意しておくべきもの
- DSN: Project Settings の Client Keysから確認
- Auth Token: アカウント設定の Auth Tokens から作成 (Projectの書き込み権限をもつTokenを発行する)
- Organization Name: Organization Settings から確認
- Project Name
設定
$ npm i @nuxtjs/sentry
build時に sentry cli が使うオプションファイル
.sentryclirc
を準備.sentryclirc[defaults] org = organizarion-name project = project-name何も考えずに config
nuxt.config.js{ modules: [ '@nuxtjs/sentry', ], sentry: { dsn: process.env.SENTRY_DSN, publishRelease: true, sourceMapStyle: 'hidden-source-map', config: { release: process.env.GIT_SHA, }, }, // *** }
release
はtag名やpackages.json に記載してるバージョンなど何でもいいのですが、とりあえずリリース時点のコミットハッシュをいれておきました。
sourceMapStyle
は webpack の sourcemap を設定するプロパティです。 デフォルトの値は"source-map"
になってるので、本番運用では"hidden-source-map"
を指定してあげるとクライアント側では SourceMap を隠してくれます。READMEにも書いてあります。困ったこと
最初はReleaseの実行に必要な環境変数がわからなかったり、本番運用で必須な SourceMap の隠し方が分からず結構時間を取られてしまいました。解決するために こちらの記事 などを参考にさせていただき、SourceMapを除去する Webpack Plugin 書いたりとかしてました。
が、今回記事をまとめるにあたって改めてドキュメントを読んでみたらしっかりと書いてあるんですよね… 残念な英語読解力を痛感しました。
初めて Nuxt と Sentry を使われる方の参考にしていただければ幸いです。
参考
- 投稿日:2020-05-19T22:47:46+09:00
【備忘録】Vue.js CLIのプロジェクト構造概要
はじめに
フロントエンドの開発をやってみようと思ってVue CLIに入門したけどなかなかプロジェクト構造に慣れないので備忘録として記事に残しておきます。
素人なので間違いがありましたらご指摘お願いします。プロジェクト雛形作成
テンプレートではwebpackを使う事にします。コマンド入力後いくらか質問される。
$ vue init webpack sample-app ? Project name sample-app ? Project description sample Application of VueCLI ? Author TaiseiInoue ? Vue build standalone ? Install vue-router? Yes ? Use ESLint to lint your code? Yes ? Pick an ESLint preset Standard ? Set up unit tests No ? Setup e2e tests with Nightwatch? No ? Should we run `npm install` for you after the project has been created? (recommended) npm1つずつ見ていく。
アプリケーションプロジェクトの名前
? Project name sample-appプロジェクトの説明
? Project description sample Application of VueCLI開発者
? Author TaiseiInoueビルドバージョンの選択
完全版がおすすめされているのでそちらを選択? Vue build standaloneVue Rooterのインストール(Y/N)
Install vue-router? YesESLintの利用
ESLintはJavaScriptの静的コード解析ツールらしい。? Use ESLint to lint your code? Yes ? Pick an ESLint preset Standard単体テスト環境
今回は未選択? Set up unit tests NoE2Eテスト環境のセットアップ
? Setup e2e tests with Nightwatch? Noインストール方法の選択
? Should we run `npm install` for you after the project has been created? (recommended) npmプロジェクト構造
僕のイメージでざっくりと説明
sample-app $ tree -dL 1 ./ ./ ├── build #webpackの設定ファイルを格納 ├── config #本番環境や開発環境などを設定するファイルを格納 ├── node_modules #ライブラリなどを格納 ├── src #この中にプログラムを書いていく ├── static #webpacに処理されないファイルを格納 └── test #テストをコードを格納sample-app % tree src src ├── App.vue #実行エントリポイントとなるコンポーネント ├── assets │ └── logo.png ├── components #コンポーネントを格納 │ ├── atoms │ ├── molecules │ ├── organisms │ └── templates ├── main.js #アプリケーションのエントリポイント └── router └── index.js # ルーティングを定義する
components
内のHeeloWorld.vue
以外は自分で作成したディレクトリ。
気になる方はAtmic Design
を参照してください。どこに何を書けばいいの?
src内のファイルの中身を見ていく
src/components
コンポーネントとはUIの部品をモジュール化したもの。
(例:カスタムしたボタンや登録フォーム、ページ自体など)
コンポーネントの名前はケバブケースかパスカルケースで書くらしいsrc/component/template/MyComponent.vue<template> // templateを記述 </template> <script> export default { // この中にMyComponentの中身を記述 name: "MyComponent" } </script> <style scoped> </style>
<style>
のscoped
属性はその単一ファイルコンポーネント内の要素にのみ<style>
を適用したいときにつける。src/router
src/router/index.js// ルート定義をするファイル import Vue from 'vue' import Router from 'vue-router' import HelloWorld from '@/components/HelloWorld' import MyComponent from '@/template/MyComponet Vue.use(Router) export default new Router({ routes: [ { //デフォルトであるルート path: '/', name: 'HelloWorld', component: HelloWorld }, { //先ほど書いたcomponentのルート path: '/hoge', name: 'MyComponent', component: template/MyComponent } ] })URLを管理する。
アドレスに対応したコンポーネントを指定する。src/App.vue
src/App.vue<template> <div id="app"> <router-view/> </div> </template> <script> export default { name: 'App' } </script> <style> </style>実行エントリーポイント。全てのコンポーネントの親みたいな存在だと思ってる
src/rooter/index.jsで設定したルーティング処理により<rooter-view/>
に指定したコンポーネントが表示される。おわりに
とりあえずここまで理解しておけば入門したと言っても過言じゃない気がしているのでここまで
- 投稿日:2020-05-19T19:18:46+09:00
Vue でウィンドウサイズの変更を検知したいとき
Vue を使っていてウィンドウのリサイズを検知するには、Vuetify というコンポーネントフレームワークの v-resize というカスタムディレクティブが便利です。
<template> <span v-resize="onResize">{{ windowSize }}</span> </template> <script> export default { data: () => ({ windowSize: { x: 0, y: 0, }, }), mounted () { this.onResize() }, methods: { onResize () { this.windowSize = { x: window.innerWidth, y: window.innerHeight } }, }, } </script>このように、テンプレートで
v-resize
に発火したいメソッド名を渡すだけで、ウィンドウのサイズが変更された時にそのメソッドを実行することができます。ウィンドウや要素の幅に応じて動的に何かを設定する必要があるコードでは便利ですね。手元のコード量がほとんど増えないのが個人的にうれしいポイントです。
- 投稿日:2020-05-19T18:26:05+09:00
【Nuxt.js】アプリ開発実践編:Nuxt + Vuex + firebaseでログイン付きToDoリスト①
前置き
前回のTODOリストに
オプション機能をつけていきます♪
https://note.com/aliz/n/n8411db2c9a20今回はログイン機能を追加?
実際にサービスを運用するには
必須の機能ですね!?
以前の記事をやっていない方でも
仕組みと書き方が分かれば大丈夫です?♀️内容が濃いのでお知らせした通り、
有料記事になりました?【使うもの】
・Firebase Authentication
・Vuex(ログイン状態の保持)【流れ】
ボリュームがあるため2回に分けます!
・ログイン画面の作成
・新規アカウント作成画面の作成
・ログイン時とログアウト時の
表示の出し分け
・+a
メールアドレス以外のログイン
エラー時の処理や
アカウント作成時にメール送信❓公式guides, Referenceの読み方
基本的にfirebaseのreferenceは
英語の状態で表示し、
自分でgoogle翻訳で翻訳しましょう??言語を日本語にすると
古いバージョンだったりするので、
最新の英語を翻訳していくのがベスト!⭕️
ただいきなり全部英語だと
欲しい情報がどこにあるか分からないので
最初は日本語で表示させて
ある程度検討をつけてから英語にしてます?
(英語も理解できるように頑張ろう…?)Step1: firebaseAuthの準備
まずはfirebaseで既に作成したプロジェクトに
ログイン方法の設定をしていきます?
簡単にできるメールアドレスから!✉️
ログインできるかどうか判断するため
firebase上でアカウントを作成してみます??後からアカウント作成画面も作ります
?Googleアカウントでの
ログインなども追加していきます・プロジェクトの概要ページから
サイドメニューのAuthenticationを選択
ログイン方法を設定を選択・メールを選択
・メールでのログインを有効にし、保存
・usersタブに戻りユーザーを追加
登録できるとユーザー情報が表示されます?
Step2: ログイン機能の実装
- 投稿日:2020-05-19T15:44:55+09:00
スプレッドシートをDB代わりにGASのWebアプリを作成しデータ更新させてみた。Vue版 その4[完結編]~~
本稿でいよいよ完結します。
(はやくコロナも終わってくれないかなぁ)
今までのはこちら↓
スプレッドシートをDB代わりにGASのWebアプリを作成しデータ更新させてみた。Vue版 その1~~
スプレッドシートをDB代わりにGASのWebアプリを作成しデータ更新させてみた。Vue版 その2~~
スプレッドシートをDB代わりにGASのWebアプリを作成しデータ更新させてみた。Vue版 その3~~ハマったところもありました。。。
スプレッドシートをDB代わりにGASのWebアプリを作成しデータ更新させてみた。Vue版、ハマったところinputの再描画編~~
スプレッドシートをDB代わりにGASのWebアプリを作成しデータ更新させてみた。Vue版、ハマったところcheckboxの再描画編~~
ソースを公開する前に、データシートとマスターシートの説明をしておこうかなと思います。(忘れてましたすみません)
現状のイケてないところ
モーダルで詳細画面を開いたときに前回の内容が現れ、1パクおいてから表示される。
⇒データが切り替わったのを確認できるので、今はこのまま。描画判定するプロパティの変更位置を後にすれば解決するかも・・・モーダルの詳細画面で更新を掛けた後、一覧をリフレッシュしていない
⇒ちょっと試したんですが、実現できず・・・データシートの構成(シート名:database)
列 A列 B列 C列 D列 E列 F列 G列 H列 I列 J列 項目の見出し リンク行 タイムスタンプ 店舗名 郵便番号 県 住所 電話番号 営業時間 設備 感想 タグのid,name rownum timestamp tempo postnum ken address telnum activetime setsubi note 表示形式 read read text text select textarea text radio checkbox textarea 設定値 30 8 2 5,30 15 3 4 3,30 データ 5 2020/05/15 16:09:34 吉野家 有楽町2号店 100-0006 東京都 千代田区有楽町2丁目 100-0006123 9-21時かも 駐車場,ドライブスルーかも,駐輪場 とってもいい データ 6 2020/05/15 14:24:24 吉野家 永田町1号店 100-0014 東京都 千代田区永田町1丁目 100-0014123 9-18時かも ドライブスルーかも,駐輪場 うまーい 例えば、「住所」の場合、「address」という文言でhtml内のid,nameに設定します。
そして、表示形式は「textarea」となり、「設定値」が「5,30」なので、5行30文字の大きさで表示します。「県」の場合は、「ken」という文言でhtml内のid,nameに設定し
表示形式は「select」で設定値が「2」なので、マスターシート(シート名:itemmaster)の2列目を参照という意味になります。マスターシートの構成(シート名:itemmaster)
A列はサポートしている表示形式になります。(固定)
B列以降はカスタマイズできます。
1行目は、何のデータなのかわかりやすい文言を入れてください。表示やシステムには関係ありません。
そして、2行目から下がその項目のデータになります。
例えば、「県」や「営業時間」、「設備」は以下のように表示されます。
ソース公開
お恥ずかしいソースですが公開します。
スプレッドシート(シート名:database)
とりあえずcsvで。データは20件。
リンク行,タイムスタンプ,店舗名,郵便番号,県,住所,電話番号,営業時間,設備,感想,来店回数,好きなメニュー rownum,timestamp,tempo,postnum,ken,address,telnum,activetime,setsubi,note,repeat,menu read,read,text,text,select,textarea,text,radio,checkbox,textarea,radio,checkbox ,,30,8,2,"5,30",15,3,4,"3,30",5,6 5,2020/05/15 16:09:34,吉野家 有楽町2号店,100-0006,東京都,千代田区有楽町2丁目,100-0006123,9-21時かも,"駐車場,ドライブスルーかも,駐輪場",とってもいい ,, 6,2020/05/15 14:24:24,吉野家 永田町1号店,100-0014,東京都,千代田区永田町1丁目,100-0014123,9-18時かも,"ドライブスルーかも,駐輪場",うまーい,, 7,2020/05/15 13:52:57,吉野家 永田町1号店,100-0014,東京都,千代田区永田町1丁目,,9-21時かも,ドライブスルーかも,うまーい,, 8,2020/05/15 17:16:39,吉野家 水道橋1号店,101-0061,東京都,千代田区神田三崎町2丁目,1234566,9-18時かも,"駐車場,駐輪場",とってもいい ,, 9,2020/05/15 17:38:07,吉野家 小川町1号店A,101-0052,東京都,千代田区神田小川町2丁目,101-0052,24時間かも,駐車場,うまい,, 10,2020/02/12 14:57:55,吉野家 秋葉原1号店,101-0023,東京都,千代田区神田松永町,,9-21時かも,"駐車場,駐輪場",うまーい,, 11,2020/02/13 14:57:55,吉野家 神保町1号店,101-0051,東京都,千代田区神田神保町2丁目,,9-21時かも,"駐車場,駐輪場",,, 12,2020/05/18 9:37:11,吉野家 淡路町1号店,101-0041,東京都,千代田区神田須田町1丁目,101-00411111,9-21時かも,"駐車場,駐輪場",うまいよ~,15回まで,スタミナ超特盛丼 13,2020/02/15 14:57:55,吉野家 お茶の水1号店,101-0062,東京都,千代田区神田駿河台2丁目,,9-21時かも,"駐車場,駐輪場",,, 14,2020/02/16 14:57:55,吉野家 神田1号店,101-0044,東京都,千代田区鍛冶町2丁目,,9-21時かも,"駐車場,駐輪場",,, 15,2020/02/17 14:57:55,吉野家 霞ヶ関1号店,100-8918,東京都,千代田区霞が関,,9-21時かも,ドライブスルーかも,,, 16,2020/02/18 14:57:55,吉野家 霞が関2号店,100-0013,東京都,千代田区霞が関,,9-21時かも,ドライブスルーかも,,, 17,2020/02/19 14:57:55,吉野家 帯広1号店,080-0011,北海道,帯広市西1条南,,9-21時かも,ドライブスルーかも,,, 18,2020/02/20 14:57:55,吉野家 旭川1号店,078-8231,北海道,旭川市豊岡1条,,9-21時かも,,,, 19,2020/02/21 14:57:55,吉野家 旭川2号店,070-0034,北海道,旭川市4条通2丁目,,9-21時かも,,,, 20,2020/02/22 14:57:55,吉野家 岩見沢1号店,068-0825,北海道,岩見沢市日の出町,,9-21時かも,,,, 21,2020/02/23 14:57:55,吉野家 新千歳1号店,006-0012,北海道,千歳市美々 新千歳空港,,9-21時かも,,,, 22,2020/02/24 14:57:55,吉野家 千歳2号店,066-0036,北海道,千歳市北栄2丁目,,9-21時かも,,,, 23,2020/02/25 14:57:55,吉野家 1厚別東1号店,004-0004,北海道,札幌市厚別区厚別東4条3丁目,,9-21時かも,,,, 24,2020/02/26 14:57:55,吉野家 苫小牧新開町店,053-0052,北海道,苫小牧市新開町2丁目,,9-21時かも,,,,同じスプレッドシート内の別シート(シート名:itemmaster)
とりあえずcsvで。
disptype,県,営業時間,設備,リピート数,メニュー read,北海道,24時間かも,駐車場,初回,牛丼 text,青森県,9-21時かも,駐輪場,5回まで,スタミナ超特盛丼 textarea,岩手県,9-18時かも,ドライブスルーかも,10回まで,肉だく牛丼 select,宮城県,PMのみかも,,15回まで,ねぎだく牛丼 radio,秋田県,,,,ねぎたま牛丼 checkbox,山形県,,,,ライザップ牛サラダエビアボカド ,福島県,,,, ,茨城県,,,, ,栃木県,,,, ,群馬県,,,, ,埼玉県,,,, ,千葉県,,,, ,東京都,,,, ,神奈川県,,,, ,新潟県,,,, ,富山県,,,, ,石川県,,,, ,福井県,,,, ,山梨県,,,, ,長野県,,,, ,岐阜県,,,, ,静岡県,,,, ,愛知県,,,, ,三重県,,,, ,滋賀県,,,, ,京都府,,,, ,大阪府,,,, ,兵庫県,,,, ,奈良県,,,, ,和歌山県,,,, ,鳥取県,,,, ,島根県,,,, ,岡山県,,,, ,広島県,,,, ,山口県,,,, ,徳島県,,,, ,香川県,,,, ,愛媛県,,,, ,高知県,,,, ,福岡県,,,, ,佐賀県,,,, ,長崎県,,,, ,熊本県,,,, ,大分県,,,, ,宮崎県,,,, ,鹿児島県,,,, ,沖縄県,,,,コード.gs
// GASでお決まりなので必要 function doGet(e) { var template = HtmlService.createTemplateFromFile('vue_index'); return template.evaluate(); } //インクルードするためのもの function include(filename) { return HtmlService.createHtmlOutputFromFile(filename).getContent(); } //マスターデータを取得 function getMasterData(col) { var spread = SpreadsheetApp.getActiveSpreadsheet() ; var sheet = spread.getSheetByName('itemmaster'); var idname = sheet.getRange(1, col, 1, 1).getValues(); var values = sheet.getRange(2, col, sheet.getRange(2, col).getNextDataCell(SpreadsheetApp.Direction.DOWN).getRow()-1, 1).getValues(); //連想配列にする var res = {}; for (i=0; i < values.length; i++) { res[i]={ id:idname + i, item:values[i][0]}; } return res; } //更新や新規登録を行う //排他制御を掛けるので更新処理を一か所にまとめた function dataSave(sheet, row1, col1, row2, col2, data){ var msg = ""; //ドキュメントロックを使用する var lock = LockService.getDocumentLock(); //30秒間のロックを取得 try { //ロックを実施する lock.waitLock(30000); //ここにメインルーチンを記述する sheet.getRange(row1, col1, row2, col2).setValues(data); //メッセージを格納 msg = "保存完了"; } catch (e) { //ロック取得できなかった時の処理等を記述する var checkword = "ロックのタイムアウト: 別のプロセスがロックを保持している時間が長すぎました。"; //通常のエラーとロックエラーを区別する if(e.message == checkword){ //ロックエラーの場合 msg = "更新処理中でした"; }else{ //ソレ以外のエラーの場合 msg = e.message; } } finally { //ロックを開放する lock.releaseLock(); //メッセージを表示する //ui.alert(msg); } } //項目名称を取得する //-1が指定された場合は、オブジェクトの設定値も返す //0以外の整数が設定された場合、その数の設定項目名を返す function getItemNameList(col){ var res = []; var spread = SpreadsheetApp.getActiveSpreadsheet() ; var sheet = spread.getSheetByName('database'); if (col == -1) { var values = sheet.getRange(1, 1, 4, sheet.getLastColumn()).getValues(); } else { var values = sheet.getRange(1, 1, 1, col).getValues(); } return values; } //タイムスタンプの新しい方から取得する。dispRowにて件数調整可能 //[行番号,日付]の二次元配列で時間で降順 function getNewDataList(){ var dispRow = 10; var res = []; var spread = SpreadsheetApp.getActiveSpreadsheet() ; var sheet = spread.getSheetByName('database'); //2行目にシステム的な項目、3,4行目にHTMLのタグのタイプ、設定値を入れたので5行目からの取得とする var values = sheet.getRange(dispRow, 1, sheet.getLastRow()-1, 2).getValues(); //ソート sorting_asc sorting_desc values.sort(sorting_desc); for (var i = 0; i < dispRow + 1; i++){ res.push(values[i]); } return res; } //スプレッドシート内を文言で検索し行番号を返す(同じ行内に複数出てくると抽出結果も重複する function rowSearch(str){ var res = []; var spread = SpreadsheetApp.getActiveSpreadsheet() ; var sheet = spread.getSheetByName('database'); var textFinder = sheet.createTextFinder(str); var ranges = textFinder.findAll(); for(var i = 0; i < ranges.length; i++){ var range = sheet.getRange(ranges[i].getA1Notation()); res.push(range.getRow()); } var res2 = uniqueArray(res); return res2; } //項目名を付けてデータを1行返す。 function getItemNameAndData (itemCount,dataNo,dataCol) { var res = []; res = getItemNameList(itemCount); if (dataNo > 0) { res.push(getCellValue(dataNo,dataCol)); // checkボックスは2つ以上選択項目があるものとする // checkボックスのデータは配列に置き換える(テキストをカンマ区切りで配列に置き換える for (var i = 0; i < res[2].length; i++) { if (res[2][i] == "checkbox"){ if (res[4][i] == '' || res[4][i] == null) { res[4][i] = []; // データがなかったらカラの配列 } else { if ( String(res[4][i]).indexOf(',') != -1) { res[4][i] = res[4][i].split(','); // データがあってカンマ区切りなら配列にする } else { res[4][i] = [res[4][i]]; // データが1つしかない場合はそのまま配列にする } } } } } else { // 新規作成用 var tmp2 = Array(res[0].length); // select radio のデータはカラにする // checkbox のデータはカラの配列にする for (var i = 0; i < res[2].length; i++) { if (res[2][i] == "select" || res[2][i] == "radio" ){ tmp2[i] = ''; } else if (res[2][i] == "checkbox") { tmp2[i] = []; } else { tmp2[i] = "未設定"; } } res.push(tmp2); } //行列を入れ替える res = arrayTranspose(res); return res; } //行番号からセル値を取得 function getCellValue(row, col){ // 現在アクティブなスプレッドシートを取得 var ss = SpreadsheetApp.getActiveSpreadsheet(); var sheet = ss.getSheetByName('database'); // そのシートにある (1, 1) のセルから3行目までのセル範囲を取得 if (col == -1 ) { var range = sheet.getRange(row, 1, 1, sheet.getLastColumn()); } else { var range = sheet.getRange(row, 1, 1, col); } // そのセル範囲の値を取得 var values = range.getValues(); values[0][0] = row; values[0][1] = Utilities.formatDate( values[0][1], 'Asia/Tokyo', 'yyyy/MM/dd HH:mm:ss'); return values[0]; } //一次配列から重複を排除する function uniqueArray(ary){ var res = []; res = ary.filter(function(value, index, self){ return self.indexOf(value) === index; }); return res; } //ソート昇順 function sorting_asc(a, b){ if(a[1] < b[1]){ return -1; }else if(a[1] > b[1] ){ return 1; }else{ return 0; } } //ソート降順 function sorting_desc(a, b){ if(a[1] > b[1]){ return -1; }else if(a[1] < b[1] ){ return 1; }else{ return 0; } } // 配列の行列入れ替え // 参考:https://qiita.com/kznr_luk/items/790f1b154d1b6d4de398 // const transpose = a => a[0].map((_, c) => a.map(r => r[c])); function arrayTranspose(a) { return a[0].map((_, c) => a.map(r => r[c])); } // splitの結果を返す function getSplit(array, _order) { var tmp = array.split(","); if (_order == -1) { return tmp; } else { return tmp[_order]; } } //itemmasterシートのB列以降のデータを配列に格納する function getItemMasterData() { var spread = SpreadsheetApp.getActiveSpreadsheet() ; var sheet = spread.getSheetByName('itemmaster'); var res = []; for (var i = 1; i < sheet.getLastColumn()+1; i++) { var tmp = getMasterData(i); res.push(tmp); } return res; } //一覧取得(検索にも対応) function getSheetData(_str) { var spread = SpreadsheetApp.getActiveSpreadsheet(); var sheet = spread.getSheetByName('database'); if (_str == '' || _str == null) { // 検索なしの一覧取得 var tmp1 = getNewDataList(); var res = []; for(var i = 0; i < tmp1.length; i++){ res.push(getCellValue(tmp1[i][0], 6)); } } else { // 検索時 var tmp1 = rowSearch(_str); var res = []; for(var i = 0; i < tmp1.length; i++){ res.push(getCellValue(tmp1[i], 6)); } } for (var i = 0; i < res.length; i++) { for (var j = 0; j < res[0].length; j++) { if (Object.prototype.toString.call(res[i][j]) == '[Object Date]') { res[i][j] = formateDate(res[i][j]); } } } return res; } //データ更新(新規も対応) function setUpdData(ary) { var spread = SpreadsheetApp.getActiveSpreadsheet(); var sheet = spread.getSheetByName('database'); var arrayUpd = [ary.length]; var typeIdx = 2; // inputなどのタイプの要素番号 var dataIdx = 4; // データの要素番号 if (ary[0][dataIdx] == "未設定") { // 新規用 arrayUpd[0] = sheet.getLastRow()+1; } else { arrayUpd[0] = ary[0][dataIdx]; // 行番号を格納 } arrayUpd[1] = getDateOfTokyo(); // タイムスタンプをセット // 更新データだけの配列を作成しながら、checkboxのデータは配列から文字列に変換する for (i=2; i < ary.length; i++) { if (ary[i][typeIdx] == "checkbox") { arrayUpd[i] = ary[i][dataIdx].join(','); } else { arrayUpd[i] = ary[i][dataIdx]; } } var tmp = []; tmp.push(arrayUpd); dataSave(sheet,arrayUpd[0], 1, 1, arrayUpd.length,tmp); return true; } // 今日の日付 var getDateOfTokyo = function() { var date = new Date(); return Utilities.formatDate( date, 'Asia/Tokyo', 'yyyy/MM/dd HH:mm:ss'); } //日付フォーマット var formateDate = function(d) { var addZero = function(n) { return (n < 10)? "0" + String(n):String(n); } var year = String(d.getFullYear()); var month = addZero(d.getMonth() + 1); var date = addZero(d.getDate()); var hour = addZero(d.getHours()); var minite = addZero(d.getMinutes()); var second = addZero(d.getSeconds()); return year + "/" + month + "/" + date + " " + hour + ":" + minite + ":" + second; };vu_js.html
<script> var vue_example = new Vue({ el: '#vue_example', data:{ listtitles:[['初', '期', '表', '示', '中', 'です']], listitems: [['初', '期', '表', '示', '中', 'です']], dispDetails: false, showContent: false, searchword: '', refitem:[], initOptions:[['初期値']], //詳細画面のデータ }, methods:{ // データプロパティ initData: function(ary){ this.listitems = ary; }, // 詳細データの更新 refData: function(ary){ this.refitem = []; this.refitem = ary; this.refitem.push('dummy'); this.refitem.pop(); //データ更新 vue_example.$forceUpdate(); this.$nextTick(); Vue.nextTick(); }, initTitles: function(ary){ this.listtitles = ary; }, // 検索文字列からデータ検索しデータ表示 search_word: function(){ //リスト取得 google.script.run.withSuccessHandler(this.initData) .withFailureHandler(function(arg){ alert("データの初期取得に失敗しました。"); }).getSheetData(this.searchword); //リストの項目名称6個を取得 google.script.run.withSuccessHandler(this.initTitles) .withFailureHandler(function(arg){ alert("リスト項目名の初期取得に失敗しました。"); }).getItemNameList(6); }, // 検索文字列のクリア search_word_clea: function(){ this.searchword = ''; }, // モーダル表示のプロパティ変更 openModal: function(){ this.dispDetails = true; this.showContent = true; }, // モーダル表示のプロパティ変更 closeModal: function(){ this.dispDetails = false; this.showContent = false; }, // プロパティinitOptionsのデータ更新 initOption: function(ary){ this.initOptions = []; this.initOptions = ary; this.initOptions.push('dummy'); this.initOptions.pop(); //データ更新 vue_example.$forceUpdate(); this.$nextTick(); }, // モーダル画面を表示し、データ取得(上記メソッドの呼び出し、引数になる) ref_item: function(_idx){ this.dispDetails = true; this.showContent = true; google.script.run.withSuccessHandler(this.initOption) .withFailureHandler(function(arg){ alert("リスト項目名の取得に失敗しました。"); }).getItemMasterData(); google.script.run.withSuccessHandler(this.refData) .withFailureHandler(function(arg){ alert("データの取得に失敗しました。"); }).getItemNameAndData(-1, this.listitems[_idx][0], -1); }, // 更新画面(モーダル)でのデータ更新後、モーダルOFF upd_item: function(){ google.script.run.withSuccessHandler(this.refitem) .withFailureHandler(function(arg){ alert("データの更新に失敗しました。"); }).setUpdData(this.refitem); // モーダル画面を閉じる this.dispDetails = false; this.showContent = false; }, // 追加画面表示 add_disp: function(){ // モーダル表示ON this.dispDetails = true; this.showContent = true; google.script.run.withSuccessHandler(this.initOption) .withFailureHandler(function(arg){ alert("リスト項目名の取得に失敗しました。"); }).getItemMasterData(); google.script.run.withSuccessHandler(this.refData) .withFailureHandler(function(arg){ alert("データの取得に失敗しました。"); }).getItemNameAndData(-1, 0, -1); }, // データ一覧取得 list_items: function(){ google.script.run.withSuccessHandler(this.initData) .withFailureHandler(function(arg){ alert("データの取得に失敗しました。"); }).getSheetData(); }, }, // 初期表示 created: function() {//リスト取得 google.script.run.withSuccessHandler(this.initData) .withFailureHandler(function(arg){ alert("データの初期取得に失敗しました。"); }).getSheetData(); //リストの項目名称6個を取得 google.script.run.withSuccessHandler(this.initTitles) .withFailureHandler(function(arg){ alert("リスト項目名の初期取得に失敗しました。"); }).getItemNameList(6); } }); </script>vue_index.html
<!DOCTYPE html> <html> <head> <meta http-equiv="Pragma" content="no-cache"> <meta http-equiv="Cache-Control" content="no-cache"> <meta http-equiv="Expires" content="0"> <base target="_top"> <?!= include('css'); ?> <script src="https://unpkg.com/vue"></script> </head> <body> Vueで作成したページです <div id="vue_example"> <input type=text name=searchword v-model="searchword" /> <button v-on:click="search_word()" class='btn-radius-blue'> 検索 </button> <button v-on:click="search_word_clea()" class='btn-radius-blue'> 検索クリア </button> <button v-on:click="add_disp()" class='btn-radius-blue'> 新規登録 </button><br><br> <table> <tr v-for="(titles,idx) in listtitles"> <th v-for="title in titles"> {{ title }} </th> <th>ボタン</th> </tr> <tr v-for="(it,idx) in listitems"> <td v-for="tmp in it">{{ tmp }}</td> <td><button v-on:click="ref_item( idx )" class='btn-radius-blue'>詳細表示</button></td> </tr> </table> <!-- ----以下はモーダル表示-------------------------------------------------------------------- --> <div id="modal" v-show="showContent" class="overlay"> <div id="modal_content" v-if="dispDetails" class="content"> <table class="modal_table"> <tr v-for="(refits,edit_Number) in refitem"> <th>{{ refits[0] }}</th> <!-- いったんすべてを表示したいならここをインする <th>{{ refits[0] }}:{{ refits[1] }}:{{ refits[2] }}:{{ refits[3] }}:</th> <td><input v-model="refits[4]" size=refits[3] maxlength=refits[3] /></td> --> <td v-if="refits[2] == 'read'">{{ refits[4] }}</td> <td v-if="refits[2] == 'text'"><input :id="refits[1]" :name="refits[1]" type=text v-model="refits[4]" size=”refits[3]” maxlength=”refits[3]” /></td> <td v-if="refits[2] == 'textarea'"> <textarea :id="refits[1]" v-model="refits[4]" rows=getSplit(refits[5],0) cols=getSplit(refits[5],1) placeholder="入力して下さい"> </textarea> </td> <td v-if="refits[2] == 'select'"> <select :id="refits[1]" :name="refits[1]" v-model="refits[4]" > <option v-for="(items,idx) in initOptions[refits[3]-1]" v-bind:value="items.item">{{ items.item }}</option> </select> </td> <td v-if="refits[2] == 'radio'"> <div v-for="(items,idx) in initOptions[refits[3]-1]" > <input :id="refits[1]" :name="refits[1]" :key="items.id" type=radio v-model="refits[4]" v-bind:value="items.item" :checked="refits[4] == items.item" />{{ items.item }}<br> <!-- 初期のチェックがないらないので、v-modelとv-bind:valueを併記する --> </div> </td> <td v-if="refits[2] == 'checkbox'"> <div v-for="(items,idx) in initOptions[refits[3]-1]"> <input :id="refits[1]" :name="refits[1]" :key="items.id" type=checkbox v-model="refits[4]" v-bind:value="items.item" :checked="refits[4].indexOf(items.item) > -1" />{{ items.item }}<br> <!-- 初期のチェックがないらないので、v-modelとv-bind:valueを併記する --> </div> </td> </tr> <!-- trのfor文終了 --> <tr> <td></td> <td><button v-on:click="upd_item" class='btn-radius-orange'> 更新 </button> <button v-on:click="closeModal" class='btn-radius-blue'>閉じる</button></td> </tr> </table> </div> </div> <!-- モーダル表示終了 -----------------------------------------------------> </div> <?!= include('vue_js'); ?> </body> </html>css.html
<style> body{ font-family:Verdana,Arial; font-size:14px; text-align: center; } div{ text-align:center; margin-left:auto; margin-right:auto; text-align:left; width: 90%; } h2{ font-size:14px; border-left:5px solid #ccc; padding:3px 0 3px 10px; margin-bottom:10px; } h3{ border-bottom:1px solid #ccc; padding:3px 0; margin-bottom:10px; } table{ width: 100%; <!-- border-collspase: collapse; --> border-collapse:separate; border-spacing: 0; } .modal_table{ width: 80%; <!-- border-collspase: collapse; --> border-collapse:separate; border-spacing: 0; } table th{ text-align: center; color:white; background: linear-gradient(#829ebc,#225588); border-left: 1px solid #3c6690; border-top: 1px solid #3c6690; border-bottom: 1px solid #3c6690; box-shadow: 0px 1px 1px rgba(255,255,255,0.3) inset; padding: 10px 10px; } table td{ text-align: center; border-left: 1px solid #a8b7c5; border-bottom: 1px solid #a8b7c5; border-top:none; padding: 5px 10px; } table tr:nth-child(odd){ background-color: #eee } table td:last-child{ border-right: 1px solid #a8b7c5; } .btn-radius-orange { display: inline-block; padding: 7px 20px; border-radius: 10px; text-decoration: none; color: #FFF; background-image: linear-gradient(45deg, #FFC107 0%, #ff8b5f 100%); transition: .4s; } .btn-radius-orange:hover { background-image: linear-gradient(45deg, #FFC107 0%, #f76a35 100%); } .btn-radius-blue { display: inline-block; padding: 7px 20px; border-radius: 10px; text-decoration: none; color: #FFF; background-image: linear-gradient(45deg, #1F436E 0%, #1C6ECD 100%); transition: .4s; } .btn-radius-blue:hover { background-image: linear-gradient(45deg, #1F436E 0%, #0000BB 100%); } input[type="text"] ,input[type="email"] { width: 30em; } select { //width:250px; } textarea { width: 30em; height: 100px; } /* #overlay{ */ .overlay{ /* 要素を重ねた時の順番 */ z-index:1; /* 画面全体を覆う設定 */ position:fixed; top:0; left:0; width:100%; height:100%; background-color:rgba(0,0,0,0.5); /* 画面の中央に要素を表示させる設定 */ display: flex; align-items: center; justify-content: center; } /* #content{ */ .content{ z-index:2; overflow-y: scroll; width:80%; height:80%; padding: 1em; background:#fff; /* 画面の中央に要素を表示させる設定 */ display: flex; justify-content: center; } </style>感想
ここまで読んで下さりありがとうございました。
表示に使用するHTMLは1ファイルで100行未満。更新も追加もこれだけ。
Vueファイルも冗長っぽい感じがあるものの135行。
コード.gsは、doPostがなくなったものの、必要な関数を追加し変わらずの320行。変にゴリゴリ書く感じもそんなにないし、これだけのコードで済むのだからやはりスゴイのかなVue。
なんだかんだで当初目標にしていた、「表示項目を増やしてもコードの編集が無いように」を達成できたので大変満足です。
項目を増やすたびにコードいじってたら手離れよくないですもんねぇ。ほんと、誰かの助けにでもなれば幸いです。
ありがとうございました。m(_'_)m
- 投稿日:2020-05-19T14:42:11+09:00
Vue.js の transition-group が効かない。
注意点 1. key には繰り返す対象から直接持ってくる。
<!-- OK: key には繰り返す対象から直接持ってくる。 --> <transition-group> <template v-for="element of array"> <div :key="element.property"> {{ element.value }} </div> </template> </transition-group><!-- NG: key には v-for の index を使わない --> <transition-group> <template v-for="(element, index) of array"> <div :key="index">{{ element.value }}</div> </template> </transition-group>注意点 2. 移動中に移動している要素のプロパティを変更しない。
値をいれると瞬間移動します。
// OK function onClick () { // NG: 移動する前に変更するというのはダメでした。 // this.array.map(element => 2* element.value) this.array = _.shuffle(array) // setTimeout で逃します。 setTimeout(() => { this.array.map(element => 2* element.value) }, 2000) }// NG: 瞬間移動します。 function onClick () { this.array = _.shuffle(array) this.array.map(element => 2* element.value) }
- 投稿日:2020-05-19T13:56:15+09:00
スプレッドシートをDB代わりにGASのWebアプリを作成しデータ更新させてみた。Vue版 その3~~
スプレッドシートをDB代わりにGASのWebアプリを作成しデータ更新させてみた。Vue版 その2~~からの続きです。
今回は更新処理を行います。
とはいっても難しいことはありません。
まずは、以下の更新ボタンをクリックします。vue_index.html<button v-on:click="upd_item" class='btn-radius-orange'> 更新 </button>「v-on:click」で指定されている「upd_item」が呼ばれます。
Vue側の以下の「upd_item」が呼ばれます。
vue_js.htmlmethods:{ //の中の・・・ // 更新画面(モーダル)でのデータ更新後、モーダルOFF upd_item: function(){ google.script.run.withSuccessHandler(this.refitem) .withFailureHandler(function(arg){ alert("データの更新に失敗しました。"); }).setUpdData(this.refitem); // モーダル画面を閉じる this.dispDetails = false; this.showContent = false; },データ更新も「google.script.run.」を使い、「コード.gs」の「setUpdData」を「this.refitem」をパラメータに呼び出します。
更新失敗時にはアラートを出すようにしています。
成功時にも一応「this.refitem」を入れてはいますが、画面遷移上、モーダルは消えて、次の詳細を表示しようとする際には新しいデータが入ってくるので特に問題なしです。「コード.gs」の「setUpdData」は以下のようになっており、更新と新規登録の両方に対応させました。新規登録のモーダルは説明していませんが、更新とほぼ同じです。データがないのでプロパティに「未設定」としたり、データを空にしたり、空の配列を作ったりしたものを表示しているだけです。
「setUpdData」では、更新時はスプレッドシートの行番号をそのままつかますが、新規登録時はmaxにプラス1して行番号を指定しています。違いはそれだけですね。スプレッドシートなのでお決まりの、どのシートかを設定します。
そして、更新データだけの配列を用意します。
指定した「this.refitem」の中には余計なものがたくさん入っていますからね。コード.gs//データ更新(新規も対応) function setUpdData(ary) { var spread = SpreadsheetApp.getActiveSpreadsheet(); var sheet = spread.getSheetByName('database'); var arrayUpd = [ary.length]; var typeIdx = 2; // inputなどのタイプの要素番号 var dataIdx = 4; // データの要素番号 if (ary[0][dataIdx] == "未設定") { // 新規用 arrayUpd[0] = sheet.getLastRow()+1; } else { arrayUpd[0] = ary[0][dataIdx]; // 行番号を格納 } arrayUpd[1] = getDateOfTokyo(); // タイムスタンプをセット // 更新データだけの配列を作成しながら、checkboxのデータは配列から文字列に変換する for (i=2; i < ary.length; i++) { if (ary[i][typeIdx] == "checkbox") { arrayUpd[i] = ary[i][dataIdx].join(','); } else { arrayUpd[i] = ary[i][dataIdx]; } } var tmp = []; tmp.push(arrayUpd); dataSave(sheet,arrayUpd[0], 1, 1, arrayUpd.length,tmp); return true; }更新データの1つ目はスプレッドシートの行番号、2つ目はタイムスタンプ。この2つは、このシリーズではお決まり(固定)とさせていただいてます。
「// 更新データだけの配列を作成しながら、checkboxのデータは配列から文字列に変換する」以降でデータを取得し、配列に入れています。
そして「checkbox」の時だけ、もう1処理しています。
それは、例えば、「駐車場」と「駐輪場」にチェックが入っていた場合、
プロパティデータ上は、[駐車場,駐輪場]という配列になっていますので、
文字列として、、、、「駐車場,駐輪場」の形にして保存することにしています。
(そのため、データを表示する際は、「駐車場,駐輪場」の文字列から[駐車場,駐輪場]の配列に(変換しています。)
そして、「var tmp = [];」で更にpushしていますが、これは、
スプレッドシートにデータを反映するには(1行しか更新しなくても)表形式的な感じで更新するので、2次元配列にする必要があります。そのため「tmp」を用意し、pushしています。ここから更に「dataSave」に飛ばしています。
ここではバッティングしないように見よう見まねで実装しています。
全ソース公開時に見ていただければと思います。あとは、モーダルを閉じて、一覧画面に戻ります。
一覧画面に戻った時にリフレッシュしたかったのですが、できず・・・
検索ボックスに何もいれずに「検索」ボタンを押せばリフレッシュされます。次は、ソースを公開していったん終了ですね。
ではまた。
追記::
完結させましたぁ~
スプレッドシートをDB代わりにGASのWebアプリを作成しデータ更新させてみた。Vue版 その4[完結編]~~
- 投稿日:2020-05-19T13:21:22+09:00
vue.js 画像をリサイズしてアップロード
画像をリサイズしてアップロード。
javascript側で 画像を多少加工してPHPに送るとexif情報を失うので、
あとでPHPで対応することができない。
よって先にexifを元にjsで加工しておく必要がある。参考
https://qiita.com/su_mi1228/items/f8f729a29f0b980f7e32ボタンのデザインの変更
https://qiita.com/yasumodev/items/c9f8e8f588ded6b179c9インストール
npm install --save blueimp-load-imageあとはコピペで動く
<template> <div> <input type="file" accept=".jpeg,.jpg,.png" @change="attachImg"> <img :src="resizedImg"> </div> </template> <script> import loadImage from 'blueimp-load-image'; export default { data() { return { resizedImg: null }; }, methods: { attachImg(e) { const file = e.target.files[0]; loadImage.parseMetaData(file, (data) => { const options = { maxHeight: 512, maxWidth: 512, canvas: true }; if (data.exif) { options.orientation = data.exif.get('Orientation'); } this.displayImage(file, options); }); }, displayImage(file, options) { loadImage( file, async (canvas) => { const data = canvas.toDataURL(file.type); console.log(data); // data_url形式をblob objectに変換 const blob = this.base64ToBlob(data, file.type); // objectのURLを生成 const url = window.URL.createObjectURL(blob); this.resizedImg = url; // resizedImgはdataで定義 }, options ); }, base64ToBlob(base64, fileType) { const bin = atob(base64.replace(/^.*,/, '')); const buffer = new Uint8Array(bin.length); for (let i = 0; i < bin.length; i++) { buffer[i] = bin.charCodeAt(i); } return new Blob([buffer.buffer], { type: fileType ? fileType : 'image/png' }); } } } </script>
- 投稿日:2020-05-19T09:43:15+09:00
スプレッドシートをDB代わりにGASのWebアプリを作成しデータ更新させてみた。Vue版 その2~~
スプレッドシートをDB代わりにGASのWebアプリを作成しデータ更新させてみた。Vue版 その1~~からの続きです。
ハマったところはこちら↓
スプレッドシートをDB代わりにGASのWebアプリを作成しデータ更新させてみた。Vue版、ハマったところinputの再描画編~~スプレッドシートをDB代わりにGASのWebアプリを作成しデータ更新させてみた。Vue版、ハマったところcheckboxの再描画編~~
前回は初期表示まででした。
今回は一覧表示されたデータから詳細データをモーダルで表示し更新させます。
以下のように一覧の右にある「詳細表示」ボタンをクリックするところからです。クリックされるとこんな感じで詳細画面を表示します。
この「詳細ボタン」はクリックしたときに「v-on:click」で「ref_item( idx )」が呼び出されます。
「idx」はfor文のインデックスです。vue_index.html<td><button v-on:click="ref_item( idx )" class='btn-radius-blue'>詳細表示</button></td>「ref_item」の説明の前に、詳細画面がモーダルで表示されるので、そのお話を少し。
モーダルで表示する箇所の冒頭を以下に記します。vue_index.html<div id="modal" v-show="showContent" class="overlay"> <div id="modal_content" v-if="dispDetails" class="content">divタグを2つ用意しました。そして1つ目には「v-show」を、2つ目には「v-if」を設定しています。
「v-show」は表示するかしないかの設定です。設定されたプロパティがtrueなら表示するし、falseなら表示しない。となります。
「v-if」は、設定されたプロパティがtrueなら内側のタグの処理をします。falseなら処理しない、ふっとばすことになります。
モーダル表示自体は1つ目のタグのclassで指定しています。
なんでそうなるはわかっていませんが、以下のようにCSSを設定しています。css.html/* #overlay{ */ .overlay{ /* 要素を重ねた時の順番 */ z-index:1; /* 画面全体を覆う設定 */ position:fixed; top:0; left:0; width:100%; height:100%; background-color:rgba(0,0,0,0.5); /* 画面の中央に要素を表示させる設定 */ display: flex; align-items: center; justify-content: center; }「ref_item」の話に戻りまして、vue_index.htmlの中で以下のように記載されています。
はじめに上記のプロパティ2つをtrueにしています。
こうすることで「v-show」で表示するようになり、
「v-if」は内側のタグを表示するようになります。
(※閉じる処理の時は2つともfalseにしています。)
ここで「v-show」だけでもいいんじゃないの?と思われるかもしれませんが、試行錯誤の結果、「v-if」も記載し、明示的に「v-if」のtrue、falseを切り替えることで「v-if」を記載したよりも内側のタグの表示が正しく表示されるようになりました。詳しくはハマったところを見てやってください。vue_js.html// モーダル画面を表示し、データ取得(上記メソッドの呼び出し、引数になる) ref_item: function(_idx){ this.dispDetails = true; this.showContent = true; // マスターデータ取得 google.script.run.withSuccessHandler(this.initOption) .withFailureHandler(function(arg){ alert("リスト項目名の取得に失敗しました。"); }).getItemMasterData(); // 詳細データ取得 google.script.run.withSuccessHandler(this.refData) .withFailureHandler(function(arg){ alert("データの取得に失敗しました。"); }).getItemNameAndData(-1, this.listitems[_idx][0], -1); },もう少し↑の説明を。
ここでもやはり「コード.gs」のfunctionを実行するにあたり「google.script.run」を使用しています。
取得に失敗したら、「.withFailureHandler」に入りアラートで「失敗しました」の旨が表示されます。
成功したら、「.withSuccessHandler」に入り、パラメータ指定されたメソッドが呼び出されます。
「メソッドが呼び出される」のであって、ここには戻り値は書かれていません。
それぞれ「initOption」「refData」メソッドが呼び出され、それぞれのパラメータに「コード.gs」のfunction「getItemMasterData」や「getItemNameAndData」の戻り値が入ります。
そのメソッドは下記になります。
そして、メソッド内で各パラメータに設定しています。vue_js.htmlmethods:{ // データプロパティ ↓ここに戻り値がくる initData: function(ary){ this.listitems = ary; }, // 詳細データの更新 ↓ここに戻り値がくる refData: function(ary){ this.refitem = []; this.refitem = ary; this.refitem.push('dummy'); this.refitem.pop(); //データ更新 vue_example.$forceUpdate(); this.$nextTick(); Vue.nextTick(); },「refData」では、再描画がうまくいかなかったときのナゴリがありますw
配列データは、pushやpopを使わないとVueが気付いてくれないというので"dummy"を入れて削除しました。
forceUpdate()やnextTick()で強制的に更新をかけることもしました。
きっと、そういうこともあるのではないかと残しておきます。
これでようやくマスターデータと詳細データの取得ができたので表示に取り掛かりましょうこちら↓になります。
vue_index.html<table class="modal_table"> <tr v-for="(refits,edit_Number) in refitem"> <th>{{ refits[0] }}</th> <td v-if="refits[2] == 'read'">{{ refits[4] }}</td> <td v-if="refits[2] == 'text'"><input :id="refits[1]" :name="refits[1]" type=text v-model="refits[4]" size=”refits[3]” maxlength=”refits[3]” /></td>リスト表示では1データを1行で左から右に表示してました。
詳細画面では、1データは上から下に表示するようになりますので、配列の並び(データの持ち方)は、リスト表示の時とは、タテ、ヨコを入れ替えた形で保持させています。
(※コツというほどでもないですが、その方がtableのfor文で回す時 楽なので。)「refitem」を回して、取得した「refits」も配列になっています。
「refits」の要素番号2がどのような表示をさせたいのかを保持しています。
そしてそれを「v-if」で場合分けしていきます。
今はとりあえず、「read」と「text」を見ていきましょう。
「read」と「text」の設定はデータシートの3行目のものになります。「read」は読ませるだけなので、特にタグもなく表示しています。
「text」はinputタグを使用し、
「:id」で「refits[1]」を、これはデータシートの2行目で設定している値になります。
「:name」でも同様です。 「:」はバインドの省略形になります。
typeはtextですね。ここには「:」はありません。
「v-model」として「refits[4]」を指定します。これによりデータが画面に表示されます。
「size」、「maxlength」共に「refits[3]」を指定しています。これはデータシートの4行目の値ですね。次はtextarea です。
設定はほぼ変わりませんが、「rows」「cols」の設定として、やはりデータシートの4行目の値を持ってくるのですが、設定値を「5,30」としています。5行30文字の設定を表しています。
「コード.gs」に「getSplit」関数を用意し、指定した要素番号の数値を返すようにしています。
こんな風に関数を呼べるんですね。vue_index.html<td v-if="refits[2] == 'textarea'"> <textarea :id="refits[1]" v-model="refits[4]" rows=getSplit(refits[5],0) cols=getSplit(refits[5],1) placeholder="入力して下さい"> </textarea> </td>続きましてselectです。
selectやradio,checkboxは選んでもらう項目が必要ですので、マスターデータシートに記載します。
データシートの4行目は、この3タイプに限りマスターデータシート列番号を記載します。
このような形でデータとマスターデータをリンク付けしてみました。
selectタグは変わりありませんが、
optionタグをマスターデータが入っている「initOptions」で回します。
この時、マスターデータのデータは連想配列になっていることに注意してくださいね。
「initOptions」で回して「items」となったデータを「v-bind:value」で「items.item」を指定しています。vue_index.html<td v-if="refits[2] == 'select'"> <select :id="refits[1]" :name="refits[1]" v-model="refits[4]" > <option v-for="(items,idx) in initOptions[refits[3]-1]" v-bind:value="items.item">{{ items.item }}</option> </select> </td>もう少しです。がんばりましょう。
radioです。
「v-for」をdivタグ内で記述していますがtemplateタグでもいいかと思います。
見栄えの関係でdivを選びました。
今までと異なる点は「:key」です。「items.id」を指定しています。「id」です。
また、初期表示で入っているデータにはチェックをつけたいので「:checked」で「refits[4] == items.item」を入れて判定させています。マスターデータと詳細データが合致していればtrueとなりチェックが入ります。
「v-model」を指定すると、「v-bind:value」にも設定されるそうなのですが、
「v-model」は「refits[4]」
「v-bind:value」は「items.item」と異なるプロパティを設定することにより
期待する動きを得ています。vue_index.html<td v-if="refits[2] == 'radio'"> <div v-for="(items,idx) in initOptions[refits[3]-1]" > <input :id="refits[1]" :name="refits[1]" :key="items.id" type=radio v-model="refits[4]" v-bind:value="items.item" :checked="refits[4] == items.item" />{{ items.item }}<br> <!-- 初期のチェックがないらないので、v-modelとv-bind:valueを併記する --> </div> </td>次のcheckboxで詳細表示はいったん終わります。ガンバ!
radioボタンとあまり変わりはありません。異なる点は
その1:データは配列で持つこと。
そうしないとデータが取れずに、true、falseでしか更新できなくなってしまいます。
その2:radioボタンと違って、複数の項目にチェックができますので、
「:checked」を「refits[4].indexOf(items.item) > -1」で判定しています。vue_index.html<td v-if="refits[2] == 'checkbox'"> <div v-for="(items,idx) in initOptions[refits[3]-1]"> <input :id="refits[1]" :name="refits[1]" :key="items.id" type=checkbox v-model="refits[4]" v-bind:value="items.item" :checked="refits[4].indexOf(items.item) > -1" />{{ items.item }}<br> <!-- 初期のチェックがないらないので、v-modelとv-bind:valueを併記する --> </div> </td>後は、更新ボタンと閉じるボタンだけですね。
更新はまた次回にします。
閉じるボタンは、「v-on:click」で「closeModal」が呼ばれてプロパティがfalseに設定され、モーダルが閉じます。vue_index.html<td><button v-on:click="upd_item" class='btn-radius-orange'> 更新 </button> <button v-on:click="closeModal" class='btn-radius-blue'>閉じる</button></td> </tr> </table> </div> </div> <!-- モーダル表示終了 ----------------------------------------------------->vue_js.html// モーダル表示のプロパティ変更 closeModal: function(){ this.dispDetails = false; this.showContent = false; },ではまた。
追記::
スプレッドシートをDB代わりにGASのWebアプリを作成しデータ更新させてみた。Vue版 その3~~で更新してみました。
- 投稿日:2020-05-19T09:37:15+09:00
Go + Vue.js でGETとPOSTをやってみる
はじめに
この記事はGo lang 駆け出しによる駆け出し向けの記事になっていますので基本的なことしか書いておりません。
今回はGo言語とVueを使ってPOSTとGETでリクエストを投げてパラメータの受け渡しをやってみる。
なおGoのフレームワークとしてEchoを使っていく。今回のコードはgithubに(最下部掲載)
Vue.js前準備
まずはvueのプロジェクト作成
次にaxiosを入れる。リクエストはaxiosを使って送る。$vue create ~~ $cd ~~ $npm install --save axios vue-axiosmain.go側
作成したプロジェクトの中にmain.goファイルを作る
今回はechoを使っていくのでフレームワークを入れる
$go get -u github.com/labstack/echo大体の流れ↓
CORSを忘れずに。
main.gopackage main import ( "net/http" "github.com/labstack/echo" "github.com/labstack/echo/middleware" ) func main(){ e := echo.New() //CORSの設定(vueのプロジェクトをGOで立てたlocalサーバーで起動する時は不要) e.Use(middleware.CORS()) // リクエストに対するHandler e.GET("/getTitle", getTitle) e.GET("/getName/:name", getName) e.POST("/postName", postName) e.POST("/postCompany", postCompany) // local サーバー e.Logger.Fatal(e.Start(":8000")) }各Handler定義
main.go// GETリクエスト func getTitle(c echo.Context) error { return c.String(http.StatusOK, "New Game") } // パラメータ付きのGETリクエスト func getName(c echo.Context) error { name := c.Param("name") return c.String(http.StatusOK, name) } // application/x-www-form-urlencoded データのPOSTリクエスト func postName(c echo.Context) error { name := c.FormValue("name") return c.String(http.StatusOK, name) } //JSON受け取り用の構造体 type JsonParam struct { Company string `json:"company"` Works string `json:"works"` } // JSONデータのPOSTリクエスト func postCompany(c echo.Context) error { param := new(JsonParam) //バインドしてJSON取得 if err := c.Bind(param); err != nil { return err } //JSONを返す return c.JSON(http.StatusOK, param) }GETリクエストではurlに
/~~/:パラメータ名
とし、Context.Param(パラメータ名)
とすることでURLに付加されたのパラメータを取得できる。POSTでデータを取得する際にデータがform-urlencodedなのかJSONなのかで取得の仕方が変わってくる。
form-urlencodedの場合はContext.FormValue(キー)
で取れる。
JSONの場合は先に**JSONの構造体を用意しておいてContext.Bind(構造体)
で取れる。これでバックエンド側の準備は完了
次にフロントのvue側を作っていく。$go run main.go今回はポート8000でローカルサーバーを起動させておく
Vue.js側
適当にビューを作作成
App.vue<template> <div id="app"> <button @click="sendRequest">リクエスト送信</button> <h1>取得結果</h1> <p>GET(パラメータ無):<br/><strong>{{title}}</strong></p> <p>GET(パラメータ有):<br/><strong>{{name1}}</strong></p> <p>POST(form-urlencoded):<br/><strong>{{name2}}</strong></p> <p>POST(JSONデータ):<br/><strong>{{company}}</strong></p> </div> </template> <script> //~~省略~~ data:()=>{ return{ title:"", name1:"", name2:"", company:"" } } </script>次にリクエストを送る関数を作成していく
App.vue<script> import axios from "axios" //~~省略~~ methods: { sendRequest: async function(){ //パラメータ無しでGETリクエスト const getRequestNoParam = await axios.get("http://localhost:8000/getTitle") //パラメータ付きでGETリクエスト const getRequest = await axios.get("http://localhost:8000/getName/ひふみん") //application/x-www-form-urlencodedでデータを送信 const params = new URLSearchParams(); params.append("name","青葉"); const postRequest = await axios.post("http://localhost:8000/postName",params) //JSONデータを送信(axiosはデフォルトでJSONを送信) const jsonPostRequest = await axios.post("http://localhost:8000/postCompany", { company: "Eagle Jump", works: "PECO" }); //取得結果をviewに反映 this.title = getRequestNoParam.data this.name1 = getRequest.data this.name2 = postRequest.data this.company = jsonPostRequest.data } } </script>axiosはPOSTリクエストの時デフォルトでJSONを送信するようになっているのでform-urlencodedを使いたいときは
URLSearchParams
APIを使う。これで完成!!
npm run serve
使ってvueのローカルサーバー(localhost:8080)で起動させればリクエストの送受信ができるはず。VueプロジェクトをGoのローカルサーバーで動かす
上の場合はvueプロジェクトを
npm run serve
使って、vueのローカルサーバー(localhost:8080)で動かして、Goはlocalhost:8000で動かしていた。これだとクロスドメインでCORSの設定が必要になる。そこで、最後におまけ的な感じでvueプロジェクトをGoで起動したローカルサーバーで動かしてみる。
まずはvueプロジェクトをビルドする。
$npm run buildうまく実行できるとdistフォルダができてるはず。
あとは、これをGoで動かすだけ。main.goを以下のように修正
main.gofunc main(){ e := echo.New() //CORSの設定(vueのプロジェクトをGOで立てたlocalサーバーで起動する時は不要) // e.Use(middleware.CORS()) // npm run buildでビルドしたものをgoで起動 corsも不要になる // /でアクセスしたときのルーティング設定 e.Static("/", "dist/") // リクエストに対するHandler e.GET("/getTitle", getTitle) e.GET("/getName/:name", getName) e.POST("/postName", postName) e.POST("/postCompany", postCompany) // local サーバー e.Logger.Fatal(e.Start(":8000")) }当然だけどVue.js側のリクエストを送るときも以下のように省略できる。
App.vue//パラメータ無しでGETリクエスト - const getRequestNoParam = await axios.get("http://localhost:8000/getTitle") + const getRequestNoParam = await axios.get("/getTitle")あとはサーバーを起動して
$go run main.golocalhost:8000にアクセスすれば別々に動かしていた時と同じように使える。
おわり
GoとVueを使って簡単にAPIのやり取りができた!
Goって結構たのしいな。
AWSとかで動かしてみたい...今回のコードGitHubからどうぞ
参考
@y_ussie 様 : Go言語のWebフレームワーク「Echo」を使ってみる ②(リクエストパラメータの扱い)
@567000 様 : Go(golang) echoとVue-cliをつなげる
ありがとうございました。
- 投稿日:2020-05-19T00:45:03+09:00
SkyWayとNuxt.jsでWebRTCのグループビデオ通話機能開発
株式会社OneSmallStepの西(@_takeshi_24)です!
最近新型コロナの影響で在宅ワークも増え、それに伴い、ビデオチャットやTV会議システムも色々と登場してきています。
オンラインのビデオ通話支える仕組みがWebRTCなのですが、今回はこのWebRTCを簡単に実装できるSkyWayについてご紹介します!WebRTCとは?
WebRTCとはHTMLのAPIの一種で、ブラウザ間で映像や音声などの大容量のデータをリアルタイムに送受信するための技術です。
従来のWebRTCはコンピュータ同士を直接P2Pでつなぐメッシュ方式でした。
メッシュ方式の場合、複数のコンピュータ同士で映像や音声を配信すると、端末が増えるにつれて回線や端末に大きな負荷がかかります。
同時に接続できる端末数に上限があります。WebRTC SFU
WebRTC SFU (Selective Forwarding Unit ) は、メッシュ方式と異なり、音声や映像をサーバ経由で配信します。
接続するコンピュータが増えても、端末自体にかかる負荷は少なく、複数端末で同時に接続することが可能です。SkyWay
SkyWayとはWebRTCを用いたビデオ通話や音声通話を簡単に実装できるSDK、APIサービスです。
導入事例を見るとレアジョブ英会話など様々なサービスで利用されているようです。しかも驚くべきことに、月当たり50万接続までは無料で使えると言う料金設定!
桁間違えてるんじゃないの?笑WebRTCを使った音声・ビデオ通話のためにクライアントサイドに必要なSDKもちゃんと準備されています。
これらを使うことで、ブラウザアプリはもちろん、iOS、Androidなどのスマホ、IoT機器からも簡単に利用することができます。
Nuxt.js/TypeScriptでSkyWayを利用
今回は、Nuxt.js(TypeScript)を使ったビデオ通話Webアプリの開発について説明します。
なお、Nuxt.jsについてはこの記事では紹介しませんので、Nuxt.jsの環境は事前にご用意してください。
開発
SkyWayAPIキーの取得
1.SkyWayを利用するために、まずは何はともあれ、SkyWayのアカウント登録をします。
https://console-webrtc-free.ecl.ntt.com/users/registration2.アカウント登録後、「新しくアプリケーションを追加する」からアプリケーションを追加します。
3.アプリケーション名、利用可能なドメイン名などを入力し、「アプリケーションを作成する」をクリックします。
SDKのインストール
1.Nuxt.jsのプロジェクトにJavaScriptのSDKをインストールします。
yarn add skyway-js
グループビデオ通話機能の開発
Interfaceの定義を作成します。
types/interface.tsexport interface SkywayMediaStream extends MediaStream { peerId: string }Componentを作成します。
SkywayVideo.vue<template> <div class="skyway-video"> <video id="local-stream"></video> <div> <button @click="mute">{{ muteText }}</button> <button @click="disconnect">切断</button> </div> <div id="remote-streams" class="remote-streams"> <div v-for="remoteStream in remoteStreams" :ref="remoteStream.peerId" :key="remoteStream.peerId" > <video autoplay :srcObject.prop="remoteStream"></video> </div> </div> </div> </template> <script lang="ts"> import Vue from 'vue' import Peer, { SfuRoom } from 'skyway-js' import { SkywayMediaStream } from '@/types/interface.ts' interface SkywayData { peer: Peer | null room: SfuRoom | null localStream: MediaStream | undefined isMute: boolean remoteStreams: SkywayMediaStream[] } export default Vue.extend({ name: 'SkywayVideo', props: { userName: { type: String, default: null }, roomName: { type: String, default: null } }, data: (): SkywayData => ({ peer: name, room: null, localStream: undefined, isMute: false, remoteStreams: [] }), computed: { muteText(): string { return this.isMute ? 'ミュート解除' : 'ミュート' } }, async mounted() { const localVideo = document.getElementById( 'local-stream' ) as HTMLMediaElement this.localStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true }) localVideo.muted = true localVideo.srcObject = this.localStream await localVideo.play() this.peer = await new Peer(this.userName, { key: process.env.SKYWAY_API_KEY || '', debug: 3 }) this.peer.on('open', this.connect) }, methods: { // 接続処理 connect() { if (!this.peer || !this.peer.open) { return } this.room = this.peer.joinRoom(this.roomName, { mode: 'sfu', stream: this.localStream }) as SfuRoom if (this.room) { this.room.on('stream', (stream: SkywayMediaStream): void => { this.remoteStreams.push(stream) }) this.room.on('peerLeave', (peerId: string): void => { const audio = document.getElementById(peerId) if (audio) { audio.remove() } }) } }, // ミュート切り替え mute(): void { if (this.localStream) { const audioTrack = this.localStream.getAudioTracks()[0] this.isMute = !this.isMute audioTrack.enabled = !this.isMute } }, // 切断 disconnect(): void { if (this.room) { this.room.close() } } } }) </script> <style lang="scss" scoped> // .skyway-video </style>コンポーネントを読み込むページは以下のような感じです。
index.vue<template> <skyway-video :room-name="roomName" :user-name="userName"></skyway-video> </template> <script lang="ts"> import Vue from 'vue' import SkywayVideo from '@/components/molecules/SkywayVideo/SkywayVideo.vue' export default Vue.extend({ components: { SkywayVideo }, data: () => ({ roomName: 'sample' }), computed: { userName(): string { return this.makeRandum(10) } }, methods: { makeRandum(retLength: number): string { // 生成する文字列に含める文字セット const chars: string = 'abcdefghijklmnopqrstuvwxyz0123456789' const charsLength: number = chars.length const ret: string[] = [...Array(retLength)].map((): string => { return chars[Math.floor(Math.random() * charsLength)] }) return ret.join('') } } }) </script> <style lang="scss" scoped> // .skyway-video </style>ポイントを解説。
- SkywayVideo.vueの
local-stream
内に自分の映像が埋め込まれます。- SkywayVideo.vueの以下の箇所で、ユーザーの接続をトリガーに、remoteStreamsにMediaStreamが追加されます。
this.room.on('stream', (stream: SkywayMediaStream): void => { this.remoteStreams.push(stream) })
- userNameとroomNameをPropsで親コンポーネントから受け取っています。
- userNameが接続のIDになります。
- roomNameはグループ通話の部屋名になり、roomNameが同じユーザーとグループ通話が可能です。
- SkywayVideo.vueの
mounted()
のなかで、ローカルのビデオを表示し、SkyWayに接続します。
- 音がハウリングしないように、
localVideo.muted = true
で、自分自身のVideoは常にミュートにしています。- SKYWAY_API_KEYは、SkyWayで取得したAPIキーを環境変数に設定したものです。
- SkywayVideo.vueの
mute()
は、自分の音声をミュートにしています。- SkywayVideo.vueの
disconnect()
は切断処理です。SkyWayの公式ドキュメントに詳細はありますので、そちらを参考にしてください。
SkyWayで話し中のユーザーにマークをつけたい
ビデオ会議システムでよくあるのが話しているユーザーのビデオにマークをつける処理です。
こちらはvoice-activity-detectionというライブラリで実現可能です。
https://github.com/Jam3/voice-activity-detectionまずは、voice-activity-detectionをインストール
yarn add voice-activity-detection
pluginsにvad.tsを以下の内容で作成します。
plugins/vad.tsimport Vue from 'vue' import { SkywayMediaStream } from '@/types/interface.ts' const vad = require('voice-activity-detection') Vue.mixin({ methods: { startVoiceDetection( this: any, stream: SkywayMediaStream, talkUpdate: (peerId: string | null) => void ) { const audioContext = new AudioContext() const vadOptions = { onVoiceStart() { talkUpdate(stream.peerId) }, onVoiceStop() { talkUpdate(null) } } // streamオブジェクトの音声検出を開始 this.vadobject = vad(audioContext, stream, vadOptions) }, stopVoiceDetection(this: any) { if (this.vadobject) { // 音声検出を終了する this.vadobject.destroy() } } } })nuxt.config.tsで、pluginを読み込みます。
nuxt.config.tsplugins: [{ src: '~/plugins/vad.ts', ssr: false }],SkywayVideo.vueに処理を追加します。追加箇所のコメントを参照。
SkywayVideo.vue<template> <div class="skyway-video"> <video id="local-stream"></video> <div> <button @click="mute">{{ muteText }}</button> <button @click="disconnect">切断</button> </div> <div id="remote-streams" class="remote-streams"> <!-- ↓追加箇所:classを追加 --> <div v-for="remoteStream in remoteStreams" :ref="remoteStream.peerId" :key="remoteStream.peerId" :class="talkingId === remoteStream.peerId ? 'talking' : ''" > <video autoplay :srcObject.prop="remoteStream"></video> </div> </div> </div> </template> <script lang="ts"> import Vue from 'vue' import Peer, { SfuRoom } from 'skyway-js' import { SkywayMediaStream } from '@/types/interface.ts' interface SkywayData { peer: Peer | null room: SfuRoom | null localStream: MediaStream | undefined isMute: boolean remoteStreams: SkywayMediaStream[] // ↓追加箇所 talkingId: string | null // ↑追加箇所 } export default Vue.extend({ name: 'SkywayVideo', props: { userName: { type: String, default: null }, roomName: { type: String, default: null } }, data: (): SkywayData => ({ peer: name, room: null, localStream: undefined, isMute: false, remoteStreams: [], // ↓追加箇所 talkingId: null // ↑追加箇所 }), computed: { muteText(): string { return this.isMute ? 'ミュート解除' : 'ミュート' } }, async mounted() { const localVideo = document.getElementById( 'local-stream' ) as HTMLMediaElement this.localStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true }) localVideo.muted = true localVideo.srcObject = this.localStream await localVideo.play() this.peer = await new Peer(this.userName, { key: process.env.SKYWAY_API_KEY || '', debug: 3 }) this.peer.on('open', this.connect) }, methods: { // 接続処理 connect() { if (!this.peer || !this.peer.open) { return } this.room = this.peer.joinRoom(this.roomName, { mode: 'sfu', stream: this.localStream }) as SfuRoom if (this.room) { // ↓追加箇所 this.room.on('stream', (stream: SkywayMediaStream): void => { ;(this as any).startVoiceDetection(stream, this.talkUpdate) this.remoteStreams.push(stream) }) // ↑追加箇所 this.room.on('peerLeave', (peerId: string): void => { const audio = document.getElementById(peerId) if (audio) { audio.remove() } }) } }, // ミュート切り替え mute(): void { if (this.localStream) { const audioTrack = this.localStream.getAudioTracks()[0] this.isMute = !this.isMute audioTrack.enabled = !this.isMute } }, // 切断 disconnect(): void { if (this.room) { this.room.close() } }, // ↓追加箇所 talkUpdate(peerId: string) { this.talkingId = peerId } // ↑追加箇所 } }) </script> <style lang="scss" scoped> // .skyway-video // ↓追加箇所 .talking { border: 3px solid #0000ff; } // ↑追加箇所 </style>plugins/vad.tsの
startVoiceDetection
で、MediaStreamの音声を検出しています。
SkywayVideo.vueで、話し中のユーザーのpeerIdをtalkingIdにセットしています。
remoteStreamsのpeerIdとtalkingIdが一致したvideo
タグにtalking
のclassを指定して、スタイルを適用させています。まとめ
話し中のユーザーにマークをつける処理は苦戦しましたが、それ以外はSkyWayのおかげで簡単にWebRTCのテレビ電話処理を作成する事ができました!
今回はテレビ電話でしたが、音声通話だけなら、videoタグじゃなくて、audioタグで可能です。いつもNuxt.jsのこととか、Firebaseのことを中心につぶやいていますので、Twitter(@_takeshi_24)のフォローよろしくお願いします!
- 投稿日:2020-05-19T00:12:39+09:00
Rails + Vue.js でページネーション付きのテーブルを簡単作成
概要
- 業務でページネーション機能を実装したので、ほとんどそのままの構成で手順を紹介
- 使用した技術は
kaminari(Rails)
とVuetify(Vue.js)
- api経由でデータを取得し、ページネーション付きで表示する
- ソースコード
Railsの開発環境は特に説明しないが、以下の記事を参考に構築しました
Rails 6 + MySQL on Dockerの環境を秒速で構築するBookモデルの定義とサンプルデータの作成
今回はデータベースに保存したBookの一覧をapiで取得します
まずはBookモデルを作成しましょう# マイグレーションファイルの作成 bin/rails g model Book name:string # マイグレーションを実行し、Booksテーブルを作成 bin/rails db:migrate
db/seeds.rb
を編集しサンプルデータを作成db/seeds.rb100.times do |n| name = "example-#{n+1}" Book.create!(name: name) endseedを実行
bin/rails db:seedこれでBookレコードが100件作成されました
kaminariのインストールとBook一覧取得用apiの作成
Bookテーブルからデータを取得する際にkaminariを使用します
kaminariをインストールします
https://github.com/kaminari/kaminariGemfile# kaminariを追記 gem 'kaminari'# kaminariのインストール。インストール完了後にサーバーを再起動させましょう bundle
app/controllers/api/books_controller.rb
を作成し、Book一覧を返すapiを実装しますapp/controllers/api/books_controller.rbclass Api::BooksController < ApplicationController def index # 表示するページの番号を指定 page = params[:page] || 1 # 1ページあたりの表示件数を指定 per = params[:per] || 10 # ページネーションで指定レコードを取得 books = Book.page(page).per(per) # ページネーションした時の全ページ数 total_pages = books.total_pages # レスポンスデータの定義 response = { # bookレコードはidとnameフィールドのみ表示する books: books.select(:id, :name), total_pages: total_pages } # json形式でレスポンスを返却 render json: response end endconfig/routes.rbRails.application.routes.draw do # Book一覧取得用のパス get '/api/books', to: 'api/books#index' # Book一覧表示用のパス get '/books', to: 'books#index' end
http://localhost:3000/api/books
にアクセスすると次のようなjsonが返ってきますBook一覧表示ページの作成
Book一覧表示用のページを作成します
Vuetifyのv-data-tableコンポーネント
とv-pagination
を使い、Axiosでapiを叩きます
※ ここでは面倒を避けるためCDN経由で環境構築をしてあります。適宜ご自身の環境に合わせた環境構築を行なってくださいapp/views/layouts/application.html.erb<!DOCTYPE html> <html> <head> <title>AppName</title> <%= csrf_meta_tags %> <%= csp_meta_tag %> <%# CDNで Vue.js, Vuetify, Axios をインストールする %> <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"> <link href="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.min.css" rel="stylesheet"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui"> <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %> </head> <%# ここも追加 %> <script src="https://cdn.jsdelivr.net/npm/vue@2.x/dist/vue.js"></script> <script src="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.js"></script> <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/es6-promise@4/dist/es6-promise.auto.min.js"></script> <body> <%= yield %> </body> </html>app/views/books/index.html.erb<div id="app"> <v-app> <v-container> <h2>Book一覧</h2> <%# テーブル作成用コンポーネント %> <v-data-table :headers="headers" :items="items" :items-per-page="itemsPerPage" hide-default-footer /> </v-container> <%# ページネーション表示用コンポーネント %> <v-pagination v-model="currentPage" :length="totalPages" <%# ページを変更した時にfetchBooksを呼び出す %> @input="fetchBooks" /> </v-ap> </div> <script> new Vue({ el: "#app", vuetify: new Vuetify(), data() { return { // テーブルのヘッダー情報。valueの値がレコードのフィールド名に紐付く headers: [ { text: "ID", value: "id"}, { text: "本の名前", value: "name"}, ], // テーブルのボディー情報。apiで取得したBook一覧をここに格納する items: [], // 表示するページの番号 currentPage: 1, // 1ページあたりの表示件数 itemsPerPage: 10, // ページネーションした時の全ページ数 totalPages: null, } }, methods: { // AxiosでBook取得apiにリクエストを送る fetchBooks() { const url = `/api/books?page=${this.currentPage}?per=${this.itemsPerPage}`; axios .get(url) .then(res => { // Book一覧を取得 this.items = res.data.books; // ページネーションした時の全ページ数を取得 this.totalPages = res.data.total_pages; }) } }, // DOMが作成された時に fetchBooks を呼び出す created() { this.fetchBooks() }, }); </script>
http://localhost:3000/books
これでページネーションは完成です
以下のようにページを切り替える度に表示が変わればOKです!