20200403のvue.jsに関する記事は13件です。

Vue.js用カレンダーライブラリ「v-calendar」の最新バージョンがimport出来ない件

プロジェクトにおいてRailsのWebpackerでVue.jsを使っていて、カレンダー機能を実装するために色々調べているとv-calendarというライブラリがあることを知った。
早速使ってみようと思って公式ドキュメントの通りインストールして設定下が、謎のエラーが発生したので記事にすることにした。
結論から言うと、エラーの原因はよく分からず、ただ後述するようにバージョンを下げてインストールするとうまく動いたので、解決策として記載する。

Version

Vue.js 2.6.11
Ruby on Rails 6.0.0

インストール

v-calendar公式ドキュメントに従ってyarnを用いてv-calendarをインストールする。

yarn add v-calendar@next

するとv-calendarの1.0.0-beta.23がインストールされた。

package.json
{
  "name": "...",
  "private": true,
  "dependencies": {
    "@coreui/coreui": "2.1.6",
    "@rails/actioncable": "^6.0.0-alpha",
    "@rails/activestorage": "^6.0.0-alpha",
    "@rails/ujs": "^6.0.0-alpha",
    "@rails/webpacker": "^4.0.7",
     // 中略
    "turbolinks": "^5.2.0",
    "v-calendar": "^1.0.0-beta.23",
    "vue": "^2.6.11",
    "vue-loader": "^15.9.1",
    "vue-template-compiler": "^2.6.11",
    "waypoints": "^4.0.1"
  },
  "version": "0.1.0",
  "devDependencies": {
    "webpack-dev-server": "^3.10.3"
  }
}

そしてwebpackerで使用するためにimportしてプラグインとして登録する。

app/javascript/packs/application.js
import Vue from 'vue'
import App from '../app.vue'
import VCalendar from 'v-calendar'
Vue.use(VCalendar)

document.addEventListener('DOMContentLoaded', () => {
  new Vue({
    el: '#vue',
    render: h => h(App)
  })
})

公式ドキュメントに従ってここまで設定してローカルサーバを起動したところ、コンソール上で"export 'default' (imported as 'VCalendar') was not found in 'v-calendar'と怒られてる。
ブラウザでも、Uncaught TypeError: Cannot read property 'mixin' of undefinedと怒られている。
公式通りにやってるのに何でや?と思って色々調べてみたがよく分からず。。。詰みかけていたところでこんなIssueを発見した。
"export 'default' (imported as 'Calendar') was not found in 'v-calendar/lib/components/calendar.umd'

わしの詰まってるエラーとほぼ同じや!!!きっとここに解決策が書いてあるはず!
と思って読んでいくと、肝心の開発者からの返答がない。
他のメンバーからも、はよ返事しろや。と怒られている。
(原文はAny responses to this?)

解決策

詰んだ………と思ってぼーっとIssueを眺めていたら、質問者のv-calendarのバージョンがv1.0.1 and 1.0.0-beta.23であることに気づく。
これもしかしたらバージョン下げたらいけんじゃね?と思い下記を実行。

yarn add v-calendar@1.0.0-beta.22

エラー発生せず解決した…!!!

結論

v-calendarを使う際はバージョンを1.0.0-beta.22に指定しよう。

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

Nuxt TypeScript + Vue Property Decorator によるカウンターのサンプルコード

概要

  • Nuxt TypeScript + Vue Property Decorator を使用して「+」「-」ボタンで数値が増減するカウンターを作成する

今回の環境

  • Node.js 13.12.0
  • Nuxt.js 2.12.2
  • TypeScript 3.8.3
  • Nuxt TypeScript (Nuxt.js 向け TypeScript サポート)
  • Vue Property Decorator 8.4.1

Nuxt TypeScript + Vue Property Decorator によるカウンターのサンプルコード

ソースコード一覧

├── nuxt.config.ts
├── package-lock.json
├── package.json
├── pages
│   └── index.vue
├── tsconfig.json
└── vue-shim.d.ts

nuxt.config.ts

nuxt.config.js の TypeScript 版。

import { Configuration } from '@nuxt/types'

const config: Configuration = {
  buildModules: ['@nuxt/typescript-build']
}
export default config

package.json

必要なライブラリの記述等。

{
  "name": "my-app",
  "scripts": {
    "dev": "nuxt-ts",
    "build": "nuxt-ts build",
    "generate": "nuxt-ts generate",
    "start": "nuxt-ts start"
  },
  "dependencies": {
    "@nuxt/typescript-runtime": "0.4.1",
    "nuxt": "2.12.2",
    "vue-property-decorator": "8.4.1"
  },
  "devDependencies": {
    "@nuxt/typescript-build": "0.6.1"
  }
}

pages/index.vue

カウンター表示用のページコンポーネント。

<template>
  <div>
    <p>Data: {{ myCount }}</p>
    <p>Data: {{ myWatch }}</p>
    <p>Computed: {{ countByComputed }}</p>
    <p>
      <button @click="increment">+</button>
      <button @click="decrement">-</button>
    </p>
  </div>
</template>

<script lang="ts">

// Vue Property Decorator と Vue Class Component をインポートする
import { Vue, Component, Watch } from 'vue-property-decorator'

// Vue Class Component を使う
@Component
export default class CounterComponent extends Vue {

  // data
  myCount: number = 0
  myWatch: number = 0

  // 算出プロパティ (computed property)
  // myCount の値が更新されたときに呼び出される
  get countByComputed(): number {
    console.log('countByComputed')
    return this.myCount
  }

  // 監視プロパティ (watched property)
  // myCount の値が更新されたときに呼び出される
  @Watch('myCount')
  countByWatched() {
    console.log('countByWatched')
    // 2倍の値をセット
    this.myWatch = this.myCount * 2
  }

  // メソッド (method)
  // 「+」ボタンクリック時に呼ばれる
  increment() {
    console.log('increment')
    this.myCount++
  }

  // メソッド (method)
  // 「-」ボタンクリック時に呼ばれる
  decrement() {
    console.log('decrement')
    this.myCount--
  }
}
</script>

tsconfig.json

ほぼ Nuxt TypeScript の公式資料通りだが、Vue Property Decorator (vue-property-decorator) を使うため experimentalDecorators の設定を追加している。

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "target": "es2018",
    "module": "esnext",
    "moduleResolution": "node",
    "lib": [
      "esnext",
      "esnext.asynciterable",
      "dom"
    ],
    "esModuleInterop": true,
    "allowJs": true,
    "sourceMap": true,
    "strict": true,
    "noEmit": true,
    "baseUrl": ".",
    "paths": {
      "~/*": [
        "./*"
      ],
      "@/*": [
        "./*"
      ]
    },
    "types": [
      "@types/node",
      "@nuxt/types"
    ]
  },
  "exclude": [
    "node_modules"
  ]
}

vue-shim.d.ts

Nuxt TypeScript の公式資料通り。
Vue ファイルの型を提供するための型宣言。

declare module "*.vue" {
  import Vue from 'vue'
  export default Vue
}

Node.js サーバを起動

package.json に記述したライブラリをインストール。

$ npm install

Node.js サーバを起動。

$ npm run dev

> my-app@ dev /Users/foobar/my-app
> nuxt-ts


   ╭─────────────────────────────────────────────╮
   │                                             │
   │   Nuxt.js v2.12.2                           │
   │   Running in development mode (universal)   │
   │                                             │
   │   Listening on: http://localhost:3000/      │
   │                                             │
   ╰─────────────────────────────────────────────╯

ℹ Preparing project for development
ℹ Initial build may take a while
✔ Builder initialized
✔ Nuxt files generated
ℹ Starting type checking service...

✔ Client
  Compiled successfully in 9.55s

✔ Server
  Compiled successfully in 7.86s

ℹ Type checking in progress...
ℹ Waiting for file changes
ℹ Memory usage: 222 MB (RSS: 305 MB)
ℹ Listening on: http://localhost:3000/

