20191204のvue.jsに関する記事は25件です。

2019年 よく使ったJavaScriptたちをまとめる

はじめに

JavaScript Advent Calendar 2019の4日目です。
本日は@watsuyo_2が「2019年 よく使ったJavaScriptたちをまとめる」を記事にします!

フロントエンドエンジニア1年目を振り返りながら、これからJavaScriptを学ぶ人やよく使うメソッドを知りたい人がわかりやすいようにまとめます。
普段、 Nuxt.js(Vue.js)Firestore を使用した開発をしているので例としてでてきますがご了承ください。

↓は1年前に転職するまでの流れを書いた記事にあります。
新年からWebエンジニアになる僕に2018年、起こったこと

Array.prototype.map()

配列の要素をひとつひとつ取り出し、第一引数の値(例で言うx)を元に処理を行い、その結果から新たな配列を作れる

実務的な例

よく使うパターンとして、オブジェクト配列をそれぞれ取り出して、Firestoreにデータ更新、取得するような時に使います。

const todos = [
  { id: 'H6BHBW7WS7H8DAT8HAK', content: '早起きする' },
  { id: 'HGX3DMZ9ZT8GN7KHYUY', content: '早く寝る' },
  { id: '4MT32NDNS6XFJUNGRPH', content: '牛乳を買う' },
  { id: 'PHCXSNTRVZHNAJYWJT6', content: 'Youtubeは1日2時間まで' }
]

todos.map(todo => (
  firebase.firestore().collection(`todos/${todo.id}`).update({ todo.content })
))

Array.prototype.filter()

配列の要素をひとつひとつ取り出し、第一引数の値(例で言うx)を元に比較を行い、trueだった結果から新たな配列を作れる

実務的な例

オブジェクト配列をそれぞれ取り出して、特定の要素を削除する場合などで使いました。

const todos = [
  { id: 'H6BHBW7WS7H8DAT8HAK', content: '早起きする' },
  { id: 'HGX3DMZ9ZT8GN7KHYUY', content: '早く寝る' },
  { id: '4MT32NDNS6XFJUNGRPH', content: '牛乳を買う' },
  { id: 'PHCXSNTRVZHNAJYWJT6', content: 'Youtubeは1日2時間まで' }
]

// 特定のtodo.idが'PHCXSNTRVZHNAJYWJT6'以外の配列を作成
const deleteOne = () => todos.filter(todo =>
  todo.id != 'PHCXSNTRVZHNAJYWJT6'
)

console.log(deleteOne())
// [
//   { id: 'H6BHBW7WS7H8DAT8HAK', content: '早起きする' },
//   { id: 'HGX3DMZ9ZT8GN7KHYUY', content: '早く寝る' },
//   { id: '4MT32NDNS6XFJUNGRPH', content: '牛乳を買う' }
// ]

Array.prototype.find()

配列の要素をひとつひとつ取り出し、第一引数の値(例で言うx)を元に比較を行い、trueだった結果を取得する

実務的な例

オブジェクト配列をそれぞれ取り出して、配列中の特定の要素を抽出する場合などで使いました。

const todos = [
  { id: 'H6BHBW7WS7H8DAT8HAK', content: '早起きする' },
  { id: 'HGX3DMZ9ZT8GN7KHYUY', content: '早く寝る' },
  { id: '4MT32NDNS6XFJUNGRPH', content: '牛乳を買う' },
  { id: 'PHCXSNTRVZHNAJYWJT6', content: 'Youtubeは1日2時間まで' }
]

const pickUpTodo = () => todos.find(todo =>
  todo.id === 'PHCXSNTRVZHNAJYWJT6'
)

console.log(pickUpTodo()) //{ id: 'PHCXSNTRVZHNAJYWJT6', content: 'Youtubeは1日2時間まで' }

Array.prototype.push()

配列に新しい要素を追加する

実務的な例

配列になにか新しい要素を追加する場面はよく遭遇しました。

const todos = [
  { id: 'H6BHBW7WS7H8DAT8HAK', content: '早起きする' },
  { id: 'HGX3DMZ9ZT8GN7KHYUY', content: '早く寝る' },
  { id: '4MT32NDNS6XFJUNGRPH', content: '牛乳を買う' },
  { id: 'PHCXSNTRVZHNAJYWJT6', content: 'Youtubeは1日2時間まで' }
]

todos.push({ id: 'WRABJ9IY4RJML7L4OB2', content: 'リーダブルコードを読む' })

console.log(todos)
// [
//  { id: 'H6BHBW7WS7H8DAT8HAK', content: '早起きする' },
//  { id: 'HGX3DMZ9ZT8GN7KHYUY', content: '早く寝る' },
//  { id: '4MT32NDNS6XFJUNGRPH', content: '牛乳を買う' },
//  { id: 'PHCXSNTRVZHNAJYWJT6', content: 'Youtubeは1日2時間まで' },
//  { id: 'WRABJ9IY4RJML7L4OB2', content: 'リーダブルコードを読む' }
// ]

String.prototype.match()

正規表現に対して文字列のマッチングし、マッチすればtrue、しなければfalseを返します。

実務的な例

入力されたデータに対し、バリデーションをかける時などでよく使いました。

const telA = '070-7374-0044'
const telB = '09024570411' 

console.log(telA.match(/^(0{1}\d{9,10})$/g)) // null
console.log(telB.match(/^(0{1}\d{9,10})$/g)) // true

三項演算子

if文の省略的な立ち位置になります。
以下がif文と三項演算子との対照表です。

パターンA: if文

const hoge = 0

if(hoge === 0) {
  return 'fuga'
} else {
  return 'hoge'
}

パターンB: 三項演算子

const hoge = 0

hoge === 0 ? 'fuga' : 'puge'

実務的な例

実務では上記の例のように全く対象のものとしては捉えておらず、可読性を重視します。
特にreturnで結果だけが欲しい場合では三項演算子を使うことが多いです。

例えばVue.js×TypeScriptに限る話になりますが、computed内で値がthisを使って参照する値が存在するかをチェックし、存在すればその値を返し、無ければnullを返す場合に使います。

computed: {
  todo (): Terms | null {
    return this.todo.titles ? todo.titles : null
  }
},

おわりに

今回、例としてあげた部分はほんの一部で、その他にもたくさんありますので奥は深いです。
振り返ってみても、JavaScriptフレームワークが様々な現場で普及している2019年ですが、基本的なメソッドや書き方は初心忘れるべからずということで押さえておきましょう。

メンションつきツイートをしていただけるとたいへん喜びます!

thanks-mentionsというQiitaの記事を作者に対してメンションを飛ばしながらツイートが出来るPWAを作りました?
ぜひ、メンションつきツイートをしていただけるとたいへんとてもとても喜びます!
popup.jpg

JavaScript Advent Calendar 2019

JavaScript Advent Calendar 2019明日の担当は、
@okumurakengoさんです!

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

Vue Devtoolsでパフォーマンス改善ポイントを見つける

この記事は、Vue.js Advent Calendar 201 9 #1 8 日目の記事です。

Vue Devtools の Performance タブを使って、パフォーマンスの改善ポイントをサクッと見つける方法を紹介したいと思います。
サクっと」というのがポイントです。

さらに、見つけた改善ポイントから実際にパフォーマンスを改善する例を紹介したいと思います。

Vue Devtools とは

Vue.js を使った開発におけるデバッグなどを助けてくれるツールです。

vuejs/vue-devtools

Chrome Extensionとしても提供されています。
機能ごとにタブが存在しており、今回はその中の Performance タブを使います。

Vue Devtools について詳しくは以下の記事が参考になると思います。

Vue Devtools で快適なデバッグ - ROXX(旧 SCOUTER)開発者ブログ

Performance タブ

Vue.js で構築されたアプリケーションのパフォーマンスを簡易的に計測できる機能です。

Chrome DevTools を使った計測ほど詳細は分からないものの、敷居が低く簡単に使うことができる印象があります。
パフォーマンスの悪いコンポーネントをサクッと見つけたい場合は、この Performance タブを使うのがおすすめです。

Performance タブの使い方についても上記の記事が参考になると思います。

Vue Devtools でパフォーマンス改善ポイントを見つける

早速、Vue Devtools の Performance タブを使ってパフォーマンス改善ポイントを見つけてみたいと思います。

改善対象のコンポーネント

改善対象のコンポーネントは以下のようなものになります。

  • 大量の選択肢から 1 つを選択するダイアログコンポーネント
  • 選択肢にマウスオーバーすると、その選択肢の説明を下の方に表示

実際の動作デモもあります。
https://vue-devtools-performance-handson.netlify.com

また、コードは以下に置いてあるのでよろしければ確認してみてください。
https://github.com/shun91/vue-devtools-performance-handson/blob/master/src/components/HeavyDialog.vue

動作がもっさり...

上記の動画やデモでも明らかですが、このダイアログは動作がもっさりしていて重いです。1

まず、ボタンをクリックしてからダイアログが開くまでが重くて、アニメーションももっさりしています。
さらに、マウスオーバーすると選択肢の背景色が変わるのですが、ここのアニメーションももっさりしています。

パフォーマンスを計測してみる

では、実際に Vue Devtools の Performance タブを使ってパフォーマンスを計測してみます。
今回は、選択肢にマウスオーバーした時のパフォーマンスを計測してみます。

まずは Frames per second から確認してみます。
その様子を動画にしてみましたが、選択肢にマウスオーバーした瞬間に明らかに fps が下がったことが分かると思います。

続いて、Component render を確認してみます。
こちらも、選択肢にマウスオーバーした瞬間に何やらたくさんの render 処理が走っているようです。

改善ポイントを見つける

計測結果をもう少し詳しく見てみます。

Component render では、左側のコンポーネント名をクリックすると、そのコンポーネント内で走った処理 (Lifecycle Hooks) にかかった時間と回数を確認することができます。

例えば、<VListTile> をクリックしてみると、以下のようになります。

なんと、選択肢にマウスオーバーしただけなのに updateRender が 600 回も実行されていて、トータルで 71333ms (≒ 71 秒!) もかかっていることが分かりました...
合わせて、<VRadio><VIcon> も同じように updateRender が 600 回も実行されていました。

つまり、<VListTile>, <VRadio>, <VIcon> のあたりにパフォーマンスを改善するためのポイントがありそうだと言えそうです。

このように、Performance タブを使えばパフォーマンスの悪いコンポーネントを簡単に見つけ出すことができます。

この記事の目的としてはここでもう果たせたのですが、せっかくなので実際にパフォーマンスを改善するところまでやってみたいと思います。

改善する

先程、<VListTile>, <VRadio>, <VIcon> に改善ポイントがありそうだということがわかりました。

実装を確認してみると、これらのコンポーネントは v-for で繰り返し描画されており、選択肢 1 つ 1 つを構成している要素だということが分かります。

<v-list-tile
  v-for="{ value } in items"
  :key="value"
  ripple
  @click="selected = value"
  @mouseenter="updateOnmoused(value)"
  @mouseleave="updateOnmoused('')"
>
  <v-list-tile-action>
    <v-radio :value="value" />
  </v-list-tile-action>

  <v-list-tile-content>
    <v-list-tile-title>{{ value }}</v-list-tile-title>
  </v-list-tile-content>
</v-list-tile>

1 つの選択肢にマウスオーバーしただけで 600 回も render 処理が走るということは、つまり、「1 つの選択肢にマウスオーバーしただけで、すべての <VListTile>, <VRadio>, <VIcon> が再描画されている」可能性がありそうです。2

これは明らかに無駄です。マウスオーバーした部分だけが再描画されれば十分のはずです。

再描画される部分をコンポーネント化する

無駄な部分まで再描画されないようにするためには、該当部分をコンポーネント化して切り出すとよいことがあります。
詳しくは以下の記事が参考になると思います。

コンポーネントを使って描画更新のコストを削減する | seihmd tech blog

では、<VListTile> の部分を <TheListItem> という名前のコンポーネントに切り出してみます。

diff --git a/src/components/HeavyDialog.vue b/src/components/HeavyDialog.vue
index cd89f9c..8297e4c 100644
--- a/src/components/HeavyDialog.vue
+++ b/src/components/HeavyDialog.vue
@@ -1,7 +1,7 @@
 <template>
   <v-dialog v-model="dialog" scrollable max-width="300px">
     <template #activator="{ on }">
-      <v-btn v-on="on" dark>open heavy dialog</v-btn>
+      <v-btn v-on="on">open light dialog</v-btn>
     </template>

     <v-card>
@@ -12,22 +12,14 @@
       <v-card-text class="pa-0" style="height: 300px;">
         <v-radio-group v-model="selected">
           <v-list class="pa-0">
-            <v-list-tile
-              v-for="{ value } in items"
-              :key="value"
-              ripple
-              @click="selected = value"
-              @mouseenter="updateOnmoused(value)"
+            <the-list-item
+              v-for="item in items"
+              :key="item.value"
+              :item="item"
+              @click="selected = item.value"
+              @mouseenter="updateOnmoused(item.value)"
               @mouseleave="updateOnmoused('')"
-            >
-              <v-list-tile-action>
-                <v-radio :value="value" />
-              </v-list-tile-action>
-
-              <v-list-tile-content>
-                <v-list-tile-title>{{ value }}</v-list-tile-title>
-              </v-list-tile-content>
-            </v-list-tile>
+            />
           </v-list>
         </v-radio-group>
       </v-card-text>
@@ -45,8 +37,11 @@

 <script lang="ts">
 import Vue from "vue";
