[Nuxt.js]2.9以下 Google material icon(マテリアルアイコン)を使う

みんな大好きGoogle fontsではアイコンも利用できますね。私は少し前までfontawesomeを使用していたのですが、最近はちょっと非推奨の環境が増えてきたので、Google fontsのアイコンに鞍替えしました。

以下はnuxt.js2.9以下で実装するための手順です。それ以降はこちらの記事にまとめました。

[Nuxt.js]2.9以上 Google material icon(マテリアルアイコン)を使う

参考

https://developers.google.com/fonts/docs/material_icons

https://fonts.google.com/icons?selected=Material+Icons

nuxt.config.jsにリンクを追加

nuxt.config.jsにGoogle Material Iconsのリンクを追加します。


    link: [
      { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
      {
        rel: 'stylesheet',
        href: 'https://fonts.googleapis.com/icon?family=Material+Icons',
      },
    ],

Google fontsサイトから使いたいアイコンを取得

下記サイトでアイコンを検索し、任意のアイコンを選択します。

https://fonts.google.com/icons?selected=Material+Icons

選択すると右側にコードが出ますので。

material icon

コピーします。

ページやコンポーネントにペースト

試しにheader.vueへ入れてみました。

header.vue
header.vue

表示結果

※左端のシェアアイコンです

[Nuxt.js]コンポーネントが表示されない(nuxt.config.jsにパスは通した?)

久しぶりにNuxtプロジェクト作成していつも通りコンポーネント作成したらコンポーネントが表示されなくて困ったので備忘録。Atomic Design(アトミックデザイン)など、componentsフォルダの中に分類のためのフォルダを作成している人のための記事です。

結論から言うと、

  • 新バージョンではコンポーネント名に内包されているフォルダ名をつけなければいけない(例:Atomsフォルダに入っているButton.vueコンポーネントは<AtomsButton/>と表記)
  • それが嫌ならnuxt.config.jsでパス通してね(下記に記載)

ということでした。

全部公式に載ってます。

https://nuxtjs.org/docs/directory-structure/components/#nested-directories

当方環境

  • @nuxt/cli v2.15.8
  • MacOS 12.1

nuxt.config.jsでパスを通す

以下公式からの引用ですが、

components/
  base/
      foo/
         CustomButton.vue

このような階層構造を持つコンポーネントをほかのコンポーネントやページで使用する際、コンポーネントの記載はこのようになります。

<BaseFooCustomButton />

でもいちいち上位フォルダの名前記載するの面倒ですよね。というわけパスを通します。
nuxt.config.jsを開き、componentsの箇所に追記します。

以下、Atomic Designを採用している場合の例です。

  components: {
    dirs: [
      '~/components',
      '~/components/atoms',
      '~/components/molecules',
      '~/components/organisms',
    ],
  },

これで、今まで(?)通り、コンポーネントのタグ名は<Button />のみでOKとなりました!

 

めでたしめでたし。

 

 

[Nuxt.js, Vue.js, ESLint]監視プロパティwatchではアロー関数が使えない

いつもお世話になっております。

監視プロパティwatcherの挙動を勉強していて気づいたことを備忘録。

監視プロパティwatch内ではアロー関数を使うべきではない

nuxt.jsで、監視プロパティであるwatchのオプションdeepの挙動を調べていたら、怒られました。

<script>
export default {
  data() {
    return {
      colors: [
        { name: 'red' },
        { name: 'blue' },
        { name: 'green' },
      ],
    }
  },
  watch: {
    colors: {
      handler:(newValue, oldValue) => {
        console.log('updated!')
      },
      deep: true,
    },
  },
}
</script>
ESLint caution: You should not use an arrow function to define a watcher
はい

なぜなら、アロー関数はthisの挙動が変わってしまうため。アロー関数は親のコンテキストをバインドしており、thisの挙動が意図しているVueインスタンスとは異なってしまう。

This rules disallows using arrow functions to defined watcher.The reason is arrow functions bind the parent context, so this will not be the Vue instance as you expect.(see here for more details (opens new window))

eslint-plugin-vue

thisの挙動についてはここがわかりやすかったです!

https://qiita.com/mejileben/items/69e5facdb60781927929

というわけで、こう


<script>
export default {
  data() {
    return {
      colors: [
        { name: 'red' },
        { name: 'blue' },
        { name: 'green' },
      ],
    }
  },
  watch: {
    colors: {
      // handler:(newValue, oldValue) => {
      handler(newValue, oldValue) {
        console.log('updated!')
      },
      deep: true,
    },
  },
}
</script>

 

[Nuxt.js]フィルターを作成する

Nuxtのファイル構成を意識したNuxt.jsでのフィルターの作成方法です。Vueのやり方だと上手く行かなかったため、備忘録。

グローバルフィルター

pluginsディレクトリ内にjsフォルダを作成し、どのページでもフィルタが読み込めるようにする方法です。今回は、数字を3桁ごとにカンマで区切るフィルタと、英語の文字列を全て大文字にするフィルタを作成します。

詳しく

まずはpagesディレクトリ内にfilter.vueの雛形を作成します。

<template>
  <div>
    <Header />
    <v-container>
      <!-- local filter -->
      <h2>Mustash</h2>
      <p>{{ price | numberFormat }}</p>
      <p>{{ text | toUpperCase }}</p>
      <h2>b-vind</h2>
      <input type="text" :value="price | numberFormat" />
      <input type="text" :value="text | toUpperCase" />
    </v-container>
  </div>
</template>


<script>
export default {
    data() {
      return {
        price: 25400,
        text: 'Hello, Nuxt.js!',
    }
  },
}
</script>
グローバルフィルターはpluginsディレクトリ内にjsフォルダを作成し、その中にフィルターの機能を記述します。
import Vue from 'vue'

Vue.filter('toUpperCase', (value) => {
  return value.toUpperCase()
})

Vue.filter('numberFormat', (value) => {
  return value.toLocaleString()
}

最後に、nuxt.config.jsにグローバルフィルタの場所を追記します。

export default {
  plugins: [ { src: '~plugins/filter.js' } ],
}

ローカルフィルター

ローカルフィルターは、pagesディレクトリ内の各ページに定義するそのページ限定のフィルターです。せっかくなので、次は長い文字列を短く表現し、省略した以降の文字は「…」と表現させる「引数あり」のフィルターを作成します。

長文
これを
こう

詳しく

まずは雛形を作成します。

<template>
  <div>
    <v-container>
      <p>{{ longText }}</p>
    </v-container>
  <Footer />
  </div>
</template>

<script>
export default {
  data() {
    return {
      longText:
        'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
    }
  },
}
</script>

この時点ではこれです。

フィルターを加えます。

<script>
export default {
  filters: {
    readMore: (text, length, suffix) => {
      return text.substring(0, length) + suffix
    },
  }

  data() {
   // 省略
}
</script>

readMoreの第一引数が適用する文字列、lengthが長さ、suffixが接尾辞です。

文字列textに対し.substringメソッドで0番目の文字から第二引数で渡した文字数(length)の、文字列の部分集合を返し、最後に指定したsuffixを加える、という処理が書かれています。

上記フィルターをlongTextに適用すると、コード全文は下記のようになります。

<template>
  <div>
    <v-container>
      <p>{{ longText | readMore(20, '...') }}</p> // フィルタを適用
    </v-container>
  <Footer />
  </div>
</template>

<script>
export default {
  filters: {
    readMore: (text, length, suffix) => {
      return text.substring(0, length) + suffix
    },
  }

  data() {
    return {
      longText:
        'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
    }
  },
}
</script>

フィルターでは第三引数まで指定していたのに、textの引数は書かれていないことに注意してください。

 

フィルターはv-bind式でもできる

mustache構文を使わないでフィルターを適用させたい時もあるかと思います。2.1.0以降でサポート。

https://jp.vuejs.org/v2/guide/filters.html

<!-- mustaches -->
{{ message | capitalize }}

<!-- v-bind -->
<div v-bind:id="rawId | formatId"></div>

v-bind式の構文は、

<div v-bind:id=”フィルタに渡すデータ | フィルタ”></div>

です。

 

おしまい

[Nuxt.js]AccuWeatherのAPIを使ってお天気アプリを制作

Nuxt.jsでAPIを使う修行を自分に課していました。というわけで、今回はお天気アプリです。

トップページのカードに、このように表示させます。

取ってきている情報自体は、任意の場所のその日のお天気と気温(と日付)だけなのですが、なかなかどうして時間のかかったものです。さて、復習しますか。

雛形となるカードを用意

まずは雛形を用意。お馴染みVuetifyを使っています。

<template>
  <div>
    <v-card class="mx-auto" max-width="374">
      <v-img
        height="250"
        src="https://images.unsplash.com/photo-1558486012-817176f84c6d?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8"
        gradient="to top, rgba(255,255,255,.4), rgba(255,255,255,.9)"
      >
        <v-list-item two-line>
          <v-list-item-content>
            <v-list-item-title class="text-h5">
              CURRENT WEATHER
            </v-list-item-title>
            <v-list-item-subtitle>YYYY-MM-DD</v-list-item-subtitle> // 今日の日付を取ってくる
            <v-list-item-subtitle>TOKYO</v-list-item-subtitle>
          </v-list-item-content>
        </v-list-item>

        <v-card-text>
          <v-row align="center">
            <v-col text-h2 cols="12">
              <v-list-item-subtitle>お天気ステータス</v-list-item-subtitle>
            </v-col>
            <v-col cols="12">
              <v-card-subtitle class="text-h1 temp">温度
                <span class="celcius">&#8451;</span>
              </v-card-subtitle>
            </v-col>
          </v-row>
        </v-card-text>
      </v-img>
    </v-card>
  </div>
</template>

AccuWeatherから情報を取ってくる

さてこのAccuWeather、ちょっと苦戦しました。アクセス1日50回(だったかな?)なら無料のお天気APIです。

https://developer.accuweather.com/

必要なことは大体公式サイトに載っているのですが、ここからお天気の情報を取ってくるには何段階かステップが必要です。

  1. AccuWeatherに登録する
  2. AccuWeatherでAppを作る
  3. APIキーゲト
  4. ロケーションキーをゲト

AccuWeatherに登録する

まずは公式サイトでアカウントを作成します。

https://developer.accuweather.com/

右上のREGISTERからですね。

AccuWeatherでAppを作る

諸々入力し終え、アカウントが作成できたら、メニューバーのMY APPからAdd a new Appを選択します。Appの名前とかはどうでもいいです。今回は無料版を使うため、Productの箇所ではLimited Trialを選択しましょう。

 

APIキーゲト

上記の設定を終えたらCreate Appボタンを押します。

APIキーが取得できました。

ロケーションキーをゲト

APIキーをゲットしたら次はロケーションキーが必要ですね。

こちらにアクセスするとロケーションキーがわかります。

https://www.accuweather.com/

検索窓から取得したいロケーションを入力します。「東京」や「サンフランシスコ」などで選択肢が表示されます。

ページ遷移した先のURL最後の数字がロケーションキーです。

AccuWeatherのメニューバーAPI REFERENCE > Forecast APIを選択し、取りたいデータを選択します。今回はHour of Hourly Forecastsを選択しました。

https://developer.accuweather.com/accuweather-forecast-api/apis/get/forecasts/v1/hourly/1hour/%7BlocationKey%7D

一番上のResource URLの部分に、お天気情報を取得するためのURLが書かれていますね。

http://dataservice.accuweather.com/forecasts/v1/hourly/1hour/{LOCATIONKEY}

この{LOCATIONKEY}の部分を先ほど取得した数値に置き換え、APIキーをつけてアクセスすると格納されている情報が見られるようになります。

例:http://dataservice.accuweather.com/forecasts/v1/hourly/1hour/226396?apikey=XXX

XXXの箇所は、先ほど取得した自分のAPIに置き換えてください。

アクセス先

Chromeの拡張機能JSON Viewを使うと見やすいです。

こちらのURLをスクリプトのmounted部分で使います。

取得したお天気情報をaxiosでゲットし、変数に代入する

 

APIキーはconstantsディレクトリのdefine.jsに記載しました。またaxiosはダウンロードしている前提です。インストール方法はこちらから

https://www.npmjs.com/package/axios

export const API_KEY = 'xxx'
<script>
import axios from 'axios'
import { API_KEY } from '~/constants/define'


export default {
  data() {
    return {
      forecasts: [],
    }
  },
  mounted() {
    axios
      .get(
        'http://dataservice.accuweather.com/forecasts/v1/hourly/1hour/226396' +
        `?apikey=${API_KEY}`
      )
      .then((response) => (this.forecasts = response.data))
  },
}
</script>

これで、先ほどアクセスした http://dataservice.accuweather.com/forecasts/v1/hourly/1hour/226396?apikey=XXX の情報をforecasts[]に代入することができました。

画面に天気を表示させてみます。カードにv-forを追加しました。

    <v-card-text v-for="forecast in forecasts" :key="forecast.id">
      <v-row align="center">
        <v-col text-h2 cols="12">
          <v-list-item-subtitle>{{
            forecast.IconPhrase
          }}</v-list-item-subtitle>
        </v-col>
        <v-col cols="12">
          <v-card-subtitle class="text-h1 temp">
            {{ forecast.Temperature.Value | toCelsius | mathFloor }}
            <span class="celcius">℃</span>
          </v-card-subtitle>
        </v-col>
      </v-row>
    </v-card-text>

気温は華氏表記だったため、フィルターを作成し摂氏表記にします。

        <v-col cols="12">
          <v-card-subtitle class="text-h1 temp">
            {{ forecast.Temperature.Value | toCelsius | mathFloor }}
            <span class="celcius">℃</span>
          </v-card-subtitle>
        </v-col>

Scriptには下記を追記

<script>

// 省略
export default {
  filters: {
    toCelsius(value) {
    return ((value - 32) * 5) / 9 // fahrenheit to celsius
    },
    mathFloor(value) {
    return Math.floor(value)
    },
  },

 

 

<script>
import axios from 'axios'
import { API_KEY } from '~/constants/define'


export default {
  filters: {
    toCelsius(value) {
    return ((value - 32) * 5) / 9
    },
    mathFloor(value) {
    return Math.floor(value)
    },
  },
  data() {
    return {
      forecasts: [],
      hasError: false,
      loading: true,
      now: '',
    }
  },
  mounted() {
    this.now = this.$dayjs().format('YYYY-MM-DD')
    axios
      .get(
        'http://dataservice.accuweather.com/forecasts/v1/hourly/1hour/226396' +
        `?apikey=${API_KEY}`
      )
      .then((response) => (this.forecasts = response.data))
      .catch(function (error) {
        console.log(error)
        this.hasError = true
      })
      .finally(() => (this.loading = false))
  },
}
</script>

あとは、dayjsを使い今日の日付を表示、error時とloading時の表示を追加して完成です。

 

      <v-list-item-content>
        <v-list-item-title class="text-h5">
          CURRENT WEATHER
        </v-list-item-title>
        <v-list-item-subtitle>YYYY-MM-DD</v-list-item-subtitle> // 今日の日付を取ってくる
        <v-list-item-subtitle>TOKYO</v-list-item-subtitle>
      </v-list-item-content>

    <v-card-text v-for="forecast in forecasts" :key="forecast.id">
      <section v-if="hasError">Error.</section>. // エラーメッセージを表示
      <section v-else> // エラーじゃない時は正常に表示
        <div v-if="loading">Loading...</div> // ローディング時の表示を追加
          <v-row align="center">
          </v-row>
        </section>
      </v-card-text>
 // 省略

export default {
 // 省略
  data() {
    return {
      forecasts: [],
      hasError: false,
      loading: true,
      now: '',
    }
  },
  mounted() {
    this.now = this.$dayjs().format('YYYY-MM-DD')
    axios
      .get(
        'http://dataservice.accuweather.com/forecasts/v1/hourly/1hour/226396' +
        `?apikey=${API_KEY}`
      )
      .then((response) => (this.forecasts = response.data))
      .catch(function (error) {
        console.log(error)
        this.hasError = true
      })
      .finally(() => (this.loading = false))
  },
}
</script>

完成コード

すみません所々 インデントが合ってないかもしれないです

<template>
  <div>
    <v-card class="mx-auto" max-width="374">
      <v-img
        height="250"
        src="https://images.unsplash.com/photo-1558486012-817176f84c6d?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8"
        gradient="to top, rgba(255,255,255,.4), rgba(255,255,255,.9)"
      >
      <v-list-item two-line>
        <v-list-item-content>
          <v-list-item-title class="text-h5">
            CURRENT WEATHER
          </v-list-item-title>
         <v-list-item-subtitle>{{ now }}</v-list-item-subtitle> // 今日の日付を取ってくる
         <v-list-item-subtitle>TOKYO</v-list-item-subtitle>
       </v-list-item-content>
     </v-list-item>

      <v-card-text v-for="forecast in forecasts" :key="forecast.id">
        <section v-if="hasError">Error.</section>
        <section v-else>
          <div v-if="loading">Loading...</div>
          <v-row align="center">
            <v-col text-h2 cols="12">
              <v-list-item-subtitle>{{
                forecast.IconPhrase
              }}</v-list-item-subtitle>
            </v-col>
            <v-col cols="12">
              <v-card-subtitle class="text-h1 temp">
                {{ forecast.Temperature.Value | toCelsius | mathFloor }}
                <span class="celcius">℃</span>
              </v-card-subtitle>
            </v-col>
           </v-row>
         </section>
        </v-card-text>
      </v-img>
    </v-card>
  </div>
</template>


<script>
import axios from 'axios'
import { API_KEY } from '~/constants/define'


export default {
filters: {
toCelsius(value) {
return ((value - 32) * 5) / 9
},
mathFloor(value) {
return Math.floor(value)
},
},
  data() {
    return {
      forecasts: [],
      hasError: false,
      loading: true,
      now: '',
    }
  },
  mounted() {
    this.now = this.$dayjs().format('YYYY-MM-DD')
    axios
      .get(
        'http://dataservice.accuweather.com/forecasts/v1/hourly/1hour/226396' +
        `?apikey=${API_KEY}`
      )
      .then((response) => (this.forecasts = response.data))
      .catch(function (error) {
        console.log(error)
        this.hasError = true
      })
      .finally(() => (this.loading = false))
  },
}
</script>


<style scoped>
.temp {
  margin-top: -2rem;
}
.celcius {
  font-size: 1.7rem;
}
</style>

参考にしたのはこちらのサイト

https://www.npmjs.com/package/iobroker.accuweather

https://www.accuweather.com/

アクセスした際は日本語になっている可能性があるため注意。

axiosのmounted部分、なんでアロー関数じゃなきゃダメなんだろう?と思っていたら、こういうことでした。

なので、functionの外でページのインスタンスであるthisを違う変数に突っ込むと、アロー関数じゃない書き方でもthisが使えるようになるようです。

mounted() {
  this.now = this.$dayjs().format('YYYY-MM-DD')
  const self = this
  axios
    .get(
      'http://dataservice.accuweather.com/forecasts/v1/hourly/1hour/226396' +
      `?apikey=${API_KEY}`
    )
    .then(function (response) {
      console.log(response)
      self.forecasts = response.data
})
// 以下省略
あるいは宣言せずに.bind(this)する
.then(
  function (response) {
    console.log(response)
    this.forecasts = response.data
  }.bind(this)
)

 

[Nuxt.js]componentsのButtonを使ってDeleteボタンDeleteBtnを実装

いつもお世話になっております。

最近アトミックデザインなるものを覚えたのですが、具体的な使い方がピン…!と来ていませんでした。

componentsをatomsやorganismsに分けるのはわかるんですけどmoleculesって一体どんなもんに使うのよ?ってなもんでした。

ところがtodoリストでリストしたアイテムのdeleteボタンを$emitで実装する過程で、なるほど!と膝を叩いたので備忘録的に残しておきたいと思います。

to-do list

このアイテム1つ1つに入っているDELETEボタンです。

親であるtodo.vueはこんな感じ。UIはVuetifyを使っています。

<v-card
  v-for="(todo, index) in todos"
  :key="index"
  max-width="344"
  outlined
  style="margin: 10px"
  >
  <v-list-item>
    <v-list-item-content style="display: block">
      <input v-model="todo.isDone" type="checkbox" />
      <v-list-item-title
        class="text-h6 mb-1 item-title"
        :class="{ done: todo.isDone }"
      >
        {{ todo.item }}
      </v-list-item-title>
      <DeleteBtn @deleteTodo="deleteItem(index)" />
    </v-list-item-content>
  </v-list-item>
</v-card>

この後DeleteBtnで<v-btn>作って$emitしてもよかったのですが、既に<Button>コンポーネントは作成済みでした。

<template>
  <v-btn
    :color="color"
    :class="classValue"
    :tile="tile"
    :disabled="disabled"
    :text="text"
    :outlined="outlined"
    :block="block"
    :elevation="elevation"
    :small="small"
    :rounded="rounded"
    @click="click"
  >
  <slot></slot>
  </v-btn>
</template>

<script>
export default {
  props: {
    click: {
      type: Function,
      required: true,
    },
    color: {
      type: String,
      default: '',
    },
    classValue: {
      type: String,
      default: '',
    },
    tile: {
      type: Boolean,
      default: false,
    },
    disabled: {
      type: Boolean,
      default: false,
    },
    text: {
      type: Boolean,
      default: false,
    },
    outlined: {
      type: Boolean,
      default: false,
    },
    block: {
      type: Boolean,
      default: false,
    },
    elevation: {
      type: String,
      default: '0',
    },
    small: {
      type: Boolean,
      default: true,
    },
    rounded: {
      type: Boolean,
      default: true,
    },
  },
}
</script>
既に作成したButtonはデフォルトのButtonとして残すのがUIの統一性を考えるにしてもいいような気がします。
コンポーネントにコンポーネントも配置できるので、DELETEボタンのファイルであるDeleteBtn.vueのボタン部分にButton.vueを使用すれば良いのですね。
このように粒度の小さいコンポーネントを用意しておき(atoms)、小さいコンポーネントを組み合わせて大きめのコンポーネント(molecules)を作るようにデザインするのに向いているのがアトミックデザインの長所ですよ〜というのは私の師匠からの受け売りです。
<template>
  <div>
    <Button :click="itemDelete">DELETE</Button>
  </div>
</template>

<script>
export default {
  methods: {
    itemDelete() {
    this.$emit('deleteTodo')
    },
  },
}
</script>

【Nuxt.js】console.logでWarning :Unexpected console statementが出たときの対処法

いつもお世話になっております。今日もESLintに振り回されています。

検証のために入れたconsole.logにESLintがWarning出してきて、動くけどナンカキモチワルイ…という状態が続いておったのですね。

In JavaScript that is designed to be executed in the browser, it’s considered a best practice to avoid using methods on console. Such messages are considered to be for debugging purposes and therefore not suitable to ship to the client. In general, calls using console should be stripped before being pushed to production.

JavaScriptはブラウザで実行されるようデザインされているからconsoleで実行されるメソッドは避けるのがベストプラクティスだぜ!とESLintさんはおっしゃる訳です。そうは言ってもデバッグせにゃあ。

そんなわけで、プロジェクトディレクトリ直下の .eslintrc.js に下記を追加。

rules: {
'no-console': 'off', // console.log();OK
'no-unused-vars': 'off', // 使っていない変数あってもOK
},

しかしこちらを追記しても消えないWarning…なぜだ?

ESLint Warning

色々な記事を読み漁ってもルールを追記することしか書かれていません。同僚に相談したところ、「なんかESLint再起動?みたいなのした気がする」。とのこと。

yarn lintを実行。npm使ってる人はnpm run lintかな。

$ projectname % yarn lint
yarn run v1.22.10
$ yarn lint:js && yarn lint:style
$ eslint --ext ".js,.vue" --ignore-path .gitignore .
$ stylelint "**/*.{vue,css}" --ignore-path .gitignore
✨ Done in 3.24s.
$ projectname % yarn dev
yarn run v1.22.10

解決しました🥳🥳🥳

どうやらわざわざyarn devを実行しなくても、エディターとESLintをリンクさせる方法があるようです。

各種エディタでESLintのプラグインが提供されています。
これを使うことでいちいちコマンドを叩かずに済みます。
http://eslint.org/docs/user-guide/integrations#editors

Step by Stepで始めるESLint