20190728のvue.jsに関する記事は19件です。

【Vue/Storybook】SVGがStorybookに表示されない問題を解決する

はじめに

Storybook with Vue上でSVGを表示するのに失敗していたので解決策をメモします。
SVGはvue-svg-loaderを使ってバンドルしています。

結論だけみたい方は以下リンクだけみてください。
解決策です

環境

Vue: 2.6.10
Storybook: 5.1.9
vue-svg-loader: 0.12.0

表示できない

設定アイコンを表示するコンポーネントを用意します。

<template>
  <setting-svg />
</template>

<script lang="ts">
import Vue from "vue";
import SettingSvg from "@/assets/img/svg/setting.svg";

export default Vue.extend({
  name: "SettingIcon",
  components: {
    SettingSvg
  }
});
</script>

Storybookを準備します。

import { storiesOf } from "@storybook/vue";
import SettingIcon from "./SettingIcon.vue";

storiesOf("Icon", module).add("設定アイコン", () => ({
  components: { SettingIcon },
  template: "<setting-icon />"
}));

これでアイコンを表示する用意ができました。
Storybookを起動します。

$ start-storybook

エラーは出ません。うまくいったと思いStorybookを表示します。

スクリーンショット 2019-07-28 23.21.39.png

何も表示されません。辛いです。
Storybookの webpack.config.js を確認します。

const path = require("path");
const rootPath = path.resolve(__dirname, "../src");

module.exports = async ({ config, mode }) => {
  config.resolve.alias["@"] = rootPath;
  config.resolve.alias["~"] = rootPath;

  config.module.rules.push({
    test: /\.s?css$/,
    loaders: ["style-loader", "css-loader", "sass-loader"]
  });

  config.module.rules.push({
    test: /\.svg(\?.*)?$/,
    loaders: ["vue-svg-loader"]
  });

  return config;
};

config.module.rules のSVG部分を出力してみます。

[
  {
    test: /\.(svg|ico|jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2|cur|ani)(\?.*)?$/,
    loader: '/Users/numatakeisuke/working/project/memory-album/node_modules/file-loader/dist/cjs.js',
    query: { name: 'static/media/[name].[hash:8].[ext]' }
  },
  { test: /\.svg(\?.*)?$/, loaders: [ 'vue-svg-loader' ] }
]

SVGにデフォルトで file-loader が当たっていることがわかります。
png などは残しておきたいので、 svg だけローダーの設定を変えます。

解決策

webpack.config.js を修正します。
SVGを file-loader の対象から外しています。

const path = require("path");
const rootPath = path.resolve(__dirname, "../src");

