20191012のvue.jsに関する記事は18件です。

Vue Head利用方法メモ

書いてあること

  • Vue Headの利用方法を試した際のメモ

参考

vue-head
vue.jsでheadの要素を設定する時はvue-headが便利!

環境

  • MacOS 10.14.6
  • Node.js v10.16.0
  • Npm 6.12.0
  • Vue 3.8.4
  • Vue Head 2.1.2

レポジトリ

vue-router-project

アプリケーション共通の情報

レイアウトファイルに共通情報を設定する

src/layouts/default.vue
<script>
export default {
  head: {
    meta() {
      return [{ name: 'application-name', content: 'vue-router-project' }]
    },
  },
}
</script>

ページ毎の情報

各ビューファイルにページの個別情報を設定する

src/views/page.vue
<script>
export default {
  head: {
    title() {
      return {
        inner: 'VueRouter Project',
        separator: '|',
        complement: 'Home',
      }
    },
    meta() {
      return [
        { name: 'title', content: 'Home' },
        { name: 'description', content: 'home description' }
      ]
    },
  },
}
</script>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.js環境変数による開発作業メモ

書いてあること

  • Vue.jsで環境変数を利用する方法メモ
  • とりあえず1人情シスでの開発なので.env.env.localで十分かなと。

参考

Modes and Environment Variables
Vue CLI 3.xからの環境変数やモードの扱い

環境

  • MacOS 10.14.6
  • Node.js v10.16.0
  • Npm 6.12.0
  • Vue 3.8.4

レポジトリ

vue-router-project

Git管理対象外に設定

.gitignore
.DS_Store
node_modules
/dist
/coverage

# env files
+.env
.env.local
.env.*.local

# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

環境変数を定義

.env.local

.env
VUE_APP_PROJECT_NAME='VUE-PROJECT'
VUE_APP_URL='https://example.com'
.env.local
VUE_APP_URL='https://development.example.com'

環境変数を利用

src/views/page.vue
<template>
  <v-container class="fill-height" fluid>
    <v-row align="center" justify="center">
      <v-flex sm12>
        <v-card width="60%" max-width="800" elevation="18" class="mx-auto">
          <v-card-title class="title">Sample Title</v-card-title>
          <v-divider />
          <v-card-text>
            <v-list>
              <v-list-item>
                {{ projectName }}
              </v-list-item>
              <v-list-item>
                {{ url }}
              </v-list-item>
            </v-list>
          </v-card-text>
        </v-card>
      </v-flex>
    </v-row>
  </v-container>
</template>

<script>
export default {
  data() {
    return {
      projectName: null,
      url: null,
    }
  },
  mounted() {
    this.projectName = process.env.VUE_APP_PROJECT_NAME
    this.url = process.env.VUE_APP_URL
  },
}
</script>

動作結果

.env.localがある場合

image.png

.env.localがない場合

image.png

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

Nuxt.js+VuetifyでSASS変数の上書きが反映されない場合の確認事項

タイトル通りですが、対象読者はNuxt.jsにVuetify(@nuxtjs/vuetify)をインストール後、VuetifyのSASS変数を変更するためにvariables.scssを作成したけど反映されずに困ってる方です。

結果的には@nuxtjs/vuetify のページをじっくり読めば問題なかったのですが、要点だけ書いておきます。

1. customVariablesの設定

nuxtjs/vuetifyのcustomVariables部分を読むと、customVariablesオプションについて以下のように書かれてます。

Provide a way to customize Vuetify SASS variables.

一例として以下のようにサンプルが載ってます。

nuxt.config.js
  vuetify: {
    customVariables: ['~/assets/variables.scss']
  }

※ customVariablesはNuxt.jsをインストールする際、テンプレートにVuetifyを選択すると最初から記述されてます。

2. treeShake: trueの設定

Nuxt.jsを開発モードで起動した場合、以下の内容通りcustomVariablesを設定するだけではvariables.scssの内容は反映されません。

Provide a way to customize Vuetify SASS variables.
Only works with tree-shaking.

実はこのtree-shaking はリンク先の記載通りデフォルトではproductionモードでしか機能しないため、yarn run devのように開発モードで実行すると機能しません。そのため上記で設定したvariables.scssが反映されずになんで????となったのがこの記事投稿のきっかけでした。

最終的にはnuxt.config.jsにtreeShake: trueの一文を加えるだけでOKでした。

nuxt.config.js
  vuetify: {
    customVariables: ['~/assets/variables.scss'],
    treeShake: true,
  }
variables.scss
// Ref: https://github.com/nuxt-community/vuetify-module#customvariables
//
// The variables you want to modify
$font-size-root: 20px;

※ Vuetifyに設定されてるSASS変数一覧はこちら

これでassets/variables.scssを変更して保存すれば自動的にコンパイルされて変更内容が画面に反映されるようになりました。ただしコンパイル時間は若干かかります。

最後に

恐らくこの問題はNuxt.jsのインストール時のテンプレートにVuetifyを選択した人が一番ハマりやすいと思います。理由はnuxt.config.jsにcustomVariablesのオプションは最初から記載してあり、ご丁寧にassets/variables.scssも用意してるのにtreeShakeについては全く記述がないからです。

参考

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

Nuxt.js+VuetifyのSASS変数を上書きする方法

タイトル通りですが、対象読者はNuxt.jsにVuetify(@nuxtjs/vuetify)をインストール後、VuetifyのSASS変数を変更するためにvariables.scssを作成したけど反映されずに困ってる方です。

結果的には@nuxtjs/vuetify のページをじっくり読めば問題なかったのですが、要点だけ書いておきます。

1. customVariablesの設定

nuxtjs/vuetifyのcustomVariables部分を読むと、customVariablesオプションについて以下のように書かれてます。

Provide a way to customize Vuetify SASS variables.

一例として以下のようにサンプルが載ってます。

nuxt.config.js
  vuetify: {
    customVariables: ['~/assets/variables.scss']
  }

※ customVariablesはNuxt.jsをインストールする際、テンプレートにVuetifyを選択すると最初から記述されてます。

2. treeShake: trueの設定

Nuxt.jsを開発モードで起動した場合、以下の内容通りcustomVariablesを設定するだけではvariables.scssの内容は反映されません。

Provide a way to customize Vuetify SASS variables.
Only works with tree-shaking.

実はこのtree-shaking はリンク先の記載通りデフォルトではproductionモードでしか機能しないため、yarn run devのように開発モードで実行すると機能しません。そのため上記で設定したvariables.scssが反映されずになんで????となったのがこの記事投稿のきっかけでした。

最終的にはnuxt.config.jsにtreeShake: trueの一文を加えるだけでOKでした。

nuxt.config.js
  vuetify: {
    customVariables: ['~/assets/variables.scss'],
    treeShake: true,
  }
variables.scss
// Ref: https://github.com/nuxt-community/vuetify-module#customvariables
//
// The variables you want to modify
$font-size-root: 20px;

これでassets/variables.scssを変更して保存すれば自動的にコンパイルされて変更内容が画面に反映されるようになりました。ただしコンパイル時間は若干かかります。

最後に

恐らくこの問題はNuxt.jsのインストール時のテンプレートにVuetifyを選択した人が一番ハマりやすいと思います。理由はnuxt.config.jsにcustomVariablesのオプションは最初から記載してあり、ご丁寧にassets/variables.scssも用意してるのにtreeShakeについては全く記述がないからです。

参考

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

EC2のLaravel6.0環境 VueとLaravelでのTIPS AWS/Laravel連載(10.5)

LaravelのシーディングにFakerを入れる

連載9回目で初期値でhoge, fugaという値を入れました。
実際テストする中だといちいちダミーの名前やタイトル、文章等を考えるのは一苦労です。

そこで便利なのがFaker。
Laravelだと最初からcomposer.jsonに入っているので、install不要です。

そしてシーディングだけでなく、Laravelのfactoryという仕組みを作って汎用性のある書き方をしてみます。

$ php artisan make:factory PostFactory --model=Post
database/factories/PostFactory.php
<?php

/** @var \Illuminate\Database\Eloquent\Factory $factory */

use App\Post;
use Faker\Generator as Faker;

$factory->define(Post::class, function (Faker $faker) {
    return [
        'title'   => $faker->sentence,
        'content' => $faker->paragraph,
    ];
});
database/seeds/PostsTableSeeder.php
...
     public function run()
     {
        $posts = factory(App\Post::class, 3)->create();
     }
$ php artisan db:seed

これで3件分のダミーデータが生成されます。

php artisan tinker
Psy Shell v0.9.9 (PHP 7.2.22  cli) by Justin Hileman
>>> Post::all();
[!] Aliasing 'Post' to 'App\Post' for this Tinker session.
=> Illuminate\Database\Eloquent\Collection {#3084
     all: [
       App\Post {#3085
         id: 1,
         title: "hoge",
         content: "fuga",
         created_at: null,
         updated_at: null,
       },
       App\Post {#3086
         id: 2,
         title: "Repellendus odit qui facilis ea sint.",
         content: "Est eos sed amet quibusdam. Et voluptatem voluptatem et accusantium qui.",
         created_at: "2019-10-12 21:05:10",
         updated_at: "2019-10-12 21:05:10",
       },
       App\Post {#3087
         id: 3,
         title: "Neque rerum ut molestiae aut sequi.",
         content: "Rem exercitationem doloremque facilis et sed qui blanditiis. Dolorem ea quo in voluptatem. Distinctio tempora dolor culpa reprehenderit fuga voluptas omnis. Animi voluptas repudiandae rem quisquam eligendi dolores. Quibusdam iste aut possimus.",
         created_at: "2019-10-12 21:05:10",
         updated_at: "2019-10-12 21:05:10",
       },
       App\Post {#3088
         id: 4,
         title: "Soluta est iure ex sequi a aspernatur.",
         content: "Eos et voluptatem sint tempore. Rerum voluptates quis est fugit voluptas sit voluptatem consequuntur. Voluptas sapiente illo quo optio.",
         created_at: "2019-10-12 21:05:10",
         updated_at: "2019-10-12 21:05:10",
       },
     ],
   }
>>>

GitHub - fzaninotto/Faker: Faker is a PHP library that generates fake data for you

Vue開発の際は

連載10回目でVue.jsを入れました。
デバッグはChromeの開発者ツールだけだといろいろしんどいので、Vue.jsのdevtoolsを入れるのをオススメします。

Vue.js devtools

入れるとこんな感じで、Chromeの開発者ツールに「Vue」タブが追加され、開発が捗ります。

vue.png

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

Vue.js用語辞典【未完】

ディレクティブ

メソッドハンドラ

  • 「クリック」や「入力」といったイベントをハンドリングする
  • クリックしたらアラートを出す!、マウスオーバーしたら文字が大きくなるみたいな動きをつけたい時使おう
  • 簡単な「v-on」ディレクティブを使った例

See the Pen pooJzQr by アズマックス (@yuki-azuma) on CodePen.

双方向データバインディング

  • 双方向にデータが同期するデータバインディング
  • そもそも、データバインディングとは、「データと描画を同期する仕組み」
  • Aが変わったら、Bも変わり、Bが変われば、Aも変わるイメージ

v-model.gif

コンポーネント

  • 名前付きの再利用可能な Vue インスタンス
  • 必要なだけ何度でも再利用できる
  • 部品ごとに使われることが多い

    • 例)
    • 画面のヘッダー部分のコンポーネント
    • フッター部分のコンポーネント、
    • サイドメニューのコンポーネント
  • 下図は、画面のパーツ構成をVueファイルの構成に置き換えた時のイメージ図
    コンポーネントの基本_—_Vue_js.png

  • 開発規模が大きくなってきたら、コンポーネントは大活躍する

  • 以下のワードもよく出てくるので、覚えておきたい

    • $emit
    • props
    • ローカルコンポーネント
    • グローバルコンポーネント
  • 参考:Vue.js「コンポーネント」入門」

  • 参考:Vue.jsのコンポーネント入門

  • 参考:Vue.jsをシンプルに理解しよう その2

その他参考

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

vueで定数定義をいい感じにするtips

概要

定数多くなったら別ファイルにまとめたいし画面でもいい感じに使いたい。
ggってもスパッと解決できなかったので記事にします。

本題

定義側
const Config = Object.freeze({
  HOGE: [ { name: "hoge" }] // ここに全部定義していく、切り分け粒度はおまかせ
})


export default Config
使う側
import Config from "~/consts/index"

data() {
  return {
    Config: Config
  }
}

こんな感じで定義しました。
もっといい方法があればぜひ教えて下さい。

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

Nuxt.js + Expressでローカルサーバ構築

今回のゴール

  • ローカルに擬似的なサーバ(スタブ)を用意して特定のリクエストで(以下イメージではPOST)レスポンスが返ってくるようにします。

ゴール2.gif

ローカルサーバを使いたい局面

  • フロントとバックエンドでチームが別れて開発されているような現場でバックエンド側の実装ができていないときにフロント側からリクエストを飛ばしてデータが返却されるようなスタブが欲しい!!そんなとき。

expressの導入

  • create-nuxt-appで最初から入れることもできます スクリーンショット 2019-10-12 15.12.23.jpg

既存のアプリケーションにexpressを導入したい場合

