20190909のvue.jsに関する記事は10件です。

ASP.NET Core でSPAではなくVue.jsを利用してみる(4)

クライアント側にVue.jsをSPA(シングルページアプリケーション)ではない方法で利用することについて記述していきます。

全体
 ・Vue.jsとblumaの設定
 ・cssをblumaに変更
 ・バリデーションの変更
 ・Vue.jsでのコンポーネントの利用(本稿)

-Vue.jsでのコンポーネントの利用-

環境

「ASp.Net Core でPostgreSQLを利用してIdentityで認証を使えるようにする」で作成した環境にVue.jsを組み込み、cssフレームワークをblumaに変更します。
- VisualStudio2019 Ver.16.2.3
- ASP.NET Core 2.2
- PostgreSQL 9.6 インストール済み、接続用のアカウント作成済み
- EntityFramework
- Vue.js 2.6.10
- bluma 0.7.5

Vue.jsを使う理由

 すいません、タイトルに偽りありでした。ようやくVue.jsの記述にたどり着きました。本当はRazorPageでクライアントコンポーネントにVue.jsが使いたかっただけなのですが。
 事の発端はJQuery使いにくい、クライアント動作の為のHTMLはサーバーサイドで作っているのでソースが分かれてしまうのがいや、RazorPageの利点を生かしたままクライアント側のコンポーネントを再利用可能な状態で整理して作りたいという思いから、どうせやるなら.Net Core、それならLinuxで動かそう、それならDBもMySQLかPostgresだよねー、JQueryやめるならBootstrapも変えちゃえと思ってたらこんなことに...。

作るもの

 そんなに大きなものは作れないので、ボタンを押すとページをロックしてページが変わるか一定時間ロック状態にするボタンを作ります。

下ごしらえ

 とりあえず、画面ロックのJavaScriptとして、「wwwroot/js/utilities.js」を作り「_Layout.cshtml」のどこかで作ったスクリプトを読み込ませておきます。

utilities.js
// スクリーンロック
function lockScreen(message, timeout) {

    // ロック用のdivを生成
    var lockElement = document.createElement('div');
    lockElement.id = "screenLock";
    var textElement = document.createElement('div');
    textElement.innerText = message;
    lockElement.appendChild(textElement);

    // ロック用のスタイル
    lockElement.style.display = 'table';
    lockElement.style.height = '100%';
    lockElement.style.left = '0px';
    lockElement.style.position = 'fixed';
    lockElement.style.top = '0px';
    lockElement.style.width = '100%';
    lockElement.style.zIndex = '9999';
    lockElement.style.opacity = '0.9';
    lockElement.style.background = "white";

    textElement.style.display = 'table-cell';
    textElement.style.textAlign = 'center';
    textElement.style.verticalAlign = 'middle';
    textElement.style.margin = 'auto';
    textElement.style.fontSize = 'x-large';

    // ロックの追加
    var objBody = document.getElementsByTagName("body").item(0);
    objBody.appendChild(lockElement);

    if (timeout) {
        // タイムアウトが設定されていれば、そのmsec後にロック解除
        setTimeout(function () {
            // ロック画面の削除
            unlockScreen();
        }, timeout);
    }
    else {
        // タイムアウトが設定されていなければ、30秒後に解除
        setTimeout(function () {
            unlockScreen();
        }, 30000);
    }
}

// スクリーンロック解除
function unlockScreen() {
    var lockElement = document.getElementById('screenLock');
    var dom_obj_parent = lockElement.parentNode;
    dom_obj_parent.removeChild(lockElement);
}

 見ての通り、画面全体を覆う「div」を作っているだけです。画面が切り替わる場合は解除不要ですがページを更新しない場合は「unlockScreen」を呼び出してロックを解除した方がいいです。(タイムアウトするはずですが、それはちっと...)

コンポーネントの作成

 「wwwroot/vue/compornents」フォルダを作成し、その下に「button-with-lock.js」を以下のように作成します。

