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

Vue + Vuetify + Storybook v6の環境を構築する

Vuetify環境にStorybookを導入する際、vue-cli-plugin-vuetify-storybookで素直にインストールするとStorybook v5が導入されてしまいます。
「どうしてもStorybook v6が使いたい.......! (新しいもの好き)」
とチャレンジしてみたところ苦戦したのでまとめてみました。

構築した環境

パッケージ バージョン
@vue/cli 4.5.11
vue 2.6.12
vuetify 2.4.5
@storybook/vue 6.1.20

Vue + Vuetify環境のプロジェクトの作成

Vuetifyのサイト(https://vuetifyjs.com/ja/getting-started/installation/ )を参考にVueプロジェクトを作成し、Vuetifyをインストールします。

# Vueプロジェクトの作成 (プリセットは「Default ([Vue 2] babel, eslint)」を選択しました)
$ vue create my-app
# 作成したプロジェクトへ移動
$ cd my-app
# Vuetifyをインストール (プリセットは「Default (recommended)」を選択しました)
$ vue add vuetify

現時点でVuetifyはVue 3には対応していません。
Vueプロジェクト作成の際にはVue 2を選択してください。

以下のコマンドを実行し、ブラウザで http://localhost:8080/ にアクセスし確認します。

$ npm run serve

無事Vue + Vuetifyの環境が構築できました。

Storybook v6のインストール

Storybookの公式サイト(https://storybook.js.org/docs/vue/get-started/install )を参考にStorybook v6をインストールします。

$ npx sb init

以下のコマンドを実行し、ブラウザで http://localhost:6006/ にアクセスし、Storybookが起動できることを確認します。

$ npm run storybook

Storybookの設定

まず、StorybookのWebpackを設定します。
Storybook v5ではwebpack.config.jsに設定を書いていたと思いますが、v6ではmain.jsに記載します。

.storybook/main.js
const path = require('path') 

module.exports = {
  "stories": [
    "../src/**/*.stories.mdx",
    "../src/**/*.stories.@(js|jsx|ts|tsx)"
  ],
  "addons": [
    "@storybook/addon-links",
    "@storybook/addon-essentials"
  ],
  // webpackの設定
  webpackFinal: async (config, { configType }) => {
    // @がsrcディレクトリをさすように設定
    config.resolve.alias['@'] = path.resolve(__dirname, '..', 'src')
    // sass-loaderを設定
    config.module.rules.push({
      test: /\.sass$/,
      use: [
        'style-loader',
        'css-loader',
        'sass-loader',
      ],
    })
    return config
  },
}

次にVuetifyプラグインファイルを少し書き換えて、optionsをexportするようにします。
(本番と同じvuetifyオプションをStorybookでも使用できるようにするためです)

src/plugins/vuetify.js
import Vue from 'vue';
import Vuetify from 'vuetify/lib/framework';

Vue.use(Vuetify);

// オプションをエクスポートします。といっても、今回は特にオプション設定していないので空オブジェクトです。
export const options = {}

export default new Vuetify(options);

Storybookの設定ファイルにvuetifyの設定を記載します。
Storybook v5ではconfig.jsに設定を記載していましたが、v6からはpreview.jsに記載することになりました。

.storybook/preview.js
import Vue from 'vue'
import Vuetify from 'vuetify'
import 'vuetify/dist/vuetify.min.css'
import { options } from '@/plugins/vuetify.js'

// Vuetifyを設定し、vuetifyインスタンスを作成
Vue.use(Vuetify)
const vuetify = new Vuetify(options)

export const parameters = {
  actions: { argTypesRegex: "^on[A-Z].*" },
}

// decoratorsでvuetifyインスタンスをVueのオプションに登録
export const decorators = [
  (story, context) => {
    const wrapped = story(context)
    return Vue.extend({
      vuetify,
      components: { wrapped },
      template: `
        <v-app>
          <v-container fluid>
            <wrapped />
          </v-container>
        </v-app>
      `
    })
  },
]

Storyを書いてみる!

まずコンポーネントファイルを作ります。

src/components/MyButton/MyButton.vue
<template>
  <v-btn color="success" @click="$emit('click')">
    {{ text }}
  </v-btn>
</template>

<script>
export default {
  name: 'MyButton',
  props: {
    text: {
      type: String,
      default: 'ボタン',
    }
  }
}
</script>

次にストーリーを書きます。

src/components/MyButton/MyButton.stories.js
import vuetify from '@/plugins/vuetify'
import MyButton from './MyButton'

export default {
  component: MyButton,
  title: 'MyComponents/MyButton',
  argTypes: {
    click: { action: 'click '},
  }
}

const Template = (args, { argTypes }) => ({
  components: { MyButton },
  props: Object.keys(argTypes),
  // vuetifyインスタンスを登録する
  vuetify,
  template: '<my-button v-bind="$props" @click="click" />',
})

export const Default = Template.bind({})
Default.args = {
  text: 'できた!'
}

これらのファイルを作り、Storybookを起動し、http://localhost:6006/?path=/story/mycomponents-mybutton--default にアクセスすると、

Storybook_MyButton.png

できました!

参考文献

以下のWebサイトにとても助けられました。

最後に

今回.storybook/preview.jsだけでなく、MyButton.stories.jsのTemplateでもvuetifyインスタンスを与えています。
このようにしないと$vuetifyを使った際にエラーになる箇所があったためです。
もっとスマートな方法があればぜひ教えてください。

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

webpack-dev-serverにドメインでアクセスする

やりたいこと

Dockerコンテナ上でnpm run serveしたときに任意のドメインでアクセスしたい

設定

nginx-proxyを利用して設定したドメインで各コンテナにアクセスできる状態にする。

nginx-proxyを利用した環境の構築はここ等を参考にしてください。

docker-compose.ymlはこんな感じ

docker-compose.yml
version: '3.5'

services:
  node:
    build: ./infra/node
    environment:
      VIRTUAL_HOST: "任意のドメイン"
      HTTPS_METHOD: "noredirect"
    expose:
      - 3000
    networks:
      - app-network
      - local-network
    command: sh -c "npm run serve"

networks:
  app-network:
    name: app-network
  local-network:
    external: true
    name: local-network

vue.config.jsに設定を記述します。

vue.config.js
module.exports = {
  devServer: {
    host: '0.0.0.0',
    port: 3000,
    disableHostCheck: true, 
    watchOptions: {
      poll: true
  }
}

以上の設定でnpm run serveした後に任意のドメインでアクセスが可能になります。

所感

今回やりたいことを実現するにあたって正直理解できていないところがたくさんある(host: '0.0.0.0'やnginx-proxyの仕組み等)ので、一つ一つ調べて理解して使うことが大事だと痛感しました。

参考

nginx-proxyを利用した環境の構築
【超便利】DockerでPHPローカル開発環境の最強構築方法

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

【Vue.js】v-modelを使った双方向データバインディングについて

v-model、双方向データバインディングについて少しだけまとめ。

データバインディングとは?

データを結びつけること

双方向とは?

vueの記述を変えると、HTML側も書き変わるような仕組み。

簡単なコード

<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>

<div id="app">
<input type="text" v-model="message"> 
  <h1>{{message}}</h1>
</div>
new Vue({
    el: '#app',
  data: {
    message: 'がんばれ'
  }
})

これで下のような表示になるが、ここで文字を入力すると、リアルタイムでh1タグで囲んでいるmessageの文字も変わる。

スクリーンショット 2021-03-03 20.36.40.png

これがv-model。

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

【Vue.js】v-onディレクティブでボタンを押したら数字が増える

<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>

<div id="app">
  <p>{{ number }}回目のクリックです</p>
  <button @click="countUp">ボタン</button>
</div>
new Vue({
    el: '#app',
  data: {
    number: 0,
  },
  methods: {
    countUp: function() {
      this.number += 1
    },
  }
})

これでスクリーンショット 2021-03-03 20.20.10.png

ボタンを押すと、数字が増えていくようなものが出来あがる。

<button @click="countUp">ボタン</button>
この@clickは本当は,v-on:clickと記述するが、このように@で省略する記法もある。

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

爆速で検索できる顔文字サイトを開発した話

きっかけ

某S●mejiに対抗するため、えりんぎという顔文字サイトを2020年1月に公開

志半ばで中途半端な状態のまま放置してドメインを失効しそうになる
閉鎖しようと思ったが友人がずっと使ってくれてたのを知る

というわけでバグだらけの「えりんぎ」をリニューアルすることに

作ったもの

顔文字をカテゴリやキーワードで検索できるサイト
えもしぇあ https://emoshare.net

製作期間は3週間で60時間くらい
※顔文字を登録する作業が10時間以上占めている( ;꒳​; )

LighthouseでPerformanceが100にならなかったけど爆速
※Google Analytics入れたら92に下がった( ;꒳​; )
99.png

技術

Laravel 6.x (普段使っている安心安定のLaravel)
Vue3 (Composition API 使ってみたかった)
Tailwind CSS (Laravel8で採用されたらしいので使ってみたかった)

実装方針

実装に時間が掛かりそうなものはできるだけ除外
コンポーネントはあまり細かく分けない
新しいことはあまりしない(躓いたら諦める)

実装したい機能

検索したときにページ遷移せずいい感じで表示したい
タップやクリックでクリップボードにコピー
コピーした履歴を持ちたい
できるだけ爆速で表示したい
いいね機能
顔文字にキーワードを持たせて検索しやすくする(管理画面で設定するのめんどくさくなって途中から設定してない)
投稿(めんどくさくなってやめた)
ダークモード(めんどくさくなってやめた)
多言語化する(顔文字に日本語を含めなくなるのでやめた)

画面設計

5分で完了
爆速
Screenshot.png

DB設計

5分で完了
爆速
ER.png

実装

いいねボタン

これ参考にした

タグクラウド

チェックボックスの状態でON/OFFを切り替える

vue.js
// 一部省略
<label :for="'category' + category.id">
  <input
      :id="'category' + category.id"
      type="checkbox"
      class="category-input hidden"
  />
  <div class="category-button-wrapper">
    <span class="category-button">
      #{{ category.name }}
    </span>
  </div>
</label>
style.scss
.category-input {
  &:checked {
    + .category-button-wrapper {
      .category-button {
        color: #fff;
        background: #f59e0b;
        border-color: #f59e0b;
      }
    }
  }

  &:active {
    + .category-button-wrapper{
      .category-button {
        color: #fff;
        background: #fbbf24;
        border-color: #fbbf24;
      }
    }
  }
}

:checked:hover で実装したところスマホでうまく動かなかったが :hover:active に変えたら動いた

コピー履歴

Local Storageでコピーした顔文字を保持

localstorage.png

エラー画面

地味に 401, 403, 404, 405, 419, 429, 500, 503 エラーに対応した
error.png

gzip圧縮

cssとjsを圧縮する
zopfli使ったら結構小さくなった

tailwind.config.js
module.exports = {
  purge: [
    './resources/js/components/**/*.vue',
    './resources/views/**/*php'
  ],
  darkMode: false, // or 'media' or 'class'
  theme: {
    extend: {},
  },
  variants: {
    extend: {},
  },
  plugins: [],
}
webpack.mix.js
const mix = require('laravel-mix')
const tailwindcss = require('tailwindcss')
const CompressionPlugin = require('compression-webpack-plugin')
const zopfli = require('node-zopfli')

mix.js('resources/js/app.js', 'public/js').vue()
  .sass('resources/sass/app.scss', 'public/css')
  .options({
    processCssUrls: false,
    postCss: [ tailwindcss('./tailwind.config.js') ],
  }).webpackConfig({
  plugins: [
    new CompressionPlugin({
      test: /\.(css)|(js)$/,
      algorithm: (content, options, fn) => {
        zopfli.gzip(content, options, fn);
      },
    })
  ]
})
.htaccess
# gzip対応
RewriteCond %{HTTP:Accept-Encoding} gzip
RewriteCond %{REQUEST_FILENAME}\.gz -s
RewriteRule ^(.+) $1.gz

<FilesMatch "\.css\.gz$">
    ForceType text/css
    AddEncoding x-gzip .gz
</FilesMatch>
<FilesMatch "\.js\.gz$">
    ForceType application/x-javascript
    AddEncoding x-gzip .gz
</FilesMatch>

デプロイ

レンタルサーバーにデプロイする方法忘れたのでこれを参考にした
ヘテムルにLaravelをデプロイする方法 (Laravel5.8)

最後に

Tailwind CSSのおかげで命名する時間を節約できたのはよかった
あとレスポンシブ対応も楽だった
Google Analytics入れたらBest Practicesが100にならないんだけどどうしたらいいの?

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

【Vue.js】Composition-APIで簡単なタイピングゲームを作ってみました

以前Vue.jsで作った奴をComposition-APIに書き換えて+αしてみました。

ロジックとかは大体同じです。

環境

version
macOS Catalina 10.15.7
npm 6.9.0
node 10.16.0
vue 2.6.11
@vue/composition-api 1.0.0-rc.2

完成品

demo

demo

AppVue
<template>
  <div id="app">
    <div v-if="!playing">Enterキーでスタート</div>
    <div v-else>
      <div>残り{{ time }}</div>
      <div>単語数:{{ typedWord }} / ミス:{{ miss.num }}</div>
      <br />
      <div>
        <div v-if="miss.status">?</div>
        <div v-else><br /></div>
        <span style="opacity: 0.3;">{{ typeChar }}</span>
        <span>{{ nowWord }}</span>
      </div>
    </div>
    <br />
    <div v-if="!time">ゲーム終了!おつかれさまでした!?</div>
  </div>
</template>

<script>
import { ref, reactive, toRefs } from '@vue/composition-api';
export default {
  setup() {
    // 単語関係ーーーーーーーーーーーーーーーーーーーーーーー
    const wordStore = reactive({
      nowWord: '',  // 出題されている単語
      typeChar: '', // 入力した単語の一文字
      typedWord: 0, // 入力し終えた単語の数
      words: [      // 出題単語
        'dog',
        'cat',
        'monkey',
        'elephant',
        'giraffe',
        'penguin',
        'lion',
        'tiger',
        'pig',
        'cow',
      ],
    });
    // wordsからランダムな単語を1つ取り出す
    const setWord = () => {
      wordStore.nowWord = wordStore.words[Math.floor(Math.random() * wordStore.words.length)];
    };

    // タイピング関係ーーーーーーーーーーーーーーーーーーーー
    const miss = reactive({
      // ミスの回数とミスしたかどうか
      num: 0,
      status: false,
    });
    const keydown = (e) => {
      // 間違えたときの処理
      if (wordStore.nowWord[0] !== e.key) {
        miss.num++;
        miss.status = true;
        return;
      }
      miss.status = false;
      // typeCharに入力できた文字を追加していく
      wordStore.typeChar += e.key;
      // 入力できたらnowWordの頭文字を削除していく
      wordStore.nowWord = wordStore.nowWord.slice(1);
      // 単語を入力し終えたときの処理
      if (!wordStore.nowWord) {
        wordStore.typedWord++;
        wordStore.typeChar = '';
        setWord();
      }
    };

    // カウントダウン関係ーーーーーーーーーーーーーーーーーー
    const time = ref(10); // カウントダウンのタイム
    const countDownTimer = () => {
      const id = setInterval(() => {
        time.value--;
        if (!time.value) {
          // ゲーム終了。keydown関数のキーイベントを削除
          removeEventListener('keydown', keydown);
          clearInterval(id);
        }
      }, 1000);
    };

    // ゲーム開始ーーーーーーーーーーーーーーーーーーーーーー
    const playing = ref(false); // ゲームが始まっているかどうか
    const startGame = (e) => {
      // 入力がEnterキーでなければゲームが始まらない
      if (e.key !== 'Enter') return;
      playing.value = true;
      // 各関数実行
      // startGame関数のキーイベントを削除
      removeEventListener('keydown', startGame);
      // keydown関数のキーイベントを発火
      addEventListener('keydown', keydown);
      setWord();
      countDownTimer();
    };
    // startGame関数のキーイベント発火
    addEventListener('keydown', startGame);

    return {
      ...toRefs(wordStore),
      playing,
      miss,
      time,
      setWord,
    };
  },
};
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  font-size: 20px;
  margin-top: 100px;
}
</style>


ロジックとそれに関連する変数ごとに分けてまとめるように意識しました。

素のjavascriptで書いてる感じが強かったです。

もっとCompotision-APIの要素を入れたかったですが、これが限界でした笑

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

[Vue]ページ遷移時にページを画面のトップに戻したい

概要

Vue Routerでページ遷移する時、前のページのスクロール位置のまま遷移してしまうので画面が中途半端な位置になってしまいます。

ページ遷移した時に画面トップに戻る処理を実装できるのがscrollBehavior関数です。

実装

const router = new VueRouter({
  mode: 'history',
  routes: [
    {
      path: '/',
      name: 'home',
      component: HomeComponent,
      props: true,
      meta: {isPublic: true},
      children: [
        {
          path: '/trend',
          name: 'trend',
          components: {
            trend: TrendTimeline,
          },
          props: true,
          meta: {isPublic: true},
        },
        {
          path: '/item',
          name: 'item',
          components: {
            item: TrendItem,
          },
          props: true,
          meta: {isPublic: true},
        }
      ]
    },
  ],
  scrollBehavior (to, from, savedPosition) {
    if (savedPosition) {
      return savedPosition
    } else {
      return { x: 0, y: 0 }
    }
  }
})

to: 遷移先のルート情報
from: 遷移元のルート情報
savedPosition: ブラウザの戻る/進むボタンがトリガーされた時にのみ使う

上記だとブラウザ戻る、進むボタンが押された時以外はページトップに戻るようになっております。

特定のルートだけページ位置を戻したくない場合は、

  scrollBehavior (to, from, savedPosition) {
    if (savedPosition || to.name == 'home' ||to.name == 'trend' || to.name == 'item') {
      return savedPosition
    } else {
      return { x: 0, y: 0 }
    }
  }

のようにto.nameで指定することで実装することができます。

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

Userモデルの変更、Vuexの導入

今日やること
1. apiのUserモデルの'first_name', 'last_name'をフロントエンド側で編集できるようにする。
2.Vuexの使い方

1ではまずapi上で情報が編集できるようにしてからフロントエンド側でも編集できるように変える。

api上でUserモデルを編集する

Userモデルにpermissionを与える

まず、Userを編集するためにはpermissionを得る必要があるが、現在設定されているものはsettings.pyで確認できる。

settings.py
REST_FRAMEWORK = {
    # Use Django's standard `django.contrib.auth` permissions,
    # or allow read-only access for unauthenticated users.
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
    ],
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.TokenAuthentication'
    ]
}

