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

ただの文字列をコンポーネントに渡す話。(Vue.js始めるおれおれアドベントカレンダー2016 – 9日目)

カテゴリー: JavaScript

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

今まで二重引用符 "…" の中に一重引用符 '…' を書かないと駄目かなって思ってたんだけど。

<my-component v-bind:message="'Hello World!'"></my-component>
or
<my-component :message="'Hello World!'"></my-component>
MyMessage = {
    template: '<p>{{message}}</p>',
    props: ['message']
}

v-bind ではないHTMLの属性の書き方でいけることに気が付いた。

<my-component message="Hello World!"></my-component>

JS側は一緒。

そうだったのか!

デモ

よくわからないこと

仕様なの?

どこに書いてあるのかよくわからない。あるならここら辺だと思うんだけど。

いや考え方の順序が違うのかな。 props のところに『親コンポーネントからデータを受け取るためにエクスポートされた属性のリスト/ハッシュです。』とあるので、そもそもこれは「属性」を受け取るものであると。一方の v-bind もコンポーネントに渡す値を云々じゃなくて、あくまで「属性」を束縛するものであると。結果的にコンポーネントに情報を渡せるけど。

あ、合点がいったわ。

ESLintプラグインでVueの書式に合わせて書こう。(Vue.js始めるおれおれアドベントカレンダー2016 – 8日目)

カテゴリー: JavaScript

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

例示されるコードと自分が書くコードの書き方が違うのが気になってきたので、lintツールを入れてみようと思いました。

eslint-plugin-vue

Vue公式でESLintのプラグインが公開されていた。

これを導入してみる。

$ npm i -S eslint eslint-plugin-vue eslint-config-vue

で .eslintrc.js を用意する。

  • .eslintrc.js
module.exports = {
    "extends": "vue",
};

あ、こんだけで良いんだ。さてlint実行。