button-with-lock.js
Vue.component('button-with-lock', {
    props: {
        label: String,
        lockmessage: String
    },
    template: '<button type="submit" class="button is-primary is-outlined" v-on:click="lockOnClick">{{label}}</button>',
    methods: {
        lockOnClick: function () {
            lockScreen(this.lockmessage);
        }
    }
});

 最初の「button-with-lock」はコンポーネント名で、使う場合にはタグ名となります。
 「props」はタグ内の属性として外部から設定できるものです。
 「template」は、挿入されるHTMLにvue独自の味付けをしたものです。ここでは「v-on:click」でonclickイベントで後述の「methods」の「lockOnClick」を実行させます。また、propで設定した「label」に入っている文字列をボタンのテキストとなるように「{{label}}」(二つの中かっこでくくる)と、その内容に置き換わります。
 最後の「methosds」はスクリプトを記述しています。ここでは下ごしらえで作成した「lockScreen」に引数として「prop」に設定されている「lockmessage」を渡しています。

 なお、ES6のみを対象とする場合、「template」ではテンプレート文字列(「`」でくくり、改行も利用できる文字列)のほうが書きやすいです。IE11もサポートしたいので文字列にしていますので、改行を使いたい場合は行末に「\」を入れる必要があります。

コンポーネントンの利用

 作成したコンポーネントをログイン画面で利用してみます。
 「Areas/identity/Pages/Account/Login.schtml」の記述を以下のようにします。

Login.schtml
@page
@model LoginModel

@{
    ViewData["Title"] = "Log in";
}
<script src="~/vue/compornents/button-with-lock.js" asp-append-version="true"></script>

@*vueのターゲットになるようにidを設定する*@
<div class="section" id="vueTarget">

    (フォームの前半は省略...)

                <div class="field has-text-centered">
                    @*ボタンをコンポーネントに置き換える*@
                    @*<button type="submit" class="button is-primary is-outlined">ログイン</button>*@
                    <button-with-lock label="ログイン" lockmessage="ログイン中..."></button-with-lock>
                </div>
            </div>
        </div>
    </form>
</div>

<script>
    //vueを利用する
    var vm = new Vue({
    el: '#vueTarget',
    });
</script>

 最初に作ったコンポーネントを読み込んでいます。(多分後のvueインスタンス作成)直前でも大丈夫)
 全体の「div」の「id」を「vueTarget」としました。これはvueを使う目印になります。(名前は何でもいいです)
 最後のほうのログインボタンを作ったコンポーネントで置き換えています。このように「props」に設定している値を属性として指定できます。
 最後にvueインスタンスを作成すると、コンポーネントが実装されます。この時「el」で対象となる「div」の「id」を「vueTarget」指定したことで、この「div」内が対象となります。(つまり作ったコンポーネント「button-with-lock」が設定した「div」の範囲外なら何もしないということです。)
 これでデバッグを実行すると、ボタンをクリックするとロックされます。(見やすいように、サーバーの「login」の実装に数秒の待ち時間を入れたほうがわかりやすいです)

RazorPageのモデルと関連付ける

 上記のボタンはそれなりに使えると思います。
 ただ、実際には作ったコンポーネントにRazorPageのモデルから値を連携させたい場面があるので、ここではとりあえずロックメッセージをモデルから取得するように変更します。(実際にはすることはないと思う)

 まずはビューモデルである「Areas/identity/Pages/Account/Login.cshtml.cs」にロックメッセージのプロパティを追加します。

Login.schtml.cs
前の方は省略...

        [BindProperty]
        public InputModel Input { get; set; }

        public IList<AuthenticationScheme> ExternalLogins { get; set; }

        public string ReturnUrl { get; set; }

        // ログイン中のメッセージを追加
        public string TestLockMessage { get; set; } = "ろぐいんちゅうでっせー";

この後ろも省略

 ログイン中のメッセージとして「TestLockMessage」を追加しています。

 次にモデルをvueインスタンスの作成時に「data」で与えるのですが、この時ひと工夫が必要でした。(結構調べるのに手間取った)。
 ビューモデルを@Json.Serialize(Model)でシリアライズして渡そうとしたのですが、単純なビューモデルではなく「PageModel」を継承しているためにこのクラスのメンバが大きいうえに循環参照していたりしてエラーになってしまいます。循環参照は無視させると、時間がかかりすぎて帰ってきませんでした。何をしようとしているのやら...。
 調べていくと「Json.Serialize」の第2引数で与える「JsonSerializerSettings」の「ContractResolver」のメソッド「GetSerializableMembers」をオーバーライドしてシリアライズする対象を選別すればいいことがわかりました。「Utility」フォルダを作って「JsonConverterExceptPageModel.cs」を以下のように作成しました。

JsonConverterExceptPageModel.cs
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Reflection;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace WebApplication1.Utility
{
    public static class JsonConverterExceptPageModel
    {
        static DefaultContractResolver resolver = new DefaultContractResolver();

        static public Microsoft.AspNetCore.Html.IHtmlContent Serialize(IJsonHelper jsonHelper, object value)
        {
            JsonSerializerSettings settings = new JsonSerializerSettings();
            NotInhelitContractResolver resolver = new NotInhelitContractResolver();
            settings.ContractResolver = resolver;

            return  jsonHelper.Serialize(value, settings);
        }

    }

    public class NotInhelitContractResolver : DefaultContractResolver
    {
        protected override List<MemberInfo> GetSerializableMembers(Type objectType)
        {
            Type pageModel = typeof(PageModel);

            List<MemberInfo> list = new List<MemberInfo>();
            foreach (var member in base.GetSerializableMembers(objectType))
            {
                if (member.DeclaringType != pageModel) list.Add(member);
            }

            return list;
        }
    }
}

 「GetSerializableMembers」のところで「PageModel」のメンバを除外するようにしています。使う環境によってはもう少し工夫がいるかもしれません。

 で、「Areas/identity/Pages/Account/Login.schtml」」を以下のように変更します。

Login.schtml
@page
@model LoginModel
@using WebApplication1.Utility   @*これを追加した*@

中略...

                <div class="field has-text-centered">
                    @*ボタンをコンポーネントに置き換える*@
                    @*<button type="submit" class="button is-primary is-outlined">ログイン</button>*@
                    <button-with-lock label="ログイン" v-bind:lockmessage="items.TestLockMessage"></button-with-lock>
                </div>
            </div>
        </div>
    </form>
</div>

<script>
    //vuew利用する
    var vm = new Vue({
    el: '#vueTarget',
    data: {  @*これを追加した*@
        items: @JsonConverterExceptPageModel.Serialize(Json, Model)
    }
    });
</script>

 作成したシリアライズ用のクラスを利用する為に、最初に「@using WebApplication1.Utility」を追加。
 ログインボタン「lockmessage」プロパティーは「vue」の「data」の「item.TestLockMessage」を参照させています。参照させる場合はこのように、「v-bind:prop名」の形になります。

 これで実行すればロックメッセージが関西弁に変わります。

 これで当初の目的であったRazorPageの利点も生かしながらクライアントサイドのコンポーネントをvue.jsで記述することができるようになったので、ひとまずこのシリーズは終わりかな。
 まあ、これを使って使えそうなコンポーネントができればどこかで公開するかもしません。

 C#.Net coreもvueも本格的には使っていないので、つたない部分があるかと思います。
 もっとすっきり記述する方法もあるかと思いますので、こうした方がいいのではとかありましたらご指摘ください。

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

Laravel Nuxt StripeでCheckout決済機能を実装する

概要

ざっくりした実装手順は下記になります。
かなり手軽に実装できました。

①Stripeのアカウントを作成
②Laravel側のStripeライブラリをインストール
③APIの決済処理を実装
④フロント側の実装

処理フロー

Stripe側にまずはカード情報を送信し、返却されたトークンと支払内容をAPIにリクエストを投げ
決済処理を実施し、処理結果を返却するという流れを行うことで
サーバ側にカード情報を持たずに決済処理を行えます。

image.png

参考

StripeCheckoutのモジュールを使用している
vue-stripe-checkout

【簡単・オシャレ】PHPでstripeの決済を導入する手順【決済システム導入】

手順

1. アカウント作成

こちらでアカウント作成

作成するとテスト用で
公開可能キーシークレットキーが発行されるのでAPI側でシークレットキーでVue側で公開可能キーを使用します。

2. StripeAPIライブラリをインストール

composer require stripe/stripe-php

3. API側の実装

Vue側でクレジットカード情報を入力し、クレジット情報が正しい場合tokenを発行されるので
API側でそのtokenを使用して決済処理を実行する

public function pay(Request $request)
    {
        // Set your secret key: remember to change this to your live secret key in production
        // See your keys here: https://dashboard.stripe.com/account/apikeys
        Stripe::setApiKey('sk_test_XXXXXXXXXXXXXXXXXXXXXXXX');

        try {
            // Token is created using Checkout or Elements!
            // Get the payment token ID submitted by the form:
            $token = $request->input('stripeToken');
            $charge = Charge::create([
                'amount' => $request->input('amount'),
                'currency' => 'jpy',
                'description' => 'Example charge',
                'source' => $token,
            ]);
        }catch (Stripe\Error\Card $e){
            // 決済に失敗したらエラーメッセージを返す
            return response()->json([
                'success' => false,
                'errors' => $e->getMessage()
            ], 422);
        }

        $user_wallet = $this->exchange->cashExchange($request);

        return response()->json([
            'success' => true,
            'data' => $user_wallet,
            'message' => '現金からポイントに換金が完了しました'
        ], 200);

    }

4. フロント側の実装

vue-stripe-checkoutをインストール
※現在(2019/9/1時点)最新でベータ版がリリースされてるが任意の金額でチェックアウトする機能がまだ未実装のため下記のバージョンを指定している

またアップデート版で金額指定できるようになったら記事にしたいと思います。

npm install vue-stripe-checkout@1.2.6 --save

プラグインを作成する

stripe-checkout.js
import Vue from 'vue'
import VueStripeCheckout from 'vue-stripe-checkout'

Vue.use(VueStripeCheckout, process.env.STRIPE_PUBLIC_KEY)

Vueのテンプレート側

クレジット決済したい画面で<vue-stripe-checkout>

<template>
  <div class="container">
    <h2 class="title is-4">クレジットで換金</h2>
    <!-- 入金額 -->
    <div class="field">
      <p class="control has-icons-left has-icons-right">
        <input
          v-model.number="amount"
          class="input is-medium"
          :class="{ 'is-danger': errors.amount }"
          type="number"
          placeholder="入金額"
        />
        <span class="icon is-small is-left">
          <i class="fas fa-yen-sign"></i>
        </span>
      </p>
      <div class="help is-danger">
        <ul v-if="errors.amount">
          <li v-for="msg in errors.amount" :key="msg">
            {{ msg }}
          </li>
        </ul>
      </div>
    </div>
    <div class="field">
      <b-field label="">
        <p>{{ current_time }} 現在</p>
      </b-field>
    </div>
    <div class="field">
      <b-field label="換金レート">
        <p>
          1000円 ⇔
          {{
            Math.floor(
              1000 *
                currency_date.credit_payment *
                currency_date.cash_rate *
                currency_date.cash_exchange
            )
          }}ポイント
        </p>
      </b-field>
    </div>
    <div class="field">
      <b-field label="購入ポイント">
        <p>
          {{
            Math.floor(
              amount *
                currency_date.credit_payment *
                currency_date.cash_rate *
                currency_date.cash_exchange
            )
          }}ポイントへ変換
        </p>
      </b-field>
    </div>

    <vue-stripe-checkout
      ref="checkoutRef"
      :name="name"
      :description="description"
      :currency="currency"
      :amount="amount"
      :locale="locale"
      :panel-label="label"
      :allow-remember-me="true"
      @done="done"
      @opened="opened"
      @closed="closed"
      @canceled="canceled"
    ></vue-stripe-checkout>

    <div class="field-button">
      <input
        class="button is-blue is-large is-fullwidth"
        value="OK"
        @click="stripeCheckout"
      />
    </div>
  </div>
</template>

スクリプト側
vue-stripe-checkout側でクレジット情報を入力し、成功したらtokenが発行されるので
それをAPI側にリクエストを投げます。

data() {
    return {
      name: 'Guild',
      description: 'カード情報を入力してください。',
      currency: 'JPY',
      label: 'カード支払い',
      locale: 'ja',
      amount: 0,
      current_time: this.$moment().format('YYYY/MM/DD HH:mm:ss'),
      currency_date: {},
      form: {
        user_id: '',
        currency_id: '',
        payment: 0,
        stripeToken: ''
      }
    }
  },
  async asyncData({ app, store }) {
    const { data } = await app.$axios.get(
      `/api/user/currency/${store.getters['event_info/currency']}`
    )
    return { currency_date: data.data[0] }
  },
  methods: {
    async stripeCheckout() {
      // this.$checkout.close()
      // is also available.
      // token - is the token object
      // args - is an object containing the billing and shipping address if enabled
      await this.$refs.checkoutRef.open()
    },
    async done({ token, args }) {
      // token - is the token object
      // args - is an object containing the billing and shipping address if enabled
      // do stuff...

      this.form.user_id = this.$nuxt.$auth.user.id
      this.form.currency_id = this.currencyId
      this.form.payment = this.amount
      this.form.stripeToken = token.id

      await this.$axios
        .$post('/api/user/wallet/pay', this.form)
        .then(data => {
          this.$router.push('/client/payment/thanks')
        })
        .catch(errors => {
          alert(JSON.stringify(errors))
        })
    },
    opened() {
      // do stuff
    },
    closed() {
      // do stuff
    },
    canceled() {
      // do stuff
    }
  }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ネタ系アプリ「キモツイ」で使用した技術(Vue.js+Firebase+Buefy)の基本的なセッティング方法

構想から半日の爆速でリリースしたネタ全振りWebサービス「キモツイ」の構成技術とその基本的なセッティング部分の解説です。

作ったサイトはこちら
キモツイ
(追記:2019年9月10日現在メンテ中です。)

「クソツイ」じゃない「キモツイ」だ!
「キモツイ」は「クソツイ」とは違う新たな概念です。ちょっとキモい感じが漂うツイートを「キモツイ」と私たちは愛を込めて呼びます。
当サイトは、キモいツイートを誰でも投稿・シェアできるサービスです。
お気に入りキモツイには「キモ投票」ができ、100票集まると殿堂入りします。
ぜひお気に入りのキモツイを投稿・シェアしてみてください。

ブログに遊び方書いています。ブログ

Vue.js + Firebase + Buefy(Bulma)のお手軽三点セット

気軽な個人開発ではもうおなじみの、フロントは「Vue.js」バックエンドはサーバレスの「Firebase」という構成です。加えてUIも楽したいのでBulmaベースでVue.js用に作られたUIフレームワークの「Buefy」を使っています。
この3点セットは何か突発的にサービスを作りたいときに本当に簡単でオススメです。
Vue.js
Firebase
Buefy

今回はこの3つのセッティングの方法を紹介します。

Vue.jsのセットアップ

VueのプロジェクトはVue CLI3を使って設定しました。
まずCLIをインストール

npm install -g @vue/cli

次にプロジェクト作成

$ vue create my-project

色々質問されるから今回は以下のように作成

? Please pick a preset: 
  example (vue-router, vuex, stylus, babel, eslint, unit-mocha) 
  default (babel, eslint) 
❯ Manually select features 

? Check the features needed for your project: 
 ◉ Babel
 ◯ TypeScript
 ◯ Progressive Web App (PWA) Support
 ◉ Router
❯◉ Vuex
 ◯ CSS Pre-processors
 ◉ Linter / Formatter
 ◯ Unit Testing
 ◯ E2E Testing

? Use history mode for router? (Requires proper server setup for index fal
lback in production) (Y/n) y

? Pick a linter / formatter config: (Use arrow keys)
❯ ESLint with error prevention only 
  ESLint + Airbnb config 
  ESLint + Standard config 
  ESLint + Prettier

? Pick additional lint features: (Press <space> to select, <a> to toggle a
ll, <i> to invert selection)
❯◉ Lint on save
 ◯ Lint and fix on commit

? Where do you prefer placing config for Babel, PostCSS, ESLint, etc.? (Us
e arrow keys)
❯ In dedicated config files 
  In package.json

? Save this as a preset for future projects? (y/N) n

あとはプロジェクトのディレクトリに移動し、


$ npm run serve

うまくローカルホストで表示されれば成功です。
これでVue.jsのセットアップは完了です。

Firebaseのプロジェクト作成とVue.jsとの連携

まずFirebaseのアカウントを作ってください。

「プロジェクトの作成」でプロジェクトの名前を入力
次にアナリティクスの登録を聞かれるので、今回は設定しないを選択
これだけでプロジェクトの作成ができます。

次に、プロジェクト内の管理画面でウェブを選択し、コードを取得します。

<!-- The core Firebase JS SDK is always required and must be listed first -->
<script src="https://www.gstatic.com/firebasejs/6.6.0/firebase-app.js"></script>

<!-- TODO: Add SDKs for Firebase products that you want to use
     https://firebase.google.com/docs/web/setup#config-web-app -->

<script>
  // Your web app's Firebase configuration
  var firebaseConfig = {
    apiKey: "XXXXXXXXXX",
    authDomain: "XXXXXXX.firebaseapp.com",
    databaseURL: "https://XXXXXXX.firebaseio.com",
    projectId: "XXXXXXXX",
    storageBucket: "",
    messagingSenderId: "XXXXXXXXXX",
    appId: "XXXXXXXXXXXXXXXXX"
  };
  // Initialize Firebase
  firebase.initializeApp(firebaseConfig);
</script>

このコードです。
ここの「script」で囲まれている部分を使います。
そして、Vue CLIで作ったプロジェクトのsrcの下に「firebase.js」ファイルを作成してこのコードを貼り付けてください。

src/firebase.js

<script>
  // Your web app's Firebase configuration
  var firebaseConfig = {
    apiKey: "XXXXXXXXXX",
    authDomain: "XXXXXXX.firebaseapp.com",
    databaseURL: "https://XXXXXXX.firebaseio.com",
    projectId: "XXXXXXXX",
    storageBucket: "",
    messagingSenderId: "XXXXXXXXXX",
    appId: "XXXXXXXXXXXXXXXXX"
  };
  // Initialize Firebase
  firebase.initializeApp(firebaseConfig);
</script>

保存したら、firebaseを初期化します。

npm install firebase --save

次に「firebase.js」に以下のようにコードを変更追加し、さらに「main.js」で最初に読み込むように設定します。ここのやり方は色々あるので好みの問題です。

src/ firebase.js

import firebase from "@firebase/app";
import "@firebase/auth";
import "@firebase/firestore";
import "@firebase/storage";

const config = {
    apiKey: "XXXXXXXXXX",
    authDomain: "XXXXXXXXXX.firebaseapp.com",
    databaseURL: "https://XXXXXXXXXX.firebaseio.com",
    projectId: "XXXXXXXXXX",
    storageBucket: "",
    messagingSenderId: "XXXXXXXXXX",
    appId: "XXXXXXXXXX"
};
export default {
    init() {
        firebase.initializeApp(config);
    }
  }

src/main.js

import Vue from 'vue'
import router from './router'
import store from './store'
import Firebase from "./firebase"

Vue.config.productionTip = false

Firebase.init();

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

ポイントは、main.jsで「Firebase.init()」で読み込ませているところです。これを忘れるとうまく動きません。

これでVue.jsとFirebaseの連携ができるようになりました。

Buefyの設定

Buefyの設定は簡単です。
npmでインストールします。

npm install buefy

次に、「main.js」に以下の加えます。

import Vue from 'vue'
import Buefy from 'buefy'
import 'buefy/dist/buefy.css'

Vue.use(Buefy)

これで完了です。アイコンを使いたい場合は「head」要素にCDNを追加すれば使えるようになります。

シンプルだけど強力な組み合わせ

以上で簡単に開発する準備ができました。
個人開発など一人の開発ならこれで事足りるので本当に便利だなぁと思います。
ホスティングもFirebase hostingで行えば本当に一瞬ですからオススメです。

最後にツイッターをやっている方はぜひ「キモツイ」で遊んでみてください。
キモツイ

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

jQueryおじさんが今更復帰してNuxt.jsとfirebaseでサービスを作ってみた。

書いた人

jQueryおじさん(Qiita祝初投稿)

簡単な自己紹介

JQuery全盛期にフロント界隈でゴリゴリ開発していた時代から
事情があって一度webフロントから引退
その間、趣味でangular js, reactなども軽く触ってはみたものの
あまりしっくりこないままjQuery最強じゃね?と思い込み
トレンドに取り残されてしまったかわいそうなjQueryおじさん、、
ちなみにサーバーサイドはちょっとPHPが書ける程度

この記事をターゲット

  • フロントしかできないから一人でサービス作ってみたいけど躊躇している人
  • フロントエンジニア歴が長くていろいろ歴史を振り返ってみたい人
  • 個人開発者様全般
  • 素人に毛が生えた程度の技術のウンチクを温かい目で見てくれるその道のプロ

作ったモノ

https://sugume.web.app
完全無料、登録なしで使える、SNSアカウント一時共有サービス
デザインが残念なままw

構成

今流行りのサーバーレス(サーバーはあるわけでこの言葉がしっくりこないが)

nuxt.js + firebase(firestore + hosting + cloud functions)

作った感想

nuxt.js も firebase もスゲー
(語彙力がやばいw)

え、DBってこんなに簡単に作れちゃっていいの?サーバーサイドエンジニア消されちゃうんじゃ?(言い過ぎ)

実を言うと最初はvue.js単体で別のアフィリ向けのサイト(DBなし)を作っていたのですが
だいぶ完成に近づいたところでSSRやんないと各ページのSEOが、、的なのこと気づいてしまい
いろいろ調べているときに nuxt.jsにたどり着いたんです。(もっと調べてから作り始めろよw)

開発スタート時点の構成

vuejs + webpack

サーバーは適当なレンタルサーバーにあげればいいかなーとか考えていた。

wbpackもすごく便利ですよね。
jQueryおじさんはgulpが流行りだした頃からフェイドアウトしていたので
いろんな処理が簡略化されていて驚き
vueJsはjQueryおじさんのくせになぜかリアクティブなサイト構築の経験が少しだけあったので
ここはすんなりクリア(angularjs, reactjs では飲み込めなかったがなんかスッキリ入ってきた)

ある程度できたところで先述の問題に直面!SEO的にどう考えてもSSRやんなきゃ!SPAじゃだめですやん
どーしよう、、
ん?何?Nuxtjsってのが便利?でもサーバー側nodeJsでしょ?レンタルサーバーで対応できないじゃん、、

jQueryおじさんはサーバーサイドの知識が少し乏しいため、
スマホの便利さを知った上で使わない50代のおっさんと同じで
AWS,netlifyやherokuの存在を知りつつも得体の知れないもの(特にサーバーサイド&黒い画面は敬遠しがち)
サーバー恐怖症だけではなく正規表現アレルギーも持病で持っているため今回の入力バリデートは苦労した。

そんなこんなでたどり着いたのがそう、google様のアレ

firebase様

え?npmごにょごにょするだけで数分でサーバー完成?しかもある程度のアクセスまで無料??
あちこちでお金出してるレンタルサーバーのお金どないしてくれんの?
ドメインも最初は別途取得予定だったけどそんなに違和感なかったのでfirebase様が提供してれたドメインでそのまま行くことに

というわけで、PHPも触われるんだからアフィサイトなんか王道のワードプレスを使えばいいのに
意地はって自前で
Nuxt.js + firebaseでアフィサイトを構築

だいたい出来てきてあとは 原稿をモリモリ描くだけだー ってところで
魔がさす。

もうこれ、あとはただの単純作業で
もしかしたらアフィで稼げるかも知れんが
エンジニア的好奇心が満ちてしまったため
少しモチベーションが下がる、、(エンジニアあるある?なのか)


そんなときある出来事がきっかけで突然別に作りたいものが空から降ってくる

それが今回のサイト
https://sugume.web.app
完全無料、登録なしで使える、SNSアカウント一時共有サービス

ログインなしのDBのちゃちゃっとした書き換えぐらいだし
firebaseでnpmごにょごにょしてたらできるんじゃないか?

ちょっとDBもデビューしつつこっちを作ってみるか
というわけで
nuxt.js + firebase(firestore + hosting)
の環境が完成!

そしてだいぶ完成したところで
これもしちょっとだけ流行ったら DBにゴミデータ残るな、、
バッチ処理で定期的に削除しなきゃ!!
と言うわけで最終的にcloud functionsも導入
DBのゴミを削除するスクリプトをhttpsトリガーで導入
それをGASで定期的に実行しておしまい。(Firebase単体できるという噂も、、)

と言うわけで冒頭の
nuxt.js + firebase(firestore + hosting + cloud functions)
が完成

工数

実質1日3時間x15日くらいです
いろいろとテンプレート作ったり吟味しながらだったり
デザインデータなしでCSSで行き当たりばったりデザイン構築したので
もう一度同じようなものを作るならもっとスピードアップできるはず。
どこかにデザインを無料でやってくれる神様みたいな人落ちてないですかね?

今回避けたもの

CSSライブラリ(bootstrap的なもの)

テンプレートをキャンセルするのにすごい手間がかかることがあるから
今回はルールを決めずにちゃちゃっと適当デザインで行きたかった

lint系

(一人開発だったら自己責任でいいよね?規模がもし大きくなったらあとから導入)

デザイン

(デザイナーほんとは欲しいけどね、、)

pug(元jade)

流派的にあんまり好きじゃないのもあるけど結局コピペーでHTMLタグ組むとどうしても閉じタグが過不足するから導入したほうがいい


長々となってしまったが
伝えたいことをまとめる。

一度引退してしまっても時々新しいものをHelloWorldしておいたほうがいい

今回ちょうど一年くらい前に vueJSだけは別でHelloWorldしてたので実装自体はスムーズだった。
(できればこれがスタンダードになって10年くらい覇権を握ってくれるとまたこれで飯が食える気が)

フロントエンジニアの PaaSのデビューはfirebaseで決まり!

いやほか試してないけど、

独学エンジニアはHelloWorldの先3歩くらい踏み込んで

できることの領域をモノづくりをしながら広げていくべし
今までの持ってるスキルとかけ合わせるととてつもなくレベルアップします。

(最悪作るものを途中で変えたって良いし納得したらリリースしなくたっていい)
今回はjQueryおじさんは firebase と出会って かなりレベルアップした気持ちでいます。


全部完璧にしなくてもいいんじゃない?
個人開発なんだからやりたいところだけ
あとはテキトー、、もしサービスが流行ってしまったら?
googleにめっちゃ請求されたら?
複数人で開発必要になったら?

そんときまた考えましょ!
リクエストがもしあれば技術的な構成の話も別途書きます。
需要があれば、、、

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

【Nuxt.js】 WebアプリSHISHOWの開発をしたときの備忘録①導入編

自己紹介

Papillon6814という名前でよく開発をしています。
僕のGithub
一つWebアプリをある程度の形までもっていくことが出来たので,記念に記事にしてついでに誰かの助けになればいいなと思っています。

作ったもの

SHISHOWというWebサービスを作ってみました!
https://www.shishow.tokyo/
*今はベータ版で,不具合も確認されています...

簡単に概要を説明すると,
- アカウントを登録して,自分の好きなゲームを登録する。(存在しなければ新しく追加する)
- 他の人のプロフィールを見ることができるのでそれを見てゲームを教えてほしい人,一緒にゲームをやりたい人に師匠になってくれるように申請を送る。
- 申請が承認されれば,その人とチャットでやりとりをすることができます。

使ったフレームワーク等をざっくりと

Nuxt.js

SEO対策をしたかったのでSSRをせねばならず,そのためにNuxtを選択しました。
実は最初はVue-cliを使って開発をしていましたが,SSRのために途中で移行しました。

Firestore

すごく使いやすかったと思っています。ほとんどのページで使っているといっても過言ではないと思います。

Cloud Functions

非同期でデータを少しずつ読みこむために使いました。jsでバックエンドを書くのは初めてでした。

Firebase Hosting

デプロイしても

この画面が表示されて困ったことがあったので,それについても書いていきたいです。

vue-cropper.js

もしかしたらこれの導入で困っている人がいるんじゃないかなと思って一応書いておきます。(チームメイトが困っていたので)

記事一覧

開発していった順に記事を投稿していきます。
第2回

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

Vue.jsでデータの受け渡しをしてみる

Vue.jsを勉強中なので、とりあえず好物の納豆の金額を画面に表示させてみました。

親コンポーネント

まずは親コンポーネント側にデータをもたせる。

let app = new Vue({
  el: '#app',
  data: {
    name1: '極小粒納豆',
    name2: '大粒納豆',
    price1: 75,
    price2: 100
  }
});

子コンポーネント

子コンポーネントにpropsを用意し親コンポーネントのデータを受け取る。

Vue.component('my-product', {
  template: `
  <div>
    <span>{{name1}}</span>:<span>{{price1}}(円)</span>
    <br>
    <span>{{name2}}</span>:<span>{{price2}}(円)</span>
  </div>`,
  props: ['name1', 'name2', 'price1', 'price2']
});

親テンプレート

親のテンプレートに記述した子のカスタムタグに、v-bindディレクティブでデータをバインドする。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <title>納豆の値段</title>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/5.0.0/normalize.min.css">
</head>
<body>
  <div id="app">
    <my-product v-bind:name1='name1' v-bind:name2='name2' v-bind:price1='price1' v-bind:price2='price2'></my-product>
  </div>
  <script src="https://jp.vuejs.org/js/vue.js"></script>
  <script src="my-product.js"></script>
  <script src="main.js"></script>
</body>
</html>
ブラウザに出力されるのは次のような内容になる。

間違いなどがありましたら教えていただけますと幸いです。
納豆は極小粒が好きです。

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

Vue初心者のメモ

コンポーネントに値を渡す

props

<ChildComponent hoge="value"></ChildComponent>
export default {
    props: [
        'hoge'
    ]
}

ディレクティブ

ディレクティブはv-から始まる特別な属性。その属性値には単一のJS式を期待する。(ただしv-forは例外)
ディレクティブの仕事は、属性値の式が変化した時に、リアクティブに副作用をDOMに適用すること。

引数

引数を取るディレクティブがある。ディレクティブ名の後にコロンで表記する。

<span v-bind:id="hogeId">HOGEHOGE</span>

<button v-on:click="registerUser">Register</button>

動的引数

角括弧で囲むことで、単一のJS式をディレクティブ引数に使うことができる(Ver2.6.0から)

<button v-on:[eventname]="registerUser">Register</button>

※動的引数の制約
動的引数の式は最終的にNULLかStringに評価されることが期待されている。それ以外の場合は警告となる。
スペースや引用符のような一部の文字は、HTMLの属性名として不正な文字のため使えない。(構文エラーとなる)
in-DOMテンプレートを使う場合、ブラウザが強制的に属性名を小文字に変換するため、キー名を大文字にするのは避けるべき

修飾子(Modifier)

ドットで表記された特別な接尾語。
ディレクティブが特別な方法で束縛されるべき、ということを示す。

<form v-on:submit.prevent="onSubmit"> ... </form>

v-bind

v-if

v-for

v-on

v-model

双方向バインディング

省略記法

v-bindとv-onは頻繁に使うため省略記法が用意されている。

v-bind

<a v-bind:href="targetUrl"> ... </a>
<a :href="targetUrl"> ... </a>
<a :[attr]="targetUrl"> ... </a>

v-on

<a v-on:click="doSomething"> ... </a>
<a @click="doSomething"> ... </a>
<a @[eventName]="doSomething"> ... </a>

Vueインスタンス

プロパティ

自身のdataオブジェクトの全てのプロパティをリアクティブシステムに追加する
-> プロパティの値が変更されるとビューが反応し、新しい値に一致するように更新される

dataのプロパティはVueインスタンスが生成された時に存在していた場合のみリアクティブ
-> 後から新しいプロパティを追加しても、その変更はビューの更新を引き起こさない

ライフサイクルフック

created, mounted, updated, destoyedなど
全てのライフサイクルフックにおいて、thisがVueインスタンスを指す形で実行される
※インスタンスプロパティやコールバックでアロー関数を使用しないこと!
-> アロー関数はthisを持たないため(親スコープまで探索される)

ライフサイクルダイアグラム

公式ドキュメントの図を参照

テンプレート構文

※直接render関数で書くことも可能

テキスト

Mustache (二重中括弧)

<span>Hello, {{ message }}</span>

生のHTML

v-htmlディレクティブ

<span v-html="rawHtml">Hello, Everyone!</span>

※動的にHTMLを描画する際にはXSSに要注意!!

属性

v-bindディレクティブ
※属性にMustacheは使えない

<span v-bind:id="targetId">Hot Coffee.<span>

※真偽値属性の場合は少し動きが異なるので注意

JavaScript式の使用

全てのデータバインディング内でJS式の使用が可能!

{{ number + 1 }}

{{ ok ? 'Yes' : 'No' }}

{{ csvString.split(',').join('&') }}

<div v-bind:id="'list_' + key">hogehoge</div>

ただし単一の式だけが評価可能。以下は動作しない。

// これは式ではなく文なのでNG
{{ var a = 1 }}

// フロー制御もNG →三項演算子なら動く
{{ if (flg) { return 'Happy!' } }}

算出プロパティ

テンプレート内に複雑な式を書くのは適切ではない。
→複雑なロジックには算出プロパティを使うとよい。

<div id="sample">
  <p>Original: {{ message }}</p>
  <p>Computed: {{ reversedMessage }}</p>
</div>
var vm = new Vue({
  el: #sample,
  data: {
    message: 'I am a perfect human.'
  },
  computed: {
    reversedMessage: function () {
      return this.message.split(' ').reverse().join(' ')
    }
  }
})

→getter関数として作用する

メソッドとの違い

メソッドを使っても同じ結果を得ることができるが、下記の違いがある。
算出プロパティ→値がキャッシュされ、プロパティが変化した時だけ再計算される
メソッド→毎回計算される

監視プロパティ(watched property)

<div id="demo">{{ fullName }}</div>
var vm = new Vue({
  el: #demo,
  data: {
    firstName: 'Takamori',
    lastName: 'Saigo',
    fullName: 'Saigo Takamori'
  },
  watch: {
    firstName: function (val) {
      this.fullName = this.lastName + ' ' + val
    },
    lastName: function (val) {
      this.fullName = val + ' ' + this.firstName
    }
  }
})

→冗長になってしまうので、算出プロパティの方がよさげ。

算出setter関数

算出プロパティはデフォルトではgetterのみだが、必要に応じてsetterも使える

// ...
  computed: {
    fullName: {
      get: function() {
        return this.firstName + ' ' + this.lastName
      },
      set: function(newVal) {
        var names = newVal.split(' ')
        this.firstName = names[0]
        this.lastName = names[length.names - 1]
      }
    }
  }
// ...

ウォッチャ

多くの場合は算出プロパティが最適だが、データの変化に応じて非同期処理やコストの高い処理を行う場合はウォッチャが便利。
詳細は公式ドキュメントを参照

クラスとスタイルのバインディング

v-bindがclassやstyleと一緒に使われる時に特別な拡張機能が提供される。
→式が文字列だけでなく、オブジェクトと配列を返すことができる。

クラスのバインディング

オブジェクト構文

<div v-bind:class="{ active: isActive}"> ... </div>

→activeクラスの有無がisActiveの真偽性によって決まる
オブジェクトは複数のフィールドを持つことも可能。
プレーンなclassと共存することも可能。
オブジェクトはインラインでなく、外部で定義することも可能。

配列構文

<div v-bind:class="[activeClass, errorClass]"> ... </div>
// ...
  data: {
    activeClass: 'active',
    errorClass: 'error-text'
  }
// ...

→配列を渡してクラスのリストを適用可能。

<div v-bind:class="[{ active: isActive }, errorClass]"> ... </div>

→配列構文の中にオブジェクト構文を使うことも可能
```

コンポーネントにおける挙動

カスタムコンポーネントにおいてclass属性を使用するとき、これらのクラスはroot要素に追加される。
この要素上に存在するクラスは上書きされない。
コンポーネントで定義されたクラス

Vue.component('my-component', {
  template: '<p class="already base"> ... </p>'
})

呼び出し時にクラスを指定

<my-component class="new top"></my-component>

描画されるHTML

<p class="already base new top"> ... </p>

インラインスタイルのバインディング

オブジェクト構文

<div v-bind:style="{ color: themeColor, fontSize: normalFontSize }">inline sample</div>
<div v-vind:style="styleObject">object sample</div>
data: {
  themaColor: #ff5531,
  normalFontSize: 14,
  styleObject: {
    color: 'blue',
    fontSize: 19
  }
}

配列構文

複数のスタイルオブジェクトを適用することができる

<div v-bind:style="[baseStyle, specialStyle]">array sample</div>

自動プリフィックス

ベンダー接頭辞が要求されるスタイルの場合、Vueが自動的に検出して追加する

条件付きレンダリング

v-if

ディレクティブの式が真を返す時だけブロックが表示される。
elseブロックを追加することも可能。

<h1 v-if="flgHit">Hitted!</h1>
<h1 v-else>Oh!Missed</h1>

templateでの条件グループ

複数要素に条件を適用したい場合、要素でグループ化できる。

<template v-if="member">
  <div>Your name</div>
  <div>hogehoge</div>
</template>

→最終的に要素は描画されない。

v-else, v-else-if

v-else, v-else-ifも使用可能。

v-show

<div v-show="ok">All right</div>

表示の有無を切り替えることができる。
CSSのdisplayプロパティで切り替えられており、DOMは常に維持されている。

v-ifとv-show

v-if: 切り替えコストが高い
v-show: 初期表示コストが高い

v-ifとv-for

v-ifとv-showを同時に使用することは推奨されない。

リストレンダリング

v-for

<ul id="example">
  <li v-for="item in items">{{ item.message }}</li>
</ul>
var example1 = new Vue({
  el: '#example',
  data: {
    items: [
      { message: 'hello' },
      { message: 'everyone' },
      { message: 'good bye' }
    ]
  }
})

v-forブロック内では、親スコープのプロパティへの完全なアクセス権を持つ
配列のインデックスを2つ目の引数としてサポートしている

<ul id="example2">
  <li v-for="(item, index) in items">
    {{ parentValue }} - {{ index }} - {{ item.message }}
  </li>
</ul>
var example1 = new Vue({
  el: '#example',
  data: {
    parentValue: 'This is Parent',
    items: [
      { message: 'hello' },
      { message: 'everyone' },
      { message: 'good bye' }
    ]
  }
})

オブジェクトのv-for

オブジェクトのプロパティもv-forで反復処理ができる。
2つ目の引数でプロパティのキーが取得できる。
3つ目の引数でインデックスが取得できる。

<div v-for="(value, key, index) in object">
  {{ index }} - {{ key }} : {{ value }}
</div>

フィルタ、ソートされた結果の表示

算出プロパティ、もしくはメソッドを使って実現可能

<li v-for="n in evenNumbers">{{ n }}</li>

<li v-for="p in even(numbers)">{{ p }}</li>
data: {
  numbers: [1, 2, 3, 4, 5, 6]
},
computed: {
  evenNumbers: function () {
    return this.numbers.filter(function (number) {
      return number % 2 === 0
    })
  }
},
method: {
  even: function (numbers) {
    return numbers.filter(function (number) {
      return number % 2 === 0
    })
  }
}

templateでのv-for

複数の要素のブロックを表示するためにtemplateブロックを使用可能

<template v-for="(item, index) in items">
  <div>{{ index }}</div>
  <span>{{ item.message }}</span>
</template>

その他はドキュメントを参照

イベントハンドリング

イベントの購読

v-onディレクティブによって、イベントの購読、イベント発火時のjsの実行が可能。
イベントハンドラのロジックは複雑になりがちなので、v-onはメソッド名を指定することが可能。

<div id='example'>
  <button v-on:click="someAction">Push</button>
</div>

$event変数を使うことで、メソッドにDOMイベントを渡すことも可能

<button v-on:click="someAction('customParam', $event)">Register</button>

イベント修飾子

  • .stop
  • .prevent
  • .capture
  • .self
  • .once
  • .passive

詳細はドキュメントを参照

キー修飾子

ドキュメント参照

フォーム入力バインディング

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

plunkerでvue その8

概要

plunkerでvueやってみた。
json読み込んで見た。

成果物

d3.json

http://embed.plnkr.co/LJpcW1F5mBCkji8xDVMe/

xhr

http://embed.plnkr.co/dw5N7Tf4KTwpkoCK7ngN/

axios

http://embed.plnkr.co/IH7fOVa3ppmIocXZqA77/

以上。

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

Nuxt.js+Firebaseのサーバーレス開発 簡易CRUD操作〜githubデプロイ迄

最近サーバーレス技術で話題のFirebaseに触れてみました。

一口にFirebaseと言っても色んな機能がありますが、今回はNuxt.js(axios利用)からFirebaseへの簡易CRUD操作を行い、簡易Webシステムのgithub-pagesデプロイ迄やってみたので、作業メモを投稿します。

ちなみにNuxt.js開発ではNuxt.jsビギナーズガイドを参考にしています。

今回作ったもの

私は福岡出身なのですが、将来的に福岡にUターンしたい思いが強いので、福岡に本社・支社を持つIT企業の下調べがてら企業情報をメモれるWebサイトを作ってみました(福岡に限らず登録出来ますが)

福岡に本社or支社を持つIT企業

フロントエンドはNuxt.jsでFirebaseのRealtime Databaseにデータ連携させています。

左側に登録済みの会社一覧を表示させ、リンク(会社名)押下で右側に詳細情報を表示。


新規登録と編集で会社情報の登録と更新を実行。


3日くらいの突貫で開発したので色々イケてない部分も多いですが...

Nuxt.js環境構築

Nuxt.jsプロジェクトを初期化して、必要なモジュールのインストール。

yarn create nuxt-app
yarn add @nuxtjs/axios
yarn add @nuxtjs/proxy

nuxt.config.jsに以下を追記。

modules: [
  '@nuxtjs/axios',
  '@nuxtjs/proxy'
],

axios: {
  // 自分のfirebaseアカウントを設定
  baseURL: 'https://nuxt-blog-service-xxxxx.firebaseio.com'
}

本当は認証とかまで実装したかったのですが、今回はCRUD操作だけなので設定はこれだけ。

レイアウト設定

ヘッダーとフッターのみコンポーネントを自作して呼び出し。

app/layouts/defalt.vue
<template>
  <div class="container">
    <TheHeader />
    <nuxt />
  </div>
</template>

<script>
import TheHeader from '~/components/common/TheHeader.vue'
import TheFooter from '~/components/common/TheFooter.vue'
export default {
  components: {
    TheHeader,
    TheFooter
  }
}
</script>

<style scoped>
.container {
  margin: 0 auto;
  max-width: 1300px;
}
</style>

一覧表示

初期表示時にFirebaseから会社情報を取得後、個別の会社情報リンク先を押下すると、vuexで管理しているデータを表示させています。

app/pages/index.vue
<template>
  <div class="main-container">
    <SearchArea />
    <div v-show="loading" class="loader"></div>
    <div v-show="!loading" class="item">
      <CompanyList />
      <CompanyDetail />
    </div>
  </div>
</template>

<script>
import { mapGetters, mapActions } from 'vuex'
import SearchArea from '~/components/pages/SearchArea.vue'
import CompanyList from '~/components/pages/CompanyList.vue'
import CompanyDetail from '~/components/pages/CompanyDetail.vue'
export default {
  components: {
    SearchArea,
    CompanyList,
    CompanyDetail
  },
  computed: {
    ...mapGetters({'loading' : 'loading'})
  }
}
</script>

<style scoped>
.main-container {
  padding: 30px 70px;
  min-height: 100vh;
}
.item {
  display: flex;
}
@media (max-width: 480px){
  .main-container {
    padding: 5px 10px;
  }
  #list {
    display: none;
  }
}
</style>

