20200401のvue.jsに関する記事は8件です。

[Nuxt.js] アップデート後にスーパーリロードを促す

背景

Nuxt.js(SPA)+APIでWebサービスを公開しているのですが、SPA側をアップデート後に

ユーザ「エラーが出ます / 表示が崩れます」
私「キャッシュを消して再読み込みしてみてください」
ユーザ「直りました」

の流れが良くあります。
(キャッシュの消し方がよくわからないという問い合わせも受けたりします)

キャッシュさせずに毎回読み込ませれば良い話ではあるのですが、
SPAなので初回読み込みに時間がかかるため、
できれば必要な時だけうまく再読み込みさせたい・・・という悩みがありました。

解決方法の概要

以下のようにして解決しました。

処理

  • API側に「期待するクライアントのバージョン」を返すエンドポイントを作成する
  • クライアント側はページ遷移ごとにそのAPIをコールし、
    「期待するクライアントのバージョン」と実際に動作しているクライアントのバージョンを比較
    クライアントのバージョンのほうが古い場合はスーパーリロードを促す

アップデート時の手順

  • アップデートさせたい更新がある場合はクライアント側バージョンをインクリメントしてデプロイ
  • クライアント側のデプロイが完了したらAPI側が「期待するクライアントのバージョン」を上げる
    (先にAPI側を上げると無限スーパーリロード地獄になるので注意)

詳細

簡略化して雰囲気で記載しています。

API側

省略。ここでは http://localhost:8080/version をコールしたら以下のレスポンスが返ってくるものとします。

{
  "expect_client_version": 1
}

私はサボってDBでバージョン番号を管理し、それを返すだけにしています。

Nuxt.js側

ページ遷移ごとにチェックさせたいので、middlewareを使います。
axiosについては設定の記載を省略していますが、
baseURLhttp://localhost:8080が設定されています。

middleware/version.js
const version = 1 // 簡単のためここにクライアント側バージョン直書き

export default async function({ app }) {
  const res = await app.$axios.$get('/version')
  // 期待するバージョン以上なら何もしない
  if (res.client_version <= version) return
  // 反映させるためにスーパーリロードを促す
  if (
    window.confirm(
      '新しいバージョンが配信されているため最新バージョンに更新します。'
    )
  ) {
    location.reload(true)
  }
}

コメントにある通り、クライアントが期待されるバージョンよりも低かった場合はダイアログでスーパーリロードを促します。

nuxt.config.js
export default {
  // ...省略

  router: {
    middleware: ['version']
  },

  // ...省略
}

これでページ遷移ごとにversion.jsの内容が実行されるようになります。

アップデート時

クライアント側のバージョンを上げる

middleware/version.js
const version = 2

// ...省略

クライアント側をデプロイする

この時点で以下の状態になります。

  • APIから返ってくる「期待するクライアントのバージョン」は1
  • ユーザが利用しているクライアントのバージョンは1と2が混在

APIが返す「期待するクライアントのバージョン」を上げる

DB管理ならupdateなど。

{
  "expect_client_version": 2
}

を返すようにします。
これでバージョン1のクライアントを使用している人だけ再読み込みを促され、
再読み込みするとバージョン2になります。

余談

要件次第ですが、APIのレスポンスに常に「期待するクライアントのバージョン」を含めて、
axiosの呼び出しの度にチェックするのもありだと思います。

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

vue-unity-webglの使い方メモ

Vue.jsからUnityのWebGLビルドを扱いたい

Vue.jsからUnityのWebGLを扱いたいタイミングがあったので調べるとvotetake/vue-unity-webglがあることを知り使ってみました。

二つともVue.jsでUnityのWebGLビルドを扱った記事です。

ただ、二つとも詳しい使い方は書いてなかったので改めて記事に残しておこうと思い記事しました。

導入と準備

Unityで吐いたWebGLをPWAで動かしてみたの記事は少し古いためnpmで扱っていましたが箱庭の音を作る際にyarnで管理する必要があることを知ったのでyarnを使って導入します。

yarn add vue-unity-webgl

で大丈夫です

そしてUnityをビルドして成果物をvueのプロジェクトの public ディレクトリに入れておきます。

index.html
<script src="<%= htmlWebpackPlugin.files.publicPath %>unitybuild/TemplateData/UnityProgress.js"></script> 
<script src="<%= htmlWebpackPlugin.files.publicPath %>unitybuild/Build/UnityLoader.js"></script> |

index.htmlでunityのjsを呼び出す用意をして完了です。

.vueで扱う際のパラメータ

