nuxtを使ってqiitaの投稿一覧サイトを作る


2018/10/27 追記
store を使うよう更新しました。
詳細はこちらを参照してください。


これまで 2 回に渡って nuxt と element-ui の使い方をみてきました。

これらを踏まえ、今日は実際に Qiita 投稿一覧サイトを作っていきたいと思います。

最初に考えた要件では以下の 3 点を挙げていました。

  • トップページから Qiita の投稿一覧ページへ移行させる
  • Qiita の投稿一覧ページではヘッダ有りとする
  • UI としては Element を利用する

これに以下 2 点を加えます。

  • 投稿検索機能
  • ページネーション機能

モジュールのバージョン

使用するモジュールのバーションは以下の通りです。

1
2
3
"nuxt": "^1.0.0-rc11",
"axios": "^0.17.1",
"element-ui": "^2.0.8"

プロジェクトの作成

新たにプロジェクトを作成します。
プロジェクト名は「hello-nuxt」としました。

1
2
3
4
$ vue init nuxt-community/starter-template hello-nuxt
$ cd hello-nuxt
$ yarn install
$ yarn run dev

axios、element-ui のインストール

axios、element-ui をインストールし、設定を行います。

1
$ yarn add axios element-ui

nuxt.config.js の build 内に以下の設定を追加します。