対象を src/ で指定するとJSファイルしか見てくれないので、 src/* にして *.vue も対象に含める。

$ ./node_modules/.bin/eslint src/*

/my/impressive/path/08-vue-lint/src/App.vue
   8:40  error  Extra semicolon                                    semi
  11:3   error  Expected indentation of 2 spaces but found 1 tab   indent
  12:4   error  Expected indentation of 2 spaces but found 2 tabs  indent
  13:3   error  Expected indentation of 0 spaces but found 1 tab   indent
  13:4   error  Unexpected trailing comma                          comma-dangle
  14:3   error  Extra semicolon                                    semi

/my/impressive/path/08-vue-lint/src/main.js
   1:27  error  Extra semicolon                                    semi
   2:29  error  Extra semicolon                                    semi
   3:14  error  Extra semicolon                                    semi
   5:33  error  Extra semicolon                                    semi
…

✖ 70 problems (70 errors, 0 warnings)

うは、なんかいっぱい言われてる。

自動修復

ESLintの機能だけど、 --fix を付けると簡単な(機械的に変換できる)書式エラーは自動的に修正してくれる。ファイルはGitで管理してるから機械変換しても怖くはないでしょう?

$ $ ./node_modules/.bin/eslint src/* --fix

/my/impressive/path/08-vue-lint/src/App.vue
   8:40  error  Extra semicolon                                    semi
  11:3   error  Expected indentation of 2 spaces but found 1 tab   indent
  12:4   error  Expected indentation of 2 spaces but found 2 tabs  indent
  13:3   error  Expected indentation of 0 spaces but found 1 tab   indent
  13:4   error  Unexpected trailing comma                          comma-dangle
  14:3   error  Extra semicolon                                    semi

/my/impressive/path/08-vue-lint/src/MyTitle.vue
   6:35  error  Extra semicolon                                    semi
   9:3   error  Expected indentation of 2 spaces but found 1 tab   indent
   9:17  error  Missing space before function parentheses          space-before-function-paren
…

✖ 51 problems (51 errors, 0 warnings)

エラーが70件から51件に減った。 *.js は全部自動で直してくれたけど、 *.vue はまるまる残ってる感じだ。

ESLintとしてはJSファイルじゃないものは対応しない方針の様子。まあそうだね。Stack Overflowでも「無理だよ☆」という回答だ……。

さてどうしたものか

JS部分だけ取り出してうまいことやれば自動変換で全て丸く収まりそうなんだけどなー。手作業で直して確認するより、手作業で自動変換できる形にして変換して戻す、みたいな形が好み。

自分の場合はVim使ってるので、こんな感じでJS部以外をコメントアウトしました。 <style> の中身には対応できないけど、今回はまだ書いてないからこれで良いや。

:g/^\s*</norm 0i///

で拡張子を変えまして。

  • App.js
///<template>
/// <div class="container">
///     <my-title></my-title>
/// </div>
///</template>

///<script>
    var MyTitle = require('./MyTitle.vue');

    module.exports = {
        components: {
            MyTitle
        },
    };
///</script>

さあどうだ。

$ ./node_modules/.bin/eslint src/* --fix
  • App.js
// /<template>
// /    <div class="container">
// /        <my-title></my-title>
// /    </div>
// /</template>

// /<script>
var MyTitle = require('./MyTitle.vue')

module.exports = {
  components: {
    MyTitle
  }
}
// /</script>

あーインデント変わっちゃったな。まあこんなもんか。ではコメントアウトを解除してファイル名を戻して。

:g/^\/\/ \//norm 4x

別にインデントはこれで怒られないけど、手動で戻しました。結局手動かあ……。

開発環境へ組み込む

ファイル変更を検知して、ビルドの前に検証するようにします。

  • package.json
{
  "scripts": {
    "start": "npm run vue-watch",
    "vue-build": "browserify -t vueify -e src/main.js -o out/app.js ",
    "vue-lint": "eslint src/*",
    "vue-watch": "chokidar src/* -c 'npm run vue-lint && npm run vue-build'"
  },
  "dependencies": {
    "browserify": "^13.1.1",
    "chokidar-cli": "^1.2.0",
    "eslint": "^3.11.1",
    "eslint-config-vue": "^2.0.1",
    "eslint-plugin-vue": "^1.0.0",
    "vue": "^2.1.4",
    "vueify": "^9.3.0",
    "vuex": "^2.0.0"
  }
}

ESLintを直接呼んだ方が、失敗時にnpmの出力が出なくて良いようにも思う。長いんだよなーあれ。

よくわからないこと

自動修正をもっとうまくやりたい

まあ仕方ないかこんなもんか。lintがある状態で書いてれば自動修正いらないし。

lint失敗時の出力を減らしたい

さっき言ったnpmのやつ。こういうの。

$ npm run vue-lint

> @ vue-lint /my/impressive/path/08-vue-lint
> eslint src/*


/my/impressive/path/08-vue-lint/src/App.vue
  45:4  error  Extra semicolon  semi

✖ 1 problem (1 error, 0 warnings)


npm ERR! Linux 4.8.4-200.fc24.x86_64
npm ERR! argv "/my/impressive/path "/my/impressive/path"run" "vue-lint"
npm ERR! node v5.7.1
npm ERR! npm  v3.8.2
npm ERR! code ELIFECYCLE
npm ERR! @ vue-lint: `eslint src/*`
npm ERR! Exit status 1
npm ERR! 
npm ERR! Failed at the @ vue-lint script 'eslint src/*'.
npm ERR! Make sure you have the latest version of node.js and npm installed.
npm ERR! If you do, this is most likely a problem with the  package,
npm ERR! not with npm itself.
npm ERR! Tell the author that this fails on your system:
npm ERR!     eslint src/*
npm ERR! You can get information on how to open an issue for this project with:
npm ERR!     npm bugs 
npm ERR! Or if that isn't available, you can get their info via:
npm ERR!     npm owner ls 
npm ERR! There is likely additional logging output above.

npm ERR! Please include the following file with any support request:
npm ERR!     /my/impressive/path/08-vue-lint/npm-debug.log

npm通さずに普通に呼ぶと簡単。

$ ./node_modules/.bin/eslint src/*

/my/impressive/path/08-vue-lint/src/App.vue
  45:4  error  Extra semicolon  semi

✖ 1 problem (1 error, 0 warnings)

うーんでもなー。lintだけ呼びたいこともあろうし、同じこと二度書きたくないし。

おしまい

これでだいぶ開発環境が整ってきた気がする。

コード

vueifyを使って良い感じにコンポーネントを扱えるようになったよ。(Vue.js始めるおれおれアドベントカレンダー2016 – 7日目)

カテゴリー: JavaScript

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

色々やろうとしたらやっぱりファイルの管理とかが手間なので、ぼちぼちツールを導入します。

なにそれ

Browserify transform for Vue.js components, with scoped CSS and component hot-reloading.

(Broserifyを使って)Vueのコンパイル的なアレをしてくれるツールです。対象のファイルはJSではなく、HTMLテンプレートとJSの両方を含む *.vue という拡張子のファイルだそうだ。

hot-reloading は自分の環境だと特にそれっぽい動きはしてくれてない。ウェブブラウザが別マシンだから?

ところでこういうのをコンパイルと呼んで良いんだろうか。

試行錯誤の記録

色々エラー出たりしたので置いておきます。

まず今まで使ってたひとつしかないJSファイルを単純にvueifyに突っ込んだら、ちゃんと動いてくれた。変換なしで通してくれたみたい。

続いてVueとVuexを require() で読み込むようにしたら、エラー。

Error: [vuex] must call Vue.use(Vuex) before creating a store instance.

あ、まあそうですね。さくっと Vue.use(Vuex) を追加。

で次。

[Vue warn]: Failed to mount component: template or render function not defined.
(found in root instance)

んーコンポーネントの template をHTMLに書いてちゃ駄目か。そりゃそうだ。

でJS側に持ってきても駄目だった。あれ? あーそうか本体(コンポーネントにしてない部分)もこっちに持ってこないとか。どうやら main.app から全体を司る App を render() で出力するのが定石らしい。(合ってる?)

でやったら、何故か画面が空っぽになる、何も出てこない……。と思ったら return してなかった。ああーそっか!

よしまあじゃあ一度全部消して、READMEの例の通りにやってみよう。

Error: Parsing file /path/to/you/work/dir/App.vue: 'import' and 'export' may only appear at the top level (23:0)

えー。

export default {} を module.exports = {} にしたら解決。よくわかってない。

ともかくこれで動いた!

ただ、最終的な出力ファイルの位置でエラーが出てくるのでわかりづらい。browserifyの方のオプションで --debug を与えるとソースマップを付けてくれるんだけど、うーんわかりづらい。

まとめ

環境

こんな感じになりました。

{
  "scripts": {
    "start": "npm run vue-watch",
    "vue-build": "browserify -t vueify -e src/main.js -o out/app.js ",
    "vue-watch": "chokidar src/* -c 'npm run vue-build'"
  },
  "dependencies": {
    "browserify": "^13.1.1",
    "chokidar-cli": "^1.2.0",
    "vue": "^2.1.4",
    "vueify": "^9.3.0",
    "vuex": "^2.0.0"
  },
}
  • src/ に元のファイルを、 out/ に出力ファイルを置くものとします。
  • npm run vue-build でビルドします。
  • npm run vue-watch でファイル監視してビルドします。
  • chokidar はファイル監視してコマンド実行するやつです。べんり!

Hello World!

  • src/main.js
const Vue = require('vue');
const Vuex = require('vuex');  // 今回使ってないけど
Vue.use(Vuex);

const App = require('./App.vue');

new Vue({
  el: '#app',
  render: function(createElement) {
    return createElement(App);
  },
});
  • src/App.vue
<style>
  h1 {
    font-style: italic;
  }
</style>

<template>
  <div class="container">
    <h1>{{message}}</h1>
  </div>
</template>

<script>
  module.exports = {
    data: function() {
      return {
        message: 'Hello World!',
      },
    },
  };
</script>

hello-vueify-world

はあー、なるほどこんな感じですか!

コンポーネント

別のファイルにして require() する感じ。無理やり感あるけど分割して、ついでにVuexも突っ込みます。

  • App.js
<template>
  <div class="container">
    <my-title></my-title>
  </div>
</template>

<script>
  var MyTitle = require('./MyTitle.vue');

  module.exports = {
    components: {
      MyTitle: MyTitle,
    },
  };
</script>
  • MyTitle.js
<template>
  <h1>{{title}}</h1>
</template>

<script>
  var store = require('./store.js');

  module.exports = {
    data: function() {
      return store.state;
    },
  };
</script>
  • store.js
const Vuex = require('vuex');

module.exports = new Vuex.Store({
  state: {
    title: 'Hello World!',
  },
});

見た目はさっきと一緒。

よくわからないこと

store.state を直接 data に指定しない?

data: store.state と書いたらこんなん言われた。

[Vue warn]: The "data" option should be a function that returns a per-instance value in component definitions.

毎回 function() 書いて return しないといけない?

export default を使うには?

これも未来JavaScriptの機能だよね。 import するやつ。例の通りに書いたのに駄目だった。コンパイル時にエラーになる。どうしたら良かったんだろうか。バベる? いつ?

hot-reloading

使えたら便利そうだけど。

lint

そろそろVue的な書き方に合わせたい。セミコロン欲しい派だけど、しばらく書いてれば慣れるだろう……。

おしまい

ビルドなしで強力なのが導入できて便利~というのがVueの最初の印象だったんだけど、結局ビルドするようになりました。まあ「ちゃんと」やろうとしたらそうなるよね。例えjQueryでも。とはいえ簡単だし、jQueryでちゃんと整えるよりやっぱり簡単じゃないだろうか。

computedすごいぞ賢いぞ! (Vue.js始めるおれおれアドベントカレンダー2016 – 6日目)

カテゴリー: JavaScript

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

Vueでは data, methods の他に、両者の特性を併せ持った computed というものを指定することができます。これすげーんでお見知りおきください。

data と methods

この二つはまあ普通なんですが、 data がデータ、 methods がメソッドです。そのまんまだ。

イニシャルを表示する例

JS側で getInitial() というメソッドを実装して、HTML側で {{getInitial()}} と書いて呼び出しています。

<div id="app">
  <p>
    <label>
      First Name:
      <input v-model="firstName" type="text" />
    </label>
    <br />
    <label>
      Last Name:
      <input v-model="lastName" type="text" />
    </label>
  </p>
  <h1>Hello {{getInitial()}}!</h1>
</div>
var app = new Vue({
  el: '#app',
  data: {
    firstName: 'ginpei',
    lastName: 'takanashi',
  },
  methods: {
    getInitial: function() {
      var firstFirstNameLetter = this.getInitialOf(this.firstName);
      var firstLastNameLetter = this.getInitialOf(this.lastName);

      var initial = firstFirstNameLetter + '.' + firstLastNameLetter + '.';
      return initial;
    },

    /**
     * @param {string} name
     * @returns {string}
     * @example
     * this.getInitialOf("alice");  // => "A"
     */
    getInitialOf: function(name) {
      return name.charAt(0).toUpperCase();
    },
  },
});

