20190507のvue.jsに関する記事は10件です。

Vue.jsでinput type="file"で選択した画像をプレビューで出す方法。

おはようございます。かけるです。
今回は、vue.jsとhtmlでinput type="file"で選択した画像をプレビューをだす方法を紹介したいと思います。
2019-05-07 (10).png
こんな感じのドラッグアンドドロップもできるものを作ります。

コード全体

        <div class="imgContent">
          <div class="imagePreview">  
          <img :src="uploadedImage" style="width:100%;" />
             <input type="file" class="file_input" name="photo" @change="onFileChange"  accept="image/*" />
          </div>
        </div>

.imgContent {
   width: 90%;
    max-width: 700px;
    margin:auto;
     margin-bottom:40px;
} 
.imagePreview {
    height:30vh;
    background: rgb(240, 240, 240);
    overflow:hidden;
    border-radius: 10px;
    background-position: center center;
    background-size: cover;
     margin-bottom:30px;
     position: relative;
 }
 .fileUpload {
  text-align: center;
  position: absolute;
  height: 25px;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  margin: auto;
  color:rgb(134, 134, 134);
  padding: 20px;
}

input自体をopacity:0;にすることで、あたかもイメージボックスを押して起動しているように見せることができます。これをベースにデザインなど変えてみたら面白いと思います。

    onFileChange(e) {
      let files = e.target.files || e.dataTransfer.files;
      this.createImage(files[0]);
    },
    // アップロードした画像を表示
    createImage(file) {
      let reader = new FileReader();
      reader.onload = (e) => {
        this.uploadedImage = e.target.result;
      };
      reader.readAsDataURL(file);
    }

createImageのFileReader()で画像が読み込まれて、それがonFileChange()で表示される仕組みになっています。

2019-05-07 (11).png
こんな感じで表示されます!!

まとめ

僕は、吉岡里帆が大好きです。
twitterやっているのでよかったら見てください!
読んでいただきありがとうございました。

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

Vue.jsの完全ビルドとランタイムビルドの違い

  • この記事は下書きに1年以上入れたままだったものをサルベージしたものです
  • 明確なバージョンは書いていませんが、時期的に2.5系が基準になっています

完全ビルドとランタイムビルドの違い

Electron (node.js) 環境でVue.jsを入れた際に、実行すると以下のWarningがConsoleに出力された。

You are using the runtime-only build of Vue where the template compiler is not available. 
Either pre-compile the templates into render functions, or use the compiler-included build.

イマイチわからないのでぐぐったら以下のページで解決法が載っていた。

Rails5.1でVue.jsで単一ファイルコンポーネントのエラーがでる
この記事からリンクされていた公式キュメント さまざまなビルドについて

どうやら、Vue.jsには完全ビルドとランタイム限定ビルドというものがあるらしい。

用語
完全: コンパイラとランタイムの両方が含まれたビルドです。

コンパイラ: テンプレート文字列を JavaScript レンダリング関数にコンパイルするためのコードです。

ランタイム: Vue インスタンスの作成やレンダリング、仮想 DOM の変更などのためのコードです。基本的にコンパイラを除く全てのものです。
WebpackなどでVueのソースをbundleした場合に、ランタイム限定ビルドとしてbundleされるためにこのWarningが発生する。

記事や公式ドキュメントにあった解決法は、完全ビルドのVue.jsをbundleする方法。
bundlerに明示的に vue/dist/vue.esm.js などをvueとして指定することで、完全ビルドのVue.jsをbundleしてしまうという解決法になる。

完全ビルドは30%重い

上記の解決法が必要になる理由は、ランタイム限定ビルドが現在Vue.jsのデフォルトになっているからであるが、
先の記事によると

完全ビルドよりも最初の設定の方が30%軽量なんですね。だから公式は初期設定をデフォルトにしているわけですね。

とのこと。

想定としては 単一ファイルコンポーネント を作って、.vueファイルをjsからimportして、
js自体もWebpackだったり、Electronアプリを今は作っているからRollup.jsでプリコンパイルして使う予定なのだから、
.vueファイルをコンパイルするコンパイラbundleする必要ないし、軽いほうがいい。

書いてたコード(失敗編)

前のプロジェクトで作ってたように、以下のような構成で作成した。

foo-component.vue
<template>
   <div>適当なHTMLタグ</div>
</template>
<script>
    // 適当なコード
    export default {
    }
</script>
<style>
      /*適当なスタイル*/
</style>
index.html
<div id="vue-root">
    <foo-component></foo-component>
</div>
index.js
import Vue from 'vue';
import fooComponent from './vue/foo-component.vue';

new Vue({
  el: '#vue-root',
  component: {
    'foo-component': fooComponent,
  }
});

で、rollup後のソースが以下。
プラグインの rollup-plugin-vue2を使ってvueのプリコンパイルを実行させている。

index.js
(function () {'use strict';

function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }

var Vue = _interopDefault(require('vue'));

/*適当なスタイル*/

// 適当なコード
    var fooComponent = {
render: function(){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',[_vm._v("適当なHTMLタグ")])},
staticRenderFns: [],
      data() {
        return {

        };
      }
    }

new Vue({
  el: '#vue-root',
  component: {
    'foo-component': fooComponent,
  }
});

}());
//# sourcemappingURL=index.js.map

importしていたfoo-component.vueはしっかりとJSのコードに直されている( render: の行)。

プリコンパイルを行なわない場合は、完全ビルド版のVue.jsは読み込んだ.vueファイルからrender行にあるようなJSコードを動的に作成して実行しているということ。
そりゃ、プリコンパイルしたほうが軽くなるよなー。

…しかし、このコードは冒頭のエラーが出て動きません

Warningが出る理由

プリコンパイルをしているのに、なんでpre-compileしろというWarningが出るのか全く理由がわからなかったが、
いろいろ試している間に、以下のコードに修正したら動作した。

