20200711のvue.jsに関する記事は16件です。

新しいMacでNode.jsのインストールからVue.js + nuxt.js + Vuetifyで開発環境手順(zsh対応版)

概要

macOS Catalina におけるフロントエンドの開発環境作成
Catalinaからbashからzshが標準になったため、zshでの環境構築方法を記載
※ バージョンは全て2020年6月30日時点のものです
新しいものが入っていても基本的には問題ないはず

brew

Mac用のパッケージ管理ソフト

インストール
$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"

coreutils

GNUコマンドを使用する

インストール
$ brew install coreutils

jq

JSONを成形、絞り込みを行う

インストール
$ brew install jq
$ echo ‘[{“hoge”: “hoge”, “fuga”: “fuga"}]’ | jq

xz

圧縮、解凍

インストール
$ brew install xz
$ xz -z hoge
$ xz -d hoge.xz

wget

ファイルのダウンロード

インストール
$ brew install wget
$ wget -r http://………/download.zip

確認

上記でインストールしたパッケージが表示されることを確認

$ brew list

nvm

Node.jsのバージョン管理ツール

nvmのGitHubを参照して最新バージョンをインストールする
https://github.com/nvm-sh/nvm#install-script

インストール
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash
環境設定
$ vim ~/.zshrc

vimが起動するので~/.zhrcに下記を追加

~/.zshrc
export NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm")"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"

ターミナルを再起動

バージョン確認
$ nvm --version

0.35.3

Node.js

JavaScript実行環境

インストール可能なバージョンを確認
$ nvm ls-remote --lts | grep Latest

v4.9.1 (Latest LTS: Argon)
v6.17.1 (Latest LTS: Boron)
v8.17.0 (Latest LTS: Carbon)
v10.21.0 (Latest LTS: Dubnium)
v12.18.1 (Latest LTS: Erbium)

お好きなバージョンをインストール
基本的には最新で良いと思います。

インストール
$ nvm install v12.18.1
バージョン確認
$ node -v

v12.18.1

npm

JavaScriptのパッケージ管理システム

yarn

npmと同じくパッケージ管理システム
今回はyarnを使用する

インストール
$ npm install -g yarn

serverless

Serverless Application(AWS, Azure, GCPなど)を構成管理デプロイするためのツール

インストール
$ npm install -g serverless

npmのライブラリを確認

バージョン確認
$ npm ls --depth=0 -g

/Users/[ユーザー名]/.nvm/versions/node/v12.18.1/lib
├── npm@6.14.5
├── serverless@1.74.1
└── yarn@1.22.4

Nuxt.js

vueのフレームワークの1つ
Routerの記述をする必要がなくなってめっちゃ便利

インストール
// プロジェクトを作成したいフォルダに移動
$ cd ***/*** 
$ yarn create nuxt-app <my-project>

上記コマンドを入力すると対話型のインストーラーが起動します
入力内容は下記を参考に自分に適したものを選択してください

? Project name: (my-project)

? Programming language: (Use arrow keys)
> TypeScript

? Package manager: (Use arrow keys)
> Yarn

? UI framework: (Use arrow keys)
> Vuetify.js

? Nuxt.js modules: (Press <space> to select, <a> to toggle all, <i> to invert selection)
 ◉ Axios
 ◉ Progressive Web App (PWA)
 ◯ Content

? Linting tools: (Press <space> to select, <a> to toggle all, <i> to invert selection)
 ◉ ESLint
 ◉ Prettier
 ◯ Lint staged files
 ◯ StyleLint

? Testing framework: (Use arrow keys)
> None 

? Rendering mode: (Use arrow keys)
> Single Page App 

? Deployment target: (Use arrow keys)
> Server (Node.js hosting) 

? Development tools: 
 ◉ jsconfig.json (Recommended for VS Code)
 ◯ Semantic Pull Requests

インストールが完了したら下記のコマンドを実行

$ cd <my-project>
$ yarn dev

? Are you interested in participation? (Y/n) 
n

http://localhost:3000/
アクセスしてVueのアイコンが表示されれば構築完了

あとはかっこいいWebサイトを作ってください!

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

Vueコマンドが使えない vue/cli [環境構築難民を救いたい]

挫折しそうな君を救う

HTMLやCSSの勉強も終わって、さぁ次はVue.jsやLaravel,Rubyの勉強だ!

そう思って、なんとかインストールし、いざコマンドを叩くと

zsh: command not found: vue

「どういうことだ?何をやってもこうなってしまう...」

せっかくやる気もあるのに...一体私が何をしたというのか...?

5分後、君はVue.jsを始めている

Vue自体はCDNを使えば簡単に体験できる。

本記事では、Vue/cliを使うための解説をする
前提としてnode.jsをインストールし、npmが使えているとする
なお、エディターはVSCodeを使う

参考書等にしたがって、Vue-cliで学習を進めようとした君
どうすれば良いのか?
結論から書こう

1.デスクトップに新規フォルダを作る
2.作ったフォルダをVSCodeのアイコンまでドラッグアンドドロップして、フォルダを開く
3.ターミナルを開く
4.コマンドを打つ

1,2の解説は不要だろう
3は画面上部を見るんだ。
ターミナルから、新しいターミナルをクリックだ

そして、4だ。次のコマンドを打つ

npm init -y

これでフォルダ内にpackage.jsonというファイルが出来上がるだろう
それを開くと、難しげな文字列が現れる。だが、大丈夫だ。全部事細かに理解する必要ない

次に

npm install vue -D

と打ち込もう。すると、package-lock.jsonとnode_modulesフォルダが生成され、
先ほど開いたpackage.jsonに
"vue": バージョン
が自動的に追記される。

これで、このローカル環境でvueが使えるようになった。
確認のために以下のコマンドを君は打つだろう

vue --version

ここで君を挫折させる憎き文字列の登場だ

zsh: command not found: vue

君を救うのはたった一文だ。

今開いているpackage.jsonに以下の文が確認できるだろうか?

package.json
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  }

ここに魔法の一文を追記する

package.json
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "vue": "vue"
  }

"test": ......
の末尾に,(カンマ)を書き忘れないようにしよう

しっかりと保存し、ターミナルに以下のコマンドを打ち込む

npm run vue --version

バージョンが表示されるはずだ。おめでとう、君の勝利だ

では、vue-cliをインストールしよう。
なお、インストールのコマンドは変わる可能性がある。
公式ページで確認しよう

npm install @vue/cli -D

その後、

npm run vue create myapp

myappの部分は君が決めていい。君の考えたイケてる名前をつけてあげよう
さぁ、あとは君の見ている参考書やサイトに書いてあることに従って進めてくれ

何が起きたのか?

「ちょ、ちょっと待ってくれよ!
-Dだとかinitだとか、"vue": "vue"だとかnpm runとか
意味が分からないじゃないか!進めたけど何も解決していないのではないか?」

落ち着け。本質を忘れてはならない。君が今したいのはVueの勉強だろう
そのためにせっせとググり、この記事までたどり着いたのだ。

ここまで示したことは、プログラミングを学習していけばなんとなく分かるようになってくる。
そう、私もよく分かってないからしっかり説明できないのだ。

だが、それでいい。君は振り返らず、存分にVueの勉強を初めてくれ。

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

【Vue】配列の追加・削除には注意が必要?

【Vue】オブジェクト追加・削除には注意が必要? の配列版です。
オブジェクトと同じく、参照するだけなら普通のJSと同だが、要素の追加・削除で嵌るポイントがあったので備忘録として。
↓の記事も参考に。

次のようなVueインスタンスのdata()charasという空の配列がセットしてあるとする。

export default {
  data () {
    return {
      charas: []
    }
  }
}

要素を追加する

オブジェクトでは$setを使ったが、追加するときは末尾であれば通常通りpush()でOKらしい。先頭に追加などは試していないのでわからず。

// 通常通り、`push`を使えばOK
this.charas.push('範馬刃牙')

要素をまとめて追加したい

スプレッド演算子...を使って配列を展開した要素をpush()で追加すればOK。

// まとめて要素を追加する
this.charas.push(...['範馬勇次郎', '列海王'])

要素を変更する

要素を変更したい時、いつも通りインデックスを使ってしまいたくなるがこれだとリアクティブにデータが反映されない(=データ更新してもテンプレート(画面)上に反映されない)。

splice()を使う。

構文は<配列>.splice(<始まりのインデックス>, <変更する要素数>, <変更後の要素>)と書くとのこと。
(書き方をいつも忘れてしまう)

見ての通り複数個まとめて変更することもできる。

// リアクティブにデータが反映されないのでこの書き方はダメ
this.charas[0] = '範馬勇一郎'
// これだとリアクティブになる
this.charas.splice(0, 1, '範馬刃牙')

要素を削除する

変更のときと一緒でsplice()を使う。

書き方は<配列>.splice(<始まりのインデックス>, <削除する要素数>)
見ての通り複数の要素をまとめて削除できる。

ちなみに戻り値は削除した要素を含む配列で破壊的メソッドとして働く。

// リアクティブにデータを削除
this.charas.splice(0, 2)

全削除したい

通常通り、arr.length = 0のイディオムを使いたくなるが、これだと配列の長さを変える操作なのでいけないみたい。
(一見、動いているようにみえるが、Vue作者もそう言っているので使うのはやめた方がよさそう)

// これは本当はダメ
this.charas.length = 0

要素をまとめて削除できるsplice()を使う。第二引数に配列の長さを渡せば全削除できる。

// これで全削除できる。
this.charas.splice(0, this.charas.length)  // => []

要素を全て入れ替えたい

配列を一度空にしてからまとめて追加すればよい

// 一旦、全削除して要素を入れ替える
this.charas.splice(0, this.charas.length)
this.charas.push(...['範馬刃牙', '範馬勇次郎', '列海王'])

配列自体を削除したい

あまり使うケースは無さそうだが、要素ではなく配列自体を削除したい場合はどうするか?

この場合はオブジェクトと同じでnullを入れればよいみたい。

// charasを削除する
this.charas= null
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails 6 + Vue(単一コンポーネント) + vue-router + Vuetify だけどMPA(SPAではない) - Multi SPA

構成

ひとつのRailsアプリケーション上に、単一コンポーネントで構成されたSPAを配置します。
まずはひとつのSPAをおくことを目標にしますが、同様に複数のSPAを置くことを意図しています。(よって完全なSPAではありません。またMulti-SPAとなります。)

構成のイメージ:

  • /shared
  • /private
  • /private/episode/:id/schedule
    • ここをSPAにする

Rails

Rails は、MVCなMPAアプリケーションをつくります。
Rails 6 webpacker を利用して vue を用います。webpacker と Vue の導入については説明を省きます。

Layout

SPA 用の layout を設けて、コントローラ(あるいはアクション)で切り替えます。ここでは application_spa とします。

application_spa は後述します。コントローラ#アクション schedule で layout を指定すると下記のようになります。これで episode_controller の schedule だけ application_spa を利用することになりました。

episode_controller.rb
  def schedule
    respond_to do |format|
      format.html { render :schedule, :layout => 'application_spa' }
    end
  end

のちに指定する vue-router と、ここで SPA対象となるRails controller#actionは、同じURLで同じSPAを使う必要があります。(そうでない場合、ひとつのURLで別のものが表示されてしまいます)

Vue 周りの構成

Vue を SFC(単一ファイルコンポーネント)で利用します。SFCなVueからは axios で API を利用します。Vuetify, vue-router, axios については説明を省略します。

この先の手順は Vue のSingle File Components 利用における Vuetify, vue-router, axios と同じです。

用意するもの:
- Vuejs
- vue-router
- Vuetify
- axios

使わないもの:
- Nuxt.js

Rails View

action view

View への routing は、Railsの routes.rb でaction schedule に向けられているとします。それにより schedule.html.haml が用いられるため、layout と schedule.html.haml をSPA用にします。

schedule.html.haml は、content_for を利用して、 layout で使用する javascript_pack_tag と stylesheet_pack_tag を切り替えられるようにします。切り替えられるようにする理由は、複数のSPAを作成する予定のためです(つまりMulti-SPAです)。

view/../schedule.html.haml
- content_for :pack, 'schedules'
#schedules

#schedules の指定は、のちほどJavaScriptで利用します。

layout

layoutは、 view で指定されたcontent_forによってJSファイルを読み込むようにします。stylesheet_pack_tag も指定しないと Vuetify のデザインが当たらないので注意しましょう。

layout/application_spa.html.haml
!!! 5
%html
  %head
    %title= ""
    = csrf_meta_tags
    = yield :meta
    = stylesheet_pack_tag "#{yield :pack}"
    = javascript_pack_tag "#{yield :pack}"
  %body
    = yield

Rails側のViewは以上です。大変シンプルですね。

Vue の呼び出し

javascript_pack_tag が呼び出す schedule.js が起点(エントリーポイント)となります。(よく見かける application.js を呼び出す部分に相当します。)

schedule.js

Rails View で指定した ID #schedules を使います。

packs/schedule.js
// 様々なimport や Vue.use の記述は省略

document.addEventListener('DOMContentLoaded', () => {
  const app = new Vue({
    el: '#schedules',  // schedule.html.haml で指定したもの
    vuetify, // import した vuetify 
    router, // import した vue-router
    render: h => h(App)
  }).$mount()
})

app.vue

schedule.js が render: h => h(App) で指定した app.vue は、 router-view を呼びます。記述には pug を用いています。

packs/app.vue
<template lang="pug">
  v-app
    v-app-bar( dark )
      v-toolbar-title Service Name is Unknown
    v-main
      v-container
        v-layout( wrap )
          router-view
</template>

router-view に割り当てられたコンポーネントは、次の router の設定で行っています。

vue router

vue 側の router の設定です。
アプリケーションが / から始まらないため、mode: 'history' を忘れないようにします。
この先、複数のSPAを作成するため、router も複数にします。そのため、 pack 配下に router/ を作成します。 router を schedule.js へ書かずに別ファイルとし packs/router/schedule.js として管理します。

packs/router/schedule.js
import VueRouter from 'vue-router';
import Schedule from '@/../components/schedule.vue';

const routes = [{
  path: '/episode/:id/schedule', component: Schedule,
  // 増えた path は、Rails 側も対応
}];

export default new VueRouter({
  base: '/private/', 
  mode: 'history',
  routes
});

base: '/private/' を指定して、アプリケーションの階層を /private 配下にしています。指定すると $route.path に含まれません。 router-link で :to を用いる際にも含まれなくなります。

この router/schedule.js は schedule.js から import されています。

axios

SFC 内で axios を用いてAPIにアクセスします。CSRF_TOKENはこちらを参照しました。

.vue の中では、methods で axios を用います。
遷移を伴う一覧を routers-list で記述しました。しかし、同じpathに一致する更新は、APIで取得した値を取り直してくれません。 そこで watch により変更を監視します。 vue-router公式では beforeRouteUpdate も使えると記載があります。

今回はbeforeRouteUpdateを利用しました。

@/../components/schedule.vue
// template は省略
// props や data は省略

methods: {
  getEpisode: function (event) {
    this.axios
      .get('/api/v1/episodes/' + this.$route.params.id + '')
      .then(response => (this.episode = response.data))
      // catch error 省略
  }
},

beforeRouteUpdate(to, from, next){
  this.getEpisode()
  next()
},
// watch を使う場合1
// watch: {
//  $route (to, from) {
//    this.getEpisode()
//  }
// },
//
// watch を使う場合2
// watch: {
//  '$route': 'getEpisode'
// },
// 

mounted () {
  this.getEpisode()
},

Vue側は以上です。これで/private/episode/:id/scheduleを起点としたSPAができました。

まとめ

Rails の layout をSPA用に分け、Railsで完結するアプリケーションと、Vue によるSPAを並存させました。Railsアプリケーション全体からみると、部分的にSPAを提供できます。

複数SPAを目的にした理由

ここでは、SPAを複数置くことを考えました。どの単位に分けると良いのかは、まだ明確な指針を持っていません。そのため、複数SPAを意図した理由についても簡単に記載します。

Vueのroutingで対応ができる範囲で複数のSPAに分けるのは、あまり意味はないだろうと考えています。エントリーポイントが schedule.js になるのかどうかの違いしかないためです。

一方で、起点ごとに異なるコンポーネントを利用する場合や、vue-router の path が遷移で繋がらない場合は、エントリーポイントを分ける意味があると考えました。

今回、構築したRailsアプリケーションのうち、スケジュール管理部分をVueのSPAにしたいと思いました。それだけでひとつのアプリケーションになり得る単位です。
また、同じアプリケーションを Episode 以外のモデルでも利用するため、再利用性の高いアプリケーションにしたいと考えました。そのためSPAによってModelを横断して取り扱えるようにしようと考えました。

今後、schedule 以外のエントリーポイントが増えることを想定しました。schedule 同様に、単独でひとつのアプリケーションになり得る単位です。たとえばログ管理を考えています。

その際に、SPAを統合するイメージが湧かず、別々に切りたいと考えました。Railsが管理する単位では1つのサービスですが、クライアント側に期待する単位では、それぞれ完結しています。たとえばスケジュール管理とログ管理はまったく別の操作性を期待しています。ログとスケジュールを同時に管理したいこともありません。

以上が、複数SPAを考えた理由です。

エントリーポイント間の差分

仮に schedule 以外に log を増やしたいとしましょう。

Rails側の作業はわずかです。
SPAにしたい箇所を、layout application_spa指定することと、log.html.haml view の中で #logs を使うこと、です。

Vue は、packs/schedule.js と同等のエントリーポイントとして packs/logs.js を用意し、 el: '#logs' とします。(もちろん他のelementでも構いません。)
また packs/logs.js は、log 用の router packs/router/logs.js を読み込みます。

axios や app.vue, Vuetify は作業が不要です。

つまり、複数のエントリーポイント間の差分は、対応する router/*.js の path と、利用するコンポーネント群の違いになります。

おわり

以上、Rails の layout をSPA用に分け、エントリーポイントを複数にすることで、Vue によるSPAを複数作成することができました。

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

Vue.js/Nuxt.jsでパララックス効果を実装

Vue/Nuxtでパララックスしようぜ!

※パララックス効果とは:スクロールしたときに、スクロール量に合わせてずれたり動いたりするやつ。

こういうやつ
sukiyaki2.gif

Vue.jsプラグインもいくつかあるけど、自前で実装した方が自由度高くて楽だった。勉強にもなるし。

実装方法概要

実装方法の大まかな説明

  • scrollイベントをlisten
  • スクロール量やら色々パラメータを取って移動量を計算
  • imgのstyleに object-position: {移動量X}% {移動量Y}% をぶち込む
index.vue
<template>
  <img
    ref="backgroundImg"
    class="background-img"
    src="~/assets/img/background.png"
    :style="`object-position: 50% ${objPosY}%;`"
  >
</template>

<script>
export default {
  data() {
    return {
      objPosY: 0
    }
  },
  mounted(){
    window.addEventListener('scroll', this.calculateScrollY) // スクロールイベントのlisten
  },
  methods: {
    calculateScrollY() {
      // this.objPosY に計算した移動量をぶち込む
    }
  }
}

以上の実装方法ですが、場合によって移動量の計算方法が異なります。

  1. img要素が完全に隠れている場合。(スクロールしないとimg要素が現れない場合)
  2. img要素が最初から見えている場合。(スクロールしなくてもimg要素が少しでも見えていて、これからスクロールする場合。)

1. img要素が完全に隠れている場合

:style="`object-position: 50% ${objPosY}%;`"
// または :style="`background-position: 50% ${objPosY}%;`"

CSSの object-position(またはbackground-position)で以下のように画像の位置が変えれるわけですが、その移動量を制御したい。
-50%とか、150%とかの状態がちょうど見えないようにスクロールしたい。0%から100%までで動かしたい。
image.png

img要素が完全に隠れている場合、画像の移動量は、以下の通りである。

  • スクロールして要素が見え始めた時は、移動量が0%である。
    • = ブラウザの表示領域のbottomが、img要素のtop に到達した時
  • スクロールして要素が見えなくなった時は、移動量が100%である。
    • = ブラウザの表示領域のtopが、img要素のbottom に到達した時

以上のように、this.objPosY がスクロール量に合わせて0から100まで動けばよい。

image.png

1.1 要素が見え始めた時にスクロール量が0となるやつを作る

getBoundingClientRect() を使ってスクロール量を取得し、新たに変数 topVisibleScrollY を定義した。

const rect = this.$refs.backgroundImg.getBoundingClientRect();
console.log(rect.top) // ブラウザの表示領域を基準とする、img要素の絶対座標(top) スクロールによって可変

const innerHeight = window.innerHeight // ブラウザの表示領域の高さ。

const topVisibleScrollY = -rect.top + innerHeight // 要素が見え始めた時にスクロール量が0。スクロールによって可変
console.log(topVisibleScrollY)

image.png

getBoundingClientRect().top は、ブラウザの表示領域を基準とする、img要素の相対座標である。
つまり、ブラウザの表示領域のtopimg要素のtopが重なった時に0になる。
「img要素のtop」を「img要素のbottom」に変えるために、img要素の高さを引いてやれば良い。

ただ、getBoundingClientRect().top は、下へスクロールするごとに値が小さくなっていくので、マイナスにして正負反転させていることに注意。こうすることで、imgが画面内に現れた後、下へのスクロールによって値は正の方へ増えていく。

これで、「スクロールして要素が見え始めた時は、移動量が0%である。」の条件がクリアできたので、今度は「じゃぁtopVisibleScrollYが何pxのときに100%とみなせばいいのか」を求めていきます。

1.2 topVisibleScrollYが何pxのときに100%とみなせばいい?

要素が見えなくなった時に topVisibleScrollY が何pxかがわかれば良い。
要素が見え始めた時を0としたスクロール量がtopVisibleScrollYだから、計算は簡単。

答えは、img要素の高さ表示領域の高さを足したやつになる。

// img要素の高さ
const height = this.$refs.backgroundImg.clientHeight

// `topVisibleScrollY`が何pxのときに100%とみなせばいいか
const bottomVisibleEndY = height + innerHeight 

image.png

1.3 要素が見えなくなった時に、移動量が100%となるやつを作る

topVisibleScrollYbottomVisibleEndY に達した時に100になるようにすればいい。

this.objPosY = topVisibleScrollY / bottomVisibleEndY * 100 // 見え始めた時は0、見えなくなった時100

topVisibleScrollYbottomVisibleEndY も単位がピクセル量なのでパーセントに変換。
おしまい!

1.4 全体のコード

index.vue
<template>
  <img
    ref="backgroundImg"
    class="background-img"
    src="~/assets/img/background.png"
    :style="`object-position: 50% ${objPosY}%;`"
  >
</template>

<script>
export default {
  data() {
    return {
      objPosY: 0
    }
  },
  mounted(){
    window.addEventListener('scroll', this.calculateScrollY) // removeEventListenerは別で書きましょう。
  },
  methods: {
    calculateScrollY() {
      const rect = this.$refs.backgroundImg.getBoundingClientRect();
      const innerHeight = window.innerHeight // 表示領域の高さ
      const topVisibleScrollY = -rect.top + innerHeight // 要素が見え始めた時にスクロール量が0。スクロールによって可変。

      const height = this.$refs.backgroundImg.clientHeight // img要素の高さ
      const bottomVisibleEndY = height + innerHeight // topVisibleScrollYが何pxのときに100%とみなせばいいか

      this.objPosY = topVisibleScrollY / bottomVisibleEndY * 100 // 見え始めた時は0、見えなくなった時100
    }
  }
}
</script>
<style>
.background-img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 50%;
  overflow: hidden;
}
</style>

2. img要素が最初から見えている場合

この場合、imgが上側へ隠れることがないので、これまでの実装方法だと移動量が0%になることがない。スクロールしてない状態でも、40%とか30%とかになってしまう。

スクロールしてない状態なら0%にしたい。ということで以下に変えれば解決。

  calculateScrollY() {
    const scrollY = window.scrollTop;
    const rect = this.$refs.backgroundImg.getBoundingClientRect();
    const height = this.$refs.backgroundImg.clientHeight
    const bottomVisibleEndY = height + rect.top + scrollY
    this.objPosY = scrollY / bottomVisibleEndY * 100
  }

image.png

スクロール量については、途中から0で始める必要がなくなったので、rect.topをやめて window.scrollTop に変更。
bottomVisibleEndY(scrollTopが何pxのときに100%とみなせばいいか) はimg要素の高さimg要素のY座標を足し合わせればOK。
img要素のY座標rect.top + scrollY で求められます。

事前知識 : CSS object-position について

今回は CSS の object-position を使って画像をずらしていたのでその説明。
参考:https://developer.mozilla.org/ja/docs/Web/CSS/object-position

画像の width と height を指定した上で、画像自体は cover にしたい時、よく使われるのは以下のようなコード。
backgroundに画像を指定する方法である。

.hoge-img {
  /* img要素ではなくdiv要素とかにつける */
  width: 100px;
  height: 100px;
  background: url('image.png') no-repeat;
  background-position: 50% 50%;  /* center; でも可 */
  background-size: cover;
}

image.png

ただこの方法は、CSSに画像のパスを配置する必要がある。なので、パスが動的に変わるような場合は、imgタグを使いたいときもある。
その際に登場するのが、object-fitobject-position

img.hoge-img {
  /* img要素につける */
  width: 100px;
  height: 100px;
  object-fit: cover;
  object-position: 50% 50%;  /* center; でも可 */
  overflow: hidden;
}

これでimg要素でも自由なサイズにクリッピングできる。ちなみにIEでは使えません。
https://caniuse.com/#feat=object-fit

今回はobject-positionを使ったが、background-positionを使っても同様にパララックスできる。
こうすればいいだけ↓↓↓

index.vue
<template>
  <div
    ref="backgroundImg"
    class="background-img"
    :style="`background-position: 50% ${objPosY}%;`"
  >
</template>
...

ついでに:addEventListenerについて

mixins.jsをpluginsフォルダにつくって、nuxt.config.jsにplugins: plugins: ['~/plugins/mixins'],とやる。
これでEventListenerを使う時は、this.listen(window, 'scroll', this.calculateScrollY) とするだけでよい。

~/plugins/mixins.js
import Vue from 'vue'

Vue.mixin({
  destroyed() {
    if (this._eventRemovers) {
      this._eventRemovers.forEach(function(eventRemover) {
        eventRemover.remove()
      })
    }
  },
  methods: {
    listen(target, eventType, callback) {
      if (!this._eventRemovers) {
        this._eventRemovers = []
      }
      target.addEventListener(eventType, callback)
      this._eventRemovers.push({
        remove() {
          target.removeEventListener(eventType, callback)
        }
      })
    }
  }
})

remove()をいちいち書かなくて良いの天才すぎる。以下から参考にさせていただきました。(多分)

PS : Twitterで「パララックス、SafariとiOSで動かなかったはずでは?」って言われたのでビビってます。とりあえずSafariでは動作確認できました。

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

Vuexでstoreのstateを初期化する

はじめに

Vuexを使用しているとき、ログアウト処理などでstoreのstateを初期状態に戻したいことがあると思います。いくつかやり方はあると思うのですが、私が手軽だと思った情報を記載します。

方法

こんな感じです。実際に利用する際はactionsなどから呼び出すことになると思いますが、ここでは最低限のコードのみ記載しています。
こちらを参考にしました。参考というよりそのまま持ってきただけですが。。

store/sample.js
// stateの初期値としたい任意のデータを定義する
function getDefaultState() {
  return {
    idToken: null,
    uid: null
  }
}

// stateを初期化する
export const state = getDefaultState()

export const mutations = {
  // stateを初期化するmutationを定義
  clearAuthData(state) {
    Object.assign(state, getDefaultState())
  }
}

おわり

とくに問題ないと思っていますが、何かご指摘あれば教えてくださいm(__)m

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

Laravelでvueを書く時のimage参照方法

vueで画像を表示する時はimgタグとbackground-imageを使う場合が考えられる。
ビミョーにやり方が違うので、備忘録として書いておきます。

imgタグを書く場合

Index.vueなど、vueファイルでimgタグを書く時は以下のように書く。

<img :src="'/images/top-appeal.jpg'" alt="" class="c-img">
やること

srcをv-bindする
②srcのダブルクォーテーション内部にシングルクォーテーションを書く
③vueファイルはトランスパイルされpublic/がディレクトリとしての起点になるので、そこからの相対パスを入力する。上記サンプルは、↓の画像を参照していることになる。
image.png

background-imageを書く場合

vueファイルのstyleタグ内でbackground-imageを書く時は、以下のようにする。

background-image: url('../image/hero-baner.jpeg');
やること

①画像ファイルをresourcesディレクトリ(scssやvueを置く場所)内に置く。
②vueでbackground-imageを書く時に、resourcesディレクトリを起点にした相対パスを書く。
↑の場合、↓の画像を参照している。
npm run watchなどでコンパイルした時にpublic/imagesに同様の画像をコピーが作られているのがわかるハズ。
image.png

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

Laravel + vueで画像を参照する時の書き方

vueで画像を表示する時はimgタグとbackground-imageを使う場合が考えられる。
ビミョーにやり方が違うので、備忘録として書いておきます。

imgタグを書く場合

Index.vueなど、vueファイルでimgタグを書く時は以下のように書く。

<img :src="'/images/top-appeal.jpg'" alt="" class="c-img">
やること

srcをv-bindする
②srcのダブルクォーテーション内部にシングルクォーテーションを書く
③vueファイルはトランスパイルされpublic/がディレクトリとしての起点になるので、そこからの相対パスを入力する。上記サンプルは、↓の画像を参照していることになる。
image.png

background-imageを書く場合

vueファイルのstyleタグ内でbackground-imageを書く時は、以下のようにする。

background-image: url('../image/hero-baner.jpeg');
やること

①画像ファイルをresourcesディレクトリ(scssやvueを置く場所)内に置く。
②vueでbackground-imageを書く時に、resourcesディレクトリを起点にした相対パスを書く。
↑の場合、↓の画像を参照している。
npm run watchなどでコンパイルした時にpublic/imagesに同様の画像をコピーが作られているのがわかるハズ。
image.png

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

NuxtプロジェクトにStorybookとStoryshotsを導入してcomponentをテストする

はじめに

試験的にNuxtプロジェクトにStorybookとStoryshotsを導入してみました。いくつかはまったところがあるので、今回、導入したときの手順を残しておきます。主なパッケージのバージョンは以下の通りであり、バージョンによって挙動が変わるのでお気を付けください!

"nuxt": "^2.13.0"
"@storybook/addon-storyshots": "^5.3.19",
"@storybook/addon-storyshots-puppeteer": "^5.3.19",
"@storybook/vue": "^5.3.19",
"babel-core": "7.0.0-bridge.0",
"babel-plugin-require-context-hook": "^1.0.0",
"jest": "^25.5.4",
"puppeteer": "^5.0.0",

また、今回作成したサンプルプロジェクトはgithubにあげているので、詳細を確認したいときはこちらにアクセスくださいm(__)m

Nuxtプロジェクトの作成

はじめにに、Nuxtプロジェクトを作成します。今回はUIにBulmaを使ってみます。
実際にプロジェクトを作っていくときは、ディレクトリ構造をデフォルトから変えていくことが多いと思うのですが、今回はあくまでもStorybookなどにフォーカスするため、そのままにします。

>npx create-nuxt-app sample-project
? Project name: sample-project
? Programming language: JavaScript
? Package manager: Npm
? UI framework: Bulma
? Nuxt.js modules: (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Linting tools: ESLint, Prettier
? Testing framework: Jest
? Rendering mode: Single Page App
? Deployment target: Static (Static/JAMStack hosting)
? Development tools: (Press <space> to select, <a> to toggle all, <i> to invert selection)

テスト対象のコンポーネントを用意

今回は、componetsディレクトリ直下に以下のようなcomponentを用意しました。このcomponentをテスト対象としてみます。

Button.vue
<template>
  <div>
    <button
      :class="['button', colors || 'is-primary', size || 'is-normal']"
      :Disabled="disable"
      @click="clickHandler"
    >
      <slot>
        <span>{{ btnText }}</span>
      </slot>
    </button>
  </div>
</template>

<script>
export default {
  props: {
    btnText: String,
    colors: String,
    size: String,
    disable: Boolean,
  },
  methods: {
    clickHandler(event) {
      this.$emit('click', event)
    },
  },
}
</script>

Storybookのセットアップ

パッケージのインストール

まずはインストールします!

npm i -D @storybook/vue @storybook/addon-actions 

環境設定

ルートディレクトリに .storybookディレクトリを作成し、以下のファイルを作成、保存します。今回はaddon-actionsを利用してみるのでaddons.jsを用意しています。

config.js
import { configure } from '@storybook/vue'
import 'bulma/css/bulma.css' // Nuxt上で読み込んでいるbulmaをstorybookでも使えるようにする

// ルートディレクトリ直下のstoriesディレクトリにある*.stories.jsファイルを読み込む
configure(require.context('../stories', true, /\.stories\.js$/), module)

webpack.config.js
const path = require('path')
const rootPath = path.resolve(__dirname, '..') // プロジェクトのルートパスを指定する

module.exports = async ({ config }) => {
  mode = 'development'

  config.resolve.extensions = ['.js', '.vue', '.json']
  config.resolve.alias['~'] = rootPath // パス解決に必要
  config.resolve.alias['@'] = rootPath // パス解決に必要

  // 必要に同じてloaderを設定することもできる
  // 今回は不要なのでやらない

  return config
}
addons.js
import '@storybook/addon-actions/register'

storiesの追加

Button.vueを利用したstoriesを追加します。プロジェクトルートディレクトリの直下にstoriesディレクトリを作成し、以下のようなファイルを追加していきます。

Button.stories.js
import { storiesOf } from '@storybook/vue'
import { action } from '@storybook/addon-actions'
import Button from '~/components/Button.vue'

storiesOf('atoms.Button', module).add('colors', () => ({
  components: { Button },
  template: `
      <div style="display: flex; flex-direction:column; align-items:center;">
        <Button colors="is-primary" @click="action" btn-text="is-primary" style="margin: 16px 0;"></Button>
        <Button colors="is-info" @click="action" btn-text="is-info" style="margin: 16px 0;"></Button>
        <Button colors="is-danger" @click="action" btn-text="is-danger" style="margin: 16px 0;"></Button>
      </div>
    `,
  methods: { action: action('クリックされました') },
}))

試しに、以下のようにstorybookの起動コマンドを追加して…

package.json
  "scripts": {
    // 以下のコマンドを追加する
    "storybook": "start-storybook -p 6006"
  },

Storybookを起動してみます。

npm run storybook

コメント 2020-07-10 185219.png

やり遂げました。

…ではなくて、ここからStoryshotsを追加していきます。。

StoryshotsによるSnapShotテスト

Storybookの出力をスナップショット(画像ではなくテキスト情報です。念のため。。)として保存しておき、テスト時に変更があったかを自動で判定することができます。

私がプロジェクトを作成したときのJest(26.0.1)とStoryshots(5.3.19)の組み合わせでは"TypeError: require.requireActual is not a function"が出て動作しませんでした。Jestを25に落とせば動くとのことでしたので、該当する場合はJestのバージョンを25に指定してから以下の作業を進めてください。(他の解決策もあるかもです)

パッケージのインストール

Storyshotsを利用するために以下のパッケージを読み込んでいきます。

npm i -D @storybook/addon-storyshots babel-plugin-require-context-hook

StorybookでimportしているCSSをMockする

Mockを追加しない場合、config.jsでBulmaをimportしている箇所で"SyntaxError: Invalid or unexpected token"と怒られてしまいます。以下に記載するものと別の手順などもありますので、詳細を確認したい方はこちらからご確認ください。

Mockファイルの作成

まずは、ルートディレクトリにmocksディレクトリを作成し、以下のファイルを追加していきます。

styleMock.js
module.exports = {}

Jestの設定

以下のようにjest.config.jsを修正してcssにMockを割り当てます。

jest.config.js
  moduleNameMapper: {
    // 以下の行をmoduleNameMapperに追加する
    '\\.(css)$': '<rootDir>/__mocks__/styleMock.js',
  },

babelの設定

babelのプラグインであるbabel-plugin-require-context-hookをbabel.config.jsに設定します。今回使用したバージョンのStoryshotsでは、webpackのrequire.contextがサポートされていないということで、この設定をしないとStorybookのconfig.jsを読み込んだときに"TypeError: require.context is not a function"となってしまいます。(こちらのissueより)

babel.config.js
  // 以下の設定を追加する
  env: {
    test: {
      plugins: ['require-context-hook'],
    },
  },

スナップショットテストのテストファイルを作成

テストファイルを保存しているディレクトリにテストファイルを追加します。今回はルートディレクトリあるtestディレクトリに以下のファイルを追加しました。

storyshots.spec.js
import path from 'path'
import initStoryshots from '@storybook/addon-storyshots'

initStoryshots({
  configPath: path.resolve(__dirname, '../.storybook'),
})

テストを実行する

テストを実行してみます。

npm run test

テストが成功すると、テストファイルを保存しているディレクトリに__snapshots__/storyshots.spec.js.snapが作成されます。このファイルは以下のように、Storybookで読みこんだButton.vueコンポーネントのスナップショットです。

storyshots.spec.js.snap
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Storyshots atoms.Button colors 1`] = `
<div
  style="display: flex; flex-direction: column; align-items: center;"
>
  <div
    style="margin: 16px 0px;"
  >
    <button
      class="button is-primary is-normal"
    >
      <span>
        is-primary
      </span>
    </button>
  </div>

  <div
    style="margin: 16px 0px;"
  >
    <button
      class="button is-info is-normal"
    >
      <span>
        is-info
      </span>
    </button>
  </div>

  <div
    style="margin: 16px 0px;"
  >
    <button
      class="button is-danger is-normal"
    >
      <span>
        is-danger
      </span>
    </button>
  </div>
</div>
`;

試しに、Button.vueのボタン内テキストを変更してみると、変更が検知されてテストが失敗します。

  <button
    class="button is-info is-normal"
  >
    <span>
-     is-primary
+     変更した文字列
    </span>
  </button>

StoryshotsによるVisual Regressionテスト

Storybookがビルドした結果を画像として保存しておき、テスト時に変更があったかを自動で判定することができます。

パッケージのインストール

必要なパッケージをインストールします。

npm i -D @storybook/addon-storyshots-puppeteer puppeteer

Storybookのビルド

以下のようにpackage.jsonにビルドコマンドを追加してそのコマンドを叩くなどしてStorybookをビルドします。

package.json
  "scripts": {
    // 以下のコマンドを追加する
    "build-storybook": "build-storybook"
  },
npm run build-storybook

Visual Regressionテストファイルの追加

テストファイルを保存しているディレクトリにテストファイルを追加します。今回はルートディレクトリあるtestディレクトリに以下のファイルを追加しました。

image.spec.js
import path from 'path'
import initStoryshots from '@storybook/addon-storyshots'
import { imageSnapshot } from '@storybook/addon-storyshots-puppeteer'

initStoryshots({
  suite: 'Image storyshots',
  test: imageSnapshot({
    storybookUrl: `file://${path.resolve(__dirname, '..', 'storybook-static')}`, // Storybookをビルド出力先を指定
  }),
  configPath: path.resolve(__dirname, '..', '.storybook'),
})