どうやら標準のパーミッションの設定が追加されているようだ。このrest_framework.permissions.DjangoModelPermissionsOrAnonReadOnlyの意味は「もし、ログインしていない状態ではreadonly」ということだ。つまり、編集するためにはトークンを指定してログインする必要がある。だが、request headersにトークンを指定してもまだ編集することができない、、、

これはUserViewsetにpermissionクラスが設定されていないためである。なのでviewsets.pyに追加しよう。

viewsets.py
class UserViewSet(viewsets.ModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer
+    permission_classes = [permissions.IsAuthenticated]

するとこのような画面になる。

スクリーンショット 2021-03-03 9.43.47.png

無事にapi上での編集を許可することができた。

フロントエンド側で編集した内容をapiへ送る

次にフロントエンド側で変更した内容をaxiosを使ってdjangoのapiに送信していこう。

axiosのPATCHメソッドを利用して、データの更新方法を確認します。PATCHでは既存のデータの上書きを行うので、更新したい項目と値の組を指定する必要があります。また更新するユーザを識別するIDも必須となります。

patchの使い方は以下のようになる。

ex.py
axios.patch('/user/id',{
   firstName: 'Jone',
   lastName : 'Dow'
  })
  .then(function (response) {
    // handle success
    console.log(response);
  })

今回はProfile.pyでユーザーが['first_name']['last_name']を変更した際に情報を渡したいので次のようになる。

Profile.py/VUE
            <form class="form">
                <div class="form-row">
                    <div class="form-group col-md-6">
                        <label>First Name:</label>
                        <input type="text" class="form-control" v-model="account['user']['first_name']">
                    </div>
                    <div class="form-group col-md-6">
                        <label>Last Name: </label>
                        <input type="text" class="form-control" v-model="account['user']['last_name']">
                    </div>
                    <v-btn @click="updateProfile">Update</v-btn>
                </div>
            </form>
Profile.py/JS
     methods: {
         updateProfile(){
             axios

                .patch('/users/' + this.account.user.id + '/',{
                 first_name: this.account['user']['first_name'],
                 last_name: this.account['user']['last_name']
             }

             )
             .then(resp => {
                 console.log(resp.data)
                 this.account.user = resp.data
                 this.$store.commit('setAccount', this.account)
             })
         }

ここで .patch('/users/' + this.account.user.id + '/'があるが、これはアカウントのUserIdを指定している。

また、first_namelast_nameの編集内容はv-modelの内容からとってくる。

v-modelとは?

Vue.jsのディレクティブの1種。
dataオブジェクトのプロパティの値とinputタブの入力値や選択値のデータを連動することができる。
簡単に言うと、画面上でボックスに入力した内容が、jsファイルのデータに反映できる便利な機能。
jsファイル側のデータを更新した場合も、画面上の入力値に連動させることができるため、双方向データバインディング(データ結合)と呼ぶ。

▼使い方
タグの属性に記述する。
v-model="プロパティ名"
 ┗ プロパティ名:dataオブジェクトの中の連動させてい値をもつプロパティ名を記述。

Vuexを使う

Vuexとは?

VuexとはVue.jsのためのデータ保存ツールです。
すべてのコンポーネントからアクセスでき、さらに意図しないデータの変更を防げます。
例えば、ログイン中のユーザーに関するデータはどのコンポーネントからも参照できたほうが便利ですよね。
Vuexはデータを1箇所に集中させて管理を容易にします。
今回の場合であればaccountの情報を管理します。

Vuexのインストール方法

では、実際にインストールをしてみましょう。 以下のコマンドでvueにインストールできます。

vue add vuex

Vuexの使い方

では、実際にVuexを使ってみましょう。

ストアの作成
ストアとは、データを保持しておく場所です。今回は、store.jsという名前のファイルを作成します。

store.js
import Vue from vue
import Vuex from vuex

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
+    account: null
  },
  mutations: {
+   setAccount(state, account) {
+    state.account = account
    }
  },
  actions: {
  },
  modules: {
  }
})

