※スマホ対応はしてません。

タグ: vue

Nuxt.js他で時計アプリ (PWA) を作ってみたよ。

カテゴリー: JavaScript

ええいや時計って……という感じはあるんだけど、とにかくNuxt.jsを使ってみたかったのです。あと自分では使う。

できあがったもの

アナログ時計が左に、デジタル時計が右に。あとポモドーロタイマー付き。

使った技術

ライブラリ、フレームワーク

ツール

ウェブ標準API

サービス

技術系感想

Nuxt.js

この記事の影響。

なんか久しぶりに「動くものがさくさく作れる」という快感を味わうことができたフレームワーク。Ruby on Railsにこなれてきた頃の快感を思い出しました。

vue-cli使って入れたんだけど、最初からディレクトリとか開発用サーバーとか、各種ビルドとか即時部分更新 (HMR) とか、追加のあれこれなしに書くだけですぐ動く。黒魔術的な感じなのもRailsっぽい。いや別にRailsと比べるものじゃないんだけど。

今回は静的HTML (SSR) でやったけど、そのうち動的なやつもやってみたい。Firebaseでホストすればできると聞いたような気がする? 未調査。別に # なURLでも構わないけども。

vue-cilで入れるやつのコマンド(公式ドキュメントより):

$ npx vue-cli init nuxt-community/starter-template my-project

Nuxt + PWA

こいつを yarn add するだけでわりとすぐ動いた。お手軽便利。これを導入するだけで用が済んでしまったので、ServiceWorkerも自前では書いてないです。

ドキュメントはあんまり親切じゃない感じがする。まだ使い方とかがあんまりわかってない。

Visual Studio Code

ずっとVimを使ってきたんだけど(最近はNeoVim)、ちょっと人に教えるのにこれはあれだよなーと思って「普通のエディター」として使い始めてみました。あらでも良いじゃない。

スクリーンショット。公式サイトから。

後発なうえに開発体制が盤石なだけあって、さすがに出来が良い。各種OSに対応している点、インストールしてすぐ使える点、プラグインが豊富かつ導入や管理も楽な点、日本語対応している点から、今後は人様にお勧めしていこうかなと思っております。

最初に導入してほしいプラグイン:

  • Code Spell Checker
  • EditorConfig
  • ESLint(JS系のひと向け)

ただ、スクロールがなかなかちょっと。これはCodeじゃなくてたぶんElectron for Windowsの問題で、Slack.exeなんかでも起こる。どうも onscroll で何かやってるとうまくスクロールが動かなくなるみたい。詳細未調査だけど、たしか自分が作ったアプリだとそんな感じだった。まあだいたいの人はMacだから問題ないね……。

あとVimプラグインはキー操作が衝突するんで、案の定アレでした。

CSS Grid

何度か使ったことあったかな。気がつけば一通りのブラウザサポートが揃っていたので。

CSS Gridは全てのモダンブラウザで動作する。画像はgrid-templateプロパティの場合。

今回複雑なレイアウトをしてはいないけれども、別にそんじょそこらの細かいところでも割と気軽に使えるので、今後はがしがし使って行きたい。

今回使ったのは右側半分。”T”の字に区切ってます。

GitHub Pages + Cloudflare

今回は全部静的ファイルなので、GitHub Pagesで公開してCloudflareでhttps化。

これも簡単にできて楽だったので、今後は同じようにサブドメインにどんどん生やしていく感じで色々作っていこうかなと思いました。

作業内容はこれ↓。

yarn

普段はnpmの方を使っているんだけど、今回はなんとなくこっちに。インストールが早いとかは聞いてるけれども、うーん特に不満はないけど利点を感じるほどでもなかった。npmも今はロックファイルあるし、普通にインストールしたら記録されるしなあ。

アプリの紹介

時計です。

ポモドーロのタイマー付きです。

おしまい

楽しかった。

Vue.jsで文字列が空のとき “Mismatching childNodes vs. VNodes” になった。

カテゴリー: JavaScript

Vue.jsで、こんなエラー(というか警告)が出た。

console.error()で &quot:[Vue warn]: The client-side rendered virtual DOM tree is not matching server-rendered content."

[Vue warn]: The client-side rendered virtual DOM tree is not matching server-rendered content. This is likely caused by incorrect HTML markup, for example nesting block-level elements inside <p>, or missing <tbody>. Bailing hydration and performing full client-side render.

再現コード

はこんな感じ。

<template>
  <section>
    <p><img />{{text}}</p>
  </section>