テストを実行する

テストを実行してみます。

npm run test

テストが成功すると、テストファイルを保存しているディレクトリに__image_snapshots__ディレクトリが作成され、その中に画像ファイルが保存されています。このファイルは以下のように、Storybookで読みこんだButton.vueコンポーネントの画像スナップショットです。こちらは画像で比較を行い、画素に変更があればテスト失敗となります。

コメント 2020-07-11 071741.png

おわりに

何度かエラーに出くわしながらも、調べながら動作させることができました。今後、Storybook周りがアップデートされたらもっと簡単に導入できるようになるのかな???

運用にあたっての追加設定やテストの自動化など、いくつか調べていこうと思います。

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

キーコードを使ってvue.jsのキーイベントの処理

key.html
<body>
    <div id="app">
        <form action="">
            <label for="name">氏名:</label>
            <input type="text" id="name" v-on:keyup.27="clear" v-model="name">
        </form>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
    <script src="key.js"></script>
</body>

key.js
new Vue ({
    el: '#app',
    data: {
        name: '匿名'
    },
    methods: {
        clear: function() {
            this.name = '';
        }
    },
});

なお、keyup.escでも同じ処理となる。
さらにキーエイリアスの作成も可能。

key.js
new Vue ({
    el: '#app',
    data: {
        name: '匿名'
    },
    methods: {
        clear: function() {
            this.name = '';
        }

    },

});