Web ブラウザで http://localhost:3000/ にアクセスすると「+」「-」ボタンと数値が増減するカウンター3つが表示される。

参考資料

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

【Vue.js】TimeTreeの予定表をAPIで取得して出力する

概要

カレンダーシェアアプリのTimeTreeのAPIが2020年1月23日に公開されました。
本記事では、Vue.jsからTimeTreeのAPIを呼び出して、予定表を出力する所までのサンプルのソースコードを公開します。

事前準備

APIの仕様を確認し、Postmanで動作確認を行います

  • 今回は、TimeTreeのAPIのGET /calendars/:calendar_id/upcoming_eventsを使用して、当日以降のカレンダーの予定情報を取得します。

  • API仕様は以下となっています。
    image.png

  • 上記の仕様の通り、calendar_idが必要ですので、PostmanでGET /calendarsでAPIを実行して、取得します。

  • 設定値

    • HTTPメソッド:"GET"
    • URI:"https://timetreeapis.com/calendars"
    • パラメータキー include:labels,members
    • Headersキー Authorization:"Bearer (事前準備で取得したパーソナルアクセストークン)"

image.png

  • レスポンスボディより、カレンダーIDを把握、控えておきます。この例では、赤枠で囲った値"aBcDeFgHiJkL1"となっています。
  • カレンダーIDが判明したので、本命のAPIの[GET /calendars/:calendar_id/upcoming_events]を同じようにPostmanで実行してみたいと思います。
  • 設定値

image.png

  • 正しく、TimeTreeの予定表を取得できているようです。

実装を開始します。

  • APIを呼び出すので、axiosを使用します。
  • axiosを使用するに当たり、本記事ではCDNでscriptタグを定義しました。
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
  • jsファイルの定義では、mounted にてAPIの呼び出しを定義しました。URI、ヘッダリクエストについては、上述のPostmanで動作確認した値と同じものとなります。
    mounted: function () {
        axios.get("https://timetreeapis.com/calendars/(Postmanで取得したカレンダーID)/upcoming_events", 
            { headers: {'Authorization': 'Bearer (事前準備で取得したパーソナルアクセストークン)'} }).then(response => (this.items = response.data.data))
    }
  • APIで取得した予定表は、以下のようにしてtableタグで出力しています。
  • moment とあるのは、日付フォーマットライブラリのMomentを使っている為です。こちら様の記事が参考になると思います。
                <table class="table">
                    <tr>
                        <th>タイトル</th><th>開始</th><th>終了</th>
                    </tr>
                    <tr v-for="item in items" >
                        <td>{{ item.attributes.title }}</td>
                        <td>{{ item.attributes.start_at | moment }}</td>
                        <td>{{ item.attributes.end_at | moment }}</td>
                    </tr>
                </table>

完成したソースコード

  • 以下公開します。
  • 見栄えの為、Bootstrap4を使用しています。
index.html
<html lang='ja'>
    <head>
        <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
        <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment-with-locales.min.js"></script>
        <script type="text/javascript" src="app.js" defer></script>
        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
    </head>
    <body>
        <div class="card">
            <div class="card-header">
                <span>予定表</span>
            </div>

            <div id="app" class="card-body">
                <table class="table">
                    <tr>
                        <th>タイトル</th><th>開始</th><th>終了</th>
                    </tr>
                    <tr v-for="item in items" >
                        <td>{{ item.attributes.title }}</td>
                        <td>{{ item.attributes.start_at | moment }}</td>
                        <td>{{ item.attributes.end_at | moment }}</td>
                    </tr>
                </table>
            </div>
        </div>
    </body>
</html>

app.js
var app = new Vue({
    el: '#app',
    filters: {
        moment: function (date) {
            return moment(date).format('YYYY/MM/DD HH:mm');
        }
    },
    data: {
        items:[],
    },
    mounted: function () {
        axios.get("https://timetreeapis.com/calendars/(Postmanで取得したカレンダーID)/upcoming_events", 
            { headers: {'Authorization': 'Bearer (事前準備で取得したパーソナルアクセストークン)'} }).then(response => (this.items = response.data.data))
    }
})

出力結果

image.png

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

【Nuxt.js】firebase導入編(RDB版):データの追加 取得をしよう

前置き

便利なfirebase!
シンプルに導入の仕方を解説していきます?
簡単なデータの書き込みと取得をしてみます?

❓そもそもfirebaseとは
簡単に言うと
オンラインにデータを保存できて
取得もできる優れものです!
自分で1から作るとなると大変ですが
firebaseを使えば簡単ですね?
https://firebase.google.com/docs/database

❓どんな時に使うか
例えば、チャットアプリ!
リアルタイムにデータを保存・同期できるので
メッセージを送り合うことが簡単になります?

例えば、フリマサイト!??
会員情報とその会員が出品した商品が
DBに保存されていきます。
それを会員はいつでも編集でき、
反映したらすぐ表示が変わるわけです?
会員自身でデータが作れて編集もできる!
といった感じです?

❓Referenceの読み方
基本的にfirebaseのreferenceは
英語の状態で表示し、
自分でgoogle翻訳で翻訳しましょう??

言語を日本語にすると
古いバージョンだったりするので、
最新の英語を翻訳していくのがベスト!⭕️
ただいきなり全部英語だと
欲しい情報がどこにあるか分からないので
最初は日本語で表示させて
ある程度検討をつけてから英語にしてます?
(英語も理解できるように頑張ろう…?)

DBの種類と違い

firebaseのDatabaseは2種類あります。
・Realtime Database
・Cloud Firestore
  (Realtime Databaseの拡張版?)
動作や料金が変わってきます?
基本は無料で使えます!?
https://firebase.google.com/docs/database/rtdb-vs-firestore

基本はCloud Firestoreがオススメです?
具体的な違いはここが参考になります!
https://techblog.kayac.com/rtdb-vs-firestore

Step1: firebaseのDBを作成

https://firebase.google.com/?hl=ja

まずはここから
1. 「使ってみる」を押す
ログインしていない場合はログイン
そもそもアカウントがない場合は
Googleアカウントを作成してください?

step1.png

2. 「プロジェクトを追加」を押す

step2.png

3. プロジェクト名を入力
下に表示されているsample-6a560が
プロジェクトIDになります。

step3.png

4. Google アナリティクスの有無を選択
なしの場合は続行でプロジェクト完成
ありの場合は続行で次のアカウント選択

step4.png

5. アカウントを選択
 Default Account for Firebaseを選択

step5.png

これでプロジェクトが完成?✨
プロジェクト概要画面へ移行します?

6. サイドメニューのDatabaseを押す

3.png

7. DBの種類を選択、作成
 オススメはCloud Firestoreですが、
 今回はシンプルにしたいので
 firebase Realtime DBです?

2.png

8. セキュリティルールを選択
 テストモードで読み取りと書き込みを
 許可しておきましょう!

これでDatabaseができました??

Step2: Appの作成

ウェブアプリにfirebaseを追加していきます!

1. ダッシュボードから</>ウェブを選択

app1.png

2. アプリのニックネームを入力
 Firebase Hostingの設定は今回はなしで⭕️

app2.png

3.「コンソールに進む」を押す
 コードはいつでも確認できるので飛ばしてOK

これでダッシュボードから
アプリが確認できるようになりました?

app4.png

Step3: firebaseのインストール

兎にも角にもまずはインストール!!!?
https://firebase.google.com/docs/web/setup

referenceのnode.jsタブを見ていきます?
Nuxtには初めからpakage.jsonがあるので
$npm initは不要ですね!

ターミナル
$ npm install --save firebase

これでpakage.jsonのdependenciesに
firebaseのバージョンが追加されていますね!

pakage.json
{
 "dependencies": {
   "firebase": "^7.12.0",
   "nuxt": "^2.0.0",
   "vue": "^2.6.11",
   "vue-template-compiler": "^2.6.11"
 },
}

Step4: firebaseと連携する