index.html
<div id="vue-root"></div>
index.js
import Vue from 'vue';
import fooComponent from './vue/foo-component.vue';

new Vue({
  el: '#vue-root',
  render: h => h(fooComponent),
});

違いは、componentの指定を止めVueのルートになるエレメントの中にコンポーネントを指定するのではなく、
直接コンポーネントにするエレメントを指定するようにしたこと。

また、Warningにも書いていたが、render functionを使うようにした。

元の記載方法、

index.html
<div id="vue-root">
    <foo-component></foo-component>
</div>

これは、ルートにdivを指定しその中に独自のコンポーネントを指定していたが、この形は インラインテンプレート という手法になる。

これ自体は便利な機能だが、インラインテンプレートの中のHTMLは実行時にコンパイルされている為に完全ビルドライブラリが必要になっていた模様。
気づけばそりゃそうだなという感じだが、結構ハマった。

ライブラリビルドを維持して解決する方法まとめ

  • インラインテンプレートも使わずにコンポーネントにする
    • .vueファイルを作らずに、render functionの中にjsをゴリゴリ書いていく形でもOK。但し書きづらい読みづらいと全く意味は無い
    • new Vue()するときにtemplate:でHTMLを書く方法もプリコンパイルされないので、ライブラリビルドでは動作しない
    • よって、.vueファイルを作ってプリコンパイルさせる方法がベスト
  • コンポーネント内でコンポーネントを使いたい場合は普通に.vueファイルの<tamplate>の中で入れ子に書いておき、new Vue() する際に component: で指定する

単一ファイルコンポーネントとマウント

コンポーネント外から$onで指定したイベントが$emitできない

以下のような構成で、ボタンクリック時のイベントは外部から指定することを想定し、
コンポーネント内では単純に$emitでイベントを発火するだけにしたいと思い、
new Vueしたインスタンスに$onでイベントを設定、
コンポーネント内からは$emitでそのイベントを発火させる形にした。

client.js
import a_vue from './vue/a.vue';
const a = new Vue({
  el: '#a',
  render: h => h(a_vue),
});

a.$on('click.button', () => {
  console.log('clicked');
});
<div id="a"></div>
a.vue
<template>
    <div>
        <button @click="clicked">押す</button>
    </div>
</template>
<script>
  export default {
    methods: {
      clicked() {
        this.$emit('click.button'); //動かない
      },
    },
  }
</script>

しかし、上記のコードでは click.button イベントが発火しなかった。
this._events(イベントが格納されるプロパティ)を出力しても空になっている。

いろいろ調べてみる

a.vue
  export default {
    methods: {
      clicked() {
        console.log(this); //VueComponent {_uid: 4, _isVue: true, $options: Object, _renderProxy: Proxy, _self: VueComponent…}
        console.log(this._events); //Object No Properties
      },
    },
  }
client.js
import a_vue from './vue/a.vue';
const a = new Vue({
  el: '#a',
  render: h => h(a_vue),
});
a.$on('click.button', () => {
  console.log('clicked');
});

console.log(a); //Vue$3 {_uid: 3, _isVue: true, $options: Object, _renderProxy: Proxy, _self: Vue$3…}
console.log(a._events); //Object click.button: Array(1)
  • コンポーネント内でのthisはVueComponentインスタンスになっている
    • _uidも4
  • コンポーネント外でのnew Vue()のインスタンスは Vue$3インスタンスになっている
    • _uidは3
    • _eventsにも値がちゃんと入っている
  • つまりインスタンスが別物

以下のようにした場合、それぞれコンポーネント外のインスタンスと同じものにアクセスできた。

a.vue
  export default {
    methods: {
      clicked() {
        console.log(this.$parent); //Vue$3 {_uid: 3, _isVue: true, $options: Object, _renderProxy: Proxy, _self: Vue$3…}
        console.log(this.$root); //Vue$3 {_uid: 3, _isVue: true, $options: Object, _renderProxy: Proxy, _self: Vue$3…}
        console.log(this.$parent._events); //Object click.button: Array(1)
      },
    },
  }

これを踏まえて、以下のようにすると外側で設定したイベントを発火できた。

this.$root.$emit('click.button');

嫌すぎる

  • a.$onでイベントバインディングしているのに、呼び出すときにthis.$root.$emitにしなきゃいけない非対称感が気持ち悪い
  • コンポーネントから見たthisがコンポーネント自体なのはわかる
  • new Vue(a_vue)したインスタンスがコンポーネントと別物なのが納得行かない

なぜthisVueComponentインスタンスになってしまうか

  • コンポーネントとして登録しているから

解釈(推測も含む)

以下の形でrender()に単一ファイルコンポーネントを渡すと、Vue$3インスタンス内のコンポーネントとして登録される。

const a = new Vue({
  el: '#a',
  render: h => h(a_vue),
});

この場合のnew Vue()したaは、.vueで作成したコンポーネントではなく、
htmlに記載した<div id="a">自体を表し、これがVue$3インスタンスになっている。

a.vueファイルをプリコンパイルした結果のJSコードは、render()を既に持っている単純なObjectとなっている。

こんな感じ
  var a_vue = {
    render: function () {
      var _vm = this;
      var _h = _vm.$createElement;
      var _c = _vm._self._c || _h;
      return _c('div', [_c('button', {on: {"click": _vm.clicked}}, [_vm._v("押す")])])
    }, staticRenderFns: [],
    methods: {
      clicked() {
        console.log(this);
        console.log(this.$root);
        console.log(this.$parent);
      },
    },
  }

ということは、元のコードは、render()内で更にrenderを持っているオブジェクトを渡していたことになる。

なので、importしてきた.vueファイルをそのままnew Vue()の引数に渡せば
VueComponentではなくVue$3インスタンスになるのではないか?

client.js
import a_vue from './vue/a.vue';
const a = new Vue(a_vue);
console.log(a); //Vue$3 {_uid: 2, _isVue: true, $options: Object, _renderProxy: Proxy, _self: Vue$3…}