+import TheListItem from "./TheListItem.vue";

 export default Vue.extend({
+  components: { TheListItem },
+
   data: () => ({
     dialog: false,
     selected: "",

変更後のコードは LightDialog.vue として保存します。

<TheListItem> の実装は以下のようになります。

<template>
  <v-list-tile
    ripple
    @click="$emit('click')"
    @mouseenter="$emit('mouseenter')"
    @mouseleave="$emit('mouseleave')"
  >
    <v-list-tile-action>
      <v-radio :value="item.value" />
    </v-list-tile-action>

    <v-list-tile-content>
      <v-list-tile-title>{{ item.value }}</v-list-tile-title>
    </v-list-tile-content>
  </v-list-tile>
</template>

<script lang="ts">
import Vue, { PropType } from "vue";

export default Vue.extend({
  props: { item: { type: Object as PropType<any>, required: true } }
});
</script>

改善後のパフォーマンスを計測してみる

コードを修正できたので、再度パフォーマンスを計測してみます。

まずは Frames per second からです。
見事に 60fps を維持することができています!

続いて Component render です。
<VListTile> は 1 回どころかまったく再描画されなくなりました!

改善前後を見比べても確かに動作が軽快になっていることが分かります。
動画だと分かりにくい場合は実際のデモを触ってみてください。

Before After

まとめ

この記事では、Vue Devtools の Performance タブを使って、パフォーマンスの改善ポイントを見つける方法を紹介しました。
さらに、見つけた改善ポイントに修正を加えて、実際にパフォーマンスを改善しました。

  • Performance タブを使えばパフォーマンスの悪いコンポーネントを簡単に見つけ出すことができます。
  • Chrome DevTools を使った計測ほど詳細は分からないものの、敷居が低く簡単に使うことができます。

また、今回使用したコードは以下のリポジトリに置いてありますのでよかったら確認してみてください。

shun91/vue-devtools-performance-handson
https://github.com/shun91/vue-devtools-performance-handson

最後までお読みいただきありがとうございました!mm

おまけ

  • 「ボタンをクリックしてからダイアログが開くまでが重い」のも、Performance タブを使って改善ポイントを見つけることができます。こちらはぜひご自身の手で試してみてください。 3
  • Vue Devtools の Performance タブについての記事が意外と見つけられなかったので、この記事が少しでも誰かのお役に立てれば嬉しいです。
  • この記事を書くためのデモを実装するのに、はじめは Vuetify 2.x を使っていたのですが、プロダクションビルドすると思ったようにパフォーマンスが悪くならない現象に遭遇しました。Vuetify 1.x にダウングレードしたところ、想定通りパフォーマンスが悪くなったので、Vuetify 2.x ではプロダクションビルド時に何らかのパフォーマンス最適化が行われるようになったみたいです。(すみません、詳しくは調べられていないです...)

  1. あえて重くなるような作りにしているだけではありますが... 

  2. この可能性を確信に変えるには、Chrome DevTools などでさらなる検証が必要ですが、この記事の主旨からはズレるので割愛します mm 

  3. 仮想スクロールを使うとパフォーマンスを改善できます。Vue.js だと vue-virtual-scroller というライブラリがあります。 

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

Vue.jsでちょっとおしゃんてぃなimport

はじめに

いなたつアドカレの四日目の記事です。

今回はVue.jsで使えるちょっとかっこつけたおしゃんてぃなimport方法について書いてくぜ

じっそー

router.js
const loadView = view => () => import(`./views/${view}.vue`)

export default new Router({

  mode: 'history',
  base: process.env.BASE_URL,
  routes: [
    {
      path: '/',
      name: 'home',
      component: loadView('Home')
    },
    {
      path: '/about',
      name: 'about',
      component: loadView('About')
    }
  ]
})

こんなかんじですね、前提として、viewに関するファイルがviewsディレクトリにまとまっているとおもってください。

loadViewで引数viewを受けて無名関数を高階的にimportする関数を返却しています。
これで、import文を別に何度も書く必要がなく、importすることができ、すこしおしゃんてぃな感じが醸し出せます。

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

vue-paginateのボタンにクリックイベントを設定する

vue-paginateのボタンにクリックイベントを設定する

vue-paginate

https://github.com/TahaSh/vue-paginate

スクリーンショット 2019-12-04 22.56.59.png

paginate-linksの要素内に、

@change="関数"

と指定するだけ。

<paginate-links for="paginate-resource" id="paginate-button" class="pagination" @change="scrollToTop" :show-step-links="true" v-if="tasks.length !== 0" :limit="3"></paginate-links>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue Composition APIで型がぶっ壊れて楽しかったです

Advent Calnder.png

これは bosyu Advent Calendar 2019 の 4日目の記事です。
よろしくね。

bosyuのフロントはVueを使っているのですが、最近 Composition API が上がってきているのでそれを使うようにしていこうぜ〜〜〜
という感じでやっております。

https://github.com/vuejs/composition-api
これね。

でこれがたまにアレなところがあるのですが、先日おもしろいところを見つけたので、それについて書いていくぞというアレです。
多分近々直ります。

なにがおきた

タイトル通りなんですが、型が不思議になりました。
具体的なコードを書くと以下のようなときに型が?????となります。

nazo.vue
export default createComponent({
  setup () {
    const state = reactive({
      status: 'hoge',
      value: 'fuga'
    });
    const v = state.value;
  }
});

image.png

ここでは state は以下のようになるように期待していますが、

type State = {
  status: string;
  value: string;
}

VSCode等でみると statestring となってしまいます :innocent:

なんでや

ということで、どんなかんじに型定義されているかみてみましょ。

https://github.com/vuejs/composition-api/blob/v0.3.4/src/reactivity/reactive.ts#L129

export function reactive<T = any>(obj: T): UnwrapRef<T> {
  if (process.env.NODE_ENV !== 'production' && !obj) {
    warn('"reactive()" is called without provide an "object".');
    // @ts-ignore
    return;
  }

  if (!isPlainObject(obj) || isReactive(obj) || isNonReactive(obj) || !Object.isExtensible(obj)) {
    return obj as any;
  }

  const observed = observe(obj);
  def(observed, ReactiveIdentifierKey, ReactiveIdentifier);
  setupAccessControl(observed);
  return observed as UnwrapRef<T>;
}

こんなかんじでした。
return のところの型が as になってて強さを感じる。

まあそれはさておきこの UnwrapRef<T> てのがきになりますね。
それを見てみましょう。

https://github.com/vuejs/composition-api/blob/v0.3.4/src/reactivity/ref.ts#L17

export type UnwrapRef<T> = T extends Ref<infer V>
  ? UnwrapRef2<V>
  : T extends BailTypes
      ? T // bail out on types that shouldn’t be unwrapped
      : T extends object ? { [K in keyof T]: UnwrapRef2<T[K]> } : T

// prettier-ignore
type UnwrapRef2<T> = T extends Ref<infer V>
  ? UnwrapRef3<V>
  : T extends BailTypes
      ? T
      : T extends object ? { [K in keyof T]: UnwrapRef3<T[K]> } : T

// ...

// prettier-ignore
type UnwrapRef10<T> = T extends Ref<infer V>
  ? V // stop recursion
  : T 

こんな感じになってます。
ConditionalTypes をつかって型を判定していますね。
http://www.typescriptlang.org/docs/handbook/advanced-types.html#conditional-types

でこの Ref の定義を見てみましょう。

https://github.com/vuejs/composition-api/blob/v0.3.4/src/reactivity/ref.ts#L9

export interface Ref<T> {
  value: T;
}

です。

つまり value をメンバーとして持っている場合は Ref を extends しているとみなされてしまい、
UnwrapRef2 UnwrapRef3 ... と流れていってしまいます。
最終的に UnwrapRef10 にたどり着き V となります。

結果的に最初に書いたように statestring のようになってしまうわけですね〜。

かいけつさく

https://github.com/vuejs/composition-api/pull/167
PRはでてますが、 Object の中に Object があったりすると、同様の問題が起きてしまうので困ったもんだなぁ〜という感じですね。

かんたんにはどうこうできるような感じではないので、できれば value というのはできる限りつかわないようにするのがとりあえずの対策かなぁ〜。

またVue-nextでは同様の問題が起きないような対処がされているようです。
https://github.com/vuejs/vue-next/blob/master/packages/reactivity/src/ref.ts

なのでこれと同様な実装を composition-api にPRでなげるかとかですかね。

まあそんな感じ。

あとは邪悪だけど

createComponent({
  setup () {
    const state = reactive({
      status: 'hoge',
      value: 'fuga'
    }) as unknown as { status: string, value: string };
    const v = state.value;
  }
});

unknown とか any とか一回型をぶっ壊して、むりやりやっちゃうとかね。
最高にダメな感じだけど。

まとめ

  • かたはむずかしい。
  • かたはたのしい。
  • as unknow as Hoge は最凶(強)。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.js初心者が学習のためにシンプルなマインスイパーを作った

はじめに

この記事は 2019 年まで素の JavaScript でコーディングしていた人間が初めて Vue.js を学習したときの記録です:notepad_spiral:
↓のURLにアクセスするとアプリに触れます。
https://vue-mine.firebaseapp.com

何を作ったか?

こんな感じのアプリです。
vue-mine.gif

見た目はあれです。今回は簡単な仕上がりを目指しました:thumbsup:(適当)
データバインディングの動きの確認をする程度の目的ですので、セルは単純な色分けで区別しています。

開発環境

Windows10
Node.js v12.13.1
Vue.js 2.6.10
vue/cli 4.1.1
Atom 1.41.0
Github https://github.com/t-ube/vue-mine

作成時につまづいた点と答え

Q. まずどのファイルをいじればいいのかわからない

A. <プロジェクトフォルダ>\src\App.vue を編集する

よっしゃさっそく作り始めたるわーと思い、コマンドラインで

vue create <プロジェクト名>

と打ち込んだ直後に僕は手が止まりましたが、 @567000 様の記事を参考にして、まずは App.vue 内の <template> を色々といじるところから始めました。
Vue.js を vue-cli を使ってシンプルにはじめてみる

Q. 表示条件の分岐ってどうやる?

A. 表示する要素のタグ内に制御用のif文を書く

Vue.js ではタグ内の v-if ディレクティブに指定した値が評価されて表示が切り替わります。
:point_down:のコードを例にとります。

App.vue
<p v-if="gameover">Game Over</p>
<p v-else-if="complete">Clear</p>
<p v-else>Please click green cell</p>

まず、変数 gameover の値が真のときに Game Over が表示されます。
それ以外でかつ変数 complete が真のときに Clear が表示されます。
それ以外のときには Please click green cell が表示されます。

素の JavaScript で getElementById で要素を取得してから表示/非表示の切り替えとか面倒なことをしていたのが嘘のようです。Vue.js は素晴らしいです:relaxed:

Q. マインスイーパーのセルの並べ方ってどうやる?

A. v-for で要素を並べて grid-template を使う

Vue.js / CSS の合わせ技です。

App.vue
<template>
~略~
<div id="map">
      <div v-for="cell in grid_size*grid_size"
~略~
</template>

<script>
~略~
#map {
  --grid-size: 6;
  display: grid;
  grid-template-columns: repeat(var(--grid-size), 30px);
  grid-template-rows: repeat(var(--grid-size), 30px);
  justify-content: center;
  align-content: end;
  padding: auto;
  margin: auto;
}
~略~

Vue.js では要素のタグ内に v-for ディレクティブを付けることで、同じ要素を繰り返し配置できます。

@miyauchoi 様の記事を参考にさせていただきました。
200行のVue.jsでスネークゲームを作った

Q. for 文で要素配置したらエラーになるんだけど?

A. v-bind:key が必要

v-for ディレクティブで要素を連続配置したらコンパイルエラーです。本当にありがとうございました。

./src/App.vue
Module Error (from ./node_modules/eslint-loader/index.js):
error: Elements in iteration expect to have 'v-bind:key' directives (vue/require-v-for-key) at src\App.vue:8:7:
   6 |     <p v-else>Please click green cell</p>
   7 |     <div id="map">
>  8 |       <div v-for="cell in grid_size*grid_size"
     |       ^
   9 |        v-on:click="dig_cell(cell-1)"
  10 |        :class="{
  11 |          cell: true,

理由はエラーメッセージに書かれているので一目瞭然ですね!
僕は軽く1時間は悩みましたがね。
:point_down:のようにコードを直しました。

App.vue
<div v-for="cell in grid_size*grid_size"
       v-on:click="dig_cell(cell-1)"
       v-bind:key="cell.id"
       :class="{

要素に値をバインドさせるためにidが必須とのことなので、v-for ディレクティブが動作しない怪現象に悩んだら v-bind:key を指定しているかを確認しましょう。

Q. クラスってどうやって作るのかわからない

A. コンポーネント化する(多分)

Google 先生で「Vue.js クラス」って検索しても CSS しかでてきません。
データを構造化したいときはどうするのでしょうか:disappointed_relieved::sos:
コンポーネント化するっぽいのですが。
これは諦めて今後の課題としました。
というわけで、セルごとのパラメータ(周囲の地雷の数、地雷フラグ、掘削フラグ)はすべて独立した配列で管理しています。

App.vue
data () {
    return {
~略~
      surround_bomb: [],
      is_bomb: [],
      is_dig: [],
~略~
    }

気持ちが悪いですね。早いところ修正したいと思います。

Q. セル要素の二次元座標はパラメータ化する?

A. 座標パラメータは不要で、配列サイズから計算する

配列のインデックスと座標の変換関数を用意します。
配列のインデックスを得るための関数 get_index に渡した座標が二次元座標の領域を超えている場合、存在しないものとして -1 を返しています。

App.vue
get_cell_x(index){
   return (Math.floor(index%this.grid_size));
},
get_cell_y(index){
   return (Math.floor(index/this.grid_size));
},
get_index(x,y){
   if(x < 0 || y < 0) return -1;
   else if(x>=this.grid_size || y>=this.grid_size) return -1;
   return ((y*this.grid_size)+x);
}

セルの周囲の地雷を数えるための関数 count_surrounding_bombs に配列のインデックスを渡すと内部で座標に変換し、周囲の 8 セル分をチェックします。

App.vue
is_bomb_index(index){
   return (this.is_bomb[index] == 1);
},
is_bomb_cell(x,y){
   return (this.is_bomb_index(this.get_index(x,y)));
},
count_surrounding_bombs(index) {
   let x = this.get_cell_x(index);
   let y = this.get_cell_y(index);
   let counts = 0;
   if(this.is_bomb_cell(x-1,y-1)) counts += 1;
   if(this.is_bomb_cell(x,y-1))   counts += 1;
   if(this.is_bomb_cell(x+1,y-1)) counts += 1;
   if(this.is_bomb_cell(x-1,y))   counts += 1;
   if(this.is_bomb_cell(x+1,y))   counts += 1;
   if(this.is_bomb_cell(x-1,y+1)) counts += 1;
   if(this.is_bomb_cell(x,y+1))   counts += 1;
   if(this.is_bomb_cell(x+1,y+1)) counts += 1;
   return counts;
}

感想

Vue.js のデータバインディングって強力ですね。
ミニゲームを簡単に作れてしまうのがすごいです:relaxed:
次はコンポーネント化にチャレンジしてみます。

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

Vee-validate3を使って、子コンポーネントのエラーを親コンポーネントで表示する

概要

vee-validate3系で親子コンポーネントのvalidationを試してみたらすごい簡単だったのでざっと紹介
サンプルコードはtypescriptになってます

使ったもの

  • Nuxt 2.8.1
  • vee-validate 3.0.3

サンプルコード

設定ファイル

plugins/veeValidate.ts
import Vue from 'vue';
import { ValidationObserver, ValidationProvider, localize, extend } from 'vee-validate';
import ja from 'vee-validate/dist/locale/ja.json';
import { required, max } from 'vee-validate/dist/rules';

Vue.component('ValidationProvider', ValidationProvider);
Vue.component('ValidationObserver', ValidationObserver);

extend('required', { ...required });
extend('max', { ...max });

localize('ja', ja);

親コンポーネント

<validation-observer> を使ってcomponentを囲むだけ
v-slot に子コンポーネントのメッセージや、validかどうかなどを取得できます

pages/test.vue
<template>
  <div class="newPost">
    <validation-observer v-slot="{ invalid, errors }">
      <template v-if="invalid">
        <div v-for="(error, i) in errors['test']" :key="`error${i}`">
          <p>{{ error }}</p>
        </div>
      </template>
      <test-form />
    </validation-observer>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { ValidationObserver } from 'vee-validate';

import TestForm from '~/components/TestForm.vue';

@Component({
  components: {
    TestForm
  }
})
export default class Test extends Vue {}
</script>

子コンポーネント

<validation-provider>で囲む

components/TestForm.vue
<template>
  <div class="test-form">
    <validation-provider vid="test" name="メッセージ" rules="required|max:10">
      <textarea v-model="message" />
    </validation-provider>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { ValidationProvider } from 'vee-validate';

@Component
export default class TestForm extends Vue {
  message: string = '';
}
</script>

vidを指定することで親コンポーネントでkeyとしてerrorsを判定できる

errors: {
  test: ['error message1', 'error message2']
}

これだけで完了です!

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

君はVue,Reactの次に来るSvelteを知っているか?

はじめに

この記事はAteam Brides Inc. Advent Calendar 2019 5日目の記事です。

はじめまして、エイチームブライズ新卒1年目の@oekazumaです。最近僕がハマっているSvelteに関して書きたいと思います!

Svelteとは?

1_OJLglSTFZ1PbwpRG0U2xXA.png

SvelteRich Harris氏によって開発されたコンパイラーでVueやReactのようにブラウザー上でコンポーネント化をするフレームワークではなく*.svelteファイルをhtml, js, cssに変換します。
「すらりとした」という意味を持つ名の通り軽量で高速。
ベンチマークでReactの35倍、Vueの50倍速いです。

Svelteの3つの魅力

公式にも書かれている下記の3つを中心に説明していきます!
1. Write less code (より少ないコードを書く)
2. No Virtual DOM (仮想DOMがない)
3. Truly reactive (本当に反応的)

Write less code(記述量が少ない)

入力フォームで変数aとbに値を入力し、足して表示するプログラムを例にしてみると

React 442文字

import React, { useState } from 'react';

export default () => {
  const [a, setA] = useState(1);
  const [b, setB] = useState(2);

  function handleChangeA(event) {
    setA(+event.target.value);
  }

  function handleChangeB(event) {
    setB(+event.target.value);
  }

  return (
    <div>
      <input type="number" value={a} onChange={handleChangeA}/>
      <input type="number" value={b} onChange={handleChangeB}/>

      <p>{a} + {b} = {a + b}</p>
    </div>
  );
};

Vue 263文字

<template>
  <div>
    <input type="number" v-model.number="a">
    <input type="number" v-model.number="b">

    <p>{{a}} + {{b}} = {{a + b}}</p>
  </div>
</template>

<script>
  export default {
    data: function() {
      return {
        a: 1,
        b: 2
      };
    }
  };
</script>

Svelte 145文字

<script>
    let a = 1;
    let b = 2;
</script>

<input type="number" bind:value={a}>
<input type="number" bind:value={b}>

<p>{a} + {b} = {a + b}</p>

すごく記述量が少ないことがわかると思います。
書き方自体はVueに似ている部分もあるので既にVueを書いている方だとそんなに違和感なく開発できそうです。

No Virtual DOM(仮想DOMがない)

仮想DOMはオーバーヘッドであると言っています。大きな理由としては「実DOMとの差分を計算するのって無料じゃないしオーバーヘッドだよね」というところにあります。
Svelteは仮想DOMを使用せずに同様のプログラミングモデルで十分なパフォーマンスで、状態遷移を考慮することなくアプリを構築できます。
以下の流れでいうとSvelteは1と4だけで済むということです。

仮想DOMでHTMLが書き換わるまでの流れ
1. 現在の状態(state)が変わる
2.再レンダリング(仮想DOMの再構成)を実行する
3.実DOMとの差分を計算する
4.実際にHTML(=実DOM)を書き換える

Truly reactive(本当に反応的)

ReactおよびVueは、状態変数が変更されたときに更新する場所を追跡できず、その結果、状態変数が存在するコンポーネント全体とそのすべての子を更新します。
一方、Svelteはアプリケーションを介してデータを追跡し、更新された変数に依存する変数のみを更新できます。

さいごに

日本では正直全然話題になっていませんが、海外のフロントエンド界隈では盛り上がっているようでこれから日本でも流行っていくのではないかなと勝手に思っています。
数年後にはVue,Reactと肩を並べて語られている気がする...(^ω^)
今は日本語文献がかなり少ないので盛り上げていってもっと身近にSvelteを感じられるようになれば嬉しいなと思います!
この記事では実践的な部分がなかったのですが、明日に@mkinsvelte3でToDoリストをチュートリアルと照らし合わせて作るぞ! 【入門編】を書いてくれるので楽しみにしていてください!

私たちのチームで働きませんか?

alt
エイチームは、インターネットを使った多様な技術を駆使し、幅広いビジネスの領域に挑戦し続ける名古屋の総合IT企業です。
そのグループ会社である株式会社エイチームブライズでは、一緒に働く仲間を募集しています!

上記求人をご覧いただき、少しでも興味を持っていただけた方は、まずはチャットでざっくばらんに話をしましょう。
技術的な話だけでなく、私たちが大切にしていることや、お任せしたいお仕事についてなどを詳しくお伝えいたします!

Qiita Jobsよりメッセージお待ちしております!

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

ゼロからVue.jsでビジュアルリグレッションテストするまでpart3/3

Part1 https://qiita.com/senku/items/07c3e2859ac90c03867a
Part2 https://qiita.com/senku/items/20e21033edd512be1d4d
Part3 ここ

前回までにStorybookを整えてきたのは、Storybookから画像を生成するためでした。
今回は画像の生成とそれ以降をやっていきます。テンション爆上げ。

Summary

  • storycapは神
  • reg-suitは神

storycapをいれる

storycapは、Storybookからスクリーンショットを生成するツールです。公式な話もどうぞ。→storybook-chrome-screenshotとzisuiとStorycapと
他にもJestから生成したり色々できるんですが、今回はやりません。

storycapインストールします。

$ npm install --save-dev storycap

puppeteerが入ってくるのでnode_modulesがでっかくなります。そこへの対処は後で。

とりあえずstorycapを動かしてみましょう。package.jsonのscriptsに、storycapを登録します。

package.json
diff --git a/package.json b/package.json
index 75cab63..0df9125 100644
--- a/package.json
+++ b/package.json
@@ -7,6 +7,7 @@
     "build": "vue-cli-service build",
     "lint": "vue-cli-service lint",
     "i18n:report": "vue-cli-service i18n:report --src './src/**/*.?(js|vue)' --locales './src/locales/**/*.json'",
+    "storycap": "storycap --serverCmd \"npm run storybook:ci\" http://localhost:6006 -o actual_images --serverTimeout 1
     "storybook:build": "vue-cli-service storybook:build -c config/storybook",
     "storybook:ci": "vue-cli-service storybook:serve -p 6006 -c config/storybook --ci",
     "storybook:serve": "vue-cli-service storybook:serve -p 6006 -c config/storybook"

Storycapのオプションを軽く説明しておきます。詳細はリポジトリのREADMEに書いてあるヨ。

オプション 設定値 説明
なし http://localhost:6006 Storybookが起動しているURLを指定します。
--serverCmd npm run storybook:ci Storybookを起動するためのコマンドを指定します。
Part1で作成したコマンドをここで使います。
-o
--outDir
actual_images キャプチャ結果の出力先ディレクトリを指定します。
--serverTimeout 120000 Storybookに接続するまでの待ち時間(ミリ秒)です。
デフォルト20秒ですが、起動が遅れた時のためにおまじない的につけてます。

今回は使いませんが、以下のオプションも使いがち。

オプション 説明
-V
--viewport
キャプチャするviewportを指定します。
-V 1024x768 -V 360x640のような複数指定もできます。
puppeteerが許可しないviewportにはできない模様。
--puppeteerLaunchConfig puppeteerのコンフィグを指定できます。
ブラウザの言語はこのオプションで渡すしかなさそう。日本語にする場合は、エスケープを含めてこんな感じ
--puppeteerLaunchConfig \"{\\\"args\\\":[\\\"--no-sandbox\\\",\\\"--disable-setuid-sandbox\\\",\\\"--disable-dev-shm-usage\\\",\\\"--lang=ja\\\"]}\"

画像が出力されるactual_images.gitignoreに登録しておきましょう。

.gitignore
diff --git a/.gitignore b/.gitignore
index a0dddc6..5b15dcf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,3 +19,5 @@ yarn-error.log*
 *.njsproj
 *.sln
 *.sw?
+
+actual_images

storycapを走らせてみます。もしnpm run storybook:servenpm run storybook:ciが動いている場合は停止してから実行してください。

$ npm run storycap

actual_imagesの下に、各Storyのキャプチャ画像が生成されましたね。これがビジュアルリグレッションテストの元ネタになります。

reg-suitをいれる

reg-suitは、2つの画像群を比較して差分をレポートしてくれるツールです。publish先としてS3とかGCS、notify先にGitHubやSlackに対応しています。今回はS3とGitHubを使います。

公式の手順ではグローバルにインストールしていますが、後々のためにプロジェクトローカルにインストールします。

$ npm install --save-dev reg-suit

設定を作成するためreg-suit initを走らせます。node_modulesの中のファイルを叩きますよ。

$ ./node_modules/.bin/reg-suit init

最初のプラグイン選択では、reg-keygen-git-hash-plugin, reg-notify-github-plugin, reg-publish-s3-pluginを選択しておきます。

次は共通の設定です。irectory contains actual images.には、storycapで出力したactual_imagesを指定しましょう。

? Working directory of reg-suit. .reg
? Directory contains actual images. actual_images // ここだけ変更
? Threshold, ranges from 0 to 1. Smaller value makes the comparison more sensitive. 0

reg-notify-github-pluginの設定は言われるがままにやります。GitHub認証のためにブラウザが開くので、認証後、通知するリポジトリのClientIDを取得して貼り付けましょう。

[reg-suit] info Set up reg-notify-github-plugin:
? notify-github plugin requires a client ID of reg-suit GitHub app. Open installation window in your browser Yes
? This repositoriy's client ID of reg-suit GitHub app // リポジトリのClientIDを入力

reg-publish-s3-pluginの設定では、AWS関連に環境変数があればバケットの自動作成も行えるみたいです。別途作ったバケットを設定することもできます。

[reg-suit] info Set up reg-publish-s3-plugin:
? Create a new S3 bucket Yes

こんな感じのregconfig.jsonが生成されれば完了です。
reg-suit initをせずに直接作成しても大丈夫です。その場合は.gitignore.regを追加されていないので、手動で追加しておきましょう。

regconfig.json
{
  "core": {
    "workingDir": ".reg",
    "actualDir": "actual_images",
    "thresholdRate": 0,
    "ximgdiff": {
      "invocationType": "client"
    }
  },
  "plugins": {
    "reg-keygen-git-hash-plugin": true,
    "reg-notify-github-plugin": {
      "clientId": "環境によってちがいます"
    },
    "reg-publish-s3-plugin": {
      "bucketName": "バケット名"
    }
  }
}

プラグインのオプションは色々設定できます。ここではreg-publish-s3-pluginのオプション例について軽く触れます。詳しくはreg-publish-s3-pluginのREADMEをみてください。

pathPrefixの指定があると、そのパスの下にファイルが配置されます。"pathPrefix": "hoge"ならS3BUCKET/hoge/COMMITID...って感じのパスになります。一つのバケットを複数のテストで使い回す場合はどうぞ。
customDomainの指定はでnotifyが通知するレポートの公開URLを調整できます。

regconfig.jsonの編集
    "reg-publish-s3-plugin": {
      "bucketName": "バケット名",,
      "pathPrefix": "配置先のPrefix"
      "customDomain": "レポート公開URLのFQDN"
    }

S3互換ストレージを使う場合はsdkOptionsでいろいろ設定できます。エンドポイントとか変えればいいですね。ここはAWS CLIのマニュアルを読んだほうがいいのかな。

regconfig.jsonの編集
    "reg-publish-s3-plugin": {
      "bucketName": "バケット名",
      "sdkOptions": {
        "endpoint": "エンドポイントのURL"
      }
    }

設定が完了したので、reg-suitを実行するためのスクリプトを作っておきましょう。

package.json
diff --git a/package.json b/package.json
index 953a07c..bbc6c2b 100644
--- a/package.json
+++ b/package.json
@@ -8,6 +8,7 @@
     "lint": "vue-cli-service lint",
     "i18n:report": "vue-cli-service i18n:report --src './src/**/*.?(js|vue)' --locales './src/locales/**/*.json'",
     "storycap": "storycap --serverCmd \"npm run storybook:ci\" http://localhost:6006 -o actual_images --serverTimeout 1
+    "reg-suit": "reg-suit run",
     "storybook:build": "vue-cli-service storybook:build -c config/storybook",
     "storybook:ci": "vue-cli-service storybook:serve -p 6006 -c config/storybook --ci",
     "storybook:serve": "vue-cli-service storybook:serve -p 6006 -c config/storybook"

これで準備ができました。

GitHubのmasterリポジトリに、ここまでのコミットをpushしておきます。(重要)

レポートを出していこう

まずは現在のコミット(masterの最新コミット)で生成したキャプチャ画像を、S3へpublishします。

今のmasterの画像をアップロード

すでにactual_imagesは生成されているので、reg-suit runを実行するだけです。

$ npm run reg-suit

実行後にS3バケットを確認すると、コミットのハッシュから始まる画像ファイルと、レポートのindex.htmlとその他ファイルがアップロードされています。
index.htmlを開くとこんな感じ。

image.png

コンポーネントを変更してPRをつくる

レポートをわかりやすくするため、見た目の差分があるPullRequestを作ります。
とりあえずブランチを切り替えましょう。

$ git checkout -b test

なんでもいいんですがHelloI18n.vueあたりを変更します。

src/components/HelloI18n.vue
diff --git a/src/components/HelloI18n.vue b/src/components/HelloI18n.vue
index 57ad691..21156c9 100644
--- a/src/components/HelloI18n.vue
+++ b/src/components/HelloI18n.vue
@@ -11,7 +11,7 @@ export default {
 <i18n>
 {
   "en": {
-    "hello": "Hello i18n in SFC!"
+    "hello": "Hello i18n in SFC!!!!!"
   }
 }
 </i18n>

コミットしてpush。

$ git add src/components/HelloI18n.vue
$ git commit -m "test commit"
$ git push origin test

GitHubにPullRequestを作成します。

image.png

参考までに、この時点でリモートブランチ(GitHub)はこんな状態になってます。

  • masterブランチの最新コミット
  • testブランチの最新コミット(masterブランチの最新コミットからfork)
    • testブランチの最新コミットから作られたPullRequest

reg-suitのpublish先のS3バケットには、masterブランチの最新コミットに対応するキャプチャ画像だけがアップロードされています。

PRにレポートを送る

現在のコミット(testブランチの最新コミット)のキャプチャ画像をstorycapで生成して、reg-suitで比較しましょう。

$ npm run storycap
$ npm run reg-suit

この作業によって、

  1. S3バケットにtestブランチの最新コミットに対応するキャプチャ画像がアップロードされ、
  2. reg-suitがmasterブランチの最新コミットtestブランチの最新コミットの画像を比較して、
  3. testブランチの最新コミットと関連するPullRequestにコメントを投稿

されます。PullRequestを見てみましょう。なんか書き込まれてますね。

image.png

コメントのリンク先のレポートでも差分が確認できます。

image.png

reg-suitはここまで自動でやってくれます。神。
基本的な動きはこれで完成です。後はCIを考えましょう。

CI戦略

CI戦略のために必要な情報を整理しておきます。

画像の比較のためには、PullRequestのfork元のコミットでstorycap+reg-suitが実行された(図A)上で、PullRequest自体の最新のコミットでstorycap+reg-suitが実行される(図B)必要があります。

master -->A - - - - - - -
          ↓ branch   ↑ PR
 branch   ---------->B  

GitHub flowを前提にすると、以下のタイミングでキャプチャの取得とreg-suitによる判定を行えばよさそうです。

  • masterブランチが進んだとき(masterへのmergeが起きたとき)
  • PullRequestが作成されたとき
  • PullRequestのブランチがpushされたとき

reg-notify-github-pluginは賢いので、これらのトリガーでnpm run storycapnpm run reg-suitが実行するだけで、PullRequestにコメントをつけるようになります。S3の容量が許すなら、全てのコミットに実行してもおそらく問題ありません。
reg-notify-slack-pluginとかだとPullRequestの存在確認ができないので、どのパターンか独自に判断しないと常に通知されることになります。

storycapをpackage.jsonから外したい

CIなんてDockerで走らせればいいんですよ!というわけでおもむろにstorycapをuninstallします。設定ファイルは残しておきます。
reg-suitをプロジェクトにインストールしたのはこのためでした。

$ npm uninstall storycap

次はDockerfileを作ります。storycapをインストールしつつ、npm run storycapnpm run reg-suitを実行させます。Puppeteer公式のRunning Puppeteer in Dockerを参考にしました。
宗教上の理由によりイメージを使い回さずdocker-buildで完結させます。
また、SSH鍵でGitHubにアクセスできると信じて、experimentalな機能でSSH鍵を渡しています。

# syntax = docker/dockerfile:experimental

FROM node:10-slim

ARG AWS_ACCESS_KEY_ID
ARG AWS_SECRET_ACCESS_KEY
ENV AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
ENV AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}

RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
    && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
    && apt-get update \
    && apt-get install -y ssh git google-chrome-unstable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf \
      --no-install-recommends \
    && rm -rf /var/lib/apt/lists/*
RUN usermod -aG audio,video node
RUN mkdir -p -m 0600 ~/.ssh
RUN --mount=type=secret,id=ssh ssh-keyscan -H github.com >> ~/.ssh/known_hosts
COPY . /home/node
WORKDIR /home/node

RUN npm ci
RUN npm install storycap

RUN npm run storycap
RUN --mount=type=secret,id=ssh npm run reg-suit

次はdocker buildするぞい。前述の宗教上の理由により--force-rmを付けます。experimentalな機能を有効にするためのDOCKER_BUILDKIT環境変数もバッチリだ。AWS用の環境変数はちゃんと--build-argで渡すんだ。

$ DOCKER_BUILDKIT=1 docker build -t reg-suit-gambaruzoi -f Dockerfile --force-rm \
--build-arg AWS_ACCESS_KEY_ID=あなたのアクセスキー \
--build-arg AWS_SECRET_ACCESS_KEY=あなたのシークレットアクセスキー .

これで環境を汚染せずにnpm run storycapnpm run reg-suitも実行できます。ヨカッタネ。

現場からは以上です。

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

Nuxt.js with TypeScript and Composition API

はじめに

この記事は Nuxt.js Advent Calendar 2019 6日目の記事です。

皆さん、Composition API 使ってますか? Composition API は現在開発中で、2020年1Qにもリリース予定の Vue.js 3.0 からデフォルトで搭載されることになっている新しいコンポーネントの実装方法です。Composition API を使うことで、散らかりがちだった実装を一箇所に固めることができたり、今まで組み合わせにくかった TypeScript との連携が比較的やりやすくなったりします。Vue.js 2.x にもプラグインとして導入すれば今すぐ使い始めることができるようになっています。

この記事では、その Composition API を Nuxt.js に導入し、さらに TypeScript を使ってなるべく1型の力を借りて型安全なフロントエンドを作成する方法を紹介します。

環境構築

諸注意

今回はnpmではなくyarnを使っていきます。npm派の方は適宜コマンドを読み替えてください。また、コマンドは全てmacOS Catalinaでの実行例となっています。Linuxなどをお使いの方はパッケージマネージャーなどが出てきた際に、適切なコマンドに読み替えてください。

準備

とりあえずnodeyarnを入れておきます。エディタはVSCodeやWebStormなどお好みで。

$ brew install node yarn

プロジェクトの初期化

yarn create コマンドでざっくり作っていきます。create-nuxt-appではまだTypeScriptを使ったプロジェクトの生成には対応していないので、いったんJavaScript用のプロジェクトを作ります。各種選択肢はお好みでOKです。一応どれを選んだかはここに書いておきます。

% yarn create nuxt-app composition-sample
yarn create v1.19.2
[1/4] ?  Resolving packages...
[2/4] ?  Fetching packages...
[3/4] ?  Linking dependencies...
[4/4] ?  Building fresh packages...
success Installed "create-nuxt-app@2.12.0" with binaries:
      - create-nuxt-app

create-nuxt-app v2.12.0
✨  Generating Nuxt.js project in composition-sample
? Project name composition-sample
? Project description My primo Nuxt.js project
? Author name Aruneko
? Choose the package manager Yarn
? Choose UI framework Vuetify.js
? Choose custom server framework None (Recommended)
? Choose Nuxt.js modules Axios
? Choose linting tools ESLint, Prettier
? Choose test framework None
? Choose rendering mode Universal (SSR)
? Choose development tools (Press <space> to select, <a> to toggle all, <i> to i
nvert selection)

TypeScript対応

基本的に公式サイトのやり方に従えばOKですが、簡単に手順を記しておきます。

まずはビルド時にTypeScriptを有効化する設定です。@nuxt/typescript-buildを入れてから設定ファイルを書き換え、tsconfig.jsonを生成するところまでやっておきます。これはあくまでビルド時にしか使わないので、開発用依存パッケージとしてインストールします。

$ yarn add --dev @nuxt/typescript-build

インストールが終わったらnuxt.config.jsを書き換えます。まずbuildModules'@nuxt/typescript-build'を書き加えましょう。

nuxt.config.js
export default {
  // 前略
  buildModules: [
    // Doc: https://github.com/nuxt-community/eslint-module
    '@nuxt/typescript-build',  // ここを追加
    '@nuxtjs/eslint-module',
    '@nuxtjs/vuetify'
  ],
  // 後略
}

そしてこの際なのでnuxt.config.jsnuxt.config.tsにリネームして、型を付けます。default exportしているあたりをいったんnuxtConfig変数に入れて型を付け、後からエクスポートするようにします。

$ mv nuxt.config.js nuxt.config.ts
nuxt.config.ts
import { Configuration } from '@nuxt/types'
import colors from 'vuetify/es5/util/colors'

const nuxtConfig: Configuration = {
  // 中略
}

module.exports = nuxtConfig

最後に次のような内容でtsconfig.jsonをプロジェクトのトップ階層に設置すれば完了です。

tsconfig.json
{
  "compilerOptions": {
    "target": "esnext",
    "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"
  ]
}

続いてランタイム時にもTypeScriptが有効になるようにしていきます。これは@nuxt/typescript-runtimeを導入して、package.jsonを書き換えるだけのお手軽作業です。こちらはランタイムなので、開発用依存としないように注意しましょう。

$ yarn add @nuxt/typescript-runtime

package.jsonの変更ですが、scriptsセクションを探してその中にある4箇所のnuxtコマンドを全てnuxt-tsコマンドに置き換えるだけです。

package.json
"scripts": {
  "dev": "nuxt-ts",
  "build": "nuxt-ts build",
  "start": "nuxt-ts start",
  "generate": "nuxt-ts generate",
  "lint": "eslint --ext .js,.vue --ignore-path .gitignore ."
}

最後にlintの設定もしましょう。せっかくESLintとPrettierをプロジェクト初期化時に有効にしてありますからね。まずは専用のESLint用定義を入れていきます。

$ yarn add -D @nuxtjs/eslint-config-typescript

インストールが終わったら.eslintrc.jsでその定義を有効化しておきます。parser を TypeScript のものに変更し、extends セクションで導入した定義を有効化してください。

.eslintrc.js
module.exports = {
  // 前略
  parserOptions: {
    // ここを変更
    parser: '@typescript-eslint/parser'
  },
  extends: [
    '@nuxtjs',
    'prettier',
    'prettier/vue',
    'plugin:prettier/recommended',
    'plugin:nuxt/recommended',
    '@nuxtjs/eslint-config-typescript'. // ここを追加
  ],
  // 後略
}

最後にpackage.jsonscriptsセクションにあるlintコマンドで.tsファイルに対してもlintが走るように対象となる拡張子を追加すれば完了です。

package.json
"scripts": {
  "dev": "nuxt-ts",
  "build": "nuxt-ts build",
  "start": "nuxt-ts start",
  "generate": "nuxt-ts generate",
  "lint": "eslint --ext .ts,.js,.vue --ignore-path .gitignore ."
}

Composition API の導入

まずは yarn でインストールしてしまいましょう。

$ yarn add @vue/composition-api

インストールできたら plugins ディレクトリの下に composition-api.ts を作って、Composition API を有効化します。

plugins/composition-api.ts
import Vue from 'vue'
import VueCompositionApi from '@vue/composition-api'

Vue.use(VueCompositionApi)

nuxt.config.ts でプラグインを有効化することもお忘れなく。

nuxt.config.ts
const nuxtConfig: Configuration = {
  // 前略
  plugins: ['@/plugins/composition-api'],
  // 後略
}

これで導入は完了です。

Composition API による実装の例

コンポーネントの作成

ではVuetifyを選択することによってデフォルトで生成されたコンポーネントを改造する形で、Composition API をどうやって Nuxt.js に組み込んでいくか説明していきます。

まずは layouts/default.vue を確認してみましょう。デフォルトの実装は以下のようになっています。なお下記コードでは<script>内を抜粋しています。

layouts/default.vue
export default {
  data() {
    return {
      clipped: false,
      drawer: false,
      fixed: false,
      items: [
        {
          icon: 'mdi-apps',
          title: 'Welcome',
          to: '/'
        },
        {
          icon: 'mdi-chart-bubble',
          title: 'Inspire',
          to: '/inspire'
        }
      ],
      miniVariant: false,
      right: true,
      rightDrawer: false,
      title: 'Vuetify.js'
    }
  }
}

これを Composition API + TypeScript で書き換えていきます。まず <script> タグに lang="ts" 属性を追加して <script lang="ts"> としてこの中に書かれたコードがTypeScriptであるということを明示してから、コードを書き換えていきます。まずは書き換えた後のコード全体を掲載しますが、おおよそは Composition API のサンプルコード通りとなります。

layouts/default.vue
<script lang="ts">
// (1) 必要なものをインポート
import { createComponent, ref } from '@vue/composition-api'

type Item = {
  icon: string
  title: string
  to: string
}

// (2) createComponentによるコンポーネントの作成
export default createComponent({
  setup() {
    // (3) refを使ったリアクティブ値の生成
    const clipped = ref(false)
    const drawer = ref(false)
    const fixed = ref(false)
    // (4) 型の明示
    const items = ref<Item[]>([
      {
        icon: 'mdi-apps',
        title: 'Welcome',
        to: '/'
      },
      {
        icon: 'mdi-chart-bubble',
        title: 'Inspire',
        to: '/inspire'
      }
    ])
    const miniVariant = ref(false)
    const right = ref(false)
    const rightDrawer = ref(false)
    const title = ref('Vuetify.js')

    // (5) Template内で使うものだけまとめて返す
    return {
      clipped,
      drawer,
      fixed,
      items,
      miniVariant,
      right,
      rightDrawer,
      title
    }
  }
})
</script>

コンポーネントの作成

Composition API では、(2) で行っているように createComponent 関数によってコンポーネントを作成します。これは @vue/composition-api から提供されるので、(1) の箇所で事前にインポートしておきます。さらに setup を使ってリアクティブな値の定義を行っていきます。今までは data() を使ってこれらの値を定義していましたが、Composition API では変更されていますので注意してください。

リアクティブ値

単体のリアクティブ値は ref を使って作成します。投入した初期値によって型推論が行われるため、(3) のように通常は型を明示する必要はありません。ただし、明示的に書きたい場合は (4) でやっているように <> を使えばOKです。

一方、型の明示が必須な場合もあります。例えば初期値では空配列だけれども後から値が変化するような場合や、null が入る可能性があるリアクティブ値を定義する場合には型を明示しなければなりません2。例えば以下のようにすると良いでしょう。

const userList = ref<User[]>([])
const nullableValue = ref<User | null>(null)

setup 関数の最後で <template> 内で使う値だけまとめて返してあげれば完了です。これで少なくとも <script> 内では TypeScript による型チェックが働くようになります3

Nuxt固有の拡張機能

Nuxt.js には asyncData など便利機能が色々と備わっています。ですが、Composition API と組み合わせた場合これらの機能を使うことは現時点では非常に難しいです。そこで Composition API にある代替機能を使ってこれらの機能を実装していきましょう。ある程度はそれで機能の代わりを果たすことができます。

非同期値の取得

API からデータを引っ張ってくるなど非同期な値を持ってきたいことはしばしばあると思います。ただ先にも述べたように asyncData は封印されていますので、別の手段を用います。 Composition API には watch という関数が用意されており、これを使って async をラップしてあげることでページ遷移してきたときに1回だけ呼んであげることができます。本来 watch は名前の通りリアクティブ値を監視して、変更があったときに指定した関数を動かすための関数ですが、なぜか4こういった使い方もできるようになっています。

const users = ref<User[]>([])

watch(async () => {
    users.value = await fetch('http://api.example.com/users/')
})

レイアウトやプロパティの指定

これは従来のやり方がそのまま使えます。props もOKです。createComponent の中でそれぞれに対応した Key-Value を設定してあげましょう。

import { createComponent } from '@vue/composition-api'

export default createComponent({
  layout: 'empty',
  props: {
    user: {
      type: Object,
      default: null
    }
  },
  setup() {
    // 略
  }
})

Nuxt Axios / Auth Moduleとの連携

これも問題なくできますが、少し準備が必要です。まず型付けを正しく行うために、nuxt.config.ts を編集します。なお、Auth Module を利用する際は事前にパッケージと型定義を導入しておいてください。あとは型定義をインポートして、declare module でどのプロパティにどの型を適用するか定義してあげましょう。

$ yarn add @nuxtjs/auth
$ yarn add -D @types/nuxtjs__auth
nuxt.config.ts
import { NuxtAxiosInstance } from '@nuxtjs/axios'
import { Auth } from 'nuxtjs__auth'

const nuxtConfig: Configuration = {
  // 省略
}

// ここのひとかたまりを追加
declare module 'vue/types/vue' {
  interface Vue {
    $auth: Auth
    $axios: NuxtAxiosInstance
  }
}

使う側では setup メソッドの第2引数に渡されてくる context 引数に実装が詰め込まれているので、そこから読むようにします。root プロパティの中に先ほど interface Vue で指定したプロパティが生えているので、呼んであげるだけです。補完もバッチリ効きますので、便利に使うことができます。

export default createComponent({
  setup(_props, context) {
    const users = ref<User[]>([])
    watch(async () => {
      // Axios Module を呼ぶ例
      users.value = await context.root.$axios.$get('/users')
    })

    const login = async () => {
      // Auth Module を呼ぶ例
      await context.root.$auth.loginWith(/* ユーザー名とパスワードを送信 */)
      context.root.$router.push('/')
    }

    return { users, login }
  }
})

