Vuex のルールと Component 設計

こんにちは、自意識過剰な正義のヒーローでお馴染みの株式会社SCOUTERの石岡 将明( @masaakikunsan )です。

前回の Flutter のブログで、「2月半ばにAPIの叩き方やページ内のレイアウト作り方などをちゃんと解説したブログを書こうと思うのでお楽しみに!」と言ったのですが、あれ以降 Flutter を触れていないので今回は普段触ってる技術について書きます。

techblog.scouter.co.jp

現在 SCOUTER では、SARDINE と back check の大きく分けて2つの開発チームで開発を行っています。 僕は、back check で プレイングマネージャーとして開発をしているのですが、SARDINE チームに最近フロントエンドの相談を受けるようになってきたので今回はその話をしていきます。

Vuex でのルール

Vue や Nuxt で開発している場合状態管理に Vuex を使っているかと思います。 なので、今回は Vuex の概念や書き方については省略し僕が開発するときのルールを紹介します。

namespaced を true にしよう

namespaced を true にすることで、モジュールごとに自動的に名前空間が付与されます。 これがないと、複数のモジュールが同じミューテーション/アクションタイプに反応することができてしまい大きくなったときに破綻してしまうでしょう。

Nuxt で開発をしている場合、モジュールモードが上記の namespaced=true になります。

ja.nuxtjs.org

mapState を使わない

Vuex には mapGetters ヘルパーが存在します。 mapGetters はゲッターの評価後の値を返すコンポーネントの computed オプションを作成します。 Vuex の state を使いたい時に filter したりやなんらか変更したいというモチベーションは結構あると思います。 そのような理由から getters では state を加工しないデータも全て乗せ mapGetters で統一するとわかりやすくなって良いでしょう。

mapMutations を使わない

state を変更する唯一の方法はミューテーションです。 なのでコンポーネント内で直接 mapMutations を使ってミューテーションをコミットする方が結構いますが、 actions でミューテーションをコミットするのが良いでしょう。 そうすることでコンポーネント内で mapActions ヘルパーを使うだけでよくなりわかりやすくなりますし、管理が楽になります。

厳格モードにしよう

普通にルールにしたがってやってたらこれは本来しなくていいのですが、結構いろんなプロジェクトで state をミューテーションハンドラの外部で変更してるコードを見ます。 なのであらかじめ厳格モードにしミューテーションハンドラ外部で state を変更できないようにしましょう。

Nuxt で開発している場合デフォルトで厳格モードになっているので特に設定などはいりません。

ja.nuxtjs.org

state を全て Vuex に逃すのをやめよう

結構 Redux を書いている人は state を全て Vuex に逃しているのですがこれは store が肥大化したり助長でまわりくどくなりいいことはありません。 状態がコンポーネント内で完結するのであればそれはローカルステートに持たせましょう。

モジュールの切り方

以前はページ毎 Vuex のファイルを作成していたのですが、そうすると複数ページで共通する state がでてきて困りました。 今はドメインごとにファイルを作成するようにしています。 そうすることで、違うモジュールの state をいじる必要性は基本的にないでしょう。

以上が僕が Vuex を書く上でのルールです。 次に Component の設計について書いていきます。

Component の設計

Component を設計する上でよく耳にするのが「再利用性の高い」です。 僕は、Component を設計する上で再利用性はあまり考えないようにしてます。 まずはじめにその理由について書きます。

再利用性の高いコンポーネント

再利用性の高いコンポーネントを作る上で、3つのことが必要だと僕は思っています。

  1. 実装者の能力
  2. デザイナーの能力
  3. 時間

1に関しては言わずもがなだとは思いますが、実装者の能力がない場合依存性が高かったり等再利用性のあまり高くないコンポーネントになってしまうことが多いでしょう。

2は以外と頭にない人が多いのですが、これも大事です。 デザイナーはコンポーネント志向をまず理解している必要があります。次に過去・未来を考え再利用性ができるものは再利用ができるように設計しデザインをしなければいけません。

デザイナーが実装もできる人間であればこれらは当然頭にあると思うので問題ないと思うのですが、必ずしも上記をデザイナーができる必要があるわけではありません。 エンジニアと協業し、しっかりとコミュニケーションをとりやれば上記を満たすデザインができるでしょう。

3の時間は当たり前だろとは思うと思うのですが、口で言うほど再利用性の高いコンポーネントは簡単には作れないので思ってる以上に時間はかかるよという話です。 再利用性の高いコンポーネントを作る上で、コンポーネントの粒度・ロジックのシンプルさ等色々と考えることがあります。 なので時間はかかるものと考えるのが良いでしょう。

再利用性の高いコンポーネントを作る難しさはここまででわかっていただけたと思います。 本題の再利用性をあまり考えなくていいと考えている理由ですが、大きい理由としてはどうせ使わないというのがあります。

プロダクト開発をしている人であれば、割と経験あると思うのですが、再利用性を考えてコンポーネントを作り込んだは良いがその後デザインで使われることがなかってのはざらにあります。 そういうことを考えると再利用性の高いコンポーネントを作る難しさとコストを考えるとあまり再利用性はあまり意識しないほうが良い意思決定でしょう。

僕は2回から3回同じデザインがでてきたら切り離しコンポーネント化するようにしています。

次にコンポーネントを作るときに考えることを書いていきます。

コンポーネントを作る上で考えていること

