20210607のvue.jsに関する記事は11件です。

【Vue.js】v-modelの修飾子の種類とその使いどころ

はじめに 仕事で使う事になったので1からVue.jsについて学んだ。 ちゃんと覚えておかないとまずそうな事を備忘録として1つ1つ残しておく。 v-modelの修飾子の種類とその使いどころ v-modelにはいくつか便利な修飾子があるが、その使いどころも含めて整理する。 何も修飾子がない場合 以下の動画のように即時に入力内容が反映される(双方向データバインディングになる)。 動画のソースコードは以下。 vue.App.vue <template> <div class="container-sm"> <!-- 省略 --> <div> <h2>イベントのフォーム</h2> <div class="mb-3"> <label for="title" class="form-label">タイトル</label> <input type="text" class="form-control" id="title" v-model="eventData.title" /> <p>{{ eventData.title }}</p> </div> </div> </div> </template> ソースコード全体は以下。 .lazy修飾子 どうなるのか? DOMイベントのchangeが発火した時にmodel(v-modelで指定したデータ)に内容が反映されるようになる。 動画のソースコードは以下。 vue.App.vue <template> <div class="container-sm"> <!-- 省略 --> <div> <h2>イベントのフォーム</h2> <div class="mb-3"> <label for="title" class="form-label">タイトル</label> <input type="text" class="form-control" id="title" v-model.lazy="eventData.title" /> <p>{{ eventData.title }}</p> </div> </div> </div> </template> <script> // 省略 export default { data() { return { // 省略 eventData: { title: "タイトル", }, }; }, // 省略 ソースコード全体は以下。 ※ちなみに、DOMイベントのchangeは、 タグで入力してフォーカスを外した時 タグで入力してEnterキーを押下した時 といった時に発火する。 使いどころ バリデーションなどで入力時に逐一走らせるよりは入力後に走らせたい時など。 その場合にこの.lazyを使う事で、DOMイベントのchangeが発火してmodleが反映されたらバリデーションを走らせるといった事ができるようになる。 .number修飾子 どうなるのか? 基本的に<input type="number">と指定したとしても以下のようにtypeofを出力するとstringになってしまうが、 .number修飾子をv-modelに付与するとnumberとして扱うようにできる。 動画のソースコードは以下。 vue.App.vue <template> <div class="container-sm"> <!-- 省略 --> <div> <h2>イベントのフォーム</h2> <div class="mb-3"> <label for="maxNumber" class="form-label">最大人数</label> <input type="number" class="form-control" id="maxNumber" v-model.number="eventData.maxNumber" /> <p>{{ typeof eventData.maxNumber }}</p> </div> </div> </div> </template> <script> // 省略 export default { data() { return { // 省略 eventData: { title: "タイトル", maxNumber: 0, }, }; }, // 省略 ソースコード全体は以下。 使いどころ v-modelでデータバインディングしている変数の型を厳格にnumberにしたい時など。 .trim修飾子 どうなるのか? 先頭・最後尾の空白を削除してv-modelの変数にデータバインディングしてくれる。 動画のソースコードは以下。 App.vue <template> <div class="container-sm"> <!-- 省略 --> <div> <h2>イベントのフォーム</h2> <div class="mb-3"> <label for="host" class="form-label">主催者</label> <input type="text" class="form-control" id="host" v-model.trim="eventData.host" /> <pre>{{ eventData.host }}</pre> </div> </div> </div> </template> <script> // 省略 export default { data() { return { // 省略 eventData: { host: "", }, }; }, // 省略 ソースコード全体は以下。 使いどころ 先頭・最後尾の空白を削除してv-modelの変数にデータバインディングしたい時など。 Vue.jsの勉強メモ一覧記事へのリンク Vue.jsについて勉強した際に書いた勉強メモ記事のリンクを集約した記事。 https://qiita.com/yuta-katayama-23/items/dabefb59d16a83f1a1d4
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[rollup]vuejsのカスタムコンポーネントを外部スクリプトファイルとして提供する

