20200530のvue.jsに関する記事は9件です。

Webサービス『フェスメーカー』をリリースした話と、デザインに自信の無いエンジニアに向けたWebサービスを少しでもおしゃれに見せるテクニック

フェスメーカー画像

先日、新しいWebサービス「フェスメーカー」をリリースしました!

あのインクを打ち合うゲームをしている方なら分かってくれると思うのですが、その名の通り、あのゲームの「フェス」を自分たちで勝手に開催したり、参加したりするWebサービスとなっております。気軽に遊んで頂ければと思います!


さて、自分語りになってしまいますが、私は個人開発を始めてから1年間で4つほどWebサービスをリリースしてきました。(そして、ユーザが定着しないまま2つのWebサービスを閉鎖しました...)

個人開発はその名の通り個人ですべて開発しなければなりません。

私は本業としてエンジニアをしていますが、普段はほとんどコードを書いたりテストを行っているので、デプロイの設定など、普段の業務ではあまり経験しないことも、個人開発では自分で行わなければなりません。
そして「エンジニアの本業ではないが、最も個人開発中に頭を悩ませるもの」それがデザインだと思っています。

個人開発を始めた頃はCSSをいじって→実際の表示を確認して→いじって→一旦寝かせて→やっぱり気に入らなくて→いじって...みたいなことを何回もしてしまっていました。(今もちょいちょいやってしまいますが)

今回は私が個人開発を通していく中で身につけた、お手軽にデザインをレベルアップさせられるテクニックをいくつか紹介したいと思います。あまりデザインに自信のないエンジニアの方に1つでも参考になるものがあれば幸いです!

注意
偉そうなタイトルを付けていますが、自分は非デザイナーです。デザインが分からない人が、分からない人なりに実践で身につけたテクニックなので、あまり知識的なことは書けていません。予めご了承ください。

CSSフレームワークを上手に扱うコツ

bootstrap,vuetify,tailwindcssなど、デザインをお助けしてくれるさまざまなCSSフレームワークがありますが、使いこなせないとダサいデザインになります(いわゆる「bootstrap臭」がする状態)

フレームワークで準備されているコンポーネントを使いすぎない

「...あまり強いコンポーネントを使うなよ。弱く見えるぞ
ということで、いろいろ便利なコンポーネントは揃っているとは思うのですが、あまり使いすぎると「なんかどこかで見たことあるようなページだな」という印象を持ってしまいます。
また、bootstrapのジャンボトロンなど、臭いコンポーネントは特に取り扱い注意だと思います。
(正直、自分もまだ色々なコンポーネントに頼ってしまっている部分があるので、この辺りは自分に向けて言っている部分でもあります...)

フォントを変更する

英字オンリーのサイトであればフレームワーク側で定義されているデフォルトのフォントでも問題ないのですが、日本語フォントを組み合わせると大体ダサくなります。font-familyのおすすめ設定などがありますので日本語でもきれいに表示されるようにしましょう。

2020年流行?font-familyのオススメ設定はこれ!【CSS】
2020年に最適なfont-familyの書き方

きちんと整列させる

ノンデザイナーズ・デザインブックという、まさにこのネタにうってつけの本があるのですが、とにかくしつこいほどテキストは「整列」させなさい、と言われています。

この画像の赤線のように、テキストの外側に沿った直線を描けるようにテキストを整列させると自然とスッキリ見えます。

Slice 1.png

中央揃えの使いすぎに注意

テキストの整列で気をつけたいのが「中央揃え」で、中央揃えをどこでも使うようになってしまうと、上の画像のように整列させることが難しくなってしまいます。(ノンデザイナーズ・デザインブックにも「中央揃えを多用すると全体的な整列が失われる」という記述があります)
ついつい中央揃えを使いがちになってしまいますが、見出しなど目立たせたい部分にのみとどめておくほうが良さそうです。

避けては通れぬレスポンシブ

GoogleAnalyticsでサイトのデバイス別セッション数を見てみると大体6,7割がモバイル端末、2,3割がPCといった結果になります。
エンジニアをしているとPCに触る時間が多いのであまり実感できませんが、スマートフォンで閲覧される方のほうがとても多いです。
スマートフォンで閲覧しても、PCで閲覧しても、できるだけ違和感を少なくして表示させるようにしましょう。

基本的にスマートフォンでは横幅いっぱい、PCでは全体の横幅に対して50%~70%に要素をとどめておくと自然な表示になると思います。

以下はvuetifyで実装した際の例です。(他のフレームワークにおいてもGrid関連のページを調べていただければ、同様のものが再現できると思います)

<v-container fluid>
  <v-row>
    <v-col cols="12" md="8" offset-md="2" lg="6" offset-lg="3">
      ...      
    </v-col>
  </v-row>
</v-container>

白を「#FFFFFF」にしない、黒を「#000000」にしない

白黒に限らず、原色の色を使うのはできる限りやめましょうということです。
表現が難しいですがのっぺりした印象になってしまいます。

原色の場合

div {
  background-color: #000000;
  color: #FFFFFF;
}

image.png

フェスメーカーの場合

div {
  background-color: #212121;
  color: #F0F0F0;
}

image.png

比較結果がちょっと地味な結果になってしまいましたが、下の方がほんの少し見やすいですよね。

アイコンは積極的に使う

デザイン云々よりUI寄りの話ですが、アイコンを積極的に使ってできるだけユーザに分かりやすくしましょう。
実装している自分からすればしつこいぐらい分かりやすくしたほうが、初めて訪れたユーザにとって丁度いいはずです。

「テキストのみ」と「テキスト+アイコン」では後者の方が圧倒的にユーザに対する説明力が高いと思います。

以下のコードはフェスメーカーでのログインボタン部分です。ボタンに各SNSアカウントを示したアイコンを付加したものにしています。
(アイコンはFontAwesomeのものを使用しています)

<v-col cols="12">
  <v-btn href="/login/twitter" large color="#1DA1F2" width="230" dark>
    <v-icon small>fab fa-twitter</v-icon>
    <span class="ml-2">Twitterでログイン</span>
  </v-btn>
</v-col>
<v-col cols="12">
  <v-btn href="/login/facebook" large color="#4267B2" width="230" dark>
    <v-icon small>fab fa-facebook-f</v-icon>
    <span class="ml-2">Facebookでログイン</span>
  </v-btn>
</v-col>
<v-col cols="12">
  <v-btn href="/login/google" large color="#dd4b39" width="230" dark>
    <v-icon small>fab fa-google</v-icon>
    <span class="ml-2">Googleでログイン</span>
  </v-btn>
</v-col>

実際に表示されるログインボタン↓
image.png

おわりに

いろいろ書きましたが、他のwebサービスにはもっといろんなテクニックが隠されていると思います。(このqiitaにもいろいろ参考できるテクニックが隠されています。実際にフェスメーカーのトップページ上部はかなりqiitaを参考にしました)
開発中にデザインで行き詰まったら、他のwebサービスを参考にしてとりあえず真似してみるとだいたい上手く行くと思います。
(他のことにも言えると思いますが、あまりちゃんとした知識のないまま自己流でやり続けると、目も当てられないものになりますからね)

ではでは、ここまで読んでいただきありがとうございました!
みなさんもぜひ個人開発してみましょう!

フェスメーカー
筆者のtwitter

参考URL

うわっ、私のサイトBootstrapくさすぎ!? たった数文字変えるだけでBootstrapのくさみが抜ける7つのCSSテクニック。

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