context.root.$router.push なんかをしれっと使ってますが、だいたい欲しいもの($el$store など)は context.root に生えているので、困ったらまずここを探してみると良いでしょう。何が入っているかは補完機能が全部教えてくれます。ちなみに emitcontext.emitcontext 直下にぶら下がっています。

おわりに

ここまで Nuxt.js に Composition API を導入し、TypeScript で型を付けながら実装する方法を紹介してきました。Composition API 自体がまだちょっと洗練されておらず、特に Nuxt.js が用意している専用機能に関するサポートに関しては手つかずの状況ではあります。しかし、あんまり凝ったことをしないのであれば十分使えるかなといった手応えです。来年Q1にも予定されている Vue.js 3.0 とそれをベースにした新しい Nuxt.js を楽しみにしつつ、年を越したいと思います。


  1. たまにanyを書かざるを得ないのはご愛敬 

  2. 例えば初期値に null を入れたら Ref<null> に推論されるので、後から何かしらの型を持つ値を代入しようとしたときに型エラーになります 

  3. <template> 内で型チェックできる方法があれば教えてください...... 

  4. 初回に計算されて、それ以降変更を検知する箇所がないため、そういう動作になるものと思われます 

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

Jestテスト実行時「Unknown custom element」が発生した場合の対処法