1
2
build: {
vendor: ['axios', 'element-ui'],

nuxt.config.js の plugins、css に以下の設定を追加します。

1
2
3
4
plugins: ['~plugins/element-ui', { src: '~plugins/element-ui', ssr: false }],
css: [
'element-ui/lib/theme-chalk/index.css'
],

/plugins/element-ui.js を作成します。

トップページの作成

/pages/index.vue の内容を変更します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<section class="container">
<div>
<h1 class="title">
Qiita
</h1>
<h4 class="subtitle">
Qiita is a technical knowledge sharing and collaboration platform for programmers.
</h4>
<div class="links">
<nuxt-link to="/search" class="button--white"><i class="el-icon-search"></i> Search</nuxt-link>
</div>
</div>
</section>
</template>....以下略

Qiita の投稿一覧ページ用のヘッダー、フッター作成

components/Header.vue を作成します。

1
2
3
4
5
<template>
<div class="header">
<b><nuxt-link to="/">Hello Qiita with Nuxt.js \\\\ ٩(*'ω'*)و ////</nuxt-link></b>
</div>
</template>....以下略

components/Footer.vue を作成します。

1
2
3
4
<template>
<div class="footer">
</div>
</template>....以下略

続いて layouts/navbar.vue を追加します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<div>
<my-header />
<nuxt />
<my-footer />
</div>
</template>

<script>
import MyHeader from '~/components/Header.vue'
import MyFooter from '~/components/Footer.vue'
export default {
name: 'navbar',
components: {
MyHeader,
MyFooter
}
}
</script>

Qiita の投稿一覧ページの作成

検索部の作成

pages/search.vue を作成します。

ここではページ表示時に Qiita の API をコールする機能と
検索画面に入力されたキーワードを元に Qiita の API をコールする機能、
入力値のバリデーション機能を持ちます。
エンターキー押下で検索が走るようにもします。

投稿(Qiita API のレスポンスデータ)を描画する部分は別途作ります(コード上では my-list が当該箇所)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
<template>
<div>
<el-container>
<el-main>
<!-- 検索フォーム -->
<el-form :inline="true" :model="searchForm" ref="searchForm" :rules="rules" @submit.native.prevent>
<el-form-item prop="keyword">
<el-input placeholder="search by keyword" prefix-icon="el-icon-search" v-model="searchForm.keyword"@keyup.enter.native="search('searchForm')" />
</el-form-item>
<el-form-item>
<el-button @click="search('searchForm')">search</el-button>
</el-form-item>
</el-form>
<!-- 投稿一覧 -->
<my-list :lists="mylist" :hasData="hasData" />
</el-main>
</el-container>
</div>
</template>

<script lang="babel">
import axios from 'axios'
import MyList from '~/components/List.vue'
const BASE_URL = 'https://qiita.com/api/v2/'
export default {
layout: 'navbar',
components: {
// 投稿一覧を表示するコンポーネント
MyList
},
data () {
return {
// 検索フォーム
searchForm: {
keyword: ''
},
// バリデーションルール
rules: {
keyword: [
{ required: true, message: 'Please input the keyword', trigger: 'blur' }
]
},
mylist: [],
hasData: true
}
},
created () {
// ページ描画時にキーワード「nuxt.js」でQiitaのAPIをコール
this.searchForm.keyword = 'nuxt.js'
this.sendRequest()
this.searchForm.keyword = ''
},
methods: {
// キーワード検索時に呼ばれるメソッド。バリデーション含む
search (form) {
this.$refs[form].validate((valid) => {
if (!valid) {
return false
}
this.sendRequest()
})
},
// リクエスト送信
sendRequest () {
axios.get(BASE_URL+ 'items', {
headers: {'Content-Type': 'application/json'},
params: {
page: 1,
per_page: 20,
query: this.searchForm.keyword
}
})
.then(response => {
if (response.data.length === 0) {
this.hasData = false
}
this.mylist = response.data
})
.catch(e => {
console.error('error:', e)
})
}
}
}
</script>....以下略

投稿一覧部の作成

components/List.vue を作成します。

ここでは search.vue で取得した Qiita API のレスポンスデータを受け取り描画する機能と
トップへスクロールする機能を持ちます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
<template>
<div>
<div v-if="lists.length === 0 && !hasData">
<i class="el-icon-warning">&nbsp;No results found for your keyword.</i>
</div>
<div v-else>
<!-- 投稿一覧 -->
<el-col :span="6" v-for="(element, index) in lists" :key="index" class="col-style">
<el-card :body-style="{ padding: '15px' }" class="box-card">
<div slot="header" class="clearfix">
<a :href="element.url" target="_blank">{{ element.title }}</a>
</div>
<div class="bottom clearfix content-style text">
<div>{{ element.created_at }}</div>
<span>
<img :src="element.user.profile_image_url" width="15" height="15" />
<template v-if="element.user.description">
<el-popover slot="description" placement="top-start" width="300" trigger="hover" :content="element.user.description">
<span slot="reference">&nbsp;{{ element.user.id }}</span>
</el-popover>
</template>
<template v-else>
<span>&nbsp;{{ element.user.id }}</span>
</template>
</span>
&nbsp;
<span>
<i class="el-icon-star-off">{{ element.likes_count }}</i>
</span>
<div>{{ getDescription(element.body) }}</div>
<el-tag size="mini" type="info" class="tab-style" v-for="(tag, index) in element.tags" :key="index">{{ tag.name }}</el-tag>
</div>
</el-card>
</el-col>
<!-- スクロール用のボタン -->
<div v-if="250 < scrollY" class="page-component-up">
<transition name="fade">
<i class="el-icon-caret-top" @click="scrollTop" />
</transition>
</div>
</div>
</div>
</template>

<script lang="babel">
export default {
// search.vueで取得したQiita APIのレスポンスデータの受け取り
props: ['lists', 'hasData'],
data () {
return {
scrollY: 0
}
},
mounted () {
window.addEventListener('scroll', this.handleScroll)
},
methods: {
// ボディ部のトリミング
getDescription: function (body) {
return body.slice(0, 100) + '...'
},
// 現在の上部からのスクロール量取得
handleScroll: function () {
this.scrollY = window.scrollY
},
// トップへスクロール
scrollTop: function () {
document.body.scrollTop = 0
document.documentElement.scrollTop = 0
}
}
}
</script>....以下略

ページネーションの作成

続いて、ページネーションを作成しようと思いした…が
レスポンスヘッダーに含まれている「Total-Count」が取得できないため今回は断念しました。

デモ

https://aytdm.github.io/hello-nuxt/
今回は GitHub Pages にデプロイしました。
※PC での閲覧推奨です。

まとめ

コンポーネント間のデータの受け渡しやリスト、イベントハンドリングなども行うことができ、
基本的な操作は体験できたように思います:)
#ストアやトランジションはまたの機会に…!

デモで分かるようにレスポンシブではなく、スマホだととても見れないので
別の形式で表示するか、element-ui 以外の UI ライブラリを選定した方が良さそうです。
この点を考慮していませんでした。。。

テスト周りは追々勉強&追加していこうと思います。

vue.js は画面で行われる処理が分かりやすくていいですね。
使える機会がある際は積極的に使っていきたいと思います。

ソースコード

v1.0

参考

Share