僕は、コンポーネント作る上で以下3つを意識しています。

  1. コンポーネントの役割を考える
  2. 依存を少なくする
  3. なるべく状態を持たないようにする

コンポーネントの役割を考える

コンポーネント志向で開発をしている場合 Presentational Component と Container Component を耳にしたことあると思います。 基本的にコンポーネントはこの分類で考えましょう。

Presentational Component とは、View に特化したコンポーネントで、自身では状態は持ちません。 Presentational Component では、コンポーネントに閉じた状態を除き状態を持たず、props で渡ってきたデータを表示するようにします。

Container Component とは、Presentational Component にデータを渡したり、機能をもたせたりと子コンポーネントの振る舞いを定義していきます。

この2つで大枠分類して進めると状態管理とかでも悩んだり基本的にしないでしょう。 ただ例外も存在するとは思うのでその都度チームで話しあいルールを決めると良いかとおもいます。

Nuxt で開発する場合、僕は基本的にpages に Container の役割をもたせています。 コンポーネントに閉じていない状態やロジックや Vuex による操作は全て pages で行うようにしており、そうすることで pages からコンポーネントへ上から下に流れるようなイメージで開発が進められて良いです。 ただ、pages が肥大化する場合もあるのでそのような場合は Container Component を作成し、pages と Presentational Component の間に挟んでいます。

依存を少なくする

これは、再利用性にも関わってくる話です。 依存が大きいと再利用性もしずらくある一定の場所でしか使えなくなってしまうデメリットがあります。 そして依存が大きいコードはレビュアーのコストも上がります。 依存している箇所のコードを読みしっかりと理解していないと正しいレビューができないデメリットがあります。

なので、再利用性は考えなくてもいいですが、なるべく依存は少なくしましょう。 そうすることで、修正なども用意に行えます。

なるべく状態を持たないようにする

状態を持つということはそれだけでその状態に振り回される為依存が大きいということです。 状態をもたせるかどうか責務で考え、状態をもって良いか良くないかをしっかりと考えましょう。 ここまでを踏まえて考えれば状態を持つべきかどうかはわかるかと思います。

コンポーネントを作成する時は、しっかりとそのコンポーネントの責務などを考えて粒度やデータの持ち方を考えて作成しましょう。

さいごに

Vuex のルールや Componentの設計について書きましたがいかがだったでしょうか? 他にも考えていることはあると思うのですが、言語化できていないので今回はこれぐらいにしようかと思います。 状態管理やコンポーネントの設計は トライ&エラーを繰り返していくしか身につける方法はないと僕は考えています。 なので、いっぱい失敗して自分なりのルールを見つけられるようになりましょう。 この記事がどこかで役に立つと幸いです。

現在、back check チームでは、エンジニアの募集をしております。 僕と一緒にフロントエンドを朝まで語り尽くしたいフロントエンドエンジニアやレベルの高いフロントエンドチームと最高のプロダクトを作って行きたいサーバーサイドエンジニアは是非ご応募お願いいたいします!

www.wantedly.com

www.wantedly.com

SaaSのPdMが持つべき3つの考え方

はじめに

こんにちは、株式会社SCOUTERの開発責任者の小平(@ryotakodaira )です。 業務では、SARDINEという人材紹介会社向けの業務管理システムを開発しています。

SARDINEは月額課金型のビジネスモデルでユーザーに提供されれており、いわゆるSaaSと呼ばれるカテゴリのサービスとなっております。 弊社の場合は、人材紹介会社向けというかなり限定的なユーザーをターゲットにしているバーティカルSaaSとなっております。

バーティカルSaaSについてはこちらの記事で説明されています。

boxil.jp

基本的には自社で開発しているプロダクトを中心にお客様の課題を解決し、それを長期に渡ってご利用いただくことでビジネスが成り立っています。

前述の通り事業上プロダクトが中心にあるため、プロダクトマネージャーとしてそのプロダクトの方針を決定する役割のメンバーが必要になります。

プロダクトマネージャーはプロダクト開発のスペシャリストでありながらも同時に事業戦略を深く理解し、ビジネス課題の発見から解決といった期待役割を持つ非常に責任の範囲が広いポジションであり、そのポジションの広さ故、何を大切にしながら動けばよいのかがわからなくなってしまうこともあります。

僕自身そのような経験がありますが、その中でも今の自分を構成した常に持ち続けるべき考え方や姿勢を紹介していきます。

定量評価と定性評価のバランス感覚

ここで言う定量評価はデータによる評価、定性評価はユーザーや事業に関わる者の声としています。

データによる評価はプロダクトが 今どこにいてどういう状態なのか を測るためためには非常に有効になります。

最近はデータによる意思決定こそが重要という声を聞くことが多くなりました。僕自身それは間違ってはいないと考えております。 しかし、意思決定をするときにデータに頼りすぎるのも危険である場合もあるため定量と定性のバランスを大事にしています。

例えば、立ち上げフェーズなどではユーザー数もそこまで多くないため統計的に信憑性のあるデータが集まるまでには時間がかかる場合が多いですが、プロダクトマネージャーは事業戦略に基づいて意思決定をし続けなければなりません。そのときにデータによる定量評価や仮設を定性評価によって裏付けることでプロダクトを前に進めるようにしていました。

その後はデータによる定量的な評価を行うことができるように開発時に計測ポイントの定義とコードを仕込むことをやり続けるのがポイントです。

中長期を見据えた判断

