20200301のvue.jsに関する記事は7件です。

プログラミングスクール卒業後3ヶ月たった所感

2019年10月に最近youtubeでよく広告を見る某プログラミングスクールに通い、11月より機械学習エンジニア・データサイエンティストとして現職につきました。
卒業後3ヶ月経った今、スクールで学んだことで役立ったこと、今大変なことなどをつらつらと書いていきたいと思います。

経歴

2015年4月 食品会社の研究・開発職として4年間務める
2019年7月 プログラミングスクール学習開始
2019年10月 プログラミングスクール卒業
2019年11月 機械学習エンジニア・データサイエンティストとして転職

スキルセット

言語の使用状況

スクール 転職後
HTML
CSS
JavaScript
SQL
Linux
Ruby -
Python -

フレームワークの使用状況

スクール 転職後
SCSS
jQuery
Vue -
Ruby on Rails -
Flask -
Django -

インフラ等

スクール 転職後
Git/Github
Nginx -
EC2(AWS)
S3(AWS)

スクールについて

役立ったこと

  • HTML/CSS/JavaScript/Linuxについて基礎が学べ、今でもシステムを作成する際に役立っている
  • Ruby→Pythonに言語が変わったがどちらも動的型付けなため、基礎的な文法を学ぶことへの学習コストはかからなかった
  • EC2およびS3の作成・運用の基礎が学べ、現職ではシステムのデプロイは全てAWSを用いているため、すんなり業務に入ることができた
  • 現職では頻繁には使用しないが、Gitを利用した複数人の開発の経験は業務の随所で役立つ
  • メンターへの質問シートの書き方を守ることで、先輩エンジニアなどに質問する際に、何が問題で・どこで詰まっているのかが明確に伝えられ、回答を頂きやすい

役立たなかったこと

基本的には学んだことはある程度役立つと思いますが、下記点は仕方ないと考えていました。

  • スクールでRubyを学んだが、現職では全く使用しないこと(機械学習案件を志望していたため、仕方ないと割り切ってはいました)

転職前にやるべきだったこと

  1. 簡単でもいいので、新しい職場で使用するであろう言語で簡易的なシステムやアプリをデプロイまで持っていく
理由

コードの書き方や、フレームワークを予め触っておいて、素早く職場に馴染めるようにもっと努力をすべきと思いました。

  1. AWSのサービスをより理解しておく
理由

最近のサービスのデプロイのほとんどはAWSを利用していると思います。無料枠内で学ぶのもいいですが、実際には有料で数万円数十万円をかけてシステムを運用します。そのため少しの金額でも身銭を切って、ドメインの取り方を学んだり、アクセスの分散方法を学ぶなど、AWSというサービスを少しでも多く理解しておいた方がよかったと思いました。

  1. 綺麗なコードを書く意識をつける・練習する
理由

シンプルにレビュアーが大変なのと、あとで改修する際に自分でもわからなくなってしまうので、綺麗なコードを意識することは非常に大事です。(作者はめっちゃ冗長に書きすぎて、最初はレビューの時間が非常にかかってしまいました)

今大変なこと

  1. 想像以上にJavaScriptを多用するため、様々な機能を覚える必要がある
  2. 基本的に少人数でシステムを作成したり、データ分析を行うため分からない部分が多く、仕事が止まりやすい

他にもいろいろ大変なこともあるのですが、結論としては悩んだらわかりそうな人に聞く!!これが仕事をうまく進める方法だと思います。私自身人に聞くということが苦手で、どうしても1人でやろうとしがちだったのでした。しかし、先輩エンジニアに聞いたら数分で解決してくれたり、アドバイスしてくれるため、1時間もかけて調べるよりも、わかる人に聞いた方が100%早いです!!

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

Vue.jsとBlazorでWebアプリを作り比べてみての比較

僕は1か月ちょっとの間Vue.jsとBlazor(Wasm)双方でMHWIルーレットを作っていました。
(モンスターハンターワールド:アイスボーン用に装備とかクエストを自動で決めるツール)

Vue.jsとBlazorの書き心地って似てるよねってことでそれぞれ作ってみてその比較を書き留めます。

実装

Vue.js
Blazor
ソースコード

環境

Vue.js

  • @vue/cli 4.1.2
    • Babel
    • TypeScript
    • Router
    • Vuex

Blazor

  • Blazor WebAssembly 3.2.0 preview1

メリット・デメリット

◎>〇>△>× の順で良い。

Vue.js

評価 内容
ロードが高速。
ビルドファイルの容量が小さめ(MHWIルーレットVE 1.43MB)(しかし、ネイティブよりは容量が大分多い)
style scopedの利用可。
TypeScriptを簡単に導入できる(vue-cliにて選択するだけ)。
vue-property-decoratorも個人的に結構好き。
Vue Routerの設定が少し煩雑。
各種ファイル読み込みでimport文が多くなりがち。
状態管理ライブラリ(Vuex)が公式で提供されている。
画面上の要素は適切に更新されるので気を使う必要がない。
html上の改行や空白が削除される。

Blazor

評価 内容
× ロードが遅い。
× ビルドファイルの容量が大きい(MHWIルーレットBR 4.91MB)
× フレームワークとしてコンポーネントへのCSSのインポート方法が(おそらく)用意されていない。
html5の仕様としてbodyの中に埋め込むことを許可されたりされなかったりしているみたいだけど、おおよそのブラウザがbody内styleを認識できるので妥協した。お行儀良くないし、推奨もされないだろう。
(大体)C#のみで処理を書くことができる。
× DOM APIの操作にはJavaScriptに頼らなくてはならない。
また、JSRuntime.InvokeAsyncで扱えるのが関数/メソッドのみなので、プロパティやフィールドへのアクセスができない。推奨されないが必要とあればevalとかしてお茶を濁す。一応綺麗にjsにアクセスする方法もある。個別にjsファイル用意してindex.htmlに埋め込むのも面倒だと思う。
ルーターの設定が対象ページの冒頭に1行追記するだけで良いので簡単。
プロジェクト丸ごとインポートしてくれるので名前空間を分けない限りは呼び出し時の定義が不要。
状態管理ライブラリが用意されていない。ただし、ライブラリの追加なしで似たような書き方は可能。
画面上の要素を変更した時、StateHasChangedを呼び出して手動で画面を更新する必要がある。
html上の改行や空白が削除されない。

機能的には全体的にVue.jsが勝っているなぁという印象でした。

ディレクトリ構成

それぞれ初期化した時に作られるディレクトリ構成について。

Vue.js

├─public
└─src
    ├─assets
    ├─components
    ├─router
    ├─store
    └─views

Blazor

├─Pages
├─Shared
└─wwwroot

ディレクトリの役割

項目 Vue.js Blazor
静的ファイル(index.html,CSS) /public /wwwroot
動的に読み込む静的ファイル /src/assets /wwwroot
ページファイル /src/views /Pages
コンポーネントファイル /src/components /Shares
Vuex等状態管理 /src/store 任意
ルーターの設定 /src/router なし

今回のMHWIルーレットでは一部ディレクトリ構成を下記のように追加しています。

項目 Vue.js Blazor
動的に読み込む静的ファイル /wwwroot/Assets
Vuex等状態管理 /Store
その他汎用スクリプト(.ts/.cs) /src/lib /Lib

設定ファイル

Vue

│  babel.config.js
│  package.json
│  tsconfig.json
│  vue.config.js
│  
└─src
    │  main.ts
    │  shims-tsx.d.ts
    │  shims-vue.d.ts
    │  
    └─router
            index.ts

/babel.config.js

Babelの設定ファイル。

/package.json

プロジェクトの設定ファイル。

/tsconfig.json

TypeScriptの設定ファイル。

/vue.config.js

ドメイン直下以外のパスに配置するとき以外は必要なファイルです。
記入したパス向けにビルドを行います。

/src/main.ts

エントリーポイント。
App.vueとVue Routerを起動している様子。
テンプレート作成時にルーターを設定していれば改めて触る必要はないです。

/src/shims-tsx.d.ts, /src/shims-vue.d.ts

多分TypeScriptの型を定義しているファイル。

/router/index.ts

ルーターで切り替えるページ数が増えるたびに追記する必要があります。
ページに割り当てたいコンポーネントをパスとともに記述します。

Blazor

│  base.patch
│  mh-lret.csproj
│  Program.cs
│  _Imports.razor

/base.patch

ドメイン直下以外のパスに配置するとき以外は/wwwroot/index.htmlのbase要素のパスを変更する必要があります。
それを自動化するために作ったpatchファイルです。

/mh-lret.csproj

プロジェクトの設定ファイルです。

/Program.cs

エントリーポイント。
App.razorを起動しているみたいです。
サービスを追加する場合はここに追記します。

今回は状態管理用のクラスを追加するため追記しています。

var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");
builder.Services.AddScoped<AppState>(); //<-追加

await builder.Build().RunAsync();

参考1 参考2

/_Imports.razor

razorファイルで常に参照できる名前空間を追記します。

index.html

アプリを反映するための基本htmlファイルです。
アプリ起動に失敗した場合の処理はデフォルトで大体ここに書かれています。
head要素はここにしか書けないので結構重要そう。
Blazor的にはC#から呼び出せるjsファイルを配置できる唯一のファイルです。
Blazorはロードが凄く長いのでスプラッシュ画面を用意したりしました。

Vue.js

/public/index.html
<!DOCTYPE html>
<html lang=ja>

<head>
    <title>MHWIルーレットVE</title>
</head>

<body>
    <noscript>
    <strong>We're sorry but ve doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
</html>

Blazor

/wwwroot/index.html
<!DOCTYPE html>
<html lang=ja>

<head>
    <base href="/" />
    <title>MHWIルーレットBR</title>
</head>

<body>
    <app>Loading...</app>
    <div id="blazor-error-ui" style=display:none>
        An unhandled error has occurred.
        <a href="" class="reload">Reload</a>
        <a class="dismiss">?</a>
    </div>
    <script src="_framework/blazor.webassembly.js"></script>
</body>

</html>

App Component/ルーター

基準となるコンポーネントファイルとルーターについて。

Vue.js

最初に呼び出されるコンポーネントファイルはApp.vueとなります。
ルーターを使用する場合はルーター用のリンク(router-link)とページコンポーネントが表示される場所(router-view)を記載します。
ルーターのパス構成は/src/router/index.tsから行う必要があります。

App.vue
<template>
<main v-cloak>
    <router-view/>
    <div id=nav>
        <router-link to="/readme/">ルーレットについて</router-link>
        <router-link to="/">MHWI</router-link>
        <a href="/lret/mhwi-br/">(BR)</a>
    </div>
</main>
</template>

Blazor

最初に呼び出されるコンポーネントファイルはApp.razorとなります。
デフォルトではルーターの設定だけが書かれており、レイアウトに関する情報は全てMainLayout.razorに書かれています。
ルーターを使用する場合はルーター用のリンク(NavLink)とページコンポーネントが表示される場所(@Body)を記載します。

MainLayout.razor
@inherits LayoutComponentBase

