20201218のvue.jsに関する記事は15件です。

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周りとかネットワークとかもっと汎用性のある記事かけたら良いなーと思う。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.ts
import 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になっているようです。

スクリーンショット 2020-12-18 22.20.28.png

コンポーネント内ナビゲーションガード

ナビゲーションガードも同様に、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>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

通常通りツイートを埋め込む

image.png

image.png

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.ts
declare module "vue-tweet-embed";

作成した d.ts を使うように tsconfig.json の compilerOptionstypeRoots を追加します。

tsconfig.json
{
  "compilerOptions": {
    "typeRoots": ["types"],

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

初学者必見!Headerにある項目を動的に変化させる方法

今回は、ログインしている時だけ「ログアウト」ボタンを表示させたいと思ったので、そのやり方を備忘録として書いていきます。

最後まで見て、よかったらLGTMお願いします!!(今じゃなくていいです)


それでは順を追って説明します。


Vuexの機能を使い、sotre/index.jsのstateにユーザーがログインしているかを判断するauthを設定します。

store/index.js
import 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にある項目を動的に変化させる方法」でした。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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);

ss01.png
取れている様です。

これをヴィシュアライズしていきますが、ヴィジュアライズには
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 );
}

これでひとまず出来上がりです。

アナライザーノードは周波数スペクトラムも取得できるので時間を見てそちらも挑戦してみます。

ss02.png


:christmas_tree: FORK Advent Calendar 2020
:arrow_left: 19日目 Vue.jsのSSGフレームワークのGridsomeはすごいぞ @Kodak_tmo
:arrow_right: 21日目 ml5.js の FaceApi で遊んでみる @kinoleaf

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 stop

Webpackerとは

世界に挨拶することができたので、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.js
import 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.rb
 get '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についてはこちらの記事が参考になるかもです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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.vue
methods:{
    click(){
     // DOM はまだ更新されない
        this.$nextTick(function(){
       // DOM が更新された
            console.log()
        });
    }
}

参考:
API-Vue.nextTick( [callback, context] )

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

確認ダイアログ(confirm)でユーザー応答を待つ【Nuxt.js】

はじめに

Webアプリを開発していて必ず欲しくなる確認ダイアログ。
image.png

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オブジェクトから取るため、layoutscreatedstoreに格納します。
(もっといいやりかたありそう......)

default.vue
created () {
  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!')
  }
}

image.png

と出てきて確認してくれます。

おわりに

目標としていた3点を満たすコンポーネントを実装できました。

  • テキストが変更できる
  • window.confirmのように処理を中断してtrue / falseを返せる
  • 前ページ共通で使いまわせる(各ページに配置などしない)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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を回していたりすると、検知できない!といったケースが多いです。

オブジェクトに配列があるデータなどの扱いは注意しましょう!というお話しでした。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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タグは使えない)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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でできない時に使う

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.jsまとめ① はじめに

下書きが上限に達しているので仮の状態で投稿しています

elプロパティ
どこを対象にとるかを設定
dataプロパティ
データを設定

v-on
clickなどの動作を処理
methods
いろんな関数を並べる所

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

gRPC-WebとGoとVue.jsで簡素なチャット

はじめに

何だか良くわからないけどよく聞くgRPC-Webなるものを触りだけでも理解すべく辛うじてチャット呼べそうなものを作ってみました。

2020-12-18_02:05:20.png

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.go

func (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/proto

Docker

Go

FROM golang:latest

WORKDIR /server
COPY . .
RUN go mod download
RUN go build -o app
CMD ./app

JavaScript

FROM  node:lts-slim

WORKDIR /client

COPY . .
RUN npm install

docker-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"'

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【1から始めるvue.js】ディレクティブについて

ディレクティブとは

「v-」で始まる属性のこと、vue.jsに何らかの指示を行うこと。

ディレクティブについての例は下記を参照

https://012-jp.vuejs.org/api/directives.html

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む