よくプロダクトマネージャーはプロダクトにおけるCEOと言い表されることがあります。 僕自身プロダクトマネージャーというポジションをやってみてまさにその通りだと感じています。

更にプロダクトマネージャーは会社のCEOや経営陣ですら判断できないことについて意思決定する必要がります。 何がそれに該当するかというと、プロダクト開発における攻めと守りのバランスの判断です。

通常、プロダクトは事業が継続している限り常に開発が続き、時間と共にコードベースは大きくなっていくことでしょう。そしてその過程で生み出された技術的負債と今後ずっと付き合っていくこととなります。

立ち上げフェーズが終わったプロダクトは 短期的にビジネスをより伸ばすための開発長期的に安定的な成長を阻害しないための開発(技術負債の返済) の両方のバランスを取って開発案件の意思決定する必要がります。

この意思決定には事業戦略への深い理解とシステムの状態を正確に把握していることが前提条件となるため、プロダクトマネージャー以外には正しく判断することが出来ません。プロダクトマネージャーは短期〜長期を総合的に考え、一見遠回りになりそうな開発でも将来像からの逆算で判断する能力が必要となります。

プロダクトに対する姿勢

バーティカルSaaSのようなユーザーのビジネス課題を解決するためのサービスは一般的に開発チームのメンバーや自分自身がそのサービスのユーザーになることはないと思います。

しかし、プロダクトを中心に事業を展開している以上、事業に関するドメインの深い知識が求められます。自分に普段馴染みのない業種だっとしてもそれを自分ごとにとらえてソリューションの1つとしてプロダクト開発に落とし込むことをプロダクトマネージャーには求められます。

また、プロダクトマネージャーはプロダクトについて全責任を負い、言い訳をせずに失敗から学びそれを克服する姿勢が必要となります。

それと、反感を買うかもしれませんがプロダクトを成長させるために自分の全てをつぎ込むくらいの決意と姿勢があるくらいの方が個人的には良いと思っています。笑

まとめ

プロダクトマネージャーとは開発チームに属しながらもエンジニアやデザイナーとは全く違う視点が求められる不思議なポジションになります。

それでもプロダクト開発においては必ず必要なポジションで有ることは間違いなく、これという正解が無い中で開発チームに価値の高いを仕事をしてもらえるかはプロダクトマネージャーの手腕にかかっています。

責務範囲が広く大変なポジションではありますが、ユーザーにとって 最高の課題解決のソリューション となるプロダクトを創れる人が増えて欲しいですし、この記事から何か気づきを発見してもらえたら嬉しいと思っております。

最後に

事業・サービスが成長していくにあたって、これからもメンバーを増やしていきたいと思っています。

新規事業や既存事業の拡大も考えているため将来のPM候補も絶賛募集中です!

興味のある方は下記からご応募いただくか、@ryotakodairaにご連絡ください!!

www.wantedly.com

www.wantedly.com

www.wantedly.com

Vue.jsイベント修飾子.stopと.preventの使いどころ

こんにちは!SCOUTERでフロントエンドエンジニアをしている匠平です。 SCOUTERではフロントエンドをVue.js(Nuxt.js)で開発しています。

Vue.jsのイベント修飾子の使いどころがいまいち掴めていなかったので、簡単な事例を挙げて理解していきたいと思います。

Vue.jsのイベント修飾子

イベント修飾子は以下の6種類があります。

  • .stop
  • .prevent
  • .capture
  • .self
  • .once
  • .passive

公式ドキュメント イベント修飾子

中でも使用頻度が比較的多い.stop.preventの使い方を見ていきましょう。

使い方と使いどころ

.stop

.stopはJavaScriptのstopPropagationを呼び出します。

現在のイベントのさらなる伝播 (propagation) を止めます。

MDN event.stopPropagation

Vue.jsのドキュメントには、親要素への伝搬を止める、とあります。Vue.jsのイベント修飾子.stopは、子要素のイベントが親のイベントを呼び出さない、ということを設定するわけですね。

<!-- クリックイベントの伝搬が止まります -->
<a v-on:click.stop="doThis"></a>

.stopの動作サンプル

サンプルとして、彼女の実家の呼び鈴モデルを作りました。

実家住まいの彼女の家に訪問するとき、なるべく彼女のお父さんには会いたくないですよね。そんなときにこのイベント修飾子.stopが有効です。

codepen.io

buttonタグを押すと、彼女がお出迎えするアラートが表示されます。2つのbuttonタグはいずれもお父さん(fatherクラスを持ったdiv)に囲まれていて、同じようにクリックイベントを持っています。過保護なお父さんなんですかね。

普通に呼び鈴を押してしまうと、彼女のあとにお父さんも出てきてしまいます。お父さんを呼び出さないよう彼女に伝えておきましょう。

buttonのクリックイベントに.stopを与えてあげると、見事父親ブロックを回避することができました。

子要素に.stopを与えることで、親要素への伝搬を止めるという動作です。実生活でも活用していきたいですね。

.prevent

.preventはJavaScriptのEvent.preventDefaultメソッドを呼び出します。

Event インターフェースの preventDefault() メソッドは、イベントが明示的に処理されない場合に user agent に、そのデフォルトアクションを通常どおりに行うべきではないと伝えます。

MDN Event.preventDefault()

.preventの動作サンプル

どのような場面で使うのか、こちらもサンプルで見ていきましょう。

codepen.io