plugin/firebase.jsを作ります?

【plugin/firebase.js】
Step3で見たreferenceの続きです。
・firebaseをimport
・firebase.initializeApp()
 アプリでfirebaseの初期化
・その中にFirebase SDK snippetを貼り付け
 (Step2で作成したアプリのコード)

plugin/firebase.js
import firebase from "firebase/app"

if (!firebase.apps.length) {
 firebase.initializeApp({
   apiKey: "貼り付け",
   authDomain: "貼り付け",
   databaseURL: "貼り付け",
   projectId: "貼り付け",
   storageBucket: "貼り付け",
   messagingSenderId: "貼り付け",
   appId: "貼り付け",
   measurementId: "貼り付け"
 })
}

export default firebase

apiKeyなどの見方は
firebaseプロジェクトダッシュボードの
ここの歯車を押して…

5.png

この画面の下にある…

6.png

ここです!
7.png

これで準備は整いました?

Step4: DBにinputでデータを追加してみる

現在のDBには何もありません。
8.png

inputでこのデータを送信してみましょう?

9.png

index.vue
<template>
 <div class="page">
   <label>
     <span>
       お名前:
     </span>
     <input
       type="text"
       v-model="user.name"
     >
   </label>
   <label>
     <span>
       email:
     </span>
     <input
       type="text"
       v-model="user.email"
     >
   </label>
   <button
     type="button"
     @click="submit"
   >
     Submit
   </button>
 </div>
</template>

<script>
import firebase from '@/plugins/firebase'

export default {
 data () {
   return {
     user: {
       name: "",
       email: ""
     },
   }
 },
 methods: {
   submit () {
     let Ref = firebase.database().ref()
     Ref.push({ name: this.user.name, body: this.user.email })
     .then(response => {
       console.log(response, this.user)
     })
   },
 },
}
</script>

【結果】

Databaseに反映されていますね?
consoleに表示されたkeyとも
バッチリ合っています?

10.png

【解説】
・plugin/firebase.jsをimportする

・firebase.database().ref()
 firebase.database()でデータが取り出せます。
 ref()でdatabaseのURL指定をします。
 引数が空=firebase.jsで貼り付けたURLです?
https://firebase.google.com/docs/reference/js/firebase.database
https://firebase.google.com/docs/reference/js/firebase.database.Database#ref

・push
 変数にしたURLにデータをpushします?
 データの書き込みmethodには
 set, push, update, transactionがあります。
 setはまるごと上書き
 pushはkeyでまとまったデータを追加
https://firebase.google.com/docs/reference/js/firebase.database.Reference#set

・.then
 通信が成功した時の処理を書きます。

【上手くいかないぞ?】
テストモードではなく
ロックモードになっている可能性があります。
その場合はDatabaseのルールを
trueにしてみてください〜!

11.png

Step5: DBからデータを取得してみる

先ほどのデータを取得してみましょう!?
keyでまとまったデータ全てではなく、
このkeyのnameだけを取得してみます?

10.png

Databaseのkey(赤枠の部分)を押してみましょう!
下の階層になったことでURLが変わりましたね!
ちなみにURLはブラウザではなくここです?

スクリーンショット 2020-03-28 15.36.32.png

【index.vue】
methodsのfetchDataを追加し、
template内でdivで表示させます。

index.vue
<template>
 <div class="page">
   <label>
     <span>
       お名前:
     </span>
     <input
       type="text"
       v-model="user.name"
     >
   </label>
   <label>
     <span>
       email:
     </span>
     <input
       type="text"
       v-model="user.email"
     >
   </label>
   <button
     type="button"
     @click="submit"
   >
     Submit
   </button>
   <button
     type="button"
     @click="fetchData"
   >
     fetchData
   </button>
   <div id="user.name" />
 </div>
</template>

<script>
import firebase from '@/plugins/firebase'

export default {
 data () {
   return {
     user: {
       name: "",
       email: ""
     },
   }
 },
 methods: {
   submit () {
     let Ref = firebase.database().ref()
     Ref.push({ name: this.user.name, body: this.user.email })
     .then(response => {
       console.log(response, this.user)
     })
   },
   fetchData () {
     let Ref = firebase.database().ref('/-M3Ub7gJeswkN3I7BSYk')
     Ref.on('value', function(snapshot){
       document.getElementById("user.name").innerHTML = snapshot.child("name").val()
     })
   },
 },
}
</script>

【結果】

fetch.gif

【解説】
・firebase.database().ref()
 下階層のURLに変更します!
 firebase.database().ref('/{ key名 }')

・ Ref.on('value', function(snapshot){ 処理 })
 データの取得をします?
https://firebase.google.com/docs/database/admin/retrieve-data

・document.getElementById〜
 div id="user.name"部分に表示させます

・snapshot.child("name").val();
 snapshot内(database内)で指定したURLの子
 つまり今回でいうと-M3Ub7gJeswkN3I7BSYkの下の
 nameの値だけを取得しています?

記事が公開したときにわかる様に、
note・Twitterフォローをお願いします?

https://twitter.com/aLizlab

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

Vue Internals①astを中心に 1-4mountComponent

はじめに

前回でgenerateが終わり、compileまでが終わりました。
今回はmountComponentでどのように実際のdomが出来上がっていくかを見ていきたいと思います。

mount.callから

まずmount.call(this, el, hydrating)のthisはvmで、elはdiv#vue_exampleです。
vm

$attrs: (...)
$listeners: (...)
message: (...)
$data: (...)
$props: (...)
_uid: 0
_isVue: true
$options: {components: {…}, directives: {…}, filters: {…}, el: "#vue_example", _base: ƒ, …}
_renderProxy: Proxy {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …}
_self: Vue {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …}
$parent: undefined
$root: Vue {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …}
$children: []
$refs: {}
_watcher: Watcher {vm: Vue, deep: false, user: false, lazy: false, sync: false, …}
_vnode: VNode {tag: "div", data: {…}, children: Array(1), text: undefined, elm: div#vue_example, …}
_staticTrees: null
$vnode: undefined
$slots: {}
$scopedSlots: {}
_c: ƒ (a, b, c, d)
$createElement: ƒ (a, b, c, d)
_watchers: (4) [Watcher, Watcher, Watcher, Watcher]
_data: {__ob__: Observer}
$el: div#vue_example

このなかで1-1で作った_data,_renderProxyや、1-3で作ったoptionsのrenderが重要な役割を果たしていきます。

mount.callからmountComponentへ

function mountComponent (
    vm,
    el,
    hydrating
  ) {
    vm.$el = el;

    callHook(vm, 'beforeMount');

    var updateComponent;

    updateComponent = function () {
        vm._update(vm._render(), hydrating);
      };


    // we set this to vm._watcher inside the watcher's constructor
    // since the watcher's initial patch may call $forceUpdate (e.g. inside child
    // component's mounted hook), which relies on vm._watcher being already defined
    new Watcher(vm, updateComponent, noop, {
      before: function before () {
        if (vm._isMounted && !vm._isDestroyed) {
          callHook(vm, 'beforeUpdate');
        }
      }
    }, true /* isRenderWatcher */);
    hydrating = false;

    // manually mounted instance, call mounted on self
    // mounted is called for render-created child components in its inserted hook
    if (vm.$vnode == null) {
      vm._isMounted = true;
      callHook(vm, 'mounted');
    }
    return vm
  }

vm.$elを作り、updateConmonentFunctionを作って、new Watcherに行く
watcher絡みは詳しくは後でやります。

new Watcher

var Watcher = function Watcher (
    vm,
    expOrFn,
    cb,
    options,
    isRenderWatcher
  ) {
    this.vm = vm;
    if (isRenderWatcher) {
      vm._watcher = this;
    }
    vm._watchers.push(this);

    this.cb = cb;
    this.id = ++uid$2; // uid for batching
    this.active = true;
    this.dirty = this.lazy; // for lazy watchers
    this.deps = [];
    this.newDeps = [];
    this.depIds = new _Set();
    this.newDepIds = new _Set();
    this.expression = expOrFn.toString();
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn;
    } else {
      this.getter = parsePath(expOrFn);
      if (!this.getter) {
        this.getter = noop;
        warn(
          "Failed watching path: \"" + expOrFn + "\" " +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        );
      }
    }
    this.value = this.lazy
      ? undefined
      : this.get();
  };

