PHPerKaigi 2019 に参加・登壇してきました。

こんにちはみなさん ゴツメ装備エンジニアこと @niisan-tokyo です。
3/30, 31 でPHPerKaigiに参加・登壇してきましたので、その報告となります。
(3/29の前夜祭は会社の締め会で出れなかったのです。)

PHPerKaigi 2019

PHPerKaigiはPHPer同士の相互交流を主なテーマとし、去年から始まりました。

phperkaigi.jp

このイベントは相互交流に重きをおいているだけあって、トークにおいては必ず質疑の時間がタイムテーブルに盛り込まれていたり、
気軽に聞けるCIやフレームワークの相談会があったり、突発的に喋りたくなった人のためのアンカンファレンススペースがあったりしまして、
様々な楽しみ方ができるようになっています。

また、このイベントの大きな特徴としては運営が非常に親切かつ丁寧というところにあります。
去年、私が「ビールしかないんかぁ」(お酒飲めない人) とつぶやいた直後に烏龍茶がデプロイされたりして、少しの不満も迅速に解消しようという不断の努力を感じます。

なお、LTの時間にビールがデプロイされる予定だったらしいですが、手違いで届いていなかったようです。 これに一番ショックを受けていたのが、当の実行委員長だったというね。

FOOD

私のいつものスタイルとして、カンファレンスで出てきた食事を紹介しています。
今回は朝昼晩と全部あったので、それぞれ載せてみました。

f:id:niikura23:20190401082405j:plain
朝ごはん

朝ごはんはドーナッツですね。
朝から糖分を補給し、頭の回転を良くしようという考えではないだろうかと推測したりしなかったり。

f:id:niikura23:20190401083100j:plain
ランチ

ランチにデプロイされたお弁当ですね。
メルカリさんのランチセッションを聞く前に食べ尽くしてしまいました。

f:id:niikura23:20190401083406j:plain
懇親会ディナー

懇親会で出てきたディナーです。
ちゃんと麦茶もあったりしたので、満足できました。

トーク

たくさんのトークがあったのですが、私が登壇で頭が一杯になってたりするので、印象に残っているのだけ紹介します。

speakerdeck.com

以前から講演をしてもらったり、講演を聞いたりしている郡山先生ですが、とにかく見ている世界が違うのかと思うほどの内容で、これを聞けただけでも参加した意味があったなって思ったほどです。
とりあえず、無心になって資料を見るなり公演を聴くなりして、「REST」ってなんなのかって言うことを、見つめ直すべきだなって思います。

speakerdeck.com

LTのネタで川島さんが話した内容ですが、これ、レギュラートークで聞きたかったなぁって思います。 クラスの依存関係を可視化できる、できたてのツールでして、私も後で使ってみようかなって思います。

登壇

今回は幸運にもCFP通ったので、私も登壇してきました。

f:id:niikura23:20190401085442p:plain
登壇時のワタシ

緊張しいたわけではないですが、下むいちゃってますね。 これは反省点ですな。。。

発表資料はこちらです。

speakerdeck.com

内容は見てもらえばわかると思いますが、言及されていない内容に非同期のときのAuthの挙動の説明がないので、補足しておきます。

  1. 一郎くんがログイン => Auth::user() == 一郎くんになるがクリーナーによってリセット
  2. 次郎くんがログイン => Auth::user() == 次郎くんになるがクリーナーによってリセット
  3. 一郎くんが自身の名前を参照 => Auth::user() == 一郎くんになり、viewの描画が始まる前に5秒待つ
  4. 次郎くんが自身の名前を参照 => Auth::user() == 次郎くんになり、viewの描画が始まる前に5秒待つ
  5. 一郎くんが待ち時間後に自身の名前を参照 => Auth::user() == 次郎くんの状態でviewが描画され、クリーナーによってリセット
  6. 次郎くんが待ち時間後に自身の名前を参照 => Auth::user()はクリーナーによりリセットされており、ログアウト状態になった

という、面倒な経緯でした。

まとめ

というわけで、PHPerKaigi 2019 に参加してきたレポートでした。
来年は弊社のメンバーがもっと参加するといいな!

さいごに

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

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

www.wantedly.com

www.wantedly.com

www.wantedly.com

Vueのコンポーネント単体テストを始めてみよう

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

最近、社内でLaravelのテストについての会話が活発に交わされるようになりました。 その会話を聞いていて、Vueのテストはないのだろうか?と興味を持ったのでテストの導入方法と、簡単なテストを動かしてみたことについて書いてみました。

なぜテストをするのか?

Vueの公式ドキュメントにはテストの利点についてこう記述してあります。

コンポーネント単体テストにはたくさんの利点があります:

コンポーネントがどう動作すべきかのドキュメントを提供します
・過度な手動テストの時間を節約します
・新しい機能におけるバグを減らします
・設計を改良します
リファクタリングを容易にします

