作ったアプリ
こんなアプリを@ionic/vueで作ってみました。
アプリの機能としては
- アプリっぽい動きを実装する
- スマホっぽい無限スクロールで、スクロールの下限に達したらAPIでfetchして、スクロールを伸ばす
- メモの作成
- メモの閲覧
を実装しました。
書くきっかけ
A Vue from Ionicという記事を見つけて、自分で試してみようと思ったのがきっかけです。
調べてみたらこんな記事を発見。beepというオープンソースのサンプルアプリがあったので、これを参考にして開発を進めることにしました。
利用したライブラリ
{
"name": "vue-ionic-sample",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"build-android": "npx cap sync android && npx cap copy android",
"build-ios": "npx cap sync && npx cap copy ios"
},
"dependencies": {
"@capacitor/cli": "^1.0.0-beta.11",
"@capacitor/core": "^1.0.0-beta.11",
"@capacitor/ios": "^1.0.0-beta.11",
"@ionic/core": "^4.0.0-beta.17",
"@modus/ionic-vue": "^1.1.1",
"axios": "^0.18.0",
"vue": "^2.5.17",
"vue-class-component": "^6.0.0",
"vue-property-decorator": "^7.0.0",
"vue-router": "^3.0.1",
"vue-router-layout": "^0.1.2",
"vuex": "^3.0.1",
"vuex-class": "^0.3.1",
"vuex-type-helper": "^1.2.0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^3.2.0",
"@vue/cli-plugin-typescript": "^3.2.0",
"@vue/cli-service": "^3.2.0",
"typescript": "3.0.3",
"vue-auto-routing": "^0.3.0",
"vue-cli-plugin-auto-routing": "^0.2.1",
"vue-template-compiler": "^2.5.17"
}
}
執筆時点(2018/12/8)では上記のライブラリを利用しています。
vue-cliの開始時点で
- Babel
- TypeScript
- Vue-Router
- Vuex
- Tslint
- vue-class-component
等々はあらかじめ入った状態で、開発をはじめたので独自で導入したプラグインは下記になります
"dependencies": {
+ "@capacitor/cli": "^1.0.0-beta.11",
+ "@capacitor/core": "^1.0.0-beta.11",
+ "@capacitor/ios": "^1.0.0-beta.11",
+ "@ionic/core": "^4.0.0-beta.17",
+ "@modus/ionic-vue": "^1.1.1",
+ "axios": "^0.18.0",
"vue": "^2.5.17",
"vue-class-component": "^6.0.0",
"vue-property-decorator": "^7.0.0",
"vue-router": "^3.0.1",
- "vuex": "^3.0.1"
+ "vue-router-layout": "^0.1.2",
+ "vuex": "^3.0.1",
+ "vuex-class": "^0.3.1",
+ "vuex-type-helper": "^1.2.0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^3.2.0",
"@vue/cli-plugin-typescript": "^3.2.0",
"@vue/cli-service": "^3.2.0",
- "typescript": "^3.0.0",
+ "typescript": "3.0.3",
+ "vue-auto-routing": "^0.3.0",
+ "vue-cli-plugin-auto-routing": "^0.2.1",
"vue-template-compiler": "^2.5.17"
}
}
typescriptのversionが下がってるのは、ライブラリ導入過程で問題が発生したので、そのワークアラウンドです。
開発途中にて導入したそれぞれのライブラリについて簡単に説明したいと思います。
@ionic/vue
- vueのtemplateにて、ionicの独自のタグを実装できるようになる
- Vue-Routerをラップした「IonicVueRouter」を利用する必要がある
Capacitor
- ハイブリッドアプリのビルドやネイティブのSDKの利用を簡単にできるようになる
- 今回作成したアプリでは、vueのビルド結果をxcodeのプロジェクトとしてビルドするために利用している
vue-cli-plugin-auto-routing
- vue-cliのroutingがNuxt.jsと同様になる
- ただ、今回はIonicVueRouterに関連した問題が発生したため、完全にNuxt.jsのroutingに同じになっていない
実装の解説
main.ts/index.html
import Vue from 'vue';
import { Ionic, IonicAPI } from '@modus/ionic-vue';
import router from './router';
import store from './stores/store';
Vue.config.productionTip = false;
Ionic.init();
Vue.use(IonicAPI);
new Vue({
router,
store,
}).$mount('#app');
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>vue-ionic-sample</title>
</head>
<body>
<noscript>
<strong>We're sorry but vue-ionic-sample doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<ion-app id="app">
<ion-vue-router/>
</ion-app>
<!-- built files will be auto injected -->
</body>
</html>
GettingStartedに載っているように、vueの起動処理を変更しました。
また、index.htmlにionincの独自タグを書くにあたり vue.config.js
に
runtimeCompiler: true
を追加しました。
router.ts
IonicVueRouterへの対応
import Vue from 'vue';
import { IonicVueRouter } from '@modus/ionic-vue';
import routes from 'vue-auto-routing';
Vue.use(IonicVueRouter);
export default new IonicVueRouter({
// @see node_modules/@modus/ionic-vue/src/router.ts
direction: 1,
viewCount: 0,
mode: 'history',
routes: routes.map((route) => {
return { ...route, path: '/' + route.path };
}),
});
GettingStartedに載っているように、routingを変更しました。
ただ、typescriptを利用していると、下記の direction
と directionOverride
の型定義が原因でコンパイルができないという問題が発生しました。
export default class Router extends _VueRouter {
direction: number;
directionOverride: number | null;
viewCount: number;
prevRouteStack: Route[];
history: any;
static installed: boolean;
static install: PluginFunction<never>;
constructor(args?: RouterArgs);
extendHistory(): void;
canGoBack(): boolean;
guessDirection(nextRoute: Route): number;
}
そこで、IonicVueRouterのコンストラクタを見たところ、下記のように引数に値がない場合の初期値が定義されていたので、その値をIonicVueRouterの初期化処理にて代入しています。
export default class Router extends _VueRouter {
direction: number;
directionOverride: number | null;
viewCount: number;
prevRouteStack: Route[];
history: any;
static installed: boolean;
static install: PluginFunction<never>;
constructor(args: RouterArgs = {} as RouterArgs) {
super(args);
// The direction user navigates in
this.direction = args.direction || 1;
// Override normal direction
this.directionOverride = null;
// Number of views navigated
this.viewCount = args.viewCount || 0;
vue-cli-plugin-auto-routingへの対応
vue-cli-plugin-auto-routingを導入した時点では、router.tsが下記のように更新されます。
import Vue from 'vue';
import Router from 'vue-router';
import routes from 'vue-auto-routing';
import { createRouterLayout } from 'vue-router-layout';
Vue.use(Router);
const RouterLayout = createRouterLayout(layout => {
return import('@/layouts/' + layout + '.vue');
});
export default new Router({
routes: [
{
path: '/',
component: RouterLayout,
children: routes
}
]
})
const RouterLayout = createRouterLayout(layout => {
return import('@/layouts/' + layout + '.vue');
});
export default new Router({
routes: [
{
path: '/',
component: RouterLayout,
children: routes
}
]
})
/
を対象とした親のroutingが一つあり、その親がlayoutをcomponentとして持ち、さらにchildrenとしてpages以下を自動生成したroutingが持つという感じです。
ただ、これだと下記の@ionic/vue側の実装と相性が悪いです。
<template>
<ion-router-outlet
ref="ionRouterOutlet"
@click="catchIonicGoBack">
<router-view
v-if="customTransition"
:name="name"/>
<transition
v-else
:css="bindCSS"
mode="in-out"
@before-enter="beforeEnter"
@enter="enter"
@after-enter="afterEnter"
@enter-cancelled="enterCancelled"
@before-leave="beforeLeave"
@leave="leave"
@after-leave="afterLeave"
@leave-cancelled="leaveCancelled"
>
<router-view :name="name"/>
</transition>
</ion-router-outlet>
</template>
なぜなら、親のroutingが /
を対象とした一つであるため、URLが変更された場合に
<router-view
v-if="customTransition"
:name="name"/>
の変更が検知されず、transitionの描画が実行されないためです。
そのため対応としては、/
を対象とした親のroutingを挟まずに直接にchildrenを router.ts
に配置するようにしました。
routes: routes.map((route) => {
return { ...route, path: '/' + route.path };
}),
こうすることで、 router-view
が変更され、transitionの描画が実行されます。
pages/about.vue
<template>
<ion-page class="ion-page">
<ion-header class="header">
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button default-href="/" />
</ion-buttons>
<ion-title>このアプリについて</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="content">
<h2>
利用してる技術
</h2>
<ion-list>
<ion-item>vue-cli</ion-item>
<ion-item>ionic</ion-item>
<ion-item>capacitor</ion-item>
</ion-list>
<h2>スマホっぽいスクロール</h2>
<ion-list>
<ion-item v-for="(item, index) in itemsForList" :key="index">
{{item}}
</ion-item>
</ion-list>
<ion-infinite-scroll @ionInfinite="doInfinite">
<ion-infinite-scroll-content></ion-infinite-scroll-content>
</ion-infinite-scroll>
</ion-content>
</ion-page>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import axios from 'axios';
@Component
export default class About extends Vue {
public items: string[] = [];
get itemsForList() {
return this.items;
}
async created() {
const posts = await axios.get('https://jsonplaceholder.typicode.com/posts');
this.items = posts.data.map((post: any) => {
return post.title;
});
}
async doInfinite(ionInfiniteEvent: any) {
const posts = await axios.get('https://jsonplaceholder.typicode.com/posts');
const body = posts.data.map((comment: any) => {
return comment.body;
});
setTimeout(() => {
this.items.push(...body);
ionInfiniteEvent.target.complete();
}, 500);
}
}
</script>
/about
のrouting先です。
でスクロールが下限に達したら ionInfinite
というeventがdispatchされるので、それをキャッチして doInfinite()
を実行します。
doInfinite()
では、 jsonplaceholderというAPIを叩いて、それを this.items
に追加しています。
また、Loadingっぽさを演出するため500ミリ秒待たせています。
pages/create/index.vue
<template>
<ion-page class="ion-page">
<ion-header class="header">
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button default-href="/" />
</ion-buttons>
<ion-title>メモを作る</ion-title>
<ion-buttons slot="end">
<ion-button :disabled="!isValidMemo" clear @click="createMemo">
<span v-if="requestPending">
<ion-spinner/>
</span>
<span v-else>Done</span>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content class="content">
<ion-textarea
class="textarea"
type="text"
:value="memo"
large
@input="memo = $event.target.value"
placeholder="メモを入力してね"
></ion-textarea>
</ion-content>
</ion-page>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { Action, Getter } from 'vuex-class';
@Component
export default class About extends Vue {
public memo: string = '';
public requestPending: boolean = false;
@Action('memo/add') addMemo: any;
@Getter('memo/memos') memos!: string[];
get isValidMemo() {
return this.memo !== '';
}
createMemo() {
this.requestPending = true;
setTimeout(() => {
this.addMemo(this.memo);
this.requestPending = false;
this.$router.push('/');
}, 500);
}
}
</script>
/create
のrouting先です。
右上のDONEボタンをクリックしたら createMemo()
が実行されます。
createMemo()
では this.memo
をVuexのstoreに保存しています。
また、Savingっぽさを演出するため500ミリ秒待たせています。
pages/read/index.vue
<template>
<ion-page class="ion-page">
<ion-header class="header">
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button default-href="/" />
</ion-buttons>
<ion-title>メモ一覧</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="content">
<ion-list>
<ion-item v-for="(memo, index) in memos" :key="index">
{{memo}}
</ion-item>
</ion-list>
</ion-content>
</ion-page>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { Getter } from 'vuex-class';
@Component
export default class About extends Vue {
@Getter('memo/memos') memos!: string[];
}
</script>
/read
のrouting先です。
VuexのStoreに保存した memos
を表示します。
アプリのビルド方法
下記のように capacitor.config.json
の webDir
に、ビルド結果が生成されるフォルダを指定します。
{
"appId": "com.example.app",
"appName": "vue-ionic-memo",
"bundledWebRuntime": false,
"webDir": "dist"
}
そしてプロダクションビルドを実行したあとに package.json
に書いた下記のスクリプトを実行します。
"build-ios": "npx cap sync && npx cap copy ios"
詳細についてはCapacitorの公式ドキュメント を御覧ください。
作ってみた感想
with Nuxt.js ???
開発当初は、自分の所属するオープンロジで利用しているNuxt.jsで@ionic/vueの導入を試みました。
ただVue-RouterをIonicVueRouterに置き換える際に、Nuxt.jsでは実現が困難だったため、vue-cliでやることにしました。
結果としては、世間でよく言われる「Nuxt.jsは規約/vue-cliはカスタマイズ」というメリット・デメリットを理解することができたりで、非常に勉強になりました。
@ionic/vue
ほぼvueだけでネイティブアプリっぽい動きを実現できるのは素晴らしいなと思います。
もし時間があったら、vue-nativeと比較して、双方のメリット・デメリット等を勉強してみたいと思います。