stateを使うことで”account”というデータはどこからでも参照できるようになりました。(グローバルにする)

次にstateで定義したaccountは直接書き込むことはできないのでmutationsにaccountの値を設定して、、、

ん〜〜、なんだか難しい

Vue.js + Vuexでデータが循環する全体像

一度、vuexについてしっかりと学ぶ必要がありそうなのでここでまとめておく。

vuexの図解

スクリーンショット 2021-03-03 10.49.39.png

  1. アプリ1つに対してStoreも1つ

これはvuexをインストールした際に自動生成されるフォルダーである。(store/index.js)

  1. データの変更は必ずmutationを経由
    mutationsに登録した関数を呼ぶために使うのが「$store.commit()」です。
    storeのmutationsに登録した関数以外の場所でstateを変更してはいけません。

  2. storeに保存したデータはstateから読み取る

あらゆるコンポーネントが$storeにアクセスできます。
データの読み取りは$store.stateで。
②で説明した通り、読み取ったstateを直接書き換えてはいけません。

Vuexの使い方(改めて)

ここまで理解した時点で改めて実際のコードを確認しよう

store.js
import Vue from vue
import Vuex from vuex

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
+    account: null
  },
  mutations: {
+   setAccount(state, account) {
+    state.account = account
    }
  },
  actions: {
  },
  modules: {
  }
})
Nabar.vue/VUE
        ul.navbar-nav 
          li.nav-item(v-if="!account")
            a(href="/login/") Login
          li.nav-item(v-if="account")
            a(href="/" @click="logout") Sign Out