結果、エラーもなく生成された。

仕上げ

但し、ここまでの自体だとブラウザには何も表示されない。
当然と言えば当然で、elを指定していないのでブラウザにマウントされていない。

この辺の説明はさらっとこの辺に書いている。
https://jp.vuejs.org/v2/api/#vm-mount

Vue インスタンスがインスタンス化において el オプションを受け取らない場合は、DOM 要素は関連付けなしで、”アンマウント(マウントされていない)” 状態になります

手動でマウントするには、$mountを使う。

client.js
import a_vue from './vue/a.vue';
const a = new Vue(a_vue);
a.$mount('#a');

これで無事にブラウザに表示された。
また、.vueファイルのメソッドでthisVue$3インスタンスとなり、無事にイベントは実行される。

マウントを手動で行うには上記のコードだが、
単純に以下のようにnew Vue()する前にプロパティを追加しても動作する。

client.js
import a_vue from './vue/a.vue';
a_vue.el = "#a";
const a = new Vue(a_vue);

.vueファイル内でelを書き込んで置いても動作する。

a.vue
  export default {
    el: "#a",
    methods: {
      clicked() {
        //
      },
    },
  }

とは言え、この書き方は.vueとhtmlファイルが密結合になるのであまり綺麗じゃないので、
JS内でmountするかelを指定するほうが綺麗だとは思う。

まとめ

https://github.com/vuejs-templates/webpack/issues/215#issuecomment-287652462 これで十分

  • new Vue()ではrender functionに沿って新たなVue$3インスタンスを作成する
  • 単一ファイルコンポーネントは、importしてきたときrender functionを持っているが、ただのObjectである
  • render functionの引数が全てJSコードであれば、インスタンスの生成はライブラリビルドでも実行できる
  • render functionの中にrender functionを入れ子に含めると、内側はVueComponentインスタンスになる
  • これらを踏まえて、実装時にはコンポーネントとするか単一のVueインスタンスとするかを意識しないとハマる

蛇足の雑感

  • 公式ドキュメントはわかりづらい
    • 日本語は直訳だし元の文章がよくなさそう
    • マウントとかアンマウントの話も$mountの中にしか書いてなかったりするし
  • vuejs-templatesは結構役立つ
    • マウントの様々な仕方は このへんを参考にした
    • ただ、vuejs-templatesもやっぱり見づらい
  • WEB上のFAQはVue.jsに限ってはあまりアテにならない
    • この問題でもthis.$root.$emit使うとか書いてるところが多かったし
    • ライブラリビルドの件も根本に踏み込んでるところが出てこなかったし
    • でも、WEB上の記事がわかりづらくなっているのは、Vue.jsが2から仕様が結構変わったのとかも影響してそう
  • 結局console.logが最強
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Youtubeの同じ動画を永遠と再生させる

実装

iframeで以下を埋め込む

<iframe
  class="movie"
  type="text/html"
  width="720"
  height="405"
  src="https://www.youtube.com/embed/VIDEOID?autoplay=1&loop=1&playlist=VIDEOID"
  frameborder="0"
  allowfullscreen
></iframe>

autoplay=1で自動再生
loop=1でループ
playlistをしないと自動再生が効かない

サンプル

以下のように自動再生、無限ループができます。
https://mystifying-lewin-f6c484.netlify.com/youtube

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

Vuetifyを使ってみた話

vuetifyってなによ?

VuetifyはVueのUIコンポーネントライブラリーです。
シンプルでスタイリッシュなUIが多くあります。
デザイン力が無い自分もそこそこカッコいいページを作ることができるようになるありがたいものです。
マテリアルデザインを採用しているようです。
なんとなくみたことあるようなUIが手軽に使えるのでオススメです。

とりあえず使ってみる

インストール

console
npm install -g @vue/cli  <-- Vue-cliがすでに入ってる人は飛ばして構いません!
vue create hogehoge      <-- Vueのプロジェクトを作成
vue add vuetify          <-- これをするといい感じに自動でVuetifyを使える環境にしてくれます

npm run serve            <-- サーバー起動!

localhost:8080にアクセスするとこんな感じの画面になります!
いかにもスタートページって感じですね。
FireShot Capture 002 - my-app - localhost.png

*フッターとかちょっといじっちゃってますw

これは下のような階層構造の中のコンポーネンツ内のHelloWorldの内容が表示されています。
ヘッダーとかはApp.vueに書いてありますね

スクリーンショット 2019-05-07 14.58.19.png

HelloWorld.vue
<template>
  <v-container>
    <v-layout
      text-xs-center
      wrap
    >
      <v-flex xs12>
        <v-img
          :src="require('../assets/logo.svg')"
          class="my-3"
          contain
          height="200"
        ></v-img>
      </v-flex>

      <v-flex mb-4>
        <h1 class="display-2 font-weight-bold mb-3">
          Welcome to Vuetify
        </h1>
        <p class="subheading font-weight-regular">
          For help and collaboration with other Vuetify developers,
          <br>please join our online
          <a href="https://community.vuetifyjs.com" target="_blank">Discord Community</a>
        </p>
      </v-flex>

      <v-flex
        mb-5
        xs12
      >
        <h2 class="headline font-weight-bold mb-3">What's next?</h2>

        <v-layout justify-center>
          <a
            v-for="(next, i) in whatsNext"
            :key="i"
            :href="next.href"
            class="subheading mx-3"
            target="_blank"
          >
            {{ next.text }}
          </a>
        </v-layout>
      </v-flex>

      <v-flex
        xs12
        mb-5
      >
        <h2 class="headline font-weight-bold mb-3">Important Links</h2>

        <v-layout justify-center>
          <a
            v-for="(link, i) in importantLinks"
            :key="i"
            :href="link.href"
            class="subheading mx-3"
            target="_blank"
          >
            {{ link.text }}
          </a>
        </v-layout>
      </v-flex>

      <v-flex
        xs12
        mb-5
      >
        <h2 class="headline font-weight-bold mb-3">Ecosystem</h2>

        <v-layout justify-center>
          <a
            v-for="(eco, i) in ecosystem"
            :key="i"
            :href="eco.href"
            class="subheading mx-3"
            target="_blank"
          >
            {{ eco.text }}
          </a>
        </v-layout>
      </v-flex>
    </v-layout>
  </v-container>