Vue.config.keyCodes = {
    // camelCase won`t work
    'no-change': 29,
};
key.html
<div id="app">
        <form action="">
            <label for="name">氏名:</label>
            <input type="text" id="name" v-on:keyup.no-change="clear" v-model="name">
        </form>
    </div>

なお、同時押しはalt.exact.81やalt.exact.enterなど重ねれば問題なし。exactがない場合は他のボタンが押されても反応する。

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

修飾子onceを使った、一度しか引けないおみくじボタン

omikuji.html
<body>
    <div id="app">
        <input type="button" value="結果表示" v-on:click.once="onclick">//clickの後にonceをつける
        <p v-cloak>今日の運勢は{{ result }}です</p>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
    <script src="omikuji.js"></script>
</body>


omikuji.js
new Vue ({
    el: '#app',
    data: {
        result: '_'
    },
    methods: {
        onclick: function(){
           var ranNum = Math.floor(Math.random() * 10) +1;
           if(ranNum === 1) {
               this.result = '大吉'
           } else {
               this.result = '大凶'
           }
        }
    },
});
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.jsで起こる、ページ読み込み時のマスタッシュ構文のちらつきを回避

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vue.js</title>
</head>
<body>
    <div id="app">
        <p v-cloak>{{ message }}</p><!-- cloakはid="app"のタグにかいても大丈夫。 -->
    </div>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
    <script src="https://cdn.jsdelivr.net/npm/lodash@4.17.5/lodash.min.js"></script>
    <script src="vue.js"></script>