基本的にvue-unity-webglのREADME.mdに書いてある通りで問題はないのですが中身を見ると追加でパラメータが渡せる様だったので残しておきます。

<unity
   src="unitybuild/Build/unitybuild.json"
   v-bind="{
      width: 640,
      height: 480,
      hideFooter: true,
      externalProgress: true,
      module: { TOTAL_STACK: 6 * 1024 * 1024 }
   }"
   unityLoader="unitybuild/Build/UnityLoader.js"
></unity>
  • width heightはわかりやすくunityを表示する大きさになります。

  • hideFooterはUnityのWebGLにデフォルトでついているフルスクリーン機能などを使わない設定です
    本当に非表示にする場合はcssの方で隠せば大丈夫のはずです。

  • externalProgressはfalseだとカスタムのprogress操作ができるそうですがUnityデフォルトのProgressを使いたい場合にtrueで設定します。
    カスタムのProgress操作はvue-unity-webglのREADME.mdにあるので興味がある人は確認してみてください。

  • moduleは扱えるスタック領域などの値を変えれる設定のようです。ここに関しては Unity:WebGLでメモリエラーに苦しんだ話を参考に設定させていただきました。

おわり

vue-unity-webglについて扱う話は以上になります。基本的にはREADME.mdをみつつ操作したら大丈夫だと思いますが中身を覗くと追加で設定できるパラメータがあったのでメモがてら記事にしました。
そしてもっとカスタム的に扱いたい場合はvue-unity-webglを参考にすれば自前で呼び出してコントロールも可能かなと思っています。Vueじゃなくても扱えるような中身になっていると思うので興味ある人はのぞいて見てください。

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

Firebaseホスティングで公開しているSPAのサイトをGoogle検索にひっかかるようにする(SSRなしで試す)

先日公開した「ログ情報共有サービスLogCrow」にて、登録されたログデータに公開フラグがついているものをGoogle検索からでもひっかかるようにしてみました。

LogCrowのシステム構成は以前の記事で紹介した通り、フロントエンドはVue.jsで開発し、Firebaseホスティングを使って公開をしています。バックエンドはGAEのGoのスタンダード環境を利用しています。

元々SPAで単一のページ上でコンテンツを切り替えてログ情報を表示していたのですが、当然それだとGoogleの検索結果としてはトップページしかインデックスされません。各登録ログの情報がインデックス化されるようにチャレンジしてみました。
ついでに、各登録ログの情報をFacebook等で共有したときにその情報がうまく共有されるようOGP設定も入れてみました。

試した流れは以下です。

  • 各ログのページに個別のURLが割り当てられるようページ構成を変更
  • sitemap.xmlをバックエンドで自動作成し、登録済みログ情報のクローリング依頼リクエストを投げられるにする
  • HTMLのヘッダーのメタ情報にOGP設定を追加(Firebase functionsを利用)
  • Cloud Schedulerで定期的にsitemap.xmlをクロールするようリクエストを行う

以下、順番に説明します。

各ログのページに個別URLを設定

Vue routerの設定で以下のように/log/<ログID>というパスが来たときにLogViewを表示するようなルートを設定します。
このとき、Routerのmode指定を'history'に指定するのが重要です。
デフォルトの設定の場合、'hash'モードになっており、/#/log/<ログID>という感じで実際のpathの手間に#が挟まる形でURL遷移されてしまいます。
普通にブラウザで画面遷移して見る分にはこれでも問題ないのですが、クローラーでチェックされる際、#以降のURLパスが無視されてしまい正しくインデックス化されないです。
なので、historyモードにしてURLパスを認識してもらえるようにします。

・・・略
Vue.use(Router)

let router = new Router({
  mode: 'history',
  routes: [
    {
      path: '/',
      name: 'Main',
      component: Main
    },
    {
      path: '/new',
      name: 'New',
      component: New,
      meta: { requiresAuth: true }
    },
    {
      path: '/log/:logid',
      name: 'LogView',
      component: LogView
    },
・・・略
  ]
})

LogViewのコンポーネント側ではthis.$route.params.logidという感じでパスに含まれるlogidの情報を取得して表示内容を切り替えるような実装をすればOKです。

sitemap.xmlをバックエンドで自動作成し、登録済みログ情報のクローリング依頼リクエストを投げられるにする

各ログのページがどこかからリンクされていてクローラーが順次辿れるようになっていればsitemapをあえて作る必要はないかと思いますが、今回のLogCrowでは登録されたログは検索することで表示されるような仕様となっているため、クローラーがたどるようなことができません。
そこで、sitemapを作ってログの個別ページをクローリング対象にしてもらえるようにします。