</template>

<script>
  export default {
    data: () => ({
      ecosystem: [
        {
          text: 'vuetify-loader',
          href: 'https://github.com/vuetifyjs/vuetify-loader'
        },
        {
          text: 'github',
          href: 'https://github.com/vuetifyjs/vuetify'
        },
        {
          text: 'awesome-vuetify',
          href: 'https://github.com/vuetifyjs/awesome-vuetify'
        }
      ],
      importantLinks: [
        {
          text: 'Documentation',
          href: 'https://vuetifyjs.com'
        },
        {
          text: 'Chat',
          href: 'https://community.vuetifyjs.com'
        },
        {
          text: 'Made with Vuetify',
          href: 'https://madewithvuetifyjs.com'
        },
        {
          text: 'Twitter',
          href: 'https://twitter.com/vuetifyjs'
        },
        {
          text: 'Articles',
          href: 'https://medium.com/vuetify'
        }
      ],
      whatsNext: [
        {
          text: 'Explore components',
          href: 'https://vuetifyjs.com/components/api-explorer'
        },
        {
          text: 'Select a layout',
          href: 'https://vuetifyjs.com/layout/pre-defined'
        },
        {
          text: 'Frequently Asked Questions',
          href: 'https://vuetifyjs.com/getting-started/frequently-asked-questions'
        }

      ]
    })
  }
</script>

Helloworldを変えてみる

Helloworldの内容をVuetifyの公式ページにあるコンポートの紹介のものに書き換えてみます!

HelloWorld.vue
<template>
  <v-carousel hide-controls>
    <v-carousel-item
      v-for="(item,i) in items"
      :key="i"
      :src="item.src"
    ></v-carousel-item>
  </v-carousel>
</template>

<script>
  export default {
    data () {
      return {
        items: [
          {
            src: 'https://cdn.vuetifyjs.com/images/carousel/squirrel.jpg'
          },
          {
            src: 'https://cdn.vuetifyjs.com/images/carousel/sky.jpg'
          },
          {
            src: 'https://cdn.vuetifyjs.com/images/carousel/bird.jpg'
          },
          {
            src: 'https://cdn.vuetifyjs.com/images/carousel/planet.jpg'
          }
        ]
      }
    }
  }
</script>

これだけで画像のスライドショーが表示されます。
vx7nc-8q10z.gif

まとめ

Vuetifyめっちゃ便利です。
もっと色々なUIコンポーネントがあるのでぜひお試しを!

参考

Vuetify公式ページ
Vuetifyをインストールして使ってみる@reflet様

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

laravelにVue.jsを入れる前の、5分間の事前学習

◆コンパイル

ソースコードを機械語に変換する

◆ビルド

バグがないことを確かめて(静的解析)、問題がなければ実行できる形のファイルに組み立てること
(プリプロセッサ:定数置き換え・コメント削除などコンパイル前の準備) このあとコンパイル。
コンパイルはビルドの中に含まれる

◆sass

cssの機能を拡張した言語(効率よくかける)
scssも同様
sassはインデントで分けて、scssはcssと書き方は同じだがこちらもインデントで分ける

◆node.js参考記事

サーバーサイドで使えるJavaScriptの代表的なものの一つ
クライアントもサーバーサイドもJavaScriptで書ければ楽ということが利用される最大の理由

◆npm参考記事

node.jsのパッケージ管理ツール
node.jsのパッケージは用意された便利機能の集まり

◆webpack

複数javaScriptのファイルがあり、またそのファイルごとに依存関係がある(なくてももちろんいい)場合でも、それを解決し、
一つのファイルにまとめてくれる機能(ビルド)
laravelのwebpack.mix.jsファイルを見てみると、

let mix = require('laravel-mix');

mix.js('resources/assets/js/app.js', 'public/js')
   .sass('resources/assets/sass/app.scss', 'public/css');

resources/assets/js/app.jsがビルドしたいファイル
public/jsが出力先
ほかにも指定したいものがあれば、どんどん指定できる。

◆npm run dev

laravelにvue.jsを導入するときに使うと、思うのですが、これで先ほどwebpack.mix.jsで指定した内容をビルドして、出力してくれる。

◆laravel mix関数参考記事

バージョン付けしたファイルの最新の名前を自動的に解決

mix.js('resources/assets/js/app.js', 'public/js')
   .version();

npm run devで出力されているため、asset('js/app.js')でも参照できる。バージョン管理がない場合はassetでいいと思います
参考記事

◆導入

参考記事をもとに進めました。
laravel-vue.js-docker-comose

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

canvasを使って画像中の特定範囲にテキストを描画する (文字画像ジェネレーター)

やりたいこと

  • 画像上の特定範囲の上下左右中心にテキストを描画
  • リアルタイムで編集
  • 編集後の画像をコピー、ダウンロード (本記事では含まない)

いわゆる吹き出し画像向けの文字画像ジェネレーター

作ってみた (デモ)

フロントのフレームワークはVueとVuetifyを利用しました。

See the Pen sentence-image-generator by icchi (@icchi) on CodePen.

ポイント

Canvasで描画

描画するだけであればvueと親和性の高いSVGのほうが個人的には扱いやすいと思います。ただし、今回は描画画像をBase64形式でコピー、またはダウンロードしたいという条件があり、それも含めて考慮するとSVGよりもCanvasの方が実装工数が低いと判断しました。