module.exports = async ({ config, mode }) => {
  config.resolve.alias["@"] = rootPath;
  config.resolve.alias["~"] = rootPath;

  config.module.rules = config.module.rules.map(rule => {
    if (rule.test.toString().includes("svg")) {
      const test = rule.test
        .toString()
        .replace("svg|", "")
        .replace(/\//g, "");
      return { ...rule, test: new RegExp(test) };
    } else {
      return rule;
    }
  });

  config.module.rules.push({
    test: /\.s?css$/,
    loaders: ["style-loader", "css-loader", "sass-loader"]
  });

  config.module.rules.push({
    test: /\.svg(\?.*)?$/,
    loaders: ["vue-svg-loader"]
  });

  return config;
};

Storybookを再起動すると無事に表示されました。

参考

https://github.com/storybookjs/storybook/issues/6758

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

NuxtのHTML templateはpugを使わないメリットもある

こんにちは、プログラミングスクールのレビューサイト「スクールレポート」を運営しているアカネヤ(@ToshioAkaneya)です。

NuxtのHTML templateはpugを使わないメリットもある

タイトルの通りです。pugは誰でも綺麗にコードがかけるため素晴らしいのですが、エディターの保管が素のHTMLに比べて弱い
という傾向があります。僕のしようしているIntelliJでもその傾向があります。(ちゃんと設定すれば問題ないのかもしれませんが...)

その点HTMLの補完は正確です。この記事が参考になれば幸いです。

終わりに

Ruby on RailsとVueで作成したプログラミングスクールのレビューサイトを運営しています。良ければご覧ください。https://school-report.com/

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

VirtualDOMのメリット

ReactやVueで採用されている仮想DOM(Virtual DOM)の何が良いの?って話。

そもそもDOMとは?

DOMはXML形式の文書をツリー構造で表現したものです。このDOMを参照することで、各要素にアクセスすることができ、見た目やコンテンツを変更することができます。

とりあえず、「WEBページを表示するのに必要な設計書みたいなもの」と思ってもらえると良いと思います。

仮想DOMの説明の前に、ブラウザのレンダリングの流れについて

仮想DOMのメリットを知るためには、ブラウザのレンダリングの流れを知る必要があります。

ブラウザはページを描画するまでに以下の処理を行っています。

  • Parse HTML/CSSを解析
  • DOM, CSSOM DOMツリーとCSSOMツリーを組み合わせて、レンダーツリー作成
  • Layout 描画する位置、サイズの決定
  • Paint 描画処理
  • Composite 各レイヤーを重ねる

DOM操作を行うと、レンダーツリーの更新が発生し、再度レイアウトとペイント処理が走ります。
これら「レイアウト」と「ペイント」処理が負荷が高い処理と言われています。
そのため、描画速度を上げるためには、一般的に不要なDOM操作を減らすという手段が取られています。

レンダリングの流れについて、より詳しく知りたい方は、以下の記事を参考にしてください。
- ブラウザの仕組み: 最新ウェブブラウザの内部構造
- ブラウザレンダリング入門〜知ることで見える世界〜

仮想DOMとは?

実際のDOMと対となる、構造体。

先程書いたように、DOM操作を行うたびにレンダーツリーの更新が発生し、再度レイアウトとペイント処理が走ります。

この描画時の負荷を抑えてくれる仕組みが、仮想DOMです。

【本題】仮想DOMのメリット

仮想DOMを使うことで、以下のようにDOM操作を最小限に抑えることができます。

  • DOMと対になる仮想DOMを定義し、ページが変化するときには、まず仮想DOMを変更
  • 仮想DOMはブラウザレンダリングとは切り離されているため、変更しても影響はない
  • 仮想DOMの差分をチェックし、変更部分のDOMのみを変更
  • 最小限のDOM操作でページを変化させることができる

image.png

まとめ

仮想DOMとは、DOMと対になる構造体で、ReactやVueは、仮想DOMの差分をチェックし、DOMの再構築を最小限に抑えてくれる!

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

'jQuery' is not defined を解決する。

エラー

Module Warning (from ./node_modules/eslint-loader/index.js):
error: 'jQuery' is not defined

原因

jqueryを定義していない。

解決

main.jsにjqueryを定義する。
以下コードを追加。

main.js
import jquery from 'jquery'

参考

https://github.com/SimulatedGREG/electron-vue/issues/259

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

[vue-router] BeforeRouteLeaveが2度発火する

TL;DR

  • Vue-RouterBeforeRouteLeave等ナビゲーションガードが二回発火する場合がある。
  • 遷移先のコンポーネントに何らかのリダイレクトがあるときに発生する。
  • toを調べることで回避可能。

同じ問題を扱ったissue
=> https://github.com/vuejs/vue-router/issues/2102

本件は仕様として扱われるようなので、今後しばらく対応される見込みはなさそう。

再現環境

  • Chrome バージョン: 75.0.3770.142
  • Nuxt 2.6
  • vue-router 3.0

リポジトリと検証動画

https://github.com/IKKO-Ohta/invoke_brl_twice
output.gif

トップページ - ページ1 - ページ2という構成で、

  • ページ2のバックボタンを押すと、window.confirm()を表示する。「OK」を押すとページ1に遷移する。
  • ページ1では、ページ2からの遷移をキャッチするとトップページに飛ばす

想定としては、例えば商品販売サイトにおいて、ページ2が注文完了画面で、ページ1が決済確認ページ、くらいのとき。
しかしこのとき、ページ2のBeforeRouteLeave が2度発火する。window.confirm()のOKを1度押しても再度ダイアログが表示される。

どうして起こるか?

予想

ex.png

2 => 1, 1 => top のような遷移をしそうな気がする。

実際

rec.png
実際のページ2のbeforeRouteLeaveはこのように挙動する。

beforeRouteLeaveが 2=>1 と 2 => top の2回分呼ばれる。引数のfrom.nameto.nameの中身を調べると確かにそうなっている。1=>top へのリダイレクトがこの2回目の遷移を誘発させる。このとき1=>topの遷移でもない! あくまで from="2", to="top"のbeforeRouteLeaveが走る!

解決策

ページ2beforeRouteLeavetoを検査し、一回目の遷移についてガードしないようにすれば良い。

before

  beforeRouteLeave(to, from, next) {
    window.confirm("このページを離れますか?") ? next() : next(false);
  }

after

  beforeRouteLeave(to, from, next) {
    if (to.name === "first") {
      next();
      return;
    }

    window.confirm("このページを離れますか?") ? next() : next(false);
  }

なお、afterは可読性の低いコードなので(一見「戻る」のときにはガードしない、というコードに見える)公式issueなどを引用して適宜コメントに追記することを推奨する。

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

docker-composeを使ってVeu.jsのプロジェクトを作成して、Dockerで動かしてみた

docker-composeを使ってVeu.jsのプロジェクトを作成して、Dockerで動かしてみました。

プロジェクトを作成するディレクトリを作成する

directory
/hoge_directory

プロジェクトを作成するためのdocker-compose.ymlを作成する

directory
/hoge_directory
    |_ docker-compose.yml
docker-compose.yml
version: '3'
services:
  node:
    image: node:12.7.0-alpine
    volumes:
      - .:/vuejs

今回はコンテナ内で作成したVue.jsプロジェクトを、ホストに同期させて作るので、
ホスト側のhoge_directoryとコンテナ内の/vuejsをマウントする

    volumes:
      - .:/vuejs

docker-composeでサービスを起動して、コンテナ内に入る

sh
$ docker-compose run node sh

マウントされている/vuejsに移動する

sh
/ # cd vuejs/

Vue CLIをインストールする

今回はVue CLIを使用して、プロジェクトを作成するので、Vue CLIをインストールする

sh
/vuejs # yarn global add @vue/cli

Vue.jsのプロジェクトを作成する

sh
/vuejs # vue create .

上記のコマンドを実行すると、対話式でプロジェクトの設定をしていく

YESを選択
?  Your connection to the default yarn registry seems to be slow.
   Use https://registry.npm.taobao.org for faster installation? (Y/n) Y
YESを選択
? Generate project in current directory? (Y/n) Y
defaultを選択
? Please pick a preset: (Use arrow keys)
❯ default (babel, eslint) 
  Manually select features
Yarnを選択
? Pick the package manager to use when installing dependencies: (Use arrow keys)
❯ Use Yarn 
  Use NPM 

コンテナ内と、ホスト側に、プロジェクトが作成される

コンテナ
/vuejs
    |_ node_modules
    |_ public
    |_ src
    |_ .gitignore
    |_ babel.config.js
    |_ docker-compose.yml
    |_ package.json
    |_ README.md
    |_ yarn.lock
ホスト
/hoge_directory
    |_ node_modules
    |_ public
    |_ src
    |_ .gitignore
    |_ babel.config.js
    |_ docker-compose.yml
    |_ package.json
    |_ README.md
    |_ yarn.lock

プロジェクトの作成完了!!!

dockerVue.jsを動かす

Dockerfileを作成する

directory
/hoge_directory
    |_ node_modules
    |_ public
    |_ src
    |_ .gitignore
    |_ babel.config.js
    |_ docker-compose.yml
    |_ package.json
    |_ README.md
    |_ yarn.lock
    |_ Dockerfile <- 作成
Dockerfile
FROM node:12.7.0-alpine

WORKDIR /myapp

COPY package.json ./
COPY yarn.lock ./

RUN yarn install

docker-compose.ymlを修正する

docker-compose.yml
version: '3'
services:
  view:
    build: .
    command: yarn run serve
    volumes:
      - .:/myapp
      - /myapp/node_modules
    ports:
      - "8000:8080"

Vue.jsプロジェクトをマウントした場合に、
コンテナ側のnode_modulesが上書きされて、削除される場合があるので、
マウントされないようにする

volumes:
      - ./vuejs:/myapp
      - /myapp/node_modules <- マウントで上書きされなくなる

(プロジェクトが作成される時に作成される.gitignoreでは、
node_modulesはデフォルトで無視されているので、Gitで共有していると、
コンテナが立ち上がらなくなる)

サービスを起動して、コンテナを立ち上げる

sh
$ docker-compose up -d

アクセスする

URL
http://localhost:8000/

スクリーンショット 2019-07-28 13.19.29.png

終わり

マウントした時に、node_modulesが上書きされて、削除されるのは、
なかなかハマりました...

https://github.com/tubutubumustard/vuejs_docker

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

docker-composeを使ってVue.jsのプロジェクトを作成して、Dockerで動かしてみた

docker-composeを使ってVue.jsのプロジェクトを作成して、Dockerで動かしてみました。

プロジェクトを作成するディレクトリを作成する

directory
/hoge_directory

プロジェクトを作成するためのdocker-compose.ymlを作成する

directory
/hoge_directory
    |_ docker-compose.yml
docker-compose.yml
version: '3'
services:
  node:
    image: node:12.7.0-alpine
    volumes:
      - .:/vuejs

今回はコンテナ内で作成したVue.jsプロジェクトを、ホストに同期させて作るので、
ホスト側のhoge_directoryとコンテナ内の/vuejsをマウントする

    volumes:
      - .:/vuejs

docker-composeでサービスを起動して、コンテナ内に入る

sh
$ docker-compose run node sh

マウントされている/vuejsに移動する

sh
/ # cd vuejs/

Vue CLIをインストールする

今回はVue CLIを使用して、プロジェクトを作成するので、Vue CLIをインストールする

sh
/vuejs # yarn global add @vue/cli

Vue.jsのプロジェクトを作成する

sh
/vuejs # vue create .

上記のコマンドを実行すると、対話式でプロジェクトの設定をしていく

YESを選択
?  Your connection to the default yarn registry seems to be slow.
   Use https://registry.npm.taobao.org for faster installation? (Y/n) Y
YESを選択
? Generate project in current directory? (Y/n) Y
defaultを選択
? Please pick a preset: (Use arrow keys)
❯ default (babel, eslint) 
  Manually select features
Yarnを選択
? Pick the package manager to use when installing dependencies: (Use arrow keys)
❯ Use Yarn 
  Use NPM 

コンテナ内と、ホスト側に、プロジェクトが作成される

コンテナ
/vuejs
    |_ node_modules
    |_ public
    |_ src
    |_ .gitignore
    |_ babel.config.js
    |_ docker-compose.yml
    |_ package.json
    |_ README.md
    |_ yarn.lock
ホスト
/hoge_directory
    |_ node_modules
    |_ public
    |_ src
    |_ .gitignore
    |_ babel.config.js
    |_ docker-compose.yml
    |_ package.json
    |_ README.md
    |_ yarn.lock

プロジェクトの作成完了!!!

dockerVue.jsを動かす

Dockerfileを作成する

directory
/hoge_directory
    |_ node_modules
    |_ public
    |_ src
    |_ .gitignore
    |_ babel.config.js
    |_ docker-compose.yml
    |_ package.json
    |_ README.md
    |_ yarn.lock
    |_ Dockerfile <- 作成
Dockerfile
FROM node:12.7.0-alpine

WORKDIR /myapp

COPY package.json ./
COPY yarn.lock ./

RUN yarn install

docker-compose.ymlを修正する

docker-compose.yml
version: '3'
services:
  view:
    build: .
    command: yarn run serve
    volumes:
      - .:/myapp
      - /myapp/node_modules
    ports:
      - "8000:8080"

Vue.jsプロジェクトをマウントした場合に、
コンテナ側のnode_modulesが上書きされて、削除される場合があるので、
マウントされないようにする

volumes:
      - ./vuejs:/myapp
      - /myapp/node_modules <- マウントで上書きされなくなる

(プロジェクトが作成される時に作成される.gitignoreでは、
node_modulesはデフォルトで無視されているので、Gitで共有していると、
コンテナが立ち上がらなくなる)

サービスを起動して、コンテナを立ち上げる

sh
$ docker-compose up -d

アクセスする

URL
http://localhost:8000/

スクリーンショット 2019-07-28 13.19.29.png

終わり

マウントした時に、node_modulesが上書きされて、削除されるのは、
なかなかハマりました...

https://github.com/tubutubumustard/vuejs_docker

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

Nuxt.js Vue.js Vuexの設計についてちょっと。

Vue.jsとちょっとだけ仲良くなれた気がする。
基本的な考え方はこれと同じで、
https://qiita.com/nekobato/items/08bc7eabb1921871be50
実装にcomponentタグを使うと多分うまく、moleculesとatomsの分離ができそう。
mixinもvuetifyみたいに~ableを使ったら効率化できそう。
https://github.com/vuetifyjs/vuetify/tree/master/packages/vuetify/src/mixins/bootable
mixinの呼び出し側でimplements Overlayableってできたらさらに安全に実装できそう。
organismsはdata providerとして使うのとmarginの調節くらいになるのなぁという印象。organismsはatomに依存するのも、moleculesに依存するのもありにしよう。ということで週末で設計に関してちょっと成長しました。

あとは、Vuexの永続化について、ServersideとClientSideで分けた方が良いのかな。

pluginごと分けて動くのかは検証が必要だけど多分動く。

あとは、jsに関してはgetLocationがVueのdirective?pluginとして機能しているけど、切り離した方が良いなぁ。
vuex側でロジックを持たせるのか、vueでロジックを持たせるのかはちょっとむずいなぁ。

fetchかasyncDataかという論争に関して、

nuxtSserverinitって結局何ができるの?

    // ここでdata属性を使ってしまうと、更新が厄介になるので結局、fetchにして、serversiderenderingできるならそっちでやりたい
    const res = {
      currentLocation: store.getters['location/getCurrentLocation'],
      popularAreas: store.getters['location/getPopularAreas'],
      nearByCities: store.getters['location/getNearByCities'],
      nearByInstagrams: store.getters['location/getNearByInstagrams'],
      loaded: store.getters['location/getLoadAtOnceCalledByIndexPage'],
      nearByTweets: [],
    }

結局どこでデータの生計をやるかという問題を考えた時に、
reactiveなままで、加工しなければいけないということを考えると、actionになるんだけどあっている?

一応page コンポーネントでmapGettersやってもちゃんとserversiderenderingとして機能していることはわかった。

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

LodashでObjectをマージする方法

VueとかVuexでObjectをlodashを使って、必要なところだけマージしたい時に何回か調べるのでメモまでに。

lodashとかをあまり知らない人からすれば同じだけれど、よく知っている人からしたら実装意図がわからなくなってしまう問題。

ただのObjectsをマージするなら
_.defaults();

prototype chainまで見るなら(Object.assignと似てる)
_.assign();

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

Vue.jsでポートフォリオサイトを作ってみた

はじめに

まだ大した内容ができてないですが、最低限のページを作成したので、公開します。
見た目や内容は今後調整していきます。
GitHub Pagesで公開しました。

ポートフォリオ
GitHub: https://github.com/minuro/portfolio

環境構築

以下の記事で書いた内容で環境を構築しました。
Vue.jsの環境構築

Vuetify

今回はVuetifyを導入しました。
これは、Vue.js用のマテリアルデザインのフレームワークです。
vueプロジェクトのディレクトリに移動後、以下のコマンドで導入できます。
色々聞かれますが、とくにこだわらなければエンターで大丈夫です。

vue add vuetify

基本的な作り方

簡単に作り方を記載しておきます。
※以下のソースはある程度省略しています。

ルートの作成

src/router.jsにページを記載します。
例えば以下のように記載します。

src/router.js
import Vue from 'vue'
import Router from 'vue-router'
import Top from './views/Top.vue' //importを追加
import About from './views/About.vue' //importを追加

Vue.use(Router)

export default new Router({
  mode: 'history',
  base: process.env.BASE_URL,
  routes: [ //routesを追加
    {
      path: '/',      //url
      name: 'top',    //ページ名
      component: Top  //コンポーネント指定
    },
    {
      path: '/about',
      name: 'about',
      component: About
    }
 ]
})

ルートパスで、views/Top.vueにアクセスでき、ルート/aboutのパスで、views/About.vueにアクセスできます。

表示の作成

src/App.vueにあるように、router-viewを記載した箇所に、先ほどルートで指定したコンポーネントの内容が表示されます。

また、ルートを切り変える際は、router-linkで記載したリンクから遷移できます。
nameのところには、ルートで指定した名前を記載します。

<router-link :to="{ name: 'about' }">

コンポーネントの作成

templateタグの中に内容を記載します。
v-となっているタグはVuetifyのタグで、詳しくはドキュメントに書かれています。
Grid system — Vuetify.js

text-xs-centerjustify-centerは内容を中央寄せにし、
rowは要素を横方向に並べ、wrapは横方向に入らない際に改行を行います。
xs12などは横幅の指定になります。

<template>
  <v-container text-xs-center>
    <v-layout row wrap justify-center>
      <v-flex xs12>
        <h1>About</h1>
      </v-flex>
    </v-layout>
  </v-container>
</template>

GitHub Pages

ビルド

ファイル作成後は、npm run buildコマンドを実行することで、プロジェクトがビルドされます。
これにより、ビルドファイルがデフォルトだとdistディレクトリに作成されます。
しかし、GitHub Pagesで公開するためには、docsディレクトリを作成する必要があります。
そのため、以下のように設定を修正します。

まず、プロジェクト直下に、vue.config.jsというファイルを作成します。
その中に、以下の内容を記載します。

vue.config.js
module.exports = {
    publicPath: process.env.NODE_ENV === 'production'
      ? '/"プロジェクト名"/' //ここは各自のプロジェクト名を入力します。
      : '/',
    outputDir: 'docs',
  }

これは、本番環境時は/"プロジェクト名"/というパスでファイルにアクセスし、ビルドファイルの出力先ディレクトリをdocsにするというような内容です。

上記の設定を完了したら、npm run buildを実行します。

GitHub Pagesの設定

ビルドを実行して作成された、docsディレクトリをmasterブランチにプッシュします。

GiuHubにて、該当のリポジトリのSetting欄の真ん中ほどにGitHub Pagesという欄があるため、以下のように、Source欄でmaster branch/docs folderを選択します。
githubpages.png

反映されると、https://"ユーザー名".github.io/"リポジトリ名"/のURLでアクセスできるようになります。

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

vue.js elementui 複数フォームを使う

vue.jsで 1ページで複数のフォームを for 文などで繰り返し使う。

考え方としては
親vue から子vueをfor文で読み出す。
その際に 変数 id や title などを渡す。

ってな感じでしょうか。

まずは親vueからコンポーネントを読み出しましょう

Yumeanswershow.vue
<div v-for="(v,key) in list" style="display: block;margin-top: 15px;">
    {{v.id}} : {{v.title}}
    <yumeansweradd-component :id="v.id" :title="v.title"></yumeansweradd-component>
</div>


//略
export default {

components: {// 非同期で読み込み
    'yumeansweradd-component': () => import('./YumeansweraddComponent.vue')
},

今度は 子 vue

YumeansweraddComponent.vue
<template>

    <div>

        <el-form :model="form" :rules="rules" ref="form">
            <el-form-item :label="id + title" prop="message">
                <el-input type="textarea" v-model="form.message" :rows=5></el-input>
            </el-form-item>
            <el-form-item>
                <div class="t-c">
                    <el-button type="primary" @click="submitForm('form')">登録</el-button>
                </div>
            </el-form-item>
        </el-form>

    </div>


</template>

<script>

    import axios from 'axios'


    export default {
        props: ['id','title'],//親要素から id を取得
        data() {
            return {
                form: {
                    message:'',
                },
                rules: {
                    message: [
                        { required: true, message: '入力してください', trigger: 'change' }
                    ],
                },

            };

        },

        created () {
            // console.log("idは" + this.id);
        },

        methods: {

            submitForm(formName) {

                this.$refs[formName].validate((valid) => {
                    if (valid) {
                        console.log("送信しました");
                    } else {
                        console.log('error submit!!');
                        return false;
                    }
                });

            },


        }


    }

</script>





以上。

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

Vue.jsについての基礎(インストール〜基本構文)

はじめに

おはようございます。こんにちは。こんばんは。
ワタタクです。
今回はVue.jsについて見けいけたらいいなと書いています。
Vue.jsに関しては少し触ったことがある程度なので、今後の為にと勉強させていただきますので
もし間違いとかありましたらご教授、アドバイスなんかいただけたら幸いでございます。
では早速参ります。

Vue.jsとは?

公式サイト

Vue.js(ヴュージェイエス)、またはVueは、Webアプリケーションにおけるユーザーインターフェイスを構築するための、オープンソースのJavaScriptフレームワークである。他のJavaScriptライブラリを使用するプロジェクトへの導入において、容易になるように設計されている。一方で高機能なシングルページアプリケーション(SPA)を構築することも可能である。

chromeの拡張機能のvuejs-devtoolsを入れて置くと便利。

環境構築

今回はCDNを使わずvue-cliを使います。

nodeがインストールされているか確認する

$ node -v
v11.2.0

もし、コマンドを叩いてもversionが表示されなかったらnode.jsをインストールしてください。
以下も確認して置いてください。

$ npm -v
6.9.0

vue.jsのインストール

上記のことを確認したら、いよいよvue.jsをインストールしていきます。

$ npm install -g vue-cli

成功すれば以下を確認してください。

$ vue -V
3.1.1

確認できれば次のコマンドを実行してください。

$ vue init webpack test-vue

※test-vueの部分はプロジェクト名&ディレクトリ名になります。
いろいろ聞かれますが、全部EnterでもOKです。
実行が終わったら下記を実行してください。

$ cd test-vue
$ npm run dev

localhost:8080にブラウザからアクセスしてみてください。Vueアプリケーションの土台ができており、
下記の画面が表示されるはずです。

test-vue.png

これで環境構築は終了です。
お疲れ様でした。

基本構文

いろんなファイルが出来ててキョどりますが大丈夫です。
基本的に難しい設定をしなければ、だいたいsrcディレクトリの中のVueファイルだけをいじるだけです。

v-showディレクティブ

まず最初に要素を表示するかどうかを決める条件付きレンダリングです。

HelloWold.vue
<template>
  <div class="hello">
    <h1>{{ msg }}</h1>
<!--////////////////追加/////////////////-->
    <h2 v-show="showText">表示項目</h2>
<!--/////////////////////////////////-->
  </div>
</template>

<script>
export default {
  name: 'HelloWorld',
  data () {
    return {
      msg: 'Welcome to Your Vue.js App',
//----------------------追加----------------------
      showText: false
//-----------------------------------------------
    }
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>

</style>

v-showディレクティブはタグの中にいれて書きます。上のコード内ではイコールでshowTextという変数をみてます。この変数の名前は任意ですが、これがfalseなら非表示、trueなら表示となります。

イベントハンドリング

ボタンをクリックしたら表示、非表示が切り替わる仕組みを作ります。
早速コードを見てみましょう。

HelloWold.vue
<template>
  <div class="hello">
    <h1>{{ msg }}</h1>
<!--////////////////追加/////////////////-->
    <button v-on:click="toggle">toggle</button>
<!--/////////////////////////////////-->
    <h2 v-show="showText">表示項目</h2>
  </div>
</template>

<script>
export default {
  name: 'HelloWorld',
  data () {
    return {
      msg: 'Welcome to Your Vue.js App',
      showText: false
    }
  }//追加
  ,methods: { 
    toggle: function () {
      this.showText = !this.showText
    }
  }
   //-----------------------------------------------
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>

</style>

v-onディレクティブ

buttonタグにv-on:clickというものがついてます。v-onでイベント発火時の JavaScript の実行が可能になります。clickしたら呼ばれる関数名をtoggleにしてます。これはmethodsオブジェクトの中で定義されています。
v-on:は@とも書ける

methodsオブジェクト

methodsオブジェクトの中に実行したい関数をガシガシ定義していきます。これをv-on:clickで読んだり、他の関数から叩いたりします。

thisについて

data内で定義した値はthis.showTextのようにthisを使って参照していきます。

v-modelディレクティブ

v-modelはVue.jsを使ってフォームを構築する際によく使う機能です。
v-modelはv-onとv-bindをまとめて一行で書くためのシュガーシンタックスです。
例としてフォームに入力した文字数をカウントする仕組みを作ります。

HelloWould.vue
<template>
  <div class="hello">
    <h1>{{ msg }}</h1>
<!--////////////////追加/////////////////-->
    <input type="text" name="text" v-model="count">
    <h3>現在の文字数:{{ charaCount }}文字</h3>
<!--/////////////////////////////////-->

  </div>
</template>

<script>
export default {
  name: 'HelloWorld',
  data () {
    return {
      msg: 'Welcome to Your Vue.js App',
      //追加
      count: ''
    }
  }
  ,//追加
  computed: {
    charaCount: function() {
        return this.count.length;
    }
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>

</style>

ここで重要なのは、「computed」です。
computedという機能を使えば、dataで保持している値に変更がある度に、それを利用した値をリアルタイムに生成できます。

ついでにもう一つ。
computedに似ているのですが、「watch」という機能を紹介します。
watchは、dataで保有している値に1対1で対応して、その値が変更されたときに動作するメソッドのようなものです。
Vue公式ガイドではwatchよりもcomputedを推してます。

以上が双方向データバインディング(v-model)の基本的な使い方で、webアプリを作成するときには非常によく使う手法ですので、必ずマスターしてください。

v-forディレクティブ

v-forディレクティブを利用して、配列の要素を繰り返し表示させてみます。

HelloWold.vue
<template>
  <div class="hello">
    <h1>{{ msg }}</h1>
<!--////////////////追加/////////////////-->
    <table>
          <thead>
              <tr>
                  <th>タイトル</th>
                  <th>著者</th>
              </tr>
          </thead>
          <tbody>
              <tr v-for="book in books" v-bind:key="book.id">
                  <td>{{ book.title }}</td>
                  <td>{{ book.author }}</td>
              </tr>
          </tbody>
      </table>
<!--/////////////////////////////////-->
  </div>
</template>

<script>
export default {
  name: 'HelloWorld',
  data () {
    return {
      msg: 'Welcome to Your Vue.js App',
      //追加
      books: [
          { id: 1, title: '坊っちゃん', author: '夏目漱石' },
          { id: 2, title: '人間失格', author: '太宰治' },
          { id: 3, title: 'ノルウェイの森', author: '村上春樹' }
      ]
    }
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>

</style>

booksプロパティとしての配列からbookという変数名に置き換えたオブジェクトを取得しています。v-forディレクティブは一般的に下記のような記述をします。

v-for="変数名 in 配列やオブジェクト"

また、v-forディレクティブを利用する場合、v-bindディレクティブで一意の(ユニークな)keyプロパティを設定する必要があります。
最低、今までのことが理解できればVue.jsの基本構文は大丈夫だと思います。
あとの描き方なんかは公式サイト参照でお願いします。

コンポーネント

初めて触る人に対して、一番理解に苦しむと思いますが、(作者は苦労しました。)
Vue.jsに限らず、Reactなどのjavascriptフレームワークでは必須の考え方なので理解しておきましょう。
マップアプリを作りながら解説していきます。
その前に、以下のコマンドを入力してください。

$ npm install --save materialize-css@1.0.0-rc.1

次に「main.js」に以下を記述し、vue-routerの設定をコメントアウトで潰してください。

js;main.js
import Vue from 'vue'
import App from './App'
//import router from './router'
<!--//////////////追加///////////////////////-->
import 'materialize-css'
import 'materialize-css/dist/css/materialize.min.css'
<!--/////////////////////////////////////-->
Vue.config.productionTip = false

/* eslint-disable no-new */
new Vue({
  el: '#app',
  //router,
  components: { App },
  template: '<App/>'
})

その次に、「index.html」に以下を記述しGoogleMapApiを読み込んでください。

これで準備が整いました。
では、「components/Map.vue」をつくります。
作れたら、App.vue(親コンポーネント)に以下のコードを記述してください。

App.vue
<template>
  <div id="app">
    <nav class="blue navbar">
      <div class="nav-wrapper">
        <a href="#" class="brand-logo center"><i class="material-icons left" >ヘッダーだよん!!</i></a>
      </div>
    </nav>

    <!-- Mapをレンダリング -->
    <Map v-bind:center="center"></Map>
  </div>
</template>

<script>
// Mapを読込
import Map from './components/Map.vue';

export default {
  name: 'app',
  components: {
    Map
  },

  data () {
    return {
     // 地図のセンター位置(東京駅)
     //{ lat: 緯度, lng: 経度 }
     center: { lat: 35.681298, lng: 139.7640529 }
    }
  },
}
</script>

<style>

</style>

ここで重要なのは

import Map from './components/Map.vue';

components: {
    Map
}

です。以下で子コンポーネント(Map)を読み込んでいます。
そして<Map v-bind:center="center"></Map>でレンダリングしています。

※v-bind, :value
親 -> 子にデータを受け渡す際、親側で記述します。親からデータを渡すための窓口のイメージです。

v-bind:プロパティ名もしくは:プロパティ名という書き方をします。プロパティ名は、後述する子要素のpropsの変数名のことです。

次に「components/Map.vue」です。(子コンポネート)

Map.vue
<template>
  <div class="main-area">
    <div class="main-area-inner">
      <div id="map" ref="map"></div>
    </div>
  </div>
</template>

<script>
  export default {

    // 親のコンポーネントからpropsでdataを受け取る
    props: ['center'],

    data () {
      return {
        // Map Objectを保存する
        map: null,
        // Markerオブジェクトを配列で保存する
        markers: [],
      }
    },

    mounted () {
      // Mapの初期処理を実行する
      const map = this.$refs.map;
      this.map = new window.google.maps.Map(map, {
        center: this.center,
        zoom: 17
      });

      // マーカーを画面上に置く
      this.markers = [];
      const marker = new window.google.maps.Marker({
        position: {
          lat: this.center.lat,
          lng: this.center.lng
        },
        map: this.map,
        animation: google.maps.Animation.DROP
      });
     }

  }

</script>

<style>

</style>

ここで重要なのはprops
propsは、親 -> 子にデータを受け渡す際、子側で記述します。子がデータを受け取るための窓口のイメージです。

非同期通信と$emit

まず、今回非同期通信はaxiosを使います。

インストール

$ npm install --save axios

main.jsにてaxiosを取り込みます。これですべてのコンポーネントにおいて「this.$axios」でaxiosが利用できるようになります。

main.js
import Vue from 'vue'
import App from './App'
//import router from './router'

import 'materialize-css';
import 'materialize-css/dist/css/materialize.min.css'

import axios from 'axios' //追加

Vue.config.productionTip = false

Vue.prototype.$axios = axios //追加

/* eslint-disable no-new */
new Vue({
  el: '#app',
  //router,
  components: { App },
  template: '<App/>'
})

※任意のコンポーネントの中にてaxiosをインポートすることも可能です。その場合は「this.$axios」ではなく「axios」となります。

さて先ほど作ったマップアプリを使いながらaxios$emitの解説をしていきましょう。
※今回、楽天トラベルAPIを使います。

前回、行ったように「components/Card.vue」を作成し、コンポーネントを読みこんでください。

vue;Card.vue
<template>
  <div class="card">
      <div class="row">
        <div class="col s3 image">
          <img v-bind:src="this.rankings.hotelImageUrl" class="responsive-img" />
        </div>
        <div class="col s9 content">
            <i class="left">ランキング:<font color="red">{{ this.rankings.rank }}</font></i>
            <i class="left">ホテル名:{{ this.rankings.hotelName }}</i>
            <i class="left">都道府県:{{ this.rankings.middleClassName }}</i>
        </div>
      </div>
  </div>
</template>

<script>

  export default {
    data () {
      return {
        rankings: ""
      }
    },
    mounted () {
      // 楽天APIからデータを取得する
      this.searchPlans();
    },
    methods: {
      // 楽天APIに接続して、データを取得する
      searchPlans () {
        this.$axios.get(" 楽天APIからのURL ",{
        }).then((resp) => {
          this.rankings = resp.data.Rankings[0].Ranking.hotels[0].hotel

          this.$emit("ranking", resp.data.Rankings[0].Ranking);
        }).catch(err => {
          console.log(err);
        });
      }
    }
  }
</script>
 
<style scoped>

</style>
App.vue
<template>
  <Card v-on:ranking="getRanking"></Card>
</template>

<script>
data () {
    return {

      rankings: [],

    }
  },
  methods: {
    getRanking(ranking) {
      this.rankings = ranking;
    },
  }
}
</script>

<style>

</style>

ここで重要となってくるのがaxios(非同期通信)の書き方this.$emitの使い方です。
まずaxiosの書き方は

//GET
axios.get('URL', {
  params: {
    キー: 値
  }
})
.then(function (response) {
  console.log(response);
})
.catch(function (error) {
  console.log(error);
});

//※これでも可
axios.get('/URL?キー: 値')
  .then(function (response) {
console.log(response);
})
.catch(function (error) {
  console.log(error);
});

//POST
axios.post('URL', {
  キー: 値,
  キー: 値
})
.then(function (response) {
  console.log(response);
})
.catch(function (error) {
  console.log(error);
});

です。

続いてthis.$emitの説明をします。
簡単にいうと、子 -> 親にデータを受け渡しするときに使います。
例えば、子コンポネートの変数hogeの値を親コンポーネントに渡したい時は

子コンポート
this.$emit("イベント名", hoge);

として、親コンポートで

親コンポーネント
<Template>
  <子コンポーネント v-on:イベント名:"関数"></子コンポーネント>
</Template>
<script>
data () {
  return {
    hoge: ""
  }
},
methods: {
  関数名 (hoge) {
    this.hoge = hoge;
  },

}
</script>

このように使うと子コンポーネントの値が親で使えます。

まとめ

最後にややこしいけどめっちゃ重要なことをまとめておきます。
Vueの親子コンポーネント間でデータをやりとりするときの鉄則は Events Up, Props Down です。
すなわち、

  • 親 -> 子へデータを受け渡す際にはv-bindpropsを使う(props down)
  • 子 -> 親へデータを受け渡す際には$emitv-onを使う(events up)

最後に

長くなりましたが今回はこの辺で、最後まで読んでいただきありがとうございました。
もし、間違い等、アドバイス、ご指摘等有れば教えていただけたら幸いです。

次回はVue-routerの使い方についてです。
今後はVuex,Nuxt.jsについて触れていきたいと思います。

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

【Vuex】firebaseでtwitterログインし、Vueインスタンス生成時に認証状態をストアに格納することでログイン維持する

Vuexでログイン認証の状態管理をしたい

vue.jsとfirebaseでwebサービス開発をしています。
ログイン認証機能を実装するにあたって状態管理にハマりまくりました。

大筋は、下記のチュートリアルを参考に行なっております。
Vue + Vue Router + Vuex + Laravelで写真共有アプリを作ろう (6) 認証機能とVuex

twitterでは何度紹介したかわかりませんがめちゃくちゃわかりやすいです。。本当に感謝
こちらのチュートリアルに沿って実装つつ、認証機能を、firebaseを使ったtwitterログインに置き換えて実装しています。

環境

MacOS HighSierra
vue.js 2.9.6
firebase 6.11.0

前提

  • vue.js・Vuex・firebase・firestoreをインストール
  • firebaseを使ってtwitterログイン認証を行う
  • ユーザー情報をVuexのステートに格納する方法でログイン状態を管理する

やりたいこと

  • ページをリロードしてもログイン状態を維持したい
    • ページをリロードするとVueインスタンスが再生成されるため、ステートが初期値のnullに戻ってしまうことが問題でした。

処理の流れ

  • ページリロード・ログイン後などVueインスタンスを生成する時に毎回ログイン状態を確認する
  • ユーザーが存在していたらステート更新

問題点

  • Vueインスタンス生成時に毎回ログイン状態を確認し、ユーザーがいたらステート更新、いなかったらnullとする
    • 挙動自体はうまくいったのですが、下記のエラーが無限ループしていました。
[Vue warn]: Error in callback for watcher "function () { return this._data.$$state }": 
"Error: [vuex] do not mutate vuex store state outside mutation handlers."

ソースコード

index.js
// Vueインスタンスを生成する関数
const createApp = () => {
  // 認証状態を確認する
  firebase.auth().onAuthStateChanged(user => {
    if (user) {
     // ユーザーが存在していたら、ストアにuser情報を渡す
      store.dispatch('auth/currentUser', user)
    } else {
     // いなかったらnull
      store.dispatch('auth/currentUser', null)
    }
  })

  new Vue({
    el: '#app',
    'router': router,
    'store': store,
    components: { App },
    template: '<App/>'
  })
}
// 最後に関数実行
createApp()
auth.js
const actions = {
  currentUser (context, user) {
    if (!user) {
      context.commit('setUser', null)
    } else {
        // ステート更新
        context.commit('setUser', user)
      })
    }
  }
}

const mutations = {
  setUser (state, user) {
    // ユーザー情報を更新
    state.user = user
  }
}

原因と解決方法

「ミューテーションハンドラの外で変更するな」って怒られてる・・なんだこれってなってたんですが、違いました。

firebase.auth().onAuthStateChanged の戻り値 user をそのままセットしていたのが原因 でした。
userに含まれている情報を改めて連想配列にセットし直すことで解決。

auth.js
const actions = {
  currentUser (context, user) {
    if (!user) {
      context.commit('setUser', null)
    } else {
      // userで得られた値でストアに入れたい情報をセットする
      const currentUser = {
          'displayName': user.displayName,
          'photoURL': user.photoURL,
          'uid': user.uid,
      }
      // ステート更新
      context.commit('setUser', currentUser)
      })
    }
  }
}

こちらを読んで思い至りました。
Vuex.store.stateの値をmutationsで変更しようとしたら、mutationsを使わずに値を変えるなと怒られた

// エラーメッセージ
[Vue warn]: Error in callback for watcher "function () { return this._data.$$state }": 
"Error: [vuex] do not mutate vuex store state outside mutation handlers."

コールバック関数の戻り値でエラーを吐いているみたいなことが一文目に書いてありますね・・。

まとめ

Vueインスタンス生成時に認証状態を確認し、ステート更新処理を入れることで、ログイン状態を維持することができるようになりました。
firebaseを使うことによってOauth認証部分が楽チンに実装できるので嬉しいですね。
その分、firebaseの特性を知らないと思わぬところでハマるなあと思う日々であります。
同時に、今回初めてSPA書いているのですが、SPAの特性もよく理解する必要があるなぁと思うことしきりです。

勉強になりました。

おかしなところがあったら、コメント欄か、twitterでお教え頂けたら幸いですm(__)m

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

v-forで回している選択肢に、動的に色をつけたいときに簡単に書く

v-forで回している選択肢に、動的に色をつけたいときに簡単に書く。

v-for="(hoge, key) in hoges"
:class="{'hoge-color': key === current_key}"

<script>

    data() {
        return {
            current_key: 0, // 初期表示のとき
        }
    }

'hoge-color': key === current_keyここのロジックをメソッド化してもいいけど、めんどいのでこう書いた。

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

can not set reatcive property on undefined,Vueで複数選択可のチェックボックスを実装するときのエラー

Vueで次のエラーを解消するための一つの方法。
ノートのメモなので動かしてないので、違ってたらすみません。

Vueで複数選択可のチェックボックスを実装するときのエラーです。

結論を言うと、dataにあらかじめ定義してないよというエラーみたい。
dataにあらかじめ定義してなくてもよい場合と、定義してないといけない場合があるみたい。

can not set reatcive property on undefined, null, or primitive.value
<label
    v-for="(hoge, key) in hoges"
    :key = "key"
    :for = "key"
>
<input
    type="checkbox"
    v-model="hoges.hoge_id"
    :value="hoge.id"
    id="index"
>
{{ hoge.names }}

<script>

data: ~ 中略
// 定義しておく
    hoges: {
        hoge_id: []
    }


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

ASP.NET Core + Vue.js (TypeScript)で開発する時の覚書

ASP.NET Core + Vue開発時まとめ

概要

きっとみんなこのスタックで開発したいはずなのに、
個別の情報は散在していてまとまった情報がないのでまとめてみた
(AngularとReactはVSやCore SDKのテンプレートがあるのにVueはなぜかない...)

できるようになること

  • C#(Server side) + TypeScript + Vue(Client)での開発
  • Hot Module Reloadingによるデバッグ時でのフロントエンドコードの修正・リアルタイム反映
  • リリース・デプロイ時のWebpackの設定

この記事で触れないもの

  • Vue CLI : 結局Webpackを触らないとやってられないし、サーバはASP.NET Coreを使えばいいからあまりメリットがない
  • 認証周り : ASP.NET Coreサーバ単体で運用するんだったらASP.NET Core Identity, バックエンドを分けてCORS設定してJWTをVuexに入れるとか考えられるがここでは割愛。

環境

Windows

  • Visual Studio
  • .Net Core SDK
  • Node開発モジュール

Mac, Linux, WSL

  • .Net Core SDK
  • Node.js
  • Visual Studio Code
    • Vetur
    • TS Lint
    • C# Extension

これらがインストール済みであることが前提。
Node, Net Core SDK、VS Codeなどは出来れば最新のものが望ましい。

準備

VSはASP.NET Coreのテンプレから構築。GUIでどれを選んでもいい。
CLIベースのツールで以降は説明。

新規のprojectを作成し、パッケージを追加する

dotnet new webapp --name QiitaArticle
cd QiitaArticle
dotnet add package Microsoft.AspNetCore.SpaServices
dotnet add package Microsoft.Typescript.MSBuild
dotnet add package Microsoft.VisualStudio.Web.BrowserLink
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet tool install --global dotnet-aspnet-codegenerator

Nodeの設定とモジュールのインストール

npm init # デフォルトの設定でOK
npm i vue axios
npm i -D webpack webpack-cli webpack-hot-middleware clean-webpack-plugin aspnet-webpack webpack-dev-middleware
npm i -D typescript ts-loader css-loader vue-loader vue-template-compiler webpack-merge terser-webpack-plugin
npm i -D vue-class-component vue-property-decorator vuex vue-router

各種設定ファイルを配置する

mkdir Frontend

ここに以下を配置していく

tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "experimentalDecorators": true,
    "sourceMap": true,
    "moduleResolution": "node",
    "lib": ["dom", "es5"]
  },
  "exclude": [
    "node_modules"
  ]
}

互換性はES5とした。APIの通信でPromiseよりもC#と相性が非常にいい async / await記法を使いたいのでlibに追加することで可能。
Vueのコンポーネントの各要素の属性を指定するデコレータは現時点ではExperimentalなのでその旨明記。

vue-shims.d.ts
declare module '*.vue' {
    import Vue from 'vue';
    export default Vue;
}

VueファイルをTSとして読み込むために必要

Webpackの設定

開発と本番で設定ファイルを分ける。共通部分は common にいれる。

webpack.common.js
const path = require('path');
const VueLoaderPlugin = require('vue-loader/lib/plugin');

module.exports = {
    entry: { main: './Frontend/index.ts' },
    output: {
        path: path.resolve(__dirname, 'wwwroot'),
        filename: 'js/[name].js',
        publicPath: '/'
    },
    resolve: {
        extensions: ['.ts', '.js', '.vue', '.json'],
        alias: {
            'vue$': 'vue/dist/vue.esm.js'
        }
    },
    module: {
        rules: [
            {
                test: /\.vue$/,
                loader: 'vue-loader',
                options: {}
            },
            {
                test: /\.tsx?$/,
                loader: 'ts-loader',
                exclude: /node_modules/,
                options: {
                    appendTsSuffixTo: [/\.vue$/]
                }
            }
        ]
    },
    plugins: [
        new VueLoaderPlugin(),
    ]
};

Devではソースマップを有効にする。

webpack.dev.js
const merge = require('webpack-merge');
const common = require('./webpack.common.js');

module.exports = merge(common, {
    mode: 'development',
    devtool: 'inline-source-map',
    devServer: {
        contentBase: './wwwroot',
    }
});

Prodではminifyする。

webpack.prod.js
const merge = require('webpack-merge');
const common = require('./webpack.common.js');
const TerserPlugin = require('terser-webpack-plugin');

module.exports = merge(common, {
    mode: 'production',
    optimization: {
        minimizer: [
            new TerserPlugin({
                terserOptions: { ecma: 5, compress: true,
                    output: { comments: false, beautify: false }
                }
            })
        ]
    }
});

package.jsonを編集して、Webpackのコマンドを呼び分ける

package.json
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
+   "build": "webpack --config webpack.dev.js"
+   "release": "webpack --config webpack.prod.js"

  },

本番デプロイ用にcsprojを編集すうる

QiitaArticle.csproj
    <PackageReference Include="Microsoft.VisualStudio.Web.BrowserLink" Version="2.2.0" />
  </ItemGroup>

+ <Target Name="RunWebpack" AfterTargets="ComputeFilesToPublish">
+   <Exec Command="npm i" />
+   <Exec Command="npm run release" />
+ </Target>
</Project>

ASP.NET CoreでHMRを使えるようにする

Sartup.cs
using Microsoft.Extensions.DependencyInjection;
+ using Microsoft.AspNetCore.SpaServices.Webpack;

//(中略)

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
+               app.UseBrowserLink();
+               app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions
+               {
+                   HotModuleReplacement = true,
+                   ConfigFile = @"./webpack.dev.js"
+               });
            }
            else
            {
                app.UseExceptionHandler("/Error");
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();
            app.UseCookiePolicy();

-           app.UseMvc();
+           app.UseMvc(routes =>
+           {
+               routes.MapRoute(
+                   name: "default",
+                   template: "{controller=Home}/{action=Index}/{id?}");
+               routes.MapSpaFallbackRoute(name: "spa-fallback", new { controller = "Home", action = "Index" });
+           });        
        }