Nuxt.jsとFirebaseで個人開発アプリをシェアできるウェブサービスを作った話

はじめに

4月で2年目になったウェブエンジニアです。普段はFEとBEを3:7くらいの割合でコードを書いています。
1年目は仕事で使っている技術やノウハウを学ぶことで精一杯でしたが、最近は仕事をこなしていく中で興味を持った技術について勉強する余裕が出てきました。
そこで、1年間ウェブエンジニアとして仕事をし、学んだ知識を生かして自分で1からウェブサービスを作ってみようと思い立ちました。
この記事では、備忘録という意味合いを兼ねつつ、なぜ、どのようにして、どんなものを作ったのかを書いていこうと思います。

作ったサービス

アプリガーデンという個人開発アプリをシェアできるサービスを作りました。
Twitter、Google、Githubのいずれかのアカウントがあれば簡単にユーザー登録ができ、アプリを投稿することができます。
また、ユーザー登録をせずに他の方々が作ったアプリを見るだけということも可能です。
スクリーンショット 2020-05-28 20.24.13.png

昨今、プログラミングへの関心が高まり、学びやすい環境になり個人でアプリケーションを開発するハードルが下がったと感じています。
アプリが完成したら公開してできるだけ多くの人に使ってほしいと思うのがエンジニアの性ですが、宣伝という壁が立ちはだかります。
法人であれば広告を出稿したりプレス掲載したりという方法を取ることができますが個人で行うとなると難しいのではないかと思います。
そんな個人開発エンジニアの方々が気軽に、簡単に作ったアプリをシェアできる場を作りたいなと思ったのがアプリガーデンを開発したきっかけです。
アプリをシェアすることはもちろん、ユーザー登録をせずにアプリを見るだけということも可能で、他のエンジニアが作ったアプリを見てモチベーションをもらうという使い方もできます。

システムアーキテクチャ

architecture.001.png

フロントエンドにNuxt.js、バックエンドにFirebase、CI/CDにはGitHub Actionsを利用しています。
UIフレームワークにはVuetifyを採用しています。コンポーネントが豊富で日本語のドキュメントが充実しているのでとても使いやすいです。
バックエンドはできるだけコードを書く量を減らしたかったので全てFirebaseでまかなっています。ウェブコンソールで1つプロジェクトを作ることで様々なプラットフォームを利用できるので管理も楽になります。
今回はSNS認証のためにAuthentication、アプリ情報やユーザー情報を保存するためにFirestore、ウェブサイトとして公開するためにHostingを利用しています。独自ドメインへの接続も含めて全て無料プランで利用することができます。

Firebase Authentication
Firebase Firestore
Firebase Hosting

Firebase HostingへのデプロイはGitHub Actionsで自動化しています。
releaseブランチへのコミットがトリガーとなりビルドからデプロイまで自動で行われるようにしています。

GitHub Actions

おわりに

実装期間は主に土日に作業をして1ヶ月ほどでした。
Firebaseのおかげでバックエンドはウェブコンソールからぽちぽちするだけで環境を作ることができるので楽をすることができました。
今のところ最低限の機能しか実装できていないのでより使いやすくなるように拡張をしていきたいと思います。

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

Vue.js + TypeScript で TypeSafe に Vuex したいんじゃぁ…

Vuex 使うとコード補完が効かなくなりがちです。
ということで可能な限り補完を効かせる挑戦をしてみましょう。

ちなみに、Module まで考え始めると詰みます。これはモジュールに対応していない記事になります。

TypeScript のコンソールアプリに Vue と Vuex を追加して以下のようなコードを書きました。
こういうの試すときにコンソールアプリでいけるのは、まじで神ってる。

index.ts
import Vuex from 'vuex';
import Vue from 'vue';

Vue.use(Vuex);

// 何も考えずに書いた
const store = new Vuex.Store({
    state: {
        count: 0,
    },
    mutations: {
        countup(state, payload: number) {
            state.count += payload;
        }
    },
    getters: {
        square(state) {
            return state.count * state.count;
        }
    },
    actions: {
        execute({ commit }, payload: { count: number; amount: number; }) {
            let count = 0;
            const handle = setInterval(() => {
                if (count < payload.count) {
                    count++;
                    commit('countup', payload.amount);
                } else {
                    clearInterval(handle);
                }
            }, 1000);
        },
    },
});

// 値が変わった時の処理を追加しておく(じゃないと何も表示されないので)
store.watch(x => x.count, (value, oldValue) => {
    console.log(`The value changed to ${value} from ${oldValue}. double is ${store.getters.square}`);
});

// ディスパッチ
store.dispatch('execute', { count: 3, amount: 10 });
// もしくは↓
// store.dispatch({
//     type: 'execute',
//     count: 3,
//     amount: 10,
// });

まず、dispatch と commit と getters でタイプセーフじゃないのが嫌ですね。本当に嫌!
ということで、世の中でタイプセーフにするにはどうしてるのか探してみたら 4 年前の記事になるのですが、以下の記事を見つけました。

https://qiita.com/wonderful_panda/items/752793cd1a8de0150610

これでいいのでは?という気持ちもありますが Store の型定義を変えてるのが気になりました。
いや、まぁそれでいいんですが、じゃぁ Store の型定義を書き換えないとどうなる??というのを試したくなったので以下のようにやってみました。

typesafe-vuex-generator.ts
import { ActionContext } from 'vuex';

export type TypedMutationTree<TMutations, TState> = {
    [key in keyof TMutations]: (state: TState, payload: TMutations[key]) => void;
}

export type TypedActionTree<TActions, TState> = {
    [key in keyof TActions]: (context: ActionContext<TState, TState>, payload: TActions[key]) => void;
}

interface PayloadWityTypeGeneratorType<T> {
    <K extends keyof T, P extends T[K]>(type: K, payload: P): { type: K; } & P;
}

class PayloadWithTypeGenerator<T> {
    create: PayloadWityTypeGeneratorType<T> = (type, payload) => {
        return { type, ...payload };
    }
}

export function createPayloadWithTypeGenerator<T>(): PayloadWityTypeGeneratorType<T> {
    return new PayloadWithTypeGenerator<T>().create;
}

これを使うと、こんな感じになる。

index.ts
import Vuex, { ActionContext, GetterTree } from 'vuex';
import Vue from 'vue';
import * as TypesafeVuex from './typesafe-vuex-generator';

Vue.use(Vuex);

// State の型定義
interface MyState {
    count: number;
}

// Mutations の型定義みたいな役割
type MyMutations = {
    // name: type of payload
    countup: { amount: number }; // mutation の payload はオブジェクトである必要がある(number とかはだめ)
}

// Actions の型定義みたいな役割
type MyActions = {
    // name: type of payload
    execute: { count: number, amount: number }; // action の payload はオブジェクトである必要がある(number とかはだめ)
}

// TypedMutationTree から作ると型をちゃんと認識してもらえる
const mutations: TypesafeVuex.TypedMutationTree<MyMutations, MyState> = {
    countup(state, payload) {
        state.count += payload.amount;
    }
};

// commit と dispatch に渡す payloadWityType を生成する人
const action = TypesafeVuex.createPayloadWithTypeGenerator<MyActions>();
const mutation = TypesafeVuex.createPayloadWithTypeGenerator<MyMutations>();