<main>
    @Body
    <div id=nav>
        <NavLink href="./readme/" Match="NavLinkMatch.All">ルーレットについて</NavLink>
        <NavLink href="./" Match="NavLinkMatch.All">MHWI</NavLink>
        <a href="/lret/mhwi-ve/">(VE)</a>
    </div>
</main>

文法の比較

思い付きで事細かに構文比較を作っていったら記事が肥大化しました。
文法の網羅範囲としては今回MHWIルーレットを作成するのに必要となった分のみです。
ここから先はVueVsBlazorにソースコードをまとめています。

R. ルーター

3ページの切り替えができるルーター。
Vue.jsでは/src/router.index.tsをページが増えるたびに追加する必要があります。

Vue.js

コンポーネント+ルーター配置
└─src
    │  App.vue
    │  main.ts
    │  shims-tsx.d.ts
    │  shims-vue.d.ts
    │
    ├─components
    │      NavMenu.vue
    │
    ├─router
    │      index.ts
    │
    └─views
            Index.vue
            PageA.vue
            PageB.vue
router/index.ts
import Vue from "vue";
import VueRouter from "vue-router";
import Index from "@/views/Index.vue";
import PageA from "@/views/PageA.vue";
import PageB from "@/views/PageB.vue";

Vue.use(VueRouter);

const routes=[
    {
        path: "/",
        name: "Index",
        component: Index
    },
    {
        path: "/PageA",
        name: "PageA",
        component: PageA
    },
    {
        path: "/PageB",
        name: "PageB",
        component: PageB
    }
];

const router=new VueRouter({
    mode: "history",
    base: process.env.BASE_URL,
    routes
});

export default router;
App.vue
<template>
<main style=display:flex>
    <NavMenu />
    <div class=v-hr></div>
    <router-view/>
</main>
</template>

<style scoped>
.v-hr{
    margin: 0 10px;
    border-right: 5px solid #CCC;
    height: 100vh;
}
</style>

<script lang:ts>
import {Component,Vue} from "vue-property-decorator";
import NavMenu from "@/components/NavMenu.vue";

@Component({
    components:{
        NavMenu
    }
})
export default class App extends Vue{}
</script>
NavMenu.vue
<template>
<nav>
    <ol type="1" start="0">
        <li><router-link to="/">index</router-link></li>
        <li><router-link to="/PageA">PageA</router-link></li>
        <li><router-link to="/PageB">PageB</router-link></li>
    </ol>
</nav>
</template>

<style scoped>
.router-link-exact-active{
    color: #FF0000;
    font-weight: bold;
}
</style>
PageA.vue
<template>
<div>
    <h1>PageA</h1>
</div>
</template>
PageB.vue
<template>
<div>
    <h1>PageB</h1>
</div>
</template>

Blazor

コンポーネント配置
│  App.razor
│  MainLayout.razor
│
├─Pages
│      Index.razor
│      PageA.razor
│      PageB.razor
│
└─Shared
       NavMenu.razor
App.razor
<Router AppAssembly="@typeof(Program).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
    </Found>
    <NotFound>
        <LayoutView Layout="@typeof(MainLayout)">
            <p>Sorry, there's nothing at this address.</p>
        </LayoutView>
    </NotFound>
</Router>
MainLayout.razor
@inherits LayoutComponentBase

<main style=display:flex>
    <NavMenu />
    <div class=v-hr></div>
    @Body
</main>

<style>
.v-hr{
    margin: 0 10px;
    border-right: 5px solid #CCC;
    height: 100vh;
}
</style>
NavMenu.razor
<div>
    <ol type="1" start="0">
        <li><NavLink Match="NavLinkMatch.All" href="./">index</NavLink></li>
        <li><NavLink Match="NavLinkMatch.All" href="./PageA">PageA</NavLink></li>
        <li><NavLink Match="NavLinkMatch.All" href="./PageB">PageB</NavLink></li>
    </ol>
</div>

<style>
nav .active{
    color: #FF0000;
    font-weight: bold;
}
</style>
Index.razor
@page "/"

<div>
    <h1>Hello Blazor!</h1>
</div>
PageA.razor
@page "/PageA"

<div>
    <h1>PageA</h1>
</div>
PageA.razor
@page "/PageB"

<div>
    <h1>PageA</h1>
</div>

0. 最小のコンポーネント

コンポーネントの内容はただのhtmlから可能です。
Vue.jsはtemplateタグで囲み、1つのブロックのみとする必要があります。

Vue.js

Index.vue
<template>
<div>
    <h1>Hello Vue.js!</h1>
</div>
</template>

Blazor

Index.razor
@page "/"

<div>
    <h1>Hello Blazor!</h1>
</div>

1. スタイルシート

BlazorはCSSに対する特別な構文がないので諦めてbodyに直置きすることにしました。

Vue.js

StyleBlock.vue
<template>
<div id=index>
    <h1>Hello Vue.js!</h1>
</div>
</template>

<style scoped>
div#index{
    color: #0000FF;
}
</style>

Blazor

StyleBlock.razor
@page "/StyleBlock"

<div id=index>
    <h1>Hello Blazor!</h1>
</div>

<style>
div#index{
    color: #0000FF;
}
</style>

2. コードブロック/スクリプトの埋め込み