画面左側のリスト表示ロジック

app/components/pages/CompanyList.vue
<template>
  <div id="list" class="item-contents">
    <div v-for="(company, index) in companys" :key="company.id">
      <div class="list-contents">
        <div class="editCompany button" @click="editData(index)">編 集</div>
        <div class="button" @click="deleteData(index, company.name)">削 除</div>
        <div class="company-link" @click="detailData(index)">{{ company.name }}</div>
      </div>
    </div>
  </div>
</template>

<script>
import { mapGetters, mapActions } from 'vuex'
export default {
  mounted() {
    this.init()
  },
  computed: {
    ...mapGetters({'companys' : 'companys', 'company' : 'company'})
  },
  methods: {
    async init() {
      await this.fetchCompanys()
      this.clearCompany()
    },
    async detailData(index) {
      this.fetchCompany({ index : index })
    },
    async editData(index) {
      this.fetchCompany({ index : index })
      this.$router.push(`/edit`)
    },
    async deleteData(index, name) {
      try {
        await this.deleteCompany({ index : index })
        this.$notify({
          type: 'success',
          title: '削除成功',
          position: 'bottom-right',
          duration: 1000
        })
      } catch (e) {
        this.$notify.error({
          title: '削除失敗',
          position: 'bottom-right',
          duration: 1000
        })
      }
      this.clearCompany()
    },
    ...mapActions(['fetchCompanys', 'fetchCompany', 'deleteCompany', 'clearCompany'])
  }
}
</script>