const actions: TypesafeVuex.TypedActionTree<MyActions, MyState> = {
    execute({ commit }, payload) {
        let count = 0;
        const handle = setInterval(() => {
            if (count < payload.count) {
                count++;
                // commit の引数は上で定義した mutation を使うと補完が効く
                commit(mutation("countup", { amount: payload.amount }));
            } else {
                clearInterval(handle);
            }
        }, 1000);
    },
};

const getters: GetterTree<MyState, MyState> = {
    square(state) {
        return state.count * state.count;
    } 
};

const store = new Vuex.Store<MyState>({
    state: {
        count: 0,
    },
    mutations,
    getters,
    actions,
});

// 値が変わった時の処理を追加しておく(じゃないと何も表示されないので)
store.watch(x => x.count, (value, oldValue) => {
    // getters は any のまま…
    console.log(`The value changed to ${value} from ${oldValue}. double is ${store.getters.square}`);
});

// ディスパッチの引数は上で定義した action で作ると補完が効く
store.dispatch(action("execute", { count: 3, amount: 10 }));

イケてない制約として Mutation の payload に number とかのプリミティブ型を使えないというのがあります…。う~ん。

元記事の人のように、Store の型自体をいじって commit や dispatch のシグネチャを変えないとこれ以上は無理っぽい?
ということで元記事の人のコードを参考に、もうちょっとだけ汎用的に使えるように typesafe-vuex.ts というものを作ってみました。

typesafe-vuex.ts
import Vuex, { ActionContext, MutationTree, ActionTree, GetterTree, CommitOptions, StoreOptions } from 'vuex';

export type TypedMutationTree<TState, TMutations> = {
    [key in keyof TMutations]: (state: TState, payload: TMutations[key]) => void;
}

export type TypedActionTree<TState, TMutations, TActions> = {
    [key in keyof TActions]: (context: TypesafeActionContext<TState, TActions, TMutations>, payload: TActions[key]) => void;
}

export type TypedGetterTree<TState, TGetters> = {
    [key in keyof TGetters]: (state: TState) => TGetters[key];
}

export type PayloadWithType<T, K extends keyof T> = {
    type: K
} & T

interface TypesafeDispatch<TActions> {
    <K extends keyof TActions>(type: K, payload: TActions[K]): Promise<any>;
    <K extends keyof TActions>(payloadWithType: PayloadWithType<TActions, K>): Promise<any>;
}

interface TypesafeCommit<TMutations> {
    <K extends keyof TMutations>(type: K, payload: TMutations[K], options?: CommitOptions): void;
    <K extends keyof TMutations>(payloadWithType: PayloadWithType<TMutations, K>, options?: CommitOptions): void;
}

interface TypesafeActionContext<TState, TActions, TMutations> extends ActionContext<TState, TState> {
    dispatch: TypesafeDispatch<TActions>;
    commit: TypesafeCommit<TMutations>;
}

export declare class TypesafeVuexStore<TState, TMutations, TActions, TGetters> extends Vuex.Store<TState> {
    mutations?: TypedMutationTree<TState, TMutations>;
    actions?: TypedActionTree<TState, TMutations, TActions>;
    readonly getters: TGetters;

    dispatch: TypesafeDispatch<TActions>;
    commit: TypesafeCommit<TMutations>;
}

これを使うと以下のように書けます。補完つきで。

index.ts
import Vuex from 'vuex';
import Vue from 'vue';
import * as TypesafeVuex from './typesafe-vuex';

Vue.use(Vuex);

// State の型定義
interface MyState {
    count: number;
}

// mutations の型定義みたいな役割
type MyMutations = {
    // name: type of payload
    countup: number;
}

// actions の型定義みたいな役割
type MyActions = {
    // name: type of payload
    execute: { count: number, amount: number };
}

// getters の型定義みたいな役割
type MyGetters = {
    square: number;
}

// 実際に使う Store の型
type MyStore = TypesafeVuex.TypesafeVuexStore<MyState, MyMutations, MyActions, MyGetters>;

// TypedMutationTree から作ると型をちゃんと認識してもらえる
const mutations: TypesafeVuex.TypedMutationTree<MyState, MyMutations> = {
    countup(state, payload) {
        state.count += payload;
    }
};

const actions: TypesafeVuex.TypedActionTree<MyState, MyMutations, MyActions> = {
    execute({ commit }, payload) {
        let count = 0;
        const handle = setInterval(() => {
            if (count < payload.count) {
                count++;
                // commit も補完が効く
                commit("countup", payload.amount );
            } else {
                clearInterval(handle);
            }
        }, 1000);
    },
};

const getters: TypesafeVuex.TypedGetterTree<MyState, MyGetters> = {
    square(state) {
        return state.count * state.count;
    } 
};

const store: MyStore = new Vuex.Store<MyState>({
    state: {
        count: 0,
    },
    mutations,
    getters,
    actions,
});

// 値が変わった時の処理を追加しておく(じゃないと何も表示されないので)
store.watch(x => x.count, (value, oldValue) => {
    // getters も type safe
    console.log(`The value changed to ${value} from ${oldValue}. double is ${store.getters.square}`);
});

// ディスパッチも補完が効く
store.dispatch("execute", { count: 3, amount: 10 });

モジュールに対応しようとすると、もうひと頑張りいりそうだけど、これと同じ要領でいけるかな???
あと、mapGetters とかを使った時の補完はあきらめた。

まとめ

頭の体操みたいで楽しいですね。

というか、もっといい感じにやる方法があれば知りたい。

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

TypeScript で TypeSafe に Vuex したいんじゃぁ…

Vuex 使うとコード補完が効かなくなりがちです。
ということで可能な限り補完を効かせる挑戦をしてみましょう。

TypeScript のコンソールアプリに Vue と Vuex を追加して以下のようなコードを書きました。
こういうの試すときにコンソールアプリでいけるのは、まじで神ってる。

index.ts
import Vuex from 'vuex';
import Vue from 'vue';

Vue.use(Vuex);

// 何も考えずに書いた
const store = new Vuex.Store({
    state: {
        count: 0,
    },
    mutations: {
        countup(state, payload: number) {
            state.count += payload;
        }
    },
    getters: {
        square(state) {
            return state.count * state.count;
        }
    },
    actions: {
        execute({ commit }, payload: { count: number; amount: number; }) {
            let count = 0;
            const handle = setInterval(() => {
                if (count < payload.count) {
                    count++;
                    commit('countup', payload.amount);
                } else {
                    clearInterval(handle);
                }
            }, 1000);
        },
    },
});

// 値が変わった時の処理を追加しておく(じゃないと何も表示されないので)
store.watch(x => x.count, (value, oldValue) => {
    console.log(`The value changed to ${value} from ${oldValue}. double is ${store.getters.square}`);
});

// ディスパッチ
store.dispatch('execute', { count: 3, amount: 10 });
// もしくは↓
// store.dispatch({
//     type: 'execute',
//     count: 3,
//     amount: 10,
// });

まず、dispatch と commit と getters でタイプセーフじゃないのが嫌ですね。本当に嫌!
ということで、世の中でタイプセーフにするにはどうしてるのか探してみたら 4 年前の記事になるのですが、以下の記事を見つけました。

https://qiita.com/wonderful_panda/items/752793cd1a8de0150610

これでいいのでは?という気持ちもありますが Store の型定義を変えてるのが気になりました。
いや、まぁそれでいいんですが、じゃぁ Store の型定義を書き換えないとどうなる??というのを試したくなったので以下のようにやってみました。

typesafe-vuex-generator.ts
import { ActionContext } from 'vuex';

