- 投稿日:2019-07-28T23:51:31+09:00
【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を表示します。
何も表示されません。辛いです。
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を再起動すると無事に表示されました。
参考
- 投稿日:2019-07-28T23:24:09+09:00
NuxtのHTML templateはpugを使わないメリットもある
こんにちは、プログラミングスクールのレビューサイト「スクールレポート」を運営しているアカネヤ(@ToshioAkaneya)です。
NuxtのHTML templateはpugを使わないメリットもある
タイトルの通りです。pugは誰でも綺麗にコードがかけるため素晴らしいのですが、エディターの保管が素のHTMLに比べて弱い
という傾向があります。僕のしようしているIntelliJでもその傾向があります。(ちゃんと設定すれば問題ないのかもしれませんが...)その点HTMLの補完は正確です。この記事が参考になれば幸いです。
終わりに
Ruby on RailsとVueで作成したプログラミングスクールのレビューサイトを運営しています。良ければご覧ください。https://school-report.com/
- 投稿日:2019-07-28T21:35:48+09:00
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操作でページを変化させることができる
まとめ
仮想DOMとは、DOMと対になる構造体で、ReactやVueは、仮想DOMの差分をチェックし、DOMの再構築を最小限に抑えてくれる!
- 投稿日:2019-07-28T21:21:40+09:00
'jQuery' is not defined を解決する。
- 投稿日:2019-07-28T20:59:16+09:00
[vue-router] BeforeRouteLeaveが2度発火する
TL;DR
Vue-Router
のBeforeRouteLeave
等ナビゲーションガードが二回発火する場合がある。- 遷移先のコンポーネントに何らかのリダイレクトがあるときに発生する。
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
トップページ - ページ1 - ページ2という構成で、
- ページ2のバックボタンを押すと、
window.confirm()
を表示する。「OK」を押すとページ1に遷移する。- ページ1では、ページ2からの遷移をキャッチするとトップページに飛ばす
想定としては、例えば商品販売サイトにおいて、ページ2が注文完了画面で、ページ1が決済確認ページ、くらいのとき。
しかしこのとき、ページ2のBeforeRouteLeave
が2度発火する。window.confirm()
のOKを1度押しても再度ダイアログが表示される。どうして起こるか?
予想
2 => 1, 1 => top のような遷移をしそうな気がする。
実際
実際のページ2のbeforeRouteLeave
はこのように挙動する。
beforeRouteLeave
が 2=>1 と 2 => top の2回分呼ばれる。引数のfrom.name
とto.name
の中身を調べると確かにそうなっている。1=>top へのリダイレクトがこの2回目の遷移を誘発させる。このとき1=>topの遷移でもない! あくまで from="2", to="top"のbeforeRouteLeave
が走る!解決策
ページ2
のbeforeRouteLeave
のto
を検査し、一回目の遷移についてガードしないようにすれば良い。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などを引用して適宜コメントに追記することを推奨する。
- 投稿日:2019-07-28T19:36:03+09:00
docker-composeを使ってVeu.jsのプロジェクトを作成して、Dockerで動かしてみた
docker-compose
を使ってVeu.js
のプロジェクトを作成して、Dockerで動かしてみました。プロジェクトを作成するディレクトリを作成する
directory/hoge_directoryプロジェクトを作成するための
docker-compose.yml
を作成するdirectory/hoge_directory |_ docker-compose.ymldocker-compose.ymlversion: '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) YYESを選択? Generate project in current directory? (Y/n) Ydefaultを選択? Please pick a preset: (Use arrow keys) ❯ default (babel, eslint) Manually select featuresYarnを選択? 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プロジェクトの作成完了!!!
docker
でVue.js
を動かすDockerfileを作成する
directory/hoge_directory |_ node_modules |_ public |_ src |_ .gitignore |_ babel.config.js |_ docker-compose.yml |_ package.json |_ README.md |_ yarn.lock |_ Dockerfile <- 作成DockerfileFROM node:12.7.0-alpine WORKDIR /myapp COPY package.json ./ COPY yarn.lock ./ RUN yarn install
docker-compose.yml
を修正するdocker-compose.ymlversion: '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アクセスする
URLhttp://localhost:8000/終わり
マウントした時に、
node_modules
が上書きされて、削除されるのは、
なかなかハマりました...
- 投稿日:2019-07-28T19:36:03+09:00
docker-composeを使ってVue.jsのプロジェクトを作成して、Dockerで動かしてみた
docker-compose
を使ってVue.js
のプロジェクトを作成して、Dockerで動かしてみました。プロジェクトを作成するディレクトリを作成する
directory/hoge_directoryプロジェクトを作成するための
docker-compose.yml
を作成するdirectory/hoge_directory |_ docker-compose.ymldocker-compose.ymlversion: '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) YYESを選択? Generate project in current directory? (Y/n) Ydefaultを選択? Please pick a preset: (Use arrow keys) ❯ default (babel, eslint) Manually select featuresYarnを選択? 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プロジェクトの作成完了!!!
docker
でVue.js
を動かすDockerfileを作成する
directory/hoge_directory |_ node_modules |_ public |_ src |_ .gitignore |_ babel.config.js |_ docker-compose.yml |_ package.json |_ README.md |_ yarn.lock |_ Dockerfile <- 作成DockerfileFROM node:12.7.0-alpine WORKDIR /myapp COPY package.json ./ COPY yarn.lock ./ RUN yarn install
docker-compose.yml
を修正するdocker-compose.ymlversion: '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アクセスする
URLhttp://localhost:8000/終わり
マウントした時に、
node_modules
が上書きされて、削除されるのは、
なかなかハマりました...
- 投稿日:2019-07-28T18:30:55+09:00
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として機能していることはわかった。
- 投稿日:2019-07-28T18:26:30+09:00
LodashでObjectをマージする方法
VueとかVuexでObjectをlodashを使って、必要なところだけマージしたい時に何回か調べるのでメモまでに。
lodashとかをあまり知らない人からすれば同じだけれど、よく知っている人からしたら実装意図がわからなくなってしまう問題。
ただのObjectsをマージするなら
_.defaults();prototype chainまで見るなら(Object.assignと似てる)
_.assign();
- 投稿日:2019-07-28T18:12:58+09:00
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.jsimport 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-center
やjustify-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.jsmodule.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
を選択します。
反映されると、
https://"ユーザー名".github.io/"リポジトリ名"/
のURLでアクセスできるようになります。
- 投稿日:2019-07-28T16:59:19+09:00
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>以上。
- 投稿日:2019-07-28T16:57:01+09:00
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.0vue.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 devlocalhost:8080にブラウザからアクセスしてみてください。Vueアプリケーションの土台ができており、
下記の画面が表示されるはずです。これで環境構築は終了です。
お疲れ様でした。基本構文
いろんなファイルが出来ててキョどりますが大丈夫です。
基本的に難しい設定をしなければ、だいたい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.jsimport 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 axiosmain.jsにてaxiosを取り込みます。これですべてのコンポーネントにおいて「this.$axios」でaxiosが利用できるようになります。
main.jsimport 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-bind
とprops
を使う(props down)- 子 -> 親へデータを受け渡す際には
$emit
とv-on
を使う(events up)最後に
長くなりましたが今回はこの辺で、最後まで読んでいただきありがとうございました。
もし、間違い等、アドバイス、ご指摘等有れば教えていただけたら幸いです。次回はVue-routerの使い方についてです。
今後はVuex,Nuxt.jsについて触れていきたいと思います。
- 投稿日:2019-07-28T11:38:24+09:00
【Vuex】firebaseでtwitterログインし、Vueインスタンス生成時に認証状態をストアに格納することでログイン維持する
Vuexでログイン認証の状態管理をしたい
vue.jsとfirebaseでwebサービス開発をしています。
ログイン認証機能を実装するにあたって状態管理にハマりまくりました。大筋は、下記のチュートリアルを参考に行なっております。
Vue + Vue Router + Vuex + Laravelで写真共有アプリを作ろう (6) 認証機能とVuextwitterでは何度紹介したかわかりませんがめちゃくちゃわかりやすいです。。本当に感謝
こちらのチュートリアルに沿って実装つつ、認証機能を、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.jsconst 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.jsconst 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
- 投稿日:2019-07-28T11:10:29+09:00
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
ここのロジックをメソッド化してもいいけど、めんどいのでこう書いた。
- 投稿日:2019-07-28T11:01:49+09:00
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: [] }
- 投稿日:2019-07-28T06:52:45+09:00
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-codegeneratorNodeの設定とモジュールのインストール
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.tsdeclare module '*.vue' { import Vue from 'vue'; export default Vue; }VueファイルをTSとして読み込むために必要
Webpackの設定
開発と本番で設定ファイルを分ける。共通部分は
common
にいれる。webpack.common.jsconst 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.jsconst 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.jsconst 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.csusing 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.tsindex.tsimport 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 --referenceScriptLibrariesControllers/SampleCOntroller.csusing 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.tsimport 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.参考
- TypeScript Vue Starter(Github)
- Webpack Production
- ASP.NET Core + Node (Microsoft Docs)
- TypeScriptでVue.jsを書く
- 投稿日:2019-07-28T02:48:17+09:00
Vue.jsでカスタムディレクティブを実装する
はじめに
Vue.jsには、
v-if
やv-model
、v-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
の箇所には、ディレクティブが紐づいた要素を含むコンポーネントのdata
やcomputed
の値などを設定でき、その値に応じて柔軟に要素の挙動を制御することが可能になります。例として、紐づいた要素を直径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.directive
APIはカスタムディレクティブをグローバルに登録しますが、これをローカル(個々のコンポーネント)に登録したい場合は、コンポーネントの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要素に動きをもたせたいけど、個別にコンポーネント化するまでもないかな...といった場面できっと役にたつはず!
使いどころを見極めて、今後の開発にも積極的に活かしていきたいと思います。
- 投稿日:2019-07-28T01:12:25+09:00
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側から。
JavaScriptVue.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..."
形式でサンプルコードを書いて練習しようと思います。
- 投稿日:2019-07-28T00:26:08+09:00
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-app2. 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"? Overwrite4.TypeScriptライクな表記に変更する
デコレーターを使います。
併せてtsファイルをvueファイルから切り出します。page/Index.tsimport 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化成功です。