Fall back routeはVue RouterでClient Side Renderingした際に必ず必要なので入れる。

VueがHello worldできることを確認する

Frontend
├───Components
│   └───HelloWorld.vue
├───index.ts
├───tsconfig.json
└───vue-shims.d.ts
index.ts
import Vue from 'vue'
import HelloComponent from './Components/HelloWorld.vue'

let v : Vue = new Vue({
    el: '#app',
    template: `
    <div>
        Name: <input v-model="name" type="text">
        <hello-component :name="name" :initialEnthusiasm="5" />
    </div>
    `,
    data: { name: 'World' },
    components: { HelloComponent }
});
Components/HelloWorld.vue
<template>
    <div>
        <div class="greeting"> Hello {{ name }} {{ exclamationMarks }}</div>
        <button @click="decrement"> -</button>
        <button @click="increment"> +</button>
     </div>
</template>

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

@Component
export default class HelloDecorator extends Vue {
    @Prop() name!: string;
    @Prop() initialEnthusiasm!: number;

    enthusiasm = this.initialEnthusiasm;

    increment() {
        this.enthusiasm++;
    }
    decrement() {
        if (this.enthusiasm > 1) {
            this.enthusiasm--;
        }
    }

    get exclamationMarks(): string {
        return Array(this.enthusiasm + 1).join('!');
    }
}
</script>
Pages/Shared/_Layout.cshtml
    </environment>
    <script src="~/js/site.js" asp-append-version="true"></script>