フォームにテキストを入力してsubmit送信するとアラートが発動します。

上側のフォームで送信してアラートを閉じると、画面が更新されてフォーム内の文字列が消えてしまいます。これはそもそもsubmitがaction属性で画面遷移を想定しているからですね。

下側のフォームで送信してアラートを閉じても、フォームに入力した文字列はそのまま残ってるのが分かります。ここではフォームのsubmitに.preventを与えています。

意図しない画面遷移、画面更新を避けるため.preventが有効なんですね。

submitが成功したかどうかをモーダルやポップアップで表示したいことがあると思いますが、画面が切り替わって正しく動作しないことがあるので.preventが必要になります。

まとめ

.stop.preventは使用頻度が多いので、これらの動作をしっかり覚えておきたいですね。

この記事がお役に立てれば嬉しいです。

最後に

SCOUTER社では私たちと一緒に働く仲間を募集中です! ご興味のある方はぜひご応募ください!

www.wantedly.com

www.wantedly.com

Laravel/Vue.js勉強会#7

こんにちは、SCOUTERでフロントエンドエンジニアのhirokiです。

LaraVue勉強会も皆様のおかげで第7回を迎えることが出来ました!いつもたくさんの方にお越しいただき運営をしている側としては嬉しい限りです!

今回株式会社オープンロジさんのご協力のもと池袋にある貸会議室で行いました!

発表

初めてのLaravelで学習を始めるまでのお話

弊社SCOUTERで10月からjoinしてくれたshowさんの発表です! 結局公式ドキュメントを信じようという話をしてくれました!

f:id:hiroki-nishizawa:20190201132942j:plain 自分でいろいろ勉強してわからなくなって調べて、その調べたものが分からないという話を、聞いていて僕も意味わかんなかったな。。。とか、今でもわからないな。。。とか後ろで見ながら思っていましたw とてもテンポの良くておもしろいLTでした!僕も頑張っていきます!

speakerdeck.com

コンシューマ向けVue SPAでプロダクトを開発して得た知見。

今日2/1から弊社SCOUTERにjoinしてくださるjiyuujinさんの発表です! f:id:hiroki-nishizawa:20190201130026j:plain

SPAでプロダクトを開発していたということで「にゃんこスタジオ」さんで得た知見をすべてではないですがお話してくれました! これから一緒に頑張りましょう!よろしくおねがいします!

master.d1xmp1dbc0142d.amplifyapp.com

実運用におけるLaravelとNuxtでのRepositoryのレイヤ分割の話

上記タイトルに有るように「実運用におけるLaravelとNuxtでのRepositoryのレイヤ分割」についてkon_shouさんが話してくれました!

f:id:hiroki-nishizawa:20190201124725j:plain

デメリットでも書いてある初見殺し。。。自分も思っていましたw ただメリットを見た時に責任分担を明確にできるというところはこれから人数増えてきた時にとてもいいと思いました!

speakerdeck.com

VueのUIフレームワークをまとめました2019

株式会社プラムザの中の人をやっているplumsaさんのLTです!

f:id:hiroki-nishizawa:20190201122157j:plain

VueのUIフレームワークgithubスターランキングや直近3ヶ月のnpmインストール数をランキング形式にして発表してくれました! 弊社ではelement-uiを使用しているのですが、レスポンシブ対応をしていないのを初めて知りました!@plumsaさんが使っているVuetify。。。使ったことがないので使ってみようかなと思います!

Testing on Laravel

弊社SCOUTERで業務委託で仕事をしてくれているnunulkさんのLTです! f:id:hiroki-nishizawa:20190201123941j:plain いろいろなテストのやり方について発表してくれました! 個人的にテストをガッツリ書いたことがないのでModel Factory + Fakerが気になりました!これからもどんどんテストを書いていきましょう!

speakerdeck.com

懇親会

発表後はちょっとしたフードとドリンクを用意したのでみんなでわいわい懇親会をしました! f:id:hiroki-nishizawa:20190201120434j:plain

まとめ

今回SCOUTERの人が3人も出てしまうという初めての勉強会でした!が、LTをやっていただいた5名それぞれいろいろな角度からLaravel,Vueについて話して、とても有意義な時間でした!

次回はの日程、場所ともに未定なので、次回以降会場提供してくださる方いましたら弊社CTO @kotamatまでご連絡お願いします!

最後に

SCOUTER社では一緒に頑張ってくれる方を募集しております。 デザイン、エンジニアの皆さん興味のある方はご応募お願いします!

www.wantedly.com

FlutterでWebエンジニアが1日でアプリを作った

こんにちは、株式会社SCOUTERの石岡 将明( @masaakikunsan )です。

2019年が始まり、一ヶ月が過ぎようとしていますが、皆さんはどうお過ごしでしょうか? 私は、プログラミングを始め2年が経ちWeb以外にも手をつけていこうかと考えている今日このごろです。

ということで、今回は弊社サービス SARDINEAPIと Flutter で求人が見れるアプリを作ったので Flutter について書いていこうと思います。

Flutter とは

flutter.io

AndroidiPhoneアプリ開発を行うためのフレームワークです。 Flutter の開発は、 Google によって開発された Dart というプログラミング言語を使用します。

Flutter の特徴

  • 同じコードを使用して、AndroidiPhoneアプリ開発ができる
  • Hot Reload により、リアルタイムで変更が確認できる
  • Material や Cupertino といった Widget を使用することでアプリっぽい見た目を簡単につくれる