Vue.jsではscriptタグ内(TypeScriptを使う場合はlang=ts要)に、
Blazorでは@codeブロック内にスクリプト処理を記述します。
コードブロックで定義している変数はhtml内に埋め込むことができます。
(Vue.jsは{{hoge}}(thisは常に付加される)、C#は@hogeで呼び出し)

Vue.js

ScriptBlock.vue
<template>
<div>
    <h1>{{title}}</h1>
</div>
</template>

<script lang=ts>
import {Component,Vue} from "vue-property-decorator";

@Component
export default class ScriptBlock extends Vue{
    title="Hello Vue.js!";
}
</script>

Blazor

ScriptBlock.razor
@page "/ScriptBlock"

<div>
    <h1>@title</h1>
</div>

@code{
    string title="Hello Blazor!";
}

3. html中への数式埋め込み

html中に埋め込めるのは変数のみではありません。
数式を埋め込むこともできます。
Vue.js{{}}内ではグローバルオブジェクトを扱えないことについて注意します。
Blazorでの@のエスケープは@@と記述します。

Vue.js

Formula.vue
<template>
<div>
    <h1>10!={{10*9*8*7*6*5*4*3*2*1}}</h1>
</div>
</template>

Blazor

Formula.razor
@page "/Formula"

<div>
    <h1>10!=@(10*9*8*7*6*5*4*3*2*1)</h1>
</div>

4. ライフサイクルメソッド

Vue.js/Blazorにはhtmlのonload/unonloadのように
コンポーネントの状態でフックされるライフサイクルメソッドというものがあります。

項目 Vue.js Blazor
初期化前 beforeCreate
初期化後 created OnInitialized
OnInitializedAsync
レンダリング前 beforeMount OnParametersSet
OnParametersSetAsync
レンダリング後 mounted OnAfterRender
OnAfterRenderAsync※1
変更前 beforeUpdate
変更後 updated OnAfterRender
OnAfterRenderAsync※1
アクティブ化時 activated
非アクティブ化時 deactivated
破棄前 beforeUpdate
破棄時 beforeDestroy Dispose※2
  • ※1: firstRender引数で初回かどうか判別。
  • ※2: ページリロード時には動作しない。

Vue.jsのライフサイクル
Blazorのライフサイクル

初期化処理は大体レンダリング後に該当する処理で間に合う気がします。
Blazorはデストラクタがページ更新時に動作しないので注意が必要です。
(その場合、現状としてはjsのunonloadをなんとか使うしかない?)

Vue.js

LifeCycle.vue
<template>
<div>
    <h1>{{title}}</h1>
</div>
</template>

<script lang=ts>
import {Component,Vue} from "vue-property-decorator";

@Component
export default class LifeCycle extends Vue{
    title="Hello Vue.js!";

    async mounted(){
        await new Promise(res=>setTimeout(res,5000));
        this.title+=" 5s passed!";
    }
}
</script>

Blazor

LifeCycle.razor
@page "/LifeCycle"

<div>
    <h1>@title</h1>
</div>

@code{
    string title="Hello Blazor!";

    protected override async Task OnAfterRenderAsync(bool firstRender){
        if(!firstRender) return;
        await Task.Delay(5000);
        title+=" 5s passed!";
        StateHasChanged();
    }
}

5. DOM API

Vue.jsではブラウザ依存のDOM APIをそのまま使用することができます。
BlazorではJavaScriptに頼らずDOM APIを扱うことは不可能なので
JavaScriptのメソッドを呼び出す必要があります。

JavaScriptを使うにはIJSRuntimeをinjectして
IJSRuntime.InvoveAsync・IJSRuntime.InvokeVoidAsyncメソッドを呼び出します。

プロパティの類には一切取得できないのでその用途にはevalを使うか
別にjsファイルを用意して関数として呼び出す必要があります。

Vue.js

UseDOMAPI.vue
<template>
<div>
    <h1>Alert</h1>
</div>
</template>

<script lang=ts>
import {Component,Vue} from "vue-property-decorator";

@Component
export default class UseDOMAPI extends Vue{
    async mounted(){
        var title=document.title;
        alert(title);
    }
}
</script>

Blazor

UseDOMAPI.razor
@page "/UseDOMAPI"

<div>
    <h1>Alert</h1>
</div>

@inject IJSRuntime js
@code{
    protected override async Task OnAfterRenderAsync(bool firstRender){
        if(!firstRender) return;
        var title=await js.InvokeAsync<string>("eval","document.title");
        await js.InvokeVoidAsync("alert",title);
    }
}

6. 双方向バインディング

Vue.js/Blazorではdocument.element.valueを直接操作する代わりに変数と要素の値をバインド(同期)します。
Vue.jsではv-modelを、Blazorでは@bindを属性に指定します。

Vue.js

BindingInput.vue
<template>
<div>
    <h1>{{title}}</h1>
    <input v-model="title">
</div>
</template>

<script lang=ts>
import {Component,Vue} from "vue-property-decorator";

@Component
export default class BindingInput extends Vue{
    title="Hello Vue.js!";
}
</script>

Blazor

BindingInput.razor
@page "/BindingInput"

<div>
    <h1>@title</h1>
    <input @bind="title">
</div>

@code{
    string title="Hello Blazor!";
}

7. 片方向バインディング

変数からdocument.element.valueを一方的に更新することもできます。

Vue.jsではv-bind:valueを属性に指定します。
v-bind:は:のように省略することができるので:valueと書けます。
Blazorではvalueに変数として割り当てることができます。

Vue.js

BindingInputMutual.vue
<template>
<div>
    <h1>{{title}}</h1>
    <input :value="title">
</div>
</template>

<script lang=ts>
import {Component,Vue} from "vue-property-decorator";

@Component
export default class BindingInputMutual extends Vue{
    title="Hello Vue.js!";

    async mounted(){
        for(;;){
            await new Promise(res=>setTimeout(res,2000));
            this.title+=">";
        }
    }
}
</script>

Blazor

BindingInputMutual.razor
@page "/BindingInputMutual"

<div>
    <h1>@title</h1>
    <input value=@title>
</div>

@code{
    string title="Hello Blazor!";

    protected override async Task OnAfterRenderAsync(bool firstRender){
        if(!firstRender) return;
        for(;;){
            await Task.Delay(2000);
            title+=">";
            StateHasChanged();
        }
    }
}

8.イベントハンドラ

イベントハンドラはそれぞれちょいちょい表記が異なるので通常のhtmlについても併記します。
Vue.jsではonclick属性の代わりにv-on:click属性を使用します。
v-on:は@と表記することもできるので@clickと書くことが出ます。

Blazorでは通常のイベント名に@を付けたものがイベント属性となります。
つまりこの例では@onclick属性です。

Vue.js、Blazorどちらでも指定するものはメソッド名のみでメソッド呼び出しを意味する"()"は不要です。

HTML

<html>
<body>
    <button onclick="openDialog()">Click Me!</button>

    <script>
        var title="Hello HTML!";

        function openDialog(){
            alert(title);
        }
    </script>
</body>
</html>

Vue.js

EventHandler.vue
<template>
<div>
    <button @click="openDialog">Click Me!</button>
</div>
</template>

<script lang=ts>
import {Component,Vue} from "vue-property-decorator";

@Component
export default class EventHandler extends Vue{
    title="Hello Vue.js!";

    openDialog(){
        alert(this.title);
    }
}
</script>

Blazor

Index.razor
@page "/EventHandler"

<div>
    <button @onclick="openDialog">Click Me!</button>
</div>

@inject IJSRuntime js

@code{
    string title="Hello Blazor!";

    async void openDialog(){
        await js.InvokeVoidAsync("alert",title);
    }
}

9. onchangeイベント

Vue.jsでは@chengeイベントとv-modelを同時に使うことができますが、
Blazorでは@onchangeイベントと@bindを同時に使うことはできません。
厳密には動作は異なりますが、@onclickイベントと僅かに遅延させることで同じような動作を得ることができます。

Vue.js

OnChangeEvent.vue
<template>
<div>
    <h1>Check: {{isChecked}}</h1>
    <input id=chk type=checkbox v-model="isChecked" @change="chkChange">
    <label for=chk>CheckBox</label>
</div>
</template>

<script lang=ts>
import {Component,Vue} from "vue-property-decorator";

@Component
export default class OnChangeEvent extends Vue{
    isChecked=false;

    chkChange(){
        alert(`Check: ${this.isChecked}`);
    }
}
</script>

Blazor

OnChangeEvent.razor
@page "/OnChangeEvent"

<div>
    <h1>Check: @isChecked</h1>
    <input id=chk type=checkbox @bind="isChecked" @onclick="chkChange">
    <label for=chk>CheckBox</label>
</div>

@inject IJSRuntime js;
@code{
    bool isChecked=false;

    async Task chkChange(){
        await Task.Delay(1);
        await js.InvokeVoidAsync("alert",$"Check: {isChecked}");
    }
}

10. スタイルバインディング

スクリプトによるスタイルの変更はhtmlではdocument.element.styleの変更によって行われていました。
Vue.js/Blazorではその代わり属性に直接値をバインドさせることで変更を行います。

Vue.jsではv-bind:style属性にJSON形式の文字列で渡します。
変更したいスタイルのキーに対してスタイルの文字列を返す処理を書き込むか
スタイルの含まれる文字列変数を割り当てます。

Blazorではstyle属性に渡す文字列を直接編集することでスタイルの変更が可能です。

Vue.js

BindingStyle.vue
<template>
<div>
    <h1>Check: {{isChecked}}</h1>
    <input id=chk type=checkbox v-model="isChecked">
    <label for=chk>CheckBox</label>
    <div :style="{color: isChecked? 'blue': 'red'}">
        Change Style!
    </div>
</div>
</template>

<script lang=ts>
import {Component,Vue} from "vue-property-decorator";

@Component
export default class BindingStyle extends Vue{
    isChecked=false;
}
</script>

Blazor

BindingStyle.razor
@page "/BindingStyle"

<div>
    <h1>Check: @isChecked</h1>
    <input id=chk type=checkbox @bind="isChecked">@(""
    )<label for=chk>CheckBox</label>
    <div style=@("color:"+(isChecked? "blue": "red"))>
        Change Style!
    </div>
</div>

@code{
    bool isChecked=false;
}

11. クラスバインディング

クラスについてもスタイルと同様にバインドできます。

Vue.jsではv-bind:class属性にJSON形式の文字列で渡します。
変更したいclass名のキーに対してboolean値を割り当てて行います。

Blazorではスタイルの変更と同様にclass属性に渡す文字列を直接編集します。

Vue.js

BindingClass.vue
<template>
<div>
    <h1>Check: {{isChecked}}</h1>
    <input id=chk type=checkbox v-model="isChecked">
    <label for=chk>CheckBox</label>
    <div :class="{clsA: isChecked, clsB: !isChecked}">
        Change Style!
    </div>
</div>
</template>

<style scoped>
.clsA{
    color: blue;
    font-size: 1.5em;
    text-decoration: underline solid;
}

.clsB{
    color: red;
    font-size: 1.0em;
    font-style: italic;
}
</style>

<script lang=ts>
import {Component,Vue} from "vue-property-decorator";

@Component
export default class BindingClass extends Vue{
    isChecked=false;
}
</script>

Blazor

BindingClass.razor
@page "/BindingClass"

<div>
    <h1>Check: @isChecked</h1>
    <input id=chk type=checkbox @bind="isChecked">@(""
    )<label for=chk>CheckBox</label>
    <div class=@(isChecked? "clsA": "clsB")>
        Change Style!
    </div>
</div>

<style>
.clsA{
    color: blue;
    font-size: 1.5em;
    text-decoration: underline solid;
}

.clsB{
    color: red;
    font-size: 1.0em;
    font-style: italic;
}
</style>

@code{
    bool isChecked=false;
}

12. if(場合分け)

Vue.js/Blazorでは場合分けで表示状態を変更できます。

Vue.jsではv-if属性を対象の要素に含めると表示状態を変更できます。
v-ifで表示の切り替えを行うとライフサイクルが働くこととなります。
コンポーネントの状態を保ったまま表示切替を行いたい場合はv-showを使います。
(内部的にdisplay: none;を使っています)

Blazorでは@ifを使います。
動作としてはVue.jsにおけるv-ifと同様です。
コンポーネントの状態を保つ場合はstyle/class属性で直接隠すようにします。

Vue.js

IfAndShow.vue
<template>
<div>
    <h1>Check: {{isChecked}}</h1>
    <input id=chk type=checkbox v-model="isChecked">
    <label for=chk>CheckBox</label>
    <div v-if="isChecked">
        <input>
    </div>
    <div v-show="isChecked">
        <input>
    </div>
</div>
</template>

<script lang=ts>
import {Component,Vue} from "vue-property-decorator";

@Component
export default class IfAndShow extends Vue{
    isChecked=false;
}
</script>

Blazor

IfAndShow.razor
@page "/IfAndShow"

<div>
    <h1>Check: @isChecked</h1>
    <input id=chk type=checkbox @bind="isChecked">@(""
    )<label for=chk>CheckBox</label>
    @if(isChecked){
        <div>
            <input>
        </div>
    }
    <div style=@("display:"+(isChecked? "": "none"))>
        <input>
    </div>
</div>

@code{
    bool isChecked=false;
}

13. foreach(繰り返し)

Vue.js/Blazorでは同じ構成のタグであれば繰り返して表示させることができます。

Vue.jsではv-for属性を繰り返したい要素に含めます。
これはtemplateタグを無名のタグとして使い、囲んでループさせても問題ありません。
大体の主流なブラウザではオブジェクトの順序は一定となりますが、仕様上で保障されていないので注意してください。
(Mapも一応使えますが情報量が少なくて少し怪しいです。
書き方はv-for="[key,value] of list]"と通常のfor文っぽく書くと使えるようです)

Blazorでは@for/@foreachを使います。

v-bind:key/@keyについて

ループで生成されるリストにはコンポーネントの同一性などを担保するためにkey属性の追加が推奨されます。
動作が高速になるとも言われます。
v-bind:keyはVue.js、@keyはBlazorにおける表記方法です。
Vue.js
Blazor

Vue.js

ForEachLoop.vue
<template>
<div>
    <div v-for="(isChecked,key) in dict">
        <input :id="key" type=checkbox v-model="dict[key]" :key="key">
        <label :for="key">{{key}}</label>
    </div>
</div>
</template>

<script lang=ts>
import {Component,Vue} from "vue-property-decorator";

@Component
export default class ForEachLoop extends Vue{
    dict:{[s:string]:boolean}={
        A:true,
        B:true,
        C:true,
        D:false,
        E:false
    };
}
</script>

Blazor

ForEachLoop.razor
@page "/ForEachLoop"

<div>
    @foreach(var (key,isChecked) in dict){
        <div>
            <input id=@key type=checkbox @bind="dict[key]" @key="key">@(""
            )<label for=@key>@key</label>
        </div>
    }
</div>

@code{
    Dictionary<string,bool> dict=new Dictionary<string,bool>{
        {"A",true},
        {"B",true},
        {"C",true},
        {"D",false},
        {"E",false}
    };
}

14. コンポーネントの追加

Vue.js/Blazorではhtmlタグ中に自作の要素(コンポーネント)を埋め込むことをできます。
Blazorでは自動で全てのコンポーネントを読み込みますが、
Vue.jsではimport文で読み込むコンポーネントを指定する必要があります。

Vue.js

ComponentA.vue
<template>
<div>
    <h3>ComponentA</h3>
    <textarea></textarea>
</div>
</template>
ComponentB.vue
<template>
<div>
    <input id=chk type=checkbox>
    <label for=chk>ComponentB</label>
</div>
</template>
AddComponent.vue
<template>
<div>
    <ComponentA />
    <ComponentB />
</div>
</template>

<script lang=ts>
import {Component,Vue} from "vue-property-decorator";
import ComponentA from "@/components/ComponentA.vue";
import ComponentB from "@/components/ComponentB.vue";

@Component({
    components:{
        ComponentA,
        ComponentB
    }
})
export default class AddComponent extends Vue{}
</script>

Blazor

ComponentA.razor
<div>
    <h3>ComponentA</h3>
    <textarea></textarea>
</div>
ComponentB.razor
<div>
    <input id=chk type=checkbox>
    <label for=chk>ComponentB</label>
</div>
AddComponent.razor
@page "/AddComponent"

<div>
    <ComponentA />
    <ComponentB />
</div>

15. コンポーネントの属性

Vue.js/Blazorでは子コンポーネントに属性を与え、
与えられた子コンポーネント中でプロパティとして使用することができます。
Vue.jsでは@Prop、Blazorでは[Parameter]で属性名を指定します。

Vue.js

ComponentC.vue
<template>
<div :style="{color: color}">
    Input Attribute={{msg}}
</div>
</template>

<script lang=ts>
import {Component,Prop,Vue} from "vue-property-decorator";

@Component
export default class ComponentC extends Vue{
    @Prop()
    private msg:string;
    @Prop()
    private color:string;
}
</script>
ComponentAttribute.vue
<template>
<div>
    <ComponentC msg="View Message" color="#FF00FF" />
</div>
</template>

<script lang=ts>
import {Component,Vue} from "vue-property-decorator";
import ComponentC from "@/components/ComponentC.vue";

@Component({
    components:{
        ComponentC
    }
})
export default class ComponentAttribute extends Vue{}
</script>

Blazor

ComponentC.razor
<div style=@($"color: {color}")>
    Input Attribute=@msg
</div>

@code{
    [Parameter]
    public string msg{get;set;}
    [Parameter]
    public string color{get;set;}
}
ComponentAttribute.razor
@page "/ComponentAttribute"

<div>
    <ComponentC msg="View Message" color="#FF00FF" />
</div>

16. コンポーネントのメソッド呼び出し

Vue.js/Blazorでは子コンポーネント中のメンバーを呼び出すことができます。

Vue.jsではref属性、Blazorでは@ref属性でバインドする変数名を指定します。
クラス中でも使用するためにそれぞれ宣言が必要です。

Vue.js

Toast.vue
<template>
<dialog :open="isShow">
    {{msg}}
</dialog>
</template>

<script lang=ts>
import {Component,Vue,Prop} from "vue-property-decorator";

@Component
export default class Toast extends Vue{
    isShow=false;
    msg="";

    public async show(msg:string){
        this.msg=msg;
        this.isShow=true;
        await new Promise(res=>setTimeout(res,1500));
        this.isShow=false;
    }
}
</script>
ComponentMethod.vue
<template>
<div>
    <Toast ref="toast" />
    <button @click="viewToast">Click Me!</button>
</div>
</template>

<script lang=ts>
import {Component,Vue} from "vue-property-decorator";
import Toast from "@/components/Toast.vue";

@Component({
    components:{
        Toast
    }
})
export default class ComponentMethod extends Vue{
    $refs!:{toast: Toast};

    async viewToast(){
        await this.$refs.toast.show("View Torst!");
    }
}
</script>

Blazor

Toast.razor
<dialog open=@isShow>
    @msg
</dialog>

@code{
    bool isShow=false;
    string msg="";

    public async Task show(string msg){
        this.msg=msg;
        isShow=true;
        StateHasChanged();
        await Task.Delay(2500);
        isShow=false;
        StateHasChanged();
    }
}
ComponentMethod.razor
@page "/ComponentMethod"

<div>
    <Toast @ref="toast" />
    <button @onclick="viewToast">Click Me!</button>
</div>

@code{
    Toast toast;

    async Task viewToast(){
        await toast.show("View Toast!");
    }
}

17. 状態管理コンテナ

どのコンポーネントからでも参照できるグローバル変数のようなもの。
ここでは変数の読み書き程度の極々簡単のみ行っています。

Vue.jsでは公式なライブラリとしてVuexが存在します。
Blazorでは特にそのようなものは存在しませんが、Blazorの基本機能のみで同じようにコンテナを扱う方法が存在します。
参考1 参考2

詳細については割愛し、動作例のみ記載します。

Vue.js

/src/store/index.ts
import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);
export default new Vuex.Store({
    state:{
        books: [] as string[],
        date: null as Date
    },
    mutations:{
        setBooks(state,books:string[]){
            state.books=books;
        },
        setDate(state,date:Date){
            state.date=date;
        }
    }
});
BooksInput.vue
<template>
<div>
    <div><textarea v-model="bookList" id="bookList"></textarea></div>
    <button @click="setBooks">Set Books!</button>
