Nuxt.jsビギナーズガイド
posted with ヨメレバ
花谷拓磨 シーアンドアール研究所 2018年10月
サンプルアプリの作成
環境構築
- npx create-nuxt-app test-blog
以下を選択。- UI framework: Element
- Nuxt.js modules: Axios – Promise based HTTP client
- Linting tools: ESLint
- Testing framework: None
- Rendering mode: Universal (SSR / SSG)
- Deployment target: Server (Node.js hosting)
- appディレクトリを作成し、いくつかのディレクトリをその中へ移動する。
- cd test-blog
- mkdir app
- mv components pages plugins static store app/
appディレクトリに移動すると以下のメリットがある。- アプリケーション以外のコードを別の場所に配置することができる(testディレクトリなど)
- Linterやコードフォーマッタのパス指定をappにすることで、nuxt.config.jsなどをignoreする必要がなくなる
- nuxt.config.jsに追加
srcDir: 'app',
- npm run devで開発サーバで実行
共通レイアウトの作成
1. mkdir app/assets; touch app/assets/common.css
アプリケーション全体用のCSSを定義。
html {
font-size: 16px;
word-spacing: 1px;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
box-sizing: border-box;
}
*, *:before, *:after {
box-sizing: border-box;
margin: 0;
}
.wrapper {
background: #FAFAFA;
width: 100%;
min-height: calc(100vh - 61px);
}
.el-card {
flex: 1;
}
.container:not(.__nuxt-error-page) {
width: 96%;
max-width: 980px;
padding: 30px 0;
margin: 0 auto;
min-height: calc(100vh - 61px);
display: flex;
justify-content: center;
align-items: flex-start;
}
.text-left {
text-align: left;
}
.text-center {
text-align: center;
}
.text-right {
text-align: right;
}
p {
margin: 16px 0;
}
2. touch app/components/TheHeader.vue
<template>
<el-menu mode="horizontal" :router="true">
<el-menu-item index="1" style="pointer-events:none;">
Nuxt Diary App
</el-menu-item>
<el-menu-item index="2" :route="{ path: '/posts/' }">
投稿一覧
</el-menu-item>
<no-ssr>
<el-menu-item index="4" style="float: right;" :route="{ path: topLink }">
<span>ログイン</span>
</el-menu-item>
</no-ssr>
<el-menu-item index="5" style="float: right" :route="{ path: '/posts/new' }">
新規投稿
</el-menu-item>
</el-menu>
</template>
<script>
export default {
computed: {
topLink () {
if (this.$store.getters.user) {
return '/posts/'
}
return '/'
}
}
}
</script>
Vue.jsのスタイルガイド「ページで1つしか存在しないコンポーネントの名前はTheで始める」
https://jp.vuejs.org/v2/style-guide/index.html#単一インスタンスのコンポーネント名-強く推奨
ログイン画面へのリンクは書籍通りにすると、ログイン中は「Redirected when going from “/posts/” to “/” via a navigation guard.」という警告が表示されてしまった。
ログイン中は/へアクセスすると/posts/にリダイレクトするようにしているため、2回リダイレクトが発生してしまうことが原因であると思われる。
よって、ログイン状態を判定してリンクを/と/posts/で切り替えるようにした。
3. mkdir app/layouts; touch app/layouts/default.vue
<template>
<TheHeader />
</template>
<script>
import TheHeader from '~/components/TheHeader.vue'
export default {
components: {
TheHeader
}
}
</script>
4. nuxt.config.jsにCSSの読み込みを追加
css: [
'element-ui/lib/theme-chalk/index.css',
'~/assets/common.css'
],
ログインページと投稿一覧ページの作成
1. app/pages/index.vueを修正。
<template>
<section class="container">
<el-card style="flex: 1">
<div slot="header" class="clearfix">
<span>ログイン</span>
</div>
<form>
<div class="form-content">
<span>ユーザー ID</span>
<el-input placeholder="" v-model="formData.id"/>
</div>
<div class="form-content">
<el-checkbox v-model="isCreateMode">アカウントを作成する</el-checkbox>
</div>
<div class="text-right">
<el-button type="primary" @click="handleClickSubmit">{{ buttonText }}</el-button>
</div>
</form>
</el-card>
</section>
</template>
<script>
export default {
asyncData () {
return {
isCreateMode: false,
formData: {
id: ''
}
}
},
computed: {
buttonText () {
return this.isCreateMode ? '新規登録' : 'ログイン'
}
}
}
</script>
<style scoped>
.form-content {
margin: 16px 0;
}
</style>
2. mkdir app/pages/posts; touch app/pages/posts/index.vue
<template>
<section class="container posts-page">
<el-card>
<div slot="header" class="clearfix">
<span>新着投稿</span>
</div>
<el-table
:data="showPosts"
style="width: 100%"
class="table"
>
<el-table-column
prop="title"
label="タイトル">
</el-table-column>
<el-table-column
prop="user.id"
label="投稿者"
width="180">
</el-table-column>
<el-table-column
prop="created_at"
label="投稿日時"
width="240">
</el-table-column>
</el-table>
</el-card>
</section>
</template>
<script>
export default {
computed: {
showPosts () {
return [
{
id: '001',
title: 'テストタイトル',
body: 'ここに本文が入ります。',
created_at: '2021/10/30 06:00:00',
user: {
id: 'hoge'
}
},
{
id: '002',
title: 'テストタイトル2',
body: 'ここに本文2が入ります。',
created_at: '2021/10/30 06:10:00',
user: {
id: 'hoge'
}
}
]
}
}
}
</script>
<style>
.posts-page .el-table__row {
cursor: pointer;
}
</style>
ログイン機能の実装
1. touch app/store/index.js
export const state = () => ({
isLoggedIn: false,
user: null
})
export const getters = {
isLoggedIn: state => state.isLoggedIn,
user: state => state.user
}
export const mutations = {
setUser (state, { user }) {
state.user = user
state.isLoggedIn = true
}
}
export const actions = {
async login ({ commit }, { id }) {
const user = await this.$axios.$get(`/users/${id}.json`)
if (!user.id) {
throw new Error('Invalid user')
}
commit('setUser', { user })
},
async register ({ commit }, { id }) {
const payload = {}
payload[id] = { id }
await this.$axios.$patch('/users.json', payload)
const user = await this.$axios.$get(`/users/${id}.json`)
if (!user.id) {
throw new Error('Invalid user')
}
commit('setUser', { user })
}
}
2. app/pages/index.vueを修正。
<template>
<section class="container">
<el-card style="flex: 1">
<div slot="header" class="clearfix">
<span>ログイン</span>
</div>
<form>
<div class="form-content">
<span>ユーザー ID</span>
<el-input placeholder="" v-model="formData.id"/>
</div>
<div class="form-content">
<el-checkbox v-model="isCreateMode">アカウントを作成する</el-checkbox>
</div>
<div class="text-right">
<el-button type="primary" @click="handleClickSubmit">{{ buttonText }}</el-button>
</div>
</form>
</el-card>
</section>
</template>
<script>
import { mapGetters, mapActions } from 'vuex'
export default {
asyncData ({ redirect, store }) {
if (store.getters.user) {
redirect('/posts/')
}
return {
isCreateMode: false,
formData: {
id: ''
}
}
},
computed: {
buttonText () {
return this.isCreateMode ? '新規登録' : 'ログイン'
},
...mapGetters(['user'])
},
methods: {
async handleClickSubmit () {
if (this.isCreateMode) {
try {
await this.register({ ...this.formData })
this.$notify({
type: 'success',
title: 'アカウント作成完了',
message: `${this.formData.id} として登録しました`,
position: 'bottom-right',
duration: 1000
})
this.$router.push('/posts/')
} catch (e) {
this.$notify.error({
title: 'アカウント作成失敗',
message: '既に登録されているか、不正なユーザー ID です',
position: 'bottom-right',
duration: 1000
})
}
} else {
try {
await this.login({ ...this.formData })
this.$notify({
type: 'success',
title: 'ログイン成功',
message: `${this.formData.id} としてログインしました`,
position: 'bottom-right',
duration: 1000
})
this.$router.push('/posts/')
} catch (e) {
this.$notify.error({
title: 'ログイン失敗',
message: '不正なユーザー ID です',
position: 'bottom-right',
duration: 1000
})
}
}
},
...mapActions(['login', 'register'])
}
}
</script>
<style scoped>
.form-content {
margin: 16px 0;
}
</style>
3. touch static/users.json; touch static/users/hoge.json
書籍ではfirebaseを使っているが、ここではローカルにjsonを用意する。
{
"id": "hoge"
}
{
"id": "hoge"
}