<style scoped>
#list {
  width: 35vw;
  min-height: 60vh;
}
.list-contents {
  margin: 15px 0;
  padding-bottom: 15px;
  border-collapse:separate;
  border-spacing: 15px 0;
  border-bottom: solid 1px #c0c0c0;
}
.list-contents div {
  display:table-cell;
  vertical-align: middle;
}
.editCompany {
  background-color: #6495ed;
}
.company-link {
  color: #6495ed;
}
.company-link:hover {
  color: #ff69b4;
}
</style>

会社情報の表示。

app/components/pages/CompanyDetail.vue
<template>
  <div id="detail" class="item-contents" v-if="company === null">
    <p style="padding: 15px 0 15px 15px;">会社名を選択してください!!</p>
  </div>
  <div id="detail" class="item-contents" v-else>
    <div>
      <p class="column">◾️企業名</p>
      <p class="value">{{ company.name }}</p>
    </div>
    <div>
      <p class="column">◾️リンク先</p>
      <p class="value"><a :href=company.link target="_blank">{{ company.link }}</a></p>
    </div>
    <div id="address-area">
      <p class="column">◾️住所</p>
      <p class="value">{{ company.address }}</p>
    </div>
    <div>
      <p class="column">◾️業務内容</p>
      <p class="value">{{ company.job }}</p>
    </div>
    <div id="usedSkills">
      <p class="column">◾️使われている技術</p>
      <div class="skill" v-for="skill in company.usedSkills" :key=skill.key>
        <div class="value skill-img">
        <div style="text-align:center;"><img :src=skill.path /></div>
          <p>{{ skill.key }}</p>
        </div>
      </div>
    </div>
    <div class="r-skill">
      <p class="column">◾️求められるスキル</p>
      <p class="value">{{ company.requiredSkill1 }}</p>
      <p class="value">{{ company.requiredSkill2 }}</p>
      <p class="value">{{ company.requiredSkill3 }}</p>
      <p class="value">{{ company.requiredSkill4 }}</p>
      <p class="value">{{ company.requiredSkill5 }}</p>
    </div>
    <div class="r-skill">
      <p class="column">◾️歓迎されるスキル</p>
      <p class="value">{{ company.welcomedSkill1 }}</p>
      <p class="value">{{ company.welcomedSkill2 }}</p>
      <p class="value">{{ company.welcomedSkill3 }}</p>
      <p class="value">{{ company.welcomedSkill4 }}</p>
      <p class="value">{{ company.welcomedSkill5 }}</p>
    </div>
  </div>