自動テストは大規模な開発チームが複雑なコードベースを保つことを可能にします。

テストを書くことで、コンポーネントがどういう振る舞いをするべきかや、 コンポーネントの設計に問題があることに気づけるメリットがあるようです。

テストの導入方法

Vueにはvue-test-utilsというVueコンポーネント単体テストのための公式ライブラリが用意されています。上記に加え、テストフレームワークJestを使うことで簡単にテストを導入することが出来ます。

新しくプロジェクトを作成する際は vue-clivue createコマンドを用いればvue-test-utilsJestを簡単にインストールすることができます。

f:id:ryonnsui1201:20190327034808p:plain

Unit Testingを選択

f:id:ryonnsui1201:20190327034823p:plain

Jestを選択

既存のプロジェクトに導入する際は、devDependencies にvue-test-utils、Jestをインストールしましょう。

yarn add -D @vue/test-utils babel-jest jest vue-jest

※babelを使っていない場合、babel-jestは不要です。

Jestカバレッジも簡単に取ることが出来ます。
package.jsoncollectCoverageオプションを加えることでカバレッジを取ることができるようになり、collectCoverageFromオプションにカバレッジ収集対象のファイルを配列で定義することができます。

"collectCoverage": true,
"collectCoverageFrom": ["**/*.{js,vue}", "!**/node_modules/**"],

f:id:ryonnsui1201:20190329102122p:plain

更に詳しいオプションの詳細については、 Jest configuration documentationも参考にしてみてください。

テストを実践してみよう

テストの書き方を掴むために、propsによる描画のテストを行ってみます。

以下のようなpropsを持つ簡単なコンポーネントを作成します。

RemoveUser.vue

<template>
  <div class="User">
    <span>{{name}}</span>
    <template v-if="isAdmin">
      <button @click="removeUser()">削除</button>
    </template>
  </div>
</template>

<script>
  export default {
    props: {
      id: Number,
      name: String,
      isAdmin: Boolean
    },
    methods: {
      removeUser() {
        this.$emit('removeUser', {id: this.id})
      }
    }
  }
</script>

それでは、実際にテストを行ってみましょう。
プロジェクトのtests/unit内にテストファイルを作成します。
テストは.spec.js.test.jsというファイルを作成すれば自動で検知してくれます。

yarn test:unitでテストを実行します。

また、--watchを指定することで、テストコードに変更を検知して、変更があるたびにテスト実行することができます。

RemoveUser.spec.js

import {mount} from '@vue/test-utils'
import RemoveUser from '@/components/RemoveUser.vue'

describe("user", () => {
  const adminPropsData = {
    id: 1,
    name: 'ユーザー1',
    isAdmin: true,
  }
  const userPropsData = {
    id: 1,
    name: 'ユーザー1',
    isAdmin: false,
  }
  test('emitされるか', () => {
    const wrapper = mount(RemoveUser, {
      propsData: adminPropsData
    })
    wrapper.find('button').trigger('click')
    // イベント発火してるかどうか
    expect(wrapper.emitted('removeUser')).toBeTruthy()
    // emit時にidが渡されてくるかどうか
    expect(wrapper.emitted('removeUser')[0]).toEqual([{id: 1}])
  })
  test('nameが正しく表示されているか', () => {
    const wrapper = mount(RemoveUser, {
      propsData: adminPropsData
    })
    expect(wrapper.find('span').text()).toBe(adminPropsData.name)
  })
  test('isAdminの値によって削除ボタンが表示されてるか', () => {
    const adminWrapper = mount(RemoveUser, {
      propsData: adminPropsData
    })
    const userWrapper = mount(RemoveUser, {
      propsData: userPropsData
    })
    expect(adminWrapper.find('button').exists()).toBeTruthy()
    expect(userWrapper.find('button').exists()).toBeFalsy()
  })
})

テストコードを箇所に分けて、説明していきます。

test('emitされるか', () => {
    const wrapper = mount(RemoveUser, {
      propsData: adminPropsData
    })
    wrapper.find('button').trigger('click')
    // イベント発火してるかどうか
    expect(wrapper.emitted('removeUser')).toBeTruthy()
    // emit時にidが渡されてくるかどうか
    expect(wrapper.emitted('removeUser')[0]).toEqual([{id: 1}])
  })

ここでは、コンポーネント内でクリックイベントが発火したときに 正しくemitされているかを確認しています。

ここで

・ボタンをクリックしたときにemitが動いているか
・emit時に、propsで渡したidが返ってくるか
が分かります。

f:id:ryonnsui1201:20190328210955p:plain

test('nameが正しく表示されているか', () => {
        const wrapper = mount(RemoveUser, {
            propsData: adminPropsData
        })
        expect(wrapper.find('span').text()).toBe(adminPropsData.name)
    })

ここでは、propsで渡したnameが正しく描画されているかを確認しています。