Vuejs自作コンポーネントをライブラリ化 やりたいこと Vuejsのデータ構造と、カスタムビューコンポーネントを 外部ライブラリ化することで、メインの画面構造を laravel、nodejs/dot、golang/ginのtemplate経由で 容易に作れるようにしたい 背景 SPA(Vuejs-cli)での画面構成を行うと、微妙な部分での構築がうまくいかない Vuejs-CDNでは、部分的に適用するのが容易だが、データ構造等はある程度固めておきたい なので、storeなどのデータ構造や、viewツール等は外部コンポーネントのライブラリとしてSPAで構築するが、画面構造は CDN形式で作りたかった 何に悩んでいたのか? 単純に勉強不足だったのだが、Vue-CDN等でライブラリとして分割している方法を調べていたので、その結果をここに記す 一般的なSPAの構造 こんな感じでmain.js内に Vue の実体をいれるのが多く説明されている index.html <html> <head> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>> </head> <body> <div id="app"> </div> <script src="main.js"></script> </body> </html> main.js import Vue from 'vue' import App from './App' new Vue({ el: '#app', components: { App }, template: '<App/>' }) やりたいこと VueJSのCDNと同じように、コンポーネントをライブラリ化して 以下のように使いたい index.html <html> <head> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script src="app.js"></script> </head> <body> <div id="app"> </div> </body> <script> new Vue({ el: '#app', components: { App }, }) </script> </html> app.js import App from 'app.vue' export default { name: "App", template: `<div><a>Hello World!</a></div>` } 解決させた結果 rollupを使ってのライブラリ化 javascript自体は、全然勉強していなかったので esm等全然しらないというか、ぶっちゃけ今もよくわからないのですが、いろいろと調べた結果、rollup を利用することで実装が出来たので、それらを備忘録の意味も込めてここに記します ※webpack等への連携は、この後頑張りますが、大枠で出来たので記載するかは微妙です 所定の環境を構築 各ソースを展開し、環境構築を行うこと 環境構築 $ mkdir testrollup $ cd testrollup $ npm install $ npm run build  を実行することで、dist/配下にpackage.jsonで--fileに定義したライブラリファイルが生成される 作り方の説明 実コードは、以降のサンプルコードを参照していただければと思いますので、ポイントのみを抽出して説明します 結局、わかりたかったのは、scriptタグから独自ライブラリを指定させる方法でした その方法の大事なところは以下のとおりです pollup.config.js でoutput.name を指定する esm形式で、名前を指定することはできるが、ESModuleではスコープをグローバルにするのは本末転倒かと。 sample <script type="module"> import PARENT, { MyComponet } from 'parent.esm.js'; console.log(PARENT); </script> じゃあ、VueJSなどはどうしているのかと調べたら... 普通に rollup で制御してくれていました rollup.config.jsで、output.nameを定義すると設定した形式で読み込まれてくれています dist/index.html <html> <head> <script src="https://unpkg.com/vuex@3.1.1/dist/vuex.js"></script> <script src="https://unpkg.com/vue/dist/vue.js"></script> <script src="./parent.min.js"></script> ← ここで取込むと、PARENT としてアクセス可能となる </head> <body> ..snip.. </body> <script> console.log(PARENT); ..snip.. </script> </html> spa化させる際のexport の構造に注意する script でhtml上で展開した際のdefaultの名前を定義する src/entry.js import MyComponent from './mycomponent.vue'; export default MyComponent export { MyComponent } と定義したものを読込ませると、以下の実装すると dist/index.html <html> ..snip.. <script> new Vue({ el: '#app', components: { 'mycomponent': MyComponent }, data() { return { value: "My Component" }; } }); </script> </html> 以下のようなエラーが出てしまう error vue.js:634 [Vue warn]: Failed to mount component: template or render function not defined. found in ---> <Mycomponent> <Root> 実体を見ると、以下の通りの構造をしているので当たり前である scriptで取込んだオブジェクトの実体 + Object MyComponent: {name: "MyComponent", template: "\n <div>\n <input\n type=\"text\"\n v-mode…>\n <br />\n <a>{{ message }}</a>\n </div>\n ", props: {…}, __file: "src/mycomponent.vue", data: ƒ} default: MyComponent: {name: "MyComponent", template: "\n <div>\n <input\n type=\"text\"\n v-mode…>\n <br />\n <a>{{ message }}</a>\n </div>\n ", props: {…}, __file: "src/mycomponent.vue", data: ƒ} よって、以下のような実装が必要となる dist/index.html <html> ..snip.. <script> new Vue({ el: '#app', components: { 'mycomponent': PARENT.default }, or components: { 'mycomponent': PARENT.MyComponent }, ..snip.. }); </script> </html> これで何ができるようになるか? ※以下はコンパイル等していないので動くかは不明ですw 実際にやりたいこと <html> <head> <script src="https://unpkg.com/vuex@3.1.1/dist/vuex.js"></script> <script src="https://unpkg.com/vue/dist/vue.js"></script> <script src="./exstore.min.js"></script> ← exports.nameをExStore <script src="./exview.min.js"></script> ← exports.nameをExView <script src="./exsidebar.min.js"></script> ← exports.nameをExSideBar </head> <body> <div id="app"> <mybutton eventhandler="onFlipFlot"></mybutton> <mycomponent value="hello world"></mycomponent> <mysidemenu :top="bTop"></mysidemenu> </div> </body> <script> new Vue({ el: '#app', ExStore.Store, ← vuejs/store をライブラリ化 components: { ← components をライブラリ化 'mycomponent': ExView.MyComponent, 'mybutton': ExView.MyButton, 'mysidemenu': ExSideBar.SideMenu, }, data() { return { value: "My Component", bTop: true }; }, methods: { onFlipFlot: function() { this.bTop = !this.bTop } } }); </script> </html> 終わり.. [参考] サンプルコード tree testrollup │ package.json │ rollup.config.js │ ├─dist │ index.html │ parent.esm.js [生成ファイル] │ parent.esm.js.map [生成ファイル] │ parent.min.js [生成ファイル] │ parent.min.js.map [生成ファイル] │ parent.umd.js [生成ファイル] │ parent.umd.js.map [生成ファイル] │ └─src entry.js mycomponent.vue package.json { "name": "testrollup", "version": "1.0.0", "description": "", "scripts": { "build": "npm run build:umd & npm run build:es & npm run build:unpkg", "build:umd": "rollup --config rollup.config.js --format umd --file dist/parent.umd.js", "build:es": "rollup --config rollup.config.js --format es --file dist/parent.esm.js", "build:unpkg": "rollup --config rollup.config.js --format iife --file dist/parent.min.js" }, "author": "", "license": "ISC", "devDependencies": { "@rollup/plugin-buble": "^0.21.3", "@rollup/plugin-commonjs": "^19.0.0", "@vue/compiler-sfc": "^3.0.11", "rollup": "^2.51.0", "rollup-plugin-vue": "^6.0.0", "vue": "^2.6.13" } } rollup.config.js import commonjs from "@rollup/plugin-commonjs"; import vue from "rollup-plugin-vue"; import buble from "@rollup/plugin-buble"; const src_path = './src'; export default { input: src_path + '/entry.js', output: { name: "PARENT", ← script でhtml上で展開した際のdefaultの名前を定義する exports: "named", sourcemap: true, }, plugins: [ vue({ css: true, // css を <style> タグとして注入 compileTemplate: true, // 明示的にテンプレートを描画関数に変換 }), commonjs(), buble() ], } dist/index.html <html> <head> <script src="https://unpkg.com/vuex@3.1.1/dist/vuex.js"></script> <script src="https://unpkg.com/vue/dist/vue.js"></script> <script src="./parent.min.js"></script> </head> <body> <div id="app"> <mycomponent value="hello world"></mycomponent> </div> </body> <script> console.log(PARENT); new Vue({ el: '#app', components: { 'mycomponent': PARENT.MyComponent }, data() { return { value: "My Component" }; } }); </script> </html> src/entry.js import MyComponent from './mycomponent.vue'; const PARENT= { MyComponent }; export default PARENT export { MyComponent } src/mycomponent.vue <script> export default { name: 'MyComponent', template : ` <div> <input type="text" v-model="message" /> <br /> <a>{{ message }}</a> </div> `, props: { value: { type: String, default: "", }, }, data: function() { return { message: this.value } } } </script>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Nuxt.jsでGoogleBooksAPIを使用して検索結果を表示してみた

1.はじめに 2.内容 3.おわりに 1. はじめに GoogleBooksAPIとaxiosを使用して書籍検索システムを作りました。 画像とタイトルを表示させたいと思います。 2. 内容 axiosでGoogleBooksAPIからデータを取得 GoogleBooksAPIから情報を取得するたにaxiosでリクエストを送ります。 data () { return { books: [], keyword: '', url: 'https://www.googleapis.com/books/v1/volumes?q=' } } URLとユーザーが検索できるkeywordはdataで取り扱います。空のbook配列には取得した書籍データを格納します。 <v-text-field v-model="keyword" /> keywordはtemplateのテキストと結び助けておくことで検索機能として動くようにしました。 methods: { get () { this.$axios.$get(this.url + this.keyword + '&maxResults=15') .then(this.setBooks) }, setBooks (res) { this.books = res.data.items } keywordを入力した後に検索ボタンを押すとgetメソッドが発火して書籍情報を取得→bookに格納というように動いてくれます。 <v-card v-for="book in books" :key="book.id" class="ma-0" > <v-row> <v-cols class="my-4 ml-7"> <img :src="volumeInfo.imageLinks.thumbnail"> </v-cols> <v-cols> <v-card-title>{{ book.volumeInfo.title }}</v-card-title> <v-card-text>{{ book.volumeInfo.authors) }}</v-card-text> </v-cols> </v-row> </v-card> items以降のデータを取り出してfor文で表示させたら完成です 問題が見つかる 問題が見つかりました。タイトルや著者情報がない時や、サムネイル画像がないとundefinedでエラーになってしまいます。特に画像のない本が結構あったので対策は必須です。 解決策① v-ifを使って条件分岐で情報を切り替える これは真っ先に思いついたのですが、だいぶ冗長になってしまうのと、そもそも公式で非推奨のやり方になってるのでやめました。 解決策② methodsで条件分岐してくれるオブジェクトを作成 スマートだしかっこいい気がする!ということでこちらを採用! title (valu) { return valu.volumeInfo.title ? valu.volumeInfo.title : 'No title' }, authors (valu) { return valu.volumeInfo.authors ? valu.volumeInfo.authors : 'No authors' }, image (valu) { return valu.volumeInfo.imageLinks ? valu.volumeInfo.imageLinks.thumbnail : noImage } <v-cols class="my-4 ml-7"> <img :src="image(book)"> </v-cols> <v-cols> <v-card-title>{{ title(book) }}</v-card-title> <v-card-text>{{ authors(book) }}</v-card-text> </v-cols> 引数をとって処理を分けることでかっこよくなりました。 こういうのってなんか気持ちいですよね〜 5. おわりに これでなんとか検索結果を表示させることはできました。初めての実装でしたが オブジェクトを用意して条件分岐をするというのは非常に勉強になりました。 課題〜誰か助けて〜 今度はオブジェクトとfor文を使って表示させた画像、タイトル、著者をクリックするとすぐ下に表示されるという機能を追加したいです。やり方を考えているのですが、なかなか見つけられないのと、思いつかなくて困っています。誰か教えてください泣 ①検索リスト表示 ←今回のとこ ②ユーザークリック ③クリックした情報がすぐ下に表示される  ← ここがわからない ④表示されたものを保存する という流れを想定しています。 とりあえず根気強く調べてみようと思います。ではまた!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

chromaticでUIのレビューをしてみる

はじめに 前回は、vuetifyを入れたvueプロジェクトのstorybookをchromaticにホスティングするまでの手順をまとめました。 次は、chromaticを使用してレビューや差分の見方などをまとめます。 環境 node:v12.18.2 npm:6.14.5 yarn:1.22.4 vue/cli:4.5.4 npx:6.14.5 プロジェクトの修正 ブランチの作成と切り替え GitHubを使用する時は、branchを作成して修正・マージを行うのが一般的だと思います。 そのため、まずはbranchを作成して、切り替えます。 git branch feature/add_button_to_mypage git checkout feature/add_button_to_mypage ソースの修正 普通にソースの修正をします。ここでは、差分がわかりやすいようにページにボタンを追加します。 diff --git a/src/views/MyPage.vue b/src/views/MyPage.vue index ecc6939..a9955d9 100644 --- a/src/views/MyPage.vue +++ b/src/views/MyPage.vue <v-col> <simple-button type="primary" text="プライマリーボタン"/> </v-col> + <v-col> + <simple-button color="error" text="太字ボタン" bold/> + </v-col> <v-col> <simple-button type="inversion" color="success" text="反転色付きボタン"/> </v-col> ソースのコミットとpush ここも普通にソースのコミットとpushを行います。 git add * git commit -m "ページにボタン追加" git push origin feature/add_button_to_mypage chromaticへの反映 chromaticへpush chromaticへのpushをしないと修正した内容がchromaticへ反映されないため、pushします。 前回の手順通りに行っているとyarnコマンドに追加されているはずなのでyarnコマンドでpushします。 コマンドを実行すると差分があるため、失敗します。 yarn chromatic 実行結果 × Found 1 visual change: failing with exit code 1 Pass --exit-zero-on-changes to succeed this command regardless of changes. Pass --auto-accept-changes to succeed and automatically accept any changes. i Review the changes at レビューURL error Command failed with exit code 1. info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command. 修正の反映チェック chromaticに反映させるためには、差分が意図した修正であったかをチェックする必要があります。 上のコンソールで表示されたレビューURLをブラウザで開くと以下の画面が表示されます。 それぞれの差分がTESTSに出ているため、Changesのコンポーネント(例だとMyPage)をクリックして内容を確認します。 クリック後は写真のように差分がWeb画面で見れます。 左が修正前で右が修正後になります。右の修正した箇所に色付きで差分が表示されます。 ※サンプルが悪くて気持ち悪い色になっちゃいました。 右のDiffボタンを押すと純粋な画面を見ることもできます。 意図した修正ならばAcceptを選択すると、Acceptedになります。 これでchromaticへの反映が終わりました。 chromaticでのレビュー chromatic on GitHubのインストール GitHubで作成したプルリクエストがchromaticにも反映されるようになるために Chromatic on GitHubをインストールします。 左のPRsを選択して、表示されるInstall Chromatic on GitHubをクリックします。 既にリポジトリの連携がすんでいる場合は選択済みになっているリポジトリ名を確認します。 Approveをクリックします。 Gitのパスワードを入力します。 GitHubのプルリクエストを作成する GitHubのプルリクエストがメインとなるため、まずGitHubのプルリクエストを作成します。 Pull requestsタグを選択して、New pull requestをクリック compareのところに修正したブランチを選択して、Create pull requestをクリック chromaticでのプルリクエスト GitHubのプルリクエスト作成後、1~2分後にchromaticのPRsの画面上にもGitHubで作成したプルリクエストが表示されています。 プルリクエストを選択すると詳細画面になます。 Activityタグは、レビュワーの追加や承認した人と時間のログの表示などこのプルリクエストに対して行われた操作やステータスが表示されます。 Componentsタグは作成していたstorybookが表示されます。 Changesetは修正の反映チェックで見たものと同じような画面の差分が表示されます。画面差分に対してコメントを残すこともできます。 これらを確認してOKであれば右のApproveボタンを押して承認をするとActivityタブの画面に承認されたことが記録されます。 マージ マージはGitHub側で行う必要があるため、View PR on GitHubを選択してGitHub側のプルリクエストに飛びます。GitHubのプルリクエスト上でマージします。 Tips chromaticへのpushの自動化 公式に書いているようにGitHub Actionsを使用するとわざわざyarnコマンドでpushする必要がなくなります。 終わりに chromaticにホスティングした内容をレビューするまでの手順をまとめました。開発した物のUIを見ながらレビューできるのはとても良いと思いましたが、ソースはGitHubに移動しないと見えない点が微妙に感じました。 見た目の差分が出るだけで充分レビューしやすい状態になっていますが、修正差分の範囲が大きい場合ほとんどが差分になり逆にソースを見た方が早いというようにならないかが少し不安です。 ですが、スクラムのような小さい単位での開発やデザイナーが積極的に参加するチームならばとても強力なツールになるように思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.js入門 Vol.4 ~外部API呼び出し編~

こんにちは! LIFULLエンジニアの吉永です。 本日も最近あまり関わらなくなったのであまりキャッチアップできていなかったフロントエンド開発技術についてインプットした内容を備忘録として記載していきます。 本記事の概要 Vue.js入門 Vol.1 ~jQueryとの対比編~ Vue.js入門 Vol.2 ~jQueryとの対比編~ Vue.js入門 Vol.3 ~基礎まとめで簡易家計簿を作る編~ 上記3記事の続きで、外部APIを呼び出して、その結果を画面に反映させるアプリケーションを作るまでを解説していきます! 本記事で利用させていただいた外部APIと注意事項について tsukumijimaさんの天気予報 API(livedoor 天気互換)を利用させていただきました。 2020年夏に突然サービスが終了してしまった「livedoor 天気」というサービスの互換APIを提供してくださっています。 この記事をお読みの方で、この記事に沿って動作確認アプリケーションを開発される方は事前に必ず、上記ページの注意事項を良く確認してから本記事の内容に沿って実装をするようにしてください。 ※tsukumijimaさん 便利なAPIを無料で公開していただき誠にありがとうございます。 作成するアプリケーションの仕様 天気予報情報を外部APIから取得して画面に表示させる。 天気予報を取得する都道府県、地域を選択して取得ボタンを押すと外部APIを呼び出して取得する。 天気予報を取得する都道府県をプルダウンメニューから選択したら地域プルダウンメニューの内容を都道府県配下の地域に置き換え、未選択状態にする。 取得ボタンは都道府県、地域が未選択中には押せないようにする。 取得ボタンは外部API呼び出し中はローディング表示を行い、取得が終わるまではボタンを押せないようにする。 外部API呼び出しに失敗した場合はエラーメッセージを画面に表示させる。 画面仕様 初期表示 天気予報表示時 エラー発生時 天気予報取得アプリ~完成版デモ~ See the Pen 天気予報取得アプリ~完成版デモ~ by Yuta Yoshinaga (@yuta-yoshinaga) on CodePen. この後の解説部分では各ソースコードをパーツごとに表記しているので、ソースコードの全文を見たい方は上記codepenへのリンクから参照してください。 天気予報取得アプリ実装 入力フォーム <h1 class="title">天気予報</h1> <div class="columns"> <div class="column"> <label class="label">都道府県</label> <select v-model="curPref" @change="prefChange"> <option v-for="pref in prefs">{{ pref.name }}</option> </select> </div> <div class="column"> <label class="label">地域</label> <select v-model="curCity"> <option v-for="city in citys" :value="city.id">{{ city.name }}</option> </select> </div> <div class="column"> <button :class="btnClass" @click="getWeather" :disabled="canSendBtn">取得</button> </div> </div> 入力フォームではセレクトボックスを二つとボタンを一つ用意します。 ※@clickはv-onディレクティブの省略形の書き方で、:valueはv-bindディレクティブの省略形です。 エラーメッセージ表示部分 <div v-if="hasError"> <article class="message is-danger"> <div class="message-header"> <p>Error</p> </div> <div class="message-body"> {{ errorMessage }} </div> </article> </div> v-ifディレクティブを使ってエラーがあった際にのみ有効になる部分です。 天気予報表示部分 <table class="table is-bordered is-striped" v-if="curWether"> <tr> <td>予報の発表日時</td> <td>{{ curWether.publicTimeFormatted }}</td> </tr> <tr> <td>予報を発表した気象台</td> <td>{{ curWether.publishingOffice }}</td> </tr> <tr> <td>タイトル・見出し</td> <td>{{ curWether.title }}</td> </tr> <tr> <td>リクエストされたデータの地域に該当する気象庁 HP の天気予報の URL</td> <td><a :href="curWether.link">{{ curWether.link }}</a></td> </tr> <tr> <td>天気概況文</td> <td> <table class="table is-bordered is-striped"> <tr> <td>天気概況文の発表時刻</td> <td>{{ curWether.description.publicTimeFormatted }}</td> </tr> <tr> <td>天気概況文</td> <td>{{ curWether.description.text }}</td> </tr> </table> </td> </tr> <tr> <td>都道府県天気予報の予報日毎の配列</td> <td> <table class="table is-bordered is-striped" v-for="forecast in curWether.forecasts"> <tr> <td>予報日</td> <td>{{ forecast.date }}</td> </tr> <tr> <td>予報日(今日・明日・明後日のいずれか)</td> <td>{{ forecast.dateLabel }}</td> </tr> <tr> <td>天気(晴れ、曇り、雨など)</td> <td>{{ forecast.telop }}</td> </tr> <tr> <td>天気詳細</td> <td> <table class="table is-bordered is-striped"> <tr> <td>詳細な天気情報</td> <td>{{ forecast.detail.weather }}</td> </tr> <tr> <td>風の強さ</td> <td>{{ forecast.detail.wind }}</td> </tr> <tr> <td>波の高さ(海に面している地域のみ)</td> <td>{{ forecast.detail.wave }}</td> </tr> </table> </td> </tr> <tr> <td>最高気温</td> <td> <table class="table is-bordered is-striped"> <tr> <td>摂氏 (°C)</td> <td>{{ forecast.temperature.max.celsius }}</td> </tr> <tr> <td>華氏 (°F)</td> <td>{{ forecast.temperature.max.fahrenheit }}</td> </tr> </table> </td> </tr> <tr> <td>最低気温</td> <td> <table class="table is-bordered is-striped"> <tr> <td>摂氏 (°C)</td> <td>{{ forecast.temperature.min.celsius }}</td> </tr> <tr> <td>華氏 (°F)</td> <td>{{ forecast.temperature.min.fahrenheit }}</td> </tr> </table> </td> </tr> <tr> <td>降水確率</td> <td> <table class="table is-bordered is-striped"> <tr> <td>0 時から 6 時までの降水確率</td> <td>{{ forecast.chanceOfRain.T00_06 }}</td> </tr> <tr> <td>6 時から 12 時までの降水確率</td> <td>{{ forecast.chanceOfRain.T06_12 }}</td> </tr> <tr> <td>12 時から 18 時までの降水確率</td> <td>{{ forecast.chanceOfRain.T12_18 }}</td> </tr> <tr> <td>18 時から 24 時までの降水確率</td> <td>{{ forecast.chanceOfRain.T18_24 }}</td> </tr> </table> </td> </tr> <tr> <td>天気アイコン</td> <td> <img :src="forecast.image.url" :alt="forecast.image.title" :width="forecast.image.width" :height="forecast.image.height"> </td> </tr> </table> </td> </tr> <tr> <td>予報を発表した地域を定義</td> <td> <table class="table is-bordered is-striped"> <tr> <td>地方名</td> <td>{{ curWether.location.area }}</td> </tr> <tr> <td>都道府県名</td> <td>{{ curWether.location.prefecture }}</td> </tr> <tr> <td>一次細分区域名</td> <td>{{ curWether.location.district }}</td> </tr> <tr> <td>地域名(気象観測所名)</td> <td>{{ curWether.location.city }}</td> </tr> </table> </td> </tr> <tr> <td>copyright</td> <td> <table class="table is-bordered is-striped"> <tr> <td>コピーライトの文言</td> <td>{{ curWether.copyright.title }}</td> </tr> <tr> <td>天気予報 API(livedoor 天気互換)の URL</td> <td><a :href="curWether.copyright.link">{{ curWether.copyright.link }}</a></td> </tr> <tr> <td>天気予報 API(livedoor 天気互換)のアイコン</td> <td><img :src="curWether.copyright.image.url" :alt="curWether.copyright.image.title" :width="curWether.copyright.image.width" :height="curWether.copyright.image.height"> </td> </tr> <tr> <td>天気予報 API(livedoor 天気互換)で使用している気象データの配信元(気象庁)</td> <td> <table> <tr> <td>link</td> <td><a :href="curWether.copyright.provider[0].link">{{ curWether.copyright.provider[0].link }}</a> </td> </tr> <tr> <td>name</td> <td>{{ curWether.copyright.provider[0].name }}</td> </tr> <tr> <td>note</td> <td>{{ curWether.copyright.provider[0].note }}</td> </tr> </table> </td> </tr> </table> </td> </tr> </table> 天気予報 API(livedoor 天気互換)のレスポンスフィールドの内容を参考に、APIから取得したデータを画面上に表示させるための部分になります。 都道府県データJS const infoTbl = { prefs: [ { name: "北海道", citys: [ { name: "稚内", id: "011000" }, { name: "旭川", id: "012010" }, { name: "留萌", id: "012020" }, { name: "網走", id: "013010" }, { name: "北見", id: "013020" }, { name: "紋別", id: "013030" }, { name: "根室", id: "014010" }, { name: "釧路", id: "014020" }, { name: "帯広", id: "014030" }, { name: "室蘭", id: "015010" }, { name: "浦河", id: "015020" }, { name: "札幌", id: "016010" }, { name: "岩見沢", id: "016020" }, { name: "倶知安", id: "016030" }, { name: "函館", id: "017010" }, { name: "江差", id: "017020" }, ] }, { name: "青森県", citys: [ { name: "青森", id: "020010" }, { name: "むつ", id: "020020" }, { name: "八戸", id: "020030" }, ] }, { name: "岩手県", citys: [ { name: "盛岡", id: "030010" }, { name: "宮古", id: "030020" }, { name: "大船渡", id: "030030" }, ] }, { name: "宮城県", citys: [ { name: "仙台", id: "040010" }, { name: "白石", id: "040020" }, ] }, { name: "秋田県", citys: [ { name: "秋田", id: "050010" }, { name: "横手", id: "050020" }, ] }, { name: "山形県", citys: [ { name: "山形", id: "060010" }, { name: "米沢", id: "060020" }, { name: "酒田", id: "060030" }, { name: "新庄", id: "060040" }, ] }, { name: "福島県", citys: [ { name: "福島", id: "070010" }, { name: "小名浜", id: "070020" }, { name: "若松", id: "070030" }, ] }, { name: "茨城県", citys: [ { name: "水戸", id: "080010" }, { name: "土浦", id: "080020" }, ] }, { name: "栃木県", citys: [ { name: "宇都宮", id: "090010" }, { name: "大田原", id: "090020" }, ] }, { name: "群馬県", citys: [ { name: "前橋", id: "100010" }, { name: "みなかみ", id: "100020" }, ] }, { name: "埼玉県", citys: [ { name: "さいたま", id: "110010" }, { name: "熊谷", id: "110020" }, { name: "秩父", id: "110030" }, ] }, { name: "千葉県", citys: [ { name: "千葉", id: "120010" }, { name: "銚子", id: "120020" }, { name: "館山", id: "120030" }, ] }, { name: "東京都", citys: [ { name: "東京", id: "130010" }, { name: "大島", id: "130020" }, { name: "八丈島", id: "130030" }, { name: "父島", id: "130040" }, ] }, { name: "神奈川県", citys: [ { name: "横浜", id: "140010" }, { name: "小田原", id: "140020" }, ] }, { name: "新潟県", citys: [ { name: "新潟", id: "150010" }, { name: "長岡", id: "150020" }, { name: "高田", id: "150030" }, { name: "相川", id: "150040" }, ] }, { name: "富山県", citys: [ { name: "富山", id: "160010" }, { name: "伏木", id: "160020" }, ] }, { name: "石川県", citys: [ { name: "金沢", id: "170010" }, { name: "輪島", id: "170020" }, ] }, { name: "福井県", citys: [ { name: "福井", id: "180010" }, { name: "敦賀", id: "180020" }, ] }, { name: "山梨県", citys: [ { name: "甲府", id: "190010" }, { name: "河口湖", id: "190020" }, ] }, { name: "長野県", citys: [ { name: "長野", id: "200010" }, { name: "松本", id: "200020" }, { name: "飯田", id: "200030" }, ] }, { name: "岐阜県", citys: [ { name: "岐阜", id: "210010" }, { name: "高山", id: "210020" }, ] }, { name: "静岡県", citys: [ { name: "静岡", id: "220010" }, { name: "網代", id: "220020" }, { name: "三島", id: "220030" }, { name: "浜松", id: "220040" }, ] }, { name: "愛知県", citys: [ { name: "名古屋", id: "230010" }, { name: "豊橋", id: "230020" }, ] }, { name: "三重県", citys: [ { name: "津", id: "240010" }, { name: "尾鷲", id: "240020" }, ] }, { name: "滋賀県", citys: [ { name: "大津", id: "250010" }, { name: "彦根", id: "250020" }, ] }, { name: "京都府", citys: [ { name: "京都", id: "260010" }, { name: "舞鶴", id: "260020" }, ] }, { name: "大阪府", citys: [ { name: "大阪", id: "270000" }, ] }, { name: "兵庫県", citys: [ { name: "神戸", id: "280010" }, { name: "豊岡", id: "280020" }, ] }, { name: "奈良県", citys: [ { name: "奈良", id: "290010" }, { name: "風屋", id: "290020" }, ] }, { name: "和歌山県", citys: [ { name: "和歌山", id: "300010" }, { name: "潮岬", id: "300020" }, ] }, { name: "鳥取県", citys: [ { name: "鳥取", id: "310010" }, { name: "米子", id: "310020" }, ] }, { name: "島根県", citys: [ { name: "松江", id: "320010" }, { name: "浜田", id: "320020" }, { name: "西郷", id: "320030" }, ] }, { name: "岡山県", citys: [ { name: "岡山", id: "330010" }, { name: "津山", id: "330020" }, ] }, { name: "広島県", citys: [ { name: "広島", id: "340010" }, { name: "庄原", id: "340020" }, ] }, { name: "山口県", citys: [ { name: "下関", id: "350010" }, { name: "山口", id: "350020" }, { name: "柳井", id: "350030" }, { name: "萩", id: "350040" }, ] }, { name: "徳島県", citys: [ { name: "徳島", id: "360010" }, { name: "日和佐", id: "360020" }, ] }, { name: "香川県", citys: [ { name: "高松", id: "370000" }, ] }, { name: "愛媛県", citys: [ { name: "松山", id: "380010" }, { name: "新居浜", id: "380020" }, { name: "宇和島", id: "380030" }, ] }, { name: "高知県", citys: [ { name: "高知", id: "390010" }, { name: "室戸岬", id: "390020" }, { name: "清水", id: "390030" }, ] }, { name: "福岡県", citys: [ { name: "福岡", id: "400010" }, { name: "八幡", id: "400020" }, { name: "飯塚", id: "400030" }, { name: "久留米", id: "400040" }, ] }, { name: "佐賀県", citys: [ { name: "佐賀", id: "410010" }, { name: "伊万里", id: "410020" }, ] }, { name: "長崎県", citys: [ { name: "長崎", id: "420010" }, { name: "佐世保", id: "420020" }, { name: "厳原", id: "420030" }, { name: "福江", id: "420040" }, ] }, { name: "熊本県", citys: [ { name: "熊本", id: "430010" }, { name: "阿蘇乙姫", id: "430020" }, { name: "牛深", id: "430030" }, { name: "人吉", id: "430040" }, ] }, { name: "大分県", citys: [ { name: "大分", id: "440010" }, { name: "中津", id: "440020" }, { name: "日田", id: "440030" }, { name: "佐伯", id: "440040" }, ] }, { name: "宮崎県", citys: [ { name: "宮崎", id: "450010" }, { name: "延岡", id: "450020" }, { name: "都城", id: "450030" }, { name: "高千穂", id: "450040" }, ] }, { name: "鹿児島県", citys: [ { name: "鹿児島", id: "460010" }, { name: "鹿屋", id: "460020" }, { name: "種子島", id: "460030" }, { name: "名瀬", id: "460040" }, ] }, { name: "沖縄県", citys: [ { name: "那覇", id: "471010" }, { name: "名護", id: "471020" }, { name: "久米島", id: "471030" }, { name: "南大東", id: "472000" }, { name: "宮古島", id: "473000" }, { name: "石垣島", id: "474010" }, { name: "与那国島", id: "474020" }, ] }, ] }; 本当はこちらも外部から取得したかったのですが、CORSの制限でブラウザJSからは取得できなかったのでひとまずJS上でデータを持つようにしました。 参照させてもらったデータはこちらです。 メイン処理JS const app = new Vue({ el: '#app', data: { prefs: infoTbl.prefs, citys: null, curPref: null, curCity: null, curWether: null, hasError: false, errorMessage: "", loading: false, }, computed: { btnClass: function () { return { button: true, 'is-primary': true, 'is-loading': this.loading }; }, canSendBtn: function () { return this.curPref && this.curCity && !this.loading ? false : true; } }, methods: { prefChange: function () { let pref = this.prefs.filter(pref => pref.name === this.curPref); if (pref.length != 0) { this.citys = pref[0].citys; this.curCity = null; } }, getWeather: function () { this.hasError = false; this.errorMessage = ""; this.loading = true; this.curWether = null; axios.get('https://weather.tsukumijima.net/api/forecast/city/' + this.curCity) .then(function (response) { if (response.data) { if (response.data.error) { this.hasError = true; this.errorMessage = response.data.error; } else { this.curWether = response.data; } } }.bind(this)) .catch(function (error) { this.hasError = true; this.errorMessage = error; }.bind(this)) .finally(function () { this.loading = false; }.bind(this)) } } }) ひとまず全体像です。 各詳細を以降で解説していきます。 computed 算出プロパティと呼ばれるもので、メソッドと違って関数内部で依存している値が変化したら自動的にキャッシュに反映してくれて、変化がなければキャッシュから値を取得するようにできます。 btnClass: function () { return { button: true, 'is-primary': true, 'is-loading': this.loading }; }, こちらの処理は取得ボタンのclass属性を返却する算出プロパティなのですが、this.loadingというプロパティがtrueならis-loadingというクラスが取得ボタンに追加されるようになっています。 このようにしておくことで、取得ボタンを押してから、this.loadingがfalseになるまではボタンの見た目をローディング表示に変えることが可能です。 canSendBtn: function () { return this.curPref && this.curCity && !this.loading ? false : true; } こちらは取得ボタンを押せるようにするかしないかを返却する算出プロパティです。 this.curPrefには都道府県選択プルダウンメニューで現在選択中の値が、this.curCityには地域選択プルダウンメニューで現在選択中の値が入るようになっており、未選択状態であればボタンをdisabledにして押せなくするようにしています。 今回はそれらに加えて、ローディング表示中も同様にボタンを押せなくしており、こうすることでAPIからの呼び出しが完了するまでの間はボタンを押せないので2重送信をできないようにすることができます。 methods prefChange: function () { let pref = this.prefs.filter(pref => pref.name === this.curPref); if (pref.length != 0) { this.citys = pref[0].citys; this.curCity = null; } }, こちらは都道府県選択プルダウンメニューで選択値が変化した際に呼ばれるメソッドです。 this.curPrefには現在選択中の都道府県の名称が入っているので、this.prefsというマスタデータ内を検索して、一致したデータから子要素の地域情報を取得してthis.citysに代入しています。 this.citysはHTML側ではv-forディレクティブで参照されており、内容が変わると自動的にHTML側にも反映されます。 最後にthis.curCityをnullにすることで、地域選択プルダウンメニューを未選択上に初期化しています。 getWeather: function () { this.hasError = false; this.errorMessage = ""; this.loading = true; this.curWether = null; axios.get('https://weather.tsukumijima.net/api/forecast/city/' + this.curCity) .then(function (response) { if (response.data) { if (response.data.error) { this.hasError = true; this.errorMessage = response.data.error; } else { this.curWether = response.data; } } }.bind(this)) .catch(function (error) { this.hasError = true; this.errorMessage = error; }.bind(this)) .finally(function () { this.loading = false; }.bind(this)) } 最後に外部API呼び出し部分です。 このメソッドは取得ボタンクリック時に呼ばれていて、API呼び出し前にまずは各種ステータスや変数を初期化しています。 ここでthis.loadingをtrueにすることで、算出プロパティが更新されて画面にも反映されるようになります。 外部取得用の処理にはaxiosというブラウザやnode.jsで動くPromiseベースの HTTPクライアントを使用しています。 jQueryで良く使用されていた$.ajaxと似たようなものですが、最近ではこちらを利用するのが主流のようです。 通信完了後はエラーがあればcatchへ、通信上ではエラーがなければthenの中の処理が実行されます。 ただし、thenにはHTTPのレスポンスコードが2xx系などの時に来るようになっているので、リクエストパラメーターにエラーがあった際などには外部APIの仕様次第ですが、レスポンスコードは2xx系で返却し、エラーメッセージをレスポンスデータに格納しているパターンもあります。 天気予報 API(livedoor 天気互換)はレスポンスコードは2xx系でもエラーメッセージが格納されていることもあるので、thenの中でもエラーがないかを確認してから取得したデータをVueオブジェクトのプロパティへ代入するようにしています。 この辺りは呼び出し対象のAPIの仕様書を読むか、動作確認を行ってわざとエラーを発生させてみてなどの挙動を確認しておくと良いと思います。 最後に いかがでしたでしょうか? 今回はJSから外部APIを呼び出してVue.jsで利用する実装例をご紹介しました。 jQueryでは重宝していたajaxの代用になるaxiosというライブラリがあることも知れたのと、利用方法はajaxとそんなに変わっていないのですんなりと処理を記述できたことがjQueryでの開発経験が少し役に立ったようで良かったです。 あとは登録不要で簡単に利用できるtsukumijimaさんの天気予報 API(livedoor 天気互換)には本当に感謝です!おかげで学習がはかどりました! 皆さんも外部API呼び出しの学習用やテスト用に使えるAPIなどを是非ご自身で調べてみてください。 ※利用規約などは事前にきちんと確認しましょう! それではまた次の記事でお会いしましょう!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.js vue-star-ratingの導入方法

背景 vue.jsにて星評価を実装する必要があり、備忘録として導入方法を残します。 環境 vue@2.6.12 導入方法 ターミナルにて以下コマンド実行 npm install vue-star-rating --save router/index.jsに以下を記述 index.js import StarRating from 'vue-star-rating' Vue.use(StarRating); Views側の記述方法 ※コンポーネントと同じ記述方法 views/xx.vue <template> <star-rating></star-rating> </template> <script> import StarRating from "vue-star-rating" export default{ components:{ StarRating } } 以上です。 補足 星の値を取得したい場合は、 ・v-model="rating" ・detaプロパディにrating :0 参考記事 (https://www.kabanoki.net/4632/)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

コピペで最速で試すTwilioビデオ通話の実装 #twilio_online_contest_2021

Twilioのエルは一つ、今回のハッカソンで覚えたことです。 もはやどれが正解か分からないのがコンテンツ ということでチェック事項が多いTwillllllioですが、APIトークンやシークレットキーなども複数あって中々概要掴むのが大変でした。 【勝手企画】Twilioオンラインコンテストに応募しちゃおうハッカソン ビデオ映像のみのサンプルでまずは動かしたい ハッカソン参加者が、モジュールのインストールなどでつまづいたりをみていると、ミニマムなサンプルがやはり欲しいなと思う今日この頃...... とりあえずコピペかつフロントエンドだけで試せる実装をしてみました。 トークンをコードに直書きしてたりするので、 この実装は実用性を考えるとオススメできないですが、まず動く、大枠を掴むって意味合いだと良いと思います。 また、公式が推奨してるとかではないので悪しからず。 Twilio WebRTCハンズオン(ホワイトボード編)のコードから機能をバッサリ削除してます。 マイク利用 発話検知 ホワイトボード機能 などを排除して、カメラのみで1対1(2人)がアクセスしたときに映像を送りあえる部分だけの実装になります。なのでビデオ通話と書きましたが若干釣りですね。 コピペで最速で試す 前提として、Twilioアカウントは取得しておいてください。 1. トークン取得 トークンも色々あって初見だと手強いんですよね。。という感じでこの手順だと試しやすいかなと思っています。 1-1. サブアカウント作成 初めての場合はサブアカウント作成をします。 Dashboard > 設定 > サブアカウント でサブアカウントを作成します。 参考: Twilio WebRTCハンズオン(ホワイトボード編) 1-2. 管理画面からアクセストークンを2つ発行 テスティングツールでアクセストークンを発行できます。 ここで2回アクセストークンを発行します。 クレデンシャルアイデンティティ: 適当な文字列 Room名を選択する: 空 でOKです。 2. コピペコード全文 今回はVue.jsを利用しています。 PCの適当なフォルダにindex.htmlとscript.jsを作成しましょう。 index.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Camera Test</title> <style> #video-zone{ display: flex; justify-content: space-around; } #video-zone video{ width: 300px; } </style> </head> <body> <h1>Twilio WebRTCビデオ通話テスト (1対1 && Webカメラのみ)</h1> <div id="app"> <div id="room-controls"> <button id="button-join" @click="onclickA">Aさんとして入室</button> <button id="button-join" @click="onclickB">Bさんとして入室</button> </div> <div id="video-zone"> <div id="my-video"> <video id="myStream" autoplay playsinline></video> </div> </div> </div> <script src="https://media.twiliocdn.com/sdk/js/video/releases/2.7.3/twilio-video.min.js"></script> <script src="https://unpkg.com/vue@next"></script> <script src="script.js"></script> </body> </html> 先ほど発行したアクセストークン二つをtoken1とtoken2として設定します。 script.js //Vue v3 const app = Vue.createApp({ data: () => ({ ROOM_NAME: 'VideoRoom', // 部屋の名前 Video: Twilio.Video, // Twilio Video JS SDK stream: {}, // localStream: {}, videoRoom: {}, dataTrack: {}, token1: 'トークン1', token2: 'トークン2', }), mounted: async function() { this.preview(); this.dataTrack = new this.Video.LocalDataTrack(); }, methods: { //ビデオプレビュー preview: async function(){ try { this.stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); document.querySelector('#myStream').srcObject = this.stream; // this.localStream = this.stream; } catch (error) { console.log(error); } }, // 部屋に入室 connectRoom: async function(token){ try { const room = await this.Video.connect(token, { name: this.ROOM_NAME, }); console.log(`Connected to Room ${room.name}`); this.videoRoom = room; // すでに入室している参加者を表示 room.participants.forEach(this.participantConnected); // 誰かが入室してきたときの処理 room.on("participantConnected", this.participantConnected); } catch (error) { console.log(error); } }, onclickA: function(){ console.log('A Click--'); this.connectRoom(this.token1)}, //Aとしてルームに接続 onclickB: function(){ console.log('B Click--'); this.connectRoom(this.token2)}, //Bとしてルームに接続 //他の参加者が入室した処理 participantConnected: function(participant){ console.log(`connect!`); console.log(`Participant ${participant.identity} connected'`); // 参加者を表示する const div = document.createElement("div"); div.id = participant.sid; div.classList.add("remote-video"); // 参加者のトラックが届いたとき participant.on('trackSubscribed', (track) => this.trackSubscribed(div, track)); // 参加者の画像を表示 const videoZone = document.getElementById('video-zone'); videoZone.appendChild(div); }, //サブスクライブ trackSubscribed: function(div, track){ const child = div.appendChild(track.attach()); if (track.kind === "video") { child.classList.add("video-style"); } }, }, }); app.mount('#app'); まだ余計なコード残ってるかもしれないですが一旦 3. 起動 VSCodeのライブサーバーやserveなどでローカルサーバーを起動させましょう。 僕はserveをnpx起動するのが最近は多いです。 $ npx serve -p 3000 4. 試し方 http://localhost:3000にアクセスすれば表示されます。 二つのタブでページを開き、片方でAさん、もう片方はBさんで入室しましょう。 トークンは1時間しか使えないので注意 Twilioは動的にアクセストークンを発行していて1時間に一回更新されるとのことです、1時間以内ならこのトークンで試すことが出来ますが、それ以上使っていく場合は大元のハンズオン資料をもとに実装していきましょう。 このコードに追加する場合はボタンクリックした際にアクセストークンを生成するサーバー(を作成しておいて)にアクセスしてトークン取得すると良さそうです。 また、今回二つのアクセストークンを作成しましたが、アクセストークン一つで良いのでは?と思う人もいそうです。 通常、動的にトークンを払い出す場合は、アイデンティティは同じでも、時間を加味して別のトークンが払い出されるので、同じユーザでも別のトークンになりますけど、今回は全く同じトークンですからね。 と赤い芸人さんからコメント頂きました。ありがとうございます。 同じアクセストークン同士だとうまく動いてくれない模様ですね。 まとめ こんな感じでNode.jsなども利用せずにフロントだけでTwilioのWebRTC機能を試すことが出来ました。 分解して再構築してみると理解深まりますね!色々と排除したのでホワイトボード側のデータやり取りについてはちゃんと追えてないですが苦笑 また、1時間でトークンは切れるみたいですが、使い終わったらトークンを削除しておくのが良いと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

コピペで最速で試すTwilioビデオ通話の実装

Twilioのエルは一つ、今回のハッカソンで覚えたことです。 もはやどれが正解か分からないのがコンテンツ ということでチェック事項が多いTwillllllioですが、APIトークンやシークレットキーなども複数あって中々概要掴むのが大変でした。 【勝手企画】Twilioオンラインコンテストに応募しちゃおうハッカソン ビデオ映像のみのサンプルでまずは動かしたい ハッカソン参加者が、モジュールのインストールなどでつまづいたりをみていると、ミニマムなサンプルがやはり欲しいなと思う今日この頃...... とりあえずコピペかつフロントエンドだけで試せる実装をしてみました。 トークンをコードに直書きしてたりするので、 この実装は実用性を考えるとオススメできないですが、まず動く、大枠を掴むって意味合いだと良いと思います。 また、公式が推奨してるとかではないので悪しからず。 Twilio WebRTCハンズオン(ホワイトボード編)のコードから機能をバッサリ削除してます。 マイク利用 発話検知 ホワイトボード機能 などを排除して、カメラのみで1対1(2人)がアクセスしたときに映像を送りあえる部分だけの実装になります。なのでビデオ通話と書きましたが若干釣りですね。 コピペで最速で試す 前提として、Twilioアカウントは取得しておいてください。 1. トークン取得 トークンも色々あって初見だと手強いんですよね。。という感じでこの手順だと試しやすいかなと思っています。 1-1. サブアカウント作成 初めての場合はサブアカウント作成をします。 Dashboard > 設定 > サブアカウント でサブアカウントを作成します。 参考: Twilio WebRTCハンズオン(ホワイトボード編) 1-2. 管理画面からアクセストークンを2つ発行 テスティングツールでアクセストークンを発行できます。 ここで2回アクセストークンを発行します。 クレデンシャルアイデンティティ: 適当な文字列 Room名を選択する: 空 でOKです。 2. コピペコード(全文) 今回はVue.jsを利用しています。 PCの適当なフォルダにindex.htmlとscript.jsを作成しましょう。 index.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Camera Test</title> <style> #video-zone{ display: flex; justify-content: space-around; } #video-zone video{ width: 300px; } </style> </head> <body> <h1>Twilio WebRTCビデオ通話テスト (1対1 && Webカメラのみ)</h1> <div id="app"> <div id="room-controls"> <button id="button-join" @click="onclickA">Aさんとして入室</button> <button id="button-join" @click="onclickB">Bさんとして入室</button> </div> <div id="video-zone"> <div id="my-video"> <video id="myStream" autoplay playsinline></video> </div> </div> </div> <script src="https://media.twiliocdn.com/sdk/js/video/releases/2.7.3/twilio-video.min.js"></script> <script src="https://unpkg.com/vue@next"></script> <script src="script.js"></script> </body> </html> 先ほど発行したアクセストークン二つをtoken1とtoken2として設定します。 script.js //Vue v3 const app = Vue.createApp({ data: () => ({ ROOM_NAME: 'VideoRoom', // 部屋の名前 Video: Twilio.Video, // Twilio Video JS SDK stream: {}, // localStream: {}, videoRoom: {}, dataTrack: {}, token1: 'トークン1', token2: 'トークン2', }), mounted: async function() { this.preview(); this.dataTrack = new this.Video.LocalDataTrack(); }, methods: { //ビデオプレビュー preview: async function(){ try { this.stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); document.querySelector('#myStream').srcObject = this.stream; // this.localStream = this.stream; } catch (error) { console.log(error); } }, // 部屋に入室 connectRoom: async function(token){ try { const room = await this.Video.connect(token, { name: this.ROOM_NAME, }); console.log(`Connected to Room ${room.name}`); this.videoRoom = room; // すでに入室している参加者を表示 room.participants.forEach(this.participantConnected); // 誰かが入室してきたときの処理 room.on("participantConnected", this.participantConnected); } catch (error) { console.log(error); } }, onclickA: function(){ console.log('A Click--'); this.connectRoom(this.token1)}, //Aとしてルームに接続 onclickB: function(){ console.log('B Click--'); this.connectRoom(this.token2)}, //Bとしてルームに接続 //他の参加者が入室した処理 participantConnected: function(participant){ console.log(`connect!`); console.log(`Participant ${participant.identity} connected'`); // 参加者を表示する const div = document.createElement("div"); div.id = participant.sid; div.classList.add("remote-video"); // 参加者のトラックが届いたとき participant.on('trackSubscribed', (track) => this.trackSubscribed(div, track)); // 参加者の画像を表示 const videoZone = document.getElementById('video-zone'); videoZone.appendChild(div); }, //サブスクライブ trackSubscribed: function(div, track){ const child = div.appendChild(track.attach()); if (track.kind === "video") { child.classList.add("video-style"); } }, }, }); app.mount('#app'); まだ余計なコード残ってるかもしれないですが一旦こんな感じにまとめてみました。 3. 起動 VSCodeのライブサーバーやserveなどでローカルサーバーを起動させましょう。 僕はserveをnpx起動するのが最近は多いです。 $ npx serve -p 3000 4. 試し方 http://localhost:3000にアクセスすれば表示されます。 二つのタブでページを開き、片方でAさん、もう片方はBさんで入室しましょう。 トークンは1時間しか使えないので注意 Twilioは動的にアクセストークンを発行していて1時間に一回更新されるとのことです、1時間以内ならこのトークンで試すことが出来ますが、それ以上使っていく場合は大元のハンズオン資料をもとに実装していきましょう。 このコードに追加する場合はボタンクリックした際にアクセストークンを生成するサーバー(を作成しておいて)にアクセスしてトークン取得すると良さそうです。 また、今回二つのアクセストークンを作成しましたが、アクセストークン一つで良いのでは?と思う人もいそうです。 通常、動的にトークンを払い出す場合は、アイデンティティは同じでも、時間を加味して別のトークンが払い出されるので、同じユーザでも別のトークンになりますけど、今回は全く同じトークンですからね。 と赤い芸人さんからコメント頂きました。ありがとうございます。 同じアクセストークン同士だとうまく動いてくれない模様ですね。 まとめ こんな感じでNode.jsなども利用せずにフロントだけでTwilioのWebRTC機能を試すことが出来ました。 分解して再構築してみると理解深まりますね!色々と排除したのでホワイトボード側のデータやり取りについてはちゃんと追えてないですが苦笑 また、1時間でトークンは切れるみたいですが、使い終わったらトークンを削除しておくのが良いと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RailsとVueを使ったPFをherokuにデプロイするのに苦戦した話

初めての投稿。ドキドキ… バックエンドにRails フロントエンドにVueを使って、ポートフォリオを作成しHerokuにでプロイしようとしたらエラーで苦戦したのでメモ。 こちらの記事を参考に進めて $ git push heroku master ここで苦戦。いくらやっても途中でエラー。 Your bundle is locked to mimemagic (0.3.5), but that version could not be found in any of the sources listed in your Gemfile. If you haven't changed sources, that means the author of mimemagic (0.3.5) has removed it. You'll need to update your bundle to a version other than mimemagic (0.3.5) that hasn't been removed in order to install. 調べたらmimemagicが削除されていてできないことが判明。 そこで下の記事が見つかり railsのアップデートを行う。 無事完了!! 今後もアウトプットに投稿していこうと思います。初めての投稿ってドキドキしてしまった
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue×Firebaseでステージング環境と本番環境を使い分ける

はじめに 先日アプリを開発するにあたり、Vue.js × Firebase の構成をとることにしたのですが、 当然、開発のためにステージング環境と本番環境の二つを構成したくなるわけです。 また、ホスティングだけではなく、Firebase Authentication, FireStore, Google Analyticsと Firebaseのサービスを使い倒していました。 当然これらも開発環境と本番環境で使い分けたいわけです。 その際にいろいろと調べたのですが、Nuxt.jsありきの解消方法を書いた記事は見かけるのですが、 今回の私のようにただのVue.jsのみを切り分けたいという場合の記事がほとんど見つからなかったので、 私の備忘録と後世のためにこのメモを残します。 根本的にどう切り分けるのか? Firebaseの環境はどうすれば切り替わるのか? Firebaseのサービスを利用するために取得した設定情報があるかと思います。 こんな感じのやつ。 const firebaseConfig = { apiKey: "", authDomain: "", projectId: "", storageBucket: "", messagingSenderId: "", appId: "", measurementId: "", }; これが設定情報そのままなわけです。 ここでいえば、firebaseConfigという変数の中身を、ステージング用なのか、本番用なのかで切り分ければいいわけですね。 実行環境がステージングなのか本番なのかはどう判断するのか? process.env.NODE_ENVという変数を使います。 これは実行環境が、開発用なのか、本番用なのかを判断するための情報を保持しています。 現在のprocess.env.NODE_ENVは、nodeコマンド→process.env.NODE_ENVを入力することで確認できます。 つまりprocess.env.NODE_ENVの変数の中身を見に行って、実行環境が開発なのか、本番なのかを判断し、 上記の"firebaseConfig"の中身を書き換えてあげればいいわけですね。 実行環境を切り分けるにはどうするのか? cross-envというライブラリを使います。 cross-env は Node のライブラリで npm スクリプト実行時に任意の環境変数を設定できます。 こいつを使って環境変数を切り分けます。 まずは環境に放り込む。 npm install --save cross-env あとはvue-cliとかと合わせて使ってやればいい。 以下ではNODE_ENVの設定を"development"にしています。 cross-env NODE_ENV="development" ここまでくれば、後の流れは簡単ですね。 Firebaseでステージング・本番用のプロジェクトをそれぞれ作成し、それぞれのAPIキーを取得する。 cross-envを環境にインストール firebaseへの接続を定義しているプログラム付近に、process.env.NODE_ENV === "production"なら本番設定、そうでないならステージング設定というようなif文をつけてやる。 ここで3番という見慣れぬ内容が出てきたと思うので、以降で解説します。 実際の設定方法 package.jsonの編集 package.jsonの"script"部分に、serve や buildが定義されていると思います。 初期の状態だと、多分こういう感じになっているはずです。 "scripts": { "serve": "vue-cli-service serve", "build": "vue-cli-service build", }, コマンドの実行前に、今から実行するコマンドが開発用にたたくのか、本番用にたたくのかを教えてあげます。 その際の書き方はこういう感じです。 "scripts": { "dev": "cross-env NODE_ENV=\"development\" vue-cli-service serve", "stage": "cross-env NODE_ENV=\"development\" vue-cli-service build", "serve": "cross-env NODE_ENV=\"production\" vue-cli-service serve", "build": "cross-env NODE_ENV=\"production\" vue-cli-service build", }, npm run dev, npm run stageの場合はNODE_ENVは"development"として、npm run serve, nopm run buildの場合は NODE_ENVは"production"として、扱うことができます。 そうすることで、プログラムの内部でNODE_ENVを取得した際に、開発と本番で結果が異なる状態を作ることができます。 firebaseインスタンスの初期化プログラム周りの修正 次に、開発と本番の切り分けがない状態だと、Firebaseへのインスタンスを初期化する部分は、 こういう感じの状態のはずです。 const firebaseConfig = { apiKey: "", authDomain: "", projectId: "", storageBucket: "", messagingSenderId: "", appId: "", measurementId: "", }; const firebaseApp = firebase.initializeApp(firebaseConfig); それをこういう風に書き換えます。 const environment = process.env.NODE_ENV || "development"; const envSet = require(`./env.${environment}.js`); const firebaseConfig = envSet.CONFIG; const firebaseApp = firebase.initializeApp(firebaseConfig); 変わったのは、先頭の4行分です。 process.env.NODE_ENVの値が設定されていない場合は、"./env.development.js"に記載したfirebase接続情報を取得し、 そうでない場合はprocess.env.NODE_ENVの値の"./env.xxx.js"からfirebase接続情報を取得します。 最後に、firebase hostingへデプロイする際に設定を切り分けてあげればOKです。 firebase hostingへのデプロイ先を切り分ける firebase deployを実行したときのターゲットプロジェクトは、 firebase projects:list をたたくと調べることができます。 firebase projects:list この結果、currentとなっているプロジェクトがfirebase deployなどを実行したときのターゲットとなるプロジェクトです。 ターゲットを切り替えるには、firebase use コマンドを使います。 firebase use プロジェクトID プロジェクトIDはfirebase projects:listで表示されています。 最後に再びpackage.jsonに今の一連の流れを記述してあげれば終わりです。 NODE_ENVで環境変数の内容を切り分ける。→ 切り分けられた状態でbuild → デプロイ先のプロジェクトを変更 → デプロイ って感じです。 "scripts": { "staging": "cross-env NODE_ENV=\"development\" vue-cli-service build && firebase use 開発用ProjecID && firebase deploy", "deploy": "cross-env NODE_ENV=\"production\" vue-cli-service build && firebase use 本番用ProjecID&& firebase deploy", },
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

演習型ITリテラシー学習アプリを作りました。

こんにちは。 私は今までに複数のアプリを作ってきたのですが、使って貰ったのは家族や知人の狭い範囲でした。 今回は、思い切って公開するので、ぜひご一読お願いできればと思います! なにを作ったのか 一言で言うならば、タイトルにもあるように、演習型ITリテラシー学習アプリです。 アプリの構想はProgateを元にしていて、実際にアプリを操作して、事例に触れながらITリテラシーを「楽しく」学んで貰えることを目指しています。 リリースしたばかりなので、レッスンはまだ「フィッシング詐欺」だけなのですが、これからアップデートできればと思います。 なぜ作ったのか 昨今でもないですが、日本はITリテラシーが低いとよく耳にします。 佐川急便やAmazonなどを装ったメールが届くことはいろいろなところで目に、耳にしますが、本物かどうかわからないという人もちらほら見ます。 そういった人に向けて、是非役に立てればいいなと思って開発しました。 私自身もさほど高い方ではない気がしているので、自分の勉強も兼ねています。 アプリの中身 ・アプリランディングページ ・レッスンスライド ・レッスン ・現在動作が不安定で、読み込みに時間が掛かる場合があります。恐縮ですが、気長にお待ちいただけると幸いです。。。 使い方 Lesson1である「フィッシング詐欺」は、会員登録をしなくても受講していただけます。 「まずはここから始めてみましょう」にある、フィッシング詐欺をクリックすると、Lesson1が始まります。 その後は案内に沿って進めていただければ大丈夫です。 会員登録は、通常の登録とGoogleでのログインができます。 退会処理もできますので、不要と思えばページの下部から「退会手続き」を選択し、処理を進めてください。 開発に当たっての詳細 使用技術 ・PHP, Laravel ・Vue.js ・Docker ・AWS 大変だった部分 大変だったのは、認証機能の理解です。 当アプリは、ログインにメール認証を行っています。 Email Verificationを使いましたが、元から用意されてある分見えない部分があって理解に時間がかかりました。 現在のアプリの状態は、ログインしていようがしていまいが、使える機能に違いがないので、メール認証を完了していなくともログインはできる状態です。 今後は、基本的にこの仕組みは変えずに、より詳細な機能を利用する際は、verifiedのミドルウェアを通すようにして、認証して貰ってから機能を使う仕組みにして行こうと思います。 記事をご覧いただいた方へお願い 是非アプリを使っていただいて、感想や修正依頼、アイデアやその他様々なご意見のフィードバックをいただけたら非常に嬉しいです。 その際は、この記事に直接、もしくは以下に送っていただければと思います。 GitHub アプリ公式TwitterのDM よろしくお願いします。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む