export type TypedMutationTree<TMutations, TState> = {
    [key in keyof TMutations]: (state: TState, payload: TMutations[key]) => void;
}

export type TypedActionTree<TActions, TState> = {
    [key in keyof TActions]: (context: ActionContext<TState, TState>, payload: TActions[key]) => void;
}

interface PayloadWityTypeGeneratorType<T> {
    <K extends keyof T, P extends T[K]>(type: K, payload: P): { type: K; } & P;
}

class PayloadWithTypeGenerator<T> {
    create: PayloadWityTypeGeneratorType<T> = (type, payload) => {
        return { type, ...payload };
    }
}

export function createPayloadWithTypeGenerator<T>(): PayloadWityTypeGeneratorType<T> {
    return new PayloadWithTypeGenerator<T>().create;
}

これを使うと、こんな感じになる。

index.ts
import Vuex, { ActionContext, GetterTree } from 'vuex';
import Vue from 'vue';
import * as TypesafeVuex from './typesafe-vuex-generator';

Vue.use(Vuex);

// State の型定義
interface MyState {
    count: number;
}

// Mutations の型定義みたいな役割
type MyMutations = {
    // name: type of payload
    countup: { amount: number }; // mutation の payload はオブジェクトである必要がある(number とかはだめ)
}

// Actions の型定義みたいな役割
type MyActions = {
    // name: type of payload
    execute: { count: number, amount: number }; // action の payload はオブジェクトである必要がある(number とかはだめ)
}

// TypedMutationTree から作ると型をちゃんと認識してもらえる
const mutations: TypesafeVuex.TypedMutationTree<MyMutations, MyState> = {
    countup(state, payload) {
        state.count += payload.amount;
    }
};

// commit と dispatch に渡す payloadWityType を生成する人
const action = TypesafeVuex.createPayloadWithTypeGenerator<MyActions>();
const mutation = TypesafeVuex.createPayloadWithTypeGenerator<MyMutations>();

const actions: TypesafeVuex.TypedActionTree<MyActions, MyState> = {
    execute({ commit }, payload) {
        let count = 0;
        const handle = setInterval(() => {
            if (count < payload.count) {
                count++;
                // commit の引数は上で定義した mutation を使うと補完が効く
                commit(mutation("countup", { amount: payload.amount }));
            } else {
                clearInterval(handle);
            }
        }, 1000);
    },
};

const getters: GetterTree<MyState, MyState> = {
    square(state) {
        return state.count * state.count;
    } 
};

const store = new Vuex.Store<MyState>({
    state: {
        count: 0,
    },
    mutations,
    getters,
    actions,
});

// 値が変わった時の処理を追加しておく(じゃないと何も表示されないので)
store.watch(x => x.count, (value, oldValue) => {
    // getters は any のまま…
    console.log(`The value changed to ${value} from ${oldValue}. double is ${store.getters.square}`);
});

// ディスパッチの引数は上で定義した action で作ると補完が効く
store.dispatch(action("execute", { count: 3, amount: 10 }));

イケてない制約として Mutation の payload に number とかのプリミティブ型を使えないというのがあります…。う~ん。

元記事の人のように、Store の型自体をいじって commit や dispatch のシグネチャを変えないとこれ以上は無理っぽい?
ということで元記事の人のコードを参考に、もうちょっとだけ汎用的に使えるように typesafe-vuex.ts というものを作ってみました。

typesafe-vuex.ts
import Vuex, { ActionContext, MutationTree, ActionTree, GetterTree, CommitOptions, StoreOptions } from 'vuex';

export type TypedMutationTree<TState, TMutations> = {
    [key in keyof TMutations]: (state: TState, payload: TMutations[key]) => void;
}

export type TypedActionTree<TState, TMutations, TActions> = {
    [key in keyof TActions]: (context: TypesafeActionContext<TState, TActions, TMutations>, payload: TActions[key]) => void;
}

export type TypedGetterTree<TState, TGetters> = {
    [key in keyof TGetters]: (state: TState) => TGetters[key];
}

export type PayloadWithType<T, K extends keyof T> = {
    type: K
} & T

interface TypesafeDispatch<TActions> {
    <K extends keyof TActions>(type: K, payload: TActions[K]): Promise<any>;
    <K extends keyof TActions>(payloadWithType: PayloadWithType<TActions, K>): Promise<any>;
}

interface TypesafeCommit<TMutations> {
    <K extends keyof TMutations>(type: K, payload: TMutations[K], options?: CommitOptions): void;
    <K extends keyof TMutations>(payloadWithType: PayloadWithType<TMutations, K>, options?: CommitOptions): void;
}

interface TypesafeActionContext<TState, TActions, TMutations> extends ActionContext<TState, TState> {
    dispatch: TypesafeDispatch<TActions>;
    commit: TypesafeCommit<TMutations>;
}

export declare class TypesafeVuexStore<TState, TMutations, TActions, TGetters> extends Vuex.Store<TState> {
    mutations?: TypedMutationTree<TState, TMutations>;
    actions?: TypedActionTree<TState, TMutations, TActions>;
    readonly getters: TGetters;

    dispatch: TypesafeDispatch<TActions>;
    commit: TypesafeCommit<TMutations>;
}

これを使うと以下のように書けます。補完つきで。

index.ts
import Vuex from 'vuex';
import Vue from 'vue';
import * as TypesafeVuex from './typesafe-vuex';

Vue.use(Vuex);

// State の型定義
interface MyState {
    count: number;
}

// mutations の型定義みたいな役割
type MyMutations = {
    // name: type of payload
    countup: number;
}

// actions の型定義みたいな役割
type MyActions = {
    // name: type of payload
    execute: { count: number, amount: number };
}

// getters の型定義みたいな役割
type MyGetters = {
    square: number;
}

// 実際に使う Store の型
type MyStore = TypesafeVuex.TypesafeVuexStore<MyState, MyMutations, MyActions, MyGetters>;

// TypedMutationTree から作ると型をちゃんと認識してもらえる
const mutations: TypesafeVuex.TypedMutationTree<MyState, MyMutations> = {
    countup(state, payload) {
        state.count += payload;
    }
};

const actions: TypesafeVuex.TypedActionTree<MyState, MyMutations, MyActions> = {
    execute({ commit }, payload) {
        let count = 0;
        const handle = setInterval(() => {
            if (count < payload.count) {
                count++;
                // commit も補完が効く
                commit("countup", payload.amount );
            } else {
                clearInterval(handle);
            }
        }, 1000);
    },
};

const getters: TypesafeVuex.TypedGetterTree<MyState, MyGetters> = {
    square(state) {
        return state.count * state.count;
    } 
};

const store: MyStore = new Vuex.Store<MyState>({
    state: {
        count: 0,
    },
    mutations,
    getters,
    actions,
});

// 値が変わった時の処理を追加しておく(じゃないと何も表示されないので)
store.watch(x => x.count, (value, oldValue) => {
    // getters も type safe
    console.log(`The value changed to ${value} from ${oldValue}. double is ${store.getters.square}`);
});

// ディスパッチも補完が効く
store.dispatch("execute", { count: 3, amount: 10 });

モジュールに対応しようとすると、もうひと頑張りいりそうだけど、これと同じ要領でいけるかな???
あと、mapGetters とかを使った時の補完はあきらめた。

まとめ

頭の体操みたいで楽しいですね。

というか、もっといい感じにやる方法があれば知りたい。

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

