20200204のvue.jsに関する記事は12件です。

【Vue.js】Vuexを使ったとき、フォームでv-modelを使うと不具合が出る問題の対処

はじめに

Vuexを使って状態管理すると、フォーム入力でv-modelを使うと、
「ちょっと!勝手にstate変えずにちゃんとmutation通してくれるかな:weary:
と怒られる問題があります。

自分はこれに対してどうするのがベストか分からず、試行錯誤を繰り返していました。

そんな時に、手間がかからずラクな方法こちらのブログで発見したので、メモしておきます。

環境

OS: macOS Catalina 10.15.3
Vue: 2.6.10
vuex: 3.1.2

前提

ユーザー情報を入力するフォームがあったとします。

今回はシンプルな例にするため、そのユーザー情報の中でもnameを入力する<input>タグを例にします。

v-modelを使った場合

sample.vue
<template>
<!-- 略 -->
  <input v-model="user.name" >
<!-- 略 -->
</template>

シンプルにこれでOKです。

が、Vuexを使ってこのuserの情報を管理している場合は冒頭の
「ちょっと!勝手にstate変えずにちゃんとmutation通してくれるかな:weary:
と怒られる問題が発生します。

これを大した手間をかけずに解決する方法が以下です。

改善後

<input>タグ内

sample.vue
<template>
<!-- 略 -->
  <input
    :value="user.name"
    @input="updateUser($event, 'name')"
  >
<!-- 略 -->
</template>

ここで<input>タグ内のinputイベントで次のupdateUserメソッドが発火、このときのDOMイベントが$eventに入ります。

<input>タグのあるコンポーネントのmethods

sample.vue
<script>
//略
  export default {
//略
    methods: {
      updateUser(event, key_name) {
        this.$store.commit('updateUserState', { 
      value: event.target.value, key_name });
      }
    }
  }
//略
</script>

先程のDOMイベントから入力された文字列valueを抜き出し、key-nameと共に以下Vuexストア内mutationsで定義されたupdateUserState()の引数として渡します。

Vuexストア内

store.js
export default ({
//略
  mutations: {
    updateUserState (state, { value, key_name }) {
      state.user[key_name] = value;
    }
  }
//略
})

最後に、このupdateUserState()でストアのuserの値を変更し、ストア内と<input>タグ内に入力された文字列が同じになります。

以上です!
使い回しやすいコードで超助かりました:relaxed:

おわりに

最後まで読んで頂きありがとうございました:bow_tone1:

どなたかの参考になれば幸いです:relaxed:

参考にさせて頂いたサイト(いつもありがとうございます)

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

Vue.js 3.0 の新機能を試す。 〜 Portal 編〜

2020年Q1リリース予定のVue.js 3.0の新機能 Portal を試してみたのでまとめます。

(参考)
以下でVue 3.0(vue-next)の環境構築、他の新機能についてもまとめています。

Portalとは?

定義したコンポーネントが属するDOMツリーとは別の場所に、まるでテレポートしたかのようにコンポーネントを移動できる機能です。
直接指定の位置に表示できるので、

  • Modal、Tooltipなど要素の上部レイヤーに表示したいコンテンツでもz-indexでのCSSハックが不要になる
  • Modalなどの独立したコンポーネントに対して親要素のstyle干渉を防げる

などの利点があります。

Vue.js 2系でもサードパーティのプラグイン LinusBorg/portal-vue を使えば実現できます。
Vue.js 3.0ではそれを仮想DOMレベルでサポートするようです。(実装詳細まではあまり理解してない)

Feb-04-2020 20-13-33.gif
(PortalVueのロゴ。概念を表していてとても良い。)

Portalの書き方

<Portal>コンポーネントにtargetを指定した上で、内部に別箇所で描画したい要素を記載するだけです。それだけで、targetで指定したセレクタの要素内部に、<Portal>内部の要素が移動して描画されます。
セレクタで指定する要素は、VueのDOMツリー外部のものでも可能です。

sample.vue
<Portal target="セレクタ">
  <!-- 以下がtargetで指定した要素に移動して描画される -->
  <MyModal />
</Portal>

Portalのサンプルアプリの実装

下記画像のようなサンプルアプリを作っていきます。
画像右側のデバックコンソールをみると、モーダル要素はApp.vueがマウントされている<div id="app">ではなく、<div id="myModal">内部に表示されるのがわかると思います。

一応動作コードはこちらのリポジトリにあります。
https://github.com/kawamataryo/vue-next-ts-webpack-preview/tree/portal-demo

Feb-04-2020 07-06-45 (1).gif

App.vue

まずPortalを定義するApp.vueの実装です。

<Portal>コンポーネントの要素としてtarget="#portal-target"を指定しています。
そして内部にv-ifで表示制御するモーダル要素を配置しています。

App.vue
<template>
  <div class="container">
    <img src="./logo.png" />
    <h1>Modal demo</h1>
    <button @click="toggleModal">toggle modal</button>
    <Portal target="#portal-target">
    <!-- 以下要素がid="portal-target"内に描画される -->
      <div v-if="isPortalShow" id="myModal" class="modal">
        <div class="modal-content">
          <span class="close" @click="toggleModal">&times;</span>
          <p>modall content</p>
        </div>
      </div>
    </Portal>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from "vue";