</div>
</template>

<style scoped>
    #bookList{
        height: 300px;
        width: 300px;
    }
</style>

<script lang=ts>
import {Component,Vue,Prop} from "vue-property-decorator";
import store from "@/store";

@Component
export default class BooksInput extends Vue{
    bookList="";

    public setBooks(){
        store.commit("setBooks",this.bookList.split(/\r|\n|\r\n/).filter(s=>s!=""));
        store.commit("setDate",new Date());
        alert("setBooks!");
    }
}
</script>
StateContainer.vue
<template>
<div>
    <BooksInput />
    <button @click="getBooks">Get Books!</button>
    <h3>BookLists ({{date}})</h3>
    <ul>
        <li v-for="book in books" :key="book">{{book}}</li>
    </ul>
</div>
</template>

<script lang=ts>
import {Component,Vue} from "vue-property-decorator";
import BooksInput from "@/components/BooksInput.vue";
import store from "@/store";

@Component({
    components:{
        BooksInput
    }
})
export default class StateContainer extends Vue{
    books:string[]=[];
    date:Date=null;

    getBooks(){
        this.books=store.state.books;
        this.date=store.state.date;
    }
}
</script>

Blazor

BlazorではProgram.csにサービスとしてコンテナを登録する必要があります。
これは本記事の/設定ファイル/Program.csにて記述しています。

/Store/AppStore.cs
using System;

public class AppState{
    public string[] books{get;private set;}=new string[]{};
    public DateTime? date{get;private set;}=null;

    public void setBooks(string[] books){
        this.books=books;
        NotifyStateChanged();
    }
    public void setDate(DateTime date){
        this.date=date;
        NotifyStateChanged();
    }

    public event Action OnChange;
    private void NotifyStateChanged()=>OnChange?.Invoke();
}
BooksInput.razor
<div >
    <div><textarea @bind="bookList" id="bookList"></textarea></div>
    <button @onclick="setBooks">Set Books!</button>
</div>

<style>
    #bookList{
        height: 300px;
        width: 300px;
    }
</style>

@inject IJSRuntime js;
@inject AppState state;

@code{
    string bookList="";

    public void setBooks(){
        state.setBooks(Array.FindAll(bookList.Replace("\r\n","\n").Split(new[]{'\n','\r'}),s=>s!=""));
        state.setDate(DateTime.Now);
        js.InvokeVoidAsync("alert","setBooks!");
    }
}
StateContainer.razor
@page "/StateContainer"

<div>
    <BooksInput />
    <button @onclick="getBooks">Get Books!</button>
    <h3>BookLists (@date)</h3>
    <ul>
        @foreach(var book in books){<li @key="book">@book</li>}
    </ul>
</div>

@inject AppState state;
@code{
    string[] books={};
    DateTime? date=null;

    void getBooks(){
        books=state.books;
        date=state.date;
        StateHasChanged();
    }
}

18. JSONの読み込み

Vue.js/BlazorではJSONファイルを読み込んで表示することができます。
(クライアントサイドなので書き込みはできません)

Vue.jsではrequire関数を使用し、JSONファイルを/src/assets/以下に配置します。
BlazorではHttpClient.GetJsonAsyncを使用し、JSONファイルを/wwwroot/以下に配置します。

ここでは次のJSONファイルを読み込みします。

weapons.json
[
    "大剣",
    "太刀",
    "片手剣",
    "双剣",
    "ハンマー",
    "狩猟笛",
    "ランス",
    "ガンランス",
    "スラッシュアックス",
    "チャージアックス",
    "操虫棍",
    "ライトボウガン",
    "ヘビィボウガン",
    "弓"
]

Vue.js

ReadJSON.vue
<template>
<div>
    <h3>Read JSON</h3>
    <ul>
        <li v-for="value in list" :key="value">{{value}}</li>
    </ul>
</div>
</template>

<script lang=ts>
import {Component,Vue} from "vue-property-decorator";

@Component
export default class ReadJSON extends Vue{
    list:string[]=[];

    mounted(){
        this.list=require("@/assets/weapons.json");
    }
}
</script>

Blazor

ReadJSON.razor
@page "/ReadJSON"

<div>
    <h3>Read JSON</h3>
    <ul>
        @foreach(var value in list){<li @key="value">@value</li>}
    </ul>
</div>

@inject HttpClient http;

@code{
    string[] list={};

    protected override async Task OnAfterRenderAsync(bool firstRender){
        if(!firstRender) return;
        list=await http.GetJsonAsync<string[]>("Assets/weapons.json?0");
        StateHasChanged();
    }
}

まとめ

例によってまとまりもなくずらずら書いた記事になりました。
Vue.jsとBlazorで同じことをやりたいとき少しは役に立つのかもしれません。

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

VueとCSSとTypeScriptでシューティングゲーム「ネコメザシアタック2020」を作ったのでソースと解説

こんにちは:cat: 今日は2/22の猫の日に合わせて個人開発したゲーム「ネコメザシアタック」の技術的なポイントを解説する記事です。去年のバージョンはこちら

作ったもの

ezgif-6-22637686d4bf.gif

ソース: https://github.com/yuneco/mezashi2
アプリ: https://nekomzs2.web.app/
(PCでも遊べるけどスマホ推薦です)

使っている技術

そろそろリリース見込みのVue3を先取りした構成です

  • Vue(Vue2 + CompositionAPI)
  • TypeScript
  • CSS Transition(ほとんどのアニメーション)
  • SVG(画像 + 一部のアニメーション)
  • Firebase (Hosting + FireStoreでランキング)

おしながき(この記事の内容)

作ったもの全部を解説していくとキリがないので、主に去年からの差分を中心に面白いポイントだけ説明していきます。

アニメーションのポイント
 :cat: 角丸の地面を歩くアニメーション
 :cat: 星から星に飛び移るアニメーション
CSS Transitionでゲームを作るときの悩みと解決法
 :cat: アニメーションの途中で現在の位置や角度をどうやって取得するか
新しい技術スタックのポイント
 :cat: TypeScript + CompositionAPIの採用

それでは行ってみましょう :cat2::dash:

ポイント1: 角丸の地面を歩くアニメーション

まず今回の目玉である「たまさんが角丸の星の上をぴょんぴょん跳ねたり歩いたりする表現」を作っていきましょう。(※たまさんはこのゲームのメインキャラクターです)

長方形の上を歩く

いきなり角丸は難しそうなので、ひとまず星はただの長方形にしました。こんな感じでパラメーターtamaXを渡すといい感じにコーナーリングしてくれるようにしてみます。

たまさんコンポーネント
<TamaSan :tamaX="1.39" /> 

image.png

この計算は単純だけど面倒なので、独立した関数にしておきます。

Angle8.ts
import Pos from './Pos' // x, y, r(角度)をセットで保持する値クラス

export default {
  /**
  * 指定したX位置(一周=8)に対応する座標・回転角を求めます
  * @param val X位置
  * @param gw 地面の幅
  * @param gh 地面の高さ
  */
  at: (val: number, gw: number, gh: number): Pos => {
    const segIndex = Math.floor(val)
    const prog = val - segIndex
    const turnsR = Math.floor(val / 8) * 360

    switch (segIndex % 8) {
      case 0:
        return new Pos(gw * prog, 0, 0 + turnsR)
      case 1:
        return new Pos(gw, 0, prog * 90 + turnsR)
      case 2:
        return new Pos(gw, gh * prog, 90 + turnsR)
      case 3:
        return new Pos(gw, gh, 90 + prog * 90 + turnsR)
      case 4:
        return new Pos(gw * (1 - prog), gh, 180 + turnsR)
      case 5:
        return new Pos(0, gh, 180 + prog * 90 + turnsR)
      case 6:
        return new Pos(0, gh * (1 - prog), 270 + turnsR)
      default:
        return new Pos(0, 0, 270 + prog * 90 + turnsR)
    }
  }
}

これでTamaSanコンポーネントでは