initial
イニシャルを表示する例

まあ普通ですね。

computed

computed を使うと、先ほどの例がこう書けます。(だいたい一緒なので違う部分だけ。)

<h1>Hello {{initial}}!</h1>
computed: {
  initial: function() {
    var firstFirstNameLetter = this.getInitialOf(this.firstName);
    var firstLastNameLetter = this.getInitialOf(this.lastName);

    var initial = firstFirstNameLetter + '.' + firstLastNameLetter + '.';
    return initial;
  },
},

HTML側では data のように名前を書くだけ、JS側では methods のように関数を実装しています。

何かをあれこれ処理しているのではなくて、ここでは(処理した)結果をただ表示するぞ、というのが分かりやすいかと思います。え、そうでもない?

キャッシュ

メソッドとの最大の違いは、結果がキャッシュされ毎回実行されないという点です。

ドキュメントから引用します。( message が data に格納されており、それを順序反転する処理 reversedMessage が computed で定義されている文脈です。)

算出プロパティの代わりに、同じような関数をメソッドとして定義することも可能です。最終的には、2つのアプローチは完全に同じ結果になります。しかしながら、算出プロパティは依存関係にもとづきキャッシュされるという違いがあります。算出プロパティは、それが依存するものが更新されたときにだけ再評価されます。これはつまり、message が変わらない限りは、reversedMessage に何度アクセスしても、関数を再び実行することなく以前計算された結果を即時に返すということです。