なぜ Flutter を選んだのか

冒頭でも述べた通り、私は2019年はアプリにも手を出そうと考えていました。 アプリをやるなら、Swift や Kotlinなどいろいろ選択肢がある中で私は React が書けるので React Native に手を出そうとしていました。

そんな中とある日に、「JavaScript のような Java のようなプログラミング言語Dart で開発できる Flutter というフレームワークがあるよ」と天の声が聞こえ、お!と思い Twitter に やるぞの意思表示をしたところ 実質 React + TS と言われたので触らない理由はなかったです。

私は、 普段 JS で開発をしており、フリー時代には Javaフレームワークである Spring Boot でAPI開発もしたことがあり、React 開発経験者でもある為 Flutter に興味を持ち触ることにしました。

f:id:masaakikunsan:20190130120702p:plain

入門

それでは早速、Flutter の入門を解説していきます。

まずはじめに Flutter SDK をダウンロードします。下記 URL からダウンロードしましょう。

flutter.io

SDK のダウンロードが終わったら、好きな場所で解凍してください。

次に、Flutter の コマンドをどこからでも使えるように、PATH の登録を行います。 私は zsh を使っているので .zshrc に以下を追加しました。

export PATH=$PATH:$HOME/flutter/bin

追加したら、反映するために下記コマンドを実行します。

$ source ~/.zshrc

これで Flutter が使える状態です。

開発をするための設定

今回は iOS の開発をする予定だったので XCode の設定をしました。 App Store か 公式サイトから XCode をインストールしましょう。

インストールが完了したら、ライセンスに同意しましょう。

$ sudo xcodebuild -license

これで Flutter 開発をするまでの準備が整いました。

プロジェクトの作成

プロジェクトを作成しきましょう。 flutter create <project_name> をターミナルで実行することでプロジェクトを作成できます。 作成したら起動してみましょう。

$ cd project_name
$ flutter run

そうすると Counter のアプリが開かれるかと思います。

デモコードの解説

ディレクトリを見ると lib フォルダがあります。 ここにアプリケーションのプログラムを書いていきます。

