- 投稿日:2020-12-18T23:30:28+09:00
2020年に使った技術
うなすけ氏が書いてるのを見て、自分もやってみようかなと思いました
(フロントエンド、サーバーサイドエンジニアとか関係無しに個人的なものとして)参考:https://blog.unasuke.com/2020/wrap-up-my-coding/
Application
- frontend
- Vue.js
- backend
- python, Flask
- java, Spring Boot
- other
- Docker
- Kubernetes
Vue.js, python, flaskは去年からずっと利用しているが、java, Spring Bootはつい最近触り始めた。
今のところ雰囲気でなんとかなっているが、ちゃんと勉強しとこうかなぁというところ。
Dockerも前から触っていたがkubernetesは今年になってちゃんとやった。Cloud (GCP)
- computing
- GAE (Google Application Engine)
- GKE (Google Kubernetes Engine)
- GCE (Google Compute Engine)
- Cloud Run
- firebase
- Container Registry
- DB / storage
- datastore
- BigQuery
- Cloud SQL
- realtime database
- Cloud Storage
- network
- Pub/Sub
- Cloud Endpoint
- Cloud Tasks
- Cloud Load Balancing
- Cloud DNS
- monitoring
- Stackdriver
- Cloud monitoring
- other
- Cloud workflow
- secret manager
GCPばかりいじっていた。やってるプロダクト、プロジェクトにも深く関わるのが大きな要因だが、couseraの学習コースとかでも触る機会が多かった。仕事だとGAEがメインだったが、cousera上ではGCEを作ったり消したりを繰り返していた。あとは新しく出たサービスが気になって動かしてみたものもある。
去年と比べるとネットワーク周りの設定を行ったり、GKEをガッツリさわった。
なお今年AWSは触らなかった模様。エディタの話は以下に書いたので割愛。
https://qiita.com/woody-kawagoe/items/2ab0226dd325bba5681b感想
GCPばかりやってて他に書く技術記事無えなぁと思いつつ過ごしていたけど振り返ってみると本当にGCP関連しかしてないことがわかってきた。仕事でかなり使うというのもそうだけど、新しいサービスが日々出るので気になるところであった。それとたまたま仕事でGKE触る機会ができたのも良かった。別にインフラエンジニアやってるわけではなくwebアプリ開発の方が主の業務なのだが、技術的な興味の向き先がどっちかというとインフラよりなのかもしれない。来年はkubernetes周りとかネットワークとかもっと汎用性のある記事かけたら良いなーと思う。
- 投稿日:2020-12-18T22:43:44+09:00
Vue3 Composition API でのVue Routerの使い方
Vue2までのOptions APIでは、
this.$router
のようにthis
を介してルーターオブジェクトにアクセスをしていました。しかしながら、Composition APIのsetup
メソッドないではthis
にアクセスすることができません。そのため、今までとは違うアプローチからアクセスする必要があります。useRouter()・useRoute()に置き換える
ルーターオブジェクトを得るためには、
vue-rouer
からuseRouter
またはuseRoute
関数をインポートします。それぞれの関数は以下のように対応しています。
Options API Composition API this.$router useRouter this.$route useRoute <script lang="ts"> import { defineComponent } from 'vue' import { useRoute, useRouter } from 'vue-router' export default defineComponent({ setup() { const router = useRouter() const route = useRoute() const fetch = () => { const { id } = route.params store.getTodo(id) } const goHome = () => { router.push('/') } return { goHome, } }, }) </script>
useRoute()
関数から取得できるroute
オブジェクトは、リアクティブとなっています。なお、テンプレート内では今までどおり、
$router
、$route
にアクセスできます。ですから、router
または$route
オブジェクトをreturnする必要はありません。setupメソッド外からはアクセスすることはできない
先程例示した
goHome
関数は汎用的に使えそうなのでComposition APIの流儀にならって別のファイルに分割してみたいところです。試しに、この関数をsrc/composable/router/index.ts
に配置してそこから呼び出すように変えてみましょう。src/composable/router/index.tsimport router from '@/router' import { useRouter } from 'vue-router' const router = useRouter() export const goHome = () => { router.push('/') }somePage.vue<template> <button @click="goHome">トップへ戻る</button> </template> <script lang="ts"> import { defineComponent } from 'vue' import { goHome } from '@/composables/router' export default defineComponent({ setup() { return { goHome, } }, }) </script>ボタンを押してみても、思いに反して反応してくれません。コンソールエラーを見てみると、どうやら
useRouter()
から得たrouter
オブジェクトがundefined
になっているようです。コンポーネント内ナビゲーションガード
ナビゲーションガードも同様に、
vue-router
から関数をインポートして使います。Opsitions APIにおける
beforeRouteEnter
に対応するものは提供されていないようです。
Options API Composition API beforeRouteEnter - beforeRouteUpdate onBeforeRouteLeave beforeRouteLeave onBeforeRouteLeave <script lang="ts"> import { defineComponent } from 'vue' import { onBeforeRouteLeave, onBeforeRouteUpdate, } from 'vue-router' export default defineComponent({ setup() { onBeforeRouteLeave((to, from) => { // }) onBeforeRouteUpdate((to, from) => { // }) }, }) </script>
- 投稿日:2020-12-18T17:32:25+09:00
Nuxt(Vue)でツイートを埋め込みたい(埋め込みウィジェット, vue-tweet-embed)
この記事は「株式会社オープンストリーム "小ネタ" Advent Calendar 2020」の 17 日目の記事です。
以前、Nuxt(Vue)に Twitter のツイートを埋め込むときは vue-tweet-embed のパッケージを使うとよさそうと考えましたが、 うまくいかないときがあったので諦めてました。
確かめる機会ができたのでここにまとめます。
環境
- Nuxt.js 2.14.9
- Storybook 6.0.10
$ node -v v14.5.0 $ yarn -v 1.22.5通常通りツイートを埋め込む
Twitter のコードを使ってツイートを埋め込むときは次の 2 つの情報が必要になります。
- screen_name
- tweet の id
DB のテーブルに
id
しか保存していない場合は、別のテーブルからscreen_name
を持ってこなければならないので厄介です。また、埋め込み用コードのほかに https://platform.twitter.com/widgets.js のスクリプトを読み込む必要があり、読み込む位置によって SPA でページ遷移した後の挙動がおかしくなるなど注意が必要です。
私もスクリプトをねじ込んでツイートを埋め込みましたが、こちらのコードのほうが SPA でのページ遷移が考慮されているため紹介します。
Nuxt.js で Twitter シェアボタンやツイートの埋め込みなどの widgets を追加する方法 | mintsu's blog
https://blog.mintsu-dev.com/posts/2020-05-23-nuxt-spa-twitter-share/vue-tweet-embed を使う
初めに使ったのは vue-tweet-embed です。screen_name が不要でツイートの ID だけで埋め込み表示までできるため結構重宝しました。
yarn add vue-tweet-embed最初のうちはきちんとツイートが埋め込みで表示されてたのですが、README の方法で記述するとうまくいきません。
import Tweet from 'vue-tweet-embed/tweet'現在の
vue-tweet-embed
だとyarn dev
ができますが、コンパイルで次のエラーが出ます。This dependency was not found:
- vue-tweet-embed/tweet in ./node_modules/babel-loader/lib??ref--12-0!./node_modules/ts-loader??ref--12-1!./node_modules/@nuxt/components/dist/loader.js??ref--0-0!./node_modules/vue-loader/lib??vue-loader-options!./pages/index.vue?vue&type=script&lang=ts&
vue-tweet-embed
が古いなどプロジェクトの構成によっては次のエラーが出てyarn dev
すらできなくなります。warning nuxt > @nuxt/webpack > @nuxt/babel-preset-app > core-js@2.6.11: core-js@<3 is no longer maintained and not recommended for usage due to the number of issues. Please, upgrade your dependencies to the actual version of core
-js@3.
error eslint@6.7.2: The engine "node" is incompatible with this module. Expected version "^8.10.0 || ^10.13.0 || >=11.10.1". Got "11.4.0"
error Found incompatible module.当時は解決できなくて 1-2 年ぐらい放置していたのですが、次の方法でうまくいきました。
import { Tweet } from 'vue-tweet-embed'また、Tweet コンポーネントを読み込めても"Whoops! We couldn't access this Tweet. "が表示される場合は、ツイートの ID が JavaScript の仕様で丸められた可能性があります。
この場合は Twitter API でいうid_str
を ID として使います。このパッケージは 2020/12/16 現在で Last publish が a year ago になっているため、今後更新されない場合は先ほどの通常通りな方法を試すしかないようです。
TypeScript で使用する場合は自分で d.ts を作る必要がある
TypeScript を使ったコンポーネントで
vue-tweet-embed
使うと型がないというエラーがでます。npm で型定義ファイルがないので自分で作る必要があります。any 型とみなして良いのであれば次の内容で d.ts ファイルを作ります。そうでなければ
vue-tweet-embed
のパッケージからソースコードを自分で読んで自分で型を定義します。。。types/vue-tweet-embed.d.tsdeclare module "vue-tweet-embed";作成した d.ts を使うように tsconfig.json の
compilerOptions
でtypeRoots
を追加します。tsconfig.json{ "compilerOptions": { "typeRoots": ["types"],
- 投稿日:2020-12-18T17:17:06+09:00
初学者必見!Headerにある項目を動的に変化させる方法
今回は、ログインしている時だけ「ログアウト」ボタンを表示させたいと思ったので、そのやり方を備忘録として書いていきます。
最後まで見て、よかったらLGTMお願いします!!(今じゃなくていいです)
それでは順を追って説明します。
Vuexの機能を使い、sotre/index.jsのstateにユーザーがログインしているかを判断するauthを設定します。
store/index.jsimport Vue from "vue"; import Vuex from "vuex"; import createPersistedState from "vuex-persistedstate"; Vue.use(Vuex); export default new Vuex.Store({ plugins: [createPersistedState()], //永続的にログインできるプラグイン state: { auth: "", //userがログインしているか user: "", //user情報 }, )}vuex-persistedstateについてはこちらの記事でご紹介しているので、ぜひご覧ください。
次に、computedを使って、authの変更を検知します。
computedは自動で値の変更を検知してくれるので大変便利です。components/Header.vue<div class="headB" :class="{'open' : isClass}"> <ul> ~ <li> <a @click="logoutButton" v-if="auth === true">ログアウト</a> <a @click="loginButton" v-else>ログイン</a> </li> </ul> </div> <script> export default{ computed: { auth() { return this.$store.state.auth; //store/index.jsのstateにあるauthを取得 } }, } </script>これでログインしているときにログアウトが表示されるようになります。
これはHeaderだけでなく、他にも応用できそうですね。
以上、「Headerにある項目を動的に変化させる方法」でした。
- 投稿日:2020-12-18T16:04:22+09:00
MediaDevicesとWeb Audio API Vue.jsとThree.js で 音声の波形表示
MediaDevicesとWeb Audio API vuejsとthreejs
概要
- WebRTCの話が盛り上がってたのでjsでカメラとマイクを使う方法をおさらいする
- デバイスの選択をする仕組みをvueで作ってみる
- ビデオデバイスの映像をvideoタグで表示してみる
- オーディオデバイスの音声をAnalyserNodeを使ってビジュアライズしてみる
WebRTCの話が盛り上がってたのでjsでカメラとマイクを使う方法をおさらいする
社内でWebRTCについて話題が上がり、そこで以前webGLでカメラ映像をテクスチャとして取り込んでエフェクトかけて遊べる何やらを試作したのを思い出したので、すっかり忘れたその方法を復習してみました。
まずはカメラとマイクのデバイスを取得してVIDEOタグで再生させてみる。
var constraints = { audio: true, video: true }; navigator.mediaDevices.getUserMedia(constraints).then(function(stream){ document.getElementById("video").srcObject = stream; document.getElementById("video").play(); }).catch(function(err){ console.log("!!!!",err); });これでブラウザでHTMLに事前に貼り付けておいたVIDEOタグ(#video)にカメラの映像が表示される様になしました。
ただこれだとカメラもマイクも自動選択なのでHangoutMeetの開始時の様にデバイス選択できる様にしたい。
デバイスの選択をする仕組みをvueで作ってみる
enumerateDevices()という命令でデバイスリストを取得できる様です。
MediaDevices.enumerateDevices() - Web API | MDN
navigator.mediaDevices.enumerateDevices().then(function(devices){ console.log(devices); });さっそくリスト取得してみましたが思いの外いっぱい出てきます。
取得できるデバイスの単品のデータ構造はこんな感じで
{ "deviceId": "default", "groupId": "13eaa2c10d3b436a8bbb085b5af46a8a721eea3c271546792e9dfe15a1e6c4c2", "kind": "audioinput", "label": "既定 - External Microphone (Built-in)" }
kind
がデバイスの種類を示す様で、MDNによれば
"videoinput" "audioinput" "audiooutput"
の3種類あるそうです。
思いの外多かったのは出力用のデバイス "audiooutput" があったからですね。
入力だけとばかり思ってましたがmediaDevicesには出力も含まれる様です。このリストから"videoinput" "audioinput"を抽出して選択できる様にします。
jsはこんな感じで
var app = new Vue({ el:"#app", data:{ videomedias: [], audiomedias: [] } }); navigator.mediaDevices.enumerateDevices().then(function(devices){ var videomedias = []; var audiomedias = []; devices.forEach(device => { if( device.kind == "videoinput" ){ videomedias.push(device); } if( device.kind == "audioinput" ){ audiomedias.push(device); } }); Vue.set(app,"videomedias",videomedias); Vue.set(app,"audiomedias",audiomedias); });HTMLはこんな感じ
<div id="app"> <div> <select name="sel_video" id="sel_video"> <option v-for="(item, index) in videomedias" :value="index" >{{item.label}}</option> </select> <select name="sel_audio" id="sel_audio"> <option v-for="(item, index) in audiomedias" :value="index" >{{item.label}}</option> </select> </div> </div>これでリストアップはできたので、フォーム入力バインディングを使って簡単に値を取れる様にしておきます。
var app = new Vue({ el:"#app", data:{ videomedias: [], audiomedias: [], selectedvideo: 0, selectedaudio: 0 } });<div id="app"> <div> <select name="sel_video" id="sel_video" v-model="selectedvideo"> <option v-for="(item, index) in videomedias" :value="index" >{{item.label}}</option> </select> <select name="sel_audio" id="sel_audio" v-model="selectedaudio"> <option v-for="(item, index) in audiomedias" :value="index" >{{item.label}}</option> </select> </div> </div>あとはボタンを追加して選択したデバイスを使った処理を行います。
<button @click="startvideo()" >開始</button>methods:{ startvideo:function(){ ... } }ビデオデバイスの映像をvideoタグで表示してみる
実際にカメラデバイス指定をしてvideoタグで表示する処理をボタンを押したら実行する様にします。
methods:{ startvideo:function(){ var constraints = { video: { deviceId: this.selectedvideo != -1 ? this.videomedias[this.selectedvideo].deviceId : null, width: 1280, height: 720 } }; navigator.mediaDevices.getUserMedia(constraints).then(function(stream){ document.getElementById("video").srcObject = stream; document.getElementById("video").play(); }).catch(function(err){ console.log("!!!!",err); }); } }これでvueで作ったボタンを押せば、指定したカメラの映像がブラウザ上で再生されます。
オーディオデバイスの音声をAnalyserNodeを使ってビジュアライズしてみる
ここまでやってカメラの画像をwebGLでどうこうじゃなくて、音声の波形情報とか表示できないかなと思い調べてみると
Web Audio APIを使って波形や周波数スペクトラムデータが取れる模様Visualizations with Web Audio API - Web API | MDN
MDNの解説に従いオーディオデバイスからオーディオストリーム取得し、それを元にオーディオソースを取得し、
アナライザーノードに接続します。var audio_ctx = new AudioContext(); var analyser = audio_ctx.createAnalyser(); var audioinput; var audio_constraints = { audio:{ deviceId: this.selectedaudio != -1 ? this.audiomedias[this.selectedaudio].deviceId : null, } }; navigator.mediaDevices.getUserMedia(audio_constraints).then(function(stream){ audioinput = audio_ctx.createMediaStreamSource(stream); }).catch(function(err){ console.log("!!!!",err); });何も変わらないですがこれで取れているはず・・・。
実際に値を表示してみます。var dataArray = new Float32Array(analyser.frequencyBinCount); setInterval(function(){ analyser.getFloatTimeDomainData(dataArray); console.log(dataArray); },100);これをヴィシュアライズしていきますが、ヴィジュアライズには
Three.jsを使っていきます。まずは、色々表示のための準備をしていきます。
ラインで波形を表示しますがある程度なめらかが必要ですので、点の数が500個でデータを用意します。var scene; var renderer; var camera; var points = []; var line; var width = 1280; var height = 300; scene = new THREE.Scene(); camera = new THREE.OrthographicCamera( width / -2, width / 2, height / 2, height / -2, 1, 1000 ); scene.add(camera); for(var i = 0 ; i < 500; i++){ points.push(new THREE.Vector2(i / 500 * width, 0)); } var geometry = new THREE.BufferGeometry().setFromPoints( points ); var material = new THREE.LineBasicMaterial({ color: 0xff0000; }); line = new THREE.Line( geometry, material ); line.position.x = width / -2; scene.add( line ); renderer = new THREE.WebGLRenderer(); renderer.setSize(width,height); document.body.appendChild( renderer.domElement );あとはレンダリングの処理を書きます。
function animate(){ requestAnimationFrame( animate ); renderer.render( scene, camera ); }これで animation関数 を実行すれば
アニメーションのレンダリングが開始されます。今のところただまっすぐ赤い線を引くだけですので、ここにアナライザーノードからの波形データを入れていきます。
まず頂点データを変更するためにジオメトリから頂点データを引っ張ってきます。
var positions = line.geometry.attributes.position.array;ここには x y z の順番で頂点データが入っていますので、そのうち y の値を変化させて、波形のデータを反映させます。
波形のデータは -1〜1 の範囲でデータが入ってくるはずですので 表示領域の半分の高さを乗算した値を入れていきます。
ただし、波形データのデータ長と、頂点データのデータ長が一致していないので計算で補正して波形データの値を拾っていっています。function animate(){ var height = 300; var positions = line.geometry.attributes.position.array; for(var i = 0 ; i < positions.length; i+=3){ positions[i+1] = height/2 * dataArray[Math.floor( i/positions.length * dataArray.length) ]; } requestAnimationFrame( animate ); renderer.render( scene, camera ); }さらにラインの頂点データの変更を反映するには
line.geometry.attributes.position.needsUpdate = true;の様にレンダリング前にneedsUpdateにtrueをセットする必要があるとのこと。
function animate(){ var height = 300; var positions = line.geometry.attributes.position.array; for(var i = 0 ; i < positions.length; i+=3){ positions[i+1] = height/2 * dataArray[Math.floor( i/positions.length * dataArray.length) ]; } line.geometry.attributes.position.needsUpdate = true; requestAnimationFrame( animate ); renderer.render( scene, camera ); }これでひとまず出来上がりです。
アナライザーノードは周波数スペクトラムも取得できるので時間を見てそちらも挑戦してみます。
FORK Advent Calendar 2020
19日目 Vue.jsのSSGフレームワークのGridsomeはすごいぞ @Kodak_tmo
21日目 ml5.js の FaceApi で遊んでみる @kinoleaf
- 投稿日:2020-12-18T14:33:03+09:00
RailsとVueでアプリを作るための環境構築
この記事はRailsとVueでHello Vue!をすることを目的としています。
プロジェクトの作成
何はともあれrails newですよね。ちなみにこの時点で--webpack=vueオプションでvueを始めからインストールできますが、今回はそれ以外の方法を紹介します。
と言ってもrails webpacker:install:vueをあとで叩くだけです。% rails -v Rails 6.0.3.4 % rails new memo-memo -d mysql --skip-test % cd memo-memo実はmysqlのインストールで躓いてそちらの記事も書いたので参考にしてください。
今回は失敗していない体(てい)で進みます。
rails newでmysqlのインストールに失敗するデータベースの作成
% rails db:create Created database 'memo_memo_development' Created database 'memo_memo_test'Hello World!
% rails s Webpacker configuration file not found xxx/memo-memo/config/webpacker.yml. Please run rails webpacker:install Error: No such file or directory @ rb_sysopen - xxx/memo-memo/config/webpacker.yml (RuntimeError)webpackerがインストールされていないと怒られたので
% rails webpacker:install % rails s => Booting Puma => Rails 6.0.3.4 application starting in development => Run `rails server --help` for more startup options Puma starting in single mode... * Version 4.3.7 (ruby 2.6.3-p62), codename: Mysterious Traveller * Min threads: 5, max threads: 5 * Environment: development * Listening on tcp://127.0.0.1:3000 * Listening on tcp://[::1]:3000 Use Ctrl-C to stopWebpackerとは
世界に挨拶することができたので、Webpackerを用いてVueを使えるようにしていきます。
ここでWebpackerとは何かわからない方向けに説明すると、WebpackerとはRailsにWebpackを入れるためのライブラリーで、Webpackの設定をよしなにしてくれています。WebpackとはJSなどのファイルをひとつにまとめてくれるものになります。カッコよく言うと、モジュールバンドラーです。ファイルをひとつにまとめる理由はブラウザの読み込み速度を速くするためです。CPUで計算するのに比べて、ファイルを取りに行く方が圧倒的に時間がかかる処理で、ファイルを読み込む回数を減らすことがブラウザの読み込み速度に大きな効果があります。ちなみにコンパイルはWebpack本来の機能ではなく、あくまでwebpackerにloaderを入れることで実現しています。
また、実務ではWebpackerではエラーが起きた時に何が原因かわかりづらくなってしまうため、楽せずWebpackを使うらしいです。ただ、私と同じ初学者の方はWebpackerから入って問題ないと思います。
話が長くなりそうなので次に進みます。Vueのインストール
% rails webpacker:install:vueいくつかのファイルが追加されたと思いますが、重要なファイルはapp/javascript/packs/hello_vue.jsとapp/javascript/app.vueになります。これらのファイルでhello vue!ができるようになっています。
app/javascript/packs/hello_vue.jsimport Vue from 'vue' import App from '../app.vue' document.addEventListener('DOMContentLoaded', () => { const app = new Vue({ render: h => h(App) }).$mount() document.body.appendChild(app.$el) console.log(app) })app/javascript/app.vue<template> <div id="app"> <p>{{ message }}</p> </div> </template> <script> export default { data: function () { return { message: "Hello Vue!" } } } </script> <style scoped> p { font-size: 2em; text-align: center; } </style>何故、app.vueの他にhello_vue.jsが必要なのかと言うと、vueファイルを直接読み込まずにjsファイル介して読み込むためです。html.erbでhello_vue.jsを読み込めば、hello_vue.jsはapp.vueを読み込んでいるのでapp.vueを表示できます。app.vueファイルのscriptタグ内のmessageという変数にHello Vue!が定義されていて、templateタグ内のpタグの中に変数messageが書かれていることで、Hello Vue!が出力されることは何となくわかると思います。詳しい説明は割愛させていただきます。
Hello Vue!の表示
これからHello Vue!を表示するための簡単なページを作成したいと思います。
流れとしてはルーティング→コントローラー→ビューになります。
ここでは'localhost:3000/home'にアクセスするとHomeコントローラーのindexアクションにルーティングされて、indexアクションからapp/view/home/index.html.erbを表示させたいと思います。そのindex.html.erbでhello_vue.jsファイルを読み込むことでHello Vue!を表示します。それではルーティングの設定を行います。
routes.rbget 'home', to: 'home#index'この状態でlocalhost:3000/homeにアクセスするとどうなるかわかりますか?
'uninitialized constant HomeController'と出ていると思います。Homeコントローラが定義されていないので当たり前ですよね。Homeコントローラーを作っていきます。% rails g controller home create app/controllers/home_controller.rb invoke erb create app/views/home invoke helper create app/helpers/home_helper.rb invoke assets invoke scss create app/assets/stylesheets/home.scss作成されたapp/controllers/home_controller.rbファイルを開いて、indexアクションを追加します。
class HomeController < ApplicationController def index end endこれでindexアクションの定義は終わりです。何も定義しなくてもいいのは、暗黙的にrenderが呼ばれて、アクションと名前で対応付けられたテンプレートが実行されるからですよね。
ちなみにこの状態でlocalhost:3000/homeにアクセスするとどうなるかわかりますか?
missing a templateですよね。
次にapp/view/home/index.html.erbを作成します。作成したらhello_vue.jsを読み込んでください。
ビューでJavaScript packをインクルードするにはjavascript_pack_tag ''を使います。今回hello_vue.js を読み込みたいので、pack名の箇所にhello_vueを記載しています。<%= javascript_pack_tag 'hello_vue' %>それでは、http://localhost:3000/home にアクセスして「Hello Vue!」が表示されているか確認しましょう!
お疲れ様です。実際に開発していくとなると、rails sの他にbin/webpack-dev-serverのコマンドも実行していた方がいいです。このコマンドはJSファイルのホットリロードを行ってくれるものになります。rails sとbin/webpack-dev-serverをひとつのファイルに記述して、1つのコマンドで2つのコマンドを実行することも可能です。詳しくは説明しませんが、foremanというgemを必要とします。'foreman rails s bin/webpack-dev-server'で調べると出てくると思います。
また、bin/webpack-dev-serverについてはこちらの記事が参考になるかもです。
- 投稿日:2020-12-18T13:11:38+09:00
【vue.js】TypeError: Cannot read property 'xxxx' of undefined
下記のエラーが出ています!
vue.esm.js?efeb:628 [Vue warn]: Error in v-on handler: "TypeError: Cannot read property 'index' of undefined"
vue.esm.js?TypeError: Cannot read property 'index' of undefined
原因: DOM はまだ更新されないです!!!!!!
解決方法: 「$nextTick」を使えばokです!
callback の実行を遅延し、DOM の更新サイクル後に実行します。DOM の更新を待ち受けるためにいくつかのデータを更新した直後に使用してください。callback の this コンテキストは自動的にこのメソッドを呼びだすインスタンスに束縛されることを除いて、グローバルな Vue.nextTick と同じです。
index.vuemethods:{ click(){ // DOM はまだ更新されない this.$nextTick(function(){ // DOM が更新された console.log() }); } }
- 投稿日:2020-12-18T12:53:37+09:00
確認ダイアログ(confirm)でユーザー応答を待つ【Nuxt.js】
はじめに
window.confirm('こういうやつ!')と書いてもいいのですが「デザインをこだわるために自作したい!」となって作成しました。
目標
作りたかったものは以下です。
- テキストが変更できる
- 前ページ共通で使いまわせる(各ページに配置などしない)
window.confirm
のように処理を中断してtrue / false
を返せるダイアログを作る
ダイアログコンポーネントを作ります(HTML部分は適当&CSSは省略)。
DialogConfirm.vue<template> <div v-if="isShown" class="dialog"> <p> {{ text }} </p> <div class="dialog__buttons"> <button @click="ok()"> OK </button> <button @click="cancel()"> Cancel </button> </div> </div> </template> <script lang="ts"> import Vue from 'vue'; type Data = { isShown: boolean, text: string, resolve: (v: boolean) => void } export default Vue.extend({ data (): Data { return { isShown: false, text: '', resolve: () => {} }; }, methods: { confirm (text: string): Promise<boolean> { this.text = text; this.isShown = true; return new Promise((resolve: (v: boolean) => void) => { this.resolve = resolve; }); }, ok () { this.reset(); this.resolve(true); }, cancel () { this.reset(); this.resolve(false); }, reset () { this.isShown = false; this.text = ''; } } }); </script>
confirm
メソッドでpromiseの解決をせず、コンポーネント内のdataにresolve
を逃がしています。confirm (text: string): Promise<boolean> { this.text = text; this.isShown = true; return new Promise((resolve: (v: boolean) => void) => { this.resolve = resolve; }); }そしてダイアログ内のボタン押下でresolveしPromiseでラッピングした
boolean
を返します。ok () { this.reset(); this.resolve(true); }, cancel () { this.reset(); this.resolve(false); }Vueは
refs
を使うことで子コンポーネントのメソッドを実行できます。
これを利用しダイアログコンポーネントのconfirm
メソッドを叩くことを想定しています。各ページから呼び出せるようにする
作成したダイアログコンポーネントを適当な
layouts
に配置し、呼び出せるようにします。共通でメソッドを扱いたいため、メソッドを
store
に格納することにしました。
refs
はVueオブジェクトから取るため、layouts
のcreated
でstore
に格納します。
(もっといいやりかたありそう......)default.vuecreated () { this.$store.dispatch('setConfirmMethod', this.confirm); // ストアに格納 }, methods: { confirm(text: string): Promise<boolean> { return (this.$refs.dialog as InstanceType<typeof DialogConfirm>).confirm(text); } }使いかた
これで適当なif文内でストアの
confirm
を呼んでやるとasync click() { if (await this.$store.state.confirm('こういうやつ!'))) { window.alert('confirm!') } }と出てきて確認してくれます。
おわりに
目標としていた3点を満たすコンポーネントを実装できました。
- テキストが変更できる
window.confirm
のように処理を中断してtrue / false
を返せる- 前ページ共通で使いまわせる(各ページに配置などしない)
- 投稿日:2020-12-18T12:20:44+09:00
Vue+Typescript 変更を追跡する(ちゃんとWatchする)ようなオブジェクトの扱い方
前回、Vue + Typescriptで配列の変更を検知しない問題について書きました。
今回はObjectの扱いについてですが、
Objectは配列と比べるとかなり気楽に扱えます。というのも、Typescriptだと未定義・未initializeのオブジェクトを使うことはほぼないので、
公式に記載されているような以下のアウトな形式Vue はプロパティの追加または削除を検出できません。
const vm = { a: 1 } // `vm.a` は今リアクティブです vm.b = 2 // `vm.b` はリアクティブでは"ありません"ですが、これもVue + Typescriptではおそらく以下のように書く必要がありますので
// 型を定義してあげる必要がある。ここで上の例のようにbが抜けていると、そもそもコンパイルが通らない interface Vm { a: number; b: number; } @Component({ components: { } }) export default class Qiita extends Vue { // 初期化してあげないといけない(これはStateとかに持たせたほうが管理が楽です) private vm: Vm = { a: 0, b: 0 }これであれば、変更は正常に検知されます。
Typescriptの強みですね。もちろん、状況によっては
any
型などでオブジェクトを定義しなければならない局面もあるでしょう。
その場合は、公式にあるようにObject.assign
を使えば解決することができます。this.vm = Object.assign({}, this.vm, { a: 1, b: 2 })基本問題になるのは前回解説した配列で、
配列が空でv-forを回していたりすると、検知できない!といったケースが多いです。オブジェクトに配列があるデータなどの扱いは注意しましょう!というお話しでした。
- 投稿日:2020-12-18T09:13:48+09:00
Vue CLIでdebuggerを使えるようにする方法
あらすじ
開発に
debugger
を使う人が少ないせいか、
ESLintでdebugger
を許可する方法がなかなか見つからず
長い間泣きながら開発をしていた問題をやっと解決したので、
簡潔に記録しておきます。1. 全てのファイルで使えるようにする方法
ルートフォルダ直下に作成されている
package.json
の"rules"
の内容に追記する。package.json"rules": { "no-debugger": 0 }2. 特定のファイルで使えるようにする方法
.jsファイルであればファイルの一番上に、
.vueファイルであればscriptタグ内の一番上にコメントを追記する。/* eslint-disable no-debugger */
補足
console.log()
を使えるようにする方法
debugger
だけでなくconsole.log()
も使えなくなる人がいるそうです。
その場合、上記のno-debugger
の部分をno-console
に変えて、
同じように設定することで解決できます。警告を残す方法
0
ではなく1
を指定すると、コンパイル時にエラーではなく警告が発生するようになるそうです。
無視することに慣れてしまうような警告を出す意味があるのかは疑問ですが、活用したい方はどうぞ。Special Thanks
Why I can't use debugger or console.log on my Vue app - Stack Overflow
たった一行!!Vue CLIでconsole.logを有効化する方法 - Qiita
- 投稿日:2020-12-18T04:09:49+09:00
Vue.jsまとめ③「条件付きレンダリング」と「リストレンダリング」
下書きが上限に達しているので仮の状態で投稿しています
v-if="true or false"
条件でレンダリングするかしないか決定するv-else
v-if, v-else-ifの直後に使用
else文と同様に扱うことができるv-else-if
v-ifの直後
else if文templateタグ
不必要な要素を加えずにv-ifを複数の要素に適応させるv-show
display:none;によって非表示にする(v-ifはtemplateタグごとなくなる)v-ifとv-showの使い分け
v-if 切り替えコスト高い
v-show 初期描画コスト高い(最初に全てDOMに追加するから)v-for リストレンダリング
{{ 要素 }} で配列をリスト化
{{ インデックス }} {{ 要素 }} でインデックスも扱える
{{ 要素 }} でオブジェクトをリスト化
{{ インデックス }} {{ キー }} {{ 要素 }} でキーとインデックスも扱える
templateタグとv-forを組み合わせることもできる(基本的には後述のkey属性を使う)
{{ 要素 }} で整数をリスト化
inではなくofでも可能
:key="要素" を一緒に記述することでそれぞれを対応付けることができる(templateタグは使えない)
- 投稿日:2020-12-18T04:09:04+09:00
Vue.jsまとめ② Vue.jsの基礎、テンプレート構文
下書きが上限に達しているので仮の状態で投稿しています
{{}}二重中括弧でdataやmethodsの値を呼び出す
単一の式を書くVueインスタンス内で自分のインスタンスにアクセスするときはthisを使う
v-text
テキストを表示
v-once
一度だけ描画される
後から変更されない
v-html
htmlで表示する
悪戯できるから信頼のあるものだけ扱うv-vind:属性、:属性に省略できる
(属性)を呼び出す
:[属性]で動的に呼び出せる
v-vind="オプジェクト"でまとめて呼び出せるv-on:動作、@動作に省略できる
動作をした瞬間に動作を実行する
イベントオブジェクトを取得できる(methodsにeventの引数を与える、v-on側にはいらない)
v-on:動作="関数(引数)"で引数を持たせられる。イベントと一緒に使うときは$eventも使う
イベント修飾子、v-on:動作.stopで停止、.preventでデフォルトの挙動を阻止
キー修飾子、.enterや.spaceなどで特定のキーのみに反応するようにできる
v-on:[動作]で動的に呼び出せるv-model、双方向データバインディング
テンプレート側からモデル側を変更することができる(通常はモデルからテンプレート)computedプロパティ
動的な表現ができる(dataではどうできな表現はできない)computedとmethodsの違い
computed,参照先の値の変化時に更新、()付けない、推奨
methods,関係ない値の変化時にも更新、()付けるcomputedとwatchの違い
computed,同期処理
watch,非同期処理 computedでできない時に使う
- 投稿日:2020-12-18T04:07:37+09:00
Vue.jsまとめ① はじめに
下書きが上限に達しているので仮の状態で投稿しています
elプロパティ
どこを対象にとるかを設定
dataプロパティ
データを設定v-on
clickなどの動作を処理
methods
いろんな関数を並べる所
- 投稿日:2020-12-18T02:06:43+09:00
gRPC-WebとGoとVue.jsで簡素なチャット
はじめに
何だか良くわからないけどよく聞くgRPC-Webなるものを触りだけでも理解すべく辛うじてチャット呼べそうなものを作ってみました。
gRPC
https://grpc.io/
Protocol BuffersやHTTP2などを利用した環境に依存せず実行できる高パフォーマンスのRPCフレームワーク。Protocol Buffers
https://developers.google.com/protocol-buffers
言語やプラットフォームに依存しない構造データを定義できる。
コンパイルして指定の言語のコードを生成できる。proto
test.proto
service TestService { rpc Login(User) returns (User) {} } message User { string name = 1; string token = 2; }Go
protoファイルからコンパイルしてGoのコードを生成。
test.pb.gofunc (t *testServiceClient) Login(ctx context.Context, in *User, opts ...grpc.CallOption) (*User, error) { out := new(User) err := t.cc.Invoke(ctx, "/test.TestService/Login", in, out, opts...) if err != nil { return nil, err } return out, nil } type User struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` Token string `protobuf:"bytes,2,opt,name=token,proto3" json:"token,omitempty"` }HTTP2
https://http2.info/
HTTP1からの変更例
- テキストからバイナリ
- ステートレスからステートフル
- 1つのTCPコネクションの中で複数のHTTP Requestと複数のHTTP Response
gRPC-Web
https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md
https://grpc.io/docs/platforms/web/basics/
ブラウザの制限によりネイティブのgRCPとは違う実装。envoy
https://www.envoyproxy.io/docs/envoy/latest/
gRCPとgRCP-Webを接続するためには特別なプロキシが必要でデフォルトがenvoy。コード
https://github.com/tayusa/grpc-web-simple-chat
protoファイル定義
syntax = "proto3"; package chat; option go_package = "server/proto"; // よくあるデータ型は定義してあるので読み込む import "google/protobuf/empty.proto"; import "google/protobuf/timestamp.proto"; // やりとりを定義 service ChatService { rpc Login(User) returns (User) {} rpc Logout(User) returns (google.protobuf.Empty) {} rpc SendMessage(Message) returns (Message) {} // 複数の場合、stream使う。 rpc GetMessage(User) returns (stream Message) {} } // やりとりするデータを定義 message Message { // 番号はただの順番 string content = 1; // 自分で定義した型 User user = 2; google.protobuf.Timestamp created_at = 3; } message User { string name = 1; string token = 2; }上記以外にもいろんな書式があって表現力高い。
コンパイルしてコード生成
https://github.com/protocolbuffers/protobuf
からコンパイラをダウンロード。
パッケージマネージャーからインストールもできる。
Arch Linuxなら
$ sudo paman -S protobuf
Goのコードを生成するときは
$ go get -u github.com/golang/protobuf/protoc-gen-go
gRPC-Webのコードを生成するときは
$ npm install -g protoc-gen-grpc-web
言語、出力先をを指定してコンパイル$ protoc chat.proto \ --go_out=plugins="grpc:." \ --js_out=import_style=commonjs:client/src/proto \ --grpc-web_out=import_style=commonjs,mode=grpcwebtext:client/src/proto生成したコード
https://github.com/tayusa/grpc-web-simple-chat/blob/master/server/proto/chat.pb.go
https://github.com/tayusa/grpc-web-simple-chat/tree/master/client/src/protoDocker
Go
FROM golang:latest WORKDIR /server COPY . . RUN go mod download RUN go build -o app CMD ./appJavaScript
FROM node:lts-slim WORKDIR /client COPY . . RUN npm installdocker-compose.yml
3つのコンテナ動かす。
version: '3' services: envoy: image: envoyproxy/envoy:v1.14.1 command: /usr/local/bin/envoy -c /etc/envoy/envoy.yaml -l debug volumes: - ./envoy:/etc/envoy ports: - '10000:10000' links: - 'server' container_name: 'envoy' server: build: context: ./server dockerfile: Dockerfile command: /server/app ports: - '50051:50051' volumes: - ./server:/go/src/server container_name: 'server' client: build: context: ./client dockerfile: Dockerfile command: npm run serve ports: - '8080:8080' volumes: - ./client:/client links: - 'envoy' container_name: 'client'Envoy
$ docker run --rm -it envoyproxy/envoy:v1.14.1 bash
で/etc/envoy/envoy.yamlをコピーして来てポートなどを書き換えて利用します。
.ymlにするとエラーになり時間が消えてなくなります。admin: access_log_path: /tmp/admin_access.log address: socket_address: { address: 0.0.0.0, port_value: 9901 } static_resources: listeners: - name: listener_0 address: socket_address: { address: 0.0.0.0, port_value: 10000 } filter_chains: - filters: - name: envoy.http_connection_manager config: codec_type: auto stat_prefix: ingress_http route_config: name: local_route virtual_hosts: - name: local_service domains: ["*"] routes: - match: { prefix: "/" } route: cluster: chat_service max_grpc_timeout: 0s cors: allow_origin_string_match: - prefix: "*" allow_methods: GET, PUT, DELETE, POST, OPTIONS allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout max_age: "1728000" expose_headers: custom-header-1,grpc-status,grpc-message http_filters: - name: envoy.grpc_web - name: envoy.cors - name: envoy.router clusters: - name: chat_service connect_timeout: 0.25s type: logical_dns http2_protocol_options: {} lb_policy: round_robin # win/mac hosts: Use address: host.docker.internal instead of address: localhost in the line below hosts: [{ socket_address: { address: server, port_value: 50051 }}]Go
https://github.com/tayusa/grpc-web-simple-chat/blob/master/server/main.go
https://github.com/tayusa/grpc-web-simple-chat/blob/master/server/server.go
https://github.com/tayusa/grpc-web-simple-chat/blob/master/server/signal.go生成されたインターフェイス
type ChatServiceServer interface { Login(context.Context, *User) (*User, error) Logout(context.Context, *User) (*empty.Empty, error) SendMessage(context.Context, *Message) (*Message, error) // 複数のレスポンスの場合、戻り値がない。引数にレスポンスのためのコネクション。 GetMessage(*User, ChatService_GetMessageServer) error }protoで生成したGoのインターフェイスに合わせてメソッドを定義していく。
1つのリクエストで1つのレスポンス
func (s *server) Login(ctx context.Context, user *pb.User) (*pb.User, error) { log.Println("Try to logged in.") clientExists := false s.clients.Range(func(_, client interface{}) bool { if value, ok := client.(string); ok && value == user.GetName() { clientExists = true return false } return true }) if clientExists { return &pb.User{}, fmt.Errorf("\"%s\" is already in use.", user.GetName()) } user.Token = genToken() s.clients.Store(user.GetToken(), user.GetName()) log.Printf("%s logged in.\n", user.GetName()) return user, nil }1つのリクエストで複数のレスポンス
func (s *server) GetMessage(user *pb.User, stream pb.ChatService_GetMessageServer) error { s.wg.Add(1) defer s.wg.Done() streamCh := s.createStreamCh(user.GetToken()) defer s.deleteStreamCh(user.GetToken()) for { select { case msg, ok := <-streamCh: if !ok { return nil } // ここでレスポンスしてる。メソッドは終了しない。 if err := stream.Send(msg); err != nil { log.Println("Sending error.") return err } case <-s.exitCh: log.Printf("%s exit.\n", user.GetName()) return nil } } }JavaScript
https://github.com/tayusa/grpc-web-simple-chat/blob/master/client/src/api/client.js
https://github.com/tayusa/grpc-web-simple-chat/blob/master/client/src/components/Chat.vueコンパイルして生成したクライアント
import { ChatServiceClient } from '../proto/chat_grpc_web_pb' export default new ChatServiceClient('http://localhost:10000', null, null)Vueのscript
// クライアント読み込む import client from '../api/client.js' // コンパイルして生成した型を読み込む import { Message, User } from '../proto/chat_pb' // googleが定義してる型を読み込む // import { Empty } from 'google-protobuf/google/protobuf/empty_pb'; import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb'; export default { name: "Chat", data: () => ({ userName: "", userToken: "", message: "", messages: [], stream: null, }), filters: { toLocaleString: (value) => { return (new Date(value.getSeconds() * 1000)).toLocaleString() } }, methods: { login: async function(e) { e.preventDefault(); if (!this.userName) { return; } await client .login(this.getUser(), {}, (err, user) => { if (err != null) { console.log(err); } else { this.userToken = user.getToken(); this.stream = this.fetchMessageStream() } }) }, sendMessage: async function(e) { e.preventDefault(); if (!this.message) { return; } // 生成した型に入れてく。 // セッターが生えてるので利用する。 const message = new Message(); message.setContent(this.message); message.setUser(this.getUser()); const timestamp = new Timestamp(); // ここはどこにも書いてなくて、開発者コンソールで中身を全部読んだ。 timestamp.fromDate(new Date()); message.setCreatedAt(timestamp); await client .sendMessage(message, {}, (err, res) => { if (err != null) { console.log(err); } this.message = ''; }) }, fetchMessageStream: function() { const stream = client.getMessage(this.getUser()); // メッセージが来たら発火するイベント stream.on('data', message => { console.log(message); this.messages = [...this.messages, message]; }); return stream; }, getUser: function() { const user = new User(); user.setName(this.userName); user.setToken(this.userToken); return user; } } };参考
GoでgRPC使う際のクイックスタート
https://grpc.io/docs/languages/go/quickstart/
protocol bufferが生成するGoのコードの説明
https://developers.google.com/protocol-buffers/docs/reference/go-generated
gRCPのGo実装
https://github.com/grpc/grpc-go
ブラウザためのgRCPのJavaScript実装
https://github.com/grpc/grpc-web
Goのライブラリのドキュメント
https://godoc.org/google.golang.org/grpc
GCPのドキュメントにある構成例
https://cloud.google.com/endpoints/docs/grpc/grpc-service-config?hl=ja試す
$ git clone https://github.com/tayusa/grpc-web-simple-chat.git
$ cd grpc-web-simple-chat
$ docker-compose up -d --build
$ chromium http://localhost:8080
サーバーだけ試す
curlは使えないのでgrpc-cli
パッケージマネージャーからインストール
$ sudo paman -S grpc-cli
$ grpc_cli ls localhost:50051 chat.ChatService -l
$ grpc_cli call localhost:50051 ChatService.Login 'name: "John"'
$ grpc_cli call localhost:50051 ChatService.SendMessage 'content: "Hey"'