+   <script src="~/js/main.js" asp-append-version="true"></script>

    @RenderSection("Scripts", required: false)
</body>
Pages/Index.cshtml
    <p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
+   <div id="app"></div>
+   <div id="api"></div>
</div>
dotnet run

でエラーが出ないことを確認。またDeveloper ToolのConsole上に [HMR] と表示されていれば、
フロントエンドのコードをデバッグ実行中に編集しても変更が反映される。

APIを追加して、コンポーネントから呼び出して表示させる

dotnet aspnet-codegenerator controller -name SampleController --relativeFolderPath Controllers --useDefaultLayout --referenceScriptLibraries
Controllers/SampleCOntroller.cs
using Microsoft.AspNetCore.Mvc;

namespace QiitaArticle.Controllers
{
    [Route("api/sample")]
    [ApiController]
    public class SampleController : Controller
    {
        [HttpGet]
        public IActionResult Index()
        {
            return Json(new string[] {"Vue.js", "With", "ASP.NET Core", "Rocks!"});
        }
    }
}
Frontend/Components/CallingApi.vue
<template>
    <div>
        <ul v-for="c in content" :key="c">
            <li>{{c}}</li>
        </ul>
     </div>
</template>

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

@Component
export default class CallingApi extends Vue {
    content : string[] = [];
    async mounted() : Promise<void> {
        this.content =  (await axios.get('api/sample')).data;
    }
}
</script>
index.ts
import Vue from 'vue'
import HelloComponent from './Components/HelloWorld.vue'
+ import CallingApi from './Components/CallingApi.vue'