さっそく lib/main.dart のコードを見ていきましょう。

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        // This is the theme of your application.
        //
        // Try running your application with "flutter run". You'll see the
        // application has a blue toolbar. Then, without quitting the app, try
        // changing the primarySwatch below to Colors.green and then invoke
        // "hot reload" (press "r" in the console where you ran "flutter run",
        // or simply save your changes to "hot reload" in a Flutter IDE).
        // Notice that the counter didn't reset back to zero; the application
        // is not restarted.
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  // This widget is the home page of your application. It is stateful, meaning
  // that it has a State object (defined below) that contains fields that affect
  // how it looks.

  // This class is the configuration for the state. It holds the values (in this
  // case the title) provided by the parent (in this case the App widget) and
  // used by the build method of the State. Fields in a Widget subclass are
  // always marked "final".

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      // This call to setState tells the Flutter framework that something has
      // changed in this State, which causes it to rerun the build method below
      // so that the display can reflect the updated values. If we changed
      // _counter without calling setState(), then the build method would not be
      // called again, and so nothing would appear to happen.
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    // This method is rerun every time setState is called, for instance as done
    // by the _incrementCounter method above.
    //
    // The Flutter framework has been optimized to make rerunning build methods
    // fast, so that you can just rebuild anything that needs updating rather
    // than having to individually change instances of widgets.
    return Scaffold(
      appBar: AppBar(
        // Here we take the value from the MyHomePage object that was created by
        // the App.build method, and use it to set our appbar title.
        title: Text(widget.title),
      ),
      body: Center(
        // Center is a layout widget. It takes a single child and positions it
        // in the middle of the parent.
        child: Column(
          // Column is also layout widget. It takes a list of children and
          // arranges them vertically. By default, it sizes itself to fit its
          // children horizontally, and tries to be as tall as its parent.
          //
          // Invoke "debug painting" (press "p" in the console, choose the
          // "Toggle Debug Paint" action from the Flutter Inspector in Android
          // Studio, or the "Toggle Debug Paint" command in Visual Studio Code)
          // to see the wireframe for each widget.
          //
          // Column has various properties to control how it sizes itself and
          // how it positions its children. Here we use mainAxisAlignment to
          // center the children vertically; the main axis here is the vertical
          // axis because Columns are vertical (the cross axis would be
          // horizontal).
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

コードを読めばだいたい想像つくと思うので細かい説明は省きます。

画面表示は Widget と呼ばれる部品によって作成されています。 Sample Codeでは StatefulWidget つまりステートを持てる Widget でボタンとテキストをもったClassがあり、それを MyApp class のHomeに設定しています。 MyApp classではtitleの設定や、themeの設定もしており、main関数を使ってアプリを起動しているといった感じです。

作成物

今回は、SARDINE API を使用し、ログインし求人が見れるアプリを作りました。 Navigator でページ遷移したり、API を叩いてログインや求人を取得したりしています。

昨日 Flutter の勉強を始め、1日で作ったのでもう少し詳しくなったタイミングでコードと一緒に紹介するブログを書かせてください。

まとめ

Flutter は Web エンジニアでもすぐにかじることができ良いフレームワークでした。 しかし、用意されている Widget を検索したり仕様を把握しないといけないのでちゃんと書けるレベルになるにはかなりドキュメントを読む必要があるなと感じました。 また、アプリでは Web と違い localStorage が使えなかったりと State 周りでかなり苦労しそうです。 次ちゃんと触るときは Redux が使えるようなので Redux で状態管理も考えながら作成しようかなと思います。

2月半ばにAPIの叩き方やページ内のレイアウト作り方などをちゃんと解説したブログを書こうと思うのでお楽しみに!

最後に

現在、株式会社SCOUTERでは、エンジニア、デザイナーの募集をしております。

興味のある方は、是非下記からご応募お願い致します!

www.wantedly.com

www.wantedly.com

www.wantedly.com

markdown-itの導入方法

こんにちは!
株式会社SCOUTER開発部フロントエンドエンジニアの佐藤(@r_sato1201)です

先日、業務でCMSでコンテンツを管理するLPをNuxtプロジェクトで作成しました。 その際に、Markdownをhtmlにパースする必要がありmarkdown-itを触ったので、 備忘録も兼ねてNuxtプロジェクトを使用する際のmarkdown-itについて書きたいと思います。

導入方法

@nuxtjs/markdownitのインストール

まずは、markdown-itのnpmパッケージをインストールします。

yarn add @nuxtjs/markdownit

nuxt.config.jsにmarkdownitの設定を記述

nuxt.config.jsにnuxtjs/markdownitのモジュール読み込みと主なオプションを記述します。 ※各オプションデフォルト値です

nuxt.config.js

modules: [
    '@nuxtjs/markdownit'
  ],
markdownit: {
    preset: 'default'
    injected: true, 
    breaks: true, 
    html: true, 
    linkify: true,
    typography: true, 
    xhtmlOut: true,
    langPrefix: 'language-',
    quotes: '“”‘’',
    highlight: function (/*str, lang*/) { return ''; },
  },

各オプションを説明します。

オプション名 説明
preset パースするファイルの規格(Markdown/CommonMark)を指定します (※詳細は後で記述します)
injected $mdを利用してMarkdownをhtmlにパースする
breaks Markdown内の改行コードを<br>に変換する
html HTMLタグを有効にする
linkify URLに似たテキストをリンクに自動変換する
typography 言語に依存していない引用符などを綺麗にする
xhtmlOut 単一のタグを閉じるには 「/」を使用する
langPrefix コードブロックのCSSクラス名の接頭辞に付加します。
quotes typographyが有効になっていて'"を同時に使用している際に自動で識別し変換してくれます。
highlight 第1引数で指定したタグ名に対して、第2引数で指定した言語名のシンタックスハイライトを行う処理を指定できます。返り値にシンタックスハイライト後の内容を返すよう指定できます。

CommonMarkとMarkdownについて

CommonMarkとはMarkdownで曖昧になっている細部の構文仕様を明確化したものです。 (CommonMarkについて詳しく知りたい方は以下を参照して下さい)

commonmark.org

CommonMarkとMarkdownでは細かい仕様の差異があります。 例えば、リストの見出しです。 Markdownでは、見出しを以下のように記述することが出来ます。

# Heading

CommonMarkでは、これはリスト内の見出しとして解釈されてしまうため、エスケープする必要があります。

\# Heading

このような差異があるため、前述したpresetで規格を指定することにより 適切にhtmlにパースすることが出来ます。

利用方法

$md を使ってパースする場合

オプションのinjected:trueに設定すると、$mdという変数が使えます。 sample.vue

<template>
  <div v-html="$md.render(model)"></div>
</template>

<script>
export default {
  data() {
    return {
      model: '# Hello World!'
    }
  }
}
</script>

CMSからMarkdown形式のデータが渡ってきている場合は $md.render(model) 上記のrenderの引数に、Markdown形式のデータを渡すことでパースされHTMLにレンダリングされます。

.mdファイルを使ってパースする場合

どこかのディレクトリに格納されているMarkdownファイルをimportしcomputedで返し、v-htmlでレンダリングすることで表示できます。
sample.md

# Hello World!!

sample.vue

<template>
  <div v-html="sample"></div>
</template>

<script>
  import sample from '../sample.md'

  export default {
    computed: {
      sample() {
        return sample
      }
    }
  }
</script>

パースしたhtmlにスタイルを当てる際の注意点

v-html によって作成された DOM コンテンツはの子要素には、data 属性がつかなく、 <style scoped>とスコープをつけCSSの影響範囲をコンポーネント内に収めている場合 data属性がついていない要素にはCSSが当たりません。
つまり、markdown-it で$md を使ってパースする場合、通常通りCSSを当ててもうまく効かないので ディープセレクタを使用して当てるようにしましょう。

※ディープセレクタを仕様する際はsass環境が必要となるのでnode-sass および sass-loaderのパッケージをインストールしてください

sample.vue

<template>
    <div class="sample-wrapper">
      <div v-html="$md.render(model)"></div>
    </div>
</template>

<script>
export default {
  data() {
    return {
      model: '# Hello World!'
    }
  }
}
</script>

<style  lang="scss" scoped>
.sample-wrapper /deep/ h1 {
  border-left: solid 2px #6ac5b6;
}
</style>

拡張方法

markdown-itには機能を拡張できるパッケージが複数あります。 まず、yarnなどでパッケージをインストールします。

yarn add markdown-it-<パッケージ名>

nuxt.config.jsのmarkdownitオプションに以下の記述でパッケージを読み込むよう設定することで markdown機能を拡張することができます。

nuxt.config.js

markdownit: {
    use: [
      'markdown-it-<パッケージ名>',
    ]
  },

いくつかの拡張パッケージを紹介します。

脚注

f:id:ryonnsui1201:20190123230953p:plain

ブログなどで使う脚注を使用することが出来ます。 使用する際は脚注を指定したい箇所に[^n]を記述し 脚注として表示したい内容は[^n]:を先頭に付けます。

sample_footnote.md

これは脚注[^1]のテストです

[^1]:これは1つ目の脚注の内容です

導入方法は以下となります。

yarn add markdown-it-footnote

nuxt.config.js

markdownit: {
    use: [
      'markdown-it-footnote',
    ]
  },

ハイライト

f:id:ryonnsui1201:20190123231001p:plain

指定箇所がmark要素で囲まれ、ハイライト表示をすることができます。 使用する際は適用させたい箇所の前後に==を記述します。 デフォルの背景色は黄色ですが、mark要素にcssを当てることで自由なスタイルで表示させることが出来ます。

sample_highlight.md

==ハイライト表示==のテストです

導入方法は以下となります。

yarn add markdown-it-mark

nuxt.config.js

markdownit: {
    use: [
      'markdown-it-mark',
    ]
  },

Youtube埋め込み

YouTubeの埋め込みをすることができます。 使用する際は以下のサンプルのように、[]内にyoutubeを指定し()内に表示させたい動画のIDを指定します。 動画のIDではなく、動画のURLでも可能です。
sample_video.md

[youtube](movie id)

導入方法は以下となります。

yarn add markdown-it-video

nuxt.config.js

markdownit: {
    use: [
      'markdown-it-video',
    ]
  },

さいごに

私のような経験が浅いエンジニアの方の力に少しでもなることができればと思い書きました。

現在、株式会社SCOUTERでは、エンジニア、デザイナーの募集をしております。

興味のある方は、是非下記からご応募お願い致します!

www.wantedly.com

www.wantedly.com

www.wantedly.com

normalizrの使い方

こんにちは株式会社SCOUTERでフロントエンドエンジニアをしているhirokinishizawaです。

弊社サービスである「人材紹介会社向けの業務管理システム」SARDINEで新しく無料業務管理ツールをリリースしました。 無料業務管理ツールを開発するにあたり設計段階で複雑にネストされるのがわかっていたのでnormalizrを導入することになりました。 今回このブログでは導入したnormalizrの使い方を書いていきます。

normalizrとは

normalizrはデータを正規化するためのライブラリです。 公開されているAPIでも、自社サービスで作成しているAPIでも複雑にネストされているデータを扱うことが多々あると思います。

複雑にネストされているデータを取り扱うのはなかなか大変ですが正規化する事によりEntity毎にidをキーとしたオブジェクトになるためidで辿ることでデータを取ってくることができます。

開発環境

nuxt: 2.2.0
normalizr: 3.3.0

インストール

yarn add normalizr
or
npm install normalizr

使い方の説明

やりたいこと

冒頭で正規化するとidをキーとしたオブジェクトになりidで辿ることでデータを取ってくることが出来るという話を少ししたと思いますが、実際にどのように使うのかを正規化していないデータをstore/module/denormalize.js、正規化したデータをstore/module/normalize.jsのstateに保管して比較しながら使い方の説明をして行きたいと思います。

実際に正規化していないデータと正規化したデータが以下のようになります。

正規化していないデータ

store/module/denormalize.js

// state.posts
  posts: [
    {
      id: 100,
      text: 'テスト1',
      user: {
        id: 100002,
        name: 'Michael',
      },
      comments: [
        {
          id: 200,
          text: 'コメント1',
          post_id: 100,
          user: {
            id: 100000,
            name: 'Jone',
          },
        },
        {
          id: 201,
          text: 'コメント2',
          post_id: 100,
          user: {
            id: 100001,
            name: 'Mary',
          },
        },
      ],
    },
    {
      id: 101,
      text: 'テスト2',
      user: {
        id: 100001,
        name: 'Mary',
      },
      comments: [
        {
          id: 202,
          text: 'コメント3',
          post_id: 101,
          user: {
            id: 100000,
            name: 'Jone',
          },
        },
        {
          id: 203,
          text: 'コメント4',
          post_id: 101,
          user: {
            id: 100000,
            name: 'Jone',
          },
        },
      ],
    },
  ],

正規化したデータ

store/module/normalize.js

// state.posts
  posts: {
    100: {
      id: 100,
      text: 'テスト1',
      user: 100002,
      comments: [200, 201],
    },
    101: {
      id: 101,
      text: 'テスト2',
      user: 100001,
      comments: [202, 203],
    },
  },
// state.comments
  comments: {
    200: {
      id: 200,
      text: 'コメント1',
      post_id: 100,
      user: 100000,
    },
    201: {
      id: 201,
      text: 'コメント2',
      post_id: 101,
      user: 100001,
    },
    202: {
      id: 202,
      text: 'コメント3',
      post_id: 102,
      user: 100000,
    },
    203: {
      id: 203,
      text: 'コメント4',
      post_id: 103,
      user: 100000,
    },
  },
// state.users
  users: {
    100000: {
      id: 100000,
      name: 'Jone',
    },
    100001: {
      id: 100001,
      name: 'Mary',
    },
    100002: {
      id: 100002,
      name: 'Michael',
    },
  },

このようなデータになるまでの説明をしていきたいと思います。

normalizrを実行してstateに保管する

始めにSchema情報を定義してnormalizrを実行するコードを書きます。


import { normalize, schema } from 'normalizr'

// userを表すスキーマ定義
const user = new schema.Entity('users');

// commentを表すスキーマ定義
// commentの中には `user` というデータがありそこに先ほど定義した`user`をセットします。
const comment = new schema.Entity('comments', {
 user: user
});

// postを表すスキーマを定義します
// postにも`user`というデータがあり先ほどと同じように先ほど定義した`user`をセットします。
// postには`comments`というデータがあり`user`と同じように先ほど定義した`comment`をセットします。
const post = new schema.Entity('posts', { 
  user: user,
  comments: [ comment ]
});

// 引数(data)には正規化していないデータを渡します。
export const normalizePost = (data) => {
  return normalize(data, post)
}

引数のdataには先程の正規化する前のデータを入れることにより以下のような正規化されたデータが生成されます。

results: [100, 101],
entities: {
  posts: {
    100: {
      id: 100,
      text: 'テスト1',
      user: 100002,
      comments: [200, 201],
    },
    101: {
      id: 101,
      text: 'テスト2',
      user: 100001,
      comments: [202, 203],
    },
  },
  comments: {
    200: {
      id: 200,
      text: 'コメント1',
      post_id: 100,
      user: 100000,
    },
    201: {
      id: 201,
      text: 'コメント2',
      post_id: 100,
      user: 100001,
    },
    202: {
      id: 202,
      text: 'コメント3',
      post_id: 101,
      user: 100000,
    },
    203: {
      id: 203,
      text: 'コメント4',
      post_id: 101,
      user: 100000,
    },
  },
  users: {
    100000: {
      id: 100000,
      name: 'Jone',
    },
    100001: {
      id: 100001,
      name: 'Mary',
    },
    100002: {
      id: 100002,
      name: 'Michael',
    },
  },
}

上記のデータの各項目ごとにstateを保管すればデータの保管は完了です。

正規化したデータの使い方

次に先ほどstateにいれた正規化されたデータの使い方を正規化していないデータと比較しながら書いていきます。

正規化していない場合

store/module/denormalize.js

const getters = {
  getPostById: (state) => (postId) => state.posts.filter((post) => {
    return post.id === postId
  })
}

index.vue

<template>
  <div>
// 投稿した内容
    {{post.text}}
// 投稿したユーザー名
    {{post.user.name}}
  </div>
  <div v-for="(comment, key) in post.comments" :key="key">
// コメントテキスト
    {{comment.text}}
// コメントしたユーザー名
    {{comment.user.name}}
  </div>
</template>

<script>
  computed: {
    ...mapGetters({
      getPostById: 'post/getPostById'
    }),
    post() {
      return this.getPostById(100)
    }
  }
</script>

this.getPostById(100)のデータ

    {
      id: 100,
      text: 'テスト1',
      user: {
        id: 100002,
        name: 'Michael',
      },
      comments: [
        {
          id: 200,
          text: 'コメント1',
          post_id: 100,
          user: {
            id: 100000,
            name: 'Jone',
          },
        },
        {
          id: 201,
          text: 'コメント2',
          post_id: 100,
          user: {
            id: 100001,
            name: 'Mary',
          },
        },
      ],
    },

正規化した場合

store/module/normalize.js

const getters = {
  getPostById: (state) => (postId) => state.posts[postId],
  getCommentById: (state) => (commentId) => state.comments[commentId]
  getUserById: (state) => (userId) => state.users[userId]
},

index.vue

<template>
  <div>
// 投稿した内容
    {{post.text}}
// 投稿したユーザー名
    {{contributor.name}}
  </div>
  <div v-for="(comment, key) in post.comments" :key="key">
// コメントテキスト
    {{getCommentById(comment).text}}
// コメントしたユーザー名
    {{getUserById(getCommentById(comment).user).name}}
  </div>
</template>

<script>
  computed: {
    ...mapGetters('store', [
      'getPostById',
      'getCommentById',
      'getUserById',
    ]),
    post() {
      return this.getPostById(100)
    },
    contributor() {
      return this.getUserById(this.post.user)
    }
  }
</script>

this.getPostById(100)のデータ

    {
      100: {
        id: 100,
        text: 'テスト1',
        user: 100002,
        comments: [200, 201],
      },
    },

this.getUserById(this.post.user)のデータ

  100002: {
      id: 100002,
      name: 'Michael',
    },
  },

正規化することによりcommentやuserのように少し複雑にネストされていてもidで取り扱うことがてきるようになります。

まとめ

サンプルデータではあまり複雑にネストされていないので、normalizrの実行するのに少し書くコード量を増やせばidをキーとしたオブジェクトに変換してくれるぐらいと思った人もいるかと思います。ですが普段みなさんが扱っているデータは上記なんかよりもっと複雑にネストされているかと思います。複雑にネストされていてもnormalizrを実行することでデータの階層がなくなりEntity毎にidで辿れるということでロジックがシンプルになり、総合的にみたらコード量も減るというとこが魅力的だなと感じました。

最後に

サービスの成長と共にこれから一緒に切磋琢磨していけるメンバーを増やしていきたいと思っています。

興味のある方は下記からご応募いただくか、hirokinishizawaにご連絡ください!

www.wantedly.com

www.wantedly.com

www.wantedly.com