canvasであれば下記のようにたった1行のコードでcanvasオブジェクトからbase64形式の画像を取得可能です。

canvas.toDataURL("image/png");
// canvas: 次のような感じで取得できるcanvasオブジェクト
// const canvas = document.getElementById("canvas");

SVGでもHeadless Chromeを利用した以下のようなnode moduleで変換は可能ですが、canvasに比べ利用ライブラリの規模は大きくなってしまいます。
https://www.npmjs.com/package/convert-svg-to-png

テキスト描画範囲の座標指定

吹き出し(特定範囲)内にテキストを描画するために、その範囲を定義しておく必要があります。

今回は以下のように実装しました。
左上を原点として、範囲の左上、右下の座標を原点からの割合(小数)で定義しています。

textareaRate: { // 画像中のテキスト表示エリアの左上・右下座標(左上基準)
  start: {
    x: 0.26,
    y: 0.1
  },
  end: {
    x: 0.72,
    y: 0.34
  }
}

テキスト描画における基準点の位置

canvasにテキストを入力する場合、その指定座標に対する描画位置に若干クセがあります。

以下の画像がわかりやすいですが、デフォルトの設定だと指定座標が文字の左下に位置するように描画されます。

WVwFw.png
https://stackoverflow.com/questions/11120392/android-center-text-on-canvas

今回は吹き出し範囲の上下左右中心にテキストを描画したいため、指定座標がテキストの中心に位置するようにしてあげる必要があります。canvasのcontext設定項目の中には指定座標に対するテキスト位置のオプションが用意されているので、それらを以下のように指定します。

// get context
const canvas = document.getElementById("canvas");
const context = canvas.getContext("2d");

// set align center(horizontal/vertical)
context.textAlign = "center";
context.textBaseline = "middle";

改善点

自動フォントサイズ調整

例えばAPIライクなサービスを想定して完全な自動生成を実現するに、吹き出し範囲からテキストがはみ出さないよう描画する量に合わせてフォントサイズを調節

参考文献

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

Electronで時計アプリをCentos7用に作る

Electronで時計アプリをCentos7用に作る

なんか GNOME って、標準の時計アプリが見づらいなぁと思ってたので
折角だからElectronで作るかぁと重い腰をあげる。

Electronに移植

以下をElectronに移植する。
理由はカッコいいから!
Digital Clock with Vue.js
Toshiyuki TAKAHASHI氏の作成物。

前提・環境

・CentOS7.6
・nodeJS / Yarn導入済

環境構築

yarn add -D electron@latest --save-dev
sudo npm i electron-packager -g

※以下がでたら、参照してください

FATAL:setuid_sandbox_host.cc(157)] The SUID sandbox helper binary was found,〜

移植ソース

index.js
index.js
'use strict';

const electron = require('electron');
const app = electron.app;
const BrowserWindow = electron.BrowserWindow;

let mainWindow;

app.on('window-all-closed', function() {
  if (process.platform != 'darwin') {
    app.quit();
  }
});

app.on('ready', function() {
  mainWindow = new BrowserWindow({
    titleBarStyle: 'hidden', "width": 400, "height": 130,
    "frame": false,  "resizable": false, autoHideMenuBar: true, maximizable: false,
  }); 
  mainWindow.loadURL('file://' + __dirname + '/index.html');
  mainWindow.on('closed', function() {
    mainWindow = null;
  });
});

index.html
index.html
<html>
<head>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Share+Tech+Mono">
<link rel="stylesheet" href="css/main.css">
<script src="js/vue.js"></script>
</head>
<body>
<div id="clock">
  <p class="date">{{ date }}</p>
  <p class="time">{{ time }}</p>
  <script src="js/main.js"></script>
</div>
</body>
</html>

js/main.js
js/main.js
var clock = new Vue({
    el: '#clock',
    data: {
        time: '',
        date: ''
    }
});

var week = ['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'];
var timerID = setInterval(updateTime, 1000);
updateTime();
function updateTime() {
    var cd = new Date();
    clock.time = zeroPadding(cd.getHours(), 2) + ':' + zeroPadding(cd.getMinutes(), 2) + ':' + zeroPadding(cd.getSeconds(), 2);
    clock.date = zeroPadding(cd.getFullYear(), 4) + '-' + zeroPadding(cd.getMonth()+1, 2) + '-' + zeroPadding(cd.getDate(), 2) + ' ' + week[cd.getDay()];
};

function zeroPadding(num, digit) {
    var zero = '';
    for(var i = 0; i < digit; i++) {
        zero += '0';
    }
    return (zero + num).slice(-digit);
}

css/main.css
css/main.css
html {
  -webkit-app-region: drag;
}

html, body {
  height: 100%;
  width:  100%;
  margin: 0;
  background: #0f3854;
  background-size: 100%;
  /*background-color: rgba(0, 0, 0, 0.0);*/
}

p {
  margin: 0;
  padding: 0;
}
#clock {
  font-family: 'Share Tech Mono', monospace;
  color: #ffffff;
  text-align: center;
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  color: #daf6ff;
  text-shadow: 0 0 20px rgba(10, 175, 230, 1),  0 0 20px rgba(10, 175, 230, 0);
}
#clock > .time {
  letter-spacing: 0.05em;
  font-size: 80px;
  padding: 5px 0;
}
#clock > .date {
  letter-spacing: 0.1em;
  font-size: 24px;
}
#clock > .text {
  letter-spacing: 0.1em;
  font-size: 12px;
  padding: 20px 0 0;
}

vue.js

面倒だからwget

cd js
wget https://cdn.jsdelivr.net/npm/vue -O vue.js

仕上げ

sudo electron-packager ./electron_clock/ DigitalClock --platform=linux --arch=x64 --electronVersion=5.0.0 --asar
sudo mv DigitalClock-linux-x64/ DigitalClock
sudo chown root:root DigitalClock/chrome-sandbox 
sudo chmod 4755 DigitalClock/chrome-sandbox 