</body>
</html>

     [v-cloak] {
         display: none;
     }
new Vue ({
    el: '#app',
    data: {
       message: 'こんにちは'
    },
});

もはや必須ですね。

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

v-スタイルプロパティの設定

styleをバインディングするにあたっての書き方

1パターン目

style.html
<body>
    <div id="app">
        <div v-bind:style="{ backgroundColor: 'Aqua', fontSize: '1.5em' }">こんにちわ</div>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
    <script src="vue.js"></script>
</body>
</html>

style.js
new Vue ({
    el: '#app',

});

2パターン目

style.html
<body>
    <div id="app">
        <div v-bind:style="[color,size]">こんにちわ</div>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
    <script src="vue.js"></script>
</body>
</html>
style.js
new Vue ({
    el: '#app',
    data: {
       color: {
           backgroundColor: 'Aqua',
           color: 'Red'
       },
       size: {
           fontSize: '1.5em'
       }
    },
});

styleに配列、オブジェクトを入れることができるということを知った。
改めて見ると結構勉強になるなぁ。
こちらの方がとても詳しく書いてくださってます。
https://qiita.com/yuta-38/items/94cdde004fb225f8b1b2

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

vueで属性をバインディングし、画像の大きさを動的に変更する

タグの属性も動的に変えることができる。