AzureとLINEとVue.jsで投稿画像のHappiness数値判定Botを作ってみた

はじめに

エンジニアが披露宴の余興を頼まれたら

こちらの記事に感化されて、Microsoft Azure で作成してみました。

コードは、Github に置いてあります。

HappinessChecker

動作

【LINEとランキングページ】

line01.png

【ダウンロード機能】

line02.png

参加者で投稿画像を共有できるように、ダウンロード機能を設けています

【リアルタイム通信】

line03.gif

上記わかりづらいですが、異なるブラウザでランキングページを表示して、スマホから更新している状態です。

ランキングページでは「新しい画像が投稿」された場合と「いいね!ボタン」が押下されたときに、

ブラウザを更新なしで、変更がリアルタイムで反映されるようにしています

ちなみに、「いいね!ボタン」はユーザ管理をしていないので、いいね!と思った分だけ押下可能です。

アーキテクチャ図

architecture.png

LINEとの連携部分はDurable Functions を使い、Vue.js との連携部分はAzureFunctionsで使っています。

工夫した点

IHttpClientFactory

LINE との連携部分では、HttpClient で通信を行っています。

このクラスでは IDisposable が実装されますが、これを using ステートメント内で宣言およびインスタンス化することはお勧めできません。その理由は、HttpClient オブジェクトが破棄されても、基になるソケットがすぐに解放されず、ソケットの枯渇の問題が発生する可能性があるということにあります。

ドキュメントによるとHttpClient のインスタンスは使いまわしを行うことがベストプラクティスのため、IHttpClientFactory を DI して使うようにしました。

Startup.cs
using HappinessFunctionApp.Common;
using HappinessFunctionApp.Extension;
using Microsoft.Azure.Documents.Client;
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using System;

[assembly: FunctionsStartup(typeof(HappinessFunctionApp.Startup))]
namespace HappinessFunctionApp
{
    public class Startup : FunctionsStartup
    {
        // 依存関係
        public override void Configure(IFunctionsHostBuilder builder)
        {
            // IHttpClientFactory を使用する
            builder.Services.AddHttpClient<HttpClientService>();
            builder.Services.AddSingleton(provider =>
            {
                ConnectionPolicy ConnectionPolicy = new ConnectionPolicy
                {
                    ConnectionMode = ConnectionMode.Direct,
                    ConnectionProtocol = Protocol.Tcp
                };

                return new DocumentClient(new Uri(AppSettings.Instance.COSMOSDB_ENDPOINT), AppSettings.Instance.COSMOSDB_KEY, ConnectionPolicy);
            });
        }
    }
}

  • キーとして文字列を使用する必要なしに、名前付きクライアントと同じ機能を提供します。
  • クライアントを使用するときに、IntelliSense とコンパイラのヘルプが提供されます。
  • 特定の HttpClient を構成してそれと対話する 1 つの場所を提供します。 たとえば、単一の型指定されたクライアントは、次のために使用 される場合があります。
    • 単一のバックエンド エンドポイント用。
    • エンドポイントを処理するすべてのロジックをカプセル化するため。
  • DI に対応しており、アプリ内の必要な場所に挿入できます。

今回の用途的に「型指定されたクライアント」で実装しました。

HttpClientService
using HappinessFunctionApp.Common;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace HappinessFunctionApp.Extension
{
    public class HttpClientService
    {
        private readonly HttpClient _client;

        public HttpClientService(HttpClient client)
        {
            _client = client;
        }
        public async Task<HttpResponseMessage> PostJsonAsync<T>(string requestUri, T value)
        {
            _client.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/json;");
            return await _client.PostAsJsonAsync(requestUri, value, CancellationToken.None).ConfigureAwait(false);
        }

        public async Task<HttpResponseMessage> PostLineJsonAsync<T>(string requestUri, T value)
        {
            _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", AppSettings.Instance.LINE_CHANNEL_ACCESS_TOKEN);
            _client.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/json;");
            return await _client.PostAsJsonAsync(requestUri, value, CancellationToken.None).ConfigureAwait(false);
        }

        public async Task<Stream> GetStreamAsync(string requestUri)
        {
            var response = await _client.GetAsync(requestUri);
            return response.Content.ReadAsStreamAsync().Result;
        }

        public async Task<string> GetAsync(string requestUri)
        {
            var response = await _client.GetAsync(requestUri);
            return response.Content.ReadAsStringAsync().Result;
        }
    }
}


DocumentClient

各 DocumentClient インスタンスと CosmosClient インスタンスはスレッドセーフであり、直接モードで動作しているときには効率的な接続管理とアドレスのキャッシュが実行されます。 効率的な接続管理と SDK クライアントのパフォーマンス向上を実現するために、アプリケーションの有効期間中は、AppDomain ごとに単一のインスタンスを使用することをお勧めします。

DocumentClient インスタンスを使いまわすことが、ベストプラクティスのため、こちらも DI して使います。

Startup.cs
using HappinessFunctionApp.Common;
using HappinessFunctionApp.Extension;
using Microsoft.Azure.Documents.Client;
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using System;

[assembly: FunctionsStartup(typeof(HappinessFunctionApp.Startup))]
namespace HappinessFunctionApp
{
    public class Startup : FunctionsStartup
    {
        // 依存関係
        public override void Configure(IFunctionsHostBuilder builder)
        {
            // IHttpClientFactory を使用する
            builder.Services.AddHttpClient<HttpClientService>();
            builder.Services.AddSingleton(provider =>
            {
                ConnectionPolicy ConnectionPolicy = new ConnectionPolicy
                {
                    ConnectionMode = ConnectionMode.Direct,
                    ConnectionProtocol = Protocol.Tcp
                };

                return new DocumentClient(new Uri(AppSettings.Instance.COSMOSDB_ENDPOINT), AppSettings.Instance.COSMOSDB_KEY, ConnectionPolicy);
            });
        }
    }
}

Azure Functions Proxy

投稿された画像を保存しているストレージアカウントのエンドポイントを表示したくなかったので、
Azure Functions Proxy 経由で画像表示を行うようにした。

従量課金の AppService Plan だとコールドスタートなので、初回は画像表示に遅延が発生してしまうので、プラン変更を行うか、他の方法で実装すべきだったなと少し思っています。

Proxies.json

{
  "$schema": "http://json.schemastore.org/proxies",
  "proxies": {
    "img": {
      "matchCondition": {
        "methods": [ "GET" ],
        "route": "/image/{filename}"
      },
      "backendUri": "https://<your Strage Account endpoint>.blob.core.windows.net/uploadimage/{filename}"
    }
  }
}

Azure SignalR

今回、リアルタイムでのコンテンツ更新を行いたかったので Azure SignalR を使いました。

ランキング情報の json をそのまま配信したかったので、シンプルにデフォルトで実装しています。

SignalRFunction.cs
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Extensions.SignalRService;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;