実行

~/DigitalClock/DigitalClock

所感

Electron。いいんだけど、ただ時計表示するのに、225Mbのアプリ。
Mbのインフレが半端ない。
Chromiumは、IEにも使われるし、もはやJavaと同じように個別インストールするようにすれば
もう少し軽くなるんじゃ。。。と思った。

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

Vue.jsでよく使うテストコード(jest/test-utils)

Vue/Vuexのテストコードをまとめてみました。
「jest」と「@vue/test-utils」を使用しています。

環境

  • vue: ^2.6.10
  • vuex: ^3.1.0
  • jest: ^24.7.1
  • @vue/test-utils: ^1.0.0-beta.29

clickメソッドが実行されるかどうか

test.vue
button(
  @click="clickButton"
)

methods: {
  clickButton() {
    console.log('click button');
  },
},
test
describe('v-on', () => {
  test('clickでclickButtonメソッドが実行されること', () => {
    const wrapper = shallowMount(test);
    const stub = jest.fn();
    wrapper.setMethods({
      clickButton: stub,
    });
    wrapper.trigger('click');
    expect(stub).toHaveBeenCalled();
  });
});

slotにコンテンツが挿入できてること

test.vue
div
  slot
test
describe('slot', () => {
  test('slotにコンテンツが挿入できること', () => {
    const wrapper = shallowMount(test, {
      slots: {
        default: '<div data-test="slotContent">slot content</div>',
      },
    });
    const slotContent = wrapper.find('[data-test="slotContent"]');
    expect(slotContent.exists()).toBe(true);
    expect(slotContent.text()).toBe('slot content');
  });
});

propsが受け取れること

test.vue
props: {
  hoge: {
    type: String,
    default: '',
    required: true,
  },
},
test
describe('props', () => {
  test('propsを受け取れること', () => {
    const wrapper = shallowMount(test);
    wrapper.setProps({
      hoge: 'hogeだよ',
    });
    expect(wrapper.vm.$props.hoge).toBe('hogeだよ');
  });
});

propsの型が合っているかどうか

test.vue
props: {
  hoge: {
    type: String,
    default: '',
    required: true,
  },
},
test.vue
describe('props', () => {
  test('型が期待しているとおりか', () => {
    const requiredProps = {
      hoge: 'hogeだよ',
    };
    const wrapper = shallowMount(test, {
      propsData: requiredProps,
    });
    expect(typeof wrapper.vm.$props.hoge).toBe('string');
  });
});

propsがrequired:trueになっているかどうか

test.vue
props: {
  hoge: {
    type: String,
    default: '',
    required: true,
  },
},
test
describe('props', () => {
  test('requiredがtrueになっていること', () => {
    const requiredProps = {
      hoge: 'hogeだよ',
    };
    const wrapper = shallowMount(test, {
      propsData: requiredProps,
    });
    expect(wrapper.vm.$options.props.hoge.required).toBe(true);
  });
});

emitされること

test.vue
input(
  type="text"
  @input="inputText"
)

methods: {
  inputText(e) {
    this.$emit('input-text', e);
  },
},
test
describe('inputText', () => {
    test('emitされること', () => {
      const wrapper = shallowMount(test);
      wrapper.vm.inputText({ target: { value: 'valueだよ' } });
      expect(wrapper.emitted('input-text')).not.toBeUndefined();
    });
  });
});

emitで値が渡されること

test.vue
input(
  type="text"
  @input="inputText"
)

methods: {
  inputText(e) {
    this.$emit('input-text', e);
  },
},
test
describe('inputText', () => {
  test('emitで値が渡されること', () => {
    const wrapper = shallowMount(test);
    wrapper.vm.inputText({ target: { value: 'valueだよ' } });
    expect(wrapper.emitted('input-text')[0][0].target.value).toBe('valueだよ');
  });
});

stateの値をpropsに渡せているかどうか(mapState使用)

Sample.vue
<template lang="pug">
  te-sample(
    :label01="label01"
  )
</template>

<script>
import { mapState } from 'vuex';
import TeSample from '@/components/templates/TeSample';

export default {
  components: {
    TeSample,
  },
  computed: {
    ...mapState({
      label01(state) {
        return state.label01;
      },
    }),
  },
};
</script>
test
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Sample from '@/components/pages/Sample';
import TeSample from '@/components/templates/TeSample';

const localVue = createLocalVue();
localVue.use(Vuex);

const store = new Vuex.Store({
  state: {
    label01: 'valueだよ',
  },
});

describe('Sample test', () => {
  let wrapper;
  beforeEach(() => {
    wrapper = shallowMount(Sample, {
      store,
      localVue,
    });
  });
  describe('mount', () => {
    test('TeSampleにstateの値をpropsで渡せているかどうか', () => {
      expect(wrapper.find(TeSample).props().label01).toBe('valueだよ');
    });
  });
});

dispatchした値が渡されるかどうか

test.vue
<template lang="pug">
  div
    slot(
      :inputText="inputText"
    )
</template>

<script>
export default {
  props: {
    actionType: {
      type: String,
      default: '',
      required: true,
    },
  },
  methods: {
    inputText(e) {
      return this.$store.dispatch(this.actionType, {
        value: e.target.value,
      });
    },
  },
};
</script>
test
import Vuex from 'vuex';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import test from '@/components/test';

const localVue = createLocalVue();
localVue.use(Vuex);

describe('test', () => {
  let wrapper;
  const mockStore = {
    dispatch: jest.fn(),
  };
  beforeEach(() => {
    wrapper = shallowMount(test, {
      mocks: {
        $store: mockStore,
      },
      localVue,
    });
  });
  describe('mount', () => {
    test('dispatchで値が渡されること', () => {
      wrapper.setProps({
        actionType: 'actionだよ',
      });
      wrapper.vm.inputText({ target: { value: 'valueだよ' } });
      expect(mockStore.dispatch).toHaveBeenCalledWith(
        'actionだよ', {
          value: 'valueだよ',
        },
      );
    });
  });
});