Navbar.vue/JS
<script>
export default {
  name: 'Navbar',
  data(){
    return {
    }
  },computed: {
    account() {
      return this.$store.state.account
    }
  },
  methods:{
    logout(){
      alert("are you sure?")
      localStorage.removeItem("account")
      this.$store.commit('setAccount', null) 
      this.$router.push('/')
    }
  }
}
</script>

ストアに保存されているデータを変更するときは、毎回コミットして保存します。 流れは以下の通りです。

変更ボタンを押すとmethodsの”logout”ファンクションが実行される
第1引数のsetAccuntが実行され、store.jsのmutationsのAccountの第2引数に’null’が渡される
state.accountがnullになる。
これでデータの変更が完了しました。

今日の課題

・ vuetifyを使ってUI部分を整える
・ Web Scrapingを自分で学習しインスタの投稿写真のリンクURLをpython上で取得する(ヒント:beautifulライブラリーを使う)

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

[Vue/Nuxt]client-onlyのコンポーネントはrefで取得できない

client-onlyはSSRさせるアプリケーションにおいて、SSRさせずにクライアント側で展開させたい場合に使うタグです。

<template>
  <div>
    <client-only>
      no-ssr
    </client-only>
    <p>ssr</p>
  </div>
</template>

このclient-only内に展開したコンポーネントをrefで取得するケースで、まず通常の取得ケースは以下のようになります。