参照している値を自動的に監視して、最低限の実行で処理してくれるそうです。すんげー。

注意点

逆に、 data の値を参照していない場合、例えば先のドキュメントでは Date.now() が使われていましたこういう場合には、キャッシュされた値は更新されなくなってしまうようです。

メソッドはあまり使わない?

というわけで画面に出力する計算は computed でやるのが良さそうです。

Date.now() みたいにVue外から情報を得る場合は、取得した結果を data に入れてそれを computed で出力するーみたいな感じでしょうか。コンポーネントなら porps 経由で。

するとメソッドを使う場面は、今回みたいにDRY的に単純に処理をまとめるのと、イベントハンドラを定義するのくらいでしょうか。

ユーザー一覧の各項目で情報を加工する場合はどうだろ。 doSomething(list[i]) みたいなのが必要? いやコンポーネントに分けて、子の方で porps で受けた値を computed でやれば良さそう。

というのを作ってみた:

component-computed

うーんこんな感じなんだろうか。日時情報の扱いはもっと上手にやれそうな気がする?

Vuexをさっくりインストールしたけどまだよくわからない。(Vue.js始めるおれおれアドベントカレンダー2016 – 5日目)

カテゴリー: JavaScript

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

いよいよVuexを使ってみ始めることにしました。なんかまだよくわかってないけどこれ使うと良い感じになるんでしょ?