attr.html
<body>
    <div id="app">
        <select v-model="attr">
            <option value="height">高さ</option>
            <option value="width"></option>
        </select>
        <input type="text" size="5" v-model="size"><br>
        <img src="https://wings.msn.to/image/wings.jpg" v-bind:[attr]="size"><!-- []を使う -->
    </div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script>

</body>
image.js
var app = new Vue({
    el: '#app',
    data() {
        return {
            attr: 'width',
            size: 100
        }
    },
})

あまり見慣れないし、使う機会もなさそうだが、知ってて損はなさそう。

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

複数の属性(value, type,classなど)をVueのdataでまとめて管理する方法

こういうやり方もあるのかって感じで書いておく。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vue.js</title>
</head>
<body>
    <div id="app">
        <form action="">
           <label for="memo">メモ:</label>
           <!-- <input type="text" id="memo" size="20" max-length="14" required> 本来(HTMLのみ) -->
           <!-- <input type="text" id="memo" :size="size" max-length="maxLength" :required="required"> ひとつずつ指定 -->
           <input type="text" id="memo" v-bind="attrs">
        </form>
    </div>
    <script src="vue.js"></script>
</body>
</html>

new Vue ({
    el: '#app',
    data: {
       attrs: {
           size: 20,
           maxlength: 14,
           required: true,
       }
    },
});

属性ごとに段落を変えるVueのやり方があまり好きでないので、長くなる場合はこっちで書いてみようかな。
コンポーネントでは最近この書き方を始めた。

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

v-ボタンを押して配列の一部を変更する

Vueでは配列の一部を変更しても検知されない。
そういった場合は、Vue.set(配列, 配列の番号, 値');

array.html
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
</head>
<body>
    <div id="app">
        <div>
            <form action="">
                <input type="button" value="追加" v-on:click="addPajama">
                <input type="button" value="変更" v-on:click="changePajama">
                <input type="button" value="削除" v-on:click="deletePajama">
            </form>
            <ul>
                <li v-for="item in list" v-bind:key="item">{{ item }}</li>
            </ul>
        </div>
    </div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script>
<script>

var app = new Vue({
    el: '#app',
    data() {
        return {
            list: ['赤パジャマ', '青パジャマ', '黄パジャマ']
        }
    },
    methods: {
        addPajama(){
            this.list.push('茶パジャマ');//末尾に追加。先頭ならunshift()
        },
        changePajama(){
            Vue.set(this.list, 1, '茶パジャマ');/this.$set()でもよ        },
        deletePajama(){
            this.list.shift();
        },
    },
})
</script>

</body>
</html>

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