npm install express --save

サーバ用のファイルをプロジェクトフォルダ直下へ作成

  • まずは必要なフォルダ・ファイルの作成をします。
  • プロジェクトフォルダ直下へapiフォルダを作ってその中にserver.jsを作成、続いてmockフォルダをserver.jsと同じ階層に作成しprofileフォルダを作ります(profileフォルダ内に実際にレスポンスとして返すjsonを格納します)
//プロジェクトフォルダ直下にて
$ mkdir api
$ cd api
$ touch server.js
$ mkdir mocks
$ cd mocks
$ mkdir profile
$ cd profile
$ touch test.json

完成した階層

プロジェクトフォルダ
┗api
  ┗server.js//サーバの設定を書くファイル
  ┗mocks
    ┗profile
      ┗test.json//実際に返して欲しいデータを定義するファイル

  • ちょっとわかりにくいので画像も

スクリーンショット 2019-10-12 15.32.33.jpg

server.jsを編集

  • 以下のように記述
server.js
const express = require('express')
const app = express()

app.post('/test', (req, res) => res.json(require('./mocks/profile/test.json')))
//あとは欲しいだけ上記の1行とjsonファイルを追加するだけ

app.listen(8080, () => console.log('API Mock Server is running'))
  • expressをappとして用意し、8080ポートをlistenしています。
  • app.post(リクエスト,(req, res) => res.(require(返却して欲しいデータを記述したjson)))とすることで最初のゴールのようなレスポンスが返却されます。
  • あとは欲しいだけ上記の1行とjsonファイルを追加するだけ

jsonサンプル

  • 内容はなんでもいいのですがゴールの動画で食わしたjsonは以下です。
test.json
[
  {
    "header": "テスト テスト",
    "test1": "test",
    "test2": "テスと",
    "test3": "1",
    "items": []
  }
]

package.jsonに起動コマンドを追加する

package.json
"scripts": {
  //省略
  "mock:api": "node api/server.js"//追加
}
  • これでnpm run mock:apiでローカルサーバが起動できます。

起動とcurlでPOST

  • 起動
npm run mock:api

  • POST(別のターミナルを開いて)
curl -XPOST http://localhost:8080/test
  • これでゴールのようなレスポンスが返ってきているはずです

所感

  • nuxt.jsのフロントに特化した開発担当になった場合は使えるかな、と。以上です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

はてなブックマークのコメントを Qiita 記事内に埋め込む Chrome 拡張機能 を Vue.js で作った

はじめに

Qiita もはてなもこよなく愛する方のために、以下画像のように Qiita の記事の終わりにはてなブックマークのコメントを読み込んで表示する拡張機能 Hatiina を作った。

※拡張機能を公開したのが 2019/10/12 なので、もしかしたら審査リジェクトで見えなくなる期間が出てくるかも。

image.png