f:id:ryonnsui1201:20190328211618p:plain

test('isAdminの値によって削除ボタンが表示されてるか', () => {
  const adminWrapper = mount(RemoveUser, {
    propsData: adminPropsData
  })
  const userWrapper = mount(RemoveUser, {
    propsData: userPropsData
  })
  expect(adminWrapper.find('button').exists()).toBeTruthy()
  expect(userWrapper.find('button').exists()).toBeFalsy()
})

ここでは、権限によって削除ボタンが表示の出し分けをしているかを確認しています。

ここで

・isAdmin = true(管理者) の場合、削除ボタンが表示されている
・isAdmin = false(一般) の場合、削除ボタンが表示されていない

ということが分かります。

f:id:ryonnsui1201:20190328212226p:plain

ちなみに、開発をする上では
① 実装する機能の要件を元にテストコードを書く
② テストコードのに表現されている要件を満たすコードを書く
③ テストが成功する状態を維持しつつリファクタリングしていく

という順番で行うと良いと思います。

さいごに

フロントのテストはどんなものなんだろうと、軽い気持ちでテストについて調べてみました。 導入自体は簡単で手軽に始められますが、何をテストするべきか、しないべきかの切り分けが非常に難しく重要であると感じました。 今回は、Vueコンポーネント単体テストの導入方法、シンプルなテストに留めましたが、 今後、Vuex周りのテストやE2Eテストについても調べ学習していきたいと思います。

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

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

www.wantedly.com

www.wantedly.com

www.wantedly.com

参考資料

1年間のプログラミングを通して得た3つの教訓

はじめに

こんにちは株式会社SCOUTERでエンジニアをしているhirokinishizawaです。 業務では、SARDINEという人材紹介会社向けの業務管理システムを開発しています。

2019年2月1日でプログラミングというものに出会ってから1年が経ち楽しい時期やつらい時期もありましたが、1年間実務だけやってきた自分が得た教訓をプログラミングを始めたばかりの方やこれから始めてみたいと思っている方に共有をし、少しでもお役に立てればいいなぁと思います。

自分の経歴

入社する前

自分は日大付属の高校に通っていて特にやりたいこともなく大学受験をせずに卒業しました。(決して、決して行けなかったわけではないです!!多分。。。)

卒業をした後も特にやりたいことはなく主に軌道工事、塗装工事、屋上防水工事などいわゆる建築現場職人の仕事をしたり、土日などは父親の仕事の手伝いで3D図面を書いていたりしました。

そんな何の目標もないまま月日が流れ2018年1月に兄の繋がりで弊社CEO中嶋に出会いエンジニアという職業を知りその場で2月から入社をさせていただくことになりました。

入社してから

入社してからの流れは下記のようになります

ProgateでHTML&CSS、JavaScript、PHPを勉強
↓
オウンドメディア作成(jsはほぼやってもらった)
↓
サービスLP作成
↓
NuxtMeetUpのLT登壇
↓
サービス開発

このような流れでサービス開発に合流しました。

ありきたりかも知れませんが自分の得た教訓をこれから話していこうかと思います。

1.遠慮するのは時間の無駄

これはエンジニアに限った話ではないですが経験者、未経験者問わず入社してからわからないことは多々あるかと思います。

  • みんな他の仕事があるし質問するのは悪いのではないか。
  • 質問の回答が自分の求めていた回答ではなく、その時に別の聞き方ができずもう一度同じ質問するのが気まずい。
  • そもそも何で詰まってるのかがわからないから質問が出来ない。

質問したけど質問した内容の意味が分からない時に苦い顔するので再び質問しに行きにくいだったり、皆忙しいから質問しずらいだったり。いろいろな思いがあり質問しに行きずらいことがあるかと思いますが全て時間の無駄使いです、気にせず質問しましょう。

もちろん時間を割いて教えてくれるのですから最低限考えて質問しなければいけないとは思いますが、わからないものは一人で考えても分からないので、とにかく一つ一つ確認しましょう。

実際に自分は結構周りの目を気にしてしまうタイプで半年近くもの期間を無駄にしたなぁと今でも後悔しています。ですが困ったことがあったら質問する。再び分からなくなったら質問するを繰り返した結果自分の成長を実感できたのも事実です。

  • 自分が質問する時に気をつけていたこと
    • コードベースで話が出来ないぐらいわからない時はタスクベースでなにをしたいのかを伝えてまずは調べ方を教えてもらう
    • 調べてもわからない時はどのように検索したのかを先に伝えてその上で相手だったらどういうような対応をするのかを聞く
    • 言っている(回答の)意味が分からない時はちょっと止めてもらい出てきた単語だったりのメモを取り後ほど検索する

など意識していました。ただ自分は開発の質問をテキストでするのが苦手で口頭で聞きたいタイプなのですが、そうではない方もいるのでそこは臨機応変な対応ができる人になりましょう。

