/var/log/Sawada.log

SAINO中毒患者の備忘録。

Vuetifyを使いポートフォリオで遊んでいこう

はじめに

どうも,こたつでアイスを食べるさわだです.
Muroran Institute of Technology Advent Calendar 2019の枠の拝借して記事を書きます.
今回はポートフォリオを作った話です. --> My Portfolio

adventar.org

遅れて...こ゛め゛ん゛な゛さ゛い゛て゛し゛た゛😭

...さて,目次です.

ポートフォリオとは

語源はPortafoglio,dictionary.comにある定義によれば,

  1. a flat, portable case for carrying loose papers, drawings, etc.
  2. such a case for carrying documents of a government department.
  3. the total holdings of the securities, commercial paper, etc., of a financial institution or private investor.
  4. the office or post of a minister of state or member of a cabinet.

となります,いわゆる「書類ケース,そこに収納されているもの全て」の様なイメージですかね.
自分としては作品や活動をまとめたものという認識だったのですが,金融では保有資産の一覧などを示すようです.

ちなにみPortafoglioで画像検索するとこんな感じ.これは...財布ですね.なるほど.

f:id:takuzoo3868:20191222141839p:plain
portareは「持っていく」,foglioは「紙」の意味

余談ですが,クリエイター志望の方は
以下のようなサービスを使ってアピールしているそうです.

www.vivivit.com

同じ様に情報系の学生もサクッと作る場合は,Wantedly
LinkedInを活用して職務歴兼ポートフォリオを作成してるかと思います.
自分の場合も一応登録していますが,その根本にあるのは今回のポートフォリオサイトです.

ポートフォリオ・ビフォー・アフター

さて,サービスが充実してるのに,なんでわざわざサイト作ったの?と言われると,
パフォーマンスとかレンダリングエンジンの勉強にもなるかな?と思ったからです.
web技術の動向や仕様も一年でかなり動きがあるそうで*1
追従し理解するべくサイトを構築し,バックボーンを把握しておくのは悪くないはず.

去年の3月にどうやら初めて自己紹介用ページを作成したようです.
あれから一年半以上経過し,✝就活✝のため今一度情報を整理する必要が発生しています*2
日々の精進も大事ですが,自分何やってきたっけ?という
振り返りを節目で行っておくのも大事です.

旧ページには,

  • めっちゃスクロールしないといけない
  • レスポンシブデザイン()
  • 古くなったフレームワーク

などなど盛りだくさんの問題点を抱えている状況でした.
ユーザーエクスペリエンスなどあったものではありません.完全に自己満です.
そこで就活にはちょっと遅い気もしますが,多少見やすくなるよう劇的ビフォー・アフターを開始した訳です.

開発環境

  • Mac OS X 10.14.4 (x86_64 Darwin 18.5.0)
  • Node.js v12.13.1 (nodebrew 1.0.1)
  • Node Package Manager v6.13.4

編集自体はローカルで行っています.エディタについてはお好みで*3
主に扱うのが HTML/CSS/JavaScript なので依存環境は少なめです.
挙動確認は FireFox 71.0Chrome 79.0.3945でDevToolsを使いながらやっています.

Vue.jsの導入にあたっては,Vue CLIを利用します.

cli.vuejs.org

このCLIを使うことで,

  • Vue.jsを使ったプロジェクト雛形の作成
  • 開発用サーバの起動
  • JSのトランスパイル*4
  • プロジェクトのビルド
  • 静的コード解析
  • 一部のパフォーマンス計測

等が楽に実行できるようになります.
インストールして,プロジェクト作成までの流れは以下の通り.
対話形式でプリセットやCSSプリプロセッサの選択,様々なオプションの設定が実行されます.

$ npm install -g vue-cli 
$ cd <project_dir>
$ vue create <project_name>

Vue CLI v4.1.1
? Please pick a preset: (Use arrow keys)
❯ default (babel, eslint)
  Manually select features

諸々のセットアップが終わりnpm run serveを実行して,
http://localhost:8080/へアクセスしサンプルページが表示されれば開発のための初歩は終了です.

f:id:takuzoo3868:20191222193946p:plainf:id:takuzoo3868:20191222194122p:plainf:id:takuzoo3868:20191222194140p:plain
サンプルページ,WebUI

実際に手元で動作検証する時,自分はvue uiでVue CLI WebUIを起動します.
理由はサービス起動などタスク実行時に速度統計などCUIでは確認できない情報が見えるからです.
不要パッケージもサクッと削除できるので今回は重宝しました.

フレームワークの更新

春にVue.js Vuetifyを導入してほくほくしていましたが,
この半年の間にかなりアップデートされていました.
しかも春の時点では自分のレスポンシブ対応もいまいち!

Vue.jsはJSフレームワークの一種であり,データバインディングが非常に優秀で,
Webアプリケーションのユーザーインターフェースを割と効率的に構築できます.

VuetifyVue.jsによるWebアプリケーションへ,マテリアルデザインコンポーネントを追加します.
CSSを直接弄らなくても見やすい構成になるので重宝します.