actionでcommitの値を渡せているかどうか

actions.js
export const SAMPLE_ACTION = 'SAMPLE_ACTION';

const actions = {
  action01(context, payload) {
    context.commit(SAMPLE_ACTION, payload);
  },
};

export default actions;
test
import actions, { SAMPLE_ACTION } from '@/store/actions';

describe('actions test', () => {
  test('action01のcommitでデータを渡せるかどうか', () => {
    const commit = jest.fn();
    const { action01 } = actions;
    const payload = {
      value: 'valueだよ',
    };
    action01({ commit }, payload);

    expect(commit).toHaveBeenCalledWith(SAMPLE_ACTION, payload);
  });
});

mutationでStateの値が更新できてるかどうか

mutations.js
import { SAMPLE_ACTION } from './actions';

const mutations = {
  [SAMPLE_ACTION](state, inputData) {
    const data = state;
    const { value, name } = inputData;
    data[name] = value;
  },
};

export default mutations;
test
import mutations from '@/store/mutations';

describe('SAMPLE_ACTION test', () => {
  test('SAMPLE_ACTIONのactionでstateを更新する', () => {
    const inputData = {
      name: 'label01',
      value: 'valueだよ',
    };
    const state = {
      label01: '',
    };

    mutations.SAMPLE_ACTION(state, inputData);

    expect(state).toEqual({
      label01: 'valueだよ',
    });
  });
});
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vueでwindowのグローバル変数を呼ぶときの話

概要

twitterのシェアボタンwidjetをあるオブジェクトがロードされてから表示するようにしたら、
widjetにならなかったので、twttr.widgets.load()で呼び出そうとしたら、
うまく呼び出せなくて、呼び出すことができた方法のメモです。

Twitterウィジェット

https://publish.twitter.com/#
上記のTwitter公式から生成したものですが、下記のようにwidjets.jsを読み込んで、class="twitter-share-button"を指定すると、aタグがtwitterのシェアボタンウィジェットになります。

<a href="https://twitter.com/share?ref_src=twsrc%5Etfw" class="twitter-share-button" data-show-count="false">Tweet</a>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

しかし、最初から表示されてるオブジェクトにしか適用されず、Vueのv-ifディレクティブを使用したりして動的にボタンを表示する場合は適用されません。
この場合、widjets.jsが読み込まれた際にグローバル変数に追加されるtwttrを利用して、twttr.widgets.load()をしてウィジェットを描画する必要があります。

非同期で読み込んだFacebook, Twitterボタンを再描画する

Vueの単一ファイルコンポーネントの場合

このとき、Vueの単一ファイルコンポーネントの場合、下記のようにscriptタグ内でtwttr.widgets.load()を呼ぼうとするとtwttrが存在しないと言われてしまいます。
どうやら、グローバル変数に直接アクセスができないことが問題のようでした。

twitter.vue
...
<script>
export default {
  name: 'Twitter',
  data () {
    return {
    }
  },
  methods: {
    reloadWidgets () {
      twttr.widgets.load()
    }
  }
}
</script>
...

ですので、この場合は下記のようにwindowからアクセスすることで、twttrにアクセスすることができ、twttr.widgets.load()を呼ぶことができます.

twitter.vue
...
<script>
export default {
  name: 'Twitter',
  data () {
    return {
    }
  },
  methods: {
    reloadWidgets () {
      window.twttr.widgets.load()
    }
  }
}
</script>
...

あとがき

色々試行錯誤していたため、書いてる最中にwindowをスクリプトタグから呼べるんじゃないかと気づきました。
試行錯誤中に試してはいたのですが、他の要因からうまくできなかったため、この記事を書いている最中は下記のように、
main.jsVue.prototype.$twttrに変数を割り当ててインスタンスプロパティを設定する方法を紹介しようとしていました。
この方法だと、コンポーネント内からthis.$twttrで参照することができます。

main.js
Vue.prototype.$twttr = window.twttr

ちなみに、スマホから見るとなぜかwidjetsがundefinedと言われて、うまくウィジェット化できなかったため、
実際の実装ではhttps://twitter.com/intent/tweetを利用して、見た目は自作しました。

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

チンケなアプリを作って実感した、「個人開発って、物凄い事なんじゃないのか?」という事。

個人開発に興味があるものの、フロントエンドが苦手なんで何とか克服しておきたかった。
そんで、まずはチンケなアプリをサクッと作ってみようかと思って、ちょこちょこ手を動かしてみた。

作ったの、こんな感じ。

【 異名ジェネレータ 】
ランダムに言葉を生成して、適当な異名を探す。英語/日本語。
https://another-name-generator.netlify.com/

【 CSV to TABLE 】
CSV をテキストエリアに入力したら、ちょっといい感じにテーブル化する。
DLして遊ぼうと思ってたデータがCSV形式だったんで、その過程で作ってみた。
https://csv-to-table.netlify.com/

「うっわ! ショボ!」
って思ったかもしれないが、まぁ、その通り。否定する気は全くありません。
正直、練習という要素が強いんで、こんな問題を解決しようとか、世の中にどんなインパクトを与えようとかは、ほぼ考えずに作りました。

多分、「こんなの、素人でも作るのに3日もいらないんじゃね?」という印象を持った人も居るんじゃないかと思う。
ってか自分も、そのぐらいでサクっと作れるボリュームを考えていたつもりだ。

しかし、実際に手を動かしてみると、こんな有様だ。
手を動かしている間、日誌というか雑多な記録をつけるようにしていたんで、その経緯が以下のような感じになりました。
(クソ長いです。リンク先をクリックせず、そのままスクロールしてもらってOKです。)

Part 1. 異名ジェネレータ:公開までの振り返り