new Vue({
    el: '#app',
    template: `
    <div>
        Name: <input v-model="name" type="text">
        <hello-component :name="name" :initialEnthusiasm="5" />
    </div>
    `,
    data: { name: 'World' },
    components: { HelloComponent }
});
+ new CallingApi({}).$mount('#api');

これでControllerから取り出した内容が表示されるはず

本番デプロイ

環境変数を変えてProductionモードで見てみる。

dotnet publish -c Release
export ASPNETCORE_ENVIRONMENT=Production
cd ./bin/Release/aspnetcoreapp2.0/publish
dotnet QiitaArticle.dll

これで起動した後、F12を押してConsoleを見てみると、minifyされたJSが読み込まれていて、 HMR が消えている。
実際はこれをサーバに置けばOK.

参考

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

Vue.jsでカスタムディレクティブを実装する

はじめに

Vue.jsには、v-ifv-modelv-forなどのvから始まる特別な属性があります。これらはディレクティブと呼ばれ、与えられたデータをもとにスマートなDOM操作を行ってくれます。例えばv-ifディレクティブは、与えられたデータを真偽値で評価し、その結果によって、結びついたDOM要素を描画したり削除したりしてくれます。

ディレクティブはDOMの操作において大変便利な機能ですが、Vueがデフォルトで用意してくれているディレクティブでは対応しきれないようなDOM操作を行いたい場面も当然ありますよね。
そのような少々込み入ったDOM操作を行う場合、従来のように対象DOM要素を検索してープロパティを変更してーなどと個別にやってもいいのですが、できればVueのアプリケーションの中に再利用可能な状態で取り込めると便利です。
その需要を満たすべく、Vueにはカスタムディレクティブという機能があります。