ログのデータは随時登録され変更されていくため、静的なsitemap.xmlを作れば良いというわけではありません。
そのため、今回はGAE上で動かしているバックエンド側でsitemap.xmlを返す実装をしました。

package main

import (
        "time"
        "fmt"
        "net/http"
        "github.com/labstack/echo"
        "google.golang.org/appengine"
        firebase "firebase.google.com/go"
        "google.golang.org/api/iterator"
)

type urlset struct {
        Namespace string `xml:"xmlns,attr"`
        Urls []SitemapUrl `xml:"url"`
}

type SitemapUrl struct {
        Loc string `xml:"loc"`
        Lastmod  time.Time `xml:"lastmod"`
}

func main() {
        if err := profiler.Start(profiler.Config{
                Service:        "logcrow",
                ServiceVersion: "1.0.0",
        }); err != nil {
                fmt.Println("Profiler error")
        }
        //e := echo.New()
        e.Use(middleware.Logger())
        e.Use(middleware.Recover())
        e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
                AllowOrigins: []string{"https://logcrow.firebaseapp.com"},
                AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept, echo.HeaderAuthorization},
                AllowMethods: []string{"GET", "POST", "DELETE", "PUT", "OPTIONS"},
        }))
        private_g := e.Group("", AuthMiddleware)
        public_g := e.Group("")
        public_g.GET("/log", GetLog)
        public_g.GET("/sitemap.xml", GetSitemap)
        appengine.Main()
}
・・・
func GetSitemap(c echo.Context) error {
        r := c.Request()
        projectID := "..."
        conf := &firebase.Config{ProjectID: projectID}
        ctx := appengine.NewContext(r)
        app, err := firebase.NewApp(ctx, conf)
        if err != nil {
                fmt.Printf("Firebase Error\n")
                fmt.Println(err)
        }
        client, err := app.Firestore(ctx)
        if err != nil {
                fmt.Printf("Firestore Error\n")
        }
        defer client.Close()

        iter := client.Collection("xxx").Documents(ctx)
        urlset := urlset{}
        urlset.Namespace = "http://www.sitemaps.org/schemas/sitemap/0.9"
        for {
                doc, err := iter.Next()
                if err == iterator.Done {
                        break
                }
                if err != nil {
                        fmt.Println("Error")
                        fmt.Println(err)
                }
                sitemapUrl := SitemapUrl{}
                logEntry := LogEntry{}
                doc.DataTo(&logEntry)
                if logEntry.UpdatedAt.IsZero() == true {
                        continue
                }
                sitemapUrl.Loc = "https://logcrow.firebaseapp.com/log/" + doc.Ref.ID
                sitemapUrl.Lastmod = logEntry.UpdatedAt
                urlset.Urls = append(urlset.Urls, sitemapUrl)
        }

        return c.XML(http.StatusOK, urlset)
}

こんな感じでfirestoreから取得したデータの内容を元にXMLを組み立て、レスポンスを返します。
sitemap.xmlにアクセスすると以下のような感じのxmlが返却されるようになります。

<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>
      https://logcrow.firebaseapp.com/log/2yjS48xJDeYtUtK76Gx1Rw-T2SVg6FQ7e6nK5sXY3FA
    </loc>
    <lastmod>2020-01-16T06:11:30.120946Z</lastmod>
  </url>
  <url>
    <loc>
      https://logcrow.firebaseapp.com/log/36KHVJPQYbCEpGRjw3vL-01gpWT0bLV8-qjLgwjeDMM
    </loc>
    <lastmod>2020-02-17T05:27:59.943381Z</lastmod>
  </url>
  <url>
    <loc>
      https://logcrow.firebaseapp.com/log/6ng3kroRQ8j0uKRM-qh3kXKyNy0r3lhNb5p7XnUidMc
    </loc>
    <lastmod>2020-03-19T11:49:28.388672Z</lastmod>
  </url>
</urlset>

このsitemapをGoogleにリクエストするにはGoogle Search Consoleから登録するか、robots.txtに記載して公開するか、pingで登録するかで依頼することができます。

pingで投げる場合は以下のような感じです。

$ curl -XGET http://www.google.com/ping?sitemap=https%3A%2F%2Flogcrow.appspot.com%2Fsitemap.xml

いつクローリングされるかは不明なので、しばらく待った後、Google検索でひっかけてみます。
すると以下のような感じで検索でひっかけることが可能です。

Google_Search_Before.png

実際にインデックス登録されているかどうかはGoogle Search Consoleから確認が可能です。

Google_Search_Console.png