TL:DR;

  • クエリパラメータ(?hoge=fuga&foo=bar)やアンカー(#xxx)付きの URL でもブコメを表示できる
    • メールなどから Qiita にアクセスした場合とか
  • スター獲得数順にソート
  • 実装には Vue.js (とその他いくつかの NPM packages)を利用

作ろうと思った動機

1. 普段の作業を簡略化するため

自分はググって Qiita を見ることが多いが、毎週来る Qiita のニュースメールや Twitter からの通知メールから気になった記事を見ることも結構ある。こういうやつ。

image.png

image.png

で、記事本文だけでなく、なるべく記事中のコメントやはてなブックマークのコメントも見ることで、「記事中で言及されている内容に対して、他の人や玄人はどう考えているのか」や「他に良いツール/やり方/考え方は無いか」などの参考にしている。のだが、↑のリンクから飛ぶとはてなブックマーク拡張機能でコメントが読み込めない

image.png

画像から分かるかもしれないが、原因は URL 内のクエリパラメータやアンカーにある。記事本文の素の URL にページ移動するとコメントが正しく表示できる

image.png

…できるんだけど、「メールから記事開く」→「アドレスバーにフォーカス」→「URL 修正して移動」→「はてなブックマーク拡張機能をクリック」という手順が無駄だし面倒すぎてイライラしていた。

というわけで作った。

2. 技術的に試したいことがあった

Vue.js を使った拡張機能の記事をいくつか見たが、どれもメインページ側(という表現で良いか不安)に作用するものではなく、ポップアップページ(拡張機能アイコンのクリックで表示されるやつ)に関するものだった。(自分が見た範囲では)

「俺が Vue.js を使いたいのはそこじゃないんだよ!」と思って「無いなら試してしまえ」の精神で作った。ちなみに「使ってみたかった」なので、「Vue.js じゃないと作れない」というものではない。

拡張機能の紹介

Hatiina - Chrome ウェブストア

ウェブストアの画像や説明が雑なのはご愛嬌ということで…

できること

ページ遷移なしに「素の URL」に付いているブコメを読み込んで表示。

もうほぼ説明することは無い(前章までに書いてある)ので、簡単にスクショで紹介。

読み込み直後

ブコメは閉じた状態、クリックすると展開される

image.png

ブコメを展開

スター数順にソートされた状態。ブックマーク追加ページへのリンクや、各ユーザーへのリンクなども表示。

image.png

ブコメが無い場合

こうなる。

image.png

できないこと

あくまで「読み込んで表示」だけなので、ページ内でブックマークしたり、コメントにスターを付けたりはできない。ただし、リンク付けてるので(面倒だけど)ページ遷移すればできるはず。

技術的な話

(初歩的な内容も含むが)拡張機能内で使っているツールや設定などを書いていく。「もっと他にいいやり方あるよ」とかあればコメント貰えると嬉しい。

リポジトリ: 17number/chrome-extension-hatiina: Chrome extension for qiita

開発環境

macBook Pro で開発した。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.15
BuildVersion:   19A583

$ node -v
v12.7.0

$ npm -v
6.10.3

使用パッケージ

package.json より

package.json
  "dependencies": {
    "@fortawesome/fontawesome-svg-core": "^1.2.25",
    "@fortawesome/free-brands-svg-icons": "^5.11.2",
    "@fortawesome/free-regular-svg-icons": "^5.11.2",
    "@fortawesome/free-solid-svg-icons": "^5.11.2",
    "@fortawesome/vue-fontawesome": "^0.1.7",
    "axios": "^0.19.0",
    "crx-hotreload": "^1.0.4",
    "moment-timezone": "^0.5.26",
    "vue": "^2.6.10",
    "webextension-polyfill": "^0.5.0"
  },
  "devDependencies": {
    "@vue/cli-service": "^3.11.0",
    "cpx": "^1.5.0",
    "mkdirp2": "^1.0.4",
    "node-sass": "^4.12.0",
    "npm-run-all": "^4.1.5",
    "npm-watch": "^0.6.0",
    "rimraf": "^3.0.0",
    "sass-loader": "^8.0.0",
    "terser-webpack-plugin": "^2.1.3",
    "vue-svg-loader": "^0.12.0",
    "vue-template-compiler": "^2.6.10"
  }

Vue.js を主ページ側で使う方法

整理すればそんなに難しいことじゃない。(ここに辿り着くまでに割と右往左往した)

  • manifest.json で先に Vue.js (の定義されたファイル)を読み込む
  • Vue.js のターゲットとなる HTML 要素を埋め込む
  • 埋め込まれた HTML 要素に対して Vue のインスタンスを生成する

manifest.json より

manifest.json
{
  ...
  "content_scripts": [
    {
      "matches": [
        "https://qiita.com/*/items*"
      ],
      "js": [
        "js/chunk-vendors.js",  # Vue.js のコードはここに入っている(ので先に読み込む)
        "js/app.js"             # ここで Vue.js のインスタンス生成とか
      ],
    }
  ],
  ...
}

app.js(エントリポイント)

app.js
import Vue from "vue";
import App from "./App.vue";

(() => {
  'use strict';

  document
    .querySelector(".p-items > .p-items_container > .p-items_main")
    .insertAdjacentHTML(
      `afterBegin`,
      `<div id="hatiina"></div>`
    );

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

この辺りの話は自分のブログに書いているので合わせて載せておく。

Chrome 拡張機能の開発中にホットリロード(自動読み込み)する方法

ビルド関連

ビルドは vue-cli-service で行う。Content Scripts 側と Background Scripts 側とでコマンドを分けている。

Content Scripts 側

--no-clean オプションを付けている理由は後述。

package.json
  "scripts": {
    "compile:content": "vue-cli-service build --no-clean src/app.js",
  },

ポイントは vue.config.js による設定で、filenameHashingproductionSourceMapfalse にしていること。

vue.config.js
module.exports = {
  filenameHashing: false,
  productionSourceMap: false,
};

filenameHashing の設定をしていないと、app.1234abcd.js のようなファイル名で出力されてしまい、manifest.json で指定するソースファイルの記述を修正しないといけなくなる。頑張れば自動で変更できると思うが、それよりはファイル名を固定にした方が良い。

productionSourceMap の設定は、ソースマップファイルは不要だよね、というだけ。

Background Scripts 側

Background Scripts 側のビルドは色々と試した結果、以下の設定だと上手く動作した。

package.json
  "scripts": {
    "compile:background": "vue-cli-service build --target lib --formats umd --dest dist/js --no-clean --name background src/js/background.js",
  },

これで src/js/background.js(とそこから読み込まれているパッケージなど)が、dist/js/background.umd.js に出力される。「なぜそのオプションにしたのか」については、何となくは分かるが明確に言語化できない(= 理解が浅い)ので、誤解がうまれるのを避けるためにも言及しない。

※よくよく考えると、別に Vue.js 関係ない(はてなブックマークの API を叩いて返すだけ)から、別に vue-cli-service 使う必要はなかったんじゃ…

--no-clean オプションの理由

以下の記事を参考に、ファイル変更を検知してホットリロードさせている。

Chrome Extensionの開発時にホットリロードさせる - Qiita

(試してないので確実ではないが) --no-clean を付けずにビルドして、dist ディレクトリごと削除されると挙動がおかしくなるのでは?と思い、既存コードは残したまま上書きするようにした。

自分のブログで NPM 使う設定例も書いているので、ついでに載s

Chrome 拡張機能の開発中にホットリロード(自動読み込み)する方法

その他

FontAwesome

FontAwesome は Vue.js Project で使用可能な公式パッケージがあるのでそれを利用。

Font Awesome now has an official Vue.js component that’s available for all who want to use our icons in their Vue.js projects.
Vue.js | Font Awesome より

@fortawesome/vue-fontawesomeUsage に書かれている通り、importして Vue のインスタンス生成前にゴニョゴニョすれば、<font-awesome-icon /> というタグで表示できる。

app.js
import { library } from '@fortawesome/fontawesome-svg-core';
// ↓は使いたいアイコン
// 今回は bookmark(https://fontawesome.com/icons/bookmark?style=solid) なので faBookmark
import { faBookmark } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';

(() => {
  ...

  // FontAwesome Setup
  library.add(faBookmark)
  Vue.component('font-awesome-icon', FontAwesomeIcon)
  Vue.config.productionTip = false

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

:icon で Style(Regular(far) とか Solid(fas) とか) とアイコン名を指定する。

xxx.vue
<template>
  <div id="app">
    <font-awesome-icon :icon="['fas', 'bookmark']" />
  </div>
</template>

以下の記事が分かりやすい。

Font awesome を Vue.js で使ってみよう - Qiita

画像・アイコン

はてなスター

はてなスターの画像は安定のいらすとやから拝借して利用。

いろいろな色の星のイラスト | かわいいフリー素材集 いらすとや

上記をダウンロードして、画像サイズを小さくした後に、TinyPNG で圧縮。

はてなブックマーク

公式素材があるのでそれを利用。SVG だったので、そのままプロジェクト内に取り込み。

素材集 - 株式会社はてな

拡張機能アイコン・ウェブストア画像

Canva で作成。

フォントはうずらフォントを使用、理由はなんとなく気に入ったから

ウェブストアの説明や画像はそのうちどうにかする予定…

Hatiina のイケてない点

API Request 数が多い

はてなスター数を取ってくるのに API を連打してしまっている。これは解決手段が分かってるので、別途修正予定。

なんかスター数が多い

公式拡張機能のカウント数と見比べると分かるんだけど、なんか表示されるスター数に差がある。

とは言え、全体的に多くカウントされる(= みんな多い)だけなので、(個人的には)大した問題じゃないと思って放置予定。

ブコメ数が多いとダルい

ブコメを閉じるには、一番上の箇所をクリックするしかないんだけど、全200コメント中の100コメントあたりを見ている状態だと、閉じるにはスクロールしまくるしかない。

ウィンドウ幅を狭くした時の見た目

めっちゃ狭くするとユーザーアイコンの表示が変になったり、特定の幅で Qiita 本体のレイアウトと適合しない配置になったりする。

本質的な問題じゃないので放置。

共通化などが中途半端(な気がする)

もう少しコンポーネント分割できそうな気がする。

あと、Vue.js の(把握できていない)便利機能とかで、もっとシンプルに書けそうな気もする。

あとがき

メールとかから飛んでもブコメ見れるから、はてブ民は良ければ使ってくれよな。

Hatiina - Chrome ウェブストア

※拡張機能を公開したのが 2019/10/12 なので、もしかしたら審査リジェクトで見えなくなる期間が出てくるかも。

リポジトリも公開してるから「変な拡張機能怖い」って人とか、「おかしな実装ないか調べたろ!」って人も安心だよ。

17number/chrome-extension-hatiina: Chrome extension for qiita

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

はてなブックマークのコメントを Qiita 記事内に表示する Chrome 拡張機能 を Vue.js で作った話

はじめに

Qiita もはてなもこよなく愛する方のために、以下画像のように Qiita の記事の終わりにはてなブックマークのコメントを読み込んで表示する拡張機能 Hatiina を作った。

※拡張機能を公開したのが 2019/10/12 なので、もしかしたら審査リジェクトで見えなくなる期間が出てくるかも。

image.png

TL:DR;

  • クエリパラメータ(?hoge=fuga&foo=bar)やアンカー(#xxx)付きの URL でもブコメを表示できる
    • メールなどから Qiita にアクセスした場合とか
  • スター獲得数順にソート
  • 実装には Vue.js (とその他いくつかの NPM packages)を利用

作ろうと思った動機

1. 普段の作業を簡略化するため

自分はググって Qiita を見ることが多いが、毎週来る Qiita のニュースメールや Twitter からの通知メールから気になった記事を見ることも結構ある。こういうやつ。

image.png

image.png

で、記事本文だけでなく、なるべく記事中のコメントやはてなブックマークのコメントも見ることで、「記事中で言及されている内容に対して、他の人や玄人はどう考えているのか」や「他に良いツール/やり方/考え方は無いか」などの参考にしている。のだが、↑のリンクから飛ぶとはてなブックマーク拡張機能でコメントが読み込めない

image.png

画像から分かるかもしれないが、原因は URL 内のクエリパラメータやアンカーにある。記事本文の素の URL にページ移動するとコメントが正しく表示できる

image.png

…できるんだけど、「メールから記事開く」→「アドレスバーにフォーカス」→「URL 修正して移動」→「はてなブックマーク拡張機能をクリック」という手順が無駄だし面倒すぎてイライラしていた。

というわけで作った。

2. 技術的に試したいことがあった

Vue.js を使った拡張機能の記事をいくつか見たが、どれもメインページ側(という表現で良いか不安)に作用するものではなく、ポップアップページ(拡張機能アイコンのクリックで表示されるやつ)に関するものだった。(自分が見た範囲では)

「俺が Vue.js を使いたいのはそこじゃないんだよ!」と思って「無いなら試してしまえ」の精神で作った。ちなみに「使ってみたかった」なので、「Vue.js じゃないと作れない」というものではない。

拡張機能の紹介

Hatiina - Chrome ウェブストア

ウェブストアの画像や説明が雑なのはご愛嬌ということで…

できること

ページ遷移なしに「素の URL」に付いているブコメを読み込んで表示。

もうほぼ説明することは無い(前章までに書いてある)ので、簡単にスクショで紹介。

読み込み直後

ブコメは閉じた状態、クリックすると展開される

image.png

ブコメを展開

スター数順にソートされた状態。ブックマーク追加ページへのリンクや、各ユーザーへのリンクなども表示。

image.png

ブコメが無い場合

こうなる。

image.png

できないこと

あくまで「読み込んで表示」だけなので、ページ内でブックマークしたり、コメントにスターを付けたりはできない。ただし、リンク付けてるので(面倒だけど)ページ遷移すればできるはず。

技術的な話

(初歩的な内容も含むが)拡張機能内で使っているツールや設定などを書いていく。「もっと他にいいやり方あるよ」とかあればコメント貰えると嬉しい。

リポジトリ: 17number/chrome-extension-hatiina: Chrome extension for qiita

開発環境

macBook Pro で開発した。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.15
BuildVersion:   19A583

$ node -v
v12.7.0

$ npm -v
6.10.3

使用パッケージ

package.json より

package.json
  "dependencies": {
    "@fortawesome/fontawesome-svg-core": "^1.2.25",
    "@fortawesome/free-brands-svg-icons": "^5.11.2",
    "@fortawesome/free-regular-svg-icons": "^5.11.2",
    "@fortawesome/free-solid-svg-icons": "^5.11.2",
    "@fortawesome/vue-fontawesome": "^0.1.7",
    "axios": "^0.19.0",
    "crx-hotreload": "^1.0.4",
    "moment-timezone": "^0.5.26",
    "vue": "^2.6.10",
    "webextension-polyfill": "^0.5.0"
  },
  "devDependencies": {
    "@vue/cli-service": "^3.11.0",
    "cpx": "^1.5.0",
    "mkdirp2": "^1.0.4",
    "node-sass": "^4.12.0",
    "npm-run-all": "^4.1.5",
    "npm-watch": "^0.6.0",
    "rimraf": "^3.0.0",
    "sass-loader": "^8.0.0",
    "terser-webpack-plugin": "^2.1.3",
    "vue-svg-loader": "^0.12.0",
    "vue-template-compiler": "^2.6.10"
  }

Vue.js を主ページ側で使う方法

整理すればそんなに難しいことじゃない。(ここに辿り着くまでに割と右往左往した)

  • manifest.json で先に Vue.js (の定義されたファイル)を読み込む
  • Vue.js のターゲットとなる HTML 要素を埋め込む
  • 埋め込まれた HTML 要素に対して Vue のインスタンスを生成する

manifest.json より

manifest.json
{
  ...
  "content_scripts": [
    {
      "matches": [
        "https://qiita.com/*/items*"
      ],
      "js": [
        "js/chunk-vendors.js",  # Vue.js のコードはここに入っている(ので先に読み込む)
        "js/app.js"             # ここで Vue.js のインスタンス生成とか
      ],
    }
  ],
  ...
}

app.js(エントリポイント)

app.js
import Vue from "vue";
import App from "./App.vue";

(() => {
  'use strict';

  document
    .querySelector(".p-items > .p-items_container > .p-items_main")
    .insertAdjacentHTML(
      `afterBegin`,
      `<div id="hatiina"></div>`
    );

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

この辺りの話は自分のブログに書いているので合わせて載せておく。

Chrome 拡張機能の開発中にホットリロード(自動読み込み)する方法

ビルド関連

ビルドは vue-cli-service で行う。Content Scripts 側と Background Scripts 側とでコマンドを分けている。

Content Scripts 側

--no-clean オプションを付けている理由は後述。

package.json
  "scripts": {
    "compile:content": "vue-cli-service build --no-clean src/app.js",
  },

ポイントは vue.config.js による設定で、filenameHashingproductionSourceMapfalse にしていること。

vue.config.js
module.exports = {
  filenameHashing: false,
  productionSourceMap: false,
};

filenameHashing の設定をしていないと、app.1234abcd.js のようなファイル名で出力されてしまい、manifest.json で指定するソースファイルの記述を修正しないといけなくなる。頑張れば自動で変更できると思うが、それよりはファイル名を固定にした方が良い。

productionSourceMap の設定は、ソースマップファイルは不要だよね、というだけ。

Background Scripts 側

Background Scripts 側のビルドは色々と試した結果、以下の設定だと上手く動作した。

package.json
  "scripts": {
    "compile:background": "vue-cli-service build --target lib --formats umd --dest dist/js --no-clean --name background src/js/background.js",
  },

これで src/js/background.js(とそこから読み込まれているパッケージなど)が、dist/js/background.umd.js に出力される。「なぜそのオプションにしたのか」については、何となくは分かるが明確に言語化できない(= 理解が浅い)ので、誤解がうまれるのを避けるためにも言及しない。

※よくよく考えると、別に Vue.js 関係ない(はてなブックマークの API を叩いて返すだけ)から、別に vue-cli-service 使う必要はなかったんじゃ…

--no-clean オプションの理由

以下の記事を参考に、ファイル変更を検知してホットリロードさせている。

Chrome Extensionの開発時にホットリロードさせる - Qiita

(試してないので確実ではないが) --no-clean を付けずにビルドして、dist ディレクトリごと削除されると挙動がおかしくなるのでは?と思い、既存コードは残したまま上書きするようにした。

自分のブログで NPM 使う設定例も書いているので、ついでに載s

Chrome 拡張機能の開発中にホットリロード(自動読み込み)する方法

その他

FontAwesome

FontAwesome は Vue.js Project で使用可能な公式パッケージがあるのでそれを利用。

Font Awesome now has an official Vue.js component that’s available for all who want to use our icons in their Vue.js projects.
Vue.js | Font Awesome より

@fortawesome/vue-fontawesomeUsage に書かれている通り、importして Vue のインスタンス生成前にゴニョゴニョすれば、<font-awesome-icon /> というタグで表示できる。

app.js
import { library } from '@fortawesome/fontawesome-svg-core';
// ↓は使いたいアイコン
// 今回は bookmark(https://fontawesome.com/icons/bookmark?style=solid) なので faBookmark
import { faBookmark } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';

(() => {
  ...

  // FontAwesome Setup
  library.add(faBookmark)
  Vue.component('font-awesome-icon', FontAwesomeIcon)
  Vue.config.productionTip = false

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

:icon で Style(Regular(far) とか Solid(fas) とか) とアイコン名を指定する。

xxx.vue
<template>
  <div id="app">
    <font-awesome-icon :icon="['fas', 'bookmark']" />
  </div>
</template>

以下の記事が分かりやすい。

Font awesome を Vue.js で使ってみよう - Qiita

画像・アイコン

はてなスター

はてなスターの画像は安定のいらすとやから拝借して利用。

いろいろな色の星のイラスト | かわいいフリー素材集 いらすとや

上記をダウンロードして、画像サイズを小さくした後に、TinyPNG で圧縮。

はてなブックマーク

公式素材があるのでそれを利用。SVG だったので、そのままプロジェクト内に取り込み。

素材集 - 株式会社はてな

拡張機能アイコン・ウェブストア画像

Canva で作成。

フォントはうずらフォントを使用、理由はなんとなく気に入ったから

ウェブストアの説明や画像はそのうちどうにかする予定…

Hatiina のイケてない点

API Request 数が多い

はてなスター数を取ってくるのに API を連打してしまっている。これは解決手段が分かってるので、別途修正予定。

なんかスター数が多い

公式拡張機能のカウント数と見比べると分かるんだけど、なんか表示されるスター数に差がある。

とは言え、全体的に多くカウントされる(= みんな多い)だけなので、(個人的には)大した問題じゃないと思って放置予定。

ブコメ数が多いとダルい

ブコメを閉じるには、一番上の箇所をクリックするしかないんだけど、全200コメント中の100コメントあたりを見ている状態だと、閉じるにはスクロールしまくるしかない。

ウィンドウ幅を狭くした時の見た目

めっちゃ狭くするとユーザーアイコンの表示が変になったり、特定の幅で Qiita 本体のレイアウトと適合しない配置になったりする。

本質的な問題じゃないので放置。

共通化などが中途半端(な気がする)

もう少しコンポーネント分割できそうな気がする。

あと、Vue.js の(把握できていない)便利機能とかで、もっとシンプルに書けそうな気もする。

あとがき

メールとかから飛んでもブコメ見れるから、はてブ民は良ければ使ってくれよな。

Hatiina - Chrome ウェブストア

※拡張機能を公開したのが 2019/10/12 なので、もしかしたら審査リジェクトで見えなくなる期間が出てくるかも。

リポジトリも公開してるから「変な拡張機能怖い」って人とか、「おかしな実装ないか調べたろ!」って人も安心だよ。

17number/chrome-extension-hatiina: Chrome extension for qiita

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

Vue.js を使ってChrome 拡張機能を作った話(はてなブックマークのコメントを Qiita 記事内に表示)

はじめに

Qiita もはてなもこよなく愛する方のために、以下画像のように Qiita の記事の終わりにはてなブックマークのコメントを読み込んで表示する拡張機能 Hatiina を作った。

※拡張機能を公開したのが 2019/10/12 なので、もしかしたら審査リジェクトで見えなくなる期間が出てくるかも。

image.png

TL:DR;

  • クエリパラメータ(?hoge=fuga&foo=bar)やアンカー(#xxx)付きの URL でもブコメを表示できる
    • メールなどから Qiita にアクセスした場合とか
  • スター獲得数順にソート
  • 実装には Vue.js (とその他いくつかの NPM packages)を利用

作ろうと思った動機

1. 普段の作業を簡略化するため

自分はググって Qiita を見ることが多いが、毎週来る Qiita のニュースメールや Twitter からの通知メールから気になった記事を見ることも結構ある。こういうやつ。

image.png

image.png

で、記事本文だけでなく、なるべく記事中のコメントやはてなブックマークのコメントも見ることで、「記事中で言及されている内容に対して、他の人や玄人はどう考えているのか」や「他に良いツール/やり方/考え方は無いか」などの参考にしている。のだが、↑のリンクから飛ぶとはてなブックマーク拡張機能でコメントが読み込めない

image.png

画像から分かるかもしれないが、原因は URL 内のクエリパラメータやアンカーにある。記事本文の素の URL にページ移動するとコメントが正しく表示できる

image.png

…できるんだけど、「メールから記事開く」→「アドレスバーにフォーカス」→「URL 修正して移動」→「はてなブックマーク拡張機能をクリック」という手順が無駄だし面倒すぎてイライラしていた。

というわけで作った。

2. 技術的に試したいことがあった

Vue.js を使った拡張機能の記事をいくつか見たが、どれもメインページ側(という表現で良いか不安)に作用するものではなく、ポップアップページ(拡張機能アイコンのクリックで表示されるやつ)に関するものだった。(自分が見た範囲では)

「俺が Vue.js を使いたいのはそこじゃないんだよ!」と思って「無いなら試してしまえ」の精神で作った。ちなみに「使ってみたかった」なので、「Vue.js じゃないと作れない」というものではない。

拡張機能の紹介

Hatiina - Chrome ウェブストア

ウェブストアの画像や説明が雑なのはご愛嬌ということで…

できること

ページ遷移なしに「素の URL」に付いているブコメを読み込んで表示。

もうほぼ説明することは無い(前章までに書いてある)ので、簡単にスクショで紹介。

読み込み直後

ブコメは閉じた状態、クリックすると展開される

image.png

ブコメを展開

スター数順にソートされた状態。ブックマーク追加ページへのリンクや、各ユーザーへのリンクなども表示。

image.png

ブコメが無い場合

こうなる。

image.png

できないこと

あくまで「読み込んで表示」だけなので、ページ内でブックマークしたり、コメントにスターを付けたりはできない。ただし、リンク付けてるので(面倒だけど)ページ遷移すればできるはず。

技術的な話

(初歩的な内容も含むが)拡張機能内で使っているツールや設定などを書いていく。「もっと他にいいやり方あるよ」とかあればコメント貰えると嬉しい。

リポジトリ: 17number/chrome-extension-hatiina: Chrome extension for qiita

開発環境

macBook Pro で開発した。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.15
BuildVersion:   19A583

$ node -v
v12.7.0

$ npm -v
6.10.3

使用パッケージ

package.json より

package.json
  "dependencies": {
    "@fortawesome/fontawesome-svg-core": "^1.2.25",
    "@fortawesome/free-brands-svg-icons": "^5.11.2",
    "@fortawesome/free-regular-svg-icons": "^5.11.2",
    "@fortawesome/free-solid-svg-icons": "^5.11.2",
    "@fortawesome/vue-fontawesome": "^0.1.7",
    "axios": "^0.19.0",
    "crx-hotreload": "^1.0.4",
    "moment-timezone": "^0.5.26",
    "vue": "^2.6.10",
    "webextension-polyfill": "^0.5.0"
  },
  "devDependencies": {
    "@vue/cli-service": "^3.11.0",
    "cpx": "^1.5.0",
    "mkdirp2": "^1.0.4",
    "node-sass": "^4.12.0",
    "npm-run-all": "^4.1.5",
    "npm-watch": "^0.6.0",
    "rimraf": "^3.0.0",
    "sass-loader": "^8.0.0",
    "terser-webpack-plugin": "^2.1.3",
    "vue-svg-loader": "^0.12.0",
    "vue-template-compiler": "^2.6.10"
  }

Vue.js を主ページ側で使う方法

整理すればそんなに難しいことじゃない。(ここに辿り着くまでに割と右往左往した)

  • manifest.json で先に Vue.js (の定義されたファイル)を読み込む
  • Vue.js のターゲットとなる HTML 要素を埋め込む
  • 埋め込まれた HTML 要素に対して Vue のインスタンスを生成する

manifest.json より

manifest.json
{
  ...
  "content_scripts": [
    {
      "matches": [
        "https://qiita.com/*/items*"
      ],
      "js": [
        "js/chunk-vendors.js",  # Vue.js のコードはここに入っている(ので先に読み込む)
        "js/app.js"             # ここで Vue.js のインスタンス生成とか
      ],
    }
  ],
  ...
}

app.js(エントリポイント)

app.js
import Vue from "vue";
import App from "./App.vue";

(() => {
  'use strict';

  document
    .querySelector(".p-items > .p-items_container > .p-items_main")
    .insertAdjacentHTML(
      `afterBegin`,
      `<div id="hatiina"></div>`
    );

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

この辺りの話は自分のブログに書いているので合わせて載せておく。

Chrome 拡張機能の開発中にホットリロード(自動読み込み)する方法

ビルド関連

ビルドは vue-cli-service で行う。Content Scripts 側と Background Scripts 側とでコマンドを分けている。

Content Scripts 側

--no-clean オプションを付けている理由は後述。

package.json
  "scripts": {
    "compile:content": "vue-cli-service build --no-clean src/app.js",
  },

ポイントは vue.config.js による設定で、filenameHashingproductionSourceMapfalse にしていること。

vue.config.js
module.exports = {
  filenameHashing: false,
  productionSourceMap: false,
};

filenameHashing の設定をしていないと、app.1234abcd.js のようなファイル名で出力されてしまい、manifest.json で指定するソースファイルの記述を修正しないといけなくなる。頑張れば自動で変更できると思うが、それよりはファイル名を固定にした方が良い。

productionSourceMap の設定は、ソースマップファイルは不要だよね、というだけ。

Background Scripts 側

Background Scripts 側のビルドは色々と試した結果、以下の設定だと上手く動作した。

package.json
  "scripts": {
    "compile:background": "vue-cli-service build --target lib --formats umd --dest dist/js --no-clean --name background src/js/background.js",
  },

これで src/js/background.js(とそこから読み込まれているパッケージなど)が、dist/js/background.umd.js に出力される。「なぜそのオプションにしたのか」については、何となくは分かるが明確に言語化できない(= 理解が浅い)ので、誤解がうまれるのを避けるためにも言及しない。

※よくよく考えると、別に Vue.js 関係ない(はてなブックマークの API を叩いて返すだけ)から、別に vue-cli-service 使う必要はなかったんじゃ…

--no-clean オプションの理由

以下の記事を参考に、ファイル変更を検知してホットリロードさせている。

Chrome Extensionの開発時にホットリロードさせる - Qiita

(試してないので確実ではないが) --no-clean を付けずにビルドして、dist ディレクトリごと削除されると挙動がおかしくなるのでは?と思い、既存コードは残したまま上書きするようにした。

自分のブログで NPM 使う設定例も書いているので、ついでに載s

Chrome 拡張機能の開発中にホットリロード(自動読み込み)する方法

その他

FontAwesome

FontAwesome は Vue.js Project で使用可能な公式パッケージがあるのでそれを利用。

Font Awesome now has an official Vue.js component that’s available for all who want to use our icons in their Vue.js projects.
Vue.js | Font Awesome より

@fortawesome/vue-fontawesomeUsage に書かれている通り、importして Vue のインスタンス生成前にゴニョゴニョすれば、<font-awesome-icon /> というタグで表示できる。

app.js
import { library } from '@fortawesome/fontawesome-svg-core';
// ↓は使いたいアイコン
// 今回は bookmark(https://fontawesome.com/icons/bookmark?style=solid) なので faBookmark
import { faBookmark } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';

(() => {
  ...

  // FontAwesome Setup
  library.add(faBookmark)
  Vue.component('font-awesome-icon', FontAwesomeIcon)
  Vue.config.productionTip = false

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

:icon で Style(Regular(far) とか Solid(fas) とか) とアイコン名を指定する。

xxx.vue
<template>
  <div id="app">
    <font-awesome-icon :icon="['fas', 'bookmark']" />
  </div>
</template>

以下の記事が分かりやすい。

Font awesome を Vue.js で使ってみよう - Qiita

画像・アイコン

はてなスター

はてなスターの画像は安定のいらすとやから拝借して利用。

いろいろな色の星のイラスト | かわいいフリー素材集 いらすとや

上記をダウンロードして、画像サイズを小さくした後に、TinyPNG で圧縮。

はてなブックマーク

公式素材があるのでそれを利用。SVG だったので、そのままプロジェクト内に取り込み。

素材集 - 株式会社はてな

拡張機能アイコン・ウェブストア画像

Canva で作成。

フォントはうずらフォントを使用、理由はなんとなく気に入ったから

ウェブストアの説明や画像はそのうちどうにかする予定…

Hatiina のイケてない点

API Request 数が多い

はてなスター数を取ってくるのに API を連打してしまっている。これは解決手段が分かってるので、別途修正予定。

なんかスター数が多い

公式拡張機能のカウント数と見比べると分かるんだけど、なんか表示されるスター数に差がある。

とは言え、全体的に多くカウントされる(= みんな多い)だけなので、(個人的には)大した問題じゃないと思って放置予定。

ブコメ数が多いとダルい

ブコメを閉じるには、一番上の箇所をクリックするしかないんだけど、全200コメント中の100コメントあたりを見ている状態だと、閉じるにはスクロールしまくるしかない。

ウィンドウ幅を狭くした時の見た目

めっちゃ狭くするとユーザーアイコンの表示が変になったり、特定の幅で Qiita 本体のレイアウトと適合しない配置になったりする。

本質的な問題じゃないので放置。

共通化などが中途半端(な気がする)

もう少しコンポーネント分割できそうな気がする。

あと、Vue.js の(把握できていない)便利機能とかで、もっとシンプルに書けそうな気もする。

あとがき

メールとかから飛んでもブコメ見れるから、はてブ民は良ければ使ってくれよな。

Hatiina - Chrome ウェブストア

※拡張機能を公開したのが 2019/10/12 なので、もしかしたら審査リジェクトで見えなくなる期間が出てくるかも。

リポジトリも公開してるから「変な拡張機能怖い」って人とか、「おかしな実装ないか調べたろ!」って人も安心だよ。

17number/chrome-extension-hatiina: Chrome extension for qiita

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

はてなブックマークのコメントを Qiita 記事内に埋め込む Chrome 拡張機能 Hatiina を Vue.js で作った

はじめに

Qiita もはてなもこよなく愛する方のために、以下画像のように Qiita の記事の終わりにはてなブックマークのコメントを読み込んで表示する拡張機能 Hatiina を作った。

※拡張機能を公開したのが 2019/10/12 なので、もしかしたら審査リジェクトで見えなくなる期間が出てくるかも。

image.png

TL:DR;

  • クエリパラメータ(?hoge=fuga&foo=bar)やアンカー(#xxx)付きの URL でもブコメを表示できる
    • メールなどから Qiita にアクセスした場合とか
  • スター獲得数順にソート
  • 実装には Vue.js (とその他いくつかの NPM packages)を利用

作ろうと思った動機

1. 普段の作業を簡略化するため

自分はググって Qiita を見ることが多いが、毎週来る Qiita のニュースメールや Twitter からの通知メールから気になった記事を見ることも結構ある。こういうやつ。

image.png

image.png

で、記事本文だけでなく、なるべく記事中のコメントやはてなブックマークのコメントも見ることで、「記事中で言及されている内容に対して、他の人や玄人はどう考えているのか」や「他に良いツール/やり方/考え方は無いか」などの参考にしている。のだが、↑のリンクから飛ぶとはてなブックマーク拡張機能でコメントが読み込めない

image.png

画像から分かるかもしれないが、原因は URL 内のクエリパラメータやアンカーにある。記事本文の素の URL にページ移動するとコメントが正しく表示できる

image.png

…できるんだけど、「メールから記事開く」→「アドレスバーにフォーカス」→「URL 修正して移動」→「はてなブックマーク拡張機能をクリック」という手順が無駄だし面倒すぎてイライラしていた。

というわけで作った。

2. 技術的に試したいことがあった

Vue.js を使った拡張機能の記事をいくつか見たが、どれもメインページ側(という表現で良いか不安)に作用するものではなく、ポップアップページ(拡張機能アイコンのクリックで表示されるやつ)に関するものだった。(自分が見た範囲では)

「俺が Vue.js を使いたいのはそこじゃないんだよ!」と思って「無いなら試してしまえ」の精神で作った。ちなみに「使ってみたかった」なので、「Vue.js じゃないと作れない」というものではない。

拡張機能の紹介

Hatiina - Chrome ウェブストア

ウェブストアの画像や説明が雑なのはご愛嬌ということで…

できること

ページ遷移なしに「素の URL」に付いているブコメを読み込んで表示。

もうほぼ説明することは無い(前章までに書いてある)ので、簡単にスクショで紹介。

読み込み直後

ブコメは閉じた状態、クリックすると展開される

image.png

ブコメを展開

スター数順にソートされた状態。ブックマーク追加ページへのリンクや、各ユーザーへのリンクなども表示。

image.png

ブコメが無い場合

こうなる。

image.png

できないこと

あくまで「読み込んで表示」だけなので、ページ内でブックマークしたり、コメントにスターを付けたりはできない。ただし、リンク付けてるので(面倒だけど)ページ遷移すればできるはず。

技術的な話

(初歩的な内容も含むが)拡張機能内で使っているツールや設定などを書いていく。「もっと他にいいやり方あるよ」とかあればコメント貰えると嬉しい。

リポジトリ: 17number/chrome-extension-hatiina: Chrome extension for qiita

開発環境

macBook Pro で開発した。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.15
BuildVersion:   19A583

$ node -v
v12.7.0

$ npm -v
6.10.3

使用パッケージ

package.json より

package.json
  "dependencies": {
    "@fortawesome/fontawesome-svg-core": "^1.2.25",
    "@fortawesome/free-brands-svg-icons": "^5.11.2",
    "@fortawesome/free-regular-svg-icons": "^5.11.2",
    "@fortawesome/free-solid-svg-icons": "^5.11.2",
    "@fortawesome/vue-fontawesome": "^0.1.7",
    "axios": "^0.19.0",
    "crx-hotreload": "^1.0.4",
    "moment-timezone": "^0.5.26",
    "vue": "^2.6.10",
    "webextension-polyfill": "^0.5.0"
  },
  "devDependencies": {
    "@vue/cli-service": "^3.11.0",
    "cpx": "^1.5.0",
    "mkdirp2": "^1.0.4",
    "node-sass": "^4.12.0",
    "npm-run-all": "^4.1.5",
    "npm-watch": "^0.6.0",
    "rimraf": "^3.0.0",
    "sass-loader": "^8.0.0",
    "terser-webpack-plugin": "^2.1.3",
    "vue-svg-loader": "^0.12.0",
    "vue-template-compiler": "^2.6.10"
  }

Vue.js を主ページ側で使う方法

整理すればそんなに難しいことじゃない。(ここに辿り着くまでに割と右往左往した)

  • manifest.json で先に Vue.js (の定義されたファイル)を読み込む
  • Vue.js のターゲットとなる HTML 要素を埋め込む
  • 埋め込まれた HTML 要素に対して Vue のインスタンスを生成する

manifest.json より

manifest.json
{
  ...
  "content_scripts": [
    {
      "matches": [
        "https://qiita.com/*/items*"
      ],
      "js": [
        "js/chunk-vendors.js",  # Vue.js のコードはここに入っている(ので先に読み込む)
        "js/app.js"             # ここで Vue.js のインスタンス生成とか
      ],
    }
  ],
  ...
}

app.js(エントリポイント)

app.js
import Vue from "vue";
import App from "./App.vue";

(() => {
  'use strict';

  document
    .querySelector(".p-items > .p-items_container > .p-items_main")
    .insertAdjacentHTML(
      `afterBegin`,
      `<div id="hatiina"></div>`
    );

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

この辺りの話は自分のブログに書いているので合わせて載せておく。

Chrome 拡張機能の開発中にホットリロード(自動読み込み)する方法

ビルド関連

ビルドは vue-cli-service で行う。Content Scripts 側と Background Scripts 側とでコマンドを分けている。

Content Scripts 側

--no-clean オプションを付けている理由は後述。

package.json
  "scripts": {
    "compile:content": "vue-cli-service build --no-clean src/app.js",
  },

ポイントは vue.config.js による設定で、filenameHashingproductionSourceMapfalse にしていること。

vue.config.js
module.exports = {
  filenameHashing: false,
  productionSourceMap: false,
};

filenameHashing の設定をしていないと、app.1234abcd.js のようなファイル名で出力されてしまい、manifest.json で指定するソースファイルの記述を修正しないといけなくなる。頑張れば自動で変更できると思うが、それよりはファイル名を固定にした方が良い。

productionSourceMap の設定は、ソースマップファイルは不要だよね、というだけ。

Background Scripts 側

Background Scripts 側のビルドは色々と試した結果、以下の設定だと上手く動作した。

package.json
  "scripts": {
    "compile:background": "vue-cli-service build --target lib --formats umd --dest dist/js --no-clean --name background src/js/background.js",
  },

これで src/js/background.js(とそこから読み込まれているパッケージなど)が、dist/js/background.umd.js に出力される。「なぜそのオプションにしたのか」については、何となくは分かるが明確に言語化できない(= 理解が浅い)ので、誤解がうまれるのを避けるためにも言及しない。

※よくよく考えると、別に Vue.js 関係ない(はてなブックマークの API を叩いて返すだけ)から、別に vue-cli-service 使う必要はなかったんじゃ…

--no-clean オプションの理由

以下の記事を参考に、ファイル変更を検知してホットリロードさせている。

Chrome Extensionの開発時にホットリロードさせる - Qiita

(試してないので確実ではないが) --no-clean を付けずにビルドして、dist ディレクトリごと削除されると挙動がおかしくなるのでは?と思い、既存コードは残したまま上書きするようにした。

自分のブログで NPM 使う設定例も書いているので、ついでに載s

Chrome 拡張機能の開発中にホットリロード(自動読み込み)する方法

その他

FontAwesome

FontAwesome は Vue.js Project で使用可能な公式パッケージがあるのでそれを利用。

Font Awesome now has an official Vue.js component that’s available for all who want to use our icons in their Vue.js projects.
Vue.js | Font Awesome より

@fortawesome/vue-fontawesomeUsage に書かれている通り、importして Vue のインスタンス生成前にゴニョゴニョすれば、<font-awesome-icon /> というタグで表示できる。

app.js
import { library } from '@fortawesome/fontawesome-svg-core';
// ↓は使いたいアイコン
// 今回は bookmark(https://fontawesome.com/icons/bookmark?style=solid) なので faBookmark
import { faBookmark } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';

(() => {
  ...

  // FontAwesome Setup
  library.add(faBookmark)
  Vue.component('font-awesome-icon', FontAwesomeIcon)
  Vue.config.productionTip = false

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

:icon で Style(Regular(far) とか Solid(fas) とか) とアイコン名を指定する。

xxx.vue
<template>
  <div id="app">
    <font-awesome-icon :icon="['fas', 'bookmark']" />
  </div>
</template>

以下の記事が分かりやすい。

Font awesome を Vue.js で使ってみよう - Qiita

画像・アイコン

はてなスター

はてなスターの画像は安定のいらすとやから拝借して利用。

いろいろな色の星のイラスト | かわいいフリー素材集 いらすとや

上記をダウンロードして、画像サイズを小さくした後に、TinyPNG で圧縮。

はてなブックマーク

公式素材があるのでそれを利用。SVG だったので、そのままプロジェクト内に取り込み。

素材集 - 株式会社はてな

拡張機能アイコン・ウェブストア画像

Canva で作成。

フォントはうずらフォントを使用、理由はなんとなく気に入ったから

ウェブストアの説明や画像はそのうちどうにかする予定…

Hatiina のイケてない点

API Request 数が多い

はてなスター数を取ってくるのに API を連打してしまっている。これは解決手段が分かってるので、別途修正予定。

なんかスター数が多い

公式拡張機能のカウント数と見比べると分かるんだけど、なんか表示されるスター数に差がある。

とは言え、全体的に多くカウントされる(= みんな多い)だけなので、(個人的には)大した問題じゃないと思って放置予定。

ブコメ数が多いとダルい

ブコメを閉じるには、一番上の箇所をクリックするしかないんだけど、全200コメント中の100コメントあたりを見ている状態だと、閉じるにはスクロールしまくるしかない。

ウィンドウ幅を狭くした時の見た目

めっちゃ狭くするとユーザーアイコンの表示が変になったり、特定の幅で Qiita 本体のレイアウトと適合しない配置になったりする。

本質的な問題じゃないので放置。

共通化などが中途半端(な気がする)

もう少しコンポーネント分割できそうな気がする。

あと、Vue.js の(把握できていない)便利機能とかで、もっとシンプルに書けそうな気もする。

あとがき

メールとかから飛んでもブコメ見れるから、はてブ民は良ければ使ってくれよな。

Hatiina - Chrome ウェブストア

※拡張機能を公開したのが 2019/10/12 なので、もしかしたら審査リジェクトで見えなくなる期間が出てくるかも。

リポジトリも公開してるから「変な拡張機能怖い」って人とか、「おかしな実装ないか調べたろ!」って人も安心だよ。

17number/chrome-extension-hatiina: Chrome extension for qiita

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

EC2のLaravel6.0環境でVue+Rest APIを連携する AWS/Laravel連載(10)

はじめに

前回の記事でLaravelでREST APIを作りました。

EC2のLaravel6.0環境でRest APIを準備する AWS/Laravel連載(9)

今回はAPI経由で投稿一覧表示部分をVue.js+axiosで実装します。

Vue, axiosを入れる

Laravelのpackage.jsonは最初からVue.jsとaxiosが入っているのでnpm installだけでOKです。

$ npm install

Vue部分を書きます。

resources/js/app.js
...
const app = new Vue({
  el: '#app',
  data: {
    posts: []
  },
  methods: {
    fetchPosts: function(){
      axios.get('/api/posts').then((res)=>{
        this.posts = res.data
      })
    }
  },
  created() {
    this.fetchPosts()
  },
});

上記保存したらビルドが必要です。

$ npm run dev

終わるとpublic/js/app.jsに書き出されます。

viewを修正

resources/views/home.blade.php
@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card">
                <div class="card-header">Dashboard</div>

                <div class="card-body">
                    @if (session('status'))
                        <div class="alert alert-success" role="alert">
                            {{ session('status') }}
                        </div>
                    @endif

                    @{{ posts.total }}
                    <table class="table">
                        <thead>
                            <tr>
                            <th>ID</th>
                            <th>タイトル</th>
                            <th>本文</th>
                            </tr>
                        </thead>
                        <tbody>
                            <tr v-for="post in posts.data" v-bind:key="post.id" v-cloak>
                                <td>@{{ post.id }}</td>
                                <td>@{{ post.title }}</td>
                                <td>@{{ post.content }}</td></td>
                            </tr>
                        </tbody>
                    </table>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

v-forはVue.jsでのfor文です。
app.jsの中で、APIから取ってきた投稿一覧(ページャー付き)をpostsという変数に入れています。
posts.dataの中身(配列)をループしpostという変数に入れ、件数分を表示しています。

連載10.5回という形で、Vueの開発がしやすくなるツールを紹介しています。
ぜひご確認ください。
EC2のLaravel6.0環境 VueとLaravelでのTIPS AWS/Laravel連載(10.5)

この記事の参考:
Laravel × Vue.js × axiosでTODOリストを作るよ! その1

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

SEIYU風のECサイトを作りましょう(4) Vue.js プロジェクト初期化 TypeScript使用

前回までの記事

1.Django REST framework + Vue.js「SEIYU風のECサイトを作りましょう」全6回(予定)
2.SEIYU風のECサイトを作りましょう(1)要求分析とプロジェクト初期化
3.SEIYU風のECサイトを作りましょう(2)Xadminを使って管理画面を一新します
4.SEIYU風のECサイトを作りましょう(3)Django REST frameworkで爆速APIを作りましょう

前書き

vue+typescript
このシリーズの記事を書き出した頃、フロントはjavascript使用してましたが、途中から著者がTypeScriptを習い、TypeScriptでフロント書き直すことにしました。
また、本記事はVue.js+TypeScriptの使い方を0から教えていきますので、前回までの記事を読んで無くとも楽しめると思います。
ぜひ最後までお付き合いください:relaxed:

プロジェクト初期化

Vue Cliをインストール

npm install -g @vue/cli-service-global
# or
yarn global add @vue/cli-service-global

インストール終了後、下記のコマンドでバージョン確認できたらオーケーです。

vue -V 
# or
vue --version

プロジェクトを生成する

管理しやすいため、バックエンドapiフォルダと同じディレクトリで以下のコマンドを実行してください。

vue create supermarket

Enterキー押したらオプション選択用の画面が表示されるので、本記事と同じオプションを選んでください。
キャプチャ.PNG
選択終了後インストールがスタートされます。
終了後、以下のコマンドを実行してください。

 $ cd supermarket
 $ yarn serve

その後、表示されるurlにアクセスして、正常に以下の画面が確認できたら、オーケーです。
キャプチャ.PNG

下準備

現在のプロジェクトのディレクトリは下記の通りです。

|- node_modules
|- public
|- src
|- .gitignore
|- babel.config.js
|- package.json
|- README.md
|- tsconfig.json
|- yarn.lock

まずはログインページを作ります。
モバイル画面として見せるものであるため、いくつかの修正をします。
画像

fastclickをインストール

TypeScriptを使用してる時にライブラリのインストールは基本ペアになります。

npm install @types/fastclick --save
npm install --save fastclick

fastclickライブラリの用途は、モバイルのブラウザクリック後の300msの遅延を無くすことです。

インストール終了後、main.tsfastclick を使用します。

supermarket\src\main.ts
...
import fastClick from 'fastclick'
...
(fastClick as any).attach(document.body);
...

reset.cssを使用する

今回はWeb画面をスマホアプリとして見せるため、pc用のデフォルトスタイルをリセットします。
supermarket/publicの配下にcssフォルダを作って、cssの中でreset.cssを作る、そして以下の内容を貼り付けてください。

reset.css
@charset "utf-8";html{background-color:#fff;color:#000;font-size:12px}
body,ul,ol,dl,dd,h1,h2,h3,h4,h5,h6,figure,form,fieldset,legend,input,textarea,button,p,blockquote,th,td,pre,xmp{margin:0;padding:0}
body,input,textarea,button,select,pre,xmp,tt,code,kbd,samp{line-height:1.5;font-family:tahoma,arial,"Hiragino Sans GB",simsun,sans-serif}
h1,h2,h3,h4,h5,h6,small,big,input,textarea,button,select{font-size:100%}
h1,h2,h3,h4,h5,h6,b,strong{font-weight:normal}
address,cite,dfn,em,i,optgroup,var{font-style:normal}
table{border-collapse:collapse;border-spacing:0;text-align:left}
caption,th{text-align:inherit}
ul,ol,menu{list-style:none}
fieldset,img{border:0}
img,object,input,textarea,button,select{vertical-align:middle}
article,aside,footer,header,section,nav,figure,figcaption,hgroup,details,menu{display:block}
audio,canvas,video{display:inline-block;*display:inline;*zoom:1}
blockquote:before,blockquote:after,q:before,q:after{content:"\0020"}
textarea{overflow:auto;resize:vertical}
input,textarea,button,select,a{outline:0 none;border: none;}
button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}
mark{background-color:transparent}
a,ins,s,u,del{text-decoration:none}
sup,sub{vertical-align:baseline}
html {overflow-x: hidden;height: 100%;font-size: 50px;-webkit-tap-highlight-color: transparent;}
body {font-family: Arial, "Microsoft Yahei", "Helvetica Neue", Helvetica, sans-serif;color: #333;font-size: .28em;line-height: 1;-webkit-text-size-adjust: none;}
hr {height: .02rem;margin: .1rem 0;border: medium none;border-top: .02rem solid #cacaca;}
a {color: #25a4bb;text-decoration: none;}

次にindex.htmlにリンク貼ります。

supermarket\public\index.html
...
<head>
...
<link rel="stylesheet" href="css/reset.css"/>
...
</head>

最後に、ユーザーが画面の大きさを調整不可にするオプションを metaに追加したら、下準備は出来ました。

index.html
...
<meta name="viewport" content="width=device-width,initial-scale=1.0,
    minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
...

ログインページを作る

プロジェクトデフォルトを改造します

srcフォルダ配下にあるviewscomponentsフォルダを削除し、pagesフォルダを作ります。
そして pagesの配下に loginhomeフォルダを作ります。

loginフォルダの配下にcomponentsフォルダとLogin.vueファイルを作ります。
homeフォルダの配下にcomponentsフォルダとHome.vueファイルを作ります。
完成後、ディレクトリは以下のようになります。

|- src
|-  |-  pages
|-  |-   |-  home
|-  |-   |-   |-  components
|-  |-   |-   |-  Home.vue
|-  |-   |-  login
|-  |-   |-   |-  components
|-  |-   |-   |-  Login.vue

login配下にあるcomponentsフォルダにContent.vueHeader.vueファイルを作ります。
以下の内容を書いてください。
scriptのlangはtsに指定すること忘れないで下さい。

src/pages/login/components/Content.vue
<template>
    <div class="login-content">
        login-content
    </div>
</template>

<script lang="ts">
    import {Component, Vue} from "vue-property-decorator";
    @Component({
        components:{}
    })
    export default class Content extends Vue{

    }
</script>

<style scoped>

</style>
src/pages/login/components/Header.vue
<template>
 <div class="login-header">
     login-header
 </div>
</template>

<script>
    import {Component, Vue} from "vue-property-decorator";
    @Component({
        components:{}
    })
    export default class Header extends Vue{

    }
</script>

<style scoped>

</style>

Login.vueの内容は以下のようになります。

src/pages/login/Login.vue
<template>
    <div class="login">
        <login-header></login-header>
        <login-content></login-content>
    </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import LoginHeader from "./components/Header.vue";
import LoginContent from "./components/Content.vue";
@Component({
    components:{
        LoginHeader,
        LoginContent
    }
})
export default class Login extends Vue{

}
</script>

<style scoped>

</style>

Home.vueは構造だけ書いておきます。

src/pages/home/Home.vue
<template>
    <div class="home">
        home
    </div>
</template>

<script lang="ts">
    import { Component, Vue } from 'vue-property-decorator';
    @Component({
        components:{}
    })
    export default class Login extends Vue{

    }
</script>

<style scoped>

</style>

router.tsファイルを以下のように修正します。

src/router.ts
import Vue from 'vue'
import Router from 'vue-router'
import Home from './pages/home/Home.vue'

Vue.use(Router)

export default new Router({
  mode: 'history',
  base: process.env.BASE_URL,
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home
    },
    {
      path: '/login',
      name: 'login',
      component: () => import('./pages/login/Login.vue')
    }
  ]
})

サーバーを立ち上げて動作を見てみましょう。

npm run serve

エラーなく以下の通り表示できたらオーケーです。
キャプチャ.PNG
キャプチャ.PNG

ログインページの詳細を書いていきます

先ずはContent.vueからいきます。
デザインに関しては得意ではないため、こうした方がいいでしょう、と思う所があれば、全然変えて大丈夫です。

src/pages/login/components/Content.vue
<template>
    <div class="login-content">
        <div class="logo">
            <img class="logoimg"
                 src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkGBw8QDQ8NDQ0PDw4NDxAPDQ0ODxAVDQ8QFREXFhYRFRUYHSghGRomGxUWITEhJTUrLi4uFyAzODMsNygtLisBCgoKDg0OGxAQGy8lICAtLS0tLS8vLS0rLS0tLS0tLS0tLSstLS0vLS0tLS0tLS0rLS0tLS0tKy8tLS0tLS0tL//AABEIAOEA4QMBEQACEQEDEQH/xAAbAAEBAAIDAQAAAAAAAAAAAAAAAQUGAgMEB//EAEMQAAICAgAEBAMFBAcECwAAAAECAAMEEQUGEiETMVFhB0FxIjJSgZEUQrHRFVNicqGisyQzNcEjJTZUkpOywuHw8f/EABsBAQACAwEBAAAAAAAAAAAAAAABAgMEBQYH/8QAMREBAAIBAwMDAQYGAwEAAAAAAAECAwQRIRIxQQUiURMUMmFxgeFCkaGx0fAjwfEG/9oADAMBAAIRAxEAPwD7jAQJAsBAQJAsBAQJAsBAkCwEBAkCwEBAQEBAQJAsBAkCwEBAQECQLAQEBAQECQLAQECQLAQECQLAQEBAQECQEBAsBAQJAsBAQEBAQEBAQECQLAQEBAQEBAQEBAkCwEBAQECQLAQECQLAQEBAQECQLAQEBAQECQLAQEBAQEBAQECQLAQECQLAQJAsBAQJAsBAQJAsBAkCwEBAkCwEBAkCwJAsBAQJAsBAkCwEBAQEBAQJAsBAkCwEBAkCwEBAQEBAQEBAkCwECQLAQECQLAQEBAQEBAQEBAkCwEBAQEBAQEBAkCwECQLAQECQLA6aclWd0HnWdH9Jr4tVTLkvjr3qnZhObOZFwvAQaNltilgf3agw62/x0JkvfpaGs1kYOmPMz/Ty2BWBAIPYjYPtMje3Yzg3Hact8hKD1DGsFbP+6x1slfbexv2kRO7FjzVyTMV8OjmbmOvDr+T3uP8Ao6v/AHN6CZsWKbz+DW1uuppq/Np7QyuJYXqrc+borHXlsjcxzG0tvHbqrEz5d0hcgIEgWAgSAgWAgICAgICAgSBYCAgSBg+b+MtiY62Jou1qAA/NQdsP0GvzmXDj67bND1DVTp8cWjvvH7shicSS7FGTWdqyFvcEDup9we019TacNLWnxEy2sGWuakXr2lg+GZBW9WJ++dN77nivTc98eqi0z97if1/dtWjhpHxGsZuJWKd/ZWtUHt07/iTPWZp9zyXqczOomPybDz9xuzE4Xj46MVvya1R239pa1QdZHuTofmZs9oh1tVlnHgrXzMMN8HMgq+aD90VI5+oJk07sHp1tur8mKzsp8rKZ22z3WaQemzpVH+E61axSv5PPZcltRlmZ72nj/p9bvzVx6kr+86oo6d+g1szzPqHqePS8d7T4/wAvc4se1Yj4dHC8uy64ljpEG+lfu7PYTnem6zPrNRM3natY32jtz2ZLREQzM9GokCwEBAkCwECQECwEBAQEBAQECQPn55ufE4rk0XktitaAPWklR9oe3qJrfV6bzE9nE+3zh1NqX+7v/J6PieCacZx3TrbuPLuo1/Azp6WeZU9ciZx0mO27h8N7WejKxz90aK+gLAg/wEx+o4vqUmvzEwehZJ6bV+NpZDCpY3qmu4bv7a858+0eG9tVXH5iefw2ekmeHu45wXDa+viGT2/Zx32fsPo7XY+ZB+Xz3Pc/Si9olzNRp8PXGbJ/D/sNK+M9DdWJcPuFXTf9rYP8P4RZq+pxM9NvDNfDPlx8fCssvXpszB90/eWrWgD7nZP6Sa8cs+i081xz1fxf2e7l3k+vFsORa4tsXZrAB6EHr7nU2M2oma/gw6L0quC/Xad58fg6WZrbCQCzOd6HnPmtrZNVlmY5mzu9mZFteFR1Wn7bn7q+bH8I+k9Zo8FNBg9/3p7/AOP0a+TJEcywOVzTkOdVKtYJ0vbqY/rMeT1DJbisbNac1p7Nt4dU61ILXL2a27H8R+U6+GtopHVO8tisTEcvTMixAkCwECQLAQJAsBAQEBA4V2q2+llbR0ekg6Pp2gcoAkAbJ0PUwPmPxJ4BYLznVIWqtC+L0jfQ4Gtn2IA7zVzU56oee9U0lov9WscT3bNh41eRwfFoym6TdXWqN+8H19g/oJnxZvpxWZ88OhXDXPpK0yeYh7OU+A/sVTqzBnsfbMPLpHZR/wDfWbGXJ1ytoNF9mpMTO8zP/jIjIoGT4IZf2l6/EKj73hggbP5masYccXnJEe6e8t36kdXRvy+dc98We/LOMpIrobpCj96z5k+voJ1MFIrXq+XlvVdTbLm+nHav9288WzsbHxq7M0KQnQUVlDMbAvbpHr595z72ivMvRZcuPFji2T/Zaxw/nLIzs+nGxqxTSzdVjH7VprUdR7+Q3rX5zBGWbWiIc7F6hk1GaKUjaP6tj5m5oxcBAb2LWOD4dCd7H9/Ye5meZjy6efU0wx7ml8F5zy87OqxcSmrGqZuqxlXqsFa9zsnt5dvL5zFjx0pxSsR+UNHHrcmbJFa8QzfMWNkXZTAU2MqaWshTrXzO/rOXrKZcmWdqztHZt5Ita3Z6uD8FXHIyMx0Qr3RGYaU+pPzM2NHoLVnrv3+Cta4/dedmfwOKUXlxRaLPD11lQSo35Dq8jOpalq92TFqMeWZik77PZKsyQLAQJAsBAQEBAkCwEDDc4VXvw3KTFJFzVN0dP3vcD31uJTXvy+M8icbtw8+kFnFdtgpvr76YMdb16g6P6ym+zPau8PtVvG6wPsqxPvoCVnLDHGOWJy86y37x0vyUeX/zMVrTLJFYhkOD5/bwbO4PZCfL+6Zkx38Spkp5YXm/LIyErXsKVVlA8gx9voBOX6hln6kVjw5+aedo8MvxzmFcXhv7YwBdq08JD+9Yw7D6fP8AKdbHfqpFvmF82eMeLrfOfhvxK2/jZuucvZdVb1sfoCB7DtJju5miyWvn6red27VcnA8Rsy7WBq8Txa6x5lvP7XsDNyc/s6YXr6XE6mctp433iGic78VfIzrQSfDpY1Vr8gF7E/mdzk5Lb2cv1DPOTNPxHENu+HnAGxqrM7IXpd0PhoR9pa9bLH0J9PaZsNNuZdT0zSzirOW8cz2/J8x4nk35+bZYFe2y1z0IoLELvSqAPkBL92jktbNkme+76x8P+VP2GprsgL+03AAgeVSfg36+stxHd2dHpfpR1W7yyvNnHRh0dSgG63a0qfLYHdj7DY/UTPhx9c/gr6hrI02PeO89nyrIyb8q0F2e62w6UdyST8gPlOhEVpHDyV8mXPf3TMzLbsvmCrg2PXh11i7MceJcN6RGb8R+nkPac3Nl6rbvSY710WKMcRvbvP5sTw3n7imTkLRj00O7nsgRtKN92Zt9h7zDvJTW5736axD6niCwVr4xU2aHWUBCdXsD8pd1677c93dCUgWAgICAgSBYCAgIGtce5bxS4zFx08dG6i4Gif7R95jyRxwyUt4YzJZhW7IOplViq+rAdhMDM0zk3jWbflsl5L19LFwVA8Nh5AenpqZLViIG8TGh5s/Fsyb6+nuzAIx+n7x/KaWp09suSJr54aWowTNt4eH4v1FMTCrXfQljL7bFYA/5zqRXprFY8Of6lG1Kw8/wh4I/XZnupCdJqpJ/eJI6iPbtqWrCnpuGd5yT+j6O3EaReuN4gNzgsKx3YKPMn0/OT1Rvs6c5adfRvywfEcfheAxybKU8axiyjRaxmJ2SAfL6y2PB1TxDRz/ZNL/yWjmf1l2cs8wWZ1lx8JUx6wFAPd2Y+p8vL5TLlxRSI+TQ622qtadtqx/N7MzIqxvs01VrYfPpUAAe+pwvUvU40vspzb+kOnTHHiHiwHsvvXrYlV+0R+728u31nF0V82t1VfqTvEcz8cfuyTtEO3mTllM163e56/DUqFUAg7Oye89vjzTSOIcrWen11Nom1pjZ28C5ZxsQ9Val7da8Wwgt+WuwkZM1r91tL6fh0/NeZ+ZYzmTlvhtlpuvpY3Wd28Ox1LfLZ0dTla31DDpeLczPiGa+ixZbdUw58FfExAUx8QVKfvMp27fUnuZzKf8A0FN/dSdvzZ8empjjakNmouDqGAIB/ENGd7BmjNSLxExE/MbLTGznMqFgICBIFgSBYCAgSBYEZQQQRsEaIgavxDCNTa81P3W/5fWa1q7S2K23eTWvkJVYkADruOxHkYGQsNGXWuPnIHVXV1J8iw8t/rM9b78S1s2nrkjaY3hx5z4t+w4IGOFRrCKqekAKg1skD2EZb9NeHP1+o+z4fb3niGt/DLFfxMniFvUVVCosbZLsftOdnz0AP1mPBWZndz/SqTvbNb47/wB2ucTzbMvJe1gzNY2kQbJC/uqB9J3q1ildnEz5b6jLNp7z/uz6lyjwc4mIqN/vLD4lvsxHZfyAnPzZOu271np2l+z4YrPeeZeO3Dustc9Dd2Pdhoa37zwmXRarUZ7T0zzM8zxDp7xEMNzlzH/Rla42N0tmXr1vYfKpN6B169jr6bnpdBoq6TH097T3lzddrJx+2veXzC/Pycizqtvutdj2BdyST8gP5TccWcl7zvMy+l8h8sPjqM7iFr1kDdVDuwVP7Tjfn6CTMxWOq07OrpNNNPfkn9GfzuO4LnTB2I7darr/ABnK1VtFnn3xvPy3ftFY7PTwu/Bcjw2Xr+S2HTfkDGk0uhrbekc/j+68Zot5ZydlYgICBIFgICAgIEgWAgIHXdSrqVYbBkTG6YnZr2fwx69sv2k9R5j6zBakwzVvEvBMa6gSQMDpzcau9a0yA1lVbdQr6iO+teY7x37tXU6PHqIiL+GzYNuM1P7PWorToKeF5aUjRAmxS8eETgitOiI47MZfh4fDa/FpoBub7KFiWb3Oz5D6Sus1s46bz+kNLHpMGm91K8sKnE83JtFaWsC57Kn2VUep18pxI1GozX6YnunrvadobkLK8TG6r7dLWNvbYxJJ+f8A+TuUr9Om0y2bWripvaeIaNxzm7huQdPw45IXsLLAqtr2PnqUnNHw5Gb1LBedujdy5d5h4VXYqU8Nem1yAprRbG39d9X6RXNX4Tp9dp4ttWkxM/r+7Z+aKKrKl8TJWgj7SixtKfqsrqdJbUV2r4/k6Oe9Ij3W2/Nq2Lwd7Tqm7Hs91uH8PP8AwnMt6ZqK94hr45rk+5aJ/VsHDeUkXTZLBz+BNhPzPmZs4fTaxzknf+zZrgiO7ZUUAAAaAGgPQTpRG3ENhykhAkCwEBAQECQLAQEBAQEDwZXCq37gdDeq+X6Sk44leLzDD53DnqHUSCu9bHn+kw2pMMlbRLxhSfIE/SVXTUCwhwvQWdPibYJ90EnQ9pjyYq5NurwpfFS/eGS4bn1UjpXHVd+ZTzP13MuLoxxtWNlIwxX7rwc24aZ6VhchqTUSRW6E1MT8zrvuWvMX8tDXaC+oiNp22/k1rH5JsZtPmY6rvzHUW17A6mOMf4uZHo2bfmYbry7wTCwlPh2K9zD7V7lev6D0HtM9K0q6um0NMEe2OflqnGOVsq3IZxk05BsbszWBW9hr+U6NNVjiNnF1PpGqvkm2++/nsyfAuQuhhZl2bKnYqqJ0fq3n+ki+p34qy6X0XpmLZZ/SP8t5RQAAOwA0B7TUd6I2jaHKEkCQLAQEBAQJAsBAQEBAQEBAxvHyPB9+sfX5zHl7L4+7jwGjpqLkd3Pb6CMccbpyTyyDUqfNVP1Al9oU3l1nCqPnUn/hEjpj4OqXA8Np/ql/LcdFfhPXLieFUf1f+Zv5yPp1Ouyf0TT+D/M0fTqnrk/omj8H+Zv5x9Op12UcLo/qx+Zb+cfTqjrlhcGn/aVT8Ln/AAmKse7ZltPtbPNhgICAgICAgSBYEgWAgIEgWAgSBYCB5b8Cp263XZ+p1KzSJ5laLTD0qoAAHYDsB6SyqwEBAkCwEDhZvR6ddWu2/LcSPHg4BR2tdgztvyHYbPeUrXad5XtbeNnvl1CBIFgICAgICAgICBIFgebiOdVj0vfe4SqsbdzvQG9fL6wRG5w/OqyKUvocPVYCUcb0QCR8/cGEzGz0wggIEgWAgIEgWAgSBh8HmXGuzruH1l/HxwTYChCdteTfPzEbpms7bszCCBCYFgICAgICAgICBIFgIGtfEb/g+Z/cX/UWRK1PvHw4P/U2H/cf/VeIL93k5o5utqza+GcPxxkZlihm6zqqsEEjZ9dDf01G6a143lx5a5uvszX4bxLHWjLUdSGs7rca3/DuI3JrxvDy5XOObfm3YvCsNL1xf989j9PVo6IB3od9geuo3T0xEby7uU+eGyznPkUrRVhL1nuS4HfqDe46YiUWpsxg544ndTbn4nD6zgUltmxz4zKvmdA/L56jdPREcTLLcT54A4MvFcWsMWsStqrCfsMW6WB16RuiKe7aWIzfiDnUrRl3cPCYGQR0N1btYa2T59iRsgH0jdMUieHbbz3nUZOOczAWnCzGApPVu4KSNMdHW+4Oo3OiJjhs3MuZxJGReHYtNoZS1lt1nSqEHsNfOFYiPLwcn8125V2Rh5lKVZWMNt4bbrYA6OjvzHb9YiU2rtzDCcO5+zsm04+NgLZamR02suzWmP1AFj3+95/pG6ZpEPDwziNeLzDxjJuOq6a7GPqddGlHufKR5TMb1htPAeab7MC7iedSlGMoLUqvUbXUHW+/qew9ZO6s152hibOdOJJjrxK3AqXh7MOwsJvCFtB43T0x2Yj4scVyHXENW1wrlWymxbCGuZl2UdQfIDX6yJTjiH0jl/Iy7KS2fQlF3WwFdbhl6O2jvf1lmOdvDJwggICAgSBYEgWAgSBrXxJ/4Nmf3F/1FkStT7zVuTOf+H4vDcbFve0W1KwcLUxXZsY+f0IiJXtSZl18QylweZV4hkBhiZlA6LypKr1Vgd9fMFfL0aR5I5rs7cK9eI8xpl4gLY2KgD3hSFJCsPn6lv8ACT5J4rtLxcqcZr4Rn8QxuILYni2ddThGYuOpiANeewwIP1kdi0dURs8nJuM+XVx+utStmSjlEP3gzO7dB9/lEJtO2z28uc142NwO3Bv2mXWuRUuOyN1WFy2vl/a779I3RNZm27xZfDrKOUz46lGuy0uCEdwpKqux8thd/nHhO+92S+IP/Z3hv0xv9CJ7Ip96Xd8VV3j8J1/Wr/6FkyY/LjzxmhuMUYvEL7KOGioOShZVdulu5I/tAD2/PciSscbx3eb4emj+ncwYgYY3gN4HV1bKbTTfa76Pn39YjuX+7y9/wc+9xI/P9oHf83kwjJ4a5xDgDZ/G+LUI5V0FliKPKx1C9KH23IXidqwzWPxGziPAMnB6Sc3DVA1QGnsrrsBB166XR948K7bW3YThNPBLcRBm8SzqbgoW3FLnoDD5KvhkajhaerfiGY+KuAtPDuGrT1vRjt0K7d26fDHSWIHz1Eq455l9H4LxijMoGRiuXrJK7KsD1DzGjLMcxt3e+EEBAQEBAQEBAkCwOFtSupV1VlPmrAFT9QYHm/onG/7rR/5SfyhO8u7Ixa7E8OytHT8DqCv6GEGNi11L01VpWv4UUKP0EDUOO5nFxk2CnhWNkVhv9lvdh1qOkdzv33IXiK7d3d8POWbcKq63KYNlZdniWhTsKO51v5nZJ/OIgvbfs2SzhmO1nivj0tYPKxq0L/rqSru776EdeixFdfwuoK9vYwhxtxKmUI9SMi66UZFKjXYaB8oC7FrcKLK0cJ90Mqnp+m/KBxy8Gm3QuprsC918RFbX03Bu5V4lSt1rUivrp61RQ3T6b9OwgKMauvfh1onUdt0KB1H315wCY1aubFrQWN95woDn6nzMBViVKxdKq1dt9Tqihjs7OyIHQ/CMVn8RsWgv+M1IW+u9QneXovx63Q12Vq6HsUZQV/QwhMXFrqTw6a0rQdwiKFXfroQO6AgSBYCAgICAgICAgSBYCAgSBYCBIFgICBIFgICAgICBIFgICBIFgICAgIEgWAgIEgWAgSBYCAgICAgIEgWAgSBYCAgSBYEgWAgIEgWAgIEgWAgSBYCAgICAgICAgIEgWAgIEgWAgICAgICAgIEgWAgSBYCAgSBYCAgICAgSBYCAgSBYCAgICBIFgICAgICBIFgICBIFgICAgICAgICAgSBYCAgSBYEgWAgICAgICAgICBIFgICAgICAgICAgSBYCAgSBYCAgICBIFgIAwEBAQIYAQLAQJAsBAQJAsBAQAgICBIFgIH/2Q==" alt="">
        </div>
        <div class="itemContent">
            <div class="login">
                <div class="input-container">
                    <input type="text" class="username" v-model="userName" placeholder="ユーザーネーム/アドレス">
                </div>
                <div class="input-container">
                    <input type="password" class="username" v-model="passWord" placeholder="パスワード">
                    <div class="agin">パスワード再発行</div>
                </div>
            </div>
            <div class="loginbtn" @click="loginfuc">ログイン</div>
            <div class="registration">
                <div class="registerbtn">新規登録</div>
            </div>
            <div class="logintype">
                <div class="quick-login">
                    <h4 class="txt-otherLogin">それ以外のログイン</h4>
                    <a href="" class="icon">GitHub</a>
                    <a href="" class="icon">Qiita</a>
                </div>
                <div class="agreement-tips">
                    <p>https://github.com/huanshenyi/qiita-Django-supermarket</p>
                </div>
            </div>
        </div>
    </div>
</template>

<script lang="ts">
    import {Component, Vue, Provide} from "vue-property-decorator";
    @Component({
        components:{}
    })
    export default class Content extends Vue{
     @Provide() userName:string = "";
     @Provide() passWord:string = "" ;
     loginfuc () {
        console.log("ログインが推された");
        (this as any).$router.push("/")
     }
    }
</script>

<style scoped lang="stylus">
    .login-content
        padding: 0 .25rem .25rem;
        .logo
            width 100%
            height 20%
            text-align center
            .logoimg
                width 2rem
                height 2rem
        .itemContent
            max-width: 7.5rem;
            margin: 0 auto;
            .input-container
                height: 100%;
                padding-top: .2rem;
                padding-bottom: .2rem;
                position: relative;
                overflow: hidden;
                margin-top: .4rem;
                background: #fff;
                border-bottom: .01rem solid #efefef;
                -webkit-box-sizing: border-box;
                -moz-box-sizing: border-box;
                box-sizing: border-box;
                .agin
                    border-left: 1px solid #ccc;
                    float right
                    font-size: .2rem;
                    padding-left: .16rem;
                .username
                    float left
                    padding: 0 .3rem 0 0;
                    font-size: .3rem;
                    line-height: normal;
                    border-radius: 0;
                    border: 0;
            .loginbtn
                margin-top .8rem
                width: 100%;
                height: .8rem;
                line-height: .8rem;
                display: block;
                background-color: #efefef;
                border-radius: .25rem;
                font-size: .36rem;
                color: #fff;
                background-image: linear-gradient(90deg,#fab3b3,#ffbcb3 73%,#ffcaba);
                box-shadow: 0 0.1rem 0.2rem 0 rgba(255,62,62,.2);
                text-align: center;
                font-family: PingFangSC-Semibold;
            .registration
                margin-top: .2rem;
                text-align: center;
                .registerbtn
                    width: 100%;
                    height: .8rem;
                    line-height: .8rem;
                    display: block;
                    border-radius: .25rem;
                    box-sizing: border-box;
                    border: 1px solid #ff2000;
                    color: #f10000;
                    background: #fff;
                    margin-top: .1rem;
            .logintype
                margin-top: .88rem;
                .quick-login
                    height: .8rem;
                    position: relative;
                    padding: .5rem 0 0;
                    border-top: 1px solid #efefef;
                    text-align: center;
                    .txt-otherLogin
                        font-size: .3rem;
                        font-weight: 400;
                        position: absolute;
                        top: -.14rem;
                        left: 50%;
                        background-color: #fff;
                        padding: .05rem .15rem;
                        color: #ccc;
                        -webkit-transform: translateX(-50%);
                        transform: translateX(-50%);
                        .icon
                            position: relative;
                            width: .48rem;
                            height: .48rem;
                            color: #616161;
                            margin: 0 .15rem;
                            font-size: .14rem;
            .agreement-tips
                margin-top: .12rem;
                text-align: center;
                font-size: .3rem;
                color: #bebebe;
</style>

次は Header.vueを書いていきます。

src/pages/login/components/Header.vue
<template>
 <div class="login-header">
     <div class="headerLeft">
         <router-link to="/">
         <
         </router-link>
     </div>
     <div class="headerTitle">
         <div class="title">ログイン</div>
     </div>
 </div>
</template>

<script>
    import {Component, Vue} from "vue-property-decorator";
    @Component({
        components:{}
    })
    export default class Header extends Vue{

    }
</script>

<style scoped lang="stylus">
    .login-header
        display flex
        height .86rem
        line-height .86rem
        border-bottom: 1px solid #e5e5e5;
        width: 100%;
        position: relative
        .headerLeft
            margin-left .3rem
            float left
        .headerTitle
            height .64rem
            font-size .4rem
            padding-left 2.5rem
</style>

修正後、画面の変化を見てみましょう。
下記の画像の通りに表示できたらオーケーです。
キャプチャ.PNG

次回予告

最後まで読んでいただいてありがとうございます。

次回ではJWTを使って実際ログイン機能を完成しようと思います。

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

なぜ state を直接変更すると再描画されないのか

意外と state の挙動の理由について知らない人が多いと思ったので、勉強ついでに少しまとめてみようと思います。

state の挙動について

state には以下の特徴があります。

  • state は Object.assign()スプレッド演算子(...Object) として 値を渡さないとうまく変更が反映されない
  • state を直接変更すると再描画が起こらない
  • state を他の変数に入れて、その変数を変更して state に再代入しても再描画が起こらない(または挙動がおかしくなる)

これらの挙動はフレームワーク固有の特徴ではなく Javascript 自体の特徴です!

Javascript の Object の挙動について

まず state の挙動を理解する上で一番理解しなければいけないのは Javascript 自体の挙動についてです。 state は基本的に Object で管理されています。(React の Hooks でも Object で管理する場合は同じです)

この Object がかなり重要です。Objectの特徴は以下の通りです.

  1. Object 同士で比較する場合、メモリを比較するため Object の中身を比較しているわけではない
  2. 値は参照渡しで代入される

これらが Object の特徴であり、理解しなければいけない点です。

まず1の Object 同士で比較する場合、メモリを比較するため Object の中身を比較しているわけではない について実際にコードで見ていきます。

example1.js
const a = {key: 'test'};
const b = {key: 'test'};

/* Object を比較する方法は2通りあります。 */
console.log(a === b); // result: false
console.log(Object.is(a, b)); // result: false

このように Object はメモリの比較によって結果が求められるので変数が異なれば、値が同じでも false になります。

次に2の 値は参照渡しで代入される について見ていきます。

example2.js
const a = {key: 'test'};
const b = a;

b.key = 'qiita';

console.log(a === b); // result: true
console.log(Object.is(a, b)); // result: true
console.log(`a: ${a.key}, b: ${b.key}`); // a: qiita, b: qiita

このように参照渡しで値が渡されるため変数a変数bを比較した場合に、結果がtrueになります。

結論

ここまで見てきた通り、Object は参照渡しによって値が代入されて、メモリごとの比較になるため、いちいち新しい Object を生成しなければ Object の変更が検知されないということになります。
つまり、state を直接変更した際には Object の変更が検知されていないために再描画が行われていない ということになります。
また React ではshallow equalという比較が行われているため、直接 state を変更しても再描画が行われることがありますが、挙動としては正しくないため、stateの変更をするときは新しく Object を生成するということを意識して開発しなければ予期せぬバグを産む可能性があります。

間違いや補足があれば指摘していただけたら嬉しいです。

最後まで読んでいただきありがとうございました。

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

Vuexの利用方法メモ

書いてあること

  • Vuexの利用方法を試した際のメモ

参考

Vuex

環境

  • CentOS Linux release 7.6.1810 (Core)
  • Node.js v10.16.0
  • Npm 6.10.0
  • Vue 2.6.10
  • Vuex 3.0.1

レポジトリ

vue-vuex-project

用語

  • ステート(State):アプリケーション全体の状態を保持するオブジェクト
  • ゲッター(Getters):ステートから別の値を算出する
  • ミューテーション(Mutations):ステートの値を更新する
  • アクション(Actions):非同期処理や外部APIと通信を行い、ミューテーションによりステートを更新する

事前準備

Vue CLIでVuexインストール済の場合は事前準備作業は不要。

インストール

bash
npm install -y vuex

Vuex利用設定

store.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

// ストアの定義
const store = new Vuex.Store({
  // ステート、ミューテーションなどをここに記載
})

// ストアをエクスポート
export default store
main.js
import Vue from 'vue'
import Default from './layouts/default.vue'
import router from './router'
+import store from './store'
import vuetify from './plugins/vuetify'

Vue.config.productionTip = false

Vue.prototype.$axios = axios

new Vue({
  router,
+  store,      // コンポーネントからストアを利用可能にする
  vuetify,
  render: h => h(Default),
}).$mount('#app')

ステート

定義

src/store/index.js
const store = new Vuex.Store({
  state: {
    ●●●: 内容,
  },
}

利用

算出プロパティでステートを自身のthis.●●●に紐づけて利用する。

<script>
export default {
  computed: {
    ●●●() {
      return this.$store.state.●●●
  }
}
</script>

ゲッター

定義

src/store/index.js
const store = new Vuex.Store({
  getters: {
    ●●●(state) {
      return 処理内容
    },
  },
}

利用

算出プロパティでステートを自身のthis.●●●に紐づけて利用する。

<script>
export default {
  computed: {
    ●●●() {
      return this.$store.getters.●●●
  }
}
</script>

ミューテーション

定義

src/store/index.js
const store = new Vuex.Store({
  mutations: {
    ●●●(state, payload) {
      ステートの更新処理など
    },
  },
}

利用

メソッドからミューテーションを利用する。

<script>
export default {
  methods: {
    ●●●() {
      this.$store.commit('●●●', payload)
    },
  },
}
</script>

アクション

定義

src/store/index.js
const store = new Vuex.Store({
  actions: {
    ●●●(state, payload) {
      非同期処理や外部APIと通信など
    },
  },
}

利用

メソッドからアクションを利用する。

<script>
export default {
  methods: {
    ●●●() {
      this.$store.dispatch('●●●', payload)
    },
  },
}
</script>

モジュール

名前空間を有効化(モジュール分割)

namespaced: true

利用方法の違い

ルートストア、モジュール分割なし(namespaced: false)、モジュール分割あり(namespaced: true)で下記の通り利用方法が異なる

種類 ルートストア namespaced: false namespaced: true
ステート this.$store.state.●●● this.$store.state.■■■.●●● this.$store.state.■■■.●●●
ゲッター this.$store.getters.●●● this.$store.getters.●●● this.$store.getters[■■■/●●●]
ミューテーション this.$store.commit('●●●', payload) this.$store.commit('●●●', payload) this.$store.commit('■■■/●●●', payload)
アクション this.$store.dispatch('●●●', payload) this.$store.dispatch('●●●', payload) this.$store.dispatch('■■■/●●●', payload)

ルートストアの利用

モジュール分割したストアからルートストアを利用することが可能。

ステート

<script>
export default {
  getters: {
    ●●●(state, getters, rootState, rootGetters) {
      rootState.●●●を利用
    },
  },
}
</script>

ゲッター

<script>
export default {
  getters: {
    ●●●(state, getters, rootState, rootGetters) {
      rootGetters.●●●を利用
    },
  },
}
</script>

ミューテーション

<script>
export default {
  methods: {
    ●●●({ commit }, payload) {
      commit('●●●', payload, { root: true })
    },
  },
}
</script>

アクション

<script>
export default {
  methods: {
    ●●●({ dispatch }, payload) {
      dispatch('●●●', payload, { root: true })
    },
  },
}
</script>

ヘルパー関数

ヘルパー関数によりストアの各要素をコンポーネントの算出プロパティ・メソッドに紐付けることができる

ステート

import { mapState, createNamespacedHelpers } from 'vuex'

const ■■■Helpers = createNamespacedHelpers('■■■')

export default {
  computed: {
    ...mapState(['●●●']),
    ...mapState({ ●●●: '●●●' }),
    ...mapState('■■■', ['●●●']),
    ...mapState('■■■', { ●●●: '●●●' }),
    ...■■■Helpers.mapState(['●●●']),
    ...■■■Helpers.mapState({ ●●●: '●●●' }),
  },
}

ゲッター

import {mapGetters, createNamespacedHelpers } from 'vuex'

const ■■■Helpers = createNamespacedHelpers('■■■')

export default {
  computed: {
    ...mapGetters(['●●●']),
    ...mapGetters({ ●●●: '●●●' }),
    ...mapGetters('■■■', ['●●●']),
    ...mapGetters('■■■', { ●●●: '●●●' }),
    ...■■■Helpers.mapGetters(['●●●']),
    ...■■■Helpers.mapGetters({ ●●●: '●●●' }),
  },
}

ミューテーション

import { mapMutations, createNamespacedHelpers } from 'vuex'

const ■■■Helpers = createNamespacedHelpers('■■■')

export default {
  methods: {
    ...mapMutations(['●●●']),
    ...mapMutations({ ●●●: '●●●' }),
    ...mapMutations('■■■', ['●●●']),
    ...mapMutations('■■■', { ●●●: '●●●' }),
    ...■■■Helpers.mapMutations(['●●●']),
    ...■■■Helpers.mapMutations({ ●●●: '●●●' }),
  },
}

アクション

import { mapActions, createNamespacedHelpers } from 'vuex'

const ■■■Helpers = createNamespacedHelpers('■■■')

export default {
  methods: {
    ...mapActions(['●●●']),
    ...mapActions({ ●●●: '●●●' }),
    ...mapActions('■■■', ['●●●']),
    ...mapActions('■■■', { ●●●: '●●●' }),
    ...■■■Helpers.mapActions(['●●●']),
    ...■■■Helpers.mapActions({ ●●●: '●●●' }),
  },
}

VueRouter連携

vue-router-syncをインストール

bash
npm install -y vuex-router-sync --save

RouterとStoreを同期

main.js
import Vue from 'vue'
import Default from './layouts/default.vue'
import router from './router'
import store from './store/index'
+import { sync } from 'vuex-router-sync'
import axios from 'axios'
import vuetify from './plugins/vuetify'

Vue.config.productionTip = false

// RouterとStoreを同期
+sync(store, router)

Vue.prototype.$axios = axios

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

ストアからRouter情報の取得

vue-router-syncによってルートに紐付いた情報を取得する

コンポーネントから利用

export default {
  computed: {
    ●●●() {
      return this.$store.state.route
    },
  },
}

image.png

他のストアから利用

<script>
export default {
  getters: {
    ●●●(state, getters, rootState) {
      return rootState.route
    },
  },
}
</script>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

v-ifの条件式で比較演算子(==)を使いたいメモ

すごい簡単なことだが、
Vue.jsのv-ifの条件式で比較演算子(==)を使用したいと思ったところ、
公式を見てもわかりにくかったのでメモ。

コードを見れば一発

<template>
  <div>
    <button v-if="this.id == 1">新規作成</button>
  </div>
</template>

<script>
export default {
  name: "TestBtn",
  data: function(){
    return {
      id: 1
    }
  },
};
</script>

※コードは一部抜粋

vi-ifの中で条件式を書けばいいだけ。
権限が1なら、新規作成ができるみたいなイメージ。

これだけじゃ、味気ないので、
ボタンをクリックしたら、idを変更して、
新規作成ボタンが非表示になることも確認。

<template>
  <div>
    <button v-if="this.id == 1" v-on:click="changeId">新規作成</button>
  </div>
</template>

<script>
export default {
  name: "TestBtn",
  data: function(){
    return {
      id: 1
    }
  },
  methods: {
    changeRoleId: function() {
      this.id = 2;
    }
  }
};
</script>

リアクティブに動いていることを確認できました〜

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

plunkerでvue その42

概要

plunkerでvueやってみた。
練習問題やってみた。

練習問題

canvasを使え。

成果物

https://embed.plnkr.co/sJ0fpkeUWYC1iossgZyS/

https://embed.plnkr.co/2L2BzPLC8ivlrIIts0ig/

https://embed.plnkr.co/YpMdqlIAqFkdoMljZdla/

以上。

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