namespace HappinessFunctionApp.Functions
{
    public static class SignalRFunction
    {
        [FunctionName("negotiate")]
        public static SignalRConnectionInfo GetSignalRInfo(
        [HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequest req,
        [SignalRConnectionInfo(HubName = "chat")] SignalRConnectionInfo connectionInfo)
        {
            return connectionInfo;
        }

        [FunctionName("SendMessage")]
        public static async Task SendMessage(
        [HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequest req,
        [SignalR(HubName = "chat")]IAsyncCollector<SignalRMessage> signalRMessages, ILogger log)
        {

            var data = await req.ReadAsStringAsync();

            await signalRMessages.AddAsync(new SignalRMessage
            {
                Target = "newMessage",
                Arguments = new[] { data }
            });
        }
    }
}


Vue側で、negotiate にリクエストを送信して接続情報を取得して、受け取った json を表示するだけ

Happiness.vue
 mounted () {
    // SignalRとコネクションを作成
    this.connection = new signalr.HubConnectionBuilder()
      .withUrl(process.env.VUE_APP_HOST)
      .configureLogging(signalr.LogLevel.Information)
      .build()
    console.log('connecting...')

    // SignalR Serviceへの接続
    this.connection
      .start()
      .then(() => console.log('connected!'))
      .catch(console.error)

    // SignalR Serviceへの接続
    this.connection.on('newMessage', (data) => {
      this.items = JSON.parse(data).images
      this.sumcnt = JSON.parse(data).images.length
      this.isLoading = false
      this.errored = false
      this.$emit('sumpicter', this.sumcnt)
      console.log(this.sumcnt)
      console.log(this.items)
    })
    // 切断
    this.connection.onclose(() => console.log('disconnected'))

動かすためにやること

LINE Developers に登録

以下を参照して、登録を行う

チャンネルの作成が完了したら「Channel secret」と「Channel access token」を控えておく

Azureポータル上の操作

Microsoft Azure で以下を作成する

  • Azure Functions
  • Azure Cosmos DB
  • SignalR
  • Cognitive Services Face
  • Storage Account

作成をしたら「キー」や「エンドポイント」を控えておく

storage01.png

ストレージアカウントの「BLOB」を使い、「uploadimage」というコンテナーを作成しておく

storage02.png

以降の local.settings.json の項目を AzureFunctions のアプリケーション設定に登録する

その際、SIGNALR_URL は Azure 上に作成した AzureFunctions のエンドポイントを設定する

applicationsetting.png

Happiness Function (C#)

local.settings.json

前述の作業で控えた「キー」と「エンドポイント」を設定する

ローカル実行する場合は、localhost の記載はそのままにしておく

※HOSTの記載は変更しない
 ローカルデバック時の vue.js との連携のため

local.settings.json
{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "AzureSignalRConnectionString": "<your SignalR endpoint>",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet",
    "FACE_SUBSCRIPTION_KEY": "<your faceApi subscription key>",
    "FACE_ENDPOINT": "https://<your faceApi endpoint>.cognitiveservices.azure.com/",
    "STORAGE_ACCOUNT_NAME": "<your storage account name>",
    "STORAGE_ACCESS_KEY": "<your storage subscription key>",
    "BLOB_NAME": "uploadimage",
    "COSMOSDB_ENDPOINT": "https://<your cosmos db endpoint>.documents.azure.com:443/",
    "COSMOSDB_KEY": "<your cosmos db subscription key>",
    "DATABASE_ID": "LineBotDb",
    "COLLECTION_ID": "HappinessInfo",
    "LINE_CHANNEL_ACCESS_TOKEN": "<your LINE Messaging API Access Token>",
    "LINE_CHANNEL_SECRET": "<your LINE Messaging API Channel Secret>",
    "LINE_POST_LIST": "https://<your Storage Static web site endpoint>.z11.web.core.windows.net/",
    "BLOB_URL": "https://<your Strage Account endpoint>.blob.core.windows.net/uploadimage/",
    "PROXY_URL": "https://<your azure function endpoint>.azurewebsites.net/image/",
    "SIGNALR_URL": "http://localhost:7071/api/SendMessage"
  },
  "Host": {
    "LocalHttpPort": 7071,
    "CORS": "http://localhost:8080",
    "CORSCredentials": true
  }
}

Proxies.json

投稿画像を保存するストレージアカウントのエンドポイントに書き換え

Proxies.json
{
  "$schema": "http://json.schemastore.org/proxies",
  "proxies": {
    "img": {
      "matchCondition": {
        "methods": [ "GET" ],
        "route": "/image/{filename}"
      },
      "backendUri": "https://<your Strage Account endpoint>.blob.core.windows.net/uploadimage/{filename}"
    }
  }
}

happiness_app (Vue.js)

prod.env.js

VUE_APP_HOST の書き換え

prod.env.js
'use strict'
module.exports = {
  NODE_ENV: '"production"',
  VUE_APP_HOST: '"https://<your azure function endpoint>.azurewebsites.net/api"'
}

ビルド方法

# install dependencies
npm install

# serve with hot reload at localhost:8080
npm run dev

# build for production with minification
npm run build

# build for production and view the bundle analyzer report
npm run build --report

Azure Storage にデプロイ

Azure Storage コンテナー に VScode からデプロイを行う

スクリーンショット (2).png

LINEにエンドポイントを設定する

Azure にデプロイした「LineBotHttpStart」のエンドポイントを設定すれば完了

Linesetting.png

所感

今回初めて、LINE Messaging API や Durable Function 、Cosmos DB 、SignalR 、Vue.js を使ってみました。

まだまだ Durable Function や vue.js の実装方法など今後も引き続き、勉強していくしかないなと感じました。

しかし、この個人プロジェクトかなり学びが多い!楽しかった!

参考

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

Vue.js 学習ロードマップ 無料で学べる教材リンク

これからVue.jsを学ぶ方のためにVue.js学習ロードマップを作成しました。初学者向けです。

  • Vueを学ぶための各種リンクをまとめています。
  • 上から順番に記事や、ハンズオンをやると理解しやすいように順番も考慮して構成しています。なるべく初学者でも理解しやすいようにわかりやすい記事をチョイスしているつもりです。
  • 深い内容の理解が目的ではなくVueの全体感を掴むのが目的です。

1. Vue.jsとは何か大枠を理解している

以下の記事を読む


2. MVVMとは何か理解している

Vue.jsは基本的にはMVVMパターンです。
Model、View、ViewModelがあり、ViewにModelを渡して双方向バインディングしている作りです。
データを書き換えるとUIが変わり、UIを書き換えるとデータが変わる。
そこをバインディングさせることが効率的で画期的だったという経緯があります。
ただ、それをやりすぎてしまった場合、例えばシングルページアプリケーションなどで1枚のページ内でいろいろなコンポーネントが複雑に動くようなUIを作った場合、モデルを引き渡してバインディング、バインディング、バインディング・・・とやっていくと、思わぬところで副作用が起きてしまうことがあります。
これは、Vue.jsというよりはMVVMのつらいところではありますね。

この課題を解決しようとしたのが、ReactのReduxやFluxです。
歴史を振り返ると、jQueryの時代が終わり、その後MVVMの時代が到来しました。
その後、FacebookがReactを発表しましたが、それと同時にFluxアーキテクチャという考えが登場しました。
これはMVVMのアーキテクチャを否定するもので、双方向バインディングにおいては、コンポーネントとデータを分離させてしまい、データをいじるなら必ずActionを通る設計がいいと提唱したんです。
これによりソースコードの記述量は増えますが、データの流れは一方向になる。複雑性が生まれにくいというメリットがありますね。
このような考え方は今でも主流になっています。Vue.jsにおいてはVuexが取り入れています。

VuexはまさにFlux、Reduxに影響されて設計されたもので、Vue.jsで前述の問題を解決するものです。

『Vue.js入門』の執筆者が語る、はじめてのVue.js。本腰入れる前に知りたい5つのポイント〜私のVue.jsへの想いをのせて〜 | flexy(フレキシー)より上記文章は引用

以下の記事を読む


3. Vue.jsの基礎を理解している

以下の記事を読む


4. コンポーネントとは何か理解している

以下の記事を読む


5. Vueのライフサイクルフックについて理解している

以下の記事を読む

ここからハンズオンが始まります

6. 開発ツールを導入できている

以下の記事を読み導入する


7. 以下のビデオ学習を実施し、実際にコードを書く。

以下を実施する

  • Vue.js入門 - YouTube
    • これはCDN経由でVue本体を読み込み利用するパターンの解説動画です。ちょこっと試しでVueを試してみたいというやり方のため実務ではこの方法は使いませんがVueを理解するには有用なので実施しましょう。

8. Vueのv-model、算出プロパティ(computed)、メソッド(methods)、監視プロパティ(watch)、クラスのバインドについて理解している

以下を実施する

以下の記事を読む


9. VueのPropsについて理解している

以下の記事を読む


10. Vue CLIの基本について理解している

以下の記事を読む


11. 以下のビデオ学習を実施し、実際にコードを書き、Vue.js、Vue Routerを利用した実装ができる(余裕があればVuexも理解できるとGood)

以下の15-19は少し内容が古くCLIが現在ではバージョン4にバージョンが上がっています。
以下の用にファイルの出力箇所が変更になっているので読み替えて実施してください。
    The default directory structure was changed:
    src/store.js moved to src/store/index.js;
    src/router.js renamed to src/router/index.js;
    また動画はWindows環境ですが適宜Macであればに読み替えてください。

以下を実施する

以下を読む


12. おさらいとして以下の記事に目を通している

以下の記事を読む


13. Vue.jsを利用するメリットについて理解している

以下の記事を読む

おつかれさまでした!

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

PHPと Vue.jsで簡易掲示板を作ろう〜その1(PHP編)〜

初めに

3つのパートに分けて
1.PHPでとりあえず動く掲示板を作る←今回はここ
2.SCSSで簡単なデザインを作る
3.Vue.jsで少しリッチな動きをつける

目次

・誰向けですか?
・このパートでの完成イメージとデモサイト
・PHPの記述と解説
・HTMLの記述と解説
・*の解説
・まとめ

誰向けですか?

・PHP初心者
・Vue.js初心者
・PHPで特に作りたいものがない人

このパートでの完成イメージとデモサイト

デモサイト
簡易掲示板へようこそ(XSS対策済み)

〜完成イメージ〜
ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
スクリーンショット 2020-05-30 17.54.32.png
ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
このようにクロスサイトスクリプティング(XSS)の対策をしています。

PHPの記述と解説

index.php
<?php

// htmlspecialchars()でHTMLで本来直接書くことのできない<>などの記号を書けるようにする
// メッセージ欄にscriptを記述する輩への対策(XSSの防止)*1
function h($v) {
    return htmlspecialchars($v, ENT_QUOTES, 'UTF-8');
}

$FILE = 'all_data.text'; // DBを使わずファイルに保存する
$user_id = uniqid(rand().'_'); // IDを作る(ランダムな数値_13文字の文字列)が出力される*2

date_default_timezone_set('Asia/Tokyo'); // ↓date()でどの地域の時間を使用するか決める
$date = date('Y年/m月/d日/H:i'); // date(Y,m,d,H,i)に↑で設定した時間が入る*3

$input_text = ''; // 入力するテキストを変数として代入

$post_data = []; // 一回の投稿のデータを入れる,($user_id, $date, $input_text)を入れる
$all_data = []; // 全ての投稿のデータを入れる

// file_exists(ファイル名)でこのファイルが存在するか調べる
if(file_exists($FILE)) {
    //json_decode()でJSON形式のデータを配列に戻す
    //file_get_contents()で$FILEを読み込む
    $all_data = json_decode(file_get_contents($FILE));
}

// $_SERVERにはHTTPサーバやHTTP通信に関わる情報が格納されている
// $_SERVER['REQUEST_METHOD']でブラウザからのリクエストがGETかPOSTか判別する*4
// もしPOSTのリクエストがあったら
if($_SERVER['REQUEST_METHOD'] === 'POST') {

    // リクエストを受け取った時textが空ではなかったら(送信ボタンが押された時)
    if(!empty($_POST['text'])) {

        // $post_textに投稿されたテキストを代入
        $input_text = $_POST['text'];

        // 新しく投稿されたデータ(データを配列にして入れる)HTMLで表示する時に使う
        $post_data = [$user_id, $date, $input_text];

        // 投稿をファイルに保存する
        $all_data[] = $post_data;

        // ファイルに保存する
        file_put_contents($FILE, json_encode($all_data));
    }

    //header()
    //$_SERVER['SCRIPT_NAME']は現在のスクリプトのパス(簡単に言うと今いる場所のURL)
    header('Location:' . $_SERVER['SCRIPT_NAME']);
    //プログラム終了(exitなかったら後に書かれた処理も実行してしまうから)
    exit;
}
?>



HTMLの記述と解説

HTMLの記述は同じファイルでも分けても好みでOK(今回は続けて同じファイルに記述します)

index.php
<div class="container">
        <h1>簡易掲示板へようこそ(XSS対策済み)</h1>

        <!-- 投稿ボタン -->
        <form method="post"><!-- 中の投稿ボタンが押されたらPOSTのリクエストを送る -->
            <input type="text" name="text">
            <input type="submit" value="投稿する">
        </form>

        <table>
            <?php foreach((array)$all_data as $post_data) : ?>
            <tr>
                <form method="post">
                    <td> <!-- $post_data[2]は$post_text(テキストデータ) -->
                        <?php echo h($post_data[2]); ?>
                    </td>
                    <td> <!-- $post_data[1]は$date(日付) -->
                        <?php echo $post_data[1]; ?>
                    </td>
                </form>
            </tr>
            <?php endforeach; ?>
        </table>
    </div>

~*の解説~

<!--
    *1ENT_QUOTES:["],['],[<],[>]が普通の文字として出力される
    *2uniqid()だけでは同じタイミングで実行すると同じ値が生成されるのでrand()を入れることで回避
        ↑マイクロ秒で同じでないとかぶらないので念のためである
    *3公式マニュアルにdate()の日付文字列の書式が解説されている(分かりやすい)https://www.php.net/manual/ja/function.date.php
    *4 GET:何か情報を検索したり取得するために使うためのメソッド
       POST:登録処理や更新処理などの、書き込みがありリソースが更新される可能性のある処理に対して使うメソッド
       もっと詳しく分かりやすい解説記事:https://qiita.com/kanataxa/items/522efb74421255f0e0a1
-->

まとめ

今回のパートではとりあえず動く掲示板を作成しました!
PHP学習してまだ日が浅いので間違いがあるかもしれません。その時はお教えください?‍♂️
メソッドやプロパティを一つ一つ調べながら学習するといいことが今回分かりました!
この記事を見てくださったあなたの成長を応援します!!
ーーーー
次のパート:PHPと Vue.jsで簡易掲示板を作ろう〜その2( SCSS編)〜

この記事もいかがですか?

初心者に捧げるハンバーガーメニューの作り方
初心者に捧げるヘッダーの作り方
初心者に捧げる〜PHPを使って九九の表を作ろう〜

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

Vue.jsのv-forの見落としがちなkey属性

はじめに

Vue.jsのv-for正しい使い方で実装できていますか。
細かい話ですがつい先日Vue.jsのv-for実装する時、他のソースコード見てみるとkey属性が指定されていませんでした。
後ほど説明しますが例外を除いてはkey属性を指定することが奨励されています。
(バージョン2.2.0以降の話なのでそれ以前のもので実装されている方は軽い気持ちでで読んでください。)

Vue.jsのv-forのkey属性については公式ドキュメントにも以下のように書いてあります。

繰り返される DOM の内容が単純な場合や、性能向上のために標準の動作に意図的に頼る場合を除いて、可能なときはいつでもv-forにkey属性を与えることが推奨されます。

引用:リストレンダリング — Vue.js

なぜkey属性を指定することが奨励されているのか

これはいかなるデータもコンポーネントへ自動的に渡すことはありません。なぜなら、コンポーネントはコンポーネント自身の隔離されたスコープを持っているからです。反復してデータをコンポーネントに渡すためには、プロパティを使うべきです。

引用:リストレンダリング — Vue.js

公式ドキュメントにはこう書いてありますと言ってしまえばその限りなのですが、keyを指定するとVue.jsが各要素を効率よく追跡できるようになりパフォーマンス向上するからです。

key属性を指定したソースコード

<ul id="example-1">
  <li v-for="item in items" :key="item.message">
    {{ item.message }}
  </li>
</ul>

引用:v-for で配列に要素をマッピングする

【注意】v-forにkeyを指定してはいけないパターン

Vue.jsのv-forのkey属性を推していましたが指定してはいけないパターンがあります。
keyに指定するのは一意の値である必要があり、これが一意の値にならない場合はkey属性を指定してはいけません。
正しく動作しないなどのエラーになるのでお気をつけください。

最後に

いかがでしたでしょうか。
なんとなくや前のソースコード参考にという形で実装していると思わぬ落とし穴もあるので、常に疑いながら実装することは大事かもしれませんね。

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

<style scoped>で子コンポーネントのスタイルがどう変わるか検証した(Vue CLI)

結論

<style scoped>で子コンポーネントスタイルを設定したら、そのスタイルは子コンポーネントの一番上のルートにだけ適応される

というルールを検証した結果、その通りだった。

参考:
vue-loader -子コンポーネントのルート要素-
vue-loaderのScoped CSSのスタイルが子コンポーネントのルート要素に効いてしまって辛い

Vue CLIを触り初めて1週間、コンポーネントの<style>の書き方が間違っていて、思い通りのレイアウトにならず苦労した。
後日また同じことで悩まないように、やった手順とよくなかった点をメモしておく。
そしてこの記事が誰かのためになれると嬉しい。

検証画面の構成

コンポーネントの構成

image.png

作成したファイル

- src
    - components
        - ContinueButton.vue
    - views
        - Pj002.vue

ソース

↓コンテニューボタンの単一コンポーネント

ContinueButton.vue
<template>
  <div class="continue-container">
    <p>
      CONTINUE
      <span class="continue-text">
        >
      </span>
    </p>
  </div>
</template>


↓メインページ *コンテニューボタンのスタイルはすべてココに書いている

Pj002.vue
<template>
  <div class="background">
    <main>
      <ContinueButton></ContinueButton>
    </main>
  </div>
</template>

<script>
import ContinueButton from '@/components/ContinueButton.vue';

export default {
  name: 'Pj002',
  components: {
    ContinueButton,
  },
};
</script>

<style>
@import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&display=swap');

/* Pj002.vue全体の背景 */
.background {
  height: 90%;
  position: absolute;
  top: 10%;
  right: 0;
  bottom: 0;
  left: 0;
  background-color: lightblue;
}

/* コンテニューボタンの白いボーダーラインなどを設定 */
.continue-container {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 500px;
  height: 150px;
  border: 5px solid #ffffff;
  text-align: center;
  line-height: 45px;
  opacity: 1;
}

/* コンテニューボタンの文字フォントなどを設定 */
.continue-container p {
  position: absolute;
  width: 100%;
  top: 20%;
  left: 60%;
  transform: translate(-50%, -50%);
  letter-spacing: 1.6px;
  color: #ffffff;
  font: 48px/32px Bebas Neue;
  opacity: 1;
}

/* コンテニューボタンの"CONTINUE"と">"の間の余白を設定 */
.continue-text {
  text-align: left;
  margin-left: 80px;
}
</style>


<style>を変えたらどうなるか

<style>のままの場合

見た目

理想通り。
スタイルがscopedになっていないので、CSSはグローバルで定義されていることになり、コンテニューボタンのレイアウトも問題なく表示される。
image.png

<style scoped>にした場合

見た目

image.png

変わったところ、変わらなかったところ

  1. 白いボーダーラインは何も変わらずきちんと表示されている
  2. 文字のフォントが変わっている
  3. "CONTINUE" と ">" の間の余白がなくなっている

<template>のclass属性も上記の順番で設定している。
image.png

冒頭の結論で出した通り、やはり子コンポーネントの1番上の要素は<style scoped>にしたにも関わらずレイアウトが適応されている。
それ以下のスタイルは適応されていない。

ちなみに<style module>にした場合

見た目

image.png

変わったところ、変わらなかったところ

子コンポーネントのスタイルは一番上の要素であっても適応されない。 ・・・!!?
(※"CONTINUE >"という文字列も表示されなくなった点については理由がわかっていないので、どなたかわかる方がいれば教えていただきたいです。)

ソース

class属性の記載方法がv-bindに変わるのでソースを載せておく。

ContinueButton.vue
<template>
  <div :class="$style.continue_container">
    <p>
      CONTINUE
      <span :class="$style.continue_text">
        >
      </span>
    </p>
  </div>
</template>

Pj002.vue
<template>
  <div :class="$style.background">
    <main>
      <ContinueButton></ContinueButton>
    </main>
  </div>
</template>

<script>
import ContinueButton from '@/components/ContinueButton.vue';

export default {
  name: 'Pj002',
  components: {
    ContinueButton,
  },
};
</script>

<style module>
/* Pj002.vue全体の背景 */
@import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&display=swap');

.background {
  height: 90%;
  position: absolute;
  top: 10%;
  right: 0;
  bottom: 0;
  left: 0;
  background-color: lightblue;
}

/* コンテニューボタンの白いボーダーラインなどを設定 */
.continue_container {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 500px;
  height: 150px;
  border: 5px solid #ffffff;
  text-align: center;
  line-height: 45px;
  opacity: 1;
}

/* コンテニューボタンの文字フォントなどを設定 */
.continue_container p {
  position: absolute;
  width: 100%;
  top: 20%;
  left: 60%;
  transform: translate(-50%, -50%);
  letter-spacing: 1.6px;
  color: #ffffff;
  font: 48px/32px Bebas Neue;
  opacity: 1;
}

/* コンテニューボタンの"CONTINUE"と">"の間の余白を設定 */
.continue_text {
  text-align: left;
  margin-left: 80px;
}
</style>


開発者ツールでの確認結果

<!-- --> になっている
image.png

まとめ

<style scoped>にしても、子コンポーネントの一番上のルートにはスタイルが適応されてしまうので注意すべき。
一部だけ反映されるから「CSSどっかいじってしまったかな?」なんて微調整し始めると沼。
公式のスタイルガイドにて、<style scoped>の使用は必須レベルで推奨されているので、きちんと使いこなせるようになりたい。

参考:
スタイルガイド -コンポーネントスタイルのスコープ-
【Vue.js】Scoped CSSよりCSS Modulesの方がベターだった件

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