2.アサインされたら見積もりや設計を先にしよう

アサインされたのですぐにコードを書き始めるのは一見早いように思えますが、レビューしてもらう人に自分の設計の意図が伝わらず(そもそも設計が出来てないなど)手戻りが発生し結果遅くなることがあります。

現在弊社ではスクラムを導入しておりプランニングの時間をとってチームで見積もりや設計を行っております。フロントエンドが得意な人やサーバーサイドが得意な人、どちらも卒なくこなせる人などいろいろな人がいる中で見積もりを行うわけですから、導入してすぐの頃は見積もりがずれ当初決めていたリリース予定には間に合わなかったり、アサインされた人が設計をやったりしていたので分からない時もありコミュニケーションコストだったり手戻りが起きてしまったりした時にすごく時間がかかってしまうことがありました。

最近では認識のズレは改善されてきて見積もりも大幅にずれることはなくなり、プランニングで時間をかけて見積もりや設計を行うことによってコミュニケーションコストや手戻りが起こることがほぼ無くなってきました。

仮に個人で開発をするときでも自分だけが分かればいいのではありません。もし設計が苦手だとしても癖を付けるために他の人も触るという前提でコンポーネント設計など心がけるのがいいと思います。

3.一つ一つ順番に終わらせていく

タスクAのレビューをお願いしてその間にタスクBに着手しました。タスクBをやっている最中にタスクAのレビューが終わり修正が必要。そんな時にあなたはどちらを先にやりますか?「タスクAのレビュー対応がそこまで大変ではないので後回し」、「タスクBはまだまだ掛かりそうだから先にタスクAのレビュー対応しておこう」2つだけだったらなんとかなるかと思います。

ですがレビューをお願いしている人にも他の仕事がありレビューが遅れることもあります。タスクA、タスクBまで終わりレビューのお願いをしていてレビューが遅れてしまいタスクCを着手している時にレビューが返ってきてタスクA、タスクBともに膨大な手戻りが必要になった時。そこからさらに緊急度の高いバグ報告が来たら。他にも緊急度高い修正依頼が入ってきたら。焦ってしまいがキャパシティが小さい自分にとってはパンク寸前です。

エンジニアだけの話ではないと思いますがそれぞれタスク毎に何をやるべきなのか、その中で優先度が高いのはどれか。パンク寸前でも時間は待ってくれませんし、その時間に追われてどんどん焦ってくることがあります。ですが一度立ち止まって整理し一つ一つ終わらせていくしかありません。

実際に自分は一度にタスクが増えて焦ってしまい全部に手を出そうとして結果何も進まなかったということを何度かやってしまっています。今でもマルチタスクは苦手ですがメモを使い、やることを全て書き出してから優先度をつけてやっています。

まとめ

  • 成長をするためなら自分より出来る人に質問するしかない(ある程度限度はあります)
  • 見積もりや設計に時間を取られたとしてもしっかり行えばリターンはある
  • 焦らずにやるべきことリストを作って優先度をつけてパンクしないようにする

もちろん自分の性格など関係するとは思いますが、この3つのことで悩んでいる方も少なくはないのではと思います。そんな方に少しでもお役に立ててたら幸いです!

最後に

弊社では事業・サービスが成長していくにあたってメンバーを増やしていきたいと思っています。

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

www.wantedly.com

www.wantedly.com

www.wantedly.com

僕はとりあえず dayjs !

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

皆さんは、 JavaScript の日付時刻操作をどうやっていますか?

僕は、 JavaScript の Date は罠が多くそのまま使うことをあまり好まず、今までは日付時刻の操作を行うとなれば黙って moment.js をぶっこんでいました。

フロント界隈でも少し前までは、男は黙って moment.js !って流れがありましたが、最近だと dayjs が主流になりつつあります。 今回はその dayjs について書いていきます。

そもそも

JavaScript の Date はいろいろしんどいんですが、個人的にサービス開発においてブラウザの対応をいちいちするのがだるいなと思っています。 そこで前述の通り、これまでは moment.js をぶっこんでいたのですが、 moment.js はファイルサイズが大きく、バンドルサイズが肥大化するというデメリットがありました。

ですがある日、Twitter を見ていたら dayjs と出会い心を奪われてしまい今後は黙って dayjs やなとなりました。

dayjs について

github.com

Day.js は日付と時刻をパース・検証・操作・表示する最小のモダンブラウザ向け JavaScript ライブラリであり、 Moment.js の API との広い互換性を持ちます。 Moment.js を使ったことがあればすぐにでも Day.js を使い始めることができます。

上記はドキュメントの引用なのですが、この通り、dayjs は moment.js を使えれば直ぐ使えるという魅了があります。 そして、 moment.js でのデメリットであったファイルサイズが大きいという問題を day.js では解決してくれています。