ここで、検索結果をみると、タイトルがすべてlogcrowとなっており、ログのページの内容毎にタイトルを変えるといったことができていないのがわかります。
また、このログのURLをFacebook等で共有したときの表記としてもindex.htmlに記載されたヘッダー情報を元にした内容になってしまいます。

HTMLのヘッダーのメタ情報にOGP設定を追加(Firebase functionsを利用)

そこで、ヘッダーのメタ情報にOGP設定を追加してみます。
Vue.jsのSPAの場合、ベースとなるhtmlに固定でヘッダーを仕込むのはできますが、ページ毎に動的にヘッダーを変えるのはちょっと手間がかかります。
今回、以下の2パターンのやり方を試しました。

  1. mountedでページ表示時に強引にヘッダーのメタ情報をVue.jsで書き換える方法
  2. 動的にヘッダー情報を仕込んだhtmlを返すFirebase functionsを設定する方法

1.についてはこちらで紹介されている方法で実施。
ブラウザ上で確認すると、確かにメタ情報が設定されていることがわかります。
しかし、この方法だと、URLをFacebook等でシェアしようとしたときに、メタ情報が書き換えられる前の情報で認識されてしまいうまくいきません。
そこで、2.の方法で/log/<ログID>でアクセスが来たときに、functionsを実行させ、メタ情報を仕込んだHTMLだけを返し、そのHTMLの中からwindow.locationで実際に見せたいVue.jsのログページに飛ばすような構成にしてみました。
functionで情報を返すというワンステップ処理が増えてあまり綺麗な感じではないのですが、これで処理させるとOGPの設定が効いてきます。

Facebook_share_debugger.png

これでGoogleのクローラーでインデックス化されると以下のような感じで、タイトルとかが設定されて結果が出てきます。

Google_Search_After.png

Cloud Schedulerで定期的にsitemap.xmlをクロールするようリクエストを行う

最後にsitemap.xmlは先程、手動でリクエストを投げましたが、定期的に自動で実施されるようにし、新しいログの登録があれば自動的に検索にヒットするようにします。
時間ベースでスケジューリングして実行するなら、GCPのCloud Schedulerが便利です。
cronの形式で時間指定して処理を実行できます。特に今回のようなHTTPのリクエストを投げるだけなら、Schedulerの定義だけで完結します。

CloudScheduler.png

まとめ

Firebase上でホスティングしていることもありサーバサイドレンダリングせずに済ませる方法を探して色々と試してなんとか動かせました。しかし、ちょっと強引な方法です。
特にOGP設定周りはサーバサイドレンダリング使わないとかなり手間かかります。必要に応じた検討が必要そうです。

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

Vue.js テンプレート制御ディレクティブ まとめ

はじめに

Vue.jsでTO DOアプリを作るの続きです。

今回はディレクティブを深掘りしていきます。

v-once

  • 初回だけテキストバインディングを行う。
  • 初回以降は静的なコンテンツとして扱う。
  • 描画更新のパフォーマンスを上げたいときに利用できる
var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue.js!'
  },
  methods: {
    click(e) {
      // 文字列を反転するメソッドを定義
      this.message = this.message
        .split('')
        .reverse()
        .join('')
    }
  }
})

以下はv-onceを定義しているので、初回だけテキストバインディングを行っている。

初回以降は静的なコンテンツとして扱われるので、ボタンをクリックしてもメソッドが発火しない。

<p v-once> <!-- v-onceを定義 -->>
  {{ message }} <!-- Hello Vue.js -->
</p>
<button v-on:click="clickHandler">
    文字が反転
</button>

v-pre

  • 要素と全ての子要素のコンパイルをスキップする。
  • 生のマスタッシュタグが表示される。
<p v-pre>
  {{ message }} <!-- {{ message }} -->
</p>

v-html

  • プレーンなHTMLを挿入する。
  • 指定した要素のinnerHTMLを更新できる。
  • 主にサーバーサイドから取得したHTMLを表示したいときに使う。

dataオプションのプロパティにHTMLをセットする。

data: {
  messageHtml: 'Hello <span style="color:red;">Vue.js!</span>'
}

v-htmlディレクティブで展開する。

<p v-html="messageHtml"></p>

※XSS脆弱性を引き起こす恐れがあるため、サービスを利用するユーザーが入力したコンテンツには使用しないこと。

v-cloak

ページを表示開始してから、インスタンスの作成が終わるまでにマスタッシュタグなどのコンパイル前にテンプレートが表示されてしまうのを防ぐ。

<p v-cloak> <!-- v-cloakを定義 -->
  {{ message }}
</p>

v-cloakに対してdisplay: noneを設定する。

[v-cloak] {
  display: none;
}