export default defineComponent({
  setup() {
    const isPortalShow = ref(false);

    const toggleModal = () => {
      isPortalShow.value = !isPortalShow.value;
    };
    return {
      isPortalShow,
      toggleModal
    };
  }
});
</script>

main.ts

次にAppp.vueをマウントするmain.tsです。
サンプルアプリでは、id="app"をApp.vueのマウント対象としています。

main.ts
import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')

index.html

最後にPortalの移動先となるindex.htmlです。
App.vueのマウント対象の<div id="app">、Portalのtargetとしてた<div id="portal-target">を配置します。
<div id="portal-target">自体は通常のDOM要素ですが、ここにApp.vueの<Portal>内部に指定した要素が移動して描画されます。

index.html
<doctype html>
<html lang="ja">
<head>
  <link rel="stylesheet" href="/dist/main.css">
  <meta charset="UTF-8">
  <meta name="viewport"
        content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Vue3.0 demo</title>
</head>
<body>
  <div id="app"></div>
  <div id="portal-target"></div> <!-- ここにPortal内のコンテンツが描画される -->
  <script src="/dist/main.js"></script>
</body>
</html>

終わりに

以上、Vue.js 3.0 の新機能を試す。 〜Portal 編〜 でした
z-indexや親スタイルの干渉防ぐをCSSをハックしなくて良いのは便利ですね。コードを綺麗に保てそうです。
portal-vueを使えばVue 2系の現行プロジェクトでも使えるので積極的に使っていきたいです。

参考

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

Vuetify 2.2の新機能 Presets

2020/01/02 にリリースされた Vuetify 2.2 から追加されたPresets機能がなかなかに素敵だったので、記事にしてみました。

Vuetify v2.2.0 Releas

Presets機能とは

Googleが策定するデザインガイドライン Material Design の中で作成された、Material studies をpresetのスタイルとして用意してくれる機能です。

公式ドキュメント:Vuetify Customization/Presets

下記はリリースページに掲載されているPresets機能のサンプルです。
basil
owl
shrine

Material studiesとは

Material studiesレシピアプリファイナンスアプリ教育アプリなどより具体的なアプリケーションのデザイン例と共に、そのデザインの理論的根拠コンポーネント選択について詳細に説明した、Material Designを学ぶ上で非常に有用なコンテンツとなっております。

basil_studies
rally_studies
owl_studies

要するに

Material studies のアプリのデザインを Vuetify でサクッと試せる機能

使い方

Preset は Vue CLI の plugin として実装されているので、Vue CLIを利用して追加します。
利用方法は vuetifyjs/vue-cli-plugins リポジトリにまとめられていますので、そちらも参照してください。

Vue CLIのインストール

インストール済みの方はスキップしてください。

$ npm install -g @vue/cli
# OR
$ yarn global add @vue/cli

参考:Vue CLI Getting Started

Preset Rallyを試す

# プロジェクト作成
$ vue create my-app
$ cd my-app

# vuetifyをプロジェクトに追加
$ vue add vuetify

# 任意のpresetを追加
# 今回はrallyを選択
$ vue add vuetify-preset-rally

# 起動して確認
$ yarn serve

起動すると、下記のような感じでスタイルが適用されているのがわかります。
色やフォントが変化しています。
いくつかコンポーネントのスタイルも変更されているようですが、それらは自分でコンポーネントを追加しないと分からないようです。
image.png

何が起こったのか?

vue add vuetify-preset-rally によって発生した差分は下記の通りです。

$ git status
    modified:   package.json
    modified:   public/index.html
    modified:   src/plugins/vuetify.js
    modified:   yarn.lock

public/index.html の diff は下記の通りで、fontが追加されています。

public/index.html
diff --git a/public/index.html b/public/index.html
index 2545523..ae2a70f 100644
--- a/public/index.html
+++ b/public/index.html
@@ -8,6 +8,7 @@
     <title>my-app</title>
     <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900">
     <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css">
+    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto+Condensed:300,400,500,700:100,300,400,500,700,900|Eczar:400:100,300,400,500,700,900&display=swap">
   </head>
   <body>
     <noscript>

src/plugins/vuetify.js に関しては、モジュールをimportし、vuetifyへ渡す実装が追加されていました。

src/plugins/vuetify.js
diff --git a/src/plugins/vuetify.js b/src/plugins/vuetify.js
index ec46adb..c14a53a 100644
--- a/src/plugins/vuetify.js
+++ b/src/plugins/vuetify.js
@@ -1,7 +1,9 @@
 import Vue from 'vue';
 import Vuetify from 'vuetify/lib';
+import { preset } from 'vue-cli-plugin-vuetify-preset-rally/preset'

 Vue.use(Vuetify);

 export default new Vuetify({
+  preset,
 });

ここで利用されている vue-cli-plugin-vuetify-preset-rally の実装は下記にあります。
https://github.com/vuetifyjs/vue-cli-plugins/tree/master/packages/vue-cli-plugin-vuetify-preset-rally