nuxtを使ってqiitaの投稿一覧サイトを作る(準備2)


2018/10/27 追記
store を使うよう更新しました。
詳細はこちらを参照してください。


ここのところ先日作成した LINE BOTに【cotogoto】や【repl-ai】を組み込んで遊んでいました
いやぁ・・・面白いですね!
microsoft のコグニートも使ってみたいなぁ…なんて夢が広がります

本題

だいぶ間が空きましたが、今回はページを読み込んだ際に
axios で取得したデータを描画したいと思います

前回はページを読み込んだ際に静的なデータをテーブルに描画しました

モジュールのバージョン

使用しているモジュールのバーションは以下の通りです
element-ui は 2.0 系がリリースされていますが、今回は 1.0 系で行きます

1
2
3
4
5
6
7
8
"dependencies": {
"axios": "^0.17.1",
"element-ui": "1.4.12",
"nuxt": "1.0.0-rc10",
"vue": "2.3.0",
"vue-server-renderer": "2.3.0",
"vue-template-compiler": "2.3.0"
}

使用する API

Qiita の公開 APIを使用します
ドキュメントによると、未認証ユーザーでも
1 時間に 60 回までリクエストできるそうなので未認証でいきます

コールする API は【投稿の一覧取得】です

axios のインストール

まずは axios をインストールします

1
$ yarn add axios

続いて nuxt.config.js の build 内に以下の設定を追加します

1
2
build: {
vendor: ['axios'], ...

これで axios を使う準備ができました

ページの作成

新たに pages/axiosList.vue を追加し、script 部分から作成します
axios をインポートしてレイアウトを指定します

よくよく考えると作っているデザインは 2 カラムではないのですが、気にしたらダメです。

1
2
3
4
5
6
7
8
<script lang="babel">
// axiosをインポート
import axios from 'axios'
export default {
// レイアウトを指定
layout: 'twocolumns'
}
</script>

ここまでは前回とあまり変わらないですね
次に axios で通信する部分を記述します

今回は vue インスタンスが作成された後に呼ばれる【created】で axios を呼びたいと思います

created で取得した Qiita 投稿データを lists に格納します
取得件数は 6 件、検索クエリは一先ず「nuxt.js」としました

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export default {
layout: "twocolumns",
data() {
return {
lists: []
};
},
created() {
axios
.get("https://qiita.com/api/v2/items", {
params: {
page: 1,
per_page: 6,
query: "nuxt.js"
}
})
.then(response => {
this.lists = response.data;
})
.catch(e => {
console.error("error:", e);
});
}
};

通信部分ができたので次は描画部分に取り掛かります
今回は element-ui の【カード】、【タグ】を使って描画していきます
まずは記事タイトル、作成日、投稿者のアイコン、投稿者の id、記事タグの一覧を表示します

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<template>
<div>
<el-row :gutter="10">
<el-col :xs="8" :sm="6" :md="4" :lg="3" :xl="1"><div class="grid-content bg-purple"></div></el-col>
<el-col :xs="8" :sm="8" :md="8" :lg="8" :xl="8" v-for="(element, index) in lists" :key="index" class="col-style">
<el-card class="box-card">
<div slot="header" class="clearfix">
<a :href="element.url" target="_blank">
<i class="el-icon-edit"></i>
{{ element.title }}
</a>
</div>
<div class="bottom clearfix content-style">
<div>{{ element.created_at }}</div>
<div>
<img :src="element.user.profile_image_url" width="20" height="20" />
<span>@{{ element.user.id }}</span>
</div>
<el-tag size="mini" type="info" class="tab-style" v-for="(tag, index) in element.tags" :key="index">{{ tag.name }}</el-tag>
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>

動作を見てみると・・・ちゃんと表示されました:)

ちょっと欲張ってみる

当初の目的は達成できました
記事の内容も多少覗けるともっといいですね!

…とは言え、qiita の GET /api/v2/items レスポンスにアブストラクトはありません
body を指定すると本文がまるっと表示されてしまうので、ちょっとだけ表示させるよう javascript で工夫します

script 部分に body トリミング用の関数を作成します

1
2
3
4
5
methods: {
getDescription: function (body) {
return body.slice(0, 100) + '...'
}
}

この関数を描画部分に指定します

1
<div>{{ getDescription(element.body) }}</div>

動作を見てみると・・・いい感じです ♪