やっていることはまずnew Watch.vmに引数のvmをセットし、vm.watcherにnew Watchをセットして相互に参照できるようにして、vm,wacthersにnew Watchをpush
this.expressionに
"function () {
vm.
update(vm._render(), hydrating);
}"
と関数の文字列を入れて、
this.getterには上の関数そのものを入れる

最後のthis.valueにはthis.get()の返り値が入る

 Watcher.prototype.get = function get () {
    pushTarget(this);
    var value;
    var vm = this.vm;
    try {
      value = this.getter.call(vm, vm);
    } catch (e) {
      if (this.user) {
        handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value);
      }
      popTarget();
      this.cleanupDeps();
    }
    return value
  };

this.getter.callはfunction () {
vm._update(vm._render(), hydrating);
}で、callなのでthisはvmとなる。

まずはvm._renderへ

Vue.prototype._render = function () {
      var vm = this;
      var ref = vm.$options;
      var render = ref.render;
      var _parentVnode = ref._parentVnode;

      if (_parentVnode) {
        vm.$scopedSlots = normalizeScopedSlots(
          _parentVnode.data.scopedSlots,
          vm.$slots,
          vm.$scopedSlots
        );
      }


      vm.$vnode = _parentVnode;
      // render self
      var vnode;
      try {

        currentRenderingInstance = vm;
        vnode = render.call(vm._renderProxy, vm.$createElement);
      } catch (e) {
        handleError(e, vm, "render");
        // return error render result,
        // or previous vnode to prevent render error causing blank component
        /* istanbul ignore else */
        if (vm.$options.renderError) {
          try {
            vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e);
          } catch (e) {
            handleError(e, vm, "renderError");
            vnode = vm._vnode;
          }
        } else {
          vnode = vm._vnode;
        }
      } finally {
        currentRenderingInstance = null;
      }
      // if the returned array contains only a single node, allow it
      if (Array.isArray(vnode) && vnode.length === 1) {
        vnode = vnode[0];
      }
      // return empty vnode in case the render function errored out
      if (!(vnode instanceof VNode)) {
        if (Array.isArray(vnode)) {
          warn(
            'Multiple root nodes returned from render function. Render function ' +
            'should return a single root node.',
            vm
          );
        }
        vnode = createEmptyVNode();
      }
      // set parent
      vnode.parent = _parentVnode;
      return vnode
    };
  }

vm._renderでthisはvmで、renderにはcompileで作ったrender関数

(function anonymous(
) {
with(this){return _c('div',{attrs:{"id":"vue_example"}},[_v("\n  "+_s(message)+"\n")])}
})

が入る

そして vnode = render.call(vm._renderProxy, vm.$createElement);
が実行されるとき↑の無名関数のthisはvm._renderProxyとなる、これは1-1のnew Proxyで作ったもので、hasHandlerが登録されていた。

だが、このwith(this)とは何でしょうか?
例えばobj.a=5、obj.b=7として、

 with(obj){
   console.log(a)//5
   console.log(b)//7
}

with(c=9){
  console.log(c)//9
}
console.log(c)//undefined

with(){}で{}内の変数はwith()で宣言していれば使えるようになる、といったものです。
参考
http://blog.tojiru.net/article/197591734.html

さて、withの使い方はわかったので、今回のケースではwith(this=vm._renderProxy)でした。
そもそもvm._renderProxyとはvmをProxyでwrapしたもので、? in vm._renderProxyとなった時にhasHandlerが起動するのでした。
問題の関数を簡略化すると

with(vm._renderProxy){
  return _c()
}

となります。
ここのどこに ? in vm._renderProxyがあるのでしょうか?

例として

const handler1 = {
  has (target, key) {
    console.log(target,key);
    if(key in target){
     return true;
    }else{
     return false;
    }
  }
};

const monster1 = {
  _secret: 'easily scared',
  eyeCount: 4
};

const proxy1 = new Proxy(monster1, handler1);
console.log('eyeCount' in proxy1);//true
console.log("aaa" in proxy1);//false
with(proxy1){
  console.log(eyeCount);
}

//withの結果
Object { _secret: "easily scared", eyeCount: 4 } "console"
Object { _secret: "easily scared", eyeCount: 4 } "eyeCount"
 4

上記の結果からwithを使うと変数を "console" in proxy1みたいに探しているみたいです。
なのでhasが処理に割り込むということですね。

なので今回は_c in vm._renderProxyとなります。
今回はhasのチェックは_cとか_vとか全部通るので素直に_cとかが何をしているかだけ追っていけば大丈夫です。
では戻ってrender関数を見ていきましょう。

(function anonymous(
) {
with(this){return _c('div',{attrs:{"id":"vue_example"}},[_v("\n  "+_s(message)+"\n")])}
})

ここのmessageを取得するときにvm.messageはproxyでvm.data.messageとなることを思い出し、さらにvm._data.messageはObservableという仕組みになっていました。
なのでvm.
data.messageの値を取得するときはObject.definePropertyで付けたgetが起動します。

obdefinemess.png
dep絡みはまた後でやります。

messageの値を取得した後s(message)へ,sはvm.protoにあるto_String関数のことです。
_s=toString.png
_vはcreateTextVNode関数で、
_v=createTextVNode.png
new VNode
new VNode.png

無名関数に戻って次は_c=createElement
_c=createElement.png

ここでcontext=vm、tag=div、data=attrs: {id: "vue_example"}、children=_vで作ったVNode

function createElement (
    context,
    tag,
    data,
    children,
    normalizationType,
    alwaysNormalize
  ) {
    if (Array.isArray(data) || isPrimitive(data)) {
      normalizationType = children;
      children = data;
      data = undefined;
    }
    if (isTrue(alwaysNormalize)) {
      normalizationType = ALWAYS_NORMALIZE;
    }
    return _createElement(context, tag, data, children, normalizationType)
  }
function _createElement (
    context,
    tag,
    data,
    children,
    normalizationType
  ) {

    var vnode, ns;
    if (typeof tag === 'string') {
      var Ctor;
      ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag);
      if (config.isReservedTag(tag)) {
        // platform built-in elements
        if (isDef(data) && isDef(data.nativeOn)) {
          warn(
            ("The .native modifier for v-on is only valid on components but it was used on <" + tag + ">."),
            context
          );
        }
        //今回はここ!
        vnode = new VNode(
          config.parsePlatformTagName(tag), data, children,
          undefined, undefined, context
        );
      }
    } else {
      // direct component options / constructor
      vnode = createComponent(tag, data, context, children);
    }
    if (Array.isArray(vnode)) {
      return vnode
    } else if (isDef(vnode)) {
      if (isDef(ns)) { applyNS(vnode, ns); }
      if (isDef(data)) { registerDeepBindings(data); }
      return vnode
    } else {
      return createEmptyVNode()
    }
  }

_createElementではchildrenのvNodeとattrとtagを使ってvNodeをつくる
作られたvNodeはcontextにvm、dataにattrなど必要な情報が入っている

tag: "div"
data: {attrs: {…}}
children: [VNode]
context: Vue {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …}

この作られたvNodeがvm.renderの返り値で、watcherのgetterから呼び出したvm._updateへ
```
updateComponent = function () {
vm.
update(vm._render(), hydrating);
};
```

vm.updateでは初回と二度目以降で_patchの引数が違う、ここでvNodeのdiffをやるんですが、その詳細はdiffアルゴリズム編で