つまり、moment.js のような使用感でファイルサイズがめちゃくちゃ小さくなった最強ライブラリといった感じです。 (これまで moment.js を使って人にとっての話)

使い方

ここでは、みなさんの大好きな Nuxt に day.js をぶっこんで sampleを書いていきます

最初に dayjs をいれていきましょう。

$ yarn add dayjs

次にpluginsを定義していきます。

plugins/dayjs.js

import 'dayjs/locale/ja'
import dayjs from 'dayjs'
import Vue from 'vue'

dayjs.locale('ja')

Vue.prototype.$dayjs = dayjs

あとは、nuxt.config.js に下記を追記でglobalでdayjsが使えるようになります。

plugins: ['@/plugins/dayjs']

それでは、さっそくdayjsで今日の日付が正しく表示されるかみてましょう。 index.vue を書き換えて日付を表示してみます。

<template>
  <section class="container">
    <div>
      <logo />
      <h1 class="title">
        nuxt-dayjs-sample
      </h1>
      <p>現在の時刻: {{ now }}</p>
    </div>
  </section>
</template>

<script>
import Logo from '~/components/Logo.vue'

export default {
  components: {
    Logo
  },
  data: () => ({
    now: null
  }),
  mounted() {
    this.now = this.$dayjs().format('YYYY年MM月DD日 HH:mm:ss')
  }
}
</script>

f:id:masaakikunsan:20190320142048p:plain

今日の日付の表示ができました。 あとは、やりたいことをドキュメントを見ながら実装していくだけです。

ファイルサイズ

f:id:masaakikunsan:20190320143203p:plain

※ moment との比較はちょっと用意できなかったので使ってる人は自分で確認してみてください。

さいごに

dayjs は、moment.js をこれまで使っていた人にとっては最高のライブラリです。 moment と書き方が基本的に変わらず、ファイルサイズがめちゃくちゃ小さいので使わない理由がほぼないです。

これまで moment を使ってみた人は移行コストも学習コストもほぼないので乗り換えてみてはいかがでしょうか?

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

www.wantedly.com

www.wantedly.com

Laravel/Vue.js勉強会#8 オールスターズを開催しました

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

皆様のおかげで、Laravueも第8回目を迎えました。 そこで今回は、今までのLaravue勉強会で協賛頂いた企業様をあつめ、各社からLT登壇していただくという形を試みました。

これまでの協賛頂いた企業様によるLTということもあり、今まで以上に濃い回となりました。

発表内容

グローバルstoreを利用する際はMixinではなくPlugin使った方がいいんじゃないか?説 (田原一樹さん)

f:id:masaakikunsan:20190319162257j:plain

slides.com

レアジョブさんからは、田原さんがMixinの辛さと代替案について話してくれました。 この辺の話をちゃんとしてくれてる資料・スライドはあまりないので是非一度見てみてください。

Laravelを本番環境にデプロイするまで (@yukure3)

f:id:masaakikunsan:20190319162402j:plain

speakerdeck.com

うるるさんからは、@yukure3 さんがLaravelを本番環境にデプロイするまでの話をしてくれました。 @yukure3 さんは新卒でかつ今回初LTだったらしいのですが、初とは思えないぐらいしっかりとLTをしていて良かったなと思いました。 内容もデプロイまでを細かくしっかりと解説していて良いLTでした。

SPAリリース後の問題とその対策 (@frostndays)

f:id:masaakikunsan:20190319162429j:plain

ITプロパートナーズさんからは、@frostndays さんがリリース後に発覚したSPAの問題とその対策でなにをしたかを話してくれました。 SSRをやめVueのライフサイクルに乗っ取りAPIを叩くようにした等色々話してくれました。

アイスタイル特設サイトにおけるVue.jsの導入事例 (@kubotak_public)

f:id:masaakikunsan:20190319162427j:plain

www.slideshare.net

アイスタイルさんからは @kubotak_public さんがVue.jsの導入事例の話をしてくれました。 Vue.js 関連の話はあまりなかったですが、全体的に面白かったので是非スライドを見てみてください。

Laravel Queueの運用管理 (@migrs)

f:id:masaakikunsan:20190319162428j:plain

オープンロジさんからは @migrs さんがCQRSについてどうやっているかの話をメインでしてくれました。 Laravel Telescope は初めて知ったので今度調べて見ようと思いました。

Monolith→MultiRepo→MonoRepoでのリポジトリ戦略 @kotamat

f:id:masaakikunsan:20190319162648j:plain

slides.com

弊社発表枠では CTO の松本がリポジトリ戦略について発表をしました。 弊社では、Monolith→MultiRepo→MonoRepoといった感じにリポジトリ戦略が変わってきており、各々の話やMonoRepoについて話してくれました。

懇親会

f:id:masaakikunsan:20190319162120j:plain

f:id:masaakikunsan:20190319162135j:plain