<template>
  <div>
    <client-only>
      <my-component ref="myComponentRef" />
    </client-only>
  </div>
</template>

<script>
import { ref, onMounted } from 'vue'

export default {
  setup() {
    const myComponentRef = ref(null)
    onMounted(() => {
      console.log(myComponentRef.value)
    })
    return { myComponentRef }
  }
}
</script>

refで空の変数を用意しておいて、テンプレートのrefに代入、onMounted後であればVue v2のthis.$refsの様にスクリプト内でコンポーネントにアクセスできます。
ですが、client-only内に展開したコンポーネントではこの方法ではアクセスできません。
このコードを実行するとmyComponentRef.valuenullとなります。

nextTickを使う

この場合はnextTickを利用すると解決できます。

<script>
import { ref, onMounted, nextTick } from 'vue'

export default {
  setup() {
    const myComponentRef = ref(null)
    onMounted(() => {
      nextTick(() => {
        console.log(myComponentRef.value)
      })
    })
    return { myComponentRef }
  }
}
</script>

おわり

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

【部分的SPA】初心者なりにRuby on Rails, Vue.jsでポートフォリオを作成してみた【タイピングアプリ 】

初めに

 プログラミング学習を初めて2ヶ月程度の初心者ですが、自分なりにあれこれ工夫をしながらrailsアプリに部分的にSPAを導入したポートフォリオを作成してみました。本記事ではアプリの概要について記述し、その後いくつかの記事に分けて工夫した箇所を投稿していきたいと思います。
 皆様のポートフォリオ作りに少しでもお役に立てると幸いです。
 また、所々GIF画像を挿入していますが、Qiita上だと動きが悪いかもしれません。画像をクリックして頂けるとGyazoのページに遷移致しますので、そちらでしたらヌルヌル動くかと思います。