今回は初回の動きを見ていきます。
vm._update.png
初回はpatchでoldVnodeがdiv#vue_exampleでvnodeがさっきつくったやつ

 function patch (oldVnode, vnode, hydrating, removeOnly) {
          if (isRealElement) {

            oldVnode = emptyNodeAt(oldVnode);


          // replacing existing element
          var oldElm = oldVnode.elm;
          var parentElm = nodeOps.parentNode(oldElm);

          // create new node
          createElm(
            vnode,
            insertedVnodeQueue,
            // extremely rare edge case: do not insert if old element is in a
            // leaving transition. Only happens when combining transition +
            // keep-alive + HOCs. (#4590)
            oldElm._leaveCb ? null : parentElm,
            nodeOps.nextSibling(oldElm)
          );

          // update parent placeholder node element, recursively
          if (isDef(vnode.parent)) {
            var ancestor = vnode.parent;
            var patchable = isPatchable(vnode);
            while (ancestor) {
              for (var i = 0; i < cbs.destroy.length; ++i) {
                cbs.destroy[i](ancestor);
              }
              ancestor.elm = vnode.elm;
              if (patchable) {
                for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) {
                  cbs.create[i$1](emptyNode, ancestor);
                }
                // #6513
                // invoke insert hooks that may have been merged by create hooks.
                // e.g. for directives that uses the "inserted" hook.
                var insert = ancestor.data.hook.insert;
                if (insert.merged) {
                  // start at index 1 to avoid re-invoking component mounted hook
                  for (var i$2 = 1; i$2 < insert.fns.length; i$2++) {
                    insert.fns[i$2]();
                  }
                }
              } else {
                registerRef(ancestor);
              }
              ancestor = ancestor.parent;
            }
          }

          // destroy old node
          if (isDef(parentElm)) {
            removeVnodes([oldVnode], 0, 0);
          } else if (isDef(oldVnode.tag)) {
            invokeDestroyHook(oldVnode);
          }
        }
      }

      invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
      return vnode.elm
    }
  }
 function emptyNodeAt (elm) {
      return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)
    }

emptyNodeAtでoldVNodeにtag: "div"、elm:div#vue_exampleのVNodeを入れる

nodeOpsは

createElement: ƒ createElement$1(tagName, vnode)
createElementNS: ƒ createElementNS(namespace, tagName)
createTextNode: ƒ createTextNode(text)
createComment: ƒ createComment(text)
insertBefore: ƒ insertBefore(parentNode, newNode, referenceNode)
removeChild: ƒ removeChild(node, child)
appendChild: ƒ appendChild(node, child)
parentNode: ƒ parentNode(node)
nextSibling: ƒ nextSibling(node)
tagName: ƒ tagName(node)
setTextContent: ƒ setTextContent(node, text)
setStyleScope: ƒ setStyleScope(node, scopeId)

となっていて、document.createElementなどのwrapper?

function parentNode (node) {
    return node.parentNode
  }
 function nextSibling (node) {
    return node.nextSibling
  }

createElmへ
createElmでdocument.createElement.png
ここのvnode.elm=createElement()で実際のDOMが作られる(childrenはまだ)

createElement$1.png

setScopeではよくvueを使ってるとdata-v-509622e6みたいになっていたりすると思うんですけど、ここでsetされます。今回はcss使っていないので飛ばします。

createElm続き
createElm2.png
ここのcreateChildrenでvnode.elmにchildrenをset

createChildren→createElmで、今回はcreateTextNode

 function createElm (
      vnode,
      insertedVnodeQueue,
      parentElm,
      refElm,
      nested,
      ownerArray,
      index
    ) {
      var data = vnode.data;
      var children = vnode.children;
      var tag = vnode.tag;
      if (isDef(tag)) {
        {
      } else if (isTrue(vnode.isComment)) {

      } else {
        //今回はここ!insertでparentElm(div)に↵  Hello Vue.js!↵を入れる
        vnode.elm = nodeOps.createTextNode(vnode.text);
        insert(parentElm, vnode.elm, refElm);
      }
    }
  function createTextNode (text) {
    return document.createTextNode(text)
  }

insert11.png

createElmに戻ってinvokeCreateHooks→updateAttrs→setAttrs→baseSetAttr→el.setAttributeでvNode.elmにattrsを追加

invokeCreateHooksのcsb.createは

create: Array(8)
0: ƒ updateAttrs(oldVnode, vnode)
1: ƒ updateClass(oldVnode, vnode)
2: ƒ updateDOMListeners(oldVnode, vnode)
3: ƒ updateDOMProps(oldVnode, vnode)
4: ƒ updateStyle(oldVnode, vnode)
5: ƒ _enter(_, vnode)
6: ƒ create(_, vnode)
7: ƒ updateDirectives(oldVnode, vnode)

となっていて、今回はattrsだけだけど、実際はinvokeCreateHooksでstyleとかdirectiveなどもvNode.elmに追加する,
_enterはtransition関連,createはref

function invokeCreateHooks (vnode, insertedVnodeQueue) {
      for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) {
        cbs.create[i$1](emptyNode, vnode);
      }
      i = vnode.data.hook; // Reuse variable
      if (isDef(i)) {
        if (isDef(i.create)) { i.create(emptyNode, vnode); }
        if (isDef(i.insert)) { insertedVnodeQueue.push(vnode); }
      }
    }

updateAttr.png
setAttr.png
baseSetAttr.png

createElmに戻ってparentElmに今まで作ってきたvnode.elmをinsertします。
ここでparentElmはparentElm = nodeOps.parentNode(oldElm)で、oldElmはdiv#vue_exampleのことでした。
なのでparentElmはbodyのことでした。
insertBody.png
ここでinsertBeforeをするとbodyにはもともとのdiv#vue_example({{message}})と今作ったvNode(Hello Vue.js!)が一時的に共存することとなります。

dualBody.png

ここまででcreateElmがおわりpatchに戻ります。ここからいらなくなってもともとのdiv#vue_exampleを取り除いていきます.

function patch (oldVnode, vnode, hydrating, removeOnly) {


          // destroy old node
          if (isDef(parentElm)) {
            removeVnodes([oldVnode], 0, 0);
          } else if (isDef(oldVnode.tag)) {
            invokeDestroyHook(oldVnode);
          }
        }
      }

      invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
      return vnode.elm
    }
  }

removeVNodes→removeAndInVokeRemoveHook→removeNode
removeVNodess.png

removeNode1111.png
removeNodeでbodyからdiv#vue_exampleを取り除きます。
afterRemoveNode.png
removeVNodesに戻って、invokeDestroyHookではrefやdirectiveを取り除いています。今回は関係ないので飛ばし

Vue.updateに戻ってvm.\$el=vnode.elm=新しく作ったdiv#vue_exampleとなります。
vue.$el.
Vue_ = vmとして、$elからVueInstanceが取り出せるように

これで長々としていたWatcherのthis.value=this.get()が終わります。get()の返り値はundefinedです。
ここまでWatcherでやってきたことをまとめると、
まずwatcherとvmをwatcher.vm=vm、vm.watcher=wactherで相互参照できるようにしました。
そこからwatcher.get()でvnode.elmを作って、vm.\$elを元々古いdiv#vue_exampleを指していたのを新しく作ったdiv#vue
exampleにしました。
watcherとvmは相互参照できるので、watcher側からでもきちんと新しいvm.\$elとなります。

ここまでで全体の流れは終わりです。
次回以降はwatcherやdepを見ていきたいと思います。

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

Quasar Framework の 内部検証のエラーメッセージを違う場所に表示したい

概要

Quasar Framework のフォームコンポーネントのバリデーション機能、便利ですよね。
実装方法は色々とありますが、例えばrulesを使ってこんなふうに内部検証を実装できます。

Before


See the Pen
Quasar Framework Validation - 1
by udyest (@udyest)
on CodePen.


さて、入力検証にひっかかったときのエラーメッセージを違う場所、例えばボタン押下時にダイアログで表示したいという要望が出てきました。
イレギュラーなケースですが実装していきましょう。あらかじめ実装しておいたものがこちらです。