</template>

<script>
import { mapGetters } from 'vuex'
export default {
  computed: {
    ...mapGetters({'company' : 'company'})
  }
}
</script>

<style scoped>
#detail {
  width: 65vw;
}
.column {
  margin: 10px;
  font-weight: bold;
}
.value {
  margin: 10px 10px 10px 20px;
  font-size: 13px;
}
.value div {
  margin: 0px !important;
}
.value div img {
  width: 30px;
  height: 30px;
}
.skill {
  display: inline-block;
  margin: 10px 15px 10px 15px;
}
.skill-img {
  margin: 0px !important;
}
.skill-img p {
  font-size: 10px;
  text-align: center;
}
@media (max-width: 480px){
  #detail {
    width: 100vw;
  } 
  #address-area {
    width: 85vw;
  }
  #address-area .value {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .r-skill {
    width: 85vw;
  }
  .r-skill .value {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
}
</style>

登録更新

会社情報は更新でも利用するので、Company.vueに集約。

app/pages/new.vue
<template>
  <div class="main-container">
    <div id="contents">
      <Company />
      <ul style="justify-content: center; margin-bottom:0px;">
        <li><button type="button" style="width:110px;" @click="regist">新 規 登 録</button></li>
      </ul>
    </div>
  </div>
</template>