今回はそのカスタムディレクティブについて、実際に手を動かしながら学習した内容を書いていこうと思います。

カスタムディレクティブの定義

カスタムディレクティブは、平たく言えばオリジナルのディレクティブを登録できるVueの機能です。
定義したカスタムディレクティブを属性値としてDOM要素に付与することで、特定の振る舞いを持たせることができます。

ディレクティブの定義には、Vue.directiveというAPIを使用します。
今回は、付与した要素内のテキストを強調表示するディレクティブv-importantを定義してみます。

Vue.directive('important', {
 bind(el) {
    el.style.color = 'red';
    el.style['font-weight'] = 'bold';
   }
});

第一引数には定義するディレクティブの名称を、第二引数には、DOMの振る舞いを決定するディレクティブ定義オブジェクトを指定します。
ここでは、「ディレクティブが対象の要素に紐づいた時に、要素内のテキストを赤色にして文字を太くする」という振る舞いを定義しています。

定義したカスタムディレクティブを使用してDOM操作を行うには、ディレクティブ名の先頭にv-をくっつけて、要素に属性として付与すればOKです。

<div id="app">
  <p>ここ<span v-important>テストに出る</span>ぞー。</p>
</div>

結果はこのようになります。

See the Pen eqBMev by shironeko-shobo (@shironeko-shobo) on CodePen.