関連記事

 このポートフォリオを作成するにあたって使用した技術を順次記事にしていきます。

アプリの概要

Image from Gyazo
 「エンジニア初学者用の英単語タイピングアプリ」です。
 日本語のタイピングならそれなりの速さで打てても、英単語のタイピングになった瞬間にガクッと速度が落ちますよね?また、コードは誤字脱字があるとエラーになってしまうので正確に打てているか確認が必要ですが、それもなかなか時間が掛かりますよね?(私だけ?)
 そんな悩みを解消すべく作ったのがこのアプリ「Typie」です。
 Typieはプログラミングに使用する英単語に絞ってタイピングの練習が出来るアプリです。

 URLは下記になります。PCで使用することが前提のアプリなので、レスポンシブ非対応になります。ご了承ください。
Typie_URL: http://54.95.229.185/

Github_URL:

開発環境

  • フロントエンド
    • HTML/CSS/JavaScript
    • Vue.js 2.6.12
    • Vuetify 2.4.4
  • バックエンド
    • Ruby 2.6.5
    • Ruby on Rails 6.0.3.5
  • インフラ
    • Mysql/MariaDB
    • AWS(EC2)/Nginx/Unicorn/Capistrano
  • その他
    • Visual Studio Code

使用イメージ

新規投稿

Image from Gyazo
 フォームに「タイトル」「言語名」「ソースコード」「色」を入力して送信すると、ソースコードから英単語を抽出して問題を作成する仕組みになっています。ソースコードに登場する英単語なら、それは「プログラミングに使用する言語」と言えるという発想です。