After


See the Pen
Quasar Framework Validation - 2
by udyest (@udyest)
on CodePen.


Before から After へ改修する想定で順番に解説していきます。

今回はどうしてもrulesを使った内部検証の機能を使って実装したかったものです。
errorを使った外部検証の方を使えばわかりやすく実装できそうです。

記事執筆時点での Quasar Framework のバージョンは v1.9.12 です。

実装

上に掲載した Codepen (Before) から無駄を排除した、解説のベースとなる SFC (Single File Component) を用意しました。
デザイン系のものは消して入力欄も1つにしています。
これに改修を加えていきます。

<template>
  <div>
    <q-input
      label="ユーザーID"
      v-model="userId"
      ref="userId"
      :rules="[
        val => ( val && val.length > 0 ) || 'ユーザーIDを入力してください。',
        val => ( 7 < val.length && val.length < 21 ) || 'ユーザーID8文字以上20文字以内で入力してください。',
      ]"
    />
    <q-btn label="ログイン" @click="submit" />
  </div>
</template>

<script>
export default {
  data () {
    return {
      userId: ''
    }
  },
  methods: {
    submit () {
      this.$refs.userId.validate()
      if (!this.$refs.userId.hasError) {
        this.$q.notify({
          color: 'positive',
          message: 'ログインしました'
        })
      }
    }
  }
}
</script>

入力時のバリデーションを無効にする

rulesにバリデーションルールを設定すると、入力内容が変わるたびに検証が行われます。
今回は送信時にダイアログでエラーメッセージを出したいので、送信ボタンが押されるまでは検証しないようにします。

lazy-rulesを設定することでフォーカスが外れたときのみ検証が行われるようになります。
これだけだとフォーカスが外れたときにまだ検証が行われてしまいます。
lazy-rulesはそういう機能です。

フォーカスが外れたときにも検証が行われないようにしたいのですが難しそうです。
なのでフォーカスが外れたときに行われる検証をすぐにリセットしてしまいます。
入力要素のインスタンスからresetValidation関数を実行することでバリデーションをリセットできます。
blurイベント時にリセットしましょう。

ついでにhide-bottom-spaceも指定して下のエラーメッセージを表示する余白も消しましょう。

<template>
  <div>
    <q-input
      label="ユーザーID"
      v-model="userId"
      ref="userId"
      :rules="[
        val => ( val && val.length > 0 ) || 'ユーザーIDを入力してください。',
        val => ( 7 < val.length && val.length < 21 ) || 'ユーザーID8文字以上20文字以内で入力してください。',
      ]"
      lazy-rules
      @blur="$refs.userId.resetValidation()"
      hide-bottom-space
    />
    <q-btn label="ログイン" @click="submit" />
  </div>
</template>

<script>
  // 省略
</script>

バリデーションする

ボタン押下時のバリデーション機能をカスタマイズします。
解説のベースとなるコードをもう一度貼ります。

<template>
  <!-- 省略 -->
</template>

<script>
export default {
  data () {
    return {
      userId: ''
    }
  },
  methods: {
    submit () {
      this.$refs.userId.validate()
      if (!this.$refs.userId.hasError) {
        this.$q.notify({
          color: 'positive',
          message: 'ログインしました'
        })
      }
    }
  }
}
</script>

以下のことをしています。

  1. validate関数で検証を実行
  2. hasErrorプロパティでエラーの状態かどうかを確認し、エラーがなければ通知を表示

さて、今回の改修では以下のことを実装したいです。

  • エラーメッセージを取得したい(ダイアログに表示するため)
  • 入力要素の下のエラーメッセージは消したい
  • なおかつエラー状態は保持したい(エラー箇所を赤く目立たせたままにしたい)

現在表示されているエラーメッセージは、インスタンスからinnerErrorMessageプロパティで取得できます。
同様に表示されているエラーメッセージを動的に変えるには、このinnerErrorMessageプロパティを書き換えることで実現できます。
今回はエラーメッセージを消したいので、innerErrorMessageundefinedを代入することでエラーメッセージを消すことができます。
null''だと表示する余白が残ってしまいます。

また、表示されるエラーメッセージとエラー状態は独立しています。
上記の方法でエラーメッセージを上書きしたり消したりしても、エラー状態は消えませんので入力要素は赤くハイライトされたままになります。

<template>
  <!-- 省略 -->
</template>

<script>
export default {
  data () {
    return {
      userId: ''
    }
  },
  methods: {
    submit () {
      this.$refs.userId.validate()
      if (this.$refs.userId.hasError) {
        const errorMessage = this.$refs.userId.innerErrorMessage
        this.$refs.userId.innerErrorMessage = undefined
        this.$q.dialog({
          message: errorMessage,
        })
      } else {
        this.$q.notify({
          color: 'positive',
          message: 'ログインしました'
        })
      }
    }
  }
}
</script>

これで、Quasar Framework の内部検証を使って、入力要素の下のエラーメッセージの代わりにダイアログでエラーメッセージを表示することができました。

おまけ

全体像を貼るついでにおまけ。
rulesが冗長になるときはdataの方へ移動すると<template>の中身がスッキリします。

<template>
  <div>
    <q-input
      label="ユーザーID"
      v-model="userId"
      ref="userId"
      :rules="rules"
      lazy-rules
      @blur="$refs.userId.resetValidation()"
      hide-bottom-space
    />
    <q-btn label="ログイン" @click="submit" />
  </div>
</template>

<script>
export default {
  data () {
    return {
      userId: '',
      rules: [
        val => ( val && val.length > 0 ) || 'ユーザーIDを入力してください。',
        val => ( 7 < val.length && val.length < 21 ) || 'ユーザーIDは8文字以上20文字以内で入力してください。',
      ]
    }
  },
  methods: {
    submit () {
      this.$refs.userId.validate()
      if (this.$refs.userId.hasError) {
        const errorMessage = this.$refs.userId.innerErrorMessage
        this.$refs.userId.innerErrorMessage = undefined
        this.$q.dialog({
          message: errorMessage,
        })
      } else {
        this.$q.notify({
          color: 'positive',
          message: 'ログインしました'
        })
      }
    }
  }
}
</script>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ChromeへのVue.js devtoolsの導入方法

概要

「Vue.js devtools」をChromeに入れると、デバッグ作業が快適になるよ!、って話。

インストール方法

「Vue.js devtools」でGoogle検索すると、Chromeウェブストアの以下のURLがヒットするので、クリックしてジャンプ。

https://chrome.google.com/webstore/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd?hl=ja

インストール時に「アクセスしたウェブサイト上にある自分の全データの読み取りと変更をこの拡張機能に許可します。」って聞かれるので「はい」と許可。
許可したくない場合は、いったん「いいえ」で拒否った後に次のようにする。

Chromeメニューの「拡張機能>Vue.js devtools>詳細>サイトへのアクセス」を開いて、「特定のサイト」を選択して「http://127.0.0.1/*」などと入れる。Vue.jsのデバッグを行う時のローカルサーバーは、通常は「http://127.0.0.1:8080/」になるので、このローカルサーバー上のデータに対してのみ読み取りと変更を許可すればOK。

特定のサイトに対してのみ許可する

状態を反映するため、いったんChromeを再起動する。

後は、Vue.jsの開発用ローカルサーバーを起動していつものように「http://127.0.0.1:8080/」へアクセスする。

F12キーで「ディベロッパーツール」を開いて、「Memory」等のタブの右端に(無い場合は「>>」をクリックして展開する)、「Vue」を開く(※許可したURL以外では「Vue」タブは表示されない)。

すると、次のようにデバッグできる。変数の状態をその場で確認できる。便利♪
変数の値などをその場で確認できる

以上ー。

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

Vuetify2を使ったプロジェクトのビルドが遅いので高速化する

Vuetify+Vue CLIで構築したプロジェクトで、
Vuetify2.xにアップグレードしたらnpm run serveの開始時間が著しく長くなってしまったので改善します。