github.com

今まで Vuetify 1.5.13 を使っていましたが,最新の安定版は 2.1.15 です.
メジャーアップデート後だいぶ安定してきたそうなので,これは更新せねばなりません.

しかし,既存のコンポーネントが動作しなくなる,所謂破壊的アップデートであるため
npm update では上手く更新されない可能性があります.そこで npm-check-updatesを利用します*5

github.com

これをインストールすることでncuコマンドが利用できるようになります.

  • アップデートの確認は ncu
  • メジャーアップデートも含める場合は ncu -u
  • メジャーアップデートは固定してマイナーだけ...という場合は ncu --semverLevel major
$ ncu
Checking /<project_dir>/package.json
[====================] 29/29 100%

 sass    ^1.23.7  →  ^1.24.0 

Run ncu -u to upgrade package.json

そして待っていたのが既存サイトの修正地獄だった訳です.だいぶ変更しました.

github.com

1.x → 2.1のような修正方法などはドキュメントに載ってなかったので,先人の知恵をお借りしました🙏

dev.to

qiita.com

個人的にはグリッドシステムやダークテーマの適用方法,一部コンポーネントの挙動変更に影響を受けました.
それでも記述方法が洗練されたので,慣れればすぐ扱えると思います.
修正方法はなくとも困った時はやっぱり公式が一番助かります.

vuetifyjs.com

サイト構成の変更

プロジェクトの主要ファイル構成は以下のようになります.

.
├── babel.config.js
├── dist/                    // ビルドしたデータの格納先
├── package-lock.json
├── package.json             // パッケージ管理
├── public/                  // VueCLIが自動生成するので基本弄らない
│   ├── favicon.ico
│   └── index.html
├── src/
│   ├── App.vue              // router-view toolbar footbar
│   ├── assets/              // 画像データなど
│   ├── components/          // 各コンポーネントを記載したvueファイル
│   ├── main.js              // エントリーポイント App.vue起動
│   ├── plugins/
│   │   └── vuetify.js       // Vuetify用
│   └── router/
│       └── router.js        // vue-router用
└── vue.config.js

今までは単一ページのみでApp.vueに全てのコンポーネントを表示していました.
下のコードにようにv-contentの配下にあるものです.

<template>
    <v-app dark>
        <!--    Input my contests    -->
        <v-content>
            <ToolBar/>
            <TopPage/>
            <About id="about"/>
            <Works id="works"/>
            <Footer/>
        </v-content>
    </v-app>
</template>

<script>
    import About from "./components/About";
    import Works from "./components/Works"
    import TopPage from "./components/TopPage";
    import Footer from "./components/Footer";
    import ToolBar from "./components/ToolBar";
    export default {
        name: "App",
        components: {
            ToolBar,
            TopPage,
            About,
            Works,
            Footer,
        },
    };
</script>

しかし,これでは余計にスクロールが必要なため,今回の修正では

  • トップページ
  • 自分について
  • スキル
  • 作品

の4つへ分割する事にし,ちょっと遷移しやすいようボタンを配置します.
そこで活躍するのがvue-routerです.URLベースで表示するコンポーネントの制御が可能になります.
srcrouter/router.jsを以下のように配置します.基本はこのrouter.jsへ設定を追加していきます.

import Vue from "vue";
import Router from "vue-router";
// 表示するコンポーネントをimport

Vue.use(Router);

export default new Router({
  mode: "history",
  routes: [
    //ルーティングの設定
  ]
});

ルーティングの設定にはパスの設定表示するコンポーネントルート名を設定します.
動的ルーティングやネスト化にも対応しており便利です.
そして,これらの設定を有効化するためにmain.jsrouter.jsを読み込むよう記述します.

import Vue from "vue"
import vuetify from "./plugins/vuetify"
import router from "./router/router"
import App from "./App.vue";

Vue.config.productionTip = false;

new Vue({
  vuetify,
  router,
  render: h => h(App),
}).$mount("#app");

main.jsでは他にvuetify.jsの読み込みも行っています. これらは別にmain.jsへ統合しても
動作は問題ないですが,パッケージメンテを考えると別々にしてimportするのが良いかなと思います.
以上より,App.vueの記述は以下のように更新されました.

<template>
    <v-app>
        <v-app-bar />

        <router-view />

        <v-footer />
    </v-app>
</template>

<script>
export default {
  data: () => ({
    links: [
      /* icon & link data */
    ]
  })
}
</script>

省略してますがfooterは著作表示にnew Date().getFullYear()を入れ,
最新の西暦を取得するよう変更しました.この箇所も今まで手動だったのは香ばしいですね().

現状ではtoolbarとfootbarは固定しつつ,router-viewによって表示の切替が可能になりました.
これによって多少情報が見やすくなったんじゃないかなーと思ってますが,単体のページに全部載ってたときと比べ,どちらが良いかはGoogle analyticsのデータを比較しないと...😣初代のサイトにはanalyticsを埋め込んで,どの辺を訪問者は見てるかチェックしていましたが,Vue.jsへ変更してから未実装なので早めにアップデートしなければいけません.