preset ディレクトリの中の実装を見ると、index.js によるthemeのカラーの設定や、 scsssass によるスタイルのoverrideが実装されています。
これらにより、Material studies のスタイルが Vuetify に適用される仕組みになっています。

overrideしているstyleの数はそんなに多くありませんが、Releaseページでも、

features are not supportable at this time.

と書かれているので、今後に期待です。

課題

Nuxt.jsからだとどうPresetsを追加すべきかが悩ましい
個人的にVuetifyを使う場合、セットでNuxt.jsを利用する場合が多いです。

Nuxt.jsは公式推奨の手順だと yarn create nuxt-app コマンドによる初期化手順であり、Vue CLIを利用していません。
そのため、vue add によるpluginの追加に対応していないので、Presetsをどう追加するのかが悩ましいです。

2020/02/05 追記
指定する機能は入っているようですが、いまいちうまく動作しないようです。
https://github.com/nuxt-community/vuetify-module/pull/247

指定は下記のように nuxt.config.js 中で行うと思うのですが、エラーになります。
js:nuxt.config.js
vuetify: {
preset: 'vue-cli-plugin-vuetify-preset-rally/preset'
},

まとめ

サクッとアプリを作った際に配色で悩むことがよくあるので、このような簡単に利用できるpresetがあるのは個人的に嬉しいです。
また、 Vue CLI以外でのインストール方法に関しては今後に期待です。

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

vueでscopedした親コンポーネント以下の子にstyleの影響を与えたい

結論 ::v-deep

<style scoped>
::v-deep .redText {
  color: red;
}
</style>

参考

Qitta - スコープ付きCSSで別のコンポーネントにスタイルを当てる
Vue LoaderのDeep Selectorの::v-deep

>>>/deep/をするのは知っていたんですが今では非推奨らしいですね!

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

【Nuxt.js】dayjs応用編:カウントダウンをしてみよう!

前置き

基礎編に続き応用編です。
2つの日時の差分を出していきます?
基礎編はこちら
【Nuxt.js】dayjs導入編:リアルタイムな日時を表示してみよう

https://qiita.com/aLiz/items/8de5727e274f1a9a1efe

復習

・dayjs() =パース
・format() =表示形式の指定

index.vue
<p v-text="$dayjs().format('dddd、MMMM D、YYYY h:mm A')" />

表示結果

index.vue
Tuesday、February 4、2020 1:33 PM

dddd MMMM D ……長い!?笑
省略したい場合はLLLLで同じ表記に。
この辺りも基礎編で解説済みです。
https://github.com/iamkun/dayjs/blob/dev/docs/en/I18n.md

・locale('ja')を指定で日本語表記

index.vue
<p v-text="$dayjs().locale('ja').format('dddd、MMMM D、YYYY h:mm A')" />

表示結果

index.vue
2020年2月4日 火曜日 13:33

❓ではこれは
※グローバルでlocale('ja')指定済み

index.vue
<p v-text="$dayjs('February 4, 2020').add(1, 'year').format('YYYY')" />

表示結果

index.vue
2021

ですね?
dayjsで2020年2月4日をパース(解析)
そこに1年追加し2021年2月4日とし
表示形式を年数字のみにしているためですね?

カウントダウン

では基礎編を踏まえて
2つの日時の差分を出しましょう!
あと何ヶ月、とかあと何日!
差分はditt()を使用します。

index.vue
<p v-text="$dayjs('2020-02-04').diff('2020-01-01')" />

結果はミリ秒で表示

index.vue
2937600000

ミリ秒で見てもパッとしないので
月の差分を見てみましょう〜??

index.vue
<p v-text="$dayjs('2020-02-04').diff('2020-01-01', 'month') + '月'" />

結果は1ヶ月ですね。
diffの第二引数がformatのような役割です。

index.vue
1月

日付

index.vue
<p v-text="$dayjs('2020-02-04').diff('2020-02-01', 'day') + '日'" />

結果

index.vue
3日

時間

index.vue
<p v-text="$dayjs('2020-02-04T12:30:00').diff('2020-02-04T10:30:00', 'h') + '時間'" />

結果

index.vue
2時間

基礎が分かれば簡単ですね?

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

https://twitter.com/aLizlab

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

laravel + vue.jsでSPAの開発をしていてデバックがしたい!!!

結論
SPAで変数とかの中身を見るには
laravel debugarとclock workの両方をcomposerでインストールする必要あり。

STEP1 laravel debugarのインストール

これはいつも通りですね
こちらの↓READ MEを参考に開発環境にインストール
https://github.com/barryvdh/laravel-debugbar

composer require barryvdh/laravel-debugbar --dev

STEP2 clock workのインストール

これはSPAだとHTMLに返ってこないデバックの結果をChromeのデベロッパーツールで確認できるようにするツールです!
google chrome自体へのインストールとcomposerへの両方のインストールが必要

これも以下を参照!

https://github.com/itsgoingd/clockwork

  • google chromeへのインストール

https://chrome.google.com/webstore/detail/clockwork/dmggabnehkmmfmdffgajcflpdjlnoemp

  • composerへのインストール
 composer require itsgoingd/clockwork

用法