<script>
import { mapActions } from 'vuex'
import Company from '~/components/pages/Company.vue'
export default {
  components: {
    Company
  },
  methods: {
    async regist() {
      try {
        await this.registCompany()
        this.$router.push(`/`)
      } catch (e) {
        this.$notify.error({
          title: '登録失敗',
          position: 'bottom-right',
          duration: 1000
        })
      }
    },
    ...mapActions(['registCompany'])
  }
}
</script>

<style scoped>
.main-container {
  padding: 80px 250px 30px;
}
#contents {
  margin: 10px;
  padding: 30px 30px;
  background-color: #ffffff;
  border: solid 1px #c0c0c0;
  border-radius: 2px;
  box-shadow: 2px 2px 2px rgba(0,0,0,0.4);
}
ul {
  display: flex;
  margin-bottom: 20px;
}
@media (max-width: 480px){
  .main-container {
    padding: 5px 10px;
  }
}
</style>

会社情報入力ロジック。

app/components/pages/Company.vue
<template>
  <div id="company">
    <ul>
      <li class="column">会社名.</li>
      <li><input id="name" type="text" v-model=name @change="onChange"></li>
    </ul>
    <ul>
      <li class="column">リンク.</li>
      <li><input id="link" type="text" v-model=link @change="onChange"></li>
    </ul>
    <ul>
      <li class="column">住所.</li>
      <li><input id="address" type="text" v-model=address @change="onChange"></li>
    </ul>
    <ul>
      <li class="column">業務内容.</li>
      <li><input id="job" type="text" v-model=job @change="onChange"></li>
    </ul>
    <ul>
      <li class="column">使われている技術.</li>
      <li><div><input id="usedSkillsSearch" type="text" v-model=usedSkillsSearch ref="usedSkillsSearch"></div></li>
      <li style="margin-left:10px;"><button type="button" @click="addSkill()">追 加</button></li>
    </ul>
    <ul>
      <li class="column"></li>
      <li>
        <div id="usedSkills" v-if="usedSkills.length !== 0">
          <div v-for="usedSkill in usedSkills" :key=usedSkill.key>
            <span>{{ usedSkill.key }}</span>
            <span @click="deleteSkill(usedSkill.key)"></span>
          </div>
        </div>
      </li>
    </ul>
    <ul style="margin-bottom:10px;">
      <li class="column">求められるスキル.</li>
      <li>
        <div><input class="requiredSkill" type="text" v-model=requiredSkill1 @change="onChange"></div>
        <div><input class="requiredSkill" type="text" v-model=requiredSkill2 @change="onChange"></div>
        <div><input class="requiredSkill" type="text" v-model=requiredSkill3 @change="onChange"></div>
        <div><input class="requiredSkill" type="text" v-model=requiredSkill4 @change="onChange"></div>
        <div><input class="requiredSkill" type="text" v-model=requiredSkill5 @change="onChange"></div>
      </li>
    </ul>
    <ul style="margin-bottom:10px;">
      <li class="column">歓迎されるスキル.</li>
      <li>
        <div><input class="welcomedSkill" type="text" v-model=welcomedSkill1 @change="onChange"></div>
        <div><input class="welcomedSkill" type="text" v-model=welcomedSkill2 @change="onChange"></div>
        <div><input class="welcomedSkill" type="text" v-model=welcomedSkill3 @change="onChange"></div>
        <div><input class="welcomedSkill" type="text" v-model=welcomedSkill4 @change="onChange"></div>
        <div><input class="welcomedSkill" type="text" v-model=welcomedSkill5 @change="onChange"></div>
      </li>
    </ul>
  </div>