f:id:takuzoo3868:20191223130525g:plain
routerを用いた画面遷移

ページ遷移についてはrouter.jsに沿って,コンポーネントto="<routing_path>"をタグのプロパティへ追加するだけです.上のgifのように自分のサイトではv-btntoを追加しています.各ページでVuetifyの何を使ったか等は次の節に書いていきます.

home.vue

サイトへアクセスすると一番最初に目にするページになります.
前のバージョンから導入していた自己紹介文を表示するターミナルがどーーんと中央に配置されています.
ページそのものに関するhome.vueとターミナル箇所に関するterminal.vueへ分割しています.

<template>
    <v-app dark>
        <!--    Input my contests    -->
        <v-content>
            <Terminal />

            <div class="text-center pb-6 px-5">
                <v-btn
                    x-large
                    color="grey darken-3"
                    rounded
                    to="/about"
                    class="mx-4 my-2">
                    <v-icon class="mr-2">
                        account_circle
                    </v-icon> about me
                </v-btn>
                <v-btn
                    x-large
                    color="grey darken-3"
                    rounded
                    to="/skills"
                    class="mx-4 my-2">
                    <v-icon class="mr-2">
                        dns
                    </v-icon> skills
                </v-btn>
                <v-btn
                    x-large
                    color="grey darken-3"
                    rounded
                    to="/works"
                    class="mx-4 my-2">
                    <v-icon class="mr-2">
                        work
                    </v-icon> works
                </v-btn>
            </div>
        </v-content>
    </v-app>
</template>

<script>
import Terminal from "./terminal";

export default {
  components: {
    Terminal,
  },
};
</script>

Vuetifyマテリアルデザインのガイドラインで定められた色・大きさ・配置やスペースに関する各クラスが豊富に用意されており,ユーザーはタグへ直接適用するだけで,CSSを(基本は)弄らなくてもマテリアルデザインに沿ったサイトを構築できるよう設計されています.
上のhome.vueにはterminal.vueの読み込みと下部に配置するボタンを列挙しています.プロパティやクラスにあるpb-6mx-5は余白設定のためのpaddingmarginの事です.二文字目のbxはどの方向に余白を設定するかを示し,数字はどのくらい余白を取るかを示しています.つまり,{p or m}{余白の方向}-{大きさ}となります.縦横だけでなく,右だけや下だけなど,細かく設定できる辺りが本当にすごい.詳しくはドキュメントをどうぞ,

<template>
    <v-container
        py-6
        px-5>
        <v-row
            justify="center"
            align-content="center">
            <v-col
                cols="12"
                xs="7"
                sm="10"
                md="9"
                lg="10"
                offset-xl="1">
                <h1 class="pagetitle display-3 font-weight-bold mt-8 mb-10">
                    Welcome to my portfolio!!!
                </h1>

                <v-card class="terminal-card">
                    <div class="terminal-window">
                        <header>
                            <div class="button green" />
                            <div class="button yellow" />
                            <div class="button red" />
                        </header>
                        <section class="terminal">
                            <div class="history" />
                            <div id="typed-strings">
                                <p><span class="gray"> $ </span>whoami ? ^500</p>
                                <p>
                                    <!-- code -->
                                    <span class="duo-2">const </span> <span class="duo-1">me </span> = <span class="duo-3"> new</span> <span class="uno-1">Human()</span>;<br>
                                    <br>
                                    <span class="uno-3">me.name</span> = '<span class="duo-1">Takuya Sawada</span>';<br>
                                    <span class="uno-3">me.college</span> = '<span class="duo-1">UEC</span>';<br>
                                    <span class="uno-3">me.major</span> = '<span class="duo-1">Information Science and Technology</span>';<br>
                                    <br>
                                    <span class="duo-3">while </span> (<span class="uno-3">me.</span><span class="uno-1">isLive()</span>) {  <span class="uno-5">// Is life worth living? It all depends on the liver. - William James </span> &#127867;<br>
                                    <pre>  <span class="uno-3">me.</span><span class="uno-1">coding()</span>;</pre>
                                    <pre>  <span class="uno-3">me.</span><span class="uno-1">eat()</span>;</pre>
                                    <pre>  <span class="uno-3">me.</span><span class="uno-1">sleep()</span>;</pre>
                                    <br>
                                    <pre>  <span class="duo-3">if</span> (<span class="uno-3">day</span> <span class="duo-3"> ===</span> "<span class="uno-3">off</span>"){</pre>
                                    <pre>    <span class="uno-3">me.</span><span class="uno-1">spa()</span>;</pre>
                                    <pre>    <span class="uno-3">me.</span><span class="uno-1">gardening(<span class="uno-3">Hibiscus</span>, <span class="uno-3">Banyan</span>)</span>;</pre>
                                    <pre>    <span class="uno-3">me.</span><span class="uno-1">red_ink_stamps(</span><span class="uno-3">temple</span>, <span class="uno-3">shrine</span><span class="uno-1">)</span>;</pre>
                                    <pre>  }</pre>
                                    <pre>}</pre> ^1500
                                    <!-- code -->
                                </p>
                            </div>
                            <span id="whoami" />
                            <span class="typed-cursor" />
                        </section>
                    </div>
                </v-card>
            </v-col>
        </v-row>
    </v-container>