原因

Vuetify2.0よりデフォルトで有効となったA-la-carte(treeshaking)が原因です。

A-la-carteとは

Vuetifyの数多くのコンポーネントのうち、「プログラム中で使われているもの」だけを成果物に含める機能です。使っていないコンポーネントをimportしないので、ビルドサイズが減少します。

しかしながら、これはビルド時間を増大させます。

A-la-carteを使わない場合、vuetifyの全てのコンポーネントはnode_modules/vuetify/dist/vuetify.min.jsから読み込まれます。これは既にimportの解決やminifyが済んでいるプリコンパイル済みソースであるため、これをビルド成果物に含めるための処理は極めて軽いです。

ところが、A-la-carteを用いる場合、node_modules/vuetify/lib/*(要するにVuetifyのビルド前のソースそのもの)からコンポーネントが読み込まれます。これは、Vuetify内部のimportの解決もこちらのコンパイルタイムで行わなければならないことを意味します。Vuetifyはソース数が非常に多く、これは大変な処理負荷となります。

さらに、scssなどもトランスパイルしなければならないため、これも少なからずビルド時間が増大する原因となります。A-la-carteを用いなければcssはnode_modules/vuetify/dist/vuetify.min.cssより読み込まれるので負荷はありません。

npm run serveがなぜ重いか

ところが、実際のnpm run build時にはそこまで大きくコンパイル時間が伸びることはありません(当然伸びるのは伸びるのですが)。これは、vuetify-loaderと呼ばれるWebpackプラグインによって、不要なものが削ぎ落とされているからです。必要なものだけビルドするので、使っているコンポーネント数に比例してしかビルド時間は伸びないはずです。

しかし、npm run serveにおいてはそれがうまくいかず、非常にビルド時間が伸びてしまいます。これは、Webpack4のtreeshaking(vuetify-loaderはこの機能に依存しています)がproductionビルドでしか有効にならないためです。そのため、developmentビルドでは使っているコンポーネント数に関係なく全てのコンポーネントがビルドされ、非常に時間がかかります(特に開始時)。

解決策

解決策の方針としては以下のようになります。

  • developmentビルド時は、ビルド時間を削減するためにA-la-carte(treeshaking)を利用しないようにする
  • productionビルド時は、ビルドサイズを削減するためにvuetify-loaderを用いたA-la-carteを利用する

ソースの変更

以下はVue CLI3の例ですが、Webpackを生で使っている場合でも大きくは変わらないと思います。

vue-cli-plugin-vuetifyの無効化

package.jsonより、vue-cli-plugin-vuetifyvue-cli-plugin-vuetify-essentialsの削除

package.json
  "devDependencies": {
    ...,
-   "vue-cli-plugin-vuetify": "^1.1.2",
-   "vue-cli-plugin-vuetify-essentials": "^0.8.3",
    ...
  }

vuetifyのインポートの変更

Vue CLIインストールであればplugins/vuetify.jsでvuetifyを以下のようにインポートしていると思います。

plugins/vuetify.js
import Vue from 'vue';
import Vuetify, { VList } from 'vuetify/lib';

Vue.use(Vuetify);

export default new Vuetify({ /* vuetifyのオプション */ });

これと同様にして、plugins/vuetify-dev.jsを以下の内容で作成します。

plugins/vuetify-dev.js
import Vue from 'vue';
import Vuetify from 'vuetify';
import 'vuetify/dist/vuetify.min.css';

Vue.use(Vuetify);

export default new Vuetify({ /* vuetifyのオプション */ });

さらに、main.jsでVuetifyをインポートしている部分を変更します。

main.js
  import Vue from 'vue';
- import vuetify from './plugins/vuetify';
+ const vuetify = process.env.NODE_ENV === "production"
+   ? require('./plugins/vuetify').default
+   : require('./plugins/vuetify-dev').default;
  import ...

  /* (中略) */

  new Vue({
    vuetify,
    render: h => h(App)
  }).$mount('#app');

vuetify-loaderの手動ロード

vue-cli-plugin-vuetifyを消してしまったので、vuetify-loaderを手動でWebpackに読み込ませます。
vue.config.jsを以下のように編集します。

vue.config.js
+ const vuetifyOptional = process.env.NODE_ENV === "production"
+   ? [new (require('vuetify-loader/lib/plugin'))()]
+   : [];

  module.exports = {
    ...,
+   configureWebpack: {
+     plugins: [
+       ...vuetifyOptional,
+     ],
+   },
-   transpileDependencies: ["vuetify"],
+   transpileDependencies: vuetifyOptional.length > 0 ? ["vuetify"] : [],
    ...,
  }

動作確認

npm run servenpm run buildをして正しくビルドできることを確認して下さい。


参考文献

https://vuetifyjs.com/ja/getting-started/quick-start/
https://vuetifyjs.com/ja/customization/a-la-carte/

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

VuetifyのV2以降のグリッドシステムで縦並びさせる方法

こんにちは。

Vue/NuxtのUIフレームワークとしてよく用いられるVuetifyについて書き記したいと思います。

タイトルにもある通りなんですが、縦並びさせる方法についてです。
VuetifyはV2以降はグリッドシステムが大きく変わって、自分は縦並びさせる方法わかるまでに時間かかりました。。。

V2以降は、v-row,v-colあたりを作ってレイアウトを作っていくスタイルになりました。
まず横並びは簡単です。

hgoe.vue
<template>
  <v-row>
    <v-col cols="12" style="outline: solid 1px black;">12</v-col>
    <v-col cols="6" style="outline: solid 1px black;">6</v-col>
    <v-col cols="6" style="outline: solid 1px black;">6</v-col>
    <v-col cols="4" style="outline: solid 1px black;">4</v-col>
    <v-col cols="4" style="outline: solid 1px black;">4</v-col>
    <v-col cols="4" style="outline: solid 1px black;">4</v-col>
  </v-row>
</template>

こんな感じにすると、こんな感じになります。
スクリーンショット 2020-04-03 0.41.20.png
cols="12" が横幅いっぱいです。
で、僕がやりたかったのは、縦並びにすること

hgoe.vue
<template>
  <v-row class="flex-column">
    <v-col cols="12" style="outline: solid 1px black;">12</v-col>
    <v-col cols="6" style="outline: solid 1px black;">6</v-col>
    <v-col cols="6" style="outline: solid 1px black;">6</v-col>
    <v-col cols="4" style="outline: solid 1px black;">4</v-col>
    <v-col cols="4" style="outline: solid 1px black;">4</v-col>
    <v-col cols="4" style="outline: solid 1px black;">4</v-col>
  </v-row>
</template>

v-rowにclass="flex-column"をつければ。。。
スクリーンショット 2020-04-03 0.49.11.png
縦になりました。

ネストさせて組み合わせれば複雑なレイアウトも組めます。

V2以降のVuetifyを使ってみて、縦並びさせる方法わかるまで時間かかったので、同じようなことに悩んでいる人いたらと思って書きました。

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

Vue.jsでローディング画面作ってみた

初投稿です。
御見苦しいところなどあればすみません…

仕事でVue.jsを使ってSPAを作っていた際に、どうしても少し時間がかかってしまう処理を行っている間に別の操作をさせたくなかったので、ローディング画面を表示させて他の操作を実行できないようにしました。

そのとき使った「vue-loading」が便利だったのでご紹介。

インストール

以下のコマンドでインストール

yarnの場合

yarn add vue-loading-template

npmの場合

npm install vue-loading-template

使い方

他のモジュールと同様に使いたいViewでインストールして使います。

今回は、親のコンポーネントから受け取ったisLoadingがtrueの場合、全体にマスクしてクルクルを表示させました。

Loading.vue
<template>
  <div class="modal-mask" v-if="isLoading === true">
    <div class="loading">
      <vue-loading type="spin" color="#333" :size="{ width: '50px', height: '50px'}"></vue-loading>
    </div>
  </div>