問題の現象

Jestテスト実行時、以下のようなVuetifyコンポーネントのUnknown custom elementの警告が出た場合

$ yarn test
yarn run v1.17.3
$ jest --config jest.config.js
 PASS  test/Button.spec.js
  Button
    ✓ is a Vue instance (18ms)

  console.error node_modules/vue/dist/vue.runtime.common.dev.js:621
    [Vue warn]: Unknown custom element: <v-btn> - did you register the component correctly? For recursive components, make sure to provide the "name" option.

    found in

    ---> <Button>
           <Root>

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        2.588s
Ran all test suites.
✨  Done in 5.91s.

前提

package.json

package.json
{
  "scripts": {
    "test": "jest --config jest.config.js",
  },
}

コンポーネント

components/Button.vue
<template>
  <v-btn large color="primary" @click="log">
    ボタン
  </v-btn>
</template>

<script>

export default {
  name: 'Button',
  methods: {
    log() {
      console.log('hoge')
    }
  }
}
</script>

テストコード

test/Button.spec.js
import { mount } from '@vue/test-utils'
import Button from '@/components/Button.vue'

describe('Button', () => {
  test('is a Vue instance', () => {
    const wrapper = mount(Button)
    expect(wrapper.isVueInstance()).toBeTruthy()
  })
})