オリジナルのディレクティブがとても簡単に作成できました!

フック関数

ディレクティブ定義オブジェクトではいくつかのフック関数を指定でき、DOM操作を行う関数の実行タイミングを定義できます。
先ほどの例で使用したbindがその1つで、これはディレクティブが初めて対象要素に紐づいた時に1回だけ関数を呼び出す、というフック関数です。

フック関数には以下のようなものがあります。

  • bind - ディレクティブが初めて対象要素に紐づいた時に1回だけ関数を呼び出す
  • inserted - 紐づいた要素が親要素に挿入された時に関数を実行する
  • update - ディレクティブの値の変化等に伴って、紐づいた要素を含む仮想ノードが更新される際に関数を実行する
  • componentUpdated - 紐づく要素を含むコンポーネントの仮想ノードと、子コンポーネントの仮想ノードが更新された後に関数を実行する
  • unbind - ディレクティブが紐づく要素から取り除かれた時に、1度だけ関数を実行する

このうち、updateは値の変化が起きていない場合でも関数を実行してしまう場合があるため、関数の処理の初めで変化前と変化後の値の比較を行うと良さそうです。
また、unbindは主にbindで登録したイベントリスナーの解除などに使わることが多いみたいです。

これらのフック関数を利用することで、実装したいディレクティブに合わせた任意のDOM操作を行うことができます。

フック関数の引数

先の例では、フック関数bindの引数としてelを渡していました。
これはディレクティブが紐づく要素を表していて、DOMの操作では主にこいつをいじることになります。

フック関数の引数には、elを始め以下のようなものがあります。

  • el - ディレクティブが紐づく要素
  • binding - ディレクティブの名前や、渡される値などの情報を含むオブジェクト
  • vnode - Vueのコンパイラが生成する仮想ノード
  • oldVnode - updateとcomponentUpdatedで使用できる、更新前の仮想ノード

このうち、bindingオブジェクトには以下のようなプロパティが含まれます。

  • name - 「v-」の接頭辞を除いたディレクティブの名前
  • value - ディレクティブに渡される値
  • oldValue - updateとcomponentUpdatedで使用できる更新前の値。
  • expression - ディレクティブに渡された式の文字列表現
  • arg - ディレクティブに渡される引数
  • modifiers - ディレクティブに付与された修飾子のオブジェクト

例えば、要素にディレクティブを付与する際に

<p v-test:foo.bar="3 * 3"><p>

と記述すると、各プロパティは以下のようになります。

  • binding.name => 'test'
  • binding.value => 9
  • binding.expression => '3 * 3'
  • binding.arg => 'bar'
  • binding.modifiers => { bar: true }

プロパティを活用したディレクティブの定義

bindingのプロパティを活用して、input要素に指定したデフォルト値を設定できるディレクティブ「v-default-input」を実装してみます。
しかし、ただデフォルト値を設定するだけでは面白くないので、「フォームからフォーカスが外れた時値が空だった場合にデフォルト値を入れ直す」機能も持たせます。
また、ページの初期描画時に自動でフォーカスがあたるようにするディレクティブ引数「:auto-focus」も設定可能にします。

Vue.directive('default-input', {
  inserted: (el, binding) => {
    // デフォルト値の設定
    el.value = binding.value;

    // フォーカスが外れた際に値をデフォルト値を再入力
    el.addEventListener('blur', () => {
      if (el.value === '') el.value = binding.value;
    });

    // 引数[:auto-focus]があった場合に要素に自動でフォーカスを当てる
    if (binding.arg === 'auto-focus') el.focus();
  }
});
<div id="app">
  <input id="test" type="text" v-default-input:auto-focus="'消すと復活するぞ!'">
</div>

ディレクティブに与えた値と引数を使って、多機能なディレクティブを実装できました!
ちなみに、今回のフック関数にinsertedを用いているのは、bindのタイミングでは対象の要素にフォーカスをあてることができないためです。

また、要素にディレクティブを付与している箇所で、値を'消すと復活するぞ!'とシングルクォーテーションで囲っているのは、ディレクティブが基本的にJavaScriptの式を期待するためです。
(※ そのため、値には{ test: 'foo', sample: 'bar' }といったオブジェクトリテラルを渡すことも可能です。)

コード全体と実際の動作は以下から確認してください。

See the Pen YmXPvB by shironeko-shobo (@shironeko-shobo) on CodePen.

動的にディレクティブ引数を扱う

ディレクティブの引数は、以下のように記述をすることで動的に設定できます。

<div v-test:[bar]></div>

このbarの箇所には、ディレクティブが紐づいた要素を含むコンポーネントのdatacomputedの値などを設定でき、その値に応じて柔軟に要素の挙動を制御することが可能になります。