</template>

<script>
import { mapGetters, mapActions } from 'vuex'
export default {
  data () {
    return {
      name : null,
      link : null,
      address : null,
      job : null,
      usedSkillsSearch : null,
      usedSkills : [],
      requiredSkill1 : null,
      requiredSkill2 : null,
      requiredSkill3 : null,
      requiredSkill4 : null,
      requiredSkill5 : null,
      welcomedSkill1 : null,
      welcomedSkill2 : null,
      welcomedSkill3 : null,
      welcomedSkill4 : null,
      welcomedSkill5 : null
    }
  },
  computed: {
    ...mapGetters({ 'company' : 'company' })
  },
  mounted() {
    if (this.company !== null) {
      this.init()
      this.createObject()
    }
  },
  methods: {
    init() {
      this.name = this.company.name
      this.link = this.company.link
      this.address = this.company.address
      this.job = this.company.job
      this.usedSkillsSearch = this.company.usedSkillsSearch
      this.usedSkills = (this.company.usedSkills === undefined) ? [] : this.company.usedSkills
      this.requiredSkill1 = this.company.requiredSkill1
      this.requiredSkill2 = this.company.requiredSkill2
      this.requiredSkill3 = this.company.requiredSkill3
      this.requiredSkill4 = this.company.requiredSkill4
      this.requiredSkill5 = this.company.requiredSkill5
      this.welcomedSkill1 = this.company.welcomedSkill1
      this.welcomedSkill2 = this.company.welcomedSkill2
      this.welcomedSkill3 = this.company.welcomedSkill3
      this.welcomedSkill4 = this.company.welcomedSkill4
      this.welcomedSkill5 = this.company.welcomedSkill5
    },
    createObject() {
      const targetData = {
        name : this.name,
        link : this.link,
        address : this.address,
        job : this.job,
        usedSkills : this.usedSkills,
        requiredSkill1 : this.requiredSkill1,
        requiredSkill2 : this.requiredSkill2,
        requiredSkill3 : this.requiredSkill3,
        requiredSkill4 : this.requiredSkill4,
        requiredSkill5 : this.requiredSkill5,
        welcomedSkill1 : this.welcomedSkill1,
        welcomedSkill2 : this.welcomedSkill2,
        welcomedSkill3 : this.welcomedSkill3,
        welcomedSkill4 : this.welcomedSkill4,
        welcomedSkill5 : this.welcomedSkill5
      }
      this.createTargetData({ company : targetData })
    },
    addSkill() {
      if (this.usedSkillsSearch === null || this.usedSkillsSearch === '') {
        this.$refs.usedSkillsSearch.focus();
        return
      }
      if (this.usedSkills.find(item => item.key === this.usedSkillsSearch)) {
        this.usedSkillsSearch = ''
        this.$refs.usedSkillsSearch.focus();
        return
      }
      const skillLower = this.usedSkillsSearch.toLowerCase()
      let path = null
      try {
        path = require(`../../assets/img/${skillLower}.svg`)
      } catch(e) {
        path = null
      }
      const params = { key : this.usedSkillsSearch, path : path }
      this.usedSkills.push(params)
      this.usedSkillsSearch = ''
      this.createObject()
      this.$refs.usedSkillsSearch.focus();
    },
    deleteSkill(skillKey) {
      for (let i in this.usedSkills) {
        if (this.usedSkills[i].key === skillKey) {
          this.usedSkills.splice(i, 1)
          this.createObject()
          break;
        }
      }
    },
    onChange() {
      this.createObject()
    },
    ...mapActions(['createTargetData'])
  }
}
</script>