詳細表示・編集・削除など

Image from Gyazo
 マイページを弄り倒しているGIFです。詳細表示や編集・削除は全てダイアログを表示して行うようにしました。編集や削除結果は非同期通信で反映され、地味に詳細表示ページのヘッダーの色を問題集(今後"Book"と呼びます)の色と同期させています。(この辺は半分遊んでます)

絞り込み検索

Image from Gyazo
 Bookが増えてくると目当てのものが探し辛くなるので、絞り込み検索機能も実装しました。

機能一覧

機能 概要
アプリ使用方法紹介機能 ランディングページにてアプリの使用方法をカルーセル形式で紹介しています
ユーザー管理機能 新規登録・ログイン・ログアウトが出来ます
入力補佐機能 フォームは入力内容が条件を満たしていないと枠が赤色に、満たすと緑色に変化します
Book一覧表示機能 Bookのタイトル、言語名、単語数、ハイスコアが表示されます
Book詳細表示機能 Bookのタイトル、言語名、スコアTOP5が表示される他、タイピング・編集・削除もここから行えます
Book編集機能 Bookのタイトル、言語名、色を編集出来ます
Bookの削除機能 Bookを削除出来ます
Bookの絞り込み検索機能 言語名から絞り込み検索を行えます
カウントダウン機能 いきなりタイピングが始まらないよう、3秒数えます
タイピング機能 登録された英単語を出題し、正解・不正解文字数をカウントします
残りの単語数表示機能 あとどれくらいで終わるのかが分かる機能です
途中退出機能 途中で退出してもスコアが算出されます
スコア算出機能 タイピング技能検定が使用している計算式(下記リンク)を用いてスコアを算出します
  • タイピング技能検定URL

工夫した点

 とにかく「英単語のタイピングに慣れていないエンジニア初学者向け」であることを主軸に、以下の課題をどう解決するかを考えました。

既存のエンジニア向け英単語タイピングアプリで感じた課題

  • 投稿した範囲全てが問題として出題されるタイプ
    • 1つ1つの英単語のタイピングに時間が掛かると全然進んでいる気がしない
    • 終わりが見えないストレスがある
    • 途中で抜けるとスコアは出ない
    • 入力すべき文字がハイライトされていて見づらい
    • タイマーが気になる
  • 単語単位で出題されるタイプ
    • 出題される単語を、一度全て自力で設定するのはキツい

DB設計

db.png

各テーブルについて

テーブル名 説明
Users ユーザー情報
Books Bookの情報
Words 英単語情報、初出単語のみ保存
Book_words Book/Wordの中間テーブル
Languages 言語名情報、初出言語のみ保存
Book_languages Book/Languageの中間テーブル
Scores スコア情報

苦労した点

フロントエンド

  • 思った通りにならない
    「このプロパティを追加すればこうなるはず」…と思って記述しても想像と違う結果になる。何も変化していないことが最も多い。バックエンドと違ってエラーが出ないからどこがどう間違っているのか分かりづらい。あとセンスが無い。
  • Vue.jsとのデータの受け渡し
    当然ながらインスタンス変数が無い。JSON形式に変換しようとするも、狙った通りの連想配列にならない。本当に表示するデータに限定して受け渡しをしないと、表示されるまで時間が掛かる。

バックエンド

  • 6つのテーブルの値が変動するフォームオブジェクト
    BookとWord/Languageは多対多の関係。そしてWordは追加される単語の数だけ増え、Languageは増えるとしても1つだけなので処理が全く違う。そしてBook一覧表示ではベストスコアが表示されるので、スコアが空のままだとnilclassのエラーになってしまう。
  • フォームオブジェクトの編集
    レコードの編集は簡単だと思っていた。…それは全部railsがやってくれていたからで、1から自力で実装するのはなかなかの難易度だった。

参考にした学習教材・記事

本当に色んなものを参考にさせて頂きました。
ここでは特に助けていただいたものを一部抜粋して紹介します。

Vue.js

  • 【書籍】『Vue.jsのツボとコツがゼッタイにわかる本』(中田亨, 秀和システム, 2019/3/28)
     Vue.jsの基礎はこの書籍から学びました。ミニアプリの作成を通して解説を読んでいく形式なのでとても分かりやすく、これからVue.jsの学習を始める方におすすめの一冊です。

  • 【書籍】『Vue.js&Nuxt.js超入門』(掌田津耶乃, 秀和システム, 2019/2/11)
     こちらは「アプリを作る」ことを意識して書かれている一冊です。Nuxt.jsやFirebaseについても解説があるので、最低限の基本は既に抑えているという方にはこちらがおすすめです。

  • 【有料動画】『超Vue.js2完全パック』(よしぴー, Udemy, 最終更新 2020/12/20)
     Vue.js界隈では有名な動画です。Vue.jsの基礎から応用まで解説している動画になります。話し手の方がとても早口なのですが、ちゃんと当倍速で聞き続けているとだんだんクセになっていきます。 