v-text

dataオプションのプロパティをマスタッシュ構文以外で表示したい時。

使い道あんまりなさそう。

<p v-text="message"></p> <!-- Hello Vue.js! -->

JavaScript式

データバインディング内部ではJavaScript式を利用する事ができる。

data: {
  message: 'Hello Vue.js!',
  number: 100,
  ok: true
}
<p>
  {{ number +  1}} <!-- 101 -->
  {{ message.split('').reverse().join('') }} <!-- !sj.euV olleH -->
</p>

フロー制御は三行演算子を利用する。

<p>
  {{ ok? 'YES' : 'NO' }} <!-- YES -->
</p>

以下は式では無く文なのでエラーになる。

<p>
  {{ var x = 1 }} <!-- error -->
</p>

フィルタ(ローカル)

Vue.jsでは式の終わりにフィルタを追加することができる。

data: {
  price: 29800 //dataオプションにプロパティを設定
},
filters: { // filterにメソッドを追加
  numberFormat(value) {
    return value.toLocaleString()
}

{{ 式 | フィルタ }}でfilterを実行する

<p>
  {{ price | numberFormat(price) }} <!-- 29,800 -->
</p>

v-bindディレクティブで使用する場合

<input type="text" v-bind:value="price | numberFormat(price)">

フィルタ(グローバル)

フィルタはグローバルにも定義できる。

グローバルに定義するにはVueインスタンスを生成するよりも前に記述する。

// グルーバルフィルタを定義
Vue.filter('numberFormat', function(value) {
  return value.toLocaleString()
})

// Vueインスタンス生成
var app = new Vue({
  el: '#app',
  data: {
    price: 29800
  }
})

フィルタの連結

フィルタは複数連結できる。

フィルタで加工した返り値に対して、さらにフィルタをかけることができる。

<p>
  {{ A | filterX(A) | filterY(A) }}
</p>

フィルタの引数

フィルタで引数を利用する。

例として、長いテキストを省略して...を末尾に付与するフィルタを作成する。

その際、表示する文字列末尾に付与する文字を引数で指定できるようにする。

dataオプションに長い文字列を持ったtextプロパティを用意する。

text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt

続いてフィルタを定義する。

JavaScriptのメソッドsubstringは第一引数が開始位置、第二引数が抽出する文字数を指定する。

以下のフィルタは、引数で文字列抽出する文字数連結する文字を受け取る。

Vue.filter('readMore', function(text, length, suffix) {
  return text.substring(0, length) + suffix
})

実行してみる。

<p>
  <!-- textは引数に含まないことに注意 -->
  {{ text | readMore(10, '...') }} <!-- Lorem ipsu... -->
</p>
<p>
  {{ text | readMore(5, '***') }} <!-- Lorem*** -->
</p>

v-bind省略記法

v-bindは省略記法が存在する。

プロジェクト内で統一するのが望ましい。

以下は同じ結果が出力される。

<!-- 完全な構文 -->
<a v-bind:href="url">

<!--  省略記法-->
<a :href="url">

更新履歴

:zap:Vue.jsの基本的な使い方まとめ
:zap:Vue.jsでTO DOアプリを作る
:zap:Vue.js テンプレート制御ディレクティブ まとめ :point_left:今ココ
:zap:Vue.js 算出プロパティとメソッドの違い

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

初めてのVue.js備忘録 vol.01

はじめに

今回は、エンジニアになって約半年の私がjavascriptのフレームワークであるVue.jsを使って開発を行ったので、そのVue.jsの基本的な記述について備忘録としてまとめます。

データバインディング

テキストのバインディング

  • データのバインディグのもっとも基本的な形 マスタッシュ {{ }}を利用したもの
  • 以下のような使い方ができます。
App.vue
 <template lang="pug">
    #app
        span {{ msg }} //hello
 </template>
 <script>
    export default {
        data(){
            return{
                msg:"hello"
        }
    }
 </script>
App.vue
<template lang="pug">
  .app
    span {{inc}} //numに +1された数字がバインディングされる
</template>
<script>
export default {
  data(){
    return{
      num:10
    }
  },
  computed:{
    inc(){
      return this.num + 1
    }
  }
};
</script>

属性のバインディング

  • {{ }}マスタッシュはHTML属性の内部では使えない
  • 代わりに v-bindディレクティブを使用する
App.vue
  <template lang="pug">
    #app
        span(v-bind:class="dynamicClass") {{ msg }}
  </template>
  <script>
    export default {
        data(){
            return{
                msg:"hello",
                dynamicClass:"hoge"
        }
    }
  </script>
  <style>
  .hoge{
    display:inline-block;
  }
  .fuga{
    display:none;
  }
  </style>

dynamicClassの値をhoge⇨fugaに変更すると、spanのクラスが変更され、非表示になります。

親子間のデータの受け渡し

  • Vue.jsでの親子間のデータの受け渡しは、『props down, events up』
  • 親はプロパティを経由してデータを子に伝える
  • 子はイベントを経由して親にメッセージを送る

という認識でいます。

以下のように子コンポーネントでpropsオプションを指定します。

CustomInput.vue
<template>
...
input(type="text" :placeholder="customPlaceholder" @input="updateValue")
...
</template>
<script>
export default = {
 props:{
  customPlaceholder:{
   type:String,
   default:""
  }
 },
 methods: {
  updateValue (e) {
   this.$emit('input', e.target.value)
   this.$emit('change', e.target.value)
   }
  }
}
</script>
CustomInput.vue
this.$emit('input', e.target.value)

この部分で子コンポーネントからinputに入力されたvalueを親に渡しています。

親コンポーネントでは、子に渡すデータを指定、inputEmailの値が子のコンポーネントに渡されます。

Form.vue
<template>
 custom-input(:customPlaceholder="inputEmail")
</template>
<script>
import CustomInput from "./components/CustomInput"
export default = {
  components:{
   CustomInput
 },
 data(){
  return{
   inputEmail: "メールアドレスを入力してください"
  }
 }
}
</script>

だいぶ掻い摘んで書いてみましたが、書き方どうだっけ?的なときのために。

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

Vue.js公式チュートリアルをゆっくり読んでいく2

前置き

前回に引き続き、Vue.jsの公式チュートリアルをゆっくり読んでいきます。
チュートリアルに沿ってはいますが、サンプルはよりわかりやすく単純なものに一部変えているところもあります。

前回は主にVue.jsのリアクティブシステムに触れました。今回は、Vue.js上では「ディレクティブ」と呼ばれているVue.js特有の属性を見ていきたいと思います。

Vue.jsのバージョン:v2.6.11

今回のチュートリアル参照箇所

はじめに - 宣言的レンダリング

はじめに - 条件分岐とループ

はじめに - ユーザー入力の制御

v-bind:要素の紐付け

前回はVueインスタンスのdataプロパティに持たせたデータオブジェクトの内容を、elプロパティと紐付けた「HTML要素の中で{{ message }}という形式で表示させました。

<div id="app">
    {{ message }}
</div>

<script>
    let vm = new Vue({
        el: '#app',
        data: {
            message: 'Hello Vue!'
        }
    });
</script>

この「HTML要素の中で」ということですが、HTML要素の属性部分には効くのでしょうか?
次のようにdataオブジェクトのmyFavoriteColorStyleに持たせたcssを#app要素のstyle属性に反映できるかやってみます。

<div id="app" style="{{ myFavoriteColorStyle }}">
    {{ message }}
</div>

<script>
    let vm = new Vue({
        el: '#app',
        data: {
            message: 'Hello Vue!',
            myFavoriteColorStyle: 'color:red',
        }
    });
</script>

image.png
image.png

style属性は反映されず、警告がでてしまいました。HTML属性部分には効かないようです。
しかし、Vue.jsが親切に警告内でどう書けばいいか教えてくれています。
For example, instead of <div style="{{ val }}">, use <div :style="val">.
これで書き直してみます。

<div id="app" :style="myFavoriteColorStyle"> <!-- {{ }} で囲まない -->
    {{ message }}
</div>

<script>
    let vm = new Vue({
        el: '#app',
        data: {
            message: 'Hello Vue!',
            myFavoriteColorStyle: 'color:red',
        }
    });
</script>

image.png
image.png

今度は警告もでず、スタイルが適用されました。
ここで使用した:styleですが、正式にはv-bind:styleと書きます。:styleは省略記法です。

v-bind属性はVue.js上ではディレクティブと呼ばれており、v-bind:属性名とすることで、
その属性に対してデータオブジェクトの内容を紐付けることができます。
もちろんこれもリアクティブなものなので、chromeのデベロッパーツールのコンソールで
vm.myFavoriteColorStyle = "color:blue"と打つと、すぐにそれが反映されます。

myReactive.gif

v-if:条件分岐

v-ifディレクティブは要素の表示、非表示を切り替えることができます。

<div id="app" v-if="seen">
    {{ message }}
</div>

<script>
    let vm = new Vue({
        el: '#app',
        data: {
            message: 'Hello Vue!',
            seen: true,
        }
    });
</script>

v-if.gif

v-ifディレクティブは厳密には、要素の表示・非表示を切り替えるのではなく、要素を削除・再生成しています。
vm.seen = falseしたときにHTML要素を確認してみます。

v-if2.gif

要素の表示・非表示を切り替えるディレクティブは、v-ifの他にv-showもあります。
こちらは、要素のスタイルをdisplay:noneにするだけなので、要素自体は削除されません。
v-ifは要素を削除・再生成する一方、v-showはスタイルを切り替えるだけなので後者のほうが高速に動作します。

<div id="app" v-show="seen">
    {{ message }}
</div>

<script>
    let vm = new Vue({
        el: '#app',
        data: {
            message: 'Hello Vue!',
            seen: true,
        }
    });
</script>

v-show.gif

ところでチュートリアルにも記載がありますが、Vue.jsは上記のような要素の更新が行われたときにトランジション効果を
簡単につけることができます。例えばさきほどのv-ifディレクティブの例に、フェード効果をつけてみます。

<div id="app">
    <!-- トランジション効果をつけたい要素をVue.jsに予め用意されているtransitionタグ(コンポーネント)で囲みます -->
    <transition name="fade"> <!-- name属性で指定した名前を後述のstyleで使います -->
        <p v-if="seen">{{ message }}</p>
    </transition>
</div>

<style>
    /* 「transitionタグのname属性で指定した名前 + "-enter"」で要素が追加される直前、
       「transitionタグのname属性で指定した名前 + "-leave-to"」で要素が削除された直後を表すクラス名 */
    .fade-enter, .fade-leave-to {
        opacity: 0; /* 透明状態 */
    }
    /* 「transitionタグのname属性で指定した名前 + "enter-active"」で要素の追加中
       「transitionタグのname属性で指定した名前 + "leave-active"」で要素の削除中を表すクラス名 */
    .fade-enter-active, .fade-leave-active {
        transition: opacity .5s; /* 0.5秒かけて透明度を変化させる */
    }
</style>

<script>
    let vm = new Vue({
        el: '#app',
        data: {
            message: 'Hello Vue!',
            seen: true,
        }
    });
</script>

v-if_transition.gif

参考:Enter/Leave とトランジション一覧 — Vue.js

v-for:ループ

v-forは配列の内容を取り出して表示してくれます。

<ul id="app">
    <li v-for="message in messages">
        {{ message }}
    </li>
</ul>

<script>
    let messages = ['Hello','v-for','directive'];
    let vm = new Vue({
        el: '#app',
        data: {
            messages: messages,
        }
    });
</script>

v-for.gif

もちろんリアクティブ。

v-on:イベントハンドリング

v-onディレクティブはその要素に対してイベントリスナを設定してくれます。

<button id="app" v-on:click="changeMessage">
    {{ message }}
</button>

<script>
    let vm = new Vue({
        el: '#app',
        data: {
            message: 'Hello Vue!',
        },
        methods: {
            changeMessage: function() { this.message = 'Hello Vue World!' },
        }
    });
</script>

v-on.gif

ところで、これをVue.jsを使わないで実現するとどうでしょうか。

<button id="app" onclick="changeMessage()">
    Hello Vue!
</button>

<script>
    function changeMessage() {
        document.getElementById("app").innerHTML = 'Hello Vue World!';
    }
</script>

Vue.jsを使わなくてももちろんできます。むしろこれくらいならこちらのほうが全然シンプルです。
ではVue.jsを使うメリットは何でしょうか。次の場合を考えてみます。

例えば、button要素の中のテキストを赤くしたとします。

<button id="app" onclick="changeMessage()">
    <span style="color:red">Hello Vue!</span>
</button>

<script>
    function changeMessage() {
        document.getElementById("app").innerHTML = 'Hello Vue World!';
    }
</script>

v-on_not_use.gif

せっかく赤くしたテキストがクリックすると黒に戻ってしまいました。
innerHTMLは要素内のHTMLを設定するプロパティなので、テキスト部分だけ変更したい場合は、
例えば次のようにスクリプト部分を修正する必要があります。

<button id="app" onclick="changeMessage()">
    <span style="color:red">Hello Vue!</span>
</button>

<script>
    function changeMessage() {
        document.querySelector("#app span").innerHTML = 'Hello Vue World!';
    }
</script>

v-on_not_use2.gif

これでうまくいきました。でもこれは、「innerHTMLが要素内のHTMLを設定するプロパティであること」を知っていること、
それを知った上で、要件を満たすために、「querySelectorを使う」「Hello Vue!部分だけを書き換えるためにid="app"要素の
中のspan要素を指定するセレクタを記載する」など、DOMを操作するためのいくつかの追加の知識が必要です。

これをVue.jsで実現すると、

<button id="app" v-on:click="changeMessage">
    <span style="color:red">{{ message }}</span>
</button>

<script>
    let vm = new Vue({
        el: '#app',
        data: {
            message: 'Hello Vue!',
        },
        methods: {
            changeMessage: function() { this.message = 'Hello Vue World!' },
        }
    });
</script>

スクリプト部分は一切変更せずにできました。これはVue.jsがDOM操作をやってくれるからです。
Vue.jsを使うことで、DOM操作を気にせず、ロジックのみに集中できることがVue.jsのメリットの一つでもあります。

今回はここまでです。

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

VSCodeでVueの補完ができなくなった(Vetur)

VSCodeでVueのテンプレートファイルを編集しようとしたところ補完が動かない。困る。
VeturのOutputを見たところ

Error: Cannot find module 'vscode-css-languageservice/lib/umd/data/browsers'

の文字が。

どうやらGitHubのissueによればvue-language-server v0.0.67が依存しているモジュールvscode-css-languageserviceに破壊的変更があったことが原因の模様。しかしながらvue-language-serverはここ数か月アップデートがないのでアップデートでの対応不可。

時間がなかったのでこちらを参考にビルド済みのソースコードを直接変更して対応。

勘弁してよ…。

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

Laravel7.xにVue-Routerを実装してSPAを実装してみる (2)

概要

今回はLaravel側のコントローラと通信してデータ取得できるように構成するのが目標
POSTメソッドを使用してみる

コントローラを作成

指定した配列を返す簡単なコントローラを作成

app/Http/Controllers/PageController.php
<?php
namespace App\Http\Controllers;

use Illuminate\Routing\Controller as BaseController;
use Request;

class PageController extends BaseController
{
    // リポートページのコントローラ
    public function report(Request $request)
    {
        return [
            'title' => 'report',
            'contents' => 'report contents blahblah'
        ];
    }

    // フォームページのコントローラ
    public function form(Request $request)
    {
        return [
            'title' => 'form',
            'contents' => 'form contents blahblah'
        ];
    }
}

Laravel側のラウトを設定

PageControllerの「report() / form()」メソッドのラウトを設定するコードを追加
確認のため「Route::any」で書いているが、元々は「Route::post」の方が正しいー

routes/web.php
// ページの表示処理を行う
Route::prefix('pages')->group(function () {
    Route::any('report', 'PageController@report');
    Route::any('form', 'PageController@form');
});

POST送信のためCSRF_TOKENを設定

ヘッダにCSRF_TOKENのメタデータを追加

resources/views/welcome.blade.php
<head>
    ……
    <meta name="csrf-token" content="{{ csrf_token() }}">
    ……
</head>

Axios通信でCSRF_TOKENを設定するコードを追加

resources/js/bootstrap.js
window.axios.defaults.headers.common = {
    'X-Requested-With': 'XMLHttpRequest',
    'X-CSRF-TOKEN' : document.querySelector('meta[name="csrf-token"]').getAttribute('content')
};

各VueコンポーネントでAxiosを設定

リポートページ

resources/js/pages/report.vue
<template>
    <div class="container">
        <div class="row justify-content-center">
            <div class="col-12">
                <div class="card">
                    <div class="card-header">{{ responseArr.title }}</div>
                    <div class="card-body">
                        {{ responseArr.contents }}
                    </div>
                </div>
            </div>
        </div>
    </div>
</template>
<script>
    module.exports = {
        data: function() {
            return {
                responseArr: {}
            }
        },
        mounted() {
            const self = this;
            const url = '/pages/report';
            axios.post(url).then(function(response){
                self.responseArr = response.data;
            });
        }
    }
</script>

フォームページ

resources/js/pages/form.vue
<template>
    <div class="container">
        <div class="row justify-content-center">
            <div class="col-12">
                <div class="card">
                    <div class="card-header">{{ responseArr.title }}</div>
                    <div class="card-body">
                        {{ responseArr.contents }}
                    </div>
                </div>
            </div>
        </div>
    </div>
</template>
<script>
    module.exports = {
        data: function() {
            return {
                responseArr: {}
            }
        },
        mounted() {
            const self = this;
            const url = '/pages/form';
            axios.post(url).then(function(response){
                self.responseArr = response.data;
            });
        }
    }
</script>
  • 違うのはmounted()で指定するURLのみ

結果確認

npm run dev
php artisan serve
  • リポートページ
    image.png

  • フォームページ
    image.png

コントローラからデータを取得していることを確認した!

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