発表後は、株式会社うるるさんが提供してくれた飲食を囲み懇親会をしました! 株式会社うるるさんありがとうございます。

まとめ

過去協賛企業様はどこも Laravel + Vue をがっつり使用しており非常に内容の濃い回でした。

次回は、株式会社カオナビさんに会場をお借りし 5/22 に開催します。

最後に

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

www.wantedly.com

www.wantedly.com

Web猫ブログを TypeScript化しました

先月よりジョインさせていただいている Web屋さんで猫好きな @jiyuujin 今回は度々登場となって申し訳ないですが、自身のWeb猫ブログを TypeScript化した話をさせていただきます。

webneko.info

nuxt-tsを採用します

結論を先に申し上げると nuxt-ts を採用した件で、その導入周りに留まります。導入にあたってかれこれ1年以上、メンテストップ中の公式テンプレート。。 一切頼りにはできません。今年のどこかで必ず更新してほしさあり、新たに出していただけると大変嬉しいですね😊

github.com

Web猫ブログでは個人的なアナウンスこそしていませんでしたが、今回この場を借りて TypeScript化しましたという経緯です。つい最近、 nuxt-ts 不要になるかもという Issue (下記を参照)を見たのですが、とりあえず nuxt-ts をインストールするしかありません。ビルドコマンドも nuxt から nuxt-ts を使うよう変更してあげる必要があります。

yarn add nuxt-ts
yarn add typescript @types/node ts-loader -D

nuxt-ts 不要になるの?

上記でも触れた nuxt-ts 不要になるかも、というのが以下 Issueを見たことでした。 tslint から @typescript-eslint に移行する一環と思いますが、気になる話でした。

github.com

Nuxt.configを TypeScriptで書く

Web猫ブログでは Headless CMSに Contentfulを採用。 Sitemapの生成処理や Makdownのパース処理など単純な設定に縛られず比較的コードを書く箇所が多く存在すると思いますが、これらの実装箇所が全て型安全に書けるようになったことは今までと大きく違う点だと思います。

APIを使うために型定義を設定する

asyncDatafetch などのAPIをを型安全に利用するために自分自身でVueのInterfaceを拡張してあげる必要があります (/types/nuxt.d.tsを作成すると良いでしょう) この辺りはさすがVue界隈と思う部分ですが、丁寧に書いてくださっていることに対しては感謝しかありません。Nuxt公式ページに Context一覧が存在します。そのページと照らし合わせながらぐりぐり型定義を設定します。

ja.nuxtjs.org

declare module 'nuxt' {
  import { Store } from 'vuex';
  import { Route } from 'vue-router';

  export class Builder {
    constructor(nuxt: Nuxt);
  }

  export class Nuxt {
    constructor(config?: NuxtConfig);
  }

  export interface NuxtConfig {
    dev: boolean;
    [key: string]: any;
  }

  export interface NuxtContext<S = any> {
    app: NuxtApp;
    isClient: boolean;
    isServer: boolean;
    isStatic: boolean;
    isDev: boolean;
    isHMR: boolean;
    route: Route;
    req: any;
    res: any;
    store: Store<S>;
    env: any;
    params: any;
    query: any;
    redirect(path: string): void;
    error(params: { statusCode: number; message: string }): void;
    nuxtState: any;
    beforeNuxtRender(fn: Function): any;
  }
}

上記はあくまでミニマムな構成になっています。当ブログではこの構成に加えて、 contentful 用のModelも定義しています。このように必要に応じて随時追加してあげなければならず少々面倒ではありますが、後々を考えると良い「投資」になってくれるかもしれません。

今回はミニマムな構成の下で動作確認をとることができたのでこの辺で。

リポジトリ公開中

ブログでは随時お問い合わせコメント受付中、PRも絶賛受付中です。

Web猫ブログ 本番運用中

今までの TypeScriptを採用していない版です。

github.com

Web猫ブログ β版 (いずれ本番運用予定)

既に切りました、本番運用開始されたら再度アナウンスしたいと思います。

github.com

最後に、

SCOUTERではエンジニア、デザイナーともに募集しております! 新規事業、絶賛グロース中の事業ともにLaravel, Vue.jsで開発しておりますので、 興味のある方はお声がけください!

www.wantedly.com

www.wantedly.com

www.wantedly.com

Laravel+NuxtでLIFFアプリを作ってみた

はじめに

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

日常的な業務とはそこまで関係がありませんが、LINE社が2018年にリリースした LIFF が気になっていたため、普段使っている技術を用いてLIFFを触ってみたので構築内容を紹介していきます。

LIFFとは

LIFFはLINE Front-end Frameworkの略で、LINEのトークルーム内で動作するウェブアプリの実装を可能にするプラットフォームとなっています。

LINEのトークルーム内でLIFFに登録したウェブアプリを開くと、LINEのユーザーIDやユーザー名などを取得することが可能となっています。