インストールするとclockヘルパメソッドが使えるようになります!
使い方は以下のように変数の中身を確認したり、メソッドを組み合わせて使うことができる!

image.png

これも詳しい使い方は公式ドキュメント↓参照
https://underground.works/clockwork/logging?#content

すると、、、
image.png

めちゃエラー出てるの恥ずかしいけどこんな感じで出ます!!!!!

デバックできずに困ってた人は参考にしてみてください~

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

laravel + vue.jsでSPAの開発をしていてデバッグがしたい!!!

SPAだとlaravelのエラー内容や変数とかの中身をブラウザに返すことができないけどデバッグがしたい!人用

結論
そのためには、、、
laravel debugbarとclock workの両方をcomposerでインストールする必要あり。

STEP1 laravel debugbarのインストール

これはいつも通りですね
こちらの↓READ MEを参考に開発環境にインストール
https://github.com/barryvdh/laravel-debugbar

composer require barryvdh/laravel-debugbar --dev

STEP2 clock workのインストール

これはSPAだとHTMLに返ってこないデバッグの結果をChromeのデベロッパーツールで確認できるようにするツールです!
google chrome自体へのインストールとcomposerへの両方のインストールが必要

これも以下を参照!

https://github.com/itsgoingd/clockwork

  • google chromeへのインストール

https://chrome.google.com/webstore/detail/clockwork/dmggabnehkmmfmdffgajcflpdjlnoemp

  • composerへのインストール
 composer require itsgoingd/clockwork

用法

インストールするとclockヘルパメソッドが使えるようになります!
使い方は以下のように変数の中身を確認したり、メソッドを組み合わせて使うことができる!

image.png

これも詳しい使い方は公式ドキュメント↓参照
https://underground.works/clockwork/logging?#content

また.envに以下をの設定を追加するとlaravel dubagbarが画面に表示されなくなります!

DEBUGBAR_ENABLED=false

すると、、、
image.png

めちゃエラー出てるの恥ずかしいけどこんな感じで出ます!!!!!

デバッグできずに困ってた人は参考にしてみてください~

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

FirebaseAuthで同時ログイン(二重ログイン)の禁止を実装してみる

はじめに

FirebaseAuthを利用してWebサービスを作成している際
同一アカウントへの同時ログイン(二重ログイン)を制限したい要望がありました。

テスト的に(少々強引に)実装してみた手法についてのメモです

記事の対象の環境について

サーバサイド処理はcloud functionsにて
クライアントサイドはVue.js + firebaseにて構築しています。

方針

revokerefreshtokensを呼び出して、ログインの処理を行うときに過去にログインしていた状態を破棄するようにしてみる

サーバサイド実装

const admin = require('firebase-admin');
admin.initializeApp();
export const func1 = functions.region('asia-northeast1').https.onCall(async (data, context) => {
    const uid = data.uid

    await admin.auth().revokeRefreshTokens(uid)
    return 'OK'
});

クライアント実装

<template>
<div>
<input type="email" v-model="email"/>
<input type="password" v-model="password"/>
<button @click="login">login</button>
</div>
</template>

<script>
import firebase from "firebase";

async function sleep(sec) {
  return new Promise(resolve => setTimeout(resolve, sec * 1000));
}

export default {
  name: "SignIn",
  data: [
    email: "",
    password: ""
  },
  methods: {
    async login() {
      try {
        // ログイン処理1回目
        const result = await firebase
          .auth()
          .signInWithEmailAndPassword(this.email, this.password);

          // cloud functionに定義したfunc1を呼び出す
          const func1 = firebase.app().functions("asia-northeast1").httpsCallable('func1');

          await func1(result.user.uid);

      // @todo クラウドファンクションの処理を待つためにsleepさせている
          await sleep(2); 

          // ログイン処理2回目
          const result2 = await firebase
            .auth()
            .signInWithEmailAndPassword(this.email, this.password);

          this.$router.push('/private/space');
        }
      } catch (e) {
        alert("ログインに失敗しました");
      }
    }
  }

最後に

これらの方法でとりあえずの同時ログインの禁止(以前にログインしたセッションは強制ログアウトする)を実装することはできました。
sleepさせている処理など、タイミングによって問題になりそうな処理ではあるので
なにかいい方法があればいいのですが。

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

Intellijでvueを扱う時の初期設定

Intellijでvueを使う時、設定方法をいつも忘れるので、備忘録として残しておく。

ESLint

vue create appで

? Pick a linter / formatter config: 
  ESLint with error prevention only 
  ESLint + Airbnb config 
  ESLint + Standard config 
❯ ESLint + Prettier 
  TSLint (deprecated) 
? Pick additional lint features: (Press <space> to select, <a> to toggle all, <i> to invert selection)
❯◉ Lint on save
 ◯ Lint and fix on commit