</template>

<script>
export default {
  data () {
    return {
      text: '',
    }
  },
}
</script>

たぶん、 <p> の直下には「 <img> 」と「 {{text}} から成るテキストノード」の二つがあるはずなんだけど、文字列が空だとそこにテキストノードが用意されないので、なんか予定と違うぞーと怒ってる感じだろうか。

解決策

とにかくテキストノードが存在していれば良いので、空白文字とか置いておこう。

text: '' を text: ' ' にするとか、同様に <template> の方にスペース追加するとか。

余白が嫌なら幅なしのやつ &zwnj; にしよう。

<template>
  <section>
    <p><img />&zwnj;{{text}}</p>
  </section>
</template>

とっぴんぱらりのぷう。

Vue+Webpackの開発がすごい楽ちんだ。(Vue.js始めるおれおれアドベントカレンダー2016 – 17日目分)

カテゴリー: JavaScript

Vue.js始めるおれおれアドベントカレンダー2016 – 17日目分(28日公開)

これこれ。

インストール

vue-webpack-boilerplate自体は特にインストール作業はなさそうなんだけど、その前にvue-cliが必要です。

$ npm install -g vue-cli

これでコマンドラインから vue コマンドが使えるようになります。 “vue-cli” はそのコマンドをインストールする的なやつで、ブラウザが読み込む vue.js とは別物。

開始

出力結果は省略してるけど、こんな感じ。

$ vue init webpack my-project
$ cd my-project
$ yarn install

開発サーバー起動。

$ npm run dev

Listening at http://localhost:8080

URLを開くと最初のページが出てきます。やったね。

テンプレートから生成された画面。

Ctrl+C で終了。

大丈夫そうなので、今回生成されたファイルを見ていきます。

(さらに…)

propsのvalidationをさらっと書けるvue-props-templateを作りました。(Vue.js始めるおれおれアドベントカレンダー2016 – 18日目)

カテゴリー: JavaScript

Vue.js始めるおれおれアドベントカレンダー2016 – 18日目

作りましたというか、まだもうちょっといじろうと思ってるんだけど、動きはします。

ginpei/vue-props-template: Write vue’s props through template literal.

Reactもそうなんだけど、オブジェクトを入れ子にしてく書き方、なかなか面倒じゃないですか。そういうのをなんかこう装飾子みたいにさらってできねえかなーそういやテンプレート文字列は改行も含められるんだなーあれでやれたらいいなーと考えて作ってみました。

propsの検証

Vueの機能として、型等を指定することができます。

こんな感じ。

const MyComponent = {
  props: {
    simpleString: { type: String },
    requiredNumber: { type: Number, required: true },
    defaultBoolean: { type: Boolean, default: true },
    validatedObject: {
      validator: function (value) {
        return (value.flag1 && value.flag2)
      }
    }
  }
}

vue-props-template

さっきの指定を、だいたいこう書けるようになります。

const pt = require('vue-props-template')

const MyComponent = {
  props: pt`
    string simpleString
    required number requiredNumber
    boolean defaultBoolean = true
  `
}

どうでしょうか、こういうの。

あとは validatedObject をどうしようかなあというところです。現状こんなん。

const pt = require('vue-props-template')

const MyComponent = {
  props: pt.extend(pt`
    object validatedObject
    `, {
      validatedObject: {
        validator: function (value) {
          return (value.flag1 && value.flag2)
        }
      }
    }
  }
}

よくわからないこと

便利?

個人的にはあんな風に書けたら便利だと思ってるんだけど、他の人はどうなんだろ。まあ自分で使う用でも構わないんだけど。

既存でこういうのないか?

ちょっと探して見つからなかったけどありそうで怖い。

validator の扱い

埋め込み値 ${…} の中に関数を書くのってどうかなあ。別に良いかなあ。例えばこういうの。

props: pt`
  validatedObject as ${function (value) {
    return (value.flag1 && value.flag2)
  }}
`

あーこういうのも悪くないかなあ。どうかなあ。

この場合、他の「型 名前」の順の書き方と違ってきちゃうけど、んーまあ良いか。

最近のライブラリの作り方

Vue本体を参考に、ブラウザで読み込んでもNode.js(vueify)経由でも動くようにはした。でも今はそれを直接元コードに書いてて、なんかこう「ビルド」をしてそういうのを出力するやり方の方が良いだろうか。