TamaSan.vue
  const tamaPos = computed<Pos>(() => Angle8.at(props.tamaX, ground.w, ground.h))

こんな感じで算出プロパティとして簡単に位置と角度を取得できます。

角丸を歩けるようにする

つづけてこの長方形の角をとって角丸にしていきます。
一見難しそうに思える角丸ですが、実は種明かしをするとすごく簡単。「たまさんのキャラクター本体を角丸のコーナーサイズと同じだけ宙に浮かせているだけ」です

Pasted Graphic 3.png

角丸に限らず、惑星の公転のような円運動は全て同様の理屈で単純な回転角の変更のみで表現できます。覚えておくといろんなものをくるくる回せて楽しいですよ:relaxed::star2:

ポイント2: 星から星に飛び移るアニメーション

今回のゲームでは角丸の星を一周するごとに次の星に飛び移ってゲームが進んでいきます。
一見するとこれも複雑なアニメーションを計算しているように思えますが、実は簡単なCSS Transitionのみで実現しています。

コードより前に動画を見てみると仕掛けがわかります。

ezgif-6-ba6528371c82.gif

そう、実はたまさんは星から星に飛び移っていたのではなく、土台の長方形が移動していただけ(たまさんはその場でジャンプしていただけ)だったのです。
わかってしまえば簡単ですね。

コードで見てみるとこんな感じ:

テンプレート部分
<!-- たまさんの土台(中にたまさん本体もいる) -->
<TamaHome
  :tamaX="tamaHomeState.tamaX / 100"
  :groundPos="activePlanet.pos"
  :groundSize="activePlanet.size"
  :groundRound="activePlanet.round"
/>

<!-- 惑星1 -->
<Planet
  :round="planet1State.round"
  :pos="planet1State.pos"
  :size="planet1State.size"
/>

<!-- 惑星2 -->
<Planet
  :round="planet2State.round"
  :pos="planet2State.pos"
  :size="planet2State.size"
/>

2つの惑星とたまさん(の土台。中にたまさん本体も入ってる)が並列に並んでいます。
たまさんの土台は、activePlanet(後述)の位置・角度・サイズに合わせていることに注目してください。

スクリプト部分も見てみます:

スクリプト部分
setup () {
  /** たまさん(土台)の状態 */
  const tamaHomeState = reactive<TamaHomeState>({
    tamaX: 0,
    planetIndex: 0 // 乗っている惑星のindex
  })

  const planet1State = /* 略:惑星1の位置・角度・サイズ */
  const planet1State = /* 略:惑星2の位置・角度・サイズ */

  /** planetIndexの値によって惑星1か惑星2のどちらかを返す */
  const activePlanet = computed<PlanetState>(() =>
    tamaHomeState.planetIndex % 2 === 0 ? planet1State : planet2State
  )
  // ...略
}

たまさんの土台の位置を決めるactivePlanetはcomputedを使って惑星1か惑星2のどちらかを返すようにしています。ボタンを押すたびにこのactivePlanetが切り替わることで、隣の星に飛び移る(かのような)アニメーションを表現することができるのです。

おまけ:パフォーマンス戦略

先ほどの動画の右側にVueのプロパティを表示してみました。

ezgif-6-83547f826881.gif

位置やサイズの値が書き変わるのはボタンをクリックした瞬間の一度だけなのがわかるかと思います。Tweenライブラリを使ったり、一コマごとに座標を計算してプロパティを変更すればより柔軟なアニメーションを作れますが、その分パフォーマンスは大きく低下します。CSS Transitionで実現できる部分はできるだけまかせて、JavaScript側の処理を減らしてあげると滑らかなアニメーションを実現できます。1

ポイント3:アニメーションの途中で現在の位置や角度をどうやって取得するか

上記したように、アニメーションをTweenやコマ計算ではなく、できるだけCSS Transitionに任せていくのがパフォーマンス向上の重要な戦略です。その一方で途中のアニメーションを全てCSSに任せてしまうとゲームとしては困ったこと:cat::sweat_drops: もでてきます。

今回の場合、

「タップした瞬間にカツオをタップした方に向け、メザシを発射する」
Pasted Graphic 8.png

という部分。
これを実現するにはアニメーションの途中であっても、タップしたその瞬間の位置・角度を取得する必要があります。

image.png

しかし残念なことに、CSS Transitionで変化している途中のプロパティを直接取得する方法がありません:cry:。 0.5秒後にDOMから直接style.transformを取得してもトランジション終了後の値であるtransitionX(500px) rotate(30deg)しか取得できないのです。

任意の時点の位置を取得する

まずは位置からです。
位置の取得は実は去年、当たり判定の処理を作る中でもやっています。具体的にはElement.getBoundingClientRect()を使ってピューポート上での位置を求めればOK。

TamaSan.vue
const getTamaPos = (): Pos | null => {
  // テンプレート内のDOMを取得
  // ※Vue2のthis.$refs('tamaBody')と同じ
  // この部分は後ろの節でも解説しています
  const tamaBody = tamaBody.value // div要素
  if (!tamaBody) { return null }
  const p = tamaBody.getBoundingClientRect()
  return new Pos(p.x + p.width / 2, p.y + p.height / 2, 0)
}

クリックされた時点でたまさんの本体が入っている要素の表示領域(BoundingClientRect)を取得し、その中心を現在の位置として返しています。

角度も取得する

先ほどのgetTamaPosメソットでは角度を0で返してしまっていましたが、カツオをタップした方に向けるためには、今たまさんがどっちを向いているのかを知る必要があります。角度も取得するようにしましょう。

あいにく、Element.getBoundingClientRectでは位置を知ることはできても角度はわかりません。これは、getBoundingClientRectがあくまで画面描画において要素がどこに描画されるかを求める機能しか持たないためです。位置のみを使って角度を求めるため、たまさんのなかに2つの小さなdivを置き、この2つの位置関係から角度を求めることにします。

こんな感じでたまさんの中に2つのDiv要素を配置します

Pasted Graphic 9.png

TamaSan.vue
<div class="pos-detector">
  <div class="detector-top" ref="detTop"></div>
  <div class="detector-bottom" ref="detBottom"></div>
</div>

2つDivを作って

TamaSan.vue
<style lang="scss" scoped>
.pos-detector {
  position: absolute;
  width: 0px;
  height: 100px;
  top: calc(50% - 50px);
  left: 50%;
  div {
    position: absolute;
    width: 1px;
    height: 1px;
  }
  .detector-top {
    top: 0;
  }
  .detector-bottom {
    bottom: 0;
  }
}

たまさん中央に縦に並べるだけ。
あとはこの2つのDivの位置から角度を計算します。先ほどのgetTamaPosメソッドに角度を求める処理を追加します。

TamaSan.vue
const detTop = ref<HTMLDivElement>(null) // Vue2の this.$refs.detTop の宣言
const detBottom = ref<HTMLDivElement>(null) // 同上
const getTamaPos = (): Pos | null => {
  const elTop = detTop.value
  const elBtm = detBottom.value
  if (!elTop || !elBtm) { return null }
  const pTop = elTop.getBoundingClientRect() // 上側の位置を取得
  const pBtm = elBtm.getBoundingClientRect() // 下側の位置を取得
  const cx = (pTop.x + pBtm.x) / 2 // 中心X
  const cy = (pTop.y + pBtm.y) / 2 // 中心Y
  const rad2ang = (rad: number) => rad / Math.PI * 180 // ラジアン→角度の変換関数
  const r = rad2ang(Math.atan2((pBtm.y - pTop.y), (pBtm.x - pTop.x))) // Math.atan2で角度を求める
  return new Pos(cx, cy, r)
}

「2点の座標がわかれば回転角を簡単に求められる」というのは覚えておいて損のない知識かと思います。CSSアニメーションの文脈で使うことは滅多にないと思いますが、ゲームやビジュアル表現ではよく使う計算です。

ポイント4:TypeScript + CompositionAPIの採用

:angel:この節はコードばっかりなので興味ない方は飛ばしつつ見てくださいませ:angel:

冒頭でも書いた通り、今回はもうすぐやってくるVue3を見据えて、CompositionAPI + TypeScriptの構成に挑戦しています。CompositionAPI + TypeScriptで何が変わるの?って部分は以前の記事を見てみてください。従来の書き方との対応がわかりやすいかと思います。

Vue.jsレベルを上げよう!○×ゲームを作ってTypeScript&Vue3のCompositionAPIと仲良くなる

ここでは、基本のCompositionAPI + TypeScriptは理解した上で、つまづきポイントと解決策を共有します。

$refs(テンプレートRef)どこいった問題

テンプレートRefは以下のようにしてtemplate部分で指定した要素や子コンポーネントを参照する機能です。

Vue2標準のテンプレートRef
<template>
  <div>
     <button @click="getSpan">Get ref</button>
     <span ref="msg">Hello</span>
  </div>
</template>

<script>
export default {
  methods: {
    // this.$refsでテンプレート内のSpan要素を取得できる
    getSpan () { console.log(this.$refs('msg')) }
  }
}
</script>

CompositionAPIではrefを使います。名前は似てるけど使い方はだいぶん違うので注意

CompositionAPIのテンプレートRef
<template>
  <!-- 同じなので省略 -->
</template>

<script lang="ts">
import { createComponent, ref } from '@vue/composition-api'
export default createComponent({
  setup () {
    const msg = ref() // 中身のないrefを作る。※重要なのは名前※
    // msg.valueでテンプレート内の要素にアクセスできるようになる
    const getSpan = () => { console.log(msg.value) }
    return {
      msg,
      getSpan
    }
  }
})
</script>

紛らわしいのが、このrefは基本的には従来の$dataを代替するものなのに、なぜかテンプレートRefの機能も兼ねているところ。RFCの解説にRefの説明はあるのですが、これを読んでもいまいちテンプレートRefについては理解できないのでは?という気がします....

テンプレートRefの型を決めたい(HTMLElement編)

なんとかテンプレートの要素にアクセスできたところで、次に問題なるのはTypeScriptの型問題です。このままだとmsg.valueのようにして取得した要素をspanとして扱えないのでちょっと嫌ですよね。

前項までのはなしは一応公式にサンプルも書かれているのですが、これ、JSですね...
https://vue-composition-api-rfc.netlify.com/api.html#template-refs

TSでのやり方がなぜか見つからないのですが、一応、下記のようにすれば型を明示することができます。

テンプレートRefの型を明示する
<script lang="ts">
    ...  ...
    const msg = ref<HTMLSpanElement>() // 型を明示してrefを作る
    const getSpan = () => {
      const msgSpan = msg.value // HTMLSpanElementとして取得できる
    }
    ...  ...
</script>

テンプレートRefで子コンポーネントにアクセスしたいんだってば:angry:

OK、普通のSpanやDivならなんとかなった。じゃあコンポーネントだと?
↓これでいけそうな気がするじゃないですか?

子コンポーネントにアクセス(ダメな例)
<template>
  <div>
    <TamaSan ref="tamaRef" /><!-- このたまさんにアクセスしたい -->
  </div>
</template>

<script lang="ts">
import { createComponent, ref } from '@vue/composition-api'
import TamaSan from './TamaSan.vue' // たまさんコンポーネント読み込み

