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