GitHub Ribbon GeneratorをVue.jsで書いた
Vue.js Advent Calendar 2015 - Qiita 25日目
GitHub Ribbon Generator
GitHub Ribbon Generatorというツールを作りました。
GitHub Ribbonsというのは、右上にあるFork Meボタンのことです。
- GitHub Ribbons 公式の解説
使い方
- GitHub Repository
- Position(右か左)
- Color
を入力してあげれば、そのままコピペ出来るGitHub RibbonのHTMLが出来上がるので後はコピペするだけです。
仕組み
このツール自体はコピペしてURLを書き換えとか毎回やるの面倒だなと思って作った程度ですが、 もう一つの目的としてはVue.js 1.0.0を使ってみたかったというのがあります。
大規模アプリケーションの構築 - vue.jsにかかれていること大体実装した感じになっています。 Vue.jsというと2 wayのgetter/setterを使ったデータバインディング的な感じですが、アプリが大きくなると辛くなるというのはわかっていました。(フォームとかは簡単で便利ですが)
なので、目的としては以下の2つになっています。
- React + Fluxみたいなデータの流れは実装ができるのか
.vue
というフォーマット?での書き心地を確かめたい
大したことはやってないのでazu/github-ribbon-generatorを直接見たほうが速いです。
state
このツールではstateは3つしか出てきません。
- GitHub Repository
- Position
- Color
これをUserStoreという適当なものへ出し入れ出来るようにしています。
// ChangeEmitterは
import ChangeEmitter from "./ChangeEmitter";
class UserStore extends ChangeEmitter {
constructor() {
super();
this.state = {
repositoryURL: "",
position: defaultOptions.position,
color: defaultOptions.color
};
}
setState(state) {
Object.assign(this.state, {}, state);
this.emitChange();
}
getState() {
return Object.assign({}, this.state);
}
}
const emitter = new UserStore();
export default emitter;
.vue
.vue
はVue.jsが公式に提供してるBrowseryとWebpackから扱えるフォーマットで、HTML/CSS/JSを一つにまとめたファイルです。
このツールのRoot Componentとなってるapp.vueを見てみると分かりやすいと思います。
<style>
.App {
padding-left: 2em;
padding-right: 2em;
margin-left: auto;
margin-right: auto;
padding-top: 1em;
max-width: 768px;
}
.App h1, .App h2 {
text-align: center;
font-weight: 100;
margin: 0;
}
.App .header {
margin-bottom: 1rem;
}
</style>
<template>
<div class="App">
<h1>GitHub Ribbon Generator</h1>
<div class="header">
<h2>
<iframe src="https://ghbtns.com/github-btn.html?user=azu&repo=github-ribbon-generator&type=star&count=true&size=large"
frameborder="0" scrolling="0" width="160px" height="30px"></iframe>
</h2>
<h2>Create Copy-Pastable GitHub Ribbon HTML snippet</h2>
</div>
<user-input :repository-url="state.repositoryURL"
:color="state.color"
:position="state.position"></user-input>
<copy-paste-box :repository-url="state.repositoryURL"
:color="state.color"
:position="state.position"></copy-paste-box>
<git-hub-ribbon :repository-url="state.repositoryURL"
:color="state.color"
:position="state.position"></git-hub-ribbon>
</div>
</template>
<script>
import UserInput from "./user-input.vue";
import GitHubRibbon from "./github-ribbon.vue";
import CopyPasteBox from "./copypaste-box.vue";
import Store from "../store/UserStore";
export default {
name: "App",
// App's state
data () {
return {
state: Store.getState()
};
},
components: {
UserInput,
GitHubRibbon,
CopyPasteBox
},
methods: {
// update State
updateState () {
const state = Store.getState();
this.state = Object.assign({}, state);
}
},
created () {
Store.onChange(this.updateState);
},
destroyed(){
Store.removeChange(this.updateState);
}
}
</script>
<style>
に書いたものがそのまま<style>
要素として追加されています。<style scoped>
とすればカプセル化もできます。
<template>
がそのコンポーネントなので、分かりやすく<div class="App">
で囲んでいます。<script>
はVueのコンポーネントのコードです。
Reactだと<style>
がない事以外は大体同じですが、<style>
が同じ所に書けるのでコンポーネントのスタイルを簡単に適応するのがやりやすいです。
Reactの場合も、コンポーネント毎にCSSファイルを作ってスタイルの設定をしていたので、同じ事がそのままできるのは分かりやすいです。
これはSUIT CSSという命名ルールと殆ど同じで、MyComponentというコンポーネントには.MyComponentというクラス名をつけるという命名ルールです。
逆に分かりにくい所としては、.Vue
でもcomponents
に子コンポーネントの.vue
を読み込んだインスタンスを渡す事で、<template>
の中でそのコンポーネントをタグとして書くことが出来ます。
(JSXでも大体同じ)
components: {
UserInput,
GitHubRibbon,
CopyPasteBox
},
しかし、.vue
ではcomponents
に渡した名前そのままではなく、ケバブケースにした名前で<template>
に書く必要があります。
例えば、UserInput
ならばuser-input
というようになります。(keyで指定すればそのkeyで書くことが出来ます)
<user-input :repository-url="state.repositoryURL"
:color="state.color"
:position="state.position"></user-input>
HTML の属性は大文字と小文字を区別しません。キャメルケースされた prop 名を属性として使用するとき、それらをケバブケース(kebab-case: ハイフンで句切られた)として使用する必要があります:
このケバブケースで書かないといけないのが、警告もでなくてとても分かりにくい感じでした。
(ReactのclassName
とか、Riotのriot.tag関数の制限とかも似た話ですが)
コンポーネントとデータ
ReactのPropsと同じですが、Vue.jsもprops
というの子コンポーネントで宣言してあげると、親コンポーネントから値を受け取れます。
親(App.vue)からは
<user-input :repository-url="state.repositoryURL"
:color="state.color"
:position="state.position"></user-input>
と3つの値を渡したいで、子となるuser-input.vueでは
props: {
repositoryURL: String,
position: String,
color: String
},
と宣言しています。
受け取れるデータのタイプも書くことができ、React.PropTypesよりはシンプルなので分かりやすいです。
Why is React's PropTypes naming inconsistent with JS? number, object, string, array — all good — but then "func" and "bool". Um...
— kangax (@kangax) September 24, 2015
<style>
.UserInput .UserInput-field {
margin: 1rem 0;
}
</style>
<template>
<div class="UserInput">
<form class="pure-form pure-form-aligned">
<fieldset>
<div class="UserInput-field pure-control-group">
<label>GitHub Repository:</label>
<input class="pure-input-2-3" type="text" v-model="repositoryURL"
placeholder="https://github.com/jquery/jquery">
</div>
<div class="UserInput-field pure-control-group">
<label for="position">Position:</label>
<select id="position" v-model="position">
<option v-for="position in positionList" :value="position.value">
</option>
</select>
</div>
<div class="UserInput-field pure-control-group">
<label for="color">Color:</label>
<select id="color" v-model="color">
<option v-for="color in colorList" :value="color.value">
</option>
</select>
</div>
</fieldset>
</form>
</div>
</template>
<script>
import Store from "../store/UserStore";
import {colorList, positionList} from "../util/ribbon";
export default {
name: 'UserInput',
props: {
repositoryURL: String,
position: String,
color: String
},
data() {
// http://jp.vuejs.org/guide/forms.html#Select
// Create [{ text, value }]
return {
colorList: colorList.map(function (color) {
return {text: color, value: color}
}),
positionList: positionList.map(function (position) {
return {text: position, value: position}
})
}
},
watch: {
repositoryURL(newVal, oldVal) {
Store.setRepositoryURL(newVal);
},
color(newVal, oldVal) {
Store.setColor(newVal);
},
position(newVal, oldVal) {
Store.setPosition(newVal);
}
}
};
</script>
<template>
ではその受け取った値を元に表示するHTMLを書くという感じです。
Vue.jsではwatch
プロパティに書いたキー名で、Vueインスタンス[キー名] を監視する機能があるので、
これで値が変わったらStoreの値を書き換えるStore.set*
を呼んでいます。
(vm.$watchの宣言的なバージョンですね)
Storeに直接setter的なメソッドが生えてますが、この辺をAction的なものを経由するようにしたりすれば、大体Fluxと似たようなデータフローになると思います。
今回はFluxフレームワーク的なライブラリを使わずにEventEmitterのみで書いてます。
公式でもFluxライクなフレームワークは作ってるらしいので、その辺を見てみると面白いかもしれません。
まとめ
- React + Fluxみたいなデータの流れは実装ができるのか
- => まあ普通にできそう
.vue
というフォーマット?での書き心地を確かめたい- => HTML/CSS/JSがまとまった感じ
- JSXに比べるとCSSも一緒なのは分かりやすい
- テンプレートがちょこちょこ難しい
データフローの話は公式でもvuejs/vuexみたいので模索していそうです。
Vue.jsで適当に書くとデータバインディングに頼って、どこで何が更新されているのか分からなくなるみたいな事が起きやすい印象です。 そのため、複雑になったものからデータフローをどう整理するかを色々考えてみると面白そうです。 (Reactでもいい気はしていますが)
.vue
はCSSのカプセル化もあり結構いい感じですが、ただの独自フォーマットであるのでやり過ぎると後戻りできなくなる場合があるので気をつける必要がありそうです。
- Browserify、Webpackのプラグインとして実装されているので全てをコントロールできなくて破壊的な変更が起きる可能性
- vue-loaderは特に何でも出来る感じなので、何でもやると危なそう
また、テンプレートの構文がちょこちょこ複雑(評価結果が見た目から直感的に分からない)な所があったりします。
この辺は独自のテンプレート言語を持つ宿命という感じがするので慣れなのかもしれません。
ただ、今回のツールだと大体何で書いても大した違いはない気がします。 以下の比較も読んでみると面白いかもしれません。
以上、JavaScriptの素振りの話でした。
お知らせ欄
JavaScript Primerの書籍版がAmazonで購入できます。
JavaScriptに関する最新情報は週一でJSer.infoを更新しています。
GitHub Sponsorsでの支援を募集しています。