- 投稿日:2020-09-17T21:29:38+09:00
Dockerで【TypeScript+Vue+Express+MySQL】の環境を構築する方法~Vue編~
内容
この記事は、Vueを勉強しているけどTypeScriptベースでアプリを構築したい!という方へ向けて実際に一から環境構築していく内容となっております!
ついでにDockerで構築を行いコンテナ環境下で動くアプリを作成していこうと思います!構成としては Docker + Vue.js + Express(Node.js) + MySQLをTypeScript ベースで構築をしつつモジュール拡張性を持たせる環境を目指していこうと思います!
※初めての記事となりますので至らぬ点があるかと思いますが、優しくご指摘を頂けますと幸いです。
目指す状態
- アプリケーションコンテナ
- Vue + Vuex + Vuetify + TypeScript
- APIサーバーコンテナ
- Express + Sequelize + TypeScript
- データベースコンテナ
- MySQL
「Vue → Vuex → HTTP通信(Axios) → API(Express) → MySQLデータ取得(Sequelize) → Storeにレスポンス(Express) →Vue」
といった流れになります。対象者
- Docker,Docker-Compose がインストール済みである(利用可能な状態)
- JavaScript / Vue.jsの基礎を理解している
Docker上でさくっと環境構築することを目的にしておりDockerについて詳しく触れる内容ではございません。
Dockerってそもそも何?という方は下記の記事をご参考にしていただくことをオススメします。
【図解】Dockerの全体像を理解する -前編-作業手順
Vue 編
- アプリケーションコンテナ作成
- コンテナ上でVueアプリを構築
- コンテナ起動時にアプリ実行を自動化
- Vueの主要ライブラリを導入
- Vuetify 導入
- テスト用のカウンターアプリ作成
Express + MySQL 編 (※別記事作成中)
- DBコンテナ作成
- テストデータの投入
- APIサーバーコンテナ作成
- Express導入
- TypeScript化
- Sequelize 導入
- API通信でDBのデータをVueで表示
1. アプリケーションコンテナの作成
まずはdocker-composeでコンテナを作成するためのファイルを作成していきます。
下記の構成で必要なファイルを作成します。root ├─ docker │ └─ app │ └─ Dockerfile └─ docker-compose.ymldocker-compose.yml
version: "3" services: app: container_name: app_container build: ./docker/app ports: - 8080:8080 volumes: - ./app:/app stdin_open: true tty: true environment: TZ: Asia/Tokyoこちらが根幹になる docker-compose の設定ファイルです。
後からAPIサーバーコンテナ、DBコンテナを追加しますが一旦APPコンテナのみ作成していきます。Dockerfile
FROM node:12.13-alpine RUN yarn global add @vue/cli WORKDIR /app今回はベースイメージとして軽量なalpineを採用します。
コンテナ内でvue-cliを使うのでグローバルインストールを行っておきます。コンテナ・ビルド
$ docker-compose build先ほど作成した設計書を下にイメージをビルド
コンテナ・起動
$ docker-compose up -dビルドされたコンテナを実際に起動します。
-d をつけることでデタッチドモード(バックグランド)で起動します。起動中のコンテナを確認
$ docker ps下記が出力結果となりますので実際に起動されているかチェックしましょう。
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES // ここに作成したコンテナが表示されていればOK2. コンテナ上でVueアプリ立ち上げ
ここまででベースのコンテナができたので、実際にコンテナの中に入ってアプリを構築していきます。
コンテナにアクセス
$ docker exec -it app_container sh /app # // こうなってればコンテナにアクセスできていますVueアプリ作成
$ vue create appマニュアル選択
TypeScript あり
Router あり
Vuex あり
Babel なしバージョンは2.x。
Use class-style component syntax? Yes ※これ大事
yarn ※パッケージはYarnでいこうと思います。
あとはお好みで。起動
$ cd app $ yarn servelocalhost:8080 にアクセスして表示されればOK!
ここまででコンテナの中にアプリを作成し、コンテナの内部からアプリを実行するところまでが完了です。
毎回この作業は面倒なのでコンテナ起動時に中のアプリケーションも自動で実行される設定を行っていきます。一旦アプリを停止
$ controll + cコンテナから抜ける
$ exitコンテナの停止
$ docker-compose stop3. コンテナ起動時にアプリ実行を自動化
ディレクトリ構造を変更
root直下のappディレクトリはお邪魔なのでどこかに行っていただきます。
Before
root ├─ app // ここに移植(こいつは削除) │ └─ app // このディレクトリごと │ ├─ node_modules │ ├─ public │ ├─ src │ その他もろもろ │ ├─ docker │ └─ app │ └─ Dockerfile └─ docker-compose.ymlAfter
root ├─ app │ ├─ node_modules │ ├─ public │ ├─ src │ その他もろもろ │ ├─ docker │ └─ app │ └─ Dockerfile └─ docker-compose.ymlComposeの設定ファイルに起動時のコマンドを追加
docker-compose.yml
version: "3" services: app: container_name: app_container build: ./docker/app ports: - 8080:8080 volumes: - ./app:/app stdin_open: true tty: true environment: TZ: Asia/Tokyo command: yarn serve // こいつを追加コンテナ起動
$ docker-compose up -dこの状態でlocalhost:8080にアクセスしてアプリが立ち上がっていれば成功です!
yarn serveをしなくてもdockerコンテナが起動時にアプリも起動されるようになります!prettier の導入(お好み)
JavaScriptのセミコロンが嫌いなので自動整形してくれるPrettierを導入します。
このあたりはお好みでどうぞ。コンテナへアクセス
$ docker exec -it app_container shPrettierのインストール
$ yarn add prettier --devapp/.prettierrc 作成
{ "singleQuote": true, "semi": false }4. 主要なVueライブラリの導入
実はこちらが今回の本命。
VueをTypeScriptで記述しつつ、デコレータ(@)を使うことですっきりと直感的に記述が可能になるライブラリを導入します。
この先Vueファイルで実際に見慣れない書き方が出てきますが、これらのライブラリがうまいことすっきりさせてくれています。
- vue-class-component
- vue-property-decorator
- vuex-class
- vuex-module-decorators
$ yarn add vue-class-component vue-property-decorator vuex-class vuex-module-decorators下記の記事がとてもわかりやすいのでご興味ある方は是非お読みください。
App.vueを編集
src/App.vue
<template> <div id="app"> <router-view /> </div> </template> <style> #app { width: 95%; margin: 0 auto; } </style>表示するコンポーネントはRouter側に任せたいので router-link を削除し router-view に差し替えます。
不要なファイルを削除
├─ assets │ └─ logo.ping // 削除 ├─ components │ └─ HelloWorld.vue // 削除 ├─ views // ディレクトリごと削除 │ ├─ Home │ └─ About今後使わないファイルさんたちにはご退場いただきましょう。
Test.vue 作成
/pages/Test.vue
<template> <div class="test"> <h1>Test Page</h1> </div> </template> <script lang="ts"> import { Vue, Component } from 'vue-property-decorator' @Component export default class Test extends Vue {} </script>下記をチェックするためにテスト用のコンポーネントを作成します。
1. 正しくルーティングができるか
2. コンポーネントが正しく読み込めるかRouterを編集
/router/index.ts
import Vue from 'vue' import VueRouter, { RouteConfig } from 'vue-router' import Test from '../pages/Test.vue' Vue.use(VueRouter) const routes: Array<RouteConfig> = [ { path: '/', name: 'Test', component: Test, }, ] const router = new VueRouter({ mode: 'history', base: process.env.BASE_URL, routes, }) export default router作成したTestコンポーネントがルートで呼ばれるようにルーティングの設定を行います。
ページ確認
リロードして「Test Page」が表示されればOK!
この要領でURLとコンポーネントをつなげていけば自由にページを増やすことができます!5. Vuetify 導入
ライブラリのインストール
yarn add vuetify yarn add --dev sass sass-loader fibers deepmergevuetifyだけだとエラーが出るので必要なライブラリ群も合わせてインストールします。
プラグインファイルの作成
src/plugins/vuetify.ts
import Vue from 'vue' import Vuetify from 'vuetify' import 'vuetify/dist/vuetify.min.css' Vue.use(Vuetify) const opts = {} export default new Vuetify(opts)こちらはアプリ全体でVuetifyを適用するための設定ファイルです。
main.ts編集
import Vue from 'vue' import App from './App.vue' import router from './router' import store from './store' import vuetify from './plugins/vuetify' // 追加 import 'vuetify/dist/vuetify.min.css' // 追加 Vue.config.productionTip = false new Vue({ vuetify, // 追加 router, store, render: (h) => h(App), }).$mount('#app')上記で作成したプラグインを読み込むことでアプリケーション内でVuetifyが使えるようになります。
App.vue 編集
<template> <v-app> <!-- 追加 --> <div id="app"> <router-view /> </div> </v-app> <!-- 追加 --> </template>最後にアプリ本体のViewを囲ってあげればOKです。
Test.vue編集
<v-btn large color="primary">テスト</v-btn>これでボタンが表示されていればVuetifyの導入が完了です!お疲れ様でした!
あとはこの要領でページを自由に増やしていけばOKです!6. テスト用のカウンターアプリ作成
ここまででアプリケーションの雛形は完成したのでVuex/Storeが正しく使えるかチェックしがてらカウンター機能を作っていきます。
Counter.vue 作成
pages/Counter.vue
<template> <div class="counter"> <h1>Counter</h1> </div> </template> <script lang="ts"> import { Vue, Component } from 'vue-property-decorator' @Component export default class Counter extends Vue {} </script>Counterページの作成
- router/index.ts 編集
import Vue from 'vue' import VueRouter, { RouteConfig } from 'vue-router' import Test from '../pages/Test.vue' import Counter from '../pages/Counter.vue' //追加 Vue.use(VueRouter) const routes: Array<RouteConfig> = [ { path: '/', name: 'Test', component: Test, }, { path: '/counter', // 追加 name: 'Counter', // 追加 component: Counter, // 追加 }, ] const router = new VueRouter({ mode: 'history', base: process.env.BASE_URL, routes, }) export default router「localhost:8080/counter」 で上記のコンポーネントがレンダリングされるようにルーティングを設定します。
Storeモジュールの作成
store/modules/**counter.ts**
import { getModule, Module, VuexModule, Mutation, Action, } from 'vuex-module-decorators' import store from '../index' type CounterState = { count: number } @Module({ store, dynamic: true, namespaced: true, name: 'Test' }) class CounterModule extends VuexModule implements CounterState { count = 0 @Mutation SET_COUNT(payload: number) { this.count = payload } @Action async increment() { this.SET_COUNT(this.count + 1) } @Action async decrement() { this.SET_COUNT(this.count - 1) } @Action async reset() { this.SET_COUNT(0) } } export const counterModule = getModule(CounterModule)はじめに導入した「vuex-module-decorators」で直感的にStoreモジュールを記述することができます。
今回は "count" というデータを用いて Actionが発火された時に count が 動的に変化するかを確認していきます。tsconfig.json 編集
tsconfig.json
"experimentalDecorators": true, // 追加Linterに怒られる方はこちらを設定してください。
Counter.vue 編集
pages/Counter.vue
<template> <v-card> <v-container> <v-card-title>Counter</v-card-title> <v-divider /> <v-container> <v-avatar v-for="item in count" :key="item" color="primary"> <span class="white--text headline">{{ item }}</span> </v-avatar> </v-container> <v-divider /> <v-card-actions> <v-btn @click="increment">+</v-btn> <!-- クリックされた時にincrementメソッドを実行 --> <v-btn @click="decrement">-</v-btn> <!-- クリックされた時にdecrementメソッドを実行 --> <v-btn text @click="reset">Reset</v-btn> <!-- クリックされた時にresetメソッドを実行 --> </v-card-actions> </v-container> </v-card> </template> <script lang="ts"> import { Vue, Component } from 'vue-property-decorator' import { counterModule } from '../store/modules/counter' @Component export default class Counter extends Vue { get count() { return counterModule.count // Store の count をそのまま返す } /** * カウントをプラスする */ increment() { counterModule.increment() // Store の increment メソッドを発火 } /** * カウントをマイナスする */ decrement() { counterModule.decrement() // Store の decrement メソッドを発火 } /** * カウントをリセットする */ reset() { counterModule.reset() // Store の reset メソッドを発火 } } </script> <style lang="scss" scoped></style>ここは存分にVuetifyの恩恵にあやかるとしましょう。
先ほどの count の数だけ v-avatar が生成されるように編集したらあとは動かすのみ!動作確認!
[+] [-] を押してカウンターが増減すればOKです!
お疲れ様でした!簡単なカウンターを作成してVuex(Store)の挙動を確認してみましたが、StoreのAction内でAPI通信を入れていくことで実際のDBとの連携が可能になります。次回はAPIサーバーと実際に操作するDBのコンテナ作成を行っていこうと思います!
- 投稿日:2020-09-17T21:29:15+09:00
【Vue.js】イベント発生時にajax通信で取得した値が表示されずに嵌った話
はじめに
脱jQueryを目指すという流れで、ボタンをクリックしてajax通信を行う処理をVue.jsとaxiosで実装していたのですが、「変数の中身は変更されているのに表示に反映されない?なんで?」と嵌りました。無事解決しましたので、備忘録として書き残しておきます。。。
結論だけ言うと、「thenの後にはアロー関数を使おう」です。また当方htmlやjavascriptの初心者なので、以下に記述しているサンプルコードにはひどい書き方のものも多いと思いますが、目をつぶってやってください。
追記
javascript初心者の嵌りどころである thisのスコープの違い が原因で Vueに値が渡っていなかった 可能性が高いようです。意外とシンプルな原因だけど、気付きづらかった…。
jQueryでの書き方
今までこういう処理は以下のような書き方で慣れていました。ajaxでGETし、responseのdataを表示に反映させるというもの。(本当はPOST処理を書くことが多いですが、簡略化のためGETにしています。)
index.html<body> <div id="app"> <button id="display-btn">表示する</button> <div id="name-area"></div> <div id="age-area"></div> </div> <!-- my ajax script --> <script type="text/javascript" src="myscript.js"></script> </body>myscript.js$(function() { // ボタンが押されたらAPIを呼び出し、結果を表示する $('#display-btn').click(function() { $.ajax({ url: '/get-json', type:"GET", dataType:"json", timespan:1000, success: function(data) { // 通信成功 console.log('Success!'); const results = data.ResultSet; // 名前表示 var name = document.createElement("p"); var name_text = document.createTextNode("名前:" + results['name']); name.appendChild(name_text); document.getElementById("name-area").appendChild(name); // 年齢表示 var age= document.createElement("p"); var age_text = document.createTextNode("年齢:" + results['age']); age.appendChild(age_text); document.getElementById("age-area").appendChild(age); } }) .fail(function(data){ // 通信失敗 alert("ERROR!! occurred in Backend."); }); }); });返ってくるJSONデータは以下をイメージ。
{ "name": "たなか", "age": 22 }Vue.jsとaxiosで書いてみる
html側はこんな感じ。調べてみればすぐ分かる範囲ですし、問題ないかと。
- デリミタを
[[ ]]
にしていますが、これはAPIサーバをFlaskで書くことが多く、Flaskのテンプレートエンジン(Jinja2)のデリミタ{{ }}
とコンフリクトしてしまうために変更しています。- 参考にした記事:Flask で Vue.js を使おうと思ったら出鼻をくじかれたのでメモ - Qiita
index2.html<body> <div id="app"> <button v-on:click="getJson">表示する</button> <div v-if="data"> <p>名前:[[data.name]]</p> <p>年齢:[[data.age]]</p> </div> </div> <!-- my ajax script --> <script type="text/javascript" src="myscript2.js"></script> </body>axiosのresponseを受け取ったときにfunctionを使うと嵌った
ボタンをクリックしたらGETして取得したデータを表示するスクリプトを書いていきます。
そのとき、私は以下のページなどを参考に書いていたら嵌りました。結局は最適な記事が見つからず自己流で書いてたのが悪かったのですが…。
以下のように書いたら、コンソールからはきちんと変数が入っているのが確認できるのに、リアクティブになっていないのか知らんが画面表示が更新されないという現象になり、うまく行きませんでした。
コンソールでconsole.log(this.data)
に対してObserver
になっていないことも確認できたので、responseを代入する際にリアクティブではなくなっていることが原因なのかと思ったのですが、後からリアクティブにする方法を一生懸命探しても上手く行かずでした。myscript2.jsvar app = new Vue({ el: "#app", data () { return { data: null } }, methods: { getJson: function(){ axios.get("/get-json") // thenで成功した場合の処理を書ける .then(function(responce){ this.data = response.data; console.log(this.data); // オブジェクトが代入されたか確認 // catchでエラー時の挙動を定義する }).catch(function(error){ console.log('ERROR!! occurred in Backend.'); }); } }, delimiters: ['[[', ']]'] })アロー関数を使わないとダメだった
はい、解決しました。上と比べてもらうと違いが分かるでしょうか。
.then(function(responce){
と書くのが駄目だったらしく、.then(response => {
というように、=>(アロー関数)を使用するように修正すると、画面表示が更新されるようになりました。myscript2.js(修正版)var app = new Vue({ el: "#app", data () { return { data: null } }, methods: { getJson: function(){ axios.get("/get-json") // thenで成功した場合の処理を書ける .then(response => { // !!ココが修正箇所!! this.data = response.data; console.log(this.data); // オブジェクトがきちんと代入されたか確認 // catchでエラー時の挙動を定義する }).catch(error => { // !!ココが修正箇所!! console.log('ERROR!! occurred in Backend.'); }); } }, delimiters: ['[[', ']]'] })おわりに
後から調べると、methods内のaxiosでのajax通信ではアロー関数を使って書いていた記事が多かったわけですが。。。
なぜアロー関数じゃないとダメなのか、javascript初心者ですのでいまだによく分かっていません(よく調べきっていないというのもある)。知見者がいたら教えていただきたいです。thisのスコープの違いが原因だろうと教えていただいたので、「はじめに」に追記しました。
ただこの罠に嵌っていたとき、値をその場で宣言して代入したりすると上手く表示されたりして「本当になんなんだ……!?」と怒りに震えたりしましたので、もし同じ罠に嵌った人の参考になれば幸いです。また、以下のような記事も後から見つけました。フロントエンドもきちんと作るなら、このくらい構造的に書いた方がいいのかもしれないです。
- 投稿日:2020-09-17T17:31:13+09:00
Vue.js コンポーネント 一部が描画されないとき
この記事について
Vue.jsで素人がしょーもないミスで気づくのに時間がかかった間違いを忘れないための記事
一部が描画されない
main.html<div id="app"> <my-component v-on:child-click="numberPlus" v-bind:num="num"></my-component> </div>main.jsnew Vue({ el: '#app', components: { "my-component" : myComponent }, data: { num: 1 }, methods: { numberPlus: function() { this.num += 1 } } })component.jsvar myComponent = { template: ` <button v-on:click="clickHandler">イベント発火</button>数が増えるよ〜 {{num}} `, props: ["num"], methods: { clickHandler: function() { this.$emit("child-click") } } };
component.js
に記述しているボタンの横に「数が増えるよ〜」が表示されない。。。。原因
単一タグで囲んでいないから!!!
表示させようとする要素があるなら、それらを全て一つのタグで囲んであげないとコンパイル時にエラーが起きて、ブラウザに表示されない!!!component.jsvar myComponent = { template: ` + <div> <button v-on:click="clickHandler">イベント発火</button>数が増えるよ〜{{num}} + </div> `, props: ["num"], methods: { clickHandler: function() { this.$emit("child-click") } } };
- 投稿日:2020-09-17T16:17:22+09:00
CloudflareキャッシュでWebPを(無理やり)対応させる
前提
本来はブラウザのVaryヘッダーを見て
hogehoge.png
とリクエストが来ても
WebPに対応してる&WebP画像があればhogehoge.png.webp
を返してくれますが
執筆現在、CloudflareはサーバーからのVaryヘッダーを無視してしまい
ブラウザのacceptによってwebpかpng|jpegかをハンドリングすることができません。
そのため、半ば無理やりフロントエンドでそれを実現しようと思います。
ちなみに使う技術はnuxt(webpack+vue+scss)です。
※ サーバー側でWebPを内部リダイレクトで返す処理は割愛させていただきます。結論
画像のリクエストにWebP非対応のときだけクエリをつけることで
Cloudflare上で別リクエスト扱いさせ、WebPとそれ以外を別々にキャッシュさせます。JS
const CAN_USE_WEBP = (() => { const elem = document.createElement('canvas') if (elem.getContext && elem.getContext('2d')) { return elem.toDataURL('image/webp').indexOf('data:image/webp') === 0 } return false })() if (!CAN_USE_WEBP) { // webp非対応ならクラス付与 document.body.classList.add('no-webp') }Component
画像を表示するものはすべて共通のコンポーネントで表示するようにする。
あくまで下記は一例。ようするに通常のtypeとWebP用のtype二種類を用意してあげる。<template> <picture> <source v-for="(source, i) in sources" :key="`image-def-${_uid}-${i}`" :media="source.media || false" :srcset="source.srcset" type="image/webp" /> <source v-for="(source, i) in sources" :key="`image-webp-${_uid}-${i}`" :media="source.media || false" :srcset="source.srcset + '?nowebp'" :type="defaultMIME" /> <img :src="defaultSrc + '?nowebp'" :alt="alt" :type="defaultMIME" loading="lazy" /> </picture> </template> <script lang="ts"> import { Component, Prop, Vue } from 'nuxt-property-decorator' const MIME_REGX = /\.([a-z]+)$/ @Component export default class extends Vue { @Prop({ default: '' }) sources!: { srcset: string media?: string }[] @Prop({ default: '' }) alt!: string get defaultSrc() { return this.sources[this.sources.length - 1].srcset } get defaultMIME() { const src = this.defaultSrc const match = src && src.match(MIME_REGX) switch (match && match[1]) { case 'png': return 'image/png' case 'jpg': case 'jpeg': return 'image/jpeg' case 'gif': return 'image/gif' default: console.warn(`invalid extname [${src}]`) return '' } } } </script>SCSS
// mixin定義 @mixin webp-bgimage($url) { background-image: url($url); @at-root .no-webp & { background-image: url($url+'?nowebp'); } } // 背景画像を指定するところで .class { @include webp-bgimage('hogehoge.jpg'); } // そうすると下記のように2種類が指定される(.no-webpは上記JSで設定) .class { background-image: url('hogehoge.jpg'); } .no-webp .class { background-image: url('hogehoge.jpg?nowebp'); }Nuxt(nuxt.config)
画像のパスに
[query]
が付与されるようにするexport default { build: { filenames: { img: ({ isDev }) => isDev ? '[path][name].[ext][query]' : 'images/[hash:7].[ext][query]' } } }以上です。
JSでbackground-imageなどを動的に実装するときは上で判定したCAN_USE_WEBP
を取得して
画像パスに同じように?nowebp
をつけてあげるだけです。
- 投稿日:2020-09-17T16:17:22+09:00
CloudflareでWebPとそれ以外を別々にキャッシュさせる
前提
本来はブラウザのVaryヘッダーを見て
hogehoge.png
とリクエストが来ても
WebPに対応してる&WebP画像があればhogehoge.png.webp
を返してくれますが
執筆現在、CloudflareはサーバーからのVaryヘッダーを無視してしまい
ブラウザのacceptによってwebpかpng|jpegかをハンドリングすることができません。
そのため、半ば無理やりフロントエンドでそれを実現しようと思います。
ちなみに使う技術はnuxt(webpack+vue+scss)です。
※ サーバー側でWebPを内部リダイレクトで返す処理は割愛させていただきます。結論
画像のリクエストにWebP非対応のときだけクエリをつけることで
Cloudflare上で別リクエスト扱いさせ、WebPとそれ以外を別々にキャッシュさせます。JS
const CAN_USE_WEBP = (() => { const elem = document.createElement('canvas') if (elem.getContext && elem.getContext('2d')) { return elem.toDataURL('image/webp').indexOf('data:image/webp') === 0 } return false })() if (!CAN_USE_WEBP) { // webp非対応ならクラス付与 document.body.classList.add('no-webp') }Component
画像を表示するものはすべて共通のコンポーネントで表示するようにする。
あくまで下記は一例。ようするに通常のtypeとWebP用のtype二種類を用意してあげる。<template> <picture> <source v-for="(source, i) in sources" :key="`image-def-${_uid}-${i}`" :media="source.media || false" :srcset="source.srcset" type="image/webp" /> <source v-for="(source, i) in sources" :key="`image-webp-${_uid}-${i}`" :media="source.media || false" :srcset="source.srcset + '?nowebp'" :type="defaultMIME" /> <img :src="defaultSrc + '?nowebp'" :alt="alt" :type="defaultMIME" loading="lazy" /> </picture> </template> <script lang="ts"> import { Component, Prop, Vue } from 'nuxt-property-decorator' const MIME_REGX = /\.([a-z]+)$/ @Component export default class extends Vue { @Prop({ default: '' }) sources!: { srcset: string media?: string }[] @Prop({ default: '' }) alt!: string get defaultSrc() { return this.sources[this.sources.length - 1].srcset } get defaultMIME() { const src = this.defaultSrc const match = src && src.match(MIME_REGX) switch (match && match[1]) { case 'png': return 'image/png' case 'jpg': case 'jpeg': return 'image/jpeg' case 'gif': return 'image/gif' default: console.warn(`invalid extname [${src}]`) return '' } } } </script>SCSS
// mixin定義 @mixin webp-bgimage($url) { background-image: url($url); @at-root .no-webp & { background-image: url($url+'?nowebp'); } } // 背景画像を指定するところで .class { @include webp-bgimage('hogehoge.jpg'); } // そうすると下記のように2種類が指定される(.no-webpは上記JSで設定) .class { background-image: url('hogehoge.jpg'); } .no-webp .class { background-image: url('hogehoge.jpg?nowebp'); }Nuxt(nuxt.config)
画像のパスに
[query]
が付与されるようにするexport default { build: { filenames: { img: ({ isDev }) => isDev ? '[path][name].[ext][query]' : 'images/[hash:7].[ext][query]' } } }以上です。
JSでbackground-imageなどを動的に実装するときは上で判定したCAN_USE_WEBP
を取得して
画像パスに同じように?nowebp
をつけてあげるだけです。
- 投稿日:2020-09-17T15:22:05+09:00
今更きく…Vue3とは?
まえがき
最近、社内でVue2で作られているアプリケーションのUIを刷新する計画をしており、Vue3を導入するかの議論がされました。
2018年後半からVue3の開発が開始されてから約二年。
Vue3もRCとなり、リリースに向けて動きはじめました。
Vue3のComposition API等具体的な実装部分に触れている記事はよくみますが
大枠に触れている記事が少ないので書いてみます。EvanYou氏がある記事の中で、「なぜ書き換えたか?」について語っているので、私なりにまとめました。
なぜ書き換えたか?
新しい言語機能の活用
ES2015の最新版への対応が各ブラウザで行われており、Vueも対応する必要がありました
その中でも、Proxyが一番注目しており、VueでもProxyを活用することで、パフォーマンス改善を行うことができます。アーキテクチャの問題へ対応
Vueではコードが暗黙的な結合という形で、技術負債を積み上げてきました。
それにより、コントリビューターが変更を加えることが困難になっていました。
これらを解決し、コードを変更をしやすくする必要がありました。改善点
Typescriptのサポート
Vue2はもともとプレーンESで作成されていましたが、TypeScriptをサポートしました。
内部パケージの分離
monorepo(一つのリポジトリでパッケージを管理すること)で、内部パッケージ化を行い、それぞれが独自のAPI、タイプ定義、およびテストを実装しています。
それにより、モジュール間の依存関係をより明確にし、開発者がすべてを読み、理解し、変更しやすくし、プロジェクトの貢献の障壁を下げ、長期的な保守性を改善しました。RFCプロセスの設定
ユーザーが重大な変更についてフィードバックを提供できるよう、RFC(Request for Comments)プロセスを採用しました。
議論はGitHubリポジトリで行われ、提案はプルリクエストとして送信されるため、コメントで有機的に議論が展開されます。Vueはもともと、軽量のフロントエンドフレームワークですが、更に軽量化を行っています。
仮想DOMのボトルネックの改善
Vue 3では、適切なAST変換パイプラインを使用してコンパイラーを書き直しました。
これにより、コンパイル時の最適化を行っています。
- ブロック内のノード更新の際、ツリーのトラバースの最適化を行いました。
- この最適化は、実行する必要のあるツリートラバーサルの量を1桁減らすことで、仮想DOMのオーバーヘッドの多くを回避します。
- メモリ使用率軽減が大幅に向上し、ガベージコレクションの頻度が減少します。
- 要素レベルでは、コンパイル段階で実行計画の作成を行い、ランタイムがそれをヒントに実行を行うことで高速化を実現しています。
これらの手法を組み合わせると、レンダリング更新のベンチマークが大幅に改善されています。
Vue3がVue2のCPU時間(Javascript計算の実行に費やされた時間)の10分の1未満になることもあります。バンドルサイズの最小化
フロントエンドフレームワークは、サイズそのものがパフォーマンスに影響しています。
Vueはもともと軽量(圧縮しても約23KB)でしたが、2つの問題に気がつきました。
- 利用していない機能まで、ダウンロードと解析のコストが発生している。
- 機能を追加するにつれ、容量は無限に増え続ける。
この問題を解決するにはツリーシェイキングを行い、不要なコードを削除することでした。
Vue3では、ほとんどのグローバルなAPI、内部ヘルパーをESモジュールにすることで、これを実現しました。
多くの新機能の追加にも関わらず、Vue2の半分以下の容量を実現しています。まとめ
ここでは触れていませんが多くの機能が追加されています。
- Composition APIの導入
- multi-v-model機能追加
- Teleport機能追加
- Fragment機能追加
- Suspense機能追加
- フィルターの廃止
等々…
上記に加え、パフォーマンスの向上を考えると、導入しない手はないかなと個人的に考えており
下位互換性もあるとのことなので、正式版がでたら対応を考えたいと思います。
Composition APIの導入で、少しReact側によったことからReactでもいいのでは?という賛否両論の意見が出ていますが
軽量や学習コストの低さという部分では、Vueが依然として変わらないと思うので、棲み分けとしては十分できているのかと思ってたりします。今回は、ブログをもとに私が解釈した内容で書いているので、認識の齟齬等あれば教えて頂きたいです。
元ネタ:https://increment.com/frontend/making-vue-3/?ref=madewithvuejs.com#why-rewrite
- 投稿日:2020-09-17T15:05:05+09:00
プログラマ―用言語別チャットアプリを作ってみた
- 投稿日:2020-09-17T15:05:05+09:00
リアルタイムチャットアプリ(Nuxt.js+firebase)を作ってみた
- 投稿日:2020-09-17T14:56:39+09:00
vue-routerでのNavigationDuplicatedエラー
NavigationDuplicatedエラーとは
vue-routerでの遷移時、現在いるrouteと同じrouteに移動しようとすると発生する。
このエラーで検索するとrouter.push
などの戻りをcatchすることでエラーを出さない(無視する)方法を挙げている方がいるが、そもそも現在いるrouteと同じrouteに遷移する必要はないはずなので遷移しないというのが正しいんじゃないかなと思う。遷移先が現在と同じrouteの場合遷移しない
<script> export default { methods: { transition: function() { if (this.$route.name !== this.name) { this.$router.push({ path: 'home' }) } } } } </script>名前付きルートの場合
<script> export default { props: ['name'], methods: { transition: function() { if (this.$route.name !== this.name) { this.$router.push({ name: this.name }) } } } } </script>
- 投稿日:2020-09-17T10:55:06+09:00
ローカルで使っているWebアプリを外部公開してみた
SEなので、仕事ではもちろんですが、プライベートでもプログラミングをしてはアプリ開発しています
ですが、ほぼほぼ「自分が使えればいいやー」くらいの、あらゆる意味でかなりお粗末なアプリです
せっかくVue+GitHub Action+GitHub Pages+Slackを使ってCIを構築したので、適当にローカルで使っているWebアプリを見繕って外部公開してみました公開したWebアプリ
カレーの具材がガチャれるアプリです
アプリ名は特につけてないので、そのまま「カレー具材ガチャ」にしました
URLもGitHub PagesのURLそのままです見た目修正や機能追加で、もうちょっといい感じになったら、ちゃんとしたアプリ名をつけて、ドメインを取得してカスタムします
縛り
そのまま公開したら結局自分しか使えないので、以下の制約を設けました
誰かが扱える様にする
誰でも扱える様にするのは、デザインやUI、UXの勉強が必須(だと思う)なので、とりあえずは私以外の誰かが説明無しで使えるレベルにしました
具体的には以下のことをしました
- デザインを当てる
- 「ガチャで出てくる具材」「具材の個数」「ガチャるボタン」「ガチャ結果」が最低限わかるようにする
- 色合いやテーマはとりあえずスルー(なので、見た目がカレーっぽい雰囲気がない)
- デザインそのものは極力CSS(今回はless+bootstrapなどのcssパッケージ)だけで実現する
- 簡単なアイコンをつける
- ローカルでは文字だけだったので、つまらなく感じやすそう&かなり見辛い
- fontawesomeを使用したが、パーツ不足なのでsvgでアイコン自作するかパッケージを追加する予定(svgは未対応)
誰でも扱えるよう準備、正確かつ素早く反映できる環境にする
とはいえ外部公開するので、使って頂いたユーザから何らかのご意見をもらえると思います(バグや要望など、より扱いやすくなる種になるもの)
基本的にしたことはCIの構築です技術的なこと
ここからは縛りにある内容を実現する上で、躓いたところやそれの対応方法、モヤモヤしているところを書き出しています
最終的なソースコードはこちらで公開していますコンポーネントでimportしているのに、スタイルが反映されない問題
1コンポーネントに1ファイルのlessファイルを用意していますが、インポートしているのにも関わらずlessファイルにかかれているスタイルが反映されませんでした
その時のlessファイルは↓になります// 反映されない .class-name { color: blue; } // 反映されない #id-name { color: red; } // これは反映された div { color: green; }ディベロッパーツールを開いて、cssのセレクタとクラス名、ID名を確認したら、反映されないcssのセレクタにだけランダムっぽい文字列がサフィックスされていました
イメージとしては↓になります(その時のcssを保存し忘れたので、細かいところが誤っているかもしれません・・・).class-name__hogehoge_ { color: blue; } #id-name__hogehoge_ { color: red; } div { color: green; }https://developers.karte.io/docs/understand-css-modules を見ると、
:global(.class-name)
で書くとcss側のサフィックスが抑制されるので、これを臨時で反映して、スタイルが反映されるようにしました
ですが、毎回これをするのは面倒なので、今のうちに止めたいです(html
側でサフィックスされないのが問題?):global(.class-name) { color: blue; } :global(#id-name) { color: red; }抽選対象の具材アニメーションがカクつく問題
抽選対象の具材とガチャ結果部分にアニメーションを付けていますが、抽選対象の方だけアニメーションがカクつきます
Vue.jsのトランジションにある<transition>
をガチャ結果の方に、<transition-group>
を抽選対象の方に使っているので、<transition-group>
がカクついていることになります
カクつくこと以外は期待通りのアニメーションをしている気がするので、とりあえずは保留にしていますが、もうちょっと滑らかに動かせないのかは模索しています最後に
vueの公式ガイドが充実していたので、vue起因によるつまづきはほとんどありませんでした
むしろ今までおろそかにしていたcss部分で色々躓いてた気がします
- 投稿日:2020-09-17T08:34:18+09:00
生産ラインのエラーを可視化してみる
何処でエラーが出て止まっているんだ?
私は工場に勤めていて設備が故障で停止した際の対処も仕事の一つです、長い生産ラインどこでエラーが出て止まているのか、今まではエラーが出ても現場にある表示機に映すだけで、現場に行かなければどんなエラーがどの場所で出ているかわからない(積層灯でなんとなく場所はわかっても細かい内容まではわからない)、、、ライン内の「どこで」「どのような」エラーがでて止まているのか可視化することで、現場についてから考えないですむなどのメリットがあったり、マネジャーであれば現場の状況を把握したいなどあると思います。
かっこよく映したい
生産数や稼働率は文字やグラフで出しているけど、文字やグラフ表示で「文字を読まなくてはならない」と思います。
A-Frameを使って3D表現で可視化して設備に紐づけてラインを横断して映し出すことができれば、「稼働率の低い機械はどこ?」なんてことも一発で解決できる!あと3D表示されているDashboardは「かっこいい」と思い挑戦してみました。作ったもの
https://eloquent-shockley-90175d.netlify.app/
生産ラインのエラーの可視化に挑戦してみました!3Dは苦手意識がありましたが何とか形はできた!#Protoout #JavaScript #NodeRed #RaspberryPi https://t.co/cHfZRQhQB5 pic.twitter.com/MtdfMMO4Ft
— Toshiki (@Hirasawa1987) September 16, 2020使用した技術
- Vue.js
- A-Frame
- Firebase
- axios
- Raspberry Pi4(ロボットと仮定して使用)
- Node-RED
構成図
コード
コードはこちらにあります
https://gist.github.com/Toshiki0324/4a84065db800f717eaa2cb6228b6157d躓いた部分
3Dモデルの読み込み
今回は無料でダウンロードできる3Dモデルを使用しました、実際に工場で使用するときにはCADで書いたものを
.obj
や.gltf
といった拡張子にエクスポートして使います、使用できるファイルの種類は公式サイトに記載されています。
作ったhtmlファイルをブラウザから開いても表示されない・・・という現象に陥りました、原因はいまいちわかりませんでしたが、以下の方法で解消しました。要素の位置調整
要素の動かし方にも悩みました、テキストをどのあたりに出すか、線をどこからどこに引くかなど、どうしたらいいんだと悩んでいたら
<ctrl> + <alt> + i
で編集ができるという文言を発見、公式サイトにも載っていました。Visual Inspector と言うらしいです。
左側で要素のを選択でき、右側で調整したりできます。
ちなみに終了すると元に戻るので、いじった項目と数字はメモかGyazoしましょう、めちゃくちゃ便利です。謎の警告
いざ3D表現を完成させた後Consoleを見てみると警告だらけ。
以下のものが連続して出ていました。[Vue warn]: Unknown custom element:
- did you register the component correctly?
For recursive components, make sure to provide the "name" option.こちらの警告は以下で解決しました。
Vue.jsで使う場合は
Vue.config
に設定が必要みたいです。データの読み込み
今回はFirebaseのCloudFirestoreを使用しましたmemosというコレクションIDにデータを作成したのでmemosのデータを監視して
変更があったら読み込む形でリアルタイムに反映されるようにしました。
以下はCloudFirestoreの内容です。app.jsdb.collection('memos') .onSnapshot(function (querySnapshot) { for (let change of querySnapshot.docChanges()) { if (change.type === 'added') { // データが追加された時 } else if (change.type === 'modified') { // データが変更された時 console.log("!!!!!!!!!"); location.reload(); } else if (change.type === 'removed') { // データが削除された時 } } })firestoreのデータに変更があった場合の処理に参考に察せていたできました
エラーはRaspberryPiで
実際に工場の設備からデータを吸い上げることを考えて今回は
RaspberryPi4
を使用しました。ラインが実際のものではないので今回は設置は行いません、Node-RED
を使用しUIを作成して疑似的なエラーを想定し実装しました。
以下のノードを使用しています[{"id":"8d8035c6.7855c8","type":"tab","label":"フロー 1","disabled":false,"info":""},{"id":"82b42594.911fe8","type":"debug","z":"8d8035c6.7855c8","name":"debug","active":true,"tosidebar":true,"console":true,"tostatus":true,"complete":"true","targetType":"full","statusVal":"payload","statusType":"auto","x":540,"y":280,"wires":[]},{"id":"25fc6b77.52d974","type":"Firestore out","z":"8d8035c6.7855c8","name":"","collection":"memos","document":"japan","operation":"set","admin":"7db223e2.29c07c","eject":false,"x":390,"y":280,"wires":[["82b42594.911fe8"]]},{"id":"8d096c01.5f39d","type":"ui_button","z":"8d8035c6.7855c8","name":"","group":"2b01090b.1c7886","order":0,"width":0,"height":0,"passthru":false,"label":"エラー","tooltip":"","color":"","bgcolor":"","icon":"","payload":"{ \"age\": \"エラー発生中\", \"name\": \"test0915\", \"flag\": true }","payloadType":"json","topic":"","x":210,"y":260,"wires":[["25fc6b77.52d974"]]},{"id":"16ddc24c.589f5e","type":"ui_button","z":"8d8035c6.7855c8","name":"","group":"2b01090b.1c7886","order":1,"width":0,"height":0,"passthru":false,"label":"解除","tooltip":"","color":"","bgcolor":"","icon":"","payload":"{\"age\":\"\",\"name\":\"test0915\",\"flag\":false}","payloadType":"json","topic":"","x":210,"y":300,"wires":[["25fc6b77.52d974"]]},{"id":"7db223e2.29c07c","type":"firebase admin","z":"","name":""},{"id":"2b01090b.1c7886","type":"ui_group","name":"Group 1","tab":"792f850.1442c7c","order":1,"disp":true,"width":6},{"id":"792f850.1442c7c","type":"ui_tab","name":"Tab 1","icon":"dashboard","order":1}]同じWi-Fi環境であればこんなかんじ↓でラズパイのipアドレス
http://192.168.1.〇〇:1880/ui/
にアクセスできUIが表示されます。CodePenでグラフを探す
3Dのみの表示だと味気ない気がしたので、今回はCodePenから素敵な見た目のものを持ってきました
https://codepen.io/amcharts/pen/gbLpMR
「カッコイイ!!」までは行けなかった
今回は1ヶ所しかエラーを実装できませんでしたしかも「エラー発生中」せっかくVue.jsを使用したので内容も表示できればよかった、ですが3Dで可視化できたことはうれしかった!作りこんでいくことでライン全体、若しくは工場全体を表示することも可能になりそう。
- 投稿日:2020-09-17T06:44:44+09:00
DOMとは 〜ググわか〜
DOM::「Document Object Model」::(ドキュメント オブジェクト モデル)
DOMとはこう言うもの
一つググってもの分からないのでいろんなサイトの一言まとめを集めてみた!
"DOMとはマークアップがなされたリソース(Document)をリソース要素(Object)の木構造(Model)で表現し操作可能にする仕組み、またそのモデル"
"DOMとはJavaScriptでhtmlの要素を操作するための仕組みのこと"
"DOMはブラウザがWebページを解釈したもの"
"DOMは、文書のためのプログラミングAPI"
う〜ん、意味がよくわからない・・・
と言うわけで! いろいろなまとめから分かりやすくまとめてみた。
つまり・・・
1、Webページの基本的なHTML構造を表現したものでページごとに発行されている
2、ツリー構造(のモデル)で表現されていること
3、jqueryやVue.jsとかは、これを参照して操作する対象を探すことができる!
と言うこと!
※初心者向けにイメージしやすく書いているので、多少の語弊があるかもしれません。実際に見てみよう!
見るのは簡単!DOMを調べるくらいのあなたなら、すぐに理解できる・・・はず!
このページを右クリック → 検証 → Elements(検証クリックしたら最初に出てくる左側の部分)
これこれ!
単純なモデルですが、<html>のなかに<head>と<body>が入っていて、
<head>...</head>の中にはたくさんの設定が入っていたり、
<body>...</body>の中にもdivとかaとかpとかいろいろな要素が階層的(ツリー構造)になって書かれていますね!
もはやツリー構造の説明になってる・・・
htmlでidやclassをつけると、それがDOMに反映され、目印になっていて、JSなどの言語によってブラウザ上で操作ができるわけです!言い換えると、ブラウザ上での何かを変更する際は、JSでDOMにアクセスし、その内容やプロパティを変更しているわけです。
ソースコードを書き換えるわけではありません。
もう一度まとめると
DOMとは、
ページごとに発行されるページの詳細な内容を操作可能にするもの
であることがわかった!
- 投稿日:2020-09-17T02:17:58+09:00
pandocをやめて、md-to-pdfとvscodeでマークダウンからPDFを作成した
はじめに
技術書展9で本を頒布する際に
md-to-pdf
を使ったので、備忘録もかねて、書きたいと思います。初めて、技術書展を見に行ったのが第4回、当時エンジニアですらなかったのに「いつかは技術書展で本を頒布してみたいな」と思って、技術通の人たちがごった返す秋葉原の会場にで考えていました。
それから2年以上経ち、今回はオンラインのみの開催ですが、技術書典9でやっと「実践 Vue.jsでスマホアプリをつくろう」という本(PDF)を頒布しました。
もし興味のある方は手にとってみてください!
初めて頒布したということもあり、思い入れも強く細かいところにこだわるために、色々悩んだことを残します。
概要
執筆環境は、ソースコードもあったため開発と並行するため
vscode
を選択しました。
また、マークダウンで書けば最悪PDFにどうやっても変換できるだろうと考え、マークダウンで書くことにしました。Wordとか使うとマークダウンでもなくて良いのかなと思います。
Pandocを最初に選択したが。。。。
マークダウンをPDF化するにあたり、最初に候補にあがるのが
pandoc
です。私もpandocでイケると思ってました、当時は。
正直言うと、カスタマイズにするには学習コストが少し高い印象を受けました。「ページ番号付けたいのに」とか「章の見出しをカスタマイズしたい」という時に一々調査してという感じだったので諦めました。
修得できれば便利だとは思います。
md-to-pdfに出会う
何か、シンプルで使い方がわかりやすいものがないかなと思い、MarkdownをPDFに変換する「md-to-pdf」は痒いところに手が届く素敵ツールでmd-to-pdfを見つけました。困った時のクラスメソッドさんw
md-to-pdfはシンプルなマークダウンからPDFに変換するライブラリです。npmでインストール可能です。
npm i -g md-to-pdf
下記のように、対象のマークダウンを指定して、スタイルシートを当てることできます。
求めているのは、最低限のマークダウンを変換し、スタイルシートでデザインをつけること。md-to-pdfがもっとも合っているように感じました。
npx md-to-pdf tmp/*.md --stylesheet styles/style.css
devtools
オプションをつけることで、ChroniumでHTMLとCSSの状態を確認することも可能です。
一々、PDF変換して確認だと時間がかかるので、地味に便利です。※ホットリロードは効かないです。
npx md-to-pdf <マークダウン> --devtools --stylesheet styles/style.css"工夫した点
表紙と裏表紙はあとでマージする
md-to-pdfでは、冒頭に設定を書くことができ、ページのサイズや余白を指定できます。
--- pdf_options: format: A4 margin: 20mm 10mm displayHeaderFooter: true ---しかし、これが全部に入ると表紙や裏表紙にも余白が入ってしまいます。なので、表紙と裏表紙は別で
md-to-pdf
でpdf化し、その後PDFをマージしました。手作業になってしまったところ
目次や章番号が手動になってしまいました。
markdown-index
やMarkdown TOC
で自動的で作成することができるのですが、章番号が1.1.1
のようなフォーマットだったり、目次にスタイルを当てる関係で手動にしました。おわりに
本を執筆するのも大変だけど、装丁も大変でした。。。
表紙・裏表紙の設定、目次、章番号、著者紹介、章ページなど思ったよりもデザインする箇所がたくさんあります。シンプルなツールにして、CSSでカスタマイズが簡単にできる
md-to-pdf
一度試してみてください!
- 投稿日:2020-09-17T01:12:11+09:00
[Vue.js] 名前付きスロットとスコープ付きスロットを同時に使うには
はじめに
Vue.jsでは、バージョン2.6.0でv-slotディレクティブが導入されました。これは名前付きスロットとスコープ付きスロットを統一的に扱うことのできるディレクティブなのですが、ドキュメントを読んだときに少しこんがらかったので頭の中を整理することを兼ねて投稿します。
v-slotディレクティブ
v-slotディレクティブの書き方は以下のとおりです。
v-slot:{スロット名} = "{子コンポーネントからのデータを受け取る変数}"これを親コンポーネントのテンプレート内のカスタムタグに記述します。名前付きコンポーネントのみを使うときはイコール以降は不要です。
具体例
以下に具体例を示します。
main.htmlでは、名前、スコープ付きのスロット(ヘッダー)、名前付きスロット(フッター)を指定しています。
ヘッダーに関しては、子コンポーネントからmessageを受け取っていることが分かります。名前、スコープ付きのスロットでは、v-slotを以下のように書いています。ここでは、headerがスロット名、slotPropsが子コンポーネントからのデータを受け取る変数です。
v-slot:header="slotProps"slotPropsには以下のようなオブジェクトが格納されます。オブジェクトのプロパティ名(ここでは"message")は、v-bind:messageのmessageです。すなわち、v-bind:{オブジェクトのプロパティ名}となります。
{ "message": "hello from child!" }main.html<!DOCTYPE html> <html lang='ja'> <head> <meta charset='UTF-8'> </head> <body> <div id="app"> <child-component> <template v-slot:header="slotProps"> <p>{{ slotProps.message }}</p> </template> <template v-slot:footer> <p>FOOTER</p> </template> </child-component> </div> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script src="main.js"></script> </body> </html>main.jsconst ChildComponent = { template: ` <div> <header> <slot name="header" :message="message">DEFAULT HEADER CONTENTS</slot> </header> <footer> <slot name="footer">DEFAULT FOOTER CONTENTS</slot> </footer> </div> `, data(){ return { message: "hello from child!" } } } const app = new Vue({ el: "#app", components: { ChildComponent } })実行結果
hello from child! FOOTER