困ってること

TODOリストのフォームから新規作成した項目を一覧へ渡す際に、情報のやり取り、コールバックの呼び合いがなんか面倒だった。これを楽ちんにしたい。

デモ

こちらにございます。

vue-todo

どうやって使うんだろ

と思って見てたら “TodoMVC” といういかにもなのがREADMEのExamplesにあったのでそれを見てみます。

見た結果なんか色々他にも学ぶことがありそうだったのでそっと閉じて、ドキュメントの方を参照することにしました。

始めるを得る

んー自分の場合だと、こんなんだろうか。

var store = new Vuex.Store({
  state: {
    newTask: { finished: false, name: '' },
    tasks: [
      { finished: false, name: 'Buy milk 2L' },
      { finished: false, name: 'Call to Alice' },
      { finished: false, name: 'Return books' },
    ],
  },
  mutations: {
    add: function(state) {
      state.tasks.unshift(state.newTask);
      state.newTask = { finished: false, name: '' };
    },
  },
});
window.app = new Vue({
  data: store.state,
  methods: {
    newTask_submit: function(event) {
      store.commit('add');
    },
  },
  ...
});

やった動いた! これでコールバック類を削除できる。(今はひとつしかないけど。)

mutations?

mutation

主な意味: 変化、変更、転換、(世の)移り変わり、突然変異、突然変異体、母音変異

どうやらミュータント・ニンジャ・タートルズの「ミュータント」と同源らしい。はあ。あと最近のタートルズはごついな。

普通にオブジェクト

を直接参照するのと何が違うんだろう? 編集の方もコンポーネントに渡したオブジェクトを直接編集してもらってるし。

メソッド呼び出しは commit() を経由するようになるけど、でもそれくらい?

やってることが単純で簡単すぎるせいか、まだVuexの魅力に気付けていない……。もっと難しいアプリを作ったらわかるかしらん。

おしまい

なんかとりあえずVuexの導入はすげー簡単でした。いいのか?