あとバージョン管理も実はあんまりよくわかってない。semverは良いんだけど、なんか手動でぽちぽちやるんじゃなくてnpm scriptに更新コマンドも用意して、なんて話を前聞いた気がする。

他にやること

これ全部終わったらv1.0.0ということにしようかなと思ってます。

  • validator の扱い
  • バベる
  • ドキュメントの見直し
  • GitHub pagesの充実
  • npm登録

そうか、要素に触るなら$refsとmountedを組み合わせれば良いのか。(Vue.js始めるおれおれアドベントカレンダー2016 – 16日目分)

カテゴリー: JavaScript

Vue.js始めるおれおれアドベントカレンダー2016 – 16日目分(24日公開)

しゅんしゅん動くよ。

アニメーション付きのナビバーを作ってみました、簡単でした。やったね。

基本的な作り方

  • location.hash を監視して情報更新
  • hashに該当する内容を表示
  • バー位置をhashの候補の順序から算出

簡単に作れたは良いんだけど、最後のやつどうしようかなと。

バー位置をhashの候補の順序から算出

最初に書いたコードはこう。

<nav>
  <a href="#">Home</a>
  <a href="#about">About</a>
  <a href="#contact">Contact</a>
  <span :style="underlineStyle"></span>
</nav>
const store = require('./store.js')

const hashes = ['', '#about', '#contact']

module.exports = {
  data () {
    return store.state
  },
  computed: {
    underlineStyle () {
      const itemWidth = 100
      const left = itemWidth * hashes.indexOf(this.hash)
      return {
        transform: 'translateX(' + left + 'px)'
      }
    }
  }
}

これで全然動くんだけど、疑問点が二つ。

  • 候補値 '', '#about', '#contact' をHTML側と共有しているの、どうにかならんかな
  • 項目の幅 100 をCSS側と共有しているの、どうにかならんかな

jQueryであれば実際の要素を見てあれこれするんだけど、Vueはそうはしないじゃないすか。普通。

$refs を使う?

とか言いつつ要素を見てあれこれするやつ、使ったらできるにはできた。

<nav ref="list">
  <a href="#">Home</a>
  <a href="#about">About</a>
  <a href="#contact">Contact</a>
  <span :style="underlineStyle"></span>
</nav>
computed: {
  /**
   * 項目の幅の実測値を返す。
   */
  itemWidth () {
    const elList = this.$refs.list
    const elItem = elList.firstElementChild
    return elItem.clientWidth
  },

  /**
   * 項目の `href` からhash候補値を得る。
   */
  hashes () {
    const elList = this.$refs.list
    const elItems = elList.children
    const hashes = Array.from(elItems).map(elItem => {
      let hash = elItem.getAttribute('href')
      if (hash === '#') {
        hash = ''
      }
      return hash
    })
    return hashes
  },

  underlineStyle () {
    let left

    // 最初のDOM構築の際には当然underlineStyleは呼ばれるが、
    // 最初だからDOMがまだないので、 `$refs` が使えない。
    if (this.$refs.list) {
      left = this.itemWidth * this.hashes.indexOf(this.hash)
    } else {
      // `hash` 変更時にキャッシュ値を更新するよう、
      // ここで呼んで記憶してもらう
      left = this.hash.length * 0
    }

    return {
      transform: 'translateX(' + left + 'px)'
    }
  }
},

うわあ、すごく危険な香りがする。

実際公式ガイドにもやめてねって書いてあるし。

$refs はコンポーネントが描画された後にのみ追加されます。そしてそれはリアクティブではありません。直接子コンポーネントを操作するための最終手段としての意味しかありません。テンプレートまたは算出プロパティの中での $refs の使用は避けるべきです。

うん。

テンプレートまたは算出プロパティの中での $refs の使用は避けるべきです。

そう思います。

マウントのタイミングで確認したい……

……マウント……ライフサイクル……ん?

module.exports = {
  data () {
    return {
      itemWidth: 999,
      hashes: [],
      state: store.state
    }
  },
  mounted () {
    const elList = this.$refs.list
    const elItems = elList.children

    this.itemWidth = elItems[0].clientWidth

    this.hashes = Array.from(elItems).map(elItem => {
      let hash = elItem.getAttribute('href')
      if (hash === '#') {
        hash = ''
      }
      return hash
    })
  },
  computed: {
    underlineStyle () {
      const left = this.itemWidth * this.hashes.indexOf(this.state.hash)
      return {
        transform: 'translateX(' + left + 'px)'
      }
    }
  }
}

なるほど、こういう感じか。これなら良さそう。