きっかけは
Vueなら簡単にフォームが作れるという記事を見て、おーこれは簡単そうだしやってみようかなと調べているうちにNuxt.jsを使えば楽っぽいということで、そしてまた色々と調べているうちにNuxt.jsでお店を探すアプリ作りをしようというページが特にわかりやすくサクサクと最初は写経していたのだけれども、テンプレート構文を使うよりもRender関数を使って書きたいんだなという気持ちが湧いてきて、というのもこのテンプレート構文を使うと昔に書いていたRubyとかのフレームワークぽいのができるっていうか、そういうスタンダードな書き方ができるからこそ使いやすいんやんかというわけなんだな。てことで、Render関数で書き始めた最初のうちは良かったけども、ある程度書いたころからデプロイどうしょっかなと思い始めた頃からだんだんとなんかめんどくさくなってきたのもあり、そうこうしてコーディングを続けていると、これってReact Nativeで良くない?と思ったがはいそれまでよでReact Nativeでやることにしたんだな。
最初のアレとAxios
プロジェクト作成なんかは例のあれで
$npx create-nuxt-app APPNAME
でOKなんだな。こんな簡単でも今の世の中はIT技術者不足ということらしいからYouもやっちゃいなよ。参考サイトの方はuniversal(SSR)で作成していますが、SPAを選択しました。パッケージはこのような感じ。
"dependencies": { "@nuxtjs/axios": "^5.3.6", "@vue/babel-helper-vue-jsx-merge-props": "^1.0.0", "@vue/babel-preset-jsx": "^1.1.2", "axios": "^0.19.2", "dotenv": "^8.2.0", "nuxt": "^2.0.0" }, "devDependencies": { "eslint-config-prettier": "^6.10.0", "eslint-plugin-prettier": "^3.1.2", "prettier": "^1.19.1" }
今回Render関数を使うので、JSXが使えるようにBabelを追加したくらい。Axiosについては参考サイトで公開しているものを使って引数でキーワードを取るものとIDを取るものに分けてコンポーネントごとに使うようにしました。
async getShops(str) { const position = await this.getCurrentPosition() return await axios(`${this.url}`, { params: { key: process.env.apiKey, lat: position.coords.latitude, lng: position.coords.longitude, keyword: str ? str : '', format: 'json' } }) } async getDetail(str) { return await axios(`${this.url}`, { params: { key: process.env.apiKey, id: str, format: 'json' } }) }
ちなみにこのクラスはReact Nativeの方でも使いまわした。
Pages
ここはシンプルにして、お店一覧と詳細一覧のコンポーネントとという構成で他の部分はデフォルト。というかそのまま。
/pages/index.vue <script> import Shops from '~/components/Shops' export default { components: { ShopsView }, render() { return <ShopsView /> } } </script>
/pages/_id.vue <script> import Detail from '~/components/Detail' export default { components: { Detail }, render() { return <Detail /> } } </script>
Components
これはページ一覧のページです。layoutsもデフォルトで、Pagesのindex.vueが最初に表示されてそこでShops.vueが描画されます。何ヶ月か前に作ったReact Nativeのとほぼ同じ感じですが。Vueのタグと関数で分かれているぶん読みやすい感がある。これだけ簡潔に書けるのはNuxt.jsがうまい具合にやってくれているということなのだな。
/components/Shops.vue <script> import ShopsView from '~/components/ShopsView.vue' import api from '@/api/api' export default { data() { return { shops: {}, error: false } }, methods: { async setKeyword(str) { const { data } = await api.getShops(str) this.shops = data.results.shop.map(e => e) } }, async mounted() { this.setKeyword() }, render() { return ( <ShopsView data={this.shops} print={this.print} setKeyword={this.setKeyword} /> ) } } </script>
/components/ShopsView.vue <script> export default { props: { data: {}, setKeyword: { type: Function } }, render() { return ( <div class="container"> <p class="title">近くで済ますアプリ</p> {...['指定なし', '肉', '揚げたて', 'カフェ', '手作り'].map( (word, i) => ( <button key={i} class="genre" onClick={() => this.setKeyword(`${word === '指定なし' ? '' : word}`) } > {word} </button> ) )} {this.data.length > 0 ? ( <table class="table"> {this.data.map((item, i) => ( <div onClick={() => this.$router.push(`${item.id}`)} class="list"> <tr> <th rowspan="3">{i + 1}</th> <td>{item.name}</td> </tr> <tr> <td>{item.genre.name}</td> </tr> <tr> <td>{item.genre.catch}</td> </tr> </div> ))} </table> ) : ( '見つからないよ〜' )} </div> ) } } </script> <style> .container { margin: 10px; } .title { font-weight: 300; font-size: 42px; color: #526488; word-spacing: 5px; padding-bottom: 15px; } .list { padding: 2px; } .links { padding-top: 15px; } .genre { display: inline; } </style>
これでこんな画面になってます。意外でてくる「手作り」キーワード。
/components/Detail.vue <script> import api from '@/api/api' import DetailView from '~/components/DetailView' export default { data() { return { shops: {}, error: false } }, async mounted() { const { data } = await api.getDetail(location.pathname.replace('/', '')) this.shops = await data.results.shop.map(e => e) }, render() { return <DetailView data={this.shops} /> } } </script>
/components/DetailView.vue <script> export default { props: { data: {} }, render() { const menu = this.data.length > 0 && this.data[0] console.log(menu) return ( <div class="container"> <div class="title">{menu.name}</div> <table> <tbody> <tr> <th>住所</th> <td>{menu.address}</td> </tr> <tr> <th>交通アクセス</th> <td>{menu.access}</td> </tr> <tr> <th>最寄り駅</th> <td>{menu.station_name}</td> </tr> <tr> <th>営業時間</th> <td>{menu.open}</td> </tr> <tr> <th>定休日</th> <td>{menu.close}</td> </tr> <tr> <th>平均予算</th> <td>{menu.budget && menu.budget.average}</td> </tr> <tr> <th>bikou</th> <td>{menu.budget_memo}</td> </tr> <tr> <th>席数</th> <td>{menu.capacity}</td> </tr> <tr> <th>メモ</th> <td>{menu.shop_detail_memo}</td> </tr> </tbody> </table> <div onClick={() => this.$router.back()}>もどる</div> </div> ) } } </script> <style> .container { margin: 10px; } .title { font-weight: 300; font-size: 42px; color: #526488; word-spacing: 5px; padding-bottom: 15px; } </style>
参考サイトさま一覧
bltblog 【Nuxt.jsで近くのお店を探すアプリを作成】#1 開発準備
Luftgarden Nuxt.js で簡単な画像一覧アプリを作成する – Part.1
描画関数とJSX
DMM Nuxt.jsとFirebaseでSPA×SSR×PWA×サーバーレスを実現する
UPDATE Nuxt.js プロジェクトを Heroku にデプロイして公開する方法
所感
かなり中途半端な出来でやっつけ感も漂ってる風になってしまってるけども、ルーティングやストアもちょっと触ったりはしたらけっこうシンプルに書けるようになってた(そのへんをブログ化するべきなのではとも少し思いながら)し、見た目にもやさしいのとっつきは良い感じでした。