Ruby on Railsへの組み込み

  • 【有料記事】『Rails × Vue.js でメモアプリを作成しながらモダンな開発を学ぼう!』(Daijiro, Techpit, 最終更新 2021/1)
     Vue.jsの基礎的な説明もされているのですが、どちらかというと「アプリ開発過程を追体験出来る」教材になっているので、Vue.js/Ruby on Railsの基礎は定着させた上で受講するのがよろしいかと思います。axiosやjbuilderを使って実際にアプリを作成する過程を学ぶことが出来ます。

  • 【有料記事】『【Rails / Vue.js】Snippet アプリを作ってみよう!』(kozzy, Techpit, 最終更新 2020/9)
     Vue.jsについての解説は少ないのですが、関数を使ったダイアログの表示/非表示切り替え、CRUDの実装、Vuetifyの使い方を学ぶことが出来ます。

開発に詰まった際の解決方法

 Vue.jsをRuby on Railsアプリに組み込む際に直面した課題の解決方法に関する記事です。詳細は今後違う記事にまとめようと思いますが、ここでは簡単な紹介をさせて頂きます。

  • 【無料記事】『【Rails】foremanでrails serverとwebpack-dev-serverを一度に起動する』(五十川洋平, 最終更新 2019/4/2)

  • 【無料記事】『【Rails】jbuilderの使い方辞典〜メソッドの文法と使い方がすぐわかる』(Pikawaka, 最終更新 2020/4/3)

  • 【無料記事】『【Vue】axiosで、デフォルトでCSRFトークンを設定できるようにする』(@ngron, Qiita, 最終更新 2019/10/14)

今後の課題

  • テストコードの記述
  • ドメイン取得
  • SSL取得
  • 完全なSPA化
  • ユーザー間交流
  • 絞り込み検索機能の充実化
  • dockerの学習と導入
  • CircleCIの学習と導入

 まだまだやることが沢山ありますね。今後の転職活動の状況も考えながら少しずつ進めていきたいと思います。

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

デコレーターを使おうとしたら怒られた

背景

vuex-module-decoratorsを使ってストアを宣言したら、
クラス名にて怒られた

import { Module, VuexModule, Mutation, Action } from 'vuex-module-decorators';

@Module
export default class AuthStore extends VuexModule {
}

デコレーターの実験的なサポートは将来のリリースで変更になる可能性がある機能です。'experimentalDecorators' オプションを設定してこの警告を削除します

対処

VSCodeの設定にてExperimental Decoratorsの設定を変更。

  1. 基本設定を開く
  2. 設定を開く
  3. 検索窓にexperimentalと入力
  4. JS/TS › Implicit Project Config: Experimental Decoratorsの設定にチェックを入れる

参考記事

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

Vue.js URLからidを取得

例えば以下の?がつくURLで1を取得したい場合

http://localhost:3000/maps?tweet_id=1

クエリパラメーターと言うみたいです
これで1が取得できます

<template>
<div id="app">
  {{$route.query.tweet_id}}
</div>
</template>

詳細ページでよく見る以下のURLでは

http://localhost:3000/tweets/1

これで良いと思います

<template>
<div id="app">
  {{$route.params.id}}
</div>
</template>

おまけ

axiosでJSONデータを取得するときなどはthisをつけると取得できます

      axios
        .get(`/api/v1/tweets/${this.$route.params.id}/index.json`)
        .then(response =>{
          this.analyses = response.data;
        })
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

初心者必見!!再利用するコンポーネントは「Vue.component」で定義しよう!

皆さんこんにちは!

何回も使うコンポーネントって毎回importするの面倒くさくないですか??

それにimportした後にcomponentsで定義しなきゃいけない。。。

僕の場合、ユーザーに分かりやすいようにパンくずリストのコンポーネントをページごとに必ず設置します。

このように何回も使うコンポーネントは再利用できるようにVue.componentで定義しちゃいましょう!!

main.js
import Vue from 'vue'
// パンくずリストコンポーネント
import Breadcrumb from './components/Breadcrumb'

Vue.component('Breadcrumb', Breadcrumb)

一応簡単に説明しますね。

Vue.componentは、第1引数に呼び出すときの名前、第2引数に対象のコンポーネントをそれぞれ引数にします。

作業はこれだけです!

後はいつものように呼び出してあげてください。

作業効率アップ♪

以上、「初心者必見!!再利用するコンポーネントは「Vue.component」で定義しよう!」でした!

また、何か間違っていることがあればご指摘頂けると幸いです。

他にも初心者さん向けに記事を投稿しているので、時間があれば他の記事も見て下さい!!

あと、最近「ココナラ」で環境構築のお手伝いをするサービスを始めました。

気になる方はぜひ一度ご相談ください!

Thank you for reading

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