対応内容

  • JestがVuetifyコンポーネントを認識できるように、警告が出たタグのモックを作成する
test/jest.setup.js
import Vue from 'vue'
import VueTestUtils from '@vue/test-utils'

Vue.config.silent = true

// Mock Vuetify components
VueTestUtils.config.stubs['v-btn'] = '<button><slot /></button>'
jest.config.js
module.exports = {
  setupFiles: ['<rootDir>/test/jest.setup.js'],    // Load Mock setting
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/$1',
    '^~/(.*)$': '<rootDir>/$1'
  },
  transform: {
    '^.+\\.js$': 'babel-jest',
    '.*\\.(vue)$': 'vue-jest'
  },
  moduleFileExtensions: ['js', 'json', 'vue']
}

参考サイト

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

条件によってテキストが変わるコンポーネントを分割して共通スタイルを適用する

こちらは、弁護士ドットコム Advent Calendar 2019 - Qiita の 21 日目の記事です。

要望

条件によって中身が変わるからコンポーネントは分けたいけど、スタイルは共通化したい。
JS によるロジックは特にない。

具体的なケース

ユーザーの権限によってテキストが変わるコンポーネント。

共通のスタイルを使用したいのですが、一つのコンポーネントにまとめようとすると v-if の嵐になってしまい、可読性がかなり落ちてしまいます。

イメージ
<p v-if="condition" class="style1">◯◯権限を持っているので、☓☓が出来ます。</p>
<p v-else class="style1">◯◯権限を持っていないので、☓☓が出来ません。</p>

<p v-if="condition" class="style2">△△権限を持っているので、□□が出来ます。</p>
<p v-else class="style2">ただし△△権限を持っていないので、□□が出来ません。</p>

<p class="style3">どのユーザーも☆☆は出来ます。</p> <!-- どの権限でも同じ -->

解決策

条件ごとにコンポーネントを分け、条件分岐・共通スタイル定義を親コンポーネントで行います。

スタイルを共通化させると、v-if をコンポーネントの切り替えの 1 つだけにできるので可読性が上がります。

実装

親コンポーネントにスタイルを持たせて、条件によって子コンポーネントを切り替えるようにします。

親コンポーネント
<template>
  <div class="wrapper">
    <component :is="componentName" v-bind="propData" />
  </div>
</template>

<script>
import Component1 from './Component1.vue'
import Component2 from './Component2.vue'

export default {
  components: {
    Component1,
    Component2
  },
  props: {
    condition: {
      type: String,
      required: true
    },
    username: {
      type: String,
      required: true
    }
  },
  data() {
    return {
      propData: {
        username: this.username
      }
    }
  },
  computed: {
    componentName() {
      switch (this.condition) {
        case 'cond1':
          return Component1
        case 'cond2':
          return Component2
        default:
          return null
      }
    }
  }
}
</script>

<style scoped>
.wrapper >>> .style1 {
  font-size: 20px;
}
.wrapper >>> .style2 {
  font-size: 16px;
}
.wrapper >>> .style3 {
  font-size: 16px;
  font-weight: bold;
}
</style>
Component1.vue
<template>
  <div>
    <h1>{{ username }}さん</h1>
    <p class="style1">閲覧権限を持っているので、プロパティの閲覧が出来ます。</p>
    <p class="style2">ただし編集権限を持っていないので、プロパティの編集が出来ません。</p>
    <p class="style3">どのユーザーもログインは出来ます。</p>
  </div>
</template>

<script>
export default {
  props: {
    username: {
      type: String,
      required: true
    }
  }
}
</script>
Component2.vue
<template>
  <div>
    <h1>{{ username }}さん</h1>
    <p class="style1">閲覧権限を持っているので、プロパティの閲覧が出来ます。</p>
    <p class="style2">編集権限を持っているので、プロパティの編集が出来ます。</p>
    <p class="style3">どのユーザーもログインは出来ます。</p>
  </div>
</template>

<script>
export default {
  props: {
    username: {
      type: String,
      required: true
    }
  }
}
</script>

注意

>>>(ディープセレクタ)は子孫要素すべてを対象とするので、scoped にしているからといって同じクラス名を使っているとスタイルがあたってしまいます。
コンポーネント名を prefix として付ける、BEM などの命名規則を適用する、などの対策が必要です。

また、SCSS 等を使用している場合は、>>> ではなく /deep/ を使用する必要があります。

他の選択肢

CSS を外部ファイル化して @import で読み込む

HTML, CSS, JavaScript が一緒に管理できる SFC の利点が消えてしまうので見送りました。
「全く違う場所で利用するコンポーネントだけどスタイルは共通化させたい」というときは Minxin 的に使えるかもしれません。

共通化しない

個々のコンポーネントとして取り扱えたほうがいいことが往々にしてあるので、選択肢としてはありだと思います。
今回は、共通に定義したスタイルを片方だけ変更するということが基本的にないことがわかっていたので、共通化したほうが後々楽だと判断しました。

あとがき

自分が実装したケースではこのやり方がフィットしましたが、子コンポーネント側のコードが二重管理になってしまうので、ケースごとに検討することが必要だと思います。

他に同じような悩みを抱えている人の糧になれば幸いです。

参考

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

Leaflet + Vue.js で 地図の表示位置を切り替えられるサンプルサイトを作ってみた

この記事は、 North Detail Advent Calendar 2019 の4日目の記事です。

弊社は最近地図を扱う業務が増えたので、ここ数ヶ月ほど学習がてらに "Leaflet" を使ってみてます。
もともと "Vue.js" でのサイト構築も多いので、両方を合わせるとどうなるか、というのを試してみたいと思います。

デモサイト: https://sample-leaflet-tacck.netlify.com/
ソースコード: https://github.com/tacck/sample-leaflet

画面でみると

こういう感じで表示されます。
20191204_001.png

初回は "位置情報提供の許可" が求められるので、 "許可" の方を選択してください。
20191204_003.png

「現在地」のボタンをクリックすると、ブラウザ経由で取得した位置情報の地点を地図上で表示します。(ここは弊社)
20191204_002.png

解説

Vue.js や Leaflet.js それぞれの詳しい扱い方は専用のサイトにお任せします。

ここでは、少し工夫したところを。

情報の更新

現在地取得と反映

Mainコンポーネントの中に、 Tabコンポーネント(ボタンのある領域) と Mapコンポーネント(地図のある領域) を持つ形にしています。
Mapコンポーネントは、Mainコンポーネントから与えられた緯度経度を中心に地図を表示する(変更があれば更新する)だけ、という作りです。

緯度経度の変更されるタイミングは、次の二つになります。

  • ボタンが押された場合
  • ボタンが「現在地」の時にブラウザから与えられた位置情報が navigator.geolocation.watchPosition() 経由で更新された場合

ボタンが押された場合

Tabコンポーネント の中には TabButtonコンポーネント が二つ並んでいます。
TabButtonコンポーネント は、クリックされたら "クリックされたよ" というイベントを Tabコンポーネントに返すだけです。
どちらのボタンがクリックされたかは、 Tabコンポーネント の責任範囲として、ここからさらに Mainコンポーネント へイベントを投げます。
Mainコンポーネントへは "どちらのボタンが押されたか" がわかるものを引数として渡しています。

Mainコンポーネントは、受け取った値を使って緯度経度を更新し、 Mapコンポーネントへ(props経由で)情報を渡します。

TabButton.vue
(snip)
    <b-button @click="$emit('click')" size="lg" :variant="status">
(snip)
Tab.vue
    <TabButton
      @click="clickStation"
      id="sation"
      :status="stationVariant"
      label="札幌駅"
    ></TabButton>
(snip)
    clickStation() {
      this.stationVariant = "info";
      this.hereVariant = "";

      this.$emit("changeActive", "static");
    },
Main.vue
(snip)
    <Tab @changeActive="setActive"></Tab>
(snip)
    setActive: function(location) {
      console.log("active:" + location);
      this.lat = this.geosObject[location][0];
      this.lon = this.geosObject[location][1];
    },
(snip)

地図へ反映

Mapコンポーネントでは、緯度経度の情報が更新されたこと(正確にはpropsで持つ 'lat' の値が更新されたこと)がわかるように、 watch を使っています。

Main.vue
(snip)
    <Map :lat="lat" :lon="lon"></Map>
(snip)
Map.vue
(snip)
  methods: {
    updateCurrentPosition: async function() {
      if (this.map) {
        this.map.panTo([this.lat, this.lon]);
      }
      if (this.currentCircle) {
        this.currentCircle.setLatLng([this.lat, this.lon]);
      }
      if (this.currentPoint) {
        this.currentPoint.setLatLng([this.lat, this.lon]);
      }
    }
  },
  watch: {
    lat: function() {
      this.updateCurrentPosition();
    }
  }
(snip)

まとめ

Vue.js のコンポーネントの一つとして、 Leaflet を使った地図を使うことが簡単にできました。
Mapコンポーネントの外から位置情報を与えられるので、各種サービスと連携して地図表示することも気軽にできそうです。

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

Vue/Nuxt開発効率を3倍にするVSCode拡張機能セット

この記事を読むと

VSCode拡張機能を入れて、下記のようなことが実現します
・VSCodeとは別にterminal(Command Line)の画面を開かなくても、VSCode内から直接terminal操作ができる(例えばnpm run devも)
・全てのカッコ()[]{}が種類別に色分けされる
・インデントが一眼で分かる
・htmlタグの開始タグと終了タグを同時に修正できる
・Vue.jsやNuxt.jsの構造やファイル操作が楽になる
etc...

なぜこの記事を書いたのか

VSCodeは素晴らしいテキストエディタです。
特に、Vue.jsやNuxt.jsを用いて開発している方にとっては最も有力な選択肢でしょう(そして、その選択は間違いではないと保証できます)。

さて、VSCodeはそのままでも素晴らしいエディタですが、使用するフレームワークや用途に応じて拡張機能を入れるとさらに、その卓越した機能性を発揮します。VSCodeがなぜ世界中のエンジニアから信頼され、愛されるのかを実感することができるでしょう。

特に、(Nuxtエンジニアとしてまだまだ未熟な)私にとっては、もはやVSCode拡張機能なしの開発は考えられません。
拡張機能を入れてからコードミスが激減し、不要なストレスから開放されたので、長時間のコーディングにおいても疲れを感じにくくなりました。

しかしながら、VSCodeに拡張機能を入れていない方も少なくないようです。
「エディタの設定を弄れない駆け出しエンジニア」を思い浮かべたでしょうか?むしろ逆の方を想定しています。WEB開発者やフロントエンジニアとして優れた技術や十分な経験があるからこそシンプルな機能構成を好み、コードを書く補助機能である拡張機能は必要なかったのかもしれません。

しかしながら、どんなにハイレベルなエンジニアの方にとってもやはり拡張機能は大きな助けとなると考えます。没入するような長時間の集中のために、少しでも快適な開発環境を整えることは有意義なことです。

そこで今回は、Vue.jsやNuxt.jsを用いた開発を行う方を対象に、VSCodeの拡張機能たちを紹介させて頂きます。

VSCodeへの拡張機能の追加方法

やり方色々