そのためウェブアプリでLINEのコンテキストを利用した機能の提供が可能となり、今までのMessagingAPIだけでは出来なかった体験をユーザーに提供することが出来ます。

linecorp.com

流れ

  • LINE Botの準備
  • LIFFで表示するwebページを用意(Nuxt)
  • LINE Botを操作するためのアプリケーションの準備(Laravel)
  • LIFFアプリを登録

Image from Gyazo

LINE Botの準備

LINE Botの作成

  • LINE Developersのアカウントを作成
  • 新規プロバイダーを作成
  • Messaging API で新規チャネルを作成する
    • アプリ名
      • 適当に入力
      • ここでは「サンプルBot」を指定
    • プラン
      • 「Developer Trial」を選択

LINE Botの設定

  • アクセストークン(ロングターム)を発行
    • テストのため失効までの時間を「0時間」で発行する
    • 本番で使うときには失効時間を設定した方が安全です
  • Webhook送信を「利用する」に変更
    • Webhook URL は後ほど設定します
  • 自動応答メッセージを「利用しない」に変更

ここまででLINE Botの基本的な設定は完了となります。

設定画面の一番下にQRコードが表示されていますので、QRコードを読み取って友だち追加を行います。追加後に友だち追加時のメッセージが送られてきていれば成功となります。

LIFFで表示するwebページを用意(Nuxt)

今回はNuxtで作ったwebページをNetlifyで公開することを前提に進めます。

(本来はvue-cliを使ったVueアプリケーションでも良いですが、Nuxtの方が諸々の設定が楽だっったため今回はNuxtを利用しています。)

LIFFのテストを行うためだけで合ってもhttps接続が可能なwebサイトであることをもとられますので、Netlifyやherokuでサクッと静的ファイルをホスティングするのがおすすめです。

JSでLIFFを扱うためのSDKを読み込む

SDKはLINE社がCDNで公開しているため、それを読み込む。

Nuxtは nuxt.config.js でheadタグの設定をすることができるのでNuxtの書き方に従って記述します。

// nuxt.config.js

module.exports = {
  /*
  ** Headers of the page
  */
  head: {
    script: [{ src: 'https://d.line-scdn.net/liff/1.0/sdk.js' }]
  },
}

LIFFで使うページを作成

// pages/index.vue

<template>
  <section class="container">
    <p class="line-id">LINE ID:{{ lineId }}</p>
    <div class="form">
      <div class="control">
        <input class="input" type="text" placeholder="お名前" v-model="formData.name">
      </div>
      <button class="button is-info is-fullwidth" @click="onSubmit()">送信する</button>
      <button class="button is-light is-fullwidth" @click="handleCancel()">キャンセル</button>
    </div>
  </section>
</template>

<script>
export default {
  data() {
    return {
      formData: {
        name: ''
      },
      lineId: null
    }
  },
  mounted() {  
    if (!this.canUseLIFF()) {
      return
    }

    window.liff.init(data => {
      this.lineId = data.context.userId || null
    })
  },
  methods: {
    onSubmit() {
      if (!this.canUseLIFF()) {
        return
      }

      window.liff
        .sendMessages([
          {
            type: 'text',
            text: `お名前:\n${this.formData.name}`
          },
          {
            type: 'text',
            text: '送信が完了しました'
          }
        ])
        .then(() => {
          window.liff.closeWindow()
        })
        .catch(e => {
          window.alert('Error sending message: ' + e)
        })
    },
    handleCancel() {
      if (!this.canUseLIFF()) {
        return
      }
      window.liff.closeWindow()
    },
    canUseLIFF() {
      return navigator.userAgent.indexOf('Line') !== -1 && window.liff
    }
  }
}
</script>

<style>
.container {
  margin: 0 auto;
  padding: 20px;
  min-height: 100vh;
}

.line-id {
  margin-bottom: 30px;
}

.form > * {
  margin-bottom: 10px;
}
</style>
  • mounted()
    • LINEのコンテキストを受け取ってコンポーネントのデータプロパティーに入れています
    • ここではLINEIDをコンテキストから取得してページに表示している
    • 取得できるコンテキストの一覧はこちらを御覧ください
  • onSubmit()
    • でフォームに入力されたテキストをLINEのトークにメッセージを送信している
    • liff.sendMessages() でメッセージを送信することができる
    • 送信が成功したら liff.closeWindow() でLIFFを閉じる
  • canUseLIFF()
    • LIFFを使える状態にあるかをチェックする
    • ユーザーエージェントがLINEか、LIFFのSDKが適切に読み込まれていることを確認している

yarn dev で開発環境を立ち上げると以下の画像のような表示になります。 ここでは、LINEのコンテキストを取得出来ませんので、LINEIDは表示されておらず、「送信する」ボタンをクリックしても何も表示されません。

LIFFアプリを登録