</template>

<script>
import Typed from "typed.js";

export default {
  mounted() {
    new Typed("#whoami", {
      stringsElement: "#typed-strings",
      startDelay: 2000,
      backSpeed: 15,
      typeSpeed: 1,
      loop: false,
      showCursor: false,
      contentType: "html",
    });

  },
}
</script>

<style>
    #whoami {
        white-space: pre-wrap;
    }

    @keyframes my-fade-in {
        from {
            opacity: 0;
        }
        to {
            opacity: 1;
        }
    }

    .pagetitle {
        text-align: center;
    }

    .terminal-card {
        border-radius: 0.5rem 0.5rem 0 0;
    }

    .terminal-window {
        text-align: left;
        width: auto;
        height: auto;
        min-height: 450px;
        border-radius: 8px 8px 0 0;
        margin: auto;
        position: relative;
        animation-name: my-fade-in;
        animation-duration: 4s;
    }

    .terminal-window header {
        background: #191919;
        height: 30px;
        border-radius: 0.5rem 0.5rem 0 0;
        padding-left: 10px;
    }

    .terminal-window header .button {
        width: 12px;
        height: 12px;
        margin: 10px 8px 0 0;
        display: inline-block;
        border-radius: 8px;
    }

    .terminal-window header .button.green {
        background: #32c146;
    }

    .terminal-window header .button.yellow {
        background: #f6b73e;
    }

    .terminal-window header .button.red {
        background: #f55551;
    }

    .terminal-window section.terminal {
        color: white;
        font-size: 11pt;
        background: #232323;
        white-space: pre;
        padding: 10px;
        box-sizing: border-box;
        border-bottom-left-radius: 0.5rem 0.5rem;
        border-bottom-right-radius: 0.5rem 0.5rem;
        position: absolute;
        width: 100%;
        top: 30px;
        bottom: 0;
        overflow: auto;
    }

    .terminal-window section.terminal .typed-cursor {
        opacity: 1;
        -webkit-animation: blink 0.7s infinite;
        -moz-animation: blink 0.7s infinite;
        animation: blink 0.7s infinite;
        display: inline;
    }

    .terminal-window .gray { color: gray; }
    .terminal-window .uno-1 { color: #d6e9ff}
    .terminal-window .uno-3 { color: #88b4e7}
    .terminal-window .uno-4 { color: #586f89}
    .terminal-window .uno-5 { color: #444c55}
    .terminal-window .duo-1 { color: #34febb}
    .terminal-window .duo-2 { color: #32ae85}
    .terminal-window .duo-3 { color: #57a784}
</style>

ターミナルに関して,配置はv-cardを使っていますが,実装はTyped.jsCSS芸を使っています.
先程コードがスッキリしていたのに比べ,terminal.vueは少し見にくい事がわかると思います.まぁ...この様にVue.jsは他のパッケージもimportで読み込んで使えるんだよという事を試したかったし,Vuetifyのシンプルさが際立って良いんじゃないかなと思ってます.CSS芸は思い通りの見た目を作れるようになってくると楽しいですね,沼に落ちそう

そもそもこのページを作ったのも「端末がカタカタして,疑似コードによる自己紹介がシンタックスハイライト付きで表示するホームページとかイケてるよね〜!」という動機なのでこれはこれで良いのだ!!!

ここでは,v-container > v-row > v-colというVuetify独特のグリッドシステムを使っています.これが1系からのバージョンアップで書き換えにちょっと苦労しました*6v-containerはコンテンツ全体の設定でfluidなど幅いっぱいに広げるプロパティなどが用意されています.v-rowv-colのラッパーで基本は24pxのgutterなど横一列に表示するコンポーネントの配置設定が用意されています.v-colにはコンテンツの大きさなどを設定します.col数に従い,rowでの配置などはcontainerにあればよしなにやるよというイメージでしょうか?*7概念そのものはv1.5のgridのドキュメントにあるレイアウトがわかりやすいです.「1列にcol数は12まで」を良く表現しています.

その次に設定している xs sm md lg が,このグリッドシステムをレスポンシブ対応してくれる優れたプロパティになります.画面の大きさに合わせてcol数の調整が可能です.

f:id:takuzoo3868:20191224013302p:plain
引用: https://vuetifyjs.com/en/components/grids

DevToolsで確認すると,上手く画面に収まっている事がわかります.この辺は最適な値などわからぬので自分は試行錯誤でした.デザイン関連の本を少し読もうかなと思いました...

f:id:takuzoo3868:20191224014159p:plainf:id:takuzoo3868:20191224014212p:plain
コンテンツのレスポンシブ対応

about.vue

ここは所謂自分語りをしているページです.就活生としては採用担当に自分のことを知ってもらおうと真面目に書いたつもりなんですが,人によってはポエムに見えちゃうかもしれないです,ごめんなさい.前のサイトからスキル項目を別のコンポーネントへ移動しました.

<template>
    <v-container
        py-6
        px-5>
        <v-row
            justify="center"
            align-content="center">
            <v-col
                xs="7"
                sm="10"
                md="11"
                lg="12"
                offset-xl="1">
                <h1 class="pagetitle display-3 font-weight-bold mt-8 mb-12">
                    About me
                </h1>

                <v-card>
                    <v-card-actions class="justify-center">
                        <v-avatar
                            color="grey"
                            size="230"
                            class="elevation-4 mt-n10 my-icon">
                            <v-img :src="require('../../assets/profile.png')" />
                        </v-avatar>
                    </v-card-actions>

                    <v-card-title primary-title>
                        <div>
                            <span class="display-1 font-weight-bold mb-1">{{ myname }}</span>
                            <div>{{ jobs }}</div>
                        </div>
                    </v-card-title>
                    <v-card-text>
                        <p>{{ hobby }}</p>
                        <p>{{ boyhood }}</p>
                        <p>{{ undergraduatedays }}</p>
                        <p>{{ graduatedays }}</p>
                    </v-card-text>

                    <div class="px-2">
                        <v-card-title primary-title>
                            <div class="headline">
                                Activity
                            </div>
                        </v-card-title>

                        <v-card-text>
                            <v-list
                                dark
                                two-line>
                                <v-list-item-group>
                                    <v-list-item
                                        v-for="(activity, i) in activities"
                                        :key="i"
                                        :href="activity.href">                                    
                                        <v-list-item-avatar>
                                            <v-icon
                                                class="teal"
                                                v-text="activity.icon" />
                                        </v-list-item-avatar>

                                        <v-list-item-content>
                                            <v-list-item-title v-text="activity.text" />
                                            <v-list-item-subtitle v-text="activity.subtext" />
                                        </v-list-item-content>
                                    </v-list-item>
                                </v-list-item-group>
                            </v-list>
                        </v-card-text>
                    </div>

                    <div class="px-2">
                        <v-card-title primary-title>
                            <div>
                                <div class="headline">
                                    Education
                                </div>
                            </div>
                        </v-card-title>

                        <v-timeline
                            class="px-2"
                            :dense="dense">
                            <v-timeline-item
                                v-for="(item, i) in items"
                                :key="i"
                                :color="item.color"
                                :icon="item.icon"
                                fill-dot
                                large>
                                <v-card
                                    :color="item.color"
                                    dark>
                                    <v-card-title class="body-1">
                                        {{ item.title }}
                                    </v-card-title>
                                    <v-card-text class="white text--primary pt-4">
                                        <p class="title gray">
                                            {{ item.text }}
                                        </p>
                                        <div class="gray font-weight-bold">
                                            {{ item.major }}
                                        </div>
                                        <div class="mt-2">
                                            <v-icon class="black--text text--lighten-4">
                                                calendar_today
                                            </v-icon>
                                            <span class="black--text font-weight-bold"> {{ item.date }}</span>
                                        </div>
                                        <div class="text-center">
                                            <v-btn
                                                :color="item.color"
                                                class="mx-4"
                                                :href="item.href"
                                                target="_blank"
                                                :style="item.bt_display">
                                                <v-icon class="mr-1">
                                                    {{ item.bt_icon }}
                                                </v-icon> {{ item.bt_title }}
                                            </v-btn>
                                        </div>
                                    </v-card-text>
                                </v-card>
                            </v-timeline-item>
                        </v-timeline>
                    </div>
                </v-card>
            </v-col>
        </v-row>

        <div class="text-center pb-6 px-5">
            <v-btn
                x-large
                color="grey darken-3"
                rounded
                to="/"
                class="mx-4 my-2">
                <v-icon class="mr-2">
                    home
                </v-icon> back home
            </v-btn>
        </div>
    </v-container>
</template>

<script>
export default {
  data() {
    return {
      myname: "Takuya Sawada",
      jobs: "Research area: Estimating TCP Congestion Control Algorithms / Traffic Analytics",
      hobby: "略",
      boyhood:"略",
      undergraduatedays:"略",
      graduatedays:"略",
      items: [
        {
          color: "teal darken-2",
          icon: "fa-school",
          title: "略",
          text: "略",
          major: "",
          date: "略",
          startdate: "略",
          bt_display: "display:none"
        },
        // 以下略
      ],
      activities: [
        {
          text: "LOCAL 学生部",
          subtext: "北海道を中心に技術に関心がある学生の集まり",
          href: "http://students.local.or.jp/",
          icon: "group",
        },
        // 以下略
      ],
      interval: {},
      expansionPanel: true,
    }
  },
  beforeDestroy() {
    clearInterval(this.interval)
  },
  computed: {
    dense() {
      return this.$vuetify.breakpoint.smAndDown;
    }
  },
};
</script>

<style>
    .gray {
        color: #424242;
    }

    .my-icon {
        transition: all 1s linear 0s;
        border-radius: 50%;
        cursor: pointer;
    }

    .my-icon:hover {
        transform: rotateY(360deg);
    }
</style>

記述するものがたくさんある場合,JavaScript側でデータオブジェクトとして保持しておくと,HTML側ではコンポーネントの配置などの記述のみになり,コードがより管理しやすくなります.メリットはデータを登録するだけで,v-for="(item, i) in items"のようにデータに設定したキーを指定しコンポーネント側でデータを表示でき,似た処理を記述する手間が大幅に減ります.何か追記したい場合は,データオブジェクトにサッと追加するだけです.更新する際に,サイトそのもののタグ構成を弄らなくて良いのでメンテナンスも非常に楽になります.

活動歴にはv-listを使い,今まで<ul><ol><li>で表示して少し見にくかった部分を改善しました.サブタイトルや関連するサイトへのリンクを追加しています.

学歴の箇所は今までと同じくv-timelineで時系列表示しています.変更点は,今まで変な箇所にあった学部のまとめや進学の話のv-btnを関連するカードへ配置するようにしたことです.これもデータオブジェクトに追加し,表示・非表示だけCSSdisplayに頼りました.

f:id:takuzoo3868:20191224021602p:plain
カード内へボタンを配置するよう変更

この時系列表示で遊んだのは,画面の大きさによってカードを片方向へ寄せるかどうかのdenseプロパティをよしなにやるようscript側へ追加したことです.このscriptを実装しないまま表示すると,時系列カードが左右に表示され「横文字が縦書きで!」といった見にくい状態になってしまうのです.該当箇所は以下です.

<v-timeline
    class="px-2"
    :dense="dense">

<script>
computed: {
    dense() {
      return this.$vuetify.breakpoint.smAndDown;
    }
},
</script>

Vuetifyには画面の大きさが特定の規格に切り替わる瞬間を取得できるスクリプトが用意されており,先程のcol数制御も裏では$vuetify.breakpointが判定処理しています.

f:id:takuzoo3868:20191224023858g:plain
画面の大きさを所得してプロパティ変更

skills.vue

私の肉体を構成する技術はこの分野だよというものを示したグラフが列挙されているページです.
ここはグリッドシステムを更新した以外に大きな変更点はありませんね.他にはオレンジの見た目がきついのでは?と思い青緑系統へ変更しました.

<template>
    <v-container
        py-6
        px-5>
        <v-row
            justify="center"
            align-content="center">
            <v-col
                cols="12"
                xs="7"
                sm="10"
                md="10"
                lg="10"
                offset-xl="1">
                <h1 class="pagetitle display-3 font-weight-bold mt-8 mb-8">
                    Skills
                </h1>
            </v-col>

            <v-col
                xs="12"
                sm="6"
                md="3"
                lg="3"
                v-for="skill in skills"
                :key="skill.title">
                <template>
                    <v-hover>
                        <v-card
                            slot-scope="{ hover }"
                            class="text-xs-center ma-4"
                            :class="`elevation-${hover ? 12 : 4}`"
                            color="teal darken-2">
                            <v-card-title>
                                <div class="mx-auto">
                                    <v-progress-circular
                                        :value="skill.value"
                                        :color="skill.color"
                                        :rotate="-90"
                                        width="15"
                                        size="100">
                                        {{ skill.value }}
                                    </v-progress-circular>
                                </div>
                            </v-card-title>

                            <v-card-actions>
                                <v-layout justify-space-around>
                                    <div class="headline">
                                        <v-tooltip bottom>
                                            <template v-slot:activator="{ on }">
                                                <span
                                                    :color="skill.color"
                                                    :class="`${skill.color}--text`"
                                                    class="subtitle-1"
                                                    v-on="on">
                                                    <v-icon size="18">
                                                        {{ skill.icon }}
                                                    </v-icon>
                                                    {{ skill.title }}
                                                </span>
                                            </template>
                                            <span>{{ skill.duration }}</span>
                                        </v-tooltip>
                                    </div>
                                </v-layout>
                            </v-card-actions>
                        </v-card>
                    </v-hover>
                </template>
            </v-col>
        </v-row>

        <div class="text-center pb-6 px-5">
            <v-btn
                x-large
                color="grey darken-3"
                rounded
                to="/"
                class="mx-4 my-2">
                <v-icon class="mr-2">
                    home
                </v-icon> back home
            </v-btn>
        </div>
    </v-container>
</template>

<script>
export default {
  data() {
    return {
      skills: [
        {
          title: "Python",
          value: 0,
          absoluteValue: 80,
          color: "white",
          icon: "fab fa-python",
          show: false,
          duration: "5 years",
        },
       // 略
        {
          title: "Go To BED",
          value: 0,
          absoluteValue: 100,
          color: "white",
          icon: "hotel",
          show: false,
          duration: "25 years / very good",
        },
      ],
      interval: {},
      expand: false,
    }
  },
  beforeDestroy() {
    clearInterval(this.interval)
  },
  mounted() {
    let timesRun = 0;
    this.interval = setInterval(() => {
      if (timesRun === 10) {
        clearInterval(this.interval);
        return;
      }
      timesRun += 1;
      this.skills.forEach((skill) => {
        if (skill.absoluteValue !== skill.value) {
          skill.value += 10
        }
      });
    }, 300)
  }
};
</script>

ここでちょっと面白いことをしたのは,本来画面ローディングのくるくるに使われるv-progress-circularをスキル表示の円グラフに代用したことですかね.最初はChart.jsでも入れるかなと思ったのですが,開始を0に固定して10刻みなら使えそうと気づいてからv-cardで列挙する方法へ切り替えました.寝る子は育つという事でGo to bedは100%に設定しています*8

works.vue

f:id:takuzoo3868:20191224030156p:plain

成果物とかしっかり示さないとなぁと思い,今少しずつ更新してるのがこのページです.
まだ作成段階なのでデータオブジェクトを使っていないし,試行錯誤状態なのでぐちゃぐちゃで恥ずかしいです.
次の次くらいの更新で,よりスマートな記述にします,

<template>
    <v-container
        py-6
        px-5>
        <v-row
            justify="center"
            align-content="center">
            <v-col
                cols="12"
                xs="7"
                sm="10"
                md="10"
                lg="10"
                offset-xl="1">
                <h1 class="pagetitle display-3 font-weight-bold mt-8 mb-12">
                    Works
                </h1>
            </v-col>
        </v-row>

        <v-row justify="center">
            <v-col
                cols="11"
                sm="6"
                align="center">
                <v-hover v-slot:default="{ hover }">
                    <v-card max-width="530">
                        <v-img
                            :src="require('../../../src/assets/works_portfolio.png')"
                            max-height="255">
                            <v-expand-transition>
                                <v-card
                                    v-if="hover"
                                    height="100%"
                                    width="100%"
                                    class="d-flex transition-fast-in-fast-out v-card--reveal title white--text"
                                    href="https://takuzoo3868.github.io/"
                                    target="_blank"
                                    rel="noopener">
                                    Learn More
                                    <v-icon
                                        small
                                        class="mx-2">
                                        open_in_new
                                    </v-icon>
                                </v-card>
                            </v-expand-transition>
                        </v-img>

                        <v-card-title class="text-left">
                            <h3 class="headline">
                                My Portfolio Site
                            </h3>
                        </v-card-title>
                        <v-card-text
                            class="text-left">
                            <p class="mb-1">
                                現在閲覧されている,ポートフォリオサイトです.
                            </p>
                            <p class="mb-1">
                                Vuetifyで構成し,GitHub Pagesで運用しています.
                            </p>
                        </v-card-text>
                    </v-card>
                </v-hover>
            </v-col>

            <v-col
                cols="11"
                sm="6"
                align="center">
                <v-hover v-slot:default="{ hover }">
                    <v-card max-width="530">
                        <v-img
                            :src="require('../../../src/assets/works_penta.png')"
                            max-height="255">
                            <v-expand-transition>
                                <v-card
                                    v-if="hover"
                                    height="100%"
                                    width="100%"
                                    class="d-flex transition-fast-in-fast-out v-card--reveal title white--text"
                                    href="https://github.com/takuzoo3868/penta"
                                    target="_blank"
                                    rel="noopener">
                                    Learn More
                                    <v-icon
                                        small
                                        class="mx-2">
                                        open_in_new
                                    </v-icon>
                                </v-card>
                            </v-expand-transition>
                        </v-img>

                        <v-card-title class="text-left">
                            <h3 class="headline">
                                Penta
                            </h3>
                        </v-card-title>
                        <v-card-text
                            class="text-left">
                            <p class="mb-1">
                                PENTest + Automation tool
                            </p>
                            <p class="mb-1">
                                対象環境の脆弱性チェック・可視化を目指しています.
                            </p>
                        </v-card-text>
                    </v-card>
                </v-hover>
            </v-col>
        </v-row>

        <div class="text-center pb-6 px-5">
            <v-btn
                x-large
                color="grey darken-3"
                rounded
                to="/"
                class="mx-4 my-2">
                <v-icon class="mr-2">
                    home
                </v-icon> back home
            </v-btn>
        </div>
    </v-container>
</template>

<style lang="scss" scoped>
.v-card--reveal {
  align-items: center;
  bottom: 0;
  justify-content: center;
  // opacity: 0.5;
  background-color: rgba(32, 32, 32, 0.4);
  position: absolute;
  width: 100%;
}
</style>

なんとかしてこのページをいろんな成果でより華やかにしたいところ

デプロイの半自動化

f:id:takuzoo3868:20191223045404g:plain

今まではオリジナルをdevへコミットし,ビルドされたソースコードmasterへプッシュすることで
GitHub pagesへホスティングし手動運用していました.しかしこれは何かイケてませんね? まるで上の改札機のgifみたいです.

f:id:takuzoo3868:20191223051718p:plain
目標とする構成

CIサービスを使って半自動化を上の構成の様に考えていきます.
devの変更点をプッシュすることで,CIによる環境でビルドが走りmasterへ反映できるようにします.
こちらのドキュメントを参考に作成した,暫定的なtravis.ymlは以下の通りです.
現在Github pages用のdeploy設定がv2になるよ〜と勧告中で,自分もedge: trueをセットし試してみました.

language: node_js
node_js:
  - lts/*

cache:
  directories:
    - node_modules

branches:
  only:
    - dev

install:
  - npm install
  - npm run build

script:
  - echo "Skipping tests"

deploy:
  provider: pages:git
  token:
    secure: <secure_token>
  edge: true
  keep_history: false
  target_branch: master
  local_dir: dist
  name: Travis-CI
  email: travis-ci@example.com
  commit_message: Publishing site on `date "+%Y-%m-%d %H:%M:%S"`
  on:
    branch: dev

さて,Travis CIがrepo操作できるよう適切なtokenをGitHubの設定から発行する必要があります.この値の記述方法は幾つかありますが,自分はtravisを使ったtokenの暗号化にしました.Access tokenだと漏れた時に,別の公開レポジトリをいじられる可能性もあり気持ち悪い事この上ないですが,とりあえずはドキュメントに従います.travis.ymlを配置したプロジェクトディレクトリにおいて,発行済tokenを用意した上でtravis cliを導入しましょう.rubyが必要なので注意です,

# インストール
$ gem install travis  # macならbrew install travisも可

# アカウントへログイン
$ travis login
...
This information will not be sent to Travis CI, only to api.github.com.
The password will not be displayed.

Username: <username>
Password for username: <passward>

Two-factor authentication code for <username>: <2FA code>
Successfully logged in as username!

# トークン暗号化
$ travis encrypt <token_value> --add deploy.token

以上より,RSA公開鍵を用いたsecure tokenがtravis.ymlへ自動追記されます*9.よくよく考えたら,RSAを署名に使わない貴重な実例だと思うのですがどうなんでしょう?

$ curl -s https://api.travis-ci.org/repos/<username>/<project_name>/key | jq -r '.key'
Travis CIの公開鍵をチェックできます.今のご時世,鍵長1024bitだったら,この計画はssh鍵を使うか,GitHub Actionsでデプロイする方向にしよう〜と思っていました.自分の環境で試したら鍵長4096bitはあったので,とりあえずBrent氏を信じて15年くらいは安心して使えるかなと思います*10.そろそろヤバいなと思ったら,鍵を再生成することもTravis APIで可能なので,必要に応じてこちらもチェックしましょう.

設定方法はまだ吟味する必要はありますが,これでdevへ変更をpushするとビルドが走り,masterへdistの中身がpushされるようになりました.

f:id:takuzoo3868:20191224035258p:plainf:id:takuzoo3868:20191224035319p:plain
Deployされている事を確認

404対応

変な所へアクセスされた時に,「そんなのないよ」と表示する404ページも遊びで作りました
このページだけはVue.jsではなく純粋なCSSアニメーションです.

:thinking_face:はLeoさんのCSSを参考にしました🙏

kuzlog.com

今もコンポーネント配下に置いてあります...が!ルーティングをすっかり忘れてました.
なので,これが書き終わったら年末に修正です.

おわりに

ポートフォリオ更新にあたって,遊びを入れつつ試行錯誤した点は以上になります.
Vue.jsVuetifyはわかりやすくていいなぁこれがフレームワークの恩恵かぁと思いました.
最近で言えば,pentaのダッシュボードをVuetifyで作り始めています.
ボタン一つで対象端末のスキャン,レポートの可視化ができたら便利ですよね.頑張るぞ〜!

ここまで読んだ方は色々気づいたかと思いますが,課題は山積みで,

  • iconのcssを各アイコン読み込みへ変更
  • 404のルーティング
  • Google analyticsの組込み
  • OGP設定のためにVue.jsをNuxt.jsへ刷新

など色々残っています.なので僕のポートフォリオサイト構築はまだ終わりません.
これからもいろんなフレームワークで遊びつつ,真面目なアウトプットを出していこうと思います.

*1:先日GDG TokyoでJackさんのYearly Web 2019を聞いた時に痛感しました

*2:人事面談の際に,ここに書いてあるこれは...と話を広げる事ができるので,自分の場合ポートフォリオは結構重宝しました

*3:私はVimvscodeです,戦争する気はありません穏健派です

*4:Node.jsスタイルで書かれたプログラムをブラウザ上で動作させるべく変換する処理

*5:本番環境ではやめましょう,お兄さんとの約束だぞ!

*6:以前はv-layoutやv-flexになります

*7:間違ってたらごめんなさい

*8:hoverを表示すると年齢がバレるのも遊び心です

*9:秘密鍵Travis CI側が管理しており,このsecure token自体はユーザーにも復号できないから安全というのが,多分Travis側の主張かなと思います

*10:https://sehermitage.web.fc2.com/crypto/safety.html