VSCodeは直感的な操作で、簡単に拡張機能を入れることができます。
「VSCode内から直接追加」、「ブラウザ上からダウンロード」、「Command Lineから操作」等々やり方はありますが、基本的にはVSCode内から直接追加するのが一番簡単です。

VSCode内から直接追加する方法

画面左下の歯車をクリックして、「extension」(日本語設定なら「拡張機能」)を選択しましょう。
すると、EXTENSIONS:MARKETPLACEの検索窓が表示されます。
例えば「Vetur」を追加したい場合は,「Vetur」と検索、お目当ての「Vetur」が表示されたらinstallすればOKです。簡単ですね。

拡張機能の紹介

私は下記を全て入れていますが、必要に応じて取捨選択してください。
一応、ブラウザ上のMarketplaceへのリンクを貼っていますが、VSCode内から直接インストールして頂くのが早いかと思います。

基本編

Japanese Language Pack は VS Code にローカライズされた UI を提供します。

日本語化ができます!VSCodeは平易な英語しか使用されていませんが、やはり日本語表示の方が操作が早くなります。

A VSCode extension to fast open html file in browser.

HTMLファイルをブラウザで開けるようになります。

The Material Icon Theme provides lots of icons based on Material Design for Visual Studio Code.

アイコンが分かりやすくなります。
インストールした後、設定を変更する必要があります。インストール後のメッセージダイアログに従うのが一番簡単です。

コード整形編

A VS Code extension that allows you to… highlight trailing spaces and delete them in a flash!

インデントをハイライトしてくれます。

A visual source code plugin for maintaining local history of files.

ファイルの変更履歴が保存され、変更前との比較や、復元ができるようになります。安心ですね。

GitLens supercharges the Git capabilities built into Visual Studio Code. It helps you to visualize code authorship at a glance via Git blame annotations and code lens, seamlessly navigate and explore Git repositories, gain valuable insights via powerful comparison commands, and so much more.

gitとVSCodeを強力に連携してくれます。
git関連の拡張機能もたくさんあるのですが、やはりGitLensが一番良いかなあと思います。

Automatically rename paired HTML/XML tag, same as Visual Studio IDE does.

ペアとなるHTMLタグを自動でrenameしてくれます。

Automatically add HTML/XML close tag, same as Visual Studio IDE or Sublime Text does.

HTMLの終了タグを自動で追加してくれます。

This extension allows matching brackets to be identified with colours. The user can define which characters to match, and which colours to use.

カッコ毎に色を分けてハイライトしてくれます。こちらを入れてから、VueやNuxtで(){}のネストを多用しても混乱することがなくなりました。

Beautify javascript, JSON, CSS, Sass, and HTML in Visual Studio Code.

javascriptやJSON, CSS, Sass, HTMLコードを選択し、command palletからBeautifyを実行すると綺麗に整えてくれます。

This extension contains code snippets for JavaScript in ES6 syntax for Vs Code editor (supports both JavaScript and TypeScript).

Snippetsとはよく使うコードを自動で予測・保管する機能のことです。
コードを一文字一文字全て書かなくても良いのは本当に楽ですし、コードのミスも減ります!
VSCodeには最初からHTML等ある程度のSnippetsはついていますが、もっと便利になってくれます。
JavaScript(ES6)のSnippetsは是非追加しておきましょう。

Vue.js・Nuxt.js編

Run code snippet or code file for multiple languages: C, C++, Java, JavaScript, PHP, Python, Perl, Perl 6, Ruby, Go, Lua, Groovy, PowerShell, BAT/CMD, BASH/SH, F# Script, F# (.NET Core), C# Script, C# (.NET Core), VBScript, TypeScript, CoffeeScript, Scala, Swift, Julia, Crystal, OCaml Script, R, AppleScript, Elixir, Visual Basic .NET, Clojure, Haxe, Objective-C, Rust, Racket, Scheme, AutoHotkey, AutoIt, Kotlin, Dart, Free Pascal, Haskell, Nim, D, Lisp, Kit, V, and custom command

VSCode内から各種対応言語やcommandを実行できます。
デバッグでさえVSCode内で完結してしまいます。個人的にはこれが一番便利かもしれません。

Vue tooling for VS Code, powered by vue-language-server.

言わずと知れたVeturですね。VSCodeでVueファイルを操作しているとレコメンドのメッセージが表示されるので、ご存知の方も多いでしょう。VueやNuxtを開発するならもはや必須の拡張機能です。
親を質に入れてでも追加しておきたい(※無料です)。

This extension extends Vue code editing with Go To Definition and Peek Definition support for components and filenames in single-file components with a .vue extension. It allows quickly jumping to or peeking into files that are referenced as components (from template), or as module imports (from script).

選択したコンポーネントやファイル名について、「Vueファイルのどこで定義されたか」や「定義しているコード」を表示してくれます。また、どこでも定義されていないのに使われている変数名を特定するのにも役立ちます。
これも本当に便利ですね。

Ability to duplicate files and directories in VS Code.

VSCode内で簡単にファイルやディレクトリの複製が可能になります。
Vueファイルをテンプレとして使いまわしたりするときに便利ですね。

  • Vue docs

    Viewing documentation [Vue.js, Vuex, Vue Router, Vue SSR, Vuetify, Nuxt.js, VuePress] in the VS Code!
    This extension extends Vue code editing with Go To Definition and Peek Definition support for components and filenames in single-file components with a .vue extension. It allows quickly jumping to or peeking into files that are referenced as components (from template), or as module imports (from script).

  • Vue.jsやVuex, Vue Router, Vue SSR, Vuetify, Nuxt.js, VuePressの公式ドキュメントをVSCode内から直接参照できます。
    個人的に、Chromeで開いているタブの多さと集中力は反比例するので、ドキュメントもVSCode内で完結するのはありがたいです。

要注意編

A basic spell checker that works well with camelCase code.
The goal of this spell checker is to help catch common spelling errors while keeping the number of false positives low.

私が使いこなせていないだけとは思うのですが、非推奨です。
コードのスペルミスを教えてくれるということで一見便利そうですが、英語的に不自然な変数名もエラーと一緒に表示されるので、エラー箇所の特定や修正作業がしづらくなりました。

Visual Studio Live Share enables you to collaboratively edit and debug with others in real time, regardless what programming languages you're using or app types you're building. It allows you to instantly (and securely) share your current project, and then as needed, share debugging sessions, terminal instances, localhost web apps, voice calls, and more! Developers that join your sessions receive all of their editor context from your environment (e.g. language services, debugging), which ensures they can start productively collaborating immediately, without needing to clone any repos or install any SDKs.

他人を招待してファイルの共同編集がリアルタイムでできます。イメージとしてはGoogle Documentで文書を共同編集している感じでしょうか。
オンラインでコードレビューをお願いするときも便利で、非常に素晴らしい拡張機能なんですが・・・。
一つ気になることはインストールに際して不具合が少なくない?のかなと。最初僕だけかなと思ったのですが、StackOverFlow等でも同様の投稿がありました。
Questions tagged [vscode-liveshare]

なぜ不具合が発生するのかまだきちんと、調べきれていないので推奨するのは一旦保留です。
ただ、本当に便利な機能なので懸念が解決すれば積極的に使っていきたいところです。
※今後調査結果を追記予定

おわりに

さて、お好みの拡張機能でVSCodeをカスタマイズして頂けたでしょうか。
他にもお勧めの拡張機能があれば是非コメント欄で教えてください。

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

通常の3倍のスピードだと!?~Vue.js/Nuxt.jsを開発するVSCode拡張機能セット~

てst

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

最速のフレームワーク(というのは存在しない)

何日か前にTwitterでこの投稿が話題になりました。

ReactのConcurrent Modeでは、ステートを持つ2000個のコンポーネントを安定した60fpsで再レンダーさせられるようです。一方で、ReactのいわゆるLegacy Modeでは全然60fpsにならない。何人かが、Svelteで同じデモできないかとツイートし、Svelteの創始者Rich Harris氏もデモ作ってくれました。

dev環境なので60fpsまでは出ないが、React版と違って、コンポーネントの個数を選ぶ度に遅延がないとのことです。(数に関わらず)

しかし、フレームワークの比較はそもそもこれでいいのか?

そもそも上のような数千ポリゴンの3Dボールをレンダーしたい時に、わざわざReactなどのFWを使うのはおかしくないか?実際のアプリと上のデモはかなり違うし、仮に同じものをページに置きたいとしても、Three.jsなどのライブラリですでにできることだから、それをFWで抽象化する意味は何か?ということを考えないといけないです。

何が本当に遅いかというと、ユーザーのコードです。FWのコントリビュータはパフォーマンスの最適化にベストを尽くしているが、任意のユーザーのコードはもちろん最適化できない(が、Svelteの斬新なアプローチで、ステートの更新など、期待できるパターンも最適化の対象にはなっているようです)。あまり例としては現実的ではないのですが、ユーザー(開発者)が無限ループを書いたらFWどころではなくなります。

さらに、実際のアプリで不可欠なIO処理は、ベンチマークでは測られていないです。データの取得とレンダリングはどう設計されているか?という点ではFWは多少違います。たとえば、ReactのConcurrent Modeでは、ネットワークのIO処理を待っている間に、アイドリングしているCPUを有効に使って、一部の要素をプリレンダーできます。IO処理が終わってからレンダリングを始める場合は、必然的に表示が遅くなります。

他にも指摘されたのは、測る過程。マイナーな原因だと思いますが、JSのJITコンパイラ仕様上、上のようなデモでは、1回目のレンダリング(初期ロード・マウント)を測っているか、時間が経ってから測っているかによって差が出るようです。なぜかというと、JITコンパイラがcoldな(まだ実行されていない)コードをインタプリターとして解釈するけど、よく実行されるhotなコードを、コンパイルかつ最適化してくれるからです。なので複数回レンダリングされたコンポーネントのコードがコンパイルされ速くなっている可能性もあります。それと比べて、新規マウントが多い実際のアプリでは、coldなコードが多いです。

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

ゼロからVue.jsでビジュアルリグレッションテストするまでpart2/3

Part1 https://qiita.com/senku/items/07c3e2859ac90c03867a
Part2 ここ
Part3 https://qiita.com/senku/items/08d547eda2c6ff818108

前回はVuex以外の主要なライブラリをStorybookに対応させる手順を紹介しました。
今回はVuexの話をします。ちょっと大変。

Summary

  • Vuex.Storeの引数のObjectをexportしておいて
  • モックに書き換えて
  • Storybookに使わせる

Vuexとのたたかい

とりあえずvue add vuexで入れます。

$ vue add vuex

src/store/index.jsにVuex Storeの定義が書かれています。
Vuex.Store()の引数をexportしつつ、適当なストアにまるっと書き換えます。namespaceにも対応しておくよ。

src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export const store = {
  modules: {
    test: {
      namespaced: true,
      state: {
        name: ''
      },
      mutations: {
        setName: (state, payload) => {
          state.name = payload
        }
      },
      actions: {
        async setFoo({ commit }) {
          commit('setName', 'Foo')
        },
        async setBar({ commit }) {
          commit('setName', 'Bar')
        }
      },
      getters: {
        name: state => state.name 
      }
    }
  }
}

export default new Vuex.Store(store)

src/views/Home.vue あたりでVuexを使えるようにしてみましょう。

src/views/Home.vue
diff --git a/src/views/Home.vue b/src/views/Home.vue
index fc2e940..807ed24 100644
--- a/src/views/Home.vue
+++ b/src/views/Home.vue
@@ -1,11 +1,13 @@
 <template>
   <div class="home">
+    <p>{{ name }}</p>
     <img alt="Vue logo" src="../assets/logo.png">
     <HelloWorld msg="Welcome to Your Vue.js App"/>
   </div>
 </template>

 <script>
+import { mapGetters } from 'vuex'
 // @ is an alias to /src
 import HelloWorld from '@/components/HelloWorld.vue'

@@ -13,6 +15,12 @@ export default {
   name: 'home',
   components: {
     HelloWorld
+  },
+  computed: {
+    ...mapGetters('test', ['name']),
+  },
+  created() {
+    this.$store.dispatch('test/setFoo')
   }
 }
 </script>

npm run serveで確認するとFooが表示されます。バッチリ。

image.png

Home用のStoryも作っておきましょう。いつものパターン。

src/stories/Home.stories.js
import { storiesOf } from "@storybook/vue";
import defaultDecorator from "@/stories/defaultDecorator";
import Home from "@/views/Home.vue";

storiesOf("Home", module)
  .addDecorator(defaultDecorator)
  .add("test", () => {
    return {
      components: { Home },
      template: `
      <home />
      `
    };
  });

Vuex × Storybook

いつものようにこのStoryもタダでは動きません。

image.png

まずはVue Routerと同じようにconfig/storybook/config.jsVue.use()します。

config/storybook/config.js
diff --git a/config/storybook/config.js b/config/storybook/config.js
index d8dcd31..db6a520 100644
--- a/config/storybook/config.js
+++ b/config/storybook/config.js
@@ -2,8 +2,10 @@
 import { configure } from '@storybook/vue'
 import Vue from 'vue'
 import Router from 'vue-router'
+import Vuex from 'vuex'

 Vue.use(Router)
+Vue.use(Vuex)

 const req = require.context('../../src/stories', true, /.stories.js$/)

decoratorでStoreを渡してもいいのですが、Storyごとに異なるモックデータを使いたいケースを考慮して、Storyで渡すようにしておきます。

src/stories/Home.stories.js
diff --git a/src/stories/Home.stories.js b/src/stories/Home.stories.js
index 2902105..802e067 100644
--- a/src/stories/Home.stories.js
+++ b/src/stories/Home.stories.js
@@ -1,12 +1,16 @@
 import { storiesOf } from "@storybook/vue";
 import defaultDecorator from "@/stories/defaultDecorator";
 import Home from "@/views/Home.vue";