</template>

<script>
import { VueLoading } from 'vue-loading-template';

export default {
  props: {
    isLoading: Boolean,
  },
  components: {
    VueLoading
  },
}
</script>

<style scoped>
.modal-mask {
  z-index:1;
  position:fixed;
  top:0;
  left:0;
  width:100%;
  height:100%;
  background-color:rgba(0,0,0,0.5);
  display: flex;
  align-items: center;
  justify-content: center;
}
</style>


これだけでロード中とわかりやすいクルクルを表示できます。便利…
他の表示パターンもあるので、デモサイトを見てみてください。

参考文献

Vue.jsでシンプルなローディングを表示する「vue-loading」の使い方
GitHub jkchao/vue-loading

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

Vuetifyで配列型データからボタンを作り、クリックされたら色を変える

Vue.jsで楽に綺麗にコンポーネントを作れるVuetifyフレームワークですが、楽だからこそ「さらに発展させてこうしたい!(もうちょい頑張ったらCSS書かずに済みそう...)」という時にちょっと悩みます。

なので、悩んだところをちょこちょこメモ書き&自学のために残します。誰かのお役に立てば幸いです。
フロント修行中ですので、ご指摘大歓迎です。


やりたいこと

  1. 配列型の中にある各文字列をボタンの表示に利用したい
  2. ボタンを押したら、選択されたボタンの色が変わって欲しい
  3. 押されたボタンのキー(配列内の文字列)を取得したい

実装例

template(HTML)部分
<v-btn
    v-for="(item, index) of items"
    :key="item"
    :color="selectedTemplate && index === selectedIndex ? 'warning' : null"
    @click="
        selectedTemplate = item;
        selectedIndex = index;
    "
    >
    {{ item }}
</v-btn>
script(Javascript)部分
<script>
  // Vueインスタンス内の必要部分から抜粋
    data: {
        items: [
            "A",
            "B",
            "C"
         ],
        selectedItem: null
    }
// 以下略
</script>

解説

1. 配列型の中にある各文字列をボタンの表示に利用したい

  • script内の dataで配列データ itemsを定義

  • template内の v-for="(item, index) of items"で配列データをforで回す

    • 配列内の要素 item と要素のインデックス index を取得する
    • {{ item }} で要素を表示する
    • :key="index"v-forを使う上で必要と怒られるから入れる(今回利用しないので "item"でもOK)

2. ボタンを押したら、選択されたボタンの色が変わって欲しい

  • script内の dataで選択されたボタン(ここでは selectedItem)の初期状態を定義

設定しないとエラーになります。
selectedIndexについては、下で説明する演算子( :color="selectedItem && index === selectedIndex ? 'warning' : null")の中でアクセスする際には必ず値が入っている(右辺が真の場合は値がある)のでエラーが出ない。右辺と左辺を入れ替えるとエラーが出るので、 selectedIndexも定義しておくほうが無難...。

  • template v-btn内の@click=でクリックイベントを用意

    • クリックイベント内 selectedItem = item;でそのボタンの要素を selectedItem変数に入れる
    • selectedIndex = index;でその要素のインデックスを selectedIndex変数に入れる
  • v-btn 要素が持っている属性 colorv-bindで三項演算子を入れる

    • :color="selectedItem && index === selectedIndex ? 'warning' : null"
    • もし何かしらのボタンが押されており、かつその選択されたアイテムのインデックスがこのボタン要素のインデックスと同一なら、色を warning系にする、それ以外は色つけしない

3. 押されたボタンのキー(配列内の文字列)を取得したい

  • script内で this.selectedItemに値が入る(取得タイミングには注意!押されなければ nullのまま)

余談

v-forいろいろ

配列型

  • v-for="item in items" → 配列内の要素そのものだけ取る
  • v-for="(item, index) in items → 上で使ったやつ。インデックスが取れる

辞書型

  • v-for="item in objectItems → 辞書データ内の値部分だけ取る
  • v-for="(item, name) in objectItems → 辞書データ内の値部分( item)とキー部分( name)を取る
    • 値が先なので注意
  • v-for="(item, name, index) in objectItems → もうわかる

v-btnblock属性を追加すると、ボタンの一つ一つが横の幅を全て取るので、ボタンが縦に並ぶ

イメージとしては、selectで選べるオプション要素が常に開いているような、よくあるデザインで表示できる!

template(HTML)部分
<v-btn
    block
    v-for="(item, index) of items"
    :key="item"
    :color="selectedTemplate && index === selectedIndex ? 'warning' : null"
    @click="
        selectedTemplate = item;
        selectedIndex = index;
    "
    >
    {{ item }}
</v-btn>
色を選ぶ際の三項演算子は、Boolean型のデータだともっとシンプルになる
  • 例えば「選択されていないボタンは text属性で背景をなくす」なら、下のような式になる
    • :text="!selectedItem || index !== selectedIndex"

演算を長くしたかったので無理やり上記のような式にしましたが、この式は厳密にはよくないです。右辺が真の場合は左辺も必ず真なので、 !selectedItem || は不要と言えるからです。ただし、右辺をなくす場合は dataselectedIndexを定義してやらなければエラーが出ます(右辺が偽の場合は必ず selectedIndexに値があるため、上の式ではエラーにならない)。ちなみに右辺だけだと選択したボタンも背景が消えますので判定式の意味がなくなる。

warning系のカラー?

マテリアルデザインではおなじみのsuccessやerrorなどアラート用要素に、既に組み込みクラスとしての色の組み合わせが用意されています。
Vuetifyでは、success, info, warning, errorの四つのタイプが用意されているようです。

もちろん、他の色のクラスも利用できます。

参考にしたサイト

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

【VSCode】Debugger for Chrome+RemoteDevelopmentで一日溶かした

解決する課題

VSCodeでDebugger for Chromeのプラグインを使ってvue.jsのデバックする

https://jp.vuejs.org/v2/cookbook/debugging-in-vscode.html
この方法。

ソースコードでブレークポイントを設定しようとしても灰色のマルがついて設定できない。

解決策

webpackのdevtoolの設定を、'inline-source-map'にする。

devtool: 'source-map'

devtool: 'inline-source-map'

inlineにする理由

RemoteDevelopmentを使用して開発していたため、source-mapがchromeブラウザ側で参照できてなかった様子。

Debugger for Chromeのドキュメントに次の記述がありました。

Usage with remote VSCode extensions
~~省略~~
Since the extension can't currently access the remote disk, sourcemaps can't be read from disk. If sourcemaps are inlined, they will still be used. If possible, they will be downloaded through your webserver.

remote のディスクにアクセスできないからsource-mapを参照できない。

vue.jsでは、webpackを使っていて、source-mapの設定をしていて、まさにこれに当てはまってました。

inlineだったら取得できる。

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

【VSCode】Debugger for ChromeでVue.jsのデバックする時、ハマる罠(解決済)

解決する課題

VSCodeでDebugger for Chromeのプラグインを使ってvue.jsのデバックする

https://jp.vuejs.org/v2/cookbook/debugging-in-vscode.html
この方法。

ソースコードでブレークポイントを設定しようとしても灰色のマルがついて設定できない。

解決策

webpackのdevtoolの設定を、'inline-source-map'にする。

devtool: 'source-map'

devtool: 'inline-source-map'

inlineにする理由

RemoteDevelopmentを使用して開発していたため、source-mapがchromeブラウザ側で参照できてなかった様子。

Debugger for Chromeのドキュメントに次の記述がありました。

Usage with remote VSCode extensions
~~省略~~
Since the extension can't currently access the remote disk, sourcemaps can't be read from disk. If sourcemaps are inlined, they will still be used. If possible, they will be downloaded through your webserver.

remote のディスクにアクセスできないからsource-mapを参照できない。

vue.jsでは、webpackを使っていて、source-mapの設定をしていて、まさにこれに当てはまってました。

inlineだったら取得できる。

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