BEAR.Sunday + Vue.js で始めるシングルページアプリケーション

BEAR.Sunday の[マニュアル](https://bearsunday.github.io/manuals/1.0/ja/js-ui.html)では React をフロントエンドフレームワークとしてアプリケーションを構築する例が掲載されています。

ここでは Vue.js の [webpack-simple template](https://github.com/vuejs-templates/webpack-simple) のバックエンドを BEAR.Sunday で構築したシングルページアプリケーションを作ってみたいと思います。

* 動作環境
    * PHP 7.1

    * BEAR.Package v1.7.0
    * Vue.js v2.5.11

ソースコードは GitHub で公開しています。

https://github.com/kawanamiyuu/Kawanamiyuu.VuejsSpa

本エントリで紹介しきれていない詳細は完成版のソースコードと各[コミット](https://github.com/kawanamiyuu/Kawanamiyuu.VuejsSpa/commits/master)を参照してください :sweat_smile:

## webpack-simple template から Vue アプリの雛形を作成する

まずは、[webpack-simple template](https://github.com/vuejs-templates/webpack-simple) から Vue アプリの雛形を作成します。

```console
$ vue init webpack-simple advent-vue-project 

? Project name advent-vue-project
? Project description A Vue.js project
? Author 
? License MIT
? Use sass? No

   vue-cli · Generated "advent-vue-project".

   To get started:
   
     cd advent-vue-project
     npm install
     npm run dev

$ tree -a -L 1            
.
├── .babelrc
├── .editorconfig
├── .gitignore
├── README.md
├── index.html
├── package.json
├── src
└── webpack.config.js

$ npm install 
$ npm run dev

> advent-vue-project@1.0.0 dev /Users/Yuu/workspace/advent-calendar/advent-vue-project
> cross-env NODE_ENV=development webpack-dev-server --open --hot

Project is running at http://localhost:8080/
webpack output is served from /dist/
404s will fallback to /index.html
```

`npm run dev` を実行するとブラウザで自動的にアプリケーションが起動します。

![スクリーンショット 2017-12-17 22.50.22.png](https://qiita-image-store.s3.amazonaws.com/0/3899/b41ac52d-1c67-b92d-8156-b77c0b569870.png)

## BEAR.Skeleton から BEAR アプリの雛形を作成する

次に [BEAR.Skeleton](https://github.com/bearsunday/BEAR.Skeleton) からBEAR アプリの雛形を作成します。

また、アプリケーションのエントリ画面をHTMLとしてレンダリングするため、[madapaja/twig-module](https://github.com/madapaja/Madapaja.TwigModule) をインストールします。

```console
$ composer create-project -n bear/skeleton MyAdvent.MyProject
$ tree -a -L 1
.
├── .env
├── .gitignore
├── .php_cs.dist
├── README.md
├── autoload.php
├── bootstrap
├── composer.json
├── composer.lock
├── phpcs.xml
├── phpmd.xml
├── phpunit.xml.dist
├── public
├── src
├── tests
├── var
└── vendor

$ composer require madapaja/twig-module

 TwigModule をアプリケーションにインストールし、Twigテンプレートファイルを作成する
 以下のコミットを参照
 https://github.com/kawanamiyuu/Kawanamiyuu.VuejsSpa/commit/5ba0dc0518b9063bd47988a4b29470750cc41312

$ php -S 127.0.0.1:8080 -t public
```

![スクリーンショット 2017-12-17 22.43.17.png](https://qiita-image-store.s3.amazonaws.com/0/3899/8462f797-d05e-c18a-343b-21051acdde23.png)

## Vue アプリのクライアントコードを BEAR アプリに移植する

Vue アプリケーションの `src` ディレクトリを `src-ui` として BEAR アプリケーションに移植します。

```console
$ tree -a -L 1
.
├── .babelrc <--
├── .env
├── .gitignore
├── .php_cs.dist
├── README.md
├── autoload.php
├── bootstrap
├── composer.json
├── composer.lock
├── node_modules <--
├── package.json <--
├── phpcs.xml
├── phpmd.xml
├── phpunit.xml.dist
├── public
├── src
├── src-ui <--
├── tests
├── var
├── vendor
└── webpack.config.js <--
```

`webpack.config.js` を編集して Vue アプリケーションのビルド先を BEAR アプリケーションのエントリポイントである `public` 配下に変更します。

```diff
 module.exports = {
-  entry: './src/main.js',
+  entry: './src-ui/main.js',
   output: {
-    path: path.resolve(__dirname, './dist'),
+    path: path.resolve(__dirname, './public/dist'),
     publicPath: '/dist/',
     filename: 'build.js'
   },
```

## ページコンテンツを Ajax で表示する

Vue アプリのコンテンツ（「Essential Links」と「Ecosystem」のリンクURLとラベル）を Ajax でバックエンドから取得し表示します。

クライアントサイドのルーティングには [vue-router](https://github.com/vuejs/vue-router) を使用し、バックエンドとのAjax通信には [axios](https://github.com/axios/axios) を使用します。

BEAR アプリの特定の Page リソースで Json レンダリングを行うために AOP でレンダラーを `JsonRenderer` に差し替えます。

```php
// src/Module/JsonRendererInterceptor.php (新規)

use BEAR\Resource\JsonRenderer;
use BEAR\Resource\ResourceObject;
use Ray\Aop\MethodInterceptor;
use Ray\Aop\MethodInvocation;

class JsonRendererInterceptor implements MethodInterceptor
{
    public function invoke(MethodInvocation $invocation)
    {
        $ro = $invocation->getThis();
        /* @var $ro ResourceObject */
        $ro->setRenderer(new JsonRenderer);

        return $invocation->proceed();
    }
}
``` 

```php
// src/Module/AppModule.php (編集)

use Kawanamiyuu\VuejsSpa\Annotation\Api;
use Ray\Di\AbstractModule;

class AppModule extends AbstractModule
{
    protected function configure()
    {
        // ...

        $this->bindInterceptor(
            $this->matcher->annotatedWith(Api::class),
            $this->matcher->startsWith('on'),
            [JsonRendererInterceptor::class]
        );

        // ...
    }
}
```

```php
// src/Resource/Page/Links.php (新規)

use BEAR\Resource\ResourceObject;
use Kawanamiyuu\VuejsSpa\Annotation\Api;

/**
 * @Api
 */
class Links extends ResourceObject
{
    public function onGet()
    {
        $this['essential_links'] = [
            [
                'label' => 'Core Docs',
                'url' => 'https://vuejs.org'
            ],
            // ...
        ];

        $this['ecosystem_links'] = [
            [
                'label' =>'vue-router',
                'url' => 'http://router.vuejs.org/',
            ],
            // ...
        ];

        return $this;
    }
}
```

これで初期表示後、クライアントサイドでルーティングし([該当コミット](https://github.com/kawanamiyuu/Kawanamiyuu.VuejsSpa/commit/4e6057dcd90a91299f960aac517edc28f3b63b98))、かつ非同期でコンテンツを表示する([該当コミット](https://github.com/kawanamiyuu/Kawanamiyuu.VuejsSpa/commit/ba023c3d868422943de4ac7cd098c78f0afe4868))シングルページアプリケーションが完成しました。

![スクリーンショット 2017-12-17 23.27.44.png](https://qiita-image-store.s3.amazonaws.com/0/3899/317eb49c-027d-a93a-6d4c-5c2b4bafa10b.png)

↓↓↓（`Welcome to Your Vue.js App` をクリック）

![スクリーンショット 2017-12-17 23.27.59.png](https://qiita-image-store.s3.amazonaws.com/0/3899/e6c9c358-61a3-c8e9-f5a6-ebe631752bf8.png)