+import Vuex from "vuex";
+import { store } from "@/store/index.js";

 storiesOf("Home", module)
   .addDecorator(defaultDecorator)
   .add("test", () => {
+    const mockStore = new Vuex.Store(store);
     return {
       components: { Home },
+      store: mockStore,
       template: `
       <home />
       `

StoryでもガッツリFooが表示されるようになります。

image.png

gettersのモック

getters で取得する値をストーリーによって変えたいことがありますよね。モックで対応しましょう。

Deep copyを利用したいので、とりあえずlodashを入れます。Deep copyの実装面倒だからね。

$ npm install --save-dev lodash

Vuex Storeをモックするための独自のクラスをひとつ作って、gettersを上書きするためのユーティリティ関数を作ってしまいます。後のことも考えて適当に共通化しておきます。
また、オリジナルのstoreオブジェクトを改変すると他のStoryで困る可能性があるので、cloneDeepで複製したObjectを使います。
ここがこの記事の見どころですよ。それにしてもコードフォーマットバラバラだな。

src/stories/mockVuex.js
import cloneDeep from "lodash/cloneDeep";
import Vuex from "vuex";
import { store as originalStore } from "@/store/index.js";

export default class mockVuex {
  constructor() {
    // モックするVuex StoreをDeep copy
    this.mockStore = cloneDeep(originalStore);
  }

  /**
   * Vuex Storeを生成して返却する
   * 
   * @return {Vuex.Store} store
   */
  getMockStore() {
    return new Vuex.Store(this.mockStore);
  }

  /**
   * mockStoreのプロパティを上書きする
   *
   * @param {string} field actions/getters
   * @param {string} nameSpace 'foo/bar/baz' 形式
   * @param {string} propertyName
   * @param {function} newFunction
   */
  mockProperty(field, nameSpace, propertyName, newFunction) {
    let modules = this.mockStore.modules

    // nameSpace に従ってモジュールの階層を掘る
    const diggingNameSpaces = nameSpace.split('/')
    const lastNameSpace = diggingNameSpaces.pop()
    for (const diggingNameSpace of diggingNameSpaces) {
      modules = modules[diggingNameSpace].modules
    }
    modules[lastNameSpace][field][propertyName] = newFunction
  }

  /**
   * getters を上書きする
   *
   * @param {string} nameSpace 'foo/bar/baz' 形式
   * @param {string} propertyName
   * @param {function} newFunction
   */
  mockGetters(nameSpace, propertyName, newFunction) {
    this.mockProperty("getters", nameSpace, propertyName, newFunction);
  }
}

Storyを修正します。VuexはmockVuexに隠蔽したので参照不要になりました。
mockGetters()関数を使って、gettersが返す値を変更します。
その後getMockStore()で取得したVuex StoreをStoryで使います。

src/stories/Home.stories.js
diff --git a/src/stories/Home.stories.js b/src/stories/Home.stories.js
index 802e067..d1e5e74 100644
--- a/src/stories/Home.stories.js
+++ b/src/stories/Home.stories.js
@@ -1,16 +1,16 @@
 import { storiesOf } from "@storybook/vue";
 import defaultDecorator from "@/stories/defaultDecorator";
+import mockVuex from "@/stories/mockVuex";
 import Home from "@/views/Home.vue";
-import Vuex from "vuex";
-import { store } from "@/store/index.js";

 storiesOf("Home", module)
   .addDecorator(defaultDecorator)
   .add("test", () => {
-    const mockStore = new Vuex.Store(store);
+    const vuex = new mockVuex();
+    vuex.mockGetters('test', 'name', () => 'Baz')
     return {
       components: { Home },
-      store: mockStore,
+      store: vuex.getMockStore(),
       template: `
       <home />
       `

test/nameのgettersがモックされ、Bazが表示されるようになりました。

image.png

実運用では、モック用のデータを別に用意してimportするのがいいと思います。モックサーバとかに使ってるデータがあればそれで。

actionsのモック

actionsもモックしたいですよね。
src/stories/mockVuexmockActions()関数を追加します。共通化してるのでただのラッパー関数です。

src/stories/mockVuex.jsに追加
  /**
   * actions を上書きする
   *
   * @param {string} nameSpace 'foo/bar/baz' 形式
   * @param {string} propertyName
   * @param {function} newFunction
   */
  mockActions(nameSpace, propertyName, newFunction) {
    this.mockProperty('actions', nameSpace, propertyName, newFunction)
  }

Storyを編集して、Home.vuecreated()で呼ばれていたtest/setFooアクションをモックして、quxをセットしてみます。

src/stories/Home.stories.js
diff --git a/src/stories/Home.stories.js b/src/stories/Home.stories.js
index d1e5e74..27a8fef 100644
--- a/src/stories/Home.stories.js
+++ b/src/stories/Home.stories.js
@@ -7,7 +7,7 @@ storiesOf("Home", module)
   .addDecorator(defaultDecorator)
   .add("test", () => {
     const vuex = new mockVuex();
-    vuex.mockGetters('test', 'name', () => 'Baz')
+    vuex.mockActions('test', 'setFoo', ({state}) => state.name = "qux")
     return {
       components: { Home },
       store: vuex.getMockStore(),

ガッツリquxが表示されます。やったね。

image.png

画面表示に必要なデータをモックするのはgettersのモックでだいたいなんとかなるので、actionsをモックするケースはHTTP通信をしたりStorybookで動作しない副作用を制御したい場合がほとんどだと思います。
その場合、actionsはmockVuexのコンストラクタでまとめて無効化してしまうのが楽ちんです。

src/stories/mockVuex.jsを編集
  constructor() {
    // モックするVuex StoreをDeep copy
    this.mockStore = cloneDeep(originalStore);

    // 以下使わないActionを無効化
    const NOOP = () => {}
    this.mockActions('test/setBar', NOOP)
  }

もうVuexのモックも怖くないですね。
明日からVue.jsコンポーネントのStoryを量産できるはずです。

おまけ:コンポーネントのdataを操作する

Storyから$refs経由でコンポーネントにアクセスすればいいです。mounted()のタイミングがよさげ。

  .add("edit name", () => {
    return {
      components: { Home },
      template: `
      <home ref="foo"/>
      `,
      mounted() {
        this.$refs.foo.bar = 'baz' // Homeコンポーネントのbarに'baz'をセット
      }
    };
  });

つづく

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

Vue.jsでWebRTCによるビデオ通話機能を作ってみた

グレンジ Advent Calendar 2019 6日目担当の raitome です。
グレンジでクライアントエンジニアをやっています。
最近はちょっと違う技術を触ってみたくて、Vue.jsを使って、ビデオ通話機能を作ってみました。
ネットの資料が多いですが、古いものも多くて、すぐ使えるコードが少ないので、
メモのためでもあり、必要なものを簡単にまとめました。

WebRTCについて

詳細は下記でご覧いただけます。
https://ja.wikipedia.org/wiki/WebRTC
https://qiita.com/yusuke84/items/286f569d110daede721e

使ったライブラリ

とりあえず手軽にビデオ通話機能を作りたくて、下記のライブラリを使いました。
・simple-peer: https://github.com/feross/simple-peer
  WebRTCベースで、p2pのビデオ通話を簡単に実装できるライブラリ。ファイルの転送などもできるらしい

・ simple-signal-clientとsimple-signal-server: https://github.com/t-mullen/simple-signal
  WebRTCでPeer to Peer通信を行う場合、シグナリングサーバと呼ばれる仲介サーバを用意する必要がありますので、simple-peerのためのシグナルサーバー用ライブラリsimple-signalを使いました。simple-signal-clientはクライアント側で、simple-signal-serverはサーバー側で使います。

他に、シグナルサーバーはSocket通信を使うため、socket.ioとクライアントとサーバー側のライブラリも使います。

(Skyway: https://webrtc.ecl.ntt.com/ というサードパーティのものを使えばもっと早く構築できるらしいが、有料らしいので、今回は自前で色々作ってみました)

クライアント側の準備

今回は必要な処理だけ紹介します。完成版のコードを割愛します。

1.必要なもののimport

sample_client.js
    import SimplePeer from 'simple-peer'
    import SimpleSignalClient from 'simple-signal-client'
    import io from 'socket.io-client'

2.Peerの作成

sample_client.js
    const socket = io("シグナルサーバーのurl" , {secure: true})
    let signalClient = new SimpleSignalClient(socket)

    // 下記の方法でチャットルームの感じで作れる
    signalClient.discover("チャットルームID的な何か") // これを使ってユーザーリストとかを取得できる
    signalClient.on('discover', receiveUserList) // ユーザーリスト処理のfunctionをここに入れる

2.Peerへの接続リクエスト処理

ビデオ通話したい相手のソケットIDが分かれば、下記のfunctionを使えばシグナルサーバーを経由して接続できます。

sample_client.js
    const { peer } = await signalClient.connect("自分のソケットID",  "チャットルームID的な何か"})

3.Peerの受信処理

sample_client.js
    signalClient.on('request', async (request) => {
        const { peer } = await request.accept()  // 接続リクエストを承認

        peer.on('connect', () => {
            peer.send("connected")
        })
        peer.on('stream', (remoteStream) => {
              // 向こうから受け取ったPeerのストリームを画面に表示させる
            const videoElement = document.createElement('video')
            videoElement.autoplay = true
            videoElement.srcObject = remoteStream
            container.appendChild(videoElement) // 事前に用意したコンテナに新しく作ったElementを追加する
        })
        peer.on('close', () => {
             // 停止時の必要処理
        })
        // 自分のカメラの映像も向こうに送信するので、ストリームを取得して画面に追加する。下記の関数はあとで説明します
        addLocalCameraStream()
    })

4. カメラの扱い

下記の処理で、ブラウザがカメラの使用権限を取得して、カメラのストームを扱います。
ブラウザによって挙動が少し違うので、最新版のChromeかFireFoxを使うことをオススメします。
一つ注意点としては、Chromeの場合は、セキュリティの関係で、サーバー側はSSL対応をしないと、下記のメソッドはうまく動きません。

sample_client.js
    addLocalCameraStream() {
        var mediaDevices = navigator.mediaDevices || ((navigator.mozGetUserMedia || navigator.webkitGetUserMedia || navigator.msGetUserMedia) ? {
            getUserMedia(c) {
                return new Promise(((y, n) => {
                    (navigator.mozGetUserMedia || navigator.webkitGetUserMedia).call(navigator, c, y, n);
                }));
            }
        } : null)

        mediaDevices.getUserMedia({ audio: true, video: true }).then((localStream) => {
            peer.currentPeer.addStream(localStream) // これで接続した相手にストリームを送る
            // ここでストリームを画面に表示させる処理を追加してもいい
            }).catch(err => {
        });
    }

ちなみに、mediaDevices.getDisplayMediaを使えば、画面共有機能も作れます。

上記のコードを使えば、クライアント側の最低限の機能が作れます。

シグナルサーバーの準備

シグナルサーバーのコードは割とシンプルになります。
主に必要に応じてユーザーの管理処理をカスタマイズします。

index.js
    const io = require('socket.io')(server)
    var SimpleSignalServer = require('simple-signal-server')
    var signal = new SimpleSignalServer(io)

    signal.on('request', (request) => {
        request.forward() // リクエストをそのまま進ませる
    })

    signal.on('discover', (request) => {
        // ここでユーザーのソケットIDやルームIDを管理して、request.discoverを使って、全てのpeerの必要な情報を送る
    })

    signal.on('disconnect', (socket) => {
        // 接続切れる時の処理
    })

最後に

以上でコードを使えば、必要最低限のビデオ通話機能が作れます。少し調整すれば、画面共有機能も作れます。実際にテストしてみたところ、P2Pの関係でもありますが、割とスムーズなビデオ通話ができました。

また、何か不明なところがありましたら、simple-signalのサンプルコードも一緒にご覧いただければ。

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

GASとVue.jsとSpreadsheetによるデータ連携

SpreadsheetID SheetName
123456789012345678901234567890123456789012345 UserList
ID Name Department
xxx@test.com xxx Sales
yyy@test.com yyy Development
zzz@test.com zzz Marketing
コード.gs
function doGet() {
  var html = HtmlService.createTemplateFromFile("index").evaluate();
  return html;
}


function getSS(spreadSheetID, sheetName){

  var res = SpreadsheetApp.openById(spreadSheetID)
    .getSheetByName(sheetName).getDataRange().getDisplayValues();

  var keys = res.splice(0, 1)[0];

  return value = res.map(function(row) {
    var obj = {}
    row.map(function(item, index) {
      obj[keys[index]] = item;
    });
    return obj;
  });
}


function getUserList() {

  var SSID = "123456789012345678901234567890123456789012345";
  var SN = "UserList;

  var userList = getSS(SSID, SN);
  Logger.log(userList);

  return userList; 
}


function getUserData() {
  var email = Session.getActiveUser().getEmail();

  var userList = getUserList();

  return filterdData = userList.filter(function(item, index){
    if (item.ID == email) return true;
  });
}
vue.html
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.0"></script>

<script>  
  var app = new Vue({
    el: '#app',
    data: {
      loginUserEmail: '',
      logoinUserName:'',
      logoinUserDept:'',
    },
    methods:{
      setLoginUser: function(loginUser){
        this.loginUserEmail = loginUser[0].ID;
        this.logoinUserName = loginUser[0].Name;
        this.logoinUserDept = loginUser[0].Department; 
      },           
    },
    created: function(){
      google.script.run
        .withSuccessHandler(this.setLoginUser).getUserData ();
    },
  })
</script>
index.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
  <div id="app">
    <p>User Data</p>

    <ul>
      <li>{{  loginUserEmail  }}</li>
      <li>{{  logoinUserName  }}</li>
      <li>{{  logoinUserDept  }}</li>
    </ul>

  </div>
  </body>
  <?!= HtmlService.createHtmlOutputFromFile('vue').getContent(); ?>
</html>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GASとVue.jsの連携

vue.html
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.0"></script>

<script>  
  var app = new Vue({
    el: '#app',
    data: {
      email: '',
    },
    methods:{
      initEmail: function(sessionEmail){
        this.email = sessionEmail;
      },           
    },
    created: function(){
      google.script.run
        .withSuccessHandler(this.initEmail).getLoginID();
    },
  })
</script>
コード.js
function doGet() {
  var html = HtmlService.createTemplateFromFile("index").evaluate();
  return html;
}

function getLoginID() {
  var email = Session.getActiveUser().getEmail();
  Logger.log(email);
  return email;
}

index.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
  <div id="app">
    {{ email }}
  </div>
  </body>
  <?!= HtmlService.createHtmlOutputFromFile('vue').getContent(); ?>
</html>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails+Vue.jsでバリデーションエラーを各項目の下に表示する

はじめに

Rails側のモデルに定義したバリデーションを使いつつ、
バリデーションエラーを各項目の下に表示するのに苦労したのでまとめておこうと思います。
今回、バリデーション関連以外の説明は省略させていただきます。
ご了承ください。。

やりたいこと

このように項目の下にバリデーションエラーを表示するようにします。(見た目は気にしないでください。。)
image.png

実装してみる

バリデーションを設定

ここでは必須のバリデーションだけ設定しておきます。

class Company < ApplicationRecord
  validates :name,
    presence: true
end

API側の実装

vue.js側で各項目ごとのエラーメッセージを取り出せるようにします。
具体的には下記のようなハッシュになるように加工します。
{項目名: 日本語化されたエラーメッセージ}

今回は下記のように実装しました。

@company = Company.new(create_company_params)
if @company.save
  render json: @company, status: :ok
else
  render json: { errors: @company.errors.keys.map { |key| [key, @company.errors.full_messages_for(key)]}.to_h, render: 'show.json.jbuilder' }, status: :unprocessable_entity
end

ここでerrorsにエラーメッセージを設定しています。

errors: @company.errors.keys.map { |key| [key, @company.errors.full_messages_for(key)]}.to_h

API側はこれで終わりです。

vue.js側で項目の下にエラーが表示されるようにする

まずはtemplateから実装していきます。

<template>
  <form @submit.prevent="createCompany">
    <h2>企業情報</h2>
    <div>
      <label>企業名</label>
      <input v-model="company.name" type="text">
      <!-- これでバリデーションエラーがあるときだけ表示される -->
      <p v-if="!!errors['name']" class="error" style="color: red;">{{ errors['name'][0]}}</p>
    </div>
  </form>
</template>

errorsのkeyの中に表示する項目名が含まれているかどうかで、
表示/非表示を切り替えています。

次にscriptです。

<script>
  import axios from 'axios'
  import { csrfToken } from 'rails-ujs'
  axios.defaults.headers.common['X-CSRF-Token'] = csrfToken()

  export default {
    data: function () {
      return {
        company:{
          name: ''
        },
        // バリデーションエラーがあった場合は、このerrorsにセットされます。
        errors: ''
      }
    },
    methods: {
      createCompany: function(){
        axios
          .post('api/v1/company.json', this.company)
            .then(response => {
              this.$router.push('/');
            })
            .catch(error => {
              if (error.response.data && error.response.data.errors) {
                this.errors = error.response.data.errors;
              }
            });
      }
  }
</script>

これでバリデーションエラーがあるときは各項目の下に表示されるようになるかと思います。

まとめ

今回は各項目の下にエラーメッセージを表示する方法をまとめました。
もっといい感じの方法があれば、是非コメントで教えてください。。

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

nuxt.jsで現在のウインドウサイズをどのコンポーネントからでもリアクティブなオブジェクトとして取得できるプラグインのつくりかた

はじめに

スマホに対応中に、要素の有無をcssの@media指定とdisplay:noneじゃなくてv-ifで操作したいけどめんどくさそうだから楽にしたいという欲求からプラグインの作り方から勉強したやつ。

とりあえずコード

plugins/window-resize.js
import Vue from 'vue'

Vue.use({
  install(Vue) {
    const $window = Vue.observable({
      width: 0,
      height: 0,
    })
    let queue = null
    const wait = 100

    const getWindowSize = () => {
      clearTimeout(queue);

      queue = setTimeout(function () {
        $window.width = document.documentElement.clientWidth
        $window.height = document.documentElement.clientHeight
      }, wait);
    }

    global.addEventListener('resize', getWindowSize)
    getWindowSize()

    Vue.prototype.$window = $window
  }
})
nuxt.config.js
plugins: [
//ウインドウサイズ取得のプラグインであるためSSR時には実行しないようにmode: 'client'を指定
  {src: '~/plugins/window-resize.js', mode: 'client'}, 
],

せつめい

install(Vue) - Vueのプラグインを作るときに必須のメソッド。詳しくはプラグインの記述(公式ドキュメント)
Vue.observable(object) - vue.jsおなじみのdata関数の戻り値にも適用されているメソッドで、このメソッドに処理されることによって、戻り値にリアクティブオブジェクトが得られる。Vue.observable( object )(公式ドキュメント)
setTimeout - resizeのイベントリスナーは数pxリサイズされる毎に発火され、どこからでも使える割に鬱陶しいので上記サンプルでは0.1秒スクロールが止まるまでは発火を抑制するようにしています。
global.addEventListener('resize', getWindowSize) - ここで、どこで画面をリサイズしても、ウインドウサイズが取得されるようにしている。
Vue.prototype.$window = $window - ここでどこからでもthis.$windowで使えるようにしている。

おわりに

<div v-if="$window.width > 1080">おつかー!</div>みたいな感じで表示を司ることができました!
vueプラグインなるものを初めて作ってみたのですが、Vue.observable君が便利ですね・・・
今後もいろいろ作って引き出し増やして行きたいと思ってます!

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

Vuexのmoduleのstateを呼ぶ時store.modules.◯◯.stateにならない理由

Vuexにはモジュールという機能があり、コンポーネントが複雑化して大きな一つのstoreにstateを管理するには難しくなった時のために、モジュールという単位で分けることで管理をすることが出来ます。

以下はmoduleの例です。Vuexのドキュメントに載っているものの写しです。
https://vuex.vuejs.org/ja/guide/modules.html

modules.js
const moduleA = {
  state: { ... },
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const moduleB = {
  state: { ... },
  mutations: { ... },
  actions: { ... }
}

const store = new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

store.state.a // -> `moduleA` のステート
store.state.b // -> `moduleB` のステート

moduleAとmoduleBはどちらもstateとmutationsを持っており、new Vuex.Storeでインスタンス化したオブジェクトをstore変数に入れています。その時modulesプロパティの中でmoduleAをa、moduleBをbというプロパティにセットしています。

ここで疑問が浮かんだのですが、moduleA内のstateにアクセスする時はstore.state.aとなります。「aの中のstate」を呼んでいるのに「stateの中からaを呼んでいる」ので、直感的にはstore.modules.a.stateではないかと思いました。しかしstore.state.aは間違っていません。

理由を探るためVuexのソースを見てみました。結論から言うと、Storeクラス内にモジュール内のstateとは全く別のstateというオブジェクトを作り、モジュール内のstateを代入して置き換えています。

詳しくVuexのコードを見てみます。上のコードのstore変数はnew Vuex.Storeで作ったオブジェクトなので、Storeクラスの中にstateオブジェクトがありそうです。

Storeクラスはsrc/store.jsに定義されていました。
https://github.com/vuejs/vuex/blob/dev/src/store.js
stateは以下のように定義されていました。関係のある部分だけまとめています。

src/store.js
import ModuleCollection from './module/module-collection'

export class Store {
  constructor(options = {}) {
    this._modules = new ModuleCollection(options)
    const state = this._modules.root.state
  }
}

https://github.com/vuejs/vuex/blob/ba2ff3a3de394a4c5c9a72ed7314ad3bb52f6a53/src/store.js#L53

Storeクラスのstate変数にthis._modules.root.stateが入っています。これが一番上の例でいうstore.stateに該当します。

Storeをインスタンス化する時引数に入ったオブジェクトをoptionsに入れています。このoptionsが一番上の例の{modules: {a: moduleA, b: moduleB}}の部分となります。
optiosを引数に入れたModuleCollectionクラスを初期化して_modulesに入れ、中のroot.stateを取り出しているようです。

ここで注意するべきはstore.jsの中にmodulesという変数はありません。したがってstore.modules.state.aと書いてしまうとstore.modulesがundefinedであるためにエラーになります。

ではstore.stateにセットしているrootは何なのか、moduleAとmoduleBはどのように取り出しているのかさらに掘り下げるためにModuleCollectionクラスを見てみます。こちらも関係ある部分だけを抜粋しました。
https://github.com/vuejs/vuex/blob/dev/src/module/module-collection.js

src/module/module-collection.js
import Module from './module'
import { forEachValue } from '../util'

export default class ModuleCollection {
  constructor (rawRootModule) {
    // register root module (Vuex.Store options)
    this.register([], rawRootModule, false)
  } 

  register (path, rawModule, runtime = true) {
    if (process.env.NODE_ENV !== 'production') {
      assertRawModule(path, rawModule)
    }
    const newModule = new Module(rawModule, runtime)
    if (path.length === 0) {
      this.root = newModule
    } else {
      const parent = this.get(path.slice(0, -1))
      parent.addChild(path[path.length - 1], newModule)
    }

    // register nested modules
    if (rawModule.modules) {
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        this.register(path.concat(key), rawChildModule, runtime)
      })
  }
}

ModuleCollectionをnewするときに引数にnew Vuex.Storeの引数と同じものを入れており、それを使ってregister関数を実行しています。
register関数内でModuleクラスをnewしており、this.rootに代入しています。これが先ほどstore.jsでみたthis._modules.rootとなります。

もしmodulesがあれば下の方のif(rawModule.modules)の中身が通るため、中のオブジェクトループで回して再帰でregister関数を実行しているようです。このrawChildModuleに上の例でいうmoduleAmoduleBが入ってきます。

newしているModuleクラスのコンストラクタもみてみます。
https://github.com/vuejs/vuex/blob/dev/src/module/module.js

src/module/module.js
export default class Module {
  constructor (rawModule, runtime) {
    this.runtime = runtime
    // Store some children item
    this._children = Object.create(null)
    // Store the origin module object which passed by programmer
    this._rawModule = rawModule
    const rawState = rawModule.state

    // Store the origin module's state
    this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
  }
}

このrawModuleには上の例でいうmoduleA、moduleBが入るため、その中のstateを新たにthis.stateに代入しているようです。これでstore.stateの中に各モジュール内のstateの中身を移すことが出来ました。

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

rails install:webpackerができない件について

環境

  • rails 5.2.3
  • ruby 2.6.3
  • yarn 1.19.2

エラー内容

railsでvue.jsを追加するために、rails install:webpackerをしようとしたところ

rails aborted!
Sprockets::Railtie::ManifestNeededError: Expected to find a manifest file in `app/assets/config/manifest.js`
But did not, please create this file and use it to link any assets that need
to be rendered by your app:

Example:
  //= link_tree ../images
  //= link_directory ../javascripts .js
  //= link_directory ../stylesheets .css
and restart your server
/Users/user/railsProjects/vuejs-on-rails/config/environment.rb:5:in `<main>'
/Users/user/railsProjects/vuejs-on-rails/bin/rails:9:in `<top (required)>'
/Users/user/railsProjects/vuejs-on-rails/bin/spring:15:in `require'
/Users/user/railsProjects/vuejs-on-rails/bin/spring:15:in `<top (required)>'
./bin/rails:3:in `load'
./bin/rails:3:in `<main>'
Tasks: TOP => app:template => environment
(See full trace by running task with --trace)

このようなエラーがでました。

対応

調べたところ、【Rails】rails webpacker:install に失敗する場合の対処法にたどり着き、書いてある通りにconfigにwebpacker.ymlを追加したところ、エラーが変わりませんでした?

その後、yarnのupgradeをしても変わりませんでした?

下記のエラーを確認

Expected to find a manifest file in `app/assets/config/manifest.js`

app/assets/config/manifest.jsに

app/assets/config/manifest.js
//= link_tree ../images
//= link_directory ../javascripts .js
//= link_directory ../stylesheets .css

を追加

無事にrails install:webpackerが通りました。

参考

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

CSSとVue.jsで遊ぶ【少し連想配列】

1.はじめに

今回は連想配列の復習とCSSの勉強をかねて静的サイトを作ってみました。

2.完成体

ダウンロード (1).gif
※写真は全て私の私物です。自分で試すときは好きな画像をお使いください。

3.解説

index.html
<!DOCTYPE html>
<html lang="en" dir="ltr">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>MENU BAR</title>
  </head>
  <body>
    <div class="maincontent"  id="app">
      <ul class="contentList">
        <li  v-for="content in list" :key="content.id" class="contentListChir">
          <div class="img-box">
            <a v-bind:href="content.url" target="_blank" class="atag">
              {{content.name}}
              <img v-bind:src="content.img" class="scale-img">
            </a>
          </div>
        </li>
      </ul>
    </div>
      <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
      <script>
        new Vue({
          el:'#app',
          data:{
            list:[
              {
                id:1,
                name:'kobe',
                url:'#',
                img:'IMG_5375.jpeg'
              },
              {
                id:2,
                name:'matsudo',
                url:'#',
                img:'IMG_1839.jpeg'
              },
              {
                id:3,
                name:'asakusa',
                url:'#',
                img:'IMG_1849.jpeg'
              },
              {
                id:4,
                name:'asakusa',
                url:'#',
                img:'IMG_1851.jpeg'
              },
              {
                id:5,
                name:'skytree',
                url:'#',
                img:'IMG_0390 2.jpeg'
              },
              {
                id:6,
                name:'rikugien',
                url:'#',
                img:'IMG_2077.jpeg'
              },
              {
                id:7,
                name:'kyufurukawa',
                url:'#',
                img:'IMG_2068.jpeg'
              },
            ]
          }
        })
      </script>
  </body>
  <style>
    body{
      background-color: black;
    }
    .maincontent{
      width: 805px;
      text-align: center;
      margin: 30px auto;
    }

    .contentList{
      display: flex;
      padding: auto;
      flex-wrap: wrap;/*.maincontentのwidthの範囲で自動的に折り返す*/
      margin: 70px auto;

    }
    .img-box{ /*外枠*/
      margin: 20px 20px; /*左右中央*/
      background-color: lightgray; /*背景色*/
      height: 140px; /*縦*/
      width: 200px; /*横*/
      position: relative; /*外枠に対して中の写真を中央に寄せる為、設定*/
      overflow: hidden; /*外枠からはみ出した部分を隠す*/
      border-radius: 5px; /*外枠の角に丸み*/
      text-align: center;/*aタグの文字を左右中央へ*/
    }
    .scale-img{
      position: absolute; /*position:relativeに対して絶対的な位置を決定*/
      top: 0;
      bottom: 0;
      left: 0;
      right: 0; /**top,bottom,left,rightを0にするとrealtiveを
      設定したタグに対して*左右上下中央に位置します*/
      width: 200px; /*外枠と同じ高さを設定*/
      height: 140px; /*外枠と同じ幅を設定*/
      transition-duration: 0.4s; /*変化の速度*/
    }
    .scale-img:hover { /* :hoverを付けるとカーソルが上に来た時のレイアウトを
      設定できる*/
      transform: scale(1.3,1.3); /*拡大処理(横、縦)*/
      opacity: .3; /*透過させて外枠に設定した背景色を浮かせている*/
      cursor: pointer; /*カーソルをポインターへ変化*/
    }
    .atag{
      text-decoration: none;/*リンク下線部を消去*/
      font-weight: bold;/*太文字*/
      color: black;/*白文字*/
      line-height: 140px;/*文字を上下中央に置くには親タグの高さに揃える*/
      font-size: 25px;/*文字サイズ*/
    }
  </style>
</html>

CSSの動き、解説についてはソースの中にコメントで記述してあるのでそれを参考にして下さい。

4.連想配列と配列の違い

配列は配列の中にvalueのみを収納し、自動的にindexが割り振られ、取得する際はそのindexを使ってvalueを取得します。
例:let array =['りんご','バナナ','いちご']
console.log(array[0]); ==>'りんご'

一方、連想配列はvalueと共にkeyを一緒に配列の中に入れてあげます。
keyと共に入れて何が良いのかというと取得する際にkeyによって可読性があがるというメリットがあります。
例:let obj ={name:'田中', hobby:'soccer',age:24}
console.log(obj.name)==>'田中'

例のように書けばobjの名前を取ってきているんだなと見てわかりますよね。

今回の完成体の中の連想配列を見てみると、、、
{
id:1,
name:'kobe',
url:'#',
img:'IMG_5375.jpeg'
}

画像一つの情報を連想配列に入れて、いくつもの画像のidとurl,imgをVueインスタンスのデータの配列の中に入れて上げることでそれぞれ画像を表示しているわけです。

5.最後に

今回、vueだけでなくcssを整えてみて、cssができると作品の質が一気nにあがるような気がします。プロダクトは機能も大事ですが使い手にとって心地の良いデザインを考えながらやっていきたいという想いがより強くなりました。

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