Vue Nativeでネイティブアプリ開発入門

[株式会社diffeasy](https://diffeasy.com)の日本一魅力的なプログラマー集団を作るCTO[@_takeshi_24](https://twitter.com/_takeshi_24)です。

普段はNuxt.jsでフロントエンド開発したり、Ruby on Railsでサーバーサイド開発したり、GCPでサーバー構築したりやっています。
ネイティブアプリ開発はやっていないのですが、今回「[Vue Native](https://vue-native.io/)」を利用してネイティブアプリ開発に挑戦してみました！

## Vue Nativeとは？
<img width="302" alt="スクリーンショット 2018-12-08 11.57.40.png" src="https://qiita-image-store.s3.amazonaws.com/0/3003/a05c3eef-2be8-a8dd-af92-65795fd6acf5.png">
JavaScriptでクロスプラットフォームのネイティブアプリ開発ができるフレームワークです。
2018年6月に公開されました。
https://vue-native.io/

## Vue Native開発環境構築
公式サイトのドキュメントにしたがってインストールしていきます。

```console
yarn global add expo-cli
yarn global add vue-native-cli
vue-native init chat-app
```

プロジェクトが作成されるので、プロジェクトのディレクトリに移動して、DevToolsを起動します。

```console
cd chat-app/
yarn start
```

DevToolsがブラウザで立ち上がります。
<img width="1440" alt="スクリーンショット 2018-12-09 20.03.28.png" src="https://qiita-image-store.s3.amazonaws.com/0/3003/788ad28c-2deb-4d28-3500-bfc40265313f.png">

DevTools左側の「Run on iOS simulator」をクリックするか、コンソールで「i」キーをクリックすると、iOSシミュレーターが起動します。
※事前にXcodeのインストールが必要です。

これでVue Nativeのアプリが起動する・・・と思いきや、エラーが発生。

```console
 Unable to resolve "../../App" from "node_modules/expo/AppEntry.js"
```

![AppEntry.js_Error.png](https://qiita-image-store.s3.amazonaws.com/0/3003/b2b584af-51d4-5183-cc8e-20e55db29071.png)

### 対応1 ※これはダメでした。
調べたところ、rn-cli.config.jsonを以下のように修正すると良さそう・・・だけどダメでした。。

```js:/rn-cli.config.js
const { getDefaultConfig } = require("metro-config");

module.exports = (async () => {
  const {
    resolver: { sourceExts }
  } = await getDefaultConfig();
  return {
    transformer: {
      babelTransformerPath: require.resolve("./vueTransformerPlugin.js")
    },
    resolver: {
      sourceExts: [...sourceExts, "vue"]
    }
  };
})();
```
https://github.com/GeekyAnts/vue-native-core/issues/91#issuecomment-431216570

とりあえず改めて、起動します。
![スクリーンショット 2018-12-11 11.08.57.png](https://qiita-image-store.s3.amazonaws.com/0/3003/4cdb174e-d6fb-5cd3-af40-6bca693b2271.png)

一見正常に起動して「My Vue Native App」が表示されますが、styleが効いていません。。

### 対応2
https://github.com/GeekyAnts/vue-native-core/issues/87
こちらを見ていると、同じ状況。
![スクリーンショット 2018-12-11 11.46.02.png](https://qiita-image-store.s3.amazonaws.com/0/3003/d7e2de17-8432-9084-14ea-dddde63324d7.png)

[expo](https://expo.io/)の最新バージョンの問題のようですね。
![スクリーンショット 2018-12-11 11.47.03.png](https://qiita-image-store.s3.amazonaws.com/0/3003/6202420d-d5aa-0ccd-2a2d-08b1b93f65d4.png)

そこで、バージョンを戻します。
yarn.lockとnode_modulesを削除して、package.jsonとapp.jsonを以下のように書き換えます。

```json:/package.json
{
  "main": "node_modules/expo/AppEntry.js",
  "scripts": {
    "start": "expo start",
    "android": "expo start --android",
    "ios": "expo start --ios",
    "eject": "expo eject",
    "test": "node ./node_modules/jest/bin/jest.js --watchAll"
  },
  "jest": {
    "preset": "jest-expo"
  },
  "dependencies": {
    "expo": "^29.0.0",
    "react": "16.5.0",
    "react-native": "0.55.4",
    "vue-native-core": "0.0.8",
    "vue-native-helper": "0.0.9"
  },
  "devDependencies": {
    "babel-preset-expo": "^4.0.0",
    "vue-native-scripts": "0.0.14"
  },
  "private": true
}
```

```json:/app.json
{
  "expo": {
    "name": "chat-app",
    "slug": "chat-app",
    "privacy": "public",
    "sdkVersion": "29.0.0",
    "platforms": [
      "ios",
      "android"
    ],
    "version": "1.0.0",
    "orientation": "portrait",
    "icon": "./assets/images/icon.png",
    "splash": {
      "image": "./assets/images/splash.png",
      "resizeMode": "contain",
      "backgroundColor": "#ffffff"
    },
    "updates": {
      "fallbackToCacheTimeout": 0
    },
    "assetBundlePatterns": [
      "**/*"
    ],
    "ios": {
      "supportsTablet": true
    },
    "packagerOpts": {
      "config": "rn-cli.config.js"
    }
  }
}
```

rn-cli.config.jsも元に戻しておきます。

```js:/rn-cli.config.js
module.exports = {
  getTransformModulePath() {
    return require.resolve("./vueTransformerPlugin.js");
  },
  getSourceExts() {
    return ["vue"];
  }
};
```

モジュールを入れ直します。

```console
yarn install
```

これで改めて、起動します。
![スクリーンショット 2018-12-11 11.52.38.png](https://qiita-image-store.s3.amazonaws.com/0/3003/f96b489c-ff63-0f99-0bb5-d056023c0251.png)

今度こそ起動しました！

## Vue Nativeでアプリ開発
App.vueにコードを書いていきます。
今回はシンプルにテキスト入力された文字をリスト表示するコードです。

```vue:/App.vue
<template>
  <view class="container">
    <text-input
      :style="{height: 40, width: 200, borderColor: 'gray', borderWidth: 1}"
      v-model="messageText"
    />
    <button
      :on-press="saveMessage"
      title="Save"
    />
    <text v-for="(message, index) in messages" :key="index">{{ message }}</text>
  </view>
</template>
<script>
  export default {
    data: function() {
      return {
        messageText: '',
        messages: []
      }
    },
    methods: {
      saveMessage: function() {
        this.messages.push(this.messageText)
        this.messageText = ''
      }
    }
  }
</script>
<style>
.container {
  background-color: white;
  align-items: center;
  justify-content: center;
  flex: 1;
}
.text-color-primary {
  color: blue;
}
</style>
```

以下の通り動きます！
![vue-native.gif](https://qiita-image-store.s3.amazonaws.com/0/3003/5833e244-a7a8-eba1-7e32-6e75aa839940.gif)

### Vue NativeでVuexを利用
状態管理にVuexを利用します。

Vuexをインストールします。

```console
yarn add vuex --save 
```

Storeを作成します。

```/store/modules/messages.js
const state = {
  messages: []
}
const getters = {
  messages: state => {
    return state.messages
  }
}
// actions
const actions = {
  addMessage ({ commit }, message) {
    commit('addMessage', message)
  }
}
// mutations
const mutations = {
  addMessage (state, message) {
    state.messages.push(message)
  }
}
export default {
  state,
  getters,
  actions,
  mutations
}
```

```/store/index.js
import Vue from 'vue-native-core'
import Vuex from 'vuex'
import messages from './modules/messages'

Vue.use(Vuex)

export default new Vuex.Store({
  modules: {
    messages
  }
})
```

App.vueのscriptを以下のように修正します。

```vue:/App.vue
<script>
  import Store from './store';
  export default {
    data: function() {
      return {
        messageText: ''
      }
    },
    computed: {
      messages: function() {
        return Store.getters.messages
      }
    },
    methods: {
      saveMessage: function() {
        Store.dispatch('addMessage', this.messageText)
        this.messageText = ''
      }
    }
  }
</script>
```

### Vue Native Routerで画面遷移
ここまで、/App.vueに全て処理を書いてきましたが、実際のアプリでは画面の遷移も必要になると思います。
WebのVue.jsでは、Vue Routerを使いますが、Vue NativeにはVue Native Routerというモジュールがあるので、これを利用します。

Vue Native Routerをインストールします。

```console
yarn add vue-native-router --save 
```

App.vueの内容を、/screens/MessagesScreen.vueにコピーします。
ホーム画面用の画面として、/screens/HomeScreen.vueを作成します。

App.vueにはVue Native RouterのStackNavigatorのみ配置します。

```vue:/screens/HomeScreen.vue
<template>
  <view class="container">
    <text>{{ text }}</text>
    <button
      :on-press="goMessages"
      title="Go Messages"
    />
  </view>
</template>
<script>
  export default {
    data: function() {
      return {
        text: 'Home'
      }
    },
    props: {
      navigation: {
        type: Object
      }
    },
    methods: {
      goMessages: function() {
        this.navigation.navigate("Messages")
      }
    }
  }
</script>
<style>
.container {
  background-color: white;
  align-items: center;
  justify-content: center;
  flex: 1;
}
.text-color-primary {
  color: blue;
}
</style>
```

```vue:/App.vue
<template>
  <app-navigation></app-navigation>
</template>
<script>
import { StackNavigator } from "vue-native-router";
import HomeScreen from "./screens/HomeScreen.vue";
import MessagesScreen from "./screens/MessagesScreen.vue";
const AppNavigation = StackNavigator(
  {
    Home: HomeScreen,
    Messages: MessagesScreen,
  },
  {
    initialRouteName: 'Home',
  }
);
export default {
    components: { AppNavigation }
}
</script>
```

App.vueで、screensのコンポーネントを読み込んで、初期画面として、```initialRouteName: 'Home'```を指定しています。
HomeScreens.vueの```this.navigation.navigate("Messages")```で、MessagesScreenに遷移します。

![vue-native2.gif](https://qiita-image-store.s3.amazonaws.com/0/3003/6a147373-f113-ea2c-37db-714a70f45d5a.gif)

ソースはこちら。
https://github.com/tkc24/vue-native-sample