例として、紐づいた要素を直径50pxの玉にするディレクティブv-ballを作ってみます。
このディレクティブには、玉の色を決定する引数colorを持たせ、また、色の変化をスムーズにする修飾子smoothを設定可能にします。

Vue.directive('ball', {
  bind: (el, binding) => {
    el.style.display = 'inline-block';
    el.style['background-color'] = binding.arg || 'black';
    el.style.width = '50px';
    el.style.height = '50px';
    el.style['border-radius'] = '50%';
    if (binding.modifiers.smooth) el.style.transition = '0.5s background-color ease';
  },
  update: (el, binding) => {
    el.style['background-color'] = binding.arg || 'black';
  }
});

const vm = new Vue({
  el: '#app',
  data: {
    // 動的な引数として与えるデータを保持
    color: 'black'
  },
  methods: {
    updateColor(el) {
      this.color = el.target.value;
    }
  }
});
<div id="app">
  <div v-ball:[color].smooth></div>
  <div>
    <input type="text" placeholder="white or #FFFFFF or rgb(255,255,255)"  @input="updateColor">
  </div>
</div>

サンプルのフォームに実際に色名を入力して、動作を確認してみてください。

See the Pen KONQOP by shironeko-shobo (@shironeko-shobo) on CodePen.

できましたね!今回の例のように、1つのディレクティブ定義オブジェクトに2つのフック関数を持たせることも可能です。
動的な引数として設定するデータはコンポーネント単位で用意できるため、使う場所に応じて軽い変化を与えたい場合に役立てられそうですね。

コンポーネント別に登録する

Vue.directiveAPIはカスタムディレクティブをグローバルに登録しますが、これをローカル(個々のコンポーネント)に登録したい場合は、コンポーネントのdirectivesオプションを使用することで実現できます。

// ディレクティブあり
Vue.component('hasDirective', {
  template: `
  <div id="app">
   <p>ここ<span v-important>テストに出る</span>ぞー。</p>
  </div>
  `,
  directives: {
    important: {
      bind(el) {
        el.style.color = 'red';
        el.style['font-weight'] = 'bold';
      }
    }
  }
});

// ディレクティブなし
Vue.component('noDirective', {
  template: `
  <div id="app">
   <p>ここ<span v-important>テストに出る</span>ぞー。</p>
  </div>
  `
});

const vm = new Vue({
  el: '#app'
});
<div id="app">
  <has-directive></has-directive>
  <no-directive></no-directive>
</div>

ここでは、最初の例で用いたv-importantディレクティブを流用しています。
directivesオプションを指定したhasDirectiveコンポーネントの文字だけ、強調表示されていることが分かりますね。

以下のサンプルを見てみると、ディレクティブオプションで


See the Pen
qeqYmw
by shironeko-shobo (@shironeko-shobo)
on CodePen.


ただ、カスタムディレクティブは特定のコンポーネントに依存しない汎用的な機能を持つことが多いため、アプリケーション全体を通して利用出来るよう、グローバルに登録することが一般的かと思われます。

終わりに

カスタムディレクティブは、アプリケーションで多用するDOMの操作を共通化できるとても便利な機能です。

DOM要素に動きをもたせたいけど、個別にコンポーネント化するまでもないかな...といった場面できっと役にたつはず!
使いどころを見極めて、今後の開発にも積極的に活かしていきたいと思います。

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

v-slotのサンプルコード(Vue.js初心者)

Vue.js公式サイトのスコープ付きスロットに、v-slotの解説とコード断片が載っていますが、 最初読んだ時、データの関係が私にはよく理解できず、試しにコード書いても変数のReferenceErrorで進まず。「サンプルコード全体(HTML+JavaScript)は一体どのように書けば動作するのか?」と躓いてしまいました。が、試行錯誤してやっとわかったのでここに書きます。

  • 対象Version: Vue.js 2.6.10+

躓いた箇所

この解説文。

親コンポーネント内でスロットコンテンツとして user を使えるようにするためには、 要素の属性として user をバインドします:

HTML
<span>
  <slot v-bind:user="user">
    {{ user.lastName }}
  </slot>
</span>

v-bindの両辺にuserが出てくるので、いざコードを書いていくと「自分が今ここに書いたuserは上の解説文でいうどれのこと?」と、データの関係があやふやな状態になり頭が混乱していきます。

わかった!

v-slotのコーディングを実演しているこの動画を観て、コツがわかりました!

Vue 2.6 First Look And V-Slot Tutorial!

完成したサンプルコード

See the Pen [Vue.js basics] v-slot example 1 by Kazuhiro Hashimoto (@kaz_hashimoto) on CodePen.

理解を助けるためのポイント

先にJavaScript側から。

JavaScript
Vue.component('current-user', {
  data: function() {
    return {
      xxuser: {
        firstName: 'Kazuhiro',
        lastName: 'Hashimoto'
      }
    };
  },
  template:`
<span>
  <slot v-bind:content="xxuser">
    {{ xxuser.lastName }}
  </slot>
</span>
`
});

new Vue({
  el: '#app'
});
  • v-bind:○○○="▲▲▲" の左辺と右辺で別々の名前を付けて、見た目で区別できるようにしておく。上記サンプルでは、左辺○○○をcontent、右辺▲▲▲をあえてxxuserとした。
  • dataプロパティはVue.componentの引数に渡すオブジェクト内で定義し、その値は関数リテラル。で、その関数の戻り値は、xxuserを要素に持つオブジェクト。
  • templateに記述するslotのコンテンツ{{...}}に入れるのは、v-bind:○○○="▲▲▲"の▲▲▲、つまりxxuser経由でアクセスする式。
HTML
<div id="app">
  <current-user>
    <template v-slot:default="slotProps">
      {{ slotProps.content.firstName }}
    </template>
  </current-user>
  <hr>
  <current-user></current-user>
</div>
  • v-slotの右辺の変数slotPropsでスロットプロパティを受け取る(名前は何でもよい)
  • Vue.componentで指定したdata: function() {...}の戻り値のオブジェクトが、slotPropsに相当するイメージ。
  • slotPropsからxxuserのメンバーにアクセスするには、v-bind:○○○="▲▲▲" に指定した○○○部分、つまりcontent経由でアクセスする。(ココが最初わからなくてハマった)

v-slotを使ったコーディングに慣れるまでは、自分にわかりやすいv-bind:content="xx..."形式でサンプルコードを書いて練習しようと思います。

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

Quasar(Vue.js)をTypeScript化してみる

Vue.jsでハイブリットアプリやPWAを開発するのにQuasarがとても便利そうなので、TypeScript化してみようと思います。

Quasarとは

Vue.jsでクロスプラットフォーム開発ができるフレームワークです。
SPAやPWA、SSR、Mobileアプリ(iOS、Android)、Desktopアプリなどに対応しています。
https://quasar.dev/

MobileアプリとしてはCordovaを使っているようなので、ReactNativeというよりは、IonicやOnsernUIに近いハイブリットアプリのようです。
UI Componentが豊富で、特にDesktopアプリやPC向けのSPAなどを開発する際にあると嬉しいコンポーネントが色々と用意されています。

TypeScript化する

1. Quasar CLIでアプリを作成する

チュートリアルに沿って下記コマンドを実行します。
https://quasar.dev/quasar-cli/installation

$ npm install -g @quasar/cli
$ quasar create hoge-app

2. package.jsonを修正する

Quasar CLIでサービスを稼働しやすくするため、npm scriptsに下記コマンドを追加します。

package.json
"scripts": {
  "dev": "quasar dev",
  "build": "quasar build",
  "build:pwa": "quasar build -m pwa"
}

3. Extensionを追加する

QuasarをTypeScritp化するためのExtensionを追加します。
※まだベータ版のようです。(2019/7/27時点)

$ quasar ext add @quasar/typescript

いくつか設定について聞かれますが、基本はrecommendedかYesで問題ないです。

? Please choose how to derive webpack: Use the fork-ts-checker-webpack-plugin module for type-checking (recommended)
? Rename .js files to .ts (experimental) Yes
? Will you use VSCode for this project? (Adds ESLint and Vetur configuration quirks, you must manually install the extensions) 
Yes
? Generate prettier configuration (ESLint and VScode, if used)? Yes

? Overwrite ".eslintrc.js"? Overwrite

4.TypeScriptライクな表記に変更する

デコレーターを使います。
併せてtsファイルをvueファイルから切り出します。

page/Index.ts
import Vue from 'vue';
import Component from "vue-class-component";

@Component({
  name: 'PageIndex'
})
export default class Index extends Vue {
  public message: string = "This is a test message";
}
page/Index.vue
<template>
  <q-page class="flex flex-center">
    <p>{{message}}</p>
  </q-page>
</template>

<script lang="ts" src="./Index.ts"></script>

5.サービスを起動する

npmコマンドでサービスを起動し、以下のような画面が表示されていればTypeScript化成功です。

Quasar_App.png

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