export default createComponent({
  components: { TamaSan },
  setup () {
    const tamaRef = ref<TamaSan>() // TamaSan型
    return { tamaRef }
  }
})
</script>

怒られます:anger: 。「TamaSanは型ではなく値なので、型を指定しろ」とのお言葉。以下のようにするとうまくいきます:

子コンポーネントにアクセス(うまくいく例1)
const tamaRef = ref<InstanceType<typeof TamaSan>>()

TS分かんね:innocent:ってなるやつですね。
TS初心者の私はこれ見つけるまでにStackOverflowを2時間くらいさまよいました。(そしてリンク失念しました...ごめんなさい:innocent:

また、コンポーネント固有のデータやメソッドは不要で、単にVueのコンポーネントとして扱いたいだけであれば、以下のようにすることもできます:

子コンポーネントにアクセス(うまくいく例2)
const tamaRef = ref<Vue>()

(ちなみにこの書き方だと従来の$refs$elにアクセスすることもできます)

うん、複雑。。
しかもこのあたりの型は別に自動的に判別してくれているわけではなく、あくまでも宣言に従って型を当てはめてくれているにすぎません。ちゃんと宣言すればエディタ上での作業は快適になりますが、宣言を誤ればそのまま実行時エラーなので、あまり安全とは言えない気がします。

このあたりはまだまだVue + TypeScriptの辛いところだなぁ...というのが正直な感想です。。

まとめ

そんなわけで今年も気合いで新ゲームをリリースすることができました:sob:
去年一年ことあるごとにVueでゲーム作るの楽しいよ!!!って言い続けてるのですが、イマイチまだ流れが来ていない気がします。

:relaxed: もっとみんなVueで遊ぼう :relaxed:

この記事では駆け足で流してしまった部分も、過去にいくつか解説している記事があるので、よろしければご参照くださいませ:


  1. 特にスペックの低い旧機種のiPhoneではこの恩恵が大きく出ます。今回のゲームの場合、iPhone6レベルでも一度アニメーションを開始してしまえばコマ落ちをほぼ感じずにプレイすることができます。このあたりは以前の記事will-changeで目指す60fpsのぬるぬるCSSアニメーションをご参照くださいませ。 

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

Vueチートシート

証券営業 → テックエキスパート卒業生 → インターネット広告会社(エンジニア6ヶ月目)
Twitter → https://twitter.com/ar_tokki
 
言語
 python Vuejs AWS
 機械学習と統計の勉強中
 
TECH::EXPERTを卒業後、転職して半年間の振り返り
 https://qiita.com/tokki7127/items/2eb5bbd3b1bb54e33824

Vue.js研修資料
 https://qiita.com/tokki7127/items/b3826db40e275b8e83db

この資料の目的 

Vue.js研修をするときの後輩と自分用にまとめたもの
あくまでチートシートなので詳細な説明はしていない!

EXPRESSIONS

<div id="app">
 <p>I have a {{ product }}</p>
 <p>{{ product + 's' }}</p>
 <p>{{ isWorking ? 'YES' : 'NO' }}</p> 
 <p>{{ product.getSalePrice() }}</p>
</div>

DIRECTIVES

<!-- Element inserted/removed based on truthiness:-->
<p v-if="inStock">{{ product }}</p> 

<p v-else-if="onSale">...</p>
<p v-else>...</p>

<!--Toggles the display: none CSS property:-->
<p v-show="showProductDetails">...</p> 

<!--Two-way data binding:-->
<input v-model="firstName" >

v-model.lazy="..."    <!--Syncs input after change event-->
v-model.number="..."   <!--Always returns a number-->
v-model.trim="..."    <!--Strips whitespace-->

LIST RENDERING

<li v-for="item in items" :key="item.id">  <!--:key =key always recommended -->
  {{ item }}
</li> 

<!--To access the position in the array:-->
<li v-for="(item, index) in items">... 

<!--To iterate through objects:-->
<li v-for="(value, key) in object">... 

<!--Using v-for with a component:-->
<cart-product v-for="item in products" :product="item" :key="item.id">

BINDING

<a v-bind:href="url">...</a>
  <!--↓shorthand -->
<a :href="url">...</a>

<!--True or false will add or remove attribute:-->
<!--<button :disabled="isButtonDisabled”>-->

<!--If isActive is truthy, the class ‘active’ will appear:-->
<div :class="{ active: isActive }">... 

<!--Style color set to value of activeColor:-->
<div :style="{ color: activeColor }"> 

ACTIONS / EVENTS

<!--Calls addToCart method on component:-->
<button @click="addToCart">... 
  <!--↓shorthand-->
<button v-on:click="addToCart">...

<!--Arguments can be passed:-->
<button @click="addToCart(product)">... 

<!--To prevent default behavior (e.g. page reload):-->
<form @submit.prevent="addProduct">... 

<!--Only trigger once:-->
<img @mouseover.once="showImage">...

.stop   <!--Stop all event propagation-->
.self   <!--Only trigger if event.target is element itself-->

<!--Keyboard entry example:-->
<input @keyup.enter="submit"> 

<!--Call onCopy when control-c is pressed: -->
<input @keyup.ctrl.c="onCopy">

<!--Key modifiers:-->
  .tab            .up          .ctrl
  .delete         .down        .alt
  .esc            .left        .shift
  .space          .right       .meta
 <!--Mouse modifiers:-->
.left .right .middle

COMPONENT ANATOMY

Vue.component('my-component', {
  components:{ //<!--Components that can be used in the template-->
      ProductComponent, ReviewComponent
   },
  props:{ //<!--The parameters the component accepts-->
   message: String, 
   product: Object, 
   email:{
    type: String,
    required: true,
    default: "none"
    validator:function (value) {
      //<!--TheShould return true if value is valid-->
    } 
   }
  },
  data:function() { //<!-- Must be a function-->
    return {
      firstName: 'Vue',
      lastName: 'Mastery'
    }
  },
computed: {
 //<!--Return cached values until -->
  fullName: function () {  //<!--dependencies change-->
   return this.firstName + ' ' + this.lastName
  }
},
watch: {  //<!--Called when firstName changes value-->
  firstName: function (value, oldValue) { ... }
},
methods: { ... },
template: '<span>{{ message }}</span>',
})
 //<!--Can also use backticks for multi-line-->

CUSTOM EVENTS

<!--Use props (above) to pass data into child components, 
custom events to pass data to parent elements.
Set listener on component, within its parent:-->
<button-counter v-on:incrementBy="incWithVal">

<!--Inside parent component:-->
  methods: {
    incWithVal: function (toAdd) { ... }
}

<!--Inside button-counter template:-->
<!--incrementBy = Custom event name--> 
this.$emit('incrementBy', 5) 
<!--Data sent up to parent-->
#LIFECYCLE HOOKS
```.vue
・beforeCreate    ・created
・beforeMount    ・mounted
・beforeUpdate    ・updated
・beforeDestroy   ・destroyed

USING A SINGLE SLOT

<!--Component template:-->
   <div>
     <h2>I'm a title</h2>
       <slot>
         Only displayed if no slot content
       </slot>
   </div>
Use of component with data for slot:
  <my-component>
     <p>This will go in the slot</p>
</my-component>

MULTIPLE SLOTS

<!--Component template:-->
  <div class="container">
     <header>
       <slot name="header"></slot>
     </header>
     <main>
       <slot>Default content</slot>
     </main>
     <footer>
       <slot name="footer"></slot>
     </footer>
  </div>
<!--Use of component with data for slot:-->
  <app-layout>
   <h1 slot="header">Page title</h1>
    <p>the main content.</p>
    <p slot="footer">Contact info</p>
  </app-layout>

LIBRARIES YOU SHOULD KNOW

Vue CLI
 Command line interface for rapid Vue development.
Vue Router
 Navigation for a Single-Page Application.
Vue DevTools
 Browser extension for debugging Vue applications.
Nuxt.js
 Library for server side rendering, code-splitting, hot-re- loading, static generation and more.
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

NuxtにTypeScriptを導入する備忘録(2020年3月)

NuxtでTypeScriptを導入するにあたって、まず公式のドキュメントを頼りにしてみたものの上手くいかない事があるので、備忘録的に手順をまとめてみました。ゴールは「Nuxt環境でTypeScriptによるクラスベースの開発が出来るようになる事」です。

  1. 公式の手順を踏んでTypeScriptを導入
  2. jsconfig.json の作成・編集
  3. Lint設定
  4. vue-property-decorator の導入

事前準備 : Nuxtインストール

参考までに今回インストールしたNuxtはこんな構成です。

$ npx create-nuxt-app

create-nuxt-app v2.14.0
Generating Nuxt.js project in .
? Project name nuxt-ts-demo
? Project description My terrific Nuxt.js project
? Author name mach3
? Choose the package manager Npm
? Choose UI framework None
? Choose custom server framework None (Recommended)
? Choose Nuxt.js modules Axios
? Choose linting tools ESLint
? Choose test framework None
? Choose rendering mode Single Page App
? Choose development tools jsconfig.json (Recommended for VS Code)
  • 最後の jsconfig.json はあとでまるっと書き換えてしまうので要らないといえば要らないです
  • Prettier はとにかく消耗させてくるので入れていません(個人の感想です)

1. 公式の手順を踏んでTypeScriptを導入

公式ドキュメント : https://typescript.nuxtjs.org/ja/guide/setup.html

@nuxt/typescript-build のインストール

$ npm i @nuxt/typescript-build -D

nuxt.config.js の編集

nuxt.config.js
  ...
  buildModules: [
    '@nuxtjs/eslint-module',
    '@nuxt/typescript-build' // <- 追加
  ],
  ...

tsconfig.json の追加と編集

ファイルの中身は公式の物をコピー&ペーストする。

$ touch tsconfig.json

2. jsconfig.json の作成・編集

jsconfig.json は VS Code 向けの設定ファイルです。したがって他のエディタを使用している場合はスキップしても良さそうです。

jsconfig.json はNuxtインストール時に選択肢を選べば自動生成してくれますが、内容を tsconfig.json に合わせてやる必要があります。構成は両者とも同じなので、単純にまるっと転記してしまいます。

$ cat tsconfig.json > jsconfig.json

書き換えられた jsconfig.json をエディタで開くと分かりますが、いくつか許可されていないプロパティがあるので、そこは削除します。( allowJStypes )

jsconfig.json
{
  "compilerOptions": {
    "target": "es2018",
    "module": "esnext",
    "moduleResolution": "node",
    "lib": [
      "esnext",
      "esnext.asynciterable",
      "dom"
    ],
    "esModuleInterop": true,
    "experimentalDecorators": true,
-   "allowJs": true,
    "sourceMap": true,
    "strict": true,
    "noEmit": true,
    "baseUrl": ".",
    "paths": {
      "~/*": [
        "./*"
      ],
      "@/*": [
        "./*"
      ]
    },
-   "types": [
-     "@types/node",
-     "@nuxt/types"
-   ]
  },
  "exclude": [
    "node_modules"
  ]
}

この2ファイルの内容を同期しておけば、 Vetur 等でエラーメッセージがおかしくなることも少なくなるかと。(ゼロになるとは言ってない)

3. Lint設定

ESからTSに変わるのでLint周りも変更しなければなりません。NuxtからEslint Configが提供されているのでそれを使います。

@nuxtjs/eslint-config-typescript を導入

$ npm i @nuxtjs/eslint-config-typescript -D

.eslintrc.js の編集

.eslintrc.js
module.exports = {
  ...
  parserOptions: {
-   parser: 'babel-eslint'
  },
  extends: [
    '@nuxtjs',
+   '@nuxtjs/eslint-config-typescript',
    'plugin:nuxt/recommended'
  ],
  ...
}

Thanks) Nuxt v2.9 + Typescriptの環境構築 - Qiita

4. vue-property-decorator の導入

vue-property-decorator は Vue + TypeScript でクラスベースでアプリを書いていく為のライブラリです。
別にこれを使わなくても TypeScript で書いていくことは出来ますが、入れたから使わなくてはならないわけではないのでとりあえず入れておきましょう。

導入する

$ npm i vue-property-decorator -D

使用する場合は、 tsconfig.jsonjsconfig.jsonexperimentalDecorators オプションを設定してやらないとエラーになります。

(tsconfig|jsconfig).json
{
  "compilerOptions": {
    ...
+   "experimentalDecorators": true,
    ...
  }
}

使ってみる

pages/index.vue でテストしてみます。

Before

<script>
import Logo from '~/components/Logo.vue'

export default {
  components: {
    Logo
  }
}
</script>

After

<script lang="ts">
import { Vue, Component } from 'vue-property-decorator'
import Logo from '~/components/Logo.vue'

@Component({
  components: { Logo }
})
export default class IndexPage extends Vue {
}
</script>

おまけ : 遭遇したエラーたち

Experimental support for decorators is...

Experimental support for decorators is a feature that is subject to change in a future release. Set the 'experimentalDecorators' option in your 'tsconfig' or 'jsconfig' to remove this warning.

と言われる。この機能を利用するためには、tsconfig.jsonjsconfig.jsonexperimentalDecorators オプションを設定する必要があるらしいので、その通りにする。

Please use export @dec class instead

error  Parsing error: Using the export keyword between a decorator and a class is not allowed. Please use `export @dec class` instead.

と怒られる。 @nuxtjs/eslint-config-typescript の導入が必要らしいのでそのようにする。

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

VueでFirebaseを使いログイン認証が必要な専用ページを作ろう

はじめに

今回はVueとVue RouterとFirebaseを用いてログイン認証後の必要ページを作ってみましょう!
前準備の部分が長くなるので、認証後の処理だけを見たい人はこ↓こ↑をクリック

環境

  • Vue.js 2(CLI)
  • Vue Router
  • Firebase
  • Yarn

下準備

まずは新しいプロジェクト作り

4be703b086ad849f77fce01da0fdfc7a.png
マニュアルで作りましたが、Routerが必要なのでインストールされてあれば他のをインストールしていても問題ありません。
この記事では、説明のために最小構成でプロジェクトを作成します。
b5cda66b7fe426d4c00d0d9c9affc9f8.png
次ですが、ここはhistoryモードを使うのでyで。
655d71fb30622b2d19fca7d2eef1fe27.png
この後も数回、環境の設定をどうするか聞かれますが、今回のRoutingには影響しないので好みの設定で大丈夫です。
5197b02c266ef40242a00b41e11d373c.png
無事に作成が完了したら、一度yarn serveを行い、バニラ状態のページを確認しておきましょう!
a34062308900b9e39a4610b3c333a8ef.png

Firebaseをインストール

次にFirebase CLIをインストールしましょう。
Firebase CLI 公式ドキュメント

npm

npmの場合ですと、公式ドキュメントにある通り、

グローバル

npm install -g firebase-tools

ローカル

npm install -D firebase-tools

yarn

yarnを使用している例は公式にはありませんが、いつもの方法でインストールすることは可能です。

グローバル

yarn global add firebase-tools

ローカル

yarn add firebase-tools

正しくインストールされてるか確認

最後に正しくインストールされてるか確認しましょう。

firebase -V

と入力して、バージョン表記が表示されれば正しくインストールされています!
(※2020年2月29日現在では、7.14.0が最新)

Firebaseの設定をする

インストールが無事終えたら、次はインストールしたプロジェクトのFirebaseの設定をします。

Firebase init

このコマンドを打ち込みます。
すると、英語で「初期化を行う手続きを続行してもいいかー?」(意訳)の問が出現し、
その後に、Firebaseのどの機能を使用するか選択する問が現れます。
abbda024bb2d5a0d8345252559651297.png

今回は認証だけをやりたいのですが、少なくとも1つは機能を選択しなければいけないため、Hostingを選択しました。

次にFirebaseのどのプロジェクトを使用するか?を聞かれます。
930f375809aeb1f7a759796d9674947c.png

1つ目の選択肢はすでにFirebase上に存在する既存プロジェクトを使用する
2つ目は新しくプロジェクトを作成する
となっています。
もしプロジェクトを作っていなかったら2を、既存のを使用するなら1を選択しましょう。

この後、選択した機能毎をどう設定するかの問が行われ、
その問に答えた後に、SPA(シングルページアプリケーション)として設定するかの問が行われます。
SPAを使用したいので、これにはyと入力します。

a8f960e01b79f035f9c9ccbe0d5962df.png
Firebaseの初期化の操作が終わり、このように表示されたら無事完了です!

Firebase ConsoleからAPI情報を作成

先にAPI情報などを記載するfirebase.jsを作成しましょう。
srcフォルダ配下に作成します。

firebase.js
import firebase from "firebase";
import "firebase/auth";

const config = {
  apiKey: /**/,
  authDomain: /**/,
  databaseURL: /**/,
  projectId: /**/,
  storageBucket: /**/,
  messagingSenderId: /**/,
  appId: /**/,
  measurementId: /**/
};

firebase.initializeApp(config);

export default firebase;

configの中の情報は、Firebase Consoleのプロジェクト設定から取得してきましょう。

firebase/authをImportすることで、Firebaseの認証機能を使えるようにImportしています。
これらは必ず行うようにしましょう。

これで下準備は完了です!
次から実際に作成していきましょう!

必要なコンポーネントを作ろう

ルーティングや認証を行う処理などを作る前に、先に必要なコンポーネントを作ります。
必要なコンポーネントは、

  • 登録(sing up)
  • ログイン(sing in)
  • ログアウト(sing out)
  • 認証専用ページ

の4つになるので、これらを作っていきましょう。
サイトトップは初期作成で作られたコンポーネントをそのまま使ってしまいましょう。

コンポーネントを作成

先程必要になる4つのコンポーネントを作っていきましょう。

  • 登録(sing up)
  • ログイン(sing in)
  • ログアウト(sing out)

の3つはコンポーネントは、Firebaseとの通信が必要になるため、
先ほど作っておいたAPI情報を含め初期化しておいたfirebase.jsをImportしておきましょう。
そうしないと、Firebaseの機能を使うことができません。

登録(sing up)

singup.vue
<template>
  <div>
    <h1>SING UP</h1>
    <div>
      <h3>E-mail</h3>
      <input type="text" placeholder="E-mail" v-model="email" />
    </div>
    <div>
      <h3>Password</h3>
      <input type="text" placeholder="Password" v-model="password" />
    </div>
    <button @click="createUserAccount">Sing UP!!</button>
  </div>
</template>

<script>
import firebase from "../firebase.js";
export default {
  name: "singup",
  data() {
    return {
      email: "",
      password: ""
    };
  }
};
</script>

ログイン(sing in)

singin.vue
<template>
  <div>
    <h1>SING IN</h1>
    <div>
      <h3>E-mail</h3>
      <input type="text" placeholder="E-mail" v-model="email" />
    </div>
    <div>
      <h3>Password</h3>
      <input type="text" placeholder="Password" v-model="password" />
    </div>
    <button @click="userSingIn">Sing in Now!!</button>
  </div>
</template>

<script>
import firebase from "../firebase.js";
export default {
  name: "singin",
  data() {
    return {
      email: "",
      password: ""
    };
  }
};
</script>

ログアウト(sing out)

singout.vue
<template>
  <div>
    <h1>SING OUT</h1>
    <button @click="singout">Sing out Now!!</button>
  </div>
</template>

<script>
import firebase from "../firebase.js";
export default {
  name: "singout"
};
</script>

認証専用ページ

mypage.vue
<template>
  <div>
    <h1>SING OUT</h1>
    <p>My Page!!<p>
  </div>
</template>

<script>
export default {
  name: "mypage"
};
</script>

ルーティング

次に作成したコンポーネントを合わせてルートレコードを設定していきましょう

ルートファイルを設定

router.js
import Vue from "vue";
import VueRouter from "vue-router";
import Home from "../views/Home.vue";
/*ここから新規追加*/
import Singup from "../components/singup.vue";
import Singin from "../components/singin.vue";
import Singout from "../components/singout.vue";
import Mypage from "../components/mypage.vue";
/*ここまで*/

Vue.use(VueRouter);

const routes = [
  {
    path: "/",
    name: "home",
    component: Home
  },
  {
    path: "/about",
    name: "About",
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () =>
      import(/* webpackChunkName: "about" */ "../views/About.vue")
  },
  /*ここから新規追加*/
  {
    path: "/singup",
    name: "singup",
    component: Singup
  },
  {
    path: "/singin",
    name: "singin",
    component: Singin
  },
  {
    path: "/singout",
    name: "singout",
    component: Singout
  },
  {
    path: "/mypage",
    name: "mypage",
    component: Mypage
  }
  /*ここまで*/
];

const router = new VueRouter({
  mode: "history",
  base: process.env.BASE_URL,
  routes
});

export default router;

先程作成したコンポーネントをrouter.jsに追加し、ルーティング設定しました。
次に、App.vueの中に作ったコンポーネントへ遷移するためのリンクを設定しましょう。

App.vue
<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link> |
      /*ここから新規追加*/
      <router-link to="/singup">Sing up</router-link> |
      <router-link to="/singin">Sing in</router-link> |
      <router-link to="/singout">Sing out</router-link> |
      <router-link to="/mypage">Mypage</router-link> |
      /*ここまで*/
    </div>
    <router-view />
  </div>
</template>

ここまでのを確認

yarn serveを行い、一旦ここまでの作成段階のを確認しましょう。
e0668afb493addf7c04efaa1a61ce9c7.png
Sing upからMypageまでのリンクを押し、無事ページが遷移したら成功です。

Firebaseを使い認証

FirebaseでE-mailとPasswordで登録できるように設定しよう

認証などの処理を行う前に、Firebase Console画面で、Authenticationの設定を行いましょう。

Authenticationのログイン方法で、メール/パスワードを有効化します。
8ff1cf5ec77c4e7b587ca1546b1a069e.png

02d64008938d5b32fbc3ef791a6c133f.png

メールリンクは今回は有効化しなくてもOKです。
これでメール/パスワードを用いてユーザー登録を行う準備が完了しました。

Firebaseにアカウントを登録しよう

先程作ったSingupコンポーネントに、ユーザーアカウントを追加する処理を組み込みましょう。
Firebaseのユーザーを管理する

singup.vue
<script>
  methods: {
    createUserAccount() {
      firebase
        .auth()
        .createUserWithEmailAndPassword(this.email, this.password)
        .then(() => {
          alert("Create Account");
        })
        .catch(error => {
          alert("Error!", error.message);
          console.error("Account Regeister Error", error.message);
        });
    }
  }
</script>

E-mailPasswordを用いてのよくあるユーザー登録を行うには、
firebase.authcreateUserWithEmailAndPasswordメソッドを使用します。
第一引数にE-mail、第二引数にPasswordを渡してあげると、あとはよしなにやってくれます。
非同期で行うため、結果はthenチェーン内で行う必要があります。
あとは、このメソッドをButtonコンポーネントより呼び出せば登録処理部分は完了です。

ログイン処理をしよう

次はログインを行う処理を追加するために、作っておいたSing inコンポーネントに処理を書き込んでいきましょう。

singin.vue
<script>
  methods: {
    userSingIn() {
      firebase
        .auth()
        .signInWithEmailAndPassword(this.email, this.password)
        .then(() => {
          alert("ログイン成功!");
          this.$router.push("/mypage");
        });
    }
  }
</script>

firebase.authsignInWithEmailAndPasswordメソッドを使い、ログイン処理を行います。
ユーザー登録時と同じ様に、引数にそろぞれE-mailPasswordを渡ししてあげればOKです。
無事ログイン処理が行えたら、thenチェーンで内でRouterpushメソッドを使い、ページを遷移させています。

お試しにログインしてみる

先程作ったユーザ登録画面とログイン画面で、ユーザー登録を行えログインできるか試してみましょう。
ユーザ登録画面で登録が成功とalertが表示され、Firebase Consoleの認証関連のページで、ユーザーが登録されているはずです。
b1a67ca936b76a15b9d4cc8d3fbc95cc.png

ログイン画面で、登録したE-mailPasswordでログインができ、画面がMypageに遷移したらログイン画面も無事作成できています。

ログアウト処理も作ってみる

ログインページが完了したので、次はログアウトページも作っておきましょう。

singout.vue
<script>
  methods: {
    singout() {
      firebase
        .auth()
        .signOut()
        .then(() => {
          alert("Logout!");
        })
        .catch(error => {
          alert(error);
        });
    }
  }
</script>

firebase.authsignOutメソッドを使いログアウト処理を行っています。
ログアウト処理はこれだけで完了です。
ログアウト後にページトップに戻るような動きをつけたい場合には、
ログイン処理時に使った、

this.$router.push();

を使い、pushの引数の中に遷移させたい先のルート名を渡しましょう。

ページにログイン認証を組み込もう

ログイン/ログアウトできるようになりましたが、現状ではログイン時のみしか表示させたくないMypageがログアウト時でも見ることができています。
次は、このMypageをログイン時のみ表示するようにしましょう。
(ようやくここに来て本題に到達)

認証が必要なことの状態をもたせよう

まず、router.jsに認証処理を追加していきましょう。

router.js
  {
    path: "/mypage",
    name: "mypage",
    component: Mypage,
    meta: { requiresAuth: true }
  }

まずは、mypageのルーティング部分にmetaフィールドを追加します。
Vue Router - ルートメタフィールド
metaフィールドに{ requiresAuth: true }のオブジェクトをもたせて、このルートは認証が必要であることの状態をもたせます。
ただし、これだけでは状態を持っているだけなので、ナビゲーションガード、未ログインユーザーがこのページにアクセスしてもページが表示されてしまいます。
別途、ルートガードの処理を作る必要があります。

ナビゲーションガードを実装しよう

Vue Router - ナビゲーションガード
公式にある、router.beforeEachを用いナビゲーションガードを実装してみましょう。
``

router.js
const router = new VueRouter({
  mode: "history",
  base: process.env.BASE_URL,
  routes
});

router.beforeEach((to, from, next) => {
  const requiresAuth = to.matched.some(recode => recode.meta.requiresAuth);
  if (requiresAuth) {
    next({ path: "/singin", query: { redirect: to.fullPath } });
  } else {
    next();
  }
});

export default router;

router.beforeEachexport default routerの前に追加しました。
to.matched.some(recode => recode.meta.requiresAuth)で、requiresAuthの状態をもつルートレコードなのかを確認しています。
もし、requiresAuthならnextを用いてsinginへ、そうでないならユーザーが選択したルートレコードへ遷移します。
ただし、この状態ですとログインしているかどうかを確認していないので、mypageへアクセスすると誰でもログインページへ戻ってしまいます。
次は、Firebaseからログイン状態を取得してみましょう。

Firebaseからログイン状態を取得

router.beforeEachの中にログイン状態を取得する方法もありますが、
今回はfirebase.jsに取得処理を書き、処理を分離化させる方法でやってみましょう。
firebase.jsの中に書いていきましょう。

firebase.js
firebase.getCurrentUser = () => {
  return new Promise((resolve, reject) => {
    const unsubscribe = firebase.auth().onAuthStateChanged(user => {
      unsubscribe()
      resolve(user);
    }, reject);
  });
};

Firebase のユーザーを管理する
onAuthStateChangedを用い、ユーザーがログイン状態かを確認できます。
そして、このメソッドがログイン状態になったら即時にその情報を返します。

最後に先程のrouter.beforeEachにこのログイン状態かを確認する処理を追加しましょう。

router.js
const router = new VueRouter({
  mode: "history",
  base: process.env.BASE_URL,
  routes
});

router.beforeEach(async (to, from, next) => {
  const requiresAuth = to.matched.some(recode => recode.meta.requiresAuth);
  if (requiresAuth && !(await firebase.getCurrentUser())) {
    next({ path: "/singin", query: { redirect: to.fullPath } });
  } else {
    next();
  }
});

export default router;

await firebase.getCurrentUser()は先程firebase.jsに作成したメソッドとなります。
firebase.getCurrentUser()Promiseの非同期メソッドになるので、
router.beforeEach内の関数にasyncをつけ、これも非同期メソッドとして上げる必要があります。
そして、if文の条件にfirebase.getCurrentUser()を追加しますがこれは!をつけ否定の条件にしておきます。

if (requiresAuth && !(await firebase.getCurrentUser())

そして、2つをand条件にすることで
- 認証が必要なコンポーネント
- ログイン状態でではない

を満たす場合のみに、ログインページへ遷移するようにしました。

最後に無事動作するか確認

最後に、yarn serveを行い、ログインを行ってみてページ遷移するか、
ログアウト状態でMypageへアクセスした場合に、ログイン画面へ戻るかを確認してみましょう。
無事動けば完成です!

おわり

長くなりましたが、今回はFirebaseを用いてVueでのログイン認証が必要なページの作り方を記事にしてみました。
Vueの認証が必要なページの作り方は、Firebaseを使わなくてもrouter.beforeEachの中で処理するのは共通になるので、一度覚えておけば他の認証サービスやAPIを使った場合でも応用は楽にできると思います。

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

《FE編》数式を使って質問できるQ&Aサービス「el-pot」をローンチしました

はじめに

現在法政大学の4年のKATUOと申します。大学2年生のときに授業で習ったC言語がきっかけでプログラミングを初め、ここまで複数のIT企業でインターン生の枠組みでエンジニアの業務を行ってきました。大学生の間に個人サービスをリリースするのが目標で、既に社会人として働くインターン同期の友人と2人で週末開発を進め、何とかローンチまで漕ぎ着けました。その報告をこの場をお借りしてしたいと思います。

開発したサービス

電気電子工学の知識を共有できる「el-pot」と呼ばれるサービスを開発しました。

Screen Shot 2020-03-01 at 16.22.39.png

このサービスの特徴は数式(tex)を使って質問をすることができるという点です。いままで大学の授業でわからなかった箇所を調べてSEO上位に出てくるサイトはどれも数式対応しておらず次のような表記になっていた為、非常に文章が読みづらいという課題がありました。

[(x-1)/(x+1)]’=[1(x+1)-1(x-1)]/(x+1)^2=2/(x+1)^2

この式をもっとわかりやすく次のように表示させることができます。

Screen Shot 2020-03-01 at 16.37.05.png

このように数式を文章中に挿入できるようになったことで従来のQ/Aサービスよりもわかりやすく質問/回答を行うことができるプラットフォームを実現しました。

開発の話

私はフロントエンド、友人はバックエンド・インフラ周りを担当しました。今回の記事ではフロントエンドについて簡単にお話ししたいと思います。バックエンド・インフラについては別途記事を友人が書きますので興味のある方はそちらをご覧ください。(まだ公開してないです。すいません。)

vue

フロントエンドを作るに当たって採用したフレームワークは「Vue.js」です。

vuejs

選んた理由は普段の業務で使っていて慣れているからという単純な理由です。では今回のサービス開発で使用した主要なVueライブラリ・アーキテクチャーを紹介します。

vuex

状態管理パッケージです。コンポーネント間のデータのやりとりを実装する為に採用しました。

Vuex は Vue.js アプリケーションのための 状態管理パターン + ライブラリです。 これは予測可能な方法によってのみ状態の変異を行うというルールを保証し、アプリケーション内の全てのコンポーネントのための集中型のストアとして機能します。 また Vue 公式の開発ツール拡張と連携し、設定なしでタイムトラベルデバッグやステートのスナップショットのエクスポートやインポートのような高度な機能を提供します。

vuetify

ゼロベースでコンポーネントのデザインを考えるは大変だった為、「Vuetify」というUIライブラリを採用しました。alert系やdialog系のコンポーネントが使い易かったです。

vuetify

Vuetifyは、美しく手作りされたマテリアルコンポーネントを備えたVue UIライブラリです。設計スキルは必要ありません。素晴らしいアプリケーションを作成するために必要なものはすべて手元にあります。

参照元:「vuetify 公式サイト」

mavon-editor

主要機能である数式エディタは「mavon-editor」というライブラリを使用しました。

mavon-editor

このライブラリは更新が2年前から開発がストップしているみたいです。issueなどがたくさん立っていますが、ほとんどスルーされています。なので個人的には使うことをお勧めしません。実際「el-pot.v2」では自作のライブラリを導入する予定です。(mavon-editor自体のカスタマイズが難しいという点)

atomic design

コンポーネントのディレクトリのアーキテクチャーを設計するにあたって「atomic design」を採用しました。atomic designとはコンポーネントの種類を「atoms」「molecules」「oraganisms」に分類して管理する方法です。

atomic-web-design

今後の予定

ひとまずリリースはしましたがまだまだサービスとしての質は低いです。インフラ面での可用性であったり、UI/UXの低さ、機能の少なさ。などなど数々の問題を抱えています。これらを改善する為、今後も開発を進めていく予定です。その中でも特に自分がやりたいのは「OSSの自作エディタのリリース」です。エンジニアである以上、OSSを世の中に出してみたいという気持ちがあります。なので今回自作できなかった数式エディタのOSSを作ろうと思っています。おそらくVue.jsを使って開発することになりそうです。

最後に

1日最低3回、Twitterで技術のことなどをつぶやいますのでよかったらフォローしてください。また、MAU1万程度のBlogもほぼ毎日更新していますのでこちらもよかったらご覧ください。

最後まで読んでくれてありがとうございました。

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