Image from Gyazo

  • LIFFアプリの設定画面より新規作成をします
    • エンドポイント URL に上で作ったNuxtアプリをNetlifyなどで公開したときのURLを入力してください
  • LIFF URL が発行されればLIFFアプリの作成は完了です
    • エンドポイント URLサイズ は後から編集可能です

LINE Botを操作するためのアプリケーションの準備(Laravel)

Laravelアプリケーションを用意

普段からLaravelを使っており、慣れている分素早くアプリケーションを作れるためLaravelを使用しました。 Laravelのインストール方法や初期設定は今回は割愛します。

以下のURLを御覧ください。

qiita.com

ENVの設定

.env ファイルに以下のLINEの情報を追加します。

# .env

LINE_ACCESS_TOKEN={アクセストークン(ロングターム)}
LINE_CHANNEL_SECRET={Channel Secret}

webhookを受けるエンドポイントを用意

// routes/api.php

<?php
/** @var Illuminate\Routing\Router $router */
$router = app('Illuminate\Routing\Router');

$router->post('/line/webhook', 'LineWebHookController@webHook')->name('line.webhook');

飛んでくるリクエスト処理してBotを動作させる処理の作成

// app/Http/Controllers/Api/LineWebHookController.php

<?php
/**
 * Class LineWebHookController
 * @package App\Http\Controllers\Api
 */
class LineWebHookController extends Controller
{

    /**
     * @param Request $request
     * @throws LINEBot\Exception\InvalidSignatureException
     */
    public function webHook(Request $request)
    {
        // Botの印象情報をENVから受け取る
        $lineAccessToken = config('line.access_token');
        $lineChannelSecret = config('line.channel_secret');

        // 署名をチェックする
        // 署名が正しくなければ不正なアクセスとみなして何も行わない
        $signature = $request->headers->get(HTTPHeader::LINE_SIGNATURE);
        if (!SignatureValidator::validateSignature($request->getContent(), $lineChannelSecret, $signature)) {
            return;
        }

        $lineBot = new LINEBot(new CurlHTTPClient ($lineAccessToken), ['channelSecret' => $lineChannelSecret]);

        try {
            // イベントをパースする
            /** @var LINEBot\Event\BaseEvent[]|LINEBot\Event\MessageEvent\TextMessage[] $events */
            $events = $lineBot->parseEventRequest($request->getContent(), $signature);

            foreach ($events as $event) {
                // LINEから送られてきたメッセージの内容を抽出
                $receiveText = '';
                if ($event->getType() === ActionType::MESSAGE && $event->getMessageType() === MessageType::TEXT) {
                    $receiveText = $event->getText();
                }

                // 「liff」と投稿されたら、LIFFアプリを開く
                if ($receiveText === 'liff') {
                    $message = new TextMessageBuilder('line://app/1554604976-anxJ8ogY');
                } else {
                    $message = new TextMessageBuilder('送信ありがとうございます!🙇<200d>');
                }

                // メッセージを送信
                $replyToken = $event->getReplyToken();
                $lineBot->replyMessage($replyToken, $message);
            }
        } catch (Exception $e) {
            logger($e->getMessage());
            return;
        }

        return;
    }
}

webhookURLをLINE Developersの設定画面で設定

LINE Developersで設定を行う前にwebhookURLを公開する必要があります。

今回はサンプルの開発のため、ngrokというサービスを使ってwebhookURLを公開しました。

ngrokの導入についてはこちらの記事を御覧ください。

qiita.com

webhookURLが公開出来たら、Developersの設定画面の「webhook URL」欄に公開したエンドポイントのURL文字列を入力します。

URLの設定後に「接続確認」をクリックして成功すれば設定は完了です。

完成物

LINEのトークルームで「liff」と入力すると、LIFFアプリのURLがBotから送られて来ます。

そのURLをクリックしてLIFFアプリを起動するとLINEユーザーIDが表示されています。

「お名前」フォームにテキストを入力して、送信後、LINEトークルームに入力した内容が投稿されていれば成功です!

Image from Gyazo

まとめ

LIFFという新しい技術を初めて試してみましたが、想像よりも簡単にLIFFアプリを作成することが出来ました。ただ、当然普通のウェブアプリと比べると開発中のデバックが大変でした。LINE上だとコンソールが確認できないため、ウェブアプリ上にプリントしてデバックを行うしかなさそうでした。

LIFFアプリを使えばウェブアプリでLINEのコンテキストを扱うことができるため、LINEBotの可能性が一気に広がったのではないかという印象を受けたため、様々なサービスがLIFFを利用してより良いユーザー体験を提供してくれるのではないでしょうか。

今後、弊社のプロダクトでも機会があれば取り入れたいなと思える機能でした。

最後に

弊社では最新技術のキャッチアップ、プロダクション導入を積極的に行っています!

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

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

www.wantedly.com

www.wantedly.com

www.wantedly.com