<style scoped>
ul {
  display: flex;
  margin-bottom: 20px;
}
.column {
  width: 160px;
}
#link {
  width: 300px;
}
#address {
  width: 450px;
}
#usedSkills {
  display: flex;
  flex-wrap: wrap;
  width: 485px;
  padding: 10px;
  border: solid 1px #c0c0c0;
  border-radius: 2px;
  background-color: #f0f8ff;
}
#usedSkills div {
  margin: 3px;
  padding: 7px 10px;
  font-size: 12px;
  background-color: #000000;
  border-radius: 2px;
  box-shadow: 2px 2px 2px rgba(0,0,0,0.4);
}
#usedSkills span {
  color: #ffffff;
}
.requiredSkill {
  width: 500px;
  margin-bottom: 10px;
}
.welcomedSkill {
  width: 500px;
  margin-bottom: 10px;
}
@media (max-width: 480px){
  .column {
    width: 70px;
    margin-right: 10px;
  }
  input {
    width: 220px !important;
  }
  #usedSkills {
    width: 270px;
    padding: 10px;
  }
}
</style>

Firebaseでのデータ構造

今回は会社情報を以下のJSON形式で登録。

usedSkillsは配列形式でデータ登録。

FirebaseへのCRUD操作

axiosで各種メソッド(GET/POST/PUT/DELETE)を実装。

index.jsでVuex実装。

export const state = () => ({
  companys : [],
  company : null,
  targetData : null,
  index : null
})

export const getters = {
  companys : (state) => state.companys,
  company : (state) => state.company,
  targetData : (state) => state.targetData,
  index : (state) => state.index
}

export const mutations = {
  setCompanys(state, { companys }) {
    state.companys = companys
  },
  setCompany(state, { company }) {
    state.company = company
  },
  setTargetData(state, { targetData }) {
    state.targetData = targetData
  },
  setIndex(state, { index }) {
    state.index = index
  }
}
  • 検索(GET)
export const actions = {
  async fetchCompanys({ commit }) {
    const companys = await this.$axios.$get(`companys.json`)
    commit('setCompanys', { companys })
  }
}
  • 登録(POST)
export const actions = {
  async registCompany({ commit }) { |
    const targetData = this.getters['targetData'] |
    await this.$axios.$post(`/companys.json`, targetData) |
  }
}
  • 更新(PUT)
export const actions = {
  async updateCompany({ commit }) {
    const targetData = this.getters['targetData']
    const index = this.getters['index']
    await this.$axios.$put(`/companys/${index}.json`, targetData)
  }
}
  • 削除(DELET)
export const actions = {
  async deleteCompany({ commit }, { index }) {
    await this.$axios.$delete(`/companys/${index}.json`)
    const companys = await this.$axios.$get(`/companys.json?`)
    commit('setCompanys', { companys })
  }
}

今後の課題

全て自前で作ると結構時間がかかりますが、フロントに集中出来るので随分時間は短縮できます。

が、Firebase自体が新しい技術なのでネットのノウハウも少なく、データ構造周りの設計のベストプラクティスが分からず…また今回諦めましたが、Algoliaと連携させると検索もイケてるサービスに出来そう。

あとは認証周りでしょうか…それらを差し引いても可能性を感じます。

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

nth系擬似クラスのジェネレータを作成しました。

リンク

nth-of-type generator

目的

CSSの疑似クラス nth-child / nth-last-child / nth-of-type / nty-last-of-type について、
感覚的に理解するためのジェネレータです。 CSSの学習にご活用ください。

使い方

  1. 縦方向に表示する要素の数を入力する。
  2. 横方向に表示する要素の数を入力する。
  3. テストしたい擬似クラスを選択する。
  4. 擬似クラスのカッコ内の指定方法を入力する。(odd、even、3n+1など)
  5. 生成された要素をクリックすると、擬似クラスの対象外に指定することができる。

背景

Vue.jsの練習用に、かんたんなジェネレータを作成しようと思いました。
nth-of-typeのジェネレータだったら、自分の仕事でも使えるし、うまくいけば広告収入も得られるんじゃ?と考えて作成してみました。
(後々ググったら、すでに作ってる人いましたけどね。)

感想

おもってたよりは時間かかったけど、
リアクティブなJavaScriptフレームワークや、flexboxや、Github Pagesのおかげで、
こういうジェネレータを作って公開するのがすごく楽になったのだと思います。
自分のしごとのスタイルにあわせた形の、自分用のジェネレータが気軽につくれるのはいいことだと思いました。

お願い

収益化の知識が一切なく、経験がほしいので、よかったら広告クリックしてください。
ていうか収益化の知識とかコメントください。
あと、ジェネレータについても、ここをこうしたらもっとわかりやすいとかのアドバイスもぜひ!

(スマホ用に調整してないので、スマホからだとUI微妙ですね。時間あるときに改善します。)

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