もうちょっと欲張ってみる

投稿者の自己紹介が見れると更にいいと思うので、次は element-ui の【ポップオーバー】を使って
id 名にカーソルを持っていった場合に投稿者の方が自己紹介を登録していたら表示させたいと思います

描画部分に分岐処理を入れます

1
2
3
4
5
6
7
8
<template v-if="element.user.description">
<el-popover slot="description" placement="top-start" width="300" trigger="hover" :content="element.user.description">
<span slot="reference">@{{ element.user.id }}</span>
</el-popover>
</template>
<template v-else>
<span>@{{ element.user.id }}</span>
</template>

完成です!

axiosList.vue の最終形態

デモ

まとめ

だいぶ形になってきました
準備編は今回でおしまいです
次回はこれまで作成したものを流用しつつ
element-ui2.0 へのバージョンアップやページネーションや検索機能をつけたいと思います

参考

Share

ごみ収集日前にアラートを出すLINEbot

meet line-bot

最近 引越をした友人から「ごみ出しリマインダーを作って」と言われ模索することに。

ポイントは既存かつよく使用するアプリでリマンドできること。
APIもSDKも用意されているLINEがぴったりだと思い、挑戦しました。

構想

  • Springのスケジュール機能(cron)を使って、ごみ収集日前に該当者へリマインドをプッシュ
  • 該当者から何かメッセージが来たらレスポンス
  • ごみ捨てが終わっていたら褒め、まだだったら捨てようねと励ます(大事)

レスポンスに応対する箇所は「Webhook」、リマインド箇所は「Messaging API」で実装します。

環境

  • jdk1.8
  • Spring Boot
  • Heroku

herokuでアプリケーションの作成、LINEでチャンネルの作成、Webhookの有効化、Webhookのurlの登録が必要です。

SDKの使い方に慣れる

まずLINEでチャンネルを解説し
SDKのページにあるサンプル「echo」をherokuにデプロイし、エコーバックを楽しみました。
続いてサンプル「kitchensink」をherokuにデプロイしどのように作ればいいかを理解。
機能が網羅されているので、掴みやすかったです。

プロジェクト構成

依存関係

1
2
3
4
5
<dependency>
<groupId>com.linecorp.bot</groupId>
<artifactId>line-bot-spring-boot</artifactId>
<version>1.11.0</version>
</dependency>

LINE Messaging API SDKを追加します。

プロパティ

1
2
3
line.bot.channelSecret = XXXXX
line.bot.channelToken = XXXXX
line.bot.handler.path = /callback

LINEで開設したチャンネルの情報を設定します。

プッシュの実装

confirm.png
ごみ出しが完了したか確認するため、確認テンプレートを使用します。

コントローラ化する必要はありませんが、GETで呼び出せるようにすると確認する時に楽です。

余談ですが作っている当初、この部分ははSDKを使わずに地道実装していました…w
#こちらでも動作はします。

レスポンスの実装

Webhookはメッセージやスタンプなど何か送られた際に応答を返すことができます。
確認で「はい」もしくは「いいえ」が返却されるようにしたので、この2つのワードを拾い
スタンプとメッセージを返すようにします。

replyにreplyTokenとメッセージを設定します。
メッセージはコードに記述しているようにリストで複数設定することができます。

スケジューリング

Springのスケジュール機能@Scheduledを利用します。

※@EnableSchedulingアノテーションをmain関数のあるクラスにつけるのを忘れずに

備考

環境変数

1
2
3
4
line.bot.channelSecret = ${LINE_BOT_CHANNEL_SECRET}
line.bot.channelToken= ${LINE_BOT_CHANNEL_TOKEN}
cron.setting = ${LINE_CRON}
garbage.reminder.id = ${LINE_ID}

環境変数は全てherokuに持たせるようにしました。
コンソールで変更できるのでらくちんですね!

botのアイコン

アイコンはtabagotchiから拝借。
お気に入りの拡張機能です

動作確認

まとめ

SDKを使用すると、すっきり書けて楽ですね。

自前実装ではOkHttp3ClientHttpRequestFactoryを用いたRestTemplateの実装ができました。
こういう小さな達成感は大事(笑

楽しく書けました X)
あとは気に入ってくれるといいなぁ

ソースコード

https://github.com/aytdm/garbage-reminder-bot

参考

Share