土台構築期1(レベルが低すぎて倒される期)

Vue.js:vue-element-adminで遊んでる。
Vue.js:vue-element-admin遊びの続き
Vue.js:vue-element-admin遊びの続き2
Vue.js:vue-element-admin遊びの続き3

土台構築期2(起き上って進む期)

Vue.js:次の遊び(todo-backend-golang-mc)
Vue.js:次の遊び(Vue-Bootstrap-with-Material-Design)
Vue.js:vuetifyjs/theme-blog
Vue.js:vuetifyjs/theme-freelancer
Vue.js:Vue-Bootstrap-with-Material-Design その2
haikunator で遊ぶ
haikunator で遊ぶ その2
Vue.js:Vue-Bootstrap-with-Material-Design その3
Vue.js:Vue-Bootstrap-with-Material-Design その4
Vue.js:v-bind と v-model
Vue.js:Vue-Bootstrap-with-Material-Design その5
Vue.js:「:is」が理解できなかったのでサンプルを書いた。
Vue.js:Vue-Bootstrap-with-Material-Design その6

「Vue.js わからーん!」と言いながら、突き進んでる期

Vue.js:data とか data() とか、使い分けがよく分かんない。
Vue.js:Vue-Bootstrap-with-Material-Design その7
Vue.js:通常の for文って?
Vue.js:created と mounted の使い分けがよく分かんない。
Vue.js:v-model がよく分かんなかったんで調べた。

Ver1 リリース

【自作アプリ】Vue.js:何か作った

Ver2 意外と簡単に行けそうなので、機能追加にやってみる期

【自作アプリ】異名ジェネレータ:機能追加

Ver2 エラー遭遇期

【自作アプリ】異名ジェネレータ:追加機能作成中
【自作アプリ】異名ジェネレータ:追加機能作成中 その2
【自作アプリ】異名ジェネレータ:追加機能作成中 その3 エラー遭遇中
【自作アプリ】異名ジェネレータ:追加機能作成中 その4 エラー遭遇中
【自作アプリ】異名ジェネレータ:追加機能作成中 その5 エラー遭遇中

Ver2 方法転換期

【自作アプリ】異名ジェネレータ:追加機能作成中 その6 作戦変更(ダメな方向に)
【 Node.js 】csv → json 変換ライブラリを色々試した。

Ver2 リリース

【自作アプリ】異名ジェネレータ:日本語訳機能追加

Part 2. CSV to TABLE:公開までの振り返り

【自作アプリ】次のアプリ作成中:その1
【Vue.Js】「@Click.Native.Prevent」の意味がわからなかったから調べてみた。
【Vue.Js】CSVファイル読ませてそれを画面上に反映させたかっただけなのに、何か遠回りしてる。
【Vue.Js】Vue Router で出来る範囲のことがいまいちよく分からない
【JavaScript】CSV から JSON に変換してくれるライブラリとか
【Vue.Js】Json-2-Csv 使ってみたけど、上手くいかない。
【Node.Js】csv(というライブラリ)の使い方がよく分からない。
【JavaScript】Import XXX と、Import { XXX } って、どう違うの?
【自作アプリ】CSV to TABLE を公開してみた。


要約すると、
『「わからーん!」と「全然うまくいかねー!」を繰り返しつつ、妥協に妥協を重ねた上にブチかっこ悪い着地をした挙句、一部はハリボテで出来てます♪』
という事が書かれています。
あと、読んでもらえれば分かるかと思いますが、だいぶ雰囲気で Vue.js 使ってやがります。この男。

ちなみに書き手は、システム開発についてズブの素人という訳ではありません。(フロントは得意じゃないけど)
得意なのは .NET、PHP、Java あたりで、フロントはぜいぜい jQueryを使うレベル。
どっちかというとエンプラ案件のが得意だけど、徐々にスキルセットを Web系に寄せていこうかとフロントにも手を出している。
(というかフロントは興味があって触ってる。)

実務で触ったことあるフロントエンドフレームワークは Vue.js の 1系のみで、jQuery と素の JavaScript がキメラの如く混ざり合った Chaos☆な感じで、まぁまぁパンチが効いた体験をさせてもらった。普段 Vue.js を触っている人が見ても、「分かりづらっ!」と言ってくれたシステム。

ドットインストールの Vue.js を全クリし、「基礎から学ぶ Vue.js」を読みながら手を動かしている。(全部の内容が理解できている訳では無いので、「読了」とは書かないことにした。)

そのぐらいの人間ですが、冒頭でご紹介させて頂いたアレを作るのに、上記のような大変な経路を辿りました。
作り手の能力に問題がある事を差し引いたとしても、あの程度のチンケなアプリ作るだけでも、まぁまぁな苦労がある事が伝わってくれればと思います。
 
わかった事は、
『個人開発者って、すげえ。』

あんな小さいアプリ作るだけでも、これだけの苦労をしたんだ。
個人でまっとうなプロダクトを世に送り出すなんて、どれだけ凄いんだよ。
個人開発者の方々の凄さを実感すると同時に、自分のエンジニアとしての力量の低さを嫌でも実感していまう。

という感じで、個人開発に興味があって、色々な構想を練っている方も多いと思いますが、まずは上記のようなチンケなアプリを後悔するところから初めてみてはどうでしょう?

多分、想像している以上に多かった障壁に愕然として、計画を練り直すいいきっかけになるんじゃないかと思う。

小規模なアプリであれば、人によってはもっとサクっと作れると思う。ってか、もっと上手くできる人の方が多いんじゃないかという気がしている。
その時は、
「よーし! こいつよりは上手くいったぞー!」
と、遠慮なく足蹴にしてくれれば幸いだ。それが他のエンジニアの勇気になるのなら。

まぁ、手を動かす事をやめるつもりは無いんで、こんな事を言っている人間が、数年後に何を作れるようになってるかは、1年後ぐらいに振り返ってみようかと思います。

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