これで保存時にLintが効くようになる。(誤った構文を記載するとwarningで教えてくれる

Prettierの設定

npm add --dev prettier eslint-config-prettier eslint-plugin-prettier
.eslintrc.js
module.exports = {
  root: true,
  env: {
    node: true
  },
  extends: [
    'plugin:vue/essential',
    'eslint:recommended',
    '@vue/typescript',
    'plugin:prettier/recommended'
  ],
  rules: {
    'prettier/prettier': ['error', { singleQuote: true, semi: false }],
    'vue/mustache-interpolation-spacing': ['error', 'always' | 'never'], // マスタッシュ構文は空白をいれる
    'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
    'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
  },
  parser: 'vue-eslint-parser',
  parserOptions: {
    parser: '@typescript-eslint/parser'
  },
  overrides: [
    {
      files: ['**/__tests__/*.{j,t}s?(x)'],
      env: {
        mocha: true
      }
    }
  ]
}

File Watchersを以下のように設定
image.png
これで自動的に誤った構文を正してくれる(超便利!
(※intellijのcode style設定と競合する場合がある時は、適宜、warningを除外する設定を追加する。今回の設定ではセミコロンはつけないというESlintを設定している。「Unterminated statement」でwarningが出たが、これを除外設定した。

実行設定

Run/Debug Configrationsでnpmを選択し、package.jsonにpathを、scriptにserveを設定することで実行が行えるようになる。
image.png

vuetifyの導入

vue add vuetify

typescriptの場合エラーが出るので以下で修正

tsconfig.json
...
    "sourceMap": true,
    "baseUrl": ".",
    "types": [
      "webpack-env",
      "mocha",
      "chai",
      "vuetify" #追加
    ],
    "paths": {
...

これでエラーなくvuetifyを起動できる
image.png

おわりに

なんか良い設定あったら随時更新していく!

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

Vueの子コンポーネントでpropsの変更を検知する

parent.vue
<template>
  <childComponent name="foo">
</template>
// 略
child.vue
<template>
  <div>{{ awesomeName }}</div>
</template>
<script>
export default {
  props: {
    name: String
  },
  data () {
    return {
      awesomeName: this.name
    }
  }
</script>

こういう時、親から渡す値が変更された際に検知するにはpropsをwatchで監視する。

child.vue
// 略
<script>
export default {
// 略
    watch {
      name (newName) {
        this.awesomeName = newName;
      }
    }
// 略
</script>

おういえ?

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

初心者がスクールに通わずVueとFirebaseだけでYouTube同期再生サービスを開発した

概要

今回はプログラミング経験がほぼ皆無の私でも1ヶ月程度の勉強で、FirebaseとVueを使ってサービスを公開するところまで行けましたので、開発の動機や意識したところ、苦労したところ、勉強法やモチベーションの上げ方についてもサービスの紹介とともにお教えしたいと思います。

サービス紹介

まずはサービスの紹介をさせていただきます。

今回私が作成したサービスは、YouTube同期再生プラットフォーム DJ7 です!

https://www.dj7.io

dj7pc

dj7pc

初回のアクセスでは音がならないように設定してありますのでご安心ください。

DJ7では、お友達や恋人など複数人の方々と一緒にYouTubeの動画を楽しむことを目的にしています。
もちろん、お一人でもお使いいただけます。(かく言う私も友達が少ないので普段は一人で使ってます?)

また、DJ7は無料でお使いいただけます!
(Firebaseは無料ではないのでちょっと苦しいですが笑)

ひとえに同期再生と言ってもいろいろな使い方ができます。
好きなYouTuberの動画を一緒に楽しむ、作業するときに同じ音楽をみんなで聞く。
私が知らないだけで、これよりももっと多くの使い方があると思っています。
ぜひ、みなさん好き好きの使い方をしてみてください?

また、GoogleかTwitterのアカウントを使ってサービスにログインしていただくと、以下の機能が使うことができます。

  • 誰かが作成した部屋に移動する
  • 部屋の作成
  • ログインされている場合、移動先の部屋が存在しない場合は新たに作成されるようになっています。
  • 再生履歴の利用

逆に、ログインしない状態ではトップページに表示されている部屋しか利用できません。
自分たちだけの部屋が欲しい場合、ぜひログインお願いします。

現在は機能も少なく、動画サイトもYouTubeのみの対応ですが、今後機能拡充に努めて参りますので、
ぜひDJ7をよろしくおねがいします!!

なにかご意見・ご要望があればTwitter @imataka7 または @dj7app までご連絡お願いします?

開発動機

さて、前置きが長くなってしまいましたが次は開発の動機について書いていきたいと思います。

私は普段からDiscordでMusic Botを使って知り合いと一緒に通話しながら音楽を聞いたりしていました。
しかし、機能にはずっと不満を持っていました・・・
曲が1つずつしか入れられない、長い曲を入れたときにそれに割り込んで曲を入れられない(割り込んで曲をいれて、その曲が終わったらもと流れていた曲が途中からはじまるみたいなことがしたかった)、そもそも導入がめんどう、音質がわるい、、、など枚挙にいとまがありません。

なら、自分で満足行くものを作ればいいのではないかと思い、今回のアイデアが生まれました。

今年は自分の中の目標として月に一つはアイデアを形にするというものがあり、本当はほかにも色々アイデアがありましたが、以下の理由でDJ7に決めました。

  • 数あるアイデアのなかで自分が一番欲しかったから
  • 3日程度で簡単なデモを作り、知り合いに見せたところウケがよく、自分以外にも使いたいと感じるユーザの存在を知り、このアイデアに需要があることを確信したから

また、開発に着手する前に競合の調査も行いました。
動画の同期再生ができるサイトは他にもあったのですが、私が本当に欲しかった機能を備えているものはなく、それを実装したいというのも一つのモチベーションになりました。
DJ7で特に欲しかった機能は以下のとおりです。

  • Webで使える
  • 音楽の同時鑑賞・動画の同時視聴ができる
    • ここまでは競合のサイトにもある機能です
  • URL直打ち以外にもYouTubeで検索して曲を追加する
  • シークバーの状態を共有する
  • 再生待ちリストの順番入れ替え
  • 割り込み
  • 再生履歴
  • ダークモード対応
    • 最初からダークモードみたいな色にするのではなくてちゃんと切り替えられるように
  • PWA (PCやスマホにインストールできてアプリのように振る舞うサイト)

これらを実現したいという思いのもとに今回の開発に着手しました。

技術スタック

dj7pc

今回の構成はこのような感じです。フロントエンドはVueで作り、バックエンドにはFirebaseを採用しました。

それぞれの採用意図を説明します。

Vue

ほかにReactなども候補に上がりましたが、もともとHTMLやCSSを勉強していた私にとってJSXよりもVueのほうが書きやすかったこと、日本語のドキュメントやQiitaの記事も充実しているかつ、初心者にもわかりやすかった、といった点で採用しました。

Firebase

今回のサービスの肝です。Firestoreの通知機能を使って今回の同期再生は実現しています。これがなかったらDJ7は成り立たないと言っても過言ではないです。

TypeScript

TSとclass-style-componentを採用しました。JavaScriptでも良いですが、型による補完機能やコーディングミス防止などの恩恵が受けられたほうが生産性が高まると考えたからです。

SCSS

単純にCSSより便利だからです。

GitHub Actions

今回はGitHubにpushしたときにFirebaseに自動でデプロイするために使っていました。

CSSのフレームワーク

今回は使いませんでした。CSSを自分で書いてもっと機能や使い方を覚えたいと思ったからです。本当に生産性を高めたければ使うべきです。(ただ、フレームワーク使ってるぽさはなくしたいという個人的なエゴはありました笑)

PWA

いまの時代Webアプリを作成するならば、これに対応するのは必須だと勝手に思っています。
Webの技術さえ使えればマルチプラットフォームのアプリが簡単に作成できるのですごいですよね。
ちなみに競合のサイトの中にはPWAに対応しているものはありませんでした 。

開発日程と体制について

今回は1ヶ月で公開まで持っていくということを事前に決めていましたので、公開するためには何が必要で、何がいらないかといったことは常に意識しながら取り組みました。そのために、実際に知り合いに1ユーザとして使ってもらい意見を聞きながら開発をすすめることにより、要不要の線引を自分の中で明確にさせました。

自分一人で作りたいものを作るのも結構ですが、私の場合いろんな人に使ってもらいたいと考えていたので、すぐにユーザの意見を聞ける環境に身を置けたのはとてもありがたかったです。

また、時間の使い方ですが、まず3日目くらいで同期再生の骨組みを作り、簡単に動くデモを作成しました。先程も書きましたが、このとき知り合いに見せてポジティブな意見をもらえたのが本格的に開発を進めていく強いきっかけになりました。

その後、1~2週間でロジックの部分を作りました。具体的に言うと、検索、曲の追加、部屋の作成、履歴など現在DJ7にある基本的な機能の実装をしていました。他にもGitHub Actionsの設定などもやっていました。
また、欲しい機能はここまでに実装すると決めていました。

3~4週目はサイトの見た目を決めていました。逆に言えば最初の2週間はほぼCSSの記述はせずにロジックの実装のみに焦点をあてていました。現在のサイトのレイアウトを作成やダークモード対応、レスポンシブ対応はこの週で行いました。

5週目はトップページの実装やmanifest.jsonの設定、ドメインの取得などロジックと見た目以外にサービスに必要な部分を設定したり、アイコンを作成したりしました。つい先週のことです。

苦労した点

先程も書いたとおり、一番基礎となる部分は最初の3日程度でできていましたので、今回の場合では実装自体に苦労することはあまりありませんでした。

なので、苦労した・悩んだ点としては、実装ではなく何があればサービスとして成り立つのか、どんなUI/UXならば使ってもらえるか、使っていて楽しいかでした。

こればかりは一人で悩んでいても答えは出てこないと感じ、知り合いに使ってもらい、フィードバックをもらうことで少しずつ改善点や必要なことを洗い出していくことによってサービスとして形になっていきました。

何度も何度もPDCAサイクルを回し、改善点については1ヶ月の間で50箇所以上はあったと思います。

とは言え、まだ自分で満足ができるものが作れたとは思っていないです。
まだまだ自分でも使っていて、欲しいと感じる機能がたくさんあります。

これからも開発を続けることによって、自分のサービスを良くしていき少しでも満足ができる出来にしていきたいと思っています。

また、他に悩んだ点としては収益化についてです。これは個人的な意見なんですが、サービスはユーザに対してなにか価値を提供する以外にも、自分自身に対して収益の目処が立っていて初めてサービスと呼べると思っています。それ以外はただのツール止まりか自己満足です。

なので、DJ7もどのように自分のアイデアをサービスに仕立て上げていくか悩みました。サブスクリプションにして利用できる機能に差をつけるか、広告にするか。
結論から言えばDJ7は広告を収益のモデルにしようと考えています。

サブスクリプションを採用しなかった理由ですが、私の実力がまだユーザの方々に課金していただけるにふさわしい機能や体験を提供できるレベルに至っていないことと、すべての機能を無料で使っていただくことによって、より多くの意見をいただきやすくなると考えたからです。

と言いつつも、本当に収益を上げられるかは未知数ですが・・・

勉強法とモチベーションの上げ方

まず勉強法についてですが、基本的なVueやFirebaseなどの機能については公式のドキュメントをひたすら読んでいました。特にFirestoreのドキュメントはじっくり読み込みました。これについてはQiitaなどの2次情報を見るよりも良いと感じました。
もちろん、公式のドキュメントに載ってないようなことについてはQiitaやStack Overflowが役に立ちました。

また、モチベーションの上げ方、というより保ち方ですが、私は自分が作っているものが少しずつサービスとしての体をなしていく過程を見るのがモチベーションになっていました。また、私の場合はすぐ近くに使ってくれるユーザがいたので新しく機能を実装したとき、サービスを良くしたときにリアクションをくれたことや、意見をもらえたときは新しい目標が見つかった気持ちになり、そういったこともモチベーションに繋がりました。

誰かを巻き込むことが一番モチベーションに繋がると思っています。まわりに巻き込める人がいなければSNSなども活用するのも手ではないでしょうか。せっかく始めたことなら一度は完成まで持っていきたいですよね。
もし一度始めたことをやめるのであれば、そのアイデアを私に譲ることを考慮していただけませんか?

SairiLabサロンについて

SairiLabサロンとはプログラミング初心者が集まるDiscordのサーバです。私も本当にモチベーションが落ちてしまったときは私も覗きに行ってました笑
まだ人数は少ないですが、和気あいあいとしていてとても雰囲気の良いサロンです。
新人エンジニア同士で繋がりたければぜひ来ませんか?
https://discord.gg/nTA2CcQ

少し長くなってしまいましたが、最後まで読んでいただきありがとうございました!

最後にもう一度だけサービスの紹介をして終わりたいと思います!

DJ7は、YouTubeの同期再生プラットフォームです!
YouTubeのURLか検索ワードをいれてQueueボタンを押すだけで、みんなで同じ動画が再生されます。

ご意見・ご要望があればTwitter @imataka7 または @dj7app までお願いします!

それでは、失礼します?

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

AtomicDesignでAtomsの設計を失敗した話

概要

今回は、AtomicDesignでのコンポーネント設計をした行った時にやらかしたミスを記事にしてみました。

AtomicDesignって

最初にAtomicDesignとはなんなのかを軽く振り返ってみましょう。
Qiita内外でも多くの説明がなされているので、ざっくりと。

AtomicDesignとは、コンポーネントを以下の5つの階層に分け、
コンポーネントの役割や責任を明確しそれらをルールを決めて分割していこう、という概念です。

  • Atoms(原子)
  • Molecules(分子)
  • Organisms(有機体)
  • Templates
  • Pages

上が最小のコンポーネントで、下が一番大きいコンポーネントとなります。
また、AtomicDesignのルールとして、

  • Atoms
    • これ以上機能として分割できない機能達
  • Molecules
    • Atomsの集合体
  • Organisms
    • AtomsやMoleculesの集合体で、複雑な構成
  • Templates
    • MoleculesやOrganismsを配置する設計図
  • Pages
    • Templatesに対してデータを反映した物

といった感じですね。
それぞれの役割が明確化されていて、コンポーネントをどう作っていけばいいかの方針がわかりやすくなってますね。
それでもやらかした自分とは一体

Atomsをつくってみた

まずは、設計がマズい形のAtomsを見てみましょう
今回はButton要素のコンポーネントを例に使います。

button
<template>
  <button @click="onClick">
    <slot></slot>
  </button>
</template>

<script>
export default {
  methods: {
    onClick() {
      return this.$emit("click");
    }
  }
};
</script>

<style lang="scss" scoped>
.Base__Button {
  &--green {
    //省略
  }
  &--red {
    //省略
  }
  &--yellow {
    //省略
  }
}
</style>

HTML要素とScript要素は至ってシンプルな構成です。
CSS要素に関しては、1つのファイルの中に複数のClass属性が記述する形で作ってみました。
このComponentではclass属性の指定をせずに、Componentを配置した親側でClass属性を指定するという方法をとりました。
呼び出す側の例も書いておきます。

Parent
<base-button class="Base__Button--green">HOGE</base-button>

親でClass属性を指定してる時点でとっても嫌な予感がするぞ

が、だめ!

最初はこの設計でも問題なく使えてましたが、使用するClass属性が増えて、記述が多くなってくると、
当然ながら使いづらくなってきました。

というのも、新しいスタイルが必要となった場合も同じファイルに追加していくために、
アイコンに使用する目的の無色透明で小さめのスタイルを…。
次はタブ用のスタイルを…
次はgreenだけ、ボタンのサイズを色々作りたい
次は…

となり、気づいたらClass属性の記述だけでファットになって、しかも複雑に絡み合い濃厚な味に…。

▂▅▇█▓▒░(‘ω’)░▒▓█▇▅▂ うわあああああああああ

Badな設計

どのような点がBadであったかを確認してみましょう。

  • 親と密結合
    • 親でクラス名を指定しないと見た目を固定できない
    • 下手したら親にスタイルのコードを書いている
  • ComponentのCSSがファット
    • 1つのComponentに色んなスタイルを詰め込みすぎ
  • CSSが上書きなどでスパゲッティコード化
  • Atomsの外でClass属性/CSS属性を記述
    • コンポーネントの状態を閉じ込めれてない

基礎的な注意点ばかりではありますね…。
なぜ、このような設計のComponentを作ってしまったかも反省しておきましょう。

  • 同じようなComponentを作るのはNG
    • 1つのComponentで管理やったら楽じゃん!
  • Atomsのルール通り処理の機能は分割しつつスタイルはまとめたい
  • 色んな箇所で使うなら同じComponentのほうがいいのでは
  • でも色んなデザインにしたいけど細かく制御できないから親にCSS属性を書くしか無い

というのを考えて作りやらかしてしまいました。
特に最後のは、もはやAtomsを親からスタイルを上書きしていたので、
これ普通のButtonタグでもよくない?のとこれ疎結合になっていないよね?という、色々なルール違反な状態となっていました。

そうだ、いっぱいつくればいいんだ

では、どうすればいいんだ!と悩み、色々ググって情報を漁っていたところ気づきました。
色んな人のLT資料や、記事を見ていっそのこと振り切って考えていいのかもと思ってすえたどり着いた答えが、
そうだ、必要な分のAtomsをいっぱいつくればいいんだ

HTML要素やJavascriptと同じように、CSSも見た目毎に最小単位まで絞り込んだComponentを作ってしまえばいいのだと。
方針としては、「Importしたら見た目も完成していたそのまま使える」ぐらいの疎結合と独立感で。
早速、先程のButtonを作り直してみました。
ImportしてるComponentが多くてパス修正が大変だった

BaseButtonGreen.vue
BaseButtonRed.vue
BaseButtonYellow.vue
BaseButtonRadius.vue
BaseButtonSemiRadius.vue

分解し、それぞれ作ってみました。
中身の方は、試しにBaseButtonGreen.vueを見てみましょう。

BaseButtonGreen.vue
<template>
  <button @click="onClick" class="Base__Button--green">
    <slot></slot>
  </button>
</template>

<script>
export default {
  methods: {
    onClick() {
      return this.$emit("click");
    }
  }
};
</script>

<style lang="scss" scoped>
.Base__Button--green {
  //省略
}
</style>

以前のComponentと違うのは、class属性をここで指定しCSSでは他のclass属性を記述していない、というところです。
BaseButtonGreen.vueは名前で表すように緑色のボタンに関係するCSS属性だけを記述し、
BaseButtonRed.vueは、赤色のボタンを。
BaseButtonRadius.vueでは、円形に関係するCSS属性だけを。
こうすることで、親とは疎結合になり独立性が高いComponentとなり、Importしそのまま使える事ができます。

差分がある場合には?

BaseButtonGreen.vueの縦横幅を大きい/小さいのを使いたいとなった場合ですが、
この場合はBaseButtonGreen.vueの中に差分を書くのではなく、いっそのこと別Componentにしましょう。

BaseButtonGreen.vue
BaseButtonGreenLarge.vue
BaseButtonGreenSmall.vue

ファイル名に修飾子で「Large」か「Small」などを付け加えて、違いを明確化。
このように、同じ構成や見た目だけど細部が微妙に違うAtomsを作る場合には、ファイル名に修飾子をつけました。
サイズだけではなく、hoverなどの擬似クラスでちょっとリッチなエフェクトを付けたいけど、
エフェクトがないのも必要という場合には、Componentを2つに分割する事で変更内容を1ファイルに閉じ込める事ができます。

おわり

今回はAtomicDesignで私がやらかした勘違いと失敗を、どのようにルールを見直した修正したかを記事にしてみました。
割とAtomicDesignの解説記事では、Moleculesからの作り方が多く書かれていますが、
Atomsでどうやって作るの?て記事はなかなか見ないので今回ネタにしてみました。
(ついでに自分犯した失敗の反省も込めて)

実際の現場ではAtomsは作らず、VuetifyなどのComponentFrameworkを使う事も多いと思われますが、
いざ自分でつくろうとすると案外どうやるんだったかな?とか、これでよかったよね?となりやすい部分であるので、
しっかりルール作りや方針確認をしたほうがいいですね!

また、アプリやサイトのデザインにがっつり影響する部分でもあるので
やっぱり自分で作ってると楽しいし、これからもたくさんAtomsを作っていこうかなと確信しました。
でも作りすぎるとImportがめんどくさいので、まとめて